@agentuity/cli 0.0.11 → 0.0.13

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 (51) hide show
  1. package/bin/cli.ts +43 -2
  2. package/dist/api.d.ts +5 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/auth.d.ts +2 -0
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cmd/auth/api.d.ts +7 -0
  8. package/dist/cmd/auth/api.d.ts.map +1 -1
  9. package/dist/cmd/auth/index.d.ts.map +1 -1
  10. package/dist/cmd/auth/login.d.ts.map +1 -1
  11. package/dist/cmd/auth/signup.d.ts +3 -0
  12. package/dist/cmd/auth/signup.d.ts.map +1 -0
  13. package/dist/cmd/dev/index.d.ts.map +1 -1
  14. package/dist/cmd/example/index.d.ts.map +1 -1
  15. package/dist/cmd/example/optional-auth.d.ts +3 -0
  16. package/dist/cmd/example/optional-auth.d.ts.map +1 -0
  17. package/dist/cmd/project/create.d.ts.map +1 -1
  18. package/dist/cmd/project/download.d.ts.map +1 -1
  19. package/dist/cmd/project/template-flow.d.ts +1 -0
  20. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/sound.d.ts +1 -1
  25. package/dist/sound.d.ts.map +1 -1
  26. package/dist/tui.d.ts +23 -1
  27. package/dist/tui.d.ts.map +1 -1
  28. package/dist/types.d.ts +29 -4
  29. package/dist/types.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/api.ts +16 -2
  32. package/src/auth.ts +79 -4
  33. package/src/cli.ts +51 -1
  34. package/src/cmd/auth/README.md +37 -3
  35. package/src/cmd/auth/api.ts +66 -2
  36. package/src/cmd/auth/index.ts +2 -1
  37. package/src/cmd/auth/login.ts +11 -3
  38. package/src/cmd/auth/signup.ts +51 -0
  39. package/src/cmd/dev/index.ts +135 -50
  40. package/src/cmd/example/index.ts +2 -0
  41. package/src/cmd/example/optional-auth.ts +38 -0
  42. package/src/cmd/example/sound.ts +2 -2
  43. package/src/cmd/project/create.ts +1 -0
  44. package/src/cmd/project/download.ts +37 -52
  45. package/src/cmd/project/template-flow.ts +26 -11
  46. package/src/config.ts +8 -1
  47. package/src/download.ts +2 -2
  48. package/src/index.ts +1 -0
  49. package/src/sound.ts +27 -13
  50. package/src/tui.ts +126 -9
  51. package/src/types.ts +47 -2
@@ -1,8 +1,9 @@
1
- import { join } from 'node:path';
1
+ import { join, resolve } from 'node:path';
2
2
  import { existsSync, mkdirSync, renameSync, readdirSync, cpSync, rmSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
3
4
  import { pipeline } from 'node:stream/promises';
4
5
  import { createGunzip } from 'node:zlib';
5
- import { extract } from 'tar-fs';
6
+ import { extract, type Headers } from 'tar-fs';
6
7
  import type { Logger } from '@/logger';
7
8
  import * as tui from '@/tui';
8
9
  import { downloadWithSpinner } from '@/download';
@@ -27,22 +28,12 @@ interface SetupOptions {
27
28
  logger: Logger;
28
29
  }
29
30
 
30
- export async function downloadTemplate(options: DownloadOptions): Promise<void> {
31
- const { dest, template, templateDir, templateBranch } = options;
32
-
33
- mkdirSync(dest, { recursive: true });
34
-
35
- // Copy from local directory if provided
36
- if (templateDir) {
37
- const { resolve } = await import('node:path');
38
- const sourceDir = resolve(join(templateDir, template.directory));
39
-
40
- if (!existsSync(sourceDir)) {
41
- throw new Error(`Template directory not found: ${sourceDir}`);
42
- }
43
-
44
- tui.info(`📦 Copying template from ${sourceDir}...`);
31
+ async function cleanup(sourceDir: string, dest: string) {
32
+ if (!existsSync(sourceDir)) {
33
+ throw new Error(`Template directory not found: ${sourceDir}`);
34
+ }
45
35
 
36
+ tui.spinner(`📦 Copying template from ${sourceDir}...`, async () => {
46
37
  // Copy all files from source to dest
47
38
  const files = readdirSync(sourceDir);
48
39
  for (const file of files) {
@@ -54,8 +45,23 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
54
45
  if (existsSync(gi)) {
55
46
  renameSync(gi, join(dest, '.gitignore'));
56
47
  }
48
+ });
49
+ }
50
+
51
+ export async function downloadTemplate(options: DownloadOptions): Promise<void> {
52
+ const { dest, template, templateDir, templateBranch } = options;
57
53
 
58
- return;
54
+ mkdirSync(dest, { recursive: true });
55
+
56
+ // Copy from local directory if provided
57
+ if (templateDir) {
58
+ const sourceDir = resolve(join(templateDir, template.directory));
59
+
60
+ if (!existsSync(sourceDir)) {
61
+ throw new Error(`Template directory not found: ${sourceDir}`);
62
+ }
63
+
64
+ return cleanup(sourceDir, dest);
59
65
  }
60
66
 
61
67
  // Download from GitHub
@@ -74,65 +80,43 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
74
80
  },
75
81
  async (stream) => {
76
82
  // Extract only the template directory from tarball
83
+ const prefix = `sdk-${branch}/${templatePath}/`;
77
84
  await pipeline(
78
85
  stream,
79
86
  createGunzip(),
80
87
  extract(tempDir, {
81
- map: (header) => {
82
- const prefix = `sdk-${branch}/${templatePath}/`;
83
- if (header.name.startsWith(prefix)) {
84
- header.name = header.name.substring(prefix.length);
85
- return header;
86
- }
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
- return null as any;
88
+ filter: (name: string) => name.startsWith(prefix),
89
+ map: (header: Headers) => {
90
+ header.name = header.name.substring(prefix.length);
91
+ return header;
89
92
  },
90
93
  })
91
94
  );
92
95
  }
93
96
  );
94
97
 
95
- // Move files from temp to dest
96
- const files = readdirSync(tempDir);
97
- for (const file of files) {
98
- cpSync(join(tempDir, file), join(dest, file), { recursive: true });
99
- }
100
- rmSync(tempDir, { recursive: true, force: true });
98
+ await cleanup(tempDir, dest);
101
99
 
102
- // Rename gitignore -> .gitignore
103
- const gi = join(dest, 'gitignore');
104
- if (existsSync(gi)) {
105
- renameSync(gi, join(dest, '.gitignore'));
100
+ // Extra safety: refuse to delete root or home directories
101
+ const home = homedir();
102
+ if (tempDir === '/' || tempDir === home) {
103
+ throw new Error(`Refusing to delete protected path: ${tempDir}`);
106
104
  }
105
+ rmSync(tempDir, { recursive: true, force: true });
107
106
  }
108
107
 
109
108
  export async function setupProject(options: SetupOptions): Promise<void> {
110
109
  const { dest, projectName, dirName, noInstall, noBuild, logger } = options;
111
110
 
112
- process.chdir(dest);
113
-
114
111
  // Replace {{PROJECT_NAME}} in files
115
112
  tui.info(`🔧 Setting up ${projectName}...`);
116
113
  await replaceInFiles(dest, projectName, dirName);
117
114
 
118
- // Run setup.ts if it exists (legacy)
119
- if (await Bun.file('./setup.ts').exists()) {
120
- await tui.spinner({
121
- message: 'Running setup script...',
122
- callback: async () => {
123
- const proc = Bun.spawn(['bun', './setup.ts'], { stdio: ['pipe', 'pipe', 'pipe'] });
124
- const exitCode = await proc.exited;
125
- if (exitCode !== 0) {
126
- logger.error('Setup script failed');
127
- }
128
- },
129
- });
130
- }
131
-
132
115
  // Install dependencies
133
116
  if (!noInstall) {
134
117
  const exitCode = await tui.runCommand({
135
118
  command: 'bun install',
119
+ cwd: dest,
136
120
  cmd: ['bun', 'install'],
137
121
  clearOnSuccess: true,
138
122
  });
@@ -145,6 +129,7 @@ export async function setupProject(options: SetupOptions): Promise<void> {
145
129
  if (!noBuild) {
146
130
  const exitCode = await tui.runCommand({
147
131
  command: 'bun run build',
132
+ cwd: dest,
148
133
  cmd: ['bun', 'run', 'build'],
149
134
  clearOnSuccess: true,
150
135
  });
@@ -1,13 +1,17 @@
1
1
  import { basename, resolve } from 'node:path';
2
2
  import { existsSync, readdirSync, rmSync } from 'node:fs';
3
+ import { cwd } from 'node:process';
4
+ import { homedir } from 'node:os';
3
5
  import enquirer from 'enquirer';
4
6
  import type { Logger } from '@/logger';
5
7
  import * as tui from '@/tui';
8
+ import { playSound } from '@/sound';
6
9
  import { fetchTemplates, type TemplateInfo } from './templates';
7
10
  import { downloadTemplate, setupProject } from './download';
8
11
 
9
12
  interface CreateFlowOptions {
10
13
  projectName?: string;
14
+ dir?: string;
11
15
  template?: string;
12
16
  templateDir?: string;
13
17
  templateBranch?: string;
@@ -20,6 +24,7 @@ interface CreateFlowOptions {
20
24
  export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
21
25
  const {
22
26
  projectName: initialProjectName,
27
+ dir: targetDir,
23
28
  template: initialTemplate,
24
29
  templateDir,
25
30
  templateBranch,
@@ -64,14 +69,20 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
64
69
  const dirName = projectName === '.' ? '.' : sanitizeDirectoryName(projectName);
65
70
 
66
71
  // Step 4: Determine destination directory
67
- const dest = dirName === '.' ? process.cwd() : resolve(process.cwd(), dirName);
72
+ // Expand ~ to home directory
73
+ let expandedTargetDir = targetDir;
74
+ if (expandedTargetDir && expandedTargetDir.startsWith('~')) {
75
+ expandedTargetDir = expandedTargetDir.replace(/^~/, homedir());
76
+ }
77
+ const baseDir = expandedTargetDir ? resolve(expandedTargetDir) : process.cwd();
78
+ const dest = dirName === '.' ? baseDir : resolve(baseDir, dirName);
68
79
  const destExists = existsSync(dest);
69
80
  const destEmpty = destExists ? readdirSync(dest).length === 0 : true;
70
81
 
71
82
  if (destExists && !destEmpty && dirName !== '.') {
72
83
  // In TTY mode, ask if they want to overwrite
73
84
  if (process.stdin.isTTY && !skipPrompts) {
74
- tui.warning(`Directory ${dirName} already exists and is not empty.\n`);
85
+ tui.warning(`Directory ${dest} already exists and is not empty.`, true);
75
86
  const response = await enquirer.prompt<{ overwrite: boolean }>({
76
87
  type: 'confirm',
77
88
  name: 'overwrite',
@@ -84,19 +95,23 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
84
95
  process.exit(0);
85
96
  }
86
97
 
87
- // Delete the existing directory
98
+ // Extra safety: refuse to delete root or home directories
99
+ const home = homedir();
100
+ if (dest === '/' || dest === home) {
101
+ logger.fatal(`Refusing to delete protected path: ${dest}`);
102
+ return;
103
+ }
88
104
  rmSync(dest, { recursive: true, force: true });
89
- tui.success(`Deleted ${dirName}\n`);
105
+ tui.success(`Deleted ${dest}\n`);
90
106
  } else {
91
- logger.fatal(`Directory ${dirName} already exists and is not empty.`);
107
+ logger.fatal(`Directory ${dest} already exists and is not empty.`, true);
92
108
  }
93
109
  }
94
110
 
95
111
  // Show directory and name confirmation
96
112
  if (!skipPrompts) {
97
- const displayPath = dirName === '.' ? basename(dest) : dirName;
98
113
  tui.info(`📁 Project: ${tui.bold(projectName)}`);
99
- tui.info(`📂 Directory: ${tui.bold(displayPath)}\n`);
114
+ tui.info(`📂 Directory: ${tui.bold(dest)}\n`);
100
115
  }
101
116
 
102
117
  // Step 5: Select template
@@ -152,14 +167,14 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
152
167
  tui.success('✨ Project created successfully!\n');
153
168
  tui.info('Next steps:');
154
169
  if (dirName !== '.') {
170
+ const dirDisplay = cwd() == targetDir ? basename(dirName) : dest;
155
171
  tui.newline();
156
- console.log(` 1. ${tui.bold(`cd ${dirName}`)}`);
172
+ console.log(` 1. ${tui.bold(`cd ${dirDisplay}`)}`);
157
173
  console.log(` 2. ${tui.bold('bun run dev')}`);
158
174
  } else {
159
- console.log(` 1. ${tui.bold('bun run dev')}`);
175
+ console.log(` ${tui.bold('bun run dev')}`);
160
176
  }
161
- tui.newline();
162
- console.log(`Your agents will be running at ${tui.link('http://localhost:3000')}`);
177
+ playSound();
163
178
  }
164
179
 
165
180
  /**
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { YAML } from 'bun';
2
- import { join, extname } from 'node:path';
2
+ import { join, extname, basename } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { mkdir, readdir, readFile, writeFile, chmod } from 'node:fs/promises';
5
5
  import type { Config, Profile, AuthData } from './types';
@@ -104,6 +104,13 @@ export async function loadConfig(customPath?: string): Promise<Config | null> {
104
104
  const content = await file.text();
105
105
  const config = YAML.parse(content);
106
106
 
107
+ // check to see if this is a legacy config file that might not have the required name
108
+ // and in this case we can just use the filename
109
+ const _config = config as { name?: string };
110
+ if (!_config.name) {
111
+ _config.name = basename(configPath).replace(extname(configPath), '');
112
+ }
113
+
107
114
  const result = ConfigSchema.safeParse(config);
108
115
  if (!result.success) {
109
116
  tui.error(`Invalid config in ${configPath}:`);
package/src/download.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Transform } from 'node:stream';
1
+ import { Transform, Readable } from 'node:stream';
2
2
  import * as tui from './tui';
3
3
 
4
4
  export interface DownloadOptions {
@@ -48,7 +48,7 @@ export async function downloadWithProgress(
48
48
  });
49
49
 
50
50
  // Pipe the response through the progress tracker
51
- const responseStream = response.body as unknown as NodeJS.ReadableStream;
51
+ const responseStream = Readable.fromWeb(response.body as unknown as ReadableStream);
52
52
  responseStream.pipe(progressStream);
53
53
 
54
54
  return progressStream;
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createCLI, registerCommands } from './cli';
2
2
  export { validateRuntime, isBun } from './runtime';
3
3
  export { getVersion, getRevision, getPackageName, getPackage } from './version';
4
+ export { requireAuth, optionalAuth, withAuth, withOptionalAuth } from './auth';
4
5
  export {
5
6
  loadConfig,
6
7
  saveConfig,
package/src/sound.ts CHANGED
@@ -1,25 +1,39 @@
1
- import { $ } from 'bun';
1
+ import { join } from 'node:path';
2
2
 
3
- export async function playSound(): Promise<void> {
3
+ export function playSound(): void {
4
4
  const platform = process.platform;
5
5
 
6
- let result;
6
+ let command: string[];
7
7
  switch (platform) {
8
- case 'darwin':
9
- result = await $`afplay /System/Library/Sounds/Glass.aiff`.quiet().nothrow();
8
+ case 'darwin': {
9
+ const items = [
10
+ 'Blow.aiff',
11
+ 'Bottle.aiff',
12
+ 'Frog.aiff',
13
+ 'Funk.aiff',
14
+ 'Glass.aiff',
15
+ 'Hero.aiff',
16
+ 'Morse.aiff',
17
+ 'Ping.aiff',
18
+ 'Pop.aiff',
19
+ 'Purr.aiff',
20
+ 'Sosumi.aiff',
21
+ ] as const;
22
+ const file = items[Math.floor(Math.random() * items.length)];
23
+ command = ['afplay', join('/System/Library/Sounds', file)];
10
24
  break;
25
+ }
11
26
  case 'linux':
12
- result = await $`paplay /usr/share/sounds/freedesktop/stereo/complete.oga`
13
- .quiet()
14
- .nothrow();
27
+ command = ['paplay', '/usr/share/sounds/freedesktop/stereo/complete.oga'];
15
28
  break;
16
29
  case 'win32':
17
- result = await $`rundll32 user32.dll,MessageBeep 0x00000040`.quiet().nothrow();
30
+ command = ['rundll32', 'user32.dll,MessageBeep', '0x00000040'];
18
31
  break;
32
+ default:
33
+ return;
19
34
  }
20
35
 
21
- // Fallback to terminal bell if command failed or platform unsupported
22
- if (!result || result.exitCode !== 0) {
23
- process.stdout.write('\u0007');
24
- }
36
+ Bun.spawn(command, {
37
+ stdio: ['ignore', 'ignore', 'ignore'],
38
+ }).unref();
25
39
  }
package/src/tui.ts CHANGED
@@ -82,11 +82,21 @@ export function error(message: string): void {
82
82
  console.error(`${color}${ICONS.error} ${message}${reset}`);
83
83
  }
84
84
 
85
+ /**
86
+ * Print an error message with a red X and then exit
87
+ */
88
+ export function fatal(message: string): never {
89
+ const color = getColor('error');
90
+ const reset = COLORS.reset;
91
+ console.error(`${color}${ICONS.error} ${message}${reset}`);
92
+ process.exit(1);
93
+ }
94
+
85
95
  /**
86
96
  * Print a warning message with a yellow warning icon
87
97
  */
88
- export function warning(message: string): void {
89
- const color = getColor('warning');
98
+ export function warning(message: string, asError = false): void {
99
+ const color = asError ? getColor('error') : getColor('warning');
90
100
  const reset = COLORS.reset;
91
101
  console.log(`${color}${ICONS.warning} ${message}${reset}`);
92
102
  }
@@ -200,9 +210,13 @@ export function padLeft(str: string, length: number, pad = ' '): string {
200
210
  * Creates a bordered box around the content
201
211
  *
202
212
  * Uses Bun.stringWidth() for accurate width calculation with ANSI codes and unicode
213
+ * Responsive to terminal width - adapts to narrow terminals
203
214
  */
204
215
  export function banner(title: string, body: string): void {
205
- const maxWidth = 80;
216
+ // Get terminal width, default to 80 if not available, minimum 40
217
+ const termWidth = process.stdout.columns || 80;
218
+ const maxWidth = Math.max(40, Math.min(termWidth - 2, 80)); // Between 40 and 80, with 2 char margin
219
+
206
220
  const border = {
207
221
  topLeft: '╭',
208
222
  topRight: '╮',
@@ -377,6 +391,33 @@ export async function confirm(message: string, defaultValue = true): Promise<boo
377
391
  });
378
392
  }
379
393
 
394
+ /**
395
+ * Display a signup benefits box with cyan border
396
+ * Shows the value proposition for creating an Agentuity account
397
+ */
398
+ export function showSignupBenefits(): void {
399
+ const CYAN = Bun.color('cyan', 'ansi-16m');
400
+ const TEXT =
401
+ currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
402
+ const RESET = '\x1b[0m';
403
+
404
+ const lines = [
405
+ '╔════════════════════════════════════════════╗',
406
+ `║ ⨺ Signup for Agentuity ${muted('free')}${CYAN} ║`,
407
+ '║ ║',
408
+ `║ ✓ ${TEXT}Cloud deployment, previews and CI/CD${CYAN} ║`,
409
+ `║ ✓ ${TEXT}AI Gateway, KV, Vector and more${CYAN} ║`,
410
+ `║ ✓ ${TEXT}Observability, Tracing and Logging${CYAN} ║`,
411
+ `║ ✓ ${TEXT}Organization and Team support${CYAN} ║`,
412
+ `║ ✓ ${TEXT}And much more!${CYAN} ║`,
413
+ '╚════════════════════════════════════════════╝',
414
+ ];
415
+
416
+ console.log('');
417
+ lines.forEach((line) => console.log(CYAN + line + RESET));
418
+ console.log('');
419
+ }
420
+
380
421
  /**
381
422
  * Copy text to clipboard
382
423
  * Returns true if successful, false otherwise
@@ -443,9 +484,36 @@ function getDisplayWidth(str: string): number {
443
484
  return Bun.stringWidth(withoutOSC8);
444
485
  }
445
486
 
487
+ /**
488
+ * Extract ANSI codes from the beginning of a string
489
+ */
490
+ function extractLeadingAnsiCodes(str: string): string {
491
+ // Match ANSI escape sequences at the start of the string
492
+ // eslint-disable-next-line no-control-regex
493
+ const match = str.match(/^(\x1b\[[0-9;]*m)+/);
494
+ return match ? match[0] : '';
495
+ }
496
+
497
+ /**
498
+ * Strip ANSI codes from a string
499
+ */
500
+ function stripAnsiCodes(str: string): string {
501
+ // Remove all ANSI escape sequences
502
+ // eslint-disable-next-line no-control-regex
503
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
504
+ }
505
+
506
+ /**
507
+ * Check if a string ends with ANSI reset code
508
+ */
509
+ function endsWithReset(str: string): boolean {
510
+ return str.endsWith('\x1b[0m') || str.endsWith(COLORS.reset);
511
+ }
512
+
446
513
  /**
447
514
  * Wrap text to a maximum width
448
515
  * Handles explicit newlines and word wrapping
516
+ * Preserves ANSI color codes across wrapped lines
449
517
  */
450
518
  function wrapText(text: string, maxWidth: number): string[] {
451
519
  const allLines: string[] = [];
@@ -460,6 +528,13 @@ function wrapText(text: string, maxWidth: number): string[] {
460
528
  continue;
461
529
  }
462
530
 
531
+ // Record starting index for this paragraph's lines
532
+ const paragraphStart = allLines.length;
533
+
534
+ // Extract any leading ANSI codes from the paragraph
535
+ const leadingCodes = extractLeadingAnsiCodes(paragraph);
536
+ const hasReset = endsWithReset(paragraph);
537
+
463
538
  // Wrap each paragraph
464
539
  const words = paragraph.split(' ');
465
540
  let currentLine = '';
@@ -477,13 +552,30 @@ function wrapText(text: string, maxWidth: number): string[] {
477
552
  }
478
553
  // If the word itself is longer than maxWidth, just use it as is
479
554
  // (better to have a long line than break in the middle)
480
- currentLine = word;
555
+ // But if we have leading codes and this isn't the first line, apply them
556
+ if (leadingCodes && currentLine) {
557
+ // Strip any existing codes from the word to avoid duplication
558
+ const strippedWord = stripAnsiCodes(word);
559
+ currentLine = leadingCodes + strippedWord;
560
+ } else {
561
+ currentLine = word;
562
+ }
481
563
  }
482
564
  }
483
565
 
484
566
  if (currentLine) {
485
567
  allLines.push(currentLine);
486
568
  }
569
+
570
+ // If the original paragraph had ANSI codes and ended with reset,
571
+ // ensure each wrapped line ends with reset (only for this paragraph's lines)
572
+ if (leadingCodes && hasReset) {
573
+ for (let i = paragraphStart; i < allLines.length; i++) {
574
+ if (!endsWithReset(allLines[i])) {
575
+ allLines[i] += COLORS.reset;
576
+ }
577
+ }
578
+ }
487
579
  }
488
580
 
489
581
  return allLines.length > 0 ? allLines : [''];
@@ -658,6 +750,20 @@ export interface CommandRunnerOptions {
658
750
  * Defaults to false
659
751
  */
660
752
  clearOnSuccess?: boolean;
753
+ /**
754
+ * If true or undefined, will truncate each line of output
755
+ */
756
+ truncate?: boolean;
757
+
758
+ /**
759
+ * If undefined, will show up to 3 last lines of output while running. Customize the number with this property.
760
+ */
761
+ maxLinesOutput?: number;
762
+
763
+ /**
764
+ * If undefined, will show up to 10 last lines on failure. Customize the number with this property.
765
+ */
766
+ maxLinesOnFailure?: number;
661
767
  }
662
768
 
663
769
  /**
@@ -671,7 +777,16 @@ export interface CommandRunnerOptions {
671
777
  * Shows the last 3 lines of output as it streams.
672
778
  */
673
779
  export async function runCommand(options: CommandRunnerOptions): Promise<number> {
674
- const { command, cmd, cwd, env, clearOnSuccess = false } = options;
780
+ const {
781
+ command,
782
+ cmd,
783
+ cwd,
784
+ env,
785
+ clearOnSuccess = false,
786
+ truncate = true,
787
+ maxLinesOutput = 3,
788
+ maxLinesOnFailure = 10,
789
+ } = options;
675
790
  const isTTY = process.stdout.isTTY;
676
791
 
677
792
  // If not a TTY, just run the command normally and log output
@@ -746,7 +861,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
746
861
  };
747
862
 
748
863
  // Initial display
749
- renderOutput(3);
864
+ renderOutput(maxLinesOutput);
750
865
 
751
866
  try {
752
867
  // Spawn the command
@@ -805,10 +920,12 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
805
920
  process.stdout.write(`\x1b[${linesRendered}A`);
806
921
 
807
922
  // Show compact success: ✓ command
808
- process.stdout.write(`\r\x1b[K${green}${ICONS.success}${reset} ${cmdColor}${displayCmd}${reset}\n`);
923
+ process.stdout.write(
924
+ `\r\x1b[K${green}${ICONS.success}${reset} ${cmdColor}${displayCmd}${reset}\n`
925
+ );
809
926
  } else {
810
927
  // Determine how many lines to show in final output
811
- const finalLinesToShow = exitCode === 0 ? 3 : 10;
928
+ const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
812
929
 
813
930
  // Show final status with appropriate color
814
931
  const statusColor = exitCode === 0 ? green : red;
@@ -818,7 +935,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
818
935
  const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
819
936
  for (const line of finalOutputLines) {
820
937
  let displayLine = line;
821
- if (getDisplayWidth(displayLine) > maxLineWidth) {
938
+ if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
822
939
  displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
823
940
  }
824
941
  process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
package/src/types.ts CHANGED
@@ -150,6 +150,7 @@ export type CommandContext<
150
150
  // Helper to create subcommands with proper type inference
151
151
  export function createSubcommand<
152
152
  TRequiresAuth extends boolean,
153
+ TOptionalAuth extends boolean | string,
153
154
  TArgsSchema extends z.ZodType | undefined,
154
155
  TOptionsSchema extends z.ZodType | undefined,
155
156
  >(definition: {
@@ -158,6 +159,7 @@ export function createSubcommand<
158
159
  aliases?: string[];
159
160
  toplevel?: boolean;
160
161
  requiresAuth?: TRequiresAuth;
162
+ optionalAuth?: TOptionalAuth;
161
163
  schema?: TArgsSchema extends z.ZodType
162
164
  ? TOptionsSchema extends z.ZodType
163
165
  ? { args: TArgsSchema; options: TOptionsSchema }
@@ -166,7 +168,15 @@ export function createSubcommand<
166
168
  ? { options: TOptionsSchema }
167
169
  : never;
168
170
  handler(
169
- ctx: CommandContext<TRequiresAuth extends true ? true : false, TArgsSchema, TOptionsSchema>
171
+ ctx: CommandContext<
172
+ TRequiresAuth extends true
173
+ ? true
174
+ : TOptionalAuth extends true | string
175
+ ? true | false
176
+ : false,
177
+ TArgsSchema,
178
+ TOptionsSchema
179
+ >
170
180
  ): void | Promise<void>;
171
181
  }): SubcommandDefinition {
172
182
  return definition as unknown as SubcommandDefinition;
@@ -175,6 +185,7 @@ export function createSubcommand<
175
185
  // Helper to create commands with proper type inference
176
186
  export function createCommand<
177
187
  TRequiresAuth extends boolean,
188
+ TOptionalAuth extends boolean | string,
178
189
  TArgsSchema extends z.ZodType | undefined,
179
190
  TOptionsSchema extends z.ZodType | undefined,
180
191
  >(definition: {
@@ -183,6 +194,7 @@ export function createCommand<
183
194
  aliases?: string[];
184
195
  hidden?: boolean;
185
196
  requiresAuth?: TRequiresAuth;
197
+ optionalAuth?: TOptionalAuth;
186
198
  schema?: TArgsSchema extends z.ZodType
187
199
  ? TOptionsSchema extends z.ZodType
188
200
  ? { args: TArgsSchema; options: TOptionsSchema }
@@ -191,7 +203,15 @@ export function createCommand<
191
203
  ? { options: TOptionsSchema }
192
204
  : never;
193
205
  handler?(
194
- ctx: CommandContext<TRequiresAuth extends true ? true : false, TArgsSchema, TOptionsSchema>
206
+ ctx: CommandContext<
207
+ TRequiresAuth extends true
208
+ ? true
209
+ : TOptionalAuth extends true | string
210
+ ? true | false
211
+ : false,
212
+ TArgsSchema,
213
+ TOptionsSchema
214
+ >
195
215
  ): void | Promise<void>;
196
216
  subcommands?: SubcommandDefinition[];
197
217
  }): CommandDefinition {
@@ -206,6 +226,7 @@ export type SubcommandDefinition =
206
226
  aliases?: string[];
207
227
  toplevel?: boolean;
208
228
  requiresAuth: true;
229
+ optionalAuth?: false | string;
209
230
  schema?: CommandSchemas;
210
231
  handler(ctx: CommandContext): void | Promise<void>;
211
232
  }
@@ -215,6 +236,17 @@ export type SubcommandDefinition =
215
236
  aliases?: string[];
216
237
  toplevel?: boolean;
217
238
  requiresAuth?: false;
239
+ optionalAuth: true | string;
240
+ schema?: CommandSchemas;
241
+ handler(ctx: CommandContext): void | Promise<void>;
242
+ }
243
+ | {
244
+ name: string;
245
+ description: string;
246
+ aliases?: string[];
247
+ toplevel?: boolean;
248
+ requiresAuth?: false;
249
+ optionalAuth?: false;
218
250
  schema?: CommandSchemas;
219
251
  handler(ctx: CommandContext): void | Promise<void>;
220
252
  };
@@ -227,6 +259,18 @@ export type CommandDefinition =
227
259
  aliases?: string[];
228
260
  hidden?: boolean;
229
261
  requiresAuth: true;
262
+ optionalAuth?: false | string;
263
+ schema?: CommandSchemas;
264
+ handler?(ctx: CommandContext): void | Promise<void>;
265
+ subcommands?: SubcommandDefinition[];
266
+ }
267
+ | {
268
+ name: string;
269
+ description: string;
270
+ aliases?: string[];
271
+ hidden?: boolean;
272
+ requiresAuth?: false;
273
+ optionalAuth: true | string;
230
274
  schema?: CommandSchemas;
231
275
  handler?(ctx: CommandContext): void | Promise<void>;
232
276
  subcommands?: SubcommandDefinition[];
@@ -237,6 +281,7 @@ export type CommandDefinition =
237
281
  aliases?: string[];
238
282
  hidden?: boolean;
239
283
  requiresAuth?: false;
284
+ optionalAuth?: false;
240
285
  schema?: CommandSchemas;
241
286
  handler?(ctx: CommandContext): void | Promise<void>;
242
287
  subcommands?: SubcommandDefinition[];