@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.
- package/.developer-control-center/metrics.json +1 -0
- package/.developer-control-center/status.json +1 -0
- package/.developer-control-center/timings.jsonl +3 -0
- package/.github/workflows/ci.yml +47 -0
- package/AGENTS.md +51 -0
- package/PLUGINS.md +145 -0
- package/README.md +147 -0
- package/developer-control-center.config.example.js +91 -0
- package/developer-control-center.config.js +177 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +96 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/loader.test.d.ts +2 -0
- package/dist/config/loader.test.d.ts.map +1 -0
- package/dist/config/loader.test.js +25 -0
- package/dist/config/loader.test.js.map +1 -0
- package/dist/config/presets/node.d.ts +10 -0
- package/dist/config/presets/node.d.ts.map +1 -0
- package/dist/config/presets/node.js +31 -0
- package/dist/config/presets/node.js.map +1 -0
- package/dist/config/presets/react.d.ts +10 -0
- package/dist/config/presets/react.d.ts.map +1 -0
- package/dist/config/presets/react.js +36 -0
- package/dist/config/presets/react.js.map +1 -0
- package/dist/config/types.d.ts +55 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/types.test.d.ts +2 -0
- package/dist/config/types.test.d.ts.map +1 -0
- package/dist/config/types.test.js +23 -0
- package/dist/config/types.test.js.map +1 -0
- package/dist/core/ci.d.ts +6 -0
- package/dist/core/ci.d.ts.map +1 -0
- package/dist/core/ci.js +22 -0
- package/dist/core/ci.js.map +1 -0
- package/dist/core/ci.test.d.ts +2 -0
- package/dist/core/ci.test.d.ts.map +1 -0
- package/dist/core/ci.test.js +45 -0
- package/dist/core/ci.test.js.map +1 -0
- package/dist/core/event-bus.d.ts +18 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +19 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/event-bus.test.d.ts +2 -0
- package/dist/core/event-bus.test.d.ts.map +1 -0
- package/dist/core/event-bus.test.js +49 -0
- package/dist/core/event-bus.test.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/notifier.d.ts +2 -0
- package/dist/core/notifier.d.ts.map +1 -0
- package/dist/core/notifier.js +28 -0
- package/dist/core/notifier.js.map +1 -0
- package/dist/core/notifier.test.d.ts +2 -0
- package/dist/core/notifier.test.d.ts.map +1 -0
- package/dist/core/notifier.test.js +25 -0
- package/dist/core/notifier.test.js.map +1 -0
- package/dist/core/runtime.d.ts +25 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +85 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/task-runner.d.ts +26 -0
- package/dist/core/task-runner.d.ts.map +1 -0
- package/dist/core/task-runner.js +354 -0
- package/dist/core/task-runner.js.map +1 -0
- package/dist/core/timer-plugin.d.ts +3 -0
- package/dist/core/timer-plugin.d.ts.map +1 -0
- package/dist/core/timer-plugin.js +34 -0
- package/dist/core/timer-plugin.js.map +1 -0
- package/dist/core/workspaces.d.ts +6 -0
- package/dist/core/workspaces.d.ts.map +1 -0
- package/dist/core/workspaces.js +60 -0
- package/dist/core/workspaces.js.map +1 -0
- package/dist/core/workspaces.test.d.ts +2 -0
- package/dist/core/workspaces.test.d.ts.map +1 -0
- package/dist/core/workspaces.test.js +62 -0
- package/dist/core/workspaces.test.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/manager.d.ts +13 -0
- package/dist/plugins/manager.d.ts.map +1 -0
- package/dist/plugins/manager.js +43 -0
- package/dist/plugins/manager.js.map +1 -0
- package/dist/plugins/manager.test.d.ts +2 -0
- package/dist/plugins/manager.test.d.ts.map +1 -0
- package/dist/plugins/manager.test.js +79 -0
- package/dist/plugins/manager.test.js.map +1 -0
- package/dist/plugins/types.d.ts +17 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/status/index.d.ts +3 -0
- package/dist/status/index.d.ts.map +1 -0
- package/dist/status/index.js +2 -0
- package/dist/status/index.js.map +1 -0
- package/dist/status/store.d.ts +18 -0
- package/dist/status/store.d.ts.map +1 -0
- package/dist/status/store.js +76 -0
- package/dist/status/store.js.map +1 -0
- package/dist/status/store.test.d.ts +2 -0
- package/dist/status/store.test.d.ts.map +1 -0
- package/dist/status/store.test.js +107 -0
- package/dist/status/store.test.js.map +1 -0
- package/dist/status/types.d.ts +12 -0
- package/dist/status/types.d.ts.map +1 -0
- package/dist/status/types.js +2 -0
- package/dist/status/types.js.map +1 -0
- package/dist/ui/app.d.ts +10 -0
- package/dist/ui/app.d.ts.map +1 -0
- package/dist/ui/app.js +479 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/command-list.d.ts +30 -0
- package/dist/ui/command-list.d.ts.map +1 -0
- package/dist/ui/command-list.js +45 -0
- package/dist/ui/command-list.js.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/metrics-panel.d.ts +10 -0
- package/dist/ui/metrics-panel.d.ts.map +1 -0
- package/dist/ui/metrics-panel.js +139 -0
- package/dist/ui/metrics-panel.js.map +1 -0
- package/dist/ui/panel.d.ts +16 -0
- package/dist/ui/panel.d.ts.map +1 -0
- package/dist/ui/panel.js +16 -0
- package/dist/ui/panel.js.map +1 -0
- package/dist/ui/status-panel.d.ts +16 -0
- package/dist/ui/status-panel.d.ts.map +1 -0
- package/dist/ui/status-panel.js +52 -0
- package/dist/ui/status-panel.js.map +1 -0
- package/docs/architecture.md +29 -0
- package/docs/config.md +15 -0
- package/docs/mvp.md +17 -0
- package/docs/phases.md +49 -0
- package/docs/technical-decisions.md +19 -0
- package/docs/ui.md +14 -0
- package/package.json +30 -0
- package/src/cli.ts +242 -0
- package/src/config/index.ts +2 -0
- package/src/config/loader.test.ts +30 -0
- package/src/config/loader.ts +123 -0
- package/src/config/presets/node.ts +30 -0
- package/src/config/presets/react.ts +35 -0
- package/src/config/types.test.ts +24 -0
- package/src/config/types.ts +52 -0
- package/src/core/ci.test.ts +54 -0
- package/src/core/ci.ts +26 -0
- package/src/core/event-bus.test.ts +56 -0
- package/src/core/event-bus.ts +34 -0
- package/src/core/index.ts +8 -0
- package/src/core/notifier.test.ts +30 -0
- package/src/core/notifier.ts +34 -0
- package/src/core/runtime.ts +99 -0
- package/src/core/task-runner.ts +408 -0
- package/src/core/timer-plugin.ts +34 -0
- package/src/core/workspaces.test.ts +72 -0
- package/src/core/workspaces.ts +73 -0
- package/src/index.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/manager.test.ts +92 -0
- package/src/plugins/manager.ts +54 -0
- package/src/plugins/types.ts +18 -0
- package/src/status/index.ts +2 -0
- package/src/status/store.test.ts +122 -0
- package/src/status/store.ts +88 -0
- package/src/status/types.ts +12 -0
- package/src/ui/app.tsx +606 -0
- package/src/ui/command-list.tsx +163 -0
- package/src/ui/index.tsx +10 -0
- package/src/ui/metrics-panel.tsx +234 -0
- package/src/ui/panel.tsx +76 -0
- package/src/ui/status-panel.tsx +160 -0
- package/tsconfig.json +21 -0
- 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,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
|
+
});
|