forge-cw 0.1.7 → 0.1.8
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
|
@@ -1526,9 +1526,9 @@ var init_login_manager = __esm({
|
|
|
1526
1526
|
cwBin;
|
|
1527
1527
|
spawn;
|
|
1528
1528
|
entries = /* @__PURE__ */ new Map();
|
|
1529
|
-
constructor(cwBin,
|
|
1529
|
+
constructor(cwBin, spawn8 = spawnInPty) {
|
|
1530
1530
|
this.cwBin = cwBin;
|
|
1531
|
-
this.spawn =
|
|
1531
|
+
this.spawn = spawn8;
|
|
1532
1532
|
}
|
|
1533
1533
|
start(account, harness) {
|
|
1534
1534
|
const key = `${account}::${harness}`;
|
|
@@ -3932,20 +3932,375 @@ var init_liveframe_routes = __esm({
|
|
|
3932
3932
|
}
|
|
3933
3933
|
});
|
|
3934
3934
|
|
|
3935
|
-
// ../core/dist/
|
|
3935
|
+
// ../core/dist/cw-install.js
|
|
3936
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
3937
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3938
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3939
|
+
import { delimiter as delimiter2, join as join12 } from "node:path";
|
|
3940
|
+
function readCwVersion(file) {
|
|
3941
|
+
try {
|
|
3942
|
+
const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
|
|
3943
|
+
return match ? match[1] : null;
|
|
3944
|
+
} catch {
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
function compareVersions(a, b) {
|
|
3949
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3950
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3951
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
3952
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
3953
|
+
if (diff !== 0)
|
|
3954
|
+
return diff;
|
|
3955
|
+
}
|
|
3956
|
+
return 0;
|
|
3957
|
+
}
|
|
3958
|
+
function readCwBundle(dir) {
|
|
3959
|
+
try {
|
|
3960
|
+
const { ref } = JSON.parse(readFileSync9(join12(dir, ".bundle.json"), "utf-8"));
|
|
3961
|
+
const version = readCwVersion(join12(dir, "cw"));
|
|
3962
|
+
if (!ref || !version || !existsSync11(join12(dir, "install.sh")))
|
|
3963
|
+
return null;
|
|
3964
|
+
return { dir, version, ref };
|
|
3965
|
+
} catch {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
function decideCwInstall({ bundle, installed, marker, cwOnPath, skip }) {
|
|
3970
|
+
if (skip)
|
|
3971
|
+
return { action: "skip", reason: "FORGE_SKIP_CW_INSTALL is set" };
|
|
3972
|
+
if (!bundle)
|
|
3973
|
+
return { action: "skip", reason: "this Forge carries no CW" };
|
|
3974
|
+
if (!installed) {
|
|
3975
|
+
return cwOnPath ? { action: "skip", reason: "using the cw found on PATH" } : { action: "install", reason: "CW is not installed" };
|
|
3976
|
+
}
|
|
3977
|
+
if (!installed.version)
|
|
3978
|
+
return { action: "skip", reason: "cannot read the installed CW version" };
|
|
3979
|
+
const newer = compareVersions(bundle.version, installed.version);
|
|
3980
|
+
const ownedByForge = marker !== null && marker.sha256 === installed.sha256;
|
|
3981
|
+
if (ownedByForge) {
|
|
3982
|
+
if (marker.ref === bundle.ref)
|
|
3983
|
+
return { action: "skip", reason: `CW ${installed.version} is current` };
|
|
3984
|
+
return newer >= 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `installed CW ${installed.version} is newer than ${bundle.version}` };
|
|
3985
|
+
}
|
|
3986
|
+
return newer > 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `CW ${installed.version} is current` };
|
|
3987
|
+
}
|
|
3988
|
+
function findOnPath(name, pathVar, excludeDir) {
|
|
3989
|
+
for (const dir of (pathVar ?? "").split(delimiter2)) {
|
|
3990
|
+
if (!dir || dir === excludeDir)
|
|
3991
|
+
continue;
|
|
3992
|
+
const candidate = join12(dir, name);
|
|
3993
|
+
try {
|
|
3994
|
+
if (!statSync3(candidate).isFile())
|
|
3995
|
+
continue;
|
|
3996
|
+
accessSync2(candidate, constants2.X_OK);
|
|
3997
|
+
return candidate;
|
|
3998
|
+
} catch {
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
return null;
|
|
4002
|
+
}
|
|
4003
|
+
function pathWithCw(cwHome, pathVar) {
|
|
4004
|
+
const bin = join12(cwHome, "bin");
|
|
4005
|
+
const rest = (pathVar ?? "").split(delimiter2).filter((dir) => dir && dir !== bin);
|
|
4006
|
+
return [bin, ...rest].join(delimiter2);
|
|
4007
|
+
}
|
|
4008
|
+
function readMarker(cwHome) {
|
|
4009
|
+
try {
|
|
4010
|
+
return JSON.parse(readFileSync9(join12(cwHome, MARKER_FILE), "utf-8"));
|
|
4011
|
+
} catch {
|
|
4012
|
+
return null;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
function failureMessage2(output) {
|
|
4016
|
+
const lines2 = output.replace(ANSI_RE3, "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4017
|
+
const errors = lines2.filter((l) => l.startsWith("[\u2717]")).map((l) => l.slice(3).trim());
|
|
4018
|
+
return (errors.length > 0 ? errors : lines2.slice(-3)).join(" ") || "the CW installer failed";
|
|
4019
|
+
}
|
|
4020
|
+
async function ensureCw(options) {
|
|
4021
|
+
const { cwHome, bundleDir } = options;
|
|
4022
|
+
const env = options.env ?? process.env;
|
|
4023
|
+
const run = options.run ?? runInstall;
|
|
4024
|
+
const bin = join12(cwHome, "bin", "cw");
|
|
4025
|
+
const bundle = readCwBundle(bundleDir);
|
|
4026
|
+
const installed = existsSync11(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
|
|
4027
|
+
const decision = decideCwInstall({
|
|
4028
|
+
bundle,
|
|
4029
|
+
installed,
|
|
4030
|
+
marker: readMarker(cwHome),
|
|
4031
|
+
cwOnPath: findOnPath("cw", env.PATH, join12(cwHome, "bin")) !== null,
|
|
4032
|
+
skip: env.FORGE_SKIP_CW_INSTALL === "1"
|
|
4033
|
+
});
|
|
4034
|
+
if (decision.action === "skip" || !bundle)
|
|
4035
|
+
return { action: "skipped", message: decision.reason };
|
|
4036
|
+
const { code, output } = await run(join12(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
|
|
4037
|
+
if (code !== 0 || !existsSync11(bin))
|
|
4038
|
+
return { action: "failed", message: failureMessage2(output) };
|
|
4039
|
+
const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
|
|
4040
|
+
writeFileSync4(join12(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
|
|
4041
|
+
return {
|
|
4042
|
+
action: decision.action === "install" ? "installed" : "updated",
|
|
4043
|
+
version: bundle.version,
|
|
4044
|
+
message: decision.reason
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
var MARKER_FILE, INSTALL_TIMEOUT_MS2, ANSI_RE3, sha256Of, runInstall;
|
|
4048
|
+
var init_cw_install = __esm({
|
|
4049
|
+
"../core/dist/cw-install.js"() {
|
|
4050
|
+
"use strict";
|
|
4051
|
+
MARKER_FILE = ".forge-cw.json";
|
|
4052
|
+
INSTALL_TIMEOUT_MS2 = 12e4;
|
|
4053
|
+
ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
4054
|
+
sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
|
|
4055
|
+
runInstall = (script, args, env) => new Promise((resolve5) => {
|
|
4056
|
+
execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4057
|
+
const output = `${String(stdout)}${String(stderr)}`;
|
|
4058
|
+
if (!err)
|
|
4059
|
+
return resolve5({ code: 0, output });
|
|
4060
|
+
const code = err.code;
|
|
4061
|
+
resolve5({ code: typeof code === "number" ? code : 1, output: output || err.message });
|
|
4062
|
+
});
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
});
|
|
4066
|
+
|
|
4067
|
+
// ../core/dist/kioku.js
|
|
4068
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
4069
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
4070
|
+
import { homedir as homedir5 } from "node:os";
|
|
4071
|
+
import { join as join13 } from "node:path";
|
|
4072
|
+
import { createInterface } from "node:readline";
|
|
4073
|
+
function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
|
|
4074
|
+
const fallback = join13(home, ".local", "bin", "kioku");
|
|
4075
|
+
return findOnPath("kioku", pathVar) ?? (existsSync12(fallback) ? fallback : "kioku");
|
|
4076
|
+
}
|
|
4077
|
+
function parseJson(text) {
|
|
4078
|
+
try {
|
|
4079
|
+
return JSON.parse(text);
|
|
4080
|
+
} catch {
|
|
4081
|
+
return null;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
async function mapLimit(items, limit, fn) {
|
|
4085
|
+
const out = new Array(items.length);
|
|
4086
|
+
let next = 0;
|
|
4087
|
+
const worker = async () => {
|
|
4088
|
+
while (next < items.length) {
|
|
4089
|
+
const i = next++;
|
|
4090
|
+
out[i] = await fn(items[i]);
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
4094
|
+
return out;
|
|
4095
|
+
}
|
|
4096
|
+
var KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, MISSING, brainName, Kioku;
|
|
4097
|
+
var init_kioku = __esm({
|
|
4098
|
+
"../core/dist/kioku.js"() {
|
|
4099
|
+
"use strict";
|
|
4100
|
+
init_task_review();
|
|
4101
|
+
init_cw_install();
|
|
4102
|
+
init_output();
|
|
4103
|
+
KiokuError = class extends Error {
|
|
4104
|
+
status;
|
|
4105
|
+
constructor(message, status = 502) {
|
|
4106
|
+
super(message);
|
|
4107
|
+
this.status = status;
|
|
4108
|
+
}
|
|
4109
|
+
};
|
|
4110
|
+
NOT_INSTALLED = "kioku is not installed on this machine";
|
|
4111
|
+
START_TIMEOUT_MS = 2e4;
|
|
4112
|
+
CACHE_MS2 = 5 * 6e4;
|
|
4113
|
+
ACTIVITY_SINCE = "2w";
|
|
4114
|
+
ACTIVITY_LIMIT = 40;
|
|
4115
|
+
PARALLEL = 6;
|
|
4116
|
+
MISSING = { installed: false, version: null, brains: [], projects: {}, activity: [] };
|
|
4117
|
+
brainName = (b) => b.name ?? b.key;
|
|
4118
|
+
Kioku = class {
|
|
4119
|
+
run;
|
|
4120
|
+
startTimeoutMs;
|
|
4121
|
+
projects;
|
|
4122
|
+
now;
|
|
4123
|
+
binPath;
|
|
4124
|
+
viewer = null;
|
|
4125
|
+
snapshot = null;
|
|
4126
|
+
constructor(options = {}) {
|
|
4127
|
+
this.binPath = options.bin;
|
|
4128
|
+
this.run = options.run ?? createRunner();
|
|
4129
|
+
this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
|
|
4130
|
+
this.projects = options.projects ?? (() => ({}));
|
|
4131
|
+
this.now = options.now ?? Date.now;
|
|
4132
|
+
}
|
|
4133
|
+
// resolved on first use, so a Forge that never opens Memory does not search PATH
|
|
4134
|
+
get bin() {
|
|
4135
|
+
return this.binPath ??= resolveKiokuBin();
|
|
4136
|
+
}
|
|
4137
|
+
async status(fresh = false) {
|
|
4138
|
+
const previous = this.snapshot;
|
|
4139
|
+
if (fresh || !previous || this.now() - previous.at > CACHE_MS2) {
|
|
4140
|
+
const value = this.read();
|
|
4141
|
+
this.snapshot = { at: this.now(), value };
|
|
4142
|
+
value.catch(() => {
|
|
4143
|
+
if (this.snapshot?.value === value)
|
|
4144
|
+
this.snapshot = previous;
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
const pending = fresh || !previous ? this.snapshot.value : previous.value;
|
|
4148
|
+
const [snapshot, viewer] = await Promise.all([pending, this.viewer?.catch(() => null) ?? null]);
|
|
4149
|
+
return { ...snapshot, viewer: viewer?.address ?? null };
|
|
4150
|
+
}
|
|
4151
|
+
async json(args, cwd) {
|
|
4152
|
+
const result2 = await this.run(this.bin, [...args, "--json"], cwd);
|
|
4153
|
+
return result2.code === 0 ? parseJson(result2.stdout) : null;
|
|
4154
|
+
}
|
|
4155
|
+
async read() {
|
|
4156
|
+
const [version, list] = await Promise.all([
|
|
4157
|
+
this.run(this.bin, ["--version"], homedir5()),
|
|
4158
|
+
this.json(["brain", "list"], homedir5())
|
|
4159
|
+
]);
|
|
4160
|
+
if (version.code !== 0)
|
|
4161
|
+
return MISSING;
|
|
4162
|
+
const listed = list?.brains ?? [];
|
|
4163
|
+
const entries = Object.entries(this.projects()).filter(([, p]) => existsSync12(p.path));
|
|
4164
|
+
const readBrain = async (b) => {
|
|
4165
|
+
if (!b.onDisk)
|
|
4166
|
+
return { kinds: {}, writes: [] };
|
|
4167
|
+
const [ns, log] = await Promise.all([
|
|
4168
|
+
this.json(["namespace", "list", "--brain", b.key], b.folder),
|
|
4169
|
+
this.json(["brain", "log", "--since", ACTIVITY_SINCE, "--brain", b.key], b.folder)
|
|
4170
|
+
]);
|
|
4171
|
+
const kinds = {};
|
|
4172
|
+
for (const n of ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? []) {
|
|
4173
|
+
for (const [kind, count] of Object.entries(n.pages))
|
|
4174
|
+
kinds[kind] = (kinds[kind] ?? 0) + count;
|
|
4175
|
+
}
|
|
4176
|
+
return { kinds, writes: log?.writes ?? [] };
|
|
4177
|
+
};
|
|
4178
|
+
const readChain = async (path) => (await this.json(["brain", "list"], path))?.chain ?? [];
|
|
4179
|
+
const [perBrain, chains] = await Promise.all([
|
|
4180
|
+
mapLimit(listed, PARALLEL / 2, readBrain),
|
|
4181
|
+
mapLimit(entries, PARALLEL / 2, ([, p]) => readChain(p.path))
|
|
4182
|
+
]);
|
|
4183
|
+
const activity = perBrain.flatMap((p) => p.writes).sort((x, y) => y.date.localeCompare(x.date));
|
|
4184
|
+
const brains = listed.map((b, i) => {
|
|
4185
|
+
const { kinds } = perBrain[i];
|
|
4186
|
+
const name = brainName(b);
|
|
4187
|
+
return {
|
|
4188
|
+
...b,
|
|
4189
|
+
pages: Object.values(kinds).reduce((sum, n) => sum + n, 0),
|
|
4190
|
+
kinds,
|
|
4191
|
+
touched: activity.find((w) => w.brain === name)?.date ?? null
|
|
4192
|
+
};
|
|
4193
|
+
});
|
|
4194
|
+
const projects = Object.fromEntries(entries.map(([name], i) => [name, chains[i].map(({ name: name2, tier }) => ({ name: name2, tier }))]));
|
|
4195
|
+
return {
|
|
4196
|
+
installed: true,
|
|
4197
|
+
version: parseJson(version.stdout)?.version ?? null,
|
|
4198
|
+
brains,
|
|
4199
|
+
projects,
|
|
4200
|
+
activity: activity.slice(0, ACTIVITY_LIMIT).map(({ verb, title, path, date, author, brain }) => ({ verb, title, path, date, author, brain }))
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
// one viewer at a time; a second call reuses the running one
|
|
4204
|
+
async openViewer() {
|
|
4205
|
+
this.viewer ??= this.startViewer();
|
|
4206
|
+
return (await this.viewer).address;
|
|
4207
|
+
}
|
|
4208
|
+
startViewer() {
|
|
4209
|
+
const started = new Promise((resolve5, reject) => {
|
|
4210
|
+
const child = spawn7(this.bin, ["web", "--json", "--no-open"], { cwd: homedir5(), stdio: ["ignore", "pipe", "pipe"] });
|
|
4211
|
+
const lines2 = createInterface({ input: child.stdout });
|
|
4212
|
+
let stderr = "";
|
|
4213
|
+
let settled = false;
|
|
4214
|
+
const finish = () => {
|
|
4215
|
+
settled = true;
|
|
4216
|
+
clearTimeout(timer);
|
|
4217
|
+
lines2.close();
|
|
4218
|
+
child.stdout?.resume();
|
|
4219
|
+
};
|
|
4220
|
+
const fail = (err) => {
|
|
4221
|
+
if (settled)
|
|
4222
|
+
return;
|
|
4223
|
+
finish();
|
|
4224
|
+
child.kill();
|
|
4225
|
+
reject(err);
|
|
4226
|
+
};
|
|
4227
|
+
const timer = setTimeout(() => fail(new KiokuError("kioku web did not start in time")), this.startTimeoutMs);
|
|
4228
|
+
child.stderr?.on("data", (chunk) => {
|
|
4229
|
+
stderr = (stderr + chunk.toString()).slice(-4096);
|
|
4230
|
+
});
|
|
4231
|
+
lines2.on("line", (line) => {
|
|
4232
|
+
const event = parseJson(line);
|
|
4233
|
+
if (settled || event?.event !== "listening" || !event.address?.startsWith("http://127.0.0.1:"))
|
|
4234
|
+
return;
|
|
4235
|
+
finish();
|
|
4236
|
+
resolve5({ child, address: event.address });
|
|
4237
|
+
});
|
|
4238
|
+
child.on("error", (err) => {
|
|
4239
|
+
const missing = err.code === "ENOENT";
|
|
4240
|
+
fail(new KiokuError(missing ? NOT_INSTALLED : err.message, missing ? 404 : 502));
|
|
4241
|
+
});
|
|
4242
|
+
child.on("exit", (code) => {
|
|
4243
|
+
if (this.viewer === started)
|
|
4244
|
+
this.viewer = null;
|
|
4245
|
+
fail(new KiokuError(tailOutput(stderr, 1) || `kioku web exited with code ${code}`));
|
|
4246
|
+
});
|
|
4247
|
+
});
|
|
4248
|
+
started.catch(() => {
|
|
4249
|
+
if (this.viewer === started)
|
|
4250
|
+
this.viewer = null;
|
|
4251
|
+
});
|
|
4252
|
+
return started;
|
|
4253
|
+
}
|
|
4254
|
+
stopViewer() {
|
|
4255
|
+
void this.viewer?.then((v) => v.child.kill(), () => {
|
|
4256
|
+
});
|
|
4257
|
+
this.viewer = null;
|
|
4258
|
+
}
|
|
4259
|
+
dispose() {
|
|
4260
|
+
this.stopViewer();
|
|
4261
|
+
}
|
|
4262
|
+
};
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
|
|
4266
|
+
// ../core/dist/kioku-routes.js
|
|
3936
4267
|
import { Hono as Hono4 } from "hono";
|
|
4268
|
+
function kiokuRoutes(kioku = new Kioku()) {
|
|
4269
|
+
const app = new Hono4();
|
|
4270
|
+
app.onError((err, c) => {
|
|
4271
|
+
if (err instanceof KiokuError)
|
|
4272
|
+
return c.json({ error: err.message }, err.status);
|
|
4273
|
+
throw err;
|
|
4274
|
+
});
|
|
4275
|
+
app.get("/status", async (c) => c.json(await kioku.status(c.req.query("fresh") === "1")));
|
|
4276
|
+
app.post("/viewer", async (c) => c.json({ address: await kioku.openViewer() }));
|
|
4277
|
+
app.delete("/viewer", (c) => {
|
|
4278
|
+
kioku.stopViewer();
|
|
4279
|
+
return c.json({ ok: true });
|
|
4280
|
+
});
|
|
4281
|
+
return app;
|
|
4282
|
+
}
|
|
4283
|
+
var init_kioku_routes = __esm({
|
|
4284
|
+
"../core/dist/kioku-routes.js"() {
|
|
4285
|
+
"use strict";
|
|
4286
|
+
init_kioku();
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
4289
|
+
|
|
4290
|
+
// ../core/dist/server.js
|
|
4291
|
+
import { Hono as Hono5 } from "hono";
|
|
3937
4292
|
import { cors } from "hono/cors";
|
|
3938
|
-
import { join as
|
|
3939
|
-
import { readdirSync as readdirSync6, existsSync as
|
|
4293
|
+
import { join as join14, basename as basename3, resolve as resolve4 } from "node:path";
|
|
4294
|
+
import { readdirSync as readdirSync6, existsSync as existsSync13 } from "node:fs";
|
|
3940
4295
|
function createForgeServer(options) {
|
|
3941
4296
|
const { dataDir, db: externalDb, authToken, localOnly = true } = options;
|
|
3942
|
-
const dbPath =
|
|
3943
|
-
const modulesDir =
|
|
4297
|
+
const dbPath = join14(dataDir, "forge.db");
|
|
4298
|
+
const modulesDir = join14(dataDir, "modules");
|
|
3944
4299
|
const db = externalDb ?? new ForgeDB(dbPath);
|
|
3945
4300
|
const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
|
|
3946
4301
|
const runner = new ActionRunner();
|
|
3947
4302
|
loader.discover();
|
|
3948
|
-
const app = new
|
|
4303
|
+
const app = new Hono5();
|
|
3949
4304
|
app.use("*", localOnly ? localOriginGuard() : cors());
|
|
3950
4305
|
if (authToken) {
|
|
3951
4306
|
app.use("/api/*", bearerAuth(authToken));
|
|
@@ -3973,9 +4328,11 @@ function createForgeServer(options) {
|
|
|
3973
4328
|
}
|
|
3974
4329
|
});
|
|
3975
4330
|
const ptyManager = new PTYManager(states);
|
|
3976
|
-
const generalSessions = new GeneralSessions(
|
|
4331
|
+
const generalSessions = new GeneralSessions(join14(dataDir, "general-sessions.json"));
|
|
3977
4332
|
const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
|
|
3978
4333
|
app.route("/api/liveframe", liveframeRoutes());
|
|
4334
|
+
const kioku = new Kioku({ projects: () => cwReader.getProjects() });
|
|
4335
|
+
app.route("/api/kioku", kiokuRoutes(kioku));
|
|
3979
4336
|
app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
|
|
3980
4337
|
app.post("/api/cw/terminal/kill", async (c) => {
|
|
3981
4338
|
const { project, sessionDir } = await c.req.json();
|
|
@@ -4131,21 +4488,21 @@ data: ${JSON.stringify(data)}
|
|
|
4131
4488
|
if (!dir.startsWith(home)) {
|
|
4132
4489
|
return c.json({ error: "Access denied: path must be within home directory" }, 403);
|
|
4133
4490
|
}
|
|
4134
|
-
if (!
|
|
4491
|
+
if (!existsSync13(dir)) {
|
|
4135
4492
|
return c.json({ error: "Directory not found" }, 404);
|
|
4136
4493
|
}
|
|
4137
4494
|
try {
|
|
4138
4495
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
4139
4496
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
|
|
4140
4497
|
name: e.name,
|
|
4141
|
-
path:
|
|
4142
|
-
hasPackageJson:
|
|
4143
|
-
hasGit:
|
|
4498
|
+
path: join14(dir, e.name),
|
|
4499
|
+
hasPackageJson: existsSync13(join14(dir, e.name, "package.json")),
|
|
4500
|
+
hasGit: existsSync13(join14(dir, e.name, ".git"))
|
|
4144
4501
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4145
4502
|
return c.json({
|
|
4146
4503
|
current: dir,
|
|
4147
4504
|
name: basename3(dir),
|
|
4148
|
-
parent:
|
|
4505
|
+
parent: join14(dir, ".."),
|
|
4149
4506
|
directories: dirs
|
|
4150
4507
|
});
|
|
4151
4508
|
} catch {
|
|
@@ -4163,6 +4520,7 @@ data: ${JSON.stringify(data)}
|
|
|
4163
4520
|
ptyManager.dispose();
|
|
4164
4521
|
states.dispose();
|
|
4165
4522
|
loginManager.dispose();
|
|
4523
|
+
kioku.dispose();
|
|
4166
4524
|
db.close();
|
|
4167
4525
|
}
|
|
4168
4526
|
};
|
|
@@ -4186,6 +4544,8 @@ var init_server = __esm({
|
|
|
4186
4544
|
init_login_manager();
|
|
4187
4545
|
init_general_sessions();
|
|
4188
4546
|
init_liveframe_routes();
|
|
4547
|
+
init_kioku_routes();
|
|
4548
|
+
init_kioku();
|
|
4189
4549
|
}
|
|
4190
4550
|
});
|
|
4191
4551
|
|
|
@@ -4308,8 +4668,8 @@ async function createDatabase(opts) {
|
|
|
4308
4668
|
await db.migrate();
|
|
4309
4669
|
return db;
|
|
4310
4670
|
}
|
|
4311
|
-
const { join:
|
|
4312
|
-
return new ForgeDB(
|
|
4671
|
+
const { join: join17 } = await import("node:path");
|
|
4672
|
+
return new ForgeDB(join17(opts.dataDir, "forge.db"));
|
|
4313
4673
|
}
|
|
4314
4674
|
var init_db_factory = __esm({
|
|
4315
4675
|
"../core/dist/db-factory.js"() {
|
|
@@ -4319,22 +4679,22 @@ var init_db_factory = __esm({
|
|
|
4319
4679
|
});
|
|
4320
4680
|
|
|
4321
4681
|
// ../core/dist/init.js
|
|
4322
|
-
import { existsSync as
|
|
4323
|
-
import { join as
|
|
4324
|
-
import { homedir as
|
|
4682
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4683
|
+
import { join as join15 } from "node:path";
|
|
4684
|
+
import { homedir as homedir6 } from "node:os";
|
|
4325
4685
|
function getForgeDir() {
|
|
4326
|
-
return
|
|
4686
|
+
return join15(homedir6(), ".forge");
|
|
4327
4687
|
}
|
|
4328
4688
|
function ensureForgeDir() {
|
|
4329
4689
|
const forgeDir = getForgeDir();
|
|
4330
|
-
if (
|
|
4690
|
+
if (existsSync14(forgeDir)) {
|
|
4331
4691
|
return { forgeDir, created: false };
|
|
4332
4692
|
}
|
|
4333
4693
|
mkdirSync5(forgeDir, { recursive: true });
|
|
4334
4694
|
for (const d of FORGE_SUBDIRS) {
|
|
4335
|
-
mkdirSync5(
|
|
4695
|
+
mkdirSync5(join15(forgeDir, d), { recursive: true });
|
|
4336
4696
|
}
|
|
4337
|
-
|
|
4697
|
+
writeFileSync5(join15(forgeDir, "config.json"), JSON.stringify({
|
|
4338
4698
|
port: 3e3,
|
|
4339
4699
|
theme: "dark",
|
|
4340
4700
|
openBrowser: true,
|
|
@@ -4350,138 +4710,6 @@ var init_init = __esm({
|
|
|
4350
4710
|
}
|
|
4351
4711
|
});
|
|
4352
4712
|
|
|
4353
|
-
// ../core/dist/cw-install.js
|
|
4354
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
4355
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4356
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4357
|
-
import { delimiter as delimiter2, join as join14 } from "node:path";
|
|
4358
|
-
function readCwVersion(file) {
|
|
4359
|
-
try {
|
|
4360
|
-
const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
|
|
4361
|
-
return match ? match[1] : null;
|
|
4362
|
-
} catch {
|
|
4363
|
-
return null;
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
function compareVersions(a, b) {
|
|
4367
|
-
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
4368
|
-
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
4369
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
4370
|
-
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
4371
|
-
if (diff !== 0)
|
|
4372
|
-
return diff;
|
|
4373
|
-
}
|
|
4374
|
-
return 0;
|
|
4375
|
-
}
|
|
4376
|
-
function readCwBundle(dir) {
|
|
4377
|
-
try {
|
|
4378
|
-
const { ref } = JSON.parse(readFileSync9(join14(dir, ".bundle.json"), "utf-8"));
|
|
4379
|
-
const version = readCwVersion(join14(dir, "cw"));
|
|
4380
|
-
if (!ref || !version || !existsSync13(join14(dir, "install.sh")))
|
|
4381
|
-
return null;
|
|
4382
|
-
return { dir, version, ref };
|
|
4383
|
-
} catch {
|
|
4384
|
-
return null;
|
|
4385
|
-
}
|
|
4386
|
-
}
|
|
4387
|
-
function decideCwInstall({ bundle, installed, marker, cwOnPath, skip }) {
|
|
4388
|
-
if (skip)
|
|
4389
|
-
return { action: "skip", reason: "FORGE_SKIP_CW_INSTALL is set" };
|
|
4390
|
-
if (!bundle)
|
|
4391
|
-
return { action: "skip", reason: "this Forge carries no CW" };
|
|
4392
|
-
if (!installed) {
|
|
4393
|
-
return cwOnPath ? { action: "skip", reason: "using the cw found on PATH" } : { action: "install", reason: "CW is not installed" };
|
|
4394
|
-
}
|
|
4395
|
-
if (!installed.version)
|
|
4396
|
-
return { action: "skip", reason: "cannot read the installed CW version" };
|
|
4397
|
-
const newer = compareVersions(bundle.version, installed.version);
|
|
4398
|
-
const ownedByForge = marker !== null && marker.sha256 === installed.sha256;
|
|
4399
|
-
if (ownedByForge) {
|
|
4400
|
-
if (marker.ref === bundle.ref)
|
|
4401
|
-
return { action: "skip", reason: `CW ${installed.version} is current` };
|
|
4402
|
-
return newer >= 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `installed CW ${installed.version} is newer than ${bundle.version}` };
|
|
4403
|
-
}
|
|
4404
|
-
return newer > 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `CW ${installed.version} is current` };
|
|
4405
|
-
}
|
|
4406
|
-
function findOnPath(name, pathVar, excludeDir) {
|
|
4407
|
-
for (const dir of (pathVar ?? "").split(delimiter2)) {
|
|
4408
|
-
if (!dir || dir === excludeDir)
|
|
4409
|
-
continue;
|
|
4410
|
-
const candidate = join14(dir, name);
|
|
4411
|
-
try {
|
|
4412
|
-
if (!statSync3(candidate).isFile())
|
|
4413
|
-
continue;
|
|
4414
|
-
accessSync2(candidate, constants2.X_OK);
|
|
4415
|
-
return candidate;
|
|
4416
|
-
} catch {
|
|
4417
|
-
}
|
|
4418
|
-
}
|
|
4419
|
-
return null;
|
|
4420
|
-
}
|
|
4421
|
-
function pathWithCw(cwHome, pathVar) {
|
|
4422
|
-
const bin = join14(cwHome, "bin");
|
|
4423
|
-
const rest = (pathVar ?? "").split(delimiter2).filter((dir) => dir && dir !== bin);
|
|
4424
|
-
return [bin, ...rest].join(delimiter2);
|
|
4425
|
-
}
|
|
4426
|
-
function readMarker(cwHome) {
|
|
4427
|
-
try {
|
|
4428
|
-
return JSON.parse(readFileSync9(join14(cwHome, MARKER_FILE), "utf-8"));
|
|
4429
|
-
} catch {
|
|
4430
|
-
return null;
|
|
4431
|
-
}
|
|
4432
|
-
}
|
|
4433
|
-
function failureMessage2(output) {
|
|
4434
|
-
const lines2 = output.replace(ANSI_RE3, "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4435
|
-
const errors = lines2.filter((l) => l.startsWith("[\u2717]")).map((l) => l.slice(3).trim());
|
|
4436
|
-
return (errors.length > 0 ? errors : lines2.slice(-3)).join(" ") || "the CW installer failed";
|
|
4437
|
-
}
|
|
4438
|
-
async function ensureCw(options) {
|
|
4439
|
-
const { cwHome, bundleDir } = options;
|
|
4440
|
-
const env = options.env ?? process.env;
|
|
4441
|
-
const run = options.run ?? runInstall;
|
|
4442
|
-
const bin = join14(cwHome, "bin", "cw");
|
|
4443
|
-
const bundle = readCwBundle(bundleDir);
|
|
4444
|
-
const installed = existsSync13(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
|
|
4445
|
-
const decision = decideCwInstall({
|
|
4446
|
-
bundle,
|
|
4447
|
-
installed,
|
|
4448
|
-
marker: readMarker(cwHome),
|
|
4449
|
-
cwOnPath: findOnPath("cw", env.PATH, join14(cwHome, "bin")) !== null,
|
|
4450
|
-
skip: env.FORGE_SKIP_CW_INSTALL === "1"
|
|
4451
|
-
});
|
|
4452
|
-
if (decision.action === "skip" || !bundle)
|
|
4453
|
-
return { action: "skipped", message: decision.reason };
|
|
4454
|
-
const { code, output } = await run(join14(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
|
|
4455
|
-
if (code !== 0 || !existsSync13(bin))
|
|
4456
|
-
return { action: "failed", message: failureMessage2(output) };
|
|
4457
|
-
const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
|
|
4458
|
-
writeFileSync5(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
|
|
4459
|
-
return {
|
|
4460
|
-
action: decision.action === "install" ? "installed" : "updated",
|
|
4461
|
-
version: bundle.version,
|
|
4462
|
-
message: decision.reason
|
|
4463
|
-
};
|
|
4464
|
-
}
|
|
4465
|
-
var MARKER_FILE, INSTALL_TIMEOUT_MS2, ANSI_RE3, sha256Of, runInstall;
|
|
4466
|
-
var init_cw_install = __esm({
|
|
4467
|
-
"../core/dist/cw-install.js"() {
|
|
4468
|
-
"use strict";
|
|
4469
|
-
MARKER_FILE = ".forge-cw.json";
|
|
4470
|
-
INSTALL_TIMEOUT_MS2 = 12e4;
|
|
4471
|
-
ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
4472
|
-
sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
|
|
4473
|
-
runInstall = (script, args, env) => new Promise((resolve5) => {
|
|
4474
|
-
execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4475
|
-
const output = `${String(stdout)}${String(stderr)}`;
|
|
4476
|
-
if (!err)
|
|
4477
|
-
return resolve5({ code: 0, output });
|
|
4478
|
-
const code = err.code;
|
|
4479
|
-
resolve5({ code: typeof code === "number" ? code : 1, output: output || err.message });
|
|
4480
|
-
});
|
|
4481
|
-
});
|
|
4482
|
-
}
|
|
4483
|
-
});
|
|
4484
|
-
|
|
4485
4713
|
// ../core/dist/port-in-use.js
|
|
4486
4714
|
function waitForListening(server) {
|
|
4487
4715
|
if (server.listening)
|
|
@@ -4537,6 +4765,8 @@ __export(dist_exports, {
|
|
|
4537
4765
|
ForgeDB: () => ForgeDB,
|
|
4538
4766
|
HARNESS_CAPABILITIES: () => HARNESS_CAPABILITIES,
|
|
4539
4767
|
HARNESS_NAME_RE: () => HARNESS_NAME_RE,
|
|
4768
|
+
Kioku: () => Kioku,
|
|
4769
|
+
KiokuError: () => KiokuError,
|
|
4540
4770
|
Liveframe: () => Liveframe,
|
|
4541
4771
|
LiveframeError: () => LiveframeError,
|
|
4542
4772
|
MODEL_NAME_RE: () => MODEL_NAME_RE,
|
|
@@ -4551,11 +4781,13 @@ __export(dist_exports, {
|
|
|
4551
4781
|
ensureCw: () => ensureCw,
|
|
4552
4782
|
ensureForgeDir: () => ensureForgeDir,
|
|
4553
4783
|
getForgeDir: () => getForgeDir,
|
|
4784
|
+
kiokuRoutes: () => kiokuRoutes,
|
|
4554
4785
|
liveframeRoutes: () => liveframeRoutes,
|
|
4555
4786
|
pathWithCw: () => pathWithCw,
|
|
4556
4787
|
portInUseMessage: () => portInUseMessage,
|
|
4557
4788
|
probePort: () => probePort,
|
|
4558
4789
|
quietSqliteWarning: () => quietSqliteWarning,
|
|
4790
|
+
resolveKiokuBin: () => resolveKiokuBin,
|
|
4559
4791
|
resolveListenOptions: () => resolveListenOptions,
|
|
4560
4792
|
skillRoutes: () => skillRoutes,
|
|
4561
4793
|
supports: () => supports,
|
|
@@ -4584,14 +4816,16 @@ var init_dist = __esm({
|
|
|
4584
4816
|
init_pty_routes();
|
|
4585
4817
|
init_liveframe_routes();
|
|
4586
4818
|
init_liveframe();
|
|
4819
|
+
init_kioku_routes();
|
|
4820
|
+
init_kioku();
|
|
4587
4821
|
init_port_in_use();
|
|
4588
4822
|
}
|
|
4589
4823
|
});
|
|
4590
4824
|
|
|
4591
4825
|
// src/index.ts
|
|
4592
|
-
import { existsSync as
|
|
4593
|
-
import { homedir as
|
|
4594
|
-
import { join as
|
|
4826
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
|
|
4827
|
+
import { homedir as homedir7 } from "node:os";
|
|
4828
|
+
import { join as join16 } from "node:path";
|
|
4595
4829
|
function parseArgs(argv) {
|
|
4596
4830
|
const out = { open: true };
|
|
4597
4831
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -4602,11 +4836,11 @@ function parseArgs(argv) {
|
|
|
4602
4836
|
}
|
|
4603
4837
|
return out;
|
|
4604
4838
|
}
|
|
4605
|
-
var packageRoot =
|
|
4606
|
-
var isCheckout =
|
|
4607
|
-
var consoleDist = isCheckout ?
|
|
4608
|
-
var forgeVersion = JSON.parse(readFileSync10(
|
|
4609
|
-
var bundledModulesDir = isCheckout ?
|
|
4839
|
+
var packageRoot = join16(import.meta.dirname, "..");
|
|
4840
|
+
var isCheckout = existsSync15(join16(packageRoot, "src"));
|
|
4841
|
+
var consoleDist = isCheckout ? join16(packageRoot, "../console/dist") : join16(packageRoot, "console");
|
|
4842
|
+
var forgeVersion = JSON.parse(readFileSync10(join16(packageRoot, "package.json"), "utf-8")).version;
|
|
4843
|
+
var bundledModulesDir = isCheckout ? join16(packageRoot, "../../modules") : join16(packageRoot, "modules");
|
|
4610
4844
|
async function main() {
|
|
4611
4845
|
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));
|
|
4612
4846
|
quietSqliteWarning2();
|
|
@@ -4615,8 +4849,8 @@ async function main() {
|
|
|
4615
4849
|
if (created) {
|
|
4616
4850
|
console.log("First run \u2014 initialized Forge.");
|
|
4617
4851
|
}
|
|
4618
|
-
const cwHome =
|
|
4619
|
-
const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir:
|
|
4852
|
+
const cwHome = join16(process.env.HOME ?? homedir7(), ".cw");
|
|
4853
|
+
const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join16(packageRoot, "cw") });
|
|
4620
4854
|
if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
|
|
4621
4855
|
else if (cw.action === "updated") console.log(` Updated ${cw.message}`);
|
|
4622
4856
|
else if (cw.action === "failed") console.log(` Could not install CW: ${cw.message}`);
|
|
@@ -4641,9 +4875,9 @@ async function main() {
|
|
|
4641
4875
|
version: forgeVersion
|
|
4642
4876
|
});
|
|
4643
4877
|
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
4644
|
-
if (
|
|
4878
|
+
if (existsSync15(join16(consoleDist, "index.html"))) {
|
|
4645
4879
|
server.app.use("/*", serveStatic({ root: consoleDist }));
|
|
4646
|
-
const indexHtml = readFileSync10(
|
|
4880
|
+
const indexHtml = readFileSync10(join16(consoleDist, "index.html"), "utf-8");
|
|
4647
4881
|
server.app.get("*", (c) => c.html(indexHtml));
|
|
4648
4882
|
}
|
|
4649
4883
|
const { serve } = await import("@hono/node-server");
|