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 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
+ }
@@ -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
+ }