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.
- package/README.md +3 -0
- package/dist/cli.cjs +3382 -2595
- package/dist/index.cjs +3061 -0
- package/dist/index.mjs +2991 -0
- package/dist/types/cliError.d.ts +27 -0
- package/dist/types/config/classify.d.ts +49 -0
- package/dist/types/config/load.d.ts +42 -0
- package/dist/types/config/schema.d.ts +689 -0
- package/dist/types/config/store.d.ts +46 -0
- package/dist/types/frontdoor/capability.d.ts +19 -0
- package/dist/types/frontdoor/connect.d.ts +179 -0
- package/dist/types/frontdoor/ensure.d.ts +34 -0
- package/dist/types/frontdoor/probe.d.ts +13 -0
- package/dist/types/frontdoor/state.d.ts +50 -0
- package/dist/types/frontdoor/supervisor.d.ts +48 -0
- package/dist/types/headroom/ensure.d.ts +31 -0
- package/dist/types/headroom/state.d.ts +71 -0
- package/dist/types/headroom/supervisor.d.ts +102 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/launcher/farm.d.ts +165 -0
- package/dist/types/launcher/guard.d.ts +57 -0
- package/dist/types/launcher/lock.d.ts +41 -0
- package/dist/types/launcher/ports.d.ts +133 -0
- package/dist/types/paths.d.ts +69 -0
- package/dist/types/resolve/conditions.d.ts +37 -0
- package/dist/types/resolve/decide.d.ts +45 -0
- package/dist/types/resolve/extends.d.ts +41 -0
- package/dist/types/resolve/flatten.d.ts +13 -0
- package/dist/types/resolve/match.d.ts +58 -0
- package/dist/types/resolve/pipeline.d.ts +30 -0
- package/dist/types/resolve/plan.d.ts +57 -0
- package/dist/types/resolve/projects.d.ts +61 -0
- package/dist/types/resolve/reconcile.d.ts +80 -0
- package/dist/types/resolve/types.d.ts +159 -0
- package/dist/types/resolve/walk.d.ts +85 -0
- package/package.json +16 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompiledRule, FlattenedCascade, Layer } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Cascade phase one: walk the ordered layer sequence once, spreading each layer's `categories` and `entries` over an accumulator. A later layer's value for the exact same category name, or the exact same key, replaces an earlier layer's value for that identical key. No path-specificity reasoning happens here — that is phase two's job.
|
|
4
|
+
*
|
|
5
|
+
* The entries accumulator is keyed by the **canonical** pattern, not the raw written key, so two differently-spelled but equivalent keys (`history/projects/~/work/x` and `history/projects/$HOME/work/x` after expansion) collapse into one rule here instead of racing as two separate candidates in phase two. Every surviving rule carries its own `{ layer, ordinal }`, which is what lets phase two tell a same-layer conflict from a cross-layer one without a separate provenance side-table.
|
|
6
|
+
*
|
|
7
|
+
* A key written under the `secret/` prefix is rejected outright with its own diagnostic and contributes no rule at all: `secret` is the one category no layer may open, and a deliberate attempt to name a secret path deserves a clearer error than the silent neutralisation the resolve-time floor check applies to a glob that reaches one incidentally.
|
|
8
|
+
*/
|
|
9
|
+
export declare function flattenLayers(layers: readonly Layer[], options: Readonly<{
|
|
10
|
+
home: string;
|
|
11
|
+
}>): FlattenedCascade;
|
|
12
|
+
/** Every compiled rule that matches `relPath`, ranked most-specific first. */
|
|
13
|
+
export declare function matchingRules(flattened: FlattenedCascade, relPath: string): CompiledRule[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type CategoryName } from "../config/schema";
|
|
2
|
+
import { CliError } from "../cliError";
|
|
3
|
+
import type { CompiledRule } from "./types";
|
|
4
|
+
/** True when `pattern` contains no glob metacharacter, so it can only ever match one literal path. */
|
|
5
|
+
export declare function isExactPattern(pattern: string): boolean;
|
|
6
|
+
/** The pattern's leading run of characters before the first glob metacharacter — the part that is definitely literal. */
|
|
7
|
+
export declare function literalPrefixOf(pattern: string): string;
|
|
8
|
+
/** Normalises a relative path fragment: collapses repeated slashes, drops `./` segments, and strips a trailing slash. Deliberately case-preserving and case-sensitive. */
|
|
9
|
+
export declare function normaliseRelative(fragment: string): string;
|
|
10
|
+
/** The outcome of canonicalising one entries key. */
|
|
11
|
+
export interface CanonicalKey {
|
|
12
|
+
readonly declaredCategory: CategoryName;
|
|
13
|
+
/** The pattern with its category prefix stripped, relative to `~/.claude`, with any `history/projects/` path fragment already encoded. */
|
|
14
|
+
readonly canonicalPattern: string;
|
|
15
|
+
/** The `history/projects/` path fragment as written, before encoding — retained so ambiguity reporting can name what the author actually typed. */
|
|
16
|
+
readonly projectFragment?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Raised when an entries key cannot be canonicalised. Carries the offending key so the diagnostic can name it. */
|
|
19
|
+
export declare class EntryKeyError extends CliError {
|
|
20
|
+
readonly key: string;
|
|
21
|
+
readonly reason: "unrooted-project-path" | "malformed";
|
|
22
|
+
constructor(key: string, message: string, reason: "unrooted-project-path" | "malformed");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Canonicalises one entries key into a pattern over paths relative to `~/.claude`.
|
|
26
|
+
*
|
|
27
|
+
* Every key is `<category>/<real-relative-path>`, so the category prefix is stripped and the remainder kept as written — with one deliberately narrow exception. Anything written after the literal `history/projects/` prefix is a real absolute working directory (optionally globbed), not a literal child directory name, because that directory's only real children are Claude Code's own encoded names and there is nothing else meaningful to reference there. Those fragments get `~`-expanded and forward-encoded; every other key in the whole design is a plain literal path or an ordinary glob over one, matched exactly as written.
|
|
28
|
+
*/
|
|
29
|
+
export declare function canonicaliseEntryKey(key: string, options: Readonly<{
|
|
30
|
+
home: string;
|
|
31
|
+
}>): CanonicalKey;
|
|
32
|
+
/**
|
|
33
|
+
* Compiles a canonical pattern into a matcher that covers the pattern itself *and* everything beneath it, so a rule written as `knowledge/skills` decides `skills/commit/SKILL.md` too. Two picomatch matchers are needed rather than one: `skills/**` alone covers the subtree but `projects/*` does not match `projects/x/y`, and `projects/*` alone does not cover the files inside a matched project directory.
|
|
34
|
+
*
|
|
35
|
+
* Matching is deliberately case-sensitive (`nocase: false`) whatever the host filesystem does, so the same config behaves identically on a case-insensitive APFS volume and a case-sensitive one. `dot: true` so a glob can match a leading-dot entry.
|
|
36
|
+
*/
|
|
37
|
+
export declare function compileMatcher(canonicalPattern: string): (relPath: string) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* True when some path *underneath* `dir` could match `canonicalPattern`, whether or not such a path exists today.
|
|
40
|
+
*
|
|
41
|
+
* This is what makes a conditional rule force materialisation: a directory Claude Code will write new children into cannot be symlinked wholesale if a rule might have something to say about those not-yet-existing children. Answering that from the pattern's structure rather than from the current file listing is the whole point — asking "does any existing child match" would pass the obvious test and fail the real one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function patternCouldReachUnder(canonicalPattern: string, dir: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Ranks two compiled rules that both match the same path. A positive result means `a` wins.
|
|
46
|
+
*
|
|
47
|
+
* The order, exactly:
|
|
48
|
+
*
|
|
49
|
+
* 1. **Different layers — the later layer wins, period.** This rank sits deliberately *above* exactness. Ranking exactness first would let an untrusted committed `.claude-use.json`'s exact key beat your own later, personal glob override, breaking the design's stated trust property that a directory-scoped local rule can only ever tighten what a committed file opened, never the reverse. Nothing in the design asserts that an earlier exact key beats a later glob, so nothing is lost by closing that hole.
|
|
50
|
+
* 2. Same layer: an exact literal beats a glob.
|
|
51
|
+
* 3. Same layer: the longer literal prefix wins.
|
|
52
|
+
* 4. Same layer: more path segments wins, which separates a one-wildcard pattern from a two-wildcard one at the same depth.
|
|
53
|
+
* 5. Same layer: the later ordinal — source order within the file — wins.
|
|
54
|
+
* 6. Canonical pattern, compared as a string, as a last-resort determinism guarantee.
|
|
55
|
+
*
|
|
56
|
+
* The order must be **total**: returning 0 for two distinct rules would make the winner depend on iteration order, which tests would only catch intermittently.
|
|
57
|
+
*/
|
|
58
|
+
export declare function compareSpecificity(a: CompiledRule, b: CompiledRule): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ClassifyResult } from "../config/classify";
|
|
2
|
+
import type { CategoryClassification, CategoryClassificationOverlay } from "../config/schema";
|
|
3
|
+
import { type ResolveAllResult } from "./decide";
|
|
4
|
+
import { type FarmPlan } from "./plan";
|
|
5
|
+
import { type AssembledCascade, type CascadeInput } from "./walk";
|
|
6
|
+
import type { Diagnostic, EntryFacts, FlattenedCascade } from "./types";
|
|
7
|
+
/** Everything one launch needs resolved, in one call. */
|
|
8
|
+
export interface ResolveDecisionsInput {
|
|
9
|
+
readonly facts: EntryFacts;
|
|
10
|
+
readonly cascade: CascadeInput;
|
|
11
|
+
readonly classification: {
|
|
12
|
+
readonly defaults: CategoryClassification;
|
|
13
|
+
readonly overlay?: CategoryClassificationOverlay;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The fully resolved state of one launch: the layers that composed it, every entry's decision, the farm layout, and everything worth reporting. */
|
|
17
|
+
export interface ResolvedState {
|
|
18
|
+
readonly assembled: AssembledCascade;
|
|
19
|
+
readonly flattened: FlattenedCascade;
|
|
20
|
+
readonly classification: ClassifyResult;
|
|
21
|
+
readonly decisions: ResolveAllResult["decisions"];
|
|
22
|
+
readonly farm: FarmPlan;
|
|
23
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
24
|
+
}
|
|
25
|
+
/** The top-level `~/.claude` entry names present in a fact manifest, in sorted order. */
|
|
26
|
+
export declare function topLevelNames(facts: EntryFacts): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Runs the whole pipeline for one launch: assemble the ordered layer sequence, flatten it, classify the real `~/.claude` entries, decide every path, and plan the resulting farm.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveDecisions(input: ResolveDecisionsInput): ResolvedState;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Decision, EntryFacts, FlattenedCascade } from "./types";
|
|
2
|
+
/** What the farm should contain at one path. */
|
|
3
|
+
type FarmPlanEntry =
|
|
4
|
+
/** A single symlink standing in for this whole path — always absolute, always under `claudeHome`. */
|
|
5
|
+
{
|
|
6
|
+
readonly kind: "link";
|
|
7
|
+
readonly rel: string;
|
|
8
|
+
readonly target: string;
|
|
9
|
+
}
|
|
10
|
+
/** A real local directory the farm builds itself, because the decision beneath it is not uniform or because a conditional rule could touch it. */
|
|
11
|
+
| {
|
|
12
|
+
readonly kind: "materialise";
|
|
13
|
+
readonly rel: string;
|
|
14
|
+
readonly reason: MaterialiseReason;
|
|
15
|
+
}
|
|
16
|
+
/** Nothing at all in the farm for this path. */
|
|
17
|
+
| {
|
|
18
|
+
readonly kind: "omit";
|
|
19
|
+
readonly rel: string;
|
|
20
|
+
};
|
|
21
|
+
/** Why a directory has to be built rather than symlinked. */
|
|
22
|
+
type MaterialiseReason =
|
|
23
|
+
/** Descendants of this directory resolve to different decisions, so a single symlink cannot express the outcome. */
|
|
24
|
+
"split-decision"
|
|
25
|
+
/** A conditional entries rule could match something under this directory, so the decision has to stay live. */
|
|
26
|
+
| "conditional-rule";
|
|
27
|
+
/** The full farm plan for one resolved cascade. */
|
|
28
|
+
export interface FarmPlan {
|
|
29
|
+
/** Every planned path, parents before children. */
|
|
30
|
+
readonly entries: readonly FarmPlanEntry[];
|
|
31
|
+
/** Directories the farm builds itself, in plan order. Written into the farm manifest so the next resync can tell them apart from directories Claude Code created. */
|
|
32
|
+
readonly materialised: readonly string[];
|
|
33
|
+
readonly links: readonly {
|
|
34
|
+
readonly rel: string;
|
|
35
|
+
readonly target: string;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
/** Everything `planFarm` needs, all of it already resolved. */
|
|
39
|
+
export interface PlanFarmParams {
|
|
40
|
+
readonly facts: EntryFacts;
|
|
41
|
+
readonly decisions: ReadonlyMap<string, Decision>;
|
|
42
|
+
readonly flattened: FlattenedCascade;
|
|
43
|
+
}
|
|
44
|
+
/** Indexes the fact manifest by parent path, so the plan can descend the tree without touching a filesystem. */
|
|
45
|
+
export declare function buildChildIndex(facts: EntryFacts): Map<string, string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Turns a resolved set of decisions into a concrete farm layout.
|
|
48
|
+
*
|
|
49
|
+
* A directory becomes one symlink when the decision is uniform across its whole subtree, which is what keeps the common resync cheap — a few dozen top-level symlinks rather than a rebuilt tree. It is materialised, and recursed into, when either:
|
|
50
|
+
*
|
|
51
|
+
* - its subtree's decisions genuinely disagree, so no single symlink can express the outcome; or
|
|
52
|
+
* - **any** conditional entries rule could reach something under it, even when today's evaluation happens to be uniform. This second case is the one a naive "is the decision uniform right now" check gets wrong: a symlinked directory freezes the decision for every file Claude Code creates *after* the resync, so a `newerThan` condition meant to share recent sessions would never see a session written five minutes from now. The bug only surfaces for files that do not exist yet, which is precisely why the check has to be structural rather than observational.
|
|
53
|
+
*
|
|
54
|
+
* Every symlink target is `<claudeHome>/<rel>`, unconditionally. That matters for the entries inside a materialised directory that are themselves relative symlinks escaping `~/.claude` (a real condition, not a hypothetical one — a skills directory linked out to a separate dotfiles repository, for instance). Copying such a link's own relative target verbatim would resolve to a different place from the farm's own depth, and copying its resolved absolute target would pin the farm to wherever that link pointed at resync time. Linking at `<claudeHome>/<rel>` and letting the OS follow the remaining hops from there is the only form that stays correct.
|
|
55
|
+
*/
|
|
56
|
+
export declare function planFarm(params: PlanFarmParams): FarmPlan;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward-only encoding of a real working directory into the single directory name Claude Code gives it under `~/.claude/projects/`.
|
|
3
|
+
*
|
|
4
|
+
* The encoding collapses the *entire* non-alphanumeric character class to `-`, not just the path separator: `.`, `_`, ` `, `@`, and `/` all become `-`, while letters and digits pass through with their case preserved. This was confirmed against a real installation's project directories, not assumed from the one `/`-becomes-`-` sample the README quotes.
|
|
5
|
+
*
|
|
6
|
+
* The encoding is therefore many-to-one: `~/work/clients/acme`, `~/work/clients-acme`, and `~/work-clients/acme` all produce the identical name. That makes it impossible to invert, so this module never tries: there is no decode function here, and `projects.test.ts` asserts the module's export list to keep it that way. Only the forward direction — real path to encoded name — is ever computed, and `detectEncodingAmbiguity` reports when a pattern's encoded form could plausibly correspond to more than one real path rather than resolving it silently.
|
|
7
|
+
*/
|
|
8
|
+
import { CliError } from "../cliError";
|
|
9
|
+
/** Encodes one real, fully-literal path into the directory name Claude Code would give it under `~/.claude/projects/`. */
|
|
10
|
+
export declare function encodeProjectPath(realPath: string): string;
|
|
11
|
+
/** One piece of a pattern split on its wildcard tokens: either a literal run to encode, or a wildcard to leave alone. */
|
|
12
|
+
export interface PatternFragment {
|
|
13
|
+
readonly kind: "literal" | "wildcard";
|
|
14
|
+
readonly text: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Splits a pattern into alternating literal runs and wildcard tokens. Encoding the whole pattern string in one pass would turn `*` into `-` along with everything else, silently converting a glob into a literal that matches nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function splitOnWildcards(pattern: string): PatternFragment[];
|
|
20
|
+
/** Raised when a `history/projects/` pattern's path fragment is neither `~`-rooted nor absolute, so there is no real path to encode. */
|
|
21
|
+
export declare class UnrootedProjectPathError extends CliError {
|
|
22
|
+
readonly fragment: string;
|
|
23
|
+
constructor(fragment: string);
|
|
24
|
+
}
|
|
25
|
+
/** Expands a leading `~` to the given home directory. A `~` anywhere else in the path is an ordinary character. */
|
|
26
|
+
export declare function expandHome(fragment: string, home: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Encodes a possibly-globbed real path into a pattern that matches Claude Code's own `~/.claude/projects/` directory names.
|
|
29
|
+
*
|
|
30
|
+
* Order matters and is fixed: expand `~` to the real home first (encoding `~` would turn it into `-` and lose the reference), reject anything not home-or-root-rooted, then split on wildcard tokens and encode only the literal runs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function encodeProjectPattern(fragment: string, options: Readonly<{
|
|
33
|
+
home: string;
|
|
34
|
+
}>): string;
|
|
35
|
+
/** Why a pattern's encoded form might not identify the real path its author had in mind. */
|
|
36
|
+
export type EncodingAmbiguityReason =
|
|
37
|
+
/** The literal portion contains a character other than `/` that encodes to `-`, so a sibling path differing only in that character encodes identically. */
|
|
38
|
+
"lossy-characters"
|
|
39
|
+
/** Two different patterns in the same configuration encode to the identical form. */
|
|
40
|
+
| "collides-with-sibling-pattern";
|
|
41
|
+
/** One reported ambiguity in a `history/projects/` pattern. */
|
|
42
|
+
export interface EncodingAmbiguity {
|
|
43
|
+
readonly fragment: string;
|
|
44
|
+
readonly encoded: string;
|
|
45
|
+
readonly reason: EncodingAmbiguityReason;
|
|
46
|
+
readonly detail: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reports, for a set of `history/projects/` path fragments, every case where the encoded form could plausibly correspond to more than one real path.
|
|
50
|
+
*
|
|
51
|
+
* Two signals, both actionable:
|
|
52
|
+
*
|
|
53
|
+
* - `lossy-characters` — the fragment's literal portion contains a character besides `/` that encodes to `-`. Because encoding is many-to-one, a sibling path with that character replaced by a separator (or vice versa) produces the identical name, so this pattern may match a directory belonging to a different real path.
|
|
54
|
+
* - `collides-with-sibling-pattern` — two distinct fragments in the same configuration encode identically, which proves a collision outright rather than merely suggesting one.
|
|
55
|
+
*
|
|
56
|
+
* When `existingNames` is supplied, `detail` also names how many real project directories the encoded form currently matches, so the report says what actually happened rather than only what could.
|
|
57
|
+
*/
|
|
58
|
+
export declare function detectEncodingAmbiguity(fragments: readonly string[], options: {
|
|
59
|
+
home: string;
|
|
60
|
+
existingNames?: readonly string[];
|
|
61
|
+
}): EncodingAmbiguity[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Diagnostic } from "./types";
|
|
2
|
+
/** The manifest the farm builder writes into each scratch tree before the atomic swap. */
|
|
3
|
+
export interface FarmManifest {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly builtAtMs: number;
|
|
6
|
+
readonly identity: string;
|
|
7
|
+
readonly configProfile?: string;
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly claudeHome: string;
|
|
10
|
+
/** Directories the previous resync built itself. Without this list, a directory the farm materialised and a directory Claude Code created are indistinguishable on disk — both are real, both contain real children. */
|
|
11
|
+
readonly materialised: readonly string[];
|
|
12
|
+
readonly links: readonly {
|
|
13
|
+
readonly rel: string;
|
|
14
|
+
readonly target: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/** One node of a directory listing, relative to the tree's own root. */
|
|
18
|
+
export interface ListingEntry {
|
|
19
|
+
/** Path relative to the tree root, forward-slash separated. */
|
|
20
|
+
readonly rel: string;
|
|
21
|
+
readonly kind: "dir" | "file" | "symlink";
|
|
22
|
+
/** A content hash for a file, used to recognise data that has already been adopted. Absent for directories and symlinks. */
|
|
23
|
+
readonly contentHash?: string;
|
|
24
|
+
}
|
|
25
|
+
/** What to do about one path found in the old farm. */
|
|
26
|
+
export type ReconcileAction =
|
|
27
|
+
/** New data Claude Code wrote that the canonical tree does not have. Copy it to `to`. */
|
|
28
|
+
{
|
|
29
|
+
readonly kind: "adopt";
|
|
30
|
+
readonly rel: string;
|
|
31
|
+
readonly reason: "absent-from-canonical";
|
|
32
|
+
}
|
|
33
|
+
/** New data whose canonical counterpart exists but differs. The canonical copy is authoritative; the farm copy is preserved alongside it for the user to reconcile. */
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: "adopt-conflict";
|
|
36
|
+
readonly rel: string;
|
|
37
|
+
}
|
|
38
|
+
/** Nothing to do, with the reason recorded so `claude-use check` can explain a no-op. */
|
|
39
|
+
| {
|
|
40
|
+
readonly kind: "skip";
|
|
41
|
+
readonly rel: string;
|
|
42
|
+
readonly reason: SkipReason;
|
|
43
|
+
};
|
|
44
|
+
/** Why a path in the old farm needs no action. */
|
|
45
|
+
type SkipReason =
|
|
46
|
+
/** A symlink the previous resync placed. It is a view of the canonical tree, so there is nothing to write back. */
|
|
47
|
+
"is-symlink"
|
|
48
|
+
/** A directory the previous resync materialised. Not new data itself; its children are examined individually. */
|
|
49
|
+
| "tracked-materialised-dir"
|
|
50
|
+
/** Real data whose canonical counterpart already holds identical content — already adopted, possibly by an earlier run that then crashed before the swap. */
|
|
51
|
+
| "identical-content";
|
|
52
|
+
/** The result of planning a reconciliation pass. */
|
|
53
|
+
export interface ReconciliationPlan {
|
|
54
|
+
readonly actions: readonly ReconcileAction[];
|
|
55
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
56
|
+
/** True when the manifest was missing or unusable and every real directory in the farm was treated as materialised. */
|
|
57
|
+
readonly conservative: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Everything the reconciliation planner reads. It performs no I/O of its own — the launcher executes the actions it returns. */
|
|
60
|
+
export interface ReconcileParams {
|
|
61
|
+
/** The previous resync's manifest, or undefined when it is missing or failed to parse. */
|
|
62
|
+
readonly manifest?: FarmManifest;
|
|
63
|
+
/** A flat listing of the old, still-live farm. */
|
|
64
|
+
readonly farmListing: readonly ListingEntry[];
|
|
65
|
+
/** A flat listing of the canonical `~/.claude`. */
|
|
66
|
+
readonly canonicalListing: readonly ListingEntry[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Plans the write-through reconciliation pass that runs before each resync builds its new scratch tree.
|
|
70
|
+
*
|
|
71
|
+
* A materialised directory stops being a live view of `~/.claude` and becomes a locally-built directory of symlinks frozen at resync time. Anything Claude Code writes into it afterwards — a brand-new project subdirectory, a new session file inside an existing one — lands as a real, untracked child: invisible to every other identity, and liable to be read as stale scaffolding and pruned by a later resync. This pass finds that data and copies it back into the canonical `~/.claude`, where it belongs, before anything else happens.
|
|
72
|
+
*
|
|
73
|
+
* The manifest is what makes this possible at all. A directory the farm materialised and a directory Claude Code created look identical on disk, so without the previous resync's own record of what it built, there is no way to tell the two apart. When the manifest is missing or unreadable, the pass falls back to **conservative mode** — every real directory in the farm is treated as materialised, so every real child anywhere becomes an adopt candidate — and reports it. That is deliberately over-eager rather than a silent no-op: over-adopting copies data into the place it was always meant to live, while under-adopting loses it.
|
|
74
|
+
*
|
|
75
|
+
* The pass is deliberately independent of the *new* resolved state. A directory whose new decision is `omit`, or one collapsing back from materialised to a plain symlink, still gets reconciled first — those are precisely the cases where the data would otherwise be discarded, which is the whole reason this exists.
|
|
76
|
+
*
|
|
77
|
+
* It is also idempotent: re-running against the same old farm after a crash mid-resync produces only `skip("identical-content")` for anything the interrupted run already copied across.
|
|
78
|
+
*/
|
|
79
|
+
export declare function planReconciliation(params: ReconcileParams): ReconciliationPlan;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { CategoryMap, CategoryName, Entries, LaunchFlags, OverridableCategory, WhenCondition } from "../config/schema";
|
|
2
|
+
/** A layer's index in the assembled cascade. Strictly ascending in composition order: a higher id was composed later and therefore wins on the comparator's first rank. */
|
|
3
|
+
export type LayerId = number;
|
|
4
|
+
/** Where a cascade layer came from. Purely descriptive — precedence is carried by `LayerId`, never by kind. */
|
|
5
|
+
type LayerKind = "global-config" | "config-profile" | "directory-rule" | "portable" | "portable-local" | "cli-override";
|
|
6
|
+
/** One composable layer of the cascade: its own category toggles and entries overrides, plus the entries key order captured at load time. */
|
|
7
|
+
export interface Layer {
|
|
8
|
+
readonly id: LayerId;
|
|
9
|
+
readonly kind: LayerKind;
|
|
10
|
+
/** Human-readable origin (a file path, or a profile name) used by `claude-use check` to explain a decision. */
|
|
11
|
+
readonly source: string;
|
|
12
|
+
readonly categories?: CategoryMap;
|
|
13
|
+
readonly entries?: Entries;
|
|
14
|
+
/** The entries object's own key insertion order, from `src/config/load.ts`. Falls back to `Object.keys(entries)` when absent. */
|
|
15
|
+
readonly entryOrder?: readonly string[];
|
|
16
|
+
readonly launch?: LaunchFlags;
|
|
17
|
+
}
|
|
18
|
+
/** Facts about one entry under `~/.claude`, relative to `~/.claude` itself. */
|
|
19
|
+
export interface EntryFact {
|
|
20
|
+
/** Path relative to `~/.claude`, forward-slash separated, never leading or trailing slash. */
|
|
21
|
+
readonly relPath: string;
|
|
22
|
+
readonly isDirectory: boolean;
|
|
23
|
+
/** True when the entry is itself a symlink (including the relative, tree-escaping symlinks that already exist inside a real `~/.claude`). */
|
|
24
|
+
readonly isSymlink: boolean;
|
|
25
|
+
/** The entry's own mtime. */
|
|
26
|
+
readonly mtimeMs: number;
|
|
27
|
+
/** The most recent mtime anywhere in this entry's subtree — its own mtime for a plain file. A directory-scoped `newerThan` must read this, never the directory's own inode mtime, which does not change when a file three levels down is rewritten. */
|
|
28
|
+
readonly latestMtimeMs: number;
|
|
29
|
+
/** The entry's own size. */
|
|
30
|
+
readonly sizeBytes: number;
|
|
31
|
+
/** The recursive total size of this entry's subtree — its own size for a plain file. A directory-scoped `maxSizeBytes` must read this, never the directory's own ~4KB inode size. */
|
|
32
|
+
readonly totalSizeBytes: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every fact the resolver needs, injected rather than read. Nothing in `src/resolve/` ever touches a real filesystem, git repository, clock, or environment: something else builds this structure and hands it over, which is what makes the whole resolver unit-testable with fake mtimes, a fake branch, and a fake environment.
|
|
36
|
+
*/
|
|
37
|
+
export interface EntryFacts {
|
|
38
|
+
readonly nowMs: number;
|
|
39
|
+
/** The real home directory, used to expand `~` in `history/projects/` patterns. */
|
|
40
|
+
readonly home: string;
|
|
41
|
+
/** The canonical `~/.claude` directory every relative path in `entries` is relative to. */
|
|
42
|
+
readonly claudeHome: string;
|
|
43
|
+
/** The working directory the cascade was resolved for. */
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
/** The git branch checked out at `cwd`, or undefined when `cwd` is not in a repository. */
|
|
46
|
+
readonly branch?: string;
|
|
47
|
+
/** True when `cwd`'s repository is in detached-HEAD state, in which case no `branch` condition can match. */
|
|
48
|
+
readonly branchDetached?: boolean;
|
|
49
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
50
|
+
/** Every entry under `~/.claude`, keyed by its path relative to `~/.claude`. Directories appear alongside their descendants. */
|
|
51
|
+
readonly entries: ReadonlyMap<string, EntryFact>;
|
|
52
|
+
}
|
|
53
|
+
/** A single entries override, compiled into the form the resolver actually matches against. */
|
|
54
|
+
export interface CompiledRule {
|
|
55
|
+
/** The key exactly as written in its source file, for diagnostics. */
|
|
56
|
+
readonly rawKey: string;
|
|
57
|
+
/** The category prefix the key declared. Cross-checked against the real classification of every path it matches. */
|
|
58
|
+
readonly declaredCategory: CategoryName;
|
|
59
|
+
/** The key with its category prefix stripped and (under `history/projects/`) its path fragment encoded — a pattern over paths relative to `~/.claude`. Two differently-spelled but equivalent keys share one canonical pattern and therefore collapse to one rule. */
|
|
60
|
+
readonly canonicalPattern: string;
|
|
61
|
+
readonly value: boolean;
|
|
62
|
+
readonly when?: WhenCondition;
|
|
63
|
+
readonly layer: LayerId;
|
|
64
|
+
/** The key's position within its own layer's entries object, used only by the comparator's final same-layer tie-break. */
|
|
65
|
+
readonly ordinal: number;
|
|
66
|
+
/** True when the canonical pattern contains no glob metacharacter. */
|
|
67
|
+
readonly isExact: boolean;
|
|
68
|
+
/** The pattern's leading run of literal (non-wildcard) characters. */
|
|
69
|
+
readonly literalPrefix: string;
|
|
70
|
+
/** How many `/`-separated segments the canonical pattern has. */
|
|
71
|
+
readonly segmentCount: number;
|
|
72
|
+
/** Matches the pattern itself and everything beneath it, so a rule on `skills` covers `skills/commit/SKILL.md`. */
|
|
73
|
+
readonly matches: (relPath: string) => boolean;
|
|
74
|
+
}
|
|
75
|
+
/** How a decision was reached, for `claude-use check`'s "which layer decided this" output. */
|
|
76
|
+
type DecisionVia =
|
|
77
|
+
/** The unconditional pre-cascade floor: the path's real classification is `secret`. */
|
|
78
|
+
"secret-floor"
|
|
79
|
+
/** Nothing in the classification map recognises the path's top-level entry. */
|
|
80
|
+
| "unclassified"
|
|
81
|
+
/** An entries rule matched and its condition (if any) held. */
|
|
82
|
+
| "entry-rule"
|
|
83
|
+
/** No entries rule survived; a layer's category toggle decided it. */
|
|
84
|
+
| "category-override"
|
|
85
|
+
/** No entries rule and no category toggle; the shipped default for the category decided it. */
|
|
86
|
+
| "category-default";
|
|
87
|
+
/** One entry's resolved sharing decision plus the reasoning behind it. */
|
|
88
|
+
export interface Decision {
|
|
89
|
+
readonly relPath: string;
|
|
90
|
+
readonly shared: boolean;
|
|
91
|
+
readonly via: DecisionVia;
|
|
92
|
+
readonly category: CategoryName | null;
|
|
93
|
+
/** The winning entries rule, when `via` is "entry-rule". */
|
|
94
|
+
readonly rule?: CompiledRule;
|
|
95
|
+
/** Rules that matched but whose `when` condition failed, in descending specificity — each was eliminated and resolution continued to the next candidate. */
|
|
96
|
+
readonly eliminated?: readonly EliminatedRule[];
|
|
97
|
+
}
|
|
98
|
+
/** A candidate rule that matched the path but was eliminated because its `when` condition did not hold. */
|
|
99
|
+
export interface EliminatedRule {
|
|
100
|
+
readonly rule: CompiledRule;
|
|
101
|
+
readonly failed: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
/** Every diagnostic the resolver can raise. Codes are stable strings so `claude-use check` and tests can assert on them. */
|
|
104
|
+
type DiagnosticCode =
|
|
105
|
+
/** A profile's `extends` graph contains a cycle. */
|
|
106
|
+
"EXTENDS_CYCLE"
|
|
107
|
+
/** An `extends` entry names a profile that does not exist. */
|
|
108
|
+
| "MISSING_PROFILE"
|
|
109
|
+
/** An entries key was written under the `secret/` prefix. Rejected at compile time — `secret` can never be overridden by any layer. */
|
|
110
|
+
| "SECRET_ENTRY_KEY"
|
|
111
|
+
/** An entries key under some other prefix matched a path whose real classification is `secret`. Neutralised at resolve time by the floor check. */
|
|
112
|
+
| "SECRET_PATH_NEUTRALISED"
|
|
113
|
+
/** An entries key's declared category prefix disagrees with the real classification of a path it matched. */
|
|
114
|
+
| "CATEGORY_PREFIX_MISMATCH"
|
|
115
|
+
/** An entries key is not of the form `<category>/<path>`. The schema rejects these at parse time; this fires only for a layer built in code. */
|
|
116
|
+
| "MALFORMED_ENTRY_KEY"
|
|
117
|
+
/** An earlier layer's exact key lost to a later layer's glob. Correct per the comparator, but worth surfacing rather than resolving silently. */
|
|
118
|
+
| "EXACT_ENTRY_OVERRIDDEN_BY_LATER_GLOB"
|
|
119
|
+
/** A `history/projects/` pattern's encoded form could plausibly correspond to more than one real path. */
|
|
120
|
+
| "AMBIGUOUS_PROJECT_ENCODING"
|
|
121
|
+
/** A `history/projects/` key's path fragment is neither home-rooted nor absolute, so it cannot be encoded. */
|
|
122
|
+
| "UNROOTED_PROJECT_PATH"
|
|
123
|
+
/** A `~/.claude` entry nothing in the classification map recognises. */
|
|
124
|
+
| "UNCLASSIFIED_ENTRY"
|
|
125
|
+
/** An empty `when: {}` object, which is vacuously true and therefore has no effect. */
|
|
126
|
+
| "EMPTY_WHEN"
|
|
127
|
+
/** The previous farm's manifest is missing or unreadable, so reconciliation ran in conservative mode. */
|
|
128
|
+
| "FARM_MANIFEST_MISSING"
|
|
129
|
+
/** A file Claude Code wrote into a materialised farm directory differs from the canonical copy of the same path. */
|
|
130
|
+
| "RECONCILE_CONFLICT"
|
|
131
|
+
/** Reconciliation proposed adopting a path whose real classification is `secret`. Refused: the secret floor applies to data moving into `~/.claude` exactly as it applies to data moving out. */
|
|
132
|
+
| "RECONCILE_SECRET_BLOCKED"
|
|
133
|
+
/** A previous launch was interrupted mid-swap and this launch completed or undid the half-finished work. */
|
|
134
|
+
| "FARM_SWAP_RECOVERED"
|
|
135
|
+
/** A superseded farm could not be discarded because it still held data the new farm has its own entry for, so it was left on disk for the user to look at. */
|
|
136
|
+
| "FARM_PREVIOUS_RETAINED";
|
|
137
|
+
/** Severity of a diagnostic. An `error` means a configuration layer asked for something the tool refused to do. */
|
|
138
|
+
type DiagnosticSeverity = "error" | "warning" | "info";
|
|
139
|
+
/** One structured diagnostic. */
|
|
140
|
+
export interface Diagnostic {
|
|
141
|
+
readonly code: DiagnosticCode;
|
|
142
|
+
readonly severity: DiagnosticSeverity;
|
|
143
|
+
readonly message: string;
|
|
144
|
+
/** The config key, profile name, or entry path the diagnostic concerns. */
|
|
145
|
+
readonly subject?: string;
|
|
146
|
+
/** The layer that raised it, when the diagnostic is attributable to one. */
|
|
147
|
+
readonly layer?: LayerId;
|
|
148
|
+
}
|
|
149
|
+
/** The flattened result of cascade phase one. */
|
|
150
|
+
export interface FlattenedCascade {
|
|
151
|
+
/** The last value each overridable category was set to across every layer. Categories no layer touched are absent. */
|
|
152
|
+
readonly categories: ReadonlyMap<OverridableCategory, boolean>;
|
|
153
|
+
/** One compiled rule per canonical pattern — later layers having already overwritten earlier ones for identical patterns. */
|
|
154
|
+
readonly rules: ReadonlyMap<string, CompiledRule>;
|
|
155
|
+
/** Launch flags, last layer wins per field. */
|
|
156
|
+
readonly launch: LaunchFlags;
|
|
157
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
158
|
+
}
|
|
159
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DirectoryRule, GlobalConfig, PortableConfig } from "../config/schema";
|
|
2
|
+
import { type ProfileLoader } from "./extends";
|
|
3
|
+
import type { Diagnostic, Layer } from "./types";
|
|
4
|
+
/** Injected predicate answering whether a directory can be read, so the walk stays pure. */
|
|
5
|
+
export type ReadablePredicate = (dir: string) => boolean;
|
|
6
|
+
/** Options for the upward directory walk. */
|
|
7
|
+
export interface WalkOptions {
|
|
8
|
+
/** The user's home directory, the walk's default stopping point. */
|
|
9
|
+
readonly home: string;
|
|
10
|
+
/** Where to stop walking upward, inclusive. Defaults to `home`, and is what `walkUpLimit` in the global config sets. */
|
|
11
|
+
readonly limit?: string;
|
|
12
|
+
/** Answers whether a directory is readable. A directory that is not stops the walk there rather than failing the launch. */
|
|
13
|
+
readonly isReadable?: ReadablePredicate;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Collects `cwd` and every ancestor of it, shallowest-first, the way Claude Code itself resolves nested `CLAUDE.md` files.
|
|
17
|
+
*
|
|
18
|
+
* The walk stops at (and includes) the limit directory — `$HOME` by default — or at the filesystem root when `cwd` is not beneath the limit at all. An ancestor that cannot be read stops the walk there rather than failing the launch: a directory you cannot read cannot carry a config file you were meant to obey.
|
|
19
|
+
*
|
|
20
|
+
* `cwd` is expected to already be a real path; resolving symlinks is the caller's job, so this stays free of filesystem access.
|
|
21
|
+
*/
|
|
22
|
+
export declare function walkDirectoryAncestors(cwd: string, options: WalkOptions): string[];
|
|
23
|
+
/** The up-to-three configuration sources that can apply at one directory level. */
|
|
24
|
+
export interface DirectoryLevelSources {
|
|
25
|
+
readonly dir: string;
|
|
26
|
+
/** The committed, team-shared `.claude-use.json` at this level. */
|
|
27
|
+
readonly portable?: {
|
|
28
|
+
readonly config: PortableConfig;
|
|
29
|
+
readonly entryOrder?: readonly string[];
|
|
30
|
+
readonly filepath: string;
|
|
31
|
+
};
|
|
32
|
+
/** Every rule in this user's own `~/.claude-use/directory-rules.json` whose path is exactly this level, in file order. */
|
|
33
|
+
readonly rules?: readonly {
|
|
34
|
+
readonly rule: DirectoryRule;
|
|
35
|
+
readonly entryOrder?: readonly string[];
|
|
36
|
+
readonly filepath: string;
|
|
37
|
+
}[];
|
|
38
|
+
/** The gitignored `.claude-use.local.json` at this level. */
|
|
39
|
+
readonly portableLocal?: {
|
|
40
|
+
readonly config: PortableConfig;
|
|
41
|
+
readonly entryOrder?: readonly string[];
|
|
42
|
+
readonly filepath: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Everything needed to assemble the full ordered layer sequence, all of it already loaded by the caller. */
|
|
46
|
+
export interface CascadeInput {
|
|
47
|
+
readonly home: string;
|
|
48
|
+
/** The user-global `~/.claude-use/config.json`, the least-specific layer above the shipped defaults. */
|
|
49
|
+
readonly globalConfig?: {
|
|
50
|
+
readonly config: GlobalConfig;
|
|
51
|
+
readonly entryOrder?: readonly string[];
|
|
52
|
+
readonly filepath: string;
|
|
53
|
+
};
|
|
54
|
+
/** The configuration profile chosen by the caller's own precedence rules: explicit flag, then the active identity's default, then the global default. */
|
|
55
|
+
readonly baseConfigProfile?: string;
|
|
56
|
+
readonly loadProfile: ProfileLoader;
|
|
57
|
+
/** Directory levels, shallowest-first, as produced by `walkDirectoryAncestors`. */
|
|
58
|
+
readonly levels?: readonly DirectoryLevelSources[];
|
|
59
|
+
/** One-off overrides from the command line or environment, composed last so they beat everything. */
|
|
60
|
+
readonly cliOverride?: {
|
|
61
|
+
readonly categories?: Layer["categories"];
|
|
62
|
+
readonly entries?: Layer["entries"];
|
|
63
|
+
readonly entryOrder?: readonly string[];
|
|
64
|
+
readonly launch?: Layer["launch"];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The assembled cascade: an ordered layer sequence plus what the walk itself decided. */
|
|
68
|
+
export interface AssembledCascade {
|
|
69
|
+
readonly layers: readonly Layer[];
|
|
70
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
71
|
+
/** The deepest directory-level `identity` pin encountered, which the launcher uses as a safety net beneath an explicit `@name`. */
|
|
72
|
+
readonly identityPin?: string;
|
|
73
|
+
/** The deepest directory-level `configProfile` selection encountered, which is what `claude-use configure` treats as the active profile for a path. */
|
|
74
|
+
readonly directoryConfigProfile?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Assembles the full ordered layer sequence.
|
|
78
|
+
*
|
|
79
|
+
* Order is: the user-global config, then the base configuration profile's own `extends` chain, then every directory level shallowest-to-deepest, then one-off command-line overrides.
|
|
80
|
+
*
|
|
81
|
+
* Within one directory level, up to three sources fold most-personal-last: the committed `.claude-use.json` (team-shared), then this user's own `directory-rules.json` entries for that exact path, then `.claude-use.local.json` (this clone, this user, never committed). That three-source fold happens once per level, and the whole shallowest-to-deepest walk is one continuous sequence through those folded levels — a deeper level's three-source result composes on top of a shallower level's, never gathered per-source across the whole tree first.
|
|
82
|
+
*
|
|
83
|
+
* A level source that names a `configProfile` composes that profile's whole chain in *before* its own inline overrides, so the source reads as "everything that profile says, plus what is additionally true this far down the tree" rather than swapping the base profile out wholesale.
|
|
84
|
+
*/
|
|
85
|
+
export declare function assembleCascade(input: CascadeInput): AssembledCascade;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-use",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"claude-use": "dist/cli.cjs"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"dist/cli.cjs"
|
|
27
|
+
"dist/cli.cjs",
|
|
28
|
+
"dist/index.mjs",
|
|
29
|
+
"dist/index.cjs",
|
|
30
|
+
"dist/types"
|
|
28
31
|
],
|
|
29
32
|
"scripts": {
|
|
30
33
|
"prepare": "husky && node scripts/build.mts --bundle-only",
|
|
@@ -77,5 +80,16 @@
|
|
|
77
80
|
"typescript": "^6.0.3",
|
|
78
81
|
"typescript-eslint": "^8.70.0",
|
|
79
82
|
"vitest": "^4.1.11"
|
|
83
|
+
},
|
|
84
|
+
"main": "dist/index.cjs",
|
|
85
|
+
"module": "dist/index.mjs",
|
|
86
|
+
"types": "dist/types/index.d.ts",
|
|
87
|
+
"exports": {
|
|
88
|
+
".": {
|
|
89
|
+
"types": "./dist/types/index.d.ts",
|
|
90
|
+
"import": "./dist/index.mjs",
|
|
91
|
+
"require": "./dist/index.cjs"
|
|
92
|
+
},
|
|
93
|
+
"./package.json": "./package.json"
|
|
80
94
|
}
|
|
81
95
|
}
|