@agentuity/cli 0.0.6
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/AGENTS.md +139 -0
- package/README.md +239 -0
- package/bin/cli.ts +71 -0
- package/dist/api.d.ts +25 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/banner.d.ts +2 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cmd/auth/api.d.ts +9 -0
- package/dist/cmd/auth/api.d.ts.map +1 -0
- package/dist/cmd/auth/index.d.ts +2 -0
- package/dist/cmd/auth/index.d.ts.map +1 -0
- package/dist/cmd/auth/login.d.ts +3 -0
- package/dist/cmd/auth/login.d.ts.map +1 -0
- package/dist/cmd/auth/logout.d.ts +3 -0
- package/dist/cmd/auth/logout.d.ts.map +1 -0
- package/dist/cmd/bundle/ast.d.ts +2 -0
- package/dist/cmd/bundle/ast.d.ts.map +1 -0
- package/dist/cmd/bundle/bundler.d.ts +6 -0
- package/dist/cmd/bundle/bundler.d.ts.map +1 -0
- package/dist/cmd/bundle/file.d.ts +2 -0
- package/dist/cmd/bundle/file.d.ts.map +1 -0
- package/dist/cmd/bundle/index.d.ts +2 -0
- package/dist/cmd/bundle/index.d.ts.map +1 -0
- package/dist/cmd/bundle/plugin.d.ts +4 -0
- package/dist/cmd/bundle/plugin.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts +2 -0
- package/dist/cmd/dev/index.d.ts.map +1 -0
- package/dist/cmd/example/create-user.d.ts +2 -0
- package/dist/cmd/example/create-user.d.ts.map +1 -0
- package/dist/cmd/example/create.d.ts +2 -0
- package/dist/cmd/example/create.d.ts.map +1 -0
- package/dist/cmd/example/deploy.d.ts +2 -0
- package/dist/cmd/example/deploy.d.ts.map +1 -0
- package/dist/cmd/example/index.d.ts +2 -0
- package/dist/cmd/example/index.d.ts.map +1 -0
- package/dist/cmd/example/list.d.ts +2 -0
- package/dist/cmd/example/list.d.ts.map +1 -0
- package/dist/cmd/example/run-command.d.ts +2 -0
- package/dist/cmd/example/run-command.d.ts.map +1 -0
- package/dist/cmd/example/sound.d.ts +3 -0
- package/dist/cmd/example/sound.d.ts.map +1 -0
- package/dist/cmd/example/spinner.d.ts +2 -0
- package/dist/cmd/example/spinner.d.ts.map +1 -0
- package/dist/cmd/example/steps.d.ts +2 -0
- package/dist/cmd/example/steps.d.ts.map +1 -0
- package/dist/cmd/example/version.d.ts +2 -0
- package/dist/cmd/example/version.d.ts.map +1 -0
- package/dist/cmd/index.d.ts +3 -0
- package/dist/cmd/index.d.ts.map +1 -0
- package/dist/cmd/profile/create.d.ts +2 -0
- package/dist/cmd/profile/create.d.ts.map +1 -0
- package/dist/cmd/profile/delete.d.ts +2 -0
- package/dist/cmd/profile/delete.d.ts.map +1 -0
- package/dist/cmd/profile/index.d.ts +2 -0
- package/dist/cmd/profile/index.d.ts.map +1 -0
- package/dist/cmd/profile/list.d.ts +3 -0
- package/dist/cmd/profile/list.d.ts.map +1 -0
- package/dist/cmd/profile/show.d.ts +2 -0
- package/dist/cmd/profile/show.d.ts.map +1 -0
- package/dist/cmd/profile/use.d.ts +2 -0
- package/dist/cmd/profile/use.d.ts.map +1 -0
- package/dist/cmd/project/create.d.ts +2 -0
- package/dist/cmd/project/create.d.ts.map +1 -0
- package/dist/cmd/project/delete.d.ts +2 -0
- package/dist/cmd/project/delete.d.ts.map +1 -0
- package/dist/cmd/project/index.d.ts +2 -0
- package/dist/cmd/project/index.d.ts.map +1 -0
- package/dist/cmd/project/list.d.ts +2 -0
- package/dist/cmd/project/list.d.ts.map +1 -0
- package/dist/cmd/project/show.d.ts +2 -0
- package/dist/cmd/project/show.d.ts.map +1 -0
- package/dist/cmd/version/index.d.ts +2 -0
- package/dist/cmd/version/index.d.ts.map +1 -0
- package/dist/command-prefix.d.ts +11 -0
- package/dist/command-prefix.d.ts.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/legacy-check.d.ts +6 -0
- package/dist/legacy-check.d.ts.map +1 -0
- package/dist/logger.d.ts +24 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/schema-parser.d.ts +24 -0
- package/dist/schema-parser.d.ts.map +1 -0
- package/dist/sound.d.ts +2 -0
- package/dist/sound.d.ts.map +1 -0
- package/dist/steps.d.ts +59 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/tui.d.ts +156 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/api-errors.md +115 -0
- package/src/api.ts +186 -0
- package/src/auth.ts +91 -0
- package/src/banner.ts +23 -0
- package/src/cli.ts +198 -0
- package/src/cmd/auth/README.md +95 -0
- package/src/cmd/auth/api.ts +71 -0
- package/src/cmd/auth/index.ts +9 -0
- package/src/cmd/auth/login.ts +76 -0
- package/src/cmd/auth/logout.ts +14 -0
- package/src/cmd/bundle/ast.ts +228 -0
- package/src/cmd/bundle/bundler.ts +88 -0
- package/src/cmd/bundle/file.ts +16 -0
- package/src/cmd/bundle/index.ts +38 -0
- package/src/cmd/bundle/plugin.ts +259 -0
- package/src/cmd/dev/index.ts +83 -0
- package/src/cmd/example/create-user.ts +38 -0
- package/src/cmd/example/create.ts +31 -0
- package/src/cmd/example/deploy.ts +36 -0
- package/src/cmd/example/index.ts +27 -0
- package/src/cmd/example/list.ts +32 -0
- package/src/cmd/example/run-command.ts +45 -0
- package/src/cmd/example/sound.ts +14 -0
- package/src/cmd/example/spinner.ts +44 -0
- package/src/cmd/example/steps.ts +66 -0
- package/src/cmd/example/version.ts +13 -0
- package/src/cmd/index.ts +46 -0
- package/src/cmd/profile/README.md +80 -0
- package/src/cmd/profile/create.ts +57 -0
- package/src/cmd/profile/delete.ts +52 -0
- package/src/cmd/profile/index.ts +12 -0
- package/src/cmd/profile/list.ts +27 -0
- package/src/cmd/profile/show.ts +54 -0
- package/src/cmd/profile/use.ts +30 -0
- package/src/cmd/project/create.ts +247 -0
- package/src/cmd/project/delete.ts +13 -0
- package/src/cmd/project/index.ts +11 -0
- package/src/cmd/project/list.ts +13 -0
- package/src/cmd/project/show.ts +12 -0
- package/src/cmd/version/index.ts +16 -0
- package/src/command-prefix.ts +43 -0
- package/src/config.ts +304 -0
- package/src/index.ts +40 -0
- package/src/legacy-check.ts +127 -0
- package/src/logger.ts +235 -0
- package/src/runtime.ts +22 -0
- package/src/schema-parser.ts +213 -0
- package/src/sound.ts +25 -0
- package/src/steps.ts +245 -0
- package/src/terminal.ts +151 -0
- package/src/tui.md +254 -0
- package/src/tui.ts +838 -0
- package/src/types.ts +243 -0
- package/src/version.ts +29 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CommandDefinition, SubcommandDefinition, CommandContext } from './types';
|
|
3
|
+
import { showBanner } from './banner';
|
|
4
|
+
import { requireAuth } from './auth';
|
|
5
|
+
import { parseArgsSchema, parseOptionsSchema, buildValidationInput } from './schema-parser';
|
|
6
|
+
|
|
7
|
+
export async function createCLI(version: string): Promise<Command> {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('agentuity')
|
|
12
|
+
.description('Agentuity CLI')
|
|
13
|
+
.version(version, '-V, --version', 'Display version')
|
|
14
|
+
.helpOption('-h, --help', 'Display help');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.option('--config <path>', 'Config file path', '~/.config/agentuity/config.yaml')
|
|
18
|
+
.option('--log-level <level>', 'Log level', 'info')
|
|
19
|
+
.option('--log-timestamp', 'Show timestamps in log output', false)
|
|
20
|
+
.option('--no-log-prefix', 'Hide log level prefixes', false)
|
|
21
|
+
.option('--color-scheme <scheme>', 'Color scheme: light or dark');
|
|
22
|
+
|
|
23
|
+
const skipVersionCheckOption = program.createOption(
|
|
24
|
+
'--skip-version-check',
|
|
25
|
+
'Skip version compatibility check (dev only)'
|
|
26
|
+
);
|
|
27
|
+
skipVersionCheckOption.hideHelp();
|
|
28
|
+
program.addOption(skipVersionCheckOption);
|
|
29
|
+
|
|
30
|
+
program.action(() => {
|
|
31
|
+
showBanner(version);
|
|
32
|
+
program.help();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return program;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function registerSubcommand(
|
|
39
|
+
parent: Command,
|
|
40
|
+
subcommand: SubcommandDefinition,
|
|
41
|
+
baseCtx: CommandContext,
|
|
42
|
+
hidden?: boolean
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const cmd = parent.command(subcommand.name, { hidden }).description(subcommand.description);
|
|
45
|
+
|
|
46
|
+
if (subcommand.aliases) {
|
|
47
|
+
cmd.aliases(subcommand.aliases);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Auto-generate arguments and options from schemas
|
|
51
|
+
if (subcommand.schema?.args) {
|
|
52
|
+
const parsed = parseArgsSchema(subcommand.schema.args);
|
|
53
|
+
for (const argMeta of parsed.metadata) {
|
|
54
|
+
let argSyntax: string;
|
|
55
|
+
if (argMeta.variadic) {
|
|
56
|
+
argSyntax = argMeta.optional ? `[${argMeta.name}...]` : `<${argMeta.name}...>`;
|
|
57
|
+
} else {
|
|
58
|
+
argSyntax = argMeta.optional ? `[${argMeta.name}]` : `<${argMeta.name}>`;
|
|
59
|
+
}
|
|
60
|
+
cmd.argument(argSyntax);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (subcommand.schema?.options) {
|
|
65
|
+
const parsed = parseOptionsSchema(subcommand.schema.options);
|
|
66
|
+
for (const opt of parsed) {
|
|
67
|
+
const flag = opt.name.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
68
|
+
const desc = opt.description || '';
|
|
69
|
+
if (opt.type === 'boolean') {
|
|
70
|
+
// Support negatable boolean options (--no-flag) when they have a default
|
|
71
|
+
if (opt.hasDefault) {
|
|
72
|
+
cmd.option(`--no-${flag}`, desc);
|
|
73
|
+
}
|
|
74
|
+
cmd.option(`--${flag}`, desc);
|
|
75
|
+
} else if (opt.type === 'number') {
|
|
76
|
+
cmd.option(`--${flag} <${opt.name}>`, desc, parseFloat);
|
|
77
|
+
} else {
|
|
78
|
+
cmd.option(`--${flag} <${opt.name}>`, desc);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cmd.action(async (...rawArgs: unknown[]) => {
|
|
84
|
+
const cmdObj = rawArgs[rawArgs.length - 1] as { opts: () => Record<string, unknown> };
|
|
85
|
+
const options = cmdObj.opts();
|
|
86
|
+
const args = rawArgs.slice(0, -1);
|
|
87
|
+
|
|
88
|
+
if (subcommand.requiresAuth) {
|
|
89
|
+
const auth = await requireAuth(baseCtx as CommandContext<false>);
|
|
90
|
+
|
|
91
|
+
if (subcommand.schema) {
|
|
92
|
+
try {
|
|
93
|
+
const input = buildValidationInput(subcommand.schema, args, options);
|
|
94
|
+
const ctx: Record<string, unknown> = {
|
|
95
|
+
...baseCtx,
|
|
96
|
+
auth,
|
|
97
|
+
};
|
|
98
|
+
if (subcommand.schema.args) {
|
|
99
|
+
ctx.args = subcommand.schema.args.parse(input.args);
|
|
100
|
+
}
|
|
101
|
+
if (subcommand.schema.options) {
|
|
102
|
+
ctx.opts = subcommand.schema.options.parse(input.options);
|
|
103
|
+
}
|
|
104
|
+
await subcommand.handler(ctx as CommandContext);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error && typeof error === 'object' && 'issues' in error) {
|
|
107
|
+
baseCtx.logger.error('Validation error:');
|
|
108
|
+
const issues = (error as { issues: Array<{ path: string[]; message: string }> })
|
|
109
|
+
.issues;
|
|
110
|
+
for (const issue of issues) {
|
|
111
|
+
baseCtx.logger.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
112
|
+
}
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const ctx: CommandContext<true> = {
|
|
119
|
+
...baseCtx,
|
|
120
|
+
auth,
|
|
121
|
+
};
|
|
122
|
+
await subcommand.handler(ctx);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (subcommand.schema) {
|
|
126
|
+
try {
|
|
127
|
+
const input = buildValidationInput(subcommand.schema, args, options);
|
|
128
|
+
const ctx: Record<string, unknown> = {
|
|
129
|
+
...baseCtx,
|
|
130
|
+
};
|
|
131
|
+
if (subcommand.schema.args) {
|
|
132
|
+
ctx.args = subcommand.schema.args.parse(input.args);
|
|
133
|
+
}
|
|
134
|
+
if (subcommand.schema.options) {
|
|
135
|
+
ctx.opts = subcommand.schema.options.parse(input.options);
|
|
136
|
+
}
|
|
137
|
+
await subcommand.handler(ctx as CommandContext);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error && typeof error === 'object' && 'issues' in error) {
|
|
140
|
+
baseCtx.logger.error('Validation error:');
|
|
141
|
+
const issues = (error as { issues: Array<{ path: string[]; message: string }> })
|
|
142
|
+
.issues;
|
|
143
|
+
for (const issue of issues) {
|
|
144
|
+
baseCtx.logger.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
145
|
+
}
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
await subcommand.handler(baseCtx as CommandContext<false>);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function registerCommands(
|
|
158
|
+
program: Command,
|
|
159
|
+
commands: CommandDefinition[],
|
|
160
|
+
baseCtx: CommandContext
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
for (const cmdDef of commands) {
|
|
163
|
+
if (cmdDef.subcommands) {
|
|
164
|
+
const cmd = program
|
|
165
|
+
.command(cmdDef.name, { hidden: cmdDef.hidden })
|
|
166
|
+
.description(cmdDef.description);
|
|
167
|
+
|
|
168
|
+
if (cmdDef.aliases) {
|
|
169
|
+
cmd.aliases(cmdDef.aliases);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (cmdDef.handler) {
|
|
173
|
+
cmd.action(async () => {
|
|
174
|
+
if (cmdDef.requiresAuth) {
|
|
175
|
+
const auth = await requireAuth(baseCtx as CommandContext<false>);
|
|
176
|
+
const ctx: CommandContext<true> = { ...baseCtx, auth };
|
|
177
|
+
await cmdDef.handler!(ctx);
|
|
178
|
+
} else {
|
|
179
|
+
await cmdDef.handler!(baseCtx as CommandContext<false>);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
cmd.action(() => cmd.help());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const sub of cmdDef.subcommands) {
|
|
187
|
+
await registerSubcommand(cmd, sub, baseCtx);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
await registerSubcommand(
|
|
191
|
+
program,
|
|
192
|
+
cmdDef as unknown as SubcommandDefinition,
|
|
193
|
+
baseCtx,
|
|
194
|
+
cmdDef.hidden
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Authentication Commands
|
|
2
|
+
|
|
3
|
+
The auth command provides authentication and authorization functionality for the Agentuity Platform.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### `auth login`
|
|
8
|
+
|
|
9
|
+
Login to the Agentuity Platform using a browser-based authentication flow.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agentuity auth login
|
|
13
|
+
# or
|
|
14
|
+
agentuity login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**How it works:**
|
|
18
|
+
|
|
19
|
+
1. Generates a one-time password (OTP) by calling `/cli/auth/start`
|
|
20
|
+
2. Displays the OTP and authentication URL to the user
|
|
21
|
+
3. Automatically opens the browser to the auth URL (on supported platforms)
|
|
22
|
+
4. Polls `/cli/auth/check` every 2 seconds for up to 60 seconds
|
|
23
|
+
5. Once authenticated, saves the API key, user ID, and expiration to config
|
|
24
|
+
|
|
25
|
+
**Stored data:**
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
auth:
|
|
29
|
+
api_key: 'your-api-key'
|
|
30
|
+
user_id: 'user-id'
|
|
31
|
+
expires: 1234567890
|
|
32
|
+
preferences:
|
|
33
|
+
orgId: ''
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `auth logout`
|
|
37
|
+
|
|
38
|
+
Logout of the Agentuity Cloud Platform by clearing authentication credentials.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
agentuity auth logout
|
|
42
|
+
# or
|
|
43
|
+
agentuity logout
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**What it does:**
|
|
47
|
+
|
|
48
|
+
- Clears `auth.api_key`, `auth.user_id`, and sets `auth.expires` to current time
|
|
49
|
+
- Clears `preferences.orgId`
|
|
50
|
+
- Writes changes to the active profile config file
|
|
51
|
+
|
|
52
|
+
## API Endpoints
|
|
53
|
+
|
|
54
|
+
The authentication flow uses the following API endpoints:
|
|
55
|
+
|
|
56
|
+
- `GET /cli/auth/start` - Generate OTP for login
|
|
57
|
+
- `POST /cli/auth/check` - Poll for login completion with OTP
|
|
58
|
+
|
|
59
|
+
## URL Configuration
|
|
60
|
+
|
|
61
|
+
The API and App URLs are determined in the following priority order:
|
|
62
|
+
|
|
63
|
+
1. **Environment Variables** (highest priority)
|
|
64
|
+
- `AGENTUITY_API_URL` - Override the API base URL
|
|
65
|
+
- `AGENTUITY_APP_URL` - Override the app base URL
|
|
66
|
+
|
|
67
|
+
2. **Config File Overrides**
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
overrides:
|
|
71
|
+
api_url: https://api.agentuity.io
|
|
72
|
+
app_url: https://app.agentuity.io
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. **Default Values** (lowest priority)
|
|
76
|
+
- API URL: `https://api.agentuity.com`
|
|
77
|
+
- App URL: `https://app.agentuity.com`
|
|
78
|
+
|
|
79
|
+
This allows different profiles (e.g., `local`, `production`) to point to different environments.
|
|
80
|
+
|
|
81
|
+
## Implementation Details
|
|
82
|
+
|
|
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
|
|
85
|
+
- **Config Management**: [../../config.ts](../../config.ts) provides `saveAuth()`, `clearAuth()`, and `getAuth()` helpers
|
|
86
|
+
- **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
|
|
88
|
+
- **Error Handling**: All errors are caught and displayed to user with appropriate exit codes
|
|
89
|
+
- `UpgradeRequiredError`: Shows upgrade instructions when CLI version is outdated
|
|
90
|
+
- Generic API errors: Display the error message from the server
|
|
91
|
+
- See [../../api-errors.md](../../api-errors.md) for details
|
|
92
|
+
|
|
93
|
+
## Architecture
|
|
94
|
+
|
|
95
|
+
Each command has its own `api.ts` file for command-specific API methods and types. The generic `APIClient` class and URL helpers are in the root `api.ts` file, promoting reusability while keeping command-specific logic isolated.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { APIClient } from '@/api';
|
|
2
|
+
import type { Config } from '@/types';
|
|
3
|
+
|
|
4
|
+
interface APIResponse<T> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
data?: T;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface OTPStartData {
|
|
11
|
+
otp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface OTPCompleteData {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
expires: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoginResult {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
expires: Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function generateLoginOTP(apiUrl: string, config?: Config | null): Promise<string> {
|
|
27
|
+
const client = new APIClient(apiUrl, undefined, config);
|
|
28
|
+
const resp = await client.request<APIResponse<OTPStartData>>('GET', '/cli/auth/start');
|
|
29
|
+
|
|
30
|
+
if (!resp.success) {
|
|
31
|
+
throw new Error(resp.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!resp.data) {
|
|
35
|
+
throw new Error('No OTP returned from server');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return resp.data.otp;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function pollForLoginCompletion(
|
|
42
|
+
apiUrl: string,
|
|
43
|
+
otp: string,
|
|
44
|
+
config?: Config | null,
|
|
45
|
+
timeoutMs = 60000
|
|
46
|
+
): Promise<LoginResult> {
|
|
47
|
+
const client = new APIClient(apiUrl, undefined, config);
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
|
|
50
|
+
while (Date.now() - started < timeoutMs) {
|
|
51
|
+
const resp = await client.request<APIResponse<OTPCompleteData>>('POST', '/cli/auth/check', {
|
|
52
|
+
otp,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!resp.success) {
|
|
56
|
+
throw new Error(resp.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (resp.data) {
|
|
60
|
+
return {
|
|
61
|
+
apiKey: resp.data.apiKey,
|
|
62
|
+
userId: resp.data.userId,
|
|
63
|
+
expires: new Date(resp.data.expires),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error('Login timed out');
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createCommand } from '@/types';
|
|
2
|
+
import { loginCommand } from './login';
|
|
3
|
+
import { logoutCommand } from './logout';
|
|
4
|
+
|
|
5
|
+
export const command = createCommand({
|
|
6
|
+
name: 'auth',
|
|
7
|
+
description: 'Authentication and authorization related commands',
|
|
8
|
+
subcommands: [loginCommand, logoutCommand],
|
|
9
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SubcommandDefinition } from '@/types';
|
|
2
|
+
import { getAPIBaseURL, getAppBaseURL, UpgradeRequiredError } from '@/api';
|
|
3
|
+
import { saveAuth } from '@/config';
|
|
4
|
+
import { generateLoginOTP, pollForLoginCompletion } from './api';
|
|
5
|
+
import * as tui from '@/tui';
|
|
6
|
+
|
|
7
|
+
export const loginCommand: SubcommandDefinition = {
|
|
8
|
+
name: 'login',
|
|
9
|
+
description: 'Login to the Agentuity Platform using a browser-based authentication flow',
|
|
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
|
+
console.log('Generating login OTP...');
|
|
19
|
+
|
|
20
|
+
const otp = await generateLoginOTP(apiUrl, config);
|
|
21
|
+
const authURL = `${appUrl}/auth/cli`;
|
|
22
|
+
|
|
23
|
+
const copied = await tui.copyToClipboard(otp);
|
|
24
|
+
|
|
25
|
+
tui.newline();
|
|
26
|
+
if (copied) {
|
|
27
|
+
console.log(`Code copied to clipboard: ${tui.bold(otp)}`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log('Copy the following code:');
|
|
30
|
+
tui.newline();
|
|
31
|
+
console.log(` ${tui.bold(otp)}`);
|
|
32
|
+
}
|
|
33
|
+
tui.newline();
|
|
34
|
+
console.log('Then open the URL in your browser and paste the code:');
|
|
35
|
+
tui.newline();
|
|
36
|
+
console.log(` ${tui.link(authURL)}`);
|
|
37
|
+
tui.newline();
|
|
38
|
+
console.log(tui.muted('This code will expire in 60 seconds'));
|
|
39
|
+
tui.newline();
|
|
40
|
+
|
|
41
|
+
if (process.platform === 'darwin') {
|
|
42
|
+
await tui.waitForAnyKey('Press Enter to open the URL...');
|
|
43
|
+
try {
|
|
44
|
+
Bun.spawn(['open', authURL], {
|
|
45
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore browser open errors
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('Waiting for login to complete...');
|
|
53
|
+
|
|
54
|
+
const result = await pollForLoginCompletion(apiUrl, otp, config);
|
|
55
|
+
|
|
56
|
+
await saveAuth({
|
|
57
|
+
apiKey: result.apiKey,
|
|
58
|
+
userId: result.userId,
|
|
59
|
+
expires: result.expires,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
tui.newline();
|
|
63
|
+
tui.success('Welcome to Agentuity! You are now logged in');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof UpgradeRequiredError) {
|
|
66
|
+
const bannerBody = `${error.message}\n\nVisit: ${tui.link('https://agentuity.dev/CLI/installation')}`;
|
|
67
|
+
tui.banner('CLI Upgrade Required', bannerBody);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
} else if (error instanceof Error) {
|
|
70
|
+
logger.fatal(`Login failed: ${error.message}`);
|
|
71
|
+
} else {
|
|
72
|
+
logger.fatal('Login failed');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SubcommandDefinition } from '@/types';
|
|
2
|
+
import { clearAuth } from '@/config';
|
|
3
|
+
import * as tui from '@/tui';
|
|
4
|
+
|
|
5
|
+
export const logoutCommand: SubcommandDefinition = {
|
|
6
|
+
name: 'logout',
|
|
7
|
+
description: 'Logout of the Agentuity Cloud Platform',
|
|
8
|
+
toplevel: true,
|
|
9
|
+
|
|
10
|
+
async handler() {
|
|
11
|
+
await clearAuth();
|
|
12
|
+
tui.success('You have been logged out');
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as acornLoose from 'acorn-loose';
|
|
2
|
+
import { basename, dirname, relative } from 'node:path';
|
|
3
|
+
import { generate } from 'astring';
|
|
4
|
+
|
|
5
|
+
interface ASTNode {
|
|
6
|
+
type: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ASTNodeIdentifier extends ASTNode {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ASTCallExpression extends ASTNode {
|
|
14
|
+
arguments: unknown[];
|
|
15
|
+
callee: {
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ASTPropertyNode {
|
|
21
|
+
type: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
key: ASTNodeIdentifier;
|
|
24
|
+
value: ASTNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ASTObjectExpression extends ASTNode {
|
|
28
|
+
properties: ASTPropertyNode[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ASTLiteral {
|
|
32
|
+
value: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseObjectExpressionToMap(expr: ASTObjectExpression): Map<string, string> {
|
|
36
|
+
const result = new Map<string, string>();
|
|
37
|
+
for (const prop of expr.properties) {
|
|
38
|
+
switch (prop.value.type) {
|
|
39
|
+
case 'Literal': {
|
|
40
|
+
const value = prop.value as unknown as ASTLiteral;
|
|
41
|
+
result.set(prop.key.name, value.value);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default: {
|
|
45
|
+
console.warn(
|
|
46
|
+
'AST value type %s of metadata key: %s not supported',
|
|
47
|
+
prop.value.type,
|
|
48
|
+
prop.key.name
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createObjectPropertyNode(key: string, value: string) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'Property',
|
|
59
|
+
kind: 'init',
|
|
60
|
+
key: {
|
|
61
|
+
type: 'Identifier',
|
|
62
|
+
name: key,
|
|
63
|
+
},
|
|
64
|
+
value: {
|
|
65
|
+
type: 'Literal',
|
|
66
|
+
value,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createNewMetadataNode() {
|
|
72
|
+
return {
|
|
73
|
+
type: 'Property',
|
|
74
|
+
kind: 'init',
|
|
75
|
+
key: {
|
|
76
|
+
type: 'Identifier',
|
|
77
|
+
name: 'metadata',
|
|
78
|
+
},
|
|
79
|
+
value: {
|
|
80
|
+
type: 'ObjectExpression',
|
|
81
|
+
properties: [] as ASTPropertyNode[],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const projectId = process.env.AGENTUITY_CLOUD_PROJECT_ID ?? '';
|
|
87
|
+
|
|
88
|
+
function hash(...val: string[]): string {
|
|
89
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
90
|
+
val.forEach((val) => hasher.update(val));
|
|
91
|
+
return hasher.digest().toHex();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getAgentId(identifier: string): string {
|
|
95
|
+
return hash(projectId, identifier);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type AcornParseResultType = ReturnType<typeof acornLoose.parse>;
|
|
99
|
+
|
|
100
|
+
function augmentAgentMetadataNode(
|
|
101
|
+
id: string,
|
|
102
|
+
name: string,
|
|
103
|
+
rel: string,
|
|
104
|
+
version: string,
|
|
105
|
+
ast: AcornParseResultType,
|
|
106
|
+
propvalue: ASTObjectExpression
|
|
107
|
+
): [string, Map<string, string>] {
|
|
108
|
+
const metadata = parseObjectExpressionToMap(propvalue);
|
|
109
|
+
if (!metadata.has('name')) {
|
|
110
|
+
metadata.set('name', name);
|
|
111
|
+
propvalue.properties.push(createObjectPropertyNode('name', name));
|
|
112
|
+
}
|
|
113
|
+
metadata.set('version', version);
|
|
114
|
+
metadata.set('identifier', name);
|
|
115
|
+
metadata.set('filename', rel);
|
|
116
|
+
metadata.set('id', id);
|
|
117
|
+
propvalue.properties.push(
|
|
118
|
+
createObjectPropertyNode('id', id),
|
|
119
|
+
createObjectPropertyNode('version', version),
|
|
120
|
+
createObjectPropertyNode('identifier', name),
|
|
121
|
+
createObjectPropertyNode('filename', rel)
|
|
122
|
+
);
|
|
123
|
+
const newsource = generate(ast);
|
|
124
|
+
return [newsource, metadata];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createAgentMetadataNode(
|
|
128
|
+
id: string,
|
|
129
|
+
name: string,
|
|
130
|
+
rel: string,
|
|
131
|
+
version: string,
|
|
132
|
+
ast: AcornParseResultType,
|
|
133
|
+
callargexp: ASTObjectExpression
|
|
134
|
+
): [string, Map<string, string>] {
|
|
135
|
+
const newmetadata = createNewMetadataNode();
|
|
136
|
+
const md = new Map<string, string>();
|
|
137
|
+
md.set('id', id);
|
|
138
|
+
md.set('version', version);
|
|
139
|
+
md.set('name', name);
|
|
140
|
+
md.set('identifier', name);
|
|
141
|
+
md.set('filename', rel);
|
|
142
|
+
for (const [key, value] of md) {
|
|
143
|
+
newmetadata.value.properties.push(createObjectPropertyNode(key, value));
|
|
144
|
+
}
|
|
145
|
+
callargexp.properties.push(newmetadata);
|
|
146
|
+
const newsource = generate(ast);
|
|
147
|
+
return [newsource, md];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseAgentMetadata(
|
|
151
|
+
rootDir: string,
|
|
152
|
+
filename: string,
|
|
153
|
+
contents: string
|
|
154
|
+
): [string, Map<string, string>] {
|
|
155
|
+
const ast = acornLoose.parse(contents, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
156
|
+
let exportName: string | undefined;
|
|
157
|
+
const rel = relative(rootDir, filename);
|
|
158
|
+
const name = basename(dirname(filename));
|
|
159
|
+
const id = getAgentId(name);
|
|
160
|
+
const version = hash(contents);
|
|
161
|
+
|
|
162
|
+
for (const body of ast.body) {
|
|
163
|
+
if (body.type === 'ExportDefaultDeclaration') {
|
|
164
|
+
if (body.declaration?.type === 'CallExpression') {
|
|
165
|
+
const call = body.declaration as ASTCallExpression;
|
|
166
|
+
if (call.callee.name === 'createAgent') {
|
|
167
|
+
for (const callarg of call.arguments) {
|
|
168
|
+
const callargexp = callarg as ASTObjectExpression;
|
|
169
|
+
for (const prop of callargexp.properties) {
|
|
170
|
+
if (prop.key.type === 'Identifier' && prop.key.name === 'metadata') {
|
|
171
|
+
return augmentAgentMetadataNode(
|
|
172
|
+
id,
|
|
173
|
+
name,
|
|
174
|
+
rel,
|
|
175
|
+
version,
|
|
176
|
+
ast,
|
|
177
|
+
prop.value as ASTObjectExpression
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return createAgentMetadataNode(id, name, rel, version, ast, callargexp);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const identifier = body.declaration as ASTNodeIdentifier;
|
|
186
|
+
exportName = identifier.name;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!exportName) {
|
|
191
|
+
throw new Error(`could not find default export for ${filename} using ${rootDir}`);
|
|
192
|
+
}
|
|
193
|
+
for (const body of ast.body) {
|
|
194
|
+
if (body.type === 'VariableDeclaration') {
|
|
195
|
+
for (const vardecl of body.declarations) {
|
|
196
|
+
if (vardecl.type === 'VariableDeclarator' && vardecl.id.type === 'Identifier') {
|
|
197
|
+
const identifier = vardecl.id as ASTNodeIdentifier;
|
|
198
|
+
if (identifier.name === exportName) {
|
|
199
|
+
if (vardecl.init?.type === 'CallExpression') {
|
|
200
|
+
const call = vardecl.init as ASTCallExpression;
|
|
201
|
+
if (call.callee.name === 'createAgent') {
|
|
202
|
+
for (const callarg of call.arguments) {
|
|
203
|
+
const callargexp = callarg as ASTObjectExpression;
|
|
204
|
+
for (const prop of callargexp.properties) {
|
|
205
|
+
if (prop.key.type === 'Identifier' && prop.key.name === 'metadata') {
|
|
206
|
+
return augmentAgentMetadataNode(
|
|
207
|
+
id,
|
|
208
|
+
name,
|
|
209
|
+
rel,
|
|
210
|
+
version,
|
|
211
|
+
ast,
|
|
212
|
+
prop.value as ASTObjectExpression
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return createAgentMetadataNode(id, name, rel, version, ast, callargexp);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw new Error(
|
|
226
|
+
`error parsing: ${filename}. could not find an proper createAgent defined in this file`
|
|
227
|
+
);
|
|
228
|
+
}
|