@agentuity/cli 2.0.12 → 2.0.14

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 (87) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +1 -0
  3. package/dist/agent-detection.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +15 -8
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  8. package/dist/cmd/cloud/sandbox/create.js +46 -4
  9. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  10. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  11. package/dist/cmd/cloud/sandbox/exec.js +4 -3
  12. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  13. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  14. package/dist/cmd/cloud/sandbox/run.js +9 -5
  15. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  16. package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
  17. package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
  18. package/dist/cmd/coder/start.d.ts.map +1 -1
  19. package/dist/cmd/coder/start.js +1 -0
  20. package/dist/cmd/coder/start.js.map +1 -1
  21. package/dist/cmd/coder/workspace/common.d.ts +29 -0
  22. package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
  23. package/dist/cmd/coder/workspace/common.js +83 -0
  24. package/dist/cmd/coder/workspace/common.js.map +1 -0
  25. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  26. package/dist/cmd/coder/workspace/create.js +34 -37
  27. package/dist/cmd/coder/workspace/create.js.map +1 -1
  28. package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
  29. package/dist/cmd/coder/workspace/get.js +2 -5
  30. package/dist/cmd/coder/workspace/get.js.map +1 -1
  31. package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
  32. package/dist/cmd/coder/workspace/index.js +10 -0
  33. package/dist/cmd/coder/workspace/index.js.map +1 -1
  34. package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
  35. package/dist/cmd/coder/workspace/list.js +4 -0
  36. package/dist/cmd/coder/workspace/list.js.map +1 -1
  37. package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
  38. package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
  39. package/dist/cmd/coder/workspace/refresh.js +59 -0
  40. package/dist/cmd/coder/workspace/refresh.js.map +1 -0
  41. package/dist/cmd/coder/workspace/update.d.ts +2 -0
  42. package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
  43. package/dist/cmd/coder/workspace/update.js +131 -0
  44. package/dist/cmd/coder/workspace/update.js.map +1 -0
  45. package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
  46. package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
  47. package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
  48. package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
  49. package/dist/cmd/project/random-name.d.ts +17 -0
  50. package/dist/cmd/project/random-name.d.ts.map +1 -0
  51. package/dist/cmd/project/random-name.js +144 -0
  52. package/dist/cmd/project/random-name.js.map +1 -0
  53. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  54. package/dist/cmd/project/template-flow.js +181 -153
  55. package/dist/cmd/project/template-flow.js.map +1 -1
  56. package/dist/composite-logger.d.ts.map +1 -1
  57. package/dist/composite-logger.js +19 -0
  58. package/dist/composite-logger.js.map +1 -1
  59. package/dist/config.d.ts +18 -16
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +46 -16
  62. package/dist/config.js.map +1 -1
  63. package/dist/tui/prompt.d.ts +29 -0
  64. package/dist/tui/prompt.d.ts.map +1 -1
  65. package/dist/tui/prompt.js +180 -8
  66. package/dist/tui/prompt.js.map +1 -1
  67. package/package.json +7 -7
  68. package/src/agent-detection.ts +1 -0
  69. package/src/cli.ts +30 -8
  70. package/src/cmd/cloud/sandbox/create.ts +57 -4
  71. package/src/cmd/cloud/sandbox/exec.ts +4 -3
  72. package/src/cmd/cloud/sandbox/run.ts +9 -5
  73. package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
  74. package/src/cmd/coder/start.ts +1 -0
  75. package/src/cmd/coder/workspace/common.ts +103 -0
  76. package/src/cmd/coder/workspace/create.ts +39 -43
  77. package/src/cmd/coder/workspace/get.ts +2 -5
  78. package/src/cmd/coder/workspace/index.ts +10 -0
  79. package/src/cmd/coder/workspace/list.ts +4 -0
  80. package/src/cmd/coder/workspace/refresh.ts +63 -0
  81. package/src/cmd/coder/workspace/update.ts +154 -0
  82. package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
  83. package/src/cmd/project/random-name.ts +152 -0
  84. package/src/cmd/project/template-flow.ts +199 -161
  85. package/src/composite-logger.ts +20 -0
  86. package/src/config.ts +69 -19
  87. package/src/tui/prompt.ts +214 -8
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import { getCommand } from '../../../../command-prefix';
4
4
  import * as tui from '../../../../tui';
5
5
  import { createCommand } from '../../../../types';
6
- import { createSandboxClient, getSandboxRegion } from '../util';
6
+ import { createSandboxClient, resolveSandboxTarget } from '../util';
7
7
 
8
8
  const SNAPSHOT_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
9
9
  const SNAPSHOT_TAG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
@@ -88,15 +88,15 @@ export const createSubcommand = createCommand({
88
88
  }
89
89
 
90
90
  const profileName = config?.name;
91
- const region = await getSandboxRegion(
91
+ const sandboxInfo = await resolveSandboxTarget(
92
92
  logger,
93
93
  auth,
94
- profileName,
94
+ null,
95
95
  args.sandboxId,
96
- orgId,
96
+ profileName,
97
97
  config
98
98
  );
99
- const client = createSandboxClient(logger, auth, region);
99
+ const client = createSandboxClient(logger, auth, sandboxInfo.region);
100
100
 
101
101
  const snapshot = await snapshotCreate(client, {
102
102
  sandboxId: args.sandboxId,
@@ -104,7 +104,7 @@ export const createSubcommand = createCommand({
104
104
  description: opts.description,
105
105
  tag: opts.tag,
106
106
  public: opts.public,
107
- orgId,
107
+ orgId: sandboxInfo.orgId ?? orgId,
108
108
  });
109
109
 
110
110
  if (!options.json) {
@@ -391,6 +391,7 @@ export const startSubcommand = createSubcommand({
391
391
  };
392
392
  env.AGENTUITY_CODER_API_KEY = ctx.auth.apiKey;
393
393
  env.AGENTUITY_ORGID = ctx.orgId;
394
+ env.PI_SKIP_VERSION_CHECK = '1';
394
395
 
395
396
  if (opts?.agent) {
396
397
  env.AGENTUITY_CODER_AGENT = opts.agent;
@@ -0,0 +1,103 @@
1
+ import {
2
+ type CoderCreateWorkspaceRequest,
3
+ type CoderUpdateWorkspaceRequest,
4
+ type CoderWorkspaceDetail,
5
+ } from '@agentuity/core/coder';
6
+ import { StructuredError } from '@agentuity/core';
7
+ import * as tui from '../../../tui';
8
+
9
+ export const EMPTY_WORKSPACE_ERROR =
10
+ 'A workspace needs at least one repo, dependency, setup script, saved skill, skill bucket, or agent';
11
+ export const SetupScriptValidationError = StructuredError('SetupScriptValidationError')<{
12
+ message: string;
13
+ path?: string;
14
+ }>();
15
+
16
+ export function parseCommaList(value?: string): string[] {
17
+ return value
18
+ ? value
19
+ .split(',')
20
+ .map((item) => item.trim())
21
+ .filter(Boolean)
22
+ : [];
23
+ }
24
+
25
+ export async function readSetupScript(input: {
26
+ setupScript?: string;
27
+ setupScriptFile?: string;
28
+ }): Promise<string | undefined> {
29
+ if (input.setupScript !== undefined && input.setupScriptFile) {
30
+ throw new SetupScriptValidationError({
31
+ message: 'Use either --setup-script or --setup-script-file, not both.',
32
+ });
33
+ }
34
+ if (input.setupScript !== undefined) return input.setupScript;
35
+ if (!input.setupScriptFile) return undefined;
36
+ try {
37
+ return await Bun.file(input.setupScriptFile).text();
38
+ } catch (error) {
39
+ throw new SetupScriptValidationError({
40
+ message: `Failed to read setup script file "${input.setupScriptFile}": ${
41
+ error instanceof Error ? error.message : String(error)
42
+ }`,
43
+ path: input.setupScriptFile,
44
+ cause: error,
45
+ });
46
+ }
47
+ }
48
+
49
+ export function hasWorkspaceSelections(input: CoderCreateWorkspaceRequest): boolean {
50
+ return (
51
+ (input.repos?.length ?? 0) > 0 ||
52
+ (input.dependencies?.length ?? 0) > 0 ||
53
+ Boolean(input.setupScript?.trim()) ||
54
+ (input.savedSkillIds?.length ?? 0) > 0 ||
55
+ (input.skillBucketIds?.length ?? 0) > 0 ||
56
+ (input.enabledAgents?.length ?? 0) > 0
57
+ );
58
+ }
59
+
60
+ export function hasWorkspaceUpdate(input: CoderUpdateWorkspaceRequest): boolean {
61
+ return Object.keys(input).length > 0;
62
+ }
63
+
64
+ export function formatWorkspaceValidationMessage(issues: Array<{ message: string }>): string {
65
+ const messages = [...new Set(issues.map((issue) => issue.message).filter(Boolean))];
66
+ if (messages.length === 0) {
67
+ return 'Invalid workspace configuration';
68
+ }
69
+ if (messages.includes(EMPTY_WORKSPACE_ERROR)) {
70
+ return `${EMPTY_WORKSPACE_ERROR}. Use --repo, --dependency, --setup-script, or --enabled-agents.`;
71
+ }
72
+ return messages.join('; ');
73
+ }
74
+
75
+ export function printWorkspaceSummary(workspace: CoderWorkspaceDetail): void {
76
+ const enabledAgents = Array.isArray(workspace.enabledAgents)
77
+ ? workspace.enabledAgents.filter((name): name is string => typeof name === 'string')
78
+ : [];
79
+ const dependencies = Array.isArray(workspace.dependencies) ? workspace.dependencies : [];
80
+
81
+ tui.output(` Name: ${tui.bold(workspace.name)}`);
82
+ if (workspace.description) {
83
+ tui.output(` Description: ${workspace.description}`);
84
+ }
85
+ tui.output(` Scope: ${workspace.scope}`);
86
+ tui.output(` Repos: ${workspace.repoCount}`);
87
+ tui.output(` Selections: ${workspace.selectionCount}`);
88
+ if (dependencies.length > 0) {
89
+ tui.output(` Dependencies:${dependencies.length === 1 ? ` ${dependencies[0]}` : ''}`);
90
+ for (const dependency of dependencies.length === 1 ? [] : dependencies) {
91
+ tui.output(` - ${dependency}`);
92
+ }
93
+ }
94
+ if (workspace.setupScript) {
95
+ tui.output(' Setup: configured');
96
+ }
97
+ if (workspace.snapshot?.status) {
98
+ tui.output(` Snapshot: ${workspace.snapshot.status}`);
99
+ }
100
+ if (enabledAgents.length > 0) {
101
+ tui.output(` Agents: ${enabledAgents.join(', ')}`);
102
+ }
103
+ }
@@ -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
- const EMPTY_WORKSPACE_ERROR =
15
- 'A workspace needs at least one repo, saved skill, skill bucket, or agent';
16
-
17
- function hasWorkspaceSelections(input: CoderCreateWorkspaceRequest): boolean {
18
- return (
19
- (input.repos?.length ?? 0) > 0 ||
20
- (input.savedSkillIds?.length ?? 0) > 0 ||
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" --enabled-agents code-review --description "For frontend work" --scope org'
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 description and agents',
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
- tui.output(` Name: ${tui.bold(created.name)}`);
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
- tui.output(` Name: ${tui.bold(workspace.name)}`);
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
+ });