forge-cw 0.1.0 → 0.1.2
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-BLBzdbIU.js +33 -0
- package/console/index.html +1 -1
- package/cw/.bundle.json +1 -1
- package/cw/cw +40 -0
- package/dist/index.js +248 -90
- package/package.json +3 -3
- package/console/assets/index-ItVUTuQf.js +0 -31
package/dist/index.js
CHANGED
|
@@ -260,8 +260,17 @@ var init_runner = __esm({
|
|
|
260
260
|
const proc = spawn("sh", ["-c", command], {
|
|
261
261
|
cwd: options.cwd,
|
|
262
262
|
env: { ...process.env, ...options.env },
|
|
263
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
263
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
264
|
+
detached: true
|
|
264
265
|
});
|
|
266
|
+
const stop = () => {
|
|
267
|
+
try {
|
|
268
|
+
if (proc.pid)
|
|
269
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
270
|
+
} catch {
|
|
271
|
+
proc.kill("SIGTERM");
|
|
272
|
+
}
|
|
273
|
+
};
|
|
265
274
|
const handleData = (data) => {
|
|
266
275
|
const str2 = data.toString();
|
|
267
276
|
output += str2;
|
|
@@ -270,14 +279,12 @@ var init_runner = __esm({
|
|
|
270
279
|
proc.stdout.on("data", handleData);
|
|
271
280
|
proc.stderr.on("data", handleData);
|
|
272
281
|
if (options.signal) {
|
|
273
|
-
options.signal.addEventListener("abort",
|
|
274
|
-
proc.kill("SIGTERM");
|
|
275
|
-
}, { once: true });
|
|
282
|
+
options.signal.addEventListener("abort", stop, { once: true });
|
|
276
283
|
}
|
|
277
284
|
if (options.timeout) {
|
|
278
285
|
timeoutId = setTimeout(() => {
|
|
279
286
|
timedOut = true;
|
|
280
|
-
|
|
287
|
+
stop();
|
|
281
288
|
}, options.timeout);
|
|
282
289
|
}
|
|
283
290
|
proc.on("close", (code) => {
|
|
@@ -630,45 +637,46 @@ var init_cw_reader = __esm({
|
|
|
630
637
|
getSkillDir(scope, scopeRef, name) {
|
|
631
638
|
return join2(this.getSkillConfigDir(scope, scopeRef), "skills", name);
|
|
632
639
|
}
|
|
633
|
-
|
|
640
|
+
// global, every account and every registered project
|
|
641
|
+
getSkills() {
|
|
642
|
+
return [
|
|
643
|
+
...this.scanSkills(join2(this.getSkillConfigDir("global", "global"), "skills"), "global", "global"),
|
|
644
|
+
...this.getAccounts().flatMap((a) => this.scanSkills(join2(this.getSkillConfigDir("account", a), "skills"), "account", a)),
|
|
645
|
+
...Object.entries(this.getProjects()).flatMap(([name, p]) => this.scanSkills(join2(p.path, ".claude", "skills"), "project", name))
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
scanSkills(dir, scope, scopeRef) {
|
|
634
649
|
const results = [];
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
} catch {
|
|
664
|
-
}
|
|
650
|
+
if (!existsSync2(dir))
|
|
651
|
+
return results;
|
|
652
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
653
|
+
for (const entry of entries) {
|
|
654
|
+
if (!entry.isDirectory())
|
|
655
|
+
continue;
|
|
656
|
+
if (entry.name.startsWith("acct--"))
|
|
657
|
+
continue;
|
|
658
|
+
const fullPath = join2(dir, entry.name);
|
|
659
|
+
const skillMd = join2(fullPath, "SKILL.md");
|
|
660
|
+
if (!existsSync2(skillMd))
|
|
661
|
+
continue;
|
|
662
|
+
try {
|
|
663
|
+
const content = readFileSync2(skillMd, "utf-8");
|
|
664
|
+
const { frontmatter } = this.parseFrontmatter(content);
|
|
665
|
+
const refsDir = join2(fullPath, "references");
|
|
666
|
+
const hasReferences = existsSync2(refsDir) && readdirSync2(refsDir).some((f) => f.endsWith(".md"));
|
|
667
|
+
results.push({
|
|
668
|
+
name: String(frontmatter["name"] ?? entry.name),
|
|
669
|
+
dirName: entry.name,
|
|
670
|
+
scope,
|
|
671
|
+
scopeRef,
|
|
672
|
+
description: String(frontmatter["description"] ?? ""),
|
|
673
|
+
domain: frontmatter["domain"] ? String(frontmatter["domain"]) : void 0,
|
|
674
|
+
triggers: frontmatter["triggers"] ? String(frontmatter["triggers"]) : void 0,
|
|
675
|
+
hasReferences
|
|
676
|
+
});
|
|
677
|
+
} catch {
|
|
665
678
|
}
|
|
666
|
-
}
|
|
667
|
-
scanDir(join2(this.getSkillConfigDir("global", "global"), "skills"), "global", "global");
|
|
668
|
-
if (account)
|
|
669
|
-
scanDir(join2(this.getSkillConfigDir("account", account), "skills"), "account", account);
|
|
670
|
-
if (project)
|
|
671
|
-
scanDir(join2(this.getSkillConfigDir("project", project), "skills"), "project", project);
|
|
679
|
+
}
|
|
672
680
|
return results;
|
|
673
681
|
}
|
|
674
682
|
getSkill(scope, scopeRef, name) {
|
|
@@ -2528,7 +2536,7 @@ var init_plugins = __esm({
|
|
|
2528
2536
|
|
|
2529
2537
|
// ../core/dist/skill-routes.js
|
|
2530
2538
|
import { Hono as Hono2 } from "hono";
|
|
2531
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, unlinkSync } from "node:fs";
|
|
2539
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, unlinkSync, cpSync } from "node:fs";
|
|
2532
2540
|
import { dirname as dirname2, join as join9 } from "node:path";
|
|
2533
2541
|
function installFailure(result2) {
|
|
2534
2542
|
try {
|
|
@@ -2552,10 +2560,32 @@ function skillRoutes(reader, options = {}) {
|
|
|
2552
2560
|
const remoteOf = options.remoteOf ?? createRemoteLookup(runCommand);
|
|
2553
2561
|
const app = new Hono2();
|
|
2554
2562
|
const updating = /* @__PURE__ */ new Set();
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2563
|
+
const refOf = (scope, scopeRef) => scope === "global" ? "global" : scopeRef ?? "";
|
|
2564
|
+
const knownScope = (scope, ref) => scope === "global" || (scope === "account" ? reader.getAccounts().includes(ref) : scope === "project" && ref in reader.getProjects());
|
|
2565
|
+
app.get("/", (c) => c.json(reader.getSkills()));
|
|
2566
|
+
app.post("/copy", async (c) => {
|
|
2567
|
+
const { from, to } = await c.req.json();
|
|
2568
|
+
const fromRef = refOf(from.scope, from.scopeRef);
|
|
2569
|
+
const toRef = refOf(to.scope, to.scopeRef);
|
|
2570
|
+
if (!SKILL_NAME_RE.test(from.name))
|
|
2571
|
+
return c.json({ error: "Invalid skill name" }, 400);
|
|
2572
|
+
if (!knownScope(from.scope, fromRef))
|
|
2573
|
+
return c.json({ error: `Unknown ${from.scope}: ${fromRef}` }, 404);
|
|
2574
|
+
if (!knownScope(to.scope, toRef))
|
|
2575
|
+
return c.json({ error: `Unknown ${to.scope}: ${toRef}` }, 404);
|
|
2576
|
+
const source = reader.getSkillDir(from.scope, fromRef, from.name);
|
|
2577
|
+
const target = reader.getSkillDir(to.scope, toRef, from.name);
|
|
2578
|
+
if (!existsSync7(join9(source, "SKILL.md")))
|
|
2579
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2580
|
+
if (existsSync7(target))
|
|
2581
|
+
return c.json({ error: `${from.name} already exists in ${toRef}` }, 409);
|
|
2582
|
+
try {
|
|
2583
|
+
cpSync(source, target, { recursive: true });
|
|
2584
|
+
return c.json({ ok: true });
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2587
|
+
return c.json({ error: `Failed to copy skill: ${message}` }, 500);
|
|
2588
|
+
}
|
|
2559
2589
|
});
|
|
2560
2590
|
app.get("/global/:name", (c) => {
|
|
2561
2591
|
const name = c.req.param("name");
|
|
@@ -2701,9 +2731,8 @@ function skillRoutes(reader, options = {}) {
|
|
|
2701
2731
|
if (!repo || !skill) {
|
|
2702
2732
|
return c.json({ error: "repo and skill are required" }, 400);
|
|
2703
2733
|
}
|
|
2704
|
-
const ref = scope
|
|
2705
|
-
|
|
2706
|
-
if (!known) {
|
|
2734
|
+
const ref = refOf(scope, scopeRef);
|
|
2735
|
+
if (!knownScope(scope, ref)) {
|
|
2707
2736
|
return c.json({ error: `Unknown ${scope}: ${ref}` }, 404);
|
|
2708
2737
|
}
|
|
2709
2738
|
const configDir = reader.getSkillConfigDir(scope, ref);
|
|
@@ -2754,7 +2783,7 @@ function skillRoutes(reader, options = {}) {
|
|
|
2754
2783
|
});
|
|
2755
2784
|
return app;
|
|
2756
2785
|
}
|
|
2757
|
-
var INSTALL_TIMEOUT_MS;
|
|
2786
|
+
var INSTALL_TIMEOUT_MS, SKILL_NAME_RE;
|
|
2758
2787
|
var init_skill_routes = __esm({
|
|
2759
2788
|
"../core/dist/skill-routes.js"() {
|
|
2760
2789
|
"use strict";
|
|
@@ -2762,6 +2791,7 @@ var init_skill_routes = __esm({
|
|
|
2762
2791
|
init_task_review();
|
|
2763
2792
|
init_plugins();
|
|
2764
2793
|
INSTALL_TIMEOUT_MS = 12e4;
|
|
2794
|
+
SKILL_NAME_RE = /^\w[\w.-]*$/;
|
|
2765
2795
|
}
|
|
2766
2796
|
});
|
|
2767
2797
|
|
|
@@ -3143,9 +3173,8 @@ var init_terminal_screen = __esm({
|
|
|
3143
3173
|
write(data) {
|
|
3144
3174
|
this.term.write(data);
|
|
3145
3175
|
}
|
|
3146
|
-
// a resize applies at once while writes wait to be parsed, so it waits its turn behind them
|
|
3147
3176
|
resize(cols, rows) {
|
|
3148
|
-
this.term.
|
|
3177
|
+
this.term.resize(cols, rows);
|
|
3149
3178
|
}
|
|
3150
3179
|
// rows oldest first, scrollback included, without the blank rows under the last one written
|
|
3151
3180
|
read() {
|
|
@@ -3401,17 +3430,22 @@ var init_state_log = __esm({
|
|
|
3401
3430
|
|
|
3402
3431
|
// ../core/dist/pty-routes.js
|
|
3403
3432
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3404
|
-
function takeSession(reader, project, sessionDir) {
|
|
3433
|
+
function takeSession(reader, project, sessionDir, general) {
|
|
3405
3434
|
const sessionId = `${project}::${sessionDir}`;
|
|
3406
3435
|
const pending = pendingSessions.get(sessionId);
|
|
3407
3436
|
if (pending) {
|
|
3408
3437
|
pendingSessions.delete(sessionId);
|
|
3438
|
+
if (pending.type === "general")
|
|
3439
|
+
general?.remember(sessionId, pending);
|
|
3409
3440
|
return { session: pending, isNew: true };
|
|
3410
3441
|
}
|
|
3411
3442
|
const session = reader.getSession(project, sessionDir);
|
|
3412
|
-
|
|
3443
|
+
if (session)
|
|
3444
|
+
return { session, isNew: false };
|
|
3445
|
+
const known = general?.get(sessionId);
|
|
3446
|
+
return known ? { session: known, isNew: true } : null;
|
|
3413
3447
|
}
|
|
3414
|
-
function createTerminalWss(manager, reader, { localOnly = true } = {}) {
|
|
3448
|
+
function createTerminalWss(manager, reader, { localOnly = true, general } = {}) {
|
|
3415
3449
|
const wss = new WebSocketServer({ noServer: true });
|
|
3416
3450
|
function wireWs(ws, sessionId, project, sessionDir) {
|
|
3417
3451
|
const client = {
|
|
@@ -3448,18 +3482,18 @@ function createTerminalWss(manager, reader, { localOnly = true } = {}) {
|
|
|
3448
3482
|
wireWs(ws, sessionId, project, sessionDir);
|
|
3449
3483
|
return;
|
|
3450
3484
|
}
|
|
3451
|
-
const taken = takeSession(reader, project, sessionDir);
|
|
3485
|
+
const taken = takeSession(reader, project, sessionDir, general);
|
|
3452
3486
|
if (!taken) {
|
|
3453
3487
|
console.log(`[pty-ws] Session not found: ${project}/${sessionDir}`);
|
|
3454
3488
|
ws.send(JSON.stringify({ type: "error", message: `Session not found: ${project}/${sessionDir}` }));
|
|
3455
|
-
ws.close();
|
|
3489
|
+
ws.close(REFUSED_CLOSE_CODE);
|
|
3456
3490
|
return;
|
|
3457
3491
|
}
|
|
3458
3492
|
const ptySession = manager.getOrCreate(project, sessionDir, taken.session, { isNew: taken.isNew });
|
|
3459
3493
|
if (!ptySession) {
|
|
3460
3494
|
console.error(`[pty-ws] Failed to spawn terminal for: ${project}/${sessionDir}`);
|
|
3461
3495
|
ws.send(JSON.stringify({ type: "error", message: `Failed to start terminal for ${project}/${sessionDir}` }));
|
|
3462
|
-
ws.close();
|
|
3496
|
+
ws.close(REFUSED_CLOSE_CODE);
|
|
3463
3497
|
return;
|
|
3464
3498
|
}
|
|
3465
3499
|
wireWs(ws, sessionId, project, sessionDir);
|
|
@@ -3495,16 +3529,67 @@ function parseTerminalUpgradeUrl(pathname) {
|
|
|
3495
3529
|
return null;
|
|
3496
3530
|
}
|
|
3497
3531
|
}
|
|
3532
|
+
var REFUSED_CLOSE_CODE;
|
|
3498
3533
|
var init_pty_routes = __esm({
|
|
3499
3534
|
"../core/dist/pty-routes.js"() {
|
|
3500
3535
|
"use strict";
|
|
3501
3536
|
init_cw_routes();
|
|
3502
3537
|
init_origin_guard();
|
|
3538
|
+
REFUSED_CLOSE_CODE = 4404;
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
// ../core/dist/general-sessions.js
|
|
3543
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
3544
|
+
import { dirname as dirname5 } from "node:path";
|
|
3545
|
+
var MAX_AGE_MS, GeneralSessions;
|
|
3546
|
+
var init_general_sessions = __esm({
|
|
3547
|
+
"../core/dist/general-sessions.js"() {
|
|
3548
|
+
"use strict";
|
|
3549
|
+
MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3550
|
+
GeneralSessions = class {
|
|
3551
|
+
file;
|
|
3552
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3553
|
+
constructor(file = null, now = Date.now()) {
|
|
3554
|
+
this.file = file;
|
|
3555
|
+
if (!file || !existsSync9(file))
|
|
3556
|
+
return;
|
|
3557
|
+
try {
|
|
3558
|
+
const saved = JSON.parse(readFileSync7(file, "utf-8"));
|
|
3559
|
+
for (const [key, session] of Object.entries(saved)) {
|
|
3560
|
+
if (now - Date.parse(session.created) < MAX_AGE_MS)
|
|
3561
|
+
this.sessions.set(key, session);
|
|
3562
|
+
}
|
|
3563
|
+
} catch {
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
get(key) {
|
|
3567
|
+
return this.sessions.get(key);
|
|
3568
|
+
}
|
|
3569
|
+
remember(key, session) {
|
|
3570
|
+
this.sessions.set(key, session);
|
|
3571
|
+
this.save();
|
|
3572
|
+
}
|
|
3573
|
+
forget(key) {
|
|
3574
|
+
if (this.sessions.delete(key))
|
|
3575
|
+
this.save();
|
|
3576
|
+
}
|
|
3577
|
+
save() {
|
|
3578
|
+
if (!this.file)
|
|
3579
|
+
return;
|
|
3580
|
+
try {
|
|
3581
|
+
mkdirSync4(dirname5(this.file), { recursive: true });
|
|
3582
|
+
writeFileSync3(this.file, JSON.stringify(Object.fromEntries(this.sessions), null, 2));
|
|
3583
|
+
} catch (err) {
|
|
3584
|
+
console.warn(`[pty] could not save general sessions: ${err.message}`);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3503
3588
|
}
|
|
3504
3589
|
});
|
|
3505
3590
|
|
|
3506
3591
|
// ../core/dist/liveframe.js
|
|
3507
|
-
import { existsSync as
|
|
3592
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
|
|
3508
3593
|
import { mkdir, rm } from "node:fs/promises";
|
|
3509
3594
|
import { homedir as homedir4 } from "node:os";
|
|
3510
3595
|
import { join as join11 } from "node:path";
|
|
@@ -3523,7 +3608,7 @@ function parseFrameUrl(stdout) {
|
|
|
3523
3608
|
}
|
|
3524
3609
|
function readJson2(path) {
|
|
3525
3610
|
try {
|
|
3526
|
-
return JSON.parse(
|
|
3611
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
3527
3612
|
} catch {
|
|
3528
3613
|
return null;
|
|
3529
3614
|
}
|
|
@@ -3596,7 +3681,7 @@ var init_liveframe = __esm({
|
|
|
3596
3681
|
return this.readFrame(project, frame, this.config().api);
|
|
3597
3682
|
}
|
|
3598
3683
|
listFrames() {
|
|
3599
|
-
if (!
|
|
3684
|
+
if (!existsSync10(this.root))
|
|
3600
3685
|
return [];
|
|
3601
3686
|
const { api } = this.config();
|
|
3602
3687
|
const frames = [];
|
|
@@ -3636,7 +3721,7 @@ var init_liveframe = __esm({
|
|
|
3636
3721
|
return this.exclusive(project, frame, async () => {
|
|
3637
3722
|
const projectDir = join11(this.root, project);
|
|
3638
3723
|
const dir = this.frameDir(project, frame);
|
|
3639
|
-
if (
|
|
3724
|
+
if (existsSync10(dir))
|
|
3640
3725
|
throw new LiveframeError(`${project}/${frame} already exists in ${this.root}`, 409);
|
|
3641
3726
|
await mkdir(projectDir, { recursive: true });
|
|
3642
3727
|
try {
|
|
@@ -3715,7 +3800,7 @@ var init_liveframe_routes = __esm({
|
|
|
3715
3800
|
import { Hono as Hono4 } from "hono";
|
|
3716
3801
|
import { cors } from "hono/cors";
|
|
3717
3802
|
import { join as join12, basename as basename3, resolve as resolve4 } from "node:path";
|
|
3718
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
3803
|
+
import { readdirSync as readdirSync6, existsSync as existsSync11 } from "node:fs";
|
|
3719
3804
|
function createForgeServer(options) {
|
|
3720
3805
|
const { dataDir, db: externalDb, authToken, localOnly = true } = options;
|
|
3721
3806
|
const dbPath = join12(dataDir, "forge.db");
|
|
@@ -3752,13 +3837,15 @@ function createForgeServer(options) {
|
|
|
3752
3837
|
}
|
|
3753
3838
|
});
|
|
3754
3839
|
const ptyManager = new PTYManager(states);
|
|
3755
|
-
const
|
|
3840
|
+
const generalSessions = new GeneralSessions(join12(dataDir, "general-sessions.json"));
|
|
3841
|
+
const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
|
|
3756
3842
|
app.route("/api/liveframe", liveframeRoutes());
|
|
3757
3843
|
app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
|
|
3758
3844
|
app.post("/api/cw/terminal/kill", async (c) => {
|
|
3759
3845
|
const { project, sessionDir } = await c.req.json();
|
|
3760
3846
|
const sessionId = `${project}::${sessionDir}`;
|
|
3761
3847
|
ptyManager.kill(sessionId);
|
|
3848
|
+
generalSessions.forget(sessionId);
|
|
3762
3849
|
return c.json({ ok: true });
|
|
3763
3850
|
});
|
|
3764
3851
|
async function resolveAction(c) {
|
|
@@ -3895,7 +3982,7 @@ data: ${JSON.stringify(data)}
|
|
|
3895
3982
|
if (!dir.startsWith(home)) {
|
|
3896
3983
|
return c.json({ error: "Access denied: path must be within home directory" }, 403);
|
|
3897
3984
|
}
|
|
3898
|
-
if (!
|
|
3985
|
+
if (!existsSync11(dir)) {
|
|
3899
3986
|
return c.json({ error: "Directory not found" }, 404);
|
|
3900
3987
|
}
|
|
3901
3988
|
try {
|
|
@@ -3903,8 +3990,8 @@ data: ${JSON.stringify(data)}
|
|
|
3903
3990
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
|
|
3904
3991
|
name: e.name,
|
|
3905
3992
|
path: join12(dir, e.name),
|
|
3906
|
-
hasPackageJson:
|
|
3907
|
-
hasGit:
|
|
3993
|
+
hasPackageJson: existsSync11(join12(dir, e.name, "package.json")),
|
|
3994
|
+
hasGit: existsSync11(join12(dir, e.name, ".git"))
|
|
3908
3995
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
3909
3996
|
return c.json({
|
|
3910
3997
|
current: dir,
|
|
@@ -3947,6 +4034,7 @@ var init_server = __esm({
|
|
|
3947
4034
|
init_state_log();
|
|
3948
4035
|
init_pty_routes();
|
|
3949
4036
|
init_login_manager();
|
|
4037
|
+
init_general_sessions();
|
|
3950
4038
|
init_liveframe_routes();
|
|
3951
4039
|
}
|
|
3952
4040
|
});
|
|
@@ -4081,7 +4169,7 @@ var init_db_factory = __esm({
|
|
|
4081
4169
|
});
|
|
4082
4170
|
|
|
4083
4171
|
// ../core/dist/init.js
|
|
4084
|
-
import { existsSync as
|
|
4172
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4085
4173
|
import { join as join13 } from "node:path";
|
|
4086
4174
|
import { homedir as homedir5 } from "node:os";
|
|
4087
4175
|
function getForgeDir() {
|
|
@@ -4089,14 +4177,14 @@ function getForgeDir() {
|
|
|
4089
4177
|
}
|
|
4090
4178
|
function ensureForgeDir() {
|
|
4091
4179
|
const forgeDir = getForgeDir();
|
|
4092
|
-
if (
|
|
4180
|
+
if (existsSync12(forgeDir)) {
|
|
4093
4181
|
return { forgeDir, created: false };
|
|
4094
4182
|
}
|
|
4095
|
-
|
|
4183
|
+
mkdirSync5(forgeDir, { recursive: true });
|
|
4096
4184
|
for (const d of FORGE_SUBDIRS) {
|
|
4097
|
-
|
|
4185
|
+
mkdirSync5(join13(forgeDir, d), { recursive: true });
|
|
4098
4186
|
}
|
|
4099
|
-
|
|
4187
|
+
writeFileSync4(join13(forgeDir, "config.json"), JSON.stringify({
|
|
4100
4188
|
port: 3e3,
|
|
4101
4189
|
theme: "dark",
|
|
4102
4190
|
openBrowser: true,
|
|
@@ -4115,11 +4203,11 @@ var init_init = __esm({
|
|
|
4115
4203
|
// ../core/dist/cw-install.js
|
|
4116
4204
|
import { execFile as execFile5 } from "node:child_process";
|
|
4117
4205
|
import { createHash as createHash2 } from "node:crypto";
|
|
4118
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
4206
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4119
4207
|
import { delimiter as delimiter2, join as join14 } from "node:path";
|
|
4120
4208
|
function readCwVersion(file) {
|
|
4121
4209
|
try {
|
|
4122
|
-
const match = /^CW_VERSION="([^"]+)"/m.exec(
|
|
4210
|
+
const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
|
|
4123
4211
|
return match ? match[1] : null;
|
|
4124
4212
|
} catch {
|
|
4125
4213
|
return null;
|
|
@@ -4137,9 +4225,9 @@ function compareVersions(a, b) {
|
|
|
4137
4225
|
}
|
|
4138
4226
|
function readCwBundle(dir) {
|
|
4139
4227
|
try {
|
|
4140
|
-
const { ref } = JSON.parse(
|
|
4228
|
+
const { ref } = JSON.parse(readFileSync9(join14(dir, ".bundle.json"), "utf-8"));
|
|
4141
4229
|
const version = readCwVersion(join14(dir, "cw"));
|
|
4142
|
-
if (!ref || !version || !
|
|
4230
|
+
if (!ref || !version || !existsSync13(join14(dir, "install.sh")))
|
|
4143
4231
|
return null;
|
|
4144
4232
|
return { dir, version, ref };
|
|
4145
4233
|
} catch {
|
|
@@ -4187,7 +4275,7 @@ function pathWithCw(cwHome, pathVar) {
|
|
|
4187
4275
|
}
|
|
4188
4276
|
function readMarker(cwHome) {
|
|
4189
4277
|
try {
|
|
4190
|
-
return JSON.parse(
|
|
4278
|
+
return JSON.parse(readFileSync9(join14(cwHome, MARKER_FILE), "utf-8"));
|
|
4191
4279
|
} catch {
|
|
4192
4280
|
return null;
|
|
4193
4281
|
}
|
|
@@ -4203,7 +4291,7 @@ async function ensureCw(options) {
|
|
|
4203
4291
|
const run = options.run ?? runInstall;
|
|
4204
4292
|
const bin = join14(cwHome, "bin", "cw");
|
|
4205
4293
|
const bundle = readCwBundle(bundleDir);
|
|
4206
|
-
const installed =
|
|
4294
|
+
const installed = existsSync13(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
|
|
4207
4295
|
const decision = decideCwInstall({
|
|
4208
4296
|
bundle,
|
|
4209
4297
|
installed,
|
|
@@ -4214,10 +4302,10 @@ async function ensureCw(options) {
|
|
|
4214
4302
|
if (decision.action === "skip" || !bundle)
|
|
4215
4303
|
return { action: "skipped", message: decision.reason };
|
|
4216
4304
|
const { code, output } = await run(join14(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
|
|
4217
|
-
if (code !== 0 || !
|
|
4305
|
+
if (code !== 0 || !existsSync13(bin))
|
|
4218
4306
|
return { action: "failed", message: failureMessage2(output) };
|
|
4219
4307
|
const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
|
|
4220
|
-
|
|
4308
|
+
writeFileSync5(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
|
|
4221
4309
|
return {
|
|
4222
4310
|
action: decision.action === "install" ? "installed" : "updated",
|
|
4223
4311
|
version: bundle.version,
|
|
@@ -4231,7 +4319,7 @@ var init_cw_install = __esm({
|
|
|
4231
4319
|
MARKER_FILE = ".forge-cw.json";
|
|
4232
4320
|
INSTALL_TIMEOUT_MS2 = 12e4;
|
|
4233
4321
|
ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
4234
|
-
sha256Of = (file) => createHash2("sha256").update(
|
|
4322
|
+
sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
|
|
4235
4323
|
runInstall = (script, args, env) => new Promise((resolve5) => {
|
|
4236
4324
|
execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4237
4325
|
const output = `${String(stdout)}${String(stderr)}`;
|
|
@@ -4244,6 +4332,52 @@ var init_cw_install = __esm({
|
|
|
4244
4332
|
}
|
|
4245
4333
|
});
|
|
4246
4334
|
|
|
4335
|
+
// ../core/dist/port-in-use.js
|
|
4336
|
+
function waitForListening(server) {
|
|
4337
|
+
if (server.listening)
|
|
4338
|
+
return Promise.resolve();
|
|
4339
|
+
return new Promise((resolve5, reject) => {
|
|
4340
|
+
const onError = (err) => {
|
|
4341
|
+
server.off("listening", onListening);
|
|
4342
|
+
reject(err);
|
|
4343
|
+
};
|
|
4344
|
+
const onListening = () => {
|
|
4345
|
+
server.off("error", onError);
|
|
4346
|
+
resolve5();
|
|
4347
|
+
};
|
|
4348
|
+
server.once("error", onError);
|
|
4349
|
+
server.once("listening", onListening);
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
async function probePort(port, timeoutMs = 1500) {
|
|
4353
|
+
try {
|
|
4354
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
4355
|
+
const body = await res.json().catch(() => null);
|
|
4356
|
+
return body?.status === "ok" && typeof body.modules === "number" ? "forge" : "other";
|
|
4357
|
+
} catch (err) {
|
|
4358
|
+
return err.name === "TimeoutError" ? "unresponsive" : "other";
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
function portInUseMessage(port, holder) {
|
|
4362
|
+
const next = `Start on another port with --port ${port + 1} (or FORGE_PORT=${port + 1}).`;
|
|
4363
|
+
const find = `Find it with: lsof -nP -iTCP:${port} -sTCP:LISTEN`;
|
|
4364
|
+
if (holder === "forge")
|
|
4365
|
+
return `Forge is already running at http://localhost:${port}.`;
|
|
4366
|
+
if (holder === "unresponsive") {
|
|
4367
|
+
return [
|
|
4368
|
+
`Port ${port} is taken by a process that does not answer, often a Forge suspended with Ctrl+Z.`,
|
|
4369
|
+
`${find}, then bring it back with \`fg\` in its terminal or stop it with \`kill <pid>\`.`,
|
|
4370
|
+
next
|
|
4371
|
+
].join("\n ");
|
|
4372
|
+
}
|
|
4373
|
+
return [`Port ${port} is in use by another program.`, find, next].join("\n ");
|
|
4374
|
+
}
|
|
4375
|
+
var init_port_in_use = __esm({
|
|
4376
|
+
"../core/dist/port-in-use.js"() {
|
|
4377
|
+
"use strict";
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
|
|
4247
4381
|
// ../core/dist/index.js
|
|
4248
4382
|
var dist_exports = {};
|
|
4249
4383
|
__export(dist_exports, {
|
|
@@ -4269,10 +4403,13 @@ __export(dist_exports, {
|
|
|
4269
4403
|
getForgeDir: () => getForgeDir,
|
|
4270
4404
|
liveframeRoutes: () => liveframeRoutes,
|
|
4271
4405
|
pathWithCw: () => pathWithCw,
|
|
4406
|
+
portInUseMessage: () => portInUseMessage,
|
|
4407
|
+
probePort: () => probePort,
|
|
4272
4408
|
quietSqliteWarning: () => quietSqliteWarning,
|
|
4273
4409
|
resolveListenOptions: () => resolveListenOptions,
|
|
4274
4410
|
skillRoutes: () => skillRoutes,
|
|
4275
|
-
supports: () => supports
|
|
4411
|
+
supports: () => supports,
|
|
4412
|
+
waitForListening: () => waitForListening
|
|
4276
4413
|
});
|
|
4277
4414
|
var init_dist = __esm({
|
|
4278
4415
|
"../core/dist/index.js"() {
|
|
@@ -4295,11 +4432,12 @@ var init_dist = __esm({
|
|
|
4295
4432
|
init_pty_routes();
|
|
4296
4433
|
init_liveframe_routes();
|
|
4297
4434
|
init_liveframe();
|
|
4435
|
+
init_port_in_use();
|
|
4298
4436
|
}
|
|
4299
4437
|
});
|
|
4300
4438
|
|
|
4301
4439
|
// src/index.ts
|
|
4302
|
-
import { existsSync as
|
|
4440
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
4303
4441
|
import { homedir as homedir6 } from "node:os";
|
|
4304
4442
|
import { join as join15 } from "node:path";
|
|
4305
4443
|
function parseArgs(argv) {
|
|
@@ -4313,10 +4451,10 @@ function parseArgs(argv) {
|
|
|
4313
4451
|
return out;
|
|
4314
4452
|
}
|
|
4315
4453
|
var packageRoot = join15(import.meta.dirname, "..");
|
|
4316
|
-
var isCheckout =
|
|
4454
|
+
var isCheckout = existsSync14(join15(packageRoot, "src"));
|
|
4317
4455
|
var consoleDist = isCheckout ? join15(packageRoot, "../console/dist") : join15(packageRoot, "console");
|
|
4318
4456
|
async function main() {
|
|
4319
|
-
const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
4457
|
+
const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2, waitForListening: waitForListening2, probePort: probePort2, portInUseMessage: portInUseMessage2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
4320
4458
|
quietSqliteWarning2();
|
|
4321
4459
|
const args = parseArgs(process.argv.slice(2));
|
|
4322
4460
|
const { forgeDir, created } = ensureForgeDir2();
|
|
@@ -4347,14 +4485,31 @@ async function main() {
|
|
|
4347
4485
|
localOnly
|
|
4348
4486
|
});
|
|
4349
4487
|
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
4350
|
-
const { readFileSync:
|
|
4351
|
-
if (
|
|
4488
|
+
const { readFileSync: readFileSync10 } = await import("node:fs");
|
|
4489
|
+
if (existsSync14(join15(consoleDist, "index.html"))) {
|
|
4352
4490
|
server.app.use("/*", serveStatic({ root: consoleDist }));
|
|
4353
|
-
const indexHtml =
|
|
4491
|
+
const indexHtml = readFileSync10(join15(consoleDist, "index.html"), "utf-8");
|
|
4354
4492
|
server.app.get("*", (c) => c.html(indexHtml));
|
|
4355
4493
|
}
|
|
4356
4494
|
const { serve } = await import("@hono/node-server");
|
|
4357
4495
|
const httpServer = serve({ fetch: server.app.fetch, port, hostname: host });
|
|
4496
|
+
try {
|
|
4497
|
+
await waitForListening2(httpServer);
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
if (err.code !== "EADDRINUSE") throw err;
|
|
4500
|
+
const holder = await probePort2(port);
|
|
4501
|
+
console.log(`
|
|
4502
|
+
${portInUseMessage2(port, holder)}
|
|
4503
|
+
`);
|
|
4504
|
+
if (holder === "forge" && args.open && process.env.FORGE_NO_OPEN !== "1") {
|
|
4505
|
+
try {
|
|
4506
|
+
const open = (await import("open")).default;
|
|
4507
|
+
await open(`http://localhost:${port}`);
|
|
4508
|
+
} catch {
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
process.exit(holder === "forge" ? 0 : 1);
|
|
4512
|
+
}
|
|
4358
4513
|
server.attachTerminalWs(httpServer);
|
|
4359
4514
|
console.log(`
|
|
4360
4515
|
Forge Console running at http://localhost:${port}
|
|
@@ -4373,4 +4528,7 @@ ${authToken ? " Auth: bearer token required\n" : ""}
|
|
|
4373
4528
|
}
|
|
4374
4529
|
}
|
|
4375
4530
|
}
|
|
4376
|
-
main().catch(
|
|
4531
|
+
main().catch((err) => {
|
|
4532
|
+
console.error(err);
|
|
4533
|
+
process.exit(1);
|
|
4534
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-cw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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/
|
|
43
|
-
"@forge-dev/
|
|
42
|
+
"@forge-dev/console": "0.1.0",
|
|
43
|
+
"@forge-dev/core": "0.1.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc",
|