forge-cw 0.1.13 → 0.1.14

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
@@ -7173,7 +7173,7 @@ var init_module_context = __esm({
7173
7173
 
7174
7174
  // ../core/dist/module-host.js
7175
7175
  import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4, writeFileSync as writeFileSync5 } from "node:fs";
7176
- import { basename as basename4, dirname as dirname7, join as join16, resolve as resolve6 } from "node:path";
7176
+ import { basename as basename4, dirname as dirname7, join as join16, resolve as resolve6, sep as sep4 } from "node:path";
7177
7177
  import { pathToFileURL } from "node:url";
7178
7178
  function disabled(id, c, reason, m) {
7179
7179
  const text = (v, fallback) => typeof v === "string" && v ? v : fallback;
@@ -7268,6 +7268,7 @@ var init_module_host = __esm({
7268
7268
  opts;
7269
7269
  statuses = [];
7270
7270
  loaded = /* @__PURE__ */ new Map();
7271
+ origins = /* @__PURE__ */ new Map();
7271
7272
  constructor(opts) {
7272
7273
  this.opts = opts;
7273
7274
  }
@@ -7277,6 +7278,16 @@ var init_module_host = __esm({
7277
7278
  get(id) {
7278
7279
  return this.loaded.get(id);
7279
7280
  }
7281
+ // where a listed module was found, loaded or not; the first one found wins
7282
+ locate(id) {
7283
+ const c = this.origins.get(id);
7284
+ return c && { dir: c.dir, removable: this.removable(c) };
7285
+ }
7286
+ // only what was installed into the modules folder; bundled, dev and Forge's own packages stay
7287
+ removable(c) {
7288
+ const { modulesDir } = this.opts;
7289
+ return c.source === "installed" && dirname7(c.dir) === modulesDir || c.dir.startsWith(join16(modulesDir, "node_modules") + sep4);
7290
+ }
7280
7291
  // scans, reloads and dev changes run one at a time: each one awaits imports midway
7281
7292
  queue = Promise.resolve();
7282
7293
  serial(fn) {
@@ -7293,12 +7304,17 @@ var init_module_host = __esm({
7293
7304
  this.unload(id);
7294
7305
  const statuses = [];
7295
7306
  const seen = /* @__PURE__ */ new Map();
7307
+ const origins = /* @__PURE__ */ new Map();
7296
7308
  for (const candidate of this.candidates()) {
7297
- const status = await this.loadCandidate(candidate, seen);
7298
- if (status)
7299
- statuses.push(status);
7309
+ const status = await this.load(candidate, seen);
7310
+ if (!status)
7311
+ continue;
7312
+ statuses.push(status);
7313
+ if (!origins.has(status.id))
7314
+ origins.set(status.id, candidate);
7300
7315
  }
7301
7316
  this.statuses = statuses;
7317
+ this.origins = origins;
7302
7318
  return statuses;
7303
7319
  }
7304
7320
  // only this module is unloaded and loaded again; the others keep running
@@ -7312,7 +7328,7 @@ var init_module_host = __esm({
7312
7328
  } else {
7313
7329
  this.unload(id);
7314
7330
  const seen = new Map([...this.loaded.values()].map((m) => [m.status.id, m.dir]));
7315
- const status = await this.loadCandidate(mod.candidate, seen);
7331
+ const status = await this.load(mod.candidate, seen);
7316
7332
  this.statuses = this.statuses.flatMap((s) => s === mod.status ? status ? [status] : [] : [s]);
7317
7333
  }
7318
7334
  }
@@ -7394,6 +7410,12 @@ var init_module_host = __esm({
7394
7410
  out.push(...packages(this.opts.forgeNodeModules, "node_modules", false));
7395
7411
  return out;
7396
7412
  }
7413
+ async load(c, seen) {
7414
+ const status = await this.loadCandidate(c, seen);
7415
+ if (status && this.removable(c))
7416
+ status.removable = true;
7417
+ return status;
7418
+ }
7397
7419
  async loadCandidate(c, seen) {
7398
7420
  const fallbackId = basename4(c.dir);
7399
7421
  if (!existsSync13(c.manifestFile)) {
@@ -7534,9 +7556,186 @@ var init_module_confirmations = __esm({
7534
7556
  }
7535
7557
  });
7536
7558
 
7559
+ // ../core/dist/module-install.js
7560
+ import { randomBytes as randomBytes2 } from "node:crypto";
7561
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
7562
+ import { homedir as homedir7, tmpdir } from "node:os";
7563
+ import { isAbsolute as isAbsolute2, join as join17, resolve as resolve7, sep as sep5 } from "node:path";
7564
+ async function previewManifest(spec, opts) {
7565
+ if (isPathSpec(spec, opts.cwd))
7566
+ return readFolder(expand(spec, opts.cwd));
7567
+ const tmp = mkdtempSync(join17(tmpdir(), "forge-module-pack-"));
7568
+ try {
7569
+ const packed = await opts.run("npm", ["pack", spec, "--pack-destination", tmp, "--ignore-scripts", "--json"], opts.cwd);
7570
+ if (packed.code !== 0)
7571
+ throw new Error(npmPackError(spec, packed.stderr || packed.stdout));
7572
+ const info = JSON.parse(packed.stdout)[0];
7573
+ const filename = info?.filename;
7574
+ if (!filename)
7575
+ throw new Error(`npm pack ${spec} produced no tarball`);
7576
+ const untar = await opts.run("tar", ["-xzf", join17(tmp, filename), "-C", tmp], opts.cwd);
7577
+ if (untar.code !== 0)
7578
+ throw new Error(`could not unpack ${filename}: ${untar.stderr.trim()}`);
7579
+ const preview = readFolder(join17(tmp, "package"));
7580
+ return info.name && info.version ? { ...preview, packed: { name: info.name, version: info.version } } : preview;
7581
+ } finally {
7582
+ rmSync3(tmp, { recursive: true, force: true });
7583
+ }
7584
+ }
7585
+ function npmPackError(spec, output) {
7586
+ if (/\bE404\b/.test(output))
7587
+ return `${spec} is not on npm`;
7588
+ const lines2 = output.trim().split("\n").filter((l) => !l.includes("A complete log of this run"));
7589
+ return `npm pack ${spec} failed: ${lines2.join("\n")}`;
7590
+ }
7591
+ function readFolder(dir) {
7592
+ const manifestFile = manifestFileFor(dir).file;
7593
+ if (!existsSync14(manifestFile))
7594
+ throw new Error(`${dir} has no forge-module.json`);
7595
+ const raw = JSON.parse(readFileSync12(manifestFile, "utf-8"));
7596
+ const check = checkManifest(raw, dir);
7597
+ if (check.kind === "invalid")
7598
+ throw new Error(`invalid manifest: ${check.reason}`);
7599
+ return check;
7600
+ }
7601
+ function permissionLines(manifest) {
7602
+ const p = "permissions" in manifest ? manifest.permissions ?? {} : {};
7603
+ const lines2 = [];
7604
+ for (const kind of ["network", "fs:read", "fs:write", "shell"]) {
7605
+ for (const value of p[kind] ?? [])
7606
+ lines2.push({ kind, value });
7607
+ }
7608
+ if (p.pty)
7609
+ lines2.push({ kind: "pty", value: "yes" });
7610
+ const legacy = "actions" in manifest && (manifest.actions ?? []).some((a) => "command" in a);
7611
+ if (legacy)
7612
+ lines2.push({ kind: "shell", value: "runs the shell commands in its manifest" });
7613
+ return lines2;
7614
+ }
7615
+ function describePermissions(manifest) {
7616
+ const lines2 = permissionLines(manifest).map((l) => ` ${l.kind.padEnd(9)} ${l.value}`);
7617
+ return [lines2.length ? "It asks for:" : "It asks for no permissions.", ...lines2, "", TRUST_NOTE].join("\n");
7618
+ }
7619
+ function installArgs(spec, prefix) {
7620
+ return ["install", "--ignore-scripts", "--prefix", prefix, spec];
7621
+ }
7622
+ function packageName(dir) {
7623
+ try {
7624
+ const name = JSON.parse(readFileSync12(join17(dir, "package.json"), "utf-8")).name;
7625
+ return typeof name === "string" ? name : void 0;
7626
+ } catch {
7627
+ return void 0;
7628
+ }
7629
+ }
7630
+ var TRUST_NOTE, isPathSpec, expand, InstallError, PREVIEW_TTL_MS, REGISTRY_SPEC, ModuleInstaller;
7631
+ var init_module_install = __esm({
7632
+ "../core/dist/module-install.js"() {
7633
+ "use strict";
7634
+ init_module_manifest();
7635
+ init_output();
7636
+ init_state_log();
7637
+ TRUST_NOTE = "Modules run inside Forge with your access. Forge checks every call against these permissions, but it is not a sandbox: install only modules you trust.";
7638
+ isPathSpec = (spec, cwd) => spec.startsWith(".") || spec.startsWith("~") || isAbsolute2(spec) || existsSync14(resolve7(cwd, spec)) && !spec.startsWith("@");
7639
+ expand = (spec, cwd) => resolve7(cwd, expandHome(spec));
7640
+ InstallError = class extends Error {
7641
+ code;
7642
+ constructor(code, message) {
7643
+ super(message);
7644
+ this.code = code;
7645
+ }
7646
+ };
7647
+ PREVIEW_TTL_MS = 10 * 6e4;
7648
+ REGISTRY_SPEC = /^(@[^/@\s]+\/)?[^/@\s:]+(@[^\s/:]*)?$/;
7649
+ ModuleInstaller = class {
7650
+ opts;
7651
+ previews = /* @__PURE__ */ new Map();
7652
+ busy = false;
7653
+ constructor(opts) {
7654
+ this.opts = opts;
7655
+ }
7656
+ get now() {
7657
+ return (this.opts.now ?? Date.now)();
7658
+ }
7659
+ async preview(spec) {
7660
+ const cwd = this.opts.cwd ?? homedir7();
7661
+ const trimmed = spec.trim();
7662
+ if (!trimmed)
7663
+ throw new InstallError("bad_request", "Give an npm package, a folder or a git url");
7664
+ let preview;
7665
+ try {
7666
+ preview = await previewManifest(trimmed, { cwd, run: this.opts.run });
7667
+ } catch (err) {
7668
+ throw new InstallError("bad_request", err.message);
7669
+ }
7670
+ const m = preview.manifest;
7671
+ const pinned = isPathSpec(trimmed, cwd) ? expand(trimmed, cwd) : preview.packed && REGISTRY_SPEC.test(trimmed) ? `${preview.packed.name}@${preview.packed.version}` : trimmed;
7672
+ for (const [t, p] of this.previews)
7673
+ if (p.expires < this.now)
7674
+ this.previews.delete(t);
7675
+ const token = randomBytes2(24).toString("base64url");
7676
+ this.previews.set(token, { spec: pinned, name: m.name, expires: this.now + PREVIEW_TTL_MS });
7677
+ return {
7678
+ token,
7679
+ kind: preview.kind,
7680
+ name: m.name,
7681
+ version: m.version,
7682
+ displayName: m.displayName,
7683
+ description: "description" in m && typeof m.description === "string" ? m.description : void 0,
7684
+ spec: pinned,
7685
+ permissions: permissionLines(m),
7686
+ trustNote: TRUST_NOTE
7687
+ };
7688
+ }
7689
+ async install(token) {
7690
+ const p = this.previews.get(token);
7691
+ this.previews.delete(token);
7692
+ if (!p || p.expires < this.now)
7693
+ throw new InstallError("confirmation_required", "Preview the module again before installing it");
7694
+ return this.exclusive(async () => {
7695
+ mkdirSync5(this.opts.modulesDir, { recursive: true });
7696
+ const res = await this.opts.run("npm", installArgs(p.spec, this.opts.modulesDir), this.opts.modulesDir);
7697
+ if (res.code !== 0)
7698
+ throw new InstallError("install_failed", `npm install failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
7699
+ const list = await this.opts.host.scan();
7700
+ return list.find((s) => s.name === p.name);
7701
+ });
7702
+ }
7703
+ remove(id) {
7704
+ return this.exclusive(async () => {
7705
+ const found = this.opts.host.locate(id);
7706
+ if (!found)
7707
+ throw new InstallError("not_found", `No module ${id}`);
7708
+ if (!found.removable)
7709
+ throw new InstallError("bad_request", `${id} was not installed into Forge's modules folder; a dev folder unloads when forge module dev stops`);
7710
+ const packages2 = join17(this.opts.modulesDir, "node_modules") + sep5;
7711
+ if (found.dir.startsWith(packages2)) {
7712
+ const name = packageName(found.dir) ?? found.dir.slice(packages2.length);
7713
+ const res = await this.opts.run("npm", ["uninstall", "--prefix", this.opts.modulesDir, name], this.opts.modulesDir);
7714
+ if (res.code !== 0)
7715
+ throw new InstallError("install_failed", `npm uninstall failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
7716
+ } else {
7717
+ rmSync3(found.dir, { recursive: true, force: true });
7718
+ }
7719
+ await this.opts.host.scan();
7720
+ });
7721
+ }
7722
+ async exclusive(fn) {
7723
+ if (this.busy)
7724
+ throw new InstallError("busy", "Another module is being installed or removed");
7725
+ this.busy = true;
7726
+ try {
7727
+ return await fn();
7728
+ } finally {
7729
+ this.busy = false;
7730
+ }
7731
+ }
7732
+ };
7733
+ }
7734
+ });
7735
+
7537
7736
  // ../core/dist/module-routes.js
7538
7737
  import { readFile as readFile2 } from "node:fs/promises";
7539
- import { resolve as resolve7 } from "node:path";
7738
+ import { resolve as resolve8 } from "node:path";
7540
7739
  import { Hono as Hono5 } from "hono";
7541
7740
  function moduleRoutes(host, opts) {
7542
7741
  const { db } = opts;
@@ -7560,6 +7759,26 @@ function moduleRoutes(host, opts) {
7560
7759
  await host.removeDev(path);
7561
7760
  return c.json(host.list());
7562
7761
  });
7762
+ const installing = async (c, fn) => {
7763
+ if (!opts.installer)
7764
+ return c.json({ code: "local_only", message: "Modules are installed only in local mode" }, 403);
7765
+ try {
7766
+ return await fn(opts.installer);
7767
+ } catch (err) {
7768
+ if (err instanceof InstallError)
7769
+ return c.json({ code: err.code, message: err.message }, STATUS_BY_CODE[err.code]);
7770
+ throw err;
7771
+ }
7772
+ };
7773
+ app.post("/install/preview", (c) => installing(c, async (installer) => {
7774
+ const { spec } = await c.req.json();
7775
+ return c.json(await installer.preview(typeof spec === "string" ? spec : ""));
7776
+ }));
7777
+ app.post("/install", (c) => installing(c, async (installer) => {
7778
+ const { token } = await c.req.json();
7779
+ const status = await installer.install(typeof token === "string" ? token : "");
7780
+ return c.json({ status, modules: host.list() });
7781
+ }));
7563
7782
  app.get("/notifications", (c) => c.json({
7564
7783
  items: db.listNotifications().map((n) => ({
7565
7784
  moduleId: n.moduleId,
@@ -7581,6 +7800,10 @@ function moduleRoutes(host, opts) {
7581
7800
  await host.reload(c.req.param("id"));
7582
7801
  return c.json(host.list().find((s) => s.id === c.req.param("id")) ?? removed(c.req.param("id")));
7583
7802
  });
7803
+ app.delete("/:id", (c) => installing(c, async (installer) => {
7804
+ await installer.remove(c.req.param("id"));
7805
+ return c.json(host.list());
7806
+ }));
7584
7807
  app.get("/:id/status", (c) => {
7585
7808
  const status = host.list().find((s) => s.id === c.req.param("id"));
7586
7809
  return status ? c.json(status) : c.json(removed(c.req.param("id")), 404);
@@ -7605,7 +7828,7 @@ function moduleRoutes(host, opts) {
7605
7828
  if (!mod?.manifest.view)
7606
7829
  return c.json(removed(c.req.param("id")), 404);
7607
7830
  try {
7608
- const code = await readFile2(resolve7(mod.dir, mod.manifest.view.entry), "utf-8");
7831
+ const code = await readFile2(resolve8(mod.dir, mod.manifest.view.entry), "utf-8");
7609
7832
  return c.body(code, 200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache" });
7610
7833
  } catch (err) {
7611
7834
  return c.json({ code: "not_found", message: `view bundle missing: ${err.message}` }, 404);
@@ -7806,12 +8029,16 @@ var init_module_routes = __esm({
7806
8029
  init_dist();
7807
8030
  init_module_manifest();
7808
8031
  init_module_confirmations();
8032
+ init_module_install();
7809
8033
  STATUS_BY_CODE = {
7810
8034
  bad_request: 400,
7811
8035
  unauthorized: 401,
7812
8036
  not_found: 404,
7813
8037
  unreachable: 502,
7814
- upstream: 502
8038
+ upstream: 502,
8039
+ busy: 409,
8040
+ confirmation_required: 409,
8041
+ install_failed: 502
7815
8042
  };
7816
8043
  }
7817
8044
  });
@@ -7949,12 +8176,12 @@ var init_module_events = __esm({
7949
8176
  // ../core/dist/server.js
7950
8177
  import { Hono as Hono6 } from "hono";
7951
8178
  import { cors } from "hono/cors";
7952
- import { join as join17, basename as basename5, resolve as resolve8 } from "node:path";
7953
- import { readdirSync as readdirSync8, existsSync as existsSync14 } from "node:fs";
8179
+ import { join as join18, basename as basename5, resolve as resolve9 } from "node:path";
8180
+ import { readdirSync as readdirSync8, existsSync as existsSync15 } from "node:fs";
7954
8181
  function createForgeServer(options) {
7955
8182
  const { dataDir, db: externalDb, authToken, localOnly = true } = options;
7956
- const dbPath = join17(dataDir, "forge.db");
7957
- const modulesDir = join17(dataDir, "modules");
8183
+ const dbPath = join18(dataDir, "forge.db");
8184
+ const modulesDir = join18(dataDir, "modules");
7958
8185
  const db = externalDb ?? new ForgeDB(dbPath);
7959
8186
  const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
7960
8187
  const runner = new ActionRunner();
@@ -7967,7 +8194,7 @@ function createForgeServer(options) {
7967
8194
  const cwReader = new CWReader(options.cwDir);
7968
8195
  const loginManager = new LoginManager(resolveCwBin(cwReader.cwHome));
7969
8196
  app.route("/api/cw", cwRoutes(cwReader, { loginManager, localOnly, events: { emit: (name, payload) => moduleEvents.emit(name, payload) } }));
7970
- app.route("/api/skills", skillRoutes(cwReader, { backupDir: join17(dataDir, "skill-backups") }));
8197
+ app.route("/api/skills", skillRoutes(cwReader, { backupDir: join18(dataDir, "skill-backups") }));
7971
8198
  const { remote, shadow, warning } = remoteClassifierFromEnv();
7972
8199
  if (warning)
7973
8200
  console.warn(`[state] ${warning}`);
@@ -7987,7 +8214,7 @@ function createForgeServer(options) {
7987
8214
  }
7988
8215
  });
7989
8216
  const ptyManager = new PTYManager(states);
7990
- const generalSessions = new GeneralSessions(join17(dataDir, "general-sessions.json"));
8217
+ const generalSessions = new GeneralSessions(join18(dataDir, "general-sessions.json"));
7991
8218
  const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
7992
8219
  app.route("/api/liveframe", liveframeRoutes());
7993
8220
  const kioku = new Kioku({ projects: () => cwReader.getProjects() });
@@ -8021,7 +8248,7 @@ function createForgeServer(options) {
8021
8248
  modulesDir,
8022
8249
  bundledDir: options.bundledModulesDir,
8023
8250
  forgeNodeModules: options.forgeNodeModules,
8024
- devFile: join17(dataDir, "dev-modules.json"),
8251
+ devFile: join18(dataDir, "dev-modules.json"),
8025
8252
  localOnly,
8026
8253
  deps: { db, reader: cwReader, runner },
8027
8254
  hooks: {
@@ -8052,7 +8279,8 @@ function createForgeServer(options) {
8052
8279
  });
8053
8280
  ptyManager.onLifecycle = (event, session) => moduleEvents.emit(event === "started" ? "session.started" : "session.ended", sessionEvent(session));
8054
8281
  const modulesReady = moduleHost.scan().catch((err) => console.error(`[modules] scan failed: ${err.message}`));
8055
- app.route("/api/modules", moduleRoutes(moduleHost, { db, ready: modulesReady }));
8282
+ const installer = localOnly ? new ModuleInstaller({ modulesDir, host: moduleHost, run: createRunner(void 0, 5 * 6e4) }) : void 0;
8283
+ app.route("/api/modules", moduleRoutes(moduleHost, { db, ready: modulesReady, installer }));
8056
8284
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
8057
8285
  app.post("/api/cw/terminal/kill", async (c) => {
8058
8286
  const { project, sessionDir } = await c.req.json();
@@ -8184,25 +8412,25 @@ data: ${JSON.stringify(data)}
8184
8412
  });
8185
8413
  app.get("/api/filesystem/browse", (c) => {
8186
8414
  const home = process.env.HOME ?? "/tmp";
8187
- const dir = resolve8(c.req.query("path") ?? process.cwd());
8415
+ const dir = resolve9(c.req.query("path") ?? process.cwd());
8188
8416
  if (!dir.startsWith(home)) {
8189
8417
  return c.json({ error: "Access denied: path must be within home directory" }, 403);
8190
8418
  }
8191
- if (!existsSync14(dir)) {
8419
+ if (!existsSync15(dir)) {
8192
8420
  return c.json({ error: "Directory not found" }, 404);
8193
8421
  }
8194
8422
  try {
8195
8423
  const entries = readdirSync8(dir, { withFileTypes: true });
8196
8424
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
8197
8425
  name: e.name,
8198
- path: join17(dir, e.name),
8199
- hasPackageJson: existsSync14(join17(dir, e.name, "package.json")),
8200
- hasGit: existsSync14(join17(dir, e.name, ".git"))
8426
+ path: join18(dir, e.name),
8427
+ hasPackageJson: existsSync15(join18(dir, e.name, "package.json")),
8428
+ hasGit: existsSync15(join18(dir, e.name, ".git"))
8201
8429
  })).sort((a, b) => a.name.localeCompare(b.name));
8202
8430
  return c.json({
8203
8431
  current: dir,
8204
8432
  name: basename5(dir),
8205
- parent: join17(dir, ".."),
8433
+ parent: join18(dir, ".."),
8206
8434
  directories: dirs
8207
8435
  });
8208
8436
  } catch {
@@ -8249,6 +8477,8 @@ var init_server = __esm({
8249
8477
  init_kioku_routes();
8250
8478
  init_module_host();
8251
8479
  init_module_routes();
8480
+ init_module_install();
8481
+ init_task_review();
8252
8482
  init_module_jobs();
8253
8483
  init_module_events();
8254
8484
  init_kioku();
@@ -8415,22 +8645,22 @@ var init_db_factory = __esm({
8415
8645
  });
8416
8646
 
8417
8647
  // ../core/dist/init.js
8418
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
8419
- import { join as join18 } from "node:path";
8420
- import { homedir as homedir7 } from "node:os";
8648
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
8649
+ import { join as join19 } from "node:path";
8650
+ import { homedir as homedir8 } from "node:os";
8421
8651
  function getForgeDir() {
8422
- return join18(homedir7(), ".forge");
8652
+ return join19(homedir8(), ".forge");
8423
8653
  }
8424
8654
  function ensureForgeDir() {
8425
8655
  const forgeDir = getForgeDir();
8426
- if (existsSync15(forgeDir)) {
8656
+ if (existsSync16(forgeDir)) {
8427
8657
  return { forgeDir, created: false };
8428
8658
  }
8429
- mkdirSync5(forgeDir, { recursive: true });
8659
+ mkdirSync6(forgeDir, { recursive: true });
8430
8660
  for (const d of FORGE_SUBDIRS) {
8431
- mkdirSync5(join18(forgeDir, d), { recursive: true });
8661
+ mkdirSync6(join19(forgeDir, d), { recursive: true });
8432
8662
  }
8433
- writeFileSync6(join18(forgeDir, "config.json"), JSON.stringify({
8663
+ writeFileSync6(join19(forgeDir, "config.json"), JSON.stringify({
8434
8664
  port: 3e3,
8435
8665
  theme: "dark",
8436
8666
  openBrowser: true,
@@ -8492,68 +8722,6 @@ var init_port_in_use = __esm({
8492
8722
  }
8493
8723
  });
8494
8724
 
8495
- // ../core/dist/module-install.js
8496
- import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
8497
- import { tmpdir } from "node:os";
8498
- import { isAbsolute as isAbsolute2, join as join19, resolve as resolve9 } from "node:path";
8499
- async function previewManifest(spec, opts) {
8500
- if (isPathSpec(spec, opts.cwd))
8501
- return readFolder(expand(spec, opts.cwd));
8502
- const tmp = mkdtempSync(join19(tmpdir(), "forge-module-pack-"));
8503
- try {
8504
- const packed = await opts.run("npm", ["pack", spec, "--pack-destination", tmp, "--ignore-scripts", "--json"], opts.cwd);
8505
- if (packed.code !== 0)
8506
- throw new Error(`npm pack ${spec} failed: ${(packed.stderr || packed.stdout).trim()}`);
8507
- const filename = JSON.parse(packed.stdout)[0]?.filename;
8508
- if (!filename)
8509
- throw new Error(`npm pack ${spec} produced no tarball`);
8510
- const untar = await opts.run("tar", ["-xzf", join19(tmp, filename), "-C", tmp], opts.cwd);
8511
- if (untar.code !== 0)
8512
- throw new Error(`could not unpack ${filename}: ${untar.stderr.trim()}`);
8513
- return readFolder(join19(tmp, "package"));
8514
- } finally {
8515
- rmSync3(tmp, { recursive: true, force: true });
8516
- }
8517
- }
8518
- function readFolder(dir) {
8519
- const manifestFile = manifestFileFor(dir).file;
8520
- if (!existsSync16(manifestFile))
8521
- throw new Error(`${dir} has no forge-module.json`);
8522
- const raw = JSON.parse(readFileSync12(manifestFile, "utf-8"));
8523
- const check = checkManifest(raw, dir);
8524
- if (check.kind === "invalid")
8525
- throw new Error(`invalid manifest: ${check.reason}`);
8526
- return check;
8527
- }
8528
- function describePermissions(manifest) {
8529
- const p = "permissions" in manifest ? manifest.permissions ?? {} : {};
8530
- const lines2 = [];
8531
- for (const key of ["network", "fs:read", "fs:write", "shell"]) {
8532
- for (const value of p[key] ?? [])
8533
- lines2.push(` ${key.padEnd(9)} ${value}`);
8534
- }
8535
- if (p.pty)
8536
- lines2.push(` ${"pty".padEnd(9)} yes`);
8537
- const legacy = "actions" in manifest && (manifest.actions ?? []).some((a) => "command" in a);
8538
- if (legacy)
8539
- lines2.push(" shell runs the shell commands in its manifest");
8540
- return [lines2.length ? "It asks for:" : "It asks for no permissions.", ...lines2, "", TRUST_NOTE].join("\n");
8541
- }
8542
- function installArgs(spec, prefix) {
8543
- return ["install", "--ignore-scripts", "--prefix", prefix, spec];
8544
- }
8545
- var TRUST_NOTE, isPathSpec, expand;
8546
- var init_module_install = __esm({
8547
- "../core/dist/module-install.js"() {
8548
- "use strict";
8549
- init_module_manifest();
8550
- init_state_log();
8551
- TRUST_NOTE = "Modules run inside Forge with your access. Forge checks every call against these permissions, but it is not a sandbox: install only modules you trust.";
8552
- isPathSpec = (spec, cwd) => spec.startsWith(".") || spec.startsWith("~") || isAbsolute2(spec) || existsSync16(resolve9(cwd, spec)) && !spec.startsWith("@");
8553
- expand = (spec, cwd) => resolve9(cwd, expandHome(spec));
8554
- }
8555
- });
8556
-
8557
8725
  // ../core/dist/index.js
8558
8726
  var dist_exports = {};
8559
8727
  __export(dist_exports, {
@@ -8630,7 +8798,7 @@ var init_dist2 = __esm({
8630
8798
 
8631
8799
  // src/index.ts
8632
8800
  import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
8633
- import { homedir as homedir8 } from "node:os";
8801
+ import { homedir as homedir9 } from "node:os";
8634
8802
  import { join as join20 } from "node:path";
8635
8803
  function parseArgs(argv) {
8636
8804
  const out = { open: true };
@@ -8655,7 +8823,7 @@ async function main() {
8655
8823
  if (created) {
8656
8824
  console.log("First run \u2014 initialized Forge.");
8657
8825
  }
8658
- const cwHome = join20(process.env.HOME ?? homedir8(), ".cw");
8826
+ const cwHome = join20(process.env.HOME ?? homedir9(), ".cw");
8659
8827
  const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join20(packageRoot, "cw") });
8660
8828
  if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
8661
8829
  else if (cw.action === "updated") console.log(` Updated ${cw.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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",