aliasmate 2.0.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +43 -10
  3. package/dist/cli/completion.js +4 -4
  4. package/dist/cli/completion.js.map +1 -1
  5. package/dist/cli/manage.d.ts +2 -3
  6. package/dist/cli/manage.d.ts.map +1 -1
  7. package/dist/cli/manage.js +44 -26
  8. package/dist/cli/manage.js.map +1 -1
  9. package/dist/cli/run.d.ts.map +1 -1
  10. package/dist/cli/run.js +14 -12
  11. package/dist/cli/run.js.map +1 -1
  12. package/dist/cli/save.d.ts +7 -8
  13. package/dist/cli/save.d.ts.map +1 -1
  14. package/dist/cli/save.js +71 -33
  15. package/dist/cli/save.js.map +1 -1
  16. package/dist/cli/transfer.d.ts +1 -0
  17. package/dist/cli/transfer.d.ts.map +1 -1
  18. package/dist/cli/transfer.js +40 -59
  19. package/dist/cli/transfer.js.map +1 -1
  20. package/dist/core/commands.d.ts +2 -0
  21. package/dist/core/commands.d.ts.map +1 -1
  22. package/dist/core/commands.js +40 -2
  23. package/dist/core/commands.js.map +1 -1
  24. package/dist/core/transfer.d.ts +20 -0
  25. package/dist/core/transfer.d.ts.map +1 -0
  26. package/dist/core/transfer.js +110 -0
  27. package/dist/core/transfer.js.map +1 -0
  28. package/dist/index.js +14 -11
  29. package/dist/index.js.map +1 -1
  30. package/dist/ui/format.d.ts +4 -1
  31. package/dist/ui/format.d.ts.map +1 -1
  32. package/dist/ui/format.js +61 -18
  33. package/dist/ui/format.js.map +1 -1
  34. package/dist/ui/interactive.d.ts +59 -3
  35. package/dist/ui/interactive.d.ts.map +1 -1
  36. package/dist/ui/interactive.js +604 -94
  37. package/dist/ui/interactive.js.map +1 -1
  38. package/dist/ui/onboarding.d.ts.map +1 -1
  39. package/dist/ui/onboarding.js +7 -7
  40. package/dist/ui/onboarding.js.map +1 -1
  41. package/dist/ui/prompts.d.ts +1 -12
  42. package/dist/ui/prompts.d.ts.map +1 -1
  43. package/dist/ui/prompts.js +45 -91
  44. package/dist/ui/prompts.js.map +1 -1
  45. package/dist/ui/table.d.ts +18 -0
  46. package/dist/ui/table.d.ts.map +1 -0
  47. package/dist/ui/table.js +54 -0
  48. package/dist/ui/table.js.map +1 -0
  49. package/dist/ui/theme.d.ts +8 -6
  50. package/dist/ui/theme.d.ts.map +1 -1
  51. package/dist/ui/theme.js +33 -18
  52. package/dist/ui/theme.js.map +1 -1
  53. package/package.json +7 -7
@@ -32,116 +32,626 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Tui = void 0;
37
+ exports.fuzzyMatch = fuzzyMatch;
39
38
  exports.interactiveHome = interactiveHome;
40
- const inquirer_1 = __importDefault(require("inquirer"));
39
+ const readline = __importStar(require("readline"));
41
40
  const commands_1 = require("../core/commands");
42
41
  const recent_1 = require("../core/recent");
42
+ const transfer_1 = require("../core/transfer");
43
+ const env_1 = require("../core/env");
43
44
  const format_1 = require("./format");
44
45
  const theme_1 = require("./theme");
45
- function banner(commandCount, totalRuns) {
46
- console.log();
47
- console.log(theme_1.theme.brand(' ⚡ AliasMate'));
48
- console.log(theme_1.theme.dim(` ${commandCount} saved commands · ${totalRuns} total runs`));
49
- console.log();
46
+ /** Simple subsequence fuzzy match: every query char appears in order. */
47
+ function fuzzyMatch(query, text) {
48
+ const q = query.toLowerCase();
49
+ const t = text.toLowerCase();
50
+ let i = 0;
51
+ for (const ch of t) {
52
+ if (ch === q[i])
53
+ i++;
54
+ if (i === q.length)
55
+ return true;
56
+ }
57
+ return q.length === 0;
58
+ }
59
+ /** Shared caret editing for form fields and single-line inputs. */
60
+ function editText(field, str, key) {
61
+ if (key.name === 'left') {
62
+ field.cursor = Math.max(0, field.cursor - 1);
63
+ }
64
+ else if (key.name === 'right') {
65
+ field.cursor = Math.min(field.value.length, field.cursor + 1);
66
+ }
67
+ else if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
68
+ field.cursor = 0;
69
+ }
70
+ else if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
71
+ field.cursor = field.value.length;
72
+ }
73
+ else if (key.ctrl && key.name === 'u') {
74
+ field.value = field.value.slice(field.cursor);
75
+ field.cursor = 0;
76
+ }
77
+ else if (key.name === 'backspace') {
78
+ if (field.cursor > 0) {
79
+ field.value = field.value.slice(0, field.cursor - 1) + field.value.slice(field.cursor);
80
+ field.cursor -= 1;
81
+ }
82
+ }
83
+ else if (key.name === 'delete') {
84
+ field.value = field.value.slice(0, field.cursor) + field.value.slice(field.cursor + 1);
85
+ }
86
+ else if (str && str.length === 1 && !key.ctrl && !key.meta && str >= ' ') {
87
+ field.value = field.value.slice(0, field.cursor) + str + field.value.slice(field.cursor);
88
+ field.cursor += 1;
89
+ }
90
+ else {
91
+ return false;
92
+ }
93
+ return true;
50
94
  }
51
- async function pickCommand(message) {
95
+ const ALT_SCREEN_ON = '\x1b[?1049h\x1b[?25l';
96
+ const ALT_SCREEN_OFF = '\x1b[?1049l\x1b[?25h';
97
+ const CLEAR = '\x1b[2J\x1b[H';
98
+ function loadRows() {
52
99
  const commands = (0, commands_1.listCommands)();
53
- const counts = new Map((0, recent_1.getUsageStats)().map((s) => [s.name, s.runCount]));
54
- const names = Object.keys(commands).sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || a.localeCompare(b));
55
- if (names.length === 0)
56
- return null;
57
- const { picked } = await inquirer_1.default.prompt([
58
- {
59
- type: 'list',
60
- name: 'picked',
61
- message,
62
- pageSize: 12,
63
- loop: false,
64
- choices: [
65
- ...names.map((name) => {
66
- const runs = counts.get(name) ?? 0;
67
- const runsLabel = runs > 0 ? theme_1.theme.dim(` · ${theme_1.icons.fire} ${runs}`) : '';
68
- return {
69
- name: `${name.padEnd(18)} ${theme_1.theme.dim((0, format_1.truncate)(commands[name].command, 50))}${runsLabel}`,
70
- value: name,
71
- short: name,
72
- };
73
- }),
74
- new inquirer_1.default.Separator(),
75
- { name: theme_1.theme.dim('← Back'), value: '__back__' },
76
- ],
77
- },
78
- ]);
79
- return picked === '__back__' ? null : picked;
100
+ const stats = (0, recent_1.getUsageStats)();
101
+ const counts = new Map(stats.map((s) => [s.name, s.runCount]));
102
+ const lastRuns = new Map(stats.map((s) => [s.name, s.lastRunAt]));
103
+ return Object.keys(commands)
104
+ .sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || a.localeCompare(b))
105
+ .map((name) => ({
106
+ name,
107
+ cmd: commands[name],
108
+ runs: counts.get(name) ?? 0,
109
+ lastRun: lastRuns.get(name),
110
+ }));
80
111
  }
81
- /**
82
- * The zero-args experience: a menu that makes running your most-used
83
- * commands a two-keystroke habit.
84
- */
85
- async function interactiveHome() {
86
- // Lazy imports keep the plain-command startup path fast.
87
- const { runHandler } = await Promise.resolve().then(() => __importStar(require('../cli/run')));
88
- const { saveHandler } = await Promise.resolve().then(() => __importStar(require('../cli/save')));
89
- const { editHandler, deleteHandler, statsHandler } = await Promise.resolve().then(() => __importStar(require('../cli/manage')));
90
- for (;;) {
91
- const commands = (0, commands_1.listCommands)();
92
- const stats = (0, recent_1.getUsageStats)();
93
- const totalRuns = stats.reduce((sum, s) => sum + s.runCount, 0);
94
- banner(Object.keys(commands).length, totalRuns);
95
- const hasCommands = Object.keys(commands).length > 0;
96
- const { action } = await inquirer_1.default.prompt([
97
- {
98
- type: 'list',
99
- name: 'action',
100
- message: 'What do you want to do?',
101
- loop: false,
102
- choices: [
103
- ...(hasCommands ? [{ name: `${theme_1.icons.run} Run a command`, value: 'run' }] : []),
104
- { name: `${theme_1.icons.spark} Save a new command`, value: 'save' },
105
- ...(hasCommands
106
- ? [
107
- { name: '✏️ Edit a command', value: 'edit' },
108
- { name: '🗑 Delete a command', value: 'delete' },
109
- { name: '📊 View stats', value: 'stats' },
110
- ]
111
- : []),
112
- new inquirer_1.default.Separator(),
113
- { name: theme_1.theme.dim('Quit'), value: 'quit' },
114
- ],
115
- },
116
- ]);
112
+ class Tui {
113
+ constructor(out = (t) => process.stdout.write(t)) {
114
+ this.out = out;
115
+ this.mode = 'browse';
116
+ this.filter = '';
117
+ this.selected = 0;
118
+ this.rows = loadRows();
119
+ this.form = null;
120
+ this.input = null;
121
+ this.message = '';
122
+ this.resolve = null;
123
+ }
124
+ get width() {
125
+ return process.stdout.columns || 100;
126
+ }
127
+ get height() {
128
+ return process.stdout.rows || 30;
129
+ }
130
+ visibleRows() {
131
+ if (!this.filter)
132
+ return this.rows;
133
+ // Name matches rank above matches in the command text or directory.
134
+ const byName = this.rows.filter((r) => fuzzyMatch(this.filter, r.name));
135
+ const byContent = this.rows.filter((r) => !byName.includes(r) && fuzzyMatch(this.filter, `${r.cmd.command} ${r.cmd.directory}`));
136
+ return [...byName, ...byContent];
137
+ }
138
+ refresh() {
139
+ this.rows = loadRows();
140
+ this.selected = Math.min(this.selected, Math.max(0, this.visibleRows().length - 1));
141
+ }
142
+ openForm(seed = {}) {
143
+ this.form = {
144
+ fields: [
145
+ { key: 'name', label: 'name', value: seed.name ?? '' },
146
+ { key: 'command', label: 'command', value: seed.command ?? '' },
147
+ { key: 'directory', label: 'directory', value: seed.directory ?? process.cwd() },
148
+ ].map((f) => ({ ...f, cursor: f.value.length })),
149
+ pathMode: seed.pathMode ?? 'saved',
150
+ envChoice: seed.env && Object.keys(seed.env).length > 0 ? 'keep' : 'none',
151
+ keepEnv: seed.env,
152
+ editing: seed.editing,
153
+ active: seed.editing || seed.name ? 1 : 0,
154
+ error: '',
155
+ };
156
+ this.mode = 'form';
157
+ }
158
+ // --- rendering -------------------------------------------------------------
159
+ header(subtitle) {
160
+ const left = `${theme_1.icons.dot} ${theme_1.theme.heading('aliasmate')} ${theme_1.theme.dim(subtitle)}`;
161
+ const totalRuns = this.rows.reduce((s, r) => s + r.runs, 0);
162
+ const rightText = `${this.rows.length} commands · ${totalRuns} runs`;
163
+ const leftText = `● aliasmate ${subtitle}`;
164
+ const pad = Math.max(1, this.width - leftText.length - rightText.length - 3);
165
+ return ['', ` ${left}${' '.repeat(pad)}${theme_1.theme.faint(rightText)}`, ''];
166
+ }
167
+ listPane(maxLines) {
168
+ const rows = this.visibleRows();
169
+ if (rows.length === 0) {
170
+ return this.rows.length === 0
171
+ ? [
172
+ '',
173
+ theme_1.theme.dim(' No commands yet — press ') +
174
+ theme_1.theme.accent('n') +
175
+ theme_1.theme.dim(' to create your first one.'),
176
+ ]
177
+ : ['', theme_1.theme.dim(` Nothing matches "${this.filter}"`)];
178
+ }
179
+ const nameW = Math.min(Math.max(...rows.map((r) => r.name.length), 4) + 1, 22);
180
+ const cmdW = Math.max(20, Math.min(56, this.width - nameW - 30));
181
+ const start = Math.max(0, Math.min(this.selected - Math.floor(maxLines / 2), rows.length - maxLines));
182
+ return rows.slice(start, start + maxLines).map((row, i) => {
183
+ const index = start + i;
184
+ const active = index === this.selected;
185
+ const marker = active ? theme_1.theme.accent('›') : ' ';
186
+ const name = active
187
+ ? theme_1.theme.selected(row.name.padEnd(nameW))
188
+ : theme_1.theme.name(row.name.padEnd(nameW));
189
+ const cmd = (0, format_1.truncate)(row.cmd.command, cmdW).padEnd(cmdW + 1);
190
+ const runs = row.runs > 0 ? theme_1.theme.dim(`${theme_1.icons.fire} ${row.runs}`) : theme_1.theme.faint('·');
191
+ return ` ${marker} ${name} ${active ? cmd : theme_1.theme.dim(cmd)} ${runs}`;
192
+ });
193
+ }
194
+ detailPane() {
195
+ const row = this.visibleRows()[this.selected];
196
+ if (!row)
197
+ return [];
198
+ const label = (t) => theme_1.theme.faint(t.padEnd(11));
199
+ const where = (row.cmd.pathMode ?? 'saved') === 'current'
200
+ ? 'current directory'
201
+ : (0, format_1.prettyPath)(row.cmd.directory);
202
+ const env = Object.entries(row.cmd.env ?? {})
203
+ .map(([k, v]) => `${k}=${(0, env_1.isSensitive)(k) ? (0, env_1.maskValue)(v) : v}`)
204
+ .join(theme_1.theme.faint(' · '));
205
+ const lines = [
206
+ theme_1.theme.faint(` ${'─'.repeat(Math.max(10, this.width - 2))}`),
207
+ ` ${label('command')}${(0, format_1.truncate)(row.cmd.command, this.width - 14)}`,
208
+ ` ${label('where')}${theme_1.theme.dim((0, format_1.truncate)(where, this.width - 14))}`,
209
+ ];
210
+ if (env)
211
+ lines.push(` ${label('env')}${theme_1.theme.dim((0, format_1.truncate)(env, this.width - 14))}`);
212
+ lines.push(` ${label('runs')}${theme_1.theme.dim(row.runs > 0 ? `${row.runs} · last ${(0, format_1.timeAgo)(row.lastRun)}` : 'never')}`);
213
+ return lines;
214
+ }
215
+ statsPane() {
216
+ const top = this.rows.filter((r) => r.runs > 0).slice(0, 12);
217
+ const lines = [];
218
+ if (top.length === 0) {
219
+ lines.push(theme_1.theme.dim(' Nothing run yet.'));
220
+ }
221
+ else {
222
+ const max = Math.max(...top.map((r) => r.runs));
223
+ const nameW = Math.min(Math.max(...top.map((r) => r.name.length)) + 1, 22);
224
+ for (const row of top) {
225
+ const bar = theme_1.theme.accent('▮'.repeat(Math.max(1, Math.round((row.runs / max) * 24))));
226
+ lines.push(` ${theme_1.theme.name(row.name.padEnd(nameW))} ${bar} ${theme_1.theme.dim(String(row.runs))} ${theme_1.theme.faint((0, format_1.timeAgo)(row.lastRun))}`);
227
+ }
228
+ }
229
+ return lines;
230
+ }
231
+ formPane() {
232
+ const form = this.form;
233
+ const label = (t, active) => (active ? theme_1.theme.accent('› ') : ' ') + theme_1.theme.faint(t.padEnd(11));
234
+ const lines = [];
235
+ form.fields.forEach((field, i) => {
236
+ const active = form.active === i;
237
+ const renamed = field.key === 'name' && form.editing !== undefined && field.value !== form.editing;
238
+ const value = active
239
+ ? `${field.value.slice(0, field.cursor)}${theme_1.theme.accent('▏')}${field.value.slice(field.cursor)}`
240
+ : field.value;
241
+ lines.push(` ${label(field.label, active)}${value}${renamed ? theme_1.theme.faint(` · renames ${form.editing}`) : ''}`);
242
+ });
243
+ const pmActive = form.active === 3;
244
+ const pmText = form.pathMode === 'saved'
245
+ ? 'saved directory (project command)'
246
+ : 'current directory (utility)';
247
+ lines.push(` ${label('runs in', pmActive)}${pmActive ? pmText : theme_1.theme.dim(pmText)}${pmActive ? theme_1.theme.faint(' · space to toggle') : ''}`);
248
+ const envActive = form.active === 4;
249
+ const keptCount = form.keepEnv ? Object.keys(form.keepEnv).length : 0;
250
+ const envLabels = {
251
+ none: 'none',
252
+ keep: `keep saved (${keptCount} var${keptCount === 1 ? '' : 's'})`,
253
+ capture: `capture current shell (${Object.keys((0, env_1.captureUserEnv)()).length} vars, secrets masked on export)`,
254
+ };
255
+ lines.push(` ${label('env', envActive)}${envActive ? envLabels[form.envChoice] : theme_1.theme.dim(envLabels[form.envChoice])}${envActive ? theme_1.theme.faint(' · space to cycle') : ''}`);
256
+ if (form.error) {
257
+ lines.push('');
258
+ lines.push(` ${theme_1.theme.error(form.error)}`);
259
+ }
260
+ return lines;
261
+ }
262
+ inputPane() {
263
+ const input = this.input;
264
+ const value = `${input.value.slice(0, input.cursor)}${theme_1.theme.accent('▏')}${input.value.slice(input.cursor)}`;
265
+ const lines = [` ${theme_1.theme.accent('› ')}${theme_1.theme.faint(input.label.padEnd(11))}${value}`];
266
+ if (input.kind === 'export') {
267
+ lines.push('');
268
+ lines.push(theme_1.theme.faint(' includes real secret env values — keep the file safe'));
269
+ }
270
+ if (input.error) {
271
+ lines.push('');
272
+ lines.push(` ${theme_1.theme.error(input.error)}`);
273
+ }
274
+ return lines;
275
+ }
276
+ footer() {
277
+ const key = (k, label) => `${theme_1.theme.accent(k)} ${theme_1.theme.faint(label)}`;
278
+ const sep = theme_1.theme.faint(' ');
279
+ switch (this.mode) {
280
+ case 'confirm-delete': {
281
+ const row = this.visibleRows()[this.selected];
282
+ return ` ${theme_1.theme.warning(`delete "${row?.name}"?`)} ${key('y', 'yes')}${sep}${key('n', 'no')}`;
283
+ }
284
+ case 'filter':
285
+ return ` ${theme_1.theme.accent('/')} ${this.filter}${theme_1.theme.accent('▏')} ${theme_1.theme.faint('esc clear · enter run · ↑↓ move')}`;
286
+ case 'form':
287
+ return ` ${[key('↑↓/tab', 'field'), key('←→', 'cursor'), key('enter', 'next/save'), key('esc', 'cancel')].join(sep)}`;
288
+ case 'input':
289
+ return ` ${[key('enter', this.input?.kind === 'export' ? 'export' : 'import'), key('esc', 'cancel')].join(sep)}`;
290
+ case 'stats':
291
+ return ` ${theme_1.theme.faint('any key to go back')}`;
292
+ default:
293
+ return ` ${[
294
+ key('↑↓', 'move'),
295
+ key('enter', 'run'),
296
+ key('/', 'filter'),
297
+ key('n', 'new'),
298
+ key('e', 'edit'),
299
+ key('d', 'delete'),
300
+ key('s', 'stats'),
301
+ key('x', 'export'),
302
+ key('i', 'import'),
303
+ key('q', 'quit'),
304
+ ].join(sep)}`;
305
+ }
306
+ }
307
+ render() {
308
+ const subtitle = this.mode === 'form'
309
+ ? this.form?.editing
310
+ ? `· edit ${this.form.editing}`
311
+ : '· new command'
312
+ : this.mode === 'stats'
313
+ ? '· stats'
314
+ : this.mode === 'input'
315
+ ? this.input?.kind === 'export'
316
+ ? '· export backup'
317
+ : '· import'
318
+ : '';
319
+ const out = [...this.header(subtitle)];
320
+ if (this.mode === 'stats') {
321
+ out.push(...this.statsPane());
322
+ }
323
+ else if (this.mode === 'form') {
324
+ out.push(...this.formPane());
325
+ }
326
+ else if (this.mode === 'input') {
327
+ out.push(...this.inputPane());
328
+ }
329
+ else {
330
+ const detail = this.detailPane();
331
+ const listLines = Math.max(3, this.height - out.length - detail.length - 4);
332
+ out.push(...this.listPane(listLines));
333
+ out.push('');
334
+ out.push(...detail);
335
+ }
336
+ while (out.length < this.height - 2)
337
+ out.push('');
338
+ out.push(this.message ? ` ${this.message}` : '');
339
+ out.push(this.footer());
340
+ this.out(CLEAR + out.slice(0, this.height).join('\n'));
341
+ this.message = '';
342
+ }
343
+ // --- input -----------------------------------------------------------------
344
+ /** Clear the filter but keep the same row highlighted. */
345
+ clearFilter() {
346
+ const current = this.visibleRows()[this.selected]?.name;
347
+ this.filter = '';
348
+ const index = current ? this.visibleRows().findIndex((r) => r.name === current) : -1;
349
+ this.selected = Math.max(0, index);
350
+ }
351
+ move(delta) {
352
+ const count = this.visibleRows().length;
353
+ if (count === 0)
354
+ return;
355
+ this.selected = (this.selected + delta + count) % count;
356
+ }
357
+ openInput(kind) {
358
+ const today = new Date().toISOString().slice(0, 10);
359
+ const value = kind === 'export' ? `~/aliasmate-backup-${today}.json` : '~/';
360
+ this.input = { kind, label: 'file', value, cursor: value.length, error: '' };
361
+ this.mode = 'input';
362
+ }
363
+ handleInputKey(str, key) {
364
+ const input = this.input;
365
+ if (key.name === 'escape') {
366
+ this.input = null;
367
+ this.mode = 'browse';
368
+ this.message = theme_1.theme.faint('cancelled');
369
+ return this.render();
370
+ }
371
+ if (key.name === 'return')
372
+ return this.submitInput();
373
+ if (editText(input, str, key))
374
+ input.error = '';
375
+ this.render();
376
+ }
377
+ submitInput() {
378
+ const input = this.input;
379
+ const file = input.value.trim();
380
+ if (!file) {
381
+ input.error = 'File path cannot be empty';
382
+ return this.render();
383
+ }
384
+ try {
385
+ if (input.kind === 'export') {
386
+ const count = (0, transfer_1.exportToFile)(file, { full: true });
387
+ this.message = `${theme_1.icons.ok} ${theme_1.theme.dim(`exported ${count} command${count === 1 ? '' : 's'} to ${file}`)}`;
388
+ }
389
+ else {
390
+ const result = (0, transfer_1.importFromFile)(file);
391
+ this.refresh();
392
+ const extras = [
393
+ result.skipped.length > 0 ? `${result.skipped.length} skipped (already exist)` : '',
394
+ result.invalid.length > 0 ? `${result.invalid.length} invalid` : '',
395
+ ]
396
+ .filter(Boolean)
397
+ .join(' · ');
398
+ this.message = `${theme_1.icons.ok} ${theme_1.theme.dim(`imported ${result.imported}${extras ? ` · ${extras}` : ''}`)}`;
399
+ }
400
+ this.input = null;
401
+ this.mode = 'browse';
402
+ }
403
+ catch (error) {
404
+ input.error = error.message;
405
+ }
406
+ this.render();
407
+ }
408
+ submitForm() {
409
+ const form = this.form;
410
+ const [name, command, directory] = form.fields.map((f) => f.value.trim());
411
+ const renaming = form.editing !== undefined && name !== form.editing;
412
+ const nameCheck = (0, commands_1.validateCommandName)(name);
413
+ if (nameCheck !== true) {
414
+ form.error = nameCheck;
415
+ form.active = 0;
416
+ return this.render();
417
+ }
418
+ if ((!form.editing || renaming) && (0, commands_1.commandExists)(name)) {
419
+ form.error = `"${name}" already exists — pick another name or edit it instead`;
420
+ form.active = 0;
421
+ return this.render();
422
+ }
423
+ if (!command) {
424
+ form.error = 'Command cannot be empty';
425
+ form.active = 1;
426
+ return this.render();
427
+ }
428
+ if (!directory) {
429
+ form.error = 'Directory cannot be empty';
430
+ form.active = 2;
431
+ return this.render();
432
+ }
433
+ const env = form.envChoice === 'none'
434
+ ? undefined
435
+ : form.envChoice === 'keep'
436
+ ? form.keepEnv
437
+ : (0, env_1.captureUserEnv)();
117
438
  try {
118
- if (action === 'quit')
119
- return;
120
- if (action === 'save')
121
- await saveHandler({});
122
- if (action === 'stats')
123
- statsHandler();
124
- if (action === 'run') {
125
- const name = await pickCommand('Run which command?');
126
- if (name) {
127
- await runHandler(name, undefined, {});
128
- return; // Hand the terminal back after a run.
439
+ if (renaming)
440
+ (0, commands_1.renameCommand)(form.editing, name);
441
+ (0, commands_1.saveCommand)({ name, command, directory, pathMode: form.pathMode, env });
442
+ this.message = `${theme_1.icons.ok} ${theme_1.theme.dim(renaming ? `renamed ${form.editing} → ${name}` : `saved ${name}`)}`;
443
+ const finalName = name;
444
+ this.form = null;
445
+ this.mode = 'browse';
446
+ this.refresh();
447
+ const index = this.visibleRows().findIndex((r) => r.name === finalName);
448
+ if (index >= 0)
449
+ this.selected = index;
450
+ }
451
+ catch (error) {
452
+ form.error = error.message;
453
+ }
454
+ this.render();
455
+ }
456
+ handleFormKey(str, key) {
457
+ const form = this.form;
458
+ const fieldCount = 5;
459
+ const isTextField = form.active < 3;
460
+ if (key.name === 'escape') {
461
+ this.form = null;
462
+ this.mode = 'browse';
463
+ this.message = theme_1.theme.faint('cancelled');
464
+ return this.render();
465
+ }
466
+ if (key.name === 'return') {
467
+ if (form.active < fieldCount - 1) {
468
+ form.active += 1;
469
+ }
470
+ else {
471
+ return this.submitForm();
472
+ }
473
+ return this.render();
474
+ }
475
+ if (key.name === 'up' || (key.name === 'tab' && key.shift)) {
476
+ form.active = (form.active - 1 + fieldCount) % fieldCount;
477
+ return this.render();
478
+ }
479
+ if (key.name === 'down' || key.name === 'tab') {
480
+ form.active = (form.active + 1) % fieldCount;
481
+ return this.render();
482
+ }
483
+ if (isTextField) {
484
+ editText(form.fields[form.active], str, key);
485
+ form.error = '';
486
+ return this.render();
487
+ }
488
+ if (key.name === 'space' || key.name === 'left' || key.name === 'right') {
489
+ if (form.active === 3) {
490
+ form.pathMode = form.pathMode === 'saved' ? 'current' : 'saved';
491
+ }
492
+ else {
493
+ const cycle = form.keepEnv ? ['keep', 'capture', 'none'] : ['none', 'capture'];
494
+ const next = (cycle.indexOf(form.envChoice) + 1) % cycle.length;
495
+ form.envChoice = cycle[next];
496
+ }
497
+ return this.render();
498
+ }
499
+ this.render();
500
+ }
501
+ handleKey(str, key) {
502
+ if (key.ctrl && key.name === 'c')
503
+ return this.finish(null);
504
+ if (this.mode === 'form')
505
+ return this.handleFormKey(str, key);
506
+ if (this.mode === 'input')
507
+ return this.handleInputKey(str, key);
508
+ if (this.mode === 'stats') {
509
+ this.mode = 'browse';
510
+ return this.render();
511
+ }
512
+ if (this.mode === 'confirm-delete') {
513
+ if (str === 'y') {
514
+ const row = this.visibleRows()[this.selected];
515
+ if (row) {
516
+ (0, commands_1.deleteCommand)(row.name);
517
+ this.message = `${theme_1.icons.ok} ${theme_1.theme.dim(`deleted ${row.name}`)}`;
518
+ this.refresh();
129
519
  }
130
520
  }
131
- if (action === 'edit') {
132
- const name = await pickCommand('Edit which command?');
133
- if (name)
134
- await editHandler(name, {});
521
+ this.mode = 'browse';
522
+ return this.render();
523
+ }
524
+ if (this.mode === 'filter') {
525
+ if (key.name === 'escape') {
526
+ this.clearFilter();
527
+ this.mode = 'browse';
528
+ }
529
+ else if (key.name === 'return') {
530
+ this.mode = 'browse';
531
+ return this.selectRun();
532
+ }
533
+ else if (key.name === 'backspace') {
534
+ this.filter = this.filter.slice(0, -1);
135
535
  }
136
- if (action === 'delete') {
137
- const name = await pickCommand('Delete which command?');
138
- if (name)
139
- await deleteHandler(name, {});
536
+ else if (key.name === 'up')
537
+ this.move(-1);
538
+ else if (key.name === 'down')
539
+ this.move(1);
540
+ else if (str && str.length === 1 && !key.ctrl && !key.meta) {
541
+ this.filter += str;
542
+ this.selected = 0;
140
543
  }
544
+ return this.render();
141
545
  }
142
- catch (error) {
143
- console.error(theme_1.theme.error(`\n${error.message}`));
546
+ // browse mode
547
+ switch (key.name) {
548
+ case 'up':
549
+ case 'k':
550
+ this.move(-1);
551
+ break;
552
+ case 'down':
553
+ case 'j':
554
+ this.move(1);
555
+ break;
556
+ case 'return':
557
+ return this.selectRun();
558
+ case 'escape':
559
+ if (this.filter) {
560
+ this.clearFilter();
561
+ break;
562
+ }
563
+ return this.finish(null);
564
+ default:
565
+ if (str === '/')
566
+ this.mode = 'filter';
567
+ else if (str === 'q')
568
+ return this.finish(null);
569
+ else if (str === 'n')
570
+ this.openForm();
571
+ else if (str === 'e') {
572
+ const row = this.visibleRows()[this.selected];
573
+ if (row) {
574
+ this.openForm({
575
+ name: row.name,
576
+ command: row.cmd.command,
577
+ directory: row.cmd.directory,
578
+ pathMode: row.cmd.pathMode ?? 'saved',
579
+ env: row.cmd.env,
580
+ editing: row.name,
581
+ });
582
+ }
583
+ }
584
+ else if (str === 'd' && this.visibleRows().length > 0)
585
+ this.mode = 'confirm-delete';
586
+ else if (str === 's')
587
+ this.mode = 'stats';
588
+ else if (str === 'x')
589
+ this.openInput('export');
590
+ else if (str === 'i')
591
+ this.openInput('import');
144
592
  }
593
+ this.render();
594
+ }
595
+ selectRun() {
596
+ const row = this.visibleRows()[this.selected];
597
+ if (!row) {
598
+ this.message = theme_1.theme.dim('nothing selected');
599
+ return this.render();
600
+ }
601
+ this.finish({ kind: 'run', name: row.name });
602
+ }
603
+ finish(action) {
604
+ const resolve = this.resolve;
605
+ this.resolve = null;
606
+ resolve?.(action);
607
+ }
608
+ /** Show the TUI until the user runs a command or quits (null). */
609
+ waitForAction() {
610
+ return new Promise((resolve) => {
611
+ this.resolve = resolve;
612
+ this.render();
613
+ });
614
+ }
615
+ }
616
+ exports.Tui = Tui;
617
+ function enterScreen(onKey) {
618
+ process.stdout.write(ALT_SCREEN_ON);
619
+ readline.emitKeypressEvents(process.stdin);
620
+ if (process.stdin.isTTY)
621
+ process.stdin.setRawMode(true);
622
+ process.stdin.on('keypress', onKey);
623
+ process.stdin.resume();
624
+ return () => {
625
+ process.stdin.off('keypress', onKey);
626
+ if (process.stdin.isTTY)
627
+ process.stdin.setRawMode(false);
628
+ process.stdin.pause();
629
+ process.stdout.write(ALT_SCREEN_OFF);
630
+ };
631
+ }
632
+ /**
633
+ * The full-screen home: everything (browse, create, edit, delete, stats)
634
+ * happens in the TUI. Only running a command hands the terminal back so
635
+ * output lands in normal scrollback.
636
+ */
637
+ async function interactiveHome(seed) {
638
+ const tui = new Tui();
639
+ if (seed)
640
+ tui.openForm(seed);
641
+ const onResize = () => tui.render();
642
+ const leave = enterScreen((s, k) => tui.handleKey(s, k));
643
+ process.stdout.on('resize', onResize);
644
+ let action;
645
+ try {
646
+ action = await tui.waitForAction();
647
+ }
648
+ finally {
649
+ process.stdout.off('resize', onResize);
650
+ leave();
651
+ }
652
+ if (action?.kind === 'run') {
653
+ const { runHandler } = await Promise.resolve().then(() => __importStar(require('../cli/run')));
654
+ await runHandler(action.name, undefined, {});
145
655
  }
146
656
  }
147
657
  //# sourceMappingURL=interactive.js.map