ledung 1.4.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 +3 -0
- package/README.md +3 -1
- package/dist/cli.js +714 -32
- package/dist/main.js +311 -228
- 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
|
}
|
|
@@ -15752,7 +15827,7 @@ Connection: close\r
|
|
|
15752
15827
|
packaged: PACKAGED,
|
|
15753
15828
|
version: appVersion(),
|
|
15754
15829
|
release: PACKAGED ? null : readRunningRelease(appDir()),
|
|
15755
|
-
build:
|
|
15830
|
+
build: path35.basename(appDir())
|
|
15756
15831
|
});
|
|
15757
15832
|
return this.sourceCache;
|
|
15758
15833
|
}
|
|
@@ -15762,7 +15837,7 @@ Connection: close\r
|
|
|
15762
15837
|
return {
|
|
15763
15838
|
type: "hello",
|
|
15764
15839
|
// A release is named by its folder; the npm package's folder never changes, so by its version (#105).
|
|
15765
|
-
build: PACKAGED ? `ledung@${appVersion()}` :
|
|
15840
|
+
build: PACKAGED ? `ledung@${appVersion()}` : path35.basename(appDir()),
|
|
15766
15841
|
canRestart: !!this.opts.onRestart,
|
|
15767
15842
|
source: this.source(),
|
|
15768
15843
|
userName: manager.userName,
|
|
@@ -16215,6 +16290,14 @@ Connection: close\r
|
|
|
16215
16290
|
requirePageForManager(req, sessionId) {
|
|
16216
16291
|
if (sessionId && this.opts.store.getSession(sessionId)?.role === "manager") this.requireOpenPage(req);
|
|
16217
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
|
+
}
|
|
16218
16301
|
/** Paired devices are managed only on this computer: a device can't pair others or list them. */
|
|
16219
16302
|
requireLocal(req, what = "Devices are managed") {
|
|
16220
16303
|
if (this.guard.isPublic(req)) throw new HttpError(403, `${what} in Settings on the computer Ledung runs on.`);
|
|
@@ -16477,7 +16560,7 @@ Connection: close\r
|
|
|
16477
16560
|
case "POST accounts/:id/open-folder":
|
|
16478
16561
|
this.requireLocal(req, "Folders are opened");
|
|
16479
16562
|
await this.requirePage(req, url);
|
|
16480
|
-
|
|
16563
|
+
await this.openAccountFolder(manager.accountFolder(decodeURIComponent(id)));
|
|
16481
16564
|
return { ok: true };
|
|
16482
16565
|
case "POST accounts/:id/remove":
|
|
16483
16566
|
return await manager.removeAccount(decodeURIComponent(id));
|
|
@@ -16947,19 +17030,19 @@ Connection: close\r
|
|
|
16947
17030
|
return this.opts.updater;
|
|
16948
17031
|
}
|
|
16949
17032
|
serveStatic(pathname, res, signedIn, remote) {
|
|
16950
|
-
const root =
|
|
16951
|
-
let file =
|
|
16952
|
-
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.");
|
|
16953
17036
|
if (!fs32.existsSync(file) || fs32.statSync(file).isDirectory()) {
|
|
16954
17037
|
if (!signedIn) {
|
|
16955
17038
|
res.writeHead(401, { ...this.headersFor(res), "Content-Type": "text/html; charset=utf-8" });
|
|
16956
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>`);
|
|
16957
17040
|
return;
|
|
16958
17041
|
}
|
|
16959
|
-
file =
|
|
17042
|
+
file = path35.join(root, "index.html");
|
|
16960
17043
|
if (!fs32.existsSync(file)) throw new HttpError(500, 'The page has not been built. Run "npm run build".');
|
|
16961
17044
|
}
|
|
16962
|
-
const type = CONTENT_TYPES[
|
|
17045
|
+
const type = CONTENT_TYPES[path35.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
16963
17046
|
res.writeHead(200, { ...this.headersFor(res), "Content-Type": type });
|
|
16964
17047
|
fs32.createReadStream(file).pipe(res);
|
|
16965
17048
|
}
|
|
@@ -17059,7 +17142,7 @@ async function adminCall(fn) {
|
|
|
17059
17142
|
}
|
|
17060
17143
|
|
|
17061
17144
|
// src/store/database.ts
|
|
17062
|
-
import
|
|
17145
|
+
import path36 from "node:path";
|
|
17063
17146
|
import { DatabaseSync } from "node:sqlite";
|
|
17064
17147
|
var DAILY_COLUMNS = ["at", "account", "machine", "model", "project", "role", "trigger", "input", "cache_write", "cache_read", "output", "thinking", "cost", "subagent"];
|
|
17065
17148
|
var dailyDay = (r) => `date(${r}.at / 1000, 'unixepoch', 'localtime')`;
|
|
@@ -17632,7 +17715,7 @@ var MIGRATIONS = [
|
|
|
17632
17715
|
`
|
|
17633
17716
|
];
|
|
17634
17717
|
function databaseFile(dir = userDataDir()) {
|
|
17635
|
-
return
|
|
17718
|
+
return path36.join(dir, "ledung.db");
|
|
17636
17719
|
}
|
|
17637
17720
|
function openDatabase(file = databaseFile()) {
|
|
17638
17721
|
const db = new DatabaseSync(file);
|
|
@@ -17687,7 +17770,7 @@ function quietTexts(entries) {
|
|
|
17687
17770
|
|
|
17688
17771
|
// src/store/trim.ts
|
|
17689
17772
|
import fs33 from "node:fs";
|
|
17690
|
-
import
|
|
17773
|
+
import path37 from "node:path";
|
|
17691
17774
|
var RESULT_LIMIT = 64 * 1024;
|
|
17692
17775
|
var OLD_RESULT_LIMIT = 2e3;
|
|
17693
17776
|
var DAY2 = 24 * 60 * 60 * 1e3;
|
|
@@ -17903,7 +17986,7 @@ function diskFreeFor(db) {
|
|
|
17903
17986
|
const main2 = db.prepare("PRAGMA database_list").all().find((d) => d.name === "main");
|
|
17904
17987
|
if (!main2?.file) return null;
|
|
17905
17988
|
try {
|
|
17906
|
-
const s = fs33.statfsSync(
|
|
17989
|
+
const s = fs33.statfsSync(path37.dirname(main2.file));
|
|
17907
17990
|
return Number(s.bavail) * Number(s.bsize);
|
|
17908
17991
|
} catch {
|
|
17909
17992
|
return null;
|
|
@@ -18666,9 +18749,9 @@ var GitHubBackend = class {
|
|
|
18666
18749
|
this.now = opts.now ?? Date.now;
|
|
18667
18750
|
}
|
|
18668
18751
|
/** A call to GitHub; conditional (If-None-Match) when `conditional`, answering a 304 from what was kept. */
|
|
18669
|
-
async call(method2,
|
|
18752
|
+
async call(method2, path43, body, conditional = false, accept = "application/vnd.github+json") {
|
|
18670
18753
|
if (!this.tokenValue) this.tokenValue = await this.token();
|
|
18671
|
-
const url =
|
|
18754
|
+
const url = path43.startsWith("http") ? path43 : `${API}${path43}`;
|
|
18672
18755
|
const kept = conditional ? this.etags.get(url) : void 0;
|
|
18673
18756
|
const headers = {
|
|
18674
18757
|
authorization: `Bearer ${this.tokenValue}`,
|
|
@@ -18767,10 +18850,10 @@ var GitHubBackend = class {
|
|
|
18767
18850
|
};
|
|
18768
18851
|
}
|
|
18769
18852
|
/** All pages of a list, following Link: rel="next"; every page conditional. Fresh: anything changed. */
|
|
18770
|
-
async list(
|
|
18853
|
+
async list(path43) {
|
|
18771
18854
|
const items = [];
|
|
18772
18855
|
let fresh = false;
|
|
18773
|
-
let next =
|
|
18856
|
+
let next = path43;
|
|
18774
18857
|
for (let page = 0; next && page < 20; page++) {
|
|
18775
18858
|
const r = await this.call("GET", next, void 0, true);
|
|
18776
18859
|
fresh ||= r.fresh;
|
|
@@ -18927,8 +19010,8 @@ ${lines.join("\n")}`;
|
|
|
18927
19010
|
for (const f of files.slice(1)) entries.push({ path: f.path, sha: (await blob(f.bytes)).body.sha });
|
|
18928
19011
|
await this.replaceEvidence(entries, message2);
|
|
18929
19012
|
}
|
|
18930
|
-
async getFile(
|
|
18931
|
-
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}`;
|
|
18932
19015
|
let body;
|
|
18933
19016
|
try {
|
|
18934
19017
|
body = (await this.call("GET", url, void 0, false, RAW)).body;
|
|
@@ -18936,11 +19019,11 @@ ${lines.join("\n")}`;
|
|
|
18936
19019
|
if (e instanceof BoardError && e.kind === "access") throw new BoardError("access", "it is not on the board (pruned, or never uploaded)");
|
|
18937
19020
|
throw e;
|
|
18938
19021
|
}
|
|
18939
|
-
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}.`);
|
|
18940
19023
|
return body;
|
|
18941
19024
|
}
|
|
18942
19025
|
async deleteFiles(paths, message2) {
|
|
18943
|
-
if (paths.length) await this.replaceEvidence(paths.map((
|
|
19026
|
+
if (paths.length) await this.replaceEvidence(paths.map((path43) => ({ path: path43, sha: null })), message2);
|
|
18944
19027
|
}
|
|
18945
19028
|
/**
|
|
18946
19029
|
* People for the allowlist picker (decision 5): the repo's collaborators with name and picture (needs
|
|
@@ -18993,14 +19076,14 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
18993
19076
|
this.token = opts.token ?? ghToken;
|
|
18994
19077
|
this.now = opts.now ?? Date.now;
|
|
18995
19078
|
}
|
|
18996
|
-
async call(method2,
|
|
19079
|
+
async call(method2, path43, body) {
|
|
18997
19080
|
if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
|
|
18998
19081
|
throw new BoardError("signin", SIGN_IN2);
|
|
18999
19082
|
});
|
|
19000
19083
|
const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung" };
|
|
19001
19084
|
let res;
|
|
19002
19085
|
try {
|
|
19003
|
-
res = await this.http(method2,
|
|
19086
|
+
res = await this.http(method2, path43.startsWith("http") ? path43 : `${API2}${path43}`, headers, body);
|
|
19004
19087
|
} catch (e) {
|
|
19005
19088
|
throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
|
|
19006
19089
|
}
|
|
@@ -19062,8 +19145,8 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
19062
19145
|
const owners = await this.owners();
|
|
19063
19146
|
const found = owners.find((o) => o.login.toLowerCase() === owner.toLowerCase());
|
|
19064
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.`);
|
|
19065
|
-
const
|
|
19066
|
-
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" });
|
|
19067
19150
|
if (res.status === 422) {
|
|
19068
19151
|
const why = _GitHubRepos.message(res.body);
|
|
19069
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.`);
|
|
@@ -19083,7 +19166,7 @@ var GitHubRepos = class _GitHubRepos {
|
|
|
19083
19166
|
// src/usage/queries.ts
|
|
19084
19167
|
var DAY3 = 24 * 60 * 60 * 1e3;
|
|
19085
19168
|
var tokensOf = (r) => r.input + r.cache_write + r.cache_read + r.output;
|
|
19086
|
-
var
|
|
19169
|
+
var exercised2 = false;
|
|
19087
19170
|
function push(groups, key, r) {
|
|
19088
19171
|
const list2 = groups.get(key);
|
|
19089
19172
|
if (!list2) {
|
|
@@ -19091,8 +19174,8 @@ function push(groups, key, r) {
|
|
|
19091
19174
|
return;
|
|
19092
19175
|
}
|
|
19093
19176
|
list2.push(r);
|
|
19094
|
-
if (!
|
|
19095
|
-
|
|
19177
|
+
if (!exercised2) {
|
|
19178
|
+
exercised2 = true;
|
|
19096
19179
|
exercisedLine("EXERCISED #356 usage report grouped calls in place");
|
|
19097
19180
|
}
|
|
19098
19181
|
}
|
|
@@ -19318,7 +19401,7 @@ var UsageQueries = class {
|
|
|
19318
19401
|
// src/usage/publisher.ts
|
|
19319
19402
|
import crypto12 from "node:crypto";
|
|
19320
19403
|
import fs34 from "node:fs";
|
|
19321
|
-
import
|
|
19404
|
+
import path38 from "node:path";
|
|
19322
19405
|
|
|
19323
19406
|
// src/usage/publish-file.ts
|
|
19324
19407
|
import crypto11 from "node:crypto";
|
|
@@ -19464,14 +19547,14 @@ var UsageRepo = class {
|
|
|
19464
19547
|
this.http = opts.http ?? fetchHttp;
|
|
19465
19548
|
this.token = opts.token ?? ghToken;
|
|
19466
19549
|
}
|
|
19467
|
-
async call(method2,
|
|
19550
|
+
async call(method2, path43, body, extra = {}) {
|
|
19468
19551
|
if (!this.tokenValue) this.tokenValue = await this.token().catch(() => {
|
|
19469
19552
|
throw new BoardError("signin", SIGN_IN3);
|
|
19470
19553
|
});
|
|
19471
19554
|
const headers = { authorization: `Bearer ${this.tokenValue}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28", "user-agent": "Ledung", ...extra };
|
|
19472
19555
|
let res;
|
|
19473
19556
|
try {
|
|
19474
|
-
res = await this.http(method2, `${API3}${
|
|
19557
|
+
res = await this.http(method2, `${API3}${path43}`, headers, body);
|
|
19475
19558
|
} catch (e) {
|
|
19476
19559
|
throw new BoardError("unreachable", `GitHub could not be reached: ${e.message}`);
|
|
19477
19560
|
}
|
|
@@ -19489,8 +19572,8 @@ var UsageRepo = class {
|
|
|
19489
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})` : ""}.`);
|
|
19490
19573
|
return new BoardError("unreachable", `GitHub answered ${res.status} for ${what}${why ? ` (${why})` : ""}.`);
|
|
19491
19574
|
}
|
|
19492
|
-
async ok(what, method2,
|
|
19493
|
-
const res = await this.call(method2,
|
|
19575
|
+
async ok(what, method2, path43, body) {
|
|
19576
|
+
const res = await this.call(method2, path43, body);
|
|
19494
19577
|
if (res.status < 200 || res.status >= 300) throw this.failure(what, res);
|
|
19495
19578
|
return res.body;
|
|
19496
19579
|
}
|
|
@@ -19672,7 +19755,7 @@ var UsagePublisher = class {
|
|
|
19672
19755
|
}
|
|
19673
19756
|
// --- The usage key ---------------------------------------------------------------------------------------
|
|
19674
19757
|
keyFile() {
|
|
19675
|
-
return
|
|
19758
|
+
return path38.join(this.opts.keyDir, USAGE_KEY_FILE);
|
|
19676
19759
|
}
|
|
19677
19760
|
/** The usage key, or null when this computer has none yet. */
|
|
19678
19761
|
key() {
|
|
@@ -20105,13 +20188,13 @@ function originOf(folder) {
|
|
|
20105
20188
|
|
|
20106
20189
|
// src/remote/link.ts
|
|
20107
20190
|
import fs36 from "node:fs";
|
|
20108
|
-
import
|
|
20191
|
+
import path40 from "node:path";
|
|
20109
20192
|
|
|
20110
20193
|
// src/server/acme.ts
|
|
20111
20194
|
import crypto13 from "node:crypto";
|
|
20112
20195
|
import dns from "node:dns";
|
|
20113
20196
|
import fs35 from "node:fs";
|
|
20114
|
-
import
|
|
20197
|
+
import path39 from "node:path";
|
|
20115
20198
|
import acme from "acme-client";
|
|
20116
20199
|
var ACCOUNT_KEY = "acme-account.key";
|
|
20117
20200
|
var ACME_STATE = "acme.json";
|
|
@@ -20342,8 +20425,8 @@ var CertificateManager = class {
|
|
|
20342
20425
|
const chain = Buffer.from(await client.getCertificate(finalized));
|
|
20343
20426
|
const problem2 = certificateProblem({ key, cert: chain }, name);
|
|
20344
20427
|
if (problem2) throw new Error(problem2);
|
|
20345
|
-
writePrivate(
|
|
20346
|
-
writePrivate(
|
|
20428
|
+
writePrivate(path39.join(this.opts.dir, TLS_KEY), key);
|
|
20429
|
+
writePrivate(path39.join(this.opts.dir, TLS_CERT), chain);
|
|
20347
20430
|
this.save({ issuedBy: this.opts.directoryUrl });
|
|
20348
20431
|
}
|
|
20349
20432
|
/** The certificate in remoteDir() if it can serve the name, has not run out and is this directory's; null otherwise. */
|
|
@@ -20351,7 +20434,7 @@ var CertificateManager = class {
|
|
|
20351
20434
|
const issuedBy = this.saved().issuedBy;
|
|
20352
20435
|
if (issuedBy !== null && issuedBy !== this.opts.directoryUrl) return null;
|
|
20353
20436
|
try {
|
|
20354
|
-
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)) };
|
|
20355
20438
|
if (certificateProblem(files, this.opts.publicName)) return null;
|
|
20356
20439
|
const life = lifetime(files.cert);
|
|
20357
20440
|
return this.now() < life.validUntil ? life : null;
|
|
@@ -20373,20 +20456,20 @@ var CertificateManager = class {
|
|
|
20373
20456
|
function readSaved(dir) {
|
|
20374
20457
|
const empty = { accounts: {}, issuedBy: null, failures: 0, nextTry: null, error: null };
|
|
20375
20458
|
try {
|
|
20376
|
-
const raw = JSON.parse(fs35.readFileSync(
|
|
20459
|
+
const raw = JSON.parse(fs35.readFileSync(path39.join(dir, ACME_STATE), "utf8"));
|
|
20377
20460
|
return { ...empty, ...raw, accounts: { ...raw.accounts } };
|
|
20378
20461
|
} catch {
|
|
20379
20462
|
return empty;
|
|
20380
20463
|
}
|
|
20381
20464
|
}
|
|
20382
20465
|
function writeSaved(dir, change) {
|
|
20383
|
-
writePrivate(
|
|
20466
|
+
writePrivate(path39.join(dir, ACME_STATE), Buffer.from(JSON.stringify({ ...readSaved(dir), ...change }, null, 2)));
|
|
20384
20467
|
}
|
|
20385
20468
|
function saveAccount(dir, directoryUrl, accountUrl) {
|
|
20386
20469
|
writeSaved(dir, { accounts: { ...readSaved(dir).accounts, [directoryUrl]: accountUrl } });
|
|
20387
20470
|
}
|
|
20388
20471
|
async function acmeAccount(opts) {
|
|
20389
|
-
const keyFile =
|
|
20472
|
+
const keyFile = path39.join(opts.dir, ACCOUNT_KEY);
|
|
20390
20473
|
let key;
|
|
20391
20474
|
try {
|
|
20392
20475
|
key = fs35.readFileSync(keyFile);
|
|
@@ -20902,11 +20985,11 @@ var RemoteLink = class {
|
|
|
20902
20985
|
return { name: answer.name, host: answer.host };
|
|
20903
20986
|
}
|
|
20904
20987
|
savedFile() {
|
|
20905
|
-
return
|
|
20988
|
+
return path40.join(this.opts.dir, SAVED_REMOTE);
|
|
20906
20989
|
}
|
|
20907
20990
|
};
|
|
20908
20991
|
function savedRemote(dir, log = (line) => console.log(line)) {
|
|
20909
|
-
const file =
|
|
20992
|
+
const file = path40.join(dir, SAVED_REMOTE);
|
|
20910
20993
|
try {
|
|
20911
20994
|
return parseRemote(JSON.parse(fs36.readFileSync(file, "utf8")));
|
|
20912
20995
|
} catch (e) {
|
|
@@ -20923,7 +21006,7 @@ function writePrivate2(file, data) {
|
|
|
20923
21006
|
// src/remote/admin.ts
|
|
20924
21007
|
import crypto15 from "node:crypto";
|
|
20925
21008
|
import fs37 from "node:fs";
|
|
20926
|
-
import
|
|
21009
|
+
import path41 from "node:path";
|
|
20927
21010
|
|
|
20928
21011
|
// src/remote/cloudflare.ts
|
|
20929
21012
|
var API4 = "https://api.cloudflare.com/client/v4";
|
|
@@ -20959,10 +21042,10 @@ var Cloudflare = class {
|
|
|
20959
21042
|
}
|
|
20960
21043
|
throw new Error(`The Cloudflare token reaches no zone holding ${name}.`);
|
|
20961
21044
|
}
|
|
20962
|
-
async call(method2,
|
|
21045
|
+
async call(method2, path43, body) {
|
|
20963
21046
|
let res;
|
|
20964
21047
|
try {
|
|
20965
|
-
res = await this.fetch(`${API4}${
|
|
21048
|
+
res = await this.fetch(`${API4}${path43}`, {
|
|
20966
21049
|
method: method2,
|
|
20967
21050
|
headers: { Authorization: `Bearer ${this.token}`, ...body ? { "Content-Type": "application/json" } : {} },
|
|
20968
21051
|
body: body ? JSON.stringify(body) : void 0,
|
|
@@ -21155,7 +21238,7 @@ var RelayAdmin = class {
|
|
|
21155
21238
|
return answer;
|
|
21156
21239
|
}
|
|
21157
21240
|
file() {
|
|
21158
|
-
return
|
|
21241
|
+
return path41.join(this.opts.dir, ADMIN_FILE);
|
|
21159
21242
|
}
|
|
21160
21243
|
saved() {
|
|
21161
21244
|
try {
|
|
@@ -21204,7 +21287,7 @@ async function main() {
|
|
|
21204
21287
|
let watcher = null;
|
|
21205
21288
|
const manager = new SessionManager(config, store, (event) => server?.broadcast(event));
|
|
21206
21289
|
const pollMs = boardConfig(config).pollSeconds * 1e3;
|
|
21207
|
-
manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(),
|
|
21290
|
+
manager.attachBoards((repo) => new GitHubBackend({ repo, pollMs }), new GitHubRepos(), path42.join(userDataDir(), "board-cache"));
|
|
21208
21291
|
const attached = await manager.attachRunningHosts();
|
|
21209
21292
|
const leftover = store.markUnfinishedSessionsEnded("Ledung stopped before this session ended. Resume to continue.", attached);
|
|
21210
21293
|
manager.away.begin();
|
|
@@ -21278,7 +21361,7 @@ async function main() {
|
|
|
21278
21361
|
computerSettings: config.computers,
|
|
21279
21362
|
linkConnect: config.remote?.linkVia ? connectVia(config.remote.linkVia) : void 0,
|
|
21280
21363
|
// The VAPID keys stay on this computer, next to the relay's (issue #246).
|
|
21281
|
-
push: { keyFile:
|
|
21364
|
+
push: { keyFile: path42.join(remoteDir(), "push-vapid.json") }
|
|
21282
21365
|
});
|
|
21283
21366
|
link = new RemoteLink({ server, dir: remoteDir(), configured: config.remote, joinAcme: config.relayJoin });
|
|
21284
21367
|
server.attachRemote(link);
|