@contrail/cli 3.6.0-alpha-8-alpha.6d0c626 → 3.6.0-alpha-8-alpha.e4ae6bd

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.
@@ -6,6 +6,7 @@ const core_1 = require("@oclif/core");
6
6
  const basecommand_1 = require("../../basecommand");
7
7
  const sdk_1 = require("@contrail/sdk");
8
8
  const login_1 = require("../../auth/login");
9
+ const common_1 = require("../../common");
9
10
  const types_1 = require("../../api/types");
10
11
  const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
11
12
  const batchFetch_1 = require("../../helpers/entities/batchFetch");
@@ -14,6 +15,8 @@ let MAX_NUMBER_OF_ENTITIES_TO_FETCH = undefined;
14
15
  class GetEntity extends basecommand_1.BaseCommand {
15
16
  static description = '(BETA) Get an entity';
16
17
  static examples = [
18
+ 'contrail entities get color 123',
19
+ 'contrail entities get color id1 id2 id3',
17
20
  'contrail entities get color -- id=123',
18
21
  'contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16',
19
22
  'contrail entities get orders -- updatedAt=2025-09-15T04:00:00Z..',
@@ -65,10 +68,15 @@ class GetEntity extends basecommand_1.BaseCommand {
65
68
  description: 'The name of the entity type to fetch',
66
69
  required: true,
67
70
  }),
71
+ ids: core_1.Args.string({
72
+ description: 'One or more entity ids; shorthand for the id/ids criteria',
73
+ required: false,
74
+ }),
68
75
  };
69
76
  static strict = false;
70
77
  async run() {
71
- this.log('🚨 This command is in BETA and may change or be removed at any time.');
78
+ (0, common_1.routeConsoleLogToStderr)();
79
+ console.error('🚨 This command is in BETA and may change or be removed at any time.');
72
80
  await (0, login_1.authUser)();
73
81
  const { args, flags, argv } = await this.parse(GetEntity);
74
82
  const { typePath } = args;
@@ -76,11 +84,23 @@ class GetEntity extends basecommand_1.BaseCommand {
76
84
  SHOULD_USE_PAGINATED_FIND = shouldUsePaginatedFind;
77
85
  MAX_NUMBER_OF_ENTITIES_TO_FETCH = take;
78
86
  if (MAX_NUMBER_OF_ENTITIES_TO_FETCH)
79
- console.log(`Will limit output to ${MAX_NUMBER_OF_ENTITIES_TO_FETCH} entities.`);
80
- const propertySpecificationArgs = argv
81
- .slice(1, argv.length)
82
- .filter(propertySpecifications_1.isValidPropertySpecification);
83
- const propertySpecBySlug = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(propertySpecificationArgs);
87
+ console.error(`Will limit output to ${MAX_NUMBER_OF_ENTITIES_TO_FETCH} entities.`);
88
+ const { ids: positionalIds, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
89
+ const propertySpecBySlug = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
90
+ if (positionalIds.length > 0) {
91
+ if (propertySpecBySlug.has('id') || propertySpecBySlug.has('ids')) {
92
+ this.error('Pass ids positionally or as -- id=/ids= criteria, not both.');
93
+ }
94
+ if (propertySpecBySlug.size > 0) {
95
+ this.error('Positional ids cannot be combined with other -- criteria. Use -- ids= alongside the other criteria instead.');
96
+ }
97
+ if (positionalIds.length === 1) {
98
+ propertySpecBySlug.set('id', positionalIds[0]);
99
+ }
100
+ else {
101
+ propertySpecBySlug.set('ids', positionalIds);
102
+ }
103
+ }
84
104
  const relations = relationString
85
105
  ? relationString
86
106
  .split(',')
@@ -115,7 +135,7 @@ class GetEntity extends basecommand_1.BaseCommand {
115
135
  }
116
136
  writer.write('\n]\n');
117
137
  if (outputFile)
118
- this.log(`Output saved to ${outputFile}`);
138
+ console.error(`Output saved to ${outputFile}`);
119
139
  }
120
140
  else {
121
141
  const response = await getEntityByTypePath(typePath, propertySpecBySlug, {
@@ -127,7 +147,7 @@ class GetEntity extends basecommand_1.BaseCommand {
127
147
  });
128
148
  if (outputFile) {
129
149
  fs_1.default.writeFileSync(outputFile, JSON.stringify(response, null, 2));
130
- this.log(`Output saved to ${outputFile}`);
150
+ console.error(`Output saved to ${outputFile}`);
131
151
  }
132
152
  else {
133
153
  this.log(JSON.stringify(response, null, 2));
@@ -212,7 +232,7 @@ async function* streamPaginatedFind(typePath, options = {}) {
212
232
  if (MAX_NUMBER_OF_ENTITIES_TO_FETCH && count >= MAX_NUMBER_OF_ENTITIES_TO_FETCH)
213
233
  return;
214
234
  }
215
- console.log(`Fetched ${count} entities`);
235
+ console.error(`Fetched ${count} entities`);
216
236
  if (!nextPageKey || results.length === 0)
217
237
  return;
218
238
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const create_1 = tslib_1.__importDefault(require("./create"));
5
+ const get_1 = tslib_1.__importDefault(require("./get"));
6
+ const update_1 = tslib_1.__importDefault(require("./update"));
7
+ const delete_1 = tslib_1.__importDefault(require("./delete"));
8
+ const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
9
+ describe('entities command grammar: entities <verb> <typePath> [ID...]', () => {
10
+ describe('partitionIdsAndSpecs', () => {
11
+ it('should treat tokens without an equals sign as ids', () => {
12
+ const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['id1', 'id2', 'id3']);
13
+ expect(ids).toEqual(['id1', 'id2', 'id3']);
14
+ expect(specs).toEqual([]);
15
+ });
16
+ it('should treat slug=value tokens as property specifications', () => {
17
+ const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['name=jet', 'hex=000000']);
18
+ expect(ids).toEqual([]);
19
+ expect(specs).toEqual(['name=jet', 'hex=000000']);
20
+ });
21
+ it('should split mixed tokens preserving order within each group', () => {
22
+ const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['id1', 'name=jet', 'id2', 'status=archived']);
23
+ expect(ids).toEqual(['id1', 'id2']);
24
+ expect(specs).toEqual(['name=jet', 'status=archived']);
25
+ });
26
+ it('should not classify malformed specs as ids', () => {
27
+ const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(['=value']);
28
+ expect(ids).toEqual([]);
29
+ expect(specs).toEqual([]);
30
+ });
31
+ it('should handle empty input', () => {
32
+ const { ids, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)([]);
33
+ expect(ids).toEqual([]);
34
+ expect(specs).toEqual([]);
35
+ });
36
+ });
37
+ describe('positional id arguments', () => {
38
+ it('get accepts variadic positional ids', () => {
39
+ expect(get_1.default.strict).toBe(false);
40
+ expect(get_1.default.args.ids).toBeDefined();
41
+ });
42
+ it('update accepts variadic positional ids and has no single id arg', () => {
43
+ expect(update_1.default.strict).toBe(false);
44
+ expect(update_1.default.args.ids).toBeDefined();
45
+ expect(update_1.default.args.id).toBeUndefined();
46
+ });
47
+ it('delete accepts variadic positional ids and has no --id flag', () => {
48
+ expect(delete_1.default.strict).toBe(false);
49
+ expect(delete_1.default.args.ids).toBeDefined();
50
+ expect(delete_1.default.flags.id).toBeUndefined();
51
+ });
52
+ });
53
+ describe('flag alignment across verbs', () => {
54
+ it('the input file is -i, --inputFile on every verb that accepts one', () => {
55
+ expect(create_1.default.flags.inputFile.char).toBe('i');
56
+ expect(update_1.default.flags.inputFile.char).toBe('i');
57
+ expect(delete_1.default.flags.inputFile.char).toBe('i');
58
+ expect(delete_1.default.flags.input).toBeUndefined();
59
+ });
60
+ it('skipHydrateRelationships is -S on every verb', () => {
61
+ expect(create_1.default.flags.skipHydrateRelationships.char).toBe('S');
62
+ expect(get_1.default.flags.skipHydrateRelationships.char).toBe('S');
63
+ expect(update_1.default.flags.skipHydrateRelationships.char).toBe('S');
64
+ });
65
+ it('get keeps -s for --skip', () => {
66
+ expect(get_1.default.flags.skip.char).toBe('s');
67
+ });
68
+ });
69
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const create_1 = require("./create");
4
+ const update_1 = require("./update");
5
+ const fulfilled = (value) => ({ status: 'fulfilled', value });
6
+ const rejected = (message) => ({
7
+ status: 'rejected',
8
+ reason: new Error(message),
9
+ });
10
+ describe('mutation output summaries', () => {
11
+ describe('summarizeSettledCreates', () => {
12
+ it('should put every fulfilled result under created', () => {
13
+ const inputs = [{ name: 'a' }, { name: 'b' }];
14
+ const summary = (0, create_1.summarizeSettledCreates)(inputs, [
15
+ fulfilled({ id: '1', name: 'a' }),
16
+ fulfilled({ id: '2', name: 'b' }),
17
+ ]);
18
+ expect(summary.created).toEqual([
19
+ { id: '1', name: 'a' },
20
+ { id: '2', name: 'b' },
21
+ ]);
22
+ expect(summary.failed).toEqual([]);
23
+ });
24
+ it('should pair each failed input with its error so it can be retried as-is', () => {
25
+ const inputs = [{ itemId: 'itm-1' }, { itemId: 'itm-41x' }, { itemId: 'itm-3' }];
26
+ const summary = (0, create_1.summarizeSettledCreates)(inputs, [
27
+ fulfilled({ id: 'ai-1' }),
28
+ rejected('Item with ID itm-41x does not exist'),
29
+ fulfilled({ id: 'ai-3' }),
30
+ ]);
31
+ expect(summary.created).toEqual([{ id: 'ai-1' }, { id: 'ai-3' }]);
32
+ expect(summary.failed).toEqual([
33
+ { input: { itemId: 'itm-41x' }, error: 'Item with ID itm-41x does not exist' },
34
+ ]);
35
+ });
36
+ it('should fall back to details when the SDK error message is empty', () => {
37
+ const sdkError = Object.assign(new Error(''), {
38
+ details: '{"message":"Assortment not found.","statusCode":404}',
39
+ });
40
+ const summary = (0, create_1.summarizeSettledCreates)([{ assortmentId: 'bogus' }], [{ status: 'rejected', reason: sdkError }]);
41
+ expect(summary.failed[0].error).toBe('{"message":"Assortment not found.","statusCode":404}');
42
+ });
43
+ it('should stringify reasons that are not Errors', () => {
44
+ const summary = (0, create_1.summarizeSettledCreates)([{ name: 'a' }], [{ status: 'rejected', reason: 'boom' }]);
45
+ expect(summary.failed).toEqual([{ input: { name: 'a' }, error: 'boom' }]);
46
+ });
47
+ it('should handle empty input', () => {
48
+ expect((0, create_1.summarizeSettledCreates)([], [])).toEqual({ created: [], failed: [] });
49
+ });
50
+ });
51
+ describe('summarizeSettledUpdates', () => {
52
+ it('should report updated, failed, and skipped without stopping on failures', () => {
53
+ const inputs = [{ id: 'id1' }, { id: 'id2' }, { id: 'id3' }];
54
+ const summary = (0, update_1.summarizeSettledUpdates)(inputs, [
55
+ fulfilled({ id: 'id1', status: 'archived' }),
56
+ rejected('Entity not found.'),
57
+ fulfilled({ id: 'id3', status: 'archived' }),
58
+ ], 2);
59
+ expect(summary.updated).toEqual([
60
+ { id: 'id1', status: 'archived' },
61
+ { id: 'id3', status: 'archived' },
62
+ ]);
63
+ expect(summary.failed).toEqual([{ id: 'id2', error: 'Entity not found.' }]);
64
+ expect(summary.skipped).toBe(2);
65
+ });
66
+ it('should not report a failed id as updated', () => {
67
+ const summary = (0, update_1.summarizeSettledUpdates)([{ id: 'id1' }], [rejected('boom')], 0);
68
+ expect(summary.updated).toEqual([]);
69
+ expect(summary.failed).toEqual([{ id: 'id1', error: 'boom' }]);
70
+ });
71
+ });
72
+ });
@@ -12,8 +12,19 @@ export default class UpdateEntity extends BaseCommand {
12
12
  };
13
13
  static args: {
14
14
  entityName: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
15
- id: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
15
+ ids: import("@oclif/core/lib/interfaces/parser").Arg<string, Record<string, unknown>>;
16
16
  };
17
17
  static strict: boolean;
18
18
  run(): Promise<void>;
19
19
  }
20
+ export type UpdateSummary = {
21
+ updated: any[];
22
+ failed: {
23
+ id: string;
24
+ error: string;
25
+ }[];
26
+ skipped: number;
27
+ };
28
+ export declare function summarizeSettledUpdates(inputs: {
29
+ id: string;
30
+ }[], settled: PromiseSettledResult<any>[], skipped: number): UpdateSummary;
@@ -1,23 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarizeSettledUpdates = void 0;
3
4
  const tslib_1 = require("tslib");
4
5
  const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
5
6
  const core_1 = require("@oclif/core");
6
7
  const basecommand_1 = require("../../basecommand");
7
8
  const sdk_1 = require("@contrail/sdk");
8
9
  const login_1 = require("../../auth/login");
10
+ const common_1 = require("../../common");
9
11
  const propertySpecifications_1 = require("../../helpers/propertySpecifications/propertySpecifications");
10
12
  const file_helpers_1 = require("../../helpers/file-helpers");
11
13
  class UpdateEntity extends basecommand_1.BaseCommand {
12
14
  static description = '(BETA) Update an entity';
13
15
  static examples = [
14
16
  'contrail entities update color 123 -- name=obsidian hex=000000',
17
+ 'contrail entities update item id1 id2 id3 -- status=archived # apply the same change to several entities',
15
18
  'contrail entities update color 123 -- description= notes=\'""\' # Unset description, set notes to empty string',
16
19
  ];
17
20
  static flags = {
18
21
  ...basecommand_1.BaseCommand.baseFlags,
19
22
  skipHydrateRelationships: core_1.Flags.boolean({
20
- char: 's',
23
+ char: 'S',
21
24
  description: 'Skip hydrating relationships when updating the entity',
22
25
  required: false,
23
26
  default: false,
@@ -44,33 +47,38 @@ class UpdateEntity extends basecommand_1.BaseCommand {
44
47
  description: 'The name of the entity type to update',
45
48
  required: true,
46
49
  }),
47
- id: core_1.Args.string({
48
- description: 'The unique identifier of the entity',
50
+ ids: core_1.Args.string({
51
+ description: 'One or more entity ids to update',
49
52
  required: false,
50
53
  }),
51
54
  };
52
55
  static strict = false;
53
56
  async run() {
54
- this.log('🚨 This command is in BETA and may change or be removed at any time.');
57
+ (0, common_1.routeConsoleLogToStderr)();
58
+ console.error('🚨 This command is in BETA and may change or be removed at any time.');
55
59
  await (0, login_1.authUser)();
56
60
  const { args, argv } = await this.parse(UpdateEntity);
57
- const { id: specifiedId, entityName } = args;
61
+ const { entityName } = args;
58
62
  const { dryRun: isDryRun, inputFile: inputFilename, skipHydrateRelationships, force, } = this.flags;
59
- const propertySpecArgs = argv.slice(1, argv.length).filter(propertySpecifications_1.isValidPropertySpecification);
60
- const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(propertySpecArgs);
61
- if (inputFilename && specifiedId) {
62
- throw new Error('You cannot use --input and --id together. Please specify one or the other.');
63
+ const { ids: positionalIds, specs } = (0, propertySpecifications_1.partitionIdsAndSpecs)(argv.slice(1));
64
+ const propertySpec = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
65
+ if (propertySpec.has('id') || propertySpec.has('ids')) {
66
+ this.error('The -- payload is values to write. Pass ids positionally: contrail entities update <typePath> <ID...> -- slug=value');
67
+ }
68
+ if (inputFilename && positionalIds.length > 0) {
69
+ this.error('You cannot use --inputFile and positional ids together. Please specify one or the other.');
70
+ }
71
+ if (!inputFilename && positionalIds.length === 0) {
72
+ this.error('You must provide one or more ids or an input file (-i) of entities to update.');
73
+ }
74
+ if (positionalIds.length > 0 && propertySpec.size === 0) {
75
+ this.error('Nothing to update: pass values to write after --, e.g. -- name=jet');
63
76
  }
64
77
  const entitiesToUpdate = inputFilename
65
78
  ? (0, file_helpers_1.parseFileAsJsonArray)(inputFilename)
66
- : [{ id: specifiedId }];
67
- const results = {
68
- idsSuccessfullyUpdated: new Set(),
69
- idsFailedToUpdate: new Set(),
70
- numEntriesSkipped: entitiesToUpdate.filter(({ id }) => !id).length,
71
- };
79
+ : positionalIds.map((id) => ({ id }));
80
+ const skipped = entitiesToUpdate.filter(({ id }) => !id).length;
72
81
  const entitiesWithId = entitiesToUpdate.filter(({ id }) => id);
73
- propertySpec.delete('id'); // Remove 'id' from the property spec if it exists
74
82
  // will be applied to all entities
75
83
  const fixedValues = [...propertySpec.entries()].reduce((acc, [key, value]) => {
76
84
  acc[key] = value;
@@ -79,35 +87,48 @@ class UpdateEntity extends basecommand_1.BaseCommand {
79
87
  async function updateEntity(entity) {
80
88
  const newEntityState = { ...entity, ...fixedValues };
81
89
  const id = entity.id;
82
- const promise = isDryRun
90
+ return isDryRun
83
91
  ? mockUpdateById(entityName, id, newEntityState, { skipHydrateRelationships })
84
92
  : updateEntityById(entityName, id, newEntityState, { skipHydrateRelationships, force });
85
- promise
86
- .catch(() => {
87
- results.idsFailedToUpdate.add(id);
88
- })
89
- .then(() => {
90
- results.idsSuccessfullyUpdated.add(id);
91
- });
92
- return promise;
93
93
  }
94
94
  try {
95
95
  const limit = (0, p_limit_1.default)(20);
96
- const responses = await Promise.all(entitiesWithId.map((entity) => limit(() => updateEntity(entity))));
97
- const messagePrefix = isDryRun
98
- ? 'Dry run. Update skipped. Changes would be:'
99
- : 'Entity updated successfully:';
100
- this.log(`${messagePrefix}\n\n${JSON.stringify(responses, null, 2)}`);
101
- this.log(`\nIDs successfully updated: ${Array.from(results.idsSuccessfullyUpdated).join(', ')}`);
102
- this.log(`IDs failed to update: ${Array.from(results.idsFailedToUpdate).join(', ')}`);
103
- this.log(`Number of entries skipped due to missing IDs: ${results.numEntriesSkipped}`);
96
+ const settled = await Promise.allSettled(entitiesWithId.map((entity) => limit(() => updateEntity(entity))));
97
+ const summary = summarizeSettledUpdates(entitiesWithId, settled, skipped);
98
+ if (isDryRun) {
99
+ console.error('Dry run. Update skipped. Changes would be:');
100
+ }
101
+ this.log(JSON.stringify(summary, null, 2));
102
+ if (summary.failed.length > 0) {
103
+ console.error(`${summary.failed.length} of ${entitiesWithId.length} updates failed.`);
104
+ process.exitCode = 1;
105
+ }
104
106
  }
105
107
  catch (e) {
106
- this.error(`Error fetching entity: ${e.message}`);
108
+ this.error(`Error updating entities: ${e.message}`);
107
109
  }
108
110
  }
109
111
  }
110
112
  exports.default = UpdateEntity;
113
+ // Every row is attempted; a failed row never stops the run. The summary pairs
114
+ // each failed id with its error so the failed rows can be retried.
115
+ function summarizeSettledUpdates(inputs, settled, skipped) {
116
+ const updated = [];
117
+ const failed = [];
118
+ settled.forEach((result, index) => {
119
+ if (result.status === 'fulfilled') {
120
+ updated.push(result.value);
121
+ }
122
+ else {
123
+ failed.push({
124
+ id: inputs[index].id,
125
+ error: (0, common_1.sdkErrorMessage)(result.reason),
126
+ });
127
+ }
128
+ });
129
+ return { updated, failed, skipped };
130
+ }
131
+ exports.summarizeSettledUpdates = summarizeSettledUpdates;
111
132
  async function updateEntityById(entityName, id, changes, options = {}) {
112
133
  const client = new sdk_1.Entities();
113
134
  const { skipHydrateRelationships, force } = options;
package/dist/common.d.ts CHANGED
@@ -2,3 +2,5 @@
2
2
  export declare function getSeparatorByPlatform(): string;
3
3
  export declare function delay(ms: number): Promise<NodeJS.Timeout>;
4
4
  export declare function removeSlash(string: string): string;
5
+ export declare function routeConsoleLogToStderr(): void;
6
+ export declare function sdkErrorMessage(reason: any): string;
package/dist/common.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.removeSlash = exports.delay = exports.getSeparatorByPlatform = void 0;
3
+ exports.sdkErrorMessage = exports.routeConsoleLogToStderr = exports.removeSlash = exports.delay = exports.getSeparatorByPlatform = void 0;
4
4
  function getSeparatorByPlatform() {
5
5
  const isWin = process.platform === 'win32';
6
6
  const separator = isWin ? '\\' : '/';
@@ -16,3 +16,15 @@ function removeSlash(string) {
16
16
  return string.replace(re, '-');
17
17
  }
18
18
  exports.removeSlash = removeSlash;
19
+ // The SDK and auth flow print request logs and token messages via bare
20
+ // console.log. A command whose stdout is a data contract must route those to
21
+ // stderr before any of that code runs.
22
+ function routeConsoleLogToStderr() {
23
+ console.log = (...args) => console.error(...args);
24
+ }
25
+ exports.routeConsoleLogToStderr = routeConsoleLogToStderr;
26
+ // SDK errors can carry an empty message with the response body in details
27
+ function sdkErrorMessage(reason) {
28
+ return reason?.message || reason?.details || String(reason);
29
+ }
30
+ exports.sdkErrorMessage = sdkErrorMessage;
@@ -29,4 +29,13 @@ export type PropertySpecificationBySlug = Map<string, string | number | string[]
29
29
  export type PropertySpecificationString = `${string}=${string}`;
30
30
  export declare function parsePropertySpecification(spec: PropertySpecificationString): PropertySpecification;
31
31
  export declare function parsePropertySpecificationsIntoMap(propertySpecifications: PropertySpecificationString[]): PropertySpecificationBySlug;
32
+ /**
33
+ * Splits command argv tokens (after the typePath) into positional ids and
34
+ * property specifications. A token containing '=' is a property specification
35
+ * candidate; every other token is an id.
36
+ */
37
+ export declare function partitionIdsAndSpecs(tokens: string[]): {
38
+ ids: string[];
39
+ specs: PropertySpecificationString[];
40
+ };
32
41
  export declare function isValidPropertySpecification(maybeSpec: string): maybeSpec is PropertySpecificationString;
@@ -23,7 +23,7 @@
23
23
  * contrail entities get loader-processes -- createdOn=2025-09-15..2025-09-16
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.isValidPropertySpecification = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
26
+ exports.isValidPropertySpecification = exports.partitionIdsAndSpecs = exports.parsePropertySpecificationsIntoMap = exports.parsePropertySpecification = void 0;
27
27
  const dateRanges_1 = require("./dateRanges");
28
28
  // Special tokens for empty strings
29
29
  const DOUBLE_QUOTED_EMPTY = '""';
@@ -64,23 +64,37 @@ function parsePropertySpecificationsIntoMap(propertySpecifications) {
64
64
  continue;
65
65
  }
66
66
  const { slug, value } = parsePropertySpecification(spec);
67
- if (slug === 'roles' && value !== undefined) {
68
- // Special handling for roles, which can be an array (only if value is not undefined)
69
- const rolesArray = value.split(',').map((role) => role.trim());
70
- propertySpecBySlug.set(slug, rolesArray);
71
- continue;
72
- }
73
67
  if (slug === 'ids' && value !== undefined) {
74
- // Special handling for ids, which can be an array (only if value is not undefined)
68
+ // ids is the one comma-split exception: it is a list of ids by definition
69
+ // and ids never contain commas.
75
70
  const idsArray = value.split(',').map((id) => id.trim());
76
71
  propertySpecBySlug.set(slug, idsArray);
77
72
  continue;
78
73
  }
74
+ // Repeating a key accumulates values into an array; commas in every other
75
+ // value are literal.
76
+ const existing = propertySpecBySlug.get(slug);
77
+ if (propertySpecBySlug.has(slug) && existing !== undefined && value !== undefined) {
78
+ propertySpecBySlug.set(slug, Array.isArray(existing) ? [...existing, value] : [existing, value]);
79
+ continue;
80
+ }
79
81
  propertySpecBySlug.set(slug, value);
80
82
  }
81
83
  return propertySpecBySlug;
82
84
  }
83
85
  exports.parsePropertySpecificationsIntoMap = parsePropertySpecificationsIntoMap;
86
+ /**
87
+ * Splits command argv tokens (after the typePath) into positional ids and
88
+ * property specifications. A token containing '=' is a property specification
89
+ * candidate; every other token is an id.
90
+ */
91
+ function partitionIdsAndSpecs(tokens) {
92
+ return {
93
+ ids: tokens.filter((token) => !token.includes('=')),
94
+ specs: tokens.filter(isValidPropertySpecification),
95
+ };
96
+ }
97
+ exports.partitionIdsAndSpecs = partitionIdsAndSpecs;
84
98
  function isValidPropertySpecification(maybeSpec) {
85
99
  try {
86
100
  if ((0, dateRanges_1.isDateRangeSpecification)(maybeSpec)) {
@@ -142,12 +142,17 @@ describe('parsePropertySpecificationsIntoMap', () => {
142
142
  expect(result.get('url')).toBe('https://example.com');
143
143
  expect(result.size).toBe(3);
144
144
  });
145
- it('should handle duplicate slugs by overwriting with last value', () => {
145
+ it('should accumulate repeated keys into an array', () => {
146
146
  const specs = ['name=first', 'name=second', 'name=third'];
147
147
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
148
- expect(result.get('name')).toBe('third');
148
+ expect(result.get('name')).toEqual(['first', 'second', 'third']);
149
149
  expect(result.size).toBe(1);
150
150
  });
151
+ it('should keep a single value scalar', () => {
152
+ const specs = ['status=archived'];
153
+ const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
154
+ expect(result.get('status')).toBe('archived');
155
+ });
151
156
  it('should handle property specifications with empty values', () => {
152
157
  const specs = ['name=john', 'description=', 'notes=""'];
153
158
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
@@ -163,21 +168,21 @@ describe('parsePropertySpecificationsIntoMap', () => {
163
168
  ];
164
169
  expect(() => (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs)).toThrow('Invalid property specification: invalid');
165
170
  });
166
- it('should handle roles as comma-separated array', () => {
167
- const specs = ['roles=option,family', 'name=Test'];
171
+ it('should build roles arrays from repeated keys, with commas literal', () => {
172
+ const specs = ['roles=option', 'roles=family', 'name=Test'];
168
173
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
169
174
  expect(result.get('roles')).toEqual(['option', 'family']);
170
175
  expect(result.get('name')).toBe('Test');
171
176
  });
172
- it('should handle roles as single value', () => {
173
- const specs = ['roles=option'];
177
+ it('should treat a comma in a roles value as literal', () => {
178
+ const specs = ['roles=option,family'];
174
179
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
175
- expect(result.get('roles')).toEqual(['option']);
180
+ expect(result.get('roles')).toBe('option,family');
176
181
  });
177
- it('should trim whitespace in roles array', () => {
178
- const specs = ['roles=option, family, variant'];
182
+ it('should keep commas literal in ordinary values', () => {
183
+ const specs = ['name=my,name'];
179
184
  const result = (0, propertySpecifications_1.parsePropertySpecificationsIntoMap)(specs);
180
- expect(result.get('roles')).toEqual(['option', 'family', 'variant']);
185
+ expect(result.get('name')).toBe('my,name');
181
186
  });
182
187
  it('should handle ids as comma-separated array', () => {
183
188
  const specs = ['ids=1,2,3', 'name=Test'];
package/dist/logger.js CHANGED
@@ -12,7 +12,10 @@ class Logger {
12
12
  debug: 'blue',
13
13
  };
14
14
  constructor() {
15
- const transport = new winston_1.transports.Console();
15
+ // All log output goes to stderr so stdout carries only command output (JSON)
16
+ const transport = new winston_1.transports.Console({
17
+ stderrLevels: ['error', 'warn', 'info', 'debug'],
18
+ });
16
19
  winston_1.default.addColors(this.colors);
17
20
  this.logger = (0, winston_1.createLogger)({
18
21
  level: 'info',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const logger_1 = tslib_1.__importDefault(require("./logger"));
5
+ describe('logger', () => {
6
+ it('writes every level to stderr so stdout carries only command output', () => {
7
+ const consoleTransport = logger_1.default.transports[0];
8
+ for (const level of ['error', 'warn', 'info', 'debug']) {
9
+ expect(consoleTransport.stderrLevels[level]).toBe(true);
10
+ }
11
+ });
12
+ });