@trail-pm/cli 0.1.9 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,28 +4,21 @@
4
4
 
5
5
  ## Install
6
6
 
7
- **Recommended:** add it to your app repo, then invoke with **`npx trail`** (local install does **not** put `trail` on your shell `PATH`):
7
+ **Recommended:** dev dependency in your app repo, then **`npx trail`**:
8
8
 
9
9
  ```bash
10
- npm install @trail-pm/cli
11
- # or: npm install -D @trail-pm/cli
10
+ npm install -D @trail-pm/cli
12
11
  npx trail --help
13
- npx trail init --preset solo
12
+ npx trail init
14
13
  ```
15
14
 
16
- **Global (optional):** `npm install -g @trail-pm/cli` then **`trail`** works as a normal command.
15
+ **Alternative:** `npm install -g @trail-pm/cli` so the `trail` binary is on your `PATH`.
17
16
 
18
- **Without installing:** use **`npx @trail-pm/cli <command>`** each time.
19
-
20
- Installing the package does **not** create `.trail/`; run **`npx trail init`** inside a git repo. Full guide:
21
-
22
- **https://github.com/joeydekruis/trail#installation**
17
+ Full instructions: **https://github.com/joeydekruis/trail#installation**
23
18
 
24
19
  ## Usage
25
20
 
26
- ```bash
27
- npx trail init --preset solo
28
- ```
21
+ Run **`npx trail init`** in a git repo; the CLI will ask for a sync preset unless you pass **`--preset`**.
29
22
 
30
23
  ## License
31
24
 
@@ -510,8 +510,8 @@ var GitHubClient = class {
510
510
  this.token = token;
511
511
  this.baseUrl = normalizeBaseUrl(baseUrl);
512
512
  }
513
- async request(method, path8, body) {
514
- const url = `${this.baseUrl}${path8.startsWith("/") ? path8 : `/${path8}`}`;
513
+ async request(method, path9, body) {
514
+ const url = `${this.baseUrl}${path9.startsWith("/") ? path9 : `/${path9}`}`;
515
515
  const headers = new Headers({
516
516
  Authorization: `Bearer ${this.token}`,
517
517
  Accept: "application/vnd.github+json",
@@ -541,29 +541,29 @@ var GitHubClient = class {
541
541
  per_page: String(params.per_page),
542
542
  page: String(params.page)
543
543
  });
544
- const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${search}`;
545
- const response = await this.request("GET", path8);
544
+ const path9 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${search}`;
545
+ const response = await this.request("GET", path9);
546
546
  return this.parseJson(response);
547
547
  }
548
548
  async getIssue(owner, repo, issueNumber) {
549
- const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
550
- const response = await this.request("GET", path8);
549
+ const path9 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
550
+ const response = await this.request("GET", path9);
551
551
  return this.parseJson(response);
552
552
  }
553
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);
554
+ const path9 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
555
+ const response = await this.request("PATCH", path9, patch);
556
556
  return this.parseJson(response);
557
557
  }
558
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 });
559
+ const path9 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/comments`;
560
+ const response = await this.request("POST", path9, { body });
561
561
  return this.parseJson(response);
562
562
  }
563
563
  /** Create a new issue. Returns the created issue (same shape as list/get). */
564
564
  async createIssue(owner, repo, input) {
565
- const path8 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
566
- const response = await this.request("POST", path8, {
565
+ const path9 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
566
+ const response = await this.request("POST", path9, {
567
567
  title: input.title,
568
568
  body: input.body ?? "",
569
569
  labels: input.labels ?? []
@@ -642,12 +642,64 @@ function taskToIssueUpdate(task) {
642
642
  };
643
643
  }
644
644
 
645
+ // src/core/relevant-tasks.ts
646
+ var NON_TERMINAL = [
647
+ "draft",
648
+ "todo",
649
+ "in_progress",
650
+ "in_review"
651
+ ];
652
+ function isNonTerminalStatus(status) {
653
+ return NON_TERMINAL.includes(status);
654
+ }
655
+ function isActiveForRelevance(task) {
656
+ return isNonTerminalStatus(task.status);
657
+ }
658
+ function computeRelevantTaskIds(tasks) {
659
+ const byId = /* @__PURE__ */ new Map();
660
+ for (const t of tasks) {
661
+ byId.set(t.id, t);
662
+ }
663
+ const relevant = /* @__PURE__ */ new Set();
664
+ const queue = [];
665
+ for (const t of tasks) {
666
+ if (isActiveForRelevance(t)) {
667
+ if (!relevant.has(t.id)) {
668
+ relevant.add(t.id);
669
+ queue.push(t.id);
670
+ }
671
+ }
672
+ }
673
+ function enqueue(id) {
674
+ if (relevant.has(id)) return;
675
+ relevant.add(id);
676
+ queue.push(id);
677
+ }
678
+ while (queue.length > 0) {
679
+ const id = queue.pop();
680
+ const task = byId.get(id);
681
+ if (!task) continue;
682
+ for (const d of task.depends_on) {
683
+ if (byId.has(d)) enqueue(d);
684
+ }
685
+ for (const b of task.blocks) {
686
+ if (byId.has(b)) enqueue(b);
687
+ }
688
+ if (task.parent != null && task.parent !== "" && byId.has(task.parent)) {
689
+ enqueue(task.parent);
690
+ }
691
+ }
692
+ return relevant;
693
+ }
694
+
645
695
  // src/core/sync.ts
646
696
  var ISSUES_PER_PAGE = 100;
647
697
  async function pullSync(options) {
648
698
  const { client, owner, repo, tasksDir } = options;
649
699
  const now = options.now ?? /* @__PURE__ */ new Date();
700
+ const onProgress = options.onProgress;
650
701
  let page = 1;
702
+ let issuesSoFar = 0;
651
703
  for (; ; ) {
652
704
  const issues = await client.listIssues(owner, repo, {
653
705
  state: "all",
@@ -666,6 +718,13 @@ async function pullSync(options) {
666
718
  const task = issueToTask(issue, existing, now);
667
719
  writeTaskFile(filePath, task);
668
720
  }
721
+ issuesSoFar += issues.length;
722
+ onProgress?.({
723
+ phase: "pull",
724
+ page,
725
+ issuesInPage: issues.length,
726
+ issuesSoFar
727
+ });
669
728
  if (issues.length < ISSUES_PER_PAGE) {
670
729
  break;
671
730
  }
@@ -677,22 +736,49 @@ function isLinkedTask(task) {
677
736
  }
678
737
  async function pushSync(options) {
679
738
  const { client, owner, repo, tasks } = options;
680
- for (const task of tasks) {
739
+ const only = options.onlyTaskIds;
740
+ const onProgress = options.onProgress;
741
+ const toPush = tasks.filter((task) => {
681
742
  if (task.status === "draft") {
682
- continue;
743
+ return false;
744
+ }
745
+ if (!isLinkedTask(task)) {
746
+ return false;
683
747
  }
748
+ if (only !== void 0 && !only.has(task.id)) {
749
+ return false;
750
+ }
751
+ return true;
752
+ });
753
+ let index = 0;
754
+ for (const task of toPush) {
684
755
  if (!isLinkedTask(task)) {
685
756
  continue;
686
757
  }
758
+ index += 1;
759
+ onProgress?.({
760
+ phase: "push",
761
+ index,
762
+ total: toPush.length,
763
+ taskId: task.id
764
+ });
687
765
  const patch = taskToIssueUpdate(task);
688
766
  await client.updateIssue(owner, repo, task.github.issue_number, patch);
689
767
  }
690
768
  }
691
769
  async function fullSync(options) {
692
770
  const { client, owner, repo, tasksDir, snapshotPath, now } = options;
693
- await pullSync({ client, owner, repo, tasksDir, now });
771
+ await pullSync({ client, owner, repo, tasksDir, now, onProgress: options.onProgress });
694
772
  const tasks = loadAllTasks(tasksDir);
695
- await pushSync({ client, owner, repo, tasks });
773
+ const relevant = computeRelevantTaskIds(tasks);
774
+ await pushSync({
775
+ client,
776
+ owner,
777
+ repo,
778
+ tasks,
779
+ onlyTaskIds: relevant,
780
+ onProgress: options.onProgress
781
+ });
696
782
  const snapshot = compileSnapshot(tasks, now);
697
783
  writeSnapshot(snapshotPath, snapshot);
698
784
  }
@@ -1216,6 +1302,31 @@ function runInit(options) {
1216
1302
  console.log(`Initialized Trail project at ${root}`);
1217
1303
  }
1218
1304
 
1305
+ // src/cli/prompt-preset.ts
1306
+ import * as readline from "readline/promises";
1307
+ import { stdin, stdout } from "process";
1308
+ async function promptPresetWhenInteractive() {
1309
+ const rl = readline.createInterface({ input: stdin, output: stdout });
1310
+ try {
1311
+ console.log("");
1312
+ console.log("How should Trail sync with GitHub?");
1313
+ console.log(" 1 \u2014 Solo: you run `trail sync` when you want (typical for individual work)");
1314
+ console.log(
1315
+ " 2 \u2014 Collaborative: optional auto-pull before reads; good for shared repos"
1316
+ );
1317
+ console.log(" 3 \u2014 Offline: local tasks only (no GitHub API)");
1318
+ const raw = (await rl.question("Enter 1, 2, or 3 [1]: ")).trim();
1319
+ const choice = raw === "" ? "1" : raw;
1320
+ if (choice === "1") return "solo";
1321
+ if (choice === "2") return "collaborative";
1322
+ if (choice === "3") return "offline";
1323
+ console.log("Unknown choice, using solo.");
1324
+ return "solo";
1325
+ } finally {
1326
+ rl.close();
1327
+ }
1328
+ }
1329
+
1219
1330
  // src/cli/commands/next.ts
1220
1331
  async function runNext(options) {
1221
1332
  const { tasks } = await loadTrailReadContext(process.cwd());
@@ -1237,8 +1348,27 @@ async function runNext(options) {
1237
1348
  }
1238
1349
 
1239
1350
  // src/cli/commands/promote.ts
1351
+ import fs9 from "fs";
1352
+ import path8 from "path";
1353
+
1354
+ // src/core/link-draft-issue.ts
1240
1355
  import fs8 from "fs";
1241
1356
  import path7 from "path";
1357
+ async function linkDraftToNewGitHubIssue(options) {
1358
+ const { client, owner, repo, draft, draftFilePath, tasksDir, now } = options;
1359
+ const issue = await client.createIssue(owner, repo, {
1360
+ title: draft.title,
1361
+ body: draft.description ?? "",
1362
+ labels: draft.labels
1363
+ });
1364
+ const promoted = issueToTask(issue, draft, now);
1365
+ fs8.unlinkSync(draftFilePath);
1366
+ const newPath = path7.join(tasksDir, `${issue.number}.json`);
1367
+ writeTaskFile(newPath, promoted);
1368
+ return promoted;
1369
+ }
1370
+
1371
+ // src/cli/commands/promote.ts
1242
1372
  async function runPromote(options) {
1243
1373
  const root = findTrailRoot(process.cwd());
1244
1374
  if (root === null) {
@@ -1250,7 +1380,7 @@ async function runPromote(options) {
1250
1380
  throw err;
1251
1381
  }
1252
1382
  const paths = trailPaths(root);
1253
- const raw = fs8.readFileSync(paths.configPath, "utf-8");
1383
+ const raw = fs9.readFileSync(paths.configPath, "utf-8");
1254
1384
  const config = TrailConfigSchema.parse(JSON.parse(raw));
1255
1385
  if (config.sync.preset === "offline") {
1256
1386
  throw new Error("Cannot promote a draft in offline mode (no GitHub access).");
@@ -1273,19 +1403,19 @@ async function runPromote(options) {
1273
1403
  const now = /* @__PURE__ */ new Date();
1274
1404
  const client = new GitHubClient(tokenResult.token);
1275
1405
  const { owner, repo } = config.github;
1276
- const issue = await client.createIssue(owner, repo, {
1277
- title: draft.title,
1278
- body: draft.description || "",
1279
- labels: draft.labels
1406
+ const promoted = await linkDraftToNewGitHubIssue({
1407
+ client,
1408
+ owner,
1409
+ repo,
1410
+ draft,
1411
+ draftFilePath: resolved.filePath,
1412
+ tasksDir: paths.tasksDir,
1413
+ now
1280
1414
  });
1281
- const promoted = issueToTask(issue, draft, now);
1282
- fs8.unlinkSync(resolved.filePath);
1283
- const newPath = path7.join(paths.tasksDir, `${issue.number}.json`);
1284
- writeTaskFile(newPath, promoted);
1285
1415
  rebuildSnapshot(paths, now);
1286
- console.log(`Promoted ${draft.id} \u2192 GitHub issue #${issue.number}`);
1287
- console.log(` File: ${newPath}`);
1288
- console.log(` URL: ${issue.html_url}`);
1416
+ console.log(`Promoted ${draft.id} \u2192 GitHub issue #${promoted.github?.issue_number}`);
1417
+ console.log(` File: ${path8.join(paths.tasksDir, `${promoted.github?.issue_number}.json`)}`);
1418
+ console.log(` URL: ${promoted.github?.url}`);
1289
1419
  }
1290
1420
 
1291
1421
  // src/cli/commands/show.ts
@@ -1336,7 +1466,28 @@ async function runStatus(options) {
1336
1466
  }
1337
1467
 
1338
1468
  // src/cli/commands/sync.ts
1339
- import fs9 from "fs";
1469
+ import fs11 from "fs";
1470
+
1471
+ // src/core/config-update.ts
1472
+ import fs10 from "fs";
1473
+ function writeLastFullSyncAt(paths, iso) {
1474
+ const raw = fs10.readFileSync(paths.configPath, "utf-8");
1475
+ const config = TrailConfigSchema.parse(JSON.parse(raw));
1476
+ const next = { ...config, last_full_sync_at: iso };
1477
+ fs10.writeFileSync(paths.configPath, `${JSON.stringify(next, null, 2)}
1478
+ `, "utf-8");
1479
+ }
1480
+
1481
+ // src/cli/commands/sync.ts
1482
+ function logSyncProgress(p) {
1483
+ if (p.phase === "pull") {
1484
+ console.error(
1485
+ `Sync: GitHub pull page ${p.page} \u2014 ${p.issuesInPage} issues (${p.issuesSoFar} total fetched so far)`
1486
+ );
1487
+ } else {
1488
+ console.error(`Sync: pushing to GitHub ${p.index}/${p.total} (task ${p.taskId})`);
1489
+ }
1490
+ }
1340
1491
  async function runSync(options) {
1341
1492
  const root = findTrailRoot(process.cwd());
1342
1493
  if (root === null) {
@@ -1348,7 +1499,7 @@ async function runSync(options) {
1348
1499
  throw err;
1349
1500
  }
1350
1501
  const paths = trailPaths(root);
1351
- const raw = fs9.readFileSync(paths.configPath, "utf-8");
1502
+ const raw = fs11.readFileSync(paths.configPath, "utf-8");
1352
1503
  const config = TrailConfigSchema.parse(JSON.parse(raw));
1353
1504
  if (config.sync.preset === "offline") {
1354
1505
  throw new Error("Cannot sync in offline mode");
@@ -1363,19 +1514,35 @@ async function runSync(options) {
1363
1514
  const pullOnly = options.pull === true && options.push !== true;
1364
1515
  const pushOnly = options.push === true && options.pull !== true;
1365
1516
  if (pullOnly) {
1366
- await pullSync({ client, owner, repo, tasksDir });
1517
+ await pullSync({ client, owner, repo, tasksDir, onProgress: logSyncProgress });
1367
1518
  return;
1368
1519
  }
1369
1520
  if (pushOnly) {
1370
1521
  const tasks = loadAllTasks(tasksDir);
1371
- await pushSync({ client, owner, repo, tasks });
1522
+ const relevant = computeRelevantTaskIds(tasks);
1523
+ await pushSync({
1524
+ client,
1525
+ owner,
1526
+ repo,
1527
+ tasks,
1528
+ onlyTaskIds: relevant,
1529
+ onProgress: logSyncProgress
1530
+ });
1372
1531
  return;
1373
1532
  }
1374
- await fullSync({ client, owner, repo, tasksDir, snapshotPath });
1533
+ await fullSync({
1534
+ client,
1535
+ owner,
1536
+ repo,
1537
+ tasksDir,
1538
+ snapshotPath,
1539
+ onProgress: logSyncProgress
1540
+ });
1541
+ writeLastFullSyncAt(paths, (/* @__PURE__ */ new Date()).toISOString());
1375
1542
  }
1376
1543
 
1377
1544
  // src/cli/commands/update.ts
1378
- import fs10 from "fs";
1545
+ import fs12 from "fs";
1379
1546
  var STATUSES = [
1380
1547
  "draft",
1381
1548
  "todo",
@@ -1402,7 +1569,7 @@ async function runUpdate(options) {
1402
1569
  throw err;
1403
1570
  }
1404
1571
  const paths = trailPaths(root);
1405
- const raw = fs10.readFileSync(paths.configPath, "utf-8");
1572
+ const raw = fs12.readFileSync(paths.configPath, "utf-8");
1406
1573
  const config = TrailConfigSchema.parse(JSON.parse(raw));
1407
1574
  const resolved = findTaskFileById(paths.tasksDir, options.id);
1408
1575
  if (resolved === null) {
@@ -1448,7 +1615,7 @@ async function runUpdate(options) {
1448
1615
  }
1449
1616
 
1450
1617
  // src/cli/commands/validate.ts
1451
- import fs11 from "fs";
1618
+ import fs13 from "fs";
1452
1619
  async function runValidate() {
1453
1620
  const root = findTrailRoot(process.cwd());
1454
1621
  if (root === null) {
@@ -1460,7 +1627,7 @@ async function runValidate() {
1460
1627
  throw err;
1461
1628
  }
1462
1629
  const paths = trailPaths(root);
1463
- const raw = fs11.readFileSync(paths.configPath, "utf-8");
1630
+ const raw = fs13.readFileSync(paths.configPath, "utf-8");
1464
1631
  TrailConfigSchema.parse(JSON.parse(raw));
1465
1632
  const tasks = loadAllTasks(paths.tasksDir);
1466
1633
  const snapshot = compileSnapshot(tasks);
@@ -1514,11 +1681,22 @@ async function runCli(argv) {
1514
1681
  const program = new Command();
1515
1682
  program.name("trail").description("GitHub-native task management CLI").version(readCliVersion());
1516
1683
  program.command("init").description("Initialize a Trail project in the current git repository").addOption(
1517
- new Option("--preset <name>", "Sync preset").choices(["solo", "collaborative", "offline"]).default("solo")
1684
+ new Option(
1685
+ "--preset <name>",
1686
+ "Sync preset: solo | collaborative | offline (omit to choose in the terminal)"
1687
+ ).choices(["solo", "collaborative", "offline"])
1518
1688
  ).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(
1519
- (opts) => {
1689
+ async (opts) => {
1690
+ let preset;
1691
+ if (opts.preset !== void 0) {
1692
+ preset = opts.preset;
1693
+ } else if (process.stdin.isTTY) {
1694
+ preset = await promptPresetWhenInteractive();
1695
+ } else {
1696
+ preset = "solo";
1697
+ }
1520
1698
  runInit({
1521
- preset: opts.preset,
1699
+ preset,
1522
1700
  owner: opts.owner,
1523
1701
  repo: opts.repo,
1524
1702
  skipAgentsMd: opts.skipAgentsMd === true
@@ -1602,7 +1780,7 @@ async function runCli(argv) {
1602
1780
  runContext({ id });
1603
1781
  });
1604
1782
  program.command("mcp").description("Run the Trail MCP server (stdio; for editor and agent integrations)").action(async () => {
1605
- const { runMcpServer } = await import("./run-mcp-server-HD7I5M7Z.js");
1783
+ const { runMcpServer } = await import("./run-mcp-server-NKYRI73A.js");
1606
1784
  await runMcpServer();
1607
1785
  });
1608
1786
  await program.parseAsync(argv);
@@ -1621,4 +1799,4 @@ export {
1621
1799
  getCliFailureMessage,
1622
1800
  runCli
1623
1801
  };
1624
- //# sourceMappingURL=chunk-TDIYLUKH.js.map
1802
+ //# sourceMappingURL=chunk-YSTYANXJ.js.map