@lawaty/lacode 0.9.0 → 0.11.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 +242 -2
- package/README.md +25 -16
- package/dist/commands.js +14 -3
- package/dist/index.js +963 -379
- package/dist/lacode.js +288 -65
- package/dist/pool.js +41 -6
- package/dist/tunnel.js +248 -27
- package/docs/CONFIGURATION.md +21 -7
- package/docs/OMNIROUTE.md +1 -1
- package/docs/TUNNEL.md +86 -16
- package/docs/USER-STORIES.md +94 -9
- package/package.json +1 -1
- package/presets/README.md +4 -3
- package/presets/free-tier.json +3 -3
package/dist/lacode.js
CHANGED
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/lacode
|
|
4
4
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
|
|
6
6
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
// src/tunnel/index.ts
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
10
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
11
|
+
import { homedir as homedir3 } from "node:os";
|
|
12
|
+
import { join as join4 } from "node:path";
|
|
13
|
+
|
|
14
|
+
// src/lib/notify.ts
|
|
15
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeSync } from "node:fs";
|
|
16
|
+
import { homedir as homedir2 } from "node:os";
|
|
17
|
+
import { join as join3 } from "node:path";
|
|
18
|
+
|
|
19
|
+
// src/lib/pool.ts
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
21
|
import { homedir } from "node:os";
|
|
12
22
|
import { join as join2 } from "node:path";
|
|
13
23
|
|
|
@@ -31,6 +41,36 @@ var findPackageRoot = (start) => {
|
|
|
31
41
|
};
|
|
32
42
|
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
33
43
|
|
|
44
|
+
// src/lib/pool.ts
|
|
45
|
+
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
46
|
+
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
47
|
+
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
48
|
+
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
49
|
+
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
50
|
+
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
51
|
+
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
52
|
+
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
53
|
+
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
54
|
+
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
55
|
+
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
56
|
+
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
57
|
+
var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
58
|
+
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
59
|
+
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
60
|
+
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
61
|
+
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
62
|
+
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
63
|
+
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
64
|
+
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
65
|
+
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
66
|
+
|
|
67
|
+
// src/lib/notify.ts
|
|
68
|
+
var NOTIFY_SESSION_ENV = "LACODE_NOTIFY_SESSION";
|
|
69
|
+
var NOTIFY_CLOSE_ENV = "LACODE_NOTIFY_CLOSE";
|
|
70
|
+
var notificationStateDir = () => process.env.LACODE_NOTIFY_DIR ?? join3(homedir2(), ".local", "share", "opencode", "lacode", "notify");
|
|
71
|
+
var safeCorrelationKey = (correlationKey) => correlationKey.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
72
|
+
var notificationIdFile = (correlationKey) => join3(notificationStateDir(), `${safeCorrelationKey(correlationKey)}.ids`);
|
|
73
|
+
|
|
34
74
|
// src/tunnel/index.ts
|
|
35
75
|
var ICONS = [
|
|
36
76
|
"utilities-terminal",
|
|
@@ -53,7 +93,7 @@ function resolveTunnel(env = process.env) {
|
|
|
53
93
|
user: env.OC_LOCAL_USER ?? "",
|
|
54
94
|
host: env.OC_LOCAL_HOST ?? "",
|
|
55
95
|
port: Number.isFinite(port) && port > 0 ? port : 10022,
|
|
56
|
-
key: env.OC_NOTIFY_KEY ??
|
|
96
|
+
key: env.OC_NOTIFY_KEY ?? join4(homedir3(), ".ssh", "lacode_notify"),
|
|
57
97
|
connectTimeoutMs: Number(env.OC_CONNECT_TIMEOUT_MS) || 5e3
|
|
58
98
|
};
|
|
59
99
|
}
|
|
@@ -76,15 +116,37 @@ var safeText = (raw, fallback, max) => {
|
|
|
76
116
|
};
|
|
77
117
|
var safeTitle = (raw) => safeText(raw ?? "", "LaCode", 120);
|
|
78
118
|
var safeMessage = (raw) => safeText(raw ?? "", "", 400);
|
|
119
|
+
var safeRequestId = (raw) => {
|
|
120
|
+
const clean = (raw ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
121
|
+
if (!clean) return void 0;
|
|
122
|
+
return clean.length > 128 ? clean.slice(0, 128) : clean;
|
|
123
|
+
};
|
|
124
|
+
var CLOSE_REQUEST_PREFIX = "close:";
|
|
125
|
+
var wireCorrelationKey = (raw) => safeText(raw ?? "", "", 200);
|
|
126
|
+
var presentCorrelationKey = (raw) => {
|
|
127
|
+
const clean = wireCorrelationKey(raw);
|
|
128
|
+
return clean ? clean : void 0;
|
|
129
|
+
};
|
|
130
|
+
var isCloseRequest = (correlationKey) => typeof correlationKey === "string" && correlationKey.startsWith(CLOSE_REQUEST_PREFIX);
|
|
131
|
+
var closeRequestTarget = (correlationKey) => {
|
|
132
|
+
if (!isCloseRequest(correlationKey)) return void 0;
|
|
133
|
+
const target = correlationKey?.slice(CLOSE_REQUEST_PREFIX.length).trim() ?? "";
|
|
134
|
+
return target ? target : void 0;
|
|
135
|
+
};
|
|
79
136
|
var FIELD_SEPARATOR = "\n";
|
|
80
137
|
function parsePayload(raw) {
|
|
81
|
-
const [title, message, urgency, icon, sound] = raw.split(FIELD_SEPARATOR);
|
|
138
|
+
const [title, message, urgency, icon, sound, requestID, correlationKey] = raw.split(FIELD_SEPARATOR);
|
|
139
|
+
const key = wireCorrelationKey(correlationKey);
|
|
82
140
|
return {
|
|
83
141
|
title: safeTitle(title),
|
|
84
142
|
message: safeMessage(message),
|
|
85
143
|
urgency: normalizeUrgency(urgency),
|
|
86
144
|
icon: normalizeIcon(icon),
|
|
87
|
-
sound: normalizeSound(sound)
|
|
145
|
+
sound: normalizeSound(sound),
|
|
146
|
+
requestID: safeRequestId(requestID),
|
|
147
|
+
// The seventh field, empty when there is none. A close request keeps its
|
|
148
|
+
// marker here, so the receiver can still tell it apart from a session.
|
|
149
|
+
...key ? { correlationKey: key } : {}
|
|
88
150
|
};
|
|
89
151
|
}
|
|
90
152
|
var encodePayload = (request) => [
|
|
@@ -92,7 +154,14 @@ var encodePayload = (request) => [
|
|
|
92
154
|
safeMessage(request.message),
|
|
93
155
|
normalizeUrgency(request.urgency),
|
|
94
156
|
normalizeIcon(request.icon),
|
|
95
|
-
normalizeSound(request.sound)
|
|
157
|
+
normalizeSound(request.sound),
|
|
158
|
+
// The sixth field, empty when there is no request id: an absent id and an
|
|
159
|
+
// empty one mean the same thing, and the field count never changes.
|
|
160
|
+
safeRequestId(request.requestID) ?? "",
|
|
161
|
+
// The seventh field, the correlation key, empty when there is none. The
|
|
162
|
+
// `close:` marker of a close request is part of the key and travels with
|
|
163
|
+
// it, so the far end closes instead of raising.
|
|
164
|
+
wireCorrelationKey(request.correlationKey)
|
|
96
165
|
].join(FIELD_SEPARATOR);
|
|
97
166
|
var SSH_OPTS = [
|
|
98
167
|
"-o",
|
|
@@ -112,11 +181,16 @@ var CONTROL_OPTS = [
|
|
|
112
181
|
"-o",
|
|
113
182
|
"ControlPersist=300",
|
|
114
183
|
"-o",
|
|
115
|
-
`ControlPath=${
|
|
184
|
+
`ControlPath=${join4(homedir3(), ".ssh", "lacode-cm-%C")}`
|
|
116
185
|
];
|
|
117
|
-
var run = (command2, args, timeout) => {
|
|
186
|
+
var run = (command2, args, timeout, extraEnv) => {
|
|
118
187
|
try {
|
|
119
|
-
const result = spawnSync(command2, args, {
|
|
188
|
+
const result = spawnSync(command2, args, {
|
|
189
|
+
timeout,
|
|
190
|
+
encoding: "utf8",
|
|
191
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
192
|
+
...extraEnv ? { env: { ...process.env, ...extraEnv } } : {}
|
|
193
|
+
});
|
|
120
194
|
return {
|
|
121
195
|
ok: result.status === 0,
|
|
122
196
|
stdout: result.stdout ?? "",
|
|
@@ -175,14 +249,105 @@ var NO_CHANNEL = {
|
|
|
175
249
|
linux: "notify-send is not installed",
|
|
176
250
|
darwin: "neither osascript nor terminal-notifier is installed"
|
|
177
251
|
};
|
|
252
|
+
var NEVER_EXPIRE_FLAG = "--expire-time=0";
|
|
253
|
+
var APP_NAME_FLAG = "--app-name=LaCode";
|
|
254
|
+
var NOTIFICATION_ACTIONS = [
|
|
255
|
+
["once", "Allow once"],
|
|
256
|
+
["always", "Always allow"],
|
|
257
|
+
["reject", "Deny"]
|
|
258
|
+
];
|
|
259
|
+
var ACTION_WAIT_MS = 10 * 6e4;
|
|
260
|
+
var isNotificationId = (id) => /^\d+$/.test(id);
|
|
261
|
+
var recordNotificationId = (correlationKey, id) => {
|
|
262
|
+
if (!isNotificationId(id)) return false;
|
|
263
|
+
try {
|
|
264
|
+
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
265
|
+
appendFileSync(notificationIdFile(correlationKey), `${id}
|
|
266
|
+
`, "utf8");
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var sessionBusEnv = () => {
|
|
273
|
+
if (process.env.DBUS_SESSION_BUS_ADDRESS) return {};
|
|
274
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
275
|
+
return uid === void 0 ? {} : { DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus` };
|
|
276
|
+
};
|
|
277
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
278
|
+
function closeNotification(id) {
|
|
279
|
+
if (!isNotificationId(id)) return false;
|
|
280
|
+
const env = sessionBusEnv();
|
|
281
|
+
const viaGdbus = run(
|
|
282
|
+
"gdbus",
|
|
283
|
+
[
|
|
284
|
+
"call",
|
|
285
|
+
"--session",
|
|
286
|
+
"--dest",
|
|
287
|
+
"org.freedesktop.Notifications",
|
|
288
|
+
"--object-path",
|
|
289
|
+
"/org/freedesktop/Notifications",
|
|
290
|
+
"--method",
|
|
291
|
+
"org.freedesktop.Notifications.CloseNotification",
|
|
292
|
+
id
|
|
293
|
+
],
|
|
294
|
+
CLOSE_TIMEOUT_MS,
|
|
295
|
+
env
|
|
296
|
+
);
|
|
297
|
+
if (viaGdbus.ok) return true;
|
|
298
|
+
const viaDbusSend = run(
|
|
299
|
+
"dbus-send",
|
|
300
|
+
[
|
|
301
|
+
"--session",
|
|
302
|
+
"--dest=org.freedesktop.Notifications",
|
|
303
|
+
"--type=method_call",
|
|
304
|
+
"/org/freedesktop/Notifications",
|
|
305
|
+
"org.freedesktop.Notifications.CloseNotification",
|
|
306
|
+
`uint32:${id}`
|
|
307
|
+
],
|
|
308
|
+
CLOSE_TIMEOUT_MS,
|
|
309
|
+
env
|
|
310
|
+
);
|
|
311
|
+
return viaDbusSend.ok;
|
|
312
|
+
}
|
|
313
|
+
function closeCorrelationKey(correlationKey) {
|
|
314
|
+
const file = notificationIdFile(correlationKey);
|
|
315
|
+
let content = "";
|
|
316
|
+
try {
|
|
317
|
+
content = readFileSync4(file, "utf8");
|
|
318
|
+
} catch {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
let closed = true;
|
|
322
|
+
for (const line of content.split("\n")) {
|
|
323
|
+
const id = line.trim();
|
|
324
|
+
if (!isNotificationId(id)) continue;
|
|
325
|
+
if (!closeNotification(id)) closed = false;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
unlinkSync3(file);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
return closed;
|
|
332
|
+
}
|
|
333
|
+
var splitNotifyOutput = (stdout) => {
|
|
334
|
+
const newline = stdout.indexOf("\n");
|
|
335
|
+
if (newline === -1) return { id: stdout.trim(), action: "" };
|
|
336
|
+
return { id: stdout.slice(0, newline).trim(), action: stdout.slice(newline + 1).trim() };
|
|
337
|
+
};
|
|
178
338
|
function sendLocal(request, platform = process.platform) {
|
|
339
|
+
const closing = closeRequestTarget(request.correlationKey);
|
|
340
|
+
if (closing !== void 0) {
|
|
341
|
+
const closed = closeCorrelationKey(closing);
|
|
342
|
+
return { ok: closed, detail: closed ? void 0 : "a popup could not be closed", transport: "local" };
|
|
343
|
+
}
|
|
179
344
|
const sound = normalizeSound(request.sound);
|
|
180
345
|
const playSound = () => {
|
|
181
346
|
if (sound === "none") return;
|
|
182
347
|
const channel2 = soundChannel(platform);
|
|
183
348
|
if (channel2 === "none") return;
|
|
184
|
-
const path =
|
|
185
|
-
if (!
|
|
349
|
+
const path = join4(PACKAGE_ROOT, "sounds", `${sound}.wav`);
|
|
350
|
+
if (!existsSync2(path)) return;
|
|
186
351
|
const child = spawn(channel2, [path], { detached: true, stdio: "ignore" });
|
|
187
352
|
child.on("error", () => {
|
|
188
353
|
});
|
|
@@ -195,35 +360,80 @@ function sendLocal(request, platform = process.platform) {
|
|
|
195
360
|
}
|
|
196
361
|
const title = safeTitle(request.title);
|
|
197
362
|
const body = safeMessage(request.message);
|
|
363
|
+
const requestID = safeRequestId(request.requestID);
|
|
364
|
+
const actionable = channel === "notify-send" && requestID !== void 0;
|
|
365
|
+
const actionArgs = actionable ? NOTIFICATION_ACTIONS.map(([name, label]) => `--action=${name}=${label}`) : [];
|
|
366
|
+
const correlationKey = presentCorrelationKey(request.correlationKey);
|
|
367
|
+
const trackable = channel === "notify-send" && correlationKey !== void 0;
|
|
198
368
|
const args = channel === "osascript" ? ["-e", DARWIN_SCRIPT, DARWIN_ARGV_MARKER, title, body] : channel === "terminal-notifier" ? ["-title", title, "-message", body, "-appName", "LaCode", "-priority", normalizeUrgency(request.urgency)] : (
|
|
199
|
-
//
|
|
200
|
-
[title, body, `--urgency=${normalizeUrgency(request.urgency)}`, `--icon=${normalizeIcon(request.icon)}
|
|
369
|
+
// Never auto-dismissed: the notification waits for the user to close it.
|
|
370
|
+
[title, body, APP_NAME_FLAG, `--urgency=${normalizeUrgency(request.urgency)}`, `--icon=${normalizeIcon(request.icon)}`, NEVER_EXPIRE_FLAG, ...trackable ? ["--print-id"] : [], ...actionArgs]
|
|
201
371
|
);
|
|
202
|
-
const result = run(CHANNEL_BINARY[channel], args, 5e3);
|
|
372
|
+
const result = run(CHANNEL_BINARY[channel], args, actionable ? ACTION_WAIT_MS : 5e3);
|
|
203
373
|
playSound();
|
|
204
|
-
|
|
374
|
+
const printed = trackable ? splitNotifyOutput(result.stdout) : { id: "", action: result.stdout.trim() };
|
|
375
|
+
const action = actionable ? printed.action : "";
|
|
376
|
+
if (correlationKey !== void 0 && printed.id) recordNotificationId(correlationKey, printed.id);
|
|
377
|
+
return {
|
|
378
|
+
ok: result.ok,
|
|
379
|
+
detail: result.ok ? void 0 : result.stderr || result.detail,
|
|
380
|
+
transport: "local",
|
|
381
|
+
...action ? { action } : {}
|
|
382
|
+
};
|
|
205
383
|
}
|
|
206
384
|
function sendRemote(request, tunnel) {
|
|
207
385
|
if (!tunnel.user) return { ok: false, detail: "OC_LOCAL_USER is unset; the desktop user for the tunnel is unknown", transport: "remote" };
|
|
208
386
|
const connect = ["-o", `ConnectTimeout=${Math.max(1, Math.round(tunnel.connectTimeoutMs / 1e3))}`];
|
|
387
|
+
const actionable = safeRequestId(request.requestID) !== void 0;
|
|
388
|
+
const timeoutMs = actionable ? Math.max(tunnel.connectTimeoutMs + 2e3, ACTION_WAIT_MS + 2e3) : tunnel.connectTimeoutMs + 2e3;
|
|
389
|
+
const deliver = (args) => {
|
|
390
|
+
const result = run("ssh", args, timeoutMs);
|
|
391
|
+
const action = actionable ? result.stdout.trim() : "";
|
|
392
|
+
return {
|
|
393
|
+
ok: result.ok,
|
|
394
|
+
detail: result.ok ? void 0 : result.stderr || result.detail,
|
|
395
|
+
transport: "remote",
|
|
396
|
+
...action ? { action } : {}
|
|
397
|
+
};
|
|
398
|
+
};
|
|
209
399
|
if (tunnel.host) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
400
|
+
return deliver([
|
|
401
|
+
...SSH_OPTS,
|
|
402
|
+
...CONTROL_OPTS,
|
|
403
|
+
...connect,
|
|
404
|
+
"-i",
|
|
405
|
+
tunnel.key,
|
|
406
|
+
`${tunnel.user}@${tunnel.host}`,
|
|
407
|
+
"--",
|
|
408
|
+
encodePayload(request)
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
return deliver([
|
|
412
|
+
...SSH_OPTS,
|
|
413
|
+
...connect,
|
|
414
|
+
"-i",
|
|
415
|
+
tunnel.key,
|
|
416
|
+
"-p",
|
|
417
|
+
String(tunnel.port),
|
|
418
|
+
`${tunnel.user}@localhost`,
|
|
419
|
+
"--",
|
|
420
|
+
encodePayload(request)
|
|
421
|
+
]);
|
|
223
422
|
}
|
|
224
423
|
function notify(request, env = process.env, platform = process.platform) {
|
|
225
424
|
try {
|
|
226
|
-
|
|
425
|
+
const closeKey = (env[NOTIFY_CLOSE_ENV] ?? "").trim();
|
|
426
|
+
if (closeKey) {
|
|
427
|
+
if (!isRemote(env)) {
|
|
428
|
+
const closed = closeCorrelationKey(closeKey);
|
|
429
|
+
return { ok: closed, detail: closed ? void 0 : "a popup could not be closed", transport: "local" };
|
|
430
|
+
}
|
|
431
|
+
return sendRemote({ ...request, correlationKey: `${CLOSE_REQUEST_PREFIX}${closeKey}` }, resolveTunnel(env));
|
|
432
|
+
}
|
|
433
|
+
const sessionKey = presentCorrelationKey(env[NOTIFY_SESSION_ENV]);
|
|
434
|
+
const tracked = sessionKey ? { ...request, correlationKey: request.correlationKey ?? sessionKey } : request;
|
|
435
|
+
const result = isRemote(env) ? sendRemote(tracked, resolveTunnel(env)) : sendLocal(tracked, platform);
|
|
436
|
+
return result.action ? { ...result, action: result.action } : result;
|
|
227
437
|
} catch (error) {
|
|
228
438
|
return { ok: false, detail: String(error), transport: "none" };
|
|
229
439
|
}
|
|
@@ -256,18 +466,18 @@ function describe(env = process.env, platform = process.platform) {
|
|
|
256
466
|
|
|
257
467
|
// src/lib/selfupdate.ts
|
|
258
468
|
import { spawn as spawn2 } from "node:child_process";
|
|
259
|
-
import { closeSync, mkdirSync, openSync, readFileSync as
|
|
260
|
-
import { homedir as
|
|
261
|
-
import { dirname as dirname2, join as
|
|
469
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync2, writeSync as writeSync2 } from "node:fs";
|
|
470
|
+
import { homedir as homedir4 } from "node:os";
|
|
471
|
+
import { dirname as dirname2, join as join5, resolve } from "node:path";
|
|
262
472
|
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
263
473
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
264
474
|
var FETCH_TIMEOUT_MS = 4e3;
|
|
265
475
|
var INSTALL_TIMEOUT_MS = 12e4;
|
|
266
476
|
var LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
267
477
|
var CHILD_ENV = "LACODE_SELFUPDATE_CHILD";
|
|
268
|
-
var STATE_DIR =
|
|
269
|
-
var STATE_PATH =
|
|
270
|
-
var LOCK_PATH =
|
|
478
|
+
var STATE_DIR = join5(homedir4(), ".local", "share", "opencode", "lacode");
|
|
479
|
+
var STATE_PATH = join5(STATE_DIR, "selfupdate.json");
|
|
480
|
+
var LOCK_PATH = join5(STATE_DIR, "selfupdate.lock");
|
|
271
481
|
function compareSemver(a, b) {
|
|
272
482
|
const parse = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
273
483
|
const left = parse(a);
|
|
@@ -281,13 +491,13 @@ function compareSemver(a, b) {
|
|
|
281
491
|
}
|
|
282
492
|
function readInstalledVersion(packageRoot, read = (path) => {
|
|
283
493
|
try {
|
|
284
|
-
return
|
|
494
|
+
return readFileSync5(path, "utf8");
|
|
285
495
|
} catch {
|
|
286
496
|
return void 0;
|
|
287
497
|
}
|
|
288
498
|
}) {
|
|
289
499
|
try {
|
|
290
|
-
const raw = read(
|
|
500
|
+
const raw = read(join5(packageRoot, "package.json"));
|
|
291
501
|
if (raw === void 0) return void 0;
|
|
292
502
|
const manifest = JSON.parse(raw);
|
|
293
503
|
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
@@ -298,20 +508,20 @@ function readInstalledVersion(packageRoot, read = (path) => {
|
|
|
298
508
|
}
|
|
299
509
|
function resolveCacheRoot(packageRoot, exists = (p) => {
|
|
300
510
|
try {
|
|
301
|
-
return
|
|
511
|
+
return statSync3(p).isFile();
|
|
302
512
|
} catch {
|
|
303
513
|
return false;
|
|
304
514
|
}
|
|
305
515
|
}) {
|
|
306
|
-
const tail =
|
|
516
|
+
const tail = join5("node_modules", "@lawaty", "lacode");
|
|
307
517
|
if (!packageRoot.endsWith(tail)) return void 0;
|
|
308
518
|
const cacheRoot = resolve(packageRoot, "..", "..", "..");
|
|
309
|
-
if (!exists(
|
|
519
|
+
if (!exists(join5(cacheRoot, "package.json"))) return void 0;
|
|
310
520
|
return cacheRoot;
|
|
311
521
|
}
|
|
312
522
|
function resolveOpencodeBin(execPath = process.execPath, env = process.env, exists = (p) => {
|
|
313
523
|
try {
|
|
314
|
-
return
|
|
524
|
+
return statSync3(p).isFile();
|
|
315
525
|
} catch {
|
|
316
526
|
return false;
|
|
317
527
|
}
|
|
@@ -319,10 +529,10 @@ function resolveOpencodeBin(execPath = process.execPath, env = process.env, exis
|
|
|
319
529
|
if (execPath && /(^|[\\/])opencode(\.exe)?$/.test(execPath)) return execPath;
|
|
320
530
|
for (const dir of (env.PATH ?? "").split(":")) {
|
|
321
531
|
if (!dir) continue;
|
|
322
|
-
const candidate =
|
|
532
|
+
const candidate = join5(dir, "opencode");
|
|
323
533
|
if (exists(candidate)) return candidate;
|
|
324
534
|
}
|
|
325
|
-
const fallback =
|
|
535
|
+
const fallback = join5(homedir4(), ".opencode", "bin", "opencode");
|
|
326
536
|
return exists(fallback) ? fallback : void 0;
|
|
327
537
|
}
|
|
328
538
|
async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
@@ -354,11 +564,11 @@ async function refreshCache(opts) {
|
|
|
354
564
|
const { cacheRoot, bin, latest } = opts;
|
|
355
565
|
const spawnImpl = opts.spawnImpl ?? spawn2;
|
|
356
566
|
const timeoutMs = opts.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
357
|
-
const fresh =
|
|
567
|
+
const fresh = join5(cacheRoot, "node_modules", "@lawaty", "lacode");
|
|
358
568
|
const aside = `${cacheRoot}.stale-${Date.now()}`;
|
|
359
569
|
try {
|
|
360
570
|
rmSync(aside, { recursive: true, force: true });
|
|
361
|
-
|
|
571
|
+
renameSync2(cacheRoot, aside);
|
|
362
572
|
} catch (error) {
|
|
363
573
|
return { ok: false, detail: `could not set the old cache aside: ${String(error)}` };
|
|
364
574
|
}
|
|
@@ -393,7 +603,7 @@ async function refreshCache(opts) {
|
|
|
393
603
|
}
|
|
394
604
|
} else {
|
|
395
605
|
try {
|
|
396
|
-
|
|
606
|
+
renameSync2(aside, cacheRoot);
|
|
397
607
|
} catch {
|
|
398
608
|
}
|
|
399
609
|
}
|
|
@@ -405,9 +615,9 @@ async function refreshCache(opts) {
|
|
|
405
615
|
|
|
406
616
|
// src/lib/version.ts
|
|
407
617
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
408
|
-
import { existsSync as
|
|
409
|
-
import { homedir as
|
|
410
|
-
import { isAbsolute, join as
|
|
618
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
619
|
+
import { homedir as homedir5 } from "node:os";
|
|
620
|
+
import { isAbsolute, join as join6, relative, sep } from "node:path";
|
|
411
621
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
412
622
|
|
|
413
623
|
// src/lib/jsonc.ts
|
|
@@ -460,15 +670,15 @@ var parseJsonc = (raw) => {
|
|
|
460
670
|
// src/lib/version.ts
|
|
461
671
|
var nodeStat = (path) => {
|
|
462
672
|
try {
|
|
463
|
-
return
|
|
673
|
+
return statSync4(path).mtimeMs;
|
|
464
674
|
} catch {
|
|
465
675
|
return void 0;
|
|
466
676
|
}
|
|
467
677
|
};
|
|
468
|
-
var nodeExists = (path) =>
|
|
678
|
+
var nodeExists = (path) => existsSync3(path);
|
|
469
679
|
var nodeRead = (path) => {
|
|
470
680
|
try {
|
|
471
|
-
return
|
|
681
|
+
return readFileSync6(path, "utf8");
|
|
472
682
|
} catch {
|
|
473
683
|
return void 0;
|
|
474
684
|
}
|
|
@@ -484,17 +694,17 @@ var realGit = (root) => (args) => {
|
|
|
484
694
|
};
|
|
485
695
|
var walkSources = (root) => {
|
|
486
696
|
const found = [];
|
|
487
|
-
const stack = [
|
|
697
|
+
const stack = [join6(root, "src")];
|
|
488
698
|
while (stack.length > 0) {
|
|
489
699
|
const dir = stack.pop();
|
|
490
700
|
let names;
|
|
491
701
|
try {
|
|
492
|
-
names =
|
|
702
|
+
names = readdirSync2(dir);
|
|
493
703
|
} catch {
|
|
494
704
|
continue;
|
|
495
705
|
}
|
|
496
706
|
for (const name of names) {
|
|
497
|
-
const full =
|
|
707
|
+
const full = join6(dir, name);
|
|
498
708
|
if (name.endsWith(".ts")) found.push(full);
|
|
499
709
|
else if (name === "plugins" || name === "lib" || name === "tunnel") stack.push(full);
|
|
500
710
|
}
|
|
@@ -502,8 +712,8 @@ var walkSources = (root) => {
|
|
|
502
712
|
return found;
|
|
503
713
|
};
|
|
504
714
|
var checkDist = (root, deps) => {
|
|
505
|
-
if (!deps.exists(
|
|
506
|
-
const built = deps.stat(
|
|
715
|
+
if (!deps.exists(join6(root, "dist"))) return { dist: "unknown" };
|
|
716
|
+
const built = deps.stat(join6(root, "dist", "index.js"));
|
|
507
717
|
if (built === void 0) return { dist: "unknown" };
|
|
508
718
|
const sources = deps.listSources(root).map((file) => ({ file, mtime: deps.stat(file) }));
|
|
509
719
|
const newer = sources.filter((s) => s.mtime !== void 0 && s.mtime > built).map((s) => relative(root, s.file));
|
|
@@ -544,13 +754,13 @@ var resolvesHere = (specifier, packageRoot) => {
|
|
|
544
754
|
};
|
|
545
755
|
function readVersionReport(deps = {}) {
|
|
546
756
|
const packageRoot = deps.packageRoot ?? PACKAGE_ROOT;
|
|
547
|
-
const home = deps.home ??
|
|
757
|
+
const home = deps.home ?? homedir5();
|
|
548
758
|
const git = deps.git ?? realGit(packageRoot);
|
|
549
759
|
const readFile = deps.readFile ?? nodeRead;
|
|
550
760
|
const stat = deps.stat ?? nodeStat;
|
|
551
761
|
const exists = deps.exists ?? nodeExists;
|
|
552
762
|
const listSources = deps.listSources ?? walkSources;
|
|
553
|
-
const isCheckout = exists(
|
|
763
|
+
const isCheckout = exists(join6(packageRoot, ".git"));
|
|
554
764
|
const report = {
|
|
555
765
|
version: readInstalledVersion(packageRoot, readFile),
|
|
556
766
|
root: packageRoot,
|
|
@@ -563,11 +773,11 @@ function readVersionReport(deps = {}) {
|
|
|
563
773
|
const dist = checkDist(packageRoot, { stat, exists, listSources });
|
|
564
774
|
report.dist = dist.dist;
|
|
565
775
|
if (dist.stale) report.staleSources = dist.stale;
|
|
566
|
-
const configPath = [
|
|
776
|
+
const configPath = [join6(home, ".config", "opencode", "opencode.jsonc"), join6(home, ".config", "opencode", "opencode.json")].find(
|
|
567
777
|
(candidate) => exists(candidate)
|
|
568
778
|
);
|
|
569
779
|
if (!configPath) {
|
|
570
|
-
report.note = `No opencode config found under ${
|
|
780
|
+
report.note = `No opencode config found under ${join6(home, ".config", "opencode")}; nothing to compare this copy against.`;
|
|
571
781
|
return report;
|
|
572
782
|
}
|
|
573
783
|
report.configPath = configPath;
|
|
@@ -639,6 +849,8 @@ var USAGE = `lacode \u2014 portable notifications and remote exec for LaCode
|
|
|
639
849
|
permission | error; an omitted or unrecognised SOUND is none, so a
|
|
640
850
|
four-argument caller simply gets no sound. Fire-and-forget: always
|
|
641
851
|
exits 0.
|
|
852
|
+
With LACODE_REQUEST_ID set, the notification carries buttons and the
|
|
853
|
+
name of the clicked one is printed on stdout.
|
|
642
854
|
|
|
643
855
|
lacode forward HOST -- COMMAND [ARG...]
|
|
644
856
|
Run COMMAND on HOST over ssh. Exits with the remote's status.
|
|
@@ -666,7 +878,9 @@ Environment:
|
|
|
666
878
|
OC_LOCAL_USER desktop user the tunnel authenticates as
|
|
667
879
|
OC_TUNNEL_PORT reverse-forward listener port (default 10022)
|
|
668
880
|
OC_NOTIFY_KEY dedicated key (default ~/.ssh/lacode_notify)
|
|
669
|
-
OC_CONNECT_TIMEOUT_MS / OC_SSH_TIMEOUT_MS
|
|
881
|
+
OC_CONNECT_TIMEOUT_MS / OC_SSH_TIMEOUT_MS
|
|
882
|
+
LACODE_REQUEST_ID makes a notification actionable: it carries buttons, and
|
|
883
|
+
the clicked one is printed on stdout`;
|
|
670
884
|
var argv = process.argv.slice(2);
|
|
671
885
|
var command = argv.shift();
|
|
672
886
|
switch (command) {
|
|
@@ -684,8 +898,16 @@ switch (command) {
|
|
|
684
898
|
console.error(USAGE);
|
|
685
899
|
process.exit(2);
|
|
686
900
|
}
|
|
687
|
-
const result = notify({
|
|
901
|
+
const result = notify({
|
|
902
|
+
title: rawTitle,
|
|
903
|
+
message,
|
|
904
|
+
urgency,
|
|
905
|
+
icon,
|
|
906
|
+
sound: normalizeSound(sound),
|
|
907
|
+
requestID: process.env.LACODE_REQUEST_ID
|
|
908
|
+
});
|
|
688
909
|
if (!result.ok) console.error(`lacode: notification not delivered via ${result.transport}: ${result.detail ?? "unknown reason"}`);
|
|
910
|
+
if (result.action) console.log(result.action);
|
|
689
911
|
process.exit(0);
|
|
690
912
|
break;
|
|
691
913
|
}
|
|
@@ -765,6 +987,7 @@ ${report.note}`);
|
|
|
765
987
|
const request = parsePayload(process.env.SSH_ORIGINAL_COMMAND ?? "");
|
|
766
988
|
const result = sendLocal(request);
|
|
767
989
|
if (!result.ok) console.error(`lacode receive: ${result.detail ?? "not delivered"}`);
|
|
990
|
+
if (result.action) console.log(result.action);
|
|
768
991
|
process.exit(result.ok ? 0 : 1);
|
|
769
992
|
break;
|
|
770
993
|
}
|
|
@@ -776,7 +999,7 @@ ${report.note}`);
|
|
|
776
999
|
}
|
|
777
1000
|
const { user, host, key } = resolveTunnel();
|
|
778
1001
|
const pub = `${key}.pub`;
|
|
779
|
-
if (!
|
|
1002
|
+
if (!existsSync4(key)) {
|
|
780
1003
|
const gen = spawnSync3("ssh-keygen", ["-t", "ed25519", "-N", "", "-C", "lacode-notify", "-f", key], { stdio: ["ignore", "ignore", "inherit"] });
|
|
781
1004
|
if (gen.status !== 0) {
|
|
782
1005
|
console.error("lacode tunnel setup: ssh-keygen failed; create the key by hand and re-run.");
|
|
@@ -784,7 +1007,7 @@ ${report.note}`);
|
|
|
784
1007
|
}
|
|
785
1008
|
console.log(`Generated ${key}`);
|
|
786
1009
|
}
|
|
787
|
-
const pubkey =
|
|
1010
|
+
const pubkey = existsSync4(pub) ? readFileSync7(pub, "utf8").trim() : "(public key missing)";
|
|
788
1011
|
const self = process.argv[1] ?? "lacode";
|
|
789
1012
|
console.log("\nOn the REMOTE host (where opencode runs), export:");
|
|
790
1013
|
console.log(` OC_LOCAL_USER=${user || "<desktop-user>"}`);
|