go-dev 0.7.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 CHANGED
@@ -69,6 +69,8 @@ If the same service is pulled in under **two different modes** (e.g. `keplero:bu
69
69
 
70
70
  Navigate tabs with <kbd>←</kbd>/<kbd>→</kbd>, move with <kbd>↑</kbd>/<kbd>↓</kbd>, and quit with <kbd>q</kbd>. When stdin is not a TTY (e.g. CI) and no preset is given, `go-dev` exits with an error instead of opening the TUI.
71
71
 
72
+ The selector **remembers your last launched selection per config file** and restores it the next time you open it. This state is stored in your user state directory (`$XDG_STATE_HOME/go-dev/` on Linux/macOS, `%LOCALAPPDATA%\go-dev\` on Windows), keyed by the config file's canonical absolute path — **never written into your repo**.
73
+
72
74
  **Passing Arguments to Service Commands:**
73
75
 
74
76
  To pass additional arguments from the command line to a specific service command, use a keyword flag followed by the target and its arguments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-dev",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "main": "src/index.js",
5
5
  "bin": {
6
6
  "go-dev": "bin/go-dev"
@@ -1,6 +1,7 @@
1
1
  const termkit = require('terminal-kit');
2
2
  const { savePreset } = require('./save-preset');
3
3
  const { resolveServiceExecutionGraph } = require('./dependency-resolver');
4
+ const { loadLastSelection } = require('./last-selection');
4
5
  const log = require('./logger');
5
6
 
6
7
  /**
@@ -35,11 +36,23 @@ function runInteractive(config, { configPath, presetName } = {}) {
35
36
  const chosenMode = new Map();
36
37
  for (const name of serviceNames) chosenMode.set(name, defaultModeFor(name));
37
38
 
38
- // Pre-populate from a preset when forced interactive with a preset given.
39
+ // Pre-populate the custom selection: from a preset when forced interactive
40
+ // with one, otherwise restore the last launched selection (dropping anything
41
+ // no longer valid in the current config).
39
42
  if (presetName && config.presets?.[presetName]) {
40
43
  const preset = config.presets[presetName];
41
44
  for (const s of preset.services) selected.add(s);
42
45
  for (const [s, m] of Object.entries(preset.modes ?? {})) chosenMode.set(s, m);
46
+ } else {
47
+ const last = loadLastSelection(configPath);
48
+ if (last) {
49
+ for (const s of last.services) {
50
+ if (serviceNames.includes(s)) selected.add(s);
51
+ }
52
+ for (const [s, m] of Object.entries(last.modes ?? {})) {
53
+ if (serviceNames.includes(s) && modesFor(s).includes(m)) chosenMode.set(s, m);
54
+ }
55
+ }
43
56
  }
44
57
 
45
58
  const TABS = ['Services & Modes', 'Presets'];
@@ -209,14 +222,23 @@ function runInteractive(config, { configPath, presetName } = {}) {
209
222
  // --- lifecycle -----------------------------------------------------------
210
223
  return new Promise((resolve) => {
211
224
  let finished = false;
225
+ let inPrompt = false; // true while terminal-kit's save prompts own the screen
212
226
 
213
227
  function cleanup() {
214
228
  term.removeListener('key', onKey);
229
+ term.removeListener('resize', onResize);
215
230
  term.grabInput(false);
216
231
  term.hideCursor(false);
217
232
  term.fullscreen(false);
218
233
  }
219
234
 
235
+ // terminal-kit updates term.width/term.height before emitting 'resize';
236
+ // render() recomputes its layout from those, so a full redraw is enough.
237
+ function onResize() {
238
+ if (finished || inPrompt) return;
239
+ render();
240
+ }
241
+
220
242
  function finish(result) {
221
243
  if (finished) return;
222
244
  finished = true;
@@ -232,6 +254,7 @@ function runInteractive(config, { configPath, presetName } = {}) {
232
254
  }
233
255
 
234
256
  // Hand input over to terminal-kit's prompt helpers for the save flow.
257
+ inPrompt = true;
235
258
  term.removeListener('key', onKey);
236
259
  term.hideCursor(false);
237
260
 
@@ -311,6 +334,7 @@ function runInteractive(config, { configPath, presetName } = {}) {
311
334
  term.grabInput(true);
312
335
  term.hideCursor(true);
313
336
  term.on('key', onKey);
337
+ term.on('resize', onResize);
314
338
  render();
315
339
  });
316
340
  }
@@ -0,0 +1,75 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ // Where we remember the last launched selection — a single per-user file,
6
+ // keyed by config path, kept OUTSIDE the consumer's repo so it never shows up
7
+ // in their working tree. Follows XDG state on Linux/macOS, LOCALAPPDATA on
8
+ // Windows.
9
+ function stateDir() {
10
+ if (process.platform === 'win32') {
11
+ const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
12
+ return path.join(base, 'go-dev');
13
+ }
14
+ const base = process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state');
15
+ return path.join(base, 'go-dev');
16
+ }
17
+
18
+ function stateFile() {
19
+ return path.join(stateDir(), 'last-selections.json');
20
+ }
21
+
22
+ function readAll() {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(stateFile(), 'utf8')) || {};
25
+ } catch {
26
+ return {}; // missing or corrupt — start fresh
27
+ }
28
+ }
29
+
30
+ // Key by the canonical absolute path of the config file, so the same file
31
+ // reached via a relative path, a symlink, or `..` always maps to one entry.
32
+ function keyFor(configPath) {
33
+ const absolute = path.resolve(configPath);
34
+ try {
35
+ return fs.realpathSync(absolute);
36
+ } catch {
37
+ return absolute; // file not resolvable (shouldn't happen for a loaded config)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Returns the last selection launched against this config, or null.
43
+ * @param {string} configPath
44
+ * @returns {{ name?: string, services: string[], modes: Record<string,string> } | null}
45
+ */
46
+ function loadLastSelection(configPath) {
47
+ if (!configPath) return null;
48
+ const entry = readAll()[keyFor(configPath)];
49
+ if (!entry || !Array.isArray(entry.services)) return null;
50
+ return { name: entry.name, services: entry.services, modes: entry.modes ?? {} };
51
+ }
52
+
53
+ /**
54
+ * Persists the last launched selection for this config. Best-effort: never lets
55
+ * a persistence failure (e.g. read-only home) break a launch.
56
+ * @param {string} configPath
57
+ * @param {{ name?: string, services: string[], modes?: Record<string,string> }} selection
58
+ */
59
+ function saveLastSelection(configPath, selection) {
60
+ if (!configPath || !selection) return;
61
+ try {
62
+ const all = readAll();
63
+ all[keyFor(configPath)] = {
64
+ name: selection.name,
65
+ services: selection.services,
66
+ modes: selection.modes ?? {},
67
+ };
68
+ fs.mkdirSync(stateDir(), { recursive: true });
69
+ fs.writeFileSync(stateFile(), JSON.stringify(all, null, 2), 'utf8');
70
+ } catch {
71
+ // ignore — remembering the selection is a convenience, not a requirement
72
+ }
73
+ }
74
+
75
+ module.exports = { loadLastSelection, saveLastSelection, stateFile };
package/src/run.js CHANGED
@@ -3,6 +3,7 @@ const { parseCliArgs } = require('./cli-args');
3
3
  const { findConfigFile } = require('./config');
4
4
  const { resolvePreset } = require('./dependency-resolver');
5
5
  const { runInteractive } = require('./interactive');
6
+ const { saveLastSelection } = require('./last-selection');
6
7
  const log = require('./logger');
7
8
 
8
9
  /**
@@ -41,6 +42,10 @@ async function run(argv) {
41
42
  selection = { name: presetName, ...resolvePreset(orchestrator.config, presetName) };
42
43
  }
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
+
44
49
  // Keep `remaining` (the `--args-for ...` tail) at argv index >= 3, where the
45
50
  // orchestrator's per-service args parser reads it. Index 2 is unused there.
46
51
  process.argv = [process.argv[0], process.argv[1], selection.name ?? '', ...remaining];