@skyf0xx/hedgehog 5.1.3 → 5.1.5
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/cli.mjs +86 -9
- package/package.json +1 -1
- package/src/hosts/claude/DISPATCH.md +8 -0
- package/src/hosts/version.mjs +23 -0
package/bin/cli.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile } from 'node:
|
|
|
17
17
|
import { constants, existsSync } from 'node:fs';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
20
|
-
import { spawn } from 'node:child_process';
|
|
20
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
21
21
|
import { dbInit, DB_PATH, dbAbsPath, openDb } from '../src/db/init.mjs';
|
|
22
22
|
import { loadCore, lintCore, isModuleAxis } from '../src/db/core.mjs';
|
|
23
23
|
import { planTasks, CORE_INTENT_ID } from '../src/db/plan.mjs';
|
|
@@ -52,7 +52,12 @@ import { rebuildDb } from '../src/db/rebuild.mjs';
|
|
|
52
52
|
import { loadOverrides, addOverride, orphanedOverrides, OVERRIDES_DIR } from '../src/db/overrides.mjs';
|
|
53
53
|
import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
|
|
54
54
|
import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
recordVersion,
|
|
57
|
+
checkForUpdate,
|
|
58
|
+
checkBinaryStaleness,
|
|
59
|
+
installedVersion,
|
|
60
|
+
} from '../src/hosts/version.mjs';
|
|
56
61
|
import { loadRegistry, resolveCore } from '../src/registry/index.mjs';
|
|
57
62
|
import { fetchCore, cachedCore, cachedVersions } from '../src/registry/fetch.mjs';
|
|
58
63
|
import { recordCore, installedCore } from '../src/registry/installed.mjs';
|
|
@@ -681,7 +686,11 @@ async function init({ force, core, host = DEFAULT_HOST, hostOnly = false }) {
|
|
|
681
686
|
dim(
|
|
682
687
|
` Read ${bold(`${agents}/planner.md`)} and follow it — it runs planning\n` +
|
|
683
688
|
` intake (${skills}/hedgehog-planning-intake/SKILL.md), then hands\n` +
|
|
684
|
-
` off to ${agents}/bootstrap.md
|
|
689
|
+
` off to ${agents}/bootstrap.md.\n` +
|
|
690
|
+
` Some hosts register agents/skills once, at session start: if a\n` +
|
|
691
|
+
` later dispatch by name reports one of these as not found, read\n` +
|
|
692
|
+
` its file directly instead — it becomes dispatchable by name after\n` +
|
|
693
|
+
` a session restart or a fresh context.`,
|
|
685
694
|
),
|
|
686
695
|
);
|
|
687
696
|
console.log();
|
|
@@ -806,6 +815,14 @@ async function update({ hosts }) {
|
|
|
806
815
|
'write-once content.',
|
|
807
816
|
),
|
|
808
817
|
);
|
|
818
|
+
console.log(
|
|
819
|
+
dim(
|
|
820
|
+
'Some hosts register agents/skills once, at session start: a renamed or\n' +
|
|
821
|
+
'newly-added one may not be dispatchable by name until this session\n' +
|
|
822
|
+
'restarts or a fresh context starts — read its file directly if a\n' +
|
|
823
|
+
'name-based dispatch reports it as not found.',
|
|
824
|
+
),
|
|
825
|
+
);
|
|
809
826
|
}
|
|
810
827
|
|
|
811
828
|
// Returned by resolveInstalledCore when the project has a core but its
|
|
@@ -1363,17 +1380,76 @@ async function noteAvailableUpdate() {
|
|
|
1363
1380
|
if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
|
|
1364
1381
|
try {
|
|
1365
1382
|
const { installed, latest, stale } = await checkForUpdate(DEST_ROOT);
|
|
1366
|
-
if (
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1383
|
+
if (stale) {
|
|
1384
|
+
console.error(
|
|
1385
|
+
`\n${yellow(bold('Hedgehog update available.'))} ${dim(
|
|
1386
|
+
`${installed} → ${latest}. Run \`npx @skyf0xx/hedgehog@latest update\` to refresh this project's agents and skills.`,
|
|
1387
|
+
)}`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1372
1390
|
} catch {
|
|
1373
1391
|
// Advisory only — a failed check is never worth failing a command over.
|
|
1374
1392
|
}
|
|
1375
1393
|
}
|
|
1376
1394
|
|
|
1395
|
+
// Hedgehog runs as a global install: every host (Claude Code, Cursor,
|
|
1396
|
+
// Gemini CLI) drives it through the `hedgehog` binary on PATH, and `npx
|
|
1397
|
+
// @skyf0xx/hedgehog` is expected to arrive at that same global install
|
|
1398
|
+
// rather than an ephemeral, unmanaged copy. This makes both halves of
|
|
1399
|
+
// that true on every invocation, before any command's real work starts:
|
|
1400
|
+
//
|
|
1401
|
+
// - **No global install yet** (a fresh `npx` fetch, or a dev checkout
|
|
1402
|
+
// run directly): install the latest release globally now.
|
|
1403
|
+
// - **A global install exists but is behind latest** (the running code's
|
|
1404
|
+
// own version, not any project's stamped `.hedgehog/version.json` —
|
|
1405
|
+
// that stamp answers a different question, whether a project's payload
|
|
1406
|
+
// is current): update it to latest now.
|
|
1407
|
+
//
|
|
1408
|
+
// Either way this reuses `latestVersion`'s day-long cache, so it costs a
|
|
1409
|
+
// network round trip at most once a day, not on every invocation. Runs
|
|
1410
|
+
// unattended — `npm install -g`, no confirmation prompt. Every caller of
|
|
1411
|
+
// this binary is either an agent driving it non-interactively or a human
|
|
1412
|
+
// who ran a command expecting it to just work, and there is no stdin
|
|
1413
|
+
// channel to prompt on in the general case. A failure here (offline,
|
|
1414
|
+
// permission-restricted global dir, npm itself missing) is swallowed the
|
|
1415
|
+
// same as a failed staleness check — the current process keeps running on
|
|
1416
|
+
// whatever code it already loaded rather than failing the command over
|
|
1417
|
+
// an advisory step.
|
|
1418
|
+
//
|
|
1419
|
+
// The install/update itself always runs regardless of `quiet` — silence
|
|
1420
|
+
// governs only whether this prints, matching `boundary --quiet`'s "exit
|
|
1421
|
+
// code only, nothing on either stream" contract for shell hooks.
|
|
1422
|
+
async function ensureGlobalInstall({ quiet = false } = {}) {
|
|
1423
|
+
if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
|
|
1424
|
+
try {
|
|
1425
|
+
const globalRoot = execFileSync('npm', ['root', '-g'], {
|
|
1426
|
+
encoding: 'utf8',
|
|
1427
|
+
timeout: 5_000,
|
|
1428
|
+
}).trim();
|
|
1429
|
+
const runningFromGlobal = PKG_ROOT === join(globalRoot, '@skyf0xx/hedgehog');
|
|
1430
|
+
const { latest, stale } = await checkBinaryStaleness(DEST_ROOT, PKG_VERSION);
|
|
1431
|
+
if (runningFromGlobal && !stale) return;
|
|
1432
|
+
if (!latest) return;
|
|
1433
|
+
|
|
1434
|
+
execFileSync('npm', ['install', '-g', `@skyf0xx/hedgehog@${latest}`], {
|
|
1435
|
+
stdio: 'ignore',
|
|
1436
|
+
timeout: 60_000,
|
|
1437
|
+
});
|
|
1438
|
+
if (!quiet) {
|
|
1439
|
+
console.error(
|
|
1440
|
+
`\n${dim(
|
|
1441
|
+
runningFromGlobal
|
|
1442
|
+
? `Updated hedgehog ${PKG_VERSION} → ${latest}. This run continues on ${PKG_VERSION}; the next command picks up ${latest}.`
|
|
1443
|
+
: `Installed hedgehog ${latest} globally. Future commands resolve to it directly.`,
|
|
1444
|
+
)}`,
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
} catch {
|
|
1448
|
+
// Advisory only — a failed check or install is never worth failing a
|
|
1449
|
+
// command over.
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1377
1453
|
async function nextCommand() {
|
|
1378
1454
|
await ensureDb();
|
|
1379
1455
|
|
|
@@ -2716,6 +2792,7 @@ async function main() {
|
|
|
2716
2792
|
console.log(PKG_VERSION);
|
|
2717
2793
|
return;
|
|
2718
2794
|
}
|
|
2795
|
+
await ensureGlobalInstall({ quiet: args.includes('--quiet') });
|
|
2719
2796
|
const cmd = args[0];
|
|
2720
2797
|
const force = args.includes('--force') || args.includes('-f');
|
|
2721
2798
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,14 @@ context with its own tool grant. The skills in `.claude/skills/` are
|
|
|
6
6
|
available the same way; invoke one by name rather than reimplementing what
|
|
7
7
|
it describes.
|
|
8
8
|
|
|
9
|
+
Claude Code reads that registration once, at session start — an agent or
|
|
10
|
+
skill file written mid-session (by `hedgehog init` or `hedgehog update`
|
|
11
|
+
just now) is not yet dispatchable by name in this session. If a name-based
|
|
12
|
+
dispatch reports it as not found, read the file directly from
|
|
13
|
+
`.claude/agents/` or `.claude/skills/` and follow it inline instead of
|
|
14
|
+
retrying the dispatch; it becomes dispatchable by name after a session
|
|
15
|
+
restart or a fresh context.
|
|
16
|
+
|
|
9
17
|
Clear context with `/clear` at the unit boundaries described above —
|
|
10
18
|
`hedgehog boundary` tells you whether you're at one (exit 0), and
|
|
11
19
|
`hedgehog boundary --handoff` is what the next session starts from.
|
package/src/hosts/version.mjs
CHANGED
|
@@ -152,3 +152,26 @@ export async function checkForUpdate(root, { force = false } = {}) {
|
|
|
152
152
|
stale: Boolean(installed && latest && isNewer(latest, installed)),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether the CLI binary actually executing right now — `binaryVersion`,
|
|
158
|
+
* read from this package's own package.json, not from any project's
|
|
159
|
+
* `.hedgehog/version.json` — is behind the newest published release.
|
|
160
|
+
*
|
|
161
|
+
* This is a distinct question from `checkForUpdate`: that one compares a
|
|
162
|
+
* *project's* stamped version against latest, so a shadowed, stale global
|
|
163
|
+
* install (a `hedgehog` binary resolved from PATH ahead of the scoped
|
|
164
|
+
* `npx @skyf0xx/hedgehog` package) reads its own old code, sees the
|
|
165
|
+
* project's stamp already at-or-past whatever it thinks "latest" is, and
|
|
166
|
+
* says nothing — the exact case this function exists to catch instead, by
|
|
167
|
+
* comparing the running code's own version rather than a file the running
|
|
168
|
+
* code could itself be stale relative to.
|
|
169
|
+
*/
|
|
170
|
+
export async function checkBinaryStaleness(root, binaryVersion, { force = false } = {}) {
|
|
171
|
+
const latest = await latestVersion(root, { force });
|
|
172
|
+
return {
|
|
173
|
+
binaryVersion,
|
|
174
|
+
latest,
|
|
175
|
+
stale: Boolean(binaryVersion && latest && isNewer(latest, binaryVersion)),
|
|
176
|
+
};
|
|
177
|
+
}
|