@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.
- package/bin/cli.ts +43 -2
- package/dist/api.d.ts +5 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/api.d.ts +7 -0
- package/dist/cmd/auth/api.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts +3 -0
- package/dist/cmd/auth/signup.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/example/index.d.ts.map +1 -1
- package/dist/cmd/example/optional-auth.d.ts +3 -0
- package/dist/cmd/example/optional-auth.d.ts.map +1 -0
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +1 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/sound.d.ts +1 -1
- package/dist/sound.d.ts.map +1 -1
- package/dist/tui.d.ts +23 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +29 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +16 -2
- package/src/auth.ts +79 -4
- package/src/cli.ts +51 -1
- package/src/cmd/auth/README.md +37 -3
- package/src/cmd/auth/api.ts +66 -2
- package/src/cmd/auth/index.ts +2 -1
- package/src/cmd/auth/login.ts +11 -3
- package/src/cmd/auth/signup.ts +51 -0
- package/src/cmd/dev/index.ts +135 -50
- package/src/cmd/example/index.ts +2 -0
- package/src/cmd/example/optional-auth.ts +38 -0
- package/src/cmd/example/sound.ts +2 -2
- package/src/cmd/project/create.ts +1 -0
- package/src/cmd/project/download.ts +37 -52
- package/src/cmd/project/template-flow.ts +26 -11
- package/src/config.ts +8 -1
- package/src/download.ts +2 -2
- package/src/index.ts +1 -0
- package/src/sound.ts +27 -13
- package/src/tui.ts +126 -9
- 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
|
}
|
package/src/cmd/auth/README.md
CHANGED
|
@@ -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
|
|
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**:
|
|
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
|
|
package/src/cmd/auth/api.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/cmd/auth/index.ts
CHANGED
|
@@ -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
|
});
|
package/src/cmd/auth/login.ts
CHANGED
|
@@ -15,9 +15,16 @@ export const loginCommand: SubcommandDefinition = {
|
|
|
15
15
|
const appUrl = getAppBaseURL(config);
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
|
|
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
|
|
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
|
+
};
|
package/src/cmd/dev/index.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
116
|
+
await bundle({
|
|
117
|
+
rootDir,
|
|
118
|
+
dev: true,
|
|
119
|
+
});
|
|
61
120
|
} catch {
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
130
|
+
if (!existsSync(appPath)) {
|
|
131
|
+
failure(`App file not found: ${appPath}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
71
134
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/cmd/example/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/cmd/example/sound.ts
CHANGED
|
@@ -6,9 +6,9 @@ export const soundSubcommand: SubcommandDefinition = {
|
|
|
6
6
|
name: 'sound',
|
|
7
7
|
description: 'Test completion sound',
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
handler() {
|
|
10
10
|
tui.info('Playing completion sound...');
|
|
11
|
-
|
|
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,
|