@contrail/cli 3.6.0-alpha-2 → 3.6.0-alpha-4

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-2 darwin-arm64 node-v22.19.0
20
+ @contrail/cli/3.6.0-alpha-4 darwin-arm64 node-v22.19.0
21
21
  $ contrail --help [COMMAND]
22
22
  USAGE
23
23
  $ contrail COMMAND
@@ -54,7 +54,7 @@ To get instructions on how to set up autocomplete for your shell, run `contrail
54
54
  * [`contrail entities create PATH`](#contrail-entities-create-path)
55
55
  * [`contrail entities delete ENTITYNAME`](#contrail-entities-delete-entityname)
56
56
  * [`contrail entities get TYPEPATH`](#contrail-entities-get-typepath)
57
- * [`contrail entities update ENTITYNAME ID`](#contrail-entities-update-entityname-id)
57
+ * [`contrail entities update ENTITYNAME [ID]`](#contrail-entities-update-entityname-id)
58
58
  * [`contrail file upload FILELOCATION CONTENTTYPE`](#contrail-file-upload-filelocation-contenttype)
59
59
  * [`contrail groups export`](#contrail-groups-export)
60
60
  * [`contrail groups import GROUPSFILE`](#contrail-groups-import-groupsfile)
@@ -495,7 +495,7 @@ Set a variable in the configuration
495
495
  ```
496
496
  USAGE
497
497
  $ contrail config set [-l error|warn|info|debug] [-e true|false] [--orgSlug <value>] [--environment
498
- dev|development|prod|production] [--apiUserToken <value>] [--config <value>]
498
+ dev|development|prod|production|local] [--apiUserToken <value>] [--config <value>]
499
499
 
500
500
  FLAGS
501
501
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
@@ -505,7 +505,7 @@ FLAGS
505
505
  --apiUserToken=<value> The API User Token
506
506
  --config=<value> The config name
507
507
  --environment=<option> The Environment
508
- <options: dev|development|prod|production>
508
+ <options: dev|development|prod|production|local>
509
509
  --orgSlug=<value> The org slug
510
510
 
511
511
  DESCRIPTION
@@ -625,14 +625,15 @@ EXAMPLES
625
625
 
626
626
  ```
627
627
  USAGE
628
- $ contrail entities get TYPEPATH [-l error|warn|info|debug] [-e true|false] [-o <value>] [-s] [-r <value>] [-p
629
- <value>] [-P] [-t <value>]
628
+ $ contrail entities get TYPEPATH [-l error|warn|info|debug] [-e true|false] [-o <value>] [-p <value>] [-P] [-s
629
+ <value>] [-S] [-t <value>] [-r <value>]
630
630
 
631
631
  ARGUMENTS
632
632
  TYPEPATH The name of the entity type to fetch
633
633
 
634
634
  FLAGS
635
635
  -P, --paginated If true, fetches all entities in a paginated manner
636
+ -S, --skipHydrateRelationships Skip hydrating relationships when fetching the entity
636
637
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
637
638
  <options: true|false>
638
639
  -l, --logLevel=<option> The Log Level
@@ -640,7 +641,7 @@ FLAGS
640
641
  -o, --outputFile=<value> File to save the output (optional)
641
642
  -p, --propertySlugs=<value> Comma-separated list of property slugs to include in the output
642
643
  -r, --relations=<value> Comma-separated list of relations to include in the output
643
- -s, --skipHydrateRelationships Skip hydrating relationships when fetching the entity
644
+ -s, --skip=<value> Number of entities to skip (used with paginated fetch)
644
645
  -t, --take=<value> Number of entities to fetch in a single request (default is 100)
645
646
 
646
647
  DESCRIPTION
@@ -656,19 +657,20 @@ EXAMPLES
656
657
  $ contrail entities get events -- timestamp=..2025-09-16T23:59:59Z
657
658
  ```
658
659
 
659
- ## `contrail entities update ENTITYNAME ID`
660
+ ## `contrail entities update ENTITYNAME [ID]`
660
661
 
661
662
  (BETA) Update an entity
662
663
 
663
664
  ```
664
665
  USAGE
665
- $ contrail entities update ENTITYNAME ID [-l error|warn|info|debug] [-e true|false] [-s] [-d] [-i <value>]
666
+ $ contrail entities update ENTITYNAME [ID] [-l error|warn|info|debug] [-e true|false] [-s] [-d] [-i <value>] [-F]
666
667
 
667
668
  ARGUMENTS
668
669
  ENTITYNAME The name of the entity type to update
669
670
  ID The unique identifier of the entity
670
671
 
671
672
  FLAGS
673
+ -F, --force Force update without validation. Internal use only
672
674
  -d, --dryRun If true, does not create the entity but simulates the creation
673
675
  -e, --alwaysConfirmEnv=<option> [default: true] The Environment variables
674
676
  <options: true|false>
@@ -683,7 +685,7 @@ DESCRIPTION
683
685
  EXAMPLES
684
686
  $ contrail entities update color 123 -- name=obsidian hex=000000
685
687
 
686
- $ contrail entities update color 123 -- description= notes='""' data=null # Unset description, empty notes, null data
688
+ $ contrail entities update color 123 -- description= notes='""' # Unset description, set notes to empty string
687
689
  ```
688
690
 
689
691
  ## `contrail file upload FILELOCATION CONTENTTYPE`
@@ -55,7 +55,9 @@ async function authUser() {
55
55
  logger_1.default.info(`Failed to refresh token: ${e}. Requesting auth credentials...`);
56
56
  }
57
57
  }
58
- return await authenticateWithUsernamePassword(config);
58
+ const authContext = await authenticateWithUsernamePassword(config);
59
+ await (0, config_1.storeConfig)(authContext);
60
+ return authContext;
59
61
  }
60
62
  exports.authUser = authUser;
61
63
  async function getOrgSlug(config) {
@@ -77,7 +79,7 @@ async function authenticateWithApiKey(config, apiKey, orgSlug) {
77
79
  const apiGateway = config?.apiGateway ?? CONFIGURATION.apiGateway;
78
80
  sdk_1.Auth.config = {
79
81
  apiUserToken: apiKey,
80
- apiRefreshToken: null,
82
+ apiRefreshToken: undefined,
81
83
  apiGateway,
82
84
  orgSlug,
83
85
  };
@@ -145,20 +147,25 @@ async function authenticateWithUsernamePassword(config) {
145
147
  async function setAuthContext(user, params) {
146
148
  const apiGateway = params?.apiGateway ?? CONFIGURATION.apiGateway;
147
149
  const { vibeAccessToken, vibeRefreshToken } = await getVibeAccessTokenFromCognitoAccessToken(user.idToken.jwtToken, apiGateway);
148
- // Set Auth Config
149
150
  sdk_1.Auth.config = {
150
151
  apiUserToken: vibeAccessToken,
151
152
  apiRefreshToken: vibeRefreshToken,
152
153
  apiGateway,
153
154
  orgSlug: params.orgSlug,
154
155
  };
155
- // SET SDK CONFIG
156
156
  (0, sdk_1.setConfig)(sdk_1.Auth.config);
157
157
  return refreshCurrentUserOrg();
158
158
  }
159
159
  async function refreshCurrentUserOrg() {
160
160
  logger_1.default.debug('refreshing current user org...');
161
- const currentUser = await (0, sdk_1.getCurrentUser)();
161
+ const currentUser = await (0, sdk_1.getCurrentUser)().catch((e) => {
162
+ console.error('error getting current user', e.message);
163
+ if (isExpiredTokenError(e)) {
164
+ console.log('try frefreshing');
165
+ return;
166
+ }
167
+ throw e;
168
+ });
162
169
  if (!currentUser) {
163
170
  const config = await (0, config_1.getConfig)();
164
171
  try {
@@ -181,6 +188,9 @@ async function refreshCurrentUserOrg() {
181
188
  // await checkTelemetry(current_Version);
182
189
  return Object.assign({}, sdk_1.Auth.config);
183
190
  }
191
+ function isExpiredTokenError(e) {
192
+ return e?.message?.includes('token is expired');
193
+ }
184
194
  async function getVibeAccessTokenFromCognitoAccessToken(cognitoAccessToken, apiGateway) {
185
195
  const url = `${apiGateway}/auth/cognito/`;
186
196
  const response = await fetch(url, {
@@ -15,7 +15,7 @@ class SetConfig extends basecommand_1.BaseCommand {
15
15
  environment: core_1.Flags.string({
16
16
  description: 'The Environment',
17
17
  required: false,
18
- options: ['dev', 'development', 'prod', 'production'],
18
+ options: ['dev', 'development', 'prod', 'production', 'local'],
19
19
  }),
20
20
  apiUserToken: core_1.Flags.string({
21
21
  description: 'The API User Token',
@@ -4,11 +4,12 @@ export default class GetEntity extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  outputFile: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
7
- skipHydrateRelationships: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
- relations: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
7
  propertySlugs: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
8
  paginated: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ skip: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
+ skipHydrateRelationships: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
11
  take: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
12
+ relations: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
12
13
  alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
13
14
  logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
14
15
  };
@@ -8,6 +8,7 @@ const sdk_1 = require("@contrail/sdk");
8
8
  const login_1 = require("../../auth/login");
9
9
  const types_1 = require("../../api/types");
10
10
  const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
11
+ const batchFetch_1 = require("../../helpers/entities/batchFetch");
11
12
  let SHOULD_USE_PAGINATED_FIND = false;
12
13
  let MAX_NUMBER_OF_ENTITIES_TO_FETCH = undefined;
13
14
  class GetEntity extends basecommand_1.BaseCommand {
@@ -25,17 +26,6 @@ class GetEntity extends basecommand_1.BaseCommand {
25
26
  description: 'File to save the output (optional)',
26
27
  required: false,
27
28
  }),
28
- skipHydrateRelationships: core_1.Flags.boolean({
29
- char: 's',
30
- description: 'Skip hydrating relationships when fetching the entity',
31
- required: false,
32
- default: false,
33
- }),
34
- relations: core_1.Flags.string({
35
- char: 'r',
36
- description: 'Comma-separated list of relations to include in the output',
37
- required: false,
38
- }),
39
29
  propertySlugs: core_1.Flags.string({
40
30
  char: 'p',
41
31
  description: 'Comma-separated list of property slugs to include in the output',
@@ -47,12 +37,28 @@ class GetEntity extends basecommand_1.BaseCommand {
47
37
  required: false,
48
38
  default: false,
49
39
  }),
40
+ skip: core_1.Flags.integer({
41
+ char: 's',
42
+ description: 'Number of entities to skip (used with paginated fetch)',
43
+ required: false,
44
+ }),
45
+ skipHydrateRelationships: core_1.Flags.boolean({
46
+ char: 'S',
47
+ description: 'Skip hydrating relationships when fetching the entity',
48
+ required: false,
49
+ default: false,
50
+ }),
50
51
  take: core_1.Flags.integer({
51
52
  char: 't',
52
53
  description: 'Number of entities to fetch in a single request (default is 100)',
53
54
  required: false,
54
55
  default: MAX_NUMBER_OF_ENTITIES_TO_FETCH,
55
56
  }),
57
+ relations: core_1.Flags.string({
58
+ char: 'r',
59
+ description: 'Comma-separated list of relations to include in the output',
60
+ required: false,
61
+ }),
56
62
  };
57
63
  static args = {
58
64
  typePath: core_1.Args.string({
@@ -66,9 +72,9 @@ class GetEntity extends basecommand_1.BaseCommand {
66
72
  await (0, login_1.authUser)();
67
73
  const { args, flags, argv } = await this.parse(GetEntity);
68
74
  const { typePath } = args;
69
- const { outputFile, propertySlugs, relations: relationString, skipHydrateRelationships, paginated: shouldUsePaginatedFind, take: numToTake, } = flags;
75
+ const { outputFile, paginated: shouldUsePaginatedFind, propertySlugs, relations: relationString, skip, skipHydrateRelationships, take, } = flags;
70
76
  SHOULD_USE_PAGINATED_FIND = shouldUsePaginatedFind;
71
- MAX_NUMBER_OF_ENTITIES_TO_FETCH = numToTake;
77
+ MAX_NUMBER_OF_ENTITIES_TO_FETCH = take;
72
78
  if (MAX_NUMBER_OF_ENTITIES_TO_FETCH)
73
79
  console.log(`Will limit output to ${MAX_NUMBER_OF_ENTITIES_TO_FETCH} entities.`);
74
80
  const propertySpecificationArgs = argv
@@ -92,10 +98,11 @@ class GetEntity extends basecommand_1.BaseCommand {
92
98
  // thinks it is hard to read. Consider breaking it into smaller functions.
93
99
  if (SHOULD_USE_PAGINATED_FIND) {
94
100
  const stream = streamPaginatedFind(typePath, {
101
+ criteria: convertPropertySpecIntoCriteria(propertySpecBySlug),
95
102
  skipHydrateRelationships,
103
+ skip,
96
104
  relations,
97
105
  propertySlugs: propertySlugsArray,
98
- criteria: convertPropertySpecIntoCriteria(propertySpecBySlug),
99
106
  });
100
107
  const writer = outputFile ? fs_1.default.createWriteStream(outputFile) : process.stdout;
101
108
  writer.write('[\n');
@@ -115,6 +122,8 @@ class GetEntity extends basecommand_1.BaseCommand {
115
122
  skipHydrateRelationships,
116
123
  relations,
117
124
  propertySlugs: propertySlugsArray,
125
+ skip,
126
+ take,
118
127
  });
119
128
  if (outputFile) {
120
129
  fs_1.default.writeFileSync(outputFile, JSON.stringify(response, null, 2));
@@ -132,17 +141,14 @@ class GetEntity extends basecommand_1.BaseCommand {
132
141
  }
133
142
  exports.default = GetEntity;
134
143
  async function getEntityByTypePath(typePath, propertySpec, options = {}) {
135
- const { propertySlugs, relations, skipHydrateRelationships } = options;
144
+ const { propertySlugs, relations, skipHydrateRelationships, skip, take } = options;
136
145
  const id = propertySpec?.get('id')?.toString() || undefined;
146
+ const criteria = convertPropertySpecIntoCriteria(propertySpec || new Map());
137
147
  const { root: entityName } = (0, types_1.parseTypePath)(typePath);
138
- let criteria = {};
139
148
  if (entityName === 'custom-entity' && !id) {
140
149
  criteria.typePath = typePath;
141
150
  }
142
- else {
143
- criteria = convertPropertySpecIntoCriteria(propertySpec || new Map());
144
- }
145
- return find(entityName, { id, skipHydrateRelationships, relations, propertySlugs, criteria });
151
+ return find(entityName, { id, skipHydrateRelationships, relations, propertySlugs, criteria, skip, take });
146
152
  }
147
153
  function convertPropertySpecIntoCriteria(propertySpec) {
148
154
  const criteria = {};
@@ -153,16 +159,24 @@ function convertPropertySpecIntoCriteria(propertySpec) {
153
159
  }
154
160
  async function find(entityName, options = {}) {
155
161
  const client = new sdk_1.Entities();
156
- const { criteria, id, propertySlugs, skipHydrateRelationships, relations } = options;
162
+ const { criteria, id, propertySlugs, skipHydrateRelationships, relations, skip, take } = options;
157
163
  const queryOptions = {
158
164
  entityName,
159
165
  skipHydrateRelationships,
160
166
  relations,
167
+ skip,
168
+ take
161
169
  };
162
170
  if (propertySlugs && propertySlugs.length > 0) {
163
171
  queryOptions.customParams = { propertySlugs: propertySlugs.join(',') };
164
172
  }
165
- return id ? client.get({ id, ...queryOptions }) : client.get({ criteria, ...queryOptions });
173
+ const payload = id ? { id, ...queryOptions } : { criteria, ...queryOptions };
174
+ if ((0, batchFetch_1.canBatchFetchEntity)(entityName)) {
175
+ return (0, batchFetch_1.batchFetch)(entityName, payload);
176
+ }
177
+ else {
178
+ return client.get(payload);
179
+ }
166
180
  }
167
181
  async function* streamPaginatedFind(typePath, options = {}) {
168
182
  const client = new sdk_1.Entities();
@@ -6,6 +6,7 @@ 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>;
9
10
  alwaysConfirmEnv: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
10
11
  logLevel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
12
  };
@@ -12,7 +12,7 @@ class UpdateEntity extends basecommand_1.BaseCommand {
12
12
  static description = '(BETA) Update an entity';
13
13
  static examples = [
14
14
  'contrail entities update color 123 -- name=obsidian hex=000000',
15
- 'contrail entities update color 123 -- description= notes=\'""\' data=null # Unset description, empty notes, null data',
15
+ 'contrail entities update color 123 -- description= notes=\'""\' # Unset description, set notes to empty string',
16
16
  ];
17
17
  static flags = {
18
18
  ...basecommand_1.BaseCommand.baseFlags,
@@ -33,6 +33,11 @@ class UpdateEntity extends basecommand_1.BaseCommand {
33
33
  description: 'File containing properties to update the entity (optional)',
34
34
  required: false,
35
35
  }),
36
+ force: core_1.Flags.boolean({
37
+ char: 'F',
38
+ description: 'Force update without validation. Internal use only',
39
+ required: false,
40
+ })
36
41
  };
37
42
  static args = {
38
43
  entityName: core_1.Args.string({
@@ -41,7 +46,7 @@ class UpdateEntity extends basecommand_1.BaseCommand {
41
46
  }),
42
47
  id: core_1.Args.string({
43
48
  description: 'The unique identifier of the entity',
44
- required: true,
49
+ required: false,
45
50
  }),
46
51
  };
47
52
  static strict = false;
@@ -49,34 +54,34 @@ class UpdateEntity extends basecommand_1.BaseCommand {
49
54
  this.log('🚨 This command is in BETA and may change or be removed at any time.');
50
55
  await (0, login_1.authUser)();
51
56
  const { args, argv } = await this.parse(UpdateEntity);
52
- const { id, entityName } = args;
53
- const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships } = this.flags;
57
+ const { id: specifiedId, entityName } = args;
58
+ const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships, force } = this.flags;
54
59
  const propertySpecArgs = argv.slice(1, argv.length).filter(propertySpecifications_1.isValidPropertySpecification);
55
60
  const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(propertySpecArgs);
56
- if (propertySpec.size === 0) {
57
- throw new Error('No valid properties provided for update');
58
- }
59
- if (inputFilename && id) {
61
+ if (inputFilename && specifiedId) {
60
62
  throw new Error('You cannot use --input and --id together. Please specify one or the other.');
61
63
  }
62
- const partialEntitiesArray = inputFilename
63
- ? (0, file_helpers_1.parseFileAsJsonArray)(inputFilename).map(({ id }) => ({ id }))
64
- : [{ id }];
64
+ const entitiesToUpdate = inputFilename
65
+ ? (0, file_helpers_1.parseFileAsJsonArray)(inputFilename)
66
+ : [{ id: specifiedId }];
65
67
  const results = {
66
68
  idsSuccessfullyUpdated: new Set(),
67
69
  idsFailedToUpdate: new Set(),
68
- numEntriesSkipped: partialEntitiesArray.filter(({ id }) => !id).length,
70
+ numEntriesSkipped: entitiesToUpdate.filter(({ id }) => !id).length,
69
71
  };
70
- const idsToUpdate = partialEntitiesArray.map(({ id }) => id).filter(Boolean);
72
+ const entitiesWithId = entitiesToUpdate.filter(({ id }) => id);
71
73
  propertySpec.delete('id'); // Remove 'id' from the property spec if it exists
72
- const changes = [...propertySpec.entries()].reduce((acc, [key, value]) => {
74
+ // will be applied to all entities
75
+ const fixedValues = [...propertySpec.entries()].reduce((acc, [key, value]) => {
73
76
  acc[key] = value;
74
77
  return acc;
75
78
  }, {});
76
- async function updateEntity(id) {
79
+ async function updateEntity(entity) {
80
+ const newEntityState = { ...entity, ...fixedValues };
81
+ const id = entity.id;
77
82
  const promise = isDryRun
78
- ? mockUpdateById(entityName, id, changes, { skipHydrateRelationships })
79
- : updateEntityById(entityName, id, changes, { skipHydrateRelationships });
83
+ ? mockUpdateById(entityName, id, newEntityState, { skipHydrateRelationships })
84
+ : updateEntityById(entityName, id, newEntityState, { skipHydrateRelationships, force });
80
85
  promise
81
86
  .catch(() => {
82
87
  results.idsFailedToUpdate.add(id);
@@ -87,8 +92,8 @@ class UpdateEntity extends basecommand_1.BaseCommand {
87
92
  return promise;
88
93
  }
89
94
  try {
90
- const limit = (0, p_limit_1.default)(5); // Limit concurrency to 5
91
- const responses = await Promise.all(idsToUpdate.map((id) => limit(() => updateEntity(id))));
95
+ const limit = (0, p_limit_1.default)(20);
96
+ const responses = await Promise.all(entitiesWithId.map((entity) => limit(() => updateEntity(entity))));
92
97
  const messagePrefix = isDryRun
93
98
  ? 'Dry run. Update skipped. Changes would be:'
94
99
  : 'Entity updated successfully:';
@@ -105,8 +110,10 @@ class UpdateEntity extends basecommand_1.BaseCommand {
105
110
  exports.default = UpdateEntity;
106
111
  async function updateEntityById(entityName, id, changes, options = {}) {
107
112
  const client = new sdk_1.Entities();
108
- const { skipHydrateRelationships } = options;
109
- return client.update({ entityName, id, object: changes, skipHydrateRelationships });
113
+ const { skipHydrateRelationships, force } = options;
114
+ const fullPath = force ? `maintenance/items/${id}` : undefined;
115
+ console.log(fullPath);
116
+ return client.update({ entityName, id, object: changes, skipHydrateRelationships, fullPath });
110
117
  }
111
118
  async function mockUpdateById(entityName, id, changes, options = {}) {
112
119
  const client = new sdk_1.Entities();
@@ -1,16 +1,17 @@
1
1
  export declare const ENVIRONMENTS: {
2
2
  dev: string;
3
3
  prod: string;
4
+ local: string;
4
5
  };
5
6
  export declare function initializeVibeIQConfig(): Promise<void>;
6
7
  export declare function readVibeIQConfig(): Promise<any>;
7
8
  export declare function writeVibeIQConfig(config: any): Promise<void>;
8
9
  export declare function storeVibeIQConfig(incomingConfig: any): Promise<void>;
9
10
  export declare function createConfig(fileName: any): Promise<void>;
10
- export declare function writeConfig(config: any, configName?: string): Promise<void>;
11
+ export declare function writeConfig(config: any, configName?: string | null): Promise<void>;
11
12
  export declare function getConfig(): Promise<any>;
12
- export declare function storeConfig(incomingConfig: any, configName?: string, merge?: boolean): Promise<void>;
13
+ export declare function storeConfig(incomingConfig: any, configName?: string | null, merge?: boolean): Promise<void>;
13
14
  export declare function getConfigFiles(): Promise<any>;
14
15
  export declare function getConfigContent(fileName: string): Promise<any>;
15
- export declare function validateEnvironments(envName?: any): Promise<any>;
16
+ export declare function validateEnvironments(envName?: string | null): Promise<any>;
16
17
  export declare function checkTelemetry(cliVersion: string): Promise<void>;
@@ -15,6 +15,7 @@ const defaultConfigFileLocation = (0, node_path_1.join)(configDirectoryLocation,
15
15
  exports.ENVIRONMENTS = {
16
16
  dev: 'https://api.dev.vibeiq.com/dev/api',
17
17
  prod: 'https://api.vibeiq.com/prod/api',
18
+ local: 'http://localhost:3000/api',
18
19
  };
19
20
  async function initializeVibeIQConfig() {
20
21
  try {
@@ -1,7 +1,7 @@
1
- import { AssortmentItem, Item, ProjectItem } from '@contrail/entity-types';
1
+ import { AssortmentItem, ProjectItem } from '@contrail/entity-types';
2
2
  import { EntitiesClientGetOptions } from '@contrail/sdk';
3
+ export declare function canBatchFetchEntity(entityName: string): entityName is keyof BatchFetchableEntityMap;
3
4
  export interface BatchFetchableEntityMap {
4
- item: Item;
5
5
  'project-item': ProjectItem;
6
6
  'assortment-item': AssortmentItem;
7
7
  }
@@ -1,8 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.batchFetch = void 0;
3
+ exports.batchFetch = exports.canBatchFetchEntity = void 0;
4
4
  const sdk_1 = require("@contrail/sdk");
5
5
  const util_1 = require("@contrail/util");
6
+ // Maps entity names to their types for batch fetching.
7
+ // This allows for type-safe batch fetching of entities
8
+ // such that the Type returned always matches the entity name.
9
+ const BATCH_FETCHABLE_ENTITIES = ['assortment-item', 'project-item'];
10
+ function canBatchFetchEntity(entityName) {
11
+ return BATCH_FETCHABLE_ENTITIES.includes(entityName);
12
+ }
13
+ exports.canBatchFetchEntity = canBatchFetchEntity;
6
14
  async function batchFetch(entityName, options) {
7
15
  const response = await requestBatchFetch(entityName, options);
8
16
  return parseBatchFetchResponse(response);
@@ -10,24 +10,22 @@
10
10
  *
11
11
  * It is typically used as the argv in a command.
12
12
  *
13
- * Special handling for empty values and null:
13
+ * Special handling for empty values:
14
14
  * - slug= (empty value) → property is set to undefined (unset)
15
15
  * - slug='""' or slug="''" → property is set to empty string ""
16
- * - slug=null → property is set to null
17
- * - slug="null" → property is set to the string "null"
18
16
  *
19
17
  * Examples:
20
18
  *
21
19
  * contrail entities get color -- id=123
22
20
  * contrail entities update color 123 -- name=obsidian hex=000000
23
- * contrail entities update color 123 -- description= notes='""' data=null
21
+ * contrail entities update color 123 -- description= notes='""'
24
22
  * contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
25
23
  */
26
24
  export type PropertySpecification = {
27
25
  slug: string;
28
- value: string | undefined | null;
26
+ value: string | undefined;
29
27
  };
30
- export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[] | undefined | null>;
28
+ export type PropertySpecificationBySlug = Map<string, string | number | string[] | number[] | undefined>;
31
29
  export type PropertySpecificationString = `${string}=${string}`;
32
30
  export declare function parsePropertySpecification(spec: PropertySpecificationString): PropertySpecification;
33
31
  export declare function parsePropertySpecificationsIntoMap(propertySpecifications: PropertySpecificationString[]): PropertySpecificationBySlug;
@@ -11,17 +11,15 @@
11
11
  *
12
12
  * It is typically used as the argv in a command.
13
13
  *
14
- * Special handling for empty values and null:
14
+ * Special handling for empty values:
15
15
  * - slug= (empty value) → property is set to undefined (unset)
16
16
  * - slug='""' or slug="''" → property is set to empty string ""
17
- * - slug=null → property is set to null
18
- * - slug="null" → property is set to the string "null"
19
17
  *
20
18
  * Examples:
21
19
  *
22
20
  * contrail entities get color -- id=123
23
21
  * contrail entities update color 123 -- name=obsidian hex=000000
24
- * contrail entities update color 123 -- description= notes='""' data=null
22
+ * contrail entities update color 123 -- description= notes='""'
25
23
  * contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
26
24
  */
27
25
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -40,11 +38,9 @@ function parsePropertySpecification(spec) {
40
38
  if (!slug) {
41
39
  throw new Error(`Invalid property specification: ${spec}`);
42
40
  }
43
- // Handle special cases for empty strings and null:
41
+ // Handle special cases for empty strings:
44
42
  // - x= means undefined (unset the property)
45
43
  // - x='""' or x="''" means empty string ""
46
- // - x=null means null value
47
- // - x="null" means the string "null"
48
44
  let finalValue;
49
45
  if (value === '') {
50
46
  finalValue = undefined;
@@ -52,9 +48,6 @@ function parsePropertySpecification(spec) {
52
48
  else if (value === DOUBLE_QUOTED_EMPTY || value === SINGLE_QUOTED_EMPTY) {
53
49
  finalValue = '';
54
50
  }
55
- else if (value === 'null') {
56
- finalValue = null;
57
- }
58
51
  else {
59
52
  finalValue = value;
60
53
  }
@@ -78,28 +78,6 @@ describe('parsePropertySpecification', () => {
78
78
  value: '',
79
79
  });
80
80
  });
81
- it('should parse property specification with null value', () => {
82
- const spec = 'description=null';
83
- const result = (0, propertySpecifications_1.parsePropertySpecification)(spec);
84
- expect(result).toEqual({
85
- slug: 'description',
86
- value: null,
87
- });
88
- });
89
- it('should distinguish between null and the string "null"', () => {
90
- const nullSpec = 'field=null';
91
- const stringNullSpec = 'field="null"';
92
- const nullResult = (0, propertySpecifications_1.parsePropertySpecification)(nullSpec);
93
- const stringNullResult = (0, propertySpecifications_1.parsePropertySpecification)(stringNullSpec);
94
- expect(nullResult).toEqual({
95
- slug: 'field',
96
- value: null,
97
- });
98
- expect(stringNullResult).toEqual({
99
- slug: 'field',
100
- value: '"null"',
101
- });
102
- });
103
81
  it('should throw error for spec without equals sign', () => {
104
82
  const spec = 'invalidspec';
105
83
  expect(() => (0, propertySpecifications_1.parsePropertySpecification)(spec)).toThrow('Invalid property specification: invalidspec');
@@ -175,23 +153,9 @@ describe('parsePropertySpecificationsIntoMap', () => {
175
153
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
176
154
  expect(result.get('name')).toBe('john');
177
155
  expect(result.get('description')).toBeUndefined();
178
- expect(result.get('notes')).toBe('');
156
+ expect(result.get('notes')).toBe(''); // Empty quotes are converted to empty string
179
157
  expect(result.size).toBe(3);
180
158
  });
181
- it('should handle property specifications with null values', () => {
182
- const specs = [
183
- 'name=john',
184
- 'description=null',
185
- 'notes="null"',
186
- 'empty=',
187
- ];
188
- const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
189
- expect(result.get('name')).toBe('john');
190
- expect(result.get('description')).toBeNull();
191
- expect(result.get('notes')).toBe('"null"');
192
- expect(result.get('empty')).toBeUndefined();
193
- expect(result.size).toBe(4);
194
- });
195
159
  it('should throw error for invalid property specifications', () => {
196
160
  const specs = [
197
161
  'name=john',
@@ -278,21 +242,13 @@ describe('integration tests', () => {
278
242
  const quotedSpecs = ['description=""'];
279
243
  const quotedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(quotedSpecs);
280
244
  expect(quotedMap.get('description')).toBe(''); // Quoted empty = empty string
281
- // Test case 3: Mixed usage with null
282
- const mixedSpecs = [
283
- 'name=test',
284
- 'description=',
285
- 'notes=""',
286
- 'data=null',
287
- 'status="null"',
288
- ];
245
+ // Test case 3: Mixed usage
246
+ const mixedSpecs = ['name=test', 'description=', 'notes=""'];
289
247
  const mixedMap = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(mixedSpecs);
290
248
  expect(mixedMap.get('name')).toBe('test');
291
249
  expect(mixedMap.get('description')).toBeUndefined(); // Unset
292
250
  expect(mixedMap.get('notes')).toBe(''); // Empty string
293
- expect(mixedMap.get('data')).toBeNull(); // null value
294
- expect(mixedMap.get('status')).toBe('"null"'); // String "null"
295
- expect(mixedMap.size).toBe(5);
251
+ expect(mixedMap.size).toBe(3);
296
252
  });
297
253
  });
298
254
  describe('edge cases and limitations', () => {
@@ -362,20 +318,4 @@ describe('edge cases and limitations', () => {
362
318
  value: 'value',
363
319
  });
364
320
  });
365
- it('should document all special value types', () => {
366
- // This test documents all the special value types and their parsing behavior
367
- const specs = [
368
- { input: 'a=value', expected: 'value', description: 'regular string' },
369
- { input: 'b=', expected: undefined, description: 'undefined (unset)' },
370
- { input: 'c=""', expected: '', description: 'empty string' },
371
- { input: 'd=null', expected: null, description: 'null value' },
372
- { input: 'e="null"', expected: '"null"', description: 'string "null"' },
373
- { input: 'f=123', expected: '123', description: 'numeric string' },
374
- { input: 'g=true', expected: 'true', description: 'boolean string' },
375
- ];
376
- for (const { input, expected } of specs) {
377
- const result = (0, propertySpecifications_1.parsePropertySpecification)(input);
378
- expect(result.value).toBe(expected);
379
- }
380
- });
381
321
  });
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.6.0-alpha-2",
2
+ "version": "3.6.0-alpha-4",
3
3
  "commands": {
4
4
  "app:create": {
5
5
  "id": "app:create",
@@ -604,7 +604,8 @@
604
604
  "dev",
605
605
  "development",
606
606
  "prod",
607
- "production"
607
+ "production",
608
+ "local"
608
609
  ]
609
610
  },
610
611
  "apiUserToken": {
@@ -905,22 +906,6 @@
905
906
  "required": false,
906
907
  "multiple": false
907
908
  },
908
- "skipHydrateRelationships": {
909
- "name": "skipHydrateRelationships",
910
- "type": "boolean",
911
- "char": "s",
912
- "description": "Skip hydrating relationships when fetching the entity",
913
- "required": false,
914
- "allowNo": false
915
- },
916
- "relations": {
917
- "name": "relations",
918
- "type": "option",
919
- "char": "r",
920
- "description": "Comma-separated list of relations to include in the output",
921
- "required": false,
922
- "multiple": false
923
- },
924
909
  "propertySlugs": {
925
910
  "name": "propertySlugs",
926
911
  "type": "option",
@@ -937,6 +922,22 @@
937
922
  "required": false,
938
923
  "allowNo": false
939
924
  },
925
+ "skip": {
926
+ "name": "skip",
927
+ "type": "option",
928
+ "char": "s",
929
+ "description": "Number of entities to skip (used with paginated fetch)",
930
+ "required": false,
931
+ "multiple": false
932
+ },
933
+ "skipHydrateRelationships": {
934
+ "name": "skipHydrateRelationships",
935
+ "type": "boolean",
936
+ "char": "S",
937
+ "description": "Skip hydrating relationships when fetching the entity",
938
+ "required": false,
939
+ "allowNo": false
940
+ },
940
941
  "take": {
941
942
  "name": "take",
942
943
  "type": "option",
@@ -944,6 +945,14 @@
944
945
  "description": "Number of entities to fetch in a single request (default is 100)",
945
946
  "required": false,
946
947
  "multiple": false
948
+ },
949
+ "relations": {
950
+ "name": "relations",
951
+ "type": "option",
952
+ "char": "r",
953
+ "description": "Comma-separated list of relations to include in the output",
954
+ "required": false,
955
+ "multiple": false
947
956
  }
948
957
  },
949
958
  "args": {
@@ -964,7 +973,7 @@
964
973
  "aliases": [],
965
974
  "examples": [
966
975
  "contrail entities update color 123 -- name=obsidian hex=000000",
967
- "contrail entities update color 123 -- description= notes='\"\"' data=null # Unset description, empty notes, null data"
976
+ "contrail entities update color 123 -- description= notes='\"\"' # Unset description, set notes to empty string"
968
977
  ],
969
978
  "flags": {
970
979
  "logLevel": {
@@ -1017,6 +1026,14 @@
1017
1026
  "description": "File containing properties to update the entity (optional)",
1018
1027
  "required": false,
1019
1028
  "multiple": false
1029
+ },
1030
+ "force": {
1031
+ "name": "force",
1032
+ "type": "boolean",
1033
+ "char": "F",
1034
+ "description": "Force update without validation. Internal use only",
1035
+ "required": false,
1036
+ "allowNo": false
1020
1037
  }
1021
1038
  },
1022
1039
  "args": {
@@ -1028,7 +1045,7 @@
1028
1045
  "id": {
1029
1046
  "name": "id",
1030
1047
  "description": "The unique identifier of the entity",
1031
- "required": true
1048
+ "required": false
1032
1049
  }
1033
1050
  }
1034
1051
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/cli",
3
- "version": "3.6.0-alpha-2",
3
+ "version": "3.6.0-alpha-4",
4
4
  "description": "VibeIQ's Contrail Platform CLI.",
5
5
  "bin": {
6
6
  "contrail-cli": "./bin/run",