@agilno-tech/rivet 0.1.0-alpha.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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as filesystem from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
|
|
6
|
+
import { loadProjectConfig } from '../config/load.js';
|
|
7
|
+
import { CliError, EXIT_CODES } from '../cli/output.js';
|
|
8
|
+
|
|
9
|
+
import { MAX_PROTOCOL_BYTES, PROTOCOL_DIRECTORY, MUTATION_LOCK, DIGEST, safeText, safeId, sameIdentity, lstatIfExists, readBounded, protocolRoot, canonicalBody, bodyDigest, validateDocument, protocolPath, discover, publicProtocol } from '../protocols/project.js';
|
|
10
|
+
import { protocolCompleteness, requireProtocolCompleteness } from '../protocols/project.js';
|
|
11
|
+
const MAX_QUERY_LENGTH = 256;
|
|
12
|
+
const SUBCOMMANDS = new Set(['add', 'import', 'validate', 'find', 'show', 'update', 'retire']);
|
|
13
|
+
function fail(message, code = 'INVALID_INPUT') { throw new CliError(message, code); }
|
|
14
|
+
|
|
15
|
+
function draftGuidance(id, revision) {
|
|
16
|
+
return `Write the missing sections in a project-contained Markdown source file without inventing team policy, then run rivet protocols update ${id} --from=<file> --expected-revision=${revision}. Add --publish only after review and complete validation.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function now() {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function render(metadata, body) {
|
|
24
|
+
const frontmatter = YAML.stringify(metadata, { lineWidth: 0 }).trimEnd();
|
|
25
|
+
return `---\n${frontmatter}\n---\n\n${canonicalBody(body)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function atomicWrite(path, source, fs, expectedParent, expectedDigest = null) {
|
|
29
|
+
const parent = resolve(path, '..');
|
|
30
|
+
const temporary = join(parent, `.rivet-protocol-${randomUUID()}.tmp`);
|
|
31
|
+
const lockPath = join(parent, MUTATION_LOCK);
|
|
32
|
+
let descriptor;
|
|
33
|
+
let lock;
|
|
34
|
+
try {
|
|
35
|
+
lock = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
36
|
+
const currentParent = lstatIfExists(parent, fs);
|
|
37
|
+
if (!currentParent || currentParent.isSymbolicLink() || !currentParent.isDirectory() || !sameIdentity(expectedParent, currentParent)) {
|
|
38
|
+
fail('Protocol directory changed during validation.', 'REPOSITORY_CONFLICT');
|
|
39
|
+
}
|
|
40
|
+
const current = lstatIfExists(path, fs);
|
|
41
|
+
if (expectedDigest === null) {
|
|
42
|
+
if (current) fail('Protocol already exists.', 'REPOSITORY_CONFLICT');
|
|
43
|
+
} else {
|
|
44
|
+
const id = path.slice(parent.length + 1, -3);
|
|
45
|
+
const observed = validateDocument(readBounded(path, fs, `Protocol '${id}'`), id);
|
|
46
|
+
if (observed.metadata.digest !== expectedDigest) fail('Protocol revision conflict.', 'REPOSITORY_CONFLICT');
|
|
47
|
+
}
|
|
48
|
+
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
49
|
+
fs.writeFileSync(descriptor, source, { encoding: 'utf8' });
|
|
50
|
+
fs.fsyncSync(descriptor);
|
|
51
|
+
fs.closeSync(descriptor);
|
|
52
|
+
descriptor = undefined;
|
|
53
|
+
const finalParent = lstatIfExists(parent, fs);
|
|
54
|
+
if (!finalParent || finalParent.isSymbolicLink() || !finalParent.isDirectory() || !sameIdentity(expectedParent, finalParent)) {
|
|
55
|
+
fail('Protocol directory changed during validation.', 'REPOSITORY_CONFLICT');
|
|
56
|
+
}
|
|
57
|
+
fs.renameSync(temporary, path);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
60
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
61
|
+
if (error instanceof CliError) throw error;
|
|
62
|
+
fail('Protocol write could not be completed safely.', 'REPOSITORY_CONFLICT');
|
|
63
|
+
} finally {
|
|
64
|
+
if (lock !== undefined) {
|
|
65
|
+
try { fs.closeSync(lock); } catch {}
|
|
66
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sourcePath(root, input, fs) {
|
|
72
|
+
if (typeof input !== 'string' || input.length < 1 || input.length > 512 || input.includes('\0')) fail("Option '--from' is invalid.");
|
|
73
|
+
const normalized = input.replaceAll('\\', '/');
|
|
74
|
+
const absolute = resolve(root.root, normalized);
|
|
75
|
+
const within = relative(root.root, absolute);
|
|
76
|
+
if (isAbsolute(normalized) || !within || within === '..' || within.startsWith(`..${sep}`)) fail("Option '--from' must stay within the project.");
|
|
77
|
+
let current = root.root;
|
|
78
|
+
for (const part of within.split(sep).slice(0, -1)) {
|
|
79
|
+
current = join(current, part);
|
|
80
|
+
const status = lstatIfExists(current, fs);
|
|
81
|
+
if (!status || status.isSymbolicLink() || !status.isDirectory()) fail('Protocol import source has an unsafe ancestor.', 'REPOSITORY_CONFLICT');
|
|
82
|
+
}
|
|
83
|
+
return absolute;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function requireFlags(parsed) {
|
|
87
|
+
if (!parsed || parsed.command !== 'protocols' || !SUBCOMMANDS.has(parsed.subcommand)
|
|
88
|
+
|| !Array.isArray(parsed.operands) || !parsed.flags || typeof parsed.flags !== 'object') fail('Invalid protocols command.');
|
|
89
|
+
const allowed = new Set(['project', 'json', 'from', 'include-drafts', 'include-retired', 'publish', 'expected-revision', 'expected-digest']);
|
|
90
|
+
if (Object.keys(parsed.flags).some(key => !allowed.has(key))) fail('Invalid protocols command options.');
|
|
91
|
+
if (parsed.flags.project !== undefined && (typeof parsed.flags.project !== 'string' || parsed.flags.project.length === 0)) fail("Option '--project' is invalid.");
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emit(dependencies, parsed, result, code = EXIT_CODES.SUCCESS) {
|
|
96
|
+
const json = parsed?.flags?.json === true;
|
|
97
|
+
if (json && code !== EXIT_CODES.SUCCESS) {
|
|
98
|
+
dependencies.output.json({ ok: false, error: result.error ?? { code: 'INTERNAL_ERROR', exitCode: code, message: 'Protocol operation failed.' } }, 'stderr');
|
|
99
|
+
} else if (json) {
|
|
100
|
+
dependencies.output.json({ ok: true, command: 'protocols', result }, 'stdout');
|
|
101
|
+
}
|
|
102
|
+
else if (code === EXIT_CODES.SUCCESS) dependencies.output.log(JSON.stringify(result, null, 2));
|
|
103
|
+
else dependencies.output.error(`ERROR: ${result.error?.message ?? result.message ?? 'Protocol operation failed.'}`);
|
|
104
|
+
return code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function errorResult(error) {
|
|
108
|
+
return {
|
|
109
|
+
error: {
|
|
110
|
+
code: error.code ?? 'INTERNAL_ERROR',
|
|
111
|
+
exitCode: error.exitCode ?? EXIT_CODES.INTERNAL_ERROR,
|
|
112
|
+
message: error.safeMessage ?? error.message ?? 'Protocol operation failed.',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function protocolsCommand(input, dependencies = {}) {
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = requireFlags(input);
|
|
121
|
+
const fs = dependencies.fs ?? filesystem;
|
|
122
|
+
const projectRoot = resolve(parsed.flags.project ?? dependencies.cwd?.() ?? process.cwd());
|
|
123
|
+
await (dependencies.configLoader ?? loadProjectConfig)(projectRoot, { fs });
|
|
124
|
+
const root = protocolRoot(projectRoot, fs, { create: ['add', 'import'].includes(parsed.subcommand) });
|
|
125
|
+
const operands = parsed.operands;
|
|
126
|
+
const command = parsed.subcommand, flags = parsed.flags;
|
|
127
|
+
if (flags.from !== undefined && !['add','import','update'].includes(command)) fail("Option '--from' is only valid for add/import/update.");
|
|
128
|
+
if (flags.publish !== undefined && command !== 'update') fail("Only update --publish activates a protocol.");
|
|
129
|
+
if ((flags['include-drafts'] !== undefined || flags['include-retired'] !== undefined) && !['find','show'].includes(command)) fail('Status inclusion options are only valid for find/show.');
|
|
130
|
+
if (flags['expected-digest'] !== undefined && command !== 'show') fail("Option '--expected-digest' is only valid for show.");
|
|
131
|
+
if (flags['expected-revision'] !== undefined && !['update','retire','show'].includes(command)) fail("Option '--expected-revision' is only valid for update/retire/show.");
|
|
132
|
+
if (command === 'show' && (flags['expected-revision'] !== undefined || flags['expected-digest'] !== undefined)
|
|
133
|
+
&& (!/^[1-9][0-9]{0,9}$/.test(String(flags['expected-revision'] ?? '')) || Number(flags['expected-revision']) > 1_000_000_000 || !DIGEST.test(flags['expected-digest'] ?? ''))) fail('Exact protocol lookup requires both --expected-revision and --expected-digest from the approved run.');
|
|
134
|
+
if (command === 'retire' && (operands.length !== 1 || !/^[1-9][0-9]{0,9}$/.test(String(flags['expected-revision'] ?? '')) || Number(flags['expected-revision']) > 1_000_000_000)) fail('Use rivet protocols retire <slug> --expected-revision=<current revision>.');
|
|
135
|
+
if (parsed.subcommand === 'add' && (operands.length !== 1 || (parsed.flags.from !== undefined && typeof parsed.flags.from !== 'string'))) fail('Use rivet protocols add <slug>.');
|
|
136
|
+
if (parsed.subcommand === 'import' && (operands.length !== 1 || typeof parsed.flags.from !== 'string')) fail('Use rivet protocols import <slug> --from=<path>.');
|
|
137
|
+
if (parsed.subcommand === 'validate' && operands.length > 1) fail('Use rivet protocols validate [<slug>].');
|
|
138
|
+
if (parsed.subcommand === 'find' && (operands.length !== 1 || typeof operands[0] !== 'string' || operands[0].length < 1 || operands[0].length > MAX_QUERY_LENGTH)) fail('Use rivet protocols find <query>.');
|
|
139
|
+
if (parsed.subcommand === 'show' && operands.length !== 1) fail('Use rivet protocols show <slug>.');
|
|
140
|
+
if (parsed.subcommand === 'update' && (operands.length !== 1 || typeof parsed.flags.from !== 'string' || parsed.flags.publish && parsed.flags['include-drafts'])) fail('Use rivet protocols update <slug> --from=<path> [--publish].');
|
|
141
|
+
if (parsed.subcommand === 'update' && (!/^\d+$/.test(String(parsed.flags['expected-revision'] ?? '')) || Number(parsed.flags['expected-revision']) < 1)) fail("Option '--expected-revision' is required for update.");
|
|
142
|
+
|
|
143
|
+
if (parsed.subcommand === 'add' && parsed.flags.from === undefined) {
|
|
144
|
+
const id = safeId(operands[0]);
|
|
145
|
+
const path = protocolPath(root, id);
|
|
146
|
+
if (lstatIfExists(path, fs)) fail(`Protocol '${id}' already exists.`, 'REPOSITORY_CONFLICT');
|
|
147
|
+
const metadata = { schemaVersion: 1, id, title: id.replaceAll('-', ' ').replace(/\b\w/g, letter => letter.toUpperCase()), status: 'draft', revision: 1, updatedAt: now() };
|
|
148
|
+
const body = canonicalBody(`# ${metadata.title}\n\n## Owner\n\n## Purpose\n\n## Applies when\n\n## Procedure\n\n## Required checks and evidence\n`);
|
|
149
|
+
metadata.digest = bodyDigest(metadata, body);
|
|
150
|
+
const source = render(metadata, body);
|
|
151
|
+
const record = validateDocument(source, id);
|
|
152
|
+
atomicWrite(path, source, fs, root.identity);
|
|
153
|
+
return emit(dependencies, parsed, { protocol: publicProtocol(record), message: `Created draft protocol '${id}'. ${draftGuidance(id, 1)}` });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parsed.subcommand === 'import' || parsed.subcommand === 'add') {
|
|
157
|
+
const id = safeId(operands[0]);
|
|
158
|
+
const path = protocolPath(root, id);
|
|
159
|
+
if (lstatIfExists(path, fs)) fail(`Protocol '${id}' already exists.`, 'REPOSITORY_CONFLICT');
|
|
160
|
+
const body = canonicalBody(readBounded(sourcePath(root, parsed.flags.from, fs), fs, 'Protocol import source'));
|
|
161
|
+
const heading = body.match(/^#\s+(\S.*)$/m);
|
|
162
|
+
if (!heading) fail('Protocol import source must contain a Markdown heading.');
|
|
163
|
+
const metadata = { schemaVersion: 1, id, title: safeText(heading[1].trim(), 120, 'Protocol title'), status: 'draft', revision: 1, updatedAt: now() };
|
|
164
|
+
metadata.digest = bodyDigest(metadata, body);
|
|
165
|
+
const source = render(metadata, body);
|
|
166
|
+
const record = validateDocument(source, id);
|
|
167
|
+
atomicWrite(path, source, fs, root.identity);
|
|
168
|
+
return emit(dependencies, parsed, { protocol: publicProtocol(record), message: `Imported draft protocol '${id}'. ${draftGuidance(id, 1)}` });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (parsed.subcommand === 'show') {
|
|
172
|
+
const id = safeId(operands[0]);
|
|
173
|
+
const record = validateDocument(readBounded(protocolPath(root, id), fs, `Protocol '${id}'`), id);
|
|
174
|
+
if (!record || (record.metadata.status === 'draft' && parsed.flags['include-drafts'] !== true) || (record.metadata.status === 'retired' && parsed.flags['include-retired'] !== true)) fail(`Protocol '${id}' was not found.`);
|
|
175
|
+
if (parsed.flags['expected-revision'] !== undefined && (record.metadata.status !== 'active' || record.metadata.revision !== Number(parsed.flags['expected-revision']) || record.metadata.digest !== parsed.flags['expected-digest'])) fail('Selected protocol changed or is no longer active. Create a new reviewed proposal.', 'REPOSITORY_CONFLICT');
|
|
176
|
+
return emit(dependencies, parsed, { protocol: publicProtocol(record, true), message: `Showing protocol '${id}'.` });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const all = discover(root, fs, true);
|
|
180
|
+
if (parsed.subcommand === 'validate') {
|
|
181
|
+
const records = operands.length === 1 ? all.filter(record => record.metadata.id === operands[0]) : all;
|
|
182
|
+
if (operands.length === 1 && records.length === 0) fail(`Protocol '${operands[0]}' was not found.`);
|
|
183
|
+
return emit(dependencies, parsed, { protocols: records.map(record => publicProtocol(record)), message: `Validated integrity of ${records.length} protocol${records.length === 1 ? '' : 's'}. Completeness diagnostics are reported separately; incomplete records cannot publish a new revision.` });
|
|
184
|
+
}
|
|
185
|
+
if (parsed.subcommand === 'find') {
|
|
186
|
+
const query = parsed.operands[0].normalize('NFKC').toLocaleLowerCase();
|
|
187
|
+
const records = discover(root, fs, parsed.flags['include-drafts'] === true, parsed.flags['include-retired'] === true)
|
|
188
|
+
.filter(record => `${record.metadata.title}\n${record.body}`.toLocaleLowerCase().includes(query));
|
|
189
|
+
return emit(dependencies, parsed, { protocols: records.map(record => publicProtocol(record)), message: `Found ${records.length} matching protocol${records.length === 1 ? '' : 's'}.` });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
const id = safeId(operands[0]);
|
|
194
|
+
const current = all.find(record => record.metadata.id === id);
|
|
195
|
+
if (!current) fail(`Protocol '${id}' was not found.`);
|
|
196
|
+
if (current.metadata.revision !== Number(parsed.flags['expected-revision'])) fail('Protocol revision conflict.', 'REPOSITORY_CONFLICT');
|
|
197
|
+
if (parsed.subcommand === 'retire') {
|
|
198
|
+
const metadata = { ...current.metadata, status: 'retired', revision: current.metadata.revision + 1, updatedAt: now() };
|
|
199
|
+
metadata.digest = bodyDigest(metadata, current.body);
|
|
200
|
+
const source = render(metadata, current.body), record = validateDocument(source, id);
|
|
201
|
+
atomicWrite(protocolPath(root, id), source, fs, root.identity, current.metadata.digest);
|
|
202
|
+
return emit(dependencies, parsed, {protocol: publicProtocol(record), message: `Retired protocol '${id}'. Existing runs require a new reviewed proposal.`});
|
|
203
|
+
}
|
|
204
|
+
const body = canonicalBody(readBounded(sourcePath(root, parsed.flags.from, fs), fs, 'Protocol update source'));
|
|
205
|
+
if (parsed.flags.publish === true) requireProtocolCompleteness(body);
|
|
206
|
+
const heading = body.match(/^#\s+(\S.*)$/m);
|
|
207
|
+
if (!heading) fail('Protocol update source must contain a Markdown heading.');
|
|
208
|
+
const metadata = { ...current.metadata, title: safeText(heading[1].trim(), 120, 'Protocol title'), status: parsed.flags.publish === true ? 'active' : 'draft', revision: current.metadata.revision + 1, updatedAt: now() };
|
|
209
|
+
metadata.digest = bodyDigest(metadata, body);
|
|
210
|
+
const source = render(metadata, body);
|
|
211
|
+
const record = validateDocument(source, id);
|
|
212
|
+
atomicWrite(protocolPath(root, id), source, fs, root.identity, current.metadata.digest);
|
|
213
|
+
return emit(dependencies, parsed, { protocol: publicProtocol(record), message: `Updated protocol '${id}'.${metadata.status === 'draft' ? ' ' + draftGuidance(id, metadata.revision) : ''}` });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const safe = error instanceof CliError ? error : new CliError('Protocol operation could not complete safely.', 'INTERNAL_ERROR');
|
|
216
|
+
return emit(dependencies, parsed ?? input, errorResult(safe), safe.exitCode);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function activeProtocolContextRefs(projectRoot, options = {}) {
|
|
221
|
+
const fs = options.fs ?? filesystem;
|
|
222
|
+
const root = protocolRoot(resolve(projectRoot), fs);
|
|
223
|
+
return Object.freeze(discover(root, fs, false, false).map(record => (
|
|
224
|
+
`protocol:${record.metadata.id}:${record.metadata.revision}:${record.metadata.digest}`
|
|
225
|
+
)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { MAX_PROTOCOL_BYTES, PROTOCOL_DIRECTORY, validateDocument };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { gitExecutable, resolveConfiguredProject } from '../cli/project-discovery.js';
|
|
2
|
+
import { runArgv } from '../discovery/tools.js';
|
|
3
|
+
import { CliError, EXIT_CODES } from '../cli/output.js';
|
|
4
|
+
import { createNodeProviderTransport } from '../adapters/node-transport.js';
|
|
5
|
+
import { createRepositoryProvider, discoverRepositoryRemotes, selectConfiguredRepositoryRemote } from '../repositories/index.js';
|
|
6
|
+
|
|
7
|
+
const ENDPOINTS = Object.freeze({github:'https://api.github.com',bitbucket:'https://api.bitbucket.org/2.0',gitlab:'https://gitlab.com/api/v4'});
|
|
8
|
+
const ENV_NAME = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
9
|
+
function fail(message, code) { throw new CliError(message, code); }
|
|
10
|
+
|
|
11
|
+
function selectProvider(config, repository, flags) {
|
|
12
|
+
const candidates = config.providers.providers.filter(provider => provider.kind === 'git-ci'
|
|
13
|
+
&& provider.mode !== 'disabled' && (provider.transport ?? 'direct-api') === 'direct-api'
|
|
14
|
+
&& provider.capabilities.includes('repository-read')
|
|
15
|
+
&& (flags.review === undefined || provider.capabilities.includes('checks-read'))
|
|
16
|
+
&& (!provider.projectIds?.length || provider.projectIds.includes(config.project.id))
|
|
17
|
+
&& (!provider.resourceIds?.length || provider.resourceIds.includes(repository.fullName))
|
|
18
|
+
&& typeof provider.endpoint === 'string'
|
|
19
|
+
&& provider.endpoint.replace(/\/$/,'') === ENDPOINTS[repository.provider]
|
|
20
|
+
&& (flags.provider === undefined || provider.id === flags.provider));
|
|
21
|
+
if (candidates.length !== 1) fail('Configure one applicable git-ci provider with repository-read, the correct API endpoint and repository scope. Review inspection also requires checks-read. Use --provider=<id> when several match.', 'MISSING_CONFIGURATION');
|
|
22
|
+
return candidates[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function headers(provider, environment) {
|
|
26
|
+
const credentials = provider.credentials ?? {};
|
|
27
|
+
const keys = Object.keys(credentials);
|
|
28
|
+
if (keys.length !== 1 || !['tokenEnv','accessTokenEnv','apiTokenEnv'].includes(keys[0])
|
|
29
|
+
|| !ENV_NAME.test(credentials[keys[0]])) {
|
|
30
|
+
fail('Configure one token environment reference for repository reads.', 'MISSING_CONFIGURATION');
|
|
31
|
+
}
|
|
32
|
+
const token = environment[credentials[keys[0]]];
|
|
33
|
+
if (typeof token !== 'string' || !token || token.length > 8192 || /[\s\u0000-\u001f\u007f]/.test(token)) {
|
|
34
|
+
fail('The configured repository token is missing or invalid. Set its environment variable and retry.', 'PROVIDER_UNAVAILABLE');
|
|
35
|
+
}
|
|
36
|
+
// This CLI accepts bearer credentials only; it never reads host application credentials.
|
|
37
|
+
return {authorization:`Bearer ${token}`,accept:'application/json'};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function repositoriesCommand(parsed, dependencies) {
|
|
41
|
+
const {subcommand,operands,flags} = parsed;
|
|
42
|
+
if(subcommand !== 'inspect' || operands.length
|
|
43
|
+
|| Object.keys(flags).some(key=>!['project','json','remote','provider','review'].includes(key))
|
|
44
|
+
|| (flags.review !== undefined && (!/^[1-9][0-9]{0,8}$/.test(flags.review)))
|
|
45
|
+
|| (flags.provider !== undefined && !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(flags.provider))) {
|
|
46
|
+
fail('Use rivet repositories inspect [--review=<number>] [--remote=<name>] [--provider=<id>] [--project=<path>] [--json].','INVALID_INPUT');
|
|
47
|
+
}
|
|
48
|
+
const project = await resolveConfiguredProject(dependencies.cwd(),flags.project,{runner:dependencies.runGit,env:dependencies.env});
|
|
49
|
+
let repository;
|
|
50
|
+
try {
|
|
51
|
+
const executable = await gitExecutable(dependencies.env);
|
|
52
|
+
const runner = (_command, args, options) => (dependencies.runGit ?? runArgv)(executable, args, options);
|
|
53
|
+
const remotes = await (dependencies.repositories?.discoverRemotes ?? discoverRepositoryRemotes)(project.root, {runner});
|
|
54
|
+
repository = selectConfiguredRepositoryRemote(remotes, project.config.project.repository.remote, flags.remote);
|
|
55
|
+
} catch {
|
|
56
|
+
fail('Repository remote is missing, ambiguous, changed or unsupported. Review setup --remote=<name>, or choose a GitHub.com, Bitbucket Cloud or GitLab.com remote with --remote=<name>.','REPOSITORY_CONFLICT');
|
|
57
|
+
}
|
|
58
|
+
const provider = selectProvider(project.config,repository,flags);
|
|
59
|
+
const auth = headers(provider,dependencies.env);
|
|
60
|
+
let data;
|
|
61
|
+
try {
|
|
62
|
+
const adapter = createRepositoryProvider({repository,headers:auth,
|
|
63
|
+
transport:dependencies.repositories?.transport ?? createNodeProviderTransport()});
|
|
64
|
+
data = flags.review === undefined ? await adapter.read({kind:'repository'}) : await adapter.inspect({number:Number(flags.review)});
|
|
65
|
+
} catch(error) {
|
|
66
|
+
if(error?.code === 'ERR_PROVIDER_STATE_CONFLICT') fail('The review head changed during inspection. Retry to capture a consistent snapshot.','REPOSITORY_CONFLICT');
|
|
67
|
+
fail('Repository inspection failed. Check token access, repository or review existence, rate limits and provider availability.','PROVIDER_UNAVAILABLE');
|
|
68
|
+
}
|
|
69
|
+
const result = {repository,providerId:provider.id,networkChecked:true,observation:data};
|
|
70
|
+
if(flags.json) dependencies.output.json({ok:true,result});
|
|
71
|
+
else {
|
|
72
|
+
dependencies.output.log(`${repository.fullName} (${repository.provider}, remote ${repository.remoteName})`);
|
|
73
|
+
dependencies.output.log(JSON.stringify(data,null,2).replace(/[\u0000-\u0008\u000b-\u001f\u007f\u009b]/g,c=>`\\u${c.codePointAt(0).toString(16).padStart(4,'0')}`));
|
|
74
|
+
dependencies.output.log('Read-only observation. This does not authorize merge or deployment.');
|
|
75
|
+
}
|
|
76
|
+
return EXIT_CODES.SUCCESS;
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CliError } from '../cli/output.js';
|
|
2
|
+
import { discoverGit } from '../discovery/git.js';
|
|
3
|
+
import { discoverRepositoryRemotes, parseRepositoryRemote, selectConfiguredRepositoryRemote } from '../repositories/index.js';
|
|
4
|
+
import { defaultIntegrationSetupPrompt } from '../cli/integration-setup-prompt.js';
|
|
5
|
+
|
|
6
|
+
const failure = () => new CliError('Repository remote is missing, ambiguous or changed. Review local Git remotes and use setup --remote=<name> to select the intended repository.', 'REPOSITORY_CONFLICT');
|
|
7
|
+
export async function prepareSetupRemote(root, flags, dependencies, preference) {
|
|
8
|
+
const read = async () => {
|
|
9
|
+
const git = await (dependencies.gitDiscovery ?? discoverGit)(root,{runner:dependencies.runner});
|
|
10
|
+
if (!git.repository) return [];
|
|
11
|
+
try { return await (dependencies.repositories?.discoverRemotes ?? discoverRepositoryRemotes)(root,{runner:dependencies.runner}); }
|
|
12
|
+
catch { throw failure(); }
|
|
13
|
+
};
|
|
14
|
+
const remotes = await read();
|
|
15
|
+
const choices = remotes.flatMap(remote => {
|
|
16
|
+
try { const selected=selectConfiguredRepositoryRemote([remote]);return [{name:selected.remoteName,url:selected.url}]; }
|
|
17
|
+
catch { return []; }
|
|
18
|
+
});
|
|
19
|
+
let selected;
|
|
20
|
+
if (flags.remote !== undefined || preference) {
|
|
21
|
+
try { const found=selectConfiguredRepositoryRemote(remotes,preference,flags.remote);selected={name:found.remoteName,url:found.url}; }
|
|
22
|
+
catch { throw failure(); }
|
|
23
|
+
} else if (choices.length === 1) selected=choices[0];
|
|
24
|
+
else if (choices.length > 1 && flags.write) {
|
|
25
|
+
if (flags.json || !(dependencies.terminalIsInteractive?.() ?? false)) throw failure();
|
|
26
|
+
const prompt=dependencies.setupRemotePrompt ?? defaultIntegrationSetupPrompt;
|
|
27
|
+
const name=await prompt({type:'select',message:'Choose the repository remote for inspection and delivery:',choices:choices.map(choice=>({value:choice.name,label:`${choice.name}: ${choice.url}`}))});
|
|
28
|
+
selected=choices.find(choice=>choice.name===name);
|
|
29
|
+
if (!selected) throw new CliError('Repository selection cancelled. No setup files were written.','REPOSITORY_CONFLICT');
|
|
30
|
+
}
|
|
31
|
+
const verify = async () => {
|
|
32
|
+
const current=await read();
|
|
33
|
+
if (selected) {
|
|
34
|
+
try { selectConfiguredRepositoryRemote(current,selected); } catch { throw failure(); }
|
|
35
|
+
} else {
|
|
36
|
+
const safe=current.flatMap(remote=>{try{return [{name:remote.name,url:parseRepositoryRemote(remote.url).url}];}catch{return [];}});
|
|
37
|
+
if (JSON.stringify(safe)!==JSON.stringify(choices)) throw failure();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return {selected,choices,status:selected?'selected':choices.length?'selection-required':'local-only',verify};
|
|
41
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as filesystem from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { CliError, EXIT_CODES } from '../cli/output.js';
|
|
5
|
+
import { loadProjectConfig } from '../config/load.js';
|
|
6
|
+
import { prepareSetupRemote } from './setup-remote.js';
|
|
7
|
+
import { init, prepareRepositoryRemoteUpdate } from './init.js';
|
|
8
|
+
import { inspectManagedInstall, managedInstall } from '../install/managed.js';
|
|
9
|
+
import {parseCheckCommand} from '../discovery/portable.js';
|
|
10
|
+
import {defaultIntegrationSetupPrompt} from '../cli/integration-setup-prompt.js';
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILES = ['project.yaml', 'providers.yaml', 'orchestration.yaml', 'quality.yaml'];
|
|
13
|
+
|
|
14
|
+
function emit(parsed, dependencies, result, code = EXIT_CODES.SUCCESS) {
|
|
15
|
+
if (parsed.flags.json) dependencies.output.json(result, code === 0 ? 'stdout' : 'stderr');
|
|
16
|
+
else {
|
|
17
|
+
const write = code === 0 ? dependencies.output.log : dependencies.output.error;
|
|
18
|
+
write(result.message);
|
|
19
|
+
for (const action of result.nextSteps ?? []) write(` ${action}`);
|
|
20
|
+
}
|
|
21
|
+
return code;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function captured(operation, parsed, dependencies) {
|
|
25
|
+
let value;
|
|
26
|
+
const code = await operation({ ...parsed, flags: { ...parsed.flags, json: true } }, {
|
|
27
|
+
...dependencies,
|
|
28
|
+
output: { json: payload => { value = payload; }, log() {}, error() {} },
|
|
29
|
+
});
|
|
30
|
+
return { code, value };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function existingConfiguration(root, fs) {
|
|
34
|
+
const path = join(root, '.rivet');
|
|
35
|
+
const entry = fs.lstatSync(path, { throwIfNoEntry: false });
|
|
36
|
+
if (!entry) return false;
|
|
37
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
38
|
+
throw new CliError('Project .rivet configuration must be a regular directory.', 'REPOSITORY_CONFLICT');
|
|
39
|
+
}
|
|
40
|
+
// A partially initialized directory needs explicit repair, never automatic replacement.
|
|
41
|
+
if (!CONFIG_FILES.every(name => {
|
|
42
|
+
const file = fs.lstatSync(join(path, name), { throwIfNoEntry: false });
|
|
43
|
+
return file?.isFile() && !file.isSymbolicLink();
|
|
44
|
+
})) throw new CliError('Existing .rivet configuration is incomplete. Repair it before running setup; no files were replaced.', 'REPOSITORY_CONFLICT');
|
|
45
|
+
try { await loadProjectConfig(root, { fs }); }
|
|
46
|
+
catch { throw new CliError('Existing .rivet configuration is invalid. Repair it before running setup; no files were replaced.', 'REPOSITORY_CONFLICT'); }
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function previewSteps(preview) {
|
|
51
|
+
if (!preview?.proposal?.commands) return [];
|
|
52
|
+
const gates = new Map((preview.proposal.qualityGates ?? [])
|
|
53
|
+
.map(gate => [gate.command, gate.required ? 'required' : 'optional']));
|
|
54
|
+
const commands=Object.entries(preview.proposal.commands).flatMap(([logicalId, command]) => {
|
|
55
|
+
const steps = Array.isArray(command) ? [{ cwd: '.', argv: command }] : command.steps;
|
|
56
|
+
return steps.map((step, index) => {
|
|
57
|
+
const prefix = Array.isArray(command)
|
|
58
|
+
? `project.commands.${logicalId}` : `project.commands.${logicalId}.steps[${index}]`;
|
|
59
|
+
const source = preview.proposal.provenance?.[`${prefix}.cwd`]?.source
|
|
60
|
+
?? preview.proposal.provenance?.[`${prefix}[0]`]?.source
|
|
61
|
+
?? preview.proposal.provenance?.[prefix]?.source
|
|
62
|
+
?? 'unavailable';
|
|
63
|
+
return `${logicalId} (${gates.get(logicalId) ?? 'not-gated'}): ${step.cwd} -> ${step.argv.join(' ')} [source: ${source}]`;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
const installation=preview.proposal.dependencies;
|
|
67
|
+
if(installation) {
|
|
68
|
+
commands.push(`Dependency inputs: ${JSON.stringify(installation.inputs)}. Installation requires separate approval.`);
|
|
69
|
+
for(const step of installation.steps)commands.push(`Dependency install: ${JSON.stringify(step.cwd)} -> ${JSON.stringify(step.argv)}`);
|
|
70
|
+
}
|
|
71
|
+
return commands;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function setupCommand(parsed, dependencies) {
|
|
75
|
+
const flags = parsed.flags;
|
|
76
|
+
if (parsed.operands.length || parsed.subcommand !== null
|
|
77
|
+
|| Object.keys(flags).some(key => !['project', 'global', 'target', 'write', 'json', 'remote', 'checks-json', 'dependencies-json'].includes(key))
|
|
78
|
+
|| (flags.global && (flags.project !== undefined || flags.remote !== undefined || flags['checks-json'] !== undefined || flags['dependencies-json'] !== undefined))
|
|
79
|
+
|| (flags.target !== undefined && !['claude', 'codex', 'both'].includes(flags.target))) {
|
|
80
|
+
throw new CliError('Use rivet setup [--project=<path>|--global] [--remote=<name>] [--target=claude|codex|both] [--write].', 'INVALID_INPUT');
|
|
81
|
+
}
|
|
82
|
+
const fs = dependencies.fs ?? filesystem;
|
|
83
|
+
const root = resolve(dependencies.cwd(), flags.project ?? '.');
|
|
84
|
+
const target = flags.target ?? 'both';
|
|
85
|
+
const projectArgument = root === resolve(dependencies.cwd()) ? ''
|
|
86
|
+
: ` --project='${root.replaceAll("'", "'\"'\"'")}'`;
|
|
87
|
+
const installer = { command: 'install', subcommand: null, operands: [], flags: {
|
|
88
|
+
minimal: true, target, ...(flags.global ? { global: true } : { project: root }),
|
|
89
|
+
} };
|
|
90
|
+
const services = dependencies.setup ?? {};
|
|
91
|
+
const inspect = services.inspectInstall ?? inspectManagedInstall;
|
|
92
|
+
const install = services.install ?? managedInstall;
|
|
93
|
+
const initialize = services.init ?? init;
|
|
94
|
+
// Preflight every install destination before creating any project configuration.
|
|
95
|
+
const installation = await inspect(installer, dependencies);
|
|
96
|
+
let configuration = { status: 'not-applicable' };
|
|
97
|
+
let preview, remote, remoteUpdate;
|
|
98
|
+
let checks = flags['checks-json'];
|
|
99
|
+
const installFlags=flags['dependencies-json']===undefined?{}:{'dependencies-json':flags['dependencies-json']};
|
|
100
|
+
if (!flags.global) {
|
|
101
|
+
const retained = await existingConfiguration(root, fs);
|
|
102
|
+
if(retained && (checks!==undefined || flags['dependencies-json']!==undefined)) throw new CliError('Existing configuration was preserved. Review and edit commands in .rivet/project.yaml and gates in .rivet/quality.yaml to change existing checks or dependency installation.','INVALID_INPUT');
|
|
103
|
+
const existing = retained ? await loadProjectConfig(root, {fs}) : null;
|
|
104
|
+
remote = await prepareSetupRemote(root, flags, dependencies, existing?.project.repository.remote);
|
|
105
|
+
if (retained) {
|
|
106
|
+
configuration = { status: 'preserved' };
|
|
107
|
+
if (remote.selected && JSON.stringify(remote.selected) !== JSON.stringify(existing.project.repository.remote)) {
|
|
108
|
+
const transaction = await prepareRepositoryRemoteUpdate(root, {fs, beforeCommit:remote.verify});
|
|
109
|
+
remoteUpdate = transaction.propose(remote.selected);
|
|
110
|
+
configuration = {status:'remote-proposed', projectYaml:remoteUpdate.yaml};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
preview = await captured(initialize, {
|
|
115
|
+
command: 'init', subcommand: null, operands: [], flags: { project: root, ...installFlags, ...(checks!==undefined?{'checks-json':checks}:{}) },
|
|
116
|
+
}, {...dependencies,setupRemoteSelection:remote});
|
|
117
|
+
if(preview.code===EXIT_CODES.MISSING_CONFIGURATION && checks===undefined && flags.write && !flags.json && dependencies.terminalIsInteractive?.()) {
|
|
118
|
+
const answer=await (dependencies.projectChecksPrompt??defaultIntegrationSetupPrompt)({type:'input',message:'No checks were detected. Enter this project’s verification command (for example python3 -m pytest; no shell chaining):'});
|
|
119
|
+
if(answer) {
|
|
120
|
+
try {checks=JSON.stringify({test:parseCheckCommand(answer)});}
|
|
121
|
+
catch(error) {throw new CliError(error.message,'INVALID_INPUT');}
|
|
122
|
+
preview=await captured(initialize,{command:'init',subcommand:null,operands:[],flags:{project:root,...installFlags,'checks-json':checks}}, {...dependencies,setupRemoteSelection:remote});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (preview.code !== 0) return emit(parsed, dependencies, {
|
|
126
|
+
ok: false, status: 'blocked', message: preview.value?.error?.message ?? 'Project discovery failed. No setup files were written.',
|
|
127
|
+
configuration: preview.value,
|
|
128
|
+
nextSteps: [`Run rivet init${projectArgument} --json for configuration diagnostics.`],
|
|
129
|
+
}, preview.code);
|
|
130
|
+
configuration = { ...preview.value, status: 'proposed' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const missingScripts = preview?.value.discovery.unresolved?.map(item => item.command) ?? [];
|
|
134
|
+
const tools = preview?.value.discovery.tools;
|
|
135
|
+
const missingTools = tools ? ['node', 'git', ...Object.keys(tools).filter(key => ['npm', 'pnpm', 'yarn', 'bun'].includes(key))]
|
|
136
|
+
.filter(name => !tools[name]?.present || (name === 'node' && Number.parseInt(tools.node.version, 10) < 22)) : [];
|
|
137
|
+
const blockers = [
|
|
138
|
+
...missingTools.map(name => `Install ${name === 'node' ? 'Node.js 22 or newer' : name} and make it available on PATH.`),
|
|
139
|
+
];
|
|
140
|
+
const warnings = [
|
|
141
|
+
...missingScripts.map(name => `No '${name}' package script was detected in the supported root/immediate-child scope. Add one before relying on this required check.`),
|
|
142
|
+
...(preview?.value.discovery.warnings ?? []).map(item => item.message),
|
|
143
|
+
];
|
|
144
|
+
const result = {
|
|
145
|
+
ok: true, status: 'preview', scope: flags.global ? 'global' : 'project', target,
|
|
146
|
+
configuration, installation, blockers, warnings,
|
|
147
|
+
...(remote ? {repository:{status:remote.status,selected:remote.selected??null,choices:remote.choices}} : {}),
|
|
148
|
+
message: 'Setup preview: no files were written.',
|
|
149
|
+
nextSteps: blockers.length ? [...blockers, ...warnings] : [
|
|
150
|
+
...warnings,
|
|
151
|
+
...(!flags.global && preview ? [
|
|
152
|
+
`Project configuration: ${CONFIG_FILES.map(name => join(root, '.rivet', name)).join(', ')}`,
|
|
153
|
+
...previewSteps(preview.value),
|
|
154
|
+
'Checks have not run; setup only inspected bounded project metadata.',
|
|
155
|
+
] : []),
|
|
156
|
+
...(installation.targets ?? installation.result?.targets ?? [])
|
|
157
|
+
.map(entry => `${entry.target} skill: ${entry.skillDir} (${entry.action})`),
|
|
158
|
+
...(remote?.choices.length ? remote.choices.map(choice=>`${remote.selected?.name === choice.name ? 'Selected repository remote' : 'Available repository remote'}: ${choice.name} -> ${choice.url}`) : []),
|
|
159
|
+
...(remote?.status === 'selection-required' ? ['Choose a repository with --remote=<name> when applying setup.'] : []),
|
|
160
|
+
'Repeat with --write to apply this setup.',
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
if (!flags.write) return emit(parsed, dependencies, result);
|
|
164
|
+
if (blockers.length) return emit(parsed, dependencies, {
|
|
165
|
+
...result, ok: false, status: 'blocked', message: 'Resolve the setup checks before writing configuration.',
|
|
166
|
+
}, EXIT_CODES.FAILED_GATE);
|
|
167
|
+
await remote?.verify();
|
|
168
|
+
let configurationWritten = false;
|
|
169
|
+
if (remoteUpdate) {
|
|
170
|
+
const cleanup = await remoteUpdate.commit();
|
|
171
|
+
configuration = {status:'remote-written',cleanup};
|
|
172
|
+
configurationWritten = true;
|
|
173
|
+
}
|
|
174
|
+
if (preview) {
|
|
175
|
+
const written = await captured(initialize, {
|
|
176
|
+
command: 'init', subcommand: null, operands: [], flags: { project: root, write: true, ...installFlags, ...(checks!==undefined?{'checks-json':checks}:{}) },
|
|
177
|
+
}, {...dependencies,setupRemoteSelection:remote});
|
|
178
|
+
if (written.code !== 0) return emit(parsed, dependencies, {
|
|
179
|
+
...result, ok: false, status: 'blocked', configuration: written.value,
|
|
180
|
+
message: 'Configuration could not be written safely; harness installation was not started.',
|
|
181
|
+
}, written.code);
|
|
182
|
+
configuration = { ...written.value, status: 'written' };
|
|
183
|
+
configurationWritten = true;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const installed = await captured(install, installer, dependencies);
|
|
187
|
+
if (installed.code !== 0) throw new CliError('Harness installation did not complete.', 'REPOSITORY_CONFLICT');
|
|
188
|
+
return emit(parsed, dependencies, {
|
|
189
|
+
...result, status: 'configured', configuration, installation: installed.value,
|
|
190
|
+
message: flags.global ? 'Minimal Rivet harness instructions installed globally.' : 'Project configuration and minimal Rivet harness instructions are ready.',
|
|
191
|
+
nextSteps: [
|
|
192
|
+
...warnings,
|
|
193
|
+
'Reload your harness so it discovers the Rivet skill.',
|
|
194
|
+
'Ask your harness: "Read the Rivet skill and report this project\'s configured checks."',
|
|
195
|
+
...(flags.global ? ['Run rivet setup from each project root to configure that project.'] : [
|
|
196
|
+
'Review and commit the generated setup files. Confirm repository.defaultBranch in .rivet/project.yaml and switch to that branch before starting a task.',
|
|
197
|
+
`Run rivet doctor${projectArgument} to inspect configured readiness.`,
|
|
198
|
+
`Optionally run rivet integrations setup${projectArgument} to guide read-only tracker and context configuration.`,
|
|
199
|
+
`Run rivet preflight --mode=host${projectArgument} before starting a task.`,
|
|
200
|
+
]),
|
|
201
|
+
'This configures instructions and policy; model authentication and active-harness execution are separate.',
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const denied = error instanceof CliError && error.code === 'BLOCKED_AUTHORITY';
|
|
206
|
+
return emit(parsed, dependencies, {
|
|
207
|
+
ok: false, status: configurationWritten ? 'partial' : 'blocked', configurationWritten,
|
|
208
|
+
message: denied ? error.safeMessage : 'Harness installation stopped. Existing files and any newly written project configuration are preserved.',
|
|
209
|
+
...(denied ? { error: { code: error.code, exitCode: error.exitCode, message: error.safeMessage } } : {}),
|
|
210
|
+
nextSteps: denied ? ['Existing project configuration is preserved. After normal permission approval, rerun the reviewed setup command.']
|
|
211
|
+
: ['Resolve the installation conflict, then rerun setup. Inspect rivet install --minimal --json for details.'],
|
|
212
|
+
}, denied ? EXIT_CODES.BLOCKED_AUTHORITY : EXIT_CODES.REPOSITORY_CONFLICT);
|
|
213
|
+
}
|
|
214
|
+
}
|