forge-cw 0.1.3 → 0.1.4

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
@@ -2441,14 +2441,17 @@ function manifestRepo(manifest) {
2441
2441
  return repo["url"];
2442
2442
  return void 0;
2443
2443
  }
2444
- function marketplaceRepo(marketplaces, marketplace) {
2445
- const entry = marketplaces[marketplace];
2444
+ function marketplaceSource(entry) {
2446
2445
  const source = isRecord(entry) ? entry["source"] : void 0;
2447
2446
  if (!isRecord(source))
2448
2447
  return void 0;
2449
- const value = source["repo"] ?? source["url"];
2448
+ const value = source["repo"] ?? source["url"] ?? source["path"];
2450
2449
  return typeof value === "string" ? value : void 0;
2451
2450
  }
2451
+ function splitPluginId(id) {
2452
+ const at = id.lastIndexOf("@");
2453
+ return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
2454
+ }
2452
2455
  function projectSkillNames(projectPath) {
2453
2456
  const dir = join8(projectPath, ".claude", "skills");
2454
2457
  if (!existsSync6(dir))
@@ -2456,17 +2459,13 @@ function projectSkillNames(projectPath) {
2456
2459
  return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join8(dir, e.name, "SKILL.md"))).map((e) => e.name).sort();
2457
2460
  }
2458
2461
  async function listPlugins(reader, remoteOf) {
2459
- const scopes = [
2460
- { scope: "global", scopeRef: "global" },
2461
- ...reader.getAccounts().map((a) => ({ scope: "account", scopeRef: a }))
2462
- ];
2463
2462
  const found = /* @__PURE__ */ new Map();
2464
- for (const { scope, scopeRef } of scopes) {
2463
+ for (const { scope, scopeRef } of pluginScopes(reader)) {
2465
2464
  const configDir = reader.getSkillConfigDir(scope, scopeRef);
2466
2465
  const installed = readJson(join8(configDir, "plugins", "installed_plugins.json"))["plugins"];
2467
2466
  if (!isRecord(installed))
2468
2467
  continue;
2469
- const marketplaces = readJson(join8(configDir, "plugins", "known_marketplaces.json"));
2468
+ const marketplaces = knownMarketplaces(configDir);
2470
2469
  const enabledPlugins = readJson(join8(configDir, "settings.json"))["enabledPlugins"];
2471
2470
  for (const [id, entries] of Object.entries(installed)) {
2472
2471
  if (!Array.isArray(entries))
@@ -2474,7 +2473,6 @@ async function listPlugins(reader, remoteOf) {
2474
2473
  const user = entries.find((e) => isRecord(e) && e["scope"] === "user" && typeof e["installPath"] === "string");
2475
2474
  if (!user)
2476
2475
  continue;
2477
- const marketplace = id.slice(id.lastIndexOf("@") + 1);
2478
2476
  const install = {
2479
2477
  scope,
2480
2478
  scopeRef,
@@ -2484,7 +2482,7 @@ async function listPlugins(reader, remoteOf) {
2484
2482
  lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0
2485
2483
  };
2486
2484
  const list = found.get(id) ?? [];
2487
- list.push({ install, repo: marketplaceRepo(marketplaces, marketplace) });
2485
+ list.push({ install, repo: marketplaceSource(marketplaces[splitPluginId(id).marketplace]) });
2488
2486
  found.set(id, list);
2489
2487
  }
2490
2488
  }
@@ -2498,8 +2496,7 @@ async function listPlugins(reader, remoteOf) {
2498
2496
  const match = repo ? remotes.find((r) => r.repo === repo) : void 0;
2499
2497
  plugins.push({
2500
2498
  id,
2501
- name: id.slice(0, id.lastIndexOf("@")),
2502
- marketplace: id.slice(id.lastIndexOf("@") + 1),
2499
+ ...splitPluginId(id),
2503
2500
  description: String(manifest["description"] ?? ""),
2504
2501
  repo,
2505
2502
  installs: list.map((f) => f.install),
@@ -2521,7 +2518,41 @@ function readPluginSkill(plugin, name) {
2521
2518
  return null;
2522
2519
  }
2523
2520
  }
2524
- var isRecord, inside;
2521
+ function readCatalog(location, marketplace) {
2522
+ const listed = readJson(join8(location, ".claude-plugin", "marketplace.json"))["plugins"];
2523
+ return (Array.isArray(listed) ? listed : []).filter((p) => isRecord(p) && typeof p["name"] === "string").map((p) => ({
2524
+ id: `${String(p["name"])}@${marketplace}`,
2525
+ name: String(p["name"]),
2526
+ description: String(p["description"] ?? ""),
2527
+ category: typeof p["category"] === "string" ? p["category"] : void 0
2528
+ })).sort((a, b) => a.name.localeCompare(b.name));
2529
+ }
2530
+ function listMarketplaces(reader) {
2531
+ const byName = /* @__PURE__ */ new Map();
2532
+ for (const target of pluginScopes(reader)) {
2533
+ for (const [name, entry] of Object.entries(knownMarketplaces(reader.getSkillConfigDir(target.scope, target.scopeRef)))) {
2534
+ const source = marketplaceSource(entry);
2535
+ if (!source)
2536
+ continue;
2537
+ const known = byName.get(name) ?? { name, source, scopes: [], plugins: [] };
2538
+ known.scopes.push(target);
2539
+ const location = isRecord(entry) ? entry["installLocation"] : void 0;
2540
+ if (known.plugins.length === 0 && typeof location === "string")
2541
+ known.plugins = readCatalog(location, name);
2542
+ byName.set(name, known);
2543
+ }
2544
+ }
2545
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2546
+ }
2547
+ function marketplaceSourceOf(reader, marketplace) {
2548
+ for (const { scope, scopeRef } of pluginScopes(reader)) {
2549
+ const source = marketplaceSource(knownMarketplaces(reader.getSkillConfigDir(scope, scopeRef))[marketplace]);
2550
+ if (source)
2551
+ return source;
2552
+ }
2553
+ return void 0;
2554
+ }
2555
+ var isRecord, inside, pluginScopes, knownMarketplaces, knowsMarketplace;
2525
2556
  var init_plugins = __esm({
2526
2557
  "../core/dist/plugins.js"() {
2527
2558
  "use strict";
@@ -2531,6 +2562,12 @@ var init_plugins = __esm({
2531
2562
  const p = resolve2(path);
2532
2563
  return p === r || p.startsWith(r + sep);
2533
2564
  };
2565
+ pluginScopes = (reader) => [
2566
+ { scope: "global", scopeRef: "global" },
2567
+ ...reader.getAccounts().map((a) => ({ scope: "account", scopeRef: a }))
2568
+ ];
2569
+ knownMarketplaces = (configDir) => readJson(join8(configDir, "plugins", "known_marketplaces.json"));
2570
+ knowsMarketplace = (configDir, marketplace) => isRecord(knownMarketplaces(configDir)[marketplace]);
2534
2571
  }
2535
2572
  });
2536
2573
 
@@ -2559,7 +2596,26 @@ function skillRoutes(reader, options = {}) {
2559
2596
  const runnerFor = options.runnerFor ?? ((env) => createRunner(env, INSTALL_TIMEOUT_MS));
2560
2597
  const remoteOf = options.remoteOf ?? createRemoteLookup(runCommand);
2561
2598
  const app = new Hono2();
2562
- const updating = /* @__PURE__ */ new Set();
2599
+ const busy = /* @__PURE__ */ new Set();
2600
+ const runClaude = async (scope, ref, commands, failure) => {
2601
+ const configDir = reader.getSkillConfigDir(scope, ref);
2602
+ if (busy.has(configDir))
2603
+ return { error: `A plugin command is already running for ${ref}`, status: 409 };
2604
+ busy.add(configDir);
2605
+ try {
2606
+ const { CLAUDE_CONFIG_DIR: _ignored, ...base } = envWithoutHarness();
2607
+ const run = runnerFor(scope === "global" ? base : { ...base, CLAUDE_CONFIG_DIR: configDir });
2608
+ for (const args of commands) {
2609
+ const result2 = await run("claude", args, configDir);
2610
+ if (result2.code !== 0)
2611
+ return { error: `${failure}: ${commandFailure(result2)}`, status: 500 };
2612
+ }
2613
+ return null;
2614
+ } finally {
2615
+ busy.delete(configDir);
2616
+ }
2617
+ };
2618
+ const knownAccount = (account) => account && reader.getAccounts().includes(account) ? account : null;
2563
2619
  const refOf = (scope, scopeRef) => scope === "global" ? "global" : scopeRef ?? "";
2564
2620
  const knownScope = (scope, ref) => scope === "global" || (scope === "account" ? reader.getAccounts().includes(ref) : scope === "project" && ref in reader.getProjects());
2565
2621
  app.get("/", (c) => c.json(reader.getSkills()));
@@ -2763,27 +2819,50 @@ function skillRoutes(reader, options = {}) {
2763
2819
  const install = plugin?.installs.find((i) => i.scope === scope && i.scopeRef === ref);
2764
2820
  if (!plugin || !install)
2765
2821
  return c.json({ error: `${id} is not installed in ${ref || "that scope"}` }, 404);
2766
- const configDir = reader.getSkillConfigDir(install.scope, install.scopeRef);
2767
- if (updating.has(configDir))
2768
- return c.json({ error: `An update is already running for ${ref}` }, 409);
2769
- updating.add(configDir);
2770
- try {
2771
- const { CLAUDE_CONFIG_DIR: _ignored, ...base } = envWithoutHarness();
2772
- const run = runnerFor(install.scope === "global" ? base : { ...base, CLAUDE_CONFIG_DIR: configDir });
2773
- for (const args of [["plugin", "marketplace", "update", plugin.marketplace], ["plugin", "update", plugin.id]]) {
2774
- const result2 = await run("claude", args, configDir);
2775
- if (result2.code !== 0)
2776
- return c.json({ error: `Failed to update ${plugin.name}: ${commandFailure(result2)}` }, 500);
2777
- }
2778
- } finally {
2779
- updating.delete(configDir);
2780
- }
2822
+ const failed = await runClaude(install.scope, ref, [["plugin", "marketplace", "update", plugin.marketplace], ["plugin", "update", plugin.id]], `Failed to update ${plugin.name}`);
2823
+ if (failed)
2824
+ return c.json({ error: failed.error }, failed.status);
2781
2825
  const version = (await find())?.installs.find((i) => i.scope === install.scope && i.scopeRef === ref)?.version ?? install.version;
2782
2826
  return c.json({ ok: true, version });
2783
2827
  });
2828
+ app.get("/marketplaces", (c) => c.json(listMarketplaces(reader)));
2829
+ app.post("/marketplaces", async (c) => {
2830
+ const body = await c.req.json().catch(() => ({}));
2831
+ const source = body.source?.trim() ?? "";
2832
+ if (!source || source.startsWith("-"))
2833
+ return c.json({ error: "A GitHub repo, URL or path is required" }, 400);
2834
+ const account = knownAccount(body.account);
2835
+ if (!account)
2836
+ return c.json({ error: "Unknown account" }, 404);
2837
+ const failed = await runClaude("account", account, [["plugin", "marketplace", "add", source]], `Failed to add ${source}`);
2838
+ if (failed)
2839
+ return c.json({ error: failed.error }, failed.status);
2840
+ return c.json({ ok: true });
2841
+ });
2842
+ app.post("/plugins/install", async (c) => {
2843
+ const body = await c.req.json().catch(() => ({}));
2844
+ const id = body.id ?? "";
2845
+ if (!PLUGIN_ID_RE.test(id))
2846
+ return c.json({ error: "Invalid plugin id" }, 400);
2847
+ const account = knownAccount(body.account);
2848
+ if (!account)
2849
+ return c.json({ error: "Unknown account" }, 404);
2850
+ const { name, marketplace } = splitPluginId(id);
2851
+ const commands = [["plugin", "install", id]];
2852
+ if (!knowsMarketplace(reader.getSkillConfigDir("account", account), marketplace)) {
2853
+ const source = marketplaceSourceOf(reader, marketplace);
2854
+ if (!source)
2855
+ return c.json({ error: `Unknown marketplace ${marketplace}` }, 404);
2856
+ commands.unshift(["plugin", "marketplace", "add", source]);
2857
+ }
2858
+ const failed = await runClaude("account", account, commands, `Failed to install ${name}`);
2859
+ if (failed)
2860
+ return c.json({ error: failed.error }, failed.status);
2861
+ return c.json({ ok: true });
2862
+ });
2784
2863
  return app;
2785
2864
  }
2786
- var INSTALL_TIMEOUT_MS, SKILL_NAME_RE;
2865
+ var INSTALL_TIMEOUT_MS, SKILL_NAME_RE, PLUGIN_ID_RE;
2787
2866
  var init_skill_routes = __esm({
2788
2867
  "../core/dist/skill-routes.js"() {
2789
2868
  "use strict";
@@ -2792,6 +2871,7 @@ var init_skill_routes = __esm({
2792
2871
  init_plugins();
2793
2872
  INSTALL_TIMEOUT_MS = 12e4;
2794
2873
  SKILL_NAME_RE = /^\w[\w.-]*$/;
2874
+ PLUGIN_ID_RE = /^\w[\w.-]*@\w[\w.-]*$/;
2795
2875
  }
2796
2876
  });
2797
2877
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A visual dashboard for your coding agents on Claude Code, Codex, Pi or OpenCode. Installs CW for you.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,8 +39,8 @@
39
39
  "@types/node": "^22.0.0",
40
40
  "esbuild": "^0.25.0",
41
41
  "typescript": "^5.8.0",
42
- "@forge-dev/core": "0.1.0",
43
- "@forge-dev/console": "0.1.0"
42
+ "@forge-dev/console": "0.1.0",
43
+ "@forge-dev/core": "0.1.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsc",