@epoch-agent/core 0.21.0 → 0.22.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/dist/index.js CHANGED
@@ -6296,6 +6296,7 @@ var ManifestSchema = z.object({
6296
6296
  epochVersion: z.string().min(1).optional(),
6297
6297
  homepage: z.string().min(1).optional(),
6298
6298
  keywords: z.array(z.string().min(1)).optional(),
6299
+ dependencies: z.array(z.string().min(1)).optional().catch(void 0),
6299
6300
  displayName: z.string().min(1).optional(),
6300
6301
  category: z.string().min(1).optional(),
6301
6302
  icon: z.string().min(1).optional(),
@@ -6338,6 +6339,23 @@ function readPluginManifest(dir) {
6338
6339
  })
6339
6340
  });
6340
6341
  }
6342
+ const rawDependencies = parsed?.dependencies;
6343
+ if (rawDependencies !== void 0 && !Array.isArray(rawDependencies)) {
6344
+ issues.push({
6345
+ path: `${path}.dependencies`,
6346
+ message: t("plugin_manifest.bad_dependencies", { raw: JSON.stringify(rawDependencies) })
6347
+ });
6348
+ }
6349
+ if (Array.isArray(rawDependencies)) {
6350
+ for (const item of rawDependencies) {
6351
+ if (typeof item !== "string" || !isValidPluginName(item)) {
6352
+ issues.push({
6353
+ path: `${path}.dependencies`,
6354
+ message: t("plugin_manifest.bad_dependency_name", { name: String(item) })
6355
+ });
6356
+ }
6357
+ }
6358
+ }
6341
6359
  const checked = parseLenient(ManifestSchema.optional(), parsed, void 0);
6342
6360
  if (!checked.value) {
6343
6361
  issues.push(
@@ -6639,6 +6657,7 @@ function loadPlugins(opts) {
6639
6657
  const state = readPluginRecords(statePath);
6640
6658
  const out = noPlugins();
6641
6659
  out.issues.push(...state.issues);
6660
+ const declared = /* @__PURE__ */ new Map();
6642
6661
  const epochVersion = opts.epochVersion ?? currentEpochVersion();
6643
6662
  for (const record of state.records) {
6644
6663
  const outcome = admit(record, epochVersion);
@@ -6657,6 +6676,21 @@ function loadPlugins(opts) {
6657
6676
  continue;
6658
6677
  }
6659
6678
  collect(record, out);
6679
+ if (outcome.manifest?.dependencies?.length) {
6680
+ declared.set(record.name, outcome.manifest.dependencies);
6681
+ }
6682
+ }
6683
+ const present = new Set(out.loaded.map((plugin) => plugin.name));
6684
+ for (const [name, dependencies] of declared) {
6685
+ if (!present.has(name)) continue;
6686
+ for (const dependency of dependencies) {
6687
+ if (present.has(dependency)) continue;
6688
+ diags?.warn(
6689
+ MODULE,
6690
+ t("plugin_load.dependency_missing", { name, dependency }),
6691
+ "plugin-dependency-missing"
6692
+ );
6693
+ }
6660
6694
  }
6661
6695
  for (const detail of issueDetails(out.issues)) {
6662
6696
  diags?.warn(MODULE, detail, "plugin-invalid");
@@ -6683,7 +6717,7 @@ function admit(record, epochVersion) {
6683
6717
  if (!version.ok) {
6684
6718
  return { reason: version.reason, code: "plugin-version-mismatch" };
6685
6719
  }
6686
- return {};
6720
+ return { manifest: manifest.manifest };
6687
6721
  }
6688
6722
  function collect(record, out) {
6689
6723
  const { name, path: dir } = record;
@@ -7029,708 +7063,874 @@ function firstLine3(err) {
7029
7063
  const message = err instanceof Error ? err.message : String(err);
7030
7064
  return message.split("\n")[0] ?? message;
7031
7065
  }
7032
- var NPM_SPEC = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9.^~*+-]+)?$/;
7033
- var REMOTE_URL = /^https?:\/\/[^\s'"\\^|<>]+$/;
7034
- function resolveSource(raw) {
7035
- const input = raw.trim();
7036
- if (!input) return void 0;
7037
- if (input.startsWith("github:")) {
7038
- const [slug = "", fragment] = input.slice("github:".length).split("#", 2);
7039
- if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) return void 0;
7040
- const sub = subdirOf(fragment);
7041
- if (!sub.ok) return void 0;
7042
- return {
7043
- type: "git",
7044
- raw: input,
7045
- target: `https://github.com/${slug}.git`,
7046
- ...sub.subdir ? { subdir: sub.subdir } : {}
7047
- };
7048
- }
7049
- if (input.startsWith("git+")) {
7050
- const [url = "", fragment] = input.slice("git+".length).split("#", 2);
7051
- if (!REMOTE_URL.test(url)) return void 0;
7052
- const sub = subdirOf(fragment);
7053
- if (!sub.ok) return void 0;
7054
- return { type: "git", raw: input, target: url, ...sub.subdir ? { subdir: sub.subdir } : {} };
7055
- }
7056
- if (input.startsWith("npm:")) {
7057
- const pkg = input.slice("npm:".length);
7058
- if (!NPM_SPEC.test(pkg)) return void 0;
7059
- return { type: "npm", raw: input, target: pkg };
7060
- }
7061
- if (input.startsWith("host:")) {
7062
- const opaque = input.slice("host:".length).trim();
7063
- if (!opaque) return void 0;
7064
- return { type: "host", raw: input, target: opaque };
7065
- }
7066
- if (input.startsWith("https://") || input.startsWith("http://")) {
7067
- const [url = "", fragment] = input.split("#", 2);
7068
- if (!REMOTE_URL.test(url)) return void 0;
7069
- const sha = fragment?.startsWith("sha256=") ? fragment.slice("sha256=".length) : void 0;
7070
- if (isArchiveUrl(url)) {
7071
- if (sha !== void 0 && !/^[a-f0-9]{64}$/i.test(sha)) return void 0;
7072
- return {
7073
- type: "archive",
7074
- raw: input,
7075
- target: url,
7076
- ...sha ? { sha256: sha.toLowerCase() } : {}
7077
- };
7078
- }
7079
- return { type: "git", raw: input, target: url };
7080
- }
7081
- const abs = isAbsolute(input) ? input : resolve(input);
7082
- if (isArchiveUrl(abs)) {
7083
- return { type: "archive", raw: input, target: abs, localFile: true };
7084
- }
7085
- return { type: "local", raw: input, target: abs };
7086
- }
7087
- function isArchiveUrl(url) {
7088
- return isWirePluginArchiveName(url);
7066
+ var MARKETPLACE_FILE = "epoch-marketplace.json";
7067
+ var NAME_PATTERN = PLUGIN_NAME_PATTERN;
7068
+ var EntryCategoryInput = z.union([z.string().min(1), z.array(z.string().min(1))]).optional().catch(void 0);
7069
+ var EntryShape = z.object({
7070
+ name: z.string().regex(NAME_PATTERN),
7071
+ description: z.string().min(1).optional(),
7072
+ source: z.string().min(1),
7073
+ category: EntryCategoryInput,
7074
+ keywords: z.array(z.string().min(1)).optional(),
7075
+ author: PluginAuthorSchema.optional(),
7076
+ downloads: z.number().int().nonnegative().optional(),
7077
+ likes: z.number().int().nonnegative().optional(),
7078
+ displayName: z.string().min(1).optional(),
7079
+ icon: z.string().min(1).optional(),
7080
+ version: z.string().min(1).optional(),
7081
+ changelog: z.string().min(1).optional(),
7082
+ size: z.number().int().positive().optional(),
7083
+ homepage: z.string().min(1).optional(),
7084
+ visibility: z.enum(PLUGIN_VISIBILITY).optional().catch(void 0),
7085
+ status: z.enum(["listed", "reviewing", "rejected", "offline"]).optional().catch(void 0)
7086
+ });
7087
+ var EntrySchema = EntryShape.transform(({ category, ...rest }) => {
7088
+ const picked = category === void 0 ? [] : typeof category === "string" ? [category] : category;
7089
+ return { ...rest, ...picked.length === 0 ? {} : { category: [...picked] } };
7090
+ });
7091
+ var MARKETPLACE_DISPLAY_FIELDS = [
7092
+ "status",
7093
+ "downloads",
7094
+ "likes",
7095
+ "author",
7096
+ "category",
7097
+ "visibility",
7098
+ "icon",
7099
+ "homepage",
7100
+ "size"
7101
+ ];
7102
+ var DisplaySchema = z.object({
7103
+ fields: z.array(z.string().min(1)).optional()
7104
+ });
7105
+ var CategoryNodeSchema = z.lazy(
7106
+ () => z.object({
7107
+ id: z.string().min(1),
7108
+ label: z.string().min(1).optional(),
7109
+ children: z.array(CategoryNodeSchema).optional()
7110
+ })
7111
+ );
7112
+ var CategoriesSchema = z.array(CategoryNodeSchema).refine(hasUniqueCategoryIds, { message: "category ids must be unique across the whole tree" }).optional().catch(void 0);
7113
+ function hasUniqueCategoryIds(nodes) {
7114
+ const seen = /* @__PURE__ */ new Set();
7115
+ const walk3 = (list) => list.every((node) => {
7116
+ if (seen.has(node.id)) return false;
7117
+ seen.add(node.id);
7118
+ return node.children === void 0 || walk3(node.children);
7119
+ });
7120
+ return walk3(nodes);
7089
7121
  }
7090
- function subdirOf(fragment) {
7091
- if (fragment === void 0 || fragment === "") return { ok: true };
7092
- if (!fragment.startsWith("subdir=")) return { ok: false };
7093
- const rel = fragment.slice("subdir=".length).replace(/\/+$/, "");
7094
- if (!/^[\w.-]+(?:\/[\w.-]+)*$/.test(rel)) return { ok: false };
7095
- if (rel.split("/").includes("..")) return { ok: false };
7096
- return { ok: true, subdir: rel };
7122
+ var CatalogSchema = z.object({
7123
+ name: z.string().regex(NAME_PATTERN),
7124
+ description: z.string().min(1).optional(),
7125
+ owner: z.object({ name: z.string().min(1), url: z.string().min(1).optional() }).optional(),
7126
+ display: DisplaySchema.optional(),
7127
+ categories: CategoriesSchema,
7128
+ plugins: z.array(EntrySchema)
7129
+ });
7130
+ var RecordSchema2 = z.object({
7131
+ name: z.string().regex(NAME_PATTERN),
7132
+ source: z.string().min(1),
7133
+ addedAt: z.number(),
7134
+ catalog: CatalogSchema
7135
+ });
7136
+ var StateSchema2 = z.object({
7137
+ version: z.literal(1).optional(),
7138
+ marketplaces: z.array(RecordSchema2)
7139
+ });
7140
+ function projectEntryForDisplay(entry, display) {
7141
+ const allowed = display?.fields;
7142
+ if (allowed === void 0) return entry;
7143
+ const out = { ...entry };
7144
+ for (const field of MARKETPLACE_DISPLAY_FIELDS) {
7145
+ if (!displayAllows(display, field)) delete out[field];
7146
+ }
7147
+ return out;
7097
7148
  }
7098
- function pluginRoot(source, staging) {
7099
- return source.subdir ? join(staging, source.subdir) : staging;
7149
+ function displayAllows(display, field) {
7150
+ const allowed = display?.fields;
7151
+ if (allowed === void 0) return true;
7152
+ return allowed.includes(field);
7100
7153
  }
7101
- // src/plugin/install.ts
7102
- async function installPlugin(source, opts) {
7103
- const resolved = resolveSource(source);
7104
- if (!resolved) {
7154
+ function readMarketplaces(statePath) {
7155
+ if (!existsSync(statePath)) return { records: [], issues: [] };
7156
+ let raw;
7157
+ try {
7158
+ raw = JSON.parse(readFileSync(statePath, "utf-8"));
7159
+ } catch (err) {
7105
7160
  return {
7106
- ok: false,
7107
- reason: t("plugin_install.bad_source", { source })
7161
+ records: [],
7162
+ issues: [
7163
+ { path: statePath, message: t("plugin_marketplace.bad_json", { reason: first(err) }) }
7164
+ ]
7108
7165
  };
7109
7166
  }
7110
- mkdirSync(opts.pluginsDir, { recursive: true });
7111
- const staging = join(opts.pluginsDir, `.staging-${randomUUID().slice(0, 8)}`);
7167
+ const checked = parseLenient(StateSchema2.optional(), raw, void 0);
7168
+ if (!checked.value) {
7169
+ return {
7170
+ records: [],
7171
+ issues: checked.issues.map((i) => ({
7172
+ path: statePath,
7173
+ message: t("plugin_marketplace.issue", { path: i.path, message: i.message })
7174
+ }))
7175
+ };
7176
+ }
7177
+ return { records: checked.value.marketplaces, issues: [] };
7178
+ }
7179
+ async function mutate(statePath, fn) {
7180
+ const release = await acquireStateLock(statePath);
7112
7181
  try {
7113
- return await runInstall(resolved, staging, opts);
7182
+ const next = fn(readMarketplaces(statePath).records);
7183
+ if (!next) return false;
7184
+ writeStateFile(statePath, JSON.stringify({ version: 1, marketplaces: next }, null, 2));
7185
+ return true;
7114
7186
  } finally {
7115
- rmSync(staging, { recursive: true, force: true });
7187
+ release();
7116
7188
  }
7117
7189
  }
7118
- async function runInstall(resolved, staging, opts) {
7119
- const fetched = await fetchInto(resolved, staging, opts);
7120
- if (!fetched.ok) return { ok: false, reason: fetched.reason };
7121
- const readDir = resolved.type === "local" ? resolved.target : pluginRoot(resolved, staging);
7122
- const read = readPluginManifest(readDir);
7123
- const manifest = read.manifest;
7124
- if (!manifest) {
7125
- const detail = read.issues.map((i) => t("plugin_install.issue_line", { path: i.path, message: i.message })).join("\n");
7126
- return { ok: false, reason: t("plugin_install.manifest_unusable", { detail: `
7127
- ${detail}` }) };
7128
- }
7129
- const taken = new PluginStore(opts.statePath).read().records.find((r) => r.name === manifest.name);
7130
- const conflict = taken && taken.name !== opts.replace ? taken : void 0;
7131
- const current = opts.epochVersion ?? currentEpochVersion();
7132
- const version = checkEpochVersion(manifest.epochVersion, current);
7133
- const versionUnmet = !version.ok && manifest.epochVersion ? { required: manifest.epochVersion, current, reason: version.reason } : void 0;
7134
- const preview3 = {
7135
- manifest,
7136
- source: resolved,
7137
- inventory: scanPluginDir(readDir, manifest.name),
7138
- ...conflict ? { conflict } : {},
7139
- ...versionUnmet ? { versionUnmet } : {}
7190
+ async function addMarketplace(source, opts) {
7191
+ const fetched = await fetchCatalog(source);
7192
+ if (!fetched.ok) return fetched;
7193
+ const record = {
7194
+ name: fetched.catalog.name,
7195
+ source: source.trim(),
7196
+ addedAt: Date.now(),
7197
+ catalog: fetched.catalog
7140
7198
  };
7141
- if (opts.replace !== void 0 && manifest.name !== opts.replace) {
7199
+ const written = await mutate(
7200
+ opts.statePath,
7201
+ (records) => records.some((r) => r.name === record.name) ? void 0 : [...records, record].sort((a, b) => a.name.localeCompare(b.name))
7202
+ );
7203
+ return written ? { ok: true, record } : {
7204
+ ok: false,
7205
+ reason: t("plugin_marketplace.already_added", { name: record.name })
7206
+ };
7207
+ }
7208
+ async function removeMarketplace(name, opts) {
7209
+ const written = await mutate(
7210
+ opts.statePath,
7211
+ (records) => records.some((r) => r.name === name) ? records.filter((r) => r.name !== name) : void 0
7212
+ );
7213
+ return written ? { ok: true } : { ok: false, reason: t("plugin_marketplace.not_added", { name }) };
7214
+ }
7215
+ async function updateMarketplace(name, opts) {
7216
+ const existing = readMarketplaces(opts.statePath).records.find((r) => r.name === name);
7217
+ if (!existing) return { ok: false, reason: t("plugin_marketplace.not_added", { name }) };
7218
+ const fetched = await fetchCatalog(existing.source);
7219
+ if (!fetched.ok) return fetched;
7220
+ if (fetched.catalog.name !== name) {
7142
7221
  return {
7143
7222
  ok: false,
7144
- preview: preview3,
7145
- reason: t("plugin_install.renamed", { found: manifest.name, expected: opts.replace })
7223
+ reason: t("plugin_marketplace.renamed", {
7224
+ source: existing.source,
7225
+ name: fetched.catalog.name
7226
+ })
7146
7227
  };
7147
7228
  }
7148
- if (conflict) {
7229
+ const record = { ...existing, catalog: fetched.catalog };
7230
+ await mutate(opts.statePath, (records) => records.map((r) => r.name === name ? record : r));
7231
+ return { ok: true, record };
7232
+ }
7233
+ function searchMarketplaces(keyword, statePath) {
7234
+ const needle = keyword.trim().toLowerCase();
7235
+ const hits = [];
7236
+ for (const record of readMarketplaces(statePath).records) {
7237
+ for (const entry of record.catalog.plugins) {
7238
+ const haystack = [
7239
+ entry.name,
7240
+ entry.description ?? "",
7241
+ ...entry.category ?? [],
7242
+ entry.author?.name ?? "",
7243
+ ...entry.keywords ?? []
7244
+ ].join(" ").toLowerCase();
7245
+ if (needle && !haystack.includes(needle)) continue;
7246
+ hits.push({
7247
+ marketplace: record.name,
7248
+ entry: projectEntryForDisplay(entry, record.catalog.display),
7249
+ ref: `${record.name}/${entry.name}`
7250
+ });
7251
+ }
7252
+ }
7253
+ return hits;
7254
+ }
7255
+ function resolveMarketplaceRef(input, statePath) {
7256
+ const parts = input.trim().split("/");
7257
+ if (parts.length !== 2) return void 0;
7258
+ const [market = "", plugin = ""] = parts;
7259
+ if (!NAME_PATTERN.test(market) || !NAME_PATTERN.test(plugin)) return void 0;
7260
+ const record = readMarketplaces(statePath).records.find((r) => r.name === market);
7261
+ if (!record) {
7149
7262
  return {
7150
7263
  ok: false,
7151
- preview: preview3,
7152
- reason: t("plugin_install.already_installed", {
7153
- name: conflict.name,
7154
- version: conflict.version,
7155
- source: conflict.source
7156
- })
7264
+ reason: t("plugin_marketplace.market_not_added", { market, input })
7157
7265
  };
7158
7266
  }
7159
- if (opts.confirm && !await opts.confirm(preview3)) {
7160
- return { ok: false, reason: t("plugin.install_cancelled"), cancelled: true, preview: preview3 };
7161
- }
7162
- const record = await commit(resolved, staging, preview3, opts);
7163
- return record ? { ok: true, record, preview: preview3 } : {
7164
- ok: false,
7165
- preview: preview3,
7166
- reason: t("plugin_install.raced", { name: manifest.name })
7167
- };
7168
- }
7169
- async function commit(source, staging, preview3, opts) {
7170
- const name = preview3.manifest.name;
7171
- const dest = join(opts.pluginsDir, name);
7172
- removeInstallDest(dest);
7173
- const linked = source.type === "local";
7174
- if (linked) {
7175
- try {
7176
- symlinkSync(source.target, dest, "junction");
7177
- } catch {
7178
- opts.onProgress?.(t("plugin_install.symlink_failed"));
7179
- cpSync(source.target, dest, { recursive: true });
7180
- return write(false);
7181
- }
7182
- } else {
7183
- renameSync(pluginRoot(source, staging), dest);
7267
+ const entry = record.catalog.plugins.find((p) => p.name === plugin);
7268
+ if (!entry) {
7269
+ return {
7270
+ ok: false,
7271
+ reason: t("plugin_marketplace.plugin_not_in_market", { market, plugin })
7272
+ };
7184
7273
  }
7185
- return write(linked);
7186
- function write(isLink) {
7187
- const record = {
7188
- name,
7189
- version: preview3.manifest.version,
7190
- source: source.raw,
7191
- sourceType: source.type,
7192
- path: dest,
7193
- linked: isLink,
7194
- enabled: true,
7195
- installedAt: Date.now(),
7196
- ...opts.marketplace ? { marketplace: opts.marketplace } : {},
7197
- ...source.sha256 ? { checksum: `sha256:${source.sha256}` } : {}
7274
+ const source = resolveEntrySource(entry.source, record.source);
7275
+ if (!source) {
7276
+ return {
7277
+ ok: false,
7278
+ reason: t("plugin_marketplace.entry_unresolvable", {
7279
+ market,
7280
+ plugin,
7281
+ source: entry.source
7282
+ })
7198
7283
  };
7199
- return new PluginStore(opts.statePath).mutate((records) => {
7200
- const taken = records.some((r) => r.name === name);
7201
- if (taken && opts.replace !== name) return void 0;
7202
- return {
7203
- records: taken ? records.map((r) => r.name === name ? record : r) : [...records, record],
7204
- result: record
7205
- };
7206
- });
7207
7284
  }
7285
+ return { ok: true, ref: { marketplace: market, source } };
7208
7286
  }
7209
- function removeInstallDest(dest) {
7210
- let link = false;
7211
- try {
7212
- link = lstatSync(dest).isSymbolicLink();
7213
- } catch {
7214
- }
7215
- if (!link) {
7216
- rmSync(dest, { recursive: true, force: true });
7217
- return;
7287
+ function resolveEntrySource(entry, marketSource) {
7288
+ const spec = entry.trim();
7289
+ if (!spec.startsWith(".") && !spec.startsWith("/")) return spec;
7290
+ const rel = spec.replace(/^\.\//, "").replace(/^\//, "");
7291
+ if (rel === "" || rel.split("/").includes("..")) return void 0;
7292
+ const market = marketSource.trim();
7293
+ if (market.startsWith("github:") || market.startsWith("git+")) {
7294
+ const [base = "", fragment] = market.split("#", 2);
7295
+ const prefix = fragment?.startsWith("subdir=") ? `${fragment.slice("subdir=".length)}/` : "";
7296
+ return `${base}#subdir=${prefix}${rel}`;
7218
7297
  }
7219
- try {
7220
- unlinkSync(dest);
7221
- } catch {
7222
- rmdirSync(dest);
7298
+ if (market.startsWith("https://") || market.startsWith("http://")) {
7299
+ return `${market.replace(/\/[^/]*$/, "")}/${rel}`;
7223
7300
  }
7301
+ return join(marketDir(market), rel);
7224
7302
  }
7225
- async function fetchInto(source, staging, opts) {
7226
- if (source.type === "local") return fetchLocal(source);
7227
- if (source.type === "git") return fetchGit(source, staging, opts);
7228
- if (source.type === "npm") return fetchNpm(source, staging, opts);
7229
- if (source.type === "host") return fetchFromHost(source, staging, opts);
7230
- if (source.localFile) return fetchLocalArchive(source, staging);
7231
- return fetchArchive(source, staging, opts);
7232
- }
7233
- async function fetchLocalArchive(source, staging) {
7234
- if (!existsSync(source.target)) {
7235
- return { ok: false, reason: t("plugin_install.archive_missing", { path: source.target }) };
7303
+ function findMarketplaceEntryByName(name, statePath) {
7304
+ for (const record of readMarketplaces(statePath).records) {
7305
+ const entry = record.catalog.plugins.find((plugin) => plugin.name === name);
7306
+ if (!entry) continue;
7307
+ const source = resolveEntrySource(entry.source, record.source);
7308
+ if (!source) continue;
7309
+ return { marketplace: record.name, source };
7236
7310
  }
7237
- mkdirSync(staging, { recursive: true });
7238
- return extractArchive(source.target, staging);
7311
+ return void 0;
7239
7312
  }
7240
- async function fetchFromHost(source, staging, opts) {
7241
- const ask = opts.fetchFromHost;
7242
- if (!ask) {
7243
- return { ok: false, reason: t("plugin_install.host_fetch_unavailable") };
7244
- }
7245
- opts.onProgress?.(t("plugin_install.host_fetching", { target: source.target }));
7246
- mkdirSync(staging, { recursive: true });
7247
- return withDownloadDir(async (dl) => {
7248
- let answer;
7313
+ async function fetchCatalog(source) {
7314
+ const input = source.trim();
7315
+ if (!input) return { ok: false, reason: t("plugin_marketplace.source_empty") };
7316
+ const local = localCatalogPath(input);
7317
+ if (local) {
7318
+ if (!existsSync(local)) {
7319
+ return { ok: false, reason: t("plugin_marketplace.local_missing", { path: local }) };
7320
+ }
7249
7321
  try {
7250
- answer = await ask({ target: source.target, destDir: dl });
7322
+ return parseCatalog(readFileSync(local, "utf-8"), local);
7251
7323
  } catch (err) {
7252
- return {
7253
- ok: false,
7254
- reason: t("plugin_install.host_fetch_threw", {
7255
- reason: (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? ""
7256
- })
7257
- };
7258
- }
7259
- if (!answer.ok) {
7260
- return {
7261
- ok: false,
7262
- reason: answer.reason ?? t("plugin_install.host_fetch_failed", { target: source.target })
7263
- };
7324
+ return { ok: false, reason: t("plugin_marketplace.read_failed", { reason: first(err) }) };
7264
7325
  }
7265
- if (!existsSync(answer.archivePath)) {
7326
+ }
7327
+ const url = catalogUrl(input);
7328
+ if (!url) {
7329
+ return {
7330
+ ok: false,
7331
+ reason: t("plugin_marketplace.bad_url", { input, file: MARKETPLACE_FILE })
7332
+ };
7333
+ }
7334
+ try {
7335
+ const response = await fetch(url, { redirect: "follow" });
7336
+ if (!response.ok) {
7337
+ const vars = { url, status: response.status };
7266
7338
  return {
7267
7339
  ok: false,
7268
- reason: t("plugin_install.host_fetch_no_file", { path: answer.archivePath })
7340
+ reason: looksPrivate(response.status) ? t("marketplace.fetch_failed_private", vars) : t("marketplace.fetch_failed", vars)
7269
7341
  };
7270
7342
  }
7271
- return extractArchive(answer.archivePath, staging);
7272
- });
7273
- }
7274
- function fetchLocal(source) {
7275
- if (!existsSync(source.target)) {
7276
- return { ok: false, reason: t("plugin_install.local_missing", { path: source.target }) };
7343
+ return parseCatalog(await response.text(), url);
7344
+ } catch (err) {
7345
+ return { ok: false, reason: t("marketplace.fetch_error", { url, reason: first(err) }) };
7277
7346
  }
7278
- return { ok: true };
7279
7347
  }
7280
- async function fetchGit(source, staging, opts) {
7281
- opts.onProgress?.(t("plugin_install.cloning", { target: source.target }));
7282
- const result = await runCommand("git", ["clone", "--depth", "1", source.target, staging]);
7283
- if (!result.ok) {
7284
- return { ok: false, reason: t("plugin_install.clone_failed", { detail: result.detail }) };
7348
+ function looksPrivate(status) {
7349
+ return status === 401 || status === 403 || status === 404;
7350
+ }
7351
+ function catalogUrl(input) {
7352
+ if (input.startsWith("github:")) {
7353
+ const slug = input.slice("github:".length);
7354
+ if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) return void 0;
7355
+ return `https://raw.githubusercontent.com/${slug}/HEAD/${MARKETPLACE_FILE}`;
7285
7356
  }
7286
- rmSync(join(staging, ".git"), { recursive: true, force: true });
7287
- return { ok: true };
7357
+ if (!input.startsWith("https://") && !input.startsWith("http://")) return void 0;
7358
+ return input.endsWith(".json") ? input : `${input.replace(/\/+$/, "")}/${MARKETPLACE_FILE}`;
7288
7359
  }
7289
- async function fetchNpm(source, staging, opts) {
7290
- opts.onProgress?.(t("plugin_install.npm_fetching", { target: source.target }));
7291
- mkdirSync(staging, { recursive: true });
7292
- return withDownloadDir(async (dl) => {
7293
- const packed = await runCommand(
7294
- "npm",
7295
- ["pack", source.target, "--pack-destination", dl, "--silent"],
7296
- { cwd: dl, windowsShell: true }
7297
- );
7298
- if (!packed.ok) {
7299
- return { ok: false, reason: t("plugin_install.npm_pack_failed", { detail: packed.detail }) };
7300
- }
7301
- const tarball = packed.stdout.trim().split("\n").pop()?.trim();
7302
- if (!tarball) return { ok: false, reason: t("plugin_install.npm_no_tarball") };
7303
- return extractArchive(isAbsolute(tarball) ? tarball : join(dl, tarball), staging);
7304
- });
7360
+ function localCatalogPath(input) {
7361
+ if (/^[a-z][a-z0-9+.-]+:/i.test(input) || input.startsWith("//")) return void 0;
7362
+ const abs = isAbsolute(input) ? input : resolve(input);
7363
+ if (existsSync(abs) && statSync(abs).isDirectory()) return join(abs, MARKETPLACE_FILE);
7364
+ return abs;
7305
7365
  }
7306
- async function fetchArchive(source, staging, opts) {
7307
- opts.onProgress?.(t("plugin_install.downloading", { target: source.target }));
7308
- mkdirSync(staging, { recursive: true });
7309
- let bytes;
7366
+ function marketDir(market) {
7367
+ const abs = isAbsolute(market) ? market : resolve(market);
7368
+ if (existsSync(abs) && statSync(abs).isDirectory()) return abs;
7369
+ return abs.replace(/[/\\][^/\\]*$/, "");
7370
+ }
7371
+ function parseCatalog(body2, where) {
7372
+ let raw;
7310
7373
  try {
7311
- const response = await fetch(source.target, { redirect: "follow" });
7312
- if (!response.ok) {
7313
- return {
7314
- ok: false,
7315
- reason: t("plugin_install.download_http", {
7316
- status: response.status,
7317
- statusText: response.statusText
7318
- })
7319
- };
7320
- }
7321
- bytes = Buffer.from(await response.arrayBuffer());
7374
+ raw = JSON.parse(body2);
7322
7375
  } catch (err) {
7323
7376
  return {
7324
7377
  ok: false,
7325
- reason: t("plugin_install.download_failed", {
7326
- message: err instanceof Error ? err.message : String(err)
7327
- })
7378
+ reason: t("plugin_marketplace.catalog_bad_json", { where, reason: first(err) })
7328
7379
  };
7329
7380
  }
7330
- if (source.sha256) {
7331
- const actual = createHash("sha256").update(bytes).digest("hex");
7332
- if (actual !== source.sha256) {
7333
- return {
7334
- ok: false,
7335
- reason: [
7336
- t("plugin_install.sha_mismatch"),
7337
- t("plugin_install.sha_expected", { expected: source.sha256 }),
7338
- t("plugin_install.sha_actual", { actual })
7339
- ].join("\n")
7340
- };
7341
- }
7381
+ const checked = parseLenient(CatalogSchema.optional(), raw, void 0);
7382
+ if (!checked.value) {
7383
+ const detail = checked.issues.map((i) => t("plugin_marketplace.issue", { path: i.path, message: i.message })).join(t("plugin_marketplace.catalog_issue_sep"));
7384
+ return { ok: false, reason: t("plugin_marketplace.catalog_invalid", { where, detail }) };
7342
7385
  }
7343
- return withDownloadDir(async (dl) => {
7344
- const archive = join(dl, `download${archiveExt(source.target)}`);
7345
- writeFileSync(archive, bytes);
7346
- return extractArchive(archive, staging);
7347
- });
7386
+ return { ok: true, catalog: checked.value };
7348
7387
  }
7349
- function archiveExt(url) {
7350
- const path = (url.split("?")[0] ?? "").toLowerCase();
7351
- if (path.endsWith(".tgz")) return ".tgz";
7352
- if (path.endsWith(".tar.gz")) return ".tar.gz";
7353
- return ".zip";
7388
+ function first(err) {
7389
+ const message = err instanceof Error ? err.message : String(err);
7390
+ return message.split("\n")[0] ?? t("plugin_marketplace.unknown_error");
7354
7391
  }
7355
- async function withDownloadDir(fn) {
7356
- const dir = mkdtempSync(join(tmpdir(), "epoch-plugin-dl-"));
7357
- try {
7358
- return await fn(dir);
7359
- } finally {
7360
- rmSync(dir, { recursive: true, force: true });
7392
+ var NPM_SPEC = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9.^~*+-]+)?$/;
7393
+ var REMOTE_URL = /^https?:\/\/[^\s'"\\^|<>]+$/;
7394
+ function resolveSource(raw) {
7395
+ const input = raw.trim();
7396
+ if (!input) return void 0;
7397
+ if (input.startsWith("github:")) {
7398
+ const [slug = "", fragment] = input.slice("github:".length).split("#", 2);
7399
+ if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) return void 0;
7400
+ const sub = subdirOf(fragment);
7401
+ if (!sub.ok) return void 0;
7402
+ return {
7403
+ type: "git",
7404
+ raw: input,
7405
+ target: `https://github.com/${slug}.git`,
7406
+ ...sub.subdir ? { subdir: sub.subdir } : {}
7407
+ };
7408
+ }
7409
+ if (input.startsWith("git+")) {
7410
+ const [url = "", fragment] = input.slice("git+".length).split("#", 2);
7411
+ if (!REMOTE_URL.test(url)) return void 0;
7412
+ const sub = subdirOf(fragment);
7413
+ if (!sub.ok) return void 0;
7414
+ return { type: "git", raw: input, target: url, ...sub.subdir ? { subdir: sub.subdir } : {} };
7415
+ }
7416
+ if (input.startsWith("npm:")) {
7417
+ const pkg = input.slice("npm:".length);
7418
+ if (!NPM_SPEC.test(pkg)) return void 0;
7419
+ return { type: "npm", raw: input, target: pkg };
7420
+ }
7421
+ if (input.startsWith("host:")) {
7422
+ const opaque = input.slice("host:".length).trim();
7423
+ if (!opaque) return void 0;
7424
+ return { type: "host", raw: input, target: opaque };
7425
+ }
7426
+ if (input.startsWith("https://") || input.startsWith("http://")) {
7427
+ const [url = "", fragment] = input.split("#", 2);
7428
+ if (!REMOTE_URL.test(url)) return void 0;
7429
+ const sha = fragment?.startsWith("sha256=") ? fragment.slice("sha256=".length) : void 0;
7430
+ if (isArchiveUrl(url)) {
7431
+ if (sha !== void 0 && !/^[a-f0-9]{64}$/i.test(sha)) return void 0;
7432
+ return {
7433
+ type: "archive",
7434
+ raw: input,
7435
+ target: url,
7436
+ ...sha ? { sha256: sha.toLowerCase() } : {}
7437
+ };
7438
+ }
7439
+ return { type: "git", raw: input, target: url };
7440
+ }
7441
+ const abs = isAbsolute(input) ? input : resolve(input);
7442
+ if (isArchiveUrl(abs)) {
7443
+ return { type: "archive", raw: input, target: abs, localFile: true };
7361
7444
  }
7445
+ return { type: "local", raw: input, target: abs };
7362
7446
  }
7363
- // src/plugin/root-token.ts
7364
- var PLUGIN_ROOT_TOKEN = "${EPOCH_PLUGIN_ROOT}";
7365
- var PLUGIN_ROOT_ENV = "EPOCH_PLUGIN_ROOT";
7366
- function mentionsPluginRoot(text) {
7367
- return text.includes(PLUGIN_ROOT_TOKEN);
7447
+ function isArchiveUrl(url) {
7448
+ return isWirePluginArchiveName(url);
7368
7449
  }
7369
- function substitutePluginRoot(text, root) {
7370
- return text.split(PLUGIN_ROOT_TOKEN).join(root);
7450
+ function subdirOf(fragment) {
7451
+ if (fragment === void 0 || fragment === "") return { ok: true };
7452
+ if (!fragment.startsWith("subdir=")) return { ok: false };
7453
+ const rel = fragment.slice("subdir=".length).replace(/\/+$/, "");
7454
+ if (!/^[\w.-]+(?:\/[\w.-]+)*$/.test(rel)) return { ok: false };
7455
+ if (rel.split("/").includes("..")) return { ok: false };
7456
+ return { ok: true, subdir: rel };
7371
7457
  }
7372
- async function uninstallPlugin(name, opts) {
7373
- const removed = await new PluginStore(opts.statePath).mutate((records) => {
7374
- const hit = records.find((r) => r.name === name);
7375
- if (!hit) return void 0;
7376
- return { records: records.filter((r) => r.name !== name), result: hit };
7377
- });
7378
- if (!removed) return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7379
- if (removed.linked) {
7380
- rmSync(removed.path, { force: true });
7381
- return { ok: true, message: t("plugin_lifecycle.uninstalled_link", { name }) };
7382
- }
7383
- rmSync(removed.path, { recursive: true, force: true });
7384
- return { ok: true, message: t("plugin_lifecycle.uninstalled", { name }) };
7458
+ function pluginRoot(source, staging) {
7459
+ return source.subdir ? join(staging, source.subdir) : staging;
7385
7460
  }
7386
- async function setPluginEnabled(name, enabled, opts) {
7387
- const changed = await new PluginStore(opts.statePath).mutate((records) => {
7388
- const hit = records.find((r) => r.name === name);
7389
- if (!hit) return void 0;
7461
+ // src/plugin/install.ts
7462
+ async function installPlugin(source, opts) {
7463
+ const resolved = resolveSource(source);
7464
+ if (!resolved) {
7390
7465
  return {
7391
- records: records.map((r) => r.name === name ? { ...r, enabled } : r),
7392
- result: hit.enabled !== enabled
7466
+ ok: false,
7467
+ reason: t("plugin_install.bad_source", { source })
7393
7468
  };
7394
- });
7395
- if (changed === void 0) {
7396
- return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7397
7469
  }
7398
- const verb = enabled ? t("plugin_lifecycle.verb_enable") : t("plugin_lifecycle.verb_disable");
7399
- return {
7400
- ok: true,
7401
- message: changed ? t("plugin_lifecycle.toggled", { verb, name }) : t("plugin_lifecycle.already", { name, verb })
7402
- };
7403
- }
7404
- async function updatePlugin(name, opts) {
7405
- const existing = new PluginStore(opts.statePath).read().records.find((r) => r.name === name);
7406
- if (!existing) return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7407
- if (existing.linked) {
7408
- return {
7409
- ok: true,
7410
- message: t("plugin_lifecycle.linked_no_update", { name, source: existing.source })
7411
- };
7470
+ mkdirSync(opts.pluginsDir, { recursive: true });
7471
+ const staging = join(opts.pluginsDir, `.staging-${randomUUID().slice(0, 8)}`);
7472
+ try {
7473
+ return await runInstall(resolved, staging, opts);
7474
+ } finally {
7475
+ rmSync(staging, { recursive: true, force: true });
7412
7476
  }
7413
- return installPlugin(existing.source, {
7414
- ...opts,
7415
- replace: name,
7416
- ...existing.marketplace ? { marketplace: existing.marketplace } : {}
7417
- });
7418
- }
7419
- var MARKETPLACE_FILE = "epoch-marketplace.json";
7420
- var NAME_PATTERN = PLUGIN_NAME_PATTERN;
7421
- var EntryCategoryInput = z.union([z.string().min(1), z.array(z.string().min(1))]).optional().catch(void 0);
7422
- var EntryShape = z.object({
7423
- name: z.string().regex(NAME_PATTERN),
7424
- description: z.string().min(1).optional(),
7425
- source: z.string().min(1),
7426
- category: EntryCategoryInput,
7427
- keywords: z.array(z.string().min(1)).optional(),
7428
- author: PluginAuthorSchema.optional(),
7429
- downloads: z.number().int().nonnegative().optional(),
7430
- likes: z.number().int().nonnegative().optional(),
7431
- displayName: z.string().min(1).optional(),
7432
- icon: z.string().min(1).optional(),
7433
- version: z.string().min(1).optional(),
7434
- changelog: z.string().min(1).optional(),
7435
- size: z.number().int().positive().optional(),
7436
- homepage: z.string().min(1).optional(),
7437
- visibility: z.enum(PLUGIN_VISIBILITY).optional().catch(void 0),
7438
- status: z.enum(["listed", "reviewing", "rejected", "offline"]).optional().catch(void 0)
7439
- });
7440
- var EntrySchema = EntryShape.transform(({ category, ...rest }) => {
7441
- const picked = category === void 0 ? [] : typeof category === "string" ? [category] : category;
7442
- return { ...rest, ...picked.length === 0 ? {} : { category: [...picked] } };
7443
- });
7444
- var MARKETPLACE_DISPLAY_FIELDS = [
7445
- "status",
7446
- "downloads",
7447
- "likes",
7448
- "author",
7449
- "category",
7450
- "visibility",
7451
- "icon",
7452
- "homepage",
7453
- "size"
7454
- ];
7455
- var DisplaySchema = z.object({
7456
- fields: z.array(z.string().min(1)).optional()
7457
- });
7458
- var CategoryNodeSchema = z.lazy(
7459
- () => z.object({
7460
- id: z.string().min(1),
7461
- label: z.string().min(1).optional(),
7462
- children: z.array(CategoryNodeSchema).optional()
7463
- })
7464
- );
7465
- var CategoriesSchema = z.array(CategoryNodeSchema).refine(hasUniqueCategoryIds, { message: "category ids must be unique across the whole tree" }).optional().catch(void 0);
7466
- function hasUniqueCategoryIds(nodes) {
7467
- const seen = /* @__PURE__ */ new Set();
7468
- const walk3 = (list) => list.every((node) => {
7469
- if (seen.has(node.id)) return false;
7470
- seen.add(node.id);
7471
- return node.children === void 0 || walk3(node.children);
7472
- });
7473
- return walk3(nodes);
7474
7477
  }
7475
- var CatalogSchema = z.object({
7476
- name: z.string().regex(NAME_PATTERN),
7477
- description: z.string().min(1).optional(),
7478
- owner: z.object({ name: z.string().min(1), url: z.string().min(1).optional() }).optional(),
7479
- display: DisplaySchema.optional(),
7480
- categories: CategoriesSchema,
7481
- plugins: z.array(EntrySchema)
7482
- });
7483
- var RecordSchema2 = z.object({
7484
- name: z.string().regex(NAME_PATTERN),
7485
- source: z.string().min(1),
7486
- addedAt: z.number(),
7487
- catalog: CatalogSchema
7488
- });
7489
- var StateSchema2 = z.object({
7490
- version: z.literal(1).optional(),
7491
- marketplaces: z.array(RecordSchema2)
7492
- });
7493
- function projectEntryForDisplay(entry, display) {
7494
- const allowed = display?.fields;
7495
- if (allowed === void 0) return entry;
7496
- const out = { ...entry };
7497
- for (const field of MARKETPLACE_DISPLAY_FIELDS) {
7498
- if (!displayAllows(display, field)) delete out[field];
7478
+ async function runInstall(resolved, staging, opts) {
7479
+ const rootStaging = join(staging, "root");
7480
+ const fetched = await fetchInto(resolved, rootStaging, opts);
7481
+ if (!fetched.ok) return { ok: false, reason: fetched.reason };
7482
+ const readDir = resolved.type === "local" ? resolved.target : pluginRoot(resolved, rootStaging);
7483
+ const read = readPluginManifest(readDir);
7484
+ const manifest = read.manifest;
7485
+ if (!manifest) {
7486
+ const detail = read.issues.map((i) => t("plugin_install.issue_line", { path: i.path, message: i.message })).join("\n");
7487
+ return { ok: false, reason: t("plugin_install.manifest_unusable", { detail: `
7488
+ ${detail}` }) };
7499
7489
  }
7500
- return out;
7501
- }
7502
- function displayAllows(display, field) {
7503
- const allowed = display?.fields;
7504
- if (allowed === void 0) return true;
7505
- return allowed.includes(field);
7506
- }
7507
- function readMarketplaces(statePath) {
7508
- if (!existsSync(statePath)) return { records: [], issues: [] };
7509
- let raw;
7510
- try {
7511
- raw = JSON.parse(readFileSync(statePath, "utf-8"));
7512
- } catch (err) {
7490
+ const taken = new PluginStore(opts.statePath).read().records.find((r) => r.name === manifest.name);
7491
+ const conflict = taken && taken.name !== opts.replace ? taken : void 0;
7492
+ const current = opts.epochVersion ?? currentEpochVersion();
7493
+ const version = checkEpochVersion(manifest.epochVersion, current);
7494
+ const versionUnmet = !version.ok && manifest.epochVersion ? { required: manifest.epochVersion, current, reason: version.reason } : void 0;
7495
+ const closure = await resolveClosure({
7496
+ root: manifest.name,
7497
+ declared: manifest.dependencies ?? [],
7498
+ installed: new Set(new PluginStore(opts.statePath).read().records.map((r) => r.name)),
7499
+ staging,
7500
+ opts
7501
+ });
7502
+ const preview3 = {
7503
+ manifest,
7504
+ source: resolved,
7505
+ inventory: scanPluginDir(readDir, manifest.name),
7506
+ ...conflict ? { conflict } : {},
7507
+ ...versionUnmet ? { versionUnmet } : {},
7508
+ ...closure.report.length > 0 ? { dependencies: closure.report } : {}
7509
+ };
7510
+ if (opts.replace !== void 0 && manifest.name !== opts.replace) {
7513
7511
  return {
7514
- records: [],
7515
- issues: [
7516
- { path: statePath, message: t("plugin_marketplace.bad_json", { reason: first(err) }) }
7517
- ]
7512
+ ok: false,
7513
+ preview: preview3,
7514
+ reason: t("plugin_install.renamed", { found: manifest.name, expected: opts.replace })
7518
7515
  };
7519
7516
  }
7520
- const checked = parseLenient(StateSchema2.optional(), raw, void 0);
7521
- if (!checked.value) {
7517
+ if (conflict) {
7522
7518
  return {
7523
- records: [],
7524
- issues: checked.issues.map((i) => ({
7525
- path: statePath,
7526
- message: t("plugin_marketplace.issue", { path: i.path, message: i.message })
7527
- }))
7519
+ ok: false,
7520
+ preview: preview3,
7521
+ reason: t("plugin_install.already_installed", {
7522
+ name: conflict.name,
7523
+ version: conflict.version,
7524
+ source: conflict.source
7525
+ })
7528
7526
  };
7529
7527
  }
7530
- return { records: checked.value.marketplaces, issues: [] };
7531
- }
7532
- async function mutate(statePath, fn) {
7533
- const release = await acquireStateLock(statePath);
7534
- try {
7535
- const next = fn(readMarketplaces(statePath).records);
7536
- if (!next) return false;
7537
- writeStateFile(statePath, JSON.stringify({ version: 1, marketplaces: next }, null, 2));
7538
- return true;
7539
- } finally {
7540
- release();
7528
+ if (opts.confirm && !await opts.confirm(preview3)) {
7529
+ return { ok: false, reason: t("plugin.install_cancelled"), cancelled: true, preview: preview3 };
7541
7530
  }
7542
- }
7543
- async function addMarketplace(source, opts) {
7544
- const fetched = await fetchCatalog(source);
7545
- if (!fetched.ok) return fetched;
7546
- const record = {
7547
- name: fetched.catalog.name,
7548
- source: source.trim(),
7549
- addedAt: Date.now(),
7550
- catalog: fetched.catalog
7551
- };
7552
- const written = await mutate(
7553
- opts.statePath,
7554
- (records) => records.some((r) => r.name === record.name) ? void 0 : [...records, record].sort((a, b) => a.name.localeCompare(b.name))
7531
+ const records = await commitAll(
7532
+ [
7533
+ {
7534
+ name: manifest.name,
7535
+ version: manifest.version,
7536
+ source: resolved,
7537
+ stagingDir: rootStaging,
7538
+ ...opts.marketplace ? { marketplace: opts.marketplace } : {}
7539
+ },
7540
+ ...closure.plans.map((plan) => ({
7541
+ name: plan.name,
7542
+ version: plan.manifest.version,
7543
+ source: plan.source,
7544
+ stagingDir: plan.stagingDir,
7545
+ ...plan.marketplace ? { marketplace: plan.marketplace } : {}
7546
+ }))
7547
+ ],
7548
+ opts
7555
7549
  );
7556
- return written ? { ok: true, record } : {
7550
+ const record = records?.find((r) => r.name === manifest.name);
7551
+ return record ? { ok: true, record, preview: preview3 } : {
7557
7552
  ok: false,
7558
- reason: t("plugin_marketplace.already_added", { name: record.name })
7553
+ preview: preview3,
7554
+ reason: t("plugin_install.raced", { name: manifest.name })
7559
7555
  };
7560
7556
  }
7561
- async function removeMarketplace(name, opts) {
7562
- const written = await mutate(
7563
- opts.statePath,
7564
- (records) => records.some((r) => r.name === name) ? records.filter((r) => r.name !== name) : void 0
7565
- );
7566
- return written ? { ok: true } : { ok: false, reason: t("plugin_marketplace.not_added", { name }) };
7567
- }
7568
- async function updateMarketplace(name, opts) {
7569
- const existing = readMarketplaces(opts.statePath).records.find((r) => r.name === name);
7570
- if (!existing) return { ok: false, reason: t("plugin_marketplace.not_added", { name }) };
7571
- const fetched = await fetchCatalog(existing.source);
7572
- if (!fetched.ok) return fetched;
7573
- if (fetched.catalog.name !== name) {
7574
- return {
7575
- ok: false,
7576
- reason: t("plugin_marketplace.renamed", {
7577
- source: existing.source,
7578
- name: fetched.catalog.name
7579
- })
7580
- };
7557
+ var DEPENDENCY_LIMIT = 32;
7558
+ async function commitAll(entries2, opts) {
7559
+ const records = [];
7560
+ for (const entry of entries2) {
7561
+ const dest = join(opts.pluginsDir, entry.name);
7562
+ removeInstallDest(dest);
7563
+ let linked = entry.source.type === "local";
7564
+ if (linked) {
7565
+ try {
7566
+ symlinkSync(entry.source.target, dest, "junction");
7567
+ } catch {
7568
+ opts.onProgress?.(t("plugin_install.symlink_failed"));
7569
+ cpSync(entry.source.target, dest, { recursive: true });
7570
+ linked = false;
7571
+ }
7572
+ } else {
7573
+ renameSync(pluginRoot(entry.source, entry.stagingDir), dest);
7574
+ }
7575
+ records.push({
7576
+ name: entry.name,
7577
+ version: entry.version,
7578
+ source: entry.source.raw,
7579
+ sourceType: entry.source.type,
7580
+ path: dest,
7581
+ linked,
7582
+ enabled: true,
7583
+ installedAt: Date.now(),
7584
+ ...entry.marketplace ? { marketplace: entry.marketplace } : {},
7585
+ ...entry.source.sha256 ? { checksum: `sha256:${entry.source.sha256}` } : {}
7586
+ });
7581
7587
  }
7582
- const record = { ...existing, catalog: fetched.catalog };
7583
- await mutate(opts.statePath, (records) => records.map((r) => r.name === name ? record : r));
7584
- return { ok: true, record };
7588
+ return new PluginStore(opts.statePath).mutate((existing) => {
7589
+ for (const record of records) {
7590
+ const taken = existing.some((r) => r.name === record.name);
7591
+ if (taken && opts.replace !== record.name) return void 0;
7592
+ }
7593
+ let next = existing;
7594
+ for (const record of records) {
7595
+ const taken = next.some((r) => r.name === record.name);
7596
+ next = taken ? next.map((r) => r.name === record.name ? record : r) : [...next, record];
7597
+ }
7598
+ return { records: next, result: records };
7599
+ });
7585
7600
  }
7586
- function searchMarketplaces(keyword, statePath) {
7587
- const needle = keyword.trim().toLowerCase();
7588
- const hits = [];
7589
- for (const record of readMarketplaces(statePath).records) {
7590
- for (const entry of record.catalog.plugins) {
7591
- const haystack = [
7592
- entry.name,
7593
- entry.description ?? "",
7594
- ...entry.category ?? [],
7595
- entry.author?.name ?? "",
7596
- ...entry.keywords ?? []
7597
- ].join(" ").toLowerCase();
7598
- if (needle && !haystack.includes(needle)) continue;
7599
- hits.push({
7600
- marketplace: record.name,
7601
- entry: projectEntryForDisplay(entry, record.catalog.display),
7602
- ref: `${record.name}/${entry.name}`
7601
+ async function resolveClosure(input) {
7602
+ const { opts, staging } = input;
7603
+ const seen = /* @__PURE__ */ new Set([input.root]);
7604
+ const queue = input.declared.map((name) => ({
7605
+ name,
7606
+ requiredBy: input.root
7607
+ }));
7608
+ const report3 = [];
7609
+ const plans = [];
7610
+ while (queue.length > 0) {
7611
+ const next = queue.shift();
7612
+ if (!next) break;
7613
+ const { name, requiredBy } = next;
7614
+ if (seen.has(name)) continue;
7615
+ seen.add(name);
7616
+ if (input.installed.has(name)) {
7617
+ report3.push({ name, status: "installed", requiredBy });
7618
+ continue;
7619
+ }
7620
+ if (plans.length >= DEPENDENCY_LIMIT) {
7621
+ report3.push({
7622
+ name,
7623
+ status: "unresolved",
7624
+ requiredBy,
7625
+ reason: t("plugin_install.dep_too_many", { limit: DEPENDENCY_LIMIT, root: input.root })
7626
+ });
7627
+ continue;
7628
+ }
7629
+ const located = await locateDependency(name, opts);
7630
+ if (!located) {
7631
+ report3.push({
7632
+ name,
7633
+ status: "unresolved",
7634
+ requiredBy,
7635
+ reason: t("plugin_install.dep_no_source", { name })
7636
+ });
7637
+ continue;
7638
+ }
7639
+ const source = resolveSource(located.source);
7640
+ if (!source) {
7641
+ report3.push({
7642
+ name,
7643
+ status: "unresolved",
7644
+ requiredBy,
7645
+ source: located.source,
7646
+ reason: t("plugin_install.dep_bad_source", { name, source: located.source })
7647
+ });
7648
+ continue;
7649
+ }
7650
+ opts.onProgress?.(t("plugin_install.dep_fetching", { name, source: located.source }));
7651
+ const stagingDir = join(staging, `dep-${plans.length}`);
7652
+ const fetched = await fetchInto(source, stagingDir, opts);
7653
+ if (!fetched.ok) {
7654
+ report3.push({
7655
+ name,
7656
+ status: "unresolved",
7657
+ requiredBy,
7658
+ source: located.source,
7659
+ reason: fetched.reason
7603
7660
  });
7661
+ continue;
7662
+ }
7663
+ const readDir = source.type === "local" ? source.target : pluginRoot(source, stagingDir);
7664
+ const read = readPluginManifest(readDir);
7665
+ const manifest = read.manifest;
7666
+ if (!manifest) {
7667
+ report3.push({
7668
+ name,
7669
+ status: "unresolved",
7670
+ requiredBy,
7671
+ source: located.source,
7672
+ reason: t("plugin_install.dep_bad_manifest", {
7673
+ name,
7674
+ detail: read.issues.map((issue) => issue.message).join("; ")
7675
+ })
7676
+ });
7677
+ continue;
7678
+ }
7679
+ if (manifest.name !== name) {
7680
+ report3.push({
7681
+ name,
7682
+ status: "unresolved",
7683
+ requiredBy,
7684
+ source: located.source,
7685
+ reason: t("plugin_install.dep_name_mismatch", { name, found: manifest.name })
7686
+ });
7687
+ continue;
7688
+ }
7689
+ const current = opts.epochVersion ?? currentEpochVersion();
7690
+ const version = checkEpochVersion(manifest.epochVersion, current);
7691
+ const versionUnmet = !version.ok && manifest.epochVersion ? { required: manifest.epochVersion, current, reason: version.reason } : void 0;
7692
+ report3.push({
7693
+ name,
7694
+ status: "plan",
7695
+ requiredBy,
7696
+ source: located.source,
7697
+ manifest,
7698
+ inventory: scanPluginDir(readDir, manifest.name),
7699
+ ...versionUnmet ? { versionUnmet } : {}
7700
+ });
7701
+ plans.push({
7702
+ name: manifest.name,
7703
+ source,
7704
+ stagingDir,
7705
+ manifest,
7706
+ ...located.marketplace ? { marketplace: located.marketplace } : {}
7707
+ });
7708
+ for (const nested of manifest.dependencies ?? []) {
7709
+ queue.push({ name: nested, requiredBy: manifest.name });
7604
7710
  }
7605
7711
  }
7606
- return hits;
7712
+ return { report: report3, plans };
7607
7713
  }
7608
- function resolveMarketplaceRef(input, statePath) {
7609
- const parts = input.trim().split("/");
7610
- if (parts.length !== 2) return void 0;
7611
- const [market = "", plugin = ""] = parts;
7612
- if (!NAME_PATTERN.test(market) || !NAME_PATTERN.test(plugin)) return void 0;
7613
- const record = readMarketplaces(statePath).records.find((r) => r.name === market);
7614
- if (!record) {
7615
- return {
7616
- ok: false,
7617
- reason: t("plugin_marketplace.market_not_added", { market, input })
7618
- };
7714
+ async function locateDependency(name, opts) {
7715
+ const local = opts.marketplacesStatePath ? findMarketplaceEntryByName(name, opts.marketplacesStatePath) : void 0;
7716
+ if (local) return { source: local.source, marketplace: local.marketplace };
7717
+ if (!opts.resolveDependency) return void 0;
7718
+ try {
7719
+ const answer = await opts.resolveDependency(name);
7720
+ return answer ? { source: answer } : void 0;
7721
+ } catch {
7722
+ return void 0;
7619
7723
  }
7620
- const entry = record.catalog.plugins.find((p) => p.name === plugin);
7621
- if (!entry) {
7622
- return {
7623
- ok: false,
7624
- reason: t("plugin_marketplace.plugin_not_in_market", { market, plugin })
7625
- };
7724
+ }
7725
+ function removeInstallDest(dest) {
7726
+ let link = false;
7727
+ try {
7728
+ link = lstatSync(dest).isSymbolicLink();
7729
+ } catch {
7626
7730
  }
7627
- const source = resolveEntrySource(entry.source, record.source);
7628
- if (!source) {
7629
- return {
7630
- ok: false,
7631
- reason: t("plugin_marketplace.entry_unresolvable", {
7632
- market,
7633
- plugin,
7634
- source: entry.source
7635
- })
7636
- };
7731
+ if (!link) {
7732
+ rmSync(dest, { recursive: true, force: true });
7733
+ return;
7734
+ }
7735
+ try {
7736
+ unlinkSync(dest);
7737
+ } catch {
7738
+ rmdirSync(dest);
7637
7739
  }
7638
- return { ok: true, ref: { marketplace: market, source } };
7639
7740
  }
7640
- function resolveEntrySource(entry, marketSource) {
7641
- const spec = entry.trim();
7642
- if (!spec.startsWith(".") && !spec.startsWith("/")) return spec;
7643
- const rel = spec.replace(/^\.\//, "").replace(/^\//, "");
7644
- if (rel === "" || rel.split("/").includes("..")) return void 0;
7645
- const market = marketSource.trim();
7646
- if (market.startsWith("github:") || market.startsWith("git+")) {
7647
- const [base = "", fragment] = market.split("#", 2);
7648
- const prefix = fragment?.startsWith("subdir=") ? `${fragment.slice("subdir=".length)}/` : "";
7649
- return `${base}#subdir=${prefix}${rel}`;
7650
- }
7651
- if (market.startsWith("https://") || market.startsWith("http://")) {
7652
- return `${market.replace(/\/[^/]*$/, "")}/${rel}`;
7741
+ async function fetchInto(source, staging, opts) {
7742
+ if (source.type === "local") return fetchLocal(source);
7743
+ if (source.type === "git") return fetchGit(source, staging, opts);
7744
+ if (source.type === "npm") return fetchNpm(source, staging, opts);
7745
+ if (source.type === "host") return fetchFromHost(source, staging, opts);
7746
+ if (source.localFile) return fetchLocalArchive(source, staging);
7747
+ return fetchArchive(source, staging, opts);
7748
+ }
7749
+ async function fetchLocalArchive(source, staging) {
7750
+ if (!existsSync(source.target)) {
7751
+ return { ok: false, reason: t("plugin_install.archive_missing", { path: source.target }) };
7653
7752
  }
7654
- return join(marketDir(market), rel);
7753
+ mkdirSync(staging, { recursive: true });
7754
+ return extractArchive(source.target, staging);
7655
7755
  }
7656
- async function fetchCatalog(source) {
7657
- const input = source.trim();
7658
- if (!input) return { ok: false, reason: t("plugin_marketplace.source_empty") };
7659
- const local = localCatalogPath(input);
7660
- if (local) {
7661
- if (!existsSync(local)) {
7662
- return { ok: false, reason: t("plugin_marketplace.local_missing", { path: local }) };
7663
- }
7756
+ async function fetchFromHost(source, staging, opts) {
7757
+ const ask = opts.fetchFromHost;
7758
+ if (!ask) {
7759
+ return { ok: false, reason: t("plugin_install.host_fetch_unavailable") };
7760
+ }
7761
+ opts.onProgress?.(t("plugin_install.host_fetching", { target: source.target }));
7762
+ mkdirSync(staging, { recursive: true });
7763
+ return withDownloadDir(async (dl) => {
7764
+ let answer;
7664
7765
  try {
7665
- return parseCatalog(readFileSync(local, "utf-8"), local);
7766
+ answer = await ask({ target: source.target, destDir: dl });
7666
7767
  } catch (err) {
7667
- return { ok: false, reason: t("plugin_marketplace.read_failed", { reason: first(err) }) };
7768
+ return {
7769
+ ok: false,
7770
+ reason: t("plugin_install.host_fetch_threw", {
7771
+ reason: (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? ""
7772
+ })
7773
+ };
7774
+ }
7775
+ if (!answer.ok) {
7776
+ return {
7777
+ ok: false,
7778
+ reason: answer.reason ?? t("plugin_install.host_fetch_failed", { target: source.target })
7779
+ };
7780
+ }
7781
+ if (!existsSync(answer.archivePath)) {
7782
+ return {
7783
+ ok: false,
7784
+ reason: t("plugin_install.host_fetch_no_file", { path: answer.archivePath })
7785
+ };
7668
7786
  }
7787
+ return extractArchive(answer.archivePath, staging);
7788
+ });
7789
+ }
7790
+ function fetchLocal(source) {
7791
+ if (!existsSync(source.target)) {
7792
+ return { ok: false, reason: t("plugin_install.local_missing", { path: source.target }) };
7669
7793
  }
7670
- const url = catalogUrl(input);
7671
- if (!url) {
7672
- return {
7673
- ok: false,
7674
- reason: t("plugin_marketplace.bad_url", { input, file: MARKETPLACE_FILE })
7675
- };
7794
+ return { ok: true };
7795
+ }
7796
+ async function fetchGit(source, staging, opts) {
7797
+ opts.onProgress?.(t("plugin_install.cloning", { target: source.target }));
7798
+ const result = await runCommand("git", ["clone", "--depth", "1", source.target, staging]);
7799
+ if (!result.ok) {
7800
+ return { ok: false, reason: t("plugin_install.clone_failed", { detail: result.detail }) };
7676
7801
  }
7802
+ rmSync(join(staging, ".git"), { recursive: true, force: true });
7803
+ return { ok: true };
7804
+ }
7805
+ async function fetchNpm(source, staging, opts) {
7806
+ opts.onProgress?.(t("plugin_install.npm_fetching", { target: source.target }));
7807
+ mkdirSync(staging, { recursive: true });
7808
+ return withDownloadDir(async (dl) => {
7809
+ const packed = await runCommand(
7810
+ "npm",
7811
+ ["pack", source.target, "--pack-destination", dl, "--silent"],
7812
+ { cwd: dl, windowsShell: true }
7813
+ );
7814
+ if (!packed.ok) {
7815
+ return { ok: false, reason: t("plugin_install.npm_pack_failed", { detail: packed.detail }) };
7816
+ }
7817
+ const tarball = packed.stdout.trim().split("\n").pop()?.trim();
7818
+ if (!tarball) return { ok: false, reason: t("plugin_install.npm_no_tarball") };
7819
+ return extractArchive(isAbsolute(tarball) ? tarball : join(dl, tarball), staging);
7820
+ });
7821
+ }
7822
+ async function fetchArchive(source, staging, opts) {
7823
+ opts.onProgress?.(t("plugin_install.downloading", { target: source.target }));
7824
+ mkdirSync(staging, { recursive: true });
7825
+ let bytes;
7677
7826
  try {
7678
- const response = await fetch(url, { redirect: "follow" });
7827
+ const response = await fetch(source.target, { redirect: "follow" });
7679
7828
  if (!response.ok) {
7680
- const vars = { url, status: response.status };
7681
7829
  return {
7682
7830
  ok: false,
7683
- reason: looksPrivate(response.status) ? t("marketplace.fetch_failed_private", vars) : t("marketplace.fetch_failed", vars)
7831
+ reason: t("plugin_install.download_http", {
7832
+ status: response.status,
7833
+ statusText: response.statusText
7834
+ })
7684
7835
  };
7685
7836
  }
7686
- return parseCatalog(await response.text(), url);
7837
+ bytes = Buffer.from(await response.arrayBuffer());
7687
7838
  } catch (err) {
7688
- return { ok: false, reason: t("marketplace.fetch_error", { url, reason: first(err) }) };
7839
+ return {
7840
+ ok: false,
7841
+ reason: t("plugin_install.download_failed", {
7842
+ message: err instanceof Error ? err.message : String(err)
7843
+ })
7844
+ };
7845
+ }
7846
+ if (source.sha256) {
7847
+ const actual = createHash("sha256").update(bytes).digest("hex");
7848
+ if (actual !== source.sha256) {
7849
+ return {
7850
+ ok: false,
7851
+ reason: [
7852
+ t("plugin_install.sha_mismatch"),
7853
+ t("plugin_install.sha_expected", { expected: source.sha256 }),
7854
+ t("plugin_install.sha_actual", { actual })
7855
+ ].join("\n")
7856
+ };
7857
+ }
7689
7858
  }
7859
+ return withDownloadDir(async (dl) => {
7860
+ const archive = join(dl, `download${archiveExt(source.target)}`);
7861
+ writeFileSync(archive, bytes);
7862
+ return extractArchive(archive, staging);
7863
+ });
7690
7864
  }
7691
- function looksPrivate(status) {
7692
- return status === 401 || status === 403 || status === 404;
7865
+ function archiveExt(url) {
7866
+ const path = (url.split("?")[0] ?? "").toLowerCase();
7867
+ if (path.endsWith(".tgz")) return ".tgz";
7868
+ if (path.endsWith(".tar.gz")) return ".tar.gz";
7869
+ return ".zip";
7693
7870
  }
7694
- function catalogUrl(input) {
7695
- if (input.startsWith("github:")) {
7696
- const slug = input.slice("github:".length);
7697
- if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) return void 0;
7698
- return `https://raw.githubusercontent.com/${slug}/HEAD/${MARKETPLACE_FILE}`;
7871
+ async function withDownloadDir(fn) {
7872
+ const dir = mkdtempSync(join(tmpdir(), "epoch-plugin-dl-"));
7873
+ try {
7874
+ return await fn(dir);
7875
+ } finally {
7876
+ rmSync(dir, { recursive: true, force: true });
7699
7877
  }
7700
- if (!input.startsWith("https://") && !input.startsWith("http://")) return void 0;
7701
- return input.endsWith(".json") ? input : `${input.replace(/\/+$/, "")}/${MARKETPLACE_FILE}`;
7702
7878
  }
7703
- function localCatalogPath(input) {
7704
- if (/^[a-z][a-z0-9+.-]+:/i.test(input) || input.startsWith("//")) return void 0;
7705
- const abs = isAbsolute(input) ? input : resolve(input);
7706
- if (existsSync(abs) && statSync(abs).isDirectory()) return join(abs, MARKETPLACE_FILE);
7707
- return abs;
7879
+ // src/plugin/root-token.ts
7880
+ var PLUGIN_ROOT_TOKEN = "${EPOCH_PLUGIN_ROOT}";
7881
+ var PLUGIN_ROOT_ENV = "EPOCH_PLUGIN_ROOT";
7882
+ function mentionsPluginRoot(text) {
7883
+ return text.includes(PLUGIN_ROOT_TOKEN);
7708
7884
  }
7709
- function marketDir(market) {
7710
- const abs = isAbsolute(market) ? market : resolve(market);
7711
- if (existsSync(abs) && statSync(abs).isDirectory()) return abs;
7712
- return abs.replace(/[/\\][^/\\]*$/, "");
7885
+ function substitutePluginRoot(text, root) {
7886
+ return text.split(PLUGIN_ROOT_TOKEN).join(root);
7713
7887
  }
7714
- function parseCatalog(body2, where) {
7715
- let raw;
7716
- try {
7717
- raw = JSON.parse(body2);
7718
- } catch (err) {
7888
+ async function uninstallPlugin(name, opts) {
7889
+ const removed = await new PluginStore(opts.statePath).mutate((records) => {
7890
+ const hit = records.find((r) => r.name === name);
7891
+ if (!hit) return void 0;
7892
+ return { records: records.filter((r) => r.name !== name), result: hit };
7893
+ });
7894
+ if (!removed) return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7895
+ if (removed.linked) {
7896
+ rmSync(removed.path, { force: true });
7897
+ return { ok: true, message: t("plugin_lifecycle.uninstalled_link", { name }) };
7898
+ }
7899
+ rmSync(removed.path, { recursive: true, force: true });
7900
+ return { ok: true, message: t("plugin_lifecycle.uninstalled", { name }) };
7901
+ }
7902
+ async function setPluginEnabled(name, enabled, opts) {
7903
+ const changed = await new PluginStore(opts.statePath).mutate((records) => {
7904
+ const hit = records.find((r) => r.name === name);
7905
+ if (!hit) return void 0;
7719
7906
  return {
7720
- ok: false,
7721
- reason: t("plugin_marketplace.catalog_bad_json", { where, reason: first(err) })
7907
+ records: records.map((r) => r.name === name ? { ...r, enabled } : r),
7908
+ result: hit.enabled !== enabled
7722
7909
  };
7910
+ });
7911
+ if (changed === void 0) {
7912
+ return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7723
7913
  }
7724
- const checked = parseLenient(CatalogSchema.optional(), raw, void 0);
7725
- if (!checked.value) {
7726
- const detail = checked.issues.map((i) => t("plugin_marketplace.issue", { path: i.path, message: i.message })).join(t("plugin_marketplace.catalog_issue_sep"));
7727
- return { ok: false, reason: t("plugin_marketplace.catalog_invalid", { where, detail }) };
7728
- }
7729
- return { ok: true, catalog: checked.value };
7914
+ const verb = enabled ? t("plugin_lifecycle.verb_enable") : t("plugin_lifecycle.verb_disable");
7915
+ return {
7916
+ ok: true,
7917
+ message: changed ? t("plugin_lifecycle.toggled", { verb, name }) : t("plugin_lifecycle.already", { name, verb })
7918
+ };
7730
7919
  }
7731
- function first(err) {
7732
- const message = err instanceof Error ? err.message : String(err);
7733
- return message.split("\n")[0] ?? t("plugin_marketplace.unknown_error");
7920
+ async function updatePlugin(name, opts) {
7921
+ const existing = new PluginStore(opts.statePath).read().records.find((r) => r.name === name);
7922
+ if (!existing) return { ok: false, reason: t("plugin_lifecycle.not_installed", { name }) };
7923
+ if (existing.linked) {
7924
+ return {
7925
+ ok: true,
7926
+ message: t("plugin_lifecycle.linked_no_update", { name, source: existing.source })
7927
+ };
7928
+ }
7929
+ return installPlugin(existing.source, {
7930
+ ...opts,
7931
+ replace: name,
7932
+ ...existing.marketplace ? { marketplace: existing.marketplace } : {}
7933
+ });
7734
7934
  }
7735
7935
  var TMP_SUFFIX = ".epoch-rewind";
7736
7936
  function planFor(entry, current) {
@@ -7778,7 +7978,7 @@ async function applyRewind(store, sessionId, manifest, apply, skippedConflicts)
7778
7978
  } catch (err) {
7779
7979
  return { ...empty, failed: t("rewind.stage_failed", { message: messageOf(err) }) };
7780
7980
  }
7781
- return commitAll(staged, skippedConflicts);
7981
+ return commitAll2(staged, skippedConflicts);
7782
7982
  }
7783
7983
  async function stageAll(store, sessionId, manifest, todo, byPath) {
7784
7984
  const staged = [];
@@ -7806,7 +8006,7 @@ async function stageAll(store, sessionId, manifest, todo, byPath) {
7806
8006
  }
7807
8007
  return staged;
7808
8008
  }
7809
- async function commitAll(staged, skippedConflicts) {
8009
+ async function commitAll2(staged, skippedConflicts) {
7810
8010
  const outcome = {
7811
8011
  restored: [],
7812
8012
  deleted: [],
@@ -13371,6 +13571,10 @@ var DEFAULT_MODELS = {
13371
13571
  groq: "llama-3.3-70b-versatile",
13372
13572
  mistral: "ministral-8b-latest",
13373
13573
  moonshotai: "kimi-latest",
13574
+ zhipu: "glm-4.5-flash",
13575
+ dashscope: "qwen-flash",
13576
+ bytedance: "doubao-seed-2.0-mini",
13577
+ qianfan: "ernie-speed-128k",
13374
13578
  cohere: "command-r7b-12-2024",
13375
13579
  xai: "grok-vision-beta",
13376
13580
  perplexity: "sonar",
@@ -13783,6 +13987,10 @@ var PACKAGES = {
13783
13987
  groq: "@ai-sdk/groq",
13784
13988
  mistral: "@ai-sdk/mistral",
13785
13989
  moonshotai: "@ai-sdk/moonshotai",
13990
+ zhipu: "@ai-sdk/openai-compatible",
13991
+ dashscope: "@ai-sdk/alibaba",
13992
+ bytedance: "@ai-sdk/openai-compatible",
13993
+ qianfan: "@ai-sdk/openai-compatible",
13786
13994
  cohere: "@ai-sdk/cohere",
13787
13995
  xai: "@ai-sdk/xai",
13788
13996
  perplexity: "@ai-sdk/perplexity",
@@ -13792,6 +14000,17 @@ var PACKAGES = {
13792
14000
  ollama: "ollama-ai-provider-v2",
13793
14001
  "openai-compatible": "@ai-sdk/openai-compatible"
13794
14002
  };
14003
+ function openaiCompatibleFactory(name, defaultBaseUrl) {
14004
+ return async ({ apiKey, baseUrl }) => {
14005
+ const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
14006
+ return createOpenAICompatible({
14007
+ apiKey,
14008
+ baseURL: baseUrl || defaultBaseUrl,
14009
+ name,
14010
+ includeUsage: true
14011
+ });
14012
+ };
14013
+ }
13795
14014
  var LOADERS = {
13796
14015
  openai: async ({ apiKey, baseUrl }) => {
13797
14016
  const { createOpenAI } = await import('@ai-sdk/openai');
@@ -13823,6 +14042,17 @@ var LOADERS = {
13823
14042
  const { createMoonshotAI } = await import('@ai-sdk/moonshotai');
13824
14043
  return createMoonshotAI({ apiKey });
13825
14044
  },
14045
+ zhipu: openaiCompatibleFactory("zhipu", "https://open.bigmodel.cn/api/paas/v4"),
14046
+ dashscope: async ({ apiKey, baseUrl }) => {
14047
+ const { createAlibaba } = await import('@ai-sdk/alibaba');
14048
+ const provider = createAlibaba({
14049
+ apiKey,
14050
+ baseURL: baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1"
14051
+ });
14052
+ return (model) => provider.languageModel(model);
14053
+ },
14054
+ bytedance: openaiCompatibleFactory("bytedance", "https://ark.cn-beijing.volces.com/api/v3"),
14055
+ qianfan: openaiCompatibleFactory("qianfan", "https://qianfan.baidubce.com/v2"),
13826
14056
  cohere: async ({ apiKey }) => {
13827
14057
  const { createCohere } = await import('@ai-sdk/cohere');
13828
14058
  return createCohere({ apiKey });
@@ -13851,15 +14081,7 @@ var LOADERS = {
13851
14081
  const { createOllama } = await import('ollama-ai-provider-v2');
13852
14082
  return createOllama();
13853
14083
  },
13854
- "openai-compatible": async ({ apiKey, baseUrl }) => {
13855
- const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
13856
- return createOpenAICompatible({
13857
- apiKey,
13858
- baseURL: baseUrl || "http://localhost:8080/v1",
13859
- name: "custom",
13860
- includeUsage: true
13861
- });
13862
- }
14084
+ "openai-compatible": openaiCompatibleFactory("custom", "http://localhost:8080/v1")
13863
14085
  };
13864
14086
  var ProviderSdkMissingError = class extends Error {
13865
14087
  constructor(providerType, packageName, cause) {
@@ -14491,6 +14713,10 @@ var STATIC_MODEL_CATALOG = {
14491
14713
  ],
14492
14714
  mistral: ["mistral-large-latest", "mistral-small-latest", "codestral-latest"],
14493
14715
  moonshotai: ["kimi-latest"],
14716
+ zhipu: ["glm-4.6", "glm-4.5-flash", "glm-4.5-air", "glm-4.5"],
14717
+ dashscope: ["qwen3-max", "qwen-plus", "qwen-flash", "qwen-turbo", "qwen-max-latest"],
14718
+ bytedance: ["doubao-seed-2.1-pro", "doubao-seed-2.0-mini", "doubao-seed-1.6-flash"],
14719
+ qianfan: ["ernie-5.0", "ernie-speed-128k", "ernie-4.0-turbo-8k"],
14494
14720
  cohere: ["command-r-plus-04-2024", "command-r7b-12-2024", "command-a-03-2025"],
14495
14721
  xai: ["grok-vision-beta"],
14496
14722
  perplexity: ["sonar", "sonar-pro", "sonar-reasoning"],
@@ -14506,6 +14732,10 @@ var PROVIDER_DISCOVERY = {
14506
14732
  groq: { baseUrl: "https://api.groq.com/openai/v1", auth: "bearer" },
14507
14733
  mistral: { baseUrl: "https://api.mistral.ai/v1", auth: "bearer" },
14508
14734
  moonshotai: { baseUrl: "https://api.moonshot.ai/v1", auth: "bearer" },
14735
+ zhipu: { baseUrl: "https://open.bigmodel.cn/api/paas/v4", auth: "bearer" },
14736
+ dashscope: { baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", auth: "bearer" },
14737
+ bytedance: { baseUrl: "https://ark.cn-beijing.volces.com/api/v3", auth: "bearer" },
14738
+ qianfan: { baseUrl: "https://qianfan.baidubce.com/v2", auth: "bearer" },
14509
14739
  xai: { baseUrl: "https://api.x.ai/v1", auth: "bearer" },
14510
14740
  perplexity: { baseUrl: "https://api.perplexity.ai", auth: "bearer" },
14511
14741
  togetherai: { baseUrl: "https://api.together.xyz/v1", auth: "bearer" }
@@ -14756,7 +14986,7 @@ function writeUserYaml(path, key, value) {
14756
14986
  const after = asMapping(parseYaml(next));
14757
14987
  const intact = after !== null && (dot < 0 ? onlyChanged(before, after, key, value) : onlyChangedNested(before, after, key.slice(0, dot), key.slice(dot + 1), value));
14758
14988
  if (!intact) return { ok: false, reason: "unwritable-file" };
14759
- return commit2(path, next, 384, value);
14989
+ return commit(path, next, 384, value);
14760
14990
  }
14761
14991
  function writeSettingsJson(path, key, value) {
14762
14992
  let before = {};
@@ -14776,7 +15006,7 @@ function writeSettingsJson(path, key, value) {
14776
15006
  }
14777
15007
  const body2 = `${JSON.stringify({ ...before, [key]: value }, null, 2)}
14778
15008
  `;
14779
- return commit2(path, body2, 420, value);
15009
+ return commit(path, body2, 420, value);
14780
15010
  }
14781
15011
  function scalarText(value) {
14782
15012
  return dump(value, { lineWidth: -1 }).trimEnd();
@@ -14833,7 +15063,7 @@ function sameKeysAndValues(before, after, except) {
14833
15063
  function sameData(a, b) {
14834
15064
  return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
14835
15065
  }
14836
- function commit2(path, body2, createMode, value) {
15066
+ function commit(path, body2, createMode, value) {
14837
15067
  const tmp = `${path}.${process.pid}.tmp`;
14838
15068
  try {
14839
15069
  mkdirSync(dirname(path), { recursive: true });
@@ -19965,7 +20195,7 @@ function parseSimpleToml(content, file) {
19965
20195
  const at = `${file}:${i + 1}`;
19966
20196
  if (!trimmed || trimmed.startsWith("#")) continue;
19967
20197
  if (trimmed === "[[rule]]") {
19968
- if (current) commit3(current, file, rules, issues);
20198
+ if (current) commit2(current, file, rules, issues);
19969
20199
  current = { raw: {}, line: i + 1 };
19970
20200
  continue;
19971
20201
  }
@@ -19988,7 +20218,7 @@ function parseSimpleToml(content, file) {
19988
20218
  current.raw[kv.key] = kv.value;
19989
20219
  }
19990
20220
  }
19991
- if (current) commit3(current, file, rules, issues);
20221
+ if (current) commit2(current, file, rules, issues);
19992
20222
  return { rules, issues };
19993
20223
  }
19994
20224
  function readKeyValue(line) {
@@ -20002,7 +20232,7 @@ function readKeyValue(line) {
20002
20232
  const sq = rawVal.match(/^'(.*)'$/);
20003
20233
  return { key, value: sq && sq[1] ? sq[1] : rawVal };
20004
20234
  }
20005
- function commit3(draft, file, rules, issues) {
20235
+ function commit2(draft, file, rules, issues) {
20006
20236
  const at = `${file}:${draft.line}`;
20007
20237
  issues.push(...unknownKeyIssues(POLICY_RULE_KEYS, draft.raw, at));
20008
20238
  const raw = { ...draft.raw };
@@ -22490,7 +22720,7 @@ async function importSkills(source, opts) {
22490
22720
  detail: t("skill.import_stale", void 0, opts.lang)
22491
22721
  };
22492
22722
  }
22493
- return { ok: true, ...commit4(preview3, staged, opts.skillsDir, opts.lang) };
22723
+ return { ok: true, ...commit3(preview3, staged, opts.skillsDir, opts.lang) };
22494
22724
  });
22495
22725
  }
22496
22726
  async function withStaged(source, opts, fn) {
@@ -22609,7 +22839,7 @@ function digest(entries2, staged) {
22609
22839
  }
22610
22840
  return hash.digest("hex").slice(0, 32);
22611
22841
  }
22612
- function commit4(preview3, staged, skillsDir, lang) {
22842
+ function commit3(preview3, staged, skillsDir, lang) {
22613
22843
  const byName = new Map(staged.map((s) => [s.name, s.dir]));
22614
22844
  const imported = [];
22615
22845
  const skipped = [];