@hartvig/developer-control-center 0.8.6 → 0.8.8
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/.developer-control-center/metrics.json +1 -1
- package/.developer-control-center/status.json +1 -1
- package/.developer-control-center/timings.jsonl +25 -0
- package/.github/workflows/ci.yml +1 -7
- package/coverage/Developer Control Center/dcc.config.js.html +628 -0
- package/coverage/Developer Control Center/index.html +116 -0
- package/coverage/Developer Control Center/src/config/index.html +116 -0
- package/coverage/Developer Control Center/src/config/loader.ts.html +454 -0
- package/coverage/Developer Control Center/src/core/ci.ts.html +163 -0
- package/coverage/Developer Control Center/src/core/event-bus.ts.html +187 -0
- package/coverage/Developer Control Center/src/core/index.html +191 -0
- package/coverage/Developer Control Center/src/core/notifier.ts.html +187 -0
- package/coverage/Developer Control Center/src/core/persistence.ts.html +88 -0
- package/coverage/Developer Control Center/src/core/task-runner.ts.html +1498 -0
- package/coverage/Developer Control Center/src/core/workspaces.ts.html +304 -0
- package/coverage/Developer Control Center/src/plugins/index.html +116 -0
- package/coverage/Developer Control Center/src/plugins/manager.ts.html +259 -0
- package/coverage/Developer Control Center/src/status/index.html +116 -0
- package/coverage/Developer Control Center/src/status/store.ts.html +349 -0
- package/coverage/Developer Control Center/src/ui/command-list.tsx.html +574 -0
- package/coverage/Developer Control Center/src/ui/index.html +161 -0
- package/coverage/Developer Control Center/src/ui/metrics-panel.tsx.html +787 -0
- package/coverage/Developer Control Center/src/ui/panel.tsx.html +313 -0
- package/coverage/Developer Control Center/src/ui/status-panel.tsx.html +565 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +588 -0
- package/coverage/coverage-final.json +15 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dcc.config.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/core/persistence.d.ts +2 -0
- package/dist/core/persistence.d.ts.map +1 -0
- package/dist/core/persistence.js +2 -0
- package/dist/core/persistence.js.map +1 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +5 -3
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/task-runner.d.ts +1 -0
- package/dist/core/task-runner.d.ts.map +1 -1
- package/dist/core/task-runner.js +81 -24
- package/dist/core/task-runner.js.map +1 -1
- package/dist/core/task-runner.test.d.ts +2 -0
- package/dist/core/task-runner.test.d.ts.map +1 -0
- package/dist/core/task-runner.test.js +326 -0
- package/dist/core/task-runner.test.js.map +1 -0
- package/dist/core/timer-plugin.d.ts.map +1 -1
- package/dist/core/timer-plugin.js +2 -1
- package/dist/core/timer-plugin.js.map +1 -1
- package/dist/plugins/manager.d.ts +2 -0
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +6 -2
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/manager.test.js +5 -2
- package/dist/plugins/manager.test.js.map +1 -1
- package/dist/ui/app.d.ts.map +1 -1
- package/dist/ui/app.js +124 -30
- package/dist/ui/app.js.map +1 -1
- package/dist/ui/app.test.d.ts +2 -0
- package/dist/ui/app.test.d.ts.map +1 -0
- package/dist/ui/app.test.js +157 -0
- package/dist/ui/app.test.js.map +1 -0
- package/dist/ui/command-list.test.d.ts +2 -0
- package/dist/ui/command-list.test.d.ts.map +1 -0
- package/dist/ui/command-list.test.js +104 -0
- package/dist/ui/command-list.test.js.map +1 -0
- package/dist/ui/metrics-panel.d.ts.map +1 -1
- package/dist/ui/metrics-panel.js +10 -9
- package/dist/ui/metrics-panel.js.map +1 -1
- package/dist/ui/metrics-panel.test.d.ts +2 -0
- package/dist/ui/metrics-panel.test.d.ts.map +1 -0
- package/dist/ui/metrics-panel.test.js +111 -0
- package/dist/ui/metrics-panel.test.js.map +1 -0
- package/dist/ui/panel.test.d.ts +2 -0
- package/dist/ui/panel.test.d.ts.map +1 -0
- package/dist/ui/panel.test.js +51 -0
- package/dist/ui/panel.test.js.map +1 -0
- package/dist/ui/status-panel.test.d.ts +2 -0
- package/dist/ui/status-panel.test.d.ts.map +1 -0
- package/dist/ui/status-panel.test.js +88 -0
- package/dist/ui/status-panel.test.js.map +1 -0
- package/package.json +4 -2
- package/src/cli.ts +1 -1
- package/src/core/persistence.ts +1 -0
- package/src/core/runtime.ts +7 -3
- package/src/core/task-runner.test.ts +395 -0
- package/src/core/task-runner.ts +80 -24
- package/src/core/timer-plugin.ts +2 -1
- package/src/plugins/manager.test.ts +5 -2
- package/src/plugins/manager.ts +6 -2
- package/src/ui/app.test.tsx +177 -0
- package/src/ui/app.tsx +167 -41
- package/src/ui/command-list.test.tsx +124 -0
- package/src/ui/metrics-panel.test.tsx +128 -0
- package/src/ui/metrics-panel.tsx +10 -10
- package/src/ui/panel.test.tsx +84 -0
- package/src/ui/status-panel.test.tsx +116 -0
- package/vitest.config.ts +1 -1
package/src/core/task-runner.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn,
|
|
1
|
+
import { spawn, exec, ChildProcess } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { ProkomCommand } from '../config/types.js';
|
|
4
4
|
import { StatusStore } from '../status/store.js';
|
|
@@ -8,16 +8,19 @@ import { EventBus } from './event-bus.js';
|
|
|
8
8
|
class FileWatcher {
|
|
9
9
|
private watcher?: fs.FSWatcher;
|
|
10
10
|
private debounceTimer?: ReturnType<typeof setTimeout>;
|
|
11
|
+
private exclude = /[/\\](node_modules|\.git)[/\\]/;
|
|
11
12
|
|
|
12
|
-
watch(dir: string, onChange: () => void):
|
|
13
|
+
watch(dir: string, onChange: () => void): boolean {
|
|
13
14
|
this.stop();
|
|
14
15
|
try {
|
|
15
|
-
this.watcher = fs.watch(dir, { recursive: true }, () => {
|
|
16
|
+
this.watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
|
|
17
|
+
if (filename && this.exclude.test(filename.toString())) return;
|
|
16
18
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
17
19
|
this.debounceTimer = setTimeout(onChange, 500);
|
|
18
20
|
});
|
|
21
|
+
return true;
|
|
19
22
|
} catch {
|
|
20
|
-
|
|
23
|
+
return false;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -62,7 +65,8 @@ export class TaskRunner {
|
|
|
62
65
|
|
|
63
66
|
const existing = this.statusStore.getTask(command.id);
|
|
64
67
|
if (existing?.status === 'running') {
|
|
65
|
-
return;
|
|
68
|
+
if (!command.parallel) return;
|
|
69
|
+
this.abort(command.id);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
await this.pluginManager?.executeHook('beforeRun', command);
|
|
@@ -82,12 +86,26 @@ export class TaskRunner {
|
|
|
82
86
|
|
|
83
87
|
this.eventBus.emit('task:start', command);
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
let child: ChildProcess;
|
|
90
|
+
try {
|
|
91
|
+
child = spawn(command.command, [], {
|
|
92
|
+
shell: true,
|
|
93
|
+
detached: true,
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
cwd: command.cwd,
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.statusStore.updateTask(command.id, {
|
|
100
|
+
status: 'failure',
|
|
101
|
+
output: `Failed to spawn: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
102
|
+
exitCode: -1,
|
|
103
|
+
endTime: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
this.eventBus.emit('task:error', command.id, err instanceof Error ? err : new Error(String(err)));
|
|
106
|
+
this.pluginManager?.executeHook('onError', command.id, err instanceof Error ? err : new Error(String(err)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
91
109
|
|
|
92
110
|
this.running.set(command.id, child);
|
|
93
111
|
|
|
@@ -95,6 +113,16 @@ export class TaskRunner {
|
|
|
95
113
|
this.startCheck(command);
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
let timedOut = false;
|
|
117
|
+
const timeoutMs = command.timeout ?? 0;
|
|
118
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
119
|
+
if (timeoutMs > 0) {
|
|
120
|
+
timeoutTimer = setTimeout(() => {
|
|
121
|
+
timedOut = true;
|
|
122
|
+
this.abort(command.id);
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
let output = '';
|
|
99
127
|
|
|
100
128
|
child.stdout?.on('data', (data: Buffer) => {
|
|
@@ -114,6 +142,7 @@ export class TaskRunner {
|
|
|
114
142
|
});
|
|
115
143
|
|
|
116
144
|
child.on('close', (exitCode) => {
|
|
145
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
117
146
|
this.running.delete(command.id);
|
|
118
147
|
|
|
119
148
|
if (command.toggle?.check) return;
|
|
@@ -169,6 +198,14 @@ export class TaskRunner {
|
|
|
169
198
|
}
|
|
170
199
|
this.stopWatcher(id);
|
|
171
200
|
this.stopCheck(id);
|
|
201
|
+
const existing = this.statusStore.getTask(id);
|
|
202
|
+
if (existing?.status === 'running') {
|
|
203
|
+
this.statusStore.updateTask(id, {
|
|
204
|
+
status: 'failure',
|
|
205
|
+
endTime: Date.now(),
|
|
206
|
+
});
|
|
207
|
+
this.eventBus.emit('task:complete', id, -1);
|
|
208
|
+
}
|
|
172
209
|
}
|
|
173
210
|
|
|
174
211
|
abortAll(): void {
|
|
@@ -200,12 +237,27 @@ export class TaskRunner {
|
|
|
200
237
|
});
|
|
201
238
|
this.eventBus.emit('task:complete', command.id, 0);
|
|
202
239
|
if (command.toggle?.stop) {
|
|
203
|
-
|
|
240
|
+
const stopCmd = process.platform === 'win32'
|
|
241
|
+
? this.windowsStopCommand(command.toggle.stop)
|
|
242
|
+
: command.toggle.stop;
|
|
243
|
+
spawn(stopCmd, {
|
|
204
244
|
shell: true,
|
|
205
245
|
stdio: 'ignore',
|
|
206
246
|
cwd: command.cwd,
|
|
207
|
-
});
|
|
247
|
+
}).unref();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private windowsStopCommand(cmd: string): string {
|
|
252
|
+
const pkillMatch = cmd.match(/^pkill\s+-f\s+(.+)$/);
|
|
253
|
+
if (pkillMatch) {
|
|
254
|
+
return `taskkill /F /IM ${pkillMatch[1]}* 2>nul`;
|
|
208
255
|
}
|
|
256
|
+
const killMatch = cmd.match(/^kill\s+-?\d*\s+(\d+)$/);
|
|
257
|
+
if (killMatch) {
|
|
258
|
+
return `taskkill /F /PID ${killMatch[1]} 2>nul`;
|
|
259
|
+
}
|
|
260
|
+
return cmd;
|
|
209
261
|
}
|
|
210
262
|
|
|
211
263
|
private async runPipeline(command: ProkomCommand): Promise<void> {
|
|
@@ -388,9 +440,13 @@ export class TaskRunner {
|
|
|
388
440
|
|
|
389
441
|
private startWatcher(command: ProkomCommand): void {
|
|
390
442
|
const watcher = new FileWatcher();
|
|
391
|
-
watcher.watch(process.cwd(), () => {
|
|
443
|
+
const ok = watcher.watch(process.cwd(), () => {
|
|
392
444
|
this.run(command);
|
|
393
445
|
});
|
|
446
|
+
if (!ok) {
|
|
447
|
+
this.eventBus.emit('task:output', command.id, `watch mode: recursive file watching not supported\n`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
394
450
|
this.watchers.set(command.id, watcher);
|
|
395
451
|
this.statusStore.updateTask(command.id, { watchMode: true });
|
|
396
452
|
}
|
|
@@ -425,16 +481,16 @@ export class TaskRunner {
|
|
|
425
481
|
|
|
426
482
|
const checkCmd = command.toggle!.check!;
|
|
427
483
|
const interval = setInterval(() => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
484
|
+
exec(checkCmd, { encoding: 'utf-8', timeout: 3000 }, (err) => {
|
|
485
|
+
if (err) {
|
|
486
|
+
this.statusStore.updateTask(id, {
|
|
487
|
+
status: 'failure',
|
|
488
|
+
endTime: Date.now(),
|
|
489
|
+
});
|
|
490
|
+
this.eventBus.emit('task:complete', id, -1);
|
|
491
|
+
this.stopCheck(id);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
438
494
|
}, this.CHECK_INTERVAL);
|
|
439
495
|
|
|
440
496
|
this.checkIntervals.set(id, interval);
|
package/src/core/timer-plugin.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { Plugin } from '../plugins/types.js';
|
|
4
|
+
import { PERSISTENCE_DIR } from './persistence.js';
|
|
4
5
|
|
|
5
6
|
const startTimes = new Map<string, number>();
|
|
6
|
-
const TIMINGS_FILE = path.join(process.cwd(),
|
|
7
|
+
const TIMINGS_FILE = path.join(process.cwd(), PERSISTENCE_DIR, 'timings.jsonl');
|
|
7
8
|
|
|
8
9
|
export const timerPlugin: Plugin = {
|
|
9
10
|
id: 'timer',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { PluginManager } from './manager.js';
|
|
3
3
|
import type { Plugin } from './types.js';
|
|
4
4
|
|
|
@@ -68,7 +68,8 @@ describe('PluginManager', () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
it('does not throw when a hook fails', async () => {
|
|
71
|
-
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
pm = new PluginManager((msg) => errors.push(msg));
|
|
72
73
|
pm.register({
|
|
73
74
|
id: 'bad',
|
|
74
75
|
name: 'Bad',
|
|
@@ -77,6 +78,8 @@ describe('PluginManager', () => {
|
|
|
77
78
|
await expect(
|
|
78
79
|
pm.executeHook('beforeRun', { id: 'x', label: 'X', command: 'x' }),
|
|
79
80
|
).resolves.toBeUndefined();
|
|
81
|
+
expect(errors.length).toBe(1);
|
|
82
|
+
expect(errors[0]).toContain('bad');
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
it('skips plugin with no hooks for the requested hook', async () => {
|
package/src/plugins/manager.ts
CHANGED
|
@@ -5,6 +5,10 @@ type HookName = keyof PluginHooks;
|
|
|
5
5
|
export class PluginManager {
|
|
6
6
|
private plugins = new Map<string, Plugin>();
|
|
7
7
|
|
|
8
|
+
constructor(
|
|
9
|
+
private onError?: (msg: string, err: unknown) => void,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
8
12
|
register(plugin: Plugin): void {
|
|
9
13
|
this.plugins.set(plugin.id, plugin);
|
|
10
14
|
}
|
|
@@ -33,7 +37,7 @@ export class PluginManager {
|
|
|
33
37
|
try {
|
|
34
38
|
await Promise.resolve(fn(...args));
|
|
35
39
|
} catch (e) {
|
|
36
|
-
|
|
40
|
+
this.onError?.(`Plugin ${plugin.id} ${hook} error`, e);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
}
|
|
@@ -47,7 +51,7 @@ export class PluginManager {
|
|
|
47
51
|
const p: Plugin = mod.default || mod;
|
|
48
52
|
this.register(p);
|
|
49
53
|
} catch (e) {
|
|
50
|
-
|
|
54
|
+
this.onError?.(`Failed to load plugin "${name}"`, e);
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import React, { act } from 'react';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { App } from './app.js';
|
|
5
|
+
import { EventBus } from '../core/event-bus.js';
|
|
6
|
+
import { StatusStore } from '../status/store.js';
|
|
7
|
+
|
|
8
|
+
function createMockRuntime() {
|
|
9
|
+
const eventBus = new EventBus();
|
|
10
|
+
const statusStore = new StatusStore();
|
|
11
|
+
const taskRunner = {
|
|
12
|
+
run: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
stop: vi.fn(),
|
|
14
|
+
abortAll: vi.fn(),
|
|
15
|
+
setCommands: vi.fn(),
|
|
16
|
+
abort: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
eventBus,
|
|
20
|
+
statusStore,
|
|
21
|
+
taskRunner,
|
|
22
|
+
pluginManager: { register: vi.fn(), executeHook: vi.fn(), loadFromConfig: vi.fn() },
|
|
23
|
+
gitBranch: 'main',
|
|
24
|
+
workspaces: [] as { name: string; path: string }[],
|
|
25
|
+
ci: { isCI: false, name: undefined as string | undefined },
|
|
26
|
+
stop: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const baseConfig = {
|
|
31
|
+
name: 'test-project',
|
|
32
|
+
commands: [
|
|
33
|
+
{ id: 'build', label: 'Build', command: 'npm run build', description: 'Build the project' },
|
|
34
|
+
{ id: 'test', label: 'Test', command: 'npm test' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('App', () => {
|
|
39
|
+
let runtime: ReturnType<typeof createMockRuntime>;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
runtime = createMockRuntime() as any;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders the project name in the header', () => {
|
|
50
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
51
|
+
expect(lastFrame()).toContain('test-project');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('shows the git branch', () => {
|
|
55
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
56
|
+
expect(lastFrame()).toContain('⎇ main');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders command items', () => {
|
|
60
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
61
|
+
const frame = lastFrame();
|
|
62
|
+
expect(frame).toContain('Build');
|
|
63
|
+
expect(frame).toContain('Test');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows no tasks yet in output panel', () => {
|
|
67
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
68
|
+
expect(lastFrame()).toContain('No tasks yet');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('shows selected command description in footer', () => {
|
|
72
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
73
|
+
expect(lastFrame()).toContain('Build the project');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders with groups', () => {
|
|
77
|
+
const config = {
|
|
78
|
+
name: 'test',
|
|
79
|
+
commands: [
|
|
80
|
+
{ id: 'build', label: 'Build', command: 'npm run build', group: 'Build' },
|
|
81
|
+
{ id: 'test', label: 'Test', command: 'npm test', group: 'Test' },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
const { lastFrame } = render(<App config={config} runtime={runtime as any} />);
|
|
85
|
+
expect(lastFrame()).toContain('▶ Build');
|
|
86
|
+
expect(lastFrame()).toContain('▶ Test');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('shows active profile in header', () => {
|
|
90
|
+
const config = {
|
|
91
|
+
name: 'test',
|
|
92
|
+
commands: [{ id: 'build', label: 'Build', command: 'npm run build' }],
|
|
93
|
+
profiles: { prod: { commands: [] } },
|
|
94
|
+
profile: 'prod',
|
|
95
|
+
};
|
|
96
|
+
const { lastFrame } = render(<App config={config} runtime={runtime as any} />);
|
|
97
|
+
expect(lastFrame()).toContain('⚙ prod');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('shows workspace count when present', () => {
|
|
101
|
+
runtime.workspaces = [{ name: 'pkg-a', path: '/pkg-a' }];
|
|
102
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
103
|
+
expect(lastFrame()).toContain('⊞ 1');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('handles status store updates', () => {
|
|
107
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
108
|
+
act(() => {
|
|
109
|
+
runtime.statusStore.updateTask('build', {
|
|
110
|
+
id: 'build',
|
|
111
|
+
label: 'Build',
|
|
112
|
+
status: 'running',
|
|
113
|
+
output: 'compiling...',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
const frame = lastFrame();
|
|
117
|
+
expect(frame).toContain('compiling...');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('shows confirm prompt for confirm commands', () => {
|
|
121
|
+
const config = {
|
|
122
|
+
name: 'test',
|
|
123
|
+
commands: [
|
|
124
|
+
{ id: 'deploy', label: 'Deploy', command: 'npm run deploy', confirm: true },
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
const { lastFrame, stdin } = render(<App config={config} runtime={runtime as any} />);
|
|
128
|
+
act(() => { stdin.write('\r'); });
|
|
129
|
+
const frame = lastFrame();
|
|
130
|
+
expect(frame).toContain('⚠');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('shows DCC in header', () => {
|
|
134
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
135
|
+
expect(lastFrame()).toContain('DCC');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('shows status pane (middle panel)', () => {
|
|
139
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
140
|
+
expect(lastFrame()).toContain('Status');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('shows output pane (right panel)', () => {
|
|
144
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
145
|
+
expect(lastFrame()).toContain('Output');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('shows CI badge when in CI environment', () => {
|
|
149
|
+
runtime.ci = { isCI: true, name: 'GitHub Actions' };
|
|
150
|
+
const { lastFrame } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
151
|
+
expect(lastFrame()).toContain('⊡ GitHub Actions');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('switches pane focus on Tab', () => {
|
|
155
|
+
const { lastFrame, stdin } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
156
|
+
act(() => { stdin.write('\t'); });
|
|
157
|
+
const frame = lastFrame();
|
|
158
|
+
expect(frame).toContain('Output');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('enters search mode on / key', () => {
|
|
162
|
+
const { lastFrame, stdin } = render(<App config={baseConfig} runtime={runtime as any} />);
|
|
163
|
+
act(() => { stdin.write('/'); });
|
|
164
|
+
expect(lastFrame()).toContain('🔍');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shows default footer hint for command without description', () => {
|
|
168
|
+
const config = {
|
|
169
|
+
name: 'test',
|
|
170
|
+
commands: [
|
|
171
|
+
{ id: 'foo', label: 'Foo', command: 'echo foo' },
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const { lastFrame } = render(<App config={config} runtime={runtime as any} />);
|
|
175
|
+
expect(lastFrame()).toContain('Enter to run');
|
|
176
|
+
});
|
|
177
|
+
});
|