coderifts 2.0.0 → 3.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 +92 -0
- package/bin/coderifts.js +124 -0
- package/dist/cli.js +2142 -24
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3007,7 +3007,7 @@ var require_package = __commonJS({
|
|
|
3007
3007
|
"package.json"(exports2, module2) {
|
|
3008
3008
|
module2.exports = {
|
|
3009
3009
|
name: "coderifts",
|
|
3010
|
-
version: "
|
|
3010
|
+
version: "3.1.0",
|
|
3011
3011
|
description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
3012
3012
|
author: "CodeRifts <hello@coderifts.com>",
|
|
3013
3013
|
license: "MIT",
|
|
@@ -13493,33 +13493,39 @@ var require_cloud = __commonJS({
|
|
|
13493
13493
|
"src/cloud.js"(exports2, module2) {
|
|
13494
13494
|
"use strict";
|
|
13495
13495
|
var https = require("https");
|
|
13496
|
-
var API_BASE = "https://app.coderifts.com";
|
|
13497
|
-
function
|
|
13496
|
+
var API_BASE = process.env.CODERIFTS_API_BASE || "https://app.coderifts.com";
|
|
13497
|
+
function cloudRequest(method, pathname, apiKey, bodyObj = null) {
|
|
13498
13498
|
return new Promise((resolve, reject) => {
|
|
13499
|
-
const body = JSON.stringify(
|
|
13500
|
-
const
|
|
13499
|
+
const body = bodyObj != null ? JSON.stringify(bodyObj) : null;
|
|
13500
|
+
const base = API_BASE.replace(/\/$/, "");
|
|
13501
|
+
const url = new URL(pathname.startsWith("http") ? pathname : base + pathname);
|
|
13501
13502
|
const options = {
|
|
13502
13503
|
hostname: url.hostname,
|
|
13503
|
-
port: 443,
|
|
13504
|
-
path: url.pathname,
|
|
13505
|
-
method
|
|
13504
|
+
port: url.port || (url.protocol === "http:" ? 80 : 443),
|
|
13505
|
+
path: url.pathname + url.search,
|
|
13506
|
+
method,
|
|
13506
13507
|
headers: {
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
"
|
|
13510
|
-
"
|
|
13508
|
+
Accept: "application/json",
|
|
13509
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13510
|
+
"User-Agent": "@coderifts/cli",
|
|
13511
|
+
...body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } : {}
|
|
13511
13512
|
}
|
|
13512
13513
|
};
|
|
13513
|
-
const
|
|
13514
|
+
const transport = url.protocol === "http:" ? require("http") : https;
|
|
13515
|
+
const req = transport.request(options, (res) => {
|
|
13514
13516
|
let data = "";
|
|
13515
13517
|
res.on("data", (chunk) => {
|
|
13516
13518
|
data += chunk;
|
|
13517
13519
|
});
|
|
13518
13520
|
res.on("end", () => {
|
|
13519
13521
|
try {
|
|
13520
|
-
const parsed = JSON.parse(data);
|
|
13522
|
+
const parsed = data ? JSON.parse(data) : {};
|
|
13521
13523
|
if (res.statusCode >= 400) {
|
|
13522
|
-
|
|
13524
|
+
const err = new Error(parsed.message || parsed.error || `HTTP ${res.statusCode}`);
|
|
13525
|
+
err.statusCode = res.statusCode;
|
|
13526
|
+
err.code = parsed.error || null;
|
|
13527
|
+
err.body = parsed;
|
|
13528
|
+
reject(err);
|
|
13523
13529
|
} else {
|
|
13524
13530
|
resolve(parsed);
|
|
13525
13531
|
}
|
|
@@ -13529,11 +13535,31 @@ var require_cloud = __commonJS({
|
|
|
13529
13535
|
});
|
|
13530
13536
|
});
|
|
13531
13537
|
req.on("error", reject);
|
|
13532
|
-
req.write(body);
|
|
13538
|
+
if (body) req.write(body);
|
|
13533
13539
|
req.end();
|
|
13534
13540
|
});
|
|
13535
13541
|
}
|
|
13536
|
-
|
|
13542
|
+
function cloudDiff(oldSpec, newSpec, apiKey) {
|
|
13543
|
+
return cloudRequest("POST", "/api/v1/diff", apiKey, {
|
|
13544
|
+
old_spec: oldSpec,
|
|
13545
|
+
new_spec: newSpec
|
|
13546
|
+
});
|
|
13547
|
+
}
|
|
13548
|
+
function cloudGetEnforcementStatus(repo, apiKey) {
|
|
13549
|
+
const q = encodeURIComponent(String(repo || ""));
|
|
13550
|
+
return cloudRequest("GET", `/api/v1/enforcement-status?repo=${q}`, apiKey);
|
|
13551
|
+
}
|
|
13552
|
+
function cloudGetLock(repo, apiKey) {
|
|
13553
|
+
const q = encodeURIComponent(String(repo || ""));
|
|
13554
|
+
return cloudRequest("GET", `/api/v1/lock?repo=${q}`, apiKey);
|
|
13555
|
+
}
|
|
13556
|
+
module2.exports = {
|
|
13557
|
+
cloudDiff,
|
|
13558
|
+
cloudRequest,
|
|
13559
|
+
cloudGetEnforcementStatus,
|
|
13560
|
+
cloudGetLock,
|
|
13561
|
+
API_BASE
|
|
13562
|
+
};
|
|
13537
13563
|
}
|
|
13538
13564
|
});
|
|
13539
13565
|
|
|
@@ -68958,12 +68984,15 @@ var require_deploy_gate2 = __commonJS({
|
|
|
68958
68984
|
attestation_source: "cli_flag"
|
|
68959
68985
|
};
|
|
68960
68986
|
}
|
|
68961
|
-
function deployReportResiduals(state,
|
|
68987
|
+
function deployReportResiduals(state, enforcement_inescapable, enforcement, change_set_rebound) {
|
|
68962
68988
|
const out = [];
|
|
68963
|
-
if (state
|
|
68989
|
+
if (state !== "success") return out;
|
|
68990
|
+
if (enforcement_inescapable !== true) {
|
|
68964
68991
|
if (enforcement === "ENFORCING") out.push("bypass_open");
|
|
68965
68992
|
else if (enforcement === "ADVISORY") out.push("deploy_gate_advisory");
|
|
68966
68993
|
else if (enforcement === "ABSENT") out.push("deploy_path_ungated");
|
|
68994
|
+
} else if (change_set_rebound !== true) {
|
|
68995
|
+
out.push("change_set_not_rebound");
|
|
68967
68996
|
}
|
|
68968
68997
|
return out;
|
|
68969
68998
|
}
|
|
@@ -68991,19 +69020,24 @@ var require_deploy_gate2 = __commonJS({
|
|
|
68991
69020
|
bypass_possible: !(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false)
|
|
68992
69021
|
}
|
|
68993
69022
|
};
|
|
69023
|
+
let change_set_rebound = false;
|
|
68994
69024
|
if (attested_enforcement === "ENFORCING") {
|
|
68995
|
-
if (expected_fingerprint != null)
|
|
69025
|
+
if (expected_fingerprint != null) {
|
|
69026
|
+
requiredContext.expected_fingerprint = expected_fingerprint;
|
|
69027
|
+
change_set_rebound = true;
|
|
69028
|
+
}
|
|
68996
69029
|
if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
|
|
68997
69030
|
}
|
|
68998
69031
|
const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
|
|
68999
|
-
const
|
|
69032
|
+
const enforcement_inescapable = gate.inescapable_deploy === true;
|
|
69033
|
+
const inescapable_deploy = enforcement_inescapable && change_set_rebound === true;
|
|
69000
69034
|
return {
|
|
69001
69035
|
deploy_check_status: gate.state,
|
|
69002
69036
|
reason: gate.reason,
|
|
69003
69037
|
must_re_preflight: REPAIRABLE.has(gate.reason),
|
|
69004
69038
|
attested_enforcement,
|
|
69005
69039
|
gate: { deploy_allowed: gate.deploy_allowed, reason: gate.reason, inescapable_deploy },
|
|
69006
|
-
report_residuals: deployReportResiduals(gate.state,
|
|
69040
|
+
report_residuals: deployReportResiduals(gate.state, enforcement_inescapable, attested_enforcement, change_set_rebound),
|
|
69007
69041
|
coverage_deploy_input: deployCoverageInput(attested_enforcement, inescapable_deploy)
|
|
69008
69042
|
};
|
|
69009
69043
|
}
|
|
@@ -69513,6 +69547,560 @@ var require_publish_gate = __commonJS({
|
|
|
69513
69547
|
}
|
|
69514
69548
|
});
|
|
69515
69549
|
|
|
69550
|
+
// src/registry-validation-core.js
|
|
69551
|
+
var require_registry_validation_core = __commonJS({
|
|
69552
|
+
"src/registry-validation-core.js"(exports2, module2) {
|
|
69553
|
+
"use strict";
|
|
69554
|
+
var yaml = require_js_yaml();
|
|
69555
|
+
function safeParse(content) {
|
|
69556
|
+
if (!content) return null;
|
|
69557
|
+
try {
|
|
69558
|
+
const trimmed = content.trim();
|
|
69559
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
69560
|
+
return JSON.parse(trimmed);
|
|
69561
|
+
}
|
|
69562
|
+
return yaml.load(trimmed);
|
|
69563
|
+
} catch (_) {
|
|
69564
|
+
return null;
|
|
69565
|
+
}
|
|
69566
|
+
}
|
|
69567
|
+
function validateOpenApiSpec(parsed) {
|
|
69568
|
+
if (!parsed || typeof parsed !== "object") {
|
|
69569
|
+
return { valid: false, error: "Not a valid YAML or JSON object" };
|
|
69570
|
+
}
|
|
69571
|
+
if (parsed.openapi) {
|
|
69572
|
+
const ver = String(parsed.openapi);
|
|
69573
|
+
if (ver.startsWith("3.")) {
|
|
69574
|
+
return { valid: true, version: ver };
|
|
69575
|
+
}
|
|
69576
|
+
return { valid: false, error: `Unsupported OpenAPI version: ${ver}` };
|
|
69577
|
+
}
|
|
69578
|
+
if (parsed.swagger) {
|
|
69579
|
+
const ver = String(parsed.swagger);
|
|
69580
|
+
if (ver.startsWith("2.")) {
|
|
69581
|
+
return { valid: true, version: ver };
|
|
69582
|
+
}
|
|
69583
|
+
return { valid: false, error: `Unsupported Swagger version: ${ver}` };
|
|
69584
|
+
}
|
|
69585
|
+
return { valid: false, error: "Missing required field: 'openapi' or 'swagger'" };
|
|
69586
|
+
}
|
|
69587
|
+
function extractEndpoints(parsed) {
|
|
69588
|
+
const endpoints = [];
|
|
69589
|
+
const paths = parsed.paths || {};
|
|
69590
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
69591
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
69592
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
69593
|
+
for (const method of httpMethods) {
|
|
69594
|
+
if (pathItem[method]) {
|
|
69595
|
+
endpoints.push({
|
|
69596
|
+
path,
|
|
69597
|
+
method: method.toUpperCase(),
|
|
69598
|
+
operationId: pathItem[method].operationId || ""
|
|
69599
|
+
});
|
|
69600
|
+
}
|
|
69601
|
+
}
|
|
69602
|
+
}
|
|
69603
|
+
return endpoints;
|
|
69604
|
+
}
|
|
69605
|
+
function extractSchemaNames(parsed) {
|
|
69606
|
+
const schemas = [];
|
|
69607
|
+
const components = parsed.components?.schemas || {};
|
|
69608
|
+
for (const [name, schema] of Object.entries(components)) {
|
|
69609
|
+
const hash = JSON.stringify(schema);
|
|
69610
|
+
schemas.push({ name, hash });
|
|
69611
|
+
}
|
|
69612
|
+
return schemas;
|
|
69613
|
+
}
|
|
69614
|
+
function extractDefinedScopes(parsed) {
|
|
69615
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
69616
|
+
const schemes = parsed.components?.securitySchemes || {};
|
|
69617
|
+
for (const scheme of Object.values(schemes)) {
|
|
69618
|
+
if (scheme.type === "oauth2" && scheme.flows) {
|
|
69619
|
+
for (const flow of Object.values(scheme.flows)) {
|
|
69620
|
+
if (flow.scopes) {
|
|
69621
|
+
for (const scope of Object.keys(flow.scopes)) {
|
|
69622
|
+
scopes.add(scope);
|
|
69623
|
+
}
|
|
69624
|
+
}
|
|
69625
|
+
}
|
|
69626
|
+
}
|
|
69627
|
+
}
|
|
69628
|
+
return scopes;
|
|
69629
|
+
}
|
|
69630
|
+
function extractUsedScopes(parsed) {
|
|
69631
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
69632
|
+
if (Array.isArray(parsed.security)) {
|
|
69633
|
+
for (const req of parsed.security) {
|
|
69634
|
+
for (const scopeList of Object.values(req)) {
|
|
69635
|
+
if (Array.isArray(scopeList)) {
|
|
69636
|
+
for (const s of scopeList) scopes.add(s);
|
|
69637
|
+
}
|
|
69638
|
+
}
|
|
69639
|
+
}
|
|
69640
|
+
}
|
|
69641
|
+
const paths = parsed.paths || {};
|
|
69642
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
69643
|
+
for (const pathItem of Object.values(paths)) {
|
|
69644
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
69645
|
+
for (const method of httpMethods) {
|
|
69646
|
+
const op = pathItem[method];
|
|
69647
|
+
if (op?.security && Array.isArray(op.security)) {
|
|
69648
|
+
for (const req of op.security) {
|
|
69649
|
+
for (const scopeList of Object.values(req)) {
|
|
69650
|
+
if (Array.isArray(scopeList)) {
|
|
69651
|
+
for (const s of scopeList) scopes.add(s);
|
|
69652
|
+
}
|
|
69653
|
+
}
|
|
69654
|
+
}
|
|
69655
|
+
}
|
|
69656
|
+
}
|
|
69657
|
+
}
|
|
69658
|
+
return scopes;
|
|
69659
|
+
}
|
|
69660
|
+
function extractRefs(obj, refs = /* @__PURE__ */ new Set()) {
|
|
69661
|
+
if (!obj || typeof obj !== "object") return refs;
|
|
69662
|
+
if (Array.isArray(obj)) {
|
|
69663
|
+
for (const item of obj) extractRefs(item, refs);
|
|
69664
|
+
return refs;
|
|
69665
|
+
}
|
|
69666
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
69667
|
+
if (key === "$ref" && typeof value === "string") {
|
|
69668
|
+
refs.add(value);
|
|
69669
|
+
} else {
|
|
69670
|
+
extractRefs(value, refs);
|
|
69671
|
+
}
|
|
69672
|
+
}
|
|
69673
|
+
return refs;
|
|
69674
|
+
}
|
|
69675
|
+
function refResolves(parsed, ref) {
|
|
69676
|
+
if (!ref.startsWith("#/")) return true;
|
|
69677
|
+
const parts = ref.replace("#/", "").split("/");
|
|
69678
|
+
let current = parsed;
|
|
69679
|
+
for (const part of parts) {
|
|
69680
|
+
if (!current || typeof current !== "object") return false;
|
|
69681
|
+
current = current[part];
|
|
69682
|
+
}
|
|
69683
|
+
return current !== void 0;
|
|
69684
|
+
}
|
|
69685
|
+
function validateRegistry(specs) {
|
|
69686
|
+
if (!Array.isArray(specs) || specs.length === 0) {
|
|
69687
|
+
return { valid: true, issues: [], stats: { specs_count: 0, endpoints_count: 0, schemas_count: 0 } };
|
|
69688
|
+
}
|
|
69689
|
+
const issues = [];
|
|
69690
|
+
const parsedSpecs = [];
|
|
69691
|
+
for (const { name, spec } of specs) {
|
|
69692
|
+
const parsed = safeParse(spec);
|
|
69693
|
+
if (!parsed) {
|
|
69694
|
+
issues.push({
|
|
69695
|
+
severity: "error",
|
|
69696
|
+
type: "parse_error",
|
|
69697
|
+
message: `Could not parse spec '${name}' as valid YAML or JSON`,
|
|
69698
|
+
specs: [name]
|
|
69699
|
+
});
|
|
69700
|
+
continue;
|
|
69701
|
+
}
|
|
69702
|
+
const validation = validateOpenApiSpec(parsed);
|
|
69703
|
+
if (!validation.valid) {
|
|
69704
|
+
issues.push({
|
|
69705
|
+
severity: "error",
|
|
69706
|
+
type: "invalid_spec",
|
|
69707
|
+
message: `Spec '${name}' is not a valid OpenAPI document: ${validation.error}`,
|
|
69708
|
+
specs: [name]
|
|
69709
|
+
});
|
|
69710
|
+
continue;
|
|
69711
|
+
}
|
|
69712
|
+
parsedSpecs.push({ name, parsed });
|
|
69713
|
+
}
|
|
69714
|
+
const endpointMap = /* @__PURE__ */ new Map();
|
|
69715
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
69716
|
+
const endpoints = extractEndpoints(parsed);
|
|
69717
|
+
for (const ep of endpoints) {
|
|
69718
|
+
const key = `${ep.method} ${ep.path}`;
|
|
69719
|
+
if (!endpointMap.has(key)) endpointMap.set(key, []);
|
|
69720
|
+
endpointMap.get(key).push({ spec: name, operationId: ep.operationId });
|
|
69721
|
+
}
|
|
69722
|
+
}
|
|
69723
|
+
for (const [endpoint, owners] of endpointMap) {
|
|
69724
|
+
if (owners.length > 1) {
|
|
69725
|
+
const specNames = owners.map((o) => o.spec);
|
|
69726
|
+
issues.push({
|
|
69727
|
+
severity: "warning",
|
|
69728
|
+
type: "endpoint_collision",
|
|
69729
|
+
message: `Endpoint '${endpoint}' is defined in multiple specs: ${specNames.join(", ")}`,
|
|
69730
|
+
specs: specNames
|
|
69731
|
+
});
|
|
69732
|
+
}
|
|
69733
|
+
}
|
|
69734
|
+
const schemaMap = /* @__PURE__ */ new Map();
|
|
69735
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
69736
|
+
const schemas = extractSchemaNames(parsed);
|
|
69737
|
+
for (const s of schemas) {
|
|
69738
|
+
if (!schemaMap.has(s.name)) schemaMap.set(s.name, []);
|
|
69739
|
+
schemaMap.get(s.name).push({ spec: name, hash: s.hash });
|
|
69740
|
+
}
|
|
69741
|
+
}
|
|
69742
|
+
for (const [schemaName, definitions] of schemaMap) {
|
|
69743
|
+
if (definitions.length > 1) {
|
|
69744
|
+
const uniqueHashes = new Set(definitions.map((d) => d.hash));
|
|
69745
|
+
if (uniqueHashes.size > 1) {
|
|
69746
|
+
const specNames = definitions.map((d) => d.spec);
|
|
69747
|
+
issues.push({
|
|
69748
|
+
severity: "warning",
|
|
69749
|
+
type: "schema_conflict",
|
|
69750
|
+
message: `Schema '${schemaName}' has conflicting definitions across specs: ${specNames.join(", ")}`,
|
|
69751
|
+
specs: specNames
|
|
69752
|
+
});
|
|
69753
|
+
}
|
|
69754
|
+
}
|
|
69755
|
+
}
|
|
69756
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
69757
|
+
const defined = extractDefinedScopes(parsed);
|
|
69758
|
+
const used = extractUsedScopes(parsed);
|
|
69759
|
+
for (const scope of used) {
|
|
69760
|
+
if (!defined.has(scope)) {
|
|
69761
|
+
issues.push({
|
|
69762
|
+
severity: "warning",
|
|
69763
|
+
type: "undefined_scope",
|
|
69764
|
+
message: `Scope '${scope}' is used in '${name}' but not defined in securitySchemes`,
|
|
69765
|
+
specs: [name]
|
|
69766
|
+
});
|
|
69767
|
+
}
|
|
69768
|
+
}
|
|
69769
|
+
for (const scope of defined) {
|
|
69770
|
+
if (!used.has(scope)) {
|
|
69771
|
+
issues.push({
|
|
69772
|
+
severity: "info",
|
|
69773
|
+
type: "unused_scope",
|
|
69774
|
+
message: `Scope '${scope}' is defined in '${name}' but never used in any operation`,
|
|
69775
|
+
specs: [name]
|
|
69776
|
+
});
|
|
69777
|
+
}
|
|
69778
|
+
}
|
|
69779
|
+
}
|
|
69780
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
69781
|
+
const refs = extractRefs(parsed);
|
|
69782
|
+
for (const ref of refs) {
|
|
69783
|
+
if (!refResolves(parsed, ref)) {
|
|
69784
|
+
issues.push({
|
|
69785
|
+
severity: "error",
|
|
69786
|
+
type: "unresolved_ref",
|
|
69787
|
+
message: `$ref '${ref}' in '${name}' does not resolve`,
|
|
69788
|
+
specs: [name]
|
|
69789
|
+
});
|
|
69790
|
+
}
|
|
69791
|
+
}
|
|
69792
|
+
}
|
|
69793
|
+
let totalEndpoints = 0;
|
|
69794
|
+
let totalSchemas = 0;
|
|
69795
|
+
for (const { parsed } of parsedSpecs) {
|
|
69796
|
+
totalEndpoints += extractEndpoints(parsed).length;
|
|
69797
|
+
totalSchemas += extractSchemaNames(parsed).length;
|
|
69798
|
+
}
|
|
69799
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
69800
|
+
return {
|
|
69801
|
+
valid: !hasErrors,
|
|
69802
|
+
issues,
|
|
69803
|
+
stats: {
|
|
69804
|
+
specs_count: parsedSpecs.length,
|
|
69805
|
+
endpoints_count: totalEndpoints,
|
|
69806
|
+
schemas_count: totalSchemas
|
|
69807
|
+
}
|
|
69808
|
+
};
|
|
69809
|
+
}
|
|
69810
|
+
module2.exports = {
|
|
69811
|
+
validateRegistry,
|
|
69812
|
+
safeParse,
|
|
69813
|
+
validateOpenApiSpec,
|
|
69814
|
+
// Exported for testing
|
|
69815
|
+
extractEndpoints,
|
|
69816
|
+
extractSchemaNames,
|
|
69817
|
+
extractDefinedScopes,
|
|
69818
|
+
extractUsedScopes,
|
|
69819
|
+
extractRefs,
|
|
69820
|
+
refResolves
|
|
69821
|
+
};
|
|
69822
|
+
}
|
|
69823
|
+
});
|
|
69824
|
+
|
|
69825
|
+
// src/commands/registry-gate.js
|
|
69826
|
+
var require_registry_gate = __commonJS({
|
|
69827
|
+
"src/commands/registry-gate.js"(exports2, module2) {
|
|
69828
|
+
"use strict";
|
|
69829
|
+
var fs = require("fs");
|
|
69830
|
+
var path = require("path");
|
|
69831
|
+
var chalk = require_source();
|
|
69832
|
+
var { matchGlob } = require_cjs4();
|
|
69833
|
+
var {
|
|
69834
|
+
validateRegistry,
|
|
69835
|
+
safeParse,
|
|
69836
|
+
validateOpenApiSpec
|
|
69837
|
+
} = require_registry_validation_core();
|
|
69838
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
69839
|
+
var SPEC_EXT = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
|
|
69840
|
+
function walkSpecCandidates(rootDir, {
|
|
69841
|
+
readdirSync = fs.readdirSync.bind(fs),
|
|
69842
|
+
statSync = fs.statSync.bind(fs)
|
|
69843
|
+
} = {}) {
|
|
69844
|
+
const out = [];
|
|
69845
|
+
function walk(absDir) {
|
|
69846
|
+
let entries;
|
|
69847
|
+
try {
|
|
69848
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
69849
|
+
} catch (err) {
|
|
69850
|
+
const e = new Error(`Cannot read directory ${absDir}: ${err && err.message}`);
|
|
69851
|
+
e.code = "GATE_ERROR";
|
|
69852
|
+
e.path = absDir;
|
|
69853
|
+
throw e;
|
|
69854
|
+
}
|
|
69855
|
+
for (const ent of entries) {
|
|
69856
|
+
const name = ent.name;
|
|
69857
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
69858
|
+
const abs = path.join(absDir, name);
|
|
69859
|
+
let isDir = ent.isDirectory && ent.isDirectory();
|
|
69860
|
+
let isFile = ent.isFile && ent.isFile();
|
|
69861
|
+
if (!isDir && !isFile) {
|
|
69862
|
+
try {
|
|
69863
|
+
const st = statSync(abs);
|
|
69864
|
+
isDir = st.isDirectory();
|
|
69865
|
+
isFile = st.isFile();
|
|
69866
|
+
} catch (err) {
|
|
69867
|
+
const e = new Error(`Cannot stat ${abs}: ${err && err.message}`);
|
|
69868
|
+
e.code = "GATE_ERROR";
|
|
69869
|
+
e.path = abs;
|
|
69870
|
+
throw e;
|
|
69871
|
+
}
|
|
69872
|
+
}
|
|
69873
|
+
if (isDir) {
|
|
69874
|
+
walk(abs);
|
|
69875
|
+
continue;
|
|
69876
|
+
}
|
|
69877
|
+
if (isFile) {
|
|
69878
|
+
const ext = path.extname(name).toLowerCase();
|
|
69879
|
+
if (SPEC_EXT.has(ext)) out.push(abs);
|
|
69880
|
+
}
|
|
69881
|
+
}
|
|
69882
|
+
}
|
|
69883
|
+
walk(rootDir);
|
|
69884
|
+
return out.sort();
|
|
69885
|
+
}
|
|
69886
|
+
function relPosix(rootDir, absPath) {
|
|
69887
|
+
let rel = path.relative(rootDir, absPath);
|
|
69888
|
+
if (path.sep !== "/") rel = rel.split(path.sep).join("/");
|
|
69889
|
+
return rel;
|
|
69890
|
+
}
|
|
69891
|
+
function readFileThreeState(absPath, readFileSync = fs.readFileSync.bind(fs)) {
|
|
69892
|
+
try {
|
|
69893
|
+
const content = readFileSync(absPath, "utf8");
|
|
69894
|
+
return { ok: true, content: content == null ? "" : String(content) };
|
|
69895
|
+
} catch (err) {
|
|
69896
|
+
const msg = err && err.message ? String(err.message) : String(err);
|
|
69897
|
+
return {
|
|
69898
|
+
ok: false,
|
|
69899
|
+
code: "GATE_ERROR",
|
|
69900
|
+
message: `unreadable file: ${absPath} (${msg.slice(0, 200)})`,
|
|
69901
|
+
path: absPath
|
|
69902
|
+
};
|
|
69903
|
+
}
|
|
69904
|
+
}
|
|
69905
|
+
function discoverRegistrySpecs(dir, {
|
|
69906
|
+
glob = null,
|
|
69907
|
+
readdirSync = fs.readdirSync.bind(fs),
|
|
69908
|
+
readFileSync = fs.readFileSync.bind(fs),
|
|
69909
|
+
statSync = fs.statSync.bind(fs)
|
|
69910
|
+
} = {}) {
|
|
69911
|
+
const rootDir = path.resolve(dir);
|
|
69912
|
+
let candidates;
|
|
69913
|
+
try {
|
|
69914
|
+
candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
|
|
69915
|
+
} catch (err) {
|
|
69916
|
+
return {
|
|
69917
|
+
ok: false,
|
|
69918
|
+
code: err.code || "GATE_ERROR",
|
|
69919
|
+
message: err.message || String(err),
|
|
69920
|
+
path: err.path
|
|
69921
|
+
};
|
|
69922
|
+
}
|
|
69923
|
+
if (glob) {
|
|
69924
|
+
candidates = candidates.filter((abs) => matchGlob(glob, relPosix(rootDir, abs)));
|
|
69925
|
+
}
|
|
69926
|
+
const specs = [];
|
|
69927
|
+
let skippedNonSpec = 0;
|
|
69928
|
+
for (const abs of candidates) {
|
|
69929
|
+
const read = readFileThreeState(abs, readFileSync);
|
|
69930
|
+
if (!read.ok) {
|
|
69931
|
+
return {
|
|
69932
|
+
ok: false,
|
|
69933
|
+
code: "GATE_ERROR",
|
|
69934
|
+
message: read.message,
|
|
69935
|
+
path: read.path || abs
|
|
69936
|
+
};
|
|
69937
|
+
}
|
|
69938
|
+
const name = relPosix(rootDir, abs);
|
|
69939
|
+
const parsed = safeParse(read.content);
|
|
69940
|
+
if (!parsed || typeof parsed !== "object") {
|
|
69941
|
+
specs.push({ name, spec: read.content });
|
|
69942
|
+
continue;
|
|
69943
|
+
}
|
|
69944
|
+
const shape = validateOpenApiSpec(parsed);
|
|
69945
|
+
if (!shape.valid) {
|
|
69946
|
+
if (shape.error && shape.error.includes("Missing required field: 'openapi' or 'swagger'")) {
|
|
69947
|
+
skippedNonSpec += 1;
|
|
69948
|
+
continue;
|
|
69949
|
+
}
|
|
69950
|
+
specs.push({ name, spec: read.content });
|
|
69951
|
+
continue;
|
|
69952
|
+
}
|
|
69953
|
+
specs.push({ name, spec: read.content });
|
|
69954
|
+
}
|
|
69955
|
+
return {
|
|
69956
|
+
ok: true,
|
|
69957
|
+
specs,
|
|
69958
|
+
skippedNonSpec,
|
|
69959
|
+
candidates: candidates.length,
|
|
69960
|
+
rootDir
|
|
69961
|
+
};
|
|
69962
|
+
}
|
|
69963
|
+
function findingsFailGate(issues, { warnOnly = false, errorsOnly = false } = {}) {
|
|
69964
|
+
if (warnOnly) return false;
|
|
69965
|
+
for (const i of issues || []) {
|
|
69966
|
+
if (i.severity === "error") return true;
|
|
69967
|
+
if (i.severity === "warning" && !errorsOnly) return true;
|
|
69968
|
+
}
|
|
69969
|
+
return false;
|
|
69970
|
+
}
|
|
69971
|
+
function countBySeverity(issues) {
|
|
69972
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
69973
|
+
for (const i of issues || []) {
|
|
69974
|
+
if (i.severity === "error") c.error += 1;
|
|
69975
|
+
else if (i.severity === "warning") c.warning += 1;
|
|
69976
|
+
else if (i.severity === "info") c.info += 1;
|
|
69977
|
+
}
|
|
69978
|
+
return c;
|
|
69979
|
+
}
|
|
69980
|
+
function severityColor(sev) {
|
|
69981
|
+
if (sev === "error") return chalk.red;
|
|
69982
|
+
if (sev === "warning") return chalk.yellow;
|
|
69983
|
+
return chalk.cyan;
|
|
69984
|
+
}
|
|
69985
|
+
function printFindings(issues, log) {
|
|
69986
|
+
for (const i of issues || []) {
|
|
69987
|
+
const color = severityColor(i.severity);
|
|
69988
|
+
const files = Array.isArray(i.specs) ? i.specs.join(", ") : "";
|
|
69989
|
+
log(color(`[${i.severity}] ${i.type}`) + (files ? chalk.dim(` ${files}`) : ""));
|
|
69990
|
+
log(` ${i.message}`);
|
|
69991
|
+
}
|
|
69992
|
+
}
|
|
69993
|
+
function runRegistryGate(options = {}, deps = {}) {
|
|
69994
|
+
const log = deps.log || console.log.bind(console);
|
|
69995
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
69996
|
+
const cwd = deps.cwd || process.cwd();
|
|
69997
|
+
const dirArg = options.dir != null && options.dir !== "" ? options.dir : ".";
|
|
69998
|
+
const rootDir = path.isAbsolute(dirArg) ? dirArg : path.resolve(cwd, dirArg);
|
|
69999
|
+
const readdirSync = deps.readdirSync || fs.readdirSync.bind(fs);
|
|
70000
|
+
const readFileSync = deps.readFileSync || fs.readFileSync.bind(fs);
|
|
70001
|
+
const statSync = deps.statSync || fs.statSync.bind(fs);
|
|
70002
|
+
const existsSync = deps.existsSync || fs.existsSync.bind(fs);
|
|
70003
|
+
if (!existsSync(rootDir)) {
|
|
70004
|
+
const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 directory not found: ${rootDir}`;
|
|
70005
|
+
logErr(chalk.red(msg));
|
|
70006
|
+
return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
|
|
70007
|
+
}
|
|
70008
|
+
let st;
|
|
70009
|
+
try {
|
|
70010
|
+
st = statSync(rootDir);
|
|
70011
|
+
} catch (err) {
|
|
70012
|
+
const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 cannot access ${rootDir}: ${err && err.message}`;
|
|
70013
|
+
logErr(chalk.red(msg));
|
|
70014
|
+
return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
|
|
70015
|
+
}
|
|
70016
|
+
if (!st.isDirectory()) {
|
|
70017
|
+
const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 not a directory: ${rootDir}`;
|
|
70018
|
+
logErr(chalk.red(msg));
|
|
70019
|
+
return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
|
|
70020
|
+
}
|
|
70021
|
+
const discovered = discoverRegistrySpecs(rootDir, {
|
|
70022
|
+
glob: options.glob || null,
|
|
70023
|
+
readdirSync,
|
|
70024
|
+
readFileSync,
|
|
70025
|
+
statSync
|
|
70026
|
+
});
|
|
70027
|
+
if (!discovered.ok) {
|
|
70028
|
+
const msg = `CodeRifts registry-gate: ${discovered.code} \u2014 ${discovered.message}`;
|
|
70029
|
+
logErr(chalk.red(msg));
|
|
70030
|
+
return {
|
|
70031
|
+
ok: false,
|
|
70032
|
+
exitCode: 1,
|
|
70033
|
+
code: discovered.code || "GATE_ERROR",
|
|
70034
|
+
message: discovered.message,
|
|
70035
|
+
path: discovered.path
|
|
70036
|
+
};
|
|
70037
|
+
}
|
|
70038
|
+
if (discovered.specs.length === 0) {
|
|
70039
|
+
const msg = `CodeRifts registry-gate: REGISTRY_EMPTY \u2014 no OpenAPI/Swagger specs discovered (scanned ${discovered.candidates} yaml/json file(s), skipped non-spec: ${discovered.skippedNonSpec}). An admission gate guarding nothing must fail, not pass.`;
|
|
70040
|
+
logErr(chalk.red(msg));
|
|
70041
|
+
return {
|
|
70042
|
+
ok: false,
|
|
70043
|
+
exitCode: 1,
|
|
70044
|
+
code: "REGISTRY_EMPTY",
|
|
70045
|
+
message: msg,
|
|
70046
|
+
skippedNonSpec: discovered.skippedNonSpec,
|
|
70047
|
+
candidates: discovered.candidates
|
|
70048
|
+
};
|
|
70049
|
+
}
|
|
70050
|
+
const result = validateRegistry(discovered.specs);
|
|
70051
|
+
const counts = countBySeverity(result.issues);
|
|
70052
|
+
const mode = {
|
|
70053
|
+
warnOnly: !!options.warnOnly,
|
|
70054
|
+
errorsOnly: !!options.errorsOnly
|
|
70055
|
+
};
|
|
70056
|
+
const fail = findingsFailGate(result.issues, mode);
|
|
70057
|
+
if (result.issues.length > 0) {
|
|
70058
|
+
printFindings(result.issues, log);
|
|
70059
|
+
}
|
|
70060
|
+
const summary = [
|
|
70061
|
+
`specs=${result.stats.specs_count}`,
|
|
70062
|
+
`endpoints=${result.stats.endpoints_count}`,
|
|
70063
|
+
`schemas=${result.stats.schemas_count}`,
|
|
70064
|
+
`errors=${counts.error}`,
|
|
70065
|
+
`warnings=${counts.warning}`,
|
|
70066
|
+
`info=${counts.info}`,
|
|
70067
|
+
discovered.skippedNonSpec ? `skipped_non_spec=${discovered.skippedNonSpec}` : null,
|
|
70068
|
+
mode.warnOnly ? "mode=warn-only" : mode.errorsOnly ? "mode=errors-only" : "mode=default",
|
|
70069
|
+
fail ? "FAIL" : "PASS"
|
|
70070
|
+
].filter(Boolean).join(" ");
|
|
70071
|
+
if (fail) {
|
|
70072
|
+
log(chalk.red(`CodeRifts registry-gate: ${summary}`));
|
|
70073
|
+
} else {
|
|
70074
|
+
log(chalk.green(`CodeRifts registry-gate: ${summary}`));
|
|
70075
|
+
}
|
|
70076
|
+
if (counts.error === 0 && counts.warning === 0 && counts.info === 0) {
|
|
70077
|
+
log(chalk.dim(" checks: endpoint_collision, schema_conflict, scopes, unresolved_ref \u2014 no findings"));
|
|
70078
|
+
}
|
|
70079
|
+
return {
|
|
70080
|
+
ok: !fail,
|
|
70081
|
+
exitCode: fail ? 1 : 0,
|
|
70082
|
+
code: fail ? "REGISTRY_FINDINGS" : "REGISTRY_OK",
|
|
70083
|
+
issues: result.issues,
|
|
70084
|
+
stats: result.stats,
|
|
70085
|
+
counts,
|
|
70086
|
+
skippedNonSpec: discovered.skippedNonSpec,
|
|
70087
|
+
candidates: discovered.candidates,
|
|
70088
|
+
warnOnly: mode.warnOnly,
|
|
70089
|
+
errorsOnly: mode.errorsOnly
|
|
70090
|
+
};
|
|
70091
|
+
}
|
|
70092
|
+
module2.exports = {
|
|
70093
|
+
runRegistryGate,
|
|
70094
|
+
discoverRegistrySpecs,
|
|
70095
|
+
walkSpecCandidates,
|
|
70096
|
+
findingsFailGate,
|
|
70097
|
+
countBySeverity,
|
|
70098
|
+
readFileThreeState,
|
|
70099
|
+
relPosix
|
|
70100
|
+
};
|
|
70101
|
+
}
|
|
70102
|
+
});
|
|
70103
|
+
|
|
69516
70104
|
// src/commands/init.js
|
|
69517
70105
|
var require_init = __commonJS({
|
|
69518
70106
|
"src/commands/init.js"(exports2, module2) {
|
|
@@ -69772,6 +70360,63 @@ notifications:
|
|
|
69772
70360
|
on_breaking: true
|
|
69773
70361
|
on_risk_above: 60
|
|
69774
70362
|
|
|
70363
|
+
overlap_detection: true
|
|
70364
|
+
generator_detection: true
|
|
70365
|
+
`
|
|
70366
|
+
},
|
|
70367
|
+
"ai-agent-platform": {
|
|
70368
|
+
aliases: ["ai-agent-platform", "ai-agent", "agent", "mcp"],
|
|
70369
|
+
label: "AI Agent Platform",
|
|
70370
|
+
description: "Zero-tolerance removals for agent/MCP-consumed APIs",
|
|
70371
|
+
yaml: `# CodeRifts Policy: AI Agent Platform
|
|
70372
|
+
# For APIs consumed by AI agents and MCP tool surfaces.
|
|
70373
|
+
# Agent consumers cannot renegotiate contracts at runtime \u2014 a removed field
|
|
70374
|
+
# or endpoint is a hard break for automated callers (no human can "adapt").
|
|
70375
|
+
# Portable policy vocabulary only (same keys as other templates).
|
|
70376
|
+
|
|
70377
|
+
failOnBreaking: true
|
|
70378
|
+
|
|
70379
|
+
policy:
|
|
70380
|
+
# Zero tolerance: any breaking change blocks the check.
|
|
70381
|
+
max_breaking_changes: 0
|
|
70382
|
+
# Removals that break tool schemas / agent bindings are never silent.
|
|
70383
|
+
no_delete_endpoints: true
|
|
70384
|
+
no_delete_required_fields: true
|
|
70385
|
+
require_deprecation_before_removal: true
|
|
70386
|
+
require_version_bump_on_breaking: true
|
|
70387
|
+
# Freeze merges when risk is elevated (agents amplify blast radius).
|
|
70388
|
+
freeze_on_risk_score: 55
|
|
70389
|
+
|
|
70390
|
+
# Who must approve high-impact contract changes for agent-facing surfaces.
|
|
70391
|
+
approval_matrix:
|
|
70392
|
+
endpoint_removal:
|
|
70393
|
+
- agent-platform-owners
|
|
70394
|
+
- api-governance
|
|
70395
|
+
field_removal:
|
|
70396
|
+
- agent-platform-owners
|
|
70397
|
+
auth_change:
|
|
70398
|
+
- security-team
|
|
70399
|
+
- agent-platform-owners
|
|
70400
|
+
type_change:
|
|
70401
|
+
- agent-platform-owners
|
|
70402
|
+
|
|
70403
|
+
freeze_periods: []
|
|
70404
|
+
|
|
70405
|
+
risk_scoring:
|
|
70406
|
+
# dimension_weights is the key the engine reads (0-100 scale, relative weights)
|
|
70407
|
+
dimension_weights:
|
|
70408
|
+
revenue_impact: 20
|
|
70409
|
+
blast_radius: 35
|
|
70410
|
+
app_compatibility: 30
|
|
70411
|
+
security: 15
|
|
70412
|
+
|
|
70413
|
+
linting:
|
|
70414
|
+
enabled: true
|
|
70415
|
+
rules:
|
|
70416
|
+
naming_convention: true
|
|
70417
|
+
consistent_errors: true
|
|
70418
|
+
pagination_pattern: true
|
|
70419
|
+
|
|
69775
70420
|
overlap_detection: true
|
|
69776
70421
|
generator_detection: true
|
|
69777
70422
|
`
|
|
@@ -69793,7 +70438,8 @@ generator_detection: true
|
|
|
69793
70438
|
["growth", "Balanced between speed and safety"],
|
|
69794
70439
|
["fintech", "Maximum governance for regulated industries"],
|
|
69795
70440
|
["public-api", "Backward compatibility for external consumers"],
|
|
69796
|
-
["microservices", "Internal service-to-service with blast radius focus"]
|
|
70441
|
+
["microservices", "Internal service-to-service with blast radius focus"],
|
|
70442
|
+
["ai-agent", "Zero-tolerance removals for agent/MCP-consumed APIs"]
|
|
69797
70443
|
];
|
|
69798
70444
|
for (const [name, desc] of entries) {
|
|
69799
70445
|
console.log(` ${chalk.cyan(name.padEnd(16))}${chalk.dim(desc)}`);
|
|
@@ -69829,9 +70475,10 @@ generator_detection: true
|
|
|
69829
70475
|
console.log("");
|
|
69830
70476
|
console.log(chalk.green(` Created .coderifts.yml with ${tmpl.label} policy template.`));
|
|
69831
70477
|
console.log(chalk.dim(` Edit the file to customize approval teams, freeze periods, and domain mappings.`));
|
|
70478
|
+
console.log(chalk.dim(` Agent-using repo? Run: coderifts agent-setup`));
|
|
69832
70479
|
console.log("");
|
|
69833
70480
|
}
|
|
69834
|
-
module2.exports = { init, TEMPLATES };
|
|
70481
|
+
module2.exports = { init, TEMPLATES, resolveTemplate };
|
|
69835
70482
|
}
|
|
69836
70483
|
});
|
|
69837
70484
|
|
|
@@ -95188,6 +95835,679 @@ var require_login = __commonJS({
|
|
|
95188
95835
|
}
|
|
95189
95836
|
});
|
|
95190
95837
|
|
|
95838
|
+
// src/commands/setup-required-check.js
|
|
95839
|
+
var require_setup_required_check = __commonJS({
|
|
95840
|
+
"src/commands/setup-required-check.js"(exports2, module2) {
|
|
95841
|
+
"use strict";
|
|
95842
|
+
var { execFileSync } = require("child_process");
|
|
95843
|
+
var chalk = require_source();
|
|
95844
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
95845
|
+
var CHECK_NAME = "CodeRifts / contract-gate";
|
|
95846
|
+
var EXIT = {
|
|
95847
|
+
OK: 0,
|
|
95848
|
+
/** Target already required or dry-run printed successfully. */
|
|
95849
|
+
NEEDS_APPLY: 0,
|
|
95850
|
+
/** Unknown / permission / gh missing / verify failed. */
|
|
95851
|
+
ERROR: 1,
|
|
95852
|
+
PERMISSION: 2
|
|
95853
|
+
};
|
|
95854
|
+
function extractRequiredContexts(protection) {
|
|
95855
|
+
if (!protection || typeof protection !== "object") return [];
|
|
95856
|
+
const rsc = protection.required_status_checks;
|
|
95857
|
+
if (!rsc || typeof rsc !== "object") return [];
|
|
95858
|
+
if (Array.isArray(rsc.contexts) && rsc.contexts.length) {
|
|
95859
|
+
return rsc.contexts.map((c) => String(c));
|
|
95860
|
+
}
|
|
95861
|
+
if (Array.isArray(rsc.checks)) {
|
|
95862
|
+
return rsc.checks.map((c) => c && c.context != null ? String(c.context) : "").filter(Boolean);
|
|
95863
|
+
}
|
|
95864
|
+
return [];
|
|
95865
|
+
}
|
|
95866
|
+
function classifyObservation(read, contextName = CHECK_NAME) {
|
|
95867
|
+
const status = read && read.status != null ? Number(read.status) : null;
|
|
95868
|
+
if (status === 404) {
|
|
95869
|
+
return {
|
|
95870
|
+
state: "ABSENT",
|
|
95871
|
+
context_is_required: false,
|
|
95872
|
+
required_contexts: [],
|
|
95873
|
+
protection: null,
|
|
95874
|
+
observation_error: null
|
|
95875
|
+
};
|
|
95876
|
+
}
|
|
95877
|
+
if (status === 403) {
|
|
95878
|
+
return {
|
|
95879
|
+
state: "UNKNOWN",
|
|
95880
|
+
context_is_required: false,
|
|
95881
|
+
required_contexts: [],
|
|
95882
|
+
protection: null,
|
|
95883
|
+
observation_error: "403",
|
|
95884
|
+
permission_hint: "administration:read (or repo admin) required to read branch protection"
|
|
95885
|
+
};
|
|
95886
|
+
}
|
|
95887
|
+
if (status != null && status >= 400) {
|
|
95888
|
+
return {
|
|
95889
|
+
state: "UNKNOWN",
|
|
95890
|
+
context_is_required: false,
|
|
95891
|
+
required_contexts: [],
|
|
95892
|
+
protection: null,
|
|
95893
|
+
observation_error: String(status),
|
|
95894
|
+
permission_hint: read.errorMessage || `GitHub API returned HTTP ${status}`
|
|
95895
|
+
};
|
|
95896
|
+
}
|
|
95897
|
+
const protection = read && read.body && typeof read.body === "object" ? read.body : null;
|
|
95898
|
+
if (!protection) {
|
|
95899
|
+
return {
|
|
95900
|
+
state: "ABSENT",
|
|
95901
|
+
context_is_required: false,
|
|
95902
|
+
required_contexts: [],
|
|
95903
|
+
protection: null,
|
|
95904
|
+
observation_error: null
|
|
95905
|
+
};
|
|
95906
|
+
}
|
|
95907
|
+
const required_contexts = extractRequiredContexts(protection);
|
|
95908
|
+
const context_is_required = required_contexts.some((c) => c === contextName);
|
|
95909
|
+
if (context_is_required) {
|
|
95910
|
+
return {
|
|
95911
|
+
state: "REQUIRED",
|
|
95912
|
+
context_is_required: true,
|
|
95913
|
+
required_contexts,
|
|
95914
|
+
protection,
|
|
95915
|
+
observation_error: null
|
|
95916
|
+
};
|
|
95917
|
+
}
|
|
95918
|
+
return {
|
|
95919
|
+
state: "PRESENT_NOT_REQUIRED",
|
|
95920
|
+
context_is_required: false,
|
|
95921
|
+
required_contexts,
|
|
95922
|
+
protection,
|
|
95923
|
+
observation_error: null
|
|
95924
|
+
};
|
|
95925
|
+
}
|
|
95926
|
+
function buildProtectionUpdatePayload(protection, contextName = CHECK_NAME) {
|
|
95927
|
+
const p = protection && typeof protection === "object" ? protection : {};
|
|
95928
|
+
const prev = p.required_status_checks && typeof p.required_status_checks === "object" ? p.required_status_checks : {};
|
|
95929
|
+
const contexts = extractRequiredContexts(p);
|
|
95930
|
+
const nextContexts = contexts.includes(contextName) ? contexts.slice() : contexts.concat([contextName]);
|
|
95931
|
+
const body = {
|
|
95932
|
+
required_status_checks: {
|
|
95933
|
+
strict: prev.strict === true,
|
|
95934
|
+
contexts: nextContexts,
|
|
95935
|
+
// Prefer also sending checks[] when the API used that shape so app ids survive when present.
|
|
95936
|
+
...Array.isArray(prev.checks) && prev.checks.length ? {
|
|
95937
|
+
checks: nextContexts.map((ctx) => {
|
|
95938
|
+
const existing = prev.checks.find((c) => c && c.context === ctx);
|
|
95939
|
+
return existing && existing.app_id != null ? { context: ctx, app_id: existing.app_id } : { context: ctx };
|
|
95940
|
+
})
|
|
95941
|
+
} : {}
|
|
95942
|
+
},
|
|
95943
|
+
enforce_admins: !!(p.enforce_admins && p.enforce_admins.enabled),
|
|
95944
|
+
required_pull_request_reviews: p.required_pull_request_reviews ? serializePrReviews(p.required_pull_request_reviews) : null,
|
|
95945
|
+
restrictions: p.restrictions ? {
|
|
95946
|
+
users: (p.restrictions.users || []).map((u) => u.login || u).filter(Boolean),
|
|
95947
|
+
teams: (p.restrictions.teams || []).map((t) => t.slug || t).filter(Boolean),
|
|
95948
|
+
apps: (p.restrictions.apps || []).map((a) => a.slug || a).filter(Boolean)
|
|
95949
|
+
} : null
|
|
95950
|
+
};
|
|
95951
|
+
if (typeof p.required_linear_history === "boolean") {
|
|
95952
|
+
body.required_linear_history = p.required_linear_history;
|
|
95953
|
+
} else if (p.required_linear_history && typeof p.required_linear_history.enabled === "boolean") {
|
|
95954
|
+
body.required_linear_history = p.required_linear_history.enabled;
|
|
95955
|
+
}
|
|
95956
|
+
if (typeof p.allow_force_pushes === "boolean") {
|
|
95957
|
+
body.allow_force_pushes = p.allow_force_pushes;
|
|
95958
|
+
} else if (p.allow_force_pushes && typeof p.allow_force_pushes.enabled === "boolean") {
|
|
95959
|
+
body.allow_force_pushes = p.allow_force_pushes.enabled;
|
|
95960
|
+
}
|
|
95961
|
+
if (typeof p.allow_deletions === "boolean") {
|
|
95962
|
+
body.allow_deletions = p.allow_deletions;
|
|
95963
|
+
} else if (p.allow_deletions && typeof p.allow_deletions.enabled === "boolean") {
|
|
95964
|
+
body.allow_deletions = p.allow_deletions.enabled;
|
|
95965
|
+
}
|
|
95966
|
+
if (typeof p.block_creations === "boolean") {
|
|
95967
|
+
body.block_creations = p.block_creations;
|
|
95968
|
+
} else if (p.block_creations && typeof p.block_creations.enabled === "boolean") {
|
|
95969
|
+
body.block_creations = p.block_creations.enabled;
|
|
95970
|
+
}
|
|
95971
|
+
if (typeof p.required_conversation_resolution === "boolean") {
|
|
95972
|
+
body.required_conversation_resolution = p.required_conversation_resolution;
|
|
95973
|
+
} else if (p.required_conversation_resolution && typeof p.required_conversation_resolution.enabled === "boolean") {
|
|
95974
|
+
body.required_conversation_resolution = p.required_conversation_resolution.enabled;
|
|
95975
|
+
}
|
|
95976
|
+
return body;
|
|
95977
|
+
}
|
|
95978
|
+
function serializePrReviews(rpr) {
|
|
95979
|
+
if (!rpr || typeof rpr !== "object") return null;
|
|
95980
|
+
return {
|
|
95981
|
+
dismiss_stale_reviews: !!rpr.dismiss_stale_reviews,
|
|
95982
|
+
require_code_owner_reviews: !!rpr.require_code_owner_reviews,
|
|
95983
|
+
required_approving_review_count: Number(rpr.required_approving_review_count) || 0,
|
|
95984
|
+
require_last_push_approval: !!rpr.require_last_push_approval,
|
|
95985
|
+
...Array.isArray(rpr.bypass_pull_request_allowances?.users) ? {
|
|
95986
|
+
bypass_pull_request_allowances: {
|
|
95987
|
+
users: (rpr.bypass_pull_request_allowances.users || []).map((u) => u.login || u).filter(Boolean),
|
|
95988
|
+
teams: (rpr.bypass_pull_request_allowances.teams || []).map((t) => t.slug || t).filter(Boolean),
|
|
95989
|
+
apps: (rpr.bypass_pull_request_allowances.apps || []).map((a) => a.slug || a).filter(Boolean)
|
|
95990
|
+
}
|
|
95991
|
+
} : {}
|
|
95992
|
+
};
|
|
95993
|
+
}
|
|
95994
|
+
function detectRulesetRequiredCheck(rulesets, contextName = CHECK_NAME) {
|
|
95995
|
+
const names = [];
|
|
95996
|
+
const list = Array.isArray(rulesets) ? rulesets : [];
|
|
95997
|
+
for (const rs of list) {
|
|
95998
|
+
if (!rs || typeof rs !== "object") continue;
|
|
95999
|
+
const rules = Array.isArray(rs.rules) ? rs.rules : [];
|
|
96000
|
+
for (const rule of rules) {
|
|
96001
|
+
if (!rule || rule.type !== "required_status_checks") continue;
|
|
96002
|
+
const params = rule.parameters || {};
|
|
96003
|
+
const checks = Array.isArray(params.required_status_checks) ? params.required_status_checks : Array.isArray(params.contexts) ? params.contexts.map((c) => ({ context: c })) : [];
|
|
96004
|
+
for (const c of checks) {
|
|
96005
|
+
const ctx = typeof c === "string" ? c : c && c.context;
|
|
96006
|
+
if (ctx === contextName) {
|
|
96007
|
+
names.push(String(rs.name || rs.id || "ruleset"));
|
|
96008
|
+
}
|
|
96009
|
+
}
|
|
96010
|
+
}
|
|
96011
|
+
}
|
|
96012
|
+
return { enforced: names.length > 0, ruleset_names: [...new Set(names)] };
|
|
96013
|
+
}
|
|
96014
|
+
function defaultGit(args, cwd) {
|
|
96015
|
+
return execFileSync("git", args, {
|
|
96016
|
+
cwd: cwd || process.cwd(),
|
|
96017
|
+
encoding: "utf8",
|
|
96018
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
96019
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
96020
|
+
}).trim();
|
|
96021
|
+
}
|
|
96022
|
+
function parseGitHubRemote(remoteUrl) {
|
|
96023
|
+
const s = String(remoteUrl || "").trim();
|
|
96024
|
+
let m = s.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
96025
|
+
if (m) return { owner: m[1], repo: m[2].replace(/\.git$/i, "") };
|
|
96026
|
+
m = s.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
96027
|
+
if (m) return { owner: m[1], repo: m[2].replace(/\.git$/i, "") };
|
|
96028
|
+
m = s.match(/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
96029
|
+
if (m) return { owner: m[1], repo: m[2].replace(/\.git$/i, "") };
|
|
96030
|
+
return null;
|
|
96031
|
+
}
|
|
96032
|
+
function resolveOwnerRepo(cwd, gitImpl = defaultGit) {
|
|
96033
|
+
let url;
|
|
96034
|
+
try {
|
|
96035
|
+
url = gitImpl(["remote", "get-url", "origin"], cwd);
|
|
96036
|
+
} catch {
|
|
96037
|
+
try {
|
|
96038
|
+
url = gitImpl(["config", "--get", "remote.origin.url"], cwd);
|
|
96039
|
+
} catch {
|
|
96040
|
+
return null;
|
|
96041
|
+
}
|
|
96042
|
+
}
|
|
96043
|
+
return parseGitHubRemote(url);
|
|
96044
|
+
}
|
|
96045
|
+
function resolveDefaultBranch(cwd, gitImpl = defaultGit) {
|
|
96046
|
+
try {
|
|
96047
|
+
const ref = gitImpl(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
96048
|
+
const m = ref.match(/refs\/remotes\/origin\/(.+)$/);
|
|
96049
|
+
if (m) return m[1];
|
|
96050
|
+
} catch {
|
|
96051
|
+
}
|
|
96052
|
+
for (const b of ["main", "master"]) {
|
|
96053
|
+
try {
|
|
96054
|
+
gitImpl(["rev-parse", "--verify", `origin/${b}`], cwd);
|
|
96055
|
+
return b;
|
|
96056
|
+
} catch {
|
|
96057
|
+
}
|
|
96058
|
+
}
|
|
96059
|
+
try {
|
|
96060
|
+
return gitImpl(["branch", "--show-current"], cwd) || "main";
|
|
96061
|
+
} catch {
|
|
96062
|
+
return "main";
|
|
96063
|
+
}
|
|
96064
|
+
}
|
|
96065
|
+
function defaultGhAvailable() {
|
|
96066
|
+
try {
|
|
96067
|
+
execFileSync("gh", ["--version"], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
96068
|
+
return true;
|
|
96069
|
+
} catch {
|
|
96070
|
+
return false;
|
|
96071
|
+
}
|
|
96072
|
+
}
|
|
96073
|
+
function runGhApi(args, { cwd, ghRunner } = {}) {
|
|
96074
|
+
const runner = ghRunner || ((a, c) => execFileSync("gh", a, {
|
|
96075
|
+
cwd: c || process.cwd(),
|
|
96076
|
+
encoding: "utf8",
|
|
96077
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
96078
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
96079
|
+
}));
|
|
96080
|
+
try {
|
|
96081
|
+
const raw = runner(["api", ...args], cwd);
|
|
96082
|
+
let body = null;
|
|
96083
|
+
try {
|
|
96084
|
+
body = JSON.parse(raw);
|
|
96085
|
+
} catch {
|
|
96086
|
+
body = raw;
|
|
96087
|
+
}
|
|
96088
|
+
return { ok: true, status: 200, body, raw: String(raw) };
|
|
96089
|
+
} catch (err) {
|
|
96090
|
+
const stderr = err && err.stderr ? String(err.stderr) : "";
|
|
96091
|
+
const stdout = err && err.stdout ? String(err.stdout) : "";
|
|
96092
|
+
const msg = stderr || err && err.message || String(err);
|
|
96093
|
+
const m = msg.match(/HTTP\s+(\d{3})/i) || stdout.match(/"status"\s*:\s*"(\d{3})"/);
|
|
96094
|
+
const status = m ? Number(m[1]) : err && err.status === 1 ? null : null;
|
|
96095
|
+
let body = null;
|
|
96096
|
+
try {
|
|
96097
|
+
body = JSON.parse(stdout);
|
|
96098
|
+
} catch {
|
|
96099
|
+
}
|
|
96100
|
+
let httpStatus = status;
|
|
96101
|
+
if (httpStatus == null) {
|
|
96102
|
+
if (/403|Forbidden|Resource not accessible/i.test(msg)) httpStatus = 403;
|
|
96103
|
+
else if (/404|Not Found/i.test(msg)) httpStatus = 404;
|
|
96104
|
+
else httpStatus = 500;
|
|
96105
|
+
}
|
|
96106
|
+
return {
|
|
96107
|
+
ok: false,
|
|
96108
|
+
status: httpStatus,
|
|
96109
|
+
body,
|
|
96110
|
+
raw: stdout || msg,
|
|
96111
|
+
errorMessage: msg.slice(0, 400)
|
|
96112
|
+
};
|
|
96113
|
+
}
|
|
96114
|
+
}
|
|
96115
|
+
function protectionGetPath(owner, repo, branch) {
|
|
96116
|
+
return `repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}/protection`;
|
|
96117
|
+
}
|
|
96118
|
+
function protectionPutArgs(owner, repo, branch, payload) {
|
|
96119
|
+
return {
|
|
96120
|
+
args: [
|
|
96121
|
+
"--method",
|
|
96122
|
+
"PUT",
|
|
96123
|
+
protectionGetPath(owner, repo, branch),
|
|
96124
|
+
"--input",
|
|
96125
|
+
"-"
|
|
96126
|
+
],
|
|
96127
|
+
input: JSON.stringify(payload)
|
|
96128
|
+
};
|
|
96129
|
+
}
|
|
96130
|
+
function printApplyCommand(owner, repo, branch, payload) {
|
|
96131
|
+
const path = protectionGetPath(owner, repo, branch);
|
|
96132
|
+
const json = JSON.stringify(payload, null, 2);
|
|
96133
|
+
return [
|
|
96134
|
+
`# Read-modify-write: add '${CHECK_NAME}' to required status checks (preserves other settings)`,
|
|
96135
|
+
`gh api --method PUT ${path} --input - <<'EOF'`,
|
|
96136
|
+
json,
|
|
96137
|
+
"EOF"
|
|
96138
|
+
].join("\n");
|
|
96139
|
+
}
|
|
96140
|
+
async function runSetupRequiredCheck(options = {}, deps = {}) {
|
|
96141
|
+
const cwd = deps.cwd || process.cwd();
|
|
96142
|
+
const gitImpl = deps.gitImpl || defaultGit;
|
|
96143
|
+
const log = deps.log || console.log.bind(console);
|
|
96144
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
96145
|
+
const ghAvailable = deps.ghAvailable != null ? deps.ghAvailable : defaultGhAvailable;
|
|
96146
|
+
const ghRunner = deps.ghRunner;
|
|
96147
|
+
const runApi = deps.runGhApi || ((args, o) => runGhApi(args, { ...o, ghRunner }));
|
|
96148
|
+
const doExit = deps.exit !== false;
|
|
96149
|
+
const finish = (code, payload2) => {
|
|
96150
|
+
if (options.json) log(JSON.stringify(payload2, null, 2));
|
|
96151
|
+
if (doExit) process.exit(code);
|
|
96152
|
+
return { exitCode: code, ...payload2 };
|
|
96153
|
+
};
|
|
96154
|
+
const ownerRepo = options.repo ? (() => {
|
|
96155
|
+
const m = String(options.repo).match(/^([^/]+)\/([^/]+)$/);
|
|
96156
|
+
return m ? { owner: m[1], repo: m[2] } : null;
|
|
96157
|
+
})() : resolveOwnerRepo(cwd, gitImpl);
|
|
96158
|
+
if (!ownerRepo) {
|
|
96159
|
+
logErr(chalk.red("Could not resolve owner/repo from git remote origin (or --repo OWNER/REPO)."));
|
|
96160
|
+
return finish(EXIT.ERROR, { ok: false, code: "REPO_UNRESOLVED" });
|
|
96161
|
+
}
|
|
96162
|
+
const { owner, repo } = ownerRepo;
|
|
96163
|
+
const branch = options.branch || resolveDefaultBranch(cwd, gitImpl);
|
|
96164
|
+
if (!ghAvailable()) {
|
|
96165
|
+
logErr(chalk.yellow("GitHub CLI (`gh`) not found on PATH."));
|
|
96166
|
+
logErr("Install: https://cli.github.com/ then: gh auth login");
|
|
96167
|
+
logErr("");
|
|
96168
|
+
logErr("Manual check (your credentials, not a CodeRifts key):");
|
|
96169
|
+
logErr(` gh api ${protectionGetPath(owner, repo, branch)}`);
|
|
96170
|
+
logErr("");
|
|
96171
|
+
logErr(`Required check name: ${CHECK_NAME}`);
|
|
96172
|
+
logErr("The CodeRifts App never writes branch protection (needs administration:write).");
|
|
96173
|
+
return finish(EXIT.ERROR, { ok: false, code: "GH_MISSING", owner, repo, branch, check: CHECK_NAME });
|
|
96174
|
+
}
|
|
96175
|
+
const getPath = protectionGetPath(owner, repo, branch);
|
|
96176
|
+
const read = runApi([getPath], { cwd });
|
|
96177
|
+
const obs = classifyObservation(read, CHECK_NAME);
|
|
96178
|
+
let ruleset = { enforced: false, ruleset_names: [] };
|
|
96179
|
+
try {
|
|
96180
|
+
const rsList = runApi([`repos/${owner}/${repo}/rulesets`], { cwd });
|
|
96181
|
+
if (rsList.ok && Array.isArray(rsList.body)) {
|
|
96182
|
+
const detailed = [];
|
|
96183
|
+
for (const rs of rsList.body) {
|
|
96184
|
+
if (!rs || rs.id == null) continue;
|
|
96185
|
+
const one = runApi([`repos/${owner}/${repo}/rulesets/${rs.id}`], { cwd });
|
|
96186
|
+
if (one.ok && one.body) detailed.push(one.body);
|
|
96187
|
+
else detailed.push(rs);
|
|
96188
|
+
}
|
|
96189
|
+
ruleset = detectRulesetRequiredCheck(detailed.length ? detailed : rsList.body, CHECK_NAME);
|
|
96190
|
+
}
|
|
96191
|
+
} catch {
|
|
96192
|
+
}
|
|
96193
|
+
const basePayload = {
|
|
96194
|
+
ok: true,
|
|
96195
|
+
owner,
|
|
96196
|
+
repo,
|
|
96197
|
+
branch,
|
|
96198
|
+
check: CHECK_NAME,
|
|
96199
|
+
classic: obs.state,
|
|
96200
|
+
context_is_required: obs.context_is_required,
|
|
96201
|
+
required_contexts: obs.required_contexts,
|
|
96202
|
+
ruleset_enforced: ruleset.enforced,
|
|
96203
|
+
ruleset_names: ruleset.ruleset_names
|
|
96204
|
+
};
|
|
96205
|
+
if (ruleset.enforced) {
|
|
96206
|
+
if (!options.json) {
|
|
96207
|
+
log(chalk.green(`Required via repository ruleset: ${CHECK_NAME}`));
|
|
96208
|
+
log(` repo: ${owner}/${repo}`);
|
|
96209
|
+
log(` branch: ${branch}`);
|
|
96210
|
+
log(` rulesets: ${ruleset.ruleset_names.join(", ") || "(named)"}`);
|
|
96211
|
+
log(" Classic branch protection may still be ABSENT \u2014 rulesets enforce separately.");
|
|
96212
|
+
}
|
|
96213
|
+
return finish(EXIT.OK, { ...basePayload, code: "RULESET_REQUIRED" });
|
|
96214
|
+
}
|
|
96215
|
+
if (obs.state === "REQUIRED") {
|
|
96216
|
+
if (!options.json) {
|
|
96217
|
+
log(chalk.green(`Already required: '${CHECK_NAME}'`));
|
|
96218
|
+
log(` repo: ${owner}/${repo}`);
|
|
96219
|
+
log(` branch: ${branch}`);
|
|
96220
|
+
log(" (classic branch protection \u2014 idempotent; nothing to do)");
|
|
96221
|
+
}
|
|
96222
|
+
return finish(EXIT.OK, { ...basePayload, code: "ALREADY_REQUIRED" });
|
|
96223
|
+
}
|
|
96224
|
+
if (obs.state === "UNKNOWN") {
|
|
96225
|
+
if (!options.json) {
|
|
96226
|
+
logErr(chalk.red("Cannot observe branch protection (UNKNOWN)."));
|
|
96227
|
+
logErr(` HTTP: ${obs.observation_error || "unknown"}`);
|
|
96228
|
+
logErr(` ${obs.permission_hint || "An admin with administration:read must run this command."}`);
|
|
96229
|
+
logErr(" The CodeRifts App never writes protection; an admin must grant or run this.");
|
|
96230
|
+
}
|
|
96231
|
+
return finish(EXIT.PERMISSION, { ...basePayload, ok: false, code: "PERMISSION", observation_error: obs.observation_error });
|
|
96232
|
+
}
|
|
96233
|
+
let payload;
|
|
96234
|
+
if (obs.state === "ABSENT" || !obs.protection) {
|
|
96235
|
+
payload = {
|
|
96236
|
+
required_status_checks: {
|
|
96237
|
+
strict: false,
|
|
96238
|
+
contexts: [CHECK_NAME]
|
|
96239
|
+
},
|
|
96240
|
+
enforce_admins: false,
|
|
96241
|
+
required_pull_request_reviews: null,
|
|
96242
|
+
restrictions: null
|
|
96243
|
+
};
|
|
96244
|
+
} else {
|
|
96245
|
+
payload = buildProtectionUpdatePayload(obs.protection, CHECK_NAME);
|
|
96246
|
+
}
|
|
96247
|
+
const cmdText = printApplyCommand(owner, repo, branch, payload);
|
|
96248
|
+
if (!options.apply) {
|
|
96249
|
+
if (!options.json) {
|
|
96250
|
+
log(chalk.yellow(`Status: ${obs.state} \u2014 '${CHECK_NAME}' is not a required check.`));
|
|
96251
|
+
log(` repo: ${owner}/${repo}`);
|
|
96252
|
+
log(` branch: ${branch}`);
|
|
96253
|
+
log("");
|
|
96254
|
+
log("Dry-run (default). Exact command to add the required check with your credentials:");
|
|
96255
|
+
log("");
|
|
96256
|
+
log(cmdText);
|
|
96257
|
+
log("");
|
|
96258
|
+
log("Re-run with --apply to execute that PUT and re-verify.");
|
|
96259
|
+
log("Why the App never writes this: administration:write is a trust jump we refuse (audit 3.4/1, 3.5).");
|
|
96260
|
+
}
|
|
96261
|
+
return finish(EXIT.NEEDS_APPLY, {
|
|
96262
|
+
...basePayload,
|
|
96263
|
+
code: "NEEDS_APPLY",
|
|
96264
|
+
apply_command: cmdText,
|
|
96265
|
+
apply_payload: payload
|
|
96266
|
+
});
|
|
96267
|
+
}
|
|
96268
|
+
const put = protectionPutArgs(owner, repo, branch, payload);
|
|
96269
|
+
let putResult;
|
|
96270
|
+
if (deps.ghApply) {
|
|
96271
|
+
putResult = deps.ghApply(put, { cwd });
|
|
96272
|
+
} else {
|
|
96273
|
+
try {
|
|
96274
|
+
const raw = execFileSync("gh", ["api", ...put.args], {
|
|
96275
|
+
cwd,
|
|
96276
|
+
encoding: "utf8",
|
|
96277
|
+
input: put.input,
|
|
96278
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
96279
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
96280
|
+
});
|
|
96281
|
+
putResult = { ok: true, status: 200, body: JSON.parse(raw || "{}"), raw };
|
|
96282
|
+
} catch (err) {
|
|
96283
|
+
const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
|
|
96284
|
+
const m = msg.match(/HTTP\s+(\d{3})/i);
|
|
96285
|
+
putResult = {
|
|
96286
|
+
ok: false,
|
|
96287
|
+
status: m ? Number(m[1]) : 500,
|
|
96288
|
+
errorMessage: msg.slice(0, 400),
|
|
96289
|
+
raw: msg
|
|
96290
|
+
};
|
|
96291
|
+
}
|
|
96292
|
+
}
|
|
96293
|
+
if (!putResult.ok) {
|
|
96294
|
+
if (!options.json) {
|
|
96295
|
+
logErr(chalk.red("Apply failed."));
|
|
96296
|
+
logErr(` ${putResult.errorMessage || putResult.status}`);
|
|
96297
|
+
if (putResult.status === 403) {
|
|
96298
|
+
logErr(" Need administration:write on the repository (admin).");
|
|
96299
|
+
}
|
|
96300
|
+
}
|
|
96301
|
+
return finish(
|
|
96302
|
+
putResult.status === 403 ? EXIT.PERMISSION : EXIT.ERROR,
|
|
96303
|
+
{ ...basePayload, ok: false, code: "APPLY_FAILED", apply_status: putResult.status }
|
|
96304
|
+
);
|
|
96305
|
+
}
|
|
96306
|
+
const reRead = runApi([getPath], { cwd });
|
|
96307
|
+
const reObs = classifyObservation(reRead, CHECK_NAME);
|
|
96308
|
+
if (reObs.state !== "REQUIRED") {
|
|
96309
|
+
if (!options.json) {
|
|
96310
|
+
logErr(chalk.red("Apply returned success but re-read did not show the check as required."));
|
|
96311
|
+
logErr(` classic state after apply: ${reObs.state}`);
|
|
96312
|
+
logErr(" Unverified apply is not success.");
|
|
96313
|
+
}
|
|
96314
|
+
return finish(EXIT.ERROR, {
|
|
96315
|
+
...basePayload,
|
|
96316
|
+
ok: false,
|
|
96317
|
+
code: "VERIFY_FAILED",
|
|
96318
|
+
classic_after: reObs.state
|
|
96319
|
+
});
|
|
96320
|
+
}
|
|
96321
|
+
if (!options.json) {
|
|
96322
|
+
log(chalk.green(`Success: '${CHECK_NAME}' is now required.`));
|
|
96323
|
+
log(` repo: ${owner}/${repo}`);
|
|
96324
|
+
log(` branch: ${branch}`);
|
|
96325
|
+
}
|
|
96326
|
+
return finish(EXIT.OK, {
|
|
96327
|
+
...basePayload,
|
|
96328
|
+
code: "APPLIED",
|
|
96329
|
+
classic: "REQUIRED",
|
|
96330
|
+
context_is_required: true
|
|
96331
|
+
});
|
|
96332
|
+
}
|
|
96333
|
+
module2.exports = {
|
|
96334
|
+
runSetupRequiredCheck,
|
|
96335
|
+
CHECK_NAME,
|
|
96336
|
+
EXIT,
|
|
96337
|
+
extractRequiredContexts,
|
|
96338
|
+
classifyObservation,
|
|
96339
|
+
buildProtectionUpdatePayload,
|
|
96340
|
+
detectRulesetRequiredCheck,
|
|
96341
|
+
parseGitHubRemote,
|
|
96342
|
+
resolveOwnerRepo,
|
|
96343
|
+
resolveDefaultBranch,
|
|
96344
|
+
printApplyCommand,
|
|
96345
|
+
protectionGetPath,
|
|
96346
|
+
protectionPutArgs,
|
|
96347
|
+
runGhApi
|
|
96348
|
+
};
|
|
96349
|
+
}
|
|
96350
|
+
});
|
|
96351
|
+
|
|
96352
|
+
// src/commands/status.js
|
|
96353
|
+
var require_status = __commonJS({
|
|
96354
|
+
"src/commands/status.js"(exports2, module2) {
|
|
96355
|
+
"use strict";
|
|
96356
|
+
var chalk = require_source();
|
|
96357
|
+
var { getApiKey } = require_config();
|
|
96358
|
+
var { cloudGetEnforcementStatus } = require_cloud();
|
|
96359
|
+
var { renderJson } = require_json2();
|
|
96360
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
96361
|
+
var USAGE = [
|
|
96362
|
+
"Usage: coderifts status --repo owner/repo",
|
|
96363
|
+
" or: coderifts status owner/repo",
|
|
96364
|
+
"",
|
|
96365
|
+
"Read-only: prints the cross-layer enforcement report from the CodeRifts API.",
|
|
96366
|
+
"Requires a cloud API key (coderifts login or CODERIFTS_API_KEY)."
|
|
96367
|
+
].join("\n");
|
|
96368
|
+
function isValidRepo(full) {
|
|
96369
|
+
const parts = String(full || "").split("/");
|
|
96370
|
+
if (parts.length !== 2) return false;
|
|
96371
|
+
const [owner, repo] = parts.map((p) => p.trim());
|
|
96372
|
+
return !!(owner && repo);
|
|
96373
|
+
}
|
|
96374
|
+
function colorStatus(status) {
|
|
96375
|
+
const s = String(status == null ? "" : status);
|
|
96376
|
+
const upper = s.toUpperCase();
|
|
96377
|
+
if (upper === "ENFORCING") return chalk.green(s);
|
|
96378
|
+
if (upper === "ADVISORY" || s === "declared_required" || s === "declared_optional") {
|
|
96379
|
+
return chalk.yellow(s);
|
|
96380
|
+
}
|
|
96381
|
+
if (upper === "ABSENT" || s === "no_config" || s === "not_observable_from_server") {
|
|
96382
|
+
return chalk.dim(s);
|
|
96383
|
+
}
|
|
96384
|
+
if (upper === "UNKNOWN" || s === "unknown") return chalk.red(s);
|
|
96385
|
+
if (upper === "PARTIAL") return chalk.yellow(s);
|
|
96386
|
+
return chalk.white(s);
|
|
96387
|
+
}
|
|
96388
|
+
function residualsFromReport(report) {
|
|
96389
|
+
const residuals = [];
|
|
96390
|
+
const legs = report && report.legs || {};
|
|
96391
|
+
const merge = legs.merge;
|
|
96392
|
+
if (merge) {
|
|
96393
|
+
if (merge.enforcing !== true) {
|
|
96394
|
+
residuals.push(`merge:${merge.status || "UNKNOWN"}`);
|
|
96395
|
+
}
|
|
96396
|
+
} else {
|
|
96397
|
+
residuals.push("merge:missing");
|
|
96398
|
+
}
|
|
96399
|
+
const deploy = legs.deploy;
|
|
96400
|
+
if (deploy) {
|
|
96401
|
+
residuals.push(`deploy:${deploy.status || "unknown"}`);
|
|
96402
|
+
} else {
|
|
96403
|
+
residuals.push("deploy:missing");
|
|
96404
|
+
}
|
|
96405
|
+
const runtime = legs.runtime;
|
|
96406
|
+
if (runtime) {
|
|
96407
|
+
residuals.push(`runtime:${runtime.status || "not_observable_from_server"}`);
|
|
96408
|
+
}
|
|
96409
|
+
const content = legs.content;
|
|
96410
|
+
if (content) {
|
|
96411
|
+
residuals.push(`content:${content.status || "not_observable_from_server"}`);
|
|
96412
|
+
}
|
|
96413
|
+
return residuals;
|
|
96414
|
+
}
|
|
96415
|
+
function renderStatusReport(report) {
|
|
96416
|
+
const lines = [];
|
|
96417
|
+
const repo = report && report.repo || "(unknown repo)";
|
|
96418
|
+
const legs = report && report.legs || {};
|
|
96419
|
+
const summary = report && report.summary || {};
|
|
96420
|
+
lines.push(chalk.bold(`CodeRifts enforcement \u2014 ${repo}`));
|
|
96421
|
+
if (report && report.timestamp) {
|
|
96422
|
+
lines.push(chalk.dim(` as of ${report.timestamp}`));
|
|
96423
|
+
}
|
|
96424
|
+
lines.push("");
|
|
96425
|
+
const order = [
|
|
96426
|
+
["Runtime", legs.runtime],
|
|
96427
|
+
["Merge", legs.merge],
|
|
96428
|
+
["Deploy", legs.deploy],
|
|
96429
|
+
["Content", legs.content]
|
|
96430
|
+
];
|
|
96431
|
+
for (const [label, leg] of order) {
|
|
96432
|
+
if (!leg) {
|
|
96433
|
+
lines.push(` ${label.padEnd(10)} ${chalk.dim("\u2014")}`);
|
|
96434
|
+
continue;
|
|
96435
|
+
}
|
|
96436
|
+
const statusStr = colorStatus(leg.status);
|
|
96437
|
+
const enforcing = leg.enforcing === true ? chalk.green("enforcing=true") : chalk.dim("enforcing=false");
|
|
96438
|
+
const epi = leg.epistemic_status ? chalk.dim(` [${leg.epistemic_status}]`) : "";
|
|
96439
|
+
lines.push(` ${label.padEnd(10)} ${statusStr} ${enforcing}${epi}`);
|
|
96440
|
+
if (leg.note) {
|
|
96441
|
+
lines.push(chalk.dim(` ${leg.note.slice(0, 100)}${leg.note.length > 100 ? "\u2026" : ""}`));
|
|
96442
|
+
}
|
|
96443
|
+
if (leg.epistemic_note && leg.leg === "deploy") {
|
|
96444
|
+
lines.push(chalk.dim(` ${leg.epistemic_note.slice(0, 100)}\u2026`));
|
|
96445
|
+
}
|
|
96446
|
+
}
|
|
96447
|
+
const residuals = residualsFromReport(report);
|
|
96448
|
+
lines.push("");
|
|
96449
|
+
lines.push(` Residuals: ${residuals.length ? residuals.map((r) => chalk.yellow(r)).join(", ") : chalk.green("(none \u2014 merge enforcing; other legs still not server-observable)")}`);
|
|
96450
|
+
if (summary.status) {
|
|
96451
|
+
lines.push("");
|
|
96452
|
+
lines.push(chalk.dim(` summary.status: ${summary.status}`));
|
|
96453
|
+
if (summary.can_claim_fully_enforced === false) {
|
|
96454
|
+
lines.push(chalk.dim(" can_claim_fully_enforced: false (server ceiling)"));
|
|
96455
|
+
}
|
|
96456
|
+
}
|
|
96457
|
+
lines.push("");
|
|
96458
|
+
lines.push(chalk.dim(" Read-only report. To close gaps:"));
|
|
96459
|
+
lines.push(chalk.dim(" merge \u2192 coderifts setup-required-check"));
|
|
96460
|
+
lines.push(chalk.dim(" deploy \u2192 coderifts deploy-gate --enforce (CD step)"));
|
|
96461
|
+
lines.push(chalk.dim(" runtime/content \u2192 wire @coderifts/agent-guard in the agent host"));
|
|
96462
|
+
return lines.join("\n");
|
|
96463
|
+
}
|
|
96464
|
+
async function runStatus(options = {}, deps = {}) {
|
|
96465
|
+
const getKey = deps.getApiKey || getApiKey;
|
|
96466
|
+
const fetchStatus = deps.cloudGetEnforcementStatus || cloudGetEnforcementStatus;
|
|
96467
|
+
const log = deps.log || console.log;
|
|
96468
|
+
const errLog = deps.errLog || console.error;
|
|
96469
|
+
const repo = options.repo != null && String(options.repo).trim() ? String(options.repo).trim() : null;
|
|
96470
|
+
if (!repo) {
|
|
96471
|
+
errLog(chalk.red("Error: missing repo"));
|
|
96472
|
+
errLog(USAGE);
|
|
96473
|
+
return { exitCode: 1, error: "missing_repo" };
|
|
96474
|
+
}
|
|
96475
|
+
if (!isValidRepo(repo)) {
|
|
96476
|
+
errLog(chalk.red("Error: repo must be in owner/repo form (e.g. coderifts/app)"));
|
|
96477
|
+
return { exitCode: 1, error: "invalid_repo" };
|
|
96478
|
+
}
|
|
96479
|
+
const apiKey = getKey();
|
|
96480
|
+
if (!apiKey) {
|
|
96481
|
+
errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
|
|
96482
|
+
return { exitCode: 1, error: "missing_api_key" };
|
|
96483
|
+
}
|
|
96484
|
+
let report;
|
|
96485
|
+
try {
|
|
96486
|
+
report = await fetchStatus(repo, apiKey);
|
|
96487
|
+
} catch (e) {
|
|
96488
|
+
const msg = e && e.message ? String(e.message) : "request failed";
|
|
96489
|
+
errLog(chalk.red(`Error: ${msg}`));
|
|
96490
|
+
if (e && e.code) errLog(chalk.dim(` (${e.code})`));
|
|
96491
|
+
return { exitCode: 1, error: msg };
|
|
96492
|
+
}
|
|
96493
|
+
if (options.json) {
|
|
96494
|
+
log(renderJson(report));
|
|
96495
|
+
} else {
|
|
96496
|
+
log(renderStatusReport(report));
|
|
96497
|
+
}
|
|
96498
|
+
return { exitCode: 0, report };
|
|
96499
|
+
}
|
|
96500
|
+
module2.exports = {
|
|
96501
|
+
runStatus,
|
|
96502
|
+
renderStatusReport,
|
|
96503
|
+
residualsFromReport,
|
|
96504
|
+
colorStatus,
|
|
96505
|
+
isValidRepo,
|
|
96506
|
+
USAGE
|
|
96507
|
+
};
|
|
96508
|
+
}
|
|
96509
|
+
});
|
|
96510
|
+
|
|
95191
96511
|
// src/commands/hook.js
|
|
95192
96512
|
var require_hook = __commonJS({
|
|
95193
96513
|
"src/commands/hook.js"(exports2, module2) {
|
|
@@ -95425,6 +96745,8 @@ exit 0
|
|
|
95425
96745
|
console.log("After upgrading the coderifts CLI, re-run this command so the installed");
|
|
95426
96746
|
console.log("hook matches the package (already-installed hooks are not auto-updated):");
|
|
95427
96747
|
console.log(" coderifts hook install");
|
|
96748
|
+
console.log("");
|
|
96749
|
+
console.log("Agent-using repo? Run: coderifts agent-setup");
|
|
95428
96750
|
}
|
|
95429
96751
|
function uninstall() {
|
|
95430
96752
|
const gitDir = findGitDir();
|
|
@@ -95481,6 +96803,744 @@ exit 0
|
|
|
95481
96803
|
}
|
|
95482
96804
|
});
|
|
95483
96805
|
|
|
96806
|
+
// src/commands/enforce.js
|
|
96807
|
+
var require_enforce = __commonJS({
|
|
96808
|
+
"src/commands/enforce.js"(exports2, module2) {
|
|
96809
|
+
"use strict";
|
|
96810
|
+
var chalk = require_source();
|
|
96811
|
+
var { getApiKey } = require_config();
|
|
96812
|
+
var { cloudGetEnforcementStatus } = require_cloud();
|
|
96813
|
+
var {
|
|
96814
|
+
renderStatusReport,
|
|
96815
|
+
isValidRepo
|
|
96816
|
+
} = require_status();
|
|
96817
|
+
var { runSetupRequiredCheck } = require_setup_required_check();
|
|
96818
|
+
var path = require("path");
|
|
96819
|
+
var hookMod = require_hook();
|
|
96820
|
+
var hookInstall = hookMod.install;
|
|
96821
|
+
var isCodeRiftsHook = hookMod.isCodeRiftsHook;
|
|
96822
|
+
var findGitDir = hookMod.findGitDir;
|
|
96823
|
+
var getHookPath = typeof hookMod.getHookPath === "function" ? hookMod.getHookPath : (gitDir) => path.join(gitDir, "hooks", "pre-push");
|
|
96824
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
96825
|
+
var USAGE = [
|
|
96826
|
+
"Usage: coderifts enforce --repo owner/repo [--apply]",
|
|
96827
|
+
" or: coderifts enforce owner/repo [--apply]",
|
|
96828
|
+
"",
|
|
96829
|
+
"Cross-layer orchestrator: reads enforcement-status, then closes gaps by calling existing",
|
|
96830
|
+
"setup commands (setup-required-check, hook install, deploy-gate guidance).",
|
|
96831
|
+
"",
|
|
96832
|
+
"DRY-RUN BY DEFAULT \u2014 without --apply, prints what WOULD run and mutates NOTHING.",
|
|
96833
|
+
"With --apply, threads apply into each underlying command (their own safety still applies).",
|
|
96834
|
+
"",
|
|
96835
|
+
"Requires a cloud API key for the status read (coderifts login / CODERIFTS_API_KEY).",
|
|
96836
|
+
"Merge apply uses your local `gh` credentials, not the CodeRifts API key."
|
|
96837
|
+
].join("\n");
|
|
96838
|
+
var AGENT_GUARD_GUIDANCE = [
|
|
96839
|
+
"Runtime is not_observable_from_server \u2014 wire @coderifts/agent-guard in the agent host:",
|
|
96840
|
+
" - wrap mutating tools with guardToolCall / withCodeRifts",
|
|
96841
|
+
" - see: https://coderifts.com/docs (agent-guard) and `coderifts agent-setup`",
|
|
96842
|
+
" Local pre-push hook is complementary (git path), not a substitute for agent-guard."
|
|
96843
|
+
].join("\n");
|
|
96844
|
+
var DEPLOY_GUIDANCE = [
|
|
96845
|
+
"Deploy is declared-only on the server (never server-observed ENFORCING).",
|
|
96846
|
+
"Add a CD step that runs: coderifts deploy-gate --env <env> --artifact <id> --receipt <file> --enforce",
|
|
96847
|
+
" (phase-1 default is advisory; --enforce attests ENFORCING for the pipeline).",
|
|
96848
|
+
"Also set policy.require_source_binding: true in .coderifts.yml for the deploy declaration leg."
|
|
96849
|
+
].join("\n");
|
|
96850
|
+
var CONTENT_GUIDANCE = [
|
|
96851
|
+
"Content/freshness is not_observable_from_server (registry path at resolve time).",
|
|
96852
|
+
"No server-side enforce action is available \u2014 configure registry/freshness in the agent host."
|
|
96853
|
+
].join("\n");
|
|
96854
|
+
function isMergeEnforcing(leg) {
|
|
96855
|
+
return !!(leg && leg.enforcing === true && String(leg.status).toUpperCase() === "ENFORCING");
|
|
96856
|
+
}
|
|
96857
|
+
function runHookInstall(installFn) {
|
|
96858
|
+
const prev = process.exitCode;
|
|
96859
|
+
process.exitCode = 0;
|
|
96860
|
+
try {
|
|
96861
|
+
installFn();
|
|
96862
|
+
const code = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
96863
|
+
return {
|
|
96864
|
+
ok: code === 0,
|
|
96865
|
+
detail: code === 0 ? "hook.install completed" : `hook.install set exitCode=${code}`
|
|
96866
|
+
};
|
|
96867
|
+
} catch (e) {
|
|
96868
|
+
return { ok: false, detail: e && e.message || String(e) };
|
|
96869
|
+
} finally {
|
|
96870
|
+
process.exitCode = prev;
|
|
96871
|
+
}
|
|
96872
|
+
}
|
|
96873
|
+
async function runEnforce(options = {}, deps = {}) {
|
|
96874
|
+
const apply = options.apply === true;
|
|
96875
|
+
const getKey = deps.getApiKey || getApiKey;
|
|
96876
|
+
const fetchStatus = deps.cloudGetEnforcementStatus || cloudGetEnforcementStatus;
|
|
96877
|
+
const setupCheck = deps.runSetupRequiredCheck || runSetupRequiredCheck;
|
|
96878
|
+
const installHook = deps.hookInstall || hookInstall;
|
|
96879
|
+
const isHookInstalled = deps.isCodeRiftsHook || isCodeRiftsHook;
|
|
96880
|
+
const findGit = deps.findGitDir || findGitDir;
|
|
96881
|
+
const hookPathOf = deps.getHookPath || getHookPath;
|
|
96882
|
+
const log = deps.log || console.log.bind(console);
|
|
96883
|
+
const errLog = deps.errLog || console.error.bind(console);
|
|
96884
|
+
const repo = options.repo != null && String(options.repo).trim() ? String(options.repo).trim() : null;
|
|
96885
|
+
if (!repo) {
|
|
96886
|
+
errLog(chalk.red("Error: missing repo"));
|
|
96887
|
+
errLog(USAGE);
|
|
96888
|
+
return { exitCode: 1, error: "missing_repo", outcomes: [] };
|
|
96889
|
+
}
|
|
96890
|
+
if (!isValidRepo(repo)) {
|
|
96891
|
+
errLog(chalk.red("Error: repo must be in owner/repo form (e.g. coderifts/app)"));
|
|
96892
|
+
return { exitCode: 1, error: "invalid_repo", outcomes: [] };
|
|
96893
|
+
}
|
|
96894
|
+
const apiKey = getKey();
|
|
96895
|
+
if (!apiKey) {
|
|
96896
|
+
errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
|
|
96897
|
+
return { exitCode: 1, error: "missing_api_key", outcomes: [] };
|
|
96898
|
+
}
|
|
96899
|
+
log(chalk.bold(`CodeRifts enforce \u2014 ${repo}`));
|
|
96900
|
+
log(chalk.dim(apply ? " Mode: --apply (will mutate via underlying commands where applicable)" : " Mode: dry-run (default) \u2014 no mutations; showing what WOULD run"));
|
|
96901
|
+
log("");
|
|
96902
|
+
let report;
|
|
96903
|
+
try {
|
|
96904
|
+
report = await fetchStatus(repo, apiKey);
|
|
96905
|
+
} catch (e) {
|
|
96906
|
+
const msg = e && e.message ? String(e.message) : "status request failed";
|
|
96907
|
+
errLog(chalk.red(`Error: ${msg}`));
|
|
96908
|
+
return { exitCode: 1, error: msg, outcomes: [] };
|
|
96909
|
+
}
|
|
96910
|
+
const legs = report && report.legs || {};
|
|
96911
|
+
const outcomes = [];
|
|
96912
|
+
if (isMergeEnforcing(legs.merge)) {
|
|
96913
|
+
outcomes.push({
|
|
96914
|
+
layer: "merge",
|
|
96915
|
+
action: "skip",
|
|
96916
|
+
apply,
|
|
96917
|
+
ok: true,
|
|
96918
|
+
detail: `already ENFORCING (status=${legs.merge.status})`
|
|
96919
|
+
});
|
|
96920
|
+
log(chalk.green(" Merge skip \u2014 already ENFORCING"));
|
|
96921
|
+
} else {
|
|
96922
|
+
const mergeStatus = legs.merge && legs.merge.status || "UNKNOWN";
|
|
96923
|
+
log(chalk.yellow(` Merge ${apply ? "apply" : "dry-run"} \u2014 setup-required-check (current: ${mergeStatus})`));
|
|
96924
|
+
try {
|
|
96925
|
+
const r = await setupCheck(
|
|
96926
|
+
{ repo, apply: apply === true },
|
|
96927
|
+
{
|
|
96928
|
+
exit: false,
|
|
96929
|
+
log: deps.setupLog || log,
|
|
96930
|
+
logErr: deps.setupLogErr || errLog,
|
|
96931
|
+
...deps.setupDeps || {}
|
|
96932
|
+
}
|
|
96933
|
+
);
|
|
96934
|
+
const code = r && typeof r.exitCode === "number" ? r.exitCode : 1;
|
|
96935
|
+
outcomes.push({
|
|
96936
|
+
layer: "merge",
|
|
96937
|
+
action: "setup-required-check",
|
|
96938
|
+
apply,
|
|
96939
|
+
ok: code === 0,
|
|
96940
|
+
exitCode: code,
|
|
96941
|
+
detail: r && r.code ? String(r.code) : `exit ${code}`
|
|
96942
|
+
});
|
|
96943
|
+
if (code !== 0) {
|
|
96944
|
+
errLog(chalk.red(` Merge failed (exit ${code}${r && r.code ? `, ${r.code}` : ""})`));
|
|
96945
|
+
}
|
|
96946
|
+
} catch (e) {
|
|
96947
|
+
outcomes.push({
|
|
96948
|
+
layer: "merge",
|
|
96949
|
+
action: "setup-required-check",
|
|
96950
|
+
apply,
|
|
96951
|
+
ok: false,
|
|
96952
|
+
detail: e && e.message || String(e)
|
|
96953
|
+
});
|
|
96954
|
+
errLog(chalk.red(` Merge error: ${e && e.message || e}`));
|
|
96955
|
+
}
|
|
96956
|
+
}
|
|
96957
|
+
{
|
|
96958
|
+
const gitDir = findGit && findGit();
|
|
96959
|
+
let alreadyHook = false;
|
|
96960
|
+
try {
|
|
96961
|
+
if (gitDir) {
|
|
96962
|
+
const hp = hookPathOf(gitDir);
|
|
96963
|
+
alreadyHook = !!(isHookInstalled && isHookInstalled(hp));
|
|
96964
|
+
}
|
|
96965
|
+
} catch {
|
|
96966
|
+
}
|
|
96967
|
+
if (alreadyHook && apply) {
|
|
96968
|
+
outcomes.push({
|
|
96969
|
+
layer: "runtime",
|
|
96970
|
+
action: "hook.install",
|
|
96971
|
+
apply: true,
|
|
96972
|
+
ok: true,
|
|
96973
|
+
detail: "hook already installed (idempotent skip); not server-observable ENFORCING",
|
|
96974
|
+
server_enforcing: false
|
|
96975
|
+
});
|
|
96976
|
+
log(chalk.green(" Runtime skip \u2014 CodeRifts hook already installed (local)"));
|
|
96977
|
+
} else if (apply) {
|
|
96978
|
+
log(chalk.yellow(" Runtime apply \u2014 hook.install (local; not server ENFORCING)"));
|
|
96979
|
+
const r = runHookInstall(installHook);
|
|
96980
|
+
outcomes.push({
|
|
96981
|
+
layer: "runtime",
|
|
96982
|
+
action: "hook.install",
|
|
96983
|
+
apply: true,
|
|
96984
|
+
ok: r.ok,
|
|
96985
|
+
detail: `${r.detail}; not server-observable ENFORCING`,
|
|
96986
|
+
server_enforcing: false
|
|
96987
|
+
});
|
|
96988
|
+
if (!r.ok) errLog(chalk.red(` Runtime hook install failed: ${r.detail}`));
|
|
96989
|
+
} else {
|
|
96990
|
+
outcomes.push({
|
|
96991
|
+
layer: "runtime",
|
|
96992
|
+
action: "hook.install",
|
|
96993
|
+
apply: false,
|
|
96994
|
+
ok: true,
|
|
96995
|
+
detail: alreadyHook ? "dry-run: hook already present; would re-run install (idempotent)" : "dry-run: would run coderifts hook install",
|
|
96996
|
+
server_enforcing: false
|
|
96997
|
+
});
|
|
96998
|
+
log(chalk.dim(` Runtime dry-run \u2014 would ${alreadyHook ? "re-run" : "run"} hook install (local)`));
|
|
96999
|
+
}
|
|
97000
|
+
log(chalk.dim(AGENT_GUARD_GUIDANCE.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
97001
|
+
}
|
|
97002
|
+
{
|
|
97003
|
+
const st = legs.deploy && legs.deploy.status || "unknown";
|
|
97004
|
+
outcomes.push({
|
|
97005
|
+
layer: "deploy",
|
|
97006
|
+
action: "guidance",
|
|
97007
|
+
apply: false,
|
|
97008
|
+
// never mutates GitHub here
|
|
97009
|
+
ok: true,
|
|
97010
|
+
detail: `server status=${st} (declared-only); print CD step instruction`,
|
|
97011
|
+
server_enforcing: false
|
|
97012
|
+
});
|
|
97013
|
+
log(chalk.dim(` Deploy guidance \u2014 current: ${st} (not server-enforcing)`));
|
|
97014
|
+
log(chalk.dim(DEPLOY_GUIDANCE.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
97015
|
+
}
|
|
97016
|
+
{
|
|
97017
|
+
outcomes.push({
|
|
97018
|
+
layer: "content",
|
|
97019
|
+
action: "guidance",
|
|
97020
|
+
apply: false,
|
|
97021
|
+
ok: true,
|
|
97022
|
+
detail: "not_observable_from_server; guidance only",
|
|
97023
|
+
server_enforcing: false
|
|
97024
|
+
});
|
|
97025
|
+
log(chalk.dim(" Content guidance \u2014 not_observable_from_server"));
|
|
97026
|
+
log(chalk.dim(CONTENT_GUIDANCE.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
97027
|
+
}
|
|
97028
|
+
log("");
|
|
97029
|
+
log(chalk.bold(" Per-layer outcomes"));
|
|
97030
|
+
for (const o of outcomes) {
|
|
97031
|
+
const mark = o.ok ? chalk.green("ok") : chalk.red("FAIL");
|
|
97032
|
+
const mode = o.apply === true ? "apply" : o.action === "skip" ? "skip" : "dry-run";
|
|
97033
|
+
log(` ${String(o.layer).padEnd(8)} ${mark} ${mode.padEnd(7)} ${o.action} ${chalk.dim(o.detail || "")}`);
|
|
97034
|
+
}
|
|
97035
|
+
log("");
|
|
97036
|
+
log(chalk.bold(" Cross-layer report" + (apply ? " (after actions)" : " (current / dry-run)")));
|
|
97037
|
+
let finalReport = report;
|
|
97038
|
+
if (apply) {
|
|
97039
|
+
try {
|
|
97040
|
+
finalReport = await fetchStatus(repo, apiKey);
|
|
97041
|
+
} catch (e) {
|
|
97042
|
+
errLog(chalk.yellow(` Warning: re-fetch status failed: ${e && e.message || e}`));
|
|
97043
|
+
}
|
|
97044
|
+
}
|
|
97045
|
+
if (options.json) {
|
|
97046
|
+
log(JSON.stringify({ command: "enforce", apply, repo, outcomes, report: finalReport }, null, 2));
|
|
97047
|
+
} else {
|
|
97048
|
+
log(renderStatusReport(finalReport));
|
|
97049
|
+
}
|
|
97050
|
+
const anyFail = outcomes.some((o) => o.ok === false);
|
|
97051
|
+
return {
|
|
97052
|
+
exitCode: anyFail ? 1 : 0,
|
|
97053
|
+
apply,
|
|
97054
|
+
outcomes,
|
|
97055
|
+
report: finalReport
|
|
97056
|
+
};
|
|
97057
|
+
}
|
|
97058
|
+
module2.exports = {
|
|
97059
|
+
runEnforce,
|
|
97060
|
+
isMergeEnforcing,
|
|
97061
|
+
USAGE,
|
|
97062
|
+
AGENT_GUARD_GUIDANCE,
|
|
97063
|
+
DEPLOY_GUIDANCE,
|
|
97064
|
+
CONTENT_GUIDANCE
|
|
97065
|
+
};
|
|
97066
|
+
}
|
|
97067
|
+
});
|
|
97068
|
+
|
|
97069
|
+
// src/agent-host-files.embedded.js
|
|
97070
|
+
var require_agent_host_files_embedded = __commonJS({
|
|
97071
|
+
"src/agent-host-files.embedded.js"(exports2, module2) {
|
|
97072
|
+
"use strict";
|
|
97073
|
+
var AGENT_HOST_PATHS = Object.freeze([
|
|
97074
|
+
"AGENTS.md",
|
|
97075
|
+
"CLAUDE.md",
|
|
97076
|
+
".cursor/rules/coderifts.mdc",
|
|
97077
|
+
".github/copilot-instructions.md",
|
|
97078
|
+
"coderifts-langgraph-policy.js",
|
|
97079
|
+
"openai-agent-instructions.md"
|
|
97080
|
+
]);
|
|
97081
|
+
var AGENT_HOST_FILES = Object.freeze({
|
|
97082
|
+
"AGENTS.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts agent rules\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
|
|
97083
|
+
"CLAUDE.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
|
|
97084
|
+
".cursor/rules/coderifts.mdc": '---\ndescription: CodeRifts API governance \u2014 when to preflight and how to branch\nglobs:\nalwaysApply: true\n---\n\n<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
|
|
97085
|
+
".github/copilot-instructions.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts instructions for GitHub Copilot\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
|
|
97086
|
+
"coderifts-langgraph-policy.js": '// GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js\n// System policy string for a LangGraph (or similar) agent. Content is generated;\n// identical rule sentences to AGENTS.md / other formats.\n\'use strict\';\n\nmodule.exports = "Call `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\\n\\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\\n\\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\\n\\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\\n\\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not \\"proceed with caution\\" without monitoring.\\n\\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\\n\\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\\n\\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\\n\\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.";\n',
|
|
97087
|
+
"openai-agent-instructions.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts agent instructions\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n'
|
|
97088
|
+
});
|
|
97089
|
+
module2.exports = { AGENT_HOST_FILES, AGENT_HOST_PATHS };
|
|
97090
|
+
}
|
|
97091
|
+
});
|
|
97092
|
+
|
|
97093
|
+
// src/commands/agent-setup.js
|
|
97094
|
+
var require_agent_setup = __commonJS({
|
|
97095
|
+
"src/commands/agent-setup.js"(exports2, module2) {
|
|
97096
|
+
"use strict";
|
|
97097
|
+
var fs = require("fs");
|
|
97098
|
+
var path = require("path");
|
|
97099
|
+
var chalk = require_source();
|
|
97100
|
+
var { AGENT_HOST_FILES, AGENT_HOST_PATHS } = require_agent_host_files_embedded();
|
|
97101
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
97102
|
+
var USAGE = `Usage: coderifts agent-setup [--out <dir>] [--check] [--force]
|
|
97103
|
+
|
|
97104
|
+
--out <dir> Target directory (default: current working directory)
|
|
97105
|
+
--check Exit 0 if on-disk files match embedded content; exit 1 on drift
|
|
97106
|
+
--force Overwrite existing files (default: skip collisions)
|
|
97107
|
+
Unknown flags exit 1 (never silently ignored).
|
|
97108
|
+
`;
|
|
97109
|
+
function parseAgentSetupArgs(argv) {
|
|
97110
|
+
const args = argv.slice(2);
|
|
97111
|
+
let out = null;
|
|
97112
|
+
let check = false;
|
|
97113
|
+
let force = false;
|
|
97114
|
+
let i = 0;
|
|
97115
|
+
while (i < args.length && !String(args[i]).startsWith("-")) i += 1;
|
|
97116
|
+
while (i < args.length) {
|
|
97117
|
+
const a = args[i];
|
|
97118
|
+
if (a === "--out") {
|
|
97119
|
+
const v = args[i + 1];
|
|
97120
|
+
if (v == null || v.startsWith("-")) {
|
|
97121
|
+
return { out, check, force, error: `agent-setup: --out requires a path
|
|
97122
|
+
${USAGE}` };
|
|
97123
|
+
}
|
|
97124
|
+
out = path.resolve(v);
|
|
97125
|
+
i += 2;
|
|
97126
|
+
continue;
|
|
97127
|
+
}
|
|
97128
|
+
if (a === "--check") {
|
|
97129
|
+
check = true;
|
|
97130
|
+
i += 1;
|
|
97131
|
+
continue;
|
|
97132
|
+
}
|
|
97133
|
+
if (a === "--force") {
|
|
97134
|
+
force = true;
|
|
97135
|
+
i += 1;
|
|
97136
|
+
continue;
|
|
97137
|
+
}
|
|
97138
|
+
if (a.startsWith("-")) {
|
|
97139
|
+
return { out, check, force, error: `agent-setup: unrecognized argument: ${a}
|
|
97140
|
+
${USAGE}` };
|
|
97141
|
+
}
|
|
97142
|
+
return { out, check, force, error: `agent-setup: unrecognized argument: ${a}
|
|
97143
|
+
${USAGE}` };
|
|
97144
|
+
}
|
|
97145
|
+
return { out, check, force };
|
|
97146
|
+
}
|
|
97147
|
+
function runAgentSetup(options = {}, deps = {}) {
|
|
97148
|
+
const log = deps.log || console.log.bind(console);
|
|
97149
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
97150
|
+
const doExit = deps.exit !== false;
|
|
97151
|
+
const cwd = deps.cwd || process.cwd();
|
|
97152
|
+
const exists = deps.exists || fs.existsSync.bind(fs);
|
|
97153
|
+
const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
|
|
97154
|
+
const writeFile = deps.writeFile || ((p, c) => {
|
|
97155
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
97156
|
+
fs.writeFileSync(p, c, "utf8");
|
|
97157
|
+
});
|
|
97158
|
+
let outDir = options.out ? path.resolve(String(options.out)) : cwd;
|
|
97159
|
+
let check = !!options.check;
|
|
97160
|
+
let force = !!options.force;
|
|
97161
|
+
if (deps.argv) {
|
|
97162
|
+
const parsed = parseAgentSetupArgs(deps.argv);
|
|
97163
|
+
if (parsed.error) {
|
|
97164
|
+
logErr(parsed.error.trimEnd());
|
|
97165
|
+
if (doExit) process.exit(1);
|
|
97166
|
+
return { exitCode: 1, code: "USAGE", message: parsed.error };
|
|
97167
|
+
}
|
|
97168
|
+
if (parsed.out) outDir = parsed.out;
|
|
97169
|
+
check = parsed.check;
|
|
97170
|
+
force = parsed.force;
|
|
97171
|
+
}
|
|
97172
|
+
const files = deps.files || AGENT_HOST_FILES;
|
|
97173
|
+
const relPaths = deps.paths || AGENT_HOST_PATHS;
|
|
97174
|
+
if (check) {
|
|
97175
|
+
let stale = false;
|
|
97176
|
+
for (const rel of relPaths) {
|
|
97177
|
+
const fp = path.join(outDir, rel);
|
|
97178
|
+
const expected = files[rel];
|
|
97179
|
+
if (!exists(fp)) {
|
|
97180
|
+
logErr(chalk.red(`agent-setup --check: missing ${rel}`));
|
|
97181
|
+
stale = true;
|
|
97182
|
+
continue;
|
|
97183
|
+
}
|
|
97184
|
+
const onDisk = readFile(fp);
|
|
97185
|
+
if (onDisk !== expected) {
|
|
97186
|
+
logErr(chalk.red(`agent-setup --check: drift ${rel}`));
|
|
97187
|
+
stale = true;
|
|
97188
|
+
}
|
|
97189
|
+
}
|
|
97190
|
+
if (stale) {
|
|
97191
|
+
logErr("Run: coderifts agent-setup --out " + outDir + " --force");
|
|
97192
|
+
if (doExit) process.exit(1);
|
|
97193
|
+
return { exitCode: 1, code: "DRIFT", outDir };
|
|
97194
|
+
}
|
|
97195
|
+
log(chalk.green(`agent-setup: up to date (${outDir}, ${relPaths.length} files)`));
|
|
97196
|
+
if (doExit) process.exit(0);
|
|
97197
|
+
return { exitCode: 0, code: "UP_TO_DATE", outDir };
|
|
97198
|
+
}
|
|
97199
|
+
const summary = { written: [], skipped: [], forced: [] };
|
|
97200
|
+
for (const rel of relPaths) {
|
|
97201
|
+
const fp = path.join(outDir, rel);
|
|
97202
|
+
const content = files[rel];
|
|
97203
|
+
if (exists(fp) && !force) {
|
|
97204
|
+
summary.skipped.push(rel);
|
|
97205
|
+
continue;
|
|
97206
|
+
}
|
|
97207
|
+
if (exists(fp) && force) summary.forced.push(rel);
|
|
97208
|
+
writeFile(fp, content);
|
|
97209
|
+
summary.written.push(rel);
|
|
97210
|
+
}
|
|
97211
|
+
if (!options.json) {
|
|
97212
|
+
log(chalk.bold("CodeRifts agent-setup"));
|
|
97213
|
+
log(` target: ${outDir}`);
|
|
97214
|
+
for (const rel of summary.written) {
|
|
97215
|
+
const tag = summary.forced.includes(rel) ? "overwrote" : "wrote";
|
|
97216
|
+
log(chalk.green(` ${tag}: ${rel}`));
|
|
97217
|
+
}
|
|
97218
|
+
for (const rel of summary.skipped) {
|
|
97219
|
+
log(chalk.yellow(` skipped: ${rel} (exists; use --force to overwrite)`));
|
|
97220
|
+
}
|
|
97221
|
+
log("");
|
|
97222
|
+
log(chalk.dim(` ${summary.written.length} written, ${summary.skipped.length} skipped`));
|
|
97223
|
+
}
|
|
97224
|
+
if (doExit) process.exit(0);
|
|
97225
|
+
return { exitCode: 0, code: "OK", outDir, ...summary };
|
|
97226
|
+
}
|
|
97227
|
+
module2.exports = {
|
|
97228
|
+
runAgentSetup,
|
|
97229
|
+
parseAgentSetupArgs,
|
|
97230
|
+
AGENT_HOST_FILES,
|
|
97231
|
+
AGENT_HOST_PATHS,
|
|
97232
|
+
USAGE
|
|
97233
|
+
};
|
|
97234
|
+
}
|
|
97235
|
+
});
|
|
97236
|
+
|
|
97237
|
+
// src/copilot-mcp.embedded.js
|
|
97238
|
+
var require_copilot_mcp_embedded = __commonJS({
|
|
97239
|
+
"src/copilot-mcp.embedded.js"(exports2, module2) {
|
|
97240
|
+
"use strict";
|
|
97241
|
+
var COPILOT_MCP_PATHS = Object.freeze([
|
|
97242
|
+
".vscode/mcp.json",
|
|
97243
|
+
"copilot-cloud-agent-mcp.json",
|
|
97244
|
+
"copilot-custom-agent-mcp.frontmatter.md",
|
|
97245
|
+
"docs/copilot-mcp.md"
|
|
97246
|
+
]);
|
|
97247
|
+
var COPILOT_MCP_FILES = Object.freeze({
|
|
97248
|
+
".vscode/mcp.json": '{\n "$comment": "GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.",\n "servers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "headers": {\n "Authorization": "Bearer ${input:coderifts_api_key}"\n }\n }\n },\n "inputs": [\n {\n "type": "promptString",\n "id": "coderifts_api_key",\n "description": "CodeRifts API key (https://coderifts.com) \u2014 Authorization Bearer",\n "password": true\n }\n ]\n}\n',
|
|
97249
|
+
"copilot-cloud-agent-mcp.json": '{\n "mcpServers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "tools": [\n "preflight_change_set",\n "verify_receipt",\n "get_decision_details"\n ],\n "headers": {\n "Authorization": "Bearer ${COPILOT_MCP_CODERIFTS_API_KEY}"\n }\n }\n }\n}\n',
|
|
97250
|
+
"copilot-custom-agent-mcp.frontmatter.md": "---\n# GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.\nname: coderifts-governance\ndescription: >\n CodeRifts API governance \u2014 preflight contract change sets before merge/deploy/publish,\n verify receipts, look up prior decisions. Branch on execution_action only.\ntools: ['coderifts/preflight_change_set', 'coderifts/verify_receipt', 'coderifts/get_decision_details']\nmcp-servers:\n coderifts:\n type: http\n url: https://app.coderifts.com/mcp\n tools:\n - preflight_change_set\n - verify_receipt\n - get_decision_details\n headers:\n Authorization: Bearer ${{ secrets.COPILOT_MCP_CODERIFTS_API_KEY }}\n---\n\nYou are a CodeRifts-aware agent. Before merge, deploy, publish, or tool registration when\ncontract artifacts changed, call `coderifts/preflight_change_set` with the complete base\u2192head\nchange set. Branch on `execution_action` only (CONTINUE, CONTINUE_WITH_MONITORING,\nREQUEST_APPROVAL, STOP). Do not treat `decision` or `safe_for_agent` as control flow.\nUse `coderifts/verify_receipt` to check an existing receipt; `coderifts/get_decision_details`\nfor a prior decision_id.\n",
|
|
97251
|
+
"docs/copilot-mcp.md": '# CodeRifts + GitHub Copilot MCP\n\n<!-- GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator. -->\n\nWire the **hosted** CodeRifts MCP server (`https://app.coderifts.com/mcp`) into every Copilot surface from **one**\nsource of truth (`CANONICAL_TOOL_NAMES` in `src/routes/mcp-streamable.js`).\n\n## Canonical tools (live tools/list)\n\n- `preflight_change_set`\n- `verify_receipt`\n- `get_decision_details`\n\nDo **not** list hidden/advanced aliases here. Only these 3 tools are the default\nagent-facing surface.\n\n## The servers-vs-mcpServers trap\n\n| Surface | Config location | Root key | Auth |\n|---------|-----------------|----------|------|\n| **VS Code / Copilot Chat** | `.vscode/mcp.json` | **`servers`** | `${input:coderifts_api_key}` + `inputs[]` |\n| **Copilot cloud agent + code review** | Repo **Settings \u2192 Copilot \u2192 MCP servers** (paste JSON) | **`mcpServers`** | Agents secret `COPILOT_MCP_CODERIFTS_API_KEY` in `headers` |\n| **Custom agent** (org/enterprise) | Agent profile `.md` YAML frontmatter | **`mcp-servers`** | `${{ secrets.COPILOT_MCP_CODERIFTS_API_KEY }}` |\n\nCursor / Claude Desktop / Grok use `mcpServers` in their own files \u2014 that is a **different**\necosystem. Do not copy a Cursor config into `.vscode/mcp.json`, and do not paste a VS Code\n`servers` document into GitHub repo Settings.\n\n## 1. VS Code / Copilot Chat (developer surface)\n\n1. Get an API key at https://coderifts.com\n2. Write the generated file to `.vscode/mcp.json` (or run `coderifts copilot-setup`)\n3. Reload VS Code; when prompted, paste the API key for `coderifts_api_key`\n4. In Copilot Chat Agent mode, confirm tools: preflight_change_set, verify_receipt, get_decision_details\n\n```json\n{\n "$comment": "GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.",\n "servers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "headers": {\n "Authorization": "Bearer ${input:coderifts_api_key}"\n }\n }\n },\n "inputs": [\n {\n "type": "promptString",\n "id": "coderifts_api_key",\n "description": "CodeRifts API key (https://coderifts.com) \u2014 Authorization Bearer",\n "password": true\n }\n ]\n}\n```\n\n## 2. Copilot cloud agent (PR / issue governance surface)\n\n1. Repo **Settings \u2192 Copilot \u2192 MCP servers** (or **Cloud agent \u2192 MCP configuration**)\n2. Paste the JSON below (root key **`mcpServers`**, includes required `tools` allowlist)\n3. Add an Agents secret: name `COPILOT_MCP_CODERIFTS_API_KEY`, value = CodeRifts API key\n4. Save. Validate: assign an issue to Copilot \u2192 session logs \u2192 **Start MCP Servers**\n\n```json\n{\n "mcpServers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "tools": [\n "preflight_change_set",\n "verify_receipt",\n "get_decision_details"\n ],\n "headers": {\n "Authorization": "Bearer ${COPILOT_MCP_CODERIFTS_API_KEY}"\n }\n }\n }\n}\n```\n\nNotes:\n\n- Cloud agent does **not** support OAuth remote MCP; Bearer is correct for CodeRifts.\n- Cloud agent does **not** support interactive `inputs` \u2014 use Agents secrets only.\n- `tools` is required; list the 3 canonical tools (or `["*"]` only if you accept every tool the server exposes).\n\n## 3. Custom agent (optional org/enterprise)\n\nAdd `mcp-servers` to the agent profile frontmatter (see generated\n`copilot-custom-agent-mcp.frontmatter.md`). Tool names may be namespaced as\n`coderifts/<tool>` in the profile `tools` list.\n\n## Regenerate / drift-check\n\n```bash\nnode scripts/generate-copilot-mcp.js\nnode scripts/generate-copilot-mcp.js --check\ncoderifts copilot-setup --out . # write into a repo\ncoderifts copilot-setup --check # drift vs embedded\n```\n\nServer URL and tool names always come from the live manifest \u2014 never hand-edit tool lists.\n'
|
|
97252
|
+
});
|
|
97253
|
+
module2.exports = { COPILOT_MCP_FILES, COPILOT_MCP_PATHS };
|
|
97254
|
+
}
|
|
97255
|
+
});
|
|
97256
|
+
|
|
97257
|
+
// src/commands/copilot-setup.js
|
|
97258
|
+
var require_copilot_setup = __commonJS({
|
|
97259
|
+
"src/commands/copilot-setup.js"(exports2, module2) {
|
|
97260
|
+
"use strict";
|
|
97261
|
+
var fs = require("fs");
|
|
97262
|
+
var path = require("path");
|
|
97263
|
+
var chalk = require_source();
|
|
97264
|
+
var { COPILOT_MCP_FILES, COPILOT_MCP_PATHS } = require_copilot_mcp_embedded();
|
|
97265
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
97266
|
+
var USAGE = `Usage: coderifts copilot-setup [--out <dir>] [--check] [--force]
|
|
97267
|
+
|
|
97268
|
+
--out <dir> Target directory (default: current working directory)
|
|
97269
|
+
--check Exit 0 if on-disk files match embedded content; exit 1 on drift
|
|
97270
|
+
--force Overwrite existing files (default: skip collisions)
|
|
97271
|
+
Unknown flags exit 1 (never silently ignored).
|
|
97272
|
+
|
|
97273
|
+
Writes:
|
|
97274
|
+
.vscode/mcp.json VS Code / Copilot Chat (root key: servers)
|
|
97275
|
+
copilot-cloud-agent-mcp.json Paste into Settings \u2192 Copilot \u2192 MCP (mcpServers)
|
|
97276
|
+
copilot-custom-agent-mcp.frontmatter.md Custom agent YAML frontmatter
|
|
97277
|
+
docs/copilot-mcp.md Install guide
|
|
97278
|
+
`;
|
|
97279
|
+
function parseCopilotSetupArgs(argv) {
|
|
97280
|
+
const args = argv.slice(2);
|
|
97281
|
+
let out = null;
|
|
97282
|
+
let check = false;
|
|
97283
|
+
let force = false;
|
|
97284
|
+
let i = 0;
|
|
97285
|
+
while (i < args.length && !String(args[i]).startsWith("-")) i += 1;
|
|
97286
|
+
while (i < args.length) {
|
|
97287
|
+
const a = args[i];
|
|
97288
|
+
if (a === "--out") {
|
|
97289
|
+
const v = args[i + 1];
|
|
97290
|
+
if (v == null || v.startsWith("-")) {
|
|
97291
|
+
return { out, check, force, error: `copilot-setup: --out requires a path
|
|
97292
|
+
${USAGE}` };
|
|
97293
|
+
}
|
|
97294
|
+
out = path.resolve(v);
|
|
97295
|
+
i += 2;
|
|
97296
|
+
continue;
|
|
97297
|
+
}
|
|
97298
|
+
if (a === "--check") {
|
|
97299
|
+
check = true;
|
|
97300
|
+
i += 1;
|
|
97301
|
+
continue;
|
|
97302
|
+
}
|
|
97303
|
+
if (a === "--force") {
|
|
97304
|
+
force = true;
|
|
97305
|
+
i += 1;
|
|
97306
|
+
continue;
|
|
97307
|
+
}
|
|
97308
|
+
if (a.startsWith("-")) {
|
|
97309
|
+
return { out, check, force, error: `copilot-setup: unrecognized argument: ${a}
|
|
97310
|
+
${USAGE}` };
|
|
97311
|
+
}
|
|
97312
|
+
return { out, check, force, error: `copilot-setup: unrecognized argument: ${a}
|
|
97313
|
+
${USAGE}` };
|
|
97314
|
+
}
|
|
97315
|
+
return { out, check, force };
|
|
97316
|
+
}
|
|
97317
|
+
function runCopilotSetup(options = {}, deps = {}) {
|
|
97318
|
+
const log = deps.log || console.log.bind(console);
|
|
97319
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
97320
|
+
const doExit = deps.exit !== false;
|
|
97321
|
+
const cwd = deps.cwd || process.cwd();
|
|
97322
|
+
const exists = deps.exists || fs.existsSync.bind(fs);
|
|
97323
|
+
const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
|
|
97324
|
+
const writeFile = deps.writeFile || ((p, c) => {
|
|
97325
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
97326
|
+
fs.writeFileSync(p, c, "utf8");
|
|
97327
|
+
});
|
|
97328
|
+
let outDir = options.out ? path.resolve(String(options.out)) : cwd;
|
|
97329
|
+
let check = !!options.check;
|
|
97330
|
+
let force = !!options.force;
|
|
97331
|
+
if (deps.argv) {
|
|
97332
|
+
const parsed = parseCopilotSetupArgs(deps.argv);
|
|
97333
|
+
if (parsed.error) {
|
|
97334
|
+
logErr(parsed.error.trimEnd());
|
|
97335
|
+
if (doExit) process.exit(1);
|
|
97336
|
+
return { exitCode: 1, code: "USAGE", message: parsed.error };
|
|
97337
|
+
}
|
|
97338
|
+
if (parsed.out) outDir = parsed.out;
|
|
97339
|
+
check = parsed.check;
|
|
97340
|
+
force = parsed.force;
|
|
97341
|
+
}
|
|
97342
|
+
const files = deps.files || COPILOT_MCP_FILES;
|
|
97343
|
+
const relPaths = deps.paths || COPILOT_MCP_PATHS;
|
|
97344
|
+
if (check) {
|
|
97345
|
+
let stale = false;
|
|
97346
|
+
for (const rel of relPaths) {
|
|
97347
|
+
const fp = path.join(outDir, rel);
|
|
97348
|
+
const expected = files[rel];
|
|
97349
|
+
if (!exists(fp)) {
|
|
97350
|
+
logErr(chalk.red(`copilot-setup --check: missing ${rel}`));
|
|
97351
|
+
stale = true;
|
|
97352
|
+
continue;
|
|
97353
|
+
}
|
|
97354
|
+
const onDisk = readFile(fp);
|
|
97355
|
+
if (onDisk !== expected) {
|
|
97356
|
+
logErr(chalk.red(`copilot-setup --check: drift ${rel}`));
|
|
97357
|
+
stale = true;
|
|
97358
|
+
}
|
|
97359
|
+
}
|
|
97360
|
+
if (stale) {
|
|
97361
|
+
logErr("Run: coderifts copilot-setup --out " + outDir + " --force");
|
|
97362
|
+
if (doExit) process.exit(1);
|
|
97363
|
+
return { exitCode: 1, code: "DRIFT", outDir };
|
|
97364
|
+
}
|
|
97365
|
+
log(chalk.green(`copilot-setup: up to date (${outDir}, ${relPaths.length} files)`));
|
|
97366
|
+
if (doExit) process.exit(0);
|
|
97367
|
+
return { exitCode: 0, code: "UP_TO_DATE", outDir };
|
|
97368
|
+
}
|
|
97369
|
+
const summary = { written: [], skipped: [], forced: [] };
|
|
97370
|
+
for (const rel of relPaths) {
|
|
97371
|
+
const fp = path.join(outDir, rel);
|
|
97372
|
+
const content = files[rel];
|
|
97373
|
+
if (exists(fp) && !force) {
|
|
97374
|
+
summary.skipped.push(rel);
|
|
97375
|
+
continue;
|
|
97376
|
+
}
|
|
97377
|
+
if (exists(fp) && force) summary.forced.push(rel);
|
|
97378
|
+
writeFile(fp, content);
|
|
97379
|
+
summary.written.push(rel);
|
|
97380
|
+
}
|
|
97381
|
+
if (!options.json) {
|
|
97382
|
+
log(chalk.bold("CodeRifts copilot-setup"));
|
|
97383
|
+
log(` target: ${outDir}`);
|
|
97384
|
+
for (const rel of summary.written) {
|
|
97385
|
+
const tag = summary.forced.includes(rel) ? "overwrote" : "wrote";
|
|
97386
|
+
log(chalk.green(` ${tag}: ${rel}`));
|
|
97387
|
+
}
|
|
97388
|
+
for (const rel of summary.skipped) {
|
|
97389
|
+
log(chalk.yellow(` skipped: ${rel} (exists; use --force to overwrite)`));
|
|
97390
|
+
}
|
|
97391
|
+
log("");
|
|
97392
|
+
log(chalk.dim(' VS Code: open .vscode/mcp.json \u2014 root key is "servers"'));
|
|
97393
|
+
log(chalk.dim(" Cloud: paste copilot-cloud-agent-mcp.json into Settings \u2192 Copilot \u2192 MCP"));
|
|
97394
|
+
log(chalk.dim(" Secret: COPILOT_MCP_CODERIFTS_API_KEY (Agents secret) = CodeRifts API key"));
|
|
97395
|
+
log(chalk.dim(` ${summary.written.length} written, ${summary.skipped.length} skipped`));
|
|
97396
|
+
}
|
|
97397
|
+
if (doExit) process.exit(0);
|
|
97398
|
+
return { exitCode: 0, code: "OK", outDir, ...summary };
|
|
97399
|
+
}
|
|
97400
|
+
module2.exports = {
|
|
97401
|
+
runCopilotSetup,
|
|
97402
|
+
parseCopilotSetupArgs,
|
|
97403
|
+
COPILOT_MCP_FILES,
|
|
97404
|
+
COPILOT_MCP_PATHS,
|
|
97405
|
+
USAGE
|
|
97406
|
+
};
|
|
97407
|
+
}
|
|
97408
|
+
});
|
|
97409
|
+
|
|
97410
|
+
// src/commands/lock.js
|
|
97411
|
+
var require_lock = __commonJS({
|
|
97412
|
+
"src/commands/lock.js"(exports2, module2) {
|
|
97413
|
+
"use strict";
|
|
97414
|
+
var fs = require("fs");
|
|
97415
|
+
var path = require("path");
|
|
97416
|
+
var chalk = require_source();
|
|
97417
|
+
var { getApiKey } = require_config();
|
|
97418
|
+
var { cloudGetLock } = require_cloud();
|
|
97419
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
97420
|
+
var DEFAULT_OUT = "coderifts.lock";
|
|
97421
|
+
var EPHEMERAL_LOCK_KEYS = Object.freeze([
|
|
97422
|
+
"correlation_id",
|
|
97423
|
+
"generated_at",
|
|
97424
|
+
"request_correlation_id",
|
|
97425
|
+
"meta"
|
|
97426
|
+
]);
|
|
97427
|
+
var COMMITTED_KEY_ORDER = Object.freeze([
|
|
97428
|
+
"lockfile_version",
|
|
97429
|
+
"repo",
|
|
97430
|
+
"provenance",
|
|
97431
|
+
"agents",
|
|
97432
|
+
"last_accepted_fingerprint",
|
|
97433
|
+
"decision_spec_version",
|
|
97434
|
+
"receipt_kind"
|
|
97435
|
+
]);
|
|
97436
|
+
var USAGE = [
|
|
97437
|
+
"Usage: coderifts lock --repo owner/repo [--out coderifts.lock]",
|
|
97438
|
+
" or: coderifts lock owner/repo [--out path]",
|
|
97439
|
+
"",
|
|
97440
|
+
"Fetches the observed agent-contract lockfile (coderifts.lock v1) and writes it to disk.",
|
|
97441
|
+
"Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
|
|
97442
|
+
"Observed-only: agents[] come from real usage observations; empty when none recorded.",
|
|
97443
|
+
"Written file is byte-stable for the same state (no correlation_id / generated_at)."
|
|
97444
|
+
].join("\n");
|
|
97445
|
+
function isValidRepo(full) {
|
|
97446
|
+
const parts = String(full || "").split("/");
|
|
97447
|
+
if (parts.length !== 2) return false;
|
|
97448
|
+
const [owner, repo] = parts.map((p) => p.trim());
|
|
97449
|
+
return !!(owner && repo);
|
|
97450
|
+
}
|
|
97451
|
+
function toCommittedLockfile(doc) {
|
|
97452
|
+
const src = doc && typeof doc === "object" && !Array.isArray(doc) ? doc : {};
|
|
97453
|
+
const out = {};
|
|
97454
|
+
for (const key of COMMITTED_KEY_ORDER) {
|
|
97455
|
+
if (!Object.prototype.hasOwnProperty.call(src, key)) continue;
|
|
97456
|
+
if (EPHEMERAL_LOCK_KEYS.includes(key)) continue;
|
|
97457
|
+
out[key] = src[key];
|
|
97458
|
+
}
|
|
97459
|
+
for (const k of EPHEMERAL_LOCK_KEYS) {
|
|
97460
|
+
if (Object.prototype.hasOwnProperty.call(out, k)) delete out[k];
|
|
97461
|
+
}
|
|
97462
|
+
return out;
|
|
97463
|
+
}
|
|
97464
|
+
function renderLockfileJson(doc) {
|
|
97465
|
+
return `${JSON.stringify(doc, null, 2)}
|
|
97466
|
+
`;
|
|
97467
|
+
}
|
|
97468
|
+
async function runLock(options = {}, deps = {}) {
|
|
97469
|
+
const getKey = deps.getApiKey || getApiKey;
|
|
97470
|
+
const fetchLock = deps.cloudGetLock || cloudGetLock;
|
|
97471
|
+
const log = deps.log || console.log;
|
|
97472
|
+
const errLog = deps.errLog || console.error;
|
|
97473
|
+
const writeFile = deps.writeFile || ((p, c) => {
|
|
97474
|
+
fs.mkdirSync(path.dirname(path.resolve(p)), { recursive: true });
|
|
97475
|
+
fs.writeFileSync(p, c, "utf8");
|
|
97476
|
+
});
|
|
97477
|
+
const cwd = deps.cwd || process.cwd();
|
|
97478
|
+
const repo = options.repo != null && String(options.repo).trim() ? String(options.repo).trim() : null;
|
|
97479
|
+
if (!repo) {
|
|
97480
|
+
errLog(chalk.red("Error: missing repo"));
|
|
97481
|
+
errLog(USAGE);
|
|
97482
|
+
return { exitCode: 1, error: "missing_repo" };
|
|
97483
|
+
}
|
|
97484
|
+
if (!isValidRepo(repo)) {
|
|
97485
|
+
errLog(chalk.red("Error: repo must be in owner/repo form (e.g. coderifts/app)"));
|
|
97486
|
+
return { exitCode: 1, error: "invalid_repo" };
|
|
97487
|
+
}
|
|
97488
|
+
const apiKey = getKey();
|
|
97489
|
+
if (!apiKey) {
|
|
97490
|
+
errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
|
|
97491
|
+
return { exitCode: 1, error: "missing_api_key" };
|
|
97492
|
+
}
|
|
97493
|
+
let doc;
|
|
97494
|
+
try {
|
|
97495
|
+
doc = await fetchLock(repo, apiKey);
|
|
97496
|
+
} catch (e) {
|
|
97497
|
+
const msg = e && e.message ? String(e.message) : "request failed";
|
|
97498
|
+
errLog(chalk.red(`Error: ${msg}`));
|
|
97499
|
+
if (e && e.code) errLog(chalk.dim(` (${e.code})`));
|
|
97500
|
+
return { exitCode: 1, error: msg };
|
|
97501
|
+
}
|
|
97502
|
+
if (!doc || typeof doc !== "object" || doc.lockfile_version == null) {
|
|
97503
|
+
errLog(chalk.red("Error: invalid lockfile response from API"));
|
|
97504
|
+
return { exitCode: 1, error: "invalid_response" };
|
|
97505
|
+
}
|
|
97506
|
+
const committed = toCommittedLockfile(doc);
|
|
97507
|
+
const body = renderLockfileJson(committed);
|
|
97508
|
+
const outRel = options.out != null && String(options.out).trim() ? String(options.out).trim() : DEFAULT_OUT;
|
|
97509
|
+
const outPath = path.isAbsolute(outRel) ? outRel : path.join(cwd, outRel);
|
|
97510
|
+
try {
|
|
97511
|
+
writeFile(outPath, body);
|
|
97512
|
+
} catch (e) {
|
|
97513
|
+
const msg = e && e.message ? String(e.message) : "write failed";
|
|
97514
|
+
errLog(chalk.red(`Error: failed to write ${outPath}: ${msg}`));
|
|
97515
|
+
return { exitCode: 1, error: msg };
|
|
97516
|
+
}
|
|
97517
|
+
if (options.json) {
|
|
97518
|
+
log(body.trimEnd());
|
|
97519
|
+
} else {
|
|
97520
|
+
const nAgents = Array.isArray(committed.agents) ? committed.agents.length : 0;
|
|
97521
|
+
const nOps = Array.isArray(committed.agents) ? committed.agents.reduce((n, a) => n + (a.operations && a.operations.length || 0), 0) : 0;
|
|
97522
|
+
log(chalk.bold("CodeRifts lock (observed)"));
|
|
97523
|
+
log(` repo: ${committed.repo || repo}`);
|
|
97524
|
+
log(` wrote: ${outPath}`);
|
|
97525
|
+
log(` agents: ${nAgents} operations: ${nOps}`);
|
|
97526
|
+
log(` fingerprint: ${committed.last_accepted_fingerprint || chalk.dim("(none)")}`);
|
|
97527
|
+
log(chalk.dim(" Observed-only; file is byte-stable (no correlation_id / generated_at)."));
|
|
97528
|
+
}
|
|
97529
|
+
return { exitCode: 0, doc, committed, outPath };
|
|
97530
|
+
}
|
|
97531
|
+
module2.exports = {
|
|
97532
|
+
runLock,
|
|
97533
|
+
renderLockfileJson,
|
|
97534
|
+
toCommittedLockfile,
|
|
97535
|
+
isValidRepo,
|
|
97536
|
+
DEFAULT_OUT,
|
|
97537
|
+
EPHEMERAL_LOCK_KEYS,
|
|
97538
|
+
COMMITTED_KEY_ORDER,
|
|
97539
|
+
USAGE
|
|
97540
|
+
};
|
|
97541
|
+
}
|
|
97542
|
+
});
|
|
97543
|
+
|
|
95484
97544
|
// corpus/vectors-mcp-fpfn.json
|
|
95485
97545
|
var require_vectors_mcp_fpfn = __commonJS({
|
|
95486
97546
|
"corpus/vectors-mcp-fpfn.json"(exports2, module2) {
|
|
@@ -97380,6 +99440,18 @@ program.command("publish-gate").description("Gate npm publish on contract-artifa
|
|
|
97380
99440
|
process.exitCode = code;
|
|
97381
99441
|
process.exit(code);
|
|
97382
99442
|
});
|
|
99443
|
+
program.command("registry-gate [dir]").description("Admit a directory of OpenAPI specs via local registry validation (CI gate, no cloud)").option("--glob <pattern>", "Filter discovered paths with agent-guard matchGlob (relative to dir)").option("--errors-only", "Fail only on ERROR severity (warnings are printed, exit 0)").option("--warn-only", "Advisory: print all findings, always exit 0").action((dir, options) => {
|
|
99444
|
+
const { runRegistryGate } = require_registry_gate();
|
|
99445
|
+
const result = runRegistryGate({
|
|
99446
|
+
dir: dir || ".",
|
|
99447
|
+
glob: options.glob,
|
|
99448
|
+
errorsOnly: !!options.errorsOnly,
|
|
99449
|
+
warnOnly: !!options.warnOnly
|
|
99450
|
+
});
|
|
99451
|
+
const code = result && typeof result.exitCode === "number" ? result.exitCode : 1;
|
|
99452
|
+
process.exitCode = code;
|
|
99453
|
+
process.exit(code);
|
|
99454
|
+
});
|
|
97383
99455
|
program.command("init [template]").description("Generate a .coderifts.yml from a policy template (startup, growth, fintech, public-api, microservices)").action(async (template) => {
|
|
97384
99456
|
const { init } = require_init();
|
|
97385
99457
|
await init(template);
|
|
@@ -97388,6 +99460,52 @@ program.command("login").description("Save your API key for cloud features").act
|
|
|
97388
99460
|
const { login } = require_login();
|
|
97389
99461
|
await login();
|
|
97390
99462
|
});
|
|
99463
|
+
program.command("setup-required-check").description('Guide setup of required status check "CodeRifts / contract-gate" (uses gh, your credentials)').option("--branch <name>", "Branch to protect (default: repo default branch)").option("--repo <owner/repo>", "Override owner/repo (default: git remote origin)").option("--apply", "Apply the protection change (default: print the exact gh command only)").option("--json", "Machine-readable JSON result").action(async (options) => {
|
|
99464
|
+
const { runSetupRequiredCheck } = require_setup_required_check();
|
|
99465
|
+
const result = await runSetupRequiredCheck(options);
|
|
99466
|
+
if (result && typeof result.exitCode === "number") {
|
|
99467
|
+
process.exitCode = result.exitCode;
|
|
99468
|
+
}
|
|
99469
|
+
});
|
|
99470
|
+
program.command("status [repo]").description("Show cross-layer enforcement status (Runtime / Merge / Deploy) for a repo \u2014 read-only").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--json", "Machine-readable JSON (raw API body)").action(async (repoPositional, options) => {
|
|
99471
|
+
const { runStatus } = require_status();
|
|
99472
|
+
const result = await runStatus({
|
|
99473
|
+
...options,
|
|
99474
|
+
repo: options.repo || repoPositional || null
|
|
99475
|
+
});
|
|
99476
|
+
if (result && typeof result.exitCode === "number") {
|
|
99477
|
+
process.exitCode = result.exitCode;
|
|
99478
|
+
}
|
|
99479
|
+
});
|
|
99480
|
+
program.command("enforce [repo]").description("Close enforcement gaps by chaining setup-required-check / hook / deploy guidance (dry-run default; use --apply to mutate)").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--apply", "Actually run underlying setup commands (default: dry-run only)").option("--json", "Machine-readable JSON result").action(async (repoPositional, options) => {
|
|
99481
|
+
const { runEnforce } = require_enforce();
|
|
99482
|
+
const result = await runEnforce({
|
|
99483
|
+
...options,
|
|
99484
|
+
repo: options.repo || repoPositional || null,
|
|
99485
|
+
apply: !!options.apply
|
|
99486
|
+
});
|
|
99487
|
+
if (result && typeof result.exitCode === "number") {
|
|
99488
|
+
process.exitCode = result.exitCode;
|
|
99489
|
+
}
|
|
99490
|
+
});
|
|
99491
|
+
program.command("agent-setup").description("Write AGENTS.md / CLAUDE.md / Cursor / Copilot / LangGraph / OpenAI agent rule files").option("--out <dir>", "Target directory (default: current working directory)").option("--check", "Exit 0 if on-disk files match embedded content; exit 1 on drift").option("--force", "Overwrite existing files (default: skip collisions)").action((options) => {
|
|
99492
|
+
const { runAgentSetup } = require_agent_setup();
|
|
99493
|
+
runAgentSetup(options, { exit: true });
|
|
99494
|
+
});
|
|
99495
|
+
program.command("copilot-setup").description("Write GitHub Copilot MCP configs (.vscode/mcp.json + cloud-agent paste JSON + docs)").option("--out <dir>", "Target directory (default: current working directory)").option("--check", "Exit 0 if on-disk files match embedded content; exit 1 on drift").option("--force", "Overwrite existing files (default: skip collisions)").action((options) => {
|
|
99496
|
+
const { runCopilotSetup } = require_copilot_setup();
|
|
99497
|
+
runCopilotSetup(options, { exit: true });
|
|
99498
|
+
});
|
|
99499
|
+
program.command("lock [repo]").description("Fetch the observed agent-contract lockfile (coderifts.lock v1) for a repo").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--out <path>", "Output path (default: coderifts.lock in cwd)").option("--json", "Print the lock document JSON to stdout (still writes --out)").action(async (repoPositional, options) => {
|
|
99500
|
+
const { runLock } = require_lock();
|
|
99501
|
+
const result = await runLock({
|
|
99502
|
+
...options,
|
|
99503
|
+
repo: options.repo || repoPositional || null
|
|
99504
|
+
});
|
|
99505
|
+
if (result && typeof result.exitCode === "number") {
|
|
99506
|
+
process.exitCode = result.exitCode;
|
|
99507
|
+
}
|
|
99508
|
+
});
|
|
97391
99509
|
var hookCmd = program.command("hook").description("Manage the CodeRifts pre-push Git hook");
|
|
97392
99510
|
hookCmd.command("install").description("Install the CodeRifts pre-push hook in the current Git repo").action(() => {
|
|
97393
99511
|
const { install } = require_hook();
|