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/api.js CHANGED
@@ -1,101 +1,108 @@
1
- import { exitCodeForApiFailure, RubrkitCliError } from './errors.js';
2
-
3
- export class RubrkitApiClient {
4
- /**
5
- * @param {{ apiUrl: string, apiKey: string, fetchImpl?: typeof fetch }} options
6
- */
7
- constructor({ apiUrl, apiKey, fetchImpl = globalThis.fetch }) {
8
- this.apiUrl = apiUrl.replace(/\/+$/, '');
9
- this.apiKey = apiKey;
10
- this.fetchImpl = fetchImpl;
11
- }
12
-
13
- async listArtifactBundles() {
14
- const data = await this.request('/artifact-bundles?status=active&limit=100');
15
- return Array.isArray(data.artifactBundles) ? data.artifactBundles : [];
16
- }
17
-
18
- /**
19
- * @param {string} artifactBundleId
20
- */
21
- async getArtifactBundle(artifactBundleId) {
22
- const data = await this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}`);
23
- return data;
24
- }
25
-
26
- /**
27
- * @param {string} artifactBundleId
28
- */
29
- async listFiles(artifactBundleId) {
30
- const data = await this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}/files?limit=200`);
31
- return Array.isArray(data.files) ? data.files : [];
32
- }
33
-
34
- /**
35
- * @param {string} artifactBundleId
36
- * @param {string} fileId
37
- */
38
- async getFile(artifactBundleId, fileId) {
39
- return this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}/files/${encodeURIComponent(fileId)}`);
40
- }
41
-
42
- /**
43
- * @param {string} path
44
- */
45
- /**
46
- * @param {string} path
47
- * @param {{ method?: string, body?: unknown }} [options]
48
- */
49
- async request(path, { method = 'GET', body: requestBody = undefined } = {}) {
50
- /** @type {Record<string, string>} */
51
- const headers = {
52
- accept: 'application/json',
53
- authorization: `Bearer ${this.apiKey}`,
54
- 'user-agent': 'rubrkit-cli/0.0.0-local',
55
- };
56
-
57
- if (requestBody !== undefined) {
58
- headers['content-type'] = 'application/json';
59
- }
60
-
61
- const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
62
- method,
63
- headers,
64
- body: requestBody === undefined ? undefined : JSON.stringify(requestBody),
65
- });
66
-
67
- const text = await response.text();
68
- const body = text ? parseJson(text) : {};
69
-
70
- if (!response.ok) {
71
- const error = body && typeof body === 'object' && 'error' in body ? body.error : {};
72
- const message =
73
- error && typeof error === 'object' && typeof error.message === 'string'
74
- ? error.message
75
- : `Rubrkit API request failed with HTTP ${response.status}.`;
76
- const code = error && typeof error === 'object' && typeof error.code === 'string' ? error.code : 'api_request_failed';
77
-
78
- throw new RubrkitCliError(message, {
79
- code,
80
- exitCode: exitCodeForApiFailure(code, response.status),
81
- });
82
- }
83
-
84
- return body && typeof body === 'object' && 'data' in body ? body.data : body;
85
- }
86
- }
87
-
88
- /**
89
- * @param {string} text
90
- */
91
- function parseJson(text) {
92
- try {
93
- return JSON.parse(text);
94
- } catch (error) {
95
- throw new RubrkitCliError('Rubrkit API returned invalid JSON.', {
96
- code: 'invalid_api_response',
97
- exitCode: 1,
98
- details: error,
99
- });
100
- }
101
- }
1
+ import { exitCodeForApiFailure, RubrkitCliError } from './errors.js';
2
+
3
+ export class RubrkitApiClient {
4
+ /**
5
+ * @param {{ apiUrl: string, apiKey: string, fetchImpl?: typeof fetch }} options
6
+ */
7
+ constructor({ apiUrl, apiKey, fetchImpl = globalThis.fetch }) {
8
+ this.apiUrl = apiUrl.replace(/\/+$/, '');
9
+ this.apiKey = apiKey;
10
+ this.fetchImpl = fetchImpl;
11
+ }
12
+
13
+ /**
14
+ * @param {{ labels?: string[] }} [options]
15
+ */
16
+ async listArtifactBundles({ labels = [] } = {}) {
17
+ const params = new URLSearchParams({ status: 'active', limit: '100' });
18
+ for (const label of labels) {
19
+ params.append('label', label);
20
+ }
21
+ const data = await this.request(`/artifact-bundles?${params.toString()}`);
22
+ return Array.isArray(data.artifactBundles) ? data.artifactBundles : [];
23
+ }
24
+
25
+ /**
26
+ * @param {string} artifactBundleId
27
+ */
28
+ async getArtifactBundle(artifactBundleId) {
29
+ const data = await this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}`);
30
+ return data;
31
+ }
32
+
33
+ /**
34
+ * @param {string} artifactBundleId
35
+ */
36
+ async listFiles(artifactBundleId) {
37
+ const data = await this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}/files?limit=200`);
38
+ return Array.isArray(data.files) ? data.files : [];
39
+ }
40
+
41
+ /**
42
+ * @param {string} artifactBundleId
43
+ * @param {string} fileId
44
+ */
45
+ async getFile(artifactBundleId, fileId) {
46
+ return this.request(`/artifact-bundles/${encodeURIComponent(artifactBundleId)}/files/${encodeURIComponent(fileId)}`);
47
+ }
48
+
49
+ /**
50
+ * @param {string} path
51
+ */
52
+ /**
53
+ * @param {string} path
54
+ * @param {{ method?: string, body?: unknown }} [options]
55
+ */
56
+ async request(path, { method = 'GET', body: requestBody = undefined } = {}) {
57
+ /** @type {Record<string, string>} */
58
+ const headers = {
59
+ accept: 'application/json',
60
+ authorization: `Bearer ${this.apiKey}`,
61
+ 'user-agent': 'rubrkit-cli/0.0.0-local',
62
+ };
63
+
64
+ if (requestBody !== undefined) {
65
+ headers['content-type'] = 'application/json';
66
+ }
67
+
68
+ const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
69
+ method,
70
+ headers,
71
+ body: requestBody === undefined ? undefined : JSON.stringify(requestBody),
72
+ });
73
+
74
+ const text = await response.text();
75
+ const body = text ? parseJson(text) : {};
76
+
77
+ if (!response.ok) {
78
+ const error = body && typeof body === 'object' && 'error' in body ? body.error : {};
79
+ const message =
80
+ error && typeof error === 'object' && typeof error.message === 'string'
81
+ ? error.message
82
+ : `Rubrkit API request failed with HTTP ${response.status}.`;
83
+ const code = error && typeof error === 'object' && typeof error.code === 'string' ? error.code : 'api_request_failed';
84
+
85
+ throw new RubrkitCliError(message, {
86
+ code,
87
+ exitCode: exitCodeForApiFailure(code, response.status),
88
+ });
89
+ }
90
+
91
+ return body && typeof body === 'object' && 'data' in body ? body.data : body;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @param {string} text
97
+ */
98
+ function parseJson(text) {
99
+ try {
100
+ return JSON.parse(text);
101
+ } catch (error) {
102
+ throw new RubrkitCliError('Rubrkit API returned invalid JSON.', {
103
+ code: 'invalid_api_response',
104
+ exitCode: 1,
105
+ details: error,
106
+ });
107
+ }
108
+ }
package/src/args.js CHANGED
@@ -1,175 +1,209 @@
1
- import { usageError } from './errors.js';
2
-
3
- const VALUE_FLAGS = new Set([
4
- 'destination',
5
- 'agent',
6
- 'artifact-bundle',
7
- 'artifact',
8
- 'rubric',
9
- 'format',
10
- 'output',
11
- 'fail-under',
12
- 'fail-on',
13
- 'config',
14
- 'api-url',
15
- 'api-key',
16
- ]);
17
-
18
- const BOOLEAN_FLAGS = new Set([
19
- 'all',
20
- 'yes',
21
- 'dry-run',
22
- 'force',
23
- 'prune',
24
- 'update-only',
25
- 'ci',
26
- 'local',
27
- 'remote',
28
- 'no-ai',
29
- 'watch',
30
- 'changed',
31
- 'help',
32
- 'version',
33
- ]);
34
-
35
- const AGENTS = new Set(['auto', 'codex', 'claude', 'generic']);
36
- const COMMANDS = new Set(['pull', 'validate', 'test', 'audit', 'eval', 'report']);
37
- const FORMATS = new Set(['text', 'json', 'junit']);
38
- const FAIL_ON_LEVELS = new Set(['critical', 'high', 'medium', 'low']);
39
-
40
- /**
41
- * @param {string[]} argv
42
- */
43
- export function parseArgs(argv) {
44
- const [command, ...rest] = argv;
45
-
46
- if (!command || command === '--help' || command === '-h') {
47
- return { command: command ? 'help' : 'help', selector: null, options: {} };
48
- }
49
-
50
- if (command === '--version' || command === '-v') {
51
- return { command: 'version', selector: null, options: {} };
52
- }
53
-
54
- if (!COMMANDS.has(command)) {
55
- throw usageError(`Unknown command "${command}". Run rubrkit --help for usage.`);
56
- }
57
-
58
- /** @type {Record<string, string | boolean>} */
59
- const options = {};
60
- /** @type {string[]} */
61
- const positionals = [];
62
-
63
- for (let index = 0; index < rest.length; index += 1) {
64
- const arg = rest[index];
65
-
66
- if (arg === '--') {
67
- positionals.push(...rest.slice(index + 1));
68
- break;
69
- }
70
-
71
- if (!arg.startsWith('-') || arg === '-') {
72
- positionals.push(arg);
73
- continue;
74
- }
75
-
76
- const normalized = normalizeFlag(arg);
77
-
78
- if (normalized.alias) {
79
- options[normalized.name] = true;
80
- continue;
81
- }
82
-
83
- if (BOOLEAN_FLAGS.has(normalized.name)) {
84
- if (normalized.value !== null) {
85
- throw usageError(`Flag --${normalized.name} does not accept a value.`);
86
- }
87
-
88
- options[normalized.name] = true;
89
- continue;
90
- }
91
-
92
- if (VALUE_FLAGS.has(normalized.name)) {
93
- const value = normalized.value ?? rest[index + 1];
94
-
95
- if (!value || value.startsWith('--')) {
96
- throw usageError(`Flag --${normalized.name} requires a value.`);
97
- }
98
-
99
- options[normalized.name] = value;
100
-
101
- if (normalized.value === null) {
102
- index += 1;
103
- }
104
-
105
- continue;
106
- }
107
-
108
- throw usageError(`Unknown flag --${normalized.name}.`);
109
- }
110
-
111
- if (positionals.length > 1) {
112
- throw usageError(`The ${command} command accepts at most one target or selector.`);
113
- }
114
-
115
- const selector = positionals[0] ?? null;
116
-
117
- if (selector === 'all') {
118
- options.all = true;
119
- }
120
-
121
- if (typeof options.agent === 'string' && !AGENTS.has(options.agent)) {
122
- throw usageError('--agent must be one of auto, codex, claude, or generic.');
123
- }
124
-
125
- if (typeof options.format === 'string' && !FORMATS.has(options.format)) {
126
- throw usageError('--format must be one of text, json, or junit.');
127
- }
128
-
129
- if (typeof options['fail-on'] === 'string' && !FAIL_ON_LEVELS.has(options['fail-on'])) {
130
- throw usageError('--fail-on must be one of critical, high, medium, or low.');
131
- }
132
-
133
- if (options['fail-under'] !== undefined) {
134
- const value = Number(options['fail-under']);
135
- if (!Number.isFinite(value) || value < 0 || value > 100) {
136
- throw usageError('--fail-under must be a number from 0 to 100.');
137
- }
138
- }
139
-
140
- if (options.local && options.remote) {
141
- throw usageError('Use either --local or --remote, not both.');
142
- }
143
-
144
- return { command, selector, options };
145
- }
146
-
147
- /**
148
- * @param {string} arg
149
- */
150
- function normalizeFlag(arg) {
151
- if (arg === '-y') {
152
- return { name: 'yes', value: null, alias: true };
153
- }
154
-
155
- if (arg === '-h') {
156
- return { name: 'help', value: null, alias: true };
157
- }
158
-
159
- if (!arg.startsWith('--')) {
160
- throw usageError(`Unknown short flag "${arg}".`);
161
- }
162
-
163
- const raw = arg.slice(2);
164
- const equalsIndex = raw.indexOf('=');
165
-
166
- if (equalsIndex === -1) {
167
- return { name: raw, value: null, alias: false };
168
- }
169
-
170
- return {
171
- name: raw.slice(0, equalsIndex),
172
- value: raw.slice(equalsIndex + 1),
173
- alias: false,
174
- };
175
- }
1
+ import { usageError } from './errors.js';
2
+
3
+ const VALUE_FLAGS = new Set([
4
+ 'destination',
5
+ 'agent',
6
+ 'artifact-bundle',
7
+ 'audit-run-id',
8
+ 'artifact',
9
+ 'rubric',
10
+ 'format',
11
+ 'output',
12
+ 'fail-under',
13
+ 'fail-on',
14
+ 'config',
15
+ 'api-url',
16
+ 'api-key',
17
+ ]);
18
+
19
+ const BOOLEAN_FLAGS = new Set([
20
+ 'all',
21
+ 'yes',
22
+ 'dry-run',
23
+ 'force',
24
+ 'prune',
25
+ 'update-only',
26
+ 'ci',
27
+ 'local',
28
+ 'remote',
29
+ 'no-ai',
30
+ 'no-cache',
31
+ 'watch',
32
+ 'changed',
33
+ 'help',
34
+ 'version',
35
+ ]);
36
+
37
+ const MULTI_VALUE_FLAGS = new Set(['label']);
38
+
39
+ const AGENTS = new Set(['auto', 'codex', 'claude', 'generic']);
40
+ const COMMANDS = new Set(['pull', 'validate', 'test', 'audit', 'eval', 'audit-apply', 'report']);
41
+ const FORMATS = new Set(['text', 'json', 'junit']);
42
+ const FAIL_ON_LEVELS = new Set(['critical', 'high', 'medium', 'low']);
43
+
44
+ /**
45
+ * @param {string[]} argv
46
+ */
47
+ export function parseArgs(argv) {
48
+ const [command, ...rest] = argv;
49
+
50
+ if (!command || command === '--help' || command === '-h') {
51
+ return { command: command ? 'help' : 'help', selector: null, options: {} };
52
+ }
53
+
54
+ if (command === '--version' || command === '-v') {
55
+ return { command: 'version', selector: null, options: {} };
56
+ }
57
+
58
+ if (!COMMANDS.has(command)) {
59
+ throw usageError(`Unknown command "${command}". Run rubrkit --help for usage.`);
60
+ }
61
+
62
+ /** @type {Record<string, string | boolean | string[]>} */
63
+ const options = {};
64
+ /** @type {string[]} */
65
+ const positionals = [];
66
+
67
+ for (let index = 0; index < rest.length; index += 1) {
68
+ const arg = rest[index];
69
+
70
+ if (arg === '--') {
71
+ positionals.push(...rest.slice(index + 1));
72
+ break;
73
+ }
74
+
75
+ if (!arg.startsWith('-') || arg === '-') {
76
+ positionals.push(arg);
77
+ continue;
78
+ }
79
+
80
+ const normalized = normalizeFlag(arg);
81
+
82
+ if (normalized.alias) {
83
+ options[normalized.name] = true;
84
+ continue;
85
+ }
86
+
87
+ if (BOOLEAN_FLAGS.has(normalized.name)) {
88
+ if (normalized.value !== null) {
89
+ throw usageError(`Flag --${normalized.name} does not accept a value.`);
90
+ }
91
+
92
+ options[normalized.name] = true;
93
+ continue;
94
+ }
95
+
96
+ if (MULTI_VALUE_FLAGS.has(normalized.name)) {
97
+ const value = normalized.value ?? rest[index + 1];
98
+
99
+ if (!value || value.startsWith('--')) {
100
+ throw usageError(`Flag --${normalized.name} requires a value.`);
101
+ }
102
+
103
+ const parts = value
104
+ .split(',')
105
+ .map((part) => part.trim())
106
+ .filter(Boolean);
107
+ const existing = options[normalized.name];
108
+ const current = Array.isArray(existing) ? existing : [];
109
+ options[normalized.name] = [...current, ...parts];
110
+
111
+ if (normalized.value === null) {
112
+ index += 1;
113
+ }
114
+
115
+ continue;
116
+ }
117
+
118
+ if (VALUE_FLAGS.has(normalized.name)) {
119
+ const value = normalized.value ?? rest[index + 1];
120
+
121
+ if (!value || value.startsWith('--')) {
122
+ throw usageError(`Flag --${normalized.name} requires a value.`);
123
+ }
124
+
125
+ options[normalized.name] = value;
126
+
127
+ if (normalized.value === null) {
128
+ index += 1;
129
+ }
130
+
131
+ continue;
132
+ }
133
+
134
+ throw usageError(`Unknown flag --${normalized.name}.`);
135
+ }
136
+
137
+ if (positionals.length > 1) {
138
+ throw usageError(`The ${command} command accepts at most one target or selector.`);
139
+ }
140
+
141
+ const selector = positionals[0] ?? null;
142
+
143
+ if (selector === 'all') {
144
+ options.all = true;
145
+ }
146
+
147
+ if (Array.isArray(options.label)) {
148
+ options.label = [...new Set(options.label)];
149
+
150
+ if (options.label.length > 0 && (selector || options['artifact-bundle'] || options.artifact)) {
151
+ throw usageError('--label cannot be combined with a positional selector, --artifact-bundle, or --artifact.');
152
+ }
153
+ }
154
+
155
+ if (typeof options.agent === 'string' && !AGENTS.has(options.agent)) {
156
+ throw usageError('--agent must be one of auto, codex, claude, or generic.');
157
+ }
158
+
159
+ if (typeof options.format === 'string' && !FORMATS.has(options.format)) {
160
+ throw usageError('--format must be one of text, json, or junit.');
161
+ }
162
+
163
+ if (typeof options['fail-on'] === 'string' && !FAIL_ON_LEVELS.has(options['fail-on'])) {
164
+ throw usageError('--fail-on must be one of critical, high, medium, or low.');
165
+ }
166
+
167
+ if (options['fail-under'] !== undefined) {
168
+ const value = Number(options['fail-under']);
169
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
170
+ throw usageError('--fail-under must be a number from 0 to 100.');
171
+ }
172
+ }
173
+
174
+ if (options.local && options.remote) {
175
+ throw usageError('Use either --local or --remote, not both.');
176
+ }
177
+
178
+ return { command, selector, options };
179
+ }
180
+
181
+ /**
182
+ * @param {string} arg
183
+ */
184
+ function normalizeFlag(arg) {
185
+ if (arg === '-y') {
186
+ return { name: 'yes', value: null, alias: true };
187
+ }
188
+
189
+ if (arg === '-h') {
190
+ return { name: 'help', value: null, alias: true };
191
+ }
192
+
193
+ if (!arg.startsWith('--')) {
194
+ throw usageError(`Unknown short flag "${arg}".`);
195
+ }
196
+
197
+ const raw = arg.slice(2);
198
+ const equalsIndex = raw.indexOf('=');
199
+
200
+ if (equalsIndex === -1) {
201
+ return { name: raw, value: null, alias: false };
202
+ }
203
+
204
+ return {
205
+ name: raw.slice(0, equalsIndex),
206
+ value: raw.slice(equalsIndex + 1),
207
+ alias: false,
208
+ };
209
+ }