codeply-cli 0.2.0 → 0.3.1

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 (43) hide show
  1. package/THIRD_PARTY_NOTICES.md +63 -0
  2. package/bin/codeply.js +558 -370
  3. package/lib/agent.mjs +1627 -160
  4. package/lib/ai.js +1107 -387
  5. package/lib/apply-limit.js +33 -51
  6. package/lib/apply-patch.js +217 -0
  7. package/lib/arity-table.json +138 -0
  8. package/lib/arity.js +70 -0
  9. package/lib/auth.js +118 -112
  10. package/lib/commands.js +103 -0
  11. package/lib/config.js +392 -136
  12. package/lib/design-library/artifact-template.html +500 -0
  13. package/lib/design-library/assemble-artifact.mjs +21 -0
  14. package/lib/design-library/build.mjs +226 -0
  15. package/lib/design-library/cleanup.mjs +45 -0
  16. package/lib/design-library/index.json +22380 -0
  17. package/lib/design-library/make-table-data.mjs +39 -0
  18. package/lib/design-library/query.mjs +104 -0
  19. package/lib/diagnostics.js +192 -0
  20. package/lib/edit-engine.js +171 -14
  21. package/lib/github-agent.mjs +307 -0
  22. package/lib/history.js +75 -0
  23. package/lib/mcp.js +258 -0
  24. package/lib/native-tools.mjs +189 -0
  25. package/lib/oauth-connectors.js +478 -0
  26. package/lib/permissions.js +109 -0
  27. package/lib/plugins.js +353 -0
  28. package/lib/render-md.mjs +617 -617
  29. package/lib/server.mjs +457 -0
  30. package/lib/session-db.js +220 -0
  31. package/lib/share.js +168 -0
  32. package/lib/skills.js +80 -22
  33. package/lib/snapshot.js +146 -0
  34. package/lib/subagents.js +164 -0
  35. package/lib/terminal-hints.js +154 -0
  36. package/lib/textarea.mjs +258 -258
  37. package/lib/tools.mjs +1658 -79
  38. package/lib/tui.js +7 -7
  39. package/lib/tui.mjs +1086 -992
  40. package/lib/typecheck-worker.js +147 -0
  41. package/lib/web-tools.js +158 -0
  42. package/package.json +4 -3
  43. package/skills/premium-web-design/SKILL.md +154 -0
package/bin/codeply.js CHANGED
@@ -1,370 +1,558 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Codeply CLI — apply an AI instruction to a file from the terminal.
4
- *
5
- * Single-shot by design (v1): one instruction, one file, one verified diff,
6
- * one write. No multi-file auto-detection, no conversation memory, no local
7
- * response cache — those are desktop-app features that can come later if
8
- * this is worth building out further.
9
- */
10
- const fs = require('fs');
11
- const path = require('path');
12
- const readline = require('readline/promises');
13
- const { Command } = require('commander');
14
-
15
- const auth = require('../lib/auth');
16
- const ai = require('../lib/ai');
17
- const editEngine = require('../lib/edit-engine');
18
- const applyLimit = require('../lib/apply-limit');
19
- const config = require('../lib/config');
20
-
21
- // Google brand palette
22
- const c = {
23
- dim: (s) => `\x1b[2m${s}\x1b[0m`,
24
- red: (s) => `\x1b[38;2;234;67;53m${s}\x1b[0m`, // #EA4335
25
- green: (s) => `\x1b[38;2;52;168;83m${s}\x1b[0m`, // #34A853
26
- bold: (s) => `\x1b[1m${s}\x1b[0m`,
27
- blue: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // #4285F4
28
- yellow: (s) => `\x1b[38;2;251;188;5m${s}\x1b[0m`, // #FBBC05
29
- accent: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // primary = blue
30
- secondary: (s) => `\x1b[38;2;154;160;166m${s}\x1b[0m`,
31
- muted: (s) => `\x1b[38;2;95;99;104m${s}\x1b[0m`,
32
- text: (s) => `\x1b[38;2;232;234;237m${s}\x1b[0m`,
33
- };
34
-
35
- async function confirm(question) {
36
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
- try {
38
- const answer = (await rl.question(`${question} (y/N) `)).trim().toLowerCase();
39
- return answer === 'y' || answer === 'yes';
40
- } finally {
41
- rl.close();
42
- }
43
- }
44
-
45
- function printEditPreview(edits) {
46
- edits.forEach((e, i) => {
47
- console.log(c.muted(`\n── edit ${i + 1}/${edits.length} ──`));
48
- e.search.split('\n').forEach(l => console.log(c.red(`- ${l}`)));
49
- e.replace.split('\n').forEach(l => console.log(c.green(`+ ${l}`)));
50
- });
51
- console.log('');
52
- }
53
-
54
- async function runApply(instruction, opts) {
55
- const session = await auth.getSession();
56
- if (!session) {
57
- console.error('Not signed in. Run `codeply login` first.');
58
- process.exitCode = 1;
59
- return;
60
- }
61
-
62
- const filePath = path.resolve(process.cwd(), opts.file);
63
- if (!fs.existsSync(filePath)) {
64
- console.error(`File not found: ${filePath}`);
65
- process.exitCode = 1;
66
- return;
67
- }
68
-
69
- // The shared 100/day cap only applies when spending the Codeply proxy —
70
- // Ollama (local or the user's own cloud key) doesn't touch that account.
71
- const usingCodeply = config.getConfig().provider === 'codeply';
72
- if (usingCodeply) {
73
- const limit = await applyLimit.checkApplyLimit();
74
- if (!limit.allowed) {
75
- console.error(limit.error || `Daily apply limit reached (${limit.count}/${limit.limit}). This is shared with the Codeply desktop app and resets at midnight UTC.`);
76
- console.error(c.muted(config.byokHint()));
77
- process.exitCode = 1;
78
- return;
79
- }
80
- }
81
-
82
- console.log(c.secondary(`Asking Codeply about ${path.basename(filePath)}…`));
83
- const result = await editEngine.computeInstructionEdits(instruction, filePath);
84
-
85
- if (!result.success) {
86
- console.error(c.red(`Failed: ${result.error}`));
87
- process.exitCode = 1;
88
- return;
89
- }
90
- if (!result.edits.length) {
91
- console.log(result.reason || 'No edits produced — the instruction may be too ambiguous or too large for a targeted change.');
92
- return;
93
- }
94
-
95
- printEditPreview(result.edits);
96
- if (result.badCount) {
97
- console.log(c.muted(`Note: ${result.badCount} proposed change(s) couldn't be matched against the file and were skipped.`));
98
- }
99
- console.log(c.muted(`${result.reason || ''}${result.reason ? ' — ' : ''}confidence ${result.confidence ?? '?'}%, ${result.tokensUsed || 0} tokens, model ${result.modelUsed || 'unknown'}`));
100
-
101
- if (opts.dryRun) {
102
- console.log(c.muted('\n(dry run — nothing written)'));
103
- return;
104
- }
105
-
106
- if (!opts.yes) {
107
- const ok = await confirm(c.bold(c.accent(`Apply ${result.edits.length} edit(s) to ${path.basename(filePath)}?`)));
108
- if (!ok) { console.log(c.secondary('Cancelled.')); return; }
109
- }
110
-
111
- const fileContent = fs.readFileSync(filePath, 'utf8');
112
- const applied = editEngine.applyEditsToContent(fileContent, result.edits);
113
- if (!applied.ok) {
114
- console.error(`Could not apply edit ${applied.index + 1}: ${applied.error}`);
115
- process.exitCode = 1;
116
- return;
117
- }
118
-
119
- fs.writeFileSync(filePath, applied.content);
120
- if (usingCodeply) {
121
- const linesAdded = result.edits.reduce((n, e) => n + e.replace.split('\n').length, 0);
122
- const linesRemoved = result.edits.reduce((n, e) => n + e.search.split('\n').length, 0);
123
- await applyLimit.recordApplyEvent(filePath, linesAdded, linesRemoved);
124
- }
125
- console.log(c.green(`✓ Applied to ${filePath}`));
126
- }
127
-
128
- // TUI Mode - interactive when no command is provided
129
- async function runTUI() {
130
- // Check if running in interactive terminal
131
- if (!process.stdin.isTTY) {
132
- const VERSION = require('../package.json').version;
133
- // C=blue O=red D=yellow E=green P=blue L=red Y=yellow
134
- // Fixed-width letters + trailing gap so CODEPLY reads cleanly
135
- const letterColors = [c.blue, c.red, c.yellow, c.green, c.blue, c.red, c.yellow];
136
- const logoLines = [
137
- [' ██████╗ ', ' ██████╗ ', '██████╗ ', '███████╗ ', '██████╗ ', '██╗ ', '██╗ ██╗'],
138
- ['██╔════╝ ', '██╔═══██╗', '██╔══██╗ ', '██╔════╝ ', '██╔══██╗ ', '██║ ', '╚██╗ ██╔╝'],
139
- ['██║ ', '██║ ██║', '██║ ██║ ', '█████╗ ', '██████╔╝ ', '██║ ', ' ╚████╔╝ '],
140
- ['██║ ', '██║ ██║', '██║ ██║ ', '██╔══╝ ', '██╔═══╝ ', '██║ ', ' ╚██╔╝ '],
141
- ['╚██████╗ ', '╚██████╔╝', '██████╔╝ ', '███████╗ ', '██║ ', '███████╗ ', ' ██║ '],
142
- [' ╚═════╝ ', ' ╚═════╝ ', '╚═════╝ ', '╚══════╝ ', '╚═╝ ', '╚══════╝ ', ' ╚═╝ '],
143
- ];
144
-
145
- console.log('');
146
- logoLines.forEach((line) => {
147
- console.log(line.map((seg, i) => letterColors[i](seg)).join(''));
148
- });
149
- console.log(`
150
- ${c.secondary('Apply AI instructions. Edit files. Ship faster.')} ${c.muted('v' + VERSION)}
151
-
152
- ${c.bold(c.text('Usage'))}
153
- ${c.blue('codeply')} Launch interactive TUI
154
- ${c.blue('codeply apply')} ${c.dim('<instruction>')} ${c.blue('-f')} ${c.dim('<file>')}
155
- ${c.red('codeply login')} Sign in with your Codeply account
156
- ${c.yellow('codeply logout')} Sign out
157
- ${c.green('codeply whoami')} Show current account
158
-
159
- ${c.muted('Run in a terminal for the full interactive UI · codeply --help')}
160
- `);
161
- return;
162
- }
163
-
164
- const React = await import('react');
165
- const { render } = await import('ink');
166
- const { default: App } = await import('../lib/tui.mjs');
167
-
168
- // Take the screen so the app doesn't look like output that happened to land
169
- // in a shell. Deliberately a plain clear rather than the alternate screen
170
- // buffer (\x1b[?1049h): alt-screen would discard scrollback on exit, and the
171
- // whole point of the <Static> transcript is that you can scroll back through
172
- // it afterwards.
173
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
174
-
175
- render(React.default.createElement(App), {
176
- exitOnCtrlC: false, // handled inside the app (clear draft first, then quit)
177
- });
178
- }
179
-
180
- const program = new Command();
181
- program
182
- .name('codeply')
183
- .description('Apply an AI instruction to a file from the terminal.')
184
- .version(require('../package.json').version);
185
-
186
- program
187
- .command('login')
188
- .description('Sign in with your Codeply account (email + 6-digit code).')
189
- .action(async () => { await auth.login(); });
190
-
191
- program
192
- .command('logout')
193
- .description('Sign out of the current Codeply session.')
194
- .action(async () => { await auth.logout(); });
195
-
196
- program
197
- .command('whoami')
198
- .description('Show the currently signed-in account.')
199
- .action(async () => {
200
- const session = await auth.getSession();
201
- console.log(session ? session.user.email : 'Not signed in.');
202
- });
203
-
204
- program
205
- .command('provider [name]')
206
- .description('Show or set the model backend (codeply | ollama | openrouter | groq | anthropic | openai).')
207
- .option('--key <key>', 'API key for ollama (cloud only), openrouter, groq, anthropic, or openai')
208
- .option('--model <model>', 'model name for the selected provider')
209
- .option('--host <url>', 'ollama host only (default https://ollama.com; local: http://localhost:11434)')
210
- .action(async (name, opts) => {
211
- const config = require('../lib/config');
212
-
213
- if (!name && !opts.key && !opts.model && !opts.host) {
214
- const cfg = config.getConfig();
215
- console.log(`Provider ${c.accent(config.describeProvider(cfg))}`);
216
- if (cfg.provider === 'ollama') {
217
- console.log(`Host ${cfg.ollama.host}`);
218
- console.log(`Model ${cfg.ollama.model}`);
219
- console.log(`Key ${config.maskKey(cfg.ollama.apiKey)}`);
220
- } else if (config.BYOK_PROVIDERS.includes(cfg.provider)) {
221
- console.log(`Model ${cfg[cfg.provider].model}`);
222
- console.log(`Key ${config.maskKey(cfg[cfg.provider].apiKey)}`);
223
- }
224
- console.log(c.muted(`\nConfig ${config.configPath}`));
225
- console.log(c.muted('Providers codeply, ollama, openrouter, groq, anthropic, openai'));
226
- console.log(c.muted('Set with codeply provider openrouter --key <key> [--model <model>]'));
227
- if (cfg.provider === 'codeply') {
228
- console.log(c.muted(`\n${config.byokHint()}`));
229
- }
230
- return;
231
- }
232
-
233
- if (name && !config.PROVIDERS.includes(name)) {
234
- console.error(`Unknown provider "${name}". Use one of: ${config.PROVIDERS.join(', ')}.`);
235
- process.exitCode = 1;
236
- return;
237
- }
238
-
239
- const patch = {};
240
- if (name) patch.provider = name;
241
- const target = name || config.getConfig().provider;
242
-
243
- if (target === 'ollama') {
244
- const ollama = {};
245
- if (opts.key) ollama.apiKey = opts.key;
246
- if (opts.model) ollama.model = opts.model;
247
- if (opts.host) ollama.host = opts.host;
248
- if (Object.keys(ollama).length) patch.ollama = ollama;
249
- } else if (config.BYOK_PROVIDERS.includes(target)) {
250
- const byok = {};
251
- if (opts.key) byok.apiKey = opts.key;
252
- if (opts.model) byok.model = opts.model;
253
- if (Object.keys(byok).length) patch[target] = byok;
254
- }
255
-
256
- const saved = config.saveConfig(patch);
257
- if (!saved.ok) {
258
- console.error(`Could not save config: ${saved.error}`);
259
- process.exitCode = 1;
260
- return;
261
- }
262
- console.log(c.green(`✓ Provider: ${config.describeProvider(config.getConfig())}`));
263
- console.log(c.muted(` saved to ${saved.path}`));
264
- if (opts.key) console.log(c.muted(' the key is stored outside the repo and is never committed'));
265
- });
266
-
267
- program
268
- .command('apply <instruction>')
269
- .description('Apply an instruction to a file.')
270
- .requiredOption('-f, --file <path>', 'file to edit')
271
- .option('-y, --yes', 'apply without confirmation prompt')
272
- .option('--dry-run', 'show the proposed edits without writing them')
273
- .action(runApply);
274
-
275
- const skillCmd = program
276
- .command('skill')
277
- .description('List, install, or remove agent skills (SKILL.md playbooks the agent can load mid-task).');
278
-
279
- skillCmd
280
- .command('list', { isDefault: true })
281
- .description('List every available skill.')
282
- .option('-a, --all', 'include full descriptions (default shows a compact table)')
283
- .action((opts) => {
284
- const skills = require('../lib/skills');
285
- const list = skills.listSkills();
286
- if (list.length === 0) {
287
- console.log('No skills installed.');
288
- return;
289
- }
290
- const daily = list.filter((s) => s.daily);
291
- const rest = list.filter((s) => !s.daily);
292
- console.log(c.bold(`${list.length} skills`) + c.muted(` (${daily.length} loaded by default, ${rest.length} available via list_skills or /skill install)`));
293
- console.log('');
294
- for (const s of list) {
295
- const tag = s.source === 'user' ? c.green('[user]') : s.daily ? c.blue('[daily]') : c.muted('[library]');
296
- console.log(`${s.name.padEnd(30)} ${tag}`);
297
- if (opts.all) console.log(c.muted(` ${s.description}`));
298
- }
299
- console.log('');
300
- console.log(c.muted(`Bundled from affaan-m/ECC — see skills/SOURCE.md. Add your own: codeply skill install <path-or-github-url>`));
301
- });
302
-
303
- skillCmd
304
- .command('install <source>')
305
- .description('Install a skill from a local directory (containing SKILL.md) or a GitHub URL/owner-repo.')
306
- .option('-n, --name <name>', 'name to install under (single-skill sources only; ignored for a whole-repo install)')
307
- .action(async (source, opts) => {
308
- const skills = require('../lib/skills');
309
- const isLocal = fs.existsSync(source);
310
-
311
- if (isLocal) {
312
- const result = skills.installFromLocalDir(path.resolve(process.cwd(), source), opts.name);
313
- if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
314
- console.log(c.green(`✓ Installed "${result.name}"`));
315
- console.log(c.muted(` ${result.dest}`));
316
- return;
317
- }
318
-
319
- console.log(c.secondary(`Fetching from ${source}…`));
320
- let result;
321
- try {
322
- result = await skills.installFromGitHub(source, opts.name);
323
- } catch (e) {
324
- console.error(c.red(`Install failed: ${e.message}`));
325
- process.exitCode = 1;
326
- return;
327
- }
328
- if (!result.ok) {
329
- console.error(c.red(result.error || `No skills installed from ${source}.`));
330
- process.exitCode = 1;
331
- return;
332
- }
333
- console.log(c.green(`✓ Installed ${result.installed.length} skill(s) from ${result.source}`));
334
- for (const i of result.installed) console.log(c.muted(` ${i.name} (${i.fileCount} file(s)) → ${i.dest}`));
335
- if (result.failed.length) {
336
- console.log(c.yellow(` ${result.failed.length} skipped (fetch failed):`));
337
- for (const f of result.failed) console.log(c.muted(` ${f.dir}: ${f.error}`));
338
- }
339
- });
340
-
341
- skillCmd
342
- .command('remove <name>')
343
- .description('Remove a user-installed skill. Bundled skills cannot be removed this way.')
344
- .action((name) => {
345
- const skills = require('../lib/skills');
346
- const result = skills.removeSkill(name);
347
- if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
348
- console.log(c.green(`✓ Removed "${name}"`));
349
- });
350
-
351
- skillCmd
352
- .command('show <name>')
353
- .description('Print a skill\'s full instructions.')
354
- .action((name) => {
355
- const skills = require('../lib/skills');
356
- const body = skills.loadSkillBody(name);
357
- if (body == null) {
358
- console.error(c.red(`No skill named "${name}". Run \`codeply skill list\` to see what's available.`));
359
- process.exitCode = 1;
360
- return;
361
- }
362
- console.log(body);
363
- });
364
-
365
- // If no arguments provided, launch TUI
366
- if (process.argv.length <= 2) {
367
- runTUI();
368
- } else {
369
- program.parseAsync(process.argv);
370
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codeply CLI - apply an AI instruction to a file from the terminal.
4
+ *
5
+ * Single-shot by design (v1): one instruction, one file, one verified diff,
6
+ * one write. No multi-file auto-detection, no conversation memory, no local
7
+ * response cache - those are desktop-app features that can come later if
8
+ * this is worth building out further.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const readline = require('readline/promises');
13
+ const { Command } = require('commander');
14
+
15
+ const auth = require('../lib/auth');
16
+ const ai = require('../lib/ai');
17
+ const editEngine = require('../lib/edit-engine');
18
+ const applyLimit = require('../lib/apply-limit');
19
+ const config = require('../lib/config');
20
+
21
+ // Google brand palette
22
+ const c = {
23
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
24
+ red: (s) => `\x1b[38;2;234;67;53m${s}\x1b[0m`, // #EA4335
25
+ green: (s) => `\x1b[38;2;52;168;83m${s}\x1b[0m`, // #34A853
26
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
27
+ blue: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // #4285F4
28
+ yellow: (s) => `\x1b[38;2;251;188;5m${s}\x1b[0m`, // #FBBC05
29
+ accent: (s) => `\x1b[38;2;66;133;244m${s}\x1b[0m`, // primary = blue
30
+ secondary: (s) => `\x1b[38;2;154;160;166m${s}\x1b[0m`,
31
+ muted: (s) => `\x1b[38;2;95;99;104m${s}\x1b[0m`,
32
+ text: (s) => `\x1b[38;2;232;234;237m${s}\x1b[0m`,
33
+ };
34
+
35
+ async function confirm(question) {
36
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
+ try {
38
+ const answer = (await rl.question(`${question} (y/N) `)).trim().toLowerCase();
39
+ return answer === 'y' || answer === 'yes';
40
+ } finally {
41
+ rl.close();
42
+ }
43
+ }
44
+
45
+ function printEditPreview(edits) {
46
+ edits.forEach((e, i) => {
47
+ console.log(c.muted(`\n── edit ${i + 1}/${edits.length} ──`));
48
+ e.search.split('\n').forEach(l => console.log(c.red(`- ${l}`)));
49
+ e.replace.split('\n').forEach(l => console.log(c.green(`+ ${l}`)));
50
+ });
51
+ console.log('');
52
+ }
53
+
54
+ async function runApply(instruction, opts) {
55
+ const session = await auth.getSession();
56
+ if (!session) {
57
+ console.error('Not signed in. Run `codeply login` first.');
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ const filePath = path.resolve(process.cwd(), opts.file);
63
+ if (!fs.existsSync(filePath)) {
64
+ console.error(`File not found: ${filePath}`);
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+
69
+ // The shared 100/day cap only applies when spending the Codeply proxy -
70
+ // Ollama (local or the user's own cloud key) doesn't touch that account.
71
+ const usingCodeply = config.getConfig().provider === 'codeply';
72
+ if (usingCodeply) {
73
+ const limit = await applyLimit.checkApplyLimit();
74
+ if (!limit.allowed) {
75
+ console.error(limit.error || `Daily apply limit reached (${limit.count}/${limit.limit}). This is shared with the Codeply desktop app and resets at midnight UTC.`);
76
+ console.error(c.muted(config.byokHint()));
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ }
81
+
82
+ console.log(c.secondary(`Asking Codeply about ${path.basename(filePath)}…`));
83
+ const result = await editEngine.computeInstructionEdits(instruction, filePath);
84
+
85
+ if (!result.success) {
86
+ console.error(c.red(`Failed: ${result.error}`));
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ if (!result.edits.length) {
91
+ console.log(result.reason || 'No edits produced - the instruction may be too ambiguous or too large for a targeted change.');
92
+ return;
93
+ }
94
+
95
+ printEditPreview(result.edits);
96
+ if (result.badCount) {
97
+ console.log(c.muted(`Note: ${result.badCount} proposed change(s) couldn't be matched against the file and were skipped.`));
98
+ }
99
+ console.log(c.muted(`${result.reason || ''}${result.reason ? ' - ' : ''}confidence ${result.confidence ?? '?'}%, ${result.tokensUsed || 0} tokens, model ${result.modelUsed || 'unknown'}`));
100
+
101
+ if (opts.dryRun) {
102
+ console.log(c.muted('\n(dry run - nothing written)'));
103
+ return;
104
+ }
105
+
106
+ if (!opts.yes) {
107
+ const ok = await confirm(c.bold(c.accent(`Apply ${result.edits.length} edit(s) to ${path.basename(filePath)}?`)));
108
+ if (!ok) { console.log(c.secondary('Cancelled.')); return; }
109
+ }
110
+
111
+ const fileContent = fs.readFileSync(filePath, 'utf8');
112
+ const applied = editEngine.applyEditsToContent(fileContent, result.edits);
113
+ if (!applied.ok) {
114
+ console.error(`Could not apply edit ${applied.index + 1}: ${applied.error}`);
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+
119
+ fs.writeFileSync(filePath, applied.content);
120
+ if (usingCodeply) {
121
+ const linesAdded = result.edits.reduce((n, e) => n + e.replace.split('\n').length, 0);
122
+ const linesRemoved = result.edits.reduce((n, e) => n + e.search.split('\n').length, 0);
123
+ await applyLimit.recordApplyEvent(filePath, linesAdded, linesRemoved);
124
+ }
125
+ console.log(c.green(`✓ Applied to ${filePath}`));
126
+ }
127
+
128
+ // TUI Mode - interactive when no command is provided
129
+ async function runTUI() {
130
+ // Check if running in interactive terminal
131
+ if (!process.stdin.isTTY) {
132
+ const VERSION = require('../package.json').version;
133
+ // C=blue O=red D=yellow E=green P=blue L=red Y=yellow
134
+ // Fixed-width letters + trailing gap so CODEPLY reads cleanly
135
+ const letterColors = [c.blue, c.red, c.yellow, c.green, c.blue, c.red, c.yellow];
136
+ const logoLines = [
137
+ [' ██████╗ ', ' ██████╗ ', '██████╗ ', '███████╗ ', '██████╗ ', '██╗ ', '██╗ ██╗'],
138
+ ['██╔════╝ ', '██╔═══██╗', '██╔══██╗ ', '██╔════╝ ', '██╔══██╗ ', '██║ ', '╚██╗ ██╔╝'],
139
+ ['██║ ', '██║ ██║', '██║ ██║ ', '█████╗ ', '██████╔╝ ', '██║ ', ' ╚████╔╝ '],
140
+ ['██║ ', '██║ ██║', '██║ ██║ ', '██╔══╝ ', '██╔═══╝ ', '██║ ', ' ╚██╔╝ '],
141
+ ['╚██████╗ ', '╚██████╔╝', '██████╔╝ ', '███████╗ ', '██║ ', '███████╗ ', ' ██║ '],
142
+ [' ╚═════╝ ', ' ╚═════╝ ', '╚═════╝ ', '╚══════╝ ', '╚═╝ ', '╚══════╝ ', ' ╚═╝ '],
143
+ ];
144
+
145
+ console.log('');
146
+ logoLines.forEach((line) => {
147
+ console.log(line.map((seg, i) => letterColors[i](seg)).join(''));
148
+ });
149
+ console.log(`
150
+ ${c.secondary('Apply AI instructions. Edit files. Ship faster.')} ${c.muted('v' + VERSION)}
151
+
152
+ ${c.bold(c.text('Usage'))}
153
+ ${c.blue('codeply')} Launch interactive TUI
154
+ ${c.blue('codeply apply')} ${c.dim('<instruction>')} ${c.blue('-f')} ${c.dim('<file>')}
155
+ ${c.red('codeply login')} Sign in with your Codeply account
156
+ ${c.yellow('codeply logout')} Sign out
157
+ ${c.green('codeply whoami')} Show current account
158
+
159
+ ${c.muted('Run in a terminal for the full interactive UI · codeply --help')}
160
+ `);
161
+ return;
162
+ }
163
+
164
+ const React = await import('react');
165
+ const { render } = await import('ink');
166
+ const { default: App } = await import('../lib/tui.mjs');
167
+
168
+ // Take the screen so the app doesn't look like output that happened to land
169
+ // in a shell. Deliberately a plain clear rather than the alternate screen
170
+ // buffer (\x1b[?1049h): alt-screen would discard scrollback on exit, and the
171
+ // whole point of the <Static> transcript is that you can scroll back through
172
+ // it afterwards.
173
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
174
+
175
+ render(React.default.createElement(App), {
176
+ exitOnCtrlC: false, // handled inside the app (clear draft first, then quit)
177
+ });
178
+ }
179
+
180
+ const program = new Command();
181
+ program
182
+ .name('codeply')
183
+ .description('Apply an AI instruction to a file from the terminal.')
184
+ .version(require('../package.json').version);
185
+
186
+ program
187
+ .command('login')
188
+ .description('Sign in with your Codeply account (email + emailed code).')
189
+ .action(async () => { await auth.login(); });
190
+
191
+ program
192
+ .command('logout')
193
+ .description('Sign out of the current Codeply session.')
194
+ .action(async () => { await auth.logout(); });
195
+
196
+ program
197
+ .command('whoami')
198
+ .description('Show the currently signed-in account.')
199
+ .action(async () => {
200
+ const session = await auth.getSession();
201
+ console.log(session ? session.user.email : 'Not signed in.');
202
+ });
203
+
204
+ program
205
+ .command('provider [name]')
206
+ .description('Show or set the model backend (codeply | ollama | openrouter | groq | anthropic | openai | google | qwen).')
207
+ .option('--key <key>', 'API key for ollama (cloud only), openrouter, groq, anthropic, openai, google, or qwen')
208
+ .option('--model <model>', 'model name for the selected provider')
209
+ .option('--host <url>', 'ollama host only (default https://ollama.com; local: http://localhost:11434)')
210
+ .option('--base-url <url>', 'qwen only - Alibaba Model Studio compatible-mode endpoint (deployment-specific)')
211
+ .action(async (name, opts) => {
212
+ const config = require('../lib/config');
213
+
214
+ if (!name && !opts.key && !opts.model && !opts.host && !opts.baseUrl) {
215
+ const cfg = config.getConfig();
216
+ console.log(`Provider ${c.accent(config.describeProvider(cfg))}`);
217
+ if (cfg.provider === 'ollama') {
218
+ console.log(`Host ${cfg.ollama.host}`);
219
+ console.log(`Model ${cfg.ollama.model}`);
220
+ console.log(`Key ${config.maskKey(cfg.ollama.apiKey)}`);
221
+ } else if (config.BYOK_PROVIDERS.includes(cfg.provider)) {
222
+ console.log(`Model ${cfg[cfg.provider].model}`);
223
+ console.log(`Key ${config.maskKey(cfg[cfg.provider].apiKey)}`);
224
+ }
225
+ console.log(c.muted(`\nConfig ${config.configPath}`));
226
+ console.log(c.muted(`Providers ${config.PROVIDERS.join(', ')}`));
227
+ console.log(c.muted('Set with codeply provider openrouter --key <key> [--model <model>]'));
228
+ if (cfg.provider === 'codeply') {
229
+ console.log(c.muted(`\n${config.byokHint()}`));
230
+ }
231
+ return;
232
+ }
233
+
234
+ if (name && !config.PROVIDERS.includes(name)) {
235
+ console.error(`Unknown provider "${name}". Use one of: ${config.PROVIDERS.join(', ')}.`);
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+
240
+ const patch = {};
241
+ if (name) patch.provider = name;
242
+ const target = name || config.getConfig().provider;
243
+
244
+ if (target === 'ollama') {
245
+ const ollama = {};
246
+ if (opts.key) ollama.apiKey = opts.key;
247
+ if (opts.model) ollama.model = opts.model;
248
+ if (opts.host) ollama.host = opts.host;
249
+ if (Object.keys(ollama).length) patch.ollama = ollama;
250
+ } else if (config.BYOK_PROVIDERS.includes(target)) {
251
+ const byok = {};
252
+ if (opts.key) byok.apiKey = opts.key;
253
+ if (opts.model) byok.model = opts.model;
254
+ if (target === 'qwen' && opts.baseUrl) byok.baseUrl = opts.baseUrl;
255
+ if (Object.keys(byok).length) patch[target] = byok;
256
+ }
257
+
258
+ const saved = config.saveConfig(patch);
259
+ if (!saved.ok) {
260
+ console.error(`Could not save config: ${saved.error}`);
261
+ process.exitCode = 1;
262
+ return;
263
+ }
264
+ console.log(c.green(`✓ Provider: ${config.describeProvider(config.getConfig())}`));
265
+ console.log(c.muted(` saved to ${saved.path}`));
266
+ if (opts.key) console.log(c.muted(' the key is stored outside the repo and is never committed'));
267
+ });
268
+
269
+ program
270
+ .command('apply <instruction>')
271
+ .description('Apply an instruction to a file.')
272
+ .requiredOption('-f, --file <path>', 'file to edit')
273
+ .option('-y, --yes', 'apply without confirmation prompt')
274
+ .option('--dry-run', 'show the proposed edits without writing them')
275
+ .action(runApply);
276
+
277
+ const skillCmd = program
278
+ .command('skill')
279
+ .description('List, install, or remove agent skills (SKILL.md playbooks the agent can load mid-task).');
280
+
281
+ skillCmd
282
+ .command('list', { isDefault: true })
283
+ .description('List every available skill.')
284
+ .option('-a, --all', 'include full descriptions (default shows a compact table)')
285
+ .action((opts) => {
286
+ const skills = require('../lib/skills');
287
+ const list = skills.listSkills();
288
+ if (list.length === 0) {
289
+ console.log('No skills installed.');
290
+ return;
291
+ }
292
+ const daily = list.filter((s) => s.daily);
293
+ const rest = list.filter((s) => !s.daily);
294
+ console.log(c.bold(`${list.length} skills`) + c.muted(` (${daily.length} loaded by default, ${rest.length} available via list_skills or /skill install)`));
295
+ console.log('');
296
+ for (const s of list) {
297
+ const tag = s.source === 'user' ? c.green('[user]') : s.daily ? c.blue('[daily]') : c.muted('[library]');
298
+ console.log(`${s.name.padEnd(30)} ${tag}`);
299
+ if (opts.all) console.log(c.muted(` ${s.description}`));
300
+ }
301
+ console.log('');
302
+ console.log(c.muted(`Bundled from affaan-m/ECC - see skills/SOURCE.md. Add your own: codeply skill install <path-or-github-url>`));
303
+ });
304
+
305
+ skillCmd
306
+ .command('install <source>')
307
+ .description('Install a skill from a local directory (containing SKILL.md) or a GitHub URL/owner-repo.')
308
+ .option('-n, --name <name>', 'name to install under (single-skill sources only; ignored for a whole-repo install)')
309
+ .action(async (source, opts) => {
310
+ const skills = require('../lib/skills');
311
+ const isLocal = fs.existsSync(source);
312
+
313
+ if (isLocal) {
314
+ const result = skills.installFromLocalDir(path.resolve(process.cwd(), source), opts.name);
315
+ if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
316
+ console.log(c.green(`✓ Installed "${result.name}"`));
317
+ console.log(c.muted(` ${result.dest}`));
318
+ return;
319
+ }
320
+
321
+ console.log(c.secondary(`Fetching from ${source}…`));
322
+ let result;
323
+ try {
324
+ result = await skills.installFromGitHub(source, opts.name);
325
+ } catch (e) {
326
+ console.error(c.red(`Install failed: ${e.message}`));
327
+ process.exitCode = 1;
328
+ return;
329
+ }
330
+ if (!result.ok) {
331
+ console.error(c.red(result.error || `No skills installed from ${source}.`));
332
+ process.exitCode = 1;
333
+ return;
334
+ }
335
+ console.log(c.green(`✓ Installed ${result.installed.length} skill(s) from ${result.source}`));
336
+ for (const i of result.installed) console.log(c.muted(` ${i.name} (${i.fileCount} file(s)) → ${i.dest}`));
337
+ if (result.failed.length) {
338
+ console.log(c.yellow(` ${result.failed.length} skipped (fetch failed):`));
339
+ for (const f of result.failed) console.log(c.muted(` ${f.dir}: ${f.error}`));
340
+ }
341
+ });
342
+
343
+ skillCmd
344
+ .command('remove <name>')
345
+ .description('Remove a user-installed skill. Bundled skills cannot be removed this way.')
346
+ .action((name) => {
347
+ const skills = require('../lib/skills');
348
+ const result = skills.removeSkill(name);
349
+ if (!result.ok) { console.error(c.red(result.error)); process.exitCode = 1; return; }
350
+ console.log(c.green(`✓ Removed "${name}"`));
351
+ });
352
+
353
+ skillCmd
354
+ .command('show <name>')
355
+ .description('Print a skill\'s full instructions.')
356
+ .action((name) => {
357
+ const skills = require('../lib/skills');
358
+ const body = skills.loadSkillBody(name);
359
+ if (body == null) {
360
+ console.error(c.red(`No skill named "${name}". Run \`codeply skill list\` to see what's available.`));
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ console.log(body);
365
+ });
366
+
367
+ const pluginCmd = program
368
+ .command('plugin')
369
+ .description('Install, share and manage plugins: bundles of commands, skills, MCP servers and instructions.');
370
+
371
+ function printPluginSummary(s) {
372
+ console.log(c.bold(`${s.name}${s.version ? ' ' + s.version : ''}`) + (s.author ? c.muted(` by ${s.author}`) : ''));
373
+ if (s.description) console.log(` ${s.description}`);
374
+ const parts = [];
375
+ if (s.commands) parts.push(`${s.commands} command${s.commands === 1 ? '' : 's'} (/${s.name}:...)`);
376
+ if (s.skills) parts.push(`${s.skills} skill${s.skills === 1 ? '' : 's'}`);
377
+ if (s.instructions) parts.push('agent instructions');
378
+ if (parts.length) console.log(c.muted(` Adds: ${parts.join(', ')}`));
379
+ if (s.mcpServers.length) {
380
+ console.log(c.yellow(' Starts MCP servers (these can run programs on this computer):'));
381
+ for (const m of s.mcpServers) console.log(` ${m.name}: ${m.runs || m.url}`);
382
+ }
383
+ }
384
+
385
+ pluginCmd
386
+ .command('list', { isDefault: true })
387
+ .description('List installed plugins.')
388
+ .action(() => {
389
+ const list = require('../lib/plugins').listPlugins(process.cwd());
390
+ if (!list.length) {
391
+ console.log('No plugins installed. Try: codeply plugin install <owner/repo | git URL | folder>');
392
+ return;
393
+ }
394
+ for (const p of list) {
395
+ const tag = [p.scope, p.enabled ? '' : 'disabled'].filter(Boolean).join(', ');
396
+ console.log(`${p.name.padEnd(24)} ${p.version.padEnd(8)} ${c.muted(`[${tag}]`)} ${p.description}`);
397
+ }
398
+ });
399
+
400
+ pluginCmd
401
+ .command('install <source>')
402
+ .description('Install from owner/repo, a git URL (add #branch or #tag), or a local folder.')
403
+ .option('--project', 'install for this project only (default: every project)')
404
+ .option('--force', 'replace an existing plugin of the same name')
405
+ .option('-y, --yes', 'do not ask before installing')
406
+ .action(async (source, opts) => {
407
+ const plugins = require('../lib/plugins');
408
+ console.log(c.secondary(`Fetching ${source}...`));
409
+ const prepared = await plugins.prepareInstall(source);
410
+ if (!prepared.ok) { console.error(c.red(prepared.error)); process.exitCode = 1; return; }
411
+ console.log('');
412
+ printPluginSummary(prepared.summary);
413
+ console.log('');
414
+ if (!opts.yes && !(await confirm(c.bold(c.accent(`Install ${prepared.name}?`))))) { plugins.discardInstall(prepared); console.log('Cancelled.'); return; }
415
+ const done = plugins.finishInstall(prepared, { scope: opts.project ? 'project' : 'user', cwd: process.cwd(), force: !!opts.force });
416
+ if (!done.ok) { console.error(c.red(done.error)); process.exitCode = 1; return; }
417
+ console.log(c.green(`Installed ${done.name}`) + c.muted(` ${done.dest}`));
418
+ });
419
+
420
+ pluginCmd
421
+ .command('update [name]')
422
+ .description('Re-fetch a plugin (or all of them) from where it was installed.')
423
+ .option('-y, --yes', 'do not ask before updating')
424
+ .action(async (name, opts) => {
425
+ const plugins = require('../lib/plugins');
426
+ const names = name ? [name] : plugins.listPlugins(process.cwd()).filter((p) => p.source).map((p) => p.name);
427
+ if (!names.length) { console.log('Nothing to update.'); return; }
428
+ for (const n of names) {
429
+ const prepared = await plugins.prepareUpdate(n, process.cwd());
430
+ if (!prepared.ok) { console.error(c.red(`${n}: ${prepared.error}`)); process.exitCode = 1; continue; }
431
+ const oldMcp = new Set(prepared.before.mcpServers.map((m) => `${m.name}|${m.runs}|${m.url}`));
432
+ const added = prepared.summary.mcpServers.filter((m) => !oldMcp.has(`${m.name}|${m.runs}|${m.url}`));
433
+ if (added.length && !opts.yes) {
434
+ console.log(c.yellow(`${n} now starts new or changed MCP servers:`));
435
+ for (const m of added) console.log(` ${m.name}: ${m.runs || m.url}`);
436
+ if (!(await confirm(c.bold(c.accent(`Update ${n}?`))))) { plugins.discardInstall(prepared); console.log(`Skipped ${n}.`); continue; }
437
+ }
438
+ const done = plugins.finishInstall(prepared, { scope: prepared.scope, cwd: process.cwd(), force: true });
439
+ if (!done.ok) { console.error(c.red(`${n}: ${done.error}`)); process.exitCode = 1; continue; }
440
+ console.log(c.green(`Updated ${n}`) + (done.summary.version ? c.muted(` ${done.summary.version}`) : ''));
441
+ }
442
+ });
443
+
444
+ pluginCmd
445
+ .command('remove <name>')
446
+ .description('Uninstall a plugin.')
447
+ .action((name) => {
448
+ const r = require('../lib/plugins').removePlugin(name, process.cwd());
449
+ if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
450
+ console.log(c.green(`Removed ${r.name}`));
451
+ });
452
+
453
+ for (const [verb, on] of [['enable', true], ['disable', false]]) {
454
+ pluginCmd
455
+ .command(`${verb} <name>`)
456
+ .description(`${on ? 'Turn a plugin back on' : 'Turn a plugin off without removing it'}.`)
457
+ .action((name) => {
458
+ const r = require('../lib/plugins').setEnabled(name, on, process.cwd());
459
+ if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
460
+ console.log(c.green(`${on ? 'Enabled' : 'Disabled'} ${r.name}`));
461
+ });
462
+ }
463
+
464
+ pluginCmd
465
+ .command('init <name>')
466
+ .description('Create a new plugin folder to fill in and push to GitHub.')
467
+ .action((name) => {
468
+ const r = require('../lib/plugins').scaffoldPlugin(name, process.cwd());
469
+ if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
470
+ console.log(c.green(`Created ${r.dir}`));
471
+ console.log(c.muted(`Try it locally: codeply plugin install ${r.dir}\nShare it: push the folder to GitHub, then others run: codeply plugin install <you>/${r.name}`));
472
+ });
473
+
474
+ const githubCmd = program
475
+ .command('github')
476
+ .description('The GitHub agent: comment /codeply on an issue or pull request and Craft does the work in your own Actions runner.');
477
+
478
+ githubCmd
479
+ .command('install')
480
+ .description('Add the workflow file that lets /codeply comments start a run.')
481
+ .action(async () => {
482
+ const { installWorkflow } = await import('../lib/github-agent.mjs');
483
+ const r = installWorkflow(process.cwd());
484
+ if (!r.ok) { console.error(c.red(r.error)); process.exitCode = 1; return; }
485
+ console.log(c.green(`Created ${r.file}`));
486
+ console.log([
487
+ '',
488
+ 'Finish in your repository settings:',
489
+ ' 1. Settings > Secrets and variables > Actions > New repository secret: CODEPLY_API_KEY (your own model key).',
490
+ ' 2. Optional variables: CODEPLY_PROVIDER (openai, anthropic, gemini, openrouter, groq, deepseek), CODEPLY_MODEL, CODEPLY_BASE_URL.',
491
+ ' 3. Settings > Actions > General > Workflow permissions: "Read and write" and allow Actions to create pull requests.',
492
+ ' 4. Commit the workflow, then comment "/codeply <what you want>" on an issue or pull request.',
493
+ '',
494
+ 'Only the owner, members and collaborators can start a run. "/codeply ask ..." and "/codeply plan ..." never change files.',
495
+ ].join('\n'));
496
+ });
497
+
498
+ githubCmd
499
+ .command('run')
500
+ .description('Handle the current GitHub Actions event (used by the workflow).')
501
+ .option('--max-steps <n>', 'step budget for the agent', '40')
502
+ .action(async (opts) => {
503
+ const g = await import('../lib/github-agent.mjs');
504
+ const cfg = { ...process.env };
505
+ const token = cfg.GITHUB_TOKEN || cfg.GH_TOKEN;
506
+ // The agent runs commands and MCP servers; it must never see these.
507
+ for (const k of ['GITHUB_TOKEN', 'GH_TOKEN', 'CODEPLY_API_KEY', 'ACTIONS_RUNTIME_TOKEN', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN']) delete process.env[k];
508
+ if (!cfg.GITHUB_EVENT_NAME || !cfg.GITHUB_EVENT_PATH) { console.error(c.red('This runs inside GitHub Actions (GITHUB_EVENT_NAME and GITHUB_EVENT_PATH are not set).')); process.exitCode = 1; return; }
509
+ let event;
510
+ try { event = JSON.parse(fs.readFileSync(cfg.GITHUB_EVENT_PATH, 'utf8')); } catch (e) { console.error(c.red(`Could not read the event: ${e.message}`)); process.exitCode = 1; return; }
511
+ const r = g.routeFromEnv(cfg);
512
+ // A setup error is still reported on the issue, so the person who asked isn't left waiting.
513
+ const out = await g.runGithubAgent({
514
+ eventName: cfg.GITHUB_EVENT_NAME, event, token, route: r.route, setupError: r.error, env: cfg, cwd: process.cwd(),
515
+ maxSteps: Math.max(1, Number(opts.maxSteps) || 40), log: (m) => console.log(m),
516
+ });
517
+ console.log(`${out.status}${out.url ? ` ${out.url}` : ''}`);
518
+ if (out.status === 'failed') { console.error(c.red(out.message || 'Failed.')); process.exitCode = 1; }
519
+ });
520
+
521
+ program
522
+ .command('serve')
523
+ .description('Run the engine as a local HTTP API with a live event stream, for the desktop app, phone, editors and scripts.')
524
+ .option('-p, --port <port>', 'port to listen on (0 picks a free one)', '4096')
525
+ .option('--host <host>', 'address to bind; anything but 127.0.0.1 exposes the engine to the network', '127.0.0.1')
526
+ .option('--password <password>', 'password clients must send (default: CODEPLY_SERVER_PASSWORD, else a random one is printed)')
527
+ .option('--cwd <dir>', 'default project folder for new chats', process.cwd())
528
+ .option('--data-dir <dir>', 'where chats are stored (default ~/.codeply/server)')
529
+ .option('--cors <origin...>', 'browser origins allowed to call the API')
530
+ .action(async (opts) => {
531
+ const { startServer } = await import('../lib/server.mjs');
532
+ const password = opts.password || process.env.CODEPLY_SERVER_PASSWORD || undefined;
533
+ let srv;
534
+ try {
535
+ srv = await startServer({ port: Number(opts.port), host: opts.host, password, cwd: opts.cwd, dataDir: opts.dataDir, allowedOrigins: opts.cors });
536
+ } catch (e) {
537
+ console.error(c.red(`Could not start the server: ${e.message}`));
538
+ process.exitCode = 1;
539
+ return;
540
+ }
541
+ console.log(c.green(`✓ Codeply engine listening on ${srv.url}`));
542
+ console.log(` Password ${srv.password}${password ? '' : c.muted(' (generated for this run)')}`);
543
+ console.log(c.muted(` Chats ${srv.storage === 'json' ? 'JSON file (this Node has no SQLite)' : `SQLite (${srv.storage})`}`));
544
+ console.log(c.muted(` Try curl -H "Authorization: Bearer ${srv.password}" ${srv.url}/doc`));
545
+ if (!['127.0.0.1', 'localhost', '::1'].includes(opts.host)) {
546
+ console.log(c.yellow(' Bound beyond this machine over plain HTTP. Put it behind a tunnel or TLS proxy before exposing it.'));
547
+ }
548
+ const stop = async () => { await srv.close(); process.exit(0); };
549
+ process.on('SIGINT', stop);
550
+ process.on('SIGTERM', stop);
551
+ });
552
+
553
+ // If no arguments provided, launch TUI
554
+ if (process.argv.length <= 2) {
555
+ runTUI();
556
+ } else {
557
+ program.parseAsync(process.argv);
558
+ }