@skyf0xx/hedgehog 6.0.2 → 6.0.3

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/README.md CHANGED
@@ -202,9 +202,7 @@ Where possible, Hedgehog uses a battle-tested blueprint in [`hedgehog-core-autho
202
202
 
203
203
  ### Existing codebases
204
204
 
205
- Hedgehog also adopts onto existing repos.
206
-
207
- It scans the repo's shape and is able to create new changes with the same scoped, verified, committed loop.
205
+ Hedgehog also adopts onto a repo it didn't build. It reads the repo and proposes a layer chain that verifies with the repo's own test/lint/build commands.
208
206
 
209
207
  ## Why Hedgehog Works
210
208
 
@@ -223,7 +221,7 @@ Mention `Hedgehog` whenever you want to build something with it.
223
221
  2. Check Python >=3.10 is installed — it runs [CodeGraphContext](https://github.com/CodeGraphContext/CodeGraphContext), the code index every project uses.
224
222
  3. Check CodeGraphContext is installed and configured.
225
223
  4. If anything is missing, offer to set it up with the `hedgehog-code-intelligence-setup` skill — `init` stops without it.
226
- 5. Run the install commands above.
224
+ 5. Run the install commands below.
227
225
 
228
226
  </details>
229
227
 
package/bin/cli.mjs CHANGED
@@ -80,6 +80,7 @@ import { rebuildDb } from '../src/db/rebuild.mjs';
80
80
  import { loadOverrides, addOverride, orphanedOverrides, OVERRIDES_DIR } from '../src/db/overrides.mjs';
81
81
  import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
82
82
  import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
83
+ import { wrapSection } from '../src/hosts/claude-md-merge.mjs';
83
84
  import {
84
85
  recordVersion,
85
86
  checkForUpdate,
@@ -88,7 +89,7 @@ import {
88
89
  } from '../src/hosts/version.mjs';
89
90
  import { loadRegistry, resolveCore } from '../src/registry/index.mjs';
90
91
  import { fetchCore, cachedCore, cachedVersions, cachedEngine } from '../src/registry/fetch.mjs';
91
- import { recordCore, installedCore } from '../src/registry/installed.mjs';
92
+ import { recordCore, installedCore, ADOPTED_CORE_NAME } from '../src/registry/installed.mjs';
92
93
 
93
94
  const AUTHORED_CORE_PATH = '.hedgehog/core.yaml';
94
95
  const CODE_INTELLIGENCE_CONFIG_PATH = '.hedgehog/code-intelligence.json';
@@ -472,7 +473,13 @@ async function writePlannedFile(f) {
472
473
  join(f.merge.includeRoot ?? PKG_ROOT, f.merge.include),
473
474
  'utf8',
474
475
  );
475
- out = out.replaceAll('{{CORE_SECTION}}', section.trimEnd());
476
+ // Wrapped in the same markers appendCoreSection uses on a
477
+ // brownfield CLAUDE.md with no {{CORE_SECTION}} placeholder to
478
+ // substitute into — see src/hosts/claude-md-merge.mjs. Keeping
479
+ // both paths' output mutually recognizable is what lets
480
+ // hasCoreSection/appendCoreSection treat a template-filled file
481
+ // and an appended one the same way on a later re-run.
482
+ out = out.replaceAll('{{CORE_SECTION}}', wrapSection(section));
476
483
  }
477
484
  const dispatch = await readFile(join(PKG_ROOT, f.merge.dispatch), 'utf8');
478
485
  await writeFile(f.dest, out.replaceAll('{{HOST_DISPATCH}}', dispatch.trimEnd()));
@@ -544,6 +551,9 @@ ${bold('Usage')}
544
551
  npx @skyf0xx/hedgehog init --pwa-app scaffold the pwa-app core now
545
552
  npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core now
546
553
  npx @skyf0xx/hedgehog cores list every core this release can install
554
+ npx @skyf0xx/hedgehog core record-adopted land the authored core's agents/skills and record
555
+ this project as adopted (hedgehog-adopt calls this
556
+ after writing .hedgehog/core.yaml; not for other cores)
547
557
  npx @skyf0xx/hedgehog init --cursor install for Cursor (default: Claude Code)
548
558
  npx @skyf0xx/hedgehog init --host=claude,gemini install for several coding agents at once
549
559
  npx @skyf0xx/hedgehog init --all-hosts install for every supported coding agent
@@ -1173,6 +1183,84 @@ async function resolveInstalledCore() {
1173
1183
  }
1174
1184
  }
1175
1185
 
1186
+ // `hedgehog core record-adopted` — the record path for `hedgehog-adopt`
1187
+ // (shipped in @skyf0xx/hedgehog-core-authored), which brings the
1188
+ // discipline to an existing repo by writing `.hedgehog/core.yaml` and
1189
+ // `.hedgehog/adoption.md` directly. That path has no `init` step and so
1190
+ // never fetches the `authored` package or calls `recordCore` — a no-flag
1191
+ // `init` (what the offer skill actually runs before adoption) installs
1192
+ // only the shared engine payload, never a core's own agents/skills, and
1193
+ // `bootstrap` (the only other place a core package gets fetched) is
1194
+ // explicitly skipped for adoption. Left alone, `hedgehog-authored-loop`
1195
+ // and `layer-eng` — the skill and agent adoption hands off to — would
1196
+ // never land on disk, and even if they did by some other means, `update`
1197
+ // would have nothing telling it they're there: `resolveInstalledCore`
1198
+ // falls through to `detectUnrecordedCore`, which only looks for a root
1199
+ // `core.yaml` (the shipped-core workspace marker) and finds nothing for
1200
+ // an adopted repo's `.hedgehog/core.yaml`, so it would read as "no core
1201
+ // yet" and update would silently rewrite `.claude/agents`/`.claude/skills`
1202
+ // down to just the shared payload, deleting the authored package's files
1203
+ // with nothing put back.
1204
+ //
1205
+ // This command is both fixes at once: it fetches the `authored` package
1206
+ // and lands its agents/skills for every host this project already has
1207
+ // installed (never its workspace, template, or vendor_skills — adoption
1208
+ // already wrote its own CLAUDE.md section and root workspace is the one
1209
+ // thing adoption must never touch), then records the core with
1210
+ // `adopted: true` so `update` refreshes it correctly from here on.
1211
+ // Idempotent and safe to re-run — e.g. from a later `hedgehog-adopt` pass
1212
+ // adding new change-work — since it always overwrites from the current
1213
+ // package rather than merging.
1214
+ async function recordAdoptedCommand() {
1215
+ const entry = await resolveCore(ADOPTED_CORE_NAME);
1216
+ if (!entry) {
1217
+ console.error(
1218
+ `${red('authored core not in this release.')} This Hedgehog release's registry has no\n` +
1219
+ `entry named "authored" — nothing to install. Update Hedgehog and retry.\n`,
1220
+ );
1221
+ process.exitCode = 1;
1222
+ return;
1223
+ }
1224
+
1225
+ let core;
1226
+ try {
1227
+ core = await fetchCore(entry);
1228
+ } catch (err) {
1229
+ console.error(`\n${red(bold('Core unavailable.'))} ${err.message}\n`);
1230
+ process.exitCode = 1;
1231
+ return;
1232
+ }
1233
+
1234
+ const targets = await installedHosts(DEST_ROOT);
1235
+ let written = 0;
1236
+ for (const host of targets) {
1237
+ const h = HOSTS[host];
1238
+ for (const payloadEntry of corePayload(core, h, { hostOnly: true })) {
1239
+ for (const f of await plannedFiles(payloadEntry)) {
1240
+ await writePlannedFile(f);
1241
+ written++;
1242
+ console.log(` ${green('install')} ${relative(DEST_ROOT, f.dest)}`);
1243
+ }
1244
+ }
1245
+ }
1246
+
1247
+ await recordCore(DEST_ROOT, { name: core.manifest.name, version: core.version, adopted: true });
1248
+
1249
+ console.log(
1250
+ `\n${green(bold('Adopted core recorded.'))} ${dim(
1251
+ `${written} file(s) written for ${targets.map((h) => HOSTS[h].label).join(', ')}`,
1252
+ )}\n`,
1253
+ );
1254
+ console.log(
1255
+ dim(
1256
+ 'hedgehog-authored-loop and layer-eng are now on disk and this project is\n' +
1257
+ 'recorded as adopted — `hedgehog update` will keep them current alongside\n' +
1258
+ "the shared engine payload, and will never treat this project's core as a\n" +
1259
+ 'name it gets to change.\n',
1260
+ ),
1261
+ );
1262
+ }
1263
+
1176
1264
  async function dbRebuildCommand() {
1177
1265
  const corePath = await resolveCorePath();
1178
1266
  if (!corePath) {
@@ -3569,6 +3657,21 @@ function engineNote(engine) {
3569
3657
  return `${engine} ${dim(`— CLI is ${PKG_VERSION}`)}`;
3570
3658
  }
3571
3659
 
3660
+ // `hedgehog core record-adopted` — see recordAdoptedCommand for why this
3661
+ // exists. `cores` (plural) lists the registry; `core` (singular) acts on
3662
+ // this project's own core, so the two don't collide despite the name.
3663
+ async function coreCommand(args) {
3664
+ const sub = args[0];
3665
+ if (sub !== 'record-adopted') {
3666
+ console.error(
3667
+ `${red('Unknown core subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog core record-adopted\n`,
3668
+ );
3669
+ process.exitCode = 1;
3670
+ return;
3671
+ }
3672
+ await recordAdoptedCommand();
3673
+ }
3674
+
3572
3675
  // Wraps prose to `width`, indenting every line after the first so it sits
3573
3676
  // under the column its label opened.
3574
3677
  function wrapProse(text, width, indent) {
@@ -3675,6 +3778,11 @@ async function main() {
3675
3778
  return;
3676
3779
  }
3677
3780
 
3781
+ if (cmd === 'core') {
3782
+ await coreCommand(args.slice(1));
3783
+ return;
3784
+ }
3785
+
3678
3786
  if (cmd === 'update') {
3679
3787
  if (args.includes('--check')) {
3680
3788
  await updateCheck();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "6.0.2",
3
+ "version": "6.0.3",
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": {
package/src/db/plan.mjs CHANGED
@@ -5,10 +5,11 @@
5
5
  //
6
6
  // full-stack-app and pwa-app: one task per layer per intent (an intent is
7
7
  // a domain module — see the core definition's `{module}` placeholder).
8
- // landing-page: one task per phase, no module axis. All three are the
9
- // same operation walk a core definition's layer chain once per intent
10
- // because a linear chain is the degenerate case of the layer graph
11
- // (spec: MVP scope item 5).
8
+ // landing-page, deepseek-harness, and authored (whether designed from
9
+ // scratch or adopted onto an existing repo): one task per phase/layer, no
10
+ // module axis. All are the same operation walk a core definition's layer
11
+ // chain once per intent — because a linear chain is the degenerate case of
12
+ // the layer graph (spec: MVP scope item 5).
12
13
  //
13
14
  // Layer cardinality: a layer marked `once: true` in the core definition
14
15
  // opts out of that per-intent multiplication and compiles a single task
@@ -0,0 +1,69 @@
1
+ // Merges a core's CLAUDE.md section into a project's root instructions
2
+ // file, for the one path where that file is not a Hedgehog shell: a
3
+ // repo adopted onto existing, hand-written content that has no
4
+ // {{CORE_SECTION}} placeholder at all.
5
+ //
6
+ // `writePlannedFile` in bin/cli.mjs (the `merge` entry kind) fills
7
+ // {{CORE_SECTION}} by substring replacement, which requires the shell's
8
+ // placeholder to already be present — true for every project `init`
9
+ // ever wrote, since it always starts from src/templates/CLAUDE.md. An
10
+ // adopted repo's CLAUDE.md predates Hedgehog entirely and carries no
11
+ // such marker, so that replacement has nothing to replace. This module
12
+ // is the one place that gap is closed: appending a delimited section
13
+ // instead of substituting into one.
14
+ //
15
+ // Referenced by name (not substance) from hedgehog-adopt's own SKILL.md
16
+ // in the @skyf0xx/hedgehog-core-authored package — that skill invokes
17
+ // `appendCoreSection` the same way it already invokes `loadCore` from
18
+ // src/db/core.mjs, via `node -e "import('<path-to-hedgehog-install>/
19
+ // src/hosts/claude-md-merge.mjs')..."`.
20
+
21
+ const MARKER_START = '<!-- hedgehog:core-section start -->';
22
+ const MARKER_END = '<!-- hedgehog:core-section end -->';
23
+
24
+ // True when `content` already carries a Hedgehog-managed section — from
25
+ // either mechanism: the shell's own {{CORE_SECTION}} placeholder (still
26
+ // unfilled, or already filled by writePlannedFile's substring
27
+ // replacement — filled content stays wrapped in the same markers, so
28
+ // this check still finds it) or this module's own appended, delimited
29
+ // block.
30
+ export function hasCoreSection(content) {
31
+ return content.includes('{{CORE_SECTION}}') || content.includes(MARKER_START);
32
+ }
33
+
34
+ // Wraps `section` (a core's CLAUDE.core.*.md content, verbatim) in the
35
+ // stable markers that make both the substring-replace path
36
+ // (writePlannedFile, bin/cli.mjs) and this append path idempotent and
37
+ // mutually recognizable. Exported so writePlannedFile's {{CORE_SECTION}}
38
+ // substitution wraps its own output the same way, rather than each path
39
+ // hand-rolling the marker text.
40
+ export function wrapSection(section) {
41
+ return `${MARKER_START}\n\n${section.trimEnd()}\n\n${MARKER_END}`;
42
+ }
43
+
44
+ // Appends `section` to `existingContent` as a clearly delimited block,
45
+ // never touching a byte of what's already there. Idempotent: if a
46
+ // marked section is already present, its content is replaced in place
47
+ // (so a later `hedgehog-adopt` re-run, or a core switch, updates the
48
+ // section without duplicating it) rather than appending a second copy.
49
+ //
50
+ // This is the merge path for a CLAUDE.md hedgehog-adopt's Step 5 finds
51
+ // with no {{CORE_SECTION}} placeholder — the file predates Hedgehog and
52
+ // was never written from src/templates/CLAUDE.md. Every file that shell
53
+ // ever produced already has the placeholder, so that case always goes
54
+ // through writePlannedFile's ordinary substitution instead; this
55
+ // function is never called on that path.
56
+ export function appendCoreSection(existingContent, section) {
57
+ const block = wrapSection(section);
58
+ const markerRe = new RegExp(
59
+ `${escapeRe(MARKER_START)}[\\s\\S]*?${escapeRe(MARKER_END)}`,
60
+ );
61
+ if (markerRe.test(existingContent)) {
62
+ return existingContent.replace(markerRe, block);
63
+ }
64
+ return `${existingContent.trimEnd()}\n\n${block}\n`;
65
+ }
66
+
67
+ function escapeRe(s) {
68
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "6.0.2",
3
+ "version": "6.0.3",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -4,7 +4,7 @@
4
4
  "name": "full-stack-app",
5
5
  "flag": "--ts-full-stack-app",
6
6
  "package": "@skyf0xx/hedgehog-core-full-stack-app",
7
- "version": "^1.0.0",
7
+ "version": "^1.0.6",
8
8
  "language": "typescript",
9
9
  "repository": "https://github.com/skyf0xx/hedgehog-core-full-stack-app",
10
10
  "selects_when": "The description names server-side logic across most of the app: authorization more expressive than per-object row-level security, background jobs, scheduled work or webhook receivers as the app's primary function, server-rendered or SEO-critical pages beyond an app shell, or a data model whose working set doesn't sensibly fit on a device. Persistent domain data alone is not the signal — a tracker, journal, notebook, or planner whose data fits on the user's device is pwa-app, even with sharing, accounts, or multi-device sync in scope, and even with one or two entities that must be server-authoritative. If the project has both a marketing page and a real app behind it, this is still full-stack-app: the page becomes routes inside apps/web, not a separate project."
@@ -13,7 +13,7 @@
13
13
  "name": "pwa-app",
14
14
  "flag": "--pwa-app",
15
15
  "package": "@skyf0xx/hedgehog-core-pwa-app",
16
- "version": "^1.0.0",
16
+ "version": "^1.0.4",
17
17
  "language": "typescript",
18
18
  "repository": "https://github.com/skyf0xx/hedgehog-core-pwa-app",
19
19
  "selects_when": "The description names an app whose data model fits on the user's device and whose reads and writes are the user's own — a tracker, journal, notebook, planner, offline reference, or utility. Offline capability or installability named explicitly is a strong signal. Sharing, collaboration, accounts, and multi-device sync do NOT disqualify a project: Dexie Cloud provides sync, authentication, and server-enforced per-object access control, so a shared list, a family calendar, or a small team's board is still this core. A small number of entities that must be server-authoritative — a points balance, a reward ledger, anything a client must not write to directly — do NOT disqualify a project either: those entities are declared --remote and backed by Supabase (Postgres + RLS + Edge Functions) behind the same repository interface, while the rest of the app stays local-first. What routes a project to full-stack-app is server-side logic across most of the app, not the presence of one or two such entities: authorization beyond row-level policies, background jobs or webhooks as the app's primary function, server-rendered pages, or a working set too large for a device."
@@ -22,7 +22,7 @@
22
22
  "name": "landing-page",
23
23
  "flag": "--landing-page",
24
24
  "package": "@skyf0xx/hedgehog-core-landing-page",
25
- "version": "^1.0.0",
25
+ "version": "^1.0.2",
26
26
  "language": "typescript",
27
27
  "repository": "https://github.com/skyf0xx/hedgehog-core-landing-page",
28
28
  "selects_when": "The description is a marketing/announcement/waitlist/portfolio page (or a small handful of such pages) with no persistent domain data of its own. A page that only collects an email into a third-party form service, or has no state at all, qualifies. The bar is \"no domain module\" — a landing page with a dozen sections is still landing-page, not promoted to full-stack-app for being long."
@@ -31,7 +31,7 @@
31
31
  "name": "deepseek-harness",
32
32
  "flag": "--deepseek-harness",
33
33
  "package": "@skyf0xx/hedgehog-core-deepseek-harness",
34
- "version": ">=0.1.0 <1.0.0",
34
+ "version": ">=0.2.3 <1.0.0",
35
35
  "language": "typescript",
36
36
  "repository": "https://github.com/skyf0xx/hedgehog-core-deepseek-harness",
37
37
  "selects_when": "The description names building a plugin, tool, hook, or extension for DeepSeek Harness (DSH) — a Cordis-based agent framework — or otherwise extending an existing DSH installation with new capabilities via its plugin/bundle system. Concrete signals: DSH, DeepSeek Harness, Cordis, `defineTool`, `ctx.tools.register`, a `cordis.patch.yml` manifest, or a request to add a tool that a DSH agent can call. This is the core most often confused with authored: authored fits when no shipped core matches and the planner must design a system shape from scratch, but a DSH plugin already has a fixed, battle-tested shape (the six-layer scaffold → logic → wiring → smoke → bundle → join sequence, one plugin per intent) that authored's from-scratch design would only reinvent worse. Route here whenever the target of the work is a DSH plugin, not a general \"no other core fits\" fallback."
@@ -39,7 +39,7 @@
39
39
  {
40
40
  "name": "authored",
41
41
  "package": "@skyf0xx/hedgehog-core-authored",
42
- "version": "^1.0.0",
42
+ "version": "^1.0.2",
43
43
  "language": "typescript",
44
44
  "repository": "https://github.com/skyf0xx/hedgehog-core-authored",
45
45
  "selects_when": "Neither shipped core fits, but the description names a real artifact a Builder step would produce — just not in either shipped core's shape. This core is designed by the planner rather than chosen from a fixed set: hedgehog-planning-intake's Phase 0 elicits the drivers first, then hedgehog-core-design names the system shape, picks the stack, derives the layers, and writes .hedgehog/core.yaml. It carries the same enforcement as a shipped core — ordered layers, scoped file access, verification before completion — but the sequence is designed for this project rather than battle-tested across many."
@@ -1,14 +1,16 @@
1
1
  // Core package registry. One entry per Hedgehog core — full-stack-app,
2
- // pwa-app, landing-page, authored — naming the npm package that ships its
3
- // agents, skills, and (for the three shipped cores) scaffold, plus the CLI
4
- // flag `hedgehog init` accepts for it and the prose `planner` reads aloud
5
- // in Phase 0 to choose one. A fixed table, one entry per core, discovered
6
- // by name or flag rather than convention.
2
+ // pwa-app, landing-page, deepseek-harness, and authored — naming the npm
3
+ // package that ships its agents, skills, and (for the first four) scaffold,
4
+ // plus the CLI flag `hedgehog init` accepts for it and the prose `planner`
5
+ // reads aloud in Phase 0 to choose one. A fixed table, one entry per core,
6
+ // discovered by name or flag rather than convention.
7
7
  //
8
8
  // `authored` carries no flag — it is never selected off a fixed list at
9
- // install time. hedgehog-core-design chooses it during planning, from the
10
- // drivers hedgehog-planning-intake's Phase 0 elicits, not from a value a
11
- // user passes to `init`.
9
+ // install time. hedgehog-core-design chooses it during planning (from-scratch
10
+ // design path), or hedgehog-adopt brings it to an existing repo (brownfield
11
+ // adoption path), from the drivers hedgehog-planning-intake's Phase 0 elicits
12
+ // or the repo characteristics adoption discovers, not from a value a user
13
+ // passes to `init`.
12
14
 
13
15
  import { readFile } from 'node:fs/promises';
14
16
  import { dirname, join } from 'node:path';
@@ -6,34 +6,62 @@
6
6
  // install time has nothing recorded until its bootstrap-core skill lands
7
7
  // the core — `installedCore` returns null until then, and `update` limits
8
8
  // itself to the shared payload.
9
+ //
10
+ // `authored` has a second entry point with no `init` step at all:
11
+ // `hedgehog-adopt` brings the discipline to a repo that already exists by
12
+ // writing `.hedgehog/core.yaml` itself, directly, and never calls
13
+ // `recordCore` (see that skill, shipped in
14
+ // @skyf0xx/hedgehog-core-authored — adoption has no npm package of its
15
+ // own to fetch, just a core.yaml and adoption.md derived from the
16
+ // existing repo). `hedgehog core record-adopted` (bin/cli.mjs) is the
17
+ // record path for that case instead: it lands the authored package's
18
+ // agents/skills — otherwise never installed, since adoption's `init` runs
19
+ // with no `--core` flag and `bootstrap` is skipped entirely for adoption
20
+ // — and calls `recordCore` with `adopted: true`. That flag is the one
21
+ // thing distinguishing an adopted record from a normal one: `update`'s
22
+ // resolveInstalledCore refreshes an adopted project's payload the same as
23
+ // any other (same package, same agents/skills, kept current) but must
24
+ // never treat `record.name` changing upstream, or the record going
25
+ // missing, as license to fetch and install a *different* core the way it
26
+ // would for a project that chose one at `init` — adoption's core is a
27
+ // fixed fact of the repo (its `.hedgehog/core.yaml`), not a choice
28
+ // `update` gets to revisit.
9
29
 
10
30
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
11
31
  import { dirname, join } from 'node:path';
12
32
 
33
+ export const ADOPTED_CORE_NAME = 'authored';
34
+
13
35
  const CORE_PATH = '.hedgehog/core.json';
14
36
 
15
37
  /**
16
38
  * Record the core a project installed and the exact version resolved for
17
39
  * it. Written after the core's files land, so the record describes what
18
- * is on disk.
40
+ * is on disk. `adopted: true` marks a record written by `hedgehog core
41
+ * record-adopted` rather than by `init` — see the module comment above.
19
42
  */
20
- export async function recordCore(root, { name, version }) {
43
+ export async function recordCore(root, { name, version, adopted = false }) {
21
44
  const path = join(root, CORE_PATH);
22
45
  await mkdir(dirname(path), { recursive: true });
23
46
  await writeFile(
24
47
  path,
25
- `${JSON.stringify({ core: name, version, installedAt: new Date().toISOString() }, null, 2)}\n`,
48
+ `${JSON.stringify(
49
+ { core: name, version, installedAt: new Date().toISOString(), ...(adopted ? { adopted: true } : {}) },
50
+ null,
51
+ 2,
52
+ )}\n`,
26
53
  );
27
54
  }
28
55
 
29
56
  /**
30
- * The core `update` should refresh — `{ name, version }` — or null when
31
- * this project has no core installed yet.
57
+ * The core `update` should refresh — `{ name, version, adopted }` — or
58
+ * null when this project has no core installed yet. `adopted` is always a
59
+ * boolean, `false` for every record `init` writes.
32
60
  */
33
61
  export async function installedCore(root) {
34
62
  try {
35
- const { core, version } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
36
- return core ? { name: core, version } : null;
63
+ const { core, version, adopted } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
64
+ return core ? { name: core, version, adopted: adopted === true } : null;
37
65
  } catch {
38
66
  return null;
39
67
  }