@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.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/dist/main.js +626 -308
  3. 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 z8 } from "zod";
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, z8.never());
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
- z8.object({ ok: z8.literal(true) }),
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 join16, resolve as resolve8 } from "node:path";
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 z9 } from "zod";
607
+ import { z as z10 } from "zod";
574
608
  var FilePublicationSchema = FileCommitSchema.safeExtend({
575
609
  configRevision: RevisionSchema.optional()
576
610
  }).strict();
577
- var PublishFilesSchema = z9.object({ manifest: BlobRefSchema }).strict();
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 = WAL; PRAGMA synchronous = FULL;`
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 join6, relative as relative2, isAbsolute, sep as sep2 } from "node:path";
954
- import { z as z11 } from "zod";
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 join4, resolve } from "node:path";
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 join3, relative, sep } from "node:path";
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 = join3(parent, part);
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 join3(root, ...parts);
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 (missing(error)) return null;
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 open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
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 open(directory, "r");
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 = join3(dirname3(target), `.teammy-${randomUUID2()}.tmp`);
1192
- const file = await open(temporary, "wx", 384);
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 (!missing(error)) throw error;
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 join4(await realpath2(dirname4(path)), basename(path));
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 ignorePlan(root) {
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(join4(p, ".git"));
1362
+ await lstat2(join5(p, ".git"));
1267
1363
  repo = true;
1268
1364
  break;
1269
1365
  } catch (e) {
1270
- if (!missing(e)) throw e;
1366
+ if (!missing2(e)) throw e;
1271
1367
  }
1272
1368
  if (p === dirname4(p)) break;
1273
1369
  }
1274
- if (repo) {
1275
- let tracked;
1276
- try {
1277
- tracked = (await exec("git", ["-C", parent, "--literal-pathspecs", "ls-files", "-z", "--", name2], {
1278
- maxBuffer: 1024 * 1024
1279
- })).stdout;
1280
- } catch {
1281
- throw new Error("Cannot inspect the Git index. Check Git access before connecting.");
1282
- }
1283
- if (tracked)
1284
- throw new Error(
1285
- "Git already tracks this workspace path. Remove it from the Git index yourself before connecting."
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 (!missing(e)) throw e;
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
- throw new Error(
1331
- 'Workspace path already exists. Choose another directory; for a connected workspace use "teammy sync on".'
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 join5 } from "node:path";
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 = join5(directory, "logs");
1482
+ const logs = join6(directory, "logs");
1372
1483
  mkdirSync4(logs, { recursive: true, mode: 448 });
1373
- this.path = join5(logs, "client.jsonl");
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 z10 } from "zod";
1431
- var CredentialSchema = z10.string().regex(/^tmy_[a-f0-9]{64}$/);
1432
- var EnrollmentSchema = z10.object({
1433
- operationId: z10.uuid(),
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 = z10.object({
1440
- checkIntervalSeconds: z10.number().min(1).max(86400),
1441
- fileSettleSeconds: z10.number().min(0).max(86400)
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 = z10.object({
1444
- checkedAt: z10.string(),
1445
- published: z10.boolean(),
1446
- appliedFiles: z10.number().int().nonnegative(),
1447
- pendingFiles: z10.number().int().nonnegative(),
1448
- sessionSources: z10.number().int().nonnegative(),
1449
- errors: z10.array(z10.string().max(1e3)).max(20)
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 = z10.object({
1452
- clientVersion: z10.string().max(64),
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: z10.array(z10.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
1457
- configError: z10.string().max(2e3).nullable(),
1458
- reportError: z10.string().max(2e3).nullable(),
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 = z10.object({
1462
- devices: z10.array(
1463
- z10.object({
1572
+ var WebDevicesSchema = z12.object({
1573
+ devices: z12.array(
1574
+ z12.object({
1464
1575
  memberId: NameSchema,
1465
- bound: z10.boolean(),
1466
- lastSeenAt: z10.string().nullable(),
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: z11.string(),
1868
- serverUrl: z11.string(),
1869
- token: z11.string()
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(join6(directory, "client.sqlite3"), { readOnly: true });
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(join6(directory, "client.sqlite3"), clientMigrations);
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(join6(directory, "client.sqlite3"));
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 constants2 } from "node:fs";
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 join10, resolve as resolve4 } from "node:path";
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 join7 } from "node:path";
2170
- var registrationPath = (directory, root) => join7(directory, "hook-registrations", rootKey(root) + ".json");
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 = join7(directory, "hook-registrations");
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(join7(path, name2), "utf8"));
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(join7(path, name2), error);
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 join8 } from "node:path";
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
- join8(stateDirectory, "locks", `${createHash5("sha256").update(root).digest("hex")}.sqlite3`),
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 join9, resolve as resolve3 } from "node:path";
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("~/") ? join9(home, path.slice(2)) : path
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 [join9(agentDirectory, "settings.json"), join9(project, ".pi/settings.json")]) {
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 (!missing(error)) throw new Error(`Unable to read Pi session directory setting: ${path}`);
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
- join9(agentDirectory, "sessions"),
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(join9(dirname5(entry), "session-hook.js"))},
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 = join9(agentDirectory, "extensions", `teammy-${rootKey(directory)}.js`);
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 (!missing(error)) throw error;
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 (missing(error)) return false;
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 = constants2.F_OK) {
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", join10(directory, "runtime"));
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 ?? join10(home, ".claude"),
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 ?? join10(home, ".codex"),
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: join10(home, ".tclaude"),
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: join10(home, ".tcodex"),
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 ?? join10(home, ".cursor"),
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 ?? join10(home, ".pi", "agent"), home, project),
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(join10(dir, name2), constants2.X_OK))
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(join10(home, ".cursor", "projects")))
2425
- sessions.discover("cursor", join10(home, ".cursor", "projects"), project);
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 = join10(configRoot, adapter.config);
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 (!missing(error)) throw error;
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 = join10(dirname6(cliEntry), "session-hook.js");
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 = join10(directory, "hook-backups", rootKey(root));
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 writeFile(join10(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
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 (missing(e)) return "";
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 writeFile(
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", join10(directory, "runtime"));
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(join10(directory, "client.sqlite3"), { readOnly: true });
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: join10(project, ".claude/settings.local.json"), agent: "claude-code", created: false },
2619
- { path: join10(project, ".codex/hooks.json"), agent: "codex", created: false },
2620
- { path: join10(gitRoot, ".cursor/hooks.json"), agent: "cursor", created: false }
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", join10(directory, "runtime"));
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 writeFile(config.exclude.path, text.replace(config.exclude.line + "\n", ""));
2796
+ await writeFile2(config.exclude.path, text.replace(config.exclude.line + "\n", ""));
2682
2797
  }
2683
2798
  removed++;
2684
2799
  } catch (error) {
2685
- if (!missing(error))
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 join12 } from "node:path";
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 open2, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
2708
- import { isAbsolute as isAbsolute3, join as join11 } from "node:path";
2709
- import { z as z12 } from "zod";
2710
- var HintSchema = z12.object({
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: z12.string().min(1).max(4096).refine(isAbsolute3).optional(),
2713
- nativeId: z12.string().min(1).max(512).optional(),
2714
- projectDir: z12.string().min(1).max(4096).refine(isAbsolute3)
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 join11(directory, "session-hints", createHash7("sha256").update(root).digest("hex"));
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 = join11(
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 open2(temporary, "wx", 384);
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 = join11(inbox(directory, root), entry.name);
2864
+ const path = join12(inbox(directory, root), entry.name);
2750
2865
  try {
2751
- const handle = await open2(path, "r");
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"] ?? join12(homedir2(), ".teammy", "client"))
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 join15, resolve as resolve7 } from "node:path";
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 join13 } from "node:path";
2863
- import { z as z13 } from "zod";
2864
- var RequestSchema = z13.object({
2865
- method: z13.enum(["ping", "wake", "status", "once"]),
2866
- root: z13.string().max(4096).optional()
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 join13(
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 = join13(socketPath(directory), "..");
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 join14, resolve as resolve6 } from "node:path";
2963
- import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile2, rename as rename3, rm } from "node:fs/promises";
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: join14(userHome, "Library", "LaunchAgents", id + ".plist"),
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: join14(userHome, ".config", "systemd", "user", id + ".service"),
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 writeFile2(temp, definition.content, { mode: 384 });
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(join14(directory, "runtime", "daemon.json"), "utf8"));
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(join15(directory, "runtime/installation.json"), "utf8"));
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", join15(directory, "runtime"));
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 = join15(directory, "backups");
3370
+ const backups = join16(directory, "backups");
3256
3371
  await mkdir9(backups, { recursive: true, mode: 448 });
3257
- backup = join15(backups, `before-schema-${before}.sqlite3`);
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(join15(directory, "client.sqlite3"));
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(join15(directory, "client.sqlite3"), clientMigrations).close();
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(join15(directory, "runtime/installation.json"), installation);
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: readdir3 } = await import("node:fs/promises");
3310
- for (const file of await readdir3(join15(directory, "backups"))) {
3311
- if (/^before-schema-\d+\.sqlite3$/.test(file) && join15(directory, "backups", file) !== backup)
3312
- await rm2(join15(directory, "backups", file));
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: join16(homedir4(), ".teammy", "client") },
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": join16(
3433
- process.env.CLAUDE_CONFIG_DIR ?? join16(homedir4(), ".claude"),
3547
+ "claude-code": join17(
3548
+ process.env.CLAUDE_CONFIG_DIR ?? join17(homedir4(), ".claude"),
3434
3549
  "projects"
3435
3550
  ),
3436
- codex: join16(process.env.CODEX_HOME ?? join16(homedir4(), ".codex"), "sessions"),
3437
- cursor: join16(
3438
- process.env.CURSOR_CONFIG_DIR ?? join16(homedir4(), ".cursor"),
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: join16(process.env.PI_CODING_AGENT_DIR ?? join16(homedir4(), ".pi", "agent"), "sessions"),
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 setTimeout4 } from "node:timers/promises";
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 join18, resolve as resolve10 } from "node:path";
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 mkdir10 } from "node:fs/promises";
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/local-config.ts
3502
- import { constants as constants3 } from "node:fs";
3503
- import { open as open3, writeFile as writeFile3 } from "node:fs/promises";
3504
- import { join as join17 } from "node:path";
3505
- import { z as z14 } from "zod";
3506
- import ignore from "ignore";
3507
- var LOCAL_CONFIG_NAME = ".teammy-local.json";
3508
- var LocalConfigSchema = z14.object({
3509
- files: z14.object({ ignore: z14.array(z14.string().max(1024)).max(256).default([]) }).strict().default({ ignore: [] })
3510
- }).strict();
3511
- async function ensureLocalConfig(root) {
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 writeFile3(
3514
- join17(root, LOCAL_CONFIG_NAME),
3515
- JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
3516
- { flag: "wx", mode: 384 }
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 (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
3645
+ if (entries.length === 0 && missing2(error))
3646
+ return { identity: null, fingerprint: "", date: null };
3647
+ throw error;
3520
3648
  }
3521
3649
  }
3522
- async function loadLocalRules(root) {
3523
- let rules = [];
3650
+ async function identityIfPresent(path) {
3524
3651
  try {
3525
- const file = await open3(
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 (!missing(error))
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
- `${LOCAL_CONFIG_NAME}: invalid or unreadable configuration; uploads paused.`,
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
- const matcher = ignore().add(rules);
3553
- return {
3554
- fingerprint: JSON.stringify(rules),
3555
- excludes: (path, directory = false) => ignored(path) || matcher.ignores(path + (directory && !path.endsWith("/") ? "/" : ""))
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
- const token = CredentialSchema.parse((await readFile5(options.tokenFile, "utf8")).trim());
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
- const binding2 = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
3591
- if (options.project && binding2.workspaceId !== 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
- const binding2 = await transport.enroll(request);
3621
- state.saveBinding(root, transport.url, request.credential, binding2);
3622
- project.completeEnrollment();
3851
+ binding = await transport.enroll(request);
3852
+ token = request.credential;
3853
+ enrolled = true;
3623
3854
  }
3624
- const binding = state.binding(root);
3625
- const client = new HubClient(binding.serverUrl, 1e4, binding);
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 mkdir10(await safePath(root, binding.memberId));
3881
+ if (!own) await mkdir11(await safePath(root, binding.memberId));
3637
3882
  await ensureLocalConfig(root);
3638
- const { token: _, ...visible } = binding;
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: join18(homedir5(), ".teammy", "client") },
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 --member ID [--root DIR]\n\nCreates ./workspaces, adds Git ignore rules and starts login-session background sync.\nAdvanced: --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]" : ""}
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 ?? (values.reconnect ? existing?.memberId : void 0);
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 randomUUID7 } from "node:crypto";
4146
+ import { randomUUID as randomUUID8 } from "node:crypto";
3830
4147
  import { watch } from "node:fs";
3831
- import { lstat as lstat4, mkdir as mkdir12 } from "node:fs/promises";
3832
- import { join as join20, sep as sep5 } from "node:path";
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 randomUUID6 } from "node:crypto";
3837
- import { link, mkdir as mkdir11, open as open4, readFile as readFile6, unlink as unlink3 } from "node:fs/promises";
3838
- import { dirname as dirname10, join as join19, resolve as resolve11 } from "node:path";
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 mkdir11(dirname10(target), { recursive: true, mode: 448 });
3863
- const temporary = `${target}.${randomUUID6()}.tmp`;
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 = dirname10(target);
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 === dirname10(this.root)) break;
3888
- directory = dirname10(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 join19(this.root, "sha256", sha256.slice(0, 2), sha256);
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: missing2 } = hashes.length ? await this.transport.missingBlobs(hashes) : { missing: [] };
3939
- for (const hash2 of missing2) {
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(join20(stateDirectory, "blobs"));
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 : join20(root, binding.memberId);
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 lstat4(this.ownRoot);
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 mkdir12(ownerPath).catch((error) => {
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: randomUUID7(),
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 mkdir12(ownerRoot).catch((error) => {
4700
+ await mkdir13(ownerRoot).catch((error) => {
4384
4701
  if (error.code !== "EEXIST") throw error;
4385
4702
  });
4386
- const ownerStat = await lstat4(ownerRoot);
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 randomUUID8 } from "node:crypto";
4439
- import { join as join23 } from "node:path";
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 readdir2 } from "node:fs/promises";
4451
- import { basename as basename4, dirname as dirname12, relative as relative5, isAbsolute as isAbsolute5, sep as sep6, join as join21 } from "node:path";
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 basename3, dirname as dirname11, resolve as resolve12 } from "node:path";
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 !== basename3(dirname11(path)) || meta.subagentInfo != null)
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 = basename3(dirname11(dirname11(path)));
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) => basename4(path) === "store.db";
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 = dirname12(path);
4608
- while (basename4(container) !== "agent-transcripts" && dirname12(container) !== container)
4609
- container = dirname12(container);
4610
- if (basename4(container) !== "agent-transcripts")
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 = basename4(dirname12(container));
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 readdir2(directory, { withFileTypes: true })) {
4622
- if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(join21(directory, entry.name));
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: basename4(path, ".jsonl"), cwd: await cursorScope(path, projectDir) } : void 0;
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 join22 } from "node:path";
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 = join22(current, entry.name);
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(join23(directory, "blobs"));
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: randomUUID8(), sessionKey: key, parentRevision: revision, blob },
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: missing2 } = await this.client.missingBlobs([request.blob.sha256]);
5005
- if (missing2.some((hash2) => hash2 !== request.blob.sha256))
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 (missing2.length) {
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 setTimeout4(Math.max(100, job.nextAt - Date.now()), void 0, {
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 join24, resolve as resolve13 } from "node:path";
5395
- import { mkdir as mkdir13, realpath as realpath9 } from "node:fs/promises";
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: join24(homedir6(), ".teammy", "client") },
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 mkdir13(resolve13(values["state-dir"]), { recursive: true, mode: 448 });
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 join25 } from "node:path";
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(join25(directory, "runtime", "daemon.json"), { pid: process.pid, directory });
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(join25(directory, "runtime", "daemon.json")).catch(() => {
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
  `;