bisync-cli 0.0.8 → 0.0.9
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/bisync.js +407 -90
- package/package.json +1 -1
package/dist/bisync.js
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/bin.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
5
6
|
import { createWriteStream, mkdirSync } from "fs";
|
|
6
7
|
import { readdir, stat } from "fs/promises";
|
|
7
8
|
import { homedir } from "os";
|
|
8
|
-
import { join, resolve } from "path";
|
|
9
|
+
import { join, relative, resolve, sep } from "path";
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
import { setTimeout } from "timers/promises";
|
|
12
|
+
import { parseArgs as parseArgsUtil } from "util";
|
|
9
13
|
// package.json
|
|
10
|
-
var version = "0.0.
|
|
14
|
+
var version = "0.0.9";
|
|
11
15
|
|
|
12
16
|
// src/bin.ts
|
|
13
17
|
var CONFIG_DIR = join(homedir(), ".agent-bisync");
|
|
@@ -17,17 +21,40 @@ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
|
17
21
|
var DEBUG_LOG_PATH = join(CONFIG_DIR, "debug.log");
|
|
18
22
|
var CLIENT_ID = "bisync-cli";
|
|
19
23
|
var MAX_LINES_PER_BATCH = 200;
|
|
20
|
-
var
|
|
21
|
-
var
|
|
24
|
+
var MAX_STATE_CHUNKS_PER_BATCH = 50;
|
|
25
|
+
var MAX_STATE_CHUNK_SIZE = 256 * 1024;
|
|
26
|
+
var parseArgs = () => {
|
|
27
|
+
const parsed = parseArgsUtil({
|
|
28
|
+
args: process.argv.slice(2),
|
|
29
|
+
options: {
|
|
30
|
+
verbose: { type: "boolean" },
|
|
31
|
+
version: { type: "boolean" },
|
|
32
|
+
help: { type: "boolean" },
|
|
33
|
+
force: { type: "boolean" },
|
|
34
|
+
"site-url": { type: "string" },
|
|
35
|
+
siteUrl: { type: "string" }
|
|
36
|
+
},
|
|
37
|
+
allowPositionals: true
|
|
38
|
+
});
|
|
39
|
+
const values = parsed.values;
|
|
40
|
+
const positionals = parsed.positionals;
|
|
41
|
+
return { values, positionals };
|
|
42
|
+
};
|
|
43
|
+
var printUsage = () => {
|
|
22
44
|
console.log(`bisync <command>
|
|
23
45
|
|
|
24
46
|
Commands:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
auth login --site-url <url> Authenticate via device flow
|
|
48
|
+
auth logout Sign out of the CLI
|
|
49
|
+
session list List session ids
|
|
50
|
+
setup --site-url <url> Configure hooks and authenticate
|
|
51
|
+
hook <HOOK> Handle Claude hook input (stdin JSON)
|
|
28
52
|
|
|
29
53
|
Global options:
|
|
30
54
|
--verbose Enable verbose logging
|
|
55
|
+
--help Show usage
|
|
56
|
+
|
|
57
|
+
Version: ${version} (Bun ${Bun.version})
|
|
31
58
|
`);
|
|
32
59
|
};
|
|
33
60
|
var LOG_LEVEL = "info";
|
|
@@ -46,14 +73,19 @@ var getDebugLogStream = () => {
|
|
|
46
73
|
return null;
|
|
47
74
|
}
|
|
48
75
|
};
|
|
49
|
-
var log = (level, message) => {
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
var log = (level, message, fields) => {
|
|
77
|
+
let logMessage = `[${level}] ${message}`;
|
|
78
|
+
if (fields) {
|
|
79
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
80
|
+
logMessage += ` ${key}=${value}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
getDebugLogStream()?.write(`[${new Date().toISOString()}]${logMessage}
|
|
52
84
|
`);
|
|
53
85
|
if (level < LOG_LEVEL) {
|
|
54
86
|
return;
|
|
55
87
|
}
|
|
56
|
-
console.log(logMessage);
|
|
88
|
+
console.log(`[bisync]${logMessage}`);
|
|
57
89
|
};
|
|
58
90
|
var readConfig = async () => {
|
|
59
91
|
return await Bun.file(CONFIG_PATH).json();
|
|
@@ -63,9 +95,34 @@ var writeConfig = async (config) => {
|
|
|
63
95
|
createPath: true
|
|
64
96
|
});
|
|
65
97
|
};
|
|
98
|
+
var readConfigIfExists = async () => {
|
|
99
|
+
const configFile = Bun.file(CONFIG_PATH);
|
|
100
|
+
if (!await configFile.exists()) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return await configFile.json();
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var promptYesNo = async (question, defaultValue) => {
|
|
110
|
+
if (!process.stdin.isTTY) {
|
|
111
|
+
log("warn", `${question} Using default ${defaultValue ? "yes" : "no"} (stdin not interactive).`);
|
|
112
|
+
return defaultValue;
|
|
113
|
+
}
|
|
114
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
115
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
+
const answer = await new Promise((resolve2) => rl.question(`${question}${suffix}`, resolve2));
|
|
117
|
+
rl.close();
|
|
118
|
+
const normalized = answer.trim().toLowerCase();
|
|
119
|
+
if (!normalized) {
|
|
120
|
+
return defaultValue;
|
|
121
|
+
}
|
|
122
|
+
return normalized === "y" || normalized === "yes";
|
|
123
|
+
};
|
|
66
124
|
var resolveSiteUrl = (args) => {
|
|
67
|
-
const
|
|
68
|
-
const argValue = argIndex >= 0 ? args[argIndex + 1] : undefined;
|
|
125
|
+
const argValue = args["site-url"] ?? args.siteUrl;
|
|
69
126
|
const envValue = process.env.BISYNC_SITE_URL;
|
|
70
127
|
if (argValue) {
|
|
71
128
|
return { siteUrl: argValue, source: "arg" };
|
|
@@ -83,8 +140,21 @@ var openBrowser = async (url) => {
|
|
|
83
140
|
await Bun.$`open ${url}`;
|
|
84
141
|
} catch {}
|
|
85
142
|
};
|
|
143
|
+
var buildVerificationUrl = (data) => {
|
|
144
|
+
if (data.verification_uri_complete) {
|
|
145
|
+
return data.verification_uri_complete;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(data.verification_uri);
|
|
149
|
+
url.searchParams.set("user_code", data.user_code);
|
|
150
|
+
return url.toString();
|
|
151
|
+
} catch {
|
|
152
|
+
const separator = data.verification_uri.includes("?") ? "&" : "?";
|
|
153
|
+
return `${data.verification_uri}${separator}user_code=${encodeURIComponent(data.user_code)}`;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
86
156
|
var deviceAuthFlow = async (siteUrl) => {
|
|
87
|
-
log("debug", `device auth start
|
|
157
|
+
log("debug", `device auth start`, { siteUrl });
|
|
88
158
|
const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
|
|
89
159
|
method: "POST",
|
|
90
160
|
headers: { "Content-Type": "application/json" },
|
|
@@ -92,20 +162,20 @@ var deviceAuthFlow = async (siteUrl) => {
|
|
|
92
162
|
});
|
|
93
163
|
if (!codeResponse.ok) {
|
|
94
164
|
const errorText = await codeResponse.text();
|
|
95
|
-
log("debug", `device code request failed status
|
|
165
|
+
log("debug", `device code request failed`, { status: codeResponse.status });
|
|
96
166
|
throw new Error(`Device code request failed: ${codeResponse.status} ${errorText}`);
|
|
97
167
|
}
|
|
98
|
-
log("debug", `device code request ok status
|
|
168
|
+
log("debug", `device code request ok`, { status: codeResponse.status });
|
|
99
169
|
const codeData = await codeResponse.json();
|
|
100
|
-
const verificationUrl = codeData
|
|
101
|
-
log(
|
|
102
|
-
log(
|
|
170
|
+
const verificationUrl = buildVerificationUrl(codeData);
|
|
171
|
+
console.log(`Authorize this device: ${verificationUrl}`);
|
|
172
|
+
console.log(`Your Verification Code: ${codeData.user_code}`);
|
|
103
173
|
await openBrowser(verificationUrl);
|
|
104
174
|
const intervalMs = Math.max(1, codeData.interval ?? 5) * 1000;
|
|
105
|
-
log("debug", `device auth polling
|
|
175
|
+
log("debug", `device auth polling`, { intervalMs });
|
|
106
176
|
let pollDelay = intervalMs;
|
|
107
177
|
while (true) {
|
|
108
|
-
await
|
|
178
|
+
await setTimeout(pollDelay);
|
|
109
179
|
const tokenResponse = await fetch(`${siteUrl}/api/auth/device/token`, {
|
|
110
180
|
method: "POST",
|
|
111
181
|
headers: { "Content-Type": "application/json" },
|
|
@@ -117,7 +187,7 @@ var deviceAuthFlow = async (siteUrl) => {
|
|
|
117
187
|
});
|
|
118
188
|
const tokenData = await tokenResponse.json();
|
|
119
189
|
if (tokenResponse.ok && tokenData.access_token) {
|
|
120
|
-
log("debug", `device auth success status
|
|
190
|
+
log("debug", `device auth success`, { status: tokenResponse.status });
|
|
121
191
|
return tokenData.access_token;
|
|
122
192
|
}
|
|
123
193
|
if (tokenData.error === "authorization_pending") {
|
|
@@ -125,10 +195,13 @@ var deviceAuthFlow = async (siteUrl) => {
|
|
|
125
195
|
}
|
|
126
196
|
if (tokenData.error === "slow_down") {
|
|
127
197
|
pollDelay += 1000;
|
|
128
|
-
log("debug", `device auth slow_down nextDelayMs
|
|
198
|
+
log("debug", `device auth slow_down`, { nextDelayMs: pollDelay });
|
|
129
199
|
continue;
|
|
130
200
|
}
|
|
131
|
-
log("debug", `device auth failed
|
|
201
|
+
log("debug", `device auth failed`, {
|
|
202
|
+
status: tokenResponse.status,
|
|
203
|
+
error: tokenData.error ?? "unknown"
|
|
204
|
+
});
|
|
132
205
|
throw new Error(`Device auth failed: ${tokenData.error ?? "unknown"} ${tokenData.error_description ?? ""}`.trim());
|
|
133
206
|
}
|
|
134
207
|
};
|
|
@@ -149,10 +222,10 @@ var ensureHookEntry = (entries, next) => {
|
|
|
149
222
|
return entries;
|
|
150
223
|
};
|
|
151
224
|
var mergeSettings = async () => {
|
|
152
|
-
log("debug", `mergeSettings path
|
|
225
|
+
log("debug", `mergeSettings`, { path: CLAUDE_SETTINGS_PATH });
|
|
153
226
|
const settingsFile = Bun.file(CLAUDE_SETTINGS_PATH);
|
|
154
227
|
const settingsExists = await settingsFile.exists();
|
|
155
|
-
log("debug", `mergeSettings existing
|
|
228
|
+
log("debug", `mergeSettings`, { existing: settingsExists });
|
|
156
229
|
let current = {};
|
|
157
230
|
if (settingsExists) {
|
|
158
231
|
try {
|
|
@@ -191,9 +264,14 @@ var mergeSettings = async () => {
|
|
|
191
264
|
log("debug", `mergeSettings wrote settings`);
|
|
192
265
|
try {
|
|
193
266
|
const info = await stat(CLAUDE_SETTINGS_PATH);
|
|
194
|
-
log("debug", `mergeSettings file
|
|
267
|
+
log("debug", `mergeSettings file`, {
|
|
268
|
+
size: info.size,
|
|
269
|
+
mtime: new Date(info.mtimeMs).toISOString()
|
|
270
|
+
});
|
|
195
271
|
} catch (error) {
|
|
196
|
-
log("debug", `mergeSettings stat failed
|
|
272
|
+
log("debug", `mergeSettings stat failed`, {
|
|
273
|
+
error: error instanceof Error ? error.message : String(error)
|
|
274
|
+
});
|
|
197
275
|
}
|
|
198
276
|
};
|
|
199
277
|
var findSessionFile = async (sessionId) => {
|
|
@@ -226,10 +304,103 @@ var findSessionFile = async (sessionId) => {
|
|
|
226
304
|
await walk(CLAUDE_PROJECTS_DIR);
|
|
227
305
|
return bestPath;
|
|
228
306
|
};
|
|
229
|
-
var
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
307
|
+
var isDirectory = async (path) => {
|
|
308
|
+
try {
|
|
309
|
+
return (await stat(path)).isDirectory();
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var resolveSessionDir = async (sessionFile) => {
|
|
315
|
+
if (!sessionFile.endsWith(".jsonl")) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const candidate = sessionFile.slice(0, -".jsonl".length);
|
|
319
|
+
return await isDirectory(candidate) ? candidate : null;
|
|
320
|
+
};
|
|
321
|
+
var findSessionDir = async (sessionId) => {
|
|
322
|
+
let bestPath = null;
|
|
323
|
+
let bestMtimeMs = 0;
|
|
324
|
+
const walk = async (dir) => {
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
328
|
+
} catch {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const fullPath = join(dir, entry.name);
|
|
333
|
+
if (!entry.isDirectory()) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (entry.name === sessionId) {
|
|
337
|
+
const info = await stat(fullPath);
|
|
338
|
+
if (!bestPath || info.mtimeMs > bestMtimeMs) {
|
|
339
|
+
bestPath = fullPath;
|
|
340
|
+
bestMtimeMs = info.mtimeMs;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
await walk(fullPath);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
await walk(CLAUDE_PROJECTS_DIR);
|
|
347
|
+
return bestPath;
|
|
348
|
+
};
|
|
349
|
+
var normalizePath = (value) => value.split(sep).join("/");
|
|
350
|
+
var collectSessionStateFiles = async (sessionDir) => {
|
|
351
|
+
const files = [];
|
|
352
|
+
const walk = async (dir) => {
|
|
353
|
+
let entries;
|
|
354
|
+
try {
|
|
355
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
356
|
+
} catch {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
const fullPath = join(dir, entry.name);
|
|
361
|
+
if (entry.isDirectory()) {
|
|
362
|
+
await walk(fullPath);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (!entry.isFile()) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const info = await stat(fullPath);
|
|
369
|
+
files.push({
|
|
370
|
+
fullPath,
|
|
371
|
+
relativePath: normalizePath(relative(sessionDir, fullPath)),
|
|
372
|
+
size: info.size,
|
|
373
|
+
mtimeMs: info.mtimeMs
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
await walk(sessionDir);
|
|
378
|
+
return files;
|
|
379
|
+
};
|
|
380
|
+
var buildStateChunks = async (sessionDir) => {
|
|
381
|
+
const files = await collectSessionStateFiles(sessionDir);
|
|
382
|
+
const chunks = [];
|
|
383
|
+
for (const file of files) {
|
|
384
|
+
const data = new Uint8Array(await Bun.file(file.fullPath).arrayBuffer());
|
|
385
|
+
const fileHash = createHash("sha256").update(data).digest("hex");
|
|
386
|
+
const encoded = Buffer.from(data).toString("base64");
|
|
387
|
+
const partCount = Math.max(1, Math.ceil(encoded.length / MAX_STATE_CHUNK_SIZE));
|
|
388
|
+
for (let partIndex = 0;partIndex < partCount; partIndex += 1) {
|
|
389
|
+
const start = partIndex * MAX_STATE_CHUNK_SIZE;
|
|
390
|
+
const end = start + MAX_STATE_CHUNK_SIZE;
|
|
391
|
+
chunks.push({
|
|
392
|
+
path: file.relativePath,
|
|
393
|
+
content: encoded.slice(start, end),
|
|
394
|
+
encoding: "base64",
|
|
395
|
+
fileHash,
|
|
396
|
+
partIndex,
|
|
397
|
+
partCount,
|
|
398
|
+
size: file.size,
|
|
399
|
+
mtimeMs: file.mtimeMs
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return chunks;
|
|
233
404
|
};
|
|
234
405
|
var buildLogLines = (raw) => {
|
|
235
406
|
const lines = raw.split(`
|
|
@@ -272,49 +443,154 @@ var uploadLogs = async (siteUrl, token, sessionId, raw) => {
|
|
|
272
443
|
}
|
|
273
444
|
}
|
|
274
445
|
};
|
|
446
|
+
var uploadStateFiles = async (siteUrl, token, sessionId, chunks) => {
|
|
447
|
+
for (let i = 0;i < chunks.length; i += MAX_STATE_CHUNKS_PER_BATCH) {
|
|
448
|
+
const batch = chunks.slice(i, i + MAX_STATE_CHUNKS_PER_BATCH);
|
|
449
|
+
const response = await fetch(`${siteUrl}/api/ingest/state`, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: {
|
|
452
|
+
"Content-Type": "application/json",
|
|
453
|
+
Authorization: `Bearer ${token}`,
|
|
454
|
+
...process.env.BISYNC_SESSION_ID ? { "X-Bisync-Session-Id": process.env.BISYNC_SESSION_ID } : {}
|
|
455
|
+
},
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
session_id: sessionId,
|
|
458
|
+
files: batch
|
|
459
|
+
})
|
|
460
|
+
});
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
const errorText = await response.text();
|
|
463
|
+
const error = new Error(`State ingestion failed: ${response.status} ${errorText}`);
|
|
464
|
+
error.status = response.status;
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var signOutRemote = async (siteUrl, token) => {
|
|
470
|
+
try {
|
|
471
|
+
const response = await fetch(`${siteUrl}/api/auth/sign-out`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: {
|
|
474
|
+
"Content-Type": "application/json",
|
|
475
|
+
Authorization: `Bearer ${token}`
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify({})
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
const errorText = await response.text();
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
error: `${response.status} ${errorText}`.trim()
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return { ok: true };
|
|
487
|
+
} catch (error) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
error: error instanceof Error ? error.message : String(error)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
};
|
|
275
494
|
var runSetup = async (args) => {
|
|
276
495
|
log("debug", `runSetup start`);
|
|
277
|
-
const force = args.
|
|
278
|
-
log("debug",
|
|
496
|
+
const force = Boolean(args.force);
|
|
497
|
+
log("debug", "runSetup", { force });
|
|
279
498
|
const configExists = await Bun.file(CONFIG_PATH).exists();
|
|
280
|
-
log("debug",
|
|
499
|
+
log("debug", "config", { path: CONFIG_PATH, exists: configExists });
|
|
281
500
|
let existingConfig = null;
|
|
282
501
|
if (configExists) {
|
|
283
502
|
try {
|
|
284
503
|
existingConfig = await readConfig();
|
|
285
|
-
log("debug",
|
|
504
|
+
log("debug", "config loaded", {
|
|
505
|
+
siteUrl: existingConfig.siteUrl,
|
|
506
|
+
token: existingConfig.token ? "present" : "missing"
|
|
507
|
+
});
|
|
286
508
|
} catch {
|
|
287
509
|
log("debug", `config load failed; continuing without existing config`);
|
|
288
510
|
existingConfig = null;
|
|
289
511
|
}
|
|
290
512
|
}
|
|
291
513
|
const resolution = resolveSiteUrl(args);
|
|
292
|
-
log("debug",
|
|
514
|
+
log("debug", "siteUrl resolved", {
|
|
515
|
+
siteUrl: resolution.siteUrl ?? "null",
|
|
516
|
+
source: resolution.source
|
|
517
|
+
});
|
|
293
518
|
const siteUrl = resolution.siteUrl ?? existingConfig?.siteUrl ?? null;
|
|
294
519
|
if (!resolution.siteUrl && existingConfig?.siteUrl) {
|
|
295
|
-
log("debug",
|
|
520
|
+
log("debug", "siteUrl fallback to existing config", { siteUrl: existingConfig.siteUrl });
|
|
296
521
|
}
|
|
297
522
|
if (!siteUrl) {
|
|
298
|
-
log("debug",
|
|
523
|
+
log("debug", "runSetup aborted", { error: "missing-site-url" });
|
|
299
524
|
throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
|
|
300
525
|
}
|
|
301
526
|
if (configExists && existingConfig?.token && !force) {
|
|
302
|
-
log("debug",
|
|
527
|
+
log("debug", "using existing token; updating Claude settings only");
|
|
303
528
|
await mergeSettings();
|
|
304
|
-
log("info",
|
|
529
|
+
log("info", "Updated Claude settings using existing credentials", { path: CONFIG_PATH });
|
|
305
530
|
return;
|
|
306
531
|
}
|
|
307
532
|
const token = await deviceAuthFlow(siteUrl);
|
|
308
533
|
await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
|
|
309
|
-
log("debug",
|
|
534
|
+
log("debug", "config written", { path: CONFIG_PATH });
|
|
310
535
|
await mergeSettings();
|
|
311
|
-
log("info",
|
|
536
|
+
log("info", "Configured Claude settings and saved credentials", { path: CONFIG_PATH });
|
|
537
|
+
};
|
|
538
|
+
var runAuthLogin = async (args) => {
|
|
539
|
+
log("debug", "runAuthLogin start");
|
|
540
|
+
const force = Boolean(args.force);
|
|
541
|
+
const config = await readConfigIfExists();
|
|
542
|
+
const resolution = resolveSiteUrl(args);
|
|
543
|
+
const siteUrl = resolution.siteUrl ?? config?.siteUrl ?? null;
|
|
544
|
+
if (!siteUrl) {
|
|
545
|
+
log("error", "runAuthLogin aborted", { error: "missing-site-url" });
|
|
546
|
+
throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
|
|
547
|
+
}
|
|
548
|
+
if (config?.token) {
|
|
549
|
+
const shouldReauth = force || await promptYesNo("Already signed in. Sign out and re-authenticate?", false);
|
|
550
|
+
if (!shouldReauth) {
|
|
551
|
+
log("info", "Keeping existing credentials.");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const signOutResult = await signOutRemote(siteUrl, config.token);
|
|
555
|
+
await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
|
|
556
|
+
if (!signOutResult.ok) {
|
|
557
|
+
log("warn", `Remote sign-out failed: ${signOutResult.error ?? "unknown error"}. Continuing with login.`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const token = await deviceAuthFlow(siteUrl);
|
|
561
|
+
await writeConfig({ siteUrl, token, clientId: CLIENT_ID });
|
|
562
|
+
log("info", `Signed in and saved credentials to ${CONFIG_PATH}`);
|
|
563
|
+
};
|
|
564
|
+
var runAuthLogout = async (args) => {
|
|
565
|
+
log("debug", `runAuthLogout start`);
|
|
566
|
+
const config = await readConfigIfExists();
|
|
567
|
+
const resolution = resolveSiteUrl(args);
|
|
568
|
+
const siteUrl = resolution.siteUrl ?? config?.siteUrl ?? null;
|
|
569
|
+
if (!siteUrl) {
|
|
570
|
+
if (!config?.token) {
|
|
571
|
+
log("info", "Already signed out.");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
throw new Error("Missing site URL. Provide --site-url or BISYNC_SITE_URL.");
|
|
575
|
+
}
|
|
576
|
+
if (!config?.token) {
|
|
577
|
+
await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
|
|
578
|
+
log("info", "Already signed out.");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const signOutResult = await signOutRemote(siteUrl, config.token);
|
|
582
|
+
await writeConfig({ siteUrl, token: "", clientId: CLIENT_ID });
|
|
583
|
+
if (!signOutResult.ok) {
|
|
584
|
+
log("warn", `Remote sign-out failed: ${signOutResult.error ?? "unknown error"}. Local credentials cleared.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
log("info", "Signed out.");
|
|
312
588
|
};
|
|
313
|
-
var
|
|
314
|
-
log("debug", `
|
|
589
|
+
var runSessionList = async () => {
|
|
590
|
+
log("debug", `runSessionList start`, { config: CONFIG_PATH });
|
|
315
591
|
const config = await readConfig();
|
|
316
592
|
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
317
|
-
log("debug", `
|
|
593
|
+
log("debug", `runSessionList`, { siteUrl });
|
|
318
594
|
const response = await fetch(`${siteUrl}/api/list-sessions`, {
|
|
319
595
|
method: "GET",
|
|
320
596
|
headers: {
|
|
@@ -323,20 +599,21 @@ var runVerify = async () => {
|
|
|
323
599
|
});
|
|
324
600
|
if (!response.ok) {
|
|
325
601
|
const errorText = await response.text();
|
|
326
|
-
log("debug", `
|
|
327
|
-
throw new Error(`
|
|
602
|
+
log("debug", `runSessionList failed`, { status: response.status });
|
|
603
|
+
throw new Error(`Session list failed: ${response.status} ${errorText}`);
|
|
328
604
|
}
|
|
605
|
+
log("debug", "runSessionList ok", { status: response.status });
|
|
329
606
|
const data = await response.json();
|
|
330
607
|
if (data.sessions.length === 0) {
|
|
331
608
|
log("info", "No sessions found.");
|
|
332
609
|
return;
|
|
333
610
|
}
|
|
334
611
|
for (const session of data.sessions) {
|
|
335
|
-
log(
|
|
612
|
+
console.log(session.externalId);
|
|
336
613
|
}
|
|
337
614
|
};
|
|
338
615
|
var runHook = async (hookName) => {
|
|
339
|
-
log("debug", `runHook start hook
|
|
616
|
+
log("debug", `runHook start`, { hook: hookName });
|
|
340
617
|
const stdinRaw = await new Promise((resolve2, reject) => {
|
|
341
618
|
let data = "";
|
|
342
619
|
process.stdin.setEncoding("utf8");
|
|
@@ -346,24 +623,19 @@ var runHook = async (hookName) => {
|
|
|
346
623
|
process.stdin.on("end", () => resolve2(data));
|
|
347
624
|
process.stdin.on("error", (error) => reject(error));
|
|
348
625
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!stdinRaw.trim()) {
|
|
352
|
-
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=empty-stdin
|
|
353
|
-
`);
|
|
626
|
+
log("debug", `runHook`, { hook: hookName, stdin: stdinRaw.trim() });
|
|
627
|
+
if (!stdinRaw.trim())
|
|
354
628
|
return;
|
|
355
|
-
}
|
|
356
629
|
const stdinPayload = JSON.parse(stdinRaw);
|
|
357
630
|
const sessionId = stdinPayload.session_id;
|
|
358
631
|
if (!sessionId) {
|
|
359
|
-
|
|
360
|
-
`);
|
|
632
|
+
log("debug", `runHook`, { hook: hookName, error: "missing-session-id" });
|
|
361
633
|
return;
|
|
362
634
|
}
|
|
363
635
|
const config = await readConfig();
|
|
364
636
|
const siteUrl = process.env.BISYNC_SITE_URL ?? config.siteUrl;
|
|
365
637
|
const token = config.token;
|
|
366
|
-
log("debug", `runHook
|
|
638
|
+
log("debug", `runHook`, { siteUrl });
|
|
367
639
|
let sessionFile = null;
|
|
368
640
|
if (typeof stdinPayload.transcript_path === "string") {
|
|
369
641
|
const resolvedPath = resolve(stdinPayload.transcript_path);
|
|
@@ -375,57 +647,102 @@ var runHook = async (hookName) => {
|
|
|
375
647
|
sessionFile = await findSessionFile(sessionId);
|
|
376
648
|
}
|
|
377
649
|
if (!sessionFile) {
|
|
378
|
-
log("debug", `runHook session
|
|
379
|
-
await appendDebugLog(`[${new Date().toISOString()}] hook=${hookName} error=session-file-not-found session_id=${sessionId}
|
|
380
|
-
`);
|
|
381
|
-
return;
|
|
650
|
+
log("debug", `runHook`, { sessionId, error: "session-file-not-found" });
|
|
382
651
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
652
|
+
let sessionDir = null;
|
|
653
|
+
if (sessionFile) {
|
|
654
|
+
sessionDir = await resolveSessionDir(sessionFile);
|
|
655
|
+
}
|
|
656
|
+
if (!sessionDir) {
|
|
657
|
+
sessionDir = await findSessionDir(sessionId);
|
|
658
|
+
}
|
|
659
|
+
if (!sessionFile && !sessionDir) {
|
|
389
660
|
return;
|
|
390
661
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
662
|
+
if (sessionFile) {
|
|
663
|
+
log("debug", `runHook`, { sessionFile });
|
|
664
|
+
const raw = await Bun.file(sessionFile).text();
|
|
665
|
+
if (!raw.trim()) {
|
|
666
|
+
log("debug", `runHook`, { sessionId, error: "empty-log" });
|
|
667
|
+
} else {
|
|
668
|
+
try {
|
|
669
|
+
await uploadLogs(siteUrl, token, sessionId, raw);
|
|
670
|
+
log("debug", `runHook`, { sessionId, error: "upload-ok" });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
log("debug", `runHook`, {
|
|
673
|
+
sessionId,
|
|
674
|
+
error: error instanceof Error ? error.message : String(error)
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (sessionDir) {
|
|
680
|
+
log("debug", `runHook`, { sessionDir });
|
|
681
|
+
try {
|
|
682
|
+
const stateChunks = await buildStateChunks(sessionDir);
|
|
683
|
+
if (stateChunks.length === 0) {
|
|
684
|
+
log("debug", `runHook`, { sessionId, error: "no-state-files" });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
await uploadStateFiles(siteUrl, token, sessionId, stateChunks);
|
|
688
|
+
log("debug", `runHook`, { sessionId, error: "state-upload-ok" });
|
|
689
|
+
} catch (error) {
|
|
690
|
+
log("debug", `runHook`, {
|
|
691
|
+
sessionId,
|
|
692
|
+
error: error instanceof Error ? error.message : String(error)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
398
695
|
}
|
|
399
696
|
};
|
|
400
697
|
var main = async () => {
|
|
401
|
-
|
|
698
|
+
const { values, positionals } = parseArgs();
|
|
699
|
+
if (values.help) {
|
|
700
|
+
printUsage();
|
|
701
|
+
process.exit(0);
|
|
702
|
+
}
|
|
703
|
+
if (values.version) {
|
|
402
704
|
console.log(`\u25B6\uFE0E ${version} (Bun ${Bun.version})`);
|
|
403
705
|
process.exit(0);
|
|
404
706
|
}
|
|
405
|
-
if (
|
|
707
|
+
if (values.verbose) {
|
|
406
708
|
LOG_LEVEL = "debug";
|
|
407
709
|
}
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
log("debug", `argv=${JSON.stringify(rawArgs)}`);
|
|
710
|
+
const [command, subcommand] = positionals;
|
|
711
|
+
log("debug", "argv", { argv: JSON.stringify(process.argv.slice(2)) });
|
|
411
712
|
if (!command) {
|
|
412
|
-
|
|
713
|
+
printUsage();
|
|
413
714
|
process.exit(1);
|
|
414
715
|
}
|
|
415
716
|
try {
|
|
416
717
|
if (command === "setup") {
|
|
417
|
-
await runSetup(
|
|
418
|
-
|
|
718
|
+
await runSetup(values);
|
|
719
|
+
process.exit(0);
|
|
419
720
|
}
|
|
420
|
-
if (command === "
|
|
421
|
-
|
|
422
|
-
|
|
721
|
+
if (command === "auth") {
|
|
722
|
+
if (subcommand === "login") {
|
|
723
|
+
await runAuthLogin(values);
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
if (subcommand === "logout") {
|
|
727
|
+
await runAuthLogout(values);
|
|
728
|
+
process.exit(0);
|
|
729
|
+
}
|
|
730
|
+
printUsage();
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
if (command === "session") {
|
|
734
|
+
if (subcommand === "list") {
|
|
735
|
+
await runSessionList();
|
|
736
|
+
process.exit(0);
|
|
737
|
+
}
|
|
738
|
+
printUsage();
|
|
739
|
+
process.exit(1);
|
|
423
740
|
}
|
|
424
741
|
if (command === "hook") {
|
|
425
|
-
await runHook(
|
|
426
|
-
|
|
742
|
+
await runHook(subcommand ?? "unknown");
|
|
743
|
+
process.exit(0);
|
|
427
744
|
}
|
|
428
|
-
|
|
745
|
+
printUsage();
|
|
429
746
|
process.exit(1);
|
|
430
747
|
} catch (error) {
|
|
431
748
|
log("error", error instanceof Error ? error.message : String(error));
|