dflow-sdd-ddd 0.1.0 → 0.2.0
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/CHANGELOG.md +1176 -0
- package/CONTRIBUTING.md +123 -0
- package/README.md +199 -157
- package/TEMPLATE-COVERAGE.md +46 -0
- package/TEMPLATE-LANGUAGE-GLOSSARY.md +52 -0
- package/bin/dflow.js +73 -8
- package/docs/evaluating-dflow.md +226 -0
- package/docs/migrating-to-dflow-v1.md +212 -0
- package/docs/npm-publish-checklist.md +93 -0
- package/docs/release-versioning-policy.md +99 -0
- package/docs/using-with-claude-code.md +207 -0
- package/docs/using-with-codex.md +244 -0
- package/docs/why-ddd-for-ai.md +35 -0
- package/lib/init.js +444 -66
- package/package.json +13 -7
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +92 -0
- package/templates/{webforms → brownfield}/scaffolding/CLAUDE-md-snippet.md +8 -9
- package/templates/{webforms → brownfield}/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/{webforms → brownfield}/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/{webforms → brownfield}/scaffolding/_conventions.md +2 -1
- package/templates/{webforms → brownfield}/scaffolding/_overview.md +2 -2
- package/templates/{webforms → brownfield}/templates/context-map.md +1 -1
- package/templates/{webforms → brownfield}/templates/glossary.md +1 -1
- package/templates/{webforms → brownfield}/templates/models.md +1 -1
- package/templates/{webforms → brownfield}/templates/phase-spec.md +1 -1
- package/templates/{webforms → brownfield}/templates/rules.md +1 -1
- package/templates/{webforms → brownfield}/templates/tech-debt.md +1 -1
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +92 -0
- package/templates/{core → greenfield}/scaffolding/CLAUDE-md-snippet.md +15 -14
- package/templates/{core → greenfield}/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/{core → greenfield}/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/{core → greenfield}/scaffolding/_conventions.md +2 -1
- package/templates/{core → greenfield}/scaffolding/_overview.md +2 -2
- package/templates/{core → greenfield}/scaffolding/architecture-decisions-README.md +1 -1
- package/templates/{core → greenfield}/templates/context-map.md +1 -1
- package/templates/{core → greenfield}/templates/events.md +1 -1
- package/templates/{core → greenfield}/templates/glossary.md +1 -1
- package/templates/{core → greenfield}/templates/models.md +1 -1
- package/templates/{core → greenfield}/templates/phase-spec.md +1 -1
- package/templates/{core → greenfield}/templates/rules.md +1 -1
- package/templates/{core → greenfield}/templates/tech-debt.md +1 -1
- /package/templates/{webforms → brownfield}/templates/CLAUDE.md +0 -0
- /package/templates/{webforms → brownfield}/templates/_index.md +0 -0
- /package/templates/{webforms → brownfield}/templates/behavior.md +0 -0
- /package/templates/{webforms → brownfield}/templates/context-definition.md +0 -0
- /package/templates/{webforms → brownfield}/templates/lightweight-spec.md +0 -0
- /package/templates/{core → greenfield}/templates/CLAUDE.md +0 -0
- /package/templates/{core → greenfield}/templates/_index.md +0 -0
- /package/templates/{core → greenfield}/templates/aggregate-design.md +0 -0
- /package/templates/{core → greenfield}/templates/behavior.md +0 -0
- /package/templates/{core → greenfield}/templates/context-definition.md +0 -0
- /package/templates/{core → greenfield}/templates/lightweight-spec.md +0 -0
package/lib/init.js
CHANGED
|
@@ -3,6 +3,8 @@ const path = require('node:path');
|
|
|
3
3
|
const readline = require('node:readline');
|
|
4
4
|
const { TextDecoder } = require('node:util');
|
|
5
5
|
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
6
8
|
const MIN_NODE_VERSION = '22.0.0';
|
|
7
9
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
8
10
|
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
|
|
@@ -37,19 +39,6 @@ const PROJECT_TYPE_OPTIONS = [
|
|
|
37
39
|
}
|
|
38
40
|
];
|
|
39
41
|
|
|
40
|
-
const EDITION_OPTIONS = [
|
|
41
|
-
{
|
|
42
|
-
key: 'core',
|
|
43
|
-
label: 'ASP.NET Core - Clean Architecture + DDD',
|
|
44
|
-
aliases: ['core', 'asp.net core', 'aspnet core']
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
key: 'webforms',
|
|
48
|
-
label: 'ASP.NET WebForms - progressive domain extraction',
|
|
49
|
-
aliases: ['webforms', 'asp.net webforms', 'aspnet webforms']
|
|
50
|
-
}
|
|
51
|
-
];
|
|
52
|
-
|
|
53
42
|
const PROSE_LANGUAGE_OPTIONS = [
|
|
54
43
|
{
|
|
55
44
|
key: 'zh-TW',
|
|
@@ -88,11 +77,29 @@ const OPTIONAL_FILE_OPTIONS = [
|
|
|
88
77
|
key: 'git-flow',
|
|
89
78
|
label: 'Git principles - Git Flow',
|
|
90
79
|
aliases: ['git principles - git flow', 'git flow', 'gitflow']
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const AI_AGENT_OPTIONS = [
|
|
84
|
+
{
|
|
85
|
+
key: 'agents',
|
|
86
|
+
label: 'AGENTS.md - Codex / Copilot coding agent',
|
|
87
|
+
aliases: ['agents', 'agents.md', 'codex', 'copilot agent', 'copilot coding agent']
|
|
91
88
|
},
|
|
92
89
|
{
|
|
93
90
|
key: 'claude',
|
|
94
|
-
label: 'CLAUDE.md
|
|
91
|
+
label: 'CLAUDE.md - Claude Code',
|
|
95
92
|
aliases: ['claude', 'claude.md']
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'gemini',
|
|
96
|
+
label: 'GEMINI.md - Gemini CLI',
|
|
97
|
+
aliases: ['gemini', 'gemini.md']
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: 'copilot',
|
|
101
|
+
label: '.github/copilot-instructions.md - GitHub Copilot',
|
|
102
|
+
aliases: ['copilot', 'github copilot', 'copilot-instructions', 'copilot-instructions.md']
|
|
96
103
|
}
|
|
97
104
|
];
|
|
98
105
|
|
|
@@ -211,6 +218,95 @@ async function runInit(options = {}) {
|
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
async function runConfigureAgents(options = {}) {
|
|
222
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
223
|
+
const stdin = options.stdin || process.stdin;
|
|
224
|
+
const stdout = options.stdout || process.stdout;
|
|
225
|
+
const stderr = options.stderr || process.stderr;
|
|
226
|
+
|
|
227
|
+
let rl;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
rl = readline.createInterface({
|
|
231
|
+
input: stdin,
|
|
232
|
+
output: stdout,
|
|
233
|
+
terminal: Boolean(stdin.isTTY && stdout.isTTY)
|
|
234
|
+
});
|
|
235
|
+
rl._dflowOutput = stdout;
|
|
236
|
+
getLinePrompter(rl);
|
|
237
|
+
|
|
238
|
+
if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
|
|
239
|
+
throw new InitError(`Dflow configure-agents requires Node.js ${MIN_NODE_VERSION}+.`, 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await assertWritableProjectRoot(cwd);
|
|
243
|
+
await assertDflowInitialized(cwd);
|
|
244
|
+
|
|
245
|
+
const projectContext = await inferProjectContext(cwd, rl, stdout, stderr);
|
|
246
|
+
const aiAgents = await askAiAgents(rl, stdout, stderr);
|
|
247
|
+
|
|
248
|
+
if (aiAgents.length === 0) {
|
|
249
|
+
throw new UserAbort('No AI agents selected. Nothing changed.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const plan = await buildConfigureAgentsPlan(cwd, {
|
|
253
|
+
...projectContext,
|
|
254
|
+
aiAgents
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
renderPreview(stdout, plan, []);
|
|
258
|
+
const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
|
|
259
|
+
|
|
260
|
+
if (!confirmed) {
|
|
261
|
+
throw new UserAbort();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
rl.close();
|
|
265
|
+
rl = undefined;
|
|
266
|
+
|
|
267
|
+
const result = await writeFilePlan(cwd, plan);
|
|
268
|
+
result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
|
|
269
|
+
|
|
270
|
+
printResultReport(stdout, result, plan.deferred);
|
|
271
|
+
printConfigureAgentsNextSteps(stdout);
|
|
272
|
+
return 0;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (rl) {
|
|
275
|
+
rl.close();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (error instanceof UserAbort) {
|
|
279
|
+
stdout.write(`${error.message}\n`);
|
|
280
|
+
return error.exitCode;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (error instanceof WritePhaseError) {
|
|
284
|
+
stderr.write(`${error.message}\n`);
|
|
285
|
+
stderr.write('Files already created were kept; clean up partial output manually if needed.\n');
|
|
286
|
+
if (error.result) {
|
|
287
|
+
printResultReport(stdout, error.result, []);
|
|
288
|
+
}
|
|
289
|
+
return error.exitCode;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (error instanceof InitError) {
|
|
293
|
+
stderr.write(`${error.message}\n`);
|
|
294
|
+
return error.exitCode;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
stderr.write(`${error && error.message ? error.message : error}\n`);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function assertDflowInitialized(cwd) {
|
|
303
|
+
const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
|
|
304
|
+
|
|
305
|
+
if (!(await pathExists(dflowSpecsPath)) || !(await containsInitializedContent(dflowSpecsPath))) {
|
|
306
|
+
throw new InitError('Dflow is not initialized in this project. Run `dflow init` first.');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
214
310
|
async function runPreflight(cwd) {
|
|
215
311
|
const warnings = [];
|
|
216
312
|
const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
|
|
@@ -226,7 +322,7 @@ async function runPreflight(cwd) {
|
|
|
226
322
|
const legacySpecsPath = path.join(cwd, 'specs');
|
|
227
323
|
if ((await pathExists(legacySpecsPath)) && (await containsInitializedContent(legacySpecsPath))) {
|
|
228
324
|
warnings.push(
|
|
229
|
-
'Detected legacy specs/. Dflow V1 will not migrate or modify it; new files will be created under dflow/specs/.'
|
|
325
|
+
'Detected legacy specs/. Dflow V1 will not migrate or modify it; new files will be created under dflow/specs/. See docs/migrating-to-dflow-v1.md for the manual migration checklist.'
|
|
230
326
|
);
|
|
231
327
|
}
|
|
232
328
|
|
|
@@ -309,16 +405,16 @@ async function detectProjectSignals(cwd) {
|
|
|
309
405
|
}
|
|
310
406
|
}
|
|
311
407
|
|
|
312
|
-
let
|
|
408
|
+
let trackHint = null;
|
|
313
409
|
if (coreSignal && !webFormsSignal) {
|
|
314
|
-
|
|
410
|
+
trackHint = 'greenfield';
|
|
315
411
|
} else if (webFormsSignal && !coreSignal) {
|
|
316
|
-
|
|
412
|
+
trackHint = 'brownfield';
|
|
317
413
|
}
|
|
318
414
|
|
|
319
415
|
return {
|
|
320
416
|
hasSourceTree: hasSourceTree || relNames.has('src'),
|
|
321
|
-
|
|
417
|
+
trackHint
|
|
322
418
|
};
|
|
323
419
|
}
|
|
324
420
|
|
|
@@ -369,9 +465,9 @@ function buildDetectionWarnings(answers, detection) {
|
|
|
369
465
|
warnings.push('Warning: source-tree signals already exist, but project type is Greenfield. Continuing with your selected project type.');
|
|
370
466
|
}
|
|
371
467
|
|
|
372
|
-
if (detection.
|
|
468
|
+
if (detection.trackHint && answers.projectType !== detection.trackHint) {
|
|
373
469
|
warnings.push(
|
|
374
|
-
`Warning: project signals look like ${
|
|
470
|
+
`Warning: project signals look like ${formatTrack(detection.trackHint)}, but selected track is ${formatTrack(answers.projectType)}. Continuing with your selected track.`
|
|
375
471
|
);
|
|
376
472
|
}
|
|
377
473
|
|
|
@@ -387,15 +483,8 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
387
483
|
defaultKey: projectTypeDefault
|
|
388
484
|
});
|
|
389
485
|
|
|
390
|
-
const edition = await askSelect(rl, stdout, stderr, {
|
|
391
|
-
id: 'Q2',
|
|
392
|
-
question: 'Which Dflow edition should initialize this project?',
|
|
393
|
-
options: EDITION_OPTIONS,
|
|
394
|
-
defaultKey: detection.editionHint
|
|
395
|
-
});
|
|
396
|
-
|
|
397
486
|
const techStackSummary = await askText(rl, stderr, {
|
|
398
|
-
id: '
|
|
487
|
+
id: 'Q2',
|
|
399
488
|
question: 'Confirm the main tech stack details for placeholders.',
|
|
400
489
|
required: true,
|
|
401
490
|
maxLength: 1000,
|
|
@@ -403,7 +492,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
403
492
|
});
|
|
404
493
|
|
|
405
494
|
const migrationContext = await askText(rl, stderr, {
|
|
406
|
-
id: '
|
|
495
|
+
id: 'Q3',
|
|
407
496
|
question: 'Is there migration or legacy context Dflow should note?',
|
|
408
497
|
required: false,
|
|
409
498
|
maxLength: 1000,
|
|
@@ -411,7 +500,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
411
500
|
});
|
|
412
501
|
|
|
413
502
|
const proseLanguageSelection = await askSelect(rl, stdout, stderr, {
|
|
414
|
-
id: '
|
|
503
|
+
id: 'Q4',
|
|
415
504
|
question: 'Project prose language for generated spec content?',
|
|
416
505
|
options: PROSE_LANGUAGE_OPTIONS,
|
|
417
506
|
defaultKey: null
|
|
@@ -423,17 +512,76 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
423
512
|
}
|
|
424
513
|
|
|
425
514
|
const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
|
|
515
|
+
const aiAgents = await askAiAgents(rl, stdout, stderr);
|
|
426
516
|
|
|
427
517
|
return {
|
|
428
518
|
projectType,
|
|
429
|
-
edition,
|
|
519
|
+
edition: projectType,
|
|
430
520
|
techStackSummary,
|
|
431
521
|
migrationContext,
|
|
432
522
|
proseLanguage,
|
|
433
|
-
optionalFiles
|
|
523
|
+
optionalFiles,
|
|
524
|
+
aiAgents
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function inferProjectContext(cwd, rl, stdout, stderr) {
|
|
529
|
+
let edition = await inferExistingEdition(cwd);
|
|
530
|
+
|
|
531
|
+
if (!edition) {
|
|
532
|
+
stderr.write('Could not infer the Dflow track from dflow/specs/. Please choose it explicitly.\n');
|
|
533
|
+
edition = await askSelect(rl, stdout, stderr, {
|
|
534
|
+
id: 'track',
|
|
535
|
+
question: 'Which Dflow track is this project using?',
|
|
536
|
+
options: PROJECT_TYPE_OPTIONS,
|
|
537
|
+
defaultKey: null
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
projectType: edition,
|
|
543
|
+
edition,
|
|
544
|
+
techStackSummary: await inferTechStackSummary(cwd),
|
|
545
|
+
migrationContext: await inferMigrationContext(cwd),
|
|
546
|
+
proseLanguage: await inferProseLanguage(cwd),
|
|
547
|
+
optionalFiles: []
|
|
434
548
|
};
|
|
435
549
|
}
|
|
436
550
|
|
|
551
|
+
async function inferExistingEdition(cwd) {
|
|
552
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/architecture/tech-debt.md'))) {
|
|
553
|
+
return 'greenfield';
|
|
554
|
+
}
|
|
555
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/migration/tech-debt.md'))) {
|
|
556
|
+
return 'brownfield';
|
|
557
|
+
}
|
|
558
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/domain/context-map.md'))) {
|
|
559
|
+
return 'greenfield';
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function inferProseLanguage(cwd) {
|
|
565
|
+
const conventionsPath = path.join(cwd, 'dflow/specs/shared/_conventions.md');
|
|
566
|
+
const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
|
|
567
|
+
const match = content.match(/Project prose language:\s*`([^`]+)`/);
|
|
568
|
+
return match ? match[1] : 'unknown';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function inferTechStackSummary(cwd) {
|
|
572
|
+
const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
|
|
573
|
+
const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
|
|
574
|
+
const match = content.match(/\|\s*Tech stack\s*\|\s*([^|\n]+?)\s*\|/i);
|
|
575
|
+
return match ? match[1].trim() : 'unknown';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function inferMigrationContext(cwd) {
|
|
579
|
+
const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
|
|
580
|
+
const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
|
|
581
|
+
const match = content.match(/\|\s*Migration \/ legacy context\s*\|\s*([^|\n]+?)\s*\|/i);
|
|
582
|
+
return match ? match[1].trim() : 'none';
|
|
583
|
+
}
|
|
584
|
+
|
|
437
585
|
async function askSelect(rl, stdout, stderr, config) {
|
|
438
586
|
let failedAttempts = 0;
|
|
439
587
|
|
|
@@ -506,7 +654,7 @@ async function askCustomProseLanguage(rl, stderr) {
|
|
|
506
654
|
|
|
507
655
|
failedAttempts += 1;
|
|
508
656
|
if (failedAttempts >= 3) {
|
|
509
|
-
throw new InitError('Too many invalid attempts for
|
|
657
|
+
throw new InitError('Too many invalid attempts for Q4a. Dflow init aborted.');
|
|
510
658
|
}
|
|
511
659
|
stderr.write(`${validation.message} (${3 - failedAttempts} attempts left)\n`);
|
|
512
660
|
}
|
|
@@ -528,7 +676,7 @@ async function askOptionalFiles(rl, stdout, stderr) {
|
|
|
528
676
|
if (!parsed.valid) {
|
|
529
677
|
failedAttempts += 1;
|
|
530
678
|
if (failedAttempts >= 3) {
|
|
531
|
-
throw new InitError('Too many invalid attempts for
|
|
679
|
+
throw new InitError('Too many invalid attempts for Q5. Dflow init aborted.');
|
|
532
680
|
}
|
|
533
681
|
stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
|
|
534
682
|
continue;
|
|
@@ -549,6 +697,31 @@ async function askOptionalFiles(rl, stdout, stderr) {
|
|
|
549
697
|
}
|
|
550
698
|
}
|
|
551
699
|
|
|
700
|
+
async function askAiAgents(rl, stdout, stderr) {
|
|
701
|
+
let failedAttempts = 0;
|
|
702
|
+
|
|
703
|
+
while (true) {
|
|
704
|
+
stdout.write('\nWhich AI coding agents should Dflow configure?\n');
|
|
705
|
+
AI_AGENT_OPTIONS.forEach((option, index) => {
|
|
706
|
+
stdout.write(` ${index + 1}. ${option.label}\n`);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const answer = await askLine(rl, 'Enter comma-separated choices or "none" (default: none): ');
|
|
710
|
+
const parsed = parseMultiselectAnswer(answer || 'none', AI_AGENT_OPTIONS, []);
|
|
711
|
+
|
|
712
|
+
if (!parsed.valid) {
|
|
713
|
+
failedAttempts += 1;
|
|
714
|
+
if (failedAttempts >= 3) {
|
|
715
|
+
throw new InitError('Too many invalid attempts for Q6. Dflow init aborted.');
|
|
716
|
+
}
|
|
717
|
+
stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return parsed.values;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
552
725
|
function askLine(rl, prompt) {
|
|
553
726
|
return getLinePrompter(rl).ask(prompt);
|
|
554
727
|
}
|
|
@@ -669,7 +842,7 @@ function parseMultiselectAnswer(answer, options, defaultKeys) {
|
|
|
669
842
|
for (const token of tokens) {
|
|
670
843
|
const selectedKey = parseSelectAnswer(token, options, null);
|
|
671
844
|
if (!selectedKey) {
|
|
672
|
-
return { valid: false, message: `Invalid
|
|
845
|
+
return { valid: false, message: `Invalid selection: ${token}` };
|
|
673
846
|
}
|
|
674
847
|
if (!selected.includes(selectedKey)) {
|
|
675
848
|
selected.push(selectedKey);
|
|
@@ -713,9 +886,6 @@ async function buildFilePlan(cwd, answers) {
|
|
|
713
886
|
|
|
714
887
|
const addTemplate = async (relativePath, sourceRel, notes, options = {}) => {
|
|
715
888
|
let content = await readPackagedTemplate(answers.edition, sourceRel);
|
|
716
|
-
if (options.extractClaudeSnippetBody) {
|
|
717
|
-
content = extractClaudeSnippetBody(content, sourceRel);
|
|
718
|
-
}
|
|
719
889
|
content = substitutePlaceholders(content, substitution);
|
|
720
890
|
if (options.injectProseLanguage) {
|
|
721
891
|
content = ensureProseLanguageSection(content, answers.proseLanguage);
|
|
@@ -737,7 +907,7 @@ async function buildFilePlan(cwd, answers) {
|
|
|
737
907
|
});
|
|
738
908
|
await addTemplate('dflow/specs/domain/glossary.md', 'templates/glossary.md', 'mandatory');
|
|
739
909
|
|
|
740
|
-
if (answers.edition === '
|
|
910
|
+
if (answers.edition === 'greenfield') {
|
|
741
911
|
await addTemplate('dflow/specs/domain/context-map.md', 'templates/context-map.md', 'mandatory');
|
|
742
912
|
await addTemplate('dflow/specs/architecture/tech-debt.md', 'templates/tech-debt.md', 'mandatory');
|
|
743
913
|
await addTemplate(
|
|
@@ -758,17 +928,57 @@ async function buildFilePlan(cwd, answers) {
|
|
|
758
928
|
if (answers.optionalFiles.includes('git-flow')) {
|
|
759
929
|
await addTemplate('dflow/specs/shared/Git-principles-gitflow.md', 'scaffolding/Git-principles-gitflow.md', 'selected');
|
|
760
930
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
await addTemplate('CLAUDE.md', 'scaffolding/CLAUDE-md-snippet.md', 'selected, snippet body only', {
|
|
767
|
-
extractClaudeSnippetBody: true
|
|
768
|
-
});
|
|
931
|
+
|
|
932
|
+
if (answers.aiAgents.length > 0) {
|
|
933
|
+
await addTemplate('dflow/specs/shared/AI-AGENT-GUIDE.md', 'scaffolding/AI-AGENT-GUIDE.md', 'selected, canonical AI agent guide');
|
|
934
|
+
for (const agent of answers.aiAgents) {
|
|
935
|
+
await addAiAgentShim(cwd, items, agent, substitution);
|
|
769
936
|
}
|
|
770
937
|
}
|
|
771
938
|
|
|
939
|
+
await finalizePlanItems(cwd, items);
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
items,
|
|
943
|
+
deferred: buildDeferredItems(answers.edition),
|
|
944
|
+
unresolvedInitPlaceholders: Array.from(substitution.entries())
|
|
945
|
+
.filter(([placeholder, value]) => placeholder === value)
|
|
946
|
+
.map(([placeholder]) => placeholder)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function buildConfigureAgentsPlan(cwd, answers) {
|
|
951
|
+
const substitution = buildSubstitutionMap(cwd, {
|
|
952
|
+
...answers,
|
|
953
|
+
optionalFiles: answers.optionalFiles || []
|
|
954
|
+
});
|
|
955
|
+
const items = [];
|
|
956
|
+
|
|
957
|
+
let content = await readPackagedTemplate(answers.edition, 'scaffolding/AI-AGENT-GUIDE.md');
|
|
958
|
+
content = substitutePlaceholders(content, substitution);
|
|
959
|
+
items.push({
|
|
960
|
+
relativePath: 'dflow/specs/shared/AI-AGENT-GUIDE.md',
|
|
961
|
+
source: `packaged:${answers.edition}/scaffolding/AI-AGENT-GUIDE.md`,
|
|
962
|
+
notes: 'canonical AI agent guide',
|
|
963
|
+
content
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
for (const agent of answers.aiAgents) {
|
|
967
|
+
await addAiAgentShim(cwd, items, agent, substitution);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
await finalizePlanItems(cwd, items);
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
items,
|
|
974
|
+
deferred: [],
|
|
975
|
+
unresolvedInitPlaceholders: Array.from(substitution.entries())
|
|
976
|
+
.filter(([placeholder, value]) => placeholder === value)
|
|
977
|
+
.map(([placeholder]) => placeholder)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function finalizePlanItems(cwd, items) {
|
|
772
982
|
for (const item of items) {
|
|
773
983
|
const absolute = path.join(cwd, item.relativePath);
|
|
774
984
|
item.action = (await pathExists(absolute)) ? 'skip' : 'create';
|
|
@@ -777,22 +987,92 @@ async function buildFilePlan(cwd, answers) {
|
|
|
777
987
|
}
|
|
778
988
|
item.size = Buffer.byteLength(item.content, 'utf8');
|
|
779
989
|
}
|
|
990
|
+
}
|
|
780
991
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
992
|
+
async function addAiAgentShim(cwd, items, agent, substitution) {
|
|
993
|
+
const target = getAiAgentTarget(agent);
|
|
994
|
+
const targetPath = path.join(cwd, target.relativePath);
|
|
995
|
+
const targetExists = await pathExists(targetPath);
|
|
996
|
+
const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
|
|
997
|
+
const relativePath = targetExists && !targetConfigured ? target.snippetPath : target.relativePath;
|
|
998
|
+
const content = substitutePlaceholders(buildAiAgentShim(target.relativePath), substitution);
|
|
999
|
+
let notes = 'selected, tool-specific shim';
|
|
1000
|
+
if (targetConfigured) {
|
|
1001
|
+
notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
|
|
1002
|
+
} else if (targetExists) {
|
|
1003
|
+
notes = `selected, ${target.relativePath} already exists; merge this snippet manually`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
items.push({
|
|
1007
|
+
relativePath,
|
|
1008
|
+
source: `generated:${agent}-shim`,
|
|
1009
|
+
notes,
|
|
1010
|
+
content
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function fileReferencesAiAgentGuide(targetPath) {
|
|
1015
|
+
try {
|
|
1016
|
+
const content = await fs.readFile(targetPath, 'utf8');
|
|
1017
|
+
return content.includes('dflow/specs/shared/AI-AGENT-GUIDE.md') ||
|
|
1018
|
+
content.includes('dflow\\specs\\shared\\AI-AGENT-GUIDE.md');
|
|
1019
|
+
} catch {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function getAiAgentTarget(agent) {
|
|
1025
|
+
const targets = {
|
|
1026
|
+
agents: {
|
|
1027
|
+
relativePath: 'AGENTS.md',
|
|
1028
|
+
snippetPath: 'dflow/specs/shared/AGENTS-md-snippet.md'
|
|
1029
|
+
},
|
|
1030
|
+
claude: {
|
|
1031
|
+
relativePath: 'CLAUDE.md',
|
|
1032
|
+
snippetPath: 'dflow/specs/shared/CLAUDE-md-snippet.md'
|
|
1033
|
+
},
|
|
1034
|
+
gemini: {
|
|
1035
|
+
relativePath: 'GEMINI.md',
|
|
1036
|
+
snippetPath: 'dflow/specs/shared/GEMINI-md-snippet.md'
|
|
1037
|
+
},
|
|
1038
|
+
copilot: {
|
|
1039
|
+
relativePath: '.github/copilot-instructions.md',
|
|
1040
|
+
snippetPath: 'dflow/specs/shared/copilot-instructions-snippet.md'
|
|
1041
|
+
}
|
|
787
1042
|
};
|
|
1043
|
+
|
|
1044
|
+
return targets[agent];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function buildAiAgentShim(targetPath) {
|
|
1048
|
+
const title = targetPath === '.github/copilot-instructions.md'
|
|
1049
|
+
? 'GitHub Copilot Repository Instructions'
|
|
1050
|
+
: `${targetPath} - Dflow Project Instructions`;
|
|
1051
|
+
|
|
1052
|
+
const importHint = targetPath === 'CLAUDE.md' || targetPath === 'GEMINI.md'
|
|
1053
|
+
? '\nIf your tool supports Markdown imports, the canonical guide is imported below:\n\n@dflow/specs/shared/AI-AGENT-GUIDE.md\n'
|
|
1054
|
+
: '';
|
|
1055
|
+
|
|
1056
|
+
return `# ${title}
|
|
1057
|
+
|
|
1058
|
+
This project uses Dflow for spec-first AI-assisted development.
|
|
1059
|
+
|
|
1060
|
+
Before planning or editing code, read and follow:
|
|
1061
|
+
|
|
1062
|
+
- \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
|
|
1063
|
+
|
|
1064
|
+
Keep tool-specific instruction files small. The Dflow guide above is the
|
|
1065
|
+
single source of truth for project workflow rules, slash-command behavior,
|
|
1066
|
+
spec locations, and SDD/DDD constraints.${importHint}
|
|
1067
|
+
`;
|
|
788
1068
|
}
|
|
789
1069
|
|
|
790
1070
|
function buildDeferredItems(edition) {
|
|
791
1071
|
const deferred = [...DEFERRED_COMMON];
|
|
792
|
-
if (edition === '
|
|
1072
|
+
if (edition === 'greenfield') {
|
|
793
1073
|
deferred.splice(3, 0, {
|
|
794
1074
|
relativePath: 'dflow/specs/domain/{context}/events.md',
|
|
795
|
-
reason: '
|
|
1075
|
+
reason: 'Greenfield only, but still needs a real bounded context.'
|
|
796
1076
|
});
|
|
797
1077
|
}
|
|
798
1078
|
return deferred;
|
|
@@ -839,6 +1119,7 @@ function buildSubstitutionMap(cwd, answers) {
|
|
|
839
1119
|
['{tech-stack-summary}', answers.techStackSummary],
|
|
840
1120
|
['{migration-context}', answers.migrationContext],
|
|
841
1121
|
['{prose-language}', answers.proseLanguage],
|
|
1122
|
+
['{dflow-version}', pkg.version],
|
|
842
1123
|
['{ASP.NET Core version}', extracted.aspNetCoreVersion || '{ASP.NET Core version}'],
|
|
843
1124
|
['{EF Core version}', extracted.efCoreVersion || '{EF Core version}'],
|
|
844
1125
|
['{MediatR version}', extracted.mediatRVersion || '{MediatR version}'],
|
|
@@ -892,14 +1173,6 @@ function extractTestFramework(text) {
|
|
|
892
1173
|
return null;
|
|
893
1174
|
}
|
|
894
1175
|
|
|
895
|
-
function extractClaudeSnippetBody(content, sourceRel) {
|
|
896
|
-
const match = content.match(/## Snippet to merge into `CLAUDE\.md`[\s\S]*?```markdown\r?\n([\s\S]*?)\r?\n```/);
|
|
897
|
-
if (!match) {
|
|
898
|
-
throw new InitError(`Internal error: CLAUDE.md snippet body not found in packaged template: ${sourceRel}`);
|
|
899
|
-
}
|
|
900
|
-
return `${match[1].trimEnd()}\n`;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
1176
|
function ensureProseLanguageSection(content, proseLanguage) {
|
|
904
1177
|
const section = buildProseLanguageSection(proseLanguage);
|
|
905
1178
|
let stripped = stripProseLanguageSections(content);
|
|
@@ -1116,6 +1389,17 @@ Recommended next steps:
|
|
|
1116
1389
|
`);
|
|
1117
1390
|
}
|
|
1118
1391
|
|
|
1392
|
+
function printConfigureAgentsNextSteps(stdout) {
|
|
1393
|
+
stdout.write(`
|
|
1394
|
+
Dflow AI agent configuration complete.
|
|
1395
|
+
|
|
1396
|
+
Recommended next steps:
|
|
1397
|
+
- Keep AI-agent-specific root files small.
|
|
1398
|
+
- Put durable workflow changes in dflow/specs/shared/AI-AGENT-GUIDE.md.
|
|
1399
|
+
- If a merge snippet was created, review it and merge the pointer into the existing tool instruction file.
|
|
1400
|
+
`);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1119
1403
|
function printList(stdout, values) {
|
|
1120
1404
|
if (!values || values.length === 0) {
|
|
1121
1405
|
stdout.write('- (none)\n');
|
|
@@ -1186,8 +1470,8 @@ function formatBytes(bytes) {
|
|
|
1186
1470
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1187
1471
|
}
|
|
1188
1472
|
|
|
1189
|
-
function
|
|
1190
|
-
return
|
|
1473
|
+
function formatTrack(track) {
|
|
1474
|
+
return track === 'greenfield' ? 'Greenfield' : 'Brownfield';
|
|
1191
1475
|
}
|
|
1192
1476
|
|
|
1193
1477
|
function escapeTableCell(value) {
|
|
@@ -1198,7 +1482,101 @@ function dedupe(values) {
|
|
|
1198
1482
|
return Array.from(new Set(values));
|
|
1199
1483
|
}
|
|
1200
1484
|
|
|
1485
|
+
async function runDoctor(options = {}) {
|
|
1486
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
1487
|
+
const stdout = options.stdout || process.stdout;
|
|
1488
|
+
const stderr = options.stderr || process.stderr;
|
|
1489
|
+
|
|
1490
|
+
try {
|
|
1491
|
+
if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
|
|
1492
|
+
throw new InitError(`Dflow doctor requires Node.js ${MIN_NODE_VERSION}+.`, 1);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const findings = [];
|
|
1496
|
+
await checkLegacyRootSpecsDir(cwd, findings);
|
|
1497
|
+
await checkLegacySharedDir(cwd, findings);
|
|
1498
|
+
await checkConventionsDflowVersion(cwd, findings);
|
|
1499
|
+
|
|
1500
|
+
printDoctorReport(stdout, cwd, findings);
|
|
1501
|
+
return 0;
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
if (error instanceof InitError) {
|
|
1504
|
+
stderr.write(`${error.message}\n`);
|
|
1505
|
+
return error.exitCode;
|
|
1506
|
+
}
|
|
1507
|
+
stderr.write(`${error && error.message ? error.message : error}\n`);
|
|
1508
|
+
return 1;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
async function checkLegacyRootSpecsDir(cwd, findings) {
|
|
1513
|
+
const legacyPath = path.join(cwd, 'specs');
|
|
1514
|
+
if ((await pathExists(legacyPath)) && (await containsInitializedContent(legacyPath))) {
|
|
1515
|
+
findings.push({
|
|
1516
|
+
level: 'warn',
|
|
1517
|
+
title: 'Legacy specs/ directory at project root',
|
|
1518
|
+
detail: 'V1 layout uses dflow/specs/ instead. The CLI does not modify root specs/.',
|
|
1519
|
+
action: 'See docs/migrating-to-dflow-v1.md (Step 1) for the manual migration steps.'
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function checkLegacySharedDir(cwd, findings) {
|
|
1525
|
+
const candidates = [
|
|
1526
|
+
path.join(cwd, 'dflow', 'specs', '_共用'),
|
|
1527
|
+
path.join(cwd, 'specs', '_共用')
|
|
1528
|
+
];
|
|
1529
|
+
for (const candidate of candidates) {
|
|
1530
|
+
if (await pathExists(candidate)) {
|
|
1531
|
+
const rel = normalizePath(path.relative(cwd, candidate));
|
|
1532
|
+
findings.push({
|
|
1533
|
+
level: 'warn',
|
|
1534
|
+
title: `Legacy ${rel}/ directory`,
|
|
1535
|
+
detail: 'V1 layout uses shared/ (canonical English directory name).',
|
|
1536
|
+
action: 'See docs/migrating-to-dflow-v1.md (Step 2) for the rename steps.'
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async function checkConventionsDflowVersion(cwd, findings) {
|
|
1543
|
+
const conventionsPath = path.join(cwd, 'dflow', 'specs', 'shared', '_conventions.md');
|
|
1544
|
+
if (!(await pathExists(conventionsPath))) return;
|
|
1545
|
+
const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
|
|
1546
|
+
if (!/^> Dflow Version:/m.test(content)) {
|
|
1547
|
+
findings.push({
|
|
1548
|
+
level: 'info',
|
|
1549
|
+
title: 'dflow/specs/shared/_conventions.md missing Dflow Version line',
|
|
1550
|
+
detail: 'V1 init writes a `> Dflow Version: <x.y.z>` line in the front matter automatically. This project predates that convention.',
|
|
1551
|
+
action: 'Optionally add the line manually so future migration / review can identify the spec convention version.'
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function printDoctorReport(stdout, cwd, findings) {
|
|
1557
|
+
stdout.write(`Dflow Doctor ${pkg.version}\n`);
|
|
1558
|
+
stdout.write(`Project: ${cwd}\n\n`);
|
|
1559
|
+
|
|
1560
|
+
if (findings.length === 0) {
|
|
1561
|
+
stdout.write('All checks passed. No legacy artifacts detected.\n');
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
for (const finding of findings) {
|
|
1566
|
+
stdout.write(`[${finding.level}] ${finding.title}\n`);
|
|
1567
|
+
stdout.write(` ${finding.detail}\n`);
|
|
1568
|
+
stdout.write(` ${finding.action}\n\n`);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const counts = { warn: 0, info: 0 };
|
|
1572
|
+
for (const f of findings) counts[f.level] = (counts[f.level] || 0) + 1;
|
|
1573
|
+
stdout.write(`${findings.length} finding(s): ${counts.warn} warn, ${counts.info} info.\n`);
|
|
1574
|
+
stdout.write('Doctor is read-only and does not modify any files.\n');
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1201
1577
|
module.exports = {
|
|
1578
|
+
runConfigureAgents,
|
|
1579
|
+
runDoctor,
|
|
1202
1580
|
runInit,
|
|
1203
1581
|
validateProseLanguage,
|
|
1204
1582
|
ensureProseLanguageSection,
|