@aemonge-dev/pi-stardeck 0.0.0-stage → 0.6.0
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/LICENSE +21 -0
- package/README.md +240 -2
- package/demo/pi-stardeck.tape +59 -0
- package/docs/architecture.md +106 -0
- package/docs/assets/pi-stardeck.gif +0 -0
- package/docs/development.md +128 -0
- package/licenses/README.md +19 -0
- package/licenses/pi-vim-LICENSE +21 -0
- package/package.json +81 -4
- package/packages/footer/README.md +15 -0
- package/packages/footer/index.ts +37 -0
- package/packages/footer/package.json +32 -0
- package/packages/footer/src/protocol.ts +10 -0
- package/packages/footer/src/quota.ts +26 -0
- package/packages/footer/src/telemetry.ts +59 -0
- package/packages/pi-stardeck/README.md +220 -0
- package/packages/pi-stardeck/index.ts +691 -0
- package/packages/pi-stardeck/package.json +32 -0
- package/packages/pi-stardeck/src/activity-age.ts +184 -0
- package/packages/pi-stardeck/src/capabilities.ts +105 -0
- package/packages/pi-stardeck/src/deck.ts +807 -0
- package/packages/pi-stardeck/src/gauge.ts +1323 -0
- package/packages/pi-stardeck/src/git-refresh.ts +5 -0
- package/packages/pi-stardeck/src/process-resources.ts +428 -0
- package/packages/pi-stardeck/src/title-state.ts +195 -0
- package/packages/pi-stardeck/src/title.ts +443 -0
- package/packages/pi-vim-top-border/LICENSE +21 -0
- package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
- package/packages/pi-vim-top-border/README.md +577 -0
- package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
- package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
- package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
- package/packages/pi-vim-top-border/index.ts +4195 -0
- package/packages/pi-vim-top-border/input-keys.ts +49 -0
- package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
- package/packages/pi-vim-top-border/mode-colors.ts +97 -0
- package/packages/pi-vim-top-border/mode-label.ts +62 -0
- package/packages/pi-vim-top-border/motions.ts +297 -0
- package/packages/pi-vim-top-border/package.json +47 -0
- package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
- package/packages/pi-vim-top-border/prompt-status.ts +131 -0
- package/packages/pi-vim-top-border/settings.ts +287 -0
- package/packages/pi-vim-top-border/text-objects.ts +389 -0
- package/packages/pi-vim-top-border/types.ts +51 -0
- package/packages/pi-vim-top-border/visual.ts +65 -0
- package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
- package/packages/status/README.md +398 -0
- package/packages/status/index.ts +1717 -0
- package/packages/status/package.json +33 -0
- package/packages/status/src/active-work.ts +235 -0
- package/packages/status/src/config.ts +218 -0
- package/packages/status/src/diagnostics.ts +238 -0
- package/packages/status/src/goal-block.ts +377 -0
- package/packages/status/src/goal.ts +170 -0
- package/packages/status/src/ledger.ts +101 -0
- package/packages/status/src/openspec.ts +394 -0
- package/packages/status/src/orchestration.ts +237 -0
- package/packages/status/src/publisher.ts +1294 -0
- package/packages/status/src/reducer.ts +190 -0
- package/packages/status/src/runtime-runs.ts +267 -0
- package/packages/status/src/state.ts +178 -0
- package/packages/status/src/title-activity.ts +224 -0
- package/packages/status/src/title-state.ts +218 -0
- package/packages/status/src/turn-cost.ts +63 -0
- package/packages/status/src/types.ts +228 -0
- package/src/deck-surface.ts +46 -0
- package/src/health.ts +227 -0
- package/src/index.ts +27 -0
- package/src/pi-status.ts +334 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { GoalHeaderState } from './goal.ts';
|
|
2
|
+
|
|
3
|
+
export type CheckState = 'pass' | 'fail' | 'running' | 'unknown';
|
|
4
|
+
export type WorkflowState = 'running' | 'blocked' | 'complete' | 'idle' | 'unknown';
|
|
5
|
+
export type WidgetColor =
|
|
6
|
+
| 'text'
|
|
7
|
+
| 'accent'
|
|
8
|
+
| 'muted'
|
|
9
|
+
| 'dim'
|
|
10
|
+
| 'success'
|
|
11
|
+
| 'error'
|
|
12
|
+
| 'warning'
|
|
13
|
+
| 'customMessageLabel'
|
|
14
|
+
| 'thinkingHigh';
|
|
15
|
+
|
|
16
|
+
export interface CommandSpec {
|
|
17
|
+
executable: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GalacticaStatusConfig {
|
|
22
|
+
version: 1;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
openspec: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
change: string | null;
|
|
27
|
+
};
|
|
28
|
+
diagnostics: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
sourceFile: string | null;
|
|
31
|
+
command: CommandSpec | null;
|
|
32
|
+
intervalSeconds: number;
|
|
33
|
+
runAutomatically: boolean;
|
|
34
|
+
timeoutSeconds: number;
|
|
35
|
+
};
|
|
36
|
+
orchestration: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
stateFile: string | null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LoadedConfig {
|
|
43
|
+
config: GalacticaStatusConfig;
|
|
44
|
+
path: string;
|
|
45
|
+
warnings: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SourceMeta {
|
|
49
|
+
source: string;
|
|
50
|
+
refreshedAt: number;
|
|
51
|
+
attemptedAt: number;
|
|
52
|
+
updatedAt?: number;
|
|
53
|
+
staleAfterMs?: number;
|
|
54
|
+
stale: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface OpenSpecTask {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
kind?: 'Story' | 'Spike' | 'Lab' | 'Hack';
|
|
62
|
+
done: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OpenSpecOverview {
|
|
66
|
+
projectRoot: string;
|
|
67
|
+
actionableChanges: number;
|
|
68
|
+
totalChanges: number;
|
|
69
|
+
completedPlans: number;
|
|
70
|
+
totalPlans: number;
|
|
71
|
+
completedTasks: number;
|
|
72
|
+
totalTasks: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type OpenSpecFeedback = 'clear' | 'no-focus' | 'unavailable';
|
|
76
|
+
|
|
77
|
+
export interface OpenSpecHierarchyProgress {
|
|
78
|
+
tasks: { completed: number; total: number };
|
|
79
|
+
stepsByTask: Record<string, { completed: number; total: number }>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface OpenSpecState {
|
|
83
|
+
projectRoot: string;
|
|
84
|
+
changeId: string;
|
|
85
|
+
title: string;
|
|
86
|
+
phase: string;
|
|
87
|
+
completedTasks: number;
|
|
88
|
+
totalTasks: number;
|
|
89
|
+
progressPercent: number;
|
|
90
|
+
pendingTasks: OpenSpecTask[];
|
|
91
|
+
allTasks?: OpenSpecTask[];
|
|
92
|
+
tasksPath?: string;
|
|
93
|
+
hierarchy?: OpenSpecHierarchyProgress;
|
|
94
|
+
meta: SourceMeta;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DiagnosticsState {
|
|
98
|
+
errors: number;
|
|
99
|
+
blockers: number;
|
|
100
|
+
warnings: number;
|
|
101
|
+
tests: CheckState;
|
|
102
|
+
typecheck: CheckState;
|
|
103
|
+
meta: SourceMeta;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface OrchestrationState {
|
|
107
|
+
phase?: string;
|
|
108
|
+
state: WorkflowState;
|
|
109
|
+
activeWorkers: number;
|
|
110
|
+
totalWorkers?: number;
|
|
111
|
+
completedFiles?: number;
|
|
112
|
+
totalFiles?: number;
|
|
113
|
+
currentTask?: string;
|
|
114
|
+
openspecTaskId?: string;
|
|
115
|
+
startedAt?: number;
|
|
116
|
+
elapsedMs?: number;
|
|
117
|
+
meta: SourceMeta;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RuntimeErrorRecord {
|
|
121
|
+
domain: 'config' | 'openspec' | 'diagnostics' | 'orchestration' | 'watcher';
|
|
122
|
+
message: string;
|
|
123
|
+
at: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface StatusSnapshot {
|
|
127
|
+
cwd: string;
|
|
128
|
+
projectRoot: string;
|
|
129
|
+
configPath: string;
|
|
130
|
+
configWarnings: string[];
|
|
131
|
+
lastRefreshAt?: number;
|
|
132
|
+
openspec: OpenSpecState | null;
|
|
133
|
+
diagnostics: DiagnosticsState | null;
|
|
134
|
+
orchestration: OrchestrationState | null;
|
|
135
|
+
errors: RuntimeErrorRecord[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CommandResult {
|
|
139
|
+
stdout: string;
|
|
140
|
+
stderr: string;
|
|
141
|
+
code: number;
|
|
142
|
+
killed?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type CommandRunner = (
|
|
146
|
+
command: string,
|
|
147
|
+
args: string[],
|
|
148
|
+
options: { cwd: string; timeoutMs: number },
|
|
149
|
+
) => Promise<CommandResult>;
|
|
150
|
+
|
|
151
|
+
export interface FancyFooterWidget {
|
|
152
|
+
id: string;
|
|
153
|
+
label?: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
content: { type: 'text'; text: string };
|
|
156
|
+
icon?:
|
|
157
|
+
| false
|
|
158
|
+
| {
|
|
159
|
+
glyphs:
|
|
160
|
+
string | Partial<Record<'nerd' | 'emoji' | 'unicode' | 'ascii', string>>;
|
|
161
|
+
color?: WidgetColor;
|
|
162
|
+
};
|
|
163
|
+
style?: { textColor?: WidgetColor; bold?: boolean };
|
|
164
|
+
layout?: {
|
|
165
|
+
enabled?: boolean;
|
|
166
|
+
row?: number;
|
|
167
|
+
position?: number;
|
|
168
|
+
align?: 'left' | 'middle' | 'right';
|
|
169
|
+
fill?: 'none' | 'grow';
|
|
170
|
+
minWidth?: number;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type FancyFooterMessage =
|
|
175
|
+
| { protocol: 1; type: 'upsert'; widget: FancyFooterWidget }
|
|
176
|
+
| { protocol: 1; type: 'remove'; id: string };
|
|
177
|
+
|
|
178
|
+
export type PublishOperation =
|
|
179
|
+
{ type: 'upsert'; widget: FancyFooterWidget } | { type: 'remove'; id: string };
|
|
180
|
+
|
|
181
|
+
// --- Frozen status contract: snapshot facts -------------------------------
|
|
182
|
+
|
|
183
|
+
export type StageValue =
|
|
184
|
+
| 'understanding'
|
|
185
|
+
| 'working'
|
|
186
|
+
| 'assuring'
|
|
187
|
+
| 'learning'
|
|
188
|
+
| 'answering'
|
|
189
|
+
| 'waiting'
|
|
190
|
+
| 'blocked'
|
|
191
|
+
| 'listening'
|
|
192
|
+
| 'aborted';
|
|
193
|
+
|
|
194
|
+
export type SelectionState =
|
|
195
|
+
| { kind: 'openspec-task'; change: string; task: string }
|
|
196
|
+
| {
|
|
197
|
+
kind: 'goal';
|
|
198
|
+
status: GoalHeaderState['status'];
|
|
199
|
+
waiting: boolean;
|
|
200
|
+
}
|
|
201
|
+
| { kind: 'session-work'; intent: string; phase: 'active' | 'validation' }
|
|
202
|
+
| { kind: 'subject'; title: string }
|
|
203
|
+
| { kind: 'none' };
|
|
204
|
+
|
|
205
|
+
export type RunSpan = {
|
|
206
|
+
id: string;
|
|
207
|
+
kind: 'agent' | 'subagent' | 'bash';
|
|
208
|
+
parent: string | null;
|
|
209
|
+
label?: string;
|
|
210
|
+
agent?: string;
|
|
211
|
+
stage: StageValue;
|
|
212
|
+
stageDeclared: boolean;
|
|
213
|
+
since: number;
|
|
214
|
+
endedAt?: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type ProgressFact = {
|
|
218
|
+
tasks: { completed: number; total: number };
|
|
219
|
+
steps: { completed: number; total: number };
|
|
220
|
+
freshAt: number;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type CockpitSnapshot = {
|
|
224
|
+
version: number;
|
|
225
|
+
selection: SelectionState;
|
|
226
|
+
runs: RunSpan[];
|
|
227
|
+
progress: ProgressFact | null;
|
|
228
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
export interface DeckModeRail {
|
|
4
|
+
plain: string;
|
|
5
|
+
styled: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DeckRenderer = (
|
|
9
|
+
width: number,
|
|
10
|
+
theme: Theme,
|
|
11
|
+
modeRail?: DeckModeRail,
|
|
12
|
+
) => string[];
|
|
13
|
+
|
|
14
|
+
export interface CockpitDeckSurface {
|
|
15
|
+
setRenderer(renderer: DeckRenderer | null): void;
|
|
16
|
+
setRequestRender(requestRender: (() => void) | null): void;
|
|
17
|
+
requestRender(): void;
|
|
18
|
+
render(width: number, theme: Theme, modeRail?: DeckModeRail): string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createCockpitDeckSurface(): CockpitDeckSurface {
|
|
22
|
+
let renderer: DeckRenderer | null = null;
|
|
23
|
+
let requestRender: (() => void) | null = null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
setRenderer(next) {
|
|
27
|
+
renderer = next;
|
|
28
|
+
requestRender?.();
|
|
29
|
+
},
|
|
30
|
+
setRequestRender(next) {
|
|
31
|
+
requestRender = next;
|
|
32
|
+
if (renderer && requestRender) requestRender();
|
|
33
|
+
},
|
|
34
|
+
requestRender() {
|
|
35
|
+
requestRender?.();
|
|
36
|
+
},
|
|
37
|
+
render(width, theme, modeRail) {
|
|
38
|
+
if (!renderer || width <= 0) return [];
|
|
39
|
+
try {
|
|
40
|
+
return renderer(width, theme, modeRail);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join, parse } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ExtensionAPI,
|
|
9
|
+
ExtensionCommandContext,
|
|
10
|
+
} from '@earendil-works/pi-coding-agent';
|
|
11
|
+
|
|
12
|
+
export type ExternalEditorHealth = 'ready' | 'missing' | 'unavailable';
|
|
13
|
+
export type StardeckHealthStatus = 'PASS' | 'WARN' | 'INFO';
|
|
14
|
+
|
|
15
|
+
export interface StardeckHealthEvidence {
|
|
16
|
+
packageVersion: string;
|
|
17
|
+
mode: string;
|
|
18
|
+
hasUI: boolean;
|
|
19
|
+
neovimTerminal: boolean;
|
|
20
|
+
externalEditor: ExternalEditorHealth;
|
|
21
|
+
commandNames: readonly string[];
|
|
22
|
+
capabilities: {
|
|
23
|
+
git: boolean;
|
|
24
|
+
openspec: boolean;
|
|
25
|
+
devbox: boolean;
|
|
26
|
+
tmux: boolean;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StardeckHealthCheck {
|
|
31
|
+
id:
|
|
32
|
+
| 'package'
|
|
33
|
+
| 'surface'
|
|
34
|
+
| 'composer'
|
|
35
|
+
| 'topology'
|
|
36
|
+
| 'git'
|
|
37
|
+
| 'openspec'
|
|
38
|
+
| 'devbox'
|
|
39
|
+
| 'tmux';
|
|
40
|
+
status: StardeckHealthStatus;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StardeckHealthReport {
|
|
45
|
+
packageVersion: string;
|
|
46
|
+
severity: 'info' | 'warning';
|
|
47
|
+
checks: readonly StardeckHealthCheck[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type HealthEvidenceProvider = (
|
|
51
|
+
pi: ExtensionAPI,
|
|
52
|
+
ctx: ExtensionCommandContext,
|
|
53
|
+
) => Promise<StardeckHealthEvidence>;
|
|
54
|
+
|
|
55
|
+
type JsonSettings = { externalEditor?: unknown };
|
|
56
|
+
|
|
57
|
+
const PACKAGE_VERSION = (
|
|
58
|
+
createRequire(import.meta.url)('../package.json') as { version: string }
|
|
59
|
+
).version;
|
|
60
|
+
|
|
61
|
+
const PACKAGE_COMMANDS = [
|
|
62
|
+
'galactica-status',
|
|
63
|
+
'galactica-status-debug',
|
|
64
|
+
'galactica-status-refresh',
|
|
65
|
+
'openspec-focus',
|
|
66
|
+
'pi-status',
|
|
67
|
+
'stardeck-health',
|
|
68
|
+
'work',
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
function check(
|
|
72
|
+
id: StardeckHealthCheck['id'],
|
|
73
|
+
status: StardeckHealthStatus,
|
|
74
|
+
message: string,
|
|
75
|
+
): StardeckHealthCheck {
|
|
76
|
+
return { id, status, message };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasDuplicateCockpitCommand(commandNames: readonly string[]): boolean {
|
|
80
|
+
return commandNames.some((name) =>
|
|
81
|
+
PACKAGE_COMMANDS.some((known) => new RegExp(`^${known}:\\d+$`, 'u').test(name)),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function evaluateStardeckHealth(
|
|
86
|
+
evidence: StardeckHealthEvidence,
|
|
87
|
+
): StardeckHealthReport {
|
|
88
|
+
const duplicate = hasDuplicateCockpitCommand(evidence.commandNames);
|
|
89
|
+
const interactive = evidence.mode === 'tui' && evidence.hasUI;
|
|
90
|
+
const checks: StardeckHealthCheck[] = [
|
|
91
|
+
duplicate
|
|
92
|
+
? check('package', 'WARN', 'duplicate cockpit commands detected')
|
|
93
|
+
: check('package', 'PASS', 'root command set is unique'),
|
|
94
|
+
interactive
|
|
95
|
+
? check('surface', 'PASS', 'interactive TUI available')
|
|
96
|
+
: check('surface', 'WARN', 'run Pi in interactive TUI mode'),
|
|
97
|
+
evidence.externalEditor === 'ready'
|
|
98
|
+
? check('composer', 'PASS', 'external editor is configured and executable')
|
|
99
|
+
: evidence.externalEditor === 'missing'
|
|
100
|
+
? check('composer', 'WARN', 'configure externalEditor for prompt composition')
|
|
101
|
+
: check('composer', 'WARN', 'configured externalEditor is not executable'),
|
|
102
|
+
evidence.neovimTerminal
|
|
103
|
+
? check('topology', 'PASS', 'Neovim terminal detected (recommended)')
|
|
104
|
+
: check('topology', 'INFO', 'plain terminal detected (Neovim recommended)'),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const id of ['git', 'openspec', 'devbox', 'tmux'] as const) {
|
|
108
|
+
checks.push(
|
|
109
|
+
check(
|
|
110
|
+
id,
|
|
111
|
+
'INFO',
|
|
112
|
+
`${evidence.capabilities[id] ? 'available' : 'absent'} (optional)`,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
packageVersion: evidence.packageVersion,
|
|
119
|
+
severity: checks.some(({ status }) => status === 'WARN') ? 'warning' : 'info',
|
|
120
|
+
checks,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatStardeckHealth(report: StardeckHealthReport): string {
|
|
125
|
+
const lines = [`pi-stardeck health ${report.packageVersion}`];
|
|
126
|
+
for (const item of report.checks) {
|
|
127
|
+
lines.push(`${item.status.padEnd(5)} ${item.id} · ${item.message}`);
|
|
128
|
+
}
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readExternalEditor(path: string): Promise<string | undefined> {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(await readFile(path, 'utf8')) as JsonSettings;
|
|
135
|
+
const value = parsed?.externalEditor;
|
|
136
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function isExecutable(command: string, pathValue: string): Promise<boolean> {
|
|
143
|
+
const candidates =
|
|
144
|
+
isAbsolute(command) || command.includes('/')
|
|
145
|
+
? [command]
|
|
146
|
+
: pathValue
|
|
147
|
+
.split(delimiter)
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.map((directory) => join(directory, command));
|
|
150
|
+
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
try {
|
|
153
|
+
await access(candidate, constants.X_OK);
|
|
154
|
+
const metadata = await stat(candidate);
|
|
155
|
+
if (metadata.isFile()) return true;
|
|
156
|
+
} catch {
|
|
157
|
+
// Keep searching the bounded PATH candidates.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function findAncestorEntry(start: string, entry: string): Promise<boolean> {
|
|
164
|
+
let current = start;
|
|
165
|
+
const root = parse(current).root;
|
|
166
|
+
while (true) {
|
|
167
|
+
try {
|
|
168
|
+
await access(join(current, entry));
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
if (current === root) return false;
|
|
172
|
+
current = dirname(current);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function inspectExternalEditor(
|
|
178
|
+
cwd: string,
|
|
179
|
+
agentDir: string,
|
|
180
|
+
pathValue: string,
|
|
181
|
+
): Promise<ExternalEditorHealth> {
|
|
182
|
+
const projectEditor = await readExternalEditor(join(cwd, '.pi', 'settings.json'));
|
|
183
|
+
const globalEditor = await readExternalEditor(join(agentDir, 'settings.json'));
|
|
184
|
+
const command = projectEditor ?? globalEditor;
|
|
185
|
+
if (!command) return 'missing';
|
|
186
|
+
return (await isExecutable(command, pathValue)) ? 'ready' : 'unavailable';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function collectStardeckHealthEvidence(
|
|
190
|
+
pi: ExtensionAPI,
|
|
191
|
+
ctx: ExtensionCommandContext,
|
|
192
|
+
): Promise<StardeckHealthEvidence> {
|
|
193
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
194
|
+
const [externalEditor, git, openspec] = await Promise.all([
|
|
195
|
+
inspectExternalEditor(ctx.cwd, agentDir, process.env.PATH ?? ''),
|
|
196
|
+
findAncestorEntry(ctx.cwd, '.git'),
|
|
197
|
+
findAncestorEntry(ctx.cwd, 'openspec'),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
packageVersion: PACKAGE_VERSION,
|
|
202
|
+
mode: ctx.mode,
|
|
203
|
+
hasUI: ctx.hasUI,
|
|
204
|
+
neovimTerminal: process.env.NVIM !== undefined,
|
|
205
|
+
externalEditor,
|
|
206
|
+
commandNames: pi.getCommands().map(({ name }) => name),
|
|
207
|
+
capabilities: {
|
|
208
|
+
git,
|
|
209
|
+
openspec,
|
|
210
|
+
devbox: process.env.DEVBOX_PROJECT_ROOT !== undefined,
|
|
211
|
+
tmux: process.env.TMUX !== undefined,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function registerStardeckHealth(
|
|
217
|
+
pi: ExtensionAPI,
|
|
218
|
+
provideEvidence: HealthEvidenceProvider = collectStardeckHealthEvidence,
|
|
219
|
+
): void {
|
|
220
|
+
pi.registerCommand('stardeck-health', {
|
|
221
|
+
description: 'Check pi-stardeck readiness and optional capabilities',
|
|
222
|
+
handler: async (_args, ctx) => {
|
|
223
|
+
const report = evaluateStardeckHealth(await provideEvidence(pi, ctx));
|
|
224
|
+
ctx.ui.notify(formatStardeckHealth(report), report.severity);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
import galacticaContextHeader from '../packages/pi-stardeck/index.js';
|
|
4
|
+
import galacticaStatus from '../packages/status/index.js';
|
|
5
|
+
import footerTelemetry from '../packages/footer/index.js';
|
|
6
|
+
import { registerStardeckHealth } from './health.js';
|
|
7
|
+
import { registerPiStatus } from './pi-status.js';
|
|
8
|
+
|
|
9
|
+
export const COCKPIT_SURFACES = {
|
|
10
|
+
contextHeader: 'deck',
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export const COCKPIT_COMPOSITION = [
|
|
14
|
+
'footer:telemetry',
|
|
15
|
+
'galactica-status:provider',
|
|
16
|
+
`galactica-context-header:${COCKPIT_SURFACES.contextHeader}`,
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export default function piStardeck(pi: ExtensionAPI): void {
|
|
20
|
+
registerPiStatus(pi);
|
|
21
|
+
footerTelemetry(pi);
|
|
22
|
+
galacticaStatus(pi);
|
|
23
|
+
galacticaContextHeader(pi, {
|
|
24
|
+
surface: COCKPIT_SURFACES.contextHeader,
|
|
25
|
+
});
|
|
26
|
+
registerStardeckHealth(pi);
|
|
27
|
+
}
|