hacklab 26.921.4 → 26.922.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +355 -26
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
// package.json
|
|
6
|
-
var version = "26.
|
|
6
|
+
var version = "26.922.7";
|
|
7
7
|
|
|
8
8
|
// src/auth.ts
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
@@ -123,19 +123,71 @@ Waiting for you to connect your account…`);
|
|
|
123
123
|
throw new Error("CLI sign-in expired. Run the command again to retry.");
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// src/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
126
|
+
// src/daemon.ts
|
|
127
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
128
|
+
import { once as once2 } from "node:events";
|
|
129
|
+
import { realpathSync } from "node:fs";
|
|
130
|
+
import { mkdir as mkdir3, readFile as readFile3, rm as rm3, stat, writeFile as writeFile3 } from "node:fs/promises";
|
|
131
|
+
import { homedir as homedir3 } from "node:os";
|
|
132
|
+
import { join as join3 } from "node:path";
|
|
133
|
+
import { text as text2 } from "node:stream/consumers";
|
|
130
134
|
|
|
131
135
|
// src/sync.ts
|
|
132
136
|
import { spawn as spawn2 } from "node:child_process";
|
|
137
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
133
138
|
import { once } from "node:events";
|
|
134
139
|
import { createRequire } from "node:module";
|
|
135
140
|
import { text } from "node:stream/consumers";
|
|
136
|
-
|
|
141
|
+
|
|
142
|
+
// src/origin.ts
|
|
143
|
+
function cliOrigin() {
|
|
144
|
+
return "https://beta.hacklab.so";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/state.ts
|
|
148
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
149
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
150
|
+
import { homedir as homedir2 } from "node:os";
|
|
151
|
+
import { join as join2 } from "node:path";
|
|
152
|
+
var stateDirectory = join2(homedir2(), ".config", "hacklab", "state");
|
|
153
|
+
function statePath(origin, directory) {
|
|
154
|
+
return join2(directory, `${createHash2("sha256").update(origin).digest("hex")}.json`);
|
|
155
|
+
}
|
|
156
|
+
async function readState(origin, directory = stateDirectory) {
|
|
157
|
+
try {
|
|
158
|
+
const value = JSON.parse(await readFile2(statePath(origin, directory), "utf8"));
|
|
159
|
+
return value && typeof value === "object" ? value : {};
|
|
160
|
+
} catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeState(origin, state, directory = stateDirectory) {
|
|
165
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
166
|
+
const path = statePath(origin, directory);
|
|
167
|
+
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
168
|
+
try {
|
|
169
|
+
await writeFile2(temporary, JSON.stringify(state), { mode: 384, flag: "wx" });
|
|
170
|
+
await rename2(temporary, path);
|
|
171
|
+
} finally {
|
|
172
|
+
await rm2(temporary, { force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/sync.ts
|
|
177
|
+
var dayMs = 24 * 60 * 60 * 1000;
|
|
178
|
+
var sinceWindowDays = 2;
|
|
179
|
+
var defaultIntervalSeconds = 15 * 60;
|
|
180
|
+
function sinceDate(from) {
|
|
181
|
+
const date = new Date(from - sinceWindowDays * dayMs);
|
|
182
|
+
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
|
183
|
+
return local.toISOString().slice(0, 10);
|
|
184
|
+
}
|
|
185
|
+
async function collectUsage(since) {
|
|
137
186
|
const require2 = createRequire(import.meta.url);
|
|
138
|
-
const
|
|
187
|
+
const args = [require2.resolve("ccusage/src/cli.js"), "session", "--json"];
|
|
188
|
+
if (since)
|
|
189
|
+
args.push("--since", since);
|
|
190
|
+
const child = spawn2(process.execPath, args, { stdio: ["ignore", "pipe", "inherit"] });
|
|
139
191
|
const [body, [exitCode]] = await Promise.all([text(child.stdout), once(child, "close")]);
|
|
140
192
|
if (exitCode !== 0)
|
|
141
193
|
throw new Error(`ccusage failed (exit ${exitCode}). Nothing synced.`);
|
|
@@ -152,7 +204,24 @@ async function collectUsage() {
|
|
|
152
204
|
throw new Error("Usage report exceeds 20 MB. Nothing synced.");
|
|
153
205
|
return body;
|
|
154
206
|
}
|
|
155
|
-
|
|
207
|
+
var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
208
|
+
function describeIssues(result) {
|
|
209
|
+
const parts = [
|
|
210
|
+
result.error,
|
|
211
|
+
...(result.issues ?? []).map((i) => `session ${i.index} — ${i.reason}`)
|
|
212
|
+
];
|
|
213
|
+
const detail = parts.filter(Boolean).join(" ");
|
|
214
|
+
return detail && !detail.endsWith(".") ? `${detail}.` : detail;
|
|
215
|
+
}
|
|
216
|
+
async function readResult(response) {
|
|
217
|
+
try {
|
|
218
|
+
const result = await response.json();
|
|
219
|
+
return result && typeof result === "object" ? result : {};
|
|
220
|
+
} catch {
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function uploadUsage(origin, token, body, quiet = false) {
|
|
156
225
|
const response = await fetch(`${origin}/api/token-usage`, {
|
|
157
226
|
method: "POST",
|
|
158
227
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
@@ -160,60 +229,320 @@ async function uploadUsage(origin, token, body) {
|
|
|
160
229
|
redirect: "error",
|
|
161
230
|
signal: AbortSignal.timeout(60000)
|
|
162
231
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
const result = await readResult(response);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const detail = describeIssues(result);
|
|
235
|
+
throw new Error(`Hacklab rejected the usage report (HTTP ${response.status}).${detail ? ` ${detail}` : ""} Run hacklab sync to retry.`);
|
|
236
|
+
}
|
|
166
237
|
if (result.ok !== true)
|
|
167
238
|
throw new Error("Hacklab did not confirm the sync. Run hacklab sync to retry.");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
239
|
+
if (!quiet) {
|
|
240
|
+
console.log(`Synced ${plural(result.sessions ?? 0, "usage session")} to Hacklab.`);
|
|
241
|
+
if (result.duplicates)
|
|
242
|
+
console.log(`Merged ${plural(result.duplicates, "repeated session")}.`);
|
|
243
|
+
if (result.skipped)
|
|
244
|
+
console.log(`Skipped ${plural(result.skipped, "unreadable session")}. ${describeIssues(result)}`.trim());
|
|
245
|
+
if (result.sessions === 0)
|
|
246
|
+
console.log("No usage found. Use your coding agent, then run hacklab sync again.");
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
var fingerprint = (body) => createHash3("sha256").update(body).digest("hex");
|
|
251
|
+
function nextInterval(result, current) {
|
|
252
|
+
const proposed = result.nextSyncSeconds;
|
|
253
|
+
return typeof proposed === "number" && Number.isFinite(proposed) && proposed >= 60 ? Math.min(Math.trunc(proposed), 24 * 60 * 60) : current;
|
|
171
254
|
}
|
|
172
255
|
async function sync() {
|
|
173
256
|
const origin = cliOrigin();
|
|
174
257
|
const token = await authenticate(origin);
|
|
175
258
|
console.log("Collecting ccusage session data…");
|
|
176
259
|
const body = await collectUsage();
|
|
177
|
-
await uploadUsage(origin, token, body);
|
|
260
|
+
const result = await uploadUsage(origin, token, body);
|
|
261
|
+
const state = await readState(origin);
|
|
262
|
+
const now = new Date().toISOString();
|
|
263
|
+
await writeState(origin, {
|
|
264
|
+
...state,
|
|
265
|
+
fingerprint: fingerprint(body),
|
|
266
|
+
fingerprintSince: "",
|
|
267
|
+
lastSyncedAt: now,
|
|
268
|
+
lastFullSyncAt: now,
|
|
269
|
+
intervalSeconds: nextInterval(result, state.intervalSeconds)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function backgroundSync() {
|
|
273
|
+
const origin = cliOrigin();
|
|
274
|
+
const token = await readCredential(origin);
|
|
275
|
+
if (!token)
|
|
276
|
+
throw new Error("No saved Hacklab authorization. Run hacklab login to reconnect this machine.");
|
|
277
|
+
const state = await readState(origin);
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
const lastSyncedAt = state.lastSyncedAt ? Date.parse(state.lastSyncedAt) : Number.NaN;
|
|
280
|
+
const interval = (state.intervalSeconds ?? defaultIntervalSeconds) * 1000;
|
|
281
|
+
if (Number.isFinite(lastSyncedAt) && now - lastSyncedAt < interval)
|
|
282
|
+
return "too-soon";
|
|
283
|
+
const lastFullSyncAt = state.lastFullSyncAt ? Date.parse(state.lastFullSyncAt) : Number.NaN;
|
|
284
|
+
const full = !Number.isFinite(lastFullSyncAt) || now - lastFullSyncAt >= dayMs;
|
|
285
|
+
const since = full ? "" : sinceDate(Number.isFinite(lastSyncedAt) ? lastSyncedAt : now);
|
|
286
|
+
const body = await collectUsage(since || undefined);
|
|
287
|
+
const current = fingerprint(body);
|
|
288
|
+
if (!full && since === state.fingerprintSince && current === state.fingerprint) {
|
|
289
|
+
await writeState(origin, { ...state, lastSyncedAt: new Date(now).toISOString() });
|
|
290
|
+
return "unchanged";
|
|
291
|
+
}
|
|
292
|
+
const result = await uploadUsage(origin, token, body, true);
|
|
293
|
+
const timestamp = new Date().toISOString();
|
|
294
|
+
await writeState(origin, {
|
|
295
|
+
...state,
|
|
296
|
+
fingerprint: current,
|
|
297
|
+
fingerprintSince: since,
|
|
298
|
+
lastSyncedAt: timestamp,
|
|
299
|
+
lastFullSyncAt: full ? timestamp : state.lastFullSyncAt,
|
|
300
|
+
intervalSeconds: nextInterval(result, state.intervalSeconds)
|
|
301
|
+
});
|
|
302
|
+
return "synced";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/daemon.ts
|
|
306
|
+
var label = "so.hacklab.sync";
|
|
307
|
+
var logPath = join3(homedir3(), ".config", "hacklab", "daemon.log");
|
|
308
|
+
var plistPath = join3(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
309
|
+
var systemdDirectory = join3(homedir3(), ".config", "systemd", "user");
|
|
310
|
+
var servicePath = join3(systemdDirectory, "hacklab-sync.service");
|
|
311
|
+
var timerPath = join3(systemdDirectory, "hacklab-sync.timer");
|
|
312
|
+
var maxLogBytes = 1024 * 1024;
|
|
313
|
+
var daemonSupported = process.platform === "darwin" || process.platform === "linux";
|
|
314
|
+
|
|
315
|
+
class DaemonError extends Error {
|
|
316
|
+
}
|
|
317
|
+
async function run(command, args) {
|
|
318
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
319
|
+
const [out, err, [code]] = await Promise.all([
|
|
320
|
+
text2(child.stdout),
|
|
321
|
+
text2(child.stderr),
|
|
322
|
+
once2(child, "close").catch(() => [1])
|
|
323
|
+
]);
|
|
324
|
+
return { code: code ?? 1, output: `${out}${err}`.trim() };
|
|
325
|
+
}
|
|
326
|
+
function command() {
|
|
327
|
+
const entry = process.argv[1];
|
|
328
|
+
if (!entry)
|
|
329
|
+
throw new DaemonError("Could not locate the Hacklab CLI on this machine.");
|
|
330
|
+
return [process.execPath, realpathSync(entry), "sync", "--daemon"];
|
|
331
|
+
}
|
|
332
|
+
var escapeXml = (value) => value.replace(/[<>&'"]/g, (character) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[character]);
|
|
333
|
+
function plist(seconds) {
|
|
334
|
+
const args = command().map((value) => ` <string>${escapeXml(value)}</string>`).join(`
|
|
335
|
+
`);
|
|
336
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
337
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
338
|
+
<plist version="1.0">
|
|
339
|
+
<dict>
|
|
340
|
+
<key>Label</key><string>${escapeXml(label)}</string>
|
|
341
|
+
<key>ProgramArguments</key>
|
|
342
|
+
<array>
|
|
343
|
+
${args}
|
|
344
|
+
</array>
|
|
345
|
+
<key>StartInterval</key><integer>${seconds}</integer>
|
|
346
|
+
<key>RunAtLoad</key><false/>
|
|
347
|
+
<key>ProcessType</key><string>Background</string>
|
|
348
|
+
<key>LowPriorityIO</key><true/>
|
|
349
|
+
<key>Nice</key><integer>5</integer>
|
|
350
|
+
<key>StandardOutPath</key><string>${escapeXml(logPath)}</string>
|
|
351
|
+
<key>StandardErrorPath</key><string>${escapeXml(logPath)}</string>
|
|
352
|
+
</dict>
|
|
353
|
+
</plist>
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
var units = (seconds) => ({
|
|
357
|
+
service: `[Unit]
|
|
358
|
+
Description=Hacklab usage sync
|
|
359
|
+
|
|
360
|
+
[Service]
|
|
361
|
+
Type=oneshot
|
|
362
|
+
Nice=5
|
|
363
|
+
ExecStart=${command().map((value) => JSON.stringify(value)).join(" ")}
|
|
364
|
+
`,
|
|
365
|
+
timer: `[Unit]
|
|
366
|
+
Description=Hacklab usage sync every ${Math.round(seconds / 60)} minutes
|
|
367
|
+
|
|
368
|
+
[Timer]
|
|
369
|
+
OnBootSec=2min
|
|
370
|
+
OnUnitActiveSec=${seconds}s
|
|
371
|
+
AccuracySec=1min
|
|
372
|
+
RandomizedDelaySec=60
|
|
373
|
+
Persistent=true
|
|
374
|
+
|
|
375
|
+
[Install]
|
|
376
|
+
WantedBy=timers.target
|
|
377
|
+
`
|
|
378
|
+
});
|
|
379
|
+
async function exists(path) {
|
|
380
|
+
try {
|
|
381
|
+
await stat(path);
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function installLaunchd(seconds) {
|
|
388
|
+
await mkdir3(join3(homedir3(), "Library", "LaunchAgents"), { recursive: true });
|
|
389
|
+
await mkdir3(join3(homedir3(), ".config", "hacklab"), { recursive: true, mode: 448 });
|
|
390
|
+
await writeFile3(plistPath, plist(seconds), { mode: 420 });
|
|
391
|
+
const target = `gui/${process.getuid?.() ?? ""}`;
|
|
392
|
+
await run("launchctl", ["bootout", `${target}/${label}`]);
|
|
393
|
+
const bootstrap = await run("launchctl", ["bootstrap", target, plistPath]);
|
|
394
|
+
if (bootstrap.code === 0)
|
|
395
|
+
return plistPath;
|
|
396
|
+
const legacy = await run("launchctl", ["load", "-w", plistPath]);
|
|
397
|
+
if (legacy.code === 0)
|
|
398
|
+
return plistPath;
|
|
399
|
+
await rm3(plistPath, { force: true });
|
|
400
|
+
throw new DaemonError(`launchctl refused the Hacklab launch agent.${bootstrap.output ? ` ${bootstrap.output}` : ""}`);
|
|
401
|
+
}
|
|
402
|
+
async function installSystemd(seconds) {
|
|
403
|
+
const probe = await run("systemctl", ["--user", "--version"]);
|
|
404
|
+
if (probe.code !== 0)
|
|
405
|
+
throw new DaemonError("systemd user services are unavailable on this machine.");
|
|
406
|
+
await mkdir3(systemdDirectory, { recursive: true });
|
|
407
|
+
const { service, timer } = units(seconds);
|
|
408
|
+
await writeFile3(servicePath, service, { mode: 420 });
|
|
409
|
+
await writeFile3(timerPath, timer, { mode: 420 });
|
|
410
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
411
|
+
const enable = await run("systemctl", ["--user", "enable", "--now", "hacklab-sync.timer"]);
|
|
412
|
+
if (enable.code !== 0) {
|
|
413
|
+
await rm3(servicePath, { force: true });
|
|
414
|
+
await rm3(timerPath, { force: true });
|
|
415
|
+
throw new DaemonError(`systemd refused the Hacklab timer.${enable.output ? ` ${enable.output}` : ""}`);
|
|
416
|
+
}
|
|
417
|
+
return timerPath;
|
|
418
|
+
}
|
|
419
|
+
async function installDaemon(seconds = defaultIntervalSeconds) {
|
|
420
|
+
if (!daemonSupported)
|
|
421
|
+
throw new DaemonError("Background sync is only available on macOS and Linux.");
|
|
422
|
+
return process.platform === "darwin" ? installLaunchd(seconds) : installSystemd(seconds);
|
|
423
|
+
}
|
|
424
|
+
async function uninstallDaemon() {
|
|
425
|
+
if (process.platform === "darwin") {
|
|
426
|
+
if (!await exists(plistPath))
|
|
427
|
+
return false;
|
|
428
|
+
await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${label}`]);
|
|
429
|
+
await run("launchctl", ["unload", "-w", plistPath]);
|
|
430
|
+
await rm3(plistPath, { force: true });
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (process.platform === "linux") {
|
|
434
|
+
if (!await exists(timerPath))
|
|
435
|
+
return false;
|
|
436
|
+
await run("systemctl", ["--user", "disable", "--now", "hacklab-sync.timer"]);
|
|
437
|
+
await rm3(timerPath, { force: true });
|
|
438
|
+
await rm3(servicePath, { force: true });
|
|
439
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
var daemonInstalled = () => exists(process.platform === "darwin" ? plistPath : timerPath);
|
|
445
|
+
var daemonPath = () => process.platform === "darwin" ? plistPath : timerPath;
|
|
446
|
+
async function trimLog() {
|
|
447
|
+
try {
|
|
448
|
+
const { size } = await stat(logPath);
|
|
449
|
+
if (size <= maxLogBytes)
|
|
450
|
+
return;
|
|
451
|
+
const body = await readFile3(logPath, "utf8");
|
|
452
|
+
await writeFile3(logPath, body.slice(-maxLogBytes / 2), { mode: 384 });
|
|
453
|
+
} catch {}
|
|
178
454
|
}
|
|
179
455
|
|
|
180
456
|
// src/index.ts
|
|
181
457
|
var help = `Hacklab CLI
|
|
182
458
|
|
|
183
459
|
Usage:
|
|
184
|
-
hacklab sync
|
|
185
|
-
hacklab login
|
|
186
|
-
hacklab logout
|
|
187
|
-
hacklab
|
|
188
|
-
hacklab
|
|
460
|
+
hacklab sync Sign in, upload usage, and keep this machine in sync
|
|
461
|
+
hacklab login Sign in without uploading
|
|
462
|
+
hacklab logout Clear saved authorization
|
|
463
|
+
hacklab daemon status Show whether background sync is installed
|
|
464
|
+
hacklab daemon uninstall Stop syncing in the background
|
|
465
|
+
hacklab daemon install Start syncing in the background
|
|
466
|
+
hacklab --help Show help
|
|
467
|
+
hacklab --version Show version
|
|
468
|
+
`;
|
|
469
|
+
var commands = ["sync", "login", "logout", "daemon"];
|
|
470
|
+
var daemonActions = ["install", "uninstall", "status"];
|
|
471
|
+
async function offerDaemon() {
|
|
472
|
+
if (process.env.HACKLAB_NO_DAEMON === "1" || !daemonSupported)
|
|
473
|
+
return;
|
|
474
|
+
if (await daemonInstalled())
|
|
475
|
+
return;
|
|
476
|
+
try {
|
|
477
|
+
await installDaemon();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error(`
|
|
480
|
+
Could not install background sync: ${error instanceof Error ? error.message : "unknown error"}
|
|
481
|
+
Your usage was uploaded. Run hacklab sync again whenever you want to update it.`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function runDaemonCommand(action) {
|
|
485
|
+
if (action === "status") {
|
|
486
|
+
const installed = await daemonInstalled();
|
|
487
|
+
console.log(installed ? `Background sync is installed at ${daemonPath()}.
|
|
488
|
+
Log: ${logPath}` : "Background sync is not installed. Run hacklab daemon install to start it.");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (action === "uninstall") {
|
|
492
|
+
console.log(await uninstallDaemon() ? "Background sync removed. Run hacklab sync to upload manually." : "Background sync was not installed.");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
await installDaemon();
|
|
496
|
+
console.log(`Background sync installed at ${daemonPath()}. Hacklab updates every 15 minutes.`);
|
|
497
|
+
}
|
|
189
498
|
try {
|
|
190
499
|
const { values, positionals } = parseArgs({
|
|
191
500
|
args: process.argv.slice(2),
|
|
192
501
|
options: {
|
|
193
502
|
help: { type: "boolean", short: "h" },
|
|
194
|
-
version: { type: "boolean", short: "v" }
|
|
503
|
+
version: { type: "boolean", short: "v" },
|
|
504
|
+
daemon: { type: "boolean" }
|
|
195
505
|
},
|
|
196
506
|
allowPositionals: true
|
|
197
507
|
});
|
|
198
|
-
|
|
508
|
+
const [command, action] = positionals;
|
|
509
|
+
if (positionals.length > 2 || command && !commands.includes(command)) {
|
|
510
|
+
throw new Error("Unknown command. Run hacklab --help for usage.");
|
|
511
|
+
}
|
|
512
|
+
if (command === "daemon" ? !daemonActions.includes(action ?? "") : positionals.length > 1) {
|
|
199
513
|
throw new Error("Unknown command. Run hacklab --help for usage.");
|
|
200
514
|
}
|
|
201
515
|
if (values.help) {
|
|
202
516
|
console.log(help);
|
|
203
517
|
} else if (values.version) {
|
|
204
518
|
console.log(version);
|
|
205
|
-
} else if (
|
|
519
|
+
} else if (command === "login") {
|
|
206
520
|
await authenticate(cliOrigin());
|
|
207
521
|
console.log("Signed in to Hacklab. Run hacklab sync to upload usage.");
|
|
208
|
-
} else if (
|
|
522
|
+
} else if (command === "logout") {
|
|
523
|
+
await uninstallDaemon();
|
|
209
524
|
await removeCredential(cliOrigin());
|
|
210
525
|
console.log("Saved CLI authorization cleared. Run hacklab sync to sign in again.");
|
|
211
|
-
} else if (
|
|
526
|
+
} else if (command === "daemon") {
|
|
527
|
+
await runDaemonCommand(action);
|
|
528
|
+
} else if (command === "sync" && values.daemon) {
|
|
529
|
+
await trimLog();
|
|
530
|
+
const outcome = await backgroundSync();
|
|
531
|
+
if (outcome === "synced")
|
|
532
|
+
console.log(`${new Date().toISOString()} synced`);
|
|
533
|
+
} else if (command === "sync") {
|
|
212
534
|
await sync();
|
|
535
|
+
await offerDaemon();
|
|
213
536
|
} else {
|
|
214
537
|
console.log(help);
|
|
215
538
|
}
|
|
216
539
|
} catch (error) {
|
|
217
|
-
|
|
540
|
+
const message = error instanceof Error ? error.message : "Could not run Hacklab CLI.";
|
|
541
|
+
if (error instanceof DaemonError) {
|
|
542
|
+
console.error(`${message}
|
|
543
|
+
Run hacklab sync to upload manually.`);
|
|
544
|
+
} else {
|
|
545
|
+
console.error(process.argv.includes("--daemon") ? `${new Date().toISOString()} ${message}` : message);
|
|
546
|
+
}
|
|
218
547
|
process.exitCode = 1;
|
|
219
548
|
}
|
package/package.json
CHANGED