@agentuity/cli 1.0.41 → 1.0.42

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 (80) hide show
  1. package/dist/cmd/build/ast.d.ts.map +1 -1
  2. package/dist/cmd/build/ast.js +3 -3
  3. package/dist/cmd/build/ast.js.map +1 -1
  4. package/dist/cmd/build/typecheck.d.ts.map +1 -1
  5. package/dist/cmd/build/typecheck.js +52 -1
  6. package/dist/cmd/build/typecheck.js.map +1 -1
  7. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/static-renderer.js +22 -8
  9. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  10. package/dist/cmd/cloud/index.d.ts.map +1 -1
  11. package/dist/cmd/cloud/index.js +4 -0
  12. package/dist/cmd/cloud/index.js.map +1 -1
  13. package/dist/cmd/cloud/monitor.d.ts +3 -0
  14. package/dist/cmd/cloud/monitor.d.ts.map +1 -0
  15. package/dist/cmd/cloud/monitor.js +300 -0
  16. package/dist/cmd/cloud/monitor.js.map +1 -0
  17. package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
  18. package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
  19. package/dist/cmd/cloud/oidc/activity.js +54 -0
  20. package/dist/cmd/cloud/oidc/activity.js.map +1 -0
  21. package/dist/cmd/cloud/oidc/create.d.ts +2 -0
  22. package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
  23. package/dist/cmd/cloud/oidc/create.js +201 -0
  24. package/dist/cmd/cloud/oidc/create.js.map +1 -0
  25. package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
  26. package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
  27. package/dist/cmd/cloud/oidc/delete.js +56 -0
  28. package/dist/cmd/cloud/oidc/delete.js.map +1 -0
  29. package/dist/cmd/cloud/oidc/get.d.ts +2 -0
  30. package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
  31. package/dist/cmd/cloud/oidc/get.js +59 -0
  32. package/dist/cmd/cloud/oidc/get.js.map +1 -0
  33. package/dist/cmd/cloud/oidc/index.d.ts +3 -0
  34. package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
  35. package/dist/cmd/cloud/oidc/index.js +32 -0
  36. package/dist/cmd/cloud/oidc/index.js.map +1 -0
  37. package/dist/cmd/cloud/oidc/list.d.ts +2 -0
  38. package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
  39. package/dist/cmd/cloud/oidc/list.js +45 -0
  40. package/dist/cmd/cloud/oidc/list.js.map +1 -0
  41. package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
  42. package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
  43. package/dist/cmd/cloud/oidc/rotate-secret.js +63 -0
  44. package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
  45. package/dist/cmd/cloud/oidc/users.d.ts +2 -0
  46. package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
  47. package/dist/cmd/cloud/oidc/users.js +50 -0
  48. package/dist/cmd/cloud/oidc/users.js.map +1 -0
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js +14 -5
  51. package/dist/config.js.map +1 -1
  52. package/dist/utils/jsonc.d.ts +13 -0
  53. package/dist/utils/jsonc.d.ts.map +1 -0
  54. package/dist/utils/jsonc.js +63 -0
  55. package/dist/utils/jsonc.js.map +1 -0
  56. package/dist/utils/route-migration.d.ts +2 -1
  57. package/dist/utils/route-migration.d.ts.map +1 -1
  58. package/dist/utils/route-migration.js +23 -32
  59. package/dist/utils/route-migration.js.map +1 -1
  60. package/dist/utils/zip.d.ts.map +1 -1
  61. package/dist/utils/zip.js +18 -2
  62. package/dist/utils/zip.js.map +1 -1
  63. package/package.json +6 -7
  64. package/src/cmd/build/ast.ts +6 -3
  65. package/src/cmd/build/typecheck.ts +60 -1
  66. package/src/cmd/build/vite/static-renderer.ts +24 -8
  67. package/src/cmd/cloud/index.ts +4 -0
  68. package/src/cmd/cloud/monitor.ts +375 -0
  69. package/src/cmd/cloud/oidc/activity.ts +61 -0
  70. package/src/cmd/cloud/oidc/create.ts +232 -0
  71. package/src/cmd/cloud/oidc/delete.ts +63 -0
  72. package/src/cmd/cloud/oidc/get.ts +65 -0
  73. package/src/cmd/cloud/oidc/index.ts +35 -0
  74. package/src/cmd/cloud/oidc/list.ts +50 -0
  75. package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
  76. package/src/cmd/cloud/oidc/users.ts +57 -0
  77. package/src/config.ts +16 -5
  78. package/src/utils/jsonc.ts +67 -0
  79. package/src/utils/route-migration.ts +29 -40
  80. package/src/utils/zip.ts +17 -2
@@ -0,0 +1,63 @@
1
+ import { oauthClientDelete, type APIClient } from '@agentuity/core';
2
+ import { z } from 'zod';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import { ErrorCode } from '../../../errors';
5
+ import * as tui from '../../../tui';
6
+ import { createSubcommand } from '../../../types';
7
+
8
+ const OAuthClientDeleteResponseSchema = z.object({
9
+ success: z.boolean().describe('Whether the operation succeeded'),
10
+ id: z.string().describe('OAuth client id that was deleted'),
11
+ });
12
+
13
+ export const deleteSubcommand = createSubcommand({
14
+ name: 'delete',
15
+ aliases: ['del', 'rm'],
16
+ description: 'Delete an OAuth application',
17
+ tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
18
+ idempotent: true,
19
+ examples: [
20
+ { command: getCommand('cloud oidc delete <id>'), description: 'Delete OAuth application' },
21
+ {
22
+ command: getCommand('cloud oidc delete <id> --force'),
23
+ description: 'Delete OAuth application without confirmation',
24
+ },
25
+ ],
26
+ requires: { auth: true, apiClient: true },
27
+ schema: {
28
+ args: z.object({
29
+ id: z.string().describe('the OAuth client id to delete'),
30
+ }),
31
+ options: z.object({
32
+ force: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
33
+ yes: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
34
+ }),
35
+ response: OAuthClientDeleteResponseSchema,
36
+ },
37
+
38
+ async handler(ctx) {
39
+ const { args, opts, apiClient, options } = ctx;
40
+
41
+ const skipConfirm = opts.force || opts.yes;
42
+
43
+ if (!skipConfirm) {
44
+ const confirmed = await tui.confirm(`Delete OAuth application "${args.id}"?`, false);
45
+ if (!confirmed) {
46
+ tui.fatal('Operation cancelled', ErrorCode.USER_CANCELLED);
47
+ }
48
+ }
49
+
50
+ await tui.spinner('Deleting OAuth application', () => {
51
+ return oauthClientDelete(apiClient as APIClient, args.id);
52
+ });
53
+
54
+ if (!options.json) {
55
+ tui.success(`OAuth application '${args.id}' deleted successfully`);
56
+ }
57
+
58
+ return {
59
+ success: true,
60
+ id: args.id,
61
+ };
62
+ },
63
+ });
@@ -0,0 +1,65 @@
1
+ import { oauthClientGet, type APIClient } from '@agentuity/core';
2
+ import { z } from 'zod';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import { ErrorCode } from '../../../errors';
5
+ import * as tui from '../../../tui';
6
+ import { createSubcommand } from '../../../types';
7
+
8
+ export const getSubcommand = createSubcommand({
9
+ name: 'get',
10
+ description: 'Get a specific OAuth application',
11
+ tags: ['read-only', 'fast', 'requires-auth'],
12
+ examples: [
13
+ { command: getCommand('cloud oidc get <id>'), description: 'Get OAuth application details' },
14
+ ],
15
+ requires: { auth: true, apiClient: true },
16
+ idempotent: true,
17
+ schema: {
18
+ args: z.object({
19
+ id: z.string().describe('the OAuth client id'),
20
+ }),
21
+ },
22
+
23
+ async handler(ctx) {
24
+ const { args, apiClient, options } = ctx;
25
+
26
+ let client: Awaited<ReturnType<typeof oauthClientGet>>;
27
+ try {
28
+ client = await tui.spinner('Fetching OAuth application', () => {
29
+ return oauthClientGet(apiClient as APIClient, args.id);
30
+ });
31
+ } catch (error) {
32
+ if (error instanceof Error && error.message.includes('not found')) {
33
+ tui.fatal(`OAuth application '${args.id}' not found`, ErrorCode.RESOURCE_NOT_FOUND);
34
+ }
35
+ throw error;
36
+ }
37
+
38
+ if (!options.json) {
39
+ if (process.stdout.isTTY) {
40
+ tui.newline();
41
+ tui.success('OAuth Application Details:');
42
+ tui.newline();
43
+ }
44
+
45
+ const rows = [
46
+ {
47
+ ID: client.id,
48
+ Name: client.name,
49
+ Description: client.description || '-',
50
+ Type: client.client_type,
51
+ 'Homepage URL': client.homepage_url || '-',
52
+ 'Redirect URIs':
53
+ client.redirect_uris.length > 0 ? client.redirect_uris.join('\n') : '-',
54
+ Scopes: client.scopes.length > 0 ? client.scopes.join(', ') : '-',
55
+ Created: new Date(client.created_at).toLocaleString(),
56
+ Updated: new Date(client.updated_at).toLocaleString(),
57
+ },
58
+ ];
59
+
60
+ tui.table(rows, undefined, { layout: 'vertical' });
61
+ }
62
+
63
+ return client;
64
+ },
65
+ });
@@ -0,0 +1,35 @@
1
+ import { createCommand } from '../../../types';
2
+ import { getCommand } from '../../../command-prefix';
3
+ import { listSubcommand } from './list';
4
+ import { getSubcommand } from './get';
5
+ import { createSubcommand } from './create';
6
+ import { deleteSubcommand } from './delete';
7
+ import { rotateSecretSubcommand } from './rotate-secret';
8
+ import { activitySubcommand } from './activity';
9
+ import { usersSubcommand } from './users';
10
+
11
+ export const command = createCommand({
12
+ name: 'oidc',
13
+ description: 'Manage OAuth applications',
14
+ tags: ['fast', 'requires-auth'],
15
+ examples: [
16
+ { command: getCommand('cloud oidc list'), description: 'List all OAuth applications' },
17
+ {
18
+ command: getCommand(
19
+ 'cloud oidc create --name "My App" --type confidential --redirect-uris "https://example.com/callback"'
20
+ ),
21
+ description: 'Create a new OAuth application',
22
+ },
23
+ ],
24
+ subcommands: [
25
+ createSubcommand,
26
+ listSubcommand,
27
+ getSubcommand,
28
+ deleteSubcommand,
29
+ rotateSecretSubcommand,
30
+ activitySubcommand,
31
+ usersSubcommand,
32
+ ],
33
+ });
34
+
35
+ export default command;
@@ -0,0 +1,50 @@
1
+ import { oauthClientList, type APIClient } from '@agentuity/core';
2
+ import { getCommand } from '../../../command-prefix';
3
+ import * as tui from '../../../tui';
4
+ import { createSubcommand } from '../../../types';
5
+
6
+ export const listSubcommand = createSubcommand({
7
+ name: 'list',
8
+ aliases: ['ls'],
9
+ description: 'List all OAuth applications',
10
+ tags: ['read-only', 'fast', 'requires-auth'],
11
+ examples: [
12
+ { command: getCommand('cloud oidc list'), description: 'List OAuth applications' },
13
+ { command: getCommand('cloud oidc ls'), description: 'List OAuth applications' },
14
+ ],
15
+ requires: { auth: true, apiClient: true },
16
+ idempotent: true,
17
+
18
+ async handler(ctx) {
19
+ const { apiClient, options } = ctx;
20
+
21
+ const clients = await tui.spinner('Fetching OAuth applications', () => {
22
+ return oauthClientList(apiClient as APIClient);
23
+ });
24
+
25
+ if (!options.json) {
26
+ if (clients.length === 0) {
27
+ tui.info('No OAuth applications found');
28
+ } else {
29
+ if (process.stdout.isTTY) {
30
+ tui.newline();
31
+ tui.success(`OAuth Applications (${clients.length}):`);
32
+ tui.newline();
33
+ }
34
+
35
+ const rows = clients.map((client) => ({
36
+ ID: client.id,
37
+ Name: client.name,
38
+ Type: client.client_type,
39
+ Scopes: client.scopes.length,
40
+ Users: client.user_count,
41
+ Created: new Date(client.created_at).toLocaleString(),
42
+ }));
43
+
44
+ tui.table(rows);
45
+ }
46
+ }
47
+
48
+ return clients;
49
+ },
50
+ });
@@ -0,0 +1,77 @@
1
+ import { oauthClientRotateSecret, type APIClient } from '@agentuity/core';
2
+ import { z } from 'zod';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import { ErrorCode } from '../../../errors';
5
+ import * as tui from '../../../tui';
6
+ import { createSubcommand } from '../../../types';
7
+
8
+ const OAuthClientRotateSecretResponseSchema = z.object({
9
+ client_id: z.string(),
10
+ client_secret: z.string(),
11
+ });
12
+
13
+ export const rotateSecretSubcommand = createSubcommand({
14
+ name: 'rotate-secret',
15
+ description: 'Rotate the client secret for an OAuth application',
16
+ tags: ['destructive', 'requires-auth'],
17
+ examples: [
18
+ {
19
+ command: getCommand('cloud oidc rotate-secret <id>'),
20
+ description: 'Rotate OAuth client secret',
21
+ },
22
+ {
23
+ command: getCommand('cloud oidc rotate-secret <id> --force'),
24
+ description: 'Rotate OAuth client secret without confirmation',
25
+ },
26
+ ],
27
+ requires: { auth: true, apiClient: true },
28
+ idempotent: false,
29
+ schema: {
30
+ args: z.object({
31
+ id: z.string().describe('the OAuth client id'),
32
+ }),
33
+ options: z.object({
34
+ force: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
35
+ }),
36
+ response: OAuthClientRotateSecretResponseSchema,
37
+ },
38
+
39
+ async handler(ctx) {
40
+ const { args, opts, apiClient, options } = ctx;
41
+
42
+ if (!opts.force) {
43
+ const confirmed = await tui.confirm(
44
+ `Rotate secret for OAuth application "${args.id}"?`,
45
+ false
46
+ );
47
+ if (!confirmed) {
48
+ tui.fatal('Operation cancelled', ErrorCode.USER_CANCELLED);
49
+ }
50
+ }
51
+
52
+ const result = await tui.spinner('Rotating OAuth client secret', () => {
53
+ return oauthClientRotateSecret(apiClient as APIClient, args.id);
54
+ });
55
+
56
+ if (!options.json) {
57
+ tui.newline();
58
+ tui.success('OAuth client secret rotated successfully!');
59
+ tui.newline();
60
+ tui.warning('Copy the new client secret now. It will only be shown once.');
61
+ tui.newline();
62
+
63
+ tui.table(
64
+ [
65
+ {
66
+ 'Client ID': result.client_id,
67
+ 'Client Secret': result.client_secret,
68
+ },
69
+ ],
70
+ undefined,
71
+ { layout: 'vertical' }
72
+ );
73
+ }
74
+
75
+ return result;
76
+ },
77
+ });
@@ -0,0 +1,57 @@
1
+ import { oauthClientUsers, type APIClient } from '@agentuity/core';
2
+ import { z } from 'zod';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import * as tui from '../../../tui';
5
+ import { createSubcommand } from '../../../types';
6
+
7
+ const OAuthClientUsersResponseSchema = z.array(
8
+ z.object({
9
+ user_id: z.string(),
10
+ scopes: z.array(z.string()),
11
+ created_at: z.string(),
12
+ })
13
+ );
14
+
15
+ export const usersSubcommand = createSubcommand({
16
+ name: 'users',
17
+ description: 'List connected users for an OAuth application',
18
+ tags: ['read-only', 'requires-auth'],
19
+ examples: [
20
+ {
21
+ command: getCommand('cloud oidc users <id>'),
22
+ description: 'List connected users for OAuth application',
23
+ },
24
+ ],
25
+ requires: { auth: true, apiClient: true },
26
+ idempotent: true,
27
+ schema: {
28
+ args: z.object({
29
+ id: z.string().describe('the OAuth client id'),
30
+ }),
31
+ response: OAuthClientUsersResponseSchema,
32
+ },
33
+
34
+ async handler(ctx) {
35
+ const { args, apiClient, options } = ctx;
36
+
37
+ const users = await tui.spinner('Fetching connected OAuth users', () => {
38
+ return oauthClientUsers(apiClient as APIClient, args.id);
39
+ });
40
+
41
+ if (!options.json) {
42
+ if (users.length === 0) {
43
+ tui.info('No connected users found');
44
+ } else {
45
+ const rows = users.map((user) => ({
46
+ user_id: user.user_id,
47
+ scopes: user.scopes.join(', '),
48
+ connected_at: new Date(user.created_at).toLocaleString(),
49
+ }));
50
+
51
+ tui.table(rows);
52
+ }
53
+ }
54
+
55
+ return users;
56
+ },
57
+ });
package/src/config.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  APIClient as ServerAPIClient,
11
11
  } from '@agentuity/server';
12
12
  import { YAML } from 'bun';
13
- import JSON5 from 'json5';
13
+ import { parseJSONC } from './utils/jsonc';
14
14
  import { z } from 'zod';
15
15
  import { clearProfileCache } from './cache';
16
16
  import { getCatalystUrl } from './catalyst';
@@ -265,8 +265,19 @@ function formatYAML(obj: unknown, indent = 0): string {
265
265
  } else if (typeof value === 'string') {
266
266
  if (value === '') {
267
267
  lines.push(`${spaces}${key}: ""`);
268
- } else if (value.includes(':') || value.includes('#') || value.includes(' ')) {
269
- lines.push(`${spaces}${key}: "${value}"`);
268
+ } else if (
269
+ value.includes(':') ||
270
+ value.includes('#') ||
271
+ value.includes(' ') ||
272
+ value.includes('\\')
273
+ ) {
274
+ // Use single quotes to avoid YAML escape-sequence processing.
275
+ // Double-quoted YAML strings interpret backslash sequences (\n, \t, etc.),
276
+ // which breaks Windows paths like C:\Users\... where \U would be invalid.
277
+ // Single-quoted strings treat backslashes literally.
278
+ // Escape any embedded single quotes by doubling them (YAML spec).
279
+ const escaped = value.replace(/'/g, "''");
280
+ lines.push(`${spaces}${key}: '${escaped}'`);
270
281
  } else {
271
282
  lines.push(`${spaces}${key}: ${value}`);
272
283
  }
@@ -602,7 +613,7 @@ export async function loadProjectConfig(
602
613
  throw new ProjectConfigNotFoundException({ message: 'project config not found' });
603
614
  }
604
615
  const text = await file.text();
605
- const parsedConfig = JSON5.parse(text);
616
+ const parsedConfig = parseJSONC(text);
606
617
  const result = ProjectSchema.safeParse(parsedConfig);
607
618
  if (!result.success) {
608
619
  tui.error(`Invalid project config at ${configPath}:`);
@@ -707,7 +718,7 @@ export async function updateProjectConfig(
707
718
  }
708
719
 
709
720
  const text = await file.text();
710
- const existing = JSON5.parse(text);
721
+ const existing = parseJSONC(text) as Record<string, unknown>;
711
722
  const updated = { ...existing, ...updates };
712
723
 
713
724
  const result = ProjectSchema.safeParse(updated);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Parse JSON with Comments (JSONC).
3
+ *
4
+ * Strips single-line (`//`) and block (`/* *​/`) comments as well as trailing
5
+ * commas that appear before `}` or `]`, then delegates to the built-in
6
+ * `JSON.parse`. This covers the comment syntax used by `tsconfig.json` and
7
+ * similar config files without pulling in a full JSON5 parser.
8
+ *
9
+ * String literals are respected — comments and trailing commas inside quoted
10
+ * strings are left untouched.
11
+ */
12
+ export function parseJSONC(text: string): unknown {
13
+ let result = '';
14
+ let i = 0;
15
+ const len = text.length;
16
+
17
+ while (i < len) {
18
+ const ch = text[i];
19
+
20
+ // --- quoted string: copy verbatim, including any escape sequences ---
21
+ if (ch === '"') {
22
+ const start = i;
23
+ i++; // skip opening quote
24
+ while (i < len) {
25
+ if (text[i] === '\\') {
26
+ i += i + 1 < len ? 2 : 1; // skip escaped character (guard end-of-input)
27
+ } else if (text[i] === '"') {
28
+ i++; // skip closing quote
29
+ break;
30
+ } else {
31
+ i++;
32
+ }
33
+ }
34
+ result += text.slice(start, i);
35
+ continue;
36
+ }
37
+
38
+ // --- single-line comment: skip to end of line ---
39
+ if (ch === '/' && text[i + 1] === '/') {
40
+ i += 2;
41
+ while (i < len && text[i] !== '\n') {
42
+ i++;
43
+ }
44
+ continue;
45
+ }
46
+
47
+ // --- block comment: skip to closing *​/ ---
48
+ if (ch === '/' && text[i + 1] === '*') {
49
+ i += 2;
50
+ while (i < len && !(text[i] === '*' && text[i + 1] === '/')) {
51
+ i++;
52
+ }
53
+ if (i < len) {
54
+ i += 2; // skip closing */
55
+ }
56
+ continue;
57
+ }
58
+
59
+ result += ch;
60
+ i++;
61
+ }
62
+
63
+ // Strip trailing commas before } or ] (with optional whitespace between).
64
+ result = result.replace(/,(\s*[}\]])/g, '$1');
65
+
66
+ return JSON.parse(result);
67
+ }
@@ -697,7 +697,8 @@ export function performMigration(rootDir: string, routeFiles: string[]): Migrati
697
697
  * Show the migration notice and optionally perform migration.
698
698
  *
699
699
  * Called during `dev` and `build` after dependency upgrades.
700
- * Shows a banner the first time, then a shorter reminder on subsequent runs.
700
+ * Only prompts in interactive TTY sessions and only once if the user
701
+ * dismisses the prompt, it won't be shown again.
701
702
  *
702
703
  * @returns true if migration was performed, false otherwise
703
704
  */
@@ -707,6 +708,12 @@ export async function promptRouteMigration(
707
708
  options?: { interactive?: boolean }
708
709
  ): Promise<boolean> {
709
710
  const interactive = options?.interactive ?? process.stdin.isTTY;
711
+
712
+ // Only show the interactive migration prompt in TTY sessions
713
+ if (!interactive) {
714
+ return false;
715
+ }
716
+
710
717
  const eligibility = checkMigrationEligibility(rootDir);
711
718
 
712
719
  if (!eligibility.available) {
@@ -715,48 +722,30 @@ export async function promptRouteMigration(
715
722
 
716
723
  const { routeFiles, alreadyNotified } = eligibility;
717
724
 
718
- // Non-interactive mode (CI, piped, AI agent): just log a notice
719
- if (!interactive) {
720
- if (!alreadyNotified) {
721
- logger.info(
722
- '[migration] This project uses file-based routing with %d route files in src/api/. ' +
723
- 'Agentuity is moving to explicit routing, which will become the default in the next major release. ' +
724
- 'Run `agentuity dev --migrate-routes` to migrate.',
725
- routeFiles.length
726
- );
727
- writeMigrationState(rootDir, 'notified');
728
- }
725
+ // Only prompt once if the user has already been notified or dismissed, don't ask again
726
+ if (alreadyNotified) {
729
727
  return false;
730
728
  }
731
729
 
732
- // First time: show full banner
733
- if (!alreadyNotified) {
734
- tui.newline();
735
- tui.banner(
736
- ' Migrate to Explicit Routing',
737
- 'Agentuity is moving to explicit routing, which will become the\n' +
738
- 'default in the next major release. File-based route discovery\n' +
739
- 'will be deprecated.\n' +
740
- '\n' +
741
- `Your project has ${routeFiles.length} route files in src/api/ that are\n` +
742
- 'auto-discovered at build time. Explicit routing gives you a single\n' +
743
- 'src/api/index.ts that imports and mounts all sub-routers — just\n' +
744
- 'like a standard Hono application.\n' +
745
- '\n' +
746
- `${tui.muted('Before:')} ${routeFiles.length} files auto-discovered from src/api/**/*.ts\n` +
747
- `${tui.muted('After:')} One src/api/index.ts that imports and mounts them\n` +
748
- '\n' +
749
- 'Your existing route files are not modified. Your app.ts will be\n' +
750
- 'updated to import the router and pass it to createApp({ router }).',
751
- { centerTitle: false }
752
- );
753
- } else {
754
- // Subsequent runs: shorter reminder
755
- tui.newline();
756
- tui.info(
757
- `${tui.bold('Explicit routing migration available')} — run with ${tui.muted('--migrate-routes')} or choose below.`
758
- );
759
- }
730
+ tui.newline();
731
+ tui.banner(
732
+ '✨ Migrate to Explicit Routing',
733
+ 'Agentuity is moving to explicit routing, which will become the\n' +
734
+ 'default in the next major release. File-based route discovery\n' +
735
+ 'will be deprecated.\n' +
736
+ '\n' +
737
+ `Your project has ${routeFiles.length} route files in src/api/ that are\n` +
738
+ 'auto-discovered at build time. Explicit routing gives you a single\n' +
739
+ 'src/api/index.ts that imports and mounts all sub-routers just\n' +
740
+ 'like a standard Hono application.\n' +
741
+ '\n' +
742
+ `${tui.muted('Before:')} ${routeFiles.length} files auto-discovered from src/api/**/*.ts\n` +
743
+ `${tui.muted('After:')} One src/api/index.ts that imports and mounts them\n` +
744
+ '\n' +
745
+ 'Your existing route files are not modified. Your app.ts will be\n' +
746
+ 'updated to import the router and pass it to createApp({ router }).',
747
+ { centerTitle: false }
748
+ );
760
749
 
761
750
  tui.newline();
762
751
 
package/src/utils/zip.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync, lstatSync } from 'node:fs';
1
2
  import { relative } from 'node:path';
2
3
  import { Glob } from 'bun';
3
4
  import AdmZip from 'adm-zip';
@@ -11,7 +12,7 @@ interface Options {
11
12
  export async function zipDir(dir: string, outdir: string, options?: Options) {
12
13
  const zip = new AdmZip();
13
14
  const files = await Array.fromAsync(
14
- new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true })
15
+ new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true, followSymlinks: false })
15
16
  );
16
17
  const total = files.length;
17
18
  let count = 0;
@@ -24,7 +25,21 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
24
25
  }
25
26
  }
26
27
  if (!skip) {
27
- zip.addLocalFile(file, undefined, rel);
28
+ try {
29
+ // Skip symlinks and directories — symlinks are workspace artefacts
30
+ // (e.g. bun's node_modules links) that cannot be resolved portably
31
+ // across machines and would cause EISDIR errors on extraction.
32
+ const stat = lstatSync(file);
33
+ if (!stat.isSymbolicLink() && !stat.isDirectory()) {
34
+ // Use addFile with explicit Unix permissions (0o644) instead of addLocalFile.
35
+ // On Windows, addLocalFile relies on OS file stats which may produce zip entries
36
+ // with incorrect Unix permission bits, causing EACCES errors when extracted on Linux.
37
+ const data = readFileSync(file);
38
+ zip.addFile(rel, data, '', 0o644);
39
+ }
40
+ } catch (err) {
41
+ throw new Error(`Failed to add file to zip: ${rel} (${file})`, { cause: err });
42
+ }
28
43
  }
29
44
  count++;
30
45
  if (options?.progress) {