@specship/specship-win32-arm64 0.19.1 → 0.20.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.
Files changed (62) hide show
  1. package/lib/dist/bin/specship.js +376 -2
  2. package/lib/dist/bin/specship.js.map +1 -1
  3. package/lib/dist/commands/specship/spec.md +19 -0
  4. package/lib/dist/db/spec-queries.d.ts.map +1 -1
  5. package/lib/dist/db/spec-queries.js +18 -3
  6. package/lib/dist/db/spec-queries.js.map +1 -1
  7. package/lib/dist/index.d.ts +13 -0
  8. package/lib/dist/index.d.ts.map +1 -1
  9. package/lib/dist/index.js +16 -0
  10. package/lib/dist/index.js.map +1 -1
  11. package/lib/dist/jira/client.d.ts +61 -0
  12. package/lib/dist/jira/client.d.ts.map +1 -1
  13. package/lib/dist/jira/client.js +120 -0
  14. package/lib/dist/jira/client.js.map +1 -1
  15. package/lib/dist/jira/config.d.ts.map +1 -1
  16. package/lib/dist/jira/config.js +7 -1
  17. package/lib/dist/jira/config.js.map +1 -1
  18. package/lib/dist/jira/publish.d.ts +201 -0
  19. package/lib/dist/jira/publish.d.ts.map +1 -0
  20. package/lib/dist/jira/publish.js +0 -0
  21. package/lib/dist/jira/publish.js.map +1 -0
  22. package/lib/dist/jira/spec-writer.d.ts +7 -0
  23. package/lib/dist/jira/spec-writer.d.ts.map +1 -1
  24. package/lib/dist/jira/spec-writer.js +15 -0
  25. package/lib/dist/jira/spec-writer.js.map +1 -1
  26. package/lib/dist/jira/types.d.ts +13 -0
  27. package/lib/dist/jira/types.d.ts.map +1 -1
  28. package/lib/dist/jira/types.js.map +1 -1
  29. package/lib/dist/mcp/jira-tools.d.ts +85 -0
  30. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  31. package/lib/dist/mcp/jira-tools.js +308 -2
  32. package/lib/dist/mcp/jira-tools.js.map +1 -1
  33. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  34. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  35. package/lib/dist/mcp/server-instructions.js +2 -1
  36. package/lib/dist/mcp/server-instructions.js.map +1 -1
  37. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  38. package/lib/dist/mcp/spec-tools.js +92 -1
  39. package/lib/dist/mcp/spec-tools.js.map +1 -1
  40. package/lib/dist/mcp/tools.d.ts.map +1 -1
  41. package/lib/dist/mcp/tools.js +14 -0
  42. package/lib/dist/mcp/tools.js.map +1 -1
  43. package/lib/dist/reflect/index.d.ts +1 -1
  44. package/lib/dist/reflect/index.d.ts.map +1 -1
  45. package/lib/dist/reflect/index.js +2 -1
  46. package/lib/dist/reflect/index.js.map +1 -1
  47. package/lib/dist/reflect/sweep.d.ts +14 -0
  48. package/lib/dist/reflect/sweep.d.ts.map +1 -1
  49. package/lib/dist/reflect/sweep.js +31 -0
  50. package/lib/dist/reflect/sweep.js.map +1 -1
  51. package/lib/dist/resolution/spec-link-resolver.d.ts +12 -2
  52. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  53. package/lib/dist/resolution/spec-link-resolver.js +20 -5
  54. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  55. package/lib/dist/types.d.ts +8 -0
  56. package/lib/dist/types.d.ts.map +1 -1
  57. package/lib/dist/types.js +12 -1
  58. package/lib/dist/types.js.map +1 -1
  59. package/lib/dist/workflows/defaults/spec-implement.yaml +6 -0
  60. package/lib/node_modules/.package-lock.json +1 -1
  61. package/lib/package.json +1 -1
  62. package/package.json +1 -1
@@ -807,6 +807,41 @@ function main() {
807
807
  `re-assert with specship_link_assert, or run /specship:check fix ${t.specId}`);
808
808
  }
809
809
  };
810
+ // JIRA push (REQ-JIRAPUB-006): a genuine drift transition on a JIRA-backed
811
+ // spec is commented onto its issue — best-effort, transition events only
812
+ // (so an ongoing drift never repeats a comment), never blocks the sync.
813
+ const pushJiraDriftComments = async (cg, result) => {
814
+ const transitions = result.driftedTransitions ?? [];
815
+ if (transitions.length === 0)
816
+ return;
817
+ try {
818
+ const { loadJiraConfig, resolveJiraCredentials } = await Promise.resolve().then(() => __importStar(require('../jira/config')));
819
+ if (!loadJiraConfig())
820
+ return; // not configured → no JIRA calls at all
821
+ const { readSpecJiraKey } = await Promise.resolve().then(() => __importStar(require('../jira/spec-writer')));
822
+ const { JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira/client')));
823
+ const { commentDriftTransitionsOnJira } = await Promise.resolve().then(() => __importStar(require('../jira/publish')));
824
+ const notes = await commentDriftTransitionsOnJira({
825
+ transitions,
826
+ specPathFor: (specId) => {
827
+ const rootId = specId.replace(/\.A\d+$/, '');
828
+ const spec = cg.getSpecQueries().getSpecById(rootId);
829
+ if (!spec)
830
+ return null;
831
+ return path.isAbsolute(spec.sourcePath)
832
+ ? spec.sourcePath
833
+ : path.join(projectPath, spec.sourcePath);
834
+ },
835
+ readKey: readSpecJiraKey,
836
+ makeClient: () => new JiraClient(resolveJiraCredentials()),
837
+ });
838
+ for (const note of notes)
839
+ console.log(`JIRA: ${note}`);
840
+ }
841
+ catch {
842
+ /* a JIRA fault must never fail a sync */
843
+ }
844
+ };
810
845
  // One-line drift-queue summary, only when non-empty (REQ-DRIFT-PUSH-002).
811
846
  const pushDriftSummary = (cg) => {
812
847
  if (!options.driftSummary)
@@ -828,6 +863,7 @@ function main() {
828
863
  if (options.quiet) {
829
864
  const result = await cg.sync();
830
865
  pushDriftNotices(result);
866
+ await pushJiraDriftComments(cg, result);
831
867
  pushDriftSummary(cg);
832
868
  cg.destroy();
833
869
  return;
@@ -856,6 +892,7 @@ function main() {
856
892
  clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
857
893
  }
858
894
  pushDriftNotices(result);
895
+ await pushJiraDriftComments(cg, result);
859
896
  pushDriftSummary(cg);
860
897
  clack.outro('Done');
861
898
  cg.destroy();
@@ -1356,6 +1393,195 @@ function main() {
1356
1393
  process.exit(1);
1357
1394
  }
1358
1395
  });
1396
+ /**
1397
+ * specship memory — capture lessons to memory and review/manage stored items
1398
+ * (MEMLESSON-DOC). `capture` turns a stated mistake into a human-gated
1399
+ * memory_rule proposal (REQ-MEMLESSON-001); nothing is written until it is
1400
+ * applied (dashboard Improvements / `specship reflect`).
1401
+ */
1402
+ const memory = program
1403
+ .command('memory')
1404
+ .description('Capture lessons to memory and review/manage stored memory items.');
1405
+ memory
1406
+ .command('capture')
1407
+ .description('Capture a lesson/anti-pattern as a human-gated memory rule (lesson text on stdin).')
1408
+ .requiredOption('--title <title>', 'short title for the lesson')
1409
+ .option('--target <target>', 'where apply would write it: "memory" (portable ~/.claude note, default) or "claude-md" (project CLAUDE.md)', 'memory')
1410
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1411
+ .option('-j, --json', 'output the created proposal as JSON')
1412
+ .action(async (options) => {
1413
+ const projectPath = resolveProjectPath(options.path);
1414
+ const target = options.target === 'claude-md' ? 'claude-md' : 'memory';
1415
+ try {
1416
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1417
+ error(`SpecShip not initialized in ${projectPath}`);
1418
+ process.exit(1);
1419
+ }
1420
+ // Lesson content on stdin so a multi-line "mistake + rule to avoid it"
1421
+ // survives shell quoting (mirrors `specship reflect --capture`).
1422
+ const chunks = [];
1423
+ for await (const c of process.stdin)
1424
+ chunks.push(c);
1425
+ const content = Buffer.concat(chunks).toString('utf-8').trim();
1426
+ if (!content) {
1427
+ error('memory capture expects the lesson on stdin (pipe or heredoc): the mistake + the rule to avoid repeating it.');
1428
+ process.exit(1);
1429
+ }
1430
+ const { SpecShip } = await loadSpecShip();
1431
+ const cg = await SpecShip.open(projectPath);
1432
+ const p = cg.reflectCaptureLesson({ title: options.title, content, target });
1433
+ if (options.json) {
1434
+ // eslint-disable-next-line no-console
1435
+ console.log(JSON.stringify(p, null, 2));
1436
+ }
1437
+ else {
1438
+ info(`Captured lesson as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
1439
+ info(`Target: ${p.targetKind === 'claude_md' ? 'project CLAUDE.md' : 'portable ~/.claude memory note'} — nothing written yet.`);
1440
+ info('Review + apply from the dashboard Improvements page (preview-diff → confirm), or `specship reflect`.');
1441
+ }
1442
+ cg.destroy();
1443
+ }
1444
+ catch (err) {
1445
+ error(`memory capture failed: ${err instanceof Error ? err.message : String(err)}`);
1446
+ process.exit(1);
1447
+ }
1448
+ });
1449
+ // Body text of a memory_rule proposal's payload (the note or the CLAUDE.md block).
1450
+ function memoryRuleBody(p) {
1451
+ if (p.payload.kind === 'memory_note')
1452
+ return p.payload.note ?? '';
1453
+ if (p.payload.kind === 'claude_md')
1454
+ return p.payload.block ?? '';
1455
+ return '';
1456
+ }
1457
+ memory
1458
+ .command('list')
1459
+ .description('List the memory rules SpecShip has applied (reflect-managed store).')
1460
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1461
+ .option('-j, --json', 'output as JSON')
1462
+ .action(async (options) => {
1463
+ const projectPath = resolveProjectPath(options.path);
1464
+ try {
1465
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1466
+ error(`SpecShip not initialized in ${projectPath}`);
1467
+ process.exit(1);
1468
+ }
1469
+ const { SpecShip } = await loadSpecShip();
1470
+ const cg = await SpecShip.open(projectPath);
1471
+ const items = cg.reflectList('applied').filter((p) => p.type === 'memory_rule');
1472
+ if (options.json) {
1473
+ // eslint-disable-next-line no-console
1474
+ console.log(JSON.stringify(items, null, 2));
1475
+ cg.destroy();
1476
+ return;
1477
+ }
1478
+ if (items.length === 0) {
1479
+ info('No memory items yet — capture one with `specship memory capture`.');
1480
+ cg.destroy();
1481
+ return;
1482
+ }
1483
+ for (const p of items) {
1484
+ const target = p.targetKind === 'claude_md' ? 'CLAUDE.md' : 'memory note';
1485
+ success(`${p.contentHash.slice(0, 12)} [${target}] ${p.title}`);
1486
+ }
1487
+ cg.destroy();
1488
+ }
1489
+ catch (err) {
1490
+ error(`memory list failed: ${err instanceof Error ? err.message : String(err)}`);
1491
+ process.exit(1);
1492
+ }
1493
+ });
1494
+ memory
1495
+ .command('remove <id>')
1496
+ .description('Remove a SpecShip-managed memory rule by id (previewed; pass --yes to write).')
1497
+ .option('--yes', 'actually remove it (without this it is a dry-run preview)')
1498
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1499
+ .action(async (id, options) => {
1500
+ const projectPath = resolveProjectPath(options.path);
1501
+ try {
1502
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1503
+ error(`SpecShip not initialized in ${projectPath}`);
1504
+ process.exit(1);
1505
+ }
1506
+ const { SpecShip } = await loadSpecShip();
1507
+ const cg = await SpecShip.open(projectPath);
1508
+ const p = cg.reflectGet(id) ??
1509
+ cg.reflectList().find((x) => x.type === 'memory_rule' && x.contentHash.startsWith(id));
1510
+ if (!p || p.type !== 'memory_rule') {
1511
+ error(`No SpecShip-managed memory rule found for "${id}".`);
1512
+ cg.destroy();
1513
+ process.exit(1);
1514
+ }
1515
+ info(`Will remove from ${p.targetPath}:`);
1516
+ // eslint-disable-next-line no-console
1517
+ console.log(memoryRuleBody(p));
1518
+ if (!options.yes) {
1519
+ info('Dry-run — nothing changed. Re-run with --yes to remove.');
1520
+ cg.destroy();
1521
+ return;
1522
+ }
1523
+ const outcome = cg.reflectUndo(p.contentHash);
1524
+ if (outcome === 'undone')
1525
+ success(`Removed ${p.contentHash.slice(0, 12)}: ${p.title}`);
1526
+ else
1527
+ info(`Nothing on disk to remove for ${p.contentHash.slice(0, 12)} (already gone).`);
1528
+ cg.destroy();
1529
+ }
1530
+ catch (err) {
1531
+ error(`memory remove failed: ${err instanceof Error ? err.message : String(err)}`);
1532
+ process.exit(1);
1533
+ }
1534
+ });
1535
+ memory
1536
+ .command('edit <id>')
1537
+ .description("Replace a memory rule's body with new content on stdin (previewed; pass --yes to write).")
1538
+ .option('--yes', 'actually apply the update (without this it is a dry-run before→after preview)')
1539
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1540
+ .action(async (id, options) => {
1541
+ const projectPath = resolveProjectPath(options.path);
1542
+ try {
1543
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1544
+ error(`SpecShip not initialized in ${projectPath}`);
1545
+ process.exit(1);
1546
+ }
1547
+ const chunks = [];
1548
+ for await (const c of process.stdin)
1549
+ chunks.push(c);
1550
+ const content = Buffer.concat(chunks).toString('utf-8').trim();
1551
+ if (!content) {
1552
+ error('memory edit expects the new body on stdin (pipe or heredoc).');
1553
+ process.exit(1);
1554
+ }
1555
+ const { SpecShip } = await loadSpecShip();
1556
+ const cg = await SpecShip.open(projectPath);
1557
+ const old = cg.reflectGet(id) ??
1558
+ cg.reflectList().find((x) => x.type === 'memory_rule' && x.contentHash.startsWith(id));
1559
+ if (!old || old.type !== 'memory_rule') {
1560
+ error(`No SpecShip-managed memory rule found for "${id}".`);
1561
+ cg.destroy();
1562
+ process.exit(1);
1563
+ }
1564
+ const title = old.title.replace(/^Lesson:\s*/, '');
1565
+ const target = old.targetKind === 'claude_md' ? 'claude-md' : 'memory';
1566
+ const next = cg.reflectCaptureLesson({ title, content, target });
1567
+ info(`Update ${old.contentHash.slice(0, 12)} — before → after:`);
1568
+ // eslint-disable-next-line no-console
1569
+ console.log('--- before ---\n' + memoryRuleBody(old) + '\n--- after ---\n' + memoryRuleBody(next));
1570
+ if (!options.yes) {
1571
+ info('Dry-run — nothing changed. Re-run with --yes to apply the update.');
1572
+ cg.destroy();
1573
+ return;
1574
+ }
1575
+ cg.reflectUndo(old.contentHash);
1576
+ const outcome = cg.reflectApply(next.contentHash);
1577
+ success(`Updated: removed ${old.contentHash.slice(0, 12)}, applied ${next.contentHash.slice(0, 12)} (${outcome}).`);
1578
+ cg.destroy();
1579
+ }
1580
+ catch (err) {
1581
+ error(`memory edit failed: ${err instanceof Error ? err.message : String(err)}`);
1582
+ process.exit(1);
1583
+ }
1584
+ });
1359
1585
  /**
1360
1586
  * specship maintainability
1361
1587
  *
@@ -3098,6 +3324,7 @@ function main() {
3098
3324
  .option('--deployment <kind>', 'Deployment kind: "cloud" or "datacenter" (inferred if omitted)')
3099
3325
  .option('--ca-cert <pem>', 'Path to a PEM CA bundle for a corporate/self-signed certificate (Data Center)')
3100
3326
  .option('--insecure-tls', 'Disable TLS certificate verification for JIRA requests only (last resort)')
3327
+ .option('--project <key>', 'Default project for spec→JIRA publishing (interactive setup offers your accessible projects)')
3101
3328
  .action(async (opts) => {
3102
3329
  const { saveJiraConfig, resolveJiraCredentials, inferDeployment, jiraConfigPath, JiraClient, JiraAuthError, } = await Promise.resolve().then(() => __importStar(require('../jira')));
3103
3330
  const clack = await importESM('@clack/prompts');
@@ -3197,7 +3424,40 @@ function main() {
3197
3424
  // Probe the connection so the user knows immediately whether it works.
3198
3425
  const client = new JiraClient(resolveJiraCredentials());
3199
3426
  const result = await client.testConnection();
3200
- clack.outro(`Connected as ${result.displayName ?? 'unknown user'}.`);
3427
+ clack.log.success(`Connected as ${result.displayName ?? 'unknown user'}.`);
3428
+ // Publish project (REQ-JIRAPUB-009.A3): an explicit --project saves
3429
+ // directly; otherwise the interactive path offers the projects this
3430
+ // account can actually browse, so the user picks instead of typing.
3431
+ if (opts.project) {
3432
+ config.project = opts.project.trim().toUpperCase();
3433
+ saveJiraConfig(config);
3434
+ clack.log.success(`Default publish project: ${config.project}.`);
3435
+ }
3436
+ else if (!flagged) {
3437
+ try {
3438
+ const projects = await client.listProjects();
3439
+ if (projects.length > 0) {
3440
+ const NONE = '__none__';
3441
+ const chosen = await clack.select({
3442
+ message: 'Default project for spec→JIRA publishing (specship_jira_publish)',
3443
+ options: [
3444
+ ...projects.map((p) => ({ value: p.key, label: `${p.key} — ${p.name}` })),
3445
+ { value: NONE, label: 'Skip — choose per publish' },
3446
+ ],
3447
+ });
3448
+ if (!cancelled(chosen) && chosen !== NONE) {
3449
+ config.project = String(chosen);
3450
+ saveJiraConfig(config);
3451
+ clack.log.success(`Default publish project: ${config.project}.`);
3452
+ }
3453
+ }
3454
+ }
3455
+ catch {
3456
+ // Listing projects is a convenience — a fault here never fails
3457
+ // an otherwise-successful configure.
3458
+ }
3459
+ }
3460
+ clack.outro('JIRA is configured.');
3201
3461
  }
3202
3462
  catch (err) {
3203
3463
  // Messages from the jira module are already scrubbed of the secret.
@@ -3219,6 +3479,24 @@ function main() {
3219
3479
  const kind = creds.deployment === 'cloud' ? 'JIRA Cloud' : 'JIRA Data Center';
3220
3480
  // Never print the credential — only the resolved identity.
3221
3481
  success(`Connected to ${kind} as ${result.displayName ?? 'unknown user'}.`);
3482
+ // Validate the configured lifecycle transition names against a live
3483
+ // workflow so a name the workflow can't fire is visible now, not a
3484
+ // silent skip at completion time (REQ-JIRATRANS-002).
3485
+ const { validateConfiguredTransitions } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
3486
+ const v = await validateConfiguredTransitions(client, creds.transitions ?? {});
3487
+ if (!v.verified) {
3488
+ info('Configured transitions: could not verify (no accessible issue to sample).');
3489
+ }
3490
+ else {
3491
+ info(`Configured transitions (checked against ${v.sampleKey}; available from its ` +
3492
+ `current state: ${v.available.join(', ') || 'none'}):`);
3493
+ for (const c of v.checks) {
3494
+ if (c.found)
3495
+ info(` ✓ ${c.role} "${c.configured}"`);
3496
+ else
3497
+ warn(` ✗ ${c.role} "${c.configured}" — not offered from ${v.sampleKey}'s current state`);
3498
+ }
3499
+ }
3222
3500
  }
3223
3501
  catch (err) {
3224
3502
  const msg = err instanceof Error ? err.message : String(err);
@@ -3227,6 +3505,41 @@ function main() {
3227
3505
  process.exit(1);
3228
3506
  }
3229
3507
  });
3508
+ /**
3509
+ * specship jira transition — move a tracked issue to a target state, or list
3510
+ * the transitions it currently offers (REQ-JIRATRANS-001). Reuses the client's
3511
+ * skip-tolerant transition: a target the workflow doesn't offer is reported
3512
+ * with the available states and nothing is written.
3513
+ */
3514
+ jira
3515
+ .command('transition <key> [state]')
3516
+ .description('Transition a JIRA issue to <state>, or list its available transitions when <state> is omitted (or with --list).')
3517
+ .option('--list', 'list the available transitions instead of applying one')
3518
+ .action(async (key, state, options) => {
3519
+ const { resolveJiraCredentials, JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira')));
3520
+ try {
3521
+ const creds = resolveJiraCredentials();
3522
+ const client = new JiraClient(creds);
3523
+ if (options.list || !state) {
3524
+ const names = (await client.listTransitions(key)).map((t) => t.name);
3525
+ success(names.length
3526
+ ? `${key} can transition to: ${names.join(', ')}.`
3527
+ : `${key} has no available transitions from its current state.`);
3528
+ return;
3529
+ }
3530
+ const res = await client.transitionIssue(key, state);
3531
+ if ('transitioned' in res) {
3532
+ success(`Moved ${key} to "${res.transitioned}".`);
3533
+ }
3534
+ else {
3535
+ warn(`Did not transition ${key} — ${res.reason}.`);
3536
+ }
3537
+ }
3538
+ catch (err) {
3539
+ error(`JIRA transition failed: ${err instanceof Error ? err.message : String(err)}`);
3540
+ process.exit(1);
3541
+ }
3542
+ });
3230
3543
  /**
3231
3544
  * specship jira track — read-only tracking view (REQ-JIRA-008). Joins each
3232
3545
  * picked issue's SpecShip work-state (from its workflow run) with its LIVE JIRA
@@ -3247,7 +3560,7 @@ function main() {
3247
3560
  const { handleSpecshipJiraTrack } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
3248
3561
  const cg = await SpecShip.open(projectRoot);
3249
3562
  try {
3250
- const result = await handleSpecshipJiraTrack({ project: options.project }, { specQueries: cg.getSpecQueries() });
3563
+ const result = await handleSpecshipJiraTrack({ project: options.project }, { specQueries: cg.getSpecQueries(), projectRoot });
3251
3564
  const out = result.content.map((c) => c.text).join('\n');
3252
3565
  // eslint-disable-next-line no-console
3253
3566
  console.log(out);
@@ -3258,6 +3571,67 @@ function main() {
3258
3571
  cg.close();
3259
3572
  }
3260
3573
  });
3574
+ /**
3575
+ * specship jira release — stamp a released version onto issues
3576
+ * (REQ-JIRAPUB-007): ensure the project version exists, add it as fixVersion
3577
+ * on each issue, and add one shipped-in comment. Idempotent — a re-run
3578
+ * creates no duplicate version, fixVersion, or comment.
3579
+ */
3580
+ jira
3581
+ .command('release <version>')
3582
+ .description('Set <version> as fixVersion on JIRA issues (creating the project version if missing) with a shipped-in comment.')
3583
+ .option('--keys <keys>', 'comma-separated issue keys (e.g., "PROJ-1,PROJ-2"); default: every published spec\'s jira_issue key')
3584
+ .option('--project <key>', 'JIRA project key the version belongs to (default: the configured publish project)')
3585
+ .option('--path <projectRoot>', 'project root whose specs/ supplies the default keys (default: cwd)')
3586
+ .action(async (version, options) => {
3587
+ const { resolveJiraCredentials, JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira')));
3588
+ const { releaseIssues } = await Promise.resolve().then(() => __importStar(require('../jira/publish')));
3589
+ const { readSpecJiraKey } = await Promise.resolve().then(() => __importStar(require('../jira/spec-writer')));
3590
+ try {
3591
+ const creds = resolveJiraCredentials();
3592
+ const projectKey = options.project ?? creds.project;
3593
+ if (!projectKey) {
3594
+ error('No JIRA project configured. Pass --project, set SPECSHIP_JIRA_PROJECT, or add "project" to your jira config.');
3595
+ process.exit(1);
3596
+ }
3597
+ let keys = (options.keys ?? '')
3598
+ .split(',')
3599
+ .map((k) => k.trim())
3600
+ .filter(Boolean);
3601
+ if (keys.length === 0) {
3602
+ // Default: every spec under <root>/specs with a jira_issue key.
3603
+ const projectRoot = path.resolve(options.path ?? process.cwd());
3604
+ const specsDir = path.join(projectRoot, 'specs');
3605
+ try {
3606
+ for (const name of fs.readdirSync(specsDir)) {
3607
+ if (!name.toLowerCase().endsWith('.md'))
3608
+ continue;
3609
+ const key = readSpecJiraKey(path.join(specsDir, name));
3610
+ if (key)
3611
+ keys.push(key);
3612
+ }
3613
+ }
3614
+ catch {
3615
+ /* no specs dir → empty keys, handled below */
3616
+ }
3617
+ keys = [...new Set(keys)];
3618
+ }
3619
+ if (keys.length === 0) {
3620
+ error('No issue keys to release — pass --keys or publish specs first (specship_jira_publish).');
3621
+ process.exit(1);
3622
+ }
3623
+ const client = new JiraClient(creds);
3624
+ const result = await releaseIssues(client, projectKey, version, keys);
3625
+ const stamped = result.issues.filter((i) => i.fixVersionAdded).length;
3626
+ const commented = result.issues.filter((i) => i.commented).length;
3627
+ success(`Version ${version}${result.versionCreated ? ' created' : ' already existed'} in ${projectKey}; ` +
3628
+ `fixVersion added to ${stamped}/${keys.length} issue(s), shipped-in comment added to ${commented}.`);
3629
+ }
3630
+ catch (err) {
3631
+ error(`JIRA release failed: ${err instanceof Error ? err.message : String(err)}`);
3632
+ process.exit(1);
3633
+ }
3634
+ });
3261
3635
  // Parse and run
3262
3636
  program.parse();
3263
3637
  } // end main()