@agentuity/cli 1.0.9 → 1.0.11

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 (138) hide show
  1. package/dist/cmd/build/ast.d.ts +1 -1
  2. package/dist/cmd/build/ast.d.ts.map +1 -1
  3. package/dist/cmd/build/ast.js +103 -5
  4. package/dist/cmd/build/ast.js.map +1 -1
  5. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  6. package/dist/cmd/build/entry-generator.js +1 -8
  7. package/dist/cmd/build/entry-generator.js.map +1 -1
  8. package/dist/cmd/build/vite/config-loader.d.ts +9 -0
  9. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
  10. package/dist/cmd/build/vite/config-loader.js +30 -0
  11. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  12. package/dist/cmd/build/vite/index.d.ts +2 -0
  13. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  14. package/dist/cmd/build/vite/index.js +2 -1
  15. package/dist/cmd/build/vite/index.js.map +1 -1
  16. package/dist/cmd/build/vite/metadata-generator.d.ts +4 -1
  17. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/metadata-generator.js +1 -0
  19. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  20. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/route-discovery.js +23 -1
  22. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  23. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  24. package/dist/cmd/build/vite/vite-asset-server-config.js +19 -14
  25. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  26. package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
  27. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  28. package/dist/cmd/build/vite/vite-builder.js +13 -8
  29. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  30. package/dist/cmd/build/vite-bundler.d.ts +2 -0
  31. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  32. package/dist/cmd/build/vite-bundler.js +2 -1
  33. package/dist/cmd/build/vite-bundler.js.map +1 -1
  34. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  35. package/dist/cmd/cloud/db/list.js +14 -1
  36. package/dist/cmd/cloud/db/list.js.map +1 -1
  37. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  38. package/dist/cmd/cloud/deploy.js +36 -22
  39. package/dist/cmd/cloud/deploy.js.map +1 -1
  40. package/dist/cmd/cloud/queue/list.d.ts.map +1 -1
  41. package/dist/cmd/cloud/queue/list.js +10 -0
  42. package/dist/cmd/cloud/queue/list.js.map +1 -1
  43. package/dist/cmd/cloud/sandbox/list.d.ts.map +1 -1
  44. package/dist/cmd/cloud/sandbox/list.js +10 -0
  45. package/dist/cmd/cloud/sandbox/list.js.map +1 -1
  46. package/dist/cmd/cloud/sandbox/runtime/list.d.ts.map +1 -1
  47. package/dist/cmd/cloud/sandbox/runtime/list.js +10 -0
  48. package/dist/cmd/cloud/sandbox/runtime/list.js.map +1 -1
  49. package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
  50. package/dist/cmd/cloud/sandbox/snapshot/list.js +10 -0
  51. package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
  52. package/dist/cmd/cloud/session/get.js +12 -12
  53. package/dist/cmd/cloud/session/get.js.map +1 -1
  54. package/dist/cmd/cloud/session/list.d.ts.map +1 -1
  55. package/dist/cmd/cloud/session/list.js +14 -4
  56. package/dist/cmd/cloud/session/list.js.map +1 -1
  57. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  58. package/dist/cmd/cloud/storage/list.js +14 -1
  59. package/dist/cmd/cloud/storage/list.js.map +1 -1
  60. package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
  61. package/dist/cmd/cloud/stream/list.js +10 -0
  62. package/dist/cmd/cloud/stream/list.js.map +1 -1
  63. package/dist/cmd/cloud/thread/list.d.ts.map +1 -1
  64. package/dist/cmd/cloud/thread/list.js +10 -0
  65. package/dist/cmd/cloud/thread/list.js.map +1 -1
  66. package/dist/cmd/dev/download.d.ts.map +1 -1
  67. package/dist/cmd/dev/download.js +63 -53
  68. package/dist/cmd/dev/download.js.map +1 -1
  69. package/dist/cmd/project/domain/check.d.ts +2 -0
  70. package/dist/cmd/project/domain/check.d.ts.map +1 -0
  71. package/dist/cmd/project/domain/check.js +131 -0
  72. package/dist/cmd/project/domain/check.js.map +1 -0
  73. package/dist/cmd/project/domain/index.d.ts +2 -0
  74. package/dist/cmd/project/domain/index.d.ts.map +1 -0
  75. package/dist/cmd/project/domain/index.js +20 -0
  76. package/dist/cmd/project/domain/index.js.map +1 -0
  77. package/dist/cmd/project/hostname/get.d.ts +2 -0
  78. package/dist/cmd/project/hostname/get.d.ts.map +1 -0
  79. package/dist/cmd/project/hostname/get.js +50 -0
  80. package/dist/cmd/project/hostname/get.js.map +1 -0
  81. package/dist/cmd/project/hostname/index.d.ts +2 -0
  82. package/dist/cmd/project/hostname/index.d.ts.map +1 -0
  83. package/dist/cmd/project/hostname/index.js +18 -0
  84. package/dist/cmd/project/hostname/index.js.map +1 -0
  85. package/dist/cmd/project/hostname/set.d.ts +2 -0
  86. package/dist/cmd/project/hostname/set.d.ts.map +1 -0
  87. package/dist/cmd/project/hostname/set.js +100 -0
  88. package/dist/cmd/project/hostname/set.js.map +1 -0
  89. package/dist/cmd/project/index.d.ts.map +1 -1
  90. package/dist/cmd/project/index.js +12 -0
  91. package/dist/cmd/project/index.js.map +1 -1
  92. package/dist/index.d.ts +1 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +1 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/steps.d.ts +9 -0
  97. package/dist/steps.d.ts.map +1 -1
  98. package/dist/steps.js +131 -71
  99. package/dist/steps.js.map +1 -1
  100. package/dist/tui.d.ts +2 -2
  101. package/dist/tui.d.ts.map +1 -1
  102. package/dist/tui.js +6 -4
  103. package/dist/tui.js.map +1 -1
  104. package/dist/types.d.ts +4 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js.map +1 -1
  107. package/package.json +6 -7
  108. package/src/cmd/build/ast.ts +141 -5
  109. package/src/cmd/build/entry-generator.ts +1 -8
  110. package/src/cmd/build/vite/config-loader.ts +35 -0
  111. package/src/cmd/build/vite/index.ts +4 -0
  112. package/src/cmd/build/vite/metadata-generator.ts +5 -1
  113. package/src/cmd/build/vite/route-discovery.ts +34 -1
  114. package/src/cmd/build/vite/vite-asset-server-config.ts +22 -13
  115. package/src/cmd/build/vite/vite-builder.ts +20 -8
  116. package/src/cmd/build/vite-bundler.ts +4 -0
  117. package/src/cmd/cloud/db/list.ts +14 -1
  118. package/src/cmd/cloud/deploy.ts +46 -21
  119. package/src/cmd/cloud/queue/list.ts +10 -0
  120. package/src/cmd/cloud/sandbox/list.ts +10 -0
  121. package/src/cmd/cloud/sandbox/runtime/list.ts +10 -0
  122. package/src/cmd/cloud/sandbox/snapshot/list.ts +10 -0
  123. package/src/cmd/cloud/session/get.ts +12 -12
  124. package/src/cmd/cloud/session/list.ts +28 -18
  125. package/src/cmd/cloud/storage/list.ts +14 -1
  126. package/src/cmd/cloud/stream/list.ts +18 -8
  127. package/src/cmd/cloud/thread/list.ts +15 -5
  128. package/src/cmd/dev/download.ts +76 -78
  129. package/src/cmd/project/domain/check.ts +146 -0
  130. package/src/cmd/project/domain/index.ts +20 -0
  131. package/src/cmd/project/hostname/get.ts +54 -0
  132. package/src/cmd/project/hostname/index.ts +18 -0
  133. package/src/cmd/project/hostname/set.ts +123 -0
  134. package/src/cmd/project/index.ts +12 -0
  135. package/src/index.ts +1 -1
  136. package/src/steps.ts +139 -74
  137. package/src/tui.ts +6 -4
  138. package/src/types.ts +4 -2
@@ -0,0 +1,123 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { projectHostnameSet } from '@agentuity/server';
5
+ import { getCommand } from '../../../command-prefix';
6
+ import { isJSONMode } from '../../../output';
7
+ import { ErrorCode } from '../../../errors';
8
+
9
+ // Client-side reserved names list (mirrors server-side list)
10
+ const RESERVED_NAMES = new Set([
11
+ 'app',
12
+ 'api',
13
+ 'catalyst',
14
+ 'pulse',
15
+ 'streams',
16
+ 'registry',
17
+ 'ion',
18
+ 'status',
19
+ 'admin',
20
+ 'www',
21
+ 'mail',
22
+ 'dns',
23
+ 'console',
24
+ 'dashboard',
25
+ 'docs',
26
+ 'help',
27
+ 'support',
28
+ 'billing',
29
+ 'test',
30
+ 'staging',
31
+ 'dev',
32
+ 'prod',
33
+ 'ns0',
34
+ 'ns1',
35
+ 'ns2',
36
+ ]);
37
+
38
+ const HostnameSetResponseSchema = z.object({
39
+ hostname: z.string().describe('The vanity hostname that was set'),
40
+ url: z.string().describe('The full URL'),
41
+ });
42
+
43
+ export const setSubcommand = createSubcommand({
44
+ name: 'set',
45
+ description: 'Set a custom vanity hostname for the project on agentuity.run',
46
+ tags: ['mutating', 'fast', 'requires-auth', 'requires-project'],
47
+ requires: { auth: true, apiClient: true, project: true },
48
+ examples: [
49
+ {
50
+ command: getCommand('project hostname set my-cool-api'),
51
+ description: 'Set a custom hostname',
52
+ },
53
+ ],
54
+ schema: {
55
+ args: z.object({
56
+ hostname: z.string().describe('the vanity hostname (e.g., my-cool-api)'),
57
+ }),
58
+ response: HostnameSetResponseSchema,
59
+ },
60
+
61
+ async handler(ctx) {
62
+ const { args, apiClient, project, options, logger } = ctx;
63
+ const jsonMode = isJSONMode(options);
64
+
65
+ const hostname = args.hostname.toLowerCase().trim();
66
+
67
+ // Client-side validation: DNS label regex
68
+ const hostnameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
69
+ if (!hostnameRegex.test(hostname)) {
70
+ logger.fatal(
71
+ 'Invalid hostname: must contain only lowercase letters, numbers, and hyphens, and must start and end with a letter or number',
72
+ ErrorCode.VALIDATION_FAILED
73
+ );
74
+ }
75
+
76
+ // Max 63 chars (DNS label limit)
77
+ if (hostname.length > 63) {
78
+ logger.fatal(
79
+ 'Invalid hostname: must be 63 characters or fewer',
80
+ ErrorCode.VALIDATION_FAILED
81
+ );
82
+ }
83
+
84
+ // Must not match reserved hash patterns
85
+ const reservedHashPattern = /^[dp][a-f0-9]{16}$/;
86
+ if (reservedHashPattern.test(hostname)) {
87
+ logger.fatal(
88
+ 'Invalid hostname: this pattern is reserved for internal use',
89
+ ErrorCode.VALIDATION_FAILED
90
+ );
91
+ }
92
+
93
+ // Must not be a reserved name
94
+ if (RESERVED_NAMES.has(hostname)) {
95
+ logger.fatal(
96
+ `Invalid hostname: "${hostname}" is a reserved name`,
97
+ ErrorCode.VALIDATION_FAILED
98
+ );
99
+ }
100
+
101
+ const result = jsonMode
102
+ ? await projectHostnameSet(apiClient, {
103
+ projectId: project.projectId,
104
+ hostname,
105
+ })
106
+ : await tui.spinner('Setting hostname', () => {
107
+ return projectHostnameSet(apiClient, {
108
+ projectId: project.projectId,
109
+ hostname,
110
+ });
111
+ });
112
+
113
+ if (!jsonMode) {
114
+ tui.success(`Hostname set: ${tui.bold(result.url)}`);
115
+ tui.info('Hostname will be active after next deployment');
116
+ }
117
+
118
+ return {
119
+ hostname: result.hostname,
120
+ url: result.url,
121
+ };
122
+ },
123
+ });
@@ -6,6 +6,8 @@ import { deleteSubcommand } from './delete';
6
6
  import { showSubcommand } from './show';
7
7
  import { authCommand } from './auth';
8
8
  import { addCommand } from './add';
9
+ import { hostnameCommand } from './hostname';
10
+ import { domainCommand } from './domain';
9
11
  import { getCommand } from '../../command-prefix';
10
12
 
11
13
  export const command = createCommand({
@@ -22,6 +24,14 @@ export const command = createCommand({
22
24
  command: getCommand('project add storage'),
23
25
  description: 'Link an existing storage bucket',
24
26
  },
27
+ {
28
+ command: getCommand('project hostname get'),
29
+ description: 'Show current vanity hostname',
30
+ },
31
+ {
32
+ command: getCommand('project domain check'),
33
+ description: 'Check DNS for custom domains',
34
+ },
25
35
  ],
26
36
  subcommands: [
27
37
  createProjectSubcommand,
@@ -31,5 +41,7 @@ export const command = createCommand({
31
41
  showSubcommand,
32
42
  authCommand,
33
43
  addCommand,
44
+ hostnameCommand,
45
+ domainCommand,
34
46
  ],
35
47
  });
package/src/index.ts CHANGED
@@ -102,7 +102,7 @@ export {
102
102
  type CommandHandler,
103
103
  type TableColumn,
104
104
  } from './repl';
105
- export { runSteps, stepSuccess, stepSkipped, stepError } from './steps';
105
+ export { runSteps, stepSuccess, stepSkipped, stepError, StepInterruptError } from './steps';
106
106
  export { playSound } from './sound';
107
107
  export {
108
108
  downloadWithProgress,
package/src/steps.ts CHANGED
@@ -11,6 +11,19 @@ import { ValidationInputError, ValidationOutputError, type IssuesType } from '@a
11
11
  import { clearLastLines, isTTYLike } from './tui';
12
12
  import { appendLog, isLogCollectionEnabled } from './log-collector';
13
13
 
14
+ /**
15
+ * Error thrown when step execution is interrupted by a signal (e.g., Ctrl+C).
16
+ * Callers can catch this to perform cleanup before exiting.
17
+ */
18
+ export class StepInterruptError extends Error {
19
+ public readonly exitCode: number;
20
+ constructor(exitCode = 130) {
21
+ super('Step execution interrupted');
22
+ this.name = 'StepInterruptError';
23
+ this.exitCode = exitCode;
24
+ }
25
+ }
26
+
14
27
  // Spinner frames
15
28
  const FRAMES = ['◐', '◓', '◑', '◒'];
16
29
 
@@ -77,6 +90,7 @@ export const stepError = (message: string, cause?: Error, output?: string[]): St
77
90
  */
78
91
  export interface StepContext {
79
92
  progress: (n: number) => void;
93
+ signal: AbortSignal;
80
94
  }
81
95
 
82
96
  /**
@@ -300,14 +314,6 @@ export function pauseStepUI(clear = false): () => void {
300
314
  };
301
315
  }
302
316
 
303
- /**
304
- * Get exit function (Bun.exit or process.exit)
305
- */
306
- function getExitFn(): (code: number) => never {
307
- const bunExit = (globalThis as { Bun?: { exit?: (code: number) => never } }).Bun?.exit;
308
- return typeof bunExit === 'function' ? bunExit : process.exit.bind(process);
309
- }
310
-
311
317
  /**
312
318
  * Install interrupt handlers (SIGINT/SIGTERM + raw mode)
313
319
  */
@@ -367,19 +373,24 @@ async function runStepsTUI(steps: Step[]): Promise<void> {
367
373
  let currentStepIndex = -1;
368
374
  let currentFrameIndex = 0;
369
375
 
376
+ // Create abort controller for cooperative cancellation
377
+ const abortController = new AbortController();
378
+
370
379
  // Hide cursor
371
380
  process.stdout.write('\x1B[?25l');
372
381
 
373
382
  // Set up interrupt handler
374
- const exit = getExitFn();
383
+ let restoreInterrupts: (() => void) | null = null;
375
384
  const onInterrupt = () => {
376
385
  if (interrupted) return;
377
386
  interrupted = true;
387
+ abortController.abort();
378
388
  if (activeInterval) clearInterval(activeInterval);
389
+ restoreInterrupts?.();
390
+ restoreInterrupts = null;
379
391
  process.stdout.write('\x1B[?25h\n'); // Show cursor
380
- exit(130);
381
392
  };
382
- const restoreInterrupts = installInterruptHandlers(onInterrupt);
393
+ restoreInterrupts = installInterruptHandlers(onInterrupt);
383
394
 
384
395
  // Force re-render function
385
396
  const forceRerender = (skipMove = false) => {
@@ -476,7 +487,10 @@ async function runStepsTUI(steps: Step[]): Promise<void> {
476
487
 
477
488
  // Run the step
478
489
  try {
479
- const outcome = await step.run({ progress: progressCallback });
490
+ const outcome = await step.run({
491
+ progress: progressCallback,
492
+ signal: abortController.signal,
493
+ });
480
494
 
481
495
  // Update state from outcome
482
496
  if (outcome.status === 'success') {
@@ -493,6 +507,11 @@ async function runStepsTUI(steps: Step[]): Promise<void> {
493
507
  stepState.output = outcome.output;
494
508
  }
495
509
  } catch (err) {
510
+ // If the step was aborted due to cancellation, treat as interrupt
511
+ if (err instanceof Error && err.name === 'AbortError') {
512
+ interrupted = true;
513
+ throw new StepInterruptError();
514
+ }
496
515
  stepState.status = 'error';
497
516
  stepState.errorMessage = err instanceof Error ? err.message : String(err);
498
517
  stepState.errorCause = err instanceof Error ? err : undefined;
@@ -522,6 +541,14 @@ async function runStepsTUI(steps: Step[]): Promise<void> {
522
541
 
523
542
  // Handle errors
524
543
  if (stepState.status === 'error') {
544
+ // If the error is due to abort/cancellation, treat as interrupt
545
+ if (
546
+ stepState.errorCause instanceof Error &&
547
+ stepState.errorCause.name === 'AbortError'
548
+ ) {
549
+ interrupted = true;
550
+ throw new StepInterruptError();
551
+ }
525
552
  const errorColor = getColor('red');
526
553
  const errorMsg = stepState.errorMessage || 'An unknown error occurred';
527
554
  console.error(`\n${errorColor}Error: ${errorMsg}${COLORS.reset}`);
@@ -537,13 +564,19 @@ async function runStepsTUI(steps: Step[]): Promise<void> {
537
564
  }
538
565
  }
539
566
 
567
+ // If interrupted during step execution, throw so callers' finally blocks run
568
+ if (interrupted) {
569
+ throw new StepInterruptError();
570
+ }
571
+
540
572
  // Show cursor
541
573
  process.stdout.write('\x1B[?25h');
542
574
  } catch (err) {
543
575
  process.stdout.write('\x1B[?25h');
544
576
  throw err;
545
577
  } finally {
546
- restoreInterrupts();
578
+ restoreInterrupts?.();
579
+ restoreInterrupts = null;
547
580
  getTotalLinesFn = null; // Clear pause capability
548
581
  forceRerenderFn = null;
549
582
  }
@@ -558,76 +591,108 @@ async function runStepsPlain(steps: Step[]): Promise<void> {
558
591
  const yellowColor = getColor('yellow');
559
592
  const redColor = getColor('red');
560
593
 
561
- for (const step of steps) {
562
- let outcome: StepOutcome;
563
-
564
- try {
565
- outcome = await step.run({ progress: () => {} });
566
- } catch (err) {
567
- outcome = {
568
- status: 'error',
569
- message: err instanceof Error ? err.message : String(err),
570
- cause: err instanceof Error ? err : undefined,
571
- };
572
- }
594
+ const abortController = new AbortController();
595
+ let interrupted = false;
596
+ const onInterrupt = () => {
597
+ if (interrupted) return;
598
+ interrupted = true;
599
+ abortController.abort();
600
+ };
601
+ process.on('SIGINT', onInterrupt);
602
+ process.on('SIGTERM', onInterrupt);
573
603
 
574
- // Build step state for clean log emission
575
- const stepState: StepState = {
576
- label: step.label,
577
- status: outcome.status,
578
- output: outcome.output,
579
- skipReason: outcome.status === 'skipped' ? outcome.reason : undefined,
580
- errorMessage: outcome.status === 'error' ? outcome.message : undefined,
581
- errorCause: outcome.status === 'error' ? outcome.cause : undefined,
582
- };
604
+ try {
605
+ for (const step of steps) {
606
+ if (abortController.signal.aborted) break;
583
607
 
584
- // Emit clean log for this step
585
- emitCleanStepLog(stepState);
608
+ let outcome: StepOutcome;
586
609
 
587
- // Print final state
588
- if (outcome.status === 'success') {
589
- console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
590
- if (outcome.output && outcome.output.length > 0) {
591
- console.log(`${grayColor}╭─ Output${COLORS.reset}`);
592
- for (const line of outcome.output) {
593
- console.log(`${grayColor}│${COLORS.reset} ${line}`);
610
+ try {
611
+ outcome = await step.run({ progress: () => {}, signal: abortController.signal });
612
+ } catch (err) {
613
+ // If the step was aborted due to cancellation, treat as interrupt
614
+ if (err instanceof Error && err.name === 'AbortError') {
615
+ interrupted = true;
616
+ throw new StepInterruptError();
594
617
  }
595
- console.log(`${grayColor}╰─${COLORS.reset}`);
596
- console.log('');
618
+ outcome = {
619
+ status: 'error',
620
+ message: err instanceof Error ? err.message : String(err),
621
+ cause: err instanceof Error ? err : undefined,
622
+ };
597
623
  }
598
- } else if (outcome.status === 'skipped') {
599
- const reason = outcome.reason ? ` ${grayColor}(${outcome.reason})${COLORS.reset}` : '';
600
- console.log(`${yellowColor}${ICONS.skipped}${COLORS.reset} ${step.label}${reason}`);
601
- if (outcome.output && outcome.output.length > 0) {
602
- console.log(`${grayColor}╭─ Output${COLORS.reset}`);
603
- for (const line of outcome.output) {
604
- console.log(`${grayColor}│${COLORS.reset} ${line}`);
624
+
625
+ // Build step state for clean log emission
626
+ const stepState: StepState = {
627
+ label: step.label,
628
+ status: outcome.status,
629
+ output: outcome.output,
630
+ skipReason: outcome.status === 'skipped' ? outcome.reason : undefined,
631
+ errorMessage: outcome.status === 'error' ? outcome.message : undefined,
632
+ errorCause: outcome.status === 'error' ? outcome.cause : undefined,
633
+ };
634
+
635
+ // Emit clean log for this step
636
+ emitCleanStepLog(stepState);
637
+
638
+ // Print final state
639
+ if (outcome.status === 'success') {
640
+ console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
641
+ if (outcome.output && outcome.output.length > 0) {
642
+ console.log(`${grayColor}╭─ Output${COLORS.reset}`);
643
+ for (const line of outcome.output) {
644
+ console.log(`${grayColor}│${COLORS.reset} ${line}`);
645
+ }
646
+ console.log(`${grayColor}╰─${COLORS.reset}`);
647
+ console.log('');
605
648
  }
606
- console.log(`${grayColor}╰─${COLORS.reset}`);
607
- console.log('');
608
- }
609
- } else {
610
- console.log(`${redColor}${ICONS.error}${COLORS.reset} ${step.label}`);
611
- if (outcome.output && outcome.output.length > 0) {
612
- console.log(`${grayColor}╭─ Output${COLORS.reset}`);
613
- for (const line of outcome.output) {
614
- console.log(`${grayColor}│${COLORS.reset} ${line}`);
649
+ } else if (outcome.status === 'skipped') {
650
+ const reason = outcome.reason ? ` ${grayColor}(${outcome.reason})${COLORS.reset}` : '';
651
+ console.log(`${yellowColor}${ICONS.skipped}${COLORS.reset} ${step.label}${reason}`);
652
+ if (outcome.output && outcome.output.length > 0) {
653
+ console.log(`${grayColor}╭─ Output${COLORS.reset}`);
654
+ for (const line of outcome.output) {
655
+ console.log(`${grayColor}│${COLORS.reset} ${line}`);
656
+ }
657
+ console.log(`${grayColor}╰─${COLORS.reset}`);
658
+ console.log('');
615
659
  }
616
- console.log(`${grayColor}╰─${COLORS.reset}`);
617
- console.log('');
618
- }
619
- const errorColor = getColor('red');
620
- const errorMsg = outcome.message || 'An unknown error occurred';
621
- console.error(`\n${errorColor}Error: ${errorMsg}${COLORS.reset}`);
622
- if (
623
- outcome.cause instanceof ValidationInputError ||
624
- outcome.cause instanceof ValidationOutputError
625
- ) {
626
- printValidationIssues(outcome.cause.issues);
660
+ } else {
661
+ // If the error is due to abort/cancellation, treat as interrupt
662
+ if (outcome.cause instanceof Error && outcome.cause.name === 'AbortError') {
663
+ interrupted = true;
664
+ throw new StepInterruptError();
665
+ }
666
+ console.log(`${redColor}${ICONS.error}${COLORS.reset} ${step.label}`);
667
+ if (outcome.output && outcome.output.length > 0) {
668
+ console.log(`${grayColor}╭─ Output${COLORS.reset}`);
669
+ for (const line of outcome.output) {
670
+ console.log(`${grayColor}│${COLORS.reset} ${line}`);
671
+ }
672
+ console.log(`${grayColor}╰─${COLORS.reset}`);
673
+ console.log('');
674
+ }
675
+ const errorColor = getColor('red');
676
+ const errorMsg = outcome.message || 'An unknown error occurred';
677
+ console.error(`\n${errorColor}Error: ${errorMsg}${COLORS.reset}`);
678
+ if (
679
+ outcome.cause instanceof ValidationInputError ||
680
+ outcome.cause instanceof ValidationOutputError
681
+ ) {
682
+ printValidationIssues(outcome.cause.issues);
683
+ }
684
+ console.error('');
685
+ process.exit(1);
627
686
  }
628
- console.error('');
629
- process.exit(1);
630
687
  }
688
+
689
+ // If interrupted during step execution, throw so callers' finally blocks run
690
+ if (interrupted) {
691
+ throw new StepInterruptError();
692
+ }
693
+ } finally {
694
+ process.off('SIGINT', onInterrupt);
695
+ process.off('SIGTERM', onInterrupt);
631
696
  }
632
697
  }
633
698
 
package/src/tui.ts CHANGED
@@ -737,7 +737,7 @@ export function banner(title: string, body: string, options?: BannerOptions): vo
737
737
  /**
738
738
  * Wait for any key press before continuing
739
739
  * Displays a prompt message and waits for user input
740
- * Exits with code 1 if CTRL+C is pressed
740
+ * Raises SIGINT if CTRL+C is pressed
741
741
  */
742
742
  export async function waitForAnyKey(message = 'Press Enter to continue...'): Promise<void> {
743
743
  process.stdout.write(muted(message));
@@ -765,7 +765,8 @@ export async function waitForAnyKey(message = 'Press Enter to continue...'): Pro
765
765
  // Check for CTRL+C (character code 3)
766
766
  if (data.length === 1 && data[0] === 3) {
767
767
  console.log('\n');
768
- process.exit(1);
768
+ process.kill(process.pid, 'SIGINT');
769
+ return;
769
770
  }
770
771
 
771
772
  console.log('');
@@ -777,7 +778,7 @@ export async function waitForAnyKey(message = 'Press Enter to continue...'): Pro
777
778
  /**
778
779
  * Prompts user with a yes/no question
779
780
  * Returns true for yes, false for no
780
- * Exits with code 1 if CTRL+C is pressed
781
+ * Raises SIGINT if CTRL+C is pressed
781
782
  */
782
783
  export async function confirm(message: string, defaultValue = true): Promise<boolean> {
783
784
  const suffix = defaultValue ? '[Y/n]' : '[y/N]';
@@ -805,7 +806,8 @@ export async function confirm(message: string, defaultValue = true): Promise<boo
805
806
  // Check for CTRL+C (character code 3)
806
807
  if (data.length === 1 && data[0] === 3) {
807
808
  console.log('\n');
808
- process.exit(1);
809
+ process.kill(process.pid, 'SIGINT');
810
+ return;
809
811
  }
810
812
 
811
813
  const input = data.toString().trim().toLowerCase();
package/src/types.ts CHANGED
@@ -228,8 +228,10 @@ export interface AgentuityConfig {
228
228
  analytics?: boolean | AnalyticsConfig;
229
229
 
230
230
  /**
231
- * Vite plugins to add to the client build
232
- * These are added AFTER Agentuity's built-in plugins
231
+ * Vite plugins for the client build (src/web/).
232
+ * Should include your framework plugin (e.g., @vitejs/plugin-react).
233
+ * If no framework plugin is detected, React is added automatically
234
+ * for backwards compatibility.
233
235
  */
234
236
  plugins?: import('vite').PluginOption[];
235
237
  /**