@agentuity/cli 1.0.10 → 1.0.12
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 +1 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +103 -5
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
- package/dist/cmd/build/vite/config-loader.js +1 -1
- package/dist/cmd/build/vite/config-loader.js.map +1 -1
- package/dist/cmd/build/vite/index.d.ts +2 -0
- package/dist/cmd/build/vite/index.d.ts.map +1 -1
- package/dist/cmd/build/vite/index.js +2 -1
- package/dist/cmd/build/vite/index.js.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.d.ts +4 -1
- package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.js +1 -0
- package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +23 -1
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +1 -0
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/build/vite-bundler.d.ts +2 -0
- package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
- package/dist/cmd/build/vite-bundler.js +2 -1
- package/dist/cmd/build/vite-bundler.js.map +1 -1
- package/dist/cmd/cloud/db/list.d.ts.map +1 -1
- package/dist/cmd/cloud/db/list.js +14 -1
- package/dist/cmd/cloud/db/list.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +36 -22
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/queue/list.d.ts.map +1 -1
- package/dist/cmd/cloud/queue/list.js +10 -0
- package/dist/cmd/cloud/queue/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/list.js +10 -0
- package/dist/cmd/cloud/sandbox/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/runtime/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/runtime/list.js +10 -0
- package/dist/cmd/cloud/sandbox/runtime/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.js +10 -0
- package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
- package/dist/cmd/cloud/session/get.js +12 -12
- package/dist/cmd/cloud/session/get.js.map +1 -1
- package/dist/cmd/cloud/session/list.d.ts.map +1 -1
- package/dist/cmd/cloud/session/list.js +14 -4
- package/dist/cmd/cloud/session/list.js.map +1 -1
- package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/list.js +14 -1
- package/dist/cmd/cloud/storage/list.js.map +1 -1
- package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/list.js +10 -0
- package/dist/cmd/cloud/stream/list.js.map +1 -1
- package/dist/cmd/cloud/thread/list.d.ts.map +1 -1
- package/dist/cmd/cloud/thread/list.js +10 -0
- package/dist/cmd/cloud/thread/list.js.map +1 -1
- package/dist/cmd/project/domain/check.d.ts +2 -0
- package/dist/cmd/project/domain/check.d.ts.map +1 -0
- package/dist/cmd/project/domain/check.js +131 -0
- package/dist/cmd/project/domain/check.js.map +1 -0
- package/dist/cmd/project/domain/index.d.ts +2 -0
- package/dist/cmd/project/domain/index.d.ts.map +1 -0
- package/dist/cmd/project/domain/index.js +20 -0
- package/dist/cmd/project/domain/index.js.map +1 -0
- package/dist/cmd/project/hostname/get.d.ts +2 -0
- package/dist/cmd/project/hostname/get.d.ts.map +1 -0
- package/dist/cmd/project/hostname/get.js +50 -0
- package/dist/cmd/project/hostname/get.js.map +1 -0
- package/dist/cmd/project/hostname/index.d.ts +2 -0
- package/dist/cmd/project/hostname/index.d.ts.map +1 -0
- package/dist/cmd/project/hostname/index.js +18 -0
- package/dist/cmd/project/hostname/index.js.map +1 -0
- package/dist/cmd/project/hostname/set.d.ts +2 -0
- package/dist/cmd/project/hostname/set.d.ts.map +1 -0
- package/dist/cmd/project/hostname/set.js +100 -0
- package/dist/cmd/project/hostname/set.js.map +1 -0
- package/dist/cmd/project/index.d.ts.map +1 -1
- package/dist/cmd/project/index.js +12 -0
- package/dist/cmd/project/index.js.map +1 -1
- package/dist/cmd/upgrade/index.d.ts.map +1 -1
- package/dist/cmd/upgrade/index.js +14 -8
- package/dist/cmd/upgrade/index.js.map +1 -1
- package/dist/cmd/upgrade/npm-availability.d.ts +12 -0
- package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -1
- package/dist/cmd/upgrade/npm-availability.js +85 -6
- package/dist/cmd/upgrade/npm-availability.js.map +1 -1
- package/dist/cmd/upgrade/npm-availability.test.d.ts +2 -0
- package/dist/cmd/upgrade/npm-availability.test.d.ts.map +1 -0
- package/dist/cmd/upgrade/npm-availability.test.js +48 -0
- package/dist/cmd/upgrade/npm-availability.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/steps.d.ts +9 -0
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +131 -71
- package/dist/steps.js.map +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +6 -4
- package/dist/tui.js.map +1 -1
- package/dist/version-check.js +3 -6
- package/dist/version-check.js.map +1 -1
- package/package.json +6 -6
- package/src/cmd/build/ast.ts +141 -5
- package/src/cmd/build/vite/config-loader.ts +1 -3
- package/src/cmd/build/vite/index.ts +4 -0
- package/src/cmd/build/vite/metadata-generator.ts +5 -1
- package/src/cmd/build/vite/route-discovery.ts +34 -1
- package/src/cmd/build/vite/vite-builder.ts +3 -0
- package/src/cmd/build/vite-bundler.ts +4 -0
- package/src/cmd/cloud/db/list.ts +14 -1
- package/src/cmd/cloud/deploy.ts +46 -21
- package/src/cmd/cloud/queue/list.ts +10 -0
- package/src/cmd/cloud/sandbox/list.ts +10 -0
- package/src/cmd/cloud/sandbox/runtime/list.ts +10 -0
- package/src/cmd/cloud/sandbox/snapshot/list.ts +10 -0
- package/src/cmd/cloud/session/get.ts +12 -12
- package/src/cmd/cloud/session/list.ts +28 -18
- package/src/cmd/cloud/storage/list.ts +14 -1
- package/src/cmd/cloud/stream/list.ts +18 -8
- package/src/cmd/cloud/thread/list.ts +15 -5
- package/src/cmd/project/domain/check.ts +146 -0
- package/src/cmd/project/domain/index.ts +20 -0
- package/src/cmd/project/hostname/get.ts +54 -0
- package/src/cmd/project/hostname/index.ts +18 -0
- package/src/cmd/project/hostname/set.ts +123 -0
- package/src/cmd/project/index.ts +12 -0
- package/src/cmd/upgrade/index.ts +23 -9
- package/src/cmd/upgrade/npm-availability.test.ts +65 -0
- package/src/cmd/upgrade/npm-availability.ts +103 -6
- package/src/index.ts +1 -1
- package/src/steps.ts +139 -74
- package/src/tui.ts +6 -4
- package/src/version-check.ts +6 -6
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
import { loadProjectConfig } from '../../../config';
|
|
6
|
+
import { isJSONMode } from '../../../output';
|
|
7
|
+
import {
|
|
8
|
+
checkCustomDomainForDNS,
|
|
9
|
+
isSuccess,
|
|
10
|
+
isPending,
|
|
11
|
+
isMissing,
|
|
12
|
+
isMisconfigured,
|
|
13
|
+
isError,
|
|
14
|
+
} from '../../../domain';
|
|
15
|
+
|
|
16
|
+
export const checkSubcommand = createSubcommand({
|
|
17
|
+
name: 'check',
|
|
18
|
+
description: 'Check DNS configuration for custom domains',
|
|
19
|
+
tags: ['read-only', 'slow', 'requires-auth', 'requires-project'],
|
|
20
|
+
requires: { auth: true, project: true },
|
|
21
|
+
idempotent: true,
|
|
22
|
+
examples: [
|
|
23
|
+
{
|
|
24
|
+
command: getCommand('project domain check'),
|
|
25
|
+
description: 'Check all configured domains',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: getCommand('project domain check --domain example.com'),
|
|
29
|
+
description: 'Check a specific domain',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
schema: {
|
|
33
|
+
options: z.object({
|
|
34
|
+
domain: z.string().optional().describe('Specific domain to check'),
|
|
35
|
+
}),
|
|
36
|
+
response: z.object({
|
|
37
|
+
domains: z.array(
|
|
38
|
+
z.object({
|
|
39
|
+
domain: z.string(),
|
|
40
|
+
recordType: z.string(),
|
|
41
|
+
target: z.string(),
|
|
42
|
+
status: z.string(),
|
|
43
|
+
success: z.boolean(),
|
|
44
|
+
})
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async handler(ctx) {
|
|
50
|
+
const { opts, options, projectDir, config, project } = ctx;
|
|
51
|
+
const jsonMode = isJSONMode(options);
|
|
52
|
+
|
|
53
|
+
// Determine which domains to check
|
|
54
|
+
let domainsToCheck: string[];
|
|
55
|
+
|
|
56
|
+
if (opts?.domain) {
|
|
57
|
+
domainsToCheck = [opts.domain.toLowerCase().trim()];
|
|
58
|
+
} else {
|
|
59
|
+
const projectConfig = await loadProjectConfig(projectDir, config);
|
|
60
|
+
domainsToCheck = projectConfig.deployment?.domains ?? [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (domainsToCheck.length === 0) {
|
|
64
|
+
if (!jsonMode) {
|
|
65
|
+
tui.info('No custom domains configured for this project');
|
|
66
|
+
tui.info(`Use ${tui.bold(getCommand('project add domain <domain>'))} to add one`);
|
|
67
|
+
}
|
|
68
|
+
return { domains: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const results = jsonMode
|
|
72
|
+
? await checkCustomDomainForDNS(project.projectId, domainsToCheck, config)
|
|
73
|
+
: await tui.spinner({
|
|
74
|
+
message: `Checking DNS for ${domainsToCheck.length} ${tui.plural(domainsToCheck.length, 'domain', 'domains')}`,
|
|
75
|
+
clearOnSuccess: true,
|
|
76
|
+
callback: () => checkCustomDomainForDNS(project.projectId, domainsToCheck, config),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const domainResults = results.map((r) => {
|
|
80
|
+
let status: string;
|
|
81
|
+
let statusRaw: string;
|
|
82
|
+
let success = false;
|
|
83
|
+
|
|
84
|
+
if (isSuccess(r)) {
|
|
85
|
+
status = tui.colorSuccess(`${tui.ICONS.success} Configured`);
|
|
86
|
+
statusRaw = 'configured';
|
|
87
|
+
success = true;
|
|
88
|
+
} else if (isPending(r)) {
|
|
89
|
+
status = tui.colorWarning('⏳ Pending');
|
|
90
|
+
statusRaw = 'pending';
|
|
91
|
+
} else if (isMisconfigured(r)) {
|
|
92
|
+
status = tui.colorWarning(`${tui.ICONS.warning} ${r.misconfigured}`);
|
|
93
|
+
statusRaw = 'misconfigured';
|
|
94
|
+
} else if (isError(r)) {
|
|
95
|
+
status = tui.colorError(`${tui.ICONS.error} ${r.error}`);
|
|
96
|
+
statusRaw = 'error';
|
|
97
|
+
} else if (isMissing(r)) {
|
|
98
|
+
status = tui.colorError(`${tui.ICONS.error} Missing`);
|
|
99
|
+
statusRaw = 'missing';
|
|
100
|
+
} else {
|
|
101
|
+
status = tui.colorError(`${tui.ICONS.error} Unknown`);
|
|
102
|
+
statusRaw = 'unknown';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
domain: r.domain,
|
|
107
|
+
recordType: r.recordType,
|
|
108
|
+
target: r.target,
|
|
109
|
+
status,
|
|
110
|
+
statusRaw,
|
|
111
|
+
success,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!jsonMode) {
|
|
116
|
+
tui.newline();
|
|
117
|
+
for (const r of domainResults) {
|
|
118
|
+
console.log(` ${tui.colorInfo('Domain:')} ${tui.colorPrimary(r.domain)}`);
|
|
119
|
+
console.log(` ${tui.colorInfo('Type:')} ${tui.colorPrimary(r.recordType)}`);
|
|
120
|
+
console.log(` ${tui.colorInfo('Target:')} ${tui.colorPrimary(r.target)}`);
|
|
121
|
+
console.log(` ${tui.colorInfo('Status:')} ${r.status}`);
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const allGood = domainResults.every((r) => r.success);
|
|
126
|
+
if (allGood) {
|
|
127
|
+
tui.success('All domains are correctly configured');
|
|
128
|
+
} else {
|
|
129
|
+
const failCount = domainResults.filter((r) => !r.success).length;
|
|
130
|
+
tui.warning(
|
|
131
|
+
`${failCount} ${tui.plural(failCount, 'domain has', 'domains have')} DNS issues — add a CNAME record pointing to the target shown above`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
domains: domainResults.map((r) => ({
|
|
138
|
+
domain: r.domain,
|
|
139
|
+
recordType: r.recordType,
|
|
140
|
+
target: r.target,
|
|
141
|
+
status: r.statusRaw,
|
|
142
|
+
success: r.success,
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createCommand } from '../../../types';
|
|
2
|
+
import { checkSubcommand } from './check';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
|
|
5
|
+
export const domainCommand = createCommand({
|
|
6
|
+
name: 'domain',
|
|
7
|
+
description: 'Manage custom domains for the project',
|
|
8
|
+
tags: ['fast', 'requires-auth'],
|
|
9
|
+
examples: [
|
|
10
|
+
{
|
|
11
|
+
command: getCommand('project domain check'),
|
|
12
|
+
description: 'Check DNS for all custom domains',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
command: getCommand('project domain check --domain example.com'),
|
|
16
|
+
description: 'Check DNS for a specific domain',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
subcommands: [checkSubcommand],
|
|
20
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { projectHostnameGet } from '@agentuity/server';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { isJSONMode } from '../../../output';
|
|
7
|
+
|
|
8
|
+
const HostnameGetResponseSchema = z.object({
|
|
9
|
+
hostname: z.string().nullable().describe('The vanity hostname'),
|
|
10
|
+
url: z.string().nullable().describe('The full URL'),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const getSubcommand = createSubcommand({
|
|
14
|
+
name: 'get',
|
|
15
|
+
description: 'Show the current vanity hostname for the project',
|
|
16
|
+
tags: ['read-only', 'fast', 'requires-auth', 'requires-project'],
|
|
17
|
+
requires: { auth: true, apiClient: true, project: true },
|
|
18
|
+
idempotent: true,
|
|
19
|
+
examples: [
|
|
20
|
+
{
|
|
21
|
+
command: getCommand('project hostname get'),
|
|
22
|
+
description: 'Show current hostname',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
schema: {
|
|
26
|
+
response: HostnameGetResponseSchema,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async handler(ctx) {
|
|
30
|
+
const { apiClient, project, options } = ctx;
|
|
31
|
+
const jsonMode = isJSONMode(options);
|
|
32
|
+
|
|
33
|
+
const result = jsonMode
|
|
34
|
+
? await projectHostnameGet(apiClient, { projectId: project.projectId })
|
|
35
|
+
: await tui.spinner('Fetching hostname', () => {
|
|
36
|
+
return projectHostnameGet(apiClient, { projectId: project.projectId });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!jsonMode) {
|
|
40
|
+
if (result.hostname) {
|
|
41
|
+
tui.success(`Hostname: ${tui.bold(result.hostname)}`);
|
|
42
|
+
tui.info(`URL: ${result.url}`);
|
|
43
|
+
} else {
|
|
44
|
+
tui.info('No vanity hostname set for this project');
|
|
45
|
+
tui.info(`Use ${tui.bold(getCommand('project hostname set <hostname>'))} to set one`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
hostname: result.hostname,
|
|
51
|
+
url: result.url,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createCommand } from '../../../types';
|
|
2
|
+
import { getSubcommand } from './get';
|
|
3
|
+
import { setSubcommand } from './set';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
|
|
6
|
+
export const hostnameCommand = createCommand({
|
|
7
|
+
name: 'hostname',
|
|
8
|
+
description: 'Manage the project vanity hostname on agentuity.run',
|
|
9
|
+
tags: ['fast', 'requires-auth'],
|
|
10
|
+
examples: [
|
|
11
|
+
{ command: getCommand('project hostname get'), description: 'Show current hostname' },
|
|
12
|
+
{
|
|
13
|
+
command: getCommand('project hostname set my-cool-api'),
|
|
14
|
+
description: 'Set a custom hostname',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
subcommands: [getSubcommand, setSubcommand],
|
|
18
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { projectHostnameSet } from '@agentuity/server';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import { isJSONMode } from '../../../output';
|
|
7
|
+
import { ErrorCode } from '../../../errors';
|
|
8
|
+
|
|
9
|
+
// Client-side reserved names list (mirrors server-side list)
|
|
10
|
+
const RESERVED_NAMES = new Set([
|
|
11
|
+
'app',
|
|
12
|
+
'api',
|
|
13
|
+
'catalyst',
|
|
14
|
+
'pulse',
|
|
15
|
+
'streams',
|
|
16
|
+
'registry',
|
|
17
|
+
'ion',
|
|
18
|
+
'status',
|
|
19
|
+
'admin',
|
|
20
|
+
'www',
|
|
21
|
+
'mail',
|
|
22
|
+
'dns',
|
|
23
|
+
'console',
|
|
24
|
+
'dashboard',
|
|
25
|
+
'docs',
|
|
26
|
+
'help',
|
|
27
|
+
'support',
|
|
28
|
+
'billing',
|
|
29
|
+
'test',
|
|
30
|
+
'staging',
|
|
31
|
+
'dev',
|
|
32
|
+
'prod',
|
|
33
|
+
'ns0',
|
|
34
|
+
'ns1',
|
|
35
|
+
'ns2',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const HostnameSetResponseSchema = z.object({
|
|
39
|
+
hostname: z.string().describe('The vanity hostname that was set'),
|
|
40
|
+
url: z.string().describe('The full URL'),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const setSubcommand = createSubcommand({
|
|
44
|
+
name: 'set',
|
|
45
|
+
description: 'Set a custom vanity hostname for the project on agentuity.run',
|
|
46
|
+
tags: ['mutating', 'fast', 'requires-auth', 'requires-project'],
|
|
47
|
+
requires: { auth: true, apiClient: true, project: true },
|
|
48
|
+
examples: [
|
|
49
|
+
{
|
|
50
|
+
command: getCommand('project hostname set my-cool-api'),
|
|
51
|
+
description: 'Set a custom hostname',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
schema: {
|
|
55
|
+
args: z.object({
|
|
56
|
+
hostname: z.string().describe('the vanity hostname (e.g., my-cool-api)'),
|
|
57
|
+
}),
|
|
58
|
+
response: HostnameSetResponseSchema,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async handler(ctx) {
|
|
62
|
+
const { args, apiClient, project, options, logger } = ctx;
|
|
63
|
+
const jsonMode = isJSONMode(options);
|
|
64
|
+
|
|
65
|
+
const hostname = args.hostname.toLowerCase().trim();
|
|
66
|
+
|
|
67
|
+
// Client-side validation: DNS label regex
|
|
68
|
+
const hostnameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
69
|
+
if (!hostnameRegex.test(hostname)) {
|
|
70
|
+
logger.fatal(
|
|
71
|
+
'Invalid hostname: must contain only lowercase letters, numbers, and hyphens, and must start and end with a letter or number',
|
|
72
|
+
ErrorCode.VALIDATION_FAILED
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Max 63 chars (DNS label limit)
|
|
77
|
+
if (hostname.length > 63) {
|
|
78
|
+
logger.fatal(
|
|
79
|
+
'Invalid hostname: must be 63 characters or fewer',
|
|
80
|
+
ErrorCode.VALIDATION_FAILED
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Must not match reserved hash patterns
|
|
85
|
+
const reservedHashPattern = /^[dp][a-f0-9]{16}$/;
|
|
86
|
+
if (reservedHashPattern.test(hostname)) {
|
|
87
|
+
logger.fatal(
|
|
88
|
+
'Invalid hostname: this pattern is reserved for internal use',
|
|
89
|
+
ErrorCode.VALIDATION_FAILED
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Must not be a reserved name
|
|
94
|
+
if (RESERVED_NAMES.has(hostname)) {
|
|
95
|
+
logger.fatal(
|
|
96
|
+
`Invalid hostname: "${hostname}" is a reserved name`,
|
|
97
|
+
ErrorCode.VALIDATION_FAILED
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = jsonMode
|
|
102
|
+
? await projectHostnameSet(apiClient, {
|
|
103
|
+
projectId: project.projectId,
|
|
104
|
+
hostname,
|
|
105
|
+
})
|
|
106
|
+
: await tui.spinner('Setting hostname', () => {
|
|
107
|
+
return projectHostnameSet(apiClient, {
|
|
108
|
+
projectId: project.projectId,
|
|
109
|
+
hostname,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!jsonMode) {
|
|
114
|
+
tui.success(`Hostname set: ${tui.bold(result.url)}`);
|
|
115
|
+
tui.info('Hostname will be active after next deployment');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
hostname: result.hostname,
|
|
120
|
+
url: result.url,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
package/src/cmd/project/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { deleteSubcommand } from './delete';
|
|
|
6
6
|
import { showSubcommand } from './show';
|
|
7
7
|
import { authCommand } from './auth';
|
|
8
8
|
import { addCommand } from './add';
|
|
9
|
+
import { hostnameCommand } from './hostname';
|
|
10
|
+
import { domainCommand } from './domain';
|
|
9
11
|
import { getCommand } from '../../command-prefix';
|
|
10
12
|
|
|
11
13
|
export const command = createCommand({
|
|
@@ -22,6 +24,14 @@ export const command = createCommand({
|
|
|
22
24
|
command: getCommand('project add storage'),
|
|
23
25
|
description: 'Link an existing storage bucket',
|
|
24
26
|
},
|
|
27
|
+
{
|
|
28
|
+
command: getCommand('project hostname get'),
|
|
29
|
+
description: 'Show current vanity hostname',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
command: getCommand('project domain check'),
|
|
33
|
+
description: 'Check DNS for custom domains',
|
|
34
|
+
},
|
|
25
35
|
],
|
|
26
36
|
subcommands: [
|
|
27
37
|
createProjectSubcommand,
|
|
@@ -31,5 +41,7 @@ export const command = createCommand({
|
|
|
31
41
|
showSubcommand,
|
|
32
42
|
authCommand,
|
|
33
43
|
addCommand,
|
|
44
|
+
hostnameCommand,
|
|
45
|
+
domainCommand,
|
|
34
46
|
],
|
|
35
47
|
});
|
package/src/cmd/upgrade/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { getCommand } from '../../command-prefix';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ErrorCode, createError, exitWithError } from '../../errors';
|
|
6
6
|
import * as tui from '../../tui';
|
|
7
|
-
import { $ } from 'bun';
|
|
8
7
|
import { tmpdir } from 'node:os';
|
|
9
8
|
import { getInstallationType, type InstallationType } from '../../utils/installation-type';
|
|
10
9
|
|
|
@@ -84,14 +83,17 @@ async function performBunUpgrade(
|
|
|
84
83
|
const { installWithRetry } = await import('./npm-availability');
|
|
85
84
|
|
|
86
85
|
try {
|
|
86
|
+
const { spawnWithTimeout } = await import('./npm-availability');
|
|
87
|
+
|
|
87
88
|
await installWithRetry(
|
|
88
89
|
async () => {
|
|
89
90
|
// Use bun to install the specific version globally
|
|
90
91
|
// Run from tmpdir to avoid interference from any local package.json/node_modules
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
// spawnWithTimeout kills the process if it exceeds 30s (INSTALL_TIMEOUT_MS)
|
|
93
|
+
const result = await spawnWithTimeout(
|
|
94
|
+
['bun', 'add', '-g', `@agentuity/cli@${npmVersion}`],
|
|
95
|
+
{ cwd: tmpdir(), timeout: 30_000 }
|
|
96
|
+
);
|
|
95
97
|
return { exitCode: result.exitCode, stderr: result.stderr };
|
|
96
98
|
},
|
|
97
99
|
{ onRetry }
|
|
@@ -107,8 +109,10 @@ async function performBunUpgrade(
|
|
|
107
109
|
* Verify the upgrade was successful by checking the installed version
|
|
108
110
|
*/
|
|
109
111
|
async function verifyUpgrade(expectedVersion: string): Promise<void> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const { spawnWithTimeout } = await import('./npm-availability');
|
|
113
|
+
|
|
114
|
+
// Run agentuity version to check the installed version (5s timeout — local command, sub-second normally)
|
|
115
|
+
const result = await spawnWithTimeout(['agentuity', 'version'], { timeout: 5_000 });
|
|
112
116
|
|
|
113
117
|
if (result.exitCode !== 0) {
|
|
114
118
|
throw new Error('Failed to verify upgrade - could not run agentuity version');
|
|
@@ -219,8 +223,12 @@ export const command = createCommand({
|
|
|
219
223
|
|
|
220
224
|
if (!isAvailable) {
|
|
221
225
|
tui.warning('The new version is not yet available on npm.');
|
|
222
|
-
tui.info(
|
|
223
|
-
|
|
226
|
+
tui.info(
|
|
227
|
+
'This can happen right after a release. Please try again in a few minutes.'
|
|
228
|
+
);
|
|
229
|
+
tui.newline();
|
|
230
|
+
tui.info('You can also upgrade manually:');
|
|
231
|
+
console.log(` ${tui.muted('curl -fsSL https://agentuity.sh | sh')}`);
|
|
224
232
|
return {
|
|
225
233
|
upgraded: false,
|
|
226
234
|
from: currentVersion,
|
|
@@ -301,6 +309,12 @@ export const command = createCommand({
|
|
|
301
309
|
installationType,
|
|
302
310
|
};
|
|
303
311
|
|
|
312
|
+
tui.newline();
|
|
313
|
+
tui.info('If the upgrade continues to fail, you can reinstall manually:');
|
|
314
|
+
tui.newline();
|
|
315
|
+
console.log(` ${tui.muted('curl -fsSL https://agentuity.sh | sh')}`);
|
|
316
|
+
tui.newline();
|
|
317
|
+
|
|
304
318
|
exitWithError(
|
|
305
319
|
createError(ErrorCode.INTERNAL_ERROR, 'Upgrade failed', errorDetails),
|
|
306
320
|
logger,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { spawnWithTimeout } from './npm-availability';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// bun info requires a package.json in its cwd, and isVersionAvailableOnNpm
|
|
7
|
+
// uses tmpdir() as cwd. Ensure a minimal package.json exists there for tests.
|
|
8
|
+
const tmpPackageJson = join(tmpdir(), 'package.json');
|
|
9
|
+
let createdPackageJson = false;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const file = Bun.file(tmpPackageJson);
|
|
13
|
+
if (!(await file.exists())) {
|
|
14
|
+
await Bun.write(file, '{}');
|
|
15
|
+
createdPackageJson = true;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
if (createdPackageJson) {
|
|
21
|
+
const { unlink } = await import('node:fs/promises');
|
|
22
|
+
await unlink(tmpPackageJson).catch(() => {});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('spawnWithTimeout kills process on timeout', async () => {
|
|
27
|
+
// spawn a command that hangs with a short timeout (500ms)
|
|
28
|
+
await expect(
|
|
29
|
+
spawnWithTimeout(['bun', '-e', 'setTimeout(()=>{},60000)'], { timeout: 500 })
|
|
30
|
+
).rejects.toThrow(/timed out/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('spawnWithTimeout returns result when command completes in time', async () => {
|
|
34
|
+
const result = await spawnWithTimeout(['bun', '-e', "console.log('hello')"], {
|
|
35
|
+
timeout: 5_000,
|
|
36
|
+
});
|
|
37
|
+
expect(result.exitCode).toBe(0);
|
|
38
|
+
expect(result.stdout.toString().trim()).toBe('hello');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('spawnWithTimeout returns non-zero exit code without throwing', async () => {
|
|
42
|
+
const result = await spawnWithTimeout(['bun', '-e', 'process.exit(1)'], { timeout: 5_000 });
|
|
43
|
+
expect(result.exitCode).not.toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test(
|
|
47
|
+
'isVersionAvailableOnNpm returns true for a known version',
|
|
48
|
+
async () => {
|
|
49
|
+
const { isVersionAvailableOnNpm } = await import('./npm-availability');
|
|
50
|
+
// Use a known-good old version that definitely exists
|
|
51
|
+
const result = await isVersionAvailableOnNpm('1.0.10');
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
},
|
|
54
|
+
15_000
|
|
55
|
+
); // generous test timeout but the function itself has 10s subprocess timeout
|
|
56
|
+
|
|
57
|
+
test(
|
|
58
|
+
'isVersionAvailableOnNpm returns false for non-existent version',
|
|
59
|
+
async () => {
|
|
60
|
+
const { isVersionAvailableOnNpm } = await import('./npm-availability');
|
|
61
|
+
const result = await isVersionAvailableOnNpm('999.999.999');
|
|
62
|
+
expect(result).toBe(false);
|
|
63
|
+
},
|
|
64
|
+
15_000
|
|
65
|
+
);
|
|
@@ -3,11 +3,93 @@
|
|
|
3
3
|
* Used to verify a version is available via bun's resolver before attempting upgrade.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { $ } from 'bun';
|
|
7
6
|
import { tmpdir } from 'node:os';
|
|
8
7
|
|
|
9
8
|
const PACKAGE_SPEC = '@agentuity/cli';
|
|
10
9
|
|
|
10
|
+
/** Default timeout for `bun info` subprocess calls (10 seconds) */
|
|
11
|
+
const BUN_INFO_TIMEOUT_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
/** Default timeout for install (`bun add -g`) subprocess calls (30 seconds) */
|
|
14
|
+
const INSTALL_TIMEOUT_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run a command via Bun.spawn with a timeout that kills the process.
|
|
18
|
+
* Returns { exitCode, stdout, stderr } similar to Bun's $ shell result.
|
|
19
|
+
*/
|
|
20
|
+
export async function spawnWithTimeout(
|
|
21
|
+
cmd: string[],
|
|
22
|
+
options: { cwd?: string; timeout: number }
|
|
23
|
+
): Promise<{ exitCode: number; stdout: Buffer; stderr: Buffer }> {
|
|
24
|
+
const proc = Bun.spawn(cmd, {
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
stdout: 'pipe',
|
|
27
|
+
stderr: 'pipe',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
timedOut = true;
|
|
33
|
+
proc.kill();
|
|
34
|
+
}, options.timeout);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const [exitCode, stdoutBytes, stderrBytes] = await Promise.all([
|
|
38
|
+
proc.exited,
|
|
39
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
40
|
+
new Response(proc.stderr).arrayBuffer(),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
if (timedOut) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Command timed out after ${options.timeout}ms: ${cmd.join(' ')}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
exitCode,
|
|
51
|
+
stdout: Buffer.from(stdoutBytes),
|
|
52
|
+
stderr: Buffer.from(stderrBytes),
|
|
53
|
+
};
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sentinel error thrown exclusively by withTimeout so the retry loop can
|
|
61
|
+
* distinguish a genuine timeout from other failures (e.g. permission errors).
|
|
62
|
+
*/
|
|
63
|
+
class TimeoutError extends Error {
|
|
64
|
+
constructor(description: string, timeoutMs: number) {
|
|
65
|
+
super(`${description} timed out after ${timeoutMs}ms`);
|
|
66
|
+
this.name = 'TimeoutError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Race a promise against a timeout. Unlike spawnWithTimeout (which kills a process),
|
|
72
|
+
* this is a generic wrapper for any async operation (e.g. the installFn callback).
|
|
73
|
+
*
|
|
74
|
+
* Throws a {@link TimeoutError} (not a plain Error) so callers can tell
|
|
75
|
+
* timeouts apart from other exceptions.
|
|
76
|
+
*/
|
|
77
|
+
async function withTimeout<T>(
|
|
78
|
+
promise: Promise<T>,
|
|
79
|
+
timeoutMs: number,
|
|
80
|
+
description: string
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
83
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
84
|
+
timer = setTimeout(() => reject(new TimeoutError(description, timeoutMs)), timeoutMs);
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
88
|
+
} finally {
|
|
89
|
+
clearTimeout(timer!);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
11
93
|
/**
|
|
12
94
|
* Check if a specific version of @agentuity/cli is resolvable by bun.
|
|
13
95
|
* Uses `bun info` to verify bun's own resolver/CDN can see the version,
|
|
@@ -20,10 +102,10 @@ const PACKAGE_SPEC = '@agentuity/cli';
|
|
|
20
102
|
export async function isVersionAvailableOnNpm(version: string): Promise<boolean> {
|
|
21
103
|
const normalizedVersion = version.replace(/^v/, '');
|
|
22
104
|
try {
|
|
23
|
-
const result = await
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
105
|
+
const result = await spawnWithTimeout(
|
|
106
|
+
['bun', 'info', `${PACKAGE_SPEC}@${normalizedVersion}`, '--json'],
|
|
107
|
+
{ cwd: tmpdir(), timeout: BUN_INFO_TIMEOUT_MS }
|
|
108
|
+
);
|
|
27
109
|
if (result.exitCode !== 0) {
|
|
28
110
|
return false;
|
|
29
111
|
}
|
|
@@ -147,7 +229,22 @@ export async function installWithRetry(
|
|
|
147
229
|
let delay = initialDelayMs;
|
|
148
230
|
|
|
149
231
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
150
|
-
|
|
232
|
+
let result: { exitCode: number; stderr: Buffer };
|
|
233
|
+
try {
|
|
234
|
+
result = await withTimeout(installFn(), INSTALL_TIMEOUT_MS, 'Install command');
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Only retry on timeouts — non-timeout errors (permissions, disk, etc.) are fatal
|
|
237
|
+
if (!(error instanceof TimeoutError)) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
if (attempt === maxAttempts) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
onRetry?.(attempt, delay);
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
245
|
+
delay = Math.min(Math.round(delay * multiplier), maxDelayMs);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
151
248
|
|
|
152
249
|
if (result.exitCode === 0) {
|
|
153
250
|
return result;
|
package/src/index.ts
CHANGED
|
@@ -102,7 +102,7 @@ export {
|
|
|
102
102
|
type CommandHandler,
|
|
103
103
|
type TableColumn,
|
|
104
104
|
} from './repl';
|
|
105
|
-
export { runSteps, stepSuccess, stepSkipped, stepError } from './steps';
|
|
105
|
+
export { runSteps, stepSuccess, stepSkipped, stepError, StepInterruptError } from './steps';
|
|
106
106
|
export { playSound } from './sound';
|
|
107
107
|
export {
|
|
108
108
|
downloadWithProgress,
|