@bpmnkit/cli 0.0.9
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 +113 -0
- package/dist/args.js +101 -0
- package/dist/client.js +19 -0
- package/dist/color.js +50 -0
- package/dist/commands/admin-shared.js +180 -0
- package/dist/commands/bpmn.js +143 -0
- package/dist/commands/completion.js +52 -0
- package/dist/commands/connector.js +172 -0
- package/dist/commands/index.js +47 -0
- package/dist/commands/profile.js +325 -0
- package/dist/commands/relations.js +28 -0
- package/dist/commands/settings.js +138 -0
- package/dist/commands/shared.js +196 -0
- package/dist/commands/worker.js +135 -0
- package/dist/completion.js +89 -0
- package/dist/generated/admin-commands.js +407 -0
- package/dist/generated/commands.js +2101 -0
- package/dist/help.js +117 -0
- package/dist/index.js +4 -0
- package/dist/output.js +262 -0
- package/dist/profile-tui.js +229 -0
- package/dist/profile.js +103 -0
- package/dist/run.js +210 -0
- package/dist/settings-tui.js +195 -0
- package/dist/tui.js +2544 -0
- package/dist/types.js +2 -0
- package/package.json +32 -0
package/dist/profile.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// ─── Config directory ─────────────────────────────────────────────────────────
|
|
5
|
+
function configDir() {
|
|
6
|
+
const p = platform();
|
|
7
|
+
if (p === "win32")
|
|
8
|
+
return join(process.env.APPDATA ?? homedir(), "casen");
|
|
9
|
+
if (p === "darwin")
|
|
10
|
+
return join(homedir(), "Library", "Application Support", "casen");
|
|
11
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "casen");
|
|
12
|
+
}
|
|
13
|
+
function configFilePath() {
|
|
14
|
+
return join(configDir(), "config.json");
|
|
15
|
+
}
|
|
16
|
+
// ─── Read / write ─────────────────────────────────────────────────────────────
|
|
17
|
+
function readStore() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(configFilePath(), "utf8");
|
|
20
|
+
const store = JSON.parse(raw);
|
|
21
|
+
if (!store.meta)
|
|
22
|
+
store.meta = {};
|
|
23
|
+
return store;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { profiles: {}, active: null, meta: {} };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function writeStore(store) {
|
|
30
|
+
const dir = configDir();
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
writeFileSync(configFilePath(), JSON.stringify(store, null, 2), "utf8");
|
|
33
|
+
}
|
|
34
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
35
|
+
export function listProfiles() {
|
|
36
|
+
const store = readStore();
|
|
37
|
+
return Object.entries(store.profiles).map(([name, config]) => ({
|
|
38
|
+
name,
|
|
39
|
+
apiType: store.meta[name]?.apiType ?? "c8",
|
|
40
|
+
config,
|
|
41
|
+
createdAt: store.meta[name]?.createdAt ?? null,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
export function getProfile(name) {
|
|
45
|
+
const store = readStore();
|
|
46
|
+
const config = store.profiles[name];
|
|
47
|
+
if (!config)
|
|
48
|
+
return undefined;
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
apiType: store.meta[name]?.apiType ?? "c8",
|
|
52
|
+
config,
|
|
53
|
+
createdAt: store.meta[name]?.createdAt ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function getActiveProfile() {
|
|
57
|
+
const store = readStore();
|
|
58
|
+
if (!store.active)
|
|
59
|
+
return undefined;
|
|
60
|
+
return getProfile(store.active);
|
|
61
|
+
}
|
|
62
|
+
export function getActiveName() {
|
|
63
|
+
return readStore().active;
|
|
64
|
+
}
|
|
65
|
+
export function saveProfile(name, config, apiType = "c8") {
|
|
66
|
+
const store = readStore();
|
|
67
|
+
store.profiles[name] = config;
|
|
68
|
+
if (!store.meta[name]) {
|
|
69
|
+
store.meta[name] = { createdAt: new Date().toISOString(), apiType };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
store.meta[name].apiType = apiType;
|
|
73
|
+
}
|
|
74
|
+
// Auto-activate if this is the first profile
|
|
75
|
+
if (store.active === null)
|
|
76
|
+
store.active = name;
|
|
77
|
+
writeStore(store);
|
|
78
|
+
}
|
|
79
|
+
export function deleteProfile(name) {
|
|
80
|
+
const store = readStore();
|
|
81
|
+
if (!(name in store.profiles))
|
|
82
|
+
return false;
|
|
83
|
+
const { [name]: _removed, ...rest } = store.profiles;
|
|
84
|
+
store.profiles = rest;
|
|
85
|
+
if (store.active === name) {
|
|
86
|
+
const remaining = Object.keys(store.profiles);
|
|
87
|
+
store.active = remaining.length > 0 ? (remaining[0] ?? null) : null;
|
|
88
|
+
}
|
|
89
|
+
writeStore(store);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
export function useProfile(name) {
|
|
93
|
+
const store = readStore();
|
|
94
|
+
if (!(name in store.profiles))
|
|
95
|
+
return false;
|
|
96
|
+
store.active = name;
|
|
97
|
+
writeStore(store);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
export function getConfigFilePath() {
|
|
101
|
+
return configFilePath();
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=profile.js.map
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { appendAuditEntry, createAdminClientFromProfile, createClientFromProfile, getActiveName, getProfile, } from "@bpmnkit/profiles";
|
|
2
|
+
import { parseArgs } from "./args.js";
|
|
3
|
+
import { commandGroups } from "./commands/index.js";
|
|
4
|
+
import { getRuntimeCompletions } from "./completion.js";
|
|
5
|
+
import { printCommandHelp, printGlobalHelp, printGroupHelp, printVersion } from "./help.js";
|
|
6
|
+
import { createNullWriter, createOutputWriter, printRawResponse } from "./output.js";
|
|
7
|
+
import { runProfileManager } from "./profile-tui.js";
|
|
8
|
+
import { runSettingsManager } from "./settings-tui.js";
|
|
9
|
+
import { runGroupTui, runMainTui } from "./tui.js";
|
|
10
|
+
// ─── Profile info ─────────────────────────────────────────────────────────────
|
|
11
|
+
function buildProfileInfo(profileName) {
|
|
12
|
+
const effectiveName = profileName ?? getActiveName() ?? "none";
|
|
13
|
+
const p = profileName ? getProfile(profileName) : getProfile(effectiveName);
|
|
14
|
+
if (!p)
|
|
15
|
+
return { name: effectiveName, info: [{ key: "status", value: "profile not found" }] };
|
|
16
|
+
const info = [
|
|
17
|
+
{ key: "name", value: p.name },
|
|
18
|
+
{ key: "apiType", value: p.apiType },
|
|
19
|
+
{ key: "baseUrl", value: p.config.baseUrl ?? "(default)" },
|
|
20
|
+
{ key: "createdAt", value: p.createdAt ?? "unknown" },
|
|
21
|
+
];
|
|
22
|
+
const auth = p.config.auth;
|
|
23
|
+
if (auth) {
|
|
24
|
+
info.push({ key: "auth.type", value: auth.type });
|
|
25
|
+
if (auth.type === "bearer") {
|
|
26
|
+
info.push({ key: "auth.token", value: "***" });
|
|
27
|
+
}
|
|
28
|
+
else if (auth.type === "oauth2") {
|
|
29
|
+
info.push({ key: "auth.clientId", value: auth.clientId });
|
|
30
|
+
info.push({ key: "auth.clientSecret", value: "***" });
|
|
31
|
+
info.push({ key: "auth.tokenUrl", value: auth.tokenUrl });
|
|
32
|
+
if (auth.scope)
|
|
33
|
+
info.push({ key: "auth.scope", value: auth.scope });
|
|
34
|
+
}
|
|
35
|
+
else if (auth.type === "basic") {
|
|
36
|
+
info.push({ key: "auth.username", value: auth.username });
|
|
37
|
+
info.push({ key: "auth.password", value: "***" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { name: effectiveName, info };
|
|
41
|
+
}
|
|
42
|
+
// ─── Error display ────────────────────────────────────────────────────────────
|
|
43
|
+
function printError(msg, colors) {
|
|
44
|
+
const red = colors ? "\x1b[31m" : "";
|
|
45
|
+
const reset = colors ? "\x1b[0m" : "";
|
|
46
|
+
process.stderr.write(`${red}error${reset}: ${msg}\n`);
|
|
47
|
+
}
|
|
48
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
49
|
+
export async function run(argv) {
|
|
50
|
+
// ── Completion protocol ───────────────────────────────────────────────────
|
|
51
|
+
// casen --complete <cursorWordIndex> -- <words...>
|
|
52
|
+
const completeIdx = argv.indexOf("--complete");
|
|
53
|
+
if (completeIdx !== -1) {
|
|
54
|
+
const cursorIdx = Number(argv[completeIdx + 1] ?? "0");
|
|
55
|
+
const dashDash = argv.indexOf("--", completeIdx + 2);
|
|
56
|
+
const words = dashDash >= 0 ? argv.slice(dashDash + 1) : [];
|
|
57
|
+
const suggestions = getRuntimeCompletions(commandGroups, cursorIdx, words);
|
|
58
|
+
process.stdout.write(`${suggestions.join("\n")}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// ── Global parse ──────────────────────────────────────────────────────────
|
|
62
|
+
const { positional, flags } = parseArgs(argv);
|
|
63
|
+
const noColor = flags["no-color"] === true;
|
|
64
|
+
const colors = !noColor && process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
65
|
+
const wantVersion = flags.version === true || flags.v === true;
|
|
66
|
+
const wantHelp = flags.help === true || flags.h === true;
|
|
67
|
+
if (wantVersion) {
|
|
68
|
+
printVersion();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const outputFormat = (flags.output ?? flags.o ?? "table");
|
|
72
|
+
const profileName = flags.profile ?? flags.p;
|
|
73
|
+
// ── Top-level: main menu TUI or help ─────────────────────────────────────
|
|
74
|
+
if (positional.length === 0) {
|
|
75
|
+
if (wantHelp) {
|
|
76
|
+
printGlobalHelp(commandGroups, colors);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const { name: pName, info: pInfo } = buildProfileInfo(profileName);
|
|
80
|
+
await runMainTui(commandGroups, () => Promise.resolve(createClientFromProfile(profileName)), () => Promise.resolve(createAdminClientFromProfile(profileName)), { profile: pName, profileInfo: pInfo });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const getClient = () => Promise.resolve(createClientFromProfile(profileName));
|
|
85
|
+
const getAdminClient = () => Promise.resolve(createAdminClientFromProfile(profileName));
|
|
86
|
+
// ── Find group ────────────────────────────────────────────────────────────
|
|
87
|
+
const groupToken = positional[0] ?? "";
|
|
88
|
+
const group = commandGroups.find((g) => g.name === groupToken || g.aliases?.includes(groupToken));
|
|
89
|
+
if (!group) {
|
|
90
|
+
printError(`Unknown resource: "${groupToken}". Run \`casen --help\` to see all resources.`, colors);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// ── TUI (no subcommand, no --help) ───────────────────────────────────────
|
|
95
|
+
if (positional.length === 1 && !wantHelp) {
|
|
96
|
+
if (group.name === "profile") {
|
|
97
|
+
await runProfileManager();
|
|
98
|
+
}
|
|
99
|
+
else if (group.name === "settings") {
|
|
100
|
+
await runSettingsManager();
|
|
101
|
+
}
|
|
102
|
+
else if (group.name !== "completion") {
|
|
103
|
+
const { name: pName, info: pInfo } = buildProfileInfo(profileName);
|
|
104
|
+
await runGroupTui(group, commandGroups, getClient, getAdminClient, {
|
|
105
|
+
profile: pName,
|
|
106
|
+
profileInfo: pInfo,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
printGroupHelp(group, colors);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// ── Group-level help ──────────────────────────────────────────────────────
|
|
115
|
+
if (positional.length === 1 || (wantHelp && positional.length === 1)) {
|
|
116
|
+
printGroupHelp(group, colors);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// ── Find command ──────────────────────────────────────────────────────────
|
|
120
|
+
const cmdToken = positional[1] ?? "";
|
|
121
|
+
const cmd = group.commands.find((c) => c.name === cmdToken || c.aliases?.includes(cmdToken));
|
|
122
|
+
if (!cmd) {
|
|
123
|
+
printError(`Unknown command: "${group.name} ${cmdToken}". Run \`casen ${group.name} --help\` to see available commands.`, colors);
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// ── Command-level help ────────────────────────────────────────────────────
|
|
128
|
+
if (wantHelp) {
|
|
129
|
+
printCommandHelp(group, cmd, colors);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// ── Execute ───────────────────────────────────────────────────────────────
|
|
133
|
+
const isRaw = flags.raw === true;
|
|
134
|
+
const effectiveProfile = profileName ?? getActiveName() ?? "default";
|
|
135
|
+
// Redact secret-looking flag values before storing in audit log
|
|
136
|
+
const SECRET_FLAG_RE = /secret|password|token/i;
|
|
137
|
+
const auditFlags = {};
|
|
138
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
139
|
+
auditFlags[k] = SECRET_FLAG_RE.test(k) ? "***" : v;
|
|
140
|
+
}
|
|
141
|
+
const auditPositional = positional.slice(2);
|
|
142
|
+
// Wrap client factories to capture the last raw HTTP response
|
|
143
|
+
// Typed as array to prevent TypeScript's control-flow narrowing from collapsing to never
|
|
144
|
+
const rawCaptureRef = [null];
|
|
145
|
+
const instrumentedGetClient = () => {
|
|
146
|
+
const client = createClientFromProfile(profileName);
|
|
147
|
+
client.on("rawResponse", (evt) => {
|
|
148
|
+
rawCaptureRef[0] = evt;
|
|
149
|
+
});
|
|
150
|
+
return Promise.resolve(client);
|
|
151
|
+
};
|
|
152
|
+
const instrumentedGetAdminClient = () => {
|
|
153
|
+
const client = createAdminClientFromProfile(profileName);
|
|
154
|
+
client.on("rawResponse", (evt) => {
|
|
155
|
+
rawCaptureRef[0] = evt;
|
|
156
|
+
});
|
|
157
|
+
return Promise.resolve(client);
|
|
158
|
+
};
|
|
159
|
+
const output = isRaw ? createNullWriter() : createOutputWriter(outputFormat, noColor);
|
|
160
|
+
const ctx = {
|
|
161
|
+
positional: auditPositional,
|
|
162
|
+
flags,
|
|
163
|
+
output,
|
|
164
|
+
getClient: instrumentedGetClient,
|
|
165
|
+
getAdminClient: instrumentedGetAdminClient,
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
await cmd.run(ctx);
|
|
169
|
+
appendAuditEntry(effectiveProfile, {
|
|
170
|
+
group: group.name,
|
|
171
|
+
command: cmd.name,
|
|
172
|
+
positional: auditPositional,
|
|
173
|
+
flags: auditFlags,
|
|
174
|
+
status: "ok",
|
|
175
|
+
});
|
|
176
|
+
const capture = rawCaptureRef[0];
|
|
177
|
+
if (isRaw && capture) {
|
|
178
|
+
printRawResponse(capture, noColor);
|
|
179
|
+
}
|
|
180
|
+
else if (capture) {
|
|
181
|
+
// Always show status code in normal mode
|
|
182
|
+
const statusFn = capture.status >= 200 && capture.status < 300
|
|
183
|
+
? (s) => `\x1b[32m${s}\x1b[39m`
|
|
184
|
+
: (s) => `\x1b[31m${s}\x1b[39m`;
|
|
185
|
+
const statusStr = colors ? statusFn(`HTTP ${capture.status}`) : `HTTP ${capture.status}`;
|
|
186
|
+
process.stdout.write(`\n${statusStr}\n`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const capture = rawCaptureRef[0];
|
|
191
|
+
if (isRaw && capture) {
|
|
192
|
+
printRawResponse(capture, noColor);
|
|
193
|
+
}
|
|
194
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
195
|
+
appendAuditEntry(effectiveProfile, {
|
|
196
|
+
group: group.name,
|
|
197
|
+
command: cmd.name,
|
|
198
|
+
positional: auditPositional,
|
|
199
|
+
flags: auditFlags,
|
|
200
|
+
status: "error",
|
|
201
|
+
error: msg,
|
|
202
|
+
});
|
|
203
|
+
printError(msg, colors);
|
|
204
|
+
if (flags.debug) {
|
|
205
|
+
process.stderr.write(`\n${String(err)}\n`);
|
|
206
|
+
}
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { getSettings, saveSettings } from "@bpmnkit/profiles";
|
|
2
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
3
|
+
const CSI = "\x1b[";
|
|
4
|
+
const HIDE_CURSOR = `${CSI}?25l`;
|
|
5
|
+
const SHOW_CURSOR = `${CSI}?25h`;
|
|
6
|
+
const ALT_ON = `${CSI}?1049h`;
|
|
7
|
+
const ALT_OFF = `${CSI}?1049l`;
|
|
8
|
+
const CLEAR = `${CSI}2J${CSI}H`;
|
|
9
|
+
function inv(s) {
|
|
10
|
+
return `${CSI}7m${s}${CSI}m`;
|
|
11
|
+
}
|
|
12
|
+
function bold(s) {
|
|
13
|
+
return `${CSI}1m${s}${CSI}m`;
|
|
14
|
+
}
|
|
15
|
+
function dim(s) {
|
|
16
|
+
return `${CSI}2m${s}${CSI}m`;
|
|
17
|
+
}
|
|
18
|
+
function green(s) {
|
|
19
|
+
return `${CSI}32m${s}${CSI}m`;
|
|
20
|
+
}
|
|
21
|
+
function red(s) {
|
|
22
|
+
return `${CSI}31m${s}${CSI}m`;
|
|
23
|
+
}
|
|
24
|
+
function cyan(s) {
|
|
25
|
+
return `${CSI}36m${s}${CSI}m`;
|
|
26
|
+
}
|
|
27
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: needed for ANSI stripping
|
|
28
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
29
|
+
function vlen(s) {
|
|
30
|
+
return s.replace(ANSI_RE, "").length;
|
|
31
|
+
}
|
|
32
|
+
function padEnd(s, n) {
|
|
33
|
+
const v = vlen(s);
|
|
34
|
+
return v < n ? s + " ".repeat(n - v) : s;
|
|
35
|
+
}
|
|
36
|
+
const SETTING_ROWS = [
|
|
37
|
+
{
|
|
38
|
+
key: "auditLogSize",
|
|
39
|
+
label: "audit-log-size",
|
|
40
|
+
description: "Number of actions to keep in the audit log per profile (0 = disabled)",
|
|
41
|
+
type: "number",
|
|
42
|
+
min: 0,
|
|
43
|
+
max: 1000,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
function loadSettings() {
|
|
47
|
+
return getSettings();
|
|
48
|
+
}
|
|
49
|
+
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
50
|
+
function render(state) {
|
|
51
|
+
const termCols = process.stdout.columns ?? 80;
|
|
52
|
+
const labelW = SETTING_ROWS.reduce((m, r) => Math.max(m, r.label.length), 0) + 4;
|
|
53
|
+
const out = [];
|
|
54
|
+
out.push("");
|
|
55
|
+
out.push(` ${bold("casen — Settings")}`);
|
|
56
|
+
out.push("");
|
|
57
|
+
out.push(` ${dim(padEnd("SETTING", labelW))} ${dim("VALUE")}`);
|
|
58
|
+
out.push(dim(` ${"─".repeat(termCols - 4)}`));
|
|
59
|
+
for (let i = 0; i < SETTING_ROWS.length; i++) {
|
|
60
|
+
const row = SETTING_ROWS[i];
|
|
61
|
+
if (!row)
|
|
62
|
+
continue;
|
|
63
|
+
const isCursor = i === state.cursor;
|
|
64
|
+
const rawValue = state.settings[row.key];
|
|
65
|
+
const valueStr = state.editing && isCursor ? `${cyan(`[${state.editValue}_]`)}` : String(rawValue);
|
|
66
|
+
const labelPart = padEnd(isCursor ? cyan(row.label) : row.label, labelW);
|
|
67
|
+
const content = ` ${labelPart} ${valueStr}`;
|
|
68
|
+
const padded = content + " ".repeat(Math.max(0, termCols - vlen(content) - 1));
|
|
69
|
+
out.push(isCursor ? inv(padded) : padded);
|
|
70
|
+
}
|
|
71
|
+
out.push("");
|
|
72
|
+
const selectedRow = SETTING_ROWS[state.cursor];
|
|
73
|
+
if (selectedRow) {
|
|
74
|
+
out.push(` ${dim(selectedRow.description)}`);
|
|
75
|
+
}
|
|
76
|
+
out.push("");
|
|
77
|
+
if (state.message) {
|
|
78
|
+
out.push(` ${state.message}`);
|
|
79
|
+
}
|
|
80
|
+
else if (state.editing) {
|
|
81
|
+
out.push(` ${dim("type")} new value ${cyan("enter")} save ${cyan("esc")} cancel ${dim("bksp")} delete`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
out.push(` ${dim("↑↓")} navigate ${cyan("enter")} edit ${cyan("q")} quit`);
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`${CLEAR}${out.join("\n")}\n`);
|
|
87
|
+
}
|
|
88
|
+
// ─── Key handling ─────────────────────────────────────────────────────────────
|
|
89
|
+
function handleKey(key, state, done) {
|
|
90
|
+
state.message = "";
|
|
91
|
+
if (state.editing) {
|
|
92
|
+
if (key === "\r" || key === "\n") {
|
|
93
|
+
// Commit edit
|
|
94
|
+
const row = SETTING_ROWS[state.cursor];
|
|
95
|
+
if (row) {
|
|
96
|
+
const n = Number(state.editValue);
|
|
97
|
+
const min = row.min ?? 0;
|
|
98
|
+
const max = row.max ?? Number.MAX_SAFE_INTEGER;
|
|
99
|
+
if (!Number.isNaN(n) && n >= min && n <= max) {
|
|
100
|
+
const updated = {};
|
|
101
|
+
updated[row.key] = n;
|
|
102
|
+
saveSettings(updated);
|
|
103
|
+
state.settings = loadSettings();
|
|
104
|
+
state.message = green(`✓ Saved: ${row.label} = ${n}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
state.message = red(`Invalid value — must be a number between ${min} and ${max}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
state.editing = false;
|
|
111
|
+
state.editValue = "";
|
|
112
|
+
}
|
|
113
|
+
else if (key === "\x1b") {
|
|
114
|
+
// Cancel
|
|
115
|
+
state.editing = false;
|
|
116
|
+
state.editValue = "";
|
|
117
|
+
}
|
|
118
|
+
else if (key === "\x7f" || key === "\x08") {
|
|
119
|
+
// Backspace
|
|
120
|
+
state.editValue = state.editValue.slice(0, -1);
|
|
121
|
+
}
|
|
122
|
+
else if (key >= "0" && key <= "9") {
|
|
123
|
+
state.editValue += key;
|
|
124
|
+
}
|
|
125
|
+
render(state);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
switch (key) {
|
|
129
|
+
case "\x1b[A": // up
|
|
130
|
+
if (state.cursor > 0)
|
|
131
|
+
state.cursor--;
|
|
132
|
+
break;
|
|
133
|
+
case "\x1b[B": // down
|
|
134
|
+
if (state.cursor < SETTING_ROWS.length - 1)
|
|
135
|
+
state.cursor++;
|
|
136
|
+
break;
|
|
137
|
+
case "\r":
|
|
138
|
+
case "\n": {
|
|
139
|
+
// Start editing
|
|
140
|
+
const row = SETTING_ROWS[state.cursor];
|
|
141
|
+
if (row) {
|
|
142
|
+
state.editing = true;
|
|
143
|
+
state.editValue = String(state.settings[row.key]);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "q":
|
|
148
|
+
case "Q":
|
|
149
|
+
case "\x03": // Ctrl+C
|
|
150
|
+
case "\x1b": // ESC
|
|
151
|
+
done();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
render(state);
|
|
155
|
+
}
|
|
156
|
+
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
157
|
+
export async function runSettingsManager() {
|
|
158
|
+
// Non-interactive fallback
|
|
159
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
160
|
+
const settings = loadSettings();
|
|
161
|
+
for (const row of SETTING_ROWS) {
|
|
162
|
+
process.stdout.write(`${row.label}: ${String(settings[row.key])}\n`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(ALT_ON + HIDE_CURSOR);
|
|
167
|
+
let cleaned = false;
|
|
168
|
+
const cleanup = () => {
|
|
169
|
+
if (cleaned)
|
|
170
|
+
return;
|
|
171
|
+
cleaned = true;
|
|
172
|
+
process.stdout.write(ALT_OFF + SHOW_CURSOR);
|
|
173
|
+
if (process.stdin.isTTY)
|
|
174
|
+
process.stdin.setRawMode(false);
|
|
175
|
+
process.stdin.pause();
|
|
176
|
+
};
|
|
177
|
+
process.on("exit", cleanup);
|
|
178
|
+
const state = {
|
|
179
|
+
settings: loadSettings(),
|
|
180
|
+
cursor: 0,
|
|
181
|
+
editing: false,
|
|
182
|
+
editValue: "",
|
|
183
|
+
message: "",
|
|
184
|
+
};
|
|
185
|
+
render(state);
|
|
186
|
+
await new Promise((resolve) => {
|
|
187
|
+
process.stdin.setRawMode(true);
|
|
188
|
+
process.stdin.resume();
|
|
189
|
+
process.stdin.setEncoding("utf8");
|
|
190
|
+
process.stdin.on("data", (key) => handleKey(key, state, resolve));
|
|
191
|
+
});
|
|
192
|
+
cleanup();
|
|
193
|
+
process.removeListener("exit", cleanup);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=settings-tui.js.map
|