Spaces:
Runtime error
Runtime error
File size: 10,952 Bytes
a6b96c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | "use strict";
/**
* Docs β Commands for the docs-update workflow
*
* Provides `cmdDocsInit` which returns project signals, existing doc inventory
* with GSD marker detection, doc tooling detection, monorepo awareness, and
* model resolution. Used by Phase 2 to route doc generation appropriately.
*
* ADR-457 build-at-publish: the hand-written bin/lib/docs.cjs collapsed
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
* from the prior hand-written .cjs; only strict types are added.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
// eslint-disable-next-line @typescript-eslint/no-require-imports
const io = require("./io.cjs");
const { output } = io;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const configLoader = require("./config-loader.cjs");
const { loadConfig } = configLoader;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const modelResolver = require("./model-resolver.cjs");
const { resolveModelInternal } = modelResolver;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const coreUtils = require("./core-utils.cjs");
const { pathExistsInternal, toPosixPath } = coreUtils;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const agentInstallCheck = require("./agent-install-check.cjs");
const { checkAgentsInstalled } = agentInstallCheck;
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
// βββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const GSD_MARKER = '<!-- generated-by: gsd-doc-writer -->';
const SKIP_DIRS = new Set([
'node_modules', '.git', '.planning', '.claude', '__pycache__',
'target', 'dist', 'build', '.next', '.nuxt', 'coverage',
'.vscode', '.idea',
]);
// βββ Private helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Check whether a file begins with the GSD doc writer marker.
* Reads the first 500 bytes only β avoids loading large files.
*/
function hasGsdMarker(filePath) {
try {
const buf = Buffer.alloc(500);
const fd = node_fs_1.default.openSync(filePath, 'r');
const bytesRead = node_fs_1.default.readSync(fd, buf, 0, 500, 0);
node_fs_1.default.closeSync(fd);
return buf.slice(0, bytesRead).toString('utf-8').includes(GSD_MARKER);
}
catch {
return false;
}
}
/**
* Recursively scan the project root (immediate .md files) and docs/ directory
* (up to 4 levels deep) for Markdown files, excluding dirs in SKIP_DIRS.
*/
function scanExistingDocs(cwd) {
const MAX_DEPTH = 4;
const results = [];
function walkDir(dir, depth) {
if (depth > MAX_DEPTH)
return;
try {
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name))
continue;
const abs = node_path_1.default.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(abs, depth + 1);
}
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const rel = toPosixPath(node_path_1.default.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
}
}
}
catch { /* directory may not exist β best-effort */ }
}
// Scan root-level .md files (non-recursive)
try {
const entries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const abs = node_path_1.default.join(cwd, entry.name);
const rel = toPosixPath(node_path_1.default.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
}
}
}
catch { /* best-effort */ }
// Recursively scan docs/ directory
const docsDir = node_path_1.default.join(cwd, 'docs');
walkDir(docsDir, 1);
// Fallback: if docs/ does not exist, try documentation/ or doc/
try {
node_fs_1.default.statSync(docsDir);
}
catch {
const alternatives = ['documentation', 'doc'];
for (const alt of alternatives) {
const altDir = node_path_1.default.join(cwd, alt);
try {
const stat = node_fs_1.default.statSync(altDir);
if (stat.isDirectory()) {
walkDir(altDir, 1);
break;
}
}
catch { /* not present */ }
}
}
return results.sort((a, b) => a.path.localeCompare(b.path));
}
/**
* Detect project type signals from the filesystem and package.json.
* All checks are best-effort and never throw.
*/
function detectProjectType(cwd) {
const exists = (rel) => {
try {
return pathExistsInternal(cwd, rel);
}
catch {
return false;
}
};
// Read package.json once β used by has_cli_bin, is_monorepo, has_tests checks.
const pkgRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, 'package.json'));
let pkg = null;
if (pkgRaw) {
try {
pkg = JSON.parse(pkgRaw);
}
catch { /* invalid JSON */ }
}
// has_cli_bin: package.json has a `bin` field
const binField = pkg?.['bin'];
const has_cli_bin = !!(binField && (typeof binField === 'string' ||
(typeof binField === 'object' && Object.keys(binField).length > 0)));
// is_monorepo: pnpm-workspace.yaml, lerna.json, or package.json workspaces
let is_monorepo = exists('pnpm-workspace.yaml') || exists('lerna.json');
if (!is_monorepo && pkg) {
is_monorepo = Array.isArray(pkg['workspaces']) && pkg['workspaces'].length > 0;
}
// has_tests: common test directories or test frameworks in devDependencies
let has_tests = exists('test') || exists('tests') || exists('__tests__') || exists('spec');
if (!has_tests && pkg) {
const devDeps = Object.keys(pkg['devDependencies'] || {});
has_tests = devDeps.some(d => ['vitest', 'jest', 'mocha', 'jasmine', 'ava'].includes(d));
}
// has_deploy_config: various deployment config files
const deployFiles = [
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
'fly.toml', 'render.yaml', 'vercel.json', 'netlify.toml', 'railway.json',
'.github/workflows/deploy.yml', '.github/workflows/deploy.yaml',
];
const has_deploy_config = deployFiles.some(f => exists(f));
return {
has_package_json: exists('package.json'),
has_api_routes: (exists('src/app/api') || exists('routes') || exists('src/routes') ||
exists('api') || exists('server')),
has_cli_bin,
is_open_source: exists('LICENSE') || exists('LICENSE.md'),
has_deploy_config,
is_monorepo,
has_tests,
};
}
/**
* Detect known documentation tooling in the project.
*/
function detectDocTooling(cwd) {
const exists = (rel) => {
try {
return pathExistsInternal(cwd, rel);
}
catch {
return false;
}
};
return {
docusaurus: exists('docusaurus.config.js') || exists('docusaurus.config.ts'),
vitepress: (exists('.vitepress/config.js') ||
exists('.vitepress/config.ts') ||
exists('.vitepress/config.mts')),
mkdocs: exists('mkdocs.yml'),
storybook: exists('.storybook'),
};
}
/**
* Extract monorepo workspace globs from pnpm-workspace.yaml, package.json
* workspaces, or lerna.json.
*/
function detectMonorepoWorkspaces(cwd) {
// pnpm-workspace.yaml
const pnpmRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, 'pnpm-workspace.yaml'));
if (pnpmRaw) {
const workspaces = [];
for (const line of pnpmRaw.split('\n')) {
const m = line.match(/^\s*-\s+['"]?(.+?)['"]?\s*$/);
if (m)
workspaces.push(m[1].trim());
}
if (workspaces.length > 0)
return workspaces;
}
// package.json workspaces
const pkgRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, 'package.json'));
if (pkgRaw) {
try {
const pkg = JSON.parse(pkgRaw);
if (Array.isArray(pkg['workspaces']) && pkg['workspaces'].length > 0) {
return pkg['workspaces'];
}
}
catch { /* invalid JSON */ }
}
// lerna.json
const lernaRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(cwd, 'lerna.json'));
if (lernaRaw) {
try {
const lerna = JSON.parse(lernaRaw);
if (Array.isArray(lerna['packages']) && lerna['packages'].length > 0) {
return lerna['packages'];
}
}
catch { /* invalid JSON */ }
}
return [];
}
// βββ Public commands ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Return JSON context for the docs-update workflow: project signals, existing
* doc inventory, doc tooling detection, monorepo workspaces, and model
* resolution. Follows the cmdInitMapCodebase pattern.
*
* @example
* node gsd-tools.cjs docs-init --raw
*/
function cmdDocsInit(cwd, raw) {
const config = loadConfig(cwd);
const result = {
doc_writer_model: resolveModelInternal(cwd, 'gsd-doc-writer'),
commit_docs: config.commit_docs,
existing_docs: scanExistingDocs(cwd),
project_type: detectProjectType(cwd),
doc_tooling: detectDocTooling(cwd),
monorepo_workspaces: detectMonorepoWorkspaces(cwd),
planning_exists: pathExistsInternal(cwd, '.planning'),
};
// Inject project_root and agent installation status (mirrors withProjectRoot in init.cjs)
result['project_root'] = cwd;
const agentStatus = checkAgentsInstalled();
result['agents_installed'] = agentStatus.agents_installed;
result['missing_agents'] = agentStatus.missing_agents;
output(result, raw, undefined);
}
module.exports = { cmdDocsInit };
|