@dsh-cc/settings-cascade 0.5.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/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +57 -0
- package/README.zh.md +56 -0
- package/lib/auto-mode.d.ts +47 -0
- package/lib/auto-mode.d.ts.map +1 -0
- package/lib/auto-mode.js +46 -0
- package/lib/auto-mode.js.map +1 -0
- package/lib/cc-key-aliases.d.ts +22 -0
- package/lib/cc-key-aliases.d.ts.map +1 -0
- package/lib/cc-key-aliases.js +43 -0
- package/lib/cc-key-aliases.js.map +1 -0
- package/lib/env.d.ts +38 -0
- package/lib/env.d.ts.map +1 -0
- package/lib/env.js +69 -0
- package/lib/env.js.map +1 -0
- package/lib/index.d.ts +190 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +414 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +23 -0
- package/lib/invariant.js.map +1 -0
- package/lib/local-root.d.ts +59 -0
- package/lib/local-root.d.ts.map +1 -0
- package/lib/local-root.js +140 -0
- package/lib/local-root.js.map +1 -0
- package/lib/merge.d.ts +53 -0
- package/lib/merge.d.ts.map +1 -0
- package/lib/merge.js +126 -0
- package/lib/merge.js.map +1 -0
- package/lib/permissions.d.ts +37 -0
- package/lib/permissions.d.ts.map +1 -0
- package/lib/permissions.js +26 -0
- package/lib/permissions.js.map +1 -0
- package/lib/persist.d.ts +64 -0
- package/lib/persist.d.ts.map +1 -0
- package/lib/persist.js +183 -0
- package/lib/persist.js.map +1 -0
- package/package.json +55 -0
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@dsh-cc/settings-cascade`.
|
|
3
|
+
* @module @dsh-cc/settings-cascade/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@dsh-cc/settings-cascade';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'settings-cascade-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: this provider's contracts are the cross-file merge
|
|
12
|
+
* and policy first-source-wins — composition logic proven by package tests;
|
|
13
|
+
* the in-process commit relation is owned by `@deepseek-ai/dsh-settings`.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => { };
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
/* jscpd:ignore-end */
|
|
23
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant.js","sourceRoot":"","sources":["../src/invariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,YAAY,GAAG,0BAA0B,CAAA;AAE/C,oCAAoC;AACpC,MAAM,CAAC,MAAM,IAAI,GAAG,4BAA4B,CAAA;AAChD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,CAAC,CAAA;AAEpC;;;;GAIG;AACH,MAAM,OAAO,GAAuB,GAAG,EAAE,GAAE,CAAC,CAAA;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,GAAY,EAAuB,EAAE,CACzD,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AACjE,sBAAsB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-settings directory resolution: Claude Code reads
|
|
3
|
+
* `.claude/settings.local.json` from the git **main checkout root** when the
|
|
4
|
+
* session starts inside a linked worktree, or from the git toplevel when it
|
|
5
|
+
* starts in a subdirectory. This module computes that directory without
|
|
6
|
+
* touching the session cwd or git working tree. Git probes and `stat` are read-only.
|
|
7
|
+
*
|
|
8
|
+
* The linked-worktree detector and canonicalisation mirror the TUI project
|
|
9
|
+
* identity code in `packages/ui/tui/src/project.ts` (`isLinkedWorktree`,
|
|
10
|
+
* `canonical`) — see the comment there pointing back here. Keep the two in
|
|
11
|
+
* sync; they must not drift.
|
|
12
|
+
*
|
|
13
|
+
* Node builtins only: this file is imported through the
|
|
14
|
+
* `@dsh-cc/settings-cascade/local-root` subpath by packages that must
|
|
15
|
+
* not pull in cordis or the settings runtime.
|
|
16
|
+
*
|
|
17
|
+
* @module @dsh-cc/settings-cascade/local-root
|
|
18
|
+
*/
|
|
19
|
+
/** A successful synchronous git invocation. */
|
|
20
|
+
export interface LocalRootExecResult {
|
|
21
|
+
stdout: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run one git argv in `cwd`, synchronously. Returns undefined on spawn
|
|
25
|
+
* failure, non-zero exit, or timeout. Injectable so tests script the git
|
|
26
|
+
* conversation; the default spawns real `git`.
|
|
27
|
+
*/
|
|
28
|
+
export type LocalRootExec = (argv: readonly string[], cwd: string) => LocalRootExecResult | undefined;
|
|
29
|
+
/** Injectable environment for {@link resolveLocalSettingsDir} (tests only). */
|
|
30
|
+
export interface LocalRootDeps {
|
|
31
|
+
/** Git probe; defaults to a bounded `spawnSync('git', …)`. */
|
|
32
|
+
exec?: LocalRootExec;
|
|
33
|
+
/** Home directory; defaults to `os.homedir()`. */
|
|
34
|
+
homedir?: string;
|
|
35
|
+
/** Platform override; defaults to `process.platform`. */
|
|
36
|
+
platform?: NodeJS.Platform;
|
|
37
|
+
/** Current uid; when undefined the uid comparison is skipped. */
|
|
38
|
+
getuid?: () => number | undefined;
|
|
39
|
+
/** stat-like probe returning the owner uid; may throw (fail-closed). */
|
|
40
|
+
stat?: (path: string) => {
|
|
41
|
+
uid: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Test hook: drop the module-level memo. */
|
|
45
|
+
export declare function __clearLocalRootCache(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the directory that holds `.claude/settings.local.json` for a
|
|
48
|
+
* launch directory. Hoists to the git main checkout root (linked worktree)
|
|
49
|
+
* or the git toplevel (subdirectory start) unless a safety fallback applies:
|
|
50
|
+
* not a git repo, Windows, repo root equals `$HOME`, a bare-main hoist
|
|
51
|
+
* target without `.git`, or ownership of the repo root / `.git` / `.claude`
|
|
52
|
+
* cannot be confirmed as the current user (fail-closed). Paths *inside* the
|
|
53
|
+
* file still resolve against the launch directory.
|
|
54
|
+
*
|
|
55
|
+
* Results are memoised per resolved `cwd` for the default exec only;
|
|
56
|
+
* injected execs bypass the cache entirely (see `__clearLocalRootCache`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveLocalSettingsDir(cwd: string, deps?: LocalRootDeps): string;
|
|
59
|
+
//# sourceMappingURL=local-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-root.d.ts","sourceRoot":"","sources":["../src/local-root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,mBAAmB,GAAG,SAAS,CAAA;AAErG,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;IAC1B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IACjC,wEAAwE;IACxE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACzC;AAyCD,6CAA6C;AAC7C,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CASrF"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-settings directory resolution: Claude Code reads
|
|
3
|
+
* `.claude/settings.local.json` from the git **main checkout root** when the
|
|
4
|
+
* session starts inside a linked worktree, or from the git toplevel when it
|
|
5
|
+
* starts in a subdirectory. This module computes that directory without
|
|
6
|
+
* touching the session cwd or git working tree. Git probes and `stat` are read-only.
|
|
7
|
+
*
|
|
8
|
+
* The linked-worktree detector and canonicalisation mirror the TUI project
|
|
9
|
+
* identity code in `packages/ui/tui/src/project.ts` (`isLinkedWorktree`,
|
|
10
|
+
* `canonical`) — see the comment there pointing back here. Keep the two in
|
|
11
|
+
* sync; they must not drift.
|
|
12
|
+
*
|
|
13
|
+
* Node builtins only: this file is imported through the
|
|
14
|
+
* `@dsh-cc/settings-cascade/local-root` subpath by packages that must
|
|
15
|
+
* not pull in cordis or the settings runtime.
|
|
16
|
+
*
|
|
17
|
+
* @module @dsh-cc/settings-cascade/local-root
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
21
|
+
import { homedir as osHomedir } from 'node:os';
|
|
22
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
23
|
+
/** Git probe timeout, mirroring the TUI `gitExecSync` bound. */
|
|
24
|
+
const GIT_PROBE_TIMEOUT_MS = 2000;
|
|
25
|
+
/** The default exec: real `git` via spawnSync with a bounded timeout. */
|
|
26
|
+
const defaultExec = (argv, cwd) => {
|
|
27
|
+
const result = spawnSync('git', [...argv], { cwd, encoding: 'utf8', timeout: GIT_PROBE_TIMEOUT_MS });
|
|
28
|
+
if (result.error !== undefined || result.status !== 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
return { stdout: result.stdout };
|
|
31
|
+
};
|
|
32
|
+
/** Canonicalise a path: resolve then dereference symlinks; resolve on failure. */
|
|
33
|
+
function canonical(path) {
|
|
34
|
+
try {
|
|
35
|
+
return realpathSync(path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return resolve(path);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True when `commonDir` marks a *linked* worktree whose main root is its
|
|
43
|
+
* parent. Mirrors `isLinkedWorktree` in `packages/ui/tui/src/project.ts` —
|
|
44
|
+
* keep the two in sync (see the module doc comment there).
|
|
45
|
+
*/
|
|
46
|
+
function isLinkedWorktree(commonDir, top) {
|
|
47
|
+
if (!commonDir.endsWith(`${sep}.git`))
|
|
48
|
+
return false;
|
|
49
|
+
return resolve(commonDir) !== resolve(join(top, '.git'));
|
|
50
|
+
}
|
|
51
|
+
/** Best-effort owner uid; undefined when stat fails (absent or unreadable). */
|
|
52
|
+
function ownerUid(path, deps) {
|
|
53
|
+
const stat = deps.stat ?? (path => statSync(path));
|
|
54
|
+
try {
|
|
55
|
+
return stat(path).uid;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Module-level memo keyed by `resolve(cwd)`, default-exec calls only. */
|
|
62
|
+
const cache = new Map();
|
|
63
|
+
/** Test hook: drop the module-level memo. */
|
|
64
|
+
export function __clearLocalRootCache() {
|
|
65
|
+
cache.clear();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the directory that holds `.claude/settings.local.json` for a
|
|
69
|
+
* launch directory. Hoists to the git main checkout root (linked worktree)
|
|
70
|
+
* or the git toplevel (subdirectory start) unless a safety fallback applies:
|
|
71
|
+
* not a git repo, Windows, repo root equals `$HOME`, a bare-main hoist
|
|
72
|
+
* target without `.git`, or ownership of the repo root / `.git` / `.claude`
|
|
73
|
+
* cannot be confirmed as the current user (fail-closed). Paths *inside* the
|
|
74
|
+
* file still resolve against the launch directory.
|
|
75
|
+
*
|
|
76
|
+
* Results are memoised per resolved `cwd` for the default exec only;
|
|
77
|
+
* injected execs bypass the cache entirely (see `__clearLocalRootCache`).
|
|
78
|
+
*/
|
|
79
|
+
export function resolveLocalSettingsDir(cwd, deps = {}) {
|
|
80
|
+
const key = resolve(cwd);
|
|
81
|
+
if (deps.exec === undefined) {
|
|
82
|
+
const memoised = cache.get(key);
|
|
83
|
+
if (memoised !== undefined)
|
|
84
|
+
return memoised;
|
|
85
|
+
}
|
|
86
|
+
const result = resolveUncached(cwd, deps);
|
|
87
|
+
if (deps.exec === undefined)
|
|
88
|
+
cache.set(key, result);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function resolveUncached(cwd, deps) {
|
|
92
|
+
// Windows paths/permissions differ; Claude Code keeps the local file local.
|
|
93
|
+
if ((deps.platform ?? process.platform) === 'win32')
|
|
94
|
+
return resolve(cwd);
|
|
95
|
+
const exec = deps.exec ?? defaultExec;
|
|
96
|
+
const topRaw = exec(['rev-parse', '--show-toplevel'], cwd)?.stdout.trim();
|
|
97
|
+
if (topRaw === undefined || topRaw.length === 0)
|
|
98
|
+
return resolve(cwd);
|
|
99
|
+
const commonRaw = exec(['rev-parse', '--git-common-dir'], cwd)?.stdout.trim();
|
|
100
|
+
if (commonRaw === undefined || commonRaw.length === 0)
|
|
101
|
+
return resolve(cwd);
|
|
102
|
+
const topAbs = canonical(topRaw);
|
|
103
|
+
const commonDir = canonical(resolve(cwd, commonRaw));
|
|
104
|
+
const hoist = isLinkedWorktree(commonDir, topAbs);
|
|
105
|
+
const mainRoot = hoist ? dirname(commonDir) : topAbs;
|
|
106
|
+
// Bare-main guard: a hoisted common dir like `/x/repo.git` would resolve
|
|
107
|
+
// the "main root" to `/x`, which is not a checkout. A missing or
|
|
108
|
+
// unstatable `<mainRoot>/.git` refuses the hoist (the required-`.git`
|
|
109
|
+
// check below also fails closed on it).
|
|
110
|
+
if (hoist && ownerUid(join(mainRoot, '.git'), deps) === undefined)
|
|
111
|
+
return resolve(cwd);
|
|
112
|
+
// Never let the local file live directly in $HOME.
|
|
113
|
+
if (canonical(deps.homedir ?? osHomedir()) === mainRoot)
|
|
114
|
+
return resolve(cwd);
|
|
115
|
+
// Tests inject `getuid`; production uses `process.getuid` (absent on
|
|
116
|
+
// win32, which already returned above). An explicit `getuid: () => undefined`
|
|
117
|
+
// skips the uid comparison while still fail-closing on a throwing stat.
|
|
118
|
+
const uid = (deps.getuid ?? (() => process.getuid?.()))();
|
|
119
|
+
// Fail-closed: mainRoot and <mainRoot>/.git must stat AND (when the uid is
|
|
120
|
+
// observable) be owned by the current user. <mainRoot>/.claude is optional
|
|
121
|
+
// — ENOENT is fine, the file may be created later — but an EACCES on a
|
|
122
|
+
// 0700 `.git` must not escape the guard.
|
|
123
|
+
const checks = [
|
|
124
|
+
[mainRoot, true],
|
|
125
|
+
[join(mainRoot, '.git'), true],
|
|
126
|
+
[join(mainRoot, '.claude'), false],
|
|
127
|
+
];
|
|
128
|
+
for (const [path, required] of checks) {
|
|
129
|
+
const owner = ownerUid(path, deps);
|
|
130
|
+
if (owner === undefined) {
|
|
131
|
+
if (required)
|
|
132
|
+
return resolve(cwd);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (uid !== undefined && owner !== uid)
|
|
136
|
+
return resolve(cwd);
|
|
137
|
+
}
|
|
138
|
+
return mainRoot;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=local-root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-root.js","sourceRoot":"","sources":["../src/local-root.ts"],"names":[],"mappings":"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"}
|
package/lib/merge.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge semantics for the settings cascade. Plain objects merge
|
|
3
|
+
* recursively; permission objects (`allow`/`deny`/`ask`) union their rule
|
|
4
|
+
* arrays with `deny` taking precedence over `allow`; every other array and
|
|
5
|
+
* scalar value from a higher layer replaces the lower layer wholesale.
|
|
6
|
+
* @module @dsh-cc/settings-cascade/merge
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Merge one permission object over a lower one. `allow`, `deny`, and `ask`
|
|
10
|
+
* union across layers, and the unioned `deny` set is removed from `allow` so a
|
|
11
|
+
* higher-layer deny always wins over a lower-layer allow. Other keys deep-merge
|
|
12
|
+
* with ordinary rules (arrays override).
|
|
13
|
+
* @param lower - the lower-priority permission object.
|
|
14
|
+
* @param higher - the higher-priority permission object.
|
|
15
|
+
* @returns the merged permission object.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mergePermissionObject(lower: Record<string, unknown>, higher: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Compute the unioned deny set and the allow set that excludes it — the
|
|
20
|
+
* `deny`-precedence rule applied to one lower and one higher permission object.
|
|
21
|
+
* @param lower - the lower-priority permission object.
|
|
22
|
+
* @param higher - the higher-priority permission object.
|
|
23
|
+
* @returns the unioned `deny` and deduped `allow` with denied rules removed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function unionDenyPrecedence(lower: {
|
|
26
|
+
allow?: string[];
|
|
27
|
+
deny?: string[];
|
|
28
|
+
}, higher: {
|
|
29
|
+
allow?: string[];
|
|
30
|
+
deny?: string[];
|
|
31
|
+
}): {
|
|
32
|
+
allow: string[];
|
|
33
|
+
deny: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Merge one JSON-compatible value over a lower one. When both are plain
|
|
37
|
+
* objects the merge recurses — through the permission rule for permission
|
|
38
|
+
* objects and per-key otherwise; any other pair lets the higher value replace
|
|
39
|
+
* the lower wholesale. Neither input is mutated.
|
|
40
|
+
* @param lower - the lower-priority value.
|
|
41
|
+
* @param higher - the higher-priority value; `undefined` keeps the lower value.
|
|
42
|
+
* @returns the merged value.
|
|
43
|
+
*/
|
|
44
|
+
export declare function mergeValue<T = unknown>(lower: T, higher: unknown): T;
|
|
45
|
+
/**
|
|
46
|
+
* Merge one whole namespace section over a lower section (recursive deep
|
|
47
|
+
* merge with the permission array rules and higher-array-override semantics).
|
|
48
|
+
* @param lower - the lower-priority raw section.
|
|
49
|
+
* @param higher - the higher-priority raw section.
|
|
50
|
+
* @returns the merged raw section.
|
|
51
|
+
*/
|
|
52
|
+
export declare function mergeSettingsSection(lower: Record<string, unknown>, higher: Record<string, unknown>): Record<string, unknown>;
|
|
53
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAiCH;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCzB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAC5C,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAKrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,GAAG,CAAC,CAapE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB"}
|
package/lib/merge.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge semantics for the settings cascade. Plain objects merge
|
|
3
|
+
* recursively; permission objects (`allow`/`deny`/`ask`) union their rule
|
|
4
|
+
* arrays with `deny` taking precedence over `allow`; every other array and
|
|
5
|
+
* scalar value from a higher layer replaces the lower layer wholesale.
|
|
6
|
+
* @module @dsh-cc/settings-cascade/merge
|
|
7
|
+
*/
|
|
8
|
+
/** The permission rule arrays that merge by union across layers. */
|
|
9
|
+
const PERMISSION_KEYS = ['allow', 'deny', 'ask'];
|
|
10
|
+
/** Whether a value is a plain data object (not an array, null, or instance). */
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
13
|
+
return false;
|
|
14
|
+
const proto = Object.getPrototypeOf(value);
|
|
15
|
+
return proto === Object.prototype || proto === null;
|
|
16
|
+
}
|
|
17
|
+
/** Whether an object holds at least one permission rule array. */
|
|
18
|
+
function isPermissionObject(value) {
|
|
19
|
+
return PERMISSION_KEYS.some(key => key in value);
|
|
20
|
+
}
|
|
21
|
+
/** Concatenate string arrays and deduplicate, preserving first-seen order. */
|
|
22
|
+
function unionStrings(...lists) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const list of lists) {
|
|
26
|
+
if (list === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
for (const entry of list) {
|
|
29
|
+
if (!seen.has(entry)) {
|
|
30
|
+
seen.add(entry);
|
|
31
|
+
out.push(entry);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Merge one permission object over a lower one. `allow`, `deny`, and `ask`
|
|
39
|
+
* union across layers, and the unioned `deny` set is removed from `allow` so a
|
|
40
|
+
* higher-layer deny always wins over a lower-layer allow. Other keys deep-merge
|
|
41
|
+
* with ordinary rules (arrays override).
|
|
42
|
+
* @param lower - the lower-priority permission object.
|
|
43
|
+
* @param higher - the higher-priority permission object.
|
|
44
|
+
* @returns the merged permission object.
|
|
45
|
+
*/
|
|
46
|
+
export function mergePermissionObject(lower, higher) {
|
|
47
|
+
const lowerAllow = lower['allow'];
|
|
48
|
+
const lowerDeny = lower['deny'];
|
|
49
|
+
const lowerAsk = lower['ask'];
|
|
50
|
+
const higherAllow = higher['allow'];
|
|
51
|
+
const higherDeny = higher['deny'];
|
|
52
|
+
const higherAsk = higher['ask'];
|
|
53
|
+
const deny = unionStrings(lowerDeny, higherDeny);
|
|
54
|
+
const denied = new Set(deny);
|
|
55
|
+
const allow = unionStrings(lowerAllow, higherAllow).filter(rule => !denied.has(rule));
|
|
56
|
+
const ask = unionStrings(lowerAsk, higherAsk);
|
|
57
|
+
// Start from both layers' non-permission keys; the derived allow/deny/ask
|
|
58
|
+
// replace the lower layer's originals below, and empty permission arrays are
|
|
59
|
+
// omitted — an empty `allow` after deny filtering means "nothing allowed",
|
|
60
|
+
// and JSON settings conventionally drop empty lists.
|
|
61
|
+
const merged = {};
|
|
62
|
+
for (const [key, value] of Object.entries(lower)) {
|
|
63
|
+
if (PERMISSION_KEYS.includes(key))
|
|
64
|
+
continue;
|
|
65
|
+
merged[key] = value;
|
|
66
|
+
}
|
|
67
|
+
for (const [key, value] of Object.entries(higher)) {
|
|
68
|
+
if (PERMISSION_KEYS.includes(key))
|
|
69
|
+
continue;
|
|
70
|
+
merged[key] = key in merged ? mergeValue(merged[key], value) : value;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...merged,
|
|
74
|
+
...(allow.length > 0 ? { allow } : {}),
|
|
75
|
+
...(deny.length > 0 ? { deny } : {}),
|
|
76
|
+
...(ask.length > 0 ? { ask } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compute the unioned deny set and the allow set that excludes it — the
|
|
81
|
+
* `deny`-precedence rule applied to one lower and one higher permission object.
|
|
82
|
+
* @param lower - the lower-priority permission object.
|
|
83
|
+
* @param higher - the higher-priority permission object.
|
|
84
|
+
* @returns the unioned `deny` and deduped `allow` with denied rules removed.
|
|
85
|
+
*/
|
|
86
|
+
export function unionDenyPrecedence(lower, higher) {
|
|
87
|
+
const deny = unionStrings(lower.deny, higher.deny);
|
|
88
|
+
const denied = new Set(deny);
|
|
89
|
+
const allow = unionStrings(lower.allow, higher.allow).filter(rule => !denied.has(rule));
|
|
90
|
+
return { allow, deny };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Merge one JSON-compatible value over a lower one. When both are plain
|
|
94
|
+
* objects the merge recurses — through the permission rule for permission
|
|
95
|
+
* objects and per-key otherwise; any other pair lets the higher value replace
|
|
96
|
+
* the lower wholesale. Neither input is mutated.
|
|
97
|
+
* @param lower - the lower-priority value.
|
|
98
|
+
* @param higher - the higher-priority value; `undefined` keeps the lower value.
|
|
99
|
+
* @returns the merged value.
|
|
100
|
+
*/
|
|
101
|
+
export function mergeValue(lower, higher) {
|
|
102
|
+
if (higher === undefined)
|
|
103
|
+
return lower;
|
|
104
|
+
if (isPlainObject(lower) && isPlainObject(higher)) {
|
|
105
|
+
if (isPermissionObject(lower) || isPermissionObject(higher)) {
|
|
106
|
+
return mergePermissionObject(lower, higher);
|
|
107
|
+
}
|
|
108
|
+
const merged = { ...lower };
|
|
109
|
+
for (const [key, value] of Object.entries(higher)) {
|
|
110
|
+
merged[key] = key in merged ? mergeValue(merged[key], value) : value;
|
|
111
|
+
}
|
|
112
|
+
return merged;
|
|
113
|
+
}
|
|
114
|
+
return higher;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Merge one whole namespace section over a lower section (recursive deep
|
|
118
|
+
* merge with the permission array rules and higher-array-override semantics).
|
|
119
|
+
* @param lower - the lower-priority raw section.
|
|
120
|
+
* @param higher - the higher-priority raw section.
|
|
121
|
+
* @returns the merged raw section.
|
|
122
|
+
*/
|
|
123
|
+
export function mergeSettingsSection(lower, higher) {
|
|
124
|
+
return mergeValue(lower, higher);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=merge.js.map
|
package/lib/merge.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-compatible `permissions` field schema, exported as a standalone
|
|
3
|
+
* value so the permission-rule engine (B2) and the settings cascade share one
|
|
4
|
+
* definition of the settings.json `permissions` shape.
|
|
5
|
+
* @module @dsh-cc/settings-cascade/permissions
|
|
6
|
+
*/
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
/** A single permission rule — a tool-scoped match string, e.g. `Bash(npm run *)`. */
|
|
9
|
+
export declare const PermissionRuleSchema: z<string>;
|
|
10
|
+
/** `defaultMode` values — the default permission mode when a tool needs access. */
|
|
11
|
+
export declare const PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "bypassPermissions", "auto"];
|
|
12
|
+
export type PermissionMode = (typeof PERMISSION_MODES)[number];
|
|
13
|
+
/** The settings.json `permissions` section, matching Claude Code's schema. */
|
|
14
|
+
export interface Permissions {
|
|
15
|
+
/** Tool operations allowed without prompting. */
|
|
16
|
+
allow?: string[];
|
|
17
|
+
/** Tool operations always blocked. */
|
|
18
|
+
deny?: string[];
|
|
19
|
+
/** Tool operations that always prompt for confirmation. */
|
|
20
|
+
ask?: string[];
|
|
21
|
+
/** Default permission mode when a tool needs access. */
|
|
22
|
+
defaultMode?: PermissionMode;
|
|
23
|
+
/** `'disable'` turns off the ability to bypass permission prompts. */
|
|
24
|
+
disableBypassPermissionsMode?: 'disable';
|
|
25
|
+
/** Additional directories included in the permission scope. */
|
|
26
|
+
additionalDirectories?: string[];
|
|
27
|
+
/** Protected file wildcard patterns — file writes to them are high risk. */
|
|
28
|
+
protectedFiles?: string[];
|
|
29
|
+
/** Raw dangerous-command regex sources for the risk classifier. */
|
|
30
|
+
dangerousPatterns?: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Schemastery schema for the `permissions` section. Unknown fields pass
|
|
34
|
+
* through unmodified, matching Claude Code's passthrough behavior.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PermissionsSchema: z<Permissions>;
|
|
37
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,EAAE,CAAC,CAAC,MAAM,CAAc,CAAA;AAEzD,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,0EAA2E,CAAA;AACxG,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,EAAE,CAAA;IACd,wDAAwD;IACxD,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,sEAAsE;IACtE,4BAA4B,CAAC,EAAE,SAAS,CAAA;IACxC,+DAA+D;IAC/D,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAA;IAChC,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,mEAAmE;IACnE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,CAAC,WAAW,CAS3C,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-compatible `permissions` field schema, exported as a standalone
|
|
3
|
+
* value so the permission-rule engine (B2) and the settings cascade share one
|
|
4
|
+
* definition of the settings.json `permissions` shape.
|
|
5
|
+
* @module @dsh-cc/settings-cascade/permissions
|
|
6
|
+
*/
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
/** A single permission rule — a tool-scoped match string, e.g. `Bash(npm run *)`. */
|
|
9
|
+
export const PermissionRuleSchema = z.string();
|
|
10
|
+
/** `defaultMode` values — the default permission mode when a tool needs access. */
|
|
11
|
+
export const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'bypassPermissions', 'auto'];
|
|
12
|
+
/**
|
|
13
|
+
* Schemastery schema for the `permissions` section. Unknown fields pass
|
|
14
|
+
* through unmodified, matching Claude Code's passthrough behavior.
|
|
15
|
+
*/
|
|
16
|
+
export const PermissionsSchema = z.object({
|
|
17
|
+
allow: z.array(z.string()),
|
|
18
|
+
deny: z.array(z.string()),
|
|
19
|
+
ask: z.array(z.string()),
|
|
20
|
+
defaultMode: z.union(PERMISSION_MODES),
|
|
21
|
+
disableBypassPermissionsMode: z.union(['disable']),
|
|
22
|
+
additionalDirectories: z.array(z.string()),
|
|
23
|
+
protectedFiles: z.array(z.string()),
|
|
24
|
+
dangerousPatterns: z.array(z.string()),
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAc,CAAC,CAAC,MAAM,EAAE,CAAA;AAEzD,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,CAAU,CAAA;AAuBxG;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAmB,CAAC,CAAC,MAAM,CAAC;IACxD,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACzB,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACtC,4BAA4B,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC;IAClD,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1C,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACnC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CACvC,CAAC,CAAA"}
|
package/lib/persist.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surgical-delta persistence for the settings cascade. When a write reaches
|
|
3
|
+
* the provider it carries the full merged section for one namespace; writing
|
|
4
|
+
* that whole section into the user settings file would smear higher-layer
|
|
5
|
+
* (project/local/flag/policy) contributions into the user layer. Instead the
|
|
6
|
+
* section is diffed against the last-published shadow and only the leaf-level
|
|
7
|
+
* change ops are applied onto the user file's own section. File writes are
|
|
8
|
+
* atomic (temp + rename) so a crash never leaves a half-written document.
|
|
9
|
+
* @module @dsh-cc/settings-cascade/persist
|
|
10
|
+
*/
|
|
11
|
+
/** One leaf-level change to a JSON section, mirroring the seam's path ops. */
|
|
12
|
+
export type JsonOp = {
|
|
13
|
+
op: 'set';
|
|
14
|
+
path: string[];
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
op: 'unset';
|
|
18
|
+
path: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Diff two JSON sections into leaf-level ops. Leaves equal by deep value
|
|
22
|
+
* equality produce no op; changed or added leaves produce `set`; keys present
|
|
23
|
+
* in `prev` but absent in `next` produce `unset`. Arrays compare as whole
|
|
24
|
+
* leaves. When both sides are plain objects the diff recurses; when one side
|
|
25
|
+
* is not a plain object while the other is, the node is replaced wholesale.
|
|
26
|
+
* Neither input is mutated.
|
|
27
|
+
* @param prev - the earlier section (the shadow of what was published before).
|
|
28
|
+
* @param next - the desired section (the merged write target).
|
|
29
|
+
* @param path - the JSON path accumulated so far; defaults to the root `[]`.
|
|
30
|
+
* @returns the ordered ops that transform `prev` into `next`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function diffSections(prev: unknown, next: unknown, path?: string[]): JsonOp[];
|
|
33
|
+
/**
|
|
34
|
+
* Apply a list of ops onto a detached JSON section, returning the next
|
|
35
|
+
* section. Mirrors the seam's `applyPathOp`: a nested `set` that meets a
|
|
36
|
+
* non-plain-object intermediate node replaces that node wholesale with an
|
|
37
|
+
* object built around the remaining path; `unset` on a missing key is a
|
|
38
|
+
* no-op; a `set` with an empty path requires a plain object and replaces the
|
|
39
|
+
* whole section; an `unset` with an empty path yields `{}`. The input is not
|
|
40
|
+
* mutated.
|
|
41
|
+
* @param section - the current section (`undefined` when the namespace is absent).
|
|
42
|
+
* @param ops - the ops to apply in order.
|
|
43
|
+
* @returns the resulting section.
|
|
44
|
+
*/
|
|
45
|
+
export declare function applyOpsToSection(section: unknown, ops: JsonOp[]): unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Read one settings document into a plain object. Absence and whitespace-only
|
|
48
|
+
* content read as `{}`; invalid JSON or a non-object root fail loud with the
|
|
49
|
+
* path, mirroring the cascade provider's load-time parse.
|
|
50
|
+
* @param path - the absolute settings file path.
|
|
51
|
+
* @returns the parsed root document, or `{}` when the file is absent or blank.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readUserFile(path: string): Promise<Record<string, unknown>>;
|
|
54
|
+
/**
|
|
55
|
+
* Write a JSON root document atomically. The parent directory is created with
|
|
56
|
+
* mode `0o700`, the document is written to a random temp sibling under mode
|
|
57
|
+
* `0o600`, then renamed over the target. On any failure after the temp file is
|
|
58
|
+
* created it is best-effort removed before the error rethrows. The temp name
|
|
59
|
+
* is random (rather than fixed) so concurrent writers never collide on it.
|
|
60
|
+
* @param path - the absolute target settings file path.
|
|
61
|
+
* @param root - the plain-object document to serialize.
|
|
62
|
+
*/
|
|
63
|
+
export declare function writeJsonAtomic(path: string, root: Record<string, unknown>): Promise<void>;
|
|
64
|
+
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../src/persist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,8EAA8E;AAC9E,MAAM,MAAM,MAAM,GACd;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC7C;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAqBnC;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAmBxF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAM1E;AA2CD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAmBjF;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhG"}
|