@noir-ai/create 1.2.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.js ADDED
@@ -0,0 +1,712 @@
1
+ // src/manifest.ts
2
+ import { join, relative } from "path";
3
+ import {
4
+ AGENTS_MD_FILENAME,
5
+ emitAgentsMd,
6
+ resolveAdapter
7
+ } from "@noir-ai/adapters";
8
+ import {
9
+ CONTEXT_BLOCK,
10
+ IGNORE_BLOCK,
11
+ managedBlock,
12
+ NOIR_DIR,
13
+ paths,
14
+ RULES_BLOCK
15
+ } from "@noir-ai/core";
16
+ var BRIEF_BLOCK = managedBlock("brief", "html");
17
+ var P = {
18
+ projectId: `${NOIR_DIR}/project.id`,
19
+ config: `${NOIR_DIR}/config.yml`,
20
+ noirMd: `${NOIR_DIR}/NOIR.md`,
21
+ rulesMd: `${NOIR_DIR}/rules/RULES.md`
22
+ };
23
+ var MANIFEST_PATH_PARITY = [
24
+ [P.projectId, paths.projectId],
25
+ [P.config, paths.config],
26
+ [P.noirMd, paths.noirMd],
27
+ [P.rulesMd, paths.rulesMd]
28
+ ];
29
+ function buildManifest(ctx) {
30
+ return [...hostAgnosticEntries(ctx), ...buildHostArtifacts(resolveAdapter(ctx.host), ctx)];
31
+ }
32
+ function hostAgnosticEntries(ctx) {
33
+ return [
34
+ {
35
+ path: P.projectId,
36
+ mode: "skipIfExists",
37
+ content: `${ctx.projectId}
38
+ `,
39
+ description: "canonical project id (store DB is named after it)"
40
+ },
41
+ {
42
+ path: P.config,
43
+ mode: "skipIfExists",
44
+ template: "config.yml.tmpl",
45
+ description: "user config seed (host + mode)"
46
+ },
47
+ {
48
+ path: P.noirMd,
49
+ mode: "managedBlock",
50
+ block: BRIEF_BLOCK,
51
+ template: "noir.md.tmpl",
52
+ description: "NOIR.md auto-brief (project id pointer)"
53
+ },
54
+ {
55
+ path: P.rulesMd,
56
+ mode: "skipIfExists",
57
+ template: "rules-seed.md.tmpl",
58
+ description: "AI working-rules seed"
59
+ },
60
+ // --- ignore files (host-agnostic; co-owned via IGNORE_BLOCK) ------------
61
+ {
62
+ path: ".gitignore",
63
+ mode: "managedBlock",
64
+ block: IGNORE_BLOCK,
65
+ template: "gitignore.tmpl",
66
+ description: ".gitignore noir managed block"
67
+ },
68
+ {
69
+ path: ".dockerignore",
70
+ mode: "managedBlock",
71
+ block: IGNORE_BLOCK,
72
+ template: "dockerignore.tmpl",
73
+ description: ".dockerignore noir managed block"
74
+ },
75
+ {
76
+ path: ".npmignore",
77
+ mode: "managedBlock",
78
+ block: IGNORE_BLOCK,
79
+ template: "npmignore.tmpl",
80
+ description: ".npmignore noir managed block"
81
+ },
82
+ {
83
+ path: ".prettierignore",
84
+ mode: "managedBlock",
85
+ block: IGNORE_BLOCK,
86
+ template: "prettierignore.tmpl",
87
+ description: ".prettierignore noir managed block"
88
+ }
89
+ ];
90
+ }
91
+ function buildHostArtifacts(adapter, ctx) {
92
+ const ectx = { root: ctx.root };
93
+ const host = adapter.id;
94
+ const entries = [];
95
+ const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
96
+ if (emitsAgentsMd) {
97
+ entries.push({
98
+ path: hostRel(adapter.agentsMdPath?.(ectx) ?? join(ctx.root, AGENTS_MD_FILENAME), ctx.root),
99
+ mode: "regenerate",
100
+ host,
101
+ content: emitAgentsMd(ectx),
102
+ description: `AGENTS.md (${host}'s native context surface; @-imports .noir/)`
103
+ });
104
+ }
105
+ switch (host) {
106
+ case "claude":
107
+ entries.push({
108
+ path: "CLAUDE.md",
109
+ mode: "managedBlock",
110
+ host,
111
+ block: CONTEXT_BLOCK,
112
+ template: "claude-context-block.md.tmpl",
113
+ description: "CLAUDE.md context @import block"
114
+ });
115
+ entries.push({
116
+ path: "CLAUDE.md",
117
+ mode: "managedBlock",
118
+ host,
119
+ block: RULES_BLOCK,
120
+ template: "claude-rules-block.md.tmpl",
121
+ description: "CLAUDE.md rules @import block"
122
+ });
123
+ break;
124
+ case "gemini":
125
+ entries.push({
126
+ path: "GEMINI.md",
127
+ mode: "managedBlock",
128
+ host,
129
+ block: CONTEXT_BLOCK,
130
+ content: "@.noir/NOIR.md",
131
+ description: "GEMINI.md context @-import block"
132
+ });
133
+ entries.push({
134
+ path: "GEMINI.md",
135
+ mode: "managedBlock",
136
+ host,
137
+ block: RULES_BLOCK,
138
+ content: "@.noir/rules/RULES.md",
139
+ description: "GEMINI.md rules @-import block"
140
+ });
141
+ break;
142
+ case "agents-md":
143
+ case "cursor":
144
+ case "opencode":
145
+ break;
146
+ }
147
+ const mcpAbs = adapter.mcpConfigPath?.(ectx) ?? join(ctx.root, ".mcp.json");
148
+ const mcpRel = hostRel(mcpAbs, ctx.root);
149
+ if (host === "claude") {
150
+ const mcpTemplate = ctx.transport === "streamable-http" ? "mcp.http.json.tmpl" : "mcp.stdio.json.tmpl";
151
+ entries.push({
152
+ path: mcpRel,
153
+ mode: "regenerate",
154
+ host,
155
+ template: mcpTemplate,
156
+ description: "host MCP server pointer"
157
+ });
158
+ } else {
159
+ const mcpContent = `${adapter.emitMcpConfig(ectx, {
160
+ transport: ctx.transport,
161
+ ...ctx.url !== void 0 ? { url: ctx.url } : {}
162
+ })}
163
+ `;
164
+ entries.push({
165
+ path: mcpRel,
166
+ mode: "regenerate",
167
+ host,
168
+ content: mcpContent,
169
+ description: `${host} MCP server pointer`
170
+ });
171
+ }
172
+ return entries;
173
+ }
174
+ function hostRel(abs, root) {
175
+ const rel = relative(root, abs);
176
+ if (rel.length === 0 || rel.startsWith("..") || rel.startsWith("/")) {
177
+ throw new Error(`buildHostArtifacts: path '${abs}' is not under root '${root}'`);
178
+ }
179
+ return rel.replace(/\\/g, "/");
180
+ }
181
+
182
+ // src/migrations/index.ts
183
+ import { existsSync, readFileSync, writeFileSync } from "fs";
184
+ import { join as join2 } from "path";
185
+
186
+ // src/migrations/runner.ts
187
+ function runMigrations(root, from, to, opts = {}) {
188
+ const window = pickWindow(from ?? "0.0.0", to, MIGRATIONS);
189
+ const ctx = { root, dryRun: opts.dryRun === true };
190
+ const aggregate = { changed: [], conflicts: [], notes: [] };
191
+ const ran = [];
192
+ for (const script of window) {
193
+ ran.push(`${script.from}\u2192${script.to}`);
194
+ let res;
195
+ try {
196
+ res = script.run(ctx);
197
+ } catch (err) {
198
+ const msg = err instanceof Error ? err.message : String(err);
199
+ aggregate.conflicts.push(`<runner>:${script.from}\u2192${script.to} threw: ${msg}`);
200
+ continue;
201
+ }
202
+ aggregate.changed.push(...res.changed);
203
+ aggregate.conflicts.push(...res.conflicts);
204
+ aggregate.notes.push(...res.notes);
205
+ }
206
+ return {
207
+ ...aggregate,
208
+ from,
209
+ to,
210
+ ran
211
+ };
212
+ }
213
+ function pickWindow(from, to, scripts) {
214
+ return scripts.filter((s) => compareVer(s.from) >= compareVer(from) && compareVer(s.to) <= compareVer(to)).sort((a, b) => compareVer(a.to) - compareVer(b.to));
215
+ }
216
+ function compareVer(v) {
217
+ const parts = v.split("-")[0]?.split(".") ?? [];
218
+ let n = 0;
219
+ for (let i = 0; i < 3; i++) {
220
+ const p = Number(parts[i] ?? "0");
221
+ n = n * 1e3 + (Number.isFinite(p) ? p : 0);
222
+ }
223
+ return n;
224
+ }
225
+
226
+ // src/migrations/index.ts
227
+ var synthetic = {
228
+ from: "1.0.0",
229
+ to: "1.0.0",
230
+ description: "no-op synthetic migration (runner smoke test)",
231
+ run: (ctx) => {
232
+ const result = { changed: [], conflicts: [], notes: [] };
233
+ if (process.env.NOIR_TEST_FORCE_CONFLICT === "1" && !ctx.dryRun) {
234
+ const file = join2(ctx.root, ".noir", "scaffold-version");
235
+ if (existsSync(file)) {
236
+ const prev = readFileSync(file, "utf8");
237
+ const merged = applyInlineConflict(prev, "noir-scaffold=1.0.0\n", "ours", "theirs");
238
+ writeFileSync(file, merged, "utf8");
239
+ result.conflicts.push(".noir/scaffold-version");
240
+ }
241
+ }
242
+ result.notes.push("synthetic 1.0.0\u21921.0.0 migration ran");
243
+ return result;
244
+ }
245
+ };
246
+ var MIGRATIONS = [synthetic];
247
+ function applyInlineConflict(ours, theirs, oursLabel = "ours", theirsLabel = "theirs") {
248
+ return `<<<<<<< ${oursLabel}
249
+ ${ours}=======
250
+ ${theirs}>>>>>>> ${theirsLabel}
251
+ `;
252
+ }
253
+ function applyWithConflict(ours, theirs, path) {
254
+ if (ours === theirs) return { text: ours, conflicted: false };
255
+ return {
256
+ text: applyInlineConflict(ours, theirs, path, path),
257
+ conflicted: true
258
+ };
259
+ }
260
+
261
+ // src/scaffold.ts
262
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs";
263
+ import { dirname as dirname4, join as join7 } from "path";
264
+ import { createProjectId, paths as paths2 } from "@noir-ai/core";
265
+
266
+ // src/scaffold-version.ts
267
+ import { mkdirSync, readFileSync as readFileSync3 } from "fs";
268
+ import { dirname as dirname2, join as join4 } from "path";
269
+ import { NOIR_DIR as NOIR_DIR2 } from "@noir-ai/core";
270
+
271
+ // src/writers.ts
272
+ import {
273
+ closeSync,
274
+ existsSync as existsSync2,
275
+ openSync,
276
+ readFileSync as readFileSync2,
277
+ renameSync,
278
+ rmSync,
279
+ writeFileSync as writeFileSync2,
280
+ writeSync
281
+ } from "fs";
282
+ import { basename, dirname, join as join3 } from "path";
283
+ import { stripManagedBlock, writeManagedRegion } from "@noir-ai/core";
284
+ function regenerate(absPath, content) {
285
+ const dir = dirname(absPath);
286
+ const tmp = join3(
287
+ dir,
288
+ `.${basename(absPath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
289
+ );
290
+ let fd;
291
+ try {
292
+ fd = openSync(tmp, "w");
293
+ writeSync(fd, content, 0, "utf8");
294
+ closeSync(fd);
295
+ fd = void 0;
296
+ renameSync(tmp, absPath);
297
+ } finally {
298
+ if (fd !== void 0) {
299
+ try {
300
+ closeSync(fd);
301
+ } catch {
302
+ }
303
+ }
304
+ try {
305
+ rmSync(tmp, { force: true });
306
+ } catch {
307
+ }
308
+ }
309
+ return { path: absPath, mode: "regenerate", written: true };
310
+ }
311
+ function managedBlock2(absPath, block, regionText) {
312
+ writeManagedRegion(absPath, block, regionText);
313
+ return { path: absPath, mode: "managedBlock", written: true };
314
+ }
315
+ function managedBlocks(absPath, regions) {
316
+ if (regions.length === 0) {
317
+ throw new Error("managedBlocks requires at least one region");
318
+ }
319
+ if (regions.length === 1) {
320
+ const only = regions[0];
321
+ if (!only) throw new Error("managedBlocks: undefined region");
322
+ return managedBlock2(absPath, only.block, only.regionText);
323
+ }
324
+ let content = "";
325
+ try {
326
+ content = readFileSync2(absPath, "utf8");
327
+ } catch {
328
+ }
329
+ let stripped = content;
330
+ for (const r of regions) {
331
+ stripped = stripManagedBlock(stripped, r.block);
332
+ }
333
+ const regionsJoined = regions.map((r) => r.regionText).join("\n");
334
+ const next = stripped.trim().length > 0 ? `${stripped.trimEnd()}
335
+
336
+ ${regionsJoined}` : regionsJoined;
337
+ writeFileSync2(absPath, next, "utf8");
338
+ return { path: absPath, mode: "managedBlock", written: true };
339
+ }
340
+ function buildRegion(block, body) {
341
+ return `${block.begin}
342
+ ${body.trimEnd()}
343
+ ${block.end}
344
+ `;
345
+ }
346
+ function skipIfExists(absPath, content) {
347
+ if (existsSync2(absPath)) {
348
+ return { path: absPath, mode: "skipIfExists", written: false };
349
+ }
350
+ writeFileSync2(absPath, content, "utf8");
351
+ return { path: absPath, mode: "skipIfExists", written: true };
352
+ }
353
+
354
+ // src/scaffold-version.ts
355
+ var CURRENT_SCAFFOLD_VERSION = "1.0.0";
356
+ var PREFIX = "noir-scaffold=";
357
+ function scaffoldVersionPath(root) {
358
+ return join4(root, NOIR_DIR2, "scaffold-version");
359
+ }
360
+ function readScaffoldVersion(root) {
361
+ let raw;
362
+ try {
363
+ raw = readFileSync3(scaffoldVersionPath(root), "utf8");
364
+ } catch {
365
+ return null;
366
+ }
367
+ for (const line of raw.split("\n")) {
368
+ const trimmed = line.trim();
369
+ if (trimmed.startsWith(PREFIX)) {
370
+ const v = trimmed.slice(PREFIX.length).trim();
371
+ if (v.length > 0) return v;
372
+ }
373
+ }
374
+ return null;
375
+ }
376
+ function writeScaffoldVersion(root, version) {
377
+ const file = scaffoldVersionPath(root);
378
+ mkdirSync(dirname2(file), { recursive: true });
379
+ regenerate(file, `${PREFIX}${version}
380
+ `);
381
+ }
382
+
383
+ // src/stack-detect.ts
384
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
385
+ import { join as join5 } from "path";
386
+ var NODE_FRAMEWORKS = [
387
+ ["next", "next"],
388
+ ["vite", "vite"],
389
+ ["express", "express"],
390
+ ["fastify", "fastify"],
391
+ ["nuxt", "nuxt"],
392
+ ["remix", "remix"],
393
+ ["@sveltejs/kit", "sveltekit"],
394
+ ["@angular/core", "angular"],
395
+ ["react", "react"],
396
+ ["vue", "vue"]
397
+ ];
398
+ var PYTHON_FRAMEWORKS = [
399
+ ["fastapi", "fastapi"],
400
+ ["flask", "flask"],
401
+ ["django", "django"],
402
+ ["sanic", "sanic"],
403
+ ["starlette", "starlette"],
404
+ ["tornado", "tornado"],
405
+ ["aiohttp", "aiohttp"],
406
+ ["bottle", "bottle"],
407
+ ["pyramid", "pyramid"],
408
+ ["falcon", "falcon"]
409
+ ];
410
+ function readJson(file) {
411
+ try {
412
+ return JSON.parse(readFileSync4(file, "utf8"));
413
+ } catch {
414
+ return void 0;
415
+ }
416
+ }
417
+ function detectStack(root) {
418
+ const languages = /* @__PURE__ */ new Set();
419
+ const frameworks = /* @__PURE__ */ new Set();
420
+ let monorepo = false;
421
+ let packageManager = null;
422
+ const pjPath = join5(root, "package.json");
423
+ if (existsSync3(pjPath)) {
424
+ const pj = readJson(pjPath);
425
+ if (pj) {
426
+ const hasTs = Boolean(pj.devDependencies?.typescript) || existsSync3(join5(root, "tsconfig.json"));
427
+ languages.add(hasTs ? "typescript" : "javascript");
428
+ const deps = /* @__PURE__ */ new Set([
429
+ ...Object.keys(pj.dependencies ?? {}),
430
+ ...Object.keys(pj.devDependencies ?? {})
431
+ ]);
432
+ for (const [dep, id] of NODE_FRAMEWORKS) {
433
+ if (deps.has(dep)) frameworks.add(id);
434
+ }
435
+ const ws = pj.workspaces;
436
+ if (Array.isArray(ws) || typeof ws === "object" && ws !== null && Array.isArray(ws.packages)) {
437
+ monorepo = true;
438
+ }
439
+ if (typeof pj.packageManager === "string" && pj.packageManager.length > 0) {
440
+ const m = pj.packageManager.split("@")[0];
441
+ if (m) packageManager = m;
442
+ }
443
+ }
444
+ }
445
+ if (existsSync3(join5(root, "pnpm-workspace.yaml"))) {
446
+ monorepo = true;
447
+ packageManager = packageManager ?? "pnpm";
448
+ }
449
+ if (existsSync3(join5(root, "turbo.json")) || existsSync3(join5(root, "nx.json"))) {
450
+ monorepo = true;
451
+ }
452
+ if (!packageManager) {
453
+ if (existsSync3(join5(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
454
+ else if (existsSync3(join5(root, "yarn.lock"))) packageManager = "yarn";
455
+ else if (existsSync3(join5(root, "package-lock.json"))) packageManager = "npm";
456
+ }
457
+ if (existsSync3(join5(root, "pyproject.toml")) || existsSync3(join5(root, "requirements.txt")) || existsSync3(join5(root, "Pipfile")) || existsSync3(join5(root, "setup.py"))) {
458
+ languages.add("python");
459
+ if (existsSync3(join5(root, "pyproject.toml"))) {
460
+ const raw = safeRead(join5(root, "pyproject.toml"));
461
+ if (raw) {
462
+ for (const [dep, id] of PYTHON_FRAMEWORKS) {
463
+ if (pyprojectHasDep(raw, dep)) frameworks.add(id);
464
+ }
465
+ }
466
+ }
467
+ }
468
+ if (existsSync3(join5(root, "go.mod"))) {
469
+ languages.add("go");
470
+ packageManager = packageManager ?? "go-modules";
471
+ }
472
+ if (existsSync3(join5(root, "Cargo.toml"))) {
473
+ languages.add("rust");
474
+ const raw = safeRead(join5(root, "Cargo.toml"));
475
+ if (raw) {
476
+ if (/^\s*actix-web\b/m.test(raw)) frameworks.add("actix-web");
477
+ if (/^\s*actix\s*=/m.test(raw)) frameworks.add("actix");
478
+ if (/^\s*axum\s*=/m.test(raw)) frameworks.add("axum");
479
+ if (/^\s*rocket\s*=/m.test(raw)) frameworks.add("rocket");
480
+ }
481
+ packageManager = packageManager ?? "cargo";
482
+ }
483
+ return {
484
+ languages: [...languages].sort(),
485
+ monorepo,
486
+ frameworks: [...frameworks].sort(),
487
+ packageManager
488
+ };
489
+ }
490
+ function safeRead(file) {
491
+ try {
492
+ return readFileSync4(file, "utf8");
493
+ } catch {
494
+ return void 0;
495
+ }
496
+ }
497
+ function pyprojectHasDep(raw, dep) {
498
+ const esc = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
499
+ const before = `(?:^|["'\\s])`;
500
+ const after = `(?=\\s|["'<>=!~;]|\\[|$)`;
501
+ return new RegExp(`${before}${esc}${after}`, "m").test(raw);
502
+ }
503
+
504
+ // src/template.ts
505
+ var TOKEN = /\{\{\s*([^}{}]+?)\s*\}\}/g;
506
+ function render(template, vars) {
507
+ return template.replace(TOKEN, (whole, name) => {
508
+ if (!Object.hasOwn(vars, name)) return whole;
509
+ const v = vars[name];
510
+ if (v === void 0 || v === null) return whole;
511
+ return String(v);
512
+ });
513
+ }
514
+
515
+ // src/template-loader.ts
516
+ import { readFileSync as readFileSync5 } from "fs";
517
+ import { dirname as dirname3, join as join6, resolve } from "path";
518
+ import { fileURLToPath } from "url";
519
+ var DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();
520
+ function resolveTemplatesDir() {
521
+ const override = process.env.NOIR_TEMPLATES_DIR;
522
+ if (override && override.length > 0) return resolve(override);
523
+ const here = dirname3(fileURLToPath(import.meta.url));
524
+ return join6(here, "..", "templates");
525
+ }
526
+ function loadTemplate(name) {
527
+ return readFileSync5(join6(DEFAULT_TEMPLATES_DIR, name), "utf8");
528
+ }
529
+ function templatesDir() {
530
+ return DEFAULT_TEMPLATES_DIR;
531
+ }
532
+
533
+ // src/scaffold.ts
534
+ var WRITER_BY_MODE = {
535
+ // 'runtime' subset = regenerate + managedBlock (the always-safe-to-rewrite
536
+ // entries). sync + init --upgrade emit only this subset; skipIfExists is
537
+ // reserved for first-run init/create so user edits survive.
538
+ regenerate: "runtime",
539
+ managedBlock: "runtime",
540
+ skipIfExists: "all"
541
+ };
542
+ async function scaffold(opts) {
543
+ const host = opts.host ?? "claude";
544
+ const transport = opts.transport ?? "stdio";
545
+ if (transport === "streamable-http" && !opts.url) {
546
+ throw new Error("transport 'streamable-http' requires opts.url");
547
+ }
548
+ if (opts.mode === "create" && !opts.dryRun) {
549
+ mkdirSync2(opts.root, { recursive: true });
550
+ }
551
+ const idFile = readProjectIdFile(opts.root);
552
+ const projectId = resolveProjectId(opts, idFile);
553
+ if (!opts.dryRun && idFile.state === "corrupt") {
554
+ rmSync2(paths2.projectId(opts.root), { force: true });
555
+ }
556
+ const fromVersion = readScaffoldVersion(opts.root);
557
+ const stack = detectStack(opts.root);
558
+ const migrationsRan = [];
559
+ const migrationConflicts = [];
560
+ if (opts.mode === "init" && opts.upgrade === true && fromVersion !== null) {
561
+ const m = runMigrations(opts.root, fromVersion, CURRENT_SCAFFOLD_VERSION, {
562
+ dryRun: opts.dryRun === true
563
+ });
564
+ migrationsRan.push(...m.ran);
565
+ migrationConflicts.push(...m.conflicts);
566
+ }
567
+ const manifest = buildManifest({ root: opts.root, projectId, host, transport, url: opts.url });
568
+ const emitRuntimeOnly = opts.mode === "sync" || opts.mode === "init" && opts.upgrade === true;
569
+ const vars = {
570
+ root: opts.root,
571
+ projectId,
572
+ host,
573
+ transport,
574
+ url: opts.url
575
+ };
576
+ const written = [];
577
+ const skipped = [];
578
+ const groups = groupApplicableByPath(manifest, host, emitRuntimeOnly);
579
+ for (const [relPath, entries] of groups) {
580
+ const abs = join7(opts.root, relPath);
581
+ const managed = entries.filter((e) => e.mode === "managedBlock");
582
+ if (managed.length >= 2) {
583
+ if (opts.dryRun) {
584
+ written.push(relPath);
585
+ continue;
586
+ }
587
+ mkdirSync2(dirname4(abs), { recursive: true });
588
+ const regions = managed.map((e) => {
589
+ const block = e.block;
590
+ if (!block) {
591
+ throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);
592
+ }
593
+ return { block, regionText: buildRegion(block, renderEntry(e, vars)) };
594
+ });
595
+ managedBlocks(abs, regions);
596
+ written.push(relPath);
597
+ continue;
598
+ }
599
+ if (!opts.dryRun) mkdirSync2(dirname4(abs), { recursive: true });
600
+ for (const entry of entries) {
601
+ if (opts.dryRun) {
602
+ if (entry.mode === "skipIfExists" && existsSync4(abs)) skipped.push(entry.path);
603
+ else written.push(entry.path);
604
+ continue;
605
+ }
606
+ const body = renderEntry(entry, vars);
607
+ if (entry.mode === "regenerate") {
608
+ regenerate(abs, body);
609
+ written.push(entry.path);
610
+ } else if (entry.mode === "managedBlock") {
611
+ const block = entry.block;
612
+ if (!block) {
613
+ throw new Error(`manifest entry ${entry.path}: managedBlock mode missing 'block'`);
614
+ }
615
+ if (isNoirMdPath(entry.path)) healLegacyNoirMd(abs);
616
+ managedBlock2(abs, block, buildRegion(block, body));
617
+ written.push(entry.path);
618
+ } else {
619
+ const out = skipIfExists(abs, body);
620
+ if (out.written) written.push(entry.path);
621
+ else skipped.push(entry.path);
622
+ }
623
+ }
624
+ }
625
+ if ((opts.mode === "init" || opts.mode === "create") && !opts.dryRun) {
626
+ writeScaffoldVersion(opts.root, CURRENT_SCAFFOLD_VERSION);
627
+ }
628
+ return {
629
+ written,
630
+ skipped,
631
+ migrationsRan,
632
+ migrationConflicts,
633
+ stack,
634
+ projectId,
635
+ fromVersion,
636
+ toVersion: CURRENT_SCAFFOLD_VERSION,
637
+ host
638
+ };
639
+ }
640
+ function readProjectIdFile(root) {
641
+ let raw;
642
+ try {
643
+ raw = readFileSync6(paths2.projectId(root), "utf8");
644
+ } catch {
645
+ return { state: "absent", id: null };
646
+ }
647
+ const trimmed = raw.trim();
648
+ return trimmed.length > 0 ? { state: "valid", id: trimmed } : { state: "corrupt", id: null };
649
+ }
650
+ function resolveProjectId(opts, idFile) {
651
+ if (opts.projectId !== void 0) return opts.projectId;
652
+ if (idFile.state === "valid") return idFile.id;
653
+ if (opts.mode === "sync") {
654
+ throw new Error(`Noir is not initialized in ${opts.root}. Run \`noir init\` first.`);
655
+ }
656
+ return createProjectId();
657
+ }
658
+ function groupApplicableByPath(manifest, host, emitRuntimeOnly) {
659
+ const groups = /* @__PURE__ */ new Map();
660
+ for (const entry of manifest) {
661
+ if (entry.host !== void 0 && entry.host !== host) continue;
662
+ if (emitRuntimeOnly && WRITER_BY_MODE[entry.mode] !== "runtime") continue;
663
+ const list = groups.get(entry.path);
664
+ if (list) list.push(entry);
665
+ else groups.set(entry.path, [entry]);
666
+ }
667
+ return groups;
668
+ }
669
+ function isNoirMdPath(relPath) {
670
+ return relPath === ".noir/NOIR.md";
671
+ }
672
+ function healLegacyNoirMd(absPath) {
673
+ let content;
674
+ try {
675
+ content = readFileSync6(absPath, "utf8");
676
+ } catch {
677
+ return;
678
+ }
679
+ if (/<!-- noir:[a-z]+ begin -->/.test(content)) return;
680
+ rmSync2(absPath, { force: true });
681
+ }
682
+ function renderEntry(entry, vars) {
683
+ if (entry.template !== void 0) {
684
+ return render(loadTemplate(entry.template), vars);
685
+ }
686
+ if (entry.content !== void 0) return entry.content;
687
+ throw new Error(`manifest entry ${entry.path}: must define 'content' or 'template'`);
688
+ }
689
+ export {
690
+ BRIEF_BLOCK,
691
+ CURRENT_SCAFFOLD_VERSION,
692
+ MANIFEST_PATH_PARITY,
693
+ MIGRATIONS,
694
+ applyInlineConflict,
695
+ applyWithConflict,
696
+ buildHostArtifacts,
697
+ buildManifest,
698
+ buildRegion,
699
+ detectStack,
700
+ loadTemplate,
701
+ managedBlock2 as managedBlock,
702
+ readScaffoldVersion,
703
+ regenerate,
704
+ render,
705
+ runMigrations,
706
+ scaffold,
707
+ scaffoldVersionPath,
708
+ skipIfExists,
709
+ templatesDir,
710
+ writeScaffoldVersion
711
+ };
712
+ //# sourceMappingURL=index.js.map