@icedq/cli 0.2.0 → 0.2.1

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/src/bin/icedq.js CHANGED
@@ -1,84 +1,84 @@
1
- #!/usr/bin/env node
2
- import { Command, Option } from 'commander';
3
- import { runExport } from '../commands/export.js';
4
- import { runImport } from '../commands/import.js';
5
- import { runGenerateMapping } from '../commands/generate-mapping.js';
6
- import { CliError } from '../core/errors.js';
7
- import { log } from '../core/logger.js';
8
-
9
- const program = new Command();
10
-
11
- program
12
- .name('icedq')
13
- .description('CLI for iceDQ rule and workflow promotion')
14
- .version('0.1.0');
15
-
16
- function addGlobalOptions(cmd) {
17
- cmd
18
- .option('--icedq-url <url>', 'iceDQ instance base URL [env: ICEDQ_URL]')
19
- .option('--keycloak-url <url>', 'Keycloak token endpoint base [env: ICEDQ_KEYCLOAK_URL]')
20
- .option('--client-id <id>', 'OAuth client ID [env: ICEDQ_CLIENT_ID]')
21
- .option('--client-secret <secret>', 'OAuth client secret [env: ICEDQ_CLIENT_SECRET]')
22
- .option('--org-id <id>', '[env: ICEDQ_ORG_ID]')
23
- .option('--account-id <id>', '[env: ICEDQ_ACCOUNT_ID]')
24
- .option('--workspace-id <id>', '[env: ICEDQ_WORKSPACE_ID]')
25
- .option('--verify-ssl <bool>', 'verify TLS (default true)')
26
- .option('--timeout <seconds>', 'polling timeout in seconds', '1800')
27
- .addOption(new Option('--output <format>', 'output format').choices(['text', 'json', 'markdown']).default('text'))
28
- .option('-v, --verbose', 'verbose logging')
29
- .option('-q, --quiet', 'suppress non-error logging');
30
- return cmd;
31
- }
32
-
33
- addGlobalOptions(
34
- program
35
- .command('export')
36
- .description('Export rules, workflows, or folders to a bundle')
37
- .requiredOption('--resource <kind>', 'rule | workflow | folder')
38
- .requiredOption('--id <uuid>', 'resource UUID')
39
- .option('--include-child', 'recurse folder children (folder resource only)', false)
40
- .requiredOption('--output-file <path>', 'where to write the bundle ZIP')
41
- ).action(async (opts, cmd) => wrap(() => runExport(merge(cmd, opts))));
42
-
43
- addGlobalOptions(
44
- program
45
- .command('generate-mapping')
46
- .description('Auto-generate a mapping file from an export bundle')
47
- .requiredOption('--bundle <path>', 'path to the export ZIP')
48
- .requiredOption('--output-file <path>', 'where to write the mapping JSON')
49
- ).action(async (opts, cmd) => wrap(() => runGenerateMapping(merge(cmd, opts))));
50
-
51
- addGlobalOptions(
52
- program
53
- .command('import')
54
- .description('Import a bundle into the target workspace')
55
- .requiredOption('--bundle <path>', 'path to the export ZIP')
56
- .requiredOption('--kind <kind>', 'rules | workflows')
57
- .requiredOption('--mapping-file <path>', 'mapping JSON (auto-generation arrives in v0.2)')
58
- .option('--use-fqn', 'use FQN (name) resolution instead of UUIDs', false)
59
- .option('--strict', 'exit non-zero on any skipped rule', false)
60
- .option('--terminate-on-conflict', 'cancel any active import in the target workspace and retry', false)
61
- .option('--retain-log <path>', 'write the full import log to this path')
62
- ).action(async (opts, cmd) => wrap(() => runImport(merge(cmd, opts))));
63
-
64
- function merge(cmd, localOpts) {
65
- return { ...cmd.parent.opts(), ...cmd.opts(), ...localOpts };
66
- }
67
-
68
- async function wrap(fn) {
69
- try {
70
- await fn();
71
- } catch (err) {
72
- if (err instanceof CliError) {
73
- log.error(err.message);
74
- process.exit(err.exitCode || 1);
75
- }
76
- log.error(`Unexpected error: ${err.message}`, { stack: err.stack });
77
- process.exit(1);
78
- }
79
- }
80
-
81
- program.parseAsync(process.argv).catch((err) => {
82
- log.error(`Fatal: ${err.message}`);
83
- process.exit(1);
84
- });
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { runExport } from '../commands/export.js';
4
+ import { runImport } from '../commands/import.js';
5
+ import { runGenerateMapping } from '../commands/generate-mapping.js';
6
+ import { CliError } from '../core/errors.js';
7
+ import { log } from '../core/logger.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('icedq')
13
+ .description('CLI for iceDQ rule and workflow promotion')
14
+ .version('0.1.0');
15
+
16
+ function addGlobalOptions(cmd) {
17
+ cmd
18
+ .option('--icedq-url <url>', 'iceDQ instance base URL [env: ICEDQ_URL]')
19
+ .option('--keycloak-url <url>', 'Keycloak token endpoint base [env: ICEDQ_KEYCLOAK_URL]')
20
+ .option('--client-id <id>', 'OAuth client ID [env: ICEDQ_CLIENT_ID]')
21
+ .option('--client-secret <secret>', 'OAuth client secret [env: ICEDQ_CLIENT_SECRET]')
22
+ .option('--org-id <id>', '[env: ICEDQ_ORG_ID]')
23
+ .option('--account-id <id>', '[env: ICEDQ_ACCOUNT_ID]')
24
+ .option('--workspace-id <id>', '[env: ICEDQ_WORKSPACE_ID]')
25
+ .option('--verify-ssl <bool>', 'verify TLS (default true)')
26
+ .option('--timeout <seconds>', 'polling timeout in seconds', '1800')
27
+ .addOption(new Option('--output <format>', 'output format').choices(['text', 'json', 'markdown']).default('text'))
28
+ .option('-v, --verbose', 'verbose logging')
29
+ .option('-q, --quiet', 'suppress non-error logging');
30
+ return cmd;
31
+ }
32
+
33
+ addGlobalOptions(
34
+ program
35
+ .command('export')
36
+ .description('Export rules, workflows, or folders to a bundle')
37
+ .requiredOption('--resource <kind>', 'rule | workflow | folder')
38
+ .requiredOption('--id <uuid>', 'resource UUID')
39
+ .option('--include-child', 'recurse folder children (folder resource only)', false)
40
+ .requiredOption('--output-file <path>', 'where to write the bundle ZIP')
41
+ ).action(async (opts, cmd) => wrap(() => runExport(merge(cmd, opts))));
42
+
43
+ addGlobalOptions(
44
+ program
45
+ .command('generate-mapping')
46
+ .description('Auto-generate a mapping file from an export bundle')
47
+ .requiredOption('--bundle <path>', 'path to the export ZIP')
48
+ .requiredOption('--output-file <path>', 'where to write the mapping JSON')
49
+ ).action(async (opts, cmd) => wrap(() => runGenerateMapping(merge(cmd, opts))));
50
+
51
+ addGlobalOptions(
52
+ program
53
+ .command('import')
54
+ .description('Import a bundle into the target workspace')
55
+ .requiredOption('--bundle <path>', 'path to the export ZIP')
56
+ .requiredOption('--kind <kind>', 'rules | workflows')
57
+ .requiredOption('--mapping-file <path>', 'mapping JSON (auto-generation arrives in v0.2)')
58
+ .option('--use-fqn', 'use FQN (name) resolution instead of UUIDs', false)
59
+ .option('--strict', 'exit non-zero on any skipped rule', false)
60
+ .option('--terminate-on-conflict', 'cancel any active import in the target workspace and retry', false)
61
+ .option('--retain-log <path>', 'write the full import log to this path')
62
+ ).action(async (opts, cmd) => wrap(() => runImport(merge(cmd, opts))));
63
+
64
+ function merge(cmd, localOpts) {
65
+ return { ...cmd.parent.opts(), ...cmd.opts(), ...localOpts };
66
+ }
67
+
68
+ async function wrap(fn) {
69
+ try {
70
+ await fn();
71
+ } catch (err) {
72
+ if (err instanceof CliError) {
73
+ log.error(err.message);
74
+ process.exit(err.exitCode || 1);
75
+ }
76
+ log.error(`Unexpected error: ${err.message}`, { stack: err.stack });
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ program.parseAsync(process.argv).catch((err) => {
82
+ log.error(`Fatal: ${err.message}`);
83
+ process.exit(1);
84
+ });
@@ -1,102 +1,102 @@
1
- import { writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { loadConfig } from '../core/config.js';
4
- import { KeycloakClientCredentialsAuth } from '../core/auth.js';
5
- import { IcedqApiClient } from '../core/client.js';
6
- import { pollTask } from '../core/poller.js';
7
- import { Reporter } from '../core/reporter.js';
8
- import { isSuccess, STATUS } from '../lib/status-enum.js';
9
- import { TaskFailedError, CliError } from '../core/errors.js';
10
- import { log, setLevel } from '../core/logger.js';
11
-
12
- const VALID_RESOURCES = new Set(['rule', 'workflow', 'folder']);
13
-
14
- function endpointForResource(resource) {
15
- // Rules export uses /exports/rules; workflows + folders use /exports/workflows
16
- return resource === 'rule' ? 'rules' : 'workflows';
17
- }
18
-
19
- export async function runExport(rawOpts) {
20
- if (rawOpts.verbose) setLevel('debug');
21
- if (rawOpts.quiet) setLevel('error');
22
-
23
- const resource = rawOpts.resource;
24
- if (!VALID_RESOURCES.has(resource)) {
25
- throw new CliError(`--resource must be one of: rule, workflow, folder`);
26
- }
27
- if (!rawOpts.id) throw new CliError('--id is required');
28
- if (!rawOpts.outputFile) throw new CliError('--output-file is required');
29
-
30
- const cfg = loadConfig(rawOpts);
31
- const auth = new KeycloakClientCredentialsAuth({
32
- keycloakUrl: cfg.keycloakUrl,
33
- clientId: cfg.clientId,
34
- clientSecret: cfg.clientSecret,
35
- verifySsl: cfg.verifySsl
36
- });
37
- const client = new IcedqApiClient({
38
- baseUrl: cfg.icedqUrl,
39
- orgId: cfg.orgId,
40
- accountId: cfg.accountId,
41
- workspaceId: cfg.workspaceId,
42
- auth,
43
- verifySsl: cfg.verifySsl
44
- });
45
-
46
- const kindEndpoint = endpointForResource(resource);
47
- const body = {
48
- objects: [
49
- {
50
- id: rawOpts.id,
51
- resource,
52
- ...(rawOpts.includeChild ? { includeChild: 'true' } : {})
53
- }
54
- ]
55
- };
56
-
57
- log.info(`Submitting export`, { resource, id: rawOpts.id, endpoint: kindEndpoint });
58
- const submitResp = await client.post(`/api/v1/exports/${kindEndpoint}`, body);
59
- const taskId = submitResp.taskInstanceId;
60
- if (!taskId) {
61
- throw new CliError(`Export submit did not return a taskInstanceId: ${JSON.stringify(submitResp)}`);
62
- }
63
- process.stderr.write(`task-id: ${taskId}\n`);
64
-
65
- log.info('Polling export task', { taskId, timeoutSec: cfg.timeoutSec });
66
- const start = Date.now();
67
- const { status, attempts, elapsedMs } = await pollTask(client, 'exports', taskId, {
68
- timeoutSec: cfg.timeoutSec,
69
- onTick: ({ status: s, attempt }) => log.debug('tick', { status: s, attempt })
70
- });
71
-
72
- let outputFile = rawOpts.outputFile;
73
- let logTail;
74
-
75
- if (isSuccess(status)) {
76
- const buffer = await client.getBinary(`/api/v1/exports/${taskId}/download`);
77
- outputFile = path.resolve(outputFile);
78
- await writeFile(outputFile, buffer);
79
- log.info('Export bundle written', { outputFile, bytes: buffer.length });
80
- } else {
81
- try {
82
- logTail = await client.get(`/api/v1/exports/${taskId}/log`);
83
- } catch (err) {
84
- log.warn('Could not retrieve task log', { error: err.message });
85
- }
86
- }
87
-
88
- const result = {
89
- command: 'export',
90
- taskId,
91
- status,
92
- attempts,
93
- durationMs: elapsedMs,
94
- outputFile: isSuccess(status) ? outputFile : undefined,
95
- elapsedSinceSubmitMs: Date.now() - start
96
- };
97
- new Reporter(rawOpts.output || 'text').emit(result);
98
-
99
- if (!isSuccess(status)) {
100
- throw new TaskFailedError(taskId, 'export', status, logTail);
101
- }
102
- }
1
+ import { writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../core/config.js';
4
+ import { KeycloakClientCredentialsAuth } from '../core/auth.js';
5
+ import { IcedqApiClient } from '../core/client.js';
6
+ import { pollTask } from '../core/poller.js';
7
+ import { Reporter } from '../core/reporter.js';
8
+ import { isSuccess, STATUS } from '../lib/status-enum.js';
9
+ import { TaskFailedError, CliError } from '../core/errors.js';
10
+ import { log, setLevel } from '../core/logger.js';
11
+
12
+ const VALID_RESOURCES = new Set(['rule', 'workflow', 'folder']);
13
+
14
+ function endpointForResource(resource) {
15
+ // Rules export uses /exports/rules; workflows + folders use /exports/workflows
16
+ return resource === 'rule' ? 'rules' : 'workflows';
17
+ }
18
+
19
+ export async function runExport(rawOpts) {
20
+ if (rawOpts.verbose) setLevel('debug');
21
+ if (rawOpts.quiet) setLevel('error');
22
+
23
+ const resource = rawOpts.resource;
24
+ if (!VALID_RESOURCES.has(resource)) {
25
+ throw new CliError(`--resource must be one of: rule, workflow, folder`);
26
+ }
27
+ if (!rawOpts.id) throw new CliError('--id is required');
28
+ if (!rawOpts.outputFile) throw new CliError('--output-file is required');
29
+
30
+ const cfg = loadConfig(rawOpts);
31
+ const auth = new KeycloakClientCredentialsAuth({
32
+ keycloakUrl: cfg.keycloakUrl,
33
+ clientId: cfg.clientId,
34
+ clientSecret: cfg.clientSecret,
35
+ verifySsl: cfg.verifySsl
36
+ });
37
+ const client = new IcedqApiClient({
38
+ baseUrl: cfg.icedqUrl,
39
+ orgId: cfg.orgId,
40
+ accountId: cfg.accountId,
41
+ workspaceId: cfg.workspaceId,
42
+ auth,
43
+ verifySsl: cfg.verifySsl
44
+ });
45
+
46
+ const kindEndpoint = endpointForResource(resource);
47
+ const body = {
48
+ objects: [
49
+ {
50
+ id: rawOpts.id,
51
+ resource,
52
+ ...(rawOpts.includeChild ? { includeChild: 'true' } : {})
53
+ }
54
+ ]
55
+ };
56
+
57
+ log.info(`Submitting export`, { resource, id: rawOpts.id, endpoint: kindEndpoint });
58
+ const submitResp = await client.post(`/api/v1/exports/${kindEndpoint}`, body);
59
+ const taskId = submitResp.taskInstanceId;
60
+ if (!taskId) {
61
+ throw new CliError(`Export submit did not return a taskInstanceId: ${JSON.stringify(submitResp)}`);
62
+ }
63
+ process.stderr.write(`task-id: ${taskId}\n`);
64
+
65
+ log.info('Polling export task', { taskId, timeoutSec: cfg.timeoutSec });
66
+ const start = Date.now();
67
+ const { status, attempts, elapsedMs } = await pollTask(client, 'exports', taskId, {
68
+ timeoutSec: cfg.timeoutSec,
69
+ onTick: ({ status: s, attempt }) => log.debug('tick', { status: s, attempt })
70
+ });
71
+
72
+ let outputFile = rawOpts.outputFile;
73
+ let logTail;
74
+
75
+ if (isSuccess(status)) {
76
+ const buffer = await client.getBinary(`/api/v1/exports/${taskId}/download`);
77
+ outputFile = path.resolve(outputFile);
78
+ await writeFile(outputFile, buffer);
79
+ log.info('Export bundle written', { outputFile, bytes: buffer.length });
80
+ } else {
81
+ try {
82
+ logTail = await client.get(`/api/v1/exports/${taskId}/log`);
83
+ } catch (err) {
84
+ log.warn('Could not retrieve task log', { error: err.message });
85
+ }
86
+ }
87
+
88
+ const result = {
89
+ command: 'export',
90
+ taskId,
91
+ status,
92
+ attempts,
93
+ durationMs: elapsedMs,
94
+ outputFile: isSuccess(status) ? outputFile : undefined,
95
+ elapsedSinceSubmitMs: Date.now() - start
96
+ };
97
+ new Reporter(rawOpts.output || 'text').emit(result);
98
+
99
+ if (!isSuccess(status)) {
100
+ throw new TaskFailedError(taskId, 'export', status, logTail);
101
+ }
102
+ }