specsmd 0.1.31 → 0.1.32

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.
@@ -96,14 +96,16 @@ function normalizePanelLine(line) {
96
96
  return {
97
97
  text: typeof line.text === 'string' ? line.text : String(line.text ?? ''),
98
98
  color: line.color,
99
- bold: Boolean(line.bold)
99
+ bold: Boolean(line.bold),
100
+ selected: Boolean(line.selected)
100
101
  };
101
102
  }
102
103
 
103
104
  return {
104
105
  text: String(line ?? ''),
105
106
  color: undefined,
106
- bold: false
107
+ bold: false,
108
+ selected: false
107
109
  };
108
110
  }
109
111
 
@@ -120,6 +122,15 @@ function fitLines(lines, maxLines, width) {
120
122
  return safeLines;
121
123
  }
122
124
 
125
+ const selectedIndex = safeLines.findIndex((line) => line.selected);
126
+ if (selectedIndex >= 0) {
127
+ const windowSize = Math.max(1, maxLines);
128
+ let start = selectedIndex - Math.floor(windowSize / 2);
129
+ start = Math.max(0, start);
130
+ start = Math.min(start, Math.max(0, safeLines.length - windowSize));
131
+ return safeLines.slice(start, start + windowSize);
132
+ }
133
+
123
134
  const visible = safeLines.slice(0, Math.max(1, maxLines - 1));
124
135
  visible.push({
125
136
  text: truncate(`... +${safeLines.length - visible.length} more`, width),
@@ -719,52 +730,208 @@ function getPanelTitles(flow, snapshot) {
719
730
  };
720
731
  }
721
732
 
733
+ function fileExists(filePath) {
734
+ try {
735
+ return fs.statSync(filePath).isFile();
736
+ } catch {
737
+ return false;
738
+ }
739
+ }
740
+
741
+ function listMarkdownFiles(dirPath) {
742
+ try {
743
+ return fs.readdirSync(dirPath, { withFileTypes: true })
744
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
745
+ .map((entry) => entry.name)
746
+ .sort((a, b) => a.localeCompare(b));
747
+ } catch {
748
+ return [];
749
+ }
750
+ }
751
+
752
+ function pushFileEntry(entries, seenPaths, candidate) {
753
+ if (!candidate || typeof candidate.path !== 'string' || typeof candidate.label !== 'string') {
754
+ return;
755
+ }
756
+
757
+ if (!fileExists(candidate.path)) {
758
+ return;
759
+ }
760
+
761
+ if (seenPaths.has(candidate.path)) {
762
+ return;
763
+ }
764
+
765
+ seenPaths.add(candidate.path);
766
+ entries.push({
767
+ path: candidate.path,
768
+ label: candidate.label,
769
+ scope: candidate.scope || 'other'
770
+ });
771
+ }
772
+
773
+ function collectFireRunFiles(run) {
774
+ if (!run || typeof run.folderPath !== 'string') {
775
+ return [];
776
+ }
777
+
778
+ const names = ['run.md'];
779
+ if (run.hasPlan) names.push('plan.md');
780
+ if (run.hasTestReport) names.push('test-report.md');
781
+ if (run.hasWalkthrough) names.push('walkthrough.md');
782
+
783
+ return names.map((fileName) => ({
784
+ label: `${run.id}/${fileName}`,
785
+ path: path.join(run.folderPath, fileName)
786
+ }));
787
+ }
788
+
789
+ function collectAidlcBoltFiles(bolt) {
790
+ if (!bolt || typeof bolt.path !== 'string') {
791
+ return [];
792
+ }
793
+
794
+ const fileNames = Array.isArray(bolt.files) && bolt.files.length > 0
795
+ ? bolt.files
796
+ : listMarkdownFiles(bolt.path);
797
+
798
+ return fileNames.map((fileName) => ({
799
+ label: `${bolt.id}/${fileName}`,
800
+ path: path.join(bolt.path, fileName)
801
+ }));
802
+ }
803
+
804
+ function collectSimpleSpecFiles(spec) {
805
+ if (!spec || typeof spec.path !== 'string') {
806
+ return [];
807
+ }
808
+
809
+ const names = [];
810
+ if (spec.hasRequirements) names.push('requirements.md');
811
+ if (spec.hasDesign) names.push('design.md');
812
+ if (spec.hasTasks) names.push('tasks.md');
813
+
814
+ return names.map((fileName) => ({
815
+ label: `${spec.name}/${fileName}`,
816
+ path: path.join(spec.path, fileName)
817
+ }));
818
+ }
819
+
722
820
  function getRunFileEntries(snapshot, flow) {
723
821
  const effectiveFlow = getEffectiveFlow(flow, snapshot);
822
+ const entries = [];
823
+ const seenPaths = new Set();
724
824
 
725
825
  if (effectiveFlow === 'aidlc') {
726
826
  const bolt = getCurrentBolt(snapshot);
727
- if (!bolt || typeof bolt.path !== 'string') {
728
- return [];
827
+ for (const file of collectAidlcBoltFiles(bolt)) {
828
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
729
829
  }
730
- const files = Array.isArray(bolt.files) ? bolt.files : [];
731
- return files.map((fileName) => ({
732
- name: fileName,
733
- path: path.join(bolt.path, fileName)
734
- }));
830
+
831
+ const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
832
+ for (const pendingBolt of pendingBolts) {
833
+ for (const file of collectAidlcBoltFiles(pendingBolt)) {
834
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
835
+ }
836
+ }
837
+
838
+ const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
839
+ for (const completedBolt of completedBolts) {
840
+ for (const file of collectAidlcBoltFiles(completedBolt)) {
841
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
842
+ }
843
+ }
844
+
845
+ const intentIds = new Set([
846
+ ...pendingBolts.map((item) => item?.intent).filter(Boolean),
847
+ ...completedBolts.map((item) => item?.intent).filter(Boolean)
848
+ ]);
849
+
850
+ for (const intentId of intentIds) {
851
+ const intentPath = path.join(snapshot?.rootPath || '', 'intents', intentId);
852
+ pushFileEntry(entries, seenPaths, {
853
+ label: `${intentId}/requirements.md`,
854
+ path: path.join(intentPath, 'requirements.md'),
855
+ scope: 'intent'
856
+ });
857
+ pushFileEntry(entries, seenPaths, {
858
+ label: `${intentId}/system-context.md`,
859
+ path: path.join(intentPath, 'system-context.md'),
860
+ scope: 'intent'
861
+ });
862
+ pushFileEntry(entries, seenPaths, {
863
+ label: `${intentId}/units.md`,
864
+ path: path.join(intentPath, 'units.md'),
865
+ scope: 'intent'
866
+ });
867
+ }
868
+ return entries;
735
869
  }
736
870
 
737
871
  if (effectiveFlow === 'simple') {
738
872
  const spec = getCurrentSpec(snapshot);
739
- if (!spec || typeof spec.path !== 'string') {
740
- return [];
873
+ for (const file of collectSimpleSpecFiles(spec)) {
874
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
741
875
  }
742
876
 
743
- const files = [];
744
- if (spec.hasRequirements) files.push('requirements.md');
745
- if (spec.hasDesign) files.push('design.md');
746
- if (spec.hasTasks) files.push('tasks.md');
877
+ const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
878
+ for (const pendingSpec of pendingSpecs) {
879
+ for (const file of collectSimpleSpecFiles(pendingSpec)) {
880
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'upcoming' });
881
+ }
882
+ }
747
883
 
748
- return files.map((fileName) => ({
749
- name: fileName,
750
- path: path.join(spec.path, fileName)
751
- }));
884
+ const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
885
+ for (const completedSpec of completedSpecs) {
886
+ for (const file of collectSimpleSpecFiles(completedSpec)) {
887
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
888
+ }
889
+ }
890
+
891
+ return entries;
752
892
  }
753
893
 
754
894
  const run = getCurrentRun(snapshot);
755
- if (!run || typeof run.folderPath !== 'string') {
756
- return [];
895
+ for (const file of collectFireRunFiles(run)) {
896
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
757
897
  }
758
898
 
759
- const files = ['run.md'];
760
- if (run.hasPlan) files.push('plan.md');
761
- if (run.hasTestReport) files.push('test-report.md');
762
- if (run.hasWalkthrough) files.push('walkthrough.md');
899
+ const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
900
+ for (const pendingItem of pendingItems) {
901
+ pushFileEntry(entries, seenPaths, {
902
+ label: `${pendingItem?.intentId || 'intent'}/${pendingItem?.id || 'work-item'}.md`,
903
+ path: pendingItem?.filePath,
904
+ scope: 'upcoming'
905
+ });
763
906
 
764
- return files.map((fileName) => ({
765
- name: fileName,
766
- path: path.join(run.folderPath, fileName)
767
- }));
907
+ if (pendingItem?.intentId) {
908
+ pushFileEntry(entries, seenPaths, {
909
+ label: `${pendingItem.intentId}/brief.md`,
910
+ path: path.join(snapshot?.rootPath || '', 'intents', pendingItem.intentId, 'brief.md'),
911
+ scope: 'intent'
912
+ });
913
+ }
914
+ }
915
+
916
+ const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
917
+ for (const completedRun of completedRuns) {
918
+ for (const file of collectFireRunFiles(completedRun)) {
919
+ pushFileEntry(entries, seenPaths, { ...file, scope: 'completed' });
920
+ }
921
+ }
922
+
923
+ const completedIntents = Array.isArray(snapshot?.intents)
924
+ ? snapshot.intents.filter((intent) => intent?.status === 'completed')
925
+ : [];
926
+ for (const intent of completedIntents) {
927
+ pushFileEntry(entries, seenPaths, {
928
+ label: `${intent.id}/brief.md`,
929
+ path: path.join(snapshot?.rootPath || '', 'intents', intent.id, 'brief.md'),
930
+ scope: 'intent'
931
+ });
932
+ }
933
+
934
+ return entries;
768
935
  }
769
936
 
770
937
  function clampIndex(value, length) {
@@ -778,13 +945,15 @@ function clampIndex(value, length) {
778
945
  }
779
946
 
780
947
  function getNoFileMessage(flow) {
781
- if (flow === 'aidlc') {
782
- return 'No bolt files (no active bolt)';
783
- }
784
- if (flow === 'simple') {
785
- return 'No spec files (no active spec)';
786
- }
787
- return 'No run files (no active run)';
948
+ return `No selectable files for ${String(flow || 'flow').toUpperCase()}`;
949
+ }
950
+
951
+ function formatScope(scope) {
952
+ if (scope === 'active') return 'ACTIVE';
953
+ if (scope === 'upcoming') return 'UPNEXT';
954
+ if (scope === 'completed') return 'DONE';
955
+ if (scope === 'intent') return 'INTENT';
956
+ return 'FILE';
788
957
  }
789
958
 
790
959
  function buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, flow) {
@@ -796,10 +965,12 @@ function buildSelectableRunFileLines(fileEntries, selectedIndex, icons, width, f
796
965
  return fileEntries.map((file, index) => {
797
966
  const isSelected = index === clampedIndex;
798
967
  const prefix = isSelected ? '>' : ' ';
968
+ const scope = formatScope(file.scope);
799
969
  return {
800
- text: truncate(`${prefix} ${icons.runFile} ${file.name}`, width),
970
+ text: truncate(`${prefix} ${icons.runFile} [${scope}] ${file.label}`, width),
801
971
  color: isSelected ? 'cyan' : undefined,
802
- bold: isSelected
972
+ bold: isSelected,
973
+ selected: isSelected
803
974
  };
804
975
  });
805
976
  }
@@ -872,7 +1043,7 @@ function buildPreviewLines(fileEntry, width, scrollOffset) {
872
1043
  content = fs.readFileSync(fileEntry.path, 'utf8');
873
1044
  } catch (error) {
874
1045
  return [{
875
- text: truncate(`Unable to read ${fileEntry.name}: ${error.message}`, width),
1046
+ text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
876
1047
  color: 'red',
877
1048
  bold: false
878
1049
  }];
@@ -1439,7 +1610,7 @@ function createDashboardApp(deps) {
1439
1610
  previewOpen
1440
1611
  ? {
1441
1612
  key: 'preview',
1442
- title: `Preview: ${selectedFile?.name || 'unknown'}`,
1613
+ title: `Preview: ${selectedFile?.label || 'unknown'}`,
1443
1614
  lines: previewLines,
1444
1615
  borderColor: 'magenta'
1445
1616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {