code-workspace-zhuiyi 1.0.0-beta.3 → 1.0.0-beta.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/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/artifacts/manifest.json +1 -1
- package/package.json +1 -1
- package/src/cli/commands/extension.js +94 -36
- package/src/cli/commands/init.js +114 -12
- package/src/cli/registry.js +1 -1
- package/src/core/config.js +29 -1
- package/src/core/extensions.js +6 -4
- package/src/core/initializer.js +10 -3
- package/src/init/clack-extension-picker.js +13 -6
- package/src/init/extension-picker.js +10 -4
- package/src/init/extension-summary.js +51 -0
- package/src/init/plan.js +2 -1
- package/src/init/registry-picker.js +10 -2
- package/src/init/wizard.js +34 -11
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ codew init .code-workspace \
|
|
|
36
36
|
--yes
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Use `--dev false` for an extension-container Workspace; this skips Workspace dependency installation, disables the system development extension, and omits the project onboarding prompt. Run `codew init .code-workspace --dev true --yes` to enable development mode again. The default for a new Workspace is `true`, and an existing Workspace keeps its saved value when the option is omitted.
|
|
40
|
+
|
|
39
41
|
Use `--tools claude`, `--tools codex`, or `--tools none` to override the default tool selection. Monitoring is not enabled implicitly; install the `monitor` extension with `--extensions monitor` or through the interactive extension picker.
|
|
40
42
|
|
|
41
43
|
Initialization writes only Workspace-owned state and integrations:
|
package/README.zh-CN.md
CHANGED
|
@@ -35,6 +35,8 @@ codew init . \
|
|
|
35
35
|
--yes
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
如果这个 Workspace 主要作为扩展容器使用,可以传 `--dev false`;该模式会跳过 Workspace 依赖安装、停用系统开发扩展并省略项目引导提示。需要恢复开发模式时执行 `codew init . --dev true --yes`。新 Workspace 默认值为 `true`;已有 Workspace 省略该参数时沿用已保存的值。
|
|
39
|
+
|
|
38
40
|
可用 `--tools claude`、`--tools codex` 或 `--tools none` 覆盖默认工具选择。监控不会隐式启用;请通过 `--extensions monitor` 或交互式扩展选择安装 `monitor` 扩展。
|
|
39
41
|
|
|
40
42
|
初始化只写入 Workspace 自有状态和集成:
|
package/artifacts/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
const { resolveWorkspaceTools } = require("../../core/tools");
|
|
26
26
|
const { createInteractiveUi, formatExtensionChoice } = require("../../init/ui");
|
|
27
27
|
const { createRegistryExtensionPicker } = require("../../init/registry-picker");
|
|
28
|
+
const { formatExtensionChanges } = require("../../init/extension-summary");
|
|
28
29
|
const { confirm } = require("../confirmation");
|
|
29
30
|
const { selectionResult, success } = require("../result");
|
|
30
31
|
|
|
@@ -43,18 +44,11 @@ function formatUninstallPlan(plan) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function formatInstallPlan(plans, options = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
` ${actionFor(plan) === "update" ? "Update" : "Install"} ${plan.id}@${plan.version} [${plan.source || "local"}] [Extension Spec ${plan.extensionSpecVersion}]`,
|
|
52
|
-
` PACKAGE ${plan.packageSha256}`,
|
|
53
|
-
...(plan.capabilities.networkHosts || []).map((host) => ` NETWORK https://${host}`),
|
|
54
|
-
...plan.artifacts.map((artifact) => ` WRITE ${artifact.target} (${artifact.kind})`),
|
|
55
|
-
...(plan.hooks || []).map((hook) => ` HOOK ${hook.id} (${hook.event}${hook.tools?.length ? ` · ${hook.tools.join(",")}` : ""}) -> ${hook.command}`),
|
|
56
|
-
]),
|
|
57
|
-
].join("\n");
|
|
47
|
+
return formatExtensionChanges(plans.map((plan) => ({ ...plan, source: plan.source || "local" })), [], {
|
|
48
|
+
title: options.title,
|
|
49
|
+
actionFor: options.actionFor,
|
|
50
|
+
action: options.action,
|
|
51
|
+
});
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
function formatRegistrySearchText(result) {
|
|
@@ -136,18 +130,35 @@ function lifecycleResultEntry(entry, action) {
|
|
|
136
130
|
function lifecycleResultText(results, action) {
|
|
137
131
|
const actionFor = typeof action === "function" ? action : () => action;
|
|
138
132
|
const lines = results.map((entry) => entry.status === "installed"
|
|
139
|
-
? `${actionFor(entry) === "upgrade" || actionFor(entry) === "update" ? "Updated" : "Installed"} ${entry.id}@${entry.version}.`
|
|
140
|
-
: entry.status === "
|
|
141
|
-
? `
|
|
142
|
-
:
|
|
133
|
+
? `${actionFor(entry) === "upgrade" || actionFor(entry) === "update" ? "Updated" : "Installed"} ${entry.name || entry.id}@${entry.version}.`
|
|
134
|
+
: entry.status === "uninstalled"
|
|
135
|
+
? `Uninstalled ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}.`
|
|
136
|
+
: entry.status === "skipped"
|
|
137
|
+
? `Skipped ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}: already current.`
|
|
138
|
+
: `Failed ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}: ${entry.message}`);
|
|
143
139
|
const installed = results.filter((entry) => entry.status === "installed").length;
|
|
140
|
+
const uninstalled = results.filter((entry) => entry.status === "uninstalled").length;
|
|
144
141
|
const skipped = results.filter((entry) => entry.status === "skipped").length;
|
|
145
142
|
const failed = results.filter((entry) => entry.status === "failed").length;
|
|
146
143
|
const hasUpdate = results.some((entry) => actionFor(entry) === "upgrade" || actionFor(entry) === "update");
|
|
147
|
-
lines.push(`Extensions: ${installed} ${hasUpdate ? "applied" : "installed"}, ${skipped} skipped, ${failed} failed.`);
|
|
144
|
+
lines.push(`Extensions: ${installed} ${hasUpdate ? "applied" : "installed"}, ${uninstalled} uninstalled, ${skipped} skipped, ${failed} failed.`);
|
|
148
145
|
return lines.join("\n");
|
|
149
146
|
}
|
|
150
147
|
|
|
148
|
+
function uninstallResultEntry(entry) {
|
|
149
|
+
return {
|
|
150
|
+
name: entry.id,
|
|
151
|
+
ok: entry.status !== "failed",
|
|
152
|
+
action: entry.status === "skipped" ? "skip" : "uninstall",
|
|
153
|
+
status: entry.status,
|
|
154
|
+
version: entry.version || null,
|
|
155
|
+
...(entry.reason ? { reason: entry.reason } : {}),
|
|
156
|
+
...(entry.removed ? { removed: entry.removed } : {}),
|
|
157
|
+
...(entry.code ? { code: entry.code } : {}),
|
|
158
|
+
...(entry.message ? { message: entry.message } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
function validateInstallOptions(invocation) {
|
|
152
163
|
if (invocation.options.version) {
|
|
153
164
|
if (invocation.args.length !== 1) {
|
|
@@ -331,27 +342,54 @@ async function executeExtensionSearch(invocation) {
|
|
|
331
342
|
}, "No extensions selected. No changes were made.");
|
|
332
343
|
}
|
|
333
344
|
const requested = normalizeExtensionNames(selected.map((entry) => entry.id));
|
|
334
|
-
const actionById = new Map(selected.map((entry) => [entry.id, entry.action
|
|
345
|
+
const actionById = new Map(selected.map((entry) => [entry.id, entry.action || "install"]));
|
|
346
|
+
const installSelections = selected.filter((entry) => entry.action !== "uninstall" && entry.action !== "keep");
|
|
347
|
+
const uninstallSelections = selected.filter((entry) => entry.action === "uninstall");
|
|
348
|
+
const planningState = structuredClone(stateInspection.state);
|
|
349
|
+
for (const entry of uninstallSelections) delete planningState.extensions[entry.id];
|
|
335
350
|
const tools = resolveWorkspaceTools({ state: loadState(invocation.root), manifestTools: loadInitManifest().tools }).tools;
|
|
336
|
-
const preparation =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
351
|
+
const preparation = installSelections.length > 0
|
|
352
|
+
? await prepareRegistryExtensionPlans({
|
|
353
|
+
...dependencies,
|
|
354
|
+
requested: installSelections.map((entry) => entry.id),
|
|
355
|
+
tools,
|
|
356
|
+
state: planningState,
|
|
357
|
+
stateError: stateInspection.error,
|
|
358
|
+
extensionsRoot: dependencies.extensionsRoot,
|
|
359
|
+
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
360
|
+
provider: dependencies.nexusProvider,
|
|
361
|
+
})
|
|
362
|
+
: { plans: [], failures: [], diagnostics: [] };
|
|
363
|
+
preparation.plans = preparation.plans.map((plan) => ({ ...plan, action: actionById.get(plan.id) || "install" }));
|
|
364
|
+
const uninstallPlans = [];
|
|
365
|
+
const uninstallPreparationFailures = [];
|
|
366
|
+
for (const entry of uninstallSelections) {
|
|
367
|
+
try {
|
|
368
|
+
uninstallPlans.push(planExtensionUninstall(invocation.root, entry.id));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
uninstallPreparationFailures.push({ id: entry.id, version: null, status: "failed", code: error.code || "EXTENSION_UNINSTALL_FAILED", message: error.message, statePersisted: false, phase: "prepare" });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const effectiveUninstallPlans = uninstallPlans.filter((plan) => plan.action !== "skip");
|
|
374
|
+
if (preparation.plans.length > 0 || effectiveUninstallPlans.length > 0) {
|
|
375
|
+
const planText = formatExtensionChanges(preparation.plans, effectiveUninstallPlans, {
|
|
349
376
|
actionFor: (plan) => actionById.get(plan.id) || "install",
|
|
350
377
|
});
|
|
351
378
|
if (!(await (dependencies.confirm || confirm)(`${planText}\nContinue?`, invocation.options))) {
|
|
352
379
|
throw new WorkspaceError("CLI_CANCELLED", "Extension search selection cancelled.");
|
|
353
380
|
}
|
|
354
381
|
}
|
|
382
|
+
const uninstallResults = [];
|
|
383
|
+
for (const plan of effectiveUninstallPlans) {
|
|
384
|
+
try {
|
|
385
|
+
uninstallResults.push({ ...applyExtensionUninstall(plan, {
|
|
386
|
+
...(dependencies || {}),
|
|
387
|
+
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
388
|
+
}), action: "uninstall" });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
uninstallResults.push({ id: plan.id, version: plan.version, status: "failed", action: "uninstall", code: error.code || "EXTENSION_UNINSTALL_FAILED", message: error.message });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
355
393
|
const batch = runExtensionBatch(invocation.root, preparation.plans, (extension) => ({
|
|
356
394
|
schemaVersion: 1,
|
|
357
395
|
extensionSpecVersion: extension.extensionSpecVersion,
|
|
@@ -359,8 +397,8 @@ async function executeExtensionSearch(invocation) {
|
|
|
359
397
|
workspace: { name: workspace.name, uuid: workspace.uuid, language: workspace.language },
|
|
360
398
|
tools,
|
|
361
399
|
}), {
|
|
362
|
-
requested,
|
|
363
|
-
preFailures: preparation.failures,
|
|
400
|
+
requested: installSelections.map((entry) => entry.id),
|
|
401
|
+
preFailures: [...preparation.failures, ...uninstallPreparationFailures],
|
|
364
402
|
useExtensionStore: true,
|
|
365
403
|
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
366
404
|
});
|
|
@@ -381,12 +419,32 @@ async function executeExtensionSearch(invocation) {
|
|
|
381
419
|
extension: entry.id,
|
|
382
420
|
version: entry.version,
|
|
383
421
|
}))),
|
|
422
|
+
...uninstallResults.filter((entry) => entry.status === "failed").map((entry) => ({
|
|
423
|
+
code: entry.code || "EXTENSION_UNINSTALL_FAILED",
|
|
424
|
+
severity: "error",
|
|
425
|
+
message: entry.message || `Extension ${entry.id} failed to uninstall.`,
|
|
426
|
+
extension: entry.id,
|
|
427
|
+
version: entry.version,
|
|
428
|
+
})),
|
|
384
429
|
];
|
|
385
|
-
const
|
|
430
|
+
const installedResults = decorateBatchResults(batch, preparation.plans, invocation.options);
|
|
431
|
+
const resultById = new Map([
|
|
432
|
+
...installedResults.map((entry) => [entry.id, entry]),
|
|
433
|
+
...uninstallResults.map((entry) => [entry.id, entry]),
|
|
434
|
+
...uninstallPreparationFailures.map((entry) => [entry.id, entry]),
|
|
435
|
+
]);
|
|
436
|
+
const rawResults = selected.map((selection) => {
|
|
437
|
+
if (selection.action === "keep") {
|
|
438
|
+
return { id: selection.id, version: stateInspection.state.extensions[selection.id]?.installed?.version || null, status: "skipped", reason: "already-installed", action: "skip" };
|
|
439
|
+
}
|
|
440
|
+
const entry = resultById.get(selection.id);
|
|
441
|
+
return entry || { id: selection.id, status: "failed", message: "Extension operation did not run." };
|
|
442
|
+
});
|
|
443
|
+
const results = rawResults.map((entry) => lifecycleResultEntry(entry, entry.action || actionById.get(entry.id) || "install"));
|
|
386
444
|
ui.close?.("Extension selection applied.");
|
|
387
|
-
return selectionResult("extension.search", requested, results
|
|
445
|
+
return selectionResult("extension.search", requested, results, {
|
|
388
446
|
diagnostics,
|
|
389
|
-
text: lifecycleResultText(results, (entry) => actionById.get(entry.id) || "install"),
|
|
447
|
+
text: lifecycleResultText(results, (entry) => entry.action || actionById.get(entry.id) || "install"),
|
|
390
448
|
});
|
|
391
449
|
}
|
|
392
450
|
|
package/src/cli/commands/init.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
|
|
3
|
-
const { loadState } = require("../../core/config");
|
|
3
|
+
const { loadState, resolveWorkspaceDev } = require("../../core/config");
|
|
4
4
|
const { WorkspaceError } = require("../../core/errors");
|
|
5
5
|
const { acquireInitLock } = require("../../core/init-lock");
|
|
6
6
|
const {
|
|
7
|
+
applyExtensionUninstall,
|
|
7
8
|
discoverSystemExtensions,
|
|
8
9
|
emptyExtensionState,
|
|
9
10
|
inspectExtensionState,
|
|
10
11
|
parseExtensionSelection,
|
|
12
|
+
planExtensionUninstall,
|
|
11
13
|
prepareExtensionPlans,
|
|
12
14
|
runExtensionBatch,
|
|
13
15
|
} = require("../../core/extensions");
|
|
@@ -19,6 +21,7 @@ const { initializeWorkspace } = require("../../core/initializer");
|
|
|
19
21
|
const { resolveWorkspaceTools } = require("../../core/tools");
|
|
20
22
|
const { collectInitPlan } = require("../../init/wizard");
|
|
21
23
|
const { createRegistryExtensionPicker, mapPickerPage } = require("../../init/registry-picker");
|
|
24
|
+
const { formatExtensionChanges } = require("../../init/extension-summary");
|
|
22
25
|
const { success } = require("../result");
|
|
23
26
|
|
|
24
27
|
function stateWithPlans(state, plans) {
|
|
@@ -54,6 +57,17 @@ function migrationData(plan) {
|
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function parseDevOption(value) {
|
|
61
|
+
if (value === undefined) return undefined;
|
|
62
|
+
if (value === "true") return true;
|
|
63
|
+
if (value === "false") return false;
|
|
64
|
+
throw new WorkspaceError("CLI_INVALID_OPTION_VALUE", "--dev requires true or false", {
|
|
65
|
+
option: "dev",
|
|
66
|
+
value,
|
|
67
|
+
allowed: ["true", "false"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
async function executeInit(invocation) {
|
|
58
72
|
const root = path.resolve(invocation.args[0] || ".");
|
|
59
73
|
const releaseInitLock = await acquireInitLock(root);
|
|
@@ -66,6 +80,8 @@ async function executeInit(invocation) {
|
|
|
66
80
|
|
|
67
81
|
async function executeInitUnlocked(invocation, root) {
|
|
68
82
|
const { options } = invocation;
|
|
83
|
+
const requestedDev = parseDevOption(options.dev);
|
|
84
|
+
const dev = resolveWorkspaceDev(root, requestedDev);
|
|
69
85
|
const dependencies = invocation.dependencies && Object.prototype.hasOwnProperty.call(invocation.dependencies, "defaultRegistryUrl")
|
|
70
86
|
? invocation.dependencies
|
|
71
87
|
: { ...(invocation.dependencies || {}), defaultRegistryUrl: DEFAULT_NEXUS_REGISTRY };
|
|
@@ -97,7 +113,13 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
97
113
|
const extensionStateInspection = inspectExtensions ? inspectExtensionState(root) : { state: emptyExtensionState(), error: null };
|
|
98
114
|
const extensionState = extensionStateInspection.state;
|
|
99
115
|
const systemCatalogResult = inspectExtensions ? discoverSystemExtensions({ tolerant: true }) : { catalog: [], invalid: [] };
|
|
100
|
-
const
|
|
116
|
+
const systemExtensionIds = systemCatalogResult.catalog.filter((entry) => entry.latestSupported).map((entry) => entry.id);
|
|
117
|
+
const systemRequestedExtensions = dev ? systemExtensionIds : [];
|
|
118
|
+
const systemRemovalPlans = dev || extensionStateInspection.error
|
|
119
|
+
? []
|
|
120
|
+
: Object.entries(extensionState.extensions)
|
|
121
|
+
.filter(([id, entry]) => entry.installed?.system === true || systemExtensionIds.includes(id))
|
|
122
|
+
.map(([id]) => planExtensionUninstall(root, id, { systemManaged: true }));
|
|
101
123
|
let systemPreparation = { plans: [], failures: [], diagnostics: [] };
|
|
102
124
|
let planningState = extensionState;
|
|
103
125
|
const prepareSystemForTools = (tools) => {
|
|
@@ -112,17 +134,27 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
112
134
|
if (!interactive) prepareSystemForTools(resolvedTools.tools);
|
|
113
135
|
let interactiveOrdinaryPreparation = { plans: [], failures: [], diagnostics: [] };
|
|
114
136
|
const prepareOrdinaryExtensions = async (selected, selectedTools) => {
|
|
137
|
+
const installSelections = selected.filter((entry) => entry.action !== "uninstall" && entry.action !== "keep");
|
|
138
|
+
const uninstallSelections = selected.filter((entry) => entry.action === "uninstall");
|
|
139
|
+
const uninstallPlans = uninstallSelections.map((entry) => planExtensionUninstall(root, entry.id));
|
|
140
|
+
const installState = structuredClone(planningState);
|
|
141
|
+
for (const entry of uninstallSelections) delete installState.extensions[entry.id];
|
|
115
142
|
interactiveOrdinaryPreparation = await prepareRegistryExtensionPlans({
|
|
116
143
|
...dependencies,
|
|
117
|
-
requested:
|
|
144
|
+
requested: installSelections.map((entry) => entry.id),
|
|
118
145
|
tools: selectedTools || resolvedTools.tools,
|
|
119
|
-
state:
|
|
146
|
+
state: installState,
|
|
120
147
|
stateError: extensionStateInspection.error,
|
|
121
148
|
extensionsRoot: dependencies.extensionsRoot,
|
|
122
149
|
extensionStoreRoot: options.extensionStoreRoot || dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
123
150
|
provider: dependencies.nexusProvider,
|
|
124
151
|
});
|
|
125
|
-
|
|
152
|
+
interactiveOrdinaryPreparation.plans = interactiveOrdinaryPreparation.plans.map((plan) => ({
|
|
153
|
+
...plan,
|
|
154
|
+
action: selected.find((entry) => entry.id === plan.id)?.action || "install",
|
|
155
|
+
}));
|
|
156
|
+
interactiveOrdinaryPreparation.uninstallPlans = uninstallPlans;
|
|
157
|
+
return interactiveOrdinaryPreparation;
|
|
126
158
|
};
|
|
127
159
|
let plan = null;
|
|
128
160
|
if (interactive) {
|
|
@@ -138,7 +170,8 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
138
170
|
initialExtensions: explicitExtensions === null ? undefined : explicitExtensions,
|
|
139
171
|
prepareSystemExtensions: prepareSystemForTools,
|
|
140
172
|
prepareExtensions: prepareOrdinaryExtensions,
|
|
141
|
-
extensionPicker: createRegistryExtensionPicker({ dependencies, state: extensionState, systemIds: new Set(
|
|
173
|
+
extensionPicker: createRegistryExtensionPicker({ dependencies, state: extensionState, systemIds: new Set(systemExtensionIds) }),
|
|
174
|
+
formatExtensionChanges,
|
|
142
175
|
});
|
|
143
176
|
} catch (error) {
|
|
144
177
|
if (error.code === "INIT_CANCELLED") {
|
|
@@ -151,7 +184,12 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
151
184
|
const ordinaryRequestedExtensions = explicitExtensions !== null
|
|
152
185
|
? explicitExtensions
|
|
153
186
|
: plan ? plan.extensions.filter((entry) => entry.system !== true).map((entry) => entry.id) : [];
|
|
154
|
-
const
|
|
187
|
+
const ordinaryRemovalPlans = plan?.extensionRemovals || [];
|
|
188
|
+
const requestedExtensions = [...new Set([
|
|
189
|
+
...ordinaryRequestedExtensions,
|
|
190
|
+
...ordinaryRemovalPlans.map((entry) => typeof entry === "string" ? entry : entry.id),
|
|
191
|
+
...systemRequestedExtensions,
|
|
192
|
+
])];
|
|
155
193
|
const extensionPreparation = { plans: [], failures: [], diagnostics: [] };
|
|
156
194
|
if (interactive) {
|
|
157
195
|
extensionPreparation.plans = [...systemPreparation.plans, ...interactiveOrdinaryPreparation.plans];
|
|
@@ -182,6 +220,7 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
182
220
|
const result = await initializeWorkspace(root, {
|
|
183
221
|
run,
|
|
184
222
|
tools,
|
|
223
|
+
dev,
|
|
185
224
|
force: options.force === true,
|
|
186
225
|
yes: plan ? true : options.yes === true,
|
|
187
226
|
workspaceName: plan?.workspace.name || options["workspace-name"],
|
|
@@ -191,7 +230,46 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
191
230
|
initPlan: plan,
|
|
192
231
|
onStage: null,
|
|
193
232
|
});
|
|
194
|
-
const
|
|
233
|
+
const systemRemovalResults = [];
|
|
234
|
+
const systemRemovalDiagnostics = [];
|
|
235
|
+
for (const removalPlan of systemRemovalPlans) {
|
|
236
|
+
try {
|
|
237
|
+
systemRemovalResults.push(applyExtensionUninstall(removalPlan, {
|
|
238
|
+
systemManaged: true,
|
|
239
|
+
extensionStoreRoot: options.extensionStoreRoot || dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
240
|
+
}));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
systemRemovalResults.push({
|
|
243
|
+
id: removalPlan.id,
|
|
244
|
+
version: removalPlan.version,
|
|
245
|
+
status: "failed",
|
|
246
|
+
code: error.code || "SYSTEM_EXTENSION_DISABLE_FAILED",
|
|
247
|
+
message: error.message,
|
|
248
|
+
});
|
|
249
|
+
systemRemovalDiagnostics.push({
|
|
250
|
+
code: "SYSTEM_EXTENSION_DISABLE_FAILED",
|
|
251
|
+
severity: "warning",
|
|
252
|
+
message: `System extension ${removalPlan.id} could not be disabled: ${error.message}`,
|
|
253
|
+
extension: removalPlan.id,
|
|
254
|
+
causeCode: error.code || null,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const ordinaryRemovalResults = [];
|
|
259
|
+
const ordinaryRemovalDiagnostics = [];
|
|
260
|
+
for (const removalPlan of ordinaryRemovalPlans) {
|
|
261
|
+
const planToApply = typeof removalPlan === "string" ? planExtensionUninstall(root, removalPlan) : removalPlan;
|
|
262
|
+
try {
|
|
263
|
+
const removal = applyExtensionUninstall(planToApply, {
|
|
264
|
+
extensionStoreRoot: options.extensionStoreRoot || dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
265
|
+
});
|
|
266
|
+
ordinaryRemovalResults.push({ ...removal, action: removal.status === "skipped" ? "skip" : "uninstall" });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
ordinaryRemovalResults.push({ id: planToApply.id, version: planToApply.version || null, status: "failed", action: "uninstall", code: error.code || "EXTENSION_UNINSTALL_FAILED", message: error.message });
|
|
269
|
+
ordinaryRemovalDiagnostics.push({ code: error.code || "EXTENSION_UNINSTALL_FAILED", severity: "warning", message: `Extension ${planToApply.id} failed to uninstall: ${error.message}`, extension: planToApply.id });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const extensionBatch = runExtensionBatch(root, extensionPlans, (extension) => ({
|
|
195
273
|
schemaVersion: 1,
|
|
196
274
|
extensionSpecVersion: extension.extensionSpecVersion,
|
|
197
275
|
extension: { id: extension.id, version: extension.version },
|
|
@@ -202,12 +280,31 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
202
280
|
},
|
|
203
281
|
tools,
|
|
204
282
|
}), {
|
|
205
|
-
requested:
|
|
283
|
+
requested: [...new Set([...ordinaryRequestedExtensions, ...systemRequestedExtensions])],
|
|
206
284
|
preFailures: extensionPreparation.failures,
|
|
207
285
|
useExtensionStore: true,
|
|
208
286
|
extensionStoreRoot: options.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
209
287
|
});
|
|
288
|
+
const extensionResultEntries = [...ordinaryRemovalResults, ...extensionBatch.results];
|
|
289
|
+
const extensionResult = {
|
|
290
|
+
requested: requestedExtensions,
|
|
291
|
+
results: extensionResultEntries,
|
|
292
|
+
summary: {
|
|
293
|
+
installed: extensionResultEntries.filter((entry) => entry.status === "installed").length,
|
|
294
|
+
uninstalled: extensionResultEntries.filter((entry) => entry.status === "uninstalled").length,
|
|
295
|
+
skipped: extensionResultEntries.filter((entry) => entry.status === "skipped").length,
|
|
296
|
+
failed: extensionResultEntries.filter((entry) => entry.status === "failed").length,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
210
299
|
const extensionDiagnostics = [
|
|
300
|
+
...(dev === false && extensionStateInspection.error ? [{
|
|
301
|
+
code: "SYSTEM_EXTENSION_DISABLE_SKIPPED",
|
|
302
|
+
severity: "warning",
|
|
303
|
+
message: `System extension cleanup was skipped because extension state could not be read: ${extensionStateInspection.error.message}`,
|
|
304
|
+
causeCode: extensionStateInspection.error.code || null,
|
|
305
|
+
}] : []),
|
|
306
|
+
...systemRemovalDiagnostics,
|
|
307
|
+
...ordinaryRemovalDiagnostics,
|
|
211
308
|
...extensionPreparation.diagnostics,
|
|
212
309
|
...extensionResult.results
|
|
213
310
|
.filter((entry) => entry.status === "failed")
|
|
@@ -240,6 +337,7 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
240
337
|
managedFiles: result.managedFiles,
|
|
241
338
|
localConfig: result.localConfig,
|
|
242
339
|
workspace: result.workspaceConfig.workspace,
|
|
340
|
+
dev: result.workspaceConfig.workspace.dev,
|
|
243
341
|
language: result.language,
|
|
244
342
|
tools: toolSelection,
|
|
245
343
|
migration: migrationData(result.migration),
|
|
@@ -247,6 +345,10 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
247
345
|
verification: result.verification,
|
|
248
346
|
plan: result.initPlan,
|
|
249
347
|
stages: result.stages.map((stage) => stage.name),
|
|
348
|
+
systemExtensions: {
|
|
349
|
+
enabled: dev,
|
|
350
|
+
results: systemRemovalResults,
|
|
351
|
+
},
|
|
250
352
|
extensions: extensionResult,
|
|
251
353
|
};
|
|
252
354
|
const lines = [
|
|
@@ -254,9 +356,9 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
254
356
|
`Workspace: ${result.workspaceConfig.workspace.name} (${result.workspaceConfig.workspace.uuid})`,
|
|
255
357
|
`Language: ${result.language}`,
|
|
256
358
|
`Tools: ${tools.length ? tools.join(", ") : "none"} (${toolSelection.source})`,
|
|
257
|
-
`Extensions: ${extensionResult.summary.installed} installed, ${extensionResult.summary.skipped} skipped, ${extensionResult.summary.failed} failed`,
|
|
359
|
+
`Extensions: ${extensionResult.summary.installed} installed, ${extensionResult.summary.uninstalled || 0} uninstalled, ${extensionResult.summary.skipped} skipped, ${extensionResult.summary.failed} failed`,
|
|
258
360
|
];
|
|
259
|
-
if (result.localConfig.action === "write" || result.permissions.action === "skip") {
|
|
361
|
+
if (result.workspaceConfig.workspace.dev !== false && (result.localConfig.action === "write" || result.permissions.action === "skip")) {
|
|
260
362
|
lines.push(tools.length > 0
|
|
261
363
|
? "Add local projects with the `codew-add-projects` skill."
|
|
262
364
|
: "Add local projects with `code-workspace project inspect`, then register a complete project record.");
|
|
@@ -264,4 +366,4 @@ async function executeInitUnlocked(invocation, root) {
|
|
|
264
366
|
return success("init", data, lines.join("\n"), extensionDiagnostics);
|
|
265
367
|
}
|
|
266
368
|
|
|
267
|
-
module.exports = { createRegistryExtensionPicker, executeInit, mapPickerPage, migrationData, stateWithPlans };
|
|
369
|
+
module.exports = { createRegistryExtensionPicker, executeInit, mapPickerPage, migrationData, parseDevOption, stateWithPlans };
|
package/src/cli/registry.js
CHANGED
|
@@ -41,7 +41,7 @@ const COMMANDS = [
|
|
|
41
41
|
{ path: ["config", "set"], args: [{ name: "key", required: true }, { name: "value", required: true }], workspace: "none", config: [], interaction: "never", effects: "planned-write", options: {} },
|
|
42
42
|
{ path: ["config", "delete"], args: [{ name: "key", required: true }], workspace: "none", config: [], interaction: "never", effects: "planned-write", options: {} },
|
|
43
43
|
{ path: ["init"], args: [{ name: "path", required: false }], workspace: "target", config: [], interaction: "optional", effects: "planned-write", options: {
|
|
44
|
-
tools: { type: "string" }, extensions: { type: "string" }, "workspace-name": { type: "string" }, language: { type: "string" },
|
|
44
|
+
tools: { type: "string" }, extensions: { type: "string" }, "workspace-name": { type: "string" }, language: { type: "string" }, dev: { type: "string" },
|
|
45
45
|
yes: { type: "boolean" }, force: { type: "boolean" },
|
|
46
46
|
} },
|
|
47
47
|
{ path: ["update"], args: [], workspace: "required", config: [], interaction: "optional", effects: "planned-write", options: {
|
package/src/core/config.js
CHANGED
|
@@ -39,6 +39,22 @@ function statePath(root) {
|
|
|
39
39
|
return path.join(root, LOCAL_DIRECTORY, STATE_FILE);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function resolveWorkspaceDev(root, explicit) {
|
|
43
|
+
if (explicit !== undefined) return explicit;
|
|
44
|
+
if (!fs.existsSync(configPath(root))) return true;
|
|
45
|
+
const document = readConfigDocument(root);
|
|
46
|
+
const dev = document.value.workspace?.dev;
|
|
47
|
+
if (dev === undefined) return true;
|
|
48
|
+
if (typeof dev !== "boolean") {
|
|
49
|
+
throw new WorkspaceError("WORKSPACE_DEV_INVALID", "workspace.dev must be a boolean", {
|
|
50
|
+
actual: dev,
|
|
51
|
+
supported: [true, false],
|
|
52
|
+
file: document.file,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return dev;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
function findWorkspaceRoot(start = process.cwd()) {
|
|
43
59
|
let current = path.resolve(start);
|
|
44
60
|
while (true) {
|
|
@@ -91,7 +107,18 @@ function normalizeWorkspaceLanguage(value, options = {}) {
|
|
|
91
107
|
function normalizeWorkspace(value, options = {}) {
|
|
92
108
|
const identity = normalizeWorkspaceIdentity(value);
|
|
93
109
|
if (!identity) return null;
|
|
94
|
-
|
|
110
|
+
const dev = value.dev;
|
|
111
|
+
if (dev !== undefined && typeof dev !== "boolean") {
|
|
112
|
+
throw new WorkspaceError("WORKSPACE_DEV_INVALID", "workspace.dev must be a boolean", {
|
|
113
|
+
actual: dev,
|
|
114
|
+
supported: [true, false],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
...identity,
|
|
119
|
+
language: normalizeWorkspaceLanguage(value.language, options),
|
|
120
|
+
...(dev === undefined ? {} : { dev }),
|
|
121
|
+
};
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
function normalizeProjects(value) {
|
|
@@ -508,6 +535,7 @@ module.exports = {
|
|
|
508
535
|
renderProjectConfig,
|
|
509
536
|
resolveProjectConfigPath,
|
|
510
537
|
resolveProjectConfigReference,
|
|
538
|
+
resolveWorkspaceDev,
|
|
511
539
|
requireWorkspaceRoot,
|
|
512
540
|
saveConfig,
|
|
513
541
|
saveProjectConfig,
|
package/src/core/extensions.js
CHANGED
|
@@ -1453,14 +1453,15 @@ function runExtensionBatch(root, plans, context, options = {}) {
|
|
|
1453
1453
|
};
|
|
1454
1454
|
}
|
|
1455
1455
|
|
|
1456
|
-
function planExtensionUninstall(root, id) {
|
|
1456
|
+
function planExtensionUninstall(root, id, options = {}) {
|
|
1457
1457
|
const extensionId = validateExtensionName(id);
|
|
1458
|
-
|
|
1458
|
+
const systemManaged = options.systemManaged === true;
|
|
1459
|
+
if (!systemManaged && isSystemExtensionId(extensionId)) {
|
|
1459
1460
|
throw extensionError("EXTENSION_SYSTEM_MANAGED", `System extension cannot be manually uninstalled: ${extensionId}`, { extension: extensionId });
|
|
1460
1461
|
}
|
|
1461
1462
|
const state = loadExtensionState(root);
|
|
1462
1463
|
const installed = state.extensions[extensionId]?.installed || null;
|
|
1463
|
-
if (installed?.system === true) {
|
|
1464
|
+
if (!systemManaged && installed?.system === true) {
|
|
1464
1465
|
throw extensionError("EXTENSION_SYSTEM_MANAGED", `System extension cannot be manually uninstalled: ${extensionId}`, { extension: extensionId });
|
|
1465
1466
|
}
|
|
1466
1467
|
if (!installed) return Object.freeze({ root: path.resolve(root), id: extensionId, action: "skip", reason: "not-installed", targets: [] });
|
|
@@ -1473,6 +1474,7 @@ function planExtensionUninstall(root, id) {
|
|
|
1473
1474
|
return Object.freeze({
|
|
1474
1475
|
root: path.resolve(root),
|
|
1475
1476
|
id: extensionId,
|
|
1477
|
+
system: installed.system === true,
|
|
1476
1478
|
action: "remove",
|
|
1477
1479
|
version: installed.version,
|
|
1478
1480
|
packageSha256: installed.packageSha256 || null,
|
|
@@ -1485,7 +1487,7 @@ function planExtensionUninstall(root, id) {
|
|
|
1485
1487
|
}
|
|
1486
1488
|
|
|
1487
1489
|
function applyExtensionUninstall(plan, options = {}) {
|
|
1488
|
-
if (plan.system === true || isSystemExtensionId(plan.id)) {
|
|
1490
|
+
if (options.systemManaged !== true && (plan.system === true || isSystemExtensionId(plan.id))) {
|
|
1489
1491
|
throw extensionError("EXTENSION_SYSTEM_MANAGED", `System extension cannot be manually uninstalled: ${plan.id}`, { extension: plan.id });
|
|
1490
1492
|
}
|
|
1491
1493
|
if (plan.action === "skip") return { id: plan.id, status: "skipped", reason: plan.reason, removed: [] };
|
package/src/core/initializer.js
CHANGED
|
@@ -50,11 +50,16 @@ async function collectWorkspaceSetup(root, options = {}) {
|
|
|
50
50
|
const file = configPath(root);
|
|
51
51
|
if (fs.existsSync(file)) {
|
|
52
52
|
const current = loadConfig(root, { defaultLanguage: language });
|
|
53
|
+
const workspace = {
|
|
54
|
+
...current.workspace,
|
|
55
|
+
dev: options.dev ?? current.workspace.dev ?? true,
|
|
56
|
+
};
|
|
53
57
|
return normalizeConfig({
|
|
54
58
|
...current,
|
|
55
|
-
workspace: current.workspace
|
|
59
|
+
workspace: current.workspace ? workspace : {
|
|
56
60
|
name: options.workspaceName || DEFAULT_WORKSPACE_NAME,
|
|
57
61
|
uuid: randomUUID(),
|
|
62
|
+
dev: options.dev ?? true,
|
|
58
63
|
},
|
|
59
64
|
});
|
|
60
65
|
}
|
|
@@ -71,7 +76,7 @@ async function collectWorkspaceSetup(root, options = {}) {
|
|
|
71
76
|
}
|
|
72
77
|
return normalizeConfig({
|
|
73
78
|
schemaVersion: 2,
|
|
74
|
-
workspace: { name: name || DEFAULT_WORKSPACE_NAME, uuid: options.workspaceUuid || randomUUID(), language },
|
|
79
|
+
workspace: { name: name || DEFAULT_WORKSPACE_NAME, uuid: options.workspaceUuid || randomUUID(), language, dev: options.dev ?? true },
|
|
75
80
|
projects: [],
|
|
76
81
|
});
|
|
77
82
|
}
|
|
@@ -106,7 +111,9 @@ async function initializeWorkspaceStages(rootInput, options = {}) {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
const dependencies = await stage("Install workspace dependencies", () =>
|
|
109
|
-
|
|
114
|
+
workspaceConfig.workspace.dev === false
|
|
115
|
+
? { action: "skip", reason: "development mode disabled" }
|
|
116
|
+
: installWorkspaceDependencies(root, { run })
|
|
110
117
|
);
|
|
111
118
|
if (dependencies.action === "install") {
|
|
112
119
|
options.transaction?.recordExternalEffect({
|
|
@@ -50,8 +50,13 @@ function mergePageSelections(selected, page, values) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function selectedResult(selected) {
|
|
54
|
-
|
|
53
|
+
function selectedResult(selected, installedIds = []) {
|
|
54
|
+
const result = [...selected.entries()].map(([id, action]) => ({ id, action }));
|
|
55
|
+
const selectedIds = new Set(selected.keys());
|
|
56
|
+
for (const id of installedIds) {
|
|
57
|
+
if (!selectedIds.has(id)) result.push({ id, action: "uninstall" });
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
function pageOptions(page) {
|
|
@@ -140,7 +145,9 @@ async function runClackExtensionPicker(options = {}) {
|
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
const selected = new Map();
|
|
143
|
-
|
|
148
|
+
const selectionActions = options.selectionActions || {};
|
|
149
|
+
for (const id of options.selectedIds || []) selected.set(String(id), selectionActions[id] || "install");
|
|
150
|
+
const installedIds = [...new Set((options.installedIds || []).map(String))];
|
|
144
151
|
let query = String(options.query || "");
|
|
145
152
|
let session = null;
|
|
146
153
|
let page = null;
|
|
@@ -203,7 +210,7 @@ async function runClackExtensionPicker(options = {}) {
|
|
|
203
210
|
}
|
|
204
211
|
if (action === "done") {
|
|
205
212
|
closeSession();
|
|
206
|
-
return { status: "submitted", selections: selectedResult(selected) };
|
|
213
|
+
return { status: "submitted", selections: selectedResult(selected, installedIds) };
|
|
207
214
|
}
|
|
208
215
|
if (action === "search") {
|
|
209
216
|
promptForQuery = true;
|
|
@@ -255,7 +262,7 @@ async function runClackExtensionPicker(options = {}) {
|
|
|
255
262
|
closeSession();
|
|
256
263
|
return {
|
|
257
264
|
status: "submitted",
|
|
258
|
-
selections: selectedResult(selected),
|
|
265
|
+
selections: selectedResult(selected, installedIds),
|
|
259
266
|
};
|
|
260
267
|
}
|
|
261
268
|
if (action === "search") {
|
|
@@ -284,7 +291,7 @@ async function runClackExtensionPicker(options = {}) {
|
|
|
284
291
|
closeSession();
|
|
285
292
|
return {
|
|
286
293
|
status: "submitted",
|
|
287
|
-
selections: selectedResult(selected),
|
|
294
|
+
selections: selectedResult(selected, installedIds),
|
|
288
295
|
};
|
|
289
296
|
}
|
|
290
297
|
if (retryAction === "retry") {
|
|
@@ -23,8 +23,8 @@ function normalizePickerItem(value) {
|
|
|
23
23
|
const status = PICKER_STATUSES.includes(item.status)
|
|
24
24
|
? item.status
|
|
25
25
|
: item.available === false ? "unavailable" : "not-installed";
|
|
26
|
-
const disabled = item.disabled === true || status === "
|
|
27
|
-
const action = disabled ? "none" : status === "installed-outdated" ? "update" : "install";
|
|
26
|
+
const disabled = item.disabled === true || status === "unavailable" || (status === "installed-current" && item.allowUninstall !== true);
|
|
27
|
+
const action = disabled ? "none" : item.action || (status === "installed-outdated" ? "update" : status === "installed-current" ? "keep" : "install");
|
|
28
28
|
return Object.freeze({
|
|
29
29
|
...item,
|
|
30
30
|
id,
|
|
@@ -69,6 +69,7 @@ function createPickerState(options = {}) {
|
|
|
69
69
|
error: null,
|
|
70
70
|
selectedIds: Object.freeze([...(options.selectedIds || [])]),
|
|
71
71
|
selectionActions: Object.freeze({ ...(options.selectionActions || {}) }),
|
|
72
|
+
installedIds: Object.freeze([...(options.installedIds || [])]),
|
|
72
73
|
cancelled: false,
|
|
73
74
|
submitted: false,
|
|
74
75
|
statusMessage: "",
|
|
@@ -80,7 +81,12 @@ function withState(state, changes) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function selectedResult(state) {
|
|
83
|
-
|
|
84
|
+
const selected = new Set(state.selectedIds);
|
|
85
|
+
const result = state.selectedIds.map((id) => ({ id, action: state.selectionActions[id] || "install" }));
|
|
86
|
+
for (const id of state.installedIds || []) {
|
|
87
|
+
if (!selected.has(id)) result.push({ id, action: "uninstall" });
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
function currentItem(state) {
|
|
@@ -284,7 +290,7 @@ async function runExtensionPicker(options = {}) {
|
|
|
284
290
|
renderedLines = lines.length + 1;
|
|
285
291
|
});
|
|
286
292
|
const events = options.events || keypressEvents(options.input || process.stdin);
|
|
287
|
-
let state = createPickerState({ query: options.query || "", selectedIds: options.selectedIds, selectionActions: options.selectionActions });
|
|
293
|
+
let state = createPickerState({ query: options.query || "", selectedIds: options.selectedIds, selectionActions: options.selectionActions, installedIds: options.installedIds });
|
|
288
294
|
let session = await resolvePageProvider(pageProvider, state.query);
|
|
289
295
|
|
|
290
296
|
async function load(method, event = "load-start") {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { LOCAL_DIRECTORY } = require("../core/config");
|
|
2
|
+
const { EXTENSION_STATE_FILE } = require("../core/extensions");
|
|
3
|
+
|
|
4
|
+
function pluralizeExtension(count) {
|
|
5
|
+
return `extension${count === 1 ? "" : "s"}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function installAction(plan, actionFor) {
|
|
9
|
+
const action = actionFor(plan);
|
|
10
|
+
return action === "update" ? "Update" : "Install";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatExtensionChanges(installPlans = [], uninstallPlans = [], options = {}) {
|
|
14
|
+
const plans = Array.from(installPlans || []);
|
|
15
|
+
const removals = Array.from(uninstallPlans || []);
|
|
16
|
+
const total = plans.length + removals.filter((plan) => plan?.action !== "skip").length;
|
|
17
|
+
if (total === 0) return "No extension changes.";
|
|
18
|
+
|
|
19
|
+
const hasInstall = plans.length > 0;
|
|
20
|
+
const hasUninstall = removals.some((plan) => plan?.action !== "skip");
|
|
21
|
+
const title = options.title || (hasInstall && hasUninstall
|
|
22
|
+
? `Apply ${total} extension changes:`
|
|
23
|
+
: hasUninstall
|
|
24
|
+
? `Uninstall ${total} ${pluralizeExtension(total)}:`
|
|
25
|
+
: `${options.action === "update" ? "Update" : "Install"} ${total} ${pluralizeExtension(total)}:`);
|
|
26
|
+
const actionFor = options.actionFor || ((plan) => plan.action || options.action || "install");
|
|
27
|
+
const lines = [title];
|
|
28
|
+
|
|
29
|
+
for (const plan of plans) {
|
|
30
|
+
const details = [plan.source, plan.extensionSpecVersion ? `Extension Spec ${plan.extensionSpecVersion}` : null].filter(Boolean);
|
|
31
|
+
lines.push(` ${installAction(plan, actionFor)} ${plan.id}@${plan.version}${details.length ? ` [${details.join("] [")}]` : ""}`);
|
|
32
|
+
lines.push(` PACKAGE ${plan.packageSha256}`);
|
|
33
|
+
for (const host of plan.capabilities?.networkHosts || []) lines.push(` NETWORK https://${host}`);
|
|
34
|
+
for (const artifact of plan.artifacts || []) lines.push(` WRITE ${artifact.target} (${artifact.kind})`);
|
|
35
|
+
for (const hook of plan.hooks || []) {
|
|
36
|
+
lines.push(` HOOK ${hook.id} (${hook.event}${hook.tools?.length ? ` · ${hook.tools.join(",")}` : ""}) -> ${hook.command}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const plan of removals) {
|
|
41
|
+
if (!plan || plan.action === "skip") continue;
|
|
42
|
+
const id = typeof plan === "string" ? plan : plan.id;
|
|
43
|
+
const version = typeof plan === "string" ? null : plan.version;
|
|
44
|
+
lines.push(` Uninstall ${id}${version ? `@${version}` : ""}`);
|
|
45
|
+
for (const target of plan.targets || []) lines.push(` REMOVE ${target}`);
|
|
46
|
+
lines.push(` REMOVE ${LOCAL_DIRECTORY}/${EXTENSION_STATE_FILE} entry`);
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { formatExtensionChanges };
|
package/src/init/plan.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
function createInitPlan({ root, workspace, tools, language, extensions = [] }) {
|
|
1
|
+
function createInitPlan({ root, workspace, tools, language, extensions = [], extensionRemovals = [] }) {
|
|
2
2
|
return {
|
|
3
3
|
root,
|
|
4
4
|
workspace,
|
|
5
5
|
tools: tools.slice(),
|
|
6
6
|
language,
|
|
7
7
|
extensions: extensions.slice(),
|
|
8
|
+
extensionRemovals: extensionRemovals.slice(),
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -23,7 +23,9 @@ function mapPickerPage(page, state = emptyExtensionState(), systemIds = new Set(
|
|
|
23
23
|
version: candidate?.version || null,
|
|
24
24
|
packageSha256: candidate?.packageSha256 || null,
|
|
25
25
|
status,
|
|
26
|
-
disabled: status === "
|
|
26
|
+
disabled: status === "unavailable",
|
|
27
|
+
allowUninstall: Boolean(installed),
|
|
28
|
+
action: status === "installed-outdated" ? "update" : status === "installed-current" ? "keep" : "install",
|
|
27
29
|
};
|
|
28
30
|
});
|
|
29
31
|
return { ...page, items };
|
|
@@ -31,9 +33,15 @@ function mapPickerPage(page, state = emptyExtensionState(), systemIds = new Set(
|
|
|
31
33
|
|
|
32
34
|
function createRegistryExtensionPicker({ dependencies = {}, state = emptyExtensionState(), systemIds = new Set() }) {
|
|
33
35
|
const createSession = dependencies.createRegistryExtensionBrowseSession || createRegistryExtensionBrowseSession;
|
|
36
|
+
const installedIds = Object.entries(state.extensions || {})
|
|
37
|
+
.filter(([id, entry]) => entry?.installed && entry.installed.system !== true && !systemIds.has(id))
|
|
38
|
+
.map(([id]) => id);
|
|
39
|
+
const installedSelectionActions = Object.fromEntries(installedIds.map((id) => [id, "keep"]));
|
|
34
40
|
return async ({ ui, query = "", selectedIds = [] }) => ui.extensionPicker({
|
|
35
41
|
query,
|
|
36
|
-
selectedIds,
|
|
42
|
+
selectedIds: selectedIds.length > 0 ? selectedIds : installedIds,
|
|
43
|
+
selectionActions: installedSelectionActions,
|
|
44
|
+
installedIds,
|
|
37
45
|
pageProvider: async (pageQuery) => {
|
|
38
46
|
const session = await createSession({
|
|
39
47
|
...dependencies,
|
package/src/init/wizard.js
CHANGED
|
@@ -17,15 +17,25 @@ function normalizePickerSelections(value) {
|
|
|
17
17
|
}).filter((entry) => entry.id);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function normalizePreparedExtensions(value, selected = []) {
|
|
21
|
+
const prepared = Array.isArray(value) ? { plans: value } : (value || {});
|
|
22
|
+
return {
|
|
23
|
+
plans: Array.from(prepared.plans || []),
|
|
24
|
+
uninstallPlans: Array.isArray(prepared.uninstallPlans)
|
|
25
|
+
? Array.from(prepared.uninstallPlans)
|
|
26
|
+
: selected.filter((entry) => entry.action === "uninstall").map((entry) => entry.id),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
async function collectExtensionPlans(options, ui, tools) {
|
|
21
31
|
if (options.extensions !== undefined) {
|
|
22
32
|
const selected = normalizePickerSelections(options.extensions);
|
|
23
|
-
if (typeof options.prepareExtensions === "function") return options.prepareExtensions(selected, tools);
|
|
33
|
+
if (typeof options.prepareExtensions === "function") return normalizePreparedExtensions(await options.prepareExtensions(selected, tools), selected);
|
|
24
34
|
const catalog = options.extensionCatalog || [];
|
|
25
|
-
return resolveExtensionPlans(catalog, selected.map((entry) => entry.id), {
|
|
35
|
+
return { plans: resolveExtensionPlans(catalog, selected.filter((entry) => entry.action !== "uninstall" && entry.action !== "keep").map((entry) => entry.id), {
|
|
26
36
|
tools,
|
|
27
37
|
state: options.extensionState,
|
|
28
|
-
});
|
|
38
|
+
}), uninstallPlans: [] };
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
if (typeof options.extensionPicker === "function") {
|
|
@@ -35,21 +45,21 @@ async function collectExtensionPlans(options, ui, tools) {
|
|
|
35
45
|
selectedIds: options.initialExtensions || [],
|
|
36
46
|
query: options.extensionQuery || "",
|
|
37
47
|
}));
|
|
38
|
-
if (typeof options.prepareExtensions === "function") return options.prepareExtensions(selected, tools);
|
|
39
|
-
return selected;
|
|
48
|
+
if (typeof options.prepareExtensions === "function") return normalizePreparedExtensions(await options.prepareExtensions(selected, tools), selected);
|
|
49
|
+
return { plans: selected.filter((entry) => entry.action !== "uninstall" && entry.action !== "keep"), uninstallPlans: selected.filter((entry) => entry.action === "uninstall").map((entry) => entry.id) };
|
|
40
50
|
} catch (error) {
|
|
41
51
|
ui.note("Extensions unavailable", [
|
|
42
52
|
error.message || "The Nexus extension Registry could not be reached.",
|
|
43
53
|
"Ordinary extensions will be skipped; core Workspace initialization can continue.",
|
|
44
54
|
]);
|
|
45
|
-
return [];
|
|
55
|
+
return { plans: [], uninstallPlans: [] };
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
// Kept for direct callers that inject a catalog (production init no longer does).
|
|
50
60
|
const extensionCatalog = options.extensionCatalog || [];
|
|
51
61
|
const supportedExtensions = extensionCatalog.filter((entry) => entry.latestSupported);
|
|
52
|
-
if (supportedExtensions.length === 0) return [];
|
|
62
|
+
if (supportedExtensions.length === 0) return { plans: [], uninstallPlans: [] };
|
|
53
63
|
const extensionNames = await ui.multiselect(
|
|
54
64
|
"Extensions (experimental, select any)",
|
|
55
65
|
supportedExtensions.map((entry) => ({
|
|
@@ -58,10 +68,10 @@ async function collectExtensionPlans(options, ui, tools) {
|
|
|
58
68
|
})),
|
|
59
69
|
options.initialExtensions !== undefined ? options.initialExtensions : []
|
|
60
70
|
);
|
|
61
|
-
return resolveExtensionPlans(extensionCatalog, extensionNames, {
|
|
71
|
+
return { plans: resolveExtensionPlans(extensionCatalog, extensionNames, {
|
|
62
72
|
tools,
|
|
63
73
|
state: options.extensionState,
|
|
64
|
-
});
|
|
74
|
+
}), uninstallPlans: [] };
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
async function collectInitPlan(root, manifest, options = {}) {
|
|
@@ -101,18 +111,31 @@ async function collectInitPlan(root, manifest, options = {}) {
|
|
|
101
111
|
const systemExtensions = typeof options.prepareSystemExtensions === "function"
|
|
102
112
|
? await options.prepareSystemExtensions(tools)
|
|
103
113
|
: (options.systemExtensions || []);
|
|
114
|
+
const ordinary = await collectExtensionPlans(options, ui, tools);
|
|
104
115
|
const extensions = [
|
|
105
116
|
...systemExtensions,
|
|
106
|
-
...
|
|
117
|
+
...ordinary.plans,
|
|
107
118
|
];
|
|
108
119
|
const workspace = existing?.workspace || { name, uuid: randomUUID() };
|
|
109
|
-
const
|
|
120
|
+
const extensionRemovals = ordinary.uninstallPlans || [];
|
|
121
|
+
const plan = createInitPlan({ root, workspace, tools, language, extensions, extensionRemovals });
|
|
122
|
+
const extensionChanges = [
|
|
123
|
+
...extensions.map((entry) => `${entry.action === "update" ? "Update" : "Install"} ${entry.id}@${entry.version}`),
|
|
124
|
+
...extensionRemovals.map((entry) => typeof entry === "string" ? `Uninstall ${entry}` : `Uninstall ${entry.id}@${entry.version}`),
|
|
125
|
+
];
|
|
110
126
|
ui.note("Ready to initialize", [
|
|
111
127
|
`Workspace ${workspace.name}`,
|
|
112
128
|
`Language ${language}`,
|
|
113
129
|
`Tools ${tools.length ? tools.join(", ") : "none"}`,
|
|
114
130
|
`Extensions ${extensions.length ? extensions.map((entry) => `${entry.id}@${entry.version} [Spec ${entry.extensionSpecVersion}] (${entry.manifestSha256})`).join(", ") : "none"}`,
|
|
131
|
+
`Changes ${extensionChanges.length ? extensionChanges.join(", ") : "none"}`,
|
|
115
132
|
]);
|
|
133
|
+
if (extensionChanges.length) {
|
|
134
|
+
const formatted = typeof options.formatExtensionChanges === "function"
|
|
135
|
+
? options.formatExtensionChanges(extensions, extensionRemovals)
|
|
136
|
+
: extensionChanges.join("\n");
|
|
137
|
+
ui.note("Extension changes", formatted.split("\n"));
|
|
138
|
+
}
|
|
116
139
|
if (existing?.projects?.length) {
|
|
117
140
|
const permissionPlan = planPermissionChanges({
|
|
118
141
|
root,
|