@agentuity/cli 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +43 -2
- package/dist/api.d.ts +5 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/api.d.ts +7 -0
- package/dist/cmd/auth/api.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts +3 -0
- package/dist/cmd/auth/signup.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/example/index.d.ts.map +1 -1
- package/dist/cmd/example/optional-auth.d.ts +3 -0
- package/dist/cmd/example/optional-auth.d.ts.map +1 -0
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +1 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/sound.d.ts +1 -1
- package/dist/sound.d.ts.map +1 -1
- package/dist/tui.d.ts +23 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +29 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +16 -2
- package/src/auth.ts +79 -4
- package/src/cli.ts +51 -1
- package/src/cmd/auth/README.md +37 -3
- package/src/cmd/auth/api.ts +66 -2
- package/src/cmd/auth/index.ts +2 -1
- package/src/cmd/auth/login.ts +11 -3
- package/src/cmd/auth/signup.ts +51 -0
- package/src/cmd/dev/index.ts +135 -50
- package/src/cmd/example/index.ts +2 -0
- package/src/cmd/example/optional-auth.ts +38 -0
- package/src/cmd/example/sound.ts +2 -2
- package/src/cmd/project/create.ts +1 -0
- package/src/cmd/project/download.ts +37 -52
- package/src/cmd/project/template-flow.ts +26 -11
- package/src/config.ts +8 -1
- package/src/download.ts +2 -2
- package/src/index.ts +1 -0
- package/src/sound.ts +27 -13
- package/src/tui.ts +126 -9
- package/src/types.ts +47 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
2
|
import { existsSync, mkdirSync, renameSync, readdirSync, cpSync, rmSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { pipeline } from 'node:stream/promises';
|
|
4
5
|
import { createGunzip } from 'node:zlib';
|
|
5
|
-
import { extract } from 'tar-fs';
|
|
6
|
+
import { extract, type Headers } from 'tar-fs';
|
|
6
7
|
import type { Logger } from '@/logger';
|
|
7
8
|
import * as tui from '@/tui';
|
|
8
9
|
import { downloadWithSpinner } from '@/download';
|
|
@@ -27,22 +28,12 @@ interface SetupOptions {
|
|
|
27
28
|
logger: Logger;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Copy from local directory if provided
|
|
36
|
-
if (templateDir) {
|
|
37
|
-
const { resolve } = await import('node:path');
|
|
38
|
-
const sourceDir = resolve(join(templateDir, template.directory));
|
|
39
|
-
|
|
40
|
-
if (!existsSync(sourceDir)) {
|
|
41
|
-
throw new Error(`Template directory not found: ${sourceDir}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
tui.info(`📦 Copying template from ${sourceDir}...`);
|
|
31
|
+
async function cleanup(sourceDir: string, dest: string) {
|
|
32
|
+
if (!existsSync(sourceDir)) {
|
|
33
|
+
throw new Error(`Template directory not found: ${sourceDir}`);
|
|
34
|
+
}
|
|
45
35
|
|
|
36
|
+
tui.spinner(`📦 Copying template from ${sourceDir}...`, async () => {
|
|
46
37
|
// Copy all files from source to dest
|
|
47
38
|
const files = readdirSync(sourceDir);
|
|
48
39
|
for (const file of files) {
|
|
@@ -54,8 +45,23 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
|
|
|
54
45
|
if (existsSync(gi)) {
|
|
55
46
|
renameSync(gi, join(dest, '.gitignore'));
|
|
56
47
|
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function downloadTemplate(options: DownloadOptions): Promise<void> {
|
|
52
|
+
const { dest, template, templateDir, templateBranch } = options;
|
|
57
53
|
|
|
58
|
-
|
|
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);
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
// Download from GitHub
|
|
@@ -74,65 +80,43 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
|
|
|
74
80
|
},
|
|
75
81
|
async (stream) => {
|
|
76
82
|
// Extract only the template directory from tarball
|
|
83
|
+
const prefix = `sdk-${branch}/${templatePath}/`;
|
|
77
84
|
await pipeline(
|
|
78
85
|
stream,
|
|
79
86
|
createGunzip(),
|
|
80
87
|
extract(tempDir, {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return header;
|
|
86
|
-
}
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
return null as any;
|
|
88
|
+
filter: (name: string) => name.startsWith(prefix),
|
|
89
|
+
map: (header: Headers) => {
|
|
90
|
+
header.name = header.name.substring(prefix.length);
|
|
91
|
+
return header;
|
|
89
92
|
},
|
|
90
93
|
})
|
|
91
94
|
);
|
|
92
95
|
}
|
|
93
96
|
);
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
const files = readdirSync(tempDir);
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
cpSync(join(tempDir, file), join(dest, file), { recursive: true });
|
|
99
|
-
}
|
|
100
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
98
|
+
await cleanup(tempDir, dest);
|
|
101
99
|
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
|
|
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}`);
|
|
106
104
|
}
|
|
105
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
export async function setupProject(options: SetupOptions): Promise<void> {
|
|
110
109
|
const { dest, projectName, dirName, noInstall, noBuild, logger } = options;
|
|
111
110
|
|
|
112
|
-
process.chdir(dest);
|
|
113
|
-
|
|
114
111
|
// Replace {{PROJECT_NAME}} in files
|
|
115
112
|
tui.info(`🔧 Setting up ${projectName}...`);
|
|
116
113
|
await replaceInFiles(dest, projectName, dirName);
|
|
117
114
|
|
|
118
|
-
// Run setup.ts if it exists (legacy)
|
|
119
|
-
if (await Bun.file('./setup.ts').exists()) {
|
|
120
|
-
await tui.spinner({
|
|
121
|
-
message: 'Running setup script...',
|
|
122
|
-
callback: async () => {
|
|
123
|
-
const proc = Bun.spawn(['bun', './setup.ts'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
124
|
-
const exitCode = await proc.exited;
|
|
125
|
-
if (exitCode !== 0) {
|
|
126
|
-
logger.error('Setup script failed');
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
115
|
// Install dependencies
|
|
133
116
|
if (!noInstall) {
|
|
134
117
|
const exitCode = await tui.runCommand({
|
|
135
118
|
command: 'bun install',
|
|
119
|
+
cwd: dest,
|
|
136
120
|
cmd: ['bun', 'install'],
|
|
137
121
|
clearOnSuccess: true,
|
|
138
122
|
});
|
|
@@ -145,6 +129,7 @@ export async function setupProject(options: SetupOptions): Promise<void> {
|
|
|
145
129
|
if (!noBuild) {
|
|
146
130
|
const exitCode = await tui.runCommand({
|
|
147
131
|
command: 'bun run build',
|
|
132
|
+
cwd: dest,
|
|
148
133
|
cmd: ['bun', 'run', 'build'],
|
|
149
134
|
clearOnSuccess: true,
|
|
150
135
|
});
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path';
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { cwd } from 'node:process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
3
5
|
import enquirer from 'enquirer';
|
|
4
6
|
import type { Logger } from '@/logger';
|
|
5
7
|
import * as tui from '@/tui';
|
|
8
|
+
import { playSound } from '@/sound';
|
|
6
9
|
import { fetchTemplates, type TemplateInfo } from './templates';
|
|
7
10
|
import { downloadTemplate, setupProject } from './download';
|
|
8
11
|
|
|
9
12
|
interface CreateFlowOptions {
|
|
10
13
|
projectName?: string;
|
|
14
|
+
dir?: string;
|
|
11
15
|
template?: string;
|
|
12
16
|
templateDir?: string;
|
|
13
17
|
templateBranch?: string;
|
|
@@ -20,6 +24,7 @@ interface CreateFlowOptions {
|
|
|
20
24
|
export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
|
|
21
25
|
const {
|
|
22
26
|
projectName: initialProjectName,
|
|
27
|
+
dir: targetDir,
|
|
23
28
|
template: initialTemplate,
|
|
24
29
|
templateDir,
|
|
25
30
|
templateBranch,
|
|
@@ -64,14 +69,20 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
|
|
|
64
69
|
const dirName = projectName === '.' ? '.' : sanitizeDirectoryName(projectName);
|
|
65
70
|
|
|
66
71
|
// Step 4: Determine destination directory
|
|
67
|
-
|
|
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);
|
|
68
79
|
const destExists = existsSync(dest);
|
|
69
80
|
const destEmpty = destExists ? readdirSync(dest).length === 0 : true;
|
|
70
81
|
|
|
71
82
|
if (destExists && !destEmpty && dirName !== '.') {
|
|
72
83
|
// In TTY mode, ask if they want to overwrite
|
|
73
84
|
if (process.stdin.isTTY && !skipPrompts) {
|
|
74
|
-
tui.warning(`Directory ${
|
|
85
|
+
tui.warning(`Directory ${dest} already exists and is not empty.`, true);
|
|
75
86
|
const response = await enquirer.prompt<{ overwrite: boolean }>({
|
|
76
87
|
type: 'confirm',
|
|
77
88
|
name: 'overwrite',
|
|
@@ -84,19 +95,23 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
|
|
|
84
95
|
process.exit(0);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
//
|
|
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
|
+
}
|
|
88
104
|
rmSync(dest, { recursive: true, force: true });
|
|
89
|
-
tui.success(`Deleted ${
|
|
105
|
+
tui.success(`Deleted ${dest}\n`);
|
|
90
106
|
} else {
|
|
91
|
-
logger.fatal(`Directory ${
|
|
107
|
+
logger.fatal(`Directory ${dest} already exists and is not empty.`, true);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
// Show directory and name confirmation
|
|
96
112
|
if (!skipPrompts) {
|
|
97
|
-
const displayPath = dirName === '.' ? basename(dest) : dirName;
|
|
98
113
|
tui.info(`📁 Project: ${tui.bold(projectName)}`);
|
|
99
|
-
tui.info(`📂 Directory: ${tui.bold(
|
|
114
|
+
tui.info(`📂 Directory: ${tui.bold(dest)}\n`);
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
// Step 5: Select template
|
|
@@ -152,14 +167,14 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
|
|
|
152
167
|
tui.success('✨ Project created successfully!\n');
|
|
153
168
|
tui.info('Next steps:');
|
|
154
169
|
if (dirName !== '.') {
|
|
170
|
+
const dirDisplay = cwd() == targetDir ? basename(dirName) : dest;
|
|
155
171
|
tui.newline();
|
|
156
|
-
console.log(` 1. ${tui.bold(`cd ${
|
|
172
|
+
console.log(` 1. ${tui.bold(`cd ${dirDisplay}`)}`);
|
|
157
173
|
console.log(` 2. ${tui.bold('bun run dev')}`);
|
|
158
174
|
} else {
|
|
159
|
-
console.log(`
|
|
175
|
+
console.log(` ${tui.bold('bun run dev')}`);
|
|
160
176
|
}
|
|
161
|
-
|
|
162
|
-
console.log(`Your agents will be running at ${tui.link('http://localhost:3000')}`);
|
|
177
|
+
playSound();
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
/**
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YAML } from 'bun';
|
|
2
|
-
import { join, extname } from 'node:path';
|
|
2
|
+
import { join, extname, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { mkdir, readdir, readFile, writeFile, chmod } from 'node:fs/promises';
|
|
5
5
|
import type { Config, Profile, AuthData } from './types';
|
|
@@ -104,6 +104,13 @@ export async function loadConfig(customPath?: string): Promise<Config | null> {
|
|
|
104
104
|
const content = await file.text();
|
|
105
105
|
const config = YAML.parse(content);
|
|
106
106
|
|
|
107
|
+
// check to see if this is a legacy config file that might not have the required name
|
|
108
|
+
// and in this case we can just use the filename
|
|
109
|
+
const _config = config as { name?: string };
|
|
110
|
+
if (!_config.name) {
|
|
111
|
+
_config.name = basename(configPath).replace(extname(configPath), '');
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
const result = ConfigSchema.safeParse(config);
|
|
108
115
|
if (!result.success) {
|
|
109
116
|
tui.error(`Invalid config in ${configPath}:`);
|
package/src/download.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transform } from 'node:stream';
|
|
1
|
+
import { Transform, Readable } from 'node:stream';
|
|
2
2
|
import * as tui from './tui';
|
|
3
3
|
|
|
4
4
|
export interface DownloadOptions {
|
|
@@ -48,7 +48,7 @@ export async function downloadWithProgress(
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// Pipe the response through the progress tracker
|
|
51
|
-
const responseStream = response.body as unknown as
|
|
51
|
+
const responseStream = Readable.fromWeb(response.body as unknown as ReadableStream);
|
|
52
52
|
responseStream.pipe(progressStream);
|
|
53
53
|
|
|
54
54
|
return progressStream;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createCLI, registerCommands } from './cli';
|
|
2
2
|
export { validateRuntime, isBun } from './runtime';
|
|
3
3
|
export { getVersion, getRevision, getPackageName, getPackage } from './version';
|
|
4
|
+
export { requireAuth, optionalAuth, withAuth, withOptionalAuth } from './auth';
|
|
4
5
|
export {
|
|
5
6
|
loadConfig,
|
|
6
7
|
saveConfig,
|
package/src/sound.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function playSound(): void {
|
|
4
4
|
const platform = process.platform;
|
|
5
5
|
|
|
6
|
-
let
|
|
6
|
+
let command: string[];
|
|
7
7
|
switch (platform) {
|
|
8
|
-
case 'darwin':
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
.quiet()
|
|
14
|
-
.nothrow();
|
|
27
|
+
command = ['paplay', '/usr/share/sounds/freedesktop/stereo/complete.oga'];
|
|
15
28
|
break;
|
|
16
29
|
case 'win32':
|
|
17
|
-
|
|
30
|
+
command = ['rundll32', 'user32.dll,MessageBeep', '0x00000040'];
|
|
18
31
|
break;
|
|
32
|
+
default:
|
|
33
|
+
return;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
36
|
+
Bun.spawn(command, {
|
|
37
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
38
|
+
}).unref();
|
|
25
39
|
}
|
package/src/tui.ts
CHANGED
|
@@ -82,11 +82,21 @@ export function error(message: string): void {
|
|
|
82
82
|
console.error(`${color}${ICONS.error} ${message}${reset}`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Print an error message with a red X and then exit
|
|
87
|
+
*/
|
|
88
|
+
export function fatal(message: string): never {
|
|
89
|
+
const color = getColor('error');
|
|
90
|
+
const reset = COLORS.reset;
|
|
91
|
+
console.error(`${color}${ICONS.error} ${message}${reset}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
/**
|
|
86
96
|
* Print a warning message with a yellow warning icon
|
|
87
97
|
*/
|
|
88
|
-
export function warning(message: string): void {
|
|
89
|
-
const color = getColor('warning');
|
|
98
|
+
export function warning(message: string, asError = false): void {
|
|
99
|
+
const color = asError ? getColor('error') : getColor('warning');
|
|
90
100
|
const reset = COLORS.reset;
|
|
91
101
|
console.log(`${color}${ICONS.warning} ${message}${reset}`);
|
|
92
102
|
}
|
|
@@ -200,9 +210,13 @@ export function padLeft(str: string, length: number, pad = ' '): string {
|
|
|
200
210
|
* Creates a bordered box around the content
|
|
201
211
|
*
|
|
202
212
|
* Uses Bun.stringWidth() for accurate width calculation with ANSI codes and unicode
|
|
213
|
+
* Responsive to terminal width - adapts to narrow terminals
|
|
203
214
|
*/
|
|
204
215
|
export function banner(title: string, body: string): void {
|
|
205
|
-
|
|
216
|
+
// Get terminal width, default to 80 if not available, minimum 40
|
|
217
|
+
const termWidth = process.stdout.columns || 80;
|
|
218
|
+
const maxWidth = Math.max(40, Math.min(termWidth - 2, 80)); // Between 40 and 80, with 2 char margin
|
|
219
|
+
|
|
206
220
|
const border = {
|
|
207
221
|
topLeft: '╭',
|
|
208
222
|
topRight: '╮',
|
|
@@ -377,6 +391,33 @@ export async function confirm(message: string, defaultValue = true): Promise<boo
|
|
|
377
391
|
});
|
|
378
392
|
}
|
|
379
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Display a signup benefits box with cyan border
|
|
396
|
+
* Shows the value proposition for creating an Agentuity account
|
|
397
|
+
*/
|
|
398
|
+
export function showSignupBenefits(): void {
|
|
399
|
+
const CYAN = Bun.color('cyan', 'ansi-16m');
|
|
400
|
+
const TEXT =
|
|
401
|
+
currentColorScheme === 'dark' ? Bun.color('white', 'ansi') : Bun.color('black', 'ansi');
|
|
402
|
+
const RESET = '\x1b[0m';
|
|
403
|
+
|
|
404
|
+
const lines = [
|
|
405
|
+
'╔════════════════════════════════════════════╗',
|
|
406
|
+
`║ ⨺ Signup for Agentuity ${muted('free')}${CYAN} ║`,
|
|
407
|
+
'║ ║',
|
|
408
|
+
`║ ✓ ${TEXT}Cloud deployment, previews and CI/CD${CYAN} ║`,
|
|
409
|
+
`║ ✓ ${TEXT}AI Gateway, KV, Vector and more${CYAN} ║`,
|
|
410
|
+
`║ ✓ ${TEXT}Observability, Tracing and Logging${CYAN} ║`,
|
|
411
|
+
`║ ✓ ${TEXT}Organization and Team support${CYAN} ║`,
|
|
412
|
+
`║ ✓ ${TEXT}And much more!${CYAN} ║`,
|
|
413
|
+
'╚════════════════════════════════════════════╝',
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
console.log('');
|
|
417
|
+
lines.forEach((line) => console.log(CYAN + line + RESET));
|
|
418
|
+
console.log('');
|
|
419
|
+
}
|
|
420
|
+
|
|
380
421
|
/**
|
|
381
422
|
* Copy text to clipboard
|
|
382
423
|
* Returns true if successful, false otherwise
|
|
@@ -443,9 +484,36 @@ function getDisplayWidth(str: string): number {
|
|
|
443
484
|
return Bun.stringWidth(withoutOSC8);
|
|
444
485
|
}
|
|
445
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Extract ANSI codes from the beginning of a string
|
|
489
|
+
*/
|
|
490
|
+
function extractLeadingAnsiCodes(str: string): string {
|
|
491
|
+
// Match ANSI escape sequences at the start of the string
|
|
492
|
+
// eslint-disable-next-line no-control-regex
|
|
493
|
+
const match = str.match(/^(\x1b\[[0-9;]*m)+/);
|
|
494
|
+
return match ? match[0] : '';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Strip ANSI codes from a string
|
|
499
|
+
*/
|
|
500
|
+
function stripAnsiCodes(str: string): string {
|
|
501
|
+
// Remove all ANSI escape sequences
|
|
502
|
+
// eslint-disable-next-line no-control-regex
|
|
503
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Check if a string ends with ANSI reset code
|
|
508
|
+
*/
|
|
509
|
+
function endsWithReset(str: string): boolean {
|
|
510
|
+
return str.endsWith('\x1b[0m') || str.endsWith(COLORS.reset);
|
|
511
|
+
}
|
|
512
|
+
|
|
446
513
|
/**
|
|
447
514
|
* Wrap text to a maximum width
|
|
448
515
|
* Handles explicit newlines and word wrapping
|
|
516
|
+
* Preserves ANSI color codes across wrapped lines
|
|
449
517
|
*/
|
|
450
518
|
function wrapText(text: string, maxWidth: number): string[] {
|
|
451
519
|
const allLines: string[] = [];
|
|
@@ -460,6 +528,13 @@ function wrapText(text: string, maxWidth: number): string[] {
|
|
|
460
528
|
continue;
|
|
461
529
|
}
|
|
462
530
|
|
|
531
|
+
// Record starting index for this paragraph's lines
|
|
532
|
+
const paragraphStart = allLines.length;
|
|
533
|
+
|
|
534
|
+
// Extract any leading ANSI codes from the paragraph
|
|
535
|
+
const leadingCodes = extractLeadingAnsiCodes(paragraph);
|
|
536
|
+
const hasReset = endsWithReset(paragraph);
|
|
537
|
+
|
|
463
538
|
// Wrap each paragraph
|
|
464
539
|
const words = paragraph.split(' ');
|
|
465
540
|
let currentLine = '';
|
|
@@ -477,13 +552,30 @@ function wrapText(text: string, maxWidth: number): string[] {
|
|
|
477
552
|
}
|
|
478
553
|
// If the word itself is longer than maxWidth, just use it as is
|
|
479
554
|
// (better to have a long line than break in the middle)
|
|
480
|
-
|
|
555
|
+
// But if we have leading codes and this isn't the first line, apply them
|
|
556
|
+
if (leadingCodes && currentLine) {
|
|
557
|
+
// Strip any existing codes from the word to avoid duplication
|
|
558
|
+
const strippedWord = stripAnsiCodes(word);
|
|
559
|
+
currentLine = leadingCodes + strippedWord;
|
|
560
|
+
} else {
|
|
561
|
+
currentLine = word;
|
|
562
|
+
}
|
|
481
563
|
}
|
|
482
564
|
}
|
|
483
565
|
|
|
484
566
|
if (currentLine) {
|
|
485
567
|
allLines.push(currentLine);
|
|
486
568
|
}
|
|
569
|
+
|
|
570
|
+
// If the original paragraph had ANSI codes and ended with reset,
|
|
571
|
+
// ensure each wrapped line ends with reset (only for this paragraph's lines)
|
|
572
|
+
if (leadingCodes && hasReset) {
|
|
573
|
+
for (let i = paragraphStart; i < allLines.length; i++) {
|
|
574
|
+
if (!endsWithReset(allLines[i])) {
|
|
575
|
+
allLines[i] += COLORS.reset;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
487
579
|
}
|
|
488
580
|
|
|
489
581
|
return allLines.length > 0 ? allLines : [''];
|
|
@@ -658,6 +750,20 @@ export interface CommandRunnerOptions {
|
|
|
658
750
|
* Defaults to false
|
|
659
751
|
*/
|
|
660
752
|
clearOnSuccess?: boolean;
|
|
753
|
+
/**
|
|
754
|
+
* If true or undefined, will truncate each line of output
|
|
755
|
+
*/
|
|
756
|
+
truncate?: boolean;
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* If undefined, will show up to 3 last lines of output while running. Customize the number with this property.
|
|
760
|
+
*/
|
|
761
|
+
maxLinesOutput?: number;
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* If undefined, will show up to 10 last lines on failure. Customize the number with this property.
|
|
765
|
+
*/
|
|
766
|
+
maxLinesOnFailure?: number;
|
|
661
767
|
}
|
|
662
768
|
|
|
663
769
|
/**
|
|
@@ -671,7 +777,16 @@ export interface CommandRunnerOptions {
|
|
|
671
777
|
* Shows the last 3 lines of output as it streams.
|
|
672
778
|
*/
|
|
673
779
|
export async function runCommand(options: CommandRunnerOptions): Promise<number> {
|
|
674
|
-
const {
|
|
780
|
+
const {
|
|
781
|
+
command,
|
|
782
|
+
cmd,
|
|
783
|
+
cwd,
|
|
784
|
+
env,
|
|
785
|
+
clearOnSuccess = false,
|
|
786
|
+
truncate = true,
|
|
787
|
+
maxLinesOutput = 3,
|
|
788
|
+
maxLinesOnFailure = 10,
|
|
789
|
+
} = options;
|
|
675
790
|
const isTTY = process.stdout.isTTY;
|
|
676
791
|
|
|
677
792
|
// If not a TTY, just run the command normally and log output
|
|
@@ -746,7 +861,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
|
|
|
746
861
|
};
|
|
747
862
|
|
|
748
863
|
// Initial display
|
|
749
|
-
renderOutput(
|
|
864
|
+
renderOutput(maxLinesOutput);
|
|
750
865
|
|
|
751
866
|
try {
|
|
752
867
|
// Spawn the command
|
|
@@ -805,10 +920,12 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
|
|
|
805
920
|
process.stdout.write(`\x1b[${linesRendered}A`);
|
|
806
921
|
|
|
807
922
|
// Show compact success: ✓ command
|
|
808
|
-
process.stdout.write(
|
|
923
|
+
process.stdout.write(
|
|
924
|
+
`\r\x1b[K${green}${ICONS.success}${reset} ${cmdColor}${displayCmd}${reset}\n`
|
|
925
|
+
);
|
|
809
926
|
} else {
|
|
810
927
|
// Determine how many lines to show in final output
|
|
811
|
-
const finalLinesToShow = exitCode === 0 ?
|
|
928
|
+
const finalLinesToShow = exitCode === 0 ? maxLinesOutput : maxLinesOnFailure;
|
|
812
929
|
|
|
813
930
|
// Show final status with appropriate color
|
|
814
931
|
const statusColor = exitCode === 0 ? green : red;
|
|
@@ -818,7 +935,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
|
|
|
818
935
|
const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
|
|
819
936
|
for (const line of finalOutputLines) {
|
|
820
937
|
let displayLine = line;
|
|
821
|
-
if (getDisplayWidth(displayLine) > maxLineWidth) {
|
|
938
|
+
if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
|
|
822
939
|
displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
|
|
823
940
|
}
|
|
824
941
|
process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
|
package/src/types.ts
CHANGED
|
@@ -150,6 +150,7 @@ export type CommandContext<
|
|
|
150
150
|
// Helper to create subcommands with proper type inference
|
|
151
151
|
export function createSubcommand<
|
|
152
152
|
TRequiresAuth extends boolean,
|
|
153
|
+
TOptionalAuth extends boolean | string,
|
|
153
154
|
TArgsSchema extends z.ZodType | undefined,
|
|
154
155
|
TOptionsSchema extends z.ZodType | undefined,
|
|
155
156
|
>(definition: {
|
|
@@ -158,6 +159,7 @@ export function createSubcommand<
|
|
|
158
159
|
aliases?: string[];
|
|
159
160
|
toplevel?: boolean;
|
|
160
161
|
requiresAuth?: TRequiresAuth;
|
|
162
|
+
optionalAuth?: TOptionalAuth;
|
|
161
163
|
schema?: TArgsSchema extends z.ZodType
|
|
162
164
|
? TOptionsSchema extends z.ZodType
|
|
163
165
|
? { args: TArgsSchema; options: TOptionsSchema }
|
|
@@ -166,7 +168,15 @@ export function createSubcommand<
|
|
|
166
168
|
? { options: TOptionsSchema }
|
|
167
169
|
: never;
|
|
168
170
|
handler(
|
|
169
|
-
ctx: CommandContext<
|
|
171
|
+
ctx: CommandContext<
|
|
172
|
+
TRequiresAuth extends true
|
|
173
|
+
? true
|
|
174
|
+
: TOptionalAuth extends true | string
|
|
175
|
+
? true | false
|
|
176
|
+
: false,
|
|
177
|
+
TArgsSchema,
|
|
178
|
+
TOptionsSchema
|
|
179
|
+
>
|
|
170
180
|
): void | Promise<void>;
|
|
171
181
|
}): SubcommandDefinition {
|
|
172
182
|
return definition as unknown as SubcommandDefinition;
|
|
@@ -175,6 +185,7 @@ export function createSubcommand<
|
|
|
175
185
|
// Helper to create commands with proper type inference
|
|
176
186
|
export function createCommand<
|
|
177
187
|
TRequiresAuth extends boolean,
|
|
188
|
+
TOptionalAuth extends boolean | string,
|
|
178
189
|
TArgsSchema extends z.ZodType | undefined,
|
|
179
190
|
TOptionsSchema extends z.ZodType | undefined,
|
|
180
191
|
>(definition: {
|
|
@@ -183,6 +194,7 @@ export function createCommand<
|
|
|
183
194
|
aliases?: string[];
|
|
184
195
|
hidden?: boolean;
|
|
185
196
|
requiresAuth?: TRequiresAuth;
|
|
197
|
+
optionalAuth?: TOptionalAuth;
|
|
186
198
|
schema?: TArgsSchema extends z.ZodType
|
|
187
199
|
? TOptionsSchema extends z.ZodType
|
|
188
200
|
? { args: TArgsSchema; options: TOptionsSchema }
|
|
@@ -191,7 +203,15 @@ export function createCommand<
|
|
|
191
203
|
? { options: TOptionsSchema }
|
|
192
204
|
: never;
|
|
193
205
|
handler?(
|
|
194
|
-
ctx: CommandContext<
|
|
206
|
+
ctx: CommandContext<
|
|
207
|
+
TRequiresAuth extends true
|
|
208
|
+
? true
|
|
209
|
+
: TOptionalAuth extends true | string
|
|
210
|
+
? true | false
|
|
211
|
+
: false,
|
|
212
|
+
TArgsSchema,
|
|
213
|
+
TOptionsSchema
|
|
214
|
+
>
|
|
195
215
|
): void | Promise<void>;
|
|
196
216
|
subcommands?: SubcommandDefinition[];
|
|
197
217
|
}): CommandDefinition {
|
|
@@ -206,6 +226,7 @@ export type SubcommandDefinition =
|
|
|
206
226
|
aliases?: string[];
|
|
207
227
|
toplevel?: boolean;
|
|
208
228
|
requiresAuth: true;
|
|
229
|
+
optionalAuth?: false | string;
|
|
209
230
|
schema?: CommandSchemas;
|
|
210
231
|
handler(ctx: CommandContext): void | Promise<void>;
|
|
211
232
|
}
|
|
@@ -215,6 +236,17 @@ export type SubcommandDefinition =
|
|
|
215
236
|
aliases?: string[];
|
|
216
237
|
toplevel?: boolean;
|
|
217
238
|
requiresAuth?: false;
|
|
239
|
+
optionalAuth: true | string;
|
|
240
|
+
schema?: CommandSchemas;
|
|
241
|
+
handler(ctx: CommandContext): void | Promise<void>;
|
|
242
|
+
}
|
|
243
|
+
| {
|
|
244
|
+
name: string;
|
|
245
|
+
description: string;
|
|
246
|
+
aliases?: string[];
|
|
247
|
+
toplevel?: boolean;
|
|
248
|
+
requiresAuth?: false;
|
|
249
|
+
optionalAuth?: false;
|
|
218
250
|
schema?: CommandSchemas;
|
|
219
251
|
handler(ctx: CommandContext): void | Promise<void>;
|
|
220
252
|
};
|
|
@@ -227,6 +259,18 @@ export type CommandDefinition =
|
|
|
227
259
|
aliases?: string[];
|
|
228
260
|
hidden?: boolean;
|
|
229
261
|
requiresAuth: true;
|
|
262
|
+
optionalAuth?: false | string;
|
|
263
|
+
schema?: CommandSchemas;
|
|
264
|
+
handler?(ctx: CommandContext): void | Promise<void>;
|
|
265
|
+
subcommands?: SubcommandDefinition[];
|
|
266
|
+
}
|
|
267
|
+
| {
|
|
268
|
+
name: string;
|
|
269
|
+
description: string;
|
|
270
|
+
aliases?: string[];
|
|
271
|
+
hidden?: boolean;
|
|
272
|
+
requiresAuth?: false;
|
|
273
|
+
optionalAuth: true | string;
|
|
230
274
|
schema?: CommandSchemas;
|
|
231
275
|
handler?(ctx: CommandContext): void | Promise<void>;
|
|
232
276
|
subcommands?: SubcommandDefinition[];
|
|
@@ -237,6 +281,7 @@ export type CommandDefinition =
|
|
|
237
281
|
aliases?: string[];
|
|
238
282
|
hidden?: boolean;
|
|
239
283
|
requiresAuth?: false;
|
|
284
|
+
optionalAuth?: false;
|
|
240
285
|
schema?: CommandSchemas;
|
|
241
286
|
handler?(ctx: CommandContext): void | Promise<void>;
|
|
242
287
|
subcommands?: SubcommandDefinition[];
|