@hartvig/developer-control-center 0.8.2

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 (193) hide show
  1. package/.developer-control-center/metrics.json +1 -0
  2. package/.developer-control-center/status.json +1 -0
  3. package/.developer-control-center/timings.jsonl +3 -0
  4. package/.github/workflows/ci.yml +47 -0
  5. package/AGENTS.md +51 -0
  6. package/PLUGINS.md +145 -0
  7. package/README.md +147 -0
  8. package/developer-control-center.config.example.js +91 -0
  9. package/developer-control-center.config.js +177 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +223 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config/index.d.ts +3 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/index.js +2 -0
  17. package/dist/config/index.js.map +1 -0
  18. package/dist/config/loader.d.ts +4 -0
  19. package/dist/config/loader.d.ts.map +1 -0
  20. package/dist/config/loader.js +96 -0
  21. package/dist/config/loader.js.map +1 -0
  22. package/dist/config/loader.test.d.ts +2 -0
  23. package/dist/config/loader.test.d.ts.map +1 -0
  24. package/dist/config/loader.test.js +25 -0
  25. package/dist/config/loader.test.js.map +1 -0
  26. package/dist/config/presets/node.d.ts +10 -0
  27. package/dist/config/presets/node.d.ts.map +1 -0
  28. package/dist/config/presets/node.js +31 -0
  29. package/dist/config/presets/node.js.map +1 -0
  30. package/dist/config/presets/react.d.ts +10 -0
  31. package/dist/config/presets/react.d.ts.map +1 -0
  32. package/dist/config/presets/react.js +36 -0
  33. package/dist/config/presets/react.js.map +1 -0
  34. package/dist/config/types.d.ts +55 -0
  35. package/dist/config/types.d.ts.map +1 -0
  36. package/dist/config/types.js +2 -0
  37. package/dist/config/types.js.map +1 -0
  38. package/dist/config/types.test.d.ts +2 -0
  39. package/dist/config/types.test.d.ts.map +1 -0
  40. package/dist/config/types.test.js +23 -0
  41. package/dist/config/types.test.js.map +1 -0
  42. package/dist/core/ci.d.ts +6 -0
  43. package/dist/core/ci.d.ts.map +1 -0
  44. package/dist/core/ci.js +22 -0
  45. package/dist/core/ci.js.map +1 -0
  46. package/dist/core/ci.test.d.ts +2 -0
  47. package/dist/core/ci.test.d.ts.map +1 -0
  48. package/dist/core/ci.test.js +45 -0
  49. package/dist/core/ci.test.js.map +1 -0
  50. package/dist/core/event-bus.d.ts +18 -0
  51. package/dist/core/event-bus.d.ts.map +1 -0
  52. package/dist/core/event-bus.js +19 -0
  53. package/dist/core/event-bus.js.map +1 -0
  54. package/dist/core/event-bus.test.d.ts +2 -0
  55. package/dist/core/event-bus.test.d.ts.map +1 -0
  56. package/dist/core/event-bus.test.js +49 -0
  57. package/dist/core/event-bus.test.js.map +1 -0
  58. package/dist/core/index.d.ts +9 -0
  59. package/dist/core/index.d.ts.map +1 -0
  60. package/dist/core/index.js +7 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/notifier.d.ts +2 -0
  63. package/dist/core/notifier.d.ts.map +1 -0
  64. package/dist/core/notifier.js +28 -0
  65. package/dist/core/notifier.js.map +1 -0
  66. package/dist/core/notifier.test.d.ts +2 -0
  67. package/dist/core/notifier.test.d.ts.map +1 -0
  68. package/dist/core/notifier.test.js +25 -0
  69. package/dist/core/notifier.test.js.map +1 -0
  70. package/dist/core/runtime.d.ts +25 -0
  71. package/dist/core/runtime.d.ts.map +1 -0
  72. package/dist/core/runtime.js +85 -0
  73. package/dist/core/runtime.js.map +1 -0
  74. package/dist/core/task-runner.d.ts +26 -0
  75. package/dist/core/task-runner.d.ts.map +1 -0
  76. package/dist/core/task-runner.js +354 -0
  77. package/dist/core/task-runner.js.map +1 -0
  78. package/dist/core/timer-plugin.d.ts +3 -0
  79. package/dist/core/timer-plugin.d.ts.map +1 -0
  80. package/dist/core/timer-plugin.js +34 -0
  81. package/dist/core/timer-plugin.js.map +1 -0
  82. package/dist/core/workspaces.d.ts +6 -0
  83. package/dist/core/workspaces.d.ts.map +1 -0
  84. package/dist/core/workspaces.js +60 -0
  85. package/dist/core/workspaces.js.map +1 -0
  86. package/dist/core/workspaces.test.d.ts +2 -0
  87. package/dist/core/workspaces.test.d.ts.map +1 -0
  88. package/dist/core/workspaces.test.js +62 -0
  89. package/dist/core/workspaces.test.js.map +1 -0
  90. package/dist/index.d.ts +16 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/plugins/index.d.ts +3 -0
  95. package/dist/plugins/index.d.ts.map +1 -0
  96. package/dist/plugins/index.js +2 -0
  97. package/dist/plugins/index.js.map +1 -0
  98. package/dist/plugins/manager.d.ts +13 -0
  99. package/dist/plugins/manager.d.ts.map +1 -0
  100. package/dist/plugins/manager.js +43 -0
  101. package/dist/plugins/manager.js.map +1 -0
  102. package/dist/plugins/manager.test.d.ts +2 -0
  103. package/dist/plugins/manager.test.d.ts.map +1 -0
  104. package/dist/plugins/manager.test.js +79 -0
  105. package/dist/plugins/manager.test.js.map +1 -0
  106. package/dist/plugins/types.d.ts +17 -0
  107. package/dist/plugins/types.d.ts.map +1 -0
  108. package/dist/plugins/types.js +2 -0
  109. package/dist/plugins/types.js.map +1 -0
  110. package/dist/status/index.d.ts +3 -0
  111. package/dist/status/index.d.ts.map +1 -0
  112. package/dist/status/index.js +2 -0
  113. package/dist/status/index.js.map +1 -0
  114. package/dist/status/store.d.ts +18 -0
  115. package/dist/status/store.d.ts.map +1 -0
  116. package/dist/status/store.js +76 -0
  117. package/dist/status/store.js.map +1 -0
  118. package/dist/status/store.test.d.ts +2 -0
  119. package/dist/status/store.test.d.ts.map +1 -0
  120. package/dist/status/store.test.js +107 -0
  121. package/dist/status/store.test.js.map +1 -0
  122. package/dist/status/types.d.ts +12 -0
  123. package/dist/status/types.d.ts.map +1 -0
  124. package/dist/status/types.js +2 -0
  125. package/dist/status/types.js.map +1 -0
  126. package/dist/ui/app.d.ts +10 -0
  127. package/dist/ui/app.d.ts.map +1 -0
  128. package/dist/ui/app.js +479 -0
  129. package/dist/ui/app.js.map +1 -0
  130. package/dist/ui/command-list.d.ts +30 -0
  131. package/dist/ui/command-list.d.ts.map +1 -0
  132. package/dist/ui/command-list.js +45 -0
  133. package/dist/ui/command-list.js.map +1 -0
  134. package/dist/ui/index.d.ts +4 -0
  135. package/dist/ui/index.d.ts.map +1 -0
  136. package/dist/ui/index.js +8 -0
  137. package/dist/ui/index.js.map +1 -0
  138. package/dist/ui/metrics-panel.d.ts +10 -0
  139. package/dist/ui/metrics-panel.d.ts.map +1 -0
  140. package/dist/ui/metrics-panel.js +139 -0
  141. package/dist/ui/metrics-panel.js.map +1 -0
  142. package/dist/ui/panel.d.ts +16 -0
  143. package/dist/ui/panel.d.ts.map +1 -0
  144. package/dist/ui/panel.js +16 -0
  145. package/dist/ui/panel.js.map +1 -0
  146. package/dist/ui/status-panel.d.ts +16 -0
  147. package/dist/ui/status-panel.d.ts.map +1 -0
  148. package/dist/ui/status-panel.js +52 -0
  149. package/dist/ui/status-panel.js.map +1 -0
  150. package/docs/architecture.md +29 -0
  151. package/docs/config.md +15 -0
  152. package/docs/mvp.md +17 -0
  153. package/docs/phases.md +49 -0
  154. package/docs/technical-decisions.md +19 -0
  155. package/docs/ui.md +14 -0
  156. package/package.json +30 -0
  157. package/src/cli.ts +242 -0
  158. package/src/config/index.ts +2 -0
  159. package/src/config/loader.test.ts +30 -0
  160. package/src/config/loader.ts +123 -0
  161. package/src/config/presets/node.ts +30 -0
  162. package/src/config/presets/react.ts +35 -0
  163. package/src/config/types.test.ts +24 -0
  164. package/src/config/types.ts +52 -0
  165. package/src/core/ci.test.ts +54 -0
  166. package/src/core/ci.ts +26 -0
  167. package/src/core/event-bus.test.ts +56 -0
  168. package/src/core/event-bus.ts +34 -0
  169. package/src/core/index.ts +8 -0
  170. package/src/core/notifier.test.ts +30 -0
  171. package/src/core/notifier.ts +34 -0
  172. package/src/core/runtime.ts +99 -0
  173. package/src/core/task-runner.ts +408 -0
  174. package/src/core/timer-plugin.ts +34 -0
  175. package/src/core/workspaces.test.ts +72 -0
  176. package/src/core/workspaces.ts +73 -0
  177. package/src/index.ts +15 -0
  178. package/src/plugins/index.ts +2 -0
  179. package/src/plugins/manager.test.ts +92 -0
  180. package/src/plugins/manager.ts +54 -0
  181. package/src/plugins/types.ts +18 -0
  182. package/src/status/index.ts +2 -0
  183. package/src/status/store.test.ts +122 -0
  184. package/src/status/store.ts +88 -0
  185. package/src/status/types.ts +12 -0
  186. package/src/ui/app.tsx +606 -0
  187. package/src/ui/command-list.tsx +163 -0
  188. package/src/ui/index.tsx +10 -0
  189. package/src/ui/metrics-panel.tsx +234 -0
  190. package/src/ui/panel.tsx +76 -0
  191. package/src/ui/status-panel.tsx +160 -0
  192. package/tsconfig.json +21 -0
  193. package/vitest.config.ts +8 -0
package/src/cli.ts ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { loadConfig } from './config/index.js';
6
+ import { Runtime, detectCI } from './core/index.js';
7
+ import { startUI } from './ui/index.js';
8
+
9
+ const VERSION = '0.1.0';
10
+
11
+ function printHelp(): void {
12
+ console.log(`
13
+ developer-control-center v${VERSION}
14
+
15
+ Usage: dcc [command] [options]
16
+
17
+ Commands:
18
+ init Scaffold a developer-control-center.config.js file
19
+ completion <shell> Generate shell completion script (bash|zsh|fish)
20
+
21
+ Options:
22
+ --profile <name> Use a specific environment profile
23
+ --help, -h Show this help message
24
+ --version, -v Show version number
25
+
26
+ Environment:
27
+ DCC_PROFILE Default profile name (overridden by --profile)
28
+
29
+ Examples:
30
+ dcc Launch the TUI
31
+ dcc init Create developer-control-center.config.js in current directory
32
+ dcc completion bash Generate bash completions
33
+ `);
34
+ }
35
+
36
+ function printInitHelp(): void {
37
+ const config = `export default {
38
+ name: '${path.basename(process.cwd())}',
39
+ commands: [
40
+ {
41
+ id: 'dev',
42
+ label: 'Dev server',
43
+ command: 'npm run dev',
44
+ watch: true,
45
+ group: 'Development',
46
+ },
47
+ {
48
+ id: 'test',
49
+ label: 'Test',
50
+ command: 'npm test',
51
+ group: 'Development',
52
+ },
53
+ {
54
+ id: 'lint',
55
+ label: 'Lint',
56
+ command: 'npm run lint',
57
+ group: 'Development',
58
+ },
59
+ {
60
+ id: 'build',
61
+ label: 'Build',
62
+ command: 'npm run build',
63
+ group: 'Build',
64
+ },
65
+ {
66
+ id: 'typecheck',
67
+ label: 'Type check',
68
+ command: 'npm run typecheck',
69
+ group: 'Build',
70
+ },
71
+ {
72
+ id: 'deploy-staging',
73
+ label: 'Deploy to staging',
74
+ command: 'npm run deploy:staging',
75
+ confirm: true,
76
+ group: 'Deploy',
77
+ },
78
+ {
79
+ id: 'deploy-production',
80
+ label: 'Deploy to production',
81
+ command: 'npm run deploy:production',
82
+ confirm: true,
83
+ group: 'Deploy',
84
+ },
85
+ ],
86
+ };
87
+ `;
88
+ fs.writeFileSync('developer-control-center.config.js', config, 'utf-8');
89
+ console.log('Created developer-control-center.config.js');
90
+ }
91
+
92
+ function printCompletion(shell: string): void {
93
+ const bin = 'dcc';
94
+
95
+ const bash = `_${bin}() {
96
+ local cur words
97
+ cur="\${COMP_WORDS[COMP_CWORD]}"
98
+ words="init completion bash zsh fish --profile --help --version"
99
+
100
+ if [[ $COMP_CWORD -eq 1 ]]; then
101
+ mapfile -t COMPREPLY < <(compgen -W "$words" -- "$cur")
102
+ elif [[ $COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "completion" ]]; then
103
+ mapfile -t COMPREPLY < <(compgen -W "bash zsh fish" -- "$cur")
104
+ fi
105
+ }
106
+ complete -F _${bin} ${bin}
107
+ `;
108
+
109
+ const zsh = `#compdef ${bin}
110
+ _${bin}() {
111
+ local -a subcommands
112
+ subcommands=(
113
+ 'init:Scaffold a config file'
114
+ 'completion:Generate shell completion script'
115
+ )
116
+
117
+ _arguments \\
118
+ '--profile[Use a specific profile]:profile' \\
119
+ '--help[Show help]' \\
120
+ '--version[Show version]' \\
121
+ '1: :->command' \\
122
+ '*: :->args'
123
+
124
+ case $state in
125
+ command)
126
+ _describe 'command' subcommands
127
+ ;;
128
+ args)
129
+ case $words[1] in
130
+ completion)
131
+ _arguments '2:shell:(bash zsh fish)'
132
+ ;;
133
+ esac
134
+ ;;
135
+ esac
136
+ }
137
+ compdef _${bin} ${bin}
138
+ `;
139
+
140
+ const fish = `function _${bin}
141
+ set -l commands init completion
142
+
143
+ complete -c ${bin} -f
144
+
145
+ complete -c ${bin} -n "not __fish_seen_subcommand_from $commands" \\
146
+ -a init -d "Scaffold a config file"
147
+ complete -c ${bin} -n "not __fish_seen_subcommand_from $commands" \\
148
+ -a completion -d "Generate shell completion script"
149
+ complete -c ${bin} -n "not __fish_seen_subcommand_from $commands" \\
150
+ -l profile -d "Use a specific profile" -r
151
+ complete -c ${bin} -n "not __fish_seen_subcommand_from $commands" \\
152
+ -l help -s h -d "Show help"
153
+ complete -c ${bin} -n "not __fish_seen_subcommand_from $commands" \\
154
+ -l version -s v -d "Show version"
155
+
156
+ complete -c ${bin} -n "__fish_seen_subcommand_from completion" \\
157
+ -a bash -d "Bash completions"
158
+ complete -c ${bin} -n "__fish_seen_subcommand_from completion" \\
159
+ -a zsh -d "Zsh completions"
160
+ complete -c ${bin} -n "__fish_seen_subcommand_from completion" \\
161
+ -a fish -d "Fish completions"
162
+ end
163
+ `;
164
+
165
+ switch (shell) {
166
+ case 'bash':
167
+ console.log(bash.trimEnd());
168
+ break;
169
+ case 'zsh':
170
+ console.log(zsh.trimEnd());
171
+ break;
172
+ case 'fish':
173
+ console.log(fish.trimEnd());
174
+ break;
175
+ default:
176
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
177
+ process.exit(1);
178
+ }
179
+ }
180
+
181
+ async function main() {
182
+ const args = process.argv.slice(2);
183
+ let configPath: string | undefined;
184
+ let profile: string | undefined;
185
+
186
+ if (args.includes('--help') || args.includes('-h')) {
187
+ printHelp();
188
+ process.exit(0);
189
+ }
190
+
191
+ if (args.includes('--version') || args.includes('-v')) {
192
+ console.log(VERSION);
193
+ process.exit(0);
194
+ }
195
+
196
+ if (args[0] === 'init') {
197
+ printInitHelp();
198
+ process.exit(0);
199
+ }
200
+
201
+ if (args[0] === 'completion') {
202
+ printCompletion(args[1] || 'bash');
203
+ process.exit(0);
204
+ }
205
+
206
+ for (let i = 0; i < args.length; i++) {
207
+ if (args[i] === '--profile' && i + 1 < args.length) {
208
+ profile = args[i + 1];
209
+ i++;
210
+ } else if (!args[i].startsWith('-')) {
211
+ configPath = args[i];
212
+ }
213
+ }
214
+
215
+ profile = profile || process.env.DCC_PROFILE;
216
+
217
+ const ci = detectCI();
218
+ if (!profile && ci.isCI) {
219
+ profile = 'ci';
220
+ }
221
+
222
+ const config = await loadConfig(configPath, profile);
223
+ const runtime = new Runtime(config);
224
+ await runtime.start();
225
+
226
+ if (ci.isCI) {
227
+ console.error(`[dcc] detected ${ci.name}, profile: ${config.profile || 'none'}`);
228
+ }
229
+
230
+ if (!process.stdin.isTTY) {
231
+ console.error('dcc requires an interactive terminal');
232
+ process.exit(1);
233
+ }
234
+ console.clear();
235
+ await startUI(config, runtime);
236
+ process.exit(0);
237
+ }
238
+
239
+ main().catch((err) => {
240
+ console.error(err);
241
+ process.exit(1);
242
+ });
@@ -0,0 +1,2 @@
1
+ export { loadConfig, mergeCommands } from './loader.js';
2
+ export type { ProkomCommand, ProkomConfig, ProkomPreset, ProkomProfile, ProkomPipeline, ProkomToggle } from './types.js';
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { loadConfig } from './loader.js';
5
+
6
+ describe('loadConfig', () => {
7
+ const configPath = path.resolve(
8
+ fileURLToPath(new URL('../../developer-control-center.config.js', import.meta.url)),
9
+ );
10
+
11
+ it('returns default config when no config file exists', async () => {
12
+ const cwd = process.cwd;
13
+ process.cwd = () => '/tmp/nonexistent' as any;
14
+ const config = await loadConfig();
15
+ expect(config.name).toBe('nonexistent');
16
+ expect(config.commands).toEqual([]);
17
+ process.cwd = cwd;
18
+ });
19
+
20
+ it('loads config from a custom path', async () => {
21
+ const config = await loadConfig(configPath);
22
+ expect(config.name).toBe('developer-control-center');
23
+ expect(config.commands.length).toBeGreaterThan(0);
24
+ });
25
+
26
+ it('applies profile when specified', async () => {
27
+ const config = await loadConfig(configPath, 'ci');
28
+ expect(config.profile).toBe('ci');
29
+ });
30
+ });
@@ -0,0 +1,123 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import { ProkomConfig, ProkomCommand, ProkomPreset } from './types.js';
5
+
6
+ const CONFIG_CANDIDATES = [
7
+ 'developer-control-center.config.mjs',
8
+ 'developer-control-center.config.cjs',
9
+ 'developer-control-center.config.js',
10
+ ];
11
+
12
+ function findConfigFile(cwd: string): string | null {
13
+ for (const file of CONFIG_CANDIDATES) {
14
+ const fullPath = path.join(cwd, file);
15
+ if (fs.existsSync(fullPath)) return fullPath;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ async function resolvePreset(
21
+ name: string,
22
+ cwd: string,
23
+ ): Promise<ProkomPreset> {
24
+ if (name.startsWith('.') || name.startsWith('/')) {
25
+ const resolved = pathToFileURL(path.resolve(cwd, name)).href;
26
+ const mod = await import(resolved);
27
+ return mod.default || mod;
28
+ }
29
+
30
+ try {
31
+ const mod = await import(`./presets/${name}.js`);
32
+ return mod.default || mod;
33
+ } catch {
34
+ const mod = await import(name);
35
+ return mod.default || mod;
36
+ }
37
+ }
38
+
39
+ export function mergeCommands(
40
+ base: ProkomCommand[],
41
+ overrides: ProkomCommand[],
42
+ ): ProkomCommand[] {
43
+ const overrideMap = new Map<string, ProkomCommand>();
44
+ for (const cmd of overrides) {
45
+ overrideMap.set(cmd.id, cmd);
46
+ }
47
+
48
+ const seen = new Set<string>();
49
+ const merged: ProkomCommand[] = [];
50
+
51
+ for (const cmd of base) {
52
+ seen.add(cmd.id);
53
+ merged.push(overrideMap.get(cmd.id) ?? cmd);
54
+ }
55
+
56
+ for (const cmd of overrides) {
57
+ if (!seen.has(cmd.id)) {
58
+ seen.add(cmd.id);
59
+ merged.push(cmd);
60
+ }
61
+ }
62
+
63
+ return merged;
64
+ }
65
+
66
+ export async function loadConfig(
67
+ configPath?: string,
68
+ profile?: string,
69
+ ): Promise<ProkomConfig> {
70
+ const cwd = process.cwd();
71
+ const resolved = configPath || findConfigFile(cwd);
72
+
73
+ if (!resolved) {
74
+ return { name: path.basename(cwd), commands: [] };
75
+ }
76
+
77
+ const configFileDir = path.dirname(resolved);
78
+ let mod;
79
+ try {
80
+ mod = await import(pathToFileURL(resolved).href);
81
+ } catch (err) {
82
+ console.error(`dcc: failed to load config file ${resolved}`);
83
+ console.error(err instanceof Error ? err.message : String(err));
84
+ return { name: path.basename(cwd), commands: [] };
85
+ }
86
+ const config: ProkomConfig = mod.default || mod;
87
+
88
+ if (config.presets && config.presets.length > 0) {
89
+ const presetList: ProkomPreset[] = [];
90
+ for (const presetName of config.presets) {
91
+ const preset = await resolvePreset(presetName, configFileDir);
92
+ presetList.push(preset);
93
+ }
94
+
95
+ const allPresetCommands = presetList.flatMap((p) => p.commands);
96
+ config.commands = mergeCommands(allPresetCommands, config.commands);
97
+ }
98
+
99
+ config.baseCommands = [...config.commands];
100
+
101
+ if (profile && config.profiles?.[profile]) {
102
+ config.commands = mergeCommands(
103
+ config.commands,
104
+ config.profiles[profile].commands,
105
+ );
106
+ config.profile = profile;
107
+ }
108
+
109
+ if (config.pipelines) {
110
+ for (const pipeline of config.pipelines) {
111
+ config.commands.push({
112
+ id: pipeline.id,
113
+ label: `▶ ${pipeline.label}`,
114
+ description: `Run pipeline: ${pipeline.steps.join(' → ')}`,
115
+ command: '',
116
+ confirm: pipeline.confirm,
117
+ pipelineSteps: pipeline.steps,
118
+ });
119
+ }
120
+ }
121
+
122
+ return config;
123
+ }
@@ -0,0 +1,30 @@
1
+ export default {
2
+ name: 'node',
3
+ commands: [
4
+ {
5
+ id: 'node-test',
6
+ label: 'Test',
7
+ command: 'npm test',
8
+ },
9
+ {
10
+ id: 'node-build',
11
+ label: 'Build',
12
+ command: 'npm run build',
13
+ },
14
+ {
15
+ id: 'node-lint',
16
+ label: 'Lint',
17
+ command: 'npm run lint',
18
+ },
19
+ {
20
+ id: 'node-typecheck',
21
+ label: 'TypeCheck',
22
+ command: 'npx tsc --noEmit',
23
+ },
24
+ {
25
+ id: 'node-clean',
26
+ label: 'Clean',
27
+ command: 'rm -rf dist',
28
+ },
29
+ ],
30
+ };
@@ -0,0 +1,35 @@
1
+ export default {
2
+ name: 'react',
3
+ commands: [
4
+ {
5
+ id: 'react-dev',
6
+ label: 'Dev server',
7
+ command: 'npm run dev',
8
+ },
9
+ {
10
+ id: 'react-build',
11
+ label: 'Build',
12
+ command: 'npm run build',
13
+ },
14
+ {
15
+ id: 'react-test',
16
+ label: 'Test',
17
+ command: 'npm test',
18
+ },
19
+ {
20
+ id: 'react-lint',
21
+ label: 'Lint',
22
+ command: 'npm run lint',
23
+ },
24
+ {
25
+ id: 'react-typecheck',
26
+ label: 'TypeCheck',
27
+ command: 'npx tsc --noEmit',
28
+ },
29
+ {
30
+ id: 'react-preview',
31
+ label: 'Preview build',
32
+ command: 'npm run preview',
33
+ },
34
+ ],
35
+ };
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('type exports', () => {
4
+ it('can build a minimal ProkomConfig', async () => {
5
+ const cfg = { name: 'test', commands: [] };
6
+ expect(cfg.name).toBe('test');
7
+ expect(cfg.commands).toEqual([]);
8
+ });
9
+
10
+ it('can build a ProkomCommand with all optional fields', async () => {
11
+ const cmd = {
12
+ id: 'test',
13
+ label: 'Test',
14
+ command: 'echo hi',
15
+ confirm: true,
16
+ watch: false,
17
+ cwd: 'packages/core',
18
+ parallel: true,
19
+ };
20
+ expect(cmd.id).toBe('test');
21
+ expect(cmd.cwd).toBe('packages/core');
22
+ expect(cmd.parallel).toBe(true);
23
+ });
24
+ });
@@ -0,0 +1,52 @@
1
+ export interface ProkomToggle {
2
+ start: string;
3
+ stop?: string;
4
+ }
5
+
6
+ export interface ProkomCommand {
7
+ id: string;
8
+ label: string;
9
+ description?: string;
10
+ command?: string;
11
+ toggle?: ProkomToggle;
12
+ confirm?: boolean;
13
+ input?: { message: string; placeholder?: string; default?: string };
14
+ onNonZeroExit?: { label: string; command: string };
15
+ timeout?: number;
16
+ watch?: boolean;
17
+ cwd?: string;
18
+ group?: string;
19
+ parallel?: boolean;
20
+ pipelineSteps?: string[];
21
+ parallelSteps?: string[];
22
+ }
23
+
24
+ export interface ProkomPreset {
25
+ name: string;
26
+ commands: ProkomCommand[];
27
+ }
28
+
29
+ export interface ProkomProfile {
30
+ commands: ProkomCommand[];
31
+ }
32
+
33
+ export interface ProkomPipeline {
34
+ id: string;
35
+ label: string;
36
+ steps: string[];
37
+ confirm?: boolean;
38
+ }
39
+
40
+ export interface ProkomConfig {
41
+ name: string;
42
+ commands: ProkomCommand[];
43
+ presets?: string[];
44
+ plugins?: string[];
45
+ profiles?: Record<string, ProkomProfile>;
46
+ profile?: string;
47
+ baseCommands?: ProkomCommand[];
48
+ notifications?: boolean;
49
+ pipelines?: ProkomPipeline[];
50
+ menuRows?: number;
51
+ outputRows?: number;
52
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { detectCI } from './ci.js';
3
+
4
+ const ORIG_ENV = { ...process.env };
5
+
6
+ beforeEach(() => {
7
+ // Clear CI-related env vars
8
+ for (const key of Object.keys(process.env)) {
9
+ if (['GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'JENKINS_URL', 'CI'].includes(key)) {
10
+ delete process.env[key];
11
+ }
12
+ }
13
+ });
14
+
15
+ afterEach(() => {
16
+ Object.assign(process.env, ORIG_ENV);
17
+ });
18
+
19
+ describe('detectCI', () => {
20
+ it('returns not CI when no CI env vars', () => {
21
+ expect(detectCI()).toEqual({ isCI: false });
22
+ });
23
+
24
+ it('detects GitHub Actions', () => {
25
+ process.env.GITHUB_ACTIONS = 'true';
26
+ expect(detectCI()).toEqual({ isCI: true, name: 'GitHub Actions' });
27
+ });
28
+
29
+ it('detects GitLab CI', () => {
30
+ process.env.GITLAB_CI = 'true';
31
+ expect(detectCI()).toEqual({ isCI: true, name: 'GitLab CI' });
32
+ });
33
+
34
+ it('detects CircleCI', () => {
35
+ process.env.CIRCLECI = 'true';
36
+ expect(detectCI()).toEqual({ isCI: true, name: 'CircleCI' });
37
+ });
38
+
39
+ it('detects Jenkins', () => {
40
+ process.env.JENKINS_URL = 'http://jenkins.example.com';
41
+ expect(detectCI()).toEqual({ isCI: true, name: 'Jenkins' });
42
+ });
43
+
44
+ it('detects generic CI', () => {
45
+ process.env.CI = 'true';
46
+ expect(detectCI()).toEqual({ isCI: true, name: 'CI' });
47
+ });
48
+
49
+ it('prefers specific CI over generic', () => {
50
+ process.env.CI = 'true';
51
+ process.env.GITHUB_ACTIONS = 'true';
52
+ expect(detectCI()).toEqual({ isCI: true, name: 'GitHub Actions' });
53
+ });
54
+ });
package/src/core/ci.ts ADDED
@@ -0,0 +1,26 @@
1
+ export interface CIInfo {
2
+ isCI: boolean;
3
+ name?: string;
4
+ }
5
+
6
+ export function detectCI(): CIInfo {
7
+ if (process.env.GITHUB_ACTIONS) {
8
+ return { isCI: true, name: 'GitHub Actions' };
9
+ }
10
+ if (process.env.GITLAB_CI) {
11
+ return { isCI: true, name: 'GitLab CI' };
12
+ }
13
+ if (process.env.CIRCLECI) {
14
+ return { isCI: true, name: 'CircleCI' };
15
+ }
16
+ if (process.env.JENKINS_URL) {
17
+ return { isCI: true, name: 'Jenkins' };
18
+ }
19
+ if (process.env.TF_BUILD) {
20
+ return { isCI: true, name: 'Azure Pipelines' };
21
+ }
22
+ if (process.env.CI) {
23
+ return { isCI: true, name: 'CI' };
24
+ }
25
+ return { isCI: false };
26
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { EventBus } from './event-bus.js';
3
+
4
+ describe('EventBus', () => {
5
+ let bus: EventBus;
6
+
7
+ beforeEach(() => {
8
+ bus = new EventBus();
9
+ });
10
+
11
+ it('emits to registered listeners', () => {
12
+ const calls: string[] = [];
13
+ bus.on('test', (msg: string) => calls.push(msg));
14
+ bus.emit('test', 'hello');
15
+ expect(calls).toEqual(['hello']);
16
+ });
17
+
18
+ it('emits to multiple listeners', () => {
19
+ const results: number[] = [];
20
+ bus.on('count', (n: number) => results.push(n * 2));
21
+ bus.on('count', (n: number) => results.push(n * 3));
22
+ bus.emit('count', 5);
23
+ expect(results).toEqual([10, 15]);
24
+ });
25
+
26
+ it('supports off/unsubscribe', () => {
27
+ const calls: string[] = [];
28
+ const fn = (x: string) => calls.push(x);
29
+ bus.on('e', fn);
30
+ bus.emit('e', 'a');
31
+ bus.off('e', fn);
32
+ bus.emit('e', 'b');
33
+ expect(calls).toEqual(['a']);
34
+ });
35
+
36
+ it('removeAll clears all listeners', () => {
37
+ const calls: string[] = [];
38
+ bus.on('x', () => calls.push('x'));
39
+ bus.on('y', () => calls.push('y'));
40
+ bus.removeAll();
41
+ bus.emit('x');
42
+ bus.emit('y');
43
+ expect(calls).toEqual([]);
44
+ });
45
+
46
+ it('does nothing for unregistered events', () => {
47
+ expect(() => bus.emit('nothing')).not.toThrow();
48
+ });
49
+
50
+ it('passes multiple arguments', () => {
51
+ const args: any[] = [];
52
+ bus.on('multi', (...a: any[]) => args.push(a));
53
+ bus.emit('multi', 1, 'two', true);
54
+ expect(args).toEqual([[1, 'two', true]]);
55
+ });
56
+ });