rubrkit 0.1.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 +126 -0
- package/bin/rubrkit.js +16 -0
- package/package.json +28 -0
- package/src/adapters.js +118 -0
- package/src/api.js +101 -0
- package/src/args.js +175 -0
- package/src/cli.js +93 -0
- package/src/config.js +169 -0
- package/src/errors.js +55 -0
- package/src/formats.js +222 -0
- package/src/index.d.ts +76 -0
- package/src/localChecks.js +680 -0
- package/src/manifest.js +118 -0
- package/src/pathSafety.js +62 -0
- package/src/prompts.js +149 -0
- package/src/pull.js +676 -0
- package/src/sdk.js +443 -0
- package/src/testingCli.js +431 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Rubrkit CLI
|
|
2
|
+
|
|
3
|
+
Local v1 package for pulling Rubrkit artifact-bundle files into agent projects and testing artifacts from local projects or CI.
|
|
4
|
+
|
|
5
|
+
The package folder is `tools/rubrkit-cli`, but the intended published package name and executable are `rubrkit` so users can run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx rubrkit pull [all|<artifact-bundle-or-artifact-selector>]
|
|
9
|
+
npx rubrkit validate <path|glob>
|
|
10
|
+
npx rubrkit test <path|glob|artifact-bundle-or-artifact-selector>
|
|
11
|
+
npx rubrkit audit <artifact-bundle-or-selector>
|
|
12
|
+
npx rubrkit eval <artifact-bundle-or-selector>
|
|
13
|
+
npx rubrkit report <job-or-run-id>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Auth
|
|
17
|
+
|
|
18
|
+
Use a Rubrkit API key for pull commands and remote checks. Prefer a pull key scoped to `artifacts:pull`. For `rubrkit test <path> --remote`, use a testing key scoped to the existing composed API flow: `artifact_bundles:write`, `files:write`, `audits:run`, `jobs:read`, and `credits:read`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
RUBRKIT_API_KEY=... rubrkit pull all --yes
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--api-key` is also supported for secret-manager wrappers. API keys are never written to `.rubrkit/manifest.json`, local reports, fixtures, or snapshots.
|
|
25
|
+
|
|
26
|
+
## Pull Examples
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
rubrkit pull
|
|
30
|
+
rubrkit pull all --yes
|
|
31
|
+
rubrkit pull team-agent-kit --destination .
|
|
32
|
+
rubrkit pull --artifact-bundle team-agent-kit --artifact AGENTS.md --yes
|
|
33
|
+
rubrkit pull all --yes --dry-run
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Useful flags:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
--destination <path>
|
|
40
|
+
--agent <auto|codex|claude|generic>
|
|
41
|
+
--artifact-bundle <id-or-name>
|
|
42
|
+
--artifact <id-or-path>
|
|
43
|
+
--rubric <rubric-id-or-path>
|
|
44
|
+
--format <text|json|junit>
|
|
45
|
+
--output <path>
|
|
46
|
+
--fail-under <score>
|
|
47
|
+
--fail-on <critical|high|medium|low>
|
|
48
|
+
--ci
|
|
49
|
+
--local
|
|
50
|
+
--remote
|
|
51
|
+
--no-ai
|
|
52
|
+
--watch
|
|
53
|
+
--changed
|
|
54
|
+
--all
|
|
55
|
+
--yes
|
|
56
|
+
--dry-run
|
|
57
|
+
--force
|
|
58
|
+
--prune
|
|
59
|
+
--update-only
|
|
60
|
+
--config <path>
|
|
61
|
+
--api-url <url>
|
|
62
|
+
--api-key <key>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Testing Examples
|
|
66
|
+
|
|
67
|
+
Local validation runs without network access or credits where possible:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
rubrkit validate docs/example.rubr_flow
|
|
71
|
+
rubrkit test "prompts/**/*.md" --local --format json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Remote checks use the public `/api/v1` API, create async jobs, and poll `/jobs/{jobId}`. Local-file remote tests create or select an artifact bundle, upload the local text files, then start an audit job:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
RUBRKIT_API_KEY=... rubrkit test team-agent-kit --remote --format json
|
|
78
|
+
RUBRKIT_API_KEY=... rubrkit audit team-agent-kit --fail-under 85 --ci
|
|
79
|
+
RUBRKIT_API_KEY=... rubrkit eval team-agent-kit --format junit --output rubrkit-junit.xml
|
|
80
|
+
RUBRKIT_API_KEY=... rubrkit report <job-id>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `--dry-run --remote` to inspect the remote request shape without calling the API.
|
|
84
|
+
|
|
85
|
+
Exit codes:
|
|
86
|
+
|
|
87
|
+
- `0`: checks passed.
|
|
88
|
+
- `1`: checks completed and failed validation or quality gates.
|
|
89
|
+
- `2`: invalid CLI usage or invalid config.
|
|
90
|
+
- `3`: authentication or authorization failure.
|
|
91
|
+
- `4`: credit, usage-limit, or circuit-breaker block.
|
|
92
|
+
- `5`: network, provider, or server failure.
|
|
93
|
+
|
|
94
|
+
## SDK
|
|
95
|
+
|
|
96
|
+
The current local package exports the SDK from `rubrkit` while final package naming awaits owner approval.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { Rubrkit } from 'rubrkit';
|
|
100
|
+
|
|
101
|
+
const client = new Rubrkit({ apiKey: process.env.RUBRKIT_API_KEY });
|
|
102
|
+
const started = await client.artifacts.test({ artifactBundleId: 'team-agent-kit' });
|
|
103
|
+
const job = await client.jobs.wait(started.jobId);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Placement
|
|
107
|
+
|
|
108
|
+
- Codex primary artifacts are placed as `AGENTS.md`; supporting files go under `.rubrkit/artifacts/<bundle>/`.
|
|
109
|
+
- Claude primary artifacts are placed as `CLAUDE.md`, or `.claude/CLAUDE.md` when that file already exists; supporting files go under `.rubrkit/artifacts/<bundle>/`.
|
|
110
|
+
- Generic placement preserves artifact paths under `.rubrkit/artifacts/<bundle>/`.
|
|
111
|
+
|
|
112
|
+
The CLI protects existing local files unless they were previously managed by Rubrkit and match the local manifest. Use `--force` to overwrite after reviewing the plan.
|
|
113
|
+
|
|
114
|
+
## Postinstall
|
|
115
|
+
|
|
116
|
+
Rubrkit does not run network sync from its own package lifecycle. Consuming projects can opt in:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"scripts": {
|
|
121
|
+
"postinstall": "rubrkit pull all --yes"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Postinstall commands should use `RUBRKIT_API_KEY` and an unambiguous selector.
|
package/bin/rubrkit.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { main } from '../src/cli.js';
|
|
4
|
+
|
|
5
|
+
const exitCode = await main({
|
|
6
|
+
argv: process.argv.slice(2),
|
|
7
|
+
env: process.env,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
stdin: process.stdin,
|
|
10
|
+
stdout: process.stdout,
|
|
11
|
+
stderr: process.stderr,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (exitCode !== 0) {
|
|
15
|
+
process.exitCode = exitCode;
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rubrkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rubrkit CLI for pulling artifact bundles into local agent projects.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rubrkit": "./bin/rubrkit.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/sdk.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"types": "./src/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"package.json"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/adapters.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { normalizeArtifactPath, slugifyPathSegment } from './pathSafety.js';
|
|
5
|
+
|
|
6
|
+
const CODEX_SIGNALS = ['AGENTS.override.md', 'AGENTS.md', '.codex/config.toml'];
|
|
7
|
+
const CLAUDE_SIGNALS = ['CLAUDE.md', '.claude/CLAUDE.md', '.claude/settings.json', '.claude/settings.local.json'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ root: string, requestedAgent?: string, fsImpl?: Pick<typeof fs, 'existsSync'> }} params
|
|
11
|
+
*/
|
|
12
|
+
export function resolveAgentAdapter({ root, requestedAgent = 'auto', fsImpl = fs }) {
|
|
13
|
+
const agent = requestedAgent === 'auto' ? detectAgent(root, fsImpl) : requestedAgent;
|
|
14
|
+
|
|
15
|
+
if (agent === 'codex') return codexAdapter;
|
|
16
|
+
if (agent === 'claude') return claudeAdapter;
|
|
17
|
+
|
|
18
|
+
return genericAdapter;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} root
|
|
23
|
+
* @param {Pick<typeof fs, 'existsSync'>} fsImpl
|
|
24
|
+
*/
|
|
25
|
+
export function detectAgent(root, fsImpl = fs) {
|
|
26
|
+
if (CODEX_SIGNALS.some((signal) => fsImpl.existsSync(path.join(root, signal)))) {
|
|
27
|
+
return 'codex';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (CLAUDE_SIGNALS.some((signal) => fsImpl.existsSync(path.join(root, signal)))) {
|
|
31
|
+
return 'claude';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'generic';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const genericAdapter = {
|
|
38
|
+
name: 'generic',
|
|
39
|
+
detectionSignals: [],
|
|
40
|
+
supportedArtifactTypes: ['agent', 'command', 'markdown', 'prompt', 'rubr_flow', 'skill', 'text', 'workflow'],
|
|
41
|
+
/**
|
|
42
|
+
* @param {PlacementInput} input
|
|
43
|
+
*/
|
|
44
|
+
place(input) {
|
|
45
|
+
return placeSupportingArtifact(input);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const codexAdapter = {
|
|
50
|
+
name: 'codex',
|
|
51
|
+
detectionSignals: CODEX_SIGNALS,
|
|
52
|
+
supportedArtifactTypes: ['agent', 'command', 'markdown', 'prompt', 'rubr_flow', 'skill', 'text', 'workflow'],
|
|
53
|
+
/**
|
|
54
|
+
* @param {PlacementInput} input
|
|
55
|
+
*/
|
|
56
|
+
place(input) {
|
|
57
|
+
const basename = path.posix.basename(normalizeArtifactPath(input.file.path));
|
|
58
|
+
|
|
59
|
+
if (basename === 'AGENTS.md' || basename === 'AGENTS.override.md') {
|
|
60
|
+
return { destinationPath: basename, reason: 'Codex project instruction file' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input.file.isPrimary) {
|
|
64
|
+
return { destinationPath: 'AGENTS.md', reason: 'Codex primary artifact placement' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return placeSupportingArtifact(input);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const claudeAdapter = {
|
|
72
|
+
name: 'claude',
|
|
73
|
+
detectionSignals: CLAUDE_SIGNALS,
|
|
74
|
+
supportedArtifactTypes: ['agent', 'command', 'markdown', 'prompt', 'rubr_flow', 'skill', 'text', 'workflow'],
|
|
75
|
+
/**
|
|
76
|
+
* @param {PlacementInput} input
|
|
77
|
+
*/
|
|
78
|
+
place(input) {
|
|
79
|
+
const normalizedPath = normalizeArtifactPath(input.file.path);
|
|
80
|
+
const basename = path.posix.basename(normalizedPath);
|
|
81
|
+
|
|
82
|
+
if (basename === 'CLAUDE.md' || basename === 'CLAUDE.local.md') {
|
|
83
|
+
return { destinationPath: basename, reason: 'Claude Code project instruction file' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input.file.isPrimary) {
|
|
87
|
+
const existingDotClaude = input.exists('.claude/CLAUDE.md');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
destinationPath: existingDotClaude ? '.claude/CLAUDE.md' : 'CLAUDE.md',
|
|
91
|
+
reason: 'Claude Code primary artifact placement',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return placeSupportingArtifact(input);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {PlacementInput} input
|
|
101
|
+
*/
|
|
102
|
+
function placeSupportingArtifact(input) {
|
|
103
|
+
const bundleSegment = slugifyPathSegment(input.artifactBundle.name ?? input.artifactBundle.id);
|
|
104
|
+
const artifactPath = normalizeArtifactPath(input.file.path);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
destinationPath: `.rubrkit/artifacts/${bundleSegment}/${artifactPath}`,
|
|
108
|
+
reason: 'Generic supporting artifact placement',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {{
|
|
114
|
+
* artifactBundle: Record<string, any>,
|
|
115
|
+
* file: Record<string, any>,
|
|
116
|
+
* exists(relativePath: string): boolean,
|
|
117
|
+
* }} PlacementInput
|
|
118
|
+
*/
|
package/src/api.js
ADDED
|
@@ -0,0 +1,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
|
+
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
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,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 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
|
+
}
|