@skyf0xx/hedgehog 5.1.2 → 5.1.4

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 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 { recordVersion, checkForUpdate, installedVersion } from '../src/hosts/version.mjs';
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';
@@ -1363,17 +1368,76 @@ async function noteAvailableUpdate() {
1363
1368
  if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
1364
1369
  try {
1365
1370
  const { installed, latest, stale } = await checkForUpdate(DEST_ROOT);
1366
- if (!stale) return;
1367
- console.error(
1368
- `\n${yellow(bold('Hedgehog update available.'))} ${dim(
1369
- `${installed} → ${latest}. Run \`npx @skyf0xx/hedgehog@latest update\` to refresh this project's agents and skills.`,
1370
- )}`,
1371
- );
1371
+ if (stale) {
1372
+ console.error(
1373
+ `\n${yellow(bold('Hedgehog update available.'))} ${dim(
1374
+ `${installed} → ${latest}. Run \`npx @skyf0xx/hedgehog@latest update\` to refresh this project's agents and skills.`,
1375
+ )}`,
1376
+ );
1377
+ }
1372
1378
  } catch {
1373
1379
  // Advisory only — a failed check is never worth failing a command over.
1374
1380
  }
1375
1381
  }
1376
1382
 
1383
+ // Hedgehog runs as a global install: every host (Claude Code, Cursor,
1384
+ // Gemini CLI) drives it through the `hedgehog` binary on PATH, and `npx
1385
+ // @skyf0xx/hedgehog` is expected to arrive at that same global install
1386
+ // rather than an ephemeral, unmanaged copy. This makes both halves of
1387
+ // that true on every invocation, before any command's real work starts:
1388
+ //
1389
+ // - **No global install yet** (a fresh `npx` fetch, or a dev checkout
1390
+ // run directly): install the latest release globally now.
1391
+ // - **A global install exists but is behind latest** (the running code's
1392
+ // own version, not any project's stamped `.hedgehog/version.json` —
1393
+ // that stamp answers a different question, whether a project's payload
1394
+ // is current): update it to latest now.
1395
+ //
1396
+ // Either way this reuses `latestVersion`'s day-long cache, so it costs a
1397
+ // network round trip at most once a day, not on every invocation. Runs
1398
+ // unattended — `npm install -g`, no confirmation prompt. Every caller of
1399
+ // this binary is either an agent driving it non-interactively or a human
1400
+ // who ran a command expecting it to just work, and there is no stdin
1401
+ // channel to prompt on in the general case. A failure here (offline,
1402
+ // permission-restricted global dir, npm itself missing) is swallowed the
1403
+ // same as a failed staleness check — the current process keeps running on
1404
+ // whatever code it already loaded rather than failing the command over
1405
+ // an advisory step.
1406
+ //
1407
+ // The install/update itself always runs regardless of `quiet` — silence
1408
+ // governs only whether this prints, matching `boundary --quiet`'s "exit
1409
+ // code only, nothing on either stream" contract for shell hooks.
1410
+ async function ensureGlobalInstall({ quiet = false } = {}) {
1411
+ if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
1412
+ try {
1413
+ const globalRoot = execFileSync('npm', ['root', '-g'], {
1414
+ encoding: 'utf8',
1415
+ timeout: 5_000,
1416
+ }).trim();
1417
+ const runningFromGlobal = PKG_ROOT === join(globalRoot, '@skyf0xx/hedgehog');
1418
+ const { latest, stale } = await checkBinaryStaleness(DEST_ROOT, PKG_VERSION);
1419
+ if (runningFromGlobal && !stale) return;
1420
+ if (!latest) return;
1421
+
1422
+ execFileSync('npm', ['install', '-g', `@skyf0xx/hedgehog@${latest}`], {
1423
+ stdio: 'ignore',
1424
+ timeout: 60_000,
1425
+ });
1426
+ if (!quiet) {
1427
+ console.error(
1428
+ `\n${dim(
1429
+ runningFromGlobal
1430
+ ? `Updated hedgehog ${PKG_VERSION} → ${latest}. This run continues on ${PKG_VERSION}; the next command picks up ${latest}.`
1431
+ : `Installed hedgehog ${latest} globally. Future commands resolve to it directly.`,
1432
+ )}`,
1433
+ );
1434
+ }
1435
+ } catch {
1436
+ // Advisory only — a failed check or install is never worth failing a
1437
+ // command over.
1438
+ }
1439
+ }
1440
+
1377
1441
  async function nextCommand() {
1378
1442
  await ensureDb();
1379
1443
 
@@ -2716,6 +2780,7 @@ async function main() {
2716
2780
  console.log(PKG_VERSION);
2717
2781
  return;
2718
2782
  }
2783
+ await ensureGlobalInstall({ quiet: args.includes('--quiet') });
2719
2784
  const cmd = args[0];
2720
2785
  const force = args.includes('--force') || args.includes('-f');
2721
2786
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "1.0.0",
3
+ "version": "5.1.3",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -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
+ }