@gitpm/cli 0.1.0 → 0.1.15

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 CHANGED
@@ -2,35 +2,16 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import { dirname, join as join2 } from "path";
5
+ import { dirname as dirname4, join as join3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { Command as Command7 } from "commander";
7
+ import { Command as Command20 } from "commander";
8
8
 
9
- // src/commands/import.ts
10
- import { importFromGitHub } from "@gitpm/sync-github";
11
- import { importFromGitLab } from "@gitpm/sync-gitlab";
9
+ // src/commands/archive.ts
10
+ import { readFile, writeFile } from "fs/promises";
11
+ import { join } from "path";
12
+ import { archiveOldEntities } from "@gitpm/core";
12
13
  import chalk2 from "chalk";
13
14
  import { Command } from "commander";
14
- import ora from "ora";
15
-
16
- // src/utils/auth.ts
17
- import { execSync } from "child_process";
18
- async function resolveToken(cliToken) {
19
- if (cliToken) return cliToken;
20
- const envToken = process.env.GITHUB_TOKEN;
21
- if (envToken) return envToken;
22
- try {
23
- const ghToken = execSync("gh auth token", {
24
- encoding: "utf-8",
25
- stdio: ["pipe", "pipe", "pipe"]
26
- }).trim();
27
- if (ghToken) return ghToken;
28
- } catch {
29
- }
30
- throw new Error(
31
- "No GitHub token found. Provide via --token flag, GITHUB_TOKEN env var, or install GitHub CLI (gh) and run 'gh auth login'."
32
- );
33
- }
34
15
 
35
16
  // src/utils/config.ts
36
17
  import { resolve } from "path";
@@ -49,6 +30,681 @@ function printError(msg) {
49
30
  function printWarning(msg) {
50
31
  console.warn(chalk.yellow(`\u26A0 ${msg}`));
51
32
  }
33
+ function progressBar(ratio, width) {
34
+ const filled = Math.round(ratio * width);
35
+ const empty = width - filled;
36
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
37
+ if (ratio >= 0.75) return chalk.green(bar);
38
+ if (ratio >= 0.25) return chalk.yellow(bar);
39
+ return chalk.red(bar);
40
+ }
41
+
42
+ // src/commands/archive.ts
43
+ async function cleanupSyncState(metaDir, entityIds) {
44
+ const statePath = join(metaDir, "sync", "github-state.json");
45
+ let raw;
46
+ try {
47
+ raw = await readFile(statePath, "utf-8");
48
+ } catch {
49
+ return 0;
50
+ }
51
+ const state = JSON.parse(raw);
52
+ let removed = 0;
53
+ for (const id of entityIds) {
54
+ if (state.entities?.[id]) {
55
+ delete state.entities[id];
56
+ removed++;
57
+ }
58
+ }
59
+ if (removed > 0) {
60
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}
61
+ `, "utf-8");
62
+ }
63
+ return removed;
64
+ }
65
+ var archiveCommand = new Command("archive").description(
66
+ "Move done/cancelled entities older than N days to .meta/archive/"
67
+ ).option(
68
+ "-d, --days <number>",
69
+ "Number of days after which done items are archived",
70
+ "7"
71
+ ).option("--dry-run", "List files that would be archived without moving them").action(async (opts, cmd) => {
72
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
73
+ const daysOld = Number.parseInt(opts.days, 10);
74
+ const dryRun = opts.dryRun ?? false;
75
+ if (Number.isNaN(daysOld) || daysOld < 0) {
76
+ printError("--days must be a non-negative integer");
77
+ process.exit(1);
78
+ }
79
+ if (dryRun) {
80
+ printWarning("Dry run \u2014 no files will be moved");
81
+ }
82
+ const result = await archiveOldEntities(metaDir, { daysOld, dryRun });
83
+ if (!result.ok) {
84
+ printError(result.error.message);
85
+ process.exit(1);
86
+ }
87
+ const { archivedFiles, archivedEntityIds } = result.value;
88
+ if (archivedFiles.length === 0) {
89
+ console.log(
90
+ chalk2.dim(
91
+ `No done/cancelled entities older than ${daysOld} day(s) found.`
92
+ )
93
+ );
94
+ return;
95
+ }
96
+ for (const file of archivedFiles) {
97
+ const label = dryRun ? "would archive" : "archived";
98
+ console.log(chalk2.dim(` ${label}: ${file}`));
99
+ }
100
+ if (!dryRun && archivedEntityIds.length > 0) {
101
+ const removed = await cleanupSyncState(metaDir, archivedEntityIds);
102
+ if (removed > 0) {
103
+ console.log(chalk2.dim(` removed ${removed} entries from sync state`));
104
+ }
105
+ }
106
+ console.log();
107
+ if (dryRun) {
108
+ printWarning(
109
+ `${archivedFiles.length} file(s) would be archived (dry run)`
110
+ );
111
+ } else {
112
+ printSuccess(
113
+ `Archived ${archivedFiles.length} file(s) to .meta/archive/`
114
+ );
115
+ }
116
+ });
117
+
118
+ // src/commands/audit.ts
119
+ import { relative } from "path";
120
+ import {
121
+ auditTree,
122
+ parseTree,
123
+ resolveRefs
124
+ } from "@gitpm/core";
125
+ import chalk3 from "chalk";
126
+ import { Command as Command2 } from "commander";
127
+ function printSection(title, items) {
128
+ if (items.length === 0) return;
129
+ console.log(chalk3.bold.yellow(` ${title} (${items.length})`));
130
+ for (const item of items) {
131
+ const rel = relative(process.cwd(), item.filePath);
132
+ console.log(
133
+ ` ${chalk3.dim("\u2022")} ${item.title} ${chalk3.dim("\u2014")} ${item.reason}`
134
+ );
135
+ console.log(` ${chalk3.dim(rel)}`);
136
+ }
137
+ console.log();
138
+ }
139
+ function printDuplicates(pairs) {
140
+ if (pairs.length === 0) return;
141
+ console.log(chalk3.bold.yellow(` Duplicate Candidates (${pairs.length})`));
142
+ for (const { a, b, similarity } of pairs) {
143
+ const pct3 = Math.round(similarity * 100);
144
+ console.log(
145
+ ` ${chalk3.dim("\u2022")} ${a.title} ${chalk3.dim("\u2194")} ${b.title} ${chalk3.cyan(`${pct3}%`)}`
146
+ );
147
+ }
148
+ console.log();
149
+ }
150
+ function printReport(report) {
151
+ console.log();
152
+ console.log(chalk3.bold(" Project Audit"));
153
+ console.log(chalk3.dim(` ${"\u2500".repeat(13)}`));
154
+ console.log(
155
+ ` ${report.summary.total} entities scanned, ${report.summary.issues} issue(s) found`
156
+ );
157
+ console.log();
158
+ printSection("Stale Stories", report.stale);
159
+ printSection("Orphan Stories (no epic)", report.orphans);
160
+ printSection("Empty Bodies", report.emptyBodies);
161
+ printSection("Zombie Epics (all stories done)", report.zombieEpics);
162
+ printDuplicates(report.duplicates);
163
+ }
164
+ var STALE_BUCKETS = [
165
+ { label: "30-59 days", min: 30, max: 60 },
166
+ { label: "60-89 days", min: 60, max: 90 },
167
+ { label: "90+ days", min: 90, max: Number.POSITIVE_INFINITY }
168
+ ];
169
+ var MARKDOWN_DEFAULT_STALE_DAYS = STALE_BUCKETS[0].min;
170
+ function staleBucketLabel(item) {
171
+ if (item.daysSinceUpdate == null) return "Unknown age";
172
+ if (item.daysSinceUpdate < STALE_BUCKETS[0].min) return "< 30 days";
173
+ for (const bucket of STALE_BUCKETS) {
174
+ if (item.daysSinceUpdate >= bucket.min && item.daysSinceUpdate < bucket.max) {
175
+ return bucket.label;
176
+ }
177
+ }
178
+ return STALE_BUCKETS[STALE_BUCKETS.length - 1].label;
179
+ }
180
+ function groupStaleByBucket(items) {
181
+ const order = [
182
+ "< 30 days",
183
+ ...STALE_BUCKETS.map((b) => b.label),
184
+ "Unknown age"
185
+ ];
186
+ const groups = /* @__PURE__ */ new Map();
187
+ for (const item of items) {
188
+ const label = staleBucketLabel(item);
189
+ const bucket = groups.get(label) ?? [];
190
+ bucket.push(item);
191
+ groups.set(label, bucket);
192
+ }
193
+ return order.filter((label) => groups.has(label)).map((label) => [label, groups.get(label)]);
194
+ }
195
+ function escapeMdCell(value) {
196
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
197
+ }
198
+ function mdTable(headers, rows) {
199
+ const headerLine = `| ${headers.join(" | ")} |`;
200
+ const sepLine = `| ${headers.map(() => "---").join(" | ")} |`;
201
+ const rowLines = rows.map((row) => `| ${row.join(" | ")} |`);
202
+ return [headerLine, sepLine, ...rowLines].join("\n");
203
+ }
204
+ function itemToRow(item) {
205
+ return [
206
+ escapeMdCell(item.id),
207
+ escapeMdCell(item.title),
208
+ escapeMdCell(relative(process.cwd(), item.filePath)),
209
+ escapeMdCell(item.reason)
210
+ ];
211
+ }
212
+ function mdItemSection(title, items) {
213
+ const lines = [`## ${title} (${items.length})`, ""];
214
+ if (items.length === 0) {
215
+ lines.push("None.", "");
216
+ return lines;
217
+ }
218
+ lines.push(
219
+ mdTable(["ID", "Title", "File", "Reason"], items.map(itemToRow)),
220
+ ""
221
+ );
222
+ return lines;
223
+ }
224
+ function buildMarkdownReport(report) {
225
+ const lines = [
226
+ "# Project Audit",
227
+ "",
228
+ `${report.summary.total} entities scanned, ${report.summary.issues} issue(s) found.`,
229
+ ""
230
+ ];
231
+ lines.push(`## Stale (${report.stale.length})`, "");
232
+ if (report.stale.length === 0) {
233
+ lines.push("None.", "");
234
+ } else {
235
+ for (const [label, items] of groupStaleByBucket(report.stale)) {
236
+ lines.push(
237
+ `### ${label} (${items.length})`,
238
+ "",
239
+ mdTable(["ID", "Title", "File", "Reason"], items.map(itemToRow)),
240
+ ""
241
+ );
242
+ }
243
+ }
244
+ lines.push(...mdItemSection("Orphans", report.orphans));
245
+ lines.push(`## Duplicates (${report.duplicates.length})`, "");
246
+ if (report.duplicates.length === 0) {
247
+ lines.push("None.", "");
248
+ } else {
249
+ lines.push(
250
+ mdTable(
251
+ ["A", "B", "Similarity"],
252
+ report.duplicates.map((d) => [
253
+ escapeMdCell(`${d.a.title} (${d.a.id})`),
254
+ escapeMdCell(`${d.b.title} (${d.b.id})`),
255
+ `${Math.round(d.similarity * 100)}%`
256
+ ])
257
+ ),
258
+ ""
259
+ );
260
+ }
261
+ lines.push(...mdItemSection("Empty bodies", report.emptyBodies));
262
+ lines.push(...mdItemSection("Zombie epics", report.zombieEpics));
263
+ return lines.join("\n");
264
+ }
265
+ var auditCommand = new Command2("audit").description(
266
+ "Detect stale issues, orphans, duplicates, and project hygiene issues"
267
+ ).option(
268
+ "--stale-days <days>",
269
+ `Days before a todo is considered stale (${MARKDOWN_DEFAULT_STALE_DAYS} with --markdown unless set)`,
270
+ (value) => Number.parseInt(value, 10),
271
+ 90
272
+ ).option("--json", "Output as JSON").option(
273
+ "--markdown",
274
+ "Output as a Markdown report with age-tiered stale sections (for CI artifacts)"
275
+ ).action(async (opts, cmd) => {
276
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
277
+ const parseResult = await parseTree(metaDir);
278
+ if (!parseResult.ok) {
279
+ printError(parseResult.error.message);
280
+ process.exit(1);
281
+ }
282
+ const resolveResult = resolveRefs(parseResult.value);
283
+ if (!resolveResult.ok) {
284
+ printError(resolveResult.error.message);
285
+ process.exit(1);
286
+ }
287
+ const staleDaysIsDefault = cmd.getOptionValueSource("staleDays") === "default";
288
+ const staleDays = opts.markdown && staleDaysIsDefault ? MARKDOWN_DEFAULT_STALE_DAYS : opts.staleDays;
289
+ const report = auditTree(resolveResult.value, { staleDays });
290
+ if (opts.markdown) {
291
+ console.log(buildMarkdownReport(report));
292
+ } else if (opts.json) {
293
+ console.log(JSON.stringify(report, null, 2));
294
+ } else if (report.summary.issues === 0) {
295
+ printSuccess("No issues found \u2014 project is clean!");
296
+ } else {
297
+ printReport(report);
298
+ }
299
+ if (report.summary.issues > 0) {
300
+ process.exit(1);
301
+ }
302
+ });
303
+
304
+ // src/commands/commit.ts
305
+ import { execFileSync, execSync } from "child_process";
306
+ import { Command as Command3 } from "commander";
307
+ var commitCommand = new Command3("commit").description("Commit all .meta/ changes atomically").requiredOption("-m, --message <message>", "Commit message").option("--author <author>", 'Git author (e.g. "Name <email>")').action(async (opts, cmd) => {
308
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
309
+ try {
310
+ execFileSync("git", ["add", metaDir], { stdio: "pipe" });
311
+ try {
312
+ execSync("git diff --cached --quiet", { stdio: "pipe" });
313
+ printError("No changes to commit in .meta/");
314
+ process.exit(1);
315
+ } catch {
316
+ }
317
+ const args = ["commit", "-m", opts.message];
318
+ if (opts.author) {
319
+ args.push("--author", opts.author);
320
+ }
321
+ const env = { ...process.env, SKIP: "commitizen" };
322
+ const result = execFileSync("git", args, {
323
+ stdio: "pipe",
324
+ env,
325
+ encoding: "utf-8"
326
+ });
327
+ const hashMatch = result.match(/\[[\w-/]+ ([a-f0-9]+)\]/);
328
+ const hash = hashMatch ? hashMatch[1] : "";
329
+ printSuccess(`Committed${hash ? ` (${hash})` : ""}: ${opts.message}`);
330
+ } catch (err) {
331
+ const message = err instanceof Error ? err.stderr || err.message : String(err);
332
+ printError(`Commit failed: ${message}`);
333
+ process.exit(1);
334
+ }
335
+ });
336
+
337
+ // src/commands/context.ts
338
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
339
+ import { resolve as resolve2 } from "path";
340
+ import {
341
+ computeEpicProgress,
342
+ computeMilestoneProgress,
343
+ parseTree as parseTree2,
344
+ resolveRefs as resolveRefs2
345
+ } from "@gitpm/core";
346
+ import { Command as Command4 } from "commander";
347
+ var CONTEXT_START_MARKER = "<!-- gitpm:context:start -->";
348
+ var CONTEXT_END_MARKER = "<!-- gitpm:context:end -->";
349
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["done", "cancelled"]);
350
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["in_progress", "in_review"]);
351
+ var MAX_UNASSIGNED_ITEMS = 10;
352
+ function sanitizeTitle(title) {
353
+ let current = title;
354
+ let previous;
355
+ do {
356
+ previous = current;
357
+ current = current.replace(/<!--[\s\S]*?-->/g, " ");
358
+ } while (current !== previous);
359
+ return current.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\s+/g, " ").trim();
360
+ }
361
+ function localDate(date = /* @__PURE__ */ new Date()) {
362
+ const y = date.getFullYear();
363
+ const m = String(date.getMonth() + 1).padStart(2, "0");
364
+ const d = String(date.getDate()).padStart(2, "0");
365
+ return `${y}-${m}-${d}`;
366
+ }
367
+ function pct(ratio) {
368
+ return `${Math.round(ratio * 100)}%`;
369
+ }
370
+ function findActiveMilestone(tree) {
371
+ const active = tree.milestones.filter((m) => !DONE_STATUSES.has(m.status));
372
+ const sorted = [...active].sort((a, b) => {
373
+ if (!a.target_date && !b.target_date) return 0;
374
+ if (!a.target_date) return 1;
375
+ if (!b.target_date) return -1;
376
+ return a.target_date.localeCompare(b.target_date);
377
+ });
378
+ return sorted[0];
379
+ }
380
+ function findCurrentSprint(tree, today) {
381
+ const inRange = tree.sprints.filter((s) => {
382
+ if (DONE_STATUSES.has(s.status)) return false;
383
+ const start = s.start_date.slice(0, 10);
384
+ const end = s.end_date.slice(0, 10);
385
+ return today >= start && today <= end;
386
+ });
387
+ return inRange.find((s) => s.status === "in_progress") ?? inRange[0];
388
+ }
389
+ function findActiveEpics(tree) {
390
+ return tree.epics.filter((e) => ACTIVE_STATUSES.has(e.status));
391
+ }
392
+ function findUnassignedItems(tree) {
393
+ return tree.stories.filter(
394
+ (s) => !DONE_STATUSES.has(s.status) && !ACTIVE_STATUSES.has(s.status) && s.assignee == null
395
+ ).map((s) => ({ title: s.title, epicTitle: s.resolvedEpic?.title }));
396
+ }
397
+ function renderContext(tree, today = localDate()) {
398
+ const lines = [];
399
+ lines.push("## Project Context (generated by gitpm)");
400
+ lines.push("");
401
+ lines.push("### Active Milestone");
402
+ const milestone = findActiveMilestone(tree);
403
+ if (milestone) {
404
+ const progress = computeMilestoneProgress(milestone, tree.epics);
405
+ const due = milestone.target_date ? ` \u2014 due ${milestone.target_date.slice(0, 10)}` : "";
406
+ lines.push(
407
+ `**${sanitizeTitle(milestone.title)}** \u2014 ${pct(progress.progress)} done (${progress.done}/${progress.total} stories)${due}`
408
+ );
409
+ } else {
410
+ lines.push("No active milestone.");
411
+ }
412
+ lines.push("");
413
+ lines.push("### Current Sprint");
414
+ const sprint = findCurrentSprint(tree, today);
415
+ if (sprint) {
416
+ const total = sprint.resolvedStories.length;
417
+ const done = sprint.resolvedStories.filter(
418
+ (s) => DONE_STATUSES.has(s.status)
419
+ ).length;
420
+ const start = sprint.start_date.slice(0, 10);
421
+ const end = sprint.end_date.slice(0, 10);
422
+ lines.push(
423
+ `**${sanitizeTitle(sprint.title)}** \u2014 ${done}/${total} stories done (${start} \u2192 ${end})`
424
+ );
425
+ } else {
426
+ lines.push("No sprint currently active.");
427
+ }
428
+ lines.push("");
429
+ lines.push("### Active Epics");
430
+ const activeEpics = findActiveEpics(tree);
431
+ if (activeEpics.length > 0) {
432
+ for (const epic of activeEpics) {
433
+ const progress = computeEpicProgress(epic);
434
+ lines.push(
435
+ `- **${sanitizeTitle(epic.title)}** \u2014 ${progress.done}/${progress.total} done`
436
+ );
437
+ }
438
+ } else {
439
+ lines.push("No active epics.");
440
+ }
441
+ lines.push("");
442
+ lines.push("### Unassigned Backlog");
443
+ const unassigned = findUnassignedItems(tree);
444
+ if (unassigned.length > 0) {
445
+ for (const item of unassigned.slice(0, MAX_UNASSIGNED_ITEMS)) {
446
+ const epic = item.epicTitle ? ` (${sanitizeTitle(item.epicTitle)})` : "";
447
+ lines.push(`- ${sanitizeTitle(item.title)}${epic}`);
448
+ }
449
+ if (unassigned.length > MAX_UNASSIGNED_ITEMS) {
450
+ lines.push(`- \u2026and ${unassigned.length - MAX_UNASSIGNED_ITEMS} more`);
451
+ }
452
+ } else {
453
+ lines.push("None.");
454
+ }
455
+ lines.push("");
456
+ return lines.join("\n");
457
+ }
458
+ function escapeRegExp(text) {
459
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
460
+ }
461
+ function findOwnLineMarkers(haystack, marker) {
462
+ const pattern = new RegExp(
463
+ `^[ \\t]*${escapeRegExp(marker)}[ \\t]*(?=\\r?$)`,
464
+ "gm"
465
+ );
466
+ return [...haystack.matchAll(pattern)].map((m) => ({
467
+ start: m.index,
468
+ end: m.index + m[0].length
469
+ }));
470
+ }
471
+ function spliceIntoClaudeMd(existing, section) {
472
+ const block = `${CONTEXT_START_MARKER}
473
+
474
+ ${section}
475
+ ${CONTEXT_END_MARKER}`;
476
+ const starts = findOwnLineMarkers(existing, CONTEXT_START_MARKER);
477
+ const ends = findOwnLineMarkers(existing, CONTEXT_END_MARKER);
478
+ if (starts.length === 0 && ends.length === 0) {
479
+ const trimmed = existing.replace(/\n+$/, "");
480
+ const separator = trimmed.length > 0 ? "\n\n" : "";
481
+ return { ok: true, value: `${trimmed}${separator}${block}
482
+ ` };
483
+ }
484
+ const [start] = starts;
485
+ const [end] = ends;
486
+ if (starts.length !== 1 || ends.length !== 1 || start === void 0 || end === void 0 || end.start < start.end) {
487
+ return {
488
+ ok: false,
489
+ error: new Error(
490
+ `CLAUDE.md has unbalanced gitpm context markers (found ${starts.length} start, ${ends.length} end on their own lines; expected exactly one "${CONTEXT_START_MARKER}" line followed by one "${CONTEXT_END_MARKER}" line). Fix the markers manually and re-run.`
491
+ )
492
+ };
493
+ }
494
+ const before = existing.slice(0, start.start);
495
+ const after = existing.slice(end.end);
496
+ return { ok: true, value: `${before}${block}${after}` };
497
+ }
498
+ function errorMessage(err) {
499
+ return err instanceof Error ? err.message : String(err);
500
+ }
501
+ function isEnoent(err) {
502
+ return typeof err === "object" && err !== null && err.code === "ENOENT";
503
+ }
504
+ var contextCommand = new Command4("context").description(
505
+ "Generate project context (milestone, sprint, epics, unassigned backlog) from the .meta tree"
506
+ ).option("--output <file>", "Write context to a file instead of stdout").option(
507
+ "--update",
508
+ "Splice context into ./CLAUDE.md between gitpm:context markers"
509
+ ).action(async (opts, cmd) => {
510
+ if (opts.update && opts.output) {
511
+ printError("--update and --output cannot be used together");
512
+ process.exit(1);
513
+ }
514
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
515
+ const parseResult = await parseTree2(metaDir);
516
+ if (!parseResult.ok) {
517
+ printError(parseResult.error.message);
518
+ process.exit(1);
519
+ }
520
+ const resolveResult = resolveRefs2(parseResult.value);
521
+ if (!resolveResult.ok) {
522
+ printError(resolveResult.error.message);
523
+ process.exit(1);
524
+ }
525
+ const section = renderContext(resolveResult.value);
526
+ if (opts.update) {
527
+ const claudeMdPath = resolve2(process.cwd(), "CLAUDE.md");
528
+ let existing = "";
529
+ try {
530
+ existing = await readFile2(claudeMdPath, "utf-8");
531
+ } catch (err) {
532
+ if (!isEnoent(err)) {
533
+ printError(`Failed to read ${claudeMdPath}: ${errorMessage(err)}`);
534
+ process.exit(1);
535
+ }
536
+ }
537
+ const updated = spliceIntoClaudeMd(existing, section);
538
+ if (!updated.ok) {
539
+ printError(updated.error.message);
540
+ process.exit(1);
541
+ }
542
+ try {
543
+ await writeFile2(claudeMdPath, updated.value, "utf-8");
544
+ } catch (err) {
545
+ printError(`Failed to write ${claudeMdPath}: ${errorMessage(err)}`);
546
+ process.exit(1);
547
+ }
548
+ printSuccess(`Updated ${claudeMdPath}`);
549
+ return;
550
+ }
551
+ if (opts.output) {
552
+ const outputPath = resolve2(process.cwd(), opts.output);
553
+ try {
554
+ await writeFile2(outputPath, `${section}
555
+ `, "utf-8");
556
+ } catch (err) {
557
+ printError(`Failed to write ${outputPath}: ${errorMessage(err)}`);
558
+ process.exit(1);
559
+ }
560
+ printSuccess(`Wrote context to ${outputPath}`);
561
+ return;
562
+ }
563
+ console.log(section);
564
+ });
565
+
566
+ // src/commands/create.ts
567
+ import { relative as relative2 } from "path";
568
+ import {
569
+ createEpic,
570
+ createMilestone,
571
+ createStory,
572
+ parseTree as parseTree3
573
+ } from "@gitpm/core";
574
+ import { Command as Command5 } from "commander";
575
+ var storySubcommand = new Command5("story").description("Create a new story").requiredOption("--title <title>", "Story title").option(
576
+ "--priority <priority>",
577
+ "Priority (low, medium, high, critical)",
578
+ "medium"
579
+ ).option("--status <status>", "Initial status", "backlog").option("--labels <labels>", "Comma-separated labels").option("--epic <epic>", "Epic ID or directory slug to place story under").option("--assignee <assignee>", "Assignee").option("--estimate <estimate>", "Estimate (number)").option("--body <body>", "Body content").action(async (opts, cmd) => {
580
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
581
+ let epicId;
582
+ let epicSlug;
583
+ if (opts.epic) {
584
+ const parseResult = await parseTree3(metaDir);
585
+ if (!parseResult.ok) {
586
+ printError(parseResult.error.message);
587
+ process.exit(1);
588
+ }
589
+ const matchById = parseResult.value.epics.find((e) => e.id === opts.epic);
590
+ if (matchById) {
591
+ epicId = matchById.id;
592
+ const parts = matchById.filePath.split("/");
593
+ const epicIdx = parts.indexOf("epics");
594
+ if (epicIdx >= 0 && epicIdx + 1 < parts.length) {
595
+ epicSlug = parts[epicIdx + 1];
596
+ }
597
+ } else {
598
+ const matchBySlug = parseResult.value.epics.find(
599
+ (e) => e.filePath.includes(`/epics/${opts.epic}/`)
600
+ );
601
+ if (matchBySlug) {
602
+ epicId = matchBySlug.id;
603
+ epicSlug = opts.epic;
604
+ } else {
605
+ printError(`Epic not found: ${opts.epic}`);
606
+ process.exit(1);
607
+ }
608
+ }
609
+ }
610
+ const result = await createStory(metaDir, {
611
+ title: opts.title,
612
+ priority: opts.priority,
613
+ status: opts.status,
614
+ labels: opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0,
615
+ epicId,
616
+ epicSlug,
617
+ assignee: opts.assignee,
618
+ estimate: opts.estimate ? Number(opts.estimate) : void 0,
619
+ body: opts.body
620
+ });
621
+ if (!result.ok) {
622
+ printError(result.error.message);
623
+ process.exit(1);
624
+ }
625
+ const rel = relative2(process.cwd(), result.value.filePath);
626
+ printSuccess(`Created story ${result.value.id} at ${rel}`);
627
+ });
628
+ var epicSubcommand = new Command5("epic").description("Create a new epic").requiredOption("--title <title>", "Epic title").option(
629
+ "--priority <priority>",
630
+ "Priority (low, medium, high, critical)",
631
+ "medium"
632
+ ).option("--status <status>", "Initial status", "backlog").option("--labels <labels>", "Comma-separated labels").option("--milestone <milestone>", "Milestone ID to link").option("--owner <owner>", "Owner").option("--body <body>", "Body content").action(async (opts, cmd) => {
633
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
634
+ const result = await createEpic(metaDir, {
635
+ title: opts.title,
636
+ priority: opts.priority,
637
+ status: opts.status,
638
+ labels: opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0,
639
+ milestoneId: opts.milestone,
640
+ owner: opts.owner,
641
+ body: opts.body
642
+ });
643
+ if (!result.ok) {
644
+ printError(result.error.message);
645
+ process.exit(1);
646
+ }
647
+ const rel = relative2(process.cwd(), result.value.filePath);
648
+ printSuccess(`Created epic ${result.value.id} at ${rel}`);
649
+ });
650
+ var milestoneSubcommand = new Command5("milestone").description("Create a new milestone").requiredOption("--title <title>", "Milestone title").option("--status <status>", "Initial status", "backlog").option("--target-date <date>", "Target date (ISO format)").option("--body <body>", "Body content").action(async (opts, cmd) => {
651
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
652
+ const result = await createMilestone(metaDir, {
653
+ title: opts.title,
654
+ status: opts.status,
655
+ targetDate: opts.targetDate,
656
+ body: opts.body
657
+ });
658
+ if (!result.ok) {
659
+ printError(result.error.message);
660
+ process.exit(1);
661
+ }
662
+ const rel = relative2(process.cwd(), result.value.filePath);
663
+ printSuccess(`Created milestone ${result.value.id} at ${rel}`);
664
+ });
665
+ var createCommand = new Command5("create").description("Create a new .meta/ entity").addCommand(storySubcommand).addCommand(epicSubcommand).addCommand(milestoneSubcommand);
666
+
667
+ // src/commands/import.ts
668
+ import { dirname } from "path";
669
+ import {
670
+ findAdapterByName,
671
+ loadAdapters,
672
+ loadGitpmConfig,
673
+ runHooks
674
+ } from "@gitpm/core";
675
+ import chalk4 from "chalk";
676
+ import { Command as Command6 } from "commander";
677
+ import ora from "ora";
678
+
679
+ // src/utils/auth.ts
680
+ import { execSync as execSync2 } from "child_process";
681
+ function ghAuthToken() {
682
+ try {
683
+ const token = execSync2("gh auth token", {
684
+ encoding: "utf-8",
685
+ stdio: ["pipe", "pipe", "pipe"]
686
+ }).trim();
687
+ return token || void 0;
688
+ } catch {
689
+ return void 0;
690
+ }
691
+ }
692
+ var ADAPTER_CREDENTIALS = {
693
+ github: { tokenEnv: "GITHUB_TOKEN", fallback: ghAuthToken },
694
+ gitlab: { tokenEnv: "GITLAB_TOKEN" },
695
+ jira: { tokenEnv: "JIRA_TOKEN", emailEnv: "JIRA_EMAIL" },
696
+ bitbucket: { tokenEnv: "BITBUCKET_TOKEN", emailEnv: "BITBUCKET_EMAIL" }
697
+ };
698
+ function envValue(name) {
699
+ if (!name) return void 0;
700
+ return process.env[name] || void 0;
701
+ }
702
+ function resolveCredentials(adapterName, cliToken) {
703
+ const spec = ADAPTER_CREDENTIALS[adapterName];
704
+ const token = cliToken || envValue(spec?.tokenEnv) || spec?.fallback?.() || void 0;
705
+ const email = envValue(spec?.emailEnv);
706
+ return { token, email };
707
+ }
52
708
 
53
709
  // src/commands/import.ts
54
710
  var VALID_LINK_STRATEGIES = [
@@ -56,48 +712,55 @@ var VALID_LINK_STRATEGIES = [
56
712
  "sub-issues",
57
713
  "milestone",
58
714
  "labels",
59
- "all"
60
- ];
61
- var VALID_GITLAB_LINK_STRATEGIES = [
62
- "body-refs",
63
- "native-epics",
64
- "milestone",
65
- "labels",
66
- "all"
715
+ "score",
716
+ "all",
717
+ "native-epics"
67
718
  ];
68
- var importCommand = new Command("import").description("Import project data from GitHub or GitLab into .meta/").option(
719
+ var importCommand = new Command6("import").description("Import project data from a remote platform into .meta/").option(
69
720
  "--source <source>",
70
- "Source platform: github or gitlab (default: github)",
721
+ "Source platform: github, gitlab, jira, linear, or bitbucket (default: github)",
71
722
  "github"
72
723
  ).option("--repo <owner/repo>", "GitHub repository (owner/repo)").option(
73
724
  "--project <value>",
74
- "GitHub Project number or GitLab project path (namespace/project)"
75
- ).option("--token <token>", "Personal access token").option("--base-url <url>", "GitLab base URL (default: https://gitlab.com)").option(
725
+ "GitHub Project number, GitLab project path (namespace/project), or Jira project key"
726
+ ).option("--token <token>", "Personal access token").option(
727
+ "--base-url <url>",
728
+ "GitLab base URL (default: https://gitlab.com) or Jira site URL"
729
+ ).option("--site <host>", "Jira site, e.g. mycompany.atlassian.net").option("--project-key <key>", "Jira project key, e.g. PROJ").option("--board <id>", "Jira board ID (optional, for sprint import)").option("--team <id>", "Linear team ID").option(
730
+ "--api-key <key>",
731
+ "Linear API key (default: LINEAR_API_KEY environment variable)"
732
+ ).option(
76
733
  "--link-strategy <strategy>",
77
734
  "Epic-story linkage strategy (default: all)"
78
- ).action(async (opts, cmd) => {
735
+ ).option("--adapter <name>", "Force a specific adapter by name").action(async (opts, cmd) => {
79
736
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
80
- const source = opts.source;
81
- if (source === "gitlab") {
82
- await importGitLab(opts, metaDir);
83
- } else if (source === "github") {
84
- await importGitHub(opts, metaDir);
85
- } else {
86
- printError(`Unknown source "${source}". Use "github" or "gitlab".`);
737
+ const source = opts.adapter ?? opts.source;
738
+ const rootDir = dirname(metaDir);
739
+ const configResult = await loadGitpmConfig(rootDir);
740
+ if (!configResult.ok) {
741
+ printError(`Failed to load config: ${configResult.error.message}`);
87
742
  process.exit(1);
88
743
  }
89
- });
90
- async function importGitHub(opts, metaDir) {
91
- const repo = opts.repo;
92
- if (!repo || !repo.includes("/") || repo.split("/").length !== 2) {
93
- printError('--repo is required for GitHub import. Expected "owner/repo".');
744
+ const config = configResult.value;
745
+ const adaptersResult = await loadAdapters(config, rootDir);
746
+ if (!adaptersResult.ok) {
747
+ printError(`Failed to load adapters: ${adaptersResult.error.message}`);
94
748
  process.exit(1);
95
749
  }
96
- let token;
97
- try {
98
- token = await resolveToken(opts.token);
99
- } catch (err) {
100
- printError(err instanceof Error ? err.message : "Failed to resolve token.");
750
+ const adapter = findAdapterByName(adaptersResult.value, source);
751
+ if (!adapter) {
752
+ const available = adaptersResult.value.map((a) => a.name);
753
+ if (available.length === 0) {
754
+ printError(
755
+ `Adapter "${source}" is not installed. Install it with:
756
+ npm install -g @gitpm/sync-${source}`
757
+ );
758
+ } else {
759
+ printError(
760
+ `Adapter "${source}" not found. Available: ${available.join(", ")}
761
+ Install it with: npm install -g @gitpm/sync-${source}`
762
+ );
763
+ }
101
764
  process.exit(1);
102
765
  }
103
766
  const linkStrategy = opts.linkStrategy;
@@ -109,49 +772,28 @@ async function importGitHub(opts, metaDir) {
109
772
  );
110
773
  process.exit(1);
111
774
  }
112
- const spinner = ora("Importing from GitHub...").start();
113
- const result = await importFromGitHub({
114
- token,
115
- repo,
116
- projectNumber: opts.project ? Number.parseInt(opts.project, 10) : void 0,
775
+ const { token, email } = resolveCredentials(adapter.name, opts.token);
776
+ const preHook = await runHooks(config, "pre-import", {
117
777
  metaDir,
118
- linkStrategy
778
+ event: "pre-import",
779
+ adapterName: adapter.name
119
780
  });
120
- if (!result.ok) {
121
- spinner.fail("Import failed.");
122
- printError(result.error.message);
123
- process.exit(1);
124
- }
125
- spinner.succeed("Import complete.");
126
- printImportSummary(result.value);
127
- }
128
- async function importGitLab(opts, metaDir) {
129
- const project = opts.project ?? opts.repo;
130
- if (!project) {
131
- printError("--project (namespace/project) is required for GitLab import.");
132
- process.exit(1);
133
- }
134
- const token = opts.token ?? process.env.GITLAB_TOKEN;
135
- if (!token) {
136
- printError(
137
- "No GitLab token found. Provide via --token flag or GITLAB_TOKEN env var."
138
- );
139
- process.exit(1);
140
- }
141
- const linkStrategy = opts.linkStrategy;
142
- if (linkStrategy && !VALID_GITLAB_LINK_STRATEGIES.includes(
143
- linkStrategy
144
- )) {
145
- printError(
146
- `Invalid link strategy "${linkStrategy}". Must be one of: ${VALID_GITLAB_LINK_STRATEGIES.join(", ")}`
147
- );
781
+ if (!preHook.ok) {
782
+ printError(`Pre-import hook failed: ${preHook.error.message}`);
148
783
  process.exit(1);
149
784
  }
150
- const spinner = ora("Importing from GitLab...").start();
151
- const result = await importFromGitLab({
785
+ const spinner = ora(`Importing from ${adapter.displayName}...`).start();
786
+ const jira = resolveJiraOptions(adapter.name, opts);
787
+ const result = await adapter.import({
152
788
  token,
153
- project,
789
+ email,
790
+ repo: opts.repo,
791
+ project: opts.project ?? opts.repo,
792
+ projectNumber: opts.project && !Number.isNaN(Number.parseInt(opts.project, 10)) ? Number.parseInt(opts.project, 10) : void 0,
154
793
  baseUrl: opts.baseUrl,
794
+ ...jira,
795
+ teamId: opts.team,
796
+ apiKey: opts.apiKey,
155
797
  metaDir,
156
798
  linkStrategy
157
799
  });
@@ -162,11 +804,27 @@ async function importGitLab(opts, metaDir) {
162
804
  }
163
805
  spinner.succeed("Import complete.");
164
806
  printImportSummary(result.value);
807
+ await runHooks(config, "post-import", {
808
+ metaDir,
809
+ event: "post-import",
810
+ adapterName: adapter.name,
811
+ result: result.value
812
+ });
813
+ });
814
+ function resolveJiraOptions(adapterName, opts) {
815
+ const isJira = adapterName === "jira";
816
+ const site = opts.site ?? (isJira && opts.baseUrl ? siteFromUrl(opts.baseUrl) : void 0);
817
+ const projectKey = opts.projectKey ?? (isJira ? opts.project : void 0);
818
+ const boardId = opts.board !== void 0 && !Number.isNaN(Number.parseInt(opts.board, 10)) ? Number.parseInt(opts.board, 10) : void 0;
819
+ return { site, projectKey, boardId };
820
+ }
821
+ function siteFromUrl(url) {
822
+ return url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/+$/, "");
165
823
  }
166
824
  function printImportSummary(value) {
167
825
  const { milestones, epics, stories, totalFiles } = value;
168
826
  console.log();
169
- console.log(chalk2.bold("Summary:"));
827
+ console.log(chalk4.bold("Summary:"));
170
828
  console.log(` Milestones: ${milestones}`);
171
829
  console.log(` Epics: ${epics}`);
172
830
  console.log(` Stories: ${stories}`);
@@ -175,16 +833,203 @@ function printImportSummary(value) {
175
833
  printSuccess(
176
834
  `Imported ${milestones + epics + stories} entities (${totalFiles} files).`
177
835
  );
178
- console.log(chalk2.dim("Run `gitpm validate` to verify the imported tree."));
836
+ console.log(chalk4.dim("Run `gitpm validate` to verify the imported tree."));
179
837
  }
180
838
 
181
839
  // src/commands/init.ts
182
- import { readdir } from "fs/promises";
183
- import { join } from "path";
840
+ import { mkdir, readdir, stat, writeFile as writeFile3 } from "fs/promises";
841
+ import { dirname as dirname2, join as join2, relative as relative3, resolve as resolve3 } from "path";
184
842
  import { scaffoldMeta } from "@gitpm/core";
185
843
  import { input } from "@inquirer/prompts";
186
- import chalk3 from "chalk";
187
- import { Command as Command2 } from "commander";
844
+ import chalk5 from "chalk";
845
+ import { Command as Command7 } from "commander";
846
+
847
+ // src/templates/claude-skill.ts
848
+ var GITPM_SKILL_TEMPLATE = `---
849
+ name: gitpm
850
+ description: Use when working with .meta/ project management entities (stories, epics, milestones, roadmaps, PRDs) in a project that uses GitPM. Provides commands to query, filter, read, update fields, create, move, and commit entities without manual file parsing. Prefer these over grep/read/Edit on .meta/ files.
851
+ ---
852
+
853
+ # gitpm \u2014 Project management via .meta/ files
854
+
855
+ This project uses [GitPM](https://github.com/yevheniidehtiar/gitpm) to manage project management data (stories, epics, milestones, roadmaps, PRDs) as files under \`.meta/\`. Entities are Markdown files with YAML frontmatter, synced bidirectionally with GitHub Issues.
856
+
857
+ ## When to use the \`gitpm\` CLI vs reading files directly
858
+
859
+ **Prefer \`gitpm\` commands** \u2014 they are 10\xD7 fewer tool calls than grep+read+Edit loops and validate writes against the schema:
860
+
861
+ | Goal | Use |
862
+ |---|---|
863
+ | Find tasks by status / priority / label / epic | \`gitpm query\` |
864
+ | Display one entity (or an epic + its stories) | \`gitpm show\` |
865
+ | Update a frontmatter field (status, priority, labels, assignee) | \`gitpm set\` |
866
+ | Create a new story / epic / milestone | \`gitpm create\` |
867
+ | Move a story between epics (or to/from orphan) | \`gitpm move\` |
868
+ | Stage + commit all .meta/ changes atomically | \`gitpm commit\` |
869
+ | Push local .meta/ to GitHub Issues | \`gitpm push\` |
870
+ | Pull GitHub Issues into local .meta/ | \`gitpm pull\` |
871
+ | Bidirectional sync | \`gitpm sync\` |
872
+ | Validate the whole tree | \`gitpm validate\` |
873
+
874
+ **Avoid** \`grep\` / \`cat\` / \`Edit\` directly on \`.meta/*.md\` unless you truly need a raw view. The CLI preserves frontmatter ordering, validates against Zod schemas, and keeps GitHub sync state consistent.
875
+
876
+ ## Common recipes
877
+
878
+ ### Find all in-progress stories
879
+ \`\`\`bash
880
+ gitpm query --type story --status in_progress
881
+ \`\`\`
882
+
883
+ ### Find high/critical stories across all types
884
+ \`\`\`bash
885
+ gitpm query --priority high,critical --format table
886
+ \`\`\`
887
+
888
+ ### Show an epic and every story under it
889
+ \`\`\`bash
890
+ gitpm show --epic auth-system
891
+ \`\`\`
892
+
893
+ ### Show a specific story with its full body
894
+ \`\`\`bash
895
+ gitpm show .meta/epics/auth/stories/login.md --full
896
+ \`\`\`
897
+
898
+ ### Change a story's status to in_progress
899
+ \`\`\`bash
900
+ gitpm set .meta/epics/auth/stories/login.md status=in_progress
901
+ \`\`\`
902
+
903
+ ### Append labels (array operation)
904
+ \`\`\`bash
905
+ gitpm set .meta/stories/onboarding.md labels+=backend labels+=security
906
+ \`\`\`
907
+
908
+ ### Remove a label
909
+ \`\`\`bash
910
+ gitpm set .meta/stories/onboarding.md labels-=stale
911
+ \`\`\`
912
+
913
+ ### Create a new story under an existing epic
914
+ \`\`\`bash
915
+ gitpm create story \\
916
+ --title "Add OAuth callback handler" \\
917
+ --epic auth-system \\
918
+ --priority high \\
919
+ --labels backend,oauth
920
+ \`\`\`
921
+
922
+ ### Create a standalone story (no epic)
923
+ \`\`\`bash
924
+ gitpm create story --title "Flaky test in checkout" --priority medium
925
+ \`\`\`
926
+
927
+ ### Move a story from orphan into an epic
928
+ \`\`\`bash
929
+ gitpm move .meta/stories/orphan.md --to-epic auth-system
930
+ \`\`\`
931
+
932
+ ### Commit all .meta/ changes
933
+ \`\`\`bash
934
+ gitpm commit -m "feat(pm): progress auth-system stories"
935
+ \`\`\`
936
+
937
+ ### Push local changes to GitHub Issues
938
+ \`\`\`bash
939
+ gitpm push --token "$GITHUB_TOKEN"
940
+ \`\`\`
941
+
942
+ ## Field update syntax for \`gitpm set\`
943
+
944
+ | Syntax | Effect |
945
+ |---|---|
946
+ | \`field=value\` | Replace the value |
947
+ | \`field+=value\` | Append to an array field (labels, dependencies) |
948
+ | \`field-=value\` | Remove from an array field |
949
+ | \`field=null\` | Clear the value |
950
+
951
+ Multiple assignments in one call are applied atomically:
952
+ \`\`\`bash
953
+ gitpm set .meta/stories/auth.md status=in_progress assignee=alice labels+=urgent
954
+ \`\`\`
955
+
956
+ ## Entity layout
957
+
958
+ \`\`\`
959
+ .meta/
960
+ \u251C\u2500\u2500 roadmap/
961
+ \u2502 \u251C\u2500\u2500 roadmap.yaml # top-level roadmap
962
+ \u2502 \u2514\u2500\u2500 milestones/*.md # milestones
963
+ \u251C\u2500\u2500 epics/
964
+ \u2502 \u2514\u2500\u2500 <slug>/
965
+ \u2502 \u251C\u2500\u2500 epic.md # the epic
966
+ \u2502 \u2514\u2500\u2500 stories/*.md # stories nested under this epic
967
+ \u251C\u2500\u2500 stories/*.md # orphan stories (no epic)
968
+ \u251C\u2500\u2500 prds/*.md # product requirements docs
969
+ \u2514\u2500\u2500 sync/
970
+ \u251C\u2500\u2500 github-config.yaml
971
+ \u2514\u2500\u2500 github-state.json
972
+ \`\`\`
973
+
974
+ ## Allowed field values
975
+
976
+ - **status**: \`backlog\` \xB7 \`todo\` \xB7 \`in_progress\` \xB7 \`in_review\` \xB7 \`done\` \xB7 \`cancelled\`
977
+ - **priority**: \`low\` \xB7 \`medium\` \xB7 \`high\` \xB7 \`critical\`
978
+
979
+ ## After modifying .meta/ files
980
+
981
+ Always run \`gitpm push\` so GitHub Issues stay in sync:
982
+
983
+ \`\`\`bash
984
+ gitpm push --token "$GITHUB_TOKEN"
985
+ \`\`\`
986
+
987
+ Use \`gitpm sync --strategy local-wins\` when you have both local edits and remote changes.
988
+
989
+ ## Story lifecycle (three-state flow)
990
+
991
+ Stories move through four states tied to the release pipeline:
992
+
993
+ \`\`\`
994
+ todo \u2500\u2500\u25BA in_progress \u2500\u2500\u25BA in_review \u2500\u2500\u25BA done
995
+ \`\`\`
996
+
997
+ **You are responsible for the first two transitions only** \u2014 the release-please
998
+ CI job promotes \`in_review\` \u2192 \`done\` automatically, and \`post-merge-sync.yml\`
999
+ closes the GitHub issue once the release PR merges.
1000
+
1001
+ | Transition | Who | When |
1002
+ |---|---|---|
1003
+ | \`todo\` \u2192 \`in_progress\` | **You** | Before writing any code for the story |
1004
+ | \`in_progress\` \u2192 \`in_review\` | **You** | Inside the work PR itself, committed with the code |
1005
+ | \`in_review\` \u2192 \`done\` | release-please bot | When the release PR is created/updated |
1006
+ | GitHub issue closes | post-merge-sync bot | After the release PR merges |
1007
+
1008
+ **Rule 1 \u2014 When you start work on a story, flip it to in_progress:**
1009
+ \`\`\`bash
1010
+ gitpm set .meta/epics/<epic>/stories/<story>.md status=in_progress
1011
+ \`\`\`
1012
+
1013
+ **Rule 2 \u2014 When you raise the PR that implements the story, do two things in the same PR:**
1014
+
1015
+ 1. Flip the story's status in the diff:
1016
+ \`\`\`bash
1017
+ gitpm set .meta/epics/<epic>/stories/<story>.md status=in_review
1018
+ \`\`\`
1019
+ 2. Reference the story file path(s) in the PR body under "Related GitPM stories" (the PR template has a section for it).
1020
+
1021
+ **Do NOT manually set status=done or close GitHub issues** \u2014 both are handled by CI. Setting \`status=done\` yourself short-circuits the release PR audit surface.
1022
+
1023
+ If a PR has no \`.meta/\` story (docs, tooling, typo fix): skip both rules. CI will find 0 \`in_review\` stories and exit cleanly.
1024
+
1025
+ ## Help
1026
+
1027
+ - \`gitpm --help\` \u2014 list all commands
1028
+ - \`gitpm <command> --help\` \u2014 flags for a specific command
1029
+ - \`gitpm validate\` \u2014 run before committing to catch schema errors
1030
+ `;
1031
+
1032
+ // src/commands/init.ts
188
1033
  async function collectFiles(dir, prefix = "") {
189
1034
  const entries = await readdir(dir, { withFileTypes: true });
190
1035
  const results = [];
@@ -192,7 +1037,7 @@ async function collectFiles(dir, prefix = "") {
192
1037
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
193
1038
  if (entry.isDirectory()) {
194
1039
  results.push(rel);
195
- const children = await collectFiles(join(dir, entry.name), rel);
1040
+ const children = await collectFiles(join2(dir, entry.name), rel);
196
1041
  results.push(...children);
197
1042
  } else {
198
1043
  results.push(rel);
@@ -200,65 +1045,243 @@ async function collectFiles(dir, prefix = "") {
200
1045
  }
201
1046
  return results;
202
1047
  }
203
- var initCommand = new Command2("init").description("Initialize a new .meta/ project structure").argument("[project-name]", "Name of the project").action(async (nameArg, _opts, cmd) => {
1048
+ async function fileExists(path) {
1049
+ try {
1050
+ await stat(path);
1051
+ return true;
1052
+ } catch {
1053
+ return false;
1054
+ }
1055
+ }
1056
+ async function scaffoldClaudeSkill(metaDir) {
1057
+ const projectRoot = dirname2(resolve3(metaDir));
1058
+ const skillDir = join2(projectRoot, ".claude", "skills", "gitpm");
1059
+ const skillPath = join2(skillDir, "SKILL.md");
1060
+ if (await fileExists(skillPath)) {
1061
+ return {
1062
+ status: "skipped",
1063
+ path: skillPath,
1064
+ reason: "already exists"
1065
+ };
1066
+ }
1067
+ await mkdir(skillDir, { recursive: true });
1068
+ await writeFile3(skillPath, GITPM_SKILL_TEMPLATE, "utf-8");
1069
+ return { status: "created", path: skillPath };
1070
+ }
1071
+ var initCommand = new Command7("init").description("Initialize a new .meta/ project structure").argument("[project-name]", "Name of the project").option(
1072
+ "--no-claude-skill",
1073
+ "Skip writing the Claude Code skill into .claude/skills/gitpm/"
1074
+ ).action(
1075
+ async (nameArg, opts, cmd) => {
1076
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1077
+ let projectName = nameArg;
1078
+ if (!projectName) {
1079
+ projectName = await input({
1080
+ message: "Project name:",
1081
+ required: true
1082
+ });
1083
+ }
1084
+ const result = await scaffoldMeta(metaDir, projectName);
1085
+ if (!result.ok) {
1086
+ printError(result.error.message);
1087
+ process.exit(1);
1088
+ }
1089
+ const files = await collectFiles(metaDir);
1090
+ console.log();
1091
+ console.log(chalk5.bold(".meta/"));
1092
+ for (const file of files) {
1093
+ const parts = file.split("/");
1094
+ const indent = " ".repeat(parts.length);
1095
+ const name = parts[parts.length - 1];
1096
+ const isDir = !name.includes(".");
1097
+ console.log(`${indent}${isDir ? chalk5.blue(`${name}/`) : name}`);
1098
+ }
1099
+ const fileCount = files.filter((f) => f.includes(".")).length;
1100
+ console.log();
1101
+ printSuccess(`Created ${fileCount} files in .meta/`);
1102
+ if (opts.claudeSkill !== false) {
1103
+ const skillResult = await scaffoldClaudeSkill(metaDir);
1104
+ const relPath = relative3(process.cwd(), skillResult.path) || skillResult.path;
1105
+ if (skillResult.status === "created") {
1106
+ printSuccess(`Installed Claude Code skill at ${relPath}`);
1107
+ } else {
1108
+ console.log(
1109
+ chalk5.dim(
1110
+ `\u2022 Claude Code skill at ${relPath} ${skillResult.reason} \u2014 skipped`
1111
+ )
1112
+ );
1113
+ }
1114
+ }
1115
+ }
1116
+ );
1117
+
1118
+ // src/commands/move.ts
1119
+ import { relative as relative4, resolve as resolve4 } from "path";
1120
+ import { moveStory } from "@gitpm/core";
1121
+ import { Command as Command8 } from "commander";
1122
+ var moveCommand = new Command8("move").description("Move a story between epics or to standalone").argument("<file>", "Path to the story file").option("--to-epic <epic>", "Move to an epic (by ID or directory slug)").option("--to-orphan", "Move out of an epic to .meta/stories/").action(async (file, opts, cmd) => {
204
1123
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
205
- let projectName = nameArg;
206
- if (!projectName) {
207
- projectName = await input({
208
- message: "Project name:",
209
- required: true
210
- });
1124
+ const filePath = resolve4(process.cwd(), file);
1125
+ if (!opts.toEpic && !opts.toOrphan) {
1126
+ printError("Specify --to-epic or --to-orphan");
1127
+ process.exit(1);
211
1128
  }
212
- const result = await scaffoldMeta(metaDir, projectName);
1129
+ const result = await moveStory(metaDir, filePath, {
1130
+ toEpic: opts.toEpic,
1131
+ toOrphan: opts.toOrphan
1132
+ });
213
1133
  if (!result.ok) {
214
1134
  printError(result.error.message);
215
1135
  process.exit(1);
216
1136
  }
217
- const files = await collectFiles(metaDir);
218
- console.log();
219
- console.log(chalk3.bold(".meta/"));
220
- for (const file of files) {
221
- const parts = file.split("/");
222
- const indent = " ".repeat(parts.length);
223
- const name = parts[parts.length - 1];
224
- const isDir = !name.includes(".");
225
- console.log(`${indent}${isDir ? chalk3.blue(`${name}/`) : name}`);
226
- }
227
- const fileCount = files.filter((f) => f.includes(".")).length;
228
- console.log();
229
- printSuccess(`Created ${fileCount} files in .meta/`);
1137
+ const oldRel = relative4(process.cwd(), result.value.oldPath);
1138
+ const newRel = relative4(process.cwd(), result.value.newPath);
1139
+ printSuccess(`Moved ${oldRel} \u2192 ${newRel}`);
1140
+ });
1141
+
1142
+ // src/commands/next.ts
1143
+ import { relative as relative5 } from "path";
1144
+ import { parseTree as parseTree4 } from "@gitpm/core";
1145
+ import chalk6 from "chalk";
1146
+ import { Command as Command9 } from "commander";
1147
+ var PRIORITY_ORDER = {
1148
+ critical: 0,
1149
+ high: 1,
1150
+ medium: 2,
1151
+ low: 3
1152
+ };
1153
+ var PICKABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo"]);
1154
+ var nextCommand = new Command9("next").description("Show the next stories ready to be picked up").option("-n, --count <number>", "Number of stories to show", "5").option(
1155
+ "-a, --assignee <name>",
1156
+ "Filter by assignee (case-insensitive match)"
1157
+ ).action(async (opts, cmd) => {
1158
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1159
+ const count = Number.parseInt(opts.count, 10);
1160
+ const parseResult = await parseTree4(metaDir);
1161
+ if (!parseResult.ok) {
1162
+ printError(parseResult.error.message);
1163
+ process.exit(1);
1164
+ }
1165
+ let stories = parseResult.value.stories.filter((s) => PICKABLE_STATUSES.has(s.status)).sort((a, b) => {
1166
+ const pa = PRIORITY_ORDER[a.priority] ?? 99;
1167
+ const pb = PRIORITY_ORDER[b.priority] ?? 99;
1168
+ if (pa !== pb) return pa - pb;
1169
+ if (a.status !== b.status) return a.status === "todo" ? -1 : 1;
1170
+ return 0;
1171
+ });
1172
+ if (opts.assignee) {
1173
+ const needle = opts.assignee.toLowerCase();
1174
+ stories = stories.filter(
1175
+ (s) => s.assignee != null && s.assignee.toLowerCase() === needle
1176
+ );
1177
+ }
1178
+ stories = stories.slice(0, count);
1179
+ if (stories.length === 0) {
1180
+ console.log(chalk6.yellow("No stories ready to be picked up."));
1181
+ return;
1182
+ }
1183
+ console.log(chalk6.bold(`Next ${stories.length} stories to pick up:
1184
+ `));
1185
+ for (const story of stories) {
1186
+ const file = relative5(process.cwd(), story.filePath);
1187
+ const pri = formatPriority(story.priority);
1188
+ const status = chalk6.dim(`[${story.status}]`);
1189
+ const assignee = story.assignee ? chalk6.cyan(`@${story.assignee}`) : chalk6.dim("unassigned");
1190
+ console.log(` ${pri} ${status} ${story.title} ${assignee}`);
1191
+ console.log(` ${chalk6.dim(file)}
1192
+ `);
1193
+ }
230
1194
  });
1195
+ function formatPriority(p) {
1196
+ switch (p) {
1197
+ case "critical":
1198
+ return chalk6.red("\u25CF");
1199
+ case "high":
1200
+ return chalk6.yellow("\u25CF");
1201
+ case "medium":
1202
+ return chalk6.blue("\u25CF");
1203
+ case "low":
1204
+ return chalk6.dim("\u25CF");
1205
+ default:
1206
+ return chalk6.dim("\u25CB");
1207
+ }
1208
+ }
231
1209
 
232
1210
  // src/commands/pull.ts
233
- import {
234
- loadConfig as loadGitHubConfig,
235
- syncWithGitHub
236
- } from "@gitpm/sync-github";
237
- import {
238
- loadConfig as loadGitLabConfig,
239
- syncWithGitLab
240
- } from "@gitpm/sync-gitlab";
241
- import chalk5 from "chalk";
242
- import { Command as Command3 } from "commander";
1211
+ import { runHooks as runHooks2 } from "@gitpm/core";
1212
+ import chalk8 from "chalk";
1213
+ import { Command as Command10 } from "commander";
243
1214
  import ora2 from "ora";
244
1215
 
1216
+ // src/utils/adapters.ts
1217
+ import { dirname as dirname3 } from "path";
1218
+ import {
1219
+ detectAdapter,
1220
+ findAdapterByName as findAdapterByName2,
1221
+ loadAdapters as loadAdapters2,
1222
+ loadGitpmConfig as loadGitpmConfig2
1223
+ } from "@gitpm/core";
1224
+ async function resolveAdapter(metaDir, adapterName) {
1225
+ const rootDir = dirname3(metaDir);
1226
+ const configResult = await loadGitpmConfig2(rootDir);
1227
+ if (!configResult.ok) {
1228
+ printError(`Failed to load config: ${configResult.error.message}`);
1229
+ process.exit(1);
1230
+ }
1231
+ const config = configResult.value;
1232
+ const adaptersResult = await loadAdapters2(config, rootDir);
1233
+ if (!adaptersResult.ok) {
1234
+ printError(adaptersResult.error.message);
1235
+ printError(
1236
+ "No sync adapters installed. Install at least one adapter package, e.g.:\n npm install -g @gitpm/sync-github\n npm install -g @gitpm/sync-gitlab\n npm install -g @gitpm/sync-jira\n npm install -g @gitpm/sync-linear\n npm install -g @gitpm/sync-bitbucket"
1237
+ );
1238
+ process.exit(1);
1239
+ }
1240
+ const adapters = adaptersResult.value;
1241
+ if (adapters.length === 0) {
1242
+ printError(
1243
+ "No sync adapters installed. Install at least one adapter package, e.g.:\n npm install -g @gitpm/sync-github"
1244
+ );
1245
+ process.exit(1);
1246
+ }
1247
+ let adapter = null;
1248
+ if (adapterName) {
1249
+ adapter = findAdapterByName2(adapters, adapterName);
1250
+ if (!adapter) {
1251
+ printError(
1252
+ `Adapter "${adapterName}" not found. Available: ${adapters.map((a) => a.name).join(", ")}`
1253
+ );
1254
+ process.exit(1);
1255
+ }
1256
+ } else {
1257
+ adapter = await detectAdapter(adapters, metaDir);
1258
+ if (!adapter) {
1259
+ printError(
1260
+ "No sync config found. Run `gitpm import` first to set up sync."
1261
+ );
1262
+ process.exit(1);
1263
+ }
1264
+ }
1265
+ return { adapter, config };
1266
+ }
1267
+
245
1268
  // src/utils/conflict-ui.ts
246
1269
  import { select } from "@inquirer/prompts";
247
- import chalk4 from "chalk";
1270
+ import chalk7 from "chalk";
248
1271
  async function promptConflictResolution(conflicts) {
249
1272
  const resolutions = [];
250
1273
  for (const conflict of conflicts) {
251
1274
  console.log();
252
- console.log(chalk4.yellow("\u2501\u2501\u2501 CONFLICT \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1275
+ console.log(chalk7.yellow("\u2501\u2501\u2501 CONFLICT \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
253
1276
  console.log(
254
- `Entity: ${chalk4.bold(`"${conflict.entityTitle}"`)} (${conflict.entityId})`
1277
+ `Entity: ${chalk7.bold(`"${conflict.entityTitle}"`)} (${conflict.entityId})`
255
1278
  );
256
1279
  console.log(`Type: ${conflict.entityType}`);
257
1280
  console.log(`Field: ${conflict.field}`);
258
1281
  console.log();
259
- console.log(` Local: ${chalk4.green(String(conflict.localValue))}`);
260
- console.log(` Remote: ${chalk4.blue(String(conflict.remoteValue))}`);
261
- console.log(chalk4.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1282
+ console.log(` Local: ${chalk7.green(String(conflict.localValue))}`);
1283
+ console.log(` Remote: ${chalk7.blue(String(conflict.remoteValue))}`);
1284
+ console.log(chalk7.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
262
1285
  const pick = await select({
263
1286
  message: "How do you want to resolve this conflict?",
264
1287
  choices: [
@@ -279,40 +1302,37 @@ async function promptConflictResolution(conflicts) {
279
1302
  }
280
1303
 
281
1304
  // src/commands/pull.ts
282
- var pullCommand = new Command3("pull").description("Pull changes from GitHub or GitLab into local .meta/").option("--token <token>", "Personal access token").option(
1305
+ var VALID_STRATEGIES = ["local-wins", "remote-wins", "ask"];
1306
+ var pullCommand = new Command10("pull").description("Pull changes from remote platform into local .meta/").option("--token <token>", "Personal access token").option(
283
1307
  "--strategy <strategy>",
284
1308
  "Conflict resolution strategy (local-wins, remote-wins, ask)",
285
1309
  "remote-wins"
286
- ).action(async (opts, cmd) => {
1310
+ ).option("--adapter <name>", "Force a specific adapter (e.g. github, gitlab)").action(async (opts, cmd) => {
287
1311
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
288
- const strategy = opts.strategy;
289
- const ghConfig = await loadGitHubConfig(metaDir);
290
- const glConfig = await loadGitLabConfig(metaDir);
291
- if (glConfig.ok) {
292
- await pullFromGitLab(opts, metaDir, glConfig.value, strategy);
293
- } else if (ghConfig.ok) {
294
- await pullFromGitHub(opts, metaDir, ghConfig.value.repo, strategy);
295
- } else {
1312
+ if (!VALID_STRATEGIES.includes(opts.strategy)) {
296
1313
  printError(
297
- "No sync config found. Run `gitpm import` first to set up sync."
1314
+ `Invalid strategy "${opts.strategy}". Must be one of: ${VALID_STRATEGIES.join(", ")}`
298
1315
  );
299
1316
  process.exit(1);
300
1317
  }
301
- });
302
- async function pullFromGitHub(opts, metaDir, repo, strategy) {
303
- let token;
304
- try {
305
- token = await resolveToken(opts.token);
306
- } catch (err) {
307
- printError(err instanceof Error ? err.message : "Failed to resolve token.");
1318
+ const strategy = opts.strategy;
1319
+ const { adapter, config } = await resolveAdapter(metaDir, opts.adapter);
1320
+ const { token, email } = resolveCredentials(adapter.name, opts.token);
1321
+ const preHook = await runHooks2(config, "pre-sync", {
1322
+ metaDir,
1323
+ event: "pre-sync",
1324
+ adapterName: adapter.name
1325
+ });
1326
+ if (!preHook.ok) {
1327
+ printError(`Pre-sync hook failed: ${preHook.error.message}`);
308
1328
  process.exit(1);
309
1329
  }
310
- const spinner = ora2("Pulling from GitHub...").start();
311
- const result = await syncWithGitHub({
1330
+ const spinner = ora2(`Pulling from ${adapter.displayName}...`).start();
1331
+ const result = await adapter.sync({
312
1332
  token,
313
- repo,
1333
+ email,
314
1334
  metaDir,
315
- strategy: strategy === "ask" ? "ask" : strategy
1335
+ strategy
316
1336
  });
317
1337
  if (!result.ok) {
318
1338
  spinner.fail("Pull failed.");
@@ -321,281 +1341,936 @@ async function pullFromGitHub(opts, metaDir, repo, strategy) {
321
1341
  }
322
1342
  spinner.succeed("Pull complete.");
323
1343
  await handleConflictsAndPrint(result.value, strategy);
324
- }
325
- async function pullFromGitLab(opts, metaDir, config, strategy) {
326
- const token = opts.token ?? process.env.GITLAB_TOKEN;
327
- if (!token) {
328
- printError(
329
- "No GitLab token found. Provide via --token flag or GITLAB_TOKEN env var."
1344
+ await runHooks2(config, "post-sync", {
1345
+ metaDir,
1346
+ event: "post-sync",
1347
+ adapterName: adapter.name,
1348
+ result: result.value
1349
+ });
1350
+ });
1351
+ async function handleConflictsAndPrint(result, strategy) {
1352
+ const { pulled, conflicts, resolved, skipped } = result;
1353
+ if (strategy === "ask" && conflicts.length > 0) {
1354
+ console.log();
1355
+ console.log(
1356
+ chalk8.yellow(`${conflicts.length} conflict(s) require resolution:`)
1357
+ );
1358
+ const resolutions = await promptConflictResolution(
1359
+ conflicts
1360
+ );
1361
+ console.log(
1362
+ chalk8.dim(
1363
+ `Resolved ${resolutions.length}, skipped ${conflicts.length - resolutions.length}`
1364
+ )
330
1365
  );
1366
+ }
1367
+ console.log();
1368
+ console.log(chalk8.bold("Pull Summary:"));
1369
+ console.log(` Pulled: ${pulled.milestones + pulled.issues} changes`);
1370
+ console.log(` Conflicts resolved: ${resolved}`);
1371
+ console.log(` Skipped: ${skipped}`);
1372
+ console.log();
1373
+ printSuccess("Pull complete.");
1374
+ }
1375
+
1376
+ // src/commands/push.ts
1377
+ import { runHooks as runHooks3 } from "@gitpm/core";
1378
+ import { confirm } from "@inquirer/prompts";
1379
+ import chalk9 from "chalk";
1380
+ import { Command as Command11 } from "commander";
1381
+ import ora3 from "ora";
1382
+ var pushCommand = new Command11("push").description("Push local .meta/ changes to remote platform").option("--token <token>", "Personal access token").option("--team <id>", "Linear team ID").option(
1383
+ "--api-key <key>",
1384
+ "Linear API key (default: LINEAR_API_KEY environment variable)"
1385
+ ).option("--dry-run", "Preview changes without pushing").option("--yes", "Skip confirmation prompt").option("--adapter <name>", "Force a specific adapter (e.g. github, gitlab)").action(async (opts, cmd) => {
1386
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1387
+ const { adapter, config } = await resolveAdapter(metaDir, opts.adapter);
1388
+ const { token, email } = resolveCredentials(adapter.name, opts.token);
1389
+ if (opts.dryRun) {
1390
+ const spinner = ora3("Calculating changes...").start();
1391
+ const result2 = await adapter.export({
1392
+ token,
1393
+ email,
1394
+ metaDir,
1395
+ teamId: opts.team,
1396
+ apiKey: opts.apiKey,
1397
+ dryRun: true
1398
+ });
1399
+ if (!result2.ok) {
1400
+ spinner.fail("Dry run failed.");
1401
+ printError(result2.error.message);
1402
+ process.exit(1);
1403
+ }
1404
+ spinner.succeed("Dry run complete.");
1405
+ printPreview(result2.value);
1406
+ return;
1407
+ }
1408
+ const previewSpinner = ora3("Calculating changes...").start();
1409
+ const preview = await adapter.export({
1410
+ token,
1411
+ email,
1412
+ metaDir,
1413
+ teamId: opts.team,
1414
+ apiKey: opts.apiKey,
1415
+ dryRun: true
1416
+ });
1417
+ if (!preview.ok) {
1418
+ previewSpinner.fail("Failed to calculate changes.");
1419
+ printError(preview.error.message);
1420
+ process.exit(1);
1421
+ }
1422
+ previewSpinner.succeed("Changes calculated.");
1423
+ printPreview(preview.value);
1424
+ if (preview.value.totalChanges === 0) {
1425
+ printSuccess("Nothing to push \u2014 already in sync.");
1426
+ return;
1427
+ }
1428
+ if (!opts.yes) {
1429
+ const confirmed = await confirm({
1430
+ message: `Push these changes to ${adapter.displayName}?`,
1431
+ default: false
1432
+ });
1433
+ if (!confirmed) {
1434
+ console.log(chalk9.dim("Push cancelled."));
1435
+ return;
1436
+ }
1437
+ }
1438
+ const preHook = await runHooks3(config, "pre-export", {
1439
+ metaDir,
1440
+ event: "pre-export",
1441
+ adapterName: adapter.name
1442
+ });
1443
+ if (!preHook.ok) {
1444
+ printError(`Pre-export hook failed: ${preHook.error.message}`);
331
1445
  process.exit(1);
332
1446
  }
333
- const spinner = ora2("Pulling from GitLab...").start();
334
- const result = await syncWithGitLab({
1447
+ const pushSpinner = ora3(`Pushing to ${adapter.displayName}...`).start();
1448
+ const result = await adapter.export({
335
1449
  token,
336
- project: config.project,
337
- projectId: config.project_id,
338
- baseUrl: config.base_url,
1450
+ email,
339
1451
  metaDir,
340
- strategy: strategy === "ask" ? "ask" : strategy
1452
+ teamId: opts.team,
1453
+ apiKey: opts.apiKey,
1454
+ dryRun: false
341
1455
  });
342
1456
  if (!result.ok) {
343
- spinner.fail("Pull failed.");
1457
+ pushSpinner.fail("Push failed.");
344
1458
  printError(result.error.message);
345
1459
  process.exit(1);
346
1460
  }
347
- spinner.succeed("Pull complete.");
348
- await handleConflictsAndPrint(result.value, strategy);
349
- }
350
- async function handleConflictsAndPrint(result, strategy) {
351
- const { pulled, conflicts, resolved, skipped } = result;
352
- if (strategy === "ask" && conflicts.length > 0) {
353
- console.log();
1461
+ pushSpinner.succeed("Push complete.");
1462
+ const { created, updated } = result.value;
1463
+ console.log(
1464
+ ` Created ${created.milestones + created.issues}, Updated ${updated.milestones + updated.issues}`
1465
+ );
1466
+ await runHooks3(config, "post-export", {
1467
+ metaDir,
1468
+ event: "post-export",
1469
+ adapterName: adapter.name,
1470
+ result: result.value
1471
+ });
1472
+ });
1473
+ function printPreview(result) {
1474
+ console.log();
1475
+ console.log(chalk9.bold("Changes to push:"));
1476
+ console.log(` New milestones: ${result.created.milestones}`);
1477
+ console.log(` New issues: ${result.created.issues}`);
1478
+ console.log(` Updated milestones: ${result.updated.milestones}`);
1479
+ console.log(` Updated issues: ${result.updated.issues}`);
1480
+ console.log(` Total changes: ${result.totalChanges}`);
1481
+ console.log();
1482
+ }
1483
+
1484
+ // src/commands/quality.ts
1485
+ import { relative as relative6 } from "path";
1486
+ import {
1487
+ loadQualityConfig,
1488
+ parseTree as parseTree5,
1489
+ resolveRefs as resolveRefs3,
1490
+ scoreEntities
1491
+ } from "@gitpm/core";
1492
+ import chalk10 from "chalk";
1493
+ import { Command as Command12 } from "commander";
1494
+ function gradeColor(grade) {
1495
+ switch (grade) {
1496
+ case "A":
1497
+ return chalk10.green;
1498
+ case "B":
1499
+ return chalk10.cyan;
1500
+ case "C":
1501
+ return chalk10.yellow;
1502
+ case "D":
1503
+ return chalk10.magenta;
1504
+ default:
1505
+ return chalk10.red;
1506
+ }
1507
+ }
1508
+ function gradeForAverage(avg) {
1509
+ if (avg >= 8) return "A";
1510
+ if (avg >= 6) return "B";
1511
+ if (avg >= 4) return "C";
1512
+ if (avg >= 2) return "D";
1513
+ return "F";
1514
+ }
1515
+ function printReport2(report) {
1516
+ console.log();
1517
+ console.log(chalk10.bold(" Entity Quality Report"));
1518
+ console.log(chalk10.dim(` ${"\u2500".repeat(21)}`));
1519
+ const grades = [
1520
+ { grade: "A", range: "8-9", count: report.distribution.A },
1521
+ { grade: "B", range: "6-7", count: report.distribution.B },
1522
+ { grade: "C", range: "4-5", count: report.distribution.C },
1523
+ { grade: "D", range: "2-3", count: report.distribution.D },
1524
+ { grade: "F", range: "0-1", count: report.distribution.F }
1525
+ ];
1526
+ for (const { grade, range, count } of grades) {
1527
+ const label = `${grade} (${range}):`;
1528
+ const unit = count === 1 ? "entity" : "entities";
1529
+ console.log(` ${gradeColor(grade)(label.padEnd(10))} ${count} ${unit}`);
1530
+ }
1531
+ const avgGrade = gradeForAverage(report.average);
1532
+ console.log();
1533
+ console.log(
1534
+ ` Average: ${report.average.toFixed(1)} (${gradeColor(avgGrade)(avgGrade)})`
1535
+ );
1536
+ const lowest = report.entities.filter((e) => e.score < 6).sort((a, b) => a.score - b.score).slice(0, 10);
1537
+ if (lowest.length > 0) {
1538
+ console.log();
1539
+ console.log(chalk10.bold(" Lowest scoring:"));
1540
+ for (const e of lowest) {
1541
+ const rel = relative6(process.cwd(), e.filePath);
1542
+ const color = gradeColor(e.grade);
1543
+ console.log(
1544
+ ` ${color(`[${e.grade}]`)} ${e.id} (${e.score}/${e.maxScore}) ${chalk10.dim("\u2014")} ${chalk10.dim(rel)}`
1545
+ );
1546
+ }
1547
+ }
1548
+ console.log();
1549
+ }
1550
+ var qualityCommand = new Command12("quality").description("Score entity quality across the .meta/ tree").option(
1551
+ "--threshold <number>",
1552
+ "Minimum average score (overrides config)",
1553
+ Number.parseFloat
1554
+ ).action(async (opts, cmd) => {
1555
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1556
+ const parseResult = await parseTree5(metaDir);
1557
+ if (!parseResult.ok) {
1558
+ printError(parseResult.error.message);
1559
+ process.exit(1);
1560
+ }
1561
+ const resolveResult = resolveRefs3(parseResult.value);
1562
+ if (!resolveResult.ok) {
1563
+ printError(resolveResult.error.message);
1564
+ process.exit(1);
1565
+ }
1566
+ const configResult = await loadQualityConfig(metaDir);
1567
+ if (!configResult.ok) {
1568
+ printError(configResult.error.message);
1569
+ process.exit(1);
1570
+ }
1571
+ const config = configResult.value;
1572
+ const report = scoreEntities(resolveResult.value, config);
1573
+ if (report.entities.length === 0) {
1574
+ console.log(chalk10.dim(" No scorable entities found in .meta/"));
1575
+ return;
1576
+ }
1577
+ printReport2(report);
1578
+ const threshold = opts.threshold ?? config?.threshold?.min_average;
1579
+ if (threshold != null && report.average < threshold) {
1580
+ printError(
1581
+ `Average score ${report.average.toFixed(1)} is below threshold ${threshold}`
1582
+ );
1583
+ process.exit(1);
1584
+ }
1585
+ });
1586
+
1587
+ // src/commands/query.ts
1588
+ import { filterEntities, formatEntities, parseTree as parseTree6 } from "@gitpm/core";
1589
+ import { Command as Command13 } from "commander";
1590
+ var DEFAULT_FIELDS = [
1591
+ "id",
1592
+ "title",
1593
+ "type",
1594
+ "status",
1595
+ "priority",
1596
+ "filePath"
1597
+ ];
1598
+ var queryCommand = new Command13("query").description("Query and filter .meta/ entities").option(
1599
+ "--type <types>",
1600
+ "Entity types (comma-separated: story,epic,milestone,prd)"
1601
+ ).option("--status <statuses>", "Filter by status (comma-separated)").option("--priority <priorities>", "Filter by priority (comma-separated)").option("--label <labels>", "Filter by labels (comma-separated, match any)").option("--epic <epic>", "Filter stories by epic ID or directory slug").option("--assignee <assignee>", "Filter by assignee").option("--search <text>", "Text search in title and body").option(
1602
+ "--fields <fields>",
1603
+ "Fields to display (comma-separated)",
1604
+ DEFAULT_FIELDS.join(",")
1605
+ ).option("--format <format>", "Output format: table, json, csv", "table").action(async (opts, cmd) => {
1606
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1607
+ const parseResult = await parseTree6(metaDir);
1608
+ if (!parseResult.ok) {
1609
+ printError(parseResult.error.message);
1610
+ process.exit(1);
1611
+ }
1612
+ const filter = {};
1613
+ if (opts.type) {
1614
+ filter.type = opts.type.split(",").map((s) => s.trim());
1615
+ }
1616
+ if (opts.status) {
1617
+ filter.status = opts.status.split(",").map((s) => s.trim());
1618
+ }
1619
+ if (opts.priority) {
1620
+ filter.priority = opts.priority.split(",").map((s) => s.trim());
1621
+ }
1622
+ if (opts.label) {
1623
+ filter.labels = opts.label.split(",").map((s) => s.trim());
1624
+ }
1625
+ if (opts.epic) {
1626
+ filter.epic = opts.epic;
1627
+ }
1628
+ if (opts.assignee) {
1629
+ filter.assignee = opts.assignee;
1630
+ }
1631
+ if (opts.search) {
1632
+ filter.search = opts.search;
1633
+ }
1634
+ const entities = filterEntities(parseResult.value, filter);
1635
+ const fields = opts.fields.split(",").map((s) => s.trim());
1636
+ const output = formatEntities(entities, { fields, format: opts.format });
1637
+ console.log(output);
1638
+ });
1639
+
1640
+ // src/commands/set.ts
1641
+ import { resolve as resolve5 } from "path";
1642
+ import {
1643
+ applyAssignments,
1644
+ parseAssignment,
1645
+ parseFile,
1646
+ writeFile as writeFile4
1647
+ } from "@gitpm/core";
1648
+ import { Command as Command14 } from "commander";
1649
+ var setCommand = new Command14("set").description("Update frontmatter fields on a .meta/ entity").argument("<file>", "Path to the entity file").argument(
1650
+ "<assignments...>",
1651
+ "Field assignments (field=value, field+=value, field-=value)"
1652
+ ).action(async (file, assignments) => {
1653
+ const filePath = resolve5(process.cwd(), file);
1654
+ const parseResult = await parseFile(filePath);
1655
+ if (!parseResult.ok) {
1656
+ printError(parseResult.error.message);
1657
+ process.exit(1);
1658
+ }
1659
+ const parsed = [];
1660
+ for (const expr of assignments) {
1661
+ const result = parseAssignment(expr);
1662
+ if (!result.ok) {
1663
+ printError(result.error.message);
1664
+ process.exit(1);
1665
+ }
1666
+ parsed.push(result.value);
1667
+ }
1668
+ const applyResult = applyAssignments(parseResult.value, parsed);
1669
+ if (!applyResult.ok) {
1670
+ printError(applyResult.error.message);
1671
+ process.exit(1);
1672
+ }
1673
+ const writeResult = await writeFile4(applyResult.value, filePath);
1674
+ if (!writeResult.ok) {
1675
+ printError(writeResult.error.message);
1676
+ process.exit(1);
1677
+ }
1678
+ printSuccess(`Updated ${file}`);
1679
+ });
1680
+
1681
+ // src/commands/show.ts
1682
+ import { relative as relative7, resolve as resolve6 } from "path";
1683
+ import { parseTree as parseTree7, resolveRefs as resolveRefs4 } from "@gitpm/core";
1684
+ import chalk11 from "chalk";
1685
+ import { Command as Command15 } from "commander";
1686
+ function formatFieldValue(key, value) {
1687
+ if (value === null || value === void 0) return chalk11.dim("\u2014");
1688
+ if (Array.isArray(value)) {
1689
+ return value.length > 0 ? value.join(", ") : chalk11.dim("[]");
1690
+ }
1691
+ if (typeof value === "object") {
1692
+ return JSON.stringify(value);
1693
+ }
1694
+ if (key === "priority") {
1695
+ const p = String(value);
1696
+ switch (p) {
1697
+ case "critical":
1698
+ return chalk11.red(p);
1699
+ case "high":
1700
+ return chalk11.yellow(p);
1701
+ case "medium":
1702
+ return chalk11.blue(p);
1703
+ default:
1704
+ return chalk11.dim(p);
1705
+ }
1706
+ }
1707
+ if (key === "status") {
1708
+ const s = String(value);
1709
+ switch (s) {
1710
+ case "done":
1711
+ return chalk11.green(s);
1712
+ case "in_progress":
1713
+ case "in_review":
1714
+ return chalk11.cyan(s);
1715
+ case "cancelled":
1716
+ return chalk11.strikethrough(chalk11.dim(s));
1717
+ default:
1718
+ return s;
1719
+ }
1720
+ }
1721
+ return String(value);
1722
+ }
1723
+ var RESOLVED_KEYS = /* @__PURE__ */ new Set([
1724
+ "resolvedStories",
1725
+ "resolvedEpic",
1726
+ "resolvedEpics",
1727
+ "resolvedMilestone",
1728
+ "resolvedMilestones",
1729
+ "resolvedPrds"
1730
+ ]);
1731
+ function displayEntity(entity, full) {
1732
+ const { filePath, body, ...frontmatter } = entity;
1733
+ console.log(chalk11.bold(entity.title));
1734
+ console.log(chalk11.dim(relative7(process.cwd(), entity.filePath)));
1735
+ console.log();
1736
+ for (const [key, value] of Object.entries(frontmatter)) {
1737
+ if (key === "title" || RESOLVED_KEYS.has(key)) continue;
1738
+ console.log(` ${chalk11.dim(`${key}:`)} ${formatFieldValue(key, value)}`);
1739
+ }
1740
+ if (full && typeof body === "string" && body.trim()) {
1741
+ console.log();
1742
+ console.log(body);
1743
+ }
1744
+ console.log();
1745
+ }
1746
+ function displayStoryRow(story) {
1747
+ const pri = story.priority;
1748
+ let priBullet;
1749
+ switch (pri) {
1750
+ case "critical":
1751
+ priBullet = chalk11.red("\u25CF");
1752
+ break;
1753
+ case "high":
1754
+ priBullet = chalk11.yellow("\u25CF");
1755
+ break;
1756
+ case "medium":
1757
+ priBullet = chalk11.blue("\u25CF");
1758
+ break;
1759
+ default:
1760
+ priBullet = chalk11.dim("\u25CF");
1761
+ }
1762
+ const status = chalk11.dim(`[${story.status}]`);
1763
+ const id = chalk11.dim(`(${story.id})`);
1764
+ console.log(` ${priBullet} ${status} ${story.title} ${id}`);
1765
+ }
1766
+ var showCommand = new Command15("show").description("Display .meta/ entities with structured output").argument("[target]", "Entity file path or ID").option("--epic <epic>", "Show an epic and all its stories (by ID or slug)").option(
1767
+ "--milestone <milestone>",
1768
+ "Show a milestone and linked epics (by ID)"
1769
+ ).option("--full", "Include body content", false).option("--format <format>", "Output format: pretty, json", "pretty").action(async (target, opts, cmd) => {
1770
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1771
+ const parseResult = await parseTree7(metaDir);
1772
+ if (!parseResult.ok) {
1773
+ printError(parseResult.error.message);
1774
+ process.exit(1);
1775
+ }
1776
+ const tree = parseResult.value;
1777
+ if (!opts.epic && !opts.milestone) {
1778
+ if (!target) {
1779
+ printError("Specify a target, --epic, or --milestone");
1780
+ process.exit(1);
1781
+ }
1782
+ const entity = findEntity(tree, target);
1783
+ if (!entity) {
1784
+ printError(`Entity not found: ${target}`);
1785
+ process.exit(1);
1786
+ }
1787
+ if (opts.format === "json") {
1788
+ console.log(JSON.stringify(entity, null, 2));
1789
+ } else {
1790
+ displayEntity(entity, opts.full);
1791
+ }
1792
+ return;
1793
+ }
1794
+ const resolveResult = resolveRefs4(tree);
1795
+ if (!resolveResult.ok) {
1796
+ printError(resolveResult.error.message);
1797
+ process.exit(1);
1798
+ }
1799
+ const resolved = resolveResult.value;
1800
+ if (opts.epic) {
1801
+ const epic = findEpic(resolved, opts.epic);
1802
+ if (!epic) {
1803
+ printError(`Epic not found: ${opts.epic}`);
1804
+ process.exit(1);
1805
+ }
1806
+ if (opts.format === "json") {
1807
+ console.log(
1808
+ JSON.stringify({ epic, stories: epic.resolvedStories }, null, 2)
1809
+ );
1810
+ } else {
1811
+ displayEntity(epic, opts.full);
1812
+ if (epic.resolvedStories.length > 0) {
1813
+ console.log(chalk11.bold(`Stories (${epic.resolvedStories.length}):`));
1814
+ for (const story of epic.resolvedStories) {
1815
+ displayStoryRow(story);
1816
+ }
1817
+ console.log();
1818
+ }
1819
+ }
1820
+ return;
1821
+ }
1822
+ if (opts.milestone) {
1823
+ const ms = findMilestone(resolved, opts.milestone);
1824
+ if (!ms) {
1825
+ printError(`Milestone not found: ${opts.milestone}`);
1826
+ process.exit(1);
1827
+ }
1828
+ if (opts.format === "json") {
1829
+ console.log(
1830
+ JSON.stringify({ milestone: ms, epics: ms.resolvedEpics }, null, 2)
1831
+ );
1832
+ } else {
1833
+ displayEntity(ms, opts.full);
1834
+ if (ms.resolvedEpics.length > 0) {
1835
+ console.log(chalk11.bold(`Epics (${ms.resolvedEpics.length}):`));
1836
+ for (const epic of ms.resolvedEpics) {
1837
+ displayStoryRow(epic);
1838
+ }
1839
+ console.log();
1840
+ }
1841
+ }
1842
+ return;
1843
+ }
1844
+ });
1845
+ function findEpic(resolved, epicRef) {
1846
+ return resolved.epics.find(
1847
+ (e) => e.id === epicRef || e.filePath.includes(`/epics/${epicRef}/`)
1848
+ );
1849
+ }
1850
+ function findMilestone(resolved, milestoneRef) {
1851
+ return resolved.milestones.find((m) => m.id === milestoneRef);
1852
+ }
1853
+ function findEntity(tree, target) {
1854
+ const all = [
1855
+ ...tree.stories,
1856
+ ...tree.epics,
1857
+ ...tree.milestones,
1858
+ ...tree.roadmaps,
1859
+ ...tree.prds
1860
+ ];
1861
+ const resolved = resolve6(process.cwd(), target);
1862
+ const byPath = all.find((e) => e.filePath === resolved);
1863
+ if (byPath) return byPath;
1864
+ return all.find((e) => e.id === target);
1865
+ }
1866
+
1867
+ // src/commands/sprint.ts
1868
+ import { relative as relative8, resolve as resolvePath } from "path";
1869
+ import {
1870
+ createSprint,
1871
+ parseTree as parseTree8,
1872
+ resolveRefs as resolveRefs5,
1873
+ writeFile as writeFile5
1874
+ } from "@gitpm/core";
1875
+ import { checkbox, confirm as confirm2 } from "@inquirer/prompts";
1876
+ import chalk12 from "chalk";
1877
+ import { Command as Command16 } from "commander";
1878
+ function sprintProgress(sprint) {
1879
+ const total = sprint.resolvedStories.length;
1880
+ const done = sprint.resolvedStories.filter(
1881
+ (s) => s.status === "done" || s.status === "cancelled"
1882
+ ).length;
1883
+ return { done, total, ratio: total > 0 ? done / total : 0 };
1884
+ }
1885
+ function isPickableStory(story) {
1886
+ return story.status === "todo" || story.status === "backlog";
1887
+ }
1888
+ function findSprintByIdOrPath(sprints, idOrPath) {
1889
+ const looksLikePath = idOrPath.includes("/") || idOrPath.endsWith(".md");
1890
+ if (looksLikePath) {
1891
+ const target = resolvePath(process.cwd(), idOrPath);
1892
+ return sprints.find((s) => resolvePath(s.filePath) === target);
1893
+ }
1894
+ return sprints.find((s) => s.id === idOrPath);
1895
+ }
1896
+ function printPointsSummary(totalPoints, capacity) {
1897
+ if (capacity === void 0) {
1898
+ console.log(chalk12.dim(` Points: ${totalPoints} pts (no capacity set)`));
1899
+ return;
1900
+ }
1901
+ const ratio = capacity > 0 ? totalPoints / capacity : totalPoints > 0 ? 1 : 0;
1902
+ const bar = progressBar(Math.min(ratio, 1), 16);
1903
+ const label = `${totalPoints}/${capacity} pts`;
1904
+ if (totalPoints > capacity) {
1905
+ console.log(` ${bar} ${chalk12.red(`${label} \u2014 over capacity`)}`);
1906
+ } else {
1907
+ console.log(` ${bar} ${label}`);
1908
+ }
1909
+ }
1910
+ var sprintCreateCommand = new Command16("create").description("Create a new sprint").requiredOption("--title <title>", "Sprint title").requiredOption("--start <date>", "Start date (YYYY-MM-DD)").requiredOption("--end <date>", "End date (YYYY-MM-DD)").option("--capacity <points>", "Story point capacity", Number.parseInt).action(async (opts, cmd) => {
1911
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1912
+ const result = await createSprint(metaDir, {
1913
+ title: opts.title,
1914
+ startDate: opts.start,
1915
+ endDate: opts.end,
1916
+ capacity: opts.capacity
1917
+ });
1918
+ if (!result.ok) {
1919
+ printError(result.error.message);
1920
+ process.exit(1);
1921
+ }
1922
+ const rel = relative8(process.cwd(), result.value.filePath);
1923
+ printSuccess(`Created sprint: ${rel} (${result.value.id})`);
1924
+ });
1925
+ var sprintListCommand = new Command16("list").description("List all sprints with progress").option("--json", "Output as JSON").action(async (opts, cmd) => {
1926
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
1927
+ const parseResult = await parseTree8(metaDir);
1928
+ if (!parseResult.ok) {
1929
+ printError(parseResult.error.message);
1930
+ process.exit(1);
1931
+ }
1932
+ const resolveResult = resolveRefs5(parseResult.value);
1933
+ if (!resolveResult.ok) {
1934
+ printError(resolveResult.error.message);
1935
+ process.exit(1);
1936
+ }
1937
+ const sprints = resolveResult.value.sprints;
1938
+ if (opts.json) {
1939
+ const data = sprints.map((sp) => {
1940
+ const { done, total, ratio } = sprintProgress(sp);
1941
+ return {
1942
+ id: sp.id,
1943
+ title: sp.title,
1944
+ status: sp.status,
1945
+ start_date: sp.start_date,
1946
+ end_date: sp.end_date,
1947
+ capacity: sp.capacity,
1948
+ stories: total,
1949
+ done,
1950
+ progress: ratio
1951
+ };
1952
+ });
1953
+ console.log(JSON.stringify(data, null, 2));
1954
+ return;
1955
+ }
1956
+ if (sprints.length === 0) {
354
1957
  console.log(
355
- chalk5.yellow(`${conflicts.length} conflict(s) require resolution:`)
1958
+ chalk12.dim(" No sprints found. Create one with: gitpm sprint create")
356
1959
  );
357
- const resolutions = await promptConflictResolution(
358
- conflicts
1960
+ return;
1961
+ }
1962
+ console.log();
1963
+ console.log(chalk12.bold(" Sprints"));
1964
+ console.log(chalk12.dim(` ${"\u2500".repeat(7)}`));
1965
+ for (const sp of sprints) {
1966
+ const { done, total, ratio } = sprintProgress(sp);
1967
+ const pct3 = Math.round(ratio * 100);
1968
+ const dateRange = chalk12.dim(
1969
+ `${sp.start_date.slice(0, 10)} \u2192 ${sp.end_date.slice(0, 10)}`
359
1970
  );
1971
+ const cap = sp.capacity ? chalk12.dim(` (cap: ${sp.capacity}pts)`) : "";
360
1972
  console.log(
361
- chalk5.dim(
362
- `Resolved ${resolutions.length}, skipped ${conflicts.length - resolutions.length}`
363
- )
1973
+ ` ${progressBar(ratio, 16)} ${pct3}% ${sp.title} ${dateRange}${cap}`
1974
+ );
1975
+ console.log(
1976
+ ` ${" ".repeat(16)} ${chalk12.dim(`${done}/${total} stories`)} ${chalk12.cyan(sp.status)}`
364
1977
  );
365
1978
  }
366
1979
  console.log();
367
- console.log(chalk5.bold("Pull Summary:"));
368
- console.log(` Pulled: ${pulled.milestones + pulled.issues} changes`);
369
- console.log(` Conflicts resolved: ${resolved}`);
370
- console.log(` Skipped: ${skipped}`);
371
- console.log();
372
- printSuccess("Pull complete.");
373
- }
374
-
375
- // src/commands/push.ts
376
- import {
377
- exportToGitHub,
378
- loadConfig as loadGitHubConfig2
379
- } from "@gitpm/sync-github";
380
- import {
381
- exportToGitLab,
382
- loadConfig as loadGitLabConfig2
383
- } from "@gitpm/sync-gitlab";
384
- import { confirm } from "@inquirer/prompts";
385
- import chalk6 from "chalk";
386
- import { Command as Command4 } from "commander";
387
- import ora3 from "ora";
388
- var pushCommand = new Command4("push").description("Push local .meta/ changes to GitHub or GitLab").option("--token <token>", "Personal access token").option("--dry-run", "Preview changes without pushing").option("--yes", "Skip confirmation prompt").action(async (opts, cmd) => {
1980
+ });
1981
+ var sprintShowCommand = new Command16("show").description("Show sprint details").argument("<sprint-id>", "Sprint ID").action(async (sprintId, _opts, cmd) => {
389
1982
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
390
- const ghConfig = await loadGitHubConfig2(metaDir);
391
- const glConfig = await loadGitLabConfig2(metaDir);
392
- if (glConfig.ok) {
393
- await pushToGitLab(opts, metaDir, glConfig.value);
394
- } else if (ghConfig.ok) {
395
- await pushToGitHub(opts, metaDir, ghConfig.value.repo);
396
- } else {
397
- printError(
398
- "No sync config found. Run `gitpm import` first to set up sync."
399
- );
1983
+ const parseResult = await parseTree8(metaDir);
1984
+ if (!parseResult.ok) {
1985
+ printError(parseResult.error.message);
400
1986
  process.exit(1);
401
1987
  }
402
- });
403
- async function pushToGitHub(opts, metaDir, repo) {
404
- let token;
405
- try {
406
- token = await resolveToken(opts.token);
407
- } catch (err) {
408
- printError(err instanceof Error ? err.message : "Failed to resolve token.");
1988
+ const resolveResult = resolveRefs5(parseResult.value);
1989
+ if (!resolveResult.ok) {
1990
+ printError(resolveResult.error.message);
409
1991
  process.exit(1);
410
1992
  }
411
- if (opts.dryRun) {
412
- const spinner = ora3("Calculating changes...").start();
413
- const result2 = await exportToGitHub({ token, repo, metaDir, dryRun: true });
414
- if (!result2.ok) {
415
- spinner.fail("Dry run failed.");
416
- printError(result2.error.message);
417
- process.exit(1);
418
- }
419
- spinner.succeed("Dry run complete.");
420
- printPreview(result2.value);
421
- return;
422
- }
423
- const previewSpinner = ora3("Calculating changes...").start();
424
- const preview = await exportToGitHub({
425
- token,
426
- repo,
427
- metaDir,
428
- dryRun: true
429
- });
430
- if (!preview.ok) {
431
- previewSpinner.fail("Failed to calculate changes.");
432
- printError(preview.error.message);
1993
+ const sprint = resolveResult.value.sprints.find((s) => s.id === sprintId);
1994
+ if (!sprint) {
1995
+ printError(`Sprint not found: ${sprintId}`);
433
1996
  process.exit(1);
434
1997
  }
435
- previewSpinner.succeed("Changes calculated.");
436
- printPreview(preview.value);
437
- if (preview.value.totalChanges === 0) {
438
- printSuccess("Nothing to push \u2014 already in sync.");
439
- return;
1998
+ const { done, total, ratio } = sprintProgress(sprint);
1999
+ const pct3 = Math.round(ratio * 100);
2000
+ console.log();
2001
+ console.log(chalk12.bold(` ${sprint.title}`));
2002
+ console.log(chalk12.dim(` ${"\u2500".repeat(sprint.title.length)}`));
2003
+ console.log(` Status: ${chalk12.cyan(sprint.status)}`);
2004
+ console.log(
2005
+ ` Period: ${sprint.start_date.slice(0, 10)} \u2192 ${sprint.end_date.slice(0, 10)}`
2006
+ );
2007
+ if (sprint.capacity) console.log(` Capacity: ${sprint.capacity} pts`);
2008
+ console.log(
2009
+ ` Progress: ${progressBar(ratio, 16)} ${pct3}% (${done}/${total})`
2010
+ );
2011
+ console.log();
2012
+ if (sprint.resolvedStories.length > 0) {
2013
+ console.log(chalk12.bold(" Stories:"));
2014
+ for (const story of sprint.resolvedStories) {
2015
+ const statusColor = story.status === "done" ? chalk12.green : story.status === "in_progress" ? chalk12.yellow : chalk12.dim;
2016
+ console.log(
2017
+ ` ${statusColor("\u25CF")} ${story.title} ${chalk12.dim(`[${story.status}]`)}`
2018
+ );
2019
+ }
2020
+ } else {
2021
+ console.log(chalk12.dim(" No stories assigned."));
440
2022
  }
441
- if (!opts.yes) {
442
- const confirmed = await confirm({
443
- message: "Push these changes to GitHub?",
444
- default: false
2023
+ console.log();
2024
+ });
2025
+ var sprintPlanCommand = new Command16("plan").description(
2026
+ "Plan a sprint by picking stories to include (interactively, or via --add/--remove)"
2027
+ ).argument("<sprint>", "Sprint ID or file path").option(
2028
+ "--add <story-id...>",
2029
+ "Add story IDs to the sprint (non-interactive)"
2030
+ ).option(
2031
+ "--remove <story-id...>",
2032
+ "Remove story IDs from the sprint (non-interactive)"
2033
+ ).action(
2034
+ async (sprintArg, opts, cmd) => {
2035
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
2036
+ const parseResult = await parseTree8(metaDir);
2037
+ if (!parseResult.ok) {
2038
+ printError(parseResult.error.message);
2039
+ process.exit(1);
2040
+ }
2041
+ const resolveResult = resolveRefs5(parseResult.value);
2042
+ if (!resolveResult.ok) {
2043
+ printError(resolveResult.error.message);
2044
+ process.exit(1);
2045
+ }
2046
+ const sprint = findSprintByIdOrPath(
2047
+ resolveResult.value.sprints,
2048
+ sprintArg
2049
+ );
2050
+ if (!sprint) {
2051
+ printError(`Sprint not found: ${sprintArg}`);
2052
+ process.exit(1);
2053
+ }
2054
+ const allStories = resolveResult.value.stories;
2055
+ const storyById = new Map(allStories.map((s) => [s.id, s]));
2056
+ const currentIds = new Set(sprint.stories.map((ref) => ref.id));
2057
+ const otherSprintsByStory = /* @__PURE__ */ new Map();
2058
+ for (const other of resolveResult.value.sprints) {
2059
+ if (other.id === sprint.id) continue;
2060
+ if (other.status === "done" || other.status === "cancelled") continue;
2061
+ for (const ref of other.stories) {
2062
+ const titles = otherSprintsByStory.get(ref.id) ?? [];
2063
+ if (!titles.includes(other.title)) titles.push(other.title);
2064
+ otherSprintsByStory.set(ref.id, titles);
2065
+ }
2066
+ }
2067
+ const pointsFor = (ids) => [...ids].reduce(
2068
+ (sum, id) => sum + (storyById.get(id)?.estimate ?? 0),
2069
+ 0
2070
+ );
2071
+ const warnAlreadyPlanned = (ids) => {
2072
+ for (const id of ids) {
2073
+ if (currentIds.has(id)) continue;
2074
+ const others = otherSprintsByStory.get(id);
2075
+ if (others) {
2076
+ const title = storyById.get(id)?.title ?? id;
2077
+ const names = others.map((t) => `"${t}"`).join(", ");
2078
+ printWarning(
2079
+ others.length === 1 ? `Story "${title}" is already in sprint ${names}; its points will count toward both.` : `Story "${title}" is already in ${others.length} other sprints (${names}); its points will count toward all of them.`
2080
+ );
2081
+ }
2082
+ }
2083
+ };
2084
+ let nextIds;
2085
+ const nonInteractive = Boolean(opts.add?.length || opts.remove?.length);
2086
+ if (nonInteractive) {
2087
+ const addIds = opts.add ?? [];
2088
+ const unknownIds = addIds.filter((id) => !storyById.has(id));
2089
+ if (unknownIds.length > 0) {
2090
+ printError(
2091
+ `Unknown story ID${unknownIds.length > 1 ? "s" : ""}: ${unknownIds.join(", ")}`
2092
+ );
2093
+ process.exit(1);
2094
+ }
2095
+ nextIds = new Set(currentIds);
2096
+ for (const id of addIds) nextIds.add(id);
2097
+ for (const id of opts.remove ?? []) nextIds.delete(id);
2098
+ warnAlreadyPlanned(nextIds);
2099
+ } else {
2100
+ const pickable = allStories.filter(isPickableStory);
2101
+ const untouchedIds = new Set(
2102
+ [...currentIds].filter(
2103
+ (id) => !storyById.has(id) || !isPickableStory(storyById.get(id))
2104
+ )
2105
+ );
2106
+ if (pickable.length === 0) {
2107
+ console.log(
2108
+ chalk12.dim(" No todo/backlog stories available to plan.")
2109
+ );
2110
+ nextIds = currentIds;
2111
+ } else {
2112
+ const selected = await checkbox({
2113
+ message: `Select stories for "${sprint.title}"`,
2114
+ choices: pickable.map((story) => {
2115
+ const others = otherSprintsByStory.get(story.id);
2116
+ const inOthers = others ? ` ${chalk12.yellow(`(in ${others.join(", ")})`)}` : "";
2117
+ return {
2118
+ name: `${story.title} ${chalk12.dim(`[${story.priority}]`)} ${chalk12.dim(`(${story.estimate ?? "?"} pts)`)}${inOthers}`,
2119
+ value: story.id,
2120
+ checked: currentIds.has(story.id)
2121
+ };
2122
+ })
2123
+ });
2124
+ nextIds = /* @__PURE__ */ new Set([...untouchedIds, ...selected]);
2125
+ warnAlreadyPlanned(nextIds);
2126
+ console.log();
2127
+ printPointsSummary(pointsFor(nextIds), sprint.capacity);
2128
+ const proceed = await confirm2({
2129
+ message: `Save this selection to "${sprint.title}"?`,
2130
+ default: true
2131
+ });
2132
+ if (!proceed) {
2133
+ console.log(chalk12.dim(" Cancelled \u2014 no changes written."));
2134
+ return;
2135
+ }
2136
+ }
2137
+ }
2138
+ const totalPoints = pointsFor(nextIds);
2139
+ const seenIds = /* @__PURE__ */ new Set();
2140
+ const keptRefs = sprint.stories.filter((ref) => {
2141
+ if (!nextIds.has(ref.id) || seenIds.has(ref.id)) return false;
2142
+ seenIds.add(ref.id);
2143
+ return true;
445
2144
  });
446
- if (!confirmed) {
447
- console.log(chalk6.dim("Push cancelled."));
448
- return;
2145
+ const addedRefs = [...nextIds].filter((id) => !currentIds.has(id)).map((id) => ({ id }));
2146
+ const { resolvedStories: _resolvedStories, ...sprintBase } = sprint;
2147
+ const updatedSprint = {
2148
+ ...sprintBase,
2149
+ stories: [...keptRefs, ...addedRefs],
2150
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2151
+ };
2152
+ const writeResult = await writeFile5(updatedSprint, sprint.filePath);
2153
+ if (!writeResult.ok) {
2154
+ printError(writeResult.error.message);
2155
+ process.exit(1);
449
2156
  }
2157
+ console.log();
2158
+ printSuccess(
2159
+ `Updated sprint "${sprint.title}" (${nextIds.size} ${nextIds.size === 1 ? "story" : "stories"})`
2160
+ );
2161
+ printPointsSummary(totalPoints, sprint.capacity);
2162
+ console.log();
450
2163
  }
451
- const pushSpinner = ora3("Pushing to GitHub...").start();
452
- const result = await exportToGitHub({ token, repo, metaDir, dryRun: false });
453
- if (!result.ok) {
454
- pushSpinner.fail("Push failed.");
455
- printError(result.error.message);
456
- process.exit(1);
457
- }
458
- pushSpinner.succeed("Push complete.");
459
- const { created, updated } = result.value;
2164
+ );
2165
+ var sprintCommand = new Command16("sprint").description("Manage sprints (time-boxed iterations)").addCommand(sprintCreateCommand).addCommand(sprintListCommand).addCommand(sprintShowCommand).addCommand(sprintPlanCommand);
2166
+
2167
+ // src/commands/status.ts
2168
+ import {
2169
+ computeProjectProgress,
2170
+ parseTree as parseTree9,
2171
+ resolveRefs as resolveRefs6
2172
+ } from "@gitpm/core";
2173
+ import chalk13 from "chalk";
2174
+ import { Command as Command17 } from "commander";
2175
+ function pct2(ratio) {
2176
+ return `${Math.round(ratio * 100)}%`;
2177
+ }
2178
+ function printProjectProgress(progress) {
2179
+ console.log();
2180
+ console.log(chalk13.bold(" Project Status"));
2181
+ console.log(chalk13.dim(` ${"\u2500".repeat(14)}`));
2182
+ const { overall } = progress;
460
2183
  console.log(
461
- ` Created ${created.milestones + created.issues}, Updated ${updated.milestones + updated.issues}`
2184
+ ` ${progressBar(overall.progress, 20)} ${pct2(overall.progress)} (${overall.done}/${overall.total} stories)`
462
2185
  );
463
- }
464
- async function pushToGitLab(opts, metaDir, config) {
465
- const token = opts.token ?? process.env.GITLAB_TOKEN;
466
- if (!token) {
467
- printError(
468
- "No GitLab token found. Provide via --token flag or GITLAB_TOKEN env var."
2186
+ console.log();
2187
+ for (const ms of progress.milestones) {
2188
+ const dateStr = ms.targetDate ? chalk13.dim(` \u2014 due ${ms.targetDate.slice(0, 10)}`) : "";
2189
+ console.log(
2190
+ chalk13.bold(` \u{1F4CC} ${ms.title}`) + dateStr + chalk13.dim(` (${pct2(ms.progress)})`)
469
2191
  );
470
- process.exit(1);
2192
+ for (const ep of ms.epics) {
2193
+ const counts = chalk13.dim(
2194
+ `${ep.done}/${ep.total} done` + (ep.inProgress > 0 ? `, ${ep.inProgress} active` : "") + (ep.blocked > 0 ? `, ${ep.blocked} blocked` : "")
2195
+ );
2196
+ console.log(
2197
+ ` ${progressBar(ep.progress, 16)} ${pct2(ep.progress)} ${ep.title}`
2198
+ );
2199
+ console.log(` ${" ".repeat(16)} ${counts}`);
2200
+ }
2201
+ if (ms.epics.length === 0) {
2202
+ console.log(chalk13.dim(" (no epics linked)"));
2203
+ }
2204
+ console.log();
471
2205
  }
472
- if (opts.dryRun) {
473
- const spinner = ora3("Calculating changes...").start();
474
- const result2 = await exportToGitLab({
475
- token,
476
- project: config.project,
477
- projectId: config.project_id,
478
- baseUrl: config.base_url,
479
- metaDir,
480
- dryRun: true
481
- });
482
- if (!result2.ok) {
483
- spinner.fail("Dry run failed.");
484
- printError(result2.error.message);
485
- process.exit(1);
2206
+ if (progress.orphanEpics.length > 0) {
2207
+ console.log(chalk13.bold(" Unlinked Epics"));
2208
+ for (const ep of progress.orphanEpics) {
2209
+ const counts = chalk13.dim(`${ep.done}/${ep.total} done`);
2210
+ console.log(
2211
+ ` ${progressBar(ep.progress, 16)} ${pct2(ep.progress)} ${ep.title} ${counts}`
2212
+ );
486
2213
  }
487
- spinner.succeed("Dry run complete.");
488
- printPreview(result2.value);
489
- return;
2214
+ console.log();
490
2215
  }
491
- const previewSpinner = ora3("Calculating changes...").start();
492
- const preview = await exportToGitLab({
493
- token,
494
- project: config.project,
495
- projectId: config.project_id,
496
- baseUrl: config.base_url,
497
- metaDir,
498
- dryRun: true
499
- });
500
- if (!preview.ok) {
501
- previewSpinner.fail("Failed to calculate changes.");
502
- printError(preview.error.message);
2216
+ }
2217
+ var statusCommand = new Command17("status").description("Show project progress across milestones and epics").option("--json", "Output as JSON").action(async (opts, cmd) => {
2218
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
2219
+ const parseResult = await parseTree9(metaDir);
2220
+ if (!parseResult.ok) {
2221
+ printError(parseResult.error.message);
503
2222
  process.exit(1);
504
2223
  }
505
- previewSpinner.succeed("Changes calculated.");
506
- printPreview(preview.value);
507
- if (preview.value.totalChanges === 0) {
508
- printSuccess("Nothing to push \u2014 already in sync.");
509
- return;
2224
+ const resolveResult = resolveRefs6(parseResult.value);
2225
+ if (!resolveResult.ok) {
2226
+ printError(resolveResult.error.message);
2227
+ process.exit(1);
510
2228
  }
511
- if (!opts.yes) {
512
- const confirmed = await confirm({
513
- message: "Push these changes to GitLab?",
514
- default: false
515
- });
516
- if (!confirmed) {
517
- console.log(chalk6.dim("Push cancelled."));
518
- return;
519
- }
2229
+ const progress = computeProjectProgress(resolveResult.value);
2230
+ if (opts.json) {
2231
+ console.log(JSON.stringify(progress, null, 2));
2232
+ return;
520
2233
  }
521
- const pushSpinner = ora3("Pushing to GitLab...").start();
522
- const result = await exportToGitLab({
523
- token,
524
- project: config.project,
525
- projectId: config.project_id,
526
- baseUrl: config.base_url,
527
- metaDir,
528
- dryRun: false
529
- });
530
- if (!result.ok) {
531
- pushSpinner.fail("Push failed.");
532
- printError(result.error.message);
533
- process.exit(1);
2234
+ if (progress.overall.total === 0) {
2235
+ console.log(chalk13.dim(" No stories found in .meta/"));
2236
+ return;
534
2237
  }
535
- pushSpinner.succeed("Push complete.");
536
- const { created, updated } = result.value;
537
- console.log(
538
- ` Created ${created.milestones + created.issues}, Updated ${updated.milestones + updated.issues}`
539
- );
540
- }
541
- function printPreview(result) {
542
- console.log();
543
- console.log(chalk6.bold("Changes to push:"));
544
- console.log(` New milestones: ${result.created.milestones}`);
545
- console.log(` New issues: ${result.created.issues}`);
546
- console.log(` Updated milestones: ${result.updated.milestones}`);
547
- console.log(` Updated issues: ${result.updated.issues}`);
548
- console.log(` Total changes: ${result.totalChanges}`);
549
- console.log();
550
- }
2238
+ printProjectProgress(progress);
2239
+ });
551
2240
 
552
2241
  // src/commands/sync.ts
553
- import {
554
- loadConfig as loadGitHubConfig3,
555
- syncWithGitHub as syncWithGitHub2
556
- } from "@gitpm/sync-github";
557
- import {
558
- loadConfig as loadGitLabConfig3,
559
- syncWithGitLab as syncWithGitLab2
560
- } from "@gitpm/sync-gitlab";
561
- import { confirm as confirm2 } from "@inquirer/prompts";
562
- import chalk7 from "chalk";
563
- import { Command as Command5 } from "commander";
2242
+ import { runHooks as runHooks4 } from "@gitpm/core";
2243
+ import { confirm as confirm3 } from "@inquirer/prompts";
2244
+ import chalk14 from "chalk";
2245
+ import { Command as Command18 } from "commander";
564
2246
  import ora4 from "ora";
565
- var syncCommand = new Command5("sync").description("Bidirectional sync between local .meta/ and GitHub/GitLab").option("--token <token>", "Personal access token").option(
2247
+ var VALID_STRATEGIES2 = ["local-wins", "remote-wins", "ask"];
2248
+ var syncCommand = new Command18("sync").description("Bidirectional sync between local .meta/ and remote platform").option("--token <token>", "Personal access token").option(
566
2249
  "--strategy <strategy>",
567
2250
  "Conflict resolution strategy (local-wins, remote-wins, ask)",
568
2251
  "ask"
569
- ).option("--dry-run", "Preview changes without syncing").option("--yes", "Skip confirmation prompt").action(async (opts, cmd) => {
2252
+ ).option("--team <id>", "Linear team ID").option(
2253
+ "--api-key <key>",
2254
+ "Linear API key (default: LINEAR_API_KEY environment variable)"
2255
+ ).option("--dry-run", "Preview changes without syncing").option("--yes", "Skip confirmation prompt").option("--adapter <name>", "Force a specific adapter (e.g. github, gitlab)").action(async (opts, cmd) => {
570
2256
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
571
- const strategy = opts.strategy;
572
- const ghConfig = await loadGitHubConfig3(metaDir);
573
- const glConfig = await loadGitLabConfig3(metaDir);
574
- if (glConfig.ok) {
575
- await syncGitLab(opts, metaDir, glConfig.value, strategy);
576
- } else if (ghConfig.ok) {
577
- await syncGitHub(opts, metaDir, ghConfig.value.repo, strategy);
578
- } else {
2257
+ if (!VALID_STRATEGIES2.includes(opts.strategy)) {
579
2258
  printError(
580
- "No sync config found. Run `gitpm import` first to set up sync."
2259
+ `Invalid strategy "${opts.strategy}". Must be one of: ${VALID_STRATEGIES2.join(", ")}`
581
2260
  );
582
2261
  process.exit(1);
583
2262
  }
584
- });
585
- async function syncGitHub(opts, metaDir, repo, strategy) {
586
- let token;
587
- try {
588
- token = await resolveToken(opts.token);
589
- } catch (err) {
590
- printError(err instanceof Error ? err.message : "Failed to resolve token.");
591
- process.exit(1);
592
- }
2263
+ const strategy = opts.strategy;
2264
+ const { adapter, config } = await resolveAdapter(metaDir, opts.adapter);
2265
+ const { token, email } = resolveCredentials(adapter.name, opts.token);
593
2266
  if (opts.dryRun) {
594
2267
  const spinner2 = ora4("Calculating sync changes...").start();
595
- const result2 = await syncWithGitHub2({
2268
+ const result2 = await adapter.sync({
596
2269
  token,
597
- repo,
2270
+ email,
598
2271
  metaDir,
2272
+ teamId: opts.team,
2273
+ apiKey: opts.apiKey,
599
2274
  strategy,
600
2275
  dryRun: true
601
2276
  });
@@ -605,81 +2280,36 @@ async function syncGitHub(opts, metaDir, repo, strategy) {
605
2280
  process.exit(1);
606
2281
  }
607
2282
  spinner2.succeed("Dry run complete.");
608
- printSyncSummary(result2.value, "GitHub");
2283
+ printSyncSummary(result2.value, adapter.displayName);
609
2284
  return;
610
2285
  }
611
2286
  if (!opts.yes) {
612
- const confirmed = await confirm2({
613
- message: "Run bidirectional sync with GitHub?",
2287
+ const confirmed = await confirm3({
2288
+ message: `Run bidirectional sync with ${adapter.displayName}?`,
614
2289
  default: true
615
2290
  });
616
2291
  if (!confirmed) {
617
- console.log(chalk7.dim("Sync cancelled."));
2292
+ console.log(chalk14.dim("Sync cancelled."));
618
2293
  return;
619
2294
  }
620
2295
  }
621
- const spinner = ora4("Syncing with GitHub...").start();
622
- const result = await syncWithGitHub2({
623
- token,
624
- repo,
2296
+ const preHook = await runHooks4(config, "pre-sync", {
625
2297
  metaDir,
626
- strategy: strategy === "ask" ? "ask" : strategy
2298
+ event: "pre-sync",
2299
+ adapterName: adapter.name
627
2300
  });
628
- if (!result.ok) {
629
- spinner.fail("Sync failed.");
630
- printError(result.error.message);
631
- process.exit(1);
632
- }
633
- spinner.succeed("Sync complete.");
634
- await handleConflicts(result.value, strategy);
635
- printSyncSummary(result.value, "GitHub");
636
- }
637
- async function syncGitLab(opts, metaDir, config, strategy) {
638
- const token = opts.token ?? process.env.GITLAB_TOKEN;
639
- if (!token) {
640
- printError(
641
- "No GitLab token found. Provide via --token flag or GITLAB_TOKEN env var."
642
- );
2301
+ if (!preHook.ok) {
2302
+ printError(`Pre-sync hook failed: ${preHook.error.message}`);
643
2303
  process.exit(1);
644
2304
  }
645
- if (opts.dryRun) {
646
- const spinner2 = ora4("Calculating sync changes...").start();
647
- const result2 = await syncWithGitLab2({
648
- token,
649
- project: config.project,
650
- projectId: config.project_id,
651
- baseUrl: config.base_url,
652
- metaDir,
653
- strategy,
654
- dryRun: true
655
- });
656
- if (!result2.ok) {
657
- spinner2.fail("Dry run failed.");
658
- printError(result2.error.message);
659
- process.exit(1);
660
- }
661
- spinner2.succeed("Dry run complete.");
662
- printSyncSummary(result2.value, "GitLab");
663
- return;
664
- }
665
- if (!opts.yes) {
666
- const confirmed = await confirm2({
667
- message: "Run bidirectional sync with GitLab?",
668
- default: true
669
- });
670
- if (!confirmed) {
671
- console.log(chalk7.dim("Sync cancelled."));
672
- return;
673
- }
674
- }
675
- const spinner = ora4("Syncing with GitLab...").start();
676
- const result = await syncWithGitLab2({
2305
+ const spinner = ora4(`Syncing with ${adapter.displayName}...`).start();
2306
+ const result = await adapter.sync({
677
2307
  token,
678
- project: config.project,
679
- projectId: config.project_id,
680
- baseUrl: config.base_url,
2308
+ email,
681
2309
  metaDir,
682
- strategy: strategy === "ask" ? "ask" : strategy
2310
+ teamId: opts.team,
2311
+ apiKey: opts.apiKey,
2312
+ strategy
683
2313
  });
684
2314
  if (!result.ok) {
685
2315
  spinner.fail("Sync failed.");
@@ -688,20 +2318,26 @@ async function syncGitLab(opts, metaDir, config, strategy) {
688
2318
  }
689
2319
  spinner.succeed("Sync complete.");
690
2320
  await handleConflicts(result.value, strategy);
691
- printSyncSummary(result.value, "GitLab");
692
- }
2321
+ printSyncSummary(result.value, adapter.displayName);
2322
+ await runHooks4(config, "post-sync", {
2323
+ metaDir,
2324
+ event: "post-sync",
2325
+ adapterName: adapter.name,
2326
+ result: result.value
2327
+ });
2328
+ });
693
2329
  async function handleConflicts(result, strategy) {
694
2330
  const { conflicts } = result;
695
2331
  if (strategy === "ask" && conflicts.length > 0) {
696
2332
  console.log();
697
2333
  console.log(
698
- chalk7.yellow(`${conflicts.length} conflict(s) require resolution:`)
2334
+ chalk14.yellow(`${conflicts.length} conflict(s) require resolution:`)
699
2335
  );
700
2336
  const resolutions = await promptConflictResolution(
701
2337
  conflicts
702
2338
  );
703
2339
  console.log(
704
- chalk7.dim(
2340
+ chalk14.dim(
705
2341
  `Resolved ${resolutions.length}, skipped ${conflicts.length - resolutions.length}`
706
2342
  )
707
2343
  );
@@ -710,35 +2346,68 @@ async function handleConflicts(result, strategy) {
710
2346
  function printSyncSummary(result, platform) {
711
2347
  const pushedTotal = result.pushed.milestones + result.pushed.issues;
712
2348
  const pulledTotal = result.pulled.milestones + result.pulled.issues;
2349
+ const failedCount = result.failedEntities?.length ?? 0;
2350
+ if (result.resumedFromCheckpoint) {
2351
+ console.log();
2352
+ console.log(chalk14.yellow("Resumed from previous checkpoint."));
2353
+ }
713
2354
  console.log();
714
- console.log(chalk7.bold("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
715
- console.log(chalk7.bold("\u2502 Sync Complete \u2502"));
716
- console.log(chalk7.bold("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
717
2355
  console.log(
718
- `${chalk7.bold("\u2502")} Pushed to ${platform.padEnd(6)} \u2502 ${String(pushedTotal).padEnd(17)} ${chalk7.bold("\u2502")}`
2356
+ chalk14.bold(
2357
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"
2358
+ )
2359
+ );
2360
+ console.log(chalk14.bold("\u2502 Sync Complete \u2502"));
2361
+ console.log(
2362
+ chalk14.bold(
2363
+ "\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"
2364
+ )
2365
+ );
2366
+ console.log(
2367
+ `${chalk14.bold("\u2502")} Pushed to ${platform.padEnd(6)} \u2502 ${String(pushedTotal).padEnd(17)} ${chalk14.bold("\u2502")}`
2368
+ );
2369
+ console.log(
2370
+ `${chalk14.bold("\u2502")} Pulled to local \u2502 ${String(pulledTotal).padEnd(17)} ${chalk14.bold("\u2502")}`
719
2371
  );
720
2372
  console.log(
721
- `${chalk7.bold("\u2502")} Pulled to local \u2502 ${String(pulledTotal).padEnd(17)} ${chalk7.bold("\u2502")}`
2373
+ `${chalk14.bold("\u2502")} Conflicts \u2502 ${String(`${result.resolved} resolved`).padEnd(17)} ${chalk14.bold("\u2502")}`
722
2374
  );
723
2375
  console.log(
724
- `${chalk7.bold("\u2502")} Conflicts \u2502 ${String(`${result.resolved} resolved`).padEnd(17)} ${chalk7.bold("\u2502")}`
2376
+ `${chalk14.bold("\u2502")} Skipped \u2502 ${String(result.skipped).padEnd(17)} ${chalk14.bold("\u2502")}`
725
2377
  );
2378
+ if (failedCount > 0) {
2379
+ console.log(
2380
+ `${chalk14.bold("\u2502")} ${chalk14.red("Failed")} \u2502 ${chalk14.red(String(failedCount).padEnd(17))} ${chalk14.bold("\u2502")}`
2381
+ );
2382
+ }
726
2383
  console.log(
727
- `${chalk7.bold("\u2502")} Errors \u2502 ${String(result.skipped).padEnd(17)} ${chalk7.bold("\u2502")}`
2384
+ chalk14.bold(
2385
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
2386
+ )
728
2387
  );
729
- console.log(chalk7.bold("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2388
+ if (failedCount > 0 && result.failedEntities) {
2389
+ console.log();
2390
+ console.log(
2391
+ chalk14.red(
2392
+ `${failedCount} entity sync(s) failed. A checkpoint was saved \u2014 re-run sync to resume.`
2393
+ )
2394
+ );
2395
+ for (const f of result.failedEntities) {
2396
+ console.log(chalk14.dim(` ${f.entityId}: ${f.error}`));
2397
+ }
2398
+ }
730
2399
  console.log();
731
2400
  printSuccess("Sync complete.");
732
2401
  }
733
2402
 
734
2403
  // src/commands/validate.ts
735
- import { relative } from "path";
736
- import { parseTree, resolveRefs, validateTree } from "@gitpm/core";
737
- import chalk8 from "chalk";
738
- import { Command as Command6 } from "commander";
739
- var validateCommand = new Command6("validate").description("Validate the .meta/ project tree").action(async (_opts, cmd) => {
2404
+ import { relative as relative9 } from "path";
2405
+ import { parseTree as parseTree10, resolveRefs as resolveRefs7, validateTree } from "@gitpm/core";
2406
+ import chalk15 from "chalk";
2407
+ import { Command as Command19 } from "commander";
2408
+ var validateCommand = new Command19("validate").description("Validate the .meta/ project tree").action(async (_opts, cmd) => {
740
2409
  const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
741
- const parseResult = await parseTree(metaDir);
2410
+ const parseResult = await parseTree10(metaDir);
742
2411
  if (!parseResult.ok) {
743
2412
  printError(parseResult.error.message);
744
2413
  process.exit(1);
@@ -746,28 +2415,28 @@ var validateCommand = new Command6("validate").description("Validate the .meta/
746
2415
  const tree = parseResult.value;
747
2416
  if (tree.errors.length > 0) {
748
2417
  for (const err of tree.errors) {
749
- const rel = relative(process.cwd(), err.filePath);
750
- console.log(chalk8.red(` \u2717 ${rel}: ${err.message}`));
2418
+ const rel = relative9(process.cwd(), err.filePath);
2419
+ console.log(chalk15.red(` \u2717 ${rel}: ${err.message}`));
751
2420
  }
752
2421
  }
753
- const resolveResult = resolveRefs(tree);
2422
+ const resolveResult = resolveRefs7(tree);
754
2423
  if (!resolveResult.ok) {
755
2424
  printError(resolveResult.error.message);
756
2425
  process.exit(1);
757
2426
  }
758
2427
  const result = validateTree(resolveResult.value);
759
2428
  for (const err of result.errors) {
760
- const rel = err.filePath ? relative(process.cwd(), err.filePath) : "";
2429
+ const rel = err.filePath ? relative9(process.cwd(), err.filePath) : "";
761
2430
  const location = rel ? `${rel}: ` : "";
762
2431
  const id = err.entityId ? `[${err.entityId}] ` : "";
763
- console.log(chalk8.red(` \u2717 ${location}${id}${err.code}: ${err.message}`));
2432
+ console.log(chalk15.red(` \u2717 ${location}${id}${err.code}: ${err.message}`));
764
2433
  }
765
2434
  for (const warn of result.warnings) {
766
- const rel = warn.filePath ? relative(process.cwd(), warn.filePath) : "";
2435
+ const rel = warn.filePath ? relative9(process.cwd(), warn.filePath) : "";
767
2436
  const location = rel ? `${rel}: ` : "";
768
2437
  const id = warn.entityId ? `[${warn.entityId}] ` : "";
769
2438
  console.log(
770
- chalk8.yellow(` \u26A0 ${location}${id}${warn.code}: ${warn.message}`)
2439
+ chalk15.yellow(` \u26A0 ${location}${id}${warn.code}: ${warn.message}`)
771
2440
  );
772
2441
  }
773
2442
  if (!result.valid) {
@@ -786,16 +2455,29 @@ var validateCommand = new Command6("validate").description("Validate the .meta/
786
2455
  });
787
2456
 
788
2457
  // src/index.ts
789
- var __dirname = dirname(fileURLToPath(import.meta.url));
2458
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
790
2459
  var pkg = JSON.parse(
791
- readFileSync(join2(__dirname, "..", "package.json"), "utf-8")
2460
+ readFileSync(join3(__dirname, "..", "package.json"), "utf-8")
792
2461
  );
793
- var program = new Command7();
2462
+ var program = new Command20();
794
2463
  program.name("gitpm").description("Git-native project management").version(pkg.version).option("--meta-dir <path>", "Path to .meta directory", ".meta").option("--token <token>", "Personal access token (GitHub or GitLab)");
795
2464
  program.addCommand(initCommand);
796
2465
  program.addCommand(validateCommand);
2466
+ program.addCommand(qualityCommand);
2467
+ program.addCommand(nextCommand);
2468
+ program.addCommand(queryCommand);
2469
+ program.addCommand(showCommand);
2470
+ program.addCommand(setCommand);
2471
+ program.addCommand(createCommand);
2472
+ program.addCommand(moveCommand);
2473
+ program.addCommand(commitCommand);
797
2474
  program.addCommand(importCommand);
798
2475
  program.addCommand(pushCommand);
799
2476
  program.addCommand(pullCommand);
800
2477
  program.addCommand(syncCommand);
2478
+ program.addCommand(statusCommand);
2479
+ program.addCommand(sprintCommand);
2480
+ program.addCommand(auditCommand);
2481
+ program.addCommand(archiveCommand);
2482
+ program.addCommand(contextCommand);
801
2483
  program.parse();