wicked-crew 0.7.25 → 0.7.26
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/dist/api/endpoint-manifest-live.d.ts.map +1 -1
- package/dist/api/endpoint-manifest-live.js +5 -0
- package/dist/api/endpoint-manifest-live.js.map +1 -1
- package/dist/api/eval-compare.d.ts +306 -0
- package/dist/api/eval-compare.d.ts.map +1 -0
- package/dist/api/eval-compare.js +610 -0
- package/dist/api/eval-compare.js.map +1 -0
- package/dist/api/eval-sample.d.ts +158 -0
- package/dist/api/eval-sample.d.ts.map +1 -0
- package/dist/api/eval-sample.js +88 -0
- package/dist/api/eval-sample.js.map +1 -0
- package/dist/api/routes.d.ts +5 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +31 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +12 -2
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +17 -5
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/server.d.ts +20 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +62 -9
- package/dist/api/server.js.map +1 -1
- package/dist/api/skills.d.ts +88 -0
- package/dist/api/skills.d.ts.map +1 -0
- package/dist/api/skills.js +264 -0
- package/dist/api/skills.js.map +1 -0
- package/dist/api/testing.d.ts +2 -75
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +14 -28
- package/dist/api/testing.js.map +1 -1
- package/dist/core/adapter.d.ts +42 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +83 -12
- package/dist/core/adapter.js.map +1 -1
- package/dist/interactive/bridge-root.d.ts +117 -7
- package/dist/interactive/bridge-root.d.ts.map +1 -1
- package/dist/interactive/bridge-root.js +229 -9
- package/dist/interactive/bridge-root.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts +2 -0
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +3 -19
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/doc-list-routes.d.ts +42 -0
- package/dist/interactive/doc-list-routes.d.ts.map +1 -0
- package/dist/interactive/doc-list-routes.js +165 -0
- package/dist/interactive/doc-list-routes.js.map +1 -0
- package/dist/interactive/project-root.d.ts +36 -0
- package/dist/interactive/project-root.d.ts.map +1 -0
- package/dist/interactive/project-root.js +45 -0
- package/dist/interactive/project-root.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +4 -1
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +4 -22
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/projects/default-project.d.ts +10 -0
- package/dist/projects/default-project.d.ts.map +1 -0
- package/dist/projects/default-project.js +10 -0
- package/dist/projects/default-project.js.map +1 -0
- package/dist/projects/routes.d.ts +0 -2
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +1 -2
- package/dist/projects/routes.js.map +1 -1
- package/dist/skills/bundle.d.ts +94 -0
- package/dist/skills/bundle.d.ts.map +1 -0
- package/dist/skills/bundle.js +187 -0
- package/dist/skills/bundle.js.map +1 -0
- package/dist/skills/contain.d.ts +57 -0
- package/dist/skills/contain.d.ts.map +1 -0
- package/dist/skills/contain.js +90 -0
- package/dist/skills/contain.js.map +1 -0
- package/dist/skills/core-closure.d.ts +83 -0
- package/dist/skills/core-closure.d.ts.map +1 -0
- package/dist/skills/core-closure.js +130 -0
- package/dist/skills/core-closure.js.map +1 -0
- package/dist/skills/engine-env.d.ts +48 -0
- package/dist/skills/engine-env.d.ts.map +1 -0
- package/dist/skills/engine-env.js +69 -0
- package/dist/skills/engine-env.js.map +1 -0
- package/dist/skills/frontmatter.d.ts +77 -0
- package/dist/skills/frontmatter.d.ts.map +1 -0
- package/dist/skills/frontmatter.js +190 -0
- package/dist/skills/frontmatter.js.map +1 -0
- package/dist/skills/guards.d.ts +72 -0
- package/dist/skills/guards.d.ts.map +1 -0
- package/dist/skills/guards.js +147 -0
- package/dist/skills/guards.js.map +1 -0
- package/dist/skills/live-generations.d.ts +80 -0
- package/dist/skills/live-generations.d.ts.map +1 -0
- package/dist/skills/live-generations.js +163 -0
- package/dist/skills/live-generations.js.map +1 -0
- package/dist/skills/plugin-source.d.ts +109 -0
- package/dist/skills/plugin-source.d.ts.map +1 -0
- package/dist/skills/plugin-source.js +272 -0
- package/dist/skills/plugin-source.js.map +1 -0
- package/dist/skills/refs.d.ts +58 -0
- package/dist/skills/refs.d.ts.map +1 -0
- package/dist/skills/refs.js +133 -0
- package/dist/skills/refs.js.map +1 -0
- package/dist/skills/root-fence.d.ts +53 -0
- package/dist/skills/root-fence.d.ts.map +1 -0
- package/dist/skills/root-fence.js +109 -0
- package/dist/skills/root-fence.js.map +1 -0
- package/dist/skills/root-names.d.ts +42 -0
- package/dist/skills/root-names.d.ts.map +1 -0
- package/dist/skills/root-names.js +44 -0
- package/dist/skills/root-names.js.map +1 -0
- package/dist/skills/runtime.d.ts +130 -0
- package/dist/skills/runtime.d.ts.map +1 -0
- package/dist/skills/runtime.js +246 -0
- package/dist/skills/runtime.js.map +1 -0
- package/dist/skills/store.d.ts +895 -0
- package/dist/skills/store.d.ts.map +1 -0
- package/dist/skills/store.js +3575 -0
- package/dist/skills/store.js.map +1 -0
- package/dist/skills/tree.d.ts +292 -0
- package/dist/skills/tree.d.ts.map +1 -0
- package/dist/skills/tree.js +645 -0
- package/dist/skills/tree.js.map +1 -0
- package/dist/skills/venv.d.ts +52 -0
- package/dist/skills/venv.d.ts.map +1 -0
- package/dist/skills/venv.js +80 -0
- package/dist/skills/venv.js.map +1 -0
- package/dist/studio/assets/index-CK1uJWs2.css +32 -0
- package/dist/studio/assets/index-FcZg3WDR.js +539 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +605 -7
- package/endpoint-manifest.json +183 -1
- package/package.json +4 -3
- package/dist/studio/assets/index-C4iz4uAw.js +0 -537
- package/dist/studio/assets/index-DEEQcRQ_.css +0 -32
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which published generations a LIVE run may still be reading (design v3 DECISION 1: "old
|
|
3
|
+
* generations are reaped only after no live session references them").
|
|
4
|
+
*
|
|
5
|
+
* The engine resolves `WICKED_SKILLS_SNAPSHOT` at every worker spawn and reports the generation it
|
|
6
|
+
* ADMITTED as a `skillsSnapshotHanded` CoreEvent ({session, gen, …}) — the authoritative record of
|
|
7
|
+
* which generation each session's spawn was actually handed. Crew keeps TWO kinds of pin:
|
|
8
|
+
*
|
|
9
|
+
* session pins a session is tracked from its first event; a `skillsSnapshotHanded` pins the
|
|
10
|
+
* EXACT generation the engine reports (its `gen` field), never "whatever `current`
|
|
11
|
+
* happens to be when crew observes some later event" (codex round 3); every
|
|
12
|
+
* generation published WHILE the session is live is pinned to it too — its next
|
|
13
|
+
* unit spawn reads (and will report) the new `current`; the terminal frame
|
|
14
|
+
* (`sessionCompleted` / `sessionFailed` / `runCancelled` — the same triple
|
|
15
|
+
* api/seat-health.ts drops assignments on) releases every pin the session held.
|
|
16
|
+
*
|
|
17
|
+
* launch pins EXPLICIT ACCOUNTING for the gap between a launch and the engine's report. When
|
|
18
|
+
* the daemon hands a launch to the engine (`CoreAdapter.onLaunch`: launchRun,
|
|
19
|
+
* resumeRun, confirmGate, launchCampaign, resumeCampaign — every path a spawn can
|
|
20
|
+
* come from; the campaign's own DAG-node runs are pinned as the engine announces
|
|
21
|
+
* them, `campaignNodeStarted`, under the campaign's umbrella pin), crew opens a pin
|
|
22
|
+
* for that launch holding the generation the env exports RIGHT NOW, and every
|
|
23
|
+
* generation published after that is added to it (the spawn may read any of them).
|
|
24
|
+
* A launch pin is released ONLY when the engine reports the handed generation for
|
|
25
|
+
* that session (`skillsSnapshotHanded` — the session pin takes over), when the
|
|
26
|
+
* run/campaign reaches a terminal state, or when the engine REJECTED the launch
|
|
27
|
+
* (nothing will ever read it). It never expires by publish count or age (codex
|
|
28
|
+
* round 4: a bounded window let a run's generation be reaped while its handoff
|
|
29
|
+
* report was still outstanding — publish count bounds nothing about handoff delay).
|
|
30
|
+
* Reaping skips any generation with an unreleased launch pin.
|
|
31
|
+
*
|
|
32
|
+
* The ledger is in-memory: the engine is in-process, so a daemon restart ends every worker it
|
|
33
|
+
* spawned, and a resumed run's launch (`resumeRun`) opens a fresh launch pin.
|
|
34
|
+
*/
|
|
35
|
+
import type { CoreEvent } from '../core/types.js';
|
|
36
|
+
export type LiveGenerationObservation = 'pinned' | 'released' | 'ignored';
|
|
37
|
+
/** What the daemon hands the engine: a run (launch / resume / gate answer) or a campaign. */
|
|
38
|
+
export type LaunchKind = 'run' | 'campaign';
|
|
39
|
+
export declare class LiveGenerations {
|
|
40
|
+
/** Session pins: session id → the generations the engine reported + every one published since. */
|
|
41
|
+
private readonly sessions;
|
|
42
|
+
/** Launch pins: `run:<id>` / `campaign:<id>` → the generations the launch may read, until it is accounted for. */
|
|
43
|
+
private readonly launchPins;
|
|
44
|
+
/** The generation the env exports right now (`exported`), or `null` when none is (fallback / unpublished). */
|
|
45
|
+
private exportedGen;
|
|
46
|
+
private static key;
|
|
47
|
+
/**
|
|
48
|
+
* Fold one CoreEvent: track a live session, pin the EXACT generation the engine reports on a
|
|
49
|
+
* `skillsSnapshotHanded` (releasing that session's launch pin — the engine has accounted for the
|
|
50
|
+
* launch), release on a terminal frame; pin a campaign's node run as the engine announces it and
|
|
51
|
+
* release the campaign's pin on its terminal frame. A non-`skillsSnapshotHanded` session event
|
|
52
|
+
* only tracks the session — it never pins "current at event time" (the bug codex round 3
|
|
53
|
+
* replaced): the generation a spawn actually used comes from the engine's own report, and the
|
|
54
|
+
* pre-report window is covered by the launch pin.
|
|
55
|
+
*/
|
|
56
|
+
observe(event: CoreEvent): LiveGenerationObservation;
|
|
57
|
+
/**
|
|
58
|
+
* The env now exports generation `gen` to new launches (`WICKED_SKILLS_SNAPSHOT`;
|
|
59
|
+
* runtime.afterPublish), or nothing (`null`). A launch pin opened after this starts from it.
|
|
60
|
+
*/
|
|
61
|
+
exported(gen: number | null): void;
|
|
62
|
+
/**
|
|
63
|
+
* The daemon is handing a launch to the engine (before the engine call, so no spawn can read the
|
|
64
|
+
* env ahead of the pin): open its launch pin at the generation the env exports right now. The pin
|
|
65
|
+
* accumulates every later publish (`published`) and is released only by the engine's report for
|
|
66
|
+
* that session, the run's / campaign's terminal frame, or `launchRejected`.
|
|
67
|
+
*/
|
|
68
|
+
launched(kind: LaunchKind, id: string): void;
|
|
69
|
+
/** The engine REFUSED the launch (the call rejected): nothing will read it — release its pin. */
|
|
70
|
+
launchRejected(kind: LaunchKind, id: string): void;
|
|
71
|
+
/** A generation was just published: every live session and every open launch may read it at its next spawn. */
|
|
72
|
+
published(gen: number): void;
|
|
73
|
+
/** The union of every live session's pins and every open launch pin — what the reaper must keep. */
|
|
74
|
+
pinned(): ReadonlySet<number>;
|
|
75
|
+
/** Live sessions being tracked (diagnostics + tests). */
|
|
76
|
+
liveSessions(): string[];
|
|
77
|
+
/** Launches handed to the engine and not yet accounted for (`run:<id>` / `campaign:<id>`; diagnostics + tests). */
|
|
78
|
+
openLaunches(): string[];
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=live-generations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-generations.d.ts","sourceRoot":"","sources":["../../src/skills/live-generations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAclD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1E,6FAA6F;AAC7F,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,UAAU,CAAC;AAe5C,qBAAa,eAAe;IAC1B,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,kHAAkH;IAClH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,8GAA8G;IAC9G,OAAO,CAAC,WAAW,CAAuB;IAE1C,OAAO,CAAC,MAAM,CAAC,GAAG;IAIlB;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,yBAAyB;IAoCpD;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIlC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAO5C,iGAAiG;IACjG,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIlD,+GAA+G;IAC/G,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAK5B,oGAAoG;IACpG,MAAM,IAAI,WAAW,CAAC,MAAM,CAAC;IAO7B,yDAAyD;IACzD,YAAY,IAAI,MAAM,EAAE;IAIxB,mHAAmH;IACnH,YAAY,IAAI,MAAM,EAAE;CAGzB"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which published generations a LIVE run may still be reading (design v3 DECISION 1: "old
|
|
3
|
+
* generations are reaped only after no live session references them").
|
|
4
|
+
*
|
|
5
|
+
* The engine resolves `WICKED_SKILLS_SNAPSHOT` at every worker spawn and reports the generation it
|
|
6
|
+
* ADMITTED as a `skillsSnapshotHanded` CoreEvent ({session, gen, …}) — the authoritative record of
|
|
7
|
+
* which generation each session's spawn was actually handed. Crew keeps TWO kinds of pin:
|
|
8
|
+
*
|
|
9
|
+
* session pins a session is tracked from its first event; a `skillsSnapshotHanded` pins the
|
|
10
|
+
* EXACT generation the engine reports (its `gen` field), never "whatever `current`
|
|
11
|
+
* happens to be when crew observes some later event" (codex round 3); every
|
|
12
|
+
* generation published WHILE the session is live is pinned to it too — its next
|
|
13
|
+
* unit spawn reads (and will report) the new `current`; the terminal frame
|
|
14
|
+
* (`sessionCompleted` / `sessionFailed` / `runCancelled` — the same triple
|
|
15
|
+
* api/seat-health.ts drops assignments on) releases every pin the session held.
|
|
16
|
+
*
|
|
17
|
+
* launch pins EXPLICIT ACCOUNTING for the gap between a launch and the engine's report. When
|
|
18
|
+
* the daemon hands a launch to the engine (`CoreAdapter.onLaunch`: launchRun,
|
|
19
|
+
* resumeRun, confirmGate, launchCampaign, resumeCampaign — every path a spawn can
|
|
20
|
+
* come from; the campaign's own DAG-node runs are pinned as the engine announces
|
|
21
|
+
* them, `campaignNodeStarted`, under the campaign's umbrella pin), crew opens a pin
|
|
22
|
+
* for that launch holding the generation the env exports RIGHT NOW, and every
|
|
23
|
+
* generation published after that is added to it (the spawn may read any of them).
|
|
24
|
+
* A launch pin is released ONLY when the engine reports the handed generation for
|
|
25
|
+
* that session (`skillsSnapshotHanded` — the session pin takes over), when the
|
|
26
|
+
* run/campaign reaches a terminal state, or when the engine REJECTED the launch
|
|
27
|
+
* (nothing will ever read it). It never expires by publish count or age (codex
|
|
28
|
+
* round 4: a bounded window let a run's generation be reaped while its handoff
|
|
29
|
+
* report was still outstanding — publish count bounds nothing about handoff delay).
|
|
30
|
+
* Reaping skips any generation with an unreleased launch pin.
|
|
31
|
+
*
|
|
32
|
+
* The ledger is in-memory: the engine is in-process, so a daemon restart ends every worker it
|
|
33
|
+
* spawned, and a resumed run's launch (`resumeRun`) opens a fresh launch pin.
|
|
34
|
+
*/
|
|
35
|
+
/** The frames after which a session spawns nothing more. */
|
|
36
|
+
const TERMINAL_EVENT_TYPES = new Set(['sessionCompleted', 'sessionFailed', 'runCancelled']);
|
|
37
|
+
/** The frames after which a campaign launches no more node runs. */
|
|
38
|
+
const CAMPAIGN_TERMINAL_EVENT_TYPES = new Set(['campaignCompleted', 'campaignFailed', 'campaignCancelled']);
|
|
39
|
+
/** The engine's per-spawn record of the generation it handed a session. */
|
|
40
|
+
const SKILLS_SNAPSHOT_HANDED = 'skillsSnapshotHanded';
|
|
41
|
+
/** The engine's announcement of a campaign DAG-node run: `{campaign, node, runId}`. */
|
|
42
|
+
const CAMPAIGN_NODE_STARTED = 'campaignNodeStarted';
|
|
43
|
+
/** The generation a `skillsSnapshotHanded` reports, or `null` (a live-cache fallback carries no
|
|
44
|
+
* reapable generation — `gen: null`). */
|
|
45
|
+
function handedGeneration(event) {
|
|
46
|
+
const gen = event.gen;
|
|
47
|
+
if (typeof gen !== 'string' || !/^\d+$/.test(gen))
|
|
48
|
+
return null;
|
|
49
|
+
const n = Number(gen);
|
|
50
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
51
|
+
}
|
|
52
|
+
function nonEmpty(value) {
|
|
53
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
export class LiveGenerations {
|
|
56
|
+
/** Session pins: session id → the generations the engine reported + every one published since. */
|
|
57
|
+
sessions = new Map();
|
|
58
|
+
/** Launch pins: `run:<id>` / `campaign:<id>` → the generations the launch may read, until it is accounted for. */
|
|
59
|
+
launchPins = new Map();
|
|
60
|
+
/** The generation the env exports right now (`exported`), or `null` when none is (fallback / unpublished). */
|
|
61
|
+
exportedGen = null;
|
|
62
|
+
static key(kind, id) {
|
|
63
|
+
return `${kind}:${id}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fold one CoreEvent: track a live session, pin the EXACT generation the engine reports on a
|
|
67
|
+
* `skillsSnapshotHanded` (releasing that session's launch pin — the engine has accounted for the
|
|
68
|
+
* launch), release on a terminal frame; pin a campaign's node run as the engine announces it and
|
|
69
|
+
* release the campaign's pin on its terminal frame. A non-`skillsSnapshotHanded` session event
|
|
70
|
+
* only tracks the session — it never pins "current at event time" (the bug codex round 3
|
|
71
|
+
* replaced): the generation a spawn actually used comes from the engine's own report, and the
|
|
72
|
+
* pre-report window is covered by the launch pin.
|
|
73
|
+
*/
|
|
74
|
+
observe(event) {
|
|
75
|
+
const campaign = nonEmpty(event.campaign);
|
|
76
|
+
if (campaign !== undefined && event.session === undefined) {
|
|
77
|
+
if (CAMPAIGN_TERMINAL_EVENT_TYPES.has(event.type)) {
|
|
78
|
+
return this.launchPins.delete(LiveGenerations.key('campaign', campaign)) ? 'released' : 'ignored';
|
|
79
|
+
}
|
|
80
|
+
if (event.type === CAMPAIGN_NODE_STARTED) {
|
|
81
|
+
const runId = nonEmpty(event.runId);
|
|
82
|
+
if (runId === undefined)
|
|
83
|
+
return 'ignored';
|
|
84
|
+
this.launched('run', runId);
|
|
85
|
+
return 'pinned';
|
|
86
|
+
}
|
|
87
|
+
return 'ignored';
|
|
88
|
+
}
|
|
89
|
+
const session = nonEmpty(event.session);
|
|
90
|
+
if (session === undefined)
|
|
91
|
+
return 'ignored';
|
|
92
|
+
if (TERMINAL_EVENT_TYPES.has(event.type)) {
|
|
93
|
+
const hadSession = this.sessions.delete(session);
|
|
94
|
+
const hadLaunch = this.launchPins.delete(LiveGenerations.key('run', session));
|
|
95
|
+
return hadSession || hadLaunch ? 'released' : 'ignored';
|
|
96
|
+
}
|
|
97
|
+
let gens = this.sessions.get(session);
|
|
98
|
+
if (gens === undefined) {
|
|
99
|
+
gens = new Set();
|
|
100
|
+
this.sessions.set(session, gens);
|
|
101
|
+
}
|
|
102
|
+
if (event.type === SKILLS_SNAPSHOT_HANDED) {
|
|
103
|
+
const gen = handedGeneration(event);
|
|
104
|
+
if (gen !== null)
|
|
105
|
+
gens.add(gen);
|
|
106
|
+
// The engine has accounted for this launch — which generation it used (or that it used
|
|
107
|
+
// none: a live-cache fallback). The durable session pin lives until the terminal frame.
|
|
108
|
+
this.launchPins.delete(LiveGenerations.key('run', session));
|
|
109
|
+
}
|
|
110
|
+
return 'pinned';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The env now exports generation `gen` to new launches (`WICKED_SKILLS_SNAPSHOT`;
|
|
114
|
+
* runtime.afterPublish), or nothing (`null`). A launch pin opened after this starts from it.
|
|
115
|
+
*/
|
|
116
|
+
exported(gen) {
|
|
117
|
+
this.exportedGen = gen;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The daemon is handing a launch to the engine (before the engine call, so no spawn can read the
|
|
121
|
+
* env ahead of the pin): open its launch pin at the generation the env exports right now. The pin
|
|
122
|
+
* accumulates every later publish (`published`) and is released only by the engine's report for
|
|
123
|
+
* that session, the run's / campaign's terminal frame, or `launchRejected`.
|
|
124
|
+
*/
|
|
125
|
+
launched(kind, id) {
|
|
126
|
+
const key = LiveGenerations.key(kind, id);
|
|
127
|
+
const gens = this.launchPins.get(key) ?? new Set();
|
|
128
|
+
if (this.exportedGen !== null)
|
|
129
|
+
gens.add(this.exportedGen);
|
|
130
|
+
this.launchPins.set(key, gens);
|
|
131
|
+
}
|
|
132
|
+
/** The engine REFUSED the launch (the call rejected): nothing will read it — release its pin. */
|
|
133
|
+
launchRejected(kind, id) {
|
|
134
|
+
this.launchPins.delete(LiveGenerations.key(kind, id));
|
|
135
|
+
}
|
|
136
|
+
/** A generation was just published: every live session and every open launch may read it at its next spawn. */
|
|
137
|
+
published(gen) {
|
|
138
|
+
for (const gens of this.sessions.values())
|
|
139
|
+
gens.add(gen);
|
|
140
|
+
for (const gens of this.launchPins.values())
|
|
141
|
+
gens.add(gen);
|
|
142
|
+
}
|
|
143
|
+
/** The union of every live session's pins and every open launch pin — what the reaper must keep. */
|
|
144
|
+
pinned() {
|
|
145
|
+
const out = new Set();
|
|
146
|
+
for (const gens of this.sessions.values())
|
|
147
|
+
for (const gen of gens)
|
|
148
|
+
out.add(gen);
|
|
149
|
+
for (const gens of this.launchPins.values())
|
|
150
|
+
for (const gen of gens)
|
|
151
|
+
out.add(gen);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
/** Live sessions being tracked (diagnostics + tests). */
|
|
155
|
+
liveSessions() {
|
|
156
|
+
return [...this.sessions.keys()].sort();
|
|
157
|
+
}
|
|
158
|
+
/** Launches handed to the engine and not yet accounted for (`run:<id>` / `campaign:<id>`; diagnostics + tests). */
|
|
159
|
+
openLaunches() {
|
|
160
|
+
return [...this.launchPins.keys()].sort();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=live-generations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-generations.js","sourceRoot":"","sources":["../../src/skills/live-generations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the shipped baseline comes from: the LIVE installed wicked-garden plugin.
|
|
3
|
+
*
|
|
4
|
+
* The daemon seeds (and refreshes) its skills root from the plugin Claude Code actually runs —
|
|
5
|
+
* the marketplace cache under `<CLAUDE_CONFIG_DIR|~/.claude>/plugins/cache/wicked-garden/
|
|
6
|
+
* wicked-garden/<version>` (highest version wins). That is the ONLY automatic source (design v3
|
|
7
|
+
* §3/§8; codex review of #480): the hand-installed `<config dir>/plugins/wicked-garden` copy is the
|
|
8
|
+
* exact stale artifact the operator's `clis.toml` hack pointed workers at (12.28.1 while the live
|
|
9
|
+
* cache was 12.32.0 — design v3 §"Verified mechanics"), so it is never a fallback. NEVER a repo
|
|
10
|
+
* checkout by default either. `WICKED_CREW_SKILLS_SOURCE` is the one explicit override — for
|
|
11
|
+
* tests, and for an operator who deliberately wants a checkout or that hand copy. A machine without
|
|
12
|
+
* the cache has no source at all: crew does not vendor garden — the seed says "install garden
|
|
13
|
+
* first" loudly (`SkillsSourceUnavailableError`) and the runtime leaves the engine input unset.
|
|
14
|
+
*
|
|
15
|
+
* # No-follow BELOW the root (codex round 6 on #480)
|
|
16
|
+
*
|
|
17
|
+
* The source ROOT may legitimately be reached through a symlink — operators symlink `~/.claude`,
|
|
18
|
+
* and the marketplace cache may sit behind a linked config dir — so the root is resolved ONCE
|
|
19
|
+
* (`realPluginRoot`, the one place a link is followed) and every designated entry BELOW it is
|
|
20
|
+
* lstat-walked (`noFollowEntry`): the manifest dir, `plugin.json`, each catalog, each root file,
|
|
21
|
+
* each bundle directory and everything inside it. A symlink among them is refused by name
|
|
22
|
+
* (`PluginSourceSymlinkError`) — a seed refuses to start (the runtime's `skills.config`), a
|
|
23
|
+
* refresh answers a `blocked` envelope — and nothing is copied: a linked catalog or a linked skill
|
|
24
|
+
* dir would otherwise copy bytes from outside the declared source into the daemon's root and hand
|
|
25
|
+
* them to every worker.
|
|
26
|
+
*/
|
|
27
|
+
import type { SkillSourceKind } from '../core/types.js';
|
|
28
|
+
/** Explicit plugin-source override — the seam tests and proof scripts aim at a fixture plugin. */
|
|
29
|
+
export declare const SKILLS_SOURCE_ENV = "WICKED_CREW_SKILLS_SOURCE";
|
|
30
|
+
/** Relative path of the Claude Code plugin manifest inside a plugin root. */
|
|
31
|
+
export declare const PLUGIN_MANIFEST_REL: string;
|
|
32
|
+
export interface PluginSource {
|
|
33
|
+
/** Absolute plugin root (the directory holding `.claude-plugin/plugin.json`). */
|
|
34
|
+
path: string;
|
|
35
|
+
kind: SkillSourceKind;
|
|
36
|
+
/** `version` from the plugin manifest. */
|
|
37
|
+
plugin_version: string;
|
|
38
|
+
}
|
|
39
|
+
/** The Claude Code config dir the plugin cache lives under: `CLAUDE_CONFIG_DIR`, else `~/.claude`. */
|
|
40
|
+
export declare function claudeConfigDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* A designated entry of the plugin source is a symlink — the seed/refresh is refused by name and
|
|
43
|
+
* nothing is copied (module header). `entry` is the plugin-relative POSIX path of the link.
|
|
44
|
+
*/
|
|
45
|
+
export declare class PluginSourceSymlinkError extends Error {
|
|
46
|
+
readonly root: string;
|
|
47
|
+
readonly entry: string;
|
|
48
|
+
readonly target: string;
|
|
49
|
+
constructor(root: string, entry: string, target: string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The plugin root with every link resolved — the ONE place a link is followed (an operator's
|
|
53
|
+
* symlinked config dir reaches the root). `null` when it does not exist or is not a directory.
|
|
54
|
+
*/
|
|
55
|
+
export declare function realPluginRoot(dir: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* `join(root, ...segments)` after lstat-walking every segment BELOW the canonical root: a symlink at
|
|
58
|
+
* any of them throws `PluginSourceSymlinkError` naming it; a component that does not exist (or sits
|
|
59
|
+
* below a regular file) answers `null`. `sourceSpelling` is the root as the operator spelled it, for
|
|
60
|
+
* the error.
|
|
61
|
+
*/
|
|
62
|
+
export declare function noFollowEntry(root: string, segments: ReadonlyArray<string>, sourceSpelling?: string): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* The plugin manifest's `version` at `dir`, or `null` when `dir` is not a plugin root. The manifest
|
|
65
|
+
* is read NO-FOLLOW below the (once-resolved) root: a symlinked `.claude-plugin/` or `plugin.json`
|
|
66
|
+
* throws `PluginSourceSymlinkError` — never a version read through a link (codex round 6).
|
|
67
|
+
*/
|
|
68
|
+
export declare function pluginVersionAt(dir: string): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* A TOTAL order over version strings, so "highest version wins" never depends on `readdir` order
|
|
71
|
+
* (Copilot on #480). Numeric dotted order first (`12.9.0` < `12.32.0`; a shorter release sorts
|
|
72
|
+
* before a longer one with the same prefix; a non-integer segment sorts below any integer). A
|
|
73
|
+
* prerelease suffix (`12.0.0-beta`, everything after the first `-`) sorts BELOW its release
|
|
74
|
+
* (`12.0.0-beta` < `12.0.0`), and two prereleases compare identifier by identifier the semver way
|
|
75
|
+
* (numeric identifiers numerically and before alphanumeric ones, then lexically, shorter first).
|
|
76
|
+
* Distinct strings NEVER compare equal: whatever survives all of that (`12.00.0` vs `12.0.0`) is
|
|
77
|
+
* ordered lexically.
|
|
78
|
+
*/
|
|
79
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
80
|
+
/**
|
|
81
|
+
* Classify a plugin root: a git checkout when it carries `.git`; the installed plugin when it
|
|
82
|
+
* sits under a `plugins/` directory (the marketplace cache or the hand-installed copy); anything
|
|
83
|
+
* else is an explicit plugin-shaped directory (an unpacked tarball, a test fixture).
|
|
84
|
+
*/
|
|
85
|
+
export declare function classifySource(dir: string): SkillSourceKind;
|
|
86
|
+
/** A `PluginSource` for an explicit plugin root, or `null` when it is not a plugin root. */
|
|
87
|
+
export declare function pluginSourceAt(dir: string): PluginSource | null;
|
|
88
|
+
/** The marketplace cache dir the live plugin versions live under, for a config dir. */
|
|
89
|
+
export declare function livePluginCacheDir(configDir: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Discover the live installed plugin: the explicit `WICKED_CREW_SKILLS_SOURCE` override, else the
|
|
92
|
+
* highest version in the marketplace cache. NOTHING else — in particular not the hand-installed
|
|
93
|
+
* `plugins/wicked-garden` copy (see the module header). `env`/`home` are injectable so tests never
|
|
94
|
+
* read the developer's real config dir. Returns `null` when nothing is installed — the caller says
|
|
95
|
+
* "install garden first" loudly.
|
|
96
|
+
*/
|
|
97
|
+
export declare function discoverLivePlugin(opts?: {
|
|
98
|
+
env?: NodeJS.ProcessEnv;
|
|
99
|
+
home?: string;
|
|
100
|
+
}): PluginSource | null;
|
|
101
|
+
export interface GitState {
|
|
102
|
+
git_sha: string | null;
|
|
103
|
+
git_dirty: boolean;
|
|
104
|
+
/** Present when the source is a checkout but git could not answer — the caller logs it. */
|
|
105
|
+
error?: string;
|
|
106
|
+
}
|
|
107
|
+
/** The checkout's HEAD sha + dirty flag; `{null,false}` for a non-checkout source. */
|
|
108
|
+
export declare function gitStateOf(source: PluginSource): GitState;
|
|
109
|
+
//# sourceMappingURL=plugin-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-source.d.ts","sourceRoot":"","sources":["../../src/skills/plugin-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAOH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIxD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,QAAwC,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;IACtB,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAGtG;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAE/C,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;CAS1B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAE,MAAa,GAAG,MAAM,GAAG,IAAI,CAezH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa5D;AA+CD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAI3D;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAG/D;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,YAAY,GAAG,IAAI,CAerB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAuBzD"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the shipped baseline comes from: the LIVE installed wicked-garden plugin.
|
|
3
|
+
*
|
|
4
|
+
* The daemon seeds (and refreshes) its skills root from the plugin Claude Code actually runs —
|
|
5
|
+
* the marketplace cache under `<CLAUDE_CONFIG_DIR|~/.claude>/plugins/cache/wicked-garden/
|
|
6
|
+
* wicked-garden/<version>` (highest version wins). That is the ONLY automatic source (design v3
|
|
7
|
+
* §3/§8; codex review of #480): the hand-installed `<config dir>/plugins/wicked-garden` copy is the
|
|
8
|
+
* exact stale artifact the operator's `clis.toml` hack pointed workers at (12.28.1 while the live
|
|
9
|
+
* cache was 12.32.0 — design v3 §"Verified mechanics"), so it is never a fallback. NEVER a repo
|
|
10
|
+
* checkout by default either. `WICKED_CREW_SKILLS_SOURCE` is the one explicit override — for
|
|
11
|
+
* tests, and for an operator who deliberately wants a checkout or that hand copy. A machine without
|
|
12
|
+
* the cache has no source at all: crew does not vendor garden — the seed says "install garden
|
|
13
|
+
* first" loudly (`SkillsSourceUnavailableError`) and the runtime leaves the engine input unset.
|
|
14
|
+
*
|
|
15
|
+
* # No-follow BELOW the root (codex round 6 on #480)
|
|
16
|
+
*
|
|
17
|
+
* The source ROOT may legitimately be reached through a symlink — operators symlink `~/.claude`,
|
|
18
|
+
* and the marketplace cache may sit behind a linked config dir — so the root is resolved ONCE
|
|
19
|
+
* (`realPluginRoot`, the one place a link is followed) and every designated entry BELOW it is
|
|
20
|
+
* lstat-walked (`noFollowEntry`): the manifest dir, `plugin.json`, each catalog, each root file,
|
|
21
|
+
* each bundle directory and everything inside it. A symlink among them is refused by name
|
|
22
|
+
* (`PluginSourceSymlinkError`) — a seed refuses to start (the runtime's `skills.config`), a
|
|
23
|
+
* refresh answers a `blocked` envelope — and nothing is copied: a linked catalog or a linked skill
|
|
24
|
+
* dir would otherwise copy bytes from outside the declared source into the daemon's root and hand
|
|
25
|
+
* them to every worker.
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, realpathSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join, sep } from 'node:path';
|
|
31
|
+
import { PLUGIN_NAME } from './frontmatter.js';
|
|
32
|
+
import { readFileNoFollow } from './tree.js';
|
|
33
|
+
/** Explicit plugin-source override — the seam tests and proof scripts aim at a fixture plugin. */
|
|
34
|
+
export const SKILLS_SOURCE_ENV = 'WICKED_CREW_SKILLS_SOURCE';
|
|
35
|
+
/** Relative path of the Claude Code plugin manifest inside a plugin root. */
|
|
36
|
+
export const PLUGIN_MANIFEST_REL = join('.claude-plugin', 'plugin.json');
|
|
37
|
+
/** The Claude Code config dir the plugin cache lives under: `CLAUDE_CONFIG_DIR`, else `~/.claude`. */
|
|
38
|
+
export function claudeConfigDir(env = process.env, home = homedir()) {
|
|
39
|
+
const configured = env['CLAUDE_CONFIG_DIR'];
|
|
40
|
+
return configured !== undefined && configured !== '' ? configured : join(home, '.claude');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A designated entry of the plugin source is a symlink — the seed/refresh is refused by name and
|
|
44
|
+
* nothing is copied (module header). `entry` is the plugin-relative POSIX path of the link.
|
|
45
|
+
*/
|
|
46
|
+
export class PluginSourceSymlinkError extends Error {
|
|
47
|
+
root;
|
|
48
|
+
entry;
|
|
49
|
+
target;
|
|
50
|
+
constructor(root, entry, target) {
|
|
51
|
+
super(`plugin source ${root}: ${entry} is a symlink (-> ${target}) — the plugin source is copied into the daemon's skills root and ` +
|
|
52
|
+
'handed to every worker, so a link among its designated files or directories would copy whatever it reaches; ' +
|
|
53
|
+
'refused, nothing was copied (the source root itself may be reached through a link; its contents may not)');
|
|
54
|
+
this.root = root;
|
|
55
|
+
this.entry = entry;
|
|
56
|
+
this.target = target;
|
|
57
|
+
this.name = 'PluginSourceSymlinkError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The plugin root with every link resolved — the ONE place a link is followed (an operator's
|
|
62
|
+
* symlinked config dir reaches the root). `null` when it does not exist or is not a directory.
|
|
63
|
+
*/
|
|
64
|
+
export function realPluginRoot(dir) {
|
|
65
|
+
let real;
|
|
66
|
+
try {
|
|
67
|
+
real = realpathSync(dir);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code === 'ENOENT' || err.code === 'ENOTDIR')
|
|
71
|
+
return null;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
return lstatSync(real).isDirectory() ? real : null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `join(root, ...segments)` after lstat-walking every segment BELOW the canonical root: a symlink at
|
|
78
|
+
* any of them throws `PluginSourceSymlinkError` naming it; a component that does not exist (or sits
|
|
79
|
+
* below a regular file) answers `null`. `sourceSpelling` is the root as the operator spelled it, for
|
|
80
|
+
* the error.
|
|
81
|
+
*/
|
|
82
|
+
export function noFollowEntry(root, segments, sourceSpelling = root) {
|
|
83
|
+
let cur = root;
|
|
84
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
85
|
+
cur = join(cur, segments[i]);
|
|
86
|
+
let st;
|
|
87
|
+
try {
|
|
88
|
+
st = lstatSync(cur);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const code = err.code;
|
|
92
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
93
|
+
return null;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
if (st.isSymbolicLink())
|
|
97
|
+
throw new PluginSourceSymlinkError(sourceSpelling, segments.slice(0, i + 1).join('/'), readlinkSync(cur));
|
|
98
|
+
}
|
|
99
|
+
return cur;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The plugin manifest's `version` at `dir`, or `null` when `dir` is not a plugin root. The manifest
|
|
103
|
+
* is read NO-FOLLOW below the (once-resolved) root: a symlinked `.claude-plugin/` or `plugin.json`
|
|
104
|
+
* throws `PluginSourceSymlinkError` — never a version read through a link (codex round 6).
|
|
105
|
+
*/
|
|
106
|
+
export function pluginVersionAt(dir) {
|
|
107
|
+
const root = realPluginRoot(dir);
|
|
108
|
+
if (root === null)
|
|
109
|
+
return null;
|
|
110
|
+
const manifest = noFollowEntry(root, PLUGIN_MANIFEST_REL.split(sep), dir);
|
|
111
|
+
if (manifest === null || !lstatSync(manifest).isFile())
|
|
112
|
+
return null;
|
|
113
|
+
const parsed = JSON.parse(readFileNoFollow(manifest).toString('utf8')); // the entry the walk judged is the one read (v3.5 §3)
|
|
114
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
115
|
+
return null;
|
|
116
|
+
const version = parsed.version;
|
|
117
|
+
return typeof version === 'string' && version !== '' ? version : null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A TOTAL order over version strings, so "highest version wins" never depends on `readdir` order
|
|
121
|
+
* (Copilot on #480). Numeric dotted order first (`12.9.0` < `12.32.0`; a shorter release sorts
|
|
122
|
+
* before a longer one with the same prefix; a non-integer segment sorts below any integer). A
|
|
123
|
+
* prerelease suffix (`12.0.0-beta`, everything after the first `-`) sorts BELOW its release
|
|
124
|
+
* (`12.0.0-beta` < `12.0.0`), and two prereleases compare identifier by identifier the semver way
|
|
125
|
+
* (numeric identifiers numerically and before alphanumeric ones, then lexically, shorter first).
|
|
126
|
+
* Distinct strings NEVER compare equal: whatever survives all of that (`12.00.0` vs `12.0.0`) is
|
|
127
|
+
* ordered lexically.
|
|
128
|
+
*/
|
|
129
|
+
export function compareVersions(a, b) {
|
|
130
|
+
if (a === b)
|
|
131
|
+
return 0;
|
|
132
|
+
const [coreA, preA] = splitPrerelease(a);
|
|
133
|
+
const [coreB, preB] = splitPrerelease(b);
|
|
134
|
+
const core = compareDotted(coreA, coreB);
|
|
135
|
+
if (core !== 0)
|
|
136
|
+
return core;
|
|
137
|
+
if (preA === null && preB !== null)
|
|
138
|
+
return 1;
|
|
139
|
+
if (preA !== null && preB === null)
|
|
140
|
+
return -1;
|
|
141
|
+
if (preA !== null && preB !== null) {
|
|
142
|
+
const pre = comparePrerelease(preA, preB);
|
|
143
|
+
if (pre !== 0)
|
|
144
|
+
return pre;
|
|
145
|
+
}
|
|
146
|
+
return a < b ? -1 : 1;
|
|
147
|
+
}
|
|
148
|
+
/** `12.0.0-beta.1` → [`12.0.0`, `beta.1`]; no `-` → [`v`, null]. */
|
|
149
|
+
function splitPrerelease(v) {
|
|
150
|
+
const dash = v.indexOf('-');
|
|
151
|
+
return dash === -1 ? [v, null] : [v.slice(0, dash), v.slice(dash + 1)];
|
|
152
|
+
}
|
|
153
|
+
/** Dotted numeric segments: shorter-first on a shared prefix, a non-integer segment below any integer. */
|
|
154
|
+
function compareDotted(a, b) {
|
|
155
|
+
const pa = a.split('.');
|
|
156
|
+
const pb = b.split('.');
|
|
157
|
+
const n = Math.max(pa.length, pb.length);
|
|
158
|
+
for (let i = 0; i < n; i += 1) {
|
|
159
|
+
const sa = pa[i];
|
|
160
|
+
const sb = pb[i];
|
|
161
|
+
if (sa === undefined)
|
|
162
|
+
return -1;
|
|
163
|
+
if (sb === undefined)
|
|
164
|
+
return 1;
|
|
165
|
+
const na = /^\d+$/.test(sa) ? Number(sa) : -1;
|
|
166
|
+
const nb = /^\d+$/.test(sb) ? Number(sb) : -1;
|
|
167
|
+
if (na !== nb)
|
|
168
|
+
return na < nb ? -1 : 1;
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
/** Semver prerelease identifiers: numeric before alphanumeric, numeric numerically, else lexically; shorter first. */
|
|
173
|
+
function comparePrerelease(a, b) {
|
|
174
|
+
const pa = a.split('.');
|
|
175
|
+
const pb = b.split('.');
|
|
176
|
+
const n = Math.max(pa.length, pb.length);
|
|
177
|
+
for (let i = 0; i < n; i += 1) {
|
|
178
|
+
const sa = pa[i];
|
|
179
|
+
const sb = pb[i];
|
|
180
|
+
if (sa === undefined)
|
|
181
|
+
return -1;
|
|
182
|
+
if (sb === undefined)
|
|
183
|
+
return 1;
|
|
184
|
+
const numA = /^\d+$/.test(sa);
|
|
185
|
+
const numB = /^\d+$/.test(sb);
|
|
186
|
+
if (numA && numB) {
|
|
187
|
+
if (Number(sa) !== Number(sb))
|
|
188
|
+
return Number(sa) < Number(sb) ? -1 : 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (numA !== numB)
|
|
192
|
+
return numA ? -1 : 1;
|
|
193
|
+
if (sa !== sb)
|
|
194
|
+
return sa < sb ? -1 : 1;
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Classify a plugin root: a git checkout when it carries `.git`; the installed plugin when it
|
|
200
|
+
* sits under a `plugins/` directory (the marketplace cache or the hand-installed copy); anything
|
|
201
|
+
* else is an explicit plugin-shaped directory (an unpacked tarball, a test fixture).
|
|
202
|
+
*/
|
|
203
|
+
export function classifySource(dir) {
|
|
204
|
+
if (existsSync(join(dir, '.git')))
|
|
205
|
+
return 'checkout';
|
|
206
|
+
if (dir.split(sep).includes('plugins'))
|
|
207
|
+
return 'claude-plugin-cache';
|
|
208
|
+
return 'directory';
|
|
209
|
+
}
|
|
210
|
+
/** A `PluginSource` for an explicit plugin root, or `null` when it is not a plugin root. */
|
|
211
|
+
export function pluginSourceAt(dir) {
|
|
212
|
+
const version = pluginVersionAt(dir);
|
|
213
|
+
return version === null ? null : { path: dir, kind: classifySource(dir), plugin_version: version };
|
|
214
|
+
}
|
|
215
|
+
/** The marketplace cache dir the live plugin versions live under, for a config dir. */
|
|
216
|
+
export function livePluginCacheDir(configDir) {
|
|
217
|
+
return join(configDir, 'plugins', 'cache', PLUGIN_NAME, PLUGIN_NAME);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Discover the live installed plugin: the explicit `WICKED_CREW_SKILLS_SOURCE` override, else the
|
|
221
|
+
* highest version in the marketplace cache. NOTHING else — in particular not the hand-installed
|
|
222
|
+
* `plugins/wicked-garden` copy (see the module header). `env`/`home` are injectable so tests never
|
|
223
|
+
* read the developer's real config dir. Returns `null` when nothing is installed — the caller says
|
|
224
|
+
* "install garden first" loudly.
|
|
225
|
+
*/
|
|
226
|
+
export function discoverLivePlugin(opts = {}) {
|
|
227
|
+
const env = opts.env ?? process.env;
|
|
228
|
+
const home = opts.home ?? homedir();
|
|
229
|
+
const override = env[SKILLS_SOURCE_ENV];
|
|
230
|
+
if (override !== undefined && override !== '')
|
|
231
|
+
return pluginSourceAt(override);
|
|
232
|
+
const cache = livePluginCacheDir(claudeConfigDir(env, home));
|
|
233
|
+
if (!existsSync(cache))
|
|
234
|
+
return null;
|
|
235
|
+
const versions = readdirSync(cache)
|
|
236
|
+
.filter((entry) => pluginVersionAt(join(cache, entry)) !== null)
|
|
237
|
+
.sort(compareVersions);
|
|
238
|
+
const top = versions[versions.length - 1];
|
|
239
|
+
if (top === undefined)
|
|
240
|
+
return null;
|
|
241
|
+
const dir = join(cache, top);
|
|
242
|
+
return { path: dir, kind: 'claude-plugin-cache', plugin_version: pluginVersionAt(dir) ?? top };
|
|
243
|
+
}
|
|
244
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
245
|
+
/** The checkout's HEAD sha + dirty flag; `{null,false}` for a non-checkout source. */
|
|
246
|
+
export function gitStateOf(source) {
|
|
247
|
+
if (source.kind !== 'checkout')
|
|
248
|
+
return { git_sha: null, git_dirty: false };
|
|
249
|
+
try {
|
|
250
|
+
const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
251
|
+
cwd: source.path,
|
|
252
|
+
encoding: 'utf8',
|
|
253
|
+
timeout: GIT_TIMEOUT_MS,
|
|
254
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
255
|
+
}).trim();
|
|
256
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
257
|
+
cwd: source.path,
|
|
258
|
+
encoding: 'utf8',
|
|
259
|
+
timeout: GIT_TIMEOUT_MS,
|
|
260
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
261
|
+
});
|
|
262
|
+
return { git_sha: sha, git_dirty: status.trim() !== '' };
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
return {
|
|
266
|
+
git_sha: null,
|
|
267
|
+
git_dirty: false,
|
|
268
|
+
error: err instanceof Error ? err.message : String(err),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=plugin-source.js.map
|