@tiens.nguyen/gu-cli 1.0.782 → 1.0.783
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/client-expert-run.mjs +2 -2
- package/gu-repl.mjs +171 -227
- package/package.json +1 -1
- package/preset-apply.mjs +2 -2
- package/preset-editor.mjs +90 -12
- package/presets-transfer.mjs +5 -5
package/client-expert-run.mjs
CHANGED
|
@@ -1111,7 +1111,7 @@ export async function runClientExpertSetup({
|
|
|
1111
1111
|
console.log("");
|
|
1112
1112
|
console.log(" " + cyan("✓ Model setup skipped") + dim(loadPreset
|
|
1113
1113
|
? " — the preset picker opens as soon as gu starts"
|
|
1114
|
-
: " — set them any time with /model (or /
|
|
1114
|
+
: " — set them any time with /model (or /use-presets)"));
|
|
1115
1115
|
} else if (Object.keys(written).length === 0) {
|
|
1116
1116
|
console.log(" " + cyan("✓ Models and RAG") + dim(" — already configured, left alone"));
|
|
1117
1117
|
} else {
|
|
@@ -1159,7 +1159,7 @@ export async function runClientExpertSetup({
|
|
|
1159
1159
|
dim("\n machine. The API restarts with `gu-cli api start`; its log is") +
|
|
1160
1160
|
dim(`\n ${API_LOG_FILE}\n`)
|
|
1161
1161
|
);
|
|
1162
|
-
// #301: the caller opens /
|
|
1162
|
+
// #301: the caller opens /use-presets once the terminal exists — the picker, the plan and the
|
|
1163
1163
|
// confirmation are that command's, not rebuilt here.
|
|
1164
1164
|
return { complete: true, stoppedAt: null, ...(loadPreset ? { loadPreset: true } : {}) };
|
|
1165
1165
|
}
|
package/gu-repl.mjs
CHANGED
|
@@ -636,7 +636,7 @@ function reloadWorkerEnv() {
|
|
|
636
636
|
// first-run branch also runs one — without this the same walkthrough printed twice in a single
|
|
637
637
|
// run, MongoDB instructions and all. Reported live on Windows 11.
|
|
638
638
|
let clientExpertRanThisProcess = false;
|
|
639
|
-
// #301: setup chose "Load a saved preset" — main() opens /
|
|
639
|
+
// #301: setup chose "Load a saved preset" — main() opens /use-presets before the first prompt.
|
|
640
640
|
// Declared HERE, above the first-run block: that block runs at module top level, before any
|
|
641
641
|
// declaration further down exists.
|
|
642
642
|
let openPresetsAtStart = false;
|
|
@@ -881,8 +881,9 @@ const COMMANDS = [
|
|
|
881
881
|
// object {apiUrl, apiKey, agentModel, codingModel, embedModel} and validates every field
|
|
882
882
|
// before writing. A preset file HOLDS its API key (0600, home folder — the settings files'
|
|
883
883
|
// location), so it is marked "gu": "preset" and the two commands never offer each other's files.
|
|
884
|
-
{ name: "/save-preset", usage: "/save-preset
|
|
885
|
-
{ name: "/open-presets",
|
|
884
|
+
{ name: "/save-preset", usage: "/save-preset [name]", desc: "paste a preset (from another computer's /open-presets) into your editor and save it under a name — nothing is applied" },
|
|
885
|
+
{ name: "/open-presets", usage: "/open-presets [name]", desc: "your whole setup now, keys included, read-only in your editor — to copy to another computer; with a name, edit that preset" },
|
|
886
|
+
{ name: "/use-presets", usage: "/use-presets [name]", desc: "pick a saved preset and apply it — shows what changes, backs up your setup, asks first" },
|
|
886
887
|
{ name: "/doctor", desc: "check every configured model ANSWERS — especially the embedder, which a turn only reaches after several steps" },
|
|
887
888
|
{ name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
|
|
888
889
|
// ONE name, /new (user, #278). /reset and /clear still work (HIDDEN_COMMANDS) for habit and
|
|
@@ -6823,17 +6824,6 @@ function withTerminalHandedOver(run) {
|
|
|
6823
6824
|
}
|
|
6824
6825
|
}
|
|
6825
6826
|
|
|
6826
|
-
/**
|
|
6827
|
-
* /save-preset <name> — THE PRESET IN AN EDITOR (#312). Opens $VISUAL / $EDITOR / nano / vi
|
|
6828
|
-
* (Notepad on Windows) on a temp file filled in with the preset being edited or the current
|
|
6829
|
-
* setup; save and quit stores it, quit without a change cancels. A wrong value is named and the
|
|
6830
|
-
* editor reopens with the person's OWN text, never the template again.
|
|
6831
|
-
*
|
|
6832
|
-
* /save-preset <name> <json> — the one-line form (#6), kept for scripts and for pasting the
|
|
6833
|
-
* command /load-presets copies. Both validate everything before a byte is written. A preset SAVES
|
|
6834
|
-
* the API key by design (unlike /save-settings) — stored 0600 in the home folder, marked
|
|
6835
|
-
* "gu": "preset" so the two commands never offer each other's files.
|
|
6836
|
-
*/
|
|
6837
6827
|
/**
|
|
6838
6828
|
* THIS ACCOUNT'S WHOLE SETUP as a version-2 preset (#320): every coding server and its models, the
|
|
6839
6829
|
* default coder, the agent, the embedder — and each server's API key, read through the local API the
|
|
@@ -6876,129 +6866,163 @@ async function fetchKeyForBackend(backendId) {
|
|
|
6876
6866
|
}
|
|
6877
6867
|
}
|
|
6878
6868
|
|
|
6869
|
+
/**
|
|
6870
|
+
* /save-preset [name] (#322) — PASTE A PRESET AND NAME IT. Opens an empty editor (the // header says
|
|
6871
|
+
* how to save in THAT editor); the person pastes the JSON another computer's /open-presets showed,
|
|
6872
|
+
* saves and quits. Checked in full, then stored 0600 under the name — NOTHING is applied (that is
|
|
6873
|
+
* /use-presets). `/save-preset <name> {json}` is the one-line form, for scripts.
|
|
6874
|
+
*/
|
|
6879
6875
|
async function runSavePresetCommand(line) {
|
|
6880
6876
|
const pr = await import("./presets-transfer.mjs");
|
|
6881
|
-
const
|
|
6877
|
+
const ed = await import("./preset-editor.mjs");
|
|
6882
6878
|
console.log("");
|
|
6883
|
-
const usage = "usage: /save-preset <name> (saves your whole setup) · /save-preset <name> --edit (in your editor) · /save-preset <name> {json}";
|
|
6884
6879
|
const raw = line.trim().slice("/save-preset".length).trim();
|
|
6885
|
-
const
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6880
|
+
const space = raw.search(/\s/);
|
|
6881
|
+
const given = space === -1 ? raw : raw.slice(0, space);
|
|
6882
|
+
const json = space === -1 ? "" : raw.slice(space + 1).trim();
|
|
6883
|
+
if (given && !pr.isPresetName(given)) {
|
|
6884
|
+
console.log(GUTTER + `"${given}" is not a preset name — one word, no slashes. usage: /save-preset [name]` + "\n");
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
try {
|
|
6888
|
+
let parsed = null;
|
|
6889
|
+
if (json) {
|
|
6890
|
+
parsed = ed.parsePastedPreset(json);
|
|
6891
|
+
if (!parsed.ok) {
|
|
6892
|
+
console.log(GUTTER + `Preset not saved — ${parsed.problem}` + "\n");
|
|
6893
|
+
return;
|
|
6894
|
+
}
|
|
6895
|
+
} else {
|
|
6896
|
+
parsed = await pastePresetInEditor(ed);
|
|
6897
|
+
if (!parsed) {
|
|
6898
|
+
console.log(GUTTER + dim("Not saved.") + "\n");
|
|
6899
|
+
return;
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
const name = given || (await askPresetName(pr, parsed.name || ""));
|
|
6903
|
+
if (!name) {
|
|
6904
|
+
console.log(GUTTER + dim("Not saved.") + "\n");
|
|
6891
6905
|
return;
|
|
6892
6906
|
}
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
console.log(GUTTER +
|
|
6900
|
-
|
|
6901
|
-
|
|
6907
|
+
const file = pr.presetFilePath(name, homedir());
|
|
6908
|
+
if (existsSync(file) && !(await askYesNo(`A preset called "${name}" exists — replace it?`, false))) {
|
|
6909
|
+
console.log(GUTTER + dim("Not saved.") + "\n");
|
|
6910
|
+
return;
|
|
6911
|
+
}
|
|
6912
|
+
writePresetFile(pr, file, name, parsed);
|
|
6913
|
+
console.log(GUTTER + dim(`/use-presets → ${name} applies it.`) + "\n");
|
|
6914
|
+
} catch (e) {
|
|
6915
|
+
if (wasPromptCancelled(e)) {
|
|
6916
|
+
console.log(GUTTER + dim("Not saved.") + "\n");
|
|
6917
|
+
return;
|
|
6918
|
+
}
|
|
6919
|
+
throw e;
|
|
6902
6920
|
}
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
// A WHOLE-SETUP preset (#320): written as given (dated, named), then summarised — keys masked.
|
|
6911
|
-
const storeFull = (doc) => {
|
|
6912
|
-
const out = { ...doc, gu: "preset", version: pr.FULL_PRESET_VERSION, name, savedAt: new Date().toISOString(), savedOn: hostname(),
|
|
6913
|
-
note: "The whole setup: every coding server and its models, the default coder, the agent and the embedder — and each server's API key. This file CONTAINS live API keys." };
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6923
|
+
/** Store a checked preset (`parsed` from parsePastedPreset/parseEdited) — 0600, named, dated. */
|
|
6924
|
+
function writePresetFile(pr, file, name, parsed) {
|
|
6925
|
+
if (parsed.full) {
|
|
6926
|
+
const { note: _n, savedAt: _a, savedOn: _o, ...body } = parsed.doc;
|
|
6927
|
+
const out = { ...body, gu: "preset", version: pr.FULL_PRESET_VERSION, name, savedAt: new Date().toISOString(), savedOn: hostname() };
|
|
6914
6928
|
writeFileSync(file, JSON.stringify(out, null, 2) + "\n", { mode: 0o600 });
|
|
6915
6929
|
const v = pr.validatePresetFile(JSON.stringify(out));
|
|
6916
|
-
console.log(GUTTER + `Preset "${name}" saved to ${file}
|
|
6930
|
+
console.log(GUTTER + `Preset "${name}" saved to ${file}:`);
|
|
6917
6931
|
if (v.ok && v.full) for (const l of pr.fullPresetSummary(v.value, v.codingDefault)) console.log(GUTTER + dim(l));
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
const ed = await import("./preset-editor.mjs");
|
|
6922
|
-
if (json) {
|
|
6923
|
-
const checked = ed.parseEdited(json);
|
|
6924
|
-
if (!checked.ok) {
|
|
6925
|
-
console.log(GUTTER + `Preset not saved — ${checked.problem}`);
|
|
6926
|
-
console.log("");
|
|
6927
|
-
return;
|
|
6928
|
-
}
|
|
6929
|
-
if (checked.full) storeFull(checked.doc);
|
|
6930
|
-
else store(checked.preset);
|
|
6931
|
-
return;
|
|
6932
|
+
} else {
|
|
6933
|
+
writeFileSync(file, JSON.stringify(pr.buildPreset({ name, preset: parsed.preset, host: hostname() }), null, 2) + "\n", { mode: 0o600 });
|
|
6934
|
+
console.log(GUTTER + `Preset "${name}" saved to ${file}`);
|
|
6932
6935
|
}
|
|
6936
|
+
console.log(GUTTER + dim("It CONTAINS API keys — the file is readable only by you."));
|
|
6937
|
+
}
|
|
6933
6938
|
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6939
|
+
/**
|
|
6940
|
+
* /open-presets [name] (#322).
|
|
6941
|
+
* · no name — YOUR SETUP NOW (every server, model, key, the default coder, agent, embedder),
|
|
6942
|
+
* READ-ONLY in the editor, to copy to another computer. Then it offers the clipboard too (OSC 52
|
|
6943
|
+
* reaches the person's own machine over SSH): a long setup scrolls past one screen of nano, and
|
|
6944
|
+
* selecting across a scroll with the mouse does not work.
|
|
6945
|
+
* · a name — that preset in the editor; a saved change is checked in full first and a broken one is
|
|
6946
|
+
* never stored ("Edit again?" reopens the person's own text).
|
|
6947
|
+
*/
|
|
6948
|
+
async function runOpenPresetsCommand(line) {
|
|
6949
|
+
const pr = await import("./presets-transfer.mjs");
|
|
6950
|
+
const ed = await import("./preset-editor.mjs");
|
|
6951
|
+
console.log("");
|
|
6952
|
+
const name = line.trim().slice("/open-presets".length).trim();
|
|
6953
|
+
if (!name) {
|
|
6954
|
+
const doc = await withPendingStatus("reading your servers, models and keys", () => buildFullPresetNow("my-setup"));
|
|
6942
6955
|
if (!doc) {
|
|
6943
|
-
console.log(GUTTER + red(`couldn't read your settings from ${apiBase}
|
|
6956
|
+
console.log(GUTTER + red(`couldn't read your settings from ${apiBase}.`) + "\n");
|
|
6944
6957
|
return;
|
|
6945
6958
|
}
|
|
6946
|
-
|
|
6959
|
+
const text = ed.currentSetupText(doc);
|
|
6960
|
+
const editor = ed.pickEditor({ readOnly: true });
|
|
6961
|
+
if (editor && process.stdin.isTTY) {
|
|
6962
|
+
console.log(GUTTER + dim(`Opening ${basename(editor.cmd)} read-only (${editor.saveHint}).`));
|
|
6963
|
+
const r = withTerminalHandedOver(() => ed.editText(text, { editor, fileName: "gu-setup.json" }));
|
|
6964
|
+
if (!r.ok) console.log(GUTTER + yellow(`couldn't show it — ${r.why}.`));
|
|
6965
|
+
} else {
|
|
6966
|
+
console.log(GUTTER + yellow(editor ? "an editor needs a terminal." : ed.NO_EDITOR_HINT + "."));
|
|
6967
|
+
}
|
|
6968
|
+
const v = pr.validatePresetFile(JSON.stringify(doc));
|
|
6969
|
+
if (v.ok && v.full) for (const l of pr.fullPresetSummary(v.value, v.codingDefault)) console.log(GUTTER + dim(l));
|
|
6970
|
+
if (await askYesNo("Copy it to the clipboard (keys included)?", true)) {
|
|
6971
|
+
const res = await writeClipboard(text.split("\n").filter((l) => !l.startsWith("//")).join("\n"));
|
|
6972
|
+
console.log(GUTTER + (res.ok ? accent("✓ copied — on the other computer: /save-preset, paste, save and quit.") : yellow(`couldn't copy (${res.why}).`)));
|
|
6973
|
+
}
|
|
6974
|
+
console.log("");
|
|
6975
|
+
return;
|
|
6976
|
+
}
|
|
6977
|
+
const file = pr.presetFilePath(name, homedir());
|
|
6978
|
+
if (!file) return; // a cancel word
|
|
6979
|
+
if (!existsSync(file)) {
|
|
6980
|
+
console.log(GUTTER + `no preset called "${name}" — /use-presets lists them.` + "\n");
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
let doc = null;
|
|
6984
|
+
try {
|
|
6985
|
+
doc = JSON.parse(readFileSync(file, "utf8"));
|
|
6986
|
+
} catch {
|
|
6987
|
+
/* named below */
|
|
6988
|
+
}
|
|
6989
|
+
if (doc?.gu !== "preset") {
|
|
6990
|
+
console.log(GUTTER + `${file} is not a gu preset.` + "\n");
|
|
6947
6991
|
return;
|
|
6948
6992
|
}
|
|
6949
|
-
|
|
6950
|
-
// THE EDITOR. Filled in from the preset being edited, else from the whole setup this account has now.
|
|
6951
6993
|
const editor = ed.pickEditor();
|
|
6952
6994
|
if (!editor || !process.stdin.isTTY) {
|
|
6953
|
-
console.log(GUTTER + (editor ? "an editor needs a terminal
|
|
6954
|
-
console.log("");
|
|
6995
|
+
console.log(GUTTER + yellow(editor ? "an editor needs a terminal." : ed.NO_EDITOR_HINT + ".") + "\n");
|
|
6955
6996
|
return;
|
|
6956
6997
|
}
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
try {
|
|
6960
|
-
const doc = JSON.parse(readFileSync(file, "utf8"));
|
|
6961
|
-
if (doc?.gu === "preset") start = doc;
|
|
6962
|
-
} catch {
|
|
6963
|
-
/* unreadable — start from the current setup instead */
|
|
6964
|
-
}
|
|
6965
|
-
}
|
|
6966
|
-
if (!start) start = (await buildFullPresetNow(name)) ?? ed.presetFromSettings((await fetchWorkerSettings()) ?? {});
|
|
6967
|
-
const initial = ed.editorText(name, start);
|
|
6968
|
-
console.log(GUTTER + dim(`${start.gu ? "editing" : "new"} preset "${name}" in ${basename(editor.cmd)} — save and quit to store it, quit without changes to cancel.`));
|
|
6998
|
+
const initial = ed.editorText(name, doc);
|
|
6999
|
+
console.log(GUTTER + dim(`Opening "${name}" in ${basename(editor.cmd)} — save a change, or just quit.`));
|
|
6969
7000
|
let text = initial;
|
|
6970
7001
|
for (;;) {
|
|
6971
7002
|
const r = withTerminalHandedOver(() => ed.editText(text, { editor, fileName: `${name}.json` }));
|
|
6972
7003
|
if (!r.ok) {
|
|
6973
|
-
console.log(GUTTER + `
|
|
6974
|
-
console.log("");
|
|
7004
|
+
console.log(GUTTER + `Not changed — ${r.why}.` + "\n");
|
|
6975
7005
|
return;
|
|
6976
7006
|
}
|
|
6977
7007
|
if (r.text === initial) {
|
|
6978
|
-
console.log(GUTTER + dim("Not
|
|
7008
|
+
console.log(GUTTER + dim("Not changed.") + "\n");
|
|
6979
7009
|
return;
|
|
6980
7010
|
}
|
|
6981
7011
|
const parsed = ed.parseEdited(r.text);
|
|
6982
7012
|
if (parsed.ok) {
|
|
6983
|
-
|
|
6984
|
-
|
|
7013
|
+
writePresetFile(pr, file, name, parsed);
|
|
7014
|
+
console.log("");
|
|
6985
7015
|
return;
|
|
6986
7016
|
}
|
|
6987
|
-
console.log(GUTTER + yellow(`Not saved
|
|
7017
|
+
console.log(GUTTER + yellow(`Not saved — ${parsed.problem}`));
|
|
6988
7018
|
if (!(await askYesNo("Edit again?", true))) {
|
|
6989
|
-
console.log(GUTTER + dim("Not
|
|
7019
|
+
console.log(GUTTER + dim("Not changed.") + "\n");
|
|
6990
7020
|
return;
|
|
6991
7021
|
}
|
|
6992
|
-
text = r.text; // their text, with the mistake in it — not the
|
|
7022
|
+
text = r.text; // their text, with the mistake in it — not the saved preset again
|
|
6993
7023
|
}
|
|
6994
7024
|
}
|
|
6995
7025
|
|
|
6996
|
-
/**
|
|
6997
|
-
* /load-presets — list the preset files /save-preset wrote (newest first, the arrow-key picker)
|
|
6998
|
-
* and apply the chosen one: the server + both models land as one coding-backend row, the preset's
|
|
6999
|
-
* key is saved under the preset's name and made that row's default, and the embedder is set. The
|
|
7000
|
-
* user reconfirms before anything changes, like /load-settings.
|
|
7001
|
-
*/
|
|
7002
7026
|
/**
|
|
7003
7027
|
* After Ctrl+C stopped THIS terminal's turn (task #302): say what it left behind, and give the
|
|
7004
7028
|
* question back.
|
|
@@ -7109,55 +7133,27 @@ async function runUndoCommand(line) {
|
|
|
7109
7133
|
}
|
|
7110
7134
|
|
|
7111
7135
|
/**
|
|
7112
|
-
*
|
|
7113
|
-
*
|
|
7114
|
-
*
|
|
7115
|
-
* echoed. A mistake is named; Enter then opens it IN THE EDITOR with the pasted text (not a blank
|
|
7116
|
-
* template), and Enter on nothing opens the editor on the current setup. Esc cancels.
|
|
7117
|
-
* Saved 0600 like /save-preset. → the file, or "" when cancelled.
|
|
7136
|
+
* The pasting half of /save-preset (#317, #322): an EMPTY editor with the // header (the person's
|
|
7137
|
+
* terminal editor over SSH — nano on a fresh Ubuntu server), or, with no editor or no terminal, a
|
|
7138
|
+
* one-line prompt. → the parsed preset, or null when cancelled.
|
|
7118
7139
|
*/
|
|
7119
|
-
async function
|
|
7120
|
-
const ed = await import("./preset-editor.mjs");
|
|
7140
|
+
async function pastePresetInEditor(ed) {
|
|
7121
7141
|
const editor = ed.pickEditor();
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
if (!text.trim()) return "";
|
|
7138
|
-
const p = ed.parsePastedPreset(text);
|
|
7139
|
-
if (p.ok) {
|
|
7140
|
-
parsed = p;
|
|
7141
|
-
break;
|
|
7142
|
-
}
|
|
7143
|
-
console.log(GUTTER + yellow(`Not a preset yet — ${p.problem}`));
|
|
7144
|
-
console.log(GUTTER + dim("Paste it again, or Esc to cancel."));
|
|
7145
|
-
}
|
|
7146
|
-
}
|
|
7147
|
-
const name = await askPresetName(pr, parsed.name || "preset");
|
|
7148
|
-
if (!name) return "";
|
|
7149
|
-
const file = pr.presetFilePath(name, home);
|
|
7150
|
-
if (existsSync(file) && !(await askYesNo(`A preset called "${name}" exists — replace it?`, false))) return "";
|
|
7151
|
-
// A whole-setup preset (#320) is kept as it is — every server, key and default — named and dated.
|
|
7152
|
-
const out = parsed.full
|
|
7153
|
-
? { ...parsed.doc, gu: "preset", version: pr.FULL_PRESET_VERSION, name, savedAt: new Date().toISOString(), savedOn: hostname() }
|
|
7154
|
-
: pr.buildPreset({ name, preset: parsed.preset, host: hostname() });
|
|
7155
|
-
writeFileSync(file, JSON.stringify(out, null, 2) + "\n", { mode: 0o600 });
|
|
7156
|
-
console.log(GUTTER + `Preset "${name}" saved to ${file}`);
|
|
7157
|
-
return file;
|
|
7158
|
-
} catch (e) {
|
|
7159
|
-
if (wasPromptCancelled(e)) return "";
|
|
7160
|
-
throw e;
|
|
7142
|
+
if (editor && process.stdin.isTTY) {
|
|
7143
|
+
// One short line: the save keys are in the file's own header, where they are needed.
|
|
7144
|
+
console.log(GUTTER + dim(`Opening ${basename(editor.cmd)} — paste your preset there, then save and quit.`));
|
|
7145
|
+
return fixPresetInEditor(ed, ed.pasteTemplate(editor.saveHint), { editor });
|
|
7146
|
+
}
|
|
7147
|
+
if (!editor) console.log(GUTTER + yellow(ed.NO_EDITOR_HINT) + dim(" — or paste it here, on one line."));
|
|
7148
|
+
console.log(GUTTER + "Paste the preset — its JSON, a preset file, or a /save-preset line.");
|
|
7149
|
+
console.log(GUTTER + dim("Esc cancels."));
|
|
7150
|
+
for (;;) {
|
|
7151
|
+
const text = String((await ask("preset: ")) ?? "");
|
|
7152
|
+
if (!text.trim()) return null;
|
|
7153
|
+
const p = ed.parsePastedPreset(text);
|
|
7154
|
+
if (p.ok) return p;
|
|
7155
|
+
console.log(GUTTER + yellow(`Not a preset yet — ${p.problem}`));
|
|
7156
|
+
console.log(GUTTER + dim("Paste it again, or Esc to cancel."));
|
|
7161
7157
|
}
|
|
7162
7158
|
}
|
|
7163
7159
|
|
|
@@ -7197,98 +7193,43 @@ async function askPresetName(pr, dflt) {
|
|
|
7197
7193
|
}
|
|
7198
7194
|
}
|
|
7199
7195
|
|
|
7200
|
-
|
|
7196
|
+
/**
|
|
7197
|
+
* /use-presets [name] (#322) — the saved presets, newest first: Enter APPLIES the chosen one (the plan
|
|
7198
|
+
* with keys masked, a backup of the current setup, then "apply? [y/N]" — preset-apply.mjs), o opens it
|
|
7199
|
+
* in the editor (/open-presets <name>). Presets come in through /save-preset.
|
|
7200
|
+
*/
|
|
7201
|
+
async function runUsePresetsCommand(line) {
|
|
7201
7202
|
const pr = await import("./presets-transfer.mjs");
|
|
7202
7203
|
const home = homedir();
|
|
7203
7204
|
console.log("");
|
|
7204
7205
|
let file = "";
|
|
7205
|
-
|
|
7206
|
-
const typed = line.trim().replace(/^\/(open|load)-presets\b/, "").trim();
|
|
7206
|
+
const typed = line.trim().slice("/use-presets".length).trim();
|
|
7207
7207
|
if (typed) {
|
|
7208
7208
|
file = pr.presetFilePath(typed, home);
|
|
7209
7209
|
if (!file) return; // a cancel word
|
|
7210
7210
|
if (!existsSync(file)) {
|
|
7211
|
-
console.log(GUTTER + `no preset
|
|
7212
|
-
console.log("");
|
|
7211
|
+
console.log(GUTTER + `no preset called "${typed}" — /use-presets lists them.` + "\n");
|
|
7213
7212
|
return;
|
|
7214
7213
|
}
|
|
7215
7214
|
} else {
|
|
7216
|
-
// THE PICKER DOES MORE THAN APPLY (#312): v shows the whole preset (key masked) with a
|
|
7217
|
-
// clickable "⧉ copy", c / j copy it as the /save-preset command or as JSON (key included), e
|
|
7218
|
-
// opens it in the editor. Only Enter applies. Each of the others ENDS the command: the mouse
|
|
7219
|
-
// is gu's only at the ">> " prompt (never while a picker is up), so a "⧉ copy" printed under a
|
|
7220
|
-
// picker that stayed open could not be clicked until it closed.
|
|
7221
7215
|
const ed = await import("./preset-editor.mjs");
|
|
7222
7216
|
const found = pr.presetFilesIn(listJsonFiles(home));
|
|
7223
|
-
// NO DEAD END (#317: "I need a place to input a preset I have already"): with nothing saved,
|
|
7224
|
-
// the command asks for one right here; with presets saved, the picker's LAST row does (last, so
|
|
7225
|
-
// Enter still applies the newest preset, as it always has).
|
|
7226
7217
|
if (found.length === 0) {
|
|
7227
7218
|
console.log(GUTTER + "No presets saved yet.");
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
const PASTE_ROW = "+ Paste a preset I already have…";
|
|
7237
|
-
const pick = await pickFromList([...found, PASTE_ROW], 0, { keys: ed.PICKER_KEYS });
|
|
7238
|
-
if (!pick || pick.index < 0) {
|
|
7239
|
-
console.log(GUTTER + "load cancelled.");
|
|
7240
|
-
console.log("");
|
|
7241
|
-
return;
|
|
7242
|
-
}
|
|
7243
|
-
if (pick.index === found.length) {
|
|
7244
|
-
file = await presetFromPaste(pr, home);
|
|
7245
|
-
if (!file) {
|
|
7246
|
-
console.log(GUTTER + "load cancelled.");
|
|
7247
|
-
console.log("");
|
|
7248
|
-
return;
|
|
7249
|
-
}
|
|
7250
|
-
pick.action = "select"; // a pasted preset goes on to be applied, whatever key opened the row
|
|
7251
|
-
} else {
|
|
7252
|
-
file = pr.presetFilePath(found[pick.index], home);
|
|
7253
|
-
}
|
|
7254
|
-
if (pick.action === "edit") {
|
|
7255
|
-
await runSavePresetCommand(`/save-preset ${found[pick.index]}`);
|
|
7256
|
-
return;
|
|
7257
|
-
}
|
|
7258
|
-
if (pick.action !== "select") {
|
|
7259
|
-
const shown = pr.validatePresetFile(readFileSync(file, "utf8"));
|
|
7260
|
-
if (!shown.ok) {
|
|
7261
|
-
console.log(GUTTER + `Can't show it — ${shown.problem}`);
|
|
7262
|
-
console.log("");
|
|
7263
|
-
return;
|
|
7264
|
-
}
|
|
7265
|
-
const pname = shown.name || found[pick.index];
|
|
7266
|
-
// A whole-setup preset (#320) is shown and copied AS SAVED — every server, every key.
|
|
7267
|
-
const body = shown.full ? JSON.parse(readFileSync(file, "utf8")) : shown.preset;
|
|
7268
|
-
const command = ed.presetCopyCommand(pname, body);
|
|
7269
|
-
if (pick.action === "view") {
|
|
7270
|
-
const block = ed.presetBlock(pname, body, { width: process.stdout.columns || 80, gutter: GUTTER });
|
|
7271
|
-
const [top, ...restRows] = block.rows;
|
|
7272
|
-
const cut = block.button.from;
|
|
7273
|
-
console.log(dim(top.slice(0, cut)) + accent(top.slice(cut, block.button.to)) + dim(top.slice(block.button.to)));
|
|
7274
|
-
for (const r of restRows) console.log(r);
|
|
7275
|
-
registerCopyBlock(block.rows.length, command, 0, { button: block.button, note: "✓ copied — includes the API key" });
|
|
7276
|
-
console.log(GUTTER + dim("click ⧉ copy for a /save-preset command to paste anywhere — it includes the real key."));
|
|
7277
|
-
console.log(GUTTER + dim(`/open-presets ${pname} applies it.`));
|
|
7278
|
-
console.log("");
|
|
7279
|
-
return;
|
|
7280
|
-
}
|
|
7281
|
-
const res = await writeClipboard(pick.action === "json" ? ed.presetCopyJson(body) : command);
|
|
7282
|
-
console.log(
|
|
7283
|
-
GUTTER +
|
|
7284
|
-
(res.ok
|
|
7285
|
-
? accent(`✓ copied ${pick.action === "json" ? "the JSON" : "the /save-preset command"} for "${pname}" — includes the API key.`)
|
|
7286
|
-
: yellow(`couldn't copy (${res.why}) — the preset is in ${file}.`))
|
|
7287
|
-
);
|
|
7288
|
-
console.log("");
|
|
7289
|
-
return;
|
|
7290
|
-
}
|
|
7219
|
+
console.log(GUTTER + dim("/save-preset pastes one in — copy it from another computer's /open-presets.") + "\n");
|
|
7220
|
+
return;
|
|
7221
|
+
}
|
|
7222
|
+
console.log(dim(` Presets (${ed.PICKER_HINT}):`));
|
|
7223
|
+
const pick = await pickFromList(found, 0, { keys: ed.PICKER_KEYS });
|
|
7224
|
+
if (!pick || pick.index < 0) {
|
|
7225
|
+
console.log(GUTTER + dim("nothing changed.") + "\n");
|
|
7226
|
+
return;
|
|
7291
7227
|
}
|
|
7228
|
+
if (pick.action === "open") {
|
|
7229
|
+
await runOpenPresetsCommand(`/open-presets ${found[pick.index]}`);
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
file = pr.presetFilePath(found[pick.index], home);
|
|
7292
7233
|
}
|
|
7293
7234
|
const checked = pr.validatePresetFile(readFileSync(file, "utf8"));
|
|
7294
7235
|
if (checked.ok && checked.full) {
|
|
@@ -9513,7 +9454,7 @@ let listAllowDelete = false;
|
|
|
9513
9454
|
// happened rather than encoding it into the index and hoping every caller decodes it.
|
|
9514
9455
|
let listDeleteRequested = false;
|
|
9515
9456
|
// Single letters that finish a single-select picker with an ACTION on the highlighted row, e.g.
|
|
9516
|
-
// /
|
|
9457
|
+
// /use-presets' { o: "open" } (#312, #322). Null unless a caller asks, like ctrl+d above.
|
|
9517
9458
|
let listKeys = null;
|
|
9518
9459
|
let listKeyAction = "";
|
|
9519
9460
|
function drawList(redraw) {
|
|
@@ -12457,13 +12398,13 @@ async function main() {
|
|
|
12457
12398
|
}
|
|
12458
12399
|
|
|
12459
12400
|
// #301: setup's "Load a saved preset" — the picker opens now that the terminal exists. The whole
|
|
12460
|
-
// /
|
|
12401
|
+
// /use-presets flow (the list, the plan, the confirmation, the key) is that command's own.
|
|
12461
12402
|
if (openPresetsAtStart) {
|
|
12462
12403
|
openPresetsAtStart = false;
|
|
12463
12404
|
try {
|
|
12464
|
-
await
|
|
12405
|
+
await runUsePresetsCommand("/use-presets");
|
|
12465
12406
|
} catch (e) {
|
|
12466
|
-
console.log(GUTTER + dim(`(could not open the presets: ${e?.message ?? e} — run /
|
|
12407
|
+
console.log(GUTTER + dim(`(could not open the presets: ${e?.message ?? e} — run /use-presets)`));
|
|
12467
12408
|
}
|
|
12468
12409
|
}
|
|
12469
12410
|
|
|
@@ -12678,9 +12619,12 @@ async function main() {
|
|
|
12678
12619
|
await runSavePresetCommand(line);
|
|
12679
12620
|
continue;
|
|
12680
12621
|
}
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12622
|
+
if (line === "/open-presets" || line.startsWith("/open-presets ")) {
|
|
12623
|
+
await runOpenPresetsCommand(line);
|
|
12624
|
+
continue;
|
|
12625
|
+
}
|
|
12626
|
+
if (line === "/use-presets" || line.startsWith("/use-presets ")) {
|
|
12627
|
+
await runUsePresetsCommand(line);
|
|
12684
12628
|
continue;
|
|
12685
12629
|
}
|
|
12686
12630
|
if (line === "/save-settings" || line.startsWith("/save-settings ")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.783",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/preset-apply.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* here the REAL code runs in tests against a fake API that records every call and can be told to
|
|
5
5
|
* fail — which is how "loading a preset cannot break the workspace" is checked rather than hoped.
|
|
6
6
|
*
|
|
7
|
-
* Used for /
|
|
7
|
+
* Used for /use-presets (a version-2 preset) and /load-settings — one path for all.
|
|
8
8
|
*
|
|
9
9
|
* api = {
|
|
10
10
|
* settings(): Promise<object|null> GET /api/worker/settings
|
|
@@ -101,7 +101,7 @@ export async function applyTransfer(value, { api, io, fromLine = "", carriesSecr
|
|
|
101
101
|
}
|
|
102
102
|
out.backup = b.file;
|
|
103
103
|
say(`backup of your current setup: ${b.file}`);
|
|
104
|
-
say(`to undo: /
|
|
104
|
+
say(`to undo: /use-presets ${b.file}`);
|
|
105
105
|
}
|
|
106
106
|
const patch = toSettingsPatch(value, current);
|
|
107
107
|
const saved = Object.keys(patch).length ? await api.saveSettings(patch) : true;
|
package/preset-editor.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* user's own 0600 file; root would make it root-owned and ask for a password for nothing.
|
|
12
12
|
* · The temp file holds a live key: it is created 0600 inside a private 0700 directory and the
|
|
13
13
|
* directory is removed on every path out, a Ctrl+C included.
|
|
14
|
-
* · /
|
|
14
|
+
* · /use-presets could show a preset as one framed block — the key MASKED on screen (scrollback
|
|
15
15
|
* and logs must not keep it, #248) — and copy it with the real key: as the ready-to-paste
|
|
16
16
|
* `/save-preset <name> {…}` command (paste it into gu on another computer and it is there),
|
|
17
17
|
* or as plain JSON.
|
|
@@ -52,8 +52,9 @@ export function isHeadless({ env = process.env, platform = process.platform } =
|
|
|
52
52
|
* `saveHint` is what the header tells the person to press — editors differ, and a server's nano is
|
|
53
53
|
* often someone's first.
|
|
54
54
|
*/
|
|
55
|
-
export function pickEditor({ env = process.env, platform = process.platform, has = onPath } = {}) {
|
|
55
|
+
export function pickEditor({ env = process.env, platform = process.platform, has = onPath, readOnly = false } = {}) {
|
|
56
56
|
const headless = isHeadless({ env, platform });
|
|
57
|
+
const withHints = (ed) => editorHints(ed, { readOnly, platform });
|
|
57
58
|
const named = [env.VISUAL, env.EDITOR].map(splitCommand).filter((w) => w.length);
|
|
58
59
|
for (const words of named) {
|
|
59
60
|
const base = words[0].split(/[\\/]/).pop();
|
|
@@ -65,8 +66,30 @@ export function pickEditor({ env = process.env, platform = process.platform, has
|
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The flags and the "how do I save" line for one editor.
|
|
71
|
+
*
|
|
72
|
+
* READ-ONLY (#322, /open-presets with no name — the current setup, to copy to another computer):
|
|
73
|
+
* nano -v (view mode), vim/vi -R. And for nano, SOFT-WRAP (-$): nano otherwise shows a long line cut
|
|
74
|
+
* off at the screen edge, and a key copied from an 80-column SSH terminal would come out SHORTER —
|
|
75
|
+
* a key that looks fine and is wrong. (Not macOS's /usr/bin/nano, which is pico and has no -$; the
|
|
76
|
+
* line breaks a soft-wrapped copy carries are taken out again by parseEdited.)
|
|
77
|
+
*/
|
|
78
|
+
function editorHints(ed, { readOnly = false, platform = process.platform } = {}) {
|
|
69
79
|
const base = ed.cmd.split(/[\\/]/).pop().replace(/\.exe$/i, "").toLowerCase();
|
|
80
|
+
if (readOnly) {
|
|
81
|
+
let args = ed.args;
|
|
82
|
+
if (base === "nano") {
|
|
83
|
+
if (!args.includes("-v") && !args.includes("--view")) args = ["-v", ...args];
|
|
84
|
+
if (platform !== "darwin" && !args.some((a) => a === "-$" || a === "--softwrap")) args = ["-$", ...args];
|
|
85
|
+
} else if (/^(vim?|nvim|gvim)$/.test(base) && !args.includes("-R")) args = ["-R", ...args];
|
|
86
|
+
const closeHint =
|
|
87
|
+
base === "nano" ? "nano: Ctrl+X to close"
|
|
88
|
+
: /^(vim?|nvim|gvim)$/.test(base) ? "vi: :q Enter to close"
|
|
89
|
+
: base === "notepad" ? "Notepad: close the window (nothing you change is kept)"
|
|
90
|
+
: "quit the editor (nothing you change is kept)";
|
|
91
|
+
return { ...ed, args, saveHint: closeHint, readOnly: true };
|
|
92
|
+
}
|
|
70
93
|
if (base === "nano" && !ed.args.some((a) => /^(-w|--nowrap|-b|--breaklonglines)$/.test(a))) ed = { ...ed, args: ["-w", ...ed.args] };
|
|
71
94
|
const saveHint =
|
|
72
95
|
base === "nano" ? "nano: Ctrl+O, Enter, Ctrl+X to save · Ctrl+X, N to cancel"
|
|
@@ -77,6 +100,47 @@ function withHints(ed) {
|
|
|
77
100
|
return { ...ed, saveHint };
|
|
78
101
|
}
|
|
79
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Take raw line breaks out of JSON STRINGS (never out of the structure between them). Only strings
|
|
105
|
+
* are touched, and a valid JSON string cannot contain a raw line break — so valid JSON is unchanged.
|
|
106
|
+
*/
|
|
107
|
+
export function joinBrokenStrings(src) {
|
|
108
|
+
let out = "";
|
|
109
|
+
let inStr = false;
|
|
110
|
+
for (let i = 0; i < src.length; i++) {
|
|
111
|
+
const c = src[i];
|
|
112
|
+
if (inStr) {
|
|
113
|
+
if (c === "\\") {
|
|
114
|
+
out += c + (src[i + 1] ?? "");
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === "\r" || c === "\n") continue;
|
|
119
|
+
if (c === '"') inStr = false;
|
|
120
|
+
} else if (c === '"') inStr = true;
|
|
121
|
+
out += c;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* /open-presets with no name (#322): THE CURRENT SETUP, read-only, to copy to another computer and
|
|
128
|
+
* paste into /save-preset there. The full keys are in it — that is the point — so the header says so.
|
|
129
|
+
*/
|
|
130
|
+
export function currentSetupText(doc) {
|
|
131
|
+
const { note: _n, savedAt: _a, savedOn: _o, ...body } = doc ?? {};
|
|
132
|
+
return (
|
|
133
|
+
[
|
|
134
|
+
"// Your gu setup NOW — read-only. Copy ALL of it (keys included) and on the",
|
|
135
|
+
"// other computer run /save-preset there, paste it, save and quit.",
|
|
136
|
+
"// This CONTAINS your API keys — paste it only somewhere private.",
|
|
137
|
+
].join("\n") +
|
|
138
|
+
"\n" +
|
|
139
|
+
JSON.stringify({ ...body, gu: "preset", version: FULL_PRESET_VERSION }, null, 2) +
|
|
140
|
+
"\n"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
80
144
|
/** What to say when a server has no editor at all. */
|
|
81
145
|
export const NO_EDITOR_HINT = "no editor found — install one (Ubuntu: sudo apt install nano) or set $EDITOR";
|
|
82
146
|
|
|
@@ -122,9 +186,11 @@ export function editorText(name, preset = {}) {
|
|
|
122
186
|
const { note: _n, savedAt: _a, savedOn: _o, ...body } = preset;
|
|
123
187
|
return (
|
|
124
188
|
[
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"//
|
|
189
|
+
// each line fits an 80-column SSH terminal (#322)
|
|
190
|
+
`// gu preset "${name}" — servers, models, API keys, coder, agent, embedder.`,
|
|
191
|
+
"// Edit anything, then save and quit to keep it (it is checked first).",
|
|
192
|
+
"// Quit without saving to leave it as it is. Lines starting // are ignored.",
|
|
193
|
+
"// This CONTAINS your API keys.",
|
|
128
194
|
].join("\n") +
|
|
129
195
|
"\n" +
|
|
130
196
|
JSON.stringify({ ...body, gu: "preset", version: FULL_PRESET_VERSION, name }, null, 2) +
|
|
@@ -221,11 +287,23 @@ export function jsonErrorIndex(src) {
|
|
|
221
287
|
* blanked (not removed) so a JSON error's line number is the line the person sees in the editor.
|
|
222
288
|
*/
|
|
223
289
|
export function parseEdited(text) {
|
|
224
|
-
|
|
290
|
+
let src = String(text ?? "").replace(/^[ \t]*\/\/.*$/gm, "");
|
|
225
291
|
if (!src.trim()) return { ok: false, empty: true, problem: "the file is empty." };
|
|
226
292
|
try {
|
|
227
293
|
JSON.parse(src);
|
|
228
294
|
} catch (e) {
|
|
295
|
+
// A copy from a soft-wrapping terminal or editor breaks a long line — a key — in two (#322). A
|
|
296
|
+
// JSON string can never hold a raw line break, so taking them out of strings changes nothing
|
|
297
|
+
// that was valid; it is only tried when the text does not parse as it is.
|
|
298
|
+
const joined = joinBrokenStrings(src);
|
|
299
|
+
if (joined !== src) {
|
|
300
|
+
try {
|
|
301
|
+
JSON.parse(joined);
|
|
302
|
+
return parseEdited(joined);
|
|
303
|
+
} catch {
|
|
304
|
+
/* still broken — report the ORIGINAL text's mistake, at the line the person sees */
|
|
305
|
+
}
|
|
306
|
+
}
|
|
229
307
|
// Node names the line only for SOME errors ("Expected ':' …"); a stray comma comes back as just a
|
|
230
308
|
// snippet. So when it does not say, find the spot ourselves — the person fixes a LINE in an editor.
|
|
231
309
|
let at = /line (\d+) column (\d+)/.exec(String(e?.message ?? ""));
|
|
@@ -301,7 +379,7 @@ export function presetCopyJson(preset) {
|
|
|
301
379
|
}
|
|
302
380
|
|
|
303
381
|
/**
|
|
304
|
-
* The framed block
|
|
382
|
+
* The framed block a preset view prints for "view": the key masked, "⧉ copy" in the top border.
|
|
305
383
|
* Every row fits `width - 1` so none wraps (a wrapped row breaks the click arithmetic, #109).
|
|
306
384
|
* → { rows, button: { from, to } } — the button's column span on row 0, 0-based, gutter included.
|
|
307
385
|
*/
|
|
@@ -325,10 +403,10 @@ export function presetBlock(name, preset, { width = 80, gutter = " " } = {}) {
|
|
|
325
403
|
};
|
|
326
404
|
}
|
|
327
405
|
|
|
328
|
-
/** The keys the /
|
|
329
|
-
export const PICKER_KEYS = {
|
|
406
|
+
/** The keys the /use-presets picker takes, in the order its hint line names them. */
|
|
407
|
+
export const PICKER_KEYS = { o: "open" };
|
|
330
408
|
// Short enough for one 80-column row with " Presets in ~ (…):" around it (#322 — it wrapped).
|
|
331
|
-
export const PICKER_HINT = "Enter use
|
|
409
|
+
export const PICKER_HINT = "Enter use it · o open in the editor · Esc";
|
|
332
410
|
|
|
333
411
|
/**
|
|
334
412
|
* The file the editor opens for "paste a preset" (#322: "load preset doesn't give me any editor so
|
|
@@ -347,7 +425,7 @@ export function pasteTemplate(saveHint = "save, then quit the editor") {
|
|
|
347
425
|
}
|
|
348
426
|
|
|
349
427
|
/**
|
|
350
|
-
* A PRESET SOMEONE ALREADY HAS, pasted into /
|
|
428
|
+
* A PRESET SOMEONE ALREADY HAS, pasted into /save-preset (#317: "I need a place to input a preset
|
|
351
429
|
* I have already, which is in JSON format"). Three shapes are accepted, because those are the three
|
|
352
430
|
* things a person could be holding:
|
|
353
431
|
*
|
package/presets-transfer.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MODEL PRESETS — the files /save-preset writes and /
|
|
2
|
+
* MODEL PRESETS — the files /save-preset writes and /use-presets reads (#6).
|
|
3
3
|
*
|
|
4
4
|
* HOW A PRESET DIFFERS FROM A SETTINGS FILE. /save-settings asks nothing: it writes the whole
|
|
5
5
|
* account, names the keys, and refuses to carry a single secret. /save-preset is the opposite
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* preset's name, bound to that row, and made the row's DEFAULT key so the resolver finds it.
|
|
16
16
|
*
|
|
17
17
|
* THE MARKER. A preset file carries "gu": "preset" (not "settings") so /load-settings never
|
|
18
|
-
* offers it and /
|
|
18
|
+
* offers it and /use-presets never offers a settings file — two commands, two folders' worth of
|
|
19
19
|
* files, no cross-picking.
|
|
20
20
|
*/
|
|
21
21
|
|
|
@@ -56,7 +56,7 @@ export function presetFilePath(typed, home) {
|
|
|
56
56
|
const withJson = (p) => (extname(basename(p)) ? p : `${p}.json`);
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* The preset files in the default folder, newest first — what /
|
|
59
|
+
* The preset files in the default folder, newest first — what /use-presets offers. Only files
|
|
60
60
|
* that ARE preset files (the "gu": "preset" marker). `entries` = [{ name, text, mtimeMs }], and
|
|
61
61
|
* each name is returned as just the preset name (the .json comes off — it is the name the user
|
|
62
62
|
* typed when saving).
|
|
@@ -198,7 +198,7 @@ export function presetToSettingsPatch(preset, current = {}) {
|
|
|
198
198
|
};
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/** The lines /
|
|
201
|
+
/** The lines /use-presets prints before it asks — what applying the preset will do. */
|
|
202
202
|
export function presetPlanLines(name, preset, current = {}) {
|
|
203
203
|
const same = (a, b) => clean(a).toLowerCase() === clean(b).toLowerCase();
|
|
204
204
|
const lines = [];
|
|
@@ -409,7 +409,7 @@ export function maskedFullPreset(doc) {
|
|
|
409
409
|
return { ...doc, keys: (Array.isArray(doc?.keys) ? doc.keys : []).map((k) => ({ ...k, ...(k.key ? { key: mask(k.key) } : {}) })) };
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
-
/** The lines /
|
|
412
|
+
/** The lines /use-presets prints before applying a version-2 preset (keys masked). */
|
|
413
413
|
export function fullPresetSummary(v, codingDefault) {
|
|
414
414
|
const lines = [];
|
|
415
415
|
for (const b of v.backends) {
|