@teammy/cli 0.1.0 → 0.1.2
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/README.md +7 -7
- package/dist/main.js +626 -308
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -150,7 +150,8 @@ var MemberUpdateSchema = z3.object({
|
|
|
150
150
|
var ProjectCreateSchema = z3.object({
|
|
151
151
|
operationId: z3.uuid(),
|
|
152
152
|
projectName: z3.string().trim().min(1).max(120),
|
|
153
|
-
description: z3.string().max(2e3).default("")
|
|
153
|
+
description: z3.string().max(2e3).default(""),
|
|
154
|
+
workspacePath: z3.string().trim().min(1).optional()
|
|
154
155
|
}).strict();
|
|
155
156
|
var ProjectSummarySchema = z3.object({
|
|
156
157
|
projectId: NameSchema,
|
|
@@ -285,7 +286,7 @@ var SessionSearchSchema = z5.object({
|
|
|
285
286
|
});
|
|
286
287
|
|
|
287
288
|
// packages/transport/hub-client.ts
|
|
288
|
-
import { z as
|
|
289
|
+
import { z as z9 } from "zod";
|
|
289
290
|
import { createHash } from "node:crypto";
|
|
290
291
|
|
|
291
292
|
// packages/protocol/index.ts
|
|
@@ -330,6 +331,28 @@ function sharedPathAllowed(path, paths) {
|
|
|
330
331
|
);
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
// packages/protocol/client-access.ts
|
|
335
|
+
import { z as z8 } from "zod";
|
|
336
|
+
var ConnectSecretSchema = z8.string().regex(/^[a-f0-9]{64}$/);
|
|
337
|
+
var BrowserConnectRequestSchema = z8.object({
|
|
338
|
+
id: z8.uuid(),
|
|
339
|
+
projectId: NameSchema,
|
|
340
|
+
deviceId: DeviceIdSchema,
|
|
341
|
+
secret: ConnectSecretSchema,
|
|
342
|
+
label: z8.string().min(1).max(255)
|
|
343
|
+
}).strict();
|
|
344
|
+
var BrowserConnectStartSchema = z8.object({ id: z8.uuid(), expiresAt: z8.number() });
|
|
345
|
+
var BrowserConnectPollSchema = z8.discriminatedUnion("state", [
|
|
346
|
+
z8.object({ state: z8.literal("pending") }),
|
|
347
|
+
z8.object({ state: z8.literal("approved"), memberId: NameSchema })
|
|
348
|
+
]);
|
|
349
|
+
var BrowserConnectInfoSchema = z8.object({
|
|
350
|
+
projectName: z8.string(),
|
|
351
|
+
label: z8.string(),
|
|
352
|
+
approvedBy: NameSchema.nullable(),
|
|
353
|
+
expiresAt: z8.number()
|
|
354
|
+
});
|
|
355
|
+
|
|
333
356
|
// packages/transport/hub-client.ts
|
|
334
357
|
var HubRequestError = class extends Error {
|
|
335
358
|
constructor(message2, status, code, requestId) {
|
|
@@ -370,6 +393,17 @@ var HubClient = class {
|
|
|
370
393
|
get url() {
|
|
371
394
|
return this.baseUrl.toString();
|
|
372
395
|
}
|
|
396
|
+
startBrowserConnect(input) {
|
|
397
|
+
return this.request("POST", "api/v1/devices/connect", BrowserConnectStartSchema, input);
|
|
398
|
+
}
|
|
399
|
+
pollBrowserConnect(id, secret) {
|
|
400
|
+
return this.request(
|
|
401
|
+
"POST",
|
|
402
|
+
`api/v1/devices/connect/${encodeURIComponent(id)}/poll`,
|
|
403
|
+
BrowserConnectPollSchema,
|
|
404
|
+
{ secret }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
373
407
|
bind(deviceId, projectId) {
|
|
374
408
|
return this.request("POST", "api/v1/bind", BindingSchema, {
|
|
375
409
|
deviceId,
|
|
@@ -400,7 +434,7 @@ var HubClient = class {
|
|
|
400
434
|
void 0,
|
|
401
435
|
BLOB_TRANSFER_TIMEOUT_MS
|
|
402
436
|
);
|
|
403
|
-
if (!response.ok) return this.decode(response, signal,
|
|
437
|
+
if (!response.ok) return this.decode(response, signal, z9.never());
|
|
404
438
|
const reader = response.body?.getReader();
|
|
405
439
|
if (!reader) throw new Error("Server returned no content body.");
|
|
406
440
|
const parts = [];
|
|
@@ -445,7 +479,7 @@ var HubClient = class {
|
|
|
445
479
|
return this.request(
|
|
446
480
|
"PUT",
|
|
447
481
|
"api/v1/devices/self/status",
|
|
448
|
-
|
|
482
|
+
z9.object({ ok: z9.literal(true) }),
|
|
449
483
|
status
|
|
450
484
|
);
|
|
451
485
|
}
|
|
@@ -566,15 +600,15 @@ var HubClient = class {
|
|
|
566
600
|
import { createHash as createHash8 } from "node:crypto";
|
|
567
601
|
import { realpath as realpath6 } from "node:fs/promises";
|
|
568
602
|
import { homedir as homedir4 } from "node:os";
|
|
569
|
-
import { dirname as dirname9, join as
|
|
603
|
+
import { dirname as dirname9, join as join17, resolve as resolve8 } from "node:path";
|
|
570
604
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
571
605
|
|
|
572
606
|
// packages/protocol/file-publication.ts
|
|
573
|
-
import { z as
|
|
607
|
+
import { z as z10 } from "zod";
|
|
574
608
|
var FilePublicationSchema = FileCommitSchema.safeExtend({
|
|
575
609
|
configRevision: RevisionSchema.optional()
|
|
576
610
|
}).strict();
|
|
577
|
-
var PublishFilesSchema =
|
|
611
|
+
var PublishFilesSchema = z10.object({ manifest: BlobRefSchema }).strict();
|
|
578
612
|
|
|
579
613
|
// packages/state/sqlite.ts
|
|
580
614
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
@@ -591,7 +625,7 @@ function transaction(db, action) {
|
|
|
591
625
|
throw error;
|
|
592
626
|
}
|
|
593
627
|
}
|
|
594
|
-
function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
628
|
+
function openDatabase(path, migrations, busyTimeoutMs = 5e3, journalMode = "WAL") {
|
|
595
629
|
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
|
|
596
630
|
throw new Error("Invalid SQLite busy timeout.");
|
|
597
631
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -599,7 +633,7 @@ function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
|
599
633
|
try {
|
|
600
634
|
chmodSync(path, 384);
|
|
601
635
|
db.exec(
|
|
602
|
-
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode =
|
|
636
|
+
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = ${journalMode}; PRAGMA synchronous = FULL;`
|
|
603
637
|
);
|
|
604
638
|
transaction(db, () => {
|
|
605
639
|
const version = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
@@ -950,8 +984,8 @@ var RuntimeState = class {
|
|
|
950
984
|
|
|
951
985
|
// apps/client/state.ts
|
|
952
986
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
953
|
-
import { join as
|
|
954
|
-
import { z as
|
|
987
|
+
import { join as join7, relative as relative2, isAbsolute, sep as sep2 } from "node:path";
|
|
988
|
+
import { z as z13 } from "zod";
|
|
955
989
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
956
990
|
|
|
957
991
|
// apps/client/runtime/state-access.ts
|
|
@@ -1052,25 +1086,88 @@ function stateVersion(directory) {
|
|
|
1052
1086
|
import { execFile } from "node:child_process";
|
|
1053
1087
|
import { promisify } from "node:util";
|
|
1054
1088
|
import { lstat as lstat2, mkdir as mkdir2, realpath as realpath2, rmdir as rmdir2 } from "node:fs/promises";
|
|
1055
|
-
import { basename, dirname as dirname4, join as
|
|
1089
|
+
import { basename, dirname as dirname4, join as join5, resolve } from "node:path";
|
|
1056
1090
|
|
|
1057
1091
|
// apps/client/files/local-fs.ts
|
|
1058
1092
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
1059
|
-
import { constants } from "node:fs";
|
|
1060
|
-
import { lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink } from "node:fs/promises";
|
|
1061
|
-
import { dirname as dirname3, join as
|
|
1093
|
+
import { constants as constants2 } from "node:fs";
|
|
1094
|
+
import { lstat, mkdir, open as open2, readdir, realpath, rename, rmdir, unlink } from "node:fs/promises";
|
|
1095
|
+
import { dirname as dirname3, join as join4, relative, sep } from "node:path";
|
|
1062
1096
|
import { setImmediate } from "node:timers/promises";
|
|
1097
|
+
|
|
1098
|
+
// packages/workspace/local-config.ts
|
|
1099
|
+
import { constants } from "node:fs";
|
|
1100
|
+
import { open, writeFile } from "node:fs/promises";
|
|
1101
|
+
import { join as join3 } from "node:path";
|
|
1102
|
+
import { z as z11 } from "zod";
|
|
1103
|
+
import ignore from "ignore";
|
|
1104
|
+
function ignored(path) {
|
|
1105
|
+
return path.split("/").some(
|
|
1106
|
+
(part) => part === ".git" || part === ".DS_Store" || /^\.teammy(?:[-.]|$)/i.test(part) || /(?:~|\.(?:swp|swx|tmp|part))$/i.test(part)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1063
1109
|
function missing(error) {
|
|
1064
1110
|
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
|
|
1065
1111
|
}
|
|
1112
|
+
var LOCAL_CONFIG_NAME = ".teammy-local.json";
|
|
1113
|
+
var LocalConfigSchema = z11.object({
|
|
1114
|
+
files: z11.object({ ignore: z11.array(z11.string().max(1024)).max(256).default([]) }).strict().default({ ignore: [] })
|
|
1115
|
+
}).strict();
|
|
1116
|
+
async function ensureLocalConfig(root) {
|
|
1117
|
+
try {
|
|
1118
|
+
await writeFile(
|
|
1119
|
+
join3(root, LOCAL_CONFIG_NAME),
|
|
1120
|
+
JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
|
|
1121
|
+
{ flag: "wx", mode: 384 }
|
|
1122
|
+
);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async function loadLocalRules(root) {
|
|
1128
|
+
let rules = [];
|
|
1129
|
+
try {
|
|
1130
|
+
const file = await open(
|
|
1131
|
+
join3(root, LOCAL_CONFIG_NAME),
|
|
1132
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
1133
|
+
);
|
|
1134
|
+
try {
|
|
1135
|
+
const before = await file.stat();
|
|
1136
|
+
if (!before.isFile() || before.size > 65536)
|
|
1137
|
+
throw new Error("Expected a regular configuration file no larger than 64 KiB.");
|
|
1138
|
+
const buffer = Buffer.alloc(65537);
|
|
1139
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
1140
|
+
if (bytesRead > 65536 || bytesRead !== before.size)
|
|
1141
|
+
throw new Error("Configuration exceeds the read limit or changed while reading.");
|
|
1142
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
|
|
1143
|
+
const after = await file.stat();
|
|
1144
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
1145
|
+
throw new Error("Configuration is being saved; retry on the next check.");
|
|
1146
|
+
rules = LocalConfigSchema.parse(JSON.parse(text)).files.ignore;
|
|
1147
|
+
} finally {
|
|
1148
|
+
await file.close();
|
|
1149
|
+
}
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (!missing(error))
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
`${LOCAL_CONFIG_NAME}: invalid or unreadable configuration; uploads paused.`,
|
|
1154
|
+
{ cause: error }
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const matcher = ignore().add(rules);
|
|
1158
|
+
return {
|
|
1159
|
+
fingerprint: JSON.stringify(rules),
|
|
1160
|
+
excludes: (path, directory = false) => ignored(path) || matcher.ignores(path + (directory && !path.endsWith("/") ? "/" : ""))
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// apps/client/files/local-fs.ts
|
|
1165
|
+
function missing2(error) {
|
|
1166
|
+
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
|
|
1167
|
+
}
|
|
1066
1168
|
function signature(stat2) {
|
|
1067
1169
|
return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeNs, stat2.ctimeNs].join(":");
|
|
1068
1170
|
}
|
|
1069
|
-
function ignored(path) {
|
|
1070
|
-
return path.split("/").some(
|
|
1071
|
-
(part) => part === ".git" || part === ".DS_Store" || /^\.teammy(?:[-.]|$)/i.test(part) || /(?:~|\.(?:swp|swx|tmp|part))$/i.test(part)
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
1171
|
async function safePath(root, path, createParents = false) {
|
|
1075
1172
|
FilePathSchema.parse(path);
|
|
1076
1173
|
const rootStat = await lstat(root);
|
|
@@ -1079,7 +1176,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
1079
1176
|
const parts = path.split("/");
|
|
1080
1177
|
let parent = root;
|
|
1081
1178
|
for (const part of parts.slice(0, -1)) {
|
|
1082
|
-
parent =
|
|
1179
|
+
parent = join4(parent, part);
|
|
1083
1180
|
if (createParents)
|
|
1084
1181
|
await mkdir(parent).catch((error) => {
|
|
1085
1182
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -1089,7 +1186,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
1089
1186
|
if (!stat2.isDirectory())
|
|
1090
1187
|
throw Object.assign(new Error(`Parent is not a directory: ${path}`), { code: "ENOTDIR" });
|
|
1091
1188
|
}
|
|
1092
|
-
return
|
|
1189
|
+
return join4(root, ...parts);
|
|
1093
1190
|
}
|
|
1094
1191
|
async function metadata(root, path) {
|
|
1095
1192
|
try {
|
|
@@ -1099,7 +1196,7 @@ async function metadata(root, path) {
|
|
|
1099
1196
|
throw new Error(`Unsupported filesystem entry: ${path}`);
|
|
1100
1197
|
return { signature: signature(stat2), directory: stat2.isDirectory() };
|
|
1101
1198
|
} catch (error) {
|
|
1102
|
-
if (
|
|
1199
|
+
if (missing2(error)) return null;
|
|
1103
1200
|
throw error;
|
|
1104
1201
|
}
|
|
1105
1202
|
}
|
|
@@ -1149,7 +1246,7 @@ async function readStable(root, path) {
|
|
|
1149
1246
|
const inside = relative(await realpath(root), resolvedBefore);
|
|
1150
1247
|
if (inside.startsWith(`..${sep}`) || inside === "..")
|
|
1151
1248
|
throw new Error(`File escapes sync root: ${path}`);
|
|
1152
|
-
const file = await
|
|
1249
|
+
const file = await open2(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
1153
1250
|
try {
|
|
1154
1251
|
const before = await file.stat({ bigint: true });
|
|
1155
1252
|
if (!before.isFile() || before.size > BigInt(MAX_FILE_BYTES))
|
|
@@ -1179,7 +1276,7 @@ async function readStable(root, path) {
|
|
|
1179
1276
|
}
|
|
1180
1277
|
async function syncDirectory(directory) {
|
|
1181
1278
|
if (process.platform === "win32") return;
|
|
1182
|
-
const handle = await
|
|
1279
|
+
const handle = await open2(directory, "r");
|
|
1183
1280
|
try {
|
|
1184
1281
|
await handle.sync();
|
|
1185
1282
|
} finally {
|
|
@@ -1188,8 +1285,8 @@ async function syncDirectory(directory) {
|
|
|
1188
1285
|
}
|
|
1189
1286
|
async function replaceFile(root, path, bytes, expected) {
|
|
1190
1287
|
const target = await safePath(root, path, true);
|
|
1191
|
-
const temporary =
|
|
1192
|
-
const file = await
|
|
1288
|
+
const temporary = join4(dirname3(target), `.teammy-${randomUUID2()}.tmp`);
|
|
1289
|
+
const file = await open2(temporary, "wx", 384);
|
|
1193
1290
|
try {
|
|
1194
1291
|
try {
|
|
1195
1292
|
await file.writeFile(bytes);
|
|
@@ -1213,7 +1310,7 @@ async function replaceFile(root, path, bytes, expected) {
|
|
|
1213
1310
|
}
|
|
1214
1311
|
} finally {
|
|
1215
1312
|
await unlink(temporary).catch((error) => {
|
|
1216
|
-
if (!
|
|
1313
|
+
if (!missing2(error)) throw error;
|
|
1217
1314
|
});
|
|
1218
1315
|
}
|
|
1219
1316
|
}
|
|
@@ -1241,7 +1338,7 @@ async function removeFile(root, path, expected) {
|
|
|
1241
1338
|
var exec = promisify(execFile);
|
|
1242
1339
|
async function workspacePath(input) {
|
|
1243
1340
|
const path = resolve(input);
|
|
1244
|
-
return
|
|
1341
|
+
return join5(await realpath2(dirname4(path)), basename(path));
|
|
1245
1342
|
}
|
|
1246
1343
|
async function directoryIdentity(root) {
|
|
1247
1344
|
const stat2 = await lstat2(root, { bigint: true });
|
|
@@ -1257,33 +1354,43 @@ async function verifyWorkspace(state, root) {
|
|
|
1257
1354
|
"Workspace directory was replaced. Restore the original directory before syncing."
|
|
1258
1355
|
);
|
|
1259
1356
|
}
|
|
1260
|
-
async function
|
|
1357
|
+
async function workspaceGitInfo(root) {
|
|
1261
1358
|
const parent = dirname4(root), name2 = basename(root);
|
|
1262
|
-
if (/[\r\n]/.test(name2)) throw new Error("Workspace name cannot contain a newline.");
|
|
1263
1359
|
let repo = false;
|
|
1264
1360
|
for (let p = parent; ; p = dirname4(p)) {
|
|
1265
1361
|
try {
|
|
1266
|
-
await lstat2(
|
|
1362
|
+
await lstat2(join5(p, ".git"));
|
|
1267
1363
|
repo = true;
|
|
1268
1364
|
break;
|
|
1269
1365
|
} catch (e) {
|
|
1270
|
-
if (!
|
|
1366
|
+
if (!missing2(e)) throw e;
|
|
1271
1367
|
}
|
|
1272
1368
|
if (p === dirname4(p)) break;
|
|
1273
1369
|
}
|
|
1274
|
-
if (repo) {
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1370
|
+
if (!repo) return { repo: false, tracked: false, date: null };
|
|
1371
|
+
try {
|
|
1372
|
+
const tracked = (await exec("git", ["-C", parent, "--literal-pathspecs", "ls-files", "-z", "--", name2], {
|
|
1373
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1374
|
+
})).stdout;
|
|
1375
|
+
const date = tracked ? await exec("git", [
|
|
1376
|
+
"-C",
|
|
1377
|
+
parent,
|
|
1378
|
+
"--literal-pathspecs",
|
|
1379
|
+
"log",
|
|
1380
|
+
"-1",
|
|
1381
|
+
"--format=%cI",
|
|
1382
|
+
"--",
|
|
1383
|
+
name2
|
|
1384
|
+
]).then((result2) => result2.stdout.trim() || null).catch(() => null) : null;
|
|
1385
|
+
return { repo: true, tracked: Boolean(tracked), date };
|
|
1386
|
+
} catch {
|
|
1387
|
+
throw new Error("Cannot inspect the Git index. Check Git access before connecting.");
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async function ignorePlan(root) {
|
|
1391
|
+
const parent = dirname4(root), name2 = basename(root);
|
|
1392
|
+
if (/[\r\n]/.test(name2)) throw new Error("Workspace name cannot contain a newline.");
|
|
1393
|
+
if ((await workspaceGitInfo(root)).repo) {
|
|
1287
1394
|
try {
|
|
1288
1395
|
await exec("git", ["-C", parent, "check-ignore", "--no-index", "-q", "--", name2 + "/"]);
|
|
1289
1396
|
return;
|
|
@@ -1314,7 +1421,7 @@ async function prepareWorkspace(state, root, input, reconnect = false) {
|
|
|
1314
1421
|
await lstat2(root);
|
|
1315
1422
|
present = true;
|
|
1316
1423
|
} catch (e) {
|
|
1317
|
-
if (!
|
|
1424
|
+
if (!missing2(e)) throw e;
|
|
1318
1425
|
}
|
|
1319
1426
|
if (present) {
|
|
1320
1427
|
if (reconnect && state.binding(root)) {
|
|
@@ -1327,12 +1434,16 @@ async function prepareWorkspace(state, root, input, reconnect = false) {
|
|
|
1327
1434
|
if (ignore3) await replaceFile(ignore3.parent, ".gitignore", ignore3.bytes, ignore3.signature);
|
|
1328
1435
|
return;
|
|
1329
1436
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1437
|
+
if (prior || state.binding(root))
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
'Workspace is already prepared. Use the original connect command or "teammy sync on".'
|
|
1440
|
+
);
|
|
1441
|
+
await directoryIdentity(root);
|
|
1442
|
+
return true;
|
|
1333
1443
|
}
|
|
1334
1444
|
if (state.binding(root) || prior)
|
|
1335
1445
|
throw new Error("Connected workspace is missing. Restore its directory before syncing.");
|
|
1446
|
+
if ((await workspaceGitInfo(root)).tracked) return true;
|
|
1336
1447
|
const ignore2 = await ignorePlan(root);
|
|
1337
1448
|
await mkdir2(root);
|
|
1338
1449
|
try {
|
|
@@ -1352,7 +1463,7 @@ function completePreparation(state, root) {
|
|
|
1352
1463
|
|
|
1353
1464
|
// apps/client/runtime/log.ts
|
|
1354
1465
|
import { appendFileSync, mkdirSync as mkdirSync4, renameSync as renameSync2, statSync, unlinkSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1355
|
-
import { join as
|
|
1466
|
+
import { join as join6 } from "node:path";
|
|
1356
1467
|
import { createHash as createHash3 } from "node:crypto";
|
|
1357
1468
|
function rootKey(root) {
|
|
1358
1469
|
return createHash3("sha256").update(root).digest("hex").slice(0, 16);
|
|
@@ -1368,9 +1479,9 @@ function diagnostic(error) {
|
|
|
1368
1479
|
var ClientLog = class {
|
|
1369
1480
|
constructor(directory, limit = 1024 * 1024) {
|
|
1370
1481
|
this.limit = limit;
|
|
1371
|
-
const logs =
|
|
1482
|
+
const logs = join6(directory, "logs");
|
|
1372
1483
|
mkdirSync4(logs, { recursive: true, mode: 448 });
|
|
1373
|
-
this.path =
|
|
1484
|
+
this.path = join6(logs, "client.jsonl");
|
|
1374
1485
|
}
|
|
1375
1486
|
limit;
|
|
1376
1487
|
path;
|
|
@@ -1427,43 +1538,43 @@ var ClientLog = class {
|
|
|
1427
1538
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
1428
1539
|
|
|
1429
1540
|
// packages/protocol/devices.ts
|
|
1430
|
-
import { z as
|
|
1431
|
-
var CredentialSchema =
|
|
1432
|
-
var EnrollmentSchema =
|
|
1433
|
-
operationId:
|
|
1541
|
+
import { z as z12 } from "zod";
|
|
1542
|
+
var CredentialSchema = z12.string().regex(/^tmy_[a-f0-9]{64}$/);
|
|
1543
|
+
var EnrollmentSchema = z12.object({
|
|
1544
|
+
operationId: z12.uuid(),
|
|
1434
1545
|
projectId: NameSchema,
|
|
1435
1546
|
memberId: NameSchema,
|
|
1436
1547
|
deviceId: DeviceIdSchema,
|
|
1437
1548
|
credential: CredentialSchema
|
|
1438
1549
|
}).strict();
|
|
1439
|
-
var EffectivePolicySchema =
|
|
1440
|
-
checkIntervalSeconds:
|
|
1441
|
-
fileSettleSeconds:
|
|
1550
|
+
var EffectivePolicySchema = z12.object({
|
|
1551
|
+
checkIntervalSeconds: z12.number().min(1).max(86400),
|
|
1552
|
+
fileSettleSeconds: z12.number().min(0).max(86400)
|
|
1442
1553
|
}).strict();
|
|
1443
|
-
var SyncSummarySchema =
|
|
1444
|
-
checkedAt:
|
|
1445
|
-
published:
|
|
1446
|
-
appliedFiles:
|
|
1447
|
-
pendingFiles:
|
|
1448
|
-
sessionSources:
|
|
1449
|
-
errors:
|
|
1554
|
+
var SyncSummarySchema = z12.object({
|
|
1555
|
+
checkedAt: z12.string(),
|
|
1556
|
+
published: z12.boolean(),
|
|
1557
|
+
appliedFiles: z12.number().int().nonnegative(),
|
|
1558
|
+
pendingFiles: z12.number().int().nonnegative(),
|
|
1559
|
+
sessionSources: z12.number().int().nonnegative(),
|
|
1560
|
+
errors: z12.array(z12.string().max(1e3)).max(20)
|
|
1450
1561
|
}).strict();
|
|
1451
|
-
var DeviceStatusSchema =
|
|
1452
|
-
clientVersion:
|
|
1562
|
+
var DeviceStatusSchema = z12.object({
|
|
1563
|
+
clientVersion: z12.string().max(64),
|
|
1453
1564
|
receivedRevision: RevisionSchema.nullable(),
|
|
1454
1565
|
appliedRevision: RevisionSchema.nullable(),
|
|
1455
1566
|
effectivePolicy: EffectivePolicySchema,
|
|
1456
|
-
overrides:
|
|
1457
|
-
configError:
|
|
1458
|
-
reportError:
|
|
1567
|
+
overrides: z12.array(z12.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
|
|
1568
|
+
configError: z12.string().max(2e3).nullable(),
|
|
1569
|
+
reportError: z12.string().max(2e3).nullable(),
|
|
1459
1570
|
lastSync: SyncSummarySchema.nullable()
|
|
1460
1571
|
}).strict();
|
|
1461
|
-
var WebDevicesSchema =
|
|
1462
|
-
devices:
|
|
1463
|
-
|
|
1572
|
+
var WebDevicesSchema = z12.object({
|
|
1573
|
+
devices: z12.array(
|
|
1574
|
+
z12.object({
|
|
1464
1575
|
memberId: NameSchema,
|
|
1465
|
-
bound:
|
|
1466
|
-
lastSeenAt:
|
|
1576
|
+
bound: z12.boolean(),
|
|
1577
|
+
lastSeenAt: z12.string().nullable(),
|
|
1467
1578
|
status: DeviceStatusSchema.nullable()
|
|
1468
1579
|
})
|
|
1469
1580
|
)
|
|
@@ -1864,9 +1975,9 @@ var SessionState = class {
|
|
|
1864
1975
|
|
|
1865
1976
|
// apps/client/state.ts
|
|
1866
1977
|
var LocalBindingSchema = BindingSchema.extend({
|
|
1867
|
-
root:
|
|
1868
|
-
serverUrl:
|
|
1869
|
-
token:
|
|
1978
|
+
root: z13.string(),
|
|
1979
|
+
serverUrl: z13.string(),
|
|
1980
|
+
token: z13.string()
|
|
1870
1981
|
});
|
|
1871
1982
|
var clientMigrations = [
|
|
1872
1983
|
String.raw`
|
|
@@ -1885,7 +1996,11 @@ var clientMigrations = [
|
|
|
1885
1996
|
projectFilesClientMigration,
|
|
1886
1997
|
sessionSourcesMigration,
|
|
1887
1998
|
atomicFilesClientMigration,
|
|
1888
|
-
sessionParserMigration
|
|
1999
|
+
sessionParserMigration,
|
|
2000
|
+
`CREATE TABLE client_workspace_replacements (
|
|
2001
|
+
root TEXT PRIMARY KEY REFERENCES connections(root), directory TEXT NOT NULL,
|
|
2002
|
+
original_identity TEXT, incoming_identity TEXT, committed INTEGER NOT NULL DEFAULT 0
|
|
2003
|
+
);`
|
|
1889
2004
|
];
|
|
1890
2005
|
var clientSchemaVersion = clientMigrations.length;
|
|
1891
2006
|
var ClientState = class extends FilePublicationState {
|
|
@@ -1904,19 +2019,19 @@ var ClientState = class extends FilePublicationState {
|
|
|
1904
2019
|
throw new Error("Client state needs an upgrade. Run teammy repair.");
|
|
1905
2020
|
if (options.readOnly) {
|
|
1906
2021
|
if (!version) throw new Error("No client state is available.");
|
|
1907
|
-
db = new DatabaseSync3(
|
|
2022
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"), { readOnly: true });
|
|
1908
2023
|
} else if (!version) {
|
|
1909
2024
|
access2?.close();
|
|
1910
2025
|
access2 = void 0;
|
|
1911
2026
|
const exclusive = options.maintenance ? void 0 : stateAccess(directory, true);
|
|
1912
2027
|
try {
|
|
1913
|
-
db = openDatabase(
|
|
2028
|
+
db = openDatabase(join7(directory, "client.sqlite3"), clientMigrations);
|
|
1914
2029
|
} finally {
|
|
1915
2030
|
exclusive?.close();
|
|
1916
2031
|
}
|
|
1917
2032
|
if (!options.maintenance) access2 = stateAccess(directory);
|
|
1918
2033
|
} else {
|
|
1919
|
-
db = new DatabaseSync3(
|
|
2034
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"));
|
|
1920
2035
|
db.exec("PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;");
|
|
1921
2036
|
}
|
|
1922
2037
|
} catch (error) {
|
|
@@ -1993,10 +2108,10 @@ async function stateLocation(root, directory) {
|
|
|
1993
2108
|
// apps/client/sessions/setup.ts
|
|
1994
2109
|
import { execFile as execFile2 } from "node:child_process";
|
|
1995
2110
|
import { promisify as promisify2 } from "node:util";
|
|
1996
|
-
import { access, mkdir as mkdir5, readFile as readFile2, writeFile, realpath as realpath4 } from "node:fs/promises";
|
|
1997
|
-
import { constants as
|
|
2111
|
+
import { access, mkdir as mkdir5, readFile as readFile2, writeFile as writeFile2, realpath as realpath4 } from "node:fs/promises";
|
|
2112
|
+
import { constants as constants3 } from "node:fs";
|
|
1998
2113
|
import { homedir } from "node:os";
|
|
1999
|
-
import { dirname as dirname6, join as
|
|
2114
|
+
import { dirname as dirname6, join as join11, resolve as resolve4 } from "node:path";
|
|
2000
2115
|
import { createHash as createHash6 } from "node:crypto";
|
|
2001
2116
|
|
|
2002
2117
|
// apps/client/sessions/normalize.ts
|
|
@@ -2166,8 +2281,8 @@ var JsonlNormalizer = class {
|
|
|
2166
2281
|
|
|
2167
2282
|
// apps/client/sessions/registration.ts
|
|
2168
2283
|
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2169
|
-
import { join as
|
|
2170
|
-
var registrationPath = (directory, root) =>
|
|
2284
|
+
import { join as join8 } from "node:path";
|
|
2285
|
+
var registrationPath = (directory, root) => join8(directory, "hook-registrations", rootKey(root) + ".json");
|
|
2171
2286
|
function readRegistration(directory, root) {
|
|
2172
2287
|
const path = registrationPath(directory, root);
|
|
2173
2288
|
if (!existsSync3(path)) return null;
|
|
@@ -2180,16 +2295,16 @@ function saveRegistration(directory, value) {
|
|
|
2180
2295
|
writePrivateJson(registrationPath(directory, value.root), value);
|
|
2181
2296
|
}
|
|
2182
2297
|
function registrations(directory, onError) {
|
|
2183
|
-
const path =
|
|
2298
|
+
const path = join8(directory, "hook-registrations");
|
|
2184
2299
|
if (!existsSync3(path)) return [];
|
|
2185
2300
|
return readdirSync2(path).filter((name2) => /^[a-f0-9]{16}\.json$/.test(name2)).flatMap((name2) => {
|
|
2186
2301
|
try {
|
|
2187
|
-
const value = JSON.parse(readFileSync3(
|
|
2302
|
+
const value = JSON.parse(readFileSync3(join8(path, name2), "utf8"));
|
|
2188
2303
|
const registration = readRegistration(directory, value.root);
|
|
2189
2304
|
if (!registration) throw new Error("Missing Hook registration.");
|
|
2190
2305
|
return [registration];
|
|
2191
2306
|
} catch (error) {
|
|
2192
|
-
onError(
|
|
2307
|
+
onError(join8(path, name2), error);
|
|
2193
2308
|
return [];
|
|
2194
2309
|
}
|
|
2195
2310
|
});
|
|
@@ -2197,12 +2312,12 @@ function registrations(directory, onError) {
|
|
|
2197
2312
|
|
|
2198
2313
|
// apps/client/root-lock.ts
|
|
2199
2314
|
import { createHash as createHash5 } from "node:crypto";
|
|
2200
|
-
import { join as
|
|
2315
|
+
import { join as join9 } from "node:path";
|
|
2201
2316
|
function acquireRootLock(root, stateDirectory) {
|
|
2202
2317
|
let lock;
|
|
2203
2318
|
try {
|
|
2204
2319
|
lock = openDatabase(
|
|
2205
|
-
|
|
2320
|
+
join9(stateDirectory, "locks", `${createHash5("sha256").update(root).digest("hex")}.sqlite3`),
|
|
2206
2321
|
[],
|
|
2207
2322
|
0
|
|
2208
2323
|
);
|
|
@@ -2221,29 +2336,29 @@ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
|
2221
2336
|
|
|
2222
2337
|
// apps/client/sessions/pi-extension.ts
|
|
2223
2338
|
import { mkdir as mkdir4, readFile } from "node:fs/promises";
|
|
2224
|
-
import { dirname as dirname5, basename as basename2, join as
|
|
2339
|
+
import { dirname as dirname5, basename as basename2, join as join10, resolve as resolve3 } from "node:path";
|
|
2225
2340
|
var marker = (directory) => `// Teammy Pi collection ${rootKey(directory)}`;
|
|
2226
2341
|
function piPath(path, home, cwd) {
|
|
2227
2342
|
return resolve3(
|
|
2228
2343
|
cwd,
|
|
2229
|
-
path === "~" ? home : path.startsWith("~/") ?
|
|
2344
|
+
path === "~" ? home : path.startsWith("~/") ? join10(home, path.slice(2)) : path
|
|
2230
2345
|
);
|
|
2231
2346
|
}
|
|
2232
2347
|
async function piDiscoveryDirectories(agentDirectory, project, home, env) {
|
|
2233
2348
|
let configured;
|
|
2234
|
-
for (const path of [
|
|
2349
|
+
for (const path of [join10(agentDirectory, "settings.json"), join10(project, ".pi/settings.json")]) {
|
|
2235
2350
|
try {
|
|
2236
2351
|
const settings = JSON.parse(await readFile(path, "utf8"));
|
|
2237
2352
|
if (typeof settings?.sessionDir === "string" && settings.sessionDir)
|
|
2238
2353
|
configured = settings.sessionDir;
|
|
2239
2354
|
} catch (error) {
|
|
2240
|
-
if (!
|
|
2355
|
+
if (!missing2(error)) throw new Error(`Unable to read Pi session directory setting: ${path}`);
|
|
2241
2356
|
}
|
|
2242
2357
|
}
|
|
2243
2358
|
configured = env.PI_CODING_AGENT_SESSION_DIR || configured;
|
|
2244
2359
|
return [
|
|
2245
2360
|
.../* @__PURE__ */ new Set([
|
|
2246
|
-
|
|
2361
|
+
join10(agentDirectory, "sessions"),
|
|
2247
2362
|
...configured ? [piPath(configured, home, project)] : []
|
|
2248
2363
|
])
|
|
2249
2364
|
];
|
|
@@ -2264,7 +2379,7 @@ export default function teammyCollection(pi) {
|
|
|
2264
2379
|
transcript_path: file,
|
|
2265
2380
|
});
|
|
2266
2381
|
const child = spawn(${JSON.stringify(process.execPath)}, [
|
|
2267
|
-
${JSON.stringify(
|
|
2382
|
+
${JSON.stringify(join10(dirname5(entry), "session-hook.js"))},
|
|
2268
2383
|
'--agent', 'pi', '--state-dir', ${JSON.stringify(directory)},
|
|
2269
2384
|
'--integration-file', ${JSON.stringify(path)},
|
|
2270
2385
|
], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true, shell: false });
|
|
@@ -2287,13 +2402,13 @@ export default function teammyCollection(pi) {
|
|
|
2287
2402
|
`;
|
|
2288
2403
|
}
|
|
2289
2404
|
async function installPiExtension(directory, registry, agentDirectory, entry) {
|
|
2290
|
-
const path =
|
|
2405
|
+
const path = join10(agentDirectory, "extensions", `teammy-${rootKey(directory)}.js`);
|
|
2291
2406
|
await mkdir4(dirname5(path), { recursive: true });
|
|
2292
2407
|
let original = null;
|
|
2293
2408
|
try {
|
|
2294
2409
|
original = await readStable(dirname5(path), basename2(path));
|
|
2295
2410
|
} catch (error) {
|
|
2296
|
-
if (!
|
|
2411
|
+
if (!missing2(error)) throw error;
|
|
2297
2412
|
}
|
|
2298
2413
|
if (original && !original.bytes.toString("utf8").startsWith(marker(directory) + "\n"))
|
|
2299
2414
|
throw new Error(`Pi extension is not owned by Teammy: ${path}`);
|
|
@@ -2316,7 +2431,7 @@ async function removePiExtension(directory, path) {
|
|
|
2316
2431
|
await removeFile(dirname5(path), basename2(path), original.signature);
|
|
2317
2432
|
return true;
|
|
2318
2433
|
} catch (error) {
|
|
2319
|
-
if (
|
|
2434
|
+
if (missing2(error)) return false;
|
|
2320
2435
|
throw error;
|
|
2321
2436
|
}
|
|
2322
2437
|
}
|
|
@@ -2324,7 +2439,7 @@ async function removePiExtension(directory, path) {
|
|
|
2324
2439
|
// apps/client/sessions/setup.ts
|
|
2325
2440
|
var exec2 = promisify2(execFile2);
|
|
2326
2441
|
var quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
2327
|
-
async function exists(path, mode =
|
|
2442
|
+
async function exists(path, mode = constants3.F_OK) {
|
|
2328
2443
|
try {
|
|
2329
2444
|
await access(path, mode);
|
|
2330
2445
|
return true;
|
|
@@ -2335,7 +2450,7 @@ async function exists(path, mode = constants2.F_OK) {
|
|
|
2335
2450
|
async function setupSessions(state, root, directory, options = {}) {
|
|
2336
2451
|
await realpath4(root);
|
|
2337
2452
|
if (!state.binding(root)) throw new Error("Connect this root before installing hooks.");
|
|
2338
|
-
const lock = acquireRootLock("hooks",
|
|
2453
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2339
2454
|
try {
|
|
2340
2455
|
const prior = readRegistration(directory, root);
|
|
2341
2456
|
if (prior && !prior.enabled && !options.enable)
|
|
@@ -2355,7 +2470,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2355
2470
|
{
|
|
2356
2471
|
agent: "claude-code",
|
|
2357
2472
|
executables: ["claude"],
|
|
2358
|
-
storage: env.CLAUDE_CONFIG_DIR ??
|
|
2473
|
+
storage: env.CLAUDE_CONFIG_DIR ?? join11(home, ".claude"),
|
|
2359
2474
|
directories: ["projects"],
|
|
2360
2475
|
config: ".claude/settings.local.json",
|
|
2361
2476
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2363,7 +2478,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2363
2478
|
{
|
|
2364
2479
|
agent: "codex",
|
|
2365
2480
|
executables: ["codex"],
|
|
2366
|
-
storage: env.CODEX_HOME ??
|
|
2481
|
+
storage: env.CODEX_HOME ?? join11(home, ".codex"),
|
|
2367
2482
|
directories: ["sessions"],
|
|
2368
2483
|
config: ".codex/hooks.json",
|
|
2369
2484
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2372,7 +2487,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2372
2487
|
agent: "claude-code",
|
|
2373
2488
|
instance: "tclaude",
|
|
2374
2489
|
executables: ["tclaude"],
|
|
2375
|
-
storage:
|
|
2490
|
+
storage: join11(home, ".tclaude"),
|
|
2376
2491
|
directories: ["projects"],
|
|
2377
2492
|
config: ".claude/settings.local.json",
|
|
2378
2493
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2381,7 +2496,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2381
2496
|
agent: "codex",
|
|
2382
2497
|
instance: "tcodex",
|
|
2383
2498
|
executables: ["tcodex"],
|
|
2384
|
-
storage:
|
|
2499
|
+
storage: join11(home, ".tcodex"),
|
|
2385
2500
|
directories: ["sessions"],
|
|
2386
2501
|
config: ".codex/hooks.json",
|
|
2387
2502
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2389,7 +2504,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2389
2504
|
{
|
|
2390
2505
|
agent: "cursor",
|
|
2391
2506
|
executables: ["agent", "cursor-agent"],
|
|
2392
|
-
storage: env.CURSOR_CONFIG_DIR ??
|
|
2507
|
+
storage: env.CURSOR_CONFIG_DIR ?? join11(home, ".cursor"),
|
|
2393
2508
|
directories: ["chats", "projects"],
|
|
2394
2509
|
config: ".cursor/hooks.json",
|
|
2395
2510
|
events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
|
|
@@ -2397,7 +2512,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2397
2512
|
{
|
|
2398
2513
|
agent: "pi",
|
|
2399
2514
|
executables: ["pi"],
|
|
2400
|
-
storage: piPath(env.PI_CODING_AGENT_DIR ??
|
|
2515
|
+
storage: piPath(env.PI_CODING_AGENT_DIR ?? join11(home, ".pi", "agent"), home, project),
|
|
2401
2516
|
directories: []
|
|
2402
2517
|
}
|
|
2403
2518
|
];
|
|
@@ -2411,7 +2526,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2411
2526
|
for (const adapter of defaults) {
|
|
2412
2527
|
const installed = options.agents ? options.agents.includes(adapter.instance ?? adapter.agent) : await exists(adapter.storage) || (await Promise.all(
|
|
2413
2528
|
(env.PATH ?? "").split(":").flatMap(
|
|
2414
|
-
(dir) => adapter.executables.map((name2) => exists(
|
|
2529
|
+
(dir) => adapter.executables.map((name2) => exists(join11(dir, name2), constants3.X_OK))
|
|
2415
2530
|
)
|
|
2416
2531
|
)).some(Boolean);
|
|
2417
2532
|
const piProfiles = adapter.agent === "pi" ? registry.configs.filter((config) => config.kind === "pi-extension").map((config) => dirname6(dirname6(config.path))) : [];
|
|
@@ -2421,8 +2536,8 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2421
2536
|
if (!registry.excludedDiscovery?.includes(source))
|
|
2422
2537
|
sessions.discover(adapter.agent, source, project);
|
|
2423
2538
|
}
|
|
2424
|
-
if (adapter.agent === "cursor" && !registry.excludedDiscovery?.includes(
|
|
2425
|
-
sessions.discover("cursor",
|
|
2539
|
+
if (adapter.agent === "cursor" && !registry.excludedDiscovery?.includes(join11(home, ".cursor", "projects")))
|
|
2540
|
+
sessions.discover("cursor", join11(home, ".cursor", "projects"), project);
|
|
2426
2541
|
if (!configured.includes(adapter.agent)) configured.push(adapter.agent);
|
|
2427
2542
|
if (adapter.agent === "pi") {
|
|
2428
2543
|
for (const profile of /* @__PURE__ */ new Set([...piProfiles, ...installed ? [adapter.storage] : []])) {
|
|
@@ -2448,7 +2563,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2448
2563
|
}
|
|
2449
2564
|
if (!adapter.config) continue;
|
|
2450
2565
|
const configRoot = adapter.agent === "cursor" ? gitRoot : project;
|
|
2451
|
-
const path =
|
|
2566
|
+
const path = join11(configRoot, adapter.config);
|
|
2452
2567
|
if (written.has(path)) continue;
|
|
2453
2568
|
written.add(path);
|
|
2454
2569
|
try {
|
|
@@ -2457,7 +2572,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2457
2572
|
try {
|
|
2458
2573
|
original = await readStable(configRoot, adapter.config);
|
|
2459
2574
|
} catch (error) {
|
|
2460
|
-
if (!
|
|
2575
|
+
if (!missing2(error)) throw error;
|
|
2461
2576
|
}
|
|
2462
2577
|
const config = original ? JSON.parse(original.bytes.toString("utf8")) : {};
|
|
2463
2578
|
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
@@ -2467,7 +2582,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2467
2582
|
const hooks = object(config.hooks);
|
|
2468
2583
|
const marker2 = hookMarker(directory, root);
|
|
2469
2584
|
const cliEntry = options.entry ?? resolve4(process.argv[1]);
|
|
2470
|
-
const hookEntry =
|
|
2585
|
+
const hookEntry = join11(dirname6(cliEntry), "session-hook.js");
|
|
2471
2586
|
const invocation = [
|
|
2472
2587
|
process.execPath,
|
|
2473
2588
|
hookEntry,
|
|
@@ -2501,10 +2616,10 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2501
2616
|
const bytes = Buffer.from(JSON.stringify(config, null, 2) + "\n");
|
|
2502
2617
|
if (original?.bytes.equals(bytes)) continue;
|
|
2503
2618
|
if (original) {
|
|
2504
|
-
const backupDir =
|
|
2619
|
+
const backupDir = join11(directory, "hook-backups", rootKey(root));
|
|
2505
2620
|
await mkdir5(backupDir, { recursive: true, mode: 448 });
|
|
2506
2621
|
const hash2 = createHash6("sha256").update(original.bytes).digest("hex");
|
|
2507
|
-
await
|
|
2622
|
+
await writeFile2(join11(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
|
|
2508
2623
|
mode: 384
|
|
2509
2624
|
});
|
|
2510
2625
|
}
|
|
@@ -2522,7 +2637,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2522
2637
|
"info/exclude"
|
|
2523
2638
|
])).stdout.trim();
|
|
2524
2639
|
const old = await readFile2(exclude, "utf8").catch((e) => {
|
|
2525
|
-
if (
|
|
2640
|
+
if (missing2(e)) return "";
|
|
2526
2641
|
throw e;
|
|
2527
2642
|
});
|
|
2528
2643
|
const rule = "/" + relativePath.replace(/[\\*?[\]#! ]/g, "\\$&");
|
|
@@ -2533,7 +2648,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2533
2648
|
line: "# teammy-hook-" + rootKey(directory) + "-" + rootKey(root) + "\n" + rule
|
|
2534
2649
|
};
|
|
2535
2650
|
saveRegistration(directory, registry);
|
|
2536
|
-
await
|
|
2651
|
+
await writeFile2(
|
|
2537
2652
|
exclude,
|
|
2538
2653
|
old + (old && !old.endsWith("\n") ? "\n" : "") + owned.exclude.line + "\n"
|
|
2539
2654
|
);
|
|
@@ -2554,7 +2669,7 @@ function hookMarker(directory, root) {
|
|
|
2554
2669
|
return `teammy-session-${rootKey(directory)}-${rootKey(root)}`;
|
|
2555
2670
|
}
|
|
2556
2671
|
function setDiscoveryPreference(directory, root, source, enabled) {
|
|
2557
|
-
const lock = acquireRootLock("hooks",
|
|
2672
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2558
2673
|
try {
|
|
2559
2674
|
const registry = readRegistration(directory, root);
|
|
2560
2675
|
if (!registry) return;
|
|
@@ -2595,7 +2710,7 @@ async function legacyRegistrations(directory, explicitRoot) {
|
|
|
2595
2710
|
const roots = new Set(explicitRoot ? [explicitRoot] : []);
|
|
2596
2711
|
let db;
|
|
2597
2712
|
try {
|
|
2598
|
-
db = new DatabaseSync4(
|
|
2713
|
+
db = new DatabaseSync4(join11(directory, "client.sqlite3"), { readOnly: true });
|
|
2599
2714
|
for (const row of db.prepare("SELECT root FROM connections WHERE binding_json IS NOT NULL").all())
|
|
2600
2715
|
if (!explicitRoot || row.root === explicitRoot) roots.add(String(row.root));
|
|
2601
2716
|
} catch {
|
|
@@ -2615,15 +2730,15 @@ async function legacyRegistrations(directory, explicitRoot) {
|
|
|
2615
2730
|
} catch {
|
|
2616
2731
|
}
|
|
2617
2732
|
const configs = [
|
|
2618
|
-
{ path:
|
|
2619
|
-
{ path:
|
|
2620
|
-
{ path:
|
|
2733
|
+
{ path: join11(project, ".claude/settings.local.json"), agent: "claude-code", created: false },
|
|
2734
|
+
{ path: join11(project, ".codex/hooks.json"), agent: "codex", created: false },
|
|
2735
|
+
{ path: join11(gitRoot, ".cursor/hooks.json"), agent: "cursor", created: false }
|
|
2621
2736
|
];
|
|
2622
2737
|
saveRegistration(directory, { format: 1, root, projectDir: project, enabled: true, configs });
|
|
2623
2738
|
}
|
|
2624
2739
|
}
|
|
2625
2740
|
async function teardownSessions(directory, root) {
|
|
2626
|
-
const lock = acquireRootLock("hooks",
|
|
2741
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2627
2742
|
const issues = [];
|
|
2628
2743
|
let removed = 0;
|
|
2629
2744
|
try {
|
|
@@ -2678,11 +2793,11 @@ async function teardownSessions(directory, root) {
|
|
|
2678
2793
|
);
|
|
2679
2794
|
if (config.exclude && !await exists(config.path)) {
|
|
2680
2795
|
const text = await readFile2(config.exclude.path, "utf8");
|
|
2681
|
-
await
|
|
2796
|
+
await writeFile2(config.exclude.path, text.replace(config.exclude.line + "\n", ""));
|
|
2682
2797
|
}
|
|
2683
2798
|
removed++;
|
|
2684
2799
|
} catch (error) {
|
|
2685
|
-
if (!
|
|
2800
|
+
if (!missing2(error))
|
|
2686
2801
|
issues.push(
|
|
2687
2802
|
`${config.path}: ${error instanceof Error ? error.message : "Hook removal failed."}`
|
|
2688
2803
|
);
|
|
@@ -2700,32 +2815,32 @@ import { parseArgs } from "node:util";
|
|
|
2700
2815
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
2701
2816
|
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
|
|
2702
2817
|
import { homedir as homedir2 } from "node:os";
|
|
2703
|
-
import { join as
|
|
2818
|
+
import { join as join13 } from "node:path";
|
|
2704
2819
|
|
|
2705
2820
|
// apps/client/sessions/hooks.ts
|
|
2706
2821
|
import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
|
|
2707
|
-
import { mkdir as mkdir6, open as
|
|
2708
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2709
|
-
import { z as
|
|
2710
|
-
var HintSchema =
|
|
2822
|
+
import { mkdir as mkdir6, open as open3, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
2823
|
+
import { isAbsolute as isAbsolute3, join as join12 } from "node:path";
|
|
2824
|
+
import { z as z14 } from "zod";
|
|
2825
|
+
var HintSchema = z14.object({
|
|
2711
2826
|
agent: AgentSchema,
|
|
2712
|
-
path:
|
|
2713
|
-
nativeId:
|
|
2714
|
-
projectDir:
|
|
2827
|
+
path: z14.string().min(1).max(4096).refine(isAbsolute3).optional(),
|
|
2828
|
+
nativeId: z14.string().min(1).max(512).optional(),
|
|
2829
|
+
projectDir: z14.string().min(1).max(4096).refine(isAbsolute3)
|
|
2715
2830
|
});
|
|
2716
2831
|
function inbox(directory, root) {
|
|
2717
|
-
return
|
|
2832
|
+
return join12(directory, "session-hints", createHash7("sha256").update(root).digest("hex"));
|
|
2718
2833
|
}
|
|
2719
2834
|
async function recordHint(directory, root, input) {
|
|
2720
2835
|
const hint = HintSchema.parse(input);
|
|
2721
2836
|
const targetDir = inbox(directory, root);
|
|
2722
2837
|
await mkdir6(targetDir, { recursive: true, mode: 448 });
|
|
2723
|
-
const target =
|
|
2838
|
+
const target = join12(
|
|
2724
2839
|
targetDir,
|
|
2725
2840
|
`${createHash7("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
|
|
2726
2841
|
);
|
|
2727
2842
|
const temporary = `${target}.${randomUUID5()}.tmp`;
|
|
2728
|
-
const handle = await
|
|
2843
|
+
const handle = await open3(temporary, "wx", 384);
|
|
2729
2844
|
try {
|
|
2730
2845
|
await handle.writeFile(JSON.stringify(hint));
|
|
2731
2846
|
} finally {
|
|
@@ -2746,9 +2861,9 @@ async function consumeHints(directory, root, sessions, refresh) {
|
|
|
2746
2861
|
for await (const entry of dir) {
|
|
2747
2862
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
2748
2863
|
if (count++ >= 50) break;
|
|
2749
|
-
const path =
|
|
2864
|
+
const path = join12(inbox(directory, root), entry.name);
|
|
2750
2865
|
try {
|
|
2751
|
-
const handle = await
|
|
2866
|
+
const handle = await open3(path, "r");
|
|
2752
2867
|
let body;
|
|
2753
2868
|
try {
|
|
2754
2869
|
if ((await handle.stat()).size > 16384) throw new Error("Oversized hook hint.");
|
|
@@ -2801,7 +2916,7 @@ async function runHook(args) {
|
|
|
2801
2916
|
}
|
|
2802
2917
|
});
|
|
2803
2918
|
const directory = await realpath5(
|
|
2804
|
-
resolve5(values["state-dir"] ??
|
|
2919
|
+
resolve5(values["state-dir"] ?? join13(homedir2(), ".teammy", "client"))
|
|
2805
2920
|
);
|
|
2806
2921
|
const payload = values.file ? {} : await hookPayload();
|
|
2807
2922
|
let selectedRoot = values.root;
|
|
@@ -2852,28 +2967,28 @@ async function runHook(args) {
|
|
|
2852
2967
|
// apps/client/runtime/maintenance.ts
|
|
2853
2968
|
import { mkdir as mkdir9, rename as rename4, rm as rm2, readFile as readFile4 } from "node:fs/promises";
|
|
2854
2969
|
import { existsSync as existsSync4 } from "node:fs";
|
|
2855
|
-
import { join as
|
|
2970
|
+
import { join as join16, resolve as resolve7 } from "node:path";
|
|
2856
2971
|
import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
|
|
2857
2972
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2858
2973
|
|
|
2859
2974
|
// apps/client/runtime/ipc.ts
|
|
2860
2975
|
import { createConnection } from "node:net";
|
|
2861
2976
|
import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
|
|
2862
|
-
import { join as
|
|
2863
|
-
import { z as
|
|
2864
|
-
var RequestSchema =
|
|
2865
|
-
method:
|
|
2866
|
-
root:
|
|
2977
|
+
import { join as join14 } from "node:path";
|
|
2978
|
+
import { z as z15 } from "zod";
|
|
2979
|
+
var RequestSchema = z15.object({
|
|
2980
|
+
method: z15.enum(["ping", "wake", "status", "once"]),
|
|
2981
|
+
root: z15.string().max(4096).optional()
|
|
2867
2982
|
}).strict();
|
|
2868
2983
|
function socketPath(directory) {
|
|
2869
|
-
return
|
|
2984
|
+
return join14(
|
|
2870
2985
|
"/tmp",
|
|
2871
2986
|
`teammy-${process.getuid?.() ?? "user"}-${rootKey(directory)}`,
|
|
2872
2987
|
"control.sock"
|
|
2873
2988
|
);
|
|
2874
2989
|
}
|
|
2875
2990
|
async function prepareSocketDirectory(directory) {
|
|
2876
|
-
const path =
|
|
2991
|
+
const path = join14(socketPath(directory), "..");
|
|
2877
2992
|
await mkdir7(path, { recursive: true, mode: 448 });
|
|
2878
2993
|
const stat2 = await lstat3(path);
|
|
2879
2994
|
if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.())
|
|
@@ -2959,8 +3074,8 @@ async function runningDaemon(directory) {
|
|
|
2959
3074
|
import { execFile as execFile3 } from "node:child_process";
|
|
2960
3075
|
import { promisify as promisify3 } from "node:util";
|
|
2961
3076
|
import { homedir as homedir3 } from "node:os";
|
|
2962
|
-
import { dirname as dirname8, join as
|
|
2963
|
-
import { mkdir as mkdir8, readFile as readFile3, writeFile as
|
|
3077
|
+
import { dirname as dirname8, join as join15, resolve as resolve6 } from "node:path";
|
|
3078
|
+
import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile3, rename as rename3, rm } from "node:fs/promises";
|
|
2964
3079
|
import { setTimeout as setTimeout3 } from "node:timers/promises";
|
|
2965
3080
|
var execute = promisify3(execFile3);
|
|
2966
3081
|
var exec3 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
|
|
@@ -2977,7 +3092,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2977
3092
|
if (platform === "darwin")
|
|
2978
3093
|
return {
|
|
2979
3094
|
id,
|
|
2980
|
-
path:
|
|
3095
|
+
path: join15(userHome, "Library", "LaunchAgents", id + ".plist"),
|
|
2981
3096
|
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
2982
3097
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2983
3098
|
<plist version="1.0"><dict><key>Label</key><string>${id}</string><key>ProgramArguments</key><array>${args.map((a) => `<string>${xml(a)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer><key>WorkingDirectory</key><string>${xml(userHome)}</string></dict></plist>
|
|
@@ -2986,7 +3101,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2986
3101
|
if (platform === "linux")
|
|
2987
3102
|
return {
|
|
2988
3103
|
id: id + ".service",
|
|
2989
|
-
path:
|
|
3104
|
+
path: join15(userHome, ".config", "systemd", "user", id + ".service"),
|
|
2990
3105
|
content: `[Unit]
|
|
2991
3106
|
Description=Teammy background synchronization
|
|
2992
3107
|
[Service]
|
|
@@ -3047,7 +3162,7 @@ async function ensureDaemon(directory, version) {
|
|
|
3047
3162
|
await mkdir8(dirname8(definition.path), { recursive: true });
|
|
3048
3163
|
if (old !== definition.content) {
|
|
3049
3164
|
const temp = definition.path + `.${process.pid}.tmp`;
|
|
3050
|
-
await
|
|
3165
|
+
await writeFile3(temp, definition.content, { mode: 384 });
|
|
3051
3166
|
await rename3(temp, definition.path);
|
|
3052
3167
|
}
|
|
3053
3168
|
if (process.platform === "darwin") {
|
|
@@ -3093,7 +3208,7 @@ async function stopDaemon(directory, remove = false) {
|
|
|
3093
3208
|
let loaded = false;
|
|
3094
3209
|
let pid;
|
|
3095
3210
|
try {
|
|
3096
|
-
const saved = JSON.parse(await readFile3(
|
|
3211
|
+
const saved = JSON.parse(await readFile3(join15(directory, "runtime", "daemon.json"), "utf8"));
|
|
3097
3212
|
if (saved.directory === directory && Number.isSafeInteger(saved.pid) && saved.pid > 0)
|
|
3098
3213
|
pid = saved.pid;
|
|
3099
3214
|
} catch {
|
|
@@ -3203,14 +3318,14 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3203
3318
|
};
|
|
3204
3319
|
let installed;
|
|
3205
3320
|
try {
|
|
3206
|
-
installed = JSON.parse(await readFile4(
|
|
3321
|
+
installed = JSON.parse(await readFile4(join16(directory, "runtime/installation.json"), "utf8"));
|
|
3207
3322
|
} catch {
|
|
3208
3323
|
}
|
|
3209
3324
|
const probe = options.daemonStartup ? { state: "absent" } : await probeDaemon(directory);
|
|
3210
3325
|
const changed = probe.state === "ready" && (probe.info.version !== version || probe.info.protocol !== CONTROL_PROTOCOL || probe.info.executable !== process.execPath || probe.info.entry !== resolve7(process.argv[1]));
|
|
3211
3326
|
const required = options.force || options.stop || options.uninstall || schema !== clientSchemaVersion || JSON.stringify(installed) !== JSON.stringify(installation) || changed || probe.state === "unresponsive" || probe.state === "incompatible" || Boolean(record && (options.resume || ["repair", "upgrade"].includes(record.reason)));
|
|
3212
3327
|
if (!required) return { maintained: false, enabled: 0, stopped: 0, issues: [] };
|
|
3213
|
-
const mutex = acquireRootLock("maintenance",
|
|
3328
|
+
const mutex = acquireRootLock("maintenance", join16(directory, "runtime"));
|
|
3214
3329
|
const log = new ClientLog(directory);
|
|
3215
3330
|
let access2;
|
|
3216
3331
|
let legacy;
|
|
@@ -3252,13 +3367,13 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3252
3367
|
);
|
|
3253
3368
|
let backup;
|
|
3254
3369
|
if (before > 0 && before < clientSchemaVersion) {
|
|
3255
|
-
const backups =
|
|
3370
|
+
const backups = join16(directory, "backups");
|
|
3256
3371
|
await mkdir9(backups, { recursive: true, mode: 448 });
|
|
3257
|
-
backup =
|
|
3372
|
+
backup = join16(backups, `before-schema-${before}.sqlite3`);
|
|
3258
3373
|
if (!existsSync4(backup)) {
|
|
3259
3374
|
const temp = backup + ".tmp";
|
|
3260
3375
|
await rm2(temp, { force: true });
|
|
3261
|
-
const db = new DatabaseSync5(
|
|
3376
|
+
const db = new DatabaseSync5(join16(directory, "client.sqlite3"));
|
|
3262
3377
|
try {
|
|
3263
3378
|
db.prepare("VACUUM INTO ?").run(temp);
|
|
3264
3379
|
} finally {
|
|
@@ -3277,7 +3392,7 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3277
3392
|
...pauseReason ? { pauseReason } : {},
|
|
3278
3393
|
...backup ? { backup } : {}
|
|
3279
3394
|
});
|
|
3280
|
-
openDatabase(
|
|
3395
|
+
openDatabase(join16(directory, "client.sqlite3"), clientMigrations).close();
|
|
3281
3396
|
state = new ClientState(directory, { maintenance: true });
|
|
3282
3397
|
const runtime = new RuntimeState(state);
|
|
3283
3398
|
if (options.intent) {
|
|
@@ -3300,16 +3415,16 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3300
3415
|
issues.push(`${root}: ${diagnostic(error)}`);
|
|
3301
3416
|
}
|
|
3302
3417
|
}
|
|
3303
|
-
writePrivateJson(
|
|
3418
|
+
writePrivateJson(join16(directory, "runtime/installation.json"), installation);
|
|
3304
3419
|
if (pauseReason)
|
|
3305
3420
|
writeMaintenance(directory, { reason: pauseReason, phase: "paused", version });
|
|
3306
3421
|
else await rm2(maintenancePath(directory), { force: true });
|
|
3307
3422
|
restart = !options.daemonStartup && !pauseReason && enabled > 0;
|
|
3308
3423
|
if (backup) {
|
|
3309
|
-
const { readdir:
|
|
3310
|
-
for (const file of await
|
|
3311
|
-
if (/^before-schema-\d+\.sqlite3$/.test(file) &&
|
|
3312
|
-
await rm2(
|
|
3424
|
+
const { readdir: readdir4 } = await import("node:fs/promises");
|
|
3425
|
+
for (const file of await readdir4(join16(directory, "backups"))) {
|
|
3426
|
+
if (/^before-schema-\d+\.sqlite3$/.test(file) && join16(directory, "backups", file) !== backup)
|
|
3427
|
+
await rm2(join16(directory, "backups", file));
|
|
3313
3428
|
}
|
|
3314
3429
|
}
|
|
3315
3430
|
log.write(
|
|
@@ -3369,7 +3484,7 @@ async function runSessionCommand(args, version) {
|
|
|
3369
3484
|
args: args.slice(1),
|
|
3370
3485
|
options: {
|
|
3371
3486
|
root: { type: "string", default: resolve8("workspaces") },
|
|
3372
|
-
"state-dir": { type: "string", default:
|
|
3487
|
+
"state-dir": { type: "string", default: join17(homedir4(), ".teammy", "client") },
|
|
3373
3488
|
agent: { type: "string" },
|
|
3374
3489
|
file: { type: "string" },
|
|
3375
3490
|
directory: { type: "string" },
|
|
@@ -3429,17 +3544,17 @@ async function runSessionCommand(args, version) {
|
|
|
3429
3544
|
} else {
|
|
3430
3545
|
const agent = AgentSchema.parse(values.agent);
|
|
3431
3546
|
const defaults = {
|
|
3432
|
-
"claude-code":
|
|
3433
|
-
process.env.CLAUDE_CONFIG_DIR ??
|
|
3547
|
+
"claude-code": join17(
|
|
3548
|
+
process.env.CLAUDE_CONFIG_DIR ?? join17(homedir4(), ".claude"),
|
|
3434
3549
|
"projects"
|
|
3435
3550
|
),
|
|
3436
|
-
codex:
|
|
3437
|
-
cursor:
|
|
3438
|
-
process.env.CURSOR_CONFIG_DIR ??
|
|
3551
|
+
codex: join17(process.env.CODEX_HOME ?? join17(homedir4(), ".codex"), "sessions"),
|
|
3552
|
+
cursor: join17(
|
|
3553
|
+
process.env.CURSOR_CONFIG_DIR ?? join17(homedir4(), ".cursor"),
|
|
3439
3554
|
"chats",
|
|
3440
3555
|
createHash8("md5").update(projectDir).digest("hex")
|
|
3441
3556
|
),
|
|
3442
|
-
pi:
|
|
3557
|
+
pi: join17(process.env.PI_CODING_AGENT_DIR ?? join17(homedir4(), ".pi", "agent"), "sessions"),
|
|
3443
3558
|
dsh: void 0
|
|
3444
3559
|
};
|
|
3445
3560
|
const path = values.directory ?? defaults[agent];
|
|
@@ -3486,74 +3601,185 @@ async function runSessionCommand(args, version) {
|
|
|
3486
3601
|
|
|
3487
3602
|
// apps/client/sync-command.ts
|
|
3488
3603
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
3489
|
-
import { setTimeout as
|
|
3604
|
+
import { setTimeout as setTimeout5 } from "node:timers/promises";
|
|
3490
3605
|
|
|
3491
3606
|
// apps/client/client-command.ts
|
|
3492
3607
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
3493
|
-
import { join as
|
|
3608
|
+
import { join as join19, resolve as resolve10 } from "node:path";
|
|
3494
3609
|
import { homedir as homedir5 } from "node:os";
|
|
3495
3610
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
3496
3611
|
|
|
3497
3612
|
// apps/client/connection.ts
|
|
3498
|
-
import { readFile as readFile5, mkdir as
|
|
3613
|
+
import { readFile as readFile5, mkdir as mkdir11 } from "node:fs/promises";
|
|
3499
3614
|
import { createInterface } from "node:readline/promises";
|
|
3500
3615
|
|
|
3501
|
-
// apps/client/
|
|
3502
|
-
import {
|
|
3503
|
-
import {
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3616
|
+
// apps/client/workspace-replacement.ts
|
|
3617
|
+
import { lstat as lstat4, mkdir as mkdir10, mkdtemp, readdir as readdir2, rename as rename5, rm as rm3 } from "node:fs/promises";
|
|
3618
|
+
import { dirname as dirname10, join as join18 } from "node:path";
|
|
3619
|
+
var ConnectionCancelled = class extends Error {
|
|
3620
|
+
constructor() {
|
|
3621
|
+
super("\u5DF2\u53D6\u6D88\u63A5\u5165\uFF0C\u539F\u5DE5\u4F5C\u533A\u672A\u6539\u52A8\u3002");
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
async function localSnapshot(root) {
|
|
3625
|
+
const entries = [];
|
|
3626
|
+
let date = 0;
|
|
3512
3627
|
try {
|
|
3513
|
-
await
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3628
|
+
const identity = await directoryIdentity(root);
|
|
3629
|
+
const pending = [""];
|
|
3630
|
+
while (pending.length) {
|
|
3631
|
+
const path = pending.pop();
|
|
3632
|
+
const stat2 = await lstat4(join18(root, path), { bigint: true });
|
|
3633
|
+
entries.push([path, signature(stat2)]);
|
|
3634
|
+
if (stat2.isFile()) date = Math.max(date, Number(stat2.mtimeMs));
|
|
3635
|
+
if (stat2.isDirectory() && !stat2.isSymbolicLink())
|
|
3636
|
+
for (const name2 of await readdir2(join18(root, path))) pending.push(join18(path, name2));
|
|
3637
|
+
}
|
|
3638
|
+
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3639
|
+
return {
|
|
3640
|
+
identity,
|
|
3641
|
+
fingerprint: JSON.stringify(entries),
|
|
3642
|
+
date: date ? new Date(date).toISOString() : null
|
|
3643
|
+
};
|
|
3518
3644
|
} catch (error) {
|
|
3519
|
-
if (
|
|
3645
|
+
if (entries.length === 0 && missing2(error))
|
|
3646
|
+
return { identity: null, fingerprint: "", date: null };
|
|
3647
|
+
throw error;
|
|
3520
3648
|
}
|
|
3521
3649
|
}
|
|
3522
|
-
async function
|
|
3523
|
-
let rules = [];
|
|
3650
|
+
async function identityIfPresent(path) {
|
|
3524
3651
|
try {
|
|
3525
|
-
|
|
3526
|
-
join17(root, LOCAL_CONFIG_NAME),
|
|
3527
|
-
constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
|
|
3528
|
-
);
|
|
3529
|
-
try {
|
|
3530
|
-
const before = await file.stat();
|
|
3531
|
-
if (!before.isFile() || before.size > 65536)
|
|
3532
|
-
throw new Error("Expected a regular configuration file no larger than 64 KiB.");
|
|
3533
|
-
const buffer = Buffer.alloc(65537);
|
|
3534
|
-
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
3535
|
-
if (bytesRead > 65536 || bytesRead !== before.size)
|
|
3536
|
-
throw new Error("Configuration exceeds the read limit or changed while reading.");
|
|
3537
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
|
|
3538
|
-
const after = await file.stat();
|
|
3539
|
-
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
3540
|
-
throw new Error("Configuration is being saved; retry on the next check.");
|
|
3541
|
-
rules = LocalConfigSchema.parse(JSON.parse(text)).files.ignore;
|
|
3542
|
-
} finally {
|
|
3543
|
-
await file.close();
|
|
3544
|
-
}
|
|
3652
|
+
return await directoryIdentity(path);
|
|
3545
3653
|
} catch (error) {
|
|
3546
|
-
if (
|
|
3654
|
+
if (missing2(error)) return null;
|
|
3655
|
+
throw error;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
async function recoverWorkspaceReplacement(state, root) {
|
|
3659
|
+
const row = state.db.prepare("SELECT * FROM client_workspace_replacements WHERE root=?").get(root);
|
|
3660
|
+
if (!row) return;
|
|
3661
|
+
const directory = String(row.directory);
|
|
3662
|
+
const previous = join18(directory, "previous");
|
|
3663
|
+
const currentIdentity = await identityIfPresent(root);
|
|
3664
|
+
if (row.committed) {
|
|
3665
|
+
if (currentIdentity !== row.incoming_identity)
|
|
3547
3666
|
throw new Error(
|
|
3548
|
-
|
|
3549
|
-
{ cause: error }
|
|
3667
|
+
"Connected workspace was replaced. Preserve the recovery directory before retrying."
|
|
3550
3668
|
);
|
|
3669
|
+
} else {
|
|
3670
|
+
const previousIdentity = await identityIfPresent(previous);
|
|
3671
|
+
if (previousIdentity) {
|
|
3672
|
+
if (previousIdentity !== row.original_identity || currentIdentity && currentIdentity !== row.incoming_identity)
|
|
3673
|
+
throw new Error(
|
|
3674
|
+
"Workspace changed during recovery. Preserve both directories before retrying."
|
|
3675
|
+
);
|
|
3676
|
+
if (currentIdentity) await rm3(root, { recursive: true });
|
|
3677
|
+
await rename5(previous, root);
|
|
3678
|
+
} else if (currentIdentity !== row.original_identity) {
|
|
3679
|
+
if (row.original_identity === null && currentIdentity === row.incoming_identity)
|
|
3680
|
+
await rm3(root, { recursive: true });
|
|
3681
|
+
else
|
|
3682
|
+
throw new Error(
|
|
3683
|
+
"Original workspace is unavailable. Preserve the recovery directory before retrying."
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3551
3686
|
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3687
|
+
await rm3(directory, { recursive: true, force: true });
|
|
3688
|
+
state.db.prepare("DELETE FROM client_workspace_replacements WHERE root=?").run(root);
|
|
3689
|
+
}
|
|
3690
|
+
async function replaceExistingWorkspace(state, root, input, client, binding, confirm) {
|
|
3691
|
+
const original = await localSnapshot(root);
|
|
3692
|
+
const git = await workspaceGitInfo(root);
|
|
3693
|
+
const shared = await client.sharedFilesConfig();
|
|
3694
|
+
const members = (await client.members()).members;
|
|
3695
|
+
const snapshots = [];
|
|
3696
|
+
let remoteDate = null;
|
|
3697
|
+
for (const owner of [.../* @__PURE__ */ new Set([...members, binding.memberId, PROJECT_FILE_OWNER])]) {
|
|
3698
|
+
let revision;
|
|
3699
|
+
let after = "";
|
|
3700
|
+
const files = [];
|
|
3701
|
+
for (; ; ) {
|
|
3702
|
+
const page = await client.fileManifest(owner, revision, after);
|
|
3703
|
+
if (revision !== void 0 && page.revision !== revision)
|
|
3704
|
+
throw new Error("Manifest changed version during pagination.");
|
|
3705
|
+
revision = page.revision;
|
|
3706
|
+
files.push(...page.files);
|
|
3707
|
+
if (page.nextCursor === null) {
|
|
3708
|
+
snapshots.push({ owner, revision, complete: page.complete, files });
|
|
3709
|
+
break;
|
|
3710
|
+
}
|
|
3711
|
+
if (page.nextCursor <= after) throw new Error("Manifest pagination did not advance.");
|
|
3712
|
+
after = page.nextCursor;
|
|
3713
|
+
}
|
|
3714
|
+
if (revision > 0) {
|
|
3715
|
+
const latest = (await client.fileChanges(owner, revision - 1)).revisions[0];
|
|
3716
|
+
if (latest?.revision === revision && (!remoteDate || latest.createdAt > remoteDate))
|
|
3717
|
+
remoteDate = latest.createdAt;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
if (!await confirm({
|
|
3721
|
+
localDate: git.date ?? original.date,
|
|
3722
|
+
localDateSource: git.date ? "git" : "files",
|
|
3723
|
+
remoteDate,
|
|
3724
|
+
fileCount: snapshots.reduce((count, item) => count + item.files.length, 0),
|
|
3725
|
+
tracked: git.tracked
|
|
3726
|
+
}))
|
|
3727
|
+
throw new ConnectionCancelled();
|
|
3728
|
+
const ignore2 = await ignorePlan(root);
|
|
3729
|
+
const directory = await mkdtemp(join18(dirname10(root), ".teammy-connect-"));
|
|
3730
|
+
const incoming = join18(directory, "incoming");
|
|
3731
|
+
try {
|
|
3732
|
+
state.db.prepare(
|
|
3733
|
+
"INSERT INTO client_workspace_replacements(root,directory,original_identity) VALUES (?,?,?)"
|
|
3734
|
+
).run(root, directory, original.identity);
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
await rm3(directory, { recursive: true, force: true });
|
|
3737
|
+
throw error;
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
await mkdir10(incoming);
|
|
3741
|
+
for (const snapshot of snapshots) {
|
|
3742
|
+
const target = snapshot.owner === PROJECT_FILE_OWNER ? incoming : join18(incoming, snapshot.owner);
|
|
3743
|
+
if (target !== incoming) await mkdir10(target);
|
|
3744
|
+
for (const file of snapshot.files) {
|
|
3745
|
+
const bytes = await client.downloadBlob(file.sha256);
|
|
3746
|
+
if (bytes.byteLength !== file.size) throw new Error(`Content size mismatch: ${file.path}`);
|
|
3747
|
+
await replaceFile(target, file.path, bytes, null);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
const identity = await directoryIdentity(incoming);
|
|
3751
|
+
state.db.prepare("UPDATE client_workspace_replacements SET incoming_identity=? WHERE root=?").run(identity, root);
|
|
3752
|
+
const current = await localSnapshot(root);
|
|
3753
|
+
if (current.identity !== original.identity || current.fingerprint !== original.fingerprint)
|
|
3754
|
+
throw new Error("\u4E0B\u8F7D\u671F\u95F4\u672C\u5730 workspaces \u5DF2\u53D8\u5316\uFF0C\u672A\u6267\u884C\u66FF\u6362\uFF1B\u8BF7\u786E\u8BA4\u6587\u4EF6\u540E\u91CD\u65B0 connect\u3002");
|
|
3755
|
+
if (original.identity) await rename5(root, join18(directory, "previous"));
|
|
3756
|
+
await rename5(incoming, root);
|
|
3757
|
+
transaction(state.db, () => {
|
|
3758
|
+
for (const snapshot of snapshots) {
|
|
3759
|
+
const prefix = snapshot.owner === PROJECT_FILE_OWNER ? "project_" : "";
|
|
3760
|
+
if (snapshot.owner === binding.memberId || prefix) {
|
|
3761
|
+
state.db.prepare(
|
|
3762
|
+
`INSERT OR REPLACE INTO ${prefix}publications(root,revision,initialized,complete) VALUES (?,?,1,?)`
|
|
3763
|
+
).run(root, snapshot.revision, Number(snapshot.complete));
|
|
3764
|
+
state.db.prepare(`DELETE FROM ${prefix}owner_files WHERE root=?`).run(root);
|
|
3765
|
+
for (const file of snapshot.files)
|
|
3766
|
+
state.db.prepare(`INSERT INTO ${prefix}owner_files VALUES (?,?,?,?)`).run(root, file.path, file.sha256, file.size);
|
|
3767
|
+
}
|
|
3768
|
+
state.db.prepare(`INSERT OR REPLACE INTO ${prefix}received_progress VALUES (?,?,?)`).run(root, snapshot.owner, snapshot.revision);
|
|
3769
|
+
state.db.prepare(`DELETE FROM ${prefix}received_files WHERE root=? AND owner=?`).run(root, snapshot.owner);
|
|
3770
|
+
for (const file of snapshot.files)
|
|
3771
|
+
state.db.prepare(`INSERT INTO ${prefix}received_files VALUES (?,?,?,?,?)`).run(root, snapshot.owner, file.path, file.sha256, file.size);
|
|
3772
|
+
}
|
|
3773
|
+
state.db.prepare("INSERT OR REPLACE INTO project_file_settings VALUES (?,?)").run(root, JSON.stringify(shared));
|
|
3774
|
+
state.db.prepare("INSERT INTO client_preparations(root,identity,input_json) VALUES (?,?,?)").run(root, identity, JSON.stringify(input));
|
|
3775
|
+
state.db.prepare("UPDATE client_workspace_replacements SET committed=1 WHERE root=?").run(root);
|
|
3776
|
+
});
|
|
3777
|
+
} catch (error) {
|
|
3778
|
+
await recoverWorkspaceReplacement(state, root);
|
|
3779
|
+
throw error;
|
|
3780
|
+
}
|
|
3781
|
+
await recoverWorkspaceReplacement(state, root);
|
|
3782
|
+
if (ignore2) await replaceFile(ignore2.parent, ".gitignore", ignore2.bytes, ignore2.signature);
|
|
3557
3783
|
}
|
|
3558
3784
|
|
|
3559
3785
|
// apps/client/connection.ts
|
|
@@ -3561,6 +3787,7 @@ async function connectProject(state, root, directory, options) {
|
|
|
3561
3787
|
const lock = acquireRootLock(root, directory);
|
|
3562
3788
|
try {
|
|
3563
3789
|
state.assertRootAvailable(root);
|
|
3790
|
+
if (options.prepare) await recoverWorkspaceReplacement(state, root);
|
|
3564
3791
|
const transport = new HubClient(options.url);
|
|
3565
3792
|
const existing = state.binding(root);
|
|
3566
3793
|
const project = new ClientProjectState(state, root);
|
|
@@ -3578,19 +3805,23 @@ async function connectProject(state, root, directory, options) {
|
|
|
3578
3805
|
"This client requires a multi-project server. Update the server before connecting."
|
|
3579
3806
|
);
|
|
3580
3807
|
let member = options.member;
|
|
3808
|
+
const input = { url: transport.url, project: options.project ?? "", member: member ?? "" };
|
|
3809
|
+
const replace = options.prepare ? await prepareWorkspace(state, root, input, Boolean(options.reconnect)) : false;
|
|
3810
|
+
let binding = existing;
|
|
3811
|
+
let token = existing?.token;
|
|
3812
|
+
let enrolled = false;
|
|
3581
3813
|
if (existing && !options.reconnect && !options.tokenFile) {
|
|
3582
3814
|
if (member && member !== existing.memberId)
|
|
3583
3815
|
throw new Error("This root belongs to a different member.");
|
|
3584
3816
|
const client2 = new HubClient(existing.serverUrl, 1e4, existing);
|
|
3585
3817
|
await client2.members();
|
|
3586
3818
|
} else if (options.tokenFile) {
|
|
3587
|
-
|
|
3819
|
+
token = CredentialSchema.parse((await readFile5(options.tokenFile, "utf8")).trim());
|
|
3588
3820
|
const deviceId = state.deviceFor(root);
|
|
3589
3821
|
const client2 = new HubClient(options.url, 1e4, { token, deviceId });
|
|
3590
|
-
|
|
3591
|
-
if (options.project &&
|
|
3822
|
+
binding = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
|
|
3823
|
+
if (options.project && binding.workspaceId !== options.project)
|
|
3592
3824
|
throw new Error("Token belongs to a different project.");
|
|
3593
|
-
state.saveBinding(root, client2.url, token, binding2);
|
|
3594
3825
|
} else {
|
|
3595
3826
|
member ??= existing?.memberId;
|
|
3596
3827
|
if (!member && process.stdin.isTTY) {
|
|
@@ -3617,12 +3848,26 @@ async function connectProject(state, root, directory, options) {
|
|
|
3617
3848
|
projectId,
|
|
3618
3849
|
Boolean(options.reconnect)
|
|
3619
3850
|
);
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3851
|
+
binding = await transport.enroll(request);
|
|
3852
|
+
token = request.credential;
|
|
3853
|
+
enrolled = true;
|
|
3623
3854
|
}
|
|
3624
|
-
|
|
3625
|
-
const client = new HubClient(
|
|
3855
|
+
if (!binding || !token) throw new Error("Connection credentials are unavailable.");
|
|
3856
|
+
const client = new HubClient(transport.url, 1e4, { ...binding, token });
|
|
3857
|
+
if (replace) {
|
|
3858
|
+
if (!options.confirmReplacement)
|
|
3859
|
+
throw new Error("Existing workspaces requires confirmation before connecting.");
|
|
3860
|
+
await replaceExistingWorkspace(
|
|
3861
|
+
state,
|
|
3862
|
+
root,
|
|
3863
|
+
input,
|
|
3864
|
+
client,
|
|
3865
|
+
binding,
|
|
3866
|
+
options.confirmReplacement
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
const localBinding = state.saveBinding(root, transport.url, token, binding);
|
|
3870
|
+
if (enrolled) project.completeEnrollment();
|
|
3626
3871
|
if (info.capabilities.includes("project-config")) {
|
|
3627
3872
|
try {
|
|
3628
3873
|
await applyProjectFile(project, await client.projectConfig());
|
|
@@ -3633,9 +3878,9 @@ async function connectProject(state, root, directory, options) {
|
|
|
3633
3878
|
}
|
|
3634
3879
|
const own = await metadata(root, binding.memberId);
|
|
3635
3880
|
if (own && !own.directory) throw new Error("Member path is not a directory.");
|
|
3636
|
-
if (!own) await
|
|
3881
|
+
if (!own) await mkdir11(await safePath(root, binding.memberId));
|
|
3637
3882
|
await ensureLocalConfig(root);
|
|
3638
|
-
const { token: _, ...visible } =
|
|
3883
|
+
const { token: _, ...visible } = localBinding;
|
|
3639
3884
|
return { ...visible, configRevision: project.cached()?.config.configRevision ?? null };
|
|
3640
3885
|
} finally {
|
|
3641
3886
|
lock.close();
|
|
@@ -3658,6 +3903,16 @@ function result(title, detail) {
|
|
|
3658
3903
|
function json(value) {
|
|
3659
3904
|
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
3660
3905
|
}
|
|
3906
|
+
function browserPrompt(url) {
|
|
3907
|
+
const ansi = process.stderr.isTTY && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
|
|
3908
|
+
const title = "\u5728\u7F51\u9875\u786E\u8BA4\u8EAB\u4EFD";
|
|
3909
|
+
const link2 = redact(url);
|
|
3910
|
+
return ansi ? `\x1B[1m${title}\x1B[22m
|
|
3911
|
+
\x1B[1;4m${link2}\x1B[0m
|
|
3912
|
+
` : `${title}
|
|
3913
|
+
${link2}
|
|
3914
|
+
`;
|
|
3915
|
+
}
|
|
3661
3916
|
|
|
3662
3917
|
// apps/client/runtime/diagnostics.ts
|
|
3663
3918
|
import { resolve as resolve9 } from "node:path";
|
|
@@ -3738,10 +3993,53 @@ async function runDiagnostics(command, options, version) {
|
|
|
3738
3993
|
return issue ? 1 : 0;
|
|
3739
3994
|
}
|
|
3740
3995
|
|
|
3996
|
+
// apps/client/browser-connect.ts
|
|
3997
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID6 } from "node:crypto";
|
|
3998
|
+
import { hostname } from "node:os";
|
|
3999
|
+
import { basename as basename3, dirname as dirname11 } from "node:path";
|
|
4000
|
+
import { spawn } from "node:child_process";
|
|
4001
|
+
import { setTimeout as setTimeout4 } from "node:timers/promises";
|
|
4002
|
+
function openBrowser(url) {
|
|
4003
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return;
|
|
4004
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : void 0;
|
|
4005
|
+
if (!command) return;
|
|
4006
|
+
const child = spawn(command, [url], { detached: true, stdio: "ignore" });
|
|
4007
|
+
child.on("error", () => {
|
|
4008
|
+
});
|
|
4009
|
+
child.unref();
|
|
4010
|
+
}
|
|
4011
|
+
async function browserConnectMember(state, root, client, projectId, options = {}) {
|
|
4012
|
+
const info = await client.serverInfo();
|
|
4013
|
+
if (!info.capabilities.includes("browser-connect"))
|
|
4014
|
+
throw new Error("\u670D\u52A1\u7AEF\u5C1A\u672A\u652F\u6301\u7F51\u9875\u786E\u8BA4\u8EAB\u4EFD\u3002\u8BF7\u5148\u4F7F\u7528 --member \u7528\u6237ID\uFF0C\u6216\u5347\u7EA7\u670D\u52A1\u7AEF\u3002");
|
|
4015
|
+
const secret = randomBytes2(32).toString("hex");
|
|
4016
|
+
const started = await client.startBrowserConnect({
|
|
4017
|
+
id: randomUUID6(),
|
|
4018
|
+
projectId,
|
|
4019
|
+
deviceId: state.deviceFor(root),
|
|
4020
|
+
secret,
|
|
4021
|
+
label: `${hostname()} \xB7 ${basename3(dirname11(root))}`.slice(0, 255)
|
|
4022
|
+
});
|
|
4023
|
+
const url = new URL(`connect?request=${encodeURIComponent(started.id)}`, client.url).href;
|
|
4024
|
+
(options.write ?? ((text) => process.stderr.write(text)))(browserPrompt(url));
|
|
4025
|
+
(options.open ?? openBrowser)(url);
|
|
4026
|
+
while (Date.now() < started.expiresAt) {
|
|
4027
|
+
options.signal?.throwIfAborted();
|
|
4028
|
+
try {
|
|
4029
|
+
const status = await client.pollBrowserConnect(started.id, secret);
|
|
4030
|
+
if (status.state === "approved") return status.memberId;
|
|
4031
|
+
} catch (error) {
|
|
4032
|
+
if (error instanceof HubRequestError && error.status < 500) throw error;
|
|
4033
|
+
}
|
|
4034
|
+
await setTimeout4(options.intervalMs ?? 2e3, void 0, { signal: options.signal });
|
|
4035
|
+
}
|
|
4036
|
+
throw new Error("\u7F51\u9875\u786E\u8BA4\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C teammy connect\u3002");
|
|
4037
|
+
}
|
|
4038
|
+
|
|
3741
4039
|
// apps/client/client-command.ts
|
|
3742
4040
|
var locationOptions = {
|
|
3743
4041
|
root: { type: "string", default: resolve10("workspaces") },
|
|
3744
|
-
"state-dir": { type: "string", default:
|
|
4042
|
+
"state-dir": { type: "string", default: join19(homedir5(), ".teammy", "client") },
|
|
3745
4043
|
json: { type: "boolean" },
|
|
3746
4044
|
help: { type: "boolean", short: "h" }
|
|
3747
4045
|
};
|
|
@@ -3756,12 +4054,13 @@ async function runClientCommand(command, args, version) {
|
|
|
3756
4054
|
"token-file": { type: "string" },
|
|
3757
4055
|
reconnect: { type: "boolean" },
|
|
3758
4056
|
"no-start": { type: "boolean" },
|
|
4057
|
+
yes: { type: "boolean", short: "y" },
|
|
3759
4058
|
all: { type: "boolean" }
|
|
3760
4059
|
}
|
|
3761
4060
|
});
|
|
3762
4061
|
if (values.help) {
|
|
3763
4062
|
process.stdout.write(
|
|
3764
|
-
command === "connect" ? "Usage: teammy connect --url URL --project ID
|
|
4063
|
+
command === "connect" ? "Usage: teammy connect --url URL --project ID [--root DIR]\n\nOpens a browser to confirm your user ID; --member ID remains available for scripts.\nCreates ./workspaces or confirms replacing an existing checkout with the server version.\nAdds Git ignore rules without changing tracked files in the index, then starts background sync.\nAdvanced: --yes (confirm replacement), --no-start, --reconnect (stop sync first), --token-file FILE, --state-dir DIR, --json\n" : `Usage: teammy ${command} [--root DIR] [--state-dir DIR] [--json]${command === "logs" ? " [--all]" : ""}
|
|
3765
4064
|
`
|
|
3766
4065
|
);
|
|
3767
4066
|
return 0;
|
|
@@ -3782,29 +4081,42 @@ async function runClientCommand(command, args, version) {
|
|
|
3782
4081
|
const transport = new HubClient(url);
|
|
3783
4082
|
const selectedProject = values.project ?? (values.reconnect ? existing?.workspaceId : void 0);
|
|
3784
4083
|
const project = selectedProject === void 0 && values["token-file"] ? void 0 : NameSchema.parse(selectedProject);
|
|
3785
|
-
let member = values.member ??
|
|
3786
|
-
if (!member && !values["token-file"] && process.stdin.isTTY) {
|
|
3787
|
-
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
3788
|
-
try {
|
|
3789
|
-
member = (await rl.question("Member ID: ")).trim();
|
|
3790
|
-
} finally {
|
|
3791
|
-
rl.close();
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
if (!values["token-file"]) member = NameSchema.parse(member);
|
|
3795
|
-
await prepareWorkspace(
|
|
3796
|
-
state,
|
|
3797
|
-
root,
|
|
3798
|
-
{ url: transport.url, project: project ?? "", member: member ?? "" },
|
|
3799
|
-
Boolean(values.reconnect)
|
|
3800
|
-
);
|
|
4084
|
+
let member = values.member ?? existing?.memberId;
|
|
3801
4085
|
if (!values["no-start"]) await checkServiceSupport();
|
|
4086
|
+
if (!member && !values["token-file"])
|
|
4087
|
+
member = await browserConnectMember(state, root, transport, project);
|
|
4088
|
+
if (!values["token-file"]) member = NameSchema.parse(member);
|
|
3802
4089
|
const connected = await connectProject(state, root, directory, {
|
|
3803
4090
|
url: transport.url,
|
|
3804
4091
|
project,
|
|
3805
4092
|
member,
|
|
3806
4093
|
tokenFile: values["token-file"],
|
|
3807
|
-
reconnect: values.reconnect
|
|
4094
|
+
reconnect: values.reconnect,
|
|
4095
|
+
prepare: true,
|
|
4096
|
+
confirmReplacement: async (preview) => {
|
|
4097
|
+
const date = (value) => value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "\u6682\u65E0\u8BB0\u5F55";
|
|
4098
|
+
const prompt = [
|
|
4099
|
+
"\u5C06\u4F7F\u7528\u670D\u52A1\u5668\u7684\u5DE5\u4F5C\u533A workspaces/ \u66FF\u6362\u672C\u5730\u5185\u5BB9\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",
|
|
4100
|
+
` \u670D\u52A1\u5668 workspaces/ \u6700\u540E\u66F4\u65B0\u65F6\u95F4\uFF1A${date(preview.remoteDate)}`,
|
|
4101
|
+
` \u672C\u5730 workspaces/ \u6700\u540E\u66F4\u65B0\u65F6\u95F4\uFF1A${date(preview.localDate)}`,
|
|
4102
|
+
"[Y/n] "
|
|
4103
|
+
].join("\n");
|
|
4104
|
+
if (!values.yes) {
|
|
4105
|
+
if (!process.stdin.isTTY || values.json)
|
|
4106
|
+
throw new Error(
|
|
4107
|
+
"\u5DF2\u6709 workspaces \u9700\u8981\u786E\u8BA4\u66FF\u6362\u3002\u8BF7\u5728\u4EA4\u4E92\u7EC8\u7AEF\u91CD\u65B0\u8FD0\u884C\uFF0C\u6216\u660E\u786E\u4F20\u5165 --yes\u3002"
|
|
4108
|
+
);
|
|
4109
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
4110
|
+
try {
|
|
4111
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
4112
|
+
if (!["", "y", "yes", "\u662F"].includes(answer)) return false;
|
|
4113
|
+
} finally {
|
|
4114
|
+
rl.close();
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
if (!values.json) result("\u6B63\u5728\u4E0B\u8F7D\u5DE5\u4F5C\u533A\u2026");
|
|
4118
|
+
return true;
|
|
4119
|
+
}
|
|
3808
4120
|
});
|
|
3809
4121
|
const collection = await setupSessions(state, root, directory, { enable: true });
|
|
3810
4122
|
for (const issue of collection.issues) log.write("sessions.setup.failed", root, issue);
|
|
@@ -3818,6 +4130,11 @@ async function runClientCommand(command, args, version) {
|
|
|
3818
4130
|
result("\u90E8\u5206\u5BF9\u8BDD\u91C7\u96C6 Hook \u672A\u914D\u7F6E", "\u540E\u53F0\u53D1\u73B0\u4ECD\u53EF\u7528\uFF0C\u8BE6\u60C5\u89C1 teammy logs");
|
|
3819
4131
|
return 0;
|
|
3820
4132
|
} catch (e) {
|
|
4133
|
+
if (e instanceof ConnectionCancelled) {
|
|
4134
|
+
if (values.json) json({ cancelled: true });
|
|
4135
|
+
else result(e.message);
|
|
4136
|
+
return 0;
|
|
4137
|
+
}
|
|
3821
4138
|
log.write(`${command}.failed`, root, diagnostic(e));
|
|
3822
4139
|
throw e;
|
|
3823
4140
|
} finally {
|
|
@@ -3826,16 +4143,16 @@ async function runClientCommand(command, args, version) {
|
|
|
3826
4143
|
}
|
|
3827
4144
|
|
|
3828
4145
|
// apps/client/files/synchronizer.ts
|
|
3829
|
-
import { randomUUID as
|
|
4146
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
3830
4147
|
import { watch } from "node:fs";
|
|
3831
|
-
import { lstat as
|
|
3832
|
-
import { join as
|
|
4148
|
+
import { lstat as lstat5, mkdir as mkdir13 } from "node:fs/promises";
|
|
4149
|
+
import { join as join21, sep as sep5 } from "node:path";
|
|
3833
4150
|
import { performance } from "node:perf_hooks";
|
|
3834
4151
|
|
|
3835
4152
|
// packages/storage/local-blob-store.ts
|
|
3836
|
-
import { createHash as createHash9, randomUUID as
|
|
3837
|
-
import { link, mkdir as
|
|
3838
|
-
import { dirname as
|
|
4153
|
+
import { createHash as createHash9, randomUUID as randomUUID7 } from "node:crypto";
|
|
4154
|
+
import { link, mkdir as mkdir12, open as open4, readFile as readFile6, unlink as unlink3 } from "node:fs/promises";
|
|
4155
|
+
import { dirname as dirname12, join as join20, resolve as resolve11 } from "node:path";
|
|
3839
4156
|
function hash(content) {
|
|
3840
4157
|
return createHash9("sha256").update(content).digest("hex");
|
|
3841
4158
|
}
|
|
@@ -3859,8 +4176,8 @@ var LocalBlobStore = class {
|
|
|
3859
4176
|
const bytes = Buffer.from(content);
|
|
3860
4177
|
const sha256 = hash(bytes);
|
|
3861
4178
|
const target = this.pathFor(sha256);
|
|
3862
|
-
await
|
|
3863
|
-
const temporary = `${target}.${
|
|
4179
|
+
await mkdir12(dirname12(target), { recursive: true, mode: 448 });
|
|
4180
|
+
const temporary = `${target}.${randomUUID7()}.tmp`;
|
|
3864
4181
|
const file = await open4(temporary, "wx", 384);
|
|
3865
4182
|
try {
|
|
3866
4183
|
try {
|
|
@@ -3876,7 +4193,7 @@ var LocalBlobStore = class {
|
|
|
3876
4193
|
await this.get(sha256);
|
|
3877
4194
|
}
|
|
3878
4195
|
if (process.platform !== "win32") {
|
|
3879
|
-
let directory =
|
|
4196
|
+
let directory = dirname12(target);
|
|
3880
4197
|
for (; ; ) {
|
|
3881
4198
|
const handle = await open4(directory, "r");
|
|
3882
4199
|
try {
|
|
@@ -3884,8 +4201,8 @@ var LocalBlobStore = class {
|
|
|
3884
4201
|
} finally {
|
|
3885
4202
|
await handle.close();
|
|
3886
4203
|
}
|
|
3887
|
-
if (directory ===
|
|
3888
|
-
directory =
|
|
4204
|
+
if (directory === dirname12(this.root)) break;
|
|
4205
|
+
directory = dirname12(directory);
|
|
3889
4206
|
}
|
|
3890
4207
|
}
|
|
3891
4208
|
} finally {
|
|
@@ -3907,7 +4224,7 @@ var LocalBlobStore = class {
|
|
|
3907
4224
|
}
|
|
3908
4225
|
pathFor(sha256) {
|
|
3909
4226
|
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
3910
|
-
return
|
|
4227
|
+
return join20(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
3911
4228
|
}
|
|
3912
4229
|
};
|
|
3913
4230
|
|
|
@@ -3935,8 +4252,8 @@ var PublicationQueue = class {
|
|
|
3935
4252
|
pending.batch.changes.slice(pending.nextIndex, end).flatMap((change) => change.kind === "upsert" ? [change.sha256] : [])
|
|
3936
4253
|
)
|
|
3937
4254
|
];
|
|
3938
|
-
const { missing:
|
|
3939
|
-
for (const hash2 of
|
|
4255
|
+
const { missing: missing3 } = hashes.length ? await this.transport.missingBlobs(hashes) : { missing: [] };
|
|
4256
|
+
for (const hash2 of missing3) {
|
|
3940
4257
|
if (!hashes.includes(hash2)) throw new Error("Server requested unrelated content.");
|
|
3941
4258
|
const content = await this.blobs.get(hash2);
|
|
3942
4259
|
if (!content) throw new Error("A queued content snapshot is missing from local storage.");
|
|
@@ -3973,7 +4290,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3973
4290
|
if (!binding) throw new Error("Connect this root before synchronizing.");
|
|
3974
4291
|
this.binding = binding;
|
|
3975
4292
|
this.client = new HubClient(binding.serverUrl, 3e4, binding, this.abort.signal);
|
|
3976
|
-
this.blobs = new LocalBlobStore(
|
|
4293
|
+
this.blobs = new LocalBlobStore(join21(stateDirectory, "blobs"));
|
|
3977
4294
|
this.projectScope = options.scope === "project";
|
|
3978
4295
|
this.owner = this.projectScope ? PROJECT_FILE_OWNER : binding.memberId;
|
|
3979
4296
|
this.publication = new FilePublicationState(state.db, options.scope);
|
|
@@ -3983,7 +4300,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3983
4300
|
commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch, batch.configRevision) : this.client.commitFiles(batch)
|
|
3984
4301
|
});
|
|
3985
4302
|
this.index = new SyncState(state, root, options.scope);
|
|
3986
|
-
this.ownRoot = this.projectScope ? root :
|
|
4303
|
+
this.ownRoot = this.projectScope ? root : join21(root, binding.memberId);
|
|
3987
4304
|
this.common = this.projectScope ? void 0 : new _FileSynchronizer(state, root, stateDirectory, { ...options, scope: "project" });
|
|
3988
4305
|
const started = performance.now();
|
|
3989
4306
|
const epoch = Date.now();
|
|
@@ -4100,7 +4417,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
4100
4417
|
if (pending?.batch.changes.some((change) => this.excludes(change.path)))
|
|
4101
4418
|
await this.resetPublication();
|
|
4102
4419
|
await this.initializeAuthor();
|
|
4103
|
-
const ownStat = await
|
|
4420
|
+
const ownStat = await lstat5(this.ownRoot);
|
|
4104
4421
|
if (!ownStat.isDirectory() || ownStat.isSymbolicLink())
|
|
4105
4422
|
throw new Error("Author directory is unavailable or is a symlink.");
|
|
4106
4423
|
this.listen();
|
|
@@ -4184,7 +4501,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
4184
4501
|
}
|
|
4185
4502
|
}
|
|
4186
4503
|
const ownerPath = this.projectScope ? this.root : await safePath(this.root, this.owner);
|
|
4187
|
-
await
|
|
4504
|
+
await mkdir13(ownerPath).catch((error) => {
|
|
4188
4505
|
if (error.code !== "EEXIST") throw error;
|
|
4189
4506
|
});
|
|
4190
4507
|
this.publication.initializePublication(this.root, manifest.revision, manifest.files, {
|
|
@@ -4332,7 +4649,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
4332
4649
|
return;
|
|
4333
4650
|
if (changes.length || !publication.complete) {
|
|
4334
4651
|
this.publication.enqueue(this.root, {
|
|
4335
|
-
operationId:
|
|
4652
|
+
operationId: randomUUID8(),
|
|
4336
4653
|
parentRevision: publication.revision,
|
|
4337
4654
|
changes,
|
|
4338
4655
|
...this.projectScope ? { configRevision: this.sharedConfig.revision } : {}
|
|
@@ -4380,10 +4697,10 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
4380
4697
|
}
|
|
4381
4698
|
const job = this.index.job(owner);
|
|
4382
4699
|
const ownerRoot = this.projectScope ? this.root : await safePath(this.root, owner);
|
|
4383
|
-
await
|
|
4700
|
+
await mkdir13(ownerRoot).catch((error) => {
|
|
4384
4701
|
if (error.code !== "EEXIST") throw error;
|
|
4385
4702
|
});
|
|
4386
|
-
const ownerStat = await
|
|
4703
|
+
const ownerStat = await lstat5(ownerRoot);
|
|
4387
4704
|
if (!ownerStat.isDirectory() || ownerStat.isSymbolicLink())
|
|
4388
4705
|
throw new Error("Mirror root is not an ordinary directory.");
|
|
4389
4706
|
let count = 0;
|
|
@@ -4435,8 +4752,8 @@ function message(error) {
|
|
|
4435
4752
|
}
|
|
4436
4753
|
|
|
4437
4754
|
// apps/client/sessions/synchronizer.ts
|
|
4438
|
-
import { randomUUID as
|
|
4439
|
-
import { join as
|
|
4755
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
4756
|
+
import { join as join24 } from "node:path";
|
|
4440
4757
|
import { performance as performance3 } from "node:perf_hooks";
|
|
4441
4758
|
|
|
4442
4759
|
// packages/protocol/session-key.ts
|
|
@@ -4447,14 +4764,14 @@ function sessionKey(agent, nativeId) {
|
|
|
4447
4764
|
|
|
4448
4765
|
// apps/client/sessions/source.ts
|
|
4449
4766
|
import { createReadStream, realpathSync } from "node:fs";
|
|
4450
|
-
import { stat, realpath as realpath8, readdir as
|
|
4451
|
-
import { basename as
|
|
4767
|
+
import { stat, realpath as realpath8, readdir as readdir3 } from "node:fs/promises";
|
|
4768
|
+
import { basename as basename5, dirname as dirname14, relative as relative5, isAbsolute as isAbsolute5, sep as sep6, join as join22 } from "node:path";
|
|
4452
4769
|
import { createInterface as createInterface3 } from "node:readline";
|
|
4453
4770
|
import * as zlib from "node:zlib";
|
|
4454
4771
|
|
|
4455
4772
|
// apps/client/sessions/cursor.ts
|
|
4456
4773
|
import { createHash as createHash11 } from "node:crypto";
|
|
4457
|
-
import { basename as
|
|
4774
|
+
import { basename as basename4, dirname as dirname13, resolve as resolve12 } from "node:path";
|
|
4458
4775
|
import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
|
|
4459
4776
|
import { fileURLToPath } from "node:url";
|
|
4460
4777
|
function fields(bytes) {
|
|
@@ -4503,7 +4820,7 @@ function readCursor(path, projectDir, checkCwd) {
|
|
|
4503
4820
|
if (typeof hex !== "string" || !/^(?:[a-fA-F0-9]{2})+$/.test(hex))
|
|
4504
4821
|
throw new Error("Invalid Cursor metadata.");
|
|
4505
4822
|
const meta = object(JSON.parse(Buffer.from(hex, "hex").toString("utf8")));
|
|
4506
|
-
if (typeof meta.agentId !== "string" || meta.agentId !==
|
|
4823
|
+
if (typeof meta.agentId !== "string" || meta.agentId !== basename4(dirname13(path)) || meta.subagentInfo != null)
|
|
4507
4824
|
throw new Error("Invalid or delegated Cursor session identity.");
|
|
4508
4825
|
const blob = (id) => {
|
|
4509
4826
|
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid Cursor blob reference.");
|
|
@@ -4515,7 +4832,7 @@ function readCursor(path, projectDir, checkCwd) {
|
|
|
4515
4832
|
return bytes;
|
|
4516
4833
|
};
|
|
4517
4834
|
const root = fields(blob(String(meta.latestRootBlobId)));
|
|
4518
|
-
const bucket =
|
|
4835
|
+
const bucket = basename4(dirname13(dirname13(path)));
|
|
4519
4836
|
const md5 = (cwd2) => createHash11("md5").update(resolve12(cwd2)).digest("hex");
|
|
4520
4837
|
let cwd = md5(projectDir) === bucket ? projectDir : void 0;
|
|
4521
4838
|
for (const uri of root.get(9) ?? []) {
|
|
@@ -4580,7 +4897,7 @@ function within(project, cwd) {
|
|
|
4580
4897
|
return rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute5(rel);
|
|
4581
4898
|
}
|
|
4582
4899
|
var cursorEncoded = (cwd) => cwd.replace(/^[/\\]+/, "").replace(/[/\\]/g, "-");
|
|
4583
|
-
var cursorDatabase = (path) =>
|
|
4900
|
+
var cursorDatabase = (path) => basename5(path) === "store.db";
|
|
4584
4901
|
async function sourceSignature(path, agent) {
|
|
4585
4902
|
const paths = agent === "cursor" && cursorDatabase(path) ? [path, `${path}-wal`] : [path];
|
|
4586
4903
|
const signatures = [];
|
|
@@ -4604,12 +4921,12 @@ async function sourceSignature(path, agent) {
|
|
|
4604
4921
|
return JSON.stringify(signatures);
|
|
4605
4922
|
}
|
|
4606
4923
|
async function cursorScope(path, project) {
|
|
4607
|
-
let container =
|
|
4608
|
-
while (
|
|
4609
|
-
container =
|
|
4610
|
-
if (
|
|
4924
|
+
let container = dirname14(path);
|
|
4925
|
+
while (basename5(container) !== "agent-transcripts" && dirname14(container) !== container)
|
|
4926
|
+
container = dirname14(container);
|
|
4927
|
+
if (basename5(container) !== "agent-transcripts")
|
|
4611
4928
|
throw new Error("Cursor transcript workspace cannot be verified.");
|
|
4612
|
-
const encoded =
|
|
4929
|
+
const encoded = basename5(dirname14(container));
|
|
4613
4930
|
const matches = [];
|
|
4614
4931
|
const visit = async (directory) => {
|
|
4615
4932
|
const prefix = cursorEncoded(directory);
|
|
@@ -4618,8 +4935,8 @@ async function cursorScope(path, project) {
|
|
|
4618
4935
|
return;
|
|
4619
4936
|
}
|
|
4620
4937
|
if (!encoded.startsWith(prefix + "-")) return;
|
|
4621
|
-
for (const entry of await
|
|
4622
|
-
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(
|
|
4938
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
4939
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(join22(directory, entry.name));
|
|
4623
4940
|
}
|
|
4624
4941
|
};
|
|
4625
4942
|
await visit(project);
|
|
@@ -4637,7 +4954,7 @@ async function readSource(agent, path, projectDir) {
|
|
|
4637
4954
|
});
|
|
4638
4955
|
return { snapshot: snapshot2, incomplete: false, skipped: 0, signature: signature2 };
|
|
4639
4956
|
}
|
|
4640
|
-
const cursor = agent === "cursor" ? { nativeId:
|
|
4957
|
+
const cursor = agent === "cursor" ? { nativeId: basename5(path, ".jsonl"), cwd: await cursorScope(path, projectDir) } : void 0;
|
|
4641
4958
|
const normalizer = new JsonlNormalizer(agent, cursor);
|
|
4642
4959
|
const stream = createReadStream(path, {
|
|
4643
4960
|
end: Math.max(0, Number(JSON.parse(signature2)[0][2]) - 1)
|
|
@@ -4697,7 +5014,7 @@ async function readSource(agent, path, projectDir) {
|
|
|
4697
5014
|
// apps/client/sessions/discovery.ts
|
|
4698
5015
|
import { opendir as opendir2 } from "node:fs/promises";
|
|
4699
5016
|
import { performance as performance2 } from "node:perf_hooks";
|
|
4700
|
-
import { join as
|
|
5017
|
+
import { join as join23 } from "node:path";
|
|
4701
5018
|
|
|
4702
5019
|
// apps/client/sessions/work-queue.ts
|
|
4703
5020
|
var SessionWorkQueue = class {
|
|
@@ -4736,7 +5053,7 @@ async function* candidates(directory, agent) {
|
|
|
4736
5053
|
const current = queue.shift();
|
|
4737
5054
|
const dir = await opendir2(current);
|
|
4738
5055
|
for await (const entry of dir) {
|
|
4739
|
-
const path =
|
|
5056
|
+
const path = join23(current, entry.name);
|
|
4740
5057
|
if (entry.isDirectory()) queue.push(path);
|
|
4741
5058
|
if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" || entry.name.endsWith(".jsonl") : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
|
|
4742
5059
|
yield path;
|
|
@@ -4834,7 +5151,7 @@ var SessionSynchronizer = class {
|
|
|
4834
5151
|
const binding = state.binding(root);
|
|
4835
5152
|
if (!binding) throw new Error("Connect this root before collecting sessions.");
|
|
4836
5153
|
this.client = new HubClient(binding.serverUrl, 1e4, binding, signal);
|
|
4837
|
-
this.blobs = new LocalBlobStore(
|
|
5154
|
+
this.blobs = new LocalBlobStore(join24(directory, "blobs"));
|
|
4838
5155
|
this.index = new SessionState(state, root);
|
|
4839
5156
|
this.discovery = new SessionDiscovery(this.index);
|
|
4840
5157
|
}
|
|
@@ -4985,7 +5302,7 @@ var SessionSynchronizer = class {
|
|
|
4985
5302
|
}
|
|
4986
5303
|
this.index.stage(
|
|
4987
5304
|
source.path,
|
|
4988
|
-
{ operationId:
|
|
5305
|
+
{ operationId: randomUUID9(), sessionKey: key, parentRevision: revision, blob },
|
|
4989
5306
|
observed.signature
|
|
4990
5307
|
);
|
|
4991
5308
|
await this.flush(this.index.get(source.path));
|
|
@@ -5001,10 +5318,10 @@ var SessionSynchronizer = class {
|
|
|
5001
5318
|
async flush(source) {
|
|
5002
5319
|
const request = source.pending;
|
|
5003
5320
|
try {
|
|
5004
|
-
const { missing:
|
|
5005
|
-
if (
|
|
5321
|
+
const { missing: missing3 } = await this.client.missingBlobs([request.blob.sha256]);
|
|
5322
|
+
if (missing3.some((hash2) => hash2 !== request.blob.sha256))
|
|
5006
5323
|
throw new Error("Server requested unrelated session content.");
|
|
5007
|
-
if (
|
|
5324
|
+
if (missing3.length) {
|
|
5008
5325
|
const bytes = await this.blobs.get(request.blob.sha256);
|
|
5009
5326
|
if (!bytes) throw new Error("Queued session snapshot is missing.");
|
|
5010
5327
|
const ref = await this.client.uploadBlob(request.blob.sha256, bytes);
|
|
@@ -5368,7 +5685,7 @@ async function runSyncCommand(args, version) {
|
|
|
5368
5685
|
prior = report.errors[0] ?? "";
|
|
5369
5686
|
}
|
|
5370
5687
|
if (values.once) return report.errors.length ? 1 : 0;
|
|
5371
|
-
await
|
|
5688
|
+
await setTimeout5(Math.max(100, job.nextAt - Date.now()), void 0, {
|
|
5372
5689
|
signal: abort.signal
|
|
5373
5690
|
}).catch((e) => {
|
|
5374
5691
|
if (!abort.signal.aborted) throw e;
|
|
@@ -5391,13 +5708,13 @@ async function runSyncCommand(args, version) {
|
|
|
5391
5708
|
// apps/client/maintenance-command.ts
|
|
5392
5709
|
import { parseArgs as parseArgs5 } from "node:util";
|
|
5393
5710
|
import { homedir as homedir6 } from "node:os";
|
|
5394
|
-
import { join as
|
|
5395
|
-
import { mkdir as
|
|
5711
|
+
import { join as join25, resolve as resolve13 } from "node:path";
|
|
5712
|
+
import { mkdir as mkdir14, realpath as realpath9 } from "node:fs/promises";
|
|
5396
5713
|
async function runMaintenanceCommand(command, args, version) {
|
|
5397
5714
|
const { values } = parseArgs5({
|
|
5398
5715
|
args,
|
|
5399
5716
|
options: {
|
|
5400
|
-
"state-dir": { type: "string", default:
|
|
5717
|
+
"state-dir": { type: "string", default: join25(homedir6(), ".teammy", "client") },
|
|
5401
5718
|
stop: { type: "boolean" },
|
|
5402
5719
|
json: { type: "boolean" },
|
|
5403
5720
|
help: { type: "boolean", short: "h" }
|
|
@@ -5413,7 +5730,7 @@ ${command === "repair" ? "Repair this local client and preserve project on/off s
|
|
|
5413
5730
|
return 0;
|
|
5414
5731
|
}
|
|
5415
5732
|
if (command === "uninstall" && values.stop) throw new Error("--stop is a repair option.");
|
|
5416
|
-
await
|
|
5733
|
+
await mkdir14(resolve13(values["state-dir"]), { recursive: true, mode: 448 });
|
|
5417
5734
|
const directory = await realpath9(values["state-dir"]);
|
|
5418
5735
|
const report = await prepareClient(directory, version, {
|
|
5419
5736
|
force: true,
|
|
@@ -5438,7 +5755,7 @@ ${command === "repair" ? "Repair this local client and preserve project on/off s
|
|
|
5438
5755
|
// apps/client/runtime/daemon.ts
|
|
5439
5756
|
import { createServer } from "node:net";
|
|
5440
5757
|
import { chmod as chmod2, unlink as unlink4 } from "node:fs/promises";
|
|
5441
|
-
import { join as
|
|
5758
|
+
import { join as join26 } from "node:path";
|
|
5442
5759
|
import { resolve as resolve14 } from "node:path";
|
|
5443
5760
|
async function runDaemon(directory, version) {
|
|
5444
5761
|
await prepareClient(directory, version, { daemonStartup: true });
|
|
@@ -5572,7 +5889,7 @@ async function runDaemon(directory, version) {
|
|
|
5572
5889
|
});
|
|
5573
5890
|
});
|
|
5574
5891
|
await chmod2(socketPath(directory), 384);
|
|
5575
|
-
writePrivateJson(
|
|
5892
|
+
writePrivateJson(join26(directory, "runtime", "daemon.json"), { pid: process.pid, directory });
|
|
5576
5893
|
process.once("SIGINT", stop);
|
|
5577
5894
|
process.once("SIGTERM", stop);
|
|
5578
5895
|
log.write("daemon.started", void 0, `version=${version}`);
|
|
@@ -5595,7 +5912,7 @@ async function runDaemon(directory, version) {
|
|
|
5595
5912
|
await new Promise((resolve16) => server.close(() => resolve16()));
|
|
5596
5913
|
await unlink4(socketPath(directory)).catch(() => {
|
|
5597
5914
|
});
|
|
5598
|
-
await unlink4(
|
|
5915
|
+
await unlink4(join26(directory, "runtime", "daemon.json")).catch(() => {
|
|
5599
5916
|
});
|
|
5600
5917
|
log.write("daemon.stopped");
|
|
5601
5918
|
state.close();
|
|
@@ -5612,7 +5929,7 @@ Usage:
|
|
|
5612
5929
|
teammy --version Show the installed client version
|
|
5613
5930
|
teammy server-info --url URL Check server connection and protocol capabilities
|
|
5614
5931
|
teammy server-info --help Show connection options
|
|
5615
|
-
teammy connect --url URL --project ID [--member ID | --token-file FILE] [--reconnect] [--root DIR] [--state-dir DIR]
|
|
5932
|
+
teammy connect --url URL --project ID [--member ID | --token-file FILE] [--yes] [--reconnect] [--root DIR] [--state-dir DIR]
|
|
5616
5933
|
teammy repair [--stop] Repair or pause this local background client
|
|
5617
5934
|
teammy uninstall Remove local service and hooks; keep user data
|
|
5618
5935
|
teammy status [--root DIR] [--state-dir DIR]
|
|
@@ -5622,6 +5939,7 @@ Usage:
|
|
|
5622
5939
|
|
|
5623
5940
|
Root defaults to ./workspaces; local state defaults to ~/.teammy/client.
|
|
5624
5941
|
connect starts background sync; enabled projects resume after login.
|
|
5942
|
+
Without --member, connect opens a browser to confirm your user ID.
|
|
5625
5943
|
Use --json for scripts; manage sync timing in the server dashboard.
|
|
5626
5944
|
The hub server is installed separately as @teammy/server (command: teammy-server).
|
|
5627
5945
|
`;
|