devflow-kit 3.8.0 → 3.10.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 +231 -0
- package/README.md +6 -6
- package/dist/agents/code.md +10 -10
- package/dist/agents/fix.md +7 -7
- package/dist/agents/git.md +2 -2
- package/dist/agents/harden.md +7 -6
- package/dist/agents/refactor.md +6 -6
- package/dist/agents/skim.md +1 -1
- package/dist/agents/triage.md +7 -0
- package/dist/cli/commands/ambient.js +10 -2
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +171 -24
- package/dist/cli/commands/curation.js +79 -23
- package/dist/cli/commands/gate-level-prompts.js +39 -23
- package/dist/cli/commands/gates.js +6 -7
- package/dist/cli/commands/init.js +150 -145
- package/dist/cli/commands/install-report.js +34 -0
- package/dist/cli/commands/legacy-sweep.js +64 -0
- package/dist/cli/commands/memory.js +59 -12
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +29 -12
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +354 -60
- package/dist/commands/bug-analysis.md +71 -122
- package/dist/commands/code-review.md +11 -11
- package/dist/commands/debug.md +2 -2
- package/dist/commands/dynamic-build.md +25 -25
- package/dist/commands/dynamic-plan.md +2 -2
- package/dist/commands/dynamic-profile.md +2 -2
- package/dist/commands/dynamic-tickets.md +2 -2
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +48 -34
- package/dist/commands/plan.md +2 -2
- package/dist/commands/release.md +3 -3
- package/dist/commands/research.md +2 -2
- package/dist/commands/resolve.md +50 -46
- package/dist/commands/self-review.md +10 -9
- package/dist/compliance-off/agents/git.md +120 -0
- package/dist/compliance-off/agents/triage.md +180 -0
- package/dist/compliance-off/commands/bug-analysis.md +398 -0
- package/dist/compliance-off/commands/code-review.md +520 -0
- package/dist/compliance-off/commands/dynamic-build.md +1251 -0
- package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
- package/dist/compliance-off/commands/implement.md +736 -0
- package/dist/compliance-off/commands/plan.md +671 -0
- package/dist/compliance-off/commands/release.md +309 -0
- package/dist/compliance-off/commands/resolve.md +742 -0
- package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
- package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
- package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
- package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
- package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
- package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
- package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
- package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
- package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
- package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
- package/dist/core/agent-models.js +19 -4
- package/dist/core/area-pause-notice.js +44 -0
- package/dist/core/assets.js +126 -17
- package/dist/core/compliance-off-files.js +27 -0
- package/dist/core/curation-move-skip.js +4 -5
- package/dist/core/curation-queue-cleanup.js +6 -5
- package/dist/core/curation-store.js +1 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/evidence-policy.js +10 -8
- package/dist/core/flags.js +2 -2
- package/dist/core/installed-variants.js +63 -0
- package/dist/core/manifest.js +48 -5
- package/dist/core/mds-variants.js +700 -3
- package/dist/core/orphan-sweep.js +11 -2
- package/dist/core/plugins.js +69 -12
- package/dist/core/tracked-areas.js +138 -14
- package/dist/curation-compliance-off/agents/triage.md +176 -0
- package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
- package/dist/curation-compliance-off/commands/code-review.md +481 -0
- package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
- package/dist/curation-compliance-off/commands/implement.md +689 -0
- package/dist/curation-compliance-off/commands/plan.md +631 -0
- package/dist/curation-compliance-off/commands/release.md +279 -0
- package/dist/curation-compliance-off/commands/resolve.md +692 -0
- package/dist/curation-off/agents/code.md +10 -10
- package/dist/curation-off/agents/fix.md +7 -7
- package/dist/curation-off/agents/harden.md +7 -6
- package/dist/curation-off/agents/refactor.md +6 -6
- package/dist/curation-off/agents/triage.md +7 -0
- package/dist/curation-off/commands/bug-analysis.md +69 -119
- package/dist/curation-off/commands/code-review.md +11 -11
- package/dist/curation-off/commands/debug.md +2 -2
- package/dist/curation-off/commands/dynamic-build.md +25 -25
- package/dist/curation-off/commands/explore.md +2 -2
- package/dist/curation-off/commands/implement.md +48 -34
- package/dist/curation-off/commands/plan.md +2 -2
- package/dist/curation-off/commands/release.md +1 -1
- package/dist/curation-off/commands/resolve.md +50 -46
- package/dist/curation-off/commands/self-review.md +10 -9
- package/dist/hud/components/curation-counts.js +27 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
- package/dist/skills/git/references/pr/post-review-summary.md +12 -11
- package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
- package/dist/skills/git/references/publication-gate.md +6 -7
- package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
- package/dist/targets/claude-code/behavior-install.js +23 -9
- package/dist/targets/claude-code/charter-render.js +374 -0
- package/dist/targets/claude-code/compliance-install.js +32 -12
- package/dist/targets/claude-code/compliance-off-files.js +245 -0
- package/dist/targets/claude-code/curation-install.js +570 -100
- package/dist/targets/claude-code/installer.js +467 -62
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/linked-folder.js +182 -0
- package/dist/targets/claude-code/reference-converge.js +118 -0
- package/dist/targets/claude-code/tracker-install.js +21 -8
- package/dist/targets/claude-code/tracker-servers.js +20 -6
- package/dist/targets/claude-code/workflow-folder-install.js +231 -0
- package/dist/variant-axes.json +109 -0
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +6 -6
- package/src/assets/agents/code.mds +4 -4
- package/src/assets/agents/evaluate.md +7 -13
- package/src/assets/agents/fix.mds +1 -1
- package/src/assets/agents/git.mds +34 -2
- package/src/assets/agents/harden.mds +7 -6
- package/src/assets/agents/skim.mds +1 -1
- package/src/assets/agents/test.md +21 -12
- package/src/assets/agents/triage.mds +9 -0
- package/src/assets/agents/validate.md +27 -34
- package/src/assets/commands/_partials/_compliance.mds +5 -3
- package/src/assets/commands/_partials/_decisions.mds +10 -8
- package/src/assets/commands/_partials/_engine.mds +9 -8
- package/src/assets/commands/_partials/_publication.mds +10 -1
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/bug-analysis.mds +80 -126
- package/src/assets/commands/code-review.mds +52 -9
- package/src/assets/commands/dynamic-build.mds +109 -15
- package/src/assets/commands/dynamic-tickets.mds +13 -0
- package/src/assets/commands/implement.mds +145 -33
- package/src/assets/commands/plan.mds +19 -0
- package/src/assets/commands/release.mds +50 -1
- package/src/assets/commands/resolve.mds +229 -44
- package/src/assets/commands/self-review.mds +8 -7
- package/src/assets/mds/git/_ops.mds +29 -1
- package/src/assets/mds/git/_ops_pr.mds +20 -10
- package/src/assets/mds/git/_ops_steps.mds +27 -0
- package/src/assets/mds/git/_pr.mds +15 -15
- package/src/assets/mds/git/_references.mds +12 -7
- package/src/assets/mds/tracker/_common.mds +4 -0
- package/src/assets/mds/tracker/_github.mds +43 -4
- package/src/assets/mds/tracker/_jira.mds +44 -4
- package/src/assets/mds/tracker/_linear.mds +44 -4
- package/src/assets/rules/quality.md +2 -0
- package/src/assets/scripts/ci-wait.cjs +493 -118
- package/src/assets/scripts/hooks/assets/implement-installed +1 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
- package/src/assets/scripts/hooks/background-memory-update +79 -1
- package/src/assets/scripts/hooks/json-helper.cjs +44 -2
- package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
- package/src/assets/scripts/hooks/memory-permission +10 -2
- package/src/assets/scripts/hooks/memory-worker +11 -0
- package/src/assets/scripts/hooks/pre-compact-memory +11 -0
- package/src/assets/scripts/hooks/preamble +18 -8
- package/src/assets/scripts/hooks/queue-append +218 -1
- package/src/assets/scripts/hooks/session-start-context +89 -37
- package/src/assets/scripts/hooks/session-start-memory +39 -0
- package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
- package/src/assets/scripts/pr-evidence.cjs +46 -4
- package/src/assets/scripts/resolve-settings.cjs +244 -30
- package/src/assets/scripts/verify-evidence.cjs +708 -124
- package/src/assets/skills/agent-references/SKILL.md +7 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
- package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
- package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -2
- package/src/assets/skills/git/SKILL.md +3 -3
- package/src/assets/skills/git/references/github-api.md +2 -139
- package/src/assets/skills/git/references/github-posting.md +158 -0
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +1 -1
- package/src/assets/skills/review-methodology/references/report-template.md +1 -2
- package/src/assets/skills/testing/SKILL.md +2 -4
|
@@ -7,8 +7,8 @@ import color from 'picocolors';
|
|
|
7
7
|
import { getInstallationPaths, getClaudeDirectory, getHomeDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
|
|
8
8
|
import { getGitRoot } from '../../core/git.js';
|
|
9
9
|
import { isSameLocation } from '../../core/same-location.js';
|
|
10
|
-
import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, buildAssetMaps, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, skillsOf, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
11
|
-
import { readManifest, removeManifestPlugins } from '../../core/manifest.js';
|
|
10
|
+
import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, WORKFLOW_FOLDER_NAMES, registeredPluginNames, buildAssetMaps, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, skillsOf, selectedWorkflowFolders, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
11
|
+
import { readManifest, readManifestPlugins, removeManifestPlugins } from '../../core/manifest.js';
|
|
12
12
|
import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
|
|
13
13
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
14
14
|
import { removeAmbientHook } from './ambient.js';
|
|
@@ -32,9 +32,13 @@ import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-clean
|
|
|
32
32
|
import { getPackageRoot, isContainedIn } from '../../core/paths.js';
|
|
33
33
|
import { firstSymbolicLink } from '../../core/linked-path.js';
|
|
34
34
|
import { MANIFEST_LOCK_NAME, MANIFEST_LOCK_STALE_MARK } from '../../core/manifest-lock.js';
|
|
35
|
-
import { CURATION_TUNING_FILE_NAME, LEGACY_NAMES, MOVE_JOURNAL_SUFFIX, MOVE_LOCK_PREFIX, MOVE_LOCK_SUFFIX } from '../../core/project-paths.js';
|
|
35
|
+
import { CURATION_DIR_NAME, CURATION_TUNING_FILE_NAME, LEGACY_NAMES, MOVE_JOURNAL_SUFFIX, MOVE_LOCK_PREFIX, MOVE_LOCK_SUFFIX, getMemoryDir } from '../../core/project-paths.js';
|
|
36
|
+
import { probeTrackedArea } from '../../core/tracked-areas.js';
|
|
36
37
|
import { restampInstalledCommands } from '../../targets/claude-code/language-stamp.js';
|
|
38
|
+
import { installRenderedCharter, removeHandoffSentinel } from '../../targets/claude-code/charter-render.js';
|
|
37
39
|
import { installCommandsAndAgents } from '../../targets/claude-code/installer.js';
|
|
40
|
+
import { isUnrealFolder, newLinkReport, refuseUnrealFolder, refusedFolderStatement } from '../../targets/claude-code/linked-folder.js';
|
|
41
|
+
import { formatReplacedLinksSummary } from './install-report.js';
|
|
38
42
|
/**
|
|
39
43
|
* Where a retired repo-local install lives: `<gitRoot>/.claude` and `<gitRoot>/.devflow`.
|
|
40
44
|
*
|
|
@@ -75,10 +79,121 @@ async function scopeInstallPaths(scope, gitRoot) {
|
|
|
75
79
|
return gitRoot === null ? null : legacyLocalInstallPaths(gitRoot);
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
78
|
-
* The user scope's guard
|
|
79
|
-
*
|
|
82
|
+
* The user scope's guard for a path that is not inside a Devflow install folder, and for the removal
|
|
83
|
+
* of a whole entry. It passes everything: `~/.claude` and `~/.devflow` and their other folders, the
|
|
84
|
+
* manifest and `settings.json` are the user's own, so a symbolic link there is followed. `fs.rm` of an
|
|
85
|
+
* entry that is a link removes the link and never what it points to, so a full uninstall removes a
|
|
86
|
+
* linked Devflow folder as it removes a real one.
|
|
87
|
+
*
|
|
88
|
+
* A read, write or removal inside a Devflow install folder never uses this guard: it uses
|
|
89
|
+
* {@link devflowFolderGuard}, which refuses a link.
|
|
80
90
|
*/
|
|
81
|
-
const
|
|
91
|
+
const changeUserOwned = async () => true;
|
|
92
|
+
/** The folders directly under `~/.claude` that hold a `devflow` folder of Devflow's own. */
|
|
93
|
+
const DEVFLOW_FILE_FOLDERS = ['commands', 'agents', 'rules'];
|
|
94
|
+
/**
|
|
95
|
+
* The Devflow install folder that holds `target` and every folder below it down to the parent of
|
|
96
|
+
* `target`, outermost first; null for a target outside every Devflow install folder. A target that is
|
|
97
|
+
* itself a Devflow install folder (a `skills/devflow:*` folder) gives that folder alone. PURE.
|
|
98
|
+
*
|
|
99
|
+
* The Devflow install folders are `commands/devflow`, `agents/devflow`, `rules/devflow` and each
|
|
100
|
+
* `skills/devflow:*` folder. `~/.claude`, `~/.claude/skills` and the other folders above them are
|
|
101
|
+
* not among them.
|
|
102
|
+
*/
|
|
103
|
+
function devflowFolderChain(claudeDir, target) {
|
|
104
|
+
const rel = path.relative(claudeDir, target);
|
|
105
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
106
|
+
return null;
|
|
107
|
+
const parts = rel.split(path.sep);
|
|
108
|
+
const inFileFolder = DEVFLOW_FILE_FOLDERS.includes(parts[0]) && parts[1] === 'devflow';
|
|
109
|
+
const inSkillFolder = parts[0] === 'skills' && parts[1] !== undefined && parts[1].startsWith(SKILL_NAMESPACE);
|
|
110
|
+
if (!inFileFolder && !inSkillFolder)
|
|
111
|
+
return null;
|
|
112
|
+
const chain = [path.join(claudeDir, parts[0], parts[1])];
|
|
113
|
+
for (let depth = 2; depth < parts.length - 1; depth++) {
|
|
114
|
+
chain.push(path.join(chain[chain.length - 1], parts[depth]));
|
|
115
|
+
}
|
|
116
|
+
return chain;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The guard for a change inside the Devflow install folders of `claudeDir`.
|
|
120
|
+
*
|
|
121
|
+
* Devflow never reads, writes or removes through a link at or below its own install folders. A
|
|
122
|
+
* target is let through only when each Devflow folder on the way to it is a real folder or absent;
|
|
123
|
+
* a folder that is a link, or not a folder, refuses the target. The refusal is recorded once in
|
|
124
|
+
* `report.refusedFolders` and the link is left as it was. A target outside every Devflow install
|
|
125
|
+
* folder passes, and a link to a file that is the target itself is not followed (`fs.rm` removes
|
|
126
|
+
* the link). Never throws.
|
|
127
|
+
*/
|
|
128
|
+
function devflowFolderGuard(claudeDir, report) {
|
|
129
|
+
return async (target) => {
|
|
130
|
+
const chain = devflowFolderChain(claudeDir, target);
|
|
131
|
+
if (chain === null)
|
|
132
|
+
return true;
|
|
133
|
+
for (const folder of chain) {
|
|
134
|
+
if (report.refusedFolders.includes(folder))
|
|
135
|
+
return false;
|
|
136
|
+
if (await refuseUnrealFolder(report, folder))
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Turn the refused-folder half of an uninstall into summary lines, one per folder.
|
|
144
|
+
*
|
|
145
|
+
* An uninstall removes and changes nothing at a path that is a link, or not a folder where it needs
|
|
146
|
+
* one, and leaves the entry as it was. That looks the same on disk as an uninstall that found nothing
|
|
147
|
+
* to remove, so the line says what was skipped and what to do about it.
|
|
148
|
+
*
|
|
149
|
+
* Pure function — returns lines, logs nothing.
|
|
150
|
+
*/
|
|
151
|
+
export function formatUninstallRefusedSummary(report) {
|
|
152
|
+
return report.refusedFolders.map(folder => ({
|
|
153
|
+
level: 'warn',
|
|
154
|
+
message: `${refusedFolderStatement(folder)} Devflow removed and changed nothing there. ` +
|
|
155
|
+
`Remove the link or replace it with a real folder, then run the uninstall again.`,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** How many folders an uninstall left as they are, in a sentence that agrees with the count. */
|
|
159
|
+
function leftAsTheyAre(count) {
|
|
160
|
+
return count === 1 ? '1 folder was left as it is' : `${count} folders were left as they are`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The line for one scope's removal. With no refused folder the plugin is removed. With any, the
|
|
164
|
+
* removal is partial: the line says so and counts the folders left as they are, because a refused
|
|
165
|
+
* folder looks the same on disk as a folder that had nothing to remove.
|
|
166
|
+
*
|
|
167
|
+
* @param pluginLabel - Empty, or the selected plugin names as ` (a, b)`.
|
|
168
|
+
*
|
|
169
|
+
* Pure function — returns the line, logs nothing.
|
|
170
|
+
*/
|
|
171
|
+
export function formatPluginRemovedLine(pluginLabel, refusedCount) {
|
|
172
|
+
if (refusedCount === 0)
|
|
173
|
+
return { level: 'success', message: `Plugin removed${pluginLabel}` };
|
|
174
|
+
return { level: 'warn', message: `Plugin removed in part${pluginLabel}: ${leftAsTheyAre(refusedCount)}.` };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The last line of an uninstall. It says success only when no folder was refused in any scope;
|
|
178
|
+
* otherwise it says the uninstall was done in part and counts the folders left as they are.
|
|
179
|
+
*
|
|
180
|
+
* Pure function — returns the line, logs nothing.
|
|
181
|
+
*/
|
|
182
|
+
export function formatUninstallOutro(refusedCount) {
|
|
183
|
+
if (refusedCount === 0) {
|
|
184
|
+
return `${color.green('Devflow uninstalled successfully')}${color.dim(' Reinstall: npx devflow-kit init')}`;
|
|
185
|
+
}
|
|
186
|
+
return color.yellow(`Devflow uninstalled in part: ${leftAsTheyAre(refusedCount)}.`);
|
|
187
|
+
}
|
|
188
|
+
/** Print the links an uninstall replaced or refused, one line each, once per path. */
|
|
189
|
+
function logLinkReport(report) {
|
|
190
|
+
const lines = [
|
|
191
|
+
...formatReplacedLinksSummary({ replacedLinks: [...new Set(report.replacedLinks)] }),
|
|
192
|
+
...formatUninstallRefusedSummary({ refusedFolders: [...new Set(report.refusedFolders)] }),
|
|
193
|
+
];
|
|
194
|
+
for (const line of lines)
|
|
195
|
+
p.log[line.level](line.message);
|
|
196
|
+
}
|
|
82
197
|
/**
|
|
83
198
|
* `folder` and every path below it down to `target`, the target included, outermost
|
|
84
199
|
* first, for whichever of the two folders holds `target`; null for a target in
|
|
@@ -139,11 +254,17 @@ function legacyLocalChangeGuard(folders, skipped) {
|
|
|
139
254
|
return false;
|
|
140
255
|
};
|
|
141
256
|
}
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
257
|
+
/**
|
|
258
|
+
* The guard for one scope's full or selective phase.
|
|
259
|
+
*
|
|
260
|
+
* A legacy local install is checked for a link anywhere on the way to a target. In the user scope the
|
|
261
|
+
* full phase removes whole entries and passes ({@link changeUserOwned}). The selective phase gives
|
|
262
|
+
* its `links` report and gets {@link devflowFolderGuard}, which refuses a linked Devflow folder.
|
|
263
|
+
*/
|
|
264
|
+
function scopeChangeGuard(scope, folders, links) {
|
|
265
|
+
if (scope === 'local')
|
|
266
|
+
return legacyLocalChangeGuard(folders, 'Legacy local install not fully removed');
|
|
267
|
+
return links === undefined ? changeUserOwned : devflowFolderGuard(folders.claudeDir, links);
|
|
147
268
|
}
|
|
148
269
|
/**
|
|
149
270
|
* The plugins the manifest records as installed, as registry definitions.
|
|
@@ -163,6 +284,24 @@ export async function resolveInstalledPlugins(devflowDir) {
|
|
|
163
284
|
const resolved = DEVFLOW_PLUGINS.filter(plugin => names.has(plugin.name));
|
|
164
285
|
return resolved.length === 0 ? DEVFLOW_PLUGINS : resolved;
|
|
165
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* The plugin names the orchestrator charter is re-rendered for after `uninstall --plugin`, or null when there
|
|
289
|
+
* is no manifest record to render from. Pure.
|
|
290
|
+
*
|
|
291
|
+
* D-CHARTER-INSTALL-RENDER: the set is the manifest record (`readManifestPlugins`) minus the plugins being
|
|
292
|
+
* removed, with a legacy name mapped and only names this registry has kept ({@link registeredPluginNames}).
|
|
293
|
+
* It is deliberately NOT {@link resolveInstalledPlugins}: that falls back to the whole registry when the
|
|
294
|
+
* manifest is missing or names nothing, which is the safe direction for retaining assets, and the wrong one
|
|
295
|
+
* here. The registry would render a charter that routes to every workflow on a machine that installed none
|
|
296
|
+
* of them. With no record the caller leaves the installed charter as it is and takes only the sentinel away.
|
|
297
|
+
* A record that lists no plugin is a record of nothing installed, so it yields the empty set rather than null.
|
|
298
|
+
*/
|
|
299
|
+
export function charterPluginsAfterRemoval(recordedPlugins, removedPluginNames) {
|
|
300
|
+
if (recordedPlugins === null)
|
|
301
|
+
return null;
|
|
302
|
+
const removed = new Set(removedPluginNames);
|
|
303
|
+
return registeredPluginNames(recordedPlugins).filter(name => !removed.has(name));
|
|
304
|
+
}
|
|
166
305
|
/**
|
|
167
306
|
* Compute which assets should be removed during selective plugin uninstall.
|
|
168
307
|
* Skills and agents shared by remaining plugins are retained.
|
|
@@ -182,8 +321,11 @@ export async function resolveInstalledPlugins(devflowDir) {
|
|
|
182
321
|
* registry only when there is no manifest to read, and the dry-run and the
|
|
183
322
|
* real removal must always be given the SAME list or they describe different
|
|
184
323
|
* outcomes.
|
|
324
|
+
* @param folderWorkflows - The target-layer set (D-WORKFLOW-FOLDER-INSTALL). A folder
|
|
325
|
+
* workflow of a selected plugin is a skill to remove, unless a remaining plugin
|
|
326
|
+
* owns it too, and never a command: it has no command file.
|
|
185
327
|
*/
|
|
186
|
-
export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
328
|
+
export function computeAssetsToRemove(selectedPlugins, installedPlugins, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
187
329
|
const selectedNames = new Set(selectedPlugins.map(p => p.name));
|
|
188
330
|
const remainingPlugins = installedPlugins.filter(p => !selectedNames.has(p.name));
|
|
189
331
|
const retainedSkills = skillsOf(remainingPlugins);
|
|
@@ -203,6 +345,11 @@ export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
|
203
345
|
if (!retainedSkills.has(skill))
|
|
204
346
|
skills.push(skill);
|
|
205
347
|
}
|
|
348
|
+
const retainedFolders = selectedWorkflowFolders(remainingPlugins, folderWorkflows);
|
|
349
|
+
for (const name of selectedWorkflowFolders(selectedPlugins, folderWorkflows)) {
|
|
350
|
+
if (!retainedFolders.has(name))
|
|
351
|
+
skills.push(name);
|
|
352
|
+
}
|
|
206
353
|
for (const plugin of selectedPlugins) {
|
|
207
354
|
for (const agent of plugin.agents) {
|
|
208
355
|
if (!retainedAgents.has(agent))
|
|
@@ -212,7 +359,7 @@ export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
|
212
359
|
if (!retainedRules.has(rule))
|
|
213
360
|
rules.push(rule);
|
|
214
361
|
}
|
|
215
|
-
commands.push(...plugin.commands);
|
|
362
|
+
commands.push(...plugin.commands.filter(cmd => !folderWorkflows.has(cmd.replace(/^\//, ''))));
|
|
216
363
|
}
|
|
217
364
|
return { skills, agents, commands, rules };
|
|
218
365
|
}
|
|
@@ -285,20 +432,66 @@ const TRACKED_ENTRIES = DEVFLOW_TRACKED_PATHS.map((tracked) => ({
|
|
|
285
432
|
name: tracked.replace(/\/$/, ''),
|
|
286
433
|
isDir: tracked.endsWith('/'),
|
|
287
434
|
}));
|
|
435
|
+
/** The memory folder's name under `.devflow/`, by the name the path helper gives it. */
|
|
436
|
+
const MEMORY_FOLDER_NAME = path.basename(getMemoryDir(path.sep));
|
|
437
|
+
/**
|
|
438
|
+
* The data area an entry under `.devflow/` belongs to, or null. The curation area holds the
|
|
439
|
+
* curation folder, the old ledger folder and the backup folders; the memory area holds the
|
|
440
|
+
* memory folder. Decided by name alone, whatever the kind of entry, and without regard to
|
|
441
|
+
* ASCII letter case, as the tracked-areas check matches them.
|
|
442
|
+
*/
|
|
443
|
+
function projectDataAreaOf(name) {
|
|
444
|
+
const lower = name.toLowerCase();
|
|
445
|
+
if (lower === CURATION_DIR_NAME || lower === LEGACY_NAMES.folder || lower.startsWith(LEGACY_NAMES.backupPrefix))
|
|
446
|
+
return 'curation';
|
|
447
|
+
if (lower === MEMORY_FOLDER_NAME)
|
|
448
|
+
return 'memory';
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
288
451
|
/**
|
|
289
452
|
* Split the entries under a repository's `.devflow/` into what a confirmed cleanup
|
|
290
453
|
* removes and what it keeps (D-UNINSTALL-CARVE-OUT). An entry is kept only when both
|
|
291
454
|
* its name and its kind match a tracked path, so a stray FILE named `features` is
|
|
292
|
-
* not mistaken for the tracked directory.
|
|
455
|
+
* not mistaken for the tracked directory. An entry of a paused data area (`pausedAreas`:
|
|
456
|
+
* git tracks files in the area's folders, or did not answer) is kept too, in
|
|
457
|
+
* `keepPaused`. PURE — all three lists sorted by name.
|
|
293
458
|
*/
|
|
294
|
-
export function partitionProjectData(entries) {
|
|
459
|
+
export function partitionProjectData(entries, pausedAreas = new Set()) {
|
|
295
460
|
const isTracked = (entry) => TRACKED_ENTRIES.some((t) => t.name === entry.name && t.isDir === entry.isDir);
|
|
461
|
+
const isPaused = (entry) => {
|
|
462
|
+
const area = projectDataAreaOf(entry.name);
|
|
463
|
+
return area !== null && pausedAreas.has(area);
|
|
464
|
+
};
|
|
296
465
|
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
297
466
|
return {
|
|
298
|
-
remove: entries.filter((e) => !isTracked(e)).sort(byName),
|
|
467
|
+
remove: entries.filter((e) => !isTracked(e) && !isPaused(e)).sort(byName),
|
|
299
468
|
keep: entries.filter(isTracked).sort(byName),
|
|
469
|
+
keepPaused: entries.filter((e) => !isTracked(e) && isPaused(e)).sort(byName),
|
|
300
470
|
};
|
|
301
471
|
}
|
|
472
|
+
/** The check's own answer, from the package's own copy of it. */
|
|
473
|
+
const probeProjectDataArea = async (root, area) => (await probeTrackedArea(root, area)).answer;
|
|
474
|
+
/**
|
|
475
|
+
* The data areas that hold an entry in `entries` and are paused at `gitRoot`: git tracks
|
|
476
|
+
* files in the area's folders, or a `.git` entry stands and git did not answer. An area
|
|
477
|
+
* with no entry is not asked, and a root with no `.git` entry makes no git call. A probe
|
|
478
|
+
* that fails to answer counts as `unknown`, so the area is kept.
|
|
479
|
+
*/
|
|
480
|
+
async function pausedProjectDataAreas(gitRoot, entries, probe) {
|
|
481
|
+
const present = new Set(entries.flatMap((entry) => {
|
|
482
|
+
const area = projectDataAreaOf(entry.name);
|
|
483
|
+
return area === null ? [] : [area];
|
|
484
|
+
}));
|
|
485
|
+
const answers = await Promise.all([...present].map(async (area) => {
|
|
486
|
+
try {
|
|
487
|
+
return [area, await probe(gitRoot, area)];
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return [area, 'unknown'];
|
|
491
|
+
}
|
|
492
|
+
}));
|
|
493
|
+
return new Set(answers.flatMap(([area, answer]) => (answer === 'untracked' ? [] : [area])));
|
|
494
|
+
}
|
|
302
495
|
/**
|
|
303
496
|
* Resolve the project-data step's target: `<gitRoot>/.devflow`, partitioned.
|
|
304
497
|
*
|
|
@@ -312,6 +505,12 @@ export function partitionProjectData(entries) {
|
|
|
312
505
|
* A `.devflow` that is a symbolic link to anywhere else is skipped too, and never
|
|
313
506
|
* followed: its target is a directory devflow cannot prove it wrote, so a
|
|
314
507
|
* confirmed cleanup must not empty it — nor unlink a link the user made.
|
|
508
|
+
*
|
|
509
|
+
* A folder that git tracks is the repository's content, not devflow's data. The
|
|
510
|
+
* curation area (the curation folder, the old ledger folder and the backup folders)
|
|
511
|
+
* and the memory area (the memory folder) are each asked of the tracked-areas check at
|
|
512
|
+
* the git root, when an entry of the area stands. An area that git tracks files in, or
|
|
513
|
+
* that git did not answer for, moves its entries from the removal list to `keepPaused`.
|
|
315
514
|
*/
|
|
316
515
|
export async function resolveProjectDataPlan(opts) {
|
|
317
516
|
if (opts.gitRoot === null)
|
|
@@ -335,8 +534,10 @@ export async function resolveProjectDataPlan(opts) {
|
|
|
335
534
|
catch {
|
|
336
535
|
return { kind: 'skip', reason: 'absent' };
|
|
337
536
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
537
|
+
const entries = dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() }));
|
|
538
|
+
const paused = await pausedProjectDataAreas(opts.gitRoot, entries, opts.probe ?? probeProjectDataArea);
|
|
539
|
+
const { remove, keep, keepPaused } = partitionProjectData(entries, paused);
|
|
540
|
+
return { kind: 'plan', plan: { dir, remove, keep, keepPaused } };
|
|
340
541
|
}
|
|
341
542
|
/**
|
|
342
543
|
* The line that tells the user why the project-data step was skipped, or null
|
|
@@ -366,15 +567,21 @@ function entryLabel(entry) {
|
|
|
366
567
|
function entryList(entries) {
|
|
367
568
|
return entries.length === 0 ? '(none)' : entries.map(entryLabel).join(', ');
|
|
368
569
|
}
|
|
570
|
+
/** Why the entries of a data area stay, said beside a list of them. */
|
|
571
|
+
const KEEP_PAUSED_REASON = 'git tracks them, or git did not answer';
|
|
572
|
+
/** Why one entry of a data area stays, said beside it. */
|
|
573
|
+
const KEEP_PAUSED_REASON_ONE = 'git tracks it, or git did not answer';
|
|
369
574
|
/**
|
|
370
575
|
* The lines shown before the project-data confirm: what is removed and what is
|
|
371
|
-
* kept
|
|
576
|
+
* kept, and, only where there are such entries, what is kept because git tracks them
|
|
577
|
+
* or did not answer. PURE.
|
|
372
578
|
*/
|
|
373
579
|
export function formatProjectDataPlan(plan) {
|
|
374
580
|
return [
|
|
375
581
|
`Project data in ${plan.dir}/:`,
|
|
376
582
|
` Remove: ${entryList(plan.remove)}`,
|
|
377
583
|
` Keep (shared via git): ${entryList(plan.keep)}`,
|
|
584
|
+
...(plan.keepPaused.length === 0 ? [] : [` Keep (${KEEP_PAUSED_REASON}): ${entryList(plan.keepPaused)}`]),
|
|
378
585
|
];
|
|
379
586
|
}
|
|
380
587
|
/**
|
|
@@ -724,7 +931,7 @@ export function installArtifactPaths(devflowDir) {
|
|
|
724
931
|
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
725
932
|
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
726
933
|
*/
|
|
727
|
-
export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange =
|
|
934
|
+
export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange = changeUserOwned) {
|
|
728
935
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
729
936
|
if (await mayChange(manifestPath)) {
|
|
730
937
|
try {
|
|
@@ -832,7 +1039,7 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
832
1039
|
* @param claudeDir - Target Claude Code directory (e.g. ~/.claude)
|
|
833
1040
|
* @param devflowDir - Devflow data directory (e.g. ~/.devflow)
|
|
834
1041
|
*/
|
|
835
|
-
export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
1042
|
+
export async function enumerateDryRunExtras(claudeDir, devflowDir, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
836
1043
|
const extras = [];
|
|
837
1044
|
// 1. Whole directories removed by removeAllDevFlow
|
|
838
1045
|
for (const dir of [
|
|
@@ -854,7 +1061,8 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
854
1061
|
// dirs are by construction foreign to Devflow.
|
|
855
1062
|
// FEATURE_OWNED_SKILLS (compliance) is included so full uninstall removes
|
|
856
1063
|
// devflow:compliance even though it left the plugin registry (step 1.5).
|
|
857
|
-
|
|
1064
|
+
// The workflow skill folders (D-WORKFLOW-FOLDER-INSTALL) join through getAllSkillNames.
|
|
1065
|
+
const prefixedSkillNames = new Set([...getAllSkillNames(folderWorkflows), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
|
|
858
1066
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
859
1067
|
for (const skillName of prefixedSkillNames) {
|
|
860
1068
|
const prefixedPath = path.join(skillsDir, prefixSkillName(skillName));
|
|
@@ -946,6 +1154,9 @@ export async function runDryRunPhase(opts) {
|
|
|
946
1154
|
for (const entry of projectData.plan.remove) {
|
|
947
1155
|
extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (if confirmed)`);
|
|
948
1156
|
}
|
|
1157
|
+
for (const entry of projectData.plan.keepPaused) {
|
|
1158
|
+
extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (kept: ${KEEP_PAUSED_REASON_ONE})`);
|
|
1159
|
+
}
|
|
949
1160
|
}
|
|
950
1161
|
else {
|
|
951
1162
|
const skipped = formatProjectDataSkip(projectData.reason, projectRoot);
|
|
@@ -972,13 +1183,17 @@ export async function runDryRunPhase(opts) {
|
|
|
972
1183
|
* settings.json, so the notice tells the user to run `devflow init` for those.
|
|
973
1184
|
*
|
|
974
1185
|
* Never runs in a legacy local scope (D-CLI-NO-SYMLINK): a repository can commit a command
|
|
975
|
-
* file as a symbolic link, and the copy writes through it.
|
|
976
|
-
*
|
|
977
|
-
*
|
|
1186
|
+
* file as a symbolic link, and the copy writes through it. `devflow init` can not update a
|
|
1187
|
+
* legacy local install either, so the warning tells the user to remove it in full.
|
|
1188
|
+
*
|
|
1189
|
+
* In the user scope the copy writes into neither folder when either is a link or not a folder,
|
|
1190
|
+
* whatever the caller's guard says: writing only one folder would pair old commands with new
|
|
1191
|
+
* agents. The refused folder is recorded in `links`. A command or agent file that is a link is
|
|
1192
|
+
* replaced by a real file and recorded in `links` too; what it pointed to is left as it was.
|
|
978
1193
|
*
|
|
979
1194
|
* Skipped, with a warning, when there is no readable manifest or it lists no known plugin (the
|
|
980
|
-
* retained set would be a guess), or when a folder is behind a link the
|
|
981
|
-
* refuses. Those warnings name `devflow init` as the remedy. When the copy fails, it stops
|
|
1195
|
+
* retained set would be a guess), or when a folder is a link, not a folder, or behind a link the
|
|
1196
|
+
* caller's guard refuses. Those warnings name `devflow init` as the remedy. When the copy fails, it stops
|
|
982
1197
|
* at the first failure: files it wrote before then stay, and the warning names the same
|
|
983
1198
|
* remedy. Never throws: an uninstall must finish.
|
|
984
1199
|
* Agent model lines are reverted again after the copy, as the selective phase does before
|
|
@@ -989,7 +1204,8 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
989
1204
|
const { claudeDir, devflowDir, retained, retiredAgents } = opts;
|
|
990
1205
|
const scope = opts.scope ?? 'user';
|
|
991
1206
|
const verbose = opts.verbose ?? false;
|
|
992
|
-
const mayChange = opts.mayChange ??
|
|
1207
|
+
const mayChange = opts.mayChange ?? changeUserOwned;
|
|
1208
|
+
const links = opts.links ?? newLinkReport();
|
|
993
1209
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
994
1210
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
995
1211
|
const retired = `Removed the agent file(s) of an older Devflow release: ${retiredAgents.join(', ')}.`;
|
|
@@ -1016,7 +1232,14 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1016
1232
|
p.log.warn(`${retired} The manifest lists no known plugin, so Devflow did not rewrite the remaining files. ${remedy}`);
|
|
1017
1233
|
return;
|
|
1018
1234
|
}
|
|
1019
|
-
|
|
1235
|
+
let blocked = !(await mayChange(agentsDir)) || !(await mayChange(commandsDir));
|
|
1236
|
+
if (!blocked) {
|
|
1237
|
+
// The caller's guard may pass everything. Neither folder is written when it is a link or not a folder.
|
|
1238
|
+
const agentsRefused = await refuseUnrealFolder(links, agentsDir);
|
|
1239
|
+
const commandsRefused = await refuseUnrealFolder(links, commandsDir);
|
|
1240
|
+
blocked = agentsRefused || commandsRefused;
|
|
1241
|
+
}
|
|
1242
|
+
if (blocked) {
|
|
1020
1243
|
p.log.warn(`${retired} A linked folder blocks the rewrite. ${remedy}`);
|
|
1021
1244
|
return;
|
|
1022
1245
|
}
|
|
@@ -1026,7 +1249,9 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1026
1249
|
agentsMap: buildAssetMaps(retained).agentsMap,
|
|
1027
1250
|
claudeDir,
|
|
1028
1251
|
curation: manifest.features.curation,
|
|
1252
|
+
compliance: manifest.features.compliance.enabled,
|
|
1029
1253
|
trackerServers: effectiveTrackerServers(manifest.features.tracker),
|
|
1254
|
+
links,
|
|
1030
1255
|
warn: (msg) => p.log.warn(msg),
|
|
1031
1256
|
});
|
|
1032
1257
|
}
|
|
@@ -1041,6 +1266,30 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1041
1266
|
catch { /* the revert is best effort, as in the selective phase */ }
|
|
1042
1267
|
p.log.info(`${retired} Devflow wrote the current commands and agents of the plugins that stay installed. Run \`devflow init\` to update the hooks and scripts.`);
|
|
1043
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Re-render the installed orchestrator charter for the plugins that stay after a selective removal, so it
|
|
1271
|
+
* stops routing to a workflow whose skill was just removed, and take the implement sentinel away when
|
|
1272
|
+
* implement is among the removed.
|
|
1273
|
+
*
|
|
1274
|
+
* D-CHARTER-INSTALL-RENDER: the set comes from the MANIFEST record, read without a heal so the file stays
|
|
1275
|
+
* byte-identical, which still lists the removed plugins until the caller drops the record, never from the
|
|
1276
|
+
* registry fallback of {@link resolveInstalledPlugins}. The record is read only where the change guard lets
|
|
1277
|
+
* a change through. With no usable record there is nothing to render from and the installed charter stays
|
|
1278
|
+
* as it is, but the sentinel needs no record and goes when implement is among the removed. An uninstall
|
|
1279
|
+
* never creates a file, so a machine whose charter is not installed gets none. A failure is a warning,
|
|
1280
|
+
* never a failure of the uninstall.
|
|
1281
|
+
*/
|
|
1282
|
+
async function rerenderCharterAfterRemoval(devflowDir, removedPluginNames, mayChange) {
|
|
1283
|
+
const scriptsDir = path.join(devflowDir, 'scripts');
|
|
1284
|
+
const warn = (msg) => p.log.warn(msg);
|
|
1285
|
+
const recorded = (await mayChange(path.join(devflowDir, 'manifest.json'))) ? await readManifestPlugins(devflowDir) : null;
|
|
1286
|
+
const retained = charterPluginsAfterRemoval(recorded, removedPluginNames);
|
|
1287
|
+
if (retained === null) {
|
|
1288
|
+
await removeHandoffSentinel({ scriptsDir, removedPlugins: removedPluginNames, warn, mayChange });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
await installRenderedCharter({ scriptsDir, plugins: retained, warn, mayChange, mode: 'uninstall' });
|
|
1292
|
+
}
|
|
1044
1293
|
/**
|
|
1045
1294
|
* SELECTIVE PHASE (per scope): revert agent model overrides, remove the
|
|
1046
1295
|
* selected plugins' assets, and clean up the ambient hook if the ambient
|
|
@@ -1050,13 +1299,19 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1050
1299
|
* @param opts.devflowDir - Devflow data directory for this scope.
|
|
1051
1300
|
* @param opts.selectedPlugins - Plugin subset to remove.
|
|
1052
1301
|
* @param opts.verbose - Whether to emit verbose log lines.
|
|
1302
|
+
*
|
|
1303
|
+
* Nothing is read, written or removed through a link at or below a Devflow install folder. A
|
|
1304
|
+
* Devflow folder that is a link, or not a folder, is left as it was; the phase prints one line for
|
|
1305
|
+
* each and returns them, with the file links it replaced, in the {@link LinkReport}.
|
|
1053
1306
|
*/
|
|
1054
1307
|
export async function runSelectivePhaseForScope(opts) {
|
|
1055
1308
|
const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
|
|
1056
1309
|
const scope = opts.scope ?? 'user';
|
|
1057
1310
|
const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
|
|
1058
|
-
|
|
1059
|
-
|
|
1311
|
+
const links = newLinkReport();
|
|
1312
|
+
// D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first. In the user
|
|
1313
|
+
// scope the guard refuses a linked Devflow folder and records it in `links`.
|
|
1314
|
+
const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir }, links);
|
|
1060
1315
|
// Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
|
|
1061
1316
|
// lines from installed agent frontmatter while the files are still present.
|
|
1062
1317
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
@@ -1075,7 +1330,9 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1075
1330
|
}
|
|
1076
1331
|
catch { /* agents dir absent or revert failed — non-fatal */ }
|
|
1077
1332
|
}
|
|
1078
|
-
|
|
1333
|
+
// In the user scope the removal builds its own guard over the same report, which also checks each
|
|
1334
|
+
// orphan it sweeps; a legacy local install keeps the guard above.
|
|
1335
|
+
const sweeps = await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins, scope === 'local' ? mayChange : undefined, links);
|
|
1079
1336
|
// A retired agent file means the install is from an older release, and the commands of the
|
|
1080
1337
|
// plugins that stay installed still spawn the name that was just removed. Write their
|
|
1081
1338
|
// current commands and agents so the install stays whole. Runs before the language stamp
|
|
@@ -1090,6 +1347,7 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1090
1347
|
scope,
|
|
1091
1348
|
verbose,
|
|
1092
1349
|
mayChange,
|
|
1350
|
+
links,
|
|
1093
1351
|
});
|
|
1094
1352
|
}
|
|
1095
1353
|
// D-LANGUAGE-FOCUS-STAMP: re-stamp the installed /code-review from the selection that REMAINS.
|
|
@@ -1108,6 +1366,9 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1108
1366
|
mayChange,
|
|
1109
1367
|
});
|
|
1110
1368
|
}
|
|
1369
|
+
// D-CHARTER-INSTALL-RENDER: the charter stops routing to the removed workflows. It reads the manifest
|
|
1370
|
+
// record, so it runs before the record is dropped below.
|
|
1371
|
+
await rerenderCharterAfterRemoval(devflowDir, selectedPlugins.map(sp => sp.name), mayChange);
|
|
1111
1372
|
// D-UNINSTALL-DROPS-PLUGIN: the manifest is the install record `devflow init` seeds its plugin
|
|
1112
1373
|
// selection from (a re-init keeps the prior selection), so a plugin removed here but still listed
|
|
1113
1374
|
// would be installed again by the next plain init. Take the selected names off the list;
|
|
@@ -1140,6 +1401,8 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1140
1401
|
}
|
|
1141
1402
|
catch { /* settings.json may not exist */ }
|
|
1142
1403
|
}
|
|
1404
|
+
logLinkReport(links);
|
|
1405
|
+
return links;
|
|
1143
1406
|
}
|
|
1144
1407
|
/**
|
|
1145
1408
|
* FULL PHASE (per scope): revert agent model overrides, remove all Devflow
|
|
@@ -1167,11 +1430,13 @@ export async function runFullPhaseForScope(opts) {
|
|
|
1167
1430
|
// Revert GPT agent frontmatter before removing agents — ensures no orphaned
|
|
1168
1431
|
// GPT model lines remain if agents dir is preserved by a later partial flow.
|
|
1169
1432
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
1433
|
+
// The revert rewrites the files in the folder, so a folder that is a link is not reverted: the
|
|
1434
|
+
// removal below takes the link away whole and the files it led to stay as they are.
|
|
1170
1435
|
{
|
|
1171
1436
|
const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1172
1437
|
try {
|
|
1173
1438
|
await fs.access(agentsInstallDir);
|
|
1174
|
-
if (await mayChange(agentsInstallDir)) {
|
|
1439
|
+
if (await mayChange(agentsInstallDir) && !(await isUnrealFolder(agentsInstallDir))) {
|
|
1175
1440
|
await revertExternalAgents({
|
|
1176
1441
|
installDir: agentsInstallDir,
|
|
1177
1442
|
devflowDir,
|
|
@@ -1251,7 +1516,9 @@ export async function runFullPhaseForScope(opts) {
|
|
|
1251
1516
|
async function runProjectDataStep(plan, gates) {
|
|
1252
1517
|
const shown = `${plan.dir}/`;
|
|
1253
1518
|
if (plan.remove.length === 0) {
|
|
1254
|
-
p.log.info(
|
|
1519
|
+
p.log.info(plan.keepPaused.length === 0
|
|
1520
|
+
? `${shown} preserved (holds only files shared via git)`
|
|
1521
|
+
: `${shown} preserved (holds only files shared via git, and ${entryList(plan.keepPaused)}, which stay: ${KEEP_PAUSED_REASON})`);
|
|
1255
1522
|
return;
|
|
1256
1523
|
}
|
|
1257
1524
|
if (gates.keepDocs) {
|
|
@@ -1287,7 +1554,7 @@ async function runProjectDataStep(plan, gates) {
|
|
|
1287
1554
|
failed.push(`${entryLabel(entry)} (${err.code ?? err.message})`);
|
|
1288
1555
|
}
|
|
1289
1556
|
}
|
|
1290
|
-
if (plan.keep.length === 0 && failed.length === 0) {
|
|
1557
|
+
if (plan.keep.length === 0 && plan.keepPaused.length === 0 && failed.length === 0) {
|
|
1291
1558
|
// Nothing tracked was there: drop the now-empty directory. rmdir refuses a
|
|
1292
1559
|
// directory something wrote into meanwhile, which is then left in place.
|
|
1293
1560
|
await fs.rmdir(plan.dir).catch(() => undefined);
|
|
@@ -1298,6 +1565,8 @@ async function runProjectDataStep(plan, gates) {
|
|
|
1298
1565
|
p.log.warn(`Could not remove from ${shown}: ${failed.join(', ')}`);
|
|
1299
1566
|
if (plan.keep.length > 0)
|
|
1300
1567
|
p.log.info(`Kept: ${entryList(plan.keep)}`);
|
|
1568
|
+
if (plan.keepPaused.length > 0)
|
|
1569
|
+
p.log.info(`Kept: ${entryList(plan.keepPaused)} (${KEEP_PAUSED_REASON})`);
|
|
1301
1570
|
}
|
|
1302
1571
|
/**
|
|
1303
1572
|
* CLEANUP PHASE: post-loop extras run only on full uninstall.
|
|
@@ -1413,7 +1682,7 @@ export async function runCleanupPhase(opts) {
|
|
|
1413
1682
|
// where no symbolic link leads it out of `<gitRoot>/.claude`.
|
|
1414
1683
|
const mayChange = scope === 'local'
|
|
1415
1684
|
? legacyLocalChangeGuard(paths, 'settings.json not edited')
|
|
1416
|
-
:
|
|
1685
|
+
: changeUserOwned;
|
|
1417
1686
|
if (settingsContent !== originalContent && await mayChange(settingsPath)) {
|
|
1418
1687
|
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
1419
1688
|
if (verbose) {
|
|
@@ -1654,7 +1923,10 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1654
1923
|
catch { /* non-fatal */ }
|
|
1655
1924
|
}
|
|
1656
1925
|
}
|
|
1657
|
-
// Uninstall from each scope
|
|
1926
|
+
// Uninstall from each scope. The folders a selective uninstall refused are counted across the
|
|
1927
|
+
// scopes, so the last line can say the removal was done in part. A refusal is reported, not a
|
|
1928
|
+
// failure: the exit status stays 0, as it does for a legacy install whose links were skipped.
|
|
1929
|
+
const refusedFolders = new Set();
|
|
1658
1930
|
for (const scope of scopesToUninstall) {
|
|
1659
1931
|
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
1660
1932
|
if (paths === null) {
|
|
@@ -1669,8 +1941,9 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1669
1941
|
else {
|
|
1670
1942
|
p.log.step('Uninstalling legacy local scope (git-root/.claude/)');
|
|
1671
1943
|
}
|
|
1944
|
+
const scopeRefused = new Set();
|
|
1672
1945
|
if (isSelectiveUninstall) {
|
|
1673
|
-
await runSelectivePhaseForScope({
|
|
1946
|
+
const links = await runSelectivePhaseForScope({
|
|
1674
1947
|
claudeDir,
|
|
1675
1948
|
devflowDir,
|
|
1676
1949
|
selectedPlugins,
|
|
@@ -1678,6 +1951,10 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1678
1951
|
installedPlugins: await resolveInstalledPlugins(devflowDir),
|
|
1679
1952
|
scope,
|
|
1680
1953
|
});
|
|
1954
|
+
for (const folder of links.refusedFolders) {
|
|
1955
|
+
scopeRefused.add(folder);
|
|
1956
|
+
refusedFolders.add(folder);
|
|
1957
|
+
}
|
|
1681
1958
|
}
|
|
1682
1959
|
else {
|
|
1683
1960
|
await runFullPhaseForScope({ scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs: !!options.keepDocs, isTTY: !!process.stdin.isTTY });
|
|
@@ -1685,14 +1962,14 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1685
1962
|
const pluginLabel = isSelectiveUninstall
|
|
1686
1963
|
? ` (${selectedPluginNames.join(', ')})`
|
|
1687
1964
|
: '';
|
|
1688
|
-
|
|
1965
|
+
const removed = formatPluginRemovedLine(pluginLabel, scopeRefused.size);
|
|
1966
|
+
p.log[removed.level](removed.message);
|
|
1689
1967
|
}
|
|
1690
1968
|
// === CLEANUP EXTRAS (only for full uninstall) ===
|
|
1691
1969
|
if (!isSelectiveUninstall) {
|
|
1692
1970
|
await runCleanupPhase({ scopesToUninstall, keepDocs: !!options.keepDocs, verbose, cwd: process.cwd(), isTTY: !!process.stdin.isTTY, managedProxyPorts });
|
|
1693
1971
|
}
|
|
1694
|
-
|
|
1695
|
-
p.outro(`${status}${color.dim(' Reinstall: npx devflow-kit init')}`);
|
|
1972
|
+
p.outro(formatUninstallOutro(refusedFolders.size));
|
|
1696
1973
|
});
|
|
1697
1974
|
/**
|
|
1698
1975
|
* Remove all Devflow assets (full uninstall).
|
|
@@ -1700,7 +1977,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1700
1977
|
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
1701
1978
|
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
1702
1979
|
*/
|
|
1703
|
-
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange =
|
|
1980
|
+
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange = changeUserOwned, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
1704
1981
|
const devflowDirectories = [
|
|
1705
1982
|
{ path: path.join(claudeDir, 'commands', 'devflow'), name: 'commands' },
|
|
1706
1983
|
{ path: path.join(claudeDir, 'agents', 'devflow'), name: 'agents' },
|
|
@@ -1732,7 +2009,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1732
2009
|
// A bare dir whose name matches a live-registry skill is by construction
|
|
1733
2010
|
// foreign to Devflow (the devflow: namespace shipped in dcecda3, 2026-03-30).
|
|
1734
2011
|
// A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES).
|
|
1735
|
-
|
|
2012
|
+
// The workflow skill folders (D-WORKFLOW-FOLDER-INSTALL) are in getAllSkillNames, so the folder is removed.
|
|
2013
|
+
const prefixedSkillNames = new Set([...getAllSkillNames(folderWorkflows), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
|
|
1736
2014
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1737
2015
|
let skillsRemoved = 0;
|
|
1738
2016
|
for (const skillName of prefixedSkillNames) {
|
|
@@ -1777,7 +2055,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1777
2055
|
// registry (retired, renamed, or deleted). The loop above walks the static
|
|
1778
2056
|
// registry + LEGACY list; this sweep walks the actual directory, so orphaned
|
|
1779
2057
|
// dirs from older versions are also cleaned up. (F9)
|
|
1780
|
-
await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
2058
|
+
await sweepDevflowNamespaces(claudeDir, verbose, mayChange, undefined, folderWorkflows);
|
|
1781
2059
|
}
|
|
1782
2060
|
/**
|
|
1783
2061
|
* Registry-diff sweep of all Devflow-owned namespaces in `claudeDir`:
|
|
@@ -1797,25 +2075,38 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1797
2075
|
* and the user has no other signal that it is still there.
|
|
1798
2076
|
*
|
|
1799
2077
|
* A folder `mayChange` refuses is not swept at all (D-CLI-NO-SYMLINK,
|
|
1800
|
-
* legacyLocalChangeGuard).
|
|
1801
|
-
*
|
|
2078
|
+
* legacyLocalChangeGuard). The agents and commands folders are swept only when each is a real folder
|
|
2079
|
+
* or absent, whatever `mayChange` says, because the sweep reads and removes inside whatever the
|
|
2080
|
+
* folder names; a folder that is a link or not a folder is recorded in `links` and left as it was.
|
|
2081
|
+
*
|
|
2082
|
+
* With no `mayChange`, the sweep uses the Devflow folder guard, which also refuses each orphan that
|
|
2083
|
+
* is a link (a linked `skills/devflow:*` folder): the orphan stays and is recorded in `links`. With a
|
|
2084
|
+
* `mayChange` given, an orphan that is itself a link is only unlinked: `fs.rm` never follows the
|
|
2085
|
+
* link it is given.
|
|
1802
2086
|
*
|
|
1803
2087
|
* Returns what each sweep removed and failed to remove, so a caller can tell that
|
|
1804
2088
|
* a retired agent was removed (see {@link refreshRetainedCommandsAndAgents}).
|
|
1805
2089
|
*/
|
|
1806
|
-
export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange =
|
|
2090
|
+
export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange, links = { refusedFolders: [] }, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
1807
2091
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1808
2092
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1809
2093
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1810
2094
|
const noSweep = { scanned: 0, removed: [], failed: [] };
|
|
1811
|
-
const
|
|
1812
|
-
const
|
|
1813
|
-
const
|
|
2095
|
+
const guard = mayChange ?? devflowFolderGuard(claudeDir, links);
|
|
2096
|
+
const mayRemove = mayChange === undefined ? guard : undefined;
|
|
2097
|
+
const sweep = async (...args) => (await guard(args[0])) ? sweepOrphanedAssets(args[0], args[1], args[2], mayRemove) : noSweep;
|
|
2098
|
+
const sweepDevflowFolder = async (...args) => (await guard(args[0])) && !(await refuseUnrealFolder(links, args[0]))
|
|
2099
|
+
? sweepOrphanedAssets(args[0], args[1], args[2], mayRemove)
|
|
2100
|
+
: noSweep;
|
|
2101
|
+
const agentsSweep = await sweepDevflowFolder(agentsDir, new Set(getAllAgentNames()), mdEntryName);
|
|
2102
|
+
const commandsSweep = await sweepDevflowFolder(commandsDir, new Set(getAllCommandNames(folderWorkflows)), mdEntryName);
|
|
1814
2103
|
// FEATURE_OWNED_SKILLS (compliance) unioned into knownNames so a selective
|
|
1815
2104
|
// uninstall of another plugin does not sweep devflow:compliance — nothing
|
|
1816
2105
|
// converges after selective uninstall (only installViaFileCopy + convergeComplianceArtifacts
|
|
1817
2106
|
// on full/partial init re-materialize it). D-FO-1 applies.
|
|
1818
|
-
|
|
2107
|
+
// D-WORKFLOW-FOLDER-INSTALL: a workflow skill folder is a known skill, so a selective uninstall of
|
|
2108
|
+
// another plugin keeps it, and a stale command file of a folder workflow is an orphan here.
|
|
2109
|
+
const skillsSweep = await sweep(skillsDir, new Set([...getAllSkillNames(folderWorkflows), ...FEATURE_OWNED_SKILLS]), (entry) => entry.startsWith(SKILL_NAMESPACE) ? unprefixSkillName(entry) : null);
|
|
1819
2110
|
const byKind = [
|
|
1820
2111
|
{ kind: 'agent', sweep: agentsSweep },
|
|
1821
2112
|
{ kind: 'command', sweep: commandsSweep },
|
|
@@ -1839,17 +2130,20 @@ export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange = cha
|
|
|
1839
2130
|
* sweep the install directory for any orphaned assets whose names left the
|
|
1840
2131
|
* registry (retired agents/commands survive indefinitely without the sweep).
|
|
1841
2132
|
* For skills: only remove skills that are NOT used by any remaining plugin.
|
|
1842
|
-
* Each removal passes
|
|
1843
|
-
*
|
|
2133
|
+
* Each removal passes the guard first. A legacy local install passes `mayChange`
|
|
2134
|
+
* (D-CLI-NO-SYMLINK, legacyLocalChangeGuard). With none given, the removal uses the
|
|
2135
|
+
* Devflow folder guard: nothing is removed in or from a Devflow folder that is a link or
|
|
2136
|
+
* not a folder, and each such folder is recorded in `links` and left as it was.
|
|
1844
2137
|
*
|
|
1845
2138
|
* Returns what the closing registry-diff sweep removed.
|
|
1846
2139
|
*/
|
|
1847
|
-
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange =
|
|
1848
|
-
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
|
|
2140
|
+
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange, links = { refusedFolders: [] }, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
2141
|
+
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins, folderWorkflows);
|
|
2142
|
+
const guard = mayChange ?? devflowFolderGuard(claudeDir, links);
|
|
1849
2143
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1850
2144
|
for (const cmd of commands) {
|
|
1851
2145
|
const cmdPath = path.join(commandsDir, mdFileName(cmd.replace(/^\//, '')));
|
|
1852
|
-
if (!(await
|
|
2146
|
+
if (!(await guard(cmdPath)))
|
|
1853
2147
|
continue;
|
|
1854
2148
|
try {
|
|
1855
2149
|
await fs.rm(cmdPath, { force: true });
|
|
@@ -1864,7 +2158,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1864
2158
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1865
2159
|
for (const agent of agents) {
|
|
1866
2160
|
const agentPath = path.join(agentsDir, mdFileName(agent));
|
|
1867
|
-
if (!(await
|
|
2161
|
+
if (!(await guard(agentPath)))
|
|
1868
2162
|
continue;
|
|
1869
2163
|
try {
|
|
1870
2164
|
await fs.rm(agentPath, { force: true });
|
|
@@ -1885,7 +2179,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1885
2179
|
// namespace shipped in dcecda3, 2026-03-30), so a bare dir for a current
|
|
1886
2180
|
// registry name is by construction foreign.
|
|
1887
2181
|
const skillPath = path.join(skillsDir, prefixSkillName(skill));
|
|
1888
|
-
if (!(await
|
|
2182
|
+
if (!(await guard(skillPath)))
|
|
1889
2183
|
continue;
|
|
1890
2184
|
try {
|
|
1891
2185
|
await fs.rm(skillPath, { recursive: true, force: true });
|
|
@@ -1898,7 +2192,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1898
2192
|
const rulesDir = path.join(claudeDir, 'rules', 'devflow');
|
|
1899
2193
|
for (const rule of rules) {
|
|
1900
2194
|
const rulePath = path.join(rulesDir, mdFileName(rule));
|
|
1901
|
-
if (!(await
|
|
2195
|
+
if (!(await guard(rulePath)))
|
|
1902
2196
|
continue;
|
|
1903
2197
|
try {
|
|
1904
2198
|
await fs.rm(rulePath, { force: true });
|
|
@@ -1912,6 +2206,6 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1912
2206
|
// Removes any orphaned files whose names left the registry (retired, renamed,
|
|
1913
2207
|
// or deleted from all plugins). Spans ALL plugins so assets belonging to
|
|
1914
2208
|
// non-selected plugins are never swept.
|
|
1915
|
-
return sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
2209
|
+
return sweepDevflowNamespaces(claudeDir, verbose, mayChange, links, folderWorkflows);
|
|
1916
2210
|
}
|
|
1917
2211
|
//# sourceMappingURL=uninstall.js.map
|