forge-cw 0.1.13 → 0.1.15
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/console/assets/{index-GXv21xGs.css → index-DPf7M8cb.css} +1 -1
- package/console/assets/index-m_7gobK_.js +31 -0
- package/console/index.html +2 -2
- package/console/shared/forge-dev-ui.js +2 -2
- package/dist/index.js +309 -101
- package/package.json +3 -3
- package/console/assets/index-CC7_QwVk.js +0 -31
package/dist/index.js
CHANGED
|
@@ -2657,6 +2657,40 @@ function splitPluginId(id) {
|
|
|
2657
2657
|
const at = id.lastIndexOf("@");
|
|
2658
2658
|
return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
|
|
2659
2659
|
}
|
|
2660
|
+
function catalogVersion(configDir, marketplaces, marketplace, name) {
|
|
2661
|
+
const known = marketplaces[marketplace];
|
|
2662
|
+
const location = isRecord(known) && typeof known["installLocation"] === "string" ? known["installLocation"] : join8(configDir, "plugins", "marketplaces", marketplace);
|
|
2663
|
+
const entries = readJson(join8(location, ".claude-plugin", "marketplace.json"))["plugins"];
|
|
2664
|
+
if (!Array.isArray(entries))
|
|
2665
|
+
return void 0;
|
|
2666
|
+
const entry = entries.find((e) => isRecord(e) && e["name"] === name);
|
|
2667
|
+
if (!entry)
|
|
2668
|
+
return void 0;
|
|
2669
|
+
if (typeof entry["version"] === "string")
|
|
2670
|
+
return entry["version"];
|
|
2671
|
+
const source = entry["source"];
|
|
2672
|
+
if (typeof source !== "string")
|
|
2673
|
+
return void 0;
|
|
2674
|
+
const dir = resolve2(location, source);
|
|
2675
|
+
if (!inside(location, dir))
|
|
2676
|
+
return void 0;
|
|
2677
|
+
const version = readJson(join8(dir, ".claude-plugin", "plugin.json"))["version"];
|
|
2678
|
+
return typeof version === "string" ? version : void 0;
|
|
2679
|
+
}
|
|
2680
|
+
function hasUpdate({ latest, version }) {
|
|
2681
|
+
if (!latest || latest === version)
|
|
2682
|
+
return false;
|
|
2683
|
+
const a = numeric(latest);
|
|
2684
|
+
const b = numeric(version);
|
|
2685
|
+
if (!a || !b)
|
|
2686
|
+
return true;
|
|
2687
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
2688
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
2689
|
+
if (d !== 0)
|
|
2690
|
+
return d > 0;
|
|
2691
|
+
}
|
|
2692
|
+
return false;
|
|
2693
|
+
}
|
|
2660
2694
|
function projectSkillNames(projectPath) {
|
|
2661
2695
|
const dir = join8(projectPath, ".claude", "skills");
|
|
2662
2696
|
if (!existsSync6(dir))
|
|
@@ -2678,16 +2712,21 @@ async function listPlugins(reader, remoteOf) {
|
|
|
2678
2712
|
const user = entries.find((e) => isRecord(e) && e["scope"] === "user" && typeof e["installPath"] === "string");
|
|
2679
2713
|
if (!user)
|
|
2680
2714
|
continue;
|
|
2715
|
+
const { name, marketplace } = splitPluginId(id);
|
|
2716
|
+
const version = String(user["version"] ?? "");
|
|
2717
|
+
const latest = catalogVersion(configDir, marketplaces, marketplace, name);
|
|
2681
2718
|
const install = {
|
|
2682
2719
|
scope,
|
|
2683
2720
|
scopeRef,
|
|
2684
|
-
version
|
|
2721
|
+
version,
|
|
2685
2722
|
enabled: !isRecord(enabledPlugins) || enabledPlugins[id] !== false,
|
|
2686
2723
|
installPath: String(user["installPath"]),
|
|
2687
|
-
lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0
|
|
2724
|
+
lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0,
|
|
2725
|
+
latest,
|
|
2726
|
+
update: hasUpdate({ latest, version })
|
|
2688
2727
|
};
|
|
2689
2728
|
const list = found.get(id) ?? [];
|
|
2690
|
-
list.push({ install, repo: marketplaceSource(marketplaces[
|
|
2729
|
+
list.push({ install, repo: marketplaceSource(marketplaces[marketplace]) });
|
|
2691
2730
|
found.set(id, list);
|
|
2692
2731
|
}
|
|
2693
2732
|
}
|
|
@@ -2757,7 +2796,7 @@ function marketplaceSourceOf(reader, marketplace) {
|
|
|
2757
2796
|
}
|
|
2758
2797
|
return void 0;
|
|
2759
2798
|
}
|
|
2760
|
-
var isRecord, inside, pluginScopes, knownMarketplaces, knowsMarketplace;
|
|
2799
|
+
var isRecord, inside, pluginScopes, knownMarketplaces, numeric, knowsMarketplace;
|
|
2761
2800
|
var init_plugins = __esm({
|
|
2762
2801
|
"../core/dist/plugins.js"() {
|
|
2763
2802
|
"use strict";
|
|
@@ -2772,6 +2811,7 @@ var init_plugins = __esm({
|
|
|
2772
2811
|
...reader.getAccounts().map((a) => ({ scope: "account", scopeRef: a }))
|
|
2773
2812
|
];
|
|
2774
2813
|
knownMarketplaces = (configDir) => readJson(join8(configDir, "plugins", "known_marketplaces.json"));
|
|
2814
|
+
numeric = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
|
|
2775
2815
|
knowsMarketplace = (configDir, marketplace) => isRecord(knownMarketplaces(configDir)[marketplace]);
|
|
2776
2816
|
}
|
|
2777
2817
|
});
|
|
@@ -4896,13 +4936,13 @@ var require_parse_options = __commonJS({
|
|
|
4896
4936
|
var require_identifiers = __commonJS({
|
|
4897
4937
|
"../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/internal/identifiers.js"(exports, module) {
|
|
4898
4938
|
"use strict";
|
|
4899
|
-
var
|
|
4939
|
+
var numeric2 = /^[0-9]+$/;
|
|
4900
4940
|
var compareIdentifiers = (a, b) => {
|
|
4901
4941
|
if (typeof a === "number" && typeof b === "number") {
|
|
4902
4942
|
return a === b ? 0 : a < b ? -1 : 1;
|
|
4903
4943
|
}
|
|
4904
|
-
const anum =
|
|
4905
|
-
const bnum =
|
|
4944
|
+
const anum = numeric2.test(a);
|
|
4945
|
+
const bnum = numeric2.test(b);
|
|
4906
4946
|
if (anum && bnum) {
|
|
4907
4947
|
a = +a;
|
|
4908
4948
|
b = +b;
|
|
@@ -7173,7 +7213,7 @@ var init_module_context = __esm({
|
|
|
7173
7213
|
|
|
7174
7214
|
// ../core/dist/module-host.js
|
|
7175
7215
|
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";
|
|
7216
|
+
import { basename as basename4, dirname as dirname7, join as join16, resolve as resolve6, sep as sep4 } from "node:path";
|
|
7177
7217
|
import { pathToFileURL } from "node:url";
|
|
7178
7218
|
function disabled(id, c, reason, m) {
|
|
7179
7219
|
const text = (v, fallback) => typeof v === "string" && v ? v : fallback;
|
|
@@ -7268,6 +7308,7 @@ var init_module_host = __esm({
|
|
|
7268
7308
|
opts;
|
|
7269
7309
|
statuses = [];
|
|
7270
7310
|
loaded = /* @__PURE__ */ new Map();
|
|
7311
|
+
origins = /* @__PURE__ */ new Map();
|
|
7271
7312
|
constructor(opts) {
|
|
7272
7313
|
this.opts = opts;
|
|
7273
7314
|
}
|
|
@@ -7277,6 +7318,16 @@ var init_module_host = __esm({
|
|
|
7277
7318
|
get(id) {
|
|
7278
7319
|
return this.loaded.get(id);
|
|
7279
7320
|
}
|
|
7321
|
+
// where a listed module was found, loaded or not; the first one found wins
|
|
7322
|
+
locate(id) {
|
|
7323
|
+
const c = this.origins.get(id);
|
|
7324
|
+
return c && { dir: c.dir, removable: this.removable(c) };
|
|
7325
|
+
}
|
|
7326
|
+
// only what was installed into the modules folder; bundled, dev and Forge's own packages stay
|
|
7327
|
+
removable(c) {
|
|
7328
|
+
const { modulesDir } = this.opts;
|
|
7329
|
+
return c.source === "installed" && dirname7(c.dir) === modulesDir || c.dir.startsWith(join16(modulesDir, "node_modules") + sep4);
|
|
7330
|
+
}
|
|
7280
7331
|
// scans, reloads and dev changes run one at a time: each one awaits imports midway
|
|
7281
7332
|
queue = Promise.resolve();
|
|
7282
7333
|
serial(fn) {
|
|
@@ -7293,12 +7344,17 @@ var init_module_host = __esm({
|
|
|
7293
7344
|
this.unload(id);
|
|
7294
7345
|
const statuses = [];
|
|
7295
7346
|
const seen = /* @__PURE__ */ new Map();
|
|
7347
|
+
const origins = /* @__PURE__ */ new Map();
|
|
7296
7348
|
for (const candidate of this.candidates()) {
|
|
7297
|
-
const status = await this.
|
|
7298
|
-
if (status)
|
|
7299
|
-
|
|
7349
|
+
const status = await this.load(candidate, seen);
|
|
7350
|
+
if (!status)
|
|
7351
|
+
continue;
|
|
7352
|
+
statuses.push(status);
|
|
7353
|
+
if (!origins.has(status.id))
|
|
7354
|
+
origins.set(status.id, candidate);
|
|
7300
7355
|
}
|
|
7301
7356
|
this.statuses = statuses;
|
|
7357
|
+
this.origins = origins;
|
|
7302
7358
|
return statuses;
|
|
7303
7359
|
}
|
|
7304
7360
|
// only this module is unloaded and loaded again; the others keep running
|
|
@@ -7312,7 +7368,7 @@ var init_module_host = __esm({
|
|
|
7312
7368
|
} else {
|
|
7313
7369
|
this.unload(id);
|
|
7314
7370
|
const seen = new Map([...this.loaded.values()].map((m) => [m.status.id, m.dir]));
|
|
7315
|
-
const status = await this.
|
|
7371
|
+
const status = await this.load(mod.candidate, seen);
|
|
7316
7372
|
this.statuses = this.statuses.flatMap((s) => s === mod.status ? status ? [status] : [] : [s]);
|
|
7317
7373
|
}
|
|
7318
7374
|
}
|
|
@@ -7394,6 +7450,12 @@ var init_module_host = __esm({
|
|
|
7394
7450
|
out.push(...packages(this.opts.forgeNodeModules, "node_modules", false));
|
|
7395
7451
|
return out;
|
|
7396
7452
|
}
|
|
7453
|
+
async load(c, seen) {
|
|
7454
|
+
const status = await this.loadCandidate(c, seen);
|
|
7455
|
+
if (status && this.removable(c))
|
|
7456
|
+
status.removable = true;
|
|
7457
|
+
return status;
|
|
7458
|
+
}
|
|
7397
7459
|
async loadCandidate(c, seen) {
|
|
7398
7460
|
const fallbackId = basename4(c.dir);
|
|
7399
7461
|
if (!existsSync13(c.manifestFile)) {
|
|
@@ -7534,9 +7596,186 @@ var init_module_confirmations = __esm({
|
|
|
7534
7596
|
}
|
|
7535
7597
|
});
|
|
7536
7598
|
|
|
7599
|
+
// ../core/dist/module-install.js
|
|
7600
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
7601
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
|
|
7602
|
+
import { homedir as homedir7, tmpdir } from "node:os";
|
|
7603
|
+
import { isAbsolute as isAbsolute2, join as join17, resolve as resolve7, sep as sep5 } from "node:path";
|
|
7604
|
+
async function previewManifest(spec, opts) {
|
|
7605
|
+
if (isPathSpec(spec, opts.cwd))
|
|
7606
|
+
return readFolder(expand(spec, opts.cwd));
|
|
7607
|
+
const tmp = mkdtempSync(join17(tmpdir(), "forge-module-pack-"));
|
|
7608
|
+
try {
|
|
7609
|
+
const packed = await opts.run("npm", ["pack", spec, "--pack-destination", tmp, "--ignore-scripts", "--json"], opts.cwd);
|
|
7610
|
+
if (packed.code !== 0)
|
|
7611
|
+
throw new Error(npmPackError(spec, packed.stderr || packed.stdout));
|
|
7612
|
+
const info = JSON.parse(packed.stdout)[0];
|
|
7613
|
+
const filename = info?.filename;
|
|
7614
|
+
if (!filename)
|
|
7615
|
+
throw new Error(`npm pack ${spec} produced no tarball`);
|
|
7616
|
+
const untar = await opts.run("tar", ["-xzf", join17(tmp, filename), "-C", tmp], opts.cwd);
|
|
7617
|
+
if (untar.code !== 0)
|
|
7618
|
+
throw new Error(`could not unpack ${filename}: ${untar.stderr.trim()}`);
|
|
7619
|
+
const preview = readFolder(join17(tmp, "package"));
|
|
7620
|
+
return info.name && info.version ? { ...preview, packed: { name: info.name, version: info.version } } : preview;
|
|
7621
|
+
} finally {
|
|
7622
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
7623
|
+
}
|
|
7624
|
+
}
|
|
7625
|
+
function npmPackError(spec, output) {
|
|
7626
|
+
if (/\bE404\b/.test(output))
|
|
7627
|
+
return `${spec} is not on npm`;
|
|
7628
|
+
const lines2 = output.trim().split("\n").filter((l) => !l.includes("A complete log of this run"));
|
|
7629
|
+
return `npm pack ${spec} failed: ${lines2.join("\n")}`;
|
|
7630
|
+
}
|
|
7631
|
+
function readFolder(dir) {
|
|
7632
|
+
const manifestFile = manifestFileFor(dir).file;
|
|
7633
|
+
if (!existsSync14(manifestFile))
|
|
7634
|
+
throw new Error(`${dir} has no forge-module.json`);
|
|
7635
|
+
const raw = JSON.parse(readFileSync12(manifestFile, "utf-8"));
|
|
7636
|
+
const check = checkManifest(raw, dir);
|
|
7637
|
+
if (check.kind === "invalid")
|
|
7638
|
+
throw new Error(`invalid manifest: ${check.reason}`);
|
|
7639
|
+
return check;
|
|
7640
|
+
}
|
|
7641
|
+
function permissionLines(manifest) {
|
|
7642
|
+
const p = "permissions" in manifest ? manifest.permissions ?? {} : {};
|
|
7643
|
+
const lines2 = [];
|
|
7644
|
+
for (const kind of ["network", "fs:read", "fs:write", "shell"]) {
|
|
7645
|
+
for (const value of p[kind] ?? [])
|
|
7646
|
+
lines2.push({ kind, value });
|
|
7647
|
+
}
|
|
7648
|
+
if (p.pty)
|
|
7649
|
+
lines2.push({ kind: "pty", value: "yes" });
|
|
7650
|
+
const legacy = "actions" in manifest && (manifest.actions ?? []).some((a) => "command" in a);
|
|
7651
|
+
if (legacy)
|
|
7652
|
+
lines2.push({ kind: "shell", value: "runs the shell commands in its manifest" });
|
|
7653
|
+
return lines2;
|
|
7654
|
+
}
|
|
7655
|
+
function describePermissions(manifest) {
|
|
7656
|
+
const lines2 = permissionLines(manifest).map((l) => ` ${l.kind.padEnd(9)} ${l.value}`);
|
|
7657
|
+
return [lines2.length ? "It asks for:" : "It asks for no permissions.", ...lines2, "", TRUST_NOTE].join("\n");
|
|
7658
|
+
}
|
|
7659
|
+
function installArgs(spec, prefix) {
|
|
7660
|
+
return ["install", "--ignore-scripts", "--prefix", prefix, spec];
|
|
7661
|
+
}
|
|
7662
|
+
function packageName(dir) {
|
|
7663
|
+
try {
|
|
7664
|
+
const name = JSON.parse(readFileSync12(join17(dir, "package.json"), "utf-8")).name;
|
|
7665
|
+
return typeof name === "string" ? name : void 0;
|
|
7666
|
+
} catch {
|
|
7667
|
+
return void 0;
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
var TRUST_NOTE, isPathSpec, expand, InstallError, PREVIEW_TTL_MS, REGISTRY_SPEC, ModuleInstaller;
|
|
7671
|
+
var init_module_install = __esm({
|
|
7672
|
+
"../core/dist/module-install.js"() {
|
|
7673
|
+
"use strict";
|
|
7674
|
+
init_module_manifest();
|
|
7675
|
+
init_output();
|
|
7676
|
+
init_state_log();
|
|
7677
|
+
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.";
|
|
7678
|
+
isPathSpec = (spec, cwd) => spec.startsWith(".") || spec.startsWith("~") || isAbsolute2(spec) || existsSync14(resolve7(cwd, spec)) && !spec.startsWith("@");
|
|
7679
|
+
expand = (spec, cwd) => resolve7(cwd, expandHome(spec));
|
|
7680
|
+
InstallError = class extends Error {
|
|
7681
|
+
code;
|
|
7682
|
+
constructor(code, message) {
|
|
7683
|
+
super(message);
|
|
7684
|
+
this.code = code;
|
|
7685
|
+
}
|
|
7686
|
+
};
|
|
7687
|
+
PREVIEW_TTL_MS = 10 * 6e4;
|
|
7688
|
+
REGISTRY_SPEC = /^(@[^/@\s]+\/)?[^/@\s:]+(@[^\s/:]*)?$/;
|
|
7689
|
+
ModuleInstaller = class {
|
|
7690
|
+
opts;
|
|
7691
|
+
previews = /* @__PURE__ */ new Map();
|
|
7692
|
+
busy = false;
|
|
7693
|
+
constructor(opts) {
|
|
7694
|
+
this.opts = opts;
|
|
7695
|
+
}
|
|
7696
|
+
get now() {
|
|
7697
|
+
return (this.opts.now ?? Date.now)();
|
|
7698
|
+
}
|
|
7699
|
+
async preview(spec) {
|
|
7700
|
+
const cwd = this.opts.cwd ?? homedir7();
|
|
7701
|
+
const trimmed = spec.trim();
|
|
7702
|
+
if (!trimmed)
|
|
7703
|
+
throw new InstallError("bad_request", "Give an npm package, a folder or a git url");
|
|
7704
|
+
let preview;
|
|
7705
|
+
try {
|
|
7706
|
+
preview = await previewManifest(trimmed, { cwd, run: this.opts.run });
|
|
7707
|
+
} catch (err) {
|
|
7708
|
+
throw new InstallError("bad_request", err.message);
|
|
7709
|
+
}
|
|
7710
|
+
const m = preview.manifest;
|
|
7711
|
+
const pinned = isPathSpec(trimmed, cwd) ? expand(trimmed, cwd) : preview.packed && REGISTRY_SPEC.test(trimmed) ? `${preview.packed.name}@${preview.packed.version}` : trimmed;
|
|
7712
|
+
for (const [t, p] of this.previews)
|
|
7713
|
+
if (p.expires < this.now)
|
|
7714
|
+
this.previews.delete(t);
|
|
7715
|
+
const token = randomBytes2(24).toString("base64url");
|
|
7716
|
+
this.previews.set(token, { spec: pinned, name: m.name, expires: this.now + PREVIEW_TTL_MS });
|
|
7717
|
+
return {
|
|
7718
|
+
token,
|
|
7719
|
+
kind: preview.kind,
|
|
7720
|
+
name: m.name,
|
|
7721
|
+
version: m.version,
|
|
7722
|
+
displayName: m.displayName,
|
|
7723
|
+
description: "description" in m && typeof m.description === "string" ? m.description : void 0,
|
|
7724
|
+
spec: pinned,
|
|
7725
|
+
permissions: permissionLines(m),
|
|
7726
|
+
trustNote: TRUST_NOTE
|
|
7727
|
+
};
|
|
7728
|
+
}
|
|
7729
|
+
async install(token) {
|
|
7730
|
+
const p = this.previews.get(token);
|
|
7731
|
+
this.previews.delete(token);
|
|
7732
|
+
if (!p || p.expires < this.now)
|
|
7733
|
+
throw new InstallError("confirmation_required", "Preview the module again before installing it");
|
|
7734
|
+
return this.exclusive(async () => {
|
|
7735
|
+
mkdirSync5(this.opts.modulesDir, { recursive: true });
|
|
7736
|
+
const res = await this.opts.run("npm", installArgs(p.spec, this.opts.modulesDir), this.opts.modulesDir);
|
|
7737
|
+
if (res.code !== 0)
|
|
7738
|
+
throw new InstallError("install_failed", `npm install failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
|
|
7739
|
+
const list = await this.opts.host.scan();
|
|
7740
|
+
return list.find((s) => s.name === p.name);
|
|
7741
|
+
});
|
|
7742
|
+
}
|
|
7743
|
+
remove(id) {
|
|
7744
|
+
return this.exclusive(async () => {
|
|
7745
|
+
const found = this.opts.host.locate(id);
|
|
7746
|
+
if (!found)
|
|
7747
|
+
throw new InstallError("not_found", `No module ${id}`);
|
|
7748
|
+
if (!found.removable)
|
|
7749
|
+
throw new InstallError("bad_request", `${id} was not installed into Forge's modules folder; a dev folder unloads when forge module dev stops`);
|
|
7750
|
+
const packages2 = join17(this.opts.modulesDir, "node_modules") + sep5;
|
|
7751
|
+
if (found.dir.startsWith(packages2)) {
|
|
7752
|
+
const name = packageName(found.dir) ?? found.dir.slice(packages2.length);
|
|
7753
|
+
const res = await this.opts.run("npm", ["uninstall", "--prefix", this.opts.modulesDir, name], this.opts.modulesDir);
|
|
7754
|
+
if (res.code !== 0)
|
|
7755
|
+
throw new InstallError("install_failed", `npm uninstall failed: ${tailOutput(res.stderr || res.stdout, 6)}`);
|
|
7756
|
+
} else {
|
|
7757
|
+
rmSync3(found.dir, { recursive: true, force: true });
|
|
7758
|
+
}
|
|
7759
|
+
await this.opts.host.scan();
|
|
7760
|
+
});
|
|
7761
|
+
}
|
|
7762
|
+
async exclusive(fn) {
|
|
7763
|
+
if (this.busy)
|
|
7764
|
+
throw new InstallError("busy", "Another module is being installed or removed");
|
|
7765
|
+
this.busy = true;
|
|
7766
|
+
try {
|
|
7767
|
+
return await fn();
|
|
7768
|
+
} finally {
|
|
7769
|
+
this.busy = false;
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7772
|
+
};
|
|
7773
|
+
}
|
|
7774
|
+
});
|
|
7775
|
+
|
|
7537
7776
|
// ../core/dist/module-routes.js
|
|
7538
7777
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
7539
|
-
import { resolve as
|
|
7778
|
+
import { resolve as resolve8 } from "node:path";
|
|
7540
7779
|
import { Hono as Hono5 } from "hono";
|
|
7541
7780
|
function moduleRoutes(host, opts) {
|
|
7542
7781
|
const { db } = opts;
|
|
@@ -7560,6 +7799,26 @@ function moduleRoutes(host, opts) {
|
|
|
7560
7799
|
await host.removeDev(path);
|
|
7561
7800
|
return c.json(host.list());
|
|
7562
7801
|
});
|
|
7802
|
+
const installing = async (c, fn) => {
|
|
7803
|
+
if (!opts.installer)
|
|
7804
|
+
return c.json({ code: "local_only", message: "Modules are installed only in local mode" }, 403);
|
|
7805
|
+
try {
|
|
7806
|
+
return await fn(opts.installer);
|
|
7807
|
+
} catch (err) {
|
|
7808
|
+
if (err instanceof InstallError)
|
|
7809
|
+
return c.json({ code: err.code, message: err.message }, STATUS_BY_CODE[err.code]);
|
|
7810
|
+
throw err;
|
|
7811
|
+
}
|
|
7812
|
+
};
|
|
7813
|
+
app.post("/install/preview", (c) => installing(c, async (installer) => {
|
|
7814
|
+
const { spec } = await c.req.json();
|
|
7815
|
+
return c.json(await installer.preview(typeof spec === "string" ? spec : ""));
|
|
7816
|
+
}));
|
|
7817
|
+
app.post("/install", (c) => installing(c, async (installer) => {
|
|
7818
|
+
const { token } = await c.req.json();
|
|
7819
|
+
const status = await installer.install(typeof token === "string" ? token : "");
|
|
7820
|
+
return c.json({ status, modules: host.list() });
|
|
7821
|
+
}));
|
|
7563
7822
|
app.get("/notifications", (c) => c.json({
|
|
7564
7823
|
items: db.listNotifications().map((n) => ({
|
|
7565
7824
|
moduleId: n.moduleId,
|
|
@@ -7581,6 +7840,10 @@ function moduleRoutes(host, opts) {
|
|
|
7581
7840
|
await host.reload(c.req.param("id"));
|
|
7582
7841
|
return c.json(host.list().find((s) => s.id === c.req.param("id")) ?? removed(c.req.param("id")));
|
|
7583
7842
|
});
|
|
7843
|
+
app.delete("/:id", (c) => installing(c, async (installer) => {
|
|
7844
|
+
await installer.remove(c.req.param("id"));
|
|
7845
|
+
return c.json(host.list());
|
|
7846
|
+
}));
|
|
7584
7847
|
app.get("/:id/status", (c) => {
|
|
7585
7848
|
const status = host.list().find((s) => s.id === c.req.param("id"));
|
|
7586
7849
|
return status ? c.json(status) : c.json(removed(c.req.param("id")), 404);
|
|
@@ -7605,7 +7868,7 @@ function moduleRoutes(host, opts) {
|
|
|
7605
7868
|
if (!mod?.manifest.view)
|
|
7606
7869
|
return c.json(removed(c.req.param("id")), 404);
|
|
7607
7870
|
try {
|
|
7608
|
-
const code = await readFile2(
|
|
7871
|
+
const code = await readFile2(resolve8(mod.dir, mod.manifest.view.entry), "utf-8");
|
|
7609
7872
|
return c.body(code, 200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
7610
7873
|
} catch (err) {
|
|
7611
7874
|
return c.json({ code: "not_found", message: `view bundle missing: ${err.message}` }, 404);
|
|
@@ -7806,12 +8069,16 @@ var init_module_routes = __esm({
|
|
|
7806
8069
|
init_dist();
|
|
7807
8070
|
init_module_manifest();
|
|
7808
8071
|
init_module_confirmations();
|
|
8072
|
+
init_module_install();
|
|
7809
8073
|
STATUS_BY_CODE = {
|
|
7810
8074
|
bad_request: 400,
|
|
7811
8075
|
unauthorized: 401,
|
|
7812
8076
|
not_found: 404,
|
|
7813
8077
|
unreachable: 502,
|
|
7814
|
-
upstream: 502
|
|
8078
|
+
upstream: 502,
|
|
8079
|
+
busy: 409,
|
|
8080
|
+
confirmation_required: 409,
|
|
8081
|
+
install_failed: 502
|
|
7815
8082
|
};
|
|
7816
8083
|
}
|
|
7817
8084
|
});
|
|
@@ -7949,12 +8216,12 @@ var init_module_events = __esm({
|
|
|
7949
8216
|
// ../core/dist/server.js
|
|
7950
8217
|
import { Hono as Hono6 } from "hono";
|
|
7951
8218
|
import { cors } from "hono/cors";
|
|
7952
|
-
import { join as
|
|
7953
|
-
import { readdirSync as readdirSync8, existsSync as
|
|
8219
|
+
import { join as join18, basename as basename5, resolve as resolve9 } from "node:path";
|
|
8220
|
+
import { readdirSync as readdirSync8, existsSync as existsSync15 } from "node:fs";
|
|
7954
8221
|
function createForgeServer(options) {
|
|
7955
8222
|
const { dataDir, db: externalDb, authToken, localOnly = true } = options;
|
|
7956
|
-
const dbPath =
|
|
7957
|
-
const modulesDir =
|
|
8223
|
+
const dbPath = join18(dataDir, "forge.db");
|
|
8224
|
+
const modulesDir = join18(dataDir, "modules");
|
|
7958
8225
|
const db = externalDb ?? new ForgeDB(dbPath);
|
|
7959
8226
|
const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
|
|
7960
8227
|
const runner = new ActionRunner();
|
|
@@ -7967,7 +8234,7 @@ function createForgeServer(options) {
|
|
|
7967
8234
|
const cwReader = new CWReader(options.cwDir);
|
|
7968
8235
|
const loginManager = new LoginManager(resolveCwBin(cwReader.cwHome));
|
|
7969
8236
|
app.route("/api/cw", cwRoutes(cwReader, { loginManager, localOnly, events: { emit: (name, payload) => moduleEvents.emit(name, payload) } }));
|
|
7970
|
-
app.route("/api/skills", skillRoutes(cwReader, { backupDir:
|
|
8237
|
+
app.route("/api/skills", skillRoutes(cwReader, { backupDir: join18(dataDir, "skill-backups") }));
|
|
7971
8238
|
const { remote, shadow, warning } = remoteClassifierFromEnv();
|
|
7972
8239
|
if (warning)
|
|
7973
8240
|
console.warn(`[state] ${warning}`);
|
|
@@ -7987,7 +8254,7 @@ function createForgeServer(options) {
|
|
|
7987
8254
|
}
|
|
7988
8255
|
});
|
|
7989
8256
|
const ptyManager = new PTYManager(states);
|
|
7990
|
-
const generalSessions = new GeneralSessions(
|
|
8257
|
+
const generalSessions = new GeneralSessions(join18(dataDir, "general-sessions.json"));
|
|
7991
8258
|
const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
|
|
7992
8259
|
app.route("/api/liveframe", liveframeRoutes());
|
|
7993
8260
|
const kioku = new Kioku({ projects: () => cwReader.getProjects() });
|
|
@@ -8021,7 +8288,7 @@ function createForgeServer(options) {
|
|
|
8021
8288
|
modulesDir,
|
|
8022
8289
|
bundledDir: options.bundledModulesDir,
|
|
8023
8290
|
forgeNodeModules: options.forgeNodeModules,
|
|
8024
|
-
devFile:
|
|
8291
|
+
devFile: join18(dataDir, "dev-modules.json"),
|
|
8025
8292
|
localOnly,
|
|
8026
8293
|
deps: { db, reader: cwReader, runner },
|
|
8027
8294
|
hooks: {
|
|
@@ -8052,7 +8319,8 @@ function createForgeServer(options) {
|
|
|
8052
8319
|
});
|
|
8053
8320
|
ptyManager.onLifecycle = (event, session) => moduleEvents.emit(event === "started" ? "session.started" : "session.ended", sessionEvent(session));
|
|
8054
8321
|
const modulesReady = moduleHost.scan().catch((err) => console.error(`[modules] scan failed: ${err.message}`));
|
|
8055
|
-
|
|
8322
|
+
const installer = localOnly ? new ModuleInstaller({ modulesDir, host: moduleHost, run: createRunner(void 0, 5 * 6e4) }) : void 0;
|
|
8323
|
+
app.route("/api/modules", moduleRoutes(moduleHost, { db, ready: modulesReady, installer }));
|
|
8056
8324
|
app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
|
|
8057
8325
|
app.post("/api/cw/terminal/kill", async (c) => {
|
|
8058
8326
|
const { project, sessionDir } = await c.req.json();
|
|
@@ -8184,25 +8452,25 @@ data: ${JSON.stringify(data)}
|
|
|
8184
8452
|
});
|
|
8185
8453
|
app.get("/api/filesystem/browse", (c) => {
|
|
8186
8454
|
const home = process.env.HOME ?? "/tmp";
|
|
8187
|
-
const dir =
|
|
8455
|
+
const dir = resolve9(c.req.query("path") ?? process.cwd());
|
|
8188
8456
|
if (!dir.startsWith(home)) {
|
|
8189
8457
|
return c.json({ error: "Access denied: path must be within home directory" }, 403);
|
|
8190
8458
|
}
|
|
8191
|
-
if (!
|
|
8459
|
+
if (!existsSync15(dir)) {
|
|
8192
8460
|
return c.json({ error: "Directory not found" }, 404);
|
|
8193
8461
|
}
|
|
8194
8462
|
try {
|
|
8195
8463
|
const entries = readdirSync8(dir, { withFileTypes: true });
|
|
8196
8464
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
|
|
8197
8465
|
name: e.name,
|
|
8198
|
-
path:
|
|
8199
|
-
hasPackageJson:
|
|
8200
|
-
hasGit:
|
|
8466
|
+
path: join18(dir, e.name),
|
|
8467
|
+
hasPackageJson: existsSync15(join18(dir, e.name, "package.json")),
|
|
8468
|
+
hasGit: existsSync15(join18(dir, e.name, ".git"))
|
|
8201
8469
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
8202
8470
|
return c.json({
|
|
8203
8471
|
current: dir,
|
|
8204
8472
|
name: basename5(dir),
|
|
8205
|
-
parent:
|
|
8473
|
+
parent: join18(dir, ".."),
|
|
8206
8474
|
directories: dirs
|
|
8207
8475
|
});
|
|
8208
8476
|
} catch {
|
|
@@ -8249,6 +8517,8 @@ var init_server = __esm({
|
|
|
8249
8517
|
init_kioku_routes();
|
|
8250
8518
|
init_module_host();
|
|
8251
8519
|
init_module_routes();
|
|
8520
|
+
init_module_install();
|
|
8521
|
+
init_task_review();
|
|
8252
8522
|
init_module_jobs();
|
|
8253
8523
|
init_module_events();
|
|
8254
8524
|
init_kioku();
|
|
@@ -8415,22 +8685,22 @@ var init_db_factory = __esm({
|
|
|
8415
8685
|
});
|
|
8416
8686
|
|
|
8417
8687
|
// ../core/dist/init.js
|
|
8418
|
-
import { existsSync as
|
|
8419
|
-
import { join as
|
|
8420
|
-
import { homedir as
|
|
8688
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8689
|
+
import { join as join19 } from "node:path";
|
|
8690
|
+
import { homedir as homedir8 } from "node:os";
|
|
8421
8691
|
function getForgeDir() {
|
|
8422
|
-
return
|
|
8692
|
+
return join19(homedir8(), ".forge");
|
|
8423
8693
|
}
|
|
8424
8694
|
function ensureForgeDir() {
|
|
8425
8695
|
const forgeDir = getForgeDir();
|
|
8426
|
-
if (
|
|
8696
|
+
if (existsSync16(forgeDir)) {
|
|
8427
8697
|
return { forgeDir, created: false };
|
|
8428
8698
|
}
|
|
8429
|
-
|
|
8699
|
+
mkdirSync6(forgeDir, { recursive: true });
|
|
8430
8700
|
for (const d of FORGE_SUBDIRS) {
|
|
8431
|
-
|
|
8701
|
+
mkdirSync6(join19(forgeDir, d), { recursive: true });
|
|
8432
8702
|
}
|
|
8433
|
-
writeFileSync6(
|
|
8703
|
+
writeFileSync6(join19(forgeDir, "config.json"), JSON.stringify({
|
|
8434
8704
|
port: 3e3,
|
|
8435
8705
|
theme: "dark",
|
|
8436
8706
|
openBrowser: true,
|
|
@@ -8492,68 +8762,6 @@ var init_port_in_use = __esm({
|
|
|
8492
8762
|
}
|
|
8493
8763
|
});
|
|
8494
8764
|
|
|
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
8765
|
// ../core/dist/index.js
|
|
8558
8766
|
var dist_exports = {};
|
|
8559
8767
|
__export(dist_exports, {
|
|
@@ -8630,7 +8838,7 @@ var init_dist2 = __esm({
|
|
|
8630
8838
|
|
|
8631
8839
|
// src/index.ts
|
|
8632
8840
|
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
8633
|
-
import { homedir as
|
|
8841
|
+
import { homedir as homedir9 } from "node:os";
|
|
8634
8842
|
import { join as join20 } from "node:path";
|
|
8635
8843
|
function parseArgs(argv) {
|
|
8636
8844
|
const out = { open: true };
|
|
@@ -8655,7 +8863,7 @@ async function main() {
|
|
|
8655
8863
|
if (created) {
|
|
8656
8864
|
console.log("First run \u2014 initialized Forge.");
|
|
8657
8865
|
}
|
|
8658
|
-
const cwHome = join20(process.env.HOME ??
|
|
8866
|
+
const cwHome = join20(process.env.HOME ?? homedir9(), ".cw");
|
|
8659
8867
|
const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join20(packageRoot, "cw") });
|
|
8660
8868
|
if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
|
|
8661
8869
|
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.
|
|
3
|
+
"version": "0.1.15",
|
|
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",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@types/node": "^22.0.0",
|
|
41
41
|
"esbuild": "^0.25.0",
|
|
42
42
|
"typescript": "^5.8.0",
|
|
43
|
-
"@forge-dev/
|
|
44
|
-
"@forge-dev/
|
|
43
|
+
"@forge-dev/console": "0.1.0",
|
|
44
|
+
"@forge-dev/core": "0.1.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc",
|