@specship/specship-win32-arm64 0.19.0 → 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.
- package/lib/dist/bin/specship.js +376 -2
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/spec.md +19 -0
- package/lib/dist/db/spec-queries.d.ts.map +1 -1
- package/lib/dist/db/spec-queries.js +18 -3
- package/lib/dist/db/spec-queries.js.map +1 -1
- package/lib/dist/index.d.ts +13 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +20 -1
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/jira/client.d.ts +61 -0
- package/lib/dist/jira/client.d.ts.map +1 -1
- package/lib/dist/jira/client.js +120 -0
- package/lib/dist/jira/client.js.map +1 -1
- package/lib/dist/jira/config.d.ts.map +1 -1
- package/lib/dist/jira/config.js +7 -1
- package/lib/dist/jira/config.js.map +1 -1
- package/lib/dist/jira/publish.d.ts +201 -0
- package/lib/dist/jira/publish.d.ts.map +1 -0
- package/lib/dist/jira/publish.js +0 -0
- package/lib/dist/jira/publish.js.map +1 -0
- package/lib/dist/jira/spec-writer.d.ts +7 -0
- package/lib/dist/jira/spec-writer.d.ts.map +1 -1
- package/lib/dist/jira/spec-writer.js +15 -0
- package/lib/dist/jira/spec-writer.js.map +1 -1
- package/lib/dist/jira/types.d.ts +13 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/mcp/jira-tools.d.ts +85 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +308 -2
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +1 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +2 -1
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +99 -3
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +14 -0
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/reflect/index.d.ts +1 -1
- package/lib/dist/reflect/index.d.ts.map +1 -1
- package/lib/dist/reflect/index.js +2 -1
- package/lib/dist/reflect/index.js.map +1 -1
- package/lib/dist/reflect/sweep.d.ts +14 -0
- package/lib/dist/reflect/sweep.d.ts.map +1 -1
- package/lib/dist/reflect/sweep.js +31 -0
- package/lib/dist/reflect/sweep.js.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +30 -2
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +85 -12
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/dist/types.d.ts +8 -0
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +12 -1
- package/lib/dist/types.js.map +1 -1
- package/lib/dist/workflows/defaults/spec-implement.yaml +6 -0
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
package/lib/dist/bin/specship.js
CHANGED
|
@@ -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.
|
|
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()
|