@gitpm/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname, join as join2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { Command as Command7 } from "commander";
8
+
9
+ // src/commands/import.ts
10
+ import { importFromGitHub } from "@gitpm/sync-github";
11
+ import { importFromGitLab } from "@gitpm/sync-gitlab";
12
+ import chalk2 from "chalk";
13
+ 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
+
35
+ // src/utils/config.ts
36
+ import { resolve } from "path";
37
+ function resolveMetaDir(metaDirOption) {
38
+ return resolve(process.cwd(), metaDirOption);
39
+ }
40
+
41
+ // src/utils/output.ts
42
+ import chalk from "chalk";
43
+ function printSuccess(msg) {
44
+ console.log(chalk.green(`\u2713 ${msg}`));
45
+ }
46
+ function printError(msg) {
47
+ console.error(chalk.red(`\u2717 ${msg}`));
48
+ }
49
+ function printWarning(msg) {
50
+ console.warn(chalk.yellow(`\u26A0 ${msg}`));
51
+ }
52
+
53
+ // src/commands/import.ts
54
+ var VALID_LINK_STRATEGIES = [
55
+ "body-refs",
56
+ "sub-issues",
57
+ "milestone",
58
+ "labels",
59
+ "all"
60
+ ];
61
+ var VALID_GITLAB_LINK_STRATEGIES = [
62
+ "body-refs",
63
+ "native-epics",
64
+ "milestone",
65
+ "labels",
66
+ "all"
67
+ ];
68
+ var importCommand = new Command("import").description("Import project data from GitHub or GitLab into .meta/").option(
69
+ "--source <source>",
70
+ "Source platform: github or gitlab (default: github)",
71
+ "github"
72
+ ).option("--repo <owner/repo>", "GitHub repository (owner/repo)").option(
73
+ "--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(
76
+ "--link-strategy <strategy>",
77
+ "Epic-story linkage strategy (default: all)"
78
+ ).action(async (opts, cmd) => {
79
+ 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".`);
87
+ process.exit(1);
88
+ }
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".');
94
+ process.exit(1);
95
+ }
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.");
101
+ process.exit(1);
102
+ }
103
+ const linkStrategy = opts.linkStrategy;
104
+ if (linkStrategy && !VALID_LINK_STRATEGIES.includes(
105
+ linkStrategy
106
+ )) {
107
+ printError(
108
+ `Invalid link strategy "${linkStrategy}". Must be one of: ${VALID_LINK_STRATEGIES.join(", ")}`
109
+ );
110
+ process.exit(1);
111
+ }
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,
117
+ metaDir,
118
+ linkStrategy
119
+ });
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
+ );
148
+ process.exit(1);
149
+ }
150
+ const spinner = ora("Importing from GitLab...").start();
151
+ const result = await importFromGitLab({
152
+ token,
153
+ project,
154
+ baseUrl: opts.baseUrl,
155
+ metaDir,
156
+ linkStrategy
157
+ });
158
+ if (!result.ok) {
159
+ spinner.fail("Import failed.");
160
+ printError(result.error.message);
161
+ process.exit(1);
162
+ }
163
+ spinner.succeed("Import complete.");
164
+ printImportSummary(result.value);
165
+ }
166
+ function printImportSummary(value) {
167
+ const { milestones, epics, stories, totalFiles } = value;
168
+ console.log();
169
+ console.log(chalk2.bold("Summary:"));
170
+ console.log(` Milestones: ${milestones}`);
171
+ console.log(` Epics: ${epics}`);
172
+ console.log(` Stories: ${stories}`);
173
+ console.log(` Files: ${totalFiles}`);
174
+ console.log();
175
+ printSuccess(
176
+ `Imported ${milestones + epics + stories} entities (${totalFiles} files).`
177
+ );
178
+ console.log(chalk2.dim("Run `gitpm validate` to verify the imported tree."));
179
+ }
180
+
181
+ // src/commands/init.ts
182
+ import { readdir } from "fs/promises";
183
+ import { join } from "path";
184
+ import { scaffoldMeta } from "@gitpm/core";
185
+ import { input } from "@inquirer/prompts";
186
+ import chalk3 from "chalk";
187
+ import { Command as Command2 } from "commander";
188
+ async function collectFiles(dir, prefix = "") {
189
+ const entries = await readdir(dir, { withFileTypes: true });
190
+ const results = [];
191
+ for (const entry of entries) {
192
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
193
+ if (entry.isDirectory()) {
194
+ results.push(rel);
195
+ const children = await collectFiles(join(dir, entry.name), rel);
196
+ results.push(...children);
197
+ } else {
198
+ results.push(rel);
199
+ }
200
+ }
201
+ return results;
202
+ }
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) => {
204
+ 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
+ });
211
+ }
212
+ const result = await scaffoldMeta(metaDir, projectName);
213
+ if (!result.ok) {
214
+ printError(result.error.message);
215
+ process.exit(1);
216
+ }
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/`);
230
+ });
231
+
232
+ // 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";
243
+ import ora2 from "ora";
244
+
245
+ // src/utils/conflict-ui.ts
246
+ import { select } from "@inquirer/prompts";
247
+ import chalk4 from "chalk";
248
+ async function promptConflictResolution(conflicts) {
249
+ const resolutions = [];
250
+ for (const conflict of conflicts) {
251
+ 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"));
253
+ console.log(
254
+ `Entity: ${chalk4.bold(`"${conflict.entityTitle}"`)} (${conflict.entityId})`
255
+ );
256
+ console.log(`Type: ${conflict.entityType}`);
257
+ console.log(`Field: ${conflict.field}`);
258
+ 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"));
262
+ const pick = await select({
263
+ message: "How do you want to resolve this conflict?",
264
+ choices: [
265
+ { name: "[l] Keep local", value: "local" },
266
+ { name: "[r] Keep remote", value: "remote" },
267
+ { name: "[s] Skip (leave unresolved)", value: "skip" }
268
+ ]
269
+ });
270
+ if (pick !== "skip") {
271
+ resolutions.push({
272
+ entityId: conflict.entityId,
273
+ field: conflict.field,
274
+ pick
275
+ });
276
+ }
277
+ }
278
+ return resolutions;
279
+ }
280
+
281
+ // 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(
283
+ "--strategy <strategy>",
284
+ "Conflict resolution strategy (local-wins, remote-wins, ask)",
285
+ "remote-wins"
286
+ ).action(async (opts, cmd) => {
287
+ 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 {
296
+ printError(
297
+ "No sync config found. Run `gitpm import` first to set up sync."
298
+ );
299
+ process.exit(1);
300
+ }
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.");
308
+ process.exit(1);
309
+ }
310
+ const spinner = ora2("Pulling from GitHub...").start();
311
+ const result = await syncWithGitHub({
312
+ token,
313
+ repo,
314
+ metaDir,
315
+ strategy: strategy === "ask" ? "ask" : strategy
316
+ });
317
+ if (!result.ok) {
318
+ spinner.fail("Pull failed.");
319
+ printError(result.error.message);
320
+ process.exit(1);
321
+ }
322
+ spinner.succeed("Pull complete.");
323
+ 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."
330
+ );
331
+ process.exit(1);
332
+ }
333
+ const spinner = ora2("Pulling from GitLab...").start();
334
+ const result = await syncWithGitLab({
335
+ token,
336
+ project: config.project,
337
+ projectId: config.project_id,
338
+ baseUrl: config.base_url,
339
+ metaDir,
340
+ strategy: strategy === "ask" ? "ask" : strategy
341
+ });
342
+ if (!result.ok) {
343
+ spinner.fail("Pull failed.");
344
+ printError(result.error.message);
345
+ process.exit(1);
346
+ }
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();
354
+ console.log(
355
+ chalk5.yellow(`${conflicts.length} conflict(s) require resolution:`)
356
+ );
357
+ const resolutions = await promptConflictResolution(
358
+ conflicts
359
+ );
360
+ console.log(
361
+ chalk5.dim(
362
+ `Resolved ${resolutions.length}, skipped ${conflicts.length - resolutions.length}`
363
+ )
364
+ );
365
+ }
366
+ 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) => {
389
+ 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
+ );
400
+ process.exit(1);
401
+ }
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.");
409
+ process.exit(1);
410
+ }
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);
433
+ process.exit(1);
434
+ }
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;
440
+ }
441
+ if (!opts.yes) {
442
+ const confirmed = await confirm({
443
+ message: "Push these changes to GitHub?",
444
+ default: false
445
+ });
446
+ if (!confirmed) {
447
+ console.log(chalk6.dim("Push cancelled."));
448
+ return;
449
+ }
450
+ }
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;
460
+ console.log(
461
+ ` Created ${created.milestones + created.issues}, Updated ${updated.milestones + updated.issues}`
462
+ );
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."
469
+ );
470
+ process.exit(1);
471
+ }
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);
486
+ }
487
+ spinner.succeed("Dry run complete.");
488
+ printPreview(result2.value);
489
+ return;
490
+ }
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);
503
+ process.exit(1);
504
+ }
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;
510
+ }
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
+ }
520
+ }
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);
534
+ }
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
+ }
551
+
552
+ // 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";
564
+ 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(
566
+ "--strategy <strategy>",
567
+ "Conflict resolution strategy (local-wins, remote-wins, ask)",
568
+ "ask"
569
+ ).option("--dry-run", "Preview changes without syncing").option("--yes", "Skip confirmation prompt").action(async (opts, cmd) => {
570
+ 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 {
579
+ printError(
580
+ "No sync config found. Run `gitpm import` first to set up sync."
581
+ );
582
+ process.exit(1);
583
+ }
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
+ }
593
+ if (opts.dryRun) {
594
+ const spinner2 = ora4("Calculating sync changes...").start();
595
+ const result2 = await syncWithGitHub2({
596
+ token,
597
+ repo,
598
+ metaDir,
599
+ strategy,
600
+ dryRun: true
601
+ });
602
+ if (!result2.ok) {
603
+ spinner2.fail("Dry run failed.");
604
+ printError(result2.error.message);
605
+ process.exit(1);
606
+ }
607
+ spinner2.succeed("Dry run complete.");
608
+ printSyncSummary(result2.value, "GitHub");
609
+ return;
610
+ }
611
+ if (!opts.yes) {
612
+ const confirmed = await confirm2({
613
+ message: "Run bidirectional sync with GitHub?",
614
+ default: true
615
+ });
616
+ if (!confirmed) {
617
+ console.log(chalk7.dim("Sync cancelled."));
618
+ return;
619
+ }
620
+ }
621
+ const spinner = ora4("Syncing with GitHub...").start();
622
+ const result = await syncWithGitHub2({
623
+ token,
624
+ repo,
625
+ metaDir,
626
+ strategy: strategy === "ask" ? "ask" : strategy
627
+ });
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
+ );
643
+ process.exit(1);
644
+ }
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({
677
+ token,
678
+ project: config.project,
679
+ projectId: config.project_id,
680
+ baseUrl: config.base_url,
681
+ metaDir,
682
+ strategy: strategy === "ask" ? "ask" : strategy
683
+ });
684
+ if (!result.ok) {
685
+ spinner.fail("Sync failed.");
686
+ printError(result.error.message);
687
+ process.exit(1);
688
+ }
689
+ spinner.succeed("Sync complete.");
690
+ await handleConflicts(result.value, strategy);
691
+ printSyncSummary(result.value, "GitLab");
692
+ }
693
+ async function handleConflicts(result, strategy) {
694
+ const { conflicts } = result;
695
+ if (strategy === "ask" && conflicts.length > 0) {
696
+ console.log();
697
+ console.log(
698
+ chalk7.yellow(`${conflicts.length} conflict(s) require resolution:`)
699
+ );
700
+ const resolutions = await promptConflictResolution(
701
+ conflicts
702
+ );
703
+ console.log(
704
+ chalk7.dim(
705
+ `Resolved ${resolutions.length}, skipped ${conflicts.length - resolutions.length}`
706
+ )
707
+ );
708
+ }
709
+ }
710
+ function printSyncSummary(result, platform) {
711
+ const pushedTotal = result.pushed.milestones + result.pushed.issues;
712
+ const pulledTotal = result.pulled.milestones + result.pulled.issues;
713
+ 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
+ console.log(
718
+ `${chalk7.bold("\u2502")} Pushed to ${platform.padEnd(6)} \u2502 ${String(pushedTotal).padEnd(17)} ${chalk7.bold("\u2502")}`
719
+ );
720
+ console.log(
721
+ `${chalk7.bold("\u2502")} Pulled to local \u2502 ${String(pulledTotal).padEnd(17)} ${chalk7.bold("\u2502")}`
722
+ );
723
+ console.log(
724
+ `${chalk7.bold("\u2502")} Conflicts \u2502 ${String(`${result.resolved} resolved`).padEnd(17)} ${chalk7.bold("\u2502")}`
725
+ );
726
+ console.log(
727
+ `${chalk7.bold("\u2502")} Errors \u2502 ${String(result.skipped).padEnd(17)} ${chalk7.bold("\u2502")}`
728
+ );
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"));
730
+ console.log();
731
+ printSuccess("Sync complete.");
732
+ }
733
+
734
+ // 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) => {
740
+ const metaDir = resolveMetaDir(cmd.optsWithGlobals().metaDir);
741
+ const parseResult = await parseTree(metaDir);
742
+ if (!parseResult.ok) {
743
+ printError(parseResult.error.message);
744
+ process.exit(1);
745
+ }
746
+ const tree = parseResult.value;
747
+ if (tree.errors.length > 0) {
748
+ for (const err of tree.errors) {
749
+ const rel = relative(process.cwd(), err.filePath);
750
+ console.log(chalk8.red(` \u2717 ${rel}: ${err.message}`));
751
+ }
752
+ }
753
+ const resolveResult = resolveRefs(tree);
754
+ if (!resolveResult.ok) {
755
+ printError(resolveResult.error.message);
756
+ process.exit(1);
757
+ }
758
+ const result = validateTree(resolveResult.value);
759
+ for (const err of result.errors) {
760
+ const rel = err.filePath ? relative(process.cwd(), err.filePath) : "";
761
+ const location = rel ? `${rel}: ` : "";
762
+ const id = err.entityId ? `[${err.entityId}] ` : "";
763
+ console.log(chalk8.red(` \u2717 ${location}${id}${err.code}: ${err.message}`));
764
+ }
765
+ for (const warn of result.warnings) {
766
+ const rel = warn.filePath ? relative(process.cwd(), warn.filePath) : "";
767
+ const location = rel ? `${rel}: ` : "";
768
+ const id = warn.entityId ? `[${warn.entityId}] ` : "";
769
+ console.log(
770
+ chalk8.yellow(` \u26A0 ${location}${id}${warn.code}: ${warn.message}`)
771
+ );
772
+ }
773
+ if (!result.valid) {
774
+ console.log();
775
+ printError(
776
+ `Validation failed with ${result.errors.length} error(s) and ${result.warnings.length} warning(s)`
777
+ );
778
+ process.exit(1);
779
+ }
780
+ const entityCount = tree.stories.length + tree.epics.length + tree.milestones.length + tree.roadmaps.length + tree.prds.length;
781
+ if (result.warnings.length > 0) {
782
+ console.log();
783
+ printWarning(`${result.warnings.length} warning(s)`);
784
+ }
785
+ printSuccess(`.meta/ tree is valid (${entityCount} entities)`);
786
+ });
787
+
788
+ // src/index.ts
789
+ var __dirname = dirname(fileURLToPath(import.meta.url));
790
+ var pkg = JSON.parse(
791
+ readFileSync(join2(__dirname, "..", "package.json"), "utf-8")
792
+ );
793
+ var program = new Command7();
794
+ 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
+ program.addCommand(initCommand);
796
+ program.addCommand(validateCommand);
797
+ program.addCommand(importCommand);
798
+ program.addCommand(pushCommand);
799
+ program.addCommand(pullCommand);
800
+ program.addCommand(syncCommand);
801
+ program.parse();