@noir-ai/create 1.18.0 → 1.19.0
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 +10 -2
- package/dist/index.js +261 -81
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/noir.md.tmpl +4 -0
- package/templates/router.md.tmpl +1 -0
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
|
|
888
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
1316
|
-
import { basename as basename2, dirname as dirname5, join as
|
|
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
|
|
1329
|
-
import { dirname as dirname4, join as
|
|
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.
|
|
1474
|
+
var CURRENT_SCAFFOLD_VERSION = "1.5.0";
|
|
1332
1475
|
var PREFIX = "noir-scaffold=";
|
|
1333
1476
|
function scaffoldVersionPath(root) {
|
|
1334
|
-
return
|
|
1477
|
+
return join7(root, NOIR_DIR2, "scaffold-version");
|
|
1335
1478
|
}
|
|
1336
1479
|
function readScaffoldVersion(root) {
|
|
1337
1480
|
let raw;
|
|
1338
1481
|
try {
|
|
1339
|
-
raw =
|
|
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
|
|
1361
|
-
import { join as
|
|
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(
|
|
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 =
|
|
1399
|
-
if (
|
|
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) ||
|
|
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 (
|
|
1564
|
+
if (existsSync6(join8(root, "pnpm-workspace.yaml"))) {
|
|
1422
1565
|
monorepo = true;
|
|
1423
1566
|
packageManager = packageManager ?? "pnpm";
|
|
1424
1567
|
}
|
|
1425
|
-
if (
|
|
1568
|
+
if (existsSync6(join8(root, "turbo.json")) || existsSync6(join8(root, "nx.json"))) {
|
|
1426
1569
|
monorepo = true;
|
|
1427
1570
|
}
|
|
1428
1571
|
if (!packageManager) {
|
|
1429
|
-
if (
|
|
1430
|
-
else if (
|
|
1431
|
-
else if (
|
|
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 (
|
|
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 (
|
|
1436
|
-
const raw = safeRead(
|
|
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 (
|
|
1587
|
+
if (existsSync6(join8(root, "go.mod"))) {
|
|
1445
1588
|
languages.add("go");
|
|
1446
1589
|
packageManager = packageManager ?? "go-modules";
|
|
1447
1590
|
}
|
|
1448
|
-
if (
|
|
1591
|
+
if (existsSync6(join8(root, "Cargo.toml"))) {
|
|
1449
1592
|
languages.add("rust");
|
|
1450
|
-
const raw = safeRead(
|
|
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 &&
|
|
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 (
|
|
1492
|
-
else if (
|
|
1493
|
-
else if (
|
|
1494
|
-
else if (
|
|
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 =
|
|
1505
|
-
if (
|
|
1647
|
+
const full = join8(root, p);
|
|
1648
|
+
if (existsSync6(full)) existingAiFiles.push({ path: p, kind });
|
|
1506
1649
|
}
|
|
1507
|
-
const cursorRulesDir =
|
|
1650
|
+
const cursorRulesDir = join8(root, ".cursor", "rules");
|
|
1508
1651
|
try {
|
|
1509
|
-
if (
|
|
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
|
|
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(
|
|
1708
|
+
return ensureOwnerOnly(join9(paths2.noirDir(opts.root), ".env"));
|
|
1566
1709
|
}
|
|
1567
1710
|
function readConfiguredHost(root) {
|
|
1568
|
-
if (!
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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;
|
|
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 =
|
|
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
|
|
2251
|
+
return readFileSync8(absPath, "utf8");
|
|
2072
2252
|
} catch (err) {
|
|
2073
2253
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
2074
2254
|
return void 0;
|