@getripple/cli 1.0.9 → 1.0.10
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/dist/index.js +200 -10
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
package/dist/index.js
CHANGED
|
@@ -544,6 +544,19 @@ function parseCliArgs(argv) {
|
|
|
544
544
|
options.base = token.slice("--base=".length);
|
|
545
545
|
continue;
|
|
546
546
|
}
|
|
547
|
+
if (token === "--sha") {
|
|
548
|
+
const value = argv[i + 1];
|
|
549
|
+
if (!value || value.startsWith("-")) {
|
|
550
|
+
throw new Error("Missing value for --sha");
|
|
551
|
+
}
|
|
552
|
+
options.sha = value;
|
|
553
|
+
i++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (token.startsWith("--sha=")) {
|
|
557
|
+
options.sha = token.slice("--sha=".length);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
547
560
|
if (token === "--budget") {
|
|
548
561
|
const value = argv[i + 1];
|
|
549
562
|
if (!value || value.startsWith("-")) {
|
|
@@ -627,18 +640,21 @@ function rippleDirectRunnerHookLines() {
|
|
|
627
640
|
const GITHUB_ACTIONS_WORKFLOW_PATH = core_1.RIPPLE_CI_WORKFLOW_PATH;
|
|
628
641
|
function githubActionsWorkflow() {
|
|
629
642
|
return [
|
|
630
|
-
"name: Ripple",
|
|
643
|
+
"name: Ripple Enterprise Gate",
|
|
631
644
|
"",
|
|
632
645
|
"on:",
|
|
633
646
|
" pull_request:",
|
|
647
|
+
" push:",
|
|
648
|
+
" branches: [main, master]",
|
|
634
649
|
"",
|
|
635
650
|
"permissions:",
|
|
636
651
|
" contents: read",
|
|
637
652
|
" pull-requests: read",
|
|
653
|
+
" checks: write",
|
|
638
654
|
"",
|
|
639
655
|
"jobs:",
|
|
640
656
|
" ripple:",
|
|
641
|
-
" name: Ripple
|
|
657
|
+
" name: Ripple authorization gate",
|
|
642
658
|
" runs-on: ubuntu-latest",
|
|
643
659
|
" steps:",
|
|
644
660
|
" - name: Checkout",
|
|
@@ -649,8 +665,11 @@ function githubActionsWorkflow() {
|
|
|
649
665
|
" uses: actions/setup-node@v4",
|
|
650
666
|
" with:",
|
|
651
667
|
" node-version: 20",
|
|
652
|
-
" - name: Ripple CI",
|
|
653
|
-
` run: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations`,
|
|
668
|
+
" - name: Ripple CI gate",
|
|
669
|
+
` run: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations --sha \${{ github.sha }}`,
|
|
670
|
+
" env:",
|
|
671
|
+
" RIPPLE_API_KEY: ${{ secrets.RIPPLE_API_KEY }}",
|
|
672
|
+
" RIPPLE_CLOUD_URL: ${{ secrets.RIPPLE_CLOUD_URL }}",
|
|
654
673
|
"",
|
|
655
674
|
].join("\n");
|
|
656
675
|
}
|
|
@@ -3416,6 +3435,59 @@ async function gateCommand(options) {
|
|
|
3416
3435
|
else {
|
|
3417
3436
|
printGateSummary(gate);
|
|
3418
3437
|
}
|
|
3438
|
+
// ─── Ripple Cloud Sync ───────────────────────────────────────────────────
|
|
3439
|
+
// Records the gate decision to ripple-cloud for the compliance audit trail.
|
|
3440
|
+
// Runs on EVERY gate call — including the pre-commit hook blocking scenario.
|
|
3441
|
+
// Never blocks. Never throws. The commit result is unaffected by cloud sync.
|
|
3442
|
+
if (process.env.RIPPLE_API_KEY) {
|
|
3443
|
+
const cloudDecision = gate.canContinue ? "continue" :
|
|
3444
|
+
gate.needsHuman ? "human-review" : "blocked";
|
|
3445
|
+
const commitSha = (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3446
|
+
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3447
|
+
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3448
|
+
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3449
|
+
intentId: audit.intent.id,
|
|
3450
|
+
decision: cloudDecision,
|
|
3451
|
+
commitSha,
|
|
3452
|
+
branch,
|
|
3453
|
+
actor,
|
|
3454
|
+
source: "cli",
|
|
3455
|
+
payload: {
|
|
3456
|
+
// Identity
|
|
3457
|
+
intentId: audit.intent.id,
|
|
3458
|
+
commitSha,
|
|
3459
|
+
branch,
|
|
3460
|
+
actor,
|
|
3461
|
+
source: "pre-commit-gate",
|
|
3462
|
+
// Intent declared scope — what the developer said they would change
|
|
3463
|
+
task: audit.intent.task,
|
|
3464
|
+
targetFile: audit.intent.targetFile,
|
|
3465
|
+
controlMode: audit.intent.controlMode,
|
|
3466
|
+
boundaryRisk: audit.intent.boundaryRisk,
|
|
3467
|
+
humanGate: audit.intent.humanGate,
|
|
3468
|
+
// Gate verdict — what Ripple actually decided
|
|
3469
|
+
decision: cloudDecision,
|
|
3470
|
+
status: gate.status,
|
|
3471
|
+
canContinue: gate.canContinue,
|
|
3472
|
+
mustStop: gate.mustStop,
|
|
3473
|
+
needsHuman: gate.needsHuman,
|
|
3474
|
+
// Evidence — what actually changed
|
|
3475
|
+
blockingReasons: audit.blockingReasons ?? [],
|
|
3476
|
+
changedFiles: audit.changedFiles ?? [],
|
|
3477
|
+
changedOutsideBoundaryFiles: audit.stagedCheck?.intentValidation?.boundaryVerdict?.changedOutsideBoundaryFiles ?? [],
|
|
3478
|
+
},
|
|
3479
|
+
});
|
|
3480
|
+
if (!options.json && !options.agent) {
|
|
3481
|
+
if (cloudResult.sent) {
|
|
3482
|
+
const icon = cloudDecision === "continue" ? "✓" : "⛔";
|
|
3483
|
+
console.log(`\n[Ripple Cloud] ${icon} Gate decision recorded: ${cloudDecision} (${commitSha.slice(0, 7)})`);
|
|
3484
|
+
}
|
|
3485
|
+
else if (cloudResult.error) {
|
|
3486
|
+
console.warn(`\n[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
3419
3491
|
applyStrictExit(options.strict && !gate.canContinue);
|
|
3420
3492
|
}
|
|
3421
3493
|
function approveCommand(options) {
|
|
@@ -3684,6 +3756,31 @@ async function ciCommand(options) {
|
|
|
3684
3756
|
printGithubPolicyAuditAnnotations(summary, policySync);
|
|
3685
3757
|
}
|
|
3686
3758
|
writeGithubPolicyAuditStepSummary(summary, policySync);
|
|
3759
|
+
// FOUNDER FIX: Add Cloud Sync to the Policy Audit Path
|
|
3760
|
+
if (process.env.RIPPLE_API_KEY) {
|
|
3761
|
+
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3762
|
+
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3763
|
+
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3764
|
+
const syntheticIntentId = `ci-policy-${commitSha.slice(0, 16)}`;
|
|
3765
|
+
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3766
|
+
protocol: 'ripple-audit', // FOUNDER FIX: Satisfy the strict Zod Cloud Schema
|
|
3767
|
+
intentId: syntheticIntentId,
|
|
3768
|
+
decision: 'continue',
|
|
3769
|
+
commitSha,
|
|
3770
|
+
branch,
|
|
3771
|
+
actor,
|
|
3772
|
+
source: 'ci',
|
|
3773
|
+
payload: { mode: 'policy-audit', commitSha, branch, actor },
|
|
3774
|
+
});
|
|
3775
|
+
if (!options.json && !options.agent) {
|
|
3776
|
+
if (cloudResult.sent) {
|
|
3777
|
+
console.log(`\n[Ripple Cloud] ✓ Policy audit synced (${commitSha.slice(0, 7)})`);
|
|
3778
|
+
}
|
|
3779
|
+
else if (cloudResult.error) {
|
|
3780
|
+
console.warn(`\n[Ripple Cloud] ⚠ Sync failed: ${cloudResult.error}`);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3687
3784
|
return;
|
|
3688
3785
|
}
|
|
3689
3786
|
let intent;
|
|
@@ -3760,6 +3857,44 @@ async function ciCommand(options) {
|
|
|
3760
3857
|
printGithubAuditAnnotations(audit);
|
|
3761
3858
|
}
|
|
3762
3859
|
writeGithubAuditStepSummary(audit);
|
|
3860
|
+
// ─── Ripple Cloud Sync ──────────────────────────────────────────────────
|
|
3861
|
+
// Sends the audit result to ripple-cloud so the GitHub App can verify it.
|
|
3862
|
+
// Only runs when RIPPLE_API_KEY is set. Never blocks or throws.
|
|
3863
|
+
if (process.env.RIPPLE_API_KEY) {
|
|
3864
|
+
const gate = (0, core_1.buildRippleGateSummary)(audit);
|
|
3865
|
+
const cloudDecision = gate.canContinue ? "continue" :
|
|
3866
|
+
gate.needsHuman ? "human-review" : "blocked";
|
|
3867
|
+
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3868
|
+
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3869
|
+
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3870
|
+
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3871
|
+
intentId: intent.id,
|
|
3872
|
+
decision: cloudDecision,
|
|
3873
|
+
commitSha,
|
|
3874
|
+
branch,
|
|
3875
|
+
actor,
|
|
3876
|
+
source: "ci",
|
|
3877
|
+
payload: {
|
|
3878
|
+
intentId: intent.id,
|
|
3879
|
+
commitSha,
|
|
3880
|
+
branch,
|
|
3881
|
+
actor,
|
|
3882
|
+
decision: cloudDecision,
|
|
3883
|
+
gate,
|
|
3884
|
+
violations: audit.violations?.length ?? 0,
|
|
3885
|
+
riskLevel: audit.riskLevel ?? "unknown",
|
|
3886
|
+
},
|
|
3887
|
+
});
|
|
3888
|
+
if (!options.json && !options.agent) {
|
|
3889
|
+
if (cloudResult.sent) {
|
|
3890
|
+
console.log(`[Ripple Cloud] ✓ Audit synced (${commitSha.slice(0, 7)})`);
|
|
3891
|
+
}
|
|
3892
|
+
else if (cloudResult.error) {
|
|
3893
|
+
console.warn(`[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
3763
3898
|
applyStrictExit(options.strict && strictAuditShouldFail(audit));
|
|
3764
3899
|
}
|
|
3765
3900
|
async function doctorCommand(options) {
|
|
@@ -4030,11 +4165,11 @@ fi
|
|
|
4030
4165
|
|
|
4031
4166
|
set +e
|
|
4032
4167
|
|
|
4033
|
-
ripple_run() {
|
|
4034
|
-
${rippleDirectRunnerHookLines().join("\n")}
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
if [ -f ".ripple/intents/latest.json" ]; then
|
|
4168
|
+
ripple_run() {
|
|
4169
|
+
${rippleDirectRunnerHookLines().join("\n")}
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
if [ -f ".ripple/intents/latest.json" ]; then
|
|
4038
4173
|
echo "[Ripple] Active local intent found. Checking staged changes against approved boundary..."
|
|
4039
4174
|
ripple_run gate --staged --intent latest --agent --strict
|
|
4040
4175
|
status=$?
|
|
@@ -4086,9 +4221,64 @@ function ripplePreCommitHookScript() {
|
|
|
4086
4221
|
function ripplePostCommitHookBlock() {
|
|
4087
4222
|
return [
|
|
4088
4223
|
RIPPLE_POST_COMMIT_HOOK_START,
|
|
4089
|
-
`# Local intents are consumed after a successful commit
|
|
4224
|
+
`# Local intents are consumed after a successful commit.
|
|
4225
|
+
# If RIPPLE_API_KEY is set, the commit is synced to ripple-cloud for GitHub App verification.
|
|
4090
4226
|
set +e
|
|
4091
4227
|
|
|
4228
|
+
COMMIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
4229
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
4230
|
+
ACTOR=$(git config user.name 2>/dev/null || git config user.email 2>/dev/null || echo "unknown")
|
|
4231
|
+
CLOUD_URL=\${RIPPLE_CLOUD_URL:-https://your-ripple-cloud.vercel.app}
|
|
4232
|
+
|
|
4233
|
+
INTENT_FILE=""
|
|
4234
|
+
for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
|
|
4235
|
+
if [ -f "$intent_file" ]; then
|
|
4236
|
+
INTENT_FILE="$intent_file"
|
|
4237
|
+
break
|
|
4238
|
+
fi
|
|
4239
|
+
done
|
|
4240
|
+
|
|
4241
|
+
# Cloud sync — only if API key is set and we have an intent to sync
|
|
4242
|
+
if [ -n "$RIPPLE_API_KEY" ] && [ -n "$INTENT_FILE" ]; then
|
|
4243
|
+
INTENT_ID=$(node -e "try{const d=require('fs').readFileSync(process.argv[1],'utf8');console.log(JSON.parse(d).id||'')}catch(e){console.log('')}" "$INTENT_FILE" 2>/dev/null || echo "")
|
|
4244
|
+
if [ -n "$INTENT_ID" ]; then
|
|
4245
|
+
node -e "
|
|
4246
|
+
const https = require('https');
|
|
4247
|
+
const http = require('http');
|
|
4248
|
+
const url = new URL(process.env.CLOUD_URL + '/api/audit');
|
|
4249
|
+
const isHttps = url.protocol === 'https:';
|
|
4250
|
+
const body = JSON.stringify({
|
|
4251
|
+
intentId: process.env.INTENT_ID,
|
|
4252
|
+
decision: 'continue',
|
|
4253
|
+
commitSha: process.env.COMMIT_SHA,
|
|
4254
|
+
branch: process.env.BRANCH,
|
|
4255
|
+
actor: process.env.ACTOR,
|
|
4256
|
+
source: 'cli',
|
|
4257
|
+
payload: { commitSha: process.env.COMMIT_SHA, source: 'post-commit-hook', branch: process.env.BRANCH }
|
|
4258
|
+
});
|
|
4259
|
+
const lib = isHttps ? https : http;
|
|
4260
|
+
const req = lib.request({
|
|
4261
|
+
hostname: url.hostname,
|
|
4262
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
4263
|
+
path: url.pathname,
|
|
4264
|
+
method: 'POST',
|
|
4265
|
+
headers: {
|
|
4266
|
+
'Content-Type': 'application/json',
|
|
4267
|
+
'Authorization': 'Bearer ' + process.env.RIPPLE_API_KEY,
|
|
4268
|
+
'Content-Length': Buffer.byteLength(body)
|
|
4269
|
+
}
|
|
4270
|
+
}, res => {
|
|
4271
|
+
if (res.statusCode < 300) process.stdout.write('[Ripple Cloud] Commit synced (' + process.env.COMMIT_SHA.slice(0,7) + ')\\n');
|
|
4272
|
+
});
|
|
4273
|
+
req.setTimeout(3000, () => req.destroy());
|
|
4274
|
+
req.on('error', () => {});
|
|
4275
|
+
req.write(body);
|
|
4276
|
+
req.end();
|
|
4277
|
+
" 2>/dev/null || true
|
|
4278
|
+
fi
|
|
4279
|
+
fi
|
|
4280
|
+
|
|
4281
|
+
# Clear the local intent after sync
|
|
4092
4282
|
cleared=0
|
|
4093
4283
|
for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
|
|
4094
4284
|
if [ -f "$intent_file" ]; then
|