@getbrevo/cli 1.1.0 → 2.0.0

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 (75) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +19 -2
  3. package/agent-context/AGENTS.md +16 -5
  4. package/agent-context/SKILL.md +17 -3
  5. package/dist/api/client.d.ts +2 -0
  6. package/dist/api/client.d.ts.map +1 -1
  7. package/dist/api/client.js +50 -36
  8. package/dist/api/client.js.map +1 -1
  9. package/dist/bin/index.js +19 -4
  10. package/dist/bin/index.js.map +1 -1
  11. package/dist/commands/app/create.d.ts.map +1 -1
  12. package/dist/commands/app/create.js +240 -221
  13. package/dist/commands/app/create.js.map +1 -1
  14. package/dist/commands/app/delete.d.ts.map +1 -1
  15. package/dist/commands/app/delete.js +81 -62
  16. package/dist/commands/app/delete.js.map +1 -1
  17. package/dist/commands/app/list.d.ts.map +1 -1
  18. package/dist/commands/app/list.js +10 -3
  19. package/dist/commands/app/list.js.map +1 -1
  20. package/dist/commands/app/scaffold.d.ts.map +1 -1
  21. package/dist/commands/app/scaffold.js +12 -2
  22. package/dist/commands/app/scaffold.js.map +1 -1
  23. package/dist/commands/app/scopes.d.ts.map +1 -1
  24. package/dist/commands/app/scopes.js +2 -0
  25. package/dist/commands/app/scopes.js.map +1 -1
  26. package/dist/commands/app/start.d.ts.map +1 -1
  27. package/dist/commands/app/start.js +7 -0
  28. package/dist/commands/app/start.js.map +1 -1
  29. package/dist/commands/app/update.d.ts.map +1 -1
  30. package/dist/commands/app/update.js +297 -266
  31. package/dist/commands/app/update.js.map +1 -1
  32. package/dist/commands/login.d.ts.map +1 -1
  33. package/dist/commands/login.js +154 -138
  34. package/dist/commands/login.js.map +1 -1
  35. package/dist/commands/whoami.d.ts.map +1 -1
  36. package/dist/commands/whoami.js +27 -20
  37. package/dist/commands/whoami.js.map +1 -1
  38. package/dist/lang/en.d.ts +23 -1
  39. package/dist/lang/en.d.ts.map +1 -1
  40. package/dist/lang/en.js +25 -1
  41. package/dist/lang/en.js.map +1 -1
  42. package/dist/lib/auth-guard.d.ts.map +1 -1
  43. package/dist/lib/auth-guard.js +10 -1
  44. package/dist/lib/auth-guard.js.map +1 -1
  45. package/dist/lib/constants.d.ts +10 -3
  46. package/dist/lib/constants.d.ts.map +1 -1
  47. package/dist/lib/constants.js +19 -4
  48. package/dist/lib/constants.js.map +1 -1
  49. package/dist/lib/logger.d.ts.map +1 -1
  50. package/dist/lib/logger.js +16 -3
  51. package/dist/lib/logger.js.map +1 -1
  52. package/dist/lib/telemetry.d.ts +9 -0
  53. package/dist/lib/telemetry.d.ts.map +1 -0
  54. package/dist/lib/telemetry.js +57 -0
  55. package/dist/lib/telemetry.js.map +1 -0
  56. package/dist/lib/update-notifier.d.ts +3 -0
  57. package/dist/lib/update-notifier.d.ts.map +1 -1
  58. package/dist/lib/update-notifier.js +77 -34
  59. package/dist/lib/update-notifier.js.map +1 -1
  60. package/dist/lib/validators.d.ts +6 -0
  61. package/dist/lib/validators.d.ts.map +1 -1
  62. package/dist/lib/validators.js +10 -0
  63. package/dist/lib/validators.js.map +1 -1
  64. package/dist/services/app.d.ts +1 -1
  65. package/dist/services/app.d.ts.map +1 -1
  66. package/dist/services/app.js +6 -6
  67. package/dist/services/app.js.map +1 -1
  68. package/dist/services/scopes-html.d.ts.map +1 -1
  69. package/dist/services/scopes-html.js +208 -29
  70. package/dist/services/scopes-html.js.map +1 -1
  71. package/dist/services/skill.js +1 -1
  72. package/dist/services/skill.js.map +1 -1
  73. package/dist/types.d.ts +2 -2
  74. package/dist/types.d.ts.map +1 -1
  75. package/package.json +1 -2
@@ -48,196 +48,230 @@ const container_1 = require("../../container");
48
48
  const ui_1 = require("../../lib/ui");
49
49
  const config_1 = require("../../lib/config");
50
50
  const validators_1 = require("../../lib/validators");
51
+ const constants_1 = require("../../lib/constants");
51
52
  const inquirer_1 = __importDefault(require("inquirer"));
52
- exports.updateCommand = (0, command_handler_1.withCommandHandler)(async (options) => {
53
- const config = (0, config_1.readProjectConfig)();
54
- const hasFlags = !!(options.name !== undefined ||
55
- (options.redirectUri && options.redirectUri.length > 0) ||
56
- options.logoUri !== undefined ||
57
- (options.scope && options.scope.length > 0));
58
- // Validate --name if provided (even empty strings)
59
- if (options.name !== undefined) {
60
- const nameCheck = (0, validators_1.validateAppName)(options.name);
61
- if (nameCheck !== true)
62
- throw new errors_1.CliError(nameCheck);
53
+ const NON_INTERACTIVE_CONFIRM_ERROR = 'Cannot prompt for confirmation in non-interactive mode. Use --yes or --json to skip.';
54
+ // File exists but readProjectConfig returned null — invalid JSON or missing appId
55
+ function assertConfigFileUsable() {
56
+ const configPath = path.resolve(process.cwd(), 'app-config.json');
57
+ if (!fs.existsSync(configPath))
58
+ return;
59
+ let raw;
60
+ try {
61
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
63
62
  }
64
- // readProjectConfig normalizes appId (trim + coerce finite numbers, reject
65
- // empty) so `config.appId` is a guaranteed non-empty string whenever `config`
66
- // is non-null.
67
- const configAppId = config?.appId;
68
- const configAppIdValid = !!configAppId;
69
- // app-config.json is only usable when it describes the app being updated.
70
- // When --app-id differs from config.appId, the local file belongs to a
71
- // different app and must not be used as the update payload or merge base.
72
- const configMatches = configAppIdValid && (!options.appId || options.appId === configAppId);
73
- // No flags and no usable config: emit a specific error.
74
- if (!hasFlags && !configMatches) {
75
- // File exists but appId is empty/invalid — surface that over a
76
- // mismatch error, which would otherwise quote a garbage configAppId.
77
- if (config != null && !configAppIdValid) {
78
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_INVALID_APP_ID);
79
- }
80
- if (config != null && options.appId && options.appId !== configAppId) {
81
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_APP_ID_MISMATCH(options.appId, configAppId));
82
- }
83
- const configExists = fs.existsSync(path.resolve(process.cwd(), 'app-config.json'));
84
- if (configExists) {
85
- // File exists but readProjectConfig returned null — invalid JSON or missing appId
86
- let raw;
87
- try {
88
- raw = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'app-config.json'), 'utf-8'));
89
- }
90
- catch {
91
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_INVALID_JSON);
92
- }
93
- if (!raw ||
94
- typeof raw !== 'object' ||
95
- !('appId' in raw) ||
96
- !raw.appId) {
97
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_MISSING_APP_ID);
98
- }
99
- }
100
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_NOTHING_TO_UPDATE);
63
+ catch {
64
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_INVALID_JSON);
101
65
  }
102
- // Resolve appId: flag > config
103
- let appId;
104
- if (options.appId) {
105
- appId = options.appId;
66
+ if (!raw ||
67
+ typeof raw !== 'object' ||
68
+ !('appId' in raw) ||
69
+ !raw.appId) {
70
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_MISSING_APP_ID);
106
71
  }
107
- else if (configAppIdValid) {
108
- appId = configAppId;
72
+ }
73
+ // No flags and no usable config: emit a specific error.
74
+ function throwNoUpdateSource(config, configAppIdValid, configAppId, flagAppId) {
75
+ // File exists but appId is empty/invalid — surface that over a
76
+ // mismatch error, which would otherwise quote a garbage configAppId.
77
+ if (config != null && !configAppIdValid) {
78
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_INVALID_APP_ID);
79
+ }
80
+ if (config != null && flagAppId && flagAppId !== configAppId) {
81
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_APP_ID_MISMATCH(flagAppId, configAppId));
109
82
  }
110
- else if (config) {
83
+ assertConfigFileUsable();
84
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_NOTHING_TO_UPDATE);
85
+ }
86
+ // Resolve appId: flag > config
87
+ function resolveAppId(flagAppId, configAppId, config) {
88
+ if (flagAppId)
89
+ return flagAppId;
90
+ if (configAppId)
91
+ return configAppId;
92
+ if (config)
111
93
  throw new errors_1.CliError(en_1.messages.APP_UPDATE_INVALID_APP_ID);
94
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_NO_APP_RESOLVED);
95
+ }
96
+ async function confirmUpdate() {
97
+ const { confirmed } = await inquirer_1.default.prompt([
98
+ {
99
+ type: 'confirm',
100
+ name: 'confirmed',
101
+ message: en_1.messages.APP_UPDATE_CONFIRM,
102
+ default: true,
103
+ },
104
+ ]);
105
+ if (!confirmed) {
106
+ (0, logger_1.logInfo)(`\n ${en_1.messages.APP_UPDATE_CANCELLED}\n`);
107
+ return false;
112
108
  }
113
- if (!appId) {
114
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_NO_APP_RESOLVED);
109
+ return true;
110
+ }
111
+ function reportUpdateResult(params, jsonMode) {
112
+ if (jsonMode) {
113
+ (0, json_output_1.jsonOutput)({
114
+ app_id: params.appId,
115
+ name: params.name,
116
+ redirect_uris: params.redirectUris,
117
+ scopes: params.scopes,
118
+ ...(params.logoUri ? { logo_uri: params.logoUri } : {}),
119
+ });
120
+ return;
115
121
  }
116
- // Write back only when config describes the app being updated.
117
- const shouldWriteBack = configMatches;
118
- // No flags provided: push full config from app-config.json.
119
- // (config is guaranteed non-null here — the !hasFlags && !config case is handled above)
120
- if (!hasFlags) {
121
- const redirectUrls = config.auth?.redirectUrls;
122
- if (!redirectUrls || redirectUrls.length === 0) {
123
- throw new errors_1.CliError(en_1.messages.APP_UPDATE_NO_REDIRECT_URLS);
124
- }
125
- validateRedirectUrls(redirectUrls);
126
- const nextScopes = config.auth?.scopes ?? [];
127
- (0, validators_1.validateScopes)(nextScopes);
128
- if (!options.json) {
129
- // Fail fast before the network fetch when we'd have nowhere to show the diff.
130
- if (!options.yes && !process.stdin.isTTY) {
131
- throw new errors_1.CliError('Cannot prompt for confirmation in non-interactive mode. Use --yes or --json to skip.');
132
- }
133
- // Fetch current remote state so the summary can show deltas vs. what the
134
- // push will apply. Hard-fail on fetch error — the user asked for a diff.
135
- const remote = await fetchExistingApp(appId, options.json);
136
- renderUpdateSummary({
137
- appId,
138
- currentName: remote.name,
139
- nextName: config.appName,
140
- currentUrls: remote.redirect_uris ?? [],
141
- nextUrls: redirectUrls,
142
- currentLogoUri: remote.logo_uri,
143
- nextLogoUri: config.logoUri,
144
- currentScopes: remote.scopes ?? [],
145
- nextScopes,
146
- });
147
- }
148
- if (!options.json && !options.yes) {
149
- const { confirmed } = await inquirer_1.default.prompt([
150
- {
151
- type: 'confirm',
152
- name: 'confirmed',
153
- message: en_1.messages.APP_UPDATE_CONFIRM,
154
- default: true,
155
- },
156
- ]);
157
- if (!confirmed) {
158
- (0, logger_1.logInfo)(`\n ${en_1.messages.APP_UPDATE_CANCELLED}\n`);
159
- return;
160
- }
122
+ (0, logger_1.logSuccess)(en_1.messages.APP_UPDATE_SUCCESS);
123
+ if (params.name) {
124
+ (0, logger_1.logInfo)(` Name: ${params.name}`);
125
+ }
126
+ (0, logger_1.logInfo)(` Redirect URLs: ${params.redirectUris.length > 0 ? params.redirectUris.join(', ') : '(none)'}`);
127
+ (0, logger_1.logInfo)(` Scopes: ${params.scopes.length > 0 ? params.scopes.join(', ') : '(none)'}`);
128
+ if (params.logoUri) {
129
+ (0, logger_1.logInfo)(` Logo URL: ${params.logoUri}`);
130
+ }
131
+ if (params.configWrittenBack) {
132
+ (0, logger_1.logInfo)(' app-config.json updated.');
133
+ }
134
+ process.stdout.write('\n');
135
+ }
136
+ // No flags provided: push full config from app-config.json.
137
+ async function pushFullConfig(appId, config, options) {
138
+ const redirectUrls = config.auth?.redirectUrls;
139
+ if (!redirectUrls || redirectUrls.length === 0) {
140
+ throw new errors_1.CliError(en_1.messages.APP_UPDATE_NO_REDIRECT_URLS);
141
+ }
142
+ validateRedirectUrls(redirectUrls);
143
+ const nextScopes = config.auth?.scopes ?? [];
144
+ (0, validators_1.validateScopes)(nextScopes);
145
+ // The legacy 'all' scope is deprecated — block the push and point at the
146
+ // migration path (--scope). No silent rewrite, no escape hatch (BEX-214).
147
+ if ((0, validators_1.containsLegacyAllScope)(nextScopes)) {
148
+ throw new errors_1.CliError(en_1.messages.LEGACY_ALL_SCOPE_DEPRECATED_BLOCK);
149
+ }
150
+ if (!options.json) {
151
+ // Fail fast before the network fetch when we'd have nowhere to show the diff.
152
+ if (!options.yes && !process.stdin.isTTY) {
153
+ throw new errors_1.CliError(NON_INTERACTIVE_CONFIRM_ERROR);
161
154
  }
162
- const spinner = (0, ui_1.createSpinner)('Updating app...', { silent: options.json });
163
- await container_1.appService.updateApp(appId, {
164
- name: config.appName,
165
- redirect_uris: redirectUrls,
166
- scopes: nextScopes,
167
- ...(config.logoUri ? { logo_uri: config.logoUri } : {}),
155
+ // Fetch current remote state so the summary can show deltas vs. what the
156
+ // push will apply. Hard-fail on fetch error — the user asked for a diff.
157
+ const remote = await fetchExistingApp(appId, options.json);
158
+ renderUpdateSummary({
159
+ appId,
160
+ currentName: remote.name,
161
+ nextName: config.appName,
162
+ currentUrls: remote.redirect_uris ?? [],
163
+ nextUrls: redirectUrls,
164
+ currentLogoUri: remote.logo_uri,
165
+ nextLogoUri: config.logoUri,
166
+ currentScopes: remote.scopes ?? [],
167
+ nextScopes,
168
168
  });
169
- spinner.stop();
170
- if (config.appName)
171
- (0, config_1.saveAppName)(appId, config.appName);
172
- if (options.json) {
173
- (0, json_output_1.jsonOutput)({
174
- app_id: appId,
175
- name: config.appName,
176
- redirect_uris: redirectUrls,
177
- scopes: nextScopes,
178
- ...(config.logoUri ? { logo_uri: config.logoUri } : {}),
179
- });
180
- return;
181
- }
182
- (0, logger_1.logSuccess)(en_1.messages.APP_UPDATE_SUCCESS);
183
- if (config.appName) {
184
- (0, logger_1.logInfo)(` Name: ${config.appName}`);
185
- }
186
- (0, logger_1.logInfo)(` Redirect URLs: ${redirectUrls.join(', ')}`);
187
- (0, logger_1.logInfo)(` Scopes: ${nextScopes.length > 0 ? nextScopes.join(', ') : '(none)'}`);
188
- if (config.logoUri) {
189
- (0, logger_1.logInfo)(` Logo URL: ${config.logoUri}`);
190
- }
191
- process.stdout.write('\n');
169
+ }
170
+ if (!options.json && !options.yes && !(await confirmUpdate())) {
192
171
  return;
193
172
  }
194
- // Flags provided: merge with existing values
195
- let existingName;
196
- let existingRedirectUrls = [];
197
- let existingLogoUri;
198
- let existingScopes = [];
173
+ const spinner = (0, ui_1.createSpinner)('Updating app...', { silent: options.json });
174
+ await container_1.appService.updateApp(appId, {
175
+ name: config.appName,
176
+ redirect_uris: redirectUrls,
177
+ scopes: nextScopes,
178
+ ...(config.logoUri ? { logo_uri: config.logoUri } : {}),
179
+ });
180
+ spinner.stop();
181
+ if (config.appName)
182
+ (0, config_1.saveAppName)(appId, config.appName);
183
+ reportUpdateResult({
184
+ appId,
185
+ name: config.appName,
186
+ redirectUris: redirectUrls,
187
+ scopes: nextScopes,
188
+ logoUri: config.logoUri,
189
+ }, options.json);
190
+ }
191
+ async function resolveExistingState(appId, config, shouldWriteBack, silent) {
199
192
  const configRedirectUrls = config?.auth?.redirectUrls;
200
193
  const hasUsableConfigRedirectUrls = Array.isArray(configRedirectUrls) && configRedirectUrls.length > 0;
201
194
  if (config && shouldWriteBack && hasUsableConfigRedirectUrls) {
202
195
  // Use config as baseline only when it can safely preserve redirect URLs
203
- existingName = config.appName;
204
- existingRedirectUrls = configRedirectUrls;
205
- existingLogoUri = config.logoUri;
206
- existingScopes = config.auth?.scopes ?? [];
196
+ return {
197
+ name: config.appName,
198
+ redirectUrls: configRedirectUrls,
199
+ logoUri: config.logoUri,
200
+ scopes: config.auth?.scopes ?? [],
201
+ };
207
202
  }
208
- else if (config && shouldWriteBack) {
203
+ if (config && shouldWriteBack) {
209
204
  // Config matches the app, but missing/empty redirect URLs would otherwise clear
210
205
  // remote redirect URIs on a name-only update. Fall back to the API for preservation.
211
- const app = await fetchExistingApp(appId, options.json);
212
- existingName = config.appName ?? app.name;
213
- existingRedirectUrls = app.redirect_uris ?? [];
214
- existingLogoUri = config.logoUri ?? app.logo_uri;
215
- existingScopes = config.auth?.scopes ?? app.scopes ?? [];
216
- }
217
- else {
218
- const app = await fetchExistingApp(appId, options.json);
219
- existingName = app.name;
220
- existingRedirectUrls = app.redirect_uris ?? [];
221
- existingLogoUri = app.logo_uri;
222
- existingScopes = app.scopes ?? [];
206
+ const app = await fetchExistingApp(appId, silent);
207
+ return {
208
+ name: config.appName ?? app.name,
209
+ redirectUrls: app.redirect_uris ?? [],
210
+ logoUri: config.logoUri ?? app.logo_uri,
211
+ scopes: config.auth?.scopes ?? app.scopes ?? [],
212
+ };
223
213
  }
224
- // Merge: --name wins, --redirect-uri appends (deduplicated), --logo-uri wins
225
- const finalName = options.name ?? existingName;
226
- const appendedUrls = options.redirectUri ?? [];
227
- const mergedUrls = [...existingRedirectUrls];
228
- for (const url of appendedUrls) {
229
- if (!mergedUrls.includes(url)) {
230
- mergedUrls.push(url);
214
+ const app = await fetchExistingApp(appId, silent);
215
+ return {
216
+ name: app.name,
217
+ redirectUrls: app.redirect_uris ?? [],
218
+ logoUri: app.logo_uri,
219
+ scopes: app.scopes ?? [],
220
+ };
221
+ }
222
+ function appendUnique(existing, additions) {
223
+ const merged = [...existing];
224
+ for (const item of additions) {
225
+ if (!merged.includes(item)) {
226
+ merged.push(item);
231
227
  }
232
228
  }
233
- const finalLogoUri = options.logoUri ?? existingLogoUri;
234
- const appendedScopes = options.scope ?? [];
235
- const mergedScopes = [...existingScopes];
236
- for (const s of appendedScopes) {
237
- if (!mergedScopes.includes(s)) {
238
- mergedScopes.push(s);
239
- }
229
+ return merged;
230
+ }
231
+ // Passing --scope signals migration intent: drop the deprecated legacy 'all'
232
+ // scope from the merge baseline so the outgoing payload is clean (BEX-214).
233
+ function mergeScopes(existingScopes, appendedScopes) {
234
+ const hasScopeFlag = appendedScopes.length > 0;
235
+ const migratingLegacyScopes = hasScopeFlag && (0, validators_1.containsLegacyAllScope)(existingScopes);
236
+ const baseline = migratingLegacyScopes
237
+ ? existingScopes.filter((s) => s !== constants_1.LEGACY_ALL_SCOPE)
238
+ : existingScopes;
239
+ const mergedScopes = appendUnique(baseline, appendedScopes);
240
+ // Block any outgoing payload that still carries 'all' — either no --scope
241
+ // was passed (no migration intent), or 'all' was explicitly re-added.
242
+ if ((0, validators_1.containsLegacyAllScope)(mergedScopes)) {
243
+ throw new errors_1.CliError(en_1.messages.LEGACY_ALL_SCOPE_DEPRECATED_BLOCK);
244
+ }
245
+ return { mergedScopes, migratingLegacyScopes };
246
+ }
247
+ // Write back to app-config.json if appropriate
248
+ function writeBackProjectConfig(config, shouldWriteBack, options, mergedUrls, mergedScopes) {
249
+ if (!shouldWriteBack || !config) {
250
+ return false;
240
251
  }
252
+ const updatedConfig = { ...config };
253
+ if (options.name) {
254
+ updatedConfig.appName = options.name;
255
+ }
256
+ if (options.logoUri) {
257
+ updatedConfig.logoUri = options.logoUri;
258
+ }
259
+ updatedConfig.auth = {
260
+ ...updatedConfig.auth,
261
+ redirectUrls: mergedUrls,
262
+ scopes: mergedScopes,
263
+ };
264
+ (0, config_1.writeProjectConfig)(updatedConfig);
265
+ return true;
266
+ }
267
+ // Flags provided: merge with existing values
268
+ async function updateWithFlags(appId, config, shouldWriteBack, options) {
269
+ const existing = await resolveExistingState(appId, config, shouldWriteBack, options.json);
270
+ // Merge: --name wins, --redirect-uri appends (deduplicated), --logo-uri wins
271
+ const finalName = options.name ?? existing.name;
272
+ const mergedUrls = appendUnique(existing.redirectUrls, options.redirectUri ?? []);
273
+ const finalLogoUri = options.logoUri ?? existing.logoUri;
274
+ const { mergedScopes, migratingLegacyScopes } = mergeScopes(existing.scopes, options.scope ?? []);
241
275
  const hasRedirectUriFlag = options.redirectUri !== undefined;
242
276
  if (hasRedirectUriFlag && mergedUrls.length === 0) {
243
277
  throw new errors_1.CliError(en_1.messages.APP_UPDATE_NO_REDIRECT_URLS);
@@ -249,30 +283,22 @@ exports.updateCommand = (0, command_handler_1.withCommandHandler)(async (options
249
283
  if (!options.json) {
250
284
  renderUpdateSummary({
251
285
  appId,
252
- currentName: existingName,
286
+ currentName: existing.name,
253
287
  nextName: finalName,
254
- currentUrls: existingRedirectUrls,
288
+ currentUrls: existing.redirectUrls,
255
289
  nextUrls: mergedUrls,
256
- currentLogoUri: existingLogoUri,
290
+ currentLogoUri: existing.logoUri,
257
291
  nextLogoUri: finalLogoUri,
258
- currentScopes: existingScopes,
292
+ currentScopes: existing.scopes,
259
293
  nextScopes: mergedScopes,
294
+ migratingLegacyScopes,
260
295
  });
261
296
  }
262
297
  if (!options.json && !options.yes) {
263
298
  if (!process.stdin.isTTY) {
264
- throw new errors_1.CliError('Cannot prompt for confirmation in non-interactive mode. Use --yes or --json to skip.');
299
+ throw new errors_1.CliError(NON_INTERACTIVE_CONFIRM_ERROR);
265
300
  }
266
- const { confirmed } = await inquirer_1.default.prompt([
267
- {
268
- type: 'confirm',
269
- name: 'confirmed',
270
- message: en_1.messages.APP_UPDATE_CONFIRM,
271
- default: true,
272
- },
273
- ]);
274
- if (!confirmed) {
275
- (0, logger_1.logInfo)(`\n ${en_1.messages.APP_UPDATE_CANCELLED}\n`);
301
+ if (!(await confirmUpdate())) {
276
302
  return;
277
303
  }
278
304
  }
@@ -286,45 +312,49 @@ exports.updateCommand = (0, command_handler_1.withCommandHandler)(async (options
286
312
  spinner.stop();
287
313
  if (finalName)
288
314
  (0, config_1.saveAppName)(appId, finalName);
289
- // Write back to app-config.json if appropriate
290
- if (shouldWriteBack && config) {
291
- const updatedConfig = { ...config };
292
- if (options.name) {
293
- updatedConfig.appName = options.name;
294
- }
295
- if (options.logoUri) {
296
- updatedConfig.logoUri = options.logoUri;
297
- }
298
- updatedConfig.auth = {
299
- ...updatedConfig.auth,
300
- redirectUrls: mergedUrls,
301
- scopes: mergedScopes,
302
- };
303
- (0, config_1.writeProjectConfig)(updatedConfig);
304
- }
305
- if (options.json) {
306
- (0, json_output_1.jsonOutput)({
307
- app_id: appId,
308
- name: finalName,
309
- redirect_uris: mergedUrls,
310
- scopes: mergedScopes,
311
- ...(finalLogoUri ? { logo_uri: finalLogoUri } : {}),
312
- });
313
- return;
314
- }
315
- (0, logger_1.logSuccess)(en_1.messages.APP_UPDATE_SUCCESS);
316
- if (finalName) {
317
- (0, logger_1.logInfo)(` Name: ${finalName}`);
315
+ const configWrittenBack = writeBackProjectConfig(config, shouldWriteBack, options, mergedUrls, mergedScopes);
316
+ reportUpdateResult({
317
+ appId,
318
+ name: finalName,
319
+ redirectUris: mergedUrls,
320
+ scopes: mergedScopes,
321
+ logoUri: finalLogoUri,
322
+ configWrittenBack,
323
+ }, options.json);
324
+ }
325
+ exports.updateCommand = (0, command_handler_1.withCommandHandler)(async (options) => {
326
+ const config = (0, config_1.readProjectConfig)();
327
+ const hasFlags = !!(options.name !== undefined ||
328
+ (options.redirectUri && options.redirectUri.length > 0) ||
329
+ options.logoUri !== undefined ||
330
+ (options.scope && options.scope.length > 0));
331
+ // Validate --name if provided (even empty strings)
332
+ if (options.name !== undefined) {
333
+ const nameCheck = (0, validators_1.validateAppName)(options.name);
334
+ if (nameCheck !== true)
335
+ throw new errors_1.CliError(nameCheck);
318
336
  }
319
- (0, logger_1.logInfo)(` Redirect URLs: ${mergedUrls.length > 0 ? mergedUrls.join(', ') : '(none)'}`);
320
- (0, logger_1.logInfo)(` Scopes: ${mergedScopes.length > 0 ? mergedScopes.join(', ') : '(none)'}`);
321
- if (finalLogoUri) {
322
- (0, logger_1.logInfo)(` Logo URL: ${finalLogoUri}`);
337
+ // readProjectConfig normalizes appId (trim + coerce finite numbers, reject
338
+ // empty) so `config.appId` is a guaranteed non-empty string whenever `config`
339
+ // is non-null.
340
+ const configAppId = config?.appId;
341
+ const configAppIdValid = !!configAppId;
342
+ // app-config.json is only usable when it describes the app being updated.
343
+ // When --app-id differs from config.appId, the local file belongs to a
344
+ // different app and must not be used as the update payload or merge base.
345
+ const configMatches = configAppIdValid && (!options.appId || options.appId === configAppId);
346
+ if (!hasFlags && !configMatches) {
347
+ throwNoUpdateSource(config, configAppIdValid, configAppId, options.appId);
323
348
  }
324
- if (shouldWriteBack && config) {
325
- (0, logger_1.logInfo)(' app-config.json updated.');
349
+ const appId = resolveAppId(options.appId, configAppId, config);
350
+ // Write back only when config describes the app being updated.
351
+ const shouldWriteBack = configMatches;
352
+ if (!hasFlags) {
353
+ // config is guaranteed non-null here — the !hasFlags && !configMatches case throws above
354
+ await pushFullConfig(appId, config, options);
355
+ return;
326
356
  }
327
- process.stdout.write('\n');
357
+ await updateWithFlags(appId, config, shouldWriteBack, options);
328
358
  });
329
359
  async function fetchExistingApp(appId, silent) {
330
360
  const spinner = (0, ui_1.createSpinner)('Fetching app...', { silent });
@@ -340,59 +370,60 @@ async function fetchExistingApp(appId, silent) {
340
370
  }
341
371
  return app;
342
372
  }
343
- function renderUpdateSummary(params) {
344
- const { appId, currentName, nextName, currentUrls, nextUrls, currentLogoUri, nextLogoUri, currentScopes, nextScopes, } = params;
345
- const currentSet = new Set(currentUrls);
346
- const nextSet = new Set(nextUrls);
347
- const addedSet = new Set(nextUrls.filter((u) => !currentSet.has(u)));
348
- const removed = currentUrls.filter((u) => !nextSet.has(u));
349
- (0, logger_1.logInfo)('');
350
- (0, logger_1.logInfo)(` ${en_1.messages.APP_UPDATE_SUMMARY}`);
351
- (0, logger_1.logInfo)(` App ID: ${appId}`);
352
- if (nextName) {
353
- if (currentName && currentName !== nextName) {
354
- (0, logger_1.logInfo)(` Name: ${currentName} → ${nextName}`);
355
- }
356
- else {
357
- (0, logger_1.logInfo)(` Name: ${nextName}`);
358
- }
359
- }
360
- const lines = [
361
- ...nextUrls.map((u) => (addedSet.has(u) ? `${u} (new)` : u)),
362
- ...removed.map((u) => `${u} (removed)`),
373
+ // Diff `current` vs `next`: next values keep their order (tagged `(new)` when
374
+ // absent from current), values dropped from current trail with `(removed)`.
375
+ function diffLines(current, next) {
376
+ const currentSet = new Set(current);
377
+ const nextSet = new Set(next);
378
+ return [
379
+ ...next.map((v) => (currentSet.has(v) ? v : `${v} (new)`)),
380
+ ...current.filter((v) => !nextSet.has(v)).map((v) => `${v} (removed)`),
363
381
  ];
382
+ }
383
+ // Print a labelled block; continuation lines are indented to align under the
384
+ // first value.
385
+ function logAligned(label, lines) {
364
386
  lines.forEach((line, i) => {
365
- const prefix = i === 0 ? ' Redirect URLs: ' : ' ';
366
- (0, logger_1.logInfo)(`${prefix}${line}`);
387
+ (0, logger_1.logInfo)(`${i === 0 ? label : ' '}${line}`);
367
388
  });
368
- if (nextScopes !== undefined) {
369
- const currentScopesSet = new Set(currentScopes ?? []);
370
- const nextScopesSet = new Set(nextScopes);
371
- const addedScopes = new Set(nextScopes.filter((s) => !currentScopesSet.has(s)));
372
- const removedScopes = (currentScopes ?? []).filter((s) => !nextScopesSet.has(s));
373
- const scopeLines = nextScopes.length === 0 && removedScopes.length === 0
374
- ? ['(none)']
375
- : [
376
- ...nextScopes.map((s) => (addedScopes.has(s) ? `${s} (new)` : s)),
377
- ...removedScopes.map((s) => `${s} (removed)`),
378
- ];
379
- scopeLines.forEach((line, i) => {
380
- const prefix = i === 0 ? ' Scopes: ' : ' ';
381
- (0, logger_1.logInfo)(`${prefix}${line}`);
382
- });
389
+ }
390
+ function renderNameLine(currentName, nextName) {
391
+ if (!nextName) {
392
+ return;
383
393
  }
384
- if (currentLogoUri || nextLogoUri) {
385
- const label = ' Logo URL: ';
386
- if (currentLogoUri && nextLogoUri && currentLogoUri !== nextLogoUri) {
387
- (0, logger_1.logInfo)(`${label}${currentLogoUri} → ${nextLogoUri}`);
388
- }
389
- else if (nextLogoUri) {
390
- (0, logger_1.logInfo)(`${label}${nextLogoUri}`);
391
- }
392
- else if (currentLogoUri) {
393
- (0, logger_1.logInfo)(`${label}${currentLogoUri} (unchanged)`);
394
- }
394
+ const renamePrefix = currentName && currentName !== nextName ? `${currentName} → ` : '';
395
+ (0, logger_1.logInfo)(` Name: ${renamePrefix}${nextName}`);
396
+ }
397
+ function renderScopeLines(currentScopes, nextScopes) {
398
+ if (nextScopes === undefined) {
399
+ return;
400
+ }
401
+ const lines = diffLines(currentScopes ?? [], nextScopes);
402
+ logAligned(' Scopes: ', lines.length > 0 ? lines : ['(none)']);
403
+ }
404
+ function renderLogoLine(currentLogoUri, nextLogoUri) {
405
+ const label = ' Logo URL: ';
406
+ if (currentLogoUri && nextLogoUri && currentLogoUri !== nextLogoUri) {
407
+ (0, logger_1.logInfo)(`${label}${currentLogoUri} → ${nextLogoUri}`);
408
+ }
409
+ else if (nextLogoUri) {
410
+ (0, logger_1.logInfo)(`${label}${nextLogoUri}`);
411
+ }
412
+ else if (currentLogoUri) {
413
+ (0, logger_1.logInfo)(`${label}${currentLogoUri} (unchanged)`);
414
+ }
415
+ }
416
+ function renderUpdateSummary(params) {
417
+ (0, logger_1.logInfo)('');
418
+ (0, logger_1.logInfo)(` ${en_1.messages.APP_UPDATE_SUMMARY}`);
419
+ (0, logger_1.logInfo)(` App ID: ${params.appId}`);
420
+ renderNameLine(params.currentName, params.nextName);
421
+ logAligned(' Redirect URLs: ', diffLines(params.currentUrls, params.nextUrls));
422
+ if (params.migratingLegacyScopes) {
423
+ (0, logger_1.logInfo)(` ${en_1.messages.LEGACY_ALL_SCOPE_UPDATE_MIGRATING}`);
395
424
  }
425
+ renderScopeLines(params.currentScopes, params.nextScopes);
426
+ renderLogoLine(params.currentLogoUri, params.nextLogoUri);
396
427
  (0, logger_1.logInfo)('');
397
428
  }
398
429
  function validateRedirectUrls(urls) {