@agentuity/cli 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/AGENTS.md +139 -0
  2. package/README.md +239 -0
  3. package/bin/cli.ts +71 -0
  4. package/dist/api.d.ts +25 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/auth.d.ts +7 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/banner.d.ts +2 -0
  9. package/dist/banner.d.ts.map +1 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cmd/auth/api.d.ts +9 -0
  13. package/dist/cmd/auth/api.d.ts.map +1 -0
  14. package/dist/cmd/auth/index.d.ts +2 -0
  15. package/dist/cmd/auth/index.d.ts.map +1 -0
  16. package/dist/cmd/auth/login.d.ts +3 -0
  17. package/dist/cmd/auth/login.d.ts.map +1 -0
  18. package/dist/cmd/auth/logout.d.ts +3 -0
  19. package/dist/cmd/auth/logout.d.ts.map +1 -0
  20. package/dist/cmd/bundle/ast.d.ts +2 -0
  21. package/dist/cmd/bundle/ast.d.ts.map +1 -0
  22. package/dist/cmd/bundle/bundler.d.ts +6 -0
  23. package/dist/cmd/bundle/bundler.d.ts.map +1 -0
  24. package/dist/cmd/bundle/file.d.ts +2 -0
  25. package/dist/cmd/bundle/file.d.ts.map +1 -0
  26. package/dist/cmd/bundle/index.d.ts +2 -0
  27. package/dist/cmd/bundle/index.d.ts.map +1 -0
  28. package/dist/cmd/bundle/plugin.d.ts +4 -0
  29. package/dist/cmd/bundle/plugin.d.ts.map +1 -0
  30. package/dist/cmd/dev/index.d.ts +2 -0
  31. package/dist/cmd/dev/index.d.ts.map +1 -0
  32. package/dist/cmd/example/create-user.d.ts +2 -0
  33. package/dist/cmd/example/create-user.d.ts.map +1 -0
  34. package/dist/cmd/example/create.d.ts +2 -0
  35. package/dist/cmd/example/create.d.ts.map +1 -0
  36. package/dist/cmd/example/deploy.d.ts +2 -0
  37. package/dist/cmd/example/deploy.d.ts.map +1 -0
  38. package/dist/cmd/example/index.d.ts +2 -0
  39. package/dist/cmd/example/index.d.ts.map +1 -0
  40. package/dist/cmd/example/list.d.ts +2 -0
  41. package/dist/cmd/example/list.d.ts.map +1 -0
  42. package/dist/cmd/example/run-command.d.ts +2 -0
  43. package/dist/cmd/example/run-command.d.ts.map +1 -0
  44. package/dist/cmd/example/sound.d.ts +3 -0
  45. package/dist/cmd/example/sound.d.ts.map +1 -0
  46. package/dist/cmd/example/spinner.d.ts +2 -0
  47. package/dist/cmd/example/spinner.d.ts.map +1 -0
  48. package/dist/cmd/example/steps.d.ts +2 -0
  49. package/dist/cmd/example/steps.d.ts.map +1 -0
  50. package/dist/cmd/example/version.d.ts +2 -0
  51. package/dist/cmd/example/version.d.ts.map +1 -0
  52. package/dist/cmd/index.d.ts +3 -0
  53. package/dist/cmd/index.d.ts.map +1 -0
  54. package/dist/cmd/profile/create.d.ts +2 -0
  55. package/dist/cmd/profile/create.d.ts.map +1 -0
  56. package/dist/cmd/profile/delete.d.ts +2 -0
  57. package/dist/cmd/profile/delete.d.ts.map +1 -0
  58. package/dist/cmd/profile/index.d.ts +2 -0
  59. package/dist/cmd/profile/index.d.ts.map +1 -0
  60. package/dist/cmd/profile/list.d.ts +3 -0
  61. package/dist/cmd/profile/list.d.ts.map +1 -0
  62. package/dist/cmd/profile/show.d.ts +2 -0
  63. package/dist/cmd/profile/show.d.ts.map +1 -0
  64. package/dist/cmd/profile/use.d.ts +2 -0
  65. package/dist/cmd/profile/use.d.ts.map +1 -0
  66. package/dist/cmd/project/create.d.ts +2 -0
  67. package/dist/cmd/project/create.d.ts.map +1 -0
  68. package/dist/cmd/project/delete.d.ts +2 -0
  69. package/dist/cmd/project/delete.d.ts.map +1 -0
  70. package/dist/cmd/project/index.d.ts +2 -0
  71. package/dist/cmd/project/index.d.ts.map +1 -0
  72. package/dist/cmd/project/list.d.ts +2 -0
  73. package/dist/cmd/project/list.d.ts.map +1 -0
  74. package/dist/cmd/project/show.d.ts +2 -0
  75. package/dist/cmd/project/show.d.ts.map +1 -0
  76. package/dist/cmd/version/index.d.ts +2 -0
  77. package/dist/cmd/version/index.d.ts.map +1 -0
  78. package/dist/command-prefix.d.ts +11 -0
  79. package/dist/command-prefix.d.ts.map +1 -0
  80. package/dist/config.d.ts +16 -0
  81. package/dist/config.d.ts.map +1 -0
  82. package/dist/index.d.ts +18 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/legacy-check.d.ts +6 -0
  85. package/dist/legacy-check.d.ts.map +1 -0
  86. package/dist/logger.d.ts +24 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/runtime.d.ts +3 -0
  89. package/dist/runtime.d.ts.map +1 -0
  90. package/dist/schema-parser.d.ts +24 -0
  91. package/dist/schema-parser.d.ts.map +1 -0
  92. package/dist/sound.d.ts +2 -0
  93. package/dist/sound.d.ts.map +1 -0
  94. package/dist/steps.d.ts +59 -0
  95. package/dist/steps.d.ts.map +1 -0
  96. package/dist/terminal.d.ts +3 -0
  97. package/dist/terminal.d.ts.map +1 -0
  98. package/dist/tui.d.ts +156 -0
  99. package/dist/tui.d.ts.map +1 -0
  100. package/dist/types.d.ts +164 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/version.d.ts +10 -0
  103. package/dist/version.d.ts.map +1 -0
  104. package/package.json +46 -0
  105. package/src/api-errors.md +115 -0
  106. package/src/api.ts +186 -0
  107. package/src/auth.ts +91 -0
  108. package/src/banner.ts +23 -0
  109. package/src/cli.ts +198 -0
  110. package/src/cmd/auth/README.md +95 -0
  111. package/src/cmd/auth/api.ts +71 -0
  112. package/src/cmd/auth/index.ts +9 -0
  113. package/src/cmd/auth/login.ts +76 -0
  114. package/src/cmd/auth/logout.ts +14 -0
  115. package/src/cmd/bundle/ast.ts +228 -0
  116. package/src/cmd/bundle/bundler.ts +88 -0
  117. package/src/cmd/bundle/file.ts +16 -0
  118. package/src/cmd/bundle/index.ts +38 -0
  119. package/src/cmd/bundle/plugin.ts +259 -0
  120. package/src/cmd/dev/index.ts +83 -0
  121. package/src/cmd/example/create-user.ts +38 -0
  122. package/src/cmd/example/create.ts +31 -0
  123. package/src/cmd/example/deploy.ts +36 -0
  124. package/src/cmd/example/index.ts +27 -0
  125. package/src/cmd/example/list.ts +32 -0
  126. package/src/cmd/example/run-command.ts +45 -0
  127. package/src/cmd/example/sound.ts +14 -0
  128. package/src/cmd/example/spinner.ts +44 -0
  129. package/src/cmd/example/steps.ts +66 -0
  130. package/src/cmd/example/version.ts +13 -0
  131. package/src/cmd/index.ts +46 -0
  132. package/src/cmd/profile/README.md +80 -0
  133. package/src/cmd/profile/create.ts +57 -0
  134. package/src/cmd/profile/delete.ts +52 -0
  135. package/src/cmd/profile/index.ts +12 -0
  136. package/src/cmd/profile/list.ts +27 -0
  137. package/src/cmd/profile/show.ts +54 -0
  138. package/src/cmd/profile/use.ts +30 -0
  139. package/src/cmd/project/create.ts +247 -0
  140. package/src/cmd/project/delete.ts +13 -0
  141. package/src/cmd/project/index.ts +11 -0
  142. package/src/cmd/project/list.ts +13 -0
  143. package/src/cmd/project/show.ts +12 -0
  144. package/src/cmd/version/index.ts +16 -0
  145. package/src/command-prefix.ts +43 -0
  146. package/src/config.ts +304 -0
  147. package/src/index.ts +40 -0
  148. package/src/legacy-check.ts +127 -0
  149. package/src/logger.ts +235 -0
  150. package/src/runtime.ts +22 -0
  151. package/src/schema-parser.ts +213 -0
  152. package/src/sound.ts +25 -0
  153. package/src/steps.ts +245 -0
  154. package/src/terminal.ts +151 -0
  155. package/src/tui.md +254 -0
  156. package/src/tui.ts +838 -0
  157. package/src/types.ts +243 -0
  158. package/src/version.ts +29 -0
@@ -0,0 +1,247 @@
1
+ import { createSubcommand } from '@/types';
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';
6
+
7
+ export const createProjectSubcommand = createSubcommand({
8
+ name: 'create',
9
+ description: 'Create a new project',
10
+ aliases: ['new'],
11
+ toplevel: true,
12
+ requiresAuth: false,
13
+ schema: {
14
+ options: z.object({
15
+ name: z.string().optional().describe('Project name'),
16
+ dir: z.string().optional().describe('Directory to create the project in'),
17
+ install: z
18
+ .boolean()
19
+ .optional()
20
+ .default(true)
21
+ .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
24
+ .boolean()
25
+ .optional()
26
+ .describe('Internal: called from bun create postinstall'),
27
+ dev: z.boolean().optional().describe('Internal: use local template for testing'),
28
+ }),
29
+ },
30
+
31
+ async handler(ctx) {
32
+ 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
+ }
246
+ },
247
+ });
@@ -0,0 +1,13 @@
1
+ import { createSubcommand } from '@/types';
2
+
3
+ export const deleteSubcommand = createSubcommand({
4
+ name: 'delete',
5
+ description: 'Delete a project',
6
+ aliases: ['rm', 'del'],
7
+ requiresAuth: true,
8
+
9
+ async handler(ctx) {
10
+ const { logger } = ctx;
11
+ logger.info('TODO: Implement project delete functionality');
12
+ },
13
+ });
@@ -0,0 +1,11 @@
1
+ import { createCommand } from '@/types';
2
+ import { createProjectSubcommand } from './create';
3
+ import { listSubcommand } from './list';
4
+ import { deleteSubcommand } from './delete';
5
+ import { showSubcommand } from './show';
6
+
7
+ export const command = createCommand({
8
+ name: 'project',
9
+ description: 'Manage Agentuity projects',
10
+ subcommands: [createProjectSubcommand, listSubcommand, deleteSubcommand, showSubcommand],
11
+ });
@@ -0,0 +1,13 @@
1
+ import { createSubcommand } from '@/types';
2
+
3
+ export const listSubcommand = createSubcommand({
4
+ name: 'list',
5
+ description: 'List all projects',
6
+ aliases: ['ls'],
7
+ requiresAuth: true,
8
+
9
+ async handler(ctx) {
10
+ const { logger } = ctx;
11
+ logger.info('TODO: Implement project list functionality');
12
+ },
13
+ });
@@ -0,0 +1,12 @@
1
+ import { createSubcommand } from '@/types';
2
+
3
+ export const showSubcommand = createSubcommand({
4
+ name: 'show',
5
+ description: 'Show project details',
6
+ requiresAuth: true,
7
+
8
+ async handler(ctx) {
9
+ const { logger } = ctx;
10
+ logger.info('TODO: Implement project show functionality');
11
+ },
12
+ });
@@ -0,0 +1,16 @@
1
+ import { createCommand } from '@/types';
2
+ import { getVersion } from '@/version';
3
+ import { logger } from '@/logger';
4
+
5
+ export const command = createCommand({
6
+ name: 'version',
7
+ description: 'Display version information',
8
+
9
+ async handler() {
10
+ try {
11
+ console.log(getVersion());
12
+ } catch (error) {
13
+ logger.fatal('Failed to retrieve version: %s', error);
14
+ }
15
+ },
16
+ });
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import { getPackageName } from './version';
3
+
4
+ let cachedPrefix: string | null = null;
5
+
6
+ /**
7
+ * Detects how the CLI is being invoked and returns the appropriate command prefix.
8
+ * Returns "agentuity" if installed globally, or "bunx @agentuity/cli" if running via bunx.
9
+ */
10
+ export function getCommandPrefix(): string {
11
+ if (cachedPrefix) {
12
+ return cachedPrefix;
13
+ }
14
+
15
+ // Check if running from a globally installed package
16
+ // When installed globally, the process.argv[1] will be in a bin directory
17
+ const scriptPath = process.argv[1] || '';
18
+ const normalized = path.normalize(scriptPath);
19
+
20
+ // If the script is in node_modules/.bin or a global bin directory, it's likely global
21
+ const isGlobal =
22
+ normalized.includes(`${path.sep}bin${path.sep}`) &&
23
+ !normalized.includes(`${path.sep}node_modules${path.sep}`) &&
24
+ !normalized.includes(path.join('packages', 'cli', 'bin'));
25
+
26
+ if (isGlobal) {
27
+ cachedPrefix = 'agentuity';
28
+ } else {
29
+ // Running locally via bunx or from source
30
+ const pkgName = getPackageName();
31
+ cachedPrefix = `bunx ${pkgName}`;
32
+ }
33
+
34
+ return cachedPrefix;
35
+ }
36
+
37
+ /**
38
+ * Gets a formatted command string with the appropriate prefix.
39
+ * Example: getCommand('auth login') => 'agentuity auth login' or 'bunx @agentuity/cli auth login'
40
+ */
41
+ export function getCommand(command: string): string {
42
+ return `${getCommandPrefix()} ${command}`;
43
+ }
package/src/config.ts ADDED
@@ -0,0 +1,304 @@
1
+ import { YAML } from 'bun';
2
+ import { join, extname } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { mkdir, readdir, readFile, writeFile, chmod } from 'node:fs/promises';
5
+ import type { Config, Profile, AuthData } from './types';
6
+ import { ConfigSchema } from './types';
7
+ import * as tui from '@/tui';
8
+ import { z } from 'zod';
9
+
10
+ export function getDefaultConfigDir(): string {
11
+ return join(homedir(), '.config', 'agentuity');
12
+ }
13
+
14
+ export function getDefaultConfigPath(): string {
15
+ return join(getDefaultConfigDir(), 'config.yaml');
16
+ }
17
+
18
+ export function getProfilePath(): string {
19
+ return join(getDefaultConfigDir(), 'profile');
20
+ }
21
+
22
+ export async function ensureConfigDir(): Promise<void> {
23
+ const dir = getDefaultConfigDir();
24
+ try {
25
+ await mkdir(dir, { recursive: true, mode: 0o700 });
26
+ } catch {
27
+ // Ignore if already exists
28
+ }
29
+ }
30
+
31
+ export async function saveProfile(path: string): Promise<void> {
32
+ await ensureConfigDir();
33
+ await writeFile(getProfilePath(), path, { mode: 0o644 });
34
+ }
35
+
36
+ export async function getProfile(): Promise<string> {
37
+ const profilePath = getProfilePath();
38
+ const defaultConfigPath = getDefaultConfigPath();
39
+
40
+ try {
41
+ const file = Bun.file(profilePath);
42
+ if (await file.exists()) {
43
+ const content = await file.text();
44
+ const savedPath = content.trim();
45
+ const savedFile = Bun.file(savedPath);
46
+ if (await savedFile.exists()) {
47
+ return savedPath;
48
+ }
49
+ }
50
+ } catch {
51
+ // Fall back to default
52
+ }
53
+
54
+ return defaultConfigPath;
55
+ }
56
+
57
+ export async function fetchProfiles(): Promise<Profile[]> {
58
+ const configDir = getDefaultConfigDir();
59
+ const currentConfigPath = await getProfile();
60
+ const profiles: Profile[] = [];
61
+ const nameRegex = /\bname:\s+["']?([\w-_]+)["']?/;
62
+
63
+ try {
64
+ const entries = await readdir(configDir);
65
+
66
+ for (const entry of entries) {
67
+ if (extname(entry) === '.yaml' && !entry.includes('templates/')) {
68
+ const filePath = join(configDir, entry);
69
+
70
+ try {
71
+ const content = await readFile(filePath, 'utf-8');
72
+ const match = nameRegex.exec(content);
73
+
74
+ if (match && match[1]) {
75
+ profiles.push({
76
+ name: match[1],
77
+ filename: filePath,
78
+ selected: filePath === currentConfigPath,
79
+ });
80
+ }
81
+ } catch {
82
+ // Skip files we can't read
83
+ }
84
+ }
85
+ }
86
+ } catch {
87
+ // Directory doesn't exist or can't be read
88
+ }
89
+
90
+ return profiles;
91
+ }
92
+
93
+ export async function loadConfig(customPath?: string): Promise<Config | null> {
94
+ const configPath = customPath || (await getProfile());
95
+
96
+ try {
97
+ const file = Bun.file(configPath);
98
+ const exists = await file.exists();
99
+
100
+ if (!exists) {
101
+ return null;
102
+ }
103
+
104
+ const content = await file.text();
105
+ const config = YAML.parse(content);
106
+
107
+ const result = ConfigSchema.safeParse(config);
108
+ if (!result.success) {
109
+ tui.error(`Invalid config in ${configPath}:`);
110
+ for (const issue of result.error.issues) {
111
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
112
+ tui.bullet(`${path}: ${issue.message}`);
113
+ }
114
+ process.exit(1);
115
+ }
116
+
117
+ return result.data;
118
+ } catch (error) {
119
+ if (error instanceof Error) {
120
+ console.error(`Error loading config from ${configPath}:`, error.message);
121
+ }
122
+ return null;
123
+ }
124
+ }
125
+
126
+ function formatYAML(obj: unknown, indent = 0): string {
127
+ const spaces = ' '.repeat(indent);
128
+ const lines: string[] = [];
129
+
130
+ if (typeof obj !== 'object' || obj === null) {
131
+ return String(obj);
132
+ }
133
+
134
+ for (const [key, value] of Object.entries(obj)) {
135
+ if (value === null || value === undefined) {
136
+ continue;
137
+ }
138
+
139
+ if (typeof value === 'object' && !Array.isArray(value)) {
140
+ lines.push(`${spaces}${key}:`);
141
+ lines.push(formatYAML(value, indent + 1));
142
+ } else if (Array.isArray(value)) {
143
+ lines.push(`${spaces}${key}:`);
144
+ for (const item of value) {
145
+ if (typeof item === 'object') {
146
+ lines.push(`${spaces} -`);
147
+ lines.push(formatYAML(item, indent + 2));
148
+ } else {
149
+ lines.push(`${spaces} - ${item}`);
150
+ }
151
+ }
152
+ } else if (typeof value === 'string') {
153
+ if (value === '') {
154
+ lines.push(`${spaces}${key}: ""`);
155
+ } else if (value.includes(':') || value.includes('#') || value.includes(' ')) {
156
+ lines.push(`${spaces}${key}: "${value}"`);
157
+ } else {
158
+ lines.push(`${spaces}${key}: ${value}`);
159
+ }
160
+ } else {
161
+ lines.push(`${spaces}${key}: ${value}`);
162
+ }
163
+ }
164
+
165
+ return lines.join('\n');
166
+ }
167
+
168
+ export async function saveConfig(config: Config, customPath?: string): Promise<void> {
169
+ const configPath = customPath || (await getProfile());
170
+ await ensureConfigDir();
171
+
172
+ const content = formatYAML(config);
173
+ await writeFile(configPath, content + '\n', { mode: 0o600 });
174
+ // Ensure existing files get correct permissions on upgrade
175
+ await chmod(configPath, 0o600);
176
+ }
177
+
178
+ export async function saveAuth(auth: AuthData): Promise<void> {
179
+ const config = (await loadConfig()) || { name: 'default' };
180
+ config.auth = {
181
+ api_key: auth.apiKey,
182
+ user_id: auth.userId,
183
+ expires: auth.expires.getTime(),
184
+ };
185
+ config.preferences = config.preferences || {};
186
+ (config.preferences as Record<string, unknown>).orgId = '';
187
+ await saveConfig(config);
188
+ }
189
+
190
+ export async function clearAuth(): Promise<void> {
191
+ const config = (await loadConfig()) || { name: 'default' };
192
+ config.auth = {
193
+ api_key: '',
194
+ user_id: '',
195
+ expires: Date.now(),
196
+ };
197
+ config.preferences = config.preferences || {};
198
+ (config.preferences as Record<string, unknown>).orgId = '';
199
+ await saveConfig(config);
200
+ }
201
+
202
+ export async function getAuth(): Promise<AuthData | null> {
203
+ const config = await loadConfig();
204
+ if (!config) return null;
205
+ const auth = config.auth as { api_key?: string; user_id?: string; expires?: number } | undefined;
206
+
207
+ if (!auth || !auth.api_key || !auth.user_id) {
208
+ return null;
209
+ }
210
+
211
+ return {
212
+ apiKey: auth.api_key,
213
+ userId: auth.user_id,
214
+ expires: new Date(auth.expires || 0),
215
+ };
216
+ }
217
+
218
+ function getSchemaDescription(schema: z.ZodTypeAny): string | undefined {
219
+ return (schema as unknown as { description?: string }).description;
220
+ }
221
+
222
+ function getPlaceholderValue(schema: z.ZodTypeAny): string {
223
+ // Unwrap optional to get to the inner type
224
+ let unwrapped = schema;
225
+ if (schema instanceof z.ZodOptional) {
226
+ unwrapped = (schema._def as unknown as { innerType: z.ZodTypeAny }).innerType;
227
+ }
228
+
229
+ // Check the type using constructor name
230
+ const typeName = unwrapped.constructor.name;
231
+
232
+ switch (typeName) {
233
+ case 'ZodString':
234
+ return '""';
235
+ case 'ZodNumber':
236
+ return '0';
237
+ case 'ZodBoolean':
238
+ return 'false';
239
+ default:
240
+ return '""';
241
+ }
242
+ }
243
+
244
+ function generateYAMLTemplate(name: string): string {
245
+ const lines: string[] = [];
246
+
247
+ // Add name (required)
248
+ lines.push(`name: "${name}"`);
249
+ lines.push('');
250
+
251
+ // Get schema shape
252
+ const shape = ConfigSchema.shape;
253
+
254
+ // Only include user-configurable sections
255
+ // Skip: auth (managed by login), devmode (internal), preferences (managed by CLI)
256
+ const userConfigurableSections = ['overrides'];
257
+
258
+ // Process each top-level field
259
+ for (const [key, value] of Object.entries(shape)) {
260
+ if (key === 'name') continue;
261
+ if (!userConfigurableSections.includes(key)) continue;
262
+
263
+ const schema = value as z.ZodTypeAny;
264
+
265
+ // Unwrap optional to get to the inner schema
266
+ let innerSchema = schema;
267
+ if (schema instanceof z.ZodOptional) {
268
+ innerSchema = (schema._def as unknown as { innerType: z.ZodTypeAny }).innerType;
269
+ }
270
+
271
+ const description = getSchemaDescription(schema);
272
+
273
+ // Add section comment
274
+ if (description) {
275
+ lines.push(`# ${description}`);
276
+ }
277
+
278
+ // For object schemas, expand their properties
279
+ if (innerSchema instanceof z.ZodObject) {
280
+ const innerShape = innerSchema.shape;
281
+ lines.push(`# ${key}:`);
282
+
283
+ for (const [subKey, subValue] of Object.entries(innerShape)) {
284
+ const subSchema = subValue as z.ZodTypeAny;
285
+ const subDesc = getSchemaDescription(subSchema);
286
+ const placeholder = getPlaceholderValue(subSchema);
287
+
288
+ if (subDesc) {
289
+ lines.push(`# ${subKey}: ${placeholder} # ${subDesc}`);
290
+ } else {
291
+ lines.push(`# ${subKey}: ${placeholder}`);
292
+ }
293
+ }
294
+ } else {
295
+ const placeholder = getPlaceholderValue(schema);
296
+ lines.push(`# ${key}: ${placeholder}`);
297
+ }
298
+ lines.push('');
299
+ }
300
+
301
+ return lines.join('\n');
302
+ }
303
+
304
+ export { generateYAMLTemplate };
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ export { createCLI, registerCommands } from './cli';
2
+ export { validateRuntime, isBun } from './runtime';
3
+ export { getVersion, getRevision, getPackageName, getPackage } from './version';
4
+ export {
5
+ loadConfig,
6
+ saveConfig,
7
+ getDefaultConfigPath,
8
+ getDefaultConfigDir,
9
+ getProfilePath,
10
+ ensureConfigDir,
11
+ saveProfile,
12
+ getProfile,
13
+ fetchProfiles,
14
+ saveAuth,
15
+ clearAuth,
16
+ getAuth,
17
+ } from './config';
18
+ export { APIClient, getAPIBaseURL, getAppBaseURL, UpgradeRequiredError } from './api';
19
+ export { Logger, logger } from './logger';
20
+ export { showBanner } from './banner';
21
+ export { discoverCommands } from './cmd';
22
+ export { detectColorScheme } from './terminal';
23
+ export { getCommandPrefix, getCommand } from './command-prefix';
24
+ export * as tui from './tui';
25
+ export { runSteps, setStepsColorScheme, stepSuccess, stepSkipped, stepError } from './steps';
26
+ export { playSound } from './sound';
27
+ export type {
28
+ Config,
29
+ LogLevel,
30
+ GlobalOptions,
31
+ CommandContext,
32
+ SubcommandDefinition,
33
+ CommandDefinition,
34
+ Profile,
35
+ AuthData,
36
+ CommandSchemas,
37
+ } from './types';
38
+ export { createSubcommand, createCommand } from './types';
39
+ export type { ColorScheme } from './terminal';
40
+ export type { Step, SimpleStep, ProgressStep, StepOutcome, ProgressCallback } from './steps';