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 +2 -0
- package/package.json +1 -1
- package/src/interactive.js +25 -1
- package/src/last-selection.js +75 -0
- package/src/run.js +5 -0
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
package/src/interactive.js
CHANGED
|
@@ -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
|
|
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];
|