@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,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { findProjectRoot } = require('./lib/project-root.cjs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Store Facade for Forge
|
|
9
|
+
* Provides a backend-agnostic interface for CRUD operations on core store entities.
|
|
10
|
+
*/
|
|
11
|
+
class Store {
|
|
12
|
+
constructor(implementation) {
|
|
13
|
+
this.impl = implementation;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// --- Sprints ---
|
|
17
|
+
getSprint(id) { return this.impl.getSprint(id); }
|
|
18
|
+
listSprints(filter) { return this.impl.listSprints(filter); }
|
|
19
|
+
writeSprint(data) { return this.impl.writeSprint(data); }
|
|
20
|
+
deleteSprint(id) { return this.impl.deleteSprint(id); }
|
|
21
|
+
|
|
22
|
+
// --- Tasks ---
|
|
23
|
+
getTask(id) { return this.impl.getTask(id); }
|
|
24
|
+
listTasks(filter) { return this.impl.listTasks(filter); }
|
|
25
|
+
writeTask(data) { return this.impl.writeTask(data); }
|
|
26
|
+
deleteTask(id) { return this.impl.deleteTask(id); }
|
|
27
|
+
|
|
28
|
+
// --- Bugs ---
|
|
29
|
+
getBug(id) { return this.impl.getBug(id); }
|
|
30
|
+
listBugs(filter) { return this.impl.listBugs(filter); }
|
|
31
|
+
writeBug(data) { return this.impl.writeBug(data); }
|
|
32
|
+
deleteBug(id) { return this.impl.deleteBug(id); }
|
|
33
|
+
|
|
34
|
+
// --- Events ---
|
|
35
|
+
getEvent(id, sprintId) { return this.impl.getEvent(id, sprintId); }
|
|
36
|
+
listEvents(sprintId, filter) { return this.impl.listEvents(sprintId, filter); }
|
|
37
|
+
writeEvent(sprintId, data) { return this.impl.writeEvent(sprintId, data); }
|
|
38
|
+
deleteEvent(id, sprintId) { return this.impl.deleteEvent(id, sprintId); }
|
|
39
|
+
renameEvent(sprintId, oldFilename, newEventId) { return this.impl.renameEvent(sprintId, oldFilename, newEventId); }
|
|
40
|
+
|
|
41
|
+
// --- Features ---
|
|
42
|
+
getFeature(id) { return this.impl.getFeature(id); }
|
|
43
|
+
listFeatures(filter) { return this.impl.listFeatures(filter); }
|
|
44
|
+
writeFeature(data) { return this.impl.writeFeature(data); }
|
|
45
|
+
deleteFeature(id) { return this.impl.deleteFeature(id); }
|
|
46
|
+
|
|
47
|
+
// --- Collation State ---
|
|
48
|
+
writeCollationState(data) { return this.impl.writeCollationState(data); }
|
|
49
|
+
readCollationState() { return this.impl.readCollationState(); }
|
|
50
|
+
|
|
51
|
+
// --- Event Operations (extended) ---
|
|
52
|
+
/**
|
|
53
|
+
* Purge all event files for a sprint directory.
|
|
54
|
+
* @param {string} sprintId
|
|
55
|
+
* @param {{ dryRun?: boolean }} opts - dryRun: return file list without deleting
|
|
56
|
+
* @returns {{ purged: boolean, fileCount: number, files: string[] }}
|
|
57
|
+
*/
|
|
58
|
+
purgeEvents(sprintId, opts) { return this.impl.purgeEvents(sprintId, opts); }
|
|
59
|
+
/**
|
|
60
|
+
* List event filenames for a sprint directory.
|
|
61
|
+
* @param {string} sprintId
|
|
62
|
+
* @returns {{ filename: string, id: string }[]}
|
|
63
|
+
*/
|
|
64
|
+
listEventFilenames(sprintId) { return this.impl.listEventFilenames(sprintId); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Filesystem Implementation of the Store facade.
|
|
69
|
+
* Manages JSON flat-files in the .forge/store directory.
|
|
70
|
+
*/
|
|
71
|
+
class FSImpl {
|
|
72
|
+
constructor(configPath = '.forge/config.json') {
|
|
73
|
+
this.configPath = configPath;
|
|
74
|
+
this.storeRoot = this._resolveStoreRoot();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_resolveStoreRoot() {
|
|
78
|
+
const configPathIsAbsolute = path.isAbsolute(this.configPath);
|
|
79
|
+
const projectRoot = configPathIsAbsolute ? null : findProjectRoot();
|
|
80
|
+
try {
|
|
81
|
+
const resolved = projectRoot ? path.join(projectRoot, this.configPath) : this.configPath;
|
|
82
|
+
const config = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
83
|
+
const storePath = config.paths.store;
|
|
84
|
+
return path.isAbsolute(storePath) ? storePath
|
|
85
|
+
: projectRoot ? path.join(projectRoot, storePath) : storePath;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Fallback to default if config is missing or corrupt
|
|
88
|
+
return projectRoot ? path.join(projectRoot, '.forge', 'store') : '.forge/store';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_getPath(entity, id) {
|
|
93
|
+
const entityMap = {
|
|
94
|
+
sprint: 'sprints',
|
|
95
|
+
task: 'tasks',
|
|
96
|
+
bug: 'bugs',
|
|
97
|
+
event: 'events',
|
|
98
|
+
feature: 'features'
|
|
99
|
+
};
|
|
100
|
+
const dir = entityMap[entity];
|
|
101
|
+
if (!dir) throw new Error(`Unknown entity type: ${entity}`);
|
|
102
|
+
return path.join(this.storeRoot, dir, `${id}.json`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_readJson(filePath) {
|
|
106
|
+
if (!fs.existsSync(filePath)) return null;
|
|
107
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_writeJson(filePath, data) {
|
|
111
|
+
const dir = path.dirname(filePath);
|
|
112
|
+
if (!fs.existsSync(dir)) {
|
|
113
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Sprints
|
|
120
|
+
getSprint(id) { return this._readJson(this._getPath('sprint', id)); }
|
|
121
|
+
listSprints(filter) {
|
|
122
|
+
const dir = path.join(this.storeRoot, 'sprints');
|
|
123
|
+
if (!fs.existsSync(dir)) return [];
|
|
124
|
+
return fs.readdirSync(dir)
|
|
125
|
+
.filter(f => f.endsWith('.json'))
|
|
126
|
+
.map(f => this._readJson(path.join(dir, f)))
|
|
127
|
+
.filter(s => !s || (filter ? this._matches(s, filter) : true));
|
|
128
|
+
}
|
|
129
|
+
writeSprint(data) {
|
|
130
|
+
return this._writeJson(this._getPath('sprint', data.sprintId), data);
|
|
131
|
+
}
|
|
132
|
+
deleteSprint(id) {
|
|
133
|
+
const p = this._getPath('sprint', id);
|
|
134
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Tasks
|
|
138
|
+
getTask(id) { return this._readJson(this._getPath('task', id)); }
|
|
139
|
+
listTasks(filter) {
|
|
140
|
+
const dir = path.join(this.storeRoot, 'tasks');
|
|
141
|
+
if (!fs.existsSync(dir)) return [];
|
|
142
|
+
return fs.readdirSync(dir)
|
|
143
|
+
.filter(f => f.endsWith('.json'))
|
|
144
|
+
.map(f => this._readJson(path.join(dir, f)))
|
|
145
|
+
.filter(t => !t || (filter ? this._matches(t, filter) : true));
|
|
146
|
+
}
|
|
147
|
+
writeTask(data) {
|
|
148
|
+
return this._writeJson(this._getPath('task', data.taskId), data);
|
|
149
|
+
}
|
|
150
|
+
deleteTask(id) {
|
|
151
|
+
const p = this._getPath('task', id);
|
|
152
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Bugs
|
|
156
|
+
getBug(id) { return this._readJson(this._getPath('bug', id)); }
|
|
157
|
+
listBugs(filter) {
|
|
158
|
+
const dir = path.join(this.storeRoot, 'bugs');
|
|
159
|
+
if (!fs.existsSync(dir)) return [];
|
|
160
|
+
return fs.readdirSync(dir)
|
|
161
|
+
.filter(f => f.endsWith('.json'))
|
|
162
|
+
.map(f => this._readJson(path.join(dir, f)))
|
|
163
|
+
.filter(b => !b || (filter ? this._matches(b, filter) : true));
|
|
164
|
+
}
|
|
165
|
+
writeBug(data) {
|
|
166
|
+
return this._writeJson(this._getPath('bug', data.bugId), data);
|
|
167
|
+
}
|
|
168
|
+
deleteBug(id) {
|
|
169
|
+
const p = this._getPath('bug', id);
|
|
170
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Events
|
|
174
|
+
getEvent(id, sprintId) {
|
|
175
|
+
const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
|
|
176
|
+
return this._readJson(p);
|
|
177
|
+
}
|
|
178
|
+
listEvents(sprintId, filter) {
|
|
179
|
+
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
180
|
+
if (!fs.existsSync(dir)) return [];
|
|
181
|
+
return fs.readdirSync(dir)
|
|
182
|
+
.filter(f => f.endsWith('.json'))
|
|
183
|
+
.map(f => this._readJson(path.join(dir, f)))
|
|
184
|
+
.filter(e => !e || (filter ? this._matches(e, filter) : true));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Find an event file whose internal eventId matches the given eventId,
|
|
188
|
+
* but whose filename does not. Returns the mismatched filename (without
|
|
189
|
+
* .json extension), or null if none found. Skips _-prefixed (ephemeral) files.
|
|
190
|
+
*/
|
|
191
|
+
_findEventFileByContentId(sprintId, eventId) {
|
|
192
|
+
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
193
|
+
if (!fs.existsSync(dir)) return null;
|
|
194
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
const filename = file.slice(0, -5); // strip .json
|
|
197
|
+
if (filename === eventId) continue; // already canonical
|
|
198
|
+
const rec = this._readJson(path.join(dir, file));
|
|
199
|
+
if (rec && rec.eventId === eventId) {
|
|
200
|
+
return filename;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Rename an event file from oldFilename to match newEventId.
|
|
207
|
+
* Throws if the target file already exists (collision).
|
|
208
|
+
*/
|
|
209
|
+
renameEvent(sprintId, oldFilename, newEventId) {
|
|
210
|
+
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
211
|
+
const oldPath = path.join(dir, `${oldFilename}.json`);
|
|
212
|
+
const newPath = path.join(dir, `${newEventId}.json`);
|
|
213
|
+
if (oldPath === newPath) return; // no-op
|
|
214
|
+
if (fs.existsSync(newPath)) {
|
|
215
|
+
throw new Error(`Cannot rename event: target file already exists: ${newPath}`);
|
|
216
|
+
}
|
|
217
|
+
if (fs.existsSync(oldPath)) {
|
|
218
|
+
fs.renameSync(oldPath, newPath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
writeEvent(sprintId, data) {
|
|
222
|
+
// Detect ghost file: an existing file whose content eventId matches but
|
|
223
|
+
// whose filename does not. Rename it to the canonical name before writing.
|
|
224
|
+
const ghostFilename = this._findEventFileByContentId(sprintId, data.eventId);
|
|
225
|
+
if (ghostFilename !== null) {
|
|
226
|
+
this.renameEvent(sprintId, ghostFilename, data.eventId);
|
|
227
|
+
}
|
|
228
|
+
const p = path.join(this.storeRoot, 'events', sprintId, `${data.eventId}.json`);
|
|
229
|
+
return this._writeJson(p, data);
|
|
230
|
+
}
|
|
231
|
+
deleteEvent(id, sprintId) {
|
|
232
|
+
const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
|
|
233
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Features
|
|
237
|
+
getFeature(id) { return this._readJson(this._getPath('feature', id)); }
|
|
238
|
+
listFeatures(filter) {
|
|
239
|
+
const dir = path.join(this.storeRoot, 'features');
|
|
240
|
+
if (!fs.existsSync(dir)) return [];
|
|
241
|
+
return fs.readdirSync(dir)
|
|
242
|
+
.filter(f => f.endsWith('.json'))
|
|
243
|
+
.map(f => this._readJson(path.join(dir, f)))
|
|
244
|
+
.filter(f => !f || (filter ? this._matches(f, filter) : true));
|
|
245
|
+
}
|
|
246
|
+
writeFeature(data) {
|
|
247
|
+
return this._writeJson(this._getPath('feature', data.feature_id), data);
|
|
248
|
+
}
|
|
249
|
+
deleteFeature(id) {
|
|
250
|
+
const p = this._getPath('feature', id);
|
|
251
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Collation State
|
|
255
|
+
writeCollationState(data) {
|
|
256
|
+
const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
|
|
257
|
+
return this._writeJson(filePath, data);
|
|
258
|
+
}
|
|
259
|
+
readCollationState() {
|
|
260
|
+
const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
|
|
261
|
+
return this._readJson(filePath);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Event Operations (extended)
|
|
265
|
+
/**
|
|
266
|
+
* Purge the events directory for a given sprint.
|
|
267
|
+
* Includes a path-traversal guard: the resolved directory must remain
|
|
268
|
+
* within the events base directory. Throws on escape attempt.
|
|
269
|
+
* Note: fileCount reflects .json files only, but the directory is removed
|
|
270
|
+
* entirely (including any non-.json files).
|
|
271
|
+
*/
|
|
272
|
+
purgeEvents(sprintId, { dryRun = false } = {}) {
|
|
273
|
+
const eventsBase = path.resolve(this.storeRoot, 'events');
|
|
274
|
+
const eventsDir = path.resolve(eventsBase, sprintId);
|
|
275
|
+
// Guard: resolved path must stay within the events base directory.
|
|
276
|
+
if (!eventsDir.startsWith(eventsBase + path.sep) && eventsDir !== eventsBase) {
|
|
277
|
+
throw new Error(`Resolved events path '${eventsDir}' escapes store root — aborting purge`);
|
|
278
|
+
}
|
|
279
|
+
if (!fs.existsSync(eventsDir)) {
|
|
280
|
+
return { purged: false, fileCount: 0, files: [] };
|
|
281
|
+
}
|
|
282
|
+
const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.json'));
|
|
283
|
+
if (dryRun) {
|
|
284
|
+
return { purged: false, fileCount: files.length, files };
|
|
285
|
+
}
|
|
286
|
+
fs.rmSync(eventsDir, { recursive: true, force: true });
|
|
287
|
+
return { purged: true, fileCount: files.length, files };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* List all event filenames for a sprint directory.
|
|
292
|
+
* Returns { filename, id } objects for ALL .json files including
|
|
293
|
+
* _-prefixed ephemeral sidecars. Callers filter internally.
|
|
294
|
+
*/
|
|
295
|
+
listEventFilenames(sprintId) {
|
|
296
|
+
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
297
|
+
if (!fs.existsSync(dir)) return [];
|
|
298
|
+
return fs.readdirSync(dir)
|
|
299
|
+
.filter(f => f.endsWith('.json'))
|
|
300
|
+
.map(f => ({
|
|
301
|
+
filename: f,
|
|
302
|
+
id: f.slice(0, -5) // strip .json extension
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_matches(record, filter) {
|
|
307
|
+
if (!filter) return true;
|
|
308
|
+
return Object.entries(filter).every(([key, value]) => record[key] === value);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Export a singleton instance for the plugin, plus classes for testing
|
|
313
|
+
module.exports = new Store(new FSImpl());
|
|
314
|
+
module.exports.Store = Store;
|
|
315
|
+
module.exports.FSImpl = FSImpl;
|