@agentuity/cli 1.0.6 → 1.0.7

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.
@@ -5,19 +5,63 @@ import { projectEnvUpdate, orgEnvUpdate } from '@agentuity/server';
5
5
  import { findExistingEnvFile, writeEnvFile, looksLikeSecret, isReservedAgentuityKey, isPublicVarKey, PUBLIC_VAR_PREFIXES, } from '../../../env-util';
6
6
  import { getCommand } from '../../../command-prefix';
7
7
  import { resolveOrgId, isOrgScope } from './org-util';
8
+ /**
9
+ * Parse env set arguments into key-value pairs.
10
+ * Supports two formats:
11
+ * - Legacy: KEY VALUE (exactly 2 args)
12
+ * - KEY=VALUE format: KEY1=VALUE1 [KEY2=VALUE2 ...]
13
+ */
14
+ function parseEnvArgs(rawArgs) {
15
+ if (rawArgs.length === 0) {
16
+ tui.fatal('No arguments provided. Usage: env set KEY VALUE or env set KEY=VALUE [KEY2=VALUE2 ...]');
17
+ }
18
+ // Check if first arg contains '=' — if so, treat ALL args as KEY=VALUE format
19
+ const firstArg = rawArgs[0];
20
+ if (firstArg.includes('=')) {
21
+ const pairs = [];
22
+ for (const arg of rawArgs) {
23
+ const eqIndex = arg.indexOf('=');
24
+ if (eqIndex === -1 || eqIndex === 0) {
25
+ tui.fatal(`Invalid format: '${arg}'. Expected KEY=VALUE format.`);
26
+ }
27
+ const key = arg.substring(0, eqIndex);
28
+ const value = arg.substring(eqIndex + 1);
29
+ if (!key) {
30
+ tui.fatal(`Invalid format: '${arg}'. Key cannot be empty.`);
31
+ }
32
+ pairs.push({ key, value });
33
+ }
34
+ return pairs;
35
+ }
36
+ // Legacy format: exactly 2 args = KEY VALUE
37
+ if (rawArgs.length === 2) {
38
+ const key = rawArgs[0].trim();
39
+ if (!key) {
40
+ tui.fatal('Invalid format: key cannot be empty. Usage: env set KEY VALUE or env set KEY=VALUE');
41
+ }
42
+ return [{ key, value: rawArgs[1] }];
43
+ }
44
+ // Ambiguous: 1 arg without '=' or 3+ args without '='
45
+ if (rawArgs.length === 1) {
46
+ tui.fatal(`Missing value for '${rawArgs[0]}'. Usage: env set KEY VALUE or env set KEY=VALUE`);
47
+ }
48
+ // 3+ args without '=' in first arg
49
+ tui.fatal('Multiple variables must use KEY=VALUE format. Usage: env set KEY1=VALUE1 KEY2=VALUE2 ...');
50
+ }
8
51
  const EnvSetResponseSchema = z.object({
9
52
  success: z.boolean().describe('Whether the operation succeeded'),
10
- key: z.string().describe('Environment variable key'),
53
+ keys: z.array(z.string()).describe('Environment variable keys that were set'),
11
54
  path: z
12
55
  .string()
13
56
  .optional()
14
- .describe('Local file path where env var was saved (project scope only)'),
15
- secret: z.boolean().describe('Whether the value was stored as a secret'),
16
- scope: z.enum(['project', 'org']).describe('The scope where the variable was set'),
57
+ .describe('Local file path where env vars were saved (project scope only)'),
58
+ secretKeys: z.array(z.string()).describe('Keys that were stored as secrets'),
59
+ envKeys: z.array(z.string()).describe('Keys that were stored as env vars'),
60
+ scope: z.enum(['project', 'org']).describe('The scope where the variables were set'),
17
61
  });
18
62
  export const setSubcommand = createSubcommand({
19
63
  name: 'set',
20
- description: 'Set an environment variable or secret',
64
+ description: 'Set one or more environment variables or secrets',
21
65
  tags: ['mutating', 'updates-resource', 'slow', 'requires-auth'],
22
66
  idempotent: true,
23
67
  requires: { auth: true, apiClient: true },
@@ -27,7 +71,15 @@ export const setSubcommand = createSubcommand({
27
71
  command: getCommand('env set NODE_ENV production'),
28
72
  description: 'Set environment variable',
29
73
  },
74
+ {
75
+ command: getCommand('env set NODE_ENV=production'),
76
+ description: 'Set using KEY=VALUE format',
77
+ },
30
78
  { command: getCommand('env set PORT 3000'), description: 'Set port number' },
79
+ {
80
+ command: getCommand('env set NODE_ENV=production LOG_LEVEL=info PORT=3000'),
81
+ description: 'Set multiple variables at once',
82
+ },
31
83
  {
32
84
  command: getCommand('env set API_KEY "sk_..." --secret'),
33
85
  description: 'Set a secret value',
@@ -39,8 +91,7 @@ export const setSubcommand = createSubcommand({
39
91
  ],
40
92
  schema: {
41
93
  args: z.object({
42
- key: z.string().describe('the environment variable key'),
43
- value: z.string().describe('the environment variable value'),
94
+ args: z.array(z.string()).describe('KEY VALUE or KEY=VALUE [KEY2=VALUE2 ...]'),
44
95
  }),
45
96
  options: z.object({
46
97
  secret: z
@@ -55,74 +106,119 @@ export const setSubcommand = createSubcommand({
55
106
  response: EnvSetResponseSchema,
56
107
  },
57
108
  async handler(ctx) {
58
- const { args, opts, apiClient, project, projectDir, config } = ctx;
109
+ const { args: cmdArgs, opts, apiClient, project, projectDir, config } = ctx;
59
110
  const useOrgScope = isOrgScope(opts?.org);
111
+ const forceSecret = opts?.secret ?? false;
60
112
  // Require project context if not using org scope
61
113
  if (!useOrgScope && !project) {
62
114
  tui.fatal('Project context required. Run from a project directory or use --org for organization scope.');
63
115
  }
64
- let isSecret = opts?.secret ?? false;
65
- const isPublic = isPublicVarKey(args.key);
66
- // Validate key doesn't start with reserved AGENTUITY_ prefix (except AGENTUITY_PUBLIC_)
67
- if (isReservedAgentuityKey(args.key)) {
68
- tui.fatal('Cannot set AGENTUITY_ prefixed variables. These are reserved for system use.');
116
+ const pairs = parseEnvArgs(cmdArgs.args);
117
+ // Validate all keys first
118
+ for (const pair of pairs) {
119
+ if (isReservedAgentuityKey(pair.key)) {
120
+ tui.fatal(`Cannot set AGENTUITY_ prefixed variables: '${pair.key}'. These are reserved for system use.`);
121
+ }
69
122
  }
70
- // Validate public vars cannot be secrets
71
- if (isSecret && isPublic) {
72
- tui.fatal(`Cannot set public variables as secrets. Keys with prefixes (${PUBLIC_VAR_PREFIXES.join(', ')}) are exposed to the frontend.`);
123
+ // Reject duplicate keys
124
+ const seenKeys = new Set();
125
+ for (const pair of pairs) {
126
+ if (seenKeys.has(pair.key)) {
127
+ tui.fatal(`Duplicate key '${pair.key}'. Each variable may only be specified once.`);
128
+ }
129
+ seenKeys.add(pair.key);
73
130
  }
74
- // Auto-detect if this looks like a secret and offer to store as secret
75
- // Skip auto-detect for public vars since they can never be secrets
76
- if (!isSecret && !isPublic && looksLikeSecret(args.key, args.value)) {
77
- tui.warning(`The variable '${args.key}' looks like it should be a secret.`);
78
- const storeAsSecret = await tui.confirm('Store as a secret instead?', true);
79
- if (storeAsSecret) {
80
- isSecret = true;
131
+ // Classify each pair as env or secret
132
+ const envPairs = {};
133
+ const secretPairs = {};
134
+ const secretKeysList = [];
135
+ const envKeysList = [];
136
+ for (const pair of pairs) {
137
+ const isPublic = isPublicVarKey(pair.key);
138
+ let isSecret = forceSecret;
139
+ // Validate public vars cannot be secrets
140
+ if (isSecret && isPublic) {
141
+ tui.fatal(`Cannot set public variables as secrets. '${pair.key}' (prefix ${PUBLIC_VAR_PREFIXES.join(', ')}) is exposed to the frontend.`);
142
+ }
143
+ // Auto-detect if this looks like a secret and offer to store as secret
144
+ // Skip auto-detect for public vars since they can never be secrets
145
+ if (!isSecret && !isPublic && looksLikeSecret(pair.key, pair.value)) {
146
+ if (pairs.length === 1) {
147
+ // Single pair: offer interactive prompt (existing behavior)
148
+ tui.warning(`The variable '${pair.key}' looks like it should be a secret.`);
149
+ const storeAsSecret = await tui.confirm('Store as a secret instead?', true);
150
+ if (storeAsSecret) {
151
+ isSecret = true;
152
+ }
153
+ }
154
+ else {
155
+ // Multiple pairs: auto-detect silently
156
+ isSecret = true;
157
+ tui.info(`Auto-detected '${pair.key}' as a secret`);
158
+ }
159
+ }
160
+ if (isSecret) {
161
+ secretPairs[pair.key] = pair.value;
162
+ secretKeysList.push(pair.key);
163
+ }
164
+ else {
165
+ envPairs[pair.key] = pair.value;
166
+ envKeysList.push(pair.key);
81
167
  }
82
168
  }
83
- const label = isSecret ? 'secret' : 'environment variable';
169
+ const totalCount = pairs.length;
170
+ const allKeys = [...envKeysList, ...secretKeysList];
171
+ const secretSuffix = secretKeysList.length > 0
172
+ ? ` (${secretKeysList.length} secret${secretKeysList.length !== 1 ? 's' : ''})`
173
+ : '';
84
174
  if (useOrgScope) {
85
175
  // Organization scope
86
176
  const orgId = await resolveOrgId(apiClient, config, opts.org);
87
- const updatePayload = isSecret
88
- ? { id: orgId, secrets: { [args.key]: args.value } }
89
- : { id: orgId, env: { [args.key]: args.value } };
90
- await tui.spinner(`Setting organization ${label} in cloud`, () => {
177
+ const updatePayload = { id: orgId };
178
+ if (Object.keys(envPairs).length > 0)
179
+ updatePayload.env = envPairs;
180
+ if (Object.keys(secretPairs).length > 0)
181
+ updatePayload.secrets = secretPairs;
182
+ await tui.spinner(`Setting ${totalCount} organization variable${totalCount !== 1 ? 's' : ''} in cloud`, () => {
91
183
  return orgEnvUpdate(apiClient, updatePayload);
92
184
  });
93
- tui.success(`Organization ${isSecret ? 'secret' : 'environment variable'} '${args.key}' set successfully (affects all projects in org)`);
185
+ tui.success(`Organization variable${totalCount !== 1 ? 's' : ''} set successfully: ${allKeys.join(', ')}${secretSuffix}`);
94
186
  return {
95
187
  success: true,
96
- key: args.key,
97
- secret: isSecret,
188
+ keys: allKeys,
189
+ secretKeys: secretKeysList,
190
+ envKeys: envKeysList,
98
191
  scope: 'org',
99
192
  };
100
193
  }
101
194
  else {
102
- // Project scope (existing behavior)
103
- const updatePayload = isSecret
104
- ? { id: project.projectId, secrets: { [args.key]: args.value } }
105
- : { id: project.projectId, env: { [args.key]: args.value } };
106
- await tui.spinner(`Setting ${label} in cloud`, () => {
195
+ // Project scope
196
+ const updatePayload = { id: project.projectId };
197
+ if (Object.keys(envPairs).length > 0)
198
+ updatePayload.env = envPairs;
199
+ if (Object.keys(secretPairs).length > 0)
200
+ updatePayload.secrets = secretPairs;
201
+ await tui.spinner(`Setting ${totalCount} variable${totalCount !== 1 ? 's' : ''} in cloud`, () => {
107
202
  return projectEnvUpdate(apiClient, updatePayload);
108
203
  });
109
204
  // Update local .env file only if we have a project directory
110
- // (not when using --project-id without being in a project folder)
111
205
  let envFilePath;
112
206
  if (projectDir) {
113
207
  envFilePath = await findExistingEnvFile(projectDir);
114
- // Write only the new key - writeEnvFile preserves existing keys by default
115
- await writeEnvFile(envFilePath, { [args.key]: args.value });
208
+ const allPairsForLocal = {
209
+ ...envPairs,
210
+ ...secretPairs,
211
+ };
212
+ await writeEnvFile(envFilePath, allPairsForLocal);
116
213
  }
117
- const successMsg = envFilePath
118
- ? `${isSecret ? 'Secret' : 'Environment variable'} '${args.key}' set successfully (cloud + ${envFilePath})`
119
- : `${isSecret ? 'Secret' : 'Environment variable'} '${args.key}' set successfully (cloud only)`;
120
- tui.success(successMsg);
214
+ const locationMsg = envFilePath ? ` (cloud + ${envFilePath})` : ' (cloud only)';
215
+ tui.success(`Variable${totalCount !== 1 ? 's' : ''} set successfully: ${allKeys.join(', ')}${secretSuffix}${locationMsg}`);
121
216
  return {
122
217
  success: true,
123
- key: args.key,
218
+ keys: allKeys,
124
219
  path: envFilePath,
125
- secret: isSecret,
220
+ secretKeys: secretKeysList,
221
+ envKeys: envKeysList,
126
222
  scope: 'project',
127
223
  };
128
224
  }
@@ -1 +1 @@
1
- {"version":3,"file":"set.js","sourceRoot":"","sources":["../../../../src/cmd/cloud/env/set.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"set.js","sourceRoot":"","sources":["../../../../src/cmd/cloud/env/set.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "type": "module",
@@ -41,9 +41,9 @@
41
41
  "prepublishOnly": "bun run clean && bun run build"
42
42
  },
43
43
  "dependencies": {
44
- "@agentuity/auth": "1.0.6",
45
- "@agentuity/core": "1.0.6",
46
- "@agentuity/server": "1.0.6",
44
+ "@agentuity/auth": "1.0.7",
45
+ "@agentuity/core": "1.0.7",
46
+ "@agentuity/server": "1.0.7",
47
47
  "@datasert/cronjs-parser": "^1.4.0",
48
48
  "@terascope/fetch-github-release": "^2.2.1",
49
49
  "@vitejs/plugin-react": "^5.1.2",
@@ -61,10 +61,10 @@
61
61
  "typescript": "^5.9.0",
62
62
  "vite": "^7.2.7",
63
63
  "zod": "^4.3.5",
64
- "@agentuity/frontend": "1.0.6"
64
+ "@agentuity/frontend": "1.0.7"
65
65
  },
66
66
  "devDependencies": {
67
- "@agentuity/test-utils": "1.0.6",
67
+ "@agentuity/test-utils": "1.0.7",
68
68
  "@types/adm-zip": "^0.5.7",
69
69
  "@types/bun": "latest",
70
70
  "@types/tar-fs": "^2.0.4",
@@ -0,0 +1,180 @@
1
+ import * as tui from '../../../tui';
2
+ import { maskSecret } from '../../../env-util';
3
+
4
+ export interface EnvDiffEntry {
5
+ key: string;
6
+ /** The value from the "source" side (the side being pushed/pulled from) */
7
+ sourceValue: string;
8
+ /** The value from the "target" side (the side being overwritten), undefined if new */
9
+ targetValue?: string;
10
+ /** Whether the source side stores this as a secret */
11
+ sourceIsSecret: boolean;
12
+ /** Whether the target side stores this as a secret (undefined if new, i.e. no target) */
13
+ targetIsSecret?: boolean;
14
+ }
15
+
16
+ export interface EnvDiff {
17
+ newEntries: EnvDiffEntry[];
18
+ changedEntries: EnvDiffEntry[];
19
+ unchangedEntries: EnvDiffEntry[];
20
+ }
21
+
22
+ /**
23
+ * Compute diff between source and target env/secrets.
24
+ * "Source" is where values come from; "target" is what will be overwritten.
25
+ *
26
+ * For push: source=local, target=remote
27
+ * For pull: source=cloud, target=local
28
+ */
29
+ export function computeEnvDiff(
30
+ sourceEnv: Record<string, string>,
31
+ sourceSecrets: Record<string, string>,
32
+ targetEnv: Record<string, string>,
33
+ targetSecrets: Record<string, string>
34
+ ): EnvDiff {
35
+ const newEntries: EnvDiffEntry[] = [];
36
+ const changedEntries: EnvDiffEntry[] = [];
37
+ const unchangedEntries: EnvDiffEntry[] = [];
38
+
39
+ // Check env vars
40
+ for (const key of Object.keys(sourceEnv)) {
41
+ const sourceValue = sourceEnv[key]!;
42
+ if (key in targetEnv) {
43
+ if (sourceValue === targetEnv[key]) {
44
+ unchangedEntries.push({
45
+ key,
46
+ sourceValue,
47
+ targetValue: targetEnv[key],
48
+ sourceIsSecret: false,
49
+ targetIsSecret: false,
50
+ });
51
+ } else {
52
+ changedEntries.push({
53
+ key,
54
+ sourceValue,
55
+ targetValue: targetEnv[key],
56
+ sourceIsSecret: false,
57
+ targetIsSecret: false,
58
+ });
59
+ }
60
+ } else if (key in targetSecrets) {
61
+ // Key exists but as a different type - treat as changed
62
+ changedEntries.push({
63
+ key,
64
+ sourceValue,
65
+ targetValue: targetSecrets[key],
66
+ sourceIsSecret: false,
67
+ targetIsSecret: true,
68
+ });
69
+ } else {
70
+ newEntries.push({ key, sourceValue, sourceIsSecret: false });
71
+ }
72
+ }
73
+
74
+ // Check secrets
75
+ for (const key of Object.keys(sourceSecrets)) {
76
+ const sourceValue = sourceSecrets[key]!;
77
+ if (key in targetSecrets) {
78
+ if (sourceValue === targetSecrets[key]) {
79
+ unchangedEntries.push({
80
+ key,
81
+ sourceValue,
82
+ targetValue: targetSecrets[key],
83
+ sourceIsSecret: true,
84
+ targetIsSecret: true,
85
+ });
86
+ } else {
87
+ changedEntries.push({
88
+ key,
89
+ sourceValue,
90
+ targetValue: targetSecrets[key],
91
+ sourceIsSecret: true,
92
+ targetIsSecret: true,
93
+ });
94
+ }
95
+ } else if (key in targetEnv) {
96
+ // Key exists but as a different type - treat as changed
97
+ changedEntries.push({
98
+ key,
99
+ sourceValue,
100
+ targetValue: targetEnv[key],
101
+ sourceIsSecret: true,
102
+ targetIsSecret: false,
103
+ });
104
+ } else {
105
+ newEntries.push({ key, sourceValue, sourceIsSecret: true });
106
+ }
107
+ }
108
+
109
+ return { newEntries, changedEntries, unchangedEntries };
110
+ }
111
+
112
+ function displayValue(value: string, isSecret: boolean): string {
113
+ return isSecret ? maskSecret(value) : value;
114
+ }
115
+
116
+ export interface DisplayEnvDiffOptions {
117
+ /** Label for the operation, shown in header. E.g. "push" or "pull" */
118
+ direction: 'push' | 'pull';
119
+ }
120
+
121
+ /**
122
+ * Display a diff view of env changes.
123
+ * In TTY mode, shows a rich color-coded diff.
124
+ * In non-TTY mode, shows simple text summary.
125
+ */
126
+ export function displayEnvDiff(diff: EnvDiff, options: DisplayEnvDiffOptions): void {
127
+ const { direction } = options;
128
+
129
+ // Non-TTY: simple summary
130
+ if (!tui.isTTYLike()) {
131
+ if (diff.newEntries.length > 0) {
132
+ tui.info(
133
+ `New variables: ${diff.newEntries.length} (${diff.newEntries.map((e) => e.key).join(', ')})`
134
+ );
135
+ }
136
+ if (diff.changedEntries.length > 0) {
137
+ const verb = direction === 'push' ? 'overwritten remotely' : 'overwritten locally';
138
+ tui.warning(
139
+ `Variables that will be ${verb}: ${diff.changedEntries.length} (${diff.changedEntries.map((e) => e.key).join(', ')})`
140
+ );
141
+ }
142
+ if (diff.unchangedEntries.length > 0) {
143
+ tui.info(`Unchanged variables: ${diff.unchangedEntries.length}`);
144
+ }
145
+ return;
146
+ }
147
+
148
+ // TTY: rich diff view
149
+ tui.newline();
150
+ tui.info('Environment variable changes:');
151
+ tui.newline();
152
+
153
+ // Sort all entries by key for consistent display
154
+ const allEntries = [
155
+ ...diff.newEntries.map((e) => ({ ...e, status: 'new' as const })),
156
+ ...diff.changedEntries.map((e) => ({ ...e, status: 'changed' as const })),
157
+ ...diff.unchangedEntries.map((e) => ({ ...e, status: 'unchanged' as const })),
158
+ ].sort((a, b) => a.key.localeCompare(b.key));
159
+
160
+ for (const entry of allEntries) {
161
+ const typeLabel = entry.sourceIsSecret ? 'secret' : 'env';
162
+ const val = displayValue(entry.sourceValue, entry.sourceIsSecret);
163
+
164
+ if (entry.status === 'new') {
165
+ const line = ` ${tui.colorSuccess('+')} ${tui.bold(entry.key)}=${val} ${tui.colorMuted(`(new, ${typeLabel})`)}`;
166
+ process.stderr.write(line + '\n');
167
+ } else if (entry.status === 'changed') {
168
+ const oldVal = displayValue(entry.targetValue!, entry.targetIsSecret ?? false);
169
+ // For push: show "remote_old → local_new" (replacing remote with local)
170
+ // For pull: show "local_old → cloud_new" (replacing local with cloud)
171
+ const line = ` ${tui.colorWarning('~')} ${tui.bold(entry.key)}=${oldVal} → ${val} ${tui.colorMuted(`(changed, ${typeLabel})`)}`;
172
+ process.stderr.write(line + '\n');
173
+ } else {
174
+ const line = ` ${tui.colorMuted(`= ${entry.key}=${val} (unchanged, ${typeLabel})`)}`;
175
+ process.stderr.write(line + '\n');
176
+ }
177
+ }
178
+
179
+ tui.newline();
180
+ }
@@ -2,13 +2,24 @@ import { z } from 'zod';
2
2
  import { createSubcommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
4
  import { projectGet, orgEnvGet } from '@agentuity/server';
5
- import { findExistingEnvFile, readEnvFile, writeEnvFile, mergeEnvVars } from '../../../env-util';
5
+ import {
6
+ findExistingEnvFile,
7
+ readEnvFile,
8
+ writeEnvFile,
9
+ mergeEnvVars,
10
+ splitEnvAndSecrets,
11
+ filterAgentuitySdkKeys,
12
+ } from '../../../env-util';
6
13
  import { getCommand } from '../../../command-prefix';
7
14
  import { resolveOrgId, isOrgScope } from './org-util';
15
+ import { computeEnvDiff, displayEnvDiff } from './env-diff';
8
16
 
9
17
  const EnvPullResponseSchema = z.object({
10
18
  success: z.boolean().describe('Whether pull succeeded'),
11
19
  pulled: z.number().describe('Number of items pulled'),
20
+ newCount: z.number().describe('Number of new variables added locally'),
21
+ changedCount: z.number().describe('Number of local variables overwritten'),
22
+ unchangedCount: z.number().describe('Number of unchanged variables'),
12
23
  path: z.string().describe('Local file path where variables were saved'),
13
24
  force: z.boolean().describe('Whether force mode was used'),
14
25
  scope: z.enum(['project', 'org']).describe('The scope from which variables were pulled'),
@@ -41,13 +52,15 @@ export const pullSubcommand = createSubcommand({
41
52
  async handler(ctx) {
42
53
  const { opts, apiClient, project, projectDir, config } = ctx;
43
54
  const useOrgScope = isOrgScope(opts?.org);
55
+ const forceMode = opts?.force ?? false;
44
56
 
45
57
  // Require project context for local file operations
46
58
  if (!projectDir) {
47
59
  tui.fatal('Project context required. Run from a project directory.');
48
60
  }
49
61
 
50
- let cloudEnv: Record<string, string>;
62
+ let cloudEnvVars: Record<string, string> = {};
63
+ let cloudSecretVars: Record<string, string> = {};
51
64
  let scope: 'project' | 'org';
52
65
  let cloudApiKey: string | undefined;
53
66
 
@@ -62,7 +75,8 @@ export const pullSubcommand = createSubcommand({
62
75
  }
63
76
  );
64
77
 
65
- cloudEnv = { ...orgData.env, ...orgData.secrets };
78
+ cloudEnvVars = orgData.env || {};
79
+ cloudSecretVars = orgData.secrets || {};
66
80
  scope = 'org';
67
81
  cloudApiKey = undefined; // Orgs don't have api_key
68
82
  } else {
@@ -77,7 +91,8 @@ export const pullSubcommand = createSubcommand({
77
91
  return projectGet(apiClient, { id: project.projectId, mask: false });
78
92
  });
79
93
 
80
- cloudEnv = { ...projectData.env, ...projectData.secrets };
94
+ cloudEnvVars = projectData.env || {};
95
+ cloudSecretVars = projectData.secrets || {};
81
96
  scope = 'project';
82
97
  cloudApiKey = projectData.api_key;
83
98
  }
@@ -89,9 +104,43 @@ export const pullSubcommand = createSubcommand({
89
104
  // Preserve local AGENTUITY_SDK_KEY
90
105
  const localSdkKey = localEnv.AGENTUITY_SDK_KEY;
91
106
 
107
+ // Split local env for diff comparison (excluding AGENTUITY_ reserved keys)
108
+ const localForDiff = { ...localEnv };
109
+ delete localForDiff.AGENTUITY_SDK_KEY;
110
+ const filteredLocal = filterAgentuitySdkKeys(localForDiff);
111
+ const { env: localEnvVars, secrets: localSecretVars } = splitEnvAndSecrets(filteredLocal);
112
+
113
+ // Compute diff: cloud (source) → local (target)
114
+ const diff = computeEnvDiff(cloudEnvVars, cloudSecretVars, localEnvVars, localSecretVars);
115
+
116
+ // Display diff
117
+ displayEnvDiff(diff, { direction: 'pull' });
118
+
119
+ // If force mode and there are changes, prompt for confirmation
120
+ if (forceMode && diff.changedEntries.length > 0) {
121
+ const confirmed = await tui.confirm(
122
+ `${diff.changedEntries.length} local variable${diff.changedEntries.length !== 1 ? 's' : ''} will be overwritten. Continue?`,
123
+ true
124
+ );
125
+ if (!confirmed) {
126
+ tui.info('Pull cancelled');
127
+ return {
128
+ success: false,
129
+ pulled: 0,
130
+ newCount: 0,
131
+ changedCount: 0,
132
+ unchangedCount: 0,
133
+ path: targetEnvPath,
134
+ force: forceMode,
135
+ scope,
136
+ };
137
+ }
138
+ }
139
+
92
140
  // Merge: cloud values override local if force=true, otherwise keep local
141
+ const cloudEnv = { ...cloudEnvVars, ...cloudSecretVars };
93
142
  let mergedEnv: Record<string, string>;
94
- if (opts?.force) {
143
+ if (forceMode) {
95
144
  // Cloud values take priority
96
145
  mergedEnv = mergeEnvVars(localEnv, cloudEnv);
97
146
  } else {
@@ -122,15 +171,26 @@ export const pullSubcommand = createSubcommand({
122
171
 
123
172
  const count = Object.keys(cloudEnv).length;
124
173
  const scopeLabel = useOrgScope ? 'organization' : 'project';
125
- tui.success(
126
- `Pulled ${count} environment variable${count !== 1 ? 's' : ''} from ${scopeLabel} to ${targetEnvPath}`
127
- );
174
+
175
+ // Update success message with diff counts
176
+ if (forceMode) {
177
+ tui.success(
178
+ `Pulled ${count} variable${count !== 1 ? 's' : ''} from ${scopeLabel} (${diff.newEntries.length} new, ${diff.changedEntries.length} updated, ${diff.unchangedEntries.length} unchanged)`
179
+ );
180
+ } else {
181
+ tui.success(
182
+ `Pulled ${count} variable${count !== 1 ? 's' : ''} from ${scopeLabel} (${diff.newEntries.length} new, ${diff.changedEntries.length} skipped, ${diff.unchangedEntries.length} unchanged)`
183
+ );
184
+ }
128
185
 
129
186
  return {
130
187
  success: true,
131
188
  pulled: count,
189
+ newCount: diff.newEntries.length,
190
+ changedCount: forceMode ? diff.changedEntries.length : 0,
191
+ unchangedCount: diff.unchangedEntries.length,
132
192
  path: targetEnvPath,
133
- force: opts?.force ?? false,
193
+ force: forceMode,
134
194
  scope,
135
195
  };
136
196
  },