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.
- package/lib/dashboard/tui/app.js +211 -40
- package/package.json +1 -1
package/lib/dashboard/tui/app.js
CHANGED
|
@@ -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
|
-
|
|
728
|
-
|
|
827
|
+
for (const file of collectAidlcBoltFiles(bolt)) {
|
|
828
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
729
829
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
740
|
-
|
|
873
|
+
for (const file of collectSimpleSpecFiles(spec)) {
|
|
874
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
741
875
|
}
|
|
742
876
|
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
756
|
-
|
|
895
|
+
for (const file of collectFireRunFiles(run)) {
|
|
896
|
+
pushFileEntry(entries, seenPaths, { ...file, scope: 'active' });
|
|
757
897
|
}
|
|
758
898
|
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
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": {
|