@dotdotgod/cli 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/init.mjs ADDED
@@ -0,0 +1,245 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+
4
+ function utcTimestamp() {
5
+ return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
6
+ }
7
+
8
+ function action(status, path, extra = {}) {
9
+ return { status, path, ...extra };
10
+ }
11
+
12
+ function formatAction(item) {
13
+ const extras = [];
14
+ if (item.backup) extras.push(`backup=${item.backup}`);
15
+ if (item.add) extras.push(`add=${item.add}`);
16
+ return `${item.status.padEnd(13)} ${item.path}${extras.length > 0 ? ` ${extras.join(' ')}` : ''}`;
17
+ }
18
+
19
+ function normalizeRoot(root) {
20
+ return resolve(root);
21
+ }
22
+
23
+ function parseInitOptions(argv) {
24
+ const options = { root: null, projectName: '', dotdotSetting: false, force: false, dryRun: false, json: false };
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const arg = argv[i];
27
+ if (arg === '--json') options.json = true;
28
+ else if (arg === '--dotdot-setting') options.dotdotSetting = true;
29
+ else if (arg === '--force') options.force = true;
30
+ else if (arg === '--dry-run') options.dryRun = true;
31
+ else if (arg === '--project-name') {
32
+ const value = argv[i + 1];
33
+ if (!value || value.startsWith('-')) throw new Error('--project-name requires a value');
34
+ options.projectName = value;
35
+ i += 1;
36
+ } else if (!arg.startsWith('-') && !options.root) {
37
+ options.root = arg;
38
+ } else if (!arg.startsWith('-')) {
39
+ throw new Error(`Unexpected argument: ${arg}`);
40
+ } else {
41
+ throw new Error(`Unknown option: ${arg}`);
42
+ }
43
+ }
44
+ if (!options.root) throw new Error('Missing required argument: <root>');
45
+ options.root = normalizeRoot(options.root);
46
+ if (!options.projectName) options.projectName = basename(options.root);
47
+ return options;
48
+ }
49
+
50
+ function initError(options, code, message) {
51
+ if (options?.json) {
52
+ console.log(JSON.stringify({ ok: false, command: 'init', root: options.root, error: { code, message } }, null, 2));
53
+ } else {
54
+ console.error(message);
55
+ }
56
+ process.exit(2);
57
+ }
58
+
59
+ function writeInitFile(options, relativePath, content, actions) {
60
+ const target = join(options.root, relativePath);
61
+ if (existsSync(target) && !options.force) {
62
+ actions.push(action('skipped', target));
63
+ return;
64
+ }
65
+
66
+ let backup = '';
67
+ if (existsSync(target) && options.force) {
68
+ backup = `${target}.bak.${utcTimestamp()}`;
69
+ if (!options.dryRun) renameSync(target, backup);
70
+ }
71
+
72
+ if (options.dryRun) {
73
+ actions.push(action(backup ? 'would_replace' : 'would_create', target, backup ? { backup } : {}));
74
+ return;
75
+ }
76
+
77
+ mkdirSync(dirname(target), { recursive: true });
78
+ writeFileSync(target, `${content}\n`);
79
+ actions.push(action(backup ? 'replaced' : 'created', target, backup ? { backup } : {}));
80
+ }
81
+
82
+ function ensureGitignoreEntry(options, entry, actions) {
83
+ const target = join(options.root, '.gitignore');
84
+ const existed = existsSync(target);
85
+ let current = existed ? readFileSync(target, 'utf8') : '';
86
+ if (current.split(/\r?\n/).includes(entry)) return;
87
+
88
+ if (options.dryRun) {
89
+ actions.push(action(existed ? 'would_update' : 'would_create', target, { add: entry }));
90
+ return;
91
+ }
92
+
93
+ mkdirSync(options.root, { recursive: true });
94
+ if (current.length > 0 && !current.endsWith('\n')) current = `${current}\n`;
95
+ writeFileSync(target, `${current}${entry}\n`);
96
+ actions.push(action(existed ? 'updated' : 'created', target, { add: entry }));
97
+ }
98
+
99
+ function agentContent(projectName, dotdotSetting) {
100
+ const dotdotAgentRule = dotdotSetting ? '\n- Follow the project code conventions in `docs/arch/CODE_CONVENTIONS.md`.' : '';
101
+ return `# AGENTS.md
102
+
103
+ Canonical instructions for AI coding agents working in this repository.
104
+
105
+ ## Project
106
+
107
+ - Name: ${projectName}
108
+ - Purpose: TODO: describe the product, service, or library.
109
+ - Primary stack: TODO: list runtime, framework, database, and package manager.
110
+
111
+ ## Working Rules
112
+
113
+ - Read existing code and docs before changing behavior.
114
+ - Keep changes scoped to the user's request.
115
+ - Preserve user edits and unrelated dirty worktree changes.
116
+ - Prefer existing local patterns over introducing new abstractions.
117
+ - Update docs when behavior, architecture, or test strategy changes.
118
+ - When using the dotdotgod CLI, run \`dotdotgod validate\` after docs changes and follow its traceability guidance for behavior specs.${dotdotAgentRule}
119
+
120
+ ## Commands
121
+
122
+ Document the project-specific commands here:
123
+
124
+ \`\`\`bash
125
+ # Install dependencies
126
+ TODO
127
+
128
+ # Run tests
129
+ TODO
130
+
131
+ # Run the app
132
+ TODO
133
+ \`\`\`
134
+
135
+ ## Documentation Map
136
+
137
+ - \`docs/spec/\`: product behavior, API contracts, user-facing requirements.
138
+ - \`docs/test/\`: test strategy, regression cases, manual verification notes.
139
+ - \`docs/arch/\`: architecture decisions, code conventions, module boundaries, data flow, infrastructure/runtime dependencies, integration boundaries, and migration design.
140
+ - \`docs/\`: all directories use kebab-case; all markdown file names use UPPER_SNAKE_CASE, including \`README.md\`.
141
+ - \`docs/\`: prefer keeping individual markdown files under the configured markdown validation budgets (default 200 lines and 10,000 characters); split larger docs into focused UPPER_SNAKE_CASE files and keep \`README.md\` as the index/overview unless a narrow size-check exception is configured.
142
+ - \`docs/\`: when adding, renaming, splitting, moving, or archiving docs, update the nearest relevant \`README.md\` index/table of contents in the same change.
143
+ - \`docs/\`: each docs subdirectory \`README.md\` acts as the local table of contents; list important files, task directories, status, and a one-line purpose for each entry.
144
+ - \`docs/\`: start small with a single focused markdown file; when one domain grows into multiple docs, promote it to \`docs/<area>/<domain>/README.md\` plus related UPPER_SNAKE_CASE files in that directory.
145
+ - \`docs/arch/\`: code conventions may start as \`CODE_CONVENTIONS.md\`; when they grow across multiple topics, use \`docs/arch/conventions/README.md\` as the index with supporting UPPER_SNAKE_CASE files.
146
+ - \`docs/plan/\`: local active implementation plans. Create one kebab-case directory per task (\`docs/plan/<task-slug>/\`), keep the task overview/index in that directory's \`README.md\`, and add supporting UPPER_SNAKE_CASE plan files alongside it. Ignored by git by default.
147
+ - \`docs/archive/\`: local completed plans, temporary reports, historical notes, payload captures. Move completed plan task directories to \`docs/archive/plan/<task-slug>/\`; put temporary reports and investigations under \`docs/archive/report/<report-slug>/\`. Ignored by git by default.
148
+
149
+ ## Agent-Specific Entrypoints
150
+
151
+ - \`CLAUDE.md\` imports this file with \`@AGENTS.md\`.
152
+ - \`CODEX.md\` points users to this file.
153
+
154
+ Keep long-lived instructions here so agent-specific files do not drift.`;
155
+ }
156
+
157
+ function docsReadmeContent() {
158
+ return `# Docs
159
+
160
+ This directory keeps project knowledge close to the code.
161
+
162
+ ## Naming
163
+
164
+ - All directories under \`docs/\` use kebab-case.
165
+ - All markdown file names under \`docs/\` use UPPER_SNAKE_CASE, including \`README.md\`.
166
+ - Prefer keeping individual markdown files under the configured markdown validation budgets (default 200 lines and 10,000 characters); split larger docs into focused UPPER_SNAKE_CASE files and keep \`README.md\` as the index/overview unless a narrow size-check exception is configured.
167
+
168
+ ## Indexing
169
+
170
+ - When adding, renaming, splitting, moving, or archiving docs, update the nearest relevant \`README.md\` index/table of contents in the same change.
171
+ - Each docs subdirectory \`README.md\` acts as the local table of contents; list important files, task directories, status, and a one-line purpose for each entry.
172
+ - Start small with a single focused markdown file; when one domain grows into multiple docs, promote it to \`docs/<area>/<domain>/README.md\` plus related UPPER_SNAKE_CASE files in that directory.
173
+
174
+ ## Map
175
+
176
+ - \`spec/\`: product behavior, API contracts, user-facing requirements.
177
+ - \`test/\`: test strategy, regression cases, manual verification notes.
178
+ - \`arch/\`: architecture decisions, code conventions, module boundaries, data flow, infrastructure/runtime dependencies, integration boundaries, and migration design.
179
+ - \`plan/\`: local active implementation plans. Create one kebab-case directory per task (\`plan/<task-slug>/\`), keep the task overview/index in that directory's \`README.md\`, and add supporting UPPER_SNAKE_CASE plan files alongside it. Ignored by git by default.
180
+ - \`archive/\`: local completed plans, temporary reports, historical notes, payload captures. Move completed plan task directories to \`archive/plan/<task-slug>/\`; put temporary reports and investigations under \`archive/report/<report-slug>/\`. Ignored by git by default.`;
181
+ }
182
+
183
+ function codeConventionsContent() {
184
+ return `# Code Conventions
185
+
186
+ Dotdot code conventions for keeping implementation simple and maintainable.
187
+
188
+ ## Abstraction Boundaries
189
+
190
+ - Do not introduce unnecessary abstractions.
191
+ - Do not abstract code that is not reused.
192
+ - If code grows beyond 150 lines, consider splitting or extracting focused units even when it is not reused.
193
+ - Review files approaching 250 lines for focused extraction by responsibility.
194
+ - Treat repeated \`dotdotgod graph impact\` results that collapse onto one large file as a design signal to split mixed responsibilities by behavior.
195
+ - Dotdotgod impact reveals hotspots but does not replace focused module boundaries.
196
+ - Prefer extracting pure helpers when behavior can be tested without runtime dependencies.
197
+ - Keep runtime integration explicit and local until a stable reuse pattern appears.
198
+ - Do not abstract reused code when the reused behavior is likely to split into separate features or flows later.
199
+ - Keep source files readable as plain text for humans and coding agents.`;
200
+ }
201
+
202
+ function initFiles(options) {
203
+ const archReadmeExtra = options.dotdotSetting ? '\n\n## Index\n\n- `CODE_CONVENTIONS.md`: dotdot code conventions, including abstraction boundaries and when to split long code. If conventions grow across multiple topics, promote them to `conventions/README.md` with supporting UPPER_SNAKE_CASE files.' : '';
204
+ const files = [
205
+ ['AGENTS.md', agentContent(options.projectName, options.dotdotSetting)],
206
+ ['CLAUDE.md', '# CLAUDE.md\n\n@AGENTS.md'],
207
+ ['CODEX.md', '# CODEX.md\n\nSee [AGENTS.md](./AGENTS.md).'],
208
+ ['docs/README.md', docsReadmeContent()],
209
+ ['docs/spec/README.md', '# Specs\n\nUse this area for behavior specs, API contracts, and product requirements.\n\nFor projects using the dotdotgod CLI, behavior specs may be required by `dotdotgod validate` to include fenced `json dotdotgod` traceability blocks as the final section. The CLI owns the schema and prints property-level repair guidance when validation fails.'],
210
+ ['docs/test/README.md', '# Tests\n\nUse this area for test strategy, coverage notes, regression cases, and manual verification records.'],
211
+ ['docs/arch/README.md', `# Architecture\n\nUse this area for architecture decisions, code conventions, module boundaries, data flow notes, infrastructure/runtime dependencies, integration boundaries, and migration design.${archReadmeExtra}`],
212
+ ['docs/plan/README.md', '# Plans\n\nUse this area for active implementation plans.\n\n## Naming\n\n- Task directories use kebab-case: `docs/plan/<task-slug>/`.\n- Markdown file names use UPPER_SNAKE_CASE: `README.md`, `RESEARCH_NOTES.md`, `VERIFICATION.md`.\n\n## Structure\n\n- Create one directory per task: `docs/plan/<task-slug>/`.\n- Put the task overview, index, scope, status, and main plan in `docs/plan/<task-slug>/README.md`.\n- Add supporting research, checklists, payload captures, or verification notes as additional UPPER_SNAKE_CASE markdown files in the same task directory.\n- Move completed or superseded task directories to `docs/archive/plan/<task-slug>/`.\n\nThis directory is local-only and ignored by git by default.'],
213
+ ['docs/archive/README.md', '# Archive\n\nUse this area for local completed plans, temporary reports, historical notes, payload captures, and investigation notes.\n\n## Naming\n\n- Archived plan task directories preserve their kebab-case task slug.\n- Archived report directories use a focused kebab-case report slug.\n- Markdown file names use UPPER_SNAKE_CASE, including `README.md`.\n\n## Structure\n\n- Move completed plan task directories from `docs/plan/<task-slug>/` to `docs/archive/plan/<task-slug>/`.\n- Put temporary investigations, reports, payload captures, and historical notes under `docs/archive/report/<report-slug>/`.\n- Preserve each archive directory\'s `README.md` overview/index and supporting UPPER_SNAKE_CASE markdown files.\n- Additional archive categories can be added later as focused kebab-case subdirectories when needed.\n\nThis directory is local-only and ignored by git by default.'],
214
+ ];
215
+ if (options.dotdotSetting) files.push(['docs/arch/CODE_CONVENTIONS.md', codeConventionsContent()]);
216
+ return files;
217
+ }
218
+
219
+ export function runInit(argv, usage) {
220
+ let options;
221
+ try {
222
+ options = parseInitOptions(argv);
223
+ } catch (error) {
224
+ usage(error instanceof Error ? error.message : String(error), 'init');
225
+ return;
226
+ }
227
+
228
+ if (!options.dryRun && existsSync(options.root)) {
229
+ try {
230
+ if (!statSync(options.root).isDirectory()) initError(options, 'ROOT_NOT_DIRECTORY', `Project root is not a directory: ${options.root}`);
231
+ } catch {
232
+ initError(options, 'ROOT_NOT_FOUND', `Project root not found: ${options.root}`);
233
+ }
234
+ }
235
+
236
+ if (!options.dryRun) mkdirSync(options.root, { recursive: true });
237
+
238
+ const actions = [];
239
+ for (const [relativePath, content] of initFiles(options)) writeInitFile(options, relativePath, content, actions);
240
+ for (const entry of ['docs/plan', 'docs/archive', '.dotdotgod']) ensureGitignoreEntry(options, entry, actions);
241
+
242
+ const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun, force: options.force, dotdotSetting: options.dotdotSetting, actions };
243
+ if (options.json) console.log(JSON.stringify(payload, null, 2));
244
+ else for (const item of actions) console.log(formatAction(item));
245
+ }