rubrkit 0.1.1 → 0.3.0
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 +417 -404
- package/package.json +28 -28
- package/src/api.js +108 -101
- package/src/args.js +209 -175
- package/src/cli.js +97 -93
- package/src/config.js +186 -169
- package/src/formats.js +239 -222
- package/src/pull.js +682 -676
- package/src/sdk.js +451 -443
- package/src/testingCli.js +504 -431
package/src/cli.js
CHANGED
|
@@ -1,93 +1,97 @@
|
|
|
1
|
-
import { parseArgs } from './args.js';
|
|
2
|
-
import { resolveConfig } from './config.js';
|
|
3
|
-
import { RubrkitCliError } from './errors.js';
|
|
4
|
-
import { runPull } from './pull.js';
|
|
5
|
-
import { runTestingCommand } from './testingCli.js';
|
|
6
|
-
|
|
7
|
-
const HELP_TEXT = `Rubrkit CLI
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
rubrkit pull [all|<artifact-bundle-or-artifact-selector>] [options]
|
|
11
|
-
rubrkit
|
|
12
|
-
rubrkit
|
|
13
|
-
rubrkit
|
|
14
|
-
rubrkit
|
|
15
|
-
rubrkit
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--artifact <id-or-
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
import { parseArgs } from './args.js';
|
|
2
|
+
import { resolveConfig } from './config.js';
|
|
3
|
+
import { RubrkitCliError } from './errors.js';
|
|
4
|
+
import { runPull } from './pull.js';
|
|
5
|
+
import { runTestingCommand } from './testingCli.js';
|
|
6
|
+
|
|
7
|
+
const HELP_TEXT = `Rubrkit CLI
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
rubrkit pull [all|<artifact-bundle-or-artifact-selector>] [options]
|
|
11
|
+
rubrkit pull --label <name> [--label <name> ...] [options]
|
|
12
|
+
rubrkit validate <path|glob> [options]
|
|
13
|
+
rubrkit test <path|glob|artifact-bundle-or-artifact-selector> [options]
|
|
14
|
+
rubrkit audit <artifact-bundle-or-selector> [options]
|
|
15
|
+
rubrkit eval <artifact-bundle-or-selector> [options]
|
|
16
|
+
rubrkit report <job-or-run-id> [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--destination <path> Destination root. Defaults to the current directory.
|
|
20
|
+
--agent <auto|codex|claude|generic>
|
|
21
|
+
--artifact-bundle <id-or-name>
|
|
22
|
+
--artifact <id-or-path>
|
|
23
|
+
--label <name> Pull every artifact in bundles carrying this label.
|
|
24
|
+
Repeatable and comma-separated; matches ANY (OR).
|
|
25
|
+
--rubric <rubric-id-or-path>
|
|
26
|
+
--format <text|json|junit>
|
|
27
|
+
--output <path>
|
|
28
|
+
--fail-under <score>
|
|
29
|
+
--fail-on <critical|high|medium|low>
|
|
30
|
+
--ci
|
|
31
|
+
--local
|
|
32
|
+
--remote
|
|
33
|
+
--no-ai
|
|
34
|
+
--no-cache Bypass the audit result cache and force a fresh run.
|
|
35
|
+
--watch
|
|
36
|
+
--changed
|
|
37
|
+
--all
|
|
38
|
+
--yes Non-interactive confirmation for unambiguous pulls.
|
|
39
|
+
--dry-run Print the planned writes without changing files.
|
|
40
|
+
--force Overwrite protected local changes.
|
|
41
|
+
--prune Remove manifest-tracked files no longer selected.
|
|
42
|
+
--update-only Update only files already tracked in .rubrkit/manifest.json.
|
|
43
|
+
--config <path> Optional Rubrkit config JSON. API keys are not allowed in config.
|
|
44
|
+
--api-url <url> Defaults to RUBRKIT_API_URL or https://rubrkit.com/api/v1.
|
|
45
|
+
--api-key <key> Defaults to RUBRKIT_API_KEY.
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{
|
|
50
|
+
* argv?: string[],
|
|
51
|
+
* env?: Record<string, string | undefined>,
|
|
52
|
+
* cwd?: string,
|
|
53
|
+
* stdin?: NodeJS.ReadableStream,
|
|
54
|
+
* stdout?: NodeJS.WritableStream,
|
|
55
|
+
* stderr?: NodeJS.WritableStream,
|
|
56
|
+
* fetchImpl?: typeof fetch,
|
|
57
|
+
* }} [params]
|
|
58
|
+
*/
|
|
59
|
+
export async function main({
|
|
60
|
+
argv = process.argv.slice(2),
|
|
61
|
+
env = process.env,
|
|
62
|
+
cwd = process.cwd(),
|
|
63
|
+
stdin = process.stdin,
|
|
64
|
+
stdout = process.stdout,
|
|
65
|
+
stderr = process.stderr,
|
|
66
|
+
fetchImpl = globalThis.fetch,
|
|
67
|
+
} = {}) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = parseArgs(argv);
|
|
70
|
+
|
|
71
|
+
if (parsed.command === 'help') {
|
|
72
|
+
stdout.write(HELP_TEXT);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (parsed.command === 'version') {
|
|
77
|
+
stdout.write('rubrkit 0.0.0-local\n');
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const config = await resolveConfig({ parsed, env, cwd });
|
|
82
|
+
if (parsed.command === 'pull') {
|
|
83
|
+
await runPull({ config, stdin, stdout, stderr, fetchImpl });
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return runTestingCommand({ config, stdout, stderr, fetchImpl });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof RubrkitCliError) {
|
|
90
|
+
stderr.write(`rubrkit: ${error.message}\n`);
|
|
91
|
+
return error.exitCode;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stderr.write(`rubrkit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/config.js
CHANGED
|
@@ -1,169 +1,186 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { usageError } from './errors.js';
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_API_URL = 'https://rubrkit.com/api/v1';
|
|
7
|
-
|
|
8
|
-
const CONFIG_FIELDS = new Set([
|
|
9
|
-
'apiUrl',
|
|
10
|
-
'destination',
|
|
11
|
-
'agent',
|
|
12
|
-
'artifactBundle',
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
parsed
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { usageError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_API_URL = 'https://rubrkit.com/api/v1';
|
|
7
|
+
|
|
8
|
+
const CONFIG_FIELDS = new Set([
|
|
9
|
+
'apiUrl',
|
|
10
|
+
'destination',
|
|
11
|
+
'agent',
|
|
12
|
+
'artifactBundle',
|
|
13
|
+
'auditRunId',
|
|
14
|
+
'artifact',
|
|
15
|
+
'label',
|
|
16
|
+
'rubric',
|
|
17
|
+
'format',
|
|
18
|
+
'output',
|
|
19
|
+
'failUnder',
|
|
20
|
+
'failOn',
|
|
21
|
+
'all',
|
|
22
|
+
'yes',
|
|
23
|
+
'dryRun',
|
|
24
|
+
'force',
|
|
25
|
+
'prune',
|
|
26
|
+
'updateOnly',
|
|
27
|
+
'ci',
|
|
28
|
+
'local',
|
|
29
|
+
'remote',
|
|
30
|
+
'noAi',
|
|
31
|
+
'noCache',
|
|
32
|
+
'watch',
|
|
33
|
+
'changed',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{
|
|
38
|
+
* parsed: ReturnType<import('./args.js').parseArgs>,
|
|
39
|
+
* env?: Record<string, string | undefined>,
|
|
40
|
+
* cwd?: string,
|
|
41
|
+
* fsImpl?: Pick<typeof fs, 'readFile' | 'stat'>,
|
|
42
|
+
* }} params
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveConfig({ parsed, env = process.env, cwd = process.cwd(), fsImpl = fs }) {
|
|
45
|
+
const config = await loadConfig({ configPath: stringOption(parsed.options.config), cwd, fsImpl });
|
|
46
|
+
const options = parsed.options;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
command: parsed.command,
|
|
50
|
+
selector: parsed.selector,
|
|
51
|
+
apiKey: stringOption(options['api-key']) ?? env.RUBRKIT_API_KEY ?? null,
|
|
52
|
+
apiUrl: normalizeApiUrl(stringOption(options['api-url']) ?? env.RUBRKIT_API_URL ?? stringOption(config.apiUrl) ?? DEFAULT_API_URL),
|
|
53
|
+
destination: stringOption(options.destination) ?? stringOption(config.destination) ?? cwd,
|
|
54
|
+
agent: stringOption(options.agent) ?? stringOption(config.agent) ?? 'auto',
|
|
55
|
+
artifactBundle: stringOption(options['artifact-bundle']) ?? stringOption(config.artifactBundle) ?? null,
|
|
56
|
+
auditRunId: stringOption(options['audit-run-id']) ?? stringOption(config.auditRunId) ?? null,
|
|
57
|
+
artifact: stringOption(options.artifact) ?? stringOption(config.artifact) ?? null,
|
|
58
|
+
label: stringArrayOption(options.label).length ? stringArrayOption(options.label) : stringArrayOption(config.label),
|
|
59
|
+
rubric: stringOption(options.rubric) ?? stringOption(config.rubric) ?? null,
|
|
60
|
+
format: stringOption(options.format) ?? stringOption(config.format) ?? 'text',
|
|
61
|
+
output: stringOption(options.output) ?? stringOption(config.output) ?? null,
|
|
62
|
+
failUnder: numberOption(options['fail-under']) ?? numberOption(config.failUnder),
|
|
63
|
+
failOn: stringOption(options['fail-on']) ?? stringOption(config.failOn) ?? null,
|
|
64
|
+
all: booleanOption(options.all) || booleanOption(config.all),
|
|
65
|
+
yes: booleanOption(options.yes) || booleanOption(config.yes),
|
|
66
|
+
dryRun: booleanOption(options['dry-run']) || booleanOption(config.dryRun),
|
|
67
|
+
force: booleanOption(options.force) || booleanOption(config.force),
|
|
68
|
+
prune: booleanOption(options.prune) || booleanOption(config.prune),
|
|
69
|
+
updateOnly: booleanOption(options['update-only']) || booleanOption(config.updateOnly),
|
|
70
|
+
ci: booleanOption(options.ci) || booleanOption(config.ci),
|
|
71
|
+
local: booleanOption(options.local) || booleanOption(config.local),
|
|
72
|
+
remote: booleanOption(options.remote) || booleanOption(config.remote),
|
|
73
|
+
noAi: booleanOption(options['no-ai']) || booleanOption(config.noAi),
|
|
74
|
+
noCache: booleanOption(options['no-cache']) || booleanOption(config.noCache),
|
|
75
|
+
watch: booleanOption(options.watch) || booleanOption(config.watch),
|
|
76
|
+
changed: booleanOption(options.changed) || booleanOption(config.changed),
|
|
77
|
+
configPath: stringOption(options.config) ?? null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {{ configPath?: string | null, cwd: string, fsImpl: Pick<typeof fs, 'readFile' | 'stat'> }} params
|
|
83
|
+
*/
|
|
84
|
+
async function loadConfig({ configPath, cwd, fsImpl }) {
|
|
85
|
+
const candidate = configPath ? path.resolve(cwd, configPath) : path.join(cwd, '.rubrkit', 'config.json');
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (!configPath) {
|
|
89
|
+
await fsImpl.stat(candidate);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let parsed;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(await fsImpl.readFile(candidate, 'utf8'));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw usageError(`Could not read Rubrkit config at ${candidate}: ${error instanceof Error ? error.message : 'invalid JSON'}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
108
|
+
throw usageError('Rubrkit config must be a JSON object.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ('apiKey' in parsed || 'api-key' in parsed || 'RUBRKIT_API_KEY' in parsed) {
|
|
112
|
+
throw usageError('Do not store API keys in Rubrkit config files. Use RUBRKIT_API_KEY or --api-key instead.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const field of Object.keys(parsed)) {
|
|
116
|
+
if (!CONFIG_FIELDS.has(field)) {
|
|
117
|
+
throw usageError(`Unknown Rubrkit config field "${field}".`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {unknown} value
|
|
126
|
+
*/
|
|
127
|
+
function stringOption(value) {
|
|
128
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {unknown} value
|
|
133
|
+
*/
|
|
134
|
+
function stringArrayOption(value) {
|
|
135
|
+
if (!Array.isArray(value)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [...new Set(value.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim()))];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {unknown} value
|
|
144
|
+
*/
|
|
145
|
+
function booleanOption(value) {
|
|
146
|
+
return value === true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {unknown} value
|
|
151
|
+
*/
|
|
152
|
+
function numberOption(value) {
|
|
153
|
+
if (value === undefined || value === null || value === false) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parsed = Number(value);
|
|
158
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} value
|
|
163
|
+
*/
|
|
164
|
+
export function normalizeApiUrl(value) {
|
|
165
|
+
let parsed;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
parsed = new URL(value);
|
|
169
|
+
} catch {
|
|
170
|
+
throw usageError(`Invalid Rubrkit API URL "${value}".`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
174
|
+
throw usageError('Rubrkit API URL must use http or https.');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parsed.pathname === '/' || parsed.pathname === '') {
|
|
178
|
+
parsed.pathname = '/api/v1';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
|
|
182
|
+
parsed.search = '';
|
|
183
|
+
parsed.hash = '';
|
|
184
|
+
|
|
185
|
+
return parsed.toString().replace(/\/$/, '');
|
|
186
|
+
}
|