code-workspace-zhuiyi 1.0.0-beta.3 → 1.0.0-beta.5
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 +5 -13
- package/README.zh-CN.md +4 -13
- package/artifacts/manifest.json +1 -1
- package/docs/extension-development/extension-development-guide.zh-CN.md +40 -9
- package/package.json +1 -1
- package/src/cli/commands/extension.js +166 -38
- package/src/cli/commands/init.js +133 -12
- package/src/cli/registry.js +7 -1
- package/src/core/config.js +29 -1
- package/src/core/extension-package.js +205 -0
- package/src/core/extension-scaffold.js +193 -0
- package/src/core/extensions.js +11 -5
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Code Workspace
|
|
2
2
|
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
Code Workspace is a local multi-project registry and safety layer for Claude Code and Codex. It manages workspace identity, project locations and branches, agent instructions, writable-root permissions, validation, and optional monitoring.
|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
@@ -13,11 +15,8 @@ Code Workspace is a local multi-project registry and safety layer for Claude Cod
|
|
|
13
15
|
npm install -g code-workspace-zhuiyi@latest
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
The package provides the `code-workspace` command and the shorter `codew` alias. The legacy `code-w` alias remains available for compatibility.
|
|
17
|
-
|
|
18
18
|
## Initialize
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
Interactive initialization:
|
|
22
21
|
|
|
23
22
|
```bash
|
|
@@ -36,6 +35,8 @@ codew init .code-workspace \
|
|
|
36
35
|
--yes
|
|
37
36
|
```
|
|
38
37
|
|
|
38
|
+
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.
|
|
39
|
+
|
|
39
40
|
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
41
|
|
|
41
42
|
Initialization writes only Workspace-owned state and integrations:
|
|
@@ -224,16 +225,7 @@ codew language --json
|
|
|
224
225
|
|
|
225
226
|
`update` refreshes only Workspace-owned managed assets and never changes Agent directory authorization. Unknown local edits stop the batch before writes; review them or pass `--force` explicitly.
|
|
226
227
|
|
|
227
|
-
##
|
|
228
|
-
|
|
229
|
-
Monitor is an ordinary Nexus extension. Install it into a workspace and run its dashboard through the generic extension runtime:
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
codew extension install monitor --yes
|
|
233
|
-
codew ext monitor serve --port 3211
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
The monitor binds to loopback, combines events from multiple initialized workspaces, and keeps hook reporting (`codew ext monitor report`) failure-open. Review and trust project hooks in Codex before relying on reports.
|
|
228
|
+
## Completion
|
|
237
229
|
|
|
238
230
|
```bash
|
|
239
231
|
codew completion --shell zsh
|
package/README.zh-CN.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Code Workspace
|
|
2
2
|
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
Code Workspace 是面向 Claude Code 与 Codex 的本地多项目注册表和安全边界层,负责工作区身份、项目路径与分支、Agent 指令、可写目录权限、校验以及可选监控。
|
|
4
6
|
|
|
5
7
|
## 环境要求
|
|
@@ -13,8 +15,6 @@ Code Workspace 是面向 Claude Code 与 Codex 的本地多项目注册表和安
|
|
|
13
15
|
npm install -g code-workspace-zhuiyi@latest
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
软件包提供 `code-workspace` 命令及短别名 `codew`。为保持兼容,旧别名 `code-w` 仍然可用。
|
|
17
|
-
|
|
18
18
|
## 初始化
|
|
19
19
|
|
|
20
20
|
交互式初始化:
|
|
@@ -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 自有状态和集成:
|
|
@@ -221,17 +223,6 @@ codew language --json
|
|
|
221
223
|
|
|
222
224
|
`update` 只刷新 Workspace 自有托管资产,绝不会修改 Agent 目录授权。遇到未知本地修改时,会在任何写入前终止批次;请先审查修改,或显式传入 `--force`。
|
|
223
225
|
|
|
224
|
-
## 监控
|
|
225
|
-
|
|
226
|
-
监控以 Nexus 中的 `monitor` 扩展提供。先在 Workspace 中安装扩展,再通过通用扩展运行时启动面板:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
codew extension install monitor --yes
|
|
230
|
-
codew ext monitor serve --port 3211
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
监控服务仅绑定 loopback,可汇总多个已初始化工作区的事件;hook 上报(`codew ext monitor report`)失败不会阻断 Agent。依赖监控前,请在 Codex 中检查并信任项目 hook。
|
|
234
|
-
|
|
235
226
|
## 命令补全
|
|
236
227
|
|
|
237
228
|
```bash
|
package/artifacts/manifest.json
CHANGED
|
@@ -35,7 +35,23 @@ Host 负责确认、校验、事务提交、状态记录、升级和卸载。
|
|
|
35
35
|
|
|
36
36
|
## 2. 包目录
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
系统扩展使用固定版本目录;普通扩展建议使用 `extension init` 创建最小 package 空壳,再按 Extension Spec 添加能力并发布到 Nexus:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
codew extension init ./example-extension --yes
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
开发 package 的目录结构为:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
example-extension/
|
|
48
|
+
├── extension/
|
|
49
|
+
│ ├── manifest.json
|
|
50
|
+
│ └── init.js
|
|
51
|
+
└── package.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
系统扩展仍使用固定版本目录:
|
|
39
55
|
|
|
40
56
|
```text
|
|
41
57
|
extensions/<extension-id>/<version>/
|
|
@@ -45,7 +61,7 @@ extensions/<extension-id>/<version>/
|
|
|
45
61
|
└── SKILL.md
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
公开 CLI 的 `extension pack <source>` 要求 `<source>` 直接指向包含 `package.json` 和 `extension/` 的 package 根目录。空壳尚未声明 `outputs`、`hooks` 或 `runtime` 时不能打包。
|
|
49
65
|
|
|
50
66
|
命名要求:
|
|
51
67
|
|
|
@@ -53,11 +69,18 @@ extensions/<extension-id>/<version>/
|
|
|
53
69
|
- `version` 必须是完整 SemVer,例如 `1.0.0`;
|
|
54
70
|
- 已发布版本目录不可修改;新实现创建新的版本目录。
|
|
55
71
|
|
|
56
|
-
|
|
72
|
+
根目录 `package.json` 是固定格式的 transport envelope,不要添加 `dependencies`、`scripts` 或其他 npm lifecycle 字段。`extension pack` 只校验并打包这个 envelope,不会自动生成或修改它。
|
|
73
|
+
|
|
74
|
+
`extension init` 只询问扩展 id、显示名称、描述和版本,不预设扩展是 Skill、MCP、Hook 还是 Runtime。完成 manifest 能力声明后,再运行:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codew extension digest update ./example-extension --yes
|
|
78
|
+
codew extension pack ./example-extension --output ./dist/extensions --json
|
|
79
|
+
```
|
|
57
80
|
|
|
58
81
|
## 3. manifest
|
|
59
82
|
|
|
60
|
-
Spec v1 使用 manifest schema v3
|
|
83
|
+
Spec v1 使用 manifest schema v3。下面是声明了一个 file output 的最小完整示例;`extension init` 生成的空壳会在用户添加能力前缺少 `outputs`、`hooks` 或 `runtime`:
|
|
61
84
|
|
|
62
85
|
```json
|
|
63
86
|
{
|
|
@@ -89,7 +112,13 @@ Spec v1 使用 manifest schema v3。最小示例:
|
|
|
89
112
|
node -e "process.stdout.write(require('node:crypto').createHash('sha256').update(require('node:fs').readFileSync('extensions/example-extension/1.0.0/init.js')).digest('hex'))"
|
|
90
113
|
```
|
|
91
114
|
|
|
92
|
-
每次修改 `init.js`
|
|
115
|
+
每次修改 `init.js` 或 `extension/` 下其他文件后,使用 CLI 同步摘要:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
codew extension digest update ./example-extension --yes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
该命令会先更新 `extension/manifest.json.entrySha256`,再更新 `package.json.codeWorkspace.packageSha256`。
|
|
93
122
|
|
|
94
123
|
### 3.1 身份与展示
|
|
95
124
|
|
|
@@ -296,10 +325,10 @@ codew extension upgrade example-extension --yes
|
|
|
296
325
|
|
|
297
326
|
## 6. npm/Nexus 打包
|
|
298
327
|
|
|
299
|
-
|
|
328
|
+
打包单个 package 根目录:
|
|
300
329
|
|
|
301
330
|
```bash
|
|
302
|
-
code-workspace extension pack
|
|
331
|
+
code-workspace extension pack ./example-extension --output dist/extensions --json
|
|
303
332
|
```
|
|
304
333
|
|
|
305
334
|
输出目录缺失时会递归创建。目标文件已存在时命令拒绝覆盖;重复打包前使用新的输出目录,或清理上一次构建产物。
|
|
@@ -331,14 +360,16 @@ npm 身份固定映射为:
|
|
|
331
360
|
|
|
332
361
|
打包器会:
|
|
333
362
|
|
|
334
|
-
- 校验 manifest、入口摘要、目录类型和 Extension Spec;
|
|
335
|
-
-
|
|
363
|
+
- 校验 package envelope、manifest、入口摘要、目录类型和 Extension Spec;
|
|
364
|
+
- 原样保留已验证的 `package.json`,不生成或修改 envelope;
|
|
336
365
|
- 只归档普通文件;
|
|
337
366
|
- 拒绝符号链接、硬链接、特殊文件、路径逃逸和重复路径;
|
|
338
367
|
- 限制最多 1024 个文件、单文件 2 MiB、载荷总计 16 MiB、envelope 64 KiB;
|
|
339
368
|
- 重新读取 tarball 并验证 envelope、manifest、入口和 package digest;
|
|
340
369
|
- 使用同目录临时文件并原子提交输出。
|
|
341
370
|
|
|
371
|
+
开发 package 的 README 和其他根目录文件不会进入 tarball;最终内容严格由 `package.json.files` 中的 `extension` 决定。
|
|
372
|
+
|
|
342
373
|
成功 JSON 返回 npm name、扩展身份、tarball 绝对路径、tarball integrity、manifest/entry/package digest 和文件清单。
|
|
343
374
|
|
|
344
375
|
## 7. 发布边界
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
1
2
|
const { LOCAL_DIRECTORY, loadState } = require("../../core/config");
|
|
2
3
|
const { WorkspaceError } = require("../../core/errors");
|
|
3
4
|
const {
|
|
@@ -13,7 +14,8 @@ const {
|
|
|
13
14
|
const { loadInitManifest } = require("../../core/init");
|
|
14
15
|
const { acquireInitLock } = require("../../core/init-lock");
|
|
15
16
|
const { defaultExtensionStoreRoot } = require("../../core/extension-store");
|
|
16
|
-
const {
|
|
17
|
+
const { packExtensionSourceToDirectory } = require("../../core/extension-package");
|
|
18
|
+
const { digestUpdate, scaffoldExtensionPackage } = require("../../core/extension-scaffold");
|
|
17
19
|
const { resolveExtensionSettings } = require("../../core/extension-settings");
|
|
18
20
|
const { DEFAULT_NEXUS_REGISTRY } = require("../../core/nexus-extension-provider");
|
|
19
21
|
const {
|
|
@@ -25,6 +27,7 @@ const {
|
|
|
25
27
|
const { resolveWorkspaceTools } = require("../../core/tools");
|
|
26
28
|
const { createInteractiveUi, formatExtensionChoice } = require("../../init/ui");
|
|
27
29
|
const { createRegistryExtensionPicker } = require("../../init/registry-picker");
|
|
30
|
+
const { formatExtensionChanges } = require("../../init/extension-summary");
|
|
28
31
|
const { confirm } = require("../confirmation");
|
|
29
32
|
const { selectionResult, success } = require("../result");
|
|
30
33
|
|
|
@@ -43,18 +46,11 @@ function formatUninstallPlan(plan) {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
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");
|
|
49
|
+
return formatExtensionChanges(plans.map((plan) => ({ ...plan, source: plan.source || "local" })), [], {
|
|
50
|
+
title: options.title,
|
|
51
|
+
actionFor: options.actionFor,
|
|
52
|
+
action: options.action,
|
|
53
|
+
});
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
function formatRegistrySearchText(result) {
|
|
@@ -136,18 +132,53 @@ function lifecycleResultEntry(entry, action) {
|
|
|
136
132
|
function lifecycleResultText(results, action) {
|
|
137
133
|
const actionFor = typeof action === "function" ? action : () => action;
|
|
138
134
|
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
|
-
:
|
|
135
|
+
? `${actionFor(entry) === "upgrade" || actionFor(entry) === "update" ? "Updated" : "Installed"} ${entry.name || entry.id}@${entry.version}.`
|
|
136
|
+
: entry.status === "uninstalled"
|
|
137
|
+
? `Uninstalled ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}.`
|
|
138
|
+
: entry.status === "skipped"
|
|
139
|
+
? `Skipped ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}: already current.`
|
|
140
|
+
: `Failed ${entry.name || entry.id}${entry.version ? `@${entry.version}` : ""}: ${entry.message}`);
|
|
143
141
|
const installed = results.filter((entry) => entry.status === "installed").length;
|
|
142
|
+
const uninstalled = results.filter((entry) => entry.status === "uninstalled").length;
|
|
144
143
|
const skipped = results.filter((entry) => entry.status === "skipped").length;
|
|
145
144
|
const failed = results.filter((entry) => entry.status === "failed").length;
|
|
146
145
|
const hasUpdate = results.some((entry) => actionFor(entry) === "upgrade" || actionFor(entry) === "update");
|
|
147
|
-
lines.push(`Extensions: ${installed} ${hasUpdate ? "applied" : "installed"}, ${skipped} skipped, ${failed} failed.`);
|
|
146
|
+
lines.push(`Extensions: ${installed} ${hasUpdate ? "applied" : "installed"}, ${uninstalled} uninstalled, ${skipped} skipped, ${failed} failed.`);
|
|
148
147
|
return lines.join("\n");
|
|
149
148
|
}
|
|
150
149
|
|
|
150
|
+
function uninstallResultEntry(entry) {
|
|
151
|
+
return {
|
|
152
|
+
name: entry.id,
|
|
153
|
+
ok: entry.status !== "failed",
|
|
154
|
+
action: entry.status === "skipped" ? "skip" : "uninstall",
|
|
155
|
+
status: entry.status,
|
|
156
|
+
version: entry.version || null,
|
|
157
|
+
...(entry.reason ? { reason: entry.reason } : {}),
|
|
158
|
+
...(entry.removed ? { removed: entry.removed } : {}),
|
|
159
|
+
...(entry.code ? { code: entry.code } : {}),
|
|
160
|
+
...(entry.message ? { message: entry.message } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sameUninstallTarget(expected, current) {
|
|
165
|
+
return expected.id === current.id
|
|
166
|
+
&& expected.version === current.version
|
|
167
|
+
&& (expected.packageSha256 || null) === (current.packageSha256 || null)
|
|
168
|
+
&& JSON.stringify(expected.targets || []) === JSON.stringify(current.targets || []);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function refreshUninstallPlan(root, frozenPlan) {
|
|
172
|
+
const currentPlan = planExtensionUninstall(root, frozenPlan.id);
|
|
173
|
+
if (currentPlan.action !== frozenPlan.action || !sameUninstallTarget(frozenPlan, currentPlan)) {
|
|
174
|
+
throw new WorkspaceError("EXTENSION_STATE_CONFLICT", `Extension ${frozenPlan.id} changed after confirmation.`, {
|
|
175
|
+
extension: frozenPlan.id,
|
|
176
|
+
remediation: "Re-run extension search and review the current extension selection.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return currentPlan;
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
function validateInstallOptions(invocation) {
|
|
152
183
|
if (invocation.options.version) {
|
|
153
184
|
if (invocation.args.length !== 1) {
|
|
@@ -331,27 +362,55 @@ async function executeExtensionSearch(invocation) {
|
|
|
331
362
|
}, "No extensions selected. No changes were made.");
|
|
332
363
|
}
|
|
333
364
|
const requested = normalizeExtensionNames(selected.map((entry) => entry.id));
|
|
334
|
-
const actionById = new Map(selected.map((entry) => [entry.id, entry.action
|
|
365
|
+
const actionById = new Map(selected.map((entry) => [entry.id, entry.action || "install"]));
|
|
366
|
+
const installSelections = selected.filter((entry) => entry.action !== "uninstall" && entry.action !== "keep");
|
|
367
|
+
const uninstallSelections = selected.filter((entry) => entry.action === "uninstall");
|
|
368
|
+
const planningState = structuredClone(stateInspection.state);
|
|
369
|
+
for (const entry of uninstallSelections) delete planningState.extensions[entry.id];
|
|
335
370
|
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
|
-
|
|
371
|
+
const preparation = installSelections.length > 0
|
|
372
|
+
? await prepareRegistryExtensionPlans({
|
|
373
|
+
...dependencies,
|
|
374
|
+
requested: installSelections.map((entry) => entry.id),
|
|
375
|
+
tools,
|
|
376
|
+
state: planningState,
|
|
377
|
+
stateError: stateInspection.error,
|
|
378
|
+
extensionsRoot: dependencies.extensionsRoot,
|
|
379
|
+
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
380
|
+
provider: dependencies.nexusProvider,
|
|
381
|
+
})
|
|
382
|
+
: { plans: [], failures: [], diagnostics: [] };
|
|
383
|
+
preparation.plans = preparation.plans.map((plan) => ({ ...plan, action: actionById.get(plan.id) || "install" }));
|
|
384
|
+
const uninstallPlans = [];
|
|
385
|
+
const uninstallPreparationFailures = [];
|
|
386
|
+
for (const entry of uninstallSelections) {
|
|
387
|
+
try {
|
|
388
|
+
uninstallPlans.push(planExtensionUninstall(invocation.root, entry.id));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
uninstallPreparationFailures.push({ id: entry.id, version: null, status: "failed", code: error.code || "EXTENSION_UNINSTALL_FAILED", message: error.message, statePersisted: false, phase: "prepare" });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const effectiveUninstallPlans = uninstallPlans.filter((plan) => plan.action !== "skip");
|
|
394
|
+
if (preparation.plans.length > 0 || effectiveUninstallPlans.length > 0) {
|
|
395
|
+
const planText = formatExtensionChanges(preparation.plans, effectiveUninstallPlans, {
|
|
349
396
|
actionFor: (plan) => actionById.get(plan.id) || "install",
|
|
350
397
|
});
|
|
351
398
|
if (!(await (dependencies.confirm || confirm)(`${planText}\nContinue?`, invocation.options))) {
|
|
352
399
|
throw new WorkspaceError("CLI_CANCELLED", "Extension search selection cancelled.");
|
|
353
400
|
}
|
|
354
401
|
}
|
|
402
|
+
const uninstallResults = [];
|
|
403
|
+
for (const plan of effectiveUninstallPlans) {
|
|
404
|
+
try {
|
|
405
|
+
const currentPlan = refreshUninstallPlan(invocation.root, plan);
|
|
406
|
+
uninstallResults.push({ ...applyExtensionUninstall(currentPlan, {
|
|
407
|
+
...(dependencies || {}),
|
|
408
|
+
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
409
|
+
}), action: "uninstall" });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
uninstallResults.push({ id: plan.id, version: plan.version, status: "failed", action: "uninstall", code: error.code || "EXTENSION_UNINSTALL_FAILED", message: error.message });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
355
414
|
const batch = runExtensionBatch(invocation.root, preparation.plans, (extension) => ({
|
|
356
415
|
schemaVersion: 1,
|
|
357
416
|
extensionSpecVersion: extension.extensionSpecVersion,
|
|
@@ -359,8 +418,8 @@ async function executeExtensionSearch(invocation) {
|
|
|
359
418
|
workspace: { name: workspace.name, uuid: workspace.uuid, language: workspace.language },
|
|
360
419
|
tools,
|
|
361
420
|
}), {
|
|
362
|
-
requested,
|
|
363
|
-
preFailures: preparation.failures,
|
|
421
|
+
requested: installSelections.map((entry) => entry.id),
|
|
422
|
+
preFailures: [...preparation.failures, ...uninstallPreparationFailures],
|
|
364
423
|
useExtensionStore: true,
|
|
365
424
|
extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
|
|
366
425
|
});
|
|
@@ -381,12 +440,32 @@ async function executeExtensionSearch(invocation) {
|
|
|
381
440
|
extension: entry.id,
|
|
382
441
|
version: entry.version,
|
|
383
442
|
}))),
|
|
443
|
+
...uninstallResults.filter((entry) => entry.status === "failed").map((entry) => ({
|
|
444
|
+
code: entry.code || "EXTENSION_UNINSTALL_FAILED",
|
|
445
|
+
severity: "error",
|
|
446
|
+
message: entry.message || `Extension ${entry.id} failed to uninstall.`,
|
|
447
|
+
extension: entry.id,
|
|
448
|
+
version: entry.version,
|
|
449
|
+
})),
|
|
384
450
|
];
|
|
385
|
-
const
|
|
451
|
+
const installedResults = decorateBatchResults(batch, preparation.plans, invocation.options);
|
|
452
|
+
const resultById = new Map([
|
|
453
|
+
...installedResults.map((entry) => [entry.id, entry]),
|
|
454
|
+
...uninstallResults.map((entry) => [entry.id, entry]),
|
|
455
|
+
...uninstallPreparationFailures.map((entry) => [entry.id, entry]),
|
|
456
|
+
]);
|
|
457
|
+
const rawResults = selected.map((selection) => {
|
|
458
|
+
if (selection.action === "keep") {
|
|
459
|
+
return { id: selection.id, version: stateInspection.state.extensions[selection.id]?.installed?.version || null, status: "skipped", reason: "already-installed", action: "skip" };
|
|
460
|
+
}
|
|
461
|
+
const entry = resultById.get(selection.id);
|
|
462
|
+
return entry || { id: selection.id, status: "failed", message: "Extension operation did not run." };
|
|
463
|
+
});
|
|
464
|
+
const results = rawResults.map((entry) => lifecycleResultEntry(entry, entry.action || actionById.get(entry.id) || "install"));
|
|
386
465
|
ui.close?.("Extension selection applied.");
|
|
387
|
-
return selectionResult("extension.search", requested, results
|
|
466
|
+
return selectionResult("extension.search", requested, results, {
|
|
388
467
|
diagnostics,
|
|
389
|
-
text: lifecycleResultText(results, (entry) => actionById.get(entry.id) || "install"),
|
|
468
|
+
text: lifecycleResultText(results, (entry) => entry.action || actionById.get(entry.id) || "install"),
|
|
390
469
|
});
|
|
391
470
|
}
|
|
392
471
|
|
|
@@ -481,7 +560,7 @@ async function executeExtensionPack(invocation) {
|
|
|
481
560
|
const command = "extension.pack";
|
|
482
561
|
const dependencies = invocation.dependencies || {};
|
|
483
562
|
const settings = resolveExtensionSettings(dependencies);
|
|
484
|
-
const result = await
|
|
563
|
+
const result = await packExtensionSourceToDirectory(invocation.args[0], invocation.options.output, {
|
|
485
564
|
...dependencies,
|
|
486
565
|
scope: dependencies.scope || settings.values.scope,
|
|
487
566
|
});
|
|
@@ -492,9 +571,56 @@ async function executeExtensionPack(invocation) {
|
|
|
492
571
|
);
|
|
493
572
|
}
|
|
494
573
|
|
|
574
|
+
async function collectExtensionInitOptions(invocation, target) {
|
|
575
|
+
const options = { ...invocation.options };
|
|
576
|
+
const interactive = !options.json && options.yes !== true && process.stdin.isTTY && process.stdout.isTTY;
|
|
577
|
+
if (!interactive) return options;
|
|
578
|
+
const ui = await createInteractiveUi({
|
|
579
|
+
cancelCode: "EXTENSION_INIT_CANCELLED",
|
|
580
|
+
cancelMessage: "Extension initialization cancelled. No changes were made.",
|
|
581
|
+
});
|
|
582
|
+
const defaultId = path.basename(path.resolve(target)).toLowerCase();
|
|
583
|
+
ui.intro("Create Code Workspace extension package");
|
|
584
|
+
if (options.id === undefined) options.id = await ui.text("Extension id", defaultId);
|
|
585
|
+
const defaultName = String(options.id || defaultId).split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
586
|
+
if (options.name === undefined) options.name = await ui.text("Extension name", defaultName);
|
|
587
|
+
if (options.description === undefined) options.description = await ui.text("Description", "Code Workspace extension.");
|
|
588
|
+
if (options.version === undefined) options.version = await ui.text("Version", "0.1.0");
|
|
589
|
+
ui.close("Extension metadata ready.");
|
|
590
|
+
return options;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function executeExtensionInit(invocation) {
|
|
594
|
+
const command = "extension.init";
|
|
595
|
+
const target = invocation.args[0] || ".";
|
|
596
|
+
const options = await collectExtensionInitOptions(invocation, target);
|
|
597
|
+
const pathText = path.resolve(target);
|
|
598
|
+
const planText = `Create extension package shell in ${pathText}:\n CREATE package.json\n CREATE extension/manifest.json\n CREATE extension/init.js\nContinue?`;
|
|
599
|
+
if (!(await confirm(planText, options))) throw new WorkspaceError("CLI_CANCELLED", "Extension package initialization cancelled.");
|
|
600
|
+
const result = scaffoldExtensionPackage(target, options);
|
|
601
|
+
const text = result.action === "skip"
|
|
602
|
+
? `Extension package is already current: ${result.path}.`
|
|
603
|
+
: `Created extension package shell in ${result.path}.\nNext: add outputs, hooks, or runtime to extension/manifest.json.`;
|
|
604
|
+
return success(command, result, text);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function executeExtensionDigestUpdate(invocation) {
|
|
608
|
+
const command = "extension.digest.update";
|
|
609
|
+
const target = invocation.args[0] || ".";
|
|
610
|
+
if (!(await confirm(`Update extension digests in ${path.resolve(target)}?`, invocation.options))) {
|
|
611
|
+
throw new WorkspaceError("CLI_CANCELLED", "Extension digest update cancelled.");
|
|
612
|
+
}
|
|
613
|
+
const result = digestUpdate(target);
|
|
614
|
+
return success(command, result, result.action === "skip"
|
|
615
|
+
? `Extension digests are already current: ${result.path}.`
|
|
616
|
+
: `Updated extension digests in ${result.path}.`);
|
|
617
|
+
}
|
|
618
|
+
|
|
495
619
|
async function executeExtension(invocation) {
|
|
496
620
|
const command = invocation.definition.path.join(".");
|
|
497
621
|
if (command === "extension.pack") return await executeExtensionPack(invocation);
|
|
622
|
+
if (command === "extension.init") return await executeExtensionInit(invocation);
|
|
623
|
+
if (command === "extension.digest.update") return await executeExtensionDigestUpdate(invocation);
|
|
498
624
|
if (command === "extension.search") {
|
|
499
625
|
const dependencies = invocation.dependencies || {};
|
|
500
626
|
const interactive = dependencies.interactive ?? (!invocation.options.json && invocation.options.yes !== true && process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -523,6 +649,8 @@ module.exports = {
|
|
|
523
649
|
executeExtension,
|
|
524
650
|
executeExtensionInstall,
|
|
525
651
|
executeExtensionPack,
|
|
652
|
+
executeExtensionInit,
|
|
653
|
+
executeExtensionDigestUpdate,
|
|
526
654
|
executeExtensionSearch,
|
|
527
655
|
executeExtensionInfo,
|
|
528
656
|
executeExtensionUninstall,
|