@anthropologies/claudestory 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/dist/cli.js +779 -15
- package/dist/index.d.ts +605 -1
- package/dist/index.js +528 -1
- package/dist/mcp.js +787 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -484,6 +484,284 @@ function formatHandoverContent(filename, content, format) {
|
|
|
484
484
|
}
|
|
485
485
|
return content;
|
|
486
486
|
}
|
|
487
|
+
function formatHandoverCreateResult(filename, format) {
|
|
488
|
+
if (format === "json") {
|
|
489
|
+
return JSON.stringify(successEnvelope({ filename }), null, 2);
|
|
490
|
+
}
|
|
491
|
+
return `Created handover: ${filename}`;
|
|
492
|
+
}
|
|
493
|
+
function formatSnapshotResult(result, format) {
|
|
494
|
+
if (format === "json") {
|
|
495
|
+
return JSON.stringify(successEnvelope(result), null, 2);
|
|
496
|
+
}
|
|
497
|
+
let line = `Snapshot saved: ${result.filename} (${result.retained} retained`;
|
|
498
|
+
if (result.pruned > 0) line += `, ${result.pruned} pruned`;
|
|
499
|
+
line += ")";
|
|
500
|
+
return line;
|
|
501
|
+
}
|
|
502
|
+
function formatRecap(recap, state, format) {
|
|
503
|
+
if (format === "json") {
|
|
504
|
+
return JSON.stringify(successEnvelope(recap), null, 2);
|
|
505
|
+
}
|
|
506
|
+
const lines = [];
|
|
507
|
+
if (!recap.snapshot) {
|
|
508
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
|
|
509
|
+
lines.push("");
|
|
510
|
+
lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
|
|
511
|
+
lines.push("");
|
|
512
|
+
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
|
|
513
|
+
lines.push(`Issues: ${state.openIssueCount} open`);
|
|
514
|
+
} else {
|
|
515
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push(`Since snapshot: ${recap.snapshot.createdAt}`);
|
|
518
|
+
if (recap.partial) {
|
|
519
|
+
lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
|
|
520
|
+
}
|
|
521
|
+
const changes = recap.changes;
|
|
522
|
+
const hasChanges = hasAnyChanges(changes);
|
|
523
|
+
if (!hasChanges) {
|
|
524
|
+
lines.push("");
|
|
525
|
+
lines.push("No changes since last snapshot.");
|
|
526
|
+
} else {
|
|
527
|
+
if (changes.phases.statusChanged.length > 0) {
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push("## Phase Transitions");
|
|
530
|
+
for (const p of changes.phases.statusChanged) {
|
|
531
|
+
lines.push(`- **${escapeMarkdownInline(p.name)}** (${p.id}): ${p.from} \u2192 ${p.to}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const ticketChanges = changes.tickets;
|
|
535
|
+
if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0) {
|
|
536
|
+
lines.push("");
|
|
537
|
+
lines.push("## Tickets");
|
|
538
|
+
for (const t of ticketChanges.statusChanged) {
|
|
539
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 ${t.from} \u2192 ${t.to}`);
|
|
540
|
+
}
|
|
541
|
+
for (const t of ticketChanges.added) {
|
|
542
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **new**`);
|
|
543
|
+
}
|
|
544
|
+
for (const t of ticketChanges.removed) {
|
|
545
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const issueChanges = changes.issues;
|
|
549
|
+
if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0) {
|
|
550
|
+
lines.push("");
|
|
551
|
+
lines.push("## Issues");
|
|
552
|
+
for (const i of issueChanges.resolved) {
|
|
553
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **resolved**`);
|
|
554
|
+
}
|
|
555
|
+
for (const i of issueChanges.statusChanged) {
|
|
556
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 ${i.from} \u2192 ${i.to}`);
|
|
557
|
+
}
|
|
558
|
+
for (const i of issueChanges.added) {
|
|
559
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
|
|
563
|
+
lines.push("");
|
|
564
|
+
lines.push("## Blockers");
|
|
565
|
+
for (const name of changes.blockers.cleared) {
|
|
566
|
+
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **cleared**`);
|
|
567
|
+
}
|
|
568
|
+
for (const name of changes.blockers.added) {
|
|
569
|
+
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const actions = recap.suggestedActions;
|
|
575
|
+
lines.push("");
|
|
576
|
+
lines.push("## Suggested Actions");
|
|
577
|
+
if (actions.nextTicket) {
|
|
578
|
+
lines.push(`- **Next:** ${actions.nextTicket.id} \u2014 ${escapeMarkdownInline(actions.nextTicket.title)}${actions.nextTicket.phase ? ` (${actions.nextTicket.phase})` : ""}`);
|
|
579
|
+
}
|
|
580
|
+
if (actions.highSeverityIssues.length > 0) {
|
|
581
|
+
for (const i of actions.highSeverityIssues) {
|
|
582
|
+
lines.push(`- **${i.severity} issue:** ${i.id} \u2014 ${escapeMarkdownInline(i.title)}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (actions.recentlyClearedBlockers.length > 0) {
|
|
586
|
+
lines.push(`- **Recently cleared:** ${actions.recentlyClearedBlockers.map(escapeMarkdownInline).join(", ")}`);
|
|
587
|
+
}
|
|
588
|
+
if (!actions.nextTicket && actions.highSeverityIssues.length === 0 && actions.recentlyClearedBlockers.length === 0) {
|
|
589
|
+
lines.push("- No urgent actions.");
|
|
590
|
+
}
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
function formatExport(state, mode, phaseId, format) {
|
|
594
|
+
if (mode === "phase" && phaseId) {
|
|
595
|
+
return formatPhaseExport(state, phaseId, format);
|
|
596
|
+
}
|
|
597
|
+
return formatFullExport(state, format);
|
|
598
|
+
}
|
|
599
|
+
function formatPhaseExport(state, phaseId, format) {
|
|
600
|
+
const phase = state.roadmap.phases.find((p) => p.id === phaseId);
|
|
601
|
+
if (!phase) {
|
|
602
|
+
return formatError("not_found", `Phase "${phaseId}" not found`, format);
|
|
603
|
+
}
|
|
604
|
+
const phaseStatus = state.phaseStatus(phaseId);
|
|
605
|
+
const leaves = state.phaseTickets(phaseId);
|
|
606
|
+
const umbrellaAncestors = /* @__PURE__ */ new Map();
|
|
607
|
+
for (const leaf of leaves) {
|
|
608
|
+
if (leaf.parentTicket) {
|
|
609
|
+
const parent = state.ticketByID(leaf.parentTicket);
|
|
610
|
+
if (parent && !umbrellaAncestors.has(parent.id)) {
|
|
611
|
+
umbrellaAncestors.set(parent.id, parent);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const crossPhaseDeps = /* @__PURE__ */ new Map();
|
|
616
|
+
for (const leaf of leaves) {
|
|
617
|
+
for (const blockerId of leaf.blockedBy) {
|
|
618
|
+
const blocker = state.ticketByID(blockerId);
|
|
619
|
+
if (blocker && blocker.phase !== phaseId && !crossPhaseDeps.has(blocker.id)) {
|
|
620
|
+
crossPhaseDeps.set(blocker.id, blocker);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const relatedIssues = state.issues.filter(
|
|
625
|
+
(i) => i.status !== "resolved" && (i.phase === phaseId || i.relatedTickets.some((tid) => {
|
|
626
|
+
const t = state.ticketByID(tid);
|
|
627
|
+
return t && t.phase === phaseId;
|
|
628
|
+
}))
|
|
629
|
+
);
|
|
630
|
+
const activeBlockers = state.roadmap.blockers.filter(
|
|
631
|
+
(b) => !isBlockerCleared(b)
|
|
632
|
+
);
|
|
633
|
+
if (format === "json") {
|
|
634
|
+
return JSON.stringify(
|
|
635
|
+
successEnvelope({
|
|
636
|
+
phase: { id: phase.id, name: phase.name, description: phase.description, status: phaseStatus },
|
|
637
|
+
tickets: leaves.map((t) => ({ id: t.id, title: t.title, status: t.status, type: t.type, order: t.order })),
|
|
638
|
+
umbrellaAncestors: [...umbrellaAncestors.values()].map((t) => ({ id: t.id, title: t.title })),
|
|
639
|
+
crossPhaseDependencies: [...crossPhaseDeps.values()].map((t) => ({ id: t.id, title: t.title, status: t.status, phase: t.phase })),
|
|
640
|
+
issues: relatedIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity, status: i.status })),
|
|
641
|
+
blockers: activeBlockers.map((b) => ({ name: b.name, note: b.note ?? null }))
|
|
642
|
+
}),
|
|
643
|
+
null,
|
|
644
|
+
2
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const lines = [];
|
|
648
|
+
lines.push(`# ${escapeMarkdownInline(phase.name)} (${phase.id})`);
|
|
649
|
+
lines.push("");
|
|
650
|
+
lines.push(`Status: ${phaseStatus}`);
|
|
651
|
+
if (phase.description) {
|
|
652
|
+
lines.push(`Description: ${escapeMarkdownInline(phase.description)}`);
|
|
653
|
+
}
|
|
654
|
+
if (leaves.length > 0) {
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("## Tickets");
|
|
657
|
+
for (const t of leaves) {
|
|
658
|
+
const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
659
|
+
const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
|
|
660
|
+
lines.push(`${indicator} ${t.id}: ${escapeMarkdownInline(t.title)}${parentNote}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (crossPhaseDeps.size > 0) {
|
|
664
|
+
lines.push("");
|
|
665
|
+
lines.push("## Cross-Phase Dependencies");
|
|
666
|
+
for (const [, dep] of crossPhaseDeps) {
|
|
667
|
+
lines.push(`- ${dep.id}: ${escapeMarkdownInline(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (relatedIssues.length > 0) {
|
|
671
|
+
lines.push("");
|
|
672
|
+
lines.push("## Open Issues");
|
|
673
|
+
for (const i of relatedIssues) {
|
|
674
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (activeBlockers.length > 0) {
|
|
678
|
+
lines.push("");
|
|
679
|
+
lines.push("## Active Blockers");
|
|
680
|
+
for (const b of activeBlockers) {
|
|
681
|
+
lines.push(`- ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return lines.join("\n");
|
|
685
|
+
}
|
|
686
|
+
function formatFullExport(state, format) {
|
|
687
|
+
const phases = phasesWithStatus(state);
|
|
688
|
+
if (format === "json") {
|
|
689
|
+
return JSON.stringify(
|
|
690
|
+
successEnvelope({
|
|
691
|
+
project: state.config.project,
|
|
692
|
+
phases: phases.map((p) => ({
|
|
693
|
+
id: p.phase.id,
|
|
694
|
+
name: p.phase.name,
|
|
695
|
+
description: p.phase.description,
|
|
696
|
+
status: p.status,
|
|
697
|
+
tickets: state.phaseTickets(p.phase.id).map((t) => ({
|
|
698
|
+
id: t.id,
|
|
699
|
+
title: t.title,
|
|
700
|
+
status: t.status,
|
|
701
|
+
type: t.type
|
|
702
|
+
}))
|
|
703
|
+
})),
|
|
704
|
+
issues: state.issues.map((i) => ({
|
|
705
|
+
id: i.id,
|
|
706
|
+
title: i.title,
|
|
707
|
+
severity: i.severity,
|
|
708
|
+
status: i.status
|
|
709
|
+
})),
|
|
710
|
+
blockers: state.roadmap.blockers.map((b) => ({
|
|
711
|
+
name: b.name,
|
|
712
|
+
cleared: isBlockerCleared(b),
|
|
713
|
+
note: b.note ?? null
|
|
714
|
+
}))
|
|
715
|
+
}),
|
|
716
|
+
null,
|
|
717
|
+
2
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const lines = [];
|
|
721
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
|
|
722
|
+
lines.push("");
|
|
723
|
+
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
724
|
+
lines.push(`Issues: ${state.openIssueCount} open`);
|
|
725
|
+
lines.push("");
|
|
726
|
+
lines.push("## Phases");
|
|
727
|
+
for (const p of phases) {
|
|
728
|
+
const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
|
|
729
|
+
lines.push("");
|
|
730
|
+
lines.push(`### ${indicator} ${escapeMarkdownInline(p.phase.name)} (${p.phase.id})`);
|
|
731
|
+
if (p.phase.description) {
|
|
732
|
+
lines.push(escapeMarkdownInline(p.phase.description));
|
|
733
|
+
}
|
|
734
|
+
const tickets = state.phaseTickets(p.phase.id);
|
|
735
|
+
if (tickets.length > 0) {
|
|
736
|
+
lines.push("");
|
|
737
|
+
for (const t of tickets) {
|
|
738
|
+
const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
739
|
+
lines.push(`${ti} ${t.id}: ${escapeMarkdownInline(t.title)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (state.issues.length > 0) {
|
|
744
|
+
lines.push("");
|
|
745
|
+
lines.push("## Issues");
|
|
746
|
+
for (const i of state.issues) {
|
|
747
|
+
const resolved = i.status === "resolved" ? " \u2713" : "";
|
|
748
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const blockers = state.roadmap.blockers;
|
|
752
|
+
if (blockers.length > 0) {
|
|
753
|
+
lines.push("");
|
|
754
|
+
lines.push("## Blockers");
|
|
755
|
+
for (const b of blockers) {
|
|
756
|
+
const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
|
|
757
|
+
lines.push(`${cleared} ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return lines.join("\n");
|
|
761
|
+
}
|
|
762
|
+
function hasAnyChanges(diff) {
|
|
763
|
+
return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0;
|
|
764
|
+
}
|
|
487
765
|
function truncate(text, maxLen) {
|
|
488
766
|
if (text.length <= maxLen) return text;
|
|
489
767
|
return text.slice(0, maxLen - 3) + "...";
|
|
@@ -496,7 +774,7 @@ function formatTicketOneLiner(t, state) {
|
|
|
496
774
|
|
|
497
775
|
// src/cli/run.ts
|
|
498
776
|
init_esm_shims();
|
|
499
|
-
import { join as
|
|
777
|
+
import { join as join6 } from "path";
|
|
500
778
|
|
|
501
779
|
// src/core/index.ts
|
|
502
780
|
init_esm_shims();
|
|
@@ -847,6 +1125,7 @@ import { readdir, readFile } from "fs/promises";
|
|
|
847
1125
|
import { existsSync } from "fs";
|
|
848
1126
|
import { join, relative, extname } from "path";
|
|
849
1127
|
var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
|
|
1128
|
+
var HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
|
|
850
1129
|
async function listHandovers(handoversDir, root, warnings) {
|
|
851
1130
|
if (!existsSync(handoversDir)) return [];
|
|
852
1131
|
let entries;
|
|
@@ -876,7 +1155,16 @@ async function listHandovers(handoversDir, root, warnings) {
|
|
|
876
1155
|
});
|
|
877
1156
|
}
|
|
878
1157
|
}
|
|
879
|
-
conforming.sort((a, b) =>
|
|
1158
|
+
conforming.sort((a, b) => {
|
|
1159
|
+
const dateA = a.slice(0, 10);
|
|
1160
|
+
const dateB = b.slice(0, 10);
|
|
1161
|
+
if (dateA !== dateB) return dateB.localeCompare(dateA);
|
|
1162
|
+
const seqA = a.match(HANDOVER_SEQ_REGEX);
|
|
1163
|
+
const seqB = b.match(HANDOVER_SEQ_REGEX);
|
|
1164
|
+
if (seqA && !seqB) return -1;
|
|
1165
|
+
if (!seqA && seqB) return 1;
|
|
1166
|
+
return b.localeCompare(a);
|
|
1167
|
+
});
|
|
880
1168
|
return [...conforming, ...nonConforming];
|
|
881
1169
|
}
|
|
882
1170
|
async function readHandover(handoversDir, filename) {
|
|
@@ -1792,12 +2080,241 @@ async function initProject(root, options) {
|
|
|
1792
2080
|
};
|
|
1793
2081
|
}
|
|
1794
2082
|
|
|
2083
|
+
// src/core/snapshot.ts
|
|
2084
|
+
init_esm_shims();
|
|
2085
|
+
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
|
|
2086
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2087
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
2088
|
+
import { z as z6 } from "zod";
|
|
2089
|
+
var LoadWarningSchema = z6.object({
|
|
2090
|
+
type: z6.string(),
|
|
2091
|
+
file: z6.string(),
|
|
2092
|
+
message: z6.string()
|
|
2093
|
+
});
|
|
2094
|
+
var SnapshotV1Schema = z6.object({
|
|
2095
|
+
version: z6.literal(1),
|
|
2096
|
+
createdAt: z6.string().datetime({ offset: true }),
|
|
2097
|
+
project: z6.string(),
|
|
2098
|
+
config: ConfigSchema,
|
|
2099
|
+
roadmap: RoadmapSchema,
|
|
2100
|
+
tickets: z6.array(TicketSchema),
|
|
2101
|
+
issues: z6.array(IssueSchema),
|
|
2102
|
+
warnings: z6.array(LoadWarningSchema).optional()
|
|
2103
|
+
});
|
|
2104
|
+
var MAX_SNAPSHOTS = 20;
|
|
2105
|
+
async function saveSnapshot(root, loadResult) {
|
|
2106
|
+
const absRoot = resolve4(root);
|
|
2107
|
+
const snapshotsDir = join5(absRoot, ".story", "snapshots");
|
|
2108
|
+
await mkdir2(snapshotsDir, { recursive: true });
|
|
2109
|
+
const { state, warnings } = loadResult;
|
|
2110
|
+
const now = /* @__PURE__ */ new Date();
|
|
2111
|
+
const filename = formatSnapshotFilename(now);
|
|
2112
|
+
const snapshot = {
|
|
2113
|
+
version: 1,
|
|
2114
|
+
createdAt: now.toISOString(),
|
|
2115
|
+
project: state.config.project,
|
|
2116
|
+
config: state.config,
|
|
2117
|
+
roadmap: state.roadmap,
|
|
2118
|
+
tickets: [...state.tickets],
|
|
2119
|
+
issues: [...state.issues],
|
|
2120
|
+
...warnings.length > 0 ? {
|
|
2121
|
+
warnings: warnings.map((w) => ({
|
|
2122
|
+
type: w.type,
|
|
2123
|
+
file: w.file,
|
|
2124
|
+
message: w.message
|
|
2125
|
+
}))
|
|
2126
|
+
} : {}
|
|
2127
|
+
};
|
|
2128
|
+
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
2129
|
+
const targetPath = join5(snapshotsDir, filename);
|
|
2130
|
+
const wrapDir = join5(absRoot, ".story");
|
|
2131
|
+
await guardPath(targetPath, wrapDir);
|
|
2132
|
+
await atomicWrite(targetPath, json);
|
|
2133
|
+
const pruned = await pruneSnapshots(snapshotsDir);
|
|
2134
|
+
const entries = await listSnapshotFiles(snapshotsDir);
|
|
2135
|
+
return { filename, retained: entries.length, pruned };
|
|
2136
|
+
}
|
|
2137
|
+
async function loadLatestSnapshot(root) {
|
|
2138
|
+
const snapshotsDir = join5(resolve4(root), ".story", "snapshots");
|
|
2139
|
+
if (!existsSync4(snapshotsDir)) return null;
|
|
2140
|
+
const files = await listSnapshotFiles(snapshotsDir);
|
|
2141
|
+
if (files.length === 0) return null;
|
|
2142
|
+
for (const filename of files) {
|
|
2143
|
+
try {
|
|
2144
|
+
const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
|
|
2145
|
+
const parsed = JSON.parse(content);
|
|
2146
|
+
const snapshot = SnapshotV1Schema.parse(parsed);
|
|
2147
|
+
return { snapshot, filename };
|
|
2148
|
+
} catch {
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
function diffStates(snapshotState, currentState) {
|
|
2155
|
+
const snapTickets = new Map(snapshotState.tickets.map((t) => [t.id, t]));
|
|
2156
|
+
const curTickets = new Map(currentState.tickets.map((t) => [t.id, t]));
|
|
2157
|
+
const ticketsAdded = [];
|
|
2158
|
+
const ticketsRemoved = [];
|
|
2159
|
+
const ticketsStatusChanged = [];
|
|
2160
|
+
for (const [id, cur] of curTickets) {
|
|
2161
|
+
const snap = snapTickets.get(id);
|
|
2162
|
+
if (!snap) {
|
|
2163
|
+
ticketsAdded.push({ id, title: cur.title });
|
|
2164
|
+
} else if (snap.status !== cur.status) {
|
|
2165
|
+
ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
for (const [id, snap] of snapTickets) {
|
|
2169
|
+
if (!curTickets.has(id)) {
|
|
2170
|
+
ticketsRemoved.push({ id, title: snap.title });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
const snapIssues = new Map(snapshotState.issues.map((i) => [i.id, i]));
|
|
2174
|
+
const curIssues = new Map(currentState.issues.map((i) => [i.id, i]));
|
|
2175
|
+
const issuesAdded = [];
|
|
2176
|
+
const issuesResolved = [];
|
|
2177
|
+
const issuesStatusChanged = [];
|
|
2178
|
+
for (const [id, cur] of curIssues) {
|
|
2179
|
+
const snap = snapIssues.get(id);
|
|
2180
|
+
if (!snap) {
|
|
2181
|
+
issuesAdded.push({ id, title: cur.title });
|
|
2182
|
+
} else if (snap.status !== cur.status) {
|
|
2183
|
+
if (cur.status === "resolved") {
|
|
2184
|
+
issuesResolved.push({ id, title: cur.title });
|
|
2185
|
+
} else {
|
|
2186
|
+
issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const snapBlockers = new Map(
|
|
2191
|
+
snapshotState.roadmap.blockers.map((b) => [b.name, b])
|
|
2192
|
+
);
|
|
2193
|
+
const curBlockers = new Map(
|
|
2194
|
+
currentState.roadmap.blockers.map((b) => [b.name, b])
|
|
2195
|
+
);
|
|
2196
|
+
const blockersAdded = [];
|
|
2197
|
+
const blockersCleared = [];
|
|
2198
|
+
for (const [name, cur] of curBlockers) {
|
|
2199
|
+
const snap = snapBlockers.get(name);
|
|
2200
|
+
if (!snap) {
|
|
2201
|
+
blockersAdded.push(name);
|
|
2202
|
+
} else if (!isBlockerCleared(snap) && isBlockerCleared(cur)) {
|
|
2203
|
+
blockersCleared.push(name);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const snapPhases = snapshotState.roadmap.phases;
|
|
2207
|
+
const curPhases = currentState.roadmap.phases;
|
|
2208
|
+
const snapPhaseMap = new Map(snapPhases.map((p) => [p.id, p]));
|
|
2209
|
+
const curPhaseMap = new Map(curPhases.map((p) => [p.id, p]));
|
|
2210
|
+
const phasesAdded = [];
|
|
2211
|
+
const phasesRemoved = [];
|
|
2212
|
+
const phasesStatusChanged = [];
|
|
2213
|
+
for (const [id, curPhase] of curPhaseMap) {
|
|
2214
|
+
const snapPhase = snapPhaseMap.get(id);
|
|
2215
|
+
if (!snapPhase) {
|
|
2216
|
+
phasesAdded.push({ id, name: curPhase.name });
|
|
2217
|
+
} else {
|
|
2218
|
+
const snapStatus = snapshotState.phaseStatus(id);
|
|
2219
|
+
const curStatus = currentState.phaseStatus(id);
|
|
2220
|
+
if (snapStatus !== curStatus) {
|
|
2221
|
+
phasesStatusChanged.push({
|
|
2222
|
+
id,
|
|
2223
|
+
name: curPhase.name,
|
|
2224
|
+
from: snapStatus,
|
|
2225
|
+
to: curStatus
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
for (const [id, snapPhase] of snapPhaseMap) {
|
|
2231
|
+
if (!curPhaseMap.has(id)) {
|
|
2232
|
+
phasesRemoved.push({ id, name: snapPhase.name });
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
|
|
2237
|
+
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged },
|
|
2238
|
+
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
2239
|
+
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function buildRecap(currentState, snapshotInfo) {
|
|
2243
|
+
const next = nextTicket(currentState);
|
|
2244
|
+
const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
|
|
2245
|
+
const highSeverityIssues = currentState.issues.filter(
|
|
2246
|
+
(i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
|
|
2247
|
+
).map((i) => ({ id: i.id, title: i.title, severity: i.severity }));
|
|
2248
|
+
if (!snapshotInfo) {
|
|
2249
|
+
return {
|
|
2250
|
+
snapshot: null,
|
|
2251
|
+
changes: null,
|
|
2252
|
+
suggestedActions: {
|
|
2253
|
+
nextTicket: nextTicketAction,
|
|
2254
|
+
highSeverityIssues,
|
|
2255
|
+
recentlyClearedBlockers: []
|
|
2256
|
+
},
|
|
2257
|
+
partial: false
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
const { snapshot, filename } = snapshotInfo;
|
|
2261
|
+
const snapshotState = new ProjectState({
|
|
2262
|
+
tickets: snapshot.tickets,
|
|
2263
|
+
issues: snapshot.issues,
|
|
2264
|
+
roadmap: snapshot.roadmap,
|
|
2265
|
+
config: snapshot.config,
|
|
2266
|
+
handoverFilenames: []
|
|
2267
|
+
});
|
|
2268
|
+
const changes = diffStates(snapshotState, currentState);
|
|
2269
|
+
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
2270
|
+
return {
|
|
2271
|
+
snapshot: { filename, createdAt: snapshot.createdAt },
|
|
2272
|
+
changes,
|
|
2273
|
+
suggestedActions: {
|
|
2274
|
+
nextTicket: nextTicketAction,
|
|
2275
|
+
highSeverityIssues,
|
|
2276
|
+
recentlyClearedBlockers
|
|
2277
|
+
},
|
|
2278
|
+
partial: (snapshot.warnings ?? []).length > 0
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function formatSnapshotFilename(date) {
|
|
2282
|
+
const y = date.getUTCFullYear();
|
|
2283
|
+
const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2284
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
2285
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
2286
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2287
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2288
|
+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
2289
|
+
return `${y}-${mo}-${d}T${h}-${mi}-${s}-${ms}.json`;
|
|
2290
|
+
}
|
|
2291
|
+
async function listSnapshotFiles(dir) {
|
|
2292
|
+
try {
|
|
2293
|
+
const entries = await readdir3(dir);
|
|
2294
|
+
return entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().reverse();
|
|
2295
|
+
} catch {
|
|
2296
|
+
return [];
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async function pruneSnapshots(dir) {
|
|
2300
|
+
const files = await listSnapshotFiles(dir);
|
|
2301
|
+
if (files.length <= MAX_SNAPSHOTS) return 0;
|
|
2302
|
+
const toRemove = files.slice(MAX_SNAPSHOTS);
|
|
2303
|
+
for (const f of toRemove) {
|
|
2304
|
+
try {
|
|
2305
|
+
await unlink2(join5(dir, f));
|
|
2306
|
+
} catch {
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return toRemove.length;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1795
2312
|
// src/cli/run.ts
|
|
1796
2313
|
init_errors();
|
|
1797
2314
|
|
|
1798
2315
|
// src/cli/helpers.ts
|
|
1799
2316
|
init_esm_shims();
|
|
1800
|
-
import { resolve as
|
|
2317
|
+
import { resolve as resolve5, relative as relative3, extname as extname3 } from "path";
|
|
1801
2318
|
import { lstat as lstat2 } from "fs/promises";
|
|
1802
2319
|
var CliValidationError = class extends Error {
|
|
1803
2320
|
constructor(code, message) {
|
|
@@ -1870,10 +2387,10 @@ async function parseHandoverFilename(raw, handoversDir) {
|
|
|
1870
2387
|
`Invalid handover filename "${raw}": must have .md extension`
|
|
1871
2388
|
);
|
|
1872
2389
|
}
|
|
1873
|
-
const resolvedDir =
|
|
1874
|
-
const resolvedCandidate =
|
|
2390
|
+
const resolvedDir = resolve5(handoversDir);
|
|
2391
|
+
const resolvedCandidate = resolve5(handoversDir, raw);
|
|
1875
2392
|
const rel = relative3(resolvedDir, resolvedCandidate);
|
|
1876
|
-
if (!rel || rel.startsWith("..") ||
|
|
2393
|
+
if (!rel || rel.startsWith("..") || resolve5(resolvedDir, rel) !== resolvedCandidate) {
|
|
1877
2394
|
throw new CliValidationError(
|
|
1878
2395
|
"invalid_input",
|
|
1879
2396
|
`Invalid handover filename "${raw}": resolves outside handovers directory`
|
|
@@ -1934,7 +2451,7 @@ async function runReadCommand(format, handler) {
|
|
|
1934
2451
|
return;
|
|
1935
2452
|
}
|
|
1936
2453
|
const { state, warnings } = await loadProject(root);
|
|
1937
|
-
const handoversDir =
|
|
2454
|
+
const handoversDir = join6(root, ".story", "handovers");
|
|
1938
2455
|
const result = await handler({ state, warnings, root, handoversDir, format });
|
|
1939
2456
|
writeOutput(result.output);
|
|
1940
2457
|
let exitCode = result.exitCode ?? ExitCode.OK;
|
|
@@ -1969,7 +2486,7 @@ async function runDeleteCommand(format, force, handler) {
|
|
|
1969
2486
|
return;
|
|
1970
2487
|
}
|
|
1971
2488
|
const { state, warnings } = await loadProject(root);
|
|
1972
|
-
const handoversDir =
|
|
2489
|
+
const handoversDir = join6(root, ".story", "handovers");
|
|
1973
2490
|
if (!force && hasIntegrityWarnings(warnings)) {
|
|
1974
2491
|
writeOutput(
|
|
1975
2492
|
formatError(
|
|
@@ -2023,6 +2540,8 @@ function handleValidate(ctx) {
|
|
|
2023
2540
|
|
|
2024
2541
|
// src/cli/commands/handover.ts
|
|
2025
2542
|
init_esm_shims();
|
|
2543
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
2544
|
+
import { join as join7, resolve as resolve6 } from "path";
|
|
2026
2545
|
function handleHandoverList(ctx) {
|
|
2027
2546
|
return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
|
|
2028
2547
|
}
|
|
@@ -2074,6 +2593,59 @@ async function handleHandoverGet(filename, ctx) {
|
|
|
2074
2593
|
};
|
|
2075
2594
|
}
|
|
2076
2595
|
}
|
|
2596
|
+
function normalizeSlug(raw) {
|
|
2597
|
+
let slug = raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2598
|
+
if (slug.length > 60) slug = slug.slice(0, 60).replace(/-$/, "");
|
|
2599
|
+
if (!slug) {
|
|
2600
|
+
throw new CliValidationError(
|
|
2601
|
+
"invalid_input",
|
|
2602
|
+
`Slug is empty after normalization: "${raw}"`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
return slug;
|
|
2606
|
+
}
|
|
2607
|
+
async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
2608
|
+
if (!content.trim()) {
|
|
2609
|
+
throw new CliValidationError("invalid_input", "Handover content is empty");
|
|
2610
|
+
}
|
|
2611
|
+
const slug = normalizeSlug(slugRaw);
|
|
2612
|
+
const date = todayISO();
|
|
2613
|
+
let filename;
|
|
2614
|
+
await withProjectLock(root, { strict: false }, async () => {
|
|
2615
|
+
const absRoot = resolve6(root);
|
|
2616
|
+
const handoversDir = join7(absRoot, ".story", "handovers");
|
|
2617
|
+
await mkdir3(handoversDir, { recursive: true });
|
|
2618
|
+
const wrapDir = join7(absRoot, ".story");
|
|
2619
|
+
const datePrefix = `${date}-`;
|
|
2620
|
+
const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
|
|
2621
|
+
let maxSeq = 0;
|
|
2622
|
+
const { readdirSync } = await import("fs");
|
|
2623
|
+
try {
|
|
2624
|
+
for (const f of readdirSync(handoversDir)) {
|
|
2625
|
+
const m = f.match(seqRegex);
|
|
2626
|
+
if (m) {
|
|
2627
|
+
const n = parseInt(m[1], 10);
|
|
2628
|
+
if (n > maxSeq) maxSeq = n;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
const nextSeq = maxSeq + 1;
|
|
2634
|
+
if (nextSeq > 99) {
|
|
2635
|
+
throw new CliValidationError(
|
|
2636
|
+
"conflict",
|
|
2637
|
+
`Too many handovers for ${date}; limit is 99 per day`
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
|
|
2641
|
+
const candidatePath = join7(handoversDir, candidate);
|
|
2642
|
+
await parseHandoverFilename(candidate, handoversDir);
|
|
2643
|
+
await guardPath(candidatePath, wrapDir);
|
|
2644
|
+
await atomicWrite(candidatePath, content);
|
|
2645
|
+
filename = candidate;
|
|
2646
|
+
});
|
|
2647
|
+
return { output: formatHandoverCreateResult(filename, format) };
|
|
2648
|
+
}
|
|
2077
2649
|
|
|
2078
2650
|
// src/cli/commands/blocker.ts
|
|
2079
2651
|
init_esm_shims();
|
|
@@ -2499,7 +3071,7 @@ async function handleIssueDelete(id, format, root) {
|
|
|
2499
3071
|
|
|
2500
3072
|
// src/cli/commands/phase.ts
|
|
2501
3073
|
init_esm_shims();
|
|
2502
|
-
import { join as
|
|
3074
|
+
import { join as join8, resolve as resolve7 } from "path";
|
|
2503
3075
|
var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
2504
3076
|
var PHASE_ID_MAX_LENGTH = 40;
|
|
2505
3077
|
function validatePhaseId(id) {
|
|
@@ -2682,7 +3254,7 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
2682
3254
|
}
|
|
2683
3255
|
const targetLeaves = state.phaseTickets(reassign);
|
|
2684
3256
|
let maxOrder = targetLeaves.length > 0 ? targetLeaves[targetLeaves.length - 1].order : 0;
|
|
2685
|
-
const wrapDir =
|
|
3257
|
+
const wrapDir = resolve7(root, ".story");
|
|
2686
3258
|
const operations = [];
|
|
2687
3259
|
const sortedTickets = [...affectedTickets].sort((a, b) => a.order - b.order);
|
|
2688
3260
|
for (const ticket of sortedTickets) {
|
|
@@ -2690,21 +3262,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
2690
3262
|
const updated = { ...ticket, phase: reassign, order: maxOrder };
|
|
2691
3263
|
const parsed = TicketSchema.parse(updated);
|
|
2692
3264
|
const content = serializeJSON(parsed);
|
|
2693
|
-
const target =
|
|
3265
|
+
const target = join8(wrapDir, "tickets", `${parsed.id}.json`);
|
|
2694
3266
|
operations.push({ op: "write", target, content });
|
|
2695
3267
|
}
|
|
2696
3268
|
for (const issue of affectedIssues) {
|
|
2697
3269
|
const updated = { ...issue, phase: reassign };
|
|
2698
3270
|
const parsed = IssueSchema.parse(updated);
|
|
2699
3271
|
const content = serializeJSON(parsed);
|
|
2700
|
-
const target =
|
|
3272
|
+
const target = join8(wrapDir, "issues", `${parsed.id}.json`);
|
|
2701
3273
|
operations.push({ op: "write", target, content });
|
|
2702
3274
|
}
|
|
2703
3275
|
const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
|
|
2704
3276
|
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
2705
3277
|
const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
|
|
2706
3278
|
const roadmapContent = serializeJSON(parsedRoadmap);
|
|
2707
|
-
const roadmapTarget =
|
|
3279
|
+
const roadmapTarget = join8(wrapDir, "roadmap.json");
|
|
2708
3280
|
operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
|
|
2709
3281
|
await runTransactionUnlocked(root, operations);
|
|
2710
3282
|
} else {
|
|
@@ -2788,6 +3360,39 @@ function registerInitCommand(yargs2) {
|
|
|
2788
3360
|
);
|
|
2789
3361
|
}
|
|
2790
3362
|
|
|
3363
|
+
// src/cli/commands/recap.ts
|
|
3364
|
+
init_esm_shims();
|
|
3365
|
+
async function handleRecap(ctx) {
|
|
3366
|
+
const snapshotInfo = await loadLatestSnapshot(ctx.root);
|
|
3367
|
+
const recap = buildRecap(ctx.state, snapshotInfo);
|
|
3368
|
+
return { output: formatRecap(recap, ctx.state, ctx.format) };
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
// src/cli/commands/export.ts
|
|
3372
|
+
init_esm_shims();
|
|
3373
|
+
function handleExport(ctx, mode, phaseId) {
|
|
3374
|
+
if (mode === "phase") {
|
|
3375
|
+
if (!phaseId) {
|
|
3376
|
+
throw new CliValidationError("invalid_input", "Missing --phase value");
|
|
3377
|
+
}
|
|
3378
|
+
const phase = ctx.state.roadmap.phases.find((p) => p.id === phaseId);
|
|
3379
|
+
if (!phase) {
|
|
3380
|
+
throw new CliValidationError("not_found", `Phase "${phaseId}" not found in roadmap`);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return { output: formatExport(ctx.state, mode, phaseId, ctx.format) };
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// src/cli/commands/snapshot.ts
|
|
3387
|
+
init_esm_shims();
|
|
3388
|
+
async function handleSnapshot(root, format) {
|
|
3389
|
+
let result;
|
|
3390
|
+
await withProjectLock(root, { strict: false }, async (loadResult) => {
|
|
3391
|
+
result = await saveSnapshot(root, loadResult);
|
|
3392
|
+
});
|
|
3393
|
+
return { output: formatSnapshotResult(result, format) };
|
|
3394
|
+
}
|
|
3395
|
+
|
|
2791
3396
|
// src/cli/register.ts
|
|
2792
3397
|
function registerStatusCommand(yargs2) {
|
|
2793
3398
|
return yargs2.command(
|
|
@@ -2849,7 +3454,83 @@ function registerHandoverCommand(yargs2) {
|
|
|
2849
3454
|
(ctx) => handleHandoverGet(filename, ctx)
|
|
2850
3455
|
);
|
|
2851
3456
|
}
|
|
2852
|
-
).
|
|
3457
|
+
).command(
|
|
3458
|
+
"create",
|
|
3459
|
+
"Create a new handover document",
|
|
3460
|
+
(y2) => addFormatOption(
|
|
3461
|
+
y2.option("content", {
|
|
3462
|
+
type: "string",
|
|
3463
|
+
describe: "Handover content (markdown string)"
|
|
3464
|
+
}).option("stdin", {
|
|
3465
|
+
type: "boolean",
|
|
3466
|
+
describe: "Read content from stdin"
|
|
3467
|
+
}).option("slug", {
|
|
3468
|
+
type: "string",
|
|
3469
|
+
default: "session",
|
|
3470
|
+
describe: "Slug for filename (e.g. phase5b-wrapup)"
|
|
3471
|
+
}).conflicts("content", "stdin").check((argv) => {
|
|
3472
|
+
if (!argv.content && !argv.stdin) {
|
|
3473
|
+
throw new Error(
|
|
3474
|
+
"Specify either --content or --stdin"
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
return true;
|
|
3478
|
+
})
|
|
3479
|
+
),
|
|
3480
|
+
async (argv) => {
|
|
3481
|
+
const format = parseOutputFormat(argv.format);
|
|
3482
|
+
const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
|
|
3483
|
+
if (!root) {
|
|
3484
|
+
writeOutput(
|
|
3485
|
+
formatError("not_found", "No .story/ project found.", format)
|
|
3486
|
+
);
|
|
3487
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
let content;
|
|
3491
|
+
if (argv.stdin) {
|
|
3492
|
+
if (process.stdin.isTTY) {
|
|
3493
|
+
writeOutput(
|
|
3494
|
+
formatError("invalid_input", "Cannot read from stdin: no pipe detected. Use --content instead.", format)
|
|
3495
|
+
);
|
|
3496
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
const chunks = [];
|
|
3500
|
+
for await (const chunk of process.stdin) {
|
|
3501
|
+
chunks.push(chunk);
|
|
3502
|
+
}
|
|
3503
|
+
content = Buffer.concat(chunks).toString("utf-8");
|
|
3504
|
+
} else {
|
|
3505
|
+
content = argv.content;
|
|
3506
|
+
}
|
|
3507
|
+
try {
|
|
3508
|
+
const result = await handleHandoverCreate(
|
|
3509
|
+
content,
|
|
3510
|
+
argv.slug,
|
|
3511
|
+
format,
|
|
3512
|
+
root
|
|
3513
|
+
);
|
|
3514
|
+
writeOutput(result.output);
|
|
3515
|
+
process.exitCode = result.exitCode ?? ExitCode.OK;
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
if (err instanceof CliValidationError) {
|
|
3518
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
3519
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
3523
|
+
if (err instanceof ProjectLoaderError2) {
|
|
3524
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
3525
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3529
|
+
writeOutput(formatError("io_error", message, format));
|
|
3530
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
).demandCommand(1, "Specify a handover subcommand: list, latest, get, create").strict(),
|
|
2853
3534
|
() => {
|
|
2854
3535
|
}
|
|
2855
3536
|
);
|
|
@@ -3780,9 +4461,89 @@ function registerPhaseCommand(yargs2) {
|
|
|
3780
4461
|
}
|
|
3781
4462
|
);
|
|
3782
4463
|
}
|
|
4464
|
+
function registerSnapshotCommand(yargs2) {
|
|
4465
|
+
return yargs2.command(
|
|
4466
|
+
"snapshot",
|
|
4467
|
+
"Save current project state for session diffs",
|
|
4468
|
+
(y) => addFormatOption(y),
|
|
4469
|
+
async (argv) => {
|
|
4470
|
+
const format = parseOutputFormat(argv.format);
|
|
4471
|
+
const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
|
|
4472
|
+
if (!root) {
|
|
4473
|
+
writeOutput(
|
|
4474
|
+
formatError("not_found", "No .story/ project found.", format)
|
|
4475
|
+
);
|
|
4476
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
try {
|
|
4480
|
+
const result = await handleSnapshot(root, format);
|
|
4481
|
+
writeOutput(result.output);
|
|
4482
|
+
process.exitCode = result.exitCode ?? ExitCode.OK;
|
|
4483
|
+
} catch (err) {
|
|
4484
|
+
if (err instanceof CliValidationError) {
|
|
4485
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
4486
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
4490
|
+
if (err instanceof ProjectLoaderError2) {
|
|
4491
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
4492
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4496
|
+
writeOutput(formatError("io_error", message, format));
|
|
4497
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
function registerRecapCommand(yargs2) {
|
|
4503
|
+
return yargs2.command(
|
|
4504
|
+
"recap",
|
|
4505
|
+
"Session diff \u2014 changes since last snapshot + suggested actions",
|
|
4506
|
+
(y) => addFormatOption(y),
|
|
4507
|
+
async (argv) => {
|
|
4508
|
+
const format = parseOutputFormat(argv.format);
|
|
4509
|
+
await runReadCommand(format, handleRecap);
|
|
4510
|
+
}
|
|
4511
|
+
);
|
|
4512
|
+
}
|
|
4513
|
+
function registerExportCommand(yargs2) {
|
|
4514
|
+
return yargs2.command(
|
|
4515
|
+
"export",
|
|
4516
|
+
"Self-contained project document for sharing",
|
|
4517
|
+
(y) => addFormatOption(
|
|
4518
|
+
y.option("phase", {
|
|
4519
|
+
type: "string",
|
|
4520
|
+
describe: "Export a single phase by ID"
|
|
4521
|
+
}).option("all", {
|
|
4522
|
+
type: "boolean",
|
|
4523
|
+
describe: "Export entire project"
|
|
4524
|
+
}).conflicts("phase", "all").check((argv) => {
|
|
4525
|
+
if (!argv.phase && !argv.all) {
|
|
4526
|
+
throw new Error(
|
|
4527
|
+
"Specify either --phase <id> or --all"
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
return true;
|
|
4531
|
+
})
|
|
4532
|
+
),
|
|
4533
|
+
async (argv) => {
|
|
4534
|
+
const format = parseOutputFormat(argv.format);
|
|
4535
|
+
const mode = argv.all ? "all" : "phase";
|
|
4536
|
+
const phaseId = argv.phase ?? null;
|
|
4537
|
+
await runReadCommand(
|
|
4538
|
+
format,
|
|
4539
|
+
(ctx) => handleExport(ctx, mode, phaseId)
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
);
|
|
4543
|
+
}
|
|
3783
4544
|
|
|
3784
4545
|
// src/cli/index.ts
|
|
3785
|
-
var version = "0.1.
|
|
4546
|
+
var version = "0.1.5";
|
|
3786
4547
|
var HandledError = class extends Error {
|
|
3787
4548
|
constructor() {
|
|
3788
4549
|
super("HANDLED_ERROR");
|
|
@@ -3812,6 +4573,9 @@ cli = registerIssueCommand(cli);
|
|
|
3812
4573
|
cli = registerHandoverCommand(cli);
|
|
3813
4574
|
cli = registerBlockerCommand(cli);
|
|
3814
4575
|
cli = registerValidateCommand(cli);
|
|
4576
|
+
cli = registerSnapshotCommand(cli);
|
|
4577
|
+
cli = registerRecapCommand(cli);
|
|
4578
|
+
cli = registerExportCommand(cli);
|
|
3815
4579
|
function handleUnexpectedError(err) {
|
|
3816
4580
|
if (err instanceof HandledError) return;
|
|
3817
4581
|
const message = err instanceof Error ? err.message : String(err);
|