devflow-kit 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +1 -1
- package/dist/agents/code.md +6 -6
- package/dist/agents/fix.md +6 -6
- package/dist/agents/harden.md +7 -6
- package/dist/agents/refactor.md +6 -6
- package/dist/cli/commands/ambient.js +10 -2
- package/dist/cli/commands/backup-session.js +131 -0
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +184 -24
- package/dist/cli/commands/curation.js +23 -10
- package/dist/cli/commands/gate-level-prompts.js +39 -25
- package/dist/cli/commands/gates.js +5 -7
- package/dist/cli/commands/init.js +169 -184
- package/dist/cli/commands/install-report.js +129 -0
- package/dist/cli/commands/legacy-sweep.js +159 -0
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +75 -21
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +339 -75
- package/dist/commands/bug-analysis.md +69 -120
- package/dist/commands/dynamic-build.md +23 -23
- package/dist/commands/implement.md +46 -32
- package/dist/commands/release.md +1 -1
- package/dist/commands/resolve.md +35 -31
- package/dist/commands/self-review.md +8 -7
- 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/assets.js +126 -17
- package/dist/core/compliance-off-files.js +27 -0
- package/dist/core/compliance.js +14 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/flags.js +2 -2
- package/dist/core/fs-atomic.js +5 -1
- 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 +29 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +68 -10
- 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 +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 +6 -6
- package/dist/curation-off/agents/fix.md +6 -6
- package/dist/curation-off/agents/harden.md +7 -6
- package/dist/curation-off/agents/refactor.md +6 -6
- package/dist/curation-off/commands/bug-analysis.md +69 -119
- package/dist/curation-off/commands/dynamic-build.md +23 -23
- package/dist/curation-off/commands/implement.md +46 -32
- package/dist/curation-off/commands/release.md +1 -1
- package/dist/curation-off/commands/resolve.md +35 -31
- package/dist/curation-off/commands/self-review.md +8 -7
- package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
- 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 +62 -17
- package/dist/targets/claude-code/compliance-off-files.js +249 -0
- package/dist/targets/claude-code/curation-install.js +632 -117
- 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 +795 -199
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +176 -0
- package/dist/targets/claude-code/reference-converge.js +131 -0
- 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/tracker-install.js +21 -8
- package/dist/targets/claude-code/tracker-servers.js +20 -6
- package/dist/targets/claude-code/workflow-folder-install.js +251 -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/evaluate.md +1 -1
- package/src/assets/agents/git.mds +32 -0
- package/src/assets/agents/harden.mds +7 -6
- package/src/assets/agents/test.md +21 -12
- package/src/assets/agents/triage.mds +2 -0
- package/src/assets/agents/validate.md +10 -10
- package/src/assets/commands/_partials/_compliance.mds +5 -3
- package/src/assets/commands/_partials/_engine.mds +9 -8
- package/src/assets/commands/_partials/_publication.mds +10 -1
- package/src/assets/commands/bug-analysis.mds +80 -126
- package/src/assets/commands/code-review.mds +43 -0
- 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 +217 -32
- package/src/assets/commands/self-review.mds +8 -7
- package/src/assets/mds/git/_ops.mds +23 -0
- package/src/assets/mds/git/_ops_steps.mds +17 -0
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/mds/git/_references.mds +6 -0
- package/src/assets/mds/tracker/_common.mds +4 -0
- package/src/assets/mds/tracker/_github.mds +42 -0
- package/src/assets/mds/tracker/_jira.mds +43 -0
- package/src/assets/mds/tracker/_linear.mds +43 -0
- package/src/assets/rules/quality.md +2 -0
- package/src/assets/scripts/ci-wait.cjs +204 -126
- 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/preamble +18 -8
- package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
- package/src/assets/scripts/pr-evidence.cjs +41 -2
- package/src/assets/scripts/verify-evidence.cjs +408 -56
- 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/synthesize/bug-analysis.md +6 -2
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
- 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';
|
|
@@ -35,7 +35,12 @@ import { MANIFEST_LOCK_NAME, MANIFEST_LOCK_STALE_MARK } from '../../core/manifes
|
|
|
35
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
36
|
import { probeTrackedArea } from '../../core/tracked-areas.js';
|
|
37
37
|
import { restampInstalledCommands } from '../../targets/claude-code/language-stamp.js';
|
|
38
|
+
import { installRenderedCharter, removeHandoffSentinel } from '../../targets/claude-code/charter-render.js';
|
|
38
39
|
import { installCommandsAndAgents } from '../../targets/claude-code/installer.js';
|
|
40
|
+
import { BACKUP_ROOT_NAME, escapeName } from '../../targets/claude-code/install-backup.js';
|
|
41
|
+
import { isUnrealFolder, newLinkReport, refuseUnrealFolder, refusedFolderStatement } from '../../targets/claude-code/linked-folder.js';
|
|
42
|
+
import { formatReplacedLinksSummary } from './install-report.js';
|
|
43
|
+
import { judgeBareLegacySkill } from './legacy-sweep.js';
|
|
39
44
|
/**
|
|
40
45
|
* Where a retired repo-local install lives: `<gitRoot>/.claude` and `<gitRoot>/.devflow`.
|
|
41
46
|
*
|
|
@@ -76,10 +81,121 @@ async function scopeInstallPaths(scope, gitRoot) {
|
|
|
76
81
|
return gitRoot === null ? null : legacyLocalInstallPaths(gitRoot);
|
|
77
82
|
}
|
|
78
83
|
/**
|
|
79
|
-
* The user scope's guard
|
|
80
|
-
*
|
|
84
|
+
* The user scope's guard for a path that is not inside a Devflow install folder, and for the removal
|
|
85
|
+
* of a whole entry. It passes everything: `~/.claude` and `~/.devflow` and their other folders, the
|
|
86
|
+
* manifest and `settings.json` are the user's own, so a symbolic link there is followed. `fs.rm` of an
|
|
87
|
+
* entry that is a link removes the link and never what it points to, so a full uninstall removes a
|
|
88
|
+
* linked Devflow folder as it removes a real one.
|
|
89
|
+
*
|
|
90
|
+
* A read, write or removal inside a Devflow install folder never uses this guard: it uses
|
|
91
|
+
* {@link devflowFolderGuard}, which refuses a link.
|
|
81
92
|
*/
|
|
82
|
-
const
|
|
93
|
+
const changeUserOwned = async () => true;
|
|
94
|
+
/** The folders directly under `~/.claude` that hold a `devflow` folder of Devflow's own. */
|
|
95
|
+
const DEVFLOW_FILE_FOLDERS = ['commands', 'agents', 'rules'];
|
|
96
|
+
/**
|
|
97
|
+
* The Devflow install folder that holds `target` and every folder below it down to the parent of
|
|
98
|
+
* `target`, outermost first; null for a target outside every Devflow install folder. A target that is
|
|
99
|
+
* itself a Devflow install folder (a `skills/devflow:*` folder) gives that folder alone. PURE.
|
|
100
|
+
*
|
|
101
|
+
* The Devflow install folders are `commands/devflow`, `agents/devflow`, `rules/devflow` and each
|
|
102
|
+
* `skills/devflow:*` folder. `~/.claude`, `~/.claude/skills` and the other folders above them are
|
|
103
|
+
* not among them.
|
|
104
|
+
*/
|
|
105
|
+
function devflowFolderChain(claudeDir, target) {
|
|
106
|
+
const rel = path.relative(claudeDir, target);
|
|
107
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
108
|
+
return null;
|
|
109
|
+
const parts = rel.split(path.sep);
|
|
110
|
+
const inFileFolder = DEVFLOW_FILE_FOLDERS.includes(parts[0]) && parts[1] === 'devflow';
|
|
111
|
+
const inSkillFolder = parts[0] === 'skills' && parts[1] !== undefined && parts[1].startsWith(SKILL_NAMESPACE);
|
|
112
|
+
if (!inFileFolder && !inSkillFolder)
|
|
113
|
+
return null;
|
|
114
|
+
const chain = [path.join(claudeDir, parts[0], parts[1])];
|
|
115
|
+
for (let depth = 2; depth < parts.length - 1; depth++) {
|
|
116
|
+
chain.push(path.join(chain[chain.length - 1], parts[depth]));
|
|
117
|
+
}
|
|
118
|
+
return chain;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The guard for a change inside the Devflow install folders of `claudeDir`.
|
|
122
|
+
*
|
|
123
|
+
* Devflow never reads, writes or removes through a link at or below its own install folders. A
|
|
124
|
+
* target is let through only when each Devflow folder on the way to it is a real folder or absent;
|
|
125
|
+
* a folder that is a link, or not a folder, refuses the target. The refusal is recorded once in
|
|
126
|
+
* `report.refusedFolders` and the link is left as it was. A target outside every Devflow install
|
|
127
|
+
* folder passes, and a link to a file that is the target itself is not followed (`fs.rm` removes
|
|
128
|
+
* the link). Never throws.
|
|
129
|
+
*/
|
|
130
|
+
function devflowFolderGuard(claudeDir, report) {
|
|
131
|
+
return async (target) => {
|
|
132
|
+
const chain = devflowFolderChain(claudeDir, target);
|
|
133
|
+
if (chain === null)
|
|
134
|
+
return true;
|
|
135
|
+
for (const folder of chain) {
|
|
136
|
+
if (report.refusedFolders.includes(folder))
|
|
137
|
+
return false;
|
|
138
|
+
if (await refuseUnrealFolder(report, folder))
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Turn the refused-folder half of an uninstall into summary lines, one per folder.
|
|
146
|
+
*
|
|
147
|
+
* An uninstall removes and changes nothing at a path that is a link, or not a folder where it needs
|
|
148
|
+
* one, and leaves the entry as it was. That looks the same on disk as an uninstall that found nothing
|
|
149
|
+
* to remove, so the line says what was skipped and what to do about it.
|
|
150
|
+
*
|
|
151
|
+
* Pure function — returns lines, logs nothing.
|
|
152
|
+
*/
|
|
153
|
+
export function formatUninstallRefusedSummary(report) {
|
|
154
|
+
return report.refusedFolders.map(folder => ({
|
|
155
|
+
level: 'warn',
|
|
156
|
+
message: `${refusedFolderStatement(folder)} Devflow removed and changed nothing there. ` +
|
|
157
|
+
`Remove the link or replace it with a real folder, then run the uninstall again.`,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** How many folders an uninstall left as they are, in a sentence that agrees with the count. */
|
|
161
|
+
function leftAsTheyAre(count) {
|
|
162
|
+
return count === 1 ? '1 folder was left as it is' : `${count} folders were left as they are`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The line for one scope's removal. With no refused folder the plugin is removed. With any, the
|
|
166
|
+
* removal is partial: the line says so and counts the folders left as they are, because a refused
|
|
167
|
+
* folder looks the same on disk as a folder that had nothing to remove.
|
|
168
|
+
*
|
|
169
|
+
* @param pluginLabel - Empty, or the selected plugin names as ` (a, b)`.
|
|
170
|
+
*
|
|
171
|
+
* Pure function — returns the line, logs nothing.
|
|
172
|
+
*/
|
|
173
|
+
export function formatPluginRemovedLine(pluginLabel, refusedCount) {
|
|
174
|
+
if (refusedCount === 0)
|
|
175
|
+
return { level: 'success', message: `Plugin removed${pluginLabel}` };
|
|
176
|
+
return { level: 'warn', message: `Plugin removed in part${pluginLabel}: ${leftAsTheyAre(refusedCount)}.` };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The last line of an uninstall. It says success only when no folder was refused in any scope;
|
|
180
|
+
* otherwise it says the uninstall was done in part and counts the folders left as they are.
|
|
181
|
+
*
|
|
182
|
+
* Pure function — returns the line, logs nothing.
|
|
183
|
+
*/
|
|
184
|
+
export function formatUninstallOutro(refusedCount) {
|
|
185
|
+
if (refusedCount === 0) {
|
|
186
|
+
return `${color.green('Devflow uninstalled successfully')}${color.dim(' Reinstall: npx devflow-kit init')}`;
|
|
187
|
+
}
|
|
188
|
+
return color.yellow(`Devflow uninstalled in part: ${leftAsTheyAre(refusedCount)}.`);
|
|
189
|
+
}
|
|
190
|
+
/** Print the links an uninstall replaced or refused, one line each, once per path. */
|
|
191
|
+
function logLinkReport(report) {
|
|
192
|
+
const lines = [
|
|
193
|
+
...formatReplacedLinksSummary({ replacedLinks: [...new Set(report.replacedLinks)] }),
|
|
194
|
+
...formatUninstallRefusedSummary({ refusedFolders: [...new Set(report.refusedFolders)] }),
|
|
195
|
+
];
|
|
196
|
+
for (const line of lines)
|
|
197
|
+
p.log[line.level](line.message);
|
|
198
|
+
}
|
|
83
199
|
/**
|
|
84
200
|
* `folder` and every path below it down to `target`, the target included, outermost
|
|
85
201
|
* first, for whichever of the two folders holds `target`; null for a target in
|
|
@@ -140,11 +256,17 @@ function legacyLocalChangeGuard(folders, skipped) {
|
|
|
140
256
|
return false;
|
|
141
257
|
};
|
|
142
258
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
259
|
+
/**
|
|
260
|
+
* The guard for one scope's full or selective phase.
|
|
261
|
+
*
|
|
262
|
+
* A legacy local install is checked for a link anywhere on the way to a target. In the user scope the
|
|
263
|
+
* full phase removes whole entries and passes ({@link changeUserOwned}). The selective phase gives
|
|
264
|
+
* its `links` report and gets {@link devflowFolderGuard}, which refuses a linked Devflow folder.
|
|
265
|
+
*/
|
|
266
|
+
function scopeChangeGuard(scope, folders, links) {
|
|
267
|
+
if (scope === 'local')
|
|
268
|
+
return legacyLocalChangeGuard(folders, 'Legacy local install not fully removed');
|
|
269
|
+
return links === undefined ? changeUserOwned : devflowFolderGuard(folders.claudeDir, links);
|
|
148
270
|
}
|
|
149
271
|
/**
|
|
150
272
|
* The plugins the manifest records as installed, as registry definitions.
|
|
@@ -164,6 +286,24 @@ export async function resolveInstalledPlugins(devflowDir) {
|
|
|
164
286
|
const resolved = DEVFLOW_PLUGINS.filter(plugin => names.has(plugin.name));
|
|
165
287
|
return resolved.length === 0 ? DEVFLOW_PLUGINS : resolved;
|
|
166
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* The plugin names the orchestrator charter is re-rendered for after `uninstall --plugin`, or null when there
|
|
291
|
+
* is no manifest record to render from. Pure.
|
|
292
|
+
*
|
|
293
|
+
* D-CHARTER-INSTALL-RENDER: the set is the manifest record (`readManifestPlugins`) minus the plugins being
|
|
294
|
+
* removed, with a legacy name mapped and only names this registry has kept ({@link registeredPluginNames}).
|
|
295
|
+
* It is deliberately NOT {@link resolveInstalledPlugins}: that falls back to the whole registry when the
|
|
296
|
+
* manifest is missing or names nothing, which is the safe direction for retaining assets, and the wrong one
|
|
297
|
+
* here. The registry would render a charter that routes to every workflow on a machine that installed none
|
|
298
|
+
* of them. With no record the caller leaves the installed charter as it is and takes only the sentinel away.
|
|
299
|
+
* A record that lists no plugin is a record of nothing installed, so it yields the empty set rather than null.
|
|
300
|
+
*/
|
|
301
|
+
export function charterPluginsAfterRemoval(recordedPlugins, removedPluginNames) {
|
|
302
|
+
if (recordedPlugins === null)
|
|
303
|
+
return null;
|
|
304
|
+
const removed = new Set(removedPluginNames);
|
|
305
|
+
return registeredPluginNames(recordedPlugins).filter(name => !removed.has(name));
|
|
306
|
+
}
|
|
167
307
|
/**
|
|
168
308
|
* Compute which assets should be removed during selective plugin uninstall.
|
|
169
309
|
* Skills and agents shared by remaining plugins are retained.
|
|
@@ -183,8 +323,11 @@ export async function resolveInstalledPlugins(devflowDir) {
|
|
|
183
323
|
* registry only when there is no manifest to read, and the dry-run and the
|
|
184
324
|
* real removal must always be given the SAME list or they describe different
|
|
185
325
|
* outcomes.
|
|
326
|
+
* @param folderWorkflows - The target-layer set (D-WORKFLOW-FOLDER-INSTALL). A folder
|
|
327
|
+
* workflow of a selected plugin is a skill to remove, unless a remaining plugin
|
|
328
|
+
* owns it too, and never a command: it has no command file.
|
|
186
329
|
*/
|
|
187
|
-
export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
330
|
+
export function computeAssetsToRemove(selectedPlugins, installedPlugins, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
188
331
|
const selectedNames = new Set(selectedPlugins.map(p => p.name));
|
|
189
332
|
const remainingPlugins = installedPlugins.filter(p => !selectedNames.has(p.name));
|
|
190
333
|
const retainedSkills = skillsOf(remainingPlugins);
|
|
@@ -204,6 +347,11 @@ export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
|
204
347
|
if (!retainedSkills.has(skill))
|
|
205
348
|
skills.push(skill);
|
|
206
349
|
}
|
|
350
|
+
const retainedFolders = selectedWorkflowFolders(remainingPlugins, folderWorkflows);
|
|
351
|
+
for (const name of selectedWorkflowFolders(selectedPlugins, folderWorkflows)) {
|
|
352
|
+
if (!retainedFolders.has(name))
|
|
353
|
+
skills.push(name);
|
|
354
|
+
}
|
|
207
355
|
for (const plugin of selectedPlugins) {
|
|
208
356
|
for (const agent of plugin.agents) {
|
|
209
357
|
if (!retainedAgents.has(agent))
|
|
@@ -213,7 +361,7 @@ export function computeAssetsToRemove(selectedPlugins, installedPlugins) {
|
|
|
213
361
|
if (!retainedRules.has(rule))
|
|
214
362
|
rules.push(rule);
|
|
215
363
|
}
|
|
216
|
-
commands.push(...plugin.commands);
|
|
364
|
+
commands.push(...plugin.commands.filter(cmd => !folderWorkflows.has(cmd.replace(/^\//, ''))));
|
|
217
365
|
}
|
|
218
366
|
return { skills, agents, commands, rules };
|
|
219
367
|
}
|
|
@@ -508,6 +656,9 @@ export function userContentPaths(devflowDir) {
|
|
|
508
656
|
{ relPath: 'skills', isDir: true, label: `skill shadows (${path.join(devflowDir, 'skills')})` },
|
|
509
657
|
// Rule shadow overrides (~/.devflow/rules/{name}.md)
|
|
510
658
|
{ relPath: 'rules', isDir: true, label: `rule shadows (${path.join(devflowDir, 'rules')})` },
|
|
659
|
+
// The files an install moved out of the devflow folders because devflow did not ship them.
|
|
660
|
+
// They are the user's own files, so only a confirmed full wipe removes them.
|
|
661
|
+
{ relPath: BACKUP_ROOT_NAME, isDir: true, label: `install backups (${path.join(devflowDir, BACKUP_ROOT_NAME)})` },
|
|
511
662
|
// preference-profile.md — user-curated decision-preference profile
|
|
512
663
|
{ relPath: 'preference-profile.md', label: 'preference-profile.md' },
|
|
513
664
|
// tracker/ — the inferred, hand-editable issue-tracker conventions, one file
|
|
@@ -775,7 +926,7 @@ export function installArtifactPaths(devflowDir) {
|
|
|
775
926
|
* This function runs on the decline, cancel, non-interactive AND --keep-docs paths,
|
|
776
927
|
* so an entry here is deleted even when the user answers "no" to the full wipe.
|
|
777
928
|
* User-authored state (everything in `userContentPaths`: skill/rule shadows,
|
|
778
|
-
* preference-profile.md, the tracker conventions with the legacy tracker.md and
|
|
929
|
+
* the install backups, preference-profile.md, the tracker conventions with the legacy tracker.md and
|
|
779
930
|
* its backups, the global tuning file under both names, hud.json) is removed only by
|
|
780
931
|
* the confirmed full-dir rm.
|
|
781
932
|
* agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
|
|
@@ -785,7 +936,7 @@ export function installArtifactPaths(devflowDir) {
|
|
|
785
936
|
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
786
937
|
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
787
938
|
*/
|
|
788
|
-
export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange =
|
|
939
|
+
export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange = changeUserOwned) {
|
|
789
940
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
790
941
|
if (await mayChange(manifestPath)) {
|
|
791
942
|
try {
|
|
@@ -866,6 +1017,35 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
866
1017
|
catch { /* skills dir absent or unreadable */ }
|
|
867
1018
|
return false;
|
|
868
1019
|
}
|
|
1020
|
+
/** Why a bare legacy skill folder stays: its `SKILL.md` is a file that no release of devflow installed. */
|
|
1021
|
+
const KEPT_NOT_MADE = 'devflow did not make this folder';
|
|
1022
|
+
/** Why a bare legacy skill folder stays: there is no regular `SKILL.md`, the folder or file is a link, or a read failed. */
|
|
1023
|
+
const KEPT_UNDECIDED = 'devflow could not tell if it made this folder';
|
|
1024
|
+
/**
|
|
1025
|
+
* Sort the bare legacy skill folders that exist in `skillsDir` into those a full uninstall removes
|
|
1026
|
+
* and those it keeps. The skills folder is shared with Claude Code and other tools, so a bare name
|
|
1027
|
+
* (`python`, `plan`) does not prove that devflow made the folder. It is removed only when the check
|
|
1028
|
+
* `devflow init` uses finds a `SKILL.md` that a release installed (`judgeBareLegacySkill`). The dry
|
|
1029
|
+
* run and the removal both take this one list. A `devflow:` name is in the namespace of devflow, and
|
|
1030
|
+
* the prefixed pass removes it. Reads one file per folder at most. Never throws.
|
|
1031
|
+
*/
|
|
1032
|
+
async function planBareLegacySkills(skillsDir) {
|
|
1033
|
+
const remove = [];
|
|
1034
|
+
const keep = [];
|
|
1035
|
+
for (const name of new Set(LEGACY_SKILL_NAMES)) {
|
|
1036
|
+
if (name.startsWith(SKILL_NAMESPACE))
|
|
1037
|
+
continue;
|
|
1038
|
+
const folder = path.join(skillsDir, name);
|
|
1039
|
+
const verdict = await judgeBareLegacySkill(folder, name);
|
|
1040
|
+
if (verdict === 'devflow')
|
|
1041
|
+
remove.push(folder);
|
|
1042
|
+
else if (verdict === 'other')
|
|
1043
|
+
keep.push({ folder, reason: KEPT_NOT_MADE });
|
|
1044
|
+
else if (verdict === 'unknown')
|
|
1045
|
+
keep.push({ folder, reason: KEPT_UNDECIDED });
|
|
1046
|
+
}
|
|
1047
|
+
return { remove, keep };
|
|
1048
|
+
}
|
|
869
1049
|
// ---------------------------------------------------------------------------
|
|
870
1050
|
// Phase runners — extracted from the action function for readability and
|
|
871
1051
|
// independent testability. Each covers one logical stage of the uninstall.
|
|
@@ -883,9 +1063,10 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
883
1063
|
* and the scripts directory inside devflowDir.
|
|
884
1064
|
* 2. ALL skill removal candidates (existence-filtered):
|
|
885
1065
|
* - Prefixed (devflow:name): live registry ∪ LEGACY_SKILL_NAMES.
|
|
886
|
-
* - Bare (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY
|
|
887
|
-
* ~/.claude/skills/ is
|
|
888
|
-
* are by construction
|
|
1066
|
+
* - Bare (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY, and only a
|
|
1067
|
+
* folder that devflow made (planBareLegacySkills). ~/.claude/skills/ is
|
|
1068
|
+
* shared; bare dirs for live-registry skill names are by construction
|
|
1069
|
+
* foreign to Devflow. A bare folder that stays is listed as `(kept: <reason>)`.
|
|
889
1070
|
* 3. manifest.json (removed separately in removeDevFlowInstallArtifacts).
|
|
890
1071
|
* 4. Every artifact path from installArtifactPaths(devflowDir) — the single
|
|
891
1072
|
* source of truth shared with the real removal loop.
|
|
@@ -893,7 +1074,7 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
893
1074
|
* @param claudeDir - Target Claude Code directory (e.g. ~/.claude)
|
|
894
1075
|
* @param devflowDir - Devflow data directory (e.g. ~/.devflow)
|
|
895
1076
|
*/
|
|
896
|
-
export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
1077
|
+
export async function enumerateDryRunExtras(claudeDir, devflowDir, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
897
1078
|
const extras = [];
|
|
898
1079
|
// 1. Whole directories removed by removeAllDevFlow
|
|
899
1080
|
for (const dir of [
|
|
@@ -911,11 +1092,12 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
911
1092
|
// 2. Skills: enumerate ALL removal candidates that exist on disk.
|
|
912
1093
|
// Mirrors removeAllDevFlow's split-pass approach:
|
|
913
1094
|
// Prefixed: live registry ∪ LEGACY_SKILL_NAMES ∪ FEATURE_OWNED_SKILLS
|
|
914
|
-
// Bare: LEGACY_SKILL_NAMES only
|
|
915
|
-
// dirs are by construction foreign to Devflow.
|
|
1095
|
+
// Bare: LEGACY_SKILL_NAMES only, and only a folder that devflow made — shared
|
|
1096
|
+
// skills/ dir; live-registry bare dirs are by construction foreign to Devflow.
|
|
916
1097
|
// FEATURE_OWNED_SKILLS (compliance) is included so full uninstall removes
|
|
917
1098
|
// devflow:compliance even though it left the plugin registry (step 1.5).
|
|
918
|
-
|
|
1099
|
+
// The workflow skill folders (D-WORKFLOW-FOLDER-INSTALL) join through getAllSkillNames.
|
|
1100
|
+
const prefixedSkillNames = new Set([...getAllSkillNames(folderWorkflows), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
|
|
919
1101
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
920
1102
|
for (const skillName of prefixedSkillNames) {
|
|
921
1103
|
const prefixedPath = path.join(skillsDir, prefixSkillName(skillName));
|
|
@@ -925,14 +1107,10 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
925
1107
|
}
|
|
926
1108
|
catch { /* absent */ }
|
|
927
1109
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
extras.push(barePath);
|
|
933
|
-
}
|
|
934
|
-
catch { /* absent */ }
|
|
935
|
-
}
|
|
1110
|
+
const bareSkills = await planBareLegacySkills(skillsDir);
|
|
1111
|
+
extras.push(...bareSkills.remove);
|
|
1112
|
+
for (const { folder, reason } of bareSkills.keep)
|
|
1113
|
+
extras.push(`${escapeName(folder)} (kept: ${reason})`);
|
|
936
1114
|
// 3. manifest.json — removed separately by removeDevFlowInstallArtifacts
|
|
937
1115
|
// (NOT in installArtifactPaths). List it here so the preview is complete.
|
|
938
1116
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
@@ -1036,13 +1214,17 @@ export async function runDryRunPhase(opts) {
|
|
|
1036
1214
|
* settings.json, so the notice tells the user to run `devflow init` for those.
|
|
1037
1215
|
*
|
|
1038
1216
|
* Never runs in a legacy local scope (D-CLI-NO-SYMLINK): a repository can commit a command
|
|
1039
|
-
* file as a symbolic link, and the copy writes through it.
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1217
|
+
* file as a symbolic link, and the copy writes through it. `devflow init` can not update a
|
|
1218
|
+
* legacy local install either, so the warning tells the user to remove it in full.
|
|
1219
|
+
*
|
|
1220
|
+
* In the user scope the copy writes into neither folder when either is a link or not a folder,
|
|
1221
|
+
* whatever the caller's guard says: writing only one folder would pair old commands with new
|
|
1222
|
+
* agents. The refused folder is recorded in `links`. A command or agent file that is a link is
|
|
1223
|
+
* replaced by a real file and recorded in `links` too; what it pointed to is left as it was.
|
|
1042
1224
|
*
|
|
1043
1225
|
* Skipped, with a warning, when there is no readable manifest or it lists no known plugin (the
|
|
1044
|
-
* retained set would be a guess), or when a folder is behind a link the
|
|
1045
|
-
* refuses. Those warnings name `devflow init` as the remedy. When the copy fails, it stops
|
|
1226
|
+
* retained set would be a guess), or when a folder is a link, not a folder, or behind a link the
|
|
1227
|
+
* caller's guard refuses. Those warnings name `devflow init` as the remedy. When the copy fails, it stops
|
|
1046
1228
|
* at the first failure: files it wrote before then stay, and the warning names the same
|
|
1047
1229
|
* remedy. Never throws: an uninstall must finish.
|
|
1048
1230
|
* Agent model lines are reverted again after the copy, as the selective phase does before
|
|
@@ -1053,7 +1235,8 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1053
1235
|
const { claudeDir, devflowDir, retained, retiredAgents } = opts;
|
|
1054
1236
|
const scope = opts.scope ?? 'user';
|
|
1055
1237
|
const verbose = opts.verbose ?? false;
|
|
1056
|
-
const mayChange = opts.mayChange ??
|
|
1238
|
+
const mayChange = opts.mayChange ?? changeUserOwned;
|
|
1239
|
+
const links = opts.links ?? newLinkReport();
|
|
1057
1240
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1058
1241
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1059
1242
|
const retired = `Removed the agent file(s) of an older Devflow release: ${retiredAgents.join(', ')}.`;
|
|
@@ -1080,7 +1263,14 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1080
1263
|
p.log.warn(`${retired} The manifest lists no known plugin, so Devflow did not rewrite the remaining files. ${remedy}`);
|
|
1081
1264
|
return;
|
|
1082
1265
|
}
|
|
1083
|
-
|
|
1266
|
+
let blocked = !(await mayChange(agentsDir)) || !(await mayChange(commandsDir));
|
|
1267
|
+
if (!blocked) {
|
|
1268
|
+
// The caller's guard may pass everything. Neither folder is written when it is a link or not a folder.
|
|
1269
|
+
const agentsRefused = await refuseUnrealFolder(links, agentsDir);
|
|
1270
|
+
const commandsRefused = await refuseUnrealFolder(links, commandsDir);
|
|
1271
|
+
blocked = agentsRefused || commandsRefused;
|
|
1272
|
+
}
|
|
1273
|
+
if (blocked) {
|
|
1084
1274
|
p.log.warn(`${retired} A linked folder blocks the rewrite. ${remedy}`);
|
|
1085
1275
|
return;
|
|
1086
1276
|
}
|
|
@@ -1090,7 +1280,9 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1090
1280
|
agentsMap: buildAssetMaps(retained).agentsMap,
|
|
1091
1281
|
claudeDir,
|
|
1092
1282
|
curation: manifest.features.curation,
|
|
1283
|
+
compliance: manifest.features.compliance.enabled,
|
|
1093
1284
|
trackerServers: effectiveTrackerServers(manifest.features.tracker),
|
|
1285
|
+
links,
|
|
1094
1286
|
warn: (msg) => p.log.warn(msg),
|
|
1095
1287
|
});
|
|
1096
1288
|
}
|
|
@@ -1105,6 +1297,30 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1105
1297
|
catch { /* the revert is best effort, as in the selective phase */ }
|
|
1106
1298
|
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.`);
|
|
1107
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Re-render the installed orchestrator charter for the plugins that stay after a selective removal, so it
|
|
1302
|
+
* stops routing to a workflow whose skill was just removed, and take the implement sentinel away when
|
|
1303
|
+
* implement is among the removed.
|
|
1304
|
+
*
|
|
1305
|
+
* D-CHARTER-INSTALL-RENDER: the set comes from the MANIFEST record, read without a heal so the file stays
|
|
1306
|
+
* byte-identical, which still lists the removed plugins until the caller drops the record, never from the
|
|
1307
|
+
* registry fallback of {@link resolveInstalledPlugins}. The record is read only where the change guard lets
|
|
1308
|
+
* a change through. With no usable record there is nothing to render from and the installed charter stays
|
|
1309
|
+
* as it is, but the sentinel needs no record and goes when implement is among the removed. An uninstall
|
|
1310
|
+
* never creates a file, so a machine whose charter is not installed gets none. A failure is a warning,
|
|
1311
|
+
* never a failure of the uninstall.
|
|
1312
|
+
*/
|
|
1313
|
+
async function rerenderCharterAfterRemoval(devflowDir, removedPluginNames, mayChange) {
|
|
1314
|
+
const scriptsDir = path.join(devflowDir, 'scripts');
|
|
1315
|
+
const warn = (msg) => p.log.warn(msg);
|
|
1316
|
+
const recorded = (await mayChange(path.join(devflowDir, 'manifest.json'))) ? await readManifestPlugins(devflowDir) : null;
|
|
1317
|
+
const retained = charterPluginsAfterRemoval(recorded, removedPluginNames);
|
|
1318
|
+
if (retained === null) {
|
|
1319
|
+
await removeHandoffSentinel({ scriptsDir, removedPlugins: removedPluginNames, warn, mayChange });
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
await installRenderedCharter({ scriptsDir, plugins: retained, warn, mayChange, mode: 'uninstall' });
|
|
1323
|
+
}
|
|
1108
1324
|
/**
|
|
1109
1325
|
* SELECTIVE PHASE (per scope): revert agent model overrides, remove the
|
|
1110
1326
|
* selected plugins' assets, and clean up the ambient hook if the ambient
|
|
@@ -1114,13 +1330,19 @@ export async function refreshRetainedCommandsAndAgents(opts) {
|
|
|
1114
1330
|
* @param opts.devflowDir - Devflow data directory for this scope.
|
|
1115
1331
|
* @param opts.selectedPlugins - Plugin subset to remove.
|
|
1116
1332
|
* @param opts.verbose - Whether to emit verbose log lines.
|
|
1333
|
+
*
|
|
1334
|
+
* Nothing is read, written or removed through a link at or below a Devflow install folder. A
|
|
1335
|
+
* Devflow folder that is a link, or not a folder, is left as it was; the phase prints one line for
|
|
1336
|
+
* each and returns them, with the file links it replaced, in the {@link LinkReport}.
|
|
1117
1337
|
*/
|
|
1118
1338
|
export async function runSelectivePhaseForScope(opts) {
|
|
1119
1339
|
const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
|
|
1120
1340
|
const scope = opts.scope ?? 'user';
|
|
1121
1341
|
const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
|
|
1122
|
-
|
|
1123
|
-
|
|
1342
|
+
const links = newLinkReport();
|
|
1343
|
+
// D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first. In the user
|
|
1344
|
+
// scope the guard refuses a linked Devflow folder and records it in `links`.
|
|
1345
|
+
const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir }, links);
|
|
1124
1346
|
// Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
|
|
1125
1347
|
// lines from installed agent frontmatter while the files are still present.
|
|
1126
1348
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
@@ -1139,7 +1361,9 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1139
1361
|
}
|
|
1140
1362
|
catch { /* agents dir absent or revert failed — non-fatal */ }
|
|
1141
1363
|
}
|
|
1142
|
-
|
|
1364
|
+
// In the user scope the removal builds its own guard over the same report, which also checks each
|
|
1365
|
+
// orphan it sweeps; a legacy local install keeps the guard above.
|
|
1366
|
+
const sweeps = await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins, scope === 'local' ? mayChange : undefined, links);
|
|
1143
1367
|
// A retired agent file means the install is from an older release, and the commands of the
|
|
1144
1368
|
// plugins that stay installed still spawn the name that was just removed. Write their
|
|
1145
1369
|
// current commands and agents so the install stays whole. Runs before the language stamp
|
|
@@ -1154,6 +1378,7 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1154
1378
|
scope,
|
|
1155
1379
|
verbose,
|
|
1156
1380
|
mayChange,
|
|
1381
|
+
links,
|
|
1157
1382
|
});
|
|
1158
1383
|
}
|
|
1159
1384
|
// D-LANGUAGE-FOCUS-STAMP: re-stamp the installed /code-review from the selection that REMAINS.
|
|
@@ -1172,6 +1397,9 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1172
1397
|
mayChange,
|
|
1173
1398
|
});
|
|
1174
1399
|
}
|
|
1400
|
+
// D-CHARTER-INSTALL-RENDER: the charter stops routing to the removed workflows. It reads the manifest
|
|
1401
|
+
// record, so it runs before the record is dropped below.
|
|
1402
|
+
await rerenderCharterAfterRemoval(devflowDir, selectedPlugins.map(sp => sp.name), mayChange);
|
|
1175
1403
|
// D-UNINSTALL-DROPS-PLUGIN: the manifest is the install record `devflow init` seeds its plugin
|
|
1176
1404
|
// selection from (a re-init keeps the prior selection), so a plugin removed here but still listed
|
|
1177
1405
|
// would be installed again by the next plain init. Take the selected names off the list;
|
|
@@ -1204,6 +1432,8 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1204
1432
|
}
|
|
1205
1433
|
catch { /* settings.json may not exist */ }
|
|
1206
1434
|
}
|
|
1435
|
+
logLinkReport(links);
|
|
1436
|
+
return links;
|
|
1207
1437
|
}
|
|
1208
1438
|
/**
|
|
1209
1439
|
* FULL PHASE (per scope): revert agent model overrides, remove all Devflow
|
|
@@ -1223,19 +1453,23 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
1223
1453
|
* @param opts.isTTY - Whether the session is interactive. Injected rather than
|
|
1224
1454
|
* read from process.stdin so the prompt gate is an explicit input (matching
|
|
1225
1455
|
* resolveDevflowDirCleanup) instead of an ambient global a test cannot control.
|
|
1456
|
+
* @param opts.confirm - The prompt for the full-dir wipe (default `p.confirm`).
|
|
1226
1457
|
*/
|
|
1227
1458
|
export async function runFullPhaseForScope(opts) {
|
|
1228
1459
|
const { scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs, isTTY } = opts;
|
|
1460
|
+
const confirm = opts.confirm ?? p.confirm;
|
|
1229
1461
|
// D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first.
|
|
1230
1462
|
const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir });
|
|
1231
1463
|
// Revert GPT agent frontmatter before removing agents — ensures no orphaned
|
|
1232
1464
|
// GPT model lines remain if agents dir is preserved by a later partial flow.
|
|
1233
1465
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
1466
|
+
// The revert rewrites the files in the folder, so a folder that is a link is not reverted: the
|
|
1467
|
+
// removal below takes the link away whole and the files it led to stay as they are.
|
|
1234
1468
|
{
|
|
1235
1469
|
const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1236
1470
|
try {
|
|
1237
1471
|
await fs.access(agentsInstallDir);
|
|
1238
|
-
if (await mayChange(agentsInstallDir)) {
|
|
1472
|
+
if (await mayChange(agentsInstallDir) && !(await isUnrealFolder(agentsInstallDir))) {
|
|
1239
1473
|
await revertExternalAgents({
|
|
1240
1474
|
installDir: agentsInstallDir,
|
|
1241
1475
|
devflowDir,
|
|
@@ -1281,7 +1515,7 @@ export async function runFullPhaseForScope(opts) {
|
|
|
1281
1515
|
for (const item of userContent) {
|
|
1282
1516
|
p.log.info(` ${item}`);
|
|
1283
1517
|
}
|
|
1284
|
-
const confirmFullCleanup = await
|
|
1518
|
+
const confirmFullCleanup = await confirm({
|
|
1285
1519
|
message: `Remove entire ${devflowDir}? (includes the items listed above, plus logs and install metadata)`,
|
|
1286
1520
|
initialValue: false,
|
|
1287
1521
|
});
|
|
@@ -1481,7 +1715,7 @@ export async function runCleanupPhase(opts) {
|
|
|
1481
1715
|
// where no symbolic link leads it out of `<gitRoot>/.claude`.
|
|
1482
1716
|
const mayChange = scope === 'local'
|
|
1483
1717
|
? legacyLocalChangeGuard(paths, 'settings.json not edited')
|
|
1484
|
-
:
|
|
1718
|
+
: changeUserOwned;
|
|
1485
1719
|
if (settingsContent !== originalContent && await mayChange(settingsPath)) {
|
|
1486
1720
|
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
1487
1721
|
if (verbose) {
|
|
@@ -1722,7 +1956,10 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1722
1956
|
catch { /* non-fatal */ }
|
|
1723
1957
|
}
|
|
1724
1958
|
}
|
|
1725
|
-
// Uninstall from each scope
|
|
1959
|
+
// Uninstall from each scope. The folders a selective uninstall refused are counted across the
|
|
1960
|
+
// scopes, so the last line can say the removal was done in part. A refusal is reported, not a
|
|
1961
|
+
// failure: the exit status stays 0, as it does for a legacy install whose links were skipped.
|
|
1962
|
+
const refusedFolders = new Set();
|
|
1726
1963
|
for (const scope of scopesToUninstall) {
|
|
1727
1964
|
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
1728
1965
|
if (paths === null) {
|
|
@@ -1737,8 +1974,9 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1737
1974
|
else {
|
|
1738
1975
|
p.log.step('Uninstalling legacy local scope (git-root/.claude/)');
|
|
1739
1976
|
}
|
|
1977
|
+
const scopeRefused = new Set();
|
|
1740
1978
|
if (isSelectiveUninstall) {
|
|
1741
|
-
await runSelectivePhaseForScope({
|
|
1979
|
+
const links = await runSelectivePhaseForScope({
|
|
1742
1980
|
claudeDir,
|
|
1743
1981
|
devflowDir,
|
|
1744
1982
|
selectedPlugins,
|
|
@@ -1746,6 +1984,10 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1746
1984
|
installedPlugins: await resolveInstalledPlugins(devflowDir),
|
|
1747
1985
|
scope,
|
|
1748
1986
|
});
|
|
1987
|
+
for (const folder of links.refusedFolders) {
|
|
1988
|
+
scopeRefused.add(folder);
|
|
1989
|
+
refusedFolders.add(folder);
|
|
1990
|
+
}
|
|
1749
1991
|
}
|
|
1750
1992
|
else {
|
|
1751
1993
|
await runFullPhaseForScope({ scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs: !!options.keepDocs, isTTY: !!process.stdin.isTTY });
|
|
@@ -1753,14 +1995,14 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1753
1995
|
const pluginLabel = isSelectiveUninstall
|
|
1754
1996
|
? ` (${selectedPluginNames.join(', ')})`
|
|
1755
1997
|
: '';
|
|
1756
|
-
|
|
1998
|
+
const removed = formatPluginRemovedLine(pluginLabel, scopeRefused.size);
|
|
1999
|
+
p.log[removed.level](removed.message);
|
|
1757
2000
|
}
|
|
1758
2001
|
// === CLEANUP EXTRAS (only for full uninstall) ===
|
|
1759
2002
|
if (!isSelectiveUninstall) {
|
|
1760
2003
|
await runCleanupPhase({ scopesToUninstall, keepDocs: !!options.keepDocs, verbose, cwd: process.cwd(), isTTY: !!process.stdin.isTTY, managedProxyPorts });
|
|
1761
2004
|
}
|
|
1762
|
-
|
|
1763
|
-
p.outro(`${status}${color.dim(' Reinstall: npx devflow-kit init')}`);
|
|
2005
|
+
p.outro(formatUninstallOutro(refusedFolders.size));
|
|
1764
2006
|
});
|
|
1765
2007
|
/**
|
|
1766
2008
|
* Remove all Devflow assets (full uninstall).
|
|
@@ -1768,7 +2010,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1768
2010
|
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
1769
2011
|
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
1770
2012
|
*/
|
|
1771
|
-
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange =
|
|
2013
|
+
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange = changeUserOwned, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
1772
2014
|
const devflowDirectories = [
|
|
1773
2015
|
{ path: path.join(claudeDir, 'commands', 'devflow'), name: 'commands' },
|
|
1774
2016
|
{ path: path.join(claudeDir, 'agents', 'devflow'), name: 'agents' },
|
|
@@ -1796,11 +2038,13 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1796
2038
|
// prefixSkillName() is idempotent for already-prefixed legacy entries.
|
|
1797
2039
|
// FEATURE_OWNED_SKILLS (compliance) is included so full uninstall removes
|
|
1798
2040
|
// devflow:compliance even though it left the plugin registry (step 1.5).
|
|
1799
|
-
// Bare pass (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
|
|
2041
|
+
// Bare pass (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY, and only a folder
|
|
2042
|
+
// whose SKILL.md is a copy that a release of devflow installed (planBareLegacySkills).
|
|
2043
|
+
// Any other bare folder stays and is named. A bare dir whose name matches a
|
|
2044
|
+
// live-registry skill is by construction foreign to Devflow (the devflow: namespace
|
|
2045
|
+
// shipped in dcecda3, 2026-03-30). A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES).
|
|
2046
|
+
// The workflow skill folders (D-WORKFLOW-FOLDER-INSTALL) are in getAllSkillNames, so the folder is removed.
|
|
2047
|
+
const prefixedSkillNames = new Set([...getAllSkillNames(folderWorkflows), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
|
|
1804
2048
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1805
2049
|
let skillsRemoved = 0;
|
|
1806
2050
|
for (const skillName of prefixedSkillNames) {
|
|
@@ -1815,17 +2059,21 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1815
2059
|
}
|
|
1816
2060
|
catch { /* Skill doesn't exist */ }
|
|
1817
2061
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2062
|
+
// Bare variant: frozen legacy list only, never the live registry, and only a folder that devflow made.
|
|
2063
|
+
const bareSkills = await planBareLegacySkills(skillsDir);
|
|
2064
|
+
for (const barePath of bareSkills.remove) {
|
|
1821
2065
|
try {
|
|
1822
|
-
await fs.stat(barePath);
|
|
1823
2066
|
if (!(await mayChange(barePath)))
|
|
1824
2067
|
continue;
|
|
1825
2068
|
await fs.rm(barePath, { recursive: true, force: true });
|
|
1826
2069
|
skillsRemoved++;
|
|
1827
2070
|
}
|
|
1828
|
-
catch {
|
|
2071
|
+
catch (error) {
|
|
2072
|
+
p.log.warn(`Could not remove ${escapeName(barePath)}: ${error}`);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
for (const { folder, reason } of bareSkills.keep) {
|
|
2076
|
+
p.log.info(`Kept ${escapeName(folder)}: ${reason}`);
|
|
1829
2077
|
}
|
|
1830
2078
|
if (skillsRemoved > 0 && verbose) {
|
|
1831
2079
|
p.log.success(`Removed ${skillsRemoved} Devflow skill directories`);
|
|
@@ -1845,7 +2093,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1845
2093
|
// registry (retired, renamed, or deleted). The loop above walks the static
|
|
1846
2094
|
// registry + LEGACY list; this sweep walks the actual directory, so orphaned
|
|
1847
2095
|
// dirs from older versions are also cleaned up. (F9)
|
|
1848
|
-
await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
2096
|
+
await sweepDevflowNamespaces(claudeDir, verbose, mayChange, undefined, folderWorkflows);
|
|
1849
2097
|
}
|
|
1850
2098
|
/**
|
|
1851
2099
|
* Registry-diff sweep of all Devflow-owned namespaces in `claudeDir`:
|
|
@@ -1865,25 +2113,38 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, ma
|
|
|
1865
2113
|
* and the user has no other signal that it is still there.
|
|
1866
2114
|
*
|
|
1867
2115
|
* A folder `mayChange` refuses is not swept at all (D-CLI-NO-SYMLINK,
|
|
1868
|
-
* legacyLocalChangeGuard).
|
|
1869
|
-
*
|
|
2116
|
+
* legacyLocalChangeGuard). The agents and commands folders are swept only when each is a real folder
|
|
2117
|
+
* or absent, whatever `mayChange` says, because the sweep reads and removes inside whatever the
|
|
2118
|
+
* folder names; a folder that is a link or not a folder is recorded in `links` and left as it was.
|
|
2119
|
+
*
|
|
2120
|
+
* With no `mayChange`, the sweep uses the Devflow folder guard, which also refuses each orphan that
|
|
2121
|
+
* is a link (a linked `skills/devflow:*` folder): the orphan stays and is recorded in `links`. With a
|
|
2122
|
+
* `mayChange` given, an orphan that is itself a link is only unlinked: `fs.rm` never follows the
|
|
2123
|
+
* link it is given.
|
|
1870
2124
|
*
|
|
1871
2125
|
* Returns what each sweep removed and failed to remove, so a caller can tell that
|
|
1872
2126
|
* a retired agent was removed (see {@link refreshRetainedCommandsAndAgents}).
|
|
1873
2127
|
*/
|
|
1874
|
-
export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange =
|
|
2128
|
+
export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange, links = { refusedFolders: [] }, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
1875
2129
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1876
2130
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1877
2131
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1878
2132
|
const noSweep = { scanned: 0, removed: [], failed: [] };
|
|
1879
|
-
const
|
|
1880
|
-
const
|
|
1881
|
-
const
|
|
2133
|
+
const guard = mayChange ?? devflowFolderGuard(claudeDir, links);
|
|
2134
|
+
const mayRemove = mayChange === undefined ? guard : undefined;
|
|
2135
|
+
const sweep = async (...args) => (await guard(args[0])) ? sweepOrphanedAssets(args[0], args[1], args[2], mayRemove) : noSweep;
|
|
2136
|
+
const sweepDevflowFolder = async (...args) => (await guard(args[0])) && !(await refuseUnrealFolder(links, args[0]))
|
|
2137
|
+
? sweepOrphanedAssets(args[0], args[1], args[2], mayRemove)
|
|
2138
|
+
: noSweep;
|
|
2139
|
+
const agentsSweep = await sweepDevflowFolder(agentsDir, new Set(getAllAgentNames()), mdEntryName);
|
|
2140
|
+
const commandsSweep = await sweepDevflowFolder(commandsDir, new Set(getAllCommandNames(folderWorkflows)), mdEntryName);
|
|
1882
2141
|
// FEATURE_OWNED_SKILLS (compliance) unioned into knownNames so a selective
|
|
1883
2142
|
// uninstall of another plugin does not sweep devflow:compliance — nothing
|
|
1884
2143
|
// converges after selective uninstall (only installViaFileCopy + convergeComplianceArtifacts
|
|
1885
2144
|
// on full/partial init re-materialize it). D-FO-1 applies.
|
|
1886
|
-
|
|
2145
|
+
// D-WORKFLOW-FOLDER-INSTALL: a workflow skill folder is a known skill, so a selective uninstall of
|
|
2146
|
+
// another plugin keeps it, and a stale command file of a folder workflow is an orphan here.
|
|
2147
|
+
const skillsSweep = await sweep(skillsDir, new Set([...getAllSkillNames(folderWorkflows), ...FEATURE_OWNED_SKILLS]), (entry) => entry.startsWith(SKILL_NAMESPACE) ? unprefixSkillName(entry) : null);
|
|
1887
2148
|
const byKind = [
|
|
1888
2149
|
{ kind: 'agent', sweep: agentsSweep },
|
|
1889
2150
|
{ kind: 'command', sweep: commandsSweep },
|
|
@@ -1907,17 +2168,20 @@ export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange = cha
|
|
|
1907
2168
|
* sweep the install directory for any orphaned assets whose names left the
|
|
1908
2169
|
* registry (retired agents/commands survive indefinitely without the sweep).
|
|
1909
2170
|
* For skills: only remove skills that are NOT used by any remaining plugin.
|
|
1910
|
-
* Each removal passes
|
|
1911
|
-
*
|
|
2171
|
+
* Each removal passes the guard first. A legacy local install passes `mayChange`
|
|
2172
|
+
* (D-CLI-NO-SYMLINK, legacyLocalChangeGuard). With none given, the removal uses the
|
|
2173
|
+
* Devflow folder guard: nothing is removed in or from a Devflow folder that is a link or
|
|
2174
|
+
* not a folder, and each such folder is recorded in `links` and left as it was.
|
|
1912
2175
|
*
|
|
1913
2176
|
* Returns what the closing registry-diff sweep removed.
|
|
1914
2177
|
*/
|
|
1915
|
-
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange =
|
|
1916
|
-
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
|
|
2178
|
+
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange, links = { refusedFolders: [] }, folderWorkflows = WORKFLOW_FOLDER_NAMES) {
|
|
2179
|
+
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins, folderWorkflows);
|
|
2180
|
+
const guard = mayChange ?? devflowFolderGuard(claudeDir, links);
|
|
1917
2181
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1918
2182
|
for (const cmd of commands) {
|
|
1919
2183
|
const cmdPath = path.join(commandsDir, mdFileName(cmd.replace(/^\//, '')));
|
|
1920
|
-
if (!(await
|
|
2184
|
+
if (!(await guard(cmdPath)))
|
|
1921
2185
|
continue;
|
|
1922
2186
|
try {
|
|
1923
2187
|
await fs.rm(cmdPath, { force: true });
|
|
@@ -1932,7 +2196,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1932
2196
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1933
2197
|
for (const agent of agents) {
|
|
1934
2198
|
const agentPath = path.join(agentsDir, mdFileName(agent));
|
|
1935
|
-
if (!(await
|
|
2199
|
+
if (!(await guard(agentPath)))
|
|
1936
2200
|
continue;
|
|
1937
2201
|
try {
|
|
1938
2202
|
await fs.rm(agentPath, { force: true });
|
|
@@ -1953,7 +2217,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1953
2217
|
// namespace shipped in dcecda3, 2026-03-30), so a bare dir for a current
|
|
1954
2218
|
// registry name is by construction foreign.
|
|
1955
2219
|
const skillPath = path.join(skillsDir, prefixSkillName(skill));
|
|
1956
|
-
if (!(await
|
|
2220
|
+
if (!(await guard(skillPath)))
|
|
1957
2221
|
continue;
|
|
1958
2222
|
try {
|
|
1959
2223
|
await fs.rm(skillPath, { recursive: true, force: true });
|
|
@@ -1966,7 +2230,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1966
2230
|
const rulesDir = path.join(claudeDir, 'rules', 'devflow');
|
|
1967
2231
|
for (const rule of rules) {
|
|
1968
2232
|
const rulePath = path.join(rulesDir, mdFileName(rule));
|
|
1969
|
-
if (!(await
|
|
2233
|
+
if (!(await guard(rulePath)))
|
|
1970
2234
|
continue;
|
|
1971
2235
|
try {
|
|
1972
2236
|
await fs.rm(rulePath, { force: true });
|
|
@@ -1980,6 +2244,6 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1980
2244
|
// Removes any orphaned files whose names left the registry (retired, renamed,
|
|
1981
2245
|
// or deleted from all plugins). Spans ALL plugins so assets belonging to
|
|
1982
2246
|
// non-selected plugins are never swept.
|
|
1983
|
-
return sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
2247
|
+
return sweepDevflowNamespaces(claudeDir, verbose, mayChange, links, folderWorkflows);
|
|
1984
2248
|
}
|
|
1985
2249
|
//# sourceMappingURL=uninstall.js.map
|