@agentuity/cli 0.0.35 → 0.0.42

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 (73) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +4 -4
  3. package/dist/api.d.ts +6 -22
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/auth.d.ts +0 -2
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/banner.d.ts.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cmd/auth/api.d.ts.map +1 -1
  10. package/dist/cmd/auth/login.d.ts +1 -2
  11. package/dist/cmd/auth/login.d.ts.map +1 -1
  12. package/dist/cmd/auth/logout.d.ts +1 -2
  13. package/dist/cmd/auth/logout.d.ts.map +1 -1
  14. package/dist/cmd/auth/signup.d.ts +1 -2
  15. package/dist/cmd/auth/signup.d.ts.map +1 -1
  16. package/dist/cmd/bundle/ast.d.ts +2 -0
  17. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  18. package/dist/cmd/bundle/bundler.d.ts +1 -0
  19. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  20. package/dist/cmd/bundle/patch/index.d.ts.map +1 -1
  21. package/dist/cmd/bundle/patch/llm.d.ts +3 -0
  22. package/dist/cmd/bundle/patch/llm.d.ts.map +1 -0
  23. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  24. package/dist/cmd/dev/index.d.ts.map +1 -1
  25. package/dist/cmd/index.d.ts.map +1 -1
  26. package/dist/cmd/project/create.d.ts.map +1 -1
  27. package/dist/cmd/project/delete.d.ts.map +1 -1
  28. package/dist/cmd/project/download.d.ts.map +1 -1
  29. package/dist/cmd/project/list.d.ts.map +1 -1
  30. package/dist/cmd/project/show.d.ts.map +1 -1
  31. package/dist/cmd/project/template-flow.d.ts +3 -0
  32. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  33. package/dist/config.d.ts +11 -2
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/logger.d.ts +1 -1
  38. package/dist/logger.d.ts.map +1 -1
  39. package/dist/sound.d.ts.map +1 -1
  40. package/dist/tui.d.ts +16 -7
  41. package/dist/tui.d.ts.map +1 -1
  42. package/dist/types.d.ts +70 -7
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +3 -2
  45. package/src/api.ts +27 -138
  46. package/src/auth.ts +87 -71
  47. package/src/banner.ts +7 -2
  48. package/src/cli.ts +7 -16
  49. package/src/cmd/auth/api.ts +40 -29
  50. package/src/cmd/auth/login.ts +7 -20
  51. package/src/cmd/auth/logout.ts +3 -3
  52. package/src/cmd/auth/signup.ts +6 -6
  53. package/src/cmd/bundle/ast.ts +169 -4
  54. package/src/cmd/bundle/bundler.ts +1 -0
  55. package/src/cmd/bundle/patch/index.ts +4 -0
  56. package/src/cmd/bundle/patch/llm.ts +36 -0
  57. package/src/cmd/bundle/plugin.ts +42 -1
  58. package/src/cmd/dev/index.ts +100 -1
  59. package/src/cmd/example/optional-auth.ts +1 -1
  60. package/src/cmd/index.ts +1 -0
  61. package/src/cmd/profile/README.md +1 -1
  62. package/src/cmd/project/create.ts +10 -2
  63. package/src/cmd/project/delete.ts +43 -2
  64. package/src/cmd/project/download.ts +17 -0
  65. package/src/cmd/project/list.ts +33 -2
  66. package/src/cmd/project/show.ts +35 -3
  67. package/src/cmd/project/template-flow.ts +60 -5
  68. package/src/config.ts +77 -5
  69. package/src/index.ts +2 -2
  70. package/src/logger.ts +1 -1
  71. package/src/sound.ts +9 -3
  72. package/src/tui.ts +234 -104
  73. package/src/types.ts +97 -34
@@ -1,6 +1,7 @@
1
1
  import * as acornLoose from 'acorn-loose';
2
2
  import { basename, dirname, relative } from 'node:path';
3
3
  import { generate } from 'astring';
4
+ import { BuildMetadata } from '../../types';
4
5
 
5
6
  interface ASTNode {
6
7
  type: string;
@@ -12,9 +13,7 @@ interface ASTNodeIdentifier extends ASTNode {
12
13
 
13
14
  interface ASTCallExpression extends ASTNode {
14
15
  arguments: unknown[];
15
- callee: {
16
- name: string;
17
- };
16
+ callee: ASTMemberExpression;
18
17
  }
19
18
 
20
19
  interface ASTPropertyNode {
@@ -31,10 +30,22 @@ interface ASTObjectExpression extends ASTNode {
31
30
  properties: ASTPropertyNode[];
32
31
  }
33
32
 
34
- interface ASTLiteral {
33
+ interface ASTLiteral extends ASTNode {
35
34
  value: string;
36
35
  }
37
36
 
37
+ interface ASTMemberExpression extends ASTNode {
38
+ object: ASTNode;
39
+ property: ASTNode;
40
+ computed: boolean;
41
+ optional: boolean;
42
+ name?: string;
43
+ }
44
+
45
+ interface ASTExpressionStatement extends ASTNode {
46
+ expression: ASTCallExpression;
47
+ }
48
+
38
49
  function parseObjectExpressionToMap(expr: ASTObjectExpression): Map<string, string> {
39
50
  const result = new Map<string, string>();
40
51
  for (const prop of expr.properties) {
@@ -98,6 +109,10 @@ function getAgentId(identifier: string): string {
98
109
  return hash(projectId, identifier);
99
110
  }
100
111
 
112
+ function generateRouteId(method: string, path: string): string {
113
+ return hash(projectId, method, path);
114
+ }
115
+
101
116
  type AcornParseResultType = ReturnType<typeof acornLoose.parse>;
102
117
 
103
118
  function augmentAgentMetadataNode(
@@ -229,3 +244,153 @@ export function parseAgentMetadata(
229
244
  `error parsing: ${filename}. could not find an proper createAgent defined in this file`
230
245
  );
231
246
  }
247
+
248
+ type RouteDefinition = BuildMetadata['routes'];
249
+
250
+ export async function parseRoute(
251
+ rootDir: string,
252
+ filename: string
253
+ ): Promise<BuildMetadata['routes']> {
254
+ const contents = await Bun.file(filename).text();
255
+ const version = hash(contents);
256
+ const ast = acornLoose.parse(contents, { ecmaVersion: 'latest', sourceType: 'module' });
257
+ let exportName: string | undefined;
258
+ let variableName: string | undefined;
259
+ for (const body of ast.body) {
260
+ if (body.type === 'ExportDefaultDeclaration') {
261
+ const identifier = body.declaration as ASTNodeIdentifier;
262
+ exportName = identifier.name;
263
+ break;
264
+ }
265
+ }
266
+ if (!exportName) {
267
+ throw new Error(`could not find default export for ${filename} using ${rootDir}`);
268
+ }
269
+ for (const body of ast.body) {
270
+ if (body.type === 'VariableDeclaration') {
271
+ for (const vardecl of body.declarations) {
272
+ if (vardecl.type === 'VariableDeclarator' && vardecl.id.type === 'Identifier') {
273
+ const identifier = vardecl.id as ASTNodeIdentifier;
274
+ if (identifier.name === exportName) {
275
+ if (vardecl.init?.type === 'CallExpression') {
276
+ const call = vardecl.init as ASTCallExpression;
277
+ if (call.callee.name === 'createRouter') {
278
+ variableName = identifier.name;
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+ if (!variableName) {
288
+ throw new Error(
289
+ `error parsing: ${filename}. could not find an proper createRouter defined in this file`
290
+ );
291
+ }
292
+
293
+ const rel = relative(rootDir, filename);
294
+ const name = basename(dirname(filename));
295
+ const routes: RouteDefinition = [];
296
+ const routePrefix = filename.includes('src/agents') ? '/agent' : '/api';
297
+
298
+ for (const body of ast.body) {
299
+ if (body.type === 'ExpressionStatement') {
300
+ const statement = body as ASTExpressionStatement;
301
+ const callee = statement.expression.callee;
302
+ if (callee.object.type === 'Identifier') {
303
+ const identifier = callee.object as ASTNodeIdentifier;
304
+ if (identifier.name === variableName) {
305
+ let method = (callee.property as ASTNodeIdentifier).name;
306
+ let type = 'api';
307
+ const action = statement.expression.arguments[0];
308
+ let suffix = '';
309
+ let config: Record<string, unknown> | undefined;
310
+ switch (method) {
311
+ case 'get':
312
+ case 'put':
313
+ case 'post':
314
+ case 'patch':
315
+ case 'delete': {
316
+ const theaction = action as ASTLiteral;
317
+ if (theaction.type === 'Literal') {
318
+ suffix = theaction.value;
319
+ break;
320
+ }
321
+ break;
322
+ }
323
+ case 'stream':
324
+ case 'sse':
325
+ case 'websocket': {
326
+ type = method;
327
+ method = 'post';
328
+ const theaction = action as ASTLiteral;
329
+ if (theaction.type === 'Literal') {
330
+ suffix = theaction.value;
331
+ break;
332
+ }
333
+ break;
334
+ }
335
+ case 'sms': {
336
+ type = method;
337
+ method = 'post';
338
+ const theaction = action as ASTObjectExpression;
339
+ if (theaction.type === 'ObjectExpression') {
340
+ config = {};
341
+ theaction.properties.forEach((p) => {
342
+ if (p.value.type === 'Literal') {
343
+ const literal = p.value as ASTLiteral;
344
+ config![p.key.name] = literal.value;
345
+ }
346
+ });
347
+ const number = theaction.properties.find((p) => p.key.name === 'number');
348
+ if (number && number.value.type === 'Literal') {
349
+ const phoneNumber = number.value as ASTLiteral;
350
+ suffix = hash(phoneNumber.value);
351
+ break;
352
+ }
353
+ }
354
+ break;
355
+ }
356
+ case 'email': {
357
+ type = method;
358
+ method = 'post';
359
+ const theaction = action as ASTLiteral;
360
+ if (theaction.type === 'Literal') {
361
+ const email = theaction.value;
362
+ suffix = hash(email);
363
+ break;
364
+ }
365
+ break;
366
+ }
367
+ case 'cron': {
368
+ type = method;
369
+ method = 'post';
370
+ const theaction = action as ASTLiteral;
371
+ if (theaction.type === 'Literal') {
372
+ const number = theaction.value;
373
+ suffix = hash(number);
374
+ break;
375
+ }
376
+ break;
377
+ }
378
+ }
379
+ const thepath = `${routePrefix}/${name}/${suffix}`
380
+ .replaceAll(/\/{2,}/g, '/')
381
+ .replaceAll(/\/$/g, '');
382
+ routes.push({
383
+ id: generateRouteId(method, thepath),
384
+ method: method as 'get' | 'post' | 'put' | 'delete' | 'patch',
385
+ type: type as 'api' | 'sms' | 'email' | 'cron',
386
+ filename: rel,
387
+ path: thepath,
388
+ version,
389
+ config,
390
+ });
391
+ }
392
+ }
393
+ }
394
+ }
395
+ return routes;
396
+ }
@@ -7,6 +7,7 @@ import { getVersion } from '../../version';
7
7
  export interface BundleOptions {
8
8
  rootDir: string;
9
9
  dev?: boolean;
10
+ env?: Map<string, string>;
10
11
  }
11
12
 
12
13
  export async function bundle({ dev = false, rootDir }: BundleOptions) {
@@ -1,4 +1,5 @@
1
1
  import { generatePatches as aisdkGeneratePatches } from './aisdk';
2
+ import { generatePatches as llmGeneratePatches } from './llm';
2
3
  import { type PatchModule, searchBackwards } from './_util';
3
4
 
4
5
  export function generatePatches(): Map<string, PatchModule> {
@@ -6,6 +7,9 @@ export function generatePatches(): Map<string, PatchModule> {
6
7
  for (const [name, patch] of aisdkGeneratePatches()) {
7
8
  patches.set(name, patch);
8
9
  }
10
+ for (const [name, patch] of llmGeneratePatches()) {
11
+ patches.set(name, patch);
12
+ }
9
13
  return patches;
10
14
  }
11
15
 
@@ -0,0 +1,36 @@
1
+ import { type PatchModule, generateEnvGuard, generateGatewayEnvGuard } from './_util';
2
+
3
+ function registerLLMPatch(
4
+ patches: Map<string, PatchModule>,
5
+ module: string,
6
+ filename: string,
7
+ key: string,
8
+ baseurl: string,
9
+ name: string
10
+ ) {
11
+ patches.set(module, {
12
+ module,
13
+ filename,
14
+ body: {
15
+ before: generateEnvGuard(
16
+ key,
17
+ generateGatewayEnvGuard(key, 'process.env.AGENTUITY_SDK_KEY', baseurl, name)
18
+ ),
19
+ },
20
+ });
21
+ }
22
+
23
+ export function generatePatches(): Map<string, PatchModule> {
24
+ const patches = new Map<string, PatchModule>();
25
+ registerLLMPatch(
26
+ patches,
27
+ '@anthropic-ai',
28
+ 'index',
29
+ 'ANTHROPIC_API_KEY',
30
+ 'ANTHROPIC_BASE_URL',
31
+ 'anthropic'
32
+ );
33
+ registerLLMPatch(patches, 'groq-sdk', 'index', 'GROQ_API_KEY', 'GROQ_BASE_URL', 'groq');
34
+ registerLLMPatch(patches, 'openai', 'index', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'openai');
35
+ return patches;
36
+ }
@@ -1,7 +1,8 @@
1
1
  import type { BunPlugin } from 'bun';
2
2
  import { dirname, basename, join } from 'node:path';
3
3
  import { existsSync, writeFileSync } from 'node:fs';
4
- import { parseAgentMetadata } from './ast';
4
+ import type { BuildMetadata } from '../../types';
5
+ import { parseAgentMetadata, parseRoute } from './ast';
5
6
  import { applyPatch, generatePatches } from './patch';
6
7
 
7
8
  function toCamelCase(str: string): string {
@@ -115,6 +116,7 @@ const AgentuityBundler: BunPlugin = {
115
116
  Map<string, string>
116
117
  >();
117
118
  const transpiler = new Bun.Transpiler({ loader: 'ts' });
119
+ let routeDefinitions: BuildMetadata['routes'] = [];
118
120
 
119
121
  build.onResolve({ filter: /\/route\.ts$/, namespace: 'file' }, async (args) => {
120
122
  if (args.path.startsWith(srcDir)) {
@@ -194,6 +196,9 @@ const AgentuityBundler: BunPlugin = {
194
196
  .replace('/agents', '/agent')
195
197
  .replace('./', '/');
196
198
 
199
+ const definitions = await parseRoute(rootDir, join(srcDir, route + '.ts'));
200
+ routeDefinitions = [...routeDefinitions, ...definitions];
201
+
197
202
  let agentDetail: Record<string, string> = {};
198
203
 
199
204
  if (hasAgent) {
@@ -264,6 +269,42 @@ const AgentuityBundler: BunPlugin = {
264
269
  contents += `\n${inserts.join('\n')}`;
265
270
  }
266
271
 
272
+ // generate the build metadata
273
+ const metadata: BuildMetadata = {
274
+ routes: routeDefinitions,
275
+ agents: [],
276
+ };
277
+ for (const [, v] of agentMetadata) {
278
+ if (!v.has('filename')) {
279
+ throw new Error('agent metadata is missing expected filename property');
280
+ }
281
+ if (!v.has('id')) {
282
+ throw new Error('agent metadata is missing expected id property');
283
+ }
284
+ if (!v.has('identifier')) {
285
+ throw new Error('agent metadata is missing expected identifier property');
286
+ }
287
+ if (!v.has('version')) {
288
+ throw new Error('agent metadata is missing expected version property');
289
+ }
290
+ if (!v.has('name')) {
291
+ throw new Error('agent metadata is missing expected name property');
292
+ }
293
+ metadata.agents.push({
294
+ filename: v.get('filename')!,
295
+ id: v.get('id')!,
296
+ identifier: v.get('identifier')!,
297
+ version: v.get('version')!,
298
+ name: v.get('name')!,
299
+ description: v.get('description') ?? '<no description provided>',
300
+ });
301
+ }
302
+
303
+ const metadataFilename = Bun.file(
304
+ join(build.config.outdir!, 'agentuity.metadata.json')
305
+ );
306
+ await metadataFilename.write(JSON.stringify(metadata));
307
+
267
308
  return {
268
309
  contents,
269
310
  loader: 'ts',
@@ -3,6 +3,8 @@ import { z } from 'zod';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { bundle } from '../bundle/bundler';
5
5
  import { existsSync, FSWatcher, watch } from 'node:fs';
6
+ import { loadBuildMetadata } from '../../config';
7
+ import type { BuildMetadata } from '../../types';
6
8
  import * as tui from '../../tui';
7
9
 
8
10
  export const command = createCommand({
@@ -38,6 +40,32 @@ export const command = createCommand({
38
40
  process.exit(1);
39
41
  }
40
42
 
43
+ const devmodebody =
44
+ tui.muted('Local: ') +
45
+ tui.link('http://127.0.0.1:3000') +
46
+ '\n\n' +
47
+ tui.muted('Press ') +
48
+ tui.bold('h') +
49
+ tui.muted(' for keyboard shortcuts');
50
+
51
+ function showBanner() {
52
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
53
+ padding: 2,
54
+ topSpacer: false,
55
+ bottomSpacer: false,
56
+ centerTitle: false,
57
+ });
58
+ }
59
+
60
+ showBanner();
61
+
62
+ const env = { ...process.env };
63
+ env.AGENTUITY_SDK_DEV_MODE = 'true';
64
+ env.AGENTUITY_ENV = 'development';
65
+ env.NODE_ENV = 'development';
66
+ env.PORT = '3000';
67
+ env.AGENTUITY_PORT = env.PORT;
68
+
41
69
  const agentuityDir = resolve(rootDir, '.agentuity');
42
70
  const appPath = resolve(agentuityDir, 'app.js');
43
71
 
@@ -54,6 +82,7 @@ export const command = createCommand({
54
82
  let shuttingDownForRestart = false;
55
83
  let pendingRestart = false;
56
84
  let building = false;
85
+ let metadata: BuildMetadata | undefined;
57
86
 
58
87
  // Track restart timestamps to detect restart loops
59
88
  const restartTimestamps: number[] = [];
@@ -200,6 +229,10 @@ export const command = createCommand({
200
229
  return;
201
230
  }
202
231
 
232
+ metadata = await loadBuildMetadata(agentuityDir);
233
+
234
+ env.AGENTUITY_LOG_LEVEL = logger.level;
235
+
203
236
  logger.trace('Starting dev server: %s', appPath);
204
237
  // Use shell to run in a process group for proper cleanup
205
238
  // The 'exec' ensures the shell is replaced by the actual process
@@ -207,7 +240,8 @@ export const command = createCommand({
207
240
  cwd: rootDir,
208
241
  stdout: 'inherit',
209
242
  stderr: 'inherit',
210
- stdin: 'inherit',
243
+ stdin: process.stdin.isTTY ? 'ignore' : 'inherit', // Don't inherit stdin, we handle it ourselves
244
+ env,
211
245
  });
212
246
 
213
247
  running = true;
@@ -286,6 +320,71 @@ export const command = createCommand({
286
320
  await restart();
287
321
  logger.trace('Initial restart completed, setting up watchers');
288
322
 
323
+ // Setup keyboard shortcuts (only if we have a TTY)
324
+ if (process.stdin.isTTY) {
325
+ logger.trace('Setting up keyboard shortcuts');
326
+ process.stdin.setRawMode(true);
327
+ process.stdin.resume();
328
+ process.stdin.setEncoding('utf8');
329
+
330
+ const showHelp = () => {
331
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
332
+ console.log(tui.muted(' h') + ' - show this help');
333
+ console.log(tui.muted(' c') + ' - clear console');
334
+ console.log(tui.muted(' r') + ' - restart server');
335
+ console.log(tui.muted(' o') + ' - show routes');
336
+ console.log(tui.muted(' a') + ' - show agents');
337
+ console.log(tui.muted(' q') + ' - quit\n');
338
+ };
339
+
340
+ const showRoutes = () => {
341
+ tui.info('API Route Detail');
342
+ console.table(metadata?.routes, ['method', 'path', 'filename']);
343
+ };
344
+
345
+ const showAgents = () => {
346
+ tui.info('Agent Detail');
347
+ console.table(metadata?.agents, ['name', 'filename', 'description']);
348
+ };
349
+
350
+ process.stdin.on('data', (data) => {
351
+ const key = data.toString();
352
+
353
+ // Handle Ctrl+C
354
+ if (key === '\u0003') {
355
+ cleanup();
356
+ return;
357
+ }
358
+
359
+ // Handle other shortcuts
360
+ switch (key) {
361
+ case 'h':
362
+ showHelp();
363
+ break;
364
+ case 'c':
365
+ console.clear();
366
+ showBanner();
367
+ break;
368
+ case 'r':
369
+ tui.info('Manually restarting server...');
370
+ restart();
371
+ break;
372
+ case 'o':
373
+ showRoutes();
374
+ break;
375
+ case 'a':
376
+ showAgents();
377
+ break;
378
+ case 'q':
379
+ tui.info('Shutting down...');
380
+ cleanup();
381
+ break;
382
+ }
383
+ });
384
+
385
+ logger.trace('✓ Keyboard shortcuts enabled');
386
+ }
387
+
289
388
  // Patterns to ignore (generated files that change during build)
290
389
  const ignorePatterns = [
291
390
  /\.generated\.(js|ts|d\.ts)$/,
@@ -10,7 +10,7 @@ export const optionalAuthSubcommand: SubcommandDefinition = {
10
10
 
11
11
  // Type guard to check if auth is present
12
12
  const ctxWithAuth = ctx as CommandContext<true>;
13
- if ('auth' in ctx && ctxWithAuth.auth) {
13
+ if (ctxWithAuth.auth) {
14
14
  const auth = ctxWithAuth.auth as AuthData;
15
15
  // User chose to authenticate
16
16
  tui.success('You are authenticated!');
package/src/cmd/index.ts CHANGED
@@ -28,6 +28,7 @@ export async function discoverCommands(): Promise<CommandDefinition[]> {
28
28
  aliases: subcommand.aliases,
29
29
  hidden: true,
30
30
  requiresAuth: subcommand.requiresAuth,
31
+ optionalAuth: subcommand.optionalAuth,
31
32
  schema: subcommand.schema,
32
33
  handler: subcommand.handler,
33
34
  };
@@ -77,4 +77,4 @@ The `name` field is extracted using the regex: `/\bname:\s+["']?([\w-_]+)["']?/`
77
77
  - Profile files must have `.yaml` extension
78
78
  - Profile names must match: `^[\w-_]{3,}$` (3+ chars, alphanumeric, dashes, underscores)
79
79
  - The config loader (`loadConfig()`) automatically uses the active profile
80
- - If no profile is selected or the file doesn't exist, falls back to `config.yaml`
80
+ - If no profile is selected or the file doesn't exist, falls back to `production.yaml`
@@ -7,7 +7,7 @@ export const createProjectSubcommand = createSubcommand({
7
7
  description: 'Create a new project',
8
8
  aliases: ['new'],
9
9
  toplevel: true,
10
- requiresAuth: false,
10
+ optionalAuth: true,
11
11
  schema: {
12
12
  options: z.object({
13
13
  name: z.string().optional().describe('Project name'),
@@ -32,11 +32,17 @@ export const createProjectSubcommand = createSubcommand({
32
32
  .default(true)
33
33
  .describe('Run bun run build after installing (use --no-build to skip)'),
34
34
  confirm: z.boolean().optional().describe('Skip confirmation prompts'),
35
+ register: z
36
+ .boolean()
37
+ .default(true)
38
+ .optional()
39
+ .describe('Register the project, if authenticated (use --no-register to skip)'),
35
40
  }),
36
41
  },
37
42
 
38
43
  async handler(ctx) {
39
- const { logger, opts } = ctx;
44
+ const { logger, opts, auth, config } = ctx;
45
+
40
46
  await runCreateFlow({
41
47
  projectName: opts.name,
42
48
  dir: opts.dir,
@@ -47,6 +53,8 @@ export const createProjectSubcommand = createSubcommand({
47
53
  noBuild: opts.build === false,
48
54
  skipPrompts: opts.confirm === true,
49
55
  logger,
56
+ auth: opts.register === true ? auth : undefined,
57
+ config: config!,
50
58
  });
51
59
  },
52
60
  });
@@ -1,13 +1,54 @@
1
+ import { z } from 'zod';
1
2
  import { createSubcommand } from '../../types';
3
+ import * as tui from '../../tui';
4
+ import { projectDelete } from '@agentuity/server';
5
+ import { getAPIBaseURL, APIClient } from '../../api';
2
6
 
3
7
  export const deleteSubcommand = createSubcommand({
4
8
  name: 'delete',
5
9
  description: 'Delete a project',
6
10
  aliases: ['rm', 'del'],
7
11
  requiresAuth: true,
12
+ schema: {
13
+ args: z.object({
14
+ id: z.string().describe('the project id'),
15
+ }),
16
+ options: z.object({
17
+ confirm: z.boolean().optional().describe('Skip confirmation prompts'),
18
+ }),
19
+ },
8
20
 
9
21
  async handler(ctx) {
10
- const { logger } = ctx;
11
- logger.info('TODO: Implement project delete functionality');
22
+ const { args, opts, config } = ctx;
23
+
24
+ const apiUrl = getAPIBaseURL(config);
25
+ const client = new APIClient(apiUrl, config);
26
+
27
+ const skipConfirm = opts?.confirm === true;
28
+
29
+ if (!process.stdout.isTTY && !skipConfirm) {
30
+ tui.fatal('no TTY and --confirm is false');
31
+ }
32
+
33
+ if (!skipConfirm) {
34
+ const ok = await tui.confirm('Are you sure you want to delete', false);
35
+ if (!ok) {
36
+ return;
37
+ }
38
+ }
39
+
40
+ const deleted = await tui.spinner('Deleting project', async () => {
41
+ const val = await projectDelete(client!, args.id);
42
+ if (val.length === 1 && val[0] === args.id) {
43
+ return true;
44
+ }
45
+ return false;
46
+ });
47
+
48
+ if (deleted) {
49
+ tui.success(`Project ${args.id} deleted`);
50
+ } else {
51
+ tui.warning(`${args.id} not found`);
52
+ }
12
53
  },
13
54
  });
@@ -225,6 +225,23 @@ export async function setupProject(options: SetupOptions): Promise<void> {
225
225
  clearOnSuccess: true,
226
226
  });
227
227
 
228
+ // Configure git user in CI environments (where git config may not be set)
229
+ if (process.env.CI) {
230
+ await tui.runCommand({
231
+ command: 'git config user.email',
232
+ cwd: dest,
233
+ cmd: ['git', 'config', 'user.email', 'agentuity@example.com'],
234
+ clearOnSuccess: true,
235
+ });
236
+
237
+ await tui.runCommand({
238
+ command: 'git config user.name',
239
+ cwd: dest,
240
+ cmd: ['git', 'config', 'user.name', 'Agentuity'],
241
+ clearOnSuccess: true,
242
+ });
243
+ }
244
+
228
245
  // Add all files
229
246
  await tui.runCommand({
230
247
  command: 'git add .',
@@ -1,13 +1,44 @@
1
+ import { z } from 'zod';
1
2
  import { createSubcommand } from '../../types';
3
+ import * as tui from '../../tui';
4
+ import { projectList } from '@agentuity/server';
5
+ import { getAPIBaseURL, APIClient } from '../../api';
2
6
 
3
7
  export const listSubcommand = createSubcommand({
4
8
  name: 'list',
5
9
  description: 'List all projects',
6
10
  aliases: ['ls'],
7
11
  requiresAuth: true,
12
+ schema: {
13
+ options: z.object({
14
+ format: z
15
+ .enum(['json', 'table'])
16
+ .optional()
17
+ .describe('the output format: json, table (default)'),
18
+ }),
19
+ },
8
20
 
9
21
  async handler(ctx) {
10
- const { logger } = ctx;
11
- logger.info('TODO: Implement project list functionality');
22
+ const { config, opts } = ctx;
23
+
24
+ const apiUrl = getAPIBaseURL(config);
25
+ const client = new APIClient(apiUrl, config);
26
+
27
+ const projects = await tui.spinner('Fetching projects', () => {
28
+ return projectList(client!);
29
+ });
30
+
31
+ // TODO: might want to sort by the last org_id we used
32
+ if (projects) {
33
+ projects.sort((a, b) => {
34
+ return a.name.localeCompare(b.name);
35
+ });
36
+ }
37
+
38
+ if (opts?.format === 'json') {
39
+ console.log(JSON.stringify(projects, null, 2));
40
+ } else {
41
+ console.table(projects, ['id', 'name', 'orgName']);
42
+ }
12
43
  },
13
44
  });