chamba 0.1.0 → 0.3.0
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 +50 -2
- package/dist/cli.js +662 -161
- package/dist/server.js +355 -111
- package/inject/annotate.js +18 -49
- package/package.json +6 -7
- package/skill/SKILL.md +67 -187
- package/web/assets/highlighted-body-OFNGDK62-Bn4Eu7CG.js +1 -0
- package/web/assets/index-B9DI4F1Z.js +202 -0
- package/web/assets/index-DK_n6CTo.css +2 -0
- package/web/assets/mermaid-GHXKKRXX-CEMduc-U.js +1 -0
- package/web/index.html +17 -2
- package/web/assets/abnfDiagram-VRR7QNED-aw1_rWFY.js +0 -1
- package/web/assets/arc-BMhSK0Sy.js +0 -1
- package/web/assets/architectureDiagram-ZJ3FMSHR-BZDcNKY3.js +0 -36
- package/web/assets/blockDiagram-677ZJIJ3-CLVR_gtH.js +0 -132
- package/web/assets/c4Diagram-LMCZKHZV-YjWWkNOJ.js +0 -10
- package/web/assets/channel-8ymR7Shm.js +0 -1
- package/web/assets/chunk-2Q5K7J3B-Bd9qqRl0.js +0 -1
- package/web/assets/chunk-32BRIVSS-Cq9EhooX.js +0 -1
- package/web/assets/chunk-5VM5RSS4-DYxyelz5.js +0 -15
- package/web/assets/chunk-EX3LRPZG-COaiqIlv.js +0 -231
- package/web/assets/chunk-JWPE2WC7-CMWXKnkF.js +0 -1
- package/web/assets/chunk-MOJQB5TN-BRzAoPL7.js +0 -88
- package/web/assets/chunk-RYQCIY6F-BRght6pl.js +0 -1
- package/web/assets/chunk-V7JOEXUC-BlbCYGxG.js +0 -206
- package/web/assets/chunk-VR4S4FIN-CPoKxaoM.js +0 -1
- package/web/assets/chunk-XXDRQBXY-DeNDfu5r.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-g3gy3kDO.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-g3gy3kDO.js +0 -1
- package/web/assets/cose-bilkent-JH36ORCC-C_V1sCw0.js +0 -1
- package/web/assets/cynefin-VYW2F7L2-CtWqChj_.js +0 -178
- package/web/assets/cynefinDiagram-TSTJHNR4-Q0g7rhQg.js +0 -62
- package/web/assets/cytoscape.esm-DTSO7Bv0.js +0 -331
- package/web/assets/dagre-VKFMJZFB-spMt8V5o.js +0 -4
- package/web/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/web/assets/diagram-FQU43EPY-CF7kRW9U.js +0 -3
- package/web/assets/diagram-G47NLZAW-B-5ygaqt.js +0 -24
- package/web/assets/diagram-NH7WQ7WH-CdOGPV2-.js +0 -24
- package/web/assets/diagram-OA4YK3LP-D1_1pb2t.js +0 -30
- package/web/assets/diagram-WEI45ONY-gD_xjCgB.js +0 -41
- package/web/assets/ebnfDiagram-CCIWWBDH-aW4E-V1v.js +0 -1
- package/web/assets/erDiagram-Q63AITRT-BWndxFVg.js +0 -85
- package/web/assets/flowDiagram-23GEKE2U-CVc37qLo.js +0 -156
- package/web/assets/ganttDiagram-NO4QXBWP-DV-2BraZ.js +0 -292
- package/web/assets/gitGraphDiagram-IHSO6WYX-YCy7dADs.js +0 -106
- package/web/assets/graph-C9eacEi8.js +0 -1
- package/web/assets/highlighted-body-OFNGDK62-q_mVDG84.js +0 -1
- package/web/assets/index-BG6acTBk.css +0 -1
- package/web/assets/index-CBBSnvKL.js +0 -335
- package/web/assets/infoDiagram-FWYZ7A6U-ig4wRILz.js +0 -2
- package/web/assets/init-Gi6I4Gst.js +0 -1
- package/web/assets/ishikawaDiagram-FXEZZL3T-BE8nJNWr.js +0 -70
- package/web/assets/journeyDiagram-5HDEW3XC-_olL4LFF.js +0 -139
- package/web/assets/kanban-definition-HUTT4EX6-DTFmbw9d.js +0 -89
- package/web/assets/katex-C5jXJg4s.js +0 -257
- package/web/assets/layout-DEXfKzaS.js +0 -1
- package/web/assets/linear-CbmtQHdI.js +0 -1
- package/web/assets/map-Czzmt4hB.js +0 -1
- package/web/assets/mermaid.core-CyLpSBwh.js +0 -314
- package/web/assets/mindmap-definition-LN4V7U3C-BFXQ_blT.js +0 -96
- package/web/assets/ordinal-Cboi1Yqb.js +0 -1
- package/web/assets/pegDiagram-2B236MQR-BzFkomCY.js +0 -1
- package/web/assets/pieDiagram-ENE6RG2P-r6gWlwuV.js +0 -39
- package/web/assets/quadrantDiagram-ABIIQ3AL-DTFxtS0e.js +0 -7
- package/web/assets/railroadDiagram-RFXS5EU6-CbZAYLVe.js +0 -1
- package/web/assets/requirementDiagram-TGXJPOKE-DzGi5GvN.js +0 -84
- package/web/assets/sankeyDiagram-HTMAVEWB-CBWOwAUD.js +0 -40
- package/web/assets/sequenceDiagram-DBY2YBRQ-QX9rQ9ow.js +0 -162
- package/web/assets/sizeCapture-X5ZJPWSS-X8SU5JUW.js +0 -1
- package/web/assets/stateDiagram-2N3HPSRC-kQgWzKK8.js +0 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-2kXWc9Fv.js +0 -1
- package/web/assets/swimlanes-5IMT3BWC-eKT1_uaa.js +0 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-B5i-k7ui.js +0 -8
- package/web/assets/timeline-definition-FHXFAJF6-BRKXt19C.js +0 -120
- package/web/assets/vennDiagram-L72KCM5P-C-mbeAXb.js +0 -34
- package/web/assets/wardleyDiagram-EHGQE667-Doojx4Uh.js +0 -78
- package/web/assets/xychartDiagram-FW5EYKEG-CQZh8Trp.js +0 -7
package/dist/cli.js
CHANGED
|
@@ -1,97 +1,106 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/cli/src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
5
6
|
|
|
6
7
|
// packages/server/src/index.ts
|
|
7
8
|
import { serve as honoServe } from "@hono/node-server";
|
|
8
9
|
|
|
9
10
|
// packages/shared/src/annotations.ts
|
|
10
|
-
import { z as
|
|
11
|
+
import { z as z3 } from "zod";
|
|
11
12
|
|
|
12
13
|
// packages/shared/src/messages.ts
|
|
14
|
+
import { z as z2 } from "zod";
|
|
15
|
+
|
|
16
|
+
// packages/shared/src/session.ts
|
|
13
17
|
import { z } from "zod";
|
|
14
|
-
var
|
|
18
|
+
var sessionKindSchema = z.enum(["everyday", "spec"]);
|
|
19
|
+
var sessionStatusSchema = z.enum(["active", "ended"]);
|
|
20
|
+
var sessionMetaSchema = z.object({
|
|
15
21
|
id: z.string().min(1),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
kind: sessionKindSchema.optional(),
|
|
23
|
+
title: z.string().optional(),
|
|
24
|
+
status: sessionStatusSchema,
|
|
25
|
+
createdAt: z.string().datetime(),
|
|
26
|
+
updatedAt: z.string().datetime()
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// packages/shared/src/messages.ts
|
|
30
|
+
var attachmentSchema = z2.object({
|
|
31
|
+
id: z2.string().min(1),
|
|
32
|
+
name: z2.string().min(1),
|
|
33
|
+
relPath: z2.string().min(1),
|
|
34
|
+
contentType: z2.string().min(1),
|
|
35
|
+
size: z2.number().int().nonnegative()
|
|
20
36
|
});
|
|
21
|
-
var messagePayloadSchema =
|
|
22
|
-
text:
|
|
23
|
-
attachments:
|
|
37
|
+
var messagePayloadSchema = z2.object({
|
|
38
|
+
text: z2.string().min(1).optional(),
|
|
39
|
+
attachments: z2.array(attachmentSchema).optional()
|
|
24
40
|
}).refine((p) => (p.text?.length ?? 0) > 0 || (p.attachments?.length ?? 0) > 0, {
|
|
25
41
|
message: "a message needs text or at least one attachment"
|
|
26
42
|
});
|
|
27
|
-
var systemReasonSchema =
|
|
28
|
-
|
|
43
|
+
var systemReasonSchema = z2.enum([
|
|
44
|
+
"session-created",
|
|
45
|
+
"user-moved-away",
|
|
46
|
+
"session-ended",
|
|
47
|
+
"kind-chosen"
|
|
48
|
+
]);
|
|
49
|
+
var systemPayloadSchema = z2.object({
|
|
29
50
|
reason: systemReasonSchema,
|
|
30
|
-
to:
|
|
51
|
+
to: z2.string().min(1).optional(),
|
|
52
|
+
kind: sessionKindSchema.optional()
|
|
31
53
|
});
|
|
32
54
|
|
|
33
55
|
// packages/shared/src/annotations.ts
|
|
34
56
|
var ANNOTATION_BATCH = "annotation-batch";
|
|
35
|
-
var annotationSourceSchema =
|
|
36
|
-
file:
|
|
37
|
-
line:
|
|
38
|
-
column:
|
|
57
|
+
var annotationSourceSchema = z3.object({
|
|
58
|
+
file: z3.string().min(1),
|
|
59
|
+
line: z3.number().int().positive().optional(),
|
|
60
|
+
column: z3.number().int().positive().optional()
|
|
39
61
|
});
|
|
40
|
-
var annotationComponentSchema =
|
|
41
|
-
name:
|
|
62
|
+
var annotationComponentSchema = z3.object({
|
|
63
|
+
name: z3.string().min(1),
|
|
42
64
|
source: annotationSourceSchema.optional()
|
|
43
65
|
});
|
|
44
|
-
var annotationTargetSchema =
|
|
45
|
-
selector:
|
|
46
|
-
pageUrl:
|
|
47
|
-
viewport:
|
|
48
|
-
width:
|
|
49
|
-
height:
|
|
66
|
+
var annotationTargetSchema = z3.object({
|
|
67
|
+
selector: z3.string().min(1),
|
|
68
|
+
pageUrl: z3.string().min(1),
|
|
69
|
+
viewport: z3.object({
|
|
70
|
+
width: z3.number().int().positive(),
|
|
71
|
+
height: z3.number().int().positive()
|
|
50
72
|
}),
|
|
51
|
-
label:
|
|
73
|
+
label: z3.string().optional(),
|
|
52
74
|
component: annotationComponentSchema.optional()
|
|
53
75
|
});
|
|
54
|
-
var annotationItemSchema =
|
|
55
|
-
note:
|
|
76
|
+
var annotationItemSchema = z3.object({
|
|
77
|
+
note: z3.string().min(1).optional(),
|
|
56
78
|
target: annotationTargetSchema,
|
|
57
79
|
screenshot: attachmentSchema.optional()
|
|
58
80
|
});
|
|
59
|
-
var annotationBatchPayloadSchema =
|
|
60
|
-
items:
|
|
81
|
+
var annotationBatchPayloadSchema = z3.object({
|
|
82
|
+
items: z3.array(annotationItemSchema).min(1)
|
|
61
83
|
});
|
|
62
84
|
|
|
63
85
|
// packages/shared/src/api.ts
|
|
64
86
|
import { z as z7 } from "zod";
|
|
65
87
|
|
|
66
88
|
// packages/shared/src/events.ts
|
|
67
|
-
import { z as
|
|
68
|
-
var eventSourceSchema =
|
|
69
|
-
var eventEnvelopeSchema =
|
|
70
|
-
id:
|
|
71
|
-
seq:
|
|
72
|
-
sessionId:
|
|
73
|
-
ts:
|
|
74
|
-
type:
|
|
89
|
+
import { z as z4 } from "zod";
|
|
90
|
+
var eventSourceSchema = z4.enum(["user", "agent", "system"]);
|
|
91
|
+
var eventEnvelopeSchema = z4.object({
|
|
92
|
+
id: z4.string().uuid(),
|
|
93
|
+
seq: z4.number().int().nonnegative(),
|
|
94
|
+
sessionId: z4.string().min(1),
|
|
95
|
+
ts: z4.string().datetime(),
|
|
96
|
+
type: z4.string().min(1),
|
|
75
97
|
source: eventSourceSchema,
|
|
76
|
-
payload:
|
|
98
|
+
payload: z4.unknown()
|
|
77
99
|
});
|
|
78
100
|
|
|
79
101
|
// packages/shared/src/presence.ts
|
|
80
|
-
import { z as z4 } from "zod";
|
|
81
|
-
var presenceSchema = z4.enum(["listening", "away"]);
|
|
82
|
-
|
|
83
|
-
// packages/shared/src/session.ts
|
|
84
102
|
import { z as z5 } from "zod";
|
|
85
|
-
var
|
|
86
|
-
var sessionStatusSchema = z5.enum(["active", "ended"]);
|
|
87
|
-
var sessionMetaSchema = z5.object({
|
|
88
|
-
id: z5.string().min(1),
|
|
89
|
-
kind: sessionKindSchema,
|
|
90
|
-
title: z5.string().optional(),
|
|
91
|
-
status: sessionStatusSchema,
|
|
92
|
-
createdAt: z5.string().datetime(),
|
|
93
|
-
updatedAt: z5.string().datetime()
|
|
94
|
-
});
|
|
103
|
+
var presenceSchema = z5.enum(["listening", "working", "away"]);
|
|
95
104
|
|
|
96
105
|
// packages/shared/src/spec.ts
|
|
97
106
|
import { z as z6 } from "zod";
|
|
@@ -134,6 +143,9 @@ var createSessionReqSchema = z7.object({
|
|
|
134
143
|
kind: sessionKindSchema.optional(),
|
|
135
144
|
title: z7.string().min(1).optional()
|
|
136
145
|
});
|
|
146
|
+
var setSessionKindReqSchema = z7.object({
|
|
147
|
+
kind: sessionKindSchema
|
|
148
|
+
});
|
|
137
149
|
var messageBodySchema = z7.object({
|
|
138
150
|
text: z7.string().min(1).optional(),
|
|
139
151
|
attachmentIds: z7.array(z7.string().min(1)).optional()
|
|
@@ -166,7 +178,8 @@ var eventResSchema = z7.object({
|
|
|
166
178
|
});
|
|
167
179
|
var pollResSchema = z7.object({
|
|
168
180
|
events: z7.array(eventEnvelopeSchema),
|
|
169
|
-
cursor: cursorSchema
|
|
181
|
+
cursor: cursorSchema,
|
|
182
|
+
ended: z7.boolean().optional()
|
|
170
183
|
});
|
|
171
184
|
var fetchMoreSchema = z7.object({
|
|
172
185
|
hasMore: z7.boolean(),
|
|
@@ -192,7 +205,10 @@ var sessionResSchema = z7.object({
|
|
|
192
205
|
});
|
|
193
206
|
var healthResSchema = z7.object({
|
|
194
207
|
ok: z7.boolean(),
|
|
195
|
-
sessions: cursorSchema
|
|
208
|
+
sessions: cursorSchema,
|
|
209
|
+
home: z7.string().min(1).optional(),
|
|
210
|
+
port: z7.number().int().positive().optional(),
|
|
211
|
+
version: z7.string().min(1).optional()
|
|
196
212
|
});
|
|
197
213
|
|
|
198
214
|
// packages/shared/src/issues.ts
|
|
@@ -224,6 +240,10 @@ var wsServerMessageSchema = z9.discriminatedUnion("t", [
|
|
|
224
240
|
t: z9.literal("presence"),
|
|
225
241
|
state: presenceSchema
|
|
226
242
|
}),
|
|
243
|
+
z9.object({
|
|
244
|
+
t: z9.literal("kind"),
|
|
245
|
+
meta: sessionMetaSchema
|
|
246
|
+
}),
|
|
227
247
|
z9.object({
|
|
228
248
|
t: z9.literal("ended"),
|
|
229
249
|
meta: sessionMetaSchema
|
|
@@ -234,36 +254,176 @@ var wsServerMessageSchema = z9.discriminatedUnion("t", [
|
|
|
234
254
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
235
255
|
import { Hono } from "hono";
|
|
236
256
|
|
|
257
|
+
// packages/server/src/inject.ts
|
|
258
|
+
import { existsSync } from "node:fs";
|
|
259
|
+
import path from "node:path";
|
|
260
|
+
import { fileURLToPath } from "node:url";
|
|
261
|
+
function resolveInjectDist() {
|
|
262
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
263
|
+
const dev = path.resolve(here, "../../inject/dist");
|
|
264
|
+
const packaged = path.resolve(here, "../inject");
|
|
265
|
+
return [dev, packaged].find((dir) => existsSync(path.join(dir, "annotate.js"))) ?? dev;
|
|
266
|
+
}
|
|
267
|
+
|
|
237
268
|
// packages/server/src/render.ts
|
|
238
269
|
import { marked } from "marked";
|
|
239
270
|
|
|
271
|
+
// packages/server/src/static.ts
|
|
272
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
273
|
+
import path2 from "node:path";
|
|
274
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
275
|
+
function resolveWebDist() {
|
|
276
|
+
const here = path2.dirname(fileURLToPath2(import.meta.url));
|
|
277
|
+
const dev = path2.resolve(here, "../../web/dist");
|
|
278
|
+
const packaged = path2.resolve(here, "../web");
|
|
279
|
+
return [dev, packaged].find((dir) => existsSync2(path2.join(dir, "index.html"))) ?? dev;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// packages/server/src/app.ts
|
|
283
|
+
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
284
|
+
|
|
285
|
+
// packages/server/src/lifecycle.ts
|
|
286
|
+
import { open, readFile as readFile2, rm } from "node:fs/promises";
|
|
287
|
+
|
|
240
288
|
// packages/server/src/sessions/paths.ts
|
|
241
|
-
import
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
import { existsSync as existsSync3, realpathSync } from "node:fs";
|
|
290
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
291
|
+
import path3 from "node:path";
|
|
292
|
+
function defaultContext() {
|
|
293
|
+
return { cwd: process.cwd(), env: process.env };
|
|
294
|
+
}
|
|
295
|
+
function resolveHome(ctx = defaultContext()) {
|
|
296
|
+
return findExistingHome(ctx) ?? plannedHome(ctx);
|
|
297
|
+
}
|
|
298
|
+
function findExistingHome(ctx = defaultContext()) {
|
|
299
|
+
const dir = ctx.env.CHAMBA_DIR;
|
|
300
|
+
if (dir && dir.length > 0) return canonicalize(dir);
|
|
301
|
+
const inside = enclosingChamba(ctx.cwd);
|
|
302
|
+
if (inside) return canonicalize(inside);
|
|
303
|
+
const root = gitRoot(ctx.cwd);
|
|
304
|
+
let cursor = path3.resolve(ctx.cwd);
|
|
305
|
+
while (true) {
|
|
306
|
+
if (existsSync3(path3.join(cursor, ".chamba"))) return canonicalize(path3.join(cursor, ".chamba"));
|
|
307
|
+
if (!root) break;
|
|
308
|
+
if (cursor === root) break;
|
|
309
|
+
const parent = path3.dirname(cursor);
|
|
310
|
+
if (parent === cursor) break;
|
|
311
|
+
cursor = parent;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function plannedHome(ctx = defaultContext()) {
|
|
316
|
+
const root = gitRoot(ctx.cwd) ?? path3.resolve(ctx.cwd);
|
|
317
|
+
return canonicalize(path3.join(root, ".chamba"));
|
|
318
|
+
}
|
|
319
|
+
function gitRoot(cwd) {
|
|
320
|
+
let cursor = path3.resolve(cwd);
|
|
321
|
+
while (true) {
|
|
322
|
+
if (existsSync3(path3.join(cursor, ".git"))) return cursor;
|
|
323
|
+
const parent = path3.dirname(cursor);
|
|
324
|
+
if (parent === cursor) return null;
|
|
325
|
+
cursor = parent;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function enclosingChamba(cwd) {
|
|
329
|
+
const parts = path3.resolve(cwd).split(path3.sep);
|
|
330
|
+
const index = parts.lastIndexOf(".chamba");
|
|
331
|
+
if (index < 0) return null;
|
|
332
|
+
return parts.slice(0, index + 1).join(path3.sep) || path3.sep;
|
|
333
|
+
}
|
|
334
|
+
function canonicalize(p) {
|
|
335
|
+
const resolved = path3.resolve(p);
|
|
336
|
+
let existing = resolved;
|
|
337
|
+
const tail = [];
|
|
338
|
+
while (!existsSync3(existing)) {
|
|
339
|
+
tail.unshift(path3.basename(existing));
|
|
340
|
+
const parent = path3.dirname(existing);
|
|
341
|
+
if (parent === existing) return resolved;
|
|
342
|
+
existing = parent;
|
|
343
|
+
}
|
|
344
|
+
return path3.join(realpathSync(existing), ...tail);
|
|
245
345
|
}
|
|
246
346
|
function sessionsDir(home) {
|
|
247
|
-
return
|
|
347
|
+
return path3.join(home, "sessions");
|
|
248
348
|
}
|
|
249
349
|
function sessionDir(home, id) {
|
|
250
|
-
return
|
|
350
|
+
return path3.join(sessionsDir(home), id);
|
|
251
351
|
}
|
|
252
352
|
function metaPath(home, id) {
|
|
253
|
-
return
|
|
353
|
+
return path3.join(sessionDir(home, id), "meta.json");
|
|
354
|
+
}
|
|
355
|
+
function pidfilePath(home) {
|
|
356
|
+
return path3.join(home, "server.json");
|
|
254
357
|
}
|
|
255
358
|
function agentStatePath(home) {
|
|
256
|
-
return
|
|
359
|
+
return path3.join(home, "agent-state.json");
|
|
360
|
+
}
|
|
361
|
+
async function ensureChambaHome(home) {
|
|
362
|
+
await mkdir(sessionsDir(home), { recursive: true });
|
|
363
|
+
await ensureGitignored(home);
|
|
364
|
+
}
|
|
365
|
+
async function ensureGitignored(home) {
|
|
366
|
+
if (path3.basename(home) !== ".chamba") return;
|
|
367
|
+
const gitignore = path3.join(path3.dirname(home), ".gitignore");
|
|
368
|
+
const entry = ".chamba/";
|
|
369
|
+
try {
|
|
370
|
+
let content = "";
|
|
371
|
+
try {
|
|
372
|
+
content = await readFile(gitignore, "utf8");
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (err.code !== "ENOENT") return;
|
|
375
|
+
}
|
|
376
|
+
const already = content.split(/\r?\n/).some((line) => {
|
|
377
|
+
const trimmed = line.trim();
|
|
378
|
+
return trimmed === entry || trimmed === ".chamba";
|
|
379
|
+
});
|
|
380
|
+
if (already) return;
|
|
381
|
+
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
382
|
+
await appendFile(gitignore, `${prefix}${entry}
|
|
383
|
+
`, "utf8");
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// packages/server/src/lifecycle.ts
|
|
389
|
+
function readServerInfo(home) {
|
|
390
|
+
return readPidfile(pidfilePath(home));
|
|
391
|
+
}
|
|
392
|
+
async function readPidfile(file) {
|
|
393
|
+
try {
|
|
394
|
+
const raw = await readFile2(file, "utf8");
|
|
395
|
+
const parsed = JSON.parse(raw);
|
|
396
|
+
if (typeof parsed.pid === "number" && typeof parsed.port === "number") return parsed;
|
|
397
|
+
return null;
|
|
398
|
+
} catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
257
401
|
}
|
|
258
402
|
|
|
259
403
|
// packages/server/src/port.ts
|
|
260
404
|
var DEFAULT_PORT = 4319;
|
|
261
|
-
|
|
405
|
+
var DEFAULT_BIND_HOST = "127.0.0.1";
|
|
406
|
+
function resolveBindHost(argv = process.argv.slice(2)) {
|
|
407
|
+
const flag = readFlag(argv, "--host");
|
|
408
|
+
if (flag !== void 0 && flag.length > 0) return flag;
|
|
409
|
+
const env = process.env.CHAMBA_HOST;
|
|
410
|
+
if (env !== void 0 && env.length > 0) return env;
|
|
411
|
+
return DEFAULT_BIND_HOST;
|
|
412
|
+
}
|
|
413
|
+
function resolvePort(argv = process.argv.slice(2), home) {
|
|
262
414
|
const flag = readFlag(argv, "--port");
|
|
263
415
|
if (flag !== void 0) return parsePort(flag, "--port");
|
|
264
416
|
const env = process.env.CHAMBA_PORT;
|
|
265
417
|
if (env !== void 0 && env.length > 0) return parsePort(env, "CHAMBA_PORT");
|
|
266
|
-
return DEFAULT_PORT;
|
|
418
|
+
return home !== void 0 ? derivePort(home) : DEFAULT_PORT;
|
|
419
|
+
}
|
|
420
|
+
function derivePort(home) {
|
|
421
|
+
let hash = 2166136261;
|
|
422
|
+
for (let i = 0; i < home.length; i++) {
|
|
423
|
+
hash ^= home.charCodeAt(i);
|
|
424
|
+
hash = Math.imul(hash, 16777619);
|
|
425
|
+
}
|
|
426
|
+
return DEFAULT_PORT + Math.abs(hash) % 512;
|
|
267
427
|
}
|
|
268
428
|
function readFlag(argv, name) {
|
|
269
429
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -275,38 +435,38 @@ function readFlag(argv, name) {
|
|
|
275
435
|
}
|
|
276
436
|
function parsePort(value, source) {
|
|
277
437
|
const port = Number(value);
|
|
278
|
-
if (!Number.isInteger(port) || port <
|
|
438
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
279
439
|
throw new Error(`invalid ${source}: ${value}`);
|
|
280
440
|
}
|
|
281
441
|
return port;
|
|
282
442
|
}
|
|
283
443
|
|
|
284
444
|
// packages/server/src/version.ts
|
|
285
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
286
|
-
import
|
|
287
|
-
import { fileURLToPath } from "node:url";
|
|
445
|
+
import { existsSync as existsSync4, readFileSync } from "node:fs";
|
|
446
|
+
import path4 from "node:path";
|
|
447
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
288
448
|
function chambaVersion() {
|
|
289
|
-
let dir =
|
|
449
|
+
let dir = path4.dirname(fileURLToPath3(import.meta.url));
|
|
290
450
|
for (; ; ) {
|
|
291
|
-
const manifest =
|
|
292
|
-
if (
|
|
451
|
+
const manifest = path4.join(dir, "package.json");
|
|
452
|
+
if (existsSync4(manifest)) {
|
|
293
453
|
try {
|
|
294
454
|
const { version } = JSON.parse(readFileSync(manifest, "utf8"));
|
|
295
455
|
if (typeof version === "string" && version) return version;
|
|
296
456
|
} catch {
|
|
297
457
|
}
|
|
298
458
|
}
|
|
299
|
-
const parent =
|
|
459
|
+
const parent = path4.dirname(dir);
|
|
300
460
|
if (parent === dir) return "0.0.0-dev";
|
|
301
461
|
dir = parent;
|
|
302
462
|
}
|
|
303
463
|
}
|
|
304
464
|
|
|
305
465
|
// packages/server/src/sessions/meta.ts
|
|
306
|
-
import { readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
466
|
+
import { readdir, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
307
467
|
async function readMeta(home, id) {
|
|
308
468
|
try {
|
|
309
|
-
const raw = await
|
|
469
|
+
const raw = await readFile3(metaPath(home, id), "utf8");
|
|
310
470
|
return sessionMetaSchema.parse(JSON.parse(raw));
|
|
311
471
|
} catch (err) {
|
|
312
472
|
if (err.code === "ENOENT") return null;
|
|
@@ -354,8 +514,8 @@ function parseArgs(argv) {
|
|
|
354
514
|
}
|
|
355
515
|
|
|
356
516
|
// packages/cli/src/client.ts
|
|
357
|
-
import { readFile as
|
|
358
|
-
import
|
|
517
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
518
|
+
import path5 from "node:path";
|
|
359
519
|
var ChannelError = class extends Error {
|
|
360
520
|
constructor(status2, message) {
|
|
361
521
|
super(message);
|
|
@@ -449,9 +609,9 @@ var Client = class {
|
|
|
449
609
|
* bytes into the session directory rather than smuggling them through JSON.
|
|
450
610
|
*/
|
|
451
611
|
async uploadAttachment(id, filePath) {
|
|
452
|
-
const bytes = await
|
|
612
|
+
const bytes = await readFile4(filePath);
|
|
453
613
|
const form = new FormData();
|
|
454
|
-
form.append("file", new Blob([bytes]),
|
|
614
|
+
form.append("file", new Blob([bytes]), path5.basename(filePath));
|
|
455
615
|
const res = await this.json(
|
|
456
616
|
`/api/sessions/${encodeURIComponent(id)}/attachments`,
|
|
457
617
|
{ method: "POST", body: form, timeoutMs: 3e4 },
|
|
@@ -494,17 +654,17 @@ var Client = class {
|
|
|
494
654
|
);
|
|
495
655
|
return res.session;
|
|
496
656
|
}
|
|
497
|
-
async json(
|
|
498
|
-
const res = await this.fetch(
|
|
657
|
+
async json(path12, options, schema) {
|
|
658
|
+
const res = await this.fetch(path12, options);
|
|
499
659
|
if (!res.ok) throw await toError(res);
|
|
500
660
|
return schema.parse(await res.json());
|
|
501
661
|
}
|
|
502
|
-
async fetch(
|
|
662
|
+
async fetch(path12, options) {
|
|
503
663
|
const { method = "GET", body, timeoutMs } = options;
|
|
504
664
|
const controller = timeoutMs !== void 0 ? new AbortController() : void 0;
|
|
505
665
|
const timer = controller && timeoutMs !== void 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
506
666
|
try {
|
|
507
|
-
return await fetch(new URL(
|
|
667
|
+
return await fetch(new URL(path12, this.baseUrl), {
|
|
508
668
|
method,
|
|
509
669
|
signal: controller?.signal,
|
|
510
670
|
...bodyInit(body)
|
|
@@ -530,7 +690,7 @@ async function toError(res) {
|
|
|
530
690
|
}
|
|
531
691
|
|
|
532
692
|
// packages/cli/src/output.ts
|
|
533
|
-
import
|
|
693
|
+
import path6 from "node:path";
|
|
534
694
|
import { z as z10 } from "zod";
|
|
535
695
|
function out(message) {
|
|
536
696
|
process.stdout.write(`${message}
|
|
@@ -554,8 +714,25 @@ function eventAttachments(event) {
|
|
|
554
714
|
const parsed = z10.array(attachmentSchema).safeParse(event.payload.attachments);
|
|
555
715
|
return parsed.success ? parsed.data : [];
|
|
556
716
|
}
|
|
717
|
+
function formatSystemEvent(event) {
|
|
718
|
+
const parsed = systemPayloadSchema.safeParse(event.payload);
|
|
719
|
+
if (!parsed.success) return `\xB7 ${eventText(event)}`;
|
|
720
|
+
const { reason, to, kind } = parsed.data;
|
|
721
|
+
switch (reason) {
|
|
722
|
+
case "user-moved-away":
|
|
723
|
+
return to ? `\xB7 You moved to another session (${to})` : "\xB7 The user moved away";
|
|
724
|
+
case "session-ended":
|
|
725
|
+
return "\xB7 Session ended";
|
|
726
|
+
case "session-created":
|
|
727
|
+
return "\xB7 Session created";
|
|
728
|
+
case "kind-chosen":
|
|
729
|
+
return kind ? `\xB7 Session set to ${kind}` : "\xB7 Session kind chosen";
|
|
730
|
+
default:
|
|
731
|
+
return `\xB7 ${reason}`;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
557
734
|
function formatEvent(event) {
|
|
558
|
-
if (event.source === "system") return
|
|
735
|
+
if (event.source === "system") return formatSystemEvent(event);
|
|
559
736
|
const who = event.source === "user" ? "you" : "agent";
|
|
560
737
|
const text = eventText(event);
|
|
561
738
|
return text ? `${who}: ${text}` : `${who}:`;
|
|
@@ -582,7 +759,7 @@ function formatAnnotationBatch(event, home) {
|
|
|
582
759
|
lines.push(` component: ${target.component.name}${at}`);
|
|
583
760
|
}
|
|
584
761
|
if (item.screenshot) {
|
|
585
|
-
const abs =
|
|
762
|
+
const abs = path6.join(sessionDir(home, event.sessionId), item.screenshot.relPath);
|
|
586
763
|
lines.push(` [shot] ${item.screenshot.name} -> ${abs}`);
|
|
587
764
|
}
|
|
588
765
|
});
|
|
@@ -614,7 +791,7 @@ function formatArtifact(event, home) {
|
|
|
614
791
|
);
|
|
615
792
|
if (!parsed.success) return formatEvent(event);
|
|
616
793
|
const ref = parsed.data;
|
|
617
|
-
const abs =
|
|
794
|
+
const abs = path6.join(sessionDir(home, event.sessionId), ref.relPath);
|
|
618
795
|
return `agent [artifact ${ref.kind} "${ref.name}"] -> ${abs}`;
|
|
619
796
|
}
|
|
620
797
|
function printEvents(events, home) {
|
|
@@ -637,7 +814,7 @@ function printEvents(events, home) {
|
|
|
637
814
|
}
|
|
638
815
|
out(formatEvent(event));
|
|
639
816
|
for (const att of eventAttachments(event)) {
|
|
640
|
-
out(` [file] ${att.name} -> ${
|
|
817
|
+
out(` [file] ${att.name} -> ${path6.join(sessionDir(home, event.sessionId), att.relPath)}`);
|
|
641
818
|
}
|
|
642
819
|
}
|
|
643
820
|
}
|
|
@@ -645,15 +822,16 @@ function formatSession(meta, presence, current) {
|
|
|
645
822
|
const marker = current ? "*" : " ";
|
|
646
823
|
const title = meta.title ? ` "${meta.title}"` : "";
|
|
647
824
|
const live = presence ? ` ${presence}` : "";
|
|
648
|
-
return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind}${title}`;
|
|
825
|
+
return `${marker} ${meta.id} ${meta.status}${live} ${meta.kind ?? "unset"}${title}`;
|
|
649
826
|
}
|
|
650
827
|
|
|
651
828
|
// packages/cli/src/commands/artifact.ts
|
|
652
|
-
import { readFile as
|
|
829
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
653
830
|
|
|
654
831
|
// packages/cli/src/state.ts
|
|
655
|
-
import {
|
|
656
|
-
import
|
|
832
|
+
import { randomUUID } from "node:crypto";
|
|
833
|
+
import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
834
|
+
import path7 from "node:path";
|
|
657
835
|
import { z as z11 } from "zod";
|
|
658
836
|
var agentStateSchema = z11.object({
|
|
659
837
|
currentSessionId: z11.string().optional(),
|
|
@@ -661,7 +839,7 @@ var agentStateSchema = z11.object({
|
|
|
661
839
|
});
|
|
662
840
|
async function readState(home = resolveHome()) {
|
|
663
841
|
try {
|
|
664
|
-
const raw = await
|
|
842
|
+
const raw = await readFile5(agentStatePath(home), "utf8");
|
|
665
843
|
return agentStateSchema.parse(JSON.parse(raw));
|
|
666
844
|
} catch {
|
|
667
845
|
return { cursors: {} };
|
|
@@ -669,11 +847,16 @@ async function readState(home = resolveHome()) {
|
|
|
669
847
|
}
|
|
670
848
|
async function writeState(state, home = resolveHome()) {
|
|
671
849
|
const file = agentStatePath(home);
|
|
672
|
-
await
|
|
673
|
-
const tmp = `${file}.tmp`;
|
|
674
|
-
|
|
850
|
+
await mkdir2(path7.dirname(file), { recursive: true });
|
|
851
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
852
|
+
try {
|
|
853
|
+
await writeFile2(tmp, `${JSON.stringify(state, null, 2)}
|
|
675
854
|
`, "utf8");
|
|
676
|
-
|
|
855
|
+
await rename2(tmp, file);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
await rm3(tmp, { force: true }).catch(() => void 0);
|
|
858
|
+
throw err;
|
|
859
|
+
}
|
|
677
860
|
}
|
|
678
861
|
function cursorFor(state, id) {
|
|
679
862
|
return state.cursors[id] ?? 0;
|
|
@@ -690,7 +873,11 @@ var UsageError = class extends Error {
|
|
|
690
873
|
}
|
|
691
874
|
};
|
|
692
875
|
async function resolveSessionId(ctx) {
|
|
693
|
-
|
|
876
|
+
const flag = ctx.flags.session;
|
|
877
|
+
if (flag === "true" || flag === "") {
|
|
878
|
+
throw new UsageError("`--session` needs a session id, e.g. --session <id>");
|
|
879
|
+
}
|
|
880
|
+
if (flag) return flag;
|
|
694
881
|
const state = await readState(ctx.home);
|
|
695
882
|
if (state.currentSessionId) return state.currentSessionId;
|
|
696
883
|
throw new UsageError("no current session - run `chamba open` or pass --session <id>");
|
|
@@ -715,7 +902,7 @@ async function artifact(ctx) {
|
|
|
715
902
|
if (!file || file === "true") throw new UsageError("chamba artifact needs --file <path>");
|
|
716
903
|
let content;
|
|
717
904
|
try {
|
|
718
|
-
content = await
|
|
905
|
+
content = await readFile6(file, "utf8");
|
|
719
906
|
} catch (err) {
|
|
720
907
|
throw new UsageError(`could not read ${file}: ${err.message}`);
|
|
721
908
|
}
|
|
@@ -733,8 +920,8 @@ async function artifact(ctx) {
|
|
|
733
920
|
}
|
|
734
921
|
|
|
735
922
|
// packages/cli/src/commands/ask.ts
|
|
736
|
-
import { randomUUID } from "node:crypto";
|
|
737
|
-
import { readFile as
|
|
923
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
924
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
738
925
|
async function ask(ctx) {
|
|
739
926
|
const source = await readCardSource(ctx);
|
|
740
927
|
let parsed;
|
|
@@ -744,7 +931,7 @@ async function ask(ctx) {
|
|
|
744
931
|
throw new UsageError(`--json is not valid JSON: ${err.message}`);
|
|
745
932
|
}
|
|
746
933
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && !("cardId" in parsed)) {
|
|
747
|
-
parsed.cardId =
|
|
934
|
+
parsed.cardId = randomUUID2();
|
|
748
935
|
}
|
|
749
936
|
const result = postQuestionCardReqSchema.safeParse(parsed);
|
|
750
937
|
if (!result.success) {
|
|
@@ -762,7 +949,7 @@ async function readCardSource(ctx) {
|
|
|
762
949
|
const file = ctx.positionals[0];
|
|
763
950
|
if (file) {
|
|
764
951
|
try {
|
|
765
|
-
return await
|
|
952
|
+
return await readFile7(file, "utf8");
|
|
766
953
|
} catch (err) {
|
|
767
954
|
throw new UsageError(`could not read card file ${file}: ${err.message}`);
|
|
768
955
|
}
|
|
@@ -774,7 +961,8 @@ async function readCardSource(ctx) {
|
|
|
774
961
|
|
|
775
962
|
// packages/cli/src/commands/attach.ts
|
|
776
963
|
async function attach(ctx) {
|
|
777
|
-
const
|
|
964
|
+
const flagId = ctx.flags.session === "true" ? void 0 : ctx.flags.session || void 0;
|
|
965
|
+
const id = ctx.positionals[0] ?? flagId;
|
|
778
966
|
if (!id) throw new UsageError("usage: chamba attach <session-id>");
|
|
779
967
|
const tail = intFlag(ctx.flags.tail, 20);
|
|
780
968
|
const digest = await ctx.client.attach(id, tail);
|
|
@@ -819,8 +1007,8 @@ async function end(ctx) {
|
|
|
819
1007
|
}
|
|
820
1008
|
|
|
821
1009
|
// packages/cli/src/commands/export.ts
|
|
822
|
-
import { copyFile, mkdir as
|
|
823
|
-
import
|
|
1010
|
+
import { copyFile, mkdir as mkdir3, readFile as readFile8, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
1011
|
+
import path8 from "node:path";
|
|
824
1012
|
async function exportCmd(ctx) {
|
|
825
1013
|
const dest = ctx.positionals[0];
|
|
826
1014
|
if (!dest) throw new UsageError("usage: chamba export <dest> [--session <id>]");
|
|
@@ -829,12 +1017,12 @@ async function exportCmd(ctx) {
|
|
|
829
1017
|
const summary = await assemblePackage({
|
|
830
1018
|
events,
|
|
831
1019
|
sessionDir: sessionDir(ctx.home, id),
|
|
832
|
-
dest:
|
|
1020
|
+
dest: path8.resolve(dest)
|
|
833
1021
|
});
|
|
834
1022
|
note(
|
|
835
|
-
`exported ${id} -> ${
|
|
1023
|
+
`exported ${id} -> ${path8.resolve(dest)} (spec.md${summary.hasDraft ? "" : " placeholder"}, ${summary.assets.length} asset${summary.assets.length === 1 ? "" : "s"}, ${summary.decisions} decision${summary.decisions === 1 ? "" : "s"})`
|
|
836
1024
|
);
|
|
837
|
-
out(
|
|
1025
|
+
out(path8.resolve(dest));
|
|
838
1026
|
return 0;
|
|
839
1027
|
}
|
|
840
1028
|
async function assemblePackage(input) {
|
|
@@ -855,32 +1043,33 @@ async function assemblePackage(input) {
|
|
|
855
1043
|
if (parsed.success) decisions.push(parsed.data);
|
|
856
1044
|
}
|
|
857
1045
|
}
|
|
858
|
-
await
|
|
859
|
-
const assetsDir =
|
|
1046
|
+
await mkdir3(input.dest, { recursive: true });
|
|
1047
|
+
const assetsDir = path8.join(input.dest, "assets");
|
|
1048
|
+
await rm4(assetsDir, { recursive: true, force: true });
|
|
860
1049
|
const draftBody = canonicalDraft ? await readArtifact(input.sessionDir, canonicalDraft) : void 0;
|
|
861
1050
|
await writeFile3(
|
|
862
|
-
|
|
1051
|
+
path8.join(input.dest, "spec.md"),
|
|
863
1052
|
draftBody ?? "# Spec\n\n_No draft was authored in this session._\n",
|
|
864
1053
|
"utf8"
|
|
865
1054
|
);
|
|
866
1055
|
const assets = [];
|
|
867
1056
|
for (const ref of artifacts.values()) {
|
|
868
1057
|
if (ref === canonicalDraft) continue;
|
|
869
|
-
const src =
|
|
870
|
-
const base =
|
|
1058
|
+
const src = path8.join(input.sessionDir, ref.relPath);
|
|
1059
|
+
const base = path8.basename(ref.relPath);
|
|
871
1060
|
try {
|
|
872
|
-
await
|
|
873
|
-
await copyFile(src,
|
|
1061
|
+
await mkdir3(assetsDir, { recursive: true });
|
|
1062
|
+
await copyFile(src, path8.join(assetsDir, base));
|
|
874
1063
|
assets.push(base);
|
|
875
1064
|
} catch {
|
|
876
1065
|
}
|
|
877
1066
|
}
|
|
878
|
-
await writeFile3(
|
|
1067
|
+
await writeFile3(path8.join(input.dest, "decisions.md"), renderDecisions(decisions), "utf8");
|
|
879
1068
|
return { hasDraft: Boolean(draftBody), assets, decisions: decisions.length };
|
|
880
1069
|
}
|
|
881
1070
|
async function readArtifact(dir, ref) {
|
|
882
1071
|
try {
|
|
883
|
-
return await
|
|
1072
|
+
return await readFile8(path8.join(dir, ref.relPath), "utf8");
|
|
884
1073
|
} catch {
|
|
885
1074
|
return void 0;
|
|
886
1075
|
}
|
|
@@ -908,22 +1097,31 @@ var USAGE = `chamba - a browser companion for your terminal agent
|
|
|
908
1097
|
|
|
909
1098
|
Usage:
|
|
910
1099
|
chamba list sessions and show which is current
|
|
1100
|
+
chamba init [dir] create the .chamba workspace (at the git root, or
|
|
1101
|
+
[--here] <dir>, or the current dir with --here); prints its
|
|
1102
|
+
location and per-project port
|
|
911
1103
|
chamba open [--kind K] start a session (K: everyday | spec), print its URL
|
|
912
|
-
[--title T]
|
|
913
|
-
chamba poll
|
|
914
|
-
|
|
915
|
-
|
|
1104
|
+
[--title T] (creates the workspace at the repo root if none yet)
|
|
1105
|
+
chamba poll wait for the next human/system events, blocking across
|
|
1106
|
+
[--wait ms] quiet windows on its own until a batch arrives (it may
|
|
1107
|
+
[--for ms] include "user moved away" -> attach, or "session ended"
|
|
1108
|
+
[--once] -> stop). After a quiet stretch (--for, default 4m) it
|
|
1109
|
+
prints a heartbeat and exits 0 - just re-run. --once does
|
|
1110
|
+
one bounded check instead (scripts/tests); --wait is the
|
|
1111
|
+
per-request window (max 30s)
|
|
916
1112
|
chamba reply <text> send a line from the agent into the session
|
|
917
1113
|
[--file a,b] attach files by path (comma-separated); --image is
|
|
918
1114
|
[--image a,b] an alias. text is optional when files are attached
|
|
919
1115
|
chamba attach <id> load a session's recent tail and make it current
|
|
920
1116
|
[--tail N]
|
|
921
1117
|
chamba end end the current session
|
|
1118
|
+
chamba stop stop this project's server (it otherwise self-stops
|
|
1119
|
+
after an idle spell); the next open starts a fresh one
|
|
922
1120
|
|
|
923
1121
|
Spec sessions (open with --kind spec):
|
|
924
1122
|
chamba ask --json '{...}' post a question card (question + 2-6 options with
|
|
925
1123
|
| <file.json> descriptions, optional "recommended"); a cardId is
|
|
926
|
-
|
|
1124
|
+
created for you. The answer returns as a message
|
|
927
1125
|
chamba decision record a decision: --status confirmed|assumed
|
|
928
1126
|
--status S --summary "..." [--detail "..."]
|
|
929
1127
|
--summary T
|
|
@@ -941,45 +1139,133 @@ Spec sessions (open with --kind spec):
|
|
|
941
1139
|
|
|
942
1140
|
Global:
|
|
943
1141
|
--session <id> act on a specific session instead of the current one
|
|
944
|
-
--port <n> server port (default: CHAMBA_PORT
|
|
1142
|
+
--port <n> server port (default: CHAMBA_PORT, else a per-project
|
|
1143
|
+
port derived from the workspace location)`;
|
|
945
1144
|
function help(_ctx) {
|
|
946
1145
|
note(USAGE);
|
|
947
1146
|
return Promise.resolve(0);
|
|
948
1147
|
}
|
|
949
1148
|
|
|
1149
|
+
// packages/cli/src/commands/init.ts
|
|
1150
|
+
import path9 from "node:path";
|
|
1151
|
+
async function init(ctx) {
|
|
1152
|
+
const target = resolveTarget(ctx);
|
|
1153
|
+
const existing = findExistingHome({ cwd: target, env: {} });
|
|
1154
|
+
if (existing && !ctx.flags.here) {
|
|
1155
|
+
report("chamba workspace already exists", existing);
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
if (existing && ctx.flags.here) {
|
|
1159
|
+
note(`note: shadowing an existing workspace at ${existing}`);
|
|
1160
|
+
}
|
|
1161
|
+
await ensureChambaHome(path9.join(target, ".chamba"));
|
|
1162
|
+
const home = findExistingHome({ cwd: target, env: {} }) ?? path9.join(target, ".chamba");
|
|
1163
|
+
report("created chamba workspace", home);
|
|
1164
|
+
if (path9.basename(home) !== ".chamba") {
|
|
1165
|
+
note("warning: home is not a .chamba directory, so it will not be auto-gitignored");
|
|
1166
|
+
}
|
|
1167
|
+
return 0;
|
|
1168
|
+
}
|
|
1169
|
+
function resolveTarget(ctx) {
|
|
1170
|
+
const explicit = ctx.positionals[0];
|
|
1171
|
+
if (explicit) return path9.resolve(explicit);
|
|
1172
|
+
const root = gitRoot(process.cwd());
|
|
1173
|
+
if (root) return root;
|
|
1174
|
+
if (ctx.flags.here) return path9.resolve(process.cwd());
|
|
1175
|
+
throw new UsageError(
|
|
1176
|
+
"no .git found here - pass a directory (`chamba init <dir>`) or `chamba init --here` to choose where .chamba lives"
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
function report(headline, home) {
|
|
1180
|
+
note(headline);
|
|
1181
|
+
note(`home: ${home} port: ${derivePort(home)}`);
|
|
1182
|
+
note("each project gets its own server on its own port; other projects are unaffected");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
950
1185
|
// packages/cli/src/launch.ts
|
|
951
|
-
import { spawn } from "node:child_process";
|
|
952
|
-
import { existsSync as
|
|
1186
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
1187
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
953
1188
|
import { createRequire } from "node:module";
|
|
954
|
-
import
|
|
955
|
-
import { fileURLToPath as
|
|
1189
|
+
import path10 from "node:path";
|
|
1190
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
|
|
956
1191
|
var require2 = createRequire(import.meta.url);
|
|
957
|
-
async function ensureServer(client) {
|
|
958
|
-
if (await client.isUp())
|
|
959
|
-
|
|
960
|
-
|
|
1192
|
+
async function ensureServer(client, target) {
|
|
1193
|
+
if (await client.isUp()) {
|
|
1194
|
+
await verifyHome(client, target);
|
|
1195
|
+
return { spawned: false };
|
|
1196
|
+
}
|
|
1197
|
+
ensureDevBuilds();
|
|
1198
|
+
spawnServer(target);
|
|
1199
|
+
await waitUntilUp(client, target);
|
|
961
1200
|
return { spawned: true };
|
|
962
1201
|
}
|
|
963
|
-
function
|
|
1202
|
+
async function verifyHome(client, target) {
|
|
1203
|
+
const health = await client.health();
|
|
1204
|
+
if (health.home === target.home) return;
|
|
1205
|
+
if (health.home === void 0) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`a chamba server already owns port ${target.port} but predates workspace isolation - stop that process and retry`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
`port ${target.port} is serving a different chamba workspace (${health.home}); run from that project, or set --port / CHAMBA_PORT to run a separate server`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
function spawnServer(target) {
|
|
964
1215
|
const child = spawn(process.execPath, serverLaunchArgs(), {
|
|
965
1216
|
detached: true,
|
|
966
1217
|
stdio: "ignore",
|
|
967
|
-
env: process.env
|
|
1218
|
+
env: { ...process.env, CHAMBA_DIR: target.home, CHAMBA_PORT: String(target.port) }
|
|
968
1219
|
});
|
|
969
1220
|
child.unref();
|
|
970
1221
|
}
|
|
971
1222
|
function serverLaunchArgs() {
|
|
972
|
-
const here =
|
|
973
|
-
const packagedServer =
|
|
974
|
-
if (
|
|
1223
|
+
const here = path10.dirname(fileURLToPath4(import.meta.url));
|
|
1224
|
+
const packagedServer = path10.join(here, "server.js");
|
|
1225
|
+
if (existsSync5(packagedServer)) return [packagedServer];
|
|
975
1226
|
const serverBin = require2.resolve("@chamba/server/bin");
|
|
976
1227
|
const tsxLoader = pathToFileURL(require2.resolve("tsx")).href;
|
|
977
1228
|
return ["--import", tsxLoader, serverBin];
|
|
978
1229
|
}
|
|
979
|
-
|
|
1230
|
+
function ensureDevBuilds() {
|
|
1231
|
+
const here = path10.dirname(fileURLToPath4(import.meta.url));
|
|
1232
|
+
if (existsSync5(path10.join(here, "server.js"))) return;
|
|
1233
|
+
const repoRoot = path10.resolve(path10.dirname(require2.resolve("@chamba/server/bin")), "../../..");
|
|
1234
|
+
const deps = [
|
|
1235
|
+
{ label: "web UI", filter: "@chamba/web", dist: resolveWebDist(), marker: "index.html" },
|
|
1236
|
+
{
|
|
1237
|
+
label: "annotation client",
|
|
1238
|
+
filter: "@chamba/inject",
|
|
1239
|
+
dist: resolveInjectDist(),
|
|
1240
|
+
marker: "annotate.js"
|
|
1241
|
+
}
|
|
1242
|
+
];
|
|
1243
|
+
for (const dep of deps) {
|
|
1244
|
+
if (existsSync5(path10.join(dep.dist, dep.marker))) continue;
|
|
1245
|
+
note(`building chamba ${dep.label} (first run in this environment)...`);
|
|
1246
|
+
const result = spawnSync("pnpm", ["--filter", dep.filter, "build"], {
|
|
1247
|
+
cwd: repoRoot,
|
|
1248
|
+
encoding: "utf8"
|
|
1249
|
+
});
|
|
1250
|
+
if (result.error || result.status !== 0) {
|
|
1251
|
+
const detail = `${result.stderr ?? ""}${result.stdout ?? ""}`.trim();
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`could not build ${dep.filter} (needed to serve the ${dep.label})${detail ? `:
|
|
1254
|
+
${detail}` : ""}`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function waitUntilUp(client, target, timeoutMs = 1e4) {
|
|
980
1260
|
const deadline = Date.now() + timeoutMs;
|
|
981
1261
|
while (Date.now() < deadline) {
|
|
982
|
-
if (await client.isUp())
|
|
1262
|
+
if (await client.isUp()) {
|
|
1263
|
+
const health = await client.health();
|
|
1264
|
+
if (health.home === target.home) return;
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`port ${target.port} was taken by a different chamba workspace (${health.home ?? "unknown"})`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
983
1269
|
await delay(150);
|
|
984
1270
|
}
|
|
985
1271
|
throw new Error("server did not become healthy in time");
|
|
@@ -989,9 +1275,19 @@ function delay(ms) {
|
|
|
989
1275
|
}
|
|
990
1276
|
|
|
991
1277
|
// packages/cli/src/commands/open.ts
|
|
992
|
-
async function
|
|
993
|
-
|
|
994
|
-
|
|
1278
|
+
async function open2(ctx) {
|
|
1279
|
+
if (!ctx.workspaceExists) {
|
|
1280
|
+
if (!gitRoot(process.cwd())) {
|
|
1281
|
+
throw new UsageError(
|
|
1282
|
+
"no chamba workspace, and no .git here to anchor one - run `chamba init <dir>` or `chamba init --here` to choose where .chamba lives"
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
await ensureChambaHome(ctx.home);
|
|
1286
|
+
note(`created chamba workspace at ${ctx.home}`);
|
|
1287
|
+
}
|
|
1288
|
+
note(`home: ${ctx.home} port: ${ctx.port}`);
|
|
1289
|
+
const { spawned } = await ensureServer(ctx.client, { home: ctx.home, port: ctx.port });
|
|
1290
|
+
if (spawned) note(`started chamba server on :${ctx.port}`);
|
|
995
1291
|
const req = {
|
|
996
1292
|
...ctx.flags.kind ? { kind: parseKind(ctx.flags.kind) } : {},
|
|
997
1293
|
...ctx.flags.title ? { title: ctx.flags.title } : {}
|
|
@@ -1002,7 +1298,17 @@ async function open(ctx) {
|
|
|
1002
1298
|
const state = await readState(ctx.home);
|
|
1003
1299
|
await writeState(withSession(state, id, digest.cursor), ctx.home);
|
|
1004
1300
|
out(`http://localhost:${ctx.port}/?s=${id}`);
|
|
1301
|
+
note(" in a container? if this link is unreachable from your host:");
|
|
1302
|
+
note(
|
|
1303
|
+
` - the server bound ${resolveBindHost([])}; set CHAMBA_HOST=0.0.0.0 so the host can reach it`
|
|
1304
|
+
);
|
|
1305
|
+
note(` - forward port ${ctx.port} to the host (1:1); pin it with CHAMBA_PORT to keep it stable`);
|
|
1005
1306
|
note(`session ${id} - open the URL, then run \`chamba poll\` to wait for messages`);
|
|
1307
|
+
if (!ctx.flags.kind) {
|
|
1308
|
+
note(
|
|
1309
|
+
" no --kind given: the human picks spec or everyday in the browser; your next poll reports it as `\xB7 Session set to ...`"
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1006
1312
|
return 0;
|
|
1007
1313
|
}
|
|
1008
1314
|
function parseKind(value) {
|
|
@@ -1012,9 +1318,45 @@ function parseKind(value) {
|
|
|
1012
1318
|
}
|
|
1013
1319
|
|
|
1014
1320
|
// packages/cli/src/commands/poll.ts
|
|
1015
|
-
|
|
1321
|
+
var DEFAULT_WAIT_MS = 25e3;
|
|
1322
|
+
var MAX_WAIT_MS = 3e4;
|
|
1323
|
+
var DEFAULT_FOLLOW_MS = 4 * 6e4;
|
|
1324
|
+
var BACKOFF_START_MS = 500;
|
|
1325
|
+
var BACKOFF_MAX_MS = 5e3;
|
|
1326
|
+
var IDLE_GUARD_MS = 1e3;
|
|
1327
|
+
var realClock = {
|
|
1328
|
+
now: () => Date.now(),
|
|
1329
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1330
|
+
};
|
|
1331
|
+
async function poll(ctx, clock = realClock) {
|
|
1016
1332
|
const id = await resolveSessionId(ctx);
|
|
1017
|
-
const wait = Math.min(intFlag(ctx.flags.wait,
|
|
1333
|
+
const wait = Math.min(intFlag(ctx.flags.wait, DEFAULT_WAIT_MS), MAX_WAIT_MS);
|
|
1334
|
+
if (ctx.flags.once !== void 0) return pollOnce(ctx, id, wait);
|
|
1335
|
+
const followMs = intFlag(ctx.flags.for, DEFAULT_FOLLOW_MS);
|
|
1336
|
+
return follow(
|
|
1337
|
+
{
|
|
1338
|
+
now: clock.now,
|
|
1339
|
+
sleep: clock.sleep,
|
|
1340
|
+
cursor: async () => cursorFor(await readState(ctx.home), id),
|
|
1341
|
+
poll: (since) => ctx.client.poll(id, since, wait),
|
|
1342
|
+
deliver: async (res) => {
|
|
1343
|
+
printEvents(res.events, ctx.home);
|
|
1344
|
+
await writeState(withSession(await readState(ctx.home), id, res.cursor), ctx.home);
|
|
1345
|
+
},
|
|
1346
|
+
onError: (err, giveUp) => note(pollErrorMessage(err, giveUp)),
|
|
1347
|
+
onHeartbeat: () => note("still listening - re-run chamba poll to keep waiting"),
|
|
1348
|
+
onEnded: () => note("session has ended - nothing more to wait for"),
|
|
1349
|
+
isFatal
|
|
1350
|
+
},
|
|
1351
|
+
// The park floor never drops below the guard sleep, so a small (or zero) `--wait`
|
|
1352
|
+
// - which makes the server answer empty at once - cannot spin the loop hot.
|
|
1353
|
+
{
|
|
1354
|
+
deadline: clock.now() + followMs,
|
|
1355
|
+
parkFloorMs: Math.max(IDLE_GUARD_MS, Math.floor(wait / 2))
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
async function pollOnce(ctx, id, wait) {
|
|
1018
1360
|
const state = await readState(ctx.home);
|
|
1019
1361
|
const since = cursorFor(state, id);
|
|
1020
1362
|
const res = await ctx.client.poll(id, since, wait);
|
|
@@ -1026,8 +1368,53 @@ async function poll(ctx) {
|
|
|
1026
1368
|
await writeState(withSession(state, id, res.cursor), ctx.home);
|
|
1027
1369
|
return 0;
|
|
1028
1370
|
}
|
|
1371
|
+
async function follow(io, cfg) {
|
|
1372
|
+
let backoff = BACKOFF_START_MS;
|
|
1373
|
+
while (true) {
|
|
1374
|
+
const since = await io.cursor();
|
|
1375
|
+
const start = io.now();
|
|
1376
|
+
let res;
|
|
1377
|
+
try {
|
|
1378
|
+
res = await io.poll(since);
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
const giveUp = io.isFatal(err) || io.now() >= cfg.deadline;
|
|
1381
|
+
io.onError(err, giveUp);
|
|
1382
|
+
if (giveUp) return 1;
|
|
1383
|
+
await io.sleep(backoff);
|
|
1384
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
backoff = BACKOFF_START_MS;
|
|
1388
|
+
if (res.events.length > 0) {
|
|
1389
|
+
await io.deliver(res);
|
|
1390
|
+
return 0;
|
|
1391
|
+
}
|
|
1392
|
+
if (res.ended) {
|
|
1393
|
+
io.onEnded?.();
|
|
1394
|
+
return 0;
|
|
1395
|
+
}
|
|
1396
|
+
if (io.now() >= cfg.deadline) {
|
|
1397
|
+
io.onHeartbeat();
|
|
1398
|
+
return 0;
|
|
1399
|
+
}
|
|
1400
|
+
if (io.now() - start < cfg.parkFloorMs) await io.sleep(IDLE_GUARD_MS);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function isFatal(err) {
|
|
1404
|
+
if (err instanceof ChannelError) return err.status === 404;
|
|
1405
|
+
if (err instanceof SyntaxError) return true;
|
|
1406
|
+
return err instanceof Error && err.name === "ZodError";
|
|
1407
|
+
}
|
|
1408
|
+
function pollErrorMessage(err, giveUp) {
|
|
1409
|
+
if (err instanceof ChannelError && err.status === 404) {
|
|
1410
|
+
return "session not found - it may have been removed";
|
|
1411
|
+
}
|
|
1412
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1413
|
+
return giveUp ? `poll failed: ${detail}` : `poll failed, retrying: ${detail}`;
|
|
1414
|
+
}
|
|
1029
1415
|
|
|
1030
1416
|
// packages/cli/src/commands/reply.ts
|
|
1417
|
+
import { stat } from "node:fs/promises";
|
|
1031
1418
|
async function reply(ctx) {
|
|
1032
1419
|
const text = ctx.positionals.join(" ").trim();
|
|
1033
1420
|
const files = filePaths(ctx.flags);
|
|
@@ -1035,6 +1422,15 @@ async function reply(ctx) {
|
|
|
1035
1422
|
throw new UsageError("usage: chamba reply <text> [--file a,b] [--image a,b]");
|
|
1036
1423
|
}
|
|
1037
1424
|
const id = await resolveSessionId(ctx);
|
|
1425
|
+
for (const file of files) {
|
|
1426
|
+
try {
|
|
1427
|
+
const info = await stat(file);
|
|
1428
|
+
if (!info.isFile()) throw new UsageError(`not a file: ${file}`);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (err instanceof UsageError) throw err;
|
|
1431
|
+
throw new UsageError(`file not found: ${file}`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1038
1434
|
const attachmentIds = [];
|
|
1039
1435
|
for (const file of files) {
|
|
1040
1436
|
const attachment = await ctx.client.uploadAttachment(id, file);
|
|
@@ -1053,10 +1449,10 @@ function filePaths(flags) {
|
|
|
1053
1449
|
}
|
|
1054
1450
|
|
|
1055
1451
|
// packages/cli/src/commands/skill.ts
|
|
1056
|
-
import { cpSync, existsSync as
|
|
1452
|
+
import { cpSync, existsSync as existsSync6, rmSync } from "node:fs";
|
|
1057
1453
|
import { homedir } from "node:os";
|
|
1058
|
-
import
|
|
1059
|
-
import { fileURLToPath as
|
|
1454
|
+
import path11 from "node:path";
|
|
1455
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
1060
1456
|
async function skill(ctx) {
|
|
1061
1457
|
if (ctx.positionals[0] !== "install") {
|
|
1062
1458
|
throw new UsageError("usage: chamba skill install [--global]");
|
|
@@ -1067,28 +1463,36 @@ async function skill(ctx) {
|
|
|
1067
1463
|
return 1;
|
|
1068
1464
|
}
|
|
1069
1465
|
const base = ctx.flags.global ? homedir() : process.cwd();
|
|
1070
|
-
const target =
|
|
1466
|
+
const target = path11.join(base, ".claude", "skills", "chamba");
|
|
1467
|
+
rmSync(target, { recursive: true, force: true });
|
|
1071
1468
|
cpSync(source, target, { recursive: true });
|
|
1072
1469
|
note(`installed the chamba skill to ${target}`);
|
|
1073
1470
|
note("reload your agent's skills, then use /chamba");
|
|
1074
1471
|
return 0;
|
|
1075
1472
|
}
|
|
1076
1473
|
function resolveSkillSource() {
|
|
1077
|
-
const here =
|
|
1474
|
+
const here = path11.dirname(fileURLToPath5(import.meta.url));
|
|
1078
1475
|
const candidates = [
|
|
1079
|
-
|
|
1476
|
+
path11.resolve(here, "../skill"),
|
|
1080
1477
|
// packaged: dist/cli.js -> <pkg>/skill
|
|
1081
|
-
|
|
1478
|
+
path11.resolve(here, "../../../../skill")
|
|
1082
1479
|
// dev: packages/cli/src/commands -> <repo>/skill
|
|
1083
1480
|
];
|
|
1084
|
-
return candidates.find((dir) =>
|
|
1481
|
+
return candidates.find((dir) => existsSync6(path11.join(dir, "SKILL.md"))) ?? null;
|
|
1085
1482
|
}
|
|
1086
1483
|
|
|
1087
1484
|
// packages/cli/src/commands/status.ts
|
|
1088
1485
|
async function status(ctx) {
|
|
1486
|
+
if (!ctx.workspaceExists) {
|
|
1487
|
+
note(
|
|
1488
|
+
"no chamba workspace here - run `chamba open` to start one (creates .chamba at the repo root), or `chamba init` to choose where"
|
|
1489
|
+
);
|
|
1490
|
+
return 0;
|
|
1491
|
+
}
|
|
1089
1492
|
const state = await readState(ctx.home);
|
|
1090
|
-
const up = await ctx
|
|
1493
|
+
const up = await serverOwnsHome(ctx);
|
|
1091
1494
|
const rows = up ? await rowsFromServer(ctx) : await rowsFromDisk(ctx);
|
|
1495
|
+
note(`home: ${ctx.home}`);
|
|
1092
1496
|
if (rows.length === 0) {
|
|
1093
1497
|
note("no sessions yet - run `chamba open` to start one");
|
|
1094
1498
|
return 0;
|
|
@@ -1099,6 +1503,14 @@ async function status(ctx) {
|
|
|
1099
1503
|
}
|
|
1100
1504
|
return 0;
|
|
1101
1505
|
}
|
|
1506
|
+
async function serverOwnsHome(ctx) {
|
|
1507
|
+
if (!await ctx.client.isUp()) return false;
|
|
1508
|
+
try {
|
|
1509
|
+
return (await ctx.client.health()).home === ctx.home;
|
|
1510
|
+
} catch {
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1102
1514
|
async function rowsFromServer(ctx) {
|
|
1103
1515
|
const summaries = await ctx.client.list();
|
|
1104
1516
|
return summaries.map((s) => ({ meta: s.meta, presence: s.presence }));
|
|
@@ -1109,14 +1521,61 @@ async function rowsFromDisk(ctx) {
|
|
|
1109
1521
|
return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((meta) => ({ meta }));
|
|
1110
1522
|
}
|
|
1111
1523
|
|
|
1524
|
+
// packages/cli/src/commands/stop.ts
|
|
1525
|
+
async function stop(ctx) {
|
|
1526
|
+
const info = await readServerInfo(ctx.home);
|
|
1527
|
+
if (!info) {
|
|
1528
|
+
note("no chamba server running here");
|
|
1529
|
+
return 0;
|
|
1530
|
+
}
|
|
1531
|
+
const client = new Client(`http://127.0.0.1:${info.port}/`);
|
|
1532
|
+
if (!await ownsHome(client, ctx.home)) {
|
|
1533
|
+
note("no chamba server running here");
|
|
1534
|
+
return 0;
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
process.kill(info.pid, "SIGTERM");
|
|
1538
|
+
} catch {
|
|
1539
|
+
note("no chamba server running here");
|
|
1540
|
+
return 0;
|
|
1541
|
+
}
|
|
1542
|
+
if (await waitUntilDown(client)) {
|
|
1543
|
+
note(`stopped chamba server on :${info.port}`);
|
|
1544
|
+
return 0;
|
|
1545
|
+
}
|
|
1546
|
+
note(`sent stop to the chamba server (pid ${info.pid}) but it did not exit in time`);
|
|
1547
|
+
return 1;
|
|
1548
|
+
}
|
|
1549
|
+
async function ownsHome(client, home) {
|
|
1550
|
+
if (!await client.isUp()) return false;
|
|
1551
|
+
try {
|
|
1552
|
+
return (await client.health()).home === home;
|
|
1553
|
+
} catch {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async function waitUntilDown(client, timeoutMs = 5e3) {
|
|
1558
|
+
const deadline = Date.now() + timeoutMs;
|
|
1559
|
+
while (Date.now() < deadline) {
|
|
1560
|
+
if (!await client.isUp()) return true;
|
|
1561
|
+
await delay2(150);
|
|
1562
|
+
}
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
function delay2(ms) {
|
|
1566
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1112
1569
|
// packages/cli/src/commands/index.ts
|
|
1113
1570
|
var COMMANDS = {
|
|
1114
|
-
|
|
1571
|
+
init,
|
|
1572
|
+
open: open2,
|
|
1115
1573
|
poll,
|
|
1116
1574
|
reply,
|
|
1117
1575
|
end,
|
|
1118
1576
|
attach,
|
|
1119
1577
|
status,
|
|
1578
|
+
stop,
|
|
1120
1579
|
// Spec sessions: structured interview + live artifacts + export.
|
|
1121
1580
|
ask,
|
|
1122
1581
|
decision,
|
|
@@ -1137,21 +1596,40 @@ function runCommand(name, ctx) {
|
|
|
1137
1596
|
}
|
|
1138
1597
|
|
|
1139
1598
|
// packages/cli/src/index.ts
|
|
1599
|
+
var REQUIRES_WORKSPACE = /* @__PURE__ */ new Set([
|
|
1600
|
+
"poll",
|
|
1601
|
+
"reply",
|
|
1602
|
+
"end",
|
|
1603
|
+
"attach",
|
|
1604
|
+
"ask",
|
|
1605
|
+
"decision",
|
|
1606
|
+
"artifact",
|
|
1607
|
+
"export"
|
|
1608
|
+
]);
|
|
1140
1609
|
async function main(argv = process.argv.slice(2)) {
|
|
1141
|
-
const port = resolvePort(argv);
|
|
1142
1610
|
const { positionals, flags } = parseArgs(argv);
|
|
1143
1611
|
if (flags.version && positionals.length === 0) {
|
|
1144
1612
|
out(chambaVersion());
|
|
1145
1613
|
return 0;
|
|
1146
1614
|
}
|
|
1147
1615
|
const command = positionals[0] ?? (flags.help ? "help" : "status");
|
|
1616
|
+
const existing = findExistingHome();
|
|
1617
|
+
const home = existing ?? plannedHome();
|
|
1618
|
+
const port = resolvePort(argv, home);
|
|
1148
1619
|
const ctx = {
|
|
1149
1620
|
client: new Client(`http://127.0.0.1:${port}/`),
|
|
1150
|
-
home
|
|
1621
|
+
home,
|
|
1151
1622
|
port,
|
|
1152
1623
|
flags,
|
|
1153
|
-
positionals: positionals.slice(1)
|
|
1624
|
+
positionals: positionals.slice(1),
|
|
1625
|
+
workspaceExists: existing !== null
|
|
1154
1626
|
};
|
|
1627
|
+
if (!ctx.workspaceExists && REQUIRES_WORKSPACE.has(command)) {
|
|
1628
|
+
note(
|
|
1629
|
+
"no chamba workspace here - run `chamba open` to start one (creates .chamba at the repo root), or `chamba init` to choose where"
|
|
1630
|
+
);
|
|
1631
|
+
return 2;
|
|
1632
|
+
}
|
|
1155
1633
|
try {
|
|
1156
1634
|
return await runCommand(command, ctx);
|
|
1157
1635
|
} catch (err) {
|
|
@@ -1167,16 +1645,39 @@ function reportError(err) {
|
|
|
1167
1645
|
note(`error: ${err.message}`);
|
|
1168
1646
|
return 1;
|
|
1169
1647
|
}
|
|
1170
|
-
|
|
1171
|
-
if (cause?.code === "ECONNREFUSED") {
|
|
1648
|
+
if (errorCode(err) === "ECONNREFUSED") {
|
|
1172
1649
|
note("cannot reach the chamba server - run `chamba open` first");
|
|
1173
1650
|
return 1;
|
|
1174
1651
|
}
|
|
1175
1652
|
note(`error: ${err.message}`);
|
|
1176
1653
|
return 1;
|
|
1177
1654
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1655
|
+
function errorCode(err) {
|
|
1656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1657
|
+
const queue = [err];
|
|
1658
|
+
while (queue.length > 0) {
|
|
1659
|
+
const current = queue.shift();
|
|
1660
|
+
if (!current || typeof current !== "object" || seen.has(current)) continue;
|
|
1661
|
+
seen.add(current);
|
|
1662
|
+
const code = current.code;
|
|
1663
|
+
if (typeof code === "string") return code;
|
|
1664
|
+
const cause = current.cause;
|
|
1665
|
+
if (cause) queue.push(cause);
|
|
1666
|
+
const errors = current.errors;
|
|
1667
|
+
if (Array.isArray(errors)) queue.push(...errors);
|
|
1668
|
+
}
|
|
1669
|
+
return void 0;
|
|
1670
|
+
}
|
|
1671
|
+
function invokedAsEntry() {
|
|
1672
|
+
const entry = process.argv[1];
|
|
1673
|
+
if (entry === void 0) return false;
|
|
1674
|
+
try {
|
|
1675
|
+
return realpathSync2(entry) === realpathSync2(fileURLToPath6(import.meta.url));
|
|
1676
|
+
} catch {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (invokedAsEntry()) {
|
|
1180
1681
|
main().then(
|
|
1181
1682
|
(code) => process.exit(code),
|
|
1182
1683
|
(err) => {
|