@entelligentsia/forgecli 0.10.1 → 0.11.2
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 +78 -0
- package/README.md +21 -3
- package/dist/CHANGELOG-forge-plugin.md +22 -0
- package/dist/extensions/forgecli/add-pipeline.d.ts +19 -0
- package/dist/extensions/forgecli/add-pipeline.js +143 -0
- package/dist/extensions/forgecli/add-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/add-task.d.ts +20 -0
- package/dist/extensions/forgecli/add-task.js +154 -0
- package/dist/extensions/forgecli/add-task.js.map +1 -0
- package/dist/extensions/forgecli/calibrate.d.ts +61 -0
- package/dist/extensions/forgecli/calibrate.js +488 -0
- package/dist/extensions/forgecli/calibrate.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.d.ts +9 -1
- package/dist/extensions/forgecli/fix-bug.js +70 -8
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-commands.js +15 -22
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.js +34 -7
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-update-command.d.ts +9 -0
- package/dist/extensions/forgecli/forge-update-command.js +106 -7
- package/dist/extensions/forgecli/forge-update-command.js.map +1 -1
- package/dist/extensions/forgecli/health-check.d.ts +22 -1
- package/dist/extensions/forgecli/health-check.js +177 -4
- package/dist/extensions/forgecli/health-check.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +25 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +104 -9
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/hooks/check-update.d.ts +81 -0
- package/dist/extensions/forgecli/hooks/check-update.js +308 -0
- package/dist/extensions/forgecli/hooks/check-update.js.map +1 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.d.ts +32 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js +119 -0
- package/dist/extensions/forgecli/hooks/forge-permissions.js.map +1 -0
- package/dist/extensions/forgecli/hooks/triage-error.d.ts +23 -0
- package/dist/extensions/forgecli/hooks/triage-error.js +62 -0
- package/dist/extensions/forgecli/hooks/triage-error.js.map +1 -0
- package/dist/extensions/forgecli/hooks/write-guard.d.ts +28 -0
- package/dist/extensions/forgecli/hooks/write-guard.js +225 -0
- package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -0
- package/dist/extensions/forgecli/index.js +60 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/init-context.d.ts +1 -1
- package/dist/extensions/forgecli/init-context.js +21 -6
- package/dist/extensions/forgecli/init-context.js.map +1 -1
- package/dist/extensions/forgecli/materialize.d.ts +16 -0
- package/dist/extensions/forgecli/materialize.js +195 -0
- package/dist/extensions/forgecli/materialize.js.map +1 -0
- package/dist/extensions/forgecli/migrate.d.ts +19 -0
- package/dist/extensions/forgecli/migrate.js +258 -0
- package/dist/extensions/forgecli/migrate.js.map +1 -0
- package/dist/extensions/forgecli/migration-engine.d.ts +111 -0
- package/dist/extensions/forgecli/migration-engine.js +533 -0
- package/dist/extensions/forgecli/migration-engine.js.map +1 -0
- package/dist/extensions/forgecli/quiz-agent.d.ts +17 -0
- package/dist/extensions/forgecli/quiz-agent.js +98 -0
- package/dist/extensions/forgecli/quiz-agent.js.map +1 -0
- package/dist/extensions/forgecli/remove-command.d.ts +17 -0
- package/dist/extensions/forgecli/remove-command.js +124 -0
- package/dist/extensions/forgecli/remove-command.js.map +1 -0
- package/dist/extensions/forgecli/report-bug.d.ts +25 -0
- package/dist/extensions/forgecli/report-bug.js +159 -0
- package/dist/extensions/forgecli/report-bug.js.map +1 -0
- package/dist/extensions/forgecli/retrospective.d.ts +19 -0
- package/dist/extensions/forgecli/retrospective.js +156 -0
- package/dist/extensions/forgecli/retrospective.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +34 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +9 -1
- package/dist/extensions/forgecli/run-task.js +64 -10
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/session-registry.d.ts +27 -2
- package/dist/extensions/forgecli/session-registry.js +52 -1
- package/dist/extensions/forgecli/session-registry.js.map +1 -1
- package/dist/extensions/forgecli/status-command.d.ts +19 -0
- package/dist/extensions/forgecli/status-command.js +140 -0
- package/dist/extensions/forgecli/status-command.js.map +1 -0
- package/dist/extensions/forgecli/store-query.d.ts +22 -0
- package/dist/extensions/forgecli/store-query.js +107 -0
- package/dist/extensions/forgecli/store-query.js.map +1 -0
- package/dist/extensions/forgecli/store-repair.d.ts +17 -0
- package/dist/extensions/forgecli/store-repair.js +123 -0
- package/dist/extensions/forgecli/store-repair.js.map +1 -0
- package/dist/extensions/forgecli/thread-switcher.js +213 -28
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/update-tools.d.ts +23 -0
- package/dist/extensions/forgecli/update-tools.js +136 -0
- package/dist/extensions/forgecli/update-tools.js.map +1 -0
- package/dist/extensions/forgecli/viewport-theme.js +4 -0
- package/dist/extensions/forgecli/viewport-theme.js.map +1 -1
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/config.schema.json +83 -0
- package/dist/forge-payload/.schemas/migrations.json +2049 -0
- package/dist/forge-payload/commands/regenerate.md +17 -1
- package/dist/forge-payload/meta/personas/README.md +16 -0
- package/dist/forge-payload/meta/personas/meta-architect.md +70 -0
- package/dist/forge-payload/meta/personas/meta-bug-fixer.md +73 -0
- package/dist/forge-payload/meta/personas/meta-collator.md +72 -0
- package/dist/forge-payload/meta/personas/meta-engineer.md +70 -0
- package/dist/forge-payload/meta/personas/meta-orchestrator.md +71 -0
- package/dist/forge-payload/meta/personas/meta-product-manager.md +82 -0
- package/dist/forge-payload/meta/personas/meta-qa-engineer.md +91 -0
- package/dist/forge-payload/meta/personas/meta-supervisor.md +92 -0
- package/dist/forge-payload/meta/skill-recommendations.md +154 -0
- package/dist/forge-payload/meta/skills/meta-architect-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-bug-fixer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-collator-skills.md +41 -0
- package/dist/forge-payload/meta/skills/meta-engineer-skills.md +43 -0
- package/dist/forge-payload/meta/skills/meta-generic-skills.md +58 -0
- package/dist/forge-payload/meta/skills/meta-qa-engineer-skills.md +46 -0
- package/dist/forge-payload/meta/skills/meta-supervisor-skills.md +43 -0
- package/dist/forge-payload/meta/store-schema/bug.schema.md +71 -0
- package/dist/forge-payload/meta/store-schema/event.schema.md +76 -0
- package/dist/forge-payload/meta/store-schema/feature.schema.md +65 -0
- package/dist/forge-payload/meta/store-schema/sprint.schema.md +64 -0
- package/dist/forge-payload/meta/store-schema/task.schema.md +78 -0
- package/dist/forge-payload/meta/templates/meta-code-review.md +26 -0
- package/dist/forge-payload/meta/templates/meta-plan-review.md +28 -0
- package/dist/forge-payload/meta/templates/meta-plan.md +28 -0
- package/dist/forge-payload/meta/templates/meta-progress.md +25 -0
- package/dist/forge-payload/meta/templates/meta-retrospective.md +28 -0
- package/dist/forge-payload/meta/templates/meta-sprint-manifest.md +26 -0
- package/dist/forge-payload/meta/templates/meta-sprint-requirements.md +91 -0
- package/dist/forge-payload/meta/templates/meta-task-prompt.md +26 -0
- package/dist/forge-payload/meta/tool-specs/collate.spec.md +88 -0
- package/dist/forge-payload/meta/tool-specs/generation-manifest.spec.md +139 -0
- package/dist/forge-payload/meta/tool-specs/manage-config.spec.md +143 -0
- package/dist/forge-payload/meta/tool-specs/seed-store.spec.md +91 -0
- package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +328 -0
- package/dist/forge-payload/meta/tool-specs/validate-store.spec.md +191 -0
- package/dist/forge-payload/meta/workflows/_fragments/context-injection.md +75 -0
- package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/finalize.md +13 -0
- package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +73 -0
- package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +38 -0
- package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +39 -0
- package/dist/forge-payload/meta/workflows/meta-approve.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-collate.md +89 -0
- package/dist/forge-payload/meta/workflows/meta-commit.md +93 -0
- package/dist/forge-payload/meta/workflows/meta-enhance.md +286 -0
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +501 -0
- package/dist/forge-payload/meta/workflows/meta-implement.md +132 -0
- package/dist/forge-payload/meta/workflows/meta-migrate.md +455 -0
- package/dist/forge-payload/meta/workflows/meta-orchestrate.md +993 -0
- package/dist/forge-payload/meta/workflows/meta-plan-task.md +133 -0
- package/dist/forge-payload/meta/workflows/meta-quiz-agent.md +135 -0
- package/dist/forge-payload/meta/workflows/meta-retrospective.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +119 -0
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +108 -0
- package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +65 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +147 -0
- package/dist/forge-payload/meta/workflows/meta-update-implementation.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-update-plan.md +76 -0
- package/dist/forge-payload/meta/workflows/meta-validate.md +111 -0
- package/dist/forge-payload/tools/check-structure.cjs +344 -0
- package/dist/forge-payload/tools/list-skills.js +76 -0
- package/dist/forge-payload/tools/store-cli.cjs +27 -1
- package/dist/forge-payload/tools/substitute-placeholders.cjs +60 -8
- package/dist/forge-payload/tools/verify-integrity.cjs +86 -0
- package/package.json +2 -2
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Forge tool: check-structure
|
|
5
|
+
// Checks that all files listed in structure-manifest.json are present
|
|
6
|
+
// in a project's generated output.
|
|
7
|
+
// Usage: node check-structure.cjs [--strict] [--path <project-root>] [--validate-manifest] [--forge-root <path>]
|
|
8
|
+
// --path <project-root> Directory to check against (default: process.cwd())
|
|
9
|
+
// --strict Also report files present but NOT in the manifest (extra files)
|
|
10
|
+
// --validate-manifest Validate manifest against base-pack source files
|
|
11
|
+
// --forge-root <path> Path to the forge/ plugin directory (required with --validate-manifest)
|
|
12
|
+
//
|
|
13
|
+
// Reads .forge/config.json paths.* for directory overrides.
|
|
14
|
+
// Falls back to manifest dir field if config is absent or unparseable.
|
|
15
|
+
//
|
|
16
|
+
// Exit 0: all expected files present (or only extras found without --strict)
|
|
17
|
+
// Exit 1: any missing files detected; also exit 1 if extras found with --strict
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { getCommandsSubdir } = require('./lib/paths.cjs');
|
|
22
|
+
|
|
23
|
+
// ── Per-namespace verification logic ──────────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// Returns { present, missing, extra, total } without calling process.exit.
|
|
26
|
+
// - present: number of expected files found
|
|
27
|
+
// - missing: array of { nsKey, dir, filename }
|
|
28
|
+
// - extra: array of { nsKey, dir, filename } (populated only when strict=true)
|
|
29
|
+
// - total: total number of expected files across all namespaces
|
|
30
|
+
|
|
31
|
+
function checkNamespaces(manifest, projectRoot, options = {}) {
|
|
32
|
+
const { strict = false, configPaths = null } = options;
|
|
33
|
+
|
|
34
|
+
// Resolve config path overrides
|
|
35
|
+
let resolvedConfigPaths;
|
|
36
|
+
let projectPrefix = '';
|
|
37
|
+
if (configPaths !== null) {
|
|
38
|
+
resolvedConfigPaths = configPaths;
|
|
39
|
+
} else {
|
|
40
|
+
resolvedConfigPaths = {};
|
|
41
|
+
const configFile = path.join(projectRoot, '.forge', 'config.json');
|
|
42
|
+
if (fs.existsSync(configFile)) {
|
|
43
|
+
try {
|
|
44
|
+
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
45
|
+
resolvedConfigPaths = (cfg && cfg.paths) ? cfg.paths : {};
|
|
46
|
+
projectPrefix = (cfg && cfg.project && cfg.project.prefix)
|
|
47
|
+
? getCommandsSubdir(cfg.project.prefix)
|
|
48
|
+
: '';
|
|
49
|
+
} catch {
|
|
50
|
+
// unparseable — use defaults
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let totalPresent = 0;
|
|
56
|
+
let totalExpected = 0;
|
|
57
|
+
const allMissing = [];
|
|
58
|
+
const allExtra = [];
|
|
59
|
+
|
|
60
|
+
for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
|
|
61
|
+
const logicalKey = ns.logicalKey || nsKey;
|
|
62
|
+
let resolvedDir = (resolvedConfigPaths[logicalKey] && typeof resolvedConfigPaths[logicalKey] === 'string')
|
|
63
|
+
? resolvedConfigPaths[logicalKey]
|
|
64
|
+
: ns.dir;
|
|
65
|
+
if (ns.prefixed && projectPrefix) {
|
|
66
|
+
resolvedDir = resolvedDir + '/' + projectPrefix;
|
|
67
|
+
}
|
|
68
|
+
const absDir = path.join(projectRoot, resolvedDir);
|
|
69
|
+
|
|
70
|
+
const expected = ns.files || [];
|
|
71
|
+
totalExpected += expected.length;
|
|
72
|
+
|
|
73
|
+
const present = [];
|
|
74
|
+
const missing = [];
|
|
75
|
+
for (const filename of expected) {
|
|
76
|
+
const fullPath = path.join(absDir, filename);
|
|
77
|
+
if (fs.existsSync(fullPath)) {
|
|
78
|
+
present.push(filename);
|
|
79
|
+
} else {
|
|
80
|
+
missing.push({ nsKey, dir: resolvedDir, filename });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
totalPresent += present.length;
|
|
85
|
+
allMissing.push(...missing);
|
|
86
|
+
|
|
87
|
+
if (strict) {
|
|
88
|
+
if (fs.existsSync(absDir)) {
|
|
89
|
+
try {
|
|
90
|
+
const expectedSet = new Set(expected);
|
|
91
|
+
const found = fs.readdirSync(absDir);
|
|
92
|
+
for (const f of found) {
|
|
93
|
+
if (!expectedSet.has(f)) {
|
|
94
|
+
allExtra.push({ nsKey, dir: resolvedDir, filename: f });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
present: totalPresent,
|
|
104
|
+
missing: allMissing,
|
|
105
|
+
extra: allExtra,
|
|
106
|
+
total: totalExpected,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Manifest validation against base-pack ──────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// Convention-based reverse mapping: each manifest namespace's dir field maps
|
|
113
|
+
// to a base-pack source directory. For schemas, the source is forgeRoot/schemas/.
|
|
114
|
+
// This function validates that every file in the manifest has a corresponding
|
|
115
|
+
// base-pack source, and vice versa.
|
|
116
|
+
//
|
|
117
|
+
// Returns { manifestOnly, basePackOnly }
|
|
118
|
+
|
|
119
|
+
function validateManifest(manifest, forgeRoot) {
|
|
120
|
+
const basePackDir = path.join(forgeRoot, 'init', 'base-pack');
|
|
121
|
+
|
|
122
|
+
// Convention-based reverse mapping from manifest dir to base-pack source dir
|
|
123
|
+
const nsToSourceDir = {
|
|
124
|
+
personas: path.join(basePackDir, 'personas'),
|
|
125
|
+
skills: path.join(basePackDir, 'skills'),
|
|
126
|
+
workflows: path.join(basePackDir, 'workflows'),
|
|
127
|
+
fragments: path.join(basePackDir, 'workflows', '_fragments'),
|
|
128
|
+
templates: path.join(basePackDir, 'templates'),
|
|
129
|
+
commands: path.join(basePackDir, 'commands'),
|
|
130
|
+
// schemas: not base-pack-sourced — source is forgeRoot/schemas/
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const manifestOnly = []; // Files in manifest but not in base-pack
|
|
134
|
+
const basePackOnly = []; // Files in base-pack but not in manifest
|
|
135
|
+
|
|
136
|
+
for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
|
|
137
|
+
// Skip schemas — they ship from forgeRoot/schemas/, not base-pack
|
|
138
|
+
if (nsKey === 'schemas') continue;
|
|
139
|
+
|
|
140
|
+
const sourceDir = nsToSourceDir[nsKey];
|
|
141
|
+
if (!sourceDir) continue;
|
|
142
|
+
|
|
143
|
+
const manifestFiles = new Set(ns.files || []);
|
|
144
|
+
|
|
145
|
+
// Read base-pack source directory
|
|
146
|
+
let basePackFiles = [];
|
|
147
|
+
try {
|
|
148
|
+
basePackFiles = fs.readdirSync(sourceDir).filter(f => {
|
|
149
|
+
// Only include regular files, skip subdirectories (like _fragments under workflows)
|
|
150
|
+
const stat = fs.statSync(path.join(sourceDir, f));
|
|
151
|
+
return stat.isFile();
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
// Directory doesn't exist — all manifest files are manifestOnly
|
|
155
|
+
for (const f of manifestFiles) {
|
|
156
|
+
manifestOnly.push({ nsKey, filename: f, dir: ns.dir });
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const basePackFileSet = new Set(basePackFiles);
|
|
162
|
+
|
|
163
|
+
// Files in manifest but not in base-pack
|
|
164
|
+
for (const f of manifestFiles) {
|
|
165
|
+
if (!basePackFileSet.has(f)) {
|
|
166
|
+
manifestOnly.push({ nsKey, filename: f, dir: ns.dir });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Files in base-pack but not in manifest
|
|
171
|
+
for (const f of basePackFileSet) {
|
|
172
|
+
if (!manifestFiles.has(f)) {
|
|
173
|
+
basePackOnly.push({ nsKey, filename: f, dir: ns.dir });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { manifestOnly, basePackOnly };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Exports ────────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
module.exports = { checkNamespaces, validateManifest };
|
|
184
|
+
|
|
185
|
+
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
if (require.main === module) {
|
|
188
|
+
try {
|
|
189
|
+
// ── Parse arguments ──────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const argv = process.argv.slice(2);
|
|
192
|
+
let projectRoot = process.cwd();
|
|
193
|
+
let strict = false;
|
|
194
|
+
let validateManifestFlag = false;
|
|
195
|
+
let forgeRoot = null;
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < argv.length; i++) {
|
|
198
|
+
if (argv[i] === '--path' && argv[i + 1]) {
|
|
199
|
+
projectRoot = path.resolve(argv[++i]);
|
|
200
|
+
} else if (argv[i] === '--strict') {
|
|
201
|
+
strict = true;
|
|
202
|
+
} else if (argv[i] === '--validate-manifest') {
|
|
203
|
+
validateManifestFlag = true;
|
|
204
|
+
} else if (argv[i] === '--forge-root' && argv[i + 1]) {
|
|
205
|
+
forgeRoot = path.resolve(argv[++i]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Load structure-manifest.json ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const manifestPath = forgeRoot
|
|
212
|
+
? path.join(forgeRoot, 'schemas', 'structure-manifest.json')
|
|
213
|
+
: path.join(__dirname, '..', 'schemas', 'structure-manifest.json');
|
|
214
|
+
if (!fs.existsSync(manifestPath)) {
|
|
215
|
+
process.stderr.write(`× structure-manifest.json not found at ${manifestPath}\n`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let manifest;
|
|
220
|
+
try {
|
|
221
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
222
|
+
} catch (e) {
|
|
223
|
+
process.stderr.write(`× Failed to parse structure-manifest.json: ${e.message}\n`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Validate manifest against base-pack (if requested) ──────────────────────
|
|
228
|
+
|
|
229
|
+
if (validateManifestFlag) {
|
|
230
|
+
if (!forgeRoot) {
|
|
231
|
+
process.stderr.write('× --validate-manifest requires --forge-root <path>\n');
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { manifestOnly, basePackOnly } = validateManifest(manifest, forgeRoot);
|
|
236
|
+
|
|
237
|
+
if (manifestOnly.length > 0) {
|
|
238
|
+
process.stdout.write('× Files in manifest but absent from base-pack:\n');
|
|
239
|
+
for (const { nsKey, filename, dir } of manifestOnly) {
|
|
240
|
+
process.stdout.write(` × ${dir}/${filename} (namespace: ${nsKey})\n`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (basePackOnly.length > 0) {
|
|
245
|
+
process.stdout.write('△ Files in base-pack but absent from manifest:\n');
|
|
246
|
+
for (const { nsKey, filename, dir } of basePackOnly) {
|
|
247
|
+
process.stdout.write(` △ ${dir}/${filename} (namespace: ${nsKey})\n`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (manifestOnly.length === 0 && basePackOnly.length === 0) {
|
|
252
|
+
process.stdout.write('〇 Manifest and base-pack are in sync.\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (manifestOnly.length > 0) {
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Check namespaces ─────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
const result = checkNamespaces(manifest, projectRoot, { strict });
|
|
264
|
+
|
|
265
|
+
// ── Format output ────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
// Group results by namespace for display
|
|
268
|
+
const byNs = {};
|
|
269
|
+
for (const m of result.missing) {
|
|
270
|
+
if (!byNs[m.nsKey]) byNs[m.nsKey] = { present: 0, missing: [], extra: [] };
|
|
271
|
+
byNs[m.nsKey].missing.push(m.filename);
|
|
272
|
+
}
|
|
273
|
+
for (const e of result.extra) {
|
|
274
|
+
if (!byNs[e.nsKey]) byNs[e.nsKey] = { present: 0, missing: [], extra: [] };
|
|
275
|
+
byNs[e.nsKey].extra.push(e.filename);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Count present per namespace
|
|
279
|
+
for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
|
|
280
|
+
if (!byNs[nsKey]) byNs[nsKey] = { present: 0, missing: [], extra: [] };
|
|
281
|
+
const total = (ns.files || []).length;
|
|
282
|
+
const missingCount = byNs[nsKey].missing.length;
|
|
283
|
+
byNs[nsKey].present = total - missingCount;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const lines = [];
|
|
287
|
+
let anyMissing = result.missing.length > 0;
|
|
288
|
+
let anyExtra = result.extra.length > 0;
|
|
289
|
+
|
|
290
|
+
for (const [nsKey, ns] of Object.entries(manifest.namespaces)) {
|
|
291
|
+
const logicalKey = ns.logicalKey || nsKey;
|
|
292
|
+
const info = byNs[nsKey] || { present: 0, missing: [], extra: [] };
|
|
293
|
+
const total = (ns.files || []).length;
|
|
294
|
+
const dir = ns.dir;
|
|
295
|
+
|
|
296
|
+
if (info.missing.length === 0 && info.extra.length === 0) {
|
|
297
|
+
lines.push(`〇 ${dir}/ — ${info.present}/${total} present`);
|
|
298
|
+
} else {
|
|
299
|
+
if (info.missing.length > 0) {
|
|
300
|
+
lines.push(`× ${dir}/ — ${info.present}/${total} present, ${info.missing.length} missing:`);
|
|
301
|
+
for (const f of info.missing) {
|
|
302
|
+
lines.push(` × ${f}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (info.extra.length > 0) {
|
|
306
|
+
if (info.missing.length === 0) {
|
|
307
|
+
lines.push(`△ ${dir}/ — ${info.present}/${total} present, ${info.extra.length} extra:`);
|
|
308
|
+
}
|
|
309
|
+
for (const f of info.extra) {
|
|
310
|
+
lines.push(` △ ${f} (not in manifest)`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
process.stdout.write(line + '\n');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!anyMissing && !anyExtra) {
|
|
321
|
+
process.stdout.write(`〇 Structure check: all ${result.total} expected files present.\n`);
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const parts = [`${result.present} present`];
|
|
326
|
+
if (result.missing.length > 0) parts.push(`${result.missing.length} missing`);
|
|
327
|
+
if (strict && result.extra.length > 0) parts.push(`${result.extra.length} extra`);
|
|
328
|
+
process.stdout.write(`── Structure check: ${parts.join(', ')} (of ${result.total} expected)\n`);
|
|
329
|
+
|
|
330
|
+
if (anyMissing) {
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
// Extra-only without --strict → exit 0 already handled above
|
|
334
|
+
// Extra-only with --strict
|
|
335
|
+
if (strict && anyExtra) {
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
process.exit(0);
|
|
339
|
+
|
|
340
|
+
} catch (err) {
|
|
341
|
+
process.stderr.write(`× check-structure fatal: ${err.message}\n${err.stack}\n`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
} // end if (require.main === module)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Forge skill query helper
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node list-skills.js — print all available skill names (one per line)
|
|
6
|
+
// node list-skills.js <skill-name> — exit 0 if available, exit 1 if not
|
|
7
|
+
//
|
|
8
|
+
// Sources checked:
|
|
9
|
+
// ~/.claude/plugins/installed_plugins.json — marketplace plugins
|
|
10
|
+
// scope "user" → always available
|
|
11
|
+
// scope "local" → only if projectPath matches cwd
|
|
12
|
+
// ~/.claude/skills/ — personal skills (subdirs with SKILL.md)
|
|
13
|
+
//
|
|
14
|
+
// Uses only Node.js built-ins — no npm dependencies required.
|
|
15
|
+
// Works on Linux, macOS, and Windows wherever Claude Code runs.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// On unexpected failure, exit 0 so hook callers degrade gracefully
|
|
20
|
+
// (assume skill absent / no output) rather than propagating an error.
|
|
21
|
+
process.on('uncaughtException', () => process.exit(0));
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
|
|
27
|
+
const pluginsFile = process.env.CLAUDE_PLUGIN_DATA_ROOT
|
|
28
|
+
? path.join(process.env.CLAUDE_PLUGIN_DATA_ROOT, 'installed_plugins.json')
|
|
29
|
+
: path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
30
|
+
|
|
31
|
+
const personalSkillsDir = process.env.CLAUDE_SKILLS_DIR
|
|
32
|
+
|| path.join(os.homedir(), '.claude', 'skills');
|
|
33
|
+
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const skills = new Set();
|
|
36
|
+
|
|
37
|
+
// Source 1: marketplace plugins from installed_plugins.json
|
|
38
|
+
if (fs.existsSync(pluginsFile)) {
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(pluginsFile, 'utf8'));
|
|
41
|
+
for (const [key, installs] of Object.entries(data.plugins || {})) {
|
|
42
|
+
for (const install of installs) {
|
|
43
|
+
const isUser = install.scope === 'user';
|
|
44
|
+
const isLocal = install.scope === 'local' && install.projectPath === cwd;
|
|
45
|
+
if (isUser || isLocal) {
|
|
46
|
+
skills.add(key.split('@')[0]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch { /* non-fatal */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Source 2: personal skills — subdirectories containing a SKILL.md
|
|
54
|
+
if (fs.existsSync(personalSkillsDir)) {
|
|
55
|
+
try {
|
|
56
|
+
for (const entry of fs.readdirSync(personalSkillsDir, { withFileTypes: true })) {
|
|
57
|
+
if (!entry.isDirectory()) continue;
|
|
58
|
+
const skillMd = path.join(personalSkillsDir, entry.name, 'SKILL.md');
|
|
59
|
+
if (fs.existsSync(skillMd)) {
|
|
60
|
+
skills.add(entry.name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch { /* non-fatal */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sorted = [...skills].sort();
|
|
67
|
+
const query = process.argv[2];
|
|
68
|
+
|
|
69
|
+
if (!query) {
|
|
70
|
+
// Print all available skill names
|
|
71
|
+
if (sorted.length > 0) process.stdout.write(sorted.join('\n') + '\n');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
} else {
|
|
74
|
+
// Exit 0 if skill is available, 1 if not
|
|
75
|
+
process.exit(sorted.includes(query) ? 0 : 1);
|
|
76
|
+
}
|
|
@@ -389,6 +389,32 @@ function listEntities(entity, filter) {
|
|
|
389
389
|
case 'task': return store.listTasks(filter);
|
|
390
390
|
case 'bug': return store.listBugs(filter);
|
|
391
391
|
case 'feature': return store.listFeatures(filter);
|
|
392
|
+
case 'event': {
|
|
393
|
+
// Defect D fix: traverse all sub-directories under events/ — sprints write
|
|
394
|
+
// to events/<sprintId>/, bugs to events/bugs/, enhancements to events/enhancement/.
|
|
395
|
+
// Return the union of all event JSONs across every sub-directory, skipping
|
|
396
|
+
// sidecar files (_-prefixed) and non-JSON files.
|
|
397
|
+
const eventsBase = path.join(store.impl.storeRoot, 'events');
|
|
398
|
+
if (!fs.existsSync(eventsBase)) return [];
|
|
399
|
+
const allEvents = [];
|
|
400
|
+
const subDirs = fs.readdirSync(eventsBase, { withFileTypes: true });
|
|
401
|
+
for (const entry of subDirs) {
|
|
402
|
+
if (!entry.isDirectory()) continue;
|
|
403
|
+
const subDir = path.join(eventsBase, entry.name);
|
|
404
|
+
const files = fs.readdirSync(subDir).filter(
|
|
405
|
+
(f) => f.endsWith('.json') && !f.startsWith('_')
|
|
406
|
+
);
|
|
407
|
+
for (const file of files) {
|
|
408
|
+
try {
|
|
409
|
+
const rec = JSON.parse(fs.readFileSync(path.join(subDir, file), 'utf8'));
|
|
410
|
+
if (rec && (!filter || Object.entries(filter).every(([k, v]) => rec[k] === v))) {
|
|
411
|
+
allEvents.push(rec);
|
|
412
|
+
}
|
|
413
|
+
} catch (_) { /* skip malformed files */ }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return allEvents;
|
|
417
|
+
}
|
|
392
418
|
default:
|
|
393
419
|
console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'feature']))}`);
|
|
394
420
|
process.exit(1);
|
|
@@ -635,7 +661,7 @@ function cmdList() {
|
|
|
635
661
|
}
|
|
636
662
|
|
|
637
663
|
if (!ENTITY_TYPES.includes(entity)) {
|
|
638
|
-
console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'feature']))}`);
|
|
664
|
+
console.error(`Unknown entity type: ${entity}${formatSuggestion(suggestEntityType(entity, ['sprint', 'task', 'bug', 'event', 'feature']))}`);
|
|
639
665
|
process.exit(1);
|
|
640
666
|
}
|
|
641
667
|
|
|
@@ -343,13 +343,14 @@ function renderDeploymentTable(envs) {
|
|
|
343
343
|
* Advisory Note 3: every output path is resolved and checked to be under outRoot
|
|
344
344
|
* to prevent path traversal via symlinks or '..' segments.
|
|
345
345
|
*
|
|
346
|
-
* @param {string} basePack
|
|
346
|
+
* @param {string} basePack — absolute path to the base-pack directory
|
|
347
347
|
* @param {Map<string, string>} map
|
|
348
|
-
* @param {string} outRoot
|
|
349
|
-
* @param {boolean} dryRun
|
|
350
|
-
* @param {{ warn: function }} io
|
|
348
|
+
* @param {string} outRoot — absolute project root (e.g. '/home/user/myproject')
|
|
349
|
+
* @param {boolean} dryRun — if true, perform no writes
|
|
350
|
+
* @param {{ warn: function }} io — pluggable stderr for warnings
|
|
351
|
+
* @param {Set<string>|null} categoryFilter — Defect E: if set, only walk matching subdirs
|
|
351
352
|
*/
|
|
352
|
-
function walkBasePack(basePack, map, outRoot, dryRun, io) {
|
|
353
|
+
function walkBasePack(basePack, map, outRoot, dryRun, io, categoryFilter) {
|
|
353
354
|
const warn = (io && io.warn) || ((msg) => process.stderr.write(msg + '\n'));
|
|
354
355
|
|
|
355
356
|
// Extract prefix from substitution map for commands path computation
|
|
@@ -363,6 +364,11 @@ function walkBasePack(basePack, map, outRoot, dryRun, io) {
|
|
|
363
364
|
const stat = fs.statSync(subdirPath);
|
|
364
365
|
if (!stat.isDirectory()) continue;
|
|
365
366
|
|
|
367
|
+
// Defect E: skip subdirs not in the category filter when one is provided
|
|
368
|
+
if (categoryFilter !== null && categoryFilter !== undefined && !categoryFilter.has(subdir)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
366
372
|
let relOutputDir;
|
|
367
373
|
if (subdir === 'commands') {
|
|
368
374
|
relOutputDir = path.join('.claude', 'commands', commandsSubdir);
|
|
@@ -464,6 +470,32 @@ function walkBasePackPi(src, outRoot, dryRun, io) {
|
|
|
464
470
|
if (require.main === module) {
|
|
465
471
|
try {
|
|
466
472
|
const argv = process.argv.slice(2);
|
|
473
|
+
|
|
474
|
+
// ── Defect C fix: short-circuit --help/-h BEFORE any path resolution ────────
|
|
475
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
476
|
+
process.stdout.write([
|
|
477
|
+
'substitute-placeholders.cjs — Phase 3 (Materialize) engine.',
|
|
478
|
+
'',
|
|
479
|
+
'Usage:',
|
|
480
|
+
' node substitute-placeholders.cjs [options]',
|
|
481
|
+
'',
|
|
482
|
+
'Options:',
|
|
483
|
+
' --target <claude-code|pi> Output layout target (default: claude-code)',
|
|
484
|
+
' --src <path> Base-pack source dir for --target pi',
|
|
485
|
+
' --forge-root <path> Forge plugin root directory',
|
|
486
|
+
' --base-pack <path> Base-pack directory (probes .base-pack/ then init/base-pack/)',
|
|
487
|
+
' --config <path> Path to .forge/config.json',
|
|
488
|
+
' --context <path> Path to project-context.json',
|
|
489
|
+
' --rules <path> Path to build-base-pack-rules.json',
|
|
490
|
+
' --out <path> Output root directory (default: cwd)',
|
|
491
|
+
' --dry-run Preview without writing',
|
|
492
|
+
' --category <name[,name]> Limit materialisation to named subdirectories',
|
|
493
|
+
' (personas, skills, workflows, templates, commands)',
|
|
494
|
+
' --help, -h Show this message and exit',
|
|
495
|
+
].join('\n') + '\n');
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
|
|
467
499
|
const args = parseCliArgs(argv);
|
|
468
500
|
|
|
469
501
|
const dryRun = args.dryRun || false;
|
|
@@ -515,8 +547,20 @@ if (require.main === module) {
|
|
|
515
547
|
// Resolve forge root
|
|
516
548
|
const forgeRoot = args.forgeRoot || resolveForgeRoot();
|
|
517
549
|
|
|
518
|
-
// Resolve base-pack
|
|
519
|
-
|
|
550
|
+
// Resolve base-pack — Defect C fix: when no --base-pack flag given,
|
|
551
|
+
// probe .base-pack/ (bundled layout) before init/base-pack (source layout).
|
|
552
|
+
let basePack;
|
|
553
|
+
if (args.basePack) {
|
|
554
|
+
basePack = args.basePack;
|
|
555
|
+
} else {
|
|
556
|
+
const dotBasePack = path.resolve(process.cwd(), '.base-pack');
|
|
557
|
+
const initBasePack = path.join(forgeRoot, 'init', 'base-pack');
|
|
558
|
+
if (fs.existsSync(dotBasePack)) {
|
|
559
|
+
basePack = dotBasePack;
|
|
560
|
+
} else {
|
|
561
|
+
basePack = initBasePack;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
520
564
|
if (!fs.existsSync(basePack)) {
|
|
521
565
|
process.stderr.write(`substitute-placeholders: base-pack not found at ${basePack}\n`);
|
|
522
566
|
process.exit(1);
|
|
@@ -563,8 +607,15 @@ if (require.main === module) {
|
|
|
563
607
|
process.exit(1);
|
|
564
608
|
}
|
|
565
609
|
|
|
610
|
+
// Defect E: parse --category flag into a Set<string> filter (or null for all)
|
|
611
|
+
let categoryFilter = null;
|
|
612
|
+
if (args.category) {
|
|
613
|
+
const cats = args.category.split(',').map((c) => c.trim()).filter(Boolean);
|
|
614
|
+
categoryFilter = new Set(cats);
|
|
615
|
+
}
|
|
616
|
+
|
|
566
617
|
// Walk and materialise
|
|
567
|
-
walkBasePack(basePack, map, outRoot, dryRun, null);
|
|
618
|
+
walkBasePack(basePack, map, outRoot, dryRun, null, categoryFilter);
|
|
568
619
|
|
|
569
620
|
if (dryRun) {
|
|
570
621
|
process.stdout.write('substitute-placeholders: dry run complete (no files written)\n');
|
|
@@ -593,6 +644,7 @@ function parseCliArgs(argv) {
|
|
|
593
644
|
if (a === '--context' && argv[i + 1]) { args.context = argv[++i]; continue; }
|
|
594
645
|
if (a === '--rules' && argv[i + 1]) { args.rules = argv[++i]; continue; }
|
|
595
646
|
if (a === '--out' && argv[i + 1]) { args.out = argv[++i]; continue; }
|
|
647
|
+
if (a === '--category' && argv[i + 1]) { args.category = argv[++i]; continue; }
|
|
596
648
|
}
|
|
597
649
|
return args;
|
|
598
650
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Forge tool: verify-integrity
|
|
5
|
+
// Runtime integrity verifier — reads integrity.json, re-hashes each file, reports drift.
|
|
6
|
+
// Usage: node verify-integrity.cjs [--forge-root <path>]
|
|
7
|
+
// Exit codes: 0 = all clean, 1 = one or more files modified or missing, 2 = manifest missing
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
function computeHash(filePath) {
|
|
14
|
+
const content = fs.readFileSync(filePath);
|
|
15
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function verifyIntegrity(forgeRoot) {
|
|
19
|
+
const manifestPath = path.join(forgeRoot, 'integrity.json');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(manifestPath)) {
|
|
22
|
+
const output = '× integrity.json not found — run /forge:update to restore';
|
|
23
|
+
return { exitCode: 1, output, modified: [], missing: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let manifest;
|
|
27
|
+
try {
|
|
28
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const output = `× integrity.json is not valid JSON: ${e.message}`;
|
|
31
|
+
return { exitCode: 1, output, modified: [], missing: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { files } = manifest;
|
|
35
|
+
if (!files || typeof files !== 'object') {
|
|
36
|
+
const output = '× integrity.json has no "files" field';
|
|
37
|
+
return { exitCode: 1, output, modified: [], missing: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const modified = [];
|
|
41
|
+
const missing = [];
|
|
42
|
+
|
|
43
|
+
for (const [rel, expectedHash] of Object.entries(files)) {
|
|
44
|
+
const abs = path.join(forgeRoot, rel);
|
|
45
|
+
if (!fs.existsSync(abs)) {
|
|
46
|
+
missing.push(rel);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const actual = computeHash(abs);
|
|
50
|
+
if (actual !== expectedHash) {
|
|
51
|
+
modified.push(rel);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const totalFiles = Object.keys(files).length;
|
|
56
|
+
const problemCount = modified.length + missing.length;
|
|
57
|
+
|
|
58
|
+
if (problemCount === 0) {
|
|
59
|
+
const output = `〇 Plugin integrity — all ${totalFiles} files unmodified`;
|
|
60
|
+
return { exitCode: 0, output, modified: [], missing: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = [`△ Plugin integrity — ${problemCount} file${problemCount === 1 ? '' : 's'} modified or missing:`];
|
|
64
|
+
for (const f of modified) {
|
|
65
|
+
lines.push(` · ${f} (hash mismatch — run /forge:update to restore)`);
|
|
66
|
+
}
|
|
67
|
+
for (const f of missing) {
|
|
68
|
+
lines.push(` · ${f} (missing — run /forge:update to restore)`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { exitCode: 1, output: lines.join('\n'), modified, missing };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { verifyIntegrity };
|
|
75
|
+
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
const forgeRootIdx = args.indexOf('--forge-root');
|
|
79
|
+
const forgeRoot = forgeRootIdx !== -1
|
|
80
|
+
? path.resolve(args[forgeRootIdx + 1])
|
|
81
|
+
: path.resolve(__dirname, '..');
|
|
82
|
+
|
|
83
|
+
const result = verifyIntegrity(forgeRoot);
|
|
84
|
+
console.log(result.output);
|
|
85
|
+
process.exit(result.exitCode);
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entelligentsia/forgecli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Forge SDLC ported onto @earendil-works/pi-coding-agent — production launcher with three bin aliases (forge/forgecli/4ge). Bundles a curated fork of pi-coding-agent vendored under earendil-works names.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Entelligentsia",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"forge": {
|
|
52
|
-
"bundledVersion": "0.44.
|
|
52
|
+
"bundledVersion": "0.44.6",
|
|
53
53
|
"forgeRoot": "../forge/forge"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|