@agentuity/cli 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.ts CHANGED
@@ -8,7 +8,27 @@ import { detectColorScheme } from '../src/terminal';
8
8
  import { setColorScheme } from '../src/tui';
9
9
  import { getVersion } from '../src/version';
10
10
  import { checkLegacyCLI } from '../src/legacy-check';
11
- import type { LogLevel } from '../src/types';
11
+ import type { CommandContext, LogLevel } from '../src/types';
12
+
13
+ // Cleanup TTY state before exit
14
+ function cleanupAndExit() {
15
+ if (process.stdin.isTTY) {
16
+ process.stdin.setRawMode(false);
17
+ process.stdout.write('\x1B[?25h'); // Restore cursor
18
+ }
19
+ process.exitCode = 0;
20
+ process.exit(0);
21
+ }
22
+
23
+ // Handle Ctrl+C gracefully
24
+ process.once('SIGINT', () => {
25
+ console.log('\n');
26
+ cleanupAndExit();
27
+ });
28
+
29
+ process.once('SIGTERM', () => {
30
+ cleanupAndExit();
31
+ });
12
32
 
13
33
  validateRuntime();
14
34
 
@@ -61,11 +81,32 @@ const ctx = {
61
81
  };
62
82
 
63
83
  const commands = await discoverCommands();
64
- await registerCommands(program, commands, ctx);
84
+ await registerCommands(program, commands, ctx as unknown as CommandContext);
65
85
 
66
86
  try {
67
87
  await program.parseAsync(process.argv);
68
88
  } catch (error) {
89
+ // Don't log error if it's from Ctrl+C, user cancellation, or signal termination
90
+ if (error instanceof Error) {
91
+ const msg = error.message.toLowerCase();
92
+ if (
93
+ msg.includes('sigint') ||
94
+ msg.includes('sigterm') ||
95
+ msg.includes('user force closed') ||
96
+ msg.includes('cancelled') || // UK
97
+ msg.includes('canceled') || // US
98
+ msg === ''
99
+ ) {
100
+ process.exit(0);
101
+ }
102
+ if ('name' in error && error.name === 'AbortError') {
103
+ process.exit(0);
104
+ }
105
+ }
106
+ // Also exit cleanly if error is empty/undefined (user cancellation)
107
+ if (!error) {
108
+ process.exit(0);
109
+ }
69
110
  logger.error('CLI error:', error);
70
111
  process.exit(1);
71
112
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cmd/dev/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,OAAO,qCA2ElB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cmd/dev/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,OAAO,qCAyElB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/create.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,uBAAuB,wCAgPlC,CAAC"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/create.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,wCA+ClC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Logger } from '@/logger';
2
+ import type { TemplateInfo } from './templates';
3
+ interface DownloadOptions {
4
+ dest: string;
5
+ template: TemplateInfo;
6
+ templateDir?: string;
7
+ templateBranch?: string;
8
+ }
9
+ interface SetupOptions {
10
+ dest: string;
11
+ projectName: string;
12
+ dirName: string;
13
+ noInstall: boolean;
14
+ noBuild: boolean;
15
+ logger: Logger;
16
+ }
17
+ export declare function downloadTemplate(options: DownloadOptions): Promise<void>;
18
+ export declare function setupProject(options: SetupOptions): Promise<void>;
19
+ export {};
20
+ //# sourceMappingURL=download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/download.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,eAAe;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,YAAY;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAsBD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD9E;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCvE"}
@@ -0,0 +1,15 @@
1
+ import type { Logger } from '@/logger';
2
+ interface CreateFlowOptions {
3
+ projectName?: string;
4
+ dir?: string;
5
+ template?: string;
6
+ templateDir?: string;
7
+ templateBranch?: string;
8
+ noInstall: boolean;
9
+ noBuild: boolean;
10
+ skipPrompts: boolean;
11
+ logger: Logger;
12
+ }
13
+ export declare function runCreateFlow(options: CreateFlowOptions): Promise<void>;
14
+ export {};
15
+ //# sourceMappingURL=template-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-flow.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/template-flow.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAMvC,UAAU,iBAAiB;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0J7E"}
@@ -0,0 +1,8 @@
1
+ export interface TemplateInfo {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ directory: string;
6
+ }
7
+ export declare function fetchTemplates(localDir?: string, branch?: string): Promise<TemplateInfo[]>;
8
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/templates.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAsB,cAAc,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgChG"}
@@ -0,0 +1,25 @@
1
+ export interface DownloadOptions {
2
+ url: string;
3
+ headers?: Record<string, string>;
4
+ message?: string;
5
+ onProgress?: (percent: number, downloadedBytes: number, totalBytes: number) => void;
6
+ }
7
+ /**
8
+ * Download a file with progress tracking
9
+ * Returns the response body stream for further processing
10
+ */
11
+ export declare function downloadWithProgress(options: DownloadOptions): Promise<NodeJS.ReadableStream>;
12
+ /**
13
+ * Download a file with a TUI spinner showing progress
14
+ */
15
+ export declare function downloadWithSpinner<T>(options: DownloadOptions, processor: (stream: NodeJS.ReadableStream) => Promise<T>): Promise<T>;
16
+ /**
17
+ * Download a GitHub tarball with progress tracking
18
+ */
19
+ export interface DownloadGitHubOptions {
20
+ repo: string;
21
+ branch?: string;
22
+ message?: string;
23
+ }
24
+ export declare function downloadGitHubTarball(options: DownloadGitHubOptions): Promise<NodeJS.ReadableStream>;
25
+ //# sourceMappingURL=download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACpF;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,eAAe,GACtB,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAsChC;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAC1C,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,GACtD,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAKhC"}
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { getCommandPrefix, getCommand } from './command-prefix';
11
11
  export * as tui from './tui';
12
12
  export { runSteps, setStepsColorScheme, stepSuccess, stepSkipped, stepError } from './steps';
13
13
  export { playSound } from './sound';
14
+ export { downloadWithProgress, downloadWithSpinner, downloadGitHubTarball, type DownloadOptions as DownloadOptionsType, type DownloadGitHubOptions, } from './download';
14
15
  export type { Config, LogLevel, GlobalOptions, CommandContext, SubcommandDefinition, CommandDefinition, Profile, AuthData, CommandSchemas, } from './types';
15
16
  export { createSubcommand, createCommand } from './types';
16
17
  export type { ColorScheme } from './terminal';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EACN,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACP,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AACtF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,YAAY,EACX,MAAM,EACN,QAAQ,EACR,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,OAAO,EACP,QAAQ,EACR,cAAc,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EACN,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACP,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AACtF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EACN,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,eAAe,IAAI,mBAAmB,EAC3C,KAAK,qBAAqB,GAC1B,MAAM,YAAY,CAAC;AACpB,YAAY,EACX,MAAM,EACN,QAAQ,EACR,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,OAAO,EACP,QAAQ,EACR,cAAc,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
package/dist/sound.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare function playSound(): Promise<void>;
1
+ export declare function playSound(): void;
2
2
  //# sourceMappingURL=sound.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sound.d.ts","sourceRoot":"","sources":["../src/sound.ts"],"names":[],"mappings":"AAEA,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB/C"}
1
+ {"version":3,"file":"sound.d.ts","sourceRoot":"","sources":["../src/sound.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,IAAI,IAAI,CAoChC"}
package/dist/tui.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare function error(message: string): void;
17
17
  /**
18
18
  * Print a warning message with a yellow warning icon
19
19
  */
20
- export declare function warning(message: string): void;
20
+ export declare function warning(message: string, asError?: boolean): void;
21
21
  /**
22
22
  * Print an info message with a cyan info icon
23
23
  */
@@ -141,6 +141,11 @@ export interface CommandRunnerOptions {
141
141
  * Environment variables
142
142
  */
143
143
  env?: Record<string, string>;
144
+ /**
145
+ * If true, clear output on success and only show command + success icon
146
+ * Defaults to false
147
+ */
148
+ clearOnSuccess?: boolean;
144
149
  }
145
150
  /**
146
151
  * Run an external command and stream its output with a live UI
package/dist/tui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -24,20 +24,20 @@
24
24
  "clean": "rm -rf dist",
25
25
  "build": "bunx tsc --build --emitDeclarationOnly --force",
26
26
  "typecheck": "bunx tsc --noEmit",
27
- "test": "bun scripts/test-create-integration.ts",
28
- "prepublishOnly": "bun run clean && bun run build",
29
- "setup-dev-template": "bun scripts/setup-dev-template.ts",
30
- "simulate-create": "bun scripts/simulate-bun-create.ts"
27
+ "test": "bun scripts/test-create-flow.ts",
28
+ "prepublishOnly": "bun run clean && bun run build"
31
29
  },
32
30
  "dependencies": {
33
- "@agentuity/core": "0.0.9",
31
+ "@agentuity/core": "0.0.11",
34
32
  "acorn-loose": "^8.5.2",
35
33
  "astring": "^1.9.0",
36
34
  "commander": "^14.0.2",
37
35
  "enquirer": "^2.4.1",
36
+ "tar-fs": "^3.1.1",
38
37
  "zod": "^4.1.12"
39
38
  },
40
39
  "devDependencies": {
40
+ "@types/tar-fs": "^2.0.4",
41
41
  "typescript": "^5.9.0"
42
42
  },
43
43
  "publishConfig": {
@@ -35,9 +35,7 @@ export const command = createCommand({
35
35
  process.exit(1);
36
36
  }
37
37
 
38
- tui.newline();
39
38
  tui.info('Starting development server...');
40
- tui.newline();
41
39
 
42
40
  // Use shell to run in a process group for proper cleanup
43
41
  // The 'exec' ensures the shell is replaced by the actual process
@@ -6,9 +6,9 @@ export const soundSubcommand: SubcommandDefinition = {
6
6
  name: 'sound',
7
7
  description: 'Test completion sound',
8
8
 
9
- async handler() {
9
+ handler() {
10
10
  tui.info('Playing completion sound...');
11
- await playSound();
11
+ playSound();
12
12
  tui.success('Sound played!');
13
13
  },
14
14
  };
@@ -1,8 +1,6 @@
1
1
  import { createSubcommand } from '@/types';
2
2
  import { z } from 'zod';
3
- import enquirer from 'enquirer';
4
- import { existsSync, readdirSync } from 'node:fs';
5
- import { resolve, basename, join } from 'node:path';
3
+ import { runCreateFlow } from './template-flow';
6
4
 
7
5
  export const createProjectSubcommand = createSubcommand({
8
6
  name: 'create',
@@ -14,234 +12,41 @@ export const createProjectSubcommand = createSubcommand({
14
12
  options: z.object({
15
13
  name: z.string().optional().describe('Project name'),
16
14
  dir: z.string().optional().describe('Directory to create the project in'),
15
+ template: z.string().optional().describe('Template to use'),
16
+ templateDir: z
17
+ .string()
18
+ .optional()
19
+ .describe('Local template directory for testing (e.g., ./packages/templates)'),
20
+ templateBranch: z
21
+ .string()
22
+ .optional()
23
+ .describe('GitHub branch to use for templates (default: main)'),
17
24
  install: z
18
25
  .boolean()
19
26
  .optional()
20
27
  .default(true)
21
28
  .describe('Run bun install after creating the project (use --no-install to skip)'),
22
- confirm: z.boolean().optional().describe('Skip confirmation prompts'),
23
- fromBunCreate: z
29
+ build: z
24
30
  .boolean()
25
31
  .optional()
26
- .describe('Internal: called from bun create postinstall'),
27
- dev: z.boolean().optional().describe('Internal: use local template for testing'),
32
+ .default(true)
33
+ .describe('Run bun run build after installing (use --no-build to skip)'),
34
+ confirm: z.boolean().optional().describe('Skip confirmation prompts'),
28
35
  }),
29
36
  },
30
37
 
31
38
  async handler(ctx) {
32
39
  const { logger, opts } = ctx;
33
-
34
- // Case 2: Called from bun create postinstall
35
- if (opts.fromBunCreate) {
36
- const projectDir = process.cwd();
37
- const packageJsonPath = join(projectDir, 'package.json');
38
-
39
- if (!existsSync(packageJsonPath)) {
40
- logger.error('package.json not found in current directory');
41
- return;
42
- }
43
-
44
- // Disable log prefixes for cleaner postinstall output
45
- logger.setShowPrefix(false);
46
-
47
- const packageJsonFile = Bun.file(packageJsonPath);
48
- const packageJson = await packageJsonFile.json();
49
- const projectName = packageJson.name || basename(projectDir);
50
-
51
- logger.info(`\n🔧 Setting up ${projectName}...\n`);
52
-
53
- // Update package.json - remove bun-create metadata
54
- packageJson.name = projectName;
55
- delete packageJson['bun-create'];
56
- delete packageJson.bin;
57
- packageJson.private = true;
58
- delete packageJson.files;
59
- delete packageJson.keywords;
60
- delete packageJson.author;
61
- delete packageJson.license;
62
- delete packageJson.publishConfig;
63
- delete packageJson.description;
64
-
65
- // Remove enquirer from dependencies (only needed for setup)
66
- if (packageJson.dependencies) {
67
- delete packageJson.dependencies.enquirer;
68
- }
69
-
70
- await Bun.write(packageJsonPath, JSON.stringify(packageJson, null, '\t'));
71
- logger.info('✓ Updated package.json');
72
-
73
- // Update README.md
74
- const readmePath = join(projectDir, 'README.md');
75
- if (existsSync(readmePath)) {
76
- const readmeFile = Bun.file(readmePath);
77
- let readme = await readmeFile.text();
78
- readme = readme.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
79
- await Bun.write(readmePath, readme);
80
- logger.info('✓ Updated README.md');
81
- }
82
-
83
- // Update AGENTS.md
84
- const agentsMdPath = join(projectDir, 'AGENTS.md');
85
- if (existsSync(agentsMdPath)) {
86
- const agentsMdFile = Bun.file(agentsMdPath);
87
- let agentsMd = await agentsMdFile.text();
88
- agentsMd = agentsMd.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
89
- await Bun.write(agentsMdPath, agentsMd);
90
- logger.info('✓ Updated AGENTS.md');
91
- }
92
-
93
- // Remove setup files
94
- const filesToRemove = ['setup.ts'];
95
- for (const file of filesToRemove) {
96
- const filePath = join(projectDir, file);
97
- if (existsSync(filePath)) {
98
- await Bun.$`rm ${filePath}`;
99
- logger.info('✓ Removed ${file}');
100
- }
101
- }
102
-
103
- logger.info('\n✨ Setup complete!\n');
104
- return;
105
- }
106
-
107
- // Case 1: Normal CLI flow
108
- // Relaxed validation: any reasonable name between 2-64 characters
109
- const isValidProjectName = (name: string): boolean => {
110
- return name.trim().length >= 2 && name.trim().length <= 64;
111
- };
112
-
113
- // Transform name to URL and disk-friendly format
114
- const transformToDirectoryName = (name: string): string => {
115
- const result = name
116
- .trim()
117
- .toLowerCase()
118
- .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
119
- .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
120
- .replace(/-+/g, '-') // Replace consecutive hyphens with single hyphen
121
- .substring(0, 64); // Ensure max length
122
-
123
- // Validate result is non-empty (happens when name contains only special chars)
124
- if (!result) {
125
- throw new Error(
126
- `Invalid project name "${name}": must contain at least one alphanumeric character`
127
- );
128
- }
129
-
130
- return result;
131
- };
132
-
133
- // Get project name
134
- let projectName = opts.name;
135
- while (!projectName || !isValidProjectName(projectName)) {
136
- const result = await enquirer.prompt<{ name: string }>({
137
- type: 'input',
138
- name: 'name',
139
- message: 'Project name:',
140
- initial: projectName,
141
- validate: (value: string) => {
142
- if (!value) return 'Project name is required';
143
- if (!isValidProjectName(value)) {
144
- return 'Project name must be between 2 and 64 characters';
145
- }
146
- return true;
147
- },
148
- });
149
- projectName = result.name;
150
- }
151
-
152
- projectName = projectName.trim();
153
- const projectDirName = transformToDirectoryName(projectName);
154
-
155
- // Get directory - if specified, create the project there, otherwise create in current dir
156
- const baseDir = opts.dir ? resolve(opts.dir) : process.cwd();
157
- const targetDir = resolve(baseDir, projectDirName);
158
-
159
- // Check if directory exists and validate
160
- let shouldProceed = true;
161
- if (existsSync(targetDir)) {
162
- const files = readdirSync(targetDir);
163
- const hasFiles = files.length > 0;
164
-
165
- if (hasFiles) {
166
- if (opts.confirm === false) {
167
- logger.error(`Directory ${targetDir} is not empty and --no-confirm was specified`);
168
- return;
169
- }
170
-
171
- // Require explicit confirmation in non-TTY environments
172
- if (opts.confirm !== true && !process.stdin.isTTY) {
173
- logger.error(
174
- `Directory "${targetDir}" is not empty. Use --confirm flag in non-interactive environments.`
175
- );
176
- return;
177
- }
178
-
179
- // Interactive prompt in TTY environments
180
- if (opts.confirm !== true && process.stdin.isTTY) {
181
- const result = await enquirer.prompt<{ proceed: boolean }>({
182
- type: 'confirm',
183
- name: 'proceed',
184
- message: `Directory "${targetDir}" is not empty. Files may be overwritten. Continue?`,
185
- initial: false,
186
- });
187
- shouldProceed = result.proceed;
188
- }
189
-
190
- if (!shouldProceed) {
191
- logger.info('Operation cancelled');
192
- return;
193
- }
194
- }
195
- }
196
-
197
- // Print collected values
198
- logger.info('\n=== Project Configuration ===');
199
- logger.info(`Name: ${projectName}`);
200
- logger.info(`Directory Name: ${projectDirName}`);
201
- logger.info(`Target Directory: ${targetDir}`);
202
- logger.info('=============================\n');
203
-
204
- // Run bun create to scaffold the project
205
- logger.info('Creating project from template...');
206
-
207
- try {
208
- // Determine template name based on dev mode
209
- const templateName = opts.dev ? 'agentuity-dev' : 'agentuity';
210
-
211
- if (opts.dev) {
212
- logger.info('🔧 Dev mode: Using local template');
213
- }
214
-
215
- // Build bun create command args
216
- // Note: bun create supports --no-install to skip dependency installation
217
- const bunCreateArgs = ['bun', 'create'];
218
- if (opts.install === false) {
219
- bunCreateArgs.push('--no-install');
220
- }
221
- bunCreateArgs.push(templateName, projectDirName);
222
-
223
- logger.info(`Running: ${bunCreateArgs.join(' ')}`);
224
-
225
- const result = Bun.spawn(bunCreateArgs, {
226
- cwd: baseDir,
227
- stdout: 'inherit',
228
- stderr: 'inherit',
229
- stdin: 'inherit',
230
- });
231
-
232
- const exitCode = await result.exited;
233
-
234
- if (exitCode !== 0) {
235
- throw new Error(`bun create exited with code ${exitCode}`);
236
- }
237
-
238
- logger.info('\n✨ Project created successfully!');
239
- logger.info(`\nNext steps:`);
240
- logger.info(` cd ${projectDirName}`);
241
- logger.info(` bun run dev`);
242
- } catch (error) {
243
- logger.error('Failed to create project:', error);
244
- throw error;
245
- }
40
+ await runCreateFlow({
41
+ projectName: opts.name,
42
+ dir: opts.dir,
43
+ template: opts.template,
44
+ templateDir: opts.templateDir,
45
+ templateBranch: opts.templateBranch,
46
+ noInstall: opts.install === false,
47
+ noBuild: opts.build === false,
48
+ skipPrompts: opts.confirm === true,
49
+ logger,
50
+ });
246
51
  },
247
52
  });
@@ -0,0 +1,159 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { existsSync, mkdirSync, renameSync, readdirSync, cpSync, rmSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { createGunzip } from 'node:zlib';
6
+ import { extract, type Headers } from 'tar-fs';
7
+ import type { Logger } from '@/logger';
8
+ import * as tui from '@/tui';
9
+ import { downloadWithSpinner } from '@/download';
10
+ import type { TemplateInfo } from './templates';
11
+
12
+ const GITHUB_REPO = 'agentuity/sdk';
13
+ const GITHUB_BRANCH = 'main';
14
+
15
+ interface DownloadOptions {
16
+ dest: string;
17
+ template: TemplateInfo;
18
+ templateDir?: string;
19
+ templateBranch?: string;
20
+ }
21
+
22
+ interface SetupOptions {
23
+ dest: string;
24
+ projectName: string;
25
+ dirName: string;
26
+ noInstall: boolean;
27
+ noBuild: boolean;
28
+ logger: Logger;
29
+ }
30
+
31
+ async function cleanup(sourceDir: string, dest: string) {
32
+ if (!existsSync(sourceDir)) {
33
+ throw new Error(`Template directory not found: ${sourceDir}`);
34
+ }
35
+
36
+ tui.spinner(`📦 Copying template from ${sourceDir}...`, async () => {
37
+ // Copy all files from source to dest
38
+ const files = readdirSync(sourceDir);
39
+ for (const file of files) {
40
+ cpSync(join(sourceDir, file), join(dest, file), { recursive: true });
41
+ }
42
+
43
+ // Rename gitignore -> .gitignore
44
+ const gi = join(dest, 'gitignore');
45
+ if (existsSync(gi)) {
46
+ renameSync(gi, join(dest, '.gitignore'));
47
+ }
48
+ });
49
+ }
50
+
51
+ export async function downloadTemplate(options: DownloadOptions): Promise<void> {
52
+ const { dest, template, templateDir, templateBranch } = options;
53
+
54
+ mkdirSync(dest, { recursive: true });
55
+
56
+ // Copy from local directory if provided
57
+ if (templateDir) {
58
+ const sourceDir = resolve(join(templateDir, template.directory));
59
+
60
+ if (!existsSync(sourceDir)) {
61
+ throw new Error(`Template directory not found: ${sourceDir}`);
62
+ }
63
+
64
+ return cleanup(sourceDir, dest);
65
+ }
66
+
67
+ // Download from GitHub
68
+ const branch = templateBranch || GITHUB_BRANCH;
69
+ const templatePath = `templates/${template.directory}`;
70
+ const url = `https://codeload.github.com/${GITHUB_REPO}/tar.gz/${branch}`;
71
+ const tempDir = join(dest, '.temp-download');
72
+ mkdirSync(tempDir, { recursive: true });
73
+
74
+ await downloadWithSpinner(
75
+ {
76
+ url,
77
+ message: templateBranch
78
+ ? `Downloading template files from branch ${branch}...`
79
+ : 'Downloading template files...',
80
+ },
81
+ async (stream) => {
82
+ // Extract only the template directory from tarball
83
+ const prefix = `sdk-${branch}/${templatePath}/`;
84
+ await pipeline(
85
+ stream,
86
+ createGunzip(),
87
+ extract(tempDir, {
88
+ filter: (name: string) => name.startsWith(prefix),
89
+ map: (header: Headers) => {
90
+ header.name = header.name.substring(prefix.length);
91
+ return header;
92
+ },
93
+ })
94
+ );
95
+ }
96
+ );
97
+
98
+ await cleanup(tempDir, dest);
99
+
100
+ // Extra safety: refuse to delete root or home directories
101
+ const home = homedir();
102
+ if (tempDir === '/' || tempDir === home) {
103
+ throw new Error(`Refusing to delete protected path: ${tempDir}`);
104
+ }
105
+ rmSync(tempDir, { recursive: true, force: true });
106
+ }
107
+
108
+ export async function setupProject(options: SetupOptions): Promise<void> {
109
+ const { dest, projectName, dirName, noInstall, noBuild, logger } = options;
110
+
111
+ // Replace {{PROJECT_NAME}} in files
112
+ tui.info(`🔧 Setting up ${projectName}...`);
113
+ await replaceInFiles(dest, projectName, dirName);
114
+
115
+ // Install dependencies
116
+ if (!noInstall) {
117
+ const exitCode = await tui.runCommand({
118
+ command: 'bun install',
119
+ cwd: dest,
120
+ cmd: ['bun', 'install'],
121
+ clearOnSuccess: true,
122
+ });
123
+ if (exitCode !== 0) {
124
+ logger.error('Failed to install dependencies');
125
+ }
126
+ }
127
+
128
+ // Build project
129
+ if (!noBuild) {
130
+ const exitCode = await tui.runCommand({
131
+ command: 'bun run build',
132
+ cwd: dest,
133
+ cmd: ['bun', 'run', 'build'],
134
+ clearOnSuccess: true,
135
+ });
136
+ if (exitCode !== 0) {
137
+ logger.error('Failed to build project');
138
+ }
139
+ }
140
+ }
141
+
142
+ async function replaceInFiles(dir: string, projectName: string, dirName: string): Promise<void> {
143
+ const filesToReplace = ['package.json', 'README.md', 'AGENTS.md'];
144
+
145
+ for (const file of filesToReplace) {
146
+ const filePath = join(dir, file);
147
+ const bunFile = Bun.file(filePath);
148
+ if (await bunFile.exists()) {
149
+ let content = await bunFile.text();
150
+ // Replace human-readable name in most places
151
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
152
+ // Replace with directory name for package.json "name" field (npm package name)
153
+ if (file === 'package.json') {
154
+ content = content.replace(/"name":\s*".*?"/, `"name": "${dirName}"`);
155
+ }
156
+ await Bun.write(filePath, content);
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,197 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
3
+ import { cwd } from 'node:process';
4
+ import { homedir } from 'node:os';
5
+ import enquirer from 'enquirer';
6
+ import type { Logger } from '@/logger';
7
+ import * as tui from '@/tui';
8
+ import { playSound } from '@/sound';
9
+ import { fetchTemplates, type TemplateInfo } from './templates';
10
+ import { downloadTemplate, setupProject } from './download';
11
+
12
+ interface CreateFlowOptions {
13
+ projectName?: string;
14
+ dir?: string;
15
+ template?: string;
16
+ templateDir?: string;
17
+ templateBranch?: string;
18
+ noInstall: boolean;
19
+ noBuild: boolean;
20
+ skipPrompts: boolean;
21
+ logger: Logger;
22
+ }
23
+
24
+ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
25
+ const {
26
+ projectName: initialProjectName,
27
+ dir: targetDir,
28
+ template: initialTemplate,
29
+ templateDir,
30
+ templateBranch,
31
+ skipPrompts,
32
+ logger,
33
+ } = options;
34
+
35
+ // Step 1: Fetch available templates
36
+ if (templateDir) {
37
+ tui.info(`📋 Loading templates from local directory: ${templateDir}...\n`);
38
+ } else if (templateBranch) {
39
+ tui.info(`📋 Fetching available templates from branch: ${templateBranch}...\n`);
40
+ } else {
41
+ tui.info('📋 Fetching available templates...\n');
42
+ }
43
+ const templates = await fetchTemplates(templateDir, templateBranch);
44
+
45
+ if (templates.length === 0) {
46
+ logger.fatal('No templates available');
47
+ }
48
+
49
+ // Step 2: Get project name
50
+ let projectName = initialProjectName;
51
+ if (!projectName && !skipPrompts) {
52
+ const response = await enquirer.prompt<{ name: string }>({
53
+ type: 'input',
54
+ name: 'name',
55
+ message: 'What is the name of your project?',
56
+ initial: 'My First Agent',
57
+ validate: (value: string) => {
58
+ if (!value || value.trim().length === 0) {
59
+ return 'Project name is required';
60
+ }
61
+ return true;
62
+ },
63
+ });
64
+ projectName = response.name;
65
+ }
66
+ projectName = projectName || 'My First Agent';
67
+
68
+ // Step 3: Generate disk-friendly directory name
69
+ const dirName = projectName === '.' ? '.' : sanitizeDirectoryName(projectName);
70
+
71
+ // Step 4: Determine destination directory
72
+ // Expand ~ to home directory
73
+ let expandedTargetDir = targetDir;
74
+ if (expandedTargetDir && expandedTargetDir.startsWith('~')) {
75
+ expandedTargetDir = expandedTargetDir.replace(/^~/, homedir());
76
+ }
77
+ const baseDir = expandedTargetDir ? resolve(expandedTargetDir) : process.cwd();
78
+ const dest = dirName === '.' ? baseDir : resolve(baseDir, dirName);
79
+ const destExists = existsSync(dest);
80
+ const destEmpty = destExists ? readdirSync(dest).length === 0 : true;
81
+
82
+ if (destExists && !destEmpty && dirName !== '.') {
83
+ // In TTY mode, ask if they want to overwrite
84
+ if (process.stdin.isTTY && !skipPrompts) {
85
+ tui.warning(`Directory ${dest} already exists and is not empty.`, true);
86
+ const response = await enquirer.prompt<{ overwrite: boolean }>({
87
+ type: 'confirm',
88
+ name: 'overwrite',
89
+ message: 'Delete and overwrite the directory?',
90
+ initial: false,
91
+ });
92
+
93
+ if (!response.overwrite) {
94
+ tui.info('Operation cancelled');
95
+ process.exit(0);
96
+ }
97
+
98
+ // Extra safety: refuse to delete root or home directories
99
+ const home = homedir();
100
+ if (dest === '/' || dest === home) {
101
+ logger.fatal(`Refusing to delete protected path: ${dest}`);
102
+ return;
103
+ }
104
+ rmSync(dest, { recursive: true, force: true });
105
+ tui.success(`Deleted ${dest}\n`);
106
+ } else {
107
+ logger.fatal(`Directory ${dest} already exists and is not empty.`, true);
108
+ }
109
+ }
110
+
111
+ // Show directory and name confirmation
112
+ if (!skipPrompts) {
113
+ tui.info(`📁 Project: ${tui.bold(projectName)}`);
114
+ tui.info(`📂 Directory: ${tui.bold(dest)}\n`);
115
+ }
116
+
117
+ // Step 5: Select template
118
+ let selectedTemplate: TemplateInfo;
119
+ if (initialTemplate) {
120
+ const found = templates.find((t) => t.id === initialTemplate);
121
+ if (!found) {
122
+ logger.fatal(`Template "${initialTemplate}" not found`);
123
+ return;
124
+ }
125
+ selectedTemplate = found;
126
+ } else if (skipPrompts) {
127
+ selectedTemplate = templates[0];
128
+ } else {
129
+ const response = await enquirer.prompt<{ template: string }>({
130
+ type: 'select',
131
+ name: 'template',
132
+ message: 'Select a template:',
133
+ choices: templates.map((t) => ({
134
+ name: t.id,
135
+ message: `${t.name.padEnd(15, ' ')} ${tui.muted(t.description)}`,
136
+ })),
137
+ });
138
+ const found = templates.find((t) => t.id === response.template);
139
+ if (!found) {
140
+ logger.fatal('Template selection failed');
141
+ return;
142
+ }
143
+ selectedTemplate = found;
144
+ }
145
+
146
+ tui.info(`✨ Using template: ${tui.bold(selectedTemplate.name)}`);
147
+
148
+ // Step 6: Download template
149
+ await downloadTemplate({
150
+ dest,
151
+ template: selectedTemplate,
152
+ templateDir,
153
+ templateBranch,
154
+ });
155
+
156
+ // Step 7: Setup project (replace placeholders, install deps, build)
157
+ await setupProject({
158
+ dest,
159
+ projectName: projectName === '.' ? basename(dest) : projectName,
160
+ dirName: dirName === '.' ? basename(dest) : dirName,
161
+ noInstall: options.noInstall,
162
+ noBuild: options.noBuild,
163
+ logger,
164
+ });
165
+
166
+ // Step 8: Show completion message
167
+ tui.success('✨ Project created successfully!\n');
168
+ tui.info('Next steps:');
169
+ if (dirName !== '.') {
170
+ const dirDisplay = cwd() == targetDir ? basename(dirName) : dest;
171
+ tui.newline();
172
+ console.log(` 1. ${tui.bold(`cd ${dirDisplay}`)}`);
173
+ console.log(` 2. ${tui.bold('bun run dev')}`);
174
+ } else {
175
+ console.log(` ${tui.bold('bun run dev')}`);
176
+ }
177
+ playSound();
178
+ }
179
+
180
+ /**
181
+ * Sanitize a project name to create a safe directory/package name
182
+ * - Converts to lowercase
183
+ * - Replaces spaces and underscores with hyphens
184
+ * - Removes unsafe characters
185
+ * - Ensures it starts with a letter or number
186
+ */
187
+ function sanitizeDirectoryName(name: string): string {
188
+ return name
189
+ .toLowerCase()
190
+ .trim()
191
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
192
+ .replace(/_+/g, '-') // Replace underscores with hyphens
193
+ .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric except hyphens
194
+ .replace(/-+/g, '-') // Collapse multiple hyphens
195
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
196
+ .replace(/^[^a-z0-9]+/, ''); // Remove leading non-alphanumeric
197
+ }
@@ -0,0 +1,48 @@
1
+ const GITHUB_REPO = 'agentuity/sdk';
2
+ const GITHUB_BRANCH = 'main';
3
+ const TEMPLATES_JSON_PATH = 'templates/templates.json';
4
+
5
+ export interface TemplateInfo {
6
+ id: string;
7
+ name: string;
8
+ description: string;
9
+ directory: string;
10
+ }
11
+
12
+ interface TemplatesManifest {
13
+ templates: TemplateInfo[];
14
+ }
15
+
16
+ export async function fetchTemplates(localDir?: string, branch?: string): Promise<TemplateInfo[]> {
17
+ // Load from local directory if provided
18
+ if (localDir) {
19
+ const { join } = await import('node:path');
20
+ const { resolve } = await import('node:path');
21
+ const manifestPath = resolve(join(localDir, 'templates.json'));
22
+ const file = Bun.file(manifestPath);
23
+
24
+ if (!(await file.exists())) {
25
+ throw new Error(`templates.json not found at ${manifestPath}`);
26
+ }
27
+
28
+ const manifest = (await file.json()) as TemplatesManifest;
29
+ return manifest.templates;
30
+ }
31
+
32
+ // Fetch from GitHub
33
+ const branchToUse = branch || GITHUB_BRANCH;
34
+ const url = `https://raw.githubusercontent.com/${GITHUB_REPO}/${branchToUse}/${TEMPLATES_JSON_PATH}`;
35
+
36
+ const headers: Record<string, string> = {};
37
+ if (process.env.GITHUB_TOKEN) {
38
+ headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
39
+ }
40
+
41
+ const response = await fetch(url, { headers });
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to fetch templates: ${response.statusText}`);
44
+ }
45
+
46
+ const manifest = (await response.json()) as TemplatesManifest;
47
+ return manifest.templates;
48
+ }
@@ -0,0 +1,101 @@
1
+ import { Transform, Readable } from 'node:stream';
2
+ import * as tui from './tui';
3
+
4
+ export interface DownloadOptions {
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ message?: string;
8
+ onProgress?: (percent: number, downloadedBytes: number, totalBytes: number) => void;
9
+ }
10
+
11
+ /**
12
+ * Download a file with progress tracking
13
+ * Returns the response body stream for further processing
14
+ */
15
+ export async function downloadWithProgress(
16
+ options: DownloadOptions
17
+ ): Promise<NodeJS.ReadableStream> {
18
+ const { url, headers = {}, onProgress } = options;
19
+
20
+ // Add GITHUB_TOKEN if available and not already set
21
+ const requestHeaders = { ...headers };
22
+ if (process.env.GITHUB_TOKEN && !requestHeaders['Authorization']) {
23
+ requestHeaders['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
24
+ }
25
+
26
+ const response = await fetch(url, { headers: requestHeaders });
27
+ if (!response.ok) {
28
+ throw new Error(`Download failed: ${response.statusText}`);
29
+ }
30
+
31
+ const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
32
+ let downloadedBytes = 0;
33
+
34
+ // Create a transform stream that tracks progress
35
+ const progressStream = new Transform({
36
+ transform(chunk, _encoding, callback) {
37
+ downloadedBytes += chunk.length;
38
+
39
+ if (contentLength > 0) {
40
+ const percent = Math.min(100, Math.floor((downloadedBytes / contentLength) * 100));
41
+ if (onProgress) {
42
+ onProgress(percent, downloadedBytes, contentLength);
43
+ }
44
+ }
45
+
46
+ callback(null, chunk);
47
+ },
48
+ });
49
+
50
+ // Pipe the response through the progress tracker
51
+ const responseStream = Readable.fromWeb(response.body as unknown as ReadableStream);
52
+ responseStream.pipe(progressStream);
53
+
54
+ return progressStream;
55
+ }
56
+
57
+ /**
58
+ * Download a file with a TUI spinner showing progress
59
+ */
60
+ export async function downloadWithSpinner<T>(
61
+ options: DownloadOptions,
62
+ processor: (stream: NodeJS.ReadableStream) => Promise<T>
63
+ ): Promise<T> {
64
+ const { message = 'Downloading...' } = options;
65
+
66
+ return await tui.spinner({
67
+ type: 'progress',
68
+ message,
69
+ callback: async (updateProgress) => {
70
+ const stream = await downloadWithProgress({
71
+ ...options,
72
+ onProgress: (percent) => updateProgress(percent),
73
+ });
74
+
75
+ const result = await processor(stream);
76
+
77
+ // Ensure we show 100% at the end
78
+ updateProgress(100);
79
+
80
+ return result;
81
+ },
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Download a GitHub tarball with progress tracking
87
+ */
88
+ export interface DownloadGitHubOptions {
89
+ repo: string;
90
+ branch?: string;
91
+ message?: string;
92
+ }
93
+
94
+ export async function downloadGitHubTarball(
95
+ options: DownloadGitHubOptions
96
+ ): Promise<NodeJS.ReadableStream> {
97
+ const { repo, branch = 'main', message = 'Downloading from GitHub...' } = options;
98
+ const url = `https://codeload.github.com/${repo}/tar.gz/${branch}`;
99
+
100
+ return await downloadWithSpinner({ url, message }, async (stream) => stream);
101
+ }
package/src/index.ts CHANGED
@@ -24,6 +24,13 @@ export { getCommandPrefix, getCommand } from './command-prefix';
24
24
  export * as tui from './tui';
25
25
  export { runSteps, setStepsColorScheme, stepSuccess, stepSkipped, stepError } from './steps';
26
26
  export { playSound } from './sound';
27
+ export {
28
+ downloadWithProgress,
29
+ downloadWithSpinner,
30
+ downloadGitHubTarball,
31
+ type DownloadOptions as DownloadOptionsType,
32
+ type DownloadGitHubOptions,
33
+ } from './download';
27
34
  export type {
28
35
  Config,
29
36
  LogLevel,
package/src/sound.ts CHANGED
@@ -1,25 +1,39 @@
1
- import { $ } from 'bun';
1
+ import { join } from 'node:path';
2
2
 
3
- export async function playSound(): Promise<void> {
3
+ export function playSound(): void {
4
4
  const platform = process.platform;
5
5
 
6
- let result;
6
+ let command: string[];
7
7
  switch (platform) {
8
- case 'darwin':
9
- result = await $`afplay /System/Library/Sounds/Glass.aiff`.quiet().nothrow();
8
+ case 'darwin': {
9
+ const items = [
10
+ 'Blow.aiff',
11
+ 'Bottle.aiff',
12
+ 'Frog.aiff',
13
+ 'Funk.aiff',
14
+ 'Glass.aiff',
15
+ 'Hero.aiff',
16
+ 'Morse.aiff',
17
+ 'Ping.aiff',
18
+ 'Pop.aiff',
19
+ 'Purr.aiff',
20
+ 'Sosumi.aiff',
21
+ ] as const;
22
+ const file = items[Math.floor(Math.random() * items.length)];
23
+ command = ['afplay', join('/System/Library/Sounds', file)];
10
24
  break;
25
+ }
11
26
  case 'linux':
12
- result = await $`paplay /usr/share/sounds/freedesktop/stereo/complete.oga`
13
- .quiet()
14
- .nothrow();
27
+ command = ['paplay', '/usr/share/sounds/freedesktop/stereo/complete.oga'];
15
28
  break;
16
29
  case 'win32':
17
- result = await $`rundll32 user32.dll,MessageBeep 0x00000040`.quiet().nothrow();
30
+ command = ['rundll32', 'user32.dll,MessageBeep', '0x00000040'];
18
31
  break;
32
+ default:
33
+ return;
19
34
  }
20
35
 
21
- // Fallback to terminal bell if command failed or platform unsupported
22
- if (!result || result.exitCode !== 0) {
23
- process.stdout.write('\u0007');
24
- }
36
+ Bun.spawn(command, {
37
+ stdio: ['ignore', 'ignore', 'ignore'],
38
+ }).unref();
25
39
  }
package/src/tui.ts CHANGED
@@ -37,7 +37,7 @@ const COLORS = {
37
37
  },
38
38
  muted: {
39
39
  light: Bun.color('#808080', 'ansi') || '\x1b[90m', // gray
40
- dark: Bun.color('#DDDDDD', 'ansi') || '\x1b[37m', // light gray
40
+ dark: Bun.color('#888888', 'ansi') || '\x1b[90m', // darker gray
41
41
  },
42
42
  bold: {
43
43
  light: '\x1b[1m',
@@ -85,8 +85,8 @@ export function error(message: string): void {
85
85
  /**
86
86
  * Print a warning message with a yellow warning icon
87
87
  */
88
- export function warning(message: string): void {
89
- const color = getColor('warning');
88
+ export function warning(message: string, asError = false): void {
89
+ const color = asError ? getColor('error') : getColor('warning');
90
90
  const reset = COLORS.reset;
91
91
  console.log(`${color}${ICONS.warning} ${message}${reset}`);
92
92
  }
@@ -653,6 +653,11 @@ export interface CommandRunnerOptions {
653
653
  * Environment variables
654
654
  */
655
655
  env?: Record<string, string>;
656
+ /**
657
+ * If true, clear output on success and only show command + success icon
658
+ * Defaults to false
659
+ */
660
+ clearOnSuccess?: boolean;
656
661
  }
657
662
 
658
663
  /**
@@ -666,7 +671,7 @@ export interface CommandRunnerOptions {
666
671
  * Shows the last 3 lines of output as it streams.
667
672
  */
668
673
  export async function runCommand(options: CommandRunnerOptions): Promise<number> {
669
- const { command, cmd, cwd, env } = options;
674
+ const { command, cmd, cwd, env, clearOnSuccess = false } = options;
670
675
  const isTTY = process.stdout.isTTY;
671
676
 
672
677
  // If not a TTY, just run the command normally and log output
@@ -785,26 +790,41 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
785
790
  // Wait for process to exit
786
791
  const exitCode = await proc.exited;
787
792
 
788
- // Determine how many lines to show in final output
789
- const finalLinesToShow = exitCode === 0 ? 3 : 10;
790
-
791
793
  // Move cursor up to redraw final state
792
794
  if (linesRendered > 0) {
793
795
  process.stdout.write(`\x1b[${linesRendered}A`);
794
796
  }
795
797
 
796
- // Show final status with appropriate color
797
- const statusColor = exitCode === 0 ? green : red;
798
- process.stdout.write(`\r\x1b[K${statusColor}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
798
+ // Clear all lines if clearOnSuccess is true and command succeeded
799
+ if (clearOnSuccess && exitCode === 0) {
800
+ // Clear all rendered lines
801
+ for (let i = 0; i < linesRendered; i++) {
802
+ process.stdout.write('\r\x1b[K\n');
803
+ }
804
+ // Move cursor back up
805
+ process.stdout.write(`\x1b[${linesRendered}A`);
799
806
 
800
- // Show final output lines
801
- const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
802
- for (const line of finalOutputLines) {
803
- let displayLine = line;
804
- if (getDisplayWidth(displayLine) > maxLineWidth) {
805
- displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
807
+ // Show compact success: ✓ command
808
+ process.stdout.write(
809
+ `\r\x1b[K${green}${ICONS.success}${reset} ${cmdColor}${displayCmd}${reset}\n`
810
+ );
811
+ } else {
812
+ // Determine how many lines to show in final output
813
+ const finalLinesToShow = exitCode === 0 ? 3 : 10;
814
+
815
+ // Show final status with appropriate color
816
+ const statusColor = exitCode === 0 ? green : red;
817
+ process.stdout.write(`\r\x1b[K${statusColor}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
818
+
819
+ // Show final output lines
820
+ const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
821
+ for (const line of finalOutputLines) {
822
+ let displayLine = line;
823
+ if (getDisplayWidth(displayLine) > maxLineWidth) {
824
+ displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
825
+ }
826
+ process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
806
827
  }
807
- process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
808
828
  }
809
829
 
810
830
  return exitCode;