@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 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 architecture gate",
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 to avoid ghost-intent blocks.
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