ledung 1.3.0 → 1.5.0
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/CHANGELOG.md +4 -0
- package/README.md +3 -1
- package/dist/cli.js +714 -32
- package/dist/main.js +331 -234
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/web/dist/assets/{highlightCore-g6NEEnZr.js → highlightCore-CGEYh7VF.js} +1 -1
- package/web/dist/assets/{index-D8eK9o3f.css → index-1KospU2G.css} +1 -1
- package/web/dist/assets/{index-DcsDS7Ir.js → index-Cti8k_M1.js} +6 -6
- package/web/dist/index.html +2 -2
package/dist/main.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/main.ts
|
|
2
2
|
import fs38 from "node:fs";
|
|
3
3
|
import os16 from "node:os";
|
|
4
|
-
import
|
|
4
|
+
import path42 from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import fs10 from "node:fs";
|
|
8
|
-
import
|
|
8
|
+
import path14 from "node:path";
|
|
9
9
|
import { z as z6 } from "zod";
|
|
10
10
|
|
|
11
11
|
// src/platform/paths.ts
|
|
@@ -494,8 +494,8 @@ async function commentSource(url, ctx) {
|
|
|
494
494
|
const offline = ctx.offline();
|
|
495
495
|
if (offline) throw new Error(offline);
|
|
496
496
|
const repo = `${link.owner}/${link.name}`;
|
|
497
|
-
const
|
|
498
|
-
const answer = await ctx.ghApi([
|
|
497
|
+
const path43 = link.what === "comment" ? `repos/${repo}/issues/comments/${link.id}` : link.what === "review comment" ? `repos/${repo}/pulls/comments/${link.id}` : `repos/${repo}/pulls/${link.number}/reviews/${link.id}`;
|
|
498
|
+
const answer = await ctx.ghApi([path43], GH_TIMEOUT_MS);
|
|
499
499
|
if (!answer.ok) throw new Error(`GitHub did not give it: ${ghReason(answer)}`);
|
|
500
500
|
let c;
|
|
501
501
|
try {
|
|
@@ -2067,20 +2067,21 @@ var USAGE_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
|
2067
2067
|
// src/accounts/store.ts
|
|
2068
2068
|
import fs9 from "node:fs";
|
|
2069
2069
|
import os5 from "node:os";
|
|
2070
|
-
import
|
|
2070
|
+
import path13 from "node:path";
|
|
2071
2071
|
|
|
2072
2072
|
// src/accounts/about-file.ts
|
|
2073
2073
|
import fs8 from "node:fs";
|
|
2074
|
-
import
|
|
2074
|
+
import path12 from "node:path";
|
|
2075
2075
|
|
|
2076
2076
|
// src/accounts/signin-store.ts
|
|
2077
2077
|
import { createHash } from "node:crypto";
|
|
2078
2078
|
import fs7 from "node:fs";
|
|
2079
2079
|
import os4 from "node:os";
|
|
2080
|
-
import
|
|
2080
|
+
import path11 from "node:path";
|
|
2081
2081
|
|
|
2082
2082
|
// src/accounts/signin.ts
|
|
2083
2083
|
import { execFile as execFile2 } from "node:child_process";
|
|
2084
|
+
import path10 from "node:path";
|
|
2084
2085
|
|
|
2085
2086
|
// src/runners/claude-program.ts
|
|
2086
2087
|
import { execFile } from "node:child_process";
|
|
@@ -2395,6 +2396,16 @@ async function chooseClaude(setting = "installed", look = realLookup()) {
|
|
|
2395
2396
|
}
|
|
2396
2397
|
|
|
2397
2398
|
// src/accounts/signin.ts
|
|
2399
|
+
function signInKind(method2, subscriptionType) {
|
|
2400
|
+
if (method2 === "oauth_token") return "token";
|
|
2401
|
+
if (method2 === "api_key") return "key";
|
|
2402
|
+
if (method2 !== "claude.ai") return null;
|
|
2403
|
+
if (subscriptionType === null) return "token";
|
|
2404
|
+
return typeof subscriptionType === "string" && subscriptionType ? "login" : null;
|
|
2405
|
+
}
|
|
2406
|
+
function shownSignIn(found) {
|
|
2407
|
+
return found?.kind ?? (found?.state === "in" ? "other" : null);
|
|
2408
|
+
}
|
|
2398
2409
|
function readAuthStatus(result) {
|
|
2399
2410
|
const at2 = Date.now();
|
|
2400
2411
|
if (result.error) return { state: "unknown", method: null, detail: result.error, checkedAt: at2 };
|
|
@@ -2405,6 +2416,12 @@ function readAuthStatus(result) {
|
|
|
2405
2416
|
const method2 = typeof parsed.authMethod === "string" && parsed.authMethod !== "none" ? parsed.authMethod : null;
|
|
2406
2417
|
return parsed.loggedIn ? { state: "in", method: method2, detail: null, checkedAt: at2 } : { state: "out", method: null, detail: null, checkedAt: at2 };
|
|
2407
2418
|
}
|
|
2419
|
+
function readSignIn(result) {
|
|
2420
|
+
const found = readAuthStatus(result);
|
|
2421
|
+
if (found.state !== "in") return found;
|
|
2422
|
+
const said = jsonOf(result.stdout);
|
|
2423
|
+
return { ...found, kind: signInKind(said.authMethod, said.subscriptionType) };
|
|
2424
|
+
}
|
|
2408
2425
|
function jsonOf(text2) {
|
|
2409
2426
|
try {
|
|
2410
2427
|
const v = JSON.parse(text2);
|
|
@@ -2427,7 +2444,7 @@ async function checkSignIn(env, opts = {}) {
|
|
|
2427
2444
|
if (!binary) return { state: "unknown", method: null, detail: "no Claude Code to ask: it is not installed, or too old (see Settings \u2192 Claude Code)", checkedAt: Date.now() };
|
|
2428
2445
|
const settings = opts.where?.settings && Object.keys(opts.where.settings).length ? ["--settings", JSON.stringify(opts.where.settings)] : [];
|
|
2429
2446
|
const { command, args } = commandFor(binary);
|
|
2430
|
-
return
|
|
2447
|
+
return readSignIn(await (opts.run ?? runCommand)(command, [...args, ...settings, "auth", "status", "--json"], env, opts.timeoutMs ?? 15e3, opts.where?.cwd));
|
|
2431
2448
|
}
|
|
2432
2449
|
function shellFolder(folder) {
|
|
2433
2450
|
return folder.startsWith("~/") ? `"$HOME/${folder.slice(2)}"` : `'${folder}'`;
|
|
@@ -2438,7 +2455,7 @@ function dockerExecArgs(place, interactive = null) {
|
|
|
2438
2455
|
async function checkContainerSignIn(place, folder, opts = {}) {
|
|
2439
2456
|
const script = folder === null ? 'D=$(mktemp -d) || exit 1; CLAUDE_SECURESTORAGE_CONFIG_DIR="$D" env -u ANTHROPIC_API_KEY -u CLAUDE_CODE_OAUTH_TOKEN claude auth status --json; r=$?; rm -rf "$D"; exit $r' : `CLAUDE_SECURESTORAGE_CONFIG_DIR=${shellFolder(folder)} exec env -u ANTHROPIC_API_KEY -u CLAUDE_CODE_OAUTH_TOKEN claude auth status --json`;
|
|
2440
2457
|
const result = await (opts.run ?? runCommand)("docker", [...dockerExecArgs(place), "sh", "-c", script], process.env, opts.timeoutMs ?? 2e4);
|
|
2441
|
-
return
|
|
2458
|
+
return readSignIn(result);
|
|
2442
2459
|
}
|
|
2443
2460
|
function signInHelp(account, platform = process.platform) {
|
|
2444
2461
|
if (account.kind === "default") {
|
|
@@ -2451,6 +2468,11 @@ function signInHelp(account, platform = process.platform) {
|
|
|
2451
2468
|
function signInCommand(dir, platform = process.platform) {
|
|
2452
2469
|
return platform === "win32" ? `$env:CLAUDE_SECURESTORAGE_CONFIG_DIR="${dir}"; claude` : `CLAUDE_SECURESTORAGE_CONFIG_DIR="${dir}" claude`;
|
|
2453
2470
|
}
|
|
2471
|
+
function tokenCommand(name, how) {
|
|
2472
|
+
const run8 = how.packaged ? "ledung" : `node "${path10.join(how.appDir, "src", "cli.ts")}"`;
|
|
2473
|
+
const data = !how.dataDir ? "" : how.platform === "win32" ? `$env:LEDUNG_DATA_DIR="${how.dataDir}"; ` : `LEDUNG_DATA_DIR="${how.dataDir}" `;
|
|
2474
|
+
return `${data}${run8} account token ${name}`;
|
|
2475
|
+
}
|
|
2454
2476
|
function containerSignInCommand(place, folder) {
|
|
2455
2477
|
const dir = folder.startsWith("~/") ? `$HOME/${folder.slice(2)}` : folder;
|
|
2456
2478
|
return `docker ${dockerExecArgs(place, "-it").join(" ")} sh -c 'CLAUDE_SECURESTORAGE_CONFIG_DIR=${dir} exec claude'`;
|
|
@@ -2496,7 +2518,14 @@ async function ghApiAnswer(command, args, input, timeoutMs) {
|
|
|
2496
2518
|
|
|
2497
2519
|
// src/accounts/signin-store.ts
|
|
2498
2520
|
var CREDENTIALS = ".credentials.json";
|
|
2499
|
-
function
|
|
2521
|
+
function localUser(read = () => os4.userInfo().username) {
|
|
2522
|
+
try {
|
|
2523
|
+
return read();
|
|
2524
|
+
} catch {
|
|
2525
|
+
return process.env.USER ?? process.env.LOGNAME ?? "";
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function keychainEntry(folder, user = localUser()) {
|
|
2500
2529
|
return { service: `Claude Code-credentials-${createHash("sha256").update(folder.normalize("NFC")).digest("hex").slice(0, 8)}`, account: user };
|
|
2501
2530
|
}
|
|
2502
2531
|
async function removeSignIn(place, platform = process.platform, run8 = runWithInput) {
|
|
@@ -2507,7 +2536,7 @@ async function removeSignIn(place, platform = process.platform, run8 = runWithIn
|
|
|
2507
2536
|
const { service, account } = keychainEntry(place.folder);
|
|
2508
2537
|
await run8("security", ["delete-generic-password", "-a", account, "-s", service], null, 2e4);
|
|
2509
2538
|
} else {
|
|
2510
|
-
fs7.rmSync(
|
|
2539
|
+
fs7.rmSync(path11.join(place.folder, CREDENTIALS), { force: true });
|
|
2511
2540
|
}
|
|
2512
2541
|
} catch {
|
|
2513
2542
|
}
|
|
@@ -2541,7 +2570,7 @@ function aboutText(a, platform = process.platform) {
|
|
|
2541
2570
|
return lines.join(platform === "win32" ? "\r\n" : "\n") + (platform === "win32" ? "\r\n" : "\n");
|
|
2542
2571
|
}
|
|
2543
2572
|
function writeAboutFile(a, platform = process.platform) {
|
|
2544
|
-
const file =
|
|
2573
|
+
const file = path12.join(a.configDir, ABOUT_FILE);
|
|
2545
2574
|
const text2 = aboutText(a, platform);
|
|
2546
2575
|
try {
|
|
2547
2576
|
if (!fs8.statSync(a.configDir).isDirectory()) return false;
|
|
@@ -2559,11 +2588,12 @@ var DEFAULT_ACCOUNT = "default";
|
|
|
2559
2588
|
var AccountError = class extends Error {
|
|
2560
2589
|
};
|
|
2561
2590
|
var SIGN_IN_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
2591
|
+
var LOGIN_ENV = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", SIGN_IN_ENV];
|
|
2562
2592
|
function defaultContainerDir(name) {
|
|
2563
2593
|
return `~/.ledung-accounts/${name}`;
|
|
2564
2594
|
}
|
|
2565
2595
|
function canonicalFolder(folder) {
|
|
2566
|
-
return
|
|
2596
|
+
return path13.resolve(folder);
|
|
2567
2597
|
}
|
|
2568
2598
|
function canonicalContainerFolder(folder) {
|
|
2569
2599
|
const f = folder.trim().replace(/\/+$/, "");
|
|
@@ -2576,14 +2606,14 @@ function sameFolder(a, b, platform = process.platform) {
|
|
|
2576
2606
|
return platform === "win32" || platform === "darwin" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
2577
2607
|
}
|
|
2578
2608
|
function sharedLoginFolders() {
|
|
2579
|
-
return [
|
|
2609
|
+
return [path13.join(os5.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
|
|
2580
2610
|
}
|
|
2581
2611
|
var SHARED_CONTAINER_FOLDERS = ["~/.claude"];
|
|
2582
2612
|
var AccountStore = class {
|
|
2583
2613
|
db;
|
|
2584
2614
|
/** Ledung's own accounts folder: the default login folders. */
|
|
2585
2615
|
dir;
|
|
2586
|
-
constructor(db, dir =
|
|
2616
|
+
constructor(db, dir = path13.join(userDataDir(), "accounts")) {
|
|
2587
2617
|
this.db = db;
|
|
2588
2618
|
this.dir = dir;
|
|
2589
2619
|
}
|
|
@@ -2596,7 +2626,7 @@ var AccountStore = class {
|
|
|
2596
2626
|
}
|
|
2597
2627
|
/** The default login folder on this computer for an account of this name. */
|
|
2598
2628
|
defaultFolder(name) {
|
|
2599
|
-
return canonicalFolder(
|
|
2629
|
+
return canonicalFolder(path13.join(this.dir, name));
|
|
2600
2630
|
}
|
|
2601
2631
|
/** Adds an account: its login folder is made, empty until someone signs in to it. */
|
|
2602
2632
|
add(a, whatItIsFor) {
|
|
@@ -2844,7 +2874,7 @@ function withoutNotes(raw) {
|
|
|
2844
2874
|
}
|
|
2845
2875
|
var ClaudeAccountSetting = z6.preprocess(withoutNotes, z6.object({
|
|
2846
2876
|
whatItIsFor: z6.string().trim().max(200).optional(),
|
|
2847
|
-
signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) =>
|
|
2877
|
+
signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path14.isAbsolute(f), "give the whole path, such as D:\\Claude sign-ins\\work or /Users/you/claude-signins/work").refine((f) => !sharedLoginFolders().some((s) => sameFolder(s, f)), "that is this computer's own Claude Code folder; an account needs a folder of its own").optional(),
|
|
2848
2878
|
signInFolderInContainers: z6.string().trim().min(1, "give a folder, or leave the field out for ~/.ledung-accounts/<name>").refine((f) => canonicalContainerFolder(f) !== null, "starts with ~/ or /, of letters, digits and . _ / + - only").refine((f) => canonicalContainerFolder(f) !== "~/.claude", "that is the container user's own Claude Code folder; an account needs a folder of its own").optional()
|
|
2849
2879
|
}).strict());
|
|
2850
2880
|
var ClaudeAccountsSetting = z6.preprocess(withoutNotes, z6.record(z6.string(), ClaudeAccountSetting)).default({}).superRefine((accounts, ctx) => {
|
|
@@ -2893,7 +2923,7 @@ var LocalProject = z6.object({
|
|
|
2893
2923
|
* Folders outside the project folder where its sessions may also work, such as where its git worktrees
|
|
2894
2924
|
* go (start_session's cwd, decision of 2026-10-14). None: only the project folder and folders inside it.
|
|
2895
2925
|
*/
|
|
2896
|
-
extraFolders: z6.array(z6.string().min(1).refine((f) =>
|
|
2926
|
+
extraFolders: z6.array(z6.string().min(1).refine((f) => path14.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
|
|
2897
2927
|
model: z6.string().optional(),
|
|
2898
2928
|
/** Its manager's model; none: the same as its sessions (decision of 2026-10-13). */
|
|
2899
2929
|
managerModel: z6.string().optional(),
|
|
@@ -3010,7 +3040,7 @@ var Config = z6.object({
|
|
|
3010
3040
|
* One that is missing or older than the SDK needs is never run: sessions are refused and the page says how to fix it
|
|
3011
3041
|
* (Martin, 2026-10-27). "bundled" only when set here. Containers run their own.
|
|
3012
3042
|
*/
|
|
3013
|
-
claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" ||
|
|
3043
|
+
claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path14.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
|
|
3014
3044
|
/** The shell a "!" command from the message box runs in on Windows (issue #212): Git Bash, as Claude's Bash tool, or PowerShell. */
|
|
3015
3045
|
windowsShell: z6.enum(["bash", "powershell"]).default("bash"),
|
|
3016
3046
|
board: BoardSetting,
|
|
@@ -3043,9 +3073,9 @@ function boardConfig(config) {
|
|
|
3043
3073
|
}
|
|
3044
3074
|
function configPath(packaged = PACKAGED) {
|
|
3045
3075
|
const set = envSetting("CONFIG");
|
|
3046
|
-
if (set) return
|
|
3047
|
-
if (packaged) return
|
|
3048
|
-
return
|
|
3076
|
+
if (set) return path14.resolve(set);
|
|
3077
|
+
if (packaged) return path14.join(userDataDir(), "ledung.config.json");
|
|
3078
|
+
return path14.join(appDir(), "ledung.config.local.json");
|
|
3049
3079
|
}
|
|
3050
3080
|
function parseConfig(raw) {
|
|
3051
3081
|
const result = Config.safeParse(raw);
|
|
@@ -3084,7 +3114,7 @@ var FIRST_CONFIG = {
|
|
|
3084
3114
|
port: DEFAULT_PORT2
|
|
3085
3115
|
};
|
|
3086
3116
|
function createDefaultConfig(file = configPath()) {
|
|
3087
|
-
fs10.mkdirSync(
|
|
3117
|
+
fs10.mkdirSync(path14.dirname(file), { recursive: true });
|
|
3088
3118
|
try {
|
|
3089
3119
|
fs10.writeFileSync(file, `${JSON.stringify(FIRST_CONFIG, null, 2)}
|
|
3090
3120
|
`, { flag: "wx" });
|
|
@@ -3163,7 +3193,7 @@ function configProblem(text2) {
|
|
|
3163
3193
|
|
|
3164
3194
|
// src/config-watch.ts
|
|
3165
3195
|
import fs11 from "node:fs";
|
|
3166
|
-
import
|
|
3196
|
+
import path15 from "node:path";
|
|
3167
3197
|
var exercised358 = false;
|
|
3168
3198
|
var ConfigWatcher = class {
|
|
3169
3199
|
opts;
|
|
@@ -3302,9 +3332,9 @@ function realFile(file) {
|
|
|
3302
3332
|
full = fs11.realpathSync.native(file);
|
|
3303
3333
|
} catch (e) {
|
|
3304
3334
|
if (e.code !== "ENOENT") throw e;
|
|
3305
|
-
return { dir: fs11.realpathSync.native(
|
|
3335
|
+
return { dir: fs11.realpathSync.native(path15.dirname(file)), name: path15.basename(file) };
|
|
3306
3336
|
}
|
|
3307
|
-
return { dir:
|
|
3337
|
+
return { dir: path15.dirname(full), name: path15.basename(full) };
|
|
3308
3338
|
}
|
|
3309
3339
|
|
|
3310
3340
|
// src/platform/open-browser.ts
|
|
@@ -3336,12 +3366,12 @@ function lanNames(setting) {
|
|
|
3336
3366
|
import { execFile as execFile4 } from "node:child_process";
|
|
3337
3367
|
import fs13 from "node:fs";
|
|
3338
3368
|
import os7 from "node:os";
|
|
3339
|
-
import
|
|
3369
|
+
import path17 from "node:path";
|
|
3340
3370
|
|
|
3341
3371
|
// src/server/security.ts
|
|
3342
3372
|
import crypto3 from "node:crypto";
|
|
3343
3373
|
import fs12 from "node:fs";
|
|
3344
|
-
import
|
|
3374
|
+
import path16 from "node:path";
|
|
3345
3375
|
|
|
3346
3376
|
// src/platform/running.ts
|
|
3347
3377
|
import crypto from "node:crypto";
|
|
@@ -3540,7 +3570,7 @@ var Devices = class {
|
|
|
3540
3570
|
|
|
3541
3571
|
// src/server/security.ts
|
|
3542
3572
|
var COOKIE_NAME = "ledung_auth";
|
|
3543
|
-
function loadOrCreateToken(file =
|
|
3573
|
+
function loadOrCreateToken(file = path16.join(userDataDir(), "auth-token")) {
|
|
3544
3574
|
if (fs12.existsSync(file)) {
|
|
3545
3575
|
const token2 = fs12.readFileSync(file, "utf8").trim();
|
|
3546
3576
|
if (/^[0-9a-f]{64}$/.test(token2)) return token2;
|
|
@@ -3732,18 +3762,18 @@ var LABELS = { desktop: "Desktop", "start-menu": "Start menu", applications: "Ap
|
|
|
3732
3762
|
var ENV_FILE = "shortcut.env";
|
|
3733
3763
|
var LOG_FILE_SETTING = "LEDUNG_LOG_FILE";
|
|
3734
3764
|
function launchTarget(dataDir = userDataDir(), app = appDir(), packaged = PACKAGED) {
|
|
3735
|
-
const releaseLauncher =
|
|
3736
|
-
const fromRelease = !
|
|
3737
|
-
const script = fromRelease ? releaseLauncher :
|
|
3765
|
+
const releaseLauncher = path17.join(dataDir, "launcher.mjs");
|
|
3766
|
+
const fromRelease = !path17.relative(path17.join(dataDir, "app"), app).startsWith("..") && fs13.existsSync(releaseLauncher);
|
|
3767
|
+
const script = fromRelease ? releaseLauncher : path17.join(app, packaged ? path17.join("dist", "cli.js") : path17.join("src", "cli.ts"));
|
|
3738
3768
|
const env = {};
|
|
3739
3769
|
for (const name of ["DATA_DIR", "CONFIG"]) {
|
|
3740
3770
|
const value = envSetting(name);
|
|
3741
|
-
if (value) env[`LEDUNG_${name}`] =
|
|
3771
|
+
if (value) env[`LEDUNG_${name}`] = path17.resolve(value);
|
|
3742
3772
|
}
|
|
3743
3773
|
return Object.keys(env).length ? { node: process.execPath, script, env } : { node: process.execPath, script };
|
|
3744
3774
|
}
|
|
3745
3775
|
function startsInBackground(launch) {
|
|
3746
|
-
return
|
|
3776
|
+
return path17.basename(launch.script) !== "launcher.mjs";
|
|
3747
3777
|
}
|
|
3748
3778
|
function envFileText(env) {
|
|
3749
3779
|
return Object.entries(env).map(([name, value]) => {
|
|
@@ -3765,9 +3795,9 @@ function starterText(head, tail2) {
|
|
|
3765
3795
|
].join("\r\n");
|
|
3766
3796
|
}
|
|
3767
3797
|
function iconFiles(app = appDir()) {
|
|
3768
|
-
const built =
|
|
3769
|
-
const dir = fs13.existsSync(
|
|
3770
|
-
return { png256:
|
|
3798
|
+
const built = path17.join(app, "web", "dist");
|
|
3799
|
+
const dir = fs13.existsSync(path17.join(built, "icon-256.png")) ? built : path17.join(app, "web", "public");
|
|
3800
|
+
return { png256: path17.join(dir, "icon-256.png"), png512: path17.join(dir, "icon-512.png") };
|
|
3771
3801
|
}
|
|
3772
3802
|
function powershell(script, env) {
|
|
3773
3803
|
return new Promise((resolve, reject) => {
|
|
@@ -3783,20 +3813,20 @@ async function windowsFolders() {
|
|
|
3783
3813
|
return { desktop, "start-menu": programs, startup };
|
|
3784
3814
|
}
|
|
3785
3815
|
function macFolders() {
|
|
3786
|
-
const startup =
|
|
3816
|
+
const startup = path17.join(os7.homedir(), "Library", "LaunchAgents");
|
|
3787
3817
|
try {
|
|
3788
3818
|
fs13.accessSync("/Applications", fs13.constants.W_OK);
|
|
3789
3819
|
return { applications: "/Applications", startup };
|
|
3790
3820
|
} catch {
|
|
3791
|
-
return { applications:
|
|
3821
|
+
return { applications: path17.join(os7.homedir(), "Applications"), startup };
|
|
3792
3822
|
}
|
|
3793
3823
|
}
|
|
3794
3824
|
function linuxFolders() {
|
|
3795
|
-
return { startup:
|
|
3825
|
+
return { startup: path17.join(process.env.XDG_CONFIG_HOME ?? path17.join(os7.homedir(), ".config"), "autostart") };
|
|
3796
3826
|
}
|
|
3797
3827
|
function ownDataFolders(platform) {
|
|
3798
|
-
const root =
|
|
3799
|
-
return platform === "win32" ? { desktop:
|
|
3828
|
+
const root = path17.join(userDataDir(), "shortcuts");
|
|
3829
|
+
return platform === "win32" ? { desktop: path17.join(root, "Desktop"), "start-menu": path17.join(root, "Start menu"), startup: path17.join(root, "Startup") } : platform === "darwin" ? { applications: path17.join(root, "Applications"), startup: path17.join(root, "LaunchAgents") } : { startup: path17.join(root, "autostart") };
|
|
3800
3830
|
}
|
|
3801
3831
|
var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3802
3832
|
var winQuote = (s) => `"${s}"`;
|
|
@@ -3871,7 +3901,7 @@ var Shortcuts = class {
|
|
|
3871
3901
|
return ids.flatMap((id) => {
|
|
3872
3902
|
const folder = folders[id];
|
|
3873
3903
|
if (!folder) return [];
|
|
3874
|
-
const file =
|
|
3904
|
+
const file = path17.join(folder, kind === "windows" ? "Ledung.lnk" : "Ledung.app");
|
|
3875
3905
|
return [{ id, label: LABELS[id], file, present: kind === "windows" ? fs13.existsSync(file) : isOurApp(file) }];
|
|
3876
3906
|
});
|
|
3877
3907
|
}
|
|
@@ -3880,7 +3910,7 @@ var Shortcuts = class {
|
|
|
3880
3910
|
const folder = (await this.folders()).startup;
|
|
3881
3911
|
if (!folder) return null;
|
|
3882
3912
|
const kind = this.kind();
|
|
3883
|
-
return
|
|
3913
|
+
return path17.join(folder, SIGN_IN_FILES[kind]);
|
|
3884
3914
|
}
|
|
3885
3915
|
async signInView() {
|
|
3886
3916
|
const label = SIGN_IN_LABELS[this.kind()];
|
|
@@ -3891,7 +3921,7 @@ var Shortcuts = class {
|
|
|
3891
3921
|
async view() {
|
|
3892
3922
|
const kind = this.kind();
|
|
3893
3923
|
const asked = this.opts.meta.get(ASKED_KEY) !== null;
|
|
3894
|
-
const log = { file: this.logFile(), active: envSetting("LOG_FILE") !== void 0 &&
|
|
3924
|
+
const log = { file: this.logFile(), active: envSetting("LOG_FILE") !== void 0 && path17.resolve(envSetting("LOG_FILE")) === path17.resolve(this.logFile()) };
|
|
3895
3925
|
const signIn = await this.signInView();
|
|
3896
3926
|
if (kind === "other") {
|
|
3897
3927
|
return { kind, available: false, reason: "Shortcuts are made on Windows and macOS. Here, start Ledung with ledung.sh or the ledung command.", asked, places: [], signIn, log };
|
|
@@ -3918,7 +3948,7 @@ var Shortcuts = class {
|
|
|
3918
3948
|
if (this.kind() === "windows") await this.addWindows(places.filter((p) => !p.present));
|
|
3919
3949
|
else {
|
|
3920
3950
|
for (const p of places.filter((p2) => !p2.present)) {
|
|
3921
|
-
if (fs13.existsSync(p.file)) throw new Error(`There is already a Ledung.app in ${
|
|
3951
|
+
if (fs13.existsSync(p.file)) throw new Error(`There is already a Ledung.app in ${path17.dirname(p.file)} that Ledung didn't make. Rename or remove it, then add the shortcut again.`);
|
|
3922
3952
|
this.addMac(p.file);
|
|
3923
3953
|
}
|
|
3924
3954
|
}
|
|
@@ -3931,16 +3961,16 @@ var Shortcuts = class {
|
|
|
3931
3961
|
return this.view();
|
|
3932
3962
|
}
|
|
3933
3963
|
icoFile() {
|
|
3934
|
-
return
|
|
3964
|
+
return path17.join(this.opts.iconDir, "Ledung.ico");
|
|
3935
3965
|
}
|
|
3936
3966
|
envFile() {
|
|
3937
|
-
return
|
|
3967
|
+
return path17.join(this.opts.iconDir, ENV_FILE);
|
|
3938
3968
|
}
|
|
3939
3969
|
starterFile() {
|
|
3940
|
-
return
|
|
3970
|
+
return path17.join(this.opts.iconDir, STARTER_FILE);
|
|
3941
3971
|
}
|
|
3942
3972
|
logFile() {
|
|
3943
|
-
return this.opts.logFile ??
|
|
3973
|
+
return this.opts.logFile ?? path17.join(this.opts.iconDir, LOG_FILE);
|
|
3944
3974
|
}
|
|
3945
3975
|
/** The env file and the start script, which the icons and the start at sign-in share. */
|
|
3946
3976
|
removeShared() {
|
|
@@ -3972,11 +4002,11 @@ var Shortcuts = class {
|
|
|
3972
4002
|
}
|
|
3973
4003
|
/** The Windows Script Host that runs the start script with no window, and its arguments. */
|
|
3974
4004
|
windowsStart(extra) {
|
|
3975
|
-
const wscript =
|
|
4005
|
+
const wscript = path17.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
|
|
3976
4006
|
return { target: wscript, arguments: ["//Nologo", this.starterFile(), ...extra].map(winQuote).join(" ") };
|
|
3977
4007
|
}
|
|
3978
4008
|
async writeLinks(files, extra, icon, description) {
|
|
3979
|
-
for (const file of files) fs13.mkdirSync(
|
|
4009
|
+
for (const file of files) fs13.mkdirSync(path17.dirname(file), { recursive: true });
|
|
3980
4010
|
const spec = { files, ...this.windowsStart(extra), workdir: os7.homedir(), icon: icon ? `${icon},0` : "", description };
|
|
3981
4011
|
await powershell([
|
|
3982
4012
|
"$spec = $env:SHORTCUT_SPEC | ConvertFrom-Json",
|
|
@@ -4007,7 +4037,7 @@ var Shortcuts = class {
|
|
|
4007
4037
|
return;
|
|
4008
4038
|
}
|
|
4009
4039
|
this.writeEnvFile();
|
|
4010
|
-
fs13.mkdirSync(
|
|
4040
|
+
fs13.mkdirSync(path17.dirname(file), { recursive: true });
|
|
4011
4041
|
const { node, script } = this.opts.launch;
|
|
4012
4042
|
const kind = this.kind();
|
|
4013
4043
|
if (kind === "windows") {
|
|
@@ -4037,18 +4067,18 @@ var Shortcuts = class {
|
|
|
4037
4067
|
*/
|
|
4038
4068
|
addMac(app) {
|
|
4039
4069
|
const { node, script } = this.opts.launch;
|
|
4040
|
-
const contents =
|
|
4041
|
-
fs13.mkdirSync(
|
|
4042
|
-
fs13.mkdirSync(
|
|
4070
|
+
const contents = path17.join(app, "Contents");
|
|
4071
|
+
fs13.mkdirSync(path17.join(contents, "MacOS"), { recursive: true });
|
|
4072
|
+
fs13.mkdirSync(path17.join(contents, "Resources"), { recursive: true });
|
|
4043
4073
|
let version = "1";
|
|
4044
4074
|
try {
|
|
4045
|
-
version = String(JSON.parse(fs13.readFileSync(
|
|
4075
|
+
version = String(JSON.parse(fs13.readFileSync(path17.join(appDir(), "package.json"), "utf8")).version ?? "1");
|
|
4046
4076
|
} catch {
|
|
4047
4077
|
}
|
|
4048
|
-
fs13.writeFileSync(
|
|
4049
|
-
fs13.writeFileSync(
|
|
4078
|
+
fs13.writeFileSync(path17.join(contents, "Info.plist"), PLIST(version));
|
|
4079
|
+
fs13.writeFileSync(path17.join(contents, "Resources", "Ledung.icns"), icnsFromPngs([fs13.readFileSync(this.opts.icons.png256), fs13.readFileSync(this.opts.icons.png512)]));
|
|
4050
4080
|
const run8 = [...this.nodeOptions(), script].map(shQuote).join(" ");
|
|
4051
|
-
fs13.writeFileSync(
|
|
4081
|
+
fs13.writeFileSync(path17.join(contents, "MacOS", "Ledung"), [
|
|
4052
4082
|
"#!/bin/sh",
|
|
4053
4083
|
"# Opens a running Ledung's page, or starts Ledung in the background. Made by Ledung's Settings (issues #415, #456).",
|
|
4054
4084
|
...findNode(node),
|
|
@@ -4093,7 +4123,7 @@ function signInScript(node, run8, fallbackShell) {
|
|
|
4093
4123
|
}
|
|
4094
4124
|
function isOurApp(app) {
|
|
4095
4125
|
try {
|
|
4096
|
-
return fs13.readFileSync(
|
|
4126
|
+
return fs13.readFileSync(path17.join(app, "Contents", "Info.plist"), "utf8").includes(`<string>${BUNDLE_ID}</string>`);
|
|
4097
4127
|
} catch {
|
|
4098
4128
|
return false;
|
|
4099
4129
|
}
|
|
@@ -4105,13 +4135,13 @@ import fs32 from "node:fs";
|
|
|
4105
4135
|
import http from "node:http";
|
|
4106
4136
|
import os15 from "node:os";
|
|
4107
4137
|
import tls3 from "node:tls";
|
|
4108
|
-
import
|
|
4138
|
+
import path35 from "node:path";
|
|
4109
4139
|
import { duplexPair } from "node:stream";
|
|
4110
4140
|
import { WebSocketServer } from "ws";
|
|
4111
4141
|
|
|
4112
4142
|
// src/server/source.ts
|
|
4113
4143
|
import fs14 from "node:fs";
|
|
4114
|
-
import
|
|
4144
|
+
import path18 from "node:path";
|
|
4115
4145
|
|
|
4116
4146
|
// src/shared/version.ts
|
|
4117
4147
|
function displayVersion(version) {
|
|
@@ -4154,7 +4184,7 @@ function sourceOffer(o) {
|
|
|
4154
4184
|
}
|
|
4155
4185
|
function readRepository(appDir2) {
|
|
4156
4186
|
try {
|
|
4157
|
-
return repositoryUrl(JSON.parse(fs14.readFileSync(
|
|
4187
|
+
return repositoryUrl(JSON.parse(fs14.readFileSync(path18.join(appDir2, "package.json"), "utf8"))) ?? DEFAULT_REPOSITORY;
|
|
4158
4188
|
} catch {
|
|
4159
4189
|
return DEFAULT_REPOSITORY;
|
|
4160
4190
|
}
|
|
@@ -4755,7 +4785,7 @@ ${lines.join("\n")}`;
|
|
|
4755
4785
|
// src/sessions/manager.ts
|
|
4756
4786
|
import fs27 from "node:fs";
|
|
4757
4787
|
import os14 from "node:os";
|
|
4758
|
-
import
|
|
4788
|
+
import path29 from "node:path";
|
|
4759
4789
|
|
|
4760
4790
|
// src/sessions/remote-control.ts
|
|
4761
4791
|
function remoteControlName(role, project, enabled) {
|
|
@@ -4828,7 +4858,7 @@ var RemoteControl = class {
|
|
|
4828
4858
|
// src/runners/shell-run.ts
|
|
4829
4859
|
import { spawn as spawn2 } from "node:child_process";
|
|
4830
4860
|
import fs15 from "node:fs";
|
|
4831
|
-
import
|
|
4861
|
+
import path19 from "node:path";
|
|
4832
4862
|
|
|
4833
4863
|
// src/platform/process-tree.ts
|
|
4834
4864
|
import { execFile as execFile5 } from "node:child_process";
|
|
@@ -4901,11 +4931,11 @@ function gitBashPath(env = process.env, exists = fs15.existsSync) {
|
|
|
4901
4931
|
if (set && exists(set)) return set;
|
|
4902
4932
|
const dirs = (env.PATH ?? env.Path ?? "").split(";").filter(Boolean);
|
|
4903
4933
|
for (const dir of dirs) {
|
|
4904
|
-
if (!exists(
|
|
4905
|
-
for (const bash of [
|
|
4934
|
+
if (!exists(path19.join(dir, "git.exe"))) continue;
|
|
4935
|
+
for (const bash of [path19.join(dir, "..", "bin", "bash.exe"), path19.join(dir, "bash.exe")]) if (exists(bash)) return path19.resolve(bash);
|
|
4906
4936
|
}
|
|
4907
|
-
for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA &&
|
|
4908
|
-
const bash = root &&
|
|
4937
|
+
for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && path19.join(env.LOCALAPPDATA, "Programs")]) {
|
|
4938
|
+
const bash = root && path19.join(root, "Git", "bin", "bash.exe");
|
|
4909
4939
|
if (bash && exists(bash)) return bash;
|
|
4910
4940
|
}
|
|
4911
4941
|
return null;
|
|
@@ -5060,16 +5090,16 @@ function bangRefusal(project, folder, others) {
|
|
|
5060
5090
|
import { execFile as execFile6 } from "node:child_process";
|
|
5061
5091
|
import fs16 from "node:fs";
|
|
5062
5092
|
import os8 from "node:os";
|
|
5063
|
-
import
|
|
5093
|
+
import path20 from "node:path";
|
|
5064
5094
|
import { promisify as promisify2 } from "node:util";
|
|
5065
5095
|
var run2 = promisify2(execFile6);
|
|
5066
5096
|
var BrowseError = class extends Error {
|
|
5067
5097
|
};
|
|
5068
5098
|
function defaultRoots(configured) {
|
|
5069
|
-
return (configured?.length ? configured : [os8.homedir()]).map((r) =>
|
|
5099
|
+
return (configured?.length ? configured : [os8.homedir()]).map((r) => path20.resolve(r));
|
|
5070
5100
|
}
|
|
5071
5101
|
function defaultProjectsFolder(configured) {
|
|
5072
|
-
return
|
|
5102
|
+
return path20.resolve(configured ?? path20.join(os8.homedir(), "Projects"));
|
|
5073
5103
|
}
|
|
5074
5104
|
function real(p) {
|
|
5075
5105
|
try {
|
|
@@ -5084,12 +5114,12 @@ function isInside(folder, roots) {
|
|
|
5084
5114
|
return roots.some((r) => {
|
|
5085
5115
|
const root = real(r);
|
|
5086
5116
|
if (!root) return false;
|
|
5087
|
-
const rel =
|
|
5088
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
5117
|
+
const rel = path20.relative(root, target);
|
|
5118
|
+
return rel === "" || !rel.startsWith("..") && !path20.isAbsolute(rel);
|
|
5089
5119
|
});
|
|
5090
5120
|
}
|
|
5091
5121
|
function gitBranch(folder) {
|
|
5092
|
-
const dotGit =
|
|
5122
|
+
const dotGit = path20.join(folder, ".git");
|
|
5093
5123
|
let stat;
|
|
5094
5124
|
try {
|
|
5095
5125
|
stat = fs16.statSync(dotGit);
|
|
@@ -5098,7 +5128,7 @@ function gitBranch(folder) {
|
|
|
5098
5128
|
}
|
|
5099
5129
|
if (!stat.isDirectory()) return "";
|
|
5100
5130
|
try {
|
|
5101
|
-
const head = fs16.readFileSync(
|
|
5131
|
+
const head = fs16.readFileSync(path20.join(dotGit, "HEAD"), "utf8").trim();
|
|
5102
5132
|
return head.startsWith("ref: refs/heads/") ? head.slice("ref: refs/heads/".length) : "";
|
|
5103
5133
|
} catch {
|
|
5104
5134
|
return "";
|
|
@@ -5106,7 +5136,7 @@ function gitBranch(folder) {
|
|
|
5106
5136
|
}
|
|
5107
5137
|
var sameFolder2 = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
5108
5138
|
function listLocal(folder, roots, projects) {
|
|
5109
|
-
const dir =
|
|
5139
|
+
const dir = path20.resolve(folder || roots[0]);
|
|
5110
5140
|
if (!isInside(dir, roots)) throw new BrowseError("That folder is outside the folders Ledung may show.");
|
|
5111
5141
|
let names;
|
|
5112
5142
|
try {
|
|
@@ -5117,7 +5147,7 @@ function listLocal(folder, roots, projects) {
|
|
|
5117
5147
|
const entries = [];
|
|
5118
5148
|
for (const d of names) {
|
|
5119
5149
|
if (d.name.startsWith(".") || d.name.startsWith("$")) continue;
|
|
5120
|
-
const full =
|
|
5150
|
+
const full = path20.join(dir, d.name);
|
|
5121
5151
|
if (d.isSymbolicLink()) {
|
|
5122
5152
|
if (!isInside(full, roots)) continue;
|
|
5123
5153
|
try {
|
|
@@ -5126,10 +5156,10 @@ function listLocal(folder, roots, projects) {
|
|
|
5126
5156
|
continue;
|
|
5127
5157
|
}
|
|
5128
5158
|
} else if (!d.isDirectory()) continue;
|
|
5129
|
-
entries.push({ name: d.name, path: full, git: gitBranch(full), project: projects.find((p) => sameFolder2(
|
|
5159
|
+
entries.push({ name: d.name, path: full, git: gitBranch(full), project: projects.find((p) => sameFolder2(path20.resolve(p.path), full))?.name ?? null });
|
|
5130
5160
|
}
|
|
5131
5161
|
entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
|
|
5132
|
-
const up =
|
|
5162
|
+
const up = path20.dirname(dir);
|
|
5133
5163
|
const isRoot = roots.some((r) => sameFolder2(r, dir));
|
|
5134
5164
|
return { path: dir, parent: !isRoot && up !== dir && isInside(up, roots) ? up : null, roots, entries };
|
|
5135
5165
|
}
|
|
@@ -5180,7 +5210,7 @@ async function listContainer(container, user, folder, projects) {
|
|
|
5180
5210
|
const [dir = "/", ...lines] = out.split("\n").map((l) => l.replace(/\r$/, ""));
|
|
5181
5211
|
const entries = lines.filter(Boolean).map((l) => {
|
|
5182
5212
|
const [kind, name = "", branch = ""] = l.split(" ");
|
|
5183
|
-
const full =
|
|
5213
|
+
const full = path20.posix.join(dir, name);
|
|
5184
5214
|
return {
|
|
5185
5215
|
name,
|
|
5186
5216
|
path: full,
|
|
@@ -5188,12 +5218,12 @@ async function listContainer(container, user, folder, projects) {
|
|
|
5188
5218
|
project: projects.find((p) => p.container === container && p.path === full)?.name ?? null
|
|
5189
5219
|
};
|
|
5190
5220
|
});
|
|
5191
|
-
return { path: dir, parent: dir === "/" ? null :
|
|
5221
|
+
return { path: dir, parent: dir === "/" ? null : path20.posix.dirname(dir), roots: ["/"], entries };
|
|
5192
5222
|
}
|
|
5193
5223
|
async function createProjectFolder(parent, name, allowed, github) {
|
|
5194
|
-
const parentDir =
|
|
5224
|
+
const parentDir = path20.resolve(parent);
|
|
5195
5225
|
if (!isInside(parentDir, allowed)) throw new BrowseError("That folder is outside the folders Ledung may use.");
|
|
5196
|
-
const dir =
|
|
5226
|
+
const dir = path20.join(parentDir, name);
|
|
5197
5227
|
if (fs16.existsSync(dir)) throw new BrowseError(`There is already a folder named "${name}" there.`);
|
|
5198
5228
|
fs16.mkdirSync(dir);
|
|
5199
5229
|
try {
|
|
@@ -5221,19 +5251,19 @@ import crypto4 from "node:crypto";
|
|
|
5221
5251
|
import { EventEmitter } from "node:events";
|
|
5222
5252
|
import fs17 from "node:fs";
|
|
5223
5253
|
import net from "node:net";
|
|
5224
|
-
import
|
|
5254
|
+
import path21 from "node:path";
|
|
5225
5255
|
import readline from "node:readline";
|
|
5226
5256
|
import { PassThrough } from "node:stream";
|
|
5227
5257
|
function hostsDir() {
|
|
5228
|
-
const dir =
|
|
5258
|
+
const dir = path21.join(userDataDir(), "hosts");
|
|
5229
5259
|
fs17.mkdirSync(dir, { recursive: true });
|
|
5230
5260
|
return dir;
|
|
5231
5261
|
}
|
|
5232
|
-
var recordFile = (sessionId) =>
|
|
5262
|
+
var recordFile = (sessionId) => path21.join(hostsDir(), `${sessionId}.json`);
|
|
5233
5263
|
function listHostRecords() {
|
|
5234
5264
|
return fs17.readdirSync(hostsDir()).filter((f) => f.endsWith(".json")).flatMap((f) => {
|
|
5235
5265
|
try {
|
|
5236
|
-
return [JSON.parse(fs17.readFileSync(
|
|
5266
|
+
return [JSON.parse(fs17.readFileSync(path21.join(hostsDir(), f), "utf8"))];
|
|
5237
5267
|
} catch {
|
|
5238
5268
|
return [];
|
|
5239
5269
|
}
|
|
@@ -5256,7 +5286,7 @@ function startHost(sessionId, launch) {
|
|
|
5256
5286
|
sessionId,
|
|
5257
5287
|
pipe: pipePath(sessionId),
|
|
5258
5288
|
graceMs: GRACE_MS,
|
|
5259
|
-
logFile:
|
|
5289
|
+
logFile: path21.join(hostsDir(), `${sessionId}.log`),
|
|
5260
5290
|
secret: crypto4.randomBytes(32).toString("hex")
|
|
5261
5291
|
};
|
|
5262
5292
|
const file = recordFile(sessionId);
|
|
@@ -5851,7 +5881,7 @@ var DockerRunner = class {
|
|
|
5851
5881
|
import { execFile as execFile9 } from "node:child_process";
|
|
5852
5882
|
import fs18 from "node:fs";
|
|
5853
5883
|
import os9 from "node:os";
|
|
5854
|
-
import
|
|
5884
|
+
import path22 from "node:path";
|
|
5855
5885
|
import { promisify as promisify5 } from "node:util";
|
|
5856
5886
|
async function readStart(file, bytes) {
|
|
5857
5887
|
try {
|
|
@@ -5910,24 +5940,24 @@ var LocalRunner = class {
|
|
|
5910
5940
|
return ghApiAnswer("gh", ["api", ...args], input, timeoutMs);
|
|
5911
5941
|
}
|
|
5912
5942
|
async pluginSettingsTexts(configDir) {
|
|
5913
|
-
const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ??
|
|
5914
|
-
const files = [...PLUGIN_FILES.config.map((f) =>
|
|
5943
|
+
const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path22.join(os9.homedir(), ".claude");
|
|
5944
|
+
const files = [...PLUGIN_FILES.config.map((f) => path22.join(dir, f)), ...PLUGIN_FILES.project.map((f) => path22.join(this.cwd, f))];
|
|
5915
5945
|
const texts = await Promise.all(files.map((f) => fs18.promises.readFile(f, "utf8").catch(() => null)));
|
|
5916
5946
|
return texts.filter((t) => t !== null);
|
|
5917
5947
|
}
|
|
5918
5948
|
async skillFiles(configDir) {
|
|
5919
|
-
const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ??
|
|
5920
|
-
const folders = async (rel) => (await fs18.promises.readdir(
|
|
5949
|
+
const dir = configDir ?? process.env.CLAUDE_CONFIG_DIR ?? path22.join(os9.homedir(), ".claude");
|
|
5950
|
+
const folders = async (rel) => (await fs18.promises.readdir(path22.join(dir, rel), { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => `${rel}/${e.name}`);
|
|
5921
5951
|
const personal = (await folders("skills")).filter((f) => f !== "skills/synced");
|
|
5922
5952
|
const synced = (await Promise.all((await folders("skills/synced")).map(folders))).flat();
|
|
5923
5953
|
const files = await Promise.all([...personal, ...synced].map(async (f) => {
|
|
5924
|
-
const text2 = await readStart(
|
|
5954
|
+
const text2 = await readStart(path22.join(dir, f, "SKILL.md"), SKILL_HEAD_BYTES);
|
|
5925
5955
|
return text2 === null ? null : { path: `${f}/SKILL.md`, text: text2 };
|
|
5926
5956
|
}));
|
|
5927
5957
|
return files.filter((f) => f !== null);
|
|
5928
5958
|
}
|
|
5929
5959
|
async readProjectFile(relative) {
|
|
5930
|
-
return fs18.promises.readFile(
|
|
5960
|
+
return fs18.promises.readFile(path22.join(this.cwd, relative), "utf8").catch(() => null);
|
|
5931
5961
|
}
|
|
5932
5962
|
async pullRequestView(pr, repo, fields, timeoutMs) {
|
|
5933
5963
|
try {
|
|
@@ -6342,12 +6372,12 @@ function cpuTimes() {
|
|
|
6342
6372
|
|
|
6343
6373
|
// src/sessions/work-folder.ts
|
|
6344
6374
|
import fs20 from "node:fs";
|
|
6345
|
-
import
|
|
6375
|
+
import path23 from "node:path";
|
|
6346
6376
|
var WorkFolderError = class extends Error {
|
|
6347
6377
|
};
|
|
6348
6378
|
function within2(root, real2) {
|
|
6349
|
-
const inside =
|
|
6350
|
-
return inside === "" || !inside.startsWith("..") && !
|
|
6379
|
+
const inside = path23.relative(root, real2);
|
|
6380
|
+
return inside === "" || !inside.startsWith("..") && !path23.isAbsolute(inside);
|
|
6351
6381
|
}
|
|
6352
6382
|
function realFolder(folder) {
|
|
6353
6383
|
try {
|
|
@@ -6361,11 +6391,11 @@ function resolveWorkFolder(projectPath, extraRoots, wanted) {
|
|
|
6361
6391
|
if (!wanted.trim()) throw new WorkFolderError("No folder given.");
|
|
6362
6392
|
const project = realFolder(projectPath);
|
|
6363
6393
|
if (!project) throw new WorkFolderError(`The project folder ${projectPath} does not exist.`);
|
|
6364
|
-
const given =
|
|
6394
|
+
const given = path23.resolve(project, wanted.trim());
|
|
6365
6395
|
if (!fs20.existsSync(given)) throw new WorkFolderError(`The folder ${given} does not exist.`);
|
|
6366
6396
|
const real2 = realFolder(given);
|
|
6367
6397
|
if (!real2) throw new WorkFolderError(`${given} is not a folder.`);
|
|
6368
|
-
if (within2(project, real2)) return
|
|
6398
|
+
if (within2(project, real2)) return path23.relative(project, real2) === "" ? null : real2;
|
|
6369
6399
|
for (const root of extraRoots) {
|
|
6370
6400
|
const realRoot = realFolder(root);
|
|
6371
6401
|
if (realRoot && within2(realRoot, real2)) return real2;
|
|
@@ -6378,7 +6408,7 @@ function resolveWorkFolder(projectPath, extraRoots, wanted) {
|
|
|
6378
6408
|
|
|
6379
6409
|
// src/sessions/files.ts
|
|
6380
6410
|
import fs21 from "node:fs";
|
|
6381
|
-
import
|
|
6411
|
+
import path24 from "node:path";
|
|
6382
6412
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
6383
6413
|
var FileError = class extends Error {
|
|
6384
6414
|
};
|
|
@@ -6393,16 +6423,16 @@ function resolveInside(root, link) {
|
|
|
6393
6423
|
let real2;
|
|
6394
6424
|
try {
|
|
6395
6425
|
realRoot = fs21.realpathSync(root);
|
|
6396
|
-
real2 = fs21.realpathSync(
|
|
6426
|
+
real2 = fs21.realpathSync(path24.resolve(realRoot, wanted));
|
|
6397
6427
|
} catch {
|
|
6398
6428
|
throw new FileError(`There is no file ${wanted} in the project.`);
|
|
6399
6429
|
}
|
|
6400
|
-
const inside =
|
|
6401
|
-
if (!inside || inside.startsWith("..") ||
|
|
6430
|
+
const inside = path24.relative(realRoot, real2);
|
|
6431
|
+
if (!inside || inside.startsWith("..") || path24.isAbsolute(inside)) {
|
|
6402
6432
|
throw new FileError("Only files inside the project folder can be shown.");
|
|
6403
6433
|
}
|
|
6404
6434
|
if (fs21.statSync(real2).isDirectory()) throw new FileError(`${inside} is a folder; only files can be shown.`);
|
|
6405
|
-
return { real: real2, inside: inside.split(
|
|
6435
|
+
return { real: real2, inside: inside.split(path24.sep).join("/") };
|
|
6406
6436
|
}
|
|
6407
6437
|
function hiddenProblem(root, link, roots, refused = []) {
|
|
6408
6438
|
const { real: typed, inside } = resolveInside(root, link);
|
|
@@ -6417,14 +6447,14 @@ function hiddenProblem(root, link, roots, refused = []) {
|
|
|
6417
6447
|
for (const folder of refused) {
|
|
6418
6448
|
const at2 = longName2(folder);
|
|
6419
6449
|
if (!at2) continue;
|
|
6420
|
-
const rel =
|
|
6421
|
-
if (rel === "" || !rel.startsWith("..") && !
|
|
6450
|
+
const rel = path24.relative(at2, real2);
|
|
6451
|
+
if (rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel)) return `${inside} is not shown: it is inside ${folder}, where settings and sign-ins are kept.`;
|
|
6422
6452
|
}
|
|
6423
6453
|
const from = (r) => {
|
|
6424
6454
|
const at2 = longName2(r);
|
|
6425
|
-
return at2 === null ? null :
|
|
6455
|
+
return at2 === null ? null : path24.relative(at2, real2);
|
|
6426
6456
|
};
|
|
6427
|
-
const under = roots.map(from).find((rel) => rel !== null && rel !== "" && !rel.startsWith("..") && !
|
|
6457
|
+
const under = roots.map(from).find((rel) => rel !== null && rel !== "" && !rel.startsWith("..") && !path24.isAbsolute(rel));
|
|
6428
6458
|
const parts2 = (under ?? from(root) ?? inside).split(/[\\/]/);
|
|
6429
6459
|
return parts2.some((p) => p.startsWith(".")) ? `${inside} is in a hidden folder or is a hidden file; a chat's file viewer doesn't show those.` : null;
|
|
6430
6460
|
}
|
|
@@ -6469,7 +6499,7 @@ function handoffProblem(root, file, now = Date.now()) {
|
|
|
6469
6499
|
throw e;
|
|
6470
6500
|
}
|
|
6471
6501
|
if (!found.content.trim()) return `The handoff file ${found.path} is empty; write the handoff first.`;
|
|
6472
|
-
const age = now - fs21.statSync(
|
|
6502
|
+
const age = now - fs21.statSync(path24.join(fs21.realpathSync(root), found.path)).mtimeMs;
|
|
6473
6503
|
if (age > HANDOFF_MAX_AGE_MS) {
|
|
6474
6504
|
return `The handoff file ${found.path} was last changed ${Math.round(age / 6e4)} minutes ago; write the handoff first (it must be changed within the last hour).`;
|
|
6475
6505
|
}
|
|
@@ -6478,7 +6508,7 @@ function handoffProblem(root, file, now = Date.now()) {
|
|
|
6478
6508
|
|
|
6479
6509
|
// src/sessions/tree.ts
|
|
6480
6510
|
import fs22 from "node:fs";
|
|
6481
|
-
import
|
|
6511
|
+
import path25 from "node:path";
|
|
6482
6512
|
|
|
6483
6513
|
// src/sessions/git-read.ts
|
|
6484
6514
|
import { spawn as spawn5 } from "node:child_process";
|
|
@@ -6560,18 +6590,18 @@ function longName(p) {
|
|
|
6560
6590
|
}
|
|
6561
6591
|
}
|
|
6562
6592
|
function within3(root, real2) {
|
|
6563
|
-
const rel =
|
|
6564
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6593
|
+
const rel = path25.relative(root, real2);
|
|
6594
|
+
return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel) ? rel : null;
|
|
6565
6595
|
}
|
|
6566
6596
|
function problem(realRoot, real2, rules, refused) {
|
|
6567
6597
|
const inside = within3(realRoot, real2);
|
|
6568
6598
|
if (inside === null) return "Only folders inside the session's folder can be listed.";
|
|
6569
|
-
if (inside.split(
|
|
6599
|
+
if (inside.split(path25.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
|
|
6570
6600
|
if (refused.some((r) => within3(r, real2) !== null)) return "Folders where settings and sign-ins are kept are not listed.";
|
|
6571
6601
|
for (const r of rules.hiddenFrom) {
|
|
6572
6602
|
const at2 = longName(r);
|
|
6573
6603
|
const rel = at2 === null ? null : within3(at2, real2);
|
|
6574
|
-
if (rel && rel.split(
|
|
6604
|
+
if (rel && rel.split(path25.sep).some(hiddenName)) return "Hidden folders and files (names starting with . or $) are not listed.";
|
|
6575
6605
|
}
|
|
6576
6606
|
return null;
|
|
6577
6607
|
}
|
|
@@ -6579,9 +6609,9 @@ async function listTree(root, rel, rules) {
|
|
|
6579
6609
|
const realRoot = longName(root);
|
|
6580
6610
|
if (!realRoot) throw new FileError(`The folder ${root} does not exist.`);
|
|
6581
6611
|
const refused = rules.refused.map(longName).filter((r) => r !== null);
|
|
6582
|
-
if (
|
|
6612
|
+
if (path25.isAbsolute(rel) && /[^\\/]/.test(rel) && within3(realRoot, longName(rel) ?? path25.resolve(rel)) === null) throw new FileError("Only folders inside the session's folder can be listed.");
|
|
6583
6613
|
const wanted = rel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
6584
|
-
const real2 = longName(
|
|
6614
|
+
const real2 = longName(path25.resolve(realRoot, wanted));
|
|
6585
6615
|
if (!real2) throw new FileError(`There is no folder ${wanted} in the session's folder.`);
|
|
6586
6616
|
const why = problem(realRoot, real2, rules, refused);
|
|
6587
6617
|
if (why) throw new FileError(why);
|
|
@@ -6592,11 +6622,11 @@ async function listTree(root, rel, rules) {
|
|
|
6592
6622
|
} catch (e) {
|
|
6593
6623
|
throw new FileError(`Can't read that folder: ${e.code ?? e.message}`);
|
|
6594
6624
|
}
|
|
6595
|
-
const at2 = within3(realRoot, real2).split(
|
|
6625
|
+
const at2 = within3(realRoot, real2).split(path25.sep).filter(Boolean).join("/");
|
|
6596
6626
|
const entries = [];
|
|
6597
6627
|
for (const d of dirents) {
|
|
6598
6628
|
if (hiddenName(d.name)) continue;
|
|
6599
|
-
const full =
|
|
6629
|
+
const full = path25.join(real2, d.name);
|
|
6600
6630
|
let dir;
|
|
6601
6631
|
let size = null;
|
|
6602
6632
|
const target = d.isSymbolicLink() ? longName(full) : full;
|
|
@@ -6745,15 +6775,15 @@ function checkLineComments(value) {
|
|
|
6745
6775
|
for (const [i, c] of value.entries()) {
|
|
6746
6776
|
const bad = (what) => ({ problem: `Line comment ${i + 1}: ${what}.` });
|
|
6747
6777
|
if (!c || typeof c !== "object") return bad("not a comment");
|
|
6748
|
-
const { path:
|
|
6749
|
-
if (typeof
|
|
6778
|
+
const { path: path43, line, side, lineText, commit, body } = c;
|
|
6779
|
+
if (typeof path43 !== "string" || !path43.trim() || path43.length > 500 || /[\r\n]/.test(path43)) return bad("the file is missing");
|
|
6750
6780
|
if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return bad("the line number is missing");
|
|
6751
6781
|
if (side !== "new" && side !== "old") return bad('the side is "new" or "old"');
|
|
6752
6782
|
if (typeof lineText !== "string") return bad("the line's text is missing");
|
|
6753
6783
|
if (typeof commit !== "string" || !/^[0-9a-f]{7,64}$/.test(commit)) return bad("the commit is missing");
|
|
6754
6784
|
if (typeof body !== "string" || !body.trim()) return bad("it is empty");
|
|
6755
6785
|
if (body.length > MAX_BODY) return bad(`it is longer than ${MAX_BODY} characters`);
|
|
6756
|
-
comments.push({ path:
|
|
6786
|
+
comments.push({ path: path43, line, side, lineText: lineText.slice(0, 2e3), commit, body: body.trim() });
|
|
6757
6787
|
}
|
|
6758
6788
|
return { comments };
|
|
6759
6789
|
}
|
|
@@ -7254,15 +7284,15 @@ ${note.trim()}` : "");
|
|
|
7254
7284
|
return about?.shots && this.store.getSession(about.sessionId)?.parentId === sessionId ? about.shots : null;
|
|
7255
7285
|
}
|
|
7256
7286
|
return shots.map((s, i) => {
|
|
7257
|
-
let
|
|
7287
|
+
let path43;
|
|
7258
7288
|
try {
|
|
7259
|
-
|
|
7289
|
+
path43 = this.ctx.shotPath(sessionId, s.file.trim().replace(/\\/g, "/"));
|
|
7260
7290
|
} catch (e) {
|
|
7261
7291
|
throw new ManagementError(`Screenshot ${i + 1} (${s.file}) can't be attached: ${e.message} Nothing was asked.`);
|
|
7262
7292
|
}
|
|
7263
7293
|
const clean = (t) => t?.trim() || null;
|
|
7264
7294
|
return {
|
|
7265
|
-
path:
|
|
7295
|
+
path: path43,
|
|
7266
7296
|
// One line: the group heads the strip and its shots' lines in the answer.
|
|
7267
7297
|
group: clean(s.group && oneLine(s.group)) ?? "Screenshots",
|
|
7268
7298
|
lookFor: s.look_for.trim(),
|
|
@@ -8835,7 +8865,6 @@ var InputQueue = class {
|
|
|
8835
8865
|
}
|
|
8836
8866
|
}
|
|
8837
8867
|
};
|
|
8838
|
-
var LOGIN_ENV = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "CLAUDE_SECURESTORAGE_CONFIG_DIR"];
|
|
8839
8868
|
function claudeEnvironment(base, accountEnv, ledungInstructions) {
|
|
8840
8869
|
const env = {};
|
|
8841
8870
|
for (const [k, v] of Object.entries(base)) if (v !== void 0 && !(accountEnv && LOGIN_ENV.includes(k))) env[k] = v;
|
|
@@ -10094,7 +10123,7 @@ function unaskedTool(toolName, alwaysAsked) {
|
|
|
10094
10123
|
// src/board/attachments.ts
|
|
10095
10124
|
import crypto5 from "node:crypto";
|
|
10096
10125
|
import fs24 from "node:fs";
|
|
10097
|
-
import
|
|
10126
|
+
import path26 from "node:path";
|
|
10098
10127
|
var MAX_ATTACHMENTS = 5;
|
|
10099
10128
|
var MAX_IMAGE_BYTES2 = 5 * 1024 * 1024;
|
|
10100
10129
|
var MAX_TEXT_BYTES = 512 * 1024;
|
|
@@ -10111,7 +10140,7 @@ function newPostKey() {
|
|
|
10111
10140
|
return crypto5.randomBytes(6).toString("hex");
|
|
10112
10141
|
}
|
|
10113
10142
|
function safeName(name) {
|
|
10114
|
-
const base =
|
|
10143
|
+
const base = path26.basename(name.replace(/\\/g, "/")).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
10115
10144
|
return (base || "file").slice(-80);
|
|
10116
10145
|
}
|
|
10117
10146
|
function evidencePath(threadId, postKey, n, kind) {
|
|
@@ -10163,7 +10192,7 @@ function readEvidence(root, link) {
|
|
|
10163
10192
|
function outboxFile(cacheDir, repo, key, n, kind) {
|
|
10164
10193
|
if (!/^[0-9a-f]{12}$/.test(key) || !Number.isInteger(n) || n < 1 || n > MAX_ATTACHMENTS) throw new FileError("Not a held attachment.");
|
|
10165
10194
|
const rel = `outbox/${key}-${n}.${EXT[kind]}`;
|
|
10166
|
-
return { file:
|
|
10195
|
+
return { file: path26.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
|
|
10167
10196
|
}
|
|
10168
10197
|
function contentProblem(ref, bytes) {
|
|
10169
10198
|
if (bytes.length !== ref.size) return `its size is ${bytes.length} bytes, not the ${ref.size} the post says`;
|
|
@@ -10176,17 +10205,17 @@ function contentProblem(ref, bytes) {
|
|
|
10176
10205
|
function repoCacheDir(cacheDir, repo) {
|
|
10177
10206
|
const parts2 = repo.split("/");
|
|
10178
10207
|
if (parts2.length !== 2 || parts2.some((p) => !/^[A-Za-z0-9_.-]+$/.test(p) || /^\.+$/.test(p))) throw new FileError(`"${repo}" is not a board repo.`);
|
|
10179
|
-
return
|
|
10208
|
+
return path26.join(cacheDir, parts2[0], parts2[1]);
|
|
10180
10209
|
}
|
|
10181
10210
|
function cacheFile(cacheDir, repo, threadId, postId, n, kind) {
|
|
10182
10211
|
if (!THREAD_ID.test(threadId)) throw new FileError(`"${threadId}" is not a thread id.`);
|
|
10183
10212
|
const post2 = postId.replace(/[^A-Za-z0-9]+/g, "-").slice(0, 60);
|
|
10184
10213
|
if (!post2 || !Number.isInteger(n) || n < 1 || n > MAX_ATTACHMENTS) throw new FileError("Not an attachment of a post.");
|
|
10185
10214
|
const rel = `${threadId}/${post2}-${n}.${EXT[kind]}`;
|
|
10186
|
-
return { file:
|
|
10215
|
+
return { file: path26.join(repoCacheDir(cacheDir, repo), ...rel.split("/")), rel };
|
|
10187
10216
|
}
|
|
10188
10217
|
function writeCache(file, bytes) {
|
|
10189
|
-
fs24.mkdirSync(
|
|
10218
|
+
fs24.mkdirSync(path26.dirname(file), { recursive: true });
|
|
10190
10219
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10191
10220
|
fs24.writeFileSync(tmp, bytes);
|
|
10192
10221
|
fs24.renameSync(tmp, file);
|
|
@@ -10292,7 +10321,7 @@ function sensitiveInText(text2, rules) {
|
|
|
10292
10321
|
|
|
10293
10322
|
// src/board/board.ts
|
|
10294
10323
|
import fs25 from "node:fs";
|
|
10295
|
-
import
|
|
10324
|
+
import path27 from "node:path";
|
|
10296
10325
|
|
|
10297
10326
|
// src/board/store.ts
|
|
10298
10327
|
var unsentId = (repoPath) => `unsent-${repoPath}`;
|
|
@@ -10958,7 +10987,7 @@ ${fenced(p.text)}${this.attachmentText(p.id)}`;
|
|
|
10958
10987
|
if (a.kind !== "text") return `- ${what}: to show it to ${this.ctx.userName}, give ask_user's screenshots file "${this.boardFile(a.cacheFile)}", or in a review its evidence file.`;
|
|
10959
10988
|
let content = "";
|
|
10960
10989
|
try {
|
|
10961
|
-
content = fs25.readFileSync(
|
|
10990
|
+
content = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), "utf8");
|
|
10962
10991
|
} catch {
|
|
10963
10992
|
}
|
|
10964
10993
|
const cut2 = content.length > TEXT_INLINE_CHARS ? `${content.slice(0, TEXT_INLINE_CHARS)}
|
|
@@ -11023,7 +11052,7 @@ ${lines.join("\n")}`;
|
|
|
11023
11052
|
if (!a.snapshot || !a.kind || !this.ctx.cacheDir) throw new BoardError("access", "it was held without a copy of its files; nothing was sent. Post again to ask anew.");
|
|
11024
11053
|
let bytes;
|
|
11025
11054
|
try {
|
|
11026
|
-
bytes = fs25.readFileSync(
|
|
11055
|
+
bytes = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")));
|
|
11027
11056
|
} catch {
|
|
11028
11057
|
throw new BoardError("access", `the copy of ${a.file} ${this.ctx.userName} was shown is gone; nothing was sent. Post again to ask anew.`);
|
|
11029
11058
|
}
|
|
@@ -11041,7 +11070,7 @@ ${lines.join("\n")}`;
|
|
|
11041
11070
|
for (const a of item.attachments) {
|
|
11042
11071
|
if (!a.snapshot) continue;
|
|
11043
11072
|
try {
|
|
11044
|
-
fs25.rmSync(
|
|
11073
|
+
fs25.rmSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.snapshot.split("/")), { force: true });
|
|
11045
11074
|
} catch (e) {
|
|
11046
11075
|
console.error(`Board: could not remove ${a.snapshot} from the cache: ${e.message}`);
|
|
11047
11076
|
}
|
|
@@ -11123,7 +11152,7 @@ ${lines.join("\n")}`;
|
|
|
11123
11152
|
let bytes = null;
|
|
11124
11153
|
if (same) {
|
|
11125
11154
|
try {
|
|
11126
|
-
bytes = fs25.readFileSync(
|
|
11155
|
+
bytes = fs25.readFileSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...same.split("/")));
|
|
11127
11156
|
} catch {
|
|
11128
11157
|
}
|
|
11129
11158
|
}
|
|
@@ -11153,7 +11182,7 @@ ${lines.join("\n")}`;
|
|
|
11153
11182
|
for (const a of due) {
|
|
11154
11183
|
if (a.cacheFile && this.ctx.cacheDir && !this.store.cacheFileInUse(a.cacheFile)) {
|
|
11155
11184
|
try {
|
|
11156
|
-
fs25.rmSync(
|
|
11185
|
+
fs25.rmSync(path27.join(repoCacheDir(this.ctx.cacheDir, this.repo), ...a.cacheFile.split("/")), { force: true });
|
|
11157
11186
|
} catch (e) {
|
|
11158
11187
|
console.error(`Board: could not remove ${a.cacheFile} from the cache: ${e.message}`);
|
|
11159
11188
|
}
|
|
@@ -11803,7 +11832,7 @@ var MacroList = z8.array(z8.object({
|
|
|
11803
11832
|
|
|
11804
11833
|
// src/sessions/workflow-profiles.ts
|
|
11805
11834
|
import fs26 from "node:fs";
|
|
11806
|
-
import
|
|
11835
|
+
import path28 from "node:path";
|
|
11807
11836
|
import { z as z9 } from "zod";
|
|
11808
11837
|
var WorkflowProfilesFile = z9.preprocess(withoutComments, z9.object({
|
|
11809
11838
|
version: z9.literal(1, { message: "version must be 1" }),
|
|
@@ -11829,7 +11858,7 @@ function settingsKeyProblem(profiles) {
|
|
|
11829
11858
|
return null;
|
|
11830
11859
|
}
|
|
11831
11860
|
function profilesFilePath(workflow) {
|
|
11832
|
-
return workflow.profilesFile ?
|
|
11861
|
+
return workflow.profilesFile ? path28.resolve(workflow.folder, workflow.profilesFile) : null;
|
|
11833
11862
|
}
|
|
11834
11863
|
function parseWorkflowProfiles(file, text2) {
|
|
11835
11864
|
if (text2 === null) return { file, problem: `The workflow's profiles file ${file} was not found.` };
|
|
@@ -12404,7 +12433,7 @@ var SessionManager = class _SessionManager {
|
|
|
12404
12433
|
if (!info) return "No such session.";
|
|
12405
12434
|
const project = this.configOf(info);
|
|
12406
12435
|
if (project.runner !== "local") {
|
|
12407
|
-
return
|
|
12436
|
+
return path29.isAbsolute(file) || file.split("/").includes("..") ? "Give the handoff file relative to the project folder, inside it." : null;
|
|
12408
12437
|
}
|
|
12409
12438
|
return handoffProblem(info.workDir ?? project.path, file);
|
|
12410
12439
|
}
|
|
@@ -12784,11 +12813,13 @@ var SessionManager = class _SessionManager {
|
|
|
12784
12813
|
accountsEvent() {
|
|
12785
12814
|
const raw = this.store.getMeta(_SessionManager.IDENTITY_KEY);
|
|
12786
12815
|
const inFile = this.config.claudeAccounts;
|
|
12787
|
-
const accounts = this.accounts.list().map((a) => inFile[a.name] ? { ...a, inConfigFile: true, whatItIsFor: inFile[a.name].whatItIsFor ?? null } : a);
|
|
12816
|
+
const accounts = this.accounts.list().map((a) => this.accountView(inFile[a.name] ? { ...a, inConfigFile: true, whatItIsFor: inFile[a.name].whatItIsFor ?? null } : a));
|
|
12817
|
+
const identity = raw ? JSON.parse(raw) : null;
|
|
12818
|
+
const defaultKind = shownSignIn(this.signIns.get(DEFAULT_ACCOUNT));
|
|
12788
12819
|
return {
|
|
12789
12820
|
type: "accounts",
|
|
12790
12821
|
accounts,
|
|
12791
|
-
defaultIdentity:
|
|
12822
|
+
defaultIdentity: defaultKind ? { email: null, organization: null, plan: null, seenAt: null, ...identity, signInKind: defaultKind } : identity,
|
|
12792
12823
|
choices: this.accountChoices(),
|
|
12793
12824
|
signedOut: this.signedOut(),
|
|
12794
12825
|
claudeProblem: this.claudeProblem,
|
|
@@ -12796,6 +12827,16 @@ var SessionManager = class _SessionManager {
|
|
|
12796
12827
|
configFileProblems: this.accountFileProblems
|
|
12797
12828
|
};
|
|
12798
12829
|
}
|
|
12830
|
+
/** An account as the page shows it: how it is signed in (from the last check) and the commands to sign it in (#85). */
|
|
12831
|
+
accountView(a) {
|
|
12832
|
+
const data = envSetting("DATA_DIR");
|
|
12833
|
+
return {
|
|
12834
|
+
...a,
|
|
12835
|
+
signInKind: shownSignIn(this.signIns.get(a.name)),
|
|
12836
|
+
loginCommand: signInCommand(a.configDir),
|
|
12837
|
+
tokenCommand: tokenCommand(a.name, { packaged: PACKAGED, appDir: appDir(), dataDir: data ? path29.resolve(data) : void 0, platform: process.platform })
|
|
12838
|
+
};
|
|
12839
|
+
}
|
|
12799
12840
|
/** What the config file's accounts block asked that couldn't be done, from its last read (issue #425). */
|
|
12800
12841
|
accountFileProblems = [];
|
|
12801
12842
|
/**
|
|
@@ -12896,7 +12937,7 @@ var SessionManager = class _SessionManager {
|
|
|
12896
12937
|
const found = await Promise.all(places.map((p) => this.checkPlace(p)));
|
|
12897
12938
|
const out = places.filter((_, i) => found[i].state === "out");
|
|
12898
12939
|
const state = out.length ? "out" : found.some((f) => f.state === "unknown") ? "unknown" : "in";
|
|
12899
|
-
const combined = { state, method: found.find((f) => f.method)?.method ?? null, detail: found.find((f) => f.detail)?.detail ?? null, checkedAt: Date.now() };
|
|
12940
|
+
const combined = { state, method: found.find((f) => f.method)?.method ?? null, kind: found.find((f) => f.kind)?.kind ?? null, detail: found.find((f) => f.detail)?.detail ?? null, checkedAt: Date.now() };
|
|
12900
12941
|
this.signIns.set(name, { ...combined, help: out.map((p) => this.placeHelp(name, p)).join(" ") });
|
|
12901
12942
|
return combined;
|
|
12902
12943
|
}
|
|
@@ -12987,7 +13028,7 @@ var SessionManager = class _SessionManager {
|
|
|
12987
13028
|
const added = this.accounts.add(a);
|
|
12988
13029
|
this.publishAccounts();
|
|
12989
13030
|
void this.checkSignIns([added.name]);
|
|
12990
|
-
return added;
|
|
13031
|
+
return this.accountView(added);
|
|
12991
13032
|
} catch (e) {
|
|
12992
13033
|
throw e instanceof AccountError ? new UserError(e.message) : e;
|
|
12993
13034
|
}
|
|
@@ -13039,12 +13080,12 @@ var SessionManager = class _SessionManager {
|
|
|
13039
13080
|
*/
|
|
13040
13081
|
workflowCheck(folder, projectConfig2, name) {
|
|
13041
13082
|
if (folder) {
|
|
13042
|
-
folder =
|
|
13083
|
+
folder = path29.resolve(folder);
|
|
13043
13084
|
if (!isInside(folder, this.allowedFolders())) throw new UserError("That folder is outside the folders Ledung may use.");
|
|
13044
13085
|
}
|
|
13045
13086
|
const mayLook = (file) => {
|
|
13046
13087
|
let at2 = file;
|
|
13047
|
-
while (!fs27.existsSync(at2) &&
|
|
13088
|
+
while (!fs27.existsSync(at2) && path29.dirname(at2) !== at2) at2 = path29.dirname(at2);
|
|
13048
13089
|
return isInside(at2, this.allowedFolders());
|
|
13049
13090
|
};
|
|
13050
13091
|
const workflows = workflowsOf(this.config);
|
|
@@ -13058,7 +13099,7 @@ var SessionManager = class _SessionManager {
|
|
|
13058
13099
|
const local = this.projectStore.list().flatMap((p) => p.config.runner === "local" ? [{ name: p.config.name, path: p.config.path }] : []);
|
|
13059
13100
|
const roots = defaultRoots(this.config.browseRoots);
|
|
13060
13101
|
const projects = this.projectsFolder;
|
|
13061
|
-
const starts = fs27.existsSync(projects) && !roots.some((r) =>
|
|
13102
|
+
const starts = fs27.existsSync(projects) && !roots.some((r) => path29.resolve(r) === projects) ? [...roots, projects] : roots;
|
|
13062
13103
|
try {
|
|
13063
13104
|
return { ...listLocal(folder, this.allowedFolders().filter((f) => fs27.existsSync(f)), local), roots: starts };
|
|
13064
13105
|
} catch (e) {
|
|
@@ -13086,10 +13127,10 @@ var SessionManager = class _SessionManager {
|
|
|
13086
13127
|
* GitHub repository; then the project, and its manager started with the task, if one is given.
|
|
13087
13128
|
*/
|
|
13088
13129
|
async createProject(opts) {
|
|
13089
|
-
const parent =
|
|
13130
|
+
const parent = path29.resolve(opts.parent || this.projectsFolder);
|
|
13090
13131
|
let settings;
|
|
13091
13132
|
try {
|
|
13092
|
-
settings = parseProject({ ...withSuggestedSensitive(opts.settings), runner: "local", path:
|
|
13133
|
+
settings = parseProject({ ...withSuggestedSensitive(opts.settings), runner: "local", path: path29.join(parent, String(opts.settings.name ?? "")) });
|
|
13093
13134
|
} catch (e) {
|
|
13094
13135
|
throw new UserError(e.message);
|
|
13095
13136
|
}
|
|
@@ -13110,7 +13151,7 @@ var SessionManager = class _SessionManager {
|
|
|
13110
13151
|
/** Adds an existing project; its manager is started when first needed (decided 2026-10-11). */
|
|
13111
13152
|
addProject(config) {
|
|
13112
13153
|
if (config?.runner === "local") {
|
|
13113
|
-
const folder =
|
|
13154
|
+
const folder = path29.resolve(String(config.path ?? ""));
|
|
13114
13155
|
if (!fs27.existsSync(folder) || !fs27.statSync(folder).isDirectory()) throw new UserError("That folder doesn't exist.");
|
|
13115
13156
|
if (!isInside(folder, this.allowedFolders())) throw new UserError("That folder is outside the folders Ledung may use.");
|
|
13116
13157
|
config = { ...config, path: folder };
|
|
@@ -13377,7 +13418,7 @@ var SessionManager = class _SessionManager {
|
|
|
13377
13418
|
chats() {
|
|
13378
13419
|
const c = this.config.chats;
|
|
13379
13420
|
return {
|
|
13380
|
-
folder:
|
|
13421
|
+
folder: path29.resolve(c.folder ?? os14.homedir()),
|
|
13381
13422
|
permissionMode: c.permissionMode,
|
|
13382
13423
|
sensitive: this.chatsSensitive(),
|
|
13383
13424
|
fromConfig: { commandPatterns: c.sensitive.commandPatterns, pathPatterns: c.sensitive.pathPatterns, mcpTools: c.sensitive.mcpTools },
|
|
@@ -13455,7 +13496,7 @@ var SessionManager = class _SessionManager {
|
|
|
13455
13496
|
* start and on every resume, so a link changed or a project locked since can't move it.
|
|
13456
13497
|
*/
|
|
13457
13498
|
chatFolder(wanted) {
|
|
13458
|
-
const given =
|
|
13499
|
+
const given = path29.resolve(this.chats().folder, wanted?.trim() || ".");
|
|
13459
13500
|
let real2;
|
|
13460
13501
|
try {
|
|
13461
13502
|
real2 = fs27.realpathSync(given);
|
|
@@ -13547,7 +13588,7 @@ var SessionManager = class _SessionManager {
|
|
|
13547
13588
|
checkWorkDir(project, info) {
|
|
13548
13589
|
if (!info.workDir) return;
|
|
13549
13590
|
const again = info.project === CHATS ? this.chatFolder(info.workDir) : this.workFolder(project, info.workDir);
|
|
13550
|
-
if (again === null ||
|
|
13591
|
+
if (again === null || path29.relative(again, info.workDir) !== "") {
|
|
13551
13592
|
throw new UserError(`The session's folder ${info.workDir} no longer leads to the same folder; start a new session.`);
|
|
13552
13593
|
}
|
|
13553
13594
|
}
|
|
@@ -13703,7 +13744,7 @@ ${firstMessage}`);
|
|
|
13703
13744
|
if (info.project === CHATS) return start;
|
|
13704
13745
|
const call = this.store.lastToolCall(info.id, ["EnterWorktree", "ExitWorktree"]);
|
|
13705
13746
|
if (!call || call.name !== "EnterWorktree" || call.isError) return start;
|
|
13706
|
-
const given = typeof call.input.path === "string" && call.input.path ?
|
|
13747
|
+
const given = typeof call.input.path === "string" && call.input.path ? path29.resolve(start, call.input.path) : /worktree at (.+?) on branch /.exec(call.result ?? "")?.[1] ?? (typeof call.input.name === "string" ? path29.join(start, ".claude", "worktrees", call.input.name) : null);
|
|
13707
13748
|
if (!given) return start;
|
|
13708
13749
|
try {
|
|
13709
13750
|
return resolveWorkFolder(project.path, project.extraFolders ?? [], given) ?? project.path;
|
|
@@ -13713,7 +13754,7 @@ ${firstMessage}`);
|
|
|
13713
13754
|
}
|
|
13714
13755
|
/** The folders a chat's file viewer and tree never show from (Martin, question #317). */
|
|
13715
13756
|
settingsFolders() {
|
|
13716
|
-
return [
|
|
13757
|
+
return [path29.join(os14.homedir(), "AppData"), path29.join(os14.homedir(), "Library"), userDataDir()];
|
|
13717
13758
|
}
|
|
13718
13759
|
/** The folders the Files pane offers: where the session works, and the project folder when that is another one. */
|
|
13719
13760
|
paneFolders(sessionId) {
|
|
@@ -13722,7 +13763,7 @@ ${firstMessage}`);
|
|
|
13722
13763
|
const folders = [{ key: "session", path: own, label: info.project === CHATS ? "This chat's folder" : "This session's folder" }];
|
|
13723
13764
|
const same = (a, b) => {
|
|
13724
13765
|
try {
|
|
13725
|
-
return
|
|
13766
|
+
return path29.relative(fs27.realpathSync(a), fs27.realpathSync(b)) === "";
|
|
13726
13767
|
} catch {
|
|
13727
13768
|
return a === b;
|
|
13728
13769
|
}
|
|
@@ -13790,7 +13831,7 @@ ${firstMessage}`);
|
|
|
13790
13831
|
const hidden = hiddenProblem(root, link, this.allowedFolders(), this.settingsFolders());
|
|
13791
13832
|
if (hidden) throw new UserError(hidden);
|
|
13792
13833
|
}
|
|
13793
|
-
if (folder && this.lockedFoldersBesides(info.project).some((f) => within(f,
|
|
13834
|
+
if (folder && this.lockedFoldersBesides(info.project).some((f) => within(f, path29.resolve(root, decodeURIComponent(link.replace(/[?#].*$/, "")))))) {
|
|
13794
13835
|
throw new UserError("That file is in a locked project's folder.");
|
|
13795
13836
|
}
|
|
13796
13837
|
return read(root, link);
|
|
@@ -14268,7 +14309,7 @@ function challengeIn(res) {
|
|
|
14268
14309
|
// src/server/push.ts
|
|
14269
14310
|
import crypto7 from "node:crypto";
|
|
14270
14311
|
import fs28 from "node:fs";
|
|
14271
|
-
import
|
|
14312
|
+
import path30 from "node:path";
|
|
14272
14313
|
|
|
14273
14314
|
// src/shared/waiting.ts
|
|
14274
14315
|
var questionWaits = (q) => q.status === "open" && q.addressedTo === "user";
|
|
@@ -14326,7 +14367,7 @@ function loadOrCreateVapid(file) {
|
|
|
14326
14367
|
const { privateKey } = crypto7.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
14327
14368
|
const jwk = privateKey.export({ format: "jwk" });
|
|
14328
14369
|
const publicKey = b64(Buffer.concat([Buffer.from([4]), unb64(jwk.x), unb64(jwk.y)]));
|
|
14329
|
-
fs28.mkdirSync(
|
|
14370
|
+
fs28.mkdirSync(path30.dirname(file), { recursive: true, mode: 448 });
|
|
14330
14371
|
fs28.writeFileSync(file, JSON.stringify({ publicKey, privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) }), { mode: 384 });
|
|
14331
14372
|
return { publicKey, privateKey };
|
|
14332
14373
|
}
|
|
@@ -14485,7 +14526,7 @@ var Push = class {
|
|
|
14485
14526
|
// src/update/updater.ts
|
|
14486
14527
|
import { spawn as spawn6 } from "node:child_process";
|
|
14487
14528
|
import fs29 from "node:fs";
|
|
14488
|
-
import
|
|
14529
|
+
import path31 from "node:path";
|
|
14489
14530
|
var BRANCH = "main";
|
|
14490
14531
|
var TAIL_LINES = 15;
|
|
14491
14532
|
var MAX_CHANGES = 50;
|
|
@@ -14498,7 +14539,7 @@ var PACKAGE = "ledung";
|
|
|
14498
14539
|
var CHANNEL_KEY = "update-channel";
|
|
14499
14540
|
var devCheckTraced = false;
|
|
14500
14541
|
function readRunningRelease(appDir2) {
|
|
14501
|
-
const own = readJson(
|
|
14542
|
+
const own = readJson(path31.join(appDir2, "release.json"));
|
|
14502
14543
|
if (own && typeof own.source === "string") {
|
|
14503
14544
|
return { source: own.source, commit: typeof own.commit === "string" ? own.commit : null, releasedAt: str2(own.releasedAt) };
|
|
14504
14545
|
}
|
|
@@ -14672,7 +14713,7 @@ ${r.tail}`);
|
|
|
14672
14713
|
}
|
|
14673
14714
|
async compare(running) {
|
|
14674
14715
|
const src = running.source;
|
|
14675
|
-
if (!fs29.existsSync(
|
|
14716
|
+
if (!fs29.existsSync(path31.join(src, ".git"))) throw new Error(`The source folder ${src} is not a git clone.`);
|
|
14676
14717
|
const fetched = await this.run({ cmd: "git", args: ["fetch", "origin", BRANCH] }, src);
|
|
14677
14718
|
if (fetched.code !== 0) throw new Error(`git fetch origin ${BRANCH} failed:
|
|
14678
14719
|
${fetched.tail}`);
|
|
@@ -14755,7 +14796,7 @@ ${fetched.tail}`);
|
|
|
14755
14796
|
}
|
|
14756
14797
|
/** Why the source folder can't be updated: changes, another branch, or main has gone its own way. */
|
|
14757
14798
|
async precheck(src) {
|
|
14758
|
-
if (!fs29.existsSync(
|
|
14799
|
+
if (!fs29.existsSync(path31.join(src, ".git"))) return { reason: `The source folder ${src} is not a git clone.` };
|
|
14759
14800
|
const status = await this.run({ cmd: "git", args: ["status", "--porcelain", "--untracked-files=no"] }, src);
|
|
14760
14801
|
if (status.code !== 0) return { reason: "git status failed in the source folder.", output: status.tail };
|
|
14761
14802
|
if (status.out.trim()) {
|
|
@@ -14864,7 +14905,7 @@ function typicalCosts(turns) {
|
|
|
14864
14905
|
// src/server/tunnel-tls.ts
|
|
14865
14906
|
import crypto8 from "node:crypto";
|
|
14866
14907
|
import fs30 from "node:fs";
|
|
14867
|
-
import
|
|
14908
|
+
import path32 from "node:path";
|
|
14868
14909
|
import tls from "node:tls";
|
|
14869
14910
|
var TLS_KEY = "tls.key";
|
|
14870
14911
|
var TLS_CERT = "tls.crt";
|
|
@@ -14878,8 +14919,8 @@ var CertificateFiles = class {
|
|
|
14878
14919
|
}
|
|
14879
14920
|
/** What changed since the last call: nothing, gone, unusable, or a new key and certificate to use. */
|
|
14880
14921
|
check() {
|
|
14881
|
-
const keyFile =
|
|
14882
|
-
const certFile =
|
|
14922
|
+
const keyFile = path32.join(this.dir, TLS_KEY);
|
|
14923
|
+
const certFile = path32.join(this.dir, TLS_CERT);
|
|
14883
14924
|
let stamp;
|
|
14884
14925
|
try {
|
|
14885
14926
|
stamp = [fs30.statSync(keyFile), fs30.statSync(certFile)].map((s) => `${s.size}:${s.mtimeMs}`).join("|");
|
|
@@ -15046,22 +15087,56 @@ function protocols(body) {
|
|
|
15046
15087
|
|
|
15047
15088
|
// src/platform/open-folder.ts
|
|
15048
15089
|
import { spawn as spawn7 } from "node:child_process";
|
|
15090
|
+
import path33 from "node:path";
|
|
15091
|
+
var STILL_RUNNING_MS = 3e3;
|
|
15092
|
+
var MAX_REASON = 500;
|
|
15049
15093
|
function openFolderCommand(folder, platform = process.platform) {
|
|
15050
|
-
return platform === "win32" ? ["explorer.exe", [folder]] : platform === "darwin" ? ["open", [folder]] : ["xdg-open", [folder]];
|
|
15094
|
+
return platform === "win32" ? ["explorer.exe", [path33.win32.normalize(folder)]] : platform === "darwin" ? ["open", [folder]] : ["xdg-open", [folder]];
|
|
15051
15095
|
}
|
|
15052
|
-
function
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15096
|
+
function openRun(platform = process.platform) {
|
|
15097
|
+
return { exitCodeCounts: platform !== "win32", windowsHide: false, stillRunningMs: STILL_RUNNING_MS };
|
|
15098
|
+
}
|
|
15099
|
+
function runOpener(command, args, run8) {
|
|
15100
|
+
return new Promise((resolve, reject) => {
|
|
15101
|
+
const child = spawn7(command, args, { stdio: ["ignore", "ignore", "pipe"], detached: true, windowsHide: run8.windowsHide });
|
|
15102
|
+
let stderr = "";
|
|
15103
|
+
child.stderr.setEncoding("utf8");
|
|
15104
|
+
child.stderr.on("data", (text2) => {
|
|
15105
|
+
if (stderr.length < MAX_REASON) stderr += text2;
|
|
15106
|
+
});
|
|
15107
|
+
const settled = () => {
|
|
15108
|
+
clearTimeout(timer);
|
|
15109
|
+
child.stderr.unref();
|
|
15110
|
+
child.unref();
|
|
15111
|
+
};
|
|
15112
|
+
const timer = setTimeout(() => {
|
|
15113
|
+
settled();
|
|
15114
|
+
resolve();
|
|
15115
|
+
}, run8.stillRunningMs);
|
|
15116
|
+
child.on("error", (e) => {
|
|
15117
|
+
settled();
|
|
15118
|
+
reject(e);
|
|
15119
|
+
});
|
|
15120
|
+
child.on("close", (code, signal) => {
|
|
15121
|
+
settled();
|
|
15122
|
+
if (!run8.exitCodeCounts || code === 0) resolve();
|
|
15123
|
+
else reject(new Error(stderr.trim().slice(0, MAX_REASON) || `${command} stopped with ${code ?? signal}`));
|
|
15124
|
+
});
|
|
15056
15125
|
});
|
|
15057
|
-
|
|
15126
|
+
}
|
|
15127
|
+
var exercised = false;
|
|
15128
|
+
async function openFolder(folder) {
|
|
15129
|
+
const [command, args] = openFolderCommand(folder);
|
|
15130
|
+
await runOpener(command, args, openRun());
|
|
15131
|
+
if (!exercised) exercisedLine(`EXERCISED #83 ${command} was waited for and opened the folder`);
|
|
15132
|
+
exercised = true;
|
|
15058
15133
|
}
|
|
15059
15134
|
|
|
15060
15135
|
// src/remote/linked.ts
|
|
15061
15136
|
import crypto9 from "node:crypto";
|
|
15062
15137
|
import fs31 from "node:fs";
|
|
15063
15138
|
import https from "node:https";
|
|
15064
|
-
import
|
|
15139
|
+
import path34 from "node:path";
|
|
15065
15140
|
import tls2 from "node:tls";
|
|
15066
15141
|
var LINKS_FILE = "links.json";
|
|
15067
15142
|
var LINK_CODE_MS = 10 * 60 * 1e3;
|
|
@@ -15120,7 +15195,7 @@ var LinkedComputers = class {
|
|
|
15120
15195
|
pending = /* @__PURE__ */ new Map();
|
|
15121
15196
|
constructor(opts) {
|
|
15122
15197
|
this.opts = opts;
|
|
15123
|
-
this.file =
|
|
15198
|
+
this.file = path34.join(opts.dir, LINKS_FILE);
|
|
15124
15199
|
this.now = opts.now ?? Date.now;
|
|
15125
15200
|
}
|
|
15126
15201
|
list() {
|
|
@@ -15270,7 +15345,7 @@ var LinkedComputers = class {
|
|
|
15270
15345
|
}
|
|
15271
15346
|
}
|
|
15272
15347
|
write(links) {
|
|
15273
|
-
fs31.mkdirSync(
|
|
15348
|
+
fs31.mkdirSync(path34.dirname(this.file), { recursive: true });
|
|
15274
15349
|
const temp = `${this.file}.${process.pid}.new`;
|
|
15275
15350
|
fs31.writeFileSync(temp, JSON.stringify({ links }, null, 2), { mode: 384 });
|
|
15276
15351
|
fs31.renameSync(temp, this.file);
|
|
@@ -15423,7 +15498,7 @@ var LedungServer = class {
|
|
|
15423
15498
|
this.push?.seed(opts.manager.questions(), opts.store.pendingPermissions());
|
|
15424
15499
|
this.guard = new Guard(opts.token, opts.port, opts.lanNames ?? [], opts.lanOpen ?? false);
|
|
15425
15500
|
this.headers = {};
|
|
15426
|
-
this.webRoot = opts.webRoot ??
|
|
15501
|
+
this.webRoot = opts.webRoot ?? path35.join(appDir(), "web", "dist");
|
|
15427
15502
|
this.setPublicName(opts.publicName ?? null, opts.tunnelTls ?? null);
|
|
15428
15503
|
opts.manager.setSettings(this.settingsPort());
|
|
15429
15504
|
}
|
|
@@ -15541,12 +15616,16 @@ var LedungServer = class {
|
|
|
15541
15616
|
this.handle(req, res).catch((e) => this.fail(res, e));
|
|
15542
15617
|
});
|
|
15543
15618
|
server.on("upgrade", (req, socket, head) => {
|
|
15544
|
-
const
|
|
15545
|
-
|
|
15546
|
-
|
|
15619
|
+
const refuse = (status) => {
|
|
15620
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
15621
|
+
Connection: close\r
|
|
15622
|
+
\r
|
|
15623
|
+
`);
|
|
15547
15624
|
setTimeout(() => socket.destroy(), 1e3).unref();
|
|
15548
|
-
|
|
15549
|
-
|
|
15625
|
+
};
|
|
15626
|
+
const url = requestUrl(req);
|
|
15627
|
+
if (!url) return refuse("400 Bad Request");
|
|
15628
|
+
if (url.pathname !== "/ws" || !this.guard.hostAllowed(req) || !this.guard.originAllowed(req) || !this.guard.mayUse(req)) return refuse("403 Forbidden");
|
|
15550
15629
|
const device = this.guard.deviceOf(req)?.id ?? null;
|
|
15551
15630
|
const viaPublic = this.guard.isPublic(req);
|
|
15552
15631
|
this.wss.handleUpgrade(req, socket, head, (ws) => this.onClient(ws, device, viaPublic));
|
|
@@ -15748,7 +15827,7 @@ var LedungServer = class {
|
|
|
15748
15827
|
packaged: PACKAGED,
|
|
15749
15828
|
version: appVersion(),
|
|
15750
15829
|
release: PACKAGED ? null : readRunningRelease(appDir()),
|
|
15751
|
-
build:
|
|
15830
|
+
build: path35.basename(appDir())
|
|
15752
15831
|
});
|
|
15753
15832
|
return this.sourceCache;
|
|
15754
15833
|
}
|
|
@@ -15758,7 +15837,7 @@ var LedungServer = class {
|
|
|
15758
15837
|
return {
|
|
15759
15838
|
type: "hello",
|
|
15760
15839
|
// A release is named by its folder; the npm package's folder never changes, so by its version (#105).
|
|
15761
|
-
build: PACKAGED ? `ledung@${appVersion()}` :
|
|
15840
|
+
build: PACKAGED ? `ledung@${appVersion()}` : path35.basename(appDir()),
|
|
15762
15841
|
canRestart: !!this.opts.onRestart,
|
|
15763
15842
|
source: this.source(),
|
|
15764
15843
|
userName: manager.userName,
|
|
@@ -15817,8 +15896,9 @@ var LedungServer = class {
|
|
|
15817
15896
|
return computers;
|
|
15818
15897
|
}
|
|
15819
15898
|
async handle(req, res) {
|
|
15899
|
+
const url = requestUrl(req);
|
|
15900
|
+
if (!url) throw new HttpError(400, "Bad request path.");
|
|
15820
15901
|
if (!this.guard.hostAllowed(req)) throw new HttpError(403, "Unknown host.");
|
|
15821
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
15822
15902
|
const remote = this.guard.isPublic(req);
|
|
15823
15903
|
if (remote) {
|
|
15824
15904
|
this.publicResponses.add(res);
|
|
@@ -16210,6 +16290,14 @@ var LedungServer = class {
|
|
|
16210
16290
|
requirePageForManager(req, sessionId) {
|
|
16211
16291
|
if (sessionId && this.opts.store.getSession(sessionId)?.role === "manager") this.requireOpenPage(req);
|
|
16212
16292
|
}
|
|
16293
|
+
/** Says why, with the folder so it can be opened by hand; the page shows it (issue #83). */
|
|
16294
|
+
async openAccountFolder(folder) {
|
|
16295
|
+
try {
|
|
16296
|
+
await (this.opts.openFolder ?? openFolder)(folder);
|
|
16297
|
+
} catch (e) {
|
|
16298
|
+
throw new UserError(`Couldn't open ${folder}: ${e.message}`);
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16213
16301
|
/** Paired devices are managed only on this computer: a device can't pair others or list them. */
|
|
16214
16302
|
requireLocal(req, what = "Devices are managed") {
|
|
16215
16303
|
if (this.guard.isPublic(req)) throw new HttpError(403, `${what} in Settings on the computer Ledung runs on.`);
|
|
@@ -16472,7 +16560,7 @@ var LedungServer = class {
|
|
|
16472
16560
|
case "POST accounts/:id/open-folder":
|
|
16473
16561
|
this.requireLocal(req, "Folders are opened");
|
|
16474
16562
|
await this.requirePage(req, url);
|
|
16475
|
-
|
|
16563
|
+
await this.openAccountFolder(manager.accountFolder(decodeURIComponent(id)));
|
|
16476
16564
|
return { ok: true };
|
|
16477
16565
|
case "POST accounts/:id/remove":
|
|
16478
16566
|
return await manager.removeAccount(decodeURIComponent(id));
|
|
@@ -16942,19 +17030,19 @@ var LedungServer = class {
|
|
|
16942
17030
|
return this.opts.updater;
|
|
16943
17031
|
}
|
|
16944
17032
|
serveStatic(pathname, res, signedIn, remote) {
|
|
16945
|
-
const root =
|
|
16946
|
-
let file =
|
|
16947
|
-
if (!file.startsWith(root +
|
|
17033
|
+
const root = path35.resolve(this.webRoot);
|
|
17034
|
+
let file = path35.resolve(root, "." + decodeURIComponent(pathname));
|
|
17035
|
+
if (!file.startsWith(root + path35.sep) && file !== root) throw new HttpError(404, "Not found.");
|
|
16948
17036
|
if (!fs32.existsSync(file) || fs32.statSync(file).isDirectory()) {
|
|
16949
17037
|
if (!signedIn) {
|
|
16950
17038
|
res.writeHead(401, { ...this.headersFor(res), "Content-Type": "text/html; charset=utf-8" });
|
|
16951
17039
|
res.end(`<!doctype html><title>Ledung</title><div style="font-family:sans-serif;max-width:30em;margin:2em auto"><p>${remote ? NOT_PAIRED : "Not signed in. Open Ledung through the link it prints when it starts."}</p>${remote ? PAIR_CODE_FORM : ""}</div>`);
|
|
16952
17040
|
return;
|
|
16953
17041
|
}
|
|
16954
|
-
file =
|
|
17042
|
+
file = path35.join(root, "index.html");
|
|
16955
17043
|
if (!fs32.existsSync(file)) throw new HttpError(500, 'The page has not been built. Run "npm run build".');
|
|
16956
17044
|
}
|
|
16957
|
-
const type = CONTENT_TYPES[
|
|
17045
|
+
const type = CONTENT_TYPES[path35.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
16958
17046
|
res.writeHead(200, { ...this.headersFor(res), "Content-Type": type });
|
|
16959
17047
|
fs32.createReadStream(file).pipe(res);
|
|
16960
17048
|
}
|
|
@@ -16977,6 +17065,15 @@ var LedungServer = class {
|
|
|
16977
17065
|
}
|
|
16978
17066
|
};
|
|
16979
17067
|
var bodies = /* @__PURE__ */ new WeakMap();
|
|
17068
|
+
var badPathTraced = false;
|
|
17069
|
+
function requestUrl(req) {
|
|
17070
|
+
const url = URL.parse(req.url ?? "/", "http://127.0.0.1");
|
|
17071
|
+
if (!url && !badPathTraced) {
|
|
17072
|
+
badPathTraced = true;
|
|
17073
|
+
exercisedLine("EXERCISED #467 refused a request path that does not parse");
|
|
17074
|
+
}
|
|
17075
|
+
return url;
|
|
17076
|
+
}
|
|
16980
17077
|
function fromThisComputer(req) {
|
|
16981
17078
|
const from = req.socket.remoteAddress ?? "";
|
|
16982
17079
|
return from === "::1" || from.startsWith("127.") || from.startsWith("::ffff:127.");
|
|
@@ -17045,7 +17142,7 @@ async function adminCall(fn) {
|
|
|
17045
17142
|
}
|
|
17046
17143
|
|
|
17047
17144
|
// src/store/database.ts
|
|
17048
|
-
import
|
|
17145
|
+
import path36 from "node:path";
|
|
17049
17146
|
import { DatabaseSync } from "node:sqlite";
|
|
17050
17147
|
var DAILY_COLUMNS = ["at", "account", "machine", "model", "project", "role", "trigger", "input", "cache_write", "cache_read", "output", "thinking", "cost", "subagent"];
|
|
17051
17148
|
var dailyDay = (r) => `date(${r}.at / 1000, 'unixepoch', 'localtime')`;
|
|
@@ -17618,7 +17715,7 @@ var MIGRATIONS = [
|
|
|
17618
17715
|
`
|
|
17619
17716
|
];
|
|
17620
17717
|
function databaseFile(dir = userDataDir()) {
|
|
17621
|
-
return
|
|
17718
|
+
return path36.join(dir, "ledung.db");
|
|
17622
17719
|
}
|
|
17623
17720
|
function openDatabase(file = databaseFile()) {
|
|
17624
17721
|
const db = new DatabaseSync(file);
|
|
@@ -17673,7 +17770,7 @@ function quietTexts(entries) {
|
|
|
17673
17770
|
|
|
17674
17771
|
// src/store/trim.ts
|
|
17675
17772
|
import fs33 from "node:fs";
|
|
17676
|
-
import
|
|
17773
|
+
import path37 from "node:path";
|
|
17677
17774
|
var RESULT_LIMIT = 64 * 1024;
|
|
17678
17775
|
var OLD_RESULT_LIMIT = 2e3;
|
|
17679
17776
|
var DAY2 = 24 * 60 * 60 * 1e3;
|
|
@@ -17889,7 +17986,7 @@ function diskFreeFor(db) {
|
|
|
17889
17986
|
const main2 = db.prepare("PRAGMA database_list").all().find((d) => d.name === "main");
|
|
17890
17987
|
if (!main2?.file) return null;
|
|
17891
17988
|
try {
|
|
17892
|
-
const s = fs33.statfsSync(
|
|
17989
|
+
const s = fs33.statfsSync(path37.dirname(main2.file));
|
|
17893
17990
|
return Number(s.bavail) * Number(s.bsize);
|
|
17894
17991
|
} catch {
|
|
17895
17992
|
return null;
|
|
@@ -18652,9 +18749,9 @@ var GitHubBackend = class {
|
|
|
18652
18749
|
this.now = opts.now ?? Date.now;
|
|
18653
18750
|
}
|
|
18654
18751
|
/** A call to GitHub; conditional (If-None-Match) when `conditional`, answering a 304 from what was kept. */
|
|
18655
|
-
async call(method2,
|
|
18752
|
+
async call(method2, path43, body, conditional = false, accept = "application/vnd.github+json") {
|
|
18656
18753
|
if (!this.tokenValue) this.tokenValue = await this.token();
|
|
18657
|
-
const url =
|
|
18754
|
+
const url = path43.startsWith("http") ? path43 : `${API}${path43}`;
|
|
18658
18755
|
const kept = conditional ? this.etags.get(url) : void 0;
|
|
18659
18756
|
const headers = {
|
|
18660
18757
|
authorization: `Bearer ${this.tokenValue}`,
|
|
@@ -18753,10 +18850,10 @@ var GitHubBackend = class {
|
|
|
18753
18850
|
};
|
|
18754
18851
|
}
|
|
18755
18852
|
/** All pages of a list, following Link: rel="next"; every page conditional. Fresh: anything changed. */
|
|
18756
|
-
async list(
|
|
18853
|
+
async list(path43) {
|
|
18757
18854
|
const items = [];
|
|
18758
18855
|
let fresh = false;
|
|
18759
|
-
let next =
|
|
18856
|
+
let next = path43;
|
|
18760
18857
|
for (let page = 0; next && page < 20; page++) {
|
|
18761
18858
|
const r = await this.call("GET", next, void 0, true);
|
|
18762
18859
|
fresh ||= r.fresh;
|
|
@@ -18913,8 +19010,8 @@ ${lines.join("\n")}`;
|
|
|
18913
19010
|
for (const f of files.slice(1)) entries.push({ path: f.path, sha: (await blob(f.bytes)).body.sha });
|
|
18914
19011
|
await this.replaceEvidence(entries, message2);
|
|
18915
19012
|
}
|
|
18916
|
-
async getFile(
|
|
18917
|
-
const url = `/repos/${this.repo}/contents/${
|
|
19013
|
+
async getFile(path43) {
|
|
19014
|
+
const url = `/repos/${this.repo}/contents/${path43.split("/").map(encodeURIComponent).join("/")}?ref=${EVIDENCE_BRANCH}`;
|
|
18918
19015
|
let body;
|
|
18919
19016
|
try {
|
|
18920
19017
|
body = (await this.call("GET", url, void 0, false, RAW)).body;
|
|
@@ -18922,11 +19019,11 @@ ${lines.join("\n")}`;
|
|
|
18922
19019
|
if (e instanceof BoardError && e.kind === "access") throw new BoardError("access", "it is not on the board (pruned, or never uploaded)");
|
|
18923
19020
|
throw e;
|
|
18924
19021
|
}
|
|
18925
|
-
if (!Buffer.isBuffer(body)) throw new BoardError("unreachable", `GitHub did not send the file ${
|
|
19022
|
+
if (!Buffer.isBuffer(body)) throw new BoardError("unreachable", `GitHub did not send the file ${path43}.`);
|
|
18926
19023
|
return body;
|
|
18927
19024
|
}
|
|
18928
19025
|
async deleteFiles(paths, message2) {
|
|
18929
|
-
if (paths.length) await this.replaceEvidence(paths.map((
|
|
19026
|
+
if (paths.length) await this.replaceEvidence(paths.map((path43) => ({ path: path43, sha: null })), message2);
|
|
18930
19027
|
}
|
|
18931
19028
|
/**
|
|
18932
19029
|
* People for the allowlist picker (decision 5): the repo's collaborators with name and picture (needs
|
|
@@ -18979,14 +19076,14 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
18979
19076
|
this.token = opts.token ?? ghToken;
|
|
18980
19077
|
this.now = opts.now ?? Date.now;
|
|
18981
19078
|
}
|
|
18982
|
-
async call(method2,
|
|
19079
|
+
async call(method2, path43, body) {
|
|
18983
19080
|
if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
|
|
18984
19081
|
throw new BoardError("signin", SIGN_IN2);
|
|
18985
19082
|
});
|
|
18986
19083
|
const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung" };
|
|
18987
19084
|
let res;
|
|
18988
19085
|
try {
|
|
18989
|
-
res = await this.http(method2,
|
|
19086
|
+
res = await this.http(method2, path43.startsWith("http") ? path43 : `${API2}${path43}`, headers, body);
|
|
18990
19087
|
} catch (e) {
|
|
18991
19088
|
throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
|
|
18992
19089
|
}
|
|
@@ -19048,8 +19145,8 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
19048
19145
|
const owners = await this.owners();
|
|
19049
19146
|
const found = owners.find((o) => o.login.toLowerCase() === owner.toLowerCase());
|
|
19050
19147
|
if (!found) throw new BoardError("access", `You can't create repositories under "${owner}": it is neither your account nor an organisation you belong to.`);
|
|
19051
|
-
const
|
|
19052
|
-
const res = await this.call("POST",
|
|
19148
|
+
const path43 = found.kind === "user" ? "/user/repos" : `/orgs/${encodeURIComponent(found.login)}/repos`;
|
|
19149
|
+
const res = await this.call("POST", path43, { name, private: true, description: "Message board for Ledung managers" });
|
|
19053
19150
|
if (res.status === 422) {
|
|
19054
19151
|
const why = _GitHubRepos.message(res.body);
|
|
19055
19152
|
if (/already exists|name already/i.test(why)) throw new BoardError("access", `${found.login}/${name} already exists. Pick another name, or choose it from the list.`);
|
|
@@ -19069,7 +19166,7 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
19069
19166
|
// src/usage/queries.ts
|
|
19070
19167
|
var DAY3 = 24 * 60 * 60 * 1e3;
|
|
19071
19168
|
var tokensOf = (r) => r.input + r.cache_write + r.cache_read + r.output;
|
|
19072
|
-
var
|
|
19169
|
+
var exercised2 = false;
|
|
19073
19170
|
function push(groups, key, r) {
|
|
19074
19171
|
const list2 = groups.get(key);
|
|
19075
19172
|
if (!list2) {
|
|
@@ -19077,8 +19174,8 @@ function push(groups, key, r) {
|
|
|
19077
19174
|
return;
|
|
19078
19175
|
}
|
|
19079
19176
|
list2.push(r);
|
|
19080
|
-
if (!
|
|
19081
|
-
|
|
19177
|
+
if (!exercised2) {
|
|
19178
|
+
exercised2 = true;
|
|
19082
19179
|
exercisedLine("EXERCISED #356 usage report grouped calls in place");
|
|
19083
19180
|
}
|
|
19084
19181
|
}
|
|
@@ -19304,7 +19401,7 @@ var UsageQueries = class {
|
|
|
19304
19401
|
// src/usage/publisher.ts
|
|
19305
19402
|
import crypto12 from "node:crypto";
|
|
19306
19403
|
import fs34 from "node:fs";
|
|
19307
|
-
import
|
|
19404
|
+
import path38 from "node:path";
|
|
19308
19405
|
|
|
19309
19406
|
// src/usage/publish-file.ts
|
|
19310
19407
|
import crypto11 from "node:crypto";
|
|
@@ -19450,14 +19547,14 @@ var UsageRepo = class {
|
|
|
19450
19547
|
this.http = opts.http ?? fetchHttp;
|
|
19451
19548
|
this.token = opts.token ?? ghToken;
|
|
19452
19549
|
}
|
|
19453
|
-
async call(method2,
|
|
19550
|
+
async call(method2, path43, body, extra = {}) {
|
|
19454
19551
|
if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
|
|
19455
19552
|
throw new BoardError("signin", SIGN_IN3);
|
|
19456
19553
|
});
|
|
19457
19554
|
const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung", ...extra };
|
|
19458
19555
|
let res;
|
|
19459
19556
|
try {
|
|
19460
|
-
res = await this.http(method2, `${API3}${
|
|
19557
|
+
res = await this.http(method2, `${API3}${path43}`, headers, body);
|
|
19461
19558
|
} catch (e) {
|
|
19462
19559
|
throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
|
|
19463
19560
|
}
|
|
@@ -19475,8 +19572,8 @@ var UsageRepo = class {
|
|
|
19475
19572
|
if (res.status === 403 || res.status === 404) return new BoardError("access", `GitHub refused ${what}: ${this.repo} is missing, or the login can't write to it${why ? ` (${why})` : ""}.`);
|
|
19476
19573
|
return new BoardError("unreachable", `GitHub answered ${res.status} for ${what}${why ? ` (${why})` : ""}.`);
|
|
19477
19574
|
}
|
|
19478
|
-
async ok(what, method2,
|
|
19479
|
-
const res = await this.call(method2,
|
|
19575
|
+
async ok(what, method2, path43, body) {
|
|
19576
|
+
const res = await this.call(method2, path43, body);
|
|
19480
19577
|
if (res.status < 200 || res.status >= 300) throw this.failure(what, res);
|
|
19481
19578
|
return res.body;
|
|
19482
19579
|
}
|
|
@@ -19658,7 +19755,7 @@ var UsagePublisher = class {
|
|
|
19658
19755
|
}
|
|
19659
19756
|
// --- The usage key ---------------------------------------------------------------------------------------
|
|
19660
19757
|
keyFile() {
|
|
19661
|
-
return
|
|
19758
|
+
return path38.join(this.opts.keyDir, USAGE_KEY_FILE);
|
|
19662
19759
|
}
|
|
19663
19760
|
/** The usage key, or null when this computer has none yet. */
|
|
19664
19761
|
key() {
|
|
@@ -20091,13 +20188,13 @@ function originOf(folder) {
|
|
|
20091
20188
|
|
|
20092
20189
|
// src/remote/link.ts
|
|
20093
20190
|
import fs36 from "node:fs";
|
|
20094
|
-
import
|
|
20191
|
+
import path40 from "node:path";
|
|
20095
20192
|
|
|
20096
20193
|
// src/server/acme.ts
|
|
20097
20194
|
import crypto13 from "node:crypto";
|
|
20098
20195
|
import dns from "node:dns";
|
|
20099
20196
|
import fs35 from "node:fs";
|
|
20100
|
-
import
|
|
20197
|
+
import path39 from "node:path";
|
|
20101
20198
|
import acme from "acme-client";
|
|
20102
20199
|
var ACCOUNT_KEY = "acme-account.key";
|
|
20103
20200
|
var ACME_STATE = "acme.json";
|
|
@@ -20328,8 +20425,8 @@ var CertificateManager = class {
|
|
|
20328
20425
|
const chain = Buffer.from(await client.getCertificate(finalized));
|
|
20329
20426
|
const problem2 = certificateProblem({ key, cert: chain }, name);
|
|
20330
20427
|
if (problem2) throw new Error(problem2);
|
|
20331
|
-
writePrivate(
|
|
20332
|
-
writePrivate(
|
|
20428
|
+
writePrivate(path39.join(this.opts.dir, TLS_KEY), key);
|
|
20429
|
+
writePrivate(path39.join(this.opts.dir, TLS_CERT), chain);
|
|
20333
20430
|
this.save({ issuedBy: this.opts.directoryUrl });
|
|
20334
20431
|
}
|
|
20335
20432
|
/** The certificate in remoteDir() if it can serve the name, has not run out and is this directory's; null otherwise. */
|
|
@@ -20337,7 +20434,7 @@ var CertificateManager = class {
|
|
|
20337
20434
|
const issuedBy = this.saved().issuedBy;
|
|
20338
20435
|
if (issuedBy !== null && issuedBy !== this.opts.directoryUrl) return null;
|
|
20339
20436
|
try {
|
|
20340
|
-
const files = { key: fs35.readFileSync(
|
|
20437
|
+
const files = { key: fs35.readFileSync(path39.join(this.opts.dir, TLS_KEY)), cert: fs35.readFileSync(path39.join(this.opts.dir, TLS_CERT)) };
|
|
20341
20438
|
if (certificateProblem(files, this.opts.publicName)) return null;
|
|
20342
20439
|
const life = lifetime(files.cert);
|
|
20343
20440
|
return this.now() < life.validUntil ? life : null;
|
|
@@ -20359,20 +20456,20 @@ var CertificateManager = class {
|
|
|
20359
20456
|
function readSaved(dir) {
|
|
20360
20457
|
const empty = { accounts: {}, issuedBy: null, failures: 0, nextTry: null, error: null };
|
|
20361
20458
|
try {
|
|
20362
|
-
const raw = JSON.parse(fs35.readFileSync(
|
|
20459
|
+
const raw = JSON.parse(fs35.readFileSync(path39.join(dir, ACME_STATE), "utf8"));
|
|
20363
20460
|
return { ...empty, ...raw, accounts: { ...raw.accounts } };
|
|
20364
20461
|
} catch {
|
|
20365
20462
|
return empty;
|
|
20366
20463
|
}
|
|
20367
20464
|
}
|
|
20368
20465
|
function writeSaved(dir, change) {
|
|
20369
|
-
writePrivate(
|
|
20466
|
+
writePrivate(path39.join(dir, ACME_STATE), Buffer.from(JSON.stringify({ ...readSaved(dir), ...change }, null, 2)));
|
|
20370
20467
|
}
|
|
20371
20468
|
function saveAccount(dir, directoryUrl, accountUrl) {
|
|
20372
20469
|
writeSaved(dir, { accounts: { ...readSaved(dir).accounts, [directoryUrl]: accountUrl } });
|
|
20373
20470
|
}
|
|
20374
20471
|
async function acmeAccount(opts) {
|
|
20375
|
-
const keyFile =
|
|
20472
|
+
const keyFile = path39.join(opts.dir, ACCOUNT_KEY);
|
|
20376
20473
|
let key;
|
|
20377
20474
|
try {
|
|
20378
20475
|
key = fs35.readFileSync(keyFile);
|
|
@@ -20888,11 +20985,11 @@ var RemoteLink = class {
|
|
|
20888
20985
|
return { name: answer.name, host: answer.host };
|
|
20889
20986
|
}
|
|
20890
20987
|
savedFile() {
|
|
20891
|
-
return
|
|
20988
|
+
return path40.join(this.opts.dir, SAVED_REMOTE);
|
|
20892
20989
|
}
|
|
20893
20990
|
};
|
|
20894
20991
|
function savedRemote(dir, log = (line) => console.log(line)) {
|
|
20895
|
-
const file =
|
|
20992
|
+
const file = path40.join(dir, SAVED_REMOTE);
|
|
20896
20993
|
try {
|
|
20897
20994
|
return parseRemote(JSON.parse(fs36.readFileSync(file, "utf8")));
|
|
20898
20995
|
} catch (e) {
|
|
@@ -20909,7 +21006,7 @@ function writePrivate2(file, data) {
|
|
|
20909
21006
|
// src/remote/admin.ts
|
|
20910
21007
|
import crypto15 from "node:crypto";
|
|
20911
21008
|
import fs37 from "node:fs";
|
|
20912
|
-
import
|
|
21009
|
+
import path41 from "node:path";
|
|
20913
21010
|
|
|
20914
21011
|
// src/remote/cloudflare.ts
|
|
20915
21012
|
var API4 = "https://api.cloudflare.com/client/v4";
|
|
@@ -20945,10 +21042,10 @@ var Cloudflare = class {
|
|
|
20945
21042
|
}
|
|
20946
21043
|
throw new Error(`The Cloudflare token reaches no zone holding ${name}.`);
|
|
20947
21044
|
}
|
|
20948
|
-
async call(method2,
|
|
21045
|
+
async call(method2, path43, body) {
|
|
20949
21046
|
let res;
|
|
20950
21047
|
try {
|
|
20951
|
-
res = await this.fetch(`${API4}${
|
|
21048
|
+
res = await this.fetch(`${API4}${path43}`, {
|
|
20952
21049
|
method: method2,
|
|
20953
21050
|
headers: { Authorization: `Bearer ${this.token}`, ...body ? { "Content-Type": "application/json" } : {} },
|
|
20954
21051
|
body: body ? JSON.stringify(body) : void 0,
|
|
@@ -21141,7 +21238,7 @@ var RelayAdmin = class {
|
|
|
21141
21238
|
return answer;
|
|
21142
21239
|
}
|
|
21143
21240
|
file() {
|
|
21144
|
-
return
|
|
21241
|
+
return path41.join(this.opts.dir, ADMIN_FILE);
|
|
21145
21242
|
}
|
|
21146
21243
|
saved() {
|
|
21147
21244
|
try {
|
|
@@ -21190,7 +21287,7 @@ async function main() {
|
|
|
21190
21287
|
let watcher = null;
|
|
21191
21288
|
const manager = new SessionManager(config, store, (event) => server?.broadcast(event));
|
|
21192
21289
|
const pollMs = boardConfig(config).pollSeconds * 1e3;
|
|
21193
|
-
manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(),
|
|
21290
|
+
manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(), path42.join(userDataDir(), "board-cache"));
|
|
21194
21291
|
const attached = await manager.attachRunningHosts();
|
|
21195
21292
|
const leftover = store.markUnfinishedSessionsEnded("Ledung stopped before this session ended. Resume to continue.", attached);
|
|
21196
21293
|
manager.away.begin();
|
|
@@ -21264,7 +21361,7 @@ async function main() {
|
|
|
21264
21361
|
computerSettings: config.computers,
|
|
21265
21362
|
linkConnect: config.remote?.linkVia ? connectVia(config.remote.linkVia) : void 0,
|
|
21266
21363
|
// The VAPID keys stay on this computer, next to the relay's (issue #246).
|
|
21267
|
-
push: { keyFile:
|
|
21364
|
+
push: { keyFile: path42.join(remoteDir(), "push-vapid.json") }
|
|
21268
21365
|
});
|
|
21269
21366
|
link = new RemoteLink({ server, dir: remoteDir(), configured: config.remote, joinAcme: config.relayJoin });
|
|
21270
21367
|
server.attachRemote(link);
|