@aemonge-dev/pi-stardeck 0.0.0-stage → 0.5.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/demo/pi-stardeck.tape +59 -0
  4. package/docs/architecture.md +103 -0
  5. package/docs/assets/pi-stardeck.gif +0 -0
  6. package/docs/development.md +128 -0
  7. package/licenses/README.md +17 -0
  8. package/licenses/pi-fancy-footer-LICENSE +21 -0
  9. package/licenses/pi-vim-LICENSE +21 -0
  10. package/package.json +81 -4
  11. package/packages/pi-fancy-footer-full-palette/LICENSE +21 -0
  12. package/packages/pi-fancy-footer-full-palette/LOCAL-CHANGES.md +41 -0
  13. package/packages/pi-fancy-footer-full-palette/README.md +468 -0
  14. package/packages/pi-fancy-footer-full-palette/package.json +52 -0
  15. package/packages/pi-fancy-footer-full-palette/src/api.ts +204 -0
  16. package/packages/pi-fancy-footer-full-palette/src/config-editor.ts +634 -0
  17. package/packages/pi-fancy-footer-full-palette/src/config.ts +852 -0
  18. package/packages/pi-fancy-footer-full-palette/src/data-widgets.ts +259 -0
  19. package/packages/pi-fancy-footer-full-palette/src/git.ts +130 -0
  20. package/packages/pi-fancy-footer-full-palette/src/index.ts +479 -0
  21. package/packages/pi-fancy-footer-full-palette/src/layout.ts +309 -0
  22. package/packages/pi-fancy-footer-full-palette/src/provider-status.ts +1466 -0
  23. package/packages/pi-fancy-footer-full-palette/src/render.ts +1061 -0
  24. package/packages/pi-fancy-footer-full-palette/src/shared.ts +903 -0
  25. package/packages/pi-stardeck/README.md +219 -0
  26. package/packages/pi-stardeck/index.ts +691 -0
  27. package/packages/pi-stardeck/package.json +32 -0
  28. package/packages/pi-stardeck/src/activity-age.ts +184 -0
  29. package/packages/pi-stardeck/src/capabilities.ts +105 -0
  30. package/packages/pi-stardeck/src/deck.ts +807 -0
  31. package/packages/pi-stardeck/src/gauge.ts +1323 -0
  32. package/packages/pi-stardeck/src/git-refresh.ts +5 -0
  33. package/packages/pi-stardeck/src/process-resources.ts +428 -0
  34. package/packages/pi-stardeck/src/title-state.ts +195 -0
  35. package/packages/pi-stardeck/src/title.ts +443 -0
  36. package/packages/pi-vim-top-border/LICENSE +21 -0
  37. package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
  38. package/packages/pi-vim-top-border/README.md +577 -0
  39. package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
  40. package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
  41. package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
  42. package/packages/pi-vim-top-border/index.ts +4195 -0
  43. package/packages/pi-vim-top-border/input-keys.ts +49 -0
  44. package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
  45. package/packages/pi-vim-top-border/mode-colors.ts +97 -0
  46. package/packages/pi-vim-top-border/mode-label.ts +62 -0
  47. package/packages/pi-vim-top-border/motions.ts +297 -0
  48. package/packages/pi-vim-top-border/package.json +47 -0
  49. package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
  50. package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
  51. package/packages/pi-vim-top-border/prompt-status.ts +131 -0
  52. package/packages/pi-vim-top-border/settings.ts +287 -0
  53. package/packages/pi-vim-top-border/text-objects.ts +389 -0
  54. package/packages/pi-vim-top-border/types.ts +51 -0
  55. package/packages/pi-vim-top-border/visual.ts +65 -0
  56. package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
  57. package/packages/status/README.md +398 -0
  58. package/packages/status/index.ts +1717 -0
  59. package/packages/status/package.json +33 -0
  60. package/packages/status/src/active-work.ts +235 -0
  61. package/packages/status/src/config.ts +218 -0
  62. package/packages/status/src/diagnostics.ts +238 -0
  63. package/packages/status/src/goal-block.ts +377 -0
  64. package/packages/status/src/goal.ts +170 -0
  65. package/packages/status/src/ledger.ts +101 -0
  66. package/packages/status/src/openspec.ts +394 -0
  67. package/packages/status/src/orchestration.ts +237 -0
  68. package/packages/status/src/publisher.ts +1294 -0
  69. package/packages/status/src/reducer.ts +190 -0
  70. package/packages/status/src/runtime-runs.ts +267 -0
  71. package/packages/status/src/state.ts +178 -0
  72. package/packages/status/src/title-activity.ts +224 -0
  73. package/packages/status/src/title-state.ts +218 -0
  74. package/packages/status/src/turn-cost.ts +63 -0
  75. package/packages/status/src/types.ts +228 -0
  76. package/src/deck-surface.ts +46 -0
  77. package/src/health.ts +228 -0
  78. package/src/index.ts +28 -0
  79. package/src/pi-status.ts +334 -0
@@ -0,0 +1,237 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import type {
4
+ GalacticaStatusConfig,
5
+ OrchestrationState,
6
+ WorkflowState,
7
+ } from './types.ts';
8
+
9
+ const ORCHESTRATION_CANDIDATES = [
10
+ '.pi/status/orchestration.json',
11
+ '.pi/orchestration.json',
12
+ '.taskflow/state.json',
13
+ '.taskflow/run-state.json',
14
+ '.galactica/orchestration.json',
15
+ ] as const;
16
+
17
+ function isRecord(value: unknown): value is Record<string, unknown> {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+
21
+ function text(value: unknown): string | undefined {
22
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
23
+ }
24
+
25
+ function number(value: unknown): number | undefined {
26
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
27
+ }
28
+
29
+ function timestamp(value: unknown): number | undefined {
30
+ const numeric = number(value);
31
+ if (numeric !== undefined) return numeric;
32
+ const string = text(value);
33
+ if (!string) return undefined;
34
+ const parsed = Date.parse(string);
35
+ return Number.isFinite(parsed) ? parsed : undefined;
36
+ }
37
+
38
+ function normalizeWorkflowState(value: unknown): WorkflowState {
39
+ const state = text(value)?.toLowerCase();
40
+ switch (state) {
41
+ case 'running':
42
+ case 'active':
43
+ case 'in-progress':
44
+ case 'working':
45
+ return 'running';
46
+ case 'blocked':
47
+ case 'failed':
48
+ case 'error':
49
+ case 'failed-verification':
50
+ return 'blocked';
51
+ case 'complete':
52
+ case 'completed':
53
+ case 'done':
54
+ case 'success':
55
+ return 'complete';
56
+ case 'idle':
57
+ case 'paused':
58
+ case 'waiting':
59
+ return 'idle';
60
+ default:
61
+ return 'unknown';
62
+ }
63
+ }
64
+
65
+ function workerList(value: Record<string, unknown>): unknown[] {
66
+ return Array.isArray(value.workers)
67
+ ? value.workers
68
+ : Array.isArray(value.agents)
69
+ ? value.agents
70
+ : [];
71
+ }
72
+
73
+ function activeWorkerCount(value: Record<string, unknown>): number {
74
+ const explicit = number(
75
+ value.activeWorkers ?? value.workerCount ?? value.activeAgents ?? value.agentCount,
76
+ );
77
+ if (explicit !== undefined) return Math.max(0, Math.trunc(explicit));
78
+
79
+ const workers = workerList(value);
80
+ return workers.filter((worker) => {
81
+ if (!isRecord(worker)) return false;
82
+ return ['running', 'active', 'working', 'in-progress'].includes(
83
+ text(worker.status ?? worker.state)?.toLowerCase() ?? '',
84
+ );
85
+ }).length;
86
+ }
87
+
88
+ export function parseOrchestrationState(
89
+ value: unknown,
90
+ options: { source: string; now?: number; fallbackUpdatedAt?: number },
91
+ ): OrchestrationState {
92
+ if (!isRecord(value)) throw new Error('orchestration state is not an object');
93
+ const now = options.now ?? Date.now();
94
+ const workflow = isRecord(value.workflow) ? value.workflow : {};
95
+ const openspec = isRecord(value.openspec) ? value.openspec : {};
96
+ const task = isRecord(value.task) ? value.task : {};
97
+
98
+ const phase = text(value.phase ?? workflow.phase ?? workflow.node ?? value.role);
99
+ const state = normalizeWorkflowState(value.state ?? value.status ?? workflow.state);
100
+ const currentTask = text(
101
+ value.currentTask ??
102
+ value.current_task ??
103
+ task.title ??
104
+ task.name ??
105
+ value.blockedReason,
106
+ );
107
+ const openspecTaskId = text(
108
+ value.openspecTaskId ??
109
+ value.openspec_task_id ??
110
+ openspec.taskId ??
111
+ openspec.task_id,
112
+ );
113
+ const startedAt = timestamp(
114
+ value.startedAt ?? value.started_at ?? workflow.startedAt ?? workflow.started_at,
115
+ );
116
+ const explicitElapsed = number(
117
+ value.elapsedMs ?? value.elapsed_ms ?? workflow.elapsedMs,
118
+ );
119
+ const elapsedMs =
120
+ explicitElapsed ?? (startedAt ? Math.max(0, now - startedAt) : undefined);
121
+ const updatedAt =
122
+ timestamp(value.updatedAt ?? value.updated_at ?? value.timestamp) ??
123
+ options.fallbackUpdatedAt ??
124
+ now;
125
+ const staleAfterMs = 600_000;
126
+ const activeWorkers = activeWorkerCount(value);
127
+ const explicitTotalWorkers = number(
128
+ value.totalWorkers ?? value.totalWorkerCount ?? value.totalAgents,
129
+ );
130
+ const totalWorkers = Math.max(
131
+ activeWorkers,
132
+ Math.trunc(explicitTotalWorkers ?? workerList(value).length),
133
+ );
134
+ const progress = isRecord(value.progress) ? value.progress : {};
135
+ const fileProgress = isRecord(value.files)
136
+ ? value.files
137
+ : isRecord(progress.files)
138
+ ? progress.files
139
+ : {};
140
+ const rawCompletedFiles = number(
141
+ value.completedFiles ?? value.processedFiles ?? fileProgress.completed,
142
+ );
143
+ const rawTotalFiles = number(value.totalFiles ?? fileProgress.total);
144
+ const totalFiles =
145
+ rawTotalFiles === undefined ? undefined : Math.max(0, Math.trunc(rawTotalFiles));
146
+ const completedFiles =
147
+ rawCompletedFiles === undefined || totalFiles === undefined
148
+ ? undefined
149
+ : Math.max(0, Math.min(totalFiles, Math.trunc(rawCompletedFiles)));
150
+
151
+ if (
152
+ state === 'unknown' &&
153
+ !phase &&
154
+ !currentTask &&
155
+ activeWorkers === 0 &&
156
+ elapsedMs === undefined
157
+ ) {
158
+ throw new Error('orchestration state has no recognized workflow fields');
159
+ }
160
+
161
+ return {
162
+ ...(phase ? { phase } : {}),
163
+ state,
164
+ activeWorkers,
165
+ totalWorkers,
166
+ ...(completedFiles === undefined ? {} : { completedFiles }),
167
+ ...(totalFiles === undefined ? {} : { totalFiles }),
168
+ ...(currentTask ? { currentTask } : {}),
169
+ ...(openspecTaskId ? { openspecTaskId } : {}),
170
+ ...(startedAt ? { startedAt } : {}),
171
+ ...(elapsedMs !== undefined ? { elapsedMs: Math.max(0, elapsedMs) } : {}),
172
+ meta: {
173
+ source: options.source,
174
+ refreshedAt: now,
175
+ attemptedAt: now,
176
+ updatedAt,
177
+ staleAfterMs,
178
+ stale: now - updatedAt > staleAfterMs,
179
+ },
180
+ };
181
+ }
182
+
183
+ async function existingFile(paths: readonly string[]): Promise<string | undefined> {
184
+ for (const path of paths) {
185
+ try {
186
+ if ((await fs.stat(path)).isFile()) return path;
187
+ } catch {
188
+ // Optional state files may only exist while a workflow is active.
189
+ }
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ export async function detectOrchestrationPath(
195
+ projectRoot: string,
196
+ configuredPath?: string | null,
197
+ ): Promise<string | undefined> {
198
+ if (configuredPath) {
199
+ const path = isAbsolute(configuredPath)
200
+ ? configuredPath
201
+ : resolve(projectRoot, configuredPath);
202
+ return (await existingFile([path])) ?? path;
203
+ }
204
+ return existingFile(
205
+ ORCHESTRATION_CANDIDATES.map((path) => resolve(projectRoot, path)),
206
+ );
207
+ }
208
+
209
+ export async function collectOrchestration(options: {
210
+ projectRoot: string;
211
+ config: GalacticaStatusConfig['orchestration'];
212
+ now?: number;
213
+ }): Promise<{ state?: OrchestrationState; sourcePath?: string }> {
214
+ const sourcePath = await detectOrchestrationPath(
215
+ options.projectRoot,
216
+ options.config.stateFile,
217
+ );
218
+ if (!sourcePath) return {};
219
+
220
+ try {
221
+ const stat = await fs.stat(sourcePath);
222
+ const parsed = JSON.parse(await fs.readFile(sourcePath, 'utf8')) as unknown;
223
+ return {
224
+ sourcePath,
225
+ state: parseOrchestrationState(parsed, {
226
+ source: sourcePath,
227
+ now: options.now,
228
+ fallbackUpdatedAt: stat.mtimeMs,
229
+ }),
230
+ };
231
+ } catch (error) {
232
+ const code = isRecord(error) ? error.code : undefined;
233
+ if (code === 'ENOENT' && !options.config.stateFile) return {};
234
+ const message = error instanceof Error ? error.message : String(error);
235
+ throw new Error(`failed to read orchestration state: ${message}`);
236
+ }
237
+ }