@curdx/flow 2.2.3 → 2.2.4
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/cli/doctor-workflow.js +5 -914
- package/cli/lib/doctor-claude-settings.js +736 -0
- package/cli/lib/doctor-runtime-environment.js +196 -0
- package/cli/lib/semver.js +14 -0
- package/cli/uninstall-actions.js +323 -0
- package/cli/uninstall.js +9 -253
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const ENV_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max", "auto"];
|
|
2
|
+
const PINNED_MODEL_ENV_FAMILIES = [
|
|
3
|
+
{
|
|
4
|
+
modelVar: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
5
|
+
capsVar: "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES",
|
|
6
|
+
label: "Opus",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
modelVar: "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
10
|
+
capsVar: "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES",
|
|
11
|
+
label: "Sonnet",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
modelVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
15
|
+
capsVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES",
|
|
16
|
+
label: "Haiku",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
modelVar: "ANTHROPIC_CUSTOM_MODEL_OPTION",
|
|
20
|
+
capsVar: "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES",
|
|
21
|
+
label: "Custom model option",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function envFlagEnabled(value) {
|
|
26
|
+
if (value === true || value === 1) return true;
|
|
27
|
+
if (typeof value !== "string") return false;
|
|
28
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizedEnvValue(value) {
|
|
32
|
+
if (typeof value !== "string") return null;
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
return normalized.length > 0 ? normalized : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripExtendedContextSuffix(modelId) {
|
|
38
|
+
return modelId.replace(/\[1m\]$/i, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function looksProviderSpecificModelId(modelId) {
|
|
42
|
+
const normalized = stripExtendedContextSuffix(modelId);
|
|
43
|
+
if (normalized.includes(":") || normalized.includes("/")) return true;
|
|
44
|
+
if (/^(?:us\.)?anthropic\./i.test(normalized)) return true;
|
|
45
|
+
if (!/^claude-(?:opus|sonnet|haiku)-/i.test(normalized)) return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function positiveIntegerFromEnv(value) {
|
|
50
|
+
const normalized = normalizedEnvValue(value);
|
|
51
|
+
if (!normalized) return null;
|
|
52
|
+
if (!/^[0-9]+$/.test(normalized)) return Number.NaN;
|
|
53
|
+
const parsed = Number(normalized);
|
|
54
|
+
return parsed > 0 ? parsed : Number.NaN;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function inspectRuntimeEnvironment(env = process.env) {
|
|
58
|
+
const entries = [];
|
|
59
|
+
const inCi = envFlagEnabled(env.CI);
|
|
60
|
+
|
|
61
|
+
if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE)) {
|
|
62
|
+
entries.push({
|
|
63
|
+
level: "err",
|
|
64
|
+
text: "CLAUDE_CODE_SIMPLE enabled (bare/simple mode)",
|
|
65
|
+
details: [
|
|
66
|
+
"official docs: this disables auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md",
|
|
67
|
+
"CurDX-Flow cannot load correctly in this mode; unset it before launching Claude Code",
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
|
|
73
|
+
entries.push({
|
|
74
|
+
level: "warn",
|
|
75
|
+
text: "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT enabled",
|
|
76
|
+
details: [
|
|
77
|
+
"official docs: discovery still works, but Claude runs with the minimal system prompt and collapsed tool descriptions",
|
|
78
|
+
"CurDX-Flow may still load, but planning/review behavior can degrade versus the normal Claude Code prompt",
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const effortLevel = normalizedEnvValue(env.CLAUDE_CODE_EFFORT_LEVEL);
|
|
84
|
+
if (effortLevel && !ENV_EFFORT_LEVELS.includes(effortLevel)) {
|
|
85
|
+
entries.push({
|
|
86
|
+
level: "warn",
|
|
87
|
+
text: `CLAUDE_CODE_EFFORT_LEVEL invalid (${effortLevel})`,
|
|
88
|
+
details: [
|
|
89
|
+
`expected one of: ${ENV_EFFORT_LEVELS.join(", ")}`,
|
|
90
|
+
"invalid effort env values can make sessions harder to reason about; remove or correct it",
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
} else if (effortLevel === "low" || effortLevel === "medium") {
|
|
94
|
+
entries.push({
|
|
95
|
+
level: "warn",
|
|
96
|
+
text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
|
|
97
|
+
details: [
|
|
98
|
+
"this takes precedence over /effort and settings effortLevel",
|
|
99
|
+
"CurDX-Flow planning, verification, and review-heavy turns usually work better at high or xhigh",
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
} else if (effortLevel) {
|
|
103
|
+
entries.push({
|
|
104
|
+
level: "info",
|
|
105
|
+
text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
|
|
106
|
+
details: [
|
|
107
|
+
"session effort is pinned through the environment for this process",
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (envFlagEnabled(env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)) {
|
|
113
|
+
entries.push({
|
|
114
|
+
level: "info",
|
|
115
|
+
text: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS enabled",
|
|
116
|
+
details: [
|
|
117
|
+
"official docs: this enables experimental team surfaces such as SendMessage / TeamCreate / TeamDelete",
|
|
118
|
+
"CurDX-Flow does not depend on these runtime-gated tools, but this explains why teammate features may appear in this session",
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const syncPluginInstall = envFlagEnabled(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL);
|
|
124
|
+
const syncPluginInstallTimeout = positiveIntegerFromEnv(
|
|
125
|
+
env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS
|
|
126
|
+
);
|
|
127
|
+
const pluginSeedDir = normalizedEnvValue(env.CLAUDE_CODE_PLUGIN_SEED_DIR);
|
|
128
|
+
|
|
129
|
+
if (normalizedEnvValue(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS)) {
|
|
130
|
+
if (Number.isNaN(syncPluginInstallTimeout)) {
|
|
131
|
+
entries.push({
|
|
132
|
+
level: "warn",
|
|
133
|
+
text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS invalid",
|
|
134
|
+
details: [
|
|
135
|
+
"expected a positive integer timeout in milliseconds",
|
|
136
|
+
"invalid timeout values can make headless plugin-install behavior harder to reason about",
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
} else if (!syncPluginInstall) {
|
|
140
|
+
entries.push({
|
|
141
|
+
level: "warn",
|
|
142
|
+
text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS set without CLAUDE_CODE_SYNC_PLUGIN_INSTALL",
|
|
143
|
+
details: [
|
|
144
|
+
"official docs: the timeout only applies when synchronous plugin installation is enabled",
|
|
145
|
+
"set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 or remove the timeout override",
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (pluginSeedDir) {
|
|
152
|
+
entries.push({
|
|
153
|
+
level: "info",
|
|
154
|
+
text: "CLAUDE_CODE_PLUGIN_SEED_DIR configured",
|
|
155
|
+
details: [
|
|
156
|
+
`seed dir: ${pluginSeedDir}`,
|
|
157
|
+
"official docs: pre-populated plugin seeds let containers and CI start with marketplaces/plugins already available",
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (inCi && !syncPluginInstall && !pluginSeedDir) {
|
|
163
|
+
entries.push({
|
|
164
|
+
level: "warn",
|
|
165
|
+
text: "CI environment without synchronous or seeded plugin availability",
|
|
166
|
+
details: [
|
|
167
|
+
"prefer claude --bare -p for CI so runs do not inherit local hooks, skills, plugins, MCP discovery, or CLAUDE.md unexpectedly",
|
|
168
|
+
"official docs: in non-interactive/headless mode, marketplace plugins may install in the background and miss the first turn",
|
|
169
|
+
"set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 for headless runs that depend on plugin availability on turn one",
|
|
170
|
+
"or pre-populate plugins with CLAUDE_CODE_PLUGIN_SEED_DIR in containers/CI images",
|
|
171
|
+
"if the run needs project assets in bare mode, pass them explicitly with --plugin-dir, --settings, or --mcp-config",
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const family of PINNED_MODEL_ENV_FAMILIES) {
|
|
177
|
+
const modelId = normalizedEnvValue(env[family.modelVar]);
|
|
178
|
+
if (!modelId) continue;
|
|
179
|
+
|
|
180
|
+
const caps = normalizedEnvValue(env[family.capsVar]);
|
|
181
|
+
if (!looksProviderSpecificModelId(modelId) || caps) continue;
|
|
182
|
+
|
|
183
|
+
entries.push({
|
|
184
|
+
level: "warn",
|
|
185
|
+
text: `${family.modelVar} uses a provider-specific/custom model id`,
|
|
186
|
+
details: [
|
|
187
|
+
`${family.label} pinned to: ${modelId}`,
|
|
188
|
+
`${family.capsVar} is unset`,
|
|
189
|
+
"official docs: custom/provider model IDs can disable Claude Code feature detection for effort and thinking",
|
|
190
|
+
`declare ${family.capsVar} when pinning custom Bedrock / Vertex / Foundry / gateway model IDs`,
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { entries };
|
|
196
|
+
}
|
package/cli/lib/semver.js
CHANGED
|
@@ -93,3 +93,17 @@ export function isVersionNewer(latestVersion, currentVersion) {
|
|
|
93
93
|
export function isVersionAtLeast(version, minimumVersion) {
|
|
94
94
|
return compareVersions(version, minimumVersion) >= 0;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
export function deriveNpmDistTag(version) {
|
|
98
|
+
const { prerelease } = parseVersion(version);
|
|
99
|
+
|
|
100
|
+
if (prerelease.length === 0) {
|
|
101
|
+
return "latest";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const namedChannel = prerelease.find(
|
|
105
|
+
(token) => typeof token === "string" && token.length > 0
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return namedChannel || "prerelease";
|
|
109
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
|
|
6
|
+
import {
|
|
7
|
+
removeMcp,
|
|
8
|
+
removePluginMarketplace,
|
|
9
|
+
uninstallPlugin,
|
|
10
|
+
} from "./lib/claude-ops.js";
|
|
11
|
+
import {
|
|
12
|
+
confirm,
|
|
13
|
+
color,
|
|
14
|
+
listPlugins,
|
|
15
|
+
log,
|
|
16
|
+
resultLastLine,
|
|
17
|
+
resultOutput,
|
|
18
|
+
} from "./utils.js";
|
|
19
|
+
import {
|
|
20
|
+
UNINSTALL_STEP_COUNT,
|
|
21
|
+
getInstalledTargets,
|
|
22
|
+
getManagedMarketplaceIds,
|
|
23
|
+
selectRecommendedPluginsToRemove,
|
|
24
|
+
shouldKeepBundledMcps,
|
|
25
|
+
shouldKeepRequiredPlugins,
|
|
26
|
+
} from "./uninstall-workflow.js";
|
|
27
|
+
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
|
|
30
|
+
const RECOMMENDED = RECOMMENDED_PLUGINS.map(toUninstallTarget);
|
|
31
|
+
const REQUIRED = REQUIRED_PLUGINS.map(toUninstallTarget);
|
|
32
|
+
|
|
33
|
+
// Symlinks created by install.js (only cleaned with --purge)
|
|
34
|
+
const MANAGED_SYMLINKS = [
|
|
35
|
+
join(HOME, ".local", "bin", "bun"),
|
|
36
|
+
join(HOME, ".local", "bin", "uv"),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export async function uninstallCurdxFlowPlugin(
|
|
40
|
+
{
|
|
41
|
+
listPluginsImpl = listPlugins,
|
|
42
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
43
|
+
logImpl = log,
|
|
44
|
+
resultOutputImpl = resultOutput,
|
|
45
|
+
} = {}
|
|
46
|
+
) {
|
|
47
|
+
logImpl.blank();
|
|
48
|
+
logImpl.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
|
|
49
|
+
const curdx = listPluginsImpl().find((plugin) => plugin.name === "curdx-flow");
|
|
50
|
+
if (!curdx) {
|
|
51
|
+
logImpl.info("curdx-flow not installed, skipping");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await uninstallPluginImpl({
|
|
56
|
+
scope: "user",
|
|
57
|
+
uninstallSpec: "curdx-flow@curdx-flow-marketplace",
|
|
58
|
+
});
|
|
59
|
+
if (result.code === 0) {
|
|
60
|
+
logImpl.ok("curdx-flow uninstalled");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logImpl.err(`Uninstall failed: ${resultOutputImpl(result)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function maybeUninstallRecommendedPlugins(
|
|
68
|
+
{ yes, keepRecommended },
|
|
69
|
+
{
|
|
70
|
+
getInstalledTargetsImpl = getInstalledTargets,
|
|
71
|
+
selectRecommendedPluginsToRemoveImpl = selectRecommendedPluginsToRemove,
|
|
72
|
+
uninstallNamedPluginImpl = uninstallNamedPlugin,
|
|
73
|
+
logImpl = log,
|
|
74
|
+
} = {}
|
|
75
|
+
) {
|
|
76
|
+
logImpl.blank();
|
|
77
|
+
logImpl.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
|
|
78
|
+
if (keepRecommended) {
|
|
79
|
+
logImpl.info("Keeping recommended plugins (--keep-recommended)");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const present = getInstalledTargetsImpl(RECOMMENDED);
|
|
84
|
+
if (present.length === 0) {
|
|
85
|
+
logImpl.info("No installed recommended plugins");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const selected = await selectRecommendedPluginsToRemoveImpl({ yes, present });
|
|
90
|
+
for (const name of selected) {
|
|
91
|
+
const entry = present.find((plugin) => plugin.name === name);
|
|
92
|
+
if (!entry) continue;
|
|
93
|
+
await uninstallNamedPluginImpl(entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function uninstallNamedPlugin(
|
|
98
|
+
entry,
|
|
99
|
+
{
|
|
100
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
101
|
+
resultLastLineImpl = resultLastLine,
|
|
102
|
+
} = {}
|
|
103
|
+
) {
|
|
104
|
+
log.blank();
|
|
105
|
+
console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
|
|
106
|
+
const result = await uninstallPluginImpl(entry);
|
|
107
|
+
if (result.code === 0) {
|
|
108
|
+
console.log(` ${color.green("✓")} ${entry.name} uninstalled`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLineImpl(result)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function maybeRemoveBundledMcps(
|
|
118
|
+
{ yes, keepRecommended },
|
|
119
|
+
{
|
|
120
|
+
shouldKeepBundledMcpsImpl = shouldKeepBundledMcps,
|
|
121
|
+
confirmImpl = confirm,
|
|
122
|
+
removeMcpImpl = removeMcp,
|
|
123
|
+
logImpl = log,
|
|
124
|
+
} = {}
|
|
125
|
+
) {
|
|
126
|
+
logImpl.blank();
|
|
127
|
+
logImpl.info("Required MCP servers (context7, sequential-thinking)");
|
|
128
|
+
if (shouldKeepBundledMcpsImpl({ yes, keepRecommended })) {
|
|
129
|
+
logImpl.info(
|
|
130
|
+
color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const removeMcps = await confirmImpl(
|
|
136
|
+
`Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
|
|
137
|
+
false
|
|
138
|
+
);
|
|
139
|
+
if (!removeMcps) {
|
|
140
|
+
logImpl.info("Keeping user-level MCPs");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const mcp of BUNDLED_MCPS) {
|
|
145
|
+
const result = await removeMcpImpl({ name: mcp.name });
|
|
146
|
+
if (result.code === 0) {
|
|
147
|
+
logImpl.ok(` ${mcp.name.padEnd(22)} removed`);
|
|
148
|
+
} else {
|
|
149
|
+
logImpl.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function maybeUninstallRequiredPlugins(
|
|
155
|
+
{ yes },
|
|
156
|
+
{
|
|
157
|
+
shouldKeepRequiredPluginsImpl = shouldKeepRequiredPlugins,
|
|
158
|
+
confirmImpl = confirm,
|
|
159
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
160
|
+
logImpl = log,
|
|
161
|
+
} = {}
|
|
162
|
+
) {
|
|
163
|
+
logImpl.blank();
|
|
164
|
+
logImpl.info("Required companion plugins");
|
|
165
|
+
if (shouldKeepRequiredPluginsImpl({ yes })) {
|
|
166
|
+
logImpl.info(
|
|
167
|
+
color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const removeRequired = await confirmImpl(
|
|
173
|
+
`Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
|
|
174
|
+
false
|
|
175
|
+
);
|
|
176
|
+
if (!removeRequired) {
|
|
177
|
+
logImpl.info("Keeping required companion plugins");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const plugin of REQUIRED) {
|
|
182
|
+
const result = await uninstallPluginImpl(plugin);
|
|
183
|
+
if (result.code === 0) {
|
|
184
|
+
logImpl.ok(` ${plugin.name.padEnd(22)} uninstalled`);
|
|
185
|
+
} else {
|
|
186
|
+
logImpl.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function maybePurgeRuntimeArtifacts(
|
|
192
|
+
{ purge },
|
|
193
|
+
{
|
|
194
|
+
purgeManagedMarketplacesImpl = purgeManagedMarketplaces,
|
|
195
|
+
removeManagedSymlinksImpl = removeManagedSymlinks,
|
|
196
|
+
logImpl = log,
|
|
197
|
+
} = {}
|
|
198
|
+
) {
|
|
199
|
+
logImpl.blank();
|
|
200
|
+
logImpl.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
|
|
201
|
+
if (!purge) {
|
|
202
|
+
logImpl.info(
|
|
203
|
+
color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
|
|
204
|
+
);
|
|
205
|
+
logImpl.info(
|
|
206
|
+
color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await purgeManagedMarketplacesImpl();
|
|
212
|
+
removeManagedSymlinksImpl();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function maybeRemoveProjectState(
|
|
216
|
+
{ yes },
|
|
217
|
+
{
|
|
218
|
+
cwd = process.cwd(),
|
|
219
|
+
confirmImpl = confirm,
|
|
220
|
+
existsSyncImpl = existsSync,
|
|
221
|
+
rmSyncImpl = rmSync,
|
|
222
|
+
logImpl = log,
|
|
223
|
+
} = {}
|
|
224
|
+
) {
|
|
225
|
+
logImpl.blank();
|
|
226
|
+
logImpl.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
|
|
227
|
+
const flowDir = join(cwd, ".flow");
|
|
228
|
+
if (!existsSyncImpl(flowDir)) {
|
|
229
|
+
logImpl.info(".flow/ does not exist, skipping");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (yes) {
|
|
234
|
+
logImpl.info(
|
|
235
|
+
color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ok = await confirmImpl(
|
|
241
|
+
`${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
|
|
242
|
+
false
|
|
243
|
+
);
|
|
244
|
+
if (!ok) {
|
|
245
|
+
logImpl.info("Keeping .flow/");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
rmSyncImpl(flowDir, { recursive: true, force: true });
|
|
251
|
+
logImpl.ok(`Removed ${flowDir}`);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logImpl.err(`Removal failed: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function purgeManagedMarketplaces(
|
|
258
|
+
{
|
|
259
|
+
getManagedMarketplaceIdsImpl = getManagedMarketplaceIds,
|
|
260
|
+
removePluginMarketplaceImpl = removePluginMarketplace,
|
|
261
|
+
logImpl = log,
|
|
262
|
+
} = {}
|
|
263
|
+
) {
|
|
264
|
+
const marketplaceIds = getManagedMarketplaceIdsImpl(RECOMMENDED.concat(REQUIRED));
|
|
265
|
+
|
|
266
|
+
for (const marketplaceId of marketplaceIds) {
|
|
267
|
+
const result = await removePluginMarketplaceImpl(marketplaceId);
|
|
268
|
+
if (result.code === 0) {
|
|
269
|
+
logImpl.ok(`Removed marketplace ${marketplaceId}`);
|
|
270
|
+
} else if (!result.stderr.includes("not found")) {
|
|
271
|
+
logImpl.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function removeManagedSymlinks(
|
|
277
|
+
{
|
|
278
|
+
existsSyncImpl = existsSync,
|
|
279
|
+
isBrokenSymlinkImpl = isBrokenSymlink,
|
|
280
|
+
lstatSyncImpl = lstatSync,
|
|
281
|
+
readlinkSyncImpl = readlinkSync,
|
|
282
|
+
unlinkSyncImpl = unlinkSync,
|
|
283
|
+
logImpl = log,
|
|
284
|
+
} = {}
|
|
285
|
+
) {
|
|
286
|
+
for (const link of MANAGED_SYMLINKS) {
|
|
287
|
+
if (!existsSyncImpl(link) && !isBrokenSymlinkImpl(link)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const stat = lstatSyncImpl(link);
|
|
292
|
+
if (!stat.isSymbolicLink()) {
|
|
293
|
+
logImpl.warn(
|
|
294
|
+
`${link} is not a symlink (likely a real file placed by the user), skipping`
|
|
295
|
+
);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const target = readlinkSyncImpl(link);
|
|
299
|
+
unlinkSyncImpl(link);
|
|
300
|
+
logImpl.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logImpl.warn(`Failed to remove ${link}: ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function toUninstallTarget(entry) {
|
|
308
|
+
return {
|
|
309
|
+
name: entry.name,
|
|
310
|
+
uninstallSpec: entry.uninstallSpec,
|
|
311
|
+
uninstallArgs: entry.uninstallArgs || [],
|
|
312
|
+
marketplaceId: entry.marketplaceId,
|
|
313
|
+
scope: entry.scope,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isBrokenSymlink(pathname) {
|
|
318
|
+
try {
|
|
319
|
+
return lstatSync(pathname).isSymbolicLink();
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|