@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
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandDefinition, SubcommandDefinition, CommandContext } from './types';
3
3
  import { showBanner } from './banner';
4
- import { requireAuth } from './auth';
4
+ import { requireAuth, optionalAuth } from './auth';
5
5
  import { parseArgsSchema, parseOptionsSchema, buildValidationInput } from './schema-parser';
6
6
 
7
7
  export async function createCLI(version: string): Promise<Command> {
@@ -121,6 +121,46 @@ async function registerSubcommand(
121
121
  };
122
122
  await subcommand.handler(ctx);
123
123
  }
124
+ } else if (subcommand.optionalAuth) {
125
+ const continueText =
126
+ typeof subcommand.optionalAuth === 'string' ? subcommand.optionalAuth : undefined;
127
+ const auth = await optionalAuth(baseCtx as CommandContext<false>, continueText);
128
+
129
+ if (subcommand.schema) {
130
+ try {
131
+ const input = buildValidationInput(subcommand.schema, args, options);
132
+ const ctx: Record<string, unknown> = {
133
+ ...baseCtx,
134
+ ...(auth ? { auth } : {}),
135
+ };
136
+ if (subcommand.schema.args) {
137
+ ctx.args = subcommand.schema.args.parse(input.args);
138
+ }
139
+ if (subcommand.schema.options) {
140
+ ctx.opts = subcommand.schema.options.parse(input.options);
141
+ }
142
+ await subcommand.handler(ctx as CommandContext);
143
+ } catch (error) {
144
+ if (error && typeof error === 'object' && 'issues' in error) {
145
+ baseCtx.logger.error('Validation error:');
146
+ const issues = (error as { issues: Array<{ path: string[]; message: string }> })
147
+ .issues;
148
+ for (const issue of issues) {
149
+ baseCtx.logger.error(` ${issue.path.join('.')}: ${issue.message}`);
150
+ }
151
+ process.exit(1);
152
+ }
153
+ throw error;
154
+ }
155
+ } else if (auth) {
156
+ const ctx: CommandContext<true> = {
157
+ ...baseCtx,
158
+ auth,
159
+ };
160
+ await subcommand.handler(ctx);
161
+ } else {
162
+ await subcommand.handler(baseCtx as CommandContext<false>);
163
+ }
124
164
  } else {
125
165
  if (subcommand.schema) {
126
166
  try {
@@ -175,6 +215,16 @@ export async function registerCommands(
175
215
  const auth = await requireAuth(baseCtx as CommandContext<false>);
176
216
  const ctx: CommandContext<true> = { ...baseCtx, auth };
177
217
  await cmdDef.handler!(ctx);
218
+ } else if (cmdDef.optionalAuth) {
219
+ const continueText =
220
+ typeof cmdDef.optionalAuth === 'string' ? cmdDef.optionalAuth : undefined;
221
+ const auth = await optionalAuth(baseCtx as CommandContext<false>, continueText);
222
+ if (auth) {
223
+ const ctx: CommandContext<true> = { ...baseCtx, auth };
224
+ await cmdDef.handler!(ctx);
225
+ } else {
226
+ await cmdDef.handler!(baseCtx as CommandContext<false>);
227
+ }
178
228
  } else {
179
229
  await cmdDef.handler!(baseCtx as CommandContext<false>);
180
230
  }
@@ -33,6 +33,34 @@ preferences:
33
33
  orgId: ''
34
34
  ```
35
35
 
36
+ ### `auth signup`
37
+
38
+ Create a new Agentuity Cloud Platform account.
39
+
40
+ ```bash
41
+ agentuity auth signup
42
+ # or
43
+ agentuity signup
44
+ ```
45
+
46
+ **How it works:**
47
+
48
+ 1. Generates a random 5-character OTP locally (client-side)
49
+ 2. Displays a signup URL with the OTP code: `/sign-up?code=<otp>`
50
+ 3. User opens the URL in their browser and completes the signup process
51
+ 4. Polls `GET /cli/auth/signup/<otp>` every 2 seconds for up to 5 minutes
52
+ 5. Server returns 404 until signup is complete, then returns credentials
53
+ 6. Once complete, saves the API key, user ID, and expiration to config
54
+
55
+ **Stored data:**
56
+
57
+ ```yaml
58
+ auth:
59
+ api_key: 'your-api-key'
60
+ user_id: 'user-id'
61
+ expires: 1234567890
62
+ ```
63
+
36
64
  ### `auth logout`
37
65
 
38
66
  Logout of the Agentuity Cloud Platform by clearing authentication credentials.
@@ -55,6 +83,7 @@ The authentication flow uses the following API endpoints:
55
83
 
56
84
  - `GET /cli/auth/start` - Generate OTP for login
57
85
  - `POST /cli/auth/check` - Poll for login completion with OTP
86
+ - `GET /cli/auth/signup/<otp>` - Poll for signup completion with client-generated OTP (returns 404 until complete)
58
87
 
59
88
  ## URL Configuration
60
89
 
@@ -80,13 +109,18 @@ This allows different profiles (e.g., `local`, `production`) to point to differe
80
109
 
81
110
  ## Implementation Details
82
111
 
83
- - **Generic API Client**: [../../api.ts](../../api.ts) provides the generic `APIClient` class for HTTP requests
84
- - **Auth-specific APIs**: [api.ts](./api.ts) provides `generateLoginOTP()` and `pollForLoginCompletion()` functions
112
+ - **Generic API Client**: [../../api.ts](../../api.ts) provides the generic `APIClient` class and `APIError` for HTTP requests
113
+ - **Auth-specific APIs**: [api.ts](./api.ts) provides:
114
+ - `generateLoginOTP()` and `pollForLoginCompletion()` for login flow
115
+ - `generateSignupOTP()` and `pollForSignupCompletion()` for signup flow
85
116
  - **Config Management**: [../../config.ts](../../config.ts) provides `saveAuth()`, `clearAuth()`, and `getAuth()` helpers
86
117
  - **Browser Opening**: Uses `Bun.spawn(['open', authURL])` on non-Windows platforms to auto-open browser
87
- - **Polling**: Polls every 2 seconds with 60-second timeout
118
+ - **Polling**:
119
+ - Login: Polls every 2 seconds with 60-second timeout
120
+ - Signup: Polls every 2 seconds with 5-minute timeout, retries on 404 errors
88
121
  - **Error Handling**: All errors are caught and displayed to user with appropriate exit codes
89
122
  - `UpgradeRequiredError`: Shows upgrade instructions when CLI version is outdated
123
+ - `APIError`: Preserves HTTP status codes for proper retry logic (e.g., 404 during signup polling)
90
124
  - Generic API errors: Display the error message from the server
91
125
  - See [../../api-errors.md](../../api-errors.md) for details
92
126
 
@@ -1,4 +1,4 @@
1
- import { APIClient } from '@/api';
1
+ import { APIClient, APIError } from '@/api';
2
2
  import type { Config } from '@/types';
3
3
 
4
4
  interface APIResponse<T> {
@@ -23,6 +23,18 @@ export interface LoginResult {
23
23
  expires: Date;
24
24
  }
25
25
 
26
+ interface SignupCompleteData {
27
+ userId: string;
28
+ apiKey: string;
29
+ expiresAt: number;
30
+ }
31
+
32
+ export interface SignupResult {
33
+ apiKey: string;
34
+ userId: string;
35
+ expires: Date;
36
+ }
37
+
26
38
  export async function generateLoginOTP(apiUrl: string, config?: Config | null): Promise<string> {
27
39
  const client = new APIClient(apiUrl, undefined, config);
28
40
  const resp = await client.request<APIResponse<OTPStartData>>('GET', '/cli/auth/start');
@@ -64,8 +76,60 @@ export async function pollForLoginCompletion(
64
76
  };
65
77
  }
66
78
 
67
- await new Promise((resolve) => setTimeout(resolve, 2000));
79
+ await Bun.sleep(2000);
68
80
  }
69
81
 
70
82
  throw new Error('Login timed out');
71
83
  }
84
+
85
+ export function generateSignupOTP(): string {
86
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
87
+ let result = '';
88
+ const array = new Uint8Array(5);
89
+ crypto.getRandomValues(array);
90
+ for (let i = 0; i < 5; i++) {
91
+ result += chars[array[i] % chars.length];
92
+ }
93
+ return result;
94
+ }
95
+
96
+ export async function pollForSignupCompletion(
97
+ apiUrl: string,
98
+ otp: string,
99
+ config?: Config | null,
100
+ timeoutMs = 300000
101
+ ): Promise<SignupResult> {
102
+ const client = new APIClient(apiUrl, undefined, config);
103
+ const started = Date.now();
104
+
105
+ while (Date.now() - started < timeoutMs) {
106
+ try {
107
+ const resp = await client.request<APIResponse<SignupCompleteData>>(
108
+ 'GET',
109
+ `/cli/auth/signup/${otp}`
110
+ );
111
+
112
+ if (!resp.success) {
113
+ throw new Error(resp.message);
114
+ }
115
+
116
+ if (resp.data) {
117
+ return {
118
+ apiKey: resp.data.apiKey,
119
+ userId: resp.data.userId,
120
+ expires: new Date(resp.data.expiresAt),
121
+ };
122
+ }
123
+ } catch (error) {
124
+ if (error instanceof APIError && error.status === 404) {
125
+ await Bun.sleep(2000);
126
+ continue;
127
+ }
128
+ throw error;
129
+ }
130
+
131
+ await Bun.sleep(2000);
132
+ }
133
+
134
+ throw new Error('Signup timed out');
135
+ }
@@ -1,9 +1,10 @@
1
1
  import { createCommand } from '@/types';
2
2
  import { loginCommand } from './login';
3
3
  import { logoutCommand } from './logout';
4
+ import { signupCommand } from './signup';
4
5
 
5
6
  export const command = createCommand({
6
7
  name: 'auth',
7
8
  description: 'Authentication and authorization related commands',
8
- subcommands: [loginCommand, logoutCommand],
9
+ subcommands: [loginCommand, logoutCommand, signupCommand],
9
10
  });
@@ -15,9 +15,16 @@ export const loginCommand: SubcommandDefinition = {
15
15
  const appUrl = getAppBaseURL(config);
16
16
 
17
17
  try {
18
- console.log('Generating login OTP...');
18
+ let otp: string | undefined;
19
+
20
+ await tui.spinner('Generating login one time code...', async () => {
21
+ otp = await generateLoginOTP(apiUrl, config);
22
+ });
23
+
24
+ if (!otp) {
25
+ return;
26
+ }
19
27
 
20
- const otp = await generateLoginOTP(apiUrl, config);
21
28
  const authURL = `${appUrl}/auth/cli`;
22
29
 
23
30
  const copied = await tui.copyToClipboard(otp);
@@ -39,7 +46,8 @@ export const loginCommand: SubcommandDefinition = {
39
46
  tui.newline();
40
47
 
41
48
  if (process.platform === 'darwin') {
42
- await tui.waitForAnyKey('Press Enter to open the URL...');
49
+ await tui.waitForAnyKey('Press any key to open the URL in your browser...');
50
+ tui.newline();
43
51
  try {
44
52
  Bun.spawn(['open', authURL], {
45
53
  stdio: ['ignore', 'ignore', 'ignore'],
@@ -0,0 +1,51 @@
1
+ import type { SubcommandDefinition } from '@/types';
2
+ import { getAPIBaseURL, getAppBaseURL, UpgradeRequiredError } from '@/api';
3
+ import { saveAuth } from '@/config';
4
+ import { generateSignupOTP, pollForSignupCompletion } from './api';
5
+ import * as tui from '@/tui';
6
+
7
+ export const signupCommand: SubcommandDefinition = {
8
+ name: 'signup',
9
+ description: 'Create a new Agentuity Cloud Platform account',
10
+ toplevel: true,
11
+
12
+ async handler(ctx) {
13
+ const { logger, config } = ctx;
14
+ const apiUrl = getAPIBaseURL(config);
15
+ const appUrl = getAppBaseURL(config);
16
+
17
+ try {
18
+ const otp = generateSignupOTP();
19
+
20
+ const signupURL = `${appUrl}/sign-up?code=${otp}`;
21
+
22
+ const bannerBody = `Please open the URL in your browser:\n\n${tui.link(signupURL)}\n\n${tui.muted('Once you have completed the signup process, you will be given a one-time password to complete the signup process.')}`;
23
+
24
+ tui.banner('Signup for Agentuity', bannerBody);
25
+ tui.newline();
26
+
27
+ await tui.spinner('Waiting for signup to complete...', async () => {
28
+ const result = await pollForSignupCompletion(apiUrl, otp, config);
29
+
30
+ await saveAuth({
31
+ apiKey: result.apiKey,
32
+ userId: result.userId,
33
+ expires: result.expires,
34
+ });
35
+ });
36
+
37
+ tui.newline();
38
+ tui.success('Welcome to Agentuity! You are now logged in');
39
+ } catch (error) {
40
+ if (error instanceof UpgradeRequiredError) {
41
+ const bannerBody = `${error.message}\n\nVisit: ${tui.link('https://agentuity.dev/CLI/installation')}`;
42
+ tui.banner('CLI Upgrade Required', bannerBody);
43
+ process.exit(1);
44
+ } else if (error instanceof Error) {
45
+ logger.fatal(`Signup failed: ${error.message}`);
46
+ } else {
47
+ logger.fatal('Signup failed');
48
+ }
49
+ }
50
+ },
51
+ };
@@ -1,8 +1,8 @@
1
1
  import { createCommand } from '@/types';
2
2
  import { z } from 'zod';
3
- import { resolve } from 'node:path';
3
+ import { resolve, join } from 'node:path';
4
4
  import { bundle } from '../bundle/bundler';
5
- import { existsSync } from 'node:fs';
5
+ import { existsSync, FSWatcher, watch } from 'node:fs';
6
6
  import * as tui from '@/tui';
7
7
 
8
8
  export const command = createCommand({
@@ -13,71 +13,156 @@ export const command = createCommand({
13
13
  dir: z.string().optional().describe('Root directory of the project'),
14
14
  }),
15
15
  },
16
+ optionalAuth: 'Continue without an account (local only)',
16
17
 
17
18
  async handler(ctx) {
18
- const { opts } = ctx;
19
+ const { opts, logger } = ctx;
20
+
19
21
  const rootDir = resolve(opts.dir || process.cwd());
22
+ const appTs = join(rootDir, 'app.ts');
23
+ const srcDir = join(rootDir, 'src');
24
+ const mustHaves = [join(rootDir, 'package.json'), appTs, srcDir];
25
+ const missing: string[] = [];
26
+
27
+ for (const filename of mustHaves) {
28
+ if (!existsSync(filename)) {
29
+ missing.push(filename);
30
+ }
31
+ }
32
+
33
+ if (missing.length) {
34
+ tui.error(`${rootDir} does not appear to be a valid Agentuity project`);
35
+ for (const filename of missing) {
36
+ tui.bullet(`Missing ${filename}`);
37
+ }
38
+ process.exit(1);
39
+ }
40
+
20
41
  const agentuityDir = resolve(rootDir, '.agentuity');
21
42
  const appPath = resolve(agentuityDir, 'app.js');
22
43
 
23
- try {
24
- await tui.spinner('Building project...', async () => {
25
- await bundle({
26
- rootDir,
27
- dev: true,
28
- });
29
- });
44
+ const watches = [appTs, srcDir];
45
+ const watchers: FSWatcher[] = [];
46
+ let failures = 0;
47
+ let running = false;
48
+ let pid = 0;
49
+ let failed = false;
50
+ let devServer: Bun.Subprocess | undefined;
30
51
 
31
- tui.success('Build complete');
52
+ function failure(msg: string) {
53
+ failed = true;
54
+ failures++;
55
+ if (failures >= 5) {
56
+ tui.error(msg);
57
+ tui.fatal('too many failures, exiting');
58
+ } else {
59
+ setImmediate(() => tui.error(msg));
60
+ }
61
+ }
32
62
 
33
- if (!existsSync(appPath)) {
34
- tui.error(`App file not found: ${appPath}`);
35
- process.exit(1);
63
+ const kill = () => {
64
+ running = false;
65
+ try {
66
+ // Kill the process group (negative PID kills entire group)
67
+ process.kill(-pid, 'SIGTERM');
68
+ } catch {
69
+ // Fallback: kill the direct process
70
+ try {
71
+ if (devServer) {
72
+ devServer.kill();
73
+ }
74
+ } catch {
75
+ // Ignore if already dead
76
+ }
77
+ } finally {
78
+ devServer = undefined;
36
79
  }
80
+ };
37
81
 
38
- tui.newline();
39
- tui.info('Starting development server...');
40
- tui.newline();
41
-
42
- // Use shell to run in a process group for proper cleanup
43
- // The 'exec' ensures the shell is replaced by the actual process
44
- const devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
45
- cwd: rootDir,
46
- stdout: 'inherit',
47
- stderr: 'inherit',
48
- stdin: 'inherit',
49
- });
50
-
51
- // Handle signals to ensure entire process tree is killed
52
- const cleanup = () => {
53
- if (devServer.pid) {
54
- try {
55
- // Kill the process group (negative PID kills entire group)
56
- process.kill(-devServer.pid, 'SIGTERM');
57
- } catch {
58
- // Fallback: kill the direct process
82
+ // Handle signals to ensure entire process tree is killed
83
+ const cleanup = () => {
84
+ if (pid && running) {
85
+ kill();
86
+ }
87
+ for (const watcher of watchers) {
88
+ watcher.close();
89
+ }
90
+ watchers.length = 0;
91
+ process.exit(0);
92
+ };
93
+
94
+ process.on('SIGINT', cleanup);
95
+ process.on('SIGTERM', cleanup);
96
+
97
+ async function restart() {
98
+ try {
99
+ if (running) {
100
+ tui.info('Restarting on file change');
101
+ kill();
102
+ return;
103
+ }
104
+ await Promise.all([
105
+ tui.runCommand({
106
+ command: 'tsc',
107
+ cmd: ['bunx', 'tsc', '--noEmit'],
108
+ cwd: rootDir,
109
+ clearOnSuccess: true,
110
+ truncate: false,
111
+ maxLinesOutput: 1,
112
+ maxLinesOnFailure: 15,
113
+ }),
114
+ tui.spinner('Building project', async () => {
59
115
  try {
60
- devServer.kill();
116
+ await bundle({
117
+ rootDir,
118
+ dev: true,
119
+ });
61
120
  } catch {
62
- // Ignore if already dead
121
+ failure('Build failed');
63
122
  }
64
- }
123
+ }),
124
+ ]);
125
+
126
+ if (failed) {
127
+ return;
65
128
  }
66
- process.exit(0);
67
- };
68
129
 
69
- process.on('SIGINT', cleanup);
70
- process.on('SIGTERM', cleanup);
130
+ if (!existsSync(appPath)) {
131
+ failure(`App file not found: ${appPath}`);
132
+ return;
133
+ }
71
134
 
72
- const exitCode = await devServer.exited;
73
- process.exit(exitCode);
74
- } catch (error) {
75
- if (error instanceof Error) {
76
- tui.error(`Dev server failed: ${error.message}`);
77
- } else {
78
- tui.error('Dev server failed');
135
+ // Use shell to run in a process group for proper cleanup
136
+ // The 'exec' ensures the shell is replaced by the actual process
137
+ const devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
138
+ cwd: rootDir,
139
+ stdout: 'inherit',
140
+ stderr: 'inherit',
141
+ stdin: 'inherit',
142
+ });
143
+
144
+ running = true;
145
+ failed = false;
146
+ pid = devServer.pid;
147
+
148
+ const exitCode = await devServer.exited;
149
+ if (exitCode === 0) {
150
+ process.exit(exitCode);
151
+ }
152
+ } catch (error) {
153
+ if (error instanceof Error) {
154
+ failure(`Dev server failed: ${error.message}`);
155
+ } else {
156
+ failure('Dev server failed');
157
+ }
158
+ } finally {
159
+ running = false;
79
160
  }
80
- process.exit(1);
161
+ }
162
+ await restart();
163
+ for (const filename of watches) {
164
+ logger.trace('watching %s', filename);
165
+ watchers.push(watch(filename, { recursive: true }, restart));
81
166
  }
82
167
  },
83
168
  });
@@ -8,6 +8,7 @@ import { versionSubcommand } from './version';
8
8
  import { createUserSubcommand } from './create-user';
9
9
  import { runCommandSubcommand } from './run-command';
10
10
  import { soundSubcommand } from './sound';
11
+ import { optionalAuthSubcommand } from './optional-auth';
11
12
 
12
13
  export const command = createCommand({
13
14
  name: 'example',
@@ -23,5 +24,6 @@ export const command = createCommand({
23
24
  createUserSubcommand,
24
25
  runCommandSubcommand,
25
26
  soundSubcommand,
27
+ optionalAuthSubcommand,
26
28
  ],
27
29
  });
@@ -0,0 +1,38 @@
1
+ import type { SubcommandDefinition, CommandContext, AuthData } from '@/types';
2
+ import * as tui from '@/tui';
3
+
4
+ export const optionalAuthSubcommand: SubcommandDefinition = {
5
+ name: 'optional-auth',
6
+ description: 'Test optional authentication flow',
7
+ optionalAuth: 'Continue with local features only',
8
+ handler: async (ctx: CommandContext) => {
9
+ tui.newline();
10
+
11
+ // Type guard to check if auth is present
12
+ const ctxWithAuth = ctx as CommandContext<true>;
13
+ if ('auth' in ctx && ctxWithAuth.auth) {
14
+ const auth = ctxWithAuth.auth as AuthData;
15
+ // User chose to authenticate
16
+ tui.success('You are authenticated!');
17
+ tui.info(`User ID: ${auth.userId}`);
18
+ tui.info(`Session expires: ${auth.expires.toLocaleString()}`);
19
+ tui.newline();
20
+ tui.info('You can now access cloud features:');
21
+ tui.bullet('Deploy to production');
22
+ tui.bullet('Access remote resources');
23
+ tui.bullet('View team analytics');
24
+ } else {
25
+ // User chose to continue without auth
26
+ tui.info('Running in local mode (no authentication)');
27
+ tui.newline();
28
+ tui.info('Available local features:');
29
+ tui.bullet('Local development');
30
+ tui.bullet('Offline testing');
31
+ tui.bullet('Build and bundle');
32
+ tui.newline();
33
+ tui.warning('Some cloud features are unavailable without authentication');
34
+ }
35
+
36
+ tui.newline();
37
+ },
38
+ };
@@ -6,9 +6,9 @@ export const soundSubcommand: SubcommandDefinition = {
6
6
  name: 'sound',
7
7
  description: 'Test completion sound',
8
8
 
9
- async handler() {
9
+ handler() {
10
10
  tui.info('Playing completion sound...');
11
- await playSound();
11
+ playSound();
12
12
  tui.success('Sound played!');
13
13
  },
14
14
  };
@@ -39,6 +39,7 @@ export const createProjectSubcommand = createSubcommand({
39
39
  const { logger, opts } = ctx;
40
40
  await runCreateFlow({
41
41
  projectName: opts.name,
42
+ dir: opts.dir,
42
43
  template: opts.template,
43
44
  templateDir: opts.templateDir,
44
45
  templateBranch: opts.templateBranch,