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 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
- ## Monitor
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "releaseVersion": "1.0.0-beta.3",
3
+ "releaseVersion": "1.0.0-beta.5",
4
4
  "requirements": {
5
5
  "node": ">=20.19.0"
6
6
  },
@@ -35,7 +35,23 @@ Host 负责确认、校验、事务提交、状态记录、升级和卸载。
35
35
 
36
36
  ## 2. 包目录
37
37
 
38
- 系统扩展使用固定版本目录;普通扩展在独立仓库中维护并发布到 Nexus:
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
- 外部扩展仓库可以使用任意仓库根目录,但交给 `extension pack` 的 `<source>` 必须直接指向包含 `manifest.json` 和 `init.js` 的版本目录。
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
- 不要在扩展目录中添加运输用 `package.json`。npm 包装由 `extension pack` 生成;真实扩展目录的 `packageSha256` 是 Extension 身份的一部分。
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` 后都必须更新 `entrySha256`。其他文件不需要手工写摘要;Host 和打包器会计算完整目录摘要。
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 extensions/example-extension/1.0.0 --output dist/extensions --json
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
- - 生成不含 dependencies、bundled dependencies 和 scripts 的 envelope;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "code-workspace-zhuiyi",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Local multi-project registry and safety layer for AI coding workspaces",
5
5
  "author": "icebearx-ai",
6
6
  "license": "MIT",
@@ -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 { packExtensionToDirectory } = require("../../core/extension-package");
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
- const actionFor = options.actionFor || (() => "install");
47
- const title = options.title || `Install ${plans.length} extension${plans.length === 1 ? "" : "s"}:`;
48
- return [
49
- title,
50
- ...plans.flatMap((plan) => [
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 === "skipped"
141
- ? `Skipped ${entry.id}@${entry.version}: already current.`
142
- : `Failed ${entry.id}${entry.version ? `@${entry.version}` : ""}: ${entry.message}`);
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 === "update" ? "update" : "install"]));
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 = await prepareRegistryExtensionPlans({
337
- ...dependencies,
338
- requested,
339
- tools,
340
- state: stateInspection.state,
341
- stateError: stateInspection.error,
342
- extensionsRoot: dependencies.extensionsRoot,
343
- extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
344
- provider: dependencies.nexusProvider,
345
- });
346
- if (preparation.plans.length > 0) {
347
- const planText = formatInstallPlan(preparation.plans, {
348
- title: `Apply ${preparation.plans.length} extension${preparation.plans.length === 1 ? "" : "s"}:`,
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 results = decorateBatchResults(batch, preparation.plans, invocation.options);
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.map((entry) => lifecycleResultEntry(entry, actionById.get(entry.id) || "install")), {
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 packExtensionToDirectory(invocation.args[0], invocation.options.output, {
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,