@cjhyy/code-shell-core 0.8.8 → 0.8.10

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.
Files changed (86) hide show
  1. package/dist/automation/scheduler.js +49 -0
  2. package/dist/automation/store.d.ts +1 -1
  3. package/dist/automation/store.js +184 -10
  4. package/dist/cli/agent-server-stdio.js +7 -0
  5. package/dist/credentials/store.d.ts +14 -0
  6. package/dist/credentials/store.js +245 -42
  7. package/dist/engine/engine.js +81 -27
  8. package/dist/engine/file-history-hook.js +24 -5
  9. package/dist/engine/run-finalize.js +8 -6
  10. package/dist/engine/run-session-open.js +1 -1
  11. package/dist/engine/run-setup.d.ts +4 -0
  12. package/dist/engine/run-setup.js +5 -1
  13. package/dist/engine/run-tooling.js +7 -1
  14. package/dist/engine/run-types.d.ts +33 -0
  15. package/dist/engine/run-types.js +21 -0
  16. package/dist/engine/run-workspace.js +11 -5
  17. package/dist/engine/turn-loop.js +9 -8
  18. package/dist/goal/lifecycle.d.ts +2 -0
  19. package/dist/goal/lifecycle.js +56 -33
  20. package/dist/index.d.ts +2 -3
  21. package/dist/index.internal.d.ts +1 -0
  22. package/dist/index.internal.js +1 -0
  23. package/dist/index.js +2 -2
  24. package/dist/links/cli.d.ts +2 -0
  25. package/dist/links/cli.js +11 -4
  26. package/dist/model-catalog/index.js +19 -4
  27. package/dist/model-catalog/save-entry.js +122 -61
  28. package/dist/model-catalog/types.js +27 -23
  29. package/dist/panel-apps/installer.js +27 -14
  30. package/dist/panel-apps/manifest.d.ts +11 -11
  31. package/dist/panel-apps/manifest.js +3 -1
  32. package/dist/panel-apps/registry.js +60 -12
  33. package/dist/plugins/installedPlugins.d.ts +4 -0
  34. package/dist/plugins/installedPlugins.js +70 -30
  35. package/dist/plugins/installer/types.d.ts +12 -12
  36. package/dist/plugins/installer/update.js +37 -38
  37. package/dist/plugins/knownMarketplaces.d.ts +7 -3
  38. package/dist/plugins/knownMarketplaces.js +127 -23
  39. package/dist/plugins/pluginCatalog.js +18 -4
  40. package/dist/plugins/pluginHookApproval.js +56 -60
  41. package/dist/plugins/pluginMcpApproval.js +50 -52
  42. package/dist/profile/catalog-store.js +39 -4
  43. package/dist/profile/catalog.js +55 -15
  44. package/dist/profile/store.js +51 -21
  45. package/dist/prompt/composer.d.ts +8 -0
  46. package/dist/prompt/composer.js +8 -0
  47. package/dist/protocol/chat-session-manager.d.ts +9 -0
  48. package/dist/protocol/chat-session-manager.js +22 -0
  49. package/dist/protocol/chat-session.d.ts +5 -0
  50. package/dist/protocol/chat-session.js +1 -0
  51. package/dist/protocol/server.d.ts +2 -0
  52. package/dist/protocol/server.js +112 -29
  53. package/dist/protocol/types.d.ts +20 -0
  54. package/dist/run/FileRunStore.d.ts +2 -0
  55. package/dist/run/FileRunStore.js +153 -18
  56. package/dist/run/Heartbeat.js +63 -4
  57. package/dist/services/auto-dream.js +39 -17
  58. package/dist/services/session-memory.js +107 -8
  59. package/dist/session/file-history.d.ts +63 -2
  60. package/dist/session/file-history.js +593 -86
  61. package/dist/session/session-manager.d.ts +4 -1
  62. package/dist/session/session-manager.js +94 -47
  63. package/dist/session/transcript.js +33 -3
  64. package/dist/session/undo-target.d.ts +15 -6
  65. package/dist/session/undo-target.js +26 -9
  66. package/dist/settings/manager.d.ts +22 -3
  67. package/dist/settings/manager.js +185 -50
  68. package/dist/settings/schema.d.ts +3 -3
  69. package/dist/sources/adapters/local-files.js +49 -4
  70. package/dist/sources/catalog.js +64 -18
  71. package/dist/sources/types.d.ts +3 -3
  72. package/dist/sources/types.js +7 -4
  73. package/dist/themes/installer.js +192 -28
  74. package/dist/tool-system/builtin/add-marketplace.js +21 -1
  75. package/dist/tool-system/builtin/cron.d.ts +2 -1
  76. package/dist/tool-system/builtin/cron.js +20 -6
  77. package/dist/tool-system/builtin/index.js +48 -1
  78. package/dist/tool-system/builtin/install-capability.d.ts +52 -0
  79. package/dist/tool-system/builtin/install-capability.js +1057 -0
  80. package/dist/tool-system/builtin/skill.js +3 -1
  81. package/dist/tool-system/executor.js +1 -0
  82. package/dist/tool-system/path-policy.js +25 -33
  83. package/dist/tool-system/registry.js +5 -0
  84. package/dist/utils/file-mutex.d.ts +2 -0
  85. package/dist/utils/file-mutex.js +29 -4
  86. package/package.json +2 -1
@@ -3,10 +3,10 @@
3
3
  * Reads the Claude Code-compatible V2 shape plus optional CodeShell integrity
4
4
  * fields on each install entry.
5
5
  */
6
- import { randomUUID } from "node:crypto";
7
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
6
+ import { chmodSync, closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, } from "node:fs";
7
+ import { join } from "node:path";
9
8
  import { homedir } from "node:os";
9
+ import { mutateJsonFile } from "../utils/file-mutex.js";
10
10
  const MAX_PLUGIN_KEYS = 2_048;
11
11
  const MAX_INSTALLS_PER_KEY = 16;
12
12
  const MAX_KEY_LENGTH = 256;
@@ -16,6 +16,7 @@ const DIGEST_RE = /^[a-f0-9]{64}$/;
16
16
  const MAX_HOOK_REVIEW_ITEMS = 256;
17
17
  const MAX_HOOK_REVIEW_COMMAND_LENGTH = 4_096;
18
18
  const MAX_HOOK_REVIEW_MATCHER_LENGTH = 4_096;
19
+ const MAX_INSTALLED_PLUGINS_FILE_BYTES = 16 * 1024 * 1024;
19
20
  function userHome() {
20
21
  return process.env.HOME ?? homedir();
21
22
  }
@@ -142,52 +143,91 @@ function registryOf(value) {
142
143
  }
143
144
  export function readInstalledPlugins() {
144
145
  const path = installedPluginsPath();
145
- if (!existsSync(path))
146
- return { version: 2, plugins: {} };
146
+ let descriptor;
147
147
  try {
148
- const parsed = registryOf(JSON.parse(readFileSync(path, "utf-8")));
148
+ const metadata = lstatSync(path);
149
+ if (metadata.isSymbolicLink() ||
150
+ !metadata.isFile() ||
151
+ metadata.size > MAX_INSTALLED_PLUGINS_FILE_BYTES) {
152
+ return { version: 2, plugins: {} };
153
+ }
154
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
155
+ const opened = fstatSync(descriptor);
156
+ if (!opened.isFile() || opened.size > MAX_INSTALLED_PLUGINS_FILE_BYTES) {
157
+ return { version: 2, plugins: {} };
158
+ }
159
+ const parsed = registryOf(JSON.parse(readFileSync(descriptor, "utf-8")));
149
160
  if (parsed)
150
161
  return parsed;
151
162
  }
152
163
  catch {
153
164
  // Corrupt — treat as empty so the user can re-install.
154
165
  }
166
+ finally {
167
+ if (descriptor !== undefined)
168
+ closeSync(descriptor);
169
+ }
155
170
  return { version: 2, plugins: {} };
156
171
  }
157
172
  export function writeInstalledPlugins(data) {
158
173
  const path = installedPluginsPath();
159
- mkdirSync(dirname(path), { recursive: true });
160
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
161
- try {
162
- writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", {
163
- encoding: "utf-8",
164
- flag: "wx",
165
- mode: 0o600,
166
- });
167
- renameSync(tmp, path);
168
- }
169
- finally {
170
- rmSync(tmp, { force: true });
171
- }
174
+ const normalized = registryOf(data);
175
+ if (!normalized)
176
+ throw new Error("invalid installed plugins registry");
177
+ mutateJsonFile(path, {
178
+ parse: parseInstalledPlugins,
179
+ serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
180
+ mutation: () => ({ value: normalized }),
181
+ mode: 0o600,
182
+ maxBytes: MAX_INSTALLED_PLUGINS_FILE_BYTES,
183
+ });
184
+ if (process.platform !== "win32")
185
+ chmodSync(path, 0o600);
186
+ }
187
+ function parseInstalledPlugins(raw) {
188
+ if (raw === undefined)
189
+ return { version: 2, plugins: {} };
190
+ const parsed = registryOf(JSON.parse(raw));
191
+ if (!parsed)
192
+ throw new Error("installed plugins registry is corrupt");
193
+ return parsed;
194
+ }
195
+ export function mutateInstalledPlugins(mutation) {
196
+ return mutateJsonFile(installedPluginsPath(), {
197
+ parse: parseInstalledPlugins,
198
+ serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
199
+ mutation,
200
+ mode: 0o600,
201
+ maxBytes: MAX_INSTALLED_PLUGINS_FILE_BYTES,
202
+ });
172
203
  }
173
204
  /**
174
205
  * Append an install entry for `<plugin>@<marketplace>`. Multiple entries
175
206
  * for the same key are allowed (different scopes); MVP only writes scope:"user".
176
207
  */
177
208
  export function appendInstallEntry(key, entry) {
178
- const data = readInstalledPlugins();
179
- const list = data.plugins[key] ?? [];
180
- list.push(entry);
181
- data.plugins[key] = list;
182
- writeInstalledPlugins(data);
209
+ if (key.length === 0 || key.length > MAX_KEY_LENGTH || key.includes("\0")) {
210
+ throw new Error("invalid plugin install key");
211
+ }
212
+ const normalized = installEntryOf(entry);
213
+ if (!normalized)
214
+ throw new Error("invalid plugin install entry");
215
+ mutateInstalledPlugins((data) => {
216
+ const list = [...(data.plugins[key] ?? [])];
217
+ if (list.length >= MAX_INSTALLS_PER_KEY)
218
+ throw new Error("too many plugin installs for key");
219
+ list.push(normalized);
220
+ data.plugins[key] = list;
221
+ return { value: data };
222
+ });
183
223
  }
184
224
  export function removeInstallEntries(key) {
185
- const data = readInstalledPlugins();
186
- if (!(key in data.plugins))
187
- return false;
188
- delete data.plugins[key];
189
- writeInstalledPlugins(data);
190
- return true;
225
+ return (mutateInstalledPlugins((data) => {
226
+ if (!Object.prototype.hasOwnProperty.call(data.plugins, key))
227
+ return { result: false };
228
+ delete data.plugins[key];
229
+ return { value: data, result: true };
230
+ }) ?? false);
191
231
  }
192
232
  export function pluginInstallKey(plugin, marketplace) {
193
233
  return `${plugin}@${marketplace}`;
@@ -29,8 +29,8 @@ export declare const PluginAutomationTemplate: z.ZodObject<{
29
29
  en?: string | undefined;
30
30
  "zh-CN"?: string | undefined;
31
31
  };
32
- permissionLevel: "full" | "read-only" | "workspace-write";
33
32
  schedule: string;
33
+ permissionLevel: "full" | "read-only" | "workspace-write";
34
34
  description?: string | undefined;
35
35
  timezone?: string | undefined;
36
36
  }, {
@@ -79,8 +79,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
79
79
  en?: string | undefined;
80
80
  "zh-CN"?: string | undefined;
81
81
  };
82
- permissionLevel: "full" | "read-only" | "workspace-write";
83
82
  schedule: string;
83
+ permissionLevel: "full" | "read-only" | "workspace-write";
84
84
  description?: string | undefined;
85
85
  timezone?: string | undefined;
86
86
  }, {
@@ -108,8 +108,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
108
108
  en?: string | undefined;
109
109
  "zh-CN"?: string | undefined;
110
110
  };
111
- permissionLevel: "full" | "read-only" | "workspace-write";
112
111
  schedule: string;
112
+ permissionLevel: "full" | "read-only" | "workspace-write";
113
113
  description?: string | undefined;
114
114
  timezone?: string | undefined;
115
115
  }[];
@@ -140,8 +140,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
140
140
  en?: string | undefined;
141
141
  "zh-CN"?: string | undefined;
142
142
  };
143
- permissionLevel: "full" | "read-only" | "workspace-write";
144
143
  schedule: string;
144
+ permissionLevel: "full" | "read-only" | "workspace-write";
145
145
  description?: string | undefined;
146
146
  timezone?: string | undefined;
147
147
  }[];
@@ -249,8 +249,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
249
249
  en?: string | undefined;
250
250
  "zh-CN"?: string | undefined;
251
251
  };
252
- permissionLevel: "full" | "read-only" | "workspace-write";
253
252
  schedule: string;
253
+ permissionLevel: "full" | "read-only" | "workspace-write";
254
254
  description?: string | undefined;
255
255
  timezone?: string | undefined;
256
256
  }, {
@@ -278,8 +278,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
278
278
  en?: string | undefined;
279
279
  "zh-CN"?: string | undefined;
280
280
  };
281
- permissionLevel: "full" | "read-only" | "workspace-write";
282
281
  schedule: string;
282
+ permissionLevel: "full" | "read-only" | "workspace-write";
283
283
  description?: string | undefined;
284
284
  timezone?: string | undefined;
285
285
  }[];
@@ -310,8 +310,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
310
310
  en?: string | undefined;
311
311
  "zh-CN"?: string | undefined;
312
312
  };
313
- permissionLevel: "full" | "read-only" | "workspace-write";
314
313
  schedule: string;
314
+ permissionLevel: "full" | "read-only" | "workspace-write";
315
315
  description?: string | undefined;
316
316
  timezone?: string | undefined;
317
317
  }[];
@@ -345,8 +345,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
345
345
  en?: string | undefined;
346
346
  "zh-CN"?: string | undefined;
347
347
  };
348
- permissionLevel: "full" | "read-only" | "workspace-write";
349
348
  schedule: string;
349
+ permissionLevel: "full" | "read-only" | "workspace-write";
350
350
  description?: string | undefined;
351
351
  timezone?: string | undefined;
352
352
  }[];
@@ -634,8 +634,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
634
634
  en?: string | undefined;
635
635
  "zh-CN"?: string | undefined;
636
636
  };
637
- permissionLevel: "full" | "read-only" | "workspace-write";
638
637
  schedule: string;
638
+ permissionLevel: "full" | "read-only" | "workspace-write";
639
639
  description?: string | undefined;
640
640
  timezone?: string | undefined;
641
641
  }, {
@@ -663,8 +663,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
663
663
  en?: string | undefined;
664
664
  "zh-CN"?: string | undefined;
665
665
  };
666
- permissionLevel: "full" | "read-only" | "workspace-write";
667
666
  schedule: string;
667
+ permissionLevel: "full" | "read-only" | "workspace-write";
668
668
  description?: string | undefined;
669
669
  timezone?: string | undefined;
670
670
  }[];
@@ -695,8 +695,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
695
695
  en?: string | undefined;
696
696
  "zh-CN"?: string | undefined;
697
697
  };
698
- permissionLevel: "full" | "read-only" | "workspace-write";
699
698
  schedule: string;
699
+ permissionLevel: "full" | "read-only" | "workspace-write";
700
700
  description?: string | undefined;
701
701
  timezone?: string | undefined;
702
702
  }[];
@@ -750,8 +750,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
750
750
  en?: string | undefined;
751
751
  "zh-CN"?: string | undefined;
752
752
  };
753
- permissionLevel: "full" | "read-only" | "workspace-write";
754
753
  schedule: string;
754
+ permissionLevel: "full" | "read-only" | "workspace-write";
755
755
  description?: string | undefined;
756
756
  timezone?: string | undefined;
757
757
  }[];
@@ -7,7 +7,7 @@ import { pluginInstallDir } from "./paths.js";
7
7
  import { installPluginFromPath } from "./install.js";
8
8
  import { installPluginFromSource } from "./installFromSource.js";
9
9
  import { parseSource } from "./parseSource.js";
10
- import { pluginInstallKey, readInstalledPlugins, removeInstallEntries, writeInstalledPlugins, } from "../installedPlugins.js";
10
+ import { pluginInstallKey, mutateInstalledPlugins, readInstalledPlugins, removeInstallEntries, } from "../installedPlugins.js";
11
11
  import { pluginHookInstallRecord } from "../pluginHookIntegrity.js";
12
12
  import { pluginMcpInstallRecord } from "../pluginMcpIntegrity.js";
13
13
  import { readPluginMcp } from "./loadPluginMcp.js";
@@ -40,37 +40,37 @@ export async function reinstallAtomic(name, source, doInstall) {
40
40
  try {
41
41
  await doInstall();
42
42
  if (savedEntries) {
43
- const data = readInstalledPlugins();
44
- const installed = data.plugins[key] ?? [];
45
- let changed = false;
46
- for (const entry of installed) {
47
- const next = pluginHookInstallRecord(entry.installPath, savedEntries);
48
- const nextMcp = pluginMcpInstallRecord(entry.installPath, Object.keys(readPluginMcp(entry.installPath, name)).length > 0, savedEntries);
49
- if (entry.hookDigest !== next.hookDigest ||
50
- entry.approvedHookDigest !== next.approvedHookDigest ||
51
- JSON.stringify(entry.approvedHookSnapshot) !==
52
- JSON.stringify(next.approvedHookSnapshot) ||
53
- entry.mcpDigest !== nextMcp.mcpDigest ||
54
- entry.approvedMcpDigest !== nextMcp.approvedMcpDigest) {
55
- entry.hookDigest = next.hookDigest;
56
- if (next.approvedHookDigest)
57
- entry.approvedHookDigest = next.approvedHookDigest;
58
- else
59
- delete entry.approvedHookDigest;
60
- if (next.approvedHookSnapshot)
61
- entry.approvedHookSnapshot = next.approvedHookSnapshot;
62
- else
63
- delete entry.approvedHookSnapshot;
64
- entry.mcpDigest = nextMcp.mcpDigest;
65
- if (nextMcp.approvedMcpDigest)
66
- entry.approvedMcpDigest = nextMcp.approvedMcpDigest;
67
- else
68
- delete entry.approvedMcpDigest;
69
- changed = true;
43
+ mutateInstalledPlugins((data) => {
44
+ const installed = data.plugins[key] ?? [];
45
+ let changed = false;
46
+ for (const entry of installed) {
47
+ const next = pluginHookInstallRecord(entry.installPath, savedEntries);
48
+ const nextMcp = pluginMcpInstallRecord(entry.installPath, Object.keys(readPluginMcp(entry.installPath, name)).length > 0, savedEntries);
49
+ if (entry.hookDigest !== next.hookDigest ||
50
+ entry.approvedHookDigest !== next.approvedHookDigest ||
51
+ JSON.stringify(entry.approvedHookSnapshot) !==
52
+ JSON.stringify(next.approvedHookSnapshot) ||
53
+ entry.mcpDigest !== nextMcp.mcpDigest ||
54
+ entry.approvedMcpDigest !== nextMcp.approvedMcpDigest) {
55
+ entry.hookDigest = next.hookDigest;
56
+ if (next.approvedHookDigest)
57
+ entry.approvedHookDigest = next.approvedHookDigest;
58
+ else
59
+ delete entry.approvedHookDigest;
60
+ if (next.approvedHookSnapshot)
61
+ entry.approvedHookSnapshot = next.approvedHookSnapshot;
62
+ else
63
+ delete entry.approvedHookSnapshot;
64
+ entry.mcpDigest = nextMcp.mcpDigest;
65
+ if (nextMcp.approvedMcpDigest)
66
+ entry.approvedMcpDigest = nextMcp.approvedMcpDigest;
67
+ else
68
+ delete entry.approvedMcpDigest;
69
+ changed = true;
70
+ }
70
71
  }
71
- }
72
- if (changed)
73
- writeInstalledPlugins(data);
72
+ return changed ? { value: data } : {};
73
+ });
74
74
  }
75
75
  }
76
76
  catch (err) {
@@ -78,13 +78,12 @@ export async function reinstallAtomic(name, source, doInstall) {
78
78
  await rm(dir, { recursive: true, force: true });
79
79
  await rename(backup, dir);
80
80
  if (savedEntries) {
81
- // Restore in a single read-modify-write rather than one append per entry,
82
- // which both narrows the race window against any concurrent writer and
83
- // avoids re-appending onto whatever partial state doInstall() may have
84
- // left for this key.
85
- const data = readInstalledPlugins();
86
- data.plugins[key] = savedEntries;
87
- writeInstalledPlugins(data);
81
+ // Restore under the registry lock so a concurrent install of a different
82
+ // plugin cannot be lost while this plugin rolls back.
83
+ mutateInstalledPlugins((data) => {
84
+ data.plugins[key] = savedEntries;
85
+ return { value: data };
86
+ });
88
87
  }
89
88
  throw new PluginInstallError(`update failed; the old version of '${name}' was kept: ` +
90
89
  `${err instanceof Error ? err.message : String(err)}. Source: ${source}.`);
@@ -1,8 +1,12 @@
1
+ /** Read/write ~/.code-shell/plugins/known_marketplaces.json. */
2
+ import type { KnownMarketplaces, KnownMarketplace } from "./types.js";
1
3
  /**
2
- * Read/write ~/.code-shell/plugins/known_marketplaces.json.
3
- * File format is byte-compatible with Claude Code's analogous file.
4
+ * The single source of truth for what a marketplace name may be.
5
+ *
6
+ * Exported so entry points (AddMarketplace) reject a bad name BEFORE cloning,
7
+ * rather than discovering it at persist time and orphaning the clone.
4
8
  */
5
- import type { KnownMarketplaces, KnownMarketplace } from "./types.js";
9
+ export declare function isValidMarketplaceName(name: string): boolean;
6
10
  export declare function knownMarketplacesPath(): string;
7
11
  export declare function readKnownMarketplaces(): KnownMarketplaces;
8
12
  export declare function writeKnownMarketplaces(data: KnownMarketplaces): void;
@@ -1,46 +1,150 @@
1
+ /** Read/write ~/.code-shell/plugins/known_marketplaces.json. */
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { mutateJsonFile } from "../utils/file-mutex.js";
6
+ const MAX_MARKETPLACE_FILE_BYTES = 4 * 1024 * 1024;
7
+ const MAX_MARKETPLACES = 256;
8
+ const MAX_NAME_LENGTH = 128;
9
+ const MAX_PATH_LENGTH = 4_096;
10
+ const MAX_TIMESTAMP_LENGTH = 128;
11
+ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
+ const GITHUB_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
1
13
  /**
2
- * Read/write ~/.code-shell/plugins/known_marketplaces.json.
3
- * File format is byte-compatible with Claude Code's analogous file.
14
+ * The single source of truth for what a marketplace name may be.
15
+ *
16
+ * Exported so entry points (AddMarketplace) reject a bad name BEFORE cloning,
17
+ * rather than discovering it at persist time and orphaning the clone.
4
18
  */
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
7
- import { homedir } from "node:os";
19
+ export function isValidMarketplaceName(name) {
20
+ return name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
21
+ }
8
22
  function userHome() {
9
23
  return process.env.HOME ?? homedir();
10
24
  }
11
25
  export function knownMarketplacesPath() {
12
26
  return join(userHome(), ".code-shell", "plugins", "known_marketplaces.json");
13
27
  }
28
+ function marketplaceOf(value) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value))
30
+ return undefined;
31
+ const raw = value;
32
+ if (!raw.source || typeof raw.source !== "object" || Array.isArray(raw.source))
33
+ return undefined;
34
+ const source = raw.source;
35
+ const normalizedSource = source.source === "github" &&
36
+ typeof source.repo === "string" &&
37
+ GITHUB_REPO_RE.test(source.repo)
38
+ ? { source: "github", repo: source.repo }
39
+ : source.source === "git" &&
40
+ typeof source.url === "string" &&
41
+ source.url.length > 0 &&
42
+ source.url.length <= MAX_PATH_LENGTH &&
43
+ !source.url.includes("\0")
44
+ ? { source: "git", url: source.url }
45
+ : undefined;
46
+ if (!normalizedSource ||
47
+ typeof raw.installLocation !== "string" ||
48
+ raw.installLocation.length === 0 ||
49
+ raw.installLocation.length > MAX_PATH_LENGTH ||
50
+ raw.installLocation.includes("\0") ||
51
+ typeof raw.lastUpdated !== "string" ||
52
+ raw.lastUpdated.length === 0 ||
53
+ raw.lastUpdated.length > MAX_TIMESTAMP_LENGTH) {
54
+ return undefined;
55
+ }
56
+ const format = raw.format === "claude-code" || raw.format === "codex" || raw.format === "universal"
57
+ ? raw.format
58
+ : undefined;
59
+ if (raw.format !== undefined && !format)
60
+ return undefined;
61
+ return {
62
+ source: normalizedSource,
63
+ installLocation: raw.installLocation,
64
+ lastUpdated: raw.lastUpdated,
65
+ ...(format ? { format } : {}),
66
+ };
67
+ }
68
+ function parseKnownMarketplaces(raw) {
69
+ if (raw === undefined)
70
+ return Object.create(null);
71
+ const parsed = JSON.parse(raw);
72
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
73
+ throw new Error("known marketplaces registry must be an object");
74
+ }
75
+ const entries = Object.entries(parsed);
76
+ if (entries.length > MAX_MARKETPLACES)
77
+ throw new Error("too many known marketplaces");
78
+ const result = Object.create(null);
79
+ for (const [name, value] of entries) {
80
+ // ISOLATE, don't throw. NAME_RE is stricter than the writers that produced
81
+ // these keys (AddMarketplace accepts any non-empty string, and
82
+ // assertSafePluginName permits spaces/non-ASCII), so a single legacy name
83
+ // like "My Market" used to make the whole registry read as empty AND make
84
+ // every subsequent write throw — unrecoverable, because the read reported
85
+ // nothing for the user to fix. Skipping matches registryOf() in
86
+ // installedPlugins.ts, which is the correct shape for this file family.
87
+ if (name.length > MAX_NAME_LENGTH || !NAME_RE.test(name))
88
+ continue;
89
+ const marketplace = marketplaceOf(value);
90
+ if (!marketplace)
91
+ continue;
92
+ result[name] = marketplace;
93
+ }
94
+ return result;
95
+ }
14
96
  export function readKnownMarketplaces() {
15
97
  const path = knownMarketplacesPath();
16
- if (!existsSync(path))
17
- return {};
98
+ let descriptor;
18
99
  try {
19
- const raw = JSON.parse(readFileSync(path, "utf-8"));
20
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
21
- return raw;
100
+ const metadata = lstatSync(path);
101
+ if (metadata.isSymbolicLink() ||
102
+ !metadata.isFile() ||
103
+ metadata.size > MAX_MARKETPLACE_FILE_BYTES) {
104
+ return Object.create(null);
105
+ }
106
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
107
+ const opened = fstatSync(descriptor);
108
+ if (!opened.isFile() || opened.size > MAX_MARKETPLACE_FILE_BYTES) {
109
+ return Object.create(null);
22
110
  }
111
+ return parseKnownMarketplaces(readFileSync(descriptor, "utf8"));
23
112
  }
24
113
  catch {
25
- // Corrupted file: treat as empty so the caller can recover by re-adding.
114
+ return Object.create(null);
26
115
  }
27
- return {};
116
+ finally {
117
+ if (descriptor !== undefined)
118
+ closeSync(descriptor);
119
+ }
120
+ }
121
+ function mutateKnownMarketplaces(change) {
122
+ return mutateJsonFile(knownMarketplacesPath(), {
123
+ parse: parseKnownMarketplaces,
124
+ serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
125
+ mutation: change,
126
+ mode: 0o600,
127
+ maxBytes: MAX_MARKETPLACE_FILE_BYTES,
128
+ });
28
129
  }
29
130
  export function writeKnownMarketplaces(data) {
30
- const path = knownMarketplacesPath();
31
- mkdirSync(dirname(path), { recursive: true });
32
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
131
+ const validated = parseKnownMarketplaces(JSON.stringify(data));
132
+ mutateKnownMarketplaces(() => ({ value: validated }));
33
133
  }
34
134
  export function upsertKnownMarketplace(name, entry) {
35
- const data = readKnownMarketplaces();
36
- data[name] = entry;
37
- writeKnownMarketplaces(data);
135
+ const marketplace = marketplaceOf(entry);
136
+ if (!NAME_RE.test(name) || !marketplace)
137
+ throw new Error("invalid known marketplace");
138
+ mutateKnownMarketplaces((current) => ({ value: { ...current, [name]: marketplace } }));
38
139
  }
39
140
  export function removeKnownMarketplace(name) {
40
- const data = readKnownMarketplaces();
41
- if (!(name in data))
141
+ if (!NAME_RE.test(name))
42
142
  return false;
43
- delete data[name];
44
- writeKnownMarketplaces(data);
45
- return true;
143
+ return (mutateKnownMarketplaces((current) => {
144
+ if (!Object.prototype.hasOwnProperty.call(current, name))
145
+ return { result: false };
146
+ const next = { ...current };
147
+ delete next[name];
148
+ return { value: next, result: true };
149
+ }) ?? false);
46
150
  }
@@ -1,10 +1,11 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { readInstalledPlugins } from "./installedPlugins.js";
5
5
  import { pluginsRoot } from "./installer/paths.js";
6
6
  import { CANONICAL_PLUGIN_MANIFEST_FILE, CanonicalPluginManifest, } from "./installer/types.js";
7
7
  import { resolveSafePluginPath } from "./pluginInstaller.js";
8
+ const MAX_CANONICAL_MANIFEST_BYTES = 4 * 1024 * 1024;
8
9
  /**
9
10
  * Stable content revision used to bind a user review to the exact template
10
11
  * instantiated later. Install paths and timestamps are intentionally excluded.
@@ -20,14 +21,27 @@ export function pluginAutomationTemplateRevision(installKey, template) {
20
21
  }
21
22
  function readCanonicalManifest(installPath) {
22
23
  const file = join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE);
23
- if (!existsSync(file))
24
- return null;
24
+ let descriptor;
25
25
  try {
26
- return CanonicalPluginManifest.parse(JSON.parse(readFileSync(file, "utf-8")));
26
+ const metadata = lstatSync(file);
27
+ if (metadata.isSymbolicLink() ||
28
+ !metadata.isFile() ||
29
+ metadata.size > MAX_CANONICAL_MANIFEST_BYTES) {
30
+ return null;
31
+ }
32
+ descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
33
+ const opened = fstatSync(descriptor);
34
+ if (!opened.isFile() || opened.size > MAX_CANONICAL_MANIFEST_BYTES)
35
+ return null;
36
+ return CanonicalPluginManifest.parse(JSON.parse(readFileSync(descriptor, "utf-8")));
27
37
  }
28
38
  catch {
29
39
  return null;
30
40
  }
41
+ finally {
42
+ if (descriptor !== undefined)
43
+ closeSync(descriptor);
44
+ }
31
45
  }
32
46
  function identityFromInstallKey(installKey, manifest) {
33
47
  const at = installKey.lastIndexOf("@");