@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agaaaptr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,448 @@
1
+ import { HostId, HostAdapter } from '@noir-ai/adapters';
2
+ import { ManagedBlock } from '@noir-ai/core';
3
+
4
+ /**
5
+ * The three-mode writer — generalizes keystone-K's `writeManagedRegion` into
6
+ * the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
7
+ * an artifact class in the spec §4.5 matrix:
8
+ *
9
+ * - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).
10
+ * Always overwritten, atomically.
11
+ * - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,
12
+ * `.gitignore` noir block, …). DELEGATES to
13
+ * keystone-K's `writeManagedRegion` so user content
14
+ * outside the markers is preserved byte-for-byte and
15
+ * re-runs are idempotent. Never duplicate the
16
+ * managed-region logic.
17
+ * - {@link skipIfExists} — user-owned seeds (`RULES.md`, `config.yml`,
18
+ * `project.id`). Write once; never clobber.
19
+ *
20
+ * The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode
21
+ * functions are exported so the cli (S-T2) and tests can drive them directly
22
+ * when a one-off write is needed outside the manifest.
23
+ */
24
+ type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists';
25
+ interface WriteOutcome {
26
+ /** The absolute path that was written. */
27
+ path: string;
28
+ mode: WriteMode;
29
+ /** true when bytes hit disk; false for skipIfExists no-ops. regenerate and
30
+ * managedBlock always write (managedBlock may write identical bytes — that
31
+ * still counts as a write for telemetry purposes; the file IS up to date). */
32
+ written: boolean;
33
+ }
34
+ /** Atomic overwrite. Writes to `<file>.tmp.<pid>.<rnd>` in the same directory,
35
+ * fsyncs, then renames over the target so a crash never leaves a half-written
36
+ * file (the pointer files this is used for are read by the host agent first —
37
+ * a truncated CLAUDE.md/.mcp.json would break the very startup Noir serves).
38
+ *
39
+ * Parent directories are NOT created here — the orchestrator does that once
40
+ * for the whole manifest so a missing dir is a single, attributable failure
41
+ * rather than N silent ones inside the writer. */
42
+ declare function regenerate(absPath: string, content: string): WriteOutcome;
43
+ /** Re-emit a managed region, delegating to keystone-K's `writeManagedRegion`.
44
+ * `regionText` MUST already include the begin/end markers (matches the shape
45
+ * `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`
46
+ * callers build in core/cli). Use {@link buildRegion} to assemble it from a
47
+ * block + body. */
48
+ declare function managedBlock(absPath: string, block: ManagedBlock, regionText: string): WriteOutcome;
49
+ /** Assemble `<begin>\n<body>\n<end>\n` for a managed block. Centralized here so
50
+ * every caller (manifest rendering, tests, future migrations) produces the
51
+ * exact byte shape `writeManagedRegion` strips/expects. The trailing newline
52
+ * is part of the contract — `stripManagedBlock`'s regex eats a trailing `\n`
53
+ * so re-runs stay idempotent instead of accumulating blank lines.
54
+ *
55
+ * `body` is `trimEnd()`-ed before wrapping so template authors can keep the
56
+ * conventional trailing newline in `.tmpl` files without producing a
57
+ * double-newline before the end marker. This keeps the output byte-identical
58
+ * to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which
59
+ * S-T2 relies on for a diff-free refactor. */
60
+ declare function buildRegion(block: ManagedBlock, body: string): string;
61
+ /** Write `content` to `absPath` only if no file exists there. Returns whether
62
+ * bytes were written. Parent dirs are NOT created (orchestrator's job). */
63
+ declare function skipIfExists(absPath: string, content: string): WriteOutcome;
64
+
65
+ /**
66
+ * Declarative scaffold manifest — the single source of truth for what
67
+ * `init` / `create` / `sync` emit. Each entry is one artifact, tagged with its
68
+ * write {@link WriteMode} so the orchestrator can dispatch without knowing
69
+ * what's inside.
70
+ *
71
+ * FAITHFULNESS CONTRACT (S-T1 → S-T2 → S10): this table is a strict superset of
72
+ * the artifacts `packages/cli/src/{init,sync}.ts` wrote pre-Slice-S. The cli
73
+ * refactor (S-T2) replaced those ad-hoc writers with a call into `scaffold()`;
74
+ * the byte-for-byte output MUST stay equivalent for first-run init. S10 makes
75
+ * the manifest HOST-PARAMETRIC: {@link buildManifest} now returns host-agnostic
76
+ * entries + a {@link buildHostArtifacts} call that materializes per-host files
77
+ * (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
78
+ * for agents-md/cursor/opencode) via the resolved adapter. The claude default
79
+ * `noir init` stays BYTE-IDENTICAL to v1.1 — fix-wave I1 REMOVED the additive
80
+ * root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via
81
+ * CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).
82
+ *
83
+ * Path-derivation: repo-relative POSIX strings that mirror
84
+ * `@noir-ai/core/layout.ts` (`paths.*`). The test suite asserts
85
+ * `join(root, entry.path) === paths.X(root)` for every entry layout knows
86
+ * about, so a layout rename is caught here instead of silently drifting.
87
+ */
88
+ /** S10: `HostTag` is now the SAME `HostId` enum the adapter registry uses
89
+ * (re-exported so existing imports keep working). Pre-S10 this was the
90
+ * literal `'claude'`; widening to `HostId` lets one manifest serve every host
91
+ * via the orchestrator's host filter + {@link buildHostArtifacts}. */
92
+ type HostTag = HostId;
93
+ interface ManifestEntry {
94
+ /** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */
95
+ path: string;
96
+ mode: WriteMode;
97
+ /** Host tag; entry is skipped when opts.host !== entry.host.
98
+ * Undefined = host-agnostic (every host emits it). */
99
+ host?: HostTag;
100
+ /** Required for `managedBlock` mode: the named block to re-emit. */
101
+ block?: ManagedBlock;
102
+ /** Literal content (`regenerate`/`skipIfExists`) or literal region BODY
103
+ * (`managedBlock` — the orchestrator wraps it with the block markers).
104
+ * Mutually exclusive with {@link template}. */
105
+ content?: string;
106
+ /** Template name (resolved by `template-loader`) for content/body.
107
+ * Mutually exclusive with {@link content}. */
108
+ template?: string;
109
+ /** One-line human description for `noir doctor` + logs. */
110
+ description?: string;
111
+ }
112
+ type BuildManifestContext = {
113
+ /** Absolute repo root. Added in S10 so {@link buildHostArtifacts} can resolve
114
+ * absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the
115
+ * manifest's repo-relative POSIX shape. */
116
+ root: string;
117
+ /** Canonical project id (already created/read by the orchestrator). */
118
+ projectId: string;
119
+ /** Target host. Drives {@link buildHostArtifacts} via `resolveAdapter(host)`. */
120
+ host: HostTag;
121
+ /** MCP transport the host should use to reach Noir. */
122
+ transport: 'stdio' | 'streamable-http';
123
+ /** Required when transport is `streamable-http`. */
124
+ url?: string;
125
+ };
126
+ /** Co-owned NOIR.md auto-brief region. Defined locally (not exported from
127
+ * core) because core's keystone-K named instances cover only the three
128
+ * regions core itself writes (context/rules/ignore); the brief is the
129
+ * scaffold engine's own. Uses the SAME `managedBlock()` factory so marker
130
+ * shape stays consistent with the rest of the family. */
131
+ declare const BRIEF_BLOCK: ManagedBlock;
132
+ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
133
+ entryPath: string,
134
+ layoutFn: (root: string) => string
135
+ ]>;
136
+ /**
137
+ * Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls
138
+ * this once per scaffold run; tests assert the shape is stable.
139
+ *
140
+ * S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
141
+ * where the host-specific half comes from {@link buildHostArtifacts} (driven by
142
+ * `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
143
+ * (canonical `.noir/` store + ignore files). Fix-wave I1: {@link buildHostArtifacts}
144
+ * emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
145
+ * own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).
146
+ *
147
+ * Mode-tagging rationale per artifact (see S-T1 report for the full table):
148
+ * - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST
149
+ * NOT overwrite — that would orphan the indexed store DB named after it.
150
+ * - `config.yml` → skipIfExists. User-owned; the seed is written once.
151
+ * (The seed renders `host: {{host}}` so a `--host gemini` init persists the
152
+ * chosen host for `noir sync` to read back.)
153
+ * - `NOIR.md` → managedBlock (BRIEF_BLOCK). Auto-brief is co-owned.
154
+ * - `RULES.md` → skipIfExists. User-owned working-contract seed.
155
+ * - ignore files → managedBlock (IGNORE_BLOCK). Matches syncIgnores.
156
+ * - host entries → SEE {@link buildHostArtifacts} (regenerate / managedBlock).
157
+ */
158
+ declare function buildManifest(ctx: BuildManifestContext): ManifestEntry[];
159
+ /** Context shape passed to {@link buildHostArtifacts}. A strict subset of
160
+ * {@link BuildManifestContext} (no `projectId`/`host` — the adapter IS the
161
+ * resolved host, and host artifacts never need the project id). Exported
162
+ * separately so callers + tests can name the narrower contract. */
163
+ interface BuildHostArtifactsContext {
164
+ root: string;
165
+ transport: 'stdio' | 'streamable-http';
166
+ url?: string;
167
+ }
168
+ /**
169
+ * Materialize the host-specific manifest entries from a resolved adapter.
170
+ * SINGLE entry point — no scattered `if (host === '…')` conditionals in the
171
+ * orchestrator. Returns entries in emission order:
172
+ *
173
+ * 1. **AGENTS.md** (universal baseline) — `regenerate` at
174
+ * `adapter.agentsMdPath(ctx)` (default `<root>/AGENTS.md`), content from
175
+ * the shared `emitAgentsMd(ctx)` helper. Emitted ONLY for hosts whose
176
+ * `emitContext` IS the AGENTS.md content (agents-md, cursor, opencode) —
177
+ * for them AGENTS.md is the SINGLE native context surface AND carries the
178
+ * Noir working rules via its `@.noir/rules/RULES.md` import. claude and
179
+ * gemini have their OWN native context file (CLAUDE.md / GEMINI.md) that
180
+ * `@`-imports the canonical `.noir/` sources; emitting AGENTS.md too
181
+ * would IMPORT THOSE FILES TWICE into the host's context (2× tokens +
182
+ * drift risk), so for those two hosts AGENTS.md is SKIPPED. (Claude Code
183
+ * still discovers AGENTS.md at the repo root when present — users who
184
+ * want the universal file can drop one in by hand; Noir's auto-emission
185
+ * stays single-source per host.)
186
+ * 2. **Host-native context file** — emitted ONLY for hosts whose `emitContext`
187
+ * is NOT the AGENTS.md content (i.e. the host has its OWN context file
188
+ * with a distinct syntax). Concretely: claude → `CLAUDE.md` (CONTEXT +
189
+ * RULES managed blocks, byte-identical to v1.1 via templates); gemini →
190
+ * `GEMINI.md` (CONTEXT + RULES managed blocks with Gemini's bare `@`
191
+ * import syntax). For `agents-md`/`cursor`/`opencode` the context IS the
192
+ * AGENTS.md (already emitted in step 1) → SKIP to avoid a duplicate.
193
+ * Rules live INSIDE the host's context file: claude's in CLAUDE.md,
194
+ * gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md — NO
195
+ * host emits a separate rules file. (The prior cursor
196
+ * `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it
197
+ * collided with the C3 cursor flat-skill prune of `noir-*.mdc` under
198
+ * `.cursor/rules/`, and cursor's rules are already delivered via
199
+ * AGENTS.md's `@.noir/rules/RULES.md` import.)
200
+ * 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`
201
+ * (default `<root>/.mcp.json` for claude), content from
202
+ * `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template
203
+ * path (byte-identical parity with v1.1 + the .mcp.json parity test that
204
+ * compares against `claudeAdapter.emitMcpConfig`); other hosts use the
205
+ * adapter directly.
206
+ *
207
+ * Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with
208
+ * `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`
209
+ * as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT per C3; gemini/
210
+ * agents-md/opencode have no skill dir → skip).
211
+ */
212
+ declare function buildHostArtifacts(adapter: HostAdapter, ctx: BuildHostArtifactsContext): ManifestEntry[];
213
+
214
+ /**
215
+ * Migration model — Nx-style declarative upgrade scripts, hand-rolled.
216
+ *
217
+ * Per the Slice S design (S-OQ2 resolved → inline conflict markers, never
218
+ * interactive prompts), every migration script:
219
+ * - is a small, IDEMPOTENT TypeScript function (`run(root, opts) → result`).
220
+ * Re-running it on an already-migrated tree is a no-op (or produces the
221
+ * same conflict markers again — also safe).
222
+ * - NEVER prompts. `--no-input` CI must survive. When a migration cannot
223
+ * reconcile a co-edited region, it writes git-style conflict markers
224
+ * (`<<<<<<<` / `=======` / `>>>>>>>`) into the file and records the path in
225
+ * `result.conflicts` so the caller (doctor/CI) can fail loudly with a
226
+ * reviewable artifact rather than hang on a prompt.
227
+ *
228
+ * The registry ({@link MIGRATIONS}) is the deliverable at v1.0.0 — there are no
229
+ * real version-to-version migrations yet. The synthetic `1.0.0 → 1.0.0`
230
+ * migration exists to exercise the runner end-to-end and to give a template
231
+ * for the first real migration.
232
+ */
233
+ interface MigrationContext {
234
+ /** Absolute repo root the migration operates on. */
235
+ root: string;
236
+ /** When true, the runner performs all reads and the would-be writes, returns
237
+ * the same `changed`/`conflicts` shape, but does NOT touch disk. Used by
238
+ * `noir doctor`/CI to preview. */
239
+ dryRun?: boolean;
240
+ }
241
+ interface MigrationResult {
242
+ /** Repo-relative paths the migration modified (or would modify, in dryRun). */
243
+ changed: string[];
244
+ /** Repo-relative paths left with inline conflict markers. Always non-throwing. */
245
+ conflicts: string[];
246
+ /** Free-form notes for doctor/CI logs (e.g. "rewrote CLAUDE.md import"). */
247
+ notes: string[];
248
+ }
249
+ /** A single version-to-version step. `from`/`to` are inclusive endpoints on a
250
+ * linear history; the runner composes a chain by matching `from` of the next
251
+ * script to `to` of the previous. */
252
+ interface MigrationScript {
253
+ from: string;
254
+ to: string;
255
+ description: string;
256
+ run: (ctx: MigrationContext) => MigrationResult;
257
+ }
258
+
259
+ /**
260
+ * Migration runner. Given a `from` version present on disk and a target `to`
261
+ * version (usually {@link CURRENT_SCAFFOLD_VERSION}), walks the
262
+ * {@link MIGRATIONS} registry forward in version order, executing each step.
263
+ *
264
+ * Selection rule: a script runs when its `from` is `>= fromArg` (semver-ish
265
+ * string compare is enough at Noir's scale; we don't pull in a semver dep for
266
+ * this) AND its `to` is `<= toArg`. Scripts strictly outside the window are
267
+ * skipped. Within the window, scripts run sorted by `to` ascending so a
268
+ * multi-step upgrade (1.0.0 → 1.1.0 → 1.2.0) composes in order.
269
+ *
270
+ * The runner NEVER throws on a per-script failure — it captures the error,
271
+ * records a synthetic conflict entry (`<path>__error`), and continues so a
272
+ * single broken migration doesn't block the rest of the chain. The orchestrator
273
+ * decides whether non-empty `conflicts` is a hard failure (init --upgrade) or a
274
+ * warning (doctor).
275
+ *
276
+ * Returns the aggregate of every step's `changed`/`conflicts`/`notes`.
277
+ */
278
+ declare function runMigrations(root: string, from: string | null, to: string, opts?: {
279
+ dryRun?: boolean;
280
+ }): MigrationResult & {
281
+ from: string | null;
282
+ to: string;
283
+ ran: string[];
284
+ };
285
+
286
+ declare const MIGRATIONS: readonly MigrationScript[];
287
+ /** Write git-style inline conflict markers around `theirs`/`ours` so a human
288
+ * or AI agent can resolve later. This is the CI-safe fallback the spec locks
289
+ * in (S-OQ2) — no interactive prompts, ever. */
290
+ declare function applyInlineConflict(ours: string, theirs: string, oursLabel?: string, theirsLabel?: string): string;
291
+ /** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no
292
+ * conflict); otherwise emit inline markers. Migration authors should prefer
293
+ * this over {@link applyInlineConflict} when the "no change needed" case is
294
+ * common — it keeps re-runs truly idempotent (no spurious markers on a clean
295
+ * tree). */
296
+ declare function applyWithConflict(ours: string, theirs: string, path: string): {
297
+ text: string;
298
+ conflicted: boolean;
299
+ };
300
+
301
+ /**
302
+ * READ-ONLY stack detection. Probes for well-known marker files under `root`
303
+ * and reports ONLY what is present — never assumes. The result feeds path
304
+ * adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file
305
+ * selection, and the onboarding TUI's confirm step.
306
+ *
307
+ * Design rules (spec §4.5):
308
+ * - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:
309
+ * false, frameworks: [], packageManager: null }`.
310
+ * - Never opens network, never parses code beyond a `package.json`/`pyproject`
311
+ * dependency list. Marker files + top-level manifests only.
312
+ * - Frameworks are reported only when both the marker file is present AND the
313
+ * framework's dependency is listed (avoids false positives from a stale
314
+ * `package.json`).
315
+ */
316
+ interface StackInfo {
317
+ /** Lower-cased language ids found via marker files:
318
+ * `typescript` | `javascript` | `python` | `go` | `rust`. */
319
+ languages: string[];
320
+ /** True when a workspace manifest is present (pnpm-workspace, npm/yarn
321
+ * workspaces in package.json, turbo.json, nx.json). */
322
+ monorepo: boolean;
323
+ /** Lower-cased framework ids (e.g. `next`, `vite`, `express`, `fastapi`,
324
+ * `actix`). Empty when none detected. */
325
+ frameworks: string[];
326
+ /** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */
327
+ packageManager: string | null;
328
+ }
329
+ declare function detectStack(root: string): StackInfo;
330
+
331
+ /**
332
+ * Scaffold orchestrator. One function the cli (S-T2) calls for `noir init`,
333
+ * `noir create`, and `noir sync`; mode selects the manifest subset + how
334
+ * project identity is resolved.
335
+ *
336
+ * High-level flow:
337
+ * 1. Resolve project id (provided > existing > generated; sync requires existing).
338
+ * 2. Detect stack (READ-ONLY; always runs; never throws).
339
+ * 3. If {@link ScaffoldOptions.upgrade}, run migrations from the on-disk
340
+ * scaffold-version → {@link CURRENT_SCAFFOLD_VERSION}.
341
+ * 4. Build the manifest, filter by host + mode.
342
+ * 5. For each entry: mkdir -p, render template/content, dispatch to the
343
+ * matching writer.
344
+ * 6. On `init`/`create`, stamp `.noir/scaffold-version` LAST so a crash leaves
345
+ * an old/absent stamp rather than a misleading fresh one.
346
+ *
347
+ * The orchestrator owns dir creation (writers refuse to so that a missing dir
348
+ * is one attributable failure, not N silent ones).
349
+ */
350
+ type ScaffoldMode = 'init' | 'create' | 'sync';
351
+ interface ScaffoldOptions {
352
+ /** Absolute repo root. For `create`, the new dir (created if absent). */
353
+ root: string;
354
+ mode: ScaffoldMode;
355
+ /** Target host. Defaults to `'claude'` (the only shipped host). */
356
+ host?: HostTag;
357
+ /** MCP transport. Defaults to `'stdio'`. */
358
+ transport?: 'stdio' | 'streamable-http';
359
+ /** Required when transport is `streamable-http`. */
360
+ url?: string;
361
+ /** Explicit project id; bypasses generate/read. Mainly for tests + `create`
362
+ * flows that want deterministic ids. */
363
+ projectId?: string;
364
+ /** `noir init --upgrade`: run migrations before re-emitting, and emit only
365
+ * regenerate + managedBlock (skipIfExists left alone). Only meaningful
366
+ * with mode `'init'`. */
367
+ upgrade?: boolean;
368
+ /** Preview: compute the same written/skipped/migrated lists without touching
369
+ * disk. `noir doctor`/CI use this to report drift. */
370
+ dryRun?: boolean;
371
+ }
372
+ interface ScaffoldResult {
373
+ /** Repo-relative paths actually written. */
374
+ written: string[];
375
+ /** Repo-relative paths skipIfExists'd (already present). */
376
+ skipped: string[];
377
+ /** Migration steps executed (`<from>→<to>`), when upgrade ran. */
378
+ migrationsRan: string[];
379
+ /** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
380
+ migrationConflicts: string[];
381
+ stack: StackInfo;
382
+ projectId: string;
383
+ fromVersion: string | null;
384
+ toVersion: string;
385
+ /** The host actually emitted (post-default). */
386
+ host: HostTag;
387
+ }
388
+ declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
389
+
390
+ /**
391
+ * Scaffold version stamp — Noir's equivalent of Copier's `last-applied`.
392
+ * Stored at `.noir/scaffold-version` as a single `noir-scaffold=<semver>` line
393
+ * so `noir init --upgrade` / `noir doctor` can diff against
394
+ * {@link CURRENT_SCAFFOLD_VERSION} and decide which migrations to run.
395
+ *
396
+ * Format is line-oriented (not YAML) on purpose: it must be readable before
397
+ * `config.yml` is parsed (doctor runs even with a broken config), and the
398
+ * `key=value` shape is trivial to grep from a shell.
399
+ */
400
+ /** The scaffold version this build of @noir-ai/create ships. Bumped atomically
401
+ * whenever a manifest entry, template, or migration changes shape. */
402
+ declare const CURRENT_SCAFFOLD_VERSION = "1.0.0";
403
+ /** Path to the stamp file under `root`. Exposed for tests + doctor. */
404
+ declare function scaffoldVersionPath(root: string): string;
405
+ /** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
406
+ * Never throws — doctor must keep reporting even on a malformed stamp. */
407
+ declare function readScaffoldVersion(root: string): string | null;
408
+ /** Write the stamp, creating `.noir/` if needed. The orchestrator writes it
409
+ * LAST so a crash mid-scaffold leaves an old/absent stamp rather than a
410
+ * misleading fresh one.
411
+ *
412
+ * N2: routed through the package's atomic `regenerate()` writer (tmp+rename in
413
+ * the same dir) for consistency with the rest of the engine's durable writes —
414
+ * a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the
415
+ * stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */
416
+ declare function writeScaffoldVersion(root: string, version: string): void;
417
+
418
+ /**
419
+ * Hand-rolled `{{var}}` interpolation. No mustache/handlebars dependency —
420
+ * Noir's markdown/yaml/json templates are simple enough that a tokenizer +
421
+ * map lookup is sufficient and keeps `@noir-ai/create` dependency-light.
422
+ *
423
+ * Semantics (documented):
424
+ * - Tokens are `{{ name }}` / `{{name}}` — any amount of ASCII whitespace
425
+ * inside the braces is permitted. The name is trimmed.
426
+ * - Known vars (value is a string) are substituted verbatim. NO escaping is
427
+ * applied — callers must pre-escape for the target format (JSON/YAML/MD).
428
+ * This is intentional: the engine renders into multiple formats and a
429
+ * single universal escaper would be wrong for at least one of them.
430
+ * - Unknown vars (not in `vars`, or value `undefined`) → the original token
431
+ * is LEFT IN PLACE. Rationale: drift between template and ctx becomes
432
+ * visible (an unrendered `{{projectId}}` in CLAUDE.md is immediately
433
+ * obvious), rather than silently swallowed to empty. The spec called this
434
+ * out as the preferred failure mode.
435
+ * - Non-string var values are stringified via `String(value)` so a stray
436
+ * number/boolean still interpolates rather than printing `[object Object]`.
437
+ * - A lone `{{` with no closing `}}` is left untouched (not a token).
438
+ */
439
+ declare function render(template: string, vars: Record<string, unknown>): string;
440
+
441
+ /** Read a template's raw text by name (e.g. `noir.md.tmpl`). Throws on missing
442
+ * — an unknown template is a manifest bug and should fail loudly, not render
443
+ * an empty string silently. */
444
+ declare function loadTemplate(name: string): string;
445
+ /** Resolve a template name to its absolute path (for diagnostics + tests). */
446
+ declare function templatesDir(): string;
447
+
448
+ export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MigrationContext, type MigrationResult, type MigrationScript, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type StackInfo, type WriteMode, type WriteOutcome, applyInlineConflict, applyWithConflict, buildHostArtifacts, buildManifest, buildRegion, detectStack, loadTemplate, managedBlock, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeScaffoldVersion };