@agentuity/cli 0.0.35 → 0.0.41

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 (57) 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/cli.d.ts.map +1 -1
  8. package/dist/cmd/auth/api.d.ts.map +1 -1
  9. package/dist/cmd/auth/login.d.ts +1 -2
  10. package/dist/cmd/auth/login.d.ts.map +1 -1
  11. package/dist/cmd/auth/logout.d.ts +1 -2
  12. package/dist/cmd/auth/logout.d.ts.map +1 -1
  13. package/dist/cmd/auth/signup.d.ts +1 -2
  14. package/dist/cmd/auth/signup.d.ts.map +1 -1
  15. package/dist/cmd/bundle/bundler.d.ts +1 -0
  16. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  17. package/dist/cmd/bundle/patch/index.d.ts.map +1 -1
  18. package/dist/cmd/bundle/patch/llm.d.ts +3 -0
  19. package/dist/cmd/bundle/patch/llm.d.ts.map +1 -0
  20. package/dist/cmd/dev/index.d.ts.map +1 -1
  21. package/dist/cmd/index.d.ts.map +1 -1
  22. package/dist/cmd/project/create.d.ts.map +1 -1
  23. package/dist/cmd/project/download.d.ts.map +1 -1
  24. package/dist/cmd/project/template-flow.d.ts +3 -0
  25. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  26. package/dist/config.d.ts +10 -2
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/sound.d.ts.map +1 -1
  31. package/dist/tui.d.ts +16 -7
  32. package/dist/tui.d.ts.map +1 -1
  33. package/dist/types.d.ts +36 -7
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +3 -2
  36. package/src/api.ts +27 -138
  37. package/src/auth.ts +87 -71
  38. package/src/cli.ts +6 -15
  39. package/src/cmd/auth/api.ts +40 -29
  40. package/src/cmd/auth/login.ts +5 -16
  41. package/src/cmd/auth/logout.ts +3 -3
  42. package/src/cmd/auth/signup.ts +6 -6
  43. package/src/cmd/bundle/bundler.ts +1 -0
  44. package/src/cmd/bundle/patch/index.ts +4 -0
  45. package/src/cmd/bundle/patch/llm.ts +36 -0
  46. package/src/cmd/dev/index.ts +17 -0
  47. package/src/cmd/example/optional-auth.ts +1 -1
  48. package/src/cmd/index.ts +1 -0
  49. package/src/cmd/profile/README.md +1 -1
  50. package/src/cmd/project/create.ts +11 -3
  51. package/src/cmd/project/download.ts +17 -0
  52. package/src/cmd/project/template-flow.ts +55 -1
  53. package/src/config.ts +54 -5
  54. package/src/index.ts +2 -2
  55. package/src/sound.ts +9 -3
  56. package/src/tui.ts +163 -60
  57. package/src/types.ts +68 -34
@@ -38,6 +38,22 @@ export const command = createCommand({
38
38
  process.exit(1);
39
39
  }
40
40
 
41
+ const devmodebody = tui.muted('Local: ') + tui.link('http://127.0.0.1:3000');
42
+
43
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
44
+ padding: 2,
45
+ topSpacer: false,
46
+ bottomSpacer: false,
47
+ centerTitle: false,
48
+ });
49
+
50
+ const env = { ...process.env };
51
+ env.AGENTUITY_SDK_DEV_MODE = 'true';
52
+ env.AGENTUITY_ENV = 'development';
53
+ env.NODE_ENV = 'development';
54
+ env.PORT = '3000';
55
+ env.AGENTUITY_PORT = env.PORT;
56
+
41
57
  const agentuityDir = resolve(rootDir, '.agentuity');
42
58
  const appPath = resolve(agentuityDir, 'app.js');
43
59
 
@@ -208,6 +224,7 @@ export const command = createCommand({
208
224
  stdout: 'inherit',
209
225
  stderr: 'inherit',
210
226
  stdin: 'inherit',
227
+ env,
211
228
  });
212
229
 
213
230
  running = true;
@@ -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'),
@@ -31,12 +31,18 @@ export const createProjectSubcommand = createSubcommand({
31
31
  .optional()
32
32
  .default(true)
33
33
  .describe('Run bun run build after installing (use --no-build to skip)'),
34
- confirm: z.boolean().optional().describe('Skip confirmation prompts'),
34
+ confirm: z.boolean().optional().describe('Show 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
  });
@@ -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 .',
@@ -3,12 +3,21 @@ import { existsSync, readdirSync, rmSync, statSync } from 'node:fs';
3
3
  import { cwd } from 'node:process';
4
4
  import { homedir } from 'node:os';
5
5
  import enquirer from 'enquirer';
6
+ import {
7
+ createProject,
8
+ projectExists,
9
+ listOrganizations,
10
+ type OrganizationList,
11
+ } from '@agentuity/server';
6
12
  import type { Logger } from '../../logger';
7
13
  import * as tui from '../../tui';
8
14
  import { playSound } from '../../sound';
9
15
  import { fetchTemplates, type TemplateInfo } from './templates';
10
16
  import { downloadTemplate, setupProject } from './download';
11
17
  import { showBanner } from '../../banner';
18
+ import type { AuthData, Config } from '../../types';
19
+ import { getAPIBaseURL, APIClient } from '../../api';
20
+ import { createProjectConfig } from '../../config';
12
21
 
13
22
  interface CreateFlowOptions {
14
23
  projectName?: string;
@@ -20,6 +29,8 @@ interface CreateFlowOptions {
20
29
  noBuild: boolean;
21
30
  skipPrompts: boolean;
22
31
  logger: Logger;
32
+ auth?: AuthData;
33
+ config?: Config;
23
34
  }
24
35
 
25
36
  export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
@@ -31,6 +42,8 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
31
42
  templateBranch,
32
43
  skipPrompts,
33
44
  logger,
45
+ auth,
46
+ config,
34
47
  } = options;
35
48
 
36
49
  showBanner();
@@ -52,16 +65,39 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
52
65
 
53
66
  // Step 2: Get project name
54
67
  let projectName = initialProjectName;
68
+
69
+ let orgs: OrganizationList | undefined;
70
+ let client: APIClient | undefined;
71
+ let orgId: string | undefined;
72
+
73
+ if (auth) {
74
+ const apiUrl = getAPIBaseURL(config);
75
+ client = new APIClient(apiUrl, config);
76
+ await tui.spinner('Fetching organizations', async () => {
77
+ const resp = await listOrganizations(client!);
78
+ if (resp.data) {
79
+ orgs = resp.data;
80
+ }
81
+ });
82
+ orgId = await tui.selectOrganization(orgs!, config?.preferences?.orgId);
83
+ }
84
+
55
85
  if (!projectName && !skipPrompts) {
56
86
  const response = await enquirer.prompt<{ name: string }>({
57
87
  type: 'input',
58
88
  name: 'name',
59
89
  message: 'What is the name of your project?',
60
90
  initial: 'My First Agent',
61
- validate: (value: string) => {
91
+ validate: async (value: string) => {
62
92
  if (!value || value.trim().length === 0) {
63
93
  return 'Project name is required';
64
94
  }
95
+ if (client) {
96
+ const exists = await projectExists(client, { name: value, organization_id: orgId! });
97
+ if (exists) {
98
+ return `Project with name '${value}' already exists in this organization`;
99
+ }
100
+ }
65
101
  return true;
66
102
  },
67
103
  });
@@ -163,6 +199,24 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
163
199
  logger,
164
200
  });
165
201
 
202
+ if (auth && client && orgId) {
203
+ await tui.spinner('Registering your project', async () => {
204
+ const res = await createProject(client, {
205
+ name: projectName,
206
+ organization_id: orgId,
207
+ provider: 'bunjs',
208
+ });
209
+ if (res.success && res.data) {
210
+ return createProjectConfig(dest, {
211
+ projectId: res.data.id,
212
+ orgId,
213
+ apiKey: res.data.api_key,
214
+ });
215
+ }
216
+ tui.fatal(res.message!);
217
+ });
218
+ }
219
+
166
220
  // Step 8: Show completion message
167
221
  tui.success('✨ Project created successfully!\n');
168
222
  tui.info('Next steps:');
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@ 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';
6
- import { ConfigSchema } from './types';
6
+ import { ConfigSchema, ProjectSchema } from './types';
7
7
  import * as tui from './tui';
8
8
  import { z } from 'zod';
9
9
 
@@ -12,7 +12,7 @@ export function getDefaultConfigDir(): string {
12
12
  }
13
13
 
14
14
  export function getDefaultConfigPath(): string {
15
- return join(getDefaultConfigDir(), 'config.yaml');
15
+ return join(getDefaultConfigDir(), 'production.yaml');
16
16
  }
17
17
 
18
18
  export function getProfilePath(): string {
@@ -90,8 +90,15 @@ export async function fetchProfiles(): Promise<Profile[]> {
90
90
  return profiles;
91
91
  }
92
92
 
93
+ function expandTilde(path: string): string {
94
+ if (path.startsWith('~/')) {
95
+ return join(homedir(), path.slice(2));
96
+ }
97
+ return path;
98
+ }
99
+
93
100
  export async function loadConfig(customPath?: string): Promise<Config | null> {
94
- const configPath = customPath || (await getProfile());
101
+ const configPath = customPath ? expandTilde(customPath) : await getProfile();
95
102
 
96
103
  try {
97
104
  const file = Bun.file(configPath);
@@ -248,7 +255,7 @@ function getPlaceholderValue(schema: z.ZodTypeAny): string {
248
255
  }
249
256
  }
250
257
 
251
- function generateYAMLTemplate(name: string): string {
258
+ export function generateYAMLTemplate(name: string): string {
252
259
  const lines: string[] = [];
253
260
 
254
261
  // Add name (required)
@@ -308,4 +315,46 @@ function generateYAMLTemplate(name: string): string {
308
315
  return lines.join('\n');
309
316
  }
310
317
 
311
- export { generateYAMLTemplate };
318
+ class ProjectConfigNotFoundExpection extends Error {
319
+ public name: string;
320
+ constructor() {
321
+ super('project not found');
322
+ this.name = 'ProjectConfigNotFoundExpection';
323
+ }
324
+ }
325
+
326
+ type ProjectConfig = z.infer<typeof ProjectSchema>;
327
+
328
+ export async function loadProjectConfig(dir: string): Promise<ProjectConfig> {
329
+ const configPath = join(dir, 'agentuity.json');
330
+ const file = Bun.file(configPath);
331
+ if (!(await file.exists())) {
332
+ throw new ProjectConfigNotFoundExpection();
333
+ }
334
+ const text = await file.text();
335
+ const config = JSON.parse(text);
336
+ const result = ProjectSchema.safeParse(config);
337
+ if (!result.success) {
338
+ tui.error(`Invalid project config at ${configPath}:`);
339
+ for (const issue of result.error.issues) {
340
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
341
+ tui.bullet(`${path}: ${issue.message}`);
342
+ }
343
+ process.exit(1);
344
+ }
345
+ return result.data;
346
+ }
347
+
348
+ type InitialProjectConfig = ProjectConfig & {
349
+ apiKey: string;
350
+ };
351
+
352
+ export async function createProjectConfig(dir: string, config: InitialProjectConfig) {
353
+ const configPath = join(dir, 'agentuity.json');
354
+ const configFile = Bun.file(configPath);
355
+ await configFile.write(JSON.stringify(config, null, 2));
356
+
357
+ const envPath = join(dir, '.env');
358
+ const envFile = Bun.file(envPath);
359
+ await envFile.write(`AGENTUITY_SDK_KEY=${config.apiKey}`);
360
+ }
package/src/index.ts CHANGED
@@ -1,7 +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
+ export { requireAuth, optionalAuth } from './auth';
5
5
  export {
6
6
  loadConfig,
7
7
  saveConfig,
@@ -16,7 +16,7 @@ export {
16
16
  clearAuth,
17
17
  getAuth,
18
18
  } from './config';
19
- export { APIClient, getAPIBaseURL, getAppBaseURL, UpgradeRequiredError } from './api';
19
+ export { APIClient, getAPIBaseURL, getAppBaseURL } from './api';
20
20
  export { Logger, logger } from './logger';
21
21
  export { showBanner } from './banner';
22
22
  export { discoverCommands } from './cmd';
package/src/sound.ts CHANGED
@@ -33,7 +33,13 @@ export function playSound(): void {
33
33
  return;
34
34
  }
35
35
 
36
- Bun.spawn(command, {
37
- stdio: ['ignore', 'ignore', 'ignore'],
38
- }).unref();
36
+ if (process.stdout.isTTY && Bun.which(command[0])) {
37
+ try {
38
+ Bun.spawn(command, {
39
+ stdio: ['ignore', 'ignore', 'ignore'],
40
+ }).unref();
41
+ } catch {
42
+ /* ignore */
43
+ }
44
+ }
39
45
  }
package/src/tui.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * Provides semantic helpers for console output with automatic icons and colors.
5
5
  * Uses Bun's built-in color support and ANSI escape codes.
6
6
  */
7
-
7
+ import enquirer from 'enquirer';
8
+ import type { OrganizationList } from '@agentuity/server';
8
9
  import type { ColorScheme } from './terminal';
9
10
 
10
11
  // Icons
@@ -205,6 +206,15 @@ export function padLeft(str: string, length: number, pad = ' '): string {
205
206
  return pad.repeat(length - str.length) + str;
206
207
  }
207
208
 
209
+ interface BannerOptions {
210
+ padding?: number;
211
+ minWidth?: number;
212
+ topSpacer?: boolean;
213
+ middleSpacer?: boolean;
214
+ bottomSpacer?: boolean;
215
+ centerTitle?: boolean;
216
+ }
217
+
208
218
  /**
209
219
  * Display a formatted banner with title and body content
210
220
  * Creates a bordered box around the content
@@ -212,10 +222,12 @@ export function padLeft(str: string, length: number, pad = ' '): string {
212
222
  * Uses Bun.stringWidth() for accurate width calculation with ANSI codes and unicode
213
223
  * Responsive to terminal width - adapts to narrow terminals
214
224
  */
215
- export function banner(title: string, body: string): void {
225
+ export function banner(title: string, body: string, options?: BannerOptions): void {
216
226
  // Get terminal width, default to 80 if not available, minimum 40
217
227
  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
228
+ const minWidth = options?.minWidth ?? 40;
229
+ const maxWidth = Math.max(minWidth, Math.min(termWidth - 2, 80)); // Between 40 and 80, with 2 char margin
230
+ const padding = options?.padding ?? 4;
219
231
 
220
232
  const border = {
221
233
  topLeft: '╭',
@@ -227,14 +239,14 @@ export function banner(title: string, body: string): void {
227
239
  };
228
240
 
229
241
  // Split body into lines and wrap if needed
230
- const bodyLines = wrapText(body, maxWidth - 4); // -4 for padding and borders
242
+ const bodyLines = wrapText(body, maxWidth - padding); // -4 for padding and borders
231
243
 
232
244
  // Calculate width based on content
233
245
  const titleWidth = getDisplayWidth(title);
234
246
  const maxBodyWidth = Math.max(...bodyLines.map((line) => getDisplayWidth(line)));
235
- const contentWidth = Math.max(titleWidth, maxBodyWidth);
236
- const boxWidth = Math.min(contentWidth + 4, maxWidth); // +4 for padding
237
- const innerWidth = boxWidth - 4;
247
+ const contentWidth = Math.max(minWidth, Math.max(titleWidth, maxBodyWidth) + padding);
248
+ const boxWidth = Math.min(contentWidth, maxWidth); // +N for padding
249
+ const innerWidth = boxWidth - padding;
238
250
 
239
251
  // Colors
240
252
  const borderColor = getColor('muted');
@@ -249,41 +261,58 @@ export function banner(title: string, body: string): void {
249
261
  `${borderColor}${border.topLeft}${border.horizontal.repeat(boxWidth - 2)}${border.topRight}${reset}`
250
262
  );
251
263
 
252
- // Empty line
253
- lines.push(
254
- `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
255
- );
264
+ if (options?.topSpacer === true || options?.topSpacer === undefined) {
265
+ // Empty line
266
+ lines.push(
267
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
268
+ );
269
+ }
256
270
 
257
271
  // Title (centered and bold)
258
272
  const titleDisplayWidth = getDisplayWidth(title);
259
- const titlePadding = Math.max(0, Math.floor((innerWidth - titleDisplayWidth) / 2));
260
- const titleRightPadding = Math.max(0, innerWidth - titlePadding - titleDisplayWidth);
261
- const titleLine =
262
- ' '.repeat(titlePadding) +
263
- `${titleColor}${bold(title)}${reset}` +
264
- ' '.repeat(titleRightPadding);
265
- lines.push(
266
- `${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
267
- );
273
+ if (options?.centerTitle === true || options?.centerTitle === undefined) {
274
+ const titlePadding = Math.max(0, Math.floor((innerWidth - titleDisplayWidth) / 2));
275
+ const titleRightPadding = Math.max(
276
+ 0,
277
+ Math.max(0, innerWidth - titlePadding - titleDisplayWidth) - padding
278
+ );
279
+ const titleLine =
280
+ ' '.repeat(titlePadding) +
281
+ `${titleColor}${bold(title)}${reset}` +
282
+ ' '.repeat(titleRightPadding);
283
+ lines.push(
284
+ `${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
285
+ );
286
+ } else {
287
+ const titleRightPadding = Math.max(0, Math.max(0, innerWidth - titleDisplayWidth) - padding);
288
+ const titleLine = `${titleColor}${bold(title)}${reset}` + ' '.repeat(titleRightPadding);
289
+ lines.push(
290
+ `${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
291
+ );
292
+ }
268
293
 
269
- // Empty line
270
- lines.push(
271
- `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
272
- );
294
+ if (options?.middleSpacer === true || options?.middleSpacer === undefined) {
295
+ // Empty line
296
+ lines.push(
297
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
298
+ );
299
+ }
273
300
 
274
301
  // Body lines
275
302
  for (const line of bodyLines) {
276
303
  const lineWidth = getDisplayWidth(line);
277
- const padding = Math.max(0, innerWidth - lineWidth);
304
+ const linePadding = Math.max(0, Math.max(0, innerWidth - lineWidth) - padding);
278
305
  lines.push(
279
- `${borderColor}${border.vertical} ${reset}${line}${' '.repeat(padding)}${borderColor} ${border.vertical}${reset}`
306
+ `${borderColor}${border.vertical} ${reset}${line}${' '.repeat(linePadding)}${borderColor} ${border.vertical}${reset}`
280
307
  );
281
308
  }
282
309
 
283
- // Empty line
284
- lines.push(
285
- `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
286
- );
310
+ if (options?.bottomSpacer === true || options?.bottomSpacer === undefined) {
311
+ // Empty line
312
+ lines.push(
313
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
314
+ );
315
+ }
287
316
 
288
317
  // Bottom border
289
318
  lines.push(
@@ -418,6 +447,30 @@ export function showSignupBenefits(): void {
418
447
  console.log('');
419
448
  }
420
449
 
450
+ /**
451
+ * Display a message when unauthenticated to let the user know certain capabilities are disabled
452
+ */
453
+ export function showLoggedOutMessage(): void {
454
+ const CYAN = Bun.color('yellow', 'ansi-16m');
455
+ const TEXT =
456
+ currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
457
+ const RESET = '\x1b[0m';
458
+
459
+ const lines = [
460
+ '╔══════════════════════════════════════════════╗',
461
+ `║ ⨺ Unauthenticated (local mode) ║`,
462
+ '║ ║',
463
+ `║ ${TEXT}Certain capabilities such as the AI services${CYAN} ║`,
464
+ `║ ${TEXT}and devmode remote are unavailable when${CYAN} ║`,
465
+ `║ ${TEXT}unauthenticated.${CYAN} ║`,
466
+ '╚══════════════════════════════════════════════╝',
467
+ ];
468
+
469
+ console.log('');
470
+ lines.forEach((line) => console.log(CYAN + line + RESET));
471
+ console.log('');
472
+ }
473
+
421
474
  /**
422
475
  * Copy text to clipboard
423
476
  * Returns true if successful, false otherwise
@@ -649,6 +702,28 @@ export async function spinner<T>(
649
702
  }
650
703
 
651
704
  const message = options.message;
705
+ const reset = COLORS.reset;
706
+
707
+ // If no TTY, just execute the callback without animation
708
+ if (!process.stdout.isTTY) {
709
+ try {
710
+ const result =
711
+ options.type === 'progress'
712
+ ? await options.callback(() => {})
713
+ : typeof options.callback === 'function'
714
+ ? await options.callback()
715
+ : await options.callback;
716
+
717
+ const successColor = getColor('success');
718
+ console.log(`${successColor}${ICONS.success} ${message}${reset}`);
719
+ return result;
720
+ } catch (err) {
721
+ const errorColor = getColor('error');
722
+ console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
723
+ throw err;
724
+ }
725
+ }
726
+
652
727
  const frames = ['◐', '◓', '◑', '◒'];
653
728
  const spinnerColors = [
654
729
  { light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
@@ -657,7 +732,6 @@ export async function spinner<T>(
657
732
  { light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
658
733
  ];
659
734
  const bold = '\x1b[1m';
660
- const reset = COLORS.reset;
661
735
  const cyanColor = { light: '\x1b[36m', dark: '\x1b[96m' }[currentColorScheme];
662
736
 
663
737
  let frameIndex = 0;
@@ -905,41 +979,52 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
905
979
  // Wait for process to exit
906
980
  const exitCode = await proc.exited;
907
981
 
908
- // Move cursor up to redraw final state
982
+ // If clearOnSuccess is true and command succeeded, clear everything
983
+ if (clearOnSuccess && exitCode === 0) {
984
+ if (linesRendered > 0) {
985
+ // Move up to the command line
986
+ process.stdout.write(`\x1b[${linesRendered}A`);
987
+ // Clear each line (entire line) and move cursor back up
988
+ for (let i = 0; i < linesRendered; i++) {
989
+ process.stdout.write('\x1b[2K'); // Clear entire line
990
+ if (i < linesRendered - 1) {
991
+ process.stdout.write('\x1b[B'); // Move down one line
992
+ }
993
+ }
994
+ // Move cursor back up to original position
995
+ process.stdout.write(`\x1b[${linesRendered}A\r`);
996
+ }
997
+ return exitCode;
998
+ }
999
+
1000
+ // Clear all rendered lines completely
909
1001
  if (linesRendered > 0) {
1002
+ // Move up to the command line (first line of our output)
910
1003
  process.stdout.write(`\x1b[${linesRendered}A`);
1004
+ // Move to beginning of line and clear from cursor to end of screen
1005
+ process.stdout.write('\r\x1b[J');
911
1006
  }
912
1007
 
913
- // Clear all lines if clearOnSuccess is true and command succeeded
914
- if (clearOnSuccess && exitCode === 0) {
915
- // Clear all rendered lines
916
- for (let i = 0; i < linesRendered; i++) {
917
- process.stdout.write('\r\x1b[K\n');
918
- }
919
- // Move cursor back up
920
- process.stdout.write(`\x1b[${linesRendered}A`);
1008
+ // Determine icon based on exit code
1009
+ const icon = exitCode === 0 ? ICONS.success : ICONS.error;
1010
+ const statusColor = exitCode === 0 ? green : red;
921
1011
 
922
- // Show compact success: command
923
- process.stdout.write(
924
- `\r\x1b[K${green}${ICONS.success}${reset} ${cmdColor}${displayCmd}${reset}\n`
925
- );
926
- } else {
927
- // Determine how many lines to show in final output
928
- const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
929
-
930
- // Show final status with appropriate color
931
- const statusColor = exitCode === 0 ? green : red;
932
- process.stdout.write(`\r\x1b[K${statusColor}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
933
-
934
- // Show final output lines
935
- const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
936
- for (const line of finalOutputLines) {
937
- let displayLine = line;
938
- if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
939
- displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
940
- }
941
- process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
1012
+ // Show final status: icon + command
1013
+ process.stdout.write(
1014
+ `\r\x1b[K${statusColor}${icon}${reset} ${cmdColor}${displayCmd}${reset}\n`
1015
+ );
1016
+
1017
+ // Determine how many lines to show in final output
1018
+ const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
1019
+
1020
+ // Show final output lines
1021
+ const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
1022
+ for (const line of finalOutputLines) {
1023
+ let displayLine = line;
1024
+ if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
1025
+ displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
942
1026
  }
1027
+ process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
943
1028
  }
944
1029
 
945
1030
  return exitCode;
@@ -971,3 +1056,21 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
971
1056
  process.stdout.write('\x1B[?25h');
972
1057
  }
973
1058
  }
1059
+
1060
+ export async function selectOrganization(
1061
+ orgs: OrganizationList,
1062
+ initial?: string
1063
+ ): Promise<string> {
1064
+ if (orgs.length === 1) {
1065
+ return orgs[0].id;
1066
+ }
1067
+ const response = await enquirer.prompt<{ action: string }>({
1068
+ type: 'select',
1069
+ name: 'action',
1070
+ message: 'Select an organization',
1071
+ initial,
1072
+ choices: orgs.map((o) => ({ message: o.name, name: o.id })),
1073
+ });
1074
+
1075
+ return response.action;
1076
+ }