@agentuity/cli 0.0.12 → 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/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/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/tui.d.ts +22 -0
- 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 -48
- package/src/cmd/example/index.ts +2 -0
- package/src/cmd/example/optional-auth.ts +38 -0
- package/src/config.ts +8 -1
- package/src/index.ts +1 -0
- package/src/tui.ts +121 -6
- package/src/types.ts +47 -2
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,69 +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
|
-
|
|
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 () => {
|
|
57
115
|
try {
|
|
58
|
-
|
|
116
|
+
await bundle({
|
|
117
|
+
rootDir,
|
|
118
|
+
dev: true,
|
|
119
|
+
});
|
|
59
120
|
} catch {
|
|
60
|
-
|
|
121
|
+
failure('Build failed');
|
|
61
122
|
}
|
|
62
|
-
}
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
if (failed) {
|
|
127
|
+
return;
|
|
63
128
|
}
|
|
64
|
-
process.exit(0);
|
|
65
|
-
};
|
|
66
129
|
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
if (!existsSync(appPath)) {
|
|
131
|
+
failure(`App file not found: ${appPath}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
69
134
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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;
|
|
77
160
|
}
|
|
78
|
-
|
|
161
|
+
}
|
|
162
|
+
await restart();
|
|
163
|
+
for (const filename of watches) {
|
|
164
|
+
logger.trace('watching %s', filename);
|
|
165
|
+
watchers.push(watch(filename, { recursive: true }, restart));
|
|
79
166
|
}
|
|
80
167
|
},
|
|
81
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/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/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/tui.ts
CHANGED
|
@@ -82,6 +82,16 @@ 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
|
*/
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
864
|
+
renderOutput(maxLinesOutput);
|
|
750
865
|
|
|
751
866
|
try {
|
|
752
867
|
// Spawn the command
|
|
@@ -810,7 +925,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
|
|
|
810
925
|
);
|
|
811
926
|
} else {
|
|
812
927
|
// Determine how many lines to show in final output
|
|
813
|
-
const finalLinesToShow = exitCode === 0 ?
|
|
928
|
+
const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
|
|
814
929
|
|
|
815
930
|
// Show final status with appropriate color
|
|
816
931
|
const statusColor = exitCode === 0 ? green : red;
|
|
@@ -820,7 +935,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
|
|
|
820
935
|
const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
|
|
821
936
|
for (const line of finalOutputLines) {
|
|
822
937
|
let displayLine = line;
|
|
823
|
-
if (getDisplayWidth(displayLine) > maxLineWidth) {
|
|
938
|
+
if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
|
|
824
939
|
displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
|
|
825
940
|
}
|
|
826
941
|
process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
|