local-commander-agent-linux-arm64 0.0.0-stage → 0.3.45

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.
@@ -0,0 +1,788 @@
1
+ // agent/src/posix-lifecycle.ts
2
+ import { constants as constants2 } from "node:fs";
3
+ import { copyFile, lstat as lstat2, mkdir as mkdir2, mkdtemp, open as open2, readlink, realpath as realpath2, rename, rm, stat, symlink, unlink as unlink2 } from "node:fs/promises";
4
+ import path2 from "node:path";
5
+ import os from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import { spawn as spawn2, spawnSync } from "node:child_process";
9
+ import { setTimeout as delay } from "node:timers/promises";
10
+
11
+ // agent/src/posix-store.ts
12
+ import { constants } from "node:fs";
13
+ import { lstat, mkdir, open, readdir, realpath, unlink } from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { spawn } from "node:child_process";
16
+
17
+ // src/child-environment.ts
18
+ var COMMON_ALLOWED_KEYS = /* @__PURE__ */ new Set([
19
+ "PATH",
20
+ "TEMP",
21
+ "TMP",
22
+ "TMPDIR",
23
+ "HOME",
24
+ "USER",
25
+ "USERNAME",
26
+ "LOGNAME",
27
+ "LANG",
28
+ "LC_ALL",
29
+ "LC_CTYPE",
30
+ "TERM",
31
+ "COLORTERM",
32
+ "NO_COLOR",
33
+ "FORCE_COLOR",
34
+ "CI",
35
+ "DOTNET_ROOT",
36
+ "DOTNET_CLI_HOME"
37
+ ]);
38
+ var WINDOWS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
39
+ "PATHEXT",
40
+ "SYSTEMROOT",
41
+ "WINDIR",
42
+ "COMSPEC",
43
+ "USERPROFILE",
44
+ "HOMEDRIVE",
45
+ "HOMEPATH",
46
+ "APPDATA",
47
+ "LOCALAPPDATA",
48
+ "PROGRAMFILES",
49
+ "PROGRAMFILES(X86)",
50
+ "PROGRAMW6432",
51
+ "PROCESSOR_ARCHITECTURE",
52
+ "PROCESSOR_ARCHITEW6432",
53
+ "NUMBER_OF_PROCESSORS",
54
+ "OS",
55
+ "USERDOMAIN",
56
+ "PUBLIC"
57
+ ]);
58
+ var POSIX_ALLOWED_KEYS = /* @__PURE__ */ new Set([
59
+ "SHELL",
60
+ "XDG_CACHE_HOME",
61
+ "XDG_CONFIG_HOME",
62
+ "XDG_DATA_HOME"
63
+ ]);
64
+ var DENIED_KEYS = /* @__PURE__ */ new Set([
65
+ "NODE_OPTIONS",
66
+ "PYTHONPATH",
67
+ "PYTHONHOME",
68
+ "PYTHONSTARTUP",
69
+ "RUBYOPT",
70
+ "PERL5OPT",
71
+ "LD_PRELOAD",
72
+ "LD_LIBRARY_PATH",
73
+ "DYLD_INSERT_LIBRARIES",
74
+ "DYLD_LIBRARY_PATH",
75
+ "GIT_SSH_COMMAND",
76
+ "GIT_ASKPASS",
77
+ "SSH_ASKPASS",
78
+ "NPM_CONFIG_USERCONFIG"
79
+ ]);
80
+ var DENIED_SUBSTRINGS = [
81
+ "LCMCP",
82
+ "LOCAL_COMMANDER",
83
+ "PAYPAL",
84
+ "SQL",
85
+ "CONNECTIONSTRING",
86
+ "CONNECTION_STRING",
87
+ "PASSWORD",
88
+ "PASSWD",
89
+ "SECRET",
90
+ "TOKEN",
91
+ "CREDENTIAL",
92
+ "OPENAI",
93
+ "ANTHROPIC",
94
+ "API_KEY",
95
+ "ACCESS_KEY",
96
+ "PRIVATE_KEY"
97
+ ];
98
+ function canonicalAllowedKeys(platform) {
99
+ const keys = new Set(COMMON_ALLOWED_KEYS);
100
+ if (platform === "win32") {
101
+ for (const key of WINDOWS_ALLOWED_KEYS) keys.add(key);
102
+ } else {
103
+ for (const key of POSIX_ALLOWED_KEYS) keys.add(key);
104
+ }
105
+ const result = /* @__PURE__ */ new Map();
106
+ for (const key of keys) result.set(platform === "win32" ? key.toUpperCase() : key, key);
107
+ return result;
108
+ }
109
+ function isDeniedKey(key) {
110
+ const upper = key.toUpperCase();
111
+ if (DENIED_KEYS.has(upper)) return true;
112
+ return DENIED_SUBSTRINGS.some((item) => upper.includes(item));
113
+ }
114
+ function buildChildEnvironment(source, platform = process.platform) {
115
+ const allowed = canonicalAllowedKeys(platform);
116
+ const result = {};
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (const [rawKey, value] of Object.entries(source)) {
119
+ if (value === void 0) continue;
120
+ const lookupKey = platform === "win32" ? rawKey.toUpperCase() : rawKey;
121
+ const outputKey = allowed.get(lookupKey);
122
+ if (!outputKey) continue;
123
+ if (isDeniedKey(rawKey) || isDeniedKey(outputKey)) continue;
124
+ if (seen.has(lookupKey)) continue;
125
+ seen.add(lookupKey);
126
+ result[outputKey] = value;
127
+ }
128
+ return result;
129
+ }
130
+
131
+ // agent/src/posix-store.ts
132
+ var fail = (code) => {
133
+ throw Object.assign(Error(code), { code });
134
+ };
135
+ async function privateDirectory(directory) {
136
+ const stat2 = await lstat(directory);
137
+ if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0 || await realpath(directory) !== directory) fail("STORE_DIRECTORY_INVALID");
138
+ }
139
+ async function privateRead(filename, max) {
140
+ const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW);
141
+ try {
142
+ const stat2 = await handle.stat();
143
+ if (!stat2.isFile() || stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0 || stat2.nlink !== 1 || stat2.size > max)
144
+ return fail("STORE_FILE_INVALID");
145
+ const data = Buffer.alloc(max + 1);
146
+ let length = 0;
147
+ while (length < data.length) {
148
+ const read = await handle.read(data, length, data.length - length, null);
149
+ if (!read.bytesRead) break;
150
+ length += read.bytesRead;
151
+ }
152
+ if (length > max) return fail("STORE_FILE_INVALID");
153
+ return data.subarray(0, length);
154
+ } finally {
155
+ await handle.close();
156
+ }
157
+ }
158
+ async function syncDirectory(directory) {
159
+ const handle = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
160
+ try {
161
+ await handle.sync();
162
+ } finally {
163
+ await handle.close();
164
+ }
165
+ }
166
+ async function absent(filename) {
167
+ try {
168
+ await lstat(filename);
169
+ return false;
170
+ } catch (e) {
171
+ if (e.code === "ENOENT") return true;
172
+ throw e;
173
+ }
174
+ }
175
+ async function acquirePosixJournalGuard(directory, helper) {
176
+ await privateDirectory(directory);
177
+ const binary = await lstat(helper);
178
+ if (!binary.isFile() || binary.isSymbolicLink() || binary.mode & 18 || !(binary.mode & 73) || binary.uid !== 0 && binary.uid !== process.getuid() || await realpath(helper) !== helper) fail("JOURNAL_HELPER_INVALID");
179
+ const child = spawn(
180
+ helper,
181
+ ["--lock", path.join(directory, ".journal.guard")],
182
+ { stdio: ["pipe", "pipe", "ignore"], env: buildChildEnvironment(process.env), windowsHide: true, shell: false }
183
+ );
184
+ child.stdin.on("error", () => {
185
+ });
186
+ let ended = false;
187
+ const closed = new Promise((resolve) => child.once("close", () => {
188
+ ended = true;
189
+ resolve();
190
+ }));
191
+ const guard = { active: false, close: async () => {
192
+ if (!ended) child.stdin.end();
193
+ const timer = setTimeout(() => child.kill("SIGKILL"), 1e3);
194
+ try {
195
+ await closed;
196
+ } finally {
197
+ clearTimeout(timer);
198
+ guard.active = false;
199
+ }
200
+ } };
201
+ child.once("close", () => {
202
+ guard.active = false;
203
+ });
204
+ try {
205
+ await new Promise((resolve, reject) => {
206
+ let text = "";
207
+ const timer = setTimeout(() => {
208
+ child.kill("SIGKILL");
209
+ reject(Error("JOURNAL_LOCKED"));
210
+ }, 5e3);
211
+ const done = (error) => {
212
+ clearTimeout(timer);
213
+ error ? reject(error) : resolve();
214
+ };
215
+ child.once("error", () => done(Error("JOURNAL_HELPER_INVALID")));
216
+ child.once("close", (code) => done(Error(code === 75 ? "JOURNAL_LOCKED" : "JOURNAL_LOCK_INVALID")));
217
+ child.stdout.setEncoding("utf8");
218
+ child.stdout.on("data", (chunk) => {
219
+ text += chunk;
220
+ if (text.length < 64 && !text.includes("\n")) return;
221
+ if (text === '{"locked":true}\n') {
222
+ guard.active = true;
223
+ done();
224
+ } else {
225
+ child.kill("SIGKILL");
226
+ done(Error("JOURNAL_LOCK_INVALID"));
227
+ }
228
+ });
229
+ });
230
+ const lock = path.join(directory, "journal.lock");
231
+ if (!await absent(lock)) {
232
+ let previous;
233
+ try {
234
+ previous = JSON.parse((await privateRead(lock, 1024)).toString());
235
+ } catch {
236
+ return fail("JOURNAL_LOCK_INVALID");
237
+ }
238
+ if (!previous || Object.keys(previous).sort().join(",") !== "createdAt,nonce,pid" || !Number.isSafeInteger(previous.pid) || previous.pid <= 0 || previous.pid > 4294967295 || !/^[a-f0-9]{32}$/.test(previous.nonce) || typeof previous.createdAt !== "string" || !Number.isFinite(Date.parse(previous.createdAt)) || new Date(previous.createdAt).toISOString() !== previous.createdAt) return fail("JOURNAL_LOCK_INVALID");
239
+ let absentOwner = false;
240
+ try {
241
+ process.kill(previous.pid, 0);
242
+ } catch (e) {
243
+ absentOwner = e.code === "ESRCH";
244
+ }
245
+ if (!absentOwner || !guard.active) return fail("JOURNAL_LOCKED");
246
+ await unlink(lock);
247
+ await syncDirectory(directory);
248
+ }
249
+ if (!guard.active) return fail("JOURNAL_LOCK_LOST");
250
+ return guard;
251
+ } catch (e) {
252
+ await guard.close();
253
+ throw e;
254
+ }
255
+ }
256
+
257
+ // agent/src/posix-lifecycle.ts
258
+ var label = "app.localcmd.agent";
259
+ var PRODUCTION_ORIGIN = "https://localcmd.app";
260
+ function fail2(code) {
261
+ throw Error(code);
262
+ }
263
+ var hash = (data) => createHash("sha256").update(data).digest("hex");
264
+ var idPattern = /^[a-f0-9]{64}$/;
265
+ function privateInstallPaths() {
266
+ const home = os.homedir();
267
+ const data = path2.join(home, process.platform === "darwin" ? "Library/Application Support" : ".local/share", "LocalCommander/Runtime");
268
+ const root = path2.join(data, "LocalCommander");
269
+ return {
270
+ home,
271
+ data,
272
+ root,
273
+ state: path2.join(root, "Agent"),
274
+ versions: path2.join(root, "versions"),
275
+ service: path2.join(home, process.platform === "darwin" ? "Library/LaunchAgents/" + label + ".plist" : ".config/systemd/user/" + label + ".service")
276
+ };
277
+ }
278
+ var paths = privateInstallPaths();
279
+ var unit = label + ".service";
280
+ var domain = "gui/" + process.getuid?.();
281
+ var env = {
282
+ HOME: paths.home,
283
+ USER: os.userInfo().username,
284
+ LOGNAME: os.userInfo().username,
285
+ PATH: "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
286
+ LC_NATIVE_253_INSTALLED: "1",
287
+ ...process.platform === "linux" ? {
288
+ XDG_CONFIG_HOME: path2.join(paths.home, ".config"),
289
+ XDG_RUNTIME_DIR: "/run/user/" + process.getuid(),
290
+ DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/" + process.getuid() + "/bus"
291
+ } : {}
292
+ };
293
+ async function absent2(filename) {
294
+ try {
295
+ await lstat2(filename);
296
+ return false;
297
+ } catch (e) {
298
+ if (e.code === "ENOENT") return true;
299
+ throw e;
300
+ }
301
+ }
302
+ async function ownedParents(directory) {
303
+ if (!path2.isAbsolute(directory) || /[\0-\x1f\x7f]/.test(directory)) fail2("LIFECYCLE_PATH_INVALID");
304
+ const missing = [];
305
+ let cursor = directory;
306
+ while (await absent2(cursor)) {
307
+ missing.push(cursor);
308
+ cursor = path2.dirname(cursor);
309
+ }
310
+ for (let ancestor = cursor; ; ancestor = path2.dirname(ancestor)) {
311
+ const stat2 = await lstat2(ancestor);
312
+ if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.mode & 18 || stat2.uid !== 0 && stat2.uid !== process.getuid() || await realpath2(ancestor) !== ancestor) {
313
+ process.stderr.write(JSON.stringify({
314
+ unsafeParent: ancestor,
315
+ mode: (stat2.mode & 4095).toString(8),
316
+ owner: stat2.uid,
317
+ expectedOwner: process.getuid(),
318
+ symlink: stat2.isSymbolicLink(),
319
+ canonical: await realpath2(ancestor) === ancestor
320
+ }) + "\n");
321
+ fail2("LIFECYCLE_PATH_INVALID");
322
+ }
323
+ if (ancestor === path2.dirname(ancestor)) break;
324
+ }
325
+ for (const item of missing.reverse()) await mkdir2(item, { mode: 448 });
326
+ }
327
+ async function versionLink(name) {
328
+ const filename = path2.join(paths.root, name);
329
+ if (await absent2(filename)) return null;
330
+ const stat2 = await lstat2(filename);
331
+ if (!stat2.isSymbolicLink() || stat2.uid !== process.getuid()) fail2("LIFECYCLE_LINK_INVALID");
332
+ const target = await readlink(filename);
333
+ if (!/^versions\/[a-f0-9]{64}$/.test(target)) fail2("LIFECYCLE_LINK_INVALID");
334
+ return target.slice("versions/".length);
335
+ }
336
+ async function setVersion(name, id) {
337
+ if (!idPattern.test(id)) fail2("LIFECYCLE_LINK_INVALID");
338
+ await versionLink(name);
339
+ const target = path2.join(paths.root, name), temporary = target + "." + randomUUID();
340
+ await symlink("versions/" + id, temporary);
341
+ try {
342
+ await rename(temporary, target);
343
+ await syncDirectory(paths.root);
344
+ } finally {
345
+ await rm(temporary, { force: true });
346
+ }
347
+ }
348
+ var executableNames = /* @__PURE__ */ new Set(["node", "posix-host", "owner-store"]);
349
+ async function digestFile(filename) {
350
+ const file = await open2(filename, constants2.O_RDONLY | constants2.O_NOFOLLOW);
351
+ try {
352
+ const stat2 = await file.stat();
353
+ if (!stat2.isFile() || stat2.uid !== process.getuid() || stat2.nlink !== 1 || stat2.mode & 63 || stat2.size < 1 || stat2.size > 160 * 1024 * 1024 || executableNames.has(path2.basename(filename)) && !(stat2.mode & 64)) fail2("LIFECYCLE_ARTIFACT_INVALID");
354
+ const digest = createHash("sha256");
355
+ for await (const chunk of file.createReadStream({ autoClose: false })) digest.update(chunk);
356
+ return digest.digest("hex");
357
+ } finally {
358
+ await file.close();
359
+ }
360
+ }
361
+ async function artifact(directory) {
362
+ await privateDirectory(directory);
363
+ const bytes = await privateRead(path2.join(directory, "manifest.json"), 16384);
364
+ const manifest = JSON.parse(bytes.toString());
365
+ const names = [
366
+ "agent.mjs",
367
+ "lifecycle.mjs",
368
+ "node",
369
+ "posix-host",
370
+ "NODE-LICENSE.txt",
371
+ "job-host.mjs",
372
+ ...process.platform === "darwin" ? ["owner-store"] : []
373
+ ].sort();
374
+ if (!manifest || manifest.experimental !== true || manifest.lifecycleVersion !== 1 || manifest.platform !== process.platform || manifest.arch !== process.arch || !manifest.files || Object.keys(manifest.files).sort().join() !== names.join()) fail2("LIFECYCLE_ARTIFACT_INVALID");
375
+ for (const name of names)
376
+ if (!idPattern.test(manifest.files[name]) || await digestFile(path2.join(directory, name)) !== manifest.files[name]) fail2("LIFECYCLE_ARTIFACT_INVALID");
377
+ return { id: hash(bytes), names: [...names, "manifest.json"] };
378
+ }
379
+ async function stage(source) {
380
+ const verified = await artifact(source), target = path2.join(paths.versions, verified.id);
381
+ if (!await absent2(target)) {
382
+ if ((await artifact(target)).id !== verified.id) fail2("LIFECYCLE_ARTIFACT_INVALID");
383
+ return verified.id;
384
+ }
385
+ const temporary = await mkdtemp(path2.join(paths.versions, ".stage-"));
386
+ try {
387
+ for (const name of verified.names) {
388
+ const filename = path2.join(temporary, name);
389
+ await copyFile(path2.join(source, name), filename, constants2.COPYFILE_EXCL);
390
+ const file = await open2(filename, constants2.O_RDONLY | constants2.O_NOFOLLOW);
391
+ try {
392
+ await file.sync();
393
+ } finally {
394
+ await file.close();
395
+ }
396
+ }
397
+ if ((await artifact(temporary)).id !== verified.id) fail2("LIFECYCLE_ARTIFACT_INVALID");
398
+ await syncDirectory(temporary);
399
+ await rename(temporary, target);
400
+ await syncDirectory(paths.versions);
401
+ return verified.id;
402
+ } finally {
403
+ await rm(temporary, { recursive: true, force: true });
404
+ }
405
+ }
406
+ var logPath = () => path2.join(paths.root, "agent.log");
407
+ function serviceText(legacy = false) {
408
+ const args = [
409
+ "/usr/bin/env",
410
+ "-i",
411
+ ...Object.entries(env).map(([k, v]) => k + "=" + v),
412
+ path2.join(paths.root, "current/node"),
413
+ path2.join(paths.root, "current/agent.mjs"),
414
+ "run"
415
+ ];
416
+ if (process.platform === "linux") {
417
+ const quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%") + '"';
418
+ return "[Unit]\nDescription=Local Commander agent\nStartLimitIntervalSec=30s\nStartLimitBurst=3\n[Service]\nType=exec\nExecStart=:" + args.map(quote).join(" ") + "\nUMask=0077\nRestart=on-failure\nRestartSec=2s\nKillMode=mixed\nTimeoutStopSec=10s\nStandardOutput=null\nStandardError=" + (legacy ? "null" : "journal") + "\n[Install]\nWantedBy=default.target\n";
419
+ }
420
+ const xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
421
+ return '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>' + label + "</string><key>ProgramArguments</key><array>" + args.map((s) => "<string>" + xml(s) + "</string>").join("") + "</array><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>ThrottleInterval</key><integer>2</integer><key>ExitTimeOut</key><integer>10</integer><key>Umask</key><integer>63</integer><key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>" + (legacy ? "/dev/null" : xml(logPath())) + "</string></dict></plist>\n";
422
+ }
423
+ function manager(args, allowFailure = false) {
424
+ const result = spawnSync(
425
+ process.platform === "linux" ? "/usr/bin/systemctl" : "/bin/launchctl",
426
+ process.platform === "linux" ? ["--user", "--no-pager", ...args] : args,
427
+ { env, encoding: "utf8", timeout: 15e3, maxBuffer: 65536, shell: false, windowsHide: true }
428
+ );
429
+ if (result.error || result.signal || !allowFailure && result.status !== 0) fail2("LIFECYCLE_MANAGER_FAILED");
430
+ return result;
431
+ }
432
+ var upgradeService = false;
433
+ async function registered() {
434
+ if (await absent2(paths.service)) return false;
435
+ const text = (await privateRead(paths.service, 16384)).toString();
436
+ if (text === serviceText()) return true;
437
+ if (text !== serviceText(true)) fail2("LIFECYCLE_SERVICE_CONFLICT");
438
+ if (upgradeService) {
439
+ const temporary = paths.service + "." + randomUUID();
440
+ const handle = await open2(temporary, "wx", 384);
441
+ try {
442
+ await handle.writeFile(serviceText());
443
+ await handle.sync();
444
+ } finally {
445
+ await handle.close();
446
+ }
447
+ try {
448
+ await rename(temporary, paths.service);
449
+ await syncDirectory(path2.dirname(paths.service));
450
+ } finally {
451
+ await rm(temporary, { force: true });
452
+ }
453
+ if (process.platform === "linux") manager(["daemon-reload"]);
454
+ }
455
+ return true;
456
+ }
457
+ async function prepareLog() {
458
+ if (process.platform !== "darwin") return;
459
+ const file = logPath();
460
+ try {
461
+ if ((await stat(file)).size > 1024 * 1024) await rename(file, file + ".1");
462
+ } catch (e) {
463
+ if (e.code !== "ENOENT") throw e;
464
+ }
465
+ const handle = await open2(file, constants2.O_CREAT | constants2.O_WRONLY | constants2.O_APPEND | constants2.O_NOFOLLOW, 384);
466
+ try {
467
+ if ((await handle.stat()).uid !== process.getuid()) fail2("LIFECYCLE_PATH_INVALID");
468
+ } finally {
469
+ await handle.close();
470
+ }
471
+ }
472
+ function servicePid() {
473
+ if (process.platform === "linux") {
474
+ const result2 = manager(["show", unit, "--property=MainPID,FragmentPath,DropInPaths"]);
475
+ const fields = Object.fromEntries(result2.stdout.trim().split("\n").map((line) => {
476
+ const index = line.indexOf("=");
477
+ return [line.slice(0, index), line.slice(index + 1)];
478
+ }));
479
+ if (fields.FragmentPath !== paths.service || fields.DropInPaths !== "") fail2("LIFECYCLE_SERVICE_CONFLICT");
480
+ const pid = Number(fields.MainPID);
481
+ if (!Number.isSafeInteger(pid) || pid < 0) fail2("LIFECYCLE_STATUS_INVALID");
482
+ return pid || null;
483
+ }
484
+ const result = manager(["print", domain + "/" + label], true);
485
+ if (result.status !== 0) return null;
486
+ if (/^\s*path = (.+)$/m.exec(result.stdout)?.[1]?.trim() !== paths.service) fail2("LIFECYCLE_SERVICE_CONFLICT");
487
+ const match = /^\s*pid = ([0-9]+)\s*$/m.exec(result.stdout);
488
+ return match ? Number(match[1]) : null;
489
+ }
490
+ async function stop() {
491
+ if (!await registered()) return;
492
+ servicePid();
493
+ if (process.platform === "linux") manager(["stop", unit]);
494
+ else {
495
+ const exists = manager(["print", domain + "/" + label], true);
496
+ if (exists.status === 0) manager(["bootout", domain + "/" + label]);
497
+ }
498
+ for (let i = 0; i < 100; i++) {
499
+ if (!servicePid()) return;
500
+ await delay(100);
501
+ }
502
+ fail2("LIFECYCLE_STOP_TIMEOUT");
503
+ }
504
+ async function start() {
505
+ if (!await registered()) fail2("LIFECYCLE_NOT_INSTALLED");
506
+ await prepareLog();
507
+ const id = await versionLink("current");
508
+ if (!id || (await artifact(path2.join(paths.versions, id))).id !== id) fail2("LIFECYCLE_ARTIFACT_INVALID");
509
+ servicePid();
510
+ if (process.platform === "linux") {
511
+ manager(["reset-failed", unit]);
512
+ manager(["start", unit]);
513
+ } else if (manager(["print", domain + "/" + label], true).status !== 0) manager(["bootstrap", domain, paths.service]);
514
+ else manager(["kickstart", domain + "/" + label]);
515
+ for (let i = 0; i < 100; i++) {
516
+ const pid = servicePid();
517
+ try {
518
+ const record = JSON.parse((await privateRead(path2.join(paths.state, "agent-process.json"), 8192)).toString());
519
+ const lock = JSON.parse((await privateRead(path2.join(paths.state, "journal/journal.lock"), 1024)).toString());
520
+ if (pid && record.pid === pid && lock.pid === pid && record.runtimeDir === path2.join(paths.versions, id)) return;
521
+ if (i === 99) process.stderr.write(JSON.stringify({
522
+ servicePid: pid,
523
+ recordPid: record.pid,
524
+ journalPid: lock.pid,
525
+ canonicalRuntime: record.runtimeDir === path2.join(paths.versions, id),
526
+ linkedRuntime: record.runtimeDir === path2.join(paths.root, "current")
527
+ }) + "\n");
528
+ } catch (e) {
529
+ if (e.code !== "ENOENT") throw e;
530
+ }
531
+ await delay(100);
532
+ }
533
+ if (process.platform === "darwin") {
534
+ const detail = manager(["print", domain + "/" + label], true);
535
+ process.stderr.write(detail.stdout.split("\n").filter((line) => /^\s*(state|pid|last exit code|last terminating signal|runs) = [a-z0-9 -]+\s*$/.test(line)).join("\n") + "\n");
536
+ }
537
+ fail2("LIFECYCLE_START_TIMEOUT");
538
+ }
539
+ var pendingPath = path2.join(paths.root, "lifecycle-pending.json");
540
+ async function pendingSwitch() {
541
+ if (await absent2(pendingPath)) return null;
542
+ let value;
543
+ try {
544
+ value = JSON.parse((await privateRead(pendingPath, 1024)).toString());
545
+ } catch {
546
+ return fail2("LIFECYCLE_RECOVERY_INVALID");
547
+ }
548
+ if (!value || value.version !== 1 || typeof value.wasRunning !== "boolean" || typeof value.current !== "string" || !idPattern.test(value.current) || typeof value.next !== "string" || !idPattern.test(value.next) || value.current === value.next || value.previous !== null && (typeof value.previous !== "string" || !idPattern.test(value.previous)) || Object.keys(value).sort().join() !== "current,next,previous,version,wasRunning") fail2("LIFECYCLE_RECOVERY_INVALID");
549
+ return value;
550
+ }
551
+ async function saveSwitch(value) {
552
+ if (!await absent2(pendingPath)) fail2("LIFECYCLE_RECOVERY_REQUIRED");
553
+ const temporary = pendingPath + "." + randomUUID();
554
+ const handle = await open2(temporary, "wx", 384);
555
+ try {
556
+ await handle.writeFile(JSON.stringify(value) + "\n");
557
+ await handle.sync();
558
+ await rename(temporary, pendingPath);
559
+ await syncDirectory(paths.root);
560
+ } finally {
561
+ await handle.close();
562
+ await rm(temporary, { force: true });
563
+ }
564
+ }
565
+ async function clearSwitch() {
566
+ await unlink2(pendingPath);
567
+ await syncDirectory(paths.root);
568
+ }
569
+ async function restoreSwitch(value, restart) {
570
+ if (!await registered()) fail2("LIFECYCLE_SERVICE_CONFLICT");
571
+ const current = await versionLink("current"), previous = await versionLink("previous");
572
+ if (current !== value.current && current !== value.next || previous !== value.previous && previous !== value.current) fail2("LIFECYCLE_RECOVERY_CONFLICT");
573
+ for (const id of new Set([value.current, value.previous].filter((id2) => id2 !== null)))
574
+ if ((await artifact(path2.join(paths.versions, id))).id !== id) fail2("LIFECYCLE_ARTIFACT_INVALID");
575
+ await stop();
576
+ await setVersion("current", value.current);
577
+ if (value.previous) await setVersion("previous", value.previous);
578
+ else if (await versionLink("previous")) {
579
+ await unlink2(path2.join(paths.root, "previous"));
580
+ await syncDirectory(paths.root);
581
+ }
582
+ if (restart && value.wasRunning) await start();
583
+ await clearSwitch();
584
+ }
585
+ function agentArgs(args) {
586
+ const runtime = path2.join(paths.root, "current");
587
+ return {
588
+ file: path2.join(runtime, "node"),
589
+ args: [path2.join(runtime, "agent.mjs"), ...args],
590
+ env: { ...env, LC_CONTROL_PLANE_ORIGIN: process.env.LC_CONTROL_PLANE_ORIGIN || PRODUCTION_ORIGIN }
591
+ };
592
+ }
593
+ function agentStatus() {
594
+ const call = agentArgs(["identity-status"]);
595
+ const result = spawnSync(call.file, call.args, { env: call.env, encoding: "utf8", timeout: 3e4, maxBuffer: 65536, shell: false });
596
+ if (result.error || result.signal || result.status !== 0) fail2("AGENT_STATUS_FAILED");
597
+ const value = JSON.parse(result.stdout.trim().split("\n").at(-1));
598
+ if (typeof value?.paired !== "boolean") fail2("AGENT_STATUS_FAILED");
599
+ return value;
600
+ }
601
+ function configureAccess() {
602
+ const call = agentArgs(["configure-access"]);
603
+ const result = spawnSync(call.file, call.args, { env: call.env, stdio: ["inherit", "pipe", "inherit"], encoding: "utf8", timeout: 10 * 6e4, shell: false });
604
+ if (result.error || result.signal || result.status !== 0) fail2("ACCESS_SETUP_FAILED");
605
+ if (!result.stdout.includes("ACCESS_CONFIGURED")) fail2("ACCESS_CANCELLED");
606
+ }
607
+ async function pair() {
608
+ const call = agentArgs(["pair"]);
609
+ const child = spawn2(call.file, call.args, { env: call.env, stdio: ["ignore", "pipe", "inherit"], shell: false });
610
+ let buffer = "", paired = false;
611
+ child.stdout.setEncoding("utf8");
612
+ child.stdout.on("data", (chunk) => {
613
+ buffer += chunk;
614
+ for (let i; (i = buffer.indexOf("\n")) >= 0; ) {
615
+ const line = buffer.slice(0, i).trim();
616
+ buffer = buffer.slice(i + 1);
617
+ if (line === "DEVICE_PAIRED") {
618
+ paired = true;
619
+ continue;
620
+ }
621
+ try {
622
+ const begun = JSON.parse(line);
623
+ if (typeof begun.approvalUrl === "string") process.stdout.write("\nApprove this computer in your browser:\n " + begun.approvalUrl + "\nConfirmation code: " + begun.confirmationCode + "\nWaiting for approval...\n");
624
+ } catch {
625
+ }
626
+ }
627
+ });
628
+ const code = await new Promise((resolve, reject) => {
629
+ child.once("error", reject);
630
+ child.once("close", resolve);
631
+ });
632
+ if (code !== 0 || !paired) fail2("PAIRING_FAILED");
633
+ }
634
+ async function main() {
635
+ if (!["linux", "darwin"].includes(process.platform) || !process.getuid || !process.getuid() || process.geteuid?.() !== process.getuid()) fail2("POSIX_OWNER_REQUIRED");
636
+ const command = process.argv[2];
637
+ const purge = process.argv.length === 4 && command === "uninstall" && process.argv[3] === "--purge-state";
638
+ if (process.argv.length !== 3 && !purge || !["setup", "install", "pair", "access", "update", "rollback", "start", "stop", "status", "uninstall"].includes(command)) fail2("LIFECYCLE_ARGUMENT_INVALID");
639
+ const source = path2.dirname(fileURLToPath(import.meta.url));
640
+ if (command === "status" && await absent2(paths.root)) {
641
+ process.stdout.write(JSON.stringify({ installed: false, running: false, stateDirectory: paths.state }) + "\n");
642
+ return;
643
+ }
644
+ await ownedParents(paths.root);
645
+ await privateDirectory(paths.root);
646
+ const marker = path2.join(paths.root, "agent.marker");
647
+ if (await absent2(marker)) {
648
+ const { readdir: readdir2 } = await import("node:fs/promises");
649
+ if ((await readdir2(paths.root)).length || command !== "install" && command !== "setup") fail2("LIFECYCLE_DIRECTORY_UNKNOWN");
650
+ const handle = await open2(marker, "wx", 384);
651
+ try {
652
+ await handle.writeFile(label + "\n");
653
+ await handle.sync();
654
+ } finally {
655
+ await handle.close();
656
+ }
657
+ await syncDirectory(paths.root);
658
+ } else if ((await privateRead(marker, 128)).toString() !== label + "\n") fail2("LIFECYCLE_DIRECTORY_UNKNOWN");
659
+ const guard = await acquirePosixJournalGuard(paths.root, path2.join(source, "posix-host"));
660
+ upgradeService = command !== "status";
661
+ try {
662
+ await ownedParents(path2.dirname(paths.service));
663
+ await ownedParents(paths.versions);
664
+ await privateDirectory(paths.versions);
665
+ const pending = await pendingSwitch();
666
+ if (pending && command !== "status") await restoreSwitch(pending, !["start", "stop", "uninstall"].includes(command));
667
+ const installed = await registered(), current = await versionLink("current");
668
+ if (command === "status") {
669
+ const pid = installed ? servicePid() : null;
670
+ process.stdout.write(JSON.stringify({
671
+ installed,
672
+ running: !!pid,
673
+ pid,
674
+ current,
675
+ previous: await versionLink("previous"),
676
+ recoveryPending: !!pending,
677
+ runtimeDirectory: current ? path2.join(paths.versions, current) : null,
678
+ stateDirectory: paths.state,
679
+ dataDirectory: paths.data
680
+ }) + "\n");
681
+ return;
682
+ }
683
+ const install = async () => {
684
+ const id = await stage(source);
685
+ if (current && current !== id) fail2("LIFECYCLE_ALREADY_INSTALLED");
686
+ await setVersion("current", id);
687
+ if (!installed) {
688
+ const handle = await open2(paths.service, "wx", 384);
689
+ try {
690
+ await handle.writeFile(serviceText());
691
+ await handle.sync();
692
+ } finally {
693
+ await handle.close();
694
+ }
695
+ await syncDirectory(path2.dirname(paths.service));
696
+ }
697
+ if (process.platform === "linux") {
698
+ manager(["daemon-reload"]);
699
+ servicePid();
700
+ manager(["enable", unit]);
701
+ }
702
+ };
703
+ if (command === "install") await install();
704
+ else if (command === "setup") {
705
+ if (!installed || !current) await install();
706
+ await stop();
707
+ const identity = agentStatus();
708
+ if (!identity.paired) await pair();
709
+ configureAccess();
710
+ await start();
711
+ const status = agentStatus();
712
+ if (process.platform === "darwin") process.stdout.write("\nmacOS asks once before Local Commander first reads Documents, Desktop or Downloads; click Allow.\nTo allow it in advance: System Settings > Privacy & Security > Full Disk Access, add " + path2.join(paths.root, "current/node") + "\nAgent log: " + logPath() + "\n\n");
713
+ process.stdout.write(JSON.stringify({ action: "setup", success: true, running: !!servicePid(), accountEmail: status.accountEmail, deviceId: status.deviceId }) + "\n");
714
+ return;
715
+ } else if (command === "pair" || command === "access") {
716
+ if (!installed || !current) fail2("LIFECYCLE_NOT_INSTALLED");
717
+ const wasRunning = !!servicePid();
718
+ await stop();
719
+ if (command === "pair") {
720
+ if (agentStatus().paired) fail2("DEVICE_ALREADY_PAIRED");
721
+ await pair();
722
+ } else configureAccess();
723
+ if (wasRunning) await start();
724
+ } else if (command === "update" || command === "rollback") {
725
+ if (!installed || !current) fail2("LIFECYCLE_NOT_INSTALLED");
726
+ const next = command === "update" ? await stage(source) : await versionLink("previous");
727
+ if (!next || (await artifact(path2.join(paths.versions, next))).id !== next) fail2("LIFECYCLE_NO_ROLLBACK");
728
+ if (next === current) fail2("LIFECYCLE_VERSION_UNCHANGED");
729
+ const pending2 = { version: 1, current, previous: await versionLink("previous"), next, wasRunning: !!servicePid() };
730
+ await saveSwitch(pending2);
731
+ try {
732
+ await stop();
733
+ await setVersion("previous", current);
734
+ await setVersion("current", next);
735
+ if (pending2.wasRunning) await start();
736
+ } catch (error) {
737
+ await restoreSwitch(pending2, true);
738
+ throw error;
739
+ }
740
+ await clearSwitch();
741
+ } else if (command === "start") {
742
+ try {
743
+ await start();
744
+ } catch (error) {
745
+ await stop();
746
+ throw error;
747
+ }
748
+ } else if (command === "stop") await stop();
749
+ else if (command === "uninstall") {
750
+ if (!installed) fail2("LIFECYCLE_NOT_INSTALLED");
751
+ await stop();
752
+ if (process.platform === "linux") manager(["disable", unit]);
753
+ if (!await registered()) fail2("LIFECYCLE_SERVICE_CONFLICT");
754
+ await unlink2(paths.service);
755
+ await syncDirectory(path2.dirname(paths.service));
756
+ if (process.platform === "linux") manager(["daemon-reload"]);
757
+ for (const name of ["current", "previous"]) if (await versionLink(name)) await unlink2(path2.join(paths.root, name));
758
+ await syncDirectory(paths.root);
759
+ if (purge && !await absent2(paths.state)) {
760
+ const stat2 = await lstat2(paths.state);
761
+ if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid()) fail2("LIFECYCLE_PATH_INVALID");
762
+ if (process.platform === "linux") {
763
+ try {
764
+ const reference = JSON.parse((await privateRead(path2.join(paths.state, "owner-key.json"), 512)).toString());
765
+ if (reference?.provider === "secret-service" && typeof reference.id === "string" && /^[0-9a-f-]{36}$/.test(reference.id))
766
+ spawnSync("/usr/bin/secret-tool", ["clear", "service", "app.localcmd.agent.state.v1", "id", reference.id], { env, timeout: 15e3, shell: false });
767
+ } catch (e) {
768
+ if (e.code !== "ENOENT") throw e;
769
+ }
770
+ }
771
+ await rm(paths.state, { recursive: true, force: true });
772
+ await syncDirectory(paths.root);
773
+ }
774
+ }
775
+ process.stdout.write(JSON.stringify({ action: command, success: true, ...purge ? { stateRemoved: true } : {}, stateDirectory: paths.state, dataDirectory: paths.data }) + "\n");
776
+ } finally {
777
+ await guard.close();
778
+ }
779
+ }
780
+ if (process.argv[1] && await realpath2(process.argv[1]).catch(() => null) === fileURLToPath(import.meta.url)) {
781
+ void main().catch((error) => {
782
+ process.stderr.write(error instanceof Error && /^[A-Z0-9_]{1,64}$/.test(error.message) ? error.message + "\n" : "LIFECYCLE_FAILED\n");
783
+ process.exitCode = 1;
784
+ });
785
+ }
786
+ export {
787
+ privateInstallPaths
788
+ };