go-dev 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/bin/go-dev +2 -16
- package/package.json +3 -2
- package/spike/README.md +77 -0
- package/spike/opentui/index.js +181 -0
- package/spike/opentui/package.json +14 -0
- package/spike/shared/fake-log-source.js +89 -0
- package/spike/terminal-kit/index.js +206 -0
- package/spike/terminal-kit/package.json +13 -0
- package/src/cli-args.js +9 -2
- package/src/config.js +1 -1
- package/src/dependency-resolver.js +72 -52
- package/src/index.js +3 -7
- package/src/interactive.js +342 -0
- package/src/last-selection.js +75 -0
- package/src/orchestrator.js +21 -5
- package/src/run.js +62 -0
- package/src/save-preset.js +103 -0
package/src/orchestrator.js
CHANGED
|
@@ -16,6 +16,7 @@ const bold = (text) => `\x1b[1m${text}\x1b[0m`;
|
|
|
16
16
|
|
|
17
17
|
class Orchestrator {
|
|
18
18
|
constructor(configPath, options = {}) {
|
|
19
|
+
this.configPath = configPath;
|
|
19
20
|
this.config = loadConfig(configPath);
|
|
20
21
|
|
|
21
22
|
const level = options.logLevel ?? this.config.logLevel;
|
|
@@ -29,19 +30,34 @@ class Orchestrator {
|
|
|
29
30
|
BaseService.initialize(this.processManager, this.config.services);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ name?: string, services: string[], modes?: Record<string, string> }} selection
|
|
35
|
+
* The service selection to run — from a preset or built interactively.
|
|
36
|
+
*/
|
|
37
|
+
async start(selection) {
|
|
33
38
|
try {
|
|
34
|
-
const { dependencies, services: primaryServices } = resolveServiceExecutionGraph(
|
|
39
|
+
const { dependencies, services: primaryServices, conflicts } = resolveServiceExecutionGraph(
|
|
35
40
|
this.config,
|
|
36
|
-
|
|
41
|
+
selection,
|
|
37
42
|
);
|
|
38
43
|
|
|
39
|
-
log.info(bold(`Preset: ${
|
|
44
|
+
log.info(bold(`Preset: ${selection.name ?? 'selezione personalizzata'}`));
|
|
40
45
|
log.info(`\n${bold('Resolved dependencies')}`);
|
|
41
46
|
dependencies.forEach(s => log.info(` - ${colorService(s.name, s.mode)} (mode: ${colorMode(s.name, s.mode)})`));
|
|
42
47
|
log.info(`\n${bold('Resolved primary services')}`);
|
|
43
48
|
primaryServices.forEach(s => log.info(` - ${colorService(s.name, s.mode)} (mode: ${colorMode(s.name, s.mode)})`));
|
|
44
49
|
|
|
50
|
+
if (conflicts.length > 0) {
|
|
51
|
+
log.warn(`\n${bold('⚠ Mode conflicts')}`);
|
|
52
|
+
for (const { service, requests } of conflicts) {
|
|
53
|
+
const where = requests
|
|
54
|
+
.map(r => `${colorMode(service, r.mode)}${r.by ? ` (dep of ${r.by})` : ' (primary)'}`)
|
|
55
|
+
.join(' vs ');
|
|
56
|
+
log.warn(` - '${service}' requested in conflicting modes: ${where}.`);
|
|
57
|
+
}
|
|
58
|
+
log.warn(` go-dev runs one instance per service, so some dependencies above may be unmet.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
const extraArgs = new Map();
|
|
46
62
|
{
|
|
47
63
|
const argsToParse = process.argv.slice(3);
|
|
@@ -61,7 +77,7 @@ class Orchestrator {
|
|
|
61
77
|
}
|
|
62
78
|
if (currentService == null) {
|
|
63
79
|
if (isGettingService === false) {
|
|
64
|
-
throw new Error(`Invalid arguments, use format: npx go-dev ${
|
|
80
|
+
throw new Error(`Invalid arguments, use format: npx go-dev ${selection.name ?? '<preset>'} ${serviceArgsKeyword} <service> <args>`);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
const splitArg = arg.split(':');
|
package/src/run.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const Orchestrator = require('./orchestrator');
|
|
2
|
+
const { parseCliArgs } = require('./cli-args');
|
|
3
|
+
const { findConfigFile } = require('./config');
|
|
4
|
+
const { resolvePreset } = require('./dependency-resolver');
|
|
5
|
+
const { runInteractive } = require('./interactive');
|
|
6
|
+
const { saveLastSelection } = require('./last-selection');
|
|
7
|
+
const log = require('./logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shared CLI entry flow, used by both `bin/go-dev` and `src/index.js`.
|
|
11
|
+
*
|
|
12
|
+
* Resolves a service selection — from a preset name, or interactively when no
|
|
13
|
+
* preset is given (or `--interactive` is set) — and hands it to the orchestrator.
|
|
14
|
+
*
|
|
15
|
+
* @param {string[]} argv - argv tail (already stripped of node + script path).
|
|
16
|
+
*/
|
|
17
|
+
async function run(argv) {
|
|
18
|
+
try {
|
|
19
|
+
const { presetName, configPath, logLevel, interactive, remaining } = parseCliArgs(argv);
|
|
20
|
+
|
|
21
|
+
const resolvedConfigPath = configPath ?? findConfigFile();
|
|
22
|
+
const orchestrator = new Orchestrator(resolvedConfigPath, { logLevel });
|
|
23
|
+
|
|
24
|
+
let selection;
|
|
25
|
+
if (interactive || !presetName) {
|
|
26
|
+
if (!presetName && !process.stdin.isTTY) {
|
|
27
|
+
console.error(
|
|
28
|
+
'Error: no preset given and no interactive terminal. ' +
|
|
29
|
+
'Specify a preset (go-dev <preset>) or run in a TTY to use the interactive selector.'
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
selection = await runInteractive(orchestrator.config, {
|
|
35
|
+
configPath: resolvedConfigPath,
|
|
36
|
+
presetName,
|
|
37
|
+
});
|
|
38
|
+
if (!selection) {
|
|
39
|
+
process.exit(0); // user cancelled
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
selection = { name: presetName, ...resolvePreset(orchestrator.config, presetName) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Remember this selection (per config, in the user's state dir — never in
|
|
46
|
+
// the consumer's repo) so the interactive selector can restore it next time.
|
|
47
|
+
saveLastSelection(resolvedConfigPath, selection);
|
|
48
|
+
|
|
49
|
+
// Keep `remaining` (the `--args-for ...` tail) at argv index >= 3, where the
|
|
50
|
+
// orchestrator's per-service args parser reads it. Index 2 is unused there.
|
|
51
|
+
process.argv = [process.argv[0], process.argv[1], selection.name ?? '', ...remaining];
|
|
52
|
+
|
|
53
|
+
await orchestrator.start(selection);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Setup-phase failures (bad config, unknown preset, TUI errors). The
|
|
56
|
+
// orchestrator handles its own runtime errors and cleanup internally.
|
|
57
|
+
log.error(`\n❌ ${error.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { run };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const yaml = require('js-yaml');
|
|
3
|
+
const { loadConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders a value as a YAML scalar, quoting it only when it isn't a plain,
|
|
7
|
+
* safe identifier. Service/mode names come from config keys (already safe);
|
|
8
|
+
* user-typed preset names may need quoting.
|
|
9
|
+
*/
|
|
10
|
+
function scalar(value) {
|
|
11
|
+
return /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(value) ? value : JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the YAML block for a single preset entry, indented under `presets:`.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @param {{ services: string[], modes?: Record<string, string> }} selection
|
|
19
|
+
* @param {string} indent - leading indentation for the entry key (e.g. ' ').
|
|
20
|
+
*/
|
|
21
|
+
function buildPresetBlock(name, selection, indent) {
|
|
22
|
+
const step = ' ';
|
|
23
|
+
const lines = [
|
|
24
|
+
`${indent}${scalar(name)}:`,
|
|
25
|
+
`${indent}${step}services: [${selection.services.map(scalar).join(', ')}]`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const modes = selection.modes ?? {};
|
|
29
|
+
const modeEntries = Object.entries(modes).filter(([, mode]) => mode != null);
|
|
30
|
+
if (modeEntries.length > 0) {
|
|
31
|
+
lines.push(`${indent}${step}modes:`);
|
|
32
|
+
for (const [service, mode] of modeEntries) {
|
|
33
|
+
lines.push(`${indent}${step}${step}${scalar(service)}: ${scalar(mode)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Inserts a preset into a raw YAML config, preserving the rest of the file
|
|
42
|
+
* (comments, key order, formatting). Falls back to a full re-dump only when the
|
|
43
|
+
* existing `presets:` key isn't a plain block we can append to.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} raw
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @param {object} selection
|
|
48
|
+
* @returns {string} the new file content
|
|
49
|
+
*/
|
|
50
|
+
function insertPreset(raw, name, selection) {
|
|
51
|
+
const lines = raw.split('\n');
|
|
52
|
+
const presetsIndex = lines.findIndex((line) => /^(\s*)presets:\s*$/.test(line));
|
|
53
|
+
|
|
54
|
+
// Case A: a plain `presets:` block key exists — insert as its first entry.
|
|
55
|
+
if (presetsIndex >= 0) {
|
|
56
|
+
const baseIndent = lines[presetsIndex].match(/^(\s*)/)[1] + ' ';
|
|
57
|
+
const block = buildPresetBlock(name, selection, baseIndent);
|
|
58
|
+
lines.splice(presetsIndex + 1, 0, block);
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Case B: no `presets:` key at all — append a fresh block at the end.
|
|
63
|
+
const hasPresetsKey = lines.some((line) => /^\s*presets:/.test(line));
|
|
64
|
+
if (!hasPresetsKey) {
|
|
65
|
+
const trimmed = raw.replace(/\s*$/, '');
|
|
66
|
+
const block = buildPresetBlock(name, selection, ' ');
|
|
67
|
+
return `${trimmed}\n\npresets:\n${block}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Case C: `presets:` exists but inline (e.g. `presets: {}`) — re-dump to stay
|
|
71
|
+
// structurally correct. Comments are lost; acceptable for this edge case.
|
|
72
|
+
const parsed = yaml.load(raw) ?? {};
|
|
73
|
+
parsed.presets = parsed.presets ?? {};
|
|
74
|
+
parsed.presets[name] = {
|
|
75
|
+
services: selection.services,
|
|
76
|
+
...(Object.keys(selection.modes ?? {}).length > 0 ? { modes: selection.modes } : {}),
|
|
77
|
+
};
|
|
78
|
+
return yaml.dump(parsed, { lineWidth: 120 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Persists a selection as a named preset in the given config file, then
|
|
83
|
+
* re-validates the file by loading it. On validation failure the original
|
|
84
|
+
* content is restored and the error is re-thrown.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} configPath
|
|
87
|
+
* @param {string} name
|
|
88
|
+
* @param {{ services: string[], modes?: Record<string, string> }} selection
|
|
89
|
+
*/
|
|
90
|
+
function savePreset(configPath, name, selection) {
|
|
91
|
+
const original = fs.readFileSync(configPath, 'utf8');
|
|
92
|
+
const updated = insertPreset(original, name, selection);
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(configPath, updated, 'utf8');
|
|
95
|
+
try {
|
|
96
|
+
loadConfig(configPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
fs.writeFileSync(configPath, original, 'utf8');
|
|
99
|
+
throw new Error(`Failed to save preset '${name}': ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { savePreset, insertPreset };
|