@noir-ai/create 1.15.0 → 1.16.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
@@ -1,5 +1,6 @@
1
1
  import { HostId, HostAdapter } from '@noir-ai/adapters';
2
- import { ManagedBlock } from '@noir-ai/core';
2
+ import { FileModeOutcome, ManagedBlock } from '@noir-ai/core';
3
+ export { ensureOwnerOnly } from '@noir-ai/core';
3
4
 
4
5
  /** Absolute path of the ancestor store under `root`. */
5
6
  declare function ancestorsPath(root: string): string;
@@ -70,7 +71,6 @@ declare function detectStack(root: string): StackInfo;
70
71
  * file may be refreshed during an upgrade.
71
72
  *
72
73
  * WHY THIS EXISTS
73
- * ---------------
74
74
  * `noir init` writes two files whose only job is to be read by a human:
75
75
  * `.noir/.env.example` (documents the variable set) and `.noir/rules/RULES.md`
76
76
  * (the AI working-contract seed). Both are emitted create-only-if-absent, and
@@ -92,7 +92,6 @@ declare function detectStack(root: string): StackInfo;
92
92
  * leftover and refreshing it loses nothing.
93
93
  *
94
94
  * THE CONTRACT
95
- * ------------
96
95
  * {@link isStaleSeed} answers exactly that question. It is deliberately
97
96
  * conservative in one direction: it returns `true` only on an exact match
98
97
  * against a recorded past version, so ANY other byte difference — a user edit,
@@ -109,7 +108,6 @@ declare function detectStack(root: string): StackInfo;
109
108
  * file it just read; the argument makes it say so.
110
109
  *
111
110
  * RENDERING
112
- * ---------
113
111
  * Seed templates are normally interpolated through `render(template, vars)`
114
112
  * before writing. That step is a no-op for the two templates recorded here:
115
113
  * neither contains a `{{...}}` token, so `render(text, vars) === text` for
@@ -126,7 +124,6 @@ declare function detectStack(root: string): StackInfo;
126
124
  * requirement impossible to overlook.
127
125
  *
128
126
  * APPENDING TO THE HISTORY
129
- * ------------------------
130
127
  * Record the template bytes as they shipped, keyed by the scaffold version
131
128
  * whose `init` wrote them, and never edit an existing entry — a user's disk
132
129
  * still holds those old bytes, and rewriting history would silently reclassify
@@ -276,8 +273,15 @@ declare function buildRegion(block: ManagedBlock, body: string): string;
276
273
  * `writeFileSync`: a requested mode must land on the temp, and an interrupted
277
274
  * seed write should not leave a half-written `.noir/.env` behind. Byte output
278
275
  * is unchanged for every existing caller (same content, umask default 0o644
279
- * when no mode is requested). */
276
+ * when no mode is requested).
277
+ *
278
+ * An EXISTING file keeps this writer's create-only contract, but its mode is
279
+ * not left to drift: the orchestrator calls {@link ensureOwnerOnly} after the
280
+ * emit, which tightens a group/world-readable credential to 0600. */
280
281
  declare function skipIfExists(absPath: string, content: string, fileMode?: number): WriteOutcome;
282
+ /** What happened when the owner-only mode was re-asserted on a credential
283
+ * file — see {@link ensureOwnerOnly}. */
284
+ type EnvMode = FileModeOutcome;
281
285
 
282
286
  /**
283
287
  * Declarative scaffold manifest — the single source of truth for what
@@ -348,11 +352,12 @@ interface ManifestEntry {
348
352
  /** One-line human description for `noir doctor` + logs. */
349
353
  description?: string;
350
354
  /** Permission for a NEWLY created file (e.g. `0o600` for a credential seed).
351
- * Applied only on creation; an existing file's mode is never changed — the
352
- * `skipIfExists` contract means an existing file is not even opened. The
353
- * orchestrator forwards this to the writer and reports it on the result as
354
- * {@link ScaffoldResult.fileModes}. Ignored by every other write mode, whose
355
- * targets are regenerated rather than seeded.
355
+ * Applied only on creation — the `skipIfExists` contract means an existing
356
+ * file is not even opened. The orchestrator forwards this to the writer and
357
+ * reports it on the result as {@link ScaffoldResult.fileModes}; the mode of
358
+ * an existing credential file is re-asserted separately (`ensureOwnerOnly`).
359
+ * Ignored by every other write mode, whose targets are regenerated rather
360
+ * than seeded.
356
361
  * NOTE: POSIX-only. Windows permissions are ACL-based and ignore it. */
357
362
  fileMode?: number;
358
363
  /** Required for `mergeJson` mode: the JSON patch object (or a template that
@@ -385,6 +390,14 @@ type BuildManifestContext = {
385
390
  * .prettierignore only for JS; .dockerignore only when a Dockerfile is
386
391
  * present; an unknown/empty stack ⇒ all four, for backward compat). */
387
392
  stack?: StackInfo;
393
+ /** The `rules.enabled` switch from the project's `.noir/config.yml`. False
394
+ * drops the working-rules seed from the manifest, so no command emits or
395
+ * backfills `.noir/rules/RULES.md` for a project that opted out. Undefined
396
+ * (the schema's default state, and every caller that predates the switch)
397
+ * means enabled — a project with no `rules:` block emits the seed exactly
398
+ * as before. A file already on disk is never removed: dropping the entry
399
+ * removes the emission, not the file. */
400
+ rulesEnabled?: boolean;
388
401
  };
389
402
  /** Co-owned NOIR.md auto-brief region. Defined locally (not exported from
390
403
  * core) because core's own named block instances cover only the three
@@ -433,6 +446,12 @@ interface BuildHostArtifactsContext {
433
446
  * native install is detected, else `'noir'`). Passed through to
434
447
  * `adapter.emitMcpConfig`. */
435
448
  command: string;
449
+ /** The project's `rules.enabled` switch. False drops the rules `@`-import a
450
+ * host would otherwise emit (CLAUDE.md's rules block, GEMINI.md's rules
451
+ * block, AGENTS.md's rules import) so no host file points at a
452
+ * `.noir/rules/RULES.md` that the switch withheld. Undefined means enabled —
453
+ * byte-identical to every build that predates the switch. */
454
+ rulesEnabled?: boolean;
436
455
  }
437
456
  /**
438
457
  * Materialize the host-specific manifest entries from a resolved adapter.
@@ -466,12 +485,22 @@ interface BuildHostArtifactsContext {
466
485
  * collided with the cursor flat-skill prune of `noir-*.mdc` under
467
486
  * `.cursor/rules/`, and cursor's rules are already delivered via
468
487
  * AGENTS.md's `@.noir/rules/RULES.md` import.)
488
+ *
489
+ * That rules import/block is dropped for every host when
490
+ * `ctx.rulesEnabled` is false: the switch withholds the seed, so pointing
491
+ * a host at it would resolve to nothing. The context import is unaffected
492
+ * — the switch gates the rules, not the brief. A host file that already
493
+ * carries the rules region from when the switch was on keeps it: dropping
494
+ * the entry stops the emission, and nothing here rewrites a region out of
495
+ * an existing file.
469
496
  * 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`
470
497
  * (default `<root>/.mcp.json` for claude), content from
471
498
  * `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template
472
499
  * path (byte-identical parity with v1.1 + the .mcp.json parity test that
473
- * compares against `claudeAdapter.emitMcpConfig`); other hosts use the
474
- * adapter directly.
500
+ * compares against `claudeAdapter.emitMcpConfig`) for a repo that has not
501
+ * joined a workspace; a joined repo — one carrying a `.noir/workspace.json`
502
+ * marker — gets the workspace entry instead, on every host, whatever
503
+ * transport the run asked for (see the entry's own comment below).
475
504
  *
476
505
  * Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with
477
506
  * `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`
@@ -546,10 +575,11 @@ declare function mergeThreeWay(base: string, ours: string, theirs: string, style
546
575
  * `result.conflicts` so the caller (doctor/CI) can fail loudly with a
547
576
  * reviewable artifact rather than hang on a prompt.
548
577
  *
549
- * The registry ({@link MIGRATIONS}) carries three entries: the synthetic
550
- * `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and two real
551
- * version-to-version migrations, `1.0.0 → 1.1.0` and `1.1.0 → 1.2.0`. The
552
- * synthetic one is the copy-pasteable template for the next contributor.
578
+ * The registry ({@link MIGRATIONS}) carries four entries: the synthetic
579
+ * `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and three
580
+ * real version-to-version migrations, `1.0.0 → 1.1.0`, `1.1.0 → 1.2.0` and
581
+ * `1.2.0 → 1.3.0`. The synthetic one is the copy-pasteable template for the
582
+ * next contributor.
553
583
  */
554
584
  interface MigrationContext {
555
585
  /** Absolute repo root the migration operates on. */
@@ -558,6 +588,12 @@ interface MigrationContext {
558
588
  * the same `changed`/`conflicts` shape, but does NOT touch disk. Used by
559
589
  * `noir doctor`/CI to preview. */
560
590
  dryRun?: boolean;
591
+ /** The project's `rules.enabled` switch, when the caller knows it. False
592
+ * tells a migration that `.noir/rules/RULES.md` is not part of this project
593
+ * — the scaffold withholds the seed — so a migration must not write that
594
+ * file back, however convincing its evidence for a refresh. Undefined means
595
+ * enabled, which is the state of every project that predates the switch. */
596
+ rulesEnabled?: boolean;
561
597
  }
562
598
  interface MigrationResult {
563
599
  /** Repo-relative paths the migration modified (or would modify, in dryRun). */
@@ -598,6 +634,7 @@ interface MigrationScript {
598
634
  */
599
635
  declare function runMigrations(root: string, from: string | null, to: string, opts?: {
600
636
  dryRun?: boolean;
637
+ rulesEnabled?: boolean;
601
638
  }): MigrationResult & {
602
639
  from: string | null;
603
640
  to: string;
@@ -753,6 +790,18 @@ interface ScaffoldResult {
753
790
  * mode is never changed. Optional so existing external constructors of this
754
791
  * type (test stubs) stay valid; `scaffold()` always populates it. */
755
792
  fileModes?: Record<string, number>;
793
+ /** What the run did to the permissions of `.noir/.env` — the outcome of
794
+ * {@link ensureOwnerOnly}, which is called on every run that may write, in
795
+ * every mode, because the seeded file's 0600 mode is applied only when it is
796
+ * created.
797
+ * `'healed'` means a group/world-readable file was tightened to 0600, so a
798
+ * caller can report the fix instead of leaving the user to notice it in a
799
+ * later diagnostic; `'unchanged'` and `'unsupported'` mean nothing needed
800
+ * doing (or the platform has no POSIX mode bits) — the `'unchanged'` a fresh
801
+ * `create` reports, whose seed was written 0600 in the same run. Absent on a
802
+ * dry run, which must not touch disk. Optional so existing external
803
+ * constructors of this type stay valid. */
804
+ envMode?: EnvMode;
756
805
  }
757
806
  /** One entry in {@link ScaffoldResult.conflicts}. */
758
807
  interface ConflictRecord {
@@ -831,8 +880,11 @@ declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
831
880
  * `key=value` shape is trivial to grep from a shell.
832
881
  */
833
882
  /** The scaffold version this build of @noir-ai/create ships. Bumped atomically
834
- * whenever a manifest entry, template, or migration changes shape. */
835
- declare const CURRENT_SCAFFOLD_VERSION = "1.2.0";
883
+ * whenever a manifest entry, template, or migration changes shape. 1.3.0 adds
884
+ * the migration that points a joined repo's MCP entry back at the workspace
885
+ * bridge — a repair only the upgrade path can perform, so projects stamped
886
+ * 1.2.0 and below need a window that starts at their version to reach it. */
887
+ declare const CURRENT_SCAFFOLD_VERSION = "1.3.0";
836
888
  /** Path to the stamp file under `root`. Exposed for tests + doctor. */
837
889
  declare function scaffoldVersionPath(root: string): string;
838
890
  /** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
@@ -878,4 +930,4 @@ declare function loadTemplate(name: string): string;
878
930
  /** Resolve a template name to its absolute path (for diagnostics + tests). */
879
931
  declare function templatesDir(): string;
880
932
 
881
- export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictRecord, type ConflictResolution, type ConflictResolverReturn, type DiffLine, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MergeResult, type MigrationContext, type MigrationResult, type MigrationScript, SEED_TEMPLATE_HISTORY, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type SeedTemplateHistoryEntry, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, isStaleSeed, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };
933
+ export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictRecord, type ConflictResolution, type ConflictResolverReturn, type DiffLine, type EnvMode, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MergeResult, type MigrationContext, type MigrationResult, type MigrationScript, SEED_TEMPLATE_HISTORY, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type SeedTemplateHistoryEntry, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, isStaleSeed, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };