specsmd 0.1.53 → 0.1.55

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.
@@ -273,7 +273,7 @@ function readUntrackedFileDiff(repoRoot, absolutePath) {
273
273
  }
274
274
 
275
275
  const result = runGit(
276
- ['-c', 'color.ui=always', '--no-pager', 'diff', '--no-index', '--', '/dev/null', absolutePath],
276
+ ['-c', 'color.ui=false', '--no-pager', 'diff', '--no-index', '--', '/dev/null', absolutePath],
277
277
  repoRoot,
278
278
  { acceptedStatuses: [0, 1] }
279
279
  );
@@ -305,7 +305,7 @@ function loadGitDiffPreview(changeEntry) {
305
305
  }
306
306
  }
307
307
 
308
- const args = ['-c', 'color.ui=always', '--no-pager', 'diff'];
308
+ const args = ['-c', 'color.ui=false', '--no-pager', 'diff'];
309
309
  if (bucket === 'staged') {
310
310
  args.push('--cached');
311
311
  }
@@ -1172,6 +1172,215 @@ function getGitChangesSnapshot(snapshot) {
1172
1172
  };
1173
1173
  }
1174
1174
 
1175
+ function readGitCommandLines(repoRoot, args, options = {}) {
1176
+ if (typeof repoRoot !== 'string' || repoRoot.trim() === '' || !Array.isArray(args) || args.length === 0) {
1177
+ return [];
1178
+ }
1179
+
1180
+ const acceptedStatuses = Array.isArray(options.acceptedStatuses) && options.acceptedStatuses.length > 0
1181
+ ? options.acceptedStatuses
1182
+ : [0];
1183
+
1184
+ const result = spawnSync('git', args, {
1185
+ cwd: repoRoot,
1186
+ encoding: 'utf8',
1187
+ maxBuffer: 8 * 1024 * 1024
1188
+ });
1189
+
1190
+ if (result.error) {
1191
+ return [];
1192
+ }
1193
+
1194
+ if (typeof result.status === 'number' && !acceptedStatuses.includes(result.status)) {
1195
+ return [];
1196
+ }
1197
+
1198
+ const lines = String(result.stdout || '')
1199
+ .split(/\r?\n/)
1200
+ .map((line) => line.trim())
1201
+ .filter(Boolean);
1202
+
1203
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : null;
1204
+ if (limit == null || lines.length <= limit) {
1205
+ return lines;
1206
+ }
1207
+ return lines.slice(0, limit);
1208
+ }
1209
+
1210
+ function buildGitStatusPanelLines(snapshot) {
1211
+ const git = getGitChangesSnapshot(snapshot);
1212
+ if (!git.available) {
1213
+ return [{
1214
+ text: 'Repository unavailable in selected worktree',
1215
+ color: 'red',
1216
+ bold: true
1217
+ }];
1218
+ }
1219
+
1220
+ const tracking = git.upstream
1221
+ ? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
1222
+ : 'no upstream';
1223
+
1224
+ return [
1225
+ {
1226
+ text: `branch: ${git.branch}${git.detached ? ' [detached]' : ''}`,
1227
+ color: 'green',
1228
+ bold: true
1229
+ },
1230
+ {
1231
+ text: `tracking: ${tracking}`,
1232
+ color: 'gray',
1233
+ bold: false
1234
+ },
1235
+ {
1236
+ text: `changes: ${git.counts.total || 0} total`,
1237
+ color: 'gray',
1238
+ bold: false
1239
+ },
1240
+ {
1241
+ text: `staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0}`,
1242
+ color: 'yellow',
1243
+ bold: false
1244
+ },
1245
+ {
1246
+ text: `untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
1247
+ color: 'yellow',
1248
+ bold: false
1249
+ }
1250
+ ];
1251
+ }
1252
+
1253
+ function buildGitBranchesPanelLines(snapshot) {
1254
+ const git = getGitChangesSnapshot(snapshot);
1255
+ if (!git.available) {
1256
+ return [{
1257
+ text: 'No branch list (git unavailable)',
1258
+ color: 'gray',
1259
+ bold: false
1260
+ }];
1261
+ }
1262
+
1263
+ const branchLines = readGitCommandLines(git.rootPath, [
1264
+ '-c',
1265
+ 'color.ui=false',
1266
+ 'for-each-ref',
1267
+ '--sort=-committerdate',
1268
+ '--format=%(if)%(HEAD)%(then)*%(else) %(end) %(refname:short)',
1269
+ 'refs/heads'
1270
+ ], { limit: 8 });
1271
+
1272
+ const lines = [{
1273
+ text: 'Local branches · Remotes · Tags',
1274
+ color: 'cyan',
1275
+ bold: true
1276
+ }];
1277
+
1278
+ if (branchLines.length === 0) {
1279
+ lines.push({
1280
+ text: 'No local branches found',
1281
+ color: 'gray',
1282
+ bold: false
1283
+ });
1284
+ return lines;
1285
+ }
1286
+
1287
+ for (const line of branchLines) {
1288
+ const isCurrent = line.startsWith('*');
1289
+ lines.push({
1290
+ text: line,
1291
+ color: isCurrent ? 'green' : 'gray',
1292
+ bold: isCurrent
1293
+ });
1294
+ }
1295
+
1296
+ return lines;
1297
+ }
1298
+
1299
+ function buildGitCommitsPanelLines(snapshot) {
1300
+ const git = getGitChangesSnapshot(snapshot);
1301
+ if (!git.available) {
1302
+ return [{
1303
+ text: 'No commit log (git unavailable)',
1304
+ color: 'gray',
1305
+ bold: false
1306
+ }];
1307
+ }
1308
+
1309
+ const reflogLines = readGitCommandLines(git.rootPath, [
1310
+ '-c',
1311
+ 'color.ui=false',
1312
+ 'reflog',
1313
+ '--date=relative',
1314
+ '--pretty=format:%h %gs'
1315
+ ], { limit: 8 });
1316
+
1317
+ const lines = [{
1318
+ text: 'Commits · Reflog',
1319
+ color: 'cyan',
1320
+ bold: true
1321
+ }];
1322
+
1323
+ if (reflogLines.length === 0) {
1324
+ lines.push({
1325
+ text: 'No reflog entries',
1326
+ color: 'gray',
1327
+ bold: false
1328
+ });
1329
+ return lines;
1330
+ }
1331
+
1332
+ lines.push(...reflogLines.map((line) => ({
1333
+ text: line,
1334
+ color: 'gray',
1335
+ bold: false
1336
+ })));
1337
+ return lines;
1338
+ }
1339
+
1340
+ function buildGitStashPanelLines(snapshot) {
1341
+ const git = getGitChangesSnapshot(snapshot);
1342
+ if (!git.available) {
1343
+ return [{
1344
+ text: 'No stash info (git unavailable)',
1345
+ color: 'gray',
1346
+ bold: false
1347
+ }];
1348
+ }
1349
+
1350
+ const stashLines = readGitCommandLines(git.rootPath, [
1351
+ '-c',
1352
+ 'color.ui=false',
1353
+ 'stash',
1354
+ 'list',
1355
+ '--pretty=format:%gd %s'
1356
+ ], {
1357
+ acceptedStatuses: [0],
1358
+ limit: 4
1359
+ });
1360
+
1361
+ const lines = [{
1362
+ text: 'Stash',
1363
+ color: 'cyan',
1364
+ bold: true
1365
+ }];
1366
+
1367
+ if (stashLines.length === 0) {
1368
+ lines.push({
1369
+ text: 'No stashes',
1370
+ color: 'gray',
1371
+ bold: false
1372
+ });
1373
+ return lines;
1374
+ }
1375
+
1376
+ lines.push(...stashLines.map((line) => ({
1377
+ text: line,
1378
+ color: 'gray',
1379
+ bold: false
1380
+ })));
1381
+ return lines;
1382
+ }
1383
+
1175
1384
  function getDashboardWorktreeMeta(snapshot) {
1176
1385
  if (!snapshot || typeof snapshot !== 'object') {
1177
1386
  return null;
@@ -1349,7 +1558,7 @@ function getSectionOrderForView(view, options = {}) {
1349
1558
  return ['standards', 'stats', 'warnings', 'error-details'];
1350
1559
  }
1351
1560
  if (view === 'git') {
1352
- return ['git-changes'];
1561
+ return ['git-status', 'git-changes', 'git-branches', 'git-commits', 'git-stash', 'git-diff'];
1353
1562
  }
1354
1563
  const sections = [];
1355
1564
  if (includeWorktrees) {
@@ -2420,6 +2629,21 @@ function rowToFileEntry(row) {
2420
2629
  };
2421
2630
  }
2422
2631
 
2632
+ function firstFileEntryFromRows(rows) {
2633
+ if (!Array.isArray(rows) || rows.length === 0) {
2634
+ return null;
2635
+ }
2636
+
2637
+ for (const row of rows) {
2638
+ const entry = rowToFileEntry(row);
2639
+ if (entry) {
2640
+ return entry;
2641
+ }
2642
+ }
2643
+
2644
+ return null;
2645
+ }
2646
+
2423
2647
  function rowToWorktreeId(row) {
2424
2648
  if (!row || typeof row.key !== 'string') {
2425
2649
  return null;
@@ -2528,7 +2752,7 @@ function buildQuickHelpText(view, options = {}) {
2528
2752
  }
2529
2753
  parts.push('a current', 'f files');
2530
2754
  } else if (view === 'git') {
2531
- parts.push('d changes', 'c/A/p/P git(soon)');
2755
+ parts.push('6 status', '7 files', '8 branches', '9 commits', '0 stash', '- diff');
2532
2756
  }
2533
2757
  parts.push(`tab1 ${activeLabel}`);
2534
2758
 
@@ -2540,7 +2764,7 @@ function buildQuickHelpText(view, options = {}) {
2540
2764
  return parts.join(' | ');
2541
2765
  }
2542
2766
 
2543
- function buildLazyGitCommandStrip(view, options = {}) {
2767
+ function buildGitCommandStrip(view, options = {}) {
2544
2768
  const {
2545
2769
  hasWorktrees = false,
2546
2770
  previewOpen = false
@@ -2560,7 +2784,7 @@ function buildLazyGitCommandStrip(view, options = {}) {
2560
2784
  } else if (view === 'health') {
2561
2785
  parts.push('s standards', 't stats', 'w warnings');
2562
2786
  } else if (view === 'git') {
2563
- parts.push('d changes', 'space preview', 'c commit (soon)', 'A amend (soon)', 'p push (soon)', 'P pull (soon)');
2787
+ parts.push('6 status', '7 files', '8 branches', '9 commits', '0 stash', '- diff', 'space preview');
2564
2788
  }
2565
2789
 
2566
2790
  if (previewOpen) {
@@ -2573,7 +2797,7 @@ function buildLazyGitCommandStrip(view, options = {}) {
2573
2797
  return parts.join(' | ');
2574
2798
  }
2575
2799
 
2576
- function buildLazyGitCommandLogLine(options = {}) {
2800
+ function buildGitCommandLogLine(options = {}) {
2577
2801
  const {
2578
2802
  statusLine = '',
2579
2803
  activeFlow = 'fire',
@@ -2650,7 +2874,7 @@ function buildHelpOverlayLines(options = {}) {
2650
2874
  { text: '', color: undefined, bold: false },
2651
2875
  { text: 'Tab 5 Git Changes', color: 'yellow', bold: true },
2652
2876
  'select changed files and preview diffs',
2653
- 'commit/push/pull shortcuts are shown in footer for LazyGit-style hierarchy',
2877
+ '6 status | 7 files | 8 branches | 9 commits | 0 stash | - diff',
2654
2878
  { text: '', color: undefined, bold: false },
2655
2879
  { text: `Current view: ${String(view || 'runs').toUpperCase()}`, color: 'gray', bold: false }
2656
2880
  );
@@ -3277,32 +3501,11 @@ function createDashboardApp(deps) {
3277
3501
  const gitRows = shouldHydrateSecondaryTabs
3278
3502
  ? (() => {
3279
3503
  const git = getGitChangesSnapshot(snapshot);
3280
- const tracking = git.upstream
3281
- ? `${git.upstream} (${git.ahead > 0 ? `ahead ${git.ahead}` : 'ahead 0'}, ${git.behind > 0 ? `behind ${git.behind}` : 'behind 0'})`
3282
- : 'no upstream';
3283
- const headerRows = [{
3284
- kind: 'info',
3285
- key: 'git:branch',
3286
- label: git.available
3287
- ? `branch ${git.branch}${git.detached ? ' [detached]' : ''} | ${tracking}`
3288
- : 'git: repository unavailable in selected worktree',
3289
- color: git.available ? 'cyan' : 'red',
3290
- bold: true,
3291
- selectable: false
3292
- }, {
3293
- kind: 'info',
3294
- key: 'git:counts',
3295
- label: `changes ${git.counts.total || 0} | staged ${git.counts.staged || 0} | unstaged ${git.counts.unstaged || 0} | untracked ${git.counts.untracked || 0} | conflicts ${git.counts.conflicted || 0}`,
3296
- color: 'gray',
3297
- bold: false,
3298
- selectable: false
3299
- }];
3300
- const groups = toExpandableRows(
3504
+ return toExpandableRows(
3301
3505
  buildGitChangeGroups(snapshot),
3302
3506
  git.available ? 'Working tree clean' : 'No git changes',
3303
3507
  expandedGroups
3304
3508
  );
3305
- return [...headerRows, ...groups];
3306
3509
  })()
3307
3510
  : toLoadingRows('Loading git changes...', 'git-loading');
3308
3511
 
@@ -3338,6 +3541,9 @@ function createDashboardApp(deps) {
3338
3541
  const focusedIndex = selectionBySection[focusedSection] || 0;
3339
3542
  const selectedFocusedRow = getSelectedRow(focusedRows, focusedIndex);
3340
3543
  const selectedFocusedFile = rowToFileEntry(selectedFocusedRow);
3544
+ const selectedGitRow = getSelectedRow(gitRows, selectionBySection['git-changes'] || 0);
3545
+ const selectedGitFile = rowToFileEntry(selectedGitRow);
3546
+ const firstGitFile = firstFileEntryFromRows(gitRows);
3341
3547
 
3342
3548
  const refresh = useCallback(async (overrideSelectedWorktreeId = null) => {
3343
3549
  const now = new Date().toISOString();
@@ -3691,24 +3897,34 @@ function createDashboardApp(deps) {
3691
3897
  return;
3692
3898
  }
3693
3899
  } else if (ui.view === 'git') {
3694
- if (input === 'd') {
3900
+ if (input === '6') {
3901
+ setSectionFocus((previous) => ({ ...previous, git: 'git-status' }));
3902
+ setPaneFocus('main');
3903
+ return;
3904
+ }
3905
+ if (input === '7') {
3695
3906
  setSectionFocus((previous) => ({ ...previous, git: 'git-changes' }));
3907
+ setPaneFocus('main');
3696
3908
  return;
3697
3909
  }
3698
- if (input === 'c') {
3699
- setStatusLine('Git commit action is not wired yet (footer mirrors LazyGit command hierarchy).');
3910
+ if (input === '8') {
3911
+ setSectionFocus((previous) => ({ ...previous, git: 'git-branches' }));
3912
+ setPaneFocus('main');
3700
3913
  return;
3701
3914
  }
3702
- if (input === 'A') {
3703
- setStatusLine('Git amend action is not wired yet (footer mirrors LazyGit command hierarchy).');
3915
+ if (input === '9') {
3916
+ setSectionFocus((previous) => ({ ...previous, git: 'git-commits' }));
3917
+ setPaneFocus('main');
3704
3918
  return;
3705
3919
  }
3706
- if (input === 'p') {
3707
- setStatusLine('Git push action is not wired yet (footer mirrors LazyGit command hierarchy).');
3920
+ if (input === '0') {
3921
+ setSectionFocus((previous) => ({ ...previous, git: 'git-stash' }));
3922
+ setPaneFocus('main');
3708
3923
  return;
3709
3924
  }
3710
- if (input === 'P') {
3711
- setStatusLine('Git pull action is not wired yet (footer mirrors LazyGit command hierarchy).');
3925
+ if (input === '-') {
3926
+ setSectionFocus((previous) => ({ ...previous, git: 'git-diff' }));
3927
+ setPaneFocus('main');
3712
3928
  return;
3713
3929
  }
3714
3930
  }
@@ -4066,6 +4282,16 @@ function createDashboardApp(deps) {
4066
4282
  fullDocument: overlayPreviewOpen
4067
4283
  })
4068
4284
  : [];
4285
+ const gitInlineDiffTarget = selectedGitFile || firstGitFile || previewTarget || null;
4286
+ const gitInlineDiffLines = ui.view === 'git'
4287
+ ? buildPreviewLines(gitInlineDiffTarget, compactWidth, previewOpen && paneFocus === 'preview' ? previewScroll : 0, {
4288
+ fullDocument: false
4289
+ })
4290
+ : [];
4291
+ const gitStatusPanelLines = ui.view === 'git' ? buildGitStatusPanelLines(snapshot) : [];
4292
+ const gitBranchesPanelLines = ui.view === 'git' ? buildGitBranchesPanelLines(snapshot) : [];
4293
+ const gitCommitsPanelLines = ui.view === 'git' ? buildGitCommitsPanelLines(snapshot) : [];
4294
+ const gitStashPanelLines = ui.view === 'git' ? buildGitStashPanelLines(snapshot) : [];
4069
4295
 
4070
4296
  const shortcutsOverlayLines = buildHelpOverlayLines({
4071
4297
  view: ui.view,
@@ -4083,11 +4309,11 @@ function createDashboardApp(deps) {
4083
4309
  availableFlowCount: availableFlowIds.length,
4084
4310
  hasWorktrees: worktreeSectionEnabled
4085
4311
  });
4086
- const commandStripText = buildLazyGitCommandStrip(ui.view, {
4312
+ const commandStripText = buildGitCommandStrip(ui.view, {
4087
4313
  hasWorktrees: worktreeSectionEnabled,
4088
4314
  previewOpen
4089
4315
  });
4090
- const commandLogLine = buildLazyGitCommandLogLine({
4316
+ const commandLogLine = buildGitCommandLogLine({
4091
4317
  statusLine,
4092
4318
  activeFlow,
4093
4319
  watchEnabled,
@@ -4173,11 +4399,41 @@ function createDashboardApp(deps) {
4173
4399
  }
4174
4400
  } else if (ui.view === 'git') {
4175
4401
  panelCandidates = [
4402
+ {
4403
+ key: 'git-status',
4404
+ title: '[6]-Status',
4405
+ lines: gitStatusPanelLines,
4406
+ borderColor: 'green'
4407
+ },
4176
4408
  {
4177
4409
  key: 'git-changes',
4178
- title: panelTitles.git || 'Git Changes',
4410
+ title: '[7]-Files',
4179
4411
  lines: sectionLines['git-changes'],
4180
4412
  borderColor: 'yellow'
4413
+ },
4414
+ {
4415
+ key: 'git-branches',
4416
+ title: '[8]-Local branches',
4417
+ lines: gitBranchesPanelLines,
4418
+ borderColor: 'magenta'
4419
+ },
4420
+ {
4421
+ key: 'git-commits',
4422
+ title: '[9]-Commits',
4423
+ lines: gitCommitsPanelLines,
4424
+ borderColor: 'cyan'
4425
+ },
4426
+ {
4427
+ key: 'git-stash',
4428
+ title: '[0]-Stash',
4429
+ lines: gitStashPanelLines,
4430
+ borderColor: 'blue'
4431
+ },
4432
+ {
4433
+ key: 'git-diff',
4434
+ title: '[-]-Unstaged changes',
4435
+ lines: gitInlineDiffLines,
4436
+ borderColor: 'yellow'
4181
4437
  }
4182
4438
  ];
4183
4439
  } else {
@@ -4214,7 +4470,7 @@ function createDashboardApp(deps) {
4214
4470
  }
4215
4471
  }
4216
4472
 
4217
- if (!ui.showHelp && previewOpen && !overlayPreviewOpen) {
4473
+ if (!ui.showHelp && previewOpen && !overlayPreviewOpen && ui.view !== 'git') {
4218
4474
  panelCandidates.push({
4219
4475
  key: 'preview',
4220
4476
  title: `Preview: ${effectivePreviewTarget?.label || 'unknown'}`,
@@ -4225,7 +4481,13 @@ function createDashboardApp(deps) {
4225
4481
 
4226
4482
  if (ultraCompact) {
4227
4483
  if (previewOpen) {
4228
- panelCandidates = panelCandidates.filter((panel) => panel && (panel.key === focusedSection || panel.key === 'preview'));
4484
+ panelCandidates = panelCandidates.filter((panel) =>
4485
+ panel && (
4486
+ panel.key === focusedSection
4487
+ || panel.key === 'preview'
4488
+ || (ui.view === 'git' && panel.key === 'git-diff')
4489
+ )
4490
+ );
4229
4491
  } else {
4230
4492
  const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4231
4493
  panelCandidates = [focusedPanel || panelCandidates[0]];
@@ -4233,7 +4495,8 @@ function createDashboardApp(deps) {
4233
4495
  }
4234
4496
 
4235
4497
  const panels = allocateSingleColumnPanels(panelCandidates, contentRowsBudget);
4236
- const lazyGitHierarchyLayout = !ui.showHelp
4498
+ const gitHierarchyLayout = ui.view === 'git'
4499
+ && !ui.showHelp
4237
4500
  && !worktreeOverlayOpen
4238
4501
  && !overlayPreviewOpen
4239
4502
  && !ultraCompact
@@ -4253,10 +4516,11 @@ function createDashboardApp(deps) {
4253
4516
  });
4254
4517
 
4255
4518
  let contentNode;
4256
- if (lazyGitHierarchyLayout) {
4257
- const preferredRightPanel = previewOpen && !overlayPreviewOpen
4258
- ? panelCandidates.find((panel) => panel?.key === 'preview')
4259
- : null;
4519
+ if (gitHierarchyLayout) {
4520
+ const preferredRightPanel = panelCandidates.find((panel) => panel?.key === 'git-diff')
4521
+ || (previewOpen && !overlayPreviewOpen
4522
+ ? panelCandidates.find((panel) => panel?.key === 'preview')
4523
+ : null);
4260
4524
  const focusedPanel = panelCandidates.find((panel) => panel?.key === focusedSection);
4261
4525
  const rightPanelBase = preferredRightPanel
4262
4526
  || focusedPanel
@@ -4307,7 +4571,9 @@ function createDashboardApp(deps) {
4307
4571
  dense: densePanels,
4308
4572
  focused: rightPanel.key === 'preview'
4309
4573
  ? paneFocus === 'preview'
4310
- : (paneFocus === 'main' && rightPanel.key === focusedSection)
4574
+ : (rightPanel.key === 'git-diff'
4575
+ ? true
4576
+ : (paneFocus === 'main' && rightPanel.key === focusedSection))
4311
4577
  })
4312
4578
  )
4313
4579
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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": {