devflow-kit 3.10.0 → 3.11.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 +31 -0
- package/dist/cli/commands/backup-session.js +131 -0
- package/dist/cli/commands/compliance.js +16 -3
- package/dist/cli/commands/curation.js +12 -4
- package/dist/cli/commands/init.js +36 -55
- package/dist/cli/commands/install-report.js +95 -0
- package/dist/cli/commands/legacy-sweep.js +140 -45
- package/dist/cli/commands/rules.js +50 -13
- package/dist/cli/commands/uninstall.js +62 -24
- package/dist/core/compliance.js +14 -0
- package/dist/core/fs-atomic.js +5 -1
- package/dist/core/orphan-sweep.js +20 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +3 -2
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/targets/claude-code/compliance-install.js +38 -13
- package/dist/targets/claude-code/compliance-off-files.js +5 -1
- package/dist/targets/claude-code/curation-install.js +75 -30
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +383 -192
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +5 -11
- package/dist/targets/claude-code/reference-converge.js +15 -2
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -0
- package/dist/targets/claude-code/workflow-folder-install.js +32 -12
- package/package.json +1 -1
|
@@ -14,7 +14,9 @@ import * as path from 'path';
|
|
|
14
14
|
* two providers may legitimately both carry a `comment.md`, so the registry name has to
|
|
15
15
|
* be the relative PATH, and the walk has to descend.
|
|
16
16
|
*
|
|
17
|
-
* Never writes
|
|
17
|
+
* Never writes. The sweep names what stays and what goes; a {@link ReferenceSweepDelegate}
|
|
18
|
+
* acts on every entry the manifest does not name, and the sweep itself never removes a
|
|
19
|
+
* folder recursively. No delete-then-write window.
|
|
18
20
|
*/
|
|
19
21
|
/**
|
|
20
22
|
* Descent bound for every walk over the generated reference tree — this sweep and the
|
|
@@ -42,24 +44,27 @@ export const MAX_REFERENCE_SWEEP_DEPTH = 8;
|
|
|
42
44
|
* @param root - Directory to converge (the installed `references/tracker/` tree).
|
|
43
45
|
* @param knownRelPaths - POSIX paths relative to `root` that must survive. Derived from
|
|
44
46
|
* the build's own module registries by the caller — never hand-listed.
|
|
47
|
+
* @param delegate - Acts on every entry the manifest does not name. Required: a sweep with
|
|
48
|
+
* no delegate would have to delete what it cannot name.
|
|
45
49
|
*
|
|
46
50
|
* @returns A {@link SweepResult} whose `removed` entries are POSIX relative paths. A
|
|
47
|
-
* directory into which no manifest path descends is
|
|
48
|
-
* own relative path
|
|
49
|
-
* short, and an empty provider directory is
|
|
50
|
-
* references failed to install. A subtree left
|
|
51
|
+
* directory into which no manifest path descends is handed to the delegate WHOLE and
|
|
52
|
+
* reported by its own relative path once the delegate removed it — leaving it empty
|
|
53
|
+
* would be a convergence that stops one step short, and an empty provider directory is
|
|
54
|
+
* indistinguishable from a provider whose references failed to install. A subtree left
|
|
55
|
+
* unswept because it breached
|
|
51
56
|
* {@link MAX_REFERENCE_SWEEP_DEPTH} is reported in `failed` under its own relative
|
|
52
57
|
* path, for the same reason: everything this sweep did not converge is named.
|
|
53
58
|
*
|
|
54
59
|
* A missing or unreadable `root` is a no-op, not an error: the overlay creates the tree
|
|
55
60
|
* it converges, so an absent one simply means there is nothing to prune yet.
|
|
56
61
|
*/
|
|
57
|
-
export async function sweepOrphanedReferences(root, knownRelPaths) {
|
|
62
|
+
export async function sweepOrphanedReferences(root, knownRelPaths, delegate) {
|
|
58
63
|
const acc = { scanned: 0, removed: [], failed: [] };
|
|
59
|
-
await sweepDirectory(root, '', 0, knownRelPaths, directoryPrefixes(knownRelPaths), acc);
|
|
64
|
+
await sweepDirectory(root, '', 0, knownRelPaths, directoryPrefixes(knownRelPaths), delegate, acc);
|
|
60
65
|
return { scanned: acc.scanned, removed: acc.removed, failed: acc.failed };
|
|
61
66
|
}
|
|
62
|
-
async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc) {
|
|
67
|
+
async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, delegate, acc) {
|
|
63
68
|
if (depth > MAX_REFERENCE_SWEEP_DEPTH) {
|
|
64
69
|
// A breached bound means this subtree is never visited, so any orphan inside it
|
|
65
70
|
// survives. Report it through the same channel as a failed removal: a silent return
|
|
@@ -82,35 +87,47 @@ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc)
|
|
|
82
87
|
const relPath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
83
88
|
const fullPath = path.join(dir, entry.name);
|
|
84
89
|
// A real subdirectory is either an ancestor of something the manifest names — in
|
|
85
|
-
// which case descend — or dead weight, in which case
|
|
86
|
-
// isDirectory() is false for a symlink-to-dir, so a planted link is treated
|
|
87
|
-
// leaf and
|
|
90
|
+
// which case descend — or dead weight, in which case the delegate takes the whole
|
|
91
|
+
// subtree. isDirectory() is false for a symlink-to-dir, so a planted link is treated
|
|
92
|
+
// as a leaf and handed over rather than followed.
|
|
88
93
|
if (entry.isDirectory()) {
|
|
89
94
|
if (knownDirPrefixes.has(`${relPath}/`)) {
|
|
90
|
-
await sweepDirectory(fullPath, relPath, depth + 1, known, knownDirPrefixes, acc);
|
|
95
|
+
await sweepDirectory(fullPath, relPath, depth + 1, known, knownDirPrefixes, delegate, acc);
|
|
91
96
|
continue;
|
|
92
97
|
}
|
|
93
98
|
acc.scanned++;
|
|
94
|
-
|
|
95
|
-
await fs.rm(fullPath, { recursive: true, force: true });
|
|
96
|
-
acc.removed.push(relPath);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
acc.failed.push({ name: relPath, error: err }); /* per-item isolation */
|
|
100
|
-
}
|
|
99
|
+
await dispose(fullPath, relPath, true, delegate, acc);
|
|
101
100
|
continue;
|
|
102
101
|
}
|
|
103
102
|
acc.scanned++;
|
|
104
103
|
if (known.has(relPath))
|
|
105
104
|
continue;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
await dispose(fullPath, relPath, false, delegate, acc);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Hand one entry to the delegate and record the outcome; a throw from the delegate is a per-item failure. */
|
|
109
|
+
async function dispose(fullPath, relPath, isDirectory, delegate, acc) {
|
|
110
|
+
try {
|
|
111
|
+
const disposition = await delegate.dispose(fullPath, isDirectory);
|
|
112
|
+
switch (disposition.outcome) {
|
|
113
|
+
case 'removed':
|
|
114
|
+
acc.removed.push(relPath);
|
|
115
|
+
break;
|
|
116
|
+
case 'failed':
|
|
117
|
+
acc.failed.push({ name: relPath, error: disposition.error });
|
|
118
|
+
break;
|
|
119
|
+
case 'moved':
|
|
120
|
+
case 'kept':
|
|
121
|
+
break;
|
|
122
|
+
default: {
|
|
123
|
+
const _exhaustive = disposition;
|
|
124
|
+
void _exhaustive;
|
|
125
|
+
}
|
|
112
126
|
}
|
|
113
127
|
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
acc.failed.push({ name: relPath, error: err }); /* per-item isolation */
|
|
130
|
+
}
|
|
114
131
|
}
|
|
115
132
|
/**
|
|
116
133
|
* Every directory prefix some manifest path sits under, trailing slash included:
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { createPathSet } from './path-set.js';
|
|
2
|
+
const names = (list) => Object.freeze([...list]);
|
|
3
|
+
export const RETIRED_SHIPPED = Object.freeze({
|
|
4
|
+
commands: names([
|
|
5
|
+
'ambient',
|
|
6
|
+
'audit-claude',
|
|
7
|
+
'brainstorm',
|
|
8
|
+
'breakdown',
|
|
9
|
+
'catch-up',
|
|
10
|
+
'commit',
|
|
11
|
+
'design',
|
|
12
|
+
'devlog',
|
|
13
|
+
'get-issue',
|
|
14
|
+
'plan-next-steps',
|
|
15
|
+
'pre-commit',
|
|
16
|
+
'pre-pr',
|
|
17
|
+
'pull-request',
|
|
18
|
+
'resolve-comments',
|
|
19
|
+
'specify',
|
|
20
|
+
]),
|
|
21
|
+
agents: names([
|
|
22
|
+
'audit-architecture',
|
|
23
|
+
'audit-complexity',
|
|
24
|
+
'audit-database',
|
|
25
|
+
'audit-dependencies',
|
|
26
|
+
'audit-documentation',
|
|
27
|
+
'audit-performance',
|
|
28
|
+
'audit-security',
|
|
29
|
+
'audit-tests',
|
|
30
|
+
'audit-typescript',
|
|
31
|
+
'brainstorm',
|
|
32
|
+
'catch-up',
|
|
33
|
+
'claude-md-auditor',
|
|
34
|
+
'code-edit',
|
|
35
|
+
'code-fix',
|
|
36
|
+
'code-review',
|
|
37
|
+
'coder',
|
|
38
|
+
'commit',
|
|
39
|
+
'diagnose',
|
|
40
|
+
'get-issue',
|
|
41
|
+
'learning',
|
|
42
|
+
'pr-comments',
|
|
43
|
+
'project-state',
|
|
44
|
+
'pull-request',
|
|
45
|
+
'release',
|
|
46
|
+
'resolver',
|
|
47
|
+
'reviewer',
|
|
48
|
+
'scrutinize',
|
|
49
|
+
'scrutinizer',
|
|
50
|
+
'shepherd',
|
|
51
|
+
'simplifier',
|
|
52
|
+
'simplify',
|
|
53
|
+
'skimmer',
|
|
54
|
+
'synthesizer',
|
|
55
|
+
'tech-debt',
|
|
56
|
+
'validator',
|
|
57
|
+
]),
|
|
58
|
+
rules: names([]),
|
|
59
|
+
skills: names([
|
|
60
|
+
'accessibility',
|
|
61
|
+
'agent-teams',
|
|
62
|
+
'ambient-router',
|
|
63
|
+
'apply-feature-kb',
|
|
64
|
+
'apply-knowledge',
|
|
65
|
+
'architecture-patterns',
|
|
66
|
+
'code-smell',
|
|
67
|
+
'codebase-navigation',
|
|
68
|
+
'commit',
|
|
69
|
+
'complexity-patterns',
|
|
70
|
+
'consistency-patterns',
|
|
71
|
+
'core-patterns',
|
|
72
|
+
'database-patterns',
|
|
73
|
+
'debug',
|
|
74
|
+
'debug-orchestration',
|
|
75
|
+
'debug:guided',
|
|
76
|
+
'debug:orch',
|
|
77
|
+
'debug:triage',
|
|
78
|
+
'decisions-format',
|
|
79
|
+
'dependencies-patterns',
|
|
80
|
+
'devflow',
|
|
81
|
+
'devflow-accessibility',
|
|
82
|
+
'devflow-agent-teams',
|
|
83
|
+
'devflow-architecture-patterns',
|
|
84
|
+
'devflow-code-smell',
|
|
85
|
+
'devflow-codebase-navigation',
|
|
86
|
+
'devflow-commit',
|
|
87
|
+
'devflow-complexity-patterns',
|
|
88
|
+
'devflow-consistency-patterns',
|
|
89
|
+
'devflow-core-patterns',
|
|
90
|
+
'devflow-database-patterns',
|
|
91
|
+
'devflow-debug',
|
|
92
|
+
'devflow-dependencies-patterns',
|
|
93
|
+
'devflow-docs-framework',
|
|
94
|
+
'devflow-documentation-patterns',
|
|
95
|
+
'devflow-error-handling',
|
|
96
|
+
'devflow-frontend-design',
|
|
97
|
+
'devflow-git-safety',
|
|
98
|
+
'devflow-git-workflow',
|
|
99
|
+
'devflow-github-patterns',
|
|
100
|
+
'devflow-implementation-patterns',
|
|
101
|
+
'devflow-input-validation',
|
|
102
|
+
'devflow-pattern-check',
|
|
103
|
+
'devflow-performance-patterns',
|
|
104
|
+
'devflow-pull-request',
|
|
105
|
+
'devflow-react',
|
|
106
|
+
'devflow-regression-patterns',
|
|
107
|
+
'devflow-review-methodology',
|
|
108
|
+
'devflow-security-patterns',
|
|
109
|
+
'devflow-self-review',
|
|
110
|
+
'devflow-test-design',
|
|
111
|
+
'devflow-tests-patterns',
|
|
112
|
+
'devflow-typescript',
|
|
113
|
+
'devflow:agent-teams',
|
|
114
|
+
'devflow:ambient-router',
|
|
115
|
+
'devflow:apply-feature-kb',
|
|
116
|
+
'devflow:apply-knowledge',
|
|
117
|
+
'devflow:architecture-patterns',
|
|
118
|
+
'devflow:complexity-patterns',
|
|
119
|
+
'devflow:consistency-patterns',
|
|
120
|
+
'devflow:core-patterns',
|
|
121
|
+
'devflow:database-patterns',
|
|
122
|
+
'devflow:debug',
|
|
123
|
+
'devflow:debug-orchestration',
|
|
124
|
+
'devflow:debug:guided',
|
|
125
|
+
'devflow:debug:orch',
|
|
126
|
+
'devflow:debug:triage',
|
|
127
|
+
'devflow:dependencies-patterns',
|
|
128
|
+
'devflow:documentation-patterns',
|
|
129
|
+
'devflow:dream-curation',
|
|
130
|
+
'devflow:dream-decisions',
|
|
131
|
+
'devflow:dream-knowledge',
|
|
132
|
+
'devflow:dream-memory',
|
|
133
|
+
'devflow:explore',
|
|
134
|
+
'devflow:explore:guided',
|
|
135
|
+
'devflow:explore:orch',
|
|
136
|
+
'devflow:explore:triage',
|
|
137
|
+
'devflow:feature-kb',
|
|
138
|
+
'devflow:frontend-design',
|
|
139
|
+
'devflow:git-safety',
|
|
140
|
+
'devflow:git-workflow',
|
|
141
|
+
'devflow:github-patterns',
|
|
142
|
+
'devflow:implement',
|
|
143
|
+
'devflow:implement:guided',
|
|
144
|
+
'devflow:implement:orch',
|
|
145
|
+
'devflow:implement:triage',
|
|
146
|
+
'devflow:implementation-orchestration',
|
|
147
|
+
'devflow:implementation-patterns',
|
|
148
|
+
'devflow:input-validation',
|
|
149
|
+
'devflow:knowledge-persistence',
|
|
150
|
+
'devflow:performance-patterns',
|
|
151
|
+
'devflow:pipeline',
|
|
152
|
+
'devflow:pipeline-orchestration',
|
|
153
|
+
'devflow:pipeline:orch',
|
|
154
|
+
'devflow:plan',
|
|
155
|
+
'devflow:plan-orchestration',
|
|
156
|
+
'devflow:plan:guided',
|
|
157
|
+
'devflow:plan:orch',
|
|
158
|
+
'devflow:plan:triage',
|
|
159
|
+
'devflow:regression-patterns',
|
|
160
|
+
'devflow:release:guided',
|
|
161
|
+
'devflow:release:orch',
|
|
162
|
+
'devflow:release:triage',
|
|
163
|
+
'devflow:research',
|
|
164
|
+
'devflow:research:guided',
|
|
165
|
+
'devflow:research:orch',
|
|
166
|
+
'devflow:research:triage',
|
|
167
|
+
'devflow:resolve',
|
|
168
|
+
'devflow:resolve-orchestration',
|
|
169
|
+
'devflow:resolve:orch',
|
|
170
|
+
'devflow:review',
|
|
171
|
+
'devflow:review-orchestration',
|
|
172
|
+
'devflow:review:guided',
|
|
173
|
+
'devflow:review:orch',
|
|
174
|
+
'devflow:review:triage',
|
|
175
|
+
'devflow:router',
|
|
176
|
+
'devflow:search-first',
|
|
177
|
+
'devflow:security-patterns',
|
|
178
|
+
'devflow:self-review',
|
|
179
|
+
'devflow:sidecar',
|
|
180
|
+
'devflow:test-patterns',
|
|
181
|
+
'docs-framework',
|
|
182
|
+
'documentation-patterns',
|
|
183
|
+
'dream-curation',
|
|
184
|
+
'dream-decisions',
|
|
185
|
+
'dream-knowledge',
|
|
186
|
+
'dream-memory',
|
|
187
|
+
'error-handling',
|
|
188
|
+
'explore',
|
|
189
|
+
'explore:guided',
|
|
190
|
+
'explore:orch',
|
|
191
|
+
'explore:triage',
|
|
192
|
+
'feature-kb',
|
|
193
|
+
'frontend-design',
|
|
194
|
+
'git-safety',
|
|
195
|
+
'git-workflow',
|
|
196
|
+
'github-patterns',
|
|
197
|
+
'go',
|
|
198
|
+
'implement',
|
|
199
|
+
'implement:guided',
|
|
200
|
+
'implement:orch',
|
|
201
|
+
'implement:triage',
|
|
202
|
+
'implementation-orchestration',
|
|
203
|
+
'implementation-patterns',
|
|
204
|
+
'input-validation',
|
|
205
|
+
'java',
|
|
206
|
+
'knowledge-persistence',
|
|
207
|
+
'pattern-check',
|
|
208
|
+
'performance-patterns',
|
|
209
|
+
'pipeline',
|
|
210
|
+
'pipeline-orchestration',
|
|
211
|
+
'pipeline:orch',
|
|
212
|
+
'plan',
|
|
213
|
+
'plan-orchestration',
|
|
214
|
+
'plan:guided',
|
|
215
|
+
'plan:orch',
|
|
216
|
+
'plan:triage',
|
|
217
|
+
'pull-request',
|
|
218
|
+
'python',
|
|
219
|
+
'react',
|
|
220
|
+
'regression-patterns',
|
|
221
|
+
'release:guided',
|
|
222
|
+
'release:orch',
|
|
223
|
+
'release:triage',
|
|
224
|
+
'research',
|
|
225
|
+
'research:guided',
|
|
226
|
+
'research:orch',
|
|
227
|
+
'research:triage',
|
|
228
|
+
'resolve',
|
|
229
|
+
'resolve-orchestration',
|
|
230
|
+
'resolve:orch',
|
|
231
|
+
'review',
|
|
232
|
+
'review-methodology',
|
|
233
|
+
'review-orchestration',
|
|
234
|
+
'review:guided',
|
|
235
|
+
'review:orch',
|
|
236
|
+
'review:triage',
|
|
237
|
+
'router',
|
|
238
|
+
'rust',
|
|
239
|
+
'search-first',
|
|
240
|
+
'security-patterns',
|
|
241
|
+
'self-review',
|
|
242
|
+
'sidecar',
|
|
243
|
+
'test-design',
|
|
244
|
+
'test-driven-development',
|
|
245
|
+
'test-patterns',
|
|
246
|
+
'tests-patterns',
|
|
247
|
+
'typescript',
|
|
248
|
+
'worktree-support',
|
|
249
|
+
]),
|
|
250
|
+
skillFiles: Object.freeze({
|
|
251
|
+
'devflow:agent-references': names([
|
|
252
|
+
'references/learning/maintain.md',
|
|
253
|
+
]),
|
|
254
|
+
'devflow:git': names([
|
|
255
|
+
'references/pr/ensure-pr-ready.md',
|
|
256
|
+
]),
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
/**
|
|
260
|
+
* The files of {@link RETIRED_SHIPPED} as paths relative to the Claude dir: the command, agent and
|
|
261
|
+
* rule files, and the listed files of a skill folder. A skill folder of `skills` is not a path
|
|
262
|
+
* here, since the install moves it whole.
|
|
263
|
+
*/
|
|
264
|
+
export function retiredPathSet() {
|
|
265
|
+
const paths = [];
|
|
266
|
+
for (const name of RETIRED_SHIPPED.commands)
|
|
267
|
+
paths.push(`commands/devflow/${name}.md`);
|
|
268
|
+
for (const name of RETIRED_SHIPPED.agents)
|
|
269
|
+
paths.push(`agents/devflow/${name}.md`);
|
|
270
|
+
for (const name of RETIRED_SHIPPED.rules)
|
|
271
|
+
paths.push(`rules/devflow/${name}.md`);
|
|
272
|
+
for (const [skill, files] of Object.entries(RETIRED_SHIPPED.skillFiles)) {
|
|
273
|
+
for (const file of files)
|
|
274
|
+
paths.push(`skills/${skill}/${file}`);
|
|
275
|
+
}
|
|
276
|
+
return createPathSet(paths);
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=retired-shipped-names.js.map
|
|
@@ -24,8 +24,9 @@ import * as path from 'path';
|
|
|
24
24
|
import { skillsDir, rulesDir } from '../../core/assets.js';
|
|
25
25
|
import { ALWAYS_PRESENT_REFS, COMPLIANCE_FRAMEWORKS, normalizeFrameworks } from '../../core/compliance.js';
|
|
26
26
|
import { parseComplianceFragment, composeComplianceSkill, composeComplianceRule } from '../../core/compliance-compose.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { swapFolderIn } from './folder-swap.js';
|
|
28
|
+
import { copyDirectory, validateSkillShadow, validateRuleShadow } from './installer.js';
|
|
29
|
+
import { newLinkReport, refuseUnrealFolder, warnLinkReport, writeFileReplacingLink } from './linked-folder.js';
|
|
29
30
|
// ── Path helpers ───────────────────────────────────────────────────────────
|
|
30
31
|
/** Every registry framework id — the reference set every install carries. */
|
|
31
32
|
const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
|
|
@@ -33,6 +34,14 @@ const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
|
|
|
33
34
|
function skillTarget(claudeDir) {
|
|
34
35
|
return path.join(claudeDir, 'skills', 'devflow:compliance');
|
|
35
36
|
}
|
|
37
|
+
/** The folder the compliance skill is built in before it is renamed into place: {claudeDir}/skills/devflow:compliance.tmp */
|
|
38
|
+
export function complianceSkillTempPath(claudeDir) {
|
|
39
|
+
return `${skillTarget(claudeDir)}.tmp`;
|
|
40
|
+
}
|
|
41
|
+
/** Where the compliance skill folder waits while the new one is renamed in: {claudeDir}/skills/devflow:compliance.old */
|
|
42
|
+
export function complianceSkillAsidePath(claudeDir) {
|
|
43
|
+
return `${skillTarget(claudeDir)}.old`;
|
|
44
|
+
}
|
|
36
45
|
/** Installed compliance rule file: {claudeDir}/rules/devflow/compliance.md */
|
|
37
46
|
function ruleTarget(claudeDir) {
|
|
38
47
|
return path.join(claudeDir, 'rules', 'devflow', 'compliance.md');
|
|
@@ -93,14 +102,22 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
|
|
|
93
102
|
* `fragments` is loaded once by convergeComplianceArtifacts and shared with the rule
|
|
94
103
|
* installer — the SKILL.md and the rule compose from the same parsed set.
|
|
95
104
|
*
|
|
96
|
-
* Build under a .tmp sibling,
|
|
97
|
-
*
|
|
105
|
+
* Build under a .tmp sibling, then swap it in: the old target waits in a .old sibling while the new one
|
|
106
|
+
* is renamed in. A file in the old target that devflow did not ship moves to the backup folder first;
|
|
107
|
+
* if one cannot move, nothing is swapped and the shipped files are written into the old target (see
|
|
108
|
+
* {@link swapFolderIn}). An old target that is a link is moved aside as the link itself, never
|
|
109
|
+
* followed, so what it points to is left as it was; the link is recorded in `links`.
|
|
98
110
|
* Unexpected I/O failures are reported via warn; never thrown.
|
|
99
111
|
*/
|
|
100
|
-
async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn, links) {
|
|
112
|
+
async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn, links, backups) {
|
|
101
113
|
const canonicalSrc = path.join(skillsDir(), 'compliance');
|
|
102
114
|
const target = skillTarget(claudeDir);
|
|
103
|
-
const tmpTarget =
|
|
115
|
+
const tmpTarget = complianceSkillTempPath(claudeDir);
|
|
116
|
+
// A skills folder that is a link or not a folder is refused before anything is built: the swap
|
|
117
|
+
// would move and remove below it, in the folder the link points to. The refusal is recorded in
|
|
118
|
+
// `links`, and the converge is not clean.
|
|
119
|
+
if (await refuseUnrealFolder(links, path.dirname(target)))
|
|
120
|
+
return;
|
|
104
121
|
// Determine SKILL.md source: prefer a valid shadow, else use canonical.
|
|
105
122
|
const skillShadowDir = path.join(devflowDir, 'skills', 'compliance');
|
|
106
123
|
const shadowState = await validateSkillShadow(skillShadowDir);
|
|
@@ -150,11 +167,18 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
|
|
|
150
167
|
warn(`compliance: reference "${fw}.md" not installed — ${String(err)}`);
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
|
-
// Swap:
|
|
170
|
+
// Swap: the old target waits aside, the tmp folder is renamed into place. The link is recorded
|
|
154
171
|
// once the real folder stands in its place, so the report matches its wording.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
await swapFolderIn({
|
|
173
|
+
backups,
|
|
174
|
+
target,
|
|
175
|
+
staged: tmpTarget,
|
|
176
|
+
aside: complianceSkillAsidePath(claudeDir),
|
|
177
|
+
links,
|
|
178
|
+
mergeInto: () => copyDirectory(tmpTarget, target, undefined, links),
|
|
179
|
+
});
|
|
180
|
+
// The tmp folder is gone after a swap; a merge leaves it behind.
|
|
181
|
+
await fs.rm(tmpTarget, { recursive: true, force: true });
|
|
158
182
|
}
|
|
159
183
|
catch (err) {
|
|
160
184
|
// Clean up tmp on failure (best-effort) so no orphan is left behind.
|
|
@@ -219,7 +243,7 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
|
|
|
219
243
|
* principle in engineering.md.
|
|
220
244
|
*/
|
|
221
245
|
export async function convergeComplianceArtifacts(opts) {
|
|
222
|
-
const { claudeDir, devflowDir, enabled, frameworks, rulesEnabled, warn } = opts;
|
|
246
|
+
const { claudeDir, devflowDir, enabled, frameworks, rulesEnabled, warn, backups } = opts;
|
|
223
247
|
// S78: claudeDir precondition — reliability rule: assert invariants in production code.
|
|
224
248
|
// An empty or relative claudeDir would make skillTarget/ruleTarget resolve to unexpected
|
|
225
249
|
// locations. Warn and bail rather than running fs.rm with a potentially wrong path.
|
|
@@ -251,7 +275,7 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
251
275
|
// installSkillDir is caught internally and reported via trackingWarn, so
|
|
252
276
|
// execution always continues to the rule step.
|
|
253
277
|
const links = newLinkReport();
|
|
254
|
-
await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn, links);
|
|
278
|
+
await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn, links, backups);
|
|
255
279
|
// The rule folder is proven before the rule is written or removed: a link there sends both into
|
|
256
280
|
// another place. The skill folder is swapped whole above, and a swap never follows a link.
|
|
257
281
|
const finish = (removedPreexisting) => {
|
|
@@ -298,7 +322,7 @@ export async function convergeComplianceArtifacts(opts) {
|
|
|
298
322
|
* than the value stored in the manifest.
|
|
299
323
|
*/
|
|
300
324
|
export async function convergeFromManifest(opts) {
|
|
301
|
-
const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride } = opts;
|
|
325
|
+
const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride, backups } = opts;
|
|
302
326
|
return convergeComplianceArtifacts({
|
|
303
327
|
claudeDir,
|
|
304
328
|
devflowDir,
|
|
@@ -306,6 +330,7 @@ export async function convergeFromManifest(opts) {
|
|
|
306
330
|
frameworks: manifest.features.compliance.frameworks,
|
|
307
331
|
rulesEnabled: rulesEnabledOverride ?? manifest.features.rules,
|
|
308
332
|
warn,
|
|
333
|
+
backups,
|
|
309
334
|
});
|
|
310
335
|
}
|
|
311
336
|
//# sourceMappingURL=compliance-install.js.map
|
|
@@ -148,9 +148,13 @@ async function sameBytes(a, b) {
|
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
/** The temp file beside `target` that {@link copyIntoPlace} writes through: `<target>.tmp.<pid>`. */
|
|
152
|
+
export function copyTempPath(target) {
|
|
153
|
+
return `${target}.tmp.${process.pid}`;
|
|
154
|
+
}
|
|
151
155
|
/** Copy `source` over `target` through a sibling temp file and a rename, so no reader sees a partial file. */
|
|
152
156
|
async function copyIntoPlace(source, target) {
|
|
153
|
-
const temp =
|
|
157
|
+
const temp = copyTempPath(target);
|
|
154
158
|
try {
|
|
155
159
|
await fs.unlink(temp).catch((err) => {
|
|
156
160
|
if (errno(err) !== 'ENOENT')
|