@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.
@@ -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