@teammy/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/main.js +219 -215
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
成员设备上的命令行客户端,需要 Node.js 22.13+,推荐 Node.js 24。
|
|
4
4
|
|
|
5
|
-
0.1.0
|
|
5
|
+
0.1.1 与服务端版本保持一致,沿用 0.1.0 的完整目录发布、后台升级修复、Hook 清理与 Agent 采集能力。
|
|
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.
|
|
10
|
+
npm install -g @teammy/cli@0.1.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```sh
|
|
@@ -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.
|
|
46
|
+
npm install -g @teammy/cli@0.1.1
|
|
47
47
|
teammy repair
|
|
48
48
|
```
|
|
49
49
|
|
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,
|
|
@@ -566,7 +567,7 @@ var HubClient = class {
|
|
|
566
567
|
import { createHash as createHash8 } from "node:crypto";
|
|
567
568
|
import { realpath as realpath6 } from "node:fs/promises";
|
|
568
569
|
import { homedir as homedir4 } from "node:os";
|
|
569
|
-
import { dirname as dirname9, join as
|
|
570
|
+
import { dirname as dirname9, join as join17, resolve as resolve8 } from "node:path";
|
|
570
571
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
571
572
|
|
|
572
573
|
// packages/protocol/file-publication.ts
|
|
@@ -591,7 +592,7 @@ function transaction(db, action) {
|
|
|
591
592
|
throw error;
|
|
592
593
|
}
|
|
593
594
|
}
|
|
594
|
-
function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
595
|
+
function openDatabase(path, migrations, busyTimeoutMs = 5e3, journalMode = "WAL") {
|
|
595
596
|
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
|
|
596
597
|
throw new Error("Invalid SQLite busy timeout.");
|
|
597
598
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -599,7 +600,7 @@ function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
|
599
600
|
try {
|
|
600
601
|
chmodSync(path, 384);
|
|
601
602
|
db.exec(
|
|
602
|
-
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode =
|
|
603
|
+
`PRAGMA busy_timeout = ${busyTimeoutMs}; PRAGMA foreign_keys = ON; PRAGMA journal_mode = ${journalMode}; PRAGMA synchronous = FULL;`
|
|
603
604
|
);
|
|
604
605
|
transaction(db, () => {
|
|
605
606
|
const version = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
@@ -950,8 +951,8 @@ var RuntimeState = class {
|
|
|
950
951
|
|
|
951
952
|
// apps/client/state.ts
|
|
952
953
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
953
|
-
import { join as
|
|
954
|
-
import { z as
|
|
954
|
+
import { join as join7, relative as relative2, isAbsolute, sep as sep2 } from "node:path";
|
|
955
|
+
import { z as z12 } from "zod";
|
|
955
956
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
956
957
|
|
|
957
958
|
// apps/client/runtime/state-access.ts
|
|
@@ -1052,25 +1053,88 @@ function stateVersion(directory) {
|
|
|
1052
1053
|
import { execFile } from "node:child_process";
|
|
1053
1054
|
import { promisify } from "node:util";
|
|
1054
1055
|
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
|
|
1056
|
+
import { basename, dirname as dirname4, join as join5, resolve } from "node:path";
|
|
1056
1057
|
|
|
1057
1058
|
// apps/client/files/local-fs.ts
|
|
1058
1059
|
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
|
|
1060
|
+
import { constants as constants2 } from "node:fs";
|
|
1061
|
+
import { lstat, mkdir, open as open2, readdir, realpath, rename, rmdir, unlink } from "node:fs/promises";
|
|
1062
|
+
import { dirname as dirname3, join as join4, relative, sep } from "node:path";
|
|
1062
1063
|
import { setImmediate } from "node:timers/promises";
|
|
1064
|
+
|
|
1065
|
+
// packages/workspace/local-config.ts
|
|
1066
|
+
import { constants } from "node:fs";
|
|
1067
|
+
import { open, writeFile } from "node:fs/promises";
|
|
1068
|
+
import { join as join3 } from "node:path";
|
|
1069
|
+
import { z as z10 } from "zod";
|
|
1070
|
+
import ignore from "ignore";
|
|
1071
|
+
function ignored(path) {
|
|
1072
|
+
return path.split("/").some(
|
|
1073
|
+
(part) => part === ".git" || part === ".DS_Store" || /^\.teammy(?:[-.]|$)/i.test(part) || /(?:~|\.(?:swp|swx|tmp|part))$/i.test(part)
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1063
1076
|
function missing(error) {
|
|
1064
1077
|
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
|
|
1065
1078
|
}
|
|
1079
|
+
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: [] })
|
|
1082
|
+
}).strict();
|
|
1083
|
+
async function ensureLocalConfig(root) {
|
|
1084
|
+
try {
|
|
1085
|
+
await writeFile(
|
|
1086
|
+
join3(root, LOCAL_CONFIG_NAME),
|
|
1087
|
+
JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
|
|
1088
|
+
{ flag: "wx", mode: 384 }
|
|
1089
|
+
);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function loadLocalRules(root) {
|
|
1095
|
+
let rules = [];
|
|
1096
|
+
try {
|
|
1097
|
+
const file = await open(
|
|
1098
|
+
join3(root, LOCAL_CONFIG_NAME),
|
|
1099
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
1100
|
+
);
|
|
1101
|
+
try {
|
|
1102
|
+
const before = await file.stat();
|
|
1103
|
+
if (!before.isFile() || before.size > 65536)
|
|
1104
|
+
throw new Error("Expected a regular configuration file no larger than 64 KiB.");
|
|
1105
|
+
const buffer = Buffer.alloc(65537);
|
|
1106
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
1107
|
+
if (bytesRead > 65536 || bytesRead !== before.size)
|
|
1108
|
+
throw new Error("Configuration exceeds the read limit or changed while reading.");
|
|
1109
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
|
|
1110
|
+
const after = await file.stat();
|
|
1111
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
1112
|
+
throw new Error("Configuration is being saved; retry on the next check.");
|
|
1113
|
+
rules = LocalConfigSchema.parse(JSON.parse(text)).files.ignore;
|
|
1114
|
+
} finally {
|
|
1115
|
+
await file.close();
|
|
1116
|
+
}
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (!missing(error))
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
`${LOCAL_CONFIG_NAME}: invalid or unreadable configuration; uploads paused.`,
|
|
1121
|
+
{ cause: error }
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const matcher = ignore().add(rules);
|
|
1125
|
+
return {
|
|
1126
|
+
fingerprint: JSON.stringify(rules),
|
|
1127
|
+
excludes: (path, directory = false) => ignored(path) || matcher.ignores(path + (directory && !path.endsWith("/") ? "/" : ""))
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// apps/client/files/local-fs.ts
|
|
1132
|
+
function missing2(error) {
|
|
1133
|
+
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
|
|
1134
|
+
}
|
|
1066
1135
|
function signature(stat2) {
|
|
1067
1136
|
return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeNs, stat2.ctimeNs].join(":");
|
|
1068
1137
|
}
|
|
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
1138
|
async function safePath(root, path, createParents = false) {
|
|
1075
1139
|
FilePathSchema.parse(path);
|
|
1076
1140
|
const rootStat = await lstat(root);
|
|
@@ -1079,7 +1143,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
1079
1143
|
const parts = path.split("/");
|
|
1080
1144
|
let parent = root;
|
|
1081
1145
|
for (const part of parts.slice(0, -1)) {
|
|
1082
|
-
parent =
|
|
1146
|
+
parent = join4(parent, part);
|
|
1083
1147
|
if (createParents)
|
|
1084
1148
|
await mkdir(parent).catch((error) => {
|
|
1085
1149
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -1089,7 +1153,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
1089
1153
|
if (!stat2.isDirectory())
|
|
1090
1154
|
throw Object.assign(new Error(`Parent is not a directory: ${path}`), { code: "ENOTDIR" });
|
|
1091
1155
|
}
|
|
1092
|
-
return
|
|
1156
|
+
return join4(root, ...parts);
|
|
1093
1157
|
}
|
|
1094
1158
|
async function metadata(root, path) {
|
|
1095
1159
|
try {
|
|
@@ -1099,7 +1163,7 @@ async function metadata(root, path) {
|
|
|
1099
1163
|
throw new Error(`Unsupported filesystem entry: ${path}`);
|
|
1100
1164
|
return { signature: signature(stat2), directory: stat2.isDirectory() };
|
|
1101
1165
|
} catch (error) {
|
|
1102
|
-
if (
|
|
1166
|
+
if (missing2(error)) return null;
|
|
1103
1167
|
throw error;
|
|
1104
1168
|
}
|
|
1105
1169
|
}
|
|
@@ -1149,7 +1213,7 @@ async function readStable(root, path) {
|
|
|
1149
1213
|
const inside = relative(await realpath(root), resolvedBefore);
|
|
1150
1214
|
if (inside.startsWith(`..${sep}`) || inside === "..")
|
|
1151
1215
|
throw new Error(`File escapes sync root: ${path}`);
|
|
1152
|
-
const file = await
|
|
1216
|
+
const file = await open2(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
1153
1217
|
try {
|
|
1154
1218
|
const before = await file.stat({ bigint: true });
|
|
1155
1219
|
if (!before.isFile() || before.size > BigInt(MAX_FILE_BYTES))
|
|
@@ -1179,7 +1243,7 @@ async function readStable(root, path) {
|
|
|
1179
1243
|
}
|
|
1180
1244
|
async function syncDirectory(directory) {
|
|
1181
1245
|
if (process.platform === "win32") return;
|
|
1182
|
-
const handle = await
|
|
1246
|
+
const handle = await open2(directory, "r");
|
|
1183
1247
|
try {
|
|
1184
1248
|
await handle.sync();
|
|
1185
1249
|
} finally {
|
|
@@ -1188,8 +1252,8 @@ async function syncDirectory(directory) {
|
|
|
1188
1252
|
}
|
|
1189
1253
|
async function replaceFile(root, path, bytes, expected) {
|
|
1190
1254
|
const target = await safePath(root, path, true);
|
|
1191
|
-
const temporary =
|
|
1192
|
-
const file = await
|
|
1255
|
+
const temporary = join4(dirname3(target), `.teammy-${randomUUID2()}.tmp`);
|
|
1256
|
+
const file = await open2(temporary, "wx", 384);
|
|
1193
1257
|
try {
|
|
1194
1258
|
try {
|
|
1195
1259
|
await file.writeFile(bytes);
|
|
@@ -1213,7 +1277,7 @@ async function replaceFile(root, path, bytes, expected) {
|
|
|
1213
1277
|
}
|
|
1214
1278
|
} finally {
|
|
1215
1279
|
await unlink(temporary).catch((error) => {
|
|
1216
|
-
if (!
|
|
1280
|
+
if (!missing2(error)) throw error;
|
|
1217
1281
|
});
|
|
1218
1282
|
}
|
|
1219
1283
|
}
|
|
@@ -1241,7 +1305,7 @@ async function removeFile(root, path, expected) {
|
|
|
1241
1305
|
var exec = promisify(execFile);
|
|
1242
1306
|
async function workspacePath(input) {
|
|
1243
1307
|
const path = resolve(input);
|
|
1244
|
-
return
|
|
1308
|
+
return join5(await realpath2(dirname4(path)), basename(path));
|
|
1245
1309
|
}
|
|
1246
1310
|
async function directoryIdentity(root) {
|
|
1247
1311
|
const stat2 = await lstat2(root, { bigint: true });
|
|
@@ -1263,11 +1327,11 @@ async function ignorePlan(root) {
|
|
|
1263
1327
|
let repo = false;
|
|
1264
1328
|
for (let p = parent; ; p = dirname4(p)) {
|
|
1265
1329
|
try {
|
|
1266
|
-
await lstat2(
|
|
1330
|
+
await lstat2(join5(p, ".git"));
|
|
1267
1331
|
repo = true;
|
|
1268
1332
|
break;
|
|
1269
1333
|
} catch (e) {
|
|
1270
|
-
if (!
|
|
1334
|
+
if (!missing2(e)) throw e;
|
|
1271
1335
|
}
|
|
1272
1336
|
if (p === dirname4(p)) break;
|
|
1273
1337
|
}
|
|
@@ -1314,7 +1378,7 @@ async function prepareWorkspace(state, root, input, reconnect = false) {
|
|
|
1314
1378
|
await lstat2(root);
|
|
1315
1379
|
present = true;
|
|
1316
1380
|
} catch (e) {
|
|
1317
|
-
if (!
|
|
1381
|
+
if (!missing2(e)) throw e;
|
|
1318
1382
|
}
|
|
1319
1383
|
if (present) {
|
|
1320
1384
|
if (reconnect && state.binding(root)) {
|
|
@@ -1352,7 +1416,7 @@ function completePreparation(state, root) {
|
|
|
1352
1416
|
|
|
1353
1417
|
// apps/client/runtime/log.ts
|
|
1354
1418
|
import { appendFileSync, mkdirSync as mkdirSync4, renameSync as renameSync2, statSync, unlinkSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1355
|
-
import { join as
|
|
1419
|
+
import { join as join6 } from "node:path";
|
|
1356
1420
|
import { createHash as createHash3 } from "node:crypto";
|
|
1357
1421
|
function rootKey(root) {
|
|
1358
1422
|
return createHash3("sha256").update(root).digest("hex").slice(0, 16);
|
|
@@ -1368,9 +1432,9 @@ function diagnostic(error) {
|
|
|
1368
1432
|
var ClientLog = class {
|
|
1369
1433
|
constructor(directory, limit = 1024 * 1024) {
|
|
1370
1434
|
this.limit = limit;
|
|
1371
|
-
const logs =
|
|
1435
|
+
const logs = join6(directory, "logs");
|
|
1372
1436
|
mkdirSync4(logs, { recursive: true, mode: 448 });
|
|
1373
|
-
this.path =
|
|
1437
|
+
this.path = join6(logs, "client.jsonl");
|
|
1374
1438
|
}
|
|
1375
1439
|
limit;
|
|
1376
1440
|
path;
|
|
@@ -1427,43 +1491,43 @@ var ClientLog = class {
|
|
|
1427
1491
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
1428
1492
|
|
|
1429
1493
|
// packages/protocol/devices.ts
|
|
1430
|
-
import { z as
|
|
1431
|
-
var CredentialSchema =
|
|
1432
|
-
var EnrollmentSchema =
|
|
1433
|
-
operationId:
|
|
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(),
|
|
1434
1498
|
projectId: NameSchema,
|
|
1435
1499
|
memberId: NameSchema,
|
|
1436
1500
|
deviceId: DeviceIdSchema,
|
|
1437
1501
|
credential: CredentialSchema
|
|
1438
1502
|
}).strict();
|
|
1439
|
-
var EffectivePolicySchema =
|
|
1440
|
-
checkIntervalSeconds:
|
|
1441
|
-
fileSettleSeconds:
|
|
1503
|
+
var EffectivePolicySchema = z11.object({
|
|
1504
|
+
checkIntervalSeconds: z11.number().min(1).max(86400),
|
|
1505
|
+
fileSettleSeconds: z11.number().min(0).max(86400)
|
|
1442
1506
|
}).strict();
|
|
1443
|
-
var SyncSummarySchema =
|
|
1444
|
-
checkedAt:
|
|
1445
|
-
published:
|
|
1446
|
-
appliedFiles:
|
|
1447
|
-
pendingFiles:
|
|
1448
|
-
sessionSources:
|
|
1449
|
-
errors:
|
|
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)
|
|
1450
1514
|
}).strict();
|
|
1451
|
-
var DeviceStatusSchema =
|
|
1452
|
-
clientVersion:
|
|
1515
|
+
var DeviceStatusSchema = z11.object({
|
|
1516
|
+
clientVersion: z11.string().max(64),
|
|
1453
1517
|
receivedRevision: RevisionSchema.nullable(),
|
|
1454
1518
|
appliedRevision: RevisionSchema.nullable(),
|
|
1455
1519
|
effectivePolicy: EffectivePolicySchema,
|
|
1456
|
-
overrides:
|
|
1457
|
-
configError:
|
|
1458
|
-
reportError:
|
|
1520
|
+
overrides: z11.array(z11.enum(["checkIntervalSeconds", "fileSettleSeconds"])).max(2),
|
|
1521
|
+
configError: z11.string().max(2e3).nullable(),
|
|
1522
|
+
reportError: z11.string().max(2e3).nullable(),
|
|
1459
1523
|
lastSync: SyncSummarySchema.nullable()
|
|
1460
1524
|
}).strict();
|
|
1461
|
-
var WebDevicesSchema =
|
|
1462
|
-
devices:
|
|
1463
|
-
|
|
1525
|
+
var WebDevicesSchema = z11.object({
|
|
1526
|
+
devices: z11.array(
|
|
1527
|
+
z11.object({
|
|
1464
1528
|
memberId: NameSchema,
|
|
1465
|
-
bound:
|
|
1466
|
-
lastSeenAt:
|
|
1529
|
+
bound: z11.boolean(),
|
|
1530
|
+
lastSeenAt: z11.string().nullable(),
|
|
1467
1531
|
status: DeviceStatusSchema.nullable()
|
|
1468
1532
|
})
|
|
1469
1533
|
)
|
|
@@ -1864,9 +1928,9 @@ var SessionState = class {
|
|
|
1864
1928
|
|
|
1865
1929
|
// apps/client/state.ts
|
|
1866
1930
|
var LocalBindingSchema = BindingSchema.extend({
|
|
1867
|
-
root:
|
|
1868
|
-
serverUrl:
|
|
1869
|
-
token:
|
|
1931
|
+
root: z12.string(),
|
|
1932
|
+
serverUrl: z12.string(),
|
|
1933
|
+
token: z12.string()
|
|
1870
1934
|
});
|
|
1871
1935
|
var clientMigrations = [
|
|
1872
1936
|
String.raw`
|
|
@@ -1904,19 +1968,19 @@ var ClientState = class extends FilePublicationState {
|
|
|
1904
1968
|
throw new Error("Client state needs an upgrade. Run teammy repair.");
|
|
1905
1969
|
if (options.readOnly) {
|
|
1906
1970
|
if (!version) throw new Error("No client state is available.");
|
|
1907
|
-
db = new DatabaseSync3(
|
|
1971
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"), { readOnly: true });
|
|
1908
1972
|
} else if (!version) {
|
|
1909
1973
|
access2?.close();
|
|
1910
1974
|
access2 = void 0;
|
|
1911
1975
|
const exclusive = options.maintenance ? void 0 : stateAccess(directory, true);
|
|
1912
1976
|
try {
|
|
1913
|
-
db = openDatabase(
|
|
1977
|
+
db = openDatabase(join7(directory, "client.sqlite3"), clientMigrations);
|
|
1914
1978
|
} finally {
|
|
1915
1979
|
exclusive?.close();
|
|
1916
1980
|
}
|
|
1917
1981
|
if (!options.maintenance) access2 = stateAccess(directory);
|
|
1918
1982
|
} else {
|
|
1919
|
-
db = new DatabaseSync3(
|
|
1983
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"));
|
|
1920
1984
|
db.exec("PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;");
|
|
1921
1985
|
}
|
|
1922
1986
|
} catch (error) {
|
|
@@ -1993,10 +2057,10 @@ async function stateLocation(root, directory) {
|
|
|
1993
2057
|
// apps/client/sessions/setup.ts
|
|
1994
2058
|
import { execFile as execFile2 } from "node:child_process";
|
|
1995
2059
|
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
|
|
2060
|
+
import { access, mkdir as mkdir5, readFile as readFile2, writeFile as writeFile2, realpath as realpath4 } from "node:fs/promises";
|
|
2061
|
+
import { constants as constants3 } from "node:fs";
|
|
1998
2062
|
import { homedir } from "node:os";
|
|
1999
|
-
import { dirname as dirname6, join as
|
|
2063
|
+
import { dirname as dirname6, join as join11, resolve as resolve4 } from "node:path";
|
|
2000
2064
|
import { createHash as createHash6 } from "node:crypto";
|
|
2001
2065
|
|
|
2002
2066
|
// apps/client/sessions/normalize.ts
|
|
@@ -2166,8 +2230,8 @@ var JsonlNormalizer = class {
|
|
|
2166
2230
|
|
|
2167
2231
|
// apps/client/sessions/registration.ts
|
|
2168
2232
|
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2169
|
-
import { join as
|
|
2170
|
-
var registrationPath = (directory, root) =>
|
|
2233
|
+
import { join as join8 } from "node:path";
|
|
2234
|
+
var registrationPath = (directory, root) => join8(directory, "hook-registrations", rootKey(root) + ".json");
|
|
2171
2235
|
function readRegistration(directory, root) {
|
|
2172
2236
|
const path = registrationPath(directory, root);
|
|
2173
2237
|
if (!existsSync3(path)) return null;
|
|
@@ -2180,16 +2244,16 @@ function saveRegistration(directory, value) {
|
|
|
2180
2244
|
writePrivateJson(registrationPath(directory, value.root), value);
|
|
2181
2245
|
}
|
|
2182
2246
|
function registrations(directory, onError) {
|
|
2183
|
-
const path =
|
|
2247
|
+
const path = join8(directory, "hook-registrations");
|
|
2184
2248
|
if (!existsSync3(path)) return [];
|
|
2185
2249
|
return readdirSync2(path).filter((name2) => /^[a-f0-9]{16}\.json$/.test(name2)).flatMap((name2) => {
|
|
2186
2250
|
try {
|
|
2187
|
-
const value = JSON.parse(readFileSync3(
|
|
2251
|
+
const value = JSON.parse(readFileSync3(join8(path, name2), "utf8"));
|
|
2188
2252
|
const registration = readRegistration(directory, value.root);
|
|
2189
2253
|
if (!registration) throw new Error("Missing Hook registration.");
|
|
2190
2254
|
return [registration];
|
|
2191
2255
|
} catch (error) {
|
|
2192
|
-
onError(
|
|
2256
|
+
onError(join8(path, name2), error);
|
|
2193
2257
|
return [];
|
|
2194
2258
|
}
|
|
2195
2259
|
});
|
|
@@ -2197,12 +2261,12 @@ function registrations(directory, onError) {
|
|
|
2197
2261
|
|
|
2198
2262
|
// apps/client/root-lock.ts
|
|
2199
2263
|
import { createHash as createHash5 } from "node:crypto";
|
|
2200
|
-
import { join as
|
|
2264
|
+
import { join as join9 } from "node:path";
|
|
2201
2265
|
function acquireRootLock(root, stateDirectory) {
|
|
2202
2266
|
let lock;
|
|
2203
2267
|
try {
|
|
2204
2268
|
lock = openDatabase(
|
|
2205
|
-
|
|
2269
|
+
join9(stateDirectory, "locks", `${createHash5("sha256").update(root).digest("hex")}.sqlite3`),
|
|
2206
2270
|
[],
|
|
2207
2271
|
0
|
|
2208
2272
|
);
|
|
@@ -2221,29 +2285,29 @@ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
|
2221
2285
|
|
|
2222
2286
|
// apps/client/sessions/pi-extension.ts
|
|
2223
2287
|
import { mkdir as mkdir4, readFile } from "node:fs/promises";
|
|
2224
|
-
import { dirname as dirname5, basename as basename2, join as
|
|
2288
|
+
import { dirname as dirname5, basename as basename2, join as join10, resolve as resolve3 } from "node:path";
|
|
2225
2289
|
var marker = (directory) => `// Teammy Pi collection ${rootKey(directory)}`;
|
|
2226
2290
|
function piPath(path, home, cwd) {
|
|
2227
2291
|
return resolve3(
|
|
2228
2292
|
cwd,
|
|
2229
|
-
path === "~" ? home : path.startsWith("~/") ?
|
|
2293
|
+
path === "~" ? home : path.startsWith("~/") ? join10(home, path.slice(2)) : path
|
|
2230
2294
|
);
|
|
2231
2295
|
}
|
|
2232
2296
|
async function piDiscoveryDirectories(agentDirectory, project, home, env) {
|
|
2233
2297
|
let configured;
|
|
2234
|
-
for (const path of [
|
|
2298
|
+
for (const path of [join10(agentDirectory, "settings.json"), join10(project, ".pi/settings.json")]) {
|
|
2235
2299
|
try {
|
|
2236
2300
|
const settings = JSON.parse(await readFile(path, "utf8"));
|
|
2237
2301
|
if (typeof settings?.sessionDir === "string" && settings.sessionDir)
|
|
2238
2302
|
configured = settings.sessionDir;
|
|
2239
2303
|
} catch (error) {
|
|
2240
|
-
if (!
|
|
2304
|
+
if (!missing2(error)) throw new Error(`Unable to read Pi session directory setting: ${path}`);
|
|
2241
2305
|
}
|
|
2242
2306
|
}
|
|
2243
2307
|
configured = env.PI_CODING_AGENT_SESSION_DIR || configured;
|
|
2244
2308
|
return [
|
|
2245
2309
|
.../* @__PURE__ */ new Set([
|
|
2246
|
-
|
|
2310
|
+
join10(agentDirectory, "sessions"),
|
|
2247
2311
|
...configured ? [piPath(configured, home, project)] : []
|
|
2248
2312
|
])
|
|
2249
2313
|
];
|
|
@@ -2264,7 +2328,7 @@ export default function teammyCollection(pi) {
|
|
|
2264
2328
|
transcript_path: file,
|
|
2265
2329
|
});
|
|
2266
2330
|
const child = spawn(${JSON.stringify(process.execPath)}, [
|
|
2267
|
-
${JSON.stringify(
|
|
2331
|
+
${JSON.stringify(join10(dirname5(entry), "session-hook.js"))},
|
|
2268
2332
|
'--agent', 'pi', '--state-dir', ${JSON.stringify(directory)},
|
|
2269
2333
|
'--integration-file', ${JSON.stringify(path)},
|
|
2270
2334
|
], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true, shell: false });
|
|
@@ -2287,13 +2351,13 @@ export default function teammyCollection(pi) {
|
|
|
2287
2351
|
`;
|
|
2288
2352
|
}
|
|
2289
2353
|
async function installPiExtension(directory, registry, agentDirectory, entry) {
|
|
2290
|
-
const path =
|
|
2354
|
+
const path = join10(agentDirectory, "extensions", `teammy-${rootKey(directory)}.js`);
|
|
2291
2355
|
await mkdir4(dirname5(path), { recursive: true });
|
|
2292
2356
|
let original = null;
|
|
2293
2357
|
try {
|
|
2294
2358
|
original = await readStable(dirname5(path), basename2(path));
|
|
2295
2359
|
} catch (error) {
|
|
2296
|
-
if (!
|
|
2360
|
+
if (!missing2(error)) throw error;
|
|
2297
2361
|
}
|
|
2298
2362
|
if (original && !original.bytes.toString("utf8").startsWith(marker(directory) + "\n"))
|
|
2299
2363
|
throw new Error(`Pi extension is not owned by Teammy: ${path}`);
|
|
@@ -2316,7 +2380,7 @@ async function removePiExtension(directory, path) {
|
|
|
2316
2380
|
await removeFile(dirname5(path), basename2(path), original.signature);
|
|
2317
2381
|
return true;
|
|
2318
2382
|
} catch (error) {
|
|
2319
|
-
if (
|
|
2383
|
+
if (missing2(error)) return false;
|
|
2320
2384
|
throw error;
|
|
2321
2385
|
}
|
|
2322
2386
|
}
|
|
@@ -2324,7 +2388,7 @@ async function removePiExtension(directory, path) {
|
|
|
2324
2388
|
// apps/client/sessions/setup.ts
|
|
2325
2389
|
var exec2 = promisify2(execFile2);
|
|
2326
2390
|
var quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
2327
|
-
async function exists(path, mode =
|
|
2391
|
+
async function exists(path, mode = constants3.F_OK) {
|
|
2328
2392
|
try {
|
|
2329
2393
|
await access(path, mode);
|
|
2330
2394
|
return true;
|
|
@@ -2335,7 +2399,7 @@ async function exists(path, mode = constants2.F_OK) {
|
|
|
2335
2399
|
async function setupSessions(state, root, directory, options = {}) {
|
|
2336
2400
|
await realpath4(root);
|
|
2337
2401
|
if (!state.binding(root)) throw new Error("Connect this root before installing hooks.");
|
|
2338
|
-
const lock = acquireRootLock("hooks",
|
|
2402
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2339
2403
|
try {
|
|
2340
2404
|
const prior = readRegistration(directory, root);
|
|
2341
2405
|
if (prior && !prior.enabled && !options.enable)
|
|
@@ -2355,7 +2419,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2355
2419
|
{
|
|
2356
2420
|
agent: "claude-code",
|
|
2357
2421
|
executables: ["claude"],
|
|
2358
|
-
storage: env.CLAUDE_CONFIG_DIR ??
|
|
2422
|
+
storage: env.CLAUDE_CONFIG_DIR ?? join11(home, ".claude"),
|
|
2359
2423
|
directories: ["projects"],
|
|
2360
2424
|
config: ".claude/settings.local.json",
|
|
2361
2425
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2363,7 +2427,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2363
2427
|
{
|
|
2364
2428
|
agent: "codex",
|
|
2365
2429
|
executables: ["codex"],
|
|
2366
|
-
storage: env.CODEX_HOME ??
|
|
2430
|
+
storage: env.CODEX_HOME ?? join11(home, ".codex"),
|
|
2367
2431
|
directories: ["sessions"],
|
|
2368
2432
|
config: ".codex/hooks.json",
|
|
2369
2433
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2372,7 +2436,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2372
2436
|
agent: "claude-code",
|
|
2373
2437
|
instance: "tclaude",
|
|
2374
2438
|
executables: ["tclaude"],
|
|
2375
|
-
storage:
|
|
2439
|
+
storage: join11(home, ".tclaude"),
|
|
2376
2440
|
directories: ["projects"],
|
|
2377
2441
|
config: ".claude/settings.local.json",
|
|
2378
2442
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2381,7 +2445,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2381
2445
|
agent: "codex",
|
|
2382
2446
|
instance: "tcodex",
|
|
2383
2447
|
executables: ["tcodex"],
|
|
2384
|
-
storage:
|
|
2448
|
+
storage: join11(home, ".tcodex"),
|
|
2385
2449
|
directories: ["sessions"],
|
|
2386
2450
|
config: ".codex/hooks.json",
|
|
2387
2451
|
events: ["Stop", "SessionEnd"]
|
|
@@ -2389,7 +2453,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2389
2453
|
{
|
|
2390
2454
|
agent: "cursor",
|
|
2391
2455
|
executables: ["agent", "cursor-agent"],
|
|
2392
|
-
storage: env.CURSOR_CONFIG_DIR ??
|
|
2456
|
+
storage: env.CURSOR_CONFIG_DIR ?? join11(home, ".cursor"),
|
|
2393
2457
|
directories: ["chats", "projects"],
|
|
2394
2458
|
config: ".cursor/hooks.json",
|
|
2395
2459
|
events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
|
|
@@ -2397,7 +2461,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2397
2461
|
{
|
|
2398
2462
|
agent: "pi",
|
|
2399
2463
|
executables: ["pi"],
|
|
2400
|
-
storage: piPath(env.PI_CODING_AGENT_DIR ??
|
|
2464
|
+
storage: piPath(env.PI_CODING_AGENT_DIR ?? join11(home, ".pi", "agent"), home, project),
|
|
2401
2465
|
directories: []
|
|
2402
2466
|
}
|
|
2403
2467
|
];
|
|
@@ -2411,7 +2475,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2411
2475
|
for (const adapter of defaults) {
|
|
2412
2476
|
const installed = options.agents ? options.agents.includes(adapter.instance ?? adapter.agent) : await exists(adapter.storage) || (await Promise.all(
|
|
2413
2477
|
(env.PATH ?? "").split(":").flatMap(
|
|
2414
|
-
(dir) => adapter.executables.map((name2) => exists(
|
|
2478
|
+
(dir) => adapter.executables.map((name2) => exists(join11(dir, name2), constants3.X_OK))
|
|
2415
2479
|
)
|
|
2416
2480
|
)).some(Boolean);
|
|
2417
2481
|
const piProfiles = adapter.agent === "pi" ? registry.configs.filter((config) => config.kind === "pi-extension").map((config) => dirname6(dirname6(config.path))) : [];
|
|
@@ -2421,8 +2485,8 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2421
2485
|
if (!registry.excludedDiscovery?.includes(source))
|
|
2422
2486
|
sessions.discover(adapter.agent, source, project);
|
|
2423
2487
|
}
|
|
2424
|
-
if (adapter.agent === "cursor" && !registry.excludedDiscovery?.includes(
|
|
2425
|
-
sessions.discover("cursor",
|
|
2488
|
+
if (adapter.agent === "cursor" && !registry.excludedDiscovery?.includes(join11(home, ".cursor", "projects")))
|
|
2489
|
+
sessions.discover("cursor", join11(home, ".cursor", "projects"), project);
|
|
2426
2490
|
if (!configured.includes(adapter.agent)) configured.push(adapter.agent);
|
|
2427
2491
|
if (adapter.agent === "pi") {
|
|
2428
2492
|
for (const profile of /* @__PURE__ */ new Set([...piProfiles, ...installed ? [adapter.storage] : []])) {
|
|
@@ -2448,7 +2512,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2448
2512
|
}
|
|
2449
2513
|
if (!adapter.config) continue;
|
|
2450
2514
|
const configRoot = adapter.agent === "cursor" ? gitRoot : project;
|
|
2451
|
-
const path =
|
|
2515
|
+
const path = join11(configRoot, adapter.config);
|
|
2452
2516
|
if (written.has(path)) continue;
|
|
2453
2517
|
written.add(path);
|
|
2454
2518
|
try {
|
|
@@ -2457,7 +2521,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2457
2521
|
try {
|
|
2458
2522
|
original = await readStable(configRoot, adapter.config);
|
|
2459
2523
|
} catch (error) {
|
|
2460
|
-
if (!
|
|
2524
|
+
if (!missing2(error)) throw error;
|
|
2461
2525
|
}
|
|
2462
2526
|
const config = original ? JSON.parse(original.bytes.toString("utf8")) : {};
|
|
2463
2527
|
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
@@ -2467,7 +2531,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2467
2531
|
const hooks = object(config.hooks);
|
|
2468
2532
|
const marker2 = hookMarker(directory, root);
|
|
2469
2533
|
const cliEntry = options.entry ?? resolve4(process.argv[1]);
|
|
2470
|
-
const hookEntry =
|
|
2534
|
+
const hookEntry = join11(dirname6(cliEntry), "session-hook.js");
|
|
2471
2535
|
const invocation = [
|
|
2472
2536
|
process.execPath,
|
|
2473
2537
|
hookEntry,
|
|
@@ -2501,10 +2565,10 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2501
2565
|
const bytes = Buffer.from(JSON.stringify(config, null, 2) + "\n");
|
|
2502
2566
|
if (original?.bytes.equals(bytes)) continue;
|
|
2503
2567
|
if (original) {
|
|
2504
|
-
const backupDir =
|
|
2568
|
+
const backupDir = join11(directory, "hook-backups", rootKey(root));
|
|
2505
2569
|
await mkdir5(backupDir, { recursive: true, mode: 448 });
|
|
2506
2570
|
const hash2 = createHash6("sha256").update(original.bytes).digest("hex");
|
|
2507
|
-
await
|
|
2571
|
+
await writeFile2(join11(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
|
|
2508
2572
|
mode: 384
|
|
2509
2573
|
});
|
|
2510
2574
|
}
|
|
@@ -2522,7 +2586,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2522
2586
|
"info/exclude"
|
|
2523
2587
|
])).stdout.trim();
|
|
2524
2588
|
const old = await readFile2(exclude, "utf8").catch((e) => {
|
|
2525
|
-
if (
|
|
2589
|
+
if (missing2(e)) return "";
|
|
2526
2590
|
throw e;
|
|
2527
2591
|
});
|
|
2528
2592
|
const rule = "/" + relativePath.replace(/[\\*?[\]#! ]/g, "\\$&");
|
|
@@ -2533,7 +2597,7 @@ async function setupSessions(state, root, directory, options = {}) {
|
|
|
2533
2597
|
line: "# teammy-hook-" + rootKey(directory) + "-" + rootKey(root) + "\n" + rule
|
|
2534
2598
|
};
|
|
2535
2599
|
saveRegistration(directory, registry);
|
|
2536
|
-
await
|
|
2600
|
+
await writeFile2(
|
|
2537
2601
|
exclude,
|
|
2538
2602
|
old + (old && !old.endsWith("\n") ? "\n" : "") + owned.exclude.line + "\n"
|
|
2539
2603
|
);
|
|
@@ -2554,7 +2618,7 @@ function hookMarker(directory, root) {
|
|
|
2554
2618
|
return `teammy-session-${rootKey(directory)}-${rootKey(root)}`;
|
|
2555
2619
|
}
|
|
2556
2620
|
function setDiscoveryPreference(directory, root, source, enabled) {
|
|
2557
|
-
const lock = acquireRootLock("hooks",
|
|
2621
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2558
2622
|
try {
|
|
2559
2623
|
const registry = readRegistration(directory, root);
|
|
2560
2624
|
if (!registry) return;
|
|
@@ -2595,7 +2659,7 @@ async function legacyRegistrations(directory, explicitRoot) {
|
|
|
2595
2659
|
const roots = new Set(explicitRoot ? [explicitRoot] : []);
|
|
2596
2660
|
let db;
|
|
2597
2661
|
try {
|
|
2598
|
-
db = new DatabaseSync4(
|
|
2662
|
+
db = new DatabaseSync4(join11(directory, "client.sqlite3"), { readOnly: true });
|
|
2599
2663
|
for (const row of db.prepare("SELECT root FROM connections WHERE binding_json IS NOT NULL").all())
|
|
2600
2664
|
if (!explicitRoot || row.root === explicitRoot) roots.add(String(row.root));
|
|
2601
2665
|
} catch {
|
|
@@ -2615,15 +2679,15 @@ async function legacyRegistrations(directory, explicitRoot) {
|
|
|
2615
2679
|
} catch {
|
|
2616
2680
|
}
|
|
2617
2681
|
const configs = [
|
|
2618
|
-
{ path:
|
|
2619
|
-
{ path:
|
|
2620
|
-
{ path:
|
|
2682
|
+
{ path: join11(project, ".claude/settings.local.json"), agent: "claude-code", created: false },
|
|
2683
|
+
{ path: join11(project, ".codex/hooks.json"), agent: "codex", created: false },
|
|
2684
|
+
{ path: join11(gitRoot, ".cursor/hooks.json"), agent: "cursor", created: false }
|
|
2621
2685
|
];
|
|
2622
2686
|
saveRegistration(directory, { format: 1, root, projectDir: project, enabled: true, configs });
|
|
2623
2687
|
}
|
|
2624
2688
|
}
|
|
2625
2689
|
async function teardownSessions(directory, root) {
|
|
2626
|
-
const lock = acquireRootLock("hooks",
|
|
2690
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2627
2691
|
const issues = [];
|
|
2628
2692
|
let removed = 0;
|
|
2629
2693
|
try {
|
|
@@ -2678,11 +2742,11 @@ async function teardownSessions(directory, root) {
|
|
|
2678
2742
|
);
|
|
2679
2743
|
if (config.exclude && !await exists(config.path)) {
|
|
2680
2744
|
const text = await readFile2(config.exclude.path, "utf8");
|
|
2681
|
-
await
|
|
2745
|
+
await writeFile2(config.exclude.path, text.replace(config.exclude.line + "\n", ""));
|
|
2682
2746
|
}
|
|
2683
2747
|
removed++;
|
|
2684
2748
|
} catch (error) {
|
|
2685
|
-
if (!
|
|
2749
|
+
if (!missing2(error))
|
|
2686
2750
|
issues.push(
|
|
2687
2751
|
`${config.path}: ${error instanceof Error ? error.message : "Hook removal failed."}`
|
|
2688
2752
|
);
|
|
@@ -2700,32 +2764,32 @@ import { parseArgs } from "node:util";
|
|
|
2700
2764
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
2701
2765
|
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
|
|
2702
2766
|
import { homedir as homedir2 } from "node:os";
|
|
2703
|
-
import { join as
|
|
2767
|
+
import { join as join13 } from "node:path";
|
|
2704
2768
|
|
|
2705
2769
|
// apps/client/sessions/hooks.ts
|
|
2706
2770
|
import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
|
|
2707
|
-
import { mkdir as mkdir6, open as
|
|
2708
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2709
|
-
import { z as
|
|
2710
|
-
var HintSchema =
|
|
2771
|
+
import { mkdir as mkdir6, open as open3, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
2772
|
+
import { isAbsolute as isAbsolute3, join as join12 } from "node:path";
|
|
2773
|
+
import { z as z13 } from "zod";
|
|
2774
|
+
var HintSchema = z13.object({
|
|
2711
2775
|
agent: AgentSchema,
|
|
2712
|
-
path:
|
|
2713
|
-
nativeId:
|
|
2714
|
-
projectDir:
|
|
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)
|
|
2715
2779
|
});
|
|
2716
2780
|
function inbox(directory, root) {
|
|
2717
|
-
return
|
|
2781
|
+
return join12(directory, "session-hints", createHash7("sha256").update(root).digest("hex"));
|
|
2718
2782
|
}
|
|
2719
2783
|
async function recordHint(directory, root, input) {
|
|
2720
2784
|
const hint = HintSchema.parse(input);
|
|
2721
2785
|
const targetDir = inbox(directory, root);
|
|
2722
2786
|
await mkdir6(targetDir, { recursive: true, mode: 448 });
|
|
2723
|
-
const target =
|
|
2787
|
+
const target = join12(
|
|
2724
2788
|
targetDir,
|
|
2725
2789
|
`${createHash7("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
|
|
2726
2790
|
);
|
|
2727
2791
|
const temporary = `${target}.${randomUUID5()}.tmp`;
|
|
2728
|
-
const handle = await
|
|
2792
|
+
const handle = await open3(temporary, "wx", 384);
|
|
2729
2793
|
try {
|
|
2730
2794
|
await handle.writeFile(JSON.stringify(hint));
|
|
2731
2795
|
} finally {
|
|
@@ -2746,9 +2810,9 @@ async function consumeHints(directory, root, sessions, refresh) {
|
|
|
2746
2810
|
for await (const entry of dir) {
|
|
2747
2811
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
2748
2812
|
if (count++ >= 50) break;
|
|
2749
|
-
const path =
|
|
2813
|
+
const path = join12(inbox(directory, root), entry.name);
|
|
2750
2814
|
try {
|
|
2751
|
-
const handle = await
|
|
2815
|
+
const handle = await open3(path, "r");
|
|
2752
2816
|
let body;
|
|
2753
2817
|
try {
|
|
2754
2818
|
if ((await handle.stat()).size > 16384) throw new Error("Oversized hook hint.");
|
|
@@ -2801,7 +2865,7 @@ async function runHook(args) {
|
|
|
2801
2865
|
}
|
|
2802
2866
|
});
|
|
2803
2867
|
const directory = await realpath5(
|
|
2804
|
-
resolve5(values["state-dir"] ??
|
|
2868
|
+
resolve5(values["state-dir"] ?? join13(homedir2(), ".teammy", "client"))
|
|
2805
2869
|
);
|
|
2806
2870
|
const payload = values.file ? {} : await hookPayload();
|
|
2807
2871
|
let selectedRoot = values.root;
|
|
@@ -2852,28 +2916,28 @@ async function runHook(args) {
|
|
|
2852
2916
|
// apps/client/runtime/maintenance.ts
|
|
2853
2917
|
import { mkdir as mkdir9, rename as rename4, rm as rm2, readFile as readFile4 } from "node:fs/promises";
|
|
2854
2918
|
import { existsSync as existsSync4 } from "node:fs";
|
|
2855
|
-
import { join as
|
|
2919
|
+
import { join as join16, resolve as resolve7 } from "node:path";
|
|
2856
2920
|
import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
|
|
2857
2921
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2858
2922
|
|
|
2859
2923
|
// apps/client/runtime/ipc.ts
|
|
2860
2924
|
import { createConnection } from "node:net";
|
|
2861
2925
|
import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
|
|
2862
|
-
import { join as
|
|
2863
|
-
import { z as
|
|
2864
|
-
var RequestSchema =
|
|
2865
|
-
method:
|
|
2866
|
-
root:
|
|
2926
|
+
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()
|
|
2867
2931
|
}).strict();
|
|
2868
2932
|
function socketPath(directory) {
|
|
2869
|
-
return
|
|
2933
|
+
return join14(
|
|
2870
2934
|
"/tmp",
|
|
2871
2935
|
`teammy-${process.getuid?.() ?? "user"}-${rootKey(directory)}`,
|
|
2872
2936
|
"control.sock"
|
|
2873
2937
|
);
|
|
2874
2938
|
}
|
|
2875
2939
|
async function prepareSocketDirectory(directory) {
|
|
2876
|
-
const path =
|
|
2940
|
+
const path = join14(socketPath(directory), "..");
|
|
2877
2941
|
await mkdir7(path, { recursive: true, mode: 448 });
|
|
2878
2942
|
const stat2 = await lstat3(path);
|
|
2879
2943
|
if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.())
|
|
@@ -2959,8 +3023,8 @@ async function runningDaemon(directory) {
|
|
|
2959
3023
|
import { execFile as execFile3 } from "node:child_process";
|
|
2960
3024
|
import { promisify as promisify3 } from "node:util";
|
|
2961
3025
|
import { homedir as homedir3 } from "node:os";
|
|
2962
|
-
import { dirname as dirname8, join as
|
|
2963
|
-
import { mkdir as mkdir8, readFile as readFile3, writeFile as
|
|
3026
|
+
import { dirname as dirname8, join as join15, resolve as resolve6 } from "node:path";
|
|
3027
|
+
import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile3, rename as rename3, rm } from "node:fs/promises";
|
|
2964
3028
|
import { setTimeout as setTimeout3 } from "node:timers/promises";
|
|
2965
3029
|
var execute = promisify3(execFile3);
|
|
2966
3030
|
var exec3 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
|
|
@@ -2977,7 +3041,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2977
3041
|
if (platform === "darwin")
|
|
2978
3042
|
return {
|
|
2979
3043
|
id,
|
|
2980
|
-
path:
|
|
3044
|
+
path: join15(userHome, "Library", "LaunchAgents", id + ".plist"),
|
|
2981
3045
|
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
2982
3046
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2983
3047
|
<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 +3050,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2986
3050
|
if (platform === "linux")
|
|
2987
3051
|
return {
|
|
2988
3052
|
id: id + ".service",
|
|
2989
|
-
path:
|
|
3053
|
+
path: join15(userHome, ".config", "systemd", "user", id + ".service"),
|
|
2990
3054
|
content: `[Unit]
|
|
2991
3055
|
Description=Teammy background synchronization
|
|
2992
3056
|
[Service]
|
|
@@ -3047,7 +3111,7 @@ async function ensureDaemon(directory, version) {
|
|
|
3047
3111
|
await mkdir8(dirname8(definition.path), { recursive: true });
|
|
3048
3112
|
if (old !== definition.content) {
|
|
3049
3113
|
const temp = definition.path + `.${process.pid}.tmp`;
|
|
3050
|
-
await
|
|
3114
|
+
await writeFile3(temp, definition.content, { mode: 384 });
|
|
3051
3115
|
await rename3(temp, definition.path);
|
|
3052
3116
|
}
|
|
3053
3117
|
if (process.platform === "darwin") {
|
|
@@ -3093,7 +3157,7 @@ async function stopDaemon(directory, remove = false) {
|
|
|
3093
3157
|
let loaded = false;
|
|
3094
3158
|
let pid;
|
|
3095
3159
|
try {
|
|
3096
|
-
const saved = JSON.parse(await readFile3(
|
|
3160
|
+
const saved = JSON.parse(await readFile3(join15(directory, "runtime", "daemon.json"), "utf8"));
|
|
3097
3161
|
if (saved.directory === directory && Number.isSafeInteger(saved.pid) && saved.pid > 0)
|
|
3098
3162
|
pid = saved.pid;
|
|
3099
3163
|
} catch {
|
|
@@ -3203,14 +3267,14 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3203
3267
|
};
|
|
3204
3268
|
let installed;
|
|
3205
3269
|
try {
|
|
3206
|
-
installed = JSON.parse(await readFile4(
|
|
3270
|
+
installed = JSON.parse(await readFile4(join16(directory, "runtime/installation.json"), "utf8"));
|
|
3207
3271
|
} catch {
|
|
3208
3272
|
}
|
|
3209
3273
|
const probe = options.daemonStartup ? { state: "absent" } : await probeDaemon(directory);
|
|
3210
3274
|
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
3275
|
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
3276
|
if (!required) return { maintained: false, enabled: 0, stopped: 0, issues: [] };
|
|
3213
|
-
const mutex = acquireRootLock("maintenance",
|
|
3277
|
+
const mutex = acquireRootLock("maintenance", join16(directory, "runtime"));
|
|
3214
3278
|
const log = new ClientLog(directory);
|
|
3215
3279
|
let access2;
|
|
3216
3280
|
let legacy;
|
|
@@ -3252,13 +3316,13 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3252
3316
|
);
|
|
3253
3317
|
let backup;
|
|
3254
3318
|
if (before > 0 && before < clientSchemaVersion) {
|
|
3255
|
-
const backups =
|
|
3319
|
+
const backups = join16(directory, "backups");
|
|
3256
3320
|
await mkdir9(backups, { recursive: true, mode: 448 });
|
|
3257
|
-
backup =
|
|
3321
|
+
backup = join16(backups, `before-schema-${before}.sqlite3`);
|
|
3258
3322
|
if (!existsSync4(backup)) {
|
|
3259
3323
|
const temp = backup + ".tmp";
|
|
3260
3324
|
await rm2(temp, { force: true });
|
|
3261
|
-
const db = new DatabaseSync5(
|
|
3325
|
+
const db = new DatabaseSync5(join16(directory, "client.sqlite3"));
|
|
3262
3326
|
try {
|
|
3263
3327
|
db.prepare("VACUUM INTO ?").run(temp);
|
|
3264
3328
|
} finally {
|
|
@@ -3277,7 +3341,7 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3277
3341
|
...pauseReason ? { pauseReason } : {},
|
|
3278
3342
|
...backup ? { backup } : {}
|
|
3279
3343
|
});
|
|
3280
|
-
openDatabase(
|
|
3344
|
+
openDatabase(join16(directory, "client.sqlite3"), clientMigrations).close();
|
|
3281
3345
|
state = new ClientState(directory, { maintenance: true });
|
|
3282
3346
|
const runtime = new RuntimeState(state);
|
|
3283
3347
|
if (options.intent) {
|
|
@@ -3300,16 +3364,16 @@ async function prepareClient(directory, version, options = {}, control = service
|
|
|
3300
3364
|
issues.push(`${root}: ${diagnostic(error)}`);
|
|
3301
3365
|
}
|
|
3302
3366
|
}
|
|
3303
|
-
writePrivateJson(
|
|
3367
|
+
writePrivateJson(join16(directory, "runtime/installation.json"), installation);
|
|
3304
3368
|
if (pauseReason)
|
|
3305
3369
|
writeMaintenance(directory, { reason: pauseReason, phase: "paused", version });
|
|
3306
3370
|
else await rm2(maintenancePath(directory), { force: true });
|
|
3307
3371
|
restart = !options.daemonStartup && !pauseReason && enabled > 0;
|
|
3308
3372
|
if (backup) {
|
|
3309
3373
|
const { readdir: readdir3 } = await import("node:fs/promises");
|
|
3310
|
-
for (const file of await readdir3(
|
|
3311
|
-
if (/^before-schema-\d+\.sqlite3$/.test(file) &&
|
|
3312
|
-
await rm2(
|
|
3374
|
+
for (const file of await readdir3(join16(directory, "backups"))) {
|
|
3375
|
+
if (/^before-schema-\d+\.sqlite3$/.test(file) && join16(directory, "backups", file) !== backup)
|
|
3376
|
+
await rm2(join16(directory, "backups", file));
|
|
3313
3377
|
}
|
|
3314
3378
|
}
|
|
3315
3379
|
log.write(
|
|
@@ -3369,7 +3433,7 @@ async function runSessionCommand(args, version) {
|
|
|
3369
3433
|
args: args.slice(1),
|
|
3370
3434
|
options: {
|
|
3371
3435
|
root: { type: "string", default: resolve8("workspaces") },
|
|
3372
|
-
"state-dir": { type: "string", default:
|
|
3436
|
+
"state-dir": { type: "string", default: join17(homedir4(), ".teammy", "client") },
|
|
3373
3437
|
agent: { type: "string" },
|
|
3374
3438
|
file: { type: "string" },
|
|
3375
3439
|
directory: { type: "string" },
|
|
@@ -3429,17 +3493,17 @@ async function runSessionCommand(args, version) {
|
|
|
3429
3493
|
} else {
|
|
3430
3494
|
const agent = AgentSchema.parse(values.agent);
|
|
3431
3495
|
const defaults = {
|
|
3432
|
-
"claude-code":
|
|
3433
|
-
process.env.CLAUDE_CONFIG_DIR ??
|
|
3496
|
+
"claude-code": join17(
|
|
3497
|
+
process.env.CLAUDE_CONFIG_DIR ?? join17(homedir4(), ".claude"),
|
|
3434
3498
|
"projects"
|
|
3435
3499
|
),
|
|
3436
|
-
codex:
|
|
3437
|
-
cursor:
|
|
3438
|
-
process.env.CURSOR_CONFIG_DIR ??
|
|
3500
|
+
codex: join17(process.env.CODEX_HOME ?? join17(homedir4(), ".codex"), "sessions"),
|
|
3501
|
+
cursor: join17(
|
|
3502
|
+
process.env.CURSOR_CONFIG_DIR ?? join17(homedir4(), ".cursor"),
|
|
3439
3503
|
"chats",
|
|
3440
3504
|
createHash8("md5").update(projectDir).digest("hex")
|
|
3441
3505
|
),
|
|
3442
|
-
pi:
|
|
3506
|
+
pi: join17(process.env.PI_CODING_AGENT_DIR ?? join17(homedir4(), ".pi", "agent"), "sessions"),
|
|
3443
3507
|
dsh: void 0
|
|
3444
3508
|
};
|
|
3445
3509
|
const path = values.directory ?? defaults[agent];
|
|
@@ -3497,66 +3561,6 @@ import { createInterface as createInterface2 } from "node:readline/promises";
|
|
|
3497
3561
|
// apps/client/connection.ts
|
|
3498
3562
|
import { readFile as readFile5, mkdir as mkdir10 } from "node:fs/promises";
|
|
3499
3563
|
import { createInterface } from "node:readline/promises";
|
|
3500
|
-
|
|
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) {
|
|
3512
|
-
try {
|
|
3513
|
-
await writeFile3(
|
|
3514
|
-
join17(root, LOCAL_CONFIG_NAME),
|
|
3515
|
-
JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
|
|
3516
|
-
{ flag: "wx", mode: 384 }
|
|
3517
|
-
);
|
|
3518
|
-
} catch (error) {
|
|
3519
|
-
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
async function loadLocalRules(root) {
|
|
3523
|
-
let rules = [];
|
|
3524
|
-
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
|
-
}
|
|
3545
|
-
} catch (error) {
|
|
3546
|
-
if (!missing(error))
|
|
3547
|
-
throw new Error(
|
|
3548
|
-
`${LOCAL_CONFIG_NAME}: invalid or unreadable configuration; uploads paused.`,
|
|
3549
|
-
{ cause: error }
|
|
3550
|
-
);
|
|
3551
|
-
}
|
|
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
|
-
};
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
// apps/client/connection.ts
|
|
3560
3564
|
async function connectProject(state, root, directory, options) {
|
|
3561
3565
|
const lock = acquireRootLock(root, directory);
|
|
3562
3566
|
try {
|
|
@@ -3935,8 +3939,8 @@ var PublicationQueue = class {
|
|
|
3935
3939
|
pending.batch.changes.slice(pending.nextIndex, end).flatMap((change) => change.kind === "upsert" ? [change.sha256] : [])
|
|
3936
3940
|
)
|
|
3937
3941
|
];
|
|
3938
|
-
const { missing:
|
|
3939
|
-
for (const hash2 of
|
|
3942
|
+
const { missing: missing3 } = hashes.length ? await this.transport.missingBlobs(hashes) : { missing: [] };
|
|
3943
|
+
for (const hash2 of missing3) {
|
|
3940
3944
|
if (!hashes.includes(hash2)) throw new Error("Server requested unrelated content.");
|
|
3941
3945
|
const content = await this.blobs.get(hash2);
|
|
3942
3946
|
if (!content) throw new Error("A queued content snapshot is missing from local storage.");
|
|
@@ -5001,10 +5005,10 @@ var SessionSynchronizer = class {
|
|
|
5001
5005
|
async flush(source) {
|
|
5002
5006
|
const request = source.pending;
|
|
5003
5007
|
try {
|
|
5004
|
-
const { missing:
|
|
5005
|
-
if (
|
|
5008
|
+
const { missing: missing3 } = await this.client.missingBlobs([request.blob.sha256]);
|
|
5009
|
+
if (missing3.some((hash2) => hash2 !== request.blob.sha256))
|
|
5006
5010
|
throw new Error("Server requested unrelated session content.");
|
|
5007
|
-
if (
|
|
5011
|
+
if (missing3.length) {
|
|
5008
5012
|
const bytes = await this.blobs.get(request.blob.sha256);
|
|
5009
5013
|
if (!bytes) throw new Error("Queued session snapshot is missing.");
|
|
5010
5014
|
const ref = await this.client.uploadBlob(request.blob.sha256, bytes);
|