@entelligentsia/forgecli 0.1.0 → 0.3.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +177 -38
  3. package/dist/bin/argv.js +5 -0
  4. package/dist/bin/argv.js.map +1 -1
  5. package/dist/bin/forge.js +1 -0
  6. package/dist/bin/forge.js.map +1 -1
  7. package/dist/extensions/forgecli/ask-user-tool.d.ts +17 -0
  8. package/dist/extensions/forgecli/ask-user-tool.js +139 -0
  9. package/dist/extensions/forgecli/ask-user-tool.js.map +1 -0
  10. package/dist/extensions/forgecli/forge-commands.d.ts +21 -0
  11. package/dist/extensions/forgecli/forge-commands.js +141 -0
  12. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-init.d.ts +26 -0
  14. package/dist/extensions/forgecli/forge-init.js +948 -0
  15. package/dist/extensions/forgecli/forge-init.js.map +1 -0
  16. package/dist/extensions/forgecli/health-check.d.ts +18 -0
  17. package/dist/extensions/forgecli/health-check.js +154 -0
  18. package/dist/extensions/forgecli/health-check.js.map +1 -0
  19. package/dist/extensions/forgecli/hook-dispatcher.d.ts +34 -1
  20. package/dist/extensions/forgecli/hook-dispatcher.js +237 -3
  21. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  22. package/dist/extensions/forgecli/index.js +28 -11
  23. package/dist/extensions/forgecli/index.js.map +1 -1
  24. package/dist/extensions/forgecli/init-context.d.ts +99 -0
  25. package/dist/extensions/forgecli/init-context.js +163 -0
  26. package/dist/extensions/forgecli/init-context.js.map +1 -0
  27. package/dist/extensions/forgecli/init-progress.d.ts +39 -0
  28. package/dist/extensions/forgecli/init-progress.js +117 -0
  29. package/dist/extensions/forgecli/init-progress.js.map +1 -0
  30. package/dist/extensions/forgecli/refresh-kb-links.d.ts +18 -0
  31. package/dist/extensions/forgecli/refresh-kb-links.js +228 -0
  32. package/dist/extensions/forgecli/refresh-kb-links.js.map +1 -0
  33. package/dist/extensions/forgecli/store-validator.d.ts +13 -0
  34. package/dist/extensions/forgecli/store-validator.js +35 -0
  35. package/dist/extensions/forgecli/store-validator.js.map +1 -0
  36. package/dist/extensions/forgecli/transition-guard.d.ts +20 -0
  37. package/dist/extensions/forgecli/transition-guard.js +125 -0
  38. package/dist/extensions/forgecli/transition-guard.js.map +1 -0
  39. package/dist/forge-payload/.base-pack/commands/approve.md +22 -0
  40. package/dist/forge-payload/.base-pack/commands/collate.md +22 -0
  41. package/dist/forge-payload/.base-pack/commands/commit.md +22 -0
  42. package/dist/forge-payload/.base-pack/commands/enhance.md +37 -0
  43. package/dist/forge-payload/.base-pack/commands/fix-bug.md +22 -0
  44. package/dist/forge-payload/.base-pack/commands/implement.md +22 -0
  45. package/dist/forge-payload/.base-pack/commands/plan.md +22 -0
  46. package/dist/forge-payload/.base-pack/commands/quiz-agent.md +22 -0
  47. package/dist/forge-payload/.base-pack/commands/retrospective.md +22 -0
  48. package/dist/forge-payload/.base-pack/commands/review-code.md +22 -0
  49. package/dist/forge-payload/.base-pack/commands/review-plan.md +22 -0
  50. package/dist/forge-payload/.base-pack/commands/run-sprint.md +22 -0
  51. package/dist/forge-payload/.base-pack/commands/run-task.md +22 -0
  52. package/dist/forge-payload/.base-pack/commands/sprint-intake.md +22 -0
  53. package/dist/forge-payload/.base-pack/commands/sprint-plan.md +22 -0
  54. package/dist/forge-payload/.base-pack/commands/validate.md +22 -0
  55. package/dist/forge-payload/.claude-plugin/plugin.json +15 -0
  56. package/dist/forge-payload/.init/discovery/discover-database.md +32 -0
  57. package/dist/forge-payload/.init/discovery/discover-processes.md +31 -0
  58. package/dist/forge-payload/.init/discovery/discover-routing.md +31 -0
  59. package/dist/forge-payload/.init/discovery/discover-stack.md +33 -0
  60. package/dist/forge-payload/.init/discovery/discover-testing.md +34 -0
  61. package/dist/forge-payload/.init/generation/generate-kb-doc.md +60 -0
  62. package/dist/forge-payload/.schemas/bug.schema.json +53 -0
  63. package/dist/forge-payload/.schemas/collation-state.schema.json +16 -0
  64. package/dist/forge-payload/.schemas/event-sidecar.schema.json +22 -0
  65. package/dist/forge-payload/.schemas/event.schema.json +32 -0
  66. package/dist/forge-payload/.schemas/feature.schema.json +22 -0
  67. package/dist/forge-payload/.schemas/progress-entry.schema.json +16 -0
  68. package/dist/forge-payload/.schemas/project-context.schema.json +167 -0
  69. package/dist/forge-payload/.schemas/project-overlay.schema.json +25 -0
  70. package/dist/forge-payload/.schemas/sprint.schema.json +27 -0
  71. package/dist/forge-payload/.schemas/structure-versions.schema.json +57 -0
  72. package/dist/forge-payload/.schemas/task.schema.json +58 -0
  73. package/dist/forge-payload/.tools/banners.cjs +435 -0
  74. package/dist/forge-payload/.tools/build-context-pack.cjs +290 -0
  75. package/dist/forge-payload/.tools/build-init-context.cjs +322 -0
  76. package/dist/forge-payload/.tools/build-overlay.cjs +326 -0
  77. package/dist/forge-payload/.tools/build-persona-pack.cjs +226 -0
  78. package/dist/forge-payload/.tools/collate.cjs +1041 -0
  79. package/dist/forge-payload/.tools/generation-manifest.cjs +311 -0
  80. package/dist/forge-payload/.tools/lib/forge-root.cjs +59 -0
  81. package/dist/forge-payload/.tools/lib/paths.cjs +29 -0
  82. package/dist/forge-payload/.tools/lib/pricing.cjs +165 -0
  83. package/dist/forge-payload/.tools/lib/project-root.cjs +32 -0
  84. package/dist/forge-payload/.tools/lib/result.js +40 -0
  85. package/dist/forge-payload/.tools/lib/validate.js +131 -0
  86. package/dist/forge-payload/.tools/manage-config.cjs +340 -0
  87. package/dist/forge-payload/.tools/manage-versions.cjs +365 -0
  88. package/dist/forge-payload/.tools/seed-store.cjs +237 -0
  89. package/dist/forge-payload/.tools/store-cli.cjs +1123 -0
  90. package/dist/forge-payload/.tools/store.cjs +315 -0
  91. package/dist/forge-payload/.tools/substitute-placeholders.cjs +625 -0
  92. package/dist/forge-payload/.tools/validate-store.cjs +522 -0
  93. package/package.json +1 -1
  94. /package/dist/forge-payload/{personas → .base-pack/personas}/architect.md +0 -0
  95. /package/dist/forge-payload/{personas → .base-pack/personas}/bug-fixer.md +0 -0
  96. /package/dist/forge-payload/{personas → .base-pack/personas}/collator.md +0 -0
  97. /package/dist/forge-payload/{personas → .base-pack/personas}/engineer.md +0 -0
  98. /package/dist/forge-payload/{personas → .base-pack/personas}/librarian.md +0 -0
  99. /package/dist/forge-payload/{personas → .base-pack/personas}/orchestrator.md +0 -0
  100. /package/dist/forge-payload/{personas → .base-pack/personas}/product-manager.md +0 -0
  101. /package/dist/forge-payload/{personas → .base-pack/personas}/qa-engineer.md +0 -0
  102. /package/dist/forge-payload/{personas → .base-pack/personas}/supervisor.md +0 -0
  103. /package/dist/forge-payload/{skills → .base-pack/skills}/architect-skills.md +0 -0
  104. /package/dist/forge-payload/{skills → .base-pack/skills}/bug-fixer-skills.md +0 -0
  105. /package/dist/forge-payload/{skills → .base-pack/skills}/collator-skills.md +0 -0
  106. /package/dist/forge-payload/{skills → .base-pack/skills}/engineer-skills.md +0 -0
  107. /package/dist/forge-payload/{skills → .base-pack/skills}/generic-skills.md +0 -0
  108. /package/dist/forge-payload/{skills → .base-pack/skills}/librarian-skills.md +0 -0
  109. /package/dist/forge-payload/{skills → .base-pack/skills}/qa-engineer-skills.md +0 -0
  110. /package/dist/forge-payload/{skills → .base-pack/skills}/store-custodian-skills.md +0 -0
  111. /package/dist/forge-payload/{skills → .base-pack/skills}/supervisor-skills.md +0 -0
  112. /package/dist/forge-payload/{templates → .base-pack/templates}/CODE_REVIEW_TEMPLATE.md +0 -0
  113. /package/dist/forge-payload/{templates → .base-pack/templates}/COST_REPORT_TEMPLATE.md +0 -0
  114. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_REVIEW_TEMPLATE.md +0 -0
  115. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_SUMMARY_TEMPLATE.json +0 -0
  116. /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_TEMPLATE.md +0 -0
  117. /package/dist/forge-payload/{templates → .base-pack/templates}/PROGRESS_TEMPLATE.md +0 -0
  118. /package/dist/forge-payload/{templates → .base-pack/templates}/RETROSPECTIVE_TEMPLATE.md +0 -0
  119. /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_MANIFEST_TEMPLATE.md +0 -0
  120. /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_REQUIREMENTS_TEMPLATE.md +0 -0
  121. /package/dist/forge-payload/{templates → .base-pack/templates}/TASK_PROMPT_TEMPLATE.md +0 -0
  122. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/context-injection.md +0 -0
  123. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/event-emission-schema.md +0 -0
  124. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/finalize.md +0 -0
  125. /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/progress-reporting.md +0 -0
  126. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_approve.md +0 -0
  127. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_review_sprint_completion.md +0 -0
  128. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_intake.md +0 -0
  129. /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_plan.md +0 -0
  130. /package/dist/forge-payload/{workflows → .base-pack/workflows}/collator_agent.md +0 -0
  131. /package/dist/forge-payload/{workflows → .base-pack/workflows}/commit_task.md +0 -0
  132. /package/dist/forge-payload/{workflows → .base-pack/workflows}/fix_bug.md +0 -0
  133. /package/dist/forge-payload/{workflows → .base-pack/workflows}/implement_plan.md +0 -0
  134. /package/dist/forge-payload/{workflows → .base-pack/workflows}/migrate_structural.md +0 -0
  135. /package/dist/forge-payload/{workflows → .base-pack/workflows}/orchestrate_task.md +0 -0
  136. /package/dist/forge-payload/{workflows → .base-pack/workflows}/plan_task.md +0 -0
  137. /package/dist/forge-payload/{workflows → .base-pack/workflows}/quiz_agent.md +0 -0
  138. /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_code.md +0 -0
  139. /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_plan.md +0 -0
  140. /package/dist/forge-payload/{workflows → .base-pack/workflows}/run_sprint.md +0 -0
  141. /package/dist/forge-payload/{workflows → .base-pack/workflows}/sprint_retrospective.md +0 -0
  142. /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_implementation.md +0 -0
  143. /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_plan.md +0 -0
  144. /package/dist/forge-payload/{workflows → .base-pack/workflows}/validate_task.md +0 -0
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Forge tool: manage-versions
5
+ // Manage .forge/structure-versions.json — snapshot lifecycle for structural elements.
6
+ //
7
+ // Subcommands:
8
+ // init Write snapshot 0 at first init (idempotent: no-op if file exists)
9
+ // current Print current snapshot index and metadata
10
+ // list Print tabular summary of all snapshots
11
+ // add-snapshot Archive current structural elements and record a new snapshot entry
12
+ //
13
+ // Composition model: Working Artifact = base@pluginVersion + snapshot@currentSnapshot + user_enhancements
14
+ // Snapshot-array invariant: snapshots is ordered ascending by index; currentSnapshot always equals
15
+ // snapshots[snapshots.length - 1].index.
16
+ //
17
+ // Usage:
18
+ // node manage-versions.cjs init [--dry-run]
19
+ // node manage-versions.cjs current
20
+ // node manage-versions.cjs list
21
+ // node manage-versions.cjs add-snapshot --source <source> [--enhanced-elements <csv>] [--dry-run]
22
+ // --source <string> Required. One of: post-init | post-sprint:<ID> | on-demand
23
+ // --enhanced-elements <csv> Optional. Comma-separated list of .forge/-relative paths that were enhanced.
24
+ // --dry-run Log intent without performing I/O.
25
+ //
26
+ // Environment:
27
+ // FORGE_ROOT — path to forge plugin root (used by init to locate plugin.json and schemas)
28
+ // Falls back to auto-detection via ../../ from __dirname when FORGE_ROOT is unset.
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { resolveForgeRoot } = require('./lib/forge-root.cjs');
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Constants
36
+ // ---------------------------------------------------------------------------
37
+
38
+ // Relative path suffix for structure-versions.json (from project root)
39
+ const VERSIONS_SUFFIX = path.join('.forge', 'structure-versions.json');
40
+
41
+ // Default project root is cwd when used as CLI; exports allow test injection.
42
+ const VERSIONS_PATH = path.join(process.cwd(), VERSIONS_SUFFIX);
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Exported helpers (used by unit tests and callers)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Resolve the path to structure-versions.json for the given project root.
50
+ * @param {string} projectRoot
51
+ * @returns {string}
52
+ */
53
+ function versionsPath(projectRoot) {
54
+ return path.join(projectRoot, VERSIONS_SUFFIX);
55
+ }
56
+
57
+ /**
58
+ * Read and parse .forge/structure-versions.json.
59
+ * @param {string} projectRoot
60
+ * @returns {object} parsed document
61
+ * @throws {Error} when file does not exist or cannot be parsed
62
+ */
63
+ function readStructureVersions(projectRoot) {
64
+ const filePath = versionsPath(projectRoot);
65
+ if (!fs.existsSync(filePath)) {
66
+ throw new Error(`structure-versions.json not found at ${filePath}. Run \`manage-versions init\` first.`);
67
+ }
68
+ try {
69
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
70
+ } catch (e) {
71
+ throw new Error(`Failed to parse structure-versions.json at ${filePath}: ${e.message}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Write data to .forge/structure-versions.json atomically via .tmp.PID rename.
77
+ * @param {string} projectRoot
78
+ * @param {object} data
79
+ */
80
+ function writeStructureVersions(projectRoot, data) {
81
+ const filePath = versionsPath(projectRoot);
82
+ const json = JSON.stringify(data, null, 2) + '\n';
83
+ const tmp = filePath + '.tmp.' + process.pid;
84
+ try {
85
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
+ fs.writeFileSync(tmp, json, 'utf8');
87
+ fs.renameSync(tmp, filePath);
88
+ } catch (e) {
89
+ try { fs.unlinkSync(tmp); } catch {}
90
+ throw new Error(`Failed to write structure-versions.json: ${e.message}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Resolve forge root from env or __dirname fallback.
96
+ * Delegates to the shared forge-root.cjs helper (FR-001).
97
+ * @param {string} [envForgeRoot] - value of FORGE_ROOT env var
98
+ * @returns {string}
99
+ */
100
+
101
+ /**
102
+ * Read the plugin version from FORGE_ROOT/.claude-plugin/plugin.json.
103
+ * @param {string} forgeRoot
104
+ * @returns {string}
105
+ */
106
+ function readPluginVersion(forgeRoot) {
107
+ const pluginPath = path.join(forgeRoot, '.claude-plugin', 'plugin.json');
108
+ try {
109
+ const pkg = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
110
+ if (!pkg.version) throw new Error('version field missing');
111
+ return pkg.version;
112
+ } catch (e) {
113
+ throw new Error(`Failed to read plugin version from ${pluginPath}: ${e.message}`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Read the overlay tool version from FORGE_ROOT/schemas/project-overlay.schema.json.
119
+ * Falls back to "1.0.0" if the schema does not contain a version field.
120
+ * @param {string} forgeRoot
121
+ * @returns {string}
122
+ */
123
+ function readOverlayToolVersion(forgeRoot) {
124
+ const schemaPath = path.join(forgeRoot, 'schemas', 'project-overlay.schema.json');
125
+ try {
126
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
127
+ if (typeof schema.version === 'string' && schema.version) {
128
+ return schema.version;
129
+ }
130
+ } catch {
131
+ // Schema unreadable — fall back
132
+ }
133
+ return '1.0.0';
134
+ }
135
+
136
+ // Structural element directories that are eligible to be archived.
137
+ const STRUCTURAL_ELEMENT_DIRS = ['personas', 'skills', 'workflows', 'templates'];
138
+
139
+ /**
140
+ * Copy a file, creating intermediate directories as needed.
141
+ * @param {string} src
142
+ * @param {string} dest
143
+ */
144
+ function copyFileWithDirs(src, dest) {
145
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
146
+ fs.copyFileSync(src, dest);
147
+ }
148
+
149
+ /**
150
+ * Add a new snapshot entry to structure-versions.json and archive the
151
+ * current structural elements listed in enhancedElements.
152
+ *
153
+ * @param {string} projectRoot - path to the project root (where .forge/ lives)
154
+ * @param {string} source - snapshot source label (post-init | post-sprint:<ID> | on-demand)
155
+ * @param {string[]} enhancedElements - list of .forge/-relative paths that were enhanced
156
+ * @param {boolean} [dryRun] - when true, log intent but perform no I/O
157
+ */
158
+ function addSnapshot(projectRoot, source, enhancedElements, dryRun) {
159
+ if (!source) {
160
+ throw new Error('--source is required for add-snapshot. Provide one of: post-init | post-sprint:<ID> | on-demand');
161
+ }
162
+
163
+ const doc = readStructureVersions(projectRoot);
164
+ const nextIndex = doc.currentSnapshot + 1;
165
+ const archivePath = path.join('.forge', 'archive', `snap-${nextIndex}`);
166
+ const archiveAbsPath = path.join(projectRoot, archivePath);
167
+
168
+ // Guard: fail if archive directory already exists to prevent corruption.
169
+ if (fs.existsSync(archiveAbsPath)) {
170
+ throw new Error(
171
+ `Archive directory already exists: ${archiveAbsPath} (snap-${nextIndex}). ` +
172
+ 'Cannot create snapshot — remove or rename the existing archive directory first.'
173
+ );
174
+ }
175
+
176
+ const createdAt = new Date().toISOString();
177
+ const newSnapshot = {
178
+ index: nextIndex,
179
+ createdAt,
180
+ source,
181
+ enhancedElements: enhancedElements || [],
182
+ archivePath
183
+ };
184
+
185
+ if (dryRun) {
186
+ console.log(`[dry-run] Would create archive at: ${archiveAbsPath}`);
187
+ console.log(`[dry-run] Would archive ${(enhancedElements || []).length} element(s).`);
188
+ console.log(`[dry-run] Would write snapshot entry:`);
189
+ console.log(JSON.stringify(newSnapshot, null, 2));
190
+ return;
191
+ }
192
+
193
+ // Archive each enhanced element by copying from .forge/ into the archive dir.
194
+ for (const relPath of (enhancedElements || [])) {
195
+ const srcPath = path.join(projectRoot, '.forge', relPath);
196
+ const destPath = path.join(archiveAbsPath, relPath);
197
+ if (fs.existsSync(srcPath)) {
198
+ copyFileWithDirs(srcPath, destPath);
199
+ }
200
+ // If the source file does not exist, skip silently — the element list
201
+ // may reference files that were removed or renamed; archiving what's there
202
+ // is better than failing the whole snapshot.
203
+ }
204
+
205
+ // Also create the archive directory even if no elements were listed,
206
+ // so archivePath references a real directory.
207
+ fs.mkdirSync(archiveAbsPath, { recursive: true });
208
+
209
+ // Append snapshot entry and advance currentSnapshot.
210
+ doc.snapshots.push(newSnapshot);
211
+ doc.currentSnapshot = nextIndex;
212
+ writeStructureVersions(projectRoot, doc);
213
+
214
+ console.log(`ノ add-snapshot complete — snapshot ${nextIndex} written (source: ${source}, elements: ${(enhancedElements || []).length})`);
215
+ }
216
+
217
+ /**
218
+ * Initialise structure-versions.json with snapshot 0.
219
+ * Idempotent: if the file already exists, exits cleanly without overwriting.
220
+ *
221
+ * @param {string} projectRoot - path to the project root (where .forge/ lives)
222
+ * @param {string} forgeRoot - path to the forge plugin root
223
+ * @param {boolean} [dryRun] - when true, log intent but perform no I/O
224
+ * @param {string} [source] - source label for snapshot 0 (default: 'base-pack')
225
+ */
226
+ function initStructureVersions(projectRoot, forgeRoot, dryRun, source) {
227
+ const filePath = versionsPath(projectRoot);
228
+
229
+ // Idempotency: if the file already exists, do nothing.
230
+ if (fs.existsSync(filePath)) {
231
+ console.log(`〇 structure-versions.json already exists — skipping (idempotent).`);
232
+ return;
233
+ }
234
+
235
+ const effectiveSource = source || 'base-pack';
236
+ const basePackVersion = readPluginVersion(forgeRoot);
237
+ const overlayToolVersion = readOverlayToolVersion(forgeRoot);
238
+
239
+ const doc = {
240
+ basePackVersion,
241
+ overlayToolVersion,
242
+ currentSnapshot: 0,
243
+ snapshots: [
244
+ {
245
+ index: 0,
246
+ createdAt: new Date().toISOString(),
247
+ source: effectiveSource,
248
+ enhancedElements: [],
249
+ archivePath: null
250
+ }
251
+ ]
252
+ };
253
+
254
+ if (dryRun) {
255
+ console.log('[dry-run] Would write structure-versions.json:');
256
+ console.log(JSON.stringify(doc, null, 2));
257
+ return;
258
+ }
259
+
260
+ writeStructureVersions(projectRoot, doc);
261
+ console.log(`ノ structure-versions.json written (snapshot 0, source: ${effectiveSource}, plugin: v${basePackVersion})`);
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Exports (for unit tests)
266
+ // ---------------------------------------------------------------------------
267
+
268
+ module.exports = {
269
+ initStructureVersions,
270
+ addSnapshot,
271
+ readStructureVersions,
272
+ writeStructureVersions,
273
+ VERSIONS_PATH,
274
+ versionsPath,
275
+ readPluginVersion,
276
+ readOverlayToolVersion,
277
+ };
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // CLI
281
+ // ---------------------------------------------------------------------------
282
+
283
+ if (require.main === module) {
284
+
285
+ process.on('uncaughtException', (error) => {
286
+ console.error('Fatal manage-versions error:', error.message);
287
+ process.exit(1);
288
+ });
289
+
290
+ const args = process.argv.slice(2);
291
+ const DRY_RUN = args.includes('--dry-run');
292
+ const subcommand = args.find(a => !a.startsWith('--'));
293
+
294
+ const forgeRoot = resolveForgeRoot(process.env.FORGE_ROOT);
295
+ const projectRoot = process.cwd();
296
+
297
+ try {
298
+ switch (subcommand) {
299
+ case 'init': {
300
+ const sourceIdx = args.indexOf('--source');
301
+ const initSource = sourceIdx !== -1 ? args[sourceIdx + 1] : undefined;
302
+ initStructureVersions(projectRoot, forgeRoot, DRY_RUN, initSource);
303
+ break;
304
+ }
305
+
306
+ case 'current': {
307
+ const doc = readStructureVersions(projectRoot);
308
+ const snap = doc.snapshots.find(s => s.index === doc.currentSnapshot);
309
+ console.log(`Current snapshot: ${doc.currentSnapshot}`);
310
+ if (snap) {
311
+ console.log(` source: ${snap.source}`);
312
+ console.log(` createdAt: ${snap.createdAt}`);
313
+ console.log(` plugin: v${doc.basePackVersion}`);
314
+ }
315
+ break;
316
+ }
317
+
318
+ case 'list': {
319
+ const doc = readStructureVersions(projectRoot);
320
+ console.log(`Snapshots (current: ${doc.currentSnapshot}):`);
321
+ console.log(`${'#'.padEnd(4)} ${'source'.padEnd(20)} ${'createdAt'.padEnd(28)} elements`);
322
+ console.log('-'.repeat(70));
323
+ for (const snap of doc.snapshots) {
324
+ const current = snap.index === doc.currentSnapshot ? '*' : ' ';
325
+ const elems = snap.enhancedElements ? snap.enhancedElements.length : 0;
326
+ console.log(`${current}${String(snap.index).padEnd(3)} ${snap.source.padEnd(20)} ${snap.createdAt.padEnd(28)} ${elems}`);
327
+ }
328
+ break;
329
+ }
330
+
331
+ case 'add-snapshot': {
332
+ // Parse --source flag
333
+ const sourceIdx = args.indexOf('--source');
334
+ const source = sourceIdx !== -1 ? args[sourceIdx + 1] : null;
335
+ if (!source || source.startsWith('--')) {
336
+ console.error('× add-snapshot requires --source <value>.');
337
+ console.error(' Accepted values: post-init | post-sprint:<SPRINT_ID> | on-demand');
338
+ process.exit(1);
339
+ }
340
+
341
+ // Parse optional --enhanced-elements flag (comma-separated list)
342
+ const elementsIdx = args.indexOf('--enhanced-elements');
343
+ let enhancedElements = [];
344
+ if (elementsIdx !== -1) {
345
+ const raw = args[elementsIdx + 1];
346
+ if (raw && !raw.startsWith('--')) {
347
+ enhancedElements = raw.split(',').map(s => s.trim()).filter(Boolean);
348
+ }
349
+ }
350
+
351
+ addSnapshot(projectRoot, source, enhancedElements, DRY_RUN);
352
+ break;
353
+ }
354
+
355
+ default: {
356
+ console.error(`× Unknown subcommand: ${subcommand || '(none)'}`);
357
+ console.error(' Usage: manage-versions.cjs <init|current|list|add-snapshot> [--dry-run]');
358
+ process.exit(1);
359
+ }
360
+ }
361
+ } catch (err) {
362
+ console.error(`× manage-versions error: ${err.message}`);
363
+ process.exit(1);
364
+ }
365
+ }
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Forge tool: seed-store
5
+ // Bootstrap the JSON store from an existing engineering/ directory structure.
6
+ // Supports slug-named directories (e.g., FORGE-S06-T07-slug-aware-seed-store/)
7
+ // as well as legacy bare-ID directories (e.g., S01/, T01/, B01/).
8
+ // Usage: seed-store [--dry-run]
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const Store = require('./store.cjs');
13
+
14
+ const DRY_RUN = process.argv.includes('--dry-run');
15
+
16
+ /**
17
+ * Convert a title string to a lower-kebab-case slug, truncated to 30 chars.
18
+ * Non-alphanumeric characters are collapsed to a single hyphen.
19
+ */
20
+ function deriveSlug(title) {
21
+ return title
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9]+/g, '-')
24
+ .replace(/^-+|-+$/g, '')
25
+ .slice(0, 30)
26
+ .replace(/-+$/g, ''); // trim trailing hyphens after truncation
27
+ }
28
+
29
+ // ── Exports for testing ────────────────────────────────────────────────────────
30
+
31
+ module.exports = { deriveSlug };
32
+
33
+ // ── CLI (only when run directly) ──────────────────────────────────────────────
34
+
35
+ if (require.main === module) {
36
+ const cwd = process.cwd();
37
+
38
+ function extractTitle(dir, fallback) {
39
+ for (const file of ['PLAN.md', 'PROGRESS.md', 'INDEX.md', 'README.md']) {
40
+ const p = path.join(dir, file);
41
+ if (!fs.existsSync(p)) continue;
42
+ const m = fs.readFileSync(p, 'utf8').match(/^#\s+(.+)/m);
43
+ if (m) return m[1].trim();
44
+ }
45
+ return fallback;
46
+ }
47
+
48
+ function inferTaskStatus(taskDir) {
49
+ const p = path.join(taskDir, 'PROGRESS.md');
50
+ if (!fs.existsSync(p)) return 'planned';
51
+ const content = fs.readFileSync(p, 'utf8').toLowerCase();
52
+ if (content.includes('committed')) return 'committed';
53
+ if (content.includes('approved')) return 'approved';
54
+ if (content.includes('implemented')) return 'implemented';
55
+ if (content.includes('implementing')) return 'implementing';
56
+ return 'planned';
57
+ }
58
+
59
+ function inferSprintStatus(sprintPath, taskDirs) {
60
+ if (taskDirs.length === 0) return 'planning';
61
+ const allCommitted = taskDirs.every(t => inferTaskStatus(path.join(sprintPath, t)) === 'committed');
62
+ return allCommitted ? 'completed' : 'active';
63
+ }
64
+
65
+ try {
66
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, '.forge', 'config.json'), 'utf8'));
67
+ const prefix = config.project?.prefix || 'PROJ';
68
+ const engPath = config.paths?.engineering || 'engineering';
69
+
70
+ const sprintsDir = path.join(cwd, engPath, 'sprints');
71
+ const bugsDir = path.join(cwd, engPath, 'bugs');
72
+ const featuresDir = path.join(cwd, engPath, 'features');
73
+
74
+ let sprintCount = 0, taskCount = 0, bugCount = 0;
75
+
76
+ // --- Scaffold features/ ---
77
+ if (!DRY_RUN) {
78
+ if (!fs.existsSync(featuresDir)) {
79
+ fs.mkdirSync(featuresDir, { recursive: true });
80
+ }
81
+ } else {
82
+ if (!fs.existsSync(featuresDir)) {
83
+ console.log(`[dry-run] would scaffold directory: ${path.relative(cwd, featuresDir)}/`);
84
+ }
85
+ }
86
+
87
+ // --- Sprints and Tasks ---
88
+ if (fs.existsSync(sprintsDir)) {
89
+ // Three-tier sprint discovery:
90
+ // 1. {PREFIX}-S{NN}-* (slug-named, e.g. FORGE-S06-post-07-feedback)
91
+ // 2. {PREFIX}-S{NN} (full ID, no slug, e.g. FORGE-S06)
92
+ // 3. S{NN} (bare legacy, e.g. S01)
93
+ const prefixEscaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
+ const slugSprintRe = new RegExp(`^${prefixEscaped}-(S\\d+)-.+$`, 'i');
95
+ const fullSprintRe = new RegExp(`^${prefixEscaped}-(S\\d+)$`, 'i');
96
+ const bareSprintRe = /^S\d+$/i;
97
+
98
+ const sprintDirs = fs.readdirSync(sprintsDir)
99
+ .filter(e => fs.statSync(path.join(sprintsDir, e)).isDirectory())
100
+ .map(e => {
101
+ let match;
102
+ if ((match = e.match(slugSprintRe))) {
103
+ return { dirName: e, sprintNum: match[1], sortKey: parseInt(match[1].slice(1), 10) };
104
+ }
105
+ if ((match = e.match(fullSprintRe))) {
106
+ return { dirName: e, sprintNum: match[1], sortKey: parseInt(match[1].slice(1), 10) };
107
+ }
108
+ if ((match = e.match(bareSprintRe))) {
109
+ return { dirName: e, sprintNum: match[0].toUpperCase(), sortKey: parseInt(match[0].slice(1), 10) };
110
+ }
111
+ return null;
112
+ })
113
+ .filter(Boolean)
114
+ .sort((a, b) => a.sortKey - b.sortKey);
115
+
116
+ for (const { dirName: sprintDir, sprintNum } of sprintDirs) {
117
+ const sprintFullPath = path.join(sprintsDir, sprintDir);
118
+ const sprintId = `${prefix}-${sprintNum.toUpperCase()}`;
119
+
120
+ // Three-tier task discovery:
121
+ // 1. T{NN}-* (bare task ID with slug, e.g. T01-fix-persona-lookup)
122
+ // 2. {PREFIX}-S{NN}-T{NN}-* (full task ID with slug, e.g. FORGE-S06-T01-fix-persona)
123
+ // 3. T{NN} (bare legacy, e.g. T01)
124
+ const bareTaskSlugRe = new RegExp(`^T(\\d+)-.+$`, 'i');
125
+ const fullTaskSlugRe = new RegExp(`^${prefixEscaped}-${sprintNum.toUpperCase()}-T(\\d+)-.+$`, 'i');
126
+ const bareTaskRe = /^T(\d+)$/i;
127
+
128
+ const taskDirs = fs.readdirSync(sprintFullPath)
129
+ .filter(e => fs.statSync(path.join(sprintFullPath, e)).isDirectory())
130
+ .map(e => {
131
+ let match;
132
+ if ((match = e.match(fullTaskSlugRe))) {
133
+ return { dirName: e, taskNum: match[1], sortKey: parseInt(match[1], 10) };
134
+ }
135
+ if ((match = e.match(bareTaskSlugRe))) {
136
+ return { dirName: e, taskNum: match[1], sortKey: parseInt(match[1], 10) };
137
+ }
138
+ if ((match = e.match(bareTaskRe))) {
139
+ return { dirName: e, taskNum: String(parseInt(match[1], 10)), sortKey: parseInt(match[1], 10) };
140
+ }
141
+ return null;
142
+ })
143
+ .filter(Boolean)
144
+ .sort((a, b) => a.sortKey - b.sortKey);
145
+
146
+ if (process.env.DEBUG_SEED) console.log(`DEBUG ${sprintId} taskDirs:`, JSON.stringify(taskDirs));
147
+ const taskIds = taskDirs.map(t => `${prefix}-${sprintNum.toUpperCase()}-T${t.taskNum.padStart(2, '0')}`);
148
+
149
+ if (DRY_RUN) {
150
+ console.log(`[dry-run] would write sprint: ${sprintId}`);
151
+ } else {
152
+ Store.writeSprint({
153
+ sprintId,
154
+ title: extractTitle(sprintFullPath, `Sprint ${sprintNum}`),
155
+ status: inferSprintStatus(sprintFullPath, taskDirs.map(t => t.dirName)),
156
+ taskIds,
157
+ createdAt: new Date().toISOString(),
158
+ path: path.join(engPath, 'sprints', sprintDir),
159
+ });
160
+ }
161
+ sprintCount++;
162
+
163
+ for (const { dirName: taskDir, taskNum } of taskDirs) {
164
+ const taskFullPath = path.join(sprintFullPath, taskDir);
165
+ const taskId = `${prefix}-${sprintNum.toUpperCase()}-T${taskNum.padStart(2, '0')}`;
166
+ if (DRY_RUN) {
167
+ console.log(`[dry-run] would write task: ${taskId}`);
168
+ } else {
169
+ Store.writeTask({
170
+ taskId,
171
+ sprintId,
172
+ title: extractTitle(taskFullPath, `Task T${taskNum}`),
173
+ status: inferTaskStatus(taskFullPath),
174
+ path: path.join(engPath, 'sprints', sprintDir, taskDir),
175
+ });
176
+ }
177
+ taskCount++;
178
+ }
179
+ }
180
+ }
181
+
182
+ // --- Bugs ---
183
+ if (fs.existsSync(bugsDir)) {
184
+ // Three-tier bug discovery:
185
+ // 1. {PREFIX}-BUG-{NNN}-* (full slug, e.g. FORGE-BUG-001-sprint-runner-context)
186
+ // 2. BUG-{NNN}-* (partial slug, e.g. BUG-001-sprint-runner-context)
187
+ // 3. B{NN} (bare legacy, e.g. B01)
188
+ const prefixEscaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
189
+ const fullSlugBugRe = new RegExp(`^${prefixEscaped}-BUG-(\\d+)-.+$`, 'i');
190
+ const partialSlugBugRe = /^BUG-(\d+)-.+$/i;
191
+ const bareBugRe = /^B(\d+)$/i;
192
+
193
+ const bugDirs = fs.readdirSync(bugsDir)
194
+ .filter(e => fs.statSync(path.join(bugsDir, e)).isDirectory())
195
+ .map(e => {
196
+ let match;
197
+ if ((match = e.match(fullSlugBugRe))) {
198
+ return { dirName: e, bugNum: match[1], sortKey: parseInt(match[1], 10) };
199
+ }
200
+ if ((match = e.match(partialSlugBugRe))) {
201
+ return { dirName: e, bugNum: match[1], sortKey: parseInt(match[1], 10) };
202
+ }
203
+ if ((match = e.match(bareBugRe))) {
204
+ return { dirName: e, bugNum: match[1], sortKey: parseInt(match[1], 10) };
205
+ }
206
+ return null;
207
+ })
208
+ .filter(Boolean)
209
+ .sort((a, b) => a.sortKey - b.sortKey);
210
+
211
+ for (const { dirName: bugDir, bugNum } of bugDirs) {
212
+ const bugFullPath = path.join(bugsDir, bugDir);
213
+ const bugId = `${prefix}-BUG-${bugNum.padStart(2, '0')}`;
214
+ if (DRY_RUN) {
215
+ console.log(`[dry-run] would write bug: ${bugId}`);
216
+ } else {
217
+ Store.writeBug({
218
+ bugId,
219
+ title: extractTitle(bugFullPath, `Bug ${bugNum}`),
220
+ severity: 'minor',
221
+ status: 'reported',
222
+ path: path.join(engPath, 'bugs', bugDir),
223
+ reportedAt: new Date().toISOString(),
224
+ });
225
+ }
226
+ bugCount++;
227
+ }
228
+ }
229
+
230
+ const prefix_ = DRY_RUN ? '[dry-run] ' : '';
231
+ console.log(`${prefix_}Seeded: ${sprintCount} sprint(s), ${taskCount} task(s), ${bugCount} bug(s)`);
232
+ } catch (e) {
233
+ console.error(`Error seeding store: ${e.message}`);
234
+ process.exit(1);
235
+ }
236
+
237
+ } // end if (require.main === module)