claude-use 4.4.0 → 5.0.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.
Files changed (36) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.cjs +3382 -2595
  3. package/dist/index.cjs +3061 -0
  4. package/dist/index.mjs +2991 -0
  5. package/dist/types/cliError.d.ts +27 -0
  6. package/dist/types/config/classify.d.ts +49 -0
  7. package/dist/types/config/load.d.ts +42 -0
  8. package/dist/types/config/schema.d.ts +689 -0
  9. package/dist/types/config/store.d.ts +46 -0
  10. package/dist/types/frontdoor/capability.d.ts +19 -0
  11. package/dist/types/frontdoor/connect.d.ts +179 -0
  12. package/dist/types/frontdoor/ensure.d.ts +34 -0
  13. package/dist/types/frontdoor/probe.d.ts +13 -0
  14. package/dist/types/frontdoor/state.d.ts +50 -0
  15. package/dist/types/frontdoor/supervisor.d.ts +48 -0
  16. package/dist/types/headroom/ensure.d.ts +31 -0
  17. package/dist/types/headroom/state.d.ts +71 -0
  18. package/dist/types/headroom/supervisor.d.ts +102 -0
  19. package/dist/types/index.d.ts +18 -0
  20. package/dist/types/launcher/farm.d.ts +165 -0
  21. package/dist/types/launcher/guard.d.ts +57 -0
  22. package/dist/types/launcher/lock.d.ts +41 -0
  23. package/dist/types/launcher/ports.d.ts +133 -0
  24. package/dist/types/paths.d.ts +69 -0
  25. package/dist/types/resolve/conditions.d.ts +37 -0
  26. package/dist/types/resolve/decide.d.ts +45 -0
  27. package/dist/types/resolve/extends.d.ts +41 -0
  28. package/dist/types/resolve/flatten.d.ts +13 -0
  29. package/dist/types/resolve/match.d.ts +58 -0
  30. package/dist/types/resolve/pipeline.d.ts +30 -0
  31. package/dist/types/resolve/plan.d.ts +57 -0
  32. package/dist/types/resolve/projects.d.ts +61 -0
  33. package/dist/types/resolve/reconcile.d.ts +80 -0
  34. package/dist/types/resolve/types.d.ts +159 -0
  35. package/dist/types/resolve/walk.d.ts +85 -0
  36. package/package.json +16 -2
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The library surface of claude-use: the pure, port-injected parts other tools can call in-process instead of shelling out to the CLI. Three groups, none of which imports interactive prompting or argument parsing:
3
+ *
4
+ * - identity and configuration-profile resolution and farm sync (`resolveDecisions`, `resyncFarm`, `buildEntryFacts`, ...), pure over facts and ports passed in;
5
+ * - routing: the front door's Remote-Control-preserving, capability-authenticated CONNECT surface and its certificate authority, the front-door supervisor and ensure lifecycle, and the headroom supervisor and ensure lifecycle, each pure over its injected ports;
6
+ * - the ambient-credential guard.
7
+ *
8
+ * Everything the CLI alone needs (commander wiring, prompts, `doctor`, `check`) is deliberately not exported here.
9
+ */
10
+ export { resolveDecisions, topLevelNames, type ResolveDecisionsInput, type ResolvedState } from "./resolve/pipeline";
11
+ export type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade, Layer, LayerId } from "./resolve/types";
12
+ export { buildEntryFacts, carryOver, FARM_MANIFEST_FILENAME, readFarmManifest, recoverFarm, recoveryDiagnostics, resyncFarm, type BuildEntryFactsParams, type CarryOverParams, type CarryOverResult, type RecoverFarmParams, type RecoveryResult, type ResyncFarmParams, type ResyncFarmResult, } from "./launcher/farm";
13
+ export { CONNECT_INTERCEPT_HOST, CONNECT_LIMITS, createLeafCache, ensureCa, forwardableHeaders, generateCa, isInterceptedHost, mintLeaf, parseConnectTarget, realConnectCertStore, realConnectEffects, ROUTED_PATH_PREFIX, servedByPipeline, startConnectServer, type CaMaterial, type ConnectCertStore, type ConnectEffects, type ConnectLimits, type ConnectServerConfig, type ConnectServerHandle, type ConnectTarget, type LeafCert, } from "./frontdoor/connect";
14
+ export { ensureFrontDoor, FrontDoorStartError, type EnsureFrontDoorPorts } from "./frontdoor/ensure";
15
+ export { runFrontDoorSupervisor, type FrontDoorSupervisorPorts, type RunFrontDoorSupervisorOptions } from "./frontdoor/supervisor";
16
+ export { ensureHeadroom, HeadroomStartError, type EnsureHeadroomPorts } from "./headroom/ensure";
17
+ export { resolveSupervisorConfig, runSupervisor, type HeadroomSupervisorConfig, type RunSupervisorOptions, type SupervisorPorts } from "./headroom/supervisor";
18
+ export { AMBIENT_CREDENTIAL_VARS, detectAmbientCredential, evaluateAmbientCredentialGuard, formatAmbientCredentialGuardMessage, type AmbientCredentialDetection, type AmbientCredentialGuardResult, type AmbientCredentialVar, type EvaluateAmbientCredentialGuardParams, type InjectedCredential, } from "./launcher/guard";
@@ -0,0 +1,165 @@
1
+ import type { CategoryClassification, CategoryClassificationOverlay } from "../config/schema";
2
+ import { type FarmManifest } from "../resolve/reconcile";
3
+ import { type ResolvedState } from "../resolve/pipeline";
4
+ import type { CascadeInput } from "../resolve/walk";
5
+ import type { Diagnostic, EntryFacts } from "../resolve/types";
6
+ import type { FarmFs } from "./ports";
7
+ /** The manifest file each farm carries at its own root, recording what that resync built. Named distinctly from anything Claude Code itself writes into a config directory. */
8
+ export declare const FARM_MANIFEST_FILENAME = ".claude-use-farm.json";
9
+ /** Reads and validates the manifest at a farm root, or undefined when it is missing, unparseable, or does not validate. */
10
+ export declare function readFarmManifest(fs: FarmFs, farmRoot: string): FarmManifest | undefined;
11
+ /** Everything `buildEntryFacts` needs. Everything except the tree itself is injected, so the fact manifest is reproducible from a fake filesystem, a fake clock, and a fake branch. */
12
+ export interface BuildEntryFactsParams {
13
+ readonly fs: FarmFs;
14
+ readonly claudeHome: string;
15
+ readonly home: string;
16
+ readonly cwd: string;
17
+ readonly nowMs: number;
18
+ readonly env: Readonly<Record<string, string | undefined>>;
19
+ readonly branch?: string;
20
+ readonly branchDetached?: boolean;
21
+ }
22
+ /**
23
+ * Walks the canonical `~/.claude` and builds the `EntryFacts` manifest the pure resolver consumes.
24
+ *
25
+ * Two properties matter more than the walk itself. Symlinks are never followed: a directory symlink escaping the tree is one entry, not a subtree, which keeps the walk finite and matches how the farm links such an entry back. And every directory's `latestMtimeMs`/`totalSizeBytes` aggregate over its whole subtree rather than reporting its own inode, because a directory-scoped `newerThan` or `maxSizeBytes` condition asks about the data inside the directory — a directory's own mtime does not move when a file three levels down is rewritten.
26
+ */
27
+ export declare function buildEntryFacts(params: BuildEntryFactsParams): EntryFacts;
28
+ /** Inputs to `carryOver`. */
29
+ export interface CarryOverParams {
30
+ readonly fs: FarmFs;
31
+ /** The superseded farm, already renamed aside. */
32
+ readonly previousRoot: string;
33
+ /** The new farm, already swapped into place. */
34
+ readonly farmRoot: string;
35
+ /**
36
+ * When given, a colliding name classified as `runtime` is resolved automatically rather than reported — see `CarryOverResult.autoResolved` for why that is always safe. Optional because a caller with no classification loaded (there is none, today) should get the old, fully-manual behaviour rather than a crash; every real call site passes it.
37
+ */
38
+ readonly classification?: {
39
+ readonly defaults: CategoryClassification;
40
+ readonly overlay?: CategoryClassificationOverlay;
41
+ };
42
+ }
43
+ /** What `carryOver` moved, resolved on its own, and could not. */
44
+ export interface CarryOverResult {
45
+ /** Top-level names moved from the superseded farm into the new one. */
46
+ readonly carried: readonly string[];
47
+ /** Top-level names that collided but were resolved automatically because their category is `runtime` — see the doc comment on `carryOver` for why discarding the old copy is always safe here. */
48
+ readonly autoResolved: readonly string[];
49
+ /** Top-level names left behind because the new farm has its own entry of that name, and resolving them needs a human — everything that collided but was not classified `runtime`. */
50
+ readonly collided: readonly string[];
51
+ }
52
+ /**
53
+ * Moves an identity's own locally-written state out of a superseded farm and into the new one.
54
+ *
55
+ * This is what makes a whole-directory swap safe at all. The farm root *is* `CLAUDE_CONFIG_DIR`, so it holds more than symlinks: the identity's own `identity.json`, the credentials file Claude Code writes on Linux and Windows, its `.claude.json`, its daemon and runtime state. None of that is shared, none of it is rebuildable, and a swap that simply replaced the directory would destroy all of it.
56
+ *
57
+ * Anything the previous resync built itself is skipped rather than carried: a symlink is a view of the canonical tree with no data of its own, and a directory the manifest records as materialised had its real children adopted into `~/.claude` before the swap ever started. Everything else is real local data and moves across by rename, so a credential file is relocated rather than duplicated — never briefly existing as two copies on disk.
58
+ *
59
+ * A name that exists in both is genuinely ambiguous in general — overwriting the new farm's own entry would discard whatever the resync just decided; overwriting the old one would discard data — so by default it is reported rather than resolved here, and the caller keeps the superseded farm on disk in that case.
60
+ *
61
+ * One category is not ambiguous, though: `runtime`'s own definition (see `config/categories.default.json`'s category table in the README) is specifically "live per-process or per-machine artifacts" — daemon locks, an MCP auth-needed cache, an update-check result — that make no sense being preserved across a swap at all, let alone fought over. When `classification` is given, a colliding name whose category resolves to `runtime` is discarded from the superseded copy and left exactly as the new farm already has it, with no data ever moved: `keep-new` is not a judgement call for this category, it is what the category already means. This needs only the name's *static* classification, never the resolved shared/not-shared decision for the current directory — a `runtime` entry is disposable whether or not this identity currently chooses to share it, so no cascade resolution is needed to make the call.
62
+ *
63
+ * Exported so `launcher/farmResolve.ts` can reuse this exact collision detection (and the same `runtime` auto-resolution) for `claude-use identity resolve-conflicts`'s interactive pass, rather than a second implementation that could drift from this one.
64
+ */
65
+ export declare function carryOver(params: CarryOverParams): CarryOverResult;
66
+ /** What recovery found and did. */
67
+ export interface RecoveryResult {
68
+ /** Abandoned scratch trees removed. */
69
+ readonly removedScratch: readonly string[];
70
+ /** Set when the farm root was missing and a superseded farm was restored into its place. */
71
+ readonly restoredFrom?: string;
72
+ /** Superseded farms whose carry-over was completed and which were then discarded. */
73
+ readonly completed: readonly string[];
74
+ /** Top-level names, across every superseded farm processed, resolved automatically because their category is `runtime` — see `carryOver`'s own doc comment for why that needs no human decision. */
75
+ readonly autoResolved: readonly string[];
76
+ /** Superseded farms left on disk because they still held colliding data a human still needs to decide. */
77
+ readonly retained: readonly string[];
78
+ /** True when recovery changed anything, in which case the farm cannot be assumed to match its own manifest. */
79
+ readonly recovered: boolean;
80
+ }
81
+ /** Inputs to `recoverFarm`: the lock-taking wrapper around `recoverInterruptedSwap`. */
82
+ export interface RecoverFarmParams {
83
+ readonly fs: FarmFs;
84
+ readonly identitiesDir: string;
85
+ readonly identity: string;
86
+ readonly now: () => number;
87
+ readonly lock: ResyncFarmParams["lock"];
88
+ readonly classification?: {
89
+ readonly defaults: CategoryClassification;
90
+ readonly overlay?: CategoryClassificationOverlay;
91
+ };
92
+ }
93
+ /**
94
+ * Runs `recoverInterruptedSwap` under the identity's own lock.
95
+ *
96
+ * This exists as its own entry point because recovery has to happen *before* the identity's `identity.json` is read, not merely before the farm is rebuilt: that file lives inside the farm root, so a crash between the swap's two renames leaves it sitting in a superseded directory where nothing will find it. Reading the identity first would silently launch with its `defaultConfigProfile` and `allowAmbientCredential` unset — the first of which decides what the farm is about to share.
97
+ *
98
+ * `resyncFarm` calls `recoverInterruptedSwap` directly rather than calling this, since it already holds the lock; taking it a second time from the same process would block against itself.
99
+ */
100
+ export declare function recoverFarm(params: RecoverFarmParams): RecoveryResult;
101
+ /** Inputs to `resyncFarm`. */
102
+ export interface ResyncFarmParams {
103
+ readonly fs: FarmFs;
104
+ /** `~/.claude-use/identities` — the farm root's parent, and where the lock, scratch trees, and superseded farms all live. */
105
+ readonly identitiesDir: string;
106
+ readonly identity: string;
107
+ /** The configuration profile this launch resolved to, recorded in the manifest for `claude-use check`. */
108
+ readonly configProfile?: string;
109
+ /** The canonical `~/.claude` every farm symlink points back into. */
110
+ readonly claudeHome: string;
111
+ readonly home: string;
112
+ readonly cwd: string;
113
+ readonly env: Readonly<Record<string, string | undefined>>;
114
+ readonly branch?: string;
115
+ readonly branchDetached?: boolean;
116
+ readonly cascade: CascadeInput;
117
+ readonly classification: {
118
+ readonly defaults: CategoryClassification;
119
+ readonly overlay?: CategoryClassificationOverlay;
120
+ };
121
+ /** Reads the current time. Called repeatedly while waiting on the lock, then once more to stamp the build. */
122
+ readonly now: () => number;
123
+ /** Distinguishes this resync's scratch and superseded directory names from any other process's. `${pid}.${uuid}` in real use; a fixed string in tests. */
124
+ readonly uniqueSuffix: string;
125
+ readonly lock: {
126
+ readonly pid: number;
127
+ readonly isRunning: (pid: number) => boolean;
128
+ readonly sleep: (ms: number) => void;
129
+ readonly staleAfterMs?: number;
130
+ readonly retryDelayMs?: number;
131
+ readonly maxAttempts?: number;
132
+ };
133
+ }
134
+ /** The outcome of one resync. */
135
+ export interface ResyncFarmResult {
136
+ readonly farmRoot: string;
137
+ /** True when the resolved decision already matched what the farm's own manifest recorded, so nothing was written. */
138
+ readonly noOp: boolean;
139
+ /** The manifest the farm now carries — the newly written one, or the existing one on a no-op. */
140
+ readonly manifest: FarmManifest;
141
+ /** The fully resolved state, including the flattened cascade's launch flags, which the launcher goes on to use. */
142
+ readonly resolved: ResolvedState;
143
+ readonly adopted: readonly string[];
144
+ readonly recovery: RecoveryResult;
145
+ readonly diagnostics: readonly Diagnostic[];
146
+ readonly retainedPrevious?: string;
147
+ }
148
+ /**
149
+ * Brings one identity's farm into line with the cascade resolved for the current directory, and hands back everything the launcher needs to go on and spawn the real binary.
150
+ *
151
+ * The order below is fixed and each step depends on the one before it:
152
+ *
153
+ * 1. Take the per-identity lock, so two sessions under one identity in two directories cannot rewrite the same farm toward two different states at once.
154
+ * 2. Finish or undo any swap a previous launch was killed in the middle of, so everything after this point sees a coherent farm.
155
+ * 3. Read the previous manifest and, guided by it, list the parts of the still-live farm reconciliation is allowed to look at. This step only reads the farm; nothing mutates it.
156
+ * 4. Adopt anything Claude Code wrote into a materialised directory back into the canonical `~/.claude`.
157
+ * 5. **Then** build the fact manifest, from a canonical tree that now includes everything step 4 adopted. Building facts before adoption would make newly adopted data invisible to this resync and only visible on the next launch — the single most important ordering constraint in this module.
158
+ * 6. Resolve the cascade and plan the farm, both of them pure functions over those facts.
159
+ * 7. If the plan matches what the farm's manifest already records and step 4 adopted nothing, stop: no filesystem write happens at all. Launching repeatedly from the same directory with no configuration change is the overwhelmingly common case and it must stay cheap.
160
+ * 8. Otherwise build a scratch tree, write its manifest, and swap it into place.
161
+ * 9. Release the lock — before the real binary is spawned, never held across it.
162
+ */
163
+ export declare function resyncFarm(params: ResyncFarmParams): ResyncFarmResult;
164
+ /** Turns a recovery result into the one diagnostic worth reporting about it, or nothing when there was nothing to recover. */
165
+ export declare function recoveryDiagnostics(recovery: RecoveryResult, identity: string): Diagnostic[];
@@ -0,0 +1,57 @@
1
+ import type { CredentialTargetVar } from "../config/schema";
2
+ /**
3
+ * The environment variables that authenticate Claude Code directly from the process environment, ahead of any stored identity credential. If any of these is set, every identity would silently authenticate as the same key/token/backend while it's set — defeating the entire premise of separate identities. Order here is also lookup order: `detectAmbientCredential` reports the first one found.
4
+ */
5
+ export declare const AMBIENT_CREDENTIAL_VARS: readonly ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"];
6
+ export type AmbientCredentialVar = (typeof AMBIENT_CREDENTIAL_VARS)[number];
7
+ /** Which guarded variable was found set, and to what. */
8
+ export interface AmbientCredentialDetection {
9
+ readonly variable: AmbientCredentialVar;
10
+ }
11
+ /** A credential this launch itself exports to the child: the variable it lands in and its value. */
12
+ export interface InjectedCredential {
13
+ readonly variable: CredentialTargetVar;
14
+ readonly token: string;
15
+ }
16
+ /**
17
+ * Detects whether any of `AMBIENT_CREDENTIAL_VARS` is set to a non-empty value in `env`, returning the first one found in declared order, or undefined when none are set.
18
+ *
19
+ * An empty string counts as unset, not set — confirmed load-bearing: one of Joe's real wrapper scripts (`o`, running Claude Code against OpenRouter) does `export ANTHROPIC_API_KEY=""` specifically to *clear* it so `ANTHROPIC_AUTH_TOKEN` takes effect instead, and this must never trip the guard.
20
+ *
21
+ * `injected`, when given, is the credential this launch exports for its own identity. Its variable holding exactly that value is not ambient: it is what a claude-use launch of the same identity left in the environment of the session this one starts from (a `claude @work` run inside a `claude @work` session). The same variable holding any other value still counts.
22
+ */
23
+ export declare function detectAmbientCredential(env: Readonly<Record<string, string | undefined>>, injected?: InjectedCredential): AmbientCredentialDetection | undefined;
24
+ /**
25
+ * Builds the exact refusal message: which variable was found, why it matters, and the two ways to opt in (a one-off env var, or a persistent per-identity setting). Mirrors the message documented in the project's README. `identityName` is included in the persistent-opt-in command when known; when no identity has been resolved yet (e.g. the `CLAUDE_CONFIG_DIR`-already-set escape hatch, or no identity resolved at all), a generic `<name>` placeholder is used instead, matching the README's own generic wording.
26
+ */
27
+ export declare function formatAmbientCredentialGuardMessage(variable: AmbientCredentialVar, identityName?: string): string;
28
+ /** Inputs to the ambient-credential guard evaluation. */
29
+ export interface EvaluateAmbientCredentialGuardParams {
30
+ readonly env: Readonly<Record<string, string | undefined>>;
31
+ /** The active identity's own `allowAmbientCredential` setting from its `identity.json`, or false when no identity is known. */
32
+ readonly allowAmbientCredential: boolean;
33
+ /** True when `CLAUDE_USE_ALLOW_AMBIENT_CREDENTIAL=1` is set for this one invocation. */
34
+ readonly allowAmbientCredentialOverride: boolean;
35
+ /** The active identity's name, for the message's persistent-opt-in command. Undefined when no identity is known. */
36
+ readonly identityName?: string;
37
+ /**
38
+ * True when this launch routes through an API provider. The guard inspects the PARENT environment, before `buildEnv` runs; a provider launch has its `ANTHROPIC_AUTH_TOKEN` injected and its `ANTHROPIC_API_KEY` cleared by `buildEnv` itself, so an ambient credential in the parent environment never reaches the child and there is nothing left for this guard to protect against. Everything else about the guard (including `IdentitySchema.allowAmbientCredential`) is unchanged by this flag.
39
+ */
40
+ readonly providerSelected?: boolean;
41
+ /** The launching identity's own credential, when its credential block resolved: see `detectAmbientCredential`. */
42
+ readonly injectedCredential?: InjectedCredential;
43
+ }
44
+ /** The result of one guard evaluation: either launch may proceed, or it must be refused with an explanatory message. */
45
+ export type AmbientCredentialGuardResult = {
46
+ readonly ok: true;
47
+ } | {
48
+ readonly ok: false;
49
+ readonly variable: AmbientCredentialVar;
50
+ readonly message: string;
51
+ };
52
+ /**
53
+ * Evaluates the ambient-credential guard: refuses unless no guarded variable is set, or the active identity opted in (`allowAmbientCredential: true`), or this one invocation opted in (`CLAUDE_USE_ALLOW_AMBIENT_CREDENTIAL=1`), or a provider takes over the child's credential (see `providerSelected`).
54
+ *
55
+ * This guard is about credential isolation, not identity/config-dir selection — it must still run even when the `CLAUDE_CONFIG_DIR`-already-set escape hatch applies (callers pass `allowAmbientCredential: false` and no `identityName` in that case, since there is no active identity to consult).
56
+ */
57
+ export declare function evaluateAmbientCredentialGuard(params: EvaluateAmbientCredentialGuardParams): AmbientCredentialGuardResult;
@@ -0,0 +1,41 @@
1
+ import { CliError } from "../cliError";
2
+ import type { FarmFs } from "./ports";
3
+ /** A held identity lock. Releasing is idempotent and never removes a lock some other process has since taken. */
4
+ export interface IdentityLock {
5
+ readonly path: string;
6
+ readonly token: string;
7
+ readonly release: () => void;
8
+ }
9
+ /** Raised when another process holds the identity's lock and did not release it within the retry budget. */
10
+ export declare class IdentityLockBusyError extends CliError {
11
+ readonly identity: string;
12
+ readonly lockPath: string;
13
+ readonly holderPid: number | undefined;
14
+ constructor(identity: string, lockPath: string, holderPid: number | undefined);
15
+ }
16
+ /** Everything `acquireIdentityLock` needs, all of it injected so a test never sleeps for real, never reads a real pid table, and never writes outside its own fake filesystem. */
17
+ export interface AcquireIdentityLockParams {
18
+ readonly identity: string;
19
+ /** The directory the lock file lives in — `~/.claude-use/identities`, alongside (never inside) the farm the lock protects, so a farm swap can rename the farm root out from under itself without disturbing the lock. */
20
+ readonly dir: string;
21
+ readonly fs: FarmFs;
22
+ readonly nowMs: () => number;
23
+ readonly pid: number;
24
+ /** Answers whether a process is still running, so a lock left behind by a crash is recognised rather than waited out for the full staleness window. Zombie-aware: a holder that exited without being reaped still answers signal 0 as alive, but will never release the lock itself. */
25
+ readonly isRunning: (pid: number) => boolean;
26
+ /** Blocks for the given number of milliseconds. Synchronous by necessity: the whole launcher is synchronous, right through to `spawnSync`. */
27
+ readonly sleep: (ms: number) => void;
28
+ readonly staleAfterMs?: number;
29
+ readonly retryDelayMs?: number;
30
+ readonly maxAttempts?: number;
31
+ }
32
+ /** The path of one identity's lock file. */
33
+ export declare function identityLockPath(dir: string, identity: string): string;
34
+ /**
35
+ * Takes the per-identity resync lock, held for the whole of a resync and released before the real `claude` binary is spawned.
36
+ *
37
+ * Two terminals in two different client directories under one identity is exactly the pattern directory rules exist to support, and it is exactly the pattern that races: both launches resolve their own cascade and both want to rewrite the same identity's farm toward two different states. This serialises them.
38
+ *
39
+ * Mutual exclusion rests on one exclusive-create — a read-then-write pair would leave a window where both processes see no lock. A lock whose holder is no longer running, or whose record is older than the staleness window, or whose contents cannot be parsed at all, is removed and retaken: a machine that crashed mid-resync must not need manual cleanup before `claude` works again. A lock genuinely held by a live process is waited on, and only after the full retry budget does this give up — loudly, with the holder's pid, rather than proceeding unsynchronised.
40
+ */
41
+ export declare function acquireIdentityLock(params: AcquireIdentityLockParams): IdentityLock;
@@ -0,0 +1,133 @@
1
+ import type { ConfigFileReader } from "../config/load";
2
+ /**
3
+ * Filesystem operations the launcher needs, injected so no test in this project ever touches a real filesystem — consistent with the resolver core's own injected-facts approach in `src/resolve/types.ts`.
4
+ */
5
+ export interface FsPort {
6
+ /** Reads a file's contents as UTF-8 text, or returns undefined when it does not exist. Used for plain-text files like the persisted active-identity file. */
7
+ readonly readFileUtf8: (filePath: string) => string | undefined;
8
+ /** Reads and parses a config file (JSON/YAML/JS via cosmiconfig in the real implementation), returning undefined when it does not exist. Mirrors `src/config/load.ts`'s own injected reader shape so `loadIdentity` can compose directly with `loadConfigFile`. */
9
+ readonly readConfigFile: ConfigFileReader;
10
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. Used to name the known providers in an unknown-provider refusal. */
11
+ readonly readdir: (dir: string) => readonly string[];
12
+ }
13
+ /** What one node of a tree is, as reported by an `lstat` that never follows symlinks. */
14
+ type FarmNodeKind = "dir" | "file" | "symlink";
15
+ /** The subset of stat information the farm builder and the fact builder actually read. */
16
+ export interface FarmStat {
17
+ readonly kind: FarmNodeKind;
18
+ readonly mtimeMs: number;
19
+ readonly sizeBytes: number;
20
+ }
21
+ /**
22
+ * Every filesystem operation the farm resync performs, injected so the whole of `src/launcher/farm.ts` — symlink creation, materialisation, adoption copies, and the atomic swap itself — runs against an in-memory fake in tests and never against a real `~/.claude` or `~/.claude-use`.
23
+ *
24
+ * `lstat` never follows symlinks: a `~/.claude` that contains a directory symlink escaping the tree (a skills directory linked out to a separate dotfiles repository, say) must be recorded as one symlink entry rather than recursed into, both to keep the fact manifest finite and because the farm links such an entry at `<claudeHome>/<rel>` and lets the OS resolve the remaining hops.
25
+ */
26
+ export interface FarmFs {
27
+ /** Stats one path without following symlinks, or returns undefined when nothing is there. */
28
+ readonly lstat: (path: string) => FarmStat | undefined;
29
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. */
30
+ readonly readdir: (path: string) => readonly string[];
31
+ /** Creates a directory and any missing parents, succeeding silently when it already exists. */
32
+ readonly mkdirp: (path: string) => void;
33
+ /** Creates a symbolic link at `linkPath` pointing at `target`. */
34
+ readonly symlink: (target: string, linkPath: string) => void;
35
+ /** Renames `from` to `to`. Both are always within one directory tree on one filesystem in this module's own usage, so this is the atomic primitive the farm swap is built on. */
36
+ readonly rename: (from: string, to: string) => void;
37
+ /** Removes a path and everything beneath it, succeeding silently when it does not exist. */
38
+ readonly removeRecursive: (path: string) => void;
39
+ /** Copies a file, symlink, or whole directory tree from `from` to `to`. */
40
+ readonly copyRecursive: (from: string, to: string) => void;
41
+ /** Reads a file's contents as UTF-8 text, or undefined when it does not exist. */
42
+ readonly readFileUtf8: (path: string) => string | undefined;
43
+ /** Writes a file's full contents as UTF-8 text, creating or truncating it. */
44
+ readonly writeFileUtf8: (path: string, contents: string) => void;
45
+ /** Creates a directory (and any missing parents) readable only by its owner: mode 0700, enforced on the leaf even when it already existed with a wider mode. For a directory holding capabilities another local account must never read. */
46
+ readonly mkdirPrivate: (path: string) => void;
47
+ /** Writes a file's full contents as UTF-8 text readable only by its owner (mode 0600), atomically: a temporary sibling is written at that mode and renamed into place, so no reader ever sees a partial file or a wider mode. */
48
+ readonly writeFilePrivate: (path: string, contents: string) => void;
49
+ /** Creates a file only if it does not already exist, returning false when it does. This is the exclusive-create the identity lock depends on for its mutual exclusion — a read-then-write pair would race. */
50
+ readonly writeFileExclusive: (path: string, contents: string) => boolean;
51
+ /** A stable content hash of one file, or undefined when it is missing or is not a regular file. Used only to recognise farm data that has already been adopted into the canonical tree. */
52
+ readonly hashFile: (path: string) => string | undefined;
53
+ }
54
+ /** The outcome of one `spawnSync` call, mirroring Node's own `SpawnSyncReturns` shape narrowly to what the launcher actually reads. */
55
+ export interface SpawnResult {
56
+ /** The child's exit code, or null when it was terminated by a signal instead of exiting normally. */
57
+ readonly status: number | null;
58
+ /** The signal that terminated the child, or null when it exited normally. */
59
+ readonly signal: NodeJS.Signals | null;
60
+ /** Set when the child process could not even be spawned (e.g. the binary does not exist). */
61
+ readonly error?: Error;
62
+ }
63
+ /** Runs the real `claude` binary synchronously, injected so no test ever spawns a real process. The Node equivalent of the legacy bash tool's `exec` is `spawnSync` with `stdio: "inherit"` followed by propagating the child's own exit code — see `src/launcher/spawn.ts`. */
64
+ export interface SpawnPort {
65
+ readonly spawnSync: (command: string, args: readonly string[], options: {
66
+ readonly stdio: "inherit";
67
+ readonly env: Readonly<Record<string, string | undefined>>;
68
+ }) => SpawnResult;
69
+ }
70
+ /** The outcome of one synchronous external-command run (distinct from `SpawnPort`, which is specifically for replacing this process with the real `claude` binary — `RunPort` is for auxiliary commands like `git rev-parse` for branch detection, or `security find-generic-password` for the macOS Keychain diagnostic in `check.ts`, whose output this process needs to keep running and read). */
71
+ interface RunResult {
72
+ readonly status: number | null;
73
+ readonly stdout: string;
74
+ readonly stderr: string;
75
+ }
76
+ /** Runs an external command and captures its output, injected so no test ever shells out for real. Not used by this phase's own modules yet — declared here so every later module (branch-conditioned `when` evaluation, `check.ts`'s Keychain lookup) shares one injectable shape rather than each inventing its own. */
77
+ export interface RunPort {
78
+ readonly run: (command: string, args: readonly string[]) => RunResult;
79
+ }
80
+ /**
81
+ * The running process itself: its environment, its logical argv, and the ability to terminate with an exit code.
82
+ *
83
+ * `argv` is the *logical* CLI arguments only — whatever the user typed after `claude`/`claude-use`, already stripped of the Node executable path and the script path (`process.argv.slice(2)` in the real implementation). `exit` mirrors `process.exit`'s own `never` return type: a real process never returns from it, and a test fake enforces the same shape by throwing a marker so control flow unwinds the same way in both cases.
84
+ */
85
+ export interface ProcPort {
86
+ readonly env: Readonly<Record<string, string | undefined>>;
87
+ readonly argv: readonly string[];
88
+ readonly exit: (code: number) => never;
89
+ }
90
+ /** Where the launcher reports what it decided and why — refusals, warnings about an identity that could not be loaded, and (per the "strictly-better than the legacy script" goal) which identity/profile ended up selected. */
91
+ export interface LogPort {
92
+ readonly info: (message: string) => void;
93
+ readonly warn: (message: string) => void;
94
+ readonly error: (message: string) => void;
95
+ }
96
+ /** The running headroom daemon one launch routed through, as `HeadroomPort.ensure` reports it. The front door owns the session's routing; this names the daemon the door's headroom hop forwards to, and the project identity headroom scopes memory to. */
97
+ export interface HeadroomUp {
98
+ /** The loopback port the headroom daemon listens on: what the door's headroom hop reads live and `HEADROOM_PROXY_URL` names for anything else that wants the daemon. */
99
+ readonly port: number;
100
+ /** The project identity headroom scopes memory state to: the git repo root of the launch directory, or the directory itself outside a repository. */
101
+ readonly projectId: string;
102
+ }
103
+ /**
104
+ * Everything the launcher needs from the headroom daemon, injected so its daemon-supervision side effects stay behind a port and the launcher's own logic stays pure over it (`src/realPorts.ts` wires the real one; tests wire fakes).
105
+ *
106
+ * `ensure` brings the daemon up (spawning the detached supervisor when nothing healthy is running) and registers this launch in its session registry; `release` removes that registration when the spawned `claude` exits.
107
+ */
108
+ export interface HeadroomPort {
109
+ readonly ensure: () => HeadroomUp;
110
+ readonly release: () => void;
111
+ }
112
+ /**
113
+ * Everything the launcher needs from the front-door daemon, injected like `HeadroomPort`: `ensure` brings the door up (spawning its detached supervisor when nothing authenticated is serving), registers this launch in its session registry and returns both listeners' loopback ports plus the CA bundle the child must trust; `release` removes that registration when the spawned `claude` exits.
114
+ */
115
+ export interface FrontDoorPort {
116
+ /** `inheritedExtraCaCerts` is the parent environment's own `NODE_EXTRA_CA_CERTS`, which the returned trust bundle keeps (see `resolveTrustBundle`). */
117
+ readonly ensure: (inheritedExtraCaCerts: string | undefined) => FrontDoorUp;
118
+ readonly release: () => void;
119
+ }
120
+ /** The running front door one launch routed through, as `FrontDoorPort.ensure` reports it. */
121
+ export interface FrontDoorUp {
122
+ /** The loopback port the provider listener serves HTTPS on: the base URL every provider session points at. */
123
+ readonly port: number;
124
+ /** The loopback port the CONNECT surface listens on: what an OAuth launch points HTTPS_PROXY at. */
125
+ readonly connectPort: number;
126
+ /** The CA bundle every routed child points NODE_EXTRA_CA_CERTS at: claude-use's CA (which signs both the provider listener's leaf and the CONNECT surface's), plus whatever the parent environment already trusted that way. */
127
+ readonly trustBundlePath: string;
128
+ /** Why the trust bundle dropped something the parent environment named, when it did; the launcher logs it. */
129
+ readonly trustWarning?: string;
130
+ /** This launch's capability token, injected alongside the session headers and required by the door before it routes anything. */
131
+ readonly sessionToken: string;
132
+ }
133
+ export {};
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The resolved set of paths claude-use reads and writes under its own root.
3
+ *
4
+ * `root` is always CLAUDE_USE_HOME when that environment variable is set (used by every test in this project, and by any real installation that wants to relocate its state), falling back to `~/.claude-use` only when the variable is unset. Every other field is derived from `root` so there is exactly one place a path can go wrong.
5
+ */
6
+ export interface LayoutPaths {
7
+ /** The resolved root directory — CLAUDE_USE_HOME, or ~/.claude-use when unset. */
8
+ readonly root: string;
9
+ /** Directory holding one subdirectory per identity (symlink farm + local credentials/daemon state). */
10
+ readonly identitiesDir: string;
11
+ /** Directory holding named, reusable configuration profile JSON files. */
12
+ readonly configProfilesDir: string;
13
+ /** Directory holding one `<name>.json` provider definition per API provider. */
14
+ readonly providersDir: string;
15
+ /** Path to the directory-rules.json file describing directory-scoped identity/profile pins. */
16
+ readonly directoryRulesFile: string;
17
+ /** Path to the persisted active-identity file (the identity `claude-use identity use` selected). */
18
+ readonly activeIdentityFile: string;
19
+ /** Path to the global config.json (user-global override layer, and default profile/walk-limit settings). */
20
+ readonly globalConfigFile: string;
21
+ /** Path to the categories.local.json overlay recording answers to "unclassified entry" prompts. */
22
+ readonly categoriesLocalFile: string;
23
+ /** Path to the claude-shim.json marker recording where `claude-use shim enable` last placed a `claude`-named copy of this executable, and how. */
24
+ readonly claudeShimFile: string;
25
+ /** Directory holding the headroom daemon's coordination state: state.json, the start lock, and the session registry. */
26
+ readonly headroomDir: string;
27
+ /** Path to the headroom supervisor's state.json: pids, port, version, allowlist hash, last error. */
28
+ readonly headroomStateFile: string;
29
+ /** Path to the exclusive-create marker guarding "who spawns the supervisor" so concurrent launches start at most one. */
30
+ readonly headroomLockFile: string;
31
+ /** Directory holding one `<launcher-pid>.json` session-registry entry per live launch routed through headroom. */
32
+ readonly headroomSessionsDir: string;
33
+ /** Directory holding daemon logs (the headroom proxy's stdout/stderr and the supervisor's own output). */
34
+ readonly logsDir: string;
35
+ /** Path to the headroom daemon's combined log, appended to by both the supervisor and the proxy it owns. */
36
+ readonly headroomLogPath: string;
37
+ /** Directory holding the front-door daemon's coordination state: state.json, the start lock, and the session registry. */
38
+ readonly frontdoorDir: string;
39
+ /** Directory holding the front door's certificate authority, which signs both the provider listener's loopback leaf and the CONNECT surface's intercept leaf: ca.pem (public, handed to children through NODE_EXTRA_CA_CERTS) and ca.key (mode 0600). */
40
+ readonly frontdoorCaDir: string;
41
+ /** Path to the front door's CA certificate, generated once and stable across restarts so children keep trusting it. */
42
+ readonly frontdoorCaCertFile: string;
43
+ /** Path to the front door's CA private key, mode 0600: whoever can read it can impersonate the door to every routed child. */
44
+ readonly frontdoorCaKeyFile: string;
45
+ /** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS file plus the front door's CA, one file per distinct combination, for children that must keep trusting both. */
46
+ readonly frontdoorCaBundlesDir: string;
47
+ /** Path to the front door's state.json: its supervisor pid, its port, and the sticky port it last served on. */
48
+ readonly frontdoorStateFile: string;
49
+ /** Path to the exclusive-create marker guarding "who spawns the front-door supervisor" so concurrent launches start at most one. */
50
+ readonly frontdoorLockFile: string;
51
+ /** Directory holding one `<launcher-pid>.json` session-registry entry per live launch routed through the front door. */
52
+ readonly frontdoorSessionsDir: string;
53
+ /** Path to the front door's log: one line per lifecycle event and routed failure, never a token. */
54
+ readonly frontdoorLogPath: string;
55
+ }
56
+ /**
57
+ * Resolves the current CLAUDE_USE_HOME root: the environment variable when set to a non-empty string, otherwise `~/.claude-use`. An empty string counts as unset, consistent with how this project treats empty-string environment variables elsewhere (see the ambient-credential guard).
58
+ */
59
+ export declare function resolveClaudeUseHome(): string;
60
+ /**
61
+ * Resolves the canonical `~/.claude` directory every farm symlink points back into: `CLAUDE_USE_CLAUDE_HOME` when set to a non-empty string, otherwise `~/.claude`.
62
+ *
63
+ * The override exists for the same reason `CLAUDE_USE_HOME` does. Exercising a real resync end to end means building and swapping real directories, and doing that against a real, in-use `~/.claude` to find out whether the code is correct is not an acceptable way to find out. Pointing both variables at throwaway directories makes a full end-to-end run safe.
64
+ */
65
+ export declare function resolveClaudeHome(): string;
66
+ /** Builds the full LayoutPaths structure from a given root directory. */
67
+ export declare function buildLayoutPaths(root: string): LayoutPaths;
68
+ /** Resolves CLAUDE_USE_HOME and builds the full LayoutPaths structure in one call. */
69
+ export declare function resolveLayoutPaths(): LayoutPaths;
@@ -0,0 +1,37 @@
1
+ import { type WhenCondition } from "../config/schema";
2
+ import type { EntryFact } from "./types";
3
+ /** Parses a duration literal like `90d` or `500ms` into milliseconds. Throws on anything the schema's own regex would already have rejected, so a malformed value can never be silently treated as zero. */
4
+ export declare function parseDuration(value: string): number;
5
+ /** True when the value would satisfy the schema's duration regex. */
6
+ export declare function isDuration(value: string): boolean;
7
+ /** Everything a `when` condition can be evaluated against, injected rather than read. */
8
+ export interface ConditionContext {
9
+ readonly nowMs: number;
10
+ /** Facts about the specific entry being decided. Absent when evaluating a rule-level condition that has no single entry (a directory rule's own `when`). */
11
+ readonly fact?: EntryFact;
12
+ /** The branch checked out at `cwd`, or undefined when `cwd` is not in a repository. */
13
+ readonly branch?: string;
14
+ /** True when the repository is in detached-HEAD state, in which case no `branch` condition can match. */
15
+ readonly branchDetached?: boolean;
16
+ readonly env: Readonly<Record<string, string | undefined>>;
17
+ }
18
+ /** The result of evaluating one `when` object. */
19
+ export interface WhenEvaluation {
20
+ readonly passed: boolean;
21
+ /** Which condition fields were present and evaluated. */
22
+ readonly checked: readonly string[];
23
+ /** Which of those fields did not hold. Empty when `passed` is true. */
24
+ readonly failed: readonly string[];
25
+ }
26
+ /** True when `branch` matches `pattern`. The pattern is glob-capable (`client/*`); a detached HEAD or a non-repository directory never matches. */
27
+ export declare function matchBranch(pattern: string, branch: string | undefined, detached?: boolean): boolean;
28
+ /**
29
+ * Evaluates a `when` object. Every present field must hold — conditions AND together within one object.
30
+ *
31
+ * An absent condition is vacuously true, so `when: {}` passes; `claude-use check` warns about that rather than erroring, since an empty object is more likely a half-finished edit than an intentional statement.
32
+ *
33
+ * `newerThan`, `olderThan`, and `maxSizeBytes` read the subtree-aggregated facts (`latestMtimeMs`, `totalSizeBytes`), never the entry's own inode stat: a directory's own mtime does not change when a file three levels beneath it is rewritten, and its own size is a ~4KB inode figure that says nothing about what it contains.
34
+ */
35
+ export declare function evaluateWhen(when: WhenCondition | undefined, context: ConditionContext): WhenEvaluation;
36
+ /** True when the `when` object is present but empty, which is vacuously true and therefore has no effect. */
37
+ export declare function isVacuousWhen(when: WhenCondition | undefined): boolean;
@@ -0,0 +1,45 @@
1
+ import { type CategoryName } from "../config/schema";
2
+ import type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade } from "./types";
3
+ /** Everything phase two needs, all of it injected. */
4
+ export interface DecideParams {
5
+ readonly flattened: FlattenedCascade;
6
+ readonly facts: EntryFacts;
7
+ /** Every top-level `~/.claude` entry name mapped to its category, or to null when nothing recognises it. */
8
+ readonly classification: ReadonlyMap<string, CategoryName | null>;
9
+ }
10
+ /** A rule chosen by `selectRule`, alongside the candidates eliminated on the way to it. */
11
+ export interface RuleSelection {
12
+ readonly rule?: CompiledRule;
13
+ readonly eliminated: readonly EliminatedRule[];
14
+ }
15
+ /**
16
+ * Picks the winning entries rule for one path.
17
+ *
18
+ * Candidates are ranked by `compareSpecificity` and taken in order. A candidate whose `when` condition **fails is eliminated** and resolution continues to the next-most-specific matching rule — it never falls straight through to the category default, and a failed condition never inverts the rule's boolean value, which would not have a coherent meaning anyway. Only when no candidate survives does the caller fall back to the category.
19
+ */
20
+ export declare function selectRule(relPath: string, flattened: FlattenedCascade, context: {
21
+ nowMs: number;
22
+ branch?: string;
23
+ branchDetached?: boolean;
24
+ env: Readonly<Record<string, string | undefined>>;
25
+ fact?: EntryFact;
26
+ }): RuleSelection;
27
+ /**
28
+ * Resolves one path to a sharing decision, in exactly this order:
29
+ *
30
+ * 1. **The secret floor.** If the path's top-level entry classifies as `secret`, the answer is `false` unconditionally. This check sits *above* the entries lookup, not merely at the least-specific end of the cascade, and it fires on the path's **real classification**, never on a lexical reading of the key that matched it — so a glob written under a `runtime/` prefix that happens to reach `.credentials.json` is neutralised exactly like a deliberate `secret/` key would be.
31
+ * 2. **Unclassified.** A top-level entry nothing recognises is not shared, and is reported rather than silently guessed at.
32
+ * 3. **Entries rules**, most specific first, with a failing condition eliminating a candidate rather than ending the search.
33
+ * 4. **The category**: a layer's own toggle if any layer set one, otherwise the shipped default.
34
+ */
35
+ export declare function resolveEntry(relPath: string, params: DecideParams): {
36
+ decision: Decision;
37
+ diagnostics: Diagnostic[];
38
+ };
39
+ /** Every decision the resolver reaches, plus every diagnostic raised along the way (phase-one diagnostics included, and de-duplicated). */
40
+ export interface ResolveAllResult {
41
+ readonly decisions: ReadonlyMap<string, Decision>;
42
+ readonly diagnostics: readonly Diagnostic[];
43
+ }
44
+ /** Resolves every entry in the fact manifest. */
45
+ export declare function resolveAll(params: DecideParams): ResolveAllResult;
@@ -0,0 +1,41 @@
1
+ import type { ConfigProfile } from "../config/schema";
2
+ import type { Diagnostic, Layer, LayerId } from "./types";
3
+ /** One configuration profile as loaded from disk, with its entries key order captured at load time. */
4
+ export interface ProfileSource {
5
+ readonly name: string;
6
+ readonly profile: ConfigProfile;
7
+ readonly entryOrder?: readonly string[];
8
+ /** Where it was loaded from, for diagnostics. Defaults to the profile name. */
9
+ readonly filepath?: string;
10
+ }
11
+ /** Loads a profile by name. Injected, so nothing in the resolver reads a file. Returns undefined when no profile of that name exists. */
12
+ export type ProfileLoader = (name: string) => ProfileSource | undefined;
13
+ /** The linearised `extends` chain plus anything that went wrong resolving it. */
14
+ export interface LinearisedProfiles {
15
+ /** Profile names, base-first: every profile appears exactly once, after everything it extends. */
16
+ readonly order: readonly string[];
17
+ readonly diagnostics: readonly Diagnostic[];
18
+ }
19
+ /**
20
+ * Linearises a profile's `extends` graph into a base-first order.
21
+ *
22
+ * The walker keeps **two separate structures**, and the distinction is load-bearing rather than stylistic:
23
+ *
24
+ * - `stack` is the current depth-first path. A name already on it means the graph genuinely loops back on itself, which is the only real cycle.
25
+ * - `visited` is every name already emitted anywhere in the walk. A name already in it is a diamond — two branches sharing a base — and must simply be skipped, not re-emitted.
26
+ *
27
+ * Collapsing the two into one shared set produces a silent wrong answer in either direction. Using `stack` alone re-emits a shared base a second time in a diamond, so the base's values get re-applied on top of an intermediate profile that deliberately overrode them — the override silently disappears. Using `visited` alone false-flags every legitimate diamond as a cycle. Neither failure crashes; both just resolve to the wrong configuration.
28
+ *
29
+ * Emission is **post-order**: a profile is appended only after everything it extends, so `c extends [a, b]` where both extend `base` linearises to `[base, a, b, c]` — never `[base, a, base, b, c]`.
30
+ */
31
+ export declare function lineariseProfile(rootName: string, load: ProfileLoader): LinearisedProfiles;
32
+ /**
33
+ * Turns a profile's `extends` chain into cascade layers, base-first, starting at `startId`.
34
+ *
35
+ * A resolved profile is not a separate mechanism from the outer cascade: its chain flattens through the same ordered-layer sequence everything else uses, so a profile's resolved patch is simply one more run of inputs to the same algorithm.
36
+ */
37
+ export declare function profileLayers(rootName: string, load: ProfileLoader, startId: LayerId): {
38
+ layers: Layer[];
39
+ nextId: LayerId;
40
+ diagnostics: readonly Diagnostic[];
41
+ };