airlock-bot 0.2.15 → 0.2.17
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/dist/configure-cli/cli.d.ts +2 -0
- package/dist/configure-cli/cli.d.ts.map +1 -0
- package/dist/configure-cli/cli.js +868 -0
- package/dist/configure-cli/cli.js.map +1 -0
- package/dist/discover/cli.js +1 -1
- package/dist/discover/index.d.ts +6 -0
- package/dist/discover/index.d.ts.map +1 -1
- package/dist/discover/index.js +38 -26
- package/dist/discover/index.js.map +1 -1
- package/dist/discover/strategies/completion.d.ts +39 -0
- package/dist/discover/strategies/completion.d.ts.map +1 -0
- package/dist/discover/strategies/completion.js +481 -0
- package/dist/discover/strategies/completion.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { parseArgs } from 'util';
|
|
2
|
+
import { writeFileSync, copyFileSync, readFileSync, openSync } from 'fs';
|
|
3
|
+
import { ReadStream } from 'tty';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
6
|
+
import { discoverCliCommands } from '../discover/index.js';
|
|
7
|
+
import { createCompletionSession } from '../discover/strategies/completion.js';
|
|
8
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
9
|
+
const ESC = '\x1b[';
|
|
10
|
+
const RESET = `${ESC}0m`;
|
|
11
|
+
const BOLD = `${ESC}1m`;
|
|
12
|
+
const DIM = `${ESC}2m`;
|
|
13
|
+
const GREEN = `${ESC}32m`;
|
|
14
|
+
const YELLOW = `${ESC}33m`;
|
|
15
|
+
const RED = `${ESC}31m`;
|
|
16
|
+
const CYAN = `${ESC}36m`;
|
|
17
|
+
const BLUE = `${ESC}34m`;
|
|
18
|
+
function visibleLength(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
21
|
+
}
|
|
22
|
+
function terminalRows(line, termCols) {
|
|
23
|
+
const len = visibleLength(line);
|
|
24
|
+
if (len === 0)
|
|
25
|
+
return 1;
|
|
26
|
+
return Math.ceil(len / termCols);
|
|
27
|
+
}
|
|
28
|
+
// ── Global flag detection ────────────────────────────────────────────────────
|
|
29
|
+
/** Flags that are noise for AI agents — always stripped. */
|
|
30
|
+
const NOISE_FLAGS = new Set(['help', 'version']);
|
|
31
|
+
function detectGlobalFlags(commands) {
|
|
32
|
+
const commandNames = Object.keys(commands);
|
|
33
|
+
if (commandNames.length === 0)
|
|
34
|
+
return { universal: new Set(), noise: new Set() };
|
|
35
|
+
// Count how many commands each param appears in
|
|
36
|
+
const paramCounts = new Map();
|
|
37
|
+
for (const cmd of Object.values(commands)) {
|
|
38
|
+
for (const paramName of Object.keys(cmd.params ?? {})) {
|
|
39
|
+
paramCounts.set(paramName, (paramCounts.get(paramName) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const total = commandNames.length;
|
|
43
|
+
const universal = new Set();
|
|
44
|
+
for (const [param, count] of paramCounts) {
|
|
45
|
+
if (count === total)
|
|
46
|
+
universal.add(param);
|
|
47
|
+
}
|
|
48
|
+
return { universal, noise: NOISE_FLAGS };
|
|
49
|
+
}
|
|
50
|
+
function collectCommands(groups) {
|
|
51
|
+
const commands = {};
|
|
52
|
+
for (const group of groups) {
|
|
53
|
+
for (const entry of group.entries) {
|
|
54
|
+
commands[entry.name] = entry.config;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return commands;
|
|
58
|
+
}
|
|
59
|
+
function detectGlobalFlagsFromGroups(groups) {
|
|
60
|
+
return detectGlobalFlags(collectCommands(groups));
|
|
61
|
+
}
|
|
62
|
+
// ── Command grouping ─────────────────────────────────────────────────────────
|
|
63
|
+
function groupCommands(commands, tool) {
|
|
64
|
+
const groups = new Map();
|
|
65
|
+
// Group by the first subcommand from the exec field.
|
|
66
|
+
// e.g. exec "gog gmail send" → group "gmail", exec "gog ls" → group "(root)"
|
|
67
|
+
for (const [name, config] of Object.entries(commands)) {
|
|
68
|
+
const execParts = (config.exec ?? name).split(/\s+/);
|
|
69
|
+
// Strip the tool name prefix to get subcommand parts
|
|
70
|
+
// exec: "gog gmail send" → ["gog", "gmail", "send"] → subcommand parts: ["gmail", "send"]
|
|
71
|
+
const subParts = execParts.slice(1); // remove tool name
|
|
72
|
+
let groupName;
|
|
73
|
+
if (subParts.length > 1) {
|
|
74
|
+
// Has at least one level of nesting — group by first subcommand
|
|
75
|
+
groupName = subParts[0];
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
groupName = '(root)';
|
|
79
|
+
}
|
|
80
|
+
if (!groups.has(groupName))
|
|
81
|
+
groups.set(groupName, []);
|
|
82
|
+
groups.get(groupName).push({ name, config, enabled: true });
|
|
83
|
+
}
|
|
84
|
+
// Move (root) entries that match a group name into that group.
|
|
85
|
+
// e.g. if "gmail" is in (root) and a "gmail" group exists, move it there.
|
|
86
|
+
const rootEntries = groups.get('(root)');
|
|
87
|
+
if (rootEntries) {
|
|
88
|
+
const toMove = [];
|
|
89
|
+
for (const entry of rootEntries) {
|
|
90
|
+
if (entry.name !== tool && groups.has(entry.name) && entry.name !== '(root)') {
|
|
91
|
+
toMove.push(entry);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const entry of toMove) {
|
|
95
|
+
rootEntries.splice(rootEntries.indexOf(entry), 1);
|
|
96
|
+
groups.get(entry.name).unshift(entry); // put parent first in the group
|
|
97
|
+
}
|
|
98
|
+
if (rootEntries.length === 0)
|
|
99
|
+
groups.delete('(root)');
|
|
100
|
+
}
|
|
101
|
+
// Sort: (root) first, then alphabetical
|
|
102
|
+
const sorted = [...groups.entries()].sort(([a], [b]) => {
|
|
103
|
+
if (a === '(root)')
|
|
104
|
+
return -1;
|
|
105
|
+
if (b === '(root)')
|
|
106
|
+
return 1;
|
|
107
|
+
return a.localeCompare(b);
|
|
108
|
+
});
|
|
109
|
+
return sorted.map(([name, entries]) => ({
|
|
110
|
+
name,
|
|
111
|
+
path: name === '(root)' ? [] : [name],
|
|
112
|
+
entries: entries.sort((a, b) => a.name.localeCompare(b.name)),
|
|
113
|
+
loaded: true,
|
|
114
|
+
loading: false,
|
|
115
|
+
pendingEnabled: true,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
function createLazyGroups(tool, topLevelSubcommands, rootCommand) {
|
|
119
|
+
const groups = [];
|
|
120
|
+
if (rootCommand) {
|
|
121
|
+
groups.push({
|
|
122
|
+
name: '(root)',
|
|
123
|
+
path: [],
|
|
124
|
+
entries: [{ name: tool, config: rootCommand, enabled: true }],
|
|
125
|
+
loaded: true,
|
|
126
|
+
loading: false,
|
|
127
|
+
pendingEnabled: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const name of [...topLevelSubcommands].sort((a, b) => a.localeCompare(b))) {
|
|
131
|
+
groups.push({
|
|
132
|
+
name,
|
|
133
|
+
path: [name],
|
|
134
|
+
entries: [],
|
|
135
|
+
loaded: false,
|
|
136
|
+
loading: false,
|
|
137
|
+
pendingEnabled: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return groups;
|
|
141
|
+
}
|
|
142
|
+
// ── Compact YAML output ──────────────────────────────────────────────────────
|
|
143
|
+
function buildCompactConfig(tool, groups, globalFlags, stripGlobal) {
|
|
144
|
+
const commands = {};
|
|
145
|
+
for (const group of groups) {
|
|
146
|
+
for (const entry of group.entries) {
|
|
147
|
+
if (!entry.enabled)
|
|
148
|
+
continue;
|
|
149
|
+
const cmd = {
|
|
150
|
+
exec: entry.config.exec,
|
|
151
|
+
};
|
|
152
|
+
if (entry.config.description) {
|
|
153
|
+
cmd.description = entry.config.description;
|
|
154
|
+
}
|
|
155
|
+
// Build compact params — strip globals and noise, omit defaults
|
|
156
|
+
const params = {};
|
|
157
|
+
for (const [paramName, paramConfig] of Object.entries(entry.config.params ?? {})) {
|
|
158
|
+
if (globalFlags.noise.has(paramName))
|
|
159
|
+
continue;
|
|
160
|
+
if (stripGlobal && globalFlags.universal.has(paramName))
|
|
161
|
+
continue;
|
|
162
|
+
const compact = { type: paramConfig.type };
|
|
163
|
+
if (paramConfig.flag)
|
|
164
|
+
compact.flag = paramConfig.flag;
|
|
165
|
+
if (paramConfig.positional)
|
|
166
|
+
compact.positional = true;
|
|
167
|
+
if (paramConfig.required)
|
|
168
|
+
compact.required = true;
|
|
169
|
+
if (paramConfig.description)
|
|
170
|
+
compact.description = paramConfig.description;
|
|
171
|
+
// Omit default, positional: false, required: false — they're defaults
|
|
172
|
+
params[paramName] = compact;
|
|
173
|
+
}
|
|
174
|
+
if (Object.keys(params).length > 0)
|
|
175
|
+
cmd.params = params;
|
|
176
|
+
cmd.timeout = entry.config.timeout ?? 30;
|
|
177
|
+
commands[entry.name] = cmd;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
clis: {
|
|
182
|
+
[tool]: { commands },
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function serializeCompact(data, tool, strategy) {
|
|
187
|
+
const header = [
|
|
188
|
+
'# Auto-discovered by Airlock (interactive)',
|
|
189
|
+
`# Tool: ${tool}`,
|
|
190
|
+
`# Strategy: ${strategy}`,
|
|
191
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
192
|
+
'#',
|
|
193
|
+
'# Only selected commands are included. Global/noise flags stripped.',
|
|
194
|
+
'',
|
|
195
|
+
].join('\n');
|
|
196
|
+
return header + stringifyYaml(data, { lineWidth: 120 });
|
|
197
|
+
}
|
|
198
|
+
function matchesSearch(value, query) {
|
|
199
|
+
return value.toLowerCase().includes(query.trim().toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
function buildVisibleRows(groups, viewLevel, activeGroupIdx, searchTerm) {
|
|
202
|
+
const query = searchTerm.trim();
|
|
203
|
+
const rows = viewLevel === 'groups'
|
|
204
|
+
? buildGroupRows(groups)
|
|
205
|
+
: buildCommandRows(groups[activeGroupIdx], activeGroupIdx);
|
|
206
|
+
if (!query)
|
|
207
|
+
return rows;
|
|
208
|
+
return rows.filter((row) => {
|
|
209
|
+
if (row.type === 'group-header') {
|
|
210
|
+
return matchesSearch(row.group.name, query);
|
|
211
|
+
}
|
|
212
|
+
const haystack = [row.entry.name, row.entry.config.exec, row.entry.config.description ?? '']
|
|
213
|
+
.join(' ')
|
|
214
|
+
.trim();
|
|
215
|
+
return matchesSearch(haystack, query);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function buildGroupRows(groups) {
|
|
219
|
+
return groups.map((group, i) => ({
|
|
220
|
+
type: 'group-header',
|
|
221
|
+
group,
|
|
222
|
+
groupIdx: i,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
function buildCommandRows(group, groupIdx) {
|
|
226
|
+
return group.entries.map((entry, i) => ({
|
|
227
|
+
type: 'command',
|
|
228
|
+
entry,
|
|
229
|
+
groupIdx,
|
|
230
|
+
entryIdx: i,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
// ── TUI rendering ────────────────────────────────────────────────────────────
|
|
234
|
+
function renderGroupRow(row, isSel) {
|
|
235
|
+
const { group } = row;
|
|
236
|
+
const enabled = group.entries.filter((e) => e.enabled).length;
|
|
237
|
+
const total = group.entries.length;
|
|
238
|
+
const allOn = group.loaded ? enabled === total : group.pendingEnabled;
|
|
239
|
+
const allOff = group.loaded ? enabled === 0 : !group.pendingEnabled;
|
|
240
|
+
const check = allOn
|
|
241
|
+
? `${GREEN}[✓]${RESET}`
|
|
242
|
+
: allOff
|
|
243
|
+
? `${DIM}[ ]${RESET}`
|
|
244
|
+
: `${YELLOW}[~]${RESET}`;
|
|
245
|
+
const selMark = isSel ? `${BOLD}${YELLOW}▸ ` : ` `;
|
|
246
|
+
const nameStr = `${isSel ? BOLD : ''}${group.name}${RESET}`;
|
|
247
|
+
const countStr = group.loaded
|
|
248
|
+
? `${DIM}(${enabled}/${total} commands)${RESET}`
|
|
249
|
+
: group.loading
|
|
250
|
+
? `${YELLOW}(loading...)${RESET}`
|
|
251
|
+
: `${DIM}(not loaded yet)${RESET}`;
|
|
252
|
+
const lines = [];
|
|
253
|
+
lines.push(` ${selMark}${check} ${nameStr} ${countStr}`);
|
|
254
|
+
if (isSel) {
|
|
255
|
+
const hint = group.loaded
|
|
256
|
+
? `${DIM} ↳ [space] toggle all [enter] drill down [a] all on [n] all off${RESET}`
|
|
257
|
+
: `${DIM} ↳ [enter] load group [space] queue selection before load${RESET}`;
|
|
258
|
+
lines.push(hint);
|
|
259
|
+
if (group.error)
|
|
260
|
+
lines.push(` ${RED}${group.error}${RESET}`);
|
|
261
|
+
}
|
|
262
|
+
return lines;
|
|
263
|
+
}
|
|
264
|
+
function renderCommandRow(row, isSel, globalFlags, stripGlobal) {
|
|
265
|
+
const { entry } = row;
|
|
266
|
+
const check = entry.enabled ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
267
|
+
const selMark = isSel ? `${BOLD}${YELLOW} ▸ ` : ` `;
|
|
268
|
+
const nameStr = `${isSel ? BOLD : ''}${entry.name}${RESET}`;
|
|
269
|
+
// Count non-global params
|
|
270
|
+
const totalParams = Object.keys(entry.config.params ?? {}).length;
|
|
271
|
+
const globalCount = Object.keys(entry.config.params ?? {}).filter((p) => globalFlags.noise.has(p) || (stripGlobal && globalFlags.universal.has(p))).length;
|
|
272
|
+
const effectiveParams = totalParams - globalCount;
|
|
273
|
+
const paramStr = effectiveParams > 0 ? `${DIM}(${effectiveParams} params)${RESET}` : '';
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`${selMark}${check} ${nameStr} ${paramStr}`);
|
|
276
|
+
if (isSel) {
|
|
277
|
+
const desc = entry.config.description ?? '';
|
|
278
|
+
if (desc) {
|
|
279
|
+
const termCols = process.stdout.columns || 80;
|
|
280
|
+
const maxDesc = Math.max(20, termCols - 10);
|
|
281
|
+
const flat = desc.replace(/\s*\n\s*/g, ' ');
|
|
282
|
+
const truncated = flat.length > maxDesc ? flat.slice(0, maxDesc) + '…' : flat;
|
|
283
|
+
lines.push(` ${DIM}${truncated}${RESET}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push(` ${DIM}exec: ${entry.config.exec}${RESET}`);
|
|
286
|
+
}
|
|
287
|
+
return lines;
|
|
288
|
+
}
|
|
289
|
+
function render(groups, viewLevel, activeGroupIdx, rowIdx, tool, globalFlags, stripGlobal, searchTerm = '', searchMode = false) {
|
|
290
|
+
const out = process.stdout;
|
|
291
|
+
const termHeight = process.stdout.rows || 40;
|
|
292
|
+
const termCols = process.stdout.columns || 80;
|
|
293
|
+
out.write(`${ESC}H${ESC}2J`);
|
|
294
|
+
// Summary counts
|
|
295
|
+
const totalCommands = groups.reduce((s, g) => s + g.entries.length, 0);
|
|
296
|
+
const enabledCommands = groups.reduce((s, g) => s + g.entries.filter((e) => e.enabled).length, 0);
|
|
297
|
+
const loadedGroups = groups.filter((group) => group.loaded).length;
|
|
298
|
+
// Build header
|
|
299
|
+
const breadcrumb = viewLevel === 'commands' ? ` > ${groups[activeGroupIdx].name}` : '';
|
|
300
|
+
const headerLines = [
|
|
301
|
+
'',
|
|
302
|
+
`${BOLD}${CYAN} Airlock — Configure CLI: ${tool}${breadcrumb}${RESET}`,
|
|
303
|
+
`${DIM} ${'─'.repeat(Math.min(70, termCols - 4))}${RESET}`,
|
|
304
|
+
];
|
|
305
|
+
if (viewLevel === 'groups') {
|
|
306
|
+
headerLines.push(`${DIM} [space] toggle [l/enter] drill down [/] search [a] all on [n] all off [g] global flags [w] done [q] quit${RESET}`);
|
|
307
|
+
headerLines.push(`${DIM} global flags: ${stripGlobal ? `${GREEN}stripped${RESET}` : `${YELLOW}included${RESET}`}${DIM} (${globalFlags.universal.size} detected across loaded commands)${RESET}`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
headerLines.push(`${DIM} [space] toggle [l] inspect [h/esc] back [/] search [a] all on [n] all off [q] quit${RESET}`);
|
|
311
|
+
}
|
|
312
|
+
const searchLabel = searchMode
|
|
313
|
+
? `${YELLOW}/${searchTerm}${RESET}`
|
|
314
|
+
: searchTerm
|
|
315
|
+
? `${CYAN}/${searchTerm}${RESET}`
|
|
316
|
+
: `${DIM}/ search${RESET}`;
|
|
317
|
+
headerLines.push(`${DIM} filter: ${RESET}${searchLabel}`);
|
|
318
|
+
headerLines.push(`${DIM} ${'─'.repeat(Math.min(70, termCols - 4))}${RESET}`);
|
|
319
|
+
// Build rows
|
|
320
|
+
const rows = buildVisibleRows(groups, viewLevel, activeGroupIdx, searchTerm);
|
|
321
|
+
// Render all rows
|
|
322
|
+
const allRendered = [];
|
|
323
|
+
for (let i = 0; i < rows.length; i++) {
|
|
324
|
+
const row = rows[i];
|
|
325
|
+
const isSel = i === rowIdx;
|
|
326
|
+
let lines;
|
|
327
|
+
if (row.type === 'group-header') {
|
|
328
|
+
lines = renderGroupRow(row, isSel);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
lines = renderCommandRow(row, isSel, globalFlags, stripGlobal);
|
|
332
|
+
}
|
|
333
|
+
const screenRows = lines.map((l) => terminalRows(l, termCols));
|
|
334
|
+
allRendered.push({ lines, screenRows });
|
|
335
|
+
}
|
|
336
|
+
// Flatten for viewport calculation
|
|
337
|
+
const flatLines = [];
|
|
338
|
+
for (const rendered of allRendered) {
|
|
339
|
+
for (let j = 0; j < rendered.lines.length; j++) {
|
|
340
|
+
flatLines.push({ line: rendered.lines[j], cost: rendered.screenRows[j] });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const headerScreenRows = headerLines.reduce((sum, l) => sum + terminalRows(l, termCols), 0);
|
|
344
|
+
const footerScreenRows = 2;
|
|
345
|
+
const contentHeight = termHeight - headerScreenRows - footerScreenRows;
|
|
346
|
+
const totalScreenRows = flatLines.reduce((sum, f) => sum + f.cost, 0);
|
|
347
|
+
// Find selected row's midpoint for centering
|
|
348
|
+
let selectedStart = 0;
|
|
349
|
+
for (let i = 0; i < rowIdx && i < allRendered.length; i++) {
|
|
350
|
+
selectedStart += allRendered[i].screenRows.reduce((a, b) => a + b, 0);
|
|
351
|
+
}
|
|
352
|
+
const selectedCost = rowIdx < allRendered.length ? allRendered[rowIdx].screenRows.reduce((a, b) => a + b, 0) : 0;
|
|
353
|
+
const selectedMid = selectedStart + Math.floor(selectedCost / 2);
|
|
354
|
+
let scrollCost = Math.max(0, selectedMid - Math.floor(contentHeight / 2));
|
|
355
|
+
scrollCost = Math.max(0, Math.min(scrollCost, totalScreenRows - contentHeight));
|
|
356
|
+
// Collect viewport lines
|
|
357
|
+
const viewportLines = [];
|
|
358
|
+
let consumed = 0;
|
|
359
|
+
let usedScreenRows = 0;
|
|
360
|
+
let linesAbove = 0;
|
|
361
|
+
let linesBelow = 0;
|
|
362
|
+
for (const flat of flatLines) {
|
|
363
|
+
if (consumed + flat.cost <= scrollCost) {
|
|
364
|
+
consumed += flat.cost;
|
|
365
|
+
linesAbove++;
|
|
366
|
+
}
|
|
367
|
+
else if (usedScreenRows + flat.cost <= contentHeight) {
|
|
368
|
+
viewportLines.push(flat.line);
|
|
369
|
+
usedScreenRows += flat.cost;
|
|
370
|
+
consumed += flat.cost;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
linesBelow++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Scroll indicator
|
|
377
|
+
const scrollParts = [];
|
|
378
|
+
if (linesAbove > 0)
|
|
379
|
+
scrollParts.push(`↑ ${linesAbove} above`);
|
|
380
|
+
if (linesBelow > 0)
|
|
381
|
+
scrollParts.push(`↓ ${linesBelow} below`);
|
|
382
|
+
const scrollLine = scrollParts.length ? `${DIM} ${scrollParts.join(' ')}${RESET}` : '';
|
|
383
|
+
// Pad viewport
|
|
384
|
+
while (usedScreenRows < contentHeight) {
|
|
385
|
+
viewportLines.push('');
|
|
386
|
+
usedScreenRows++;
|
|
387
|
+
}
|
|
388
|
+
// Summary line
|
|
389
|
+
const summaryLine = `${DIM} ${enabledCommands}/${totalCommands} loaded commands selected | ${loadedGroups}/${groups.length} groups loaded | ${rows.length} visible${RESET}`;
|
|
390
|
+
// Write
|
|
391
|
+
for (const line of headerLines)
|
|
392
|
+
out.write(line + '\n');
|
|
393
|
+
for (const line of viewportLines)
|
|
394
|
+
out.write(line + '\n');
|
|
395
|
+
out.write(scrollLine + '\n');
|
|
396
|
+
out.write(summaryLine);
|
|
397
|
+
}
|
|
398
|
+
// ── Inspect mode ─────────────────────────────────────────────────────────────
|
|
399
|
+
function renderInspect(entry, globalFlags, stripGlobal, inspectScroll) {
|
|
400
|
+
const out = process.stdout;
|
|
401
|
+
out.write(`${ESC}H${ESC}2J`);
|
|
402
|
+
const check = entry.enabled ? `${GREEN}enabled${RESET}` : `${RED}disabled${RESET}`;
|
|
403
|
+
out.write(`\n${BOLD}${CYAN} Inspect: ${entry.name}${RESET} ${check}\n`);
|
|
404
|
+
out.write(`${DIM} ${'─'.repeat(70)}${RESET}\n`);
|
|
405
|
+
out.write(`${DIM} [j/k] scroll [h/esc] back [space] toggle${RESET}\n`);
|
|
406
|
+
out.write(`${DIM} ${'─'.repeat(70)}${RESET}\n\n`);
|
|
407
|
+
const lines = [];
|
|
408
|
+
lines.push(` ${BOLD}exec:${RESET} ${entry.config.exec}`);
|
|
409
|
+
if (entry.config.description) {
|
|
410
|
+
lines.push(` ${BOLD}description:${RESET} ${entry.config.description}`);
|
|
411
|
+
}
|
|
412
|
+
lines.push('');
|
|
413
|
+
lines.push(` ${BOLD}Parameters:${RESET}`);
|
|
414
|
+
for (const [paramName, param] of Object.entries(entry.config.params ?? {})) {
|
|
415
|
+
const isGlobal = globalFlags.universal.has(paramName);
|
|
416
|
+
const isNoise = globalFlags.noise.has(paramName);
|
|
417
|
+
const stripped = isNoise || (stripGlobal && isGlobal);
|
|
418
|
+
const tag = stripped ? `${DIM}[stripped]${RESET}` : isGlobal ? `${YELLOW}[global]${RESET}` : '';
|
|
419
|
+
lines.push(` ${stripped ? DIM : ''}${paramName}${RESET} ${param.type} ${param.flag ?? ''} ${tag}`);
|
|
420
|
+
if (param.description) {
|
|
421
|
+
lines.push(` ${DIM}${param.description}${RESET}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Scrollable viewport
|
|
425
|
+
const termRows = process.stdout.rows || 40;
|
|
426
|
+
const viewportHeight = termRows - 10;
|
|
427
|
+
const maxScroll = Math.max(0, lines.length - viewportHeight);
|
|
428
|
+
const scroll = Math.min(inspectScroll, maxScroll);
|
|
429
|
+
const visible = lines.slice(scroll, scroll + viewportHeight);
|
|
430
|
+
for (const line of visible) {
|
|
431
|
+
out.write(`${line}\n`);
|
|
432
|
+
}
|
|
433
|
+
if (lines.length > viewportHeight) {
|
|
434
|
+
const parts = [];
|
|
435
|
+
if (scroll > 0)
|
|
436
|
+
parts.push(`↑ ${scroll} above`);
|
|
437
|
+
const below = lines.length - scroll - viewportHeight;
|
|
438
|
+
if (below > 0)
|
|
439
|
+
parts.push(`↓ ${below} below`);
|
|
440
|
+
out.write(`\n${DIM} ${parts.join(' ')}${RESET}\n`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ── Help ──────────────────────────────────────────────────────────────────────
|
|
444
|
+
const HELP = `
|
|
445
|
+
airlock configure-cli — interactively select and configure CLI tool commands
|
|
446
|
+
|
|
447
|
+
Usage:
|
|
448
|
+
airlock configure-cli <tool> [options]
|
|
449
|
+
|
|
450
|
+
Options:
|
|
451
|
+
--fig Try Fig autocomplete specs first
|
|
452
|
+
--max-depth <n> Max subcommand recursion depth (default: 2)
|
|
453
|
+
--output, -o <path> Write YAML to file instead of action picker
|
|
454
|
+
-h, --help Show this help
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
airlock configure-cli gog
|
|
458
|
+
airlock configure-cli docker --fig
|
|
459
|
+
airlock configure-cli git --max-depth 3 -o git-commands.yaml
|
|
460
|
+
`;
|
|
461
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
462
|
+
export async function runConfigureCli(argv) {
|
|
463
|
+
const { values, positionals } = parseArgs({
|
|
464
|
+
args: argv,
|
|
465
|
+
options: {
|
|
466
|
+
output: { type: 'string', short: 'o' },
|
|
467
|
+
fig: { type: 'boolean', default: false },
|
|
468
|
+
'max-depth': { type: 'string' },
|
|
469
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
470
|
+
},
|
|
471
|
+
allowPositionals: true,
|
|
472
|
+
});
|
|
473
|
+
if (values.help || positionals.length === 0) {
|
|
474
|
+
console.log(HELP);
|
|
475
|
+
process.exit(positionals.length === 0 ? 1 : 0);
|
|
476
|
+
}
|
|
477
|
+
const tool = positionals[0];
|
|
478
|
+
const maxDepth = values['max-depth'] ? parseInt(values['max-depth'], 10) : undefined;
|
|
479
|
+
// ── Discovery phase ──────────────────────────────────────────────────────
|
|
480
|
+
process.stdout.write(`\n${BOLD}Discovering commands for ${CYAN}${tool}${RESET}${BOLD}…${RESET}\n`);
|
|
481
|
+
let strategy;
|
|
482
|
+
const completionSession = !values.fig ? createCompletionSession(tool) : null;
|
|
483
|
+
let groups;
|
|
484
|
+
if (values.output || !completionSession) {
|
|
485
|
+
const discovery = await discoverCliCommands({
|
|
486
|
+
tool,
|
|
487
|
+
fromFig: values.fig,
|
|
488
|
+
maxDepth,
|
|
489
|
+
});
|
|
490
|
+
const commands = discovery.commands;
|
|
491
|
+
strategy = discovery.strategy;
|
|
492
|
+
if (strategy.startsWith('completion:')) {
|
|
493
|
+
process.stdout.write(`${DIM} detected ${strategy.replace('completion:', '')} completion support${RESET}\n`);
|
|
494
|
+
}
|
|
495
|
+
else if (strategy === 'help-text') {
|
|
496
|
+
process.stdout.write(`${DIM} falling back to --help parsing${RESET}\n`);
|
|
497
|
+
}
|
|
498
|
+
const commandCount = Object.keys(commands).length;
|
|
499
|
+
if (commandCount === 0) {
|
|
500
|
+
console.error(`No commands discovered for "${tool}".`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
groups = groupCommands(commands, tool);
|
|
504
|
+
const globalFlags = detectGlobalFlagsFromGroups(groups);
|
|
505
|
+
let stripGlobal = true;
|
|
506
|
+
process.stdout.write(`${GREEN}Found ${commandCount} commands${RESET}` +
|
|
507
|
+
` ${DIM}(${globalFlags.universal.size} global flags detected: ${[...globalFlags.universal].join(', ')})${RESET}\n`);
|
|
508
|
+
process.stdout.write(`${DIM}Grouped into ${groups.length} sections.${RESET}\n\n`);
|
|
509
|
+
if (values.output) {
|
|
510
|
+
const data = buildCompactConfig(tool, groups, globalFlags, stripGlobal);
|
|
511
|
+
const yaml = serializeCompact(data, tool, strategy);
|
|
512
|
+
writeFileSync(values.output, yaml);
|
|
513
|
+
console.log(`Written to ${values.output}`);
|
|
514
|
+
process.exit(0);
|
|
515
|
+
}
|
|
516
|
+
await runInteractiveConfigurator(tool, strategy, groups, stripGlobal, undefined, maxDepth);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
strategy = `completion:${completionSession.adapterId}`;
|
|
520
|
+
process.stdout.write(`${DIM} detected ${strategy.replace('completion:', '')} completion support${RESET}\n`);
|
|
521
|
+
const rootCommand = completionSession.loadCommand([], tool);
|
|
522
|
+
const topLevelSubcommands = completionSession.listTopLevelSubcommands();
|
|
523
|
+
groups = createLazyGroups(tool, topLevelSubcommands, rootCommand);
|
|
524
|
+
if (groups.length === 0) {
|
|
525
|
+
console.error(`No commands discovered for "${tool}".`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
const initialGlobalFlags = detectGlobalFlagsFromGroups(groups);
|
|
529
|
+
process.stdout.write(`${GREEN}Loaded ${groups.filter((group) => group.loaded).length} section(s) immediately${RESET}` +
|
|
530
|
+
` ${DIM}(${topLevelSubcommands.length} more available to load on demand)${RESET}\n`);
|
|
531
|
+
process.stdout.write(`${DIM}Global flags currently based on loaded commands: ${[...initialGlobalFlags.universal].join(', ') || 'none'}${RESET}\n\n`);
|
|
532
|
+
await runInteractiveConfigurator(tool, strategy, groups, true, completionSession, maxDepth);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
async function runInteractiveConfigurator(tool, strategy, groups, initialStripGlobal, completionSession, maxDepth) {
|
|
536
|
+
process.stdout.write(`${BOLD}Press any key to start the configurator…${RESET}`);
|
|
537
|
+
const ttyFd = openSync('/dev/tty', 'r+');
|
|
538
|
+
const tty = new ReadStream(ttyFd);
|
|
539
|
+
tty.setEncoding('utf8');
|
|
540
|
+
await new Promise((resolve) => {
|
|
541
|
+
tty.setRawMode(true);
|
|
542
|
+
tty.once('data', () => resolve());
|
|
543
|
+
tty.resume();
|
|
544
|
+
});
|
|
545
|
+
process.stdout.write('\x1b[?1049h');
|
|
546
|
+
let viewLevel = 'groups';
|
|
547
|
+
let activeGroupIdx = 0;
|
|
548
|
+
let rowIdx = 0;
|
|
549
|
+
let done = false;
|
|
550
|
+
let quit = false;
|
|
551
|
+
let inspectMode = false;
|
|
552
|
+
let inspectScroll = 0;
|
|
553
|
+
let stripGlobal = initialStripGlobal;
|
|
554
|
+
let searchTerm = '';
|
|
555
|
+
let searchMode = false;
|
|
556
|
+
function currentGlobalFlags() {
|
|
557
|
+
return detectGlobalFlagsFromGroups(groups);
|
|
558
|
+
}
|
|
559
|
+
function getMaxIdx() {
|
|
560
|
+
return Math.max(0, buildVisibleRows(groups, viewLevel, activeGroupIdx, searchTerm).length - 1);
|
|
561
|
+
}
|
|
562
|
+
function getVisibleRows() {
|
|
563
|
+
return buildVisibleRows(groups, viewLevel, activeGroupIdx, searchTerm);
|
|
564
|
+
}
|
|
565
|
+
function getSelectedEntry() {
|
|
566
|
+
const row = getVisibleRows()[rowIdx];
|
|
567
|
+
return row?.type === 'command' ? row.entry : null;
|
|
568
|
+
}
|
|
569
|
+
function renderCurrent() {
|
|
570
|
+
const visibleRows = getVisibleRows();
|
|
571
|
+
if (rowIdx > Math.max(0, visibleRows.length - 1))
|
|
572
|
+
rowIdx = Math.max(0, visibleRows.length - 1);
|
|
573
|
+
render(groups, viewLevel, activeGroupIdx, rowIdx, tool, currentGlobalFlags(), stripGlobal, searchTerm, searchMode);
|
|
574
|
+
}
|
|
575
|
+
function ensureGroupLoaded(group, renderWhileLoading = true) {
|
|
576
|
+
if (group.loaded || group.loading || !completionSession || group.path.length === 0)
|
|
577
|
+
return;
|
|
578
|
+
group.loading = true;
|
|
579
|
+
group.error = undefined;
|
|
580
|
+
if (renderWhileLoading)
|
|
581
|
+
renderCurrent();
|
|
582
|
+
try {
|
|
583
|
+
const remainingDepth = Math.max(0, (maxDepth ?? 2) - group.path.length);
|
|
584
|
+
const discovered = completionSession.loadPath(group.path, { maxDepth: remainingDepth });
|
|
585
|
+
group.entries = Object.entries(discovered)
|
|
586
|
+
.map(([name, config]) => ({ name, config, enabled: group.pendingEnabled }))
|
|
587
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
588
|
+
group.loaded = true;
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
group.error = error instanceof Error ? error.message : 'Failed to load group';
|
|
592
|
+
group.entries = [];
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
group.loading = false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function ensureSelectedGroupsLoadedForOutput() {
|
|
599
|
+
if (!completionSession)
|
|
600
|
+
return;
|
|
601
|
+
process.stdout.write('\x1b[?1049l');
|
|
602
|
+
process.stdout.write(`\n${DIM}Loading selected groups…${RESET}\n`);
|
|
603
|
+
for (const group of groups) {
|
|
604
|
+
if (!group.loaded && group.pendingEnabled) {
|
|
605
|
+
ensureGroupLoaded(group, false);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
renderCurrent();
|
|
610
|
+
await new Promise((resolve) => {
|
|
611
|
+
tty.on('data', (key) => {
|
|
612
|
+
if (done)
|
|
613
|
+
return;
|
|
614
|
+
if (inspectMode) {
|
|
615
|
+
const entry = getSelectedEntry();
|
|
616
|
+
if (!entry) {
|
|
617
|
+
inspectMode = false;
|
|
618
|
+
renderCurrent();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (key === 'i' || key === 'h' || key === '\x1b' || key === '\x03') {
|
|
622
|
+
inspectMode = false;
|
|
623
|
+
renderCurrent();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (key === 'j' || key === `${ESC}B`)
|
|
627
|
+
inspectScroll++;
|
|
628
|
+
if (key === 'k' || key === `${ESC}A`)
|
|
629
|
+
inspectScroll = Math.max(0, inspectScroll - 1);
|
|
630
|
+
if (key === ' ')
|
|
631
|
+
entry.enabled = !entry.enabled;
|
|
632
|
+
renderInspect(entry, currentGlobalFlags(), stripGlobal, inspectScroll);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (searchMode) {
|
|
636
|
+
if (key === '\r' || key === '\n' || key === '\x1b') {
|
|
637
|
+
searchMode = false;
|
|
638
|
+
rowIdx = 0;
|
|
639
|
+
renderCurrent();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (key === '\x7f') {
|
|
643
|
+
searchTerm = searchTerm.slice(0, -1);
|
|
644
|
+
rowIdx = 0;
|
|
645
|
+
renderCurrent();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (key >= ' ' && key <= '~') {
|
|
649
|
+
searchTerm += key;
|
|
650
|
+
rowIdx = 0;
|
|
651
|
+
renderCurrent();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (key === '\x03' || key === 'q') {
|
|
657
|
+
quit = true;
|
|
658
|
+
done = true;
|
|
659
|
+
process.stdout.write('\x1b[?1049l');
|
|
660
|
+
tty.setRawMode(false);
|
|
661
|
+
tty.destroy();
|
|
662
|
+
resolve();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (viewLevel === 'groups' && (key === 'l' || key === '\r' || key === '\n')) {
|
|
666
|
+
const row = getVisibleRows()[rowIdx];
|
|
667
|
+
if (row?.type === 'group-header') {
|
|
668
|
+
const group = row.group;
|
|
669
|
+
ensureGroupLoaded(group);
|
|
670
|
+
activeGroupIdx = row.groupIdx;
|
|
671
|
+
viewLevel = 'commands';
|
|
672
|
+
rowIdx = 0;
|
|
673
|
+
renderCurrent();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (viewLevel === 'commands' && key === 'l') {
|
|
678
|
+
const entry = getSelectedEntry();
|
|
679
|
+
if (entry) {
|
|
680
|
+
inspectMode = true;
|
|
681
|
+
inspectScroll = 0;
|
|
682
|
+
renderInspect(entry, currentGlobalFlags(), stripGlobal, inspectScroll);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (viewLevel === 'groups' && key === 'w') {
|
|
687
|
+
done = true;
|
|
688
|
+
process.stdout.write('\x1b[?1049l');
|
|
689
|
+
tty.setRawMode(false);
|
|
690
|
+
tty.destroy();
|
|
691
|
+
resolve();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (viewLevel === 'commands' &&
|
|
695
|
+
(key === 'h' || key === '\x1b' || key === '\r' || key === '\n')) {
|
|
696
|
+
viewLevel = 'groups';
|
|
697
|
+
rowIdx = Math.max(0, getVisibleRows().findIndex((row) => row.type === 'group-header' && row.groupIdx === activeGroupIdx));
|
|
698
|
+
renderCurrent();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (key === '/') {
|
|
702
|
+
searchMode = true;
|
|
703
|
+
searchTerm = '';
|
|
704
|
+
rowIdx = 0;
|
|
705
|
+
renderCurrent();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const maxIdx = getMaxIdx();
|
|
709
|
+
if (key === 'j' || key === `${ESC}B`)
|
|
710
|
+
rowIdx = Math.min(rowIdx + 1, maxIdx);
|
|
711
|
+
if (key === 'k' || key === `${ESC}A`)
|
|
712
|
+
rowIdx = Math.max(rowIdx - 1, 0);
|
|
713
|
+
if (key === ' ') {
|
|
714
|
+
if (viewLevel === 'groups') {
|
|
715
|
+
const row = getVisibleRows()[rowIdx];
|
|
716
|
+
if (row?.type !== 'group-header') {
|
|
717
|
+
renderCurrent();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const group = row.group;
|
|
721
|
+
if (!group.loaded) {
|
|
722
|
+
group.pendingEnabled = !group.pendingEnabled;
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const allEnabled = group.entries.every((entry) => entry.enabled);
|
|
726
|
+
for (const entry of group.entries)
|
|
727
|
+
entry.enabled = !allEnabled;
|
|
728
|
+
group.pendingEnabled = !allEnabled;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
const row = getVisibleRows()[rowIdx];
|
|
733
|
+
const entry = row?.type === 'command' ? row.entry : null;
|
|
734
|
+
if (entry)
|
|
735
|
+
entry.enabled = !entry.enabled;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (key === 'a') {
|
|
739
|
+
if (viewLevel === 'groups') {
|
|
740
|
+
for (const group of groups) {
|
|
741
|
+
group.pendingEnabled = true;
|
|
742
|
+
for (const entry of group.entries)
|
|
743
|
+
entry.enabled = true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
const group = groups[activeGroupIdx];
|
|
748
|
+
group.pendingEnabled = true;
|
|
749
|
+
for (const entry of group.entries)
|
|
750
|
+
entry.enabled = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (key === 'n') {
|
|
754
|
+
if (viewLevel === 'groups') {
|
|
755
|
+
for (const group of groups) {
|
|
756
|
+
group.pendingEnabled = false;
|
|
757
|
+
for (const entry of group.entries)
|
|
758
|
+
entry.enabled = false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
const group = groups[activeGroupIdx];
|
|
763
|
+
group.pendingEnabled = false;
|
|
764
|
+
for (const entry of group.entries)
|
|
765
|
+
entry.enabled = false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (key === 'g' && viewLevel === 'groups') {
|
|
769
|
+
stripGlobal = !stripGlobal;
|
|
770
|
+
}
|
|
771
|
+
if (key === 'i' && viewLevel === 'commands') {
|
|
772
|
+
const entry = getSelectedEntry();
|
|
773
|
+
if (entry) {
|
|
774
|
+
inspectMode = true;
|
|
775
|
+
inspectScroll = 0;
|
|
776
|
+
renderInspect(entry, currentGlobalFlags(), stripGlobal, inspectScroll);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
renderCurrent();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
if (quit) {
|
|
784
|
+
console.log('Aborted.');
|
|
785
|
+
process.exit(0);
|
|
786
|
+
}
|
|
787
|
+
ensureSelectedGroupsLoadedForOutput();
|
|
788
|
+
const globalFlags = currentGlobalFlags();
|
|
789
|
+
const enabledCount = groups.reduce((sum, group) => sum + group.entries.filter((entry) => entry.enabled).length, 0);
|
|
790
|
+
if (enabledCount === 0) {
|
|
791
|
+
console.log('No commands selected. Nothing to output.');
|
|
792
|
+
process.exit(0);
|
|
793
|
+
}
|
|
794
|
+
const data = buildCompactConfig(tool, groups, globalFlags, stripGlobal);
|
|
795
|
+
const yaml = serializeCompact(data, tool, strategy);
|
|
796
|
+
process.stdout.write(`\n${BOLD}${CYAN}${enabledCount} commands selected.${RESET} What would you like to do?\n`);
|
|
797
|
+
process.stdout.write(` ${BOLD}[e]${RESET} edit airlock.yaml directly ${DIM}(backs up original to .bak)${RESET}\n`);
|
|
798
|
+
process.stdout.write(` ${BOLD}[c]${RESET} copy to clipboard\n`);
|
|
799
|
+
process.stdout.write(` ${BOLD}[p]${RESET} print to stdout\n`);
|
|
800
|
+
process.stdout.write(` ${BOLD}[o]${RESET} write to file\n`);
|
|
801
|
+
process.stdout.write(` ${BOLD}[q]${RESET} abort\n\n`);
|
|
802
|
+
process.stdout.write(`> `);
|
|
803
|
+
const ttyFd2 = openSync('/dev/tty', 'r+');
|
|
804
|
+
const tty2 = new ReadStream(ttyFd2);
|
|
805
|
+
tty2.setEncoding('utf8');
|
|
806
|
+
const action = await new Promise((resolve) => {
|
|
807
|
+
tty2.setRawMode(true);
|
|
808
|
+
tty2.resume();
|
|
809
|
+
tty2.once('data', (key) => {
|
|
810
|
+
tty2.setRawMode(false);
|
|
811
|
+
tty2.destroy();
|
|
812
|
+
resolve(key);
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
process.stdout.write('\n');
|
|
816
|
+
if (action === 'q' || action === '\x03') {
|
|
817
|
+
console.log('Aborted.');
|
|
818
|
+
process.exit(0);
|
|
819
|
+
}
|
|
820
|
+
if (action === 'p') {
|
|
821
|
+
console.log(`\n${BOLD}${CYAN}# Paste into your airlock.yaml:${RESET}\n`);
|
|
822
|
+
console.log(yaml);
|
|
823
|
+
console.log();
|
|
824
|
+
process.exit(0);
|
|
825
|
+
}
|
|
826
|
+
if (action === 'c') {
|
|
827
|
+
const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
|
|
828
|
+
try {
|
|
829
|
+
execSync(clipCmd, { input: yaml });
|
|
830
|
+
console.log(`${GREEN}✓ Copied to clipboard.${RESET}`);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
console.error(`Could not copy to clipboard. Here is the YAML:\n`);
|
|
834
|
+
console.log(yaml);
|
|
835
|
+
}
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
if (action === 'o') {
|
|
839
|
+
const outputPath = `${tool}-commands.yaml`;
|
|
840
|
+
writeFileSync(outputPath, yaml);
|
|
841
|
+
console.log(`${GREEN}✓ Written to ${outputPath}${RESET}`);
|
|
842
|
+
process.exit(0);
|
|
843
|
+
}
|
|
844
|
+
if (action === 'e') {
|
|
845
|
+
const configPath = './airlock.yaml';
|
|
846
|
+
try {
|
|
847
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
848
|
+
const backupPath = configPath.replace(/\.ya?ml$/i, '') + '.bak';
|
|
849
|
+
copyFileSync(configPath, backupPath);
|
|
850
|
+
const doc = parseYaml(raw);
|
|
851
|
+
const existingClis = (doc.clis ?? {});
|
|
852
|
+
const newClis = data.clis;
|
|
853
|
+
doc.clis = { ...existingClis, ...newClis };
|
|
854
|
+
writeFileSync(configPath, stringifyYaml(doc));
|
|
855
|
+
console.log(`${GREEN}✓ Updated ${configPath}${RESET}`);
|
|
856
|
+
console.log(`${DIM} Original backed up to ${backupPath}${RESET}`);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
console.error(`Could not find ${configPath}. Printing YAML instead:\n`);
|
|
860
|
+
console.log(yaml);
|
|
861
|
+
}
|
|
862
|
+
process.exit(0);
|
|
863
|
+
}
|
|
864
|
+
console.log(`\n${BOLD}${CYAN}# Paste into your airlock.yaml:${RESET}\n`);
|
|
865
|
+
console.log(yaml);
|
|
866
|
+
console.log();
|
|
867
|
+
}
|
|
868
|
+
//# sourceMappingURL=cli.js.map
|