@noir-ai/create 1.18.0-beta.1 → 1.19.0-beta.1

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/index.d.ts CHANGED
@@ -391,6 +391,9 @@ type BuildManifestContext = {
391
391
  * .prettierignore only for JS; .dockerignore only when a Dockerfile is
392
392
  * present; an unknown/empty stack ⇒ all four, for backward compat). */
393
393
  stack?: StackInfo;
394
+ /** Compiled project baseline (stack + canonical commands + entry points),
395
+ * rendered into the `.noir/NOIR.md` brief block as `{{baseline}}`. */
396
+ baseline?: string;
394
397
  /** The `rules.enabled` switch from the project's `.noir/config.yml`. False
395
398
  * drops the working-rules seed from the manifest, so no command emits or
396
399
  * backfills `.noir/rules/RULES.md` for a project that opted out. Undefined
@@ -706,6 +709,9 @@ interface ScaffoldOptions {
706
709
  /** Preview: compute the same written/skipped/migrated lists without touching
707
710
  * disk. `noir doctor`/CI use this to report drift. */
708
711
  dryRun?: boolean;
712
+ /** Drift check: a dry-run that reports only what would CHANGE (byte comparison,
713
+ * not the full planned-emit list). Used by `noir sync --check`. */
714
+ check?: boolean;
709
715
  /** Policy for a `regenerate` file that exists and DIFFERS from the
710
716
  * template, when no {@link onConflict} callback is provided. Default
711
717
  * `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's
@@ -884,8 +890,10 @@ declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
884
890
  * whenever a manifest entry, template, or migration changes shape. 1.3.0 adds
885
891
  * the migration that points a joined repo's MCP entry back at the workspace
886
892
  * bridge; 1.4.0 adds the migration that moves the SessionStart hook command off
887
- * the `.mjs` runner and onto `noir hook` (see migrations/index.ts). */
888
- declare const CURRENT_SCAFFOLD_VERSION = "1.4.0";
893
+ * the `.mjs` runner and onto `noir hook`; 1.5.0 adds the migration that appends
894
+ * the enforcement events to an existing claude hook and backfills the gemini
895
+ * hook (see migrations/index.ts). */
896
+ declare const CURRENT_SCAFFOLD_VERSION = "1.5.0";
889
897
  /** Path to the stamp file under `root`. Exposed for tests + doctor. */
890
898
  declare function scaffoldVersionPath(root: string): string;
891
899
  /** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
package/dist/index.js CHANGED
@@ -315,40 +315,8 @@ function buildHostArtifacts(adapter, ctx) {
315
315
  description: workspace !== null ? `${host} MCP server pointer (workspace "${workspace}")` : `${host} MCP server pointer`
316
316
  });
317
317
  }
318
- if (host === "claude") {
319
- const HOOK_DEDUP = " hook";
320
- const hookEntry = {
321
- hooks: {
322
- SessionStart: [
323
- {
324
- hooks: [
325
- {
326
- type: "command",
327
- command: `"${ctx.command}" hook`
328
- }
329
- ]
330
- }
331
- ],
332
- SubagentStart: [
333
- {
334
- hooks: [
335
- {
336
- type: "command",
337
- command: `"${ctx.command}" hook`
338
- }
339
- ]
340
- }
341
- ]
342
- }
343
- };
344
- entries.push({
345
- path: ".claude/settings.local.json",
346
- mode: "mergeJson",
347
- host,
348
- content: JSON.stringify(hookEntry, null, 2),
349
- dedupSubstring: HOOK_DEDUP,
350
- description: "SessionStart + SubagentStart hook entry (user-owned, written once)"
351
- });
318
+ const hookEmissions = adapter.emitHooks?.({ command: ctx.command }) ?? null;
319
+ if (hookEmissions !== null) {
352
320
  entries.push({
353
321
  path: ".noir/router.md",
354
322
  mode: "managedBlock",
@@ -366,6 +334,18 @@ function buildHostArtifacts(adapter, ctx) {
366
334
  description: "always-on anti-slop + laziness ruleset (hook-injected, not @imported)"
367
335
  });
368
336
  }
337
+ for (const emission of hookEmissions) {
338
+ const json = JSON.stringify(emission.content, null, 2);
339
+ entries.push({
340
+ path: emission.path,
341
+ mode: emission.mode,
342
+ host,
343
+ content: emission.mode === "regenerate" ? `${json}
344
+ ` : json,
345
+ ...emission.dedupSubstring !== void 0 ? { dedupSubstring: emission.dedupSubstring } : {},
346
+ description: emission.description
347
+ });
348
+ }
369
349
  }
370
350
  return entries;
371
351
  }
@@ -531,6 +511,7 @@ import { mcpConfigPathFor, noirStdioArgs, TRANSPORT_KEYS } from "@noir-ai/adapte
531
511
  import {
532
512
  atomicWriteFile as atomicWriteFile2,
533
513
  loadProjectInfo,
514
+ readProjectConfig,
534
515
  readWorkspaceMarker as readWorkspaceMarker2,
535
516
  resolveNoirCommand
536
517
  } from "@noir-ai/core";
@@ -1287,12 +1268,98 @@ var hookCommand = {
1287
1268
  return result;
1288
1269
  }
1289
1270
  };
1271
+ function applyHookEvents(ctx, rel, abs, events, createIfMissing) {
1272
+ let settings;
1273
+ if (!existsSync4(abs)) {
1274
+ if (!createIfMissing) {
1275
+ return { note: `${rel}: absent \u2014 nothing to migrate`, changed: false, conflict: false };
1276
+ }
1277
+ settings = {};
1278
+ } else {
1279
+ try {
1280
+ settings = JSON.parse(readFileSync4(abs, "utf8"));
1281
+ } catch (err) {
1282
+ return {
1283
+ note: `${rel}: unreadable (${errorMessage(err)}) \u2014 left untouched`,
1284
+ changed: false,
1285
+ conflict: true
1286
+ };
1287
+ }
1288
+ }
1289
+ const existing = settings.hooks;
1290
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
1291
+ if (!createIfMissing) {
1292
+ return {
1293
+ note: `${rel}: no hooks object \u2014 nothing to migrate`,
1294
+ changed: false,
1295
+ conflict: false
1296
+ };
1297
+ }
1298
+ settings.hooks = {};
1299
+ }
1300
+ const hooksObj = settings.hooks;
1301
+ let added = false;
1302
+ for (const [event, patch] of Object.entries(events)) {
1303
+ if (hooksObj[event] === void 0) {
1304
+ hooksObj[event] = patch;
1305
+ added = true;
1306
+ }
1307
+ }
1308
+ if (!added) {
1309
+ return { note: `${rel}: enforcement events already present`, changed: false, conflict: false };
1310
+ }
1311
+ if (ctx.dryRun) {
1312
+ return { note: `${rel}: would append enforcement events`, changed: true, conflict: false };
1313
+ }
1314
+ try {
1315
+ atomicWriteFile2(abs, `${JSON.stringify(settings, null, 2)}
1316
+ `);
1317
+ return { note: `${rel}: appended enforcement events`, changed: true, conflict: false };
1318
+ } catch (err) {
1319
+ return {
1320
+ note: `${rel}: write failed (${errorMessage(err)}) \u2014 left untouched`,
1321
+ changed: false,
1322
+ conflict: true
1323
+ };
1324
+ }
1325
+ }
1326
+ var hookEvents = {
1327
+ from: "1.4.0",
1328
+ to: "1.5.0",
1329
+ description: "append the enforcement hook events to the claude or gemini hook",
1330
+ run: (ctx) => {
1331
+ const result = { changed: [], conflicts: [], notes: [] };
1332
+ const host = readProjectConfig(ctx.root)?.host ?? "claude";
1333
+ if (host !== "claude" && host !== "gemini") {
1334
+ result.notes.push(
1335
+ `${host}: no hook migration needed (cursor is regenerate-mode; opencode/agents-md have no hook surface)`
1336
+ );
1337
+ return result;
1338
+ }
1339
+ const rel = host === "gemini" ? ".gemini/settings.json" : ".claude/settings.local.json";
1340
+ const abs = join5(ctx.root, ...rel.split("/"));
1341
+ const command = host === "gemini" ? `"${resolveNoirCommand()}" hook --host gemini` : `"${resolveNoirCommand()}" hook`;
1342
+ const events = host === "gemini" ? {
1343
+ SessionStart: [{ hooks: [{ type: "command", command }] }],
1344
+ BeforeTool: [{ matcher: ".*", hooks: [{ type: "command", command }] }]
1345
+ } : {
1346
+ PreToolUse: [{ hooks: [{ type: "command", command }] }],
1347
+ Stop: [{ hooks: [{ type: "command", command }] }]
1348
+ };
1349
+ const out = applyHookEvents(ctx, rel, abs, events, host === "gemini");
1350
+ if (out.conflict) result.conflicts.push(rel);
1351
+ else if (out.changed) result.changed.push(rel);
1352
+ result.notes.push(out.note);
1353
+ return result;
1354
+ }
1355
+ };
1290
1356
  var MIGRATIONS = [
1291
1357
  synthetic,
1292
1358
  envPointer,
1293
1359
  envTemplates,
1294
1360
  workspaceBridge,
1295
- hookCommand
1361
+ hookCommand,
1362
+ hookEvents
1296
1363
  ];
1297
1364
  function errorMessage(err) {
1298
1365
  return err instanceof Error ? err.message : String(err);
@@ -1312,31 +1379,107 @@ function applyWithConflict(ours, theirs, path) {
1312
1379
  }
1313
1380
 
1314
1381
  // src/scaffold.ts
1315
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync3 } from "fs";
1316
- import { basename as basename2, dirname as dirname5, join as join8 } from "path";
1382
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync3 } from "fs";
1383
+ import { basename as basename2, dirname as dirname5, join as join9 } from "path";
1317
1384
  import {
1318
1385
  createProjectId,
1319
1386
  loadProjectInfo as loadProjectInfo2,
1320
1387
  paths as paths2,
1321
1388
  readManagedBlock,
1322
- readProjectConfig,
1389
+ readProjectConfig as readProjectConfig2,
1323
1390
  resolveNoirCommand as resolveNoirCommand2,
1324
1391
  sha256Hex12
1325
1392
  } from "@noir-ai/core";
1326
1393
 
1394
+ // src/baseline.ts
1395
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1396
+ import { join as join6 } from "path";
1397
+ var COMMAND_KEYS = [
1398
+ ["build", "build"],
1399
+ ["test", "test"],
1400
+ ["lint", "lint"],
1401
+ ["typecheck", "typecheck"],
1402
+ ["format", "format"],
1403
+ ["start", "start"],
1404
+ ["dev", "dev"]
1405
+ ];
1406
+ var ENTRY_POINTS = [
1407
+ "src/index.ts",
1408
+ "src/index.tsx",
1409
+ "src/index.js",
1410
+ "src/index.jsx",
1411
+ "src/main.ts",
1412
+ "src/main.js",
1413
+ "src/main.py",
1414
+ "main.ts",
1415
+ "main.js",
1416
+ "main.py",
1417
+ "app/main.py",
1418
+ "src/app.py",
1419
+ "cmd/main.go",
1420
+ "src/lib.rs",
1421
+ "src/main.rs"
1422
+ ];
1423
+ function detectCommands(root) {
1424
+ const out = {};
1425
+ const pjPath = join6(root, "package.json");
1426
+ if (existsSync5(pjPath)) {
1427
+ let pj;
1428
+ try {
1429
+ pj = JSON.parse(readFileSync5(pjPath, "utf8"));
1430
+ } catch {
1431
+ pj = void 0;
1432
+ }
1433
+ for (const [name, key] of COMMAND_KEYS) {
1434
+ const value = pj?.scripts?.[key];
1435
+ if (typeof value === "string" && value !== "") out[name] = value;
1436
+ }
1437
+ }
1438
+ const makefile = join6(root, "Makefile");
1439
+ if (existsSync5(makefile)) {
1440
+ try {
1441
+ const raw = readFileSync5(makefile, "utf8");
1442
+ for (const name of ["build", "test", "lint"]) {
1443
+ if (!(name in out) && new RegExp(`^${name}:`, "m").test(raw)) out[name] = `make ${name}`;
1444
+ }
1445
+ } catch {
1446
+ }
1447
+ }
1448
+ return out;
1449
+ }
1450
+ function detectEntryPoints(root) {
1451
+ return ENTRY_POINTS.filter((p) => existsSync5(join6(root, p)));
1452
+ }
1453
+ function renderBaseline(stack, commands, entryPoints) {
1454
+ const lang = stack.languages.length > 0 ? stack.languages.join(", ") : "unknown";
1455
+ const framework = stack.frameworks.length > 0 ? ` (${stack.frameworks.join(", ")})` : "";
1456
+ const lines = [
1457
+ `- **Stack:** ${lang}${framework}${stack.monorepo ? " (monorepo)" : ""}`,
1458
+ `- **Package manager:** ${stack.packageManager ?? "none"}`
1459
+ ];
1460
+ if (stack.ci) lines.push(`- **CI:** ${stack.ci}`);
1461
+ const entries = Object.entries(commands);
1462
+ if (entries.length > 0) {
1463
+ lines.push("- **Commands:**");
1464
+ for (const [name, command] of entries) lines.push(` - \`${name}\`: \`${command}\``);
1465
+ }
1466
+ if (entryPoints.length > 0) lines.push(`- **Entry points:** ${entryPoints.join(", ")}`);
1467
+ return lines.join("\n");
1468
+ }
1469
+
1327
1470
  // src/scaffold-version.ts
1328
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "fs";
1329
- import { dirname as dirname4, join as join6 } from "path";
1471
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6 } from "fs";
1472
+ import { dirname as dirname4, join as join7 } from "path";
1330
1473
  import { NOIR_DIR as NOIR_DIR2 } from "@noir-ai/core";
1331
- var CURRENT_SCAFFOLD_VERSION = "1.4.0";
1474
+ var CURRENT_SCAFFOLD_VERSION = "1.5.0";
1332
1475
  var PREFIX = "noir-scaffold=";
1333
1476
  function scaffoldVersionPath(root) {
1334
- return join6(root, NOIR_DIR2, "scaffold-version");
1477
+ return join7(root, NOIR_DIR2, "scaffold-version");
1335
1478
  }
1336
1479
  function readScaffoldVersion(root) {
1337
1480
  let raw;
1338
1481
  try {
1339
- raw = readFileSync5(scaffoldVersionPath(root), "utf8");
1482
+ raw = readFileSync6(scaffoldVersionPath(root), "utf8");
1340
1483
  } catch {
1341
1484
  return null;
1342
1485
  }
@@ -1357,8 +1500,8 @@ function writeScaffoldVersion(root, version) {
1357
1500
  }
1358
1501
 
1359
1502
  // src/stack-detect.ts
1360
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1361
- import { join as join7 } from "path";
1503
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
1504
+ import { join as join8 } from "path";
1362
1505
  var NODE_FRAMEWORKS = [
1363
1506
  ["next", "next"],
1364
1507
  ["vite", "vite"],
@@ -1385,7 +1528,7 @@ var PYTHON_FRAMEWORKS = [
1385
1528
  ];
1386
1529
  function readJson(file) {
1387
1530
  try {
1388
- return JSON.parse(readFileSync6(file, "utf8"));
1531
+ return JSON.parse(readFileSync7(file, "utf8"));
1389
1532
  } catch {
1390
1533
  return void 0;
1391
1534
  }
@@ -1395,11 +1538,11 @@ function detectStack(root) {
1395
1538
  const frameworks = /* @__PURE__ */ new Set();
1396
1539
  let monorepo = false;
1397
1540
  let packageManager = null;
1398
- const pjPath = join7(root, "package.json");
1399
- if (existsSync5(pjPath)) {
1541
+ const pjPath = join8(root, "package.json");
1542
+ if (existsSync6(pjPath)) {
1400
1543
  const pj = readJson(pjPath);
1401
1544
  if (pj) {
1402
- const hasTs = Boolean(pj.devDependencies?.typescript) || existsSync5(join7(root, "tsconfig.json"));
1545
+ const hasTs = Boolean(pj.devDependencies?.typescript) || existsSync6(join8(root, "tsconfig.json"));
1403
1546
  languages.add(hasTs ? "typescript" : "javascript");
1404
1547
  const deps = /* @__PURE__ */ new Set([
1405
1548
  ...Object.keys(pj.dependencies ?? {}),
@@ -1418,22 +1561,22 @@ function detectStack(root) {
1418
1561
  }
1419
1562
  }
1420
1563
  }
1421
- if (existsSync5(join7(root, "pnpm-workspace.yaml"))) {
1564
+ if (existsSync6(join8(root, "pnpm-workspace.yaml"))) {
1422
1565
  monorepo = true;
1423
1566
  packageManager = packageManager ?? "pnpm";
1424
1567
  }
1425
- if (existsSync5(join7(root, "turbo.json")) || existsSync5(join7(root, "nx.json"))) {
1568
+ if (existsSync6(join8(root, "turbo.json")) || existsSync6(join8(root, "nx.json"))) {
1426
1569
  monorepo = true;
1427
1570
  }
1428
1571
  if (!packageManager) {
1429
- if (existsSync5(join7(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
1430
- else if (existsSync5(join7(root, "yarn.lock"))) packageManager = "yarn";
1431
- else if (existsSync5(join7(root, "package-lock.json"))) packageManager = "npm";
1572
+ if (existsSync6(join8(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
1573
+ else if (existsSync6(join8(root, "yarn.lock"))) packageManager = "yarn";
1574
+ else if (existsSync6(join8(root, "package-lock.json"))) packageManager = "npm";
1432
1575
  }
1433
- if (existsSync5(join7(root, "pyproject.toml")) || existsSync5(join7(root, "requirements.txt")) || existsSync5(join7(root, "Pipfile")) || existsSync5(join7(root, "setup.py"))) {
1576
+ if (existsSync6(join8(root, "pyproject.toml")) || existsSync6(join8(root, "requirements.txt")) || existsSync6(join8(root, "Pipfile")) || existsSync6(join8(root, "setup.py"))) {
1434
1577
  languages.add("python");
1435
- if (existsSync5(join7(root, "pyproject.toml"))) {
1436
- const raw = safeRead(join7(root, "pyproject.toml"));
1578
+ if (existsSync6(join8(root, "pyproject.toml"))) {
1579
+ const raw = safeRead(join8(root, "pyproject.toml"));
1437
1580
  if (raw) {
1438
1581
  for (const [dep, id] of PYTHON_FRAMEWORKS) {
1439
1582
  if (pyprojectHasDep(raw, dep)) frameworks.add(id);
@@ -1441,13 +1584,13 @@ function detectStack(root) {
1441
1584
  }
1442
1585
  }
1443
1586
  }
1444
- if (existsSync5(join7(root, "go.mod"))) {
1587
+ if (existsSync6(join8(root, "go.mod"))) {
1445
1588
  languages.add("go");
1446
1589
  packageManager = packageManager ?? "go-modules";
1447
1590
  }
1448
- if (existsSync5(join7(root, "Cargo.toml"))) {
1591
+ if (existsSync6(join8(root, "Cargo.toml"))) {
1449
1592
  languages.add("rust");
1450
- const raw = safeRead(join7(root, "Cargo.toml"));
1593
+ const raw = safeRead(join8(root, "Cargo.toml"));
1451
1594
  if (raw) {
1452
1595
  if (/^\s*actix-web\b/m.test(raw)) frameworks.add("actix-web");
1453
1596
  if (/^\s*actix\s*=/m.test(raw)) frameworks.add("actix");
@@ -1459,7 +1602,7 @@ function detectStack(root) {
1459
1602
  let pmSource = "unknown";
1460
1603
  let pmConflict = false;
1461
1604
  if (packageManager) {
1462
- if (pjPath && existsSync5(pjPath)) {
1605
+ if (pjPath && existsSync6(pjPath)) {
1463
1606
  const pj = readJson(pjPath);
1464
1607
  if (pj?.packageManager?.split("@")[0] === packageManager) {
1465
1608
  pmSource = "packageManager-field";
@@ -1488,10 +1631,10 @@ function detectStack(root) {
1488
1631
  }
1489
1632
  }
1490
1633
  let ci = null;
1491
- if (existsSync5(join7(root, ".github", "workflows"))) ci = "github";
1492
- else if (existsSync5(join7(root, ".gitlab-ci.yml"))) ci = "gitlab";
1493
- else if (existsSync5(join7(root, ".circleci", "config.yml"))) ci = "circleci";
1494
- else if (existsSync5(join7(root, "Jenkinsfile"))) ci = "jenkins";
1634
+ if (existsSync6(join8(root, ".github", "workflows"))) ci = "github";
1635
+ else if (existsSync6(join8(root, ".gitlab-ci.yml"))) ci = "gitlab";
1636
+ else if (existsSync6(join8(root, ".circleci", "config.yml"))) ci = "circleci";
1637
+ else if (existsSync6(join8(root, "Jenkinsfile"))) ci = "jenkins";
1495
1638
  const aiFiles = [
1496
1639
  ["AGENTS.md", "agents-md"],
1497
1640
  ["CLAUDE.md", "claude"],
@@ -1501,12 +1644,12 @@ function detectStack(root) {
1501
1644
  ];
1502
1645
  const existingAiFiles = [];
1503
1646
  for (const [p, kind] of aiFiles) {
1504
- const full = join7(root, p);
1505
- if (existsSync5(full)) existingAiFiles.push({ path: p, kind });
1647
+ const full = join8(root, p);
1648
+ if (existsSync6(full)) existingAiFiles.push({ path: p, kind });
1506
1649
  }
1507
- const cursorRulesDir = join7(root, ".cursor", "rules");
1650
+ const cursorRulesDir = join8(root, ".cursor", "rules");
1508
1651
  try {
1509
- if (existsSync5(cursorRulesDir)) {
1652
+ if (existsSync6(cursorRulesDir)) {
1510
1653
  const entries = readdirSync2(cursorRulesDir);
1511
1654
  for (const e of entries) {
1512
1655
  if (e.endsWith(".md"))
@@ -1528,7 +1671,7 @@ function detectStack(root) {
1528
1671
  }
1529
1672
  function safeRead(file) {
1530
1673
  try {
1531
- return readFileSync6(file, "utf8");
1674
+ return readFileSync7(file, "utf8");
1532
1675
  } catch {
1533
1676
  return void 0;
1534
1677
  }
@@ -1562,10 +1705,10 @@ function assertSafeRoot(root) {
1562
1705
  }
1563
1706
  function envOwnerOnly(opts) {
1564
1707
  if (opts.dryRun === true) return void 0;
1565
- return ensureOwnerOnly(join8(paths2.noirDir(opts.root), ".env"));
1708
+ return ensureOwnerOnly(join9(paths2.noirDir(opts.root), ".env"));
1566
1709
  }
1567
1710
  function readConfiguredHost(root) {
1568
- if (!existsSync6(paths2.config(root))) return "claude";
1711
+ if (!existsSync7(paths2.config(root))) return "claude";
1569
1712
  try {
1570
1713
  return loadProjectInfo2(root).config.host;
1571
1714
  } catch {
@@ -1573,7 +1716,7 @@ function readConfiguredHost(root) {
1573
1716
  }
1574
1717
  }
1575
1718
  function readConfiguredRulesEnabled(root) {
1576
- return readProjectConfig(root)?.rules.enabled ?? true;
1719
+ return readProjectConfig2(root)?.rules.enabled ?? true;
1577
1720
  }
1578
1721
  async function scaffold(opts) {
1579
1722
  assertSafeRoot(opts.root);
@@ -1592,6 +1735,7 @@ async function scaffold(opts) {
1592
1735
  }
1593
1736
  const fromVersion = readScaffoldVersion(opts.root);
1594
1737
  const stack = detectStack(opts.root);
1738
+ const baseline = renderBaseline(stack, detectCommands(opts.root), detectEntryPoints(opts.root));
1595
1739
  const ancestors = readAncestors(opts.root);
1596
1740
  const mergeManagedRegions = opts.mergeManagedRegions !== false;
1597
1741
  const hasIdentity = fromVersion !== null || idFile.state === "valid";
@@ -1660,7 +1804,8 @@ async function scaffold(opts) {
1660
1804
  host,
1661
1805
  transport,
1662
1806
  url: opts.url,
1663
- command
1807
+ command,
1808
+ baseline
1664
1809
  };
1665
1810
  const written = [];
1666
1811
  const skipped = [];
@@ -1681,10 +1826,26 @@ async function scaffold(opts) {
1681
1826
  };
1682
1827
  const groups = groupApplicableByPath(manifest, host, emitContext);
1683
1828
  for (const [relPath, entries] of groups) {
1684
- const abs = join8(opts.root, relPath);
1829
+ const abs = join9(opts.root, relPath);
1685
1830
  const managed = entries.filter((e) => e.mode === "managedBlock");
1686
1831
  if (managed.length >= 2) {
1687
1832
  if (opts.dryRun) {
1833
+ if (opts.check === true) {
1834
+ const onDisk2 = readOptional(abs);
1835
+ const regions2 = managed.map((e) => {
1836
+ const block = e.block;
1837
+ if (!block) {
1838
+ throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);
1839
+ }
1840
+ const theirs = buildRegion(block, renderEntry(e, vars));
1841
+ const merged = mergeManagedRegions ? mergeManagedRegion(abs, e.path, block, theirs, ancestors) : { text: theirs, conflict: false, cleanTheirs: theirs };
1842
+ return { block, regionText: merged.text };
1843
+ });
1844
+ const predicted2 = predictManagedBlocks(onDisk2 ?? "", regions2);
1845
+ if (onDisk2 !== void 0 && predicted2 === onDisk2) identical.push(relPath);
1846
+ else written.push(relPath);
1847
+ continue;
1848
+ }
1688
1849
  written.push(relPath);
1689
1850
  continue;
1690
1851
  }
@@ -1732,13 +1893,32 @@ async function scaffold(opts) {
1732
1893
  if (!opts.dryRun) mkdirSync3(dirname5(abs), { recursive: true });
1733
1894
  for (const entry of entries) {
1734
1895
  if (opts.dryRun) {
1896
+ if (opts.check === true && (entry.mode === "regenerate" || entry.mode === "managedBlock")) {
1897
+ const body2 = renderEntry(entry, vars);
1898
+ const onDisk = readOptional(abs);
1899
+ if (entry.mode === "regenerate") {
1900
+ if (onDisk === void 0 || onDisk !== body2) written.push(entry.path);
1901
+ else identical.push(entry.path);
1902
+ } else {
1903
+ const block = entry.block;
1904
+ if (!block) {
1905
+ throw new Error(`manifest entry ${entry.path}: managedBlock mode missing 'block'`);
1906
+ }
1907
+ const theirs = buildRegion(block, body2);
1908
+ const merged = mergeManagedRegions ? mergeManagedRegion(abs, entry.path, block, theirs, ancestors) : { text: theirs, conflict: false, cleanTheirs: theirs };
1909
+ const predicted = predictManagedBlock(onDisk ?? "", block, merged.text);
1910
+ if (onDisk !== void 0 && predicted === onDisk) identical.push(entry.path);
1911
+ else written.push(entry.path);
1912
+ }
1913
+ continue;
1914
+ }
1735
1915
  if (entry.mode !== "skipIfExists") {
1736
1916
  written.push(entry.path);
1737
1917
  continue;
1738
1918
  }
1739
1919
  const seedKind = refreshableSeedKind(entry, emitContext);
1740
1920
  if (seedKind === void 0) {
1741
- if (existsSync6(abs)) {
1921
+ if (existsSync7(abs)) {
1742
1922
  skipped.push(entry.path);
1743
1923
  } else {
1744
1924
  written.push(entry.path);
@@ -1931,7 +2111,7 @@ async function resolveManagedConflict(relPath, mode, opts) {
1931
2111
  function readProjectIdFile(root) {
1932
2112
  let raw;
1933
2113
  try {
1934
- raw = readFileSync7(paths2.projectId(root), "utf8");
2114
+ raw = readFileSync8(paths2.projectId(root), "utf8");
1935
2115
  } catch {
1936
2116
  return { state: "absent", id: null };
1937
2117
  }
@@ -1971,7 +2151,7 @@ function refreshableSeedKind(entry, emitContext) {
1971
2151
  async function writeWithConflict(abs, relPath, proposed, opts, internals) {
1972
2152
  let existing;
1973
2153
  try {
1974
- existing = readFileSync7(abs, "utf8");
2154
+ existing = readFileSync8(abs, "utf8");
1975
2155
  } catch {
1976
2156
  existing = void 0;
1977
2157
  }
@@ -2032,7 +2212,7 @@ function uniqueAside(abs, relPath, suffix) {
2032
2212
  rel: `${relPath}${s}`
2033
2213
  });
2034
2214
  let candidate = make(suffix);
2035
- for (let n = 1; existsSync6(candidate.abs); n++) candidate = make(`${suffix}.${n}`);
2215
+ for (let n = 1; existsSync7(candidate.abs); n++) candidate = make(`${suffix}.${n}`);
2036
2216
  return candidate;
2037
2217
  }
2038
2218
  function groupApplicableByPath(manifest, host, emitContext) {
@@ -2052,7 +2232,7 @@ function isNoirMdPath(relPath) {
2052
2232
  function healLegacyNoirMd(absPath) {
2053
2233
  let content;
2054
2234
  try {
2055
- content = readFileSync7(absPath, "utf8");
2235
+ content = readFileSync8(absPath, "utf8");
2056
2236
  } catch {
2057
2237
  return;
2058
2238
  }
@@ -2068,7 +2248,7 @@ function renderEntry(entry, vars) {
2068
2248
  }
2069
2249
  function readOptional(absPath) {
2070
2250
  try {
2071
- return readFileSync7(absPath, "utf8");
2251
+ return readFileSync8(absPath, "utf8");
2072
2252
  } catch (err) {
2073
2253
  if (err instanceof Error && err.code === "ENOENT") {
2074
2254
  return void 0;