Skip to content

Commit de28f21

Browse files
committed
chore: merge main into preview
# Conflicts: # src/cli/commands/deploy/command.tsx
2 parents ecf8c3f + ce00f57 commit de28f21

6 files changed

Lines changed: 212 additions & 48 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { AgentCoreProjectSpec } from '../../../../schema';
2+
import { computeDeployAttrs } from '../utils.js';
3+
import { describe, expect, it } from 'vitest';
4+
5+
describe('computeDeployAttrs', () => {
6+
it('computes counts from a populated spec', () => {
7+
const projectSpec = {
8+
runtimes: [{}, {}],
9+
memories: [{}],
10+
credentials: [{}, {}, {}],
11+
evaluators: [{}],
12+
onlineEvalConfigs: [{}, {}],
13+
agentCoreGateways: [{ targets: [{}, {}] }, { targets: [{}] }],
14+
policyEngines: [{ policies: [{}, {}] }, { policies: [{}] }],
15+
} as unknown as Partial<AgentCoreProjectSpec>;
16+
17+
expect(computeDeployAttrs(projectSpec, 'diff')).toEqual({
18+
runtime_count: 2,
19+
memory_count: 1,
20+
credential_count: 3,
21+
evaluator_count: 1,
22+
online_eval_count: 2,
23+
gateway_count: 2,
24+
gateway_target_count: 3,
25+
policy_engine_count: 2,
26+
policy_count: 3,
27+
mode: 'diff',
28+
});
29+
});
30+
31+
it('returns zeros for empty spec', () => {
32+
expect(computeDeployAttrs({}, 'deploy')).toEqual({
33+
runtime_count: 0,
34+
memory_count: 0,
35+
credential_count: 0,
36+
evaluator_count: 0,
37+
online_eval_count: 0,
38+
gateway_count: 0,
39+
gateway_target_count: 0,
40+
policy_engine_count: 0,
41+
policy_count: 0,
42+
mode: 'deploy',
43+
});
44+
});
45+
46+
it('handles dry-run mode', () => {
47+
const projectSpec = { runtimes: [{}] } as unknown as Partial<AgentCoreProjectSpec>;
48+
const attrs = computeDeployAttrs(projectSpec, 'dry-run');
49+
50+
expect(attrs.runtime_count).toBe(1);
51+
expect(attrs.memory_count).toBe(0);
52+
expect(attrs.mode).toBe('dry-run');
53+
});
54+
});

src/cli/commands/deploy/command.tsx

Lines changed: 105 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
import { ConfigIO, serializeResult } from '../../../lib';
12
import { getErrorMessage } from '../../errors';
3+
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
24
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
35
import { requireProject, requireTTY } from '../../tui/guards';
46
import { DeployScreen } from '../../tui/screens/deploy/DeployScreen';
57
import { handleDeploy } from './actions';
6-
import { createSpinnerProgress } from './progress';
7-
import type { DeployOptions } from './types';
8+
import type { DeployOptions, DeployResult } from './types';
9+
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from './utils';
810
import { validateDeployOptions } from './validate';
911
import type { Command } from '@commander-js/extra-typings';
1012
import { Text, render } from 'ink';
1113
import React from 'react';
1214

15+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
16+
1317
function handleDeployTUI(options: { autoConfirm?: boolean; diffMode?: boolean } = {}): void {
1418
requireProject();
1519

@@ -37,8 +41,69 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
3741
process.exit(1);
3842
}
3943

40-
const progressUtil = options.progress ? createSpinnerProgress() : undefined;
41-
const onProgress = progressUtil?.onProgress;
44+
// Compute attrs upfront from project spec (available before deploy)
45+
const mode = options.diff ? 'diff' : options.plan ? 'dry-run' : 'deploy';
46+
const attrs = await new ConfigIO()
47+
.readProjectSpec()
48+
.then(spec => computeDeployAttrs(spec, mode))
49+
.catch(() => ({ ...DEFAULT_DEPLOY_ATTRS, mode }) as const);
50+
51+
const { deployResult } = await withCommandRunTelemetry('deploy', attrs, async () => {
52+
const result = await executeDeploy(options).catch(
53+
(e): DeployResult => ({ success: false, error: e instanceof Error ? e : new Error(getErrorMessage(e)) })
54+
);
55+
if (!result.success) {
56+
return { success: false as const, error: result.error, deployResult: result };
57+
}
58+
return { success: true as const, deployResult: result };
59+
});
60+
61+
// ALL output happens here, after telemetry
62+
if (!deployResult.success) {
63+
if (options.json) {
64+
console.log(JSON.stringify(serializeResult(deployResult)));
65+
} else {
66+
console.error(deployResult.error.message);
67+
if (deployResult.logPath) {
68+
console.error(`Log: ${deployResult.logPath}`);
69+
}
70+
}
71+
process.exit(1);
72+
}
73+
74+
printDeployResult(deployResult, options);
75+
76+
if (deployResult.postDeployWarnings && deployResult.postDeployWarnings.length > 0) {
77+
process.exit(2);
78+
}
79+
process.exit(0);
80+
}
81+
82+
async function executeDeploy(options: DeployOptions): Promise<DeployResult> {
83+
let spinner: NodeJS.Timeout | undefined;
84+
85+
// Progress callback for --progress mode
86+
const onProgress = options.progress
87+
? (step: string, status: 'start' | 'success' | 'error') => {
88+
if (spinner) {
89+
clearInterval(spinner);
90+
process.stdout.write('\r\x1b[K'); // Clear line
91+
}
92+
93+
if (status === 'start') {
94+
let i = 0;
95+
process.stdout.write(`${SPINNER_FRAMES[0]} ${step}...`);
96+
spinner = setInterval(() => {
97+
i = (i + 1) % SPINNER_FRAMES.length;
98+
process.stdout.write(`\r${SPINNER_FRAMES[i]} ${step}...`);
99+
}, 80);
100+
} else if (status === 'success') {
101+
console.log(`✓ ${step}`);
102+
} else {
103+
console.log(`✗ ${step}`);
104+
}
105+
}
106+
: undefined;
42107

43108
const onResourceEvent = options.verbose
44109
? (message: string) => {
@@ -56,57 +121,57 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
56121
onResourceEvent,
57122
});
58123

59-
progressUtil?.cleanup();
124+
if (spinner) {
125+
clearInterval(spinner);
126+
process.stdout.write('\r\x1b[K');
127+
}
128+
129+
return result;
130+
}
60131

132+
function printDeployResult(result: DeployResult & { success: true }, options: DeployOptions): void {
61133
if (options.json) {
62134
console.log(JSON.stringify(result));
63-
} else if (result.success) {
64-
if (options.diff) {
65-
console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`);
66-
} else if (options.plan) {
67-
console.log(`\n✓ Dry run complete for '${result.targetName}' (stack: ${result.stackName})`);
68-
console.log('\nRun `agentcore deploy` to deploy.');
69-
} else {
70-
console.log(`\n✓ Deployed to '${result.targetName}' (stack: ${result.stackName})`);
135+
return;
136+
}
71137

72-
// Show stack outputs in non-JSON mode
73-
if (result.outputs && Object.keys(result.outputs).length > 0) {
74-
console.log('\nOutputs:');
75-
for (const [key, value] of Object.entries(result.outputs)) {
76-
console.log(` ${key}: ${value}`);
77-
}
78-
}
138+
if (options.diff) {
139+
console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`);
140+
} else if (options.plan) {
141+
console.log(`\n✓ Dry run complete for '${result.targetName}' (stack: ${result.stackName})`);
142+
console.log('\nRun `agentcore deploy` to deploy.');
143+
} else {
144+
console.log(`\n✓ Deployed to '${result.targetName}' (stack: ${result.stackName})`);
79145

80-
if (result.postDeployWarnings && result.postDeployWarnings.length > 0) {
81-
console.log('\n⚠ Post-deploy warnings:');
82-
for (const warning of result.postDeployWarnings) {
83-
console.log(` ${warning}`);
84-
}
146+
// Show stack outputs in non-JSON mode
147+
if (result.outputs && Object.keys(result.outputs).length > 0) {
148+
console.log('\nOutputs:');
149+
for (const [key, value] of Object.entries(result.outputs)) {
150+
console.log(` ${key}: ${value}`);
85151
}
152+
}
86153

87-
if (result.notes && result.notes.length > 0) {
88-
for (const note of result.notes) {
89-
console.log(`\nNote: ${note}`);
90-
}
154+
if (result.postDeployWarnings && result.postDeployWarnings.length > 0) {
155+
console.log('\n⚠ Post-deploy warnings:');
156+
for (const warning of result.postDeployWarnings) {
157+
console.log(` ${warning}`);
91158
}
159+
}
92160

93-
if (result.nextSteps && result.nextSteps.length > 0) {
94-
console.log(`Next: ${result.nextSteps.join(' | ')}`);
161+
if (result.notes && result.notes.length > 0) {
162+
for (const note of result.notes) {
163+
console.log(`\nNote: ${note}`);
95164
}
96165
}
97166

98-
if (result.logPath) {
99-
console.log(`\nLog: ${result.logPath}`);
100-
}
101-
} else {
102-
console.error(result.error);
103-
if (result.logPath) {
104-
console.error(`Log: ${result.logPath}`);
167+
if (result.nextSteps && result.nextSteps.length > 0) {
168+
console.log(`Next: ${result.nextSteps.join(' | ')}`);
105169
}
106170
}
107171

108-
const hasPostDeployWarnings = result.success && result.postDeployWarnings && result.postDeployWarnings.length > 0;
109-
process.exit(result.success ? (hasPostDeployWarnings ? 2 : 0) : 1);
172+
if (result.logPath) {
173+
console.log(`\nLog: ${result.logPath}`);
174+
}
110175
}
111176

112177
export const registerDeploy = (program: Command) => {

src/cli/commands/deploy/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { AgentCoreProjectSpec } from '../../../schema';
2+
3+
export type DeployMode = 'deploy' | 'dry-run' | 'diff';
4+
5+
export const DEFAULT_DEPLOY_ATTRS = {
6+
runtime_count: 0,
7+
memory_count: 0,
8+
credential_count: 0,
9+
evaluator_count: 0,
10+
online_eval_count: 0,
11+
gateway_count: 0,
12+
gateway_target_count: 0,
13+
policy_engine_count: 0,
14+
policy_count: 0,
15+
mode: 'deploy' as DeployMode,
16+
};
17+
18+
export function computeDeployAttrs(projectSpec: Partial<AgentCoreProjectSpec>, mode: DeployMode) {
19+
const gateways = projectSpec.agentCoreGateways ?? [];
20+
const policyEngines = projectSpec.policyEngines ?? [];
21+
return {
22+
runtime_count: (projectSpec.runtimes ?? []).length,
23+
memory_count: (projectSpec.memories ?? []).length,
24+
credential_count: (projectSpec.credentials ?? []).length,
25+
evaluator_count: (projectSpec.evaluators ?? []).length,
26+
online_eval_count: (projectSpec.onlineEvalConfigs ?? []).length,
27+
gateway_count: gateways.length,
28+
gateway_target_count: gateways.reduce((sum, g) => sum + (g.targets ?? []).length, 0),
29+
policy_engine_count: policyEngines.length,
30+
policy_count: policyEngines.reduce((sum, pe) => sum + (pe.policies ?? []).length, 0),
31+
mode,
32+
};
33+
}

src/cli/telemetry/schemas/__tests__/command-run.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('COMMAND_SCHEMAS', () => {
4747
gateway_target_count: 3,
4848
policy_engine_count: 0,
4949
policy_count: 0,
50-
has_diff: true,
50+
mode: 'diff',
5151
};
5252
expect(COMMAND_SCHEMAS.deploy.parse(attrs)).toEqual(attrs);
5353
});
@@ -64,7 +64,7 @@ describe('COMMAND_SCHEMAS', () => {
6464
gateway_target_count: 0,
6565
policy_engine_count: 0,
6666
policy_count: 0,
67-
has_diff: false,
67+
mode: 'deploy',
6868
})
6969
).toThrow();
7070
});
@@ -81,7 +81,7 @@ describe('COMMAND_SCHEMAS', () => {
8181
gateway_target_count: 0,
8282
policy_engine_count: 0,
8383
policy_count: 0,
84-
has_diff: false,
84+
mode: 'deploy',
8585
})
8686
).toThrow();
8787
});

src/cli/telemetry/schemas/command-run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const DeployAttrs = safeSchema({
100100
gateway_target_count: Count,
101101
policy_engine_count: Count,
102102
policy_count: Count,
103-
has_diff: z.boolean(),
103+
mode: z.enum(['deploy', 'dry-run', 'diff']),
104104
});
105105

106106
const DevAttrs = safeSchema({

0 commit comments

Comments
 (0)