@contrail/cli 3.6.0-alpha-8-alpha.afa500a → 3.6.0-alpha-8-alpha.fb40acc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ $ npm install -g @contrail/cli
17
17
  $ contrail COMMAND
18
18
  running command...
19
19
  $ contrail (--version)
20
- @contrail/cli/3.6.0-alpha-8-alpha.afa500a linux-x64 node-v24.21.0
20
+ @contrail/cli/3.6.0-alpha-8-alpha.fb40acc linux-x64 node-v24.21.0
21
21
  $ contrail --help [COMMAND]
22
22
  USAGE
23
23
  $ contrail COMMAND
@@ -686,7 +686,7 @@ Update an entity
686
686
 
687
687
  ```
688
688
  USAGE
689
- $ contrail entities update ENTITYNAME [IDS] [-l error|warn|info|debug] [-e true|false] [-S] [-d] [-i <value>] [-F]
689
+ $ contrail entities update ENTITYNAME [IDS] [-l error|warn|info|debug] [-e true|false] [-S] [-d] [-i <value>]
690
690
  [--suffix <value>] [--apiVersion 1|2]
691
691
 
692
692
  ARGUMENTS
@@ -694,12 +694,11 @@ ARGUMENTS
694
694
  IDS One or more entity ids to update
695
695
 
696
696
  FLAGS
697
- -F, --force Force update without validation. Internal use only
698
697
  -S, --skipHydrateRelationships Skip hydrating relationships when updating the entity
699
- -d, --dryRun If true, does not create the entity but simulates the creation
698
+ -d, --dryRun If true, does not update the entities but simulates the update
700
699
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
701
700
  <options: true|false>
702
- -i, --inputFile=<value> File containing properties to update the entity (optional)
701
+ -i, --inputFile=<value> A file with a JSON array of {id, ...} objects; each object is one entity to update.
703
702
  -l, --logLevel=<option> The Log Level
704
703
  <options: error|warn|info|debug>
705
704
  --apiVersion=<option> Api-Version header to send; selects the V1 or V2 variant of the endpoint
@@ -161,7 +161,7 @@ async function refreshCurrentUserOrg() {
161
161
  const currentUser = await (0, sdk_1.getCurrentUser)().catch((e) => {
162
162
  console.error('error getting current user', e.message);
163
163
  if (isExpiredTokenError(e)) {
164
- console.log('try frefreshing');
164
+ console.error('access token expired; attempting refresh');
165
165
  return;
166
166
  }
167
167
  throw e;
@@ -176,7 +176,8 @@ async function refreshCurrentUserOrg() {
176
176
  apiGateway: config.apiGateway,
177
177
  orgSlug: config.orgSlug,
178
178
  };
179
- console.log('fetched new access token from refresh token', newVibeAccessToken);
179
+ (0, sdk_1.setConfig)(sdk_1.Auth.config);
180
+ console.error('fetched new access token from refresh token');
180
181
  }
181
182
  catch (e) {
182
183
  throw Error(`Could not get refresh token: ${e}`);
@@ -215,9 +216,11 @@ async function requestRefreshedAccessToken(vibeRefreshToken, apiGateway) {
215
216
  },
216
217
  body: JSON.stringify({ refreshToken: vibeRefreshToken }),
217
218
  });
218
- return await response.json().then((data) => {
219
- return data.accessToken;
220
- });
219
+ const data = await response.json();
220
+ if (!response.ok || !data.accessToken) {
221
+ throw new Error(`jwt_refresh returned ${response.status} with no access token`);
222
+ }
223
+ return data.accessToken;
221
224
  }
222
225
  function getVersionFromPackageJson() {
223
226
  // Get the path to the package.json file
@@ -35,7 +35,7 @@ class GetAPIKeyApp extends basecommand_1.BaseCommand {
35
35
  },
36
36
  ];
37
37
  const answers = await inquirer_1.default.prompt(questions);
38
- logger_1.default.info(`App API key: (app:${answers.appApiKey})`);
38
+ this.log(`App API key: (app:${answers.appApiKey})`);
39
39
  }
40
40
  else {
41
41
  const appFound = installedApps.find((x) => x.app.identifier === this.flags.appIdentifier);
@@ -43,7 +43,7 @@ class GetAPIKeyApp extends basecommand_1.BaseCommand {
43
43
  logger_1.default.info('You do not have such an app installed');
44
44
  return;
45
45
  }
46
- logger_1.default.info(`App API key: (app:${appFound.apiKey.apiKey})`);
46
+ this.log(`App API key: (app:${appFound.apiKey.apiKey})`);
47
47
  }
48
48
  }
49
49
  }
@@ -290,8 +290,6 @@ class CreateEntity extends basecommand_1.BaseCommand {
290
290
  }
291
291
  }
292
292
  exports.default = CreateEntity;
293
- // Every row is attempted; a failed row never stops the run. The summary pairs
294
- // each failed input with its error so the failed rows can be retried as-is.
295
293
  function summarizeSettledCreates(inputs, settled) {
296
294
  const created = [];
297
295
  const failed = [];
@@ -538,9 +536,7 @@ function castValueToPropertyType(value, propertyType) {
538
536
  if (Array.isArray(value)) {
539
537
  return value;
540
538
  }
541
- // Migration hazard: the old comma syntax parses as one string with a
542
- // comma in it. Warn with the repeated-key equivalent instead of
543
- // guessing at a split.
539
+ // the old comma syntax parses as one literal value; warn instead of guessing at a split
544
540
  if (typeof value === 'string' && value.includes(',')) {
545
541
  console.error(`Value "${value}" for a multi-value property contains a comma and is treated as one literal value. To pass several values, repeat the key: slug=${value
546
542
  .split(',')
@@ -333,8 +333,6 @@ describe('Entity Creation E2E Happy Path', () => {
333
333
  const propertySpecs = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)([
334
334
  'roles=option',
335
335
  ]);
336
- // a single roles value stays scalar at the parse layer and is
337
- // normalized to an array where it is consumed
338
336
  const roles = [].concat(propertySpecs.get('roles'));
339
337
  // Test that property level matching works with specified roles
340
338
  expect((0, create_1.doesPropertyLevelMatchRole)(types_1.PropertyLevel.OPTION, roles[0])).toBe(true);
@@ -91,8 +91,8 @@ class DeleteEntity extends basecommand_1.BaseCommand {
91
91
  console.error('deleted entities:');
92
92
  }
93
93
  this.log(JSON.stringify(response, null, 2));
94
- if (response.idsErrored.length > 0) {
95
- console.error(`${response.idsErrored.length} of ${ids.length} deletes failed.`);
94
+ if (response.failed.length > 0) {
95
+ console.error(`${response.failed.length} of ${ids.length} deletes failed.`);
96
96
  process.exitCode = 1;
97
97
  }
98
98
  }
@@ -103,32 +103,26 @@ class DeleteEntity extends basecommand_1.BaseCommand {
103
103
  }
104
104
  exports.default = DeleteEntity;
105
105
  async function deleteEntityByIds(entityName, ids, routing = {}) {
106
- const idsDeleted = new Set();
107
- const idsNotFound = new Set();
108
- const idsErrored = new Set();
106
+ const deletedIds = [];
107
+ const notFoundIds = [];
108
+ const failed = [];
109
109
  const limit = (0, p_limit_1.default)(10);
110
110
  const deletePromises = ids.map((id) => limit(() => deleteEntityById(entityName, id, routing).then((entity) => {
111
111
  if (!entity) {
112
- idsNotFound.add(id);
113
- return null;
112
+ notFoundIds.push(id);
113
+ return;
114
114
  }
115
- idsDeleted.add(id);
116
- return entity;
115
+ deletedIds.push(id);
117
116
  }, (error) => {
118
117
  if (error.message.includes('not found')) {
119
- idsNotFound.add(id);
118
+ notFoundIds.push(id);
120
119
  }
121
120
  else {
122
- idsErrored.add(id);
121
+ failed.push({ id, error: (0, common_1.sdkErrorMessage)(error) });
123
122
  }
124
- return null;
125
123
  })));
126
124
  await Promise.all(deletePromises);
127
- return {
128
- idsDeleted: Array.from(idsDeleted),
129
- idsNotFound: Array.from(idsNotFound),
130
- idsErrored: Array.from(idsErrored),
131
- };
125
+ return { deletedIds, notFoundIds, failed };
132
126
  }
133
127
  async function deleteEntityById(entityName, id, routing = {}) {
134
128
  const client = new sdk_1.Entities();
@@ -157,6 +157,9 @@ class GetEntity extends basecommand_1.BaseCommand {
157
157
  apiVersion: apiVersion,
158
158
  take,
159
159
  });
160
+ if (response === null) {
161
+ this.error(`Entity not found: ${propertySpecBySlug.get('id')}`, { exit: 1 });
162
+ }
160
163
  if (outputFile) {
161
164
  fs_1.default.writeFileSync(outputFile, JSON.stringify(response, null, 2));
162
165
  console.error(`Output saved to ${outputFile}`);
@@ -167,6 +170,9 @@ class GetEntity extends basecommand_1.BaseCommand {
167
170
  }
168
171
  }
169
172
  catch (e) {
173
+ if (e.message?.startsWith('Entity not found:')) {
174
+ throw e;
175
+ }
170
176
  this.error(`Error fetching entity: ${e.message}`);
171
177
  }
172
178
  }
@@ -182,6 +188,8 @@ async function getEntityByTypePath(typePath, propertySpec, options = {}) {
182
188
  }
183
189
  return find(entityName, {
184
190
  id,
191
+ // a missing id returns null (no retries); the command turns it into a clean not-found error
192
+ ignore404: id !== undefined,
185
193
  skipHydrateRelationships,
186
194
  relations,
187
195
  propertySlugs,
@@ -201,9 +209,10 @@ function convertPropertySpecIntoCriteria(propertySpec) {
201
209
  }
202
210
  async function find(entityName, options = {}) {
203
211
  const client = new sdk_1.Entities();
204
- const { criteria, id, propertySlugs, skipHydrateRelationships, relations, skip, suffix, apiVersion, take, } = options;
212
+ const { criteria, id, ignore404, propertySlugs, skipHydrateRelationships, relations, skip, suffix, apiVersion, take, } = options;
205
213
  const queryOptions = {
206
214
  entityName,
215
+ ignore404,
207
216
  skipHydrateRelationships,
208
217
  relations,
209
218
  skip,
@@ -215,8 +224,9 @@ async function find(entityName, options = {}) {
215
224
  queryOptions.customParams = { propertySlugs: propertySlugs.join(',') };
216
225
  }
217
226
  const payload = id ? { id, ...queryOptions } : { criteria, ...queryOptions };
218
- // a suffix names a specific sub-route, so it always goes through the plain client
219
- if ((0, batchFetch_1.canBatchFetchEntity)(entityName) && !suffix) {
227
+ // a suffix names a specific sub-route and an id names one entity; both always go
228
+ // through the plain client (batch-fetch only understands criteria)
229
+ if ((0, batchFetch_1.canBatchFetchEntity)(entityName) && !suffix && !id) {
220
230
  return (0, batchFetch_1.batchFetch)(entityName, payload);
221
231
  }
222
232
  else {
@@ -6,7 +6,6 @@ export default class UpdateEntity extends BaseCommand {
6
6
  skipHydrateRelationships: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
7
  dryRun: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
8
  inputFile: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
- force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
9
  suffix: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
10
  apiVersion: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
12
11
  alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
@@ -27,18 +27,13 @@ class UpdateEntity extends basecommand_1.BaseCommand {
27
27
  }),
28
28
  dryRun: core_1.Flags.boolean({
29
29
  char: 'd',
30
- description: 'If true, does not create the entity but simulates the creation',
30
+ description: 'If true, does not update the entities but simulates the update',
31
31
  required: false,
32
32
  default: false,
33
33
  }),
34
34
  inputFile: core_1.Flags.string({
35
35
  char: 'i',
36
- description: 'File containing properties to update the entity (optional)',
37
- required: false,
38
- }),
39
- force: core_1.Flags.boolean({
40
- char: 'F',
41
- description: 'Force update without validation. Internal use only',
36
+ description: 'A file with a JSON array of {id, ...} objects; each object is one entity to update.',
42
37
  required: false,
43
38
  }),
44
39
  suffix: core_1.Flags.string({
@@ -67,7 +62,7 @@ class UpdateEntity extends basecommand_1.BaseCommand {
67
62
  await (0, login_1.authUser)();
68
63
  const { args, argv } = await this.parse(UpdateEntity);
69
64
  const { entityName } = args;
70
- const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships, suffix, apiVersion, force, } = this.flags;
65
+ const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships, suffix, apiVersion, } = this.flags;
71
66
  const { ids: positionalIds, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
72
67
  const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
73
68
  if (propertySpec.has('id') || propertySpec.has('ids')) {
@@ -101,7 +96,6 @@ class UpdateEntity extends basecommand_1.BaseCommand {
101
96
  skipHydrateRelationships,
102
97
  suffix,
103
98
  apiVersion: apiVersion,
104
- force,
105
99
  });
106
100
  }
107
101
  try {
@@ -123,8 +117,6 @@ class UpdateEntity extends basecommand_1.BaseCommand {
123
117
  }
124
118
  }
125
119
  exports.default = UpdateEntity;
126
- // Every row is attempted; a failed row never stops the run. The summary pairs
127
- // each failed id with its error so the failed rows can be retried.
128
120
  function summarizeSettledUpdates(inputs, settled, skipped) {
129
121
  const updated = [];
130
122
  const failed = [];
@@ -144,7 +136,7 @@ function summarizeSettledUpdates(inputs, settled, skipped) {
144
136
  exports.summarizeSettledUpdates = summarizeSettledUpdates;
145
137
  async function updateEntityById(entityName, id, changes, options = {}) {
146
138
  const client = new sdk_1.Entities();
147
- const { skipHydrateRelationships, suffix, apiVersion, force } = options;
139
+ const { skipHydrateRelationships, suffix, apiVersion } = options;
148
140
  return client.update({
149
141
  entityName,
150
142
  id,
@@ -68,7 +68,8 @@ function parsePropertySpecificationsIntoMap(propertySpecifications) {
68
68
  // ids is the one comma-split exception: it is a list of ids by definition
69
69
  // and ids never contain commas.
70
70
  const idsArray = value.split(',').map((id) => id.trim());
71
- propertySpecBySlug.set(slug, idsArray);
71
+ const existingIds = propertySpecBySlug.get(slug);
72
+ propertySpecBySlug.set(slug, Array.isArray(existingIds) ? [...existingIds, ...idsArray] : idsArray);
72
73
  continue;
73
74
  }
74
75
  const existing = propertySpecBySlug.get(slug);
@@ -190,6 +190,11 @@ describe('parsePropertySpecificationsIntoMap', () => {
190
190
  expect(result.get('ids')).toEqual(['1', '2', '3']);
191
191
  expect(result.get('name')).toBe('Test');
192
192
  });
193
+ it('should accumulate repeated ids keys', () => {
194
+ const specs = ['ids=1,2', 'ids=3'];
195
+ const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
196
+ expect(result.get('ids')).toEqual(['1', '2', '3']);
197
+ });
193
198
  it('should handle ids as single value', () => {
194
199
  const specs = ['ids=123'];
195
200
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.6.0-alpha-8-alpha.afa500a",
2
+ "version": "3.6.0-alpha-8-alpha.fb40acc",
3
3
  "commands": {
4
4
  "app:create": {
5
5
  "id": "app:create",
@@ -1077,7 +1077,7 @@
1077
1077
  "name": "dryRun",
1078
1078
  "type": "boolean",
1079
1079
  "char": "d",
1080
- "description": "If true, does not create the entity but simulates the creation",
1080
+ "description": "If true, does not update the entities but simulates the update",
1081
1081
  "required": false,
1082
1082
  "allowNo": false
1083
1083
  },
@@ -1085,18 +1085,10 @@
1085
1085
  "name": "inputFile",
1086
1086
  "type": "option",
1087
1087
  "char": "i",
1088
- "description": "File containing properties to update the entity (optional)",
1088
+ "description": "A file with a JSON array of {id, ...} objects; each object is one entity to update.",
1089
1089
  "required": false,
1090
1090
  "multiple": false
1091
1091
  },
1092
- "force": {
1093
- "name": "force",
1094
- "type": "boolean",
1095
- "char": "F",
1096
- "description": "Force update without validation. Internal use only",
1097
- "required": false,
1098
- "allowNo": false
1099
- },
1100
1092
  "suffix": {
1101
1093
  "name": "suffix",
1102
1094
  "type": "option",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/cli",
3
- "version": "3.6.0-alpha-8-alpha.afa500a",
3
+ "version": "3.6.0-alpha-8-alpha.fb40acc",
4
4
  "description": "VibeIQ's Contrail Platform CLI.",
5
5
  "bin": {
6
6
  "contrail-cli": "./bin/run",