@agentuity/cli 2.0.12 → 2.0.13
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/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -8
- package/dist/cli.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/coder/workspace/common.d.ts +29 -0
- package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/common.js +83 -0
- package/dist/cmd/coder/workspace/common.js.map +1 -0
- package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/create.js +34 -37
- package/dist/cmd/coder/workspace/create.js.map +1 -1
- package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/get.js +2 -5
- package/dist/cmd/coder/workspace/get.js.map +1 -1
- package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/index.js +10 -0
- package/dist/cmd/coder/workspace/index.js.map +1 -1
- package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/list.js +4 -0
- package/dist/cmd/coder/workspace/list.js.map +1 -1
- package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
- package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/refresh.js +59 -0
- package/dist/cmd/coder/workspace/refresh.js.map +1 -0
- package/dist/cmd/coder/workspace/update.d.ts +2 -0
- package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/update.js +131 -0
- package/dist/cmd/coder/workspace/update.js.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
- package/dist/cmd/project/random-name.d.ts +17 -0
- package/dist/cmd/project/random-name.d.ts.map +1 -0
- package/dist/cmd/project/random-name.js +144 -0
- package/dist/cmd/project/random-name.js.map +1 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +181 -153
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/composite-logger.d.ts.map +1 -1
- package/dist/composite-logger.js +19 -0
- package/dist/composite-logger.js.map +1 -1
- package/dist/config.d.ts +18 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -16
- package/dist/config.js.map +1 -1
- package/dist/tui/prompt.d.ts +29 -0
- package/dist/tui/prompt.d.ts.map +1 -1
- package/dist/tui/prompt.js +180 -8
- package/dist/tui/prompt.js.map +1 -1
- package/package.json +7 -7
- package/src/cli.ts +30 -8
- package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
- package/src/cmd/coder/workspace/common.ts +103 -0
- package/src/cmd/coder/workspace/create.ts +39 -43
- package/src/cmd/coder/workspace/get.ts +2 -5
- package/src/cmd/coder/workspace/index.ts +10 -0
- package/src/cmd/coder/workspace/list.ts +4 -0
- package/src/cmd/coder/workspace/refresh.ts +63 -0
- package/src/cmd/coder/workspace/update.ts +154 -0
- package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
- package/src/cmd/project/random-name.ts +152 -0
- package/src/cmd/project/template-flow.ts +199 -161
- package/src/composite-logger.ts +20 -0
- package/src/config.ts +69 -19
- package/src/tui/prompt.ts +214 -8
|
@@ -10,29 +10,14 @@ import * as tui from '../../../tui';
|
|
|
10
10
|
import { getCommand } from '../../../command-prefix';
|
|
11
11
|
import { ErrorCode } from '../../../errors';
|
|
12
12
|
import { resolveGitHubRepo } from '../resolve-repo';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(input.skillBucketIds?.length ?? 0) > 0 ||
|
|
22
|
-
(input.enabledAgents?.length ?? 0) > 0
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatWorkspaceValidationMessage(issues: Array<{ message: string }>): string {
|
|
27
|
-
const messages = [...new Set(issues.map((issue) => issue.message).filter(Boolean))];
|
|
28
|
-
if (messages.length === 0) {
|
|
29
|
-
return 'Invalid workspace configuration';
|
|
30
|
-
}
|
|
31
|
-
if (messages.includes(EMPTY_WORKSPACE_ERROR)) {
|
|
32
|
-
return `${EMPTY_WORKSPACE_ERROR}. Use --repo or --enabled-agents.`;
|
|
33
|
-
}
|
|
34
|
-
return messages.join('; ');
|
|
35
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
EMPTY_WORKSPACE_ERROR,
|
|
15
|
+
formatWorkspaceValidationMessage,
|
|
16
|
+
hasWorkspaceSelections,
|
|
17
|
+
parseCommaList,
|
|
18
|
+
printWorkspaceSummary,
|
|
19
|
+
readSetupScript,
|
|
20
|
+
} from './common';
|
|
36
21
|
|
|
37
22
|
export const createWorkspaceSubcommand = createSubcommand({
|
|
38
23
|
name: 'create',
|
|
@@ -49,9 +34,9 @@ export const createWorkspaceSubcommand = createSubcommand({
|
|
|
49
34
|
},
|
|
50
35
|
{
|
|
51
36
|
command: getCommand(
|
|
52
|
-
'coder workspace create "My Workspace" --
|
|
37
|
+
'coder workspace create "My Workspace" --dependency git --setup-script-file ./setup.sh --scope org'
|
|
53
38
|
),
|
|
54
|
-
description: 'Create an org-scoped workspace with
|
|
39
|
+
description: 'Create an org-scoped workspace with dependencies and a setup script',
|
|
55
40
|
},
|
|
56
41
|
{
|
|
57
42
|
command: getCommand('coder workspace create "My Workspace" --enabled-agents code-review'),
|
|
@@ -74,6 +59,18 @@ export const createWorkspaceSubcommand = createSubcommand({
|
|
|
74
59
|
scope: z.string().optional().describe('Workspace scope: user or org'),
|
|
75
60
|
repo: z.string().optional().describe('Repository URL to add'),
|
|
76
61
|
repoBranch: z.string().optional().describe('Branch for the repository'),
|
|
62
|
+
dependency: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Comma-separated APT dependencies to install into workspace snapshots'),
|
|
66
|
+
setupScript: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Inline shell script to run while preparing workspace snapshots'),
|
|
70
|
+
setupScriptFile: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Path to a shell script to run while preparing workspace snapshots'),
|
|
77
74
|
enabledAgents: z
|
|
78
75
|
.string()
|
|
79
76
|
.optional()
|
|
@@ -105,15 +102,26 @@ export const createWorkspaceSubcommand = createSubcommand({
|
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
}
|
|
105
|
+
if (opts?.dependency) {
|
|
106
|
+
body.dependencies = parseCommaList(opts.dependency);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const setupScript = await readSetupScript({
|
|
110
|
+
setupScript: opts?.setupScript,
|
|
111
|
+
setupScriptFile: opts?.setupScriptFile,
|
|
112
|
+
});
|
|
113
|
+
if (setupScript !== undefined) body.setupScript = setupScript;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
tui.fatal(`Failed to read setup script: ${msg}`, ErrorCode.VALIDATION_FAILED);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
108
119
|
if (opts?.enabledAgents) {
|
|
109
|
-
body.enabledAgents = opts.enabledAgents
|
|
110
|
-
.split(',')
|
|
111
|
-
.map((name) => name.trim())
|
|
112
|
-
.filter(Boolean);
|
|
120
|
+
body.enabledAgents = parseCommaList(opts.enabledAgents);
|
|
113
121
|
}
|
|
114
122
|
if (!hasWorkspaceSelections(body)) {
|
|
115
123
|
tui.fatal(
|
|
116
|
-
`Failed to create workspace: ${EMPTY_WORKSPACE_ERROR}. Use --repo or --enabled-agents.`,
|
|
124
|
+
`Failed to create workspace: ${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, or --enabled-agents.`,
|
|
117
125
|
ErrorCode.VALIDATION_FAILED
|
|
118
126
|
);
|
|
119
127
|
}
|
|
@@ -132,9 +140,6 @@ export const createWorkspaceSubcommand = createSubcommand({
|
|
|
132
140
|
|
|
133
141
|
try {
|
|
134
142
|
const created = await client.createWorkspace(validationResult.data);
|
|
135
|
-
const createdEnabledAgents = Array.isArray(created.enabledAgents)
|
|
136
|
-
? created.enabledAgents.filter((name): name is string => typeof name === 'string')
|
|
137
|
-
: [];
|
|
138
143
|
|
|
139
144
|
if (options.json) {
|
|
140
145
|
return created;
|
|
@@ -142,16 +147,7 @@ export const createWorkspaceSubcommand = createSubcommand({
|
|
|
142
147
|
|
|
143
148
|
tui.success(`Workspace ${created.id} created.`);
|
|
144
149
|
tui.newline();
|
|
145
|
-
|
|
146
|
-
if (created.description) {
|
|
147
|
-
tui.output(` Description: ${created.description}`);
|
|
148
|
-
}
|
|
149
|
-
tui.output(` Scope: ${created.scope}`);
|
|
150
|
-
tui.output(` Repos: ${created.repoCount}`);
|
|
151
|
-
tui.output(` Selections: ${created.selectionCount}`);
|
|
152
|
-
if (createdEnabledAgents.length > 0) {
|
|
153
|
-
tui.output(` Agents: ${createdEnabledAgents.join(', ')}`);
|
|
154
|
-
}
|
|
150
|
+
printWorkspaceSummary(created);
|
|
155
151
|
|
|
156
152
|
return created;
|
|
157
153
|
} catch (err) {
|
|
@@ -5,6 +5,7 @@ import { createSubcommand } from '../../../types';
|
|
|
5
5
|
import * as tui from '../../../tui';
|
|
6
6
|
import { getCommand } from '../../../command-prefix';
|
|
7
7
|
import { ErrorCode } from '../../../errors';
|
|
8
|
+
import { printWorkspaceSummary } from './common';
|
|
8
9
|
|
|
9
10
|
function formatRelativeTime(isoDate: string): string {
|
|
10
11
|
const parsed = new Date(isoDate).getTime();
|
|
@@ -63,11 +64,7 @@ export const getWorkspaceSubcommand = createSubcommand({
|
|
|
63
64
|
tui.header(`Workspace: ${workspace.name}`);
|
|
64
65
|
tui.newline();
|
|
65
66
|
tui.output(` ID: ${workspace.id}`);
|
|
66
|
-
|
|
67
|
-
if (workspace.description) {
|
|
68
|
-
tui.output(` Description: ${workspace.description}`);
|
|
69
|
-
}
|
|
70
|
-
tui.output(` Scope: ${workspace.scope}`);
|
|
67
|
+
printWorkspaceSummary(workspace);
|
|
71
68
|
tui.output(` Owner: ${workspace.ownerUserId}`);
|
|
72
69
|
tui.output(` Created: ${formatRelativeTime(workspace.createdAt)}`);
|
|
73
70
|
tui.output(` Updated: ${formatRelativeTime(workspace.updatedAt)}`);
|
|
@@ -2,6 +2,9 @@ import { createCommand } from '../../../types';
|
|
|
2
2
|
import { listSubcommand } from './list';
|
|
3
3
|
import { createWorkspaceSubcommand } from './create';
|
|
4
4
|
import { getWorkspaceSubcommand } from './get';
|
|
5
|
+
import { refreshWorkspaceSnapshotSubcommand } from './refresh';
|
|
6
|
+
import { updateWorkspaceSubcommand } from './update';
|
|
7
|
+
import { validateWorkspaceDependenciesSubcommand } from './validate-dependencies';
|
|
5
8
|
import { deleteWorkspaceSubcommand } from './delete';
|
|
6
9
|
import { getCommand } from '../../../command-prefix';
|
|
7
10
|
|
|
@@ -30,11 +33,18 @@ export const workspaceCommand = createCommand({
|
|
|
30
33
|
command: getCommand('coder workspace delete ws_abc123'),
|
|
31
34
|
description: 'Delete a workspace',
|
|
32
35
|
},
|
|
36
|
+
{
|
|
37
|
+
command: getCommand('coder workspace validate-dependencies git,nodejs'),
|
|
38
|
+
description: 'Validate workspace dependencies',
|
|
39
|
+
},
|
|
33
40
|
],
|
|
34
41
|
subcommands: [
|
|
35
42
|
listSubcommand,
|
|
36
43
|
createWorkspaceSubcommand,
|
|
37
44
|
getWorkspaceSubcommand,
|
|
45
|
+
updateWorkspaceSubcommand,
|
|
46
|
+
refreshWorkspaceSnapshotSubcommand,
|
|
47
|
+
validateWorkspaceDependenciesSubcommand,
|
|
38
48
|
deleteWorkspaceSubcommand,
|
|
39
49
|
],
|
|
40
50
|
});
|
|
@@ -83,6 +83,8 @@ export const listSubcommand = createSubcommand({
|
|
|
83
83
|
Name: w.name,
|
|
84
84
|
Scope: w.scope,
|
|
85
85
|
Repos: String(w.repoCount),
|
|
86
|
+
Deps: String(w.dependencies?.length ?? 0),
|
|
87
|
+
Snapshot: w.snapshot?.status ?? '',
|
|
86
88
|
Selections: String(w.selectionCount),
|
|
87
89
|
Created: formatRelativeTime(w.createdAt),
|
|
88
90
|
})),
|
|
@@ -91,6 +93,8 @@ export const listSubcommand = createSubcommand({
|
|
|
91
93
|
{ name: 'Name', alignment: 'left' },
|
|
92
94
|
{ name: 'Scope', alignment: 'center' },
|
|
93
95
|
{ name: 'Repos', alignment: 'right' },
|
|
96
|
+
{ name: 'Deps', alignment: 'right' },
|
|
97
|
+
{ name: 'Snapshot', alignment: 'left' },
|
|
94
98
|
{ name: 'Selections', alignment: 'right' },
|
|
95
99
|
{ name: 'Created', alignment: 'right' },
|
|
96
100
|
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { APIError, ValidationOutputError } from '@agentuity/core';
|
|
3
|
+
import { CoderClient } from '@agentuity/core/coder';
|
|
4
|
+
import { createSubcommand } from '../../../types';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { getCommand } from '../../../command-prefix';
|
|
7
|
+
import { ErrorCode } from '../../../errors';
|
|
8
|
+
import { printWorkspaceSummary } from './common';
|
|
9
|
+
|
|
10
|
+
export const refreshWorkspaceSnapshotSubcommand = createSubcommand({
|
|
11
|
+
name: 'refresh',
|
|
12
|
+
aliases: ['snapshot-refresh', 'rebuild'],
|
|
13
|
+
description: 'Refresh a Coder workspace snapshot',
|
|
14
|
+
tags: ['mutating', 'requires-auth'],
|
|
15
|
+
requires: { auth: true, org: true },
|
|
16
|
+
examples: [
|
|
17
|
+
{
|
|
18
|
+
command: getCommand('coder workspace refresh ws_abc123'),
|
|
19
|
+
description: 'Queue a workspace snapshot refresh',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
schema: {
|
|
23
|
+
args: z.object({
|
|
24
|
+
workspaceId: z.string().describe('Workspace ID to refresh'),
|
|
25
|
+
}),
|
|
26
|
+
options: z.object({
|
|
27
|
+
url: z.string().optional().describe('Coder API URL override'),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
async handler(ctx) {
|
|
31
|
+
const { args, opts, options } = ctx;
|
|
32
|
+
const client = new CoderClient({
|
|
33
|
+
apiKey: ctx.auth.apiKey,
|
|
34
|
+
url: opts?.url,
|
|
35
|
+
orgId: ctx.orgId,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const workspace = await client.refreshWorkspaceSnapshot(args.workspaceId);
|
|
40
|
+
if (options.json) {
|
|
41
|
+
return workspace;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
tui.success(`Workspace ${workspace.id} snapshot refresh queued.`);
|
|
45
|
+
tui.newline();
|
|
46
|
+
printWorkspaceSummary(workspace);
|
|
47
|
+
return workspace;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof ValidationOutputError) {
|
|
50
|
+
ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
|
|
51
|
+
ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
|
|
52
|
+
}
|
|
53
|
+
if (err instanceof APIError && err.status >= 400 && err.status < 500) {
|
|
54
|
+
tui.fatal(
|
|
55
|
+
`Failed to refresh workspace snapshot: ${err.message}`,
|
|
56
|
+
ErrorCode.VALIDATION_FAILED
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
tui.fatal(`Failed to refresh workspace snapshot: ${msg}`, ErrorCode.NETWORK_ERROR);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { APIError, ValidationInputError, ValidationOutputError } from '@agentuity/core';
|
|
3
|
+
import {
|
|
4
|
+
CoderClient,
|
|
5
|
+
CoderUpdateWorkspaceRequestSchema,
|
|
6
|
+
type CoderUpdateWorkspaceRequest,
|
|
7
|
+
} from '@agentuity/core/coder';
|
|
8
|
+
import { createSubcommand } from '../../../types';
|
|
9
|
+
import * as tui from '../../../tui';
|
|
10
|
+
import { getCommand } from '../../../command-prefix';
|
|
11
|
+
import { ErrorCode } from '../../../errors';
|
|
12
|
+
import { resolveGitHubRepo } from '../resolve-repo';
|
|
13
|
+
import {
|
|
14
|
+
formatWorkspaceValidationMessage,
|
|
15
|
+
hasWorkspaceUpdate,
|
|
16
|
+
parseCommaList,
|
|
17
|
+
printWorkspaceSummary,
|
|
18
|
+
readSetupScript,
|
|
19
|
+
} from './common';
|
|
20
|
+
|
|
21
|
+
export const updateWorkspaceSubcommand = createSubcommand({
|
|
22
|
+
name: 'update',
|
|
23
|
+
aliases: ['edit', 'patch'],
|
|
24
|
+
description: 'Update a Coder workspace',
|
|
25
|
+
tags: ['mutating', 'requires-auth'],
|
|
26
|
+
requires: { auth: true, org: true },
|
|
27
|
+
examples: [
|
|
28
|
+
{
|
|
29
|
+
command: getCommand('coder workspace update ws_abc123 --dependency git,nodejs'),
|
|
30
|
+
description: 'Update workspace dependencies',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
command: getCommand('coder workspace update ws_abc123 --setup-script-file ./setup.sh'),
|
|
34
|
+
description: 'Update the workspace setup script',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
schema: {
|
|
38
|
+
args: z.object({
|
|
39
|
+
workspaceId: z.string().describe('Workspace ID to update'),
|
|
40
|
+
}),
|
|
41
|
+
options: z.object({
|
|
42
|
+
url: z.string().optional().describe('Coder API URL override'),
|
|
43
|
+
name: z.string().optional().describe('Workspace name'),
|
|
44
|
+
description: z.string().optional().describe('Workspace description'),
|
|
45
|
+
scope: z.string().optional().describe('Workspace scope: user or org'),
|
|
46
|
+
repo: z.string().optional().describe('Repository URL to set'),
|
|
47
|
+
repoBranch: z.string().optional().describe('Branch for the repository'),
|
|
48
|
+
dependency: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Comma-separated APT dependencies to install into workspace snapshots'),
|
|
52
|
+
setupScript: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Inline shell script to run while preparing workspace snapshots'),
|
|
56
|
+
setupScriptFile: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Path to a shell script to run while preparing workspace snapshots'),
|
|
60
|
+
enabledAgents: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Comma-separated built-in/custom agents to include'),
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
async handler(ctx) {
|
|
67
|
+
const { args, opts, options } = ctx;
|
|
68
|
+
const client = new CoderClient({
|
|
69
|
+
apiKey: ctx.auth.apiKey,
|
|
70
|
+
url: opts?.url,
|
|
71
|
+
orgId: ctx.orgId,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const body: CoderUpdateWorkspaceRequest = {
|
|
75
|
+
...(opts?.name && { name: opts.name }),
|
|
76
|
+
...(opts?.description && { description: opts.description }),
|
|
77
|
+
...(opts?.scope && { scope: opts.scope as 'user' | 'org' }),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (opts?.repo) {
|
|
81
|
+
if (!options.json) tui.output('Resolving repository...');
|
|
82
|
+
try {
|
|
83
|
+
const resolved = await resolveGitHubRepo(client, opts.repo, opts?.repoBranch);
|
|
84
|
+
body.repos = [resolved];
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
tui.fatal(`Failed to resolve repository: ${msg}`, ErrorCode.VALIDATION_FAILED);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (opts?.dependency) {
|
|
92
|
+
body.dependencies = parseCommaList(opts.dependency);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const setupScript = await readSetupScript({
|
|
96
|
+
setupScript: opts?.setupScript,
|
|
97
|
+
setupScriptFile: opts?.setupScriptFile,
|
|
98
|
+
});
|
|
99
|
+
if (setupScript !== undefined) body.setupScript = setupScript;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
tui.fatal(`Failed to read setup script: ${msg}`, ErrorCode.VALIDATION_FAILED);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (opts?.enabledAgents) {
|
|
106
|
+
body.enabledAgents = parseCommaList(opts.enabledAgents);
|
|
107
|
+
}
|
|
108
|
+
if (!hasWorkspaceUpdate(body)) {
|
|
109
|
+
tui.fatal(
|
|
110
|
+
'Failed to update workspace: At least one field must be provided.',
|
|
111
|
+
ErrorCode.VALIDATION_FAILED
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const validationResult = CoderUpdateWorkspaceRequestSchema.safeParse(body);
|
|
116
|
+
if (!validationResult.success) {
|
|
117
|
+
ctx.logger.trace(
|
|
118
|
+
'Validation issues: %s',
|
|
119
|
+
JSON.stringify(validationResult.error.issues, null, 2)
|
|
120
|
+
);
|
|
121
|
+
tui.fatal(
|
|
122
|
+
`Failed to update workspace: ${formatWorkspaceValidationMessage(validationResult.error.issues)}`,
|
|
123
|
+
ErrorCode.VALIDATION_FAILED
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const updated = await client.updateWorkspace(args.workspaceId, validationResult.data);
|
|
129
|
+
|
|
130
|
+
if (options.json) {
|
|
131
|
+
return updated;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tui.success(`Workspace ${updated.id} updated.`);
|
|
135
|
+
tui.newline();
|
|
136
|
+
printWorkspaceSummary(updated);
|
|
137
|
+
return updated;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof ValidationInputError || err instanceof ValidationOutputError) {
|
|
140
|
+
ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
|
|
141
|
+
ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
|
|
142
|
+
tui.fatal(
|
|
143
|
+
`Failed to update workspace: ${formatWorkspaceValidationMessage(err.issues)}`,
|
|
144
|
+
ErrorCode.VALIDATION_FAILED
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (err instanceof APIError && err.status >= 400 && err.status < 500) {
|
|
148
|
+
tui.fatal(`Failed to update workspace: ${err.message}`, ErrorCode.VALIDATION_FAILED);
|
|
149
|
+
}
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
tui.fatal(`Failed to update workspace: ${msg}`, ErrorCode.NETWORK_ERROR);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { APIError, ValidationOutputError } from '@agentuity/core';
|
|
3
|
+
import { CoderClient } from '@agentuity/core/coder';
|
|
4
|
+
import { createSubcommand } from '../../../types';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { getCommand } from '../../../command-prefix';
|
|
7
|
+
import { ErrorCode } from '../../../errors';
|
|
8
|
+
import { parseCommaList } from './common';
|
|
9
|
+
|
|
10
|
+
export const validateWorkspaceDependenciesSubcommand = createSubcommand({
|
|
11
|
+
name: 'validate-dependencies',
|
|
12
|
+
aliases: ['validate-deps'],
|
|
13
|
+
description: 'Validate APT dependencies for Coder workspace snapshots',
|
|
14
|
+
tags: ['read-only', 'requires-auth'],
|
|
15
|
+
idempotent: true,
|
|
16
|
+
requires: { auth: true, org: true },
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
command: getCommand('coder workspace validate-dependencies git,nodejs'),
|
|
20
|
+
description: 'Validate dependency package names',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
schema: {
|
|
24
|
+
args: z.object({
|
|
25
|
+
dependencies: z.string().describe('Comma-separated APT dependencies to validate'),
|
|
26
|
+
}),
|
|
27
|
+
options: z.object({
|
|
28
|
+
url: z.string().optional().describe('Coder API URL override'),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
async handler(ctx) {
|
|
32
|
+
const { args, opts, options } = ctx;
|
|
33
|
+
const dependencies = parseCommaList(args.dependencies);
|
|
34
|
+
if (dependencies.length === 0) {
|
|
35
|
+
tui.fatal('At least one dependency is required.', ErrorCode.VALIDATION_FAILED);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const client = new CoderClient({
|
|
39
|
+
apiKey: ctx.auth.apiKey,
|
|
40
|
+
url: opts?.url,
|
|
41
|
+
orgId: ctx.orgId,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.validateWorkspaceDependencies(dependencies);
|
|
46
|
+
if (options.json) {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result.valid.length > 0) {
|
|
51
|
+
tui.success(`Valid dependencies: ${result.valid.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
if (result.invalid.length > 0) {
|
|
54
|
+
tui.error(`Invalid dependencies: ${result.invalid.length}`);
|
|
55
|
+
for (const invalid of result.invalid) {
|
|
56
|
+
tui.output(` - ${invalid.package}: ${invalid.error}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof ValidationOutputError) {
|
|
62
|
+
ctx.logger.trace('Validation response URL: %s', err.url ?? 'unknown');
|
|
63
|
+
ctx.logger.trace('Validation issues: %s', JSON.stringify(err.issues, null, 2));
|
|
64
|
+
}
|
|
65
|
+
if (err instanceof APIError && err.status >= 400 && err.status < 500) {
|
|
66
|
+
tui.fatal(
|
|
67
|
+
`Failed to validate dependencies: ${err.message}`,
|
|
68
|
+
ErrorCode.VALIDATION_FAILED
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
tui.fatal(`Failed to validate dependencies: ${msg}`, ErrorCode.NETWORK_ERROR);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates project-derived suggestions for resource names (DB / S3 bucket).
|
|
3
|
+
*
|
|
4
|
+
* The CLI shows these as a dim "press Enter to use ..." default in the create flow.
|
|
5
|
+
* If the user presses Enter, the suggestion is sent to the server. Otherwise the
|
|
6
|
+
* server is responsible for assigning a name when none is provided.
|
|
7
|
+
*
|
|
8
|
+
* Both suggestions are validated against the same rules the server enforces
|
|
9
|
+
* (`validateBucketName` / `validateDatabaseName` from `@agentuity/server`) so the
|
|
10
|
+
* happy path can never produce an invalid suggestion.
|
|
11
|
+
*/
|
|
12
|
+
import { validateBucketName, validateDatabaseName } from '@agentuity/server';
|
|
13
|
+
|
|
14
|
+
const BUCKET_MAX = 63;
|
|
15
|
+
const BUCKET_MIN = 3;
|
|
16
|
+
const DB_MAX = 63;
|
|
17
|
+
|
|
18
|
+
/** 3 lowercase alphanumeric chars, e.g. "k7p". */
|
|
19
|
+
function shortSuffix(): string {
|
|
20
|
+
// toString(36) yields [0-9a-z]; slice 3 chars after the "0." prefix.
|
|
21
|
+
const s = Math.random().toString(36).slice(2, 5);
|
|
22
|
+
// Pad in the (extremely unlikely) case the slice is shorter than 3 chars.
|
|
23
|
+
return s.length === 3 ? s : (s + '000').slice(0, 3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize a project name into the bucket-name alphabet:
|
|
28
|
+
* - lowercase
|
|
29
|
+
* - spaces / underscores / dots → hyphens
|
|
30
|
+
* - drop anything else
|
|
31
|
+
* - collapse and trim hyphens
|
|
32
|
+
* - strip reserved prefixes (`agentuity*`, `ag-*`, `ago-*`, `xn--`)
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeForBucket(name: string): string {
|
|
35
|
+
let out = name
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/[\s_.]+/g, '-')
|
|
39
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
40
|
+
.replace(/-+/g, '-')
|
|
41
|
+
.replace(/^-+|-+$/g, '');
|
|
42
|
+
|
|
43
|
+
// Strip reserved prefixes (rules from validateBucketName).
|
|
44
|
+
while (
|
|
45
|
+
out.startsWith('agentuity') ||
|
|
46
|
+
out.startsWith('ag-') ||
|
|
47
|
+
out.startsWith('ago-') ||
|
|
48
|
+
out.startsWith('xn--')
|
|
49
|
+
) {
|
|
50
|
+
if (out.startsWith('agentuity')) out = out.slice('agentuity'.length);
|
|
51
|
+
else if (out.startsWith('ago-')) out = out.slice('ago-'.length);
|
|
52
|
+
else if (out.startsWith('ag-')) out = out.slice('ag-'.length);
|
|
53
|
+
else if (out.startsWith('xn--')) out = out.slice('xn--'.length);
|
|
54
|
+
out = out.replace(/^-+/, '');
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sanitize a project name into the database-name alphabet:
|
|
61
|
+
* - lowercase
|
|
62
|
+
* - non `[a-z0-9_]` → `_`
|
|
63
|
+
* - collapse and trim underscores
|
|
64
|
+
* - ensure it starts with a letter or underscore (prepend `p_` otherwise)
|
|
65
|
+
* - strip reserved `pg_` prefix
|
|
66
|
+
*/
|
|
67
|
+
function sanitizeForDatabase(name: string): string {
|
|
68
|
+
let out = name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
72
|
+
.replace(/_+/g, '_')
|
|
73
|
+
.replace(/^_+|_+$/g, '');
|
|
74
|
+
|
|
75
|
+
if (!/^[a-z_]/.test(out)) {
|
|
76
|
+
out = out.length > 0 ? `p_${out}` : '';
|
|
77
|
+
}
|
|
78
|
+
while (out.startsWith('pg_')) {
|
|
79
|
+
out = out.slice(3).replace(/^_+/, '');
|
|
80
|
+
if (!/^[a-z_]/.test(out) && out.length > 0) out = `p_${out}`;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Truncate so the final string (`<base>-<suffixWithDash>`) fits within `max` chars
|
|
87
|
+
* while keeping the suffix intact and not ending on a hyphen.
|
|
88
|
+
*/
|
|
89
|
+
function truncateBaseHyphen(base: string, suffixWithDash: string, max: number): string {
|
|
90
|
+
const room = max - suffixWithDash.length;
|
|
91
|
+
if (room <= 0) return '';
|
|
92
|
+
let out = base.slice(0, room);
|
|
93
|
+
out = out.replace(/-+$/, '');
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Same as `truncateBaseHyphen` but for underscore-joined names (database). */
|
|
98
|
+
function truncateBaseUnderscore(base: string, suffixWithUnderscore: string, max: number): string {
|
|
99
|
+
const room = max - suffixWithUnderscore.length;
|
|
100
|
+
if (room <= 0) return '';
|
|
101
|
+
let out = base.slice(0, room);
|
|
102
|
+
out = out.replace(/_+$/, '');
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a suggested S3 bucket name derived from the project name.
|
|
108
|
+
* Format: `<sanitized-project>-storage-<3char>` (≤ 63 chars).
|
|
109
|
+
*
|
|
110
|
+
* Falls back to `bucket-<3char><3char>` if the project name produces nothing usable.
|
|
111
|
+
* Always returns a value that passes `validateBucketName`.
|
|
112
|
+
*/
|
|
113
|
+
export function suggestBucketName(projectName: string): string {
|
|
114
|
+
const sanitized = sanitizeForBucket(projectName);
|
|
115
|
+
|
|
116
|
+
// Try a few times in case sanitization + suffix happens to land on something invalid.
|
|
117
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
118
|
+
const suffix = `-storage-${shortSuffix()}`;
|
|
119
|
+
const base = truncateBaseHyphen(sanitized, suffix, BUCKET_MAX);
|
|
120
|
+
const candidate = base.length > 0 ? `${base}${suffix}` : `bucket${suffix}`;
|
|
121
|
+
if (candidate.length >= BUCKET_MIN && validateBucketName(candidate).valid) {
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pure fallback: short, always-valid generic name.
|
|
127
|
+
const fallback = `bucket-${shortSuffix()}${shortSuffix()}`;
|
|
128
|
+
return validateBucketName(fallback).valid ? fallback : `bucket-${shortSuffix()}aaa`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate a suggested PostgreSQL database name derived from the project name.
|
|
133
|
+
* Format: `<sanitized-project>_db_<3char>` (≤ 63 chars).
|
|
134
|
+
*
|
|
135
|
+
* Falls back to `db_<3char><3char>` if the project name produces nothing usable.
|
|
136
|
+
* Always returns a value that passes `validateDatabaseName`.
|
|
137
|
+
*/
|
|
138
|
+
export function suggestDatabaseName(projectName: string): string {
|
|
139
|
+
const sanitized = sanitizeForDatabase(projectName);
|
|
140
|
+
|
|
141
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
142
|
+
const suffix = `_db_${shortSuffix()}`;
|
|
143
|
+
const base = truncateBaseUnderscore(sanitized, suffix, DB_MAX);
|
|
144
|
+
const candidate = base.length > 0 ? `${base}${suffix}` : `db${suffix}`;
|
|
145
|
+
if (validateDatabaseName(candidate).valid) {
|
|
146
|
+
return candidate;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fallback = `db_${shortSuffix()}${shortSuffix()}`;
|
|
151
|
+
return validateDatabaseName(fallback).valid ? fallback : `db_${shortSuffix()}aaa`;
|
|
152
|
+
}
|