@vaka.world/cli 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1236 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import { cancel, confirm, intro, isCancel, log, outro, text } from "@clack/prompts";
6
+
7
+ // src/commands/init.ts
8
+ import { lstat, mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
9
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
10
+ var rules = `version: 1
11
+ checkpoints:
12
+ build:
13
+ required: true
14
+ design:
15
+ required: true
16
+ implementation:
17
+ required: true
18
+ verification:
19
+ local_status: unverified
20
+ `;
21
+ var managedStart = "<!-- BEGIN VAKA MANAGED INSTRUCTIONS -->";
22
+ var managedEnd = "<!-- END VAKA MANAGED INSTRUCTIONS -->";
23
+ var sharedGuidance = `Before implementing a feature, help the developer reason through its behavior, data, and failure cases. Keep understanding, design confirmation, and implementation authorization distinct. Do not write code until the developer explicitly authorizes the scoped implementation; design confirmation alone is not implementation approval. Explain unfamiliar code and why each change is needed.
24
+
25
+ Vaka instructions are guidance, not a technical enforcement boundary. Local session files are unverified; only a server-accepted seal can be described as server-verified.`;
26
+ var instructions = {
27
+ claude: `# Vaka
28
+
29
+ ${sharedGuidance}
30
+ `,
31
+ cursor: `---
32
+ description: Vaka architecture and reasoning guidance
33
+ alwaysApply: true
34
+ ---
35
+
36
+ ${sharedGuidance}
37
+ `,
38
+ copilot: `# Vaka
39
+
40
+ ${sharedGuidance}
41
+ `,
42
+ codex: sharedGuidance,
43
+ windsurfOwnFile: `---
44
+ trigger: always_on
45
+ description: Vaka architecture and reasoning guidance
46
+ ---
47
+
48
+ ${sharedGuidance}
49
+ `,
50
+ windsurfMerge: sharedGuidance,
51
+ clineOwnFile: `# Vaka
52
+
53
+ ${sharedGuidance}
54
+ `,
55
+ clineMerge: sharedGuidance
56
+ };
57
+ function isMissing(error) {
58
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
59
+ }
60
+ function isAlreadyExists(error) {
61
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
62
+ }
63
+ async function inspectPath(path) {
64
+ try {
65
+ const stat = await lstat(path);
66
+ if (stat.isSymbolicLink()) {
67
+ throw new Error(`Refusing to follow symbolic link: ${path}`);
68
+ }
69
+ if (stat.isDirectory()) return "directory";
70
+ if (stat.isFile()) return "file";
71
+ return void 0;
72
+ } catch (error) {
73
+ if (isMissing(error)) return void 0;
74
+ throw error;
75
+ }
76
+ }
77
+ async function detectIntegrations(projectRoot) {
78
+ const [
79
+ cursor,
80
+ claudeManifest,
81
+ claudeDirectory,
82
+ copilotManifest,
83
+ instructionsDirectory,
84
+ codexManifest,
85
+ codexDirectory,
86
+ windsurfDirectory,
87
+ windsurfLegacyFile,
88
+ clinerules
89
+ ] = await Promise.all([
90
+ inspectPath(join(projectRoot, ".cursor")),
91
+ inspectPath(join(projectRoot, "CLAUDE.md")),
92
+ inspectPath(join(projectRoot, ".claude")),
93
+ inspectPath(join(projectRoot, ".github", "copilot-instructions.md")),
94
+ inspectPath(join(projectRoot, ".github", "instructions")),
95
+ inspectPath(join(projectRoot, "AGENTS.md")),
96
+ inspectPath(join(projectRoot, ".codex")),
97
+ inspectPath(join(projectRoot, ".windsurf")),
98
+ inspectPath(join(projectRoot, ".windsurfrules")),
99
+ inspectPath(join(projectRoot, ".clinerules"))
100
+ ]);
101
+ const integrations = [];
102
+ if (cursor === "directory") integrations.push("cursor");
103
+ if (claudeManifest === "file" || claudeDirectory === "directory") {
104
+ integrations.push("claude");
105
+ }
106
+ let copilotDetected = copilotManifest === "file" || instructionsDirectory === "directory";
107
+ const vscodeSettings = join(projectRoot, ".vscode", "settings.json");
108
+ if (!copilotDetected && await inspectPath(vscodeSettings) === "file") {
109
+ const settings = await readFile(vscodeSettings, "utf8");
110
+ copilotDetected = /"github\.copilot\.[^"]+"\s*:/.test(settings);
111
+ }
112
+ if (copilotDetected) integrations.push("copilot");
113
+ if (codexManifest === "file" || codexDirectory === "directory") {
114
+ integrations.push("codex");
115
+ }
116
+ if (windsurfDirectory === "directory" || windsurfLegacyFile === "file") {
117
+ integrations.push("windsurf");
118
+ }
119
+ if (clinerules === "file" || clinerules === "directory") {
120
+ integrations.push("cline");
121
+ }
122
+ return integrations;
123
+ }
124
+ async function ensureDirectory(projectRoot, relativePath) {
125
+ const absolutePath = resolve(projectRoot, relativePath);
126
+ const fromRoot = relative(projectRoot, absolutePath);
127
+ if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
128
+ throw new Error(`Refusing to create a directory outside the project: ${relativePath}`);
129
+ }
130
+ const segments = fromRoot.split(/[/\\]/).filter(Boolean);
131
+ let current = projectRoot;
132
+ for (const segment of segments) {
133
+ current = join(current, segment);
134
+ const existing = await inspectPath(current);
135
+ if (existing === "file") {
136
+ throw new Error(`Expected a directory, found a file: ${current}`);
137
+ }
138
+ if (existing === void 0) {
139
+ try {
140
+ await mkdir(current, { mode: segment === ".vaka" ? 448 : 493 });
141
+ } catch (error) {
142
+ if (!isAlreadyExists(error)) throw error;
143
+ }
144
+ if (await inspectPath(current) !== "directory") {
145
+ throw new Error(`Could not safely create directory: ${current}`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ async function createIfMissing(projectRoot, relativePath, contents) {
151
+ const absolutePath = resolve(projectRoot, relativePath);
152
+ const parent = relative(projectRoot, absolutePath).split(/[/\\]/).slice(0, -1).join("/");
153
+ if (parent) await ensureDirectory(projectRoot, parent);
154
+ try {
155
+ await writeFile(absolutePath, contents, {
156
+ encoding: "utf8",
157
+ flag: "wx",
158
+ mode: 420
159
+ });
160
+ return "created";
161
+ } catch (error) {
162
+ if (isAlreadyExists(error)) return "skipped";
163
+ throw error;
164
+ }
165
+ }
166
+ function wrapManagedSection(body) {
167
+ return `${managedStart}
168
+ ${body}
169
+ ${managedEnd}`;
170
+ }
171
+ function mergeManagedSection(existing, body) {
172
+ const section = wrapManagedSection(body);
173
+ const start = existing.indexOf(managedStart);
174
+ const end = existing.indexOf(managedEnd);
175
+ if (start === -1 && end === -1) {
176
+ const separator = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
177
+ return `${existing}${separator}${section}
178
+ `;
179
+ }
180
+ if (start === -1 || end === -1 || end < start || existing.indexOf(managedStart, start + managedStart.length) !== -1 || existing.indexOf(managedEnd, end + managedEnd.length) !== -1) {
181
+ throw new Error("File has malformed Vaka instruction markers; refusing to modify it.");
182
+ }
183
+ const managedEndIndex = end + managedEnd.length;
184
+ return `${existing.slice(0, start)}${section}${existing.slice(managedEndIndex)}`;
185
+ }
186
+ async function mergeIntoFile(projectRoot, relativePath, sectionBody) {
187
+ const absolutePath = resolve(projectRoot, relativePath);
188
+ const parent = relative(projectRoot, absolutePath).split(/[/\\]/).slice(0, -1).join("/");
189
+ if (parent) await ensureDirectory(projectRoot, parent);
190
+ const pathType = await inspectPath(absolutePath);
191
+ if (pathType === void 0) {
192
+ try {
193
+ await writeFile(absolutePath, `${wrapManagedSection(sectionBody)}
194
+ `, {
195
+ encoding: "utf8",
196
+ flag: "wx",
197
+ mode: 420
198
+ });
199
+ return "created";
200
+ } catch (error) {
201
+ if (!isAlreadyExists(error)) throw error;
202
+ }
203
+ } else if (pathType !== "file") {
204
+ throw new Error(`Expected a regular file, found a directory: ${absolutePath}`);
205
+ }
206
+ const stat = await lstat(absolutePath);
207
+ if (!stat.isFile() || stat.isSymbolicLink()) {
208
+ throw new Error(`Expected a regular file: ${absolutePath}`);
209
+ }
210
+ const existing = await readFile(absolutePath, "utf8");
211
+ const updated = mergeManagedSection(existing, sectionBody);
212
+ if (updated === existing) return "skipped";
213
+ const temporaryPath = join(
214
+ dirname(absolutePath),
215
+ `.${basename(absolutePath)}.${crypto.randomUUID()}.tmp`
216
+ );
217
+ try {
218
+ await writeFile(temporaryPath, updated, {
219
+ encoding: "utf8",
220
+ flag: "wx",
221
+ mode: stat.mode & 511
222
+ });
223
+ await rename(temporaryPath, absolutePath);
224
+ } catch (error) {
225
+ try {
226
+ await unlink(temporaryPath);
227
+ } catch (cleanupError) {
228
+ if (!isMissing(cleanupError)) {
229
+ throw new AggregateError(
230
+ [error, cleanupError],
231
+ `Could not update ${absolutePath} or clean up ${temporaryPath}.`
232
+ );
233
+ }
234
+ }
235
+ throw error;
236
+ }
237
+ return "updated";
238
+ }
239
+ async function resolveWindsurfTarget(projectRoot) {
240
+ const directory = await inspectPath(join(projectRoot, ".windsurf"));
241
+ if (directory === "directory") {
242
+ return { mode: "own-file", relativePath: ".windsurf/rules/vaka.md" };
243
+ }
244
+ return { mode: "merge", relativePath: ".windsurfrules" };
245
+ }
246
+ async function resolveClineTarget(projectRoot) {
247
+ const existing = await inspectPath(join(projectRoot, ".clinerules"));
248
+ if (existing === "directory") {
249
+ return { mode: "own-file", relativePath: ".clinerules/vaka.md" };
250
+ }
251
+ return { mode: "merge", relativePath: ".clinerules" };
252
+ }
253
+ async function initializeProject(projectPath) {
254
+ const projectRoot = resolve(projectPath);
255
+ if (await inspectPath(projectRoot) !== "directory") {
256
+ throw new Error(`Project path is not a directory: ${projectRoot}`);
257
+ }
258
+ const integrations = await detectIntegrations(projectRoot);
259
+ const result = { integrations, created: [], updated: [], skipped: [] };
260
+ const ownFiles = [
261
+ { relativePath: ".vaka/rules.yml", contents: rules }
262
+ ];
263
+ if (integrations.includes("claude")) {
264
+ ownFiles.push({ relativePath: "CLAUDE.md", contents: instructions.claude });
265
+ }
266
+ if (integrations.includes("cursor")) {
267
+ ownFiles.push({ relativePath: ".cursor/rules/vaka.mdc", contents: instructions.cursor });
268
+ }
269
+ if (integrations.includes("copilot")) {
270
+ ownFiles.push({
271
+ relativePath: ".github/copilot-instructions.md",
272
+ contents: instructions.copilot
273
+ });
274
+ }
275
+ for (const file of ownFiles) {
276
+ const status = await createIfMissing(projectRoot, file.relativePath, file.contents);
277
+ result[status].push(file.relativePath);
278
+ }
279
+ if (integrations.includes("codex")) {
280
+ const status = await mergeIntoFile(projectRoot, "AGENTS.md", instructions.codex);
281
+ result[status].push("AGENTS.md");
282
+ }
283
+ if (integrations.includes("windsurf")) {
284
+ const target = await resolveWindsurfTarget(projectRoot);
285
+ const status = target.mode === "own-file" ? await createIfMissing(projectRoot, target.relativePath, instructions.windsurfOwnFile) : await mergeIntoFile(projectRoot, target.relativePath, instructions.windsurfMerge);
286
+ result[status].push(target.relativePath);
287
+ }
288
+ if (integrations.includes("cline")) {
289
+ const target = await resolveClineTarget(projectRoot);
290
+ const status = target.mode === "own-file" ? await createIfMissing(projectRoot, target.relativePath, instructions.clineOwnFile) : await mergeIntoFile(projectRoot, target.relativePath, instructions.clineMerge);
291
+ result[status].push(target.relativePath);
292
+ }
293
+ return result;
294
+ }
295
+
296
+ // src/commands/line-prompter.ts
297
+ import { createInterface } from "readline";
298
+
299
+ // ../../packages/core/dist/session.js
300
+ import { createHash, randomUUID } from "crypto";
301
+ import { lstat as lstat2, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
302
+ import { join as join2, resolve as resolve2 } from "path";
303
+ function canonicalize(value, ancestors) {
304
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
305
+ return JSON.stringify(value);
306
+ }
307
+ if (typeof value === "number") {
308
+ if (!Number.isFinite(value))
309
+ throw new TypeError("Session payload numbers must be finite.");
310
+ return JSON.stringify(value);
311
+ }
312
+ if (Array.isArray(value)) {
313
+ if (ancestors.has(value))
314
+ throw new TypeError("Session payload cannot contain cycles.");
315
+ ancestors.add(value);
316
+ const serialized = `[${value.map((item) => canonicalize(item, ancestors)).join(",")}]`;
317
+ ancestors.delete(value);
318
+ return serialized;
319
+ }
320
+ if (typeof value === "object") {
321
+ const object = value;
322
+ const prototype = Object.getPrototypeOf(object);
323
+ if (prototype !== Object.prototype && prototype !== null) {
324
+ throw new TypeError("Session payload must contain only plain JSON objects.");
325
+ }
326
+ if (ancestors.has(object))
327
+ throw new TypeError("Session payload cannot contain cycles.");
328
+ ancestors.add(object);
329
+ const serialized = `{${Object.keys(object).sort().map((key) => {
330
+ const item = object[key];
331
+ if (item === void 0) {
332
+ throw new TypeError("Session payload cannot contain undefined values.");
333
+ }
334
+ return `${JSON.stringify(key)}:${canonicalize(item, ancestors)}`;
335
+ }).join(",")}}`;
336
+ ancestors.delete(object);
337
+ return serialized;
338
+ }
339
+ throw new TypeError("Session payload must be valid JSON.");
340
+ }
341
+ function canonicalJson(value) {
342
+ return canonicalize(value, /* @__PURE__ */ new WeakSet());
343
+ }
344
+ function isCheckpointName(value) {
345
+ return value === "build" || value === "design" || value === "implementation";
346
+ }
347
+ function isEventActor(value) {
348
+ return value === "developer" || value === "assistant";
349
+ }
350
+ function isEventType(value) {
351
+ return value === "checkpoint-question" || value === "checkpoint-answer" || value === "design-confirmed" || value === "implementation-approved";
352
+ }
353
+ function hasExactKeys(value, keys) {
354
+ const actual = Object.keys(value).sort();
355
+ const expected = [...keys].sort();
356
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
357
+ }
358
+ function hashEvent(sessionId, event) {
359
+ const content = canonicalJson({
360
+ sessionId,
361
+ sequence: event.sequence,
362
+ checkpoint: event.checkpoint,
363
+ actor: event.actor,
364
+ type: event.type,
365
+ payload: event.payload,
366
+ recordedAt: event.recordedAt,
367
+ previousHash: event.previousHash
368
+ });
369
+ return createHash("sha256").update(content, "utf8").digest("hex");
370
+ }
371
+ function createLocalSession(options = {}) {
372
+ return {
373
+ schemaVersion: 1,
374
+ id: options.id ?? randomUUID(),
375
+ provenance: "local",
376
+ status: "unverified",
377
+ createdAt: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
378
+ events: []
379
+ };
380
+ }
381
+ function verifySessionIntegrity(session) {
382
+ if (typeof session !== "object" || session === null) {
383
+ return { valid: false, reason: "Session must be an object." };
384
+ }
385
+ const candidate = session;
386
+ if (!hasExactKeys(candidate, [
387
+ "schemaVersion",
388
+ "id",
389
+ "provenance",
390
+ "status",
391
+ "createdAt",
392
+ "events"
393
+ ]) || candidate.schemaVersion !== 1 || typeof candidate.id !== "string" || candidate.id.length === 0 || candidate.provenance !== "local" || candidate.status !== "unverified" || typeof candidate.createdAt !== "string" || Number.isNaN(Date.parse(candidate.createdAt)) || !Array.isArray(candidate.events)) {
394
+ return { valid: false, reason: "Session metadata is invalid or has an unsupported version." };
395
+ }
396
+ let previousHash = null;
397
+ let previousRecordedAt = candidate.createdAt;
398
+ for (let index = 0; index < candidate.events.length; index += 1) {
399
+ const event = candidate.events[index];
400
+ if (typeof event !== "object" || event === null || !hasExactKeys(event, [
401
+ "sequence",
402
+ "checkpoint",
403
+ "actor",
404
+ "type",
405
+ "payload",
406
+ "recordedAt",
407
+ "previousHash",
408
+ "hash"
409
+ ]) || event.sequence !== index || !isCheckpointName(event.checkpoint) || !isEventActor(event.actor) || !isEventType(event.type) || typeof event.recordedAt !== "string" || Number.isNaN(Date.parse(event.recordedAt)) || event.previousHash !== previousHash || typeof event.hash !== "string" || !/^[a-f0-9]{64}$/.test(event.hash)) {
410
+ return { valid: false, eventIndex: index, reason: "Event shape or sequence is invalid." };
411
+ }
412
+ if (Date.parse(event.recordedAt) < Date.parse(previousRecordedAt)) {
413
+ return {
414
+ valid: false,
415
+ eventIndex: index,
416
+ reason: "Event timestamp is earlier than a prior event or the session's creation time."
417
+ };
418
+ }
419
+ try {
420
+ const { hash, ...eventWithoutHash } = event;
421
+ if (hashEvent(candidate.id, eventWithoutHash) !== hash) {
422
+ return { valid: false, eventIndex: index, reason: "Event hash does not match its contents." };
423
+ }
424
+ } catch (error) {
425
+ return {
426
+ valid: false,
427
+ eventIndex: index,
428
+ reason: error instanceof Error ? error.message : String(error)
429
+ };
430
+ }
431
+ previousRecordedAt = event.recordedAt;
432
+ previousHash = event.hash;
433
+ }
434
+ return { valid: true };
435
+ }
436
+ function appendSessionEvent(session, input, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
437
+ const integrity = verifySessionIntegrity(session);
438
+ if (!integrity.valid) {
439
+ throw new Error(`Cannot append to an invalid local session${integrity.eventIndex === void 0 ? "" : ` at event ${integrity.eventIndex}`}: ${integrity.reason}`);
440
+ }
441
+ if (!isCheckpointName(input.checkpoint) || !isEventActor(input.actor) || !isEventType(input.type)) {
442
+ throw new TypeError("Session event has an unsupported checkpoint, actor, or type.");
443
+ }
444
+ if (Number.isNaN(Date.parse(recordedAt))) {
445
+ throw new TypeError("Session event timestamp must be a valid date.");
446
+ }
447
+ const previousRecordedAt = session.events.at(-1)?.recordedAt ?? session.createdAt;
448
+ if (Date.parse(recordedAt) < Date.parse(previousRecordedAt)) {
449
+ throw new RangeError("Session event timestamp cannot be earlier than the previous event or the session's creation time.");
450
+ }
451
+ const previousHash = session.events.at(-1)?.hash ?? null;
452
+ const eventWithoutHash = {
453
+ sequence: session.events.length,
454
+ checkpoint: input.checkpoint,
455
+ actor: input.actor,
456
+ type: input.type,
457
+ payload: input.payload,
458
+ recordedAt,
459
+ previousHash
460
+ };
461
+ const event = {
462
+ ...eventWithoutHash,
463
+ hash: hashEvent(session.id, eventWithoutHash)
464
+ };
465
+ return { ...session, events: [...session.events, event] };
466
+ }
467
+ function isMissing2(error) {
468
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
469
+ }
470
+ async function ensureStateDirectory(projectRoot) {
471
+ const root = resolve2(projectRoot);
472
+ const rootStat = await lstat2(root);
473
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
474
+ throw new Error(`Project path is not a safe directory: ${root}`);
475
+ }
476
+ const stateDirectory = join2(root, ".vaka");
477
+ try {
478
+ const stateStat = await lstat2(stateDirectory);
479
+ if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) {
480
+ throw new Error(`Vaka state path must be a real directory: ${stateDirectory}`);
481
+ }
482
+ } catch (error) {
483
+ if (!isMissing2(error))
484
+ throw error;
485
+ try {
486
+ await mkdir2(stateDirectory, { mode: 448 });
487
+ } catch (createError) {
488
+ if (!(typeof createError === "object" && createError !== null && "code" in createError && createError.code === "EEXIST")) {
489
+ throw createError;
490
+ }
491
+ }
492
+ const stateStat = await lstat2(stateDirectory);
493
+ if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) {
494
+ throw new Error(`Vaka state path must be a real directory: ${stateDirectory}`);
495
+ }
496
+ }
497
+ return stateDirectory;
498
+ }
499
+ async function inspectStateDirectory(projectRoot) {
500
+ const root = resolve2(projectRoot);
501
+ const rootStat = await lstat2(root);
502
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
503
+ throw new Error(`Project path is not a safe directory: ${root}`);
504
+ }
505
+ const stateDirectory = join2(root, ".vaka");
506
+ const stateStat = await lstat2(stateDirectory);
507
+ if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) {
508
+ throw new Error(`Vaka state path must be a real directory: ${stateDirectory}`);
509
+ }
510
+ return stateDirectory;
511
+ }
512
+ async function saveLocalSession(projectRoot, session) {
513
+ const integrity = verifySessionIntegrity(session);
514
+ if (!integrity.valid) {
515
+ throw new Error(`Refusing to save an invalid local session: ${integrity.reason}`);
516
+ }
517
+ const stateDirectory = await ensureStateDirectory(projectRoot);
518
+ const sessionPath = join2(stateDirectory, "session.json");
519
+ try {
520
+ const existing = await lstat2(sessionPath);
521
+ if (existing.isSymbolicLink() || !existing.isFile()) {
522
+ throw new Error(`Session path must be a regular file: ${sessionPath}`);
523
+ }
524
+ } catch (error) {
525
+ if (!isMissing2(error))
526
+ throw error;
527
+ }
528
+ const temporaryPath = join2(stateDirectory, `session.${randomUUID()}.tmp`);
529
+ try {
530
+ await writeFile2(temporaryPath, `${JSON.stringify(session, null, 2)}
531
+ `, {
532
+ encoding: "utf8",
533
+ flag: "wx",
534
+ mode: 384
535
+ });
536
+ await rename2(temporaryPath, sessionPath);
537
+ } catch (error) {
538
+ try {
539
+ await unlink2(temporaryPath);
540
+ } catch (cleanupError) {
541
+ if (!isMissing2(cleanupError)) {
542
+ throw new AggregateError([error, cleanupError], `Could not save the session or clean up ${temporaryPath}.`);
543
+ }
544
+ }
545
+ throw error;
546
+ }
547
+ }
548
+ async function loadLocalSession(projectRoot) {
549
+ const stateDirectory = await inspectStateDirectory(projectRoot);
550
+ const sessionPath = join2(stateDirectory, "session.json");
551
+ const stat = await lstat2(sessionPath);
552
+ if (!stat.isFile() || stat.isSymbolicLink()) {
553
+ throw new Error(`Session path must be a regular file: ${sessionPath}`);
554
+ }
555
+ let parsed;
556
+ try {
557
+ parsed = JSON.parse(await readFile2(sessionPath, "utf8"));
558
+ } catch (error) {
559
+ if (error instanceof SyntaxError) {
560
+ throw new Error(`Session JSON is invalid: ${sessionPath}`, { cause: error });
561
+ }
562
+ throw error;
563
+ }
564
+ const integrity = verifySessionIntegrity(parsed);
565
+ if (!integrity.valid) {
566
+ throw new Error(`Local session integrity check failed: ${integrity.reason}`);
567
+ }
568
+ return parsed;
569
+ }
570
+ async function tryLoadLocalSession(projectRoot) {
571
+ try {
572
+ return await loadLocalSession(projectRoot);
573
+ } catch (error) {
574
+ if (isMissing2(error))
575
+ return void 0;
576
+ throw error;
577
+ }
578
+ }
579
+ async function loadOrCreateLocalSession(projectRoot) {
580
+ return await tryLoadLocalSession(projectRoot) ?? createLocalSession();
581
+ }
582
+
583
+ // ../../packages/core/dist/checkpoints.js
584
+ var CHECKPOINT_QUESTIONS = [
585
+ {
586
+ id: "build-goal",
587
+ checkpoint: "build",
588
+ prompt: "What is the goal of this feature, in one or two sentences?"
589
+ },
590
+ {
591
+ id: "build-flow",
592
+ checkpoint: "build",
593
+ prompt: "What are the system boundaries and state transitions \u2014 what talks to what, and where does this sit in the flow?"
594
+ },
595
+ {
596
+ id: "build-data",
597
+ checkpoint: "build",
598
+ prompt: "What data does this touch, and why these structures, relations, and indexes?"
599
+ },
600
+ {
601
+ id: "design-edge-cases",
602
+ checkpoint: "design",
603
+ prompt: "What edge cases could break this \u2014 concurrency, idempotency, timeouts, or network failure \u2014 and how are they handled?"
604
+ }
605
+ ];
606
+ var DEFAULT_RULES = {
607
+ checkpoints: {
608
+ build: { required: true },
609
+ design: { required: true },
610
+ implementation: { required: true }
611
+ }
612
+ };
613
+ var CHECKPOINT_ORDER = ["build", "design", "implementation"];
614
+ function questionsFor(checkpoint) {
615
+ return CHECKPOINT_QUESTIONS.filter((question) => question.checkpoint === checkpoint);
616
+ }
617
+ function lastIndexOfType(events, type) {
618
+ for (let index = events.length - 1; index >= 0; index -= 1) {
619
+ if (events[index]?.type === type)
620
+ return index;
621
+ }
622
+ return -1;
623
+ }
624
+ function answeredQuestionIds(cycle, checkpoint) {
625
+ const answered = /* @__PURE__ */ new Set();
626
+ for (const event of cycle) {
627
+ if (event.checkpoint !== checkpoint || event.type !== "checkpoint-answer")
628
+ continue;
629
+ const payload = event.payload;
630
+ if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
631
+ const questionId = payload.questionId;
632
+ if (typeof questionId === "string")
633
+ answered.add(questionId);
634
+ }
635
+ }
636
+ return answered;
637
+ }
638
+ function currentCycleEvents(events) {
639
+ const lastApprovalIndex = lastIndexOfType(events, "implementation-approved");
640
+ return events.slice(lastApprovalIndex + 1);
641
+ }
642
+ function resolveResumeState(events, rules2) {
643
+ const cycle = currentCycleEvents(events);
644
+ for (const checkpoint of CHECKPOINT_ORDER) {
645
+ if (!rules2.checkpoints[checkpoint].required)
646
+ continue;
647
+ const answered = answeredQuestionIds(cycle, checkpoint);
648
+ const nextQuestion = questionsFor(checkpoint).find((question) => !answered.has(question.id));
649
+ if (nextQuestion) {
650
+ return { kind: "ask-question", question: nextQuestion };
651
+ }
652
+ if (checkpoint === "design" && lastIndexOfType(cycle, "design-confirmed") === -1) {
653
+ return { kind: "confirm-design" };
654
+ }
655
+ if (checkpoint === "implementation" && lastIndexOfType(cycle, "implementation-approved") === -1) {
656
+ return { kind: "authorize-implementation" };
657
+ }
658
+ }
659
+ return { kind: "cycle-complete" };
660
+ }
661
+
662
+ // ../../packages/core/dist/rules.js
663
+ import { lstat as lstat3, readFile as readFile3 } from "fs/promises";
664
+ import { join as join3, resolve as resolve3 } from "path";
665
+ import { parse as parseYaml } from "yaml";
666
+ var CHECKPOINT_NAMES = ["build", "design", "implementation"];
667
+ function isMissing3(error) {
668
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
669
+ }
670
+ function parseRulesDocument(path, text2) {
671
+ let parsed;
672
+ try {
673
+ parsed = parseYaml(text2);
674
+ } catch (error) {
675
+ throw new Error(`Could not parse ${path} as YAML.`, { cause: error });
676
+ }
677
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
678
+ throw new Error(`${path} must contain a YAML mapping at the top level.`);
679
+ }
680
+ const document = parsed;
681
+ const checkpointsField = document.checkpoints;
682
+ if (typeof checkpointsField !== "object" || checkpointsField === null || Array.isArray(checkpointsField)) {
683
+ throw new Error(`${path} is missing a valid top-level "checkpoints" mapping.`);
684
+ }
685
+ const checkpointsDocument = checkpointsField;
686
+ const checkpoints = {};
687
+ for (const name of CHECKPOINT_NAMES) {
688
+ const entry = checkpointsDocument[name];
689
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry) || typeof entry.required !== "boolean") {
690
+ throw new Error(`${path} is missing a boolean "checkpoints.${name}.required" value.`);
691
+ }
692
+ checkpoints[name] = { required: entry.required };
693
+ }
694
+ return { checkpoints };
695
+ }
696
+ async function loadRulesConfig(projectRoot) {
697
+ const root = resolve3(projectRoot);
698
+ const rulesPath = join3(root, ".vaka", "rules.yml");
699
+ let stat;
700
+ try {
701
+ stat = await lstat3(rulesPath);
702
+ } catch (error) {
703
+ if (isMissing3(error))
704
+ return DEFAULT_RULES;
705
+ throw error;
706
+ }
707
+ if (!stat.isFile() || stat.isSymbolicLink()) {
708
+ throw new Error(`Vaka rules path must be a regular file: ${rulesPath}`);
709
+ }
710
+ const text2 = await readFile3(rulesPath, "utf8");
711
+ return parseRulesDocument(rulesPath, text2);
712
+ }
713
+
714
+ // src/commands/start.ts
715
+ var CANCELLED = Symbol("cancelled");
716
+ async function runStart(projectRoot, prompter) {
717
+ const rules2 = await loadRulesConfig(projectRoot);
718
+ let session = await loadOrCreateLocalSession(projectRoot);
719
+ for (; ; ) {
720
+ const state = resolveResumeState(session.events, rules2);
721
+ if (state.kind === "cycle-complete") {
722
+ return { kind: "nothing-required" };
723
+ }
724
+ if (state.kind === "ask-question") {
725
+ session = appendSessionEvent(session, {
726
+ checkpoint: state.question.checkpoint,
727
+ actor: "assistant",
728
+ type: "checkpoint-question",
729
+ payload: { questionId: state.question.id, prompt: state.question.prompt }
730
+ });
731
+ await saveLocalSession(projectRoot, session);
732
+ const answer = await prompter.askQuestion(state.question);
733
+ if (answer === CANCELLED) return { kind: "cancelled" };
734
+ session = appendSessionEvent(session, {
735
+ checkpoint: state.question.checkpoint,
736
+ actor: "developer",
737
+ type: "checkpoint-answer",
738
+ payload: { questionId: state.question.id, answer }
739
+ });
740
+ await saveLocalSession(projectRoot, session);
741
+ continue;
742
+ }
743
+ if (state.kind === "confirm-design") {
744
+ const confirmed = await prompter.askConfirm(
745
+ "Does this design look right to proceed with?"
746
+ );
747
+ if (confirmed === CANCELLED) return { kind: "cancelled" };
748
+ if (!confirmed) return { kind: "design-declined" };
749
+ session = appendSessionEvent(session, {
750
+ checkpoint: "design",
751
+ actor: "developer",
752
+ type: "design-confirmed",
753
+ payload: {}
754
+ });
755
+ await saveLocalSession(projectRoot, session);
756
+ continue;
757
+ }
758
+ const approved = await prompter.askConfirm(
759
+ "Authorize implementation of exactly this scoped change?"
760
+ );
761
+ if (approved === CANCELLED) return { kind: "cancelled" };
762
+ if (!approved) return { kind: "implementation-declined" };
763
+ session = appendSessionEvent(session, {
764
+ checkpoint: "implementation",
765
+ actor: "developer",
766
+ type: "implementation-approved",
767
+ payload: {}
768
+ });
769
+ await saveLocalSession(projectRoot, session);
770
+ return { kind: "implementation-approved" };
771
+ }
772
+ }
773
+
774
+ // src/commands/line-prompter.ts
775
+ function createLineReaderPrompter(input = process.stdin, output = process.stdout) {
776
+ const rl = createInterface({ input, terminal: false });
777
+ const lines = rl[Symbol.asyncIterator]();
778
+ async function nextLine() {
779
+ const { value, done } = await lines.next();
780
+ return done ? void 0 : value;
781
+ }
782
+ return {
783
+ async askQuestion(question) {
784
+ output.write(`${question.prompt}
785
+ > `);
786
+ const line = await nextLine();
787
+ if (line === void 0) return CANCELLED;
788
+ const answer = line.trim();
789
+ if (answer.length === 0) {
790
+ throw new Error(`Expected a non-empty answer for: ${question.prompt}`);
791
+ }
792
+ return answer;
793
+ },
794
+ async askConfirm(message) {
795
+ output.write(`${message} (y/n)
796
+ > `);
797
+ const line = await nextLine();
798
+ if (line === void 0) return CANCELLED;
799
+ const normalized = line.trim().toLowerCase();
800
+ if (normalized === "y" || normalized === "yes") return true;
801
+ if (normalized === "n" || normalized === "no") return false;
802
+ throw new Error(`Expected "y" or "n" for: ${message} (got ${JSON.stringify(line)})`);
803
+ },
804
+ close() {
805
+ rl.close();
806
+ }
807
+ };
808
+ }
809
+
810
+ // src/commands/status.ts
811
+ async function runStatus(projectRoot) {
812
+ const rules2 = await loadRulesConfig(projectRoot);
813
+ const session = await tryLoadLocalSession(projectRoot);
814
+ const events = session?.events ?? [];
815
+ return {
816
+ sessionExists: session !== void 0,
817
+ totalEvents: events.length,
818
+ completedCycles: events.filter((event) => event.type === "implementation-approved").length,
819
+ resumeState: resolveResumeState(events, rules2)
820
+ };
821
+ }
822
+
823
+ // src/ui/banner.ts
824
+ import readline from "readline";
825
+
826
+ // src/ui/terminal-theme.ts
827
+ function detectTerminalTheme(env = process.env) {
828
+ const override = env.VAKA_THEME?.toLowerCase();
829
+ if (override === "light" || override === "dark") {
830
+ return override;
831
+ }
832
+ const colorFgBg = env.COLORFGBG;
833
+ if (colorFgBg) {
834
+ const bg = Number.parseInt(colorFgBg.split(";").pop() ?? "", 10);
835
+ if (!Number.isNaN(bg)) {
836
+ return bg === 7 || bg >= 9 ? "light" : "dark";
837
+ }
838
+ }
839
+ return "dark";
840
+ }
841
+ function supportsColor(stream = process.stdout, env = process.env) {
842
+ if (env.NO_COLOR !== void 0) {
843
+ return false;
844
+ }
845
+ if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") {
846
+ return true;
847
+ }
848
+ if (!stream.isTTY) {
849
+ return false;
850
+ }
851
+ return env.TERM !== "dumb";
852
+ }
853
+
854
+ // src/ui/banner.ts
855
+ var BOLD = "\x1B[1m";
856
+ var RESET = "\x1B[0m";
857
+ var HIDE_CURSOR = "\x1B[?25l";
858
+ var SHOW_CURSOR = "\x1B[?25h";
859
+ function rgb(hex) {
860
+ const r = Number.parseInt(hex.slice(1, 3), 16);
861
+ const g = Number.parseInt(hex.slice(3, 5), 16);
862
+ const b = Number.parseInt(hex.slice(5, 7), 16);
863
+ return `\x1B[38;2;${r};${g};${b}m`;
864
+ }
865
+ function stripAnsi(input) {
866
+ return input.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
867
+ }
868
+ var PALETTES = {
869
+ dark: {
870
+ border: rgb("#2C303D"),
871
+ // --ink-border
872
+ text: rgb("#F3F4F7"),
873
+ // --ink-text
874
+ muted: rgb("#9AA1B2"),
875
+ // --ink-muted (the dark-canvas muted token, not --muted-foreground)
876
+ ring: rgb("#9DB0E8"),
877
+ // --accent-light ("numerals on dark")
878
+ core: rgb("#2E478C")
879
+ // --primary
880
+ },
881
+ light: {
882
+ border: rgb("#E5E6EB"),
883
+ // --border
884
+ text: rgb("#1B1D26"),
885
+ // --foreground
886
+ muted: rgb("#70747F"),
887
+ // --muted-foreground
888
+ ring: rgb("#8E9AC2"),
889
+ // --secondary-indigo (the light-canvas pair for --accent-light)
890
+ core: rgb("#2E478C")
891
+ // --primary
892
+ }
893
+ };
894
+ var SEED_FRAMES = [
895
+ // Frame 0: The Origin (Point of Intention)
896
+ [
897
+ ` \xB7 `,
898
+ ` `,
899
+ ` \u25CF `,
900
+ ` `,
901
+ ` \xB7 `
902
+ ],
903
+ // Frame 1: The Central Sphere (First Circle of Reasoning)
904
+ [
905
+ ` . \u2500\u2500\u2500 . `,
906
+ ` / \\ `,
907
+ ` | \u25C8 | `,
908
+ ` \\ / `,
909
+ ` ' \u2500\u2500\u2500 ' `
910
+ ],
911
+ // Frame 2: The Vertical Polar Axis
912
+ [
913
+ ` . \u2500\u2500\u2500 . `,
914
+ ` \u256D\u2500/\u2500\u2500\u2500\u252C\u2500\u2500\u2500\\\u2500\u256E `,
915
+ ` \u2502\u2502 \u2502 \u2502\u2502 `,
916
+ ` \u2570\u2500\\\u2500\u2500\u2500\u2534\u2500\u2500\u2500/\u2500\u256F `,
917
+ ` ' \u2500\u2500\u2500 ' `
918
+ ],
919
+ // Frame 3: Triad Intersection (State, Schema, Edge Checkpoints)
920
+ [
921
+ ` . \u2500\u2500\u2500 . `,
922
+ ` \u256D\u2500/\u2500\u2500\u2500\u252C\u2500\u2500\u2500\\\u2500\u256E `,
923
+ ` ( ( \u25F8 \u253C \u25F9 ) ) `,
924
+ ` \u2570\u2500\\\u2500\u2500\u2500\u2534\u2500\u2500\u2500/\u2500\u256F `,
925
+ ` ' \u2500\u2500\u2500 ' `
926
+ ],
927
+ // Frame 4: Hexagonal Petals Emerge
928
+ [
929
+ ` .\u2500\u2500. .\u2500\u2500. `,
930
+ ` / \u256D\u2500\u2500\\ /\u2500\u2500\u256E \\ `,
931
+ ` | ( \u25F8 \u2715 \u25F9 ) | `,
932
+ ` \\ \u2570\u2500\u2500/ \\\u2500\u2500\u256F / `,
933
+ ` '\u2500\u2500' '\u2500\u2500' `
934
+ ],
935
+ // Frame 5: Fully Assembled Seed of Life
936
+ [
937
+ ` .\u2500\u2500. \u25B2 .\u2500\u2500. `,
938
+ ` \u256D\u2500( \u25F8 \u2572 \u2502 / \u25F9 )\u2500\u256E `,
939
+ ` \u2502 \u2572\u2500\u2500\u2500 \u25C8 \u2500\u2500\u2500\u2571 \u2502 `,
940
+ ` \u2570\u2500( \u25FA \u2571 \u2502 \\ \u25FF )\u2500\u256F `,
941
+ ` '\u2500\u2500' \u25BC '\u2500\u2500' `
942
+ ],
943
+ // Frame 6: Harmonic Pulse Lock
944
+ [
945
+ ` .\u2500\u2500. \u2726 .\u2500\u2500. `,
946
+ ` \u256D\u2500( \u25F8 \u2572 \u2502 / \u25F9 )\u2500\u256E `,
947
+ ` \u2502 \u2572\u2500\u2500\u2500 \u25C8 \u2500\u2500\u2500\u2571 \u2502 `,
948
+ ` \u2570\u2500( \u25FA \u2571 \u2502 \\ \u25FF )\u2500\u256F `,
949
+ ` '\u2500\u2500' \u2726 '\u2500\u2500' `
950
+ ]
951
+ ];
952
+ var FINAL_FRAME = SEED_FRAMES.length - 1;
953
+ function center(text2, width) {
954
+ const totalPad = Math.max(width - text2.length, 0);
955
+ const left = Math.floor(totalPad / 2);
956
+ return " ".repeat(left) + text2 + " ".repeat(totalPad - left);
957
+ }
958
+ function padEndSafe(text2, width) {
959
+ return text2.length >= width ? text2 : text2 + " ".repeat(width - text2.length);
960
+ }
961
+ function buildVakaCard(frameIndex, isPulsing, theme, colorEnabled, version) {
962
+ const palette = PALETTES[theme];
963
+ const glyph = SEED_FRAMES[frameIndex] ?? SEED_FRAMES[FINAL_FRAME];
964
+ const ringColor = isPulsing ? palette.text : palette.ring;
965
+ const coreColor = isPulsing ? palette.ring : palette.core;
966
+ const title = "YOUR MIND IS THE ARCHITECT";
967
+ const subtitle = "Architecture checkpoints for engineers.";
968
+ const footer = `v${version} \xB7 Local checkpoints: unverified`;
969
+ const innerWidth = Math.max(
970
+ ...glyph.map((line) => line.length),
971
+ title.length,
972
+ subtitle.length,
973
+ footer.length
974
+ );
975
+ const colorWrap = (code, text2, bold = false) => colorEnabled ? `${bold ? BOLD : ""}${code}${text2}${RESET}` : text2;
976
+ const border = (char) => colorWrap(palette.border, char);
977
+ const contentRow = (text2) => `${border("\u2502")} ${text2} ${border("\u2502")}`;
978
+ const glyphRow = (line, color) => contentRow(colorWrap(color, padEndSafe(line, innerWidth)));
979
+ const lines = [
980
+ `${border("\u250C")}${border("\u2500".repeat(innerWidth + 2))}${border("\u2510")}`,
981
+ contentRow(" ".repeat(innerWidth)),
982
+ glyphRow(glyph[0], ringColor),
983
+ glyphRow(glyph[1], ringColor),
984
+ glyphRow(glyph[2], coreColor),
985
+ glyphRow(glyph[3], ringColor),
986
+ glyphRow(glyph[4], ringColor),
987
+ contentRow(" ".repeat(innerWidth)),
988
+ contentRow(colorWrap(palette.text, center(title, innerWidth), true)),
989
+ contentRow(colorWrap(palette.muted, center(subtitle, innerWidth))),
990
+ contentRow(" ".repeat(innerWidth)),
991
+ contentRow(colorWrap(palette.muted, center(footer, innerWidth))),
992
+ `${border("\u2514")}${border("\u2500".repeat(innerWidth + 2))}${border("\u2518")}`
993
+ ];
994
+ return lines.join("\n");
995
+ }
996
+ function buildMinimalBanner(version) {
997
+ return `Vaka v${version} \u2014 Your mind is the architect.`;
998
+ }
999
+ async function animateVakaEntrance(options = {}) {
1000
+ const stdout = options.stdout ?? process.stdout;
1001
+ const env = options.env ?? process.env;
1002
+ const version = options.version ?? "0.1.0";
1003
+ const theme = detectTerminalTheme(env);
1004
+ const colorEnabled = supportsColor(stdout, env);
1005
+ const interactive = stdout.isTTY === true;
1006
+ const finalCard = buildVakaCard(FINAL_FRAME, false, theme, colorEnabled, version);
1007
+ const cardWidth = Math.max(...finalCard.split("\n").map((line) => stripAnsi(line).length));
1008
+ const columns = stdout.columns ?? cardWidth;
1009
+ if (!interactive) {
1010
+ stdout.write(`${finalCard}
1011
+ `);
1012
+ return;
1013
+ }
1014
+ if (columns < cardWidth) {
1015
+ stdout.write(`${buildMinimalBanner(version)}
1016
+ `);
1017
+ return;
1018
+ }
1019
+ stdout.write(HIDE_CURSOR);
1020
+ readline.cursorTo(stdout, 0, 0);
1021
+ readline.clearScreenDown(stdout);
1022
+ for (let i = 0; i < SEED_FRAMES.length; i++) {
1023
+ readline.cursorTo(stdout, 0, 0);
1024
+ stdout.write(`${buildVakaCard(i, false, theme, colorEnabled, version)}
1025
+ `);
1026
+ await new Promise((resolve4) => setTimeout(resolve4, 85));
1027
+ }
1028
+ const pulseRhythm = [true, false, true, false];
1029
+ for (const pulse of pulseRhythm) {
1030
+ readline.cursorTo(stdout, 0, 0);
1031
+ stdout.write(`${buildVakaCard(FINAL_FRAME, pulse, theme, colorEnabled, version)}
1032
+ `);
1033
+ await new Promise((resolve4) => setTimeout(resolve4, pulse ? 130 : 90));
1034
+ }
1035
+ stdout.write(SHOW_CURSOR);
1036
+ }
1037
+
1038
+ // src/update-check.ts
1039
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1040
+ import { homedir } from "os";
1041
+ import { dirname as dirname2, join as join4 } from "path";
1042
+ var REGISTRY_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/";
1043
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
1044
+ var FETCH_TIMEOUT_MS = 1500;
1045
+ function defaultCachePath() {
1046
+ return join4(homedir(), ".vaka", "update-check.json");
1047
+ }
1048
+ function readCache(cachePath) {
1049
+ try {
1050
+ const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
1051
+ if (typeof parsed.lastChecked === "number" && typeof parsed.latestVersion === "string") {
1052
+ return { lastChecked: parsed.lastChecked, latestVersion: parsed.latestVersion };
1053
+ }
1054
+ return void 0;
1055
+ } catch {
1056
+ return void 0;
1057
+ }
1058
+ }
1059
+ function writeCache(cachePath, cache) {
1060
+ try {
1061
+ mkdirSync(dirname2(cachePath), { recursive: true });
1062
+ writeFileSync(cachePath, JSON.stringify(cache), "utf8");
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ function compareVersions(a, b) {
1067
+ const toParts = (version) => (version.split(/[-+]/)[0] ?? "").split(".").map((part) => Number.parseInt(part, 10) || 0);
1068
+ const partsA = toParts(a);
1069
+ const partsB = toParts(b);
1070
+ const length = Math.max(partsA.length, partsB.length);
1071
+ for (let i = 0; i < length; i++) {
1072
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
1073
+ if (diff !== 0) {
1074
+ return diff > 0 ? 1 : -1;
1075
+ }
1076
+ }
1077
+ return 0;
1078
+ }
1079
+ async function fetchLatestVersion(packageName, fetchImpl) {
1080
+ const controller = new AbortController();
1081
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1082
+ try {
1083
+ const response = await fetchImpl(`${REGISTRY_DIST_TAGS_URL}${encodeURIComponent(packageName)}/dist-tags`, {
1084
+ signal: controller.signal
1085
+ });
1086
+ if (!response.ok) {
1087
+ return void 0;
1088
+ }
1089
+ const tags = await response.json();
1090
+ return typeof tags.latest === "string" ? tags.latest : void 0;
1091
+ } catch {
1092
+ return void 0;
1093
+ } finally {
1094
+ clearTimeout(timeout);
1095
+ }
1096
+ }
1097
+ function checkForUpdates(currentVersion, packageName, options = {}) {
1098
+ const cachePath = options.cachePath ?? defaultCachePath();
1099
+ const now = options.now ?? Date.now;
1100
+ const fetchImpl = options.fetchImpl ?? fetch;
1101
+ const cache = readCache(cachePath);
1102
+ const isStale = !cache || now() - cache.lastChecked > CACHE_TTL_MS;
1103
+ if (isStale) {
1104
+ void fetchLatestVersion(packageName, fetchImpl).then((latestVersion) => {
1105
+ if (latestVersion) {
1106
+ writeCache(cachePath, { lastChecked: now(), latestVersion });
1107
+ }
1108
+ });
1109
+ }
1110
+ if (cache && compareVersions(cache.latestVersion, currentVersion) > 0) {
1111
+ return { currentVersion, latestVersion: cache.latestVersion };
1112
+ }
1113
+ return void 0;
1114
+ }
1115
+ function formatUpdateNotice(notice, packageName) {
1116
+ const line1 = `Update available: ${notice.currentVersion} \u2192 ${notice.latestVersion}`;
1117
+ const line2 = `Run: npm install -g ${packageName}`;
1118
+ const width = Math.max(line1.length, line2.length) + 2;
1119
+ const pad = (line) => `\u2502 ${line.padEnd(width - 1)}\u2502`;
1120
+ return [
1121
+ `\u250C${"\u2500".repeat(width)}\u2510`,
1122
+ pad(line1),
1123
+ pad(line2),
1124
+ `\u2514${"\u2500".repeat(width)}\u2518`
1125
+ ].join("\n");
1126
+ }
1127
+
1128
+ // src/index.ts
1129
+ var VERSION = "0.1.0";
1130
+ var PACKAGE_NAME = "@vaka.world/cli";
1131
+ var clackPrompter = {
1132
+ async askQuestion(question) {
1133
+ const answer = await text({
1134
+ message: question.prompt,
1135
+ validate: (value) => value.trim().length === 0 ? "An answer is required." : void 0
1136
+ });
1137
+ return isCancel(answer) ? CANCELLED : answer;
1138
+ },
1139
+ async askConfirm(message) {
1140
+ const answer = await confirm({ message });
1141
+ return isCancel(answer) ? CANCELLED : answer;
1142
+ }
1143
+ };
1144
+ function describeResumeState(state) {
1145
+ switch (state.kind) {
1146
+ case "ask-question":
1147
+ return `Next: ${state.question?.prompt}`;
1148
+ case "confirm-design":
1149
+ return "Next: confirm the design.";
1150
+ case "authorize-implementation":
1151
+ return "Next: authorize implementation.";
1152
+ case "cycle-complete":
1153
+ return "Nothing required by .vaka/rules.yml for this cycle.";
1154
+ default:
1155
+ return "Unknown state.";
1156
+ }
1157
+ }
1158
+ var program = new Command().name("vaka").description("Make the reasoning behind engineering decisions visible.").version(VERSION);
1159
+ program.command("init").description("Add Vaka rules and instructions to the current project.").action(async () => {
1160
+ await animateVakaEntrance({ version: VERSION });
1161
+ intro("vaka init");
1162
+ try {
1163
+ const result = await initializeProject(process.cwd());
1164
+ for (const file of result.created) {
1165
+ log.success(`Created ${file}`);
1166
+ }
1167
+ for (const file of result.skipped) {
1168
+ log.warn(`Preserved existing file: ${file}`);
1169
+ }
1170
+ if (result.integrations.length === 0) {
1171
+ log.info(
1172
+ "No supported AI integration was detected. Created the Vaka project rules; add a Cursor, Claude Code, Codex, Copilot, Windsurf, or Cline marker to install its instructions."
1173
+ );
1174
+ } else {
1175
+ log.info(`Detected: ${result.integrations.join(", ")}`);
1176
+ }
1177
+ outro("Local setup is ready. Local records are not server-verified.");
1178
+ } catch (error) {
1179
+ log.error(error instanceof Error ? error.message : String(error));
1180
+ process.exitCode = 1;
1181
+ }
1182
+ });
1183
+ program.command("start").description(
1184
+ "Work through the Build, Design, and Implementation checkpoints for the current cycle."
1185
+ ).action(async () => {
1186
+ intro("Vaka | Your mind is the architect.");
1187
+ const nonInteractivePrompter = process.stdin.isTTY === true ? void 0 : createLineReaderPrompter();
1188
+ const prompter = nonInteractivePrompter ?? clackPrompter;
1189
+ try {
1190
+ const outcome = await runStart(process.cwd(), prompter);
1191
+ switch (outcome.kind) {
1192
+ case "implementation-approved":
1193
+ outro(
1194
+ "Implementation authorized. Local session is unverified \u2014 this records what Vaka accepted locally, not independent proof of understanding or a server verification."
1195
+ );
1196
+ break;
1197
+ case "design-declined":
1198
+ log.warn("Design not confirmed. Revisit the edge cases above, then run `vaka start` again.");
1199
+ outro("Stopped before design was confirmed.");
1200
+ break;
1201
+ case "implementation-declined":
1202
+ log.warn("Implementation not authorized. Nothing was unlocked.");
1203
+ outro("Stopped before implementation was authorized.");
1204
+ break;
1205
+ case "cancelled":
1206
+ cancel("Stopped (input ended or was interrupted). Resume anytime with `vaka start`.");
1207
+ break;
1208
+ case "nothing-required":
1209
+ outro("Nothing is required by .vaka/rules.yml for this cycle.");
1210
+ break;
1211
+ }
1212
+ } catch (error) {
1213
+ log.error(error instanceof Error ? error.message : String(error));
1214
+ process.exitCode = 1;
1215
+ } finally {
1216
+ nonInteractivePrompter?.close();
1217
+ }
1218
+ });
1219
+ program.command("status").description("Show the current local/unverified checkpoint status.").action(async () => {
1220
+ try {
1221
+ const report = await runStatus(process.cwd());
1222
+ log.info(report.sessionExists ? "Local session: unverified" : "No local session yet.");
1223
+ log.info(`Recorded events: ${report.totalEvents}`);
1224
+ log.info(`Completed cycles: ${report.completedCycles}`);
1225
+ log.info(describeResumeState(report.resumeState));
1226
+ } catch (error) {
1227
+ log.error(error instanceof Error ? error.message : String(error));
1228
+ process.exitCode = 1;
1229
+ }
1230
+ });
1231
+ await program.parseAsync(process.argv);
1232
+ var updateNotice = checkForUpdates(VERSION, PACKAGE_NAME);
1233
+ if (updateNotice) {
1234
+ console.log(formatUpdateNotice(updateNotice, PACKAGE_NAME));
1235
+ }
1236
+ //# sourceMappingURL=index.js.map