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.
@@ -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
- async start(presetName) {
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
- presetName,
41
+ selection,
37
42
  );
38
43
 
39
- log.info(bold(`Preset: ${presetName}`));
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 ${presetName} ${serviceArgsKeyword} <service> <args>`);
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 };