@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.
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +3 -3
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/typecheck.d.ts.map +1 -1
- package/dist/cmd/build/typecheck.js +52 -1
- package/dist/cmd/build/typecheck.js.map +1 -1
- package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
- package/dist/cmd/build/vite/static-renderer.js +22 -8
- package/dist/cmd/build/vite/static-renderer.js.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +4 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/monitor.d.ts +3 -0
- package/dist/cmd/cloud/monitor.d.ts.map +1 -0
- package/dist/cmd/cloud/monitor.js +300 -0
- package/dist/cmd/cloud/monitor.js.map +1 -0
- package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
- package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/activity.js +54 -0
- package/dist/cmd/cloud/oidc/activity.js.map +1 -0
- package/dist/cmd/cloud/oidc/create.d.ts +2 -0
- package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/create.js +201 -0
- package/dist/cmd/cloud/oidc/create.js.map +1 -0
- package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
- package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/delete.js +56 -0
- package/dist/cmd/cloud/oidc/delete.js.map +1 -0
- package/dist/cmd/cloud/oidc/get.d.ts +2 -0
- package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/get.js +59 -0
- package/dist/cmd/cloud/oidc/get.js.map +1 -0
- package/dist/cmd/cloud/oidc/index.d.ts +3 -0
- package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/index.js +32 -0
- package/dist/cmd/cloud/oidc/index.js.map +1 -0
- package/dist/cmd/cloud/oidc/list.d.ts +2 -0
- package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/list.js +45 -0
- package/dist/cmd/cloud/oidc/list.js.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js +63 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
- package/dist/cmd/cloud/oidc/users.d.ts +2 -0
- package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/users.js +50 -0
- package/dist/cmd/cloud/oidc/users.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/utils/jsonc.d.ts +13 -0
- package/dist/utils/jsonc.d.ts.map +1 -0
- package/dist/utils/jsonc.js +63 -0
- package/dist/utils/jsonc.js.map +1 -0
- package/dist/utils/route-migration.d.ts +2 -1
- package/dist/utils/route-migration.d.ts.map +1 -1
- package/dist/utils/route-migration.js +23 -32
- package/dist/utils/route-migration.js.map +1 -1
- package/dist/utils/zip.d.ts.map +1 -1
- package/dist/utils/zip.js +18 -2
- package/dist/utils/zip.js.map +1 -1
- package/package.json +6 -7
- package/src/cmd/build/ast.ts +6 -3
- package/src/cmd/build/typecheck.ts +60 -1
- package/src/cmd/build/vite/static-renderer.ts +24 -8
- package/src/cmd/cloud/index.ts +4 -0
- package/src/cmd/cloud/monitor.ts +375 -0
- package/src/cmd/cloud/oidc/activity.ts +61 -0
- package/src/cmd/cloud/oidc/create.ts +232 -0
- package/src/cmd/cloud/oidc/delete.ts +63 -0
- package/src/cmd/cloud/oidc/get.ts +65 -0
- package/src/cmd/cloud/oidc/index.ts +35 -0
- package/src/cmd/cloud/oidc/list.ts +50 -0
- package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
- package/src/cmd/cloud/oidc/users.ts +57 -0
- package/src/config.ts +16 -5
- package/src/utils/jsonc.ts +67 -0
- package/src/utils/route-migration.ts +29 -40
- 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
|
|
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 (
|
|
269
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
//
|
|
719
|
-
if (
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
'
|
|
737
|
-
'
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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) {
|