@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/LICENSE +201 -201
- package/NOTICE +16 -16
- package/package.json +48 -48
- package/src/bin/icedq.js +84 -84
- package/src/commands/export.js +102 -102
- package/src/commands/generate-mapping.js +217 -217
- package/src/commands/import.js +174 -174
- package/src/core/client.js +183 -183
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
|
+
});
|
package/src/commands/export.js
CHANGED
|
@@ -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
|
+
}
|