@izkac/forgekit 0.3.12 → 0.3.14
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/bin/forge.mjs +119 -107
- package/bin/forgekit.mjs +83 -83
- package/bin/review.mjs +81 -81
- package/package.json +2 -1
- package/scripts/prepack.mjs +78 -78
- package/scripts/run-tests.mjs +50 -50
- package/src/adr.mjs +236 -236
- package/src/adr.test.mjs +170 -170
- package/src/change.mjs +327 -234
- package/src/change.test.mjs +145 -83
- package/src/cleanup-sessions.mjs +84 -84
- package/src/config.mjs +103 -103
- package/src/defer.mjs +75 -75
- package/src/doctor.mjs +350 -341
- package/src/doctor.test.mjs +114 -114
- package/src/fleet.test.mjs +50 -0
- package/src/init.mjs +680 -621
- package/src/install.mjs +815 -815
- package/src/install.test.mjs +180 -180
- package/src/integrity-check.mjs +60 -60
- package/src/integrity.mjs +688 -682
- package/src/integrity.test.mjs +612 -566
- package/src/lib/fleet.mjs +61 -4
- package/src/lib.mjs +160 -143
- package/src/lib.test.mjs +128 -0
- package/src/models.defaults.json +41 -41
- package/src/new-session.mjs +99 -99
- package/src/openspec-overlays/README.md +19 -19
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
- package/src/paths.mjs +92 -92
- package/src/plan-engine.mjs +321 -278
- package/src/plan-engine.test.mjs +447 -283
- package/src/preferences.defaults.json +78 -78
- package/src/preferences.mjs +438 -438
- package/src/preferences.test.mjs +174 -174
- package/src/record-evidence.mjs +204 -204
- package/src/repo-root.mjs +33 -0
- package/src/resolve-model.mjs +312 -312
- package/src/resolve-model.test.mjs +194 -194
- package/src/review/cli.test.mjs +117 -117
- package/src/review/export.mjs +172 -172
- package/src/review/export.test.mjs +197 -197
- package/src/review/fixtures/valid-review.json +42 -42
- package/src/review/lib.mjs +894 -894
- package/src/review/lib.test.mjs +266 -266
- package/src/review/schema.json +196 -196
- package/src/review/signals.test.mjs +62 -62
- package/src/score-cli.mjs +68 -68
- package/src/score.mjs +568 -568
- package/src/score.test.mjs +366 -366
- package/src/session-reminder.mjs +207 -207
- package/src/session-status.mjs +70 -70
- package/src/set-models.mjs +186 -186
- package/src/set-phase.mjs +205 -205
- package/src/set-prefs.mjs +294 -294
- package/src/specs-sync.mjs +232 -0
- package/src/specs-sync.test.mjs +114 -0
- package/src/spine.mjs +93 -93
- package/src/triage-prompt.mjs +175 -175
- package/src/triage-prompt.test.mjs +50 -50
- package/src/vendor-openspec-overlays.mjs +176 -176
- package/src/vendor-openspec-overlays.test.mjs +62 -62
- package/vendor/skills/archive-to-adr/SKILL.md +149 -149
- package/vendor/skills/forge/SKILL.md +136 -136
- package/vendor/skills/forge/docs/forge.md +650 -647
- package/vendor/skills/forge/phases/brainstorm.md +23 -23
- package/vendor/skills/forge/phases/finish.md +90 -87
- package/vendor/skills/forge/phases/implement.md +77 -77
- package/vendor/skills/forge/phases/plan-openspec.md +60 -60
- package/vendor/skills/forge/phases/plan-specs.md +163 -117
- package/vendor/skills/forge/phases/review.md +25 -25
- package/vendor/skills/forge/phases/verify.md +124 -124
- package/vendor/skills/forge/references/forge-layout.md +85 -85
- package/vendor/skills/forge/references/pace.md +115 -115
- package/vendor/skills/forge/references/plan-routing.md +52 -51
- package/vendor/skills/forge/references/runtime-integrity.md +232 -232
- package/vendor/skills/forge/references/substantial-work.md +37 -37
- package/vendor/skills/forge/references/test-evidence.md +30 -30
- package/vendor/skills/forge/references/test-strategy.md +68 -68
- package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
- package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
- package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
- package/vendor/skills/thorough-code-review/SKILL.md +290 -290
- package/vendor/skills/thorough-code-review/examples.md +133 -133
- package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
- package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
- package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
- package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
- package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
- package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
- package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
- package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
- package/vendor/templates/adr/README.md +7 -7
- package/vendor/templates/adr/decisions.md +141 -141
- package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
- package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
- package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
- package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
- package/vendor/templates/project/claude/commands/forge-build.md +17 -17
- package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
- package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
- package/vendor/templates/project/claude/commands/forge-status.md +16 -16
- package/vendor/templates/project/claude/commands/forge.md +16 -16
- package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
- package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
- package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
- package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
- package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
- package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
- package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
- package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
- package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
- package/vendor/templates/project/cursor/commands/forge.md +16 -16
- package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
- package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -3
package/src/new-session.mjs
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Create a new Forge session under .forge/sessions/ and set it active.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* forge new mercury-console-validation
|
|
7
|
-
* forge new <slug> [--chat-id <id>] [--signal <text>]
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
defaultSession,
|
|
12
|
-
defaultStatus,
|
|
13
|
-
ensureForgeLayout,
|
|
14
|
-
FORGE_DIR,
|
|
15
|
-
makeSessionId,
|
|
16
|
-
saveSession,
|
|
17
|
-
scaffoldSessionDirs,
|
|
18
|
-
sessionPath,
|
|
19
|
-
writeActive,
|
|
20
|
-
} from './lib.mjs';
|
|
21
|
-
import { resolveSessionPaceFields } from './preferences.mjs';
|
|
22
|
-
import { warnIfDoctorFails } from './doctor.mjs';
|
|
23
|
-
import { liveOverlaps, queueMessage, sessionDirFor } from './lib/fleet.mjs';
|
|
24
|
-
|
|
25
|
-
function usage() {
|
|
26
|
-
process.stderr.write(
|
|
27
|
-
'Usage: forge new <slug> [--chat-id <id>] [--signal <text>]\n',
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const args = process.argv.slice(2);
|
|
33
|
-
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') usage();
|
|
34
|
-
|
|
35
|
-
const slug = args[0];
|
|
36
|
-
let cursorChatId = null;
|
|
37
|
-
let signalText = null;
|
|
38
|
-
for (let i = 1; i < args.length; i += 1) {
|
|
39
|
-
if (args[i] === '--chat-id' && args[i + 1]) {
|
|
40
|
-
cursorChatId = args[i + 1];
|
|
41
|
-
i += 1;
|
|
42
|
-
} else if (args[i] === '--signal' && args[i + 1]) {
|
|
43
|
-
signalText = args[i + 1];
|
|
44
|
-
i += 1;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ensureForgeLayout();
|
|
49
|
-
warnIfDoctorFails({ cwd: process.cwd() });
|
|
50
|
-
|
|
51
|
-
const sessionId = makeSessionId(slug);
|
|
52
|
-
const dir = sessionPath(sessionId);
|
|
53
|
-
scaffoldSessionDirs(dir);
|
|
54
|
-
|
|
55
|
-
const session = defaultSession(sessionId, slug);
|
|
56
|
-
if (cursorChatId) session.cursorChatId = cursorChatId;
|
|
57
|
-
|
|
58
|
-
const paceFields = resolveSessionPaceFields({
|
|
59
|
-
forgeDir: FORGE_DIR,
|
|
60
|
-
slug: session.slug,
|
|
61
|
-
signalText: signalText || session.slug,
|
|
62
|
-
});
|
|
63
|
-
Object.assign(session, paceFields);
|
|
64
|
-
|
|
65
|
-
saveSession(dir, session);
|
|
66
|
-
writeActive(sessionId);
|
|
67
|
-
|
|
68
|
-
// Fleet coordination: another live session in this working tree risks
|
|
69
|
-
// conflicting edits — surface it here and notify the other sessions' inboxes.
|
|
70
|
-
const overlaps = liveOverlaps(process.cwd(), sessionId);
|
|
71
|
-
for (const o of overlaps) {
|
|
72
|
-
queueMessage(
|
|
73
|
-
sessionDirFor(o),
|
|
74
|
-
`Fleet overlap: session "${session.slug}" (${sessionId}) just started in this project. Coordinate with the user to avoid conflicting edits.`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const out = {
|
|
79
|
-
sessionId,
|
|
80
|
-
dir,
|
|
81
|
-
session: defaultStatus(session),
|
|
82
|
-
pace: {
|
|
83
|
-
requested: session.pace,
|
|
84
|
-
resolved: session.resolvedPace,
|
|
85
|
-
reason: session.paceReason,
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
if (overlaps.length > 0) {
|
|
89
|
-
out.overlaps = overlaps.map((o) => ({
|
|
90
|
-
sessionId: o.sessionId,
|
|
91
|
-
slug: o.slug,
|
|
92
|
-
phase: o.phase,
|
|
93
|
-
engine: o.engine,
|
|
94
|
-
lastSeen: o.lastSeen ?? o.updatedAt,
|
|
95
|
-
}));
|
|
96
|
-
out.overlapAdvice =
|
|
97
|
-
'Other live sessions are working in this project. Tell the user and ask: continue anyway, use a git worktree, or pause one session.';
|
|
98
|
-
}
|
|
99
|
-
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Create a new Forge session under .forge/sessions/ and set it active.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge new mercury-console-validation
|
|
7
|
+
* forge new <slug> [--chat-id <id>] [--signal <text>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
defaultSession,
|
|
12
|
+
defaultStatus,
|
|
13
|
+
ensureForgeLayout,
|
|
14
|
+
FORGE_DIR,
|
|
15
|
+
makeSessionId,
|
|
16
|
+
saveSession,
|
|
17
|
+
scaffoldSessionDirs,
|
|
18
|
+
sessionPath,
|
|
19
|
+
writeActive,
|
|
20
|
+
} from './lib.mjs';
|
|
21
|
+
import { resolveSessionPaceFields } from './preferences.mjs';
|
|
22
|
+
import { warnIfDoctorFails } from './doctor.mjs';
|
|
23
|
+
import { liveOverlaps, queueMessage, sessionDirFor } from './lib/fleet.mjs';
|
|
24
|
+
|
|
25
|
+
function usage() {
|
|
26
|
+
process.stderr.write(
|
|
27
|
+
'Usage: forge new <slug> [--chat-id <id>] [--signal <text>]\n',
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') usage();
|
|
34
|
+
|
|
35
|
+
const slug = args[0];
|
|
36
|
+
let cursorChatId = null;
|
|
37
|
+
let signalText = null;
|
|
38
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
39
|
+
if (args[i] === '--chat-id' && args[i + 1]) {
|
|
40
|
+
cursorChatId = args[i + 1];
|
|
41
|
+
i += 1;
|
|
42
|
+
} else if (args[i] === '--signal' && args[i + 1]) {
|
|
43
|
+
signalText = args[i + 1];
|
|
44
|
+
i += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ensureForgeLayout();
|
|
49
|
+
warnIfDoctorFails({ cwd: process.cwd() });
|
|
50
|
+
|
|
51
|
+
const sessionId = makeSessionId(slug);
|
|
52
|
+
const dir = sessionPath(sessionId);
|
|
53
|
+
scaffoldSessionDirs(dir);
|
|
54
|
+
|
|
55
|
+
const session = defaultSession(sessionId, slug);
|
|
56
|
+
if (cursorChatId) session.cursorChatId = cursorChatId;
|
|
57
|
+
|
|
58
|
+
const paceFields = resolveSessionPaceFields({
|
|
59
|
+
forgeDir: FORGE_DIR,
|
|
60
|
+
slug: session.slug,
|
|
61
|
+
signalText: signalText || session.slug,
|
|
62
|
+
});
|
|
63
|
+
Object.assign(session, paceFields);
|
|
64
|
+
|
|
65
|
+
saveSession(dir, session);
|
|
66
|
+
writeActive(sessionId);
|
|
67
|
+
|
|
68
|
+
// Fleet coordination: another live session in this working tree risks
|
|
69
|
+
// conflicting edits — surface it here and notify the other sessions' inboxes.
|
|
70
|
+
const overlaps = liveOverlaps(process.cwd(), sessionId);
|
|
71
|
+
for (const o of overlaps) {
|
|
72
|
+
queueMessage(
|
|
73
|
+
sessionDirFor(o),
|
|
74
|
+
`Fleet overlap: session "${session.slug}" (${sessionId}) just started in this project. Coordinate with the user to avoid conflicting edits.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const out = {
|
|
79
|
+
sessionId,
|
|
80
|
+
dir,
|
|
81
|
+
session: defaultStatus(session),
|
|
82
|
+
pace: {
|
|
83
|
+
requested: session.pace,
|
|
84
|
+
resolved: session.resolvedPace,
|
|
85
|
+
reason: session.paceReason,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
if (overlaps.length > 0) {
|
|
89
|
+
out.overlaps = overlaps.map((o) => ({
|
|
90
|
+
sessionId: o.sessionId,
|
|
91
|
+
slug: o.slug,
|
|
92
|
+
phase: o.phase,
|
|
93
|
+
engine: o.engine,
|
|
94
|
+
lastSeen: o.lastSeen ?? o.updatedAt,
|
|
95
|
+
}));
|
|
96
|
+
out.overlapAdvice =
|
|
97
|
+
'Other live sessions are working in this project. Tell the user and ask: continue anyway, use a git worktree, or pause one session.';
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
# OpenSpec Forge overlays
|
|
2
|
-
|
|
3
|
-
Vendor OpenSpec skills and `/opsx:*` commands are upgraded in place. **Do not hand-edit** them.
|
|
4
|
-
|
|
5
|
-
Forge adaptations live here and are applied by:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
forge overlay
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Run after refreshing OpenSpec skills/commands from upstream (usually via `forge init --overlay`).
|
|
12
|
-
|
|
13
|
-
| Overlay | Targets |
|
|
14
|
-
| ------- | ------- |
|
|
15
|
-
| `opsx-apply-implement-step.md` | Step 6 in `opsx-apply` / `opsx/apply` commands |
|
|
16
|
-
| `opsx-apply-completion-step.md` | Step 7 “all done” bullet in same commands |
|
|
17
|
-
| `openspec-apply-change-footer.md` | Footer on `openspec-apply-change/SKILL.md` (all agents) |
|
|
18
|
-
|
|
19
|
-
**Forge-owned (never overwritten):** `/forge:apply` command, `forge/` skill tree.
|
|
1
|
+
# OpenSpec Forge overlays
|
|
2
|
+
|
|
3
|
+
Vendor OpenSpec skills and `/opsx:*` commands are upgraded in place. **Do not hand-edit** them.
|
|
4
|
+
|
|
5
|
+
Forge adaptations live here and are applied by:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
forge overlay
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run after refreshing OpenSpec skills/commands from upstream (usually via `forge init --overlay`).
|
|
12
|
+
|
|
13
|
+
| Overlay | Targets |
|
|
14
|
+
| ------- | ------- |
|
|
15
|
+
| `opsx-apply-implement-step.md` | Step 6 in `opsx-apply` / `opsx/apply` commands |
|
|
16
|
+
| `opsx-apply-completion-step.md` | Step 7 “all done” bullet in same commands |
|
|
17
|
+
| `openspec-apply-change-footer.md` | Footer on `openspec-apply-change/SKILL.md` (all agents) |
|
|
18
|
+
|
|
19
|
+
**Forge-owned (never overwritten):** `/forge:apply` command, `forge/` skill tree.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
---
|
|
3
|
-
|
|
4
|
-
<!-- forgekit:openspec-overlay:start -->
|
|
5
|
-
|
|
6
|
-
## Forge overlay (re-applied by `forge overlay`)
|
|
7
|
-
|
|
8
|
-
**Do not implement tasks inline in coordinator context.**
|
|
9
|
-
|
|
10
|
-
1. Prefer **`/forge:apply`** for OpenSpec implement + verify + review.
|
|
11
|
-
2. Otherwise follow vendor steps 1–5 above, then **`{{PHASES_IMPLEMENT}}`** for step 6+.
|
|
12
|
-
3. Re-apply this overlay after upgrading OpenSpec skills: `forge overlay`
|
|
13
|
-
|
|
14
|
-
<!-- forgekit:openspec-overlay:end -->
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
<!-- forgekit:openspec-overlay:start -->
|
|
5
|
+
|
|
6
|
+
## Forge overlay (re-applied by `forge overlay`)
|
|
7
|
+
|
|
8
|
+
**Do not implement tasks inline in coordinator context.**
|
|
9
|
+
|
|
10
|
+
1. Prefer **`/forge:apply`** for OpenSpec implement + verify + review.
|
|
11
|
+
2. Otherwise follow vendor steps 1–5 above, then **`{{PHASES_IMPLEMENT}}`** for step 6+.
|
|
12
|
+
3. Re-apply this overlay after upgrading OpenSpec skills: `forge overlay`
|
|
13
|
+
|
|
14
|
+
<!-- forgekit:openspec-overlay:end -->
|
|
@@ -1 +1 @@
|
|
|
1
|
-
- If all done: proceed to Forge [verify]({{PHASES_VERIFY}}) and [review]({{PHASES_REVIEW}}) when in a Forge session (or user invoked `/forge:apply`), then suggest `/opsx:archive`
|
|
1
|
+
- If all done: proceed to Forge [verify]({{PHASES_VERIFY}}) and [review]({{PHASES_REVIEW}}) when in a Forge session (or user invoked `/forge:apply`), then suggest `/opsx:archive`
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
**REQUIRED (Forge):** Wrap this loop in the implement phase —
|
|
2
|
-
`{{PHASES_IMPLEMENT}}`.
|
|
3
|
-
Use bundled `skills/subagent-driven-development` + `skills/test-driven-development` per task.
|
|
4
|
-
Prefer **`/forge:apply`** over bare `/opsx:apply` — same OpenSpec CLI steps plus verify and review.
|
|
5
|
-
|
|
6
|
-
For each pending task:
|
|
7
|
-
- Show which task is being worked on
|
|
8
|
-
- Dispatch **implementer** subagent (TDD first), then **spec** + **quality** reviewers
|
|
9
|
-
- Keep changes minimal and focused
|
|
10
|
-
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
|
11
|
-
- Continue to next task
|
|
1
|
+
**REQUIRED (Forge):** Wrap this loop in the implement phase —
|
|
2
|
+
`{{PHASES_IMPLEMENT}}`.
|
|
3
|
+
Use bundled `skills/subagent-driven-development` + `skills/test-driven-development` per task.
|
|
4
|
+
Prefer **`/forge:apply`** over bare `/opsx:apply` — same OpenSpec CLI steps plus verify and review.
|
|
5
|
+
|
|
6
|
+
For each pending task:
|
|
7
|
+
- Show which task is being worked on
|
|
8
|
+
- Dispatch **implementer** subagent (TDD first), then **spec** + **quality** reviewers
|
|
9
|
+
- Keep changes minimal and focused
|
|
10
|
+
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
|
11
|
+
- Continue to next task
|
package/src/paths.mjs
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Resolve forgekit asset roots (skills, templates).
|
|
4
|
-
*
|
|
5
|
-
* Search order for assets:
|
|
6
|
-
* 1. FORGEKIT_ROOT env (dev / linked monorepo)
|
|
7
|
-
* 2. Monorepo root (packages/cli/../../..)
|
|
8
|
-
* 3. Vendored copy inside the published package (packages/cli/vendor)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
|
-
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
|
|
18
|
-
/** @returns {string} Absolute path to packages/cli */
|
|
19
|
-
export function cliPackageRoot() {
|
|
20
|
-
return path.resolve(__dirname, '..');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Candidate forgekit roots that may contain skills/ and templates/.
|
|
25
|
-
* @returns {string[]}
|
|
26
|
-
*/
|
|
27
|
-
export function forgekitRootCandidates() {
|
|
28
|
-
const roots = [];
|
|
29
|
-
if (process.env.FORGEKIT_ROOT) roots.push(process.env.FORGEKIT_ROOT);
|
|
30
|
-
// Monorepo: packages/cli/src → ../../../
|
|
31
|
-
roots.push(path.resolve(__dirname, '..', '..', '..'));
|
|
32
|
-
// Published package: vendor/ next to src/
|
|
33
|
-
roots.push(path.join(cliPackageRoot(), 'vendor'));
|
|
34
|
-
return [...new Set(roots.map((r) => path.resolve(r)))];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @param {string} relativePath e.g. 'skills/forge' or 'templates/project'
|
|
39
|
-
* @param {{ requireFile?: string }} [opts] file that must exist under the candidate
|
|
40
|
-
* @returns {string}
|
|
41
|
-
*/
|
|
42
|
-
export function resolveAsset(relativePath, opts = {}) {
|
|
43
|
-
const requireFile = opts.requireFile;
|
|
44
|
-
const tried = [];
|
|
45
|
-
for (const root of forgekitRootCandidates()) {
|
|
46
|
-
const candidate = path.join(root, relativePath);
|
|
47
|
-
tried.push(candidate);
|
|
48
|
-
const check = requireFile ? path.join(candidate, requireFile) : candidate;
|
|
49
|
-
if (fs.existsSync(check)) return candidate;
|
|
50
|
-
}
|
|
51
|
-
throw new Error(
|
|
52
|
-
`Asset not found: ${relativePath}${requireFile ? ` (${requireFile})` : ''}.\nTried:\n ${tried.join('\n ')}`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @returns {string} package version from packages/cli/package.json
|
|
58
|
-
*/
|
|
59
|
-
export function packageVersion() {
|
|
60
|
-
const pkgPath = path.join(cliPackageRoot(), 'package.json');
|
|
61
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
62
|
-
return pkg.version ?? '0.0.0';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Stable content hash of a directory (sorted relative paths + file bytes).
|
|
67
|
-
* @param {string} dir
|
|
68
|
-
* @returns {string} sha256 hex
|
|
69
|
-
*/
|
|
70
|
-
export function hashDirectory(dir) {
|
|
71
|
-
const hash = createHash('sha256');
|
|
72
|
-
/** @type {string[]} */
|
|
73
|
-
const files = [];
|
|
74
|
-
const walk = (d, prefix = '') => {
|
|
75
|
-
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
76
|
-
if (entry.name === '.forgekit.json') continue;
|
|
77
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
78
|
-
const full = path.join(d, entry.name);
|
|
79
|
-
if (entry.isDirectory()) walk(full, rel);
|
|
80
|
-
else files.push(rel);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
walk(dir);
|
|
84
|
-
files.sort();
|
|
85
|
-
for (const rel of files) {
|
|
86
|
-
hash.update(rel);
|
|
87
|
-
hash.update('\0');
|
|
88
|
-
hash.update(fs.readFileSync(path.join(dir, ...rel.split('/'))));
|
|
89
|
-
hash.update('\0');
|
|
90
|
-
}
|
|
91
|
-
return hash.digest('hex');
|
|
92
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Resolve forgekit asset roots (skills, templates).
|
|
4
|
+
*
|
|
5
|
+
* Search order for assets:
|
|
6
|
+
* 1. FORGEKIT_ROOT env (dev / linked monorepo)
|
|
7
|
+
* 2. Monorepo root (packages/cli/../../..)
|
|
8
|
+
* 3. Vendored copy inside the published package (packages/cli/vendor)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
/** @returns {string} Absolute path to packages/cli */
|
|
19
|
+
export function cliPackageRoot() {
|
|
20
|
+
return path.resolve(__dirname, '..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Candidate forgekit roots that may contain skills/ and templates/.
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
export function forgekitRootCandidates() {
|
|
28
|
+
const roots = [];
|
|
29
|
+
if (process.env.FORGEKIT_ROOT) roots.push(process.env.FORGEKIT_ROOT);
|
|
30
|
+
// Monorepo: packages/cli/src → ../../../
|
|
31
|
+
roots.push(path.resolve(__dirname, '..', '..', '..'));
|
|
32
|
+
// Published package: vendor/ next to src/
|
|
33
|
+
roots.push(path.join(cliPackageRoot(), 'vendor'));
|
|
34
|
+
return [...new Set(roots.map((r) => path.resolve(r)))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} relativePath e.g. 'skills/forge' or 'templates/project'
|
|
39
|
+
* @param {{ requireFile?: string }} [opts] file that must exist under the candidate
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function resolveAsset(relativePath, opts = {}) {
|
|
43
|
+
const requireFile = opts.requireFile;
|
|
44
|
+
const tried = [];
|
|
45
|
+
for (const root of forgekitRootCandidates()) {
|
|
46
|
+
const candidate = path.join(root, relativePath);
|
|
47
|
+
tried.push(candidate);
|
|
48
|
+
const check = requireFile ? path.join(candidate, requireFile) : candidate;
|
|
49
|
+
if (fs.existsSync(check)) return candidate;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Asset not found: ${relativePath}${requireFile ? ` (${requireFile})` : ''}.\nTried:\n ${tried.join('\n ')}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {string} package version from packages/cli/package.json
|
|
58
|
+
*/
|
|
59
|
+
export function packageVersion() {
|
|
60
|
+
const pkgPath = path.join(cliPackageRoot(), 'package.json');
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
62
|
+
return pkg.version ?? '0.0.0';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stable content hash of a directory (sorted relative paths + file bytes).
|
|
67
|
+
* @param {string} dir
|
|
68
|
+
* @returns {string} sha256 hex
|
|
69
|
+
*/
|
|
70
|
+
export function hashDirectory(dir) {
|
|
71
|
+
const hash = createHash('sha256');
|
|
72
|
+
/** @type {string[]} */
|
|
73
|
+
const files = [];
|
|
74
|
+
const walk = (d, prefix = '') => {
|
|
75
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
76
|
+
if (entry.name === '.forgekit.json') continue;
|
|
77
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
78
|
+
const full = path.join(d, entry.name);
|
|
79
|
+
if (entry.isDirectory()) walk(full, rel);
|
|
80
|
+
else files.push(rel);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(dir);
|
|
84
|
+
files.sort();
|
|
85
|
+
for (const rel of files) {
|
|
86
|
+
hash.update(rel);
|
|
87
|
+
hash.update('\0');
|
|
88
|
+
hash.update(fs.readFileSync(path.join(dir, ...rel.split('/'))));
|
|
89
|
+
hash.update('\0');
|
|
90
|
+
}
|
|
91
|
+
return hash.digest('hex');
|
|
92
|
+
}
|