gsd-pi 2.31.2-dev.2453512 → 2.31.2-dev.392f611
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/dist/cli.js +5 -5
- package/dist/resources/extensions/gsd/auto-start.ts +4 -2
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +4 -2
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
} from './metrics.js';
|
|
19
19
|
import { loadAllCaptures, countPendingCaptures } from './captures.js';
|
|
20
20
|
import { loadEffectiveGSDPreferences } from './preferences.js';
|
|
21
|
+
import { runProviderChecks, type ProviderCheckResult } from './doctor-providers.js';
|
|
22
|
+
import { generateSkillHealthReport } from './skill-health.js';
|
|
23
|
+
import { runEnvironmentChecks, type EnvironmentCheckResult } from './doctor-environment.js';
|
|
21
24
|
|
|
22
25
|
import type { Phase } from './types.js';
|
|
23
26
|
import type { CaptureEntry } from './captures.js';
|
|
@@ -142,6 +145,22 @@ export interface CapturesInfo {
|
|
|
142
145
|
totalCount: number;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
export interface ProviderStatusSummary {
|
|
149
|
+
name: string;
|
|
150
|
+
label: string;
|
|
151
|
+
category: string;
|
|
152
|
+
ok: boolean;
|
|
153
|
+
required: boolean;
|
|
154
|
+
message: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface SkillSummaryInfo {
|
|
158
|
+
total: number;
|
|
159
|
+
warningCount: number;
|
|
160
|
+
criticalCount: number;
|
|
161
|
+
topIssue: string | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
export interface HealthInfo {
|
|
146
165
|
budgetCeiling: number | undefined;
|
|
147
166
|
tokenProfile: string;
|
|
@@ -152,6 +171,9 @@ export interface HealthInfo {
|
|
|
152
171
|
toolCalls: number;
|
|
153
172
|
assistantMessages: number;
|
|
154
173
|
userMessages: number;
|
|
174
|
+
providers: ProviderStatusSummary[];
|
|
175
|
+
skillSummary: SkillSummaryInfo;
|
|
176
|
+
environmentIssues: import("./doctor-environment.js").EnvironmentCheckResult[];
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
export interface VisualizerData {
|
|
@@ -538,7 +560,7 @@ function loadKnowledge(basePath: string): KnowledgeInfo {
|
|
|
538
560
|
|
|
539
561
|
// ─── Health Loader ────────────────────────────────────────────────────────────
|
|
540
562
|
|
|
541
|
-
function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthInfo {
|
|
563
|
+
function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null, basePath: string): HealthInfo {
|
|
542
564
|
const prefs = loadEffectiveGSDPreferences();
|
|
543
565
|
const budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
544
566
|
const tokenProfile = prefs?.preferences?.token_profile ?? 'standard';
|
|
@@ -553,6 +575,39 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
|
|
|
553
575
|
const tierBreakdown = aggregateByTier(units);
|
|
554
576
|
const tierSavingsLine = formatTierSavings(units);
|
|
555
577
|
|
|
578
|
+
// Provider checks — fast (auth.json + env vars only, no network)
|
|
579
|
+
let providers: ProviderStatusSummary[] = [];
|
|
580
|
+
try {
|
|
581
|
+
providers = runProviderChecks().map((r: ProviderCheckResult) => ({
|
|
582
|
+
name: r.name,
|
|
583
|
+
label: r.label,
|
|
584
|
+
category: r.category,
|
|
585
|
+
ok: r.status === "ok" || r.status === "unconfigured",
|
|
586
|
+
required: r.required,
|
|
587
|
+
message: r.message,
|
|
588
|
+
}));
|
|
589
|
+
} catch { /* non-fatal */ }
|
|
590
|
+
|
|
591
|
+
// Skill health summary
|
|
592
|
+
let skillSummary: SkillSummaryInfo = { total: 0, warningCount: 0, criticalCount: 0, topIssue: null };
|
|
593
|
+
try {
|
|
594
|
+
const report = generateSkillHealthReport(basePath);
|
|
595
|
+
const warnings = report.suggestions.filter(s => s.severity === "warning");
|
|
596
|
+
const criticals = report.suggestions.filter(s => s.severity === "critical");
|
|
597
|
+
skillSummary = {
|
|
598
|
+
total: report.skills.length,
|
|
599
|
+
warningCount: warnings.length,
|
|
600
|
+
criticalCount: criticals.length,
|
|
601
|
+
topIssue: report.suggestions[0]?.message ?? null,
|
|
602
|
+
};
|
|
603
|
+
} catch { /* non-fatal */ }
|
|
604
|
+
|
|
605
|
+
// Environment issues (from doctor-environment.ts, #1221)
|
|
606
|
+
let environmentIssues: EnvironmentCheckResult[] = [];
|
|
607
|
+
try {
|
|
608
|
+
environmentIssues = runEnvironmentChecks(basePath).filter(r => r.status !== "ok");
|
|
609
|
+
} catch { /* non-fatal */ }
|
|
610
|
+
|
|
556
611
|
return {
|
|
557
612
|
budgetCeiling,
|
|
558
613
|
tokenProfile,
|
|
@@ -563,6 +618,9 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
|
|
|
563
618
|
toolCalls: totals?.toolCalls ?? 0,
|
|
564
619
|
assistantMessages: totals?.assistantMessages ?? 0,
|
|
565
620
|
userMessages: totals?.userMessages ?? 0,
|
|
621
|
+
providers,
|
|
622
|
+
skillSummary,
|
|
623
|
+
environmentIssues,
|
|
566
624
|
};
|
|
567
625
|
}
|
|
568
626
|
|
|
@@ -780,7 +838,7 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
|
|
|
780
838
|
totalCount: allCaptures.length,
|
|
781
839
|
};
|
|
782
840
|
|
|
783
|
-
const health = loadHealth(units, totals);
|
|
841
|
+
const health = loadHealth(units, totals, basePath);
|
|
784
842
|
const stats = buildVisualizerStats(milestones, changelog.entries);
|
|
785
843
|
const discussion = loadDiscussionState(basePath, milestones);
|
|
786
844
|
|
|
@@ -1113,5 +1113,59 @@ export function renderHealthView(
|
|
|
1113
1113
|
lines.push(` Tool calls: ${th.fg("text", String(health.toolCalls))}`);
|
|
1114
1114
|
lines.push(` Messages: ${th.fg("text", String(health.assistantMessages))} sent / ${th.fg("text", String(health.userMessages))} received`);
|
|
1115
1115
|
|
|
1116
|
+
// Environment section — issues only (from doctor-environment.ts, #1221)
|
|
1117
|
+
if (health.environmentIssues?.length > 0) {
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
lines.push(th.fg("accent", th.bold("Environment")));
|
|
1120
|
+
lines.push("");
|
|
1121
|
+
for (const r of health.environmentIssues) {
|
|
1122
|
+
const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
|
|
1123
|
+
lines.push(` ${icon} ${th.fg("text", r.message)}`);
|
|
1124
|
+
if (r.detail) lines.push(` ${th.fg("dim", r.detail)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Providers section
|
|
1129
|
+
if (health.providers?.length > 0) {
|
|
1130
|
+
lines.push("");
|
|
1131
|
+
lines.push(th.fg("accent", th.bold("Providers")));
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
const categoryOrder = ["llm", "remote", "search", "tool"];
|
|
1134
|
+
const categoryLabels: Record<string, string> = { llm: "LLM", remote: "Notifications", search: "Search", tool: "Tools" };
|
|
1135
|
+
const grouped = new Map<string, typeof health.providers>();
|
|
1136
|
+
for (const p of health.providers) {
|
|
1137
|
+
const cat = p.category;
|
|
1138
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1139
|
+
grouped.get(cat)!.push(p);
|
|
1140
|
+
}
|
|
1141
|
+
for (const cat of categoryOrder) {
|
|
1142
|
+
const items = grouped.get(cat);
|
|
1143
|
+
if (!items || items.length === 0) continue;
|
|
1144
|
+
lines.push(` ${th.fg("dim", categoryLabels[cat] ?? cat)}`);
|
|
1145
|
+
for (const p of items) {
|
|
1146
|
+
const icon = p.ok ? th.fg("success", "✓") : th.fg("error", "✗");
|
|
1147
|
+
const msg = p.ok ? th.fg("dim", p.message) : th.fg("text", p.message);
|
|
1148
|
+
lines.push(` ${icon} ${msg}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Skills section
|
|
1154
|
+
if (health.skillSummary?.total > 0) {
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push(th.fg("accent", th.bold("Skills")));
|
|
1157
|
+
lines.push("");
|
|
1158
|
+
const { total, warningCount, criticalCount, topIssue } = health.skillSummary;
|
|
1159
|
+
const issueColor = criticalCount > 0 ? "error" : warningCount > 0 ? "warning" : "success";
|
|
1160
|
+
const issueTag = criticalCount > 0
|
|
1161
|
+
? `${criticalCount} critical`
|
|
1162
|
+
: warningCount > 0
|
|
1163
|
+
? `${warningCount} warning${warningCount > 1 ? "s" : ""}`
|
|
1164
|
+
: "all healthy";
|
|
1165
|
+
lines.push(` ${th.fg("text", String(total))} skills tracked · ${th.fg(issueColor, issueTag)}`);
|
|
1166
|
+
if (topIssue) lines.push(` ${th.fg("warning", "⚠")} ${th.fg("dim", topIssue)}`);
|
|
1167
|
+
lines.push(` ${th.fg("dim", "→ /gsd skill-health for full report")}`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1116
1170
|
return lines;
|
|
1117
1171
|
}
|
package/dist/worktree-cli.d.ts
CHANGED
|
@@ -12,7 +12,43 @@
|
|
|
12
12
|
* On session exit (via session_shutdown event), auto-commits dirty work
|
|
13
13
|
* so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
|
|
14
14
|
* when a session was launched via -w.
|
|
15
|
+
*
|
|
16
|
+
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
|
|
17
|
+
* We use createJiti() here because this module is compiled by tsc but imports
|
|
18
|
+
* from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
|
|
15
19
|
*/
|
|
20
|
+
interface ExtensionModules {
|
|
21
|
+
createWorktree: (basePath: string, name: string) => {
|
|
22
|
+
path: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
};
|
|
25
|
+
listWorktrees: (basePath: string) => Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
path: string;
|
|
28
|
+
branch: string;
|
|
29
|
+
}>;
|
|
30
|
+
removeWorktree: (basePath: string, name: string, opts?: {
|
|
31
|
+
deleteBranch?: boolean;
|
|
32
|
+
}) => void;
|
|
33
|
+
mergeWorktreeToMain: (basePath: string, name: string, commitMessage: string) => void;
|
|
34
|
+
diffWorktreeAll: (basePath: string, name: string) => {
|
|
35
|
+
added: any[];
|
|
36
|
+
modified: any[];
|
|
37
|
+
removed: any[];
|
|
38
|
+
};
|
|
39
|
+
diffWorktreeNumstat: (basePath: string, name: string) => Array<{
|
|
40
|
+
added: number;
|
|
41
|
+
removed: number;
|
|
42
|
+
}>;
|
|
43
|
+
worktreeBranchName: (name: string) => string;
|
|
44
|
+
worktreePath: (basePath: string, name: string) => string;
|
|
45
|
+
runWorktreePostCreateHook: (basePath: string, wtPath: string) => string | null;
|
|
46
|
+
nativeHasChanges: (path: string) => boolean;
|
|
47
|
+
nativeDetectMainBranch: (basePath: string) => string;
|
|
48
|
+
nativeCommitCountBetween: (basePath: string, from: string, to: string) => number;
|
|
49
|
+
inferCommitType: (name: string) => string;
|
|
50
|
+
autoCommitCurrentBranch: (wtPath: string, reason: string, name: string) => void;
|
|
51
|
+
}
|
|
16
52
|
interface WorktreeStatus {
|
|
17
53
|
name: string;
|
|
18
54
|
path: string;
|
|
@@ -24,11 +60,11 @@ interface WorktreeStatus {
|
|
|
24
60
|
uncommitted: boolean;
|
|
25
61
|
commits: number;
|
|
26
62
|
}
|
|
27
|
-
declare function getWorktreeStatus(basePath: string, name: string, wtPath: string): WorktreeStatus;
|
|
28
|
-
declare function handleList(basePath: string): void
|
|
63
|
+
declare function getWorktreeStatus(ext: ExtensionModules, basePath: string, name: string, wtPath: string): WorktreeStatus;
|
|
64
|
+
declare function handleList(basePath: string): Promise<void>;
|
|
29
65
|
declare function handleMerge(basePath: string, args: string[]): Promise<void>;
|
|
30
|
-
declare function handleClean(basePath: string): void
|
|
31
|
-
declare function handleRemove(basePath: string, args: string[]): void
|
|
32
|
-
declare function handleStatusBanner(basePath: string): void
|
|
33
|
-
declare function handleWorktreeFlag(worktreeFlag: boolean | string): void
|
|
66
|
+
declare function handleClean(basePath: string): Promise<void>;
|
|
67
|
+
declare function handleRemove(basePath: string, args: string[]): Promise<void>;
|
|
68
|
+
declare function handleStatusBanner(basePath: string): Promise<void>;
|
|
69
|
+
declare function handleWorktreeFlag(worktreeFlag: boolean | string): Promise<void>;
|
|
34
70
|
export { handleList, handleMerge, handleClean, handleRemove, handleStatusBanner, handleWorktreeFlag, getWorktreeStatus, };
|
package/dist/worktree-cli.js
CHANGED
|
@@ -12,18 +12,53 @@
|
|
|
12
12
|
* On session exit (via session_shutdown event), auto-commits dirty work
|
|
13
13
|
* so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
|
|
14
14
|
* when a session was launched via -w.
|
|
15
|
+
*
|
|
16
|
+
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
|
|
17
|
+
* We use createJiti() here because this module is compiled by tsc but imports
|
|
18
|
+
* from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
|
|
15
19
|
*/
|
|
16
20
|
import chalk from 'chalk';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
21
|
+
import { createJiti } from '@mariozechner/jiti';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
19
24
|
import { generateWorktreeName } from './worktree-name-gen.js';
|
|
20
|
-
import { nativeHasChanges, nativeDetectMainBranch, nativeCommitCountBetween, } from './resources/extensions/gsd/native-git-bridge.js';
|
|
21
|
-
import { inferCommitType } from './resources/extensions/gsd/git-service.js';
|
|
22
25
|
import { existsSync } from 'node:fs';
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
|
|
28
|
+
// Lazily-loaded extension modules (loaded once on first use via jiti)
|
|
29
|
+
let _ext = null;
|
|
30
|
+
async function loadExtensionModules() {
|
|
31
|
+
if (_ext)
|
|
32
|
+
return _ext;
|
|
33
|
+
const [wtMgr, autoWt, gitBridge, gitSvc, wt] = await Promise.all([
|
|
34
|
+
jiti.import(join(__dirname, 'resources/extensions/gsd/worktree-manager.ts'), {}),
|
|
35
|
+
jiti.import(join(__dirname, 'resources/extensions/gsd/auto-worktree.ts'), {}),
|
|
36
|
+
jiti.import(join(__dirname, 'resources/extensions/gsd/native-git-bridge.ts'), {}),
|
|
37
|
+
jiti.import(join(__dirname, 'resources/extensions/gsd/git-service.ts'), {}),
|
|
38
|
+
jiti.import(join(__dirname, 'resources/extensions/gsd/worktree.ts'), {}),
|
|
39
|
+
]);
|
|
40
|
+
_ext = {
|
|
41
|
+
createWorktree: wtMgr.createWorktree,
|
|
42
|
+
listWorktrees: wtMgr.listWorktrees,
|
|
43
|
+
removeWorktree: wtMgr.removeWorktree,
|
|
44
|
+
mergeWorktreeToMain: wtMgr.mergeWorktreeToMain,
|
|
45
|
+
diffWorktreeAll: wtMgr.diffWorktreeAll,
|
|
46
|
+
diffWorktreeNumstat: wtMgr.diffWorktreeNumstat,
|
|
47
|
+
worktreeBranchName: wtMgr.worktreeBranchName,
|
|
48
|
+
worktreePath: wtMgr.worktreePath,
|
|
49
|
+
runWorktreePostCreateHook: autoWt.runWorktreePostCreateHook,
|
|
50
|
+
nativeHasChanges: gitBridge.nativeHasChanges,
|
|
51
|
+
nativeDetectMainBranch: gitBridge.nativeDetectMainBranch,
|
|
52
|
+
nativeCommitCountBetween: gitBridge.nativeCommitCountBetween,
|
|
53
|
+
inferCommitType: gitSvc.inferCommitType,
|
|
54
|
+
autoCommitCurrentBranch: wt.autoCommitCurrentBranch,
|
|
55
|
+
};
|
|
56
|
+
return _ext;
|
|
57
|
+
}
|
|
23
58
|
// ─── Status Helpers ─────────────────────────────────────────────────────────
|
|
24
|
-
function getWorktreeStatus(basePath, name, wtPath) {
|
|
25
|
-
const diff = diffWorktreeAll(basePath, name);
|
|
26
|
-
const numstat = diffWorktreeNumstat(basePath, name);
|
|
59
|
+
function getWorktreeStatus(ext, basePath, name, wtPath) {
|
|
60
|
+
const diff = ext.diffWorktreeAll(basePath, name);
|
|
61
|
+
const numstat = ext.diffWorktreeNumstat(basePath, name);
|
|
27
62
|
const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
|
|
28
63
|
let linesAdded = 0;
|
|
29
64
|
let linesRemoved = 0;
|
|
@@ -33,19 +68,19 @@ function getWorktreeStatus(basePath, name, wtPath) {
|
|
|
33
68
|
}
|
|
34
69
|
let uncommitted = false;
|
|
35
70
|
try {
|
|
36
|
-
uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
|
|
71
|
+
uncommitted = existsSync(wtPath) && ext.nativeHasChanges(wtPath);
|
|
37
72
|
}
|
|
38
73
|
catch { /* */ }
|
|
39
74
|
let commits = 0;
|
|
40
75
|
try {
|
|
41
|
-
const mainBranch = nativeDetectMainBranch(basePath);
|
|
42
|
-
commits = nativeCommitCountBetween(basePath, mainBranch, worktreeBranchName(name));
|
|
76
|
+
const mainBranch = ext.nativeDetectMainBranch(basePath);
|
|
77
|
+
commits = ext.nativeCommitCountBetween(basePath, mainBranch, ext.worktreeBranchName(name));
|
|
43
78
|
}
|
|
44
79
|
catch { /* */ }
|
|
45
80
|
return {
|
|
46
81
|
name,
|
|
47
82
|
path: wtPath,
|
|
48
|
-
branch: worktreeBranchName(name),
|
|
83
|
+
branch: ext.worktreeBranchName(name),
|
|
49
84
|
exists: existsSync(wtPath),
|
|
50
85
|
filesChanged,
|
|
51
86
|
linesAdded,
|
|
@@ -71,65 +106,66 @@ function formatStatus(s) {
|
|
|
71
106
|
return lines.join('\n');
|
|
72
107
|
}
|
|
73
108
|
// ─── Subcommand: list ───────────────────────────────────────────────────────
|
|
74
|
-
function handleList(basePath) {
|
|
75
|
-
const
|
|
109
|
+
async function handleList(basePath) {
|
|
110
|
+
const ext = await loadExtensionModules();
|
|
111
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
76
112
|
if (worktrees.length === 0) {
|
|
77
113
|
process.stderr.write(chalk.dim('No worktrees. Create one with: gsd -w <name>\n'));
|
|
78
114
|
return;
|
|
79
115
|
}
|
|
80
116
|
process.stderr.write(chalk.bold('\nWorktrees\n\n'));
|
|
81
117
|
for (const wt of worktrees) {
|
|
82
|
-
const status = getWorktreeStatus(basePath, wt.name, wt.path);
|
|
118
|
+
const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
|
|
83
119
|
process.stderr.write(formatStatus(status) + '\n\n');
|
|
84
120
|
}
|
|
85
121
|
}
|
|
86
122
|
// ─── Subcommand: merge ──────────────────────────────────────────────────────
|
|
87
123
|
async function handleMerge(basePath, args) {
|
|
124
|
+
const ext = await loadExtensionModules();
|
|
88
125
|
const name = args[0];
|
|
89
126
|
if (!name) {
|
|
90
127
|
// If only one worktree exists, merge it
|
|
91
|
-
const worktrees = listWorktrees(basePath);
|
|
128
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
92
129
|
if (worktrees.length === 1) {
|
|
93
|
-
await doMerge(basePath, worktrees[0].name);
|
|
130
|
+
await doMerge(ext, basePath, worktrees[0].name);
|
|
94
131
|
return;
|
|
95
132
|
}
|
|
96
133
|
process.stderr.write(chalk.red('Usage: gsd worktree merge <name>\n'));
|
|
97
134
|
process.stderr.write(chalk.dim('Run gsd worktree list to see worktrees.\n'));
|
|
98
135
|
process.exit(1);
|
|
99
136
|
}
|
|
100
|
-
await doMerge(basePath, name);
|
|
137
|
+
await doMerge(ext, basePath, name);
|
|
101
138
|
}
|
|
102
|
-
async function doMerge(basePath, name) {
|
|
103
|
-
const worktrees = listWorktrees(basePath);
|
|
139
|
+
async function doMerge(ext, basePath, name) {
|
|
140
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
104
141
|
const wt = worktrees.find(w => w.name === name);
|
|
105
142
|
if (!wt) {
|
|
106
143
|
process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
|
|
107
144
|
process.exit(1);
|
|
108
145
|
}
|
|
109
|
-
const status = getWorktreeStatus(basePath, name, wt.path);
|
|
146
|
+
const status = getWorktreeStatus(ext, basePath, name, wt.path);
|
|
110
147
|
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
111
148
|
process.stderr.write(chalk.dim(`Worktree "${name}" has no changes to merge.\n`));
|
|
112
149
|
// Clean up empty worktree
|
|
113
|
-
removeWorktree(basePath, name, { deleteBranch: true });
|
|
150
|
+
ext.removeWorktree(basePath, name, { deleteBranch: true });
|
|
114
151
|
process.stderr.write(chalk.green(`Removed empty worktree ${chalk.bold(name)}.\n`));
|
|
115
152
|
return;
|
|
116
153
|
}
|
|
117
154
|
// Auto-commit dirty work before merge
|
|
118
155
|
if (status.uncommitted) {
|
|
119
156
|
try {
|
|
120
|
-
|
|
121
|
-
autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
|
|
157
|
+
ext.autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
|
|
122
158
|
process.stderr.write(chalk.dim(' Auto-committed dirty work before merge.\n'));
|
|
123
159
|
}
|
|
124
160
|
catch { /* best-effort */ }
|
|
125
161
|
}
|
|
126
|
-
const commitType = inferCommitType(name);
|
|
162
|
+
const commitType = ext.inferCommitType(name);
|
|
127
163
|
const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
|
|
128
|
-
process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(nativeDetectMainBranch(basePath))}\n`);
|
|
164
|
+
process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(ext.nativeDetectMainBranch(basePath))}\n`);
|
|
129
165
|
process.stderr.write(chalk.dim(` ${status.filesChanged} files, ${chalk.green(`+${status.linesAdded}`)} ${chalk.red(`-${status.linesRemoved}`)}\n\n`));
|
|
130
166
|
try {
|
|
131
|
-
mergeWorktreeToMain(basePath, name, commitMessage);
|
|
132
|
-
removeWorktree(basePath, name, { deleteBranch: true });
|
|
167
|
+
ext.mergeWorktreeToMain(basePath, name, commitMessage);
|
|
168
|
+
ext.removeWorktree(basePath, name, { deleteBranch: true });
|
|
133
169
|
process.stderr.write(chalk.green(`✓ Merged and cleaned up ${chalk.bold(name)}\n`));
|
|
134
170
|
process.stderr.write(chalk.dim(` commit: ${commitMessage}\n`));
|
|
135
171
|
}
|
|
@@ -141,18 +177,19 @@ async function doMerge(basePath, name) {
|
|
|
141
177
|
}
|
|
142
178
|
}
|
|
143
179
|
// ─── Subcommand: clean ──────────────────────────────────────────────────────
|
|
144
|
-
function handleClean(basePath) {
|
|
145
|
-
const
|
|
180
|
+
async function handleClean(basePath) {
|
|
181
|
+
const ext = await loadExtensionModules();
|
|
182
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
146
183
|
if (worktrees.length === 0) {
|
|
147
184
|
process.stderr.write(chalk.dim('No worktrees to clean.\n'));
|
|
148
185
|
return;
|
|
149
186
|
}
|
|
150
187
|
let cleaned = 0;
|
|
151
188
|
for (const wt of worktrees) {
|
|
152
|
-
const status = getWorktreeStatus(basePath, wt.name, wt.path);
|
|
189
|
+
const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
|
|
153
190
|
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
154
191
|
try {
|
|
155
|
-
removeWorktree(basePath, wt.name, { deleteBranch: true });
|
|
192
|
+
ext.removeWorktree(basePath, wt.name, { deleteBranch: true });
|
|
156
193
|
process.stderr.write(chalk.green(` ✓ Removed ${chalk.bold(wt.name)} (clean)\n`));
|
|
157
194
|
cleaned++;
|
|
158
195
|
}
|
|
@@ -167,19 +204,20 @@ function handleClean(basePath) {
|
|
|
167
204
|
process.stderr.write(chalk.dim(`\nCleaned ${cleaned} worktree${cleaned === 1 ? '' : 's'}.\n`));
|
|
168
205
|
}
|
|
169
206
|
// ─── Subcommand: remove ─────────────────────────────────────────────────────
|
|
170
|
-
function handleRemove(basePath, args) {
|
|
207
|
+
async function handleRemove(basePath, args) {
|
|
208
|
+
const ext = await loadExtensionModules();
|
|
171
209
|
const name = args[0];
|
|
172
210
|
if (!name) {
|
|
173
211
|
process.stderr.write(chalk.red('Usage: gsd worktree remove <name>\n'));
|
|
174
212
|
process.exit(1);
|
|
175
213
|
}
|
|
176
|
-
const worktrees = listWorktrees(basePath);
|
|
214
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
177
215
|
const wt = worktrees.find(w => w.name === name);
|
|
178
216
|
if (!wt) {
|
|
179
217
|
process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
|
|
180
218
|
process.exit(1);
|
|
181
219
|
}
|
|
182
|
-
const status = getWorktreeStatus(basePath, name, wt.path);
|
|
220
|
+
const status = getWorktreeStatus(ext, basePath, name, wt.path);
|
|
183
221
|
if (status.filesChanged > 0 || status.uncommitted) {
|
|
184
222
|
process.stderr.write(chalk.yellow(`⚠ Worktree "${name}" has unmerged changes (${status.filesChanged} files).\n`));
|
|
185
223
|
process.stderr.write(chalk.yellow(' Use --force to remove anyway, or merge first: gsd worktree merge ' + name + '\n'));
|
|
@@ -187,17 +225,18 @@ function handleRemove(basePath, args) {
|
|
|
187
225
|
process.exit(1);
|
|
188
226
|
}
|
|
189
227
|
}
|
|
190
|
-
removeWorktree(basePath, name, { deleteBranch: true });
|
|
228
|
+
ext.removeWorktree(basePath, name, { deleteBranch: true });
|
|
191
229
|
process.stderr.write(chalk.green(`✓ Removed worktree ${chalk.bold(name)}\n`));
|
|
192
230
|
}
|
|
193
231
|
// ─── Subcommand: status (default when no args) ─────────────────────────────
|
|
194
|
-
function handleStatusBanner(basePath) {
|
|
195
|
-
const
|
|
232
|
+
async function handleStatusBanner(basePath) {
|
|
233
|
+
const ext = await loadExtensionModules();
|
|
234
|
+
const worktrees = ext.listWorktrees(basePath);
|
|
196
235
|
if (worktrees.length === 0)
|
|
197
236
|
return;
|
|
198
237
|
const withChanges = worktrees.filter(wt => {
|
|
199
238
|
try {
|
|
200
|
-
const diff = diffWorktreeAll(basePath, wt.name);
|
|
239
|
+
const diff = ext.diffWorktreeAll(basePath, wt.name);
|
|
201
240
|
return diff.added.length + diff.modified.length + diff.removed.length > 0;
|
|
202
241
|
}
|
|
203
242
|
catch {
|
|
@@ -214,14 +253,15 @@ function handleStatusBanner(basePath) {
|
|
|
214
253
|
chalk.dim('Resume: gsd -w <name> | Merge: gsd worktree merge <name> | List: gsd worktree list\n\n'));
|
|
215
254
|
}
|
|
216
255
|
// ─── -w flag: create/resume worktree for interactive session ────────────────
|
|
217
|
-
function handleWorktreeFlag(worktreeFlag) {
|
|
256
|
+
async function handleWorktreeFlag(worktreeFlag) {
|
|
257
|
+
const ext = await loadExtensionModules();
|
|
218
258
|
const basePath = process.cwd();
|
|
219
259
|
// gsd -w (no name) — resume most recent worktree with changes, or create new
|
|
220
260
|
if (worktreeFlag === true) {
|
|
221
|
-
const existing = listWorktrees(basePath);
|
|
261
|
+
const existing = ext.listWorktrees(basePath);
|
|
222
262
|
const withChanges = existing.filter(wt => {
|
|
223
263
|
try {
|
|
224
|
-
const diff = diffWorktreeAll(basePath, wt.name);
|
|
264
|
+
const diff = ext.diffWorktreeAll(basePath, wt.name);
|
|
225
265
|
return diff.added.length + diff.modified.length + diff.removed.length > 0;
|
|
226
266
|
}
|
|
227
267
|
catch {
|
|
@@ -243,7 +283,7 @@ function handleWorktreeFlag(worktreeFlag) {
|
|
|
243
283
|
// Multiple active worktrees — show them and ask user to pick
|
|
244
284
|
process.stderr.write(chalk.yellow(`${withChanges.length} worktrees have unmerged changes:\n\n`));
|
|
245
285
|
for (const wt of withChanges) {
|
|
246
|
-
const status = getWorktreeStatus(basePath, wt.name, wt.path);
|
|
286
|
+
const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
|
|
247
287
|
process.stderr.write(formatStatus(status) + '\n\n');
|
|
248
288
|
}
|
|
249
289
|
process.stderr.write(chalk.dim('Specify which one: gsd -w <name>\n'));
|
|
@@ -251,12 +291,12 @@ function handleWorktreeFlag(worktreeFlag) {
|
|
|
251
291
|
}
|
|
252
292
|
// No active worktrees — create a new one
|
|
253
293
|
const name = generateWorktreeName();
|
|
254
|
-
createAndEnter(basePath, name);
|
|
294
|
+
await createAndEnter(ext, basePath, name);
|
|
255
295
|
return;
|
|
256
296
|
}
|
|
257
297
|
// gsd -w <name> — create or resume named worktree
|
|
258
298
|
const name = worktreeFlag;
|
|
259
|
-
const existing = listWorktrees(basePath);
|
|
299
|
+
const existing = ext.listWorktrees(basePath);
|
|
260
300
|
const found = existing.find(wt => wt.name === name);
|
|
261
301
|
if (found) {
|
|
262
302
|
process.chdir(found.path);
|
|
@@ -267,13 +307,13 @@ function handleWorktreeFlag(worktreeFlag) {
|
|
|
267
307
|
process.stderr.write(chalk.dim(` branch ${found.branch}\n\n`));
|
|
268
308
|
}
|
|
269
309
|
else {
|
|
270
|
-
createAndEnter(basePath, name);
|
|
310
|
+
await createAndEnter(ext, basePath, name);
|
|
271
311
|
}
|
|
272
312
|
}
|
|
273
|
-
function createAndEnter(basePath, name) {
|
|
313
|
+
async function createAndEnter(ext, basePath, name) {
|
|
274
314
|
try {
|
|
275
|
-
const info = createWorktree(basePath, name);
|
|
276
|
-
const hookError = runWorktreePostCreateHook(basePath, info.path);
|
|
315
|
+
const info = ext.createWorktree(basePath, name);
|
|
316
|
+
const hookError = ext.runWorktreePostCreateHook(basePath, info.path);
|
|
277
317
|
if (hookError) {
|
|
278
318
|
process.stderr.write(chalk.yellow(`[gsd] ${hookError}\n`));
|
|
279
319
|
}
|
package/package.json
CHANGED
|
@@ -131,9 +131,11 @@ export async function bootstrapAutoSession(
|
|
|
131
131
|
// Initialize GitServiceImpl
|
|
132
132
|
s.gitService = createGitService(s.basePath);
|
|
133
133
|
|
|
134
|
-
// Check for crash from previous session (use both old and new lock data)
|
|
134
|
+
// Check for crash from previous session (use both old and new lock data).
|
|
135
|
+
// Skip if the lock PID matches this process — acquireSessionLock() writes
|
|
136
|
+
// to the same auto.lock file before this check, so we'd always false-positive.
|
|
135
137
|
const crashLock = readCrashLock(base);
|
|
136
|
-
if (crashLock) {
|
|
138
|
+
if (crashLock && crashLock.pid !== process.pid) {
|
|
137
139
|
// We already hold the session lock, so no concurrent session is running.
|
|
138
140
|
// The crash lock is from a dead process — recover context from it.
|
|
139
141
|
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
@@ -44,6 +44,8 @@ import { handleConfig } from "./commands-config.js";
|
|
|
44
44
|
import { handleInspect } from "./commands-inspect.js";
|
|
45
45
|
import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
46
46
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
47
|
+
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
48
|
+
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
47
49
|
import { handleLogs } from "./commands-logs.js";
|
|
48
50
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
49
51
|
|
|
@@ -1068,6 +1070,11 @@ async function handleSetup(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
1068
1070
|
function formatTextStatus(state: GSDState): string {
|
|
1069
1071
|
const lines: string[] = ["GSD Status\n"];
|
|
1070
1072
|
|
|
1073
|
+
// Progress score — traffic light (#1221)
|
|
1074
|
+
const progressScore = computeProgressScore();
|
|
1075
|
+
lines.push(formatProgressLine(progressScore));
|
|
1076
|
+
lines.push("");
|
|
1077
|
+
|
|
1071
1078
|
// Phase
|
|
1072
1079
|
lines.push(`Phase: ${state.phase}`);
|
|
1073
1080
|
|
|
@@ -1114,5 +1121,17 @@ function formatTextStatus(state: GSDState): string {
|
|
|
1114
1121
|
}
|
|
1115
1122
|
}
|
|
1116
1123
|
|
|
1124
|
+
// Environment health (#1221)
|
|
1125
|
+
const envResults = runEnvironmentChecks(projectRoot());
|
|
1126
|
+
const envIssues = envResults.filter(r => r.status !== "ok");
|
|
1127
|
+
if (envIssues.length > 0) {
|
|
1128
|
+
lines.push("");
|
|
1129
|
+
lines.push("Environment:");
|
|
1130
|
+
for (const r of envIssues) {
|
|
1131
|
+
const icon = r.status === "error" ? "✗" : "⚠";
|
|
1132
|
+
lines.push(` ${icon} ${r.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1117
1136
|
return lines.join("\n");
|
|
1118
1137
|
}
|
|
@@ -23,6 +23,8 @@ import { getActiveWorktreeName } from "./worktree-command.js";
|
|
|
23
23
|
import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagent/worker-registry.js";
|
|
24
24
|
import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
|
|
25
25
|
import { estimateTimeRemaining } from "./auto-dashboard.js";
|
|
26
|
+
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
27
|
+
import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
26
28
|
|
|
27
29
|
function unitLabel(type: string): string {
|
|
28
30
|
switch (type) {
|
|
@@ -310,6 +312,15 @@ export class GSDDashboardOverlay {
|
|
|
310
312
|
elapsedParts = th.fg("dim", `since ${this.dashData.remoteSession!.startedAt.replace("T", " ").slice(0, 19)}`);
|
|
311
313
|
}
|
|
312
314
|
lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsedParts, contentWidth)));
|
|
315
|
+
|
|
316
|
+
// Progress score — traffic light indicator (#1221)
|
|
317
|
+
if (this.dashData.active || this.dashData.paused) {
|
|
318
|
+
const progressScore = computeProgressScore();
|
|
319
|
+
const progressIcon = progressScore.level === "green" ? th.fg("success", "●")
|
|
320
|
+
: progressScore.level === "yellow" ? th.fg("warning", "●")
|
|
321
|
+
: th.fg("error", "●");
|
|
322
|
+
lines.push(row(`${progressIcon} ${th.fg("text", progressScore.summary)}`));
|
|
323
|
+
}
|
|
313
324
|
lines.push(blank());
|
|
314
325
|
|
|
315
326
|
if (this.dashData.currentUnit) {
|
|
@@ -579,6 +590,23 @@ export class GSDDashboardOverlay {
|
|
|
579
590
|
}
|
|
580
591
|
}
|
|
581
592
|
|
|
593
|
+
// Environment health section (#1221) — only show issues
|
|
594
|
+
const envResults = runEnvironmentChecks(this.dashData.basePath || process.cwd());
|
|
595
|
+
const envIssues = envResults.filter(r => r.status !== "ok");
|
|
596
|
+
if (envIssues.length > 0) {
|
|
597
|
+
lines.push(blank());
|
|
598
|
+
lines.push(hr());
|
|
599
|
+
lines.push(row(th.fg("text", th.bold("Environment"))));
|
|
600
|
+
lines.push(blank());
|
|
601
|
+
for (const r of envIssues) {
|
|
602
|
+
const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
|
|
603
|
+
lines.push(row(` ${icon} ${th.fg("text", r.message)}`));
|
|
604
|
+
if (r.detail) {
|
|
605
|
+
lines.push(row(th.fg("dim", ` ${r.detail}`)));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
582
610
|
lines.push(blank());
|
|
583
611
|
lines.push(hr());
|
|
584
612
|
lines.push(centered(th.fg("dim", "↑↓ scroll · g/G top/end · esc close")));
|