@teammy/cli 0.1.1 → 0.1.3

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 +461 -147
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,27 +2,27 @@
2
2
 
3
3
  成员设备上的命令行客户端,需要 Node.js 22.13+,推荐 Node.js 24。
4
4
 
5
- 0.1.1 与服务端版本保持一致,沿用 0.1.0 的完整目录发布、后台升级修复、Hook 清理与 Agent 采集能力。
5
+ 0.1.2 支持通过网页确认连接身份,并在确认后接管已有 workspaces 目录;保留后台同步、升级修复和 Hook 采集能力。
6
6
 
7
7
  客户端与服务端保持配套版本,支持多项目后台同步、根 README / `_template/` 公共资料分发,以及 `workspaces/.teammy-local.json` 中的 `files.ignore` 忽略规则。规则相对本人目录,下一轮检查生效,不删除已共享历史。公共资料由项目设置指定的一位成员维护。自动任务整理在服务端运行。
8
8
 
9
9
  ```sh
10
- npm install -g @teammy/cli@0.1.1
10
+ npm install -g @teammy/cli@0.1.2
11
11
  ```
12
12
 
13
13
  ```sh
14
- teammy connect --url http://teammy.internal:14317 --project PROJECT_ID --member alice
14
+ teammy connect --url http://teammy.internal:14317 --project PROJECT_ID
15
15
  teammy status
16
16
  teammy sync off
17
17
  teammy sync on
18
18
  teammy logs
19
19
  ```
20
20
 
21
- 在项目目录执行 connect,创建 `./workspaces` 和本人 ID 子目录、维护 `.gitignore`,自动在后台下载同事资料并上传本人文件。命令退出后可以关闭终端;已开启的项目在下次登录系统时恢复,手动 off 的项目保持停止。日常使用不需要重复 connect。
21
+ 在项目目录执行 connect,创建 `./workspaces` 和本人 ID 子目录、维护 `.gitignore`,自动在后台下载同事资料并上传本人文件。若仓库已有未绑定的 `workspaces/`,会显示本地和远端版本日期,默认回车确认后完整下载服务端资料并替换当前目录(含本人目录),再开始同步;输入 `n` 取消,脚本可显式使用 `--yes`。已被 Git 追踪的文件允许接入,不自动修改 Git 索引。命令退出后可以关闭终端;已开启的项目在下次登录系统时恢复,手动 off 的项目保持停止。日常使用不需要重复 connect。
22
22
 
23
- 已有同名目录、文件或符号链接时拒绝接入;不自动合并已有资料。Git 已追踪该路径时也会拒绝,不修改 Git 索引。可以先自行改名保存旧目录,接入成功后将本人的文件复制到 `workspaces/<ID>/`。
23
+ 默认打开网页确认用户 ID;无桌面的开发机可复制终端链接,在自己的电脑上完成确认,不需要 SSH 隧道或本地回调端口。脚本继续使用 `--member caffei`,旧服务端也可沿用该参数。浏览器确认使用现有账号和项目成员关系,不自动创建用户或加入项目。
24
24
 
25
- 本人本地文件是发布来源,服务器旧正文不会下载到本人目录。首次本地缺失的历史文件保留在服务器,不推断为删除;确认曾在本地存在的文件,之后删除并完成稳定等待才传播。其他成员目录只接收,不反向发布,本地误改会保留并报告。
25
+ 常规新建目录接入及日常同步中,本人本地文件是发布来源,服务器旧正文不会下载覆盖;上述已有 checkout 的确认替换流程是例外。首次本地缺失的历史文件保留在服务器,不推断为删除;确认曾在本地存在的文件,之后删除并完成稳定等待才传播。其他成员目录只接收,不反向发布,本地误改会保留并报告。
26
26
 
27
27
  默认约一分钟检查一次,文件变化连续一分钟稳定后准备完整目录版本;内容可分批传输,完整版本发布后才对外可见并触发任务整理。首次下载立即开始,大批量接收自动续批。周期由全站网页设置热更新。项目会话通过 hook 及后台补扫自动登记、提取并上传;只采集属于当前项目目录的会话,不自动扩展到 monorepo 上层其他会话。已有接入可通过 `sync on` 安装本项目 hook。
28
28
 
@@ -43,7 +43,7 @@ macOS 使用用户 LaunchAgent,Linux 使用 systemd 用户服务;不安装
43
43
  本次文件发布协议依赖服务端 `atomic-file-publications` 能力,两端需配套更新。以下维护命令从 0.1.0 提供,先安装新版,再执行修复:
44
44
 
45
45
  ```sh
46
- npm install -g @teammy/cli@0.1.1
46
+ npm install -g @teammy/cli@0.1.2
47
47
  teammy repair
48
48
  ```
49
49
 
package/dist/main.js CHANGED
@@ -286,7 +286,7 @@ var SessionSearchSchema = z5.object({
286
286
  });
287
287
 
288
288
  // packages/transport/hub-client.ts
289
- import { z as z8 } from "zod";
289
+ import { z as z9 } from "zod";
290
290
  import { createHash } from "node:crypto";
291
291
 
292
292
  // packages/protocol/index.ts
@@ -331,6 +331,28 @@ function sharedPathAllowed(path, paths) {
331
331
  );
332
332
  }
333
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
+
334
356
  // packages/transport/hub-client.ts
335
357
  var HubRequestError = class extends Error {
336
358
  constructor(message2, status, code, requestId) {
@@ -371,6 +393,17 @@ var HubClient = class {
371
393
  get url() {
372
394
  return this.baseUrl.toString();
373
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
+ }
374
407
  bind(deviceId, projectId) {
375
408
  return this.request("POST", "api/v1/bind", BindingSchema, {
376
409
  deviceId,
@@ -401,7 +434,7 @@ var HubClient = class {
401
434
  void 0,
402
435
  BLOB_TRANSFER_TIMEOUT_MS
403
436
  );
404
- if (!response.ok) return this.decode(response, signal, z8.never());
437
+ if (!response.ok) return this.decode(response, signal, z9.never());
405
438
  const reader = response.body?.getReader();
406
439
  if (!reader) throw new Error("Server returned no content body.");
407
440
  const parts = [];
@@ -446,7 +479,7 @@ var HubClient = class {
446
479
  return this.request(
447
480
  "PUT",
448
481
  "api/v1/devices/self/status",
449
- z8.object({ ok: z8.literal(true) }),
482
+ z9.object({ ok: z9.literal(true) }),
450
483
  status
451
484
  );
452
485
  }
@@ -571,11 +604,11 @@ import { dirname as dirname9, join as join17, resolve as resolve8 } from "node:p
571
604
  import { parseArgs as parseArgs2 } from "node:util";
572
605
 
573
606
  // packages/protocol/file-publication.ts
574
- import { z as z9 } from "zod";
607
+ import { z as z10 } from "zod";
575
608
  var FilePublicationSchema = FileCommitSchema.safeExtend({
576
609
  configRevision: RevisionSchema.optional()
577
610
  }).strict();
578
- var PublishFilesSchema = z9.object({ manifest: BlobRefSchema }).strict();
611
+ var PublishFilesSchema = z10.object({ manifest: BlobRefSchema }).strict();
579
612
 
580
613
  // packages/state/sqlite.ts
581
614
  import { chmodSync, mkdirSync } from "node:fs";
@@ -952,7 +985,7 @@ var RuntimeState = class {
952
985
  // apps/client/state.ts
953
986
  import { randomUUID as randomUUID4 } from "node:crypto";
954
987
  import { join as join7, relative as relative2, isAbsolute, sep as sep2 } from "node:path";
955
- import { z as z12 } from "zod";
988
+ import { z as z13 } from "zod";
956
989
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
957
990
 
958
991
  // apps/client/runtime/state-access.ts
@@ -1066,7 +1099,7 @@ import { setImmediate } from "node:timers/promises";
1066
1099
  import { constants } from "node:fs";
1067
1100
  import { open, writeFile } from "node:fs/promises";
1068
1101
  import { join as join3 } from "node:path";
1069
- import { z as z10 } from "zod";
1102
+ import { z as z11 } from "zod";
1070
1103
  import ignore from "ignore";
1071
1104
  function ignored(path) {
1072
1105
  return path.split("/").some(
@@ -1077,8 +1110,8 @@ function missing(error) {
1077
1110
  return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
1078
1111
  }
1079
1112
  var LOCAL_CONFIG_NAME = ".teammy-local.json";
1080
- var LocalConfigSchema = z10.object({
1081
- files: z10.object({ ignore: z10.array(z10.string().max(1024)).max(256).default([]) }).strict().default({ ignore: [] })
1113
+ var LocalConfigSchema = z11.object({
1114
+ files: z11.object({ ignore: z11.array(z11.string().max(1024)).max(256).default([]) }).strict().default({ ignore: [] })
1082
1115
  }).strict();
1083
1116
  async function ensureLocalConfig(root) {
1084
1117
  try {
@@ -1321,9 +1354,8 @@ async function verifyWorkspace(state, root) {
1321
1354
  "Workspace directory was replaced. Restore the original directory before syncing."
1322
1355
  );
1323
1356
  }
1324
- async function ignorePlan(root) {
1357
+ async function workspaceGitInfo(root) {
1325
1358
  const parent = dirname4(root), name2 = basename(root);
1326
- if (/[\r\n]/.test(name2)) throw new Error("Workspace name cannot contain a newline.");
1327
1359
  let repo = false;
1328
1360
  for (let p = parent; ; p = dirname4(p)) {
1329
1361
  try {
@@ -1335,19 +1367,30 @@ async function ignorePlan(root) {
1335
1367
  }
1336
1368
  if (p === dirname4(p)) break;
1337
1369
  }
1338
- if (repo) {
1339
- let tracked;
1340
- try {
1341
- tracked = (await exec("git", ["-C", parent, "--literal-pathspecs", "ls-files", "-z", "--", name2], {
1342
- maxBuffer: 1024 * 1024
1343
- })).stdout;
1344
- } catch {
1345
- throw new Error("Cannot inspect the Git index. Check Git access before connecting.");
1346
- }
1347
- if (tracked)
1348
- throw new Error(
1349
- "Git already tracks this workspace path. Remove it from the Git index yourself before connecting."
1350
- );
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) {
1351
1394
  try {
1352
1395
  await exec("git", ["-C", parent, "check-ignore", "--no-index", "-q", "--", name2 + "/"]);
1353
1396
  return;
@@ -1391,12 +1434,16 @@ async function prepareWorkspace(state, root, input, reconnect = false) {
1391
1434
  if (ignore3) await replaceFile(ignore3.parent, ".gitignore", ignore3.bytes, ignore3.signature);
1392
1435
  return;
1393
1436
  }
1394
- throw new Error(
1395
- 'Workspace path already exists. Choose another directory; for a connected workspace use "teammy sync on".'
1396
- );
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;
1397
1443
  }
1398
1444
  if (state.binding(root) || prior)
1399
1445
  throw new Error("Connected workspace is missing. Restore its directory before syncing.");
1446
+ if ((await workspaceGitInfo(root)).tracked) return true;
1400
1447
  const ignore2 = await ignorePlan(root);
1401
1448
  await mkdir2(root);
1402
1449
  try {
@@ -1491,43 +1538,43 @@ var ClientLog = class {
1491
1538
  import { setTimeout as setTimeout2 } from "node:timers/promises";
1492
1539
 
1493
1540
  // packages/protocol/devices.ts
1494
- import { z as z11 } from "zod";
1495
- var CredentialSchema = z11.string().regex(/^tmy_[a-f0-9]{64}$/);
1496
- var EnrollmentSchema = z11.object({
1497
- operationId: z11.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(),
1498
1545
  projectId: NameSchema,
1499
1546
  memberId: NameSchema,
1500
1547
  deviceId: DeviceIdSchema,
1501
1548
  credential: CredentialSchema
1502
1549
  }).strict();
1503
- var EffectivePolicySchema = z11.object({
1504
- checkIntervalSeconds: z11.number().min(1).max(86400),
1505
- fileSettleSeconds: z11.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)
1506
1553
  }).strict();
1507
- var SyncSummarySchema = z11.object({
1508
- checkedAt: z11.string(),
1509
- published: z11.boolean(),
1510
- appliedFiles: z11.number().int().nonnegative(),
1511
- pendingFiles: z11.number().int().nonnegative(),
1512
- sessionSources: z11.number().int().nonnegative(),
1513
- errors: z11.array(z11.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)
1514
1561
  }).strict();
1515
- var DeviceStatusSchema = z11.object({
1516
- clientVersion: z11.string().max(64),
1562
+ var DeviceStatusSchema = z12.object({
1563
+ clientVersion: z12.string().max(64),
1517
1564
  receivedRevision: RevisionSchema.nullable(),
1518
1565
  appliedRevision: RevisionSchema.nullable(),
1519
1566
  effectivePolicy: EffectivePolicySchema,
1520
- overrides: z11.array(z11.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
1521
- configError: z11.string().max(2e3).nullable(),
1522
- reportError: z11.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(),
1523
1570
  lastSync: SyncSummarySchema.nullable()
1524
1571
  }).strict();
1525
- var WebDevicesSchema = z11.object({
1526
- devices: z11.array(
1527
- z11.object({
1572
+ var WebDevicesSchema = z12.object({
1573
+ devices: z12.array(
1574
+ z12.object({
1528
1575
  memberId: NameSchema,
1529
- bound: z11.boolean(),
1530
- lastSeenAt: z11.string().nullable(),
1576
+ bound: z12.boolean(),
1577
+ lastSeenAt: z12.string().nullable(),
1531
1578
  status: DeviceStatusSchema.nullable()
1532
1579
  })
1533
1580
  )
@@ -1928,9 +1975,9 @@ var SessionState = class {
1928
1975
 
1929
1976
  // apps/client/state.ts
1930
1977
  var LocalBindingSchema = BindingSchema.extend({
1931
- root: z12.string(),
1932
- serverUrl: z12.string(),
1933
- token: z12.string()
1978
+ root: z13.string(),
1979
+ serverUrl: z13.string(),
1980
+ token: z13.string()
1934
1981
  });
1935
1982
  var clientMigrations = [
1936
1983
  String.raw`
@@ -1949,7 +1996,11 @@ var clientMigrations = [
1949
1996
  projectFilesClientMigration,
1950
1997
  sessionSourcesMigration,
1951
1998
  atomicFilesClientMigration,
1952
- 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
+ );`
1953
2004
  ];
1954
2005
  var clientSchemaVersion = clientMigrations.length;
1955
2006
  var ClientState = class extends FilePublicationState {
@@ -2770,12 +2821,12 @@ import { join as join13 } from "node:path";
2770
2821
  import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
2771
2822
  import { mkdir as mkdir6, open as open3, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
2772
2823
  import { isAbsolute as isAbsolute3, join as join12 } from "node:path";
2773
- import { z as z13 } from "zod";
2774
- var HintSchema = z13.object({
2824
+ import { z as z14 } from "zod";
2825
+ var HintSchema = z14.object({
2775
2826
  agent: AgentSchema,
2776
- path: z13.string().min(1).max(4096).refine(isAbsolute3).optional(),
2777
- nativeId: z13.string().min(1).max(512).optional(),
2778
- projectDir: z13.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)
2779
2830
  });
2780
2831
  function inbox(directory, root) {
2781
2832
  return join12(directory, "session-hints", createHash7("sha256").update(root).digest("hex"));
@@ -2924,10 +2975,10 @@ import { setTimeout as delay } from "node:timers/promises";
2924
2975
  import { createConnection } from "node:net";
2925
2976
  import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
2926
2977
  import { join as join14 } from "node:path";
2927
- import { z as z14 } from "zod";
2928
- var RequestSchema = z14.object({
2929
- method: z14.enum(["ping", "wake", "status", "once"]),
2930
- root: z14.string().max(4096).optional()
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()
2931
2982
  }).strict();
2932
2983
  function socketPath(directory) {
2933
2984
  return join14(
@@ -3370,8 +3421,8 @@ async function prepareClient(directory, version, options = {}, control = service
3370
3421
  else await rm2(maintenancePath(directory), { force: true });
3371
3422
  restart = !options.daemonStartup && !pauseReason && enabled > 0;
3372
3423
  if (backup) {
3373
- const { readdir: readdir3 } = await import("node:fs/promises");
3374
- for (const file of await readdir3(join16(directory, "backups"))) {
3424
+ const { readdir: readdir4 } = await import("node:fs/promises");
3425
+ for (const file of await readdir4(join16(directory, "backups"))) {
3375
3426
  if (/^before-schema-\d+\.sqlite3$/.test(file) && join16(directory, "backups", file) !== backup)
3376
3427
  await rm2(join16(directory, "backups", file));
3377
3428
  }
@@ -3550,21 +3601,193 @@ async function runSessionCommand(args, version) {
3550
3601
 
3551
3602
  // apps/client/sync-command.ts
3552
3603
  import { parseArgs as parseArgs4 } from "node:util";
3553
- import { setTimeout as setTimeout4 } from "node:timers/promises";
3604
+ import { setTimeout as setTimeout5 } from "node:timers/promises";
3554
3605
 
3555
3606
  // apps/client/client-command.ts
3556
3607
  import { parseArgs as parseArgs3 } from "node:util";
3557
- import { join as join18, resolve as resolve10 } from "node:path";
3608
+ import { join as join19, resolve as resolve10 } from "node:path";
3558
3609
  import { homedir as homedir5 } from "node:os";
3559
3610
  import { createInterface as createInterface2 } from "node:readline/promises";
3560
3611
 
3561
3612
  // apps/client/connection.ts
3562
- import { readFile as readFile5, mkdir as mkdir10 } from "node:fs/promises";
3613
+ import { readFile as readFile5, mkdir as mkdir11 } from "node:fs/promises";
3563
3614
  import { createInterface } from "node:readline/promises";
3615
+
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;
3627
+ try {
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
+ };
3644
+ } catch (error) {
3645
+ if (entries.length === 0 && missing2(error))
3646
+ return { identity: null, fingerprint: "", date: null };
3647
+ throw error;
3648
+ }
3649
+ }
3650
+ async function identityIfPresent(path) {
3651
+ try {
3652
+ return await directoryIdentity(path);
3653
+ } catch (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)
3666
+ throw new Error(
3667
+ "Connected workspace was replaced. Preserve the recovery directory before retrying."
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
+ }
3686
+ }
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);
3783
+ }
3784
+
3785
+ // apps/client/connection.ts
3564
3786
  async function connectProject(state, root, directory, options) {
3565
3787
  const lock = acquireRootLock(root, directory);
3566
3788
  try {
3567
3789
  state.assertRootAvailable(root);
3790
+ if (options.prepare) await recoverWorkspaceReplacement(state, root);
3568
3791
  const transport = new HubClient(options.url);
3569
3792
  const existing = state.binding(root);
3570
3793
  const project = new ClientProjectState(state, root);
@@ -3582,19 +3805,23 @@ async function connectProject(state, root, directory, options) {
3582
3805
  "This client requires a multi-project server. Update the server before connecting."
3583
3806
  );
3584
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;
3585
3813
  if (existing && !options.reconnect && !options.tokenFile) {
3586
3814
  if (member && member !== existing.memberId)
3587
3815
  throw new Error("This root belongs to a different member.");
3588
3816
  const client2 = new HubClient(existing.serverUrl, 1e4, existing);
3589
3817
  await client2.members();
3590
3818
  } else if (options.tokenFile) {
3591
- const token = CredentialSchema.parse((await readFile5(options.tokenFile, "utf8")).trim());
3819
+ token = CredentialSchema.parse((await readFile5(options.tokenFile, "utf8")).trim());
3592
3820
  const deviceId = state.deviceFor(root);
3593
3821
  const client2 = new HubClient(options.url, 1e4, { token, deviceId });
3594
- const binding2 = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
3595
- 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)
3596
3824
  throw new Error("Token belongs to a different project.");
3597
- state.saveBinding(root, client2.url, token, binding2);
3598
3825
  } else {
3599
3826
  member ??= existing?.memberId;
3600
3827
  if (!member && process.stdin.isTTY) {
@@ -3621,12 +3848,26 @@ async function connectProject(state, root, directory, options) {
3621
3848
  projectId,
3622
3849
  Boolean(options.reconnect)
3623
3850
  );
3624
- const binding2 = await transport.enroll(request);
3625
- state.saveBinding(root, transport.url, request.credential, binding2);
3626
- project.completeEnrollment();
3851
+ binding = await transport.enroll(request);
3852
+ token = request.credential;
3853
+ enrolled = true;
3627
3854
  }
3628
- const binding = state.binding(root);
3629
- 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();
3630
3871
  if (info.capabilities.includes("project-config")) {
3631
3872
  try {
3632
3873
  await applyProjectFile(project, await client.projectConfig());
@@ -3637,9 +3878,9 @@ async function connectProject(state, root, directory, options) {
3637
3878
  }
3638
3879
  const own = await metadata(root, binding.memberId);
3639
3880
  if (own && !own.directory) throw new Error("Member path is not a directory.");
3640
- if (!own) await mkdir10(await safePath(root, binding.memberId));
3881
+ if (!own) await mkdir11(await safePath(root, binding.memberId));
3641
3882
  await ensureLocalConfig(root);
3642
- const { token: _, ...visible } = binding;
3883
+ const { token: _, ...visible } = localBinding;
3643
3884
  return { ...visible, configRevision: project.cached()?.config.configRevision ?? null };
3644
3885
  } finally {
3645
3886
  lock.close();
@@ -3662,6 +3903,16 @@ function result(title, detail) {
3662
3903
  function json(value) {
3663
3904
  process.stdout.write(JSON.stringify(value, null, 2) + "\n");
3664
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
+ }
3665
3916
 
3666
3917
  // apps/client/runtime/diagnostics.ts
3667
3918
  import { resolve as resolve9 } from "node:path";
@@ -3742,10 +3993,53 @@ async function runDiagnostics(command, options, version) {
3742
3993
  return issue ? 1 : 0;
3743
3994
  }
3744
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
+
3745
4039
  // apps/client/client-command.ts
3746
4040
  var locationOptions = {
3747
4041
  root: { type: "string", default: resolve10("workspaces") },
3748
- "state-dir": { type: "string", default: join18(homedir5(), ".teammy", "client") },
4042
+ "state-dir": { type: "string", default: join19(homedir5(), ".teammy", "client") },
3749
4043
  json: { type: "boolean" },
3750
4044
  help: { type: "boolean", short: "h" }
3751
4045
  };
@@ -3760,12 +4054,13 @@ async function runClientCommand(command, args, version) {
3760
4054
  "token-file": { type: "string" },
3761
4055
  reconnect: { type: "boolean" },
3762
4056
  "no-start": { type: "boolean" },
4057
+ yes: { type: "boolean", short: "y" },
3763
4058
  all: { type: "boolean" }
3764
4059
  }
3765
4060
  });
3766
4061
  if (values.help) {
3767
4062
  process.stdout.write(
3768
- 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]" : ""}
3769
4064
  `
3770
4065
  );
3771
4066
  return 0;
@@ -3786,29 +4081,42 @@ async function runClientCommand(command, args, version) {
3786
4081
  const transport = new HubClient(url);
3787
4082
  const selectedProject = values.project ?? (values.reconnect ? existing?.workspaceId : void 0);
3788
4083
  const project = selectedProject === void 0 && values["token-file"] ? void 0 : NameSchema.parse(selectedProject);
3789
- let member = values.member ?? (values.reconnect ? existing?.memberId : void 0);
3790
- if (!member && !values["token-file"] && process.stdin.isTTY) {
3791
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
3792
- try {
3793
- member = (await rl.question("Member ID: ")).trim();
3794
- } finally {
3795
- rl.close();
3796
- }
3797
- }
3798
- if (!values["token-file"]) member = NameSchema.parse(member);
3799
- await prepareWorkspace(
3800
- state,
3801
- root,
3802
- { url: transport.url, project: project ?? "", member: member ?? "" },
3803
- Boolean(values.reconnect)
3804
- );
4084
+ let member = values.member ?? existing?.memberId;
3805
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);
3806
4089
  const connected = await connectProject(state, root, directory, {
3807
4090
  url: transport.url,
3808
4091
  project,
3809
4092
  member,
3810
4093
  tokenFile: values["token-file"],
3811
- 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
+ }
3812
4120
  });
3813
4121
  const collection = await setupSessions(state, root, directory, { enable: true });
3814
4122
  for (const issue of collection.issues) log.write("sessions.setup.failed", root, issue);
@@ -3822,6 +4130,11 @@ async function runClientCommand(command, args, version) {
3822
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");
3823
4131
  return 0;
3824
4132
  } catch (e) {
4133
+ if (e instanceof ConnectionCancelled) {
4134
+ if (values.json) json({ cancelled: true });
4135
+ else result(e.message);
4136
+ return 0;
4137
+ }
3825
4138
  log.write(`${command}.failed`, root, diagnostic(e));
3826
4139
  throw e;
3827
4140
  } finally {
@@ -3830,16 +4143,16 @@ async function runClientCommand(command, args, version) {
3830
4143
  }
3831
4144
 
3832
4145
  // apps/client/files/synchronizer.ts
3833
- import { randomUUID as randomUUID7 } from "node:crypto";
4146
+ import { randomUUID as randomUUID8 } from "node:crypto";
3834
4147
  import { watch } from "node:fs";
3835
- import { lstat as lstat4, mkdir as mkdir12 } from "node:fs/promises";
3836
- 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";
3837
4150
  import { performance } from "node:perf_hooks";
3838
4151
 
3839
4152
  // packages/storage/local-blob-store.ts
3840
- import { createHash as createHash9, randomUUID as randomUUID6 } from "node:crypto";
3841
- import { link, mkdir as mkdir11, open as open4, readFile as readFile6, unlink as unlink3 } from "node:fs/promises";
3842
- 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";
3843
4156
  function hash(content) {
3844
4157
  return createHash9("sha256").update(content).digest("hex");
3845
4158
  }
@@ -3863,8 +4176,8 @@ var LocalBlobStore = class {
3863
4176
  const bytes = Buffer.from(content);
3864
4177
  const sha256 = hash(bytes);
3865
4178
  const target = this.pathFor(sha256);
3866
- await mkdir11(dirname10(target), { recursive: true, mode: 448 });
3867
- const temporary = `${target}.${randomUUID6()}.tmp`;
4179
+ await mkdir12(dirname12(target), { recursive: true, mode: 448 });
4180
+ const temporary = `${target}.${randomUUID7()}.tmp`;
3868
4181
  const file = await open4(temporary, "wx", 384);
3869
4182
  try {
3870
4183
  try {
@@ -3880,7 +4193,7 @@ var LocalBlobStore = class {
3880
4193
  await this.get(sha256);
3881
4194
  }
3882
4195
  if (process.platform !== "win32") {
3883
- let directory = dirname10(target);
4196
+ let directory = dirname12(target);
3884
4197
  for (; ; ) {
3885
4198
  const handle = await open4(directory, "r");
3886
4199
  try {
@@ -3888,8 +4201,8 @@ var LocalBlobStore = class {
3888
4201
  } finally {
3889
4202
  await handle.close();
3890
4203
  }
3891
- if (directory === dirname10(this.root)) break;
3892
- directory = dirname10(directory);
4204
+ if (directory === dirname12(this.root)) break;
4205
+ directory = dirname12(directory);
3893
4206
  }
3894
4207
  }
3895
4208
  } finally {
@@ -3911,7 +4224,7 @@ var LocalBlobStore = class {
3911
4224
  }
3912
4225
  pathFor(sha256) {
3913
4226
  if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
3914
- return join19(this.root, "sha256", sha256.slice(0, 2), sha256);
4227
+ return join20(this.root, "sha256", sha256.slice(0, 2), sha256);
3915
4228
  }
3916
4229
  };
3917
4230
 
@@ -3977,7 +4290,7 @@ var FileSynchronizer = class _FileSynchronizer {
3977
4290
  if (!binding) throw new Error("Connect this root before synchronizing.");
3978
4291
  this.binding = binding;
3979
4292
  this.client = new HubClient(binding.serverUrl, 3e4, binding, this.abort.signal);
3980
- this.blobs = new LocalBlobStore(join20(stateDirectory, "blobs"));
4293
+ this.blobs = new LocalBlobStore(join21(stateDirectory, "blobs"));
3981
4294
  this.projectScope = options.scope === "project";
3982
4295
  this.owner = this.projectScope ? PROJECT_FILE_OWNER : binding.memberId;
3983
4296
  this.publication = new FilePublicationState(state.db, options.scope);
@@ -3987,7 +4300,7 @@ var FileSynchronizer = class _FileSynchronizer {
3987
4300
  commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch, batch.configRevision) : this.client.commitFiles(batch)
3988
4301
  });
3989
4302
  this.index = new SyncState(state, root, options.scope);
3990
- this.ownRoot = this.projectScope ? root : join20(root, binding.memberId);
4303
+ this.ownRoot = this.projectScope ? root : join21(root, binding.memberId);
3991
4304
  this.common = this.projectScope ? void 0 : new _FileSynchronizer(state, root, stateDirectory, { ...options, scope: "project" });
3992
4305
  const started = performance.now();
3993
4306
  const epoch = Date.now();
@@ -4104,7 +4417,7 @@ var FileSynchronizer = class _FileSynchronizer {
4104
4417
  if (pending?.batch.changes.some((change) => this.excludes(change.path)))
4105
4418
  await this.resetPublication();
4106
4419
  await this.initializeAuthor();
4107
- const ownStat = await lstat4(this.ownRoot);
4420
+ const ownStat = await lstat5(this.ownRoot);
4108
4421
  if (!ownStat.isDirectory() || ownStat.isSymbolicLink())
4109
4422
  throw new Error("Author directory is unavailable or is a symlink.");
4110
4423
  this.listen();
@@ -4188,7 +4501,7 @@ var FileSynchronizer = class _FileSynchronizer {
4188
4501
  }
4189
4502
  }
4190
4503
  const ownerPath = this.projectScope ? this.root : await safePath(this.root, this.owner);
4191
- await mkdir12(ownerPath).catch((error) => {
4504
+ await mkdir13(ownerPath).catch((error) => {
4192
4505
  if (error.code !== "EEXIST") throw error;
4193
4506
  });
4194
4507
  this.publication.initializePublication(this.root, manifest.revision, manifest.files, {
@@ -4336,7 +4649,7 @@ var FileSynchronizer = class _FileSynchronizer {
4336
4649
  return;
4337
4650
  if (changes.length || !publication.complete) {
4338
4651
  this.publication.enqueue(this.root, {
4339
- operationId: randomUUID7(),
4652
+ operationId: randomUUID8(),
4340
4653
  parentRevision: publication.revision,
4341
4654
  changes,
4342
4655
  ...this.projectScope ? { configRevision: this.sharedConfig.revision } : {}
@@ -4384,10 +4697,10 @@ var FileSynchronizer = class _FileSynchronizer {
4384
4697
  }
4385
4698
  const job = this.index.job(owner);
4386
4699
  const ownerRoot = this.projectScope ? this.root : await safePath(this.root, owner);
4387
- await mkdir12(ownerRoot).catch((error) => {
4700
+ await mkdir13(ownerRoot).catch((error) => {
4388
4701
  if (error.code !== "EEXIST") throw error;
4389
4702
  });
4390
- const ownerStat = await lstat4(ownerRoot);
4703
+ const ownerStat = await lstat5(ownerRoot);
4391
4704
  if (!ownerStat.isDirectory() || ownerStat.isSymbolicLink())
4392
4705
  throw new Error("Mirror root is not an ordinary directory.");
4393
4706
  let count = 0;
@@ -4439,8 +4752,8 @@ function message(error) {
4439
4752
  }
4440
4753
 
4441
4754
  // apps/client/sessions/synchronizer.ts
4442
- import { randomUUID as randomUUID8 } from "node:crypto";
4443
- import { join as join23 } from "node:path";
4755
+ import { randomUUID as randomUUID9 } from "node:crypto";
4756
+ import { join as join24 } from "node:path";
4444
4757
  import { performance as performance3 } from "node:perf_hooks";
4445
4758
 
4446
4759
  // packages/protocol/session-key.ts
@@ -4451,14 +4764,14 @@ function sessionKey(agent, nativeId) {
4451
4764
 
4452
4765
  // apps/client/sessions/source.ts
4453
4766
  import { createReadStream, realpathSync } from "node:fs";
4454
- import { stat, realpath as realpath8, readdir as readdir2 } from "node:fs/promises";
4455
- 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";
4456
4769
  import { createInterface as createInterface3 } from "node:readline";
4457
4770
  import * as zlib from "node:zlib";
4458
4771
 
4459
4772
  // apps/client/sessions/cursor.ts
4460
4773
  import { createHash as createHash11 } from "node:crypto";
4461
- 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";
4462
4775
  import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
4463
4776
  import { fileURLToPath } from "node:url";
4464
4777
  function fields(bytes) {
@@ -4507,7 +4820,7 @@ function readCursor(path, projectDir, checkCwd) {
4507
4820
  if (typeof hex !== "string" || !/^(?:[a-fA-F0-9]{2})+$/.test(hex))
4508
4821
  throw new Error("Invalid Cursor metadata.");
4509
4822
  const meta = object(JSON.parse(Buffer.from(hex, "hex").toString("utf8")));
4510
- 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)
4511
4824
  throw new Error("Invalid or delegated Cursor session identity.");
4512
4825
  const blob = (id) => {
4513
4826
  if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid Cursor blob reference.");
@@ -4519,7 +4832,7 @@ function readCursor(path, projectDir, checkCwd) {
4519
4832
  return bytes;
4520
4833
  };
4521
4834
  const root = fields(blob(String(meta.latestRootBlobId)));
4522
- const bucket = basename3(dirname11(dirname11(path)));
4835
+ const bucket = basename4(dirname13(dirname13(path)));
4523
4836
  const md5 = (cwd2) => createHash11("md5").update(resolve12(cwd2)).digest("hex");
4524
4837
  let cwd = md5(projectDir) === bucket ? projectDir : void 0;
4525
4838
  for (const uri of root.get(9) ?? []) {
@@ -4584,7 +4897,7 @@ function within(project, cwd) {
4584
4897
  return rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute5(rel);
4585
4898
  }
4586
4899
  var cursorEncoded = (cwd) => cwd.replace(/^[/\\]+/, "").replace(/[/\\]/g, "-");
4587
- var cursorDatabase = (path) => basename4(path) === "store.db";
4900
+ var cursorDatabase = (path) => basename5(path) === "store.db";
4588
4901
  async function sourceSignature(path, agent) {
4589
4902
  const paths = agent === "cursor" && cursorDatabase(path) ? [path, `${path}-wal`] : [path];
4590
4903
  const signatures = [];
@@ -4608,12 +4921,12 @@ async function sourceSignature(path, agent) {
4608
4921
  return JSON.stringify(signatures);
4609
4922
  }
4610
4923
  async function cursorScope(path, project) {
4611
- let container = dirname12(path);
4612
- while (basename4(container) !== "agent-transcripts" && dirname12(container) !== container)
4613
- container = dirname12(container);
4614
- 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")
4615
4928
  throw new Error("Cursor transcript workspace cannot be verified.");
4616
- const encoded = basename4(dirname12(container));
4929
+ const encoded = basename5(dirname14(container));
4617
4930
  const matches = [];
4618
4931
  const visit = async (directory) => {
4619
4932
  const prefix = cursorEncoded(directory);
@@ -4622,8 +4935,8 @@ async function cursorScope(path, project) {
4622
4935
  return;
4623
4936
  }
4624
4937
  if (!encoded.startsWith(prefix + "-")) return;
4625
- for (const entry of await readdir2(directory, { withFileTypes: true })) {
4626
- 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));
4627
4940
  }
4628
4941
  };
4629
4942
  await visit(project);
@@ -4641,7 +4954,7 @@ async function readSource(agent, path, projectDir) {
4641
4954
  });
4642
4955
  return { snapshot: snapshot2, incomplete: false, skipped: 0, signature: signature2 };
4643
4956
  }
4644
- 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;
4645
4958
  const normalizer = new JsonlNormalizer(agent, cursor);
4646
4959
  const stream = createReadStream(path, {
4647
4960
  end: Math.max(0, Number(JSON.parse(signature2)[0][2]) - 1)
@@ -4701,7 +5014,7 @@ async function readSource(agent, path, projectDir) {
4701
5014
  // apps/client/sessions/discovery.ts
4702
5015
  import { opendir as opendir2 } from "node:fs/promises";
4703
5016
  import { performance as performance2 } from "node:perf_hooks";
4704
- import { join as join22 } from "node:path";
5017
+ import { join as join23 } from "node:path";
4705
5018
 
4706
5019
  // apps/client/sessions/work-queue.ts
4707
5020
  var SessionWorkQueue = class {
@@ -4740,7 +5053,7 @@ async function* candidates(directory, agent) {
4740
5053
  const current = queue.shift();
4741
5054
  const dir = await opendir2(current);
4742
5055
  for await (const entry of dir) {
4743
- const path = join22(current, entry.name);
5056
+ const path = join23(current, entry.name);
4744
5057
  if (entry.isDirectory()) queue.push(path);
4745
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")))
4746
5059
  yield path;
@@ -4838,7 +5151,7 @@ var SessionSynchronizer = class {
4838
5151
  const binding = state.binding(root);
4839
5152
  if (!binding) throw new Error("Connect this root before collecting sessions.");
4840
5153
  this.client = new HubClient(binding.serverUrl, 1e4, binding, signal);
4841
- this.blobs = new LocalBlobStore(join23(directory, "blobs"));
5154
+ this.blobs = new LocalBlobStore(join24(directory, "blobs"));
4842
5155
  this.index = new SessionState(state, root);
4843
5156
  this.discovery = new SessionDiscovery(this.index);
4844
5157
  }
@@ -4989,7 +5302,7 @@ var SessionSynchronizer = class {
4989
5302
  }
4990
5303
  this.index.stage(
4991
5304
  source.path,
4992
- { operationId: randomUUID8(), sessionKey: key, parentRevision: revision, blob },
5305
+ { operationId: randomUUID9(), sessionKey: key, parentRevision: revision, blob },
4993
5306
  observed.signature
4994
5307
  );
4995
5308
  await this.flush(this.index.get(source.path));
@@ -5372,7 +5685,7 @@ async function runSyncCommand(args, version) {
5372
5685
  prior = report.errors[0] ?? "";
5373
5686
  }
5374
5687
  if (values.once) return report.errors.length ? 1 : 0;
5375
- await setTimeout4(Math.max(100, job.nextAt - Date.now()), void 0, {
5688
+ await setTimeout5(Math.max(100, job.nextAt - Date.now()), void 0, {
5376
5689
  signal: abort.signal
5377
5690
  }).catch((e) => {
5378
5691
  if (!abort.signal.aborted) throw e;
@@ -5395,13 +5708,13 @@ async function runSyncCommand(args, version) {
5395
5708
  // apps/client/maintenance-command.ts
5396
5709
  import { parseArgs as parseArgs5 } from "node:util";
5397
5710
  import { homedir as homedir6 } from "node:os";
5398
- import { join as join24, resolve as resolve13 } from "node:path";
5399
- 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";
5400
5713
  async function runMaintenanceCommand(command, args, version) {
5401
5714
  const { values } = parseArgs5({
5402
5715
  args,
5403
5716
  options: {
5404
- "state-dir": { type: "string", default: join24(homedir6(), ".teammy", "client") },
5717
+ "state-dir": { type: "string", default: join25(homedir6(), ".teammy", "client") },
5405
5718
  stop: { type: "boolean" },
5406
5719
  json: { type: "boolean" },
5407
5720
  help: { type: "boolean", short: "h" }
@@ -5417,7 +5730,7 @@ ${command === "repair" ? "Repair this local client and preserve project on/off s
5417
5730
  return 0;
5418
5731
  }
5419
5732
  if (command === "uninstall" && values.stop) throw new Error("--stop is a repair option.");
5420
- await mkdir13(resolve13(values["state-dir"]), { recursive: true, mode: 448 });
5733
+ await mkdir14(resolve13(values["state-dir"]), { recursive: true, mode: 448 });
5421
5734
  const directory = await realpath9(values["state-dir"]);
5422
5735
  const report = await prepareClient(directory, version, {
5423
5736
  force: true,
@@ -5442,7 +5755,7 @@ ${command === "repair" ? "Repair this local client and preserve project on/off s
5442
5755
  // apps/client/runtime/daemon.ts
5443
5756
  import { createServer } from "node:net";
5444
5757
  import { chmod as chmod2, unlink as unlink4 } from "node:fs/promises";
5445
- import { join as join25 } from "node:path";
5758
+ import { join as join26 } from "node:path";
5446
5759
  import { resolve as resolve14 } from "node:path";
5447
5760
  async function runDaemon(directory, version) {
5448
5761
  await prepareClient(directory, version, { daemonStartup: true });
@@ -5576,7 +5889,7 @@ async function runDaemon(directory, version) {
5576
5889
  });
5577
5890
  });
5578
5891
  await chmod2(socketPath(directory), 384);
5579
- writePrivateJson(join25(directory, "runtime", "daemon.json"), { pid: process.pid, directory });
5892
+ writePrivateJson(join26(directory, "runtime", "daemon.json"), { pid: process.pid, directory });
5580
5893
  process.once("SIGINT", stop);
5581
5894
  process.once("SIGTERM", stop);
5582
5895
  log.write("daemon.started", void 0, `version=${version}`);
@@ -5599,7 +5912,7 @@ async function runDaemon(directory, version) {
5599
5912
  await new Promise((resolve16) => server.close(() => resolve16()));
5600
5913
  await unlink4(socketPath(directory)).catch(() => {
5601
5914
  });
5602
- await unlink4(join25(directory, "runtime", "daemon.json")).catch(() => {
5915
+ await unlink4(join26(directory, "runtime", "daemon.json")).catch(() => {
5603
5916
  });
5604
5917
  log.write("daemon.stopped");
5605
5918
  state.close();
@@ -5616,7 +5929,7 @@ Usage:
5616
5929
  teammy --version Show the installed client version
5617
5930
  teammy server-info --url URL Check server connection and protocol capabilities
5618
5931
  teammy server-info --help Show connection options
5619
- 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]
5620
5933
  teammy repair [--stop] Repair or pause this local background client
5621
5934
  teammy uninstall Remove local service and hooks; keep user data
5622
5935
  teammy status [--root DIR] [--state-dir DIR]
@@ -5626,6 +5939,7 @@ Usage:
5626
5939
 
5627
5940
  Root defaults to ./workspaces; local state defaults to ~/.teammy/client.
5628
5941
  connect starts background sync; enabled projects resume after login.
5942
+ Without --member, connect opens a browser to confirm your user ID.
5629
5943
  Use --json for scripts; manage sync timing in the server dashboard.
5630
5944
  The hub server is installed separately as @teammy/server (command: teammy-server).
5631
5945
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammy/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "The Teammy client for team collaboration and synchronization.",
5
5
  "type": "module",
6
6
  "bin": {