code-workspace-zhuiyi 1.0.0-beta.5 → 1.1.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
@@ -25,6 +25,8 @@ codew init .code-workspace
25
25
 
26
26
  `.code-workspace` is the directory name and can be determined by yourself.
27
27
 
28
+ Interactive initialization asks for “Development mode: Yes/No”. New Workspaces default to “Yes”; existing Workspaces use their saved value as the default. Pass `--dev true` or `--dev false` to select it explicitly without prompting.
29
+
28
30
  Non-interactive initialization:
29
31
 
30
32
  ```bash
package/README.zh-CN.md CHANGED
@@ -25,6 +25,8 @@ codew init .code-workspace
25
25
 
26
26
  `.code-workspace` 是目录名,可以自定义。
27
27
 
28
+ 交互式初始化会询问“开发模式:是/否”,新 Workspace 默认选择“是”;已有 Workspace 会以已保存的值作为默认选项。也可以通过 `--dev true` 或 `--dev false` 直接指定,不进入该选择。
29
+
28
30
  非交互式初始化:
29
31
 
30
32
  ```bash
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "releaseVersion": "1.0.0-beta.5",
3
+ "releaseVersion": "1.1.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.
@@ -45,12 +45,16 @@ codew extension init ./example-extension --yes
45
45
 
46
46
  ```text
47
47
  example-extension/
48
+ ├── .gitignore
49
+ ├── README.md
48
50
  ├── extension/
49
51
  │ ├── manifest.json
50
52
  │ └── init.js
51
53
  └── package.json
52
54
  ```
53
55
 
56
+ 初始化产物中的静态文件模板位于仓库的 `artifacts/templates/extension-init/`。后续调整 README、`.gitignore` 或 `extension/` 下的默认文件时,直接修改该模板目录即可;其中 `gitignore` 会在生成时映射为 `.gitignore`,以避免 npm 发布包排除点文件。`extension init` 只负责注入元数据、生成摘要并写入 package envelope。
57
+
54
58
  系统扩展仍使用固定版本目录:
55
59
 
56
60
  ```text
@@ -120,6 +124,15 @@ codew extension digest update ./example-extension --yes
120
124
 
121
125
  该命令会先更新 `extension/manifest.json.entrySha256`,再更新 `package.json.codeWorkspace.packageSha256`。
122
126
 
127
+ 完成开发后,可以在上传 Registry 前使用最终 tarball 在本地 Workspace 安装测试:
128
+
129
+ ```bash
130
+ codew extension pack ./example-extension --output ./dist
131
+ codew extension install --local ./dist/codew-ext-example-extension-0.1.0.tgz --yes
132
+ ```
133
+
134
+ `extension install --local` 只接受 `.tgz`,会复用正式安装的完整 tarball 校验、Extension Store 导入和 Workspace 事务;不支持直接安装源码目录,也不会覆盖相同 id/version 下的不同 package digest。`--local` 模式不能与 `--version`、`--allow-deprecated` 或 `--offline` 同时使用。
135
+
123
136
  ### 3.1 身份与展示
124
137
 
125
138
  - `schemaVersion` 必须是 `3`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-workspace-zhuiyi",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.1.0",
4
4
  "description": "Local multi-project registry and safety layer for AI coding workspaces",
5
5
  "author": "icebearx-ai",
6
6
  "license": "MIT",
@@ -14,6 +14,8 @@ const {
14
14
  const { loadInitManifest } = require("../../core/init");
15
15
  const { acquireInitLock } = require("../../core/init-lock");
16
16
  const { defaultExtensionStoreRoot } = require("../../core/extension-store");
17
+ const { planLocalExtensionPackage } = require("../../core/extension-registry-lifecycle");
18
+ const { importLocalExtensionTarball, prepareLocalExtensionTarball } = require("../../core/extension-local");
17
19
  const { packExtensionSourceToDirectory } = require("../../core/extension-package");
18
20
  const { digestUpdate, scaffoldExtensionPackage } = require("../../core/extension-scaffold");
19
21
  const { resolveExtensionSettings } = require("../../core/extension-settings");
@@ -207,6 +209,7 @@ function decorateBatchResults(batch, plans, options) {
207
209
  }
208
210
 
209
211
  async function executeExtensionInstall(invocation) {
212
+ if (invocation.options.local === true) return await executeExtensionInstallLocal(invocation);
210
213
  const command = "extension.install";
211
214
  const dependencies = registryDependencies(invocation.dependencies || {});
212
215
  validateInstallOptions(invocation);
@@ -315,6 +318,59 @@ async function executeExtensionInstall(invocation) {
315
318
  });
316
319
  }
317
320
 
321
+ async function executeExtensionInstallLocal(invocation) {
322
+ const command = "extension.install";
323
+ const dependencies = invocation.dependencies || {};
324
+ if (invocation.args.length !== 1) {
325
+ throw new WorkspaceError("EXTENSION_INSTALL_MODE_CONFLICT", "--local requires exactly one tarball path and no extension names.", {
326
+ remediation: "Use `codew extension install --local <tarball> --yes`.",
327
+ });
328
+ }
329
+ if (invocation.options.version || invocation.options.allowDeprecated || invocation.options.offline) {
330
+ throw new WorkspaceError("EXTENSION_INSTALL_MODE_CONFLICT", "--local cannot be combined with Registry version, deprecation, or offline options.", {
331
+ remediation: "Remove --version, --allow-deprecated, and --offline when installing a local tarball.",
332
+ });
333
+ }
334
+ const storeRoot = dependencies.extensionStoreRoot || defaultExtensionStoreRoot();
335
+ const prepared = await prepareLocalExtensionTarball(invocation.args[0], dependencies);
336
+ try {
337
+ const stateInspection = inspectExtensionState(invocation.root);
338
+ const tools = resolveWorkspaceTools({ state: loadState(invocation.root), manifestTools: loadInitManifest().tools }).tools;
339
+ const plan = planLocalExtensionPackage(prepared.packageRecord, { tools, state: stateInspection.state });
340
+ const planText = formatInstallPlan([{ ...plan, source: "local" }]);
341
+ if (!(await (dependencies.confirm || confirm)(`${planText}\nContinue?`, invocation.options))) {
342
+ throw new WorkspaceError("CLI_CANCELLED", "Local extension installation cancelled.");
343
+ }
344
+ await importLocalExtensionTarball(invocation.args[0], { ...dependencies, extensionStoreRoot: storeRoot });
345
+ const workspace = invocation.config.workspace;
346
+ const batch = runExtensionBatch(invocation.root, [plan], (extension) => ({
347
+ schemaVersion: 1,
348
+ extensionSpecVersion: extension.extensionSpecVersion,
349
+ extension: { id: extension.id, version: extension.version },
350
+ workspace: { name: workspace.name, uuid: workspace.uuid, language: workspace.language },
351
+ tools,
352
+ }), {
353
+ requested: [plan.id],
354
+ useExtensionStore: true,
355
+ extensionStoreRoot: storeRoot,
356
+ });
357
+ const results = decorateBatchResults(batch, [plan], invocation.options);
358
+ const diagnostics = batch.results.filter((item) => item.status === "failed").map((item) => ({
359
+ code: item.code || "EXTENSION_LOCAL_INSTALL_FAILED",
360
+ severity: "error",
361
+ message: item.message || `Extension ${item.id} failed to install.`,
362
+ extension: item.id,
363
+ version: item.version,
364
+ }));
365
+ return selectionResult(command, [plan.id], results.map((item) => ({ ...lifecycleResultEntry(item, "install"), archive: prepared.archive })), {
366
+ diagnostics,
367
+ text: lifecycleResultText(results, "install"),
368
+ });
369
+ } finally {
370
+ prepared.cleanup();
371
+ }
372
+ }
373
+
318
374
  async function executeExtensionSearch(invocation) {
319
375
  const dependencies = registryDependencies(invocation.dependencies || {});
320
376
  const interactive = dependencies.interactive ?? (!invocation.options.json && invocation.options.yes !== true && process.stdin.isTTY && process.stdout.isTTY);
@@ -648,6 +704,7 @@ module.exports = {
648
704
  collectRegistryExtensionInstallSelection,
649
705
  executeExtension,
650
706
  executeExtensionInstall,
707
+ executeExtensionInstallLocal,
651
708
  executeExtensionPack,
652
709
  executeExtensionInit,
653
710
  executeExtensionDigestUpdate,
@@ -99,7 +99,7 @@ async function executeInit(invocation) {
99
99
  async function executeInitUnlocked(invocation, root) {
100
100
  const { options } = invocation;
101
101
  const requestedDev = parseDevOption(options.dev);
102
- const dev = resolveWorkspaceDev(root, requestedDev);
102
+ let dev = resolveWorkspaceDev(root, requestedDev);
103
103
  const dependencies = invocation.dependencies && Object.prototype.hasOwnProperty.call(invocation.dependencies, "defaultRegistryUrl")
104
104
  ? invocation.dependencies
105
105
  : { ...(invocation.dependencies || {}), defaultRegistryUrl: DEFAULT_NEXUS_REGISTRY };
@@ -132,15 +132,11 @@ async function executeInitUnlocked(invocation, root) {
132
132
  const extensionState = extensionStateInspection.state;
133
133
  const systemCatalogResult = inspectExtensions ? discoverSystemExtensions({ tolerant: true }) : { catalog: [], invalid: [] };
134
134
  const systemExtensionIds = systemCatalogResult.catalog.filter((entry) => entry.latestSupported).map((entry) => entry.id);
135
- const systemRequestedExtensions = dev ? systemExtensionIds : [];
136
- const systemRemovalPlans = dev || extensionStateInspection.error
137
- ? []
138
- : Object.entries(extensionState.extensions)
139
- .filter(([id, entry]) => entry.installed?.system === true || systemExtensionIds.includes(id))
140
- .map(([id]) => planExtensionUninstall(root, id, { systemManaged: true }));
135
+ let systemRequestedExtensions = dev ? systemExtensionIds : [];
141
136
  let systemPreparation = { plans: [], failures: [], diagnostics: [] };
142
137
  let planningState = extensionState;
143
- const prepareSystemForTools = (tools) => {
138
+ const prepareSystemForTools = (tools, selectedDev = dev) => {
139
+ systemRequestedExtensions = selectedDev ? systemExtensionIds : [];
144
140
  systemPreparation = prepareExtensionPlans(systemCatalogResult, systemRequestedExtensions, {
145
141
  tools,
146
142
  state: extensionState,
@@ -183,6 +179,7 @@ async function executeInitUnlocked(invocation, root) {
183
179
  initialTools: resolvedTools.tools,
184
180
  workspaceName: options["workspace-name"],
185
181
  language: options.language,
182
+ dev: requestedDev,
186
183
  extensionState,
187
184
  extensions: explicitExtensions === null ? undefined : explicitExtensions,
188
185
  initialExtensions: explicitExtensions === null ? undefined : explicitExtensions,
@@ -198,6 +195,12 @@ async function executeInitUnlocked(invocation, root) {
198
195
  throw error;
199
196
  }
200
197
  }
198
+ if (plan?.workspace?.dev !== undefined) dev = plan.workspace.dev;
199
+ const systemRemovalPlans = dev || extensionStateInspection.error
200
+ ? []
201
+ : Object.entries(extensionState.extensions)
202
+ .filter(([id, entry]) => entry.installed?.system === true || systemExtensionIds.includes(id))
203
+ .map(([id]) => planExtensionUninstall(root, id, { systemManaged: true }));
201
204
  const tools = plan?.tools || resolvedTools.tools;
202
205
  const ordinaryRequestedExtensions = explicitExtensions !== null
203
206
  ? explicitExtensions
@@ -68,7 +68,7 @@ const COMMANDS = [
68
68
  tools: { type: "string" }, yes: { type: "boolean" },
69
69
  } },
70
70
  { path: ["extension", "install"], args: [{ name: "name", required: false, variadic: true }], workspace: "required", config: ["identity", "language"], interaction: "required", effects: "planned-write", options: {
71
- yes: { type: "boolean" }, version: { type: "string" }, "allow-deprecated": { type: "boolean" }, offline: { type: "boolean" },
71
+ yes: { type: "boolean" }, local: { type: "boolean" }, version: { type: "string" }, "allow-deprecated": { type: "boolean" }, offline: { type: "boolean" },
72
72
  } },
73
73
  { path: ["extension", "uninstall"], args: [{ name: "name", required: true }], workspace: "required", config: [], interaction: "required", effects: "planned-write", options: { yes: { type: "boolean" } } },
74
74
  { path: ["extension", "pack"], args: [{ name: "source", required: true }], workspace: "none", config: [], interaction: "never", effects: "planned-write", options: { output: { type: "string", required: true } } },
@@ -0,0 +1,66 @@
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
+ const { pipeline } = require("node:stream/promises");
6
+
7
+ const { WorkspaceError } = require("./errors");
8
+ const { DEFAULT_PACKAGE_LIMITS, extractExtensionTransportTarball } = require("./extension-package");
9
+ const { ensureStoredExtensionPackage, packageRecordFromDirectory } = require("./extension-store");
10
+
11
+ function localError(code, message, details = {}) {
12
+ return new WorkspaceError(code, message, details);
13
+ }
14
+
15
+ async function sha512File(file) {
16
+ const hash = crypto.createHash("sha512");
17
+ await pipeline(fs.createReadStream(file), hash);
18
+ return `sha512-${hash.digest("base64")}`;
19
+ }
20
+
21
+ async function prepareLocalExtensionTarball(file, options = {}) {
22
+ const archive = path.resolve(String(file || ""));
23
+ if (!archive.toLowerCase().endsWith(".tgz")) {
24
+ throw localError("EXTENSION_LOCAL_ARCHIVE_INVALID", "Local extension installation accepts only a .tgz tarball.", { path: archive });
25
+ }
26
+ let stat;
27
+ try { stat = fs.lstatSync(archive); } catch (error) {
28
+ throw localError("EXTENSION_LOCAL_ARCHIVE_MISSING", `Local extension tarball is missing: ${archive}`, { path: archive, cause: error.code });
29
+ }
30
+ if (!stat.isFile() || stat.isSymbolicLink()) throw localError("EXTENSION_LOCAL_ARCHIVE_INVALID", `Local extension tarball must be a regular file: ${archive}`, { path: archive });
31
+ const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "code-workspace-local-extension-"));
32
+ try {
33
+ const limits = options.packageLimits || DEFAULT_PACKAGE_LIMITS;
34
+ const inspected = await extractExtensionTransportTarball(archive, path.join(temporaryRoot, "unpacked"), { limits });
35
+ const integrity = await sha512File(archive);
36
+ const packageRecord = packageRecordFromDirectory(inspected.sourceRoot, {
37
+ id: inspected.manifest.id,
38
+ version: inspected.manifest.version,
39
+ validate: options.validate,
40
+ });
41
+ return Object.freeze({ sourceRoot: inspected.sourceRoot, packageRecord, archive: Object.freeze({ path: archive, integrity }), cleanup: () => fs.rmSync(temporaryRoot, { recursive: true, force: true }) });
42
+ } catch (error) {
43
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
44
+ if (error instanceof WorkspaceError) throw error;
45
+ throw localError("EXTENSION_LOCAL_ARCHIVE_INVALID", `Cannot import local extension tarball: ${error.message}`, { path: archive, cause: error.code });
46
+ }
47
+ }
48
+
49
+ async function importLocalExtensionTarball(file, options = {}) {
50
+ const prepared = await prepareLocalExtensionTarball(file, options);
51
+ try {
52
+ const stored = ensureStoredExtensionPackage({
53
+ sourceRoot: prepared.sourceRoot,
54
+ storeRoot: options.extensionStoreRoot,
55
+ id: prepared.packageRecord.id,
56
+ version: prepared.packageRecord.version,
57
+ provenance: { kind: "local", archiveIntegrity: prepared.archive.integrity },
58
+ validate: options.validate,
59
+ });
60
+ return Object.freeze({ ...stored, source: stored.source || "local", archive: prepared.archive, extensionSpecVersion: prepared.packageRecord.manifest.extensionSpecVersion });
61
+ } finally {
62
+ prepared.cleanup();
63
+ }
64
+ }
65
+
66
+ module.exports = { importLocalExtensionTarball, prepareLocalExtensionTarball };
@@ -717,6 +717,14 @@ function planFromPackageRecord(id, packageRecord, options = {}) {
717
717
  });
718
718
  }
719
719
 
720
+ function planLocalExtensionPackage(packageRecord, options = {}) {
721
+ return planFromPackageRecord(packageRecord.id, packageRecord, {
722
+ ...options,
723
+ source: "local",
724
+ resolutionScope: "local",
725
+ });
726
+ }
727
+
720
728
  async function prepareRegistryExtensionPlans(options = {}) {
721
729
  const requested = normalizeExtensionNames(options.requested || []);
722
730
  if (options.version && requested.length !== 1) {
@@ -791,6 +799,7 @@ module.exports = {
791
799
  getRegistryExtensionInfo,
792
800
  listRegistryExtensionChoices,
793
801
  prepareRegistryExtensionPlans,
802
+ planLocalExtensionPackage,
794
803
  resolveRegistryExtensionCandidate,
795
804
  searchRegistryExtensions,
796
805
  };
@@ -38,8 +38,25 @@ function validateScaffoldMetadata(options = {}, targetRoot) {
38
38
  return Object.freeze({ id, name, description, version });
39
39
  }
40
40
 
41
- function shellEntry() {
42
- return "#!/usr/bin/env node\n\"use strict\";\n\n// TODO: implement the extension init protocol.\n";
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;
43
60
  }
44
61
 
45
62
  function validateShellManifest(raw) {
@@ -107,7 +124,9 @@ function shellEnvelope(metadata, packageSha256) {
107
124
  function scaffoldExtensionPackage(target, options = {}) {
108
125
  const targetRoot = path.resolve(target || ".");
109
126
  const metadata = validateScaffoldMetadata(options, targetRoot);
110
- const entry = shellEntry();
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" });
111
130
  const manifest = {
112
131
  schemaVersion: 3,
113
132
  extensionSpecVersion: 1,
@@ -121,9 +140,9 @@ function scaffoldExtensionPackage(target, options = {}) {
121
140
  timeoutMs: 30000,
122
141
  };
123
142
  const files = {
143
+ ...templateFiles,
124
144
  "package.json": null,
125
145
  "extension/manifest.json": Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
126
- "extension/init.js": Buffer.from(entry, "utf8"),
127
146
  };
128
147
  const temporary = fs.mkdtempSync(path.join(path.dirname(targetRoot), `.${path.basename(targetRoot)}-`));
129
148
  try {
@@ -113,6 +113,14 @@ function normalizeProvenance(value, key = "package") {
113
113
  archiveIntegrity,
114
114
  });
115
115
  }
116
+ if (value.kind === "local") {
117
+ const allowed = new Set(["kind", "archiveIntegrity"]);
118
+ const unknown = Object.keys(value).filter((field) => !allowed.has(field));
119
+ if (unknown.length > 0) throw storeError("EXTENSION_STORE_PROVENANCE_INVALID", `Local Store provenance contains unsupported field ${unknown[0]} for ${key}`, { package: key, field: unknown[0] });
120
+ const archiveIntegrity = String(value.archiveIntegrity || "");
121
+ if (!SHA512_SRI_PATTERN.test(archiveIntegrity)) throw storeError("EXTENSION_STORE_PROVENANCE_INVALID", `Invalid local archive integrity for ${key}`, { package: key });
122
+ return Object.freeze({ kind: "local", archiveIntegrity });
123
+ }
116
124
  throw storeError("EXTENSION_STORE_PROVENANCE_INVALID", `Unsupported Store provenance kind for ${key}: ${value.kind || "<missing>"}`, { package: key, kind: value.kind || null });
117
125
  }
118
126
 
@@ -142,7 +150,7 @@ function validatePackageRecord(key, value) {
142
150
  manifestSha256: value.manifestSha256,
143
151
  entrySha256: value.entrySha256,
144
152
  packageSha256: value.packageSha256,
145
- source: provenance.kind === "builtin" ? "builtin" : "nexus",
153
+ source: provenance.kind === "builtin" ? "builtin" : provenance.kind === "local" ? "local" : "nexus",
146
154
  provenance,
147
155
  importedAt: value.importedAt || null,
148
156
  references: normalizeReferences(value.references),
@@ -276,7 +284,7 @@ function ensureStoredExtensionPackage(options = {}) {
276
284
  manifestSha256: copied.manifestSha256,
277
285
  entrySha256: copied.entrySha256,
278
286
  packageSha256: copied.packageSha256,
279
- source: provenance.kind === "builtin" ? "builtin" : "nexus",
287
+ source: provenance.kind === "builtin" ? "builtin" : provenance.kind === "local" ? "local" : "nexus",
280
288
  provenance,
281
289
  importedAt: new Date().toISOString(),
282
290
  references: registry.packages[packageKey(source.id, source.version)]?.references || normalizeReferences(),
@@ -90,6 +90,17 @@ async function collectInitPlan(root, manifest, options = {}) {
90
90
  });
91
91
  const existing = fs.existsSync(configPath(root)) ? loadConfig(root, { defaultLanguage: existingLanguage }) : null;
92
92
  const name = existing?.workspace?.name || options.workspaceName || await ui.text("Workspace name", DEFAULT_WORKSPACE_NAME);
93
+ const initialDev = options.dev ?? existing?.workspace?.dev ?? true;
94
+ const dev = options.dev !== undefined
95
+ ? options.dev
96
+ : await ui.select(
97
+ "Development mode",
98
+ [
99
+ { value: true, label: "Yes" },
100
+ { value: false, label: "No" },
101
+ ],
102
+ initialDev ? 0 : 1
103
+ );
93
104
  const languageChoices = SUPPORTED_LANGUAGES.map((entry) => ({
94
105
  value: entry.value,
95
106
  label: `${entry.label} · ${entry.value}`,
@@ -109,14 +120,17 @@ async function collectInitPlan(root, manifest, options = {}) {
109
120
  options.initialTools || ["claude", "codex"]
110
121
  );
111
122
  const systemExtensions = typeof options.prepareSystemExtensions === "function"
112
- ? await options.prepareSystemExtensions(tools)
123
+ ? await options.prepareSystemExtensions(tools, dev)
113
124
  : (options.systemExtensions || []);
114
125
  const ordinary = await collectExtensionPlans(options, ui, tools);
115
126
  const extensions = [
116
127
  ...systemExtensions,
117
128
  ...ordinary.plans,
118
129
  ];
119
- const workspace = existing?.workspace || { name, uuid: randomUUID() };
130
+ const workspace = {
131
+ ...(existing?.workspace || { name, uuid: randomUUID() }),
132
+ dev,
133
+ };
120
134
  const extensionRemovals = ordinary.uninstallPlans || [];
121
135
  const plan = createInitPlan({ root, workspace, tools, language, extensions, extensionRemovals });
122
136
  const extensionChanges = [
@@ -125,6 +139,7 @@ async function collectInitPlan(root, manifest, options = {}) {
125
139
  ];
126
140
  ui.note("Ready to initialize", [
127
141
  `Workspace ${workspace.name}`,
142
+ `Development mode ${dev ? "Yes" : "No"}`,
128
143
  `Language ${language}`,
129
144
  `Tools ${tools.length ? tools.join(", ") : "none"}`,
130
145
  `Extensions ${extensions.length ? extensions.map((entry) => `${entry.id}@${entry.version} [Spec ${entry.extensionSpecVersion}] (${entry.manifestSha256})`).join(", ") : "none"}`,