@teammy/cli 0.0.10 → 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 +31 -5
- package/bin/session-hook.js +14 -0
- package/bin/teammy.js +8 -1
- package/dist/hook.js +340 -0
- package/dist/main.js +2175 -961
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/client/main.ts
|
|
2
|
-
import { parseArgs as
|
|
3
|
-
import { resolve as
|
|
2
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
3
|
+
import { resolve as resolve15 } from "node:path";
|
|
4
4
|
|
|
5
5
|
// packages/protocol/project.ts
|
|
6
6
|
import { z as z3 } from "zod";
|
|
@@ -19,7 +19,9 @@ var MembersSchema = z.object({ members: z.array(NameSchema) });
|
|
|
19
19
|
|
|
20
20
|
// packages/protocol/files.ts
|
|
21
21
|
import { z as z2 } from "zod";
|
|
22
|
-
var MAX_BLOB_BYTES =
|
|
22
|
+
var MAX_BLOB_BYTES = 128 * 1024 * 1024;
|
|
23
|
+
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
24
|
+
var BLOB_TRANSFER_TIMEOUT_MS = 3e5;
|
|
23
25
|
var MAX_BATCH_FILES = 200;
|
|
24
26
|
var HashSchema = z2.string().regex(/^[a-f0-9]{64}$/);
|
|
25
27
|
var RevisionSchema = z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
@@ -33,25 +35,29 @@ var BlobRefSchema = z2.object({
|
|
|
33
35
|
sha256: HashSchema,
|
|
34
36
|
size: z2.number().int().min(0).max(MAX_BLOB_BYTES)
|
|
35
37
|
});
|
|
38
|
+
var FileBlobRefSchema = BlobRefSchema.extend({
|
|
39
|
+
size: z2.number().int().min(0).max(MAX_FILE_BYTES)
|
|
40
|
+
});
|
|
36
41
|
var FileChangeSchema = z2.discriminatedUnion("kind", [
|
|
37
|
-
|
|
42
|
+
FileBlobRefSchema.extend({ kind: z2.literal("upsert"), path: FilePathSchema }).strict(),
|
|
38
43
|
z2.object({ kind: z2.literal("delete"), path: FilePathSchema }).strict()
|
|
39
44
|
]);
|
|
40
45
|
var FileCommitSchema = z2.object({
|
|
41
46
|
operationId: z2.uuid(),
|
|
42
47
|
parentRevision: RevisionSchema,
|
|
43
|
-
changes: z2.array(FileChangeSchema)
|
|
48
|
+
changes: z2.array(FileChangeSchema)
|
|
44
49
|
}).strict().refine(
|
|
45
50
|
(batch) => new Set(batch.changes.map((change) => change.path)).size === batch.changes.length,
|
|
46
|
-
"Duplicate paths in a
|
|
51
|
+
"Duplicate paths in a publication."
|
|
47
52
|
);
|
|
48
53
|
var CommitResultSchema = z2.object({ operationId: z2.uuid(), revision: RevisionSchema });
|
|
49
|
-
var FileEntrySchema =
|
|
54
|
+
var FileEntrySchema = FileBlobRefSchema.extend({
|
|
50
55
|
path: FilePathSchema,
|
|
51
56
|
revision: RevisionSchema
|
|
52
57
|
});
|
|
53
58
|
var ManifestSchema = z2.object({
|
|
54
59
|
revision: RevisionSchema,
|
|
60
|
+
complete: z2.boolean().default(false),
|
|
55
61
|
files: z2.array(FileEntrySchema),
|
|
56
62
|
nextCursor: z2.string().nullable()
|
|
57
63
|
});
|
|
@@ -105,12 +111,8 @@ var ProjectConfigSchema = z3.object({
|
|
|
105
111
|
}).strict();
|
|
106
112
|
var SetupSchema = z3.object({
|
|
107
113
|
operationId: z3.uuid(),
|
|
108
|
-
projectName: z3.string().trim().min(1).max(120),
|
|
109
|
-
description: z3.string().max(2e3).default(""),
|
|
110
114
|
publicServerUrl: ServerUrlSchema,
|
|
111
|
-
adminId: NameSchema
|
|
112
|
-
members: z3.array(NameSchema).max(100).default([]),
|
|
113
|
-
sync: SyncPolicySchema.default(DEFAULT_POLICY)
|
|
115
|
+
adminId: NameSchema
|
|
114
116
|
}).strict();
|
|
115
117
|
var SetupStatusSchema = z3.object({
|
|
116
118
|
initialized: z3.boolean()
|
|
@@ -148,7 +150,8 @@ var MemberUpdateSchema = z3.object({
|
|
|
148
150
|
var ProjectCreateSchema = z3.object({
|
|
149
151
|
operationId: z3.uuid(),
|
|
150
152
|
projectName: z3.string().trim().min(1).max(120),
|
|
151
|
-
description: z3.string().max(2e3).default("")
|
|
153
|
+
description: z3.string().max(2e3).default(""),
|
|
154
|
+
workspacePath: z3.string().trim().min(1).optional()
|
|
152
155
|
}).strict();
|
|
153
156
|
var ProjectSummarySchema = z3.object({
|
|
154
157
|
projectId: NameSchema,
|
|
@@ -189,113 +192,136 @@ var UserUpdateSchema = z3.object({
|
|
|
189
192
|
}).strict();
|
|
190
193
|
|
|
191
194
|
// packages/protocol/sessions.ts
|
|
195
|
+
import { z as z5 } from "zod";
|
|
196
|
+
|
|
197
|
+
// packages/protocol/session-analysis.ts
|
|
192
198
|
import { z as z4 } from "zod";
|
|
193
|
-
var
|
|
194
|
-
var
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
var SessionAnalysisKindSchema = z4.enum(["overview", "handoff"]);
|
|
200
|
+
var SessionOverviewOutputSchema = z4.object({ title: z4.string().trim().min(1), summary: z4.string().trim().min(1) }).strict();
|
|
201
|
+
var SessionHandoffOutputSchema = z4.object({ markdown: z4.string().trim().min(1) }).strict();
|
|
202
|
+
var SessionOverviewSchema = SessionOverviewOutputSchema.extend({
|
|
203
|
+
sourceRevision: RevisionSchema,
|
|
204
|
+
sourceHash: HashSchema,
|
|
205
|
+
generatedAt: z4.string(),
|
|
206
|
+
stale: z4.boolean()
|
|
207
|
+
});
|
|
208
|
+
var SessionAnalysisStateSchema = z4.object({
|
|
209
|
+
kind: SessionAnalysisKindSchema,
|
|
210
|
+
status: z4.enum(["absent", "queued", "running", "succeeded", "failed"]),
|
|
211
|
+
available: z4.boolean(),
|
|
212
|
+
currentRevision: RevisionSchema,
|
|
213
|
+
sourceRevision: RevisionSchema.nullable(),
|
|
214
|
+
stale: z4.boolean(),
|
|
215
|
+
result: z4.union([SessionOverviewOutputSchema, SessionHandoffOutputSchema]).nullable(),
|
|
216
|
+
generatedAt: z4.string().nullable(),
|
|
217
|
+
error: z4.string().nullable()
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// packages/protocol/sessions.ts
|
|
221
|
+
var AgentSchema = z5.enum(["claude-code", "codex", "cursor", "pi", "dsh"]);
|
|
222
|
+
var SessionSnapshotSchema = z5.object({
|
|
223
|
+
formatVersion: z5.literal(1),
|
|
224
|
+
adapterVersion: z5.literal(1),
|
|
197
225
|
agent: AgentSchema,
|
|
198
|
-
nativeId:
|
|
199
|
-
cwd:
|
|
200
|
-
messages:
|
|
201
|
-
|
|
202
|
-
id:
|
|
203
|
-
role:
|
|
204
|
-
text:
|
|
205
|
-
source:
|
|
226
|
+
nativeId: z5.string().min(1).max(512),
|
|
227
|
+
cwd: z5.string().min(1).max(4096),
|
|
228
|
+
messages: z5.array(
|
|
229
|
+
z5.object({
|
|
230
|
+
id: z5.string().min(1).max(1024),
|
|
231
|
+
role: z5.enum(["user", "assistant"]),
|
|
232
|
+
text: z5.string().min(1),
|
|
233
|
+
source: z5.string().min(1).max(1024)
|
|
206
234
|
}).strict()
|
|
207
|
-
)
|
|
235
|
+
)
|
|
208
236
|
}).strict().refine(
|
|
209
237
|
(s) => new Set(s.messages.map((m) => m.id)).size === s.messages.length,
|
|
210
238
|
"Duplicate message IDs."
|
|
211
239
|
);
|
|
212
|
-
var SessionCommitSchema =
|
|
213
|
-
operationId:
|
|
240
|
+
var SessionCommitSchema = z5.object({
|
|
241
|
+
operationId: z5.uuid(),
|
|
214
242
|
sessionKey: HashSchema,
|
|
215
243
|
parentRevision: RevisionSchema,
|
|
216
244
|
blob: BlobRefSchema
|
|
217
245
|
}).strict();
|
|
218
|
-
var SessionInfoSchema =
|
|
246
|
+
var SessionInfoSchema = z5.object({
|
|
219
247
|
owner: NameSchema,
|
|
220
248
|
sessionKey: HashSchema,
|
|
221
249
|
agent: AgentSchema,
|
|
222
|
-
nativeId:
|
|
223
|
-
cwd:
|
|
224
|
-
title:
|
|
250
|
+
nativeId: z5.string(),
|
|
251
|
+
cwd: z5.string(),
|
|
252
|
+
title: z5.string(),
|
|
225
253
|
revision: RevisionSchema,
|
|
226
|
-
updatedAt:
|
|
227
|
-
messageCount:
|
|
228
|
-
blob: BlobRefSchema
|
|
254
|
+
updatedAt: z5.string(),
|
|
255
|
+
messageCount: z5.number().int().nonnegative(),
|
|
256
|
+
blob: BlobRefSchema,
|
|
257
|
+
overview: SessionOverviewSchema.optional()
|
|
229
258
|
});
|
|
230
|
-
var SessionListSchema =
|
|
231
|
-
sessions:
|
|
259
|
+
var SessionListSchema = z5.object({
|
|
260
|
+
sessions: z5.array(SessionInfoSchema),
|
|
232
261
|
nextCursor: RevisionSchema.nullable()
|
|
233
262
|
});
|
|
234
|
-
var SessionStatsSchema =
|
|
235
|
-
totalSessions:
|
|
236
|
-
totalMessages:
|
|
237
|
-
memberCount:
|
|
238
|
-
updatedAt:
|
|
263
|
+
var SessionStatsSchema = z5.object({
|
|
264
|
+
totalSessions: z5.number().int().nonnegative(),
|
|
265
|
+
totalMessages: z5.number().int().nonnegative(),
|
|
266
|
+
memberCount: z5.number().int().nonnegative(),
|
|
267
|
+
updatedAt: z5.string().nullable()
|
|
239
268
|
});
|
|
240
|
-
var SessionScopeSchema =
|
|
269
|
+
var SessionScopeSchema = z5.object({
|
|
241
270
|
owner: NameSchema.optional(),
|
|
242
271
|
sessionKey: HashSchema.optional()
|
|
243
272
|
}).strict().refine((v) => !v.sessionKey || !!v.owner, "A session scope requires an owner.");
|
|
244
|
-
var SessionSearchSchema =
|
|
245
|
-
matches:
|
|
246
|
-
|
|
273
|
+
var SessionSearchSchema = z5.object({
|
|
274
|
+
matches: z5.array(
|
|
275
|
+
z5.object({
|
|
247
276
|
owner: NameSchema,
|
|
248
277
|
sessionKey: HashSchema,
|
|
249
278
|
revision: RevisionSchema,
|
|
250
|
-
messageId:
|
|
251
|
-
role:
|
|
252
|
-
snippet:
|
|
253
|
-
title:
|
|
279
|
+
messageId: z5.string(),
|
|
280
|
+
role: z5.enum(["user", "assistant"]),
|
|
281
|
+
snippet: z5.string(),
|
|
282
|
+
title: z5.string()
|
|
254
283
|
})
|
|
255
284
|
),
|
|
256
285
|
nextCursor: RevisionSchema.nullable()
|
|
257
286
|
});
|
|
258
287
|
|
|
259
288
|
// packages/transport/hub-client.ts
|
|
260
|
-
import { z as
|
|
289
|
+
import { z as z8 } from "zod";
|
|
261
290
|
import { createHash } from "node:crypto";
|
|
262
291
|
|
|
263
292
|
// packages/protocol/index.ts
|
|
264
|
-
import { z as
|
|
293
|
+
import { z as z6 } from "zod";
|
|
265
294
|
var PROTOCOL_VERSION = 1;
|
|
266
|
-
var HealthSchema =
|
|
267
|
-
var ServerInfoSchema =
|
|
268
|
-
name:
|
|
269
|
-
version:
|
|
270
|
-
protocolVersion:
|
|
271
|
-
capabilities:
|
|
295
|
+
var HealthSchema = z6.object({ status: z6.literal("ok") });
|
|
296
|
+
var ServerInfoSchema = z6.object({
|
|
297
|
+
name: z6.literal("teammy"),
|
|
298
|
+
version: z6.string().min(1),
|
|
299
|
+
protocolVersion: z6.literal(PROTOCOL_VERSION),
|
|
300
|
+
capabilities: z6.array(z6.string())
|
|
272
301
|
});
|
|
273
|
-
var ErrorResponseSchema =
|
|
274
|
-
error:
|
|
275
|
-
code:
|
|
276
|
-
message:
|
|
277
|
-
requestId:
|
|
302
|
+
var ErrorResponseSchema = z6.object({
|
|
303
|
+
error: z6.object({
|
|
304
|
+
code: z6.string(),
|
|
305
|
+
message: z6.string(),
|
|
306
|
+
requestId: z6.string()
|
|
278
307
|
})
|
|
279
308
|
});
|
|
280
309
|
|
|
281
310
|
// packages/protocol/shared-files.ts
|
|
282
|
-
import { z as
|
|
311
|
+
import { z as z7 } from "zod";
|
|
283
312
|
var PROJECT_FILE_OWNER = "_project";
|
|
284
|
-
var FileOwnerSchema =
|
|
285
|
-
var SharedPathSchema =
|
|
313
|
+
var FileOwnerSchema = z7.union([NameSchema, z7.literal(PROJECT_FILE_OWNER)]);
|
|
314
|
+
var SharedPathSchema = z7.string().max(255).refine((path) => {
|
|
286
315
|
const directory = path.endsWith("/");
|
|
287
316
|
const name2 = directory ? path.slice(0, -1) : path;
|
|
288
317
|
return FilePathSchema.safeParse(name2).success && !name2.includes("/") && !name2.startsWith(".") && !/^_?teammy/i.test(name2) && (directory ? name2.startsWith("_") : name2.includes("."));
|
|
289
318
|
}, "Use a root file (README.md) or an underscore-prefixed directory (_template/).");
|
|
290
|
-
var SharedFilesConfigSchema =
|
|
319
|
+
var SharedFilesConfigSchema = z7.object({
|
|
291
320
|
maintainer: NameSchema.nullable(),
|
|
292
|
-
paths:
|
|
321
|
+
paths: z7.array(SharedPathSchema).max(30).refine((paths) => new Set(paths.map((p) => p.toLowerCase())).size === paths.length),
|
|
293
322
|
revision: RevisionSchema
|
|
294
323
|
}).strict();
|
|
295
324
|
var SharedFilesUpdateSchema = SharedFilesConfigSchema.omit({ revision: true }).extend({ expectedRevision: RevisionSchema }).strict();
|
|
296
|
-
var SharedFileCommitSchema = FileCommitSchema.safeExtend({
|
|
297
|
-
configRevision: RevisionSchema
|
|
298
|
-
});
|
|
299
325
|
function privateWorkspacePath(path) {
|
|
300
326
|
return path.split("/").some((part) => /^\.teammy(?:[-.]|$)/i.test(part));
|
|
301
327
|
}
|
|
@@ -359,15 +385,23 @@ var HubClient = class {
|
|
|
359
385
|
}
|
|
360
386
|
uploadBlob(hash2, content) {
|
|
361
387
|
if (content.byteLength > MAX_BLOB_BYTES)
|
|
362
|
-
throw new Error("Content exceeds the
|
|
363
|
-
return this.request(
|
|
388
|
+
throw new Error("Content exceeds the binary object size limit.");
|
|
389
|
+
return this.request(
|
|
390
|
+
"PUT",
|
|
391
|
+
`api/v1/blobs/${HashSchema.parse(hash2)}`,
|
|
392
|
+
BlobRefSchema,
|
|
393
|
+
content,
|
|
394
|
+
BLOB_TRANSFER_TIMEOUT_MS
|
|
395
|
+
);
|
|
364
396
|
}
|
|
365
397
|
async downloadBlob(hash2) {
|
|
366
398
|
const { response, signal } = await this.fetchResponse(
|
|
367
399
|
"GET",
|
|
368
|
-
`api/v1/blobs/${HashSchema.parse(hash2)}
|
|
400
|
+
`api/v1/blobs/${HashSchema.parse(hash2)}`,
|
|
401
|
+
void 0,
|
|
402
|
+
BLOB_TRANSFER_TIMEOUT_MS
|
|
369
403
|
);
|
|
370
|
-
if (!response.ok) return this.decode(response, signal,
|
|
404
|
+
if (!response.ok) return this.decode(response, signal, z8.never());
|
|
371
405
|
const reader = response.body?.getReader();
|
|
372
406
|
if (!reader) throw new Error("Server returned no content body.");
|
|
373
407
|
const parts = [];
|
|
@@ -390,7 +424,7 @@ var HubClient = class {
|
|
|
390
424
|
return bytes;
|
|
391
425
|
}
|
|
392
426
|
commitFiles(batch) {
|
|
393
|
-
return this.
|
|
427
|
+
return this.publishFiles(batch);
|
|
394
428
|
}
|
|
395
429
|
fileManifest(owner, revision, after = "", limit = 200) {
|
|
396
430
|
const query = new URLSearchParams({ after, limit: String(limit) });
|
|
@@ -412,7 +446,7 @@ var HubClient = class {
|
|
|
412
446
|
return this.request(
|
|
413
447
|
"PUT",
|
|
414
448
|
"api/v1/devices/self/status",
|
|
415
|
-
|
|
449
|
+
z8.object({ ok: z8.literal(true) }),
|
|
416
450
|
status
|
|
417
451
|
);
|
|
418
452
|
}
|
|
@@ -426,17 +460,27 @@ var HubClient = class {
|
|
|
426
460
|
return this.request("GET", "api/v1/project/shared-files", SharedFilesConfigSchema);
|
|
427
461
|
}
|
|
428
462
|
commitSharedFiles(batch, configRevision) {
|
|
429
|
-
return this.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
463
|
+
return this.publishFiles({ ...batch, configRevision });
|
|
464
|
+
}
|
|
465
|
+
async publishFiles(publication) {
|
|
466
|
+
const bytes = Buffer.from(JSON.stringify(publication));
|
|
467
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
468
|
+
if ((await this.missingBlobs([sha256])).missing.length) await this.uploadBlob(sha256, bytes);
|
|
469
|
+
return this.request(
|
|
470
|
+
"POST",
|
|
471
|
+
"api/v1/files/publications",
|
|
472
|
+
CommitResultSchema,
|
|
473
|
+
{ manifest: { sha256, size: bytes.byteLength } },
|
|
474
|
+
BLOB_TRANSFER_TIMEOUT_MS
|
|
475
|
+
);
|
|
433
476
|
}
|
|
434
477
|
commitSession(commit) {
|
|
435
478
|
return this.request(
|
|
436
479
|
"POST",
|
|
437
480
|
"api/v1/sessions/commits",
|
|
438
481
|
CommitResultSchema,
|
|
439
|
-
SessionCommitSchema.parse(commit)
|
|
482
|
+
SessionCommitSchema.parse(commit),
|
|
483
|
+
BLOB_TRANSFER_TIMEOUT_MS
|
|
440
484
|
);
|
|
441
485
|
}
|
|
442
486
|
sessions(after = 0, limit = 50) {
|
|
@@ -460,14 +504,14 @@ var HubClient = class {
|
|
|
460
504
|
SessionSearchSchema
|
|
461
505
|
);
|
|
462
506
|
}
|
|
463
|
-
async request(method, path, schema, requestBody) {
|
|
464
|
-
const { response, signal } = await this.fetchResponse(method, path, requestBody);
|
|
507
|
+
async request(method, path, schema, requestBody, timeoutMs = this.timeoutMs) {
|
|
508
|
+
const { response, signal } = await this.fetchResponse(method, path, requestBody, timeoutMs);
|
|
465
509
|
return this.decode(response, signal, schema);
|
|
466
510
|
}
|
|
467
|
-
async fetchResponse(method, path, requestBody) {
|
|
511
|
+
async fetchResponse(method, path, requestBody, timeoutMs = this.timeoutMs) {
|
|
468
512
|
if (this.credentials?.workspaceId && path.startsWith("api/v1/") && !["api/v1/bind", "api/v1/server"].includes(path))
|
|
469
513
|
path = `api/v1/projects/${encodeURIComponent(this.credentials.workspaceId)}/${path.slice("api/v1/".length)}`;
|
|
470
|
-
const timeout = AbortSignal.timeout(
|
|
514
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
471
515
|
const signal = this.abortSignal ? AbortSignal.any([timeout, this.abortSignal]) : timeout;
|
|
472
516
|
const binary = requestBody instanceof Uint8Array;
|
|
473
517
|
const response = await fetch(new URL(path, this.baseUrl), {
|
|
@@ -480,7 +524,9 @@ var HubClient = class {
|
|
|
480
524
|
"x-teammy-device": this.credentials.deviceId
|
|
481
525
|
} : {}
|
|
482
526
|
},
|
|
483
|
-
...requestBody === void 0 ? {} : {
|
|
527
|
+
...requestBody === void 0 ? {} : {
|
|
528
|
+
body: binary ? requestBody : JSON.stringify(requestBody)
|
|
529
|
+
},
|
|
484
530
|
signal,
|
|
485
531
|
redirect: "error"
|
|
486
532
|
});
|
|
@@ -518,11 +564,18 @@ var HubClient = class {
|
|
|
518
564
|
};
|
|
519
565
|
|
|
520
566
|
// apps/client/sessions/command.ts
|
|
521
|
-
import { createHash as
|
|
522
|
-
import { realpath as
|
|
523
|
-
import { homedir as
|
|
524
|
-
import { dirname as
|
|
525
|
-
import { parseArgs } from "node:util";
|
|
567
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
568
|
+
import { realpath as realpath6 } from "node:fs/promises";
|
|
569
|
+
import { homedir as homedir4 } from "node:os";
|
|
570
|
+
import { dirname as dirname9, join as join17, resolve as resolve8 } from "node:path";
|
|
571
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
572
|
+
|
|
573
|
+
// packages/protocol/file-publication.ts
|
|
574
|
+
import { z as z9 } from "zod";
|
|
575
|
+
var FilePublicationSchema = FileCommitSchema.safeExtend({
|
|
576
|
+
configRevision: RevisionSchema.optional()
|
|
577
|
+
}).strict();
|
|
578
|
+
var PublishFilesSchema = z9.object({ manifest: BlobRefSchema }).strict();
|
|
526
579
|
|
|
527
580
|
// packages/state/sqlite.ts
|
|
528
581
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
@@ -539,7 +592,7 @@ function transaction(db, action) {
|
|
|
539
592
|
throw error;
|
|
540
593
|
}
|
|
541
594
|
}
|
|
542
|
-
function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
595
|
+
function openDatabase(path, migrations, busyTimeoutMs = 5e3, journalMode = "WAL") {
|
|
543
596
|
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0)
|
|
544
597
|
throw new Error("Invalid SQLite busy timeout.");
|
|
545
598
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -547,7 +600,7 @@ function openDatabase(path, migrations, busyTimeoutMs = 5e3) {
|
|
|
547
600
|
try {
|
|
548
601
|
chmodSync(path, 384);
|
|
549
602
|
db.exec(
|
|
550
|
-
`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;`
|
|
551
604
|
);
|
|
552
605
|
transaction(db, () => {
|
|
553
606
|
const version = Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
@@ -583,8 +636,14 @@ var FilePublicationState = class {
|
|
|
583
636
|
}
|
|
584
637
|
publication(root) {
|
|
585
638
|
this.db.prepare(`INSERT OR IGNORE INTO ${this.tables.publications}(root) VALUES (?)`).run(root);
|
|
586
|
-
const row = this.db.prepare(
|
|
587
|
-
|
|
639
|
+
const row = this.db.prepare(
|
|
640
|
+
`SELECT revision, initialized, complete FROM ${this.tables.publications} WHERE root=?`
|
|
641
|
+
).get(root);
|
|
642
|
+
return {
|
|
643
|
+
revision: Number(row.revision),
|
|
644
|
+
initialized: Boolean(row.initialized),
|
|
645
|
+
complete: Boolean(row.complete)
|
|
646
|
+
};
|
|
588
647
|
}
|
|
589
648
|
initializePublication(root, revision, files, options = {}) {
|
|
590
649
|
transaction(this.db, () => {
|
|
@@ -592,8 +651,8 @@ var FilePublicationState = class {
|
|
|
592
651
|
if (this.pending(root))
|
|
593
652
|
throw new Error("Cannot replace a baseline with a publication pending.");
|
|
594
653
|
this.db.prepare(
|
|
595
|
-
`INSERT INTO ${this.tables.publications} VALUES (?, ?, 1) ON CONFLICT(root) DO UPDATE SET revision=excluded.revision, initialized=1`
|
|
596
|
-
).run(root, revision);
|
|
654
|
+
`INSERT INTO ${this.tables.publications}(root,revision,initialized,complete) VALUES (?, ?, 1, ?) ON CONFLICT(root) DO UPDATE SET revision=excluded.revision, initialized=1,complete=excluded.complete`
|
|
655
|
+
).run(root, revision, Number(options.complete ?? false));
|
|
597
656
|
this.db.prepare(`DELETE FROM ${this.tables.owner_files} WHERE root=?`).run(root);
|
|
598
657
|
for (const file of files)
|
|
599
658
|
this.db.prepare(`INSERT INTO ${this.tables.owner_files} VALUES (?, ?, ?, ?)`).run(root, file.path, file.sha256, file.size);
|
|
@@ -606,17 +665,18 @@ var FilePublicationState = class {
|
|
|
606
665
|
}
|
|
607
666
|
pending(root) {
|
|
608
667
|
const row = this.db.prepare(
|
|
609
|
-
`SELECT request_json, attempts, retry_at, last_error FROM ${this.tables.outbox} WHERE root=?`
|
|
668
|
+
`SELECT request_json, attempts, retry_at, last_error, next_index FROM ${this.tables.outbox} WHERE root=?`
|
|
610
669
|
).get(root);
|
|
611
670
|
return row ? {
|
|
612
|
-
batch:
|
|
671
|
+
batch: FilePublicationSchema.parse(JSON.parse(String(row.request_json))),
|
|
672
|
+
nextIndex: Number(row.next_index),
|
|
613
673
|
attempts: Number(row.attempts),
|
|
614
674
|
retryAt: Number(row.retry_at),
|
|
615
675
|
lastError: row.last_error === null ? null : String(row.last_error)
|
|
616
676
|
} : void 0;
|
|
617
677
|
}
|
|
618
678
|
enqueue(root, batch) {
|
|
619
|
-
const fixed =
|
|
679
|
+
const fixed = FilePublicationSchema.parse(batch);
|
|
620
680
|
transaction(this.db, () => {
|
|
621
681
|
const publication = this.publication(root);
|
|
622
682
|
if (!publication.initialized || publication.revision !== fixed.parentRevision)
|
|
@@ -625,6 +685,11 @@ var FilePublicationState = class {
|
|
|
625
685
|
this.db.prepare(`INSERT INTO ${this.tables.outbox}(root, request_json) VALUES (?, ?)`).run(root, JSON.stringify(fixed));
|
|
626
686
|
});
|
|
627
687
|
}
|
|
688
|
+
uploaded(root, operationId, nextIndex) {
|
|
689
|
+
this.db.prepare(
|
|
690
|
+
`UPDATE ${this.tables.outbox} SET next_index=? WHERE root=? AND json_extract(request_json,'$.operationId')=?`
|
|
691
|
+
).run(nextIndex, root, operationId);
|
|
692
|
+
}
|
|
628
693
|
acknowledge(root, result2) {
|
|
629
694
|
transaction(this.db, () => {
|
|
630
695
|
const pending = this.pending(root);
|
|
@@ -638,7 +703,7 @@ var FilePublicationState = class {
|
|
|
638
703
|
`INSERT INTO ${this.tables.owner_files} VALUES (?, ?, ?, ?) ON CONFLICT(root, path) DO UPDATE SET hash=excluded.hash, size=excluded.size`
|
|
639
704
|
).run(root, change.path, change.sha256, change.size);
|
|
640
705
|
}
|
|
641
|
-
this.db.prepare(`UPDATE ${this.tables.publications} SET revision
|
|
706
|
+
this.db.prepare(`UPDATE ${this.tables.publications} SET revision=?,complete=1 WHERE root=?`).run(result2.revision, root);
|
|
642
707
|
this.db.prepare(`DELETE FROM ${this.tables.outbox} WHERE root=?`).run(root);
|
|
643
708
|
});
|
|
644
709
|
}
|
|
@@ -655,6 +720,19 @@ var FilePublicationState = class {
|
|
|
655
720
|
}
|
|
656
721
|
};
|
|
657
722
|
|
|
723
|
+
// apps/client/files/atomic-schema.ts
|
|
724
|
+
var atomicFilesClientMigration = ["", "project_"].map(
|
|
725
|
+
(prefix) => `
|
|
726
|
+
ALTER TABLE ${prefix}publications ADD COLUMN complete INTEGER NOT NULL DEFAULT 0;
|
|
727
|
+
ALTER TABLE ${prefix}outbox ADD COLUMN next_index INTEGER NOT NULL DEFAULT 0;
|
|
728
|
+
-- Old outboxes contain a partial batch, not a complete publication. Reconcile
|
|
729
|
+
-- against the server (including a possibly lost acknowledgement) before rebuilding.
|
|
730
|
+
DELETE FROM ${prefix}outbox;
|
|
731
|
+
UPDATE ${prefix}publications SET initialized=0;
|
|
732
|
+
UPDATE ${prefix}file_index SET dirty=1,event_pending=1,generation=generation+1;
|
|
733
|
+
`
|
|
734
|
+
).join("\n");
|
|
735
|
+
|
|
658
736
|
// apps/client/files/state.ts
|
|
659
737
|
var syncMigration = String.raw`
|
|
660
738
|
CREATE TABLE file_index (
|
|
@@ -872,33 +950,191 @@ var RuntimeState = class {
|
|
|
872
950
|
};
|
|
873
951
|
|
|
874
952
|
// apps/client/state.ts
|
|
875
|
-
import { randomUUID as
|
|
876
|
-
import { join as
|
|
877
|
-
import { z as
|
|
953
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
954
|
+
import { join as join7, relative as relative2, isAbsolute, sep as sep2 } from "node:path";
|
|
955
|
+
import { z as z12 } from "zod";
|
|
956
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
957
|
+
|
|
958
|
+
// apps/client/runtime/state-access.ts
|
|
959
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
960
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync } from "node:fs";
|
|
961
|
+
import { join as join2 } from "node:path";
|
|
962
|
+
|
|
963
|
+
// apps/client/runtime/metadata.ts
|
|
964
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
965
|
+
import { dirname as dirname2, join } from "node:path";
|
|
966
|
+
import { randomUUID } from "node:crypto";
|
|
967
|
+
function writePrivateJson(path, value) {
|
|
968
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
969
|
+
const temp = path + `.${randomUUID()}.tmp`;
|
|
970
|
+
writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
971
|
+
renameSync(temp, path);
|
|
972
|
+
}
|
|
973
|
+
function maintenancePath(directory) {
|
|
974
|
+
return join(directory, "runtime", "maintenance.json");
|
|
975
|
+
}
|
|
976
|
+
function readMaintenance(directory) {
|
|
977
|
+
const path = maintenancePath(directory);
|
|
978
|
+
if (!existsSync(path)) return null;
|
|
979
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
980
|
+
if (value.format !== 1 || !["repair", "upgrade", "paused", "uninstalled"].includes(value.reason))
|
|
981
|
+
throw new Error("Unsupported maintenance record. Inspect teammy logs --all.");
|
|
982
|
+
return value;
|
|
983
|
+
}
|
|
984
|
+
function writeMaintenance(directory, record) {
|
|
985
|
+
writePrivateJson(maintenancePath(directory), {
|
|
986
|
+
...record,
|
|
987
|
+
format: 1,
|
|
988
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// apps/client/runtime/state-access.ts
|
|
993
|
+
function stateAccess(directory, exclusive = false) {
|
|
994
|
+
const path = join2(directory, "runtime", "access.sqlite3");
|
|
995
|
+
mkdirSync3(join2(directory, "runtime"), { recursive: true, mode: 448 });
|
|
996
|
+
const db = new DatabaseSync2(path);
|
|
997
|
+
try {
|
|
998
|
+
chmodSync2(path, 384);
|
|
999
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1000
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='access'").get()) {
|
|
1001
|
+
db.exec(
|
|
1002
|
+
"PRAGMA journal_mode=DELETE; CREATE TABLE IF NOT EXISTS access(id INTEGER PRIMARY KEY); INSERT OR IGNORE INTO access VALUES(1)"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
db.exec(exclusive ? "BEGIN EXCLUSIVE" : "BEGIN");
|
|
1006
|
+
db.prepare("SELECT id FROM access").get();
|
|
1007
|
+
if (!exclusive) {
|
|
1008
|
+
const record = readMaintenance(directory);
|
|
1009
|
+
if (record && ["repair", "upgrade"].includes(record.reason))
|
|
1010
|
+
throw new Error("Client maintenance is pending. Run teammy repair.");
|
|
1011
|
+
}
|
|
1012
|
+
return db;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
db.close();
|
|
1015
|
+
if (error instanceof Error && /locked|busy/.test(error.message))
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
"Client state is in use. Finish the local foreground command, then run teammy repair."
|
|
1018
|
+
);
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function legacyStateLocks(directory) {
|
|
1023
|
+
const locks = [];
|
|
1024
|
+
try {
|
|
1025
|
+
const path = join2(directory, "locks");
|
|
1026
|
+
if (existsSync2(path))
|
|
1027
|
+
for (const file of readdirSync(path)) {
|
|
1028
|
+
if (!/^[a-f0-9]{64}\.sqlite3$/.test(file)) continue;
|
|
1029
|
+
const db = new DatabaseSync2(join2(path, file));
|
|
1030
|
+
locks.push(db);
|
|
1031
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN IMMEDIATE");
|
|
1032
|
+
}
|
|
1033
|
+
return { close: () => locks.forEach((db) => db.close()) };
|
|
1034
|
+
} catch {
|
|
1035
|
+
locks.forEach((db) => db.close());
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
"An older sync process is still using this client. Stop its foreground command before repair."
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function stateVersion(directory) {
|
|
1042
|
+
const path = join2(directory, "client.sqlite3");
|
|
1043
|
+
if (!existsSync2(path)) return 0;
|
|
1044
|
+
const db = new DatabaseSync2(path, { readOnly: true });
|
|
1045
|
+
try {
|
|
1046
|
+
return Number(db.prepare("PRAGMA user_version").get().user_version);
|
|
1047
|
+
} finally {
|
|
1048
|
+
db.close();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
878
1051
|
|
|
879
1052
|
// apps/client/workspace.ts
|
|
880
1053
|
import { execFile } from "node:child_process";
|
|
881
1054
|
import { promisify } from "node:util";
|
|
882
1055
|
import { lstat as lstat2, mkdir as mkdir2, realpath as realpath2, rmdir as rmdir2 } from "node:fs/promises";
|
|
883
|
-
import { basename, dirname as
|
|
1056
|
+
import { basename, dirname as dirname4, join as join5, resolve } from "node:path";
|
|
884
1057
|
|
|
885
1058
|
// apps/client/files/local-fs.ts
|
|
886
|
-
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
887
|
-
import { constants } from "node:fs";
|
|
888
|
-
import { lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink } from "node:fs/promises";
|
|
889
|
-
import { dirname as
|
|
1059
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
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";
|
|
890
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
|
+
}
|
|
891
1076
|
function missing(error) {
|
|
892
1077
|
return error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR"].includes(String(error.code));
|
|
893
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
|
+
}
|
|
894
1135
|
function signature(stat2) {
|
|
895
1136
|
return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeNs, stat2.ctimeNs].join(":");
|
|
896
1137
|
}
|
|
897
|
-
function ignored(path) {
|
|
898
|
-
return path.split("/").some(
|
|
899
|
-
(part) => part === ".git" || part === ".DS_Store" || /^\.teammy(?:[-.]|$)/i.test(part) || /(?:~|\.(?:swp|swx|tmp|part))$/i.test(part)
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
1138
|
async function safePath(root, path, createParents = false) {
|
|
903
1139
|
FilePathSchema.parse(path);
|
|
904
1140
|
const rootStat = await lstat(root);
|
|
@@ -907,7 +1143,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
907
1143
|
const parts = path.split("/");
|
|
908
1144
|
let parent = root;
|
|
909
1145
|
for (const part of parts.slice(0, -1)) {
|
|
910
|
-
parent =
|
|
1146
|
+
parent = join4(parent, part);
|
|
911
1147
|
if (createParents)
|
|
912
1148
|
await mkdir(parent).catch((error) => {
|
|
913
1149
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -917,7 +1153,7 @@ async function safePath(root, path, createParents = false) {
|
|
|
917
1153
|
if (!stat2.isDirectory())
|
|
918
1154
|
throw Object.assign(new Error(`Parent is not a directory: ${path}`), { code: "ENOTDIR" });
|
|
919
1155
|
}
|
|
920
|
-
return
|
|
1156
|
+
return join4(root, ...parts);
|
|
921
1157
|
}
|
|
922
1158
|
async function metadata(root, path) {
|
|
923
1159
|
try {
|
|
@@ -927,7 +1163,7 @@ async function metadata(root, path) {
|
|
|
927
1163
|
throw new Error(`Unsupported filesystem entry: ${path}`);
|
|
928
1164
|
return { signature: signature(stat2), directory: stat2.isDirectory() };
|
|
929
1165
|
} catch (error) {
|
|
930
|
-
if (
|
|
1166
|
+
if (missing2(error)) return null;
|
|
931
1167
|
throw error;
|
|
932
1168
|
}
|
|
933
1169
|
}
|
|
@@ -977,12 +1213,12 @@ async function readStable(root, path) {
|
|
|
977
1213
|
const inside = relative(await realpath(root), resolvedBefore);
|
|
978
1214
|
if (inside.startsWith(`..${sep}`) || inside === "..")
|
|
979
1215
|
throw new Error(`File escapes sync root: ${path}`);
|
|
980
|
-
const file = await
|
|
1216
|
+
const file = await open2(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
981
1217
|
try {
|
|
982
1218
|
const before = await file.stat({ bigint: true });
|
|
983
|
-
if (!before.isFile() || before.size > BigInt(
|
|
1219
|
+
if (!before.isFile() || before.size > BigInt(MAX_FILE_BYTES))
|
|
984
1220
|
throw new Error(
|
|
985
|
-
`Only regular files up to ${
|
|
1221
|
+
`Only regular files up to ${MAX_FILE_BYTES} bytes can be synchronized: ${path}`
|
|
986
1222
|
);
|
|
987
1223
|
const buffer = Buffer.alloc(Number(before.size) + 1);
|
|
988
1224
|
let count = 0;
|
|
@@ -1007,7 +1243,7 @@ async function readStable(root, path) {
|
|
|
1007
1243
|
}
|
|
1008
1244
|
async function syncDirectory(directory) {
|
|
1009
1245
|
if (process.platform === "win32") return;
|
|
1010
|
-
const handle = await
|
|
1246
|
+
const handle = await open2(directory, "r");
|
|
1011
1247
|
try {
|
|
1012
1248
|
await handle.sync();
|
|
1013
1249
|
} finally {
|
|
@@ -1016,8 +1252,8 @@ async function syncDirectory(directory) {
|
|
|
1016
1252
|
}
|
|
1017
1253
|
async function replaceFile(root, path, bytes, expected) {
|
|
1018
1254
|
const target = await safePath(root, path, true);
|
|
1019
|
-
const temporary =
|
|
1020
|
-
const file = await
|
|
1255
|
+
const temporary = join4(dirname3(target), `.teammy-${randomUUID2()}.tmp`);
|
|
1256
|
+
const file = await open2(temporary, "wx", 384);
|
|
1021
1257
|
try {
|
|
1022
1258
|
try {
|
|
1023
1259
|
await file.writeFile(bytes);
|
|
@@ -1033,15 +1269,15 @@ async function replaceFile(root, path, bytes, expected) {
|
|
|
1033
1269
|
throw new Error(`Local file changed before replacement: ${path}`);
|
|
1034
1270
|
await safePath(root, path);
|
|
1035
1271
|
await rename(temporary, target);
|
|
1036
|
-
let directory =
|
|
1272
|
+
let directory = dirname3(target);
|
|
1037
1273
|
for (; ; ) {
|
|
1038
1274
|
await syncDirectory(directory);
|
|
1039
|
-
if (directory ===
|
|
1040
|
-
directory =
|
|
1275
|
+
if (directory === dirname3(root)) break;
|
|
1276
|
+
directory = dirname3(directory);
|
|
1041
1277
|
}
|
|
1042
1278
|
} finally {
|
|
1043
1279
|
await unlink(temporary).catch((error) => {
|
|
1044
|
-
if (!
|
|
1280
|
+
if (!missing2(error)) throw error;
|
|
1045
1281
|
});
|
|
1046
1282
|
}
|
|
1047
1283
|
}
|
|
@@ -1052,16 +1288,16 @@ async function removeFile(root, path, expected) {
|
|
|
1052
1288
|
throw new Error(`Local file changed before deletion: ${path}`);
|
|
1053
1289
|
const target = await safePath(root, path);
|
|
1054
1290
|
await unlink(target);
|
|
1055
|
-
await syncDirectory(
|
|
1056
|
-
let parent =
|
|
1291
|
+
await syncDirectory(dirname3(target));
|
|
1292
|
+
let parent = dirname3(target);
|
|
1057
1293
|
while (parent !== root) {
|
|
1058
1294
|
try {
|
|
1059
1295
|
await rmdir(parent);
|
|
1060
|
-
await syncDirectory(
|
|
1296
|
+
await syncDirectory(dirname3(parent));
|
|
1061
1297
|
} catch {
|
|
1062
1298
|
break;
|
|
1063
1299
|
}
|
|
1064
|
-
parent =
|
|
1300
|
+
parent = dirname3(parent);
|
|
1065
1301
|
}
|
|
1066
1302
|
}
|
|
1067
1303
|
|
|
@@ -1069,7 +1305,7 @@ async function removeFile(root, path, expected) {
|
|
|
1069
1305
|
var exec = promisify(execFile);
|
|
1070
1306
|
async function workspacePath(input) {
|
|
1071
1307
|
const path = resolve(input);
|
|
1072
|
-
return
|
|
1308
|
+
return join5(await realpath2(dirname4(path)), basename(path));
|
|
1073
1309
|
}
|
|
1074
1310
|
async function directoryIdentity(root) {
|
|
1075
1311
|
const stat2 = await lstat2(root, { bigint: true });
|
|
@@ -1086,18 +1322,18 @@ async function verifyWorkspace(state, root) {
|
|
|
1086
1322
|
);
|
|
1087
1323
|
}
|
|
1088
1324
|
async function ignorePlan(root) {
|
|
1089
|
-
const parent =
|
|
1325
|
+
const parent = dirname4(root), name2 = basename(root);
|
|
1090
1326
|
if (/[\r\n]/.test(name2)) throw new Error("Workspace name cannot contain a newline.");
|
|
1091
1327
|
let repo = false;
|
|
1092
|
-
for (let p = parent; ; p =
|
|
1328
|
+
for (let p = parent; ; p = dirname4(p)) {
|
|
1093
1329
|
try {
|
|
1094
|
-
await lstat2(
|
|
1330
|
+
await lstat2(join5(p, ".git"));
|
|
1095
1331
|
repo = true;
|
|
1096
1332
|
break;
|
|
1097
1333
|
} catch (e) {
|
|
1098
|
-
if (!
|
|
1334
|
+
if (!missing2(e)) throw e;
|
|
1099
1335
|
}
|
|
1100
|
-
if (p ===
|
|
1336
|
+
if (p === dirname4(p)) break;
|
|
1101
1337
|
}
|
|
1102
1338
|
if (repo) {
|
|
1103
1339
|
let tracked;
|
|
@@ -1142,7 +1378,7 @@ async function prepareWorkspace(state, root, input, reconnect = false) {
|
|
|
1142
1378
|
await lstat2(root);
|
|
1143
1379
|
present = true;
|
|
1144
1380
|
} catch (e) {
|
|
1145
|
-
if (!
|
|
1381
|
+
if (!missing2(e)) throw e;
|
|
1146
1382
|
}
|
|
1147
1383
|
if (present) {
|
|
1148
1384
|
if (reconnect && state.binding(root)) {
|
|
@@ -1179,8 +1415,8 @@ function completePreparation(state, root) {
|
|
|
1179
1415
|
}
|
|
1180
1416
|
|
|
1181
1417
|
// apps/client/runtime/log.ts
|
|
1182
|
-
import { appendFileSync, mkdirSync as
|
|
1183
|
-
import { join as
|
|
1418
|
+
import { appendFileSync, mkdirSync as mkdirSync4, renameSync as renameSync2, statSync, unlinkSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1419
|
+
import { join as join6 } from "node:path";
|
|
1184
1420
|
import { createHash as createHash3 } from "node:crypto";
|
|
1185
1421
|
function rootKey(root) {
|
|
1186
1422
|
return createHash3("sha256").update(root).digest("hex").slice(0, 16);
|
|
@@ -1196,9 +1432,9 @@ function diagnostic(error) {
|
|
|
1196
1432
|
var ClientLog = class {
|
|
1197
1433
|
constructor(directory, limit = 1024 * 1024) {
|
|
1198
1434
|
this.limit = limit;
|
|
1199
|
-
const logs =
|
|
1200
|
-
|
|
1201
|
-
this.path =
|
|
1435
|
+
const logs = join6(directory, "logs");
|
|
1436
|
+
mkdirSync4(logs, { recursive: true, mode: 448 });
|
|
1437
|
+
this.path = join6(logs, "client.jsonl");
|
|
1202
1438
|
}
|
|
1203
1439
|
limit;
|
|
1204
1440
|
path;
|
|
@@ -1215,10 +1451,10 @@ var ClientLog = class {
|
|
|
1215
1451
|
} catch {
|
|
1216
1452
|
}
|
|
1217
1453
|
try {
|
|
1218
|
-
|
|
1454
|
+
renameSync2(this.path + ".1", this.path + ".2");
|
|
1219
1455
|
} catch {
|
|
1220
1456
|
}
|
|
1221
|
-
|
|
1457
|
+
renameSync2(this.path, this.path + ".1");
|
|
1222
1458
|
}
|
|
1223
1459
|
appendFileSync(
|
|
1224
1460
|
this.path,
|
|
@@ -1237,7 +1473,7 @@ var ClientLog = class {
|
|
|
1237
1473
|
const lines = [];
|
|
1238
1474
|
for (const suffix of [".2", ".1", ""])
|
|
1239
1475
|
try {
|
|
1240
|
-
for (const line of
|
|
1476
|
+
for (const line of readFileSync2(this.path + suffix, "utf8").split("\n")) {
|
|
1241
1477
|
if (!line) continue;
|
|
1242
1478
|
try {
|
|
1243
1479
|
const item = JSON.parse(line);
|
|
@@ -1255,50 +1491,50 @@ var ClientLog = class {
|
|
|
1255
1491
|
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
1256
1492
|
|
|
1257
1493
|
// packages/protocol/devices.ts
|
|
1258
|
-
import { z as
|
|
1259
|
-
var CredentialSchema =
|
|
1260
|
-
var EnrollmentSchema =
|
|
1261
|
-
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(),
|
|
1262
1498
|
projectId: NameSchema,
|
|
1263
1499
|
memberId: NameSchema,
|
|
1264
1500
|
deviceId: DeviceIdSchema,
|
|
1265
1501
|
credential: CredentialSchema
|
|
1266
1502
|
}).strict();
|
|
1267
|
-
var EffectivePolicySchema =
|
|
1268
|
-
checkIntervalSeconds:
|
|
1269
|
-
fileSettleSeconds:
|
|
1503
|
+
var EffectivePolicySchema = z11.object({
|
|
1504
|
+
checkIntervalSeconds: z11.number().min(1).max(86400),
|
|
1505
|
+
fileSettleSeconds: z11.number().min(0).max(86400)
|
|
1270
1506
|
}).strict();
|
|
1271
|
-
var SyncSummarySchema =
|
|
1272
|
-
checkedAt:
|
|
1273
|
-
published:
|
|
1274
|
-
appliedFiles:
|
|
1275
|
-
pendingFiles:
|
|
1276
|
-
sessionSources:
|
|
1277
|
-
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)
|
|
1278
1514
|
}).strict();
|
|
1279
|
-
var DeviceStatusSchema =
|
|
1280
|
-
clientVersion:
|
|
1515
|
+
var DeviceStatusSchema = z11.object({
|
|
1516
|
+
clientVersion: z11.string().max(64),
|
|
1281
1517
|
receivedRevision: RevisionSchema.nullable(),
|
|
1282
1518
|
appliedRevision: RevisionSchema.nullable(),
|
|
1283
1519
|
effectivePolicy: EffectivePolicySchema,
|
|
1284
|
-
overrides:
|
|
1285
|
-
configError:
|
|
1286
|
-
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(),
|
|
1287
1523
|
lastSync: SyncSummarySchema.nullable()
|
|
1288
1524
|
}).strict();
|
|
1289
|
-
var WebDevicesSchema =
|
|
1290
|
-
devices:
|
|
1291
|
-
|
|
1525
|
+
var WebDevicesSchema = z11.object({
|
|
1526
|
+
devices: z11.array(
|
|
1527
|
+
z11.object({
|
|
1292
1528
|
memberId: NameSchema,
|
|
1293
|
-
bound:
|
|
1294
|
-
lastSeenAt:
|
|
1529
|
+
bound: z11.boolean(),
|
|
1530
|
+
lastSeenAt: z11.string().nullable(),
|
|
1295
1531
|
status: DeviceStatusSchema.nullable()
|
|
1296
1532
|
})
|
|
1297
1533
|
)
|
|
1298
1534
|
});
|
|
1299
1535
|
|
|
1300
1536
|
// apps/client/project-state.ts
|
|
1301
|
-
import { randomBytes, randomUUID as
|
|
1537
|
+
import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
1302
1538
|
var projectClientMigration = String.raw`
|
|
1303
1539
|
CREATE TABLE client_enrollments(root TEXT PRIMARY KEY REFERENCES connections(root),server_url TEXT NOT NULL,request_json TEXT NOT NULL);
|
|
1304
1540
|
CREATE TABLE client_project_config(root TEXT PRIMARY KEY REFERENCES connections(root),config_json TEXT NOT NULL,descriptor_hash TEXT NOT NULL);
|
|
@@ -1322,7 +1558,7 @@ var ClientProjectState = class {
|
|
|
1322
1558
|
return request2;
|
|
1323
1559
|
}
|
|
1324
1560
|
const request = {
|
|
1325
|
-
operationId:
|
|
1561
|
+
operationId: randomUUID3(),
|
|
1326
1562
|
memberId,
|
|
1327
1563
|
projectId,
|
|
1328
1564
|
deviceId,
|
|
@@ -1495,6 +1731,20 @@ function readControlStatus(state, root) {
|
|
|
1495
1731
|
return row ? DeviceStatusSchema.parse(JSON.parse(String(row.status_json))) : null;
|
|
1496
1732
|
}
|
|
1497
1733
|
|
|
1734
|
+
// apps/client/sessions/versions.ts
|
|
1735
|
+
var parserVersions = {
|
|
1736
|
+
"claude-code": 1,
|
|
1737
|
+
codex: 1,
|
|
1738
|
+
cursor: 1,
|
|
1739
|
+
pi: 2,
|
|
1740
|
+
dsh: 1
|
|
1741
|
+
};
|
|
1742
|
+
var sessionParserMigration = `
|
|
1743
|
+
ALTER TABLE session_sources ADD COLUMN parser_version INTEGER NOT NULL DEFAULT 0;
|
|
1744
|
+
ALTER TABLE session_publications ADD COLUMN pending_parser_version INTEGER NOT NULL DEFAULT 0;
|
|
1745
|
+
ALTER TABLE session_discovery_cache ADD COLUMN parser_version INTEGER NOT NULL DEFAULT 0;
|
|
1746
|
+
`;
|
|
1747
|
+
|
|
1498
1748
|
// apps/client/sessions/state.ts
|
|
1499
1749
|
var sessionClientMigration = String.raw`
|
|
1500
1750
|
CREATE TABLE session_sources (
|
|
@@ -1554,12 +1804,12 @@ var SessionState = class {
|
|
|
1554
1804
|
get(path) {
|
|
1555
1805
|
return this.sources(path)[0];
|
|
1556
1806
|
}
|
|
1557
|
-
sources(path, limit = 2147483647) {
|
|
1807
|
+
sources(path, limit = 2147483647, before = Number.MAX_SAFE_INTEGER) {
|
|
1558
1808
|
return this.state.db.prepare(
|
|
1559
|
-
`SELECT s.*, p.revision,p.hash,p.selected_path,p.pending_json,p.pending_signature,p.pending_path,p.attempts,p.retry_at
|
|
1809
|
+
`SELECT s.*, p.revision,p.hash,p.selected_path,p.pending_json,p.pending_signature,p.pending_path,p.pending_parser_version,p.attempts,p.retry_at
|
|
1560
1810
|
FROM session_sources s LEFT JOIN session_publications p USING(root,session_key)
|
|
1561
|
-
WHERE s.root=? ${path === void 0 ? "" : "AND s.path=?"} ORDER BY s.checked_at, s.path LIMIT ?`
|
|
1562
|
-
).all(this.root, ...path === void 0 ? [] : [path], limit).map((row) => ({
|
|
1811
|
+
WHERE s.root=? AND s.checked_at<? ${path === void 0 ? "" : "AND s.path=?"} ORDER BY s.checked_at, s.path LIMIT ?`
|
|
1812
|
+
).all(this.root, before, ...path === void 0 ? [] : [path], limit).map((row) => ({
|
|
1563
1813
|
path: String(row.path),
|
|
1564
1814
|
agent: row.agent,
|
|
1565
1815
|
projectDir: String(row.project_dir),
|
|
@@ -1569,6 +1819,8 @@ var SessionState = class {
|
|
|
1569
1819
|
signature: row.signature,
|
|
1570
1820
|
checkedAt: Number(row.checked_at),
|
|
1571
1821
|
verifiedAt: Number(row.verified_at),
|
|
1822
|
+
parserVersion: Number(row.parser_version),
|
|
1823
|
+
pendingParserVersion: Number(row.pending_parser_version ?? 0),
|
|
1572
1824
|
lastError: row.last_error,
|
|
1573
1825
|
adoptExisting: Boolean(row.adopt_existing),
|
|
1574
1826
|
pending: row.pending_json ? SessionCommitSchema.parse(JSON.parse(String(row.pending_json))) : null,
|
|
@@ -1607,21 +1859,28 @@ var SessionState = class {
|
|
|
1607
1859
|
}
|
|
1608
1860
|
stage(path, request, signature2) {
|
|
1609
1861
|
this.state.db.prepare(
|
|
1610
|
-
`UPDATE session_publications SET pending_json=?,pending_signature=?,pending_path=?,attempts=0,retry_at=0
|
|
1862
|
+
`UPDATE session_publications SET pending_json=?,pending_signature=?,pending_path=?,pending_parser_version=?,attempts=0,retry_at=0
|
|
1611
1863
|
WHERE root=? AND session_key=? AND pending_json IS NULL`
|
|
1612
|
-
).run(
|
|
1864
|
+
).run(
|
|
1865
|
+
JSON.stringify(request),
|
|
1866
|
+
signature2,
|
|
1867
|
+
path,
|
|
1868
|
+
parserVersions[this.get(path).agent],
|
|
1869
|
+
this.root,
|
|
1870
|
+
request.sessionKey
|
|
1871
|
+
);
|
|
1613
1872
|
}
|
|
1614
|
-
acknowledge(path, revision, hash2, signature2) {
|
|
1873
|
+
acknowledge(path, revision, hash2, signature2, parserVersion) {
|
|
1615
1874
|
const source = this.get(path);
|
|
1616
1875
|
this.state.db.prepare(
|
|
1617
1876
|
`UPDATE session_publications SET revision=?,hash=?,selected_path=?,pending_json=NULL,pending_signature=NULL,pending_path=NULL,attempts=0,retry_at=0 WHERE root=? AND session_key=?`
|
|
1618
1877
|
).run(revision, hash2, path, this.root, source.key);
|
|
1619
|
-
this.observed(path, signature2);
|
|
1878
|
+
this.observed(path, signature2, parserVersion);
|
|
1620
1879
|
}
|
|
1621
|
-
observed(path, signature2) {
|
|
1880
|
+
observed(path, signature2, parserVersion = parserVersions[this.get(path).agent]) {
|
|
1622
1881
|
this.state.db.prepare(
|
|
1623
|
-
"UPDATE session_sources SET signature=?,verified_at=?,last_error=NULL,adopt_existing=0 WHERE root=? AND path=?"
|
|
1624
|
-
).run(signature2, Date.now(), this.root, path);
|
|
1882
|
+
"UPDATE session_sources SET signature=?,verified_at=?,parser_version=?,last_error=NULL,adopt_existing=0 WHERE root=? AND path=?"
|
|
1883
|
+
).run(signature2, Date.now(), parserVersion, this.root, path);
|
|
1625
1884
|
}
|
|
1626
1885
|
baseline(path, key, revision) {
|
|
1627
1886
|
this.state.db.prepare(
|
|
@@ -1643,10 +1902,12 @@ var SessionState = class {
|
|
|
1643
1902
|
this.checked(source.path, error);
|
|
1644
1903
|
}
|
|
1645
1904
|
discover(agent, directory, projectDir) {
|
|
1905
|
+
const prior = this.directories().find((item) => item.directory === directory);
|
|
1906
|
+
if (prior?.agent === agent && prior.projectDir === projectDir) return;
|
|
1646
1907
|
this.state.db.prepare(
|
|
1647
1908
|
"INSERT INTO session_discovery VALUES (?,?,?,?) ON CONFLICT(root,directory) DO UPDATE SET agent=excluded.agent,project_dir=excluded.project_dir"
|
|
1648
1909
|
).run(this.root, directory, agent, projectDir);
|
|
1649
|
-
this.state.db.prepare("DELETE FROM session_discovery_cache WHERE root=?").run(this.root);
|
|
1910
|
+
this.state.db.prepare("DELETE FROM session_discovery_cache WHERE root=? AND substr(path,1,?)=?").run(this.root, directory.length + 1, directory + "/");
|
|
1650
1911
|
}
|
|
1651
1912
|
receivedHook(agent) {
|
|
1652
1913
|
this.state.db.prepare(
|
|
@@ -1667,33 +1928,75 @@ var SessionState = class {
|
|
|
1667
1928
|
|
|
1668
1929
|
// apps/client/state.ts
|
|
1669
1930
|
var LocalBindingSchema = BindingSchema.extend({
|
|
1670
|
-
root:
|
|
1671
|
-
serverUrl:
|
|
1672
|
-
token:
|
|
1931
|
+
root: z12.string(),
|
|
1932
|
+
serverUrl: z12.string(),
|
|
1933
|
+
token: z12.string()
|
|
1673
1934
|
});
|
|
1674
|
-
var
|
|
1675
|
-
|
|
1676
|
-
const db = openDatabase(join4(directory, "client.sqlite3"), [
|
|
1677
|
-
String.raw`
|
|
1935
|
+
var clientMigrations = [
|
|
1936
|
+
String.raw`
|
|
1678
1937
|
CREATE TABLE connections (root TEXT PRIMARY KEY, device_id TEXT NOT NULL, binding_json TEXT);
|
|
1679
1938
|
`,
|
|
1680
|
-
|
|
1939
|
+
String.raw`
|
|
1681
1940
|
CREATE TABLE publications (root TEXT PRIMARY KEY REFERENCES connections(root), revision INTEGER NOT NULL DEFAULT 0, initialized INTEGER NOT NULL DEFAULT 0);
|
|
1682
1941
|
CREATE TABLE owner_files (root TEXT NOT NULL REFERENCES connections(root), path TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL, PRIMARY KEY(root, path));
|
|
1683
1942
|
CREATE TABLE outbox (root TEXT PRIMARY KEY REFERENCES connections(root), request_json TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, retry_at INTEGER NOT NULL DEFAULT 0, last_error TEXT);
|
|
1684
1943
|
`,
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1944
|
+
syncMigration,
|
|
1945
|
+
sessionClientMigration,
|
|
1946
|
+
projectClientMigration,
|
|
1947
|
+
controlClientMigration,
|
|
1948
|
+
runtimeMigration,
|
|
1949
|
+
projectFilesClientMigration,
|
|
1950
|
+
sessionSourcesMigration,
|
|
1951
|
+
atomicFilesClientMigration,
|
|
1952
|
+
sessionParserMigration
|
|
1953
|
+
];
|
|
1954
|
+
var clientSchemaVersion = clientMigrations.length;
|
|
1955
|
+
var ClientState = class extends FilePublicationState {
|
|
1956
|
+
access;
|
|
1957
|
+
constructor(directory, options = {}) {
|
|
1958
|
+
let access2;
|
|
1959
|
+
let db;
|
|
1960
|
+
try {
|
|
1961
|
+
if (!options.maintenance) access2 = stateAccess(directory);
|
|
1962
|
+
const version = stateVersion(directory);
|
|
1963
|
+
if (version > clientSchemaVersion)
|
|
1964
|
+
throw new Error(
|
|
1965
|
+
"Database was created by a newer Teammy version. Local stop and logs remain available."
|
|
1966
|
+
);
|
|
1967
|
+
if (version > 0 && version < clientSchemaVersion)
|
|
1968
|
+
throw new Error("Client state needs an upgrade. Run teammy repair.");
|
|
1969
|
+
if (options.readOnly) {
|
|
1970
|
+
if (!version) throw new Error("No client state is available.");
|
|
1971
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"), { readOnly: true });
|
|
1972
|
+
} else if (!version) {
|
|
1973
|
+
access2?.close();
|
|
1974
|
+
access2 = void 0;
|
|
1975
|
+
const exclusive = options.maintenance ? void 0 : stateAccess(directory, true);
|
|
1976
|
+
try {
|
|
1977
|
+
db = openDatabase(join7(directory, "client.sqlite3"), clientMigrations);
|
|
1978
|
+
} finally {
|
|
1979
|
+
exclusive?.close();
|
|
1980
|
+
}
|
|
1981
|
+
if (!options.maintenance) access2 = stateAccess(directory);
|
|
1982
|
+
} else {
|
|
1983
|
+
db = new DatabaseSync3(join7(directory, "client.sqlite3"));
|
|
1984
|
+
db.exec("PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;");
|
|
1985
|
+
}
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
db?.close();
|
|
1988
|
+
access2?.close();
|
|
1989
|
+
throw error;
|
|
1990
|
+
}
|
|
1693
1991
|
super(db);
|
|
1992
|
+
this.access = access2;
|
|
1694
1993
|
}
|
|
1695
1994
|
close() {
|
|
1696
|
-
|
|
1995
|
+
try {
|
|
1996
|
+
this.db.close();
|
|
1997
|
+
} finally {
|
|
1998
|
+
this.access?.close();
|
|
1999
|
+
}
|
|
1697
2000
|
}
|
|
1698
2001
|
assertRootAvailable(root) {
|
|
1699
2002
|
for (const row of this.db.prepare("SELECT root FROM connections").all()) {
|
|
@@ -1709,7 +2012,7 @@ var ClientState = class extends FilePublicationState {
|
|
|
1709
2012
|
}
|
|
1710
2013
|
deviceFor(root) {
|
|
1711
2014
|
this.assertRootAvailable(root);
|
|
1712
|
-
this.db.prepare("INSERT OR IGNORE INTO connections (root, device_id) VALUES (?, ?)").run(root,
|
|
2015
|
+
this.db.prepare("INSERT OR IGNORE INTO connections (root, device_id) VALUES (?, ?)").run(root, randomUUID4());
|
|
1713
2016
|
return String(
|
|
1714
2017
|
this.db.prepare("SELECT device_id FROM connections WHERE root=?").get(root).device_id
|
|
1715
2018
|
);
|
|
@@ -1754,11 +2057,11 @@ async function stateLocation(root, directory) {
|
|
|
1754
2057
|
// apps/client/sessions/setup.ts
|
|
1755
2058
|
import { execFile as execFile2 } from "node:child_process";
|
|
1756
2059
|
import { promisify as promisify2 } from "node:util";
|
|
1757
|
-
import { access, mkdir as
|
|
1758
|
-
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";
|
|
1759
2062
|
import { homedir } from "node:os";
|
|
1760
|
-
import { dirname as
|
|
1761
|
-
import { createHash as
|
|
2063
|
+
import { dirname as dirname6, join as join11, resolve as resolve4 } from "node:path";
|
|
2064
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
1762
2065
|
|
|
1763
2066
|
// apps/client/sessions/normalize.ts
|
|
1764
2067
|
function object(value) {
|
|
@@ -1792,6 +2095,9 @@ var JsonlNormalizer = class {
|
|
|
1792
2095
|
nativeId;
|
|
1793
2096
|
cwd;
|
|
1794
2097
|
turn = "legacy";
|
|
2098
|
+
piParents = /* @__PURE__ */ new Map();
|
|
2099
|
+
piLeaf = null;
|
|
2100
|
+
piTree = false;
|
|
1795
2101
|
candidates = [];
|
|
1796
2102
|
add(role, content, id, line, visible = false) {
|
|
1797
2103
|
if (role !== "user" && role !== "assistant") return;
|
|
@@ -1839,8 +2145,11 @@ var JsonlNormalizer = class {
|
|
|
1839
2145
|
this.add(payload.role, payload.content, payload.id, line);
|
|
1840
2146
|
}
|
|
1841
2147
|
} else if (this.agent === "cursor") {
|
|
1842
|
-
const message2 = object(row.message);
|
|
1843
2148
|
if (row.isSidechain === true) return;
|
|
2149
|
+
const sessionId = row.sessionId ?? row.session_id;
|
|
2150
|
+
if (typeof sessionId === "string" && this.nativeId && sessionId !== this.nativeId)
|
|
2151
|
+
throw new Error("Mixed Cursor session identities.");
|
|
2152
|
+
const message2 = object(row.message);
|
|
1844
2153
|
this.add(row.role ?? message2.role, message2.content ?? row.content, row.id ?? row.uuid, line);
|
|
1845
2154
|
} else if (this.agent === "pi") {
|
|
1846
2155
|
if (row.type === "session") {
|
|
@@ -1849,6 +2158,14 @@ var JsonlNormalizer = class {
|
|
|
1849
2158
|
this.nativeId = row.id;
|
|
1850
2159
|
this.cwd = row.cwd;
|
|
1851
2160
|
}
|
|
2161
|
+
if (row.type !== "session" && typeof row.id === "string") {
|
|
2162
|
+
this.piTree ||= Object.hasOwn(row, "parentId");
|
|
2163
|
+
this.piParents.set(
|
|
2164
|
+
row.id,
|
|
2165
|
+
typeof row.parentId === "string" ? row.parentId : row.parentId === null ? null : this.piLeaf
|
|
2166
|
+
);
|
|
2167
|
+
this.piLeaf = row.id;
|
|
2168
|
+
}
|
|
1852
2169
|
if (row.type === "message") {
|
|
1853
2170
|
const message2 = object(row.message);
|
|
1854
2171
|
this.add(message2.role, message2.content, row.id, line);
|
|
@@ -1876,9 +2193,25 @@ var JsonlNormalizer = class {
|
|
|
1876
2193
|
}
|
|
1877
2194
|
}
|
|
1878
2195
|
finish() {
|
|
2196
|
+
let piBranch;
|
|
2197
|
+
if (this.agent === "pi" && this.piTree) {
|
|
2198
|
+
piBranch = /* @__PURE__ */ new Set();
|
|
2199
|
+
let id = this.piLeaf;
|
|
2200
|
+
while (id && !piBranch.has(id)) {
|
|
2201
|
+
piBranch.add(id);
|
|
2202
|
+
id = this.piParents.get(id) ?? null;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
1879
2205
|
const visibleGroups = new Set(this.candidates.filter((m) => m.visible).map((m) => m.group));
|
|
1880
2206
|
const ids = /* @__PURE__ */ new Map();
|
|
1881
|
-
const
|
|
2207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2208
|
+
const messages = this.candidates.filter((m) => !piBranch || typeof m.id === "string" && piBranch.has(m.id)).filter((m) => m.visible || !visibleGroups.has(m.group)).filter((m) => {
|
|
2209
|
+
if (typeof m.id !== "string" || !m.id) return true;
|
|
2210
|
+
const identity = JSON.stringify([m.id, m.role, m.text]);
|
|
2211
|
+
if (seen.has(identity)) return false;
|
|
2212
|
+
seen.add(identity);
|
|
2213
|
+
return true;
|
|
2214
|
+
}).map((m) => {
|
|
1882
2215
|
const base = typeof m.id === "string" && m.id ? m.id : `line:${m.line}`;
|
|
1883
2216
|
const count = (ids.get(base) ?? 0) + 1;
|
|
1884
2217
|
ids.set(base, count);
|
|
@@ -1895,10 +2228,167 @@ var JsonlNormalizer = class {
|
|
|
1895
2228
|
}
|
|
1896
2229
|
};
|
|
1897
2230
|
|
|
2231
|
+
// apps/client/sessions/registration.ts
|
|
2232
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2233
|
+
import { join as join8 } from "node:path";
|
|
2234
|
+
var registrationPath = (directory, root) => join8(directory, "hook-registrations", rootKey(root) + ".json");
|
|
2235
|
+
function readRegistration(directory, root) {
|
|
2236
|
+
const path = registrationPath(directory, root);
|
|
2237
|
+
if (!existsSync3(path)) return null;
|
|
2238
|
+
const value = JSON.parse(readFileSync3(path, "utf8"));
|
|
2239
|
+
if (value.format !== 1 || value.root !== root || !Array.isArray(value.configs))
|
|
2240
|
+
throw new Error("Invalid Hook registration.");
|
|
2241
|
+
return value;
|
|
2242
|
+
}
|
|
2243
|
+
function saveRegistration(directory, value) {
|
|
2244
|
+
writePrivateJson(registrationPath(directory, value.root), value);
|
|
2245
|
+
}
|
|
2246
|
+
function registrations(directory, onError) {
|
|
2247
|
+
const path = join8(directory, "hook-registrations");
|
|
2248
|
+
if (!existsSync3(path)) return [];
|
|
2249
|
+
return readdirSync2(path).filter((name2) => /^[a-f0-9]{16}\.json$/.test(name2)).flatMap((name2) => {
|
|
2250
|
+
try {
|
|
2251
|
+
const value = JSON.parse(readFileSync3(join8(path, name2), "utf8"));
|
|
2252
|
+
const registration = readRegistration(directory, value.root);
|
|
2253
|
+
if (!registration) throw new Error("Missing Hook registration.");
|
|
2254
|
+
return [registration];
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
onError(join8(path, name2), error);
|
|
2257
|
+
return [];
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// apps/client/root-lock.ts
|
|
2263
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2264
|
+
import { join as join9 } from "node:path";
|
|
2265
|
+
function acquireRootLock(root, stateDirectory) {
|
|
2266
|
+
let lock;
|
|
2267
|
+
try {
|
|
2268
|
+
lock = openDatabase(
|
|
2269
|
+
join9(stateDirectory, "locks", `${createHash5("sha256").update(root).digest("hex")}.sqlite3`),
|
|
2270
|
+
[],
|
|
2271
|
+
0
|
|
2272
|
+
);
|
|
2273
|
+
lock.exec("PRAGMA busy_timeout=0; BEGIN IMMEDIATE");
|
|
2274
|
+
return lock;
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
lock?.close();
|
|
2277
|
+
if (error instanceof Error && /locked|busy/.test(error.message))
|
|
2278
|
+
throw new Error("Another sync or connect process is using this root.");
|
|
2279
|
+
throw error;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// apps/client/sessions/setup.ts
|
|
2284
|
+
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
2285
|
+
|
|
2286
|
+
// apps/client/sessions/pi-extension.ts
|
|
2287
|
+
import { mkdir as mkdir4, readFile } from "node:fs/promises";
|
|
2288
|
+
import { dirname as dirname5, basename as basename2, join as join10, resolve as resolve3 } from "node:path";
|
|
2289
|
+
var marker = (directory) => `// Teammy Pi collection ${rootKey(directory)}`;
|
|
2290
|
+
function piPath(path, home, cwd) {
|
|
2291
|
+
return resolve3(
|
|
2292
|
+
cwd,
|
|
2293
|
+
path === "~" ? home : path.startsWith("~/") ? join10(home, path.slice(2)) : path
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
async function piDiscoveryDirectories(agentDirectory, project, home, env) {
|
|
2297
|
+
let configured;
|
|
2298
|
+
for (const path of [join10(agentDirectory, "settings.json"), join10(project, ".pi/settings.json")]) {
|
|
2299
|
+
try {
|
|
2300
|
+
const settings = JSON.parse(await readFile(path, "utf8"));
|
|
2301
|
+
if (typeof settings?.sessionDir === "string" && settings.sessionDir)
|
|
2302
|
+
configured = settings.sessionDir;
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
if (!missing2(error)) throw new Error(`Unable to read Pi session directory setting: ${path}`);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
configured = env.PI_CODING_AGENT_SESSION_DIR || configured;
|
|
2308
|
+
return [
|
|
2309
|
+
.../* @__PURE__ */ new Set([
|
|
2310
|
+
join10(agentDirectory, "sessions"),
|
|
2311
|
+
...configured ? [piPath(configured, home, project)] : []
|
|
2312
|
+
])
|
|
2313
|
+
];
|
|
2314
|
+
}
|
|
2315
|
+
function piExtensionSource(directory, path, entry) {
|
|
2316
|
+
return `${marker(directory)}
|
|
2317
|
+
import { spawn } from 'node:child_process';
|
|
2318
|
+
|
|
2319
|
+
export default function teammyCollection(pi) {
|
|
2320
|
+
const collect = async (_event, ctx) => {
|
|
2321
|
+
try {
|
|
2322
|
+
const manager = ctx.sessionManager;
|
|
2323
|
+
const file = manager.getSessionFile();
|
|
2324
|
+
if (!file || manager.isPersisted?.() === false) return;
|
|
2325
|
+
const payload = JSON.stringify({
|
|
2326
|
+
cwd: manager.getCwd?.() ?? ctx.cwd,
|
|
2327
|
+
session_id: manager.getSessionId(),
|
|
2328
|
+
transcript_path: file,
|
|
2329
|
+
});
|
|
2330
|
+
const child = spawn(${JSON.stringify(process.execPath)}, [
|
|
2331
|
+
${JSON.stringify(join10(dirname5(entry), "session-hook.js"))},
|
|
2332
|
+
'--agent', 'pi', '--state-dir', ${JSON.stringify(directory)},
|
|
2333
|
+
'--integration-file', ${JSON.stringify(path)},
|
|
2334
|
+
], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true, shell: false });
|
|
2335
|
+
await new Promise((resolve) => {
|
|
2336
|
+
const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} finish(); }, 2000);
|
|
2337
|
+
const finish = () => { clearTimeout(timer); resolve(); };
|
|
2338
|
+
child.once('error', finish);
|
|
2339
|
+
child.once('close', finish);
|
|
2340
|
+
child.stdin.on('error', () => {});
|
|
2341
|
+
child.stdin.end(payload);
|
|
2342
|
+
});
|
|
2343
|
+
} catch { /* Collection never affects the host session. */ }
|
|
2344
|
+
};
|
|
2345
|
+
// agent_end is supported by older Pi releases too. It follows message
|
|
2346
|
+
// persistence; retries or queued turns simply produce another hint later.
|
|
2347
|
+
for (const event of ['session_start', 'agent_end', 'session_shutdown']) {
|
|
2348
|
+
try { pi.on(event, collect); } catch {}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
`;
|
|
2352
|
+
}
|
|
2353
|
+
async function installPiExtension(directory, registry, agentDirectory, entry) {
|
|
2354
|
+
const path = join10(agentDirectory, "extensions", `teammy-${rootKey(directory)}.js`);
|
|
2355
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2356
|
+
let original = null;
|
|
2357
|
+
try {
|
|
2358
|
+
original = await readStable(dirname5(path), basename2(path));
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
if (!missing2(error)) throw error;
|
|
2361
|
+
}
|
|
2362
|
+
if (original && !original.bytes.toString("utf8").startsWith(marker(directory) + "\n"))
|
|
2363
|
+
throw new Error(`Pi extension is not owned by Teammy: ${path}`);
|
|
2364
|
+
if (!registry.configs.some((config) => config.path === path && config.kind === "pi-extension"))
|
|
2365
|
+
registry.configs.push({ path, agent: "pi", kind: "pi-extension", created: !original });
|
|
2366
|
+
saveRegistration(directory, registry);
|
|
2367
|
+
const bytes = Buffer.from(piExtensionSource(directory, path, entry));
|
|
2368
|
+
if (!original?.bytes.equals(bytes))
|
|
2369
|
+
await replaceFile(dirname5(path), basename2(path), bytes, original?.signature ?? null);
|
|
2370
|
+
}
|
|
2371
|
+
async function removePiExtension(directory, path) {
|
|
2372
|
+
const issues = [];
|
|
2373
|
+
const remaining = registrations(directory, (file) => issues.push(file));
|
|
2374
|
+
if (issues.length || remaining.some((r) => r.enabled && r.configs.some((c) => c.path === path)))
|
|
2375
|
+
return false;
|
|
2376
|
+
try {
|
|
2377
|
+
const original = await readStable(dirname5(path), basename2(path));
|
|
2378
|
+
if (!original.bytes.toString("utf8").startsWith(marker(directory) + "\n"))
|
|
2379
|
+
throw new Error(`Pi extension is not owned by Teammy: ${path}`);
|
|
2380
|
+
await removeFile(dirname5(path), basename2(path), original.signature);
|
|
2381
|
+
return true;
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
if (missing2(error)) return false;
|
|
2384
|
+
throw error;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
1898
2388
|
// apps/client/sessions/setup.ts
|
|
1899
2389
|
var exec2 = promisify2(execFile2);
|
|
1900
2390
|
var quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
1901
|
-
async function exists(path, mode =
|
|
2391
|
+
async function exists(path, mode = constants3.F_OK) {
|
|
1902
2392
|
try {
|
|
1903
2393
|
await access(path, mode);
|
|
1904
2394
|
return true;
|
|
@@ -1907,176 +2397,399 @@ async function exists(path, mode = constants2.F_OK) {
|
|
|
1907
2397
|
}
|
|
1908
2398
|
}
|
|
1909
2399
|
async function setupSessions(state, root, directory, options = {}) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
const
|
|
1913
|
-
{
|
|
1914
|
-
agent: "claude-code",
|
|
1915
|
-
executables: ["claude"],
|
|
1916
|
-
storage: env.CLAUDE_CONFIG_DIR ?? join5(home, ".claude"),
|
|
1917
|
-
directories: ["projects"],
|
|
1918
|
-
config: ".claude/settings.local.json",
|
|
1919
|
-
events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
|
|
1920
|
-
},
|
|
1921
|
-
{
|
|
1922
|
-
agent: "codex",
|
|
1923
|
-
executables: ["codex"],
|
|
1924
|
-
storage: env.CODEX_HOME ?? join5(home, ".codex"),
|
|
1925
|
-
directories: ["sessions"],
|
|
1926
|
-
config: ".codex/hooks.json",
|
|
1927
|
-
events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
|
|
1928
|
-
},
|
|
1929
|
-
{
|
|
1930
|
-
agent: "cursor",
|
|
1931
|
-
executables: ["agent", "cursor-agent"],
|
|
1932
|
-
storage: env.CURSOR_CONFIG_DIR ?? join5(home, ".cursor"),
|
|
1933
|
-
directories: ["chats", "projects"],
|
|
1934
|
-
config: ".cursor/hooks.json",
|
|
1935
|
-
events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
|
|
1936
|
-
},
|
|
1937
|
-
{
|
|
1938
|
-
agent: "pi",
|
|
1939
|
-
executables: ["pi"],
|
|
1940
|
-
storage: env.PI_CODING_AGENT_DIR ?? join5(home, ".pi", "agent"),
|
|
1941
|
-
directories: ["sessions"]
|
|
1942
|
-
}
|
|
1943
|
-
];
|
|
1944
|
-
const issues = [], configured = [];
|
|
1945
|
-
let gitRoot = project;
|
|
2400
|
+
await realpath4(root);
|
|
2401
|
+
if (!state.binding(root)) throw new Error("Connect this root before installing hooks.");
|
|
2402
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
1946
2403
|
try {
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
"
|
|
1984
|
-
"
|
|
1985
|
-
"
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
"
|
|
1990
|
-
|
|
1991
|
-
"
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
adapter.agent === "cursor" ? { command, timeout: 3 } : { hooks: [{ type: "command", command, timeout: 3 }] }
|
|
2010
|
-
];
|
|
2404
|
+
const prior = readRegistration(directory, root);
|
|
2405
|
+
if (prior && !prior.enabled && !options.enable)
|
|
2406
|
+
return { configured: [], issues: [] };
|
|
2407
|
+
const registry = prior ?? {
|
|
2408
|
+
format: 1,
|
|
2409
|
+
root,
|
|
2410
|
+
projectDir: dirname6(root),
|
|
2411
|
+
enabled: true,
|
|
2412
|
+
configs: []
|
|
2413
|
+
};
|
|
2414
|
+
registry.enabled = true;
|
|
2415
|
+
saveRegistration(directory, registry);
|
|
2416
|
+
const home = options.home ?? homedir(), env = options.env ?? process.env;
|
|
2417
|
+
const project = dirname6(root), sessions = new SessionState(state, root);
|
|
2418
|
+
const defaults = [
|
|
2419
|
+
{
|
|
2420
|
+
agent: "claude-code",
|
|
2421
|
+
executables: ["claude"],
|
|
2422
|
+
storage: env.CLAUDE_CONFIG_DIR ?? join11(home, ".claude"),
|
|
2423
|
+
directories: ["projects"],
|
|
2424
|
+
config: ".claude/settings.local.json",
|
|
2425
|
+
events: ["Stop", "SessionEnd"]
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
agent: "codex",
|
|
2429
|
+
executables: ["codex"],
|
|
2430
|
+
storage: env.CODEX_HOME ?? join11(home, ".codex"),
|
|
2431
|
+
directories: ["sessions"],
|
|
2432
|
+
config: ".codex/hooks.json",
|
|
2433
|
+
events: ["Stop", "SessionEnd"]
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
agent: "claude-code",
|
|
2437
|
+
instance: "tclaude",
|
|
2438
|
+
executables: ["tclaude"],
|
|
2439
|
+
storage: join11(home, ".tclaude"),
|
|
2440
|
+
directories: ["projects"],
|
|
2441
|
+
config: ".claude/settings.local.json",
|
|
2442
|
+
events: ["Stop", "SessionEnd"]
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
agent: "codex",
|
|
2446
|
+
instance: "tcodex",
|
|
2447
|
+
executables: ["tcodex"],
|
|
2448
|
+
storage: join11(home, ".tcodex"),
|
|
2449
|
+
directories: ["sessions"],
|
|
2450
|
+
config: ".codex/hooks.json",
|
|
2451
|
+
events: ["Stop", "SessionEnd"]
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
agent: "cursor",
|
|
2455
|
+
executables: ["agent", "cursor-agent"],
|
|
2456
|
+
storage: env.CURSOR_CONFIG_DIR ?? join11(home, ".cursor"),
|
|
2457
|
+
directories: ["chats", "projects"],
|
|
2458
|
+
config: ".cursor/hooks.json",
|
|
2459
|
+
events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
agent: "pi",
|
|
2463
|
+
executables: ["pi"],
|
|
2464
|
+
storage: piPath(env.PI_CODING_AGENT_DIR ?? join11(home, ".pi", "agent"), home, project),
|
|
2465
|
+
directories: []
|
|
2011
2466
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2467
|
+
];
|
|
2468
|
+
const issues = [], configured = [];
|
|
2469
|
+
let gitRoot = project;
|
|
2470
|
+
try {
|
|
2471
|
+
gitRoot = (await exec2("git", ["-C", project, "rev-parse", "--show-toplevel"])).stdout.trim();
|
|
2472
|
+
} catch {
|
|
2473
|
+
}
|
|
2474
|
+
const written = /* @__PURE__ */ new Set();
|
|
2475
|
+
for (const adapter of defaults) {
|
|
2476
|
+
const installed = options.agents ? options.agents.includes(adapter.instance ?? adapter.agent) : await exists(adapter.storage) || (await Promise.all(
|
|
2477
|
+
(env.PATH ?? "").split(":").flatMap(
|
|
2478
|
+
(dir) => adapter.executables.map((name2) => exists(join11(dir, name2), constants3.X_OK))
|
|
2479
|
+
)
|
|
2480
|
+
)).some(Boolean);
|
|
2481
|
+
const piProfiles = adapter.agent === "pi" ? registry.configs.filter((config) => config.kind === "pi-extension").map((config) => dirname6(dirname6(config.path))) : [];
|
|
2482
|
+
if (!installed && !piProfiles.length) continue;
|
|
2483
|
+
for (const sub of adapter.directories) {
|
|
2484
|
+
const source = resolve4(adapter.storage, sub);
|
|
2485
|
+
if (!registry.excludedDiscovery?.includes(source))
|
|
2486
|
+
sessions.discover(adapter.agent, source, project);
|
|
2487
|
+
}
|
|
2488
|
+
if (adapter.agent === "cursor" && !registry.excludedDiscovery?.includes(join11(home, ".cursor", "projects")))
|
|
2489
|
+
sessions.discover("cursor", join11(home, ".cursor", "projects"), project);
|
|
2490
|
+
if (!configured.includes(adapter.agent)) configured.push(adapter.agent);
|
|
2491
|
+
if (adapter.agent === "pi") {
|
|
2492
|
+
for (const profile of /* @__PURE__ */ new Set([...piProfiles, ...installed ? [adapter.storage] : []])) {
|
|
2493
|
+
try {
|
|
2494
|
+
if (profile !== adapter.storage && !await exists(profile))
|
|
2495
|
+
throw new Error(`Registered Pi profile is missing: ${profile}`);
|
|
2496
|
+
await installPiExtension(
|
|
2497
|
+
directory,
|
|
2498
|
+
registry,
|
|
2499
|
+
profile,
|
|
2500
|
+
options.entry ?? resolve4(process.argv[1])
|
|
2501
|
+
);
|
|
2502
|
+
for (const source of await piDiscoveryDirectories(profile, project, home, env)) {
|
|
2503
|
+
if (!registry.excludedDiscovery?.includes(source))
|
|
2504
|
+
sessions.discover("pi", source, project);
|
|
2505
|
+
}
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
issues.push(
|
|
2508
|
+
`Pi: ${error instanceof Error ? error.message : "Extension setup failed."}`
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2023
2512
|
}
|
|
2024
|
-
|
|
2513
|
+
if (!adapter.config) continue;
|
|
2514
|
+
const configRoot = adapter.agent === "cursor" ? gitRoot : project;
|
|
2515
|
+
const path = join11(configRoot, adapter.config);
|
|
2516
|
+
if (written.has(path)) continue;
|
|
2517
|
+
written.add(path);
|
|
2025
2518
|
try {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2519
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2520
|
+
let original = null;
|
|
2521
|
+
try {
|
|
2522
|
+
original = await readStable(configRoot, adapter.config);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
if (!missing2(error)) throw error;
|
|
2525
|
+
}
|
|
2526
|
+
const config = original ? JSON.parse(original.bytes.toString("utf8")) : {};
|
|
2527
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2528
|
+
throw new Error("Hook configuration must be a JSON object.");
|
|
2529
|
+
if (config.hooks !== void 0 && (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)))
|
|
2530
|
+
throw new Error("Invalid existing hooks object.");
|
|
2531
|
+
const hooks = object(config.hooks);
|
|
2532
|
+
const marker2 = hookMarker(directory, root);
|
|
2533
|
+
const cliEntry = options.entry ?? resolve4(process.argv[1]);
|
|
2534
|
+
const hookEntry = join11(dirname6(cliEntry), "session-hook.js");
|
|
2535
|
+
const invocation = [
|
|
2536
|
+
process.execPath,
|
|
2537
|
+
hookEntry,
|
|
2538
|
+
"--agent",
|
|
2539
|
+
adapter.agent,
|
|
2540
|
+
"--root",
|
|
2541
|
+
root,
|
|
2542
|
+
"--state-dir",
|
|
2543
|
+
directory,
|
|
2544
|
+
"--project-dir",
|
|
2545
|
+
project
|
|
2546
|
+
].map(quote).join(" ");
|
|
2547
|
+
const command = `( ${invocation} ) >/dev/null 2>&1 || : # ${marker2}`;
|
|
2548
|
+
removeOwned(hooks, adapter.agent, directory, root);
|
|
2549
|
+
for (const event of adapter.events) {
|
|
2550
|
+
const entries = hooks[event] ?? [];
|
|
2551
|
+
if (!Array.isArray(entries)) throw new Error(`Invalid ${event} hooks.`);
|
|
2552
|
+
hooks[event] = [
|
|
2553
|
+
...entries,
|
|
2554
|
+
adapter.agent === "cursor" ? { command, timeout: 3 } : { hooks: [{ type: "command", command, timeout: 3 }] }
|
|
2555
|
+
];
|
|
2556
|
+
}
|
|
2557
|
+
let owned = registry.configs.find((item) => item.path === path);
|
|
2558
|
+
if (!owned) {
|
|
2559
|
+
owned = { path, agent: adapter.agent, created: !original };
|
|
2560
|
+
registry.configs.push(owned);
|
|
2561
|
+
}
|
|
2562
|
+
saveRegistration(directory, registry);
|
|
2563
|
+
config.hooks = hooks;
|
|
2564
|
+
if (adapter.agent === "cursor") config.version ??= 1;
|
|
2565
|
+
const bytes = Buffer.from(JSON.stringify(config, null, 2) + "\n");
|
|
2566
|
+
if (original?.bytes.equals(bytes)) continue;
|
|
2567
|
+
if (original) {
|
|
2568
|
+
const backupDir = join11(directory, "hook-backups", rootKey(root));
|
|
2569
|
+
await mkdir5(backupDir, { recursive: true, mode: 448 });
|
|
2570
|
+
const hash2 = createHash6("sha256").update(original.bytes).digest("hex");
|
|
2571
|
+
await writeFile2(join11(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
|
|
2572
|
+
mode: 384
|
|
2040
2573
|
});
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2574
|
+
}
|
|
2575
|
+
await replaceFile(configRoot, adapter.config, bytes, original?.signature ?? null);
|
|
2576
|
+
try {
|
|
2577
|
+
const relativePath = path.slice(gitRoot.length + 1);
|
|
2578
|
+
const tracked = (await exec2("git", ["-C", gitRoot, "ls-files", "--", relativePath])).stdout;
|
|
2579
|
+
if (!tracked) {
|
|
2580
|
+
const exclude = (await exec2("git", [
|
|
2581
|
+
"-C",
|
|
2582
|
+
gitRoot,
|
|
2583
|
+
"rev-parse",
|
|
2584
|
+
"--path-format=absolute",
|
|
2585
|
+
"--git-path",
|
|
2586
|
+
"info/exclude"
|
|
2587
|
+
])).stdout.trim();
|
|
2588
|
+
const old = await readFile2(exclude, "utf8").catch((e) => {
|
|
2589
|
+
if (missing2(e)) return "";
|
|
2590
|
+
throw e;
|
|
2591
|
+
});
|
|
2592
|
+
const rule = "/" + relativePath.replace(/[\\*?[\]#! ]/g, "\\$&");
|
|
2593
|
+
if (!old.split(/\r?\n/).includes(rule)) {
|
|
2594
|
+
await mkdir5(dirname6(exclude), { recursive: true });
|
|
2595
|
+
owned.exclude = {
|
|
2596
|
+
path: exclude,
|
|
2597
|
+
line: "# teammy-hook-" + rootKey(directory) + "-" + rootKey(root) + "\n" + rule
|
|
2598
|
+
};
|
|
2599
|
+
saveRegistration(directory, registry);
|
|
2600
|
+
await writeFile2(
|
|
2601
|
+
exclude,
|
|
2602
|
+
old + (old && !old.endsWith("\n") ? "\n" : "") + owned.exclude.line + "\n"
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2045
2605
|
}
|
|
2606
|
+
} catch {
|
|
2046
2607
|
}
|
|
2047
|
-
} catch {
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
issues.push(`${path}: ${error instanceof Error ? error.message : "Hook setup failed."}`);
|
|
2048
2610
|
}
|
|
2049
|
-
} catch (error) {
|
|
2050
|
-
issues.push(`${path}: ${error instanceof Error ? error.message : "Hook setup failed."}`);
|
|
2051
2611
|
}
|
|
2612
|
+
return { configured, issues };
|
|
2613
|
+
} finally {
|
|
2614
|
+
lock.close();
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function hookMarker(directory, root) {
|
|
2618
|
+
return `teammy-session-${rootKey(directory)}-${rootKey(root)}`;
|
|
2619
|
+
}
|
|
2620
|
+
function setDiscoveryPreference(directory, root, source, enabled) {
|
|
2621
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2622
|
+
try {
|
|
2623
|
+
const registry = readRegistration(directory, root);
|
|
2624
|
+
if (!registry) return;
|
|
2625
|
+
const excluded = new Set(registry.excludedDiscovery ?? []);
|
|
2626
|
+
if (enabled) excluded.delete(source);
|
|
2627
|
+
else excluded.add(source);
|
|
2628
|
+
registry.excludedDiscovery = [...excluded];
|
|
2629
|
+
saveRegistration(directory, registry);
|
|
2630
|
+
} finally {
|
|
2631
|
+
lock.close();
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function owns(command, directory, root) {
|
|
2635
|
+
if (typeof command !== "string") return false;
|
|
2636
|
+
if (command.trimEnd().endsWith("# " + hookMarker(directory, root))) return true;
|
|
2637
|
+
return command.trimEnd().endsWith("# teammy-session-" + rootKey(root)) && command.includes(quote("--state-dir") + " " + quote(directory));
|
|
2638
|
+
}
|
|
2639
|
+
function removeOwned(hooks, agent, directory, root) {
|
|
2640
|
+
let changed = false;
|
|
2641
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
2642
|
+
if (!Array.isArray(entries)) continue;
|
|
2643
|
+
const retained = entries.flatMap((entry) => {
|
|
2644
|
+
const value = object(entry);
|
|
2645
|
+
if (agent === "cursor") return owns(value.command, directory, root) ? [] : [entry];
|
|
2646
|
+
if (!Array.isArray(value.hooks)) return [entry];
|
|
2647
|
+
const nested = value.hooks.filter((hook) => !owns(object(hook).command, directory, root));
|
|
2648
|
+
if (nested.length === value.hooks.length) return [entry];
|
|
2649
|
+
return nested.length ? [{ ...value, hooks: nested }] : [];
|
|
2650
|
+
});
|
|
2651
|
+
if (JSON.stringify(retained) === JSON.stringify(entries)) continue;
|
|
2652
|
+
changed = true;
|
|
2653
|
+
if (retained.length) hooks[event] = retained;
|
|
2654
|
+
else delete hooks[event];
|
|
2655
|
+
}
|
|
2656
|
+
return changed;
|
|
2657
|
+
}
|
|
2658
|
+
async function legacyRegistrations(directory, explicitRoot) {
|
|
2659
|
+
const roots = new Set(explicitRoot ? [explicitRoot] : []);
|
|
2660
|
+
let db;
|
|
2661
|
+
try {
|
|
2662
|
+
db = new DatabaseSync4(join11(directory, "client.sqlite3"), { readOnly: true });
|
|
2663
|
+
for (const row of db.prepare("SELECT root FROM connections WHERE binding_json IS NOT NULL").all())
|
|
2664
|
+
if (!explicitRoot || row.root === explicitRoot) roots.add(String(row.root));
|
|
2665
|
+
} catch {
|
|
2666
|
+
} finally {
|
|
2667
|
+
db?.close();
|
|
2668
|
+
}
|
|
2669
|
+
for (const root of roots) {
|
|
2670
|
+
try {
|
|
2671
|
+
if (readRegistration(directory, root)) continue;
|
|
2672
|
+
} catch {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
const project = dirname6(root);
|
|
2676
|
+
let gitRoot = project;
|
|
2677
|
+
try {
|
|
2678
|
+
gitRoot = (await exec2("git", ["-C", project, "rev-parse", "--show-toplevel"])).stdout.trim();
|
|
2679
|
+
} catch {
|
|
2680
|
+
}
|
|
2681
|
+
const configs = [
|
|
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 }
|
|
2685
|
+
];
|
|
2686
|
+
saveRegistration(directory, { format: 1, root, projectDir: project, enabled: true, configs });
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
async function teardownSessions(directory, root) {
|
|
2690
|
+
const lock = acquireRootLock("hooks", join11(directory, "runtime"));
|
|
2691
|
+
const issues = [];
|
|
2692
|
+
let removed = 0;
|
|
2693
|
+
try {
|
|
2694
|
+
await legacyRegistrations(directory, root);
|
|
2695
|
+
const selected = registrations(directory, (path, error) => {
|
|
2696
|
+
issues.push(
|
|
2697
|
+
`${path}: ${error instanceof Error ? error.message : "Invalid Hook registration."}`
|
|
2698
|
+
);
|
|
2699
|
+
}).filter((item) => !root || item.root === root);
|
|
2700
|
+
for (const registration of selected) {
|
|
2701
|
+
registration.enabled = false;
|
|
2702
|
+
try {
|
|
2703
|
+
saveRegistration(directory, registration);
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
issues.push(
|
|
2706
|
+
`${registration.root}: ${error instanceof Error ? error.message : "Hook removal failed."}`
|
|
2707
|
+
);
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
for (const config of registration.configs) {
|
|
2711
|
+
try {
|
|
2712
|
+
if (config.kind === "pi-extension") {
|
|
2713
|
+
if (await removePiExtension(directory, config.path)) removed++;
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
const original = await readStable(
|
|
2717
|
+
dirname6(config.path),
|
|
2718
|
+
config.path.split(/[\\/]/).at(-1)
|
|
2719
|
+
);
|
|
2720
|
+
const value = JSON.parse(original.bytes.toString("utf8"));
|
|
2721
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2722
|
+
throw new Error("Invalid hook configuration.");
|
|
2723
|
+
if (value.hooks !== void 0 && (!value.hooks || typeof value.hooks !== "object" || Array.isArray(value.hooks)))
|
|
2724
|
+
throw new Error("Invalid hooks object.");
|
|
2725
|
+
const hooks = object(value.hooks);
|
|
2726
|
+
if (!removeOwned(hooks, config.agent, directory, registration.root)) continue;
|
|
2727
|
+
if (Object.keys(hooks).length) value.hooks = hooks;
|
|
2728
|
+
else delete value.hooks;
|
|
2729
|
+
const empty = !Object.keys(value).length || config.agent === "cursor" && Object.keys(value).length === 1 && value.version === 1;
|
|
2730
|
+
if (empty && config.created)
|
|
2731
|
+
await removeFile(
|
|
2732
|
+
dirname6(config.path),
|
|
2733
|
+
config.path.split(/[\\/]/).at(-1),
|
|
2734
|
+
original.signature
|
|
2735
|
+
);
|
|
2736
|
+
else if (!original.bytes.equals(Buffer.from(JSON.stringify(value, null, 2) + "\n")))
|
|
2737
|
+
await replaceFile(
|
|
2738
|
+
dirname6(config.path),
|
|
2739
|
+
config.path.split(/[\\/]/).at(-1),
|
|
2740
|
+
Buffer.from(JSON.stringify(value, null, 2) + "\n"),
|
|
2741
|
+
original.signature
|
|
2742
|
+
);
|
|
2743
|
+
if (config.exclude && !await exists(config.path)) {
|
|
2744
|
+
const text = await readFile2(config.exclude.path, "utf8");
|
|
2745
|
+
await writeFile2(config.exclude.path, text.replace(config.exclude.line + "\n", ""));
|
|
2746
|
+
}
|
|
2747
|
+
removed++;
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
if (!missing2(error))
|
|
2750
|
+
issues.push(
|
|
2751
|
+
`${config.path}: ${error instanceof Error ? error.message : "Hook removal failed."}`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return { removed, issues };
|
|
2757
|
+
} finally {
|
|
2758
|
+
lock.close();
|
|
2052
2759
|
}
|
|
2053
|
-
return { configured, issues };
|
|
2054
2760
|
}
|
|
2055
2761
|
|
|
2762
|
+
// apps/client/sessions/hook-entry.ts
|
|
2763
|
+
import { parseArgs } from "node:util";
|
|
2764
|
+
import { realpath as realpath5 } from "node:fs/promises";
|
|
2765
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
|
|
2766
|
+
import { homedir as homedir2 } from "node:os";
|
|
2767
|
+
import { join as join13 } from "node:path";
|
|
2768
|
+
|
|
2056
2769
|
// apps/client/sessions/hooks.ts
|
|
2057
|
-
import { createHash as
|
|
2058
|
-
import { mkdir as
|
|
2059
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2060
|
-
import { z as
|
|
2061
|
-
var HintSchema =
|
|
2770
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
|
|
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({
|
|
2062
2775
|
agent: AgentSchema,
|
|
2063
|
-
path:
|
|
2064
|
-
nativeId:
|
|
2065
|
-
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)
|
|
2066
2779
|
});
|
|
2067
2780
|
function inbox(directory, root) {
|
|
2068
|
-
return
|
|
2781
|
+
return join12(directory, "session-hints", createHash7("sha256").update(root).digest("hex"));
|
|
2069
2782
|
}
|
|
2070
2783
|
async function recordHint(directory, root, input) {
|
|
2071
2784
|
const hint = HintSchema.parse(input);
|
|
2072
2785
|
const targetDir = inbox(directory, root);
|
|
2073
|
-
await
|
|
2074
|
-
const target =
|
|
2786
|
+
await mkdir6(targetDir, { recursive: true, mode: 448 });
|
|
2787
|
+
const target = join12(
|
|
2075
2788
|
targetDir,
|
|
2076
|
-
`${
|
|
2789
|
+
`${createHash7("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
|
|
2077
2790
|
);
|
|
2078
|
-
const temporary = `${target}.${
|
|
2079
|
-
const handle = await
|
|
2791
|
+
const temporary = `${target}.${randomUUID5()}.tmp`;
|
|
2792
|
+
const handle = await open3(temporary, "wx", 384);
|
|
2080
2793
|
try {
|
|
2081
2794
|
await handle.writeFile(JSON.stringify(hint));
|
|
2082
2795
|
} finally {
|
|
@@ -2097,9 +2810,9 @@ async function consumeHints(directory, root, sessions, refresh) {
|
|
|
2097
2810
|
for await (const entry of dir) {
|
|
2098
2811
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
2099
2812
|
if (count++ >= 50) break;
|
|
2100
|
-
const path =
|
|
2813
|
+
const path = join12(inbox(directory, root), entry.name);
|
|
2101
2814
|
try {
|
|
2102
|
-
const handle = await
|
|
2815
|
+
const handle = await open3(path, "r");
|
|
2103
2816
|
let body;
|
|
2104
2817
|
try {
|
|
2105
2818
|
if ((await handle.stat()).size > 16384) throw new Error("Oversized hook hint.");
|
|
@@ -2137,353 +2850,104 @@ async function hookPayload() {
|
|
|
2137
2850
|
}
|
|
2138
2851
|
}
|
|
2139
2852
|
|
|
2140
|
-
// apps/client/sessions/
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
setup Configure project hooks and native discovery (also done by connect)
|
|
2144
|
-
track --agent AGENT --file PATH [--project-dir DIR] [--adopt-existing]
|
|
2145
|
-
untrack --file PATH (does not delete server history)
|
|
2146
|
-
discover --agent AGENT [--directory DIR] [--project-dir DIR]
|
|
2147
|
-
undiscover --directory DIR (also untrack any previously discovered sources to stop them)
|
|
2148
|
-
sources Show registered sources, discovery directories and collection errors
|
|
2149
|
-
hook --agent AGENT [--file PATH] [--project-dir DIR]; accepts JSON on stdin
|
|
2150
|
-
list [--after N] [--limit N]
|
|
2151
|
-
read --owner MEMBER --key KEY [--revision N]
|
|
2152
|
-
search --query TEXT [--after N] [--limit N]
|
|
2153
|
-
|
|
2154
|
-
Shared options: --root DIR (./workspaces), --state-dir DIR (~/.teammy/client).
|
|
2155
|
-
AGENT: claude-code, codex, cursor, pi, dsh. Project defaults to the shared root's parent.
|
|
2156
|
-
track/discover only register local sources. Run teammy sync to collect and publish.
|
|
2157
|
-
Discover uses native default locations except DSH, which requires --directory.
|
|
2158
|
-
Hook only records a local hint, emits no stdout, and always exits successfully.
|
|
2159
|
-
`;
|
|
2160
|
-
async function runSessionCommand(args) {
|
|
2161
|
-
const command = args[0];
|
|
2162
|
-
if (!command || ["--help", "-h", "help"].includes(command)) {
|
|
2163
|
-
process.stdout.write(help);
|
|
2164
|
-
return 0;
|
|
2165
|
-
}
|
|
2166
|
-
const { values } = parseArgs({
|
|
2167
|
-
args: args.slice(1),
|
|
2168
|
-
options: {
|
|
2169
|
-
root: { type: "string", default: resolve4("workspaces") },
|
|
2170
|
-
"state-dir": { type: "string", default: join7(homedir2(), ".teammy", "client") },
|
|
2171
|
-
agent: { type: "string" },
|
|
2172
|
-
file: { type: "string" },
|
|
2173
|
-
directory: { type: "string" },
|
|
2174
|
-
"project-dir": { type: "string" },
|
|
2175
|
-
"adopt-existing": { type: "boolean" },
|
|
2176
|
-
owner: { type: "string" },
|
|
2177
|
-
key: { type: "string" },
|
|
2178
|
-
revision: { type: "string" },
|
|
2179
|
-
query: { type: "string" },
|
|
2180
|
-
after: { type: "string", default: "0" },
|
|
2181
|
-
limit: { type: "string", default: "50" },
|
|
2182
|
-
help: { type: "boolean", short: "h" }
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
if (values.help) {
|
|
2186
|
-
process.stdout.write(help);
|
|
2187
|
-
return 0;
|
|
2188
|
-
}
|
|
2189
|
-
const root = await realpath4(resolve4(values.root));
|
|
2190
|
-
const directory = await stateLocation(root, values["state-dir"]);
|
|
2191
|
-
const projectDir = await realpath4(resolve4(values["project-dir"] ?? dirname5(root)));
|
|
2192
|
-
if (command === "hook") {
|
|
2193
|
-
const payload = values.file ? {} : await hookPayload();
|
|
2194
|
-
const file = values.file ?? payload.transcript_path ?? payload.session_file ?? payload.sessionFile;
|
|
2195
|
-
const nativeId = payload.session_id ?? payload.conversation_id;
|
|
2196
|
-
if (typeof file === "string" && file || typeof nativeId === "string")
|
|
2197
|
-
await recordHint(directory, root, {
|
|
2198
|
-
agent: AgentSchema.parse(values.agent),
|
|
2199
|
-
...typeof file === "string" && file ? { path: resolve4(file) } : {},
|
|
2200
|
-
...typeof nativeId === "string" ? { nativeId } : {},
|
|
2201
|
-
projectDir
|
|
2202
|
-
});
|
|
2203
|
-
return 0;
|
|
2204
|
-
}
|
|
2205
|
-
const state = new ClientState(directory);
|
|
2853
|
+
// apps/client/sessions/hook-entry.ts
|
|
2854
|
+
async function runHook(args) {
|
|
2206
2855
|
try {
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
if (command === "track")
|
|
2217
|
-
sessions.track(
|
|
2218
|
-
AgentSchema.parse(values.agent),
|
|
2219
|
-
path,
|
|
2220
|
-
projectDir,
|
|
2221
|
-
Boolean(values["adopt-existing"])
|
|
2222
|
-
);
|
|
2223
|
-
else sessions.untrack(path);
|
|
2224
|
-
output = sessions.status();
|
|
2225
|
-
} else if (command === "discover" || command === "undiscover") {
|
|
2226
|
-
if (command === "undiscover") {
|
|
2227
|
-
if (!values.directory) throw new Error("--directory is required.");
|
|
2228
|
-
state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, await realpath4(resolve4(values.directory)));
|
|
2229
|
-
} else {
|
|
2230
|
-
const agent = AgentSchema.parse(values.agent);
|
|
2231
|
-
const defaults = {
|
|
2232
|
-
"claude-code": join7(
|
|
2233
|
-
process.env.CLAUDE_CONFIG_DIR ?? join7(homedir2(), ".claude"),
|
|
2234
|
-
"projects"
|
|
2235
|
-
),
|
|
2236
|
-
codex: join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "sessions"),
|
|
2237
|
-
cursor: join7(
|
|
2238
|
-
process.env.CURSOR_CONFIG_DIR ?? join7(homedir2(), ".cursor"),
|
|
2239
|
-
"chats",
|
|
2240
|
-
createHash7("md5").update(projectDir).digest("hex")
|
|
2241
|
-
),
|
|
2242
|
-
pi: join7(process.env.PI_CODING_AGENT_DIR ?? join7(homedir2(), ".pi", "agent"), "sessions"),
|
|
2243
|
-
dsh: void 0
|
|
2244
|
-
};
|
|
2245
|
-
const path = values.directory ?? defaults[agent];
|
|
2246
|
-
if (!path) throw new Error("DSH requires its configured persistence --directory.");
|
|
2247
|
-
sessions.discover(agent, await realpath4(resolve4(path)), projectDir);
|
|
2856
|
+
const { values } = parseArgs({
|
|
2857
|
+
args,
|
|
2858
|
+
options: {
|
|
2859
|
+
agent: { type: "string" },
|
|
2860
|
+
root: { type: "string" },
|
|
2861
|
+
"state-dir": { type: "string" },
|
|
2862
|
+
"project-dir": { type: "string" },
|
|
2863
|
+
file: { type: "string" },
|
|
2864
|
+
"integration-file": { type: "string" }
|
|
2248
2865
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
sources: sessions.status(),
|
|
2253
|
-
discovery: sessions.directories(),
|
|
2254
|
-
hooks: sessions.hooks()
|
|
2255
|
-
};
|
|
2256
|
-
else {
|
|
2257
|
-
const after = Number(values.after);
|
|
2258
|
-
const limit = Number(values.limit);
|
|
2259
|
-
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
2260
|
-
throw new Error("Invalid pagination options.");
|
|
2261
|
-
if (command === "list") output = await client.sessions(after, limit);
|
|
2262
|
-
else if (command === "search") {
|
|
2263
|
-
if (!values.query) throw new Error("--query is required.");
|
|
2264
|
-
output = await client.searchSessions(values.query, after, limit);
|
|
2265
|
-
} else if (command === "read") {
|
|
2266
|
-
if (!values.owner || !values.key) throw new Error("--owner and --key are required.");
|
|
2267
|
-
const revision = values.revision === void 0 ? void 0 : Number(values.revision);
|
|
2268
|
-
if (revision !== void 0 && (!Number.isSafeInteger(revision) || revision < 1))
|
|
2269
|
-
throw new Error("Invalid revision.");
|
|
2270
|
-
const session = await client.session(values.owner, values.key, revision);
|
|
2271
|
-
const snapshot = SessionSnapshotSchema.parse(
|
|
2272
|
-
JSON.parse(Buffer.from(await client.downloadBlob(session.blob.sha256)).toString("utf8"))
|
|
2273
|
-
);
|
|
2274
|
-
output = { session, snapshot };
|
|
2275
|
-
} else throw new Error("Unknown session command. Use teammy session --help.");
|
|
2276
|
-
}
|
|
2277
|
-
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
2278
|
-
`);
|
|
2279
|
-
return 0;
|
|
2280
|
-
} finally {
|
|
2281
|
-
state.close();
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
// apps/client/sync-command.ts
|
|
2286
|
-
import { parseArgs as parseArgs3 } from "node:util";
|
|
2287
|
-
import { setTimeout as setTimeout4 } from "node:timers/promises";
|
|
2288
|
-
|
|
2289
|
-
// apps/client/client-command.ts
|
|
2290
|
-
import { parseArgs as parseArgs2 } from "node:util";
|
|
2291
|
-
import { join as join12, resolve as resolve6 } from "node:path";
|
|
2292
|
-
import { homedir as homedir4 } from "node:os";
|
|
2293
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
2294
|
-
|
|
2295
|
-
// apps/client/connection.ts
|
|
2296
|
-
import { readFile as readFile2, mkdir as mkdir6 } from "node:fs/promises";
|
|
2297
|
-
import { createInterface } from "node:readline/promises";
|
|
2298
|
-
|
|
2299
|
-
// apps/client/root-lock.ts
|
|
2300
|
-
import { createHash as createHash8 } from "node:crypto";
|
|
2301
|
-
import { join as join8 } from "node:path";
|
|
2302
|
-
function acquireRootLock(root, stateDirectory) {
|
|
2303
|
-
let lock;
|
|
2304
|
-
try {
|
|
2305
|
-
lock = openDatabase(
|
|
2306
|
-
join8(stateDirectory, "locks", `${createHash8("sha256").update(root).digest("hex")}.sqlite3`),
|
|
2307
|
-
[],
|
|
2308
|
-
0
|
|
2309
|
-
);
|
|
2310
|
-
lock.exec("PRAGMA busy_timeout=0; BEGIN IMMEDIATE");
|
|
2311
|
-
return lock;
|
|
2312
|
-
} catch (error) {
|
|
2313
|
-
lock?.close();
|
|
2314
|
-
if (error instanceof Error && /locked|busy/.test(error.message))
|
|
2315
|
-
throw new Error("Another sync or connect process is using this root.");
|
|
2316
|
-
throw error;
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
// apps/client/local-config.ts
|
|
2321
|
-
import { constants as constants3 } from "node:fs";
|
|
2322
|
-
import { open as open3, writeFile as writeFile2 } from "node:fs/promises";
|
|
2323
|
-
import { join as join9 } from "node:path";
|
|
2324
|
-
import { z as z11 } from "zod";
|
|
2325
|
-
import ignore from "ignore";
|
|
2326
|
-
var LOCAL_CONFIG_NAME = ".teammy-local.json";
|
|
2327
|
-
var LocalConfigSchema = z11.object({
|
|
2328
|
-
files: z11.object({ ignore: z11.array(z11.string().max(1024)).max(256).default([]) }).strict().default({ ignore: [] })
|
|
2329
|
-
}).strict();
|
|
2330
|
-
async function ensureLocalConfig(root) {
|
|
2331
|
-
try {
|
|
2332
|
-
await writeFile2(
|
|
2333
|
-
join9(root, LOCAL_CONFIG_NAME),
|
|
2334
|
-
JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
|
|
2335
|
-
{ flag: "wx", mode: 384 }
|
|
2336
|
-
);
|
|
2337
|
-
} catch (error) {
|
|
2338
|
-
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
async function loadLocalRules(root) {
|
|
2342
|
-
let rules = [];
|
|
2343
|
-
try {
|
|
2344
|
-
const file = await open3(
|
|
2345
|
-
join9(root, LOCAL_CONFIG_NAME),
|
|
2346
|
-
constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
|
|
2866
|
+
});
|
|
2867
|
+
const directory = await realpath5(
|
|
2868
|
+
resolve5(values["state-dir"] ?? join13(homedir2(), ".teammy", "client"))
|
|
2347
2869
|
);
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
const
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
2359
|
-
throw new Error("Configuration is being saved; retry on the next check.");
|
|
2360
|
-
rules = LocalConfigSchema.parse(JSON.parse(text)).files.ignore;
|
|
2361
|
-
} finally {
|
|
2362
|
-
await file.close();
|
|
2363
|
-
}
|
|
2364
|
-
} catch (error) {
|
|
2365
|
-
if (!missing(error))
|
|
2366
|
-
throw new Error(
|
|
2367
|
-
`${LOCAL_CONFIG_NAME}: invalid or unreadable configuration; uploads paused.`,
|
|
2368
|
-
{ cause: error }
|
|
2369
|
-
);
|
|
2370
|
-
}
|
|
2371
|
-
const matcher = ignore().add(rules);
|
|
2372
|
-
return {
|
|
2373
|
-
fingerprint: JSON.stringify(rules),
|
|
2374
|
-
excludes: (path, directory = false) => ignored(path) || matcher.ignores(path + (directory && !path.endsWith("/") ? "/" : ""))
|
|
2375
|
-
};
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
// apps/client/connection.ts
|
|
2379
|
-
async function connectProject(state, root, directory, options) {
|
|
2380
|
-
const lock = acquireRootLock(root, directory);
|
|
2381
|
-
try {
|
|
2382
|
-
state.assertRootAvailable(root);
|
|
2383
|
-
const transport = new HubClient(options.url);
|
|
2384
|
-
const existing = state.binding(root);
|
|
2385
|
-
const project = new ClientProjectState(state, root);
|
|
2386
|
-
if (existing && existing.serverUrl !== transport.url)
|
|
2387
|
-
throw new Error("This root is connected to another server.");
|
|
2388
|
-
if (existing && options.project && options.project !== existing.workspaceId)
|
|
2389
|
-
throw new Error("This root belongs to a different project. Use a different local root.");
|
|
2390
|
-
const info = await transport.serverInfo();
|
|
2391
|
-
if (!info.capabilities.includes("multi-project"))
|
|
2392
|
-
throw new Error(
|
|
2393
|
-
"This client requires a multi-project server. Update the server before connecting."
|
|
2870
|
+
const payload = values.file ? {} : await hookPayload();
|
|
2871
|
+
let selectedRoot = values.root;
|
|
2872
|
+
if (values["integration-file"]) {
|
|
2873
|
+
if (values.agent !== "pi" || typeof payload.cwd !== "string") return;
|
|
2874
|
+
const cwd = await realpath5(payload.cwd);
|
|
2875
|
+
const candidates2 = registrations(directory, () => {
|
|
2876
|
+
}).filter(
|
|
2877
|
+
(item) => item.enabled && item.configs.some(
|
|
2878
|
+
(config) => config.kind === "pi-extension" && config.path === values["integration-file"]
|
|
2879
|
+
)
|
|
2394
2880
|
);
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
if (member && member !== existing.memberId)
|
|
2398
|
-
throw new Error("This root belongs to a different member.");
|
|
2399
|
-
const client2 = new HubClient(existing.serverUrl, 1e4, existing);
|
|
2400
|
-
await client2.members();
|
|
2401
|
-
} else if (options.tokenFile) {
|
|
2402
|
-
const token = CredentialSchema.parse((await readFile2(options.tokenFile, "utf8")).trim());
|
|
2403
|
-
const deviceId = state.deviceFor(root);
|
|
2404
|
-
const client2 = new HubClient(options.url, 1e4, { token, deviceId });
|
|
2405
|
-
const binding2 = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
|
|
2406
|
-
if (options.project && binding2.workspaceId !== options.project)
|
|
2407
|
-
throw new Error("Token belongs to a different project.");
|
|
2408
|
-
state.saveBinding(root, client2.url, token, binding2);
|
|
2409
|
-
} else {
|
|
2410
|
-
member ??= existing?.memberId;
|
|
2411
|
-
if (!member && process.stdin.isTTY) {
|
|
2412
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2881
|
+
const matches = [];
|
|
2882
|
+
for (const item of candidates2) {
|
|
2413
2883
|
try {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2884
|
+
const project = await realpath5(item.projectDir);
|
|
2885
|
+
const rel = relative4(project, cwd);
|
|
2886
|
+
if (!isAbsolute4(rel) && rel !== ".." && !rel.startsWith(".." + sep4)) matches.push(item);
|
|
2887
|
+
} catch {
|
|
2417
2888
|
}
|
|
2418
2889
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
if (
|
|
2422
|
-
throw new Error("This root belongs to a different member.");
|
|
2423
|
-
const projectId = options.project ?? existing?.workspaceId;
|
|
2424
|
-
if (!projectId)
|
|
2425
|
-
throw new Error(
|
|
2426
|
-
"--project is required for a new connection; copy the command from the project settings."
|
|
2427
|
-
);
|
|
2428
|
-
NameSchema.parse(projectId);
|
|
2429
|
-
const request = project.enrollment(
|
|
2430
|
-
transport.url,
|
|
2431
|
-
member,
|
|
2432
|
-
projectId,
|
|
2433
|
-
Boolean(options.reconnect)
|
|
2434
|
-
);
|
|
2435
|
-
const binding2 = await transport.enroll(request);
|
|
2436
|
-
state.saveBinding(root, transport.url, request.credential, binding2);
|
|
2437
|
-
project.completeEnrollment();
|
|
2890
|
+
matches.sort((a, b) => b.projectDir.length - a.projectDir.length);
|
|
2891
|
+
selectedRoot = matches[0]?.root;
|
|
2892
|
+
if (!selectedRoot) return;
|
|
2438
2893
|
}
|
|
2439
|
-
const
|
|
2440
|
-
const
|
|
2441
|
-
if (
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
}
|
|
2894
|
+
const root = await realpath5(resolve5(selectedRoot ?? "workspaces"));
|
|
2895
|
+
const registration = readRegistration(directory, root);
|
|
2896
|
+
if (!registration?.enabled) return;
|
|
2897
|
+
const projectDir = await realpath5(registration.projectDir);
|
|
2898
|
+
if (projectDir !== await realpath5(values["project-dir"] ?? dirname7(root))) return;
|
|
2899
|
+
if (typeof payload.cwd === "string") {
|
|
2900
|
+
const rel = relative4(projectDir, await realpath5(payload.cwd));
|
|
2901
|
+
if (isAbsolute4(rel) || rel === ".." || rel.startsWith(".." + sep4)) return;
|
|
2448
2902
|
}
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
if (
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2903
|
+
const file = values.file ?? payload.transcript_path ?? payload.session_file ?? payload.sessionFile;
|
|
2904
|
+
const nativeId = payload.session_id ?? payload.conversation_id;
|
|
2905
|
+
if (typeof file === "string" && file || typeof nativeId === "string")
|
|
2906
|
+
await recordHint(directory, root, {
|
|
2907
|
+
agent: values.agent,
|
|
2908
|
+
...typeof file === "string" && file ? { path: resolve5(projectDir, file) } : {},
|
|
2909
|
+
...typeof nativeId === "string" ? { nativeId } : {},
|
|
2910
|
+
projectDir
|
|
2911
|
+
});
|
|
2912
|
+
} catch {
|
|
2457
2913
|
}
|
|
2458
2914
|
}
|
|
2459
2915
|
|
|
2916
|
+
// apps/client/runtime/maintenance.ts
|
|
2917
|
+
import { mkdir as mkdir9, rename as rename4, rm as rm2, readFile as readFile4 } from "node:fs/promises";
|
|
2918
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
2919
|
+
import { join as join16, resolve as resolve7 } from "node:path";
|
|
2920
|
+
import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
|
|
2921
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2922
|
+
|
|
2460
2923
|
// apps/client/runtime/ipc.ts
|
|
2461
2924
|
import { createConnection } from "node:net";
|
|
2462
2925
|
import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
|
|
2463
|
-
import { join as
|
|
2464
|
-
import { z as
|
|
2465
|
-
var RequestSchema =
|
|
2466
|
-
method:
|
|
2467
|
-
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()
|
|
2468
2931
|
}).strict();
|
|
2469
2932
|
function socketPath(directory) {
|
|
2470
|
-
return
|
|
2933
|
+
return join14(
|
|
2471
2934
|
"/tmp",
|
|
2472
2935
|
`teammy-${process.getuid?.() ?? "user"}-${rootKey(directory)}`,
|
|
2473
2936
|
"control.sock"
|
|
2474
2937
|
);
|
|
2475
2938
|
}
|
|
2476
2939
|
async function prepareSocketDirectory(directory) {
|
|
2477
|
-
const path =
|
|
2940
|
+
const path = join14(socketPath(directory), "..");
|
|
2478
2941
|
await mkdir7(path, { recursive: true, mode: 448 });
|
|
2479
2942
|
const stat2 = await lstat3(path);
|
|
2480
2943
|
if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.())
|
|
2481
2944
|
throw new Error("Unsafe daemon socket directory.");
|
|
2482
2945
|
await chmod(path, 448);
|
|
2483
2946
|
}
|
|
2947
|
+
var CONTROL_PROTOCOL = 1;
|
|
2484
2948
|
async function daemonRequest(directory, input, timeout = 15e3) {
|
|
2485
2949
|
RequestSchema.parse(input);
|
|
2486
|
-
return new Promise((
|
|
2950
|
+
return new Promise((resolve16, reject) => {
|
|
2487
2951
|
const socket = createConnection(socketPath(directory));
|
|
2488
2952
|
socket.setEncoding("utf8");
|
|
2489
2953
|
let body = "";
|
|
@@ -2493,11 +2957,15 @@ async function daemonRequest(directory, input, timeout = 15e3) {
|
|
|
2493
2957
|
done = true;
|
|
2494
2958
|
socket.destroy();
|
|
2495
2959
|
if (error) reject(error);
|
|
2496
|
-
else
|
|
2960
|
+
else resolve16(result2);
|
|
2497
2961
|
};
|
|
2498
2962
|
socket.setTimeout(
|
|
2499
2963
|
timeout,
|
|
2500
|
-
() => finish(
|
|
2964
|
+
() => finish(
|
|
2965
|
+
Object.assign(new Error("Background service did not respond. Run teammy repair."), {
|
|
2966
|
+
code: "ETIMEDOUT"
|
|
2967
|
+
})
|
|
2968
|
+
)
|
|
2501
2969
|
);
|
|
2502
2970
|
socket.on("error", (e) => finish(e));
|
|
2503
2971
|
socket.on("end", () => {
|
|
@@ -2521,10 +2989,28 @@ async function daemonRequest(directory, input, timeout = 15e3) {
|
|
|
2521
2989
|
});
|
|
2522
2990
|
});
|
|
2523
2991
|
}
|
|
2992
|
+
async function probeDaemon(directory) {
|
|
2993
|
+
try {
|
|
2994
|
+
const info = await daemonRequest(directory, { method: "ping" }, 1e3);
|
|
2995
|
+
if (!info || !Number.isSafeInteger(info.pid) || info.pid <= 0 || typeof info.version !== "string" || info.protocol !== void 0 && info.protocol !== CONTROL_PROTOCOL)
|
|
2996
|
+
return { state: "incompatible", error: "Unsupported background control protocol." };
|
|
2997
|
+
return { state: "ready", info };
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
if (["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE"].includes(
|
|
3000
|
+
String(error.code)
|
|
3001
|
+
))
|
|
3002
|
+
return { state: "absent" };
|
|
3003
|
+
return {
|
|
3004
|
+
state: error.code === "ETIMEDOUT" ? "unresponsive" : "incompatible",
|
|
3005
|
+
error: error instanceof Error ? error.message : "Invalid background response."
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
2524
3009
|
async function runningDaemon(directory) {
|
|
2525
3010
|
try {
|
|
2526
3011
|
return await daemonRequest(directory, { method: "ping" }, 1e3);
|
|
2527
3012
|
} catch (error) {
|
|
3013
|
+
if (error instanceof Error && error.message === "Background service is stopping.") return null;
|
|
2528
3014
|
if (["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE"].includes(
|
|
2529
3015
|
String(error.code)
|
|
2530
3016
|
))
|
|
@@ -2537,8 +3023,8 @@ async function runningDaemon(directory) {
|
|
|
2537
3023
|
import { execFile as execFile3 } from "node:child_process";
|
|
2538
3024
|
import { promisify as promisify3 } from "node:util";
|
|
2539
3025
|
import { homedir as homedir3 } from "node:os";
|
|
2540
|
-
import { dirname as
|
|
2541
|
-
import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "node:fs/promises";
|
|
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";
|
|
2542
3028
|
import { setTimeout as setTimeout3 } from "node:timers/promises";
|
|
2543
3029
|
var execute = promisify3(execFile3);
|
|
2544
3030
|
var exec3 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
|
|
@@ -2555,7 +3041,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2555
3041
|
if (platform === "darwin")
|
|
2556
3042
|
return {
|
|
2557
3043
|
id,
|
|
2558
|
-
path:
|
|
3044
|
+
path: join15(userHome, "Library", "LaunchAgents", id + ".plist"),
|
|
2559
3045
|
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
2560
3046
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2561
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>
|
|
@@ -2564,7 +3050,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2564
3050
|
if (platform === "linux")
|
|
2565
3051
|
return {
|
|
2566
3052
|
id: id + ".service",
|
|
2567
|
-
path:
|
|
3053
|
+
path: join15(userHome, ".config", "systemd", "user", id + ".service"),
|
|
2568
3054
|
content: `[Unit]
|
|
2569
3055
|
Description=Teammy background synchronization
|
|
2570
3056
|
[Service]
|
|
@@ -2610,17 +3096,19 @@ async function bootstrapLaunchAgent(path) {
|
|
|
2610
3096
|
}
|
|
2611
3097
|
}
|
|
2612
3098
|
async function ensureDaemon(directory, version) {
|
|
2613
|
-
const definition = serviceDefinition(directory, process.execPath,
|
|
3099
|
+
const definition = serviceDefinition(directory, process.execPath, resolve6(process.argv[1]));
|
|
2614
3100
|
let old = "";
|
|
2615
3101
|
try {
|
|
2616
3102
|
old = await readFile3(definition.path, "utf8");
|
|
2617
3103
|
} catch (e) {
|
|
2618
3104
|
if (e.code !== "ENOENT") throw e;
|
|
2619
3105
|
}
|
|
2620
|
-
const
|
|
2621
|
-
|
|
3106
|
+
const probe = await probeDaemon(directory);
|
|
3107
|
+
const alive = probe.state === "ready" ? probe.info : null;
|
|
3108
|
+
if (alive && old === definition.content && alive.version === version && alive.protocol === CONTROL_PROTOCOL)
|
|
3109
|
+
return;
|
|
2622
3110
|
await checkServiceSupport();
|
|
2623
|
-
await mkdir8(
|
|
3111
|
+
await mkdir8(dirname8(definition.path), { recursive: true });
|
|
2624
3112
|
if (old !== definition.content) {
|
|
2625
3113
|
const temp = definition.path + `.${process.pid}.tmp`;
|
|
2626
3114
|
await writeFile3(temp, definition.content, { mode: 384 });
|
|
@@ -2634,33 +3122,528 @@ async function ensureDaemon(directory, version) {
|
|
|
2634
3122
|
loaded = true;
|
|
2635
3123
|
} catch {
|
|
2636
3124
|
}
|
|
2637
|
-
if (loaded && (old !== definition.content || alive?.version !== version)) {
|
|
2638
|
-
await exec3("launchctl", ["bootout", target]);
|
|
2639
|
-
loaded = false;
|
|
3125
|
+
if (loaded && (old !== definition.content || alive?.version !== version || alive?.protocol !== CONTROL_PROTOCOL)) {
|
|
3126
|
+
await exec3("launchctl", ["bootout", target]);
|
|
3127
|
+
loaded = false;
|
|
3128
|
+
}
|
|
3129
|
+
if (!loaded) await bootstrapLaunchAgent(definition.path);
|
|
3130
|
+
await exec3("launchctl", ["kickstart", target]);
|
|
3131
|
+
} else {
|
|
3132
|
+
await exec3("systemctl", ["--user", "daemon-reload"]);
|
|
3133
|
+
await exec3("systemctl", ["--user", "enable", definition.id]);
|
|
3134
|
+
await exec3("systemctl", [
|
|
3135
|
+
"--user",
|
|
3136
|
+
old !== definition.content || alive?.version !== version || alive?.protocol !== CONTROL_PROTOCOL ? "restart" : "start",
|
|
3137
|
+
definition.id
|
|
3138
|
+
]);
|
|
3139
|
+
}
|
|
3140
|
+
const deadline = Date.now() + 15e3;
|
|
3141
|
+
while (Date.now() < deadline) {
|
|
3142
|
+
const current = await probeDaemon(directory);
|
|
3143
|
+
if (current.state === "ready" && current.info.version === version && current.info.protocol === CONTROL_PROTOCOL)
|
|
3144
|
+
return;
|
|
3145
|
+
await setTimeout3(150);
|
|
3146
|
+
}
|
|
3147
|
+
throw new Error(
|
|
3148
|
+
"Background service did not start. Run teammy logs and check the user service manager."
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
async function wakeDaemon(directory, version) {
|
|
3152
|
+
await ensureDaemon(directory, version);
|
|
3153
|
+
await daemonRequest(directory, { method: "wake" });
|
|
3154
|
+
}
|
|
3155
|
+
async function stopDaemon(directory, remove = false) {
|
|
3156
|
+
const definition = serviceDefinition(directory, process.execPath, resolve6(process.argv[1]));
|
|
3157
|
+
let loaded = false;
|
|
3158
|
+
let pid;
|
|
3159
|
+
try {
|
|
3160
|
+
const saved = JSON.parse(await readFile3(join15(directory, "runtime", "daemon.json"), "utf8"));
|
|
3161
|
+
if (saved.directory === directory && Number.isSafeInteger(saved.pid) && saved.pid > 0)
|
|
3162
|
+
pid = saved.pid;
|
|
3163
|
+
} catch {
|
|
3164
|
+
}
|
|
3165
|
+
const target = process.platform === "darwin" ? `gui/${process.getuid()}/${definition.id}` : definition.id;
|
|
3166
|
+
try {
|
|
3167
|
+
const output = process.platform === "darwin" ? (await exec3("launchctl", ["print", target])).stdout : (await exec3("systemctl", ["--user", "show", target, "--property=LoadState,MainPID"])).stdout;
|
|
3168
|
+
loaded = process.platform === "darwin" || !output.includes("LoadState=not-found");
|
|
3169
|
+
const match = output.match(process.platform === "darwin" ? /\bpid = (\d+)/ : /MainPID=(\d+)/);
|
|
3170
|
+
if (match && Number(match[1]) > 0) pid = Number(match[1]);
|
|
3171
|
+
} catch {
|
|
3172
|
+
}
|
|
3173
|
+
if (loaded) {
|
|
3174
|
+
try {
|
|
3175
|
+
if (process.platform === "darwin")
|
|
3176
|
+
await exec3("launchctl", ["bootout", target], { timeout: 3e4 });
|
|
3177
|
+
else await exec3("systemctl", ["--user", "stop", target], { timeout: 3e4 });
|
|
3178
|
+
} catch {
|
|
3179
|
+
if (process.platform === "darwin") {
|
|
3180
|
+
await exec3("launchctl", ["kill", "SIGKILL", target]);
|
|
3181
|
+
await exec3("launchctl", ["bootout", target], { timeout: 5e3 });
|
|
3182
|
+
} else {
|
|
3183
|
+
await exec3("systemctl", ["--user", "kill", "--signal=SIGKILL", "--kill-whom=all", target]);
|
|
3184
|
+
await exec3("systemctl", ["--user", "stop", target], { timeout: 5e3 });
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
if (process.platform === "linux") await exec3("systemctl", ["--user", "disable", target]);
|
|
3188
|
+
}
|
|
3189
|
+
const probe = await probeDaemon(directory);
|
|
3190
|
+
if (probe.state === "ready") pid = probe.info.pid;
|
|
3191
|
+
if (pid) {
|
|
3192
|
+
let command = "";
|
|
3193
|
+
try {
|
|
3194
|
+
command = (await exec3("ps", ["-p", String(pid), "-o", "uid=", "-o", "command="])).stdout;
|
|
3195
|
+
} catch {
|
|
3196
|
+
}
|
|
3197
|
+
if (command.trim()) {
|
|
3198
|
+
const owned = Number(command.trim().split(/\s+/)[0]) === process.getuid?.() && command.includes("_daemon") && command.includes(directory);
|
|
3199
|
+
if (!owned) {
|
|
3200
|
+
if (probe.state !== "absent")
|
|
3201
|
+
throw new Error(
|
|
3202
|
+
"Cannot verify background process ownership; no unrelated process was stopped."
|
|
3203
|
+
);
|
|
3204
|
+
command = "";
|
|
3205
|
+
}
|
|
3206
|
+
if (owned) {
|
|
3207
|
+
try {
|
|
3208
|
+
process.kill(pid, "SIGTERM");
|
|
3209
|
+
} catch (e) {
|
|
3210
|
+
if (e.code !== "ESRCH") throw e;
|
|
3211
|
+
}
|
|
3212
|
+
const deadline2 = Date.now() + 3e4;
|
|
3213
|
+
while (Date.now() < deadline2) {
|
|
3214
|
+
try {
|
|
3215
|
+
process.kill(pid, 0);
|
|
3216
|
+
} catch {
|
|
3217
|
+
break;
|
|
3218
|
+
}
|
|
3219
|
+
await setTimeout3(100);
|
|
3220
|
+
}
|
|
3221
|
+
let current = "";
|
|
3222
|
+
try {
|
|
3223
|
+
current = (await exec3("ps", ["-p", String(pid), "-o", "uid=", "-o", "command="])).stdout;
|
|
3224
|
+
} catch {
|
|
3225
|
+
}
|
|
3226
|
+
if (current === command && current.trim()) {
|
|
3227
|
+
try {
|
|
3228
|
+
process.kill(pid, "SIGKILL");
|
|
3229
|
+
} catch (e) {
|
|
3230
|
+
if (e.code !== "ESRCH") throw e;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
const deadline = Date.now() + 5e3;
|
|
3237
|
+
for (; ; ) {
|
|
3238
|
+
try {
|
|
3239
|
+
const lock = acquireRootLock("daemon:" + directory, directory);
|
|
3240
|
+
lock.close();
|
|
3241
|
+
break;
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
if (Date.now() >= deadline) throw error;
|
|
3244
|
+
await setTimeout3(100);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
const remaining = await probeDaemon(directory);
|
|
3248
|
+
if (remaining.state !== "absent")
|
|
3249
|
+
throw new Error("Background service has not stopped. Inspect the user service manager.");
|
|
3250
|
+
await rm(socketPath(directory), { force: true });
|
|
3251
|
+
if (remove || process.platform === "darwin") await rm(definition.path, { force: true });
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// apps/client/runtime/maintenance.ts
|
|
3255
|
+
var services = { stop: stopDaemon, start: ensureDaemon };
|
|
3256
|
+
async function prepareClient(directory, version, options = {}, control = services) {
|
|
3257
|
+
const record = options.stop || options.uninstall ? null : readMaintenance(directory);
|
|
3258
|
+
if (options.daemonStartup && record)
|
|
3259
|
+
throw new Error("Client is paused for maintenance. Run teammy repair.");
|
|
3260
|
+
const schema = options.stop || options.uninstall ? void 0 : stateVersion(directory);
|
|
3261
|
+
const installation = {
|
|
3262
|
+
version,
|
|
3263
|
+
executable: process.execPath,
|
|
3264
|
+
entry: resolve7(process.argv[1]),
|
|
3265
|
+
protocol: CONTROL_PROTOCOL,
|
|
3266
|
+
schema: clientSchemaVersion
|
|
3267
|
+
};
|
|
3268
|
+
let installed;
|
|
3269
|
+
try {
|
|
3270
|
+
installed = JSON.parse(await readFile4(join16(directory, "runtime/installation.json"), "utf8"));
|
|
3271
|
+
} catch {
|
|
3272
|
+
}
|
|
3273
|
+
const probe = options.daemonStartup ? { state: "absent" } : await probeDaemon(directory);
|
|
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]));
|
|
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)));
|
|
3276
|
+
if (!required) return { maintained: false, enabled: 0, stopped: 0, issues: [] };
|
|
3277
|
+
const mutex = acquireRootLock("maintenance", join16(directory, "runtime"));
|
|
3278
|
+
const log = new ClientLog(directory);
|
|
3279
|
+
let access2;
|
|
3280
|
+
let legacy;
|
|
3281
|
+
let state;
|
|
3282
|
+
let enabled = 0, stopped = 0;
|
|
3283
|
+
let restart = false;
|
|
3284
|
+
const issues = [];
|
|
3285
|
+
const pauseReason = options.resume ? void 0 : record?.reason === "paused" || record?.reason === "uninstalled" ? record.reason : record?.pauseReason;
|
|
3286
|
+
try {
|
|
3287
|
+
const reason = options.uninstall ? "uninstalled" : options.stop ? "paused" : options.force ? "repair" : "upgrade";
|
|
3288
|
+
writeMaintenance(directory, {
|
|
3289
|
+
reason,
|
|
3290
|
+
phase: "stopping",
|
|
3291
|
+
version,
|
|
3292
|
+
...pauseReason ? { pauseReason } : {}
|
|
3293
|
+
});
|
|
3294
|
+
log.write("maintenance.started", void 0, `version=${version} reason=${reason}`);
|
|
3295
|
+
if (!options.daemonStartup) await control.stop(directory, Boolean(options.uninstall));
|
|
3296
|
+
if (options.stop || options.uninstall) {
|
|
3297
|
+
if (options.uninstall) issues.push(...(await teardownSessions(directory)).issues);
|
|
3298
|
+
writeMaintenance(directory, { reason, phase: "paused", version });
|
|
3299
|
+
log.write("maintenance.paused");
|
|
3300
|
+
} else {
|
|
3301
|
+
const deadline = Date.now() + 5e3;
|
|
3302
|
+
while (!access2) {
|
|
3303
|
+
try {
|
|
3304
|
+
access2 = stateAccess(directory, true);
|
|
3305
|
+
} catch (error) {
|
|
3306
|
+
if (!(error instanceof Error) || !error.message.includes("Client state is in use") || Date.now() >= deadline)
|
|
3307
|
+
throw error;
|
|
3308
|
+
await delay(100);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
legacy = legacyStateLocks(directory);
|
|
3312
|
+
const before = stateVersion(directory);
|
|
3313
|
+
if (before > clientSchemaVersion)
|
|
3314
|
+
throw new Error(
|
|
3315
|
+
"Database was created by a newer Teammy version. Install a compatible client; teammy repair --stop and logs remain available."
|
|
3316
|
+
);
|
|
3317
|
+
let backup;
|
|
3318
|
+
if (before > 0 && before < clientSchemaVersion) {
|
|
3319
|
+
const backups = join16(directory, "backups");
|
|
3320
|
+
await mkdir9(backups, { recursive: true, mode: 448 });
|
|
3321
|
+
backup = join16(backups, `before-schema-${before}.sqlite3`);
|
|
3322
|
+
if (!existsSync4(backup)) {
|
|
3323
|
+
const temp = backup + ".tmp";
|
|
3324
|
+
await rm2(temp, { force: true });
|
|
3325
|
+
const db = new DatabaseSync5(join16(directory, "client.sqlite3"));
|
|
3326
|
+
try {
|
|
3327
|
+
db.prepare("VACUUM INTO ?").run(temp);
|
|
3328
|
+
} finally {
|
|
3329
|
+
db.close();
|
|
3330
|
+
}
|
|
3331
|
+
const { chmod: chmod3 } = await import("node:fs/promises");
|
|
3332
|
+
await chmod3(temp, 384);
|
|
3333
|
+
await rename4(temp, backup);
|
|
3334
|
+
}
|
|
3335
|
+
log.write("maintenance.backup", void 0, backup);
|
|
3336
|
+
}
|
|
3337
|
+
writeMaintenance(directory, {
|
|
3338
|
+
reason,
|
|
3339
|
+
phase: "migrating",
|
|
3340
|
+
version,
|
|
3341
|
+
...pauseReason ? { pauseReason } : {},
|
|
3342
|
+
...backup ? { backup } : {}
|
|
3343
|
+
});
|
|
3344
|
+
openDatabase(join16(directory, "client.sqlite3"), clientMigrations).close();
|
|
3345
|
+
state = new ClientState(directory, { maintenance: true });
|
|
3346
|
+
const runtime = new RuntimeState(state);
|
|
3347
|
+
if (options.intent) {
|
|
3348
|
+
if (options.intent.enabled) await verifyWorkspace(state, options.intent.root);
|
|
3349
|
+
runtime.enable(options.intent.root, options.intent.enabled);
|
|
3350
|
+
}
|
|
3351
|
+
writeMaintenance(directory, {
|
|
3352
|
+
reason,
|
|
3353
|
+
phase: "configuring",
|
|
3354
|
+
version,
|
|
3355
|
+
...pauseReason ? { pauseReason } : {},
|
|
3356
|
+
...backup ? { backup } : {}
|
|
3357
|
+
});
|
|
3358
|
+
for (const { root } of state.db.prepare("SELECT root FROM connections WHERE binding_json IS NOT NULL").all()) {
|
|
3359
|
+
if (runtime.enabled(String(root))) enabled++;
|
|
3360
|
+
else stopped++;
|
|
3361
|
+
try {
|
|
3362
|
+
issues.push(...(await setupSessions(state, String(root), directory)).issues);
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
issues.push(`${root}: ${diagnostic(error)}`);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
writePrivateJson(join16(directory, "runtime/installation.json"), installation);
|
|
3368
|
+
if (pauseReason)
|
|
3369
|
+
writeMaintenance(directory, { reason: pauseReason, phase: "paused", version });
|
|
3370
|
+
else await rm2(maintenancePath(directory), { force: true });
|
|
3371
|
+
restart = !options.daemonStartup && !pauseReason && enabled > 0;
|
|
3372
|
+
if (backup) {
|
|
3373
|
+
const { readdir: readdir3 } = await import("node:fs/promises");
|
|
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));
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
log.write(
|
|
3380
|
+
"maintenance.completed",
|
|
3381
|
+
void 0,
|
|
3382
|
+
`version=${version} enabled=${enabled} issues=${issues.length}`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
for (const issue of issues) log.write("sessions.setup.failed", void 0, issue);
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
log.write("maintenance.failed", void 0, diagnostic(error));
|
|
3388
|
+
throw error;
|
|
3389
|
+
} finally {
|
|
3390
|
+
state?.close();
|
|
3391
|
+
legacy?.close();
|
|
3392
|
+
access2?.close();
|
|
3393
|
+
mutex.close();
|
|
3394
|
+
}
|
|
3395
|
+
if (restart) await control.start(directory, version);
|
|
3396
|
+
return { maintained: true, enabled, stopped, issues };
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// apps/client/sessions/command.ts
|
|
3400
|
+
var help = `Usage: teammy session COMMAND [options]
|
|
3401
|
+
|
|
3402
|
+
Collect Agent conversations into the conversation library (\u5BF9\u8BDD\u5E93), then browse or search them.
|
|
3403
|
+
|
|
3404
|
+
teardown Remove project hooks; --all removes all registered hooks (keeps discovery)
|
|
3405
|
+
setup Configure project hooks and native discovery (also done by connect)
|
|
3406
|
+
track --agent AGENT --file PATH [--project-dir DIR] [--adopt-existing]
|
|
3407
|
+
untrack --file PATH (does not delete server history)
|
|
3408
|
+
discover --agent AGENT [--directory DIR] [--project-dir DIR]
|
|
3409
|
+
undiscover --directory DIR (also untrack any previously discovered sources to stop them)
|
|
3410
|
+
sources Show registered sources, discovery directories and collection errors
|
|
3411
|
+
hook --agent AGENT [--file PATH] [--project-dir DIR]; accepts JSON on stdin
|
|
3412
|
+
list [--after N] [--limit N]
|
|
3413
|
+
read --owner MEMBER --key KEY [--revision N]
|
|
3414
|
+
search --query TEXT [--after N] [--limit N]
|
|
3415
|
+
|
|
3416
|
+
Shared options: --root DIR (./workspaces), --state-dir DIR (~/.teammy/client).
|
|
3417
|
+
AGENT: claude-code, codex, cursor, pi, dsh. Project defaults to the shared root's parent.
|
|
3418
|
+
track/discover only register local sources. Run teammy sync to collect and publish.
|
|
3419
|
+
Discover uses native default locations except DSH, which requires --directory.
|
|
3420
|
+
Hook only records a local hint, emits no stdout, and always exits successfully.
|
|
3421
|
+
`;
|
|
3422
|
+
async function runSessionCommand(args, version) {
|
|
3423
|
+
const command = args[0];
|
|
3424
|
+
if (!command || ["--help", "-h", "help"].includes(command)) {
|
|
3425
|
+
process.stdout.write(help);
|
|
3426
|
+
return 0;
|
|
3427
|
+
}
|
|
3428
|
+
if (command === "hook") {
|
|
3429
|
+
await runHook(args.slice(1));
|
|
3430
|
+
return 0;
|
|
3431
|
+
}
|
|
3432
|
+
const { values } = parseArgs2({
|
|
3433
|
+
args: args.slice(1),
|
|
3434
|
+
options: {
|
|
3435
|
+
root: { type: "string", default: resolve8("workspaces") },
|
|
3436
|
+
"state-dir": { type: "string", default: join17(homedir4(), ".teammy", "client") },
|
|
3437
|
+
agent: { type: "string" },
|
|
3438
|
+
file: { type: "string" },
|
|
3439
|
+
directory: { type: "string" },
|
|
3440
|
+
"project-dir": { type: "string" },
|
|
3441
|
+
"adopt-existing": { type: "boolean" },
|
|
3442
|
+
owner: { type: "string" },
|
|
3443
|
+
key: { type: "string" },
|
|
3444
|
+
revision: { type: "string" },
|
|
3445
|
+
query: { type: "string" },
|
|
3446
|
+
after: { type: "string", default: "0" },
|
|
3447
|
+
limit: { type: "string", default: "50" },
|
|
3448
|
+
help: { type: "boolean", short: "h" },
|
|
3449
|
+
all: { type: "boolean" }
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
if (values.help) {
|
|
3453
|
+
process.stdout.write(help);
|
|
3454
|
+
return 0;
|
|
3455
|
+
}
|
|
3456
|
+
if (command === "teardown") {
|
|
3457
|
+
const directory2 = await realpath6(resolve8(values["state-dir"]));
|
|
3458
|
+
const root2 = values.all ? void 0 : await realpath6(resolve8(values.root)).catch(() => resolve8(values.root));
|
|
3459
|
+
const report = await teardownSessions(directory2, root2);
|
|
3460
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
3461
|
+
return report.issues.length ? 1 : 0;
|
|
3462
|
+
}
|
|
3463
|
+
const root = await realpath6(resolve8(values.root));
|
|
3464
|
+
const directory = await stateLocation(root, values["state-dir"]);
|
|
3465
|
+
const projectDir = await realpath6(resolve8(values["project-dir"] ?? dirname9(root)));
|
|
3466
|
+
await prepareClient(directory, version);
|
|
3467
|
+
const state = new ClientState(directory);
|
|
3468
|
+
try {
|
|
3469
|
+
const binding = state.binding(root);
|
|
3470
|
+
if (!binding) throw new Error("Connect this root first.");
|
|
3471
|
+
const sessions = new SessionState(state, root);
|
|
3472
|
+
const client = new HubClient(binding.serverUrl, 1e4, binding);
|
|
3473
|
+
let output;
|
|
3474
|
+
if (command === "setup") output = await setupSessions(state, root, directory, { enable: true });
|
|
3475
|
+
else if (command === "track" || command === "untrack") {
|
|
3476
|
+
if (!values.file) throw new Error("--file is required.");
|
|
3477
|
+
const path = resolve8(values.file);
|
|
3478
|
+
if (command === "track")
|
|
3479
|
+
sessions.track(
|
|
3480
|
+
AgentSchema.parse(values.agent),
|
|
3481
|
+
path,
|
|
3482
|
+
projectDir,
|
|
3483
|
+
Boolean(values["adopt-existing"])
|
|
3484
|
+
);
|
|
3485
|
+
else sessions.untrack(path);
|
|
3486
|
+
output = sessions.status();
|
|
3487
|
+
} else if (command === "discover" || command === "undiscover") {
|
|
3488
|
+
if (command === "undiscover") {
|
|
3489
|
+
if (!values.directory) throw new Error("--directory is required.");
|
|
3490
|
+
const source = await realpath6(resolve8(values.directory));
|
|
3491
|
+
setDiscoveryPreference(directory, root, source, false);
|
|
3492
|
+
state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, source);
|
|
3493
|
+
} else {
|
|
3494
|
+
const agent = AgentSchema.parse(values.agent);
|
|
3495
|
+
const defaults = {
|
|
3496
|
+
"claude-code": join17(
|
|
3497
|
+
process.env.CLAUDE_CONFIG_DIR ?? join17(homedir4(), ".claude"),
|
|
3498
|
+
"projects"
|
|
3499
|
+
),
|
|
3500
|
+
codex: join17(process.env.CODEX_HOME ?? join17(homedir4(), ".codex"), "sessions"),
|
|
3501
|
+
cursor: join17(
|
|
3502
|
+
process.env.CURSOR_CONFIG_DIR ?? join17(homedir4(), ".cursor"),
|
|
3503
|
+
"chats",
|
|
3504
|
+
createHash8("md5").update(projectDir).digest("hex")
|
|
3505
|
+
),
|
|
3506
|
+
pi: join17(process.env.PI_CODING_AGENT_DIR ?? join17(homedir4(), ".pi", "agent"), "sessions"),
|
|
3507
|
+
dsh: void 0
|
|
3508
|
+
};
|
|
3509
|
+
const path = values.directory ?? defaults[agent];
|
|
3510
|
+
if (!path) throw new Error("DSH requires its configured persistence --directory.");
|
|
3511
|
+
const source = await realpath6(resolve8(path));
|
|
3512
|
+
setDiscoveryPreference(directory, root, source, true);
|
|
3513
|
+
sessions.discover(agent, source, projectDir);
|
|
3514
|
+
}
|
|
3515
|
+
output = sessions.directories();
|
|
3516
|
+
} else if (command === "sources")
|
|
3517
|
+
output = {
|
|
3518
|
+
sources: sessions.status(),
|
|
3519
|
+
discovery: sessions.directories(),
|
|
3520
|
+
hooks: sessions.hooks()
|
|
3521
|
+
};
|
|
3522
|
+
else {
|
|
3523
|
+
const after = Number(values.after);
|
|
3524
|
+
const limit = Number(values.limit);
|
|
3525
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
3526
|
+
throw new Error("Invalid pagination options.");
|
|
3527
|
+
if (command === "list") output = await client.sessions(after, limit);
|
|
3528
|
+
else if (command === "search") {
|
|
3529
|
+
if (!values.query) throw new Error("--query is required.");
|
|
3530
|
+
output = await client.searchSessions(values.query, after, limit);
|
|
3531
|
+
} else if (command === "read") {
|
|
3532
|
+
if (!values.owner || !values.key) throw new Error("--owner and --key are required.");
|
|
3533
|
+
const revision = values.revision === void 0 ? void 0 : Number(values.revision);
|
|
3534
|
+
if (revision !== void 0 && (!Number.isSafeInteger(revision) || revision < 1))
|
|
3535
|
+
throw new Error("Invalid revision.");
|
|
3536
|
+
const session = await client.session(values.owner, values.key, revision);
|
|
3537
|
+
const snapshot = SessionSnapshotSchema.parse(
|
|
3538
|
+
JSON.parse(Buffer.from(await client.downloadBlob(session.blob.sha256)).toString("utf8"))
|
|
3539
|
+
);
|
|
3540
|
+
output = { session, snapshot };
|
|
3541
|
+
} else throw new Error("Unknown session command. Use teammy session --help.");
|
|
3542
|
+
}
|
|
3543
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
3544
|
+
`);
|
|
3545
|
+
return 0;
|
|
3546
|
+
} finally {
|
|
3547
|
+
state.close();
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// apps/client/sync-command.ts
|
|
3552
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
3553
|
+
import { setTimeout as setTimeout4 } from "node:timers/promises";
|
|
3554
|
+
|
|
3555
|
+
// apps/client/client-command.ts
|
|
3556
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
3557
|
+
import { join as join18, resolve as resolve10 } from "node:path";
|
|
3558
|
+
import { homedir as homedir5 } from "node:os";
|
|
3559
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
3560
|
+
|
|
3561
|
+
// apps/client/connection.ts
|
|
3562
|
+
import { readFile as readFile5, mkdir as mkdir10 } from "node:fs/promises";
|
|
3563
|
+
import { createInterface } from "node:readline/promises";
|
|
3564
|
+
async function connectProject(state, root, directory, options) {
|
|
3565
|
+
const lock = acquireRootLock(root, directory);
|
|
3566
|
+
try {
|
|
3567
|
+
state.assertRootAvailable(root);
|
|
3568
|
+
const transport = new HubClient(options.url);
|
|
3569
|
+
const existing = state.binding(root);
|
|
3570
|
+
const project = new ClientProjectState(state, root);
|
|
3571
|
+
if (existing && existing.serverUrl !== transport.url)
|
|
3572
|
+
throw new Error("This root is connected to another server.");
|
|
3573
|
+
if (existing && options.project && options.project !== existing.workspaceId)
|
|
3574
|
+
throw new Error("This root belongs to a different project. Use a different local root.");
|
|
3575
|
+
const info = await transport.serverInfo();
|
|
3576
|
+
if (!info.capabilities.includes("atomic-file-publications"))
|
|
3577
|
+
throw new Error(
|
|
3578
|
+
"Update the server before connecting: complete directory publications are required."
|
|
3579
|
+
);
|
|
3580
|
+
if (!info.capabilities.includes("multi-project"))
|
|
3581
|
+
throw new Error(
|
|
3582
|
+
"This client requires a multi-project server. Update the server before connecting."
|
|
3583
|
+
);
|
|
3584
|
+
let member = options.member;
|
|
3585
|
+
if (existing && !options.reconnect && !options.tokenFile) {
|
|
3586
|
+
if (member && member !== existing.memberId)
|
|
3587
|
+
throw new Error("This root belongs to a different member.");
|
|
3588
|
+
const client2 = new HubClient(existing.serverUrl, 1e4, existing);
|
|
3589
|
+
await client2.members();
|
|
3590
|
+
} else if (options.tokenFile) {
|
|
3591
|
+
const token = CredentialSchema.parse((await readFile5(options.tokenFile, "utf8")).trim());
|
|
3592
|
+
const deviceId = state.deviceFor(root);
|
|
3593
|
+
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)
|
|
3596
|
+
throw new Error("Token belongs to a different project.");
|
|
3597
|
+
state.saveBinding(root, client2.url, token, binding2);
|
|
3598
|
+
} else {
|
|
3599
|
+
member ??= existing?.memberId;
|
|
3600
|
+
if (!member && process.stdin.isTTY) {
|
|
3601
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3602
|
+
try {
|
|
3603
|
+
member = (await rl.question("Member ID: ")).trim();
|
|
3604
|
+
} finally {
|
|
3605
|
+
rl.close();
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
if (!member) throw new Error("--member is required when input is not interactive.");
|
|
3609
|
+
NameSchema.parse(member);
|
|
3610
|
+
if (existing && existing.memberId !== member)
|
|
3611
|
+
throw new Error("This root belongs to a different member.");
|
|
3612
|
+
const projectId = options.project ?? existing?.workspaceId;
|
|
3613
|
+
if (!projectId)
|
|
3614
|
+
throw new Error(
|
|
3615
|
+
"--project is required for a new connection; copy the command from the project settings."
|
|
3616
|
+
);
|
|
3617
|
+
NameSchema.parse(projectId);
|
|
3618
|
+
const request = project.enrollment(
|
|
3619
|
+
transport.url,
|
|
3620
|
+
member,
|
|
3621
|
+
projectId,
|
|
3622
|
+
Boolean(options.reconnect)
|
|
3623
|
+
);
|
|
3624
|
+
const binding2 = await transport.enroll(request);
|
|
3625
|
+
state.saveBinding(root, transport.url, request.credential, binding2);
|
|
3626
|
+
project.completeEnrollment();
|
|
3627
|
+
}
|
|
3628
|
+
const binding = state.binding(root);
|
|
3629
|
+
const client = new HubClient(binding.serverUrl, 1e4, binding);
|
|
3630
|
+
if (info.capabilities.includes("project-config")) {
|
|
3631
|
+
try {
|
|
3632
|
+
await applyProjectFile(project, await client.projectConfig());
|
|
3633
|
+
} catch (error) {
|
|
3634
|
+
if (!(options.tokenFile && error instanceof Error && "code" in error && ["SETUP_REQUIRED", "PROJECT_NOT_FOUND"].includes(String(error.code))))
|
|
3635
|
+
throw error;
|
|
3636
|
+
}
|
|
2640
3637
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
await
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
definition.id
|
|
2650
|
-
]);
|
|
2651
|
-
}
|
|
2652
|
-
const deadline = Date.now() + 15e3;
|
|
2653
|
-
while (Date.now() < deadline) {
|
|
2654
|
-
if ((await runningDaemon(directory))?.version === version) return;
|
|
2655
|
-
await setTimeout3(150);
|
|
3638
|
+
const own = await metadata(root, binding.memberId);
|
|
3639
|
+
if (own && !own.directory) throw new Error("Member path is not a directory.");
|
|
3640
|
+
if (!own) await mkdir10(await safePath(root, binding.memberId));
|
|
3641
|
+
await ensureLocalConfig(root);
|
|
3642
|
+
const { token: _, ...visible } = binding;
|
|
3643
|
+
return { ...visible, configRevision: project.cached()?.config.configRevision ?? null };
|
|
3644
|
+
} finally {
|
|
3645
|
+
lock.close();
|
|
2656
3646
|
}
|
|
2657
|
-
throw new Error(
|
|
2658
|
-
"Background service did not start. Run teammy logs and check the user service manager."
|
|
2659
|
-
);
|
|
2660
|
-
}
|
|
2661
|
-
async function wakeDaemon(directory, version) {
|
|
2662
|
-
await ensureDaemon(directory, version);
|
|
2663
|
-
await daemonRequest(directory, { method: "wake" });
|
|
2664
3647
|
}
|
|
2665
3648
|
|
|
2666
3649
|
// apps/client/output.ts
|
|
@@ -2680,15 +3663,94 @@ function json(value) {
|
|
|
2680
3663
|
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
2681
3664
|
}
|
|
2682
3665
|
|
|
3666
|
+
// apps/client/runtime/diagnostics.ts
|
|
3667
|
+
import { resolve as resolve9 } from "node:path";
|
|
3668
|
+
import { realpath as realpath7 } from "node:fs/promises";
|
|
3669
|
+
async function runDiagnostics(command, options, version) {
|
|
3670
|
+
const directory = await realpath7(options.directory).catch(() => resolve9(options.directory));
|
|
3671
|
+
const root = await realpath7(options.root).catch(() => resolve9(options.root));
|
|
3672
|
+
const log = new ClientLog(directory);
|
|
3673
|
+
if (command === "logs") {
|
|
3674
|
+
const entries = log.tail(options.all ? void 0 : root);
|
|
3675
|
+
if (options.json) json(entries);
|
|
3676
|
+
else if (!entries.length) result("\u6CA1\u6709\u8BCA\u65AD\u8BB0\u5F55");
|
|
3677
|
+
else
|
|
3678
|
+
for (const entry of entries)
|
|
3679
|
+
process.stdout.write(
|
|
3680
|
+
`${entry.at} ${entry.event}${entry.detail ? " " + entry.detail : ""}
|
|
3681
|
+
`
|
|
3682
|
+
);
|
|
3683
|
+
return 0;
|
|
3684
|
+
}
|
|
3685
|
+
const probe = await probeDaemon(directory);
|
|
3686
|
+
let state;
|
|
3687
|
+
let issue = probe.state === "unresponsive" || probe.state === "incompatible" ? probe.error : null;
|
|
3688
|
+
let current = null;
|
|
3689
|
+
let binding = null;
|
|
3690
|
+
let control = null;
|
|
3691
|
+
let maintenance = null;
|
|
3692
|
+
try {
|
|
3693
|
+
maintenance = readMaintenance(directory);
|
|
3694
|
+
} catch (e) {
|
|
3695
|
+
issue ??= diagnostic(e);
|
|
3696
|
+
}
|
|
3697
|
+
try {
|
|
3698
|
+
state = new ClientState(directory, { readOnly: true });
|
|
3699
|
+
const stored = state.binding(root);
|
|
3700
|
+
if (stored) {
|
|
3701
|
+
const { token: _, ...safe } = stored;
|
|
3702
|
+
binding = safe;
|
|
3703
|
+
const runtime = new RuntimeState(state);
|
|
3704
|
+
current = { enabled: runtime.enabled(root), running: false, status: runtime.status(root) };
|
|
3705
|
+
control = readControlStatus(state, root);
|
|
3706
|
+
if (probe.state === "ready") {
|
|
3707
|
+
try {
|
|
3708
|
+
current = await daemonRequest(directory, { method: "status", root }, 1e3);
|
|
3709
|
+
} catch (e) {
|
|
3710
|
+
issue ??= diagnostic(e);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
} catch (e) {
|
|
3715
|
+
issue ??= diagnostic(e);
|
|
3716
|
+
} finally {
|
|
3717
|
+
state?.close();
|
|
3718
|
+
}
|
|
3719
|
+
if (options.json)
|
|
3720
|
+
json({
|
|
3721
|
+
...current,
|
|
3722
|
+
binding,
|
|
3723
|
+
control,
|
|
3724
|
+
daemon: probe.state === "ready" ? probe.info : null,
|
|
3725
|
+
daemonState: probe.state,
|
|
3726
|
+
installedVersion: version,
|
|
3727
|
+
maintenance,
|
|
3728
|
+
issue,
|
|
3729
|
+
logPath: log.path
|
|
3730
|
+
});
|
|
3731
|
+
else {
|
|
3732
|
+
const title = maintenance ? "\u540E\u53F0\u540C\u6B65\u5DF2\u6682\u505C" : issue ? "\u540C\u6B65\u9700\u8981\u5904\u7406" : !binding ? "\u5F53\u524D\u76EE\u5F55\u5C1A\u672A\u8FDE\u63A5" : !current?.enabled ? "\u540C\u6B65\u5DF2\u505C\u6B62" : !current.running ? "\u540E\u53F0\u540C\u6B65\u672A\u8FD0\u884C" : current.status?.issue ? "\u540C\u6B65\u7B49\u5F85\u5904\u7406" : "\u540C\u6B65\u5DF2\u5F00\u542F";
|
|
3733
|
+
result(
|
|
3734
|
+
title,
|
|
3735
|
+
`\u5B89\u88C5 ${version} \xB7 \u8FD0\u884C ${probe.state === "ready" ? probe.info.version : "\u672A\u5C31\u7EEA"}`
|
|
3736
|
+
);
|
|
3737
|
+
if (issue || current?.status?.issue) result("\u5F85\u5904\u7406", issue ?? current.status.issue);
|
|
3738
|
+
if (current?.status?.lastSuccessAt)
|
|
3739
|
+
process.stdout.write(` \u6700\u8FD1\u6210\u529F\uFF1A${current.status.lastSuccessAt}
|
|
3740
|
+
`);
|
|
3741
|
+
}
|
|
3742
|
+
return issue ? 1 : 0;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
2683
3745
|
// apps/client/client-command.ts
|
|
2684
3746
|
var locationOptions = {
|
|
2685
|
-
root: { type: "string", default:
|
|
2686
|
-
"state-dir": { type: "string", default:
|
|
3747
|
+
root: { type: "string", default: resolve10("workspaces") },
|
|
3748
|
+
"state-dir": { type: "string", default: join18(homedir5(), ".teammy", "client") },
|
|
2687
3749
|
json: { type: "boolean" },
|
|
2688
3750
|
help: { type: "boolean", short: "h" }
|
|
2689
3751
|
};
|
|
2690
3752
|
async function runClientCommand(command, args, version) {
|
|
2691
|
-
const { values } =
|
|
3753
|
+
const { values } = parseArgs3({
|
|
2692
3754
|
args,
|
|
2693
3755
|
options: {
|
|
2694
3756
|
...locationOptions,
|
|
@@ -2708,54 +3770,16 @@ async function runClientCommand(command, args, version) {
|
|
|
2708
3770
|
);
|
|
2709
3771
|
return 0;
|
|
2710
3772
|
}
|
|
3773
|
+
if (command === "logs" || command === "status")
|
|
3774
|
+
return runDiagnostics(
|
|
3775
|
+
command,
|
|
3776
|
+
{ root: values.root, directory: values["state-dir"], json: values.json, all: values.all },
|
|
3777
|
+
version
|
|
3778
|
+
);
|
|
2711
3779
|
const root = await workspacePath(values.root), directory = await stateLocation(root, values["state-dir"]);
|
|
3780
|
+
await prepareClient(directory, version, { resume: !values["no-start"] });
|
|
2712
3781
|
const state = new ClientState(directory), log = new ClientLog(directory), runtime = new RuntimeState(state);
|
|
2713
3782
|
try {
|
|
2714
|
-
if (command === "logs") {
|
|
2715
|
-
const entries = log.tail(values.all ? void 0 : root);
|
|
2716
|
-
if (values.json) json(entries);
|
|
2717
|
-
else if (!entries.length) result("\u6CA1\u6709\u8BCA\u65AD\u8BB0\u5F55");
|
|
2718
|
-
else
|
|
2719
|
-
for (const item of entries)
|
|
2720
|
-
process.stdout.write(
|
|
2721
|
-
`${item.at} ${item.event}${item.detail ? " " + item.detail : ""}
|
|
2722
|
-
`
|
|
2723
|
-
);
|
|
2724
|
-
return 0;
|
|
2725
|
-
}
|
|
2726
|
-
if (command === "status") {
|
|
2727
|
-
const binding = state.binding(root);
|
|
2728
|
-
if (!binding) throw new Error("This workspace is not connected. Run teammy connect first.");
|
|
2729
|
-
const daemon = await runningDaemon(directory);
|
|
2730
|
-
const current = daemon ? await daemonRequest(directory, { method: "status", root }) : { enabled: runtime.enabled(root), running: false, status: runtime.status(root) };
|
|
2731
|
-
const { token: _, ...publicBinding } = binding;
|
|
2732
|
-
if (values.json)
|
|
2733
|
-
json({
|
|
2734
|
-
...current,
|
|
2735
|
-
binding: publicBinding,
|
|
2736
|
-
control: readControlStatus(state, root),
|
|
2737
|
-
daemon,
|
|
2738
|
-
logPath: log.path
|
|
2739
|
-
});
|
|
2740
|
-
else {
|
|
2741
|
-
const status = current.status;
|
|
2742
|
-
const title = !current.enabled ? "\u540C\u6B65\u5DF2\u505C\u6B62" : !current.running ? "\u540E\u53F0\u540C\u6B65\u672A\u8FD0\u884C" : status?.issue ? "\u540C\u6B65\u7B49\u5F85\u5904\u7406" : status?.pendingDownloads || !status?.lastSuccessAt ? "\u9996\u6B21\u540C\u6B65\u4E2D" : "\u540C\u6B65\u5DF2\u5F00\u542F";
|
|
2743
|
-
result(
|
|
2744
|
-
title,
|
|
2745
|
-
new ClientProjectState(state, root).cached()?.config.projectName ?? binding.workspaceId
|
|
2746
|
-
);
|
|
2747
|
-
if (status?.issue) result("\u5F85\u5904\u7406", status.issue);
|
|
2748
|
-
else if (status?.lastSuccessAt)
|
|
2749
|
-
process.stdout.write(` \u6700\u8FD1\u6210\u529F\uFF1A${status.lastSuccessAt}
|
|
2750
|
-
`);
|
|
2751
|
-
if (status?.pendingFiles || status?.pendingDownloads)
|
|
2752
|
-
process.stdout.write(
|
|
2753
|
-
` \u5F85\u4E0A\u4F20 ${status.pendingFiles} \xB7 \u5F85\u4E0B\u8F7D ${status.pendingDownloads}
|
|
2754
|
-
`
|
|
2755
|
-
);
|
|
2756
|
-
}
|
|
2757
|
-
return 0;
|
|
2758
|
-
}
|
|
2759
3783
|
const existing = state.binding(root);
|
|
2760
3784
|
const url = values.url ?? (values.reconnect ? existing?.serverUrl : void 0);
|
|
2761
3785
|
if (!url) throw new Error("--url is required.");
|
|
@@ -2786,7 +3810,7 @@ async function runClientCommand(command, args, version) {
|
|
|
2786
3810
|
tokenFile: values["token-file"],
|
|
2787
3811
|
reconnect: values.reconnect
|
|
2788
3812
|
});
|
|
2789
|
-
const collection = await setupSessions(state, root, directory);
|
|
3813
|
+
const collection = await setupSessions(state, root, directory, { enable: true });
|
|
2790
3814
|
for (const issue of collection.issues) log.write("sessions.setup.failed", root, issue);
|
|
2791
3815
|
completePreparation(state, root);
|
|
2792
3816
|
runtime.enable(root, !values["no-start"]);
|
|
@@ -2795,7 +3819,7 @@ async function runClientCommand(command, args, version) {
|
|
|
2795
3819
|
if (values.json) json({ ...connected, collection, syncEnabled: !values["no-start"] });
|
|
2796
3820
|
else result(values["no-start"] ? "\u9879\u76EE\u5DF2\u8FDE\u63A5" : "\u5DF2\u8FDE\u63A5\uFF0C\u540E\u53F0\u540C\u6B65\u5DF2\u542F\u52A8", root);
|
|
2797
3821
|
if (collection.issues.length && !values.json)
|
|
2798
|
-
result("\u90E8\u5206\
|
|
3822
|
+
result("\u90E8\u5206\u5BF9\u8BDD\u91C7\u96C6 Hook \u672A\u914D\u7F6E", "\u540E\u53F0\u53D1\u73B0\u4ECD\u53EF\u7528\uFF0C\u8BE6\u60C5\u89C1 teammy logs");
|
|
2799
3823
|
return 0;
|
|
2800
3824
|
} catch (e) {
|
|
2801
3825
|
log.write(`${command}.failed`, root, diagnostic(e));
|
|
@@ -2806,16 +3830,16 @@ async function runClientCommand(command, args, version) {
|
|
|
2806
3830
|
}
|
|
2807
3831
|
|
|
2808
3832
|
// apps/client/files/synchronizer.ts
|
|
2809
|
-
import { randomUUID as
|
|
3833
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2810
3834
|
import { watch } from "node:fs";
|
|
2811
|
-
import { lstat as lstat4, mkdir as
|
|
2812
|
-
import { join as
|
|
3835
|
+
import { lstat as lstat4, mkdir as mkdir12 } from "node:fs/promises";
|
|
3836
|
+
import { join as join20, sep as sep5 } from "node:path";
|
|
2813
3837
|
import { performance } from "node:perf_hooks";
|
|
2814
3838
|
|
|
2815
3839
|
// packages/storage/local-blob-store.ts
|
|
2816
|
-
import { createHash as createHash9, randomUUID as
|
|
2817
|
-
import { link, mkdir as
|
|
2818
|
-
import { dirname as
|
|
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";
|
|
2819
3843
|
function hash(content) {
|
|
2820
3844
|
return createHash9("sha256").update(content).digest("hex");
|
|
2821
3845
|
}
|
|
@@ -2833,14 +3857,14 @@ var BlobIntegrityError = class extends Error {
|
|
|
2833
3857
|
var LocalBlobStore = class {
|
|
2834
3858
|
root;
|
|
2835
3859
|
constructor(root) {
|
|
2836
|
-
this.root =
|
|
3860
|
+
this.root = resolve11(root);
|
|
2837
3861
|
}
|
|
2838
3862
|
async put(content) {
|
|
2839
3863
|
const bytes = Buffer.from(content);
|
|
2840
3864
|
const sha256 = hash(bytes);
|
|
2841
3865
|
const target = this.pathFor(sha256);
|
|
2842
|
-
await
|
|
2843
|
-
const temporary = `${target}.${
|
|
3866
|
+
await mkdir11(dirname10(target), { recursive: true, mode: 448 });
|
|
3867
|
+
const temporary = `${target}.${randomUUID6()}.tmp`;
|
|
2844
3868
|
const file = await open4(temporary, "wx", 384);
|
|
2845
3869
|
try {
|
|
2846
3870
|
try {
|
|
@@ -2856,7 +3880,7 @@ var LocalBlobStore = class {
|
|
|
2856
3880
|
await this.get(sha256);
|
|
2857
3881
|
}
|
|
2858
3882
|
if (process.platform !== "win32") {
|
|
2859
|
-
let directory =
|
|
3883
|
+
let directory = dirname10(target);
|
|
2860
3884
|
for (; ; ) {
|
|
2861
3885
|
const handle = await open4(directory, "r");
|
|
2862
3886
|
try {
|
|
@@ -2864,8 +3888,8 @@ var LocalBlobStore = class {
|
|
|
2864
3888
|
} finally {
|
|
2865
3889
|
await handle.close();
|
|
2866
3890
|
}
|
|
2867
|
-
if (directory ===
|
|
2868
|
-
directory =
|
|
3891
|
+
if (directory === dirname10(this.root)) break;
|
|
3892
|
+
directory = dirname10(directory);
|
|
2869
3893
|
}
|
|
2870
3894
|
}
|
|
2871
3895
|
} finally {
|
|
@@ -2877,7 +3901,7 @@ var LocalBlobStore = class {
|
|
|
2877
3901
|
const target = this.pathFor(sha256);
|
|
2878
3902
|
let content;
|
|
2879
3903
|
try {
|
|
2880
|
-
content = await
|
|
3904
|
+
content = await readFile6(target);
|
|
2881
3905
|
} catch (error) {
|
|
2882
3906
|
if (hasCode(error, "ENOENT")) return void 0;
|
|
2883
3907
|
throw error;
|
|
@@ -2887,7 +3911,7 @@ var LocalBlobStore = class {
|
|
|
2887
3911
|
}
|
|
2888
3912
|
pathFor(sha256) {
|
|
2889
3913
|
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
2890
|
-
return
|
|
3914
|
+
return join19(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
2891
3915
|
}
|
|
2892
3916
|
};
|
|
2893
3917
|
|
|
@@ -2905,15 +3929,18 @@ var PublicationQueue = class {
|
|
|
2905
3929
|
const pending = this.state.pending(root);
|
|
2906
3930
|
if (!pending || pending.retryAt > now) return false;
|
|
2907
3931
|
try {
|
|
3932
|
+
let end = pending.nextIndex, bytes = 0;
|
|
3933
|
+
while (end < pending.batch.changes.length && end - pending.nextIndex < MAX_BATCH_FILES && bytes < 20 * 1024 * 1024) {
|
|
3934
|
+
const change = pending.batch.changes[end++];
|
|
3935
|
+
if (change.kind === "upsert") bytes += change.size;
|
|
3936
|
+
}
|
|
2908
3937
|
const hashes = [
|
|
2909
3938
|
...new Set(
|
|
2910
|
-
pending.batch.changes.flatMap(
|
|
2911
|
-
(change) => change.kind === "upsert" ? [change.sha256] : []
|
|
2912
|
-
)
|
|
3939
|
+
pending.batch.changes.slice(pending.nextIndex, end).flatMap((change) => change.kind === "upsert" ? [change.sha256] : [])
|
|
2913
3940
|
)
|
|
2914
3941
|
];
|
|
2915
|
-
const { missing:
|
|
2916
|
-
for (const hash2 of
|
|
3942
|
+
const { missing: missing3 } = hashes.length ? await this.transport.missingBlobs(hashes) : { missing: [] };
|
|
3943
|
+
for (const hash2 of missing3) {
|
|
2917
3944
|
if (!hashes.includes(hash2)) throw new Error("Server requested unrelated content.");
|
|
2918
3945
|
const content = await this.blobs.get(hash2);
|
|
2919
3946
|
if (!content) throw new Error("A queued content snapshot is missing from local storage.");
|
|
@@ -2921,9 +3948,12 @@ var PublicationQueue = class {
|
|
|
2921
3948
|
if (result2.sha256 !== hash2 || result2.size !== content.byteLength)
|
|
2922
3949
|
throw new Error("Server acknowledged different content.");
|
|
2923
3950
|
}
|
|
3951
|
+
this.state.uploaded(root, pending.batch.operationId, end);
|
|
3952
|
+
if (end < pending.batch.changes.length) return false;
|
|
2924
3953
|
this.state.acknowledge(root, await this.transport.commitFiles(pending.batch));
|
|
2925
3954
|
return true;
|
|
2926
3955
|
} catch (error) {
|
|
3956
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
2927
3957
|
const blocked = error instanceof HubRequestError && error.status >= 400 && error.status < 500 && ![408, 429].includes(error.status);
|
|
2928
3958
|
this.state.postpone(
|
|
2929
3959
|
root,
|
|
@@ -2947,17 +3977,17 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
2947
3977
|
if (!binding) throw new Error("Connect this root before synchronizing.");
|
|
2948
3978
|
this.binding = binding;
|
|
2949
3979
|
this.client = new HubClient(binding.serverUrl, 3e4, binding, this.abort.signal);
|
|
2950
|
-
this.blobs = new LocalBlobStore(
|
|
3980
|
+
this.blobs = new LocalBlobStore(join20(stateDirectory, "blobs"));
|
|
2951
3981
|
this.projectScope = options.scope === "project";
|
|
2952
3982
|
this.owner = this.projectScope ? PROJECT_FILE_OWNER : binding.memberId;
|
|
2953
3983
|
this.publication = new FilePublicationState(state.db, options.scope);
|
|
2954
3984
|
this.queue = new PublicationQueue(this.publication, this.blobs, {
|
|
2955
3985
|
missingBlobs: (hashes) => this.client.missingBlobs(hashes),
|
|
2956
3986
|
uploadBlob: (hash2, bytes) => this.client.uploadBlob(hash2, bytes),
|
|
2957
|
-
commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch,
|
|
3987
|
+
commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch, batch.configRevision) : this.client.commitFiles(batch)
|
|
2958
3988
|
});
|
|
2959
3989
|
this.index = new SyncState(state, root, options.scope);
|
|
2960
|
-
this.ownRoot = this.projectScope ? root :
|
|
3990
|
+
this.ownRoot = this.projectScope ? root : join20(root, binding.memberId);
|
|
2961
3991
|
this.common = this.projectScope ? void 0 : new _FileSynchronizer(state, root, stateDirectory, { ...options, scope: "project" });
|
|
2962
3992
|
const started = performance.now();
|
|
2963
3993
|
const epoch = Date.now();
|
|
@@ -2987,14 +4017,16 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
2987
4017
|
watchIssues = [];
|
|
2988
4018
|
lastScan = -Infinity;
|
|
2989
4019
|
busy = false;
|
|
4020
|
+
capable = false;
|
|
2990
4021
|
stopped = false;
|
|
2991
4022
|
abort = new AbortController();
|
|
2992
4023
|
async checkCapabilities() {
|
|
2993
4024
|
const info = await this.client.serverInfo();
|
|
2994
|
-
if (!["identity", "blobs", "file-revisions"].every(
|
|
4025
|
+
if (!["identity", "blobs", "file-revisions", "atomic-file-publications"].every(
|
|
2995
4026
|
(capability) => info.capabilities.includes(capability)
|
|
2996
4027
|
))
|
|
2997
|
-
throw new Error("
|
|
4028
|
+
throw new Error("Upgrade the server to support complete directory publications.");
|
|
4029
|
+
this.capable = true;
|
|
2998
4030
|
}
|
|
2999
4031
|
setSettleMs(value) {
|
|
3000
4032
|
if (!Number.isSafeInteger(value) || value < 0 || value > 864e5)
|
|
@@ -3022,6 +4054,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3022
4054
|
};
|
|
3023
4055
|
try {
|
|
3024
4056
|
try {
|
|
4057
|
+
if (!this.capable) await this.checkCapabilities();
|
|
3025
4058
|
if (this.projectScope) {
|
|
3026
4059
|
try {
|
|
3027
4060
|
this.sharedConfig = await this.client.sharedFilesConfig();
|
|
@@ -3081,11 +4114,10 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3081
4114
|
}
|
|
3082
4115
|
const blocked = await this.refreshDirty(report.issues);
|
|
3083
4116
|
await this.audit(report.issues);
|
|
3084
|
-
report.published = await this.queue.flush(this.root, this.clock());
|
|
3085
4117
|
if (!this.publication.pending(this.root) && !this.stopped) {
|
|
3086
4118
|
await this.stage(blocked, report.issues, settleMs);
|
|
3087
|
-
report.published = await this.queue.flush(this.root, this.clock()) || report.published;
|
|
3088
4119
|
}
|
|
4120
|
+
report.published = await this.queue.flush(this.root, this.clock());
|
|
3089
4121
|
} catch (error) {
|
|
3090
4122
|
report.issues.push(`publish: ${message(error)}`);
|
|
3091
4123
|
}
|
|
@@ -3129,7 +4161,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3129
4161
|
throw new Error("Manifest changed version during pagination.");
|
|
3130
4162
|
revision = page.revision;
|
|
3131
4163
|
files.push(...page.files);
|
|
3132
|
-
if (page.nextCursor === null) return { revision, files };
|
|
4164
|
+
if (page.nextCursor === null) return { revision, complete: page.complete, files };
|
|
3133
4165
|
if (page.nextCursor === after || files.length > 1e5)
|
|
3134
4166
|
throw new Error("Manifest pagination did not make bounded progress.");
|
|
3135
4167
|
after = page.nextCursor;
|
|
@@ -3156,10 +4188,12 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3156
4188
|
}
|
|
3157
4189
|
}
|
|
3158
4190
|
const ownerPath = this.projectScope ? this.root : await safePath(this.root, this.owner);
|
|
3159
|
-
await
|
|
4191
|
+
await mkdir12(ownerPath).catch((error) => {
|
|
3160
4192
|
if (error.code !== "EEXIST") throw error;
|
|
3161
4193
|
});
|
|
3162
|
-
this.publication.initializePublication(this.root, manifest.revision, manifest.files
|
|
4194
|
+
this.publication.initializePublication(this.root, manifest.revision, manifest.files, {
|
|
4195
|
+
complete: manifest.complete
|
|
4196
|
+
});
|
|
3163
4197
|
}
|
|
3164
4198
|
listen() {
|
|
3165
4199
|
if (this.options.watch === false || this.watcher || this.stopped) return;
|
|
@@ -3169,7 +4203,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3169
4203
|
this.lastScan = -Infinity;
|
|
3170
4204
|
return;
|
|
3171
4205
|
}
|
|
3172
|
-
const path = filename.toString().split(
|
|
4206
|
+
const path = filename.toString().split(sep5).join("/");
|
|
3173
4207
|
if (!this.excludes(path)) this.index.event(path, this.clock());
|
|
3174
4208
|
});
|
|
3175
4209
|
this.watcher.on("error", (error) => {
|
|
@@ -3193,6 +4227,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3193
4227
|
}
|
|
3194
4228
|
}
|
|
3195
4229
|
issues.push(...scan.issues);
|
|
4230
|
+
return scan.complete;
|
|
3196
4231
|
}
|
|
3197
4232
|
async refreshDirty(issues) {
|
|
3198
4233
|
const blocked = /* @__PURE__ */ new Set();
|
|
@@ -3233,73 +4268,80 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3233
4268
|
}
|
|
3234
4269
|
}
|
|
3235
4270
|
async stage(blocked, issues, settleMs) {
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
if (!
|
|
4271
|
+
const publication = this.publication.publication(this.root);
|
|
4272
|
+
if (publication.complete && !this.index.index(true).some((row) => !this.excludes(row.path)))
|
|
4273
|
+
return;
|
|
4274
|
+
if (!await this.reconcile("", issues)) return;
|
|
4275
|
+
const before = this.index.index().filter((row) => !this.excludes(row.path));
|
|
4276
|
+
const ready = before.filter((row) => row.dirty);
|
|
4277
|
+
if (ready.some(
|
|
4278
|
+
(row) => blocked.has(row.path) || row.eventPending || this.clock() - row.observedAt < settleMs
|
|
4279
|
+
))
|
|
4280
|
+
return;
|
|
3240
4281
|
const baseline = this.publication.ownerFiles(this.root);
|
|
3241
4282
|
const planned = new Map(baseline);
|
|
3242
4283
|
const changes = [];
|
|
3243
4284
|
const snapshots = [];
|
|
3244
|
-
let bytes = 0;
|
|
3245
4285
|
ready.sort((a, b) => Number(a.signature !== null) - Number(b.signature !== null));
|
|
3246
4286
|
for (const row of ready) {
|
|
3247
|
-
if (this.stopped
|
|
4287
|
+
if (this.stopped) return;
|
|
3248
4288
|
try {
|
|
3249
4289
|
if (row.signature === null) {
|
|
3250
|
-
const current = await metadata(this.ownRoot, row.path);
|
|
3251
|
-
if (current && !current.directory) {
|
|
3252
|
-
this.index.event(row.path, this.clock());
|
|
3253
|
-
continue;
|
|
3254
|
-
}
|
|
3255
4290
|
if (baseline.has(row.path) && row.seenLocal) {
|
|
3256
4291
|
changes.push({ kind: "delete", path: row.path });
|
|
3257
4292
|
planned.delete(row.path);
|
|
3258
|
-
} else {
|
|
3259
|
-
this.index.clean(row.path, row.generation, null, this.clock());
|
|
3260
|
-
continue;
|
|
3261
4293
|
}
|
|
3262
4294
|
snapshots.push({ path: row.path, generation: row.generation, hash: null });
|
|
3263
4295
|
continue;
|
|
3264
4296
|
}
|
|
3265
|
-
const key = row.path.toLowerCase();
|
|
3266
|
-
const collision = [...planned.keys()].some(
|
|
3267
|
-
(path) => path !== row.path && (path.toLowerCase() === key || path.toLowerCase().startsWith(`${key}/`) || key.startsWith(`${path.toLowerCase()}/`))
|
|
3268
|
-
);
|
|
3269
|
-
if (collision) {
|
|
3270
|
-
issues.push(
|
|
3271
|
-
`${row.path}: conflicts with a preserved server path; rename the local file to publish it.`
|
|
3272
|
-
);
|
|
3273
|
-
continue;
|
|
3274
|
-
}
|
|
3275
4297
|
const local = await readStable(this.ownRoot, row.path);
|
|
3276
|
-
bytes += local.bytes.length;
|
|
3277
4298
|
if (local.signature !== row.signature || this.index.get(row.path)?.generation !== row.generation) {
|
|
3278
4299
|
this.index.event(row.path, this.clock());
|
|
3279
|
-
|
|
4300
|
+
return;
|
|
3280
4301
|
}
|
|
3281
|
-
if (baseline.get(row.path)?.sha256
|
|
3282
|
-
this.
|
|
3283
|
-
|
|
4302
|
+
if (baseline.get(row.path)?.sha256 !== local.sha256) {
|
|
4303
|
+
const ref = await this.blobs.put(local.bytes);
|
|
4304
|
+
changes.push({ kind: "upsert", path: row.path, ...ref });
|
|
4305
|
+
planned.set(row.path, ref);
|
|
3284
4306
|
}
|
|
3285
|
-
const ref = await this.blobs.put(local.bytes);
|
|
3286
|
-
changes.push({ kind: "upsert", path: row.path, ...ref });
|
|
3287
|
-
planned.set(row.path, ref);
|
|
3288
4307
|
snapshots.push({ path: row.path, generation: row.generation, hash: local.sha256 });
|
|
3289
4308
|
} catch (error) {
|
|
3290
4309
|
this.index.event(row.path, this.clock());
|
|
3291
4310
|
issues.push(`${row.path}: ${message(error)}`);
|
|
4311
|
+
return;
|
|
3292
4312
|
}
|
|
3293
4313
|
}
|
|
3294
|
-
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
}
|
|
4314
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4315
|
+
for (const path of planned.keys()) {
|
|
4316
|
+
const key = path.toLowerCase();
|
|
4317
|
+
if (keys.has(key)) {
|
|
4318
|
+
issues.push(`${path}: conflicts with a preserved server path.`);
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
keys.add(key);
|
|
4322
|
+
}
|
|
4323
|
+
for (const key of keys) {
|
|
4324
|
+
const parts = key.split("/");
|
|
4325
|
+
for (let i = 1; i < parts.length; i++) {
|
|
4326
|
+
if (keys.has(parts.slice(0, i).join("/"))) {
|
|
4327
|
+
issues.push(`${key}: a file conflicts with its parent directory.`);
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (!await this.reconcile("", issues)) return;
|
|
4333
|
+
const after = this.index.index().filter((row) => !this.excludes(row.path));
|
|
4334
|
+
const generations = new Map(before.map((row) => [row.path, row.generation]));
|
|
4335
|
+
if (after.length !== before.length || after.some((row) => row.eventPending || generations.get(row.path) !== row.generation))
|
|
4336
|
+
return;
|
|
4337
|
+
if (changes.length || !publication.complete) {
|
|
4338
|
+
this.publication.enqueue(this.root, {
|
|
4339
|
+
operationId: randomUUID7(),
|
|
4340
|
+
parentRevision: publication.revision,
|
|
4341
|
+
changes,
|
|
4342
|
+
...this.projectScope ? { configRevision: this.sharedConfig.revision } : {}
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
3303
4345
|
for (const snapshot of snapshots)
|
|
3304
4346
|
this.index.clean(snapshot.path, snapshot.generation, snapshot.hash, this.clock());
|
|
3305
4347
|
}
|
|
@@ -3342,7 +4384,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3342
4384
|
}
|
|
3343
4385
|
const job = this.index.job(owner);
|
|
3344
4386
|
const ownerRoot = this.projectScope ? this.root : await safePath(this.root, owner);
|
|
3345
|
-
await
|
|
4387
|
+
await mkdir12(ownerRoot).catch((error) => {
|
|
3346
4388
|
if (error.code !== "EEXIST") throw error;
|
|
3347
4389
|
});
|
|
3348
4390
|
const ownerStat = await lstat4(ownerRoot);
|
|
@@ -3397,8 +4439,9 @@ function message(error) {
|
|
|
3397
4439
|
}
|
|
3398
4440
|
|
|
3399
4441
|
// apps/client/sessions/synchronizer.ts
|
|
3400
|
-
import { randomUUID as
|
|
3401
|
-
import { join as
|
|
4442
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
4443
|
+
import { join as join23 } from "node:path";
|
|
4444
|
+
import { performance as performance3 } from "node:perf_hooks";
|
|
3402
4445
|
|
|
3403
4446
|
// packages/protocol/session-key.ts
|
|
3404
4447
|
import { createHash as createHash10 } from "node:crypto";
|
|
@@ -3408,15 +4451,15 @@ function sessionKey(agent, nativeId) {
|
|
|
3408
4451
|
|
|
3409
4452
|
// apps/client/sessions/source.ts
|
|
3410
4453
|
import { createReadStream, realpathSync } from "node:fs";
|
|
3411
|
-
import { stat, realpath as
|
|
3412
|
-
import { basename as
|
|
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";
|
|
3413
4456
|
import { createInterface as createInterface3 } from "node:readline";
|
|
3414
4457
|
import * as zlib from "node:zlib";
|
|
3415
4458
|
|
|
3416
4459
|
// apps/client/sessions/cursor.ts
|
|
3417
4460
|
import { createHash as createHash11 } from "node:crypto";
|
|
3418
|
-
import { basename as
|
|
3419
|
-
import { DatabaseSync as
|
|
4461
|
+
import { basename as basename3, dirname as dirname11, resolve as resolve12 } from "node:path";
|
|
4462
|
+
import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
|
|
3420
4463
|
import { fileURLToPath } from "node:url";
|
|
3421
4464
|
function fields(bytes) {
|
|
3422
4465
|
const data = Buffer.from(bytes);
|
|
@@ -3454,33 +4497,30 @@ function fields(bytes) {
|
|
|
3454
4497
|
return result2;
|
|
3455
4498
|
}
|
|
3456
4499
|
function readCursor(path, projectDir, checkCwd) {
|
|
3457
|
-
const db = new
|
|
4500
|
+
const db = new DatabaseSync6(path, { readOnly: true });
|
|
3458
4501
|
try {
|
|
3459
4502
|
db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=100; BEGIN");
|
|
3460
4503
|
if (db.prepare("PRAGMA user_version").get()?.user_version !== 1)
|
|
3461
4504
|
throw new Error("Unsupported Cursor CLI store version (expected 1).");
|
|
3462
|
-
const row = db.prepare("SELECT value FROM meta WHERE key='0'
|
|
4505
|
+
const row = db.prepare("SELECT value FROM meta WHERE key='0'").get();
|
|
3463
4506
|
const hex = row?.value;
|
|
3464
4507
|
if (typeof hex !== "string" || !/^(?:[a-fA-F0-9]{2})+$/.test(hex))
|
|
3465
4508
|
throw new Error("Invalid Cursor metadata.");
|
|
3466
4509
|
const meta = object(JSON.parse(Buffer.from(hex, "hex").toString("utf8")));
|
|
3467
|
-
if (typeof meta.agentId !== "string" || meta.agentId !==
|
|
4510
|
+
if (typeof meta.agentId !== "string" || meta.agentId !== basename3(dirname11(path)) || meta.subagentInfo != null)
|
|
3468
4511
|
throw new Error("Invalid or delegated Cursor session identity.");
|
|
3469
|
-
let budget = 64 * 1024 * 1024;
|
|
3470
4512
|
const blob = (id) => {
|
|
3471
4513
|
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid Cursor blob reference.");
|
|
3472
|
-
const value = db.prepare("SELECT data FROM blobs WHERE id=?
|
|
3473
|
-
if (!(value instanceof Uint8Array))
|
|
3474
|
-
throw new Error("Cursor history missing or exceeds 64 MiB.");
|
|
4514
|
+
const value = db.prepare("SELECT data FROM blobs WHERE id=?").get(id)?.data;
|
|
4515
|
+
if (!(value instanceof Uint8Array)) throw new Error("Cursor history is missing.");
|
|
3475
4516
|
const bytes = Buffer.from(value);
|
|
3476
|
-
budget -= bytes.length;
|
|
3477
4517
|
if (createHash11("sha256").update(bytes).digest("hex") !== id)
|
|
3478
4518
|
throw new Error("Cursor blob failed integrity verification.");
|
|
3479
4519
|
return bytes;
|
|
3480
4520
|
};
|
|
3481
4521
|
const root = fields(blob(String(meta.latestRootBlobId)));
|
|
3482
|
-
const bucket =
|
|
3483
|
-
const md5 = (cwd2) => createHash11("md5").update(
|
|
4522
|
+
const bucket = basename3(dirname11(dirname11(path)));
|
|
4523
|
+
const md5 = (cwd2) => createHash11("md5").update(resolve12(cwd2)).digest("hex");
|
|
3484
4524
|
let cwd = md5(projectDir) === bucket ? projectDir : void 0;
|
|
3485
4525
|
for (const uri of root.get(9) ?? []) {
|
|
3486
4526
|
try {
|
|
@@ -3496,9 +4536,8 @@ function readCursor(path, projectDir, checkCwd) {
|
|
|
3496
4536
|
checkCwd?.(cwd);
|
|
3497
4537
|
const refs = [];
|
|
3498
4538
|
for (const archive of root.get(13) ?? root.get(11) ?? [])
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
if (refs.length > 5e4) throw new Error("Cursor history exceeds 50000 messages.");
|
|
4539
|
+
for (const ref of fields(blob(archive.toString("hex"))).get(1) ?? []) refs.push(ref);
|
|
4540
|
+
for (const ref of root.get(1) ?? []) refs.push(ref);
|
|
3502
4541
|
const messages = [];
|
|
3503
4542
|
const counts = /* @__PURE__ */ new Map();
|
|
3504
4543
|
for (const ref of refs) {
|
|
@@ -3541,11 +4580,11 @@ function readCursor(path, projectDir, checkCwd) {
|
|
|
3541
4580
|
|
|
3542
4581
|
// apps/client/sessions/source.ts
|
|
3543
4582
|
function within(project, cwd) {
|
|
3544
|
-
const rel =
|
|
3545
|
-
return rel !== ".." && !rel.startsWith(`..${
|
|
4583
|
+
const rel = relative5(project, cwd);
|
|
4584
|
+
return rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute5(rel);
|
|
3546
4585
|
}
|
|
3547
4586
|
var cursorEncoded = (cwd) => cwd.replace(/^[/\\]+/, "").replace(/[/\\]/g, "-");
|
|
3548
|
-
var cursorDatabase = (path) =>
|
|
4587
|
+
var cursorDatabase = (path) => basename4(path) === "store.db";
|
|
3549
4588
|
async function sourceSignature(path, agent) {
|
|
3550
4589
|
const paths = agent === "cursor" && cursorDatabase(path) ? [path, `${path}-wal`] : [path];
|
|
3551
4590
|
const signatures = [];
|
|
@@ -3569,12 +4608,12 @@ async function sourceSignature(path, agent) {
|
|
|
3569
4608
|
return JSON.stringify(signatures);
|
|
3570
4609
|
}
|
|
3571
4610
|
async function cursorScope(path, project) {
|
|
3572
|
-
let container =
|
|
3573
|
-
while (
|
|
3574
|
-
container =
|
|
3575
|
-
if (
|
|
4611
|
+
let container = dirname12(path);
|
|
4612
|
+
while (basename4(container) !== "agent-transcripts" && dirname12(container) !== container)
|
|
4613
|
+
container = dirname12(container);
|
|
4614
|
+
if (basename4(container) !== "agent-transcripts")
|
|
3576
4615
|
throw new Error("Cursor transcript workspace cannot be verified.");
|
|
3577
|
-
const encoded =
|
|
4616
|
+
const encoded = basename4(dirname12(container));
|
|
3578
4617
|
const matches = [];
|
|
3579
4618
|
const visit = async (directory) => {
|
|
3580
4619
|
const prefix = cursorEncoded(directory);
|
|
@@ -3584,7 +4623,7 @@ async function cursorScope(path, project) {
|
|
|
3584
4623
|
}
|
|
3585
4624
|
if (!encoded.startsWith(prefix + "-")) return;
|
|
3586
4625
|
for (const entry of await readdir2(directory, { withFileTypes: true })) {
|
|
3587
|
-
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(
|
|
4626
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(join21(directory, entry.name));
|
|
3588
4627
|
}
|
|
3589
4628
|
};
|
|
3590
4629
|
await visit(project);
|
|
@@ -3594,7 +4633,7 @@ async function cursorScope(path, project) {
|
|
|
3594
4633
|
}
|
|
3595
4634
|
async function readSource(agent, path, projectDir) {
|
|
3596
4635
|
const signature2 = await sourceSignature(path, agent);
|
|
3597
|
-
const project = await
|
|
4636
|
+
const project = await realpath8(projectDir);
|
|
3598
4637
|
if (agent === "cursor" && cursorDatabase(path)) {
|
|
3599
4638
|
const snapshot2 = readCursor(path, projectDir, (cwd) => {
|
|
3600
4639
|
if (!within(project, realpathSync(cwd)))
|
|
@@ -3602,7 +4641,7 @@ async function readSource(agent, path, projectDir) {
|
|
|
3602
4641
|
});
|
|
3603
4642
|
return { snapshot: snapshot2, incomplete: false, skipped: 0, signature: signature2 };
|
|
3604
4643
|
}
|
|
3605
|
-
const cursor = agent === "cursor" ? { nativeId:
|
|
4644
|
+
const cursor = agent === "cursor" ? { nativeId: basename4(path, ".jsonl"), cwd: await cursorScope(path, projectDir) } : void 0;
|
|
3606
4645
|
const normalizer = new JsonlNormalizer(agent, cursor);
|
|
3607
4646
|
const stream = createReadStream(path, {
|
|
3608
4647
|
end: Math.max(0, Number(JSON.parse(signature2)[0][2]) - 1)
|
|
@@ -3633,7 +4672,7 @@ async function readSource(agent, path, projectDir) {
|
|
|
3633
4672
|
}
|
|
3634
4673
|
const cwd = row.cwd ?? (row.type === "session_meta" ? object(row.payload).cwd : void 0);
|
|
3635
4674
|
if (typeof cwd === "string" && !checkedCwds.has(cwd)) {
|
|
3636
|
-
const actual = await
|
|
4675
|
+
const actual = await realpath8(cwd).catch((error) => {
|
|
3637
4676
|
if (error.code === "ENOENT" && !within(project, cwd))
|
|
3638
4677
|
throw new Error("Session cwd is outside the registered project.");
|
|
3639
4678
|
throw error;
|
|
@@ -3654,21 +4693,54 @@ async function readSource(agent, path, projectDir) {
|
|
|
3654
4693
|
if (before[0] !== after[0] || before[1] !== after[1] || Number(after[2]) < Number(before[2]) || before[2] === after[2] && before.join() !== after.join())
|
|
3655
4694
|
throw new Error("Session source changed while reading; retry next cycle.");
|
|
3656
4695
|
const snapshot = normalizer.finish();
|
|
3657
|
-
if (!within(project, await
|
|
4696
|
+
if (!within(project, await realpath8(snapshot.cwd)))
|
|
3658
4697
|
throw new Error("Session cwd is outside the registered project.");
|
|
3659
4698
|
return { snapshot, incomplete: lastBad, skipped, signature: signature2 };
|
|
3660
4699
|
}
|
|
3661
4700
|
|
|
3662
4701
|
// apps/client/sessions/discovery.ts
|
|
3663
4702
|
import { opendir as opendir2 } from "node:fs/promises";
|
|
3664
|
-
import {
|
|
4703
|
+
import { performance as performance2 } from "node:perf_hooks";
|
|
4704
|
+
import { join as join22 } from "node:path";
|
|
4705
|
+
|
|
4706
|
+
// apps/client/sessions/work-queue.ts
|
|
4707
|
+
var SessionWorkQueue = class {
|
|
4708
|
+
active = 0;
|
|
4709
|
+
waiting = [];
|
|
4710
|
+
async run(action) {
|
|
4711
|
+
if (this.active >= 2) await new Promise((resolve16) => this.waiting.push(resolve16));
|
|
4712
|
+
else this.active++;
|
|
4713
|
+
try {
|
|
4714
|
+
return await action();
|
|
4715
|
+
} finally {
|
|
4716
|
+
const next = this.waiting.shift();
|
|
4717
|
+
if (next) next();
|
|
4718
|
+
else this.active--;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
};
|
|
4722
|
+
var sessionWorkQueue = new SessionWorkQueue();
|
|
4723
|
+
var publications = /* @__PURE__ */ new Map();
|
|
4724
|
+
async function serializeSession(identity, action) {
|
|
4725
|
+
const previous = publications.get(identity) ?? Promise.resolve();
|
|
4726
|
+
const pending = previous.catch(() => {
|
|
4727
|
+
}).then(action);
|
|
4728
|
+
publications.set(identity, pending);
|
|
4729
|
+
try {
|
|
4730
|
+
return await pending;
|
|
4731
|
+
} finally {
|
|
4732
|
+
if (publications.get(identity) === pending) publications.delete(identity);
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
// apps/client/sessions/discovery.ts
|
|
3665
4737
|
async function* candidates(directory, agent) {
|
|
3666
4738
|
const queue = [directory];
|
|
3667
4739
|
while (queue.length) {
|
|
3668
4740
|
const current = queue.shift();
|
|
3669
4741
|
const dir = await opendir2(current);
|
|
3670
4742
|
for await (const entry of dir) {
|
|
3671
|
-
const path =
|
|
4743
|
+
const path = join22(current, entry.name);
|
|
3672
4744
|
if (entry.isDirectory()) queue.push(path);
|
|
3673
4745
|
if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" || entry.name.endsWith(".jsonl") : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
|
|
3674
4746
|
yield path;
|
|
@@ -3682,10 +4754,12 @@ var SessionDiscovery = class {
|
|
|
3682
4754
|
}
|
|
3683
4755
|
state;
|
|
3684
4756
|
scans = /* @__PURE__ */ new Map();
|
|
4757
|
+
pending = false;
|
|
3685
4758
|
async cycle() {
|
|
4759
|
+
this.pending = false;
|
|
3686
4760
|
const issues = [];
|
|
3687
4761
|
let entries = 500;
|
|
3688
|
-
|
|
4762
|
+
const started = performance2.now();
|
|
3689
4763
|
const configs = this.state.directories();
|
|
3690
4764
|
for (const [directory, scan] of this.scans) {
|
|
3691
4765
|
if (!configs.some((config) => config.directory === directory)) {
|
|
@@ -3701,7 +4775,7 @@ var SessionDiscovery = class {
|
|
|
3701
4775
|
}
|
|
3702
4776
|
if (scan.nextAt > Date.now()) continue;
|
|
3703
4777
|
try {
|
|
3704
|
-
while (entries > 0 &&
|
|
4778
|
+
while (entries > 0 && performance2.now() - started < 1e3) {
|
|
3705
4779
|
entries--;
|
|
3706
4780
|
const entry = await scan.iterator.next();
|
|
3707
4781
|
if (entry.done) {
|
|
@@ -3714,13 +4788,14 @@ var SessionDiscovery = class {
|
|
|
3714
4788
|
const signature2 = await sourceSignature(path, config.agent);
|
|
3715
4789
|
const db = this.state.state.db;
|
|
3716
4790
|
if (db.prepare(
|
|
3717
|
-
"SELECT 1 FROM session_discovery_cache WHERE root=? AND path=? AND signature=?"
|
|
3718
|
-
).get(this.state.root, path, signature2))
|
|
4791
|
+
"SELECT 1 FROM session_discovery_cache WHERE root=? AND path=? AND signature=? AND parser_version=?"
|
|
4792
|
+
).get(this.state.root, path, signature2, parserVersions[config.agent]))
|
|
3719
4793
|
continue;
|
|
3720
4794
|
try {
|
|
3721
|
-
const result2 = await
|
|
4795
|
+
const result2 = await sessionWorkQueue.run(
|
|
4796
|
+
() => readSource(config.agent, path, config.projectDir)
|
|
4797
|
+
);
|
|
3722
4798
|
if (!result2.snapshot.messages.length) continue;
|
|
3723
|
-
reads--;
|
|
3724
4799
|
this.state.track(config.agent, path, config.projectDir);
|
|
3725
4800
|
} catch (error) {
|
|
3726
4801
|
const message2 = error instanceof Error ? error.message : "Discovery failed.";
|
|
@@ -3730,8 +4805,8 @@ var SessionDiscovery = class {
|
|
|
3730
4805
|
continue;
|
|
3731
4806
|
}
|
|
3732
4807
|
db.prepare(
|
|
3733
|
-
"INSERT INTO session_discovery_cache VALUES (
|
|
3734
|
-
).run(this.state.root, path, signature2);
|
|
4808
|
+
"INSERT INTO session_discovery_cache(root,path,signature,parser_version) VALUES (?,?,?,?) ON CONFLICT(root,path) DO UPDATE SET signature=excluded.signature,parser_version=excluded.parser_version"
|
|
4809
|
+
).run(this.state.root, path, signature2, parserVersions[config.agent]);
|
|
3735
4810
|
}
|
|
3736
4811
|
} catch (error) {
|
|
3737
4812
|
if (error.code !== "ENOENT")
|
|
@@ -3743,6 +4818,7 @@ var SessionDiscovery = class {
|
|
|
3743
4818
|
scan.nextAt = Date.now() + 3e5;
|
|
3744
4819
|
}
|
|
3745
4820
|
}
|
|
4821
|
+
this.pending = [...this.scans.values()].some((scan) => scan.nextAt <= Date.now());
|
|
3746
4822
|
return issues;
|
|
3747
4823
|
}
|
|
3748
4824
|
async refresh() {
|
|
@@ -3758,10 +4834,11 @@ var SessionSynchronizer = class {
|
|
|
3758
4834
|
constructor(state, root, directory, signal) {
|
|
3759
4835
|
this.root = root;
|
|
3760
4836
|
this.directory = directory;
|
|
4837
|
+
this.signal = signal;
|
|
3761
4838
|
const binding = state.binding(root);
|
|
3762
4839
|
if (!binding) throw new Error("Connect this root before collecting sessions.");
|
|
3763
4840
|
this.client = new HubClient(binding.serverUrl, 1e4, binding, signal);
|
|
3764
|
-
this.blobs = new LocalBlobStore(
|
|
4841
|
+
this.blobs = new LocalBlobStore(join23(directory, "blobs"));
|
|
3765
4842
|
this.index = new SessionState(state, root);
|
|
3766
4843
|
this.discovery = new SessionDiscovery(this.index);
|
|
3767
4844
|
}
|
|
@@ -3772,8 +4849,16 @@ var SessionSynchronizer = class {
|
|
|
3772
4849
|
blobs;
|
|
3773
4850
|
discovery;
|
|
3774
4851
|
capable = false;
|
|
4852
|
+
passAt;
|
|
4853
|
+
signal;
|
|
3775
4854
|
async cycle() {
|
|
3776
|
-
const report = {
|
|
4855
|
+
const report = {
|
|
4856
|
+
published: 0,
|
|
4857
|
+
checked: 0,
|
|
4858
|
+
pending: false,
|
|
4859
|
+
issues: [],
|
|
4860
|
+
warnings: []
|
|
4861
|
+
};
|
|
3777
4862
|
report.issues.push(
|
|
3778
4863
|
...await consumeHints(
|
|
3779
4864
|
this.directory,
|
|
@@ -3783,7 +4868,10 @@ var SessionSynchronizer = class {
|
|
|
3783
4868
|
)
|
|
3784
4869
|
);
|
|
3785
4870
|
report.warnings.push(...await this.discovery.cycle());
|
|
3786
|
-
|
|
4871
|
+
this.passAt ??= Date.now();
|
|
4872
|
+
const candidates2 = this.index.sources(void 0, 201, this.passAt);
|
|
4873
|
+
const sources = candidates2.slice(0, 200);
|
|
4874
|
+
report.pending = candidates2.length > sources.length || this.discovery.pending;
|
|
3787
4875
|
if (sources.length && !this.capable) {
|
|
3788
4876
|
const info = await this.client.serverInfo();
|
|
3789
4877
|
if (!["blobs", "session-revisions"].every(
|
|
@@ -3792,60 +4880,86 @@ var SessionSynchronizer = class {
|
|
|
3792
4880
|
throw new Error("Server does not support session publication. Upgrade the server first.");
|
|
3793
4881
|
this.capable = true;
|
|
3794
4882
|
}
|
|
3795
|
-
let
|
|
3796
|
-
let
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
if (source.lastError)
|
|
3807
|
-
report.issues.push(`${source.path}: ${source.lastError} (retry deferred)`);
|
|
3808
|
-
continue;
|
|
3809
|
-
}
|
|
3810
|
-
uploads--;
|
|
3811
|
-
await this.flush(source);
|
|
3812
|
-
report.published++;
|
|
3813
|
-
continue;
|
|
3814
|
-
}
|
|
3815
|
-
const signature2 = await sourceSignature(source.path, source.agent);
|
|
3816
|
-
if (signature2 === source.signature && Date.now() - source.verifiedAt < 864e5) {
|
|
3817
|
-
this.index.checked(source.path);
|
|
3818
|
-
continue;
|
|
4883
|
+
let offset = 0;
|
|
4884
|
+
let handled = 0;
|
|
4885
|
+
const started = performance3.now();
|
|
4886
|
+
await Promise.all(
|
|
4887
|
+
Array.from({ length: 2 }, async () => {
|
|
4888
|
+
for (; ; ) {
|
|
4889
|
+
if (performance3.now() - started >= 1e3) break;
|
|
4890
|
+
const source = sources[offset++];
|
|
4891
|
+
if (!source || this.signal?.aborted) break;
|
|
4892
|
+
await sessionWorkQueue.run(() => this.collect(source.path, report));
|
|
4893
|
+
handled++;
|
|
3819
4894
|
}
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
4895
|
+
})
|
|
4896
|
+
);
|
|
4897
|
+
report.pending ||= handled < candidates2.length;
|
|
4898
|
+
if (handled >= candidates2.length) this.passAt = void 0;
|
|
4899
|
+
return report;
|
|
4900
|
+
}
|
|
4901
|
+
lock(key, action) {
|
|
4902
|
+
return serializeSession(JSON.stringify([this.client.url, this.root, key]), action);
|
|
4903
|
+
}
|
|
4904
|
+
async collect(path, report) {
|
|
4905
|
+
if (this.signal?.aborted) return;
|
|
4906
|
+
let source = this.index.get(path);
|
|
4907
|
+
report.checked++;
|
|
4908
|
+
this.index.checked(source.path, source.lastError);
|
|
4909
|
+
try {
|
|
4910
|
+
if (source.pending) {
|
|
4911
|
+
if (source.pendingPath !== source.path) return;
|
|
4912
|
+
if (source.retryAt > Date.now()) {
|
|
4913
|
+
if (source.lastError)
|
|
4914
|
+
report.issues.push(`${source.path}: ${source.lastError} (retry deferred)`);
|
|
4915
|
+
return;
|
|
3825
4916
|
}
|
|
3826
|
-
|
|
4917
|
+
await this.lock(source.key, async () => {
|
|
4918
|
+
const current = this.index.get(source.path);
|
|
4919
|
+
if (!current.pending || current.pendingPath !== current.path || current.retryAt > Date.now())
|
|
4920
|
+
return;
|
|
4921
|
+
await this.flush(current);
|
|
4922
|
+
report.published++;
|
|
4923
|
+
});
|
|
4924
|
+
return;
|
|
4925
|
+
}
|
|
4926
|
+
const signature2 = await sourceSignature(source.path, source.agent);
|
|
4927
|
+
if (source.parserVersion === parserVersions[source.agent] && signature2 === source.signature && Date.now() - source.verifiedAt < 864e5) {
|
|
4928
|
+
this.index.checked(source.path);
|
|
4929
|
+
return;
|
|
4930
|
+
}
|
|
4931
|
+
const observed = await readSource(source.agent, source.path, source.projectDir);
|
|
4932
|
+
if (!observed.snapshot.messages.length) {
|
|
4933
|
+
this.index.checked(source.path);
|
|
4934
|
+
return;
|
|
4935
|
+
}
|
|
4936
|
+
if (observed.skipped)
|
|
4937
|
+
report.warnings.push(
|
|
4938
|
+
`${source.path}: skipped ${observed.skipped} unreadable record(s); retained complete dialogue.`
|
|
4939
|
+
);
|
|
4940
|
+
if (observed.skipped && source.hash) {
|
|
4941
|
+
const previous = await this.blobs.get(source.hash);
|
|
4942
|
+
if (previous && JSON.parse(Buffer.from(previous).toString("utf8")).messages.length > observed.snapshot.messages.length) {
|
|
4943
|
+
this.index.observed(source.path, observed.signature);
|
|
3827
4944
|
report.warnings.push(
|
|
3828
|
-
`${source.path}:
|
|
4945
|
+
`${source.path}: kept the previous snapshot because the damaged copy contains fewer messages.`
|
|
3829
4946
|
);
|
|
3830
|
-
|
|
3831
|
-
const previous = await this.blobs.get(source.hash);
|
|
3832
|
-
if (previous && JSON.parse(Buffer.from(previous).toString("utf8")).messages.length > observed.snapshot.messages.length) {
|
|
3833
|
-
this.index.observed(source.path, observed.signature);
|
|
3834
|
-
report.warnings.push(
|
|
3835
|
-
`${source.path}: kept the previous snapshot because the damaged copy contains fewer messages.`
|
|
3836
|
-
);
|
|
3837
|
-
continue;
|
|
3838
|
-
}
|
|
4947
|
+
return;
|
|
3839
4948
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4949
|
+
}
|
|
4950
|
+
const key = sessionKey(source.agent, observed.snapshot.nativeId);
|
|
4951
|
+
if (source.key && source.key !== key)
|
|
4952
|
+
throw new Error(
|
|
4953
|
+
"Native session identity changed at this path; register the new source explicitly."
|
|
4954
|
+
);
|
|
4955
|
+
const bytes = Buffer.from(JSON.stringify(observed.snapshot));
|
|
4956
|
+
if (bytes.length > MAX_BLOB_BYTES)
|
|
4957
|
+
throw new Error(
|
|
4958
|
+
`Normalized session exceeds the ${MAX_BLOB_BYTES / 1024 / 1024} MiB snapshot limit.`
|
|
4959
|
+
);
|
|
4960
|
+
const blob = await this.blobs.put(bytes);
|
|
4961
|
+
await this.lock(key, async () => {
|
|
4962
|
+
source = this.index.get(source.path);
|
|
3849
4963
|
let revision = source.revision;
|
|
3850
4964
|
if (!source.key && !this.index.publication(key)) {
|
|
3851
4965
|
try {
|
|
@@ -3864,38 +4978,37 @@ var SessionSynchronizer = class {
|
|
|
3864
4978
|
this.index.baseline(source.path, key, revision);
|
|
3865
4979
|
source = this.index.get(source.path);
|
|
3866
4980
|
revision = source.revision;
|
|
3867
|
-
if (source.pending)
|
|
4981
|
+
if (source.pending) return;
|
|
3868
4982
|
if (source.agent === "cursor" && source.selectedPath && cursorDatabase(source.selectedPath) && !cursorDatabase(source.path)) {
|
|
3869
4983
|
this.index.observed(source.path, observed.signature);
|
|
3870
|
-
|
|
4984
|
+
return;
|
|
3871
4985
|
}
|
|
3872
4986
|
if (blob.sha256 === source.hash) {
|
|
3873
4987
|
this.index.acknowledge(source.path, revision, blob.sha256, observed.signature);
|
|
3874
|
-
|
|
4988
|
+
return;
|
|
3875
4989
|
}
|
|
3876
4990
|
this.index.stage(
|
|
3877
4991
|
source.path,
|
|
3878
|
-
{ operationId:
|
|
4992
|
+
{ operationId: randomUUID8(), sessionKey: key, parentRevision: revision, blob },
|
|
3879
4993
|
observed.signature
|
|
3880
4994
|
);
|
|
3881
|
-
uploads--;
|
|
3882
4995
|
await this.flush(this.index.get(source.path));
|
|
3883
4996
|
report.published++;
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4997
|
+
});
|
|
4998
|
+
} catch (error) {
|
|
4999
|
+
if (this.signal?.aborted) return;
|
|
5000
|
+
const message2 = error instanceof Error ? error.message : "Session synchronization failed.";
|
|
5001
|
+
this.index.checked(source.path, message2);
|
|
5002
|
+
report.issues.push(`${source.path}: ${message2}`);
|
|
3889
5003
|
}
|
|
3890
|
-
return report;
|
|
3891
5004
|
}
|
|
3892
5005
|
async flush(source) {
|
|
3893
5006
|
const request = source.pending;
|
|
3894
5007
|
try {
|
|
3895
|
-
const { missing:
|
|
3896
|
-
if (
|
|
5008
|
+
const { missing: missing3 } = await this.client.missingBlobs([request.blob.sha256]);
|
|
5009
|
+
if (missing3.some((hash2) => hash2 !== request.blob.sha256))
|
|
3897
5010
|
throw new Error("Server requested unrelated session content.");
|
|
3898
|
-
if (
|
|
5011
|
+
if (missing3.length) {
|
|
3899
5012
|
const bytes = await this.blobs.get(request.blob.sha256);
|
|
3900
5013
|
if (!bytes) throw new Error("Queued session snapshot is missing.");
|
|
3901
5014
|
const ref = await this.client.uploadBlob(request.blob.sha256, bytes);
|
|
@@ -3909,9 +5022,11 @@ var SessionSynchronizer = class {
|
|
|
3909
5022
|
source.path,
|
|
3910
5023
|
result2.revision,
|
|
3911
5024
|
request.blob.sha256,
|
|
3912
|
-
source.pendingSignature
|
|
5025
|
+
source.pendingSignature,
|
|
5026
|
+
source.pendingParserVersion
|
|
3913
5027
|
);
|
|
3914
5028
|
} catch (error) {
|
|
5029
|
+
if (this.signal?.aborted) throw error;
|
|
3915
5030
|
const blocked = error instanceof HubRequestError && error.status >= 400 && error.status < 500 && ![408, 429].includes(error.status);
|
|
3916
5031
|
this.index.postpone(
|
|
3917
5032
|
source,
|
|
@@ -4027,7 +5142,13 @@ var SyncJob = class {
|
|
|
4027
5142
|
pendingPublication: false,
|
|
4028
5143
|
issues: []
|
|
4029
5144
|
};
|
|
4030
|
-
let sessions = {
|
|
5145
|
+
let sessions = {
|
|
5146
|
+
published: 0,
|
|
5147
|
+
checked: 0,
|
|
5148
|
+
pending: false,
|
|
5149
|
+
issues: [],
|
|
5150
|
+
warnings: []
|
|
5151
|
+
};
|
|
4031
5152
|
try {
|
|
4032
5153
|
await verifyWorkspace(this.state, this.root);
|
|
4033
5154
|
if (!this.controlStarted) {
|
|
@@ -4089,11 +5210,20 @@ var SyncJob = class {
|
|
|
4089
5210
|
);
|
|
4090
5211
|
}
|
|
4091
5212
|
this.lastIssue = issueKey;
|
|
4092
|
-
|
|
5213
|
+
const uploadReady = [this.state, new FilePublicationState(this.state.db, "project")].some(
|
|
5214
|
+
(state) => {
|
|
5215
|
+
const pending = state.pending(this.root);
|
|
5216
|
+
return pending && pending.retryAt <= now;
|
|
5217
|
+
}
|
|
5218
|
+
);
|
|
5219
|
+
this.nextAt = now + (sessions.pending ? 1e3 : report.issues.length || this.control.enabled && (this.control.status.configError || this.control.status.reportError) ? Math.min(
|
|
5220
|
+
9e5,
|
|
5221
|
+
Math.max(this.intervalMs, 1e4) * 2 ** Math.min(this.issueCount - 1, 5)
|
|
5222
|
+
) : pendingDownloads ? 250 : uploadReady ? 1e3 : this.intervalMs);
|
|
4093
5223
|
this.runtime.save(this.root, {
|
|
4094
5224
|
state: this.stopped ? "stopped" : issue ? "waiting" : "idle",
|
|
4095
5225
|
checkedAt,
|
|
4096
|
-
lastSuccessAt: !issue && !pendingDownloads ? checkedAt : prior?.lastSuccessAt ?? null,
|
|
5226
|
+
lastSuccessAt: !issue && !pendingDownloads && !report.pendingPublication ? checkedAt : prior?.lastSuccessAt ?? null,
|
|
4097
5227
|
pendingFiles,
|
|
4098
5228
|
pendingDownloads,
|
|
4099
5229
|
issue: issue || null
|
|
@@ -4139,7 +5269,7 @@ Use teammy status or teammy logs for diagnostics.
|
|
|
4139
5269
|
`;
|
|
4140
5270
|
async function runSyncCommand(args, version) {
|
|
4141
5271
|
const action = ["on", "off"].includes(args[0] ?? "") ? args[0] : void 0;
|
|
4142
|
-
const { values } =
|
|
5272
|
+
const { values } = parseArgs4({
|
|
4143
5273
|
args: action ? args.slice(1) : args,
|
|
4144
5274
|
options: {
|
|
4145
5275
|
...locationOptions,
|
|
@@ -4179,16 +5309,29 @@ async function runSyncCommand(args, version) {
|
|
|
4179
5309
|
"Timing and recovery overrides require --once or --foreground. Manage normal sync timing in the dashboard."
|
|
4180
5310
|
);
|
|
4181
5311
|
const root = await workspacePath(values.root), directory = await stateLocation(root, values["state-dir"]);
|
|
5312
|
+
await prepareClient(directory, version, {
|
|
5313
|
+
resume: action === "on",
|
|
5314
|
+
...action ? { intent: { root, enabled: action === "on" } } : {}
|
|
5315
|
+
});
|
|
4182
5316
|
const state = new ClientState(directory), runtime = new RuntimeState(state), log = new ClientLog(directory);
|
|
4183
5317
|
try {
|
|
4184
5318
|
if (!state.binding(root)) throw new Error("Connect this workspace first.");
|
|
4185
5319
|
if (action) {
|
|
4186
5320
|
if (action === "on") await verifyWorkspace(state, root);
|
|
4187
5321
|
runtime.enable(root, action === "on");
|
|
4188
|
-
if (action === "on")
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
5322
|
+
if (action === "on") {
|
|
5323
|
+
const hooks = await setupSessions(state, root, directory);
|
|
5324
|
+
for (const issue of hooks.issues) log.write("sessions.setup.failed", root, issue);
|
|
5325
|
+
await wakeDaemon(directory, version);
|
|
5326
|
+
} else {
|
|
5327
|
+
try {
|
|
5328
|
+
if (await runningDaemon(directory))
|
|
5329
|
+
await daemonRequest(directory, { method: "wake" }, 45e3);
|
|
5330
|
+
} catch (error) {
|
|
5331
|
+
log.write("daemon.recovering", root, diagnostic(error));
|
|
5332
|
+
await stopDaemon(directory);
|
|
5333
|
+
if (runtime.roots().length) await wakeDaemon(directory, version);
|
|
5334
|
+
}
|
|
4192
5335
|
const status = runtime.status(root);
|
|
4193
5336
|
if (status) runtime.save(root, { ...status, state: "stopped" });
|
|
4194
5337
|
log.write("sync.off_requested", root);
|
|
@@ -4249,10 +5392,60 @@ async function runSyncCommand(args, version) {
|
|
|
4249
5392
|
}
|
|
4250
5393
|
}
|
|
4251
5394
|
|
|
5395
|
+
// apps/client/maintenance-command.ts
|
|
5396
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
5397
|
+
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";
|
|
5400
|
+
async function runMaintenanceCommand(command, args, version) {
|
|
5401
|
+
const { values } = parseArgs5({
|
|
5402
|
+
args,
|
|
5403
|
+
options: {
|
|
5404
|
+
"state-dir": { type: "string", default: join24(homedir6(), ".teammy", "client") },
|
|
5405
|
+
stop: { type: "boolean" },
|
|
5406
|
+
json: { type: "boolean" },
|
|
5407
|
+
help: { type: "boolean", short: "h" }
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
if (values.help) {
|
|
5411
|
+
process.stdout.write(
|
|
5412
|
+
`Usage: teammy ${command}${command === "repair" ? " [--stop]" : ""} [--state-dir DIR] [--json]
|
|
5413
|
+
|
|
5414
|
+
${command === "repair" ? "Repair this local client and preserve project on/off settings. --stop pauses the entire background service without opening the business database." : "Stop and unregister this local background service and remove its project hooks. Keeps workspaces, bindings and history; then run npm uninstall -g @teammy/cli."}
|
|
5415
|
+
`
|
|
5416
|
+
);
|
|
5417
|
+
return 0;
|
|
5418
|
+
}
|
|
5419
|
+
if (command === "uninstall" && values.stop) throw new Error("--stop is a repair option.");
|
|
5420
|
+
await mkdir13(resolve13(values["state-dir"]), { recursive: true, mode: 448 });
|
|
5421
|
+
const directory = await realpath9(values["state-dir"]);
|
|
5422
|
+
const report = await prepareClient(directory, version, {
|
|
5423
|
+
force: true,
|
|
5424
|
+
resume: true,
|
|
5425
|
+
stop: Boolean(values.stop),
|
|
5426
|
+
uninstall: command === "uninstall"
|
|
5427
|
+
});
|
|
5428
|
+
if (values.json) json(report);
|
|
5429
|
+
else {
|
|
5430
|
+
result(
|
|
5431
|
+
command === "uninstall" ? "\u672C\u673A\u63A5\u5165\u5DF2\u5378\u8F7D" : values.stop ? "\u540E\u53F0\u540C\u6B65\u5DF2\u6682\u505C" : "\u5BA2\u6237\u7AEF\u5DF2\u5C31\u7EEA",
|
|
5432
|
+
version
|
|
5433
|
+
);
|
|
5434
|
+
if (!values.stop && command !== "uninstall")
|
|
5435
|
+
result(`\u5DF2\u6062\u590D ${report.enabled} \u4E2A\u9879\u76EE\uFF1B${report.stopped} \u4E2A\u9879\u76EE\u4FDD\u6301\u505C\u6B62`);
|
|
5436
|
+
if (report.issues.length) result(`${report.issues.length} \u9879\u914D\u7F6E\u9700\u8981\u5904\u7406`, "teammy logs --all");
|
|
5437
|
+
if (command === "uninstall") result("\u5B89\u88C5\u5305\u53EF\u79FB\u9664", "npm uninstall -g @teammy/cli");
|
|
5438
|
+
}
|
|
5439
|
+
return report.issues.length ? 1 : 0;
|
|
5440
|
+
}
|
|
5441
|
+
|
|
4252
5442
|
// apps/client/runtime/daemon.ts
|
|
4253
5443
|
import { createServer } from "node:net";
|
|
4254
5444
|
import { chmod as chmod2, unlink as unlink4 } from "node:fs/promises";
|
|
5445
|
+
import { join as join25 } from "node:path";
|
|
5446
|
+
import { resolve as resolve14 } from "node:path";
|
|
4255
5447
|
async function runDaemon(directory, version) {
|
|
5448
|
+
await prepareClient(directory, version, { daemonStartup: true });
|
|
4256
5449
|
const lock = acquireRootLock("daemon:" + directory, directory), state = new ClientState(directory), runtime = new RuntimeState(state), log = new ClientLog(directory);
|
|
4257
5450
|
const jobs = /* @__PURE__ */ new Map(), retry = /* @__PURE__ */ new Map();
|
|
4258
5451
|
let closing = false, serial = Promise.resolve();
|
|
@@ -4317,7 +5510,15 @@ async function runDaemon(directory, version) {
|
|
|
4317
5510
|
void (async () => {
|
|
4318
5511
|
const input = RequestSchema.parse(JSON.parse(body.split("\n")[0]));
|
|
4319
5512
|
if (closing) throw new Error("Background service is stopping.");
|
|
4320
|
-
if (input.method === "ping")
|
|
5513
|
+
if (input.method === "ping")
|
|
5514
|
+
return {
|
|
5515
|
+
pid: process.pid,
|
|
5516
|
+
version,
|
|
5517
|
+
protocol: CONTROL_PROTOCOL,
|
|
5518
|
+
executable: process.execPath,
|
|
5519
|
+
entry: resolve14(process.argv[1]),
|
|
5520
|
+
schema: clientSchemaVersion
|
|
5521
|
+
};
|
|
4321
5522
|
if (input.method === "wake") {
|
|
4322
5523
|
retry.clear();
|
|
4323
5524
|
await reconcile();
|
|
@@ -4355,8 +5556,8 @@ async function runDaemon(directory, version) {
|
|
|
4355
5556
|
let timer;
|
|
4356
5557
|
let resolveStop = () => {
|
|
4357
5558
|
};
|
|
4358
|
-
const stopped = new Promise((
|
|
4359
|
-
resolveStop =
|
|
5559
|
+
const stopped = new Promise((resolve16) => {
|
|
5560
|
+
resolveStop = resolve16;
|
|
4360
5561
|
});
|
|
4361
5562
|
const stop = () => {
|
|
4362
5563
|
closing = true;
|
|
@@ -4367,14 +5568,15 @@ async function runDaemon(directory, version) {
|
|
|
4367
5568
|
await unlink4(socketPath(directory)).catch((e) => {
|
|
4368
5569
|
if (e.code !== "ENOENT") throw e;
|
|
4369
5570
|
});
|
|
4370
|
-
await new Promise((
|
|
5571
|
+
await new Promise((resolve16, reject) => {
|
|
4371
5572
|
server.once("error", reject);
|
|
4372
5573
|
server.listen(socketPath(directory), () => {
|
|
4373
5574
|
server.off("error", reject);
|
|
4374
|
-
|
|
5575
|
+
resolve16();
|
|
4375
5576
|
});
|
|
4376
5577
|
});
|
|
4377
5578
|
await chmod2(socketPath(directory), 384);
|
|
5579
|
+
writePrivateJson(join25(directory, "runtime", "daemon.json"), { pid: process.pid, directory });
|
|
4378
5580
|
process.once("SIGINT", stop);
|
|
4379
5581
|
process.once("SIGTERM", stop);
|
|
4380
5582
|
log.write("daemon.started", void 0, `version=${version}`);
|
|
@@ -4394,9 +5596,11 @@ async function runDaemon(directory, version) {
|
|
|
4394
5596
|
await Promise.all([...jobs.values(), ...temporaryJobs].map((job) => job.close()));
|
|
4395
5597
|
await Promise.allSettled(oneShots.values());
|
|
4396
5598
|
for (const socket of sockets) socket.destroy();
|
|
4397
|
-
await new Promise((
|
|
5599
|
+
await new Promise((resolve16) => server.close(() => resolve16()));
|
|
4398
5600
|
await unlink4(socketPath(directory)).catch(() => {
|
|
4399
5601
|
});
|
|
5602
|
+
await unlink4(join25(directory, "runtime", "daemon.json")).catch(() => {
|
|
5603
|
+
});
|
|
4400
5604
|
log.write("daemon.stopped");
|
|
4401
5605
|
state.close();
|
|
4402
5606
|
lock.close();
|
|
@@ -4413,10 +5617,12 @@ Usage:
|
|
|
4413
5617
|
teammy server-info --url URL Check server connection and protocol capabilities
|
|
4414
5618
|
teammy server-info --help Show connection options
|
|
4415
5619
|
teammy connect --url URL --project ID [--member ID | --token-file FILE] [--reconnect] [--root DIR] [--state-dir DIR]
|
|
5620
|
+
teammy repair [--stop] Repair or pause this local background client
|
|
5621
|
+
teammy uninstall Remove local service and hooks; keep user data
|
|
4416
5622
|
teammy status [--root DIR] [--state-dir DIR]
|
|
4417
5623
|
teammy sync on | off [--root DIR]
|
|
4418
5624
|
teammy logs [--root DIR] Show recent diagnostic events
|
|
4419
|
-
teammy session --help
|
|
5625
|
+
teammy session --help Collect, read and search the conversation library (\u5BF9\u8BDD\u5E93)
|
|
4420
5626
|
|
|
4421
5627
|
Root defaults to ./workspaces; local state defaults to ~/.teammy/client.
|
|
4422
5628
|
connect starts background sync; enabled projects resume after login.
|
|
@@ -4448,12 +5654,12 @@ async function runCli(args, version) {
|
|
|
4448
5654
|
return 1;
|
|
4449
5655
|
}
|
|
4450
5656
|
if (command === "_daemon") {
|
|
4451
|
-
const { values } =
|
|
5657
|
+
const { values } = parseArgs6({
|
|
4452
5658
|
args: args.slice(1),
|
|
4453
5659
|
options: { "state-dir": { type: "string" } }
|
|
4454
5660
|
});
|
|
4455
5661
|
if (!values["state-dir"]) throw new Error("Missing daemon state directory.");
|
|
4456
|
-
const directory =
|
|
5662
|
+
const directory = resolve15(values["state-dir"]);
|
|
4457
5663
|
try {
|
|
4458
5664
|
await runDaemon(directory, version);
|
|
4459
5665
|
return 0;
|
|
@@ -4462,9 +5668,17 @@ async function runCli(args, version) {
|
|
|
4462
5668
|
return 1;
|
|
4463
5669
|
}
|
|
4464
5670
|
}
|
|
5671
|
+
if (command === "repair" || command === "uninstall") {
|
|
5672
|
+
try {
|
|
5673
|
+
return await runMaintenanceCommand(command, args.slice(1), version);
|
|
5674
|
+
} catch (error) {
|
|
5675
|
+
process.stderr.write(diagnostic(error) + "\n");
|
|
5676
|
+
return 1;
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
4465
5679
|
if (command === "session") {
|
|
4466
5680
|
try {
|
|
4467
|
-
return await runSessionCommand(args.slice(1));
|
|
5681
|
+
return await runSessionCommand(args.slice(1), version);
|
|
4468
5682
|
} catch (error) {
|
|
4469
5683
|
if (args[1] === "hook") return 0;
|
|
4470
5684
|
process.stderr.write(
|
|
@@ -4497,7 +5711,7 @@ async function runCli(args, version) {
|
|
|
4497
5711
|
return 1;
|
|
4498
5712
|
}
|
|
4499
5713
|
try {
|
|
4500
|
-
const { values } =
|
|
5714
|
+
const { values } = parseArgs6({
|
|
4501
5715
|
args: args.slice(1),
|
|
4502
5716
|
options: {
|
|
4503
5717
|
help: { type: "boolean", short: "h" },
|