devflow-kit 3.10.0 → 3.12.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 +74 -0
- package/dist/agents/architect.md +87 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +21 -49
- package/dist/agents/design.md +9 -43
- package/dist/agents/fix.md +15 -29
- package/dist/agents/git.md +19 -22
- package/dist/agents/harden.md +14 -19
- package/dist/agents/knowledge.md +11 -19
- package/dist/agents/refactor.md +9 -14
- package/dist/agents/research.md +11 -34
- package/dist/agents/review.md +22 -52
- package/dist/agents/skim.md +7 -17
- package/dist/agents/triage.md +7 -14
- 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/commands/bug-analysis.md +31 -62
- package/dist/commands/code-review.md +38 -87
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +155 -211
- package/dist/commands/dynamic-plan.md +71 -93
- package/dist/commands/dynamic-profile.md +7 -155
- package/dist/commands/dynamic-tickets.md +65 -186
- package/dist/commands/explore.md +27 -70
- package/dist/commands/implement.md +98 -186
- package/dist/commands/plan.md +60 -107
- package/dist/commands/release.md +25 -91
- package/dist/commands/research.md +34 -103
- package/dist/commands/resolve.md +64 -128
- package/dist/commands/self-review.md +35 -99
- package/dist/compliance-off/agents/git.md +15 -18
- package/dist/compliance-off/agents/triage.md +7 -14
- package/dist/compliance-off/commands/bug-analysis.md +31 -62
- package/dist/compliance-off/commands/code-review.md +34 -82
- package/dist/compliance-off/commands/dynamic-build.md +149 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +94 -180
- package/dist/compliance-off/commands/plan.md +60 -107
- package/dist/compliance-off/commands/release.md +25 -90
- package/dist/compliance-off/commands/resolve.md +54 -128
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- 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 +6 -5
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
- package/dist/curation-compliance-off/commands/code-review.md +25 -62
- package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +86 -161
- package/dist/curation-compliance-off/commands/plan.md +51 -86
- package/dist/curation-compliance-off/commands/release.md +17 -78
- package/dist/curation-compliance-off/commands/resolve.md +45 -108
- package/dist/curation-off/agents/architect.md +81 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +19 -47
- package/dist/curation-off/agents/design.md +8 -41
- package/dist/curation-off/agents/fix.md +14 -27
- package/dist/curation-off/agents/harden.md +13 -18
- package/dist/curation-off/agents/knowledge.md +9 -17
- package/dist/curation-off/agents/refactor.md +8 -12
- package/dist/curation-off/agents/research.md +9 -32
- package/dist/curation-off/agents/review.md +11 -40
- package/dist/curation-off/agents/skim.md +7 -17
- package/dist/curation-off/agents/triage.md +7 -13
- package/dist/curation-off/commands/bug-analysis.md +21 -41
- package/dist/curation-off/commands/code-review.md +29 -67
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +148 -200
- package/dist/curation-off/commands/dynamic-plan.md +60 -80
- package/dist/curation-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-off/commands/explore.md +18 -50
- package/dist/curation-off/commands/implement.md +90 -167
- package/dist/curation-off/commands/plan.md +51 -86
- package/dist/curation-off/commands/release.md +17 -79
- package/dist/curation-off/commands/research.md +23 -81
- package/dist/curation-off/commands/resolve.md +55 -108
- package/dist/curation-off/commands/self-review.md +26 -78
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- 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 +77 -32
- 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/dist/variant-axes.json +5 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +3 -5
- package/src/assets/agents/_partials/_design.mds +86 -0
- package/src/assets/agents/architect.mds +41 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +21 -49
- package/src/assets/agents/design.mds +11 -114
- package/src/assets/agents/evaluate.md +6 -16
- package/src/assets/agents/explore.md +1 -2
- package/src/assets/agents/fix.mds +12 -24
- package/src/assets/agents/git.mds +20 -23
- package/src/assets/agents/harden.mds +14 -19
- package/src/assets/agents/knowledge.mds +10 -38
- package/src/assets/agents/polish.md +13 -39
- package/src/assets/agents/refactor.mds +6 -9
- package/src/assets/agents/research.mds +11 -34
- package/src/assets/agents/review.mds +12 -84
- package/src/assets/agents/skim.mds +7 -17
- package/src/assets/agents/synthesize.md +3 -21
- package/src/assets/agents/test.md +31 -32
- package/src/assets/agents/tracker.md +116 -229
- package/src/assets/agents/triage.mds +7 -26
- package/src/assets/agents/validate.md +1 -3
- package/src/assets/commands/_partials/_compliance.mds +2 -2
- package/src/assets/commands/_partials/_decisions.mds +17 -24
- package/src/assets/commands/_partials/_docs_root.mds +31 -10
- package/src/assets/commands/_partials/_engine.mds +34 -56
- package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
- package/src/assets/commands/_partials/_factory.mds +10 -104
- package/src/assets/commands/_partials/_knowledge.mds +47 -51
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +21 -21
- package/src/assets/commands/_partials/_publication.mds +3 -3
- package/src/assets/commands/_partials/_roster.mds +12 -13
- package/src/assets/commands/_partials/_settings.mds +6 -3
- package/src/assets/commands/_partials/_ticket_template.mds +3 -7
- package/src/assets/commands/_partials/_tracker.mds +10 -5
- package/src/assets/commands/_partials/_wave.mds +23 -32
- package/src/assets/commands/bug-analysis.mds +3 -7
- package/src/assets/commands/code-review.mds +12 -33
- package/src/assets/commands/debug.mds +5 -47
- package/src/assets/commands/dynamic-build.mds +48 -70
- package/src/assets/commands/dynamic-plan.mds +23 -42
- package/src/assets/commands/dynamic-profile.mds +7 -57
- package/src/assets/commands/dynamic-tickets.mds +9 -29
- package/src/assets/commands/explore.mds +5 -44
- package/src/assets/commands/implement.mds +75 -117
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +7 -53
- package/src/assets/commands/resolve.mds +40 -87
- package/src/assets/commands/self-review.mds +5 -45
- package/src/assets/mds/git/_ops_pr.mds +1 -0
- package/src/assets/mds/git/_ops_steps.mds +4 -4
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/rules/engineering.md +1 -1
- package/src/assets/rules/quality.md +2 -2
- package/src/assets/scripts/ci-wait.cjs +150 -18
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
- package/src/assets/scripts/hooks/preamble +2 -3
- package/src/assets/scripts/pr-evidence.cjs +1 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
- package/src/assets/scripts/resolve-root.cjs +186 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +355 -22
- package/src/assets/skills/accessibility/SKILL.md +28 -95
- package/src/assets/skills/agent-references/SKILL.md +2 -4
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +5 -6
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
- package/src/assets/skills/architecture/SKILL.md +29 -105
- package/src/assets/skills/boundary-validation/SKILL.md +27 -78
- package/src/assets/skills/complexity/SKILL.md +8 -104
- package/src/assets/skills/compliance/SKILL.md +9 -67
- package/src/assets/skills/consistency/SKILL.md +7 -107
- package/src/assets/skills/database/SKILL.md +7 -72
- package/src/assets/skills/dependencies/SKILL.md +9 -109
- package/src/assets/skills/dependencies/references/detection.md +10 -0
- package/src/assets/skills/dependency-research/SKILL.md +9 -63
- package/src/assets/skills/design-review/SKILL.md +1 -35
- package/src/assets/skills/docs-framework/SKILL.md +4 -47
- package/src/assets/skills/documentation/SKILL.md +7 -71
- package/src/assets/skills/documentation/references/patterns.md +1 -1
- package/src/assets/skills/documentation/references/violations.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
- package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
- package/src/assets/skills/gap-analysis/SKILL.md +1 -31
- package/src/assets/skills/git/SKILL.md +2 -22
- package/src/assets/skills/go/SKILL.md +24 -113
- package/src/assets/skills/java/SKILL.md +12 -98
- package/src/assets/skills/patterns/SKILL.md +8 -93
- package/src/assets/skills/performance/SKILL.md +29 -88
- package/src/assets/skills/python/SKILL.md +22 -120
- package/src/assets/skills/qa/SKILL.md +16 -54
- package/src/assets/skills/qa/references/browser-testing.md +0 -24
- package/src/assets/skills/qa/references/sources.md +2 -2
- package/src/assets/skills/quality-gates/SKILL.md +13 -100
- package/src/assets/skills/react/SKILL.md +33 -95
- package/src/assets/skills/regression/SKILL.md +8 -73
- package/src/assets/skills/reliability/SKILL.md +11 -120
- package/src/assets/skills/research-codebase/SKILL.md +7 -49
- package/src/assets/skills/research-competitor/SKILL.md +4 -39
- package/src/assets/skills/research-external/SKILL.md +5 -43
- package/src/assets/skills/research-market/SKILL.md +5 -43
- package/src/assets/skills/research-technology/SKILL.md +8 -68
- package/src/assets/skills/review-methodology/SKILL.md +10 -56
- package/src/assets/skills/rust/SKILL.md +19 -109
- package/src/assets/skills/security/SKILL.md +39 -66
- package/src/assets/skills/software-design/SKILL.md +20 -105
- package/src/assets/skills/software-design/references/patterns.md +84 -2
- package/src/assets/skills/software-design/references/violations.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +32 -107
- package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
- package/src/assets/skills/testing/SKILL.md +35 -53
- package/src/assets/skills/typescript/SKILL.md +23 -102
- package/src/assets/skills/ui-design/SKILL.md +26 -74
- package/src/assets/skills/worktree-support/SKILL.md +4 -10
- package/dist/curation-off/commands/dynamic-profile.md +0 -214
|
@@ -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
|
|
@@ -1,64 +1,159 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import { promises as fs } from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import { LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES } from '../../core/plugins.js';
|
|
4
|
-
import {
|
|
5
|
+
import { RETIRED_SHIPPED } from '../../core/retired-shipped-names.js';
|
|
6
|
+
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
7
|
+
import { BARE_LEGACY_SKILL_HASHES } from '../../targets/claude-code/legacy-skill-hashes.js';
|
|
8
|
+
import { directoryState, isUnrealFolder } from '../../targets/claude-code/linked-folder.js';
|
|
9
|
+
/** The prefix of a skill folder in devflow's own namespace. A folder without it is bare. */
|
|
10
|
+
const NAMESPACE_PREFIX = 'devflow:';
|
|
5
11
|
/**
|
|
6
|
-
* Whether the sweep may remove files from `dir`: the install did not refuse it,
|
|
7
|
-
*
|
|
12
|
+
* Whether the sweep may remove files from `dir`: the install did not refuse it, nothing in it stays
|
|
13
|
+
* because a move failed or the folder was too large to scan, and `lstat` shows a real folder or
|
|
14
|
+
* nothing. `readdir` and `rm` of a path below a link act on the folder it points to.
|
|
8
15
|
*/
|
|
9
|
-
async function mayRemoveFrom(report, dir) {
|
|
10
|
-
return !report.refusedFolders.includes(dir) && !(await isUnrealFolder(dir));
|
|
16
|
+
async function mayRemoveFrom(report, backups, dir) {
|
|
17
|
+
return !report.refusedFolders.includes(dir) && !backups.isKept(dir) && !(await isUnrealFolder(dir));
|
|
18
|
+
}
|
|
19
|
+
/** Each name once, in the order given. */
|
|
20
|
+
const uniqueNames = (...lists) => [...new Set(lists.flat())];
|
|
21
|
+
/**
|
|
22
|
+
* Remove `<folder>/<name>.md` for each name, by exact name. A name that is shipped today is never
|
|
23
|
+
* removed, and a file that stays is left for the session's record.
|
|
24
|
+
*/
|
|
25
|
+
async function removeRetiredMarkdown(folder, names, backups) {
|
|
26
|
+
const removed = [];
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
const file = path.join(folder, `${name}.md`);
|
|
29
|
+
if (backups.classifyAt(file, 'file') === 'shipped')
|
|
30
|
+
continue;
|
|
31
|
+
if ((await backups.removeExact(file)).status === 'removed')
|
|
32
|
+
removed.push(file);
|
|
33
|
+
}
|
|
34
|
+
return removed;
|
|
11
35
|
}
|
|
12
36
|
/**
|
|
13
37
|
* Remove the files that earlier releases installed into `commands/devflow` and `rules/devflow` and
|
|
14
|
-
* that no release installs now: the
|
|
15
|
-
* Runs on every install shape, since the full install's pre-clean covers only a full
|
|
38
|
+
* that no release installs now: the retired command and rule names and the legacy lists, each by its
|
|
39
|
+
* exact name. Runs on every install shape, since the full install's pre-clean covers only a full
|
|
40
|
+
* install. A file whose name only looks like a retired one (a `*-teams.md` of the user's) is not
|
|
41
|
+
* matched here: the install's pre-pass moves it aside and names it.
|
|
16
42
|
*
|
|
17
|
-
* A folder the install refused (`report.refusedFolders`)
|
|
18
|
-
* sweep reads and removes nothing there, and the folder
|
|
43
|
+
* A folder the install refused (`report.refusedFolders`), that the session could not scan, or that
|
|
44
|
+
* is a link now is skipped whole: the sweep reads and removes nothing there, and the folder a link
|
|
45
|
+
* points to is left as it is. Every removal goes through `backups`, so each removed file is named
|
|
46
|
+
* once, on the retired line of its record. Never throws.
|
|
19
47
|
*
|
|
20
|
-
* @param opts.legacyCommands - Legacy command names;
|
|
21
|
-
* @param opts.legacyRules - Legacy rule names;
|
|
48
|
+
* @param opts.legacyCommands - Legacy command names; added to the retired commands.
|
|
49
|
+
* @param opts.legacyRules - Legacy rule names; added to the retired rules.
|
|
22
50
|
*/
|
|
23
51
|
export async function sweepLegacyDevflowFiles(opts) {
|
|
24
|
-
const { claudeDir, report } = opts;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
52
|
+
const { claudeDir, report, backups } = opts;
|
|
53
|
+
const commands = uniqueNames(RETIRED_SHIPPED.commands, opts.legacyCommands ?? LEGACY_COMMAND_NAMES);
|
|
54
|
+
const rules = uniqueNames(RETIRED_SHIPPED.rules, opts.legacyRules ?? LEGACY_RULE_NAMES);
|
|
55
|
+
const removed = [];
|
|
28
56
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
29
|
-
if (await mayRemoveFrom(report, commandsDir)) {
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await fs.rm(path.join(commandsDir, `${legacy}.md`));
|
|
33
|
-
result.staleCommandsRemoved++;
|
|
34
|
-
}
|
|
35
|
-
catch { /* absent — expected for most entries */ }
|
|
36
|
-
}
|
|
37
|
-
// Orphaned *-teams.md workflow command variants left by the Agent Teams refactor. None is ever
|
|
38
|
-
// re-installed, so a blanket sweep is safe on any install type.
|
|
39
|
-
try {
|
|
40
|
-
for (const entry of await fs.readdir(commandsDir)) {
|
|
41
|
-
if (!entry.endsWith('-teams.md'))
|
|
42
|
-
continue;
|
|
43
|
-
try {
|
|
44
|
-
await fs.rm(path.join(commandsDir, entry));
|
|
45
|
-
result.staleCommandsRemoved++;
|
|
46
|
-
}
|
|
47
|
-
catch { /* already gone */ }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch { /* commands folder absent — nothing to sweep */ }
|
|
57
|
+
if (await mayRemoveFrom(report, backups, commandsDir)) {
|
|
58
|
+
removed.push(...await removeRetiredMarkdown(commandsDir, commands, backups));
|
|
51
59
|
}
|
|
52
60
|
const rulesDir = path.join(claudeDir, 'rules', 'devflow');
|
|
53
|
-
if (await mayRemoveFrom(report, rulesDir)) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
if (await mayRemoveFrom(report, backups, rulesDir)) {
|
|
62
|
+
removed.push(...await removeRetiredMarkdown(rulesDir, rules, backups));
|
|
63
|
+
}
|
|
64
|
+
return { removed };
|
|
65
|
+
}
|
|
66
|
+
/** The most bytes of a `SKILL.md` the check reads. Every released copy is far smaller, so a larger file is not one. */
|
|
67
|
+
const MAX_SKILL_FILE_BYTES = 1024 * 1024;
|
|
68
|
+
/**
|
|
69
|
+
* Who made a bare legacy skill folder, judged from its `SKILL.md` alone:
|
|
70
|
+
* - `devflow`: the file is a regular file whose SHA-256 is one of `released`;
|
|
71
|
+
* - `other`: the file reads and is not one of them, or is larger than any release copy;
|
|
72
|
+
* - `unknown`: the check cannot decide. There is no `SKILL.md`, it is not a regular file, the
|
|
73
|
+
* folder or the file is a link, or a read fails.
|
|
74
|
+
* An `absent` folder needs no answer. Reads one file at most. Never throws.
|
|
75
|
+
*/
|
|
76
|
+
async function judgeBareSkillFolder(folder, released) {
|
|
77
|
+
const state = await directoryState(folder);
|
|
78
|
+
if (state === 'absent')
|
|
79
|
+
return 'absent';
|
|
80
|
+
if (state !== 'real')
|
|
81
|
+
return 'unknown';
|
|
82
|
+
try {
|
|
83
|
+
const skillFile = path.join(folder, 'SKILL.md');
|
|
84
|
+
const stat = await fs.lstat(skillFile);
|
|
85
|
+
if (!stat.isFile())
|
|
86
|
+
return 'unknown';
|
|
87
|
+
if (stat.size > MAX_SKILL_FILE_BYTES)
|
|
88
|
+
return 'other';
|
|
89
|
+
const digest = createHash('sha256').update(await fs.readFile(skillFile)).digest('hex');
|
|
90
|
+
return released.includes(digest) ? 'devflow' : 'other';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* {@link judgeBareSkillFolder} for the bare legacy skill `name`, against the released copies of that
|
|
98
|
+
* name. A name with no entry in `released` was never installed bare by a release, so a folder of
|
|
99
|
+
* that name is `other`. The one check that `devflow init` and `devflow uninstall` share.
|
|
100
|
+
*
|
|
101
|
+
* @param folder - The folder of the skill, `<skills folder>/<name>`.
|
|
102
|
+
* @param released - SHA-256 hashes of the released `SKILL.md` copies by bare name; defaults to the frozen table.
|
|
103
|
+
*/
|
|
104
|
+
export function judgeBareLegacySkill(folder, name, released = BARE_LEGACY_SKILL_HASHES) {
|
|
105
|
+
return judgeBareSkillFolder(folder, Object.hasOwn(released, name) ? released[name] : []);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Deal with the skill folders that earlier installs left under the legacy names: every name of the
|
|
109
|
+
* legacy skill list, bare and `devflow:`-prefixed.
|
|
110
|
+
*
|
|
111
|
+
* A `devflow:`-prefixed folder that no release listed moves whole, with one rename and no walk, and
|
|
112
|
+
* is named; it is never deleted. A bare folder lives in the skills folder that Claude Code and other
|
|
113
|
+
* tools share, so its name proves nothing (`python`, `plan` and `review` are names of a user's own
|
|
114
|
+
* skills too). It moves only when its `SKILL.md` is a copy that a release of devflow installed
|
|
115
|
+
* (`released`). A bare folder whose `SKILL.md` is any other file stays where it is, and nothing is
|
|
116
|
+
* said. A bare folder that the check cannot judge stays, is never deleted, and is returned in
|
|
117
|
+
* `kept`, so the caller names it. A name with a listing in `retiredFiles` (the files a release
|
|
118
|
+
* shipped in that folder) goes by the listing instead: the listed files are removed, the rest of
|
|
119
|
+
* the folder moves, and a plain `rmdir` removes the folder only when it is empty. A move that fails
|
|
120
|
+
* leaves the folder and its files where they are, and the session records it. A name that is shipped
|
|
121
|
+
* today, or that an earlier step could not move, is skipped, and so is every name when the skills
|
|
122
|
+
* folder is a link or not a folder: nothing is moved through a link. Nothing here removes a folder
|
|
123
|
+
* with recursion. Never throws.
|
|
124
|
+
*
|
|
125
|
+
* @param opts.legacySkills - Folder names to deal with; defaults to the legacy skill list.
|
|
126
|
+
* @param opts.retiredFiles - Released file listings by folder name; defaults to the retired registry.
|
|
127
|
+
* @param opts.released - SHA-256 hashes of the released `SKILL.md` copies by bare name; defaults to the frozen table.
|
|
128
|
+
*/
|
|
129
|
+
export async function sweepLegacySkillFolders(opts) {
|
|
130
|
+
const { backups } = opts;
|
|
131
|
+
const retiredFiles = opts.retiredFiles ?? RETIRED_SHIPPED.skillFiles;
|
|
132
|
+
const released = opts.released ?? BARE_LEGACY_SKILL_HASHES;
|
|
133
|
+
const kept = [];
|
|
134
|
+
const skillsDir = path.join(opts.claudeDir, 'skills');
|
|
135
|
+
if ((await directoryState(skillsDir)) !== 'real')
|
|
136
|
+
return { kept };
|
|
137
|
+
for (const name of opts.legacySkills ?? LEGACY_SKILL_NAMES) {
|
|
138
|
+
const folder = path.join(skillsDir, name);
|
|
139
|
+
if (backups.isKept(folder) || backups.classifyAt(folder, 'directory') === 'shipped')
|
|
140
|
+
continue;
|
|
141
|
+
const listed = Object.hasOwn(retiredFiles, name) && retiredFiles[name].length > 0;
|
|
142
|
+
if (listed && (await directoryState(folder)) === 'real') {
|
|
143
|
+
await backups.prepass([folder]);
|
|
144
|
+
await backups.removeListed(folder);
|
|
145
|
+
}
|
|
146
|
+
else if (name.startsWith(NAMESPACE_PREFIX)) {
|
|
147
|
+
await backups.moveWhole(folder);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const verdict = await judgeBareLegacySkill(folder, name, released);
|
|
151
|
+
if (verdict === 'devflow')
|
|
152
|
+
await backups.moveWhole(folder);
|
|
153
|
+
else if (verdict === 'unknown')
|
|
154
|
+
kept.push(folder);
|
|
60
155
|
}
|
|
61
156
|
}
|
|
62
|
-
return
|
|
157
|
+
return { kept };
|
|
63
158
|
}
|
|
64
159
|
//# sourceMappingURL=legacy-sweep.js.map
|
|
@@ -10,7 +10,9 @@ import { rulesDir } from '../../core/assets.js';
|
|
|
10
10
|
import { installAllRules, validateRuleShadow } from '../../targets/claude-code/installer.js';
|
|
11
11
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
12
12
|
import { convergeBehaviorFromManifest } from '../../targets/claude-code/behavior-install.js';
|
|
13
|
-
import { newLinkReport, refuseUnrealFolder, refusedFolderMessage
|
|
13
|
+
import { newLinkReport, refuseUnrealFolder, refusedFolderMessage } from '../../targets/claude-code/linked-folder.js';
|
|
14
|
+
import { removeRulesFolder, rulesFolderRefusedMessage } from '../../targets/claude-code/rules-folder.js';
|
|
15
|
+
import { openBackupSessionOrFail, runWithBackupReport } from './backup-session.js';
|
|
14
16
|
import { formatLinksSummary } from './install-report.js';
|
|
15
17
|
/**
|
|
16
18
|
* Check whether a rule has a user shadow file at ~/.devflow/rules/{name}.md.
|
|
@@ -205,20 +207,29 @@ async function handleRuleUnshadow(name, allRules, devflowDir) {
|
|
|
205
207
|
*
|
|
206
208
|
* Warn-not-throw on per-item compliance convergence failure, so a convergence
|
|
207
209
|
* failure never undoes the rules install.
|
|
210
|
+
*
|
|
211
|
+
* The record of `backups` prints once, as the enable returns or throws.
|
|
208
212
|
*/
|
|
209
|
-
export
|
|
213
|
+
export function runRulesEnable(claudeDir, devflowDir, backups) {
|
|
214
|
+
return runWithBackupReport(backups, printBackups => enableRules(claudeDir, devflowDir, backups, printBackups));
|
|
215
|
+
}
|
|
216
|
+
async function enableRules(claudeDir, devflowDir, backups, printBackups) {
|
|
210
217
|
const rulesTarget = path.join(claudeDir, 'rules', 'devflow');
|
|
211
218
|
const manifest = await readManifest(devflowDir);
|
|
212
219
|
if (!manifest) {
|
|
213
220
|
p.log.error('No manifest found. Run devflow init first.');
|
|
221
|
+
printBackups();
|
|
214
222
|
process.exit(1);
|
|
215
223
|
}
|
|
216
224
|
const installedPlugins = DEVFLOW_PLUGINS.filter(pl => manifest.plugins.includes(pl.name));
|
|
217
225
|
const rulesMap = buildRulesMap(installedPlugins);
|
|
218
|
-
//
|
|
219
|
-
//
|
|
226
|
+
// Clear stale rules from previously uninstalled plugins before re-installing: the files devflow did
|
|
227
|
+
// not ship move to the backup folder, and the rest of the folder is removed by the session's list.
|
|
228
|
+
// A rules folder that is a link is unlinked and replaced by a real folder, never emptied through,
|
|
229
|
+
// and the link is reported. Something that is not a folder stays and is refused below.
|
|
220
230
|
const links = newLinkReport();
|
|
221
|
-
|
|
231
|
+
if ((await removeRulesFolder(rulesTarget, backups)) === 'unlinked')
|
|
232
|
+
links.replacedLinks.push(rulesTarget);
|
|
222
233
|
// A link that could not be removed is still there, and `mkdir` and every rule write would follow it.
|
|
223
234
|
// Prove the folder first; a refused folder gets no rule, no manifest write and one refusal line.
|
|
224
235
|
const rulesFolderRefused = await refuseUnrealFolder(links, rulesTarget);
|
|
@@ -268,6 +279,7 @@ export async function runRulesEnable(claudeDir, devflowDir) {
|
|
|
268
279
|
manifest,
|
|
269
280
|
warn: warnConverge,
|
|
270
281
|
rulesEnabledOverride: !installFailed,
|
|
282
|
+
backups,
|
|
271
283
|
});
|
|
272
284
|
}
|
|
273
285
|
catch (err) {
|
|
@@ -325,17 +337,42 @@ export const rulesCommand = new Command('rules')
|
|
|
325
337
|
}
|
|
326
338
|
// No positional action — fall through to flag-based handling
|
|
327
339
|
if (options.enable) {
|
|
328
|
-
await
|
|
340
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
341
|
+
if (backups === null)
|
|
342
|
+
return;
|
|
343
|
+
await runRulesEnable(claudeDir, devflowDir, backups);
|
|
329
344
|
}
|
|
330
345
|
else if (options.disable) {
|
|
331
|
-
await
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
await
|
|
346
|
+
const backups = await openBackupSessionOrFail({ claudeDir, devflowDir });
|
|
347
|
+
if (backups === null)
|
|
348
|
+
return;
|
|
349
|
+
const removal = await runWithBackupReport(backups, async () => {
|
|
350
|
+
const outcome = await removeRulesFolder(rulesTarget, backups);
|
|
351
|
+
const manifest = await readManifest(devflowDir);
|
|
352
|
+
if (manifest) {
|
|
353
|
+
manifest.features.rules = false;
|
|
354
|
+
manifest.updatedAt = new Date().toISOString();
|
|
355
|
+
await writeManifest(devflowDir, manifest);
|
|
356
|
+
}
|
|
357
|
+
return outcome;
|
|
358
|
+
});
|
|
359
|
+
switch (removal) {
|
|
360
|
+
case 'kept':
|
|
361
|
+
p.log.warn(`Rules disabled — ${rulesTarget} stays, because it still holds files that devflow did not remove`);
|
|
362
|
+
break;
|
|
363
|
+
case 'refused':
|
|
364
|
+
p.log.warn(`Rules disabled. ${rulesFolderRefusedMessage(rulesTarget)}`);
|
|
365
|
+
break;
|
|
366
|
+
case 'absent':
|
|
367
|
+
case 'removed':
|
|
368
|
+
case 'unlinked':
|
|
369
|
+
p.log.success('Rules disabled — removed rules directory');
|
|
370
|
+
break;
|
|
371
|
+
default: {
|
|
372
|
+
const unreachable = removal;
|
|
373
|
+
void unreachable;
|
|
374
|
+
}
|
|
337
375
|
}
|
|
338
|
-
p.log.success('Rules disabled — removed rules directory');
|
|
339
376
|
}
|
|
340
377
|
else if (options.status) {
|
|
341
378
|
let installedFiles = [];
|