@trail-pm/cli 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.
@@ -0,0 +1,1622 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // src/core/compile-snapshot.ts
29
+ import fs from "fs";
30
+ import path from "path";
31
+
32
+ // src/schemas/snapshot.ts
33
+ import { z as z2 } from "zod";
34
+
35
+ // src/schemas/task.ts
36
+ import { z } from "zod";
37
+ var TaskStatusSchema = z.enum([
38
+ "draft",
39
+ "todo",
40
+ "in_progress",
41
+ "in_review",
42
+ "done",
43
+ "cancelled"
44
+ ]);
45
+ var isoDateStringSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected ISO date string (YYYY-MM-DD)");
46
+ var TaskGitHubSchema = z.object({
47
+ issue_number: z.number().int(),
48
+ synced_at: z.string().datetime({ offset: true }),
49
+ url: z.string().url()
50
+ }).nullable();
51
+ var TaskAiSchema = z.object({
52
+ summary: z.string().optional(),
53
+ acceptance_criteria: z.array(z.string()).optional(),
54
+ implementation_context: z.array(z.string()).optional(),
55
+ test_strategy: z.array(z.string()).optional(),
56
+ constraints: z.array(z.string()).optional()
57
+ }).strict().optional();
58
+ var TaskSchema = z.object({
59
+ id: z.string(),
60
+ title: z.string(),
61
+ description: z.string().optional(),
62
+ status: TaskStatusSchema,
63
+ priority: z.enum(["p0", "p1", "p2", "p3"]).optional(),
64
+ type: z.enum(["feature", "bug", "chore", "epic"]),
65
+ assignee: z.string().optional(),
66
+ milestone: z.string().optional(),
67
+ branch: z.string().optional(),
68
+ labels: z.array(z.string()).default([]),
69
+ parent: z.string().nullable().optional(),
70
+ depends_on: z.array(z.string()).default([]),
71
+ blocks: z.array(z.string()).default([]),
72
+ due_date: isoDateStringSchema.optional(),
73
+ start_date: isoDateStringSchema.optional(),
74
+ estimate: z.enum(["xs", "sm", "md", "lg", "xl"]).optional(),
75
+ github: TaskGitHubSchema.optional(),
76
+ refs: z.array(
77
+ z.object({
78
+ type: z.string(),
79
+ path: z.string()
80
+ }).strict()
81
+ ).default([]),
82
+ ai: TaskAiSchema,
83
+ created_at: z.string().datetime({ offset: true }),
84
+ updated_at: z.string().datetime({ offset: true })
85
+ }).strict();
86
+
87
+ // src/schemas/snapshot.ts
88
+ var SnapshotSchema = z2.object({
89
+ generated_at: z2.string().datetime({ offset: true }),
90
+ tasks: z2.array(TaskSchema),
91
+ warnings: z2.array(
92
+ z2.object({
93
+ code: z2.string(),
94
+ message: z2.string(),
95
+ taskId: z2.string().optional()
96
+ }).strict()
97
+ )
98
+ }).strict();
99
+
100
+ // src/core/compile-snapshot.ts
101
+ var UNKNOWN_DEPENDENCY = "UNKNOWN_DEPENDENCY";
102
+ var DEPENDENCY_CYCLE = "DEPENDENCY_CYCLE";
103
+ function detectCycles(taskIds, deps) {
104
+ const cycles = [];
105
+ const visited = /* @__PURE__ */ new Set();
106
+ const visiting = /* @__PURE__ */ new Set();
107
+ const stack = [];
108
+ function recordCycle(fromIndex) {
109
+ cycles.push(stack.slice(fromIndex));
110
+ }
111
+ function dfs(u) {
112
+ visiting.add(u);
113
+ stack.push(u);
114
+ for (const v of deps.get(u) ?? []) {
115
+ if (!taskIds.has(v)) {
116
+ continue;
117
+ }
118
+ if (visiting.has(v)) {
119
+ const i = stack.indexOf(v);
120
+ if (i !== -1) {
121
+ recordCycle(i);
122
+ }
123
+ continue;
124
+ }
125
+ if (!visited.has(v)) {
126
+ dfs(v);
127
+ }
128
+ }
129
+ stack.pop();
130
+ visiting.delete(u);
131
+ visited.add(u);
132
+ }
133
+ for (const id of taskIds) {
134
+ if (!visited.has(id)) {
135
+ dfs(id);
136
+ }
137
+ }
138
+ return cycles;
139
+ }
140
+ function formatCycle(nodes) {
141
+ return nodes.join(" \u2192 ");
142
+ }
143
+ function compileSnapshot(tasks, now) {
144
+ const generatedAt = (now ?? /* @__PURE__ */ new Date()).toISOString();
145
+ const taskList = [...tasks];
146
+ const taskIds = new Set(taskList.map((t) => t.id));
147
+ const deps = /* @__PURE__ */ new Map();
148
+ for (const t of taskList) {
149
+ deps.set(t.id, t.depends_on ?? []);
150
+ }
151
+ const warnings = [];
152
+ for (const t of taskList) {
153
+ for (const depId of t.depends_on ?? []) {
154
+ if (!taskIds.has(depId)) {
155
+ warnings.push({
156
+ code: UNKNOWN_DEPENDENCY,
157
+ message: `Task "${t.id}" depends on unknown task id "${depId}"`,
158
+ taskId: t.id
159
+ });
160
+ }
161
+ }
162
+ }
163
+ for (const cycle of detectCycles(taskIds, deps)) {
164
+ warnings.push({
165
+ code: DEPENDENCY_CYCLE,
166
+ message: `Dependency cycle: ${formatCycle(cycle)}`,
167
+ taskId: cycle[0]
168
+ });
169
+ }
170
+ return {
171
+ generated_at: generatedAt,
172
+ tasks: taskList,
173
+ warnings
174
+ };
175
+ }
176
+ function writeSnapshot(snapshotPath, snapshot) {
177
+ const parsed = SnapshotSchema.parse(snapshot);
178
+ const dir = path.dirname(snapshotPath);
179
+ fs.mkdirSync(dir, { recursive: true });
180
+ const body = `${JSON.stringify(parsed, null, 2)}
181
+ `;
182
+ fs.writeFileSync(snapshotPath, body, "utf8");
183
+ }
184
+
185
+ // src/core/paths.ts
186
+ import fs2 from "fs";
187
+ import path2 from "path";
188
+ var MAX_TRAIL_ROOT_WALK = 20;
189
+ function findTrailRoot(startDir) {
190
+ let dir = path2.resolve(startDir);
191
+ for (let i = 0; i < MAX_TRAIL_ROOT_WALK; i++) {
192
+ const configPath = path2.join(dir, ".trail", "config.json");
193
+ if (fs2.existsSync(configPath)) {
194
+ return dir;
195
+ }
196
+ const parent = path2.dirname(dir);
197
+ if (parent === dir) {
198
+ break;
199
+ }
200
+ dir = parent;
201
+ }
202
+ return null;
203
+ }
204
+ function trailPaths(root) {
205
+ const trailDir = path2.join(root, ".trail");
206
+ return {
207
+ root,
208
+ trailDir,
209
+ tasksDir: path2.join(trailDir, "tasks"),
210
+ configPath: path2.join(trailDir, "config.json"),
211
+ snapshotPath: path2.join(trailDir, "snapshot.json"),
212
+ gitignorePath: path2.join(trailDir, ".gitignore")
213
+ };
214
+ }
215
+
216
+ // src/core/task-store.ts
217
+ import fs3 from "fs";
218
+ import path3 from "path";
219
+ function toValidationError(zodError, context) {
220
+ const issues = zodError.issues.map(
221
+ (i) => `${i.path.length ? i.path.join(".") : "(root)"}: ${i.message}`
222
+ );
223
+ return {
224
+ code: "VALIDATION_FAILED",
225
+ message: context ? `Task validation failed (${context})` : "Task validation failed",
226
+ details: zodError.message,
227
+ issues
228
+ };
229
+ }
230
+ function throwValidationFailed(err) {
231
+ const e = new Error(`[VALIDATION_FAILED] ${err.message}`);
232
+ e.name = "TrailError";
233
+ e.trailError = err;
234
+ throw e;
235
+ }
236
+ function isTaskStoreValidationError(e) {
237
+ return e instanceof Error && "trailError" in e && typeof e.trailError === "object" && e.trailError !== null && "code" in e.trailError && e.trailError.code === "VALIDATION_FAILED";
238
+ }
239
+ function listTaskFiles(tasksDir) {
240
+ const names = fs3.readdirSync(tasksDir);
241
+ return names.filter(
242
+ (n) => n.endsWith(".json") && n !== "snapshot.json"
243
+ ).sort((a, b) => a.localeCompare(b));
244
+ }
245
+ function readTaskFile(filePath) {
246
+ let raw;
247
+ try {
248
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf8"));
249
+ } catch (e) {
250
+ if (e instanceof SyntaxError) {
251
+ throwValidationFailed({
252
+ code: "VALIDATION_FAILED",
253
+ message: "Invalid JSON in task file",
254
+ details: filePath
255
+ });
256
+ }
257
+ throw e;
258
+ }
259
+ const parsed = TaskSchema.safeParse(raw);
260
+ if (!parsed.success) {
261
+ throwValidationFailed(toValidationError(parsed.error, filePath));
262
+ }
263
+ return parsed.data;
264
+ }
265
+ function writeTaskFile(filePath, task) {
266
+ const parsed = TaskSchema.safeParse(task);
267
+ if (!parsed.success) {
268
+ throwValidationFailed(toValidationError(parsed.error, filePath));
269
+ }
270
+ const dir = path3.dirname(filePath);
271
+ fs3.mkdirSync(dir, { recursive: true });
272
+ const body = `${JSON.stringify(parsed.data, null, 2)}
273
+ `;
274
+ fs3.writeFileSync(filePath, body, "utf8");
275
+ }
276
+ function loadAllTasks(tasksDir) {
277
+ if (!fs3.existsSync(tasksDir)) {
278
+ return [];
279
+ }
280
+ const files = listTaskFiles(tasksDir);
281
+ return files.map((name) => readTaskFile(path3.join(tasksDir, name)));
282
+ }
283
+ function findTaskFileById(tasksDir, id) {
284
+ if (!fs3.existsSync(tasksDir)) {
285
+ return null;
286
+ }
287
+ const direct = path3.join(tasksDir, `${id}.json`);
288
+ if (fs3.existsSync(direct)) {
289
+ const task = readTaskFile(direct);
290
+ if (task.id === id) {
291
+ return { filePath: direct, task };
292
+ }
293
+ }
294
+ for (const name of listTaskFiles(tasksDir)) {
295
+ const filePath = path3.join(tasksDir, name);
296
+ const task = readTaskFile(filePath);
297
+ if (task.id === id) {
298
+ return { filePath, task };
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+
304
+ // src/cli/json.ts
305
+ function printJson(data) {
306
+ console.log(JSON.stringify(data, null, 2));
307
+ }
308
+
309
+ // src/cli/commands/context.ts
310
+ function buildContextPacket(task, allTasks) {
311
+ const depTasks = task.depends_on.map((id) => allTasks.find((t) => t.id === id)).filter((t) => t !== void 0);
312
+ return {
313
+ id: task.id,
314
+ title: task.title,
315
+ status: task.status,
316
+ priority: task.priority,
317
+ goal: task.ai?.summary ?? task.description,
318
+ depends_on: task.depends_on,
319
+ depends_on_titles: Object.fromEntries(
320
+ depTasks.map((t) => [t.id, t.title])
321
+ ),
322
+ implementation_context: task.ai?.implementation_context ?? [],
323
+ constraints: task.ai?.constraints ?? [],
324
+ definition_of_done: task.ai?.acceptance_criteria ?? [],
325
+ test_strategy: task.ai?.test_strategy ?? [],
326
+ refs: task.refs,
327
+ github: task.github
328
+ };
329
+ }
330
+ function runContext(options) {
331
+ const root = findTrailRoot(process.cwd());
332
+ if (root === null) {
333
+ const err = {
334
+ code: "NOT_A_TRAIL_REPO",
335
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
336
+ path: process.cwd()
337
+ };
338
+ throw err;
339
+ }
340
+ const paths = trailPaths(root);
341
+ const resolved = findTaskFileById(paths.tasksDir, options.id);
342
+ if (resolved === null) {
343
+ throw new Error(`No task with id "${options.id}"`);
344
+ }
345
+ const allTasks = loadAllTasks(paths.tasksDir);
346
+ printJson(buildContextPacket(resolved.task, allTasks));
347
+ }
348
+
349
+ // src/core/deps.ts
350
+ function uniq(ids) {
351
+ return [...new Set(ids)];
352
+ }
353
+ function addDependency(tasksDir, taskId, dependsOnId, updatedAtIso) {
354
+ if (taskId === dependsOnId) {
355
+ throw new Error("A task cannot depend on itself");
356
+ }
357
+ const a = findTaskFileById(tasksDir, taskId);
358
+ const b = findTaskFileById(tasksDir, dependsOnId);
359
+ if (a === null) {
360
+ throw new Error(`No task with id "${taskId}"`);
361
+ }
362
+ if (b === null) {
363
+ throw new Error(`No task with id "${dependsOnId}"`);
364
+ }
365
+ const nextA = {
366
+ ...a.task,
367
+ depends_on: uniq([...a.task.depends_on, dependsOnId]),
368
+ updated_at: updatedAtIso
369
+ };
370
+ const nextB = {
371
+ ...b.task,
372
+ blocks: uniq([...b.task.blocks, taskId]),
373
+ updated_at: updatedAtIso
374
+ };
375
+ writeTaskFile(a.filePath, nextA);
376
+ writeTaskFile(b.filePath, nextB);
377
+ }
378
+ function removeDependency(tasksDir, taskId, dependsOnId, updatedAtIso) {
379
+ const a = findTaskFileById(tasksDir, taskId);
380
+ const b = findTaskFileById(tasksDir, dependsOnId);
381
+ if (a === null) {
382
+ throw new Error(`No task with id "${taskId}"`);
383
+ }
384
+ if (b === null) {
385
+ throw new Error(`No task with id "${dependsOnId}"`);
386
+ }
387
+ const nextA = {
388
+ ...a.task,
389
+ depends_on: a.task.depends_on.filter((x) => x !== dependsOnId),
390
+ updated_at: updatedAtIso
391
+ };
392
+ const nextB = {
393
+ ...b.task,
394
+ blocks: b.task.blocks.filter((x) => x !== taskId),
395
+ updated_at: updatedAtIso
396
+ };
397
+ writeTaskFile(a.filePath, nextA);
398
+ writeTaskFile(b.filePath, nextB);
399
+ }
400
+ function listDependencyEdges(tasks) {
401
+ const edges = [];
402
+ for (const t of tasks) {
403
+ for (const d of t.depends_on) {
404
+ edges.push({ from: t.id, to: d });
405
+ }
406
+ }
407
+ return edges;
408
+ }
409
+
410
+ // src/cli/read-context.ts
411
+ import fs5 from "fs";
412
+
413
+ // src/core/auth.ts
414
+ import * as childProcess from "child_process";
415
+ var AUTH_HINT = "Set GITHUB_TOKEN or install gh and run gh auth login";
416
+ function authRequired(message) {
417
+ return {
418
+ ok: false,
419
+ error: {
420
+ code: "AUTH_REQUIRED",
421
+ message,
422
+ hint: AUTH_HINT
423
+ }
424
+ };
425
+ }
426
+ function resolveGitHubToken(env) {
427
+ const e = env ?? process.env;
428
+ const fromEnv = e.GITHUB_TOKEN;
429
+ if (typeof fromEnv === "string" && fromEnv.trim() !== "") {
430
+ return { ok: true, token: fromEnv.trim() };
431
+ }
432
+ try {
433
+ const out = childProcess.execFileSync("gh", ["auth", "token"], {
434
+ encoding: "utf-8"
435
+ });
436
+ const token = out.trim();
437
+ if (token === "") {
438
+ return authRequired("GitHub CLI returned an empty token");
439
+ }
440
+ return { ok: true, token };
441
+ } catch {
442
+ return authRequired(
443
+ "No GitHub token found; could not read from environment or gh CLI"
444
+ );
445
+ }
446
+ }
447
+
448
+ // src/core/errors.ts
449
+ function formatTrailError(error) {
450
+ const lines = [`[${error.code}] ${error.message}`];
451
+ switch (error.code) {
452
+ case "AUTH_REQUIRED": {
453
+ if (error.hint) {
454
+ lines.push(error.hint);
455
+ }
456
+ break;
457
+ }
458
+ case "AUTH_FAILED": {
459
+ break;
460
+ }
461
+ case "NOT_A_TRAIL_REPO": {
462
+ if (error.path) {
463
+ lines.push(`Path: ${error.path}`);
464
+ }
465
+ break;
466
+ }
467
+ case "VALIDATION_FAILED": {
468
+ if (error.details) {
469
+ lines.push(error.details);
470
+ }
471
+ if (error.issues && error.issues.length > 0) {
472
+ for (const issue of error.issues) {
473
+ lines.push(` - ${issue}`);
474
+ }
475
+ }
476
+ break;
477
+ }
478
+ case "GITHUB_API": {
479
+ if (error.status !== void 0) {
480
+ lines.push(`HTTP status: ${error.status}`);
481
+ }
482
+ if (error.body) {
483
+ lines.push(error.body);
484
+ }
485
+ break;
486
+ }
487
+ case "SYNC_CONFLICT": {
488
+ if (error.paths && error.paths.length > 0) {
489
+ lines.push(`Paths: ${error.paths.join(", ")}`);
490
+ }
491
+ break;
492
+ }
493
+ default: {
494
+ const _exhaustive = error;
495
+ return _exhaustive;
496
+ }
497
+ }
498
+ return lines.join("\n");
499
+ }
500
+
501
+ // src/core/github-client.ts
502
+ var USER_AGENT = "trail-cli/0.0.1";
503
+ function normalizeBaseUrl(baseUrl) {
504
+ return baseUrl.replace(/\/$/, "");
505
+ }
506
+ var GitHubClient = class {
507
+ token;
508
+ baseUrl;
509
+ constructor(token, baseUrl = "https://api.github.com") {
510
+ this.token = token;
511
+ this.baseUrl = normalizeBaseUrl(baseUrl);
512
+ }
513
+ async request(method, path8, body) {
514
+ const url = `${this.baseUrl}${path8.startsWith("/") ? path8 : `/${path8}`}`;
515
+ const headers = new Headers({
516
+ Authorization: `Bearer ${this.token}`,
517
+ Accept: "application/vnd.github+json",
518
+ "X-GitHub-Api-Version": "2022-11-28",
519
+ "User-Agent": USER_AGENT
520
+ });
521
+ if (body !== void 0) {
522
+ headers.set("Content-Type", "application/json");
523
+ }
524
+ return fetch(url, {
525
+ method,
526
+ headers,
527
+ body: body !== void 0 ? JSON.stringify(body) : void 0
528
+ });
529
+ }
530
+ async parseJson(response) {
531
+ const text = await response.text();
532
+ if (!response.ok) {
533
+ const snippet = text.slice(0, 200);
534
+ throw new Error(`GitHub API ${response.status}: ${snippet}`);
535
+ }
536
+ return JSON.parse(text);
537
+ }
538
+ async listIssues(owner, repo, params) {
539
+ const search = new URLSearchParams({
540
+ state: params.state,
541
+ per_page: String(params.per_page),
542
+ page: String(params.page)
543
+ });
544
+ const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${search}`;
545
+ const response = await this.request("GET", path8);
546
+ return this.parseJson(response);
547
+ }
548
+ async getIssue(owner, repo, issueNumber) {
549
+ const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
550
+ const response = await this.request("GET", path8);
551
+ return this.parseJson(response);
552
+ }
553
+ async updateIssue(owner, repo, issueNumber, patch) {
554
+ const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
555
+ const response = await this.request("PATCH", path8, patch);
556
+ return this.parseJson(response);
557
+ }
558
+ async createIssueComment(owner, repo, issueNumber, body) {
559
+ const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/comments`;
560
+ const response = await this.request("POST", path8, { body });
561
+ return this.parseJson(response);
562
+ }
563
+ /** Create a new issue. Returns the created issue (same shape as list/get). */
564
+ async createIssue(owner, repo, input) {
565
+ const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
566
+ const response = await this.request("POST", path8, {
567
+ title: input.title,
568
+ body: input.body ?? "",
569
+ labels: input.labels ?? []
570
+ });
571
+ return this.parseJson(response);
572
+ }
573
+ };
574
+
575
+ // src/core/sync.ts
576
+ import fs4 from "fs";
577
+ import path4 from "path";
578
+
579
+ // src/core/github-mapper.ts
580
+ function statusFromIssue(issue, existing) {
581
+ if (issue.state === "closed") {
582
+ return "done";
583
+ }
584
+ const prev = existing?.status;
585
+ if (prev === "in_progress" || prev === "in_review") {
586
+ return prev;
587
+ }
588
+ return "todo";
589
+ }
590
+ function parseIssueTimestamp(issue, fallback) {
591
+ if (issue.updated_at.trim() === "") {
592
+ return fallback.toISOString();
593
+ }
594
+ return issue.updated_at;
595
+ }
596
+ function issueToTask(issue, existing, now) {
597
+ const updatedAt = parseIssueTimestamp(issue, now);
598
+ const createdAt = existing?.created_at ?? (issue.updated_at.trim() !== "" ? issue.updated_at : now.toISOString());
599
+ const raw = {
600
+ id: String(issue.number),
601
+ title: issue.title,
602
+ description: issue.body ?? "",
603
+ status: statusFromIssue(issue, existing),
604
+ type: existing?.type ?? "feature",
605
+ labels: issue.labels.map((l) => l.name),
606
+ assignee: issue.assignee?.login,
607
+ milestone: issue.milestone?.title,
608
+ depends_on: existing?.depends_on ?? [],
609
+ blocks: existing?.blocks ?? [],
610
+ refs: existing?.refs ?? [],
611
+ ai: existing?.ai,
612
+ branch: existing?.branch,
613
+ estimate: existing?.estimate,
614
+ priority: existing?.priority,
615
+ parent: existing?.parent,
616
+ due_date: existing?.due_date,
617
+ start_date: existing?.start_date,
618
+ github: {
619
+ issue_number: issue.number,
620
+ synced_at: now.toISOString(),
621
+ url: issue.html_url
622
+ },
623
+ created_at: createdAt,
624
+ updated_at: updatedAt
625
+ };
626
+ return TaskSchema.parse(raw);
627
+ }
628
+ function taskToIssueUpdate(task) {
629
+ const labels = [...task.labels];
630
+ if (task.priority) {
631
+ const priorityLabel = `priority:${task.priority}`;
632
+ if (!labels.includes(priorityLabel)) {
633
+ labels.push(priorityLabel);
634
+ }
635
+ }
636
+ const state = task.status === "done" || task.status === "cancelled" ? "closed" : "open";
637
+ return {
638
+ title: task.title,
639
+ body: task.description ?? "",
640
+ state,
641
+ labels
642
+ };
643
+ }
644
+
645
+ // src/core/sync.ts
646
+ var ISSUES_PER_PAGE = 100;
647
+ async function pullSync(options) {
648
+ const { client, owner, repo, tasksDir } = options;
649
+ const now = options.now ?? /* @__PURE__ */ new Date();
650
+ let page = 1;
651
+ for (; ; ) {
652
+ const issues = await client.listIssues(owner, repo, {
653
+ state: "all",
654
+ per_page: ISSUES_PER_PAGE,
655
+ page
656
+ });
657
+ if (issues.length === 0) {
658
+ break;
659
+ }
660
+ for (const issue of issues) {
661
+ const filePath = path4.join(tasksDir, `${issue.number}.json`);
662
+ let existing = null;
663
+ if (fs4.existsSync(filePath)) {
664
+ existing = readTaskFile(filePath);
665
+ }
666
+ const task = issueToTask(issue, existing, now);
667
+ writeTaskFile(filePath, task);
668
+ }
669
+ if (issues.length < ISSUES_PER_PAGE) {
670
+ break;
671
+ }
672
+ page += 1;
673
+ }
674
+ }
675
+ function isLinkedTask(task) {
676
+ return task.github != null && typeof task.github === "object";
677
+ }
678
+ async function pushSync(options) {
679
+ const { client, owner, repo, tasks } = options;
680
+ for (const task of tasks) {
681
+ if (task.status === "draft") {
682
+ continue;
683
+ }
684
+ if (!isLinkedTask(task)) {
685
+ continue;
686
+ }
687
+ const patch = taskToIssueUpdate(task);
688
+ await client.updateIssue(owner, repo, task.github.issue_number, patch);
689
+ }
690
+ }
691
+ async function fullSync(options) {
692
+ const { client, owner, repo, tasksDir, snapshotPath, now } = options;
693
+ await pullSync({ client, owner, repo, tasksDir, now });
694
+ const tasks = loadAllTasks(tasksDir);
695
+ await pushSync({ client, owner, repo, tasks });
696
+ const snapshot = compileSnapshot(tasks, now);
697
+ writeSnapshot(snapshotPath, snapshot);
698
+ }
699
+
700
+ // src/core/maybe-pull.ts
701
+ async function maybePullBeforeRead(config, paths) {
702
+ if (!config.sync.auto_sync_on_command || config.sync.preset === "offline") {
703
+ return;
704
+ }
705
+ const tokenResult = resolveGitHubToken();
706
+ if (!tokenResult.ok) {
707
+ console.warn(
708
+ `Warning: ${formatTrailError(tokenResult.error)} \u2014 continuing with local tasks.`
709
+ );
710
+ return;
711
+ }
712
+ const client = new GitHubClient(tokenResult.token);
713
+ const { owner, repo } = config.github;
714
+ try {
715
+ await pullSync({ client, owner, repo, tasksDir: paths.tasksDir });
716
+ } catch (e) {
717
+ const msg = e instanceof Error ? e.message : String(e);
718
+ console.warn(`Warning: auto-pull failed (${msg}) \u2014 continuing with local tasks.`);
719
+ }
720
+ }
721
+
722
+ // src/schemas/config.ts
723
+ import { z as z3 } from "zod";
724
+ var TrailConfigSchema = z3.object({
725
+ github: z3.object({
726
+ owner: z3.string(),
727
+ repo: z3.string()
728
+ }).strict(),
729
+ sync: z3.object({
730
+ preset: z3.enum(["collaborative", "solo", "offline"]),
731
+ auto_sync_on_command: z3.boolean(),
732
+ ui_poll_interval_seconds: z3.number(),
733
+ ui_idle_backoff: z3.boolean()
734
+ }).strict(),
735
+ /** Last successful full sync; useful for future `trail status` and similar. */
736
+ last_full_sync_at: z3.string().datetime({ offset: true }).optional()
737
+ }).strict();
738
+
739
+ // src/cli/read-context.ts
740
+ async function loadTrailReadContext(cwd) {
741
+ const root = findTrailRoot(cwd);
742
+ if (root === null) {
743
+ const err = {
744
+ code: "NOT_A_TRAIL_REPO",
745
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
746
+ path: cwd
747
+ };
748
+ throw err;
749
+ }
750
+ const paths = trailPaths(root);
751
+ const raw = fs5.readFileSync(paths.configPath, "utf-8");
752
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
753
+ await maybePullBeforeRead(config, paths);
754
+ const tasks = loadAllTasks(paths.tasksDir);
755
+ return { root, paths, config, tasks };
756
+ }
757
+
758
+ // src/cli/commands/list.ts
759
+ var DEFAULT_LIMIT = 25;
760
+ var HIDDEN_BY_DEFAULT = /* @__PURE__ */ new Set(["done", "cancelled"]);
761
+ function selectTasksForList(tasks, options) {
762
+ const all = options.all === true;
763
+ const limit = options.limit ?? DEFAULT_LIMIT;
764
+ const statusFilter = options.status;
765
+ const labelFilter = options.label;
766
+ let rows = tasks.filter((t) => all || !HIDDEN_BY_DEFAULT.has(t.status));
767
+ if (statusFilter !== void 0) {
768
+ rows = rows.filter((t) => t.status === statusFilter);
769
+ }
770
+ if (labelFilter !== void 0) {
771
+ rows = rows.filter((t) => t.labels.includes(labelFilter));
772
+ }
773
+ rows.sort((a, b) => a.id.localeCompare(b.id, void 0, { numeric: true }));
774
+ return rows.slice(0, limit);
775
+ }
776
+ function slimTask(t) {
777
+ return {
778
+ id: t.id,
779
+ title: t.title,
780
+ status: t.status,
781
+ priority: t.priority,
782
+ labels: t.labels
783
+ };
784
+ }
785
+ function formatTable(tasks) {
786
+ const idW = Math.max(4, ...tasks.map((t) => t.id.length), 2);
787
+ const statusW = Math.max(6, ...tasks.map((t) => t.status.length), 6);
788
+ const header = `${"ID".padEnd(idW)} ${"STATUS".padEnd(statusW)} TITLE`;
789
+ console.log(header);
790
+ console.log("-".repeat(header.length));
791
+ for (const t of tasks) {
792
+ console.log(`${t.id.padEnd(idW)} ${t.status.padEnd(statusW)} ${t.title}`);
793
+ }
794
+ }
795
+ async function runList(options) {
796
+ const { tasks } = await loadTrailReadContext(process.cwd());
797
+ const rows = selectTasksForList(tasks, options);
798
+ if (options.json) {
799
+ printJson(rows.map(slimTask));
800
+ return;
801
+ }
802
+ if (rows.length === 0) {
803
+ console.log("No tasks match the filters.");
804
+ return;
805
+ }
806
+ formatTable(rows);
807
+ }
808
+
809
+ // src/core/next-task.ts
810
+ var TERMINAL = /* @__PURE__ */ new Set(["done", "cancelled"]);
811
+ function priorityRank(p) {
812
+ switch (p) {
813
+ case "p0":
814
+ return 0;
815
+ case "p1":
816
+ return 1;
817
+ case "p2":
818
+ return 2;
819
+ case "p3":
820
+ return 3;
821
+ default:
822
+ return 4;
823
+ }
824
+ }
825
+ function compareTaskIds(a, b) {
826
+ if (/^\d+$/.test(a) && /^\d+$/.test(b)) {
827
+ const ba = BigInt(a);
828
+ const bb = BigInt(b);
829
+ return ba < bb ? -1 : ba > bb ? 1 : 0;
830
+ }
831
+ return a.localeCompare(b);
832
+ }
833
+ function dependencyResolved(dep) {
834
+ if (dep === void 0) {
835
+ return false;
836
+ }
837
+ return TERMINAL.has(dep.status);
838
+ }
839
+ function isTaskBlocked(task, byId) {
840
+ for (const depId of task.depends_on) {
841
+ if (!dependencyResolved(byId.get(depId))) {
842
+ return true;
843
+ }
844
+ }
845
+ return false;
846
+ }
847
+ function selectNextTask(tasks) {
848
+ const byId = new Map(tasks.map((t) => [t.id, t]));
849
+ const candidates = tasks.filter(
850
+ (t) => !TERMINAL.has(t.status) && !isTaskBlocked(t, byId)
851
+ );
852
+ if (candidates.length === 0) {
853
+ return null;
854
+ }
855
+ candidates.sort((a, b) => {
856
+ const pr = priorityRank(a.priority) - priorityRank(b.priority);
857
+ if (pr !== 0) {
858
+ return pr;
859
+ }
860
+ return compareTaskIds(a.id, b.id);
861
+ });
862
+ return candidates[0] ?? null;
863
+ }
864
+
865
+ // src/cli/run-cli.ts
866
+ import { readFileSync } from "fs";
867
+ import { dirname, join } from "path";
868
+ import { fileURLToPath } from "url";
869
+ import { Command, Option } from "commander";
870
+ import { ZodError } from "zod";
871
+
872
+ // src/cli/commands/create.ts
873
+ import path5 from "path";
874
+
875
+ // src/core/draft-id.ts
876
+ import { randomBytes } from "crypto";
877
+ function generateDraftId() {
878
+ return `draft-${randomBytes(4).toString("hex")}`;
879
+ }
880
+
881
+ // src/core/rebuild-snapshot.ts
882
+ function rebuildSnapshot(paths, now = /* @__PURE__ */ new Date()) {
883
+ const tasks = loadAllTasks(paths.tasksDir);
884
+ const snapshot = compileSnapshot(tasks, now);
885
+ writeSnapshot(paths.snapshotPath, snapshot);
886
+ }
887
+
888
+ // src/cli/commands/create.ts
889
+ function runCreate(options) {
890
+ const root = findTrailRoot(process.cwd());
891
+ if (root === null) {
892
+ const err = {
893
+ code: "NOT_A_TRAIL_REPO",
894
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
895
+ path: process.cwd()
896
+ };
897
+ throw err;
898
+ }
899
+ const paths = trailPaths(root);
900
+ const now = /* @__PURE__ */ new Date();
901
+ const iso = now.toISOString();
902
+ const id = generateDraftId();
903
+ const raw = {
904
+ id,
905
+ title: options.title,
906
+ description: options.description ?? "",
907
+ status: "draft",
908
+ type: options.type ?? "feature",
909
+ labels: [],
910
+ depends_on: [],
911
+ blocks: [],
912
+ refs: [],
913
+ created_at: iso,
914
+ updated_at: iso
915
+ };
916
+ if (options.priority !== void 0) {
917
+ raw.priority = options.priority;
918
+ }
919
+ const task = TaskSchema.parse(raw);
920
+ const filePath = path5.join(paths.tasksDir, `${id}.json`);
921
+ writeTaskFile(filePath, task);
922
+ rebuildSnapshot(paths, now);
923
+ console.log(`Created draft task ${id}`);
924
+ console.log(` File: ${filePath}`);
925
+ }
926
+
927
+ // src/cli/commands/dep.ts
928
+ function runDepAdd(taskId, dependsOnId) {
929
+ const root = findTrailRoot(process.cwd());
930
+ if (root === null) {
931
+ const err = {
932
+ code: "NOT_A_TRAIL_REPO",
933
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
934
+ path: process.cwd()
935
+ };
936
+ throw err;
937
+ }
938
+ const paths = trailPaths(root);
939
+ const iso = (/* @__PURE__ */ new Date()).toISOString();
940
+ addDependency(paths.tasksDir, taskId, dependsOnId, iso);
941
+ rebuildSnapshot(paths, new Date(iso));
942
+ console.log(`Added dependency: ${taskId} depends on ${dependsOnId}`);
943
+ }
944
+ function runDepRemove(taskId, dependsOnId) {
945
+ const root = findTrailRoot(process.cwd());
946
+ if (root === null) {
947
+ const err = {
948
+ code: "NOT_A_TRAIL_REPO",
949
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
950
+ path: process.cwd()
951
+ };
952
+ throw err;
953
+ }
954
+ const paths = trailPaths(root);
955
+ const iso = (/* @__PURE__ */ new Date()).toISOString();
956
+ removeDependency(paths.tasksDir, taskId, dependsOnId, iso);
957
+ rebuildSnapshot(paths, new Date(iso));
958
+ console.log(`Removed dependency: ${taskId} no longer depends on ${dependsOnId}`);
959
+ }
960
+
961
+ // src/cli/commands/done.ts
962
+ import fs6 from "fs";
963
+ function isLinkedTask2(task) {
964
+ return task.github != null && typeof task.github === "object";
965
+ }
966
+ function conventionalPrefix(type) {
967
+ switch (type) {
968
+ case "bug":
969
+ return "fix";
970
+ case "chore":
971
+ return "chore";
972
+ case "epic":
973
+ case "feature":
974
+ return "feat";
975
+ default:
976
+ return "feat";
977
+ }
978
+ }
979
+ async function runDone(options) {
980
+ const root = findTrailRoot(process.cwd());
981
+ if (root === null) {
982
+ const err = {
983
+ code: "NOT_A_TRAIL_REPO",
984
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
985
+ path: process.cwd()
986
+ };
987
+ throw err;
988
+ }
989
+ const paths = trailPaths(root);
990
+ const raw = fs6.readFileSync(paths.configPath, "utf-8");
991
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
992
+ const resolved = findTaskFileById(paths.tasksDir, options.id);
993
+ if (resolved === null) {
994
+ throw new Error(`No task with id "${options.id}"`);
995
+ }
996
+ const now = /* @__PURE__ */ new Date();
997
+ const iso = now.toISOString();
998
+ let next = {
999
+ ...resolved.task,
1000
+ status: "done",
1001
+ updated_at: iso
1002
+ };
1003
+ writeTaskFile(resolved.filePath, next);
1004
+ const offline = config.sync.preset === "offline";
1005
+ const tokenResult = resolveGitHubToken();
1006
+ if (!offline && tokenResult.ok && isLinkedTask2(next)) {
1007
+ const client = new GitHubClient(tokenResult.token);
1008
+ const { owner, repo } = config.github;
1009
+ const n = next.github.issue_number;
1010
+ await client.createIssueComment(owner, repo, n, options.message);
1011
+ await client.updateIssue(
1012
+ owner,
1013
+ repo,
1014
+ n,
1015
+ taskToIssueUpdate(next)
1016
+ );
1017
+ const synced = {
1018
+ ...next,
1019
+ github: {
1020
+ ...next.github,
1021
+ synced_at: now.toISOString()
1022
+ }
1023
+ };
1024
+ writeTaskFile(resolved.filePath, synced);
1025
+ next = synced;
1026
+ }
1027
+ rebuildSnapshot(paths, now);
1028
+ if (isLinkedTask2(next)) {
1029
+ const prefix = conventionalPrefix(next.type);
1030
+ const oneLine = options.message.replace(/\s+/g, " ").trim();
1031
+ console.log(
1032
+ `Suggested commit: ${prefix}: ${oneLine} (closes #${next.github.issue_number})`
1033
+ );
1034
+ }
1035
+ }
1036
+
1037
+ // src/cli/commands/graph.ts
1038
+ function runGraph(options) {
1039
+ const root = findTrailRoot(process.cwd());
1040
+ if (root === null) {
1041
+ const err = {
1042
+ code: "NOT_A_TRAIL_REPO",
1043
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
1044
+ path: process.cwd()
1045
+ };
1046
+ throw err;
1047
+ }
1048
+ const paths = trailPaths(root);
1049
+ const tasks = loadAllTasks(paths.tasksDir);
1050
+ const edges = listDependencyEdges(tasks);
1051
+ if (options.json) {
1052
+ printJson(edges);
1053
+ return;
1054
+ }
1055
+ if (edges.length === 0) {
1056
+ console.log("No dependencies.");
1057
+ return;
1058
+ }
1059
+ for (const e of edges) {
1060
+ console.log(`${e.from} \u2192 depends on \u2192 ${e.to}`);
1061
+ }
1062
+ }
1063
+
1064
+ // src/cli/commands/init.ts
1065
+ import * as childProcess2 from "child_process";
1066
+ import fs7 from "fs";
1067
+ import path6 from "path";
1068
+
1069
+ // src/cli/templates/user-agents.md.ts
1070
+ var USER_AGENTS_MD = `# Trail \u2014 Agent workflow
1071
+
1072
+ This repository uses [Trail](https://github.com/joeydekruis/trail) for GitHub-native task tracking. Task JSON lives in \`.trail/tasks/\`; GitHub Issues are the remote source of truth when online.
1073
+
1074
+ ## Before coding
1075
+
1076
+ 1. Run \`trail sync\` (or rely on collaborative mode) so local tasks match GitHub.
1077
+ 2. Run \`trail next\` (or \`trail next --json\`) to pick the highest-priority unblocked task.
1078
+ 3. Run \`trail context <id>\` to load a compact JSON work packet for your session.
1079
+
1080
+ ## While working
1081
+
1082
+ - Update status: \`trail update <id> --status in_progress\`
1083
+ - Use \`trail list\`, \`trail show <id>\`, and \`trail validate\` as needed.
1084
+
1085
+ ## When finished
1086
+
1087
+ 1. \`trail done <id> "what you did"\` \u2014 closes the linked GitHub issue when configured.
1088
+ 2. Commit using the suggested message (includes \`closes #N\` when linked).
1089
+
1090
+ ## Docs
1091
+
1092
+ - Trail design (upstream): see your fork or \`trail\` repo \`docs/superpowers/specs/\`
1093
+ `;
1094
+
1095
+ // src/cli/commands/init.ts
1096
+ var GITIGNORE_LINES = ["snapshot.json", "export/", "*.tmp"];
1097
+ function parseRemoteUrl(remoteUrl) {
1098
+ const trimmed = remoteUrl.trim();
1099
+ const sshMatch = /^git@github\.com:([^/]+)\/([^/\s]+)$/.exec(trimmed);
1100
+ if (sshMatch) {
1101
+ const owner = sshMatch[1];
1102
+ const repoRaw = sshMatch[2];
1103
+ if (owner === void 0 || repoRaw === void 0) {
1104
+ return null;
1105
+ }
1106
+ let repo = repoRaw;
1107
+ if (repo.endsWith(".git")) {
1108
+ repo = repo.slice(0, -".git".length);
1109
+ }
1110
+ return { owner, repo };
1111
+ }
1112
+ const httpsMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/?#]+)/.exec(trimmed);
1113
+ if (httpsMatch) {
1114
+ const owner = httpsMatch[1];
1115
+ const repoRaw = httpsMatch[2];
1116
+ if (owner === void 0 || repoRaw === void 0) {
1117
+ return null;
1118
+ }
1119
+ let repo = repoRaw;
1120
+ if (repo.endsWith(".git")) {
1121
+ repo = repo.slice(0, -".git".length);
1122
+ }
1123
+ return { owner, repo };
1124
+ }
1125
+ return null;
1126
+ }
1127
+ function resolveGitRepoRoot(cwd) {
1128
+ try {
1129
+ const out = childProcess2.execFileSync("git", ["rev-parse", "--show-toplevel"], {
1130
+ cwd,
1131
+ encoding: "utf-8"
1132
+ });
1133
+ return out.trim();
1134
+ } catch {
1135
+ return cwd;
1136
+ }
1137
+ }
1138
+ function getOriginRemoteUrl(repoRoot) {
1139
+ try {
1140
+ return childProcess2.execFileSync("git", ["remote", "get-url", "origin"], {
1141
+ cwd: repoRoot,
1142
+ encoding: "utf-8"
1143
+ }).trim();
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+ function runInit(options) {
1149
+ const cwd = process.cwd();
1150
+ const root = path6.resolve(resolveGitRepoRoot(cwd));
1151
+ const configPath = path6.join(root, ".trail", "config.json");
1152
+ if (fs7.existsSync(configPath)) {
1153
+ const err = {
1154
+ code: "VALIDATION_FAILED",
1155
+ message: "Trail is already initialized (.trail/config.json exists)."
1156
+ };
1157
+ throw err;
1158
+ }
1159
+ const hasOwner = options.owner !== void 0 && options.owner !== "";
1160
+ const hasRepo = options.repo !== void 0 && options.repo !== "";
1161
+ if (hasOwner !== hasRepo) {
1162
+ const err = {
1163
+ code: "VALIDATION_FAILED",
1164
+ message: "Provide both --owner and --repo, or neither to use git remote origin."
1165
+ };
1166
+ throw err;
1167
+ }
1168
+ let owner;
1169
+ let repo;
1170
+ if (hasOwner && hasRepo) {
1171
+ owner = options.owner;
1172
+ repo = options.repo;
1173
+ } else {
1174
+ const remoteUrl = getOriginRemoteUrl(root);
1175
+ const parsed = remoteUrl ? parseRemoteUrl(remoteUrl) : null;
1176
+ if (!parsed) {
1177
+ const err = {
1178
+ code: "VALIDATION_FAILED",
1179
+ message: "Could not determine GitHub owner/repo. Set --owner and --repo or add a github.com origin remote."
1180
+ };
1181
+ throw err;
1182
+ }
1183
+ owner = parsed.owner;
1184
+ repo = parsed.repo;
1185
+ }
1186
+ const preset = options.preset;
1187
+ const config = TrailConfigSchema.parse({
1188
+ github: { owner, repo },
1189
+ sync: {
1190
+ preset,
1191
+ auto_sync_on_command: preset === "collaborative",
1192
+ ui_poll_interval_seconds: 30,
1193
+ ui_idle_backoff: true
1194
+ }
1195
+ });
1196
+ const trailDir = path6.join(root, ".trail");
1197
+ const tasksDir = path6.join(trailDir, "tasks");
1198
+ fs7.mkdirSync(trailDir, { recursive: true });
1199
+ fs7.mkdirSync(tasksDir, { recursive: true });
1200
+ fs7.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1201
+ `, "utf-8");
1202
+ const gitignorePath = path6.join(trailDir, ".gitignore");
1203
+ fs7.writeFileSync(gitignorePath, `${GITIGNORE_LINES.join("\n")}
1204
+ `, "utf-8");
1205
+ if (options.skipAgentsMd !== true) {
1206
+ const agentsPath = path6.join(root, "AGENTS.md");
1207
+ if (!fs7.existsSync(agentsPath)) {
1208
+ fs7.writeFileSync(agentsPath, USER_AGENTS_MD, "utf-8");
1209
+ console.log(`Wrote ${agentsPath}`);
1210
+ } else {
1211
+ console.log(`Skipped AGENTS.md (file already exists)`);
1212
+ }
1213
+ }
1214
+ console.log(`Initialized Trail project at ${root}`);
1215
+ }
1216
+
1217
+ // src/cli/commands/next.ts
1218
+ async function runNext(options) {
1219
+ const { tasks } = await loadTrailReadContext(process.cwd());
1220
+ const next = selectNextTask(tasks);
1221
+ if (next === null) {
1222
+ if (options.json) {
1223
+ printJson(null);
1224
+ } else {
1225
+ console.log("No eligible next task.");
1226
+ }
1227
+ return;
1228
+ }
1229
+ if (options.json) {
1230
+ printJson(next);
1231
+ return;
1232
+ }
1233
+ const pri = next.priority ?? "\u2014";
1234
+ console.log(`Next: ${next.id} \u2014 ${next.title} (${next.status}, ${pri})`);
1235
+ }
1236
+
1237
+ // src/cli/commands/promote.ts
1238
+ import fs8 from "fs";
1239
+ import path7 from "path";
1240
+ async function runPromote(options) {
1241
+ const root = findTrailRoot(process.cwd());
1242
+ if (root === null) {
1243
+ const err = {
1244
+ code: "NOT_A_TRAIL_REPO",
1245
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
1246
+ path: process.cwd()
1247
+ };
1248
+ throw err;
1249
+ }
1250
+ const paths = trailPaths(root);
1251
+ const raw = fs8.readFileSync(paths.configPath, "utf-8");
1252
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
1253
+ if (config.sync.preset === "offline") {
1254
+ throw new Error("Cannot promote a draft in offline mode (no GitHub access).");
1255
+ }
1256
+ const tokenResult = resolveGitHubToken();
1257
+ if (!tokenResult.ok) {
1258
+ throw tokenResult.error;
1259
+ }
1260
+ const resolved = findTaskFileById(paths.tasksDir, options.id);
1261
+ if (resolved === null) {
1262
+ throw new Error(`No task with id "${options.id}"`);
1263
+ }
1264
+ const draft = resolved.task;
1265
+ if (draft.status !== "draft") {
1266
+ throw new Error(`Task "${options.id}" is not a draft (status is ${draft.status}).`);
1267
+ }
1268
+ if (draft.github != null) {
1269
+ throw new Error(`Task "${options.id}" is already linked to GitHub.`);
1270
+ }
1271
+ const now = /* @__PURE__ */ new Date();
1272
+ const client = new GitHubClient(tokenResult.token);
1273
+ const { owner, repo } = config.github;
1274
+ const issue = await client.createIssue(owner, repo, {
1275
+ title: draft.title,
1276
+ body: draft.description || "",
1277
+ labels: draft.labels
1278
+ });
1279
+ const promoted = issueToTask(issue, draft, now);
1280
+ fs8.unlinkSync(resolved.filePath);
1281
+ const newPath = path7.join(paths.tasksDir, `${issue.number}.json`);
1282
+ writeTaskFile(newPath, promoted);
1283
+ rebuildSnapshot(paths, now);
1284
+ console.log(`Promoted ${draft.id} \u2192 GitHub issue #${issue.number}`);
1285
+ console.log(` File: ${newPath}`);
1286
+ console.log(` URL: ${issue.html_url}`);
1287
+ }
1288
+
1289
+ // src/cli/commands/show.ts
1290
+ async function runShow(options) {
1291
+ const { tasks } = await loadTrailReadContext(process.cwd());
1292
+ const task = tasks.find((t) => t.id === options.id);
1293
+ if (task === void 0) {
1294
+ throw new Error(`No task with id "${options.id}"`);
1295
+ }
1296
+ if (options.json) {
1297
+ printJson(task);
1298
+ return;
1299
+ }
1300
+ console.log(`${task.id} ${task.status} ${task.title}`);
1301
+ if (task.description) {
1302
+ console.log();
1303
+ console.log(task.description);
1304
+ }
1305
+ }
1306
+
1307
+ // src/cli/commands/status.ts
1308
+ async function runStatus(options) {
1309
+ const { config, tasks } = await loadTrailReadContext(process.cwd());
1310
+ const counts = {};
1311
+ for (const s of TaskStatusSchema.options) {
1312
+ counts[s] = 0;
1313
+ }
1314
+ for (const t of tasks) {
1315
+ counts[t.status] = (counts[t.status] ?? 0) + 1;
1316
+ }
1317
+ const payload = { counts };
1318
+ if (config.last_full_sync_at !== void 0) {
1319
+ payload.last_full_sync_at = config.last_full_sync_at;
1320
+ }
1321
+ if (options.json) {
1322
+ printJson(payload);
1323
+ return;
1324
+ }
1325
+ console.log("Tasks by status:");
1326
+ for (const s of TaskStatusSchema.options) {
1327
+ console.log(` ${s}: ${counts[s] ?? 0}`);
1328
+ }
1329
+ if (config.last_full_sync_at !== void 0) {
1330
+ console.log(`Last full sync: ${config.last_full_sync_at}`);
1331
+ } else {
1332
+ console.log("Last full sync: (not recorded)");
1333
+ }
1334
+ }
1335
+
1336
+ // src/cli/commands/sync.ts
1337
+ import fs9 from "fs";
1338
+ async function runSync(options) {
1339
+ const root = findTrailRoot(process.cwd());
1340
+ if (root === null) {
1341
+ const err = {
1342
+ code: "NOT_A_TRAIL_REPO",
1343
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
1344
+ path: process.cwd()
1345
+ };
1346
+ throw err;
1347
+ }
1348
+ const paths = trailPaths(root);
1349
+ const raw = fs9.readFileSync(paths.configPath, "utf-8");
1350
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
1351
+ if (config.sync.preset === "offline") {
1352
+ throw new Error("Cannot sync in offline mode");
1353
+ }
1354
+ const tokenResult = resolveGitHubToken();
1355
+ if (!tokenResult.ok) {
1356
+ throw tokenResult.error;
1357
+ }
1358
+ const client = new GitHubClient(tokenResult.token);
1359
+ const { owner, repo } = config.github;
1360
+ const { tasksDir, snapshotPath } = paths;
1361
+ const pullOnly = options.pull === true && options.push !== true;
1362
+ const pushOnly = options.push === true && options.pull !== true;
1363
+ if (pullOnly) {
1364
+ await pullSync({ client, owner, repo, tasksDir });
1365
+ return;
1366
+ }
1367
+ if (pushOnly) {
1368
+ const tasks = loadAllTasks(tasksDir);
1369
+ await pushSync({ client, owner, repo, tasks });
1370
+ return;
1371
+ }
1372
+ await fullSync({ client, owner, repo, tasksDir, snapshotPath });
1373
+ }
1374
+
1375
+ // src/cli/commands/update.ts
1376
+ import fs10 from "fs";
1377
+ var STATUSES = [
1378
+ "draft",
1379
+ "todo",
1380
+ "in_progress",
1381
+ "in_review",
1382
+ "done",
1383
+ "cancelled"
1384
+ ];
1385
+ function isLinkedTask3(task) {
1386
+ return task.github != null && typeof task.github === "object";
1387
+ }
1388
+ async function runUpdate(options) {
1389
+ const hasField = options.status !== void 0 || options.priority !== void 0 || options.title !== void 0;
1390
+ if (!hasField) {
1391
+ throw new Error("Specify at least one of --status, --priority, or --title");
1392
+ }
1393
+ const root = findTrailRoot(process.cwd());
1394
+ if (root === null) {
1395
+ const err = {
1396
+ code: "NOT_A_TRAIL_REPO",
1397
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
1398
+ path: process.cwd()
1399
+ };
1400
+ throw err;
1401
+ }
1402
+ const paths = trailPaths(root);
1403
+ const raw = fs10.readFileSync(paths.configPath, "utf-8");
1404
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
1405
+ const resolved = findTaskFileById(paths.tasksDir, options.id);
1406
+ if (resolved === null) {
1407
+ throw new Error(`No task with id "${options.id}"`);
1408
+ }
1409
+ const now = /* @__PURE__ */ new Date();
1410
+ const iso = now.toISOString();
1411
+ let next = {
1412
+ ...resolved.task,
1413
+ updated_at: iso
1414
+ };
1415
+ if (options.status !== void 0) {
1416
+ next = { ...next, status: options.status };
1417
+ }
1418
+ if (options.priority !== void 0) {
1419
+ next = { ...next, priority: options.priority };
1420
+ }
1421
+ if (options.title !== void 0) {
1422
+ next = { ...next, title: options.title };
1423
+ }
1424
+ writeTaskFile(resolved.filePath, next);
1425
+ const offline = config.sync.preset === "offline";
1426
+ const tokenResult = resolveGitHubToken();
1427
+ if (!offline && tokenResult.ok && isLinkedTask3(next)) {
1428
+ const client = new GitHubClient(tokenResult.token);
1429
+ const { owner, repo } = config.github;
1430
+ await client.updateIssue(
1431
+ owner,
1432
+ repo,
1433
+ next.github.issue_number,
1434
+ taskToIssueUpdate(next)
1435
+ );
1436
+ const synced = {
1437
+ ...next,
1438
+ github: {
1439
+ ...next.github,
1440
+ synced_at: now.toISOString()
1441
+ }
1442
+ };
1443
+ writeTaskFile(resolved.filePath, synced);
1444
+ }
1445
+ rebuildSnapshot(paths, now);
1446
+ }
1447
+
1448
+ // src/cli/commands/validate.ts
1449
+ import fs11 from "fs";
1450
+ async function runValidate() {
1451
+ const root = findTrailRoot(process.cwd());
1452
+ if (root === null) {
1453
+ const err = {
1454
+ code: "NOT_A_TRAIL_REPO",
1455
+ message: "Not a Trail repository (missing .trail/config.json). Run `trail init` first.",
1456
+ path: process.cwd()
1457
+ };
1458
+ throw err;
1459
+ }
1460
+ const paths = trailPaths(root);
1461
+ const raw = fs11.readFileSync(paths.configPath, "utf-8");
1462
+ TrailConfigSchema.parse(JSON.parse(raw));
1463
+ const tasks = loadAllTasks(paths.tasksDir);
1464
+ const snapshot = compileSnapshot(tasks);
1465
+ for (const w of snapshot.warnings) {
1466
+ console.log(`${w.code}: ${w.message}`);
1467
+ }
1468
+ const hasCycle = snapshot.warnings.some((w) => w.code === DEPENDENCY_CYCLE);
1469
+ return hasCycle ? 1 : 0;
1470
+ }
1471
+
1472
+ // src/cli/run-cli.ts
1473
+ function readCliVersion() {
1474
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1475
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1476
+ return pkg.version ?? "0.0.0";
1477
+ }
1478
+ var TRAIL_ERROR_CODES = /* @__PURE__ */ new Set([
1479
+ "AUTH_REQUIRED",
1480
+ "AUTH_FAILED",
1481
+ "NOT_A_TRAIL_REPO",
1482
+ "VALIDATION_FAILED",
1483
+ "GITHUB_API",
1484
+ "SYNC_CONFLICT"
1485
+ ]);
1486
+ function isTrailError(value) {
1487
+ if (typeof value !== "object" || value === null) {
1488
+ return false;
1489
+ }
1490
+ if (!("code" in value) || !("message" in value)) {
1491
+ return false;
1492
+ }
1493
+ const code = value.code;
1494
+ return typeof code === "string" && TRAIL_ERROR_CODES.has(code);
1495
+ }
1496
+ function getCliFailureMessage(err) {
1497
+ if (isTrailError(err)) {
1498
+ return formatTrailError(err);
1499
+ }
1500
+ if (err instanceof Error && isTaskStoreValidationError(err)) {
1501
+ return formatTrailError(err.trailError);
1502
+ }
1503
+ if (err instanceof ZodError) {
1504
+ return err.message;
1505
+ }
1506
+ if (err instanceof Error) {
1507
+ return err.message;
1508
+ }
1509
+ return String(err);
1510
+ }
1511
+ async function runCli(argv) {
1512
+ const program = new Command();
1513
+ program.name("trail").description("GitHub-native task management CLI").version(readCliVersion());
1514
+ program.command("init").description("Initialize a Trail project in the current git repository").addOption(
1515
+ new Option("--preset <name>", "Sync preset").choices(["solo", "collaborative", "offline"]).default("solo")
1516
+ ).option("--owner <name>", "GitHub repository owner (with --repo)").option("--repo <name>", "GitHub repository name (with --owner)").option("--skip-agents-md", "Do not write AGENTS.md at the repository root").action(
1517
+ (opts) => {
1518
+ runInit({
1519
+ preset: opts.preset,
1520
+ owner: opts.owner,
1521
+ repo: opts.repo,
1522
+ skipAgentsMd: opts.skipAgentsMd === true
1523
+ });
1524
+ }
1525
+ );
1526
+ program.command("sync").description("Synchronize local tasks with GitHub Issues").option("--pull", "Only pull from GitHub").option("--push", "Only push to GitHub").action(async (opts) => {
1527
+ await runSync({ pull: opts.pull, push: opts.push });
1528
+ });
1529
+ program.command("list").description("List tasks (excludes done/cancelled unless --all)").option("--all", "Include done and cancelled tasks").addOption(
1530
+ new Option("--limit <n>", "Max tasks to show").default(25).argParser((v) => {
1531
+ const n = parseInt(v, 10);
1532
+ if (Number.isNaN(n) || n < 0) {
1533
+ throw new Error("--limit must be a non-negative integer");
1534
+ }
1535
+ return n;
1536
+ })
1537
+ ).option("--status <status>", "Filter by status").option("--label <label>", "Filter by label (must appear in task.labels)").option("--json", "Print JSON array of slim task objects").action(
1538
+ async (opts) => {
1539
+ await runList(opts);
1540
+ }
1541
+ );
1542
+ program.command("show").description("Show one task by id").argument("<id>", "Task id").option("--json", "Print full task JSON").action(async (id, opts) => {
1543
+ await runShow({ id, json: opts.json });
1544
+ });
1545
+ program.command("status").description("Task counts by status and last sync time").option("--json", "Print JSON").action(async (opts) => {
1546
+ await runStatus(opts);
1547
+ });
1548
+ program.command("next").description("Pick the next actionable task by priority and id").option("--json", "Print full task JSON or null").action(async (opts) => {
1549
+ await runNext(opts);
1550
+ });
1551
+ program.command("update").description("Update a task (local file; pushes to GitHub when linked and online)").argument("<id>", "Task id").addOption(new Option("--status <status>", "New status").choices(STATUSES)).addOption(
1552
+ new Option("--priority <p>", "Priority").choices(["p0", "p1", "p2", "p3"])
1553
+ ).option("--title <text>", "New title").action(
1554
+ async (id, opts) => {
1555
+ await runUpdate({
1556
+ id,
1557
+ status: opts.status,
1558
+ priority: opts.priority,
1559
+ title: opts.title
1560
+ });
1561
+ }
1562
+ );
1563
+ program.command("done").description("Mark a task done, optionally comment and close the GitHub issue").argument("<id>", "Task id").argument("<message...>", "Comment body for the linked issue (and suggested commit hint)").action(async (id, messageParts) => {
1564
+ const message = messageParts.join(" ").trim();
1565
+ if (message === "") {
1566
+ throw new Error("Message is required");
1567
+ }
1568
+ await runDone({ id, message });
1569
+ });
1570
+ program.command("validate").description("Compile snapshot and print validation warnings (exit 1 on dependency cycles)").action(async () => {
1571
+ const code = await runValidate();
1572
+ process.exitCode = code;
1573
+ });
1574
+ program.command("create").description("Create a local draft task (not linked to GitHub until promoted)").requiredOption("--title <text>", "Task title").option("--description <text>", "Description body").addOption(
1575
+ new Option("--type <name>", "Task type").choices(["feature", "bug", "chore", "epic"])
1576
+ ).addOption(new Option("--priority <p>", "Priority").choices(["p0", "p1", "p2", "p3"])).action(
1577
+ (opts) => {
1578
+ runCreate({
1579
+ title: opts.title,
1580
+ description: opts.description,
1581
+ type: opts.type,
1582
+ priority: opts.priority
1583
+ });
1584
+ }
1585
+ );
1586
+ program.command("promote").description("Promote a draft task to a GitHub issue and rename the task file").argument("<id>", "Draft task id").action(async (id) => {
1587
+ await runPromote({ id });
1588
+ });
1589
+ const dep = program.command("dep").description("Add or remove task dependencies");
1590
+ dep.command("add").description("Record that task A depends on task B (updates blocks/depends_on)").argument("<taskId>", "Task id").argument("<dependsOnId>", "Id of the task that must be satisfied first").action((taskId, dependsOnId) => {
1591
+ runDepAdd(taskId, dependsOnId);
1592
+ });
1593
+ dep.command("remove").description("Remove a dependency edge between two tasks").argument("<taskId>", "Task id").argument("<dependsOnId>", "Other task id").action((taskId, dependsOnId) => {
1594
+ runDepRemove(taskId, dependsOnId);
1595
+ });
1596
+ program.command("graph").description("Print task dependency edges").option("--json", "Print JSON array of { from, to } edges").action((opts) => {
1597
+ runGraph(opts);
1598
+ });
1599
+ program.command("context").description("Print a compact JSON context packet for an LLM session").argument("<id>", "Task id").action((id) => {
1600
+ runContext({ id });
1601
+ });
1602
+ program.command("mcp").description("Run the Trail MCP server (stdio; for editor and agent integrations)").action(async () => {
1603
+ const { runMcpServer } = await import("./run-mcp-server-ERHZKM7M.js");
1604
+ await runMcpServer();
1605
+ });
1606
+ await program.parseAsync(argv);
1607
+ }
1608
+
1609
+ export {
1610
+ __commonJS,
1611
+ __toESM,
1612
+ DEPENDENCY_CYCLE,
1613
+ compileSnapshot,
1614
+ buildContextPacket,
1615
+ listDependencyEdges,
1616
+ loadTrailReadContext,
1617
+ selectTasksForList,
1618
+ selectNextTask,
1619
+ getCliFailureMessage,
1620
+ runCli
1621
+ };
1622
+ //# sourceMappingURL=chunk-SFBMB3UT.js.map