code-workspace-zhuiyi 1.0.0-beta.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -226,16 +225,7 @@ codew language --json
226
225
 
227
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.
228
227
 
229
- ## Monitor
230
-
231
- Monitor is an ordinary Nexus extension. Install it into a workspace and run its dashboard through the generic extension runtime:
232
-
233
- ```bash
234
- codew extension install monitor --yes
235
- codew ext monitor serve --port 3211
236
- ```
237
-
238
- 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
239
229
 
240
230
  ```bash
241
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
  交互式初始化:
@@ -223,17 +223,6 @@ codew language --json
223
223
 
224
224
  `update` 只刷新 Workspace 自有托管资产,绝不会修改 Agent 目录授权。遇到未知本地修改时,会在任何写入前终止批次;请先审查修改,或显式传入 `--force`。
225
225
 
226
- ## 监控
227
-
228
- 监控以 Nexus 中的 `monitor` 扩展提供。先在 Workspace 中安装扩展,再通过通用扩展运行时启动面板:
229
-
230
- ```bash
231
- codew extension install monitor --yes
232
- codew ext monitor serve --port 3211
233
- ```
234
-
235
- 监控服务仅绑定 loopback,可汇总多个已初始化工作区的事件;hook 上报(`codew ext monitor report`)失败不会阻断 Agent。依赖监控前,请在 Codex 中检查并信任项目 hook。
236
-
237
226
  ## 命令补全
238
227
 
239
228
  ```bash
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "releaseVersion": "1.0.0-beta.4",
3
+ "releaseVersion": "1.0.0",
4
4
  "requirements": {
5
5
  "node": ">=20.19.0"
6
6
  },
File without changes
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // TODO: implement the extension init protocol.
@@ -35,7 +35,27 @@ 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
+ ├── .gitignore
49
+ ├── README.md
50
+ ├── extension/
51
+ │ ├── manifest.json
52
+ │ └── init.js
53
+ └── package.json
54
+ ```
55
+
56
+ 初始化产物中的静态文件模板位于仓库的 `artifacts/templates/extension-init/`。后续调整 README、`.gitignore` 或 `extension/` 下的默认文件时,直接修改该模板目录即可;其中 `gitignore` 会在生成时映射为 `.gitignore`,以避免 npm 发布包排除点文件。`extension init` 只负责注入元数据、生成摘要并写入 package envelope。
57
+
58
+ 系统扩展仍使用固定版本目录:
39
59
 
40
60
  ```text
41
61
  extensions/<extension-id>/<version>/
@@ -45,7 +65,7 @@ extensions/<extension-id>/<version>/
45
65
  └── SKILL.md
46
66
  ```
47
67
 
48
- 外部扩展仓库可以使用任意仓库根目录,但交给 `extension pack` 的 `<source>` 必须直接指向包含 `manifest.json` 和 `init.js` 的版本目录。
68
+ 公开 CLI 的 `extension pack <source>` 要求 `<source>` 直接指向包含 `package.json` 和 `extension/` 的 package 根目录。空壳尚未声明 `outputs`、`hooks` 或 `runtime` 时不能打包。
49
69
 
50
70
  命名要求:
51
71
 
@@ -53,11 +73,18 @@ extensions/<extension-id>/<version>/
53
73
  - `version` 必须是完整 SemVer,例如 `1.0.0`;
54
74
  - 已发布版本目录不可修改;新实现创建新的版本目录。
55
75
 
56
- 不要在扩展目录中添加运输用 `package.json`。npm 包装由 `extension pack` 生成;真实扩展目录的 `packageSha256` 是 Extension 身份的一部分。
76
+ 根目录 `package.json` 是固定格式的 transport envelope,不要添加 `dependencies`、`scripts` 或其他 npm lifecycle 字段。`extension pack` 只校验并打包这个 envelope,不会自动生成或修改它。
77
+
78
+ `extension init` 只询问扩展 id、显示名称、描述和版本,不预设扩展是 Skill、MCP、Hook 还是 Runtime。完成 manifest 能力声明后,再运行:
79
+
80
+ ```bash
81
+ codew extension digest update ./example-extension --yes
82
+ codew extension pack ./example-extension --output ./dist/extensions --json
83
+ ```
57
84
 
58
85
  ## 3. manifest
59
86
 
60
- Spec v1 使用 manifest schema v3。最小示例:
87
+ Spec v1 使用 manifest schema v3。下面是声明了一个 file output 的最小完整示例;`extension init` 生成的空壳会在用户添加能力前缺少 `outputs`、`hooks` 或 `runtime`:
61
88
 
62
89
  ```json
63
90
  {
@@ -89,7 +116,13 @@ Spec v1 使用 manifest schema v3。最小示例:
89
116
  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
117
  ```
91
118
 
92
- 每次修改 `init.js` 后都必须更新 `entrySha256`。其他文件不需要手工写摘要;Host 和打包器会计算完整目录摘要。
119
+ 每次修改 `init.js` 或 `extension/` 下其他文件后,使用 CLI 同步摘要:
120
+
121
+ ```bash
122
+ codew extension digest update ./example-extension --yes
123
+ ```
124
+
125
+ 该命令会先更新 `extension/manifest.json.entrySha256`,再更新 `package.json.codeWorkspace.packageSha256`。
93
126
 
94
127
  ### 3.1 身份与展示
95
128
 
@@ -296,10 +329,10 @@ codew extension upgrade example-extension --yes
296
329
 
297
330
  ## 6. npm/Nexus 打包
298
331
 
299
- 打包单个版本目录:
332
+ 打包单个 package 根目录:
300
333
 
301
334
  ```bash
302
- code-workspace extension pack extensions/example-extension/1.0.0 --output dist/extensions --json
335
+ code-workspace extension pack ./example-extension --output dist/extensions --json
303
336
  ```
304
337
 
305
338
  输出目录缺失时会递归创建。目标文件已存在时命令拒绝覆盖;重复打包前使用新的输出目录,或清理上一次构建产物。
@@ -331,14 +364,16 @@ npm 身份固定映射为:
331
364
 
332
365
  打包器会:
333
366
 
334
- - 校验 manifest、入口摘要、目录类型和 Extension Spec;
335
- - 生成不含 dependencies、bundled dependencies 和 scripts 的 envelope;
367
+ - 校验 package envelope、manifest、入口摘要、目录类型和 Extension Spec;
368
+ - 原样保留已验证的 `package.json`,不生成或修改 envelope;
336
369
  - 只归档普通文件;
337
370
  - 拒绝符号链接、硬链接、特殊文件、路径逃逸和重复路径;
338
371
  - 限制最多 1024 个文件、单文件 2 MiB、载荷总计 16 MiB、envelope 64 KiB;
339
372
  - 重新读取 tarball 并验证 envelope、manifest、入口和 package digest;
340
373
  - 使用同目录临时文件并原子提交输出。
341
374
 
375
+ 开发 package 的 README 和其他根目录文件不会进入 tarball;最终内容严格由 `package.json.files` 中的 `extension` 决定。
376
+
342
377
  成功 JSON 返回 npm name、扩展身份、tarball 绝对路径、tarball integrity、manifest/entry/package digest 和文件清单。
343
378
 
344
379
  ## 7. 发布边界
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-workspace-zhuiyi",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0",
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 {
@@ -159,6 +161,24 @@ function uninstallResultEntry(entry) {
159
161
  };
160
162
  }
161
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
+
162
182
  function validateInstallOptions(invocation) {
163
183
  if (invocation.options.version) {
164
184
  if (invocation.args.length !== 1) {
@@ -382,7 +402,8 @@ async function executeExtensionSearch(invocation) {
382
402
  const uninstallResults = [];
383
403
  for (const plan of effectiveUninstallPlans) {
384
404
  try {
385
- uninstallResults.push({ ...applyExtensionUninstall(plan, {
405
+ const currentPlan = refreshUninstallPlan(invocation.root, plan);
406
+ uninstallResults.push({ ...applyExtensionUninstall(currentPlan, {
386
407
  ...(dependencies || {}),
387
408
  extensionStoreRoot: dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
388
409
  }), action: "uninstall" });
@@ -539,7 +560,7 @@ async function executeExtensionPack(invocation) {
539
560
  const command = "extension.pack";
540
561
  const dependencies = invocation.dependencies || {};
541
562
  const settings = resolveExtensionSettings(dependencies);
542
- const result = await packExtensionToDirectory(invocation.args[0], invocation.options.output, {
563
+ const result = await packExtensionSourceToDirectory(invocation.args[0], invocation.options.output, {
543
564
  ...dependencies,
544
565
  scope: dependencies.scope || settings.values.scope,
545
566
  });
@@ -550,9 +571,56 @@ async function executeExtensionPack(invocation) {
550
571
  );
551
572
  }
552
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
+
553
619
  async function executeExtension(invocation) {
554
620
  const command = invocation.definition.path.join(".");
555
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);
556
624
  if (command === "extension.search") {
557
625
  const dependencies = invocation.dependencies || {};
558
626
  const interactive = dependencies.interactive ?? (!invocation.options.json && invocation.options.yes !== true && process.stdin.isTTY && process.stdout.isTTY);
@@ -581,6 +649,8 @@ module.exports = {
581
649
  executeExtension,
582
650
  executeExtensionInstall,
583
651
  executeExtensionPack,
652
+ executeExtensionInit,
653
+ executeExtensionDigestUpdate,
584
654
  executeExtensionSearch,
585
655
  executeExtensionInfo,
586
656
  executeExtensionUninstall,
@@ -44,6 +44,24 @@ function stateWithPlans(state, plans) {
44
44
  return next;
45
45
  }
46
46
 
47
+ function sameUninstallTarget(expected, current) {
48
+ return expected.id === current.id
49
+ && expected.version === current.version
50
+ && (expected.packageSha256 || null) === (current.packageSha256 || null)
51
+ && JSON.stringify(expected.targets || []) === JSON.stringify(current.targets || []);
52
+ }
53
+
54
+ function refreshUninstallPlan(root, frozenPlan) {
55
+ const currentPlan = planExtensionUninstall(root, frozenPlan.id);
56
+ if (currentPlan.action !== frozenPlan.action || !sameUninstallTarget(frozenPlan, currentPlan)) {
57
+ throw new WorkspaceError("EXTENSION_STATE_CONFLICT", `Extension ${frozenPlan.id} changed after confirmation.`, {
58
+ extension: frozenPlan.id,
59
+ remediation: "Re-run init and review the current extension selection.",
60
+ });
61
+ }
62
+ return currentPlan;
63
+ }
64
+
47
65
  function migrationData(plan) {
48
66
  if (!plan) return null;
49
67
  return {
@@ -258,8 +276,9 @@ async function executeInitUnlocked(invocation, root) {
258
276
  const ordinaryRemovalResults = [];
259
277
  const ordinaryRemovalDiagnostics = [];
260
278
  for (const removalPlan of ordinaryRemovalPlans) {
261
- const planToApply = typeof removalPlan === "string" ? planExtensionUninstall(root, removalPlan) : removalPlan;
279
+ const frozenPlan = typeof removalPlan === "string" ? planExtensionUninstall(root, removalPlan) : removalPlan;
262
280
  try {
281
+ const planToApply = refreshUninstallPlan(root, frozenPlan);
263
282
  const removal = applyExtensionUninstall(planToApply, {
264
283
  extensionStoreRoot: options.extensionStoreRoot || dependencies.extensionStoreRoot || defaultExtensionStoreRoot(),
265
284
  });
@@ -23,6 +23,8 @@ const COMMAND_SUMMARIES = {
23
23
  "extension install": "Install one or more extensions into the Workspace",
24
24
  "extension uninstall": "Uninstall one ordinary extension from the Workspace",
25
25
  "extension pack": "Create a verified npm tarball from an extension package",
26
+ "extension init": "Create an extension package shell",
27
+ "extension digest update": "Update extension entry and package digests",
26
28
  "extension search": "Search, install, or update extensions (TTY); read-only results in JSON/non-TTY",
27
29
  "extension info": "Show extension metadata from the configured Registry",
28
30
  "extension upgrade": "Upgrade installed ordinary extensions",
@@ -70,6 +72,10 @@ const COMMANDS = [
70
72
  } },
71
73
  { path: ["extension", "uninstall"], args: [{ name: "name", required: true }], workspace: "required", config: [], interaction: "required", effects: "planned-write", options: { yes: { type: "boolean" } } },
72
74
  { path: ["extension", "pack"], args: [{ name: "source", required: true }], workspace: "none", config: [], interaction: "never", effects: "planned-write", options: { output: { type: "string", required: true } } },
75
+ { path: ["extension", "init"], args: [{ name: "path", required: false }], workspace: "none", config: [], interaction: "optional", effects: "planned-write", options: {
76
+ id: { type: "string" }, name: { type: "string" }, description: { type: "string" }, version: { type: "string" }, yes: { type: "boolean" },
77
+ } },
78
+ { path: ["extension", "digest", "update"], args: [{ name: "path", required: false }], workspace: "none", config: [], interaction: "optional", effects: "planned-write", options: { yes: { type: "boolean" } } },
73
79
  { path: ["extension", "search"], args: [{ name: "query", required: false }], workspace: "required", config: ["identity", "language"], interaction: "required", effects: "planned-write", options: { yes: { type: "boolean" } } },
74
80
  { path: ["extension", "info"], args: [{ name: "name", required: true }], workspace: "none", config: [], interaction: "never", effects: "external", options: {} },
75
81
  { path: ["extension", "upgrade"], args: [{ name: "name", required: true, variadic: true }], workspace: "required", config: ["identity", "language"], interaction: "required", effects: "planned-write", options: { yes: { type: "boolean" }, offline: { type: "boolean" } } },
@@ -590,6 +590,209 @@ async function hashFile(file, algorithm, encoding) {
590
590
  return hash.digest(encoding);
591
591
  }
592
592
 
593
+ function readPackageJson(file) {
594
+ let value;
595
+ try {
596
+ value = JSON.parse(fs.readFileSync(file, "utf8"));
597
+ } catch (error) {
598
+ throw packageError("EXTENSION_PACKAGE_JSON_INVALID", `Cannot parse package.json: ${error.message}`, { path: file });
599
+ }
600
+ return value;
601
+ }
602
+
603
+ function inspectExtensionSourcePackage(source, options = {}) {
604
+ const packageRoot = path.resolve(source);
605
+ let stat;
606
+ try {
607
+ stat = fs.lstatSync(packageRoot);
608
+ } catch (error) {
609
+ throw packageError("EXTENSION_PACKAGE_MISSING", `Extension package directory is missing: ${packageRoot}`, {
610
+ path: packageRoot,
611
+ cause: error.code,
612
+ });
613
+ }
614
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
615
+ throw packageError("EXTENSION_PACKAGE_INVALID", `Extension package must be a regular directory: ${packageRoot}`, { path: packageRoot });
616
+ }
617
+ const packageJsonFile = path.join(packageRoot, "package.json");
618
+ const extensionRoot = path.join(packageRoot, EXTENSION_PACKAGE_ROOT);
619
+ let packageJsonStat;
620
+ try { packageJsonStat = fs.lstatSync(packageJsonFile); } catch (error) {
621
+ throw packageError("EXTENSION_NPM_ENVELOPE_MISSING", `Extension package is missing package.json: ${packageJsonFile}`, { path: packageJsonFile, cause: error.code });
622
+ }
623
+ if (!packageJsonStat.isFile() || packageJsonStat.isSymbolicLink()) {
624
+ throw packageError("EXTENSION_NPM_ENVELOPE_INVALID", `package.json must be a regular file: ${packageJsonFile}`, { path: packageJsonFile });
625
+ }
626
+ let extensionStat;
627
+ try { extensionStat = fs.lstatSync(extensionRoot); } catch (error) {
628
+ throw packageError("EXTENSION_PACKAGE_LAYOUT_INVALID", `Extension package is missing extension/: ${extensionRoot}`, { path: extensionRoot, cause: error.code });
629
+ }
630
+ if (!extensionStat.isDirectory() || extensionStat.isSymbolicLink()) {
631
+ throw packageError("EXTENSION_PACKAGE_LAYOUT_INVALID", `Extension package root must be a regular directory: ${extensionRoot}`, { path: extensionRoot });
632
+ }
633
+ const packageJsonBytes = fs.readFileSync(packageJsonFile);
634
+ const rawEnvelope = readPackageJson(packageJsonFile);
635
+ const envelope = validateExtensionTransportEnvelope(rawEnvelope, {
636
+ ...(options.scope ? { expectedScope: options.scope } : {}),
637
+ });
638
+ const inspected = inspectExtensionPackageDirectory(extensionRoot, {
639
+ expectedId: envelope.codeWorkspace.extensionId,
640
+ expectedVersion: envelope.version,
641
+ expectedExtensionSpecVersion: envelope.codeWorkspace.extensionSpecVersion,
642
+ ...(options.supportedExtensionSpecVersions ? { supportedExtensionSpecVersions: options.supportedExtensionSpecVersions } : {}),
643
+ ...(options.protectedTargets ? { protectedTargets: options.protectedTargets } : {}),
644
+ });
645
+ try {
646
+ validateExtensionTransportEnvelope(rawEnvelope, {
647
+ expectedId: inspected.id,
648
+ expectedVersion: inspected.version,
649
+ expectedExtensionSpecVersion: inspected.extensionSpecVersion,
650
+ expectedPackageSha256: inspected.packageSha256,
651
+ ...(options.scope ? { expectedScope: options.scope } : {}),
652
+ });
653
+ } catch (error) {
654
+ if (error.code === "EXTENSION_NPM_PACKAGE_DIGEST_MISMATCH") {
655
+ error.details = { ...(error.details || {}), remediation: "Run codew extension digest update <package-path> before packing." };
656
+ }
657
+ throw error;
658
+ }
659
+ if (envelope.description !== inspected.manifest.description) {
660
+ throw packageError("EXTENSION_NPM_IDENTITY_MISMATCH", "npm transport description does not match the Extension manifest", {
661
+ field: "description",
662
+ expected: inspected.manifest.description,
663
+ actual: envelope.description,
664
+ });
665
+ }
666
+ return Object.freeze({
667
+ ...inspected,
668
+ packageRoot,
669
+ extensionRoot,
670
+ packageJsonFile,
671
+ packageJsonBytes,
672
+ envelope,
673
+ });
674
+ }
675
+
676
+ function assertSourcePackageUnchanged(source, inspected, files, options = {}) {
677
+ let current;
678
+ try {
679
+ current = inspectExtensionSourcePackage(source, options);
680
+ } catch (error) {
681
+ throw packageError("EXTENSION_PACKAGE_CHANGED", `Extension package changed while packing: ${inspected.id}@${inspected.version}`, {
682
+ extension: inspected.id,
683
+ version: inspected.version,
684
+ cause: error.code,
685
+ });
686
+ }
687
+ const currentFiles = collectPackageFiles(current.extensionRoot, options.limits);
688
+ const digestMatches = current.packageJsonBytes.equals(inspected.packageJsonBytes)
689
+ && current.packageSha256 === inspected.packageSha256
690
+ && current.manifestSha256 === inspected.manifestSha256
691
+ && current.entrySha256 === inspected.entrySha256;
692
+ const filesMatch = currentFiles.length === files.length && currentFiles.every((file, index) =>
693
+ file.relative === files[index].relative && file.size === files[index].size && file.mode === files[index].mode && file.sha256 === files[index].sha256
694
+ );
695
+ if (!digestMatches || !filesMatch) {
696
+ throw packageError("EXTENSION_PACKAGE_CHANGED", `Extension package changed while packing: ${inspected.id}@${inspected.version}`, {
697
+ extension: inspected.id,
698
+ version: inspected.version,
699
+ });
700
+ }
701
+ }
702
+
703
+ async function packExtensionSourceToDirectory(source, outputDirectory, options = {}) {
704
+ // Keep the repository's built-in legacy extension layout packable while the
705
+ // public authoring workflow uses a package-root envelope.
706
+ if (!fs.existsSync(path.join(path.resolve(source), "package.json"))) {
707
+ return packExtensionToDirectory(source, outputDirectory, options);
708
+ }
709
+ const limits = normalizeLimits(options.limits);
710
+ const outputRoot = path.resolve(outputDirectory);
711
+ const inspected = inspectExtensionSourcePackage(source, {
712
+ ...(options.scope ? { scope: options.scope } : {}),
713
+ ...(options.supportedExtensionSpecVersions ? { supportedExtensionSpecVersions: options.supportedExtensionSpecVersions } : {}),
714
+ ...(options.protectedTargets ? { protectedTargets: options.protectedTargets } : {}),
715
+ });
716
+ const files = collectPackageFiles(inspected.extensionRoot, limits);
717
+ try {
718
+ fs.mkdirSync(outputRoot, { recursive: true });
719
+ } catch (error) {
720
+ throw packageError("EXTENSION_PACK_OUTPUT_CREATE_FAILED", `Cannot create output directory: ${outputRoot}`, { path: outputRoot, cause: error.code });
721
+ }
722
+ const outputStat = fs.lstatSync(outputRoot);
723
+ if (!outputStat.isDirectory() || outputStat.isSymbolicLink()) {
724
+ throw packageError("EXTENSION_PACK_OUTPUT_INVALID", `Output must be a regular directory: ${outputRoot}`, { path: outputRoot });
725
+ }
726
+ const filename = extensionNpmTarballFilename(inspected.id, inspected.version, options.scope);
727
+ const target = path.join(outputRoot, filename);
728
+ if (fs.existsSync(target)) {
729
+ throw packageError("EXTENSION_PACK_OUTPUT_EXISTS", `Extension package output already exists: ${target}`, {
730
+ path: target,
731
+ remediation: "Move or remove the existing tarball, or choose another output directory.",
732
+ });
733
+ }
734
+ const temporaryTarget = path.join(outputRoot, `.${filename}.${process.pid}.${crypto.randomUUID()}.tmp`);
735
+ const fd = fs.openSync(temporaryTarget, "wx", 0o600);
736
+ fs.closeSync(fd);
737
+ let targetReserved = false;
738
+ try {
739
+ try {
740
+ await writeTransportTarball({
741
+ sourceRoot: inspected.extensionRoot,
742
+ files,
743
+ envelopeBytes: inspected.packageJsonBytes,
744
+ target: temporaryTarget,
745
+ limits,
746
+ });
747
+ } catch (error) {
748
+ if (error instanceof WorkspaceError) throw error;
749
+ throw packageError("EXTENSION_TARBALL_CREATE_FAILED", `Cannot create extension npm tarball: ${error.message}`, { path: target, cause: error.code });
750
+ }
751
+ assertSourcePackageUnchanged(source, inspected, files, { ...options, limits });
752
+ let verified;
753
+ try {
754
+ verified = await inspectExtensionTransportTarball(temporaryTarget, {
755
+ expectedId: inspected.id,
756
+ expectedVersion: inspected.version,
757
+ expectedExtensionSpecVersion: inspected.extensionSpecVersion,
758
+ expectedPackageSha256: inspected.packageSha256,
759
+ ...(options.scope ? { expectedScope: options.scope } : {}),
760
+ expectedFiles: files,
761
+ limits,
762
+ });
763
+ } catch (error) {
764
+ if (error instanceof WorkspaceError) throw error;
765
+ throw packageError("EXTENSION_TARBALL_VERIFY_FAILED", `Cannot verify extension npm tarball: ${error.message}`, { path: target, cause: error.code });
766
+ }
767
+ if (!verified || verified.packageSha256 !== inspected.packageSha256 || verified.manifestSha256 !== inspected.manifestSha256 || verified.entrySha256 !== inspected.entrySha256) {
768
+ throw packageError("EXTENSION_TARBALL_VERIFY_FAILED", `Extension npm tarball verification failed: ${inspected.id}@${inspected.version}`, { extension: inspected.id, version: inspected.version });
769
+ }
770
+ const tarballBytes = fs.statSync(temporaryTarget).size;
771
+ const integrity = `sha512-${await hashFile(temporaryTarget, "sha512", "base64")}`;
772
+ const targetFd = fs.openSync(target, "wx", 0o600);
773
+ fs.closeSync(targetFd);
774
+ targetReserved = true;
775
+ fs.renameSync(temporaryTarget, target);
776
+ targetReserved = false;
777
+ return Object.freeze({
778
+ schemaVersion: 1,
779
+ extensionId: inspected.id,
780
+ version: inspected.version,
781
+ extensionSpecVersion: inspected.extensionSpecVersion,
782
+ npmName: inspected.envelope.name,
783
+ tarball: Object.freeze({ path: target, filename, bytes: tarballBytes, integrity, files: verified.files }),
784
+ manifestSha256: inspected.manifestSha256,
785
+ entrySha256: inspected.entrySha256,
786
+ packageSha256: inspected.packageSha256,
787
+ });
788
+ } finally {
789
+ try { fs.unlinkSync(temporaryTarget); } catch { /* cleanup is best-effort */ }
790
+ if (targetReserved) {
791
+ try { fs.unlinkSync(target); } catch { /* cleanup is best-effort */ }
792
+ }
793
+ }
794
+ }
795
+
593
796
  async function packExtensionToDirectory(source, outputDirectory, options = {}) {
594
797
  const limits = normalizeLimits(options.limits);
595
798
  const outputRoot = path.resolve(outputDirectory);
@@ -733,6 +936,8 @@ module.exports = {
733
936
  normalizeNpmScope,
734
937
  extractExtensionTransportTarball,
735
938
  inspectExtensionTransportTarball,
939
+ inspectExtensionSourcePackage,
940
+ packExtensionSourceToDirectory,
736
941
  packExtensionToDirectory,
737
942
  validateExtensionTransportEnvelope,
738
943
  };
@@ -0,0 +1,212 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const { atomicWrite, sha256 } = require("./fs");
7
+ const { createFileTransaction } = require("./transaction");
8
+ const { directoryDigest } = require("./directory-digest");
9
+ const { EXTENSION_NAME_PATTERN, parseSemver, validateManifestEnvelope } = require("./extensions");
10
+ const { validateExtensionTransportEnvelope } = require("./extension-package");
11
+ const { WorkspaceError } = require("./errors");
12
+
13
+ function scaffoldError(code, message, details = {}) {
14
+ return new WorkspaceError(code, message, details);
15
+ }
16
+
17
+ function titleFromId(id) {
18
+ return id.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
19
+ }
20
+
21
+ function readJson(file, code) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(file, "utf8"));
24
+ } catch (error) {
25
+ throw scaffoldError(code, `Cannot parse JSON file: ${file}`, { path: file, cause: error.message });
26
+ }
27
+ }
28
+
29
+ function validateScaffoldMetadata(options = {}, targetRoot) {
30
+ const derivedId = path.basename(targetRoot).toLowerCase();
31
+ const id = String(options.id || derivedId).trim();
32
+ if (!EXTENSION_NAME_PATTERN.test(id)) throw scaffoldError("EXTENSION_NAME_INVALID", `Invalid extension name: ${id || "<missing>"}`, { extension: id || null });
33
+ const name = String(options.name || titleFromId(id)).trim();
34
+ const description = String(options.description || "Code Workspace extension.").trim();
35
+ const version = parseSemver(options.version || "0.1.0").raw;
36
+ if (!name || [...name].length > 100) throw scaffoldError("EXTENSION_MANIFEST_INVALID", `Extension display name is invalid: ${id}`, { extension: id });
37
+ if (!description || [...description].length > 60 || /[\r\n]/.test(description)) throw scaffoldError("EXTENSION_MANIFEST_INVALID", `Extension description is invalid: ${id}`, { extension: id });
38
+ return Object.freeze({ id, name, description, version });
39
+ }
40
+
41
+ const SCAFFOLD_TEMPLATE_ROOT = path.join(__dirname, "..", "..", "artifacts", "templates", "extension-init");
42
+
43
+ function readTemplateFiles(root = SCAFFOLD_TEMPLATE_ROOT, current = root, result = {}) {
44
+ let entries;
45
+ try {
46
+ entries = fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
47
+ } catch (error) {
48
+ throw scaffoldError("EXTENSION_TEMPLATE_INVALID", `Cannot read extension init template: ${root}`, { path: root, cause: error.code || error.message });
49
+ }
50
+ for (const entry of entries) {
51
+ const file = path.join(current, entry.name);
52
+ const relative = path.relative(root, file).split(path.sep).join("/");
53
+ const stat = fs.lstatSync(file);
54
+ if (stat.isSymbolicLink()) throw scaffoldError("EXTENSION_TEMPLATE_INVALID", `Extension init template contains a symbolic link: ${relative}`, { path: relative });
55
+ if (stat.isDirectory()) readTemplateFiles(root, file, result);
56
+ else if (stat.isFile()) result[relative === "gitignore" ? ".gitignore" : relative] = fs.readFileSync(file);
57
+ else throw scaffoldError("EXTENSION_TEMPLATE_INVALID", `Extension init template contains a special file: ${relative}`, { path: relative });
58
+ }
59
+ return result;
60
+ }
61
+
62
+ function validateShellManifest(raw) {
63
+ const envelope = validateManifestEnvelope(raw);
64
+ if (raw.schemaVersion !== 3 || raw.experimental !== true || raw.entry !== "init.js") {
65
+ throw scaffoldError("EXTENSION_MANIFEST_INVALID", "Extension shell manifest must use schemaVersion 3, experimental true, and entry init.js", { extension: envelope.id });
66
+ }
67
+ if (!/^[a-f0-9]{64}$/.test(raw.entrySha256 || "")) throw scaffoldError("EXTENSION_MANIFEST_INVALID", `Extension ${envelope.id} entrySha256 is invalid`, { extension: envelope.id });
68
+ if (!Number.isInteger(raw.timeoutMs) || raw.timeoutMs < 1 || raw.timeoutMs > 300000) throw scaffoldError("EXTENSION_MANIFEST_INVALID", `Extension ${envelope.id} timeoutMs is invalid`, { extension: envelope.id });
69
+ return envelope;
70
+ }
71
+
72
+ function readRelativeFiles(root, current = root, result = []) {
73
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
74
+ const file = path.join(current, entry.name);
75
+ const relative = path.relative(root, file).split(path.sep).join("/");
76
+ const stat = fs.lstatSync(file);
77
+ if (stat.isSymbolicLink()) throw scaffoldError("EXTENSION_INIT_TARGET_EXISTS", `Scaffold target contains a symbolic link: ${relative}`, { path: relative });
78
+ if (stat.isDirectory()) readRelativeFiles(root, file, result);
79
+ else if (stat.isFile()) result.push(relative);
80
+ else throw scaffoldError("EXTENSION_INIT_TARGET_EXISTS", `Scaffold target contains a special file: ${relative}`, { path: relative });
81
+ }
82
+ return result;
83
+ }
84
+
85
+ function filesMatch(root, files) {
86
+ let actual;
87
+ try { actual = readRelativeFiles(root); } catch { return false; }
88
+ const expected = Object.keys(files).sort();
89
+ if (actual.length !== expected.length || actual.some((entry, index) => entry !== expected[index])) return false;
90
+ return expected.every((relative) => Buffer.from(fs.readFileSync(path.join(root, ...relative.split("/")))).equals(files[relative]));
91
+ }
92
+
93
+ function commitDirectory(temporary, target) {
94
+ fs.mkdirSync(path.dirname(target), { recursive: true });
95
+ if (!fs.existsSync(target)) {
96
+ fs.renameSync(temporary, target);
97
+ return;
98
+ }
99
+ const backup = `${target}.${process.pid}.${crypto.randomUUID()}.old`;
100
+ let moved = false;
101
+ try {
102
+ fs.renameSync(target, backup);
103
+ moved = true;
104
+ fs.renameSync(temporary, target);
105
+ fs.rmSync(backup, { recursive: true, force: true });
106
+ } catch (error) {
107
+ try { if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true }); } catch {}
108
+ try { if (moved && fs.existsSync(backup)) fs.renameSync(backup, target); } catch {}
109
+ throw scaffoldError("EXTENSION_INIT_COMMIT_FAILED", `Cannot commit extension package: ${target}`, { path: target, cause: error.code || error.message });
110
+ }
111
+ }
112
+
113
+ function shellEnvelope(metadata, packageSha256) {
114
+ return {
115
+ name: `@codew-ext/${metadata.id}`,
116
+ version: metadata.version,
117
+ description: metadata.description,
118
+ keywords: ["code-workspace-extension"],
119
+ codeWorkspace: { schemaVersion: 1, extensionId: metadata.id, extensionSpecVersion: 1, packageRoot: "extension", packageSha256 },
120
+ files: ["extension"],
121
+ };
122
+ }
123
+
124
+ function scaffoldExtensionPackage(target, options = {}) {
125
+ const targetRoot = path.resolve(target || ".");
126
+ const metadata = validateScaffoldMetadata(options, targetRoot);
127
+ const templateFiles = readTemplateFiles();
128
+ const entry = templateFiles["extension/init.js"];
129
+ if (!entry) throw scaffoldError("EXTENSION_TEMPLATE_INVALID", "Extension init template must contain extension/init.js", { path: "extension/init.js" });
130
+ const manifest = {
131
+ schemaVersion: 3,
132
+ extensionSpecVersion: 1,
133
+ experimental: true,
134
+ id: metadata.id,
135
+ name: metadata.name,
136
+ description: metadata.description,
137
+ version: metadata.version,
138
+ entry: "init.js",
139
+ entrySha256: sha256(Buffer.from(entry, "utf8")),
140
+ timeoutMs: 30000,
141
+ };
142
+ const files = {
143
+ ...templateFiles,
144
+ "package.json": null,
145
+ "extension/manifest.json": Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
146
+ };
147
+ const temporary = fs.mkdtempSync(path.join(path.dirname(targetRoot), `.${path.basename(targetRoot)}-`));
148
+ try {
149
+ for (const [relative, content] of Object.entries(files)) {
150
+ if (!content) continue;
151
+ const file = path.join(temporary, ...relative.split("/"));
152
+ fs.mkdirSync(path.dirname(file), { recursive: true });
153
+ fs.writeFileSync(file, content, { mode: relative.endsWith(".js") ? 0o755 : 0o644 });
154
+ }
155
+ const packageSha256 = directoryDigest(path.join(temporary, "extension"));
156
+ const envelope = shellEnvelope(metadata, packageSha256);
157
+ validateExtensionTransportEnvelope(envelope, { expectedId: metadata.id, expectedVersion: metadata.version, expectedExtensionSpecVersion: 1, expectedPackageSha256: packageSha256 });
158
+ files["package.json"] = Buffer.from(`${JSON.stringify(envelope, null, 2)}\n`, "utf8");
159
+ fs.writeFileSync(path.join(temporary, "package.json"), files["package.json"]);
160
+ if (fs.existsSync(targetRoot)) {
161
+ const stat = fs.lstatSync(targetRoot);
162
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw scaffoldError("EXTENSION_INIT_TARGET_EXISTS", `Scaffold target is not a regular directory: ${targetRoot}`, { path: targetRoot });
163
+ if (filesMatch(targetRoot, files)) return Object.freeze({ action: "skip", reason: "already-current", path: targetRoot, extension: metadata, files: Object.keys(files).sort() });
164
+ if (fs.readdirSync(targetRoot).length > 0) throw scaffoldError("EXTENSION_INIT_TARGET_EXISTS", `Scaffold target is not empty: ${targetRoot}`, { path: targetRoot, remediation: "Choose another directory or remove its files before running extension init." });
165
+ }
166
+ commitDirectory(temporary, targetRoot);
167
+ return Object.freeze({ action: "created", path: targetRoot, extension: metadata, files: Object.keys(files).sort() });
168
+ } finally {
169
+ try { fs.rmSync(temporary, { recursive: true, force: true }); } catch {}
170
+ }
171
+ }
172
+
173
+ function digestUpdate(target) {
174
+ const packageRoot = path.resolve(target || ".");
175
+ const packageJsonFile = path.join(packageRoot, "package.json");
176
+ const extensionRoot = path.join(packageRoot, "extension");
177
+ const manifestFile = path.join(extensionRoot, "manifest.json");
178
+ const rawPackage = readJson(packageJsonFile, "EXTENSION_NPM_ENVELOPE_INVALID");
179
+ const rawManifest = readJson(manifestFile, "EXTENSION_MANIFEST_PARSE_FAILED");
180
+ const manifest = validateShellManifest(rawManifest);
181
+ validateExtensionTransportEnvelope(rawPackage, { expectedId: manifest.id, expectedVersion: manifest.version, expectedExtensionSpecVersion: manifest.extensionSpecVersion });
182
+ if (rawPackage.description !== rawManifest.description) throw scaffoldError("EXTENSION_NPM_IDENTITY_MISMATCH", "npm transport description does not match the Extension manifest", { field: "description", expected: rawManifest.description, actual: rawPackage.description });
183
+ const entryFile = path.join(extensionRoot, ...rawManifest.entry.split("/"));
184
+ if (!fs.existsSync(entryFile)) throw scaffoldError("EXTENSION_ENTRY_MISSING", `Extension entry is missing: ${entryFile}`, { path: entryFile });
185
+ const nextManifest = { ...rawManifest, entrySha256: sha256(fs.readFileSync(entryFile)) };
186
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "code-workspace-digest-"));
187
+ try {
188
+ const tempExtension = path.join(temporary, "extension");
189
+ fs.cpSync(extensionRoot, tempExtension, { recursive: true, force: true });
190
+ fs.writeFileSync(path.join(tempExtension, "manifest.json"), `${JSON.stringify(nextManifest, null, 2)}\n`);
191
+ const packageSha256 = directoryDigest(tempExtension);
192
+ const nextPackage = { ...rawPackage, codeWorkspace: { ...rawPackage.codeWorkspace, packageSha256 } };
193
+ validateExtensionTransportEnvelope(nextPackage, { expectedId: manifest.id, expectedVersion: manifest.version, expectedExtensionSpecVersion: manifest.extensionSpecVersion, expectedPackageSha256: packageSha256 });
194
+ const nextManifestBytes = Buffer.from(`${JSON.stringify(nextManifest, null, 2)}\n`, "utf8");
195
+ const nextPackageBytes = Buffer.from(`${JSON.stringify(nextPackage, null, 2)}\n`, "utf8");
196
+ if (fs.readFileSync(packageJsonFile).equals(nextPackageBytes) && fs.readFileSync(manifestFile).equals(nextManifestBytes)) return Object.freeze({ action: "skip", reason: "already-current", path: packageRoot, extension: { id: manifest.id, version: manifest.version }, entrySha256: nextManifest.entrySha256, packageSha256, updatedFiles: [] });
197
+ const transaction = createFileTransaction([packageJsonFile, manifestFile]);
198
+ try {
199
+ atomicWrite(manifestFile, nextManifestBytes);
200
+ atomicWrite(packageJsonFile, nextPackageBytes);
201
+ transaction.commit();
202
+ } catch (error) {
203
+ transaction.rollback(error);
204
+ throw error;
205
+ }
206
+ return Object.freeze({ action: "updated", path: packageRoot, extension: { id: manifest.id, version: manifest.version }, entrySha256: nextManifest.entrySha256, packageSha256, updatedFiles: ["extension/manifest.json", "package.json"] });
207
+ } finally {
208
+ try { fs.rmSync(temporary, { recursive: true, force: true }); } catch {}
209
+ }
210
+ }
211
+
212
+ module.exports = { digestUpdate, scaffoldExtensionPackage, validateScaffoldMetadata };
@@ -307,7 +307,10 @@ function validateManifest(value, options = {}) {
307
307
  throw extensionError("EXTENSION_MANIFEST_INVALID", `Extension ${id} outputs must be an array`, { extension: id });
308
308
  }
309
309
  if ((!Array.isArray(manifest.outputs) || manifest.outputs.length === 0) && hooks.length === 0 && !runtime) {
310
- throw extensionError("EXTENSION_MANIFEST_INVALID", `Extension ${id} must declare at least one output, Hook or runtime`, { extension: id });
310
+ throw extensionError("EXTENSION_MANIFEST_INVALID", `Extension ${id} must declare at least one output, Hook or runtime`, {
311
+ extension: id,
312
+ remediation: "Add outputs, hooks, or runtime to extension/manifest.json before packing.",
313
+ });
311
314
  }
312
315
  const ids = new Set();
313
316
  const declaredArtifacts = [];
@@ -435,6 +438,7 @@ function inspectExtensionPackageDirectory(source, options = {}) {
435
438
  version: manifest.version,
436
439
  expectedSha256: manifest.entrySha256,
437
440
  actualSha256: entrySha256,
441
+ remediation: "Run codew extension digest update <package-path> before packing.",
438
442
  });
439
443
  }
440
444
  return Object.freeze({