@agentuity/cli 0.0.91 → 0.0.93

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 (53) hide show
  1. package/dist/agents-docs.d.ts +22 -0
  2. package/dist/agents-docs.d.ts.map +1 -0
  3. package/dist/agents-docs.js +36 -0
  4. package/dist/agents-docs.js.map +1 -0
  5. package/dist/banner.d.ts.map +1 -1
  6. package/dist/banner.js +11 -4
  7. package/dist/banner.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +6 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/cmd/cloud/storage/upload.d.ts.map +1 -1
  12. package/dist/cmd/cloud/storage/upload.js +40 -26
  13. package/dist/cmd/cloud/storage/upload.js.map +1 -1
  14. package/dist/cmd/dev/index.d.ts.map +1 -1
  15. package/dist/cmd/dev/index.js +3 -0
  16. package/dist/cmd/dev/index.js.map +1 -1
  17. package/dist/cmd/index.d.ts.map +1 -1
  18. package/dist/cmd/index.js +1 -0
  19. package/dist/cmd/index.js.map +1 -1
  20. package/dist/cmd/project/download.d.ts.map +1 -1
  21. package/dist/cmd/project/download.js +173 -55
  22. package/dist/cmd/project/download.js.map +1 -1
  23. package/dist/cmd/setup/index.d.ts +2 -0
  24. package/dist/cmd/setup/index.d.ts.map +1 -0
  25. package/dist/cmd/setup/index.js +37 -0
  26. package/dist/cmd/setup/index.js.map +1 -0
  27. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  28. package/dist/cmd/upgrade/index.js +6 -1
  29. package/dist/cmd/upgrade/index.js.map +1 -1
  30. package/dist/tui.d.ts +16 -0
  31. package/dist/tui.d.ts.map +1 -1
  32. package/dist/tui.js +3 -3
  33. package/dist/tui.js.map +1 -1
  34. package/dist/version-check.d.ts.map +1 -1
  35. package/dist/version-check.js +6 -1
  36. package/dist/version-check.js.map +1 -1
  37. package/dist/version.d.ts +17 -0
  38. package/dist/version.d.ts.map +1 -1
  39. package/dist/version.js +24 -0
  40. package/dist/version.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/agents-docs.ts +52 -0
  43. package/src/banner.ts +18 -4
  44. package/src/cli.ts +6 -0
  45. package/src/cmd/cloud/storage/upload.ts +43 -29
  46. package/src/cmd/dev/index.ts +5 -1
  47. package/src/cmd/index.ts +1 -0
  48. package/src/cmd/project/download.ts +194 -59
  49. package/src/cmd/setup/index.ts +42 -0
  50. package/src/cmd/upgrade/index.ts +8 -1
  51. package/src/tui.ts +3 -3
  52. package/src/version-check.ts +6 -1
  53. package/src/version.ts +28 -0
@@ -24,10 +24,18 @@ export const uploadSubcommand = createSubcommand({
24
24
  command: `${getCommand('cloud storage put')} my-bucket file.txt --content-type text/plain`,
25
25
  description: 'Upload file with content type',
26
26
  },
27
+ {
28
+ command: `${getCommand('cloud storage upload')} my-bucket file.txt --key custom-name.txt`,
29
+ description: 'Upload file with custom object key',
30
+ },
27
31
  {
28
32
  command: `cat file.txt | ${getCommand('cloud storage upload')} my-bucket -`,
29
33
  description: 'Upload from stdin',
30
34
  },
35
+ {
36
+ command: `cat data.json | ${getCommand('cloud storage upload')} my-bucket - --key data.json`,
37
+ description: 'Upload from stdin with custom key',
38
+ },
31
39
  ],
32
40
  schema: {
33
41
  args: z.object({
@@ -35,6 +43,10 @@ export const uploadSubcommand = createSubcommand({
35
43
  filename: z.string().describe('File path to upload or "-" for STDIN'),
36
44
  }),
37
45
  options: z.object({
46
+ key: z
47
+ .string()
48
+ .optional()
49
+ .describe('Remote object key (defaults to basename or "stdin" for piped uploads)'),
38
50
  contentType: z
39
51
  .string()
40
52
  .optional()
@@ -75,40 +87,38 @@ export const uploadSubcommand = createSubcommand({
75
87
  );
76
88
  }
77
89
 
78
- // TODO: we need to optimize this to not load the entire file in memory!
79
-
80
- // Read file content
81
- let fileContent: Buffer;
82
- let actualFilename: string;
90
+ // Prepare streaming upload - we don't buffer the entire file in memory
91
+ let stream: ReadableStream<Uint8Array>;
83
92
 
84
93
  if (args.filename === '-') {
85
- // Read from STDIN (binary-safe)
86
- try {
87
- const arrayBuffer = await Bun.readableStreamToArrayBuffer(Bun.stdin.stream());
88
- fileContent = Buffer.from(arrayBuffer);
89
- actualFilename = 'stdin';
90
- } catch (error) {
91
- tui.fatal(
92
- `Cannot read from stdin: ${error instanceof Error ? error.message : String(error)}`,
93
- ErrorCode.FILE_NOT_FOUND
94
- );
95
- }
94
+ // Stream from STDIN
95
+ stream = Bun.stdin.stream();
96
96
  } else {
97
- // Read from file
97
+ // Stream from file
98
98
  const file = Bun.file(args.filename);
99
99
  if (!(await file.exists())) {
100
100
  tui.fatal(`File not found: ${args.filename}`, ErrorCode.FILE_NOT_FOUND);
101
101
  }
102
- fileContent = Buffer.from(await file.arrayBuffer());
103
- actualFilename = basename(args.filename);
102
+ stream = file.stream();
104
103
  }
105
104
 
106
- // Auto-detect content type
105
+ // Derive the remote object key:
106
+ // 1. Use --key if provided
107
+ // 2. For stdin (-), default to 'stdin'
108
+ // 3. For files, use the basename
109
+ const objectKey =
110
+ opts.key && opts.key.trim().length > 0
111
+ ? opts.key
112
+ : args.filename === '-'
113
+ ? 'stdin'
114
+ : basename(args.filename);
115
+
116
+ // Auto-detect content type from the object key's extension
117
+ // This allows content-type detection for stdin when --key is provided
107
118
  let contentType = opts.contentType;
108
- if (!contentType && args.filename !== '-') {
109
- const filename = basename(args.filename);
110
- const dotIndex = filename.lastIndexOf('.');
111
- const ext = dotIndex > 0 ? filename.substring(dotIndex + 1).toLowerCase() : undefined;
119
+ if (!contentType) {
120
+ const dotIndex = objectKey.lastIndexOf('.');
121
+ const ext = dotIndex > 0 ? objectKey.substring(dotIndex + 1).toLowerCase() : undefined;
112
122
  const mimeTypes: Record<string, string> = {
113
123
  txt: 'text/plain',
114
124
  html: 'text/html',
@@ -139,11 +149,15 @@ export const uploadSubcommand = createSubcommand({
139
149
  region: bucket.region,
140
150
  });
141
151
 
152
+ // Upload using streaming - wrap the stream in a Response object
153
+ // S3Client.write accepts Response which allows streaming without buffering in memory
154
+ let bytesUploaded = 0;
155
+
142
156
  await tui.spinner({
143
- message: `Uploading ${actualFilename} to ${args.name}`,
157
+ message: `Uploading ${objectKey} to ${args.name}`,
144
158
  clearOnSuccess: true,
145
159
  callback: async () => {
146
- await s3Client.write(actualFilename, fileContent, {
160
+ bytesUploaded = await s3Client.write(objectKey, new Response(stream), {
147
161
  type: contentType,
148
162
  });
149
163
  },
@@ -151,15 +165,15 @@ export const uploadSubcommand = createSubcommand({
151
165
 
152
166
  if (!options.json) {
153
167
  tui.success(
154
- `Uploaded ${tui.bold(actualFilename)} to ${tui.bold(args.name)} (${fileContent.length} bytes)`
168
+ `Uploaded ${tui.bold(objectKey)} to ${tui.bold(args.name)} (${bytesUploaded} bytes)`
155
169
  );
156
170
  }
157
171
 
158
172
  return {
159
173
  success: true,
160
174
  bucket: args.name,
161
- filename: actualFilename,
162
- size: fileContent.length,
175
+ filename: objectKey,
176
+ size: bytesUploaded,
163
177
  };
164
178
  },
165
179
  });
@@ -25,8 +25,9 @@ import { BuildMetadata } from '@agentuity/server';
25
25
  import { getCommand } from '../../command-prefix';
26
26
  import { notifyWorkbenchClients } from '../../utils/workbench-notify';
27
27
  import { getEnvFilePaths, readEnvFile } from '../../env-util';
28
+ import { writeAgentsDocs } from '../../agents-docs';
28
29
 
29
- const shouldDisableInteractive = (interactive?: boolean) => {
30
+ const shouldDisableInteractive= (interactive?: boolean) => {
30
31
  if (!interactive) {
31
32
  return true;
32
33
  }
@@ -102,6 +103,9 @@ export const command = createCommand({
102
103
 
103
104
  await saveProjectDir(rootDir);
104
105
 
106
+ // Regenerate AGENTS.md files if they are missing (e.g., after node_modules reinstall)
107
+ await writeAgentsDocs(rootDir, { onlyIfMissing: true });
108
+
105
109
  let devmode: DevmodeResponse | undefined;
106
110
  let gravityBin: string | undefined;
107
111
  let gravityURL: string | undefined;
package/src/cmd/index.ts CHANGED
@@ -13,6 +13,7 @@ export async function discoverCommands(): Promise<CommandDefinition[]> {
13
13
  import('./profile').then((m) => m.command),
14
14
  import('./project').then((m) => m.command),
15
15
  import('./repl').then((m) => m.command),
16
+ import('./setup').then((m) => m.command),
16
17
  import('./upgrade').then((m) => m.command),
17
18
  import('./version').then((m) => m.command),
18
19
  ]);
@@ -16,13 +16,11 @@ import { extract, type Headers } from 'tar-fs';
16
16
  import { StructuredError, type Logger } from '@agentuity/core';
17
17
  import * as tui from '../../tui';
18
18
  import { downloadWithSpinner } from '../../download';
19
- import { generateLLMPrompt as generateCLIPrompt } from '../ai/prompt/llm';
20
- import { generateLLMPrompt as generateAgentPrompt } from '../ai/prompt/agent';
21
- import { generateLLMPrompt as generateWebPrompt } from '../ai/prompt/web';
22
- import { generateLLMPrompt as generateAPIPrompt } from '../ai/prompt/api';
19
+ import { writeAgentsDocs } from '../../agents-docs';
23
20
  import type { TemplateInfo } from './templates';
24
21
 
25
22
  const GITHUB_BRANCH = 'main';
23
+ const BASE_TEMPLATE_DIR = '_base';
26
24
 
27
25
  interface DownloadOptions {
28
26
  dest: string;
@@ -45,7 +43,78 @@ const TemplateDirectoryNotFoundError = StructuredError('TemplateDirectoryNotFoun
45
43
  directory: string;
46
44
  }>();
47
45
 
48
- async function cleanup(sourceDir: string, dest: string) {
46
+ async function copyTemplateFiles(sourceDir: string, dest: string, skipGitignoreRename = false) {
47
+ if (!existsSync(sourceDir)) {
48
+ return; // Source directory doesn't exist, skip (overlay may be empty)
49
+ }
50
+
51
+ // Copy all files from source to dest (overlay wins on conflicts)
52
+ const files = readdirSync(sourceDir);
53
+ for (const file of files) {
54
+ // Skip package.overlay.json - it's handled separately for merging
55
+ if (file === 'package.overlay.json') {
56
+ continue;
57
+ }
58
+ // Skip .gitkeep files - they're just placeholders for empty directories
59
+ if (file === '.gitkeep') {
60
+ continue;
61
+ }
62
+ cpSync(join(sourceDir, file), join(dest, file), { recursive: true });
63
+ }
64
+
65
+ // Rename gitignore -> .gitignore (only do this once, after all copies)
66
+ if (!skipGitignoreRename) {
67
+ const gi = join(dest, 'gitignore');
68
+ if (existsSync(gi)) {
69
+ renameSync(gi, join(dest, '.gitignore'));
70
+ }
71
+ }
72
+ }
73
+
74
+ async function mergePackageJson(dest: string, overlayDir: string) {
75
+ const basePackagePath = join(dest, 'package.json');
76
+ const overlayPackagePath = join(overlayDir, 'package.overlay.json');
77
+
78
+ // If no overlay package.json exists, nothing to merge
79
+ if (!existsSync(overlayPackagePath)) {
80
+ return;
81
+ }
82
+
83
+ // Read base package.json
84
+ const basePackage = JSON.parse(await Bun.file(basePackagePath).text());
85
+
86
+ // Read overlay package.json
87
+ const overlayPackage = JSON.parse(await Bun.file(overlayPackagePath).text());
88
+
89
+ // Merge dependencies (overlay wins on conflicts)
90
+ if (overlayPackage.dependencies) {
91
+ basePackage.dependencies = {
92
+ ...basePackage.dependencies,
93
+ ...overlayPackage.dependencies,
94
+ };
95
+ }
96
+
97
+ // Merge devDependencies (overlay wins on conflicts)
98
+ if (overlayPackage.devDependencies) {
99
+ basePackage.devDependencies = {
100
+ ...basePackage.devDependencies,
101
+ ...overlayPackage.devDependencies,
102
+ };
103
+ }
104
+
105
+ // Merge scripts (overlay wins on conflicts)
106
+ if (overlayPackage.scripts) {
107
+ basePackage.scripts = {
108
+ ...basePackage.scripts,
109
+ ...overlayPackage.scripts,
110
+ };
111
+ }
112
+
113
+ // Write merged package.json
114
+ await Bun.write(basePackagePath, JSON.stringify(basePackage, null, '\t') + '\n');
115
+ }
116
+
117
+ async function _cleanup(sourceDir: string, dest: string) {
49
118
  if (!existsSync(sourceDir)) {
50
119
  throw new TemplateDirectoryNotFoundError({
51
120
  directory: sourceDir,
@@ -75,28 +144,59 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
75
144
 
76
145
  // Copy from local directory if provided
77
146
  if (templateDir) {
78
- const sourceDir = resolve(join(templateDir, template.directory));
147
+ const baseDir = resolve(join(templateDir, BASE_TEMPLATE_DIR));
148
+ const overlayDir = resolve(join(templateDir, template.directory));
79
149
 
80
- if (!existsSync(sourceDir)) {
150
+ // Base template must exist
151
+ if (!existsSync(baseDir)) {
81
152
  throw new TemplateDirectoryNotFoundError({
82
- directory: sourceDir,
83
- message: `Template directory not found: ${sourceDir}`,
153
+ directory: baseDir,
154
+ message: `Base template directory not found: ${baseDir}`,
84
155
  });
85
156
  }
86
157
 
87
- return cleanup(sourceDir, dest);
158
+ // Overlay directory must exist (even if empty)
159
+ if (!existsSync(overlayDir)) {
160
+ throw new TemplateDirectoryNotFoundError({
161
+ directory: overlayDir,
162
+ message: `Template directory not found: ${overlayDir}`,
163
+ });
164
+ }
165
+
166
+ await tui.spinner({
167
+ type: 'progress',
168
+ message: '📦 Copying template files...',
169
+ clearOnSuccess: true,
170
+ callback: async (progress) => {
171
+ // Step 1: Copy base template files (skip gitignore rename for now)
172
+ await copyTemplateFiles(baseDir, dest, true);
173
+ progress(33);
174
+
175
+ // Step 2: Copy overlay template files (overlay wins on conflicts)
176
+ await copyTemplateFiles(overlayDir, dest, false);
177
+ progress(66);
178
+
179
+ // Step 3: Merge package.json with overlay dependencies
180
+ await mergePackageJson(dest, overlayDir);
181
+ progress(100);
182
+ },
183
+ });
184
+
185
+ return;
88
186
  }
89
187
 
90
188
  // Download from GitHub
91
189
  const branch = templateBranch || GITHUB_BRANCH;
92
- const templatePath = `templates/${template.directory}`;
190
+ const basePath = `templates/${BASE_TEMPLATE_DIR}`;
191
+ const overlayPath = `templates/${template.directory}`;
93
192
  const url = `https://agentuity.sh/template/sdk/${branch}/tar.gz`;
94
193
  const tempDir = mkdtempSync(join(tmpdir(), 'agentuity-'));
95
194
  const tarballPath = join(tempDir, 'download.tar.gz');
96
195
 
97
196
  logger.debug('[download] URL: %s', url);
98
197
  logger.debug('[download] Branch: %s', branch);
99
- logger.debug('[download] Template path: %s', templatePath);
198
+ logger.debug('[download] Base path: %s', basePath);
199
+ logger.debug('[download] Overlay path: %s', overlayPath);
100
200
  logger.debug('[download] Temp dir: %s', tempDir);
101
201
 
102
202
  try {
@@ -124,37 +224,53 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
124
224
  }
125
225
  );
126
226
 
127
- // Step 2: Extract tarball
128
- // We extract only the files within the template directory
227
+ // Step 2: Extract tarball - extract both base and overlay templates
129
228
  // The tarball structure is: sdk-{branch}/templates/{template.directory}/...
130
- const extractDir = join(tempDir, 'extract');
131
- mkdirSync(extractDir, { recursive: true });
132
-
133
- const prefix = `sdk-${branch}/${templatePath}/`;
134
- logger.debug('[extract] Extract dir: %s', extractDir);
135
- logger.debug('[extract] Filter prefix: %s', prefix);
229
+ const baseExtractDir = join(tempDir, 'base');
230
+ const overlayExtractDir = join(tempDir, 'overlay');
231
+ mkdirSync(baseExtractDir, { recursive: true });
232
+ mkdirSync(overlayExtractDir, { recursive: true });
233
+
234
+ const basePrefix = `sdk-${branch}/${basePath}/`;
235
+ const overlayPrefix = `sdk-${branch}/${overlayPath}/`;
236
+ logger.debug('[extract] Base extract dir: %s', baseExtractDir);
237
+ logger.debug('[extract] Overlay extract dir: %s', overlayExtractDir);
238
+ logger.debug('[extract] Base prefix: %s', basePrefix);
239
+ logger.debug('[extract] Overlay prefix: %s', overlayPrefix);
136
240
 
137
241
  // Track extraction stats for debugging
138
242
  let ignoredCount = 0;
139
- let extractedCount = 0;
243
+ let baseExtractedCount = 0;
244
+ let overlayExtractedCount = 0;
140
245
 
141
246
  // Track which entries we've mapped so we don't ignore them later
142
247
  // Note: tar-fs calls map BEFORE ignore (despite what docs say)
143
248
  const mappedEntries = new Set<string>();
144
249
 
145
- const extractor = extract(extractDir, {
250
+ const extractor = extract(tempDir, {
146
251
  // map callback: called FIRST, allows modifying the entry before extraction
147
- // We strip the prefix so files are extracted to the root of extractDir
252
+ // We extract base files to baseExtractDir and overlay files to overlayExtractDir
148
253
  map: (header: Headers) => {
149
254
  const originalName = header.name;
150
- if (header.name.startsWith(prefix) && header.name.length > prefix.length) {
151
- // This is a file/dir we want to extract - strip the prefix
152
- header.name = header.name.substring(prefix.length);
153
- mappedEntries.add(header.name); // Track that we mapped this
154
- logger.debug('[extract] MAP: %s -> %s', originalName, header.name);
155
- logger.debug('[extract] EXTRACT: %s', originalName);
156
- extractedCount++;
255
+
256
+ // Check if this is a base template file
257
+ if (header.name.startsWith(basePrefix) && header.name.length > basePrefix.length) {
258
+ header.name = `base/${header.name.substring(basePrefix.length)}`;
259
+ mappedEntries.add(header.name);
260
+ logger.debug('[extract] MAP BASE: %s -> %s', originalName, header.name);
261
+ baseExtractedCount++;
262
+ }
263
+ // Check if this is an overlay template file
264
+ else if (
265
+ header.name.startsWith(overlayPrefix) &&
266
+ header.name.length > overlayPrefix.length
267
+ ) {
268
+ header.name = `overlay/${header.name.substring(overlayPrefix.length)}`;
269
+ mappedEntries.add(header.name);
270
+ logger.debug('[extract] MAP OVERLAY: %s -> %s', originalName, header.name);
271
+ overlayExtractedCount++;
157
272
  }
273
+
158
274
  return header;
159
275
  },
160
276
  // ignore callback: called AFTER map, receives the MAPPED name
@@ -183,10 +299,28 @@ export async function downloadTemplate(options: DownloadOptions): Promise<void>
183
299
 
184
300
  logger.debug('[extract] Extraction complete');
185
301
  logger.debug('[extract] Ignored entries: %d', ignoredCount);
186
- logger.debug('[extract] Extracted entries: %d', extractedCount);
302
+ logger.debug('[extract] Base extracted entries: %d', baseExtractedCount);
303
+ logger.debug('[extract] Overlay extracted entries: %d', overlayExtractedCount);
187
304
 
188
- // Step 3: Copy extracted files to destination
189
- await cleanup(extractDir, dest);
305
+ // Step 3: Copy base template files, then overlay template files
306
+ await tui.spinner({
307
+ type: 'progress',
308
+ message: '📦 Copying template files...',
309
+ clearOnSuccess: true,
310
+ callback: async (progress) => {
311
+ // Copy base template files (skip gitignore rename for now)
312
+ await copyTemplateFiles(baseExtractDir, dest, true);
313
+ progress(33);
314
+
315
+ // Copy overlay template files (overlay wins on conflicts)
316
+ await copyTemplateFiles(overlayExtractDir, dest, false);
317
+ progress(66);
318
+
319
+ // Merge package.json with overlay dependencies
320
+ await mergePackageJson(dest, overlayExtractDir);
321
+ progress(100);
322
+ },
323
+ });
190
324
  } finally {
191
325
  // Clean up temp directory
192
326
  logger.debug('[cleanup] Removing temp dir: %s', tempDir);
@@ -214,6 +348,30 @@ export async function setupProject(options: SetupOptions): Promise<void> {
214
348
  }
215
349
  }
216
350
 
351
+ // Run optional template setup script if it exists
352
+ // This allows templates to run custom setup logic after bun install
353
+ const setupScriptPath = join(dest, '_setup.ts');
354
+ if (existsSync(setupScriptPath)) {
355
+ try {
356
+ const exitCode = await tui.runCommand({
357
+ command: 'bun _setup.ts',
358
+ cwd: dest,
359
+ cmd: ['bun', '_setup.ts'],
360
+ clearOnSuccess: true,
361
+ });
362
+ if (exitCode !== 0) {
363
+ logger.error('Template setup script failed');
364
+ }
365
+ } finally {
366
+ // Always delete the setup script after running (or attempting to run)
367
+ try {
368
+ rmSync(setupScriptPath);
369
+ } catch {
370
+ // Ignore errors when deleting the setup script
371
+ }
372
+ }
373
+ }
374
+
217
375
  // Build project
218
376
  if (!noBuild) {
219
377
  const exitCode = await tui.runCommand({
@@ -278,32 +436,9 @@ export async function setupProject(options: SetupOptions): Promise<void> {
278
436
  });
279
437
  }
280
438
 
281
- // generate and write the AGENTS.md for the cli
282
- const cliDir = join(dest, 'node_modules', '@agentuity', 'cli');
283
- if (existsSync(cliDir)) {
284
- const agentFile = join(cliDir, 'AGENTS.md');
285
- const prompt = generateCLIPrompt();
286
- await Bun.write(agentFile, prompt);
287
- }
288
-
289
- // generate and write AGENTS.md for each of the main folders
290
- const agentDir = join(dest, 'src', 'agent');
291
- if (existsSync(agentDir)) {
292
- const agentAPIFile = join(agentDir, 'AGENTS.md');
293
- await Bun.write(agentAPIFile, generateAgentPrompt());
294
- }
295
-
296
- const apiDir = join(dest, 'src', 'api');
297
- if (existsSync(apiDir)) {
298
- const agentAPIFile = join(apiDir, 'AGENTS.md');
299
- await Bun.write(agentAPIFile, generateAPIPrompt());
300
- }
301
-
302
- const webDir = join(dest, 'src', 'web');
303
- if (existsSync(webDir)) {
304
- const webFile = join(webDir, 'AGENTS.md');
305
- await Bun.write(webFile, generateWebPrompt());
306
- }
439
+ // Generate and write AGENTS.md files for the CLI and source folders
440
+ // Always overwrite during project setup to ensure fresh content
441
+ await writeAgentsDocs(dest);
307
442
  }
308
443
 
309
444
  async function replaceInFiles(dir: string, projectName: string, dirName: string): Promise<void> {
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { createCommand } from '../../types';
3
+ import { showBanner } from '../../banner';
4
+ import * as tui from '../../tui';
5
+ import { getCommand } from '../../command-prefix';
6
+
7
+ export const command = createCommand({
8
+ name: 'setup',
9
+ description: 'Display first-run setup information (internal use)',
10
+ hidden: true,
11
+ skipUpgradeCheck: true,
12
+ tags: ['read-only', 'fast'],
13
+ optional: { auth: true },
14
+ schema: {
15
+ options: z.object({
16
+ nonInteractive: z.boolean().optional().describe('Run in non-interactive mode'),
17
+ }),
18
+ },
19
+
20
+ async handler(ctx) {
21
+ const { opts, auth } = ctx;
22
+ const _nonInteractive = opts.nonInteractive ?? false;
23
+
24
+ tui.newline();
25
+ showBanner();
26
+ tui.newline();
27
+
28
+ if (!auth?.expires) {
29
+ tui.output(`${tui.muted('To get started, run:')}`);
30
+ tui.newline();
31
+ tui.output(
32
+ `${getCommand('login')} ${tui.muted('Login to an existing account (or signup)')}`
33
+ );
34
+ tui.output(`${getCommand('create')} ${tui.muted('Create a project')}`);
35
+ tui.output(`${getCommand('help')} ${tui.muted('List commands and options')}`);
36
+ } else {
37
+ tui.success('Welcome back! 🙌');
38
+ }
39
+
40
+ return undefined;
41
+ },
42
+ });
@@ -1,5 +1,5 @@
1
1
  import { createCommand } from '../../types';
2
- import { getVersion } from '../../version';
2
+ import { getVersion, getCompareUrl, getReleaseUrl, toTag } from '../../version';
3
3
  import { getCommand } from '../../command-prefix';
4
4
  import { z } from 'zod';
5
5
  import { ErrorCode, createError, exitWithError } from '../../errors';
@@ -301,6 +301,13 @@ export const command = createCommand({
301
301
  tui.info(`Current version: ${tui.muted(currentVersion)}`);
302
302
  tui.info(`Latest version: ${tui.bold(latestVersion)}`);
303
303
  tui.newline();
304
+ if (toTag(currentVersion) !== toTag(latestVersion)) {
305
+ tui.warning(
306
+ `What's changed: ${tui.link(getCompareUrl(currentVersion, latestVersion))}`
307
+ );
308
+ }
309
+ tui.success(`Release notes: ${tui.link(getReleaseUrl(latestVersion))}`);
310
+ tui.newline();
304
311
 
305
312
  const shouldUpgrade = await tui.confirm('Do you want to upgrade?', true);
306
313
 
package/src/tui.ts CHANGED
@@ -307,7 +307,7 @@ export function link(url: string, title?: string, color = getColor('link')): str
307
307
  /**
308
308
  * Check if terminal supports OSC 8 hyperlinks
309
309
  */
310
- function supportsHyperlinks(): boolean {
310
+ export function supportsHyperlinks(): boolean {
311
311
  const term = process.env.TERM || '';
312
312
  const termProgram = process.env.TERM_PROGRAM || '';
313
313
  const wtSession = process.env.WT_SESSION || '';
@@ -361,7 +361,7 @@ export function output(message: string): void {
361
361
  * which causes incorrect alignment. We strip OSC 8 codes first, then use Bun.stringWidth()
362
362
  * to handle regular ANSI codes and unicode characters correctly.
363
363
  */
364
- function getDisplayWidth(str: string): number {
364
+ export function getDisplayWidth(str: string): number {
365
365
  // Remove OSC-8 hyperlink sequences using Unicode escapes (\u001b = ESC, \u0007 = BEL) to satisfy linter
366
366
  // eslint-disable-next-line no-control-regex
367
367
  const withoutOSC8 = str.replace(/\u001b\]8;;[^\u0007]*\u0007/g, '');
@@ -371,7 +371,7 @@ function getDisplayWidth(str: string): number {
371
371
  /**
372
372
  * Strip all ANSI escape sequences from a string
373
373
  */
374
- function stripAnsi(str: string): string {
374
+ export function stripAnsi(str: string): string {
375
375
  // eslint-disable-next-line no-control-regex
376
376
  return str.replace(/\u001b\[[0-9;]*m/g, '').replace(/\u001b\]8;;[^\u0007]*\u0007/g, '');
377
377
  }
@@ -1,6 +1,6 @@
1
1
  import type { Config, Logger, CommandDefinition } from './types';
2
2
  import { isRunningFromExecutable, fetchLatestVersion } from './cmd/upgrade';
3
- import { getVersion } from './version';
3
+ import { getVersion, getCompareUrl, getReleaseUrl, toTag } from './version';
4
4
  import * as tui from './tui';
5
5
  import { saveConfig } from './config';
6
6
  import { $ } from 'bun';
@@ -97,6 +97,11 @@ async function promptUpgrade(currentVersion: string, latestVersion: string): Pro
97
97
  tui.info(`Current version: ${tui.muted(currentVersion)}`);
98
98
  tui.info(`Latest version: ${tui.bold(latestVersion)}`);
99
99
  tui.newline();
100
+ if (toTag(currentVersion) !== toTag(latestVersion)) {
101
+ tui.warning(`What's changed: ${tui.link(getCompareUrl(currentVersion, latestVersion))}`);
102
+ }
103
+ tui.success(`Release notes: ${tui.link(getReleaseUrl(latestVersion))}`);
104
+ tui.newline();
100
105
 
101
106
  return await tui.confirm('Would you like to upgrade now?', true);
102
107
  }
package/src/version.ts CHANGED
@@ -27,3 +27,31 @@ export function getRevision(): string {
27
27
  // Bun provides git SHA via Bun.revision
28
28
  return typeof Bun !== 'undefined' && Bun.revision ? Bun.revision.substring(0, 8) : 'unknown';
29
29
  }
30
+
31
+ const GITHUB_REPO_URL = 'https://github.com/agentuity/sdk';
32
+
33
+ /**
34
+ * Normalize a version string to a Git tag format (with 'v' prefix)
35
+ */
36
+ export function toTag(version: string): string {
37
+ return version.startsWith('v') ? version : `v${version}`;
38
+ }
39
+
40
+ /**
41
+ * Get the GitHub URL for comparing two versions
42
+ * @param fromVersion - The current/old version
43
+ * @param toVersion - The new/target version
44
+ * @returns GitHub compare URL
45
+ */
46
+ export function getCompareUrl(fromVersion: string, toVersion: string): string {
47
+ return `${GITHUB_REPO_URL}/compare/${toTag(fromVersion)}...${toTag(toVersion)}`;
48
+ }
49
+
50
+ /**
51
+ * Get the GitHub URL for a specific release
52
+ * @param version - The version to get the release URL for
53
+ * @returns GitHub release URL
54
+ */
55
+ export function getReleaseUrl(version: string): string {
56
+ return `${GITHUB_REPO_URL}/releases/tag/${toTag(version)}`;
57
+ }