@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.
- package/CHANGELOG.md +138 -0
- package/README.md +177 -38
- package/dist/bin/argv.js +5 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/forge.js +1 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/ask-user-tool.d.ts +17 -0
- package/dist/extensions/forgecli/ask-user-tool.js +139 -0
- package/dist/extensions/forgecli/ask-user-tool.js.map +1 -0
- package/dist/extensions/forgecli/forge-commands.d.ts +21 -0
- package/dist/extensions/forgecli/forge-commands.js +141 -0
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-init.d.ts +26 -0
- package/dist/extensions/forgecli/forge-init.js +948 -0
- package/dist/extensions/forgecli/forge-init.js.map +1 -0
- package/dist/extensions/forgecli/health-check.d.ts +18 -0
- package/dist/extensions/forgecli/health-check.js +154 -0
- package/dist/extensions/forgecli/health-check.js.map +1 -0
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +34 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +237 -3
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/index.js +28 -11
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/init-context.d.ts +99 -0
- package/dist/extensions/forgecli/init-context.js +163 -0
- package/dist/extensions/forgecli/init-context.js.map +1 -0
- package/dist/extensions/forgecli/init-progress.d.ts +39 -0
- package/dist/extensions/forgecli/init-progress.js +117 -0
- package/dist/extensions/forgecli/init-progress.js.map +1 -0
- package/dist/extensions/forgecli/refresh-kb-links.d.ts +18 -0
- package/dist/extensions/forgecli/refresh-kb-links.js +228 -0
- package/dist/extensions/forgecli/refresh-kb-links.js.map +1 -0
- package/dist/extensions/forgecli/store-validator.d.ts +13 -0
- package/dist/extensions/forgecli/store-validator.js +35 -0
- package/dist/extensions/forgecli/store-validator.js.map +1 -0
- package/dist/extensions/forgecli/transition-guard.d.ts +20 -0
- package/dist/extensions/forgecli/transition-guard.js +125 -0
- package/dist/extensions/forgecli/transition-guard.js.map +1 -0
- package/dist/forge-payload/.base-pack/commands/approve.md +22 -0
- package/dist/forge-payload/.base-pack/commands/collate.md +22 -0
- package/dist/forge-payload/.base-pack/commands/commit.md +22 -0
- package/dist/forge-payload/.base-pack/commands/enhance.md +37 -0
- package/dist/forge-payload/.base-pack/commands/fix-bug.md +22 -0
- package/dist/forge-payload/.base-pack/commands/implement.md +22 -0
- package/dist/forge-payload/.base-pack/commands/plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/quiz-agent.md +22 -0
- package/dist/forge-payload/.base-pack/commands/retrospective.md +22 -0
- package/dist/forge-payload/.base-pack/commands/review-code.md +22 -0
- package/dist/forge-payload/.base-pack/commands/review-plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/run-sprint.md +22 -0
- package/dist/forge-payload/.base-pack/commands/run-task.md +22 -0
- package/dist/forge-payload/.base-pack/commands/sprint-intake.md +22 -0
- package/dist/forge-payload/.base-pack/commands/sprint-plan.md +22 -0
- package/dist/forge-payload/.base-pack/commands/validate.md +22 -0
- package/dist/forge-payload/.claude-plugin/plugin.json +15 -0
- package/dist/forge-payload/.init/discovery/discover-database.md +32 -0
- package/dist/forge-payload/.init/discovery/discover-processes.md +31 -0
- package/dist/forge-payload/.init/discovery/discover-routing.md +31 -0
- package/dist/forge-payload/.init/discovery/discover-stack.md +33 -0
- package/dist/forge-payload/.init/discovery/discover-testing.md +34 -0
- package/dist/forge-payload/.init/generation/generate-kb-doc.md +60 -0
- package/dist/forge-payload/.schemas/bug.schema.json +53 -0
- package/dist/forge-payload/.schemas/collation-state.schema.json +16 -0
- package/dist/forge-payload/.schemas/event-sidecar.schema.json +22 -0
- package/dist/forge-payload/.schemas/event.schema.json +32 -0
- package/dist/forge-payload/.schemas/feature.schema.json +22 -0
- package/dist/forge-payload/.schemas/progress-entry.schema.json +16 -0
- package/dist/forge-payload/.schemas/project-context.schema.json +167 -0
- package/dist/forge-payload/.schemas/project-overlay.schema.json +25 -0
- package/dist/forge-payload/.schemas/sprint.schema.json +27 -0
- package/dist/forge-payload/.schemas/structure-versions.schema.json +57 -0
- package/dist/forge-payload/.schemas/task.schema.json +58 -0
- package/dist/forge-payload/.tools/banners.cjs +435 -0
- package/dist/forge-payload/.tools/build-context-pack.cjs +290 -0
- package/dist/forge-payload/.tools/build-init-context.cjs +322 -0
- package/dist/forge-payload/.tools/build-overlay.cjs +326 -0
- package/dist/forge-payload/.tools/build-persona-pack.cjs +226 -0
- package/dist/forge-payload/.tools/collate.cjs +1041 -0
- package/dist/forge-payload/.tools/generation-manifest.cjs +311 -0
- package/dist/forge-payload/.tools/lib/forge-root.cjs +59 -0
- package/dist/forge-payload/.tools/lib/paths.cjs +29 -0
- package/dist/forge-payload/.tools/lib/pricing.cjs +165 -0
- package/dist/forge-payload/.tools/lib/project-root.cjs +32 -0
- package/dist/forge-payload/.tools/lib/result.js +40 -0
- package/dist/forge-payload/.tools/lib/validate.js +131 -0
- package/dist/forge-payload/.tools/manage-config.cjs +340 -0
- package/dist/forge-payload/.tools/manage-versions.cjs +365 -0
- package/dist/forge-payload/.tools/seed-store.cjs +237 -0
- package/dist/forge-payload/.tools/store-cli.cjs +1123 -0
- package/dist/forge-payload/.tools/store.cjs +315 -0
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +625 -0
- package/dist/forge-payload/.tools/validate-store.cjs +522 -0
- package/package.json +1 -1
- /package/dist/forge-payload/{personas → .base-pack/personas}/architect.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/bug-fixer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/collator.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/engineer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/librarian.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/orchestrator.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/product-manager.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/qa-engineer.md +0 -0
- /package/dist/forge-payload/{personas → .base-pack/personas}/supervisor.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/architect-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/bug-fixer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/collator-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/engineer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/generic-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/librarian-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/qa-engineer-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/store-custodian-skills.md +0 -0
- /package/dist/forge-payload/{skills → .base-pack/skills}/supervisor-skills.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/CODE_REVIEW_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/COST_REPORT_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_REVIEW_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_SUMMARY_TEMPLATE.json +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PLAN_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/PROGRESS_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/RETROSPECTIVE_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_MANIFEST_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/SPRINT_REQUIREMENTS_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{templates → .base-pack/templates}/TASK_PROMPT_TEMPLATE.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/context-injection.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/event-emission-schema.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/finalize.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/_fragments/progress-reporting.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_approve.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_review_sprint_completion.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_intake.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/architect_sprint_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/collator_agent.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/commit_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/fix_bug.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/implement_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/migrate_structural.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/orchestrate_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/plan_task.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/quiz_agent.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_code.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/review_plan.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/run_sprint.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/sprint_retrospective.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_implementation.md +0 -0
- /package/dist/forge-payload/{workflows → .base-pack/workflows}/update_plan.md +0 -0
- /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)
|