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 };