jorgex-stack 1.4.0 → 1.5.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.
Files changed (2) hide show
  1. package/dist/cli.js +903 -9
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import * as p6 from "@clack/prompts";
5
+ import fs24 from "fs";
5
6
  import { pathToFileURL as pathToFileURL2 } from "url";
6
7
 
7
8
  // src/install.ts
@@ -5192,7 +5193,7 @@ function readOptional(file, fallback) {
5192
5193
  }
5193
5194
  function hashPiTarball(file) {
5194
5195
  const descriptor = fs23.openSync(file, "r");
5195
- const sha256 = createHash("sha256");
5196
+ const sha2562 = createHash("sha256");
5196
5197
  const sha512 = createHash("sha512");
5197
5198
  const buffer = Buffer.allocUnsafe(1024 * 1024);
5198
5199
  let bytes = 0;
@@ -5202,13 +5203,13 @@ function hashPiTarball(file) {
5202
5203
  if (read === 0) break;
5203
5204
  bytes += read;
5204
5205
  const chunk = buffer.subarray(0, read);
5205
- sha256.update(chunk);
5206
+ sha2562.update(chunk);
5206
5207
  sha512.update(chunk);
5207
5208
  }
5208
5209
  } finally {
5209
5210
  fs23.closeSync(descriptor);
5210
5211
  }
5211
- return { path: file, bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
5212
+ return { path: file, bytes, sha256: sha2562.digest("hex"), sha512: sha512.digest("hex") };
5212
5213
  }
5213
5214
  async function acquirePiTarball(destination) {
5214
5215
  const existing = fs23.statSync(destination, { throwIfNoEntry: false });
@@ -5443,9 +5444,848 @@ async function runManagedPiSystem(input) {
5443
5444
  });
5444
5445
  }
5445
5446
 
5447
+ // src/lib/quality-runner.ts
5448
+ import path33 from "path";
5449
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
5450
+
5451
+ // src/lib/quality-policy.ts
5452
+ var QUALITY_PROFILES = ["routine", "elevated", "high", "release"];
5453
+ function hasText(value) {
5454
+ return typeof value === "string" && value.trim() !== "";
5455
+ }
5456
+ function isQualityProfile(value) {
5457
+ return typeof value === "string" && QUALITY_PROFILES.includes(value);
5458
+ }
5459
+ function mergeStatus(current, next) {
5460
+ if (current === "fail" || next === "fail") return "fail";
5461
+ if (current === "incomplete" || next === "incomplete") return "incomplete";
5462
+ return "pass";
5463
+ }
5464
+ function resultStatus(result) {
5465
+ switch (result.status) {
5466
+ case "fail":
5467
+ return "fail";
5468
+ case "incomplete":
5469
+ return "incomplete";
5470
+ case "not-applicable":
5471
+ return "pass";
5472
+ case "pass":
5473
+ return hasText(result.evidence) ? "pass" : "incomplete";
5474
+ default:
5475
+ return "incomplete";
5476
+ }
5477
+ }
5478
+ function evaluateQualityPolicy(input) {
5479
+ const requestedProfile = input.profile === void 0 ? "routine" : input.profile;
5480
+ if (!isQualityProfile(requestedProfile)) {
5481
+ throw new Error(`Unknown quality profile: ${String(requestedProfile)}`);
5482
+ }
5483
+ const controls = /* @__PURE__ */ new Map();
5484
+ let status = "pass";
5485
+ let hasRequired = false;
5486
+ for (const control of input.controls) {
5487
+ if (!hasText(control.id) || control.requirement !== "required" && control.requirement !== "optional" || controls.has(control.id)) {
5488
+ status = mergeStatus(status, "incomplete");
5489
+ continue;
5490
+ }
5491
+ controls.set(control.id, control);
5492
+ if (control.requirement === "required") hasRequired = true;
5493
+ }
5494
+ const results = /* @__PURE__ */ new Map();
5495
+ for (const result of input.results) {
5496
+ const control = controls.get(result.controlId);
5497
+ if (!control || results.has(result.controlId)) {
5498
+ status = mergeStatus(status, "incomplete");
5499
+ }
5500
+ if (control && !results.has(result.controlId)) {
5501
+ results.set(result.controlId, result);
5502
+ }
5503
+ if (control && result.status === "not-applicable") {
5504
+ const validException = control.requirement === "optional" && control.notApplicable === true && hasText(result.reason);
5505
+ status = mergeStatus(status, validException ? "pass" : "incomplete");
5506
+ continue;
5507
+ }
5508
+ if (control) status = mergeStatus(status, resultStatus(result));
5509
+ }
5510
+ if (!hasRequired) status = mergeStatus(status, "incomplete");
5511
+ for (const control of controls.values()) {
5512
+ if (control.requirement !== "required") continue;
5513
+ const result = results.get(control.id);
5514
+ if (!result) {
5515
+ status = mergeStatus(status, "incomplete");
5516
+ continue;
5517
+ }
5518
+ if (result.status === "not-applicable") {
5519
+ status = mergeStatus(status, "incomplete");
5520
+ } else {
5521
+ status = mergeStatus(status, resultStatus(result));
5522
+ }
5523
+ }
5524
+ return { profile: requestedProfile, status };
5525
+ }
5526
+
5527
+ // src/lib/quality-receipt.ts
5528
+ import { createHash as createHash2 } from "crypto";
5529
+ var QUALITY_RECEIPT_NAMESPACE = "jorgex.quality.receipt";
5530
+ var QUALITY_RECEIPT_VERSION = 1;
5531
+ var COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
5532
+ var SHA256_PATTERN = /^[0-9a-f]{64}$/i;
5533
+ var MAX_EXCERPT_LENGTH = 512;
5534
+ var REDACTED = "[REDACTED]";
5535
+ var QUALITY_RESULT_STATUSES = [
5536
+ "pass",
5537
+ "fail",
5538
+ "incomplete",
5539
+ "not-applicable"
5540
+ ];
5541
+ var SENSITIVE_FLAG_PATTERN = /^(?:-{1,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))$/i;
5542
+ var SENSITIVE_ASSIGNMENT_PATTERN = /^(?:-{0,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))\s*[=:]\s*.+$/i;
5543
+ var SENSITIVE_OUTPUT_PATTERN = /((?:authorization\s*:\s*bearer\s+|bearer\s+))[^\s,;]+/gi;
5544
+ var SENSITIVE_KEY_VALUE_PATTERN = /((?:^|(?<=[^\w.-]))(?:-{0,2})(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\r\n,;]+)/gi;
5545
+ var SENSITIVE_SEPARATE_FLAG_PATTERN = /((?:^|(?<=[^\w.-]))-{1,2}(?:(?:[a-z][a-z0-9_.-]*[-_]?)?(?:access[-_]?token|api[-_]?key|auth(?:orization)?|client[-_]?secret|credential|pass(?:word|wd)?|refresh[-_]?token|secret|token))[ \t]+)(?:"[^"]*"|'[^']*'|[^\r\n,;]+)/gi;
5546
+ var SENSITIVE_STRUCTURED_VALUE_PATTERN = /((?:"|')?(?:token|password|api[-_]?key|access[-_]?token|_?auth(?:orization)?(?:[-_.]?token)?|aws[-_]?secret[-_]?access[-_]?key|private[-_]?key)(?:"|')?\s*:\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/gi;
5547
+ function isRecord2(value) {
5548
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5549
+ }
5550
+ function hasOwn(value, key) {
5551
+ return Object.prototype.hasOwnProperty.call(value, key);
5552
+ }
5553
+ function isDenseStringArray(value) {
5554
+ if (!Array.isArray(value)) return false;
5555
+ for (let index = 0; index < value.length; index += 1) {
5556
+ if (!hasOwn(value, String(index)) || typeof value[index] !== "string") return false;
5557
+ }
5558
+ return true;
5559
+ }
5560
+ function assertExactKeys(value, allowed, label) {
5561
+ const allowedKeys = new Set(allowed);
5562
+ const unexpected = Object.keys(value).find((key) => !allowedKeys.has(key));
5563
+ if (unexpected !== void 0) {
5564
+ throw new Error(`Unexpected ${label} field: ${unexpected}`);
5565
+ }
5566
+ }
5567
+ function requireRecord(value, label) {
5568
+ if (!isRecord2(value)) throw new Error(`Invalid ${label}`);
5569
+ return value;
5570
+ }
5571
+ function requireText(value, label) {
5572
+ if (typeof value !== "string" || value.trim() === "") {
5573
+ throw new Error(`Invalid ${label}`);
5574
+ }
5575
+ return value;
5576
+ }
5577
+ function isQualityProfile2(value) {
5578
+ return QUALITY_PROFILES.includes(value);
5579
+ }
5580
+ function isQualityReceiptResultStatus(value) {
5581
+ return QUALITY_RESULT_STATUSES.includes(value);
5582
+ }
5583
+ function requireSha(value, label, pattern) {
5584
+ const text2 = requireText(value, label);
5585
+ if (!pattern.test(text2)) throw new Error(`Invalid ${label}`);
5586
+ return text2;
5587
+ }
5588
+ function redactText(value) {
5589
+ return value.replace(SENSITIVE_STRUCTURED_VALUE_PATTERN, (_match, prefix, quotedValue) => {
5590
+ const quote = quotedValue[0];
5591
+ return `${prefix}${quote}${REDACTED}${quote}`;
5592
+ }).replace(SENSITIVE_OUTPUT_PATTERN, (_match, prefix) => {
5593
+ const normalizedPrefix = prefix.toLowerCase().startsWith("authorization") ? prefix.slice(0, prefix.toLowerCase().indexOf("bearer") + "bearer".length) : "Bearer ";
5594
+ return `${normalizedPrefix}${REDACTED}`;
5595
+ }).replace(SENSITIVE_KEY_VALUE_PATTERN, `$1${REDACTED}`).replace(SENSITIVE_SEPARATE_FLAG_PATTERN, `$1${REDACTED}`);
5596
+ }
5597
+ function redactAssignment(value) {
5598
+ const separator = value.indexOf("=") >= 0 ? "=" : ":";
5599
+ const name = value.slice(0, value.indexOf(separator));
5600
+ return `${name}${separator}${REDACTED}`;
5601
+ }
5602
+ function redactArgv(argv) {
5603
+ if (!isDenseStringArray(argv)) throw new Error("Invalid argv: sparse or non-string array");
5604
+ let redactNext = false;
5605
+ return argv.map((argument) => {
5606
+ if (redactNext) {
5607
+ redactNext = false;
5608
+ return REDACTED;
5609
+ }
5610
+ if (SENSITIVE_FLAG_PATTERN.test(argument)) {
5611
+ redactNext = true;
5612
+ return argument;
5613
+ }
5614
+ if (SENSITIVE_ASSIGNMENT_PATTERN.test(argument)) {
5615
+ return redactAssignment(argument);
5616
+ }
5617
+ return redactText(argument);
5618
+ });
5619
+ }
5620
+ function excerptFor(output) {
5621
+ const stdout = redactText(output.stdout);
5622
+ const stderr = redactText(output.stderr);
5623
+ const combined = [stdout, stderr].filter((part) => part !== "").join("\n");
5624
+ return Array.from(combined).slice(0, MAX_EXCERPT_LENGTH).join("");
5625
+ }
5626
+ function outputDigestFor(output) {
5627
+ const sanitized = {
5628
+ stdout: redactText(output.stdout),
5629
+ stderr: redactText(output.stderr)
5630
+ };
5631
+ return sha256(canonicalJson(sanitized));
5632
+ }
5633
+ function normalizeCommand(command) {
5634
+ return {
5635
+ commandId: command.commandId,
5636
+ executable: command.executable,
5637
+ argv: redactArgv(command.argv),
5638
+ exitCode: command.exitCode,
5639
+ durationMs: command.durationMs,
5640
+ excerpt: excerptFor(command.output),
5641
+ outputDigest: outputDigestFor(command.output)
5642
+ };
5643
+ }
5644
+ function normalizeResult(result) {
5645
+ if (result.status === "pass") {
5646
+ return {
5647
+ controlId: result.controlId,
5648
+ status: "pass",
5649
+ evidence: result.evidence,
5650
+ ...result.reason === void 0 ? {} : { reason: result.reason }
5651
+ };
5652
+ }
5653
+ return {
5654
+ controlId: result.controlId,
5655
+ status: result.status,
5656
+ ...result.evidence === void 0 ? {} : { evidence: result.evidence },
5657
+ ...result.reason === void 0 ? {} : { reason: result.reason }
5658
+ };
5659
+ }
5660
+ function normalizeProvenance(provenance) {
5661
+ return {
5662
+ issuer: provenance.issuer,
5663
+ executionId: provenance.executionId,
5664
+ evidenceLocator: provenance.evidenceLocator,
5665
+ evidenceDigest: provenance.evidenceDigest
5666
+ };
5667
+ }
5668
+ function canonicalValue(value) {
5669
+ if (value === null) return "null";
5670
+ switch (typeof value) {
5671
+ case "string": {
5672
+ const result = JSON.stringify(value);
5673
+ if (result === void 0) throw new Error("Unable to canonicalize string");
5674
+ return result;
5675
+ }
5676
+ case "boolean":
5677
+ return value ? "true" : "false";
5678
+ case "number": {
5679
+ if (!Number.isFinite(value)) throw new Error("Cannot canonicalize non-finite number");
5680
+ const result = JSON.stringify(value);
5681
+ if (result === void 0) throw new Error("Unable to canonicalize number");
5682
+ return result;
5683
+ }
5684
+ case "object": {
5685
+ if (Array.isArray(value)) {
5686
+ for (let index = 0; index < value.length; index += 1) {
5687
+ if (!hasOwn(value, String(index))) {
5688
+ throw new Error("Cannot canonicalize sparse array");
5689
+ }
5690
+ }
5691
+ return `[${value.map((item) => canonicalValue(item)).join(",")}]`;
5692
+ }
5693
+ const prototype = Object.getPrototypeOf(value);
5694
+ if (prototype !== Object.prototype && prototype !== null) {
5695
+ throw new Error("Cannot canonicalize non-plain object");
5696
+ }
5697
+ const object = value;
5698
+ return `{${Object.keys(object).sort().map((key) => {
5699
+ return `${JSON.stringify(key)}:${canonicalValue(object[key])}`;
5700
+ }).join(",")}}`;
5701
+ }
5702
+ default:
5703
+ throw new Error(`Cannot canonicalize ${typeof value}`);
5704
+ }
5705
+ }
5706
+ function canonicalJson(value) {
5707
+ return canonicalValue(value);
5708
+ }
5709
+ function sha256(value) {
5710
+ return createHash2("sha256").update(value, "utf8").digest("hex");
5711
+ }
5712
+ function validateIdentity(value, expected) {
5713
+ const identity = requireRecord(value, "identity");
5714
+ assertExactKeys(identity, ["profile", "baseSha", "headSha", "policyDigest"], "identity");
5715
+ const profile = requireText(identity.profile, "identity.profile");
5716
+ if (!isQualityProfile2(profile)) throw new Error(`Invalid identity.profile: ${profile}`);
5717
+ const baseSha = requireSha(identity.baseSha, "identity.baseSha", COMMIT_SHA_PATTERN);
5718
+ const headSha = requireSha(identity.headSha, "identity.headSha", COMMIT_SHA_PATTERN);
5719
+ const policyDigest = requireSha(identity.policyDigest, "identity.policyDigest", SHA256_PATTERN);
5720
+ const actual = { profile, baseSha, headSha, policyDigest };
5721
+ if (expected !== void 0) {
5722
+ for (const field of ["profile", "baseSha", "headSha", "policyDigest"]) {
5723
+ if (actual[field] !== expected[field]) {
5724
+ throw new Error(`Quality receipt identity mismatch: ${field}`);
5725
+ }
5726
+ }
5727
+ }
5728
+ return actual;
5729
+ }
5730
+ function validateProvenance(value) {
5731
+ const provenance = requireRecord(value, "provenance");
5732
+ assertExactKeys(provenance, ["issuer", "executionId", "evidenceLocator", "evidenceDigest"], "provenance");
5733
+ const issuer = requireText(provenance.issuer, "provenance.issuer");
5734
+ const executionId = requireText(provenance.executionId, "provenance.executionId");
5735
+ const evidenceLocator = requireText(provenance.evidenceLocator, "provenance.evidenceLocator");
5736
+ if (!/^https?:\/\/\S+$/.test(evidenceLocator)) {
5737
+ throw new Error("Invalid provenance.evidenceLocator");
5738
+ }
5739
+ try {
5740
+ const parsed = new URL(evidenceLocator);
5741
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
5742
+ throw new Error("unsupported locator protocol");
5743
+ }
5744
+ } catch {
5745
+ throw new Error("Invalid provenance.evidenceLocator");
5746
+ }
5747
+ const evidenceDigest = requireSha(provenance.evidenceDigest, "provenance.evidenceDigest", SHA256_PATTERN);
5748
+ return { issuer, executionId, evidenceLocator, evidenceDigest };
5749
+ }
5750
+ function validateCommand(value, index) {
5751
+ const command = requireRecord(value, `commands[${index}]`);
5752
+ assertExactKeys(
5753
+ command,
5754
+ ["commandId", "executable", "argv", "exitCode", "durationMs", "excerpt", "outputDigest"],
5755
+ `commands[${index}]`
5756
+ );
5757
+ const commandId = requireText(command.commandId, `commands[${index}].commandId`);
5758
+ const executable = requireText(command.executable, `commands[${index}].executable`);
5759
+ if (!isDenseStringArray(command.argv)) {
5760
+ throw new Error(`Invalid commands[${index}].argv`);
5761
+ }
5762
+ if (typeof command.exitCode !== "number" || !Number.isInteger(command.exitCode)) {
5763
+ throw new Error(`Invalid commands[${index}].exitCode`);
5764
+ }
5765
+ if (typeof command.durationMs !== "number" || !Number.isFinite(command.durationMs) || command.durationMs < 0) {
5766
+ throw new Error(`Invalid commands[${index}].durationMs`);
5767
+ }
5768
+ if (!hasOwn(command, "excerpt") || typeof command.excerpt !== "string") {
5769
+ throw new Error(`Invalid commands[${index}].excerpt`);
5770
+ }
5771
+ const excerpt = command.excerpt;
5772
+ if (Array.from(excerpt).length > MAX_EXCERPT_LENGTH) throw new Error(`Invalid commands[${index}].excerpt`);
5773
+ if (redactText(excerpt) !== excerpt) {
5774
+ throw new Error(`Invalid commands[${index}].excerpt: contains an unredacted secret`);
5775
+ }
5776
+ const argv = [...command.argv];
5777
+ const sanitizedArgv = redactArgv(argv);
5778
+ if (sanitizedArgv.some((argument, argumentIndex) => argument !== argv[argumentIndex])) {
5779
+ throw new Error(`Invalid commands[${index}].argv: contains an unredacted secret`);
5780
+ }
5781
+ const outputDigest = requireSha(command.outputDigest, `commands[${index}].outputDigest`, SHA256_PATTERN);
5782
+ return {
5783
+ commandId,
5784
+ executable,
5785
+ argv,
5786
+ exitCode: command.exitCode,
5787
+ durationMs: command.durationMs,
5788
+ excerpt,
5789
+ outputDigest
5790
+ };
5791
+ }
5792
+ function validateResult(value, index) {
5793
+ const result = requireRecord(value, `results[${index}]`);
5794
+ assertExactKeys(result, ["controlId", "status", "evidence", "reason"], `results[${index}]`);
5795
+ const controlId = requireText(result.controlId, `results[${index}].controlId`);
5796
+ const status = result.status;
5797
+ if (typeof status !== "string" || !isQualityReceiptResultStatus(status)) {
5798
+ throw new Error(`Invalid results[${index}].status`);
5799
+ }
5800
+ let evidence;
5801
+ if (hasOwn(result, "evidence")) {
5802
+ if (typeof result.evidence !== "string") {
5803
+ throw new Error(`Invalid results[${index}].evidence`);
5804
+ }
5805
+ evidence = result.evidence;
5806
+ }
5807
+ let reason;
5808
+ if (hasOwn(result, "reason")) {
5809
+ if (typeof result.reason !== "string") {
5810
+ throw new Error(`Invalid results[${index}].reason`);
5811
+ }
5812
+ reason = result.reason;
5813
+ }
5814
+ if (status === "pass") {
5815
+ if (evidence === void 0 || evidence.trim() === "") {
5816
+ throw new Error(`Invalid results[${index}].evidence: pass requires evidence`);
5817
+ }
5818
+ return {
5819
+ controlId,
5820
+ status,
5821
+ evidence,
5822
+ ...reason === void 0 ? {} : { reason }
5823
+ };
5824
+ }
5825
+ return {
5826
+ controlId,
5827
+ status,
5828
+ ...evidence === void 0 ? {} : { evidence },
5829
+ ...reason === void 0 ? {} : { reason }
5830
+ };
5831
+ }
5832
+ function validateQualityReceipt(value, expectedIdentity) {
5833
+ const receipt = requireRecord(value, "quality receipt");
5834
+ if (receipt.namespace !== QUALITY_RECEIPT_NAMESPACE) {
5835
+ throw new Error(`Invalid quality receipt namespace: ${String(receipt.namespace)}`);
5836
+ }
5837
+ if (receipt.version !== QUALITY_RECEIPT_VERSION) {
5838
+ throw new Error(`Unsupported quality receipt version: ${String(receipt.version)}`);
5839
+ }
5840
+ assertExactKeys(receipt, ["namespace", "version", "authority", "identity", "commands", "results", "provenance"], "receipt");
5841
+ if (receipt.authority !== "local" && receipt.authority !== "enforced") {
5842
+ throw new Error(`Invalid quality receipt authority: ${String(receipt.authority)}`);
5843
+ }
5844
+ const identity = validateIdentity(receipt.identity, expectedIdentity);
5845
+ if (!Array.isArray(receipt.commands)) throw new Error("Invalid quality receipt commands");
5846
+ if (!Array.isArray(receipt.results)) throw new Error("Invalid quality receipt results");
5847
+ for (let index = 0; index < receipt.commands.length; index += 1) {
5848
+ validateCommand(receipt.commands[index], index);
5849
+ }
5850
+ for (let index = 0; index < receipt.results.length; index += 1) {
5851
+ validateResult(receipt.results[index], index);
5852
+ }
5853
+ const hasProvenance = hasOwn(receipt, "provenance");
5854
+ if (receipt.authority === "enforced" && (!hasProvenance || receipt.provenance === void 0)) {
5855
+ throw new Error("Enforced quality receipts require provenance");
5856
+ }
5857
+ if (hasProvenance) {
5858
+ if (receipt.provenance === void 0) throw new Error("Invalid quality receipt provenance");
5859
+ validateProvenance(receipt.provenance);
5860
+ }
5861
+ void identity;
5862
+ }
5863
+ function createQualityReceipt(input) {
5864
+ const authority = input.authority;
5865
+ if (authority !== "local" && authority !== "enforced") {
5866
+ throw new Error(`Invalid quality receipt authority: ${String(authority)}`);
5867
+ }
5868
+ if (input.authority === "enforced" && input.provenance === void 0) {
5869
+ throw new Error("Enforced quality receipts require provenance");
5870
+ }
5871
+ const base = {
5872
+ namespace: QUALITY_RECEIPT_NAMESPACE,
5873
+ version: QUALITY_RECEIPT_VERSION,
5874
+ identity: {
5875
+ profile: input.identity.profile,
5876
+ baseSha: input.identity.baseSha,
5877
+ headSha: input.identity.headSha,
5878
+ policyDigest: input.identity.policyDigest
5879
+ },
5880
+ commands: input.commands.map(normalizeCommand),
5881
+ results: input.results.map(normalizeResult)
5882
+ };
5883
+ let receipt;
5884
+ if (input.authority === "enforced") {
5885
+ const provenance = input.provenance;
5886
+ if (provenance === void 0) throw new Error("Enforced quality receipts require provenance");
5887
+ receipt = {
5888
+ ...base,
5889
+ authority: "enforced",
5890
+ provenance: normalizeProvenance(provenance)
5891
+ };
5892
+ } else {
5893
+ receipt = {
5894
+ ...base,
5895
+ authority: "local",
5896
+ ...input.provenance === void 0 ? {} : { provenance: normalizeProvenance(input.provenance) }
5897
+ };
5898
+ }
5899
+ validateQualityReceipt(receipt);
5900
+ return receipt;
5901
+ }
5902
+ function serializeQualityReceipt(receipt) {
5903
+ validateQualityReceipt(receipt);
5904
+ return canonicalJson(receipt);
5905
+ }
5906
+
5907
+ // src/lib/quality-runner.ts
5908
+ function normalizeLimit(value, name) {
5909
+ if (value === void 0) return void 0;
5910
+ if (!Number.isSafeInteger(value) || value < 0) {
5911
+ throw new Error(`${name} must be a non-negative safe integer`);
5912
+ }
5913
+ return value;
5914
+ }
5915
+ var MAX_NODE_TIMEOUT_MS = 2147483647;
5916
+ var TERMINATION_GRACE_MS = 250;
5917
+ var TASKKILL_TIMEOUT_MS = 2e3;
5918
+ function normalizeTimeout(value) {
5919
+ const timeoutMs = normalizeLimit(value, "timeoutMs");
5920
+ if (timeoutMs !== void 0 && timeoutMs > MAX_NODE_TIMEOUT_MS) {
5921
+ throw new Error(`timeoutMs must not exceed ${MAX_NODE_TIMEOUT_MS}`);
5922
+ }
5923
+ return timeoutMs;
5924
+ }
5925
+ var COMMIT_SHA_PATTERN2 = /^[0-9a-f]{40}$/i;
5926
+ function isRecord3(value) {
5927
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5928
+ }
5929
+ function hasText2(value) {
5930
+ return typeof value === "string" && value.trim() !== "";
5931
+ }
5932
+ function isDenseStringArray2(value) {
5933
+ if (!Array.isArray(value)) return false;
5934
+ for (let index = 0; index < value.length; index += 1) {
5935
+ if (!Object.prototype.hasOwnProperty.call(value, String(index)) || typeof value[index] !== "string") return false;
5936
+ }
5937
+ return true;
5938
+ }
5939
+ function isNonNegativeSafeInteger(value) {
5940
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
5941
+ }
5942
+ function isQualityProfile3(value) {
5943
+ return typeof value === "string" && QUALITY_PROFILES.includes(value);
5944
+ }
5945
+ function assertValidEnvironment(value, label) {
5946
+ if (!isRecord3(value)) throw new Error(`Invalid ${label}`);
5947
+ for (const [key, entry] of Object.entries(value)) {
5948
+ if (typeof entry !== "string") throw new Error(`Invalid ${label}.${key}`);
5949
+ }
5950
+ }
5951
+ function assertQualityPlanInput(value) {
5952
+ if (!isRecord3(value)) throw new Error("Invalid quality plan");
5953
+ if (!isRecord3(value.identity)) throw new Error("Invalid quality plan identity");
5954
+ if (typeof value.identity.baseSha !== "string" || !COMMIT_SHA_PATTERN2.test(value.identity.baseSha)) {
5955
+ throw new Error("Invalid quality plan identity.baseSha");
5956
+ }
5957
+ if (typeof value.identity.headSha !== "string" || !COMMIT_SHA_PATTERN2.test(value.identity.headSha)) {
5958
+ throw new Error("Invalid quality plan identity.headSha");
5959
+ }
5960
+ if (!isQualityProfile3(value.profile)) throw new Error("Invalid quality plan profile");
5961
+ if (!Array.isArray(value.controls)) throw new Error("Invalid quality plan controls");
5962
+ const controlIds = /* @__PURE__ */ new Set();
5963
+ for (let index = 0; index < value.controls.length; index += 1) {
5964
+ const control = value.controls[index];
5965
+ if (!isRecord3(control) || !hasText2(control.id) || control.requirement !== "required" && control.requirement !== "optional") {
5966
+ throw new Error(`Invalid quality plan controls[${index}]`);
5967
+ }
5968
+ if (controlIds.has(control.id)) {
5969
+ throw new Error(`Duplicate quality plan control id: ${control.id}`);
5970
+ }
5971
+ controlIds.add(control.id);
5972
+ if (Object.prototype.hasOwnProperty.call(control, "notApplicable") && typeof control.notApplicable !== "boolean") {
5973
+ throw new Error(`Invalid quality plan controls[${index}].notApplicable`);
5974
+ }
5975
+ }
5976
+ if (!Array.isArray(value.commands)) throw new Error("Invalid quality plan commands");
5977
+ const commandIds = /* @__PURE__ */ new Set();
5978
+ const commandControlIds = /* @__PURE__ */ new Set();
5979
+ for (let index = 0; index < value.commands.length; index += 1) {
5980
+ const command = value.commands[index];
5981
+ if (!isRecord3(command)) throw new Error(`Invalid quality plan commands[${index}]`);
5982
+ const timeoutMs = command.timeoutMs;
5983
+ if (!hasText2(command.controlId) || !hasText2(command.commandId) || !hasText2(command.executable) || !isDenseStringArray2(command.argv) || !isNonNegativeSafeInteger(timeoutMs)) {
5984
+ throw new Error(`Invalid quality plan commands[${index}]`);
5985
+ }
5986
+ if (timeoutMs > MAX_NODE_TIMEOUT_MS) {
5987
+ throw new Error(`Invalid quality plan commands[${index}].timeoutMs: maximum is ${MAX_NODE_TIMEOUT_MS}`);
5988
+ }
5989
+ if (!controlIds.has(command.controlId)) {
5990
+ throw new Error(`Unknown quality plan command control id: ${command.controlId}`);
5991
+ }
5992
+ if (commandIds.has(command.commandId)) {
5993
+ throw new Error(`Duplicate quality plan command id: ${command.commandId}`);
5994
+ }
5995
+ if (commandControlIds.has(command.controlId)) {
5996
+ throw new Error(`Multiple quality plan commands for control id: ${command.controlId}`);
5997
+ }
5998
+ commandIds.add(command.commandId);
5999
+ commandControlIds.add(command.controlId);
6000
+ if (command.maxOutputBytes !== void 0 && !isNonNegativeSafeInteger(command.maxOutputBytes)) {
6001
+ throw new Error(`Invalid quality plan commands[${index}].maxOutputBytes`);
6002
+ }
6003
+ if (command.env !== void 0) assertValidEnvironment(command.env, `quality plan commands[${index}].env`);
6004
+ }
6005
+ }
6006
+ function appendOutput(captured, stream, chunk, maxOutputBytes) {
6007
+ if (maxOutputBytes === void 0) {
6008
+ captured[stream].push(chunk);
6009
+ return false;
6010
+ }
6011
+ const remaining = maxOutputBytes - captured.bytes;
6012
+ if (remaining <= 0) return chunk.length > 0;
6013
+ if (chunk.length > remaining) {
6014
+ captured[stream].push(chunk.subarray(0, remaining));
6015
+ captured.bytes += remaining;
6016
+ return true;
6017
+ }
6018
+ captured[stream].push(chunk);
6019
+ captured.bytes += chunk.length;
6020
+ return captured.bytes >= maxOutputBytes;
6021
+ }
6022
+ function decodeOutput(chunks, maxOutputBytes) {
6023
+ const output = Buffer.concat(chunks);
6024
+ if (maxOutputBytes === void 0 || output.length === 0) return output.toString("utf8");
6025
+ let end = output.length;
6026
+ let decoded = output.subarray(0, end).toString("utf8");
6027
+ while (Buffer.byteLength(decoded, "utf8") > maxOutputBytes && end > 0) {
6028
+ end -= 1;
6029
+ decoded = output.subarray(0, end).toString("utf8");
6030
+ }
6031
+ return decoded;
6032
+ }
6033
+ function killProcessTree(child) {
6034
+ const pid = child.pid;
6035
+ if (pid === void 0) return;
6036
+ if (process.platform === "win32") {
6037
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
6038
+ const taskkill = path33.join(systemRoot, "System32", "taskkill.exe");
6039
+ const result = spawnSync2(taskkill, ["/pid", String(pid), "/t", "/f"], {
6040
+ shell: false,
6041
+ stdio: "ignore",
6042
+ timeout: TASKKILL_TIMEOUT_MS,
6043
+ windowsHide: true
6044
+ });
6045
+ if (result.error !== void 0 || result.status !== 0) {
6046
+ try {
6047
+ child.kill("SIGKILL");
6048
+ } catch {
6049
+ }
6050
+ }
6051
+ return;
6052
+ }
6053
+ try {
6054
+ process.kill(-pid, "SIGKILL");
6055
+ } catch {
6056
+ try {
6057
+ child.kill("SIGKILL");
6058
+ } catch {
6059
+ }
6060
+ }
6061
+ }
6062
+ function resultFromCaptured(input, status, exitCode, startedAt, captured, maxOutputBytes, reason) {
6063
+ return {
6064
+ commandId: input.commandId,
6065
+ status,
6066
+ exitCode,
6067
+ durationMs: Math.max(0, Date.now() - startedAt),
6068
+ output: {
6069
+ stdout: decodeOutput(captured.stdout, maxOutputBytes),
6070
+ stderr: decodeOutput(captured.stderr, maxOutputBytes)
6071
+ },
6072
+ ...reason === void 0 ? {} : { reason }
6073
+ };
6074
+ }
6075
+ async function runQualityCommand(input, deps = { terminate: killProcessTree }) {
6076
+ const timeoutMs = normalizeTimeout(input.timeoutMs);
6077
+ const maxOutputBytes = normalizeLimit(input.maxOutputBytes, "maxOutputBytes");
6078
+ if (timeoutMs === void 0) throw new Error("timeoutMs is required");
6079
+ const planned = planDetectedBinCommand(input.executable, [...input.argv]);
6080
+ const startedAt = Date.now();
6081
+ const captured = { stdout: [], stderr: [], bytes: 0 };
6082
+ if (planned === null) {
6083
+ return resultFromCaptured(input, "error", null, startedAt, captured, maxOutputBytes, "unsafe-command");
6084
+ }
6085
+ return new Promise((resolve) => {
6086
+ let settled = false;
6087
+ let terminationReason;
6088
+ let spawnError;
6089
+ let timeout;
6090
+ let terminationDeadline;
6091
+ let child;
6092
+ let cleanedUp = false;
6093
+ let onStdoutData = (_chunk) => {
6094
+ };
6095
+ let onStderrData = (_chunk) => {
6096
+ };
6097
+ let onError = (_error) => {
6098
+ };
6099
+ const onErrorSink = () => {
6100
+ };
6101
+ let onClose = (_exitCode) => {
6102
+ };
6103
+ const cleanupChild = () => {
6104
+ if (cleanedUp || child === void 0) return;
6105
+ cleanedUp = true;
6106
+ child.removeListener("close", onClose);
6107
+ child.removeListener("error", onError);
6108
+ child.on("error", onErrorSink);
6109
+ child.stdout?.removeListener("data", onStdoutData);
6110
+ child.stderr?.removeListener("data", onStderrData);
6111
+ child.stdout?.destroy();
6112
+ child.stderr?.destroy();
6113
+ child.unref();
6114
+ };
6115
+ const settle = (result) => {
6116
+ if (settled) return;
6117
+ settled = true;
6118
+ if (timeout !== void 0) clearTimeout(timeout);
6119
+ if (terminationDeadline !== void 0) clearTimeout(terminationDeadline);
6120
+ cleanupChild();
6121
+ resolve(result);
6122
+ };
6123
+ try {
6124
+ child = spawn(planned.command, planned.args, {
6125
+ detached: true,
6126
+ env: input.env === void 0 ? {} : { ...input.env },
6127
+ shell: false,
6128
+ stdio: ["ignore", "pipe", "pipe"],
6129
+ windowsHide: true
6130
+ });
6131
+ } catch (error) {
6132
+ settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
6133
+ return;
6134
+ }
6135
+ const stopFor = (reason) => {
6136
+ if (settled || terminationReason !== void 0) return;
6137
+ terminationReason = reason;
6138
+ try {
6139
+ deps.terminate(child);
6140
+ } catch {
6141
+ }
6142
+ if (settled) return;
6143
+ terminationDeadline = setTimeout(() => {
6144
+ try {
6145
+ deps.terminate(child);
6146
+ } catch {
6147
+ }
6148
+ cleanupChild();
6149
+ settle(resultFromCaptured(input, "error", null, startedAt, captured, maxOutputBytes, "termination-timeout"));
6150
+ }, TERMINATION_GRACE_MS);
6151
+ };
6152
+ const onOutput = (stream, chunk) => {
6153
+ if (settled || appendOutput(captured, stream, chunk, maxOutputBytes)) {
6154
+ if (!settled && maxOutputBytes !== void 0) stopFor("output-limit");
6155
+ }
6156
+ };
6157
+ onStdoutData = (chunk) => onOutput("stdout", chunk);
6158
+ onStderrData = (chunk) => onOutput("stderr", chunk);
6159
+ onError = (error) => {
6160
+ spawnError = error;
6161
+ if (terminationReason === void 0 && (error.code === "ENOENT" || error.code === "EACCES")) {
6162
+ settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
6163
+ }
6164
+ };
6165
+ onClose = (exitCode) => {
6166
+ if (terminationReason === "timeout") {
6167
+ settle(resultFromCaptured(input, "timeout", exitCode, startedAt, captured, maxOutputBytes));
6168
+ return;
6169
+ }
6170
+ if (terminationReason === "output-limit") {
6171
+ settle(resultFromCaptured(input, "error", exitCode, startedAt, captured, maxOutputBytes, "output-limit"));
6172
+ return;
6173
+ }
6174
+ if (spawnError !== void 0) {
6175
+ settle(resultFromCaptured(input, "unavailable", null, startedAt, captured, maxOutputBytes, "spawn-error"));
6176
+ return;
6177
+ }
6178
+ settle(resultFromCaptured(
6179
+ input,
6180
+ exitCode === 0 ? "pass" : "fail",
6181
+ exitCode,
6182
+ startedAt,
6183
+ captured,
6184
+ maxOutputBytes
6185
+ ));
6186
+ };
6187
+ child.stdout?.on("data", onStdoutData);
6188
+ child.stderr?.on("data", onStderrData);
6189
+ child.once("error", onError);
6190
+ child.once("close", onClose);
6191
+ timeout = setTimeout(() => stopFor("timeout"), timeoutMs);
6192
+ });
6193
+ }
6194
+ function resultEvidence(result) {
6195
+ const exitCode = result.exitCode === null ? "none" : String(result.exitCode);
6196
+ return `exit=${exitCode}; status=${result.status}; durationMs=${result.durationMs}`;
6197
+ }
6198
+ function resultReason(result) {
6199
+ switch (result.status) {
6200
+ case "pass":
6201
+ return void 0;
6202
+ case "fail":
6203
+ return "nonzero-exit";
6204
+ case "timeout":
6205
+ return "timeout";
6206
+ case "unavailable":
6207
+ return result.reason ?? "unavailable";
6208
+ case "error":
6209
+ return result.reason ?? "error";
6210
+ }
6211
+ }
6212
+ function receiptResultFor(controlId, result) {
6213
+ const evidence = resultEvidence(result);
6214
+ if (result.status === "pass") return { controlId, status: "pass", evidence };
6215
+ return {
6216
+ controlId,
6217
+ status: result.status === "fail" ? "fail" : "incomplete",
6218
+ evidence,
6219
+ reason: resultReason(result)
6220
+ };
6221
+ }
6222
+ function missingRequiredResults(controls, commands) {
6223
+ const commandControlIds = new Set(commands.map((command) => command.controlId));
6224
+ const missing = /* @__PURE__ */ new Set();
6225
+ for (const control of controls) {
6226
+ if (control.requirement !== "required" || !hasText2(control.id) || commandControlIds.has(control.id)) continue;
6227
+ missing.add(control.id);
6228
+ }
6229
+ return [...missing].map((controlId) => ({
6230
+ controlId,
6231
+ status: "incomplete",
6232
+ evidence: "required control has no declared command",
6233
+ reason: "required-control-missing"
6234
+ }));
6235
+ }
6236
+ function receiptCommandFor(command, result) {
6237
+ return {
6238
+ commandId: command.commandId,
6239
+ executable: command.executable,
6240
+ argv: command.argv,
6241
+ exitCode: result.exitCode ?? -1,
6242
+ durationMs: result.durationMs,
6243
+ output: result.output
6244
+ };
6245
+ }
6246
+ async function runQualityPlan(input) {
6247
+ assertQualityPlanInput(input);
6248
+ const policy = { controls: input.controls, profile: input.profile };
6249
+ const policyDigest = sha256(canonicalJson(policy));
6250
+ const identity = {
6251
+ profile: input.profile,
6252
+ baseSha: input.identity.baseSha,
6253
+ headSha: input.identity.headSha,
6254
+ policyDigest
6255
+ };
6256
+ const commands = [];
6257
+ const results = [];
6258
+ for (const command of input.commands) {
6259
+ const result = await runQualityCommand(command);
6260
+ commands.push(receiptCommandFor(command, result));
6261
+ results.push(receiptResultFor(command.controlId, result));
6262
+ }
6263
+ results.push(...missingRequiredResults(input.controls, input.commands));
6264
+ const evaluation = evaluateQualityPolicy({
6265
+ profile: input.profile,
6266
+ controls: input.controls,
6267
+ results
6268
+ });
6269
+ const receipt = createQualityReceipt({
6270
+ authority: "local",
6271
+ identity,
6272
+ commands,
6273
+ results
6274
+ });
6275
+ validateQualityReceipt(receipt, identity);
6276
+ return { evaluation, receipt };
6277
+ }
6278
+
5446
6279
  // src/cli.ts
5447
6280
  var VERSION = readPackageVersion();
5448
- var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
6281
+ var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall", "quality"];
6282
+ var QUALITY_REJECTED_VALUE_FLAGS = /* @__PURE__ */ new Set([
6283
+ "--agents",
6284
+ "-a",
6285
+ "--target-dir",
6286
+ "--mode",
6287
+ "--subagent-concurrency"
6288
+ ]);
5449
6289
  async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
5450
6290
  if (!runtimes.includes("opencode") || loadModelMap().opencode) return true;
5451
6291
  const canPrompt = command === "install" && !flags.yes && !flags.dryRun && process.stdout.isTTY;
@@ -5458,7 +6298,7 @@ async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
5458
6298
  );
5459
6299
  return false;
5460
6300
  }
5461
- function parseFlags(args) {
6301
+ function parseFlags(args, allowReceipt = false) {
5462
6302
  const flags = {
5463
6303
  agents: [],
5464
6304
  dryRun: false,
@@ -5474,6 +6314,7 @@ function parseFlags(args) {
5474
6314
  removePlaywright: false,
5475
6315
  devtools: false,
5476
6316
  noDevtools: false,
6317
+ receipt: void 0,
5477
6318
  positional: [],
5478
6319
  unknownFlags: []
5479
6320
  };
@@ -5484,6 +6325,26 @@ function parseFlags(args) {
5484
6325
  };
5485
6326
  for (let i = 0; i < args.length; i++) {
5486
6327
  const arg = args[i];
6328
+ if (allowReceipt) {
6329
+ if (arg === "--help" || arg === "-h") flags.help = true;
6330
+ else if (arg === "--version" || arg === "-v") flags.version = true;
6331
+ else if (arg === "--receipt") {
6332
+ const [value, nextIndex] = readValue(i);
6333
+ if (value === void 0 || value === "") flags.unknownFlags.push(arg);
6334
+ else flags.receipt = value;
6335
+ i = nextIndex;
6336
+ } else if (arg.startsWith("--receipt=")) {
6337
+ const value = arg.slice(10);
6338
+ if (value === "") flags.unknownFlags.push(arg);
6339
+ else flags.receipt = value;
6340
+ } else if (QUALITY_REJECTED_VALUE_FLAGS.has(arg)) {
6341
+ flags.unknownFlags.push(arg);
6342
+ const [, nextIndex] = readValue(i);
6343
+ i = nextIndex;
6344
+ } else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
6345
+ else flags.positional.push(arg);
6346
+ continue;
6347
+ }
5487
6348
  if (arg === "--agents" || arg === "-a") {
5488
6349
  const [value, nextIndex] = readValue(i);
5489
6350
  flags.agents = (value ?? "").split(",").filter(Boolean);
@@ -5635,7 +6496,7 @@ function parseCliArgs(argv) {
5635
6496
  };
5636
6497
  }
5637
6498
  const command = isCommand ? first ?? "install" : "install";
5638
- const flags = parseFlags(isCommand ? rest : argv);
6499
+ const flags = parseFlags(isCommand ? rest : argv, command === "quality");
5639
6500
  if (first === "--help" || first === "-h" || flags.help) return { action: "help", command, flags };
5640
6501
  if (first === "--version" || first === "-v" || flags.version) return { action: "version", command, flags };
5641
6502
  if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
@@ -5727,8 +6588,9 @@ Comandos:
5727
6588
  doctor Estado: Engram, drift de config, hooks de Codex, key de context7
5728
6589
  restore --list para ver backups \xB7 'restore <id>' para restaurar
5729
6590
  uninstall Retira SOLO lo gestionado por el stack (con backup).
5730
- Engram se CONSERVA por defecto (memorias, binario y registro);
5731
- desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
6591
+ Engram se CONSERVA por defecto (memorias, binario y registro);
6592
+ desregistrarlo exige --remove-engram o el s\xED expl\xEDcito
6593
+ quality Ejecuta un plan JSON expl\xEDcito y emite un receipt local
5732
6594
 
5733
6595
  Opciones:
5734
6596
  --agents, -a opencode,claude-code,codex,pi Runtimes destino (default: detectados)
@@ -5744,6 +6606,7 @@ Opciones:
5744
6606
  memorias y binario quedan intactos igualmente
5745
6607
  --remove-playwright (uninstall) retira solo el paquete global de Playwright;
5746
6608
  nunca perfiles, cach\xE9 ni navegadores
6609
+ --receipt <path> (quality) escribe el receipt en ese path de forma at\xF3mica
5747
6610
 
5748
6611
  Ver PRD.md para el dise\xF1o completo.`);
5749
6612
  }
@@ -5770,12 +6633,43 @@ Flags disponibles: jorgex-stack --help`
5770
6633
  return;
5771
6634
  }
5772
6635
  const { command, flags } = parsed;
5773
- if (flags.targetDir !== void 0 && flags.agents.length !== 1) {
6636
+ if (command !== "quality" && flags.targetDir !== void 0 && flags.agents.length !== 1) {
5774
6637
  console.error("--target-dir requiere exactamente un runtime en --agents.");
5775
6638
  process.exitCode = 1;
5776
6639
  return;
5777
6640
  }
5778
6641
  switch (command) {
6642
+ case "quality": {
6643
+ if (flags.targetDir !== void 0 || flags.agents.length > 0 || flags.dryRun || flags.yes || flags.mode !== void 0 || flags.subagentConcurrency !== void 0 || flags.list || flags.check || flags.removeEngram || flags.playwright || flags.removePlaywright || flags.devtools || flags.noDevtools) {
6644
+ console.error("quality solo admite <plan.json> y, opcionalmente, --receipt <path>.");
6645
+ process.exitCode = 1;
6646
+ return;
6647
+ }
6648
+ if (flags.positional.length !== 1) {
6649
+ console.error("Uso: jorgex-stack quality <plan.json> [--receipt <path>]");
6650
+ process.exitCode = 1;
6651
+ return;
6652
+ }
6653
+ if (flags.receipt !== void 0 && flags.receipt.trim() === "") {
6654
+ console.error("--receipt requiere un path no vac\xEDo.");
6655
+ process.exitCode = 1;
6656
+ return;
6657
+ }
6658
+ try {
6659
+ const plan = JSON.parse(fs24.readFileSync(flags.positional[0], "utf8"));
6660
+ const result = await runQualityPlan(plan);
6661
+ const serialized = serializeQualityReceipt(result.receipt);
6662
+ if (flags.receipt === void 0) process.stdout.write(`${serialized}
6663
+ `);
6664
+ else writeText(flags.receipt, `${serialized}
6665
+ `);
6666
+ process.exitCode = result.evaluation.status === "pass" ? 0 : 1;
6667
+ } catch (error) {
6668
+ console.error(error instanceof Error ? error.message : String(error));
6669
+ process.exitCode = 1;
6670
+ }
6671
+ return;
6672
+ }
5779
6673
  case "install":
5780
6674
  case "sync": {
5781
6675
  const runtimes = await resolveRuntimes(flags, command === "install");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",