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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to Devflow will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.11.0] - 2026-10-31
|
|
9
|
+
|
|
10
|
+
### Upgrade notes — Files that devflow moves to a backup folder
|
|
11
|
+
|
|
12
|
+
**What you must do:**
|
|
13
|
+
- Nothing. If `devflow init` moves a file of yours, the output names the file and its new path.
|
|
14
|
+
- To restore a moved file, copy it from the backup folder, `~/.devflow/install-backups/<UTC stamp>-<8 hex>/`, to one of its stable homes. Skills: `~/.devflow/skills/<name>/`. Rules: `~/.devflow/rules/`. Commands and agents: the top-level `commands/` and `agents/` folders of the Claude directory, outside the `devflow/` subfolders. A file that you put back into a devflow folder moves again on the next `devflow init`.
|
|
15
|
+
|
|
16
|
+
**What to know:**
|
|
17
|
+
- The backup is not encrypted. Devflow never prunes it. It stays until you delete it or run a full uninstall.
|
|
18
|
+
- A file of yours with the same name as a shipped file is overwritten without a report.
|
|
19
|
+
- Relative links in a moved folder can break.
|
|
20
|
+
- Concurrent devflow runs are not supported.
|
|
21
|
+
|
|
22
|
+
### Behaviour change
|
|
23
|
+
|
|
24
|
+
- **Devflow now names the retired files that it removes, and it moves bare legacy skill folders instead of deleting them** ([#688](https://github.com/dean0x/devflow/issues/688)).
|
|
25
|
+
- `devflow init` and the toggles print each retired devflow file that they remove. Before, most of these removals printed nothing.
|
|
26
|
+
- A bare legacy skill folder, such as `skills/python` (a name from releases before v2), is now moved whole to the backup folder and named, but only when its `SKILL.md` is a copy that a release of devflow installed. Before, `devflow init` deleted every such folder. A skill of your own with such a name stays where it is. A folder that devflow cannot judge (no `SKILL.md`, or a link) also stays, and the output names it.
|
|
27
|
+
- A full `devflow uninstall` now removes a bare legacy skill folder, such as `skills/python`, only when its `SKILL.md` is a copy that a release of devflow installed. Before, it deleted every folder with such a name, without a backup, so a skill of your own with that name was lost. A skill of your own stays where it is. A folder that devflow cannot judge (no `SKILL.md`, or a link) also stays. The output names each folder that stays, and `--dry-run` lists the same folders. A `devflow:`-prefixed skill folder is removed as before.
|
|
28
|
+
- An interactive `devflow uninstall` where the install backups are the only user content in `~/.devflow` now asks before the full wipe. The prompt lists the backup folder, and a decline keeps the backups.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **`devflow init` and the curation, compliance and rules toggles no longer delete files that devflow did not ship** ([#688](https://github.com/dean0x/devflow/issues/688)).
|
|
33
|
+
- A file that you keep in `commands/devflow/`, `agents/devflow/`, `rules/devflow/`, a `skills/devflow:*` folder or its `references/` folders is now moved to `~/.devflow/install-backups/<UTC stamp>-<8 hex>/`, and the output names it. Before, these commands deleted it. This holds for a full install, for a partial install (`--plugin`) and for the three toggles.
|
|
34
|
+
- A file that cannot move stays in place with its folder, and the output names it.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
8
38
|
## [3.10.0] - 2026-10-30
|
|
9
39
|
|
|
10
40
|
### Upgrade notes — Shadowed rules and skills
|
|
@@ -2338,6 +2368,7 @@ devflow init
|
|
|
2338
2368
|
---
|
|
2339
2369
|
|
|
2340
2370
|
[Unreleased]: https://github.com/dean0x/devflow/compare/v3.8.0...HEAD
|
|
2371
|
+
[3.11.0]: https://github.com/dean0x/devflow/compare/v3.10.0...v3.11.0
|
|
2341
2372
|
[3.10.0]: https://github.com/dean0x/devflow/compare/v3.9.0...v3.10.0
|
|
2342
2373
|
[3.9.0]: https://github.com/dean0x/devflow/compare/v3.8.0...v3.9.0
|
|
2343
2374
|
[3.8.0]: https://github.com/dean0x/devflow/compare/v3.7.0...v3.8.0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backup session of one command run: made once, before the command changes anything, and its
|
|
3
|
+
* record printed once when the command ends.
|
|
4
|
+
*
|
|
5
|
+
* `devflow init`, `devflow curation`, `devflow compliance` and `devflow rules` each make one
|
|
6
|
+
* session with {@link openBackupSession} and wrap the steps that may move or remove files in
|
|
7
|
+
* {@link runWithBackupReport}. The wrapper prints the record after the steps return or throw, so a
|
|
8
|
+
* run that moved a file and then failed still names where the file went.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from 'crypto';
|
|
11
|
+
import * as p from '@clack/prompts';
|
|
12
|
+
import { WORKFLOW_FOLDERS } from '../../core/mds-variants.js';
|
|
13
|
+
import { getPackageRoot } from '../../core/paths.js';
|
|
14
|
+
import { retiredPathSet } from '../../core/retired-shipped-names.js';
|
|
15
|
+
import { createBackupSession } from '../../targets/claude-code/install-backup.js';
|
|
16
|
+
import { loadShippedSet } from '../../targets/claude-code/shipped-set.js';
|
|
17
|
+
import { formatBackupSummary } from './install-report.js';
|
|
18
|
+
/**
|
|
19
|
+
* Log each summary line at the severity it carries — the one dispatch every
|
|
20
|
+
* `SummaryLine[]` renderer shares.
|
|
21
|
+
*
|
|
22
|
+
* Exhaustive over `SummaryLine['level']` rather than an `if/else`: a level added
|
|
23
|
+
* to the interface has to be routed here, at compile time, instead of silently
|
|
24
|
+
* degrading to `info` at every call site.
|
|
25
|
+
*/
|
|
26
|
+
export function logSummaryLines(lines) {
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
switch (line.level) {
|
|
29
|
+
case 'success':
|
|
30
|
+
p.log.success(line.message);
|
|
31
|
+
break;
|
|
32
|
+
case 'info':
|
|
33
|
+
p.log.info(line.message);
|
|
34
|
+
break;
|
|
35
|
+
case 'warn':
|
|
36
|
+
p.log.warn(line.message);
|
|
37
|
+
break;
|
|
38
|
+
default: {
|
|
39
|
+
const _exhaustive = line.level;
|
|
40
|
+
void _exhaustive;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Make the backup session of a command run, before the command changes anything.
|
|
48
|
+
*
|
|
49
|
+
* The shipped set comes from the full registries and the package source, never from what an
|
|
50
|
+
* earlier install left on disk. When it cannot be built the answer is a message and no session:
|
|
51
|
+
* a session with a smaller set would move devflow's own files aside. The caller prints the
|
|
52
|
+
* message, exits non-zero and changes nothing.
|
|
53
|
+
*/
|
|
54
|
+
export async function openBackupSession(options) {
|
|
55
|
+
const loaded = await loadShippedSet({
|
|
56
|
+
devflowDir: options.devflowDir,
|
|
57
|
+
packageRoot: options.packageRoot ?? getPackageRoot(),
|
|
58
|
+
workflowFolders: WORKFLOW_FOLDERS,
|
|
59
|
+
});
|
|
60
|
+
if (!loaded.ok) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
message: `Could not list the files devflow ships, so nothing was changed: ${loaded.reason}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const session = createBackupSession({
|
|
67
|
+
shipped: loaded.shipped,
|
|
68
|
+
retired: retiredPathSet(),
|
|
69
|
+
claudeDir: options.claudeDir,
|
|
70
|
+
devflowDir: options.devflowDir,
|
|
71
|
+
now: () => new Date(),
|
|
72
|
+
bytes: size => randomBytes(size),
|
|
73
|
+
});
|
|
74
|
+
return { ok: true, session };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The session of a command that reports its failures through the exit code: the session, or null
|
|
78
|
+
* after one error line and exit code 1. The caller then returns without a change.
|
|
79
|
+
*/
|
|
80
|
+
export async function openBackupSessionOrFail(options) {
|
|
81
|
+
const opened = await openBackupSession(options);
|
|
82
|
+
if (opened.ok)
|
|
83
|
+
return opened.session;
|
|
84
|
+
p.log.error(opened.message);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run `fn`, which makes its session part-way through (it calls `begin`), and print the record of
|
|
90
|
+
* that session once: at its own `print` call, or else after `fn` returns or throws and before the
|
|
91
|
+
* caller returns or rethrows. A step that ends the process (`process.exit`) calls `print` first,
|
|
92
|
+
* since nothing after an exit runs. A throw from `fn` passes on unchanged, and a run that never
|
|
93
|
+
* calls `begin` prints nothing.
|
|
94
|
+
*
|
|
95
|
+
* @param emit - Where the lines go. Defaults to the clack log.
|
|
96
|
+
*/
|
|
97
|
+
export async function runWithLateBackupReport(fn, emit = logSummaryLines) {
|
|
98
|
+
let session = null;
|
|
99
|
+
let printed = false;
|
|
100
|
+
const report = {
|
|
101
|
+
begin: (opened) => { session = opened; },
|
|
102
|
+
print: () => {
|
|
103
|
+
if (printed || session === null)
|
|
104
|
+
return;
|
|
105
|
+
printed = true;
|
|
106
|
+
emit(formatBackupSummary(session.snapshot()));
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
return await fn(report);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
report.print();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Run `fn` and print the record of `session` once, after `fn` returns or throws and before the
|
|
118
|
+
* caller returns, rethrows or exits. A throw from `fn` passes on unchanged.
|
|
119
|
+
*
|
|
120
|
+
* `fn` receives `printBackups`, which prints the record at once and is a no-op afterwards: a step
|
|
121
|
+
* that ends the process (`process.exit`) calls it first, since nothing after an exit runs.
|
|
122
|
+
*
|
|
123
|
+
* @param emit - Where the lines go. Defaults to the clack log.
|
|
124
|
+
*/
|
|
125
|
+
export function runWithBackupReport(session, fn, emit = logSummaryLines) {
|
|
126
|
+
return runWithLateBackupReport(async (report) => {
|
|
127
|
+
report.begin(session);
|
|
128
|
+
return fn(report.print);
|
|
129
|
+
}, emit);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=backup-session.js.map
|
|
@@ -21,6 +21,7 @@ import * as path from 'path';
|
|
|
21
21
|
import * as p from '@clack/prompts';
|
|
22
22
|
import color from 'picocolors';
|
|
23
23
|
import { COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
|
|
24
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
24
25
|
import { frameworkChoices, FRAMEWORK_SELECT_MESSAGE } from './compliance-prompts.js';
|
|
25
26
|
import { COMPLIANCE_SKILL_TOKENS } from '../../core/compliance-compose.js';
|
|
26
27
|
import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, loadSettingsModule, repoComplianceStatusLines, } from '../../core/evidence-policy.js';
|
|
@@ -213,10 +214,15 @@ async function recordComplianceSwitch(devflowDir, nextState) {
|
|
|
213
214
|
* A reference that the converge could not refresh, a linked folder for example, is an error that
|
|
214
215
|
* names it, with exit 1, and the switch stays recorded as on.
|
|
215
216
|
*
|
|
216
|
-
* Returns a result and never throws; the caller prints the messages and sets the exit code.
|
|
217
|
+
* Returns a result and never throws; the caller prints the messages and sets the exit code. The
|
|
218
|
+
* record of the backup session prints once, as the change returns (an exit-1 result included),
|
|
219
|
+
* ahead of the messages.
|
|
217
220
|
*/
|
|
218
|
-
export
|
|
219
|
-
|
|
221
|
+
export function applyComplianceChange(opts) {
|
|
222
|
+
return runWithBackupReport(opts.backups, () => applyChange(opts));
|
|
223
|
+
}
|
|
224
|
+
async function applyChange(opts) {
|
|
225
|
+
const { claudeDir, devflowDir, manifest, action, setFrameworks, runningVersion, packageRoot, warn, backups } = opts;
|
|
220
226
|
const resolved = resolveComplianceCliAction(manifest.features.compliance, action, setFrameworks);
|
|
221
227
|
// normalizeFrameworks deduplicates (e.g. --set gdpr,GDPR → [gdpr]).
|
|
222
228
|
const nextState = {
|
|
@@ -240,6 +246,7 @@ export async function applyComplianceChange(opts) {
|
|
|
240
246
|
runningVersion,
|
|
241
247
|
warn,
|
|
242
248
|
packageRoot,
|
|
249
|
+
backups,
|
|
243
250
|
});
|
|
244
251
|
if (outcome.kind === 'skipped') {
|
|
245
252
|
// The compliance skill and rule do not depend on the installed version, so they converge
|
|
@@ -249,6 +256,7 @@ export async function applyComplianceChange(opts) {
|
|
|
249
256
|
devflowDir,
|
|
250
257
|
manifest: { features: { compliance: nextState, rules: manifest.features.rules } },
|
|
251
258
|
warn,
|
|
259
|
+
backups,
|
|
252
260
|
});
|
|
253
261
|
if (!nextState.enabled) {
|
|
254
262
|
return { nextState, messages: [{ level: 'error', text: outcome.message }], exitCode: 1 };
|
|
@@ -422,6 +430,10 @@ export const complianceCommand = new Command('compliance')
|
|
|
422
430
|
else {
|
|
423
431
|
action = 'disable';
|
|
424
432
|
}
|
|
433
|
+
// The session comes before the first change. Without a shipped set the command changes nothing.
|
|
434
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
435
|
+
if (backups === null)
|
|
436
|
+
return;
|
|
425
437
|
// Write the switch, converge both axes, record the installed variants (applyComplianceChange).
|
|
426
438
|
const applied = await applyComplianceChange({
|
|
427
439
|
claudeDir,
|
|
@@ -431,6 +443,7 @@ export const complianceCommand = new Command('compliance')
|
|
|
431
443
|
setFrameworks,
|
|
432
444
|
runningVersion: await readRunningVersion(),
|
|
433
445
|
warn: (msg) => p.log.warn(msg),
|
|
446
|
+
backups,
|
|
434
447
|
});
|
|
435
448
|
// Emit messages
|
|
436
449
|
for (const msg of applied.messages) {
|
|
@@ -9,6 +9,7 @@ import { loadShippedAgentDefaults } from '../../core/agent-models.js';
|
|
|
9
9
|
import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
|
|
10
10
|
import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
|
|
11
11
|
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
12
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
12
13
|
import { applyVariantToggle, describeCurationConverge, describePostingLeft, installedScriptsSkew, readRunningVersion, } from '../../targets/claude-code/curation-install.js';
|
|
13
14
|
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
14
15
|
import { AREA_PAUSE_NOTICE } from '../../core/area-pause-notice.js';
|
|
@@ -657,16 +658,19 @@ async function handleClear() {
|
|
|
657
658
|
* this CLI writes. The toggle moves the ledger folder only then: under version skew
|
|
658
659
|
* (or when that could not be told) it moves nothing.
|
|
659
660
|
*/
|
|
660
|
-
async function convergeInstalledVariants(enabled) {
|
|
661
|
+
export async function convergeInstalledVariants(enabled, backups) {
|
|
661
662
|
try {
|
|
662
|
-
|
|
663
|
+
// The record of the session prints when the toggle returns or throws, ahead of the warning
|
|
664
|
+
// below, so a file moved aside before a failure is still named.
|
|
665
|
+
const outcome = await runWithBackupReport(backups, async () => applyVariantToggle({
|
|
663
666
|
claudeDir: getClaudeDirectory(),
|
|
664
667
|
devflowDir: getDevFlowDirectory(),
|
|
665
668
|
axis: 'curation',
|
|
666
669
|
enabled,
|
|
667
670
|
runningVersion: await readRunningVersion(),
|
|
668
671
|
warn: (msg) => p.log.warn(msg),
|
|
669
|
-
|
|
672
|
+
backups,
|
|
673
|
+
}));
|
|
670
674
|
if (outcome.kind === 'skipped') {
|
|
671
675
|
p.log.warn(outcome.message);
|
|
672
676
|
return 'not-converged';
|
|
@@ -721,13 +725,17 @@ async function tryMoveForToggle(ledgerRoot) {
|
|
|
721
725
|
* tries the shared move with no wait. Only `--disable` drains, skew included.
|
|
722
726
|
*/
|
|
723
727
|
async function handleToggle(enabled) {
|
|
728
|
+
// The session comes before the first change. Without a shipped set the toggle changes nothing.
|
|
729
|
+
const backups = await openBackupSessionOrFail({ claudeDir: getClaudeDirectory(), devflowDir: getDevFlowDirectory() });
|
|
730
|
+
if (backups === null)
|
|
731
|
+
return;
|
|
724
732
|
const recorded = await writeMachineFeature(getDevFlowDirectory(), 'curation', enabled);
|
|
725
733
|
if (!recorded.ok) {
|
|
726
734
|
p.log.error(`Devflow is not installed on this machine — run ${color.cyan('devflow init')} first`);
|
|
727
735
|
process.exitCode = 1;
|
|
728
736
|
return;
|
|
729
737
|
}
|
|
730
|
-
const converged = await convergeInstalledVariants(enabled);
|
|
738
|
+
const converged = await convergeInstalledVariants(enabled, backups);
|
|
731
739
|
const ledgerRoot = await getLedgerRoot();
|
|
732
740
|
const shownSkip = converged === 'converged' && ledgerRoot ? await tryMoveForToggle(ledgerRoot) : null;
|
|
733
741
|
if (enabled) {
|
|
@@ -15,9 +15,11 @@ import { describeMoveText, describeOutcome, runInitCurationMove, shownSkipAt } f
|
|
|
15
15
|
import { realGlobalTuningMoveIO, realProjectLedgerMoveIO } from './curation-move-io.js';
|
|
16
16
|
import { describeEchoedValue } from '../../core/echo-value.js';
|
|
17
17
|
import { installViaFileCopy, installScripts } from '../../targets/claude-code/installer.js';
|
|
18
|
-
import {
|
|
18
|
+
import { logSummaryLines, openBackupSession, runWithLateBackupReport } from './backup-session.js';
|
|
19
|
+
import { formatKeptLegacySkills, formatLinksSummary, formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
|
|
19
20
|
import { mergeLinkReports, newLinkReport } from '../../targets/claude-code/linked-folder.js';
|
|
20
|
-
import { sweepLegacyDevflowFiles } from './legacy-sweep.js';
|
|
21
|
+
import { sweepLegacyDevflowFiles, sweepLegacySkillFolders } from './legacy-sweep.js';
|
|
22
|
+
import { removeRulesFolder, rulesFolderRefusedMessage } from '../../targets/claude-code/rules-folder.js';
|
|
21
23
|
import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
|
|
22
24
|
import { installSettings, installManagedSettings, installClaudeignore, hasClaudeignore, discoverProjectGitRoots, ensureDevflowGitignore, applyUserSecurityDenyList, detectDenyState, resolveSecurityAction, assertHistoricalDenySuperset, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
|
|
23
25
|
import { DEVFLOW_PLUGINS, registeredPluginNames, buildAssetMaps, buildScopedSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
|
|
@@ -119,34 +121,6 @@ export function formatSweepSummary(report) {
|
|
|
119
121
|
}
|
|
120
122
|
return lines;
|
|
121
123
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Log each summary line at the severity it carries — the one dispatch every
|
|
124
|
-
* `SummaryLine[]` renderer shares.
|
|
125
|
-
*
|
|
126
|
-
* Exhaustive over `SummaryLine['level']` rather than an `if/else`: a level added
|
|
127
|
-
* to the interface has to be routed here, at compile time, instead of silently
|
|
128
|
-
* degrading to `info` at every call site.
|
|
129
|
-
*/
|
|
130
|
-
function logSummaryLines(lines) {
|
|
131
|
-
for (const line of lines) {
|
|
132
|
-
switch (line.level) {
|
|
133
|
-
case 'success':
|
|
134
|
-
p.log.success(line.message);
|
|
135
|
-
break;
|
|
136
|
-
case 'info':
|
|
137
|
-
p.log.info(line.message);
|
|
138
|
-
break;
|
|
139
|
-
case 'warn':
|
|
140
|
-
p.log.warn(line.message);
|
|
141
|
-
break;
|
|
142
|
-
default: {
|
|
143
|
-
const _exhaustive = line.level;
|
|
144
|
-
void _exhaustive;
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
124
|
/**
|
|
151
125
|
* Converge the installed agents onto the saved model and effort assignments, after the file copy
|
|
152
126
|
* wrote the shipped defaults. Per-item failures are non-fatal.
|
|
@@ -766,7 +740,7 @@ export const initCommand = new Command('init')
|
|
|
766
740
|
.option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
|
|
767
741
|
.option('--advanced', 'Show all configuration prompts')
|
|
768
742
|
.option('--reset', 'Factory reset — restore all defaults, ignoring prior installation state')
|
|
769
|
-
.action(async (
|
|
743
|
+
.action((options) => runWithLateBackupReport(async (backupReport) => {
|
|
770
744
|
// Get package version
|
|
771
745
|
const packageJsonPath = path.join(getPackageRoot(), 'package.json');
|
|
772
746
|
let version = '';
|
|
@@ -1822,6 +1796,17 @@ export const initCommand = new Command('init')
|
|
|
1822
1796
|
const { agentsMap } = buildAssetMaps(pluginsToInstall);
|
|
1823
1797
|
// Rules: install only from selected plugins (plugin-scoped, not universal)
|
|
1824
1798
|
const rulesMap = rulesEnabled ? buildRulesMap(pluginsToInstall) : new Map();
|
|
1799
|
+
// The one backup session of this run, made before any change. The install and the converge
|
|
1800
|
+
// after it share it, so the run makes at most one backup folder. When the shipped set cannot
|
|
1801
|
+
// be built, init changes nothing.
|
|
1802
|
+
const openedBackups = await openBackupSession({ claudeDir, devflowDir });
|
|
1803
|
+
if (!openedBackups.ok) {
|
|
1804
|
+
s.stop('Installation failed');
|
|
1805
|
+
p.log.error(openedBackups.message);
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
const backups = openedBackups.session;
|
|
1809
|
+
backupReport.begin(backups);
|
|
1825
1810
|
// D32/D35: Apply one-time migrations (global + per-project) tracked at ~/.devflow/migrations.json.
|
|
1826
1811
|
// Migrations clean up ~/.devflow runtime data and never touch the installer's copy
|
|
1827
1812
|
// targets, so their position relative to installViaFileCopy carries no dependency.
|
|
@@ -1878,11 +1863,14 @@ export const initCommand = new Command('init')
|
|
|
1878
1863
|
// generated reference tree, mode-normalisation failures) reach the user rather
|
|
1879
1864
|
// than the void. Collected now, emitted after the spinner stops.
|
|
1880
1865
|
warn: (msg) => { installWarnings.push(msg); },
|
|
1866
|
+
backups,
|
|
1881
1867
|
});
|
|
1882
1868
|
}
|
|
1883
1869
|
catch (error) {
|
|
1884
1870
|
s.stop('Installation failed');
|
|
1885
1871
|
p.log.error(`${error}`);
|
|
1872
|
+
// A file the install moved aside before it failed is named now: nothing runs after the exit.
|
|
1873
|
+
backupReport.print();
|
|
1886
1874
|
process.exit(1);
|
|
1887
1875
|
}
|
|
1888
1876
|
// The one converge for both switches (D-LEARNING-VARIANT-INSTALL, AC-27): the file copy
|
|
@@ -1906,6 +1894,7 @@ export const initCommand = new Command('init')
|
|
|
1906
1894
|
},
|
|
1907
1895
|
plugins: effectivePlugins,
|
|
1908
1896
|
warn: (msg) => installWarnings.push(msg),
|
|
1897
|
+
backups,
|
|
1909
1898
|
});
|
|
1910
1899
|
if (verbose && variantConverge.converged) {
|
|
1911
1900
|
p.log.info(`Curation ${curationEnabled ? 'on' : 'off'}, compliance ${complianceEnabled ? 'on' : 'off'} variants: ` +
|
|
@@ -1943,29 +1932,22 @@ export const initCommand = new Command('init')
|
|
|
1943
1932
|
if (behaviorEnabled && !rulesEnabled) {
|
|
1944
1933
|
p.log.info('Behavior rule is on but rules are disabled, so it is not installed — run `devflow rules --enable`.');
|
|
1945
1934
|
}
|
|
1946
|
-
//
|
|
1935
|
+
// Stale skill folders from previous installations move whole to the backup folder and are named,
|
|
1936
|
+
// but a bare folder moves only when its SKILL.md is a copy that a release of devflow installed.
|
|
1937
|
+
// A bare folder that the check cannot judge stays where it is, and is named here.
|
|
1947
1938
|
s.message('Cleaning up');
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
const staleRemoved = skillRemoveResults.filter(r => r.status === 'fulfilled').length;
|
|
1951
|
-
if (staleRemoved > 0 && verbose) {
|
|
1952
|
-
p.log.info(`Cleaned up ${staleRemoved} legacy skill(s)`);
|
|
1953
|
-
}
|
|
1939
|
+
const legacySkills = await sweepLegacySkillFolders({ claudeDir, backups, legacySkills: LEGACY_SKILL_NAMES });
|
|
1940
|
+
logSummaryLines(formatKeptLegacySkills(legacySkills.kept));
|
|
1954
1941
|
// Clean up stale commands (e.g., /review → /code-review) and rules from previous installations.
|
|
1955
1942
|
// A folder the install refused, or that is a link, is not swept: nothing is read or removed through it.
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1960
|
-
if (staleRulesRemoved > 0 && verbose) {
|
|
1961
|
-
p.log.info(`Cleaned up ${staleRulesRemoved} legacy rule(s)`);
|
|
1962
|
-
}
|
|
1963
|
-
// Disable rules directory if rules not enabled
|
|
1943
|
+
// Each removed file is named on the retired line of the backup record.
|
|
1944
|
+
await sweepLegacyDevflowFiles({ claudeDir, report: installReport, backups });
|
|
1945
|
+
// Disable rules directory if rules not enabled: the files devflow did not ship move aside first.
|
|
1964
1946
|
if (!rulesEnabled) {
|
|
1965
|
-
|
|
1966
|
-
|
|
1947
|
+
const rulesFolder = path.join(claudeDir, 'rules', 'devflow');
|
|
1948
|
+
if ((await removeRulesFolder(rulesFolder, backups)) === 'refused') {
|
|
1949
|
+
p.log.warn(rulesFolderRefusedMessage(rulesFolder));
|
|
1967
1950
|
}
|
|
1968
|
-
catch { /* ignore */ }
|
|
1969
1951
|
}
|
|
1970
1952
|
// Clean up legacy hook scripts and lib files left by prior installs
|
|
1971
1953
|
// (paths relative to hooksDir; copyDirectory is additive, so stale files
|
|
@@ -2007,12 +1989,11 @@ export const initCommand = new Command('init')
|
|
|
2007
1989
|
];
|
|
2008
1990
|
const hooksDir = path.join(devflowDir, 'scripts', 'hooks');
|
|
2009
1991
|
for (const legacy of LEGACY_HOOK_FILES) {
|
|
2010
|
-
|
|
2011
|
-
try {
|
|
2012
|
-
await fs.rm(legacyPath);
|
|
2013
|
-
}
|
|
2014
|
-
catch { /* doesn't exist */ }
|
|
1992
|
+
await backups.removeExact(path.join(hooksDir, legacy));
|
|
2015
1993
|
}
|
|
1994
|
+
// Every step that moves or removes files in devflow's folders has run: name what the session
|
|
1995
|
+
// moved aside, ahead of the summary below. A throw before this point prints it on the way out.
|
|
1996
|
+
backupReport.print();
|
|
2016
1997
|
// The curation move (#529): after the legacy sweep, before the manifest persist and
|
|
2017
1998
|
// the disabled-feature drain below, so the drain sees the moved folder. Its lines
|
|
2018
1999
|
// and its one notice print with the install warnings, after the spinner stops.
|
|
@@ -2611,5 +2592,5 @@ export const initCommand = new Command('init')
|
|
|
2611
2592
|
p.log.info(`External model routing: ${color.green('enabled')} — takes effect in new Claude Code sessions`);
|
|
2612
2593
|
}
|
|
2613
2594
|
p.outro(color.green('Ready! Run any command in Claude Code to get started.'));
|
|
2614
|
-
});
|
|
2595
|
+
}));
|
|
2615
2596
|
//# sourceMappingURL=init.js.map
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import color from 'picocolors';
|
|
11
11
|
import { overlayUnitLabel } from '../../targets/claude-code/installer.js';
|
|
12
|
+
import { escapeName, formatCappedList } from '../../targets/claude-code/install-backup.js';
|
|
12
13
|
import { refusedFolderMessage, replacedLinkMessage } from '../../targets/claude-code/linked-folder.js';
|
|
13
14
|
import { SKILL_REFS_SKILL_NAME } from '../../core/mds-variants.js';
|
|
14
15
|
import { prefixSkillName } from '../../core/plugins.js';
|
|
@@ -231,4 +232,98 @@ export function formatSkillScopeSummary(report, pluginListUnchanged) {
|
|
|
231
232
|
}
|
|
232
233
|
return lines;
|
|
233
234
|
}
|
|
235
|
+
const BACKUP_SHOWN_UNITS = 5;
|
|
236
|
+
/** One moved unit as `original -> backup`; a folder carries a trailing slash. */
|
|
237
|
+
function movedUnitLabel(unit) {
|
|
238
|
+
const slash = unit.kind === 'directory' ? '/' : '';
|
|
239
|
+
return `${unit.original}${slash} -> ${unit.backup}${slash}`;
|
|
240
|
+
}
|
|
241
|
+
function failureLine(failure) {
|
|
242
|
+
const where = escapeName(failure.path);
|
|
243
|
+
const reason = escapeName(failure.reason);
|
|
244
|
+
switch (failure.action) {
|
|
245
|
+
case 'move':
|
|
246
|
+
return { level: 'warn', message: `Could not move ${where} to the backup folder (${reason}) — it stays where it is` };
|
|
247
|
+
case 'scan':
|
|
248
|
+
return { level: 'warn', message: `Could not scan ${where} (${reason}) — nothing in it was moved or removed` };
|
|
249
|
+
case 'remove':
|
|
250
|
+
return { level: 'warn', message: `Could not remove ${where} (${reason}) — it stays where it is` };
|
|
251
|
+
default: {
|
|
252
|
+
const _exhaustive = failure.action;
|
|
253
|
+
void _exhaustive;
|
|
254
|
+
return { level: 'warn', message: `Could not finish a step on ${where} (${reason})` };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The lines for the bare legacy skill folders that init kept because it could not tell if devflow
|
|
260
|
+
* made them (`sweepLegacySkillFolders`): one line per folder for the first five, then one line that
|
|
261
|
+
* counts the rest. A run that kept none gives no lines. Every printed path goes through `escapeName`.
|
|
262
|
+
*/
|
|
263
|
+
export function formatKeptLegacySkills(kept) {
|
|
264
|
+
const lines = kept.slice(0, BACKUP_SHOWN_UNITS).map((folder) => ({
|
|
265
|
+
level: 'info',
|
|
266
|
+
message: `Kept ${escapeName(folder)}: devflow could not tell if it made this folder`,
|
|
267
|
+
}));
|
|
268
|
+
if (kept.length > BACKUP_SHOWN_UNITS) {
|
|
269
|
+
lines.push({ level: 'info', message: `Kept ${kept.length - BACKUP_SHOWN_UNITS} more folder(s) for the same reason` });
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Turn the backup session's record into summary lines: what moved aside, what stayed because it
|
|
275
|
+
* could not move, and which retired files were removed. An empty snapshot gives no lines, so a
|
|
276
|
+
* clean run prints nothing.
|
|
277
|
+
*
|
|
278
|
+
* A move is not a removal. The moved line has its own wording and counts only moved units (a moved
|
|
279
|
+
* folder is one), and it never feeds the "Removed N orphaned asset(s)" line of `formatSweepSummary`.
|
|
280
|
+
* One removed name prints on one line only, by the kind of removal:
|
|
281
|
+
*
|
|
282
|
+
* - the retired line below lists the retired files the session removed itself, with
|
|
283
|
+
* `BackupSession.removeListed` (the earlier releases' commands, agents, rules and skill files)
|
|
284
|
+
* or `BackupSession.removeExact` (the legacy sweep's command and rule names, and the legacy
|
|
285
|
+
* hook scripts);
|
|
286
|
+
* - `formatSweepSummary` lists the names the registry-diff sweeps removed, which the install
|
|
287
|
+
* report records in `sweptOrphans`. A sweep removes a name only after the session has not,
|
|
288
|
+
* and the session removes no name that a sweep records.
|
|
289
|
+
*
|
|
290
|
+
* Every printed name goes through `escapeName`. Pure function — returns lines, logs nothing.
|
|
291
|
+
*/
|
|
292
|
+
export function formatBackupSummary(snapshot) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
if (snapshot.moved.length > 0) {
|
|
295
|
+
lines.push({
|
|
296
|
+
level: 'info',
|
|
297
|
+
message: `Moved ${snapshot.moved.length} item(s) that devflow does not ship out of its folders: ` +
|
|
298
|
+
formatCappedList(snapshot.moved.map(movedUnitLabel), BACKUP_SHOWN_UNITS),
|
|
299
|
+
});
|
|
300
|
+
if (snapshot.runFolder !== null) {
|
|
301
|
+
lines.push({ level: 'info', message: `Backup folder: ${escapeName(snapshot.runFolder)}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (snapshot.retiredRemoved.length > 0) {
|
|
305
|
+
lines.push({
|
|
306
|
+
level: 'info',
|
|
307
|
+
message: `Removed ${snapshot.retiredRemoved.length} retired devflow file(s): ` +
|
|
308
|
+
formatCappedList(snapshot.retiredRemoved, BACKUP_SHOWN_UNITS),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const failure of snapshot.failures)
|
|
312
|
+
lines.push(failureLine(failure));
|
|
313
|
+
for (const hit of snapshot.capHits) {
|
|
314
|
+
lines.push({
|
|
315
|
+
level: 'warn',
|
|
316
|
+
message: `${escapeName(hit.folder)} holds ${hit.entries} entries, more than the limit of ${hit.cap} — ` +
|
|
317
|
+
'devflow left it whole and moved nothing out of it',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (snapshot.rootRefusal !== null) {
|
|
321
|
+
lines.push({
|
|
322
|
+
level: 'warn',
|
|
323
|
+
message: `Could not use the backup folder ${escapeName(snapshot.rootRefusal.path)} ` +
|
|
324
|
+
`(${escapeName(snapshot.rootRefusal.reason)}) — nothing was moved aside in this run`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
234
329
|
//# sourceMappingURL=install-report.js.map
|