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