devflow-kit 2.5.0 → 3.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/CHANGELOG.md +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -12,10 +12,25 @@
|
|
|
12
12
|
* `~/.devflow/scripts` copy is never loaded: it may be older than this CLI. There
|
|
13
13
|
* is deliberately no TypeScript copy of the parser, the fold or the grammar.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* The same seam loads the sibling `resolve-settings.cjs` (loadSettingsModule),
|
|
16
|
+
* the local resolver of the per-repository settings layer — `.devflow/project.json`,
|
|
17
|
+
* the personal `.devflow/config.json` and the machine manifest. Its shapes are
|
|
18
|
+
* transcribed the same way, and there is no TypeScript copy of its fold either.
|
|
19
|
+
* It also loads the shared strict parser both resolvers use,
|
|
20
|
+
* `lib/project-config.cjs` (loadProjectConfigLib), so the CLI judges a config
|
|
21
|
+
* file's bytes exactly as the resolvers do.
|
|
22
|
+
*
|
|
23
|
+
* D-POLICY-NO-WRITE (applies ADR-024): `.devflow/project.json` is team-owned, and
|
|
16
24
|
* devflow never writes or replaces a shared file it cannot prove it wrote. This
|
|
17
25
|
* module therefore imports no fs API; the CLI only PRINTS the bytes a team may
|
|
18
|
-
* choose to commit (`evidencePolicySuggestion
|
|
26
|
+
* choose to commit (`evidencePolicySuggestion`, and the migration lines of
|
|
27
|
+
* `repoComplianceStatusLines`), all from the settings resolver's project.json
|
|
28
|
+
* serializer.
|
|
29
|
+
*
|
|
30
|
+
* D-POLICY-JSON-RETIRED: the evidence resolver never parses `.devflow/policy.json`;
|
|
31
|
+
* at a source whose project.json has no `evidence`, the file's presence alone
|
|
32
|
+
* resolves `required` (see the resolver's own note). This side neither reads nor
|
|
33
|
+
* serializes it — it only names it in the migration hint.
|
|
19
34
|
*/
|
|
20
35
|
import { createRequire } from 'module';
|
|
21
36
|
import { join } from 'path';
|
|
@@ -23,8 +38,14 @@ import { scriptsDir } from './assets.js';
|
|
|
23
38
|
// ── Transcribed shapes (resolve-evidence-policy.cjs JSDoc) ─────────────────────
|
|
24
39
|
/** Basename of the resolver under src/assets/scripts/ (and ~/.devflow/scripts/). */
|
|
25
40
|
export const RESOLVER_SCRIPT_NAME = 'resolve-evidence-policy.cjs';
|
|
41
|
+
/** Basename of the settings resolver under src/assets/scripts/ (and ~/.devflow/scripts/). */
|
|
42
|
+
export const SETTINGS_SCRIPT_NAME = 'resolve-settings.cjs';
|
|
43
|
+
/** The shared strict config parser, relative to src/assets/scripts/ (and ~/.devflow/scripts/). */
|
|
44
|
+
export const PROJECT_CONFIG_LIB_NAME = join('lib', 'project-config.cjs');
|
|
26
45
|
/** The team file the CLI suggests committing, relative to a repository root. */
|
|
27
|
-
const
|
|
46
|
+
const PROJECT_FILE = '.devflow/project.json';
|
|
47
|
+
/** The retired team file project.json's `evidence` replaces, relative to a repository root. */
|
|
48
|
+
const RETIRED_POLICY_FILE = '.devflow/policy.json';
|
|
28
49
|
/**
|
|
29
50
|
* Every key of EvidencePolicyModule and the runtime kind the loader requires of
|
|
30
51
|
* it. `satisfies` makes the compiler reject an interface key missing here.
|
|
@@ -38,7 +59,20 @@ export const EVIDENCE_POLICY_MODULE_SURFACE = Object.freeze({
|
|
|
38
59
|
FAIL_CLOSED_LINE: 'string',
|
|
39
60
|
complianceDefault: 'function',
|
|
40
61
|
resolve: 'function',
|
|
41
|
-
|
|
62
|
+
});
|
|
63
|
+
/** Every key of SettingsModule and the runtime kind the loader requires of it. */
|
|
64
|
+
export const SETTINGS_MODULE_SURFACE = Object.freeze({
|
|
65
|
+
SETTINGS_LINE_RE: 'regexp',
|
|
66
|
+
SETTINGS_FAIL_CLOSED_LINE: 'string',
|
|
67
|
+
resolveSettings: 'function',
|
|
68
|
+
serializeProjectSuggestion: 'function',
|
|
69
|
+
});
|
|
70
|
+
/** Every key of ProjectConfigLib and the runtime kind the loader requires of it. */
|
|
71
|
+
export const PROJECT_CONFIG_LIB_SURFACE = Object.freeze({
|
|
72
|
+
MAX_CONFIG_BYTES: 'number',
|
|
73
|
+
decodeConfigBytes: 'function',
|
|
74
|
+
readBoundedRegularFile: 'function',
|
|
75
|
+
collectDuplicateKeyPaths: 'function',
|
|
42
76
|
});
|
|
43
77
|
function hasKind(value, kind) {
|
|
44
78
|
switch (kind) {
|
|
@@ -46,6 +80,7 @@ function hasKind(value, kind) {
|
|
|
46
80
|
case 'object': return typeof value === 'object' && value !== null;
|
|
47
81
|
case 'regexp': return value instanceof RegExp;
|
|
48
82
|
case 'string': return typeof value === 'string';
|
|
83
|
+
case 'number': return typeof value === 'number' && Number.isFinite(value);
|
|
49
84
|
case 'function': return typeof value === 'function';
|
|
50
85
|
default: {
|
|
51
86
|
const exhaustive = kind;
|
|
@@ -54,21 +89,21 @@ function hasKind(value, kind) {
|
|
|
54
89
|
}
|
|
55
90
|
}
|
|
56
91
|
/** Surface keys that are absent or of the wrong kind on `value`, in surface order. */
|
|
57
|
-
function surfaceMismatches(value) {
|
|
92
|
+
function surfaceMismatches(value, surface) {
|
|
58
93
|
if (typeof value !== 'object' || value === null)
|
|
59
|
-
return Object.keys(
|
|
94
|
+
return Object.keys(surface);
|
|
60
95
|
const record = value;
|
|
61
|
-
return Object.entries(
|
|
96
|
+
return Object.entries(surface)
|
|
62
97
|
.filter(([key, kind]) => !hasKind(record[key], kind))
|
|
63
98
|
.map(([key]) => key);
|
|
64
99
|
}
|
|
65
100
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
101
|
+
* require() one package script and shape-check it against `surface`. Never
|
|
102
|
+
* throws: a missing file is `not-found`; a module that throws on load or lacks a
|
|
103
|
+
* surface key is `unusable`. The caller's type parameter is justified by the
|
|
104
|
+
* surface check, which `satisfies` ties to the interface's keys.
|
|
69
105
|
*/
|
|
70
|
-
|
|
71
|
-
const file = join(dir, RESOLVER_SCRIPT_NAME);
|
|
106
|
+
function loadScript(file, surface) {
|
|
72
107
|
let loaded;
|
|
73
108
|
try {
|
|
74
109
|
loaded = createRequire(import.meta.url)(file);
|
|
@@ -80,12 +115,34 @@ export function loadEvidencePolicyModule(dir = scriptsDir()) {
|
|
|
80
115
|
const detail = err instanceof Error ? err.message : String(err);
|
|
81
116
|
return { ok: false, error: { kind: 'unusable', path: file, detail } };
|
|
82
117
|
}
|
|
83
|
-
const mismatches = surfaceMismatches(loaded);
|
|
118
|
+
const mismatches = surfaceMismatches(loaded, surface);
|
|
84
119
|
if (mismatches.length > 0) {
|
|
85
120
|
return { ok: false, error: { kind: 'unusable', path: file, detail: `missing or mistyped: ${mismatches.join(', ')}` } };
|
|
86
121
|
}
|
|
87
122
|
return { ok: true, value: loaded };
|
|
88
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Load the evidence resolver from `dir` (default: the package's own scripts
|
|
126
|
+
* directory) and shape-check its surface.
|
|
127
|
+
*/
|
|
128
|
+
export function loadEvidencePolicyModule(dir = scriptsDir()) {
|
|
129
|
+
return loadScript(join(dir, RESOLVER_SCRIPT_NAME), EVIDENCE_POLICY_MODULE_SURFACE);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Load the settings resolver from `dir` (default: the package's own scripts
|
|
133
|
+
* directory) and shape-check its surface. A `resolveSettings()` call makes one
|
|
134
|
+
* local `git` call and no network call (D-SETTINGS-LOCAL-ONLY).
|
|
135
|
+
*/
|
|
136
|
+
export function loadSettingsModule(dir = scriptsDir()) {
|
|
137
|
+
return loadScript(join(dir, SETTINGS_SCRIPT_NAME), SETTINGS_MODULE_SURFACE);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Load the shared strict config parser from `dir` (default: the package's own
|
|
141
|
+
* scripts directory) and shape-check its surface.
|
|
142
|
+
*/
|
|
143
|
+
export function loadProjectConfigLib(dir = scriptsDir()) {
|
|
144
|
+
return loadScript(join(dir, PROJECT_CONFIG_LIB_NAME), PROJECT_CONFIG_LIB_SURFACE);
|
|
145
|
+
}
|
|
89
146
|
// ── Presentation (pure) ────────────────────────────────────────────────────────
|
|
90
147
|
/** `Evidence policy: <policy> (source: <source>)`, plus ` [warn: a, b]` when warnings exist. */
|
|
91
148
|
export function formatEvidencePolicyStatus(r) {
|
|
@@ -111,7 +168,7 @@ export function formatEvidencePolicyUnavailable(error) {
|
|
|
111
168
|
* The `compliance --status` line: the resolved policy for `opts.dir`, or the
|
|
112
169
|
* unavailable line when the loader failed — that line is the whole handling
|
|
113
170
|
* (ADR-028). The caller passes the compliance state it already read, so the
|
|
114
|
-
* manifest is never read twice. `resolve()` makes at most
|
|
171
|
+
* manifest is never read twice. `resolve()` makes at most three `gh` calls and
|
|
115
172
|
* bounds every subprocess with a timeout, so an offline machine degrades to a
|
|
116
173
|
* flagged result rather than a hang.
|
|
117
174
|
*/
|
|
@@ -121,27 +178,186 @@ export function evidencePolicyStatusLine(loaded, opts) {
|
|
|
121
178
|
return formatEvidencePolicyStatus(loaded.value.resolve(opts));
|
|
122
179
|
}
|
|
123
180
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
181
|
+
* The frameworks a compliance state names, for the suggestion: the raw list when
|
|
182
|
+
* the state is well-formed, else none. The settings resolver's serializer
|
|
183
|
+
* normalizes and drops unknown ids, so no id reaches the printed bytes unchecked.
|
|
184
|
+
*/
|
|
185
|
+
function suggestedFrameworks(complianceState) {
|
|
186
|
+
if (typeof complianceState !== 'object' || complianceState === null)
|
|
187
|
+
return [];
|
|
188
|
+
const frameworks = complianceState.frameworks;
|
|
189
|
+
return Array.isArray(frameworks) && frameworks.every(f => typeof f === 'string') ? frameworks : [];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* What `--enable`/`--set` print when compliance is on: the keys to add to a
|
|
193
|
+
* repository's `.devflow/project.json` on its default branch — merged into the
|
|
194
|
+
* file when it already has one, never replacing it — to hold every developer to
|
|
195
|
+
* what this machine now gets by default: the required evidence policy and this
|
|
196
|
+
* machine's frameworks. Returned only when the evidence resolver's own
|
|
126
197
|
* `complianceDefault` says `required` (compliance enabled, at any framework
|
|
127
|
-
* count); `null` otherwise. The bytes come
|
|
128
|
-
*
|
|
129
|
-
*
|
|
198
|
+
* count); `null` otherwise. The bytes come
|
|
199
|
+
* from the settings resolver's `serializeProjectSuggestion`, which returns them
|
|
200
|
+
* only when they read back through the shared parser as exactly what was asked.
|
|
201
|
+
* Nothing is written (D-POLICY-NO-WRITE, applies ADR-024).
|
|
130
202
|
*/
|
|
131
|
-
export function evidencePolicySuggestion(complianceState,
|
|
132
|
-
if (
|
|
203
|
+
export function evidencePolicySuggestion(complianceState, policy, settings) {
|
|
204
|
+
if (policy.complianceDefault(complianceState) !== 'required')
|
|
133
205
|
return null;
|
|
134
|
-
const body =
|
|
206
|
+
const body = settings.serializeProjectSuggestion({
|
|
207
|
+
evidence: 'required',
|
|
208
|
+
compliance: suggestedFrameworks(complianceState),
|
|
209
|
+
});
|
|
135
210
|
if (body === null)
|
|
136
211
|
return null;
|
|
137
212
|
return [
|
|
138
213
|
'Compliance is enabled on this machine, so repositories without a committed',
|
|
139
|
-
'
|
|
140
|
-
`working in a repository,
|
|
214
|
+
'evidence setting default to the required evidence policy here. To apply it for',
|
|
215
|
+
`everyone working in a repository, add these keys to its ${PROJECT_FILE} on its`,
|
|
216
|
+
'default branch — merged into the file when it already has one, never replacing it:',
|
|
141
217
|
'',
|
|
142
218
|
`${body}`,
|
|
143
219
|
'devflow never writes this file: the team owns it, and once committed it applies',
|
|
144
220
|
'repo-wide.',
|
|
145
221
|
].join('\n');
|
|
146
222
|
}
|
|
223
|
+
// ── The settings layer, for `--status` (pure) ──────────────────────────────────
|
|
224
|
+
/** A repo layer's file, as a `--status` line names it. */
|
|
225
|
+
export function settingsSourceFile(source) {
|
|
226
|
+
switch (source) {
|
|
227
|
+
case 'project': return PROJECT_FILE;
|
|
228
|
+
case 'personal': return '.devflow/config.json';
|
|
229
|
+
default: {
|
|
230
|
+
const exhaustive = source;
|
|
231
|
+
return exhaustive;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The effective state of a feature switch in this repository, ONLY when a repo
|
|
237
|
+
* layer narrows it — `disabled (.devflow/project.json)` — and null otherwise, so a
|
|
238
|
+
* `--status` whose machine switch alone decides prints exactly what it always has
|
|
239
|
+
* (D-FEATURES-NARROW-ONLY). A repository file that exists but is unreadable fails
|
|
240
|
+
* every field closed but the compliance lens, and a switch that closed off is
|
|
241
|
+
* labelled with that file —
|
|
242
|
+
* `disabled (.devflow/project.json is unreadable)` — since commands act on it. Any
|
|
243
|
+
* other failure (the resolver failed to load, or git could not answer) yields
|
|
244
|
+
* null: it knows nothing about this repository.
|
|
245
|
+
*/
|
|
246
|
+
export function narrowedSwitchLabel(loaded, opts, feature) {
|
|
247
|
+
if (!loaded.ok)
|
|
248
|
+
return null;
|
|
249
|
+
const settings = loaded.value.resolveSettings(opts);
|
|
250
|
+
if (!settings.ok) {
|
|
251
|
+
if (settings.unreadable === null || settings.switches[feature].on)
|
|
252
|
+
return null;
|
|
253
|
+
return `disabled (${settingsSourceFile(settings.unreadable)} is unreadable)`;
|
|
254
|
+
}
|
|
255
|
+
const state = settings.switches[feature];
|
|
256
|
+
if (state.on || state.source === 'machine')
|
|
257
|
+
return null;
|
|
258
|
+
return `disabled (${settingsSourceFile(state.source)})`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The tracker in effect in the repository at `opts.dir`, ONLY when a repository
|
|
262
|
+
* layer decides it — its committed project.json, or the personal config.json
|
|
263
|
+
* narrowing — and null otherwise. A machine whose own selection (or the github
|
|
264
|
+
* default) decides gets null, so `tracker --status` prints exactly what it always
|
|
265
|
+
* has there. So does a resolver that failed to load or failed closed: it knows
|
|
266
|
+
* nothing about this repository, and a fail-closed `github` is not a selection
|
|
267
|
+
* anyone made.
|
|
268
|
+
*/
|
|
269
|
+
export function repoTrackerSelection(loaded, opts) {
|
|
270
|
+
if (!loaded.ok)
|
|
271
|
+
return null;
|
|
272
|
+
const settings = loaded.value.resolveSettings(opts);
|
|
273
|
+
if (!settings.ok)
|
|
274
|
+
return null;
|
|
275
|
+
const source = settings.trackerSource;
|
|
276
|
+
if (source !== 'project' && source !== 'personal')
|
|
277
|
+
return null;
|
|
278
|
+
return { provider: settings.tracker, source };
|
|
279
|
+
}
|
|
280
|
+
/** A declared id list as a `--status` line shows it. */
|
|
281
|
+
function idsLabel(ids) {
|
|
282
|
+
return ids.length > 0 ? ids.join(', ') : 'generic controls only';
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The `compliance --status` lines about the repository in `opts.dir`, mirroring
|
|
286
|
+
* the resolver's lens fold (D-LENS-UNION: machine ∪ default branch ∪ worktree):
|
|
287
|
+
* the ids this checkout's project.json declares (`generic controls only` for an
|
|
288
|
+
* empty or malformed list), the ids the default branch's copy declares, the
|
|
289
|
+
* effective lens those add up to with the machine's, and a migration hint while
|
|
290
|
+
* the retired policy file is in the working tree.
|
|
291
|
+
*
|
|
292
|
+
* A broken file affects only the keys it owns. An unreadable project.json is a
|
|
293
|
+
* malformed declaration — generic — and says so, naming the file; an unreadable
|
|
294
|
+
* config.json owns no compliance, so the lines are those of a readable one. Empty
|
|
295
|
+
* when the resolver is unavailable or failed closed for any other reason, or no
|
|
296
|
+
* repository layer declares anything and there is no policy file — the status
|
|
297
|
+
* output is then unchanged.
|
|
298
|
+
*
|
|
299
|
+
* The hint states the rule (D-POLICY-JSON-RETIRED): the file is not read, and
|
|
300
|
+
* while project.json has no `evidence` its presence holds the repository at
|
|
301
|
+
* `required`. The value is not read either, so the hint shows the project.json
|
|
302
|
+
* line for each value the file may hold, from the settings resolver's serializer.
|
|
303
|
+
*/
|
|
304
|
+
export function repoComplianceStatusLines(loaded, opts) {
|
|
305
|
+
if (!loaded.ok)
|
|
306
|
+
return [];
|
|
307
|
+
const settings = loaded.value.resolveSettings(opts);
|
|
308
|
+
if (!settings.ok && settings.unreadable === null)
|
|
309
|
+
return [];
|
|
310
|
+
const lines = [];
|
|
311
|
+
if (settings.unreadable === 'project') {
|
|
312
|
+
lines.push(`Repository: generic controls only (${PROJECT_FILE} is unreadable)`);
|
|
313
|
+
}
|
|
314
|
+
else if (settings.repoCompliance !== null) {
|
|
315
|
+
lines.push(`Repository: ${idsLabel(settings.repoCompliance)} (${PROJECT_FILE})`);
|
|
316
|
+
}
|
|
317
|
+
if (settings.defaultBranchCompliance !== null) {
|
|
318
|
+
lines.push(`Default branch: ${idsLabel(settings.defaultBranchCompliance)} (its ${PROJECT_FILE})`);
|
|
319
|
+
}
|
|
320
|
+
if (lines.length > 0) {
|
|
321
|
+
const lens = settings.compliance;
|
|
322
|
+
lines.push(`Effective here: ${lens.enabled ? idsLabel(lens.frameworks) : 'off'} (this machine + the default branch + this checkout)`);
|
|
323
|
+
}
|
|
324
|
+
if (settings.retiredPolicyFile)
|
|
325
|
+
lines.push(...retiredPolicyHint(loaded.value));
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The warning a `--status` prints when this checkout's `.devflow/config.json` is
|
|
330
|
+
* tracked by git, or null (D-PERSONAL-UNTRACKED). The resolver ignores such a file
|
|
331
|
+
* and says so on stderr, but prompts run it with stderr discarded, so a status
|
|
332
|
+
* command is where the user sees why their personal settings have no effect.
|
|
333
|
+
*/
|
|
334
|
+
export function personalConfigTrackedWarning(loaded, opts) {
|
|
335
|
+
if (!loaded.ok)
|
|
336
|
+
return null;
|
|
337
|
+
if (!loaded.value.resolveSettings(opts).personalTracked)
|
|
338
|
+
return null;
|
|
339
|
+
const file = settingsSourceFile('personal');
|
|
340
|
+
return `${file} is tracked by git, so devflow ignores it — it holds personal settings. ` +
|
|
341
|
+
`Untrack it with: git rm --cached ${file}`;
|
|
342
|
+
}
|
|
343
|
+
/** The policies the hint maps, in the order it prints them. */
|
|
344
|
+
const HINT_POLICIES = ['standard', 'required'];
|
|
345
|
+
/**
|
|
346
|
+
* The migration hint for a working tree holding the retired policy file: what the
|
|
347
|
+
* file does now, and the project.json line that states each value it may hold.
|
|
348
|
+
* A value whose line the serializer refuses is left out rather than hand-built.
|
|
349
|
+
*/
|
|
350
|
+
function retiredPolicyHint(settings) {
|
|
351
|
+
const mappings = HINT_POLICIES.flatMap((policy) => {
|
|
352
|
+
const body = settings.serializeProjectSuggestion({ evidence: policy });
|
|
353
|
+
return body === null ? [] : [` ${policy.padEnd(8)} → ${body.trimEnd()}`];
|
|
354
|
+
});
|
|
355
|
+
return [
|
|
356
|
+
`Migration: ${RETIRED_POLICY_FILE} is not read. While ${PROJECT_FILE} has no "evidence",`,
|
|
357
|
+
' its presence alone holds this repository at required. Add its value to',
|
|
358
|
+
` ${PROJECT_FILE} as "evidence", and keep ${RETIRED_POLICY_FILE} until every`,
|
|
359
|
+
` teammate runs devflow 3.0 or later; only then delete it:`,
|
|
360
|
+
...mappings,
|
|
361
|
+
];
|
|
362
|
+
}
|
|
147
363
|
//# sourceMappingURL=evidence-policy.js.map
|
|
@@ -2,16 +2,19 @@ import * as path from 'path';
|
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { getFeatureConfigPath } from './project-paths.js';
|
|
4
4
|
import { parseTrackerId } from './tracker.js';
|
|
5
|
+
import { loadProjectConfigLib } from './evidence-policy.js';
|
|
5
6
|
/**
|
|
6
7
|
* Keys devflow itself once wrote and has retired. A managed write drops them
|
|
7
8
|
* rather than carrying them; no reader consults them.
|
|
8
9
|
*
|
|
9
|
-
* D-FEATURES-
|
|
10
|
-
* feature switches from the per-repo-install era. A
|
|
11
|
-
*
|
|
12
|
-
* neither decide anything nor linger to be mistaken for a switch:
|
|
13
|
-
* would leave a `learning: false` in the file that no longer does
|
|
14
|
-
* `decisions` is the pre-rename spelling of `learning`;
|
|
10
|
+
* D-FEATURES-NARROW-ONLY: `memory`, `learning` and `knowledge` were top-level
|
|
11
|
+
* per-repo feature switches from the per-repo-install era. A repository now
|
|
12
|
+
* narrows a feature only through the `features` namespace, so a stale top-level
|
|
13
|
+
* value must neither decide anything nor linger to be mistaken for a switch:
|
|
14
|
+
* carrying it would leave a `learning: false` in the file that no longer does
|
|
15
|
+
* what it says. `decisions` is the pre-rename spelling of `learning`;
|
|
16
|
+
* `autoCommit` is inert. `features` is deliberately NOT here — it is a live key,
|
|
17
|
+
* carried like any other unmanaged key (avoids PF-071).
|
|
15
18
|
*/
|
|
16
19
|
const RETIRED_CONFIG_KEYS = new Set([
|
|
17
20
|
'memory', 'learning', 'knowledge', 'decisions', 'autoCommit',
|
|
@@ -65,7 +68,7 @@ function isJsonObject(value) {
|
|
|
65
68
|
* DEFAULT_CONFIG. Pure function — no I/O, no side effects.
|
|
66
69
|
*
|
|
67
70
|
* The retired keys ({@link RETIRED_CONFIG_KEYS}) are ignored: an old config may
|
|
68
|
-
* still hold them, and none of them decides anything (D-FEATURES-
|
|
71
|
+
* still hold them, and none of them decides anything (D-FEATURES-NARROW-ONLY).
|
|
69
72
|
*
|
|
70
73
|
* Returns null when `parsed` is not a plain object (caller falls through to
|
|
71
74
|
* the next candidate path).
|
|
@@ -99,25 +102,72 @@ function coerceConfig(parsed) {
|
|
|
99
102
|
}
|
|
100
103
|
/**
|
|
101
104
|
* Read the per-repo config for a project root.
|
|
102
|
-
* Returns DEFAULT_CONFIG when the file is missing or
|
|
105
|
+
* Returns DEFAULT_CONFIG when the file is missing or unusable.
|
|
103
106
|
*/
|
|
104
107
|
export async function readConfig(projectRoot) {
|
|
105
|
-
return coerceConfig(await readConfigBody(projectRoot)) ?? { ...DEFAULT_CONFIG };
|
|
108
|
+
return coerceConfig(objectOf(await readConfigBody(projectRoot))) ?? { ...DEFAULT_CONFIG };
|
|
106
109
|
}
|
|
107
110
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
111
|
+
* Classify a config file's bytes (`null` is no file). Pure.
|
|
112
|
+
*
|
|
113
|
+
* D-CONFIG-STRICT-PARSE: `.devflow/config.json` is judged by the one parser the
|
|
114
|
+
* resolvers use (lib/project-config.cjs, D-PROJECT-STRICT-KEYS), never by a bare
|
|
115
|
+
* `JSON.parse`. The two disagreed where it mattered: `JSON.parse` keeps the
|
|
116
|
+
* LAST of two duplicate keys silently, while the resolvers read the file as
|
|
117
|
+
* saying two things and fail its keys closed — so devflow could rewrite a file
|
|
118
|
+
* into a meaning it never had. A file the parser rejects is `malformed`.
|
|
112
119
|
*/
|
|
113
|
-
|
|
120
|
+
export function classifyConfigBytes(buf, lib) {
|
|
121
|
+
const decoded = lib.decodeConfigBytes(buf);
|
|
122
|
+
if (decoded.kind === 'absent')
|
|
123
|
+
return { kind: 'absent' };
|
|
124
|
+
if (decoded.kind === 'invalid')
|
|
125
|
+
return { kind: 'malformed' };
|
|
126
|
+
let parsed;
|
|
114
127
|
try {
|
|
115
|
-
|
|
128
|
+
parsed = JSON.parse(decoded.text);
|
|
116
129
|
}
|
|
117
130
|
catch {
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
return { kind: 'malformed' };
|
|
132
|
+
}
|
|
133
|
+
if (!isJsonObject(parsed))
|
|
134
|
+
return { kind: 'malformed' };
|
|
135
|
+
const duplicates = lib.collectDuplicateKeyPaths(decoded.text);
|
|
136
|
+
if (duplicates === null || duplicates.size > 0)
|
|
137
|
+
return { kind: 'malformed' };
|
|
138
|
+
return { kind: 'object', value: parsed };
|
|
139
|
+
}
|
|
140
|
+
/** The object a config body holds, or undefined for any other body. */
|
|
141
|
+
function objectOf(body) {
|
|
142
|
+
return body.kind === 'object' ? body.value : undefined;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Read and classify a project's config file. Every read of the file goes
|
|
146
|
+
* through here, so readConfig, readConfigIfPresent and writeManagedConfig agree
|
|
147
|
+
* on what an unusable file means. Never throws.
|
|
148
|
+
*
|
|
149
|
+
* D-CONFIG-NO-FOLLOW: the bytes come from the resolvers' own bounded read
|
|
150
|
+
* (lib/project-config.cjs readBoundedRegularFile, `followSymlinks` false — the
|
|
151
|
+
* read resolve-settings' readConfigFile makes), so devflow and the settings line
|
|
152
|
+
* never disagree about which file configures the repository. A symlink —
|
|
153
|
+
* dangling or not — a directory, a FIFO or a file over MAX_CONFIG_BYTES is
|
|
154
|
+
* refused unopened and reads as `unreadable`: readers configure nothing from it,
|
|
155
|
+
* and writeManagedConfig leaves it in place (D-CONFIG-NO-REPAIR) rather than
|
|
156
|
+
* writing through the link or renaming a regular file over it.
|
|
157
|
+
*/
|
|
158
|
+
async function readConfigBody(projectRoot, lib = loadProjectConfigLib()) {
|
|
159
|
+
if (!lib.ok)
|
|
160
|
+
return { kind: 'unreadable', detail: `config parser unavailable: ${lib.error.path}` };
|
|
161
|
+
const read = lib.value.readBoundedRegularFile(getFeatureConfigPath(projectRoot), lib.value.MAX_CONFIG_BYTES, false);
|
|
162
|
+
if (read.kind === 'absent')
|
|
163
|
+
return { kind: 'absent' };
|
|
164
|
+
if (read.kind === 'refused') {
|
|
165
|
+
return {
|
|
166
|
+
kind: 'unreadable',
|
|
167
|
+
detail: `not a regular file of at most ${lib.value.MAX_CONFIG_BYTES} bytes; a symlink is never followed`,
|
|
168
|
+
};
|
|
120
169
|
}
|
|
170
|
+
return classifyConfigBytes(read.bytes, lib.value);
|
|
121
171
|
}
|
|
122
172
|
/**
|
|
123
173
|
* Serialise a config body to a project's config file.
|
|
@@ -146,8 +196,8 @@ async function writeConfigBody(projectRoot, body) {
|
|
|
146
196
|
* `managed`, by name, so a caller holding a whole FeatureConfig still cannot
|
|
147
197
|
* overwrite the file's override with its in-memory copy. The retired keys in
|
|
148
198
|
* RETIRED_CONFIG_KEYS are dropped, not carried. A body that is
|
|
149
|
-
* not a JSON object
|
|
150
|
-
*
|
|
199
|
+
* not a JSON object reads as empty, exactly as readConfigIfPresent treats it;
|
|
200
|
+
* writeManagedConfig never reaches here with a malformed file.
|
|
151
201
|
*
|
|
152
202
|
* This holds under `devflow init --reset` too: a factory reset returns
|
|
153
203
|
* devflow's own settings to their defaults through `managed`, and leaves the
|
|
@@ -164,16 +214,35 @@ export function mergeManagedConfig(existing, managed) {
|
|
|
164
214
|
}
|
|
165
215
|
/**
|
|
166
216
|
* Write devflow's managed keys to a project's config, keeping every key it
|
|
167
|
-
* does not manage (D-CONFIG-PRESERVE-UNMANAGED).
|
|
217
|
+
* does not manage (D-CONFIG-PRESERVE-UNMANAGED). Never throws.
|
|
218
|
+
*
|
|
219
|
+
* D-CONFIG-NO-REPAIR: a file that exists but is malformed or unreadable
|
|
220
|
+
* (D-CONFIG-STRICT-PARSE) is left byte-for-byte as it is, and the Result says
|
|
221
|
+
* why. The file is the user's: a syntax error in it still holds their
|
|
222
|
+
* hand-written keys — the `tracker` override first among them — and a rewrite
|
|
223
|
+
* from an empty merge would delete them silently. The resolvers fail its keys
|
|
224
|
+
* closed meanwhile, so leaving it costs nothing but the managed key.
|
|
168
225
|
*
|
|
169
226
|
* D1: Non-atomic read-modify-write. A concurrent writer could lose the other's
|
|
170
227
|
* change. Acceptable because init is a single-threaded, user-initiated command
|
|
171
228
|
* and the window is milliseconds on a local filesystem; the file swap itself is
|
|
172
229
|
* atomic (temp + rename), so a reader never sees a partial file.
|
|
173
230
|
*/
|
|
174
|
-
export async function writeManagedConfig(projectRoot, managed) {
|
|
175
|
-
const
|
|
176
|
-
await
|
|
231
|
+
export async function writeManagedConfig(projectRoot, managed, lib = loadProjectConfigLib()) {
|
|
232
|
+
const configPath = getFeatureConfigPath(projectRoot);
|
|
233
|
+
const existing = await readConfigBody(projectRoot, lib);
|
|
234
|
+
if (existing.kind === 'malformed')
|
|
235
|
+
return { ok: false, error: { kind: 'malformed', path: configPath } };
|
|
236
|
+
if (existing.kind === 'unreadable') {
|
|
237
|
+
return { ok: false, error: { kind: 'unreadable', path: configPath, detail: existing.detail } };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await writeConfigBody(projectRoot, mergeManagedConfig(objectOf(existing), managed));
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return { ok: false, error: { kind: 'write-failed', path: configPath, detail: err instanceof Error ? err.message : String(err) } };
|
|
244
|
+
}
|
|
245
|
+
return { ok: true };
|
|
177
246
|
}
|
|
178
247
|
/**
|
|
179
248
|
* Read the per-repo config for a project root, returning null when the file
|
|
@@ -185,7 +254,7 @@ export async function writeManagedConfig(projectRoot, managed) {
|
|
|
185
254
|
* distinction to carry a repo's own reviewPublication across a re-init.
|
|
186
255
|
*/
|
|
187
256
|
export async function readConfigIfPresent(projectRoot) {
|
|
188
|
-
// null when the file is absent
|
|
189
|
-
return coerceConfig(await readConfigBody(projectRoot));
|
|
257
|
+
// null when the file is absent, malformed or unreadable
|
|
258
|
+
return coerceConfig(objectOf(await readConfigBody(projectRoot)));
|
|
190
259
|
}
|
|
191
260
|
//# sourceMappingURL=feature-config.js.map
|
|
@@ -23,7 +23,7 @@ const LEGACY_KEYS = {
|
|
|
23
23
|
* shell hooks, so the CLI's status and the runtime never disagree about the
|
|
24
24
|
* same file.
|
|
25
25
|
*
|
|
26
|
-
* D-LEARNING-LEGACY-DECISIONS (a sub-decision of D-FEATURES-
|
|
26
|
+
* D-LEARNING-LEGACY-DECISIONS (a sub-decision of D-FEATURES-NARROW-ONLY):
|
|
27
27
|
* `learning` is read as `features.learning` when that is a boolean, else the
|
|
28
28
|
* legacy `features.decisions` when THAT is a boolean, else ON — readManifest's
|
|
29
29
|
* migration precedence exactly. The legacy key is otherwise honoured only once
|
package/dist/core/flags.js
CHANGED
|
@@ -1259,7 +1259,35 @@ export function convergeFlagsIntoSettings(settingsJson, record, opts) {
|
|
|
1259
1259
|
}
|
|
1260
1260
|
// ── Step 3: strip all managed keys, then apply the folded record ──────────
|
|
1261
1261
|
const stripped = stripFlags(settingsJson);
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1262
|
+
const applied = JSON.parse(applyFlags(stripped, folded));
|
|
1263
|
+
// ── Step 4: restore the pre-strip key order (D-KEY-ORDER) ────────────────
|
|
1264
|
+
const ordered = orderKeysLike(parsed, applied);
|
|
1265
|
+
const beforeEnv = asPlainObject(parsed.env);
|
|
1266
|
+
const afterEnv = asPlainObject(ordered.env);
|
|
1267
|
+
const settings = beforeEnv && afterEnv
|
|
1268
|
+
? { ...ordered, env: orderKeysLike(beforeEnv, afterEnv) }
|
|
1269
|
+
: ordered;
|
|
1270
|
+
return { settings: JSON.stringify(settings, null, 2) + '\n', record: folded };
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Return `after`'s entries ordered as `before` had them: every key `before` held,
|
|
1274
|
+
* in `before`'s order, then the keys only `after` holds, in `after`'s order.
|
|
1275
|
+
*
|
|
1276
|
+
* D-KEY-ORDER: strip-then-apply deletes every managed key and re-adds it, so each
|
|
1277
|
+
* one lands after whatever key followed it on disk. init merges the security deny
|
|
1278
|
+
* list AFTER the flags, which appends `permissions` behind them; a second init then
|
|
1279
|
+
* moved the flags behind `permissions` and re-init stopped being a no-op on disk
|
|
1280
|
+
* (#388 AC-3). Keeping the pre-converge order makes the first run's order the stable
|
|
1281
|
+
* one: a key that stays keeps its place, a key the record newly sets is appended, a
|
|
1282
|
+
* key it drops is simply absent. Values are `after`'s — only the order is borrowed.
|
|
1283
|
+
*
|
|
1284
|
+
* `Object.fromEntries` defines each key as an own data property, so a `__proto__`
|
|
1285
|
+
* key parsed from settings.json stays a key rather than becoming the prototype.
|
|
1286
|
+
*/
|
|
1287
|
+
function orderKeysLike(before, after) {
|
|
1288
|
+
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
1289
|
+
const kept = Object.keys(before).filter(k => has(after, k));
|
|
1290
|
+
const added = Object.keys(after).filter(k => !has(before, k));
|
|
1291
|
+
return Object.fromEntries([...kept, ...added].map(k => [k, after[k]]));
|
|
1264
1292
|
}
|
|
1265
1293
|
//# sourceMappingURL=flags.js.map
|
package/dist/core/fs-atomic.js
CHANGED
|
@@ -71,4 +71,31 @@ export async function writeFileAtomicExclusive(filePath, data) {
|
|
|
71
71
|
}
|
|
72
72
|
await fs.rename(tmp, filePath);
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Write a Claude Code settings file (`settings.json`) atomically.
|
|
76
|
+
*
|
|
77
|
+
* D-SETTINGS-ATOMIC: every write of a Claude settings file goes through this one
|
|
78
|
+
* helper, so no command can leave a half-written file for Claude Code — or a
|
|
79
|
+
* concurrent devflow command — to read: the bytes land in a sibling temp file
|
|
80
|
+
* and one rename swaps them in ({@link writeFileAtomicExclusive}).
|
|
81
|
+
*
|
|
82
|
+
* A settings file that is a symbolic link (a dotfiles-managed `settings.json`)
|
|
83
|
+
* stays one: the temp file and the rename target the link's resolved file, so
|
|
84
|
+
* the link keeps pointing where the user pointed it. A dangling link has no file
|
|
85
|
+
* to resolve and is replaced like a missing file.
|
|
86
|
+
*/
|
|
87
|
+
export async function writeSettingsFileAtomic(filePath, data) {
|
|
88
|
+
await writeFileAtomicExclusive(await resolveLinkedFile(filePath), data);
|
|
89
|
+
}
|
|
90
|
+
/** `filePath`, or the file it resolves to when it is a symbolic link that resolves. */
|
|
91
|
+
async function resolveLinkedFile(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
if (!(await fs.lstat(filePath)).isSymbolicLink())
|
|
94
|
+
return filePath;
|
|
95
|
+
return await fs.realpath(filePath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return filePath;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
74
101
|
//# sourceMappingURL=fs-atomic.js.map
|