dflow-sdd-ddd 0.3.0 → 0.5.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 +118 -0
- package/README.en.md +7 -9
- package/README.md +10 -12
- package/TEMPLATE-COVERAGE.md +1 -1
- package/bin/dflow.js +11 -5
- package/docs/evaluating-dflow.en.md +2 -5
- package/docs/evaluating-dflow.md +1 -3
- package/docs/examples-by-stack.md +516 -0
- package/docs/migrating-to-dflow-v1.md +1 -1
- package/docs/release-versioning-policy.md +13 -0
- package/docs/using-with-claude-code.en.md +38 -8
- package/docs/using-with-claude-code.md +33 -7
- package/docs/using-with-codex.en.md +31 -5
- package/docs/using-with-codex.md +28 -5
- package/docs/using-with-github-copilot.en.md +29 -5
- package/docs/using-with-github-copilot.md +28 -5
- package/lib/init.js +437 -46
- package/package.json +1 -1
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +42 -3
- package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +25 -15
- package/templates/brownfield/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/brownfield/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/brownfield/scaffolding/_conventions.md +1 -1
- package/templates/brownfield/scaffolding/_overview.md +40 -29
- package/templates/brownfield/templates/CLAUDE.md +25 -17
- package/templates/brownfield/templates/context-definition.md +4 -4
- package/templates/brownfield/templates/context-map.md +1 -1
- package/templates/brownfield/templates/lightweight-spec.md +3 -1
- package/templates/brownfield/templates/models.md +1 -1
- package/templates/brownfield/templates/phase-spec.md +10 -8
- package/templates/brownfield/templates/tech-debt.md +2 -2
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +42 -3
- package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +7 -6
- package/templates/greenfield/scaffolding/Git-principles-gitflow.md +2 -2
- package/templates/greenfield/scaffolding/Git-principles-trunk.md +2 -2
- package/templates/greenfield/scaffolding/_overview.md +29 -11
- package/templates/greenfield/templates/CLAUDE.md +5 -5
- package/docs/using-with-gemini-cli.en.md +0 -200
- package/docs/using-with-gemini-cli.md +0 -184
package/lib/init.js
CHANGED
|
@@ -8,6 +8,21 @@ const pkg = require('../package.json');
|
|
|
8
8
|
const MIN_NODE_VERSION = '22.0.0';
|
|
9
9
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
10
10
|
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
|
|
11
|
+
const COMMAND_REGISTRY_START = '<!-- dflow-command-registry:start -->';
|
|
12
|
+
const COMMAND_REGISTRY_END = '<!-- dflow-command-registry:end -->';
|
|
13
|
+
const EXPECTED_COMMAND_IDS = [
|
|
14
|
+
'new-feature',
|
|
15
|
+
'modify-existing',
|
|
16
|
+
'bug-fix',
|
|
17
|
+
'new-phase',
|
|
18
|
+
'finish-feature',
|
|
19
|
+
'verify',
|
|
20
|
+
'pr-review',
|
|
21
|
+
'report-dflow-feedback',
|
|
22
|
+
'status',
|
|
23
|
+
'next',
|
|
24
|
+
'cancel'
|
|
25
|
+
];
|
|
11
26
|
|
|
12
27
|
const PROSE_LANGUAGE_PATTERN =
|
|
13
28
|
/^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*$/;
|
|
@@ -91,11 +106,6 @@ const AI_AGENT_OPTIONS = [
|
|
|
91
106
|
label: 'CLAUDE.md - Claude Code',
|
|
92
107
|
aliases: ['claude', 'claude.md']
|
|
93
108
|
},
|
|
94
|
-
{
|
|
95
|
-
key: 'gemini',
|
|
96
|
-
label: 'GEMINI.md - Gemini CLI',
|
|
97
|
-
aliases: ['gemini', 'gemini.md']
|
|
98
|
-
},
|
|
99
109
|
{
|
|
100
110
|
key: 'copilot',
|
|
101
111
|
label: '.github/copilot-instructions.md - GitHub Copilot',
|
|
@@ -251,7 +261,8 @@ async function runConfigureAgents(options = {}) {
|
|
|
251
261
|
|
|
252
262
|
const plan = await buildConfigureAgentsPlan(cwd, {
|
|
253
263
|
...projectContext,
|
|
254
|
-
aiAgents
|
|
264
|
+
aiAgents,
|
|
265
|
+
commandAdapters: Boolean(options.commandAdapters)
|
|
255
266
|
});
|
|
256
267
|
|
|
257
268
|
renderPreview(stdout, plan, []);
|
|
@@ -268,7 +279,7 @@ async function runConfigureAgents(options = {}) {
|
|
|
268
279
|
result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
|
|
269
280
|
|
|
270
281
|
printResultReport(stdout, result, plan.deferred);
|
|
271
|
-
printConfigureAgentsNextSteps(stdout);
|
|
282
|
+
printConfigureAgentsNextSteps(stdout, Boolean(options.commandAdapters));
|
|
272
283
|
return 0;
|
|
273
284
|
} catch (error) {
|
|
274
285
|
if (rl) {
|
|
@@ -383,7 +394,16 @@ async function detectProjectSignals(cwd) {
|
|
|
383
394
|
name.endsWith('.csproj') ||
|
|
384
395
|
name === 'program.cs' ||
|
|
385
396
|
name === 'startup.cs' ||
|
|
386
|
-
name === 'package.json'
|
|
397
|
+
name === 'package.json' ||
|
|
398
|
+
name === 'pom.xml' ||
|
|
399
|
+
name === 'build.gradle' ||
|
|
400
|
+
name === 'build.gradle.kts' ||
|
|
401
|
+
name === 'pyproject.toml' ||
|
|
402
|
+
name === 'requirements.txt' ||
|
|
403
|
+
name === 'go.mod' ||
|
|
404
|
+
name === 'cargo.toml' ||
|
|
405
|
+
name === 'composer.json' ||
|
|
406
|
+
name === 'gemfile'
|
|
387
407
|
);
|
|
388
408
|
|
|
389
409
|
const hasWebFormsFiles = Array.from(baseNames).some((name) =>
|
|
@@ -412,9 +432,36 @@ async function detectProjectSignals(cwd) {
|
|
|
412
432
|
trackHint = 'brownfield';
|
|
413
433
|
}
|
|
414
434
|
|
|
435
|
+
const stackHints = [];
|
|
436
|
+
if (coreSignal || webFormsSignal || csprojFiles.length > 0) {
|
|
437
|
+
stackHints.push('dotnet');
|
|
438
|
+
}
|
|
439
|
+
if (baseNames.has('pom.xml') || baseNames.has('build.gradle') || baseNames.has('build.gradle.kts')) {
|
|
440
|
+
stackHints.push('java');
|
|
441
|
+
}
|
|
442
|
+
if (baseNames.has('package.json')) {
|
|
443
|
+
stackHints.push('nodejs');
|
|
444
|
+
}
|
|
445
|
+
if (baseNames.has('pyproject.toml') || baseNames.has('requirements.txt')) {
|
|
446
|
+
stackHints.push('python');
|
|
447
|
+
}
|
|
448
|
+
if (baseNames.has('go.mod')) {
|
|
449
|
+
stackHints.push('go');
|
|
450
|
+
}
|
|
451
|
+
if (baseNames.has('cargo.toml')) {
|
|
452
|
+
stackHints.push('rust');
|
|
453
|
+
}
|
|
454
|
+
if (baseNames.has('composer.json')) {
|
|
455
|
+
stackHints.push('php');
|
|
456
|
+
}
|
|
457
|
+
if (baseNames.has('gemfile')) {
|
|
458
|
+
stackHints.push('ruby');
|
|
459
|
+
}
|
|
460
|
+
|
|
415
461
|
return {
|
|
416
462
|
hasSourceTree: hasSourceTree || relNames.has('src'),
|
|
417
|
-
trackHint
|
|
463
|
+
trackHint,
|
|
464
|
+
stackHints
|
|
418
465
|
};
|
|
419
466
|
}
|
|
420
467
|
|
|
@@ -963,8 +1010,14 @@ async function buildConfigureAgentsPlan(cwd, answers) {
|
|
|
963
1010
|
content
|
|
964
1011
|
});
|
|
965
1012
|
|
|
1013
|
+
const commandRegistry = answers.commandAdapters ? parseDflowCommandRegistry(content) : [];
|
|
1014
|
+
|
|
966
1015
|
for (const agent of answers.aiAgents) {
|
|
967
|
-
await addAiAgentShim(cwd, items, agent, substitution);
|
|
1016
|
+
await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (answers.commandAdapters) {
|
|
1020
|
+
addCommandAdapterItems(items, answers.aiAgents, commandRegistry);
|
|
968
1021
|
}
|
|
969
1022
|
|
|
970
1023
|
await finalizePlanItems(cwd, items);
|
|
@@ -981,7 +1034,8 @@ async function buildConfigureAgentsPlan(cwd, answers) {
|
|
|
981
1034
|
async function finalizePlanItems(cwd, items) {
|
|
982
1035
|
for (const item of items) {
|
|
983
1036
|
const absolute = path.join(cwd, item.relativePath);
|
|
984
|
-
|
|
1037
|
+
const targetExists = await pathExists(absolute);
|
|
1038
|
+
item.action = targetExists ? (item.overwrite ? 'update' : 'skip') : 'create';
|
|
985
1039
|
if (item.action === 'skip') {
|
|
986
1040
|
item.notes = item.notes ? `${item.notes}, already exists` : 'already exists';
|
|
987
1041
|
}
|
|
@@ -989,15 +1043,23 @@ async function finalizePlanItems(cwd, items) {
|
|
|
989
1043
|
}
|
|
990
1044
|
}
|
|
991
1045
|
|
|
992
|
-
async function addAiAgentShim(cwd, items, agent, substitution) {
|
|
1046
|
+
async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
|
|
993
1047
|
const target = getAiAgentTarget(agent);
|
|
994
1048
|
const targetPath = path.join(cwd, target.relativePath);
|
|
995
1049
|
const targetExists = await pathExists(targetPath);
|
|
996
1050
|
const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
|
|
997
|
-
const
|
|
998
|
-
const
|
|
1051
|
+
const commandRegistry = options.commandRegistry || [];
|
|
1052
|
+
const codexCommandAdapterSnippet = target.relativePath === 'AGENTS.md' && commandRegistry.length > 0 && targetExists;
|
|
1053
|
+
const relativePath = codexCommandAdapterSnippet
|
|
1054
|
+
? (targetConfigured ? 'dflow/specs/shared/AGENTS-md-command-adapters-snippet.md' : target.snippetPath)
|
|
1055
|
+
: (targetExists && !targetConfigured ? target.snippetPath : target.relativePath);
|
|
1056
|
+
const content = substitutePlaceholders(buildAiAgentShim(target.relativePath, options.commandRegistry), substitution);
|
|
999
1057
|
let notes = 'selected, tool-specific shim';
|
|
1000
|
-
if (targetConfigured) {
|
|
1058
|
+
if (codexCommandAdapterSnippet && targetConfigured) {
|
|
1059
|
+
notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md; merge this command trigger snippet manually`;
|
|
1060
|
+
} else if (codexCommandAdapterSnippet) {
|
|
1061
|
+
notes = `selected, ${target.relativePath} already exists; merge this command trigger snippet manually`;
|
|
1062
|
+
} else if (targetConfigured) {
|
|
1001
1063
|
notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
|
|
1002
1064
|
} else if (targetExists) {
|
|
1003
1065
|
notes = `selected, ${target.relativePath} already exists; merge this snippet manually`;
|
|
@@ -1007,7 +1069,8 @@ async function addAiAgentShim(cwd, items, agent, substitution) {
|
|
|
1007
1069
|
relativePath,
|
|
1008
1070
|
source: `generated:${agent}-shim`,
|
|
1009
1071
|
notes,
|
|
1010
|
-
content
|
|
1072
|
+
content,
|
|
1073
|
+
overwrite: codexCommandAdapterSnippet
|
|
1011
1074
|
});
|
|
1012
1075
|
}
|
|
1013
1076
|
|
|
@@ -1031,10 +1094,6 @@ function getAiAgentTarget(agent) {
|
|
|
1031
1094
|
relativePath: 'CLAUDE.md',
|
|
1032
1095
|
snippetPath: 'dflow/specs/shared/CLAUDE-md-snippet.md'
|
|
1033
1096
|
},
|
|
1034
|
-
gemini: {
|
|
1035
|
-
relativePath: 'GEMINI.md',
|
|
1036
|
-
snippetPath: 'dflow/specs/shared/GEMINI-md-snippet.md'
|
|
1037
|
-
},
|
|
1038
1097
|
copilot: {
|
|
1039
1098
|
relativePath: '.github/copilot-instructions.md',
|
|
1040
1099
|
snippetPath: 'dflow/specs/shared/copilot-instructions-snippet.md'
|
|
@@ -1044,12 +1103,16 @@ function getAiAgentTarget(agent) {
|
|
|
1044
1103
|
return targets[agent];
|
|
1045
1104
|
}
|
|
1046
1105
|
|
|
1047
|
-
function buildAiAgentShim(targetPath) {
|
|
1106
|
+
function buildAiAgentShim(targetPath, commandRegistry = []) {
|
|
1048
1107
|
const title = targetPath === '.github/copilot-instructions.md'
|
|
1049
1108
|
? 'GitHub Copilot Repository Instructions'
|
|
1050
1109
|
: `${targetPath} - Dflow Project Instructions`;
|
|
1051
1110
|
|
|
1052
|
-
const
|
|
1111
|
+
const commandTriggerHint = targetPath === 'AGENTS.md' && commandRegistry.length > 0
|
|
1112
|
+
? buildCodexCommandTriggerSection(commandRegistry)
|
|
1113
|
+
: '';
|
|
1114
|
+
|
|
1115
|
+
const importHint = targetPath === 'CLAUDE.md'
|
|
1053
1116
|
? '\nIf your tool supports Markdown imports, the canonical guide is imported below:\n\n@dflow/specs/shared/AI-AGENT-GUIDE.md\n'
|
|
1054
1117
|
: '';
|
|
1055
1118
|
|
|
@@ -1063,10 +1126,195 @@ Before planning or editing code, read and follow:
|
|
|
1063
1126
|
|
|
1064
1127
|
Keep tool-specific instruction files small. The Dflow guide above is the
|
|
1065
1128
|
single source of truth for project workflow rules, slash-command behavior,
|
|
1066
|
-
spec locations, and SDD/DDD constraints.${importHint}
|
|
1129
|
+
spec locations, and SDD/DDD constraints.${commandTriggerHint}${importHint}
|
|
1067
1130
|
`;
|
|
1068
1131
|
}
|
|
1069
1132
|
|
|
1133
|
+
function addCommandAdapterItems(items, aiAgents, commandRegistry) {
|
|
1134
|
+
if (aiAgents.includes('claude')) {
|
|
1135
|
+
for (const command of commandRegistry) {
|
|
1136
|
+
items.push({
|
|
1137
|
+
relativePath: `.claude/commands/dflow/dflow-${command.id}.md`,
|
|
1138
|
+
source: 'generated:claude-command-adapter',
|
|
1139
|
+
notes: 'command adapter, derived from dflow command registry',
|
|
1140
|
+
content: buildThinCommandWrapper(command),
|
|
1141
|
+
overwrite: true
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (aiAgents.includes('copilot')) {
|
|
1147
|
+
for (const command of commandRegistry) {
|
|
1148
|
+
items.push({
|
|
1149
|
+
relativePath: `.github/prompts/dflow-${command.id}.prompt.md`,
|
|
1150
|
+
source: 'generated:copilot-command-adapter',
|
|
1151
|
+
notes: 'command adapter, derived from dflow command registry',
|
|
1152
|
+
content: buildThinCommandWrapper(command),
|
|
1153
|
+
overwrite: true
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function buildCodexCommandTriggerSection(commandRegistry) {
|
|
1160
|
+
const triggers = commandRegistry
|
|
1161
|
+
.map((command) => {
|
|
1162
|
+
const commandKind = command.scope === 'control' ? 'command' : 'workflow';
|
|
1163
|
+
return `- \`${command.label}\` as text, or "Run the Dflow ${command.label} ${commandKind}."`;
|
|
1164
|
+
})
|
|
1165
|
+
.join('\n');
|
|
1166
|
+
|
|
1167
|
+
return `
|
|
1168
|
+
|
|
1169
|
+
## Dflow Text Triggers
|
|
1170
|
+
|
|
1171
|
+
Codex does not install Dflow command files. When the developer asks for a
|
|
1172
|
+
canonical Dflow command, treat it as a text trigger, read the canonical guide,
|
|
1173
|
+
and execute the matching workflow or control command.
|
|
1174
|
+
|
|
1175
|
+
Recognized canonical triggers:
|
|
1176
|
+
|
|
1177
|
+
${triggers}
|
|
1178
|
+
`;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function buildThinCommandWrapper(command) {
|
|
1182
|
+
const argHint = command.argHint === '-'
|
|
1183
|
+
? 'Argument hint: none.'
|
|
1184
|
+
: `Argument hint: ${command.argHint}.`;
|
|
1185
|
+
|
|
1186
|
+
return `# /dflow-${command.id}
|
|
1187
|
+
|
|
1188
|
+
Execute the canonical \`${command.label}\` Dflow workflow or control command.
|
|
1189
|
+
|
|
1190
|
+
Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
|
|
1191
|
+
|
|
1192
|
+
${argHint}
|
|
1193
|
+
`;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function parseDflowCommandRegistry(content) {
|
|
1197
|
+
const start = content.indexOf(COMMAND_REGISTRY_START);
|
|
1198
|
+
const end = content.indexOf(COMMAND_REGISTRY_END);
|
|
1199
|
+
|
|
1200
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
1201
|
+
throw new InitError('Internal error: dflow command registry markers not found in packaged AI-AGENT-GUIDE.md.');
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const registryContent = content.slice(start + COMMAND_REGISTRY_START.length, end);
|
|
1205
|
+
const tableLines = registryContent
|
|
1206
|
+
.split(/\r?\n/)
|
|
1207
|
+
.map((line) => line.trim())
|
|
1208
|
+
.filter((line) => line.startsWith('|') && line.endsWith('|'));
|
|
1209
|
+
|
|
1210
|
+
if (tableLines.length < 3) {
|
|
1211
|
+
throw new InitError('Internal error: dflow command registry table is empty.');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const header = parseMarkdownTableRow(tableLines[0]).map((cell) => cell.toLowerCase());
|
|
1215
|
+
const expectedHeader = ['id', 'label', 'description', 'arg-hint', 'scope'];
|
|
1216
|
+
if (header.length !== expectedHeader.length || !expectedHeader.every((cell, index) => header[index] === cell)) {
|
|
1217
|
+
throw new InitError('Internal error: dflow command registry header must be: id, label, description, arg-hint, scope.');
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const delimiter = parseMarkdownTableRow(tableLines[1]);
|
|
1221
|
+
if (delimiter.length !== expectedHeader.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
1222
|
+
throw new InitError('Internal error: dflow command registry delimiter row is invalid.');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const commands = [];
|
|
1226
|
+
const seen = new Set();
|
|
1227
|
+
|
|
1228
|
+
for (const line of tableLines.slice(2)) {
|
|
1229
|
+
const cells = parseMarkdownTableRow(line);
|
|
1230
|
+
if (cells.length !== expectedHeader.length) {
|
|
1231
|
+
throw new InitError(`Internal error: invalid dflow command registry row: ${line}`);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const command = {
|
|
1235
|
+
id: stripInlineCode(cells[0]),
|
|
1236
|
+
label: stripInlineCode(cells[1]),
|
|
1237
|
+
description: stripInlineCode(cells[2]),
|
|
1238
|
+
argHint: stripInlineCode(cells[3]),
|
|
1239
|
+
scope: stripInlineCode(cells[4])
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
validateDflowCommandRegistryRow(command, seen);
|
|
1243
|
+
commands.push(command);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const actualIds = commands.map((command) => command.id);
|
|
1247
|
+
const missingIds = EXPECTED_COMMAND_IDS.filter((id) => !actualIds.includes(id));
|
|
1248
|
+
const extraIds = actualIds.filter((id) => !EXPECTED_COMMAND_IDS.includes(id));
|
|
1249
|
+
if (missingIds.length > 0 || extraIds.length > 0 || commands.length !== EXPECTED_COMMAND_IDS.length) {
|
|
1250
|
+
throw new InitError(
|
|
1251
|
+
`Internal error: dflow command registry must contain exactly these command ids: ${EXPECTED_COMMAND_IDS.join(', ')}.`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return commands;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function parseMarkdownTableRow(line) {
|
|
1259
|
+
const trimmed = line.trim();
|
|
1260
|
+
const inner = trimmed.slice(1, -1);
|
|
1261
|
+
const cells = [];
|
|
1262
|
+
let current = '';
|
|
1263
|
+
|
|
1264
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
1265
|
+
const char = inner[index];
|
|
1266
|
+
const next = inner[index + 1];
|
|
1267
|
+
|
|
1268
|
+
if (char === '\\' && next === '|') {
|
|
1269
|
+
current += '|';
|
|
1270
|
+
index += 1;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (char === '|') {
|
|
1275
|
+
cells.push(current.trim());
|
|
1276
|
+
current = '';
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
current += char;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
cells.push(current.trim());
|
|
1284
|
+
return cells;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function stripInlineCode(value) {
|
|
1288
|
+
const trimmed = String(value || '').trim();
|
|
1289
|
+
if (trimmed.startsWith('`') && trimmed.endsWith('`') && trimmed.length >= 2) {
|
|
1290
|
+
return trimmed.slice(1, -1);
|
|
1291
|
+
}
|
|
1292
|
+
return trimmed;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function validateDflowCommandRegistryRow(command, seen) {
|
|
1296
|
+
if (!/^[a-z][a-z0-9-]*$/.test(command.id)) {
|
|
1297
|
+
throw new InitError(`Internal error: invalid dflow command id: ${command.id}`);
|
|
1298
|
+
}
|
|
1299
|
+
if (seen.has(command.id)) {
|
|
1300
|
+
throw new InitError(`Internal error: duplicate dflow command id: ${command.id}`);
|
|
1301
|
+
}
|
|
1302
|
+
seen.add(command.id);
|
|
1303
|
+
|
|
1304
|
+
if (command.label !== `/dflow:${command.id}`) {
|
|
1305
|
+
throw new InitError(`Internal error: dflow command label must be /dflow:${command.id}.`);
|
|
1306
|
+
}
|
|
1307
|
+
if (!command.description) {
|
|
1308
|
+
throw new InitError(`Internal error: dflow command ${command.id} is missing a description.`);
|
|
1309
|
+
}
|
|
1310
|
+
if (!command.argHint) {
|
|
1311
|
+
throw new InitError(`Internal error: dflow command ${command.id} is missing an arg-hint.`);
|
|
1312
|
+
}
|
|
1313
|
+
if (!command.scope) {
|
|
1314
|
+
throw new InitError(`Internal error: dflow command ${command.id} is missing a scope.`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1070
1318
|
function buildDeferredItems(edition) {
|
|
1071
1319
|
const deferred = [...DEFERRED_COMMON];
|
|
1072
1320
|
if (edition === 'greenfield') {
|
|
@@ -1104,13 +1352,20 @@ async function readPackagedTemplate(edition, sourceRel) {
|
|
|
1104
1352
|
}
|
|
1105
1353
|
}
|
|
1106
1354
|
|
|
1355
|
+
const PLACEHOLDER_ALIASES = {
|
|
1356
|
+
'{Framework version}': ['{ASP.NET Core version}', '{ASP.NET WebForms version}', '{.NET Framework version}'],
|
|
1357
|
+
'{ORM / persistence}': ['{ORM / Data Access}'],
|
|
1358
|
+
'{ORM version}': ['{EF Core version}'],
|
|
1359
|
+
'{Mediator}': ['{MediatR version}']
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1107
1362
|
function buildSubstitutionMap(cwd, answers) {
|
|
1108
1363
|
const extracted = extractTechStackPlaceholders(answers.techStackSummary);
|
|
1109
1364
|
const gitSelection = answers.optionalFiles.filter((key) => key === 'git-trunk' || key === 'git-flow');
|
|
1110
1365
|
const gitStyle = gitSelection.length === 1 ? (gitSelection[0] === 'git-trunk' ? 'trunk' : 'gitflow') : null;
|
|
1111
1366
|
const systemName = path.basename(cwd);
|
|
1112
1367
|
|
|
1113
|
-
|
|
1368
|
+
const map = new Map([
|
|
1114
1369
|
['{YYYY-MM-DD}', currentLocalDate()],
|
|
1115
1370
|
['{System Name}', systemName],
|
|
1116
1371
|
['{系統名稱}', systemName],
|
|
@@ -1120,15 +1375,26 @@ function buildSubstitutionMap(cwd, answers) {
|
|
|
1120
1375
|
['{migration-context}', answers.migrationContext],
|
|
1121
1376
|
['{prose-language}', answers.proseLanguage],
|
|
1122
1377
|
['{dflow-version}', pkg.version],
|
|
1123
|
-
['{
|
|
1124
|
-
['{
|
|
1125
|
-
['{
|
|
1378
|
+
['{Language}', extracted.language || '{Language}'],
|
|
1379
|
+
['{Framework}', extracted.framework || '{Framework}'],
|
|
1380
|
+
['{Framework version}', extracted.frameworkVersion || '{Framework version}'],
|
|
1381
|
+
['{ORM / persistence}', extracted.ormPersistence || '{ORM / persistence}'],
|
|
1382
|
+
['{ORM version}', extracted.ormVersion || '{ORM version}'],
|
|
1383
|
+
['{Mediator}', extracted.mediator || '{Mediator}'],
|
|
1126
1384
|
['{Test framework}', extracted.testFramework || '{Test framework}'],
|
|
1127
|
-
['{ASP.NET WebForms version}', extracted.webFormsVersion || '{ASP.NET WebForms version}'],
|
|
1128
|
-
['{.NET Framework version}', extracted.dotNetFrameworkVersion || '{.NET Framework version}'],
|
|
1129
|
-
['{ORM / Data Access}', extracted.ormDataAccess || '{ORM / Data Access}'],
|
|
1130
1385
|
['{gitflow|trunk}', gitStyle || '{gitflow|trunk}']
|
|
1131
1386
|
]);
|
|
1387
|
+
|
|
1388
|
+
for (const [canonical, aliases] of Object.entries(PLACEHOLDER_ALIASES)) {
|
|
1389
|
+
const value = map.get(canonical);
|
|
1390
|
+
if (value === undefined) continue;
|
|
1391
|
+
const canonicalResolved = value !== canonical;
|
|
1392
|
+
for (const alias of aliases) {
|
|
1393
|
+
map.set(alias, canonicalResolved ? value : alias);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
return map;
|
|
1132
1398
|
}
|
|
1133
1399
|
|
|
1134
1400
|
function substitutePlaceholders(content, substitution) {
|
|
@@ -1139,19 +1405,104 @@ function substitutePlaceholders(content, substitution) {
|
|
|
1139
1405
|
return result;
|
|
1140
1406
|
}
|
|
1141
1407
|
|
|
1408
|
+
const LANGUAGE_PATTERNS = [
|
|
1409
|
+
/\bC#\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1410
|
+
/\bC#\b/,
|
|
1411
|
+
/\bTypeScript\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1412
|
+
/\bTypeScript\b/i,
|
|
1413
|
+
/\bJavaScript\b/i,
|
|
1414
|
+
/\bKotlin\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1415
|
+
/\bKotlin\b/i,
|
|
1416
|
+
/\bJava\s*[0-9]+\b/i,
|
|
1417
|
+
/\bJava\b/i,
|
|
1418
|
+
/\bPython\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1419
|
+
/\bPython\b/i,
|
|
1420
|
+
/\bGolang\b/i,
|
|
1421
|
+
/\bGo\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1422
|
+
/\bPHP\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1423
|
+
/\bPHP\b/i,
|
|
1424
|
+
/\bRuby\b/i
|
|
1425
|
+
];
|
|
1426
|
+
|
|
1427
|
+
const FRAMEWORK_VERSION_PATTERNS = [
|
|
1428
|
+
/\bASP\.?NET\s+Core\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1429
|
+
/\b(?:ASP\.?NET\s+WebForms|WebForms)(?:\s+[0-9]+(?:\.[0-9]+)?)?\b/i,
|
|
1430
|
+
/\b\.NET\s+Framework\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1431
|
+
/\bSpring\s+Boot\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1432
|
+
/\bSpring\s+MVC\b/i,
|
|
1433
|
+
/\bNestJS\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1434
|
+
/\bFastify\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1435
|
+
/\bExpress(?:\.js)?\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1436
|
+
/\bDjango\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1437
|
+
/\bFastAPI\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1438
|
+
/\bFlask\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1439
|
+
/\bGin\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1440
|
+
/\bEcho\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1441
|
+
/\bLaravel\s*[0-9]+(?:\.[0-9]+)?\b/i
|
|
1442
|
+
];
|
|
1443
|
+
|
|
1444
|
+
const FRAMEWORK_PATTERNS = [
|
|
1445
|
+
/\bASP\.?NET\s+Core\b/i,
|
|
1446
|
+
/\b(?:ASP\.?NET\s+WebForms|WebForms)\b/i,
|
|
1447
|
+
/\b\.NET\s+Framework\b/i,
|
|
1448
|
+
/\bSpring\s+Boot\b/i,
|
|
1449
|
+
/\bSpring\s+MVC\b/i,
|
|
1450
|
+
/\bNestJS\b/i,
|
|
1451
|
+
/\bFastify\b/i,
|
|
1452
|
+
/\bExpress(?:\.js)?\b/i,
|
|
1453
|
+
/\bDjango\b/i,
|
|
1454
|
+
/\bFastAPI\b/i,
|
|
1455
|
+
/\bFlask\b/i,
|
|
1456
|
+
/\bGin\b/i,
|
|
1457
|
+
/\bEcho\b/i,
|
|
1458
|
+
/\bLaravel\b/i
|
|
1459
|
+
];
|
|
1460
|
+
|
|
1461
|
+
const ORM_VERSION_PATTERNS = [
|
|
1462
|
+
/\b(?:EF\s+Core|Entity\s+Framework\s+Core)\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1463
|
+
/\b(?:EF6|Entity\s+Framework\s+6)\b/i,
|
|
1464
|
+
/\bHibernate\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1465
|
+
/\bSpring\s+Data\s+JPA\b/i,
|
|
1466
|
+
/\bJPA\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1467
|
+
/\bSQLAlchemy\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1468
|
+
/\bPrisma\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1469
|
+
/\bTypeORM\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1470
|
+
/\bMikro-?ORM\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1471
|
+
/\bGORM\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1472
|
+
/\bEloquent\s*[0-9]+(?:\.[0-9]+)?\b/i,
|
|
1473
|
+
/\bDoctrine\s*[0-9]+(?:\.[0-9]+)?\b/i
|
|
1474
|
+
];
|
|
1475
|
+
|
|
1476
|
+
const ORM_PERSISTENCE_PATTERNS = [
|
|
1477
|
+
/\b(?:EF\s+Core|Entity\s+Framework\s+Core)\b/i,
|
|
1478
|
+
/\b(?:EF6|Entity\s+Framework\s+6|Dapper|ADO\.NET)\b/i,
|
|
1479
|
+
/\bHibernate\b/i,
|
|
1480
|
+
/\bSpring\s+Data\s+JPA\b/i,
|
|
1481
|
+
/\bJPA\b/i,
|
|
1482
|
+
/\bSQLAlchemy\b/i,
|
|
1483
|
+
/\bSQLModel\b/i,
|
|
1484
|
+
/\bPrisma\b/i,
|
|
1485
|
+
/\bTypeORM\b/i,
|
|
1486
|
+
/\bMikro-?ORM\b/i,
|
|
1487
|
+
/\bGORM\b/i,
|
|
1488
|
+
/\bsqlx\b/i,
|
|
1489
|
+
/\bEloquent\b/i,
|
|
1490
|
+
/\bDoctrine\b/i
|
|
1491
|
+
];
|
|
1492
|
+
|
|
1142
1493
|
function extractTechStackPlaceholders(text) {
|
|
1143
1494
|
if (!text || text.toLowerCase() === 'unknown') {
|
|
1144
1495
|
return {};
|
|
1145
1496
|
}
|
|
1146
1497
|
|
|
1147
1498
|
return {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1499
|
+
language: firstPatternMatch(text, LANGUAGE_PATTERNS),
|
|
1500
|
+
framework: firstPatternMatch(text, FRAMEWORK_PATTERNS),
|
|
1501
|
+
frameworkVersion: firstPatternMatch(text, FRAMEWORK_VERSION_PATTERNS),
|
|
1502
|
+
ormPersistence: firstPatternMatch(text, ORM_PERSISTENCE_PATTERNS),
|
|
1503
|
+
ormVersion: firstPatternMatch(text, ORM_VERSION_PATTERNS),
|
|
1504
|
+
mediator: firstMatch(text, /\bMediatR\s*[0-9]+(?:\.[0-9]+)?\b/i),
|
|
1505
|
+
testFramework: extractTestFramework(text)
|
|
1155
1506
|
};
|
|
1156
1507
|
}
|
|
1157
1508
|
|
|
@@ -1160,15 +1511,38 @@ function firstMatch(text, regex) {
|
|
|
1160
1511
|
return match ? match[0] : null;
|
|
1161
1512
|
}
|
|
1162
1513
|
|
|
1163
|
-
function
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1514
|
+
function firstPatternMatch(text, patterns) {
|
|
1515
|
+
for (const pattern of patterns) {
|
|
1516
|
+
const match = text.match(pattern);
|
|
1517
|
+
if (match) {
|
|
1518
|
+
return match[0];
|
|
1519
|
+
}
|
|
1169
1520
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function extractTestFramework(text) {
|
|
1525
|
+
const patterns = [
|
|
1526
|
+
[/\bxUnit\b/i, 'xUnit'],
|
|
1527
|
+
[/\bNUnit\b/i, 'NUnit'],
|
|
1528
|
+
[/\bMSTest\b/i, 'MSTest'],
|
|
1529
|
+
[/\bJUnit\s*[0-9]+\b/i, null],
|
|
1530
|
+
[/\bJUnit\b/i, 'JUnit'],
|
|
1531
|
+
[/\bVitest\b/i, 'Vitest'],
|
|
1532
|
+
[/\bJest\b/i, 'Jest'],
|
|
1533
|
+
[/\bMocha\b/i, 'Mocha'],
|
|
1534
|
+
[/\bpytest\b/i, 'pytest'],
|
|
1535
|
+
[/\bunittest\b/i, 'unittest'],
|
|
1536
|
+
[/\bgo\s+test\b/i, 'go test'],
|
|
1537
|
+
[/\bPHPUnit\b/i, 'PHPUnit'],
|
|
1538
|
+
[/\bPest\b/i, 'Pest']
|
|
1539
|
+
];
|
|
1540
|
+
|
|
1541
|
+
for (const [pattern, name] of patterns) {
|
|
1542
|
+
const match = text.match(pattern);
|
|
1543
|
+
if (match) {
|
|
1544
|
+
return name || match[0];
|
|
1545
|
+
}
|
|
1172
1546
|
}
|
|
1173
1547
|
return null;
|
|
1174
1548
|
}
|
|
@@ -1271,6 +1645,7 @@ function renderPreview(stdout, plan, warnings) {
|
|
|
1271
1645
|
async function writeFilePlan(cwd, plan) {
|
|
1272
1646
|
const result = {
|
|
1273
1647
|
created: [],
|
|
1648
|
+
updated: [],
|
|
1274
1649
|
skipped: [],
|
|
1275
1650
|
warnings: []
|
|
1276
1651
|
};
|
|
@@ -1280,6 +1655,13 @@ async function writeFilePlan(cwd, plan) {
|
|
|
1280
1655
|
|
|
1281
1656
|
try {
|
|
1282
1657
|
if (await pathExists(targetPath)) {
|
|
1658
|
+
if (item.overwrite) {
|
|
1659
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1660
|
+
await fs.writeFile(targetPath, item.content);
|
|
1661
|
+
result.updated.push(item.relativePath);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1283
1665
|
result.skipped.push(item.relativePath);
|
|
1284
1666
|
result.warnings.push(`Skipped existing target: ${item.relativePath}`);
|
|
1285
1667
|
if (item.relativePath === 'dflow/specs/shared/_conventions.md') {
|
|
@@ -1366,6 +1748,9 @@ function printResultReport(stdout, result, deferred) {
|
|
|
1366
1748
|
stdout.write('\nCreated:\n');
|
|
1367
1749
|
printList(stdout, result.created);
|
|
1368
1750
|
|
|
1751
|
+
stdout.write('\nUpdated:\n');
|
|
1752
|
+
printList(stdout, result.updated);
|
|
1753
|
+
|
|
1369
1754
|
stdout.write('\nSkipped:\n');
|
|
1370
1755
|
printList(stdout, result.skipped);
|
|
1371
1756
|
|
|
@@ -1386,10 +1771,15 @@ Recommended next steps:
|
|
|
1386
1771
|
- For a new feature, use the Dflow new-feature workflow when it becomes available as a CLI command.
|
|
1387
1772
|
- For brownfield changes, use the Dflow modify-existing workflow when it becomes available as a CLI command.
|
|
1388
1773
|
- Before generating more specs, make sure dflow/specs/shared/_conventions.md has the correct Prose Language section.
|
|
1774
|
+
- For stack-specific examples (.NET, Java/Spring, Node/TypeScript, Python, Go, PHP/Laravel), see docs/examples-by-stack.md in the Dflow repo.
|
|
1389
1775
|
`);
|
|
1390
1776
|
}
|
|
1391
1777
|
|
|
1392
|
-
function printConfigureAgentsNextSteps(stdout) {
|
|
1778
|
+
function printConfigureAgentsNextSteps(stdout, commandAdapters = false) {
|
|
1779
|
+
const commandAdapterStep = commandAdapters
|
|
1780
|
+
? '- Command adapters use adapter-native names such as `/dflow-new-feature`; canonical `/dflow:*` names remain defined in dflow/specs/shared/AI-AGENT-GUIDE.md.\n'
|
|
1781
|
+
: '';
|
|
1782
|
+
|
|
1393
1783
|
stdout.write(`
|
|
1394
1784
|
Dflow AI agent configuration complete.
|
|
1395
1785
|
|
|
@@ -1397,6 +1787,7 @@ Recommended next steps:
|
|
|
1397
1787
|
- Keep AI-agent-specific root files small.
|
|
1398
1788
|
- Put durable workflow changes in dflow/specs/shared/AI-AGENT-GUIDE.md.
|
|
1399
1789
|
- If a merge snippet was created, review it and merge the pointer into the existing tool instruction file.
|
|
1790
|
+
${commandAdapterStep}
|
|
1400
1791
|
`);
|
|
1401
1792
|
}
|
|
1402
1793
|
|