multimodel-dev-os 3.1.0 → 3.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.
- package/.ai/policies/registry-policy.yaml +29 -1
- package/.ai/registries/trusted-keys.yaml +12 -0
- package/.ai/schema/registry-manifest.schema.json +31 -2
- package/.ai/schema/registry-policy.schema.json +37 -1
- package/.ai/schema/trusted-keys.schema.json +69 -0
- package/AGENTS.md +22 -26
- package/MEMORY.md +34 -11
- package/README.md +2 -1
- package/RUNBOOK.md +28 -36
- package/TASKS.md +15 -5
- package/bin/multimodel-dev-os.js +1366 -548
- package/docs/.vitepress/config.js +3 -1
- package/docs/architecture.md +3 -1
- package/docs/index.md +5 -5
- package/docs/npm-publishing.md +5 -5
- package/docs/package-safety.md +17 -0
- package/docs/public/llms-full.txt +5 -1
- package/docs/public/llms.txt +6 -1
- package/docs/public/sitemap.xml +15 -0
- package/docs/registry-policy.md +29 -1
- package/docs/registry-security.md +73 -6
- package/docs/registry-signing.md +70 -0
- package/docs/registry-sync.md +5 -2
- package/docs/registry-trust-store.md +66 -0
- package/docs/release-policy.md +6 -5
- package/docs/security-threat-model.md +96 -0
- package/docs/testing.md +25 -2
- package/docs/trusted-registries.md +1 -1
- package/docs/v3-roadmap.md +17 -6
- package/docs/v3.5.0-readiness.md +46 -0
- package/package.json +5 -2
- package/scripts/build-cli.js +45 -3
- package/scripts/check-build-fresh.js +52 -0
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/verify.js +327 -14
- package/scripts/verify.sh +10 -0
- package/src/catalog/loader.js +117 -0
- package/src/cli/args.js +118 -0
- package/src/cli/help.js +60 -0
- package/src/cli/main.js +6263 -0
- package/src/core/globals.js +52 -0
- package/src/core/hashes.js +15 -0
- package/src/core/policy.js +44 -0
- package/src/core/security.js +61 -0
- package/src/core/yaml.js +136 -0
- package/src/plugin/manifest.js +95 -0
- package/src/registry/provenance.js +114 -0
- package/src/registry/signing.js +392 -0
- package/src/registry/sources.js +40 -0
- package/src/registry/trust-store.js +41 -0
- package/src/registry/validation.js +45 -0
- package/src/registry/verdict.js +51 -0
- package/tests/README.md +37 -0
- package/tests/fixtures/README.md +22 -0
- package/tests/fixtures/custom-template-example/README.md +10 -0
- package/tests/fixtures/proposals/approved-append-line.md +28 -0
- package/tests/fixtures/proposals/approved-create-file.md +29 -0
- package/tests/fixtures/proposals/approved-replace-text.md +30 -0
- package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
- package/tests/fixtures/proposals/no-operations.md +18 -0
- package/tests/fixtures/proposals/path-traversal.md +29 -0
- package/tests/fixtures/proposals/pending-proposal.md +29 -0
- package/tests/fixtures/proposals/protected-path.md +29 -0
- package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
- package/tests/fixtures/registry-overrides/README.md +20 -0
- package/tests/fixtures/signed-registries/README.md +4 -0
- package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
- package/tests/smoke/README.md +37 -0
- package/tests/smoke/cli-smoke.md +49 -0
- package/tests/unit/build-output.test.js +40 -0
- package/tests/unit/catalog-loader.test.js +44 -0
- package/tests/unit/path-safety.test.js +62 -0
- package/tests/unit/plugin-manifest.test.js +94 -0
- package/tests/unit/prepublish-guard.test.js +35 -0
- package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
- package/tests/unit/registry-policy.test.js +52 -0
- package/tests/unit/registry-provenance.test.js +185 -0
- package/tests/unit/registry-public-signing.test.js +109 -0
- package/tests/unit/registry-signature-policy.test.js +100 -0
- package/tests/unit/registry-signing.test.js +193 -0
- package/tests/unit/registry-trust-store.test.js +133 -0
- package/tests/unit/registry-url-validation.test.js +64 -0
- package/tests/unit/yaml.test.js +92 -0
package/bin/multimodel-dev-os.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// src/cli/main.js
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import { join as
|
|
6
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4, readdirSync, statSync } from "fs";
|
|
7
|
+
import { join as join8, dirname as dirname4, resolve as resolve3, relative, isAbsolute as isAbsolute2, basename } from "path";
|
|
8
8
|
import { createHash as createHash2 } from "crypto";
|
|
9
9
|
import readline from "readline";
|
|
10
10
|
import { execSync, execFileSync } from "child_process";
|
|
@@ -333,7 +333,7 @@ function showHelp() {
|
|
|
333
333
|
console.log(" adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)");
|
|
334
334
|
console.log(" plugin <subcmd> Manage declarative plugins (subcmd: list, show, validate, install, status)");
|
|
335
335
|
console.log(" catalog <subcmd> Manage Workflow Marketplace & Plugin Catalog (subcmd: list, search, show, categories, recommend, install, status)");
|
|
336
|
-
console.log(" registry <subcmd> Manage trusted remote catalog registries (subcmd: list, add, remove, sync, status, verify, show, cache)");
|
|
336
|
+
console.log(" registry <subcmd> Manage trusted remote catalog registries (subcmd: list, add, remove, sync, status, verify, show, cache, keygen, lock, trust)");
|
|
337
337
|
console.log(" verify Validate structural integrity of an existing project");
|
|
338
338
|
console.log(" templates List all built-in template profiles with details");
|
|
339
339
|
console.log(" list-templates Alias for templates command");
|
|
@@ -399,13 +399,21 @@ function loadRegistryPolicy(targetDir) {
|
|
|
399
399
|
require_approval_for_remote_sync: true,
|
|
400
400
|
require_checksum: true,
|
|
401
401
|
require_signature: false,
|
|
402
|
+
require_lockfile_on_verify: false,
|
|
402
403
|
allow_untrusted_install: false,
|
|
403
404
|
allowed_write_roots: [".ai/", "adapters/"],
|
|
404
405
|
blocked_paths: [".env", ".npmrc", ".git/", "node_modules/", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"],
|
|
405
406
|
max_plugin_files: 20,
|
|
406
407
|
max_plugin_size_kb: 100,
|
|
407
408
|
max_registry_cache_size_kb: 512,
|
|
408
|
-
allowed_file_extensions: [".md", ".yaml", ".yml", ".json"]
|
|
409
|
+
allowed_file_extensions: [".md", ".yaml", ".yml", ".json"],
|
|
410
|
+
allow_unsigned_local: true,
|
|
411
|
+
allow_unsigned_bundled: true,
|
|
412
|
+
allow_unsigned_remote: false,
|
|
413
|
+
trusted_keys_file: ".ai/registries/trusted-keys.yaml",
|
|
414
|
+
allowed_signature_algorithms: ["ed25519", "hmac-sha256"],
|
|
415
|
+
require_trusted_publisher: false,
|
|
416
|
+
provenance_required: true
|
|
409
417
|
};
|
|
410
418
|
const paths = [];
|
|
411
419
|
if (targetDir) {
|
|
@@ -533,9 +541,322 @@ function saveRegistrySources(sources) {
|
|
|
533
541
|
writeFileSync(path, yaml, "utf8");
|
|
534
542
|
}
|
|
535
543
|
|
|
544
|
+
// src/registry/provenance.js
|
|
545
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
546
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
547
|
+
var LOCKFILE_VERSION = "1";
|
|
548
|
+
var LOCKFILE_FILENAME = "registry-lock.json";
|
|
549
|
+
function getLockfilePath(targetDir) {
|
|
550
|
+
return join4(targetDir, ".ai", LOCKFILE_FILENAME);
|
|
551
|
+
}
|
|
552
|
+
function loadRegistryLockfile(targetDir) {
|
|
553
|
+
const lockfilePath = getLockfilePath(targetDir);
|
|
554
|
+
const empty = { lockfile_version: LOCKFILE_VERSION, generated_at: "", entries: {} };
|
|
555
|
+
if (!existsSync4(lockfilePath)) {
|
|
556
|
+
return empty;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const raw = readFileSync5(lockfilePath, "utf8");
|
|
560
|
+
const parsed = JSON.parse(raw);
|
|
561
|
+
if (!parsed || typeof parsed !== "object" || !parsed.entries) {
|
|
562
|
+
return empty;
|
|
563
|
+
}
|
|
564
|
+
parsed.lockfile_version = parsed.lockfile_version || LOCKFILE_VERSION;
|
|
565
|
+
return parsed;
|
|
566
|
+
} catch (_e) {
|
|
567
|
+
return empty;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function saveRegistryLockfile(targetDir, lockfile) {
|
|
571
|
+
const lockfilePath = getLockfilePath(targetDir);
|
|
572
|
+
const lockfileDir = dirname2(lockfilePath);
|
|
573
|
+
if (!existsSync4(lockfileDir)) {
|
|
574
|
+
mkdirSync(lockfileDir, { recursive: true });
|
|
575
|
+
}
|
|
576
|
+
lockfile.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
577
|
+
lockfile.lockfile_version = LOCKFILE_VERSION;
|
|
578
|
+
writeFileSync2(lockfilePath, JSON.stringify(lockfile, null, 2) + "\n", "utf8");
|
|
579
|
+
}
|
|
580
|
+
function updateLockfileEntry(lockfile, name, entry) {
|
|
581
|
+
if (!lockfile.entries || typeof lockfile.entries !== "object") {
|
|
582
|
+
lockfile.entries = {};
|
|
583
|
+
}
|
|
584
|
+
lockfile.entries[name] = {
|
|
585
|
+
url: entry.url,
|
|
586
|
+
synced_at: entry.synced_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
587
|
+
catalog_sha256: entry.catalog_sha256,
|
|
588
|
+
manifest_sha256: entry.manifest_sha256 ?? null,
|
|
589
|
+
signature: entry.signature ?? null,
|
|
590
|
+
signature_alg: entry.signature_alg || "hmac-sha256",
|
|
591
|
+
public_signature_status: entry.public_signature_status ?? null,
|
|
592
|
+
public_signature_algorithm: entry.public_signature_algorithm ?? null,
|
|
593
|
+
public_signature_key_id: entry.public_signature_key_id ?? null,
|
|
594
|
+
trusted_publisher_status: entry.trusted_publisher_status ?? null,
|
|
595
|
+
trust_store_path: entry.trust_store_path ?? null,
|
|
596
|
+
trust_verdict: entry.trust_verdict ?? null,
|
|
597
|
+
lockfile_verdict: entry.lockfile_verdict ?? null,
|
|
598
|
+
verification_errors: entry.verification_errors ?? [],
|
|
599
|
+
verification_warnings: entry.verification_warnings ?? []
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/registry/signing.js
|
|
604
|
+
import { generateKeyPairSync, sign, verify, createHmac, timingSafeEqual, randomBytes } from "crypto";
|
|
605
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
606
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
607
|
+
var SIGNING_KEY_FILENAME = "registry-signing-key";
|
|
608
|
+
function getSigningKeyPath(targetDir) {
|
|
609
|
+
return join5(targetDir, ".ai", SIGNING_KEY_FILENAME);
|
|
610
|
+
}
|
|
611
|
+
function loadSigningKey(targetDir) {
|
|
612
|
+
const keyPath = getSigningKeyPath(targetDir);
|
|
613
|
+
if (!existsSync5(keyPath)) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const raw = readFileSync6(keyPath, "utf8").trim();
|
|
617
|
+
if (!/^[0-9a-f]{64}$/.test(raw)) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Signing key at '${keyPath}' is malformed. Expected a 64-character lowercase hex string (32 bytes). Re-generate with: npx multimodel-dev-os registry keygen --approved`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return raw;
|
|
623
|
+
}
|
|
624
|
+
function generateSigningKey() {
|
|
625
|
+
return randomBytes(32).toString("hex");
|
|
626
|
+
}
|
|
627
|
+
function saveSigningKey(targetDir, key) {
|
|
628
|
+
const keyPath = getSigningKeyPath(targetDir);
|
|
629
|
+
const keyDir = dirname3(keyPath);
|
|
630
|
+
if (!existsSync5(keyDir)) {
|
|
631
|
+
mkdirSync2(keyDir, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
writeFileSync3(keyPath, key + "\n", { encoding: "utf8", mode: 384 });
|
|
634
|
+
try {
|
|
635
|
+
chmodSync(keyPath, 384);
|
|
636
|
+
} catch (_e) {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function signPayload(hexKey, payload) {
|
|
640
|
+
if (typeof hexKey !== "string" || !/^[0-9a-f]{64}$/.test(hexKey)) {
|
|
641
|
+
throw new Error("Invalid signing key: must be a 64-character lowercase hex string.");
|
|
642
|
+
}
|
|
643
|
+
if (typeof payload !== "string") {
|
|
644
|
+
throw new Error("Payload to sign must be a string.");
|
|
645
|
+
}
|
|
646
|
+
const keyBytes = Buffer.from(hexKey, "hex");
|
|
647
|
+
return createHmac("sha256", keyBytes).update(payload, "utf8").digest("hex");
|
|
648
|
+
}
|
|
649
|
+
function createCanonicalPayload(data, fields) {
|
|
650
|
+
if (!data || typeof data !== "object") {
|
|
651
|
+
throw new Error("Data must be an object.");
|
|
652
|
+
}
|
|
653
|
+
if (!Array.isArray(fields)) {
|
|
654
|
+
throw new Error("Fields must be an array of strings.");
|
|
655
|
+
}
|
|
656
|
+
const sortedFields = [...fields].sort();
|
|
657
|
+
const obj = {};
|
|
658
|
+
for (const field of sortedFields) {
|
|
659
|
+
if (data[field] !== void 0) {
|
|
660
|
+
obj[field] = data[field];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return JSON.stringify(obj, (key, value) => {
|
|
664
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
665
|
+
return Object.keys(value).sort().reduce((sorted, k) => {
|
|
666
|
+
sorted[k] = value[k];
|
|
667
|
+
return sorted;
|
|
668
|
+
}, {});
|
|
669
|
+
}
|
|
670
|
+
return value;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function normalizePublicKey(input) {
|
|
674
|
+
if (typeof input !== "string") {
|
|
675
|
+
throw new Error("Public key must be a string.");
|
|
676
|
+
}
|
|
677
|
+
let trimmed = input.trim();
|
|
678
|
+
if (trimmed.startsWith("-----BEGIN PUBLIC KEY-----")) {
|
|
679
|
+
return trimmed;
|
|
680
|
+
}
|
|
681
|
+
if (trimmed.startsWith("-----BEGIN")) {
|
|
682
|
+
return trimmed;
|
|
683
|
+
}
|
|
684
|
+
const clean = trimmed.replace(/\s+/g, "");
|
|
685
|
+
const lines = [];
|
|
686
|
+
for (let i = 0; i < clean.length; i += 64) {
|
|
687
|
+
lines.push(clean.slice(i, i + 64));
|
|
688
|
+
}
|
|
689
|
+
return `-----BEGIN PUBLIC KEY-----
|
|
690
|
+
${lines.join("\n")}
|
|
691
|
+
-----END PUBLIC KEY-----`;
|
|
692
|
+
}
|
|
693
|
+
function verifyEd25519Payload(publicKey, payload, signature) {
|
|
694
|
+
if (typeof publicKey !== "string" || typeof payload !== "string" || typeof signature !== "string") {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const pubKey = normalizePublicKey(publicKey);
|
|
699
|
+
const sigBuffer = Buffer.from(signature, "base64");
|
|
700
|
+
return verify(null, Buffer.from(payload, "utf8"), pubKey, sigBuffer);
|
|
701
|
+
} catch (_e) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function verifySignatureBlock({ manifest, trustedKeys, policy = {}, hmacKey = null, source = {} }) {
|
|
706
|
+
const isBundled = source.name === "bundled";
|
|
707
|
+
const isLocal = source.type === "local";
|
|
708
|
+
const isRemote = source.type === "remote" || !isBundled && !isLocal;
|
|
709
|
+
const signatureBlocks = [];
|
|
710
|
+
if (manifest.signature && typeof manifest.signature === "object") {
|
|
711
|
+
signatureBlocks.push(manifest.signature);
|
|
712
|
+
}
|
|
713
|
+
if (Array.isArray(manifest.signatures)) {
|
|
714
|
+
signatureBlocks.push(...manifest.signatures);
|
|
715
|
+
}
|
|
716
|
+
if (signatureBlocks.length === 0) {
|
|
717
|
+
if (policy.require_signature) {
|
|
718
|
+
return { verified: false, status: "failed", error: "Signature is required by policy but missing from manifest." };
|
|
719
|
+
}
|
|
720
|
+
if (isRemote && policy.allow_unsigned_remote === false) {
|
|
721
|
+
return { verified: false, status: "failed", error: "Unsigned remote registries are not allowed by policy." };
|
|
722
|
+
}
|
|
723
|
+
if (isBundled && policy.allow_unsigned_bundled === false) {
|
|
724
|
+
return { verified: false, status: "failed", error: "Unsigned bundled registries are not allowed by policy." };
|
|
725
|
+
}
|
|
726
|
+
if (isLocal && !isBundled && policy.allow_unsigned_local === false) {
|
|
727
|
+
return { verified: false, status: "failed", error: "Unsigned local registries are not allowed by policy." };
|
|
728
|
+
}
|
|
729
|
+
return { verified: true, status: "unsigned", message: "Registry is unsigned (allowed by policy)." };
|
|
730
|
+
}
|
|
731
|
+
let verifiedCount = 0;
|
|
732
|
+
const errors = [];
|
|
733
|
+
const allowedAlgs = policy.allowed_signature_algorithms || ["ed25519", "hmac-sha256"];
|
|
734
|
+
for (const sigBlock of signatureBlocks) {
|
|
735
|
+
const alg = sigBlock.algorithm;
|
|
736
|
+
const keyId = sigBlock.key_id;
|
|
737
|
+
const signature = sigBlock.signature;
|
|
738
|
+
const signedFields = sigBlock.signed_fields;
|
|
739
|
+
if (!alg || !keyId || !signature || !Array.isArray(signedFields)) {
|
|
740
|
+
errors.push(`Malformed signature block for key_id '${keyId || "unknown"}'.`);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (!allowedAlgs.includes(alg)) {
|
|
744
|
+
errors.push(`Signature algorithm '${alg}' is not allowed by policy (allowed: ${allowedAlgs.join(", ")}).`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (alg === "hmac-sha256") {
|
|
748
|
+
if (!hmacKey) {
|
|
749
|
+
errors.push(`HMAC key not configured locally for key_id '${keyId}'.`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const payload = createCanonicalPayload(manifest, signedFields);
|
|
754
|
+
const expected = createHmac("sha256", Buffer.from(hmacKey, "hex")).update(payload, "utf8").digest("hex");
|
|
755
|
+
if (timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"))) {
|
|
756
|
+
verifiedCount++;
|
|
757
|
+
} else {
|
|
758
|
+
errors.push(`Invalid HMAC signature for key_id '${keyId}'.`);
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
errors.push(`HMAC signature verification failed: ${err.message}`);
|
|
762
|
+
}
|
|
763
|
+
} else if (alg === "ed25519") {
|
|
764
|
+
const trustedKey = trustedKeys ? trustedKeys.find((k) => k.key_id === keyId) : null;
|
|
765
|
+
if (!trustedKey) {
|
|
766
|
+
errors.push(`Key ID '${keyId}' not found in trust store.`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (trustedKey.status !== "active") {
|
|
770
|
+
errors.push(`Key ID '${keyId}' is ${trustedKey.status} (must be active).`);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const scopes = trustedKey.scopes || [];
|
|
774
|
+
if (!scopes.includes("registry") && !scopes.includes("catalog")) {
|
|
775
|
+
errors.push(`Key ID '${keyId}' does not have required scope 'registry' or 'catalog' (scopes: ${scopes.join(", ")}).`);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const payload = createCanonicalPayload(manifest, signedFields);
|
|
780
|
+
if (verifyEd25519Payload(trustedKey.public_key, payload, signature)) {
|
|
781
|
+
verifiedCount++;
|
|
782
|
+
} else {
|
|
783
|
+
errors.push(`Invalid Ed25519 signature for key_id '${keyId}'.`);
|
|
784
|
+
}
|
|
785
|
+
} catch (err) {
|
|
786
|
+
errors.push(`Ed25519 signature verification failed: ${err.message}`);
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
errors.push(`Unsupported signature algorithm '${alg}' for key_id '${keyId}'.`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (verifiedCount > 0) {
|
|
793
|
+
return {
|
|
794
|
+
verified: true,
|
|
795
|
+
status: "verified",
|
|
796
|
+
verified_signatures: signatureBlocks.map((s) => ({ key_id: s.key_id, algorithm: s.algorithm }))
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
verified: false,
|
|
801
|
+
status: "failed",
|
|
802
|
+
errors
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/registry/trust-store.js
|
|
807
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
808
|
+
import { join as join6, isAbsolute } from "path";
|
|
809
|
+
function loadTrustedKeys(targetDir, policy) {
|
|
810
|
+
const pol = policy || loadRegistryPolicy(targetDir);
|
|
811
|
+
const keyFile = pol.trusted_keys_file || ".ai/registries/trusted-keys.yaml";
|
|
812
|
+
const filePath = isAbsolute(keyFile) ? keyFile : join6(targetDir, keyFile);
|
|
813
|
+
if (!existsSync6(filePath)) {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
818
|
+
const parsed = parseYaml(raw);
|
|
819
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.trusted_publishers)) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
return parsed.trusted_publishers;
|
|
823
|
+
} catch (_e) {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/registry/verdict.js
|
|
829
|
+
function createTrustVerdict({
|
|
830
|
+
source,
|
|
831
|
+
source_type,
|
|
832
|
+
manifest_hash_status = "N/A",
|
|
833
|
+
catalog_hash_status = "N/A",
|
|
834
|
+
lockfile_status = "N/A",
|
|
835
|
+
provenance_status = "N/A",
|
|
836
|
+
signature_status = "N/A",
|
|
837
|
+
trusted_publisher_status = "N/A",
|
|
838
|
+
errors = [],
|
|
839
|
+
warnings = [],
|
|
840
|
+
final_status = "unknown"
|
|
841
|
+
}) {
|
|
842
|
+
return {
|
|
843
|
+
source,
|
|
844
|
+
source_type,
|
|
845
|
+
manifest_hash_status,
|
|
846
|
+
catalog_hash_status,
|
|
847
|
+
lockfile_status,
|
|
848
|
+
provenance_status,
|
|
849
|
+
signature_status,
|
|
850
|
+
trusted_publisher_status,
|
|
851
|
+
errors: Array.isArray(errors) ? errors : [],
|
|
852
|
+
warnings: Array.isArray(warnings) ? warnings : [],
|
|
853
|
+
final_status
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
536
857
|
// src/catalog/loader.js
|
|
537
|
-
import { existsSync as
|
|
538
|
-
import { join as
|
|
858
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
859
|
+
import { join as join7 } from "path";
|
|
539
860
|
function loadCatalog(options = {}) {
|
|
540
861
|
let catalog;
|
|
541
862
|
if (options.allSources) {
|
|
@@ -543,10 +864,10 @@ function loadCatalog(options = {}) {
|
|
|
543
864
|
} else if (options.source) {
|
|
544
865
|
catalog = loadCatalogFromSource(options.source, options);
|
|
545
866
|
} else {
|
|
546
|
-
const path =
|
|
867
|
+
const path = join7(sourceRoot, ".ai", "plugins", "catalog.yaml");
|
|
547
868
|
try {
|
|
548
|
-
if (
|
|
549
|
-
const reg = parseYaml(
|
|
869
|
+
if (existsSync7(path)) {
|
|
870
|
+
const reg = parseYaml(readFileSync8(path, "utf8"));
|
|
550
871
|
catalog = reg.catalog || { plugins: [] };
|
|
551
872
|
} else {
|
|
552
873
|
catalog = { plugins: [] };
|
|
@@ -564,10 +885,10 @@ function loadCatalogFromSource(source, options = {}) {
|
|
|
564
885
|
if (!source || source === "bundled") {
|
|
565
886
|
return loadCatalog();
|
|
566
887
|
} else if (source === "local") {
|
|
567
|
-
const localPath =
|
|
888
|
+
const localPath = join7(options.target || process.cwd(), ".ai", "plugins", "catalog.yaml");
|
|
568
889
|
try {
|
|
569
|
-
if (
|
|
570
|
-
const reg = parseYaml(
|
|
890
|
+
if (existsSync7(localPath)) {
|
|
891
|
+
const reg = parseYaml(readFileSync8(localPath, "utf8"));
|
|
571
892
|
const catalog = reg.catalog || { plugins: [] };
|
|
572
893
|
(catalog.plugins || []).forEach((p) => {
|
|
573
894
|
p._source = "local";
|
|
@@ -590,10 +911,10 @@ function loadCatalogFromSource(source, options = {}) {
|
|
|
590
911
|
process.exit(1);
|
|
591
912
|
}
|
|
592
913
|
}
|
|
593
|
-
const cachePath =
|
|
914
|
+
const cachePath = join7(sourceRoot, ".ai", "registry-cache", regName, "catalog.yaml");
|
|
594
915
|
try {
|
|
595
|
-
if (
|
|
596
|
-
const reg = parseYaml(
|
|
916
|
+
if (existsSync7(cachePath)) {
|
|
917
|
+
const reg = parseYaml(readFileSync8(cachePath, "utf8"));
|
|
597
918
|
const catalog = reg.catalog || { plugins: [] };
|
|
598
919
|
(catalog.plugins || []).forEach((p) => {
|
|
599
920
|
p._source = `remote:${regName}`;
|
|
@@ -615,10 +936,10 @@ function loadAllCatalogs(options = {}) {
|
|
|
615
936
|
p._source = "bundled";
|
|
616
937
|
allPlugins.push(p);
|
|
617
938
|
});
|
|
618
|
-
const localPath =
|
|
619
|
-
if (
|
|
939
|
+
const localPath = join7(options.target || process.cwd(), ".ai", "plugins", "catalog.yaml");
|
|
940
|
+
if (existsSync7(localPath)) {
|
|
620
941
|
try {
|
|
621
|
-
const localCat = parseYaml(
|
|
942
|
+
const localCat = parseYaml(readFileSync8(localPath, "utf8"));
|
|
622
943
|
const localPlugins = (localCat.catalog || {}).plugins || [];
|
|
623
944
|
localPlugins.forEach((p) => {
|
|
624
945
|
if (!allPlugins.some((bp) => bp.slug === p.slug)) {
|
|
@@ -631,10 +952,10 @@ function loadAllCatalogs(options = {}) {
|
|
|
631
952
|
}
|
|
632
953
|
if (policy.allow_remote_registries) {
|
|
633
954
|
sources.filter((s) => s.type !== "local" && s.enabled).forEach((s) => {
|
|
634
|
-
const cachePath =
|
|
635
|
-
if (
|
|
955
|
+
const cachePath = join7(sourceRoot, ".ai", "registry-cache", s.name, "catalog.yaml");
|
|
956
|
+
if (existsSync7(cachePath)) {
|
|
636
957
|
try {
|
|
637
|
-
const remoteCat = parseYaml(
|
|
958
|
+
const remoteCat = parseYaml(readFileSync8(cachePath, "utf8"));
|
|
638
959
|
const remotePlugins = (remoteCat.catalog || {}).plugins || [];
|
|
639
960
|
remotePlugins.forEach((p) => {
|
|
640
961
|
if (!allPlugins.some((bp) => bp.slug === p.slug)) {
|
|
@@ -1017,8 +1338,30 @@ if (COMMAND === "init") {
|
|
|
1017
1338
|
console.error("\x1B[31mError: Please specify a cache subcommand: clear.\x1B[0m");
|
|
1018
1339
|
process.exit(1);
|
|
1019
1340
|
}
|
|
1341
|
+
} else if (sub === "keygen") {
|
|
1342
|
+
handleRegistryKeygen(params);
|
|
1343
|
+
} else if (sub === "lock") {
|
|
1344
|
+
handleRegistryLock(params);
|
|
1345
|
+
} else if (sub === "trust") {
|
|
1346
|
+
const trustSub = positional[2];
|
|
1347
|
+
if (trustSub === "list") {
|
|
1348
|
+
handleRegistryTrustList(params);
|
|
1349
|
+
} else if (trustSub === "show") {
|
|
1350
|
+
const keyId = positional[3];
|
|
1351
|
+
if (!keyId) {
|
|
1352
|
+
console.error("\x1B[31mError: Please specify a key ID.\x1B[0m");
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
handleRegistryTrustShow(keyId, params);
|
|
1356
|
+
} else if (trustSub === "verify") {
|
|
1357
|
+
handleRegistryTrustVerify(params);
|
|
1358
|
+
} else {
|
|
1359
|
+
console.error("\x1B[31mError: Please specify a trust subcommand: list, show, or verify.\x1B[0m");
|
|
1360
|
+
console.log("Example: node bin/multimodel-dev-os.js registry trust list");
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1020
1363
|
} else {
|
|
1021
|
-
console.error("\x1B[31mError: Please specify a registry subcommand: list, add, remove, sync, status, verify, show, or
|
|
1364
|
+
console.error("\x1B[31mError: Please specify a registry subcommand: list, add, remove, sync, status, verify, show, cache, keygen, lock, or trust.\x1B[0m");
|
|
1022
1365
|
console.log("Example: node bin/multimodel-dev-os.js registry list");
|
|
1023
1366
|
process.exit(1);
|
|
1024
1367
|
}
|
|
@@ -1102,40 +1445,40 @@ function handleInit(options) {
|
|
|
1102
1445
|
console.log("\x1B[36mDry Run active - no actual modifications will occur\x1B[0m");
|
|
1103
1446
|
const operations = [];
|
|
1104
1447
|
const conflicts = [];
|
|
1105
|
-
let templateDir =
|
|
1106
|
-
if (!
|
|
1448
|
+
let templateDir = join8(sourceRoot, "examples", options.template);
|
|
1449
|
+
if (!existsSync8(templateDir)) {
|
|
1107
1450
|
console.warn(` \x1B[33m[WARNING] Template '${options.template}' source files could not be found.\x1B[0m`);
|
|
1108
1451
|
console.warn(` To view available templates, run: \x1B[36mnpx multimodel-dev-os templates\x1B[0m`);
|
|
1109
1452
|
console.warn(` Falling back to the stable \x1B[32m'general-app'\x1B[0m profile...
|
|
1110
1453
|
`);
|
|
1111
|
-
templateDir =
|
|
1454
|
+
templateDir = join8(sourceRoot, "examples", "general-app");
|
|
1112
1455
|
}
|
|
1113
|
-
let agentsSrc =
|
|
1114
|
-
let memorySrc =
|
|
1115
|
-
let tasksSrc =
|
|
1116
|
-
let runbookSrc =
|
|
1117
|
-
let configSrc =
|
|
1456
|
+
let agentsSrc = join8(templateDir, "AGENTS.md");
|
|
1457
|
+
let memorySrc = join8(templateDir, "MEMORY.md");
|
|
1458
|
+
let tasksSrc = join8(templateDir, "TASKS.md");
|
|
1459
|
+
let runbookSrc = join8(sourceRoot, "RUNBOOK.md");
|
|
1460
|
+
let configSrc = join8(templateDir, ".ai", "config.yaml");
|
|
1118
1461
|
if (options.caveman) {
|
|
1119
|
-
agentsSrc =
|
|
1120
|
-
memorySrc =
|
|
1121
|
-
tasksSrc =
|
|
1122
|
-
runbookSrc =
|
|
1462
|
+
agentsSrc = join8(sourceRoot, ".ai", "templates", "AGENTS.caveman.md");
|
|
1463
|
+
memorySrc = join8(sourceRoot, ".ai", "templates", "MEMORY.caveman.md");
|
|
1464
|
+
tasksSrc = join8(sourceRoot, ".ai", "templates", "TASKS.caveman.md");
|
|
1465
|
+
runbookSrc = join8(sourceRoot, ".ai", "templates", "RUNBOOK.caveman.md");
|
|
1123
1466
|
}
|
|
1124
1467
|
operations.push({ dest: "AGENTS.md", src: agentsSrc });
|
|
1125
1468
|
operations.push({ dest: "MEMORY.md", src: memorySrc });
|
|
1126
1469
|
operations.push({ dest: "TASKS.md", src: tasksSrc });
|
|
1127
1470
|
operations.push({ dest: "RUNBOOK.md", src: runbookSrc });
|
|
1128
1471
|
operations.push({ dest: ".ai/config.yaml", src: configSrc });
|
|
1129
|
-
const templateAiDir =
|
|
1130
|
-
if (
|
|
1472
|
+
const templateAiDir = join8(templateDir, ".ai");
|
|
1473
|
+
if (existsSync8(templateAiDir) && !options.caveman) {
|
|
1131
1474
|
const subdirs = ["context", "skills"];
|
|
1132
1475
|
subdirs.forEach((sub) => {
|
|
1133
|
-
const subPath =
|
|
1134
|
-
if (
|
|
1476
|
+
const subPath = join8(templateAiDir, sub);
|
|
1477
|
+
if (existsSync8(subPath)) {
|
|
1135
1478
|
readdirSync(subPath).forEach((file) => {
|
|
1136
1479
|
operations.push({
|
|
1137
|
-
dest:
|
|
1138
|
-
src:
|
|
1480
|
+
dest: join8(".ai", sub, file),
|
|
1481
|
+
src: join8(subPath, file)
|
|
1139
1482
|
});
|
|
1140
1483
|
});
|
|
1141
1484
|
}
|
|
@@ -1143,47 +1486,47 @@ function handleInit(options) {
|
|
|
1143
1486
|
}
|
|
1144
1487
|
const globalAiSubdirs = ["context", "agents", "skills", "prompts", "checks", "templates", "session-logs", "registries", "proposals", "intelligence"];
|
|
1145
1488
|
globalAiSubdirs.forEach((sub) => {
|
|
1146
|
-
const globalPath =
|
|
1147
|
-
if (
|
|
1489
|
+
const globalPath = join8(sourceRoot, ".ai", sub);
|
|
1490
|
+
if (existsSync8(globalPath)) {
|
|
1148
1491
|
readdirSync(globalPath).forEach((file) => {
|
|
1149
|
-
const destRel =
|
|
1492
|
+
const destRel = join8(".ai", sub, file);
|
|
1150
1493
|
if (!operations.some((op) => op.dest === destRel)) {
|
|
1151
1494
|
if (options.caveman && (sub === "context" || sub === "skills" || sub === "prompts" || sub === "checks")) {
|
|
1152
1495
|
return;
|
|
1153
1496
|
}
|
|
1154
1497
|
operations.push({
|
|
1155
1498
|
dest: destRel,
|
|
1156
|
-
src:
|
|
1499
|
+
src: join8(globalPath, file)
|
|
1157
1500
|
});
|
|
1158
1501
|
}
|
|
1159
1502
|
});
|
|
1160
1503
|
}
|
|
1161
1504
|
});
|
|
1162
1505
|
options.adapters.forEach((adapter) => {
|
|
1163
|
-
const adapterDir =
|
|
1164
|
-
if (
|
|
1506
|
+
const adapterDir = join8(sourceRoot, "adapters", adapter);
|
|
1507
|
+
if (existsSync8(adapterDir)) {
|
|
1165
1508
|
const copyRecursive = (currSrc, currRel) => {
|
|
1166
1509
|
if (statSync(currSrc).isDirectory()) {
|
|
1167
1510
|
readdirSync(currSrc).forEach((file) => {
|
|
1168
|
-
copyRecursive(
|
|
1511
|
+
copyRecursive(join8(currSrc, file), join8(currRel, file));
|
|
1169
1512
|
});
|
|
1170
1513
|
} else {
|
|
1171
1514
|
operations.push({
|
|
1172
|
-
dest:
|
|
1515
|
+
dest: join8("adapters", adapter, currRel),
|
|
1173
1516
|
src: currSrc
|
|
1174
1517
|
});
|
|
1175
1518
|
}
|
|
1176
1519
|
};
|
|
1177
1520
|
readdirSync(adapterDir).forEach((file) => {
|
|
1178
|
-
copyRecursive(
|
|
1521
|
+
copyRecursive(join8(adapterDir, file), file);
|
|
1179
1522
|
});
|
|
1180
1523
|
} else {
|
|
1181
1524
|
console.warn(`\x1B[33mWarning: Adapter '${adapter}' not found. Skipping.\x1B[0m`);
|
|
1182
1525
|
}
|
|
1183
1526
|
});
|
|
1184
1527
|
operations.forEach((op) => {
|
|
1185
|
-
const targetFile =
|
|
1186
|
-
if (
|
|
1528
|
+
const targetFile = join8(options.target, op.dest);
|
|
1529
|
+
if (existsSync8(targetFile)) {
|
|
1187
1530
|
if (!options.force) {
|
|
1188
1531
|
conflicts.push(op.dest);
|
|
1189
1532
|
}
|
|
@@ -1197,24 +1540,24 @@ function handleInit(options) {
|
|
|
1197
1540
|
process.exit(1);
|
|
1198
1541
|
}
|
|
1199
1542
|
operations.forEach((op) => {
|
|
1200
|
-
const targetFile =
|
|
1201
|
-
const targetDir =
|
|
1543
|
+
const targetFile = join8(options.target, op.dest);
|
|
1544
|
+
const targetDir = dirname4(targetFile);
|
|
1202
1545
|
if (options.dryRun) {
|
|
1203
1546
|
console.log(` \x1B[36m[DRY-RUN] WOULD CREATE:\x1B[0m ${op.dest}`);
|
|
1204
1547
|
} else {
|
|
1205
|
-
if (!
|
|
1206
|
-
|
|
1548
|
+
if (!existsSync8(targetDir)) {
|
|
1549
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
1207
1550
|
}
|
|
1208
|
-
const data =
|
|
1209
|
-
|
|
1551
|
+
const data = readFileSync9(op.src);
|
|
1552
|
+
writeFileSync4(targetFile, data);
|
|
1210
1553
|
console.log(` \x1B[32mCREATE:\x1B[0m ${op.dest}`);
|
|
1211
1554
|
}
|
|
1212
1555
|
});
|
|
1213
1556
|
const dirsToEnsure = [".ai/context", ".ai/skills", ".ai/session-logs"];
|
|
1214
1557
|
dirsToEnsure.forEach((d) => {
|
|
1215
|
-
const fullPath =
|
|
1216
|
-
if (!options.dryRun && !
|
|
1217
|
-
|
|
1558
|
+
const fullPath = join8(options.target, d);
|
|
1559
|
+
if (!options.dryRun && !existsSync8(fullPath)) {
|
|
1560
|
+
mkdirSync3(fullPath, { recursive: true });
|
|
1218
1561
|
console.log(` \x1B[32mCREATE DIR:\x1B[0m ${d}`);
|
|
1219
1562
|
}
|
|
1220
1563
|
});
|
|
@@ -1222,25 +1565,25 @@ function handleInit(options) {
|
|
|
1222
1565
|
options.adapters.forEach((adapter) => {
|
|
1223
1566
|
const a = ADAPTERS[adapter];
|
|
1224
1567
|
if (a && a.rules_file) {
|
|
1225
|
-
const srcFile =
|
|
1226
|
-
const destFile =
|
|
1227
|
-
const destDir =
|
|
1228
|
-
if (
|
|
1229
|
-
if (!
|
|
1230
|
-
|
|
1231
|
-
|
|
1568
|
+
const srcFile = join8(sourceRoot, "adapters", adapter, a.rules_file);
|
|
1569
|
+
const destFile = join8(options.target, a.rules_file);
|
|
1570
|
+
const destDir = dirname4(destFile);
|
|
1571
|
+
if (existsSync8(srcFile)) {
|
|
1572
|
+
if (!existsSync8(destDir))
|
|
1573
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1574
|
+
writeFileSync4(destFile, readFileSync9(srcFile));
|
|
1232
1575
|
console.log(` \x1B[32mCREATE ROOT ADAPTER FILE:\x1B[0m ${a.rules_file}`);
|
|
1233
1576
|
}
|
|
1234
1577
|
}
|
|
1235
1578
|
});
|
|
1236
|
-
const targetConfigPath =
|
|
1237
|
-
if (
|
|
1238
|
-
let configContent =
|
|
1579
|
+
const targetConfigPath = join8(options.target, ".ai/config.yaml");
|
|
1580
|
+
if (existsSync8(targetConfigPath) && options.adapters.length > 0) {
|
|
1581
|
+
let configContent = readFileSync9(targetConfigPath, "utf8");
|
|
1239
1582
|
options.adapters.forEach((adapter) => {
|
|
1240
1583
|
const regex = new RegExp(`${adapter}:\\s*false`, "g");
|
|
1241
1584
|
configContent = configContent.replace(regex, `${adapter}: true`);
|
|
1242
1585
|
});
|
|
1243
|
-
|
|
1586
|
+
writeFileSync4(targetConfigPath, configContent, "utf8");
|
|
1244
1587
|
console.log(` \x1B[32mUPDATE CONFIG:\x1B[0m Enabled selected adapters [${options.adapters.join(", ")}] in .ai/config.yaml`);
|
|
1245
1588
|
}
|
|
1246
1589
|
} else {
|
|
@@ -1284,8 +1627,8 @@ function handleVerify(options) {
|
|
|
1284
1627
|
let passed = 0;
|
|
1285
1628
|
let failed = 0;
|
|
1286
1629
|
const assertFile = (relPath) => {
|
|
1287
|
-
const fullPath =
|
|
1288
|
-
if (
|
|
1630
|
+
const fullPath = join8(options.target, relPath);
|
|
1631
|
+
if (existsSync8(fullPath) && statSync(fullPath).isFile()) {
|
|
1289
1632
|
console.log(` \x1B[32m\u2713\x1B[0m ${relPath}`);
|
|
1290
1633
|
passed++;
|
|
1291
1634
|
} else {
|
|
@@ -1356,9 +1699,9 @@ function handleDoctor(options) {
|
|
|
1356
1699
|
console.warn(` \x1B[33m[WARNING]\x1B[0m ${msg}`);
|
|
1357
1700
|
warnings++;
|
|
1358
1701
|
};
|
|
1359
|
-
const gitignorePath =
|
|
1360
|
-
if (
|
|
1361
|
-
const content =
|
|
1702
|
+
const gitignorePath = join8(options.target, ".gitignore");
|
|
1703
|
+
if (existsSync8(gitignorePath)) {
|
|
1704
|
+
const content = readFileSync9(gitignorePath, "utf8");
|
|
1362
1705
|
if (!content.includes("node_modules")) {
|
|
1363
1706
|
warn(".gitignore is missing node_modules! This will cause AI tools to choke by scanning dependencies.");
|
|
1364
1707
|
}
|
|
@@ -1368,9 +1711,9 @@ function handleDoctor(options) {
|
|
|
1368
1711
|
} else {
|
|
1369
1712
|
warn("Missing .gitignore file in target workspace! AI tools might read large build artifacts.");
|
|
1370
1713
|
}
|
|
1371
|
-
const agentsPath =
|
|
1372
|
-
if (
|
|
1373
|
-
const content =
|
|
1714
|
+
const agentsPath = join8(options.target, "AGENTS.md");
|
|
1715
|
+
if (existsSync8(agentsPath)) {
|
|
1716
|
+
const content = readFileSync9(agentsPath, "utf8");
|
|
1374
1717
|
if (!content.includes("build:") && !content.includes("build")) {
|
|
1375
1718
|
warn("AGENTS.md is missing build command specifications.");
|
|
1376
1719
|
}
|
|
@@ -1383,31 +1726,31 @@ function handleDoctor(options) {
|
|
|
1383
1726
|
} else {
|
|
1384
1727
|
warn("AGENTS.md is missing from project root.");
|
|
1385
1728
|
}
|
|
1386
|
-
const memoryPath =
|
|
1387
|
-
if (
|
|
1388
|
-
const content =
|
|
1729
|
+
const memoryPath = join8(options.target, "MEMORY.md");
|
|
1730
|
+
if (existsSync8(memoryPath)) {
|
|
1731
|
+
const content = readFileSync9(memoryPath, "utf8");
|
|
1389
1732
|
const placeholdersCount = (content.match(/null/g) || []).length;
|
|
1390
1733
|
if (placeholdersCount > 3) {
|
|
1391
1734
|
warn(`MEMORY.md contains ${placeholdersCount} empty 'null' placeholders. Update project constraints.`);
|
|
1392
1735
|
}
|
|
1393
1736
|
}
|
|
1394
|
-
const tasksPath =
|
|
1395
|
-
if (
|
|
1396
|
-
const content =
|
|
1737
|
+
const tasksPath = join8(options.target, "TASKS.md");
|
|
1738
|
+
if (existsSync8(tasksPath)) {
|
|
1739
|
+
const content = readFileSync9(tasksPath, "utf8");
|
|
1397
1740
|
if (!content.includes("- [ ]") && !content.includes("- [/]")) {
|
|
1398
1741
|
warn("TASKS.md has no active task section (no tasks marked as - [ ] or - [/]).");
|
|
1399
1742
|
}
|
|
1400
1743
|
} else {
|
|
1401
1744
|
warn("TASKS.md is missing from project root.");
|
|
1402
1745
|
}
|
|
1403
|
-
const configPath =
|
|
1404
|
-
if (
|
|
1405
|
-
const content =
|
|
1746
|
+
const configPath = join8(options.target, ".ai", "config.yaml");
|
|
1747
|
+
if (existsSync8(configPath)) {
|
|
1748
|
+
const content = readFileSync9(configPath, "utf8");
|
|
1406
1749
|
const checkAdapter = (adapterName, filename) => {
|
|
1407
1750
|
const regex = new RegExp(`${adapterName}:\\s*true`);
|
|
1408
1751
|
if (regex.test(content)) {
|
|
1409
|
-
const filePath =
|
|
1410
|
-
if (!
|
|
1752
|
+
const filePath = join8(options.target, filename);
|
|
1753
|
+
if (!existsSync8(filePath)) {
|
|
1411
1754
|
warn(`Adapter '${adapterName}' is enabled in .ai/config.yaml but matching adapter file '${filename}' is missing from root.`);
|
|
1412
1755
|
}
|
|
1413
1756
|
}
|
|
@@ -1422,9 +1765,9 @@ function handleDoctor(options) {
|
|
|
1422
1765
|
}
|
|
1423
1766
|
const sinkFolders = ["node_modules", "dist", "build", ".next", ".git"];
|
|
1424
1767
|
sinkFolders.forEach((folder) => {
|
|
1425
|
-
const fullPath =
|
|
1426
|
-
if (
|
|
1427
|
-
const gitignore =
|
|
1768
|
+
const fullPath = join8(options.target, folder);
|
|
1769
|
+
if (existsSync8(fullPath)) {
|
|
1770
|
+
const gitignore = existsSync8(gitignorePath) ? readFileSync9(gitignorePath, "utf8") : "";
|
|
1428
1771
|
if (!gitignore.includes(folder)) {
|
|
1429
1772
|
warn(`Large token-sink directory '${folder}/' is present in workspace but not ignored in .gitignore. AI tools may read it.`);
|
|
1430
1773
|
}
|
|
@@ -1448,8 +1791,8 @@ function handleValidate(options) {
|
|
|
1448
1791
|
`);
|
|
1449
1792
|
let errors = 0;
|
|
1450
1793
|
const assertPath = (relPath, type) => {
|
|
1451
|
-
const fullPath =
|
|
1452
|
-
if (
|
|
1794
|
+
const fullPath = join8(options.target, relPath);
|
|
1795
|
+
if (existsSync8(fullPath)) {
|
|
1453
1796
|
const stat = statSync(fullPath);
|
|
1454
1797
|
const isOk = type === "file" ? stat.isFile() : stat.isDirectory();
|
|
1455
1798
|
if (isOk) {
|
|
@@ -1467,15 +1810,15 @@ function handleValidate(options) {
|
|
|
1467
1810
|
core.forEach((f) => assertPath(f, "file"));
|
|
1468
1811
|
const dirs = [".ai/context", ".ai/skills", ".ai/session-logs"];
|
|
1469
1812
|
dirs.forEach((d) => assertPath(d, "dir"));
|
|
1470
|
-
const agentsPath =
|
|
1471
|
-
const agentsExist =
|
|
1813
|
+
const agentsPath = join8(options.target, ".ai/agents");
|
|
1814
|
+
const agentsExist = existsSync8(agentsPath) && statSync(agentsPath).isDirectory();
|
|
1472
1815
|
if (agentsExist) {
|
|
1473
1816
|
console.log(` \x1B[32m\u2713\x1B[0m .ai/agents (dir)`);
|
|
1474
1817
|
} else {
|
|
1475
|
-
const agentsMdPath =
|
|
1818
|
+
const agentsMdPath = join8(options.target, "AGENTS.md");
|
|
1476
1819
|
let explained = false;
|
|
1477
|
-
if (
|
|
1478
|
-
const agentsMdContent =
|
|
1820
|
+
if (existsSync8(agentsMdPath)) {
|
|
1821
|
+
const agentsMdContent = readFileSync9(agentsMdPath, "utf8");
|
|
1479
1822
|
if (agentsMdContent.includes("multimodel") || agentsMdContent.includes("orchestrator") || agentsMdContent.includes("global") || agentsMdContent.includes("role") || agentsMdContent.includes("Agent Roles")) {
|
|
1480
1823
|
explained = true;
|
|
1481
1824
|
}
|
|
@@ -1487,14 +1830,14 @@ function handleValidate(options) {
|
|
|
1487
1830
|
errors++;
|
|
1488
1831
|
}
|
|
1489
1832
|
}
|
|
1490
|
-
const configPath =
|
|
1491
|
-
if (
|
|
1492
|
-
const content =
|
|
1833
|
+
const configPath = join8(options.target, ".ai", "config.yaml");
|
|
1834
|
+
if (existsSync8(configPath)) {
|
|
1835
|
+
const content = readFileSync9(configPath, "utf8");
|
|
1493
1836
|
const assertAdapter = (adapterName, filename) => {
|
|
1494
1837
|
const regex = new RegExp(`${adapterName}:\\s*true`);
|
|
1495
1838
|
if (regex.test(content)) {
|
|
1496
|
-
const fullPath =
|
|
1497
|
-
if (
|
|
1839
|
+
const fullPath = join8(options.target, filename);
|
|
1840
|
+
if (existsSync8(fullPath)) {
|
|
1498
1841
|
console.log(` \x1B[32m\u2713\x1B[0m ${filename} (enabled adapter rules file verified)`);
|
|
1499
1842
|
} else {
|
|
1500
1843
|
console.error(` \x1B[31m\u2717 ${filename} (adapter '${adapterName}' is enabled in .ai/config.yaml, but rule file is missing!)\x1B[0m`);
|
|
@@ -1538,12 +1881,12 @@ function handleValidate(options) {
|
|
|
1538
1881
|
}
|
|
1539
1882
|
}
|
|
1540
1883
|
function handleListModels(options) {
|
|
1541
|
-
const registryPath =
|
|
1542
|
-
if (!
|
|
1884
|
+
const registryPath = join8(sourceRoot, ".ai", "models", "registry.yaml");
|
|
1885
|
+
if (!existsSync8(registryPath)) {
|
|
1543
1886
|
console.error("Error: Model registry not found.");
|
|
1544
1887
|
process.exit(1);
|
|
1545
1888
|
}
|
|
1546
|
-
const registry = parseYaml(
|
|
1889
|
+
const registry = parseYaml(readFileSync9(registryPath, "utf8"));
|
|
1547
1890
|
const models = registry.models || {};
|
|
1548
1891
|
if (options && options.json) {
|
|
1549
1892
|
console.log(JSON.stringify(models, null, 2));
|
|
@@ -1564,12 +1907,12 @@ function handleListModels(options) {
|
|
|
1564
1907
|
console.log("\nUse \x1B[36mshow-model <model-alias>\x1B[0m to view detailed model capabilities.\n");
|
|
1565
1908
|
}
|
|
1566
1909
|
function handleShowModel(name) {
|
|
1567
|
-
const registryPath =
|
|
1568
|
-
if (!
|
|
1910
|
+
const registryPath = join8(sourceRoot, ".ai", "models", "registry.yaml");
|
|
1911
|
+
if (!existsSync8(registryPath)) {
|
|
1569
1912
|
console.error("Error: Model registry not found.");
|
|
1570
1913
|
process.exit(1);
|
|
1571
1914
|
}
|
|
1572
|
-
const registry = parseYaml(
|
|
1915
|
+
const registry = parseYaml(readFileSync9(registryPath, "utf8"));
|
|
1573
1916
|
const models = registry.models || {};
|
|
1574
1917
|
const m = models[name];
|
|
1575
1918
|
if (!m) {
|
|
@@ -1594,12 +1937,12 @@ function handleShowModel(name) {
|
|
|
1594
1937
|
console.log();
|
|
1595
1938
|
}
|
|
1596
1939
|
function handleListProviders() {
|
|
1597
|
-
const providersPath =
|
|
1598
|
-
if (!
|
|
1940
|
+
const providersPath = join8(sourceRoot, ".ai", "models", "providers.yaml");
|
|
1941
|
+
if (!existsSync8(providersPath)) {
|
|
1599
1942
|
console.error("Error: Providers registry not found.");
|
|
1600
1943
|
process.exit(1);
|
|
1601
1944
|
}
|
|
1602
|
-
const reg = parseYaml(
|
|
1945
|
+
const reg = parseYaml(readFileSync9(providersPath, "utf8"));
|
|
1603
1946
|
const providers = reg.providers || {};
|
|
1604
1947
|
console.log(`
|
|
1605
1948
|
\u{1F50C} \x1B[36mAI Providers [v${version}]\x1B[0m`);
|
|
@@ -1614,12 +1957,12 @@ function handleListProviders() {
|
|
|
1614
1957
|
console.log();
|
|
1615
1958
|
}
|
|
1616
1959
|
function handleRouteModel(task) {
|
|
1617
|
-
const presetsPath =
|
|
1618
|
-
if (!
|
|
1960
|
+
const presetsPath = join8(sourceRoot, ".ai", "models", "routing-presets.yaml");
|
|
1961
|
+
if (!existsSync8(presetsPath)) {
|
|
1619
1962
|
console.error("Error: Routing presets not found.");
|
|
1620
1963
|
process.exit(1);
|
|
1621
1964
|
}
|
|
1622
|
-
const reg = parseYaml(
|
|
1965
|
+
const reg = parseYaml(readFileSync9(presetsPath, "utf8"));
|
|
1623
1966
|
const presets = reg.presets || {};
|
|
1624
1967
|
const preset = presets[task];
|
|
1625
1968
|
if (!preset) {
|
|
@@ -1634,12 +1977,12 @@ function handleRouteModel(task) {
|
|
|
1634
1977
|
console.log();
|
|
1635
1978
|
}
|
|
1636
1979
|
function handleListAdapters(options) {
|
|
1637
|
-
const adaptersPath =
|
|
1638
|
-
if (!
|
|
1980
|
+
const adaptersPath = join8(sourceRoot, ".ai", "adapters", "registry.yaml");
|
|
1981
|
+
if (!existsSync8(adaptersPath)) {
|
|
1639
1982
|
console.error("Error: Adapters registry not found.");
|
|
1640
1983
|
process.exit(1);
|
|
1641
1984
|
}
|
|
1642
|
-
const reg = parseYaml(
|
|
1985
|
+
const reg = parseYaml(readFileSync9(adaptersPath, "utf8"));
|
|
1643
1986
|
const adapters = reg.adapters || {};
|
|
1644
1987
|
if (options && options.json) {
|
|
1645
1988
|
console.log(JSON.stringify(adapters, null, 2));
|
|
@@ -1659,12 +2002,12 @@ function handleListAdapters(options) {
|
|
|
1659
2002
|
console.log("\nUse \x1B[36mshow-adapter <adapter-name>\x1B[0m to view detailed adapter metadata.\n");
|
|
1660
2003
|
}
|
|
1661
2004
|
function handleShowAdapter(name) {
|
|
1662
|
-
const adaptersPath =
|
|
1663
|
-
if (!
|
|
2005
|
+
const adaptersPath = join8(sourceRoot, ".ai", "adapters", "registry.yaml");
|
|
2006
|
+
if (!existsSync8(adaptersPath)) {
|
|
1664
2007
|
console.error("Error: Adapters registry not found.");
|
|
1665
2008
|
process.exit(1);
|
|
1666
2009
|
}
|
|
1667
|
-
const reg = parseYaml(
|
|
2010
|
+
const reg = parseYaml(readFileSync9(adaptersPath, "utf8"));
|
|
1668
2011
|
const adapters = reg.adapters || {};
|
|
1669
2012
|
const a = adapters[name];
|
|
1670
2013
|
if (!a) {
|
|
@@ -1680,8 +2023,8 @@ function handleShowAdapter(name) {
|
|
|
1680
2023
|
console.log();
|
|
1681
2024
|
}
|
|
1682
2025
|
function handleListSkills(options) {
|
|
1683
|
-
const skillsDir =
|
|
1684
|
-
if (!
|
|
2026
|
+
const skillsDir = join8(options.target, ".ai", "skills");
|
|
2027
|
+
if (!existsSync8(skillsDir)) {
|
|
1685
2028
|
console.log("\n\x1B[33m[Notice] .ai/skills directory is not initialized in the target workspace.\x1B[0m\n");
|
|
1686
2029
|
return;
|
|
1687
2030
|
}
|
|
@@ -1695,16 +2038,16 @@ function handleListSkills(options) {
|
|
|
1695
2038
|
console.log("\nUse \x1B[36mshow-skill <skill-name>\x1B[0m to read a skill's prompt text.\n");
|
|
1696
2039
|
}
|
|
1697
2040
|
function handleShowSkill(name, options) {
|
|
1698
|
-
const skillsDir =
|
|
1699
|
-
const skillFile =
|
|
1700
|
-
if (!
|
|
2041
|
+
const skillsDir = join8(options.target, ".ai", "skills");
|
|
2042
|
+
const skillFile = join8(skillsDir, name.endsWith(".md") ? name : `${name}.md`);
|
|
2043
|
+
if (!existsSync8(skillFile)) {
|
|
1701
2044
|
console.error(`\x1B[31mError: Skill '${name}' not found in target .ai/skills/.\x1B[0m`);
|
|
1702
2045
|
process.exit(1);
|
|
1703
2046
|
}
|
|
1704
2047
|
console.log(`
|
|
1705
2048
|
\u{1F4D6} \x1B[36mSkill Prompt: ${name}\x1B[0m`);
|
|
1706
2049
|
console.log("==================================================");
|
|
1707
|
-
console.log(
|
|
2050
|
+
console.log(readFileSync9(skillFile, "utf8"));
|
|
1708
2051
|
console.log();
|
|
1709
2052
|
}
|
|
1710
2053
|
function parseThresholdToBytes(val) {
|
|
@@ -1728,13 +2071,13 @@ function handleDoctorTokens(options) {
|
|
|
1728
2071
|
const filesFound = [];
|
|
1729
2072
|
const ignoredDirs = [".git", "node_modules", "dist", "build", ".next", ".expo", "bin", "assets", "docs", "web-build", "out", "coverage", ".nuxt", ".svelte-kit", "bower_components", "vendor"];
|
|
1730
2073
|
function scan(dir) {
|
|
1731
|
-
if (!
|
|
2074
|
+
if (!existsSync8(dir))
|
|
1732
2075
|
return;
|
|
1733
2076
|
const items = readdirSync(dir);
|
|
1734
2077
|
for (const item of items) {
|
|
1735
2078
|
if (ignoredDirs.includes(item))
|
|
1736
2079
|
continue;
|
|
1737
|
-
const fullPath =
|
|
2080
|
+
const fullPath = join8(dir, item);
|
|
1738
2081
|
try {
|
|
1739
2082
|
const stat = statSync(fullPath);
|
|
1740
2083
|
if (stat.isDirectory()) {
|
|
@@ -1796,19 +2139,19 @@ function handleValidateTemplate(name) {
|
|
|
1796
2139
|
console.log(` \x1B[32m\u2713\x1B[0m Registry key: ${k}`);
|
|
1797
2140
|
}
|
|
1798
2141
|
});
|
|
1799
|
-
const templateDir =
|
|
1800
|
-
if (!
|
|
2142
|
+
const templateDir = join8(sourceRoot, "examples", name);
|
|
2143
|
+
if (!existsSync8(templateDir)) {
|
|
1801
2144
|
console.error(` \x1B[31m\u2717 Source folder missing: examples/${name}\x1B[0m`);
|
|
1802
2145
|
errors++;
|
|
1803
2146
|
} else {
|
|
1804
2147
|
console.log(` \x1B[32m\u2713\x1B[0m Source folder: examples/${name}`);
|
|
1805
2148
|
if (Array.isArray(t.required_files)) {
|
|
1806
2149
|
t.required_files.forEach((f) => {
|
|
1807
|
-
const filePath =
|
|
1808
|
-
const globalPath =
|
|
1809
|
-
if (
|
|
2150
|
+
const filePath = join8(templateDir, f);
|
|
2151
|
+
const globalPath = join8(sourceRoot, f);
|
|
2152
|
+
if (existsSync8(filePath)) {
|
|
1810
2153
|
console.log(` \x1B[32m\u2713\x1B[0m Required file (template override): ${f}`);
|
|
1811
|
-
} else if (
|
|
2154
|
+
} else if (existsSync8(globalPath)) {
|
|
1812
2155
|
console.log(` \x1B[32m\u2713\x1B[0m Required file (global fallback): ${f}`);
|
|
1813
2156
|
} else {
|
|
1814
2157
|
console.error(` \x1B[31m\u2717 Required file missing: ${f}\x1B[0m`);
|
|
@@ -1847,22 +2190,22 @@ function handleValidateAdapter(name) {
|
|
|
1847
2190
|
console.log(` \x1B[32m\u2713\x1B[0m Registry key: ${k}`);
|
|
1848
2191
|
}
|
|
1849
2192
|
});
|
|
1850
|
-
const adapterDir =
|
|
1851
|
-
if (!
|
|
2193
|
+
const adapterDir = join8(sourceRoot, "adapters", name);
|
|
2194
|
+
if (!existsSync8(adapterDir)) {
|
|
1852
2195
|
console.error(` \x1B[31m\u2717 Source folder missing: adapters/${name}\x1B[0m`);
|
|
1853
2196
|
errors++;
|
|
1854
2197
|
} else {
|
|
1855
2198
|
console.log(` \x1B[32m\u2713\x1B[0m Source folder: adapters/${name}`);
|
|
1856
|
-
const setupFile =
|
|
1857
|
-
if (
|
|
2199
|
+
const setupFile = join8(adapterDir, "setup.md");
|
|
2200
|
+
if (existsSync8(setupFile)) {
|
|
1858
2201
|
console.log(` \x1B[32m\u2713\x1B[0m Required file: setup.md`);
|
|
1859
2202
|
} else {
|
|
1860
2203
|
console.error(` \x1B[31m\u2717 Required file missing: adapters/${name}/setup.md\x1B[0m`);
|
|
1861
2204
|
errors++;
|
|
1862
2205
|
}
|
|
1863
2206
|
if (a.rules_file) {
|
|
1864
|
-
const rulesFile =
|
|
1865
|
-
if (
|
|
2207
|
+
const rulesFile = join8(adapterDir, a.rules_file);
|
|
2208
|
+
if (existsSync8(rulesFile)) {
|
|
1866
2209
|
console.log(` \x1B[32m\u2713\x1B[0m Rules file: ${a.rules_file}`);
|
|
1867
2210
|
} else {
|
|
1868
2211
|
console.error(` \x1B[31m\u2717 Rules file missing: adapters/${name}/${a.rules_file}\x1B[0m`);
|
|
@@ -1883,18 +2226,18 @@ function handleValidateAdapter(name) {
|
|
|
1883
2226
|
}
|
|
1884
2227
|
}
|
|
1885
2228
|
function handleValidateSkill(name, options) {
|
|
1886
|
-
const skillsDir =
|
|
1887
|
-
let skillFile =
|
|
1888
|
-
if (!
|
|
1889
|
-
skillFile =
|
|
2229
|
+
const skillsDir = join8(options.target, ".ai", "skills");
|
|
2230
|
+
let skillFile = join8(skillsDir, name.endsWith(".md") ? name : `${name}.md`);
|
|
2231
|
+
if (!existsSync8(skillFile)) {
|
|
2232
|
+
skillFile = join8(sourceRoot, ".ai", "skills", name.endsWith(".md") ? name : `${name}.md`);
|
|
1890
2233
|
}
|
|
1891
|
-
if (!
|
|
2234
|
+
if (!existsSync8(skillFile)) {
|
|
1892
2235
|
console.error(`\x1B[31mError: Skill '${name}' not found.\x1B[0m`);
|
|
1893
2236
|
process.exit(1);
|
|
1894
2237
|
}
|
|
1895
2238
|
console.log(`
|
|
1896
2239
|
\u{1F4CB} \x1B[34mValidating Skill: ${name}\x1B[0m`);
|
|
1897
|
-
const content =
|
|
2240
|
+
const content = readFileSync9(skillFile, "utf8");
|
|
1898
2241
|
let errors = 0;
|
|
1899
2242
|
const reqHeaders = [
|
|
1900
2243
|
{ header: "# Purpose", regex: /^#\s+Purpose/mi },
|
|
@@ -1943,8 +2286,8 @@ Validating Template: ${name}`);
|
|
|
1943
2286
|
errors++;
|
|
1944
2287
|
}
|
|
1945
2288
|
});
|
|
1946
|
-
const templateDir =
|
|
1947
|
-
if (t.status === "stable" && !
|
|
2289
|
+
const templateDir = join8(sourceRoot, "examples", name);
|
|
2290
|
+
if (t.status === "stable" && !existsSync8(templateDir)) {
|
|
1948
2291
|
console.error(` \x1B[31m\u2717 Stable template source folder missing: examples/${name}\x1B[0m`);
|
|
1949
2292
|
errors++;
|
|
1950
2293
|
}
|
|
@@ -1978,7 +2321,7 @@ function handleDoctorRelease(options) {
|
|
|
1978
2321
|
let warnings = 0;
|
|
1979
2322
|
let packageVersion = "unknown";
|
|
1980
2323
|
try {
|
|
1981
|
-
const pkg = JSON.parse(
|
|
2324
|
+
const pkg = JSON.parse(readFileSync9(join8(sourceRoot, "package.json"), "utf8"));
|
|
1982
2325
|
packageVersion = pkg.version;
|
|
1983
2326
|
console.log(` \x1B[32m\u2713\x1B[0m package.json version: ${packageVersion}`);
|
|
1984
2327
|
} catch (e) {
|
|
@@ -1986,9 +2329,9 @@ function handleDoctorRelease(options) {
|
|
|
1986
2329
|
warnings++;
|
|
1987
2330
|
}
|
|
1988
2331
|
const checkInstallScript = (filename, regex) => {
|
|
1989
|
-
const filePath =
|
|
1990
|
-
if (
|
|
1991
|
-
const content =
|
|
2332
|
+
const filePath = join8(sourceRoot, filename);
|
|
2333
|
+
if (existsSync8(filePath)) {
|
|
2334
|
+
const content = readFileSync9(filePath, "utf8");
|
|
1992
2335
|
const match = content.match(regex);
|
|
1993
2336
|
if (match && match[1] === packageVersion) {
|
|
1994
2337
|
console.log(` \x1B[32m\u2713\x1B[0m ${filename} version aligns: ${match[1]}`);
|
|
@@ -2002,8 +2345,8 @@ function handleDoctorRelease(options) {
|
|
|
2002
2345
|
checkInstallScript("scripts/install.ps1", /\$VERSION\s*=\s*"([^"]+)"/i);
|
|
2003
2346
|
const blacklist = [".npmrc"];
|
|
2004
2347
|
blacklist.forEach((file) => {
|
|
2005
|
-
const fullPath =
|
|
2006
|
-
if (
|
|
2348
|
+
const fullPath = join8(sourceRoot, file);
|
|
2349
|
+
if (existsSync8(fullPath)) {
|
|
2007
2350
|
console.warn(` \x1B[33m[WARNING]\x1B[0m Blacklisted file found in release root: ${file}`);
|
|
2008
2351
|
warnings++;
|
|
2009
2352
|
} else {
|
|
@@ -2011,11 +2354,11 @@ function handleDoctorRelease(options) {
|
|
|
2011
2354
|
}
|
|
2012
2355
|
});
|
|
2013
2356
|
const scanSafety = (dir) => {
|
|
2014
|
-
if (!
|
|
2357
|
+
if (!existsSync8(dir))
|
|
2015
2358
|
return;
|
|
2016
2359
|
const items = readdirSync(dir);
|
|
2017
2360
|
for (const item of items) {
|
|
2018
|
-
const fullPath =
|
|
2361
|
+
const fullPath = join8(dir, item);
|
|
2019
2362
|
try {
|
|
2020
2363
|
const stat = statSync(fullPath);
|
|
2021
2364
|
if (stat.isDirectory()) {
|
|
@@ -2030,7 +2373,7 @@ function handleDoctorRelease(options) {
|
|
|
2030
2373
|
}
|
|
2031
2374
|
}
|
|
2032
2375
|
};
|
|
2033
|
-
scanSafety(
|
|
2376
|
+
scanSafety(join8(sourceRoot, "examples"));
|
|
2034
2377
|
console.log("\n==================================================");
|
|
2035
2378
|
if (warnings > 0) {
|
|
2036
2379
|
console.warn(` \x1B[33mRelease doctor complete with ${warnings} warnings.\x1B[0m
|
|
@@ -2043,11 +2386,11 @@ function scanTarget(targetDir) {
|
|
|
2043
2386
|
const files = [];
|
|
2044
2387
|
let ignoredCount = 0;
|
|
2045
2388
|
function walk(dir) {
|
|
2046
|
-
if (!
|
|
2389
|
+
if (!existsSync8(dir))
|
|
2047
2390
|
return;
|
|
2048
2391
|
const items = readdirSync(dir);
|
|
2049
2392
|
for (const item of items) {
|
|
2050
|
-
const fullPath =
|
|
2393
|
+
const fullPath = join8(dir, item);
|
|
2051
2394
|
const relPath = relative(targetDir, fullPath).replace(/\\/g, "/");
|
|
2052
2395
|
if (shouldIgnorePath(relPath)) {
|
|
2053
2396
|
ignoredCount++;
|
|
@@ -2086,7 +2429,7 @@ function detectFrameworkSignals(files, targetDir) {
|
|
|
2086
2429
|
if (hasFile("package.json")) {
|
|
2087
2430
|
signals.push("Node.js");
|
|
2088
2431
|
try {
|
|
2089
|
-
const pkg = JSON.parse(
|
|
2432
|
+
const pkg = JSON.parse(readFileSync9(join8(targetDir, "package.json"), "utf8"));
|
|
2090
2433
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2091
2434
|
if (deps["react"])
|
|
2092
2435
|
signals.push("React");
|
|
@@ -2168,8 +2511,8 @@ function detectAiDevOsSignals(files) {
|
|
|
2168
2511
|
}
|
|
2169
2512
|
function detectRisks(files, targetDir) {
|
|
2170
2513
|
const risks = [];
|
|
2171
|
-
const gitignorePath =
|
|
2172
|
-
const gitignoreContent =
|
|
2514
|
+
const gitignorePath = join8(targetDir, ".gitignore");
|
|
2515
|
+
const gitignoreContent = existsSync8(gitignorePath) ? readFileSync9(gitignorePath, "utf8") : "";
|
|
2173
2516
|
const hasFolder = (name) => files.some((f) => f.relPath.split("/")[0] === name);
|
|
2174
2517
|
if (hasFolder("node_modules") && !gitignoreContent.includes("node_modules")) {
|
|
2175
2518
|
risks.push({
|
|
@@ -2226,13 +2569,13 @@ function buildMemoryIndex(targetDir) {
|
|
|
2226
2569
|
};
|
|
2227
2570
|
}
|
|
2228
2571
|
function writeMemoryFiles(targetDir, index) {
|
|
2229
|
-
const intelDir =
|
|
2230
|
-
if (!
|
|
2231
|
-
|
|
2572
|
+
const intelDir = join8(targetDir, ".ai", "intelligence");
|
|
2573
|
+
if (!existsSync8(intelDir)) {
|
|
2574
|
+
mkdirSync3(intelDir, { recursive: true });
|
|
2232
2575
|
}
|
|
2233
|
-
const hashJsonPath =
|
|
2234
|
-
|
|
2235
|
-
const summaryMdPath =
|
|
2576
|
+
const hashJsonPath = join8(intelDir, "memory.hash.json");
|
|
2577
|
+
writeFileSync4(hashJsonPath, JSON.stringify(index, null, 2), "utf8");
|
|
2578
|
+
const summaryMdPath = join8(intelDir, "memory.summary.md");
|
|
2236
2579
|
let md = `# MultiModel Dev OS Repository Memory Summary
|
|
2237
2580
|
|
|
2238
2581
|
`;
|
|
@@ -2281,16 +2624,16 @@ function writeMemoryFiles(targetDir, index) {
|
|
|
2281
2624
|
md += `- ${step}
|
|
2282
2625
|
`;
|
|
2283
2626
|
});
|
|
2284
|
-
|
|
2627
|
+
writeFileSync4(summaryMdPath, md, "utf8");
|
|
2285
2628
|
}
|
|
2286
2629
|
function diffMemory(targetDir) {
|
|
2287
|
-
const hashJsonPath =
|
|
2288
|
-
if (!
|
|
2630
|
+
const hashJsonPath = join8(targetDir, ".ai", "intelligence", "memory.hash.json");
|
|
2631
|
+
if (!existsSync8(hashJsonPath)) {
|
|
2289
2632
|
return null;
|
|
2290
2633
|
}
|
|
2291
2634
|
let existing;
|
|
2292
2635
|
try {
|
|
2293
|
-
existing = JSON.parse(
|
|
2636
|
+
existing = JSON.parse(readFileSync9(hashJsonPath, "utf8"));
|
|
2294
2637
|
} catch (e) {
|
|
2295
2638
|
return null;
|
|
2296
2639
|
}
|
|
@@ -2425,9 +2768,9 @@ function handleMemoryDiff(options) {
|
|
|
2425
2768
|
console.log();
|
|
2426
2769
|
}
|
|
2427
2770
|
function handleFeedbackAdd(options) {
|
|
2428
|
-
const intelDir =
|
|
2429
|
-
if (!options.dryRun && !
|
|
2430
|
-
|
|
2771
|
+
const intelDir = join8(options.target, ".ai", "intelligence");
|
|
2772
|
+
if (!options.dryRun && !existsSync8(intelDir)) {
|
|
2773
|
+
mkdirSync3(intelDir, { recursive: true });
|
|
2431
2774
|
}
|
|
2432
2775
|
const addIdx = process.argv.indexOf("add");
|
|
2433
2776
|
const text = addIdx !== -1 && process.argv[addIdx + 1] && !process.argv[addIdx + 1].startsWith("-") ? process.argv[addIdx + 1] : null;
|
|
@@ -2452,15 +2795,15 @@ function handleFeedbackAdd(options) {
|
|
|
2452
2795
|
};
|
|
2453
2796
|
rawRecord.hash = createHash2("sha256").update(JSON.stringify(rawRecord)).digest("hex");
|
|
2454
2797
|
const recordLine = JSON.stringify(rawRecord) + "\n";
|
|
2455
|
-
const feedbackLogPath =
|
|
2798
|
+
const feedbackLogPath = join8(intelDir, "feedback-log.jsonl");
|
|
2456
2799
|
if (options.dryRun) {
|
|
2457
2800
|
console.log(`\x1B[36m[DRY-RUN] WOULD APPEND TO ${feedbackLogPath}:\x1B[0m`);
|
|
2458
2801
|
console.log(recordLine.trim());
|
|
2459
2802
|
} else {
|
|
2460
2803
|
try {
|
|
2461
2804
|
let isDuplicate = false;
|
|
2462
|
-
if (
|
|
2463
|
-
const lines =
|
|
2805
|
+
if (existsSync8(feedbackLogPath)) {
|
|
2806
|
+
const lines = readFileSync9(feedbackLogPath, "utf8").split("\n");
|
|
2464
2807
|
for (const line of lines) {
|
|
2465
2808
|
if (!line.trim())
|
|
2466
2809
|
continue;
|
|
@@ -2478,7 +2821,7 @@ function handleFeedbackAdd(options) {
|
|
|
2478
2821
|
console.log(`\x1B[33mFeedback already exists. Skipping duplicate entry.\x1B[0m`);
|
|
2479
2822
|
return;
|
|
2480
2823
|
}
|
|
2481
|
-
|
|
2824
|
+
writeFileSync4(feedbackLogPath, recordLine, { flag: "a", encoding: "utf8" });
|
|
2482
2825
|
console.log(`\u2714 Feedback successfully added (ID: ${rawRecord.id})`);
|
|
2483
2826
|
} catch (e) {
|
|
2484
2827
|
console.error(`\x1B[31mError: Failed to write to feedback-log.jsonl: ${e.message}\x1B[0m`);
|
|
@@ -2487,13 +2830,13 @@ function handleFeedbackAdd(options) {
|
|
|
2487
2830
|
}
|
|
2488
2831
|
}
|
|
2489
2832
|
function handleFeedbackList(options) {
|
|
2490
|
-
const feedbackLogPath =
|
|
2491
|
-
if (!
|
|
2833
|
+
const feedbackLogPath = join8(options.target, ".ai", "intelligence", "feedback-log.jsonl");
|
|
2834
|
+
if (!existsSync8(feedbackLogPath)) {
|
|
2492
2835
|
console.log("No feedback logged yet.");
|
|
2493
2836
|
return;
|
|
2494
2837
|
}
|
|
2495
2838
|
try {
|
|
2496
|
-
const content =
|
|
2839
|
+
const content = readFileSync9(feedbackLogPath, "utf8");
|
|
2497
2840
|
const lines = content.split("\n").filter((l) => l.trim() !== "");
|
|
2498
2841
|
if (lines.length === 0) {
|
|
2499
2842
|
console.log("No feedback logged yet.");
|
|
@@ -2525,14 +2868,14 @@ function handleFeedbackList(options) {
|
|
|
2525
2868
|
}
|
|
2526
2869
|
}
|
|
2527
2870
|
function handleFeedbackSummarize(options) {
|
|
2528
|
-
const intelDir =
|
|
2529
|
-
const feedbackLogPath =
|
|
2530
|
-
if (!
|
|
2871
|
+
const intelDir = join8(options.target, ".ai", "intelligence");
|
|
2872
|
+
const feedbackLogPath = join8(intelDir, "feedback-log.jsonl");
|
|
2873
|
+
if (!existsSync8(feedbackLogPath)) {
|
|
2531
2874
|
console.log("No feedback logs found to compile.");
|
|
2532
2875
|
return;
|
|
2533
2876
|
}
|
|
2534
2877
|
try {
|
|
2535
|
-
const content =
|
|
2878
|
+
const content = readFileSync9(feedbackLogPath, "utf8");
|
|
2536
2879
|
const lines = content.split("\n").filter((l) => l.trim() !== "");
|
|
2537
2880
|
if (lines.length === 0) {
|
|
2538
2881
|
console.log("No feedback logs found to compile.");
|
|
@@ -2577,12 +2920,12 @@ function handleFeedbackSummarize(options) {
|
|
|
2577
2920
|
`;
|
|
2578
2921
|
});
|
|
2579
2922
|
});
|
|
2580
|
-
const targetRulesPath =
|
|
2923
|
+
const targetRulesPath = join8(intelDir, "learning-rules.md");
|
|
2581
2924
|
if (options.dryRun) {
|
|
2582
2925
|
console.log(`\x1B[36m[DRY-RUN] WOULD WRITE TO ${targetRulesPath}:\x1B[0m`);
|
|
2583
2926
|
console.log(md);
|
|
2584
2927
|
} else {
|
|
2585
|
-
|
|
2928
|
+
writeFileSync4(targetRulesPath, md, "utf8");
|
|
2586
2929
|
console.log(`\u2714 Compiled ${lines.length} feedback items into learning rules in .ai/intelligence/learning-rules.md`);
|
|
2587
2930
|
}
|
|
2588
2931
|
} catch (e) {
|
|
@@ -2591,9 +2934,9 @@ function handleFeedbackSummarize(options) {
|
|
|
2591
2934
|
}
|
|
2592
2935
|
}
|
|
2593
2936
|
function handleImprovePropose(options) {
|
|
2594
|
-
const proposalsDir =
|
|
2595
|
-
if (!options.dryRun && !
|
|
2596
|
-
|
|
2937
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
2938
|
+
if (!options.dryRun && !existsSync8(proposalsDir)) {
|
|
2939
|
+
mkdirSync3(proposalsDir, { recursive: true });
|
|
2597
2940
|
}
|
|
2598
2941
|
const now = /* @__PURE__ */ new Date();
|
|
2599
2942
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -2609,15 +2952,15 @@ function handleImprovePropose(options) {
|
|
|
2609
2952
|
let suggestedChange = "No code suggestions compiled.";
|
|
2610
2953
|
let verifyCommand = "npm run verify";
|
|
2611
2954
|
let rollbackPlan = "git checkout -- .";
|
|
2612
|
-
const gitignorePath =
|
|
2613
|
-
const agentsPath =
|
|
2614
|
-
if (!
|
|
2955
|
+
const gitignorePath = join8(options.target, ".gitignore");
|
|
2956
|
+
const agentsPath = join8(options.target, "AGENTS.md");
|
|
2957
|
+
if (!existsSync8(gitignorePath)) {
|
|
2615
2958
|
problem = "Missing .gitignore file in target workspace. AI agents may scan large build directories and run out of token context.";
|
|
2616
2959
|
evidence = `.gitignore file is not present at root directory: ${options.target}`;
|
|
2617
2960
|
affectedFiles = [".gitignore"];
|
|
2618
2961
|
suggestedChange = "Create a standard .gitignore file to exclude node_modules, build/ and dist/ directories.";
|
|
2619
2962
|
rollbackPlan = "git clean -fd .gitignore";
|
|
2620
|
-
} else if (!
|
|
2963
|
+
} else if (!existsSync8(agentsPath)) {
|
|
2621
2964
|
problem = "Missing AGENTS.md document in target workspace. Models will lack stack-specific implementation blueprints.";
|
|
2622
2965
|
evidence = `AGENTS.md file is not present at root directory: ${options.target}`;
|
|
2623
2966
|
affectedFiles = ["AGENTS.md"];
|
|
@@ -2671,18 +3014,18 @@ ${suggestedChange}
|
|
|
2671
3014
|
* **Rollback Command**: \`${rollbackPlan}\`
|
|
2672
3015
|
* **Approval Status**: PENDING (Manual approval required before implementation)
|
|
2673
3016
|
`;
|
|
2674
|
-
const proposalFile =
|
|
3017
|
+
const proposalFile = join8(proposalsDir, `${id}.md`);
|
|
2675
3018
|
if (options.dryRun) {
|
|
2676
3019
|
console.log(`\x1B[36m[DRY-RUN] WOULD WRITE PROPOSAL TO ${proposalFile}:\x1B[0m`);
|
|
2677
3020
|
console.log(md);
|
|
2678
3021
|
} else {
|
|
2679
|
-
|
|
3022
|
+
writeFileSync4(proposalFile, md, "utf8");
|
|
2680
3023
|
console.log(`\u2714 Created codebase improvement proposal: .ai/proposals/${id}.md`);
|
|
2681
3024
|
}
|
|
2682
3025
|
}
|
|
2683
3026
|
function handleImproveReview(options) {
|
|
2684
|
-
const proposalsDir =
|
|
2685
|
-
if (!
|
|
3027
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
3028
|
+
if (!existsSync8(proposalsDir)) {
|
|
2686
3029
|
console.log("No improvement proposals found.");
|
|
2687
3030
|
return;
|
|
2688
3031
|
}
|
|
@@ -2696,8 +3039,8 @@ function handleImproveReview(options) {
|
|
|
2696
3039
|
\u{1F4CB} \x1B[36mCodebase Improvement Proposals\x1B[0m`);
|
|
2697
3040
|
console.log("==================================================");
|
|
2698
3041
|
files.forEach((file) => {
|
|
2699
|
-
const fullPath =
|
|
2700
|
-
const content =
|
|
3042
|
+
const fullPath = join8(proposalsDir, file);
|
|
3043
|
+
const content = readFileSync9(fullPath, "utf8");
|
|
2701
3044
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2702
3045
|
if (!fmMatch)
|
|
2703
3046
|
return;
|
|
@@ -2720,8 +3063,8 @@ function handleImproveReview(options) {
|
|
|
2720
3063
|
}
|
|
2721
3064
|
}
|
|
2722
3065
|
function handleImproveStatus(options) {
|
|
2723
|
-
const proposalsDir =
|
|
2724
|
-
if (!
|
|
3066
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
3067
|
+
if (!existsSync8(proposalsDir)) {
|
|
2725
3068
|
console.log("Improvement Proposal Engine Status:");
|
|
2726
3069
|
console.log(" Total Proposals: 0");
|
|
2727
3070
|
console.log(" Pending Approval: 0");
|
|
@@ -2733,7 +3076,7 @@ function handleImproveStatus(options) {
|
|
|
2733
3076
|
let approved = 0;
|
|
2734
3077
|
let rejected = 0;
|
|
2735
3078
|
files.forEach((file) => {
|
|
2736
|
-
const content =
|
|
3079
|
+
const content = readFileSync9(join8(proposalsDir, file), "utf8");
|
|
2737
3080
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2738
3081
|
if (fmMatch) {
|
|
2739
3082
|
const metadata = parseYaml(fmMatch[1]) || {};
|
|
@@ -2769,7 +3112,7 @@ function validatePath(targetRoot, relPath) {
|
|
|
2769
3112
|
}
|
|
2770
3113
|
const resolved = resolve3(targetRoot, relPath);
|
|
2771
3114
|
const relativeFromRoot = relative(targetRoot, resolved);
|
|
2772
|
-
if (relativeFromRoot.startsWith("..") ||
|
|
3115
|
+
if (relativeFromRoot.startsWith("..") || isAbsolute2(relativeFromRoot) || resolved === targetRoot) {
|
|
2773
3116
|
return { valid: false, reason: `Path '${relPath}' resolves outside the target root.`, type: "outside" };
|
|
2774
3117
|
}
|
|
2775
3118
|
const parts = relativeFromRoot.replace(/\\/g, "/").split("/");
|
|
@@ -2809,11 +3152,11 @@ function validateProposal(proposalFile, targetRoot) {
|
|
|
2809
3152
|
permissions: { status: "skip" },
|
|
2810
3153
|
constraints: { status: "skip" }
|
|
2811
3154
|
};
|
|
2812
|
-
if (!
|
|
3155
|
+
if (!existsSync8(proposalFile)) {
|
|
2813
3156
|
gates.frontmatter = { status: "fail", reason: "missing frontmatter" };
|
|
2814
3157
|
return { valid: false, reason: "missing frontmatter", gates };
|
|
2815
3158
|
}
|
|
2816
|
-
const content =
|
|
3159
|
+
const content = readFileSync9(proposalFile, "utf8");
|
|
2817
3160
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2818
3161
|
if (!fmMatch) {
|
|
2819
3162
|
gates.frontmatter = { status: "fail", reason: "missing frontmatter" };
|
|
@@ -2924,7 +3267,7 @@ function validateProposal(proposalFile, targetRoot) {
|
|
|
2924
3267
|
constraintsStatus = "fail";
|
|
2925
3268
|
constraintsReason = `unsupported operation type`;
|
|
2926
3269
|
}
|
|
2927
|
-
} else if (
|
|
3270
|
+
} else if (existsSync8(resolvedPath) && !op.overwrite) {
|
|
2928
3271
|
if (constraintsStatus === "pass") {
|
|
2929
3272
|
constraintsStatus = "fail";
|
|
2930
3273
|
constraintsReason = `create_file target exists without overwrite`;
|
|
@@ -2943,13 +3286,13 @@ function validateProposal(proposalFile, targetRoot) {
|
|
|
2943
3286
|
constraintsStatus = "fail";
|
|
2944
3287
|
constraintsReason = `unsupported operation type`;
|
|
2945
3288
|
}
|
|
2946
|
-
} else if (!
|
|
3289
|
+
} else if (!existsSync8(resolvedPath)) {
|
|
2947
3290
|
if (constraintsStatus === "pass") {
|
|
2948
3291
|
constraintsStatus = "fail";
|
|
2949
3292
|
constraintsReason = `replace_text zero matches`;
|
|
2950
3293
|
}
|
|
2951
3294
|
} else {
|
|
2952
|
-
const fileContent =
|
|
3295
|
+
const fileContent = readFileSync9(resolvedPath, "utf8");
|
|
2953
3296
|
let count = 0;
|
|
2954
3297
|
let pos = fileContent.indexOf(op.find);
|
|
2955
3298
|
while (pos !== -1) {
|
|
@@ -3118,7 +3461,7 @@ function handleImproveDiff(proposalFile, options) {
|
|
|
3118
3461
|
console.log(`
|
|
3119
3462
|
\x1B[33m[Operation #${idx + 1}] Target: ${op.path}\x1B[0m`);
|
|
3120
3463
|
if (type === "create_file") {
|
|
3121
|
-
const exists =
|
|
3464
|
+
const exists = existsSync8(op.resolvedPath);
|
|
3122
3465
|
if (exists) {
|
|
3123
3466
|
console.log(` \x1B[31m\u26A0\uFE0F [Overwriting existing file]\x1B[0m`);
|
|
3124
3467
|
} else {
|
|
@@ -3128,10 +3471,10 @@ function handleImproveDiff(proposalFile, options) {
|
|
|
3128
3471
|
console.log(` + [File content: ${linesCount} line(s), overwrite: ${!!op.overwrite}]`);
|
|
3129
3472
|
printTruncatedLines(op.content, " +", "\x1B[32m");
|
|
3130
3473
|
} else if (type === "append_line") {
|
|
3131
|
-
const exists =
|
|
3474
|
+
const exists = existsSync8(op.resolvedPath);
|
|
3132
3475
|
let currentFileContent = "";
|
|
3133
3476
|
if (exists) {
|
|
3134
|
-
currentFileContent =
|
|
3477
|
+
currentFileContent = readFileSync9(op.resolvedPath, "utf8");
|
|
3135
3478
|
}
|
|
3136
3479
|
const fileLines = currentFileContent.split(/\r?\n/);
|
|
3137
3480
|
const lineExists = fileLines.some((l) => l.trim() === op.line.trim());
|
|
@@ -3164,14 +3507,14 @@ function handleImproveApply(proposalFile, options) {
|
|
|
3164
3507
|
if (!validation.valid) {
|
|
3165
3508
|
console.error(`\x1B[31mValidation FAILED: ${validation.reason}\x1B[0m`);
|
|
3166
3509
|
const applyId2 = `apply-${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14)}`;
|
|
3167
|
-
const logDir2 =
|
|
3168
|
-
if (!
|
|
3510
|
+
const logDir2 = join8(options.target, ".ai", "proposals");
|
|
3511
|
+
if (!existsSync8(logDir2)) {
|
|
3169
3512
|
try {
|
|
3170
|
-
|
|
3513
|
+
mkdirSync3(logDir2, { recursive: true });
|
|
3171
3514
|
} catch (e) {
|
|
3172
3515
|
}
|
|
3173
3516
|
}
|
|
3174
|
-
const logFile2 =
|
|
3517
|
+
const logFile2 = join8(logDir2, "apply-log.jsonl");
|
|
3175
3518
|
const record2 = {
|
|
3176
3519
|
id: applyId2,
|
|
3177
3520
|
proposal_id: validation.proposalId || basename(proposalFile, ".md"),
|
|
@@ -3186,7 +3529,7 @@ function handleImproveApply(proposalFile, options) {
|
|
|
3186
3529
|
notes: `Validation failed: ${validation.reason}`
|
|
3187
3530
|
};
|
|
3188
3531
|
try {
|
|
3189
|
-
|
|
3532
|
+
writeFileSync4(logFile2, JSON.stringify(record2) + "\n", { flag: "a", encoding: "utf8" });
|
|
3190
3533
|
} catch (err) {
|
|
3191
3534
|
}
|
|
3192
3535
|
process.exit(1);
|
|
@@ -3214,8 +3557,8 @@ Applying changes...`);
|
|
|
3214
3557
|
if (!filesChanged.includes(relPath)) {
|
|
3215
3558
|
filesChanged.push(relPath);
|
|
3216
3559
|
}
|
|
3217
|
-
if (
|
|
3218
|
-
const fileContent =
|
|
3560
|
+
if (existsSync8(op.resolvedPath)) {
|
|
3561
|
+
const fileContent = readFileSync9(op.resolvedPath, "utf8");
|
|
3219
3562
|
beforeHashes[relPath] = getSha256(fileContent);
|
|
3220
3563
|
} else {
|
|
3221
3564
|
beforeHashes[relPath] = null;
|
|
@@ -3225,12 +3568,12 @@ Applying changes...`);
|
|
|
3225
3568
|
const relPath = relative(options.target, op.resolvedPath).replace(/\\/g, "/");
|
|
3226
3569
|
console.log(` Executing Operation #${idx + 1} (${op.type}) on '${relPath}'...`);
|
|
3227
3570
|
if (op.type === "create_file") {
|
|
3228
|
-
const dir =
|
|
3229
|
-
if (!
|
|
3230
|
-
|
|
3571
|
+
const dir = dirname4(op.resolvedPath);
|
|
3572
|
+
if (!existsSync8(dir)) {
|
|
3573
|
+
mkdirSync3(dir, { recursive: true });
|
|
3231
3574
|
}
|
|
3232
|
-
const exists =
|
|
3233
|
-
|
|
3575
|
+
const exists = existsSync8(op.resolvedPath);
|
|
3576
|
+
writeFileSync4(op.resolvedPath, op.content, "utf8");
|
|
3234
3577
|
if (exists) {
|
|
3235
3578
|
console.log(` [OVERWRITTEN] Overwrote existing file '${relPath}'.`);
|
|
3236
3579
|
} else {
|
|
@@ -3238,8 +3581,8 @@ Applying changes...`);
|
|
|
3238
3581
|
}
|
|
3239
3582
|
} else if (op.type === "append_line") {
|
|
3240
3583
|
let content = "";
|
|
3241
|
-
if (
|
|
3242
|
-
content =
|
|
3584
|
+
if (existsSync8(op.resolvedPath)) {
|
|
3585
|
+
content = readFileSync9(op.resolvedPath, "utf8");
|
|
3243
3586
|
}
|
|
3244
3587
|
const fileLines = content.split(/\r?\n/);
|
|
3245
3588
|
const lineExists = fileLines.some((l) => l.trim() === op.line.trim());
|
|
@@ -3249,17 +3592,17 @@ Applying changes...`);
|
|
|
3249
3592
|
newContent += "\n";
|
|
3250
3593
|
}
|
|
3251
3594
|
newContent += op.line + "\n";
|
|
3252
|
-
const dir =
|
|
3253
|
-
if (!
|
|
3254
|
-
|
|
3595
|
+
const dir = dirname4(op.resolvedPath);
|
|
3596
|
+
if (!existsSync8(dir)) {
|
|
3597
|
+
mkdirSync3(dir, { recursive: true });
|
|
3255
3598
|
}
|
|
3256
|
-
|
|
3599
|
+
writeFileSync4(op.resolvedPath, newContent, "utf8");
|
|
3257
3600
|
console.log(` [APPENDED] Appended 1 line to '${relPath}'.`);
|
|
3258
3601
|
} else {
|
|
3259
3602
|
console.log(` [IDEMPOTENT] Line already exists in '${relPath}'. Skipping append.`);
|
|
3260
3603
|
}
|
|
3261
3604
|
} else if (op.type === "replace_text") {
|
|
3262
|
-
const fileContent =
|
|
3605
|
+
const fileContent = readFileSync9(op.resolvedPath, "utf8");
|
|
3263
3606
|
let count = 0;
|
|
3264
3607
|
let pos = fileContent.indexOf(op.find);
|
|
3265
3608
|
while (pos !== -1) {
|
|
@@ -3274,14 +3617,14 @@ Applying changes...`);
|
|
|
3274
3617
|
if (count > 0)
|
|
3275
3618
|
count = 1;
|
|
3276
3619
|
}
|
|
3277
|
-
|
|
3620
|
+
writeFileSync4(op.resolvedPath, newContent, "utf8");
|
|
3278
3621
|
console.log(` [REPLACED] Replaced ${count} occurrence(s) of find text in '${relPath}'.`);
|
|
3279
3622
|
}
|
|
3280
3623
|
});
|
|
3281
3624
|
filesChanged.forEach((relPath) => {
|
|
3282
3625
|
const fullPath = resolve3(options.target, relPath);
|
|
3283
|
-
if (
|
|
3284
|
-
const fileContent =
|
|
3626
|
+
if (existsSync8(fullPath)) {
|
|
3627
|
+
const fileContent = readFileSync9(fullPath, "utf8");
|
|
3285
3628
|
afterHashes[relPath] = getSha256(fileContent);
|
|
3286
3629
|
} else {
|
|
3287
3630
|
afterHashes[relPath] = null;
|
|
@@ -3293,11 +3636,11 @@ Applying changes...`);
|
|
|
3293
3636
|
notes = `Execution error: ${e.message}`;
|
|
3294
3637
|
console.error(`\x1B[31mError applying proposal: ${e.message}\x1B[0m`);
|
|
3295
3638
|
}
|
|
3296
|
-
const logDir =
|
|
3297
|
-
if (!
|
|
3298
|
-
|
|
3639
|
+
const logDir = join8(options.target, ".ai", "proposals");
|
|
3640
|
+
if (!existsSync8(logDir)) {
|
|
3641
|
+
mkdirSync3(logDir, { recursive: true });
|
|
3299
3642
|
}
|
|
3300
|
-
const logFile =
|
|
3643
|
+
const logFile = join8(logDir, "apply-log.jsonl");
|
|
3301
3644
|
const record = {
|
|
3302
3645
|
id: applyId,
|
|
3303
3646
|
proposal_id: proposalId,
|
|
@@ -3312,7 +3655,7 @@ Applying changes...`);
|
|
|
3312
3655
|
notes
|
|
3313
3656
|
};
|
|
3314
3657
|
try {
|
|
3315
|
-
|
|
3658
|
+
writeFileSync4(logFile, JSON.stringify(record) + "\n", { flag: "a", encoding: "utf8" });
|
|
3316
3659
|
} catch (err) {
|
|
3317
3660
|
console.error(`\x1B[31mFailed to write to audit log: ${err.message}\x1B[0m`);
|
|
3318
3661
|
}
|
|
@@ -3327,13 +3670,13 @@ Applying changes...`);
|
|
|
3327
3670
|
}
|
|
3328
3671
|
}
|
|
3329
3672
|
function handleImproveLog(options) {
|
|
3330
|
-
const logFile =
|
|
3331
|
-
if (!
|
|
3673
|
+
const logFile = join8(options.target, ".ai", "proposals", "apply-log.jsonl");
|
|
3674
|
+
if (!existsSync8(logFile)) {
|
|
3332
3675
|
console.log("No apply log found.");
|
|
3333
3676
|
return;
|
|
3334
3677
|
}
|
|
3335
3678
|
try {
|
|
3336
|
-
const lines =
|
|
3679
|
+
const lines = readFileSync9(logFile, "utf8").trim().split(/\r?\n/);
|
|
3337
3680
|
console.log(`
|
|
3338
3681
|
\u{1F4DC} \x1B[36mApplied Proposals Audit Log\x1B[0m`);
|
|
3339
3682
|
console.log("==================================================");
|
|
@@ -3363,9 +3706,9 @@ function handleStatus(options) {
|
|
|
3363
3706
|
let pkgName = "unknown";
|
|
3364
3707
|
let pkgVersion2 = "unknown";
|
|
3365
3708
|
try {
|
|
3366
|
-
const pkgPath =
|
|
3367
|
-
if (
|
|
3368
|
-
const pkg = JSON.parse(
|
|
3709
|
+
const pkgPath = join8(options.target, "package.json");
|
|
3710
|
+
if (existsSync8(pkgPath)) {
|
|
3711
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
3369
3712
|
pkgName = pkg.name || pkgName;
|
|
3370
3713
|
pkgVersion2 = pkg.version || pkgVersion2;
|
|
3371
3714
|
}
|
|
@@ -3380,12 +3723,12 @@ function handleStatus(options) {
|
|
|
3380
3723
|
console.log(` \x1B[33mFramework & Dependency Signals:\x1B[0m`);
|
|
3381
3724
|
console.log(` Frameworks: ${frameworkSignals.join(", ") || "None"}`);
|
|
3382
3725
|
console.log(` Dependencies: ${dependencySignals.join(", ") || "None"}`);
|
|
3383
|
-
const memoryHashPath =
|
|
3726
|
+
const memoryHashPath = join8(options.target, ".ai", "intelligence", "memory.hash.json");
|
|
3384
3727
|
let memoryStatus = "\x1B[31mMISSING\x1B[0m";
|
|
3385
3728
|
let lastBuildTime = "N/A";
|
|
3386
|
-
if (
|
|
3729
|
+
if (existsSync8(memoryHashPath)) {
|
|
3387
3730
|
try {
|
|
3388
|
-
const memObj = JSON.parse(
|
|
3731
|
+
const memObj = JSON.parse(readFileSync9(memoryHashPath, "utf8"));
|
|
3389
3732
|
lastBuildTime = memObj.generated_at || "N/A";
|
|
3390
3733
|
const diff = diffMemory(options.target);
|
|
3391
3734
|
if (diff) {
|
|
@@ -3402,30 +3745,30 @@ function handleStatus(options) {
|
|
|
3402
3745
|
console.log(` \x1B[33mMemory State:\x1B[0m`);
|
|
3403
3746
|
console.log(` Status: ${memoryStatus}`);
|
|
3404
3747
|
console.log(` Last Built: ${lastBuildTime}`);
|
|
3405
|
-
const feedbackPath =
|
|
3748
|
+
const feedbackPath = join8(options.target, ".ai", "intelligence", "feedback-log.jsonl");
|
|
3406
3749
|
let feedbackCount = 0;
|
|
3407
|
-
if (
|
|
3750
|
+
if (existsSync8(feedbackPath)) {
|
|
3408
3751
|
try {
|
|
3409
|
-
feedbackCount =
|
|
3752
|
+
feedbackCount = readFileSync9(feedbackPath, "utf8").trim().split(/\r?\n/).filter((l) => l.trim() !== "").length;
|
|
3410
3753
|
} catch (e) {
|
|
3411
3754
|
}
|
|
3412
3755
|
}
|
|
3413
|
-
const rulesPath =
|
|
3414
|
-
const rulesStatus =
|
|
3756
|
+
const rulesPath = join8(options.target, ".ai", "intelligence", "learning-rules.md");
|
|
3757
|
+
const rulesStatus = existsSync8(rulesPath) ? "\x1B[32mPRESENT\x1B[0m" : "\x1B[31mMISSING\x1B[0m";
|
|
3415
3758
|
console.log(` \x1B[33mFeedback Loop & Rules:\x1B[0m`);
|
|
3416
3759
|
console.log(` Feedback Count: ${feedbackCount}`);
|
|
3417
3760
|
console.log(` Learning Rules: ${rulesStatus}`);
|
|
3418
|
-
const proposalsDir =
|
|
3761
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
3419
3762
|
let pendingCount = 0;
|
|
3420
3763
|
let approvedCount = 0;
|
|
3421
3764
|
let rejectedCount = 0;
|
|
3422
3765
|
let totalProposals = 0;
|
|
3423
|
-
if (
|
|
3766
|
+
if (existsSync8(proposalsDir)) {
|
|
3424
3767
|
try {
|
|
3425
3768
|
const propFiles = readdirSync(proposalsDir).filter((f) => f.startsWith("proposal-") && f.endsWith(".md"));
|
|
3426
3769
|
totalProposals = propFiles.length;
|
|
3427
3770
|
propFiles.forEach((file) => {
|
|
3428
|
-
const content =
|
|
3771
|
+
const content = readFileSync9(join8(proposalsDir, file), "utf8");
|
|
3429
3772
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3430
3773
|
if (fmMatch) {
|
|
3431
3774
|
const metadata = parseYaml(fmMatch[1]) || {};
|
|
@@ -3446,26 +3789,26 @@ function handleStatus(options) {
|
|
|
3446
3789
|
console.log(` Pending: \x1B[33m${pendingCount}\x1B[0m`);
|
|
3447
3790
|
console.log(` Approved: \x1B[32m${approvedCount}\x1B[0m`);
|
|
3448
3791
|
console.log(` Rejected: \x1B[31m${rejectedCount}\x1B[0m`);
|
|
3449
|
-
const applyLogPath =
|
|
3792
|
+
const applyLogPath = join8(options.target, ".ai", "proposals", "apply-log.jsonl");
|
|
3450
3793
|
let applyLogCount = 0;
|
|
3451
|
-
if (
|
|
3794
|
+
if (existsSync8(applyLogPath)) {
|
|
3452
3795
|
try {
|
|
3453
|
-
applyLogCount =
|
|
3796
|
+
applyLogCount = readFileSync9(applyLogPath, "utf8").trim().split(/\r?\n/).filter((l) => l.trim() !== "").length;
|
|
3454
3797
|
} catch (e) {
|
|
3455
3798
|
}
|
|
3456
3799
|
}
|
|
3457
3800
|
console.log(` \x1B[33mApply Audit Log:\x1B[0m`);
|
|
3458
3801
|
console.log(` Apply Count: ${applyLogCount}`);
|
|
3459
3802
|
let nextMove = "mmdo status";
|
|
3460
|
-
if (!
|
|
3803
|
+
if (!existsSync8(join8(options.target, ".ai", "config.yaml"))) {
|
|
3461
3804
|
nextMove = "\x1B[36mnpx multimodel-dev-os init\x1B[0m (initialize MultiModel Dev OS first)";
|
|
3462
|
-
} else if (!
|
|
3805
|
+
} else if (!existsSync8(memoryHashPath)) {
|
|
3463
3806
|
nextMove = "\x1B[36mnpx multimodel-dev-os memory build\x1B[0m (initialize memory index)";
|
|
3464
3807
|
} else {
|
|
3465
3808
|
const diff = diffMemory(options.target);
|
|
3466
3809
|
if (diff && (diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0)) {
|
|
3467
3810
|
nextMove = "\x1B[36mnpx multimodel-dev-os memory refresh\x1B[0m (update memory with changes)";
|
|
3468
|
-
} else if (feedbackCount > 0 && !
|
|
3811
|
+
} else if (feedbackCount > 0 && !existsSync8(rulesPath)) {
|
|
3469
3812
|
nextMove = "\x1B[36mnpx multimodel-dev-os feedback summarize\x1B[0m (compile feedback into learning rules)";
|
|
3470
3813
|
} else if (pendingCount > 0) {
|
|
3471
3814
|
nextMove = "\x1B[36mnpx multimodel-dev-os improve review\x1B[0m (review pending proposals)";
|
|
@@ -3479,11 +3822,11 @@ function handleStatus(options) {
|
|
|
3479
3822
|
`);
|
|
3480
3823
|
}
|
|
3481
3824
|
function getWorkflowsPath(target) {
|
|
3482
|
-
let workflowsPath =
|
|
3825
|
+
let workflowsPath = join8(target, ".ai", "registries", "workflows.yaml");
|
|
3483
3826
|
let usingFallback = false;
|
|
3484
|
-
if (!
|
|
3485
|
-
const fallbackPath =
|
|
3486
|
-
if (
|
|
3827
|
+
if (!existsSync8(workflowsPath)) {
|
|
3828
|
+
const fallbackPath = join8(sourceRoot, ".ai", "registries", "workflows.yaml");
|
|
3829
|
+
if (existsSync8(fallbackPath)) {
|
|
3487
3830
|
workflowsPath = fallbackPath;
|
|
3488
3831
|
usingFallback = true;
|
|
3489
3832
|
}
|
|
@@ -3492,7 +3835,7 @@ function getWorkflowsPath(target) {
|
|
|
3492
3835
|
}
|
|
3493
3836
|
function handleWorkflowList(options) {
|
|
3494
3837
|
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3495
|
-
if (!
|
|
3838
|
+
if (!existsSync8(workflowsPath)) {
|
|
3496
3839
|
console.log("No workflows registry found.");
|
|
3497
3840
|
return;
|
|
3498
3841
|
}
|
|
@@ -3500,7 +3843,7 @@ function handleWorkflowList(options) {
|
|
|
3500
3843
|
console.log("\x1B[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1B[0m");
|
|
3501
3844
|
}
|
|
3502
3845
|
try {
|
|
3503
|
-
const registry = parseYaml(
|
|
3846
|
+
const registry = parseYaml(readFileSync9(workflowsPath, "utf8")) || {};
|
|
3504
3847
|
const workflows = registry.workflows || {};
|
|
3505
3848
|
console.log(`
|
|
3506
3849
|
\u2699 \x1B[36mRegistered Workflows\x1B[0m`);
|
|
@@ -3522,7 +3865,7 @@ function handleWorkflowList(options) {
|
|
|
3522
3865
|
}
|
|
3523
3866
|
function handleWorkflowShow(wName, options) {
|
|
3524
3867
|
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3525
|
-
if (!
|
|
3868
|
+
if (!existsSync8(workflowsPath)) {
|
|
3526
3869
|
console.log("No workflows registry found.");
|
|
3527
3870
|
return;
|
|
3528
3871
|
}
|
|
@@ -3530,7 +3873,7 @@ function handleWorkflowShow(wName, options) {
|
|
|
3530
3873
|
console.log("\x1B[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1B[0m");
|
|
3531
3874
|
}
|
|
3532
3875
|
try {
|
|
3533
|
-
const registry = parseYaml(
|
|
3876
|
+
const registry = parseYaml(readFileSync9(workflowsPath, "utf8")) || {};
|
|
3534
3877
|
const workflows = registry.workflows || {};
|
|
3535
3878
|
const wf = workflows[wName];
|
|
3536
3879
|
if (!wf) {
|
|
@@ -3563,7 +3906,7 @@ function handleWorkflowShow(wName, options) {
|
|
|
3563
3906
|
}
|
|
3564
3907
|
function handleWorkflowPlan(wName, options) {
|
|
3565
3908
|
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3566
|
-
if (!
|
|
3909
|
+
if (!existsSync8(workflowsPath)) {
|
|
3567
3910
|
console.log("No workflows registry found.");
|
|
3568
3911
|
return;
|
|
3569
3912
|
}
|
|
@@ -3571,7 +3914,7 @@ function handleWorkflowPlan(wName, options) {
|
|
|
3571
3914
|
console.log("\x1B[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1B[0m");
|
|
3572
3915
|
}
|
|
3573
3916
|
try {
|
|
3574
|
-
const registry = parseYaml(
|
|
3917
|
+
const registry = parseYaml(readFileSync9(workflowsPath, "utf8")) || {};
|
|
3575
3918
|
const workflows = registry.workflows || {};
|
|
3576
3919
|
const wf = workflows[wName];
|
|
3577
3920
|
if (!wf) {
|
|
@@ -3598,7 +3941,7 @@ function handleWorkflowPlan(wName, options) {
|
|
|
3598
3941
|
}
|
|
3599
3942
|
function handleWorkflowRun(wName, options) {
|
|
3600
3943
|
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3601
|
-
if (!
|
|
3944
|
+
if (!existsSync8(workflowsPath)) {
|
|
3602
3945
|
console.log("No workflows registry found.");
|
|
3603
3946
|
return;
|
|
3604
3947
|
}
|
|
@@ -3606,7 +3949,7 @@ function handleWorkflowRun(wName, options) {
|
|
|
3606
3949
|
console.log("\x1B[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1B[0m");
|
|
3607
3950
|
}
|
|
3608
3951
|
try {
|
|
3609
|
-
const registry = parseYaml(
|
|
3952
|
+
const registry = parseYaml(readFileSync9(workflowsPath, "utf8")) || {};
|
|
3610
3953
|
const workflows = registry.workflows || {};
|
|
3611
3954
|
const wf = workflows[wName];
|
|
3612
3955
|
if (!wf) {
|
|
@@ -3658,17 +4001,17 @@ function handleWorkflowRun(wName, options) {
|
|
|
3658
4001
|
}
|
|
3659
4002
|
}
|
|
3660
4003
|
function handleHandoffBuild(options) {
|
|
3661
|
-
const intelDir =
|
|
3662
|
-
if (!
|
|
3663
|
-
|
|
4004
|
+
const intelDir = join8(options.target, ".ai", "intelligence");
|
|
4005
|
+
if (!existsSync8(intelDir)) {
|
|
4006
|
+
mkdirSync3(intelDir, { recursive: true });
|
|
3664
4007
|
}
|
|
3665
|
-
const handoffPath =
|
|
4008
|
+
const handoffPath = join8(intelDir, "handoff.md");
|
|
3666
4009
|
let pkgName = "unknown";
|
|
3667
4010
|
let pkgVersion2 = "unknown";
|
|
3668
4011
|
try {
|
|
3669
|
-
const pkgPath =
|
|
3670
|
-
if (
|
|
3671
|
-
const pkg = JSON.parse(
|
|
4012
|
+
const pkgPath = join8(options.target, "package.json");
|
|
4013
|
+
if (existsSync8(pkgPath)) {
|
|
4014
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
3672
4015
|
pkgName = pkg.name || pkgName;
|
|
3673
4016
|
pkgVersion2 = pkg.version || pkgVersion2;
|
|
3674
4017
|
}
|
|
@@ -3677,12 +4020,12 @@ function handleHandoffBuild(options) {
|
|
|
3677
4020
|
const { files } = scanTarget(options.target);
|
|
3678
4021
|
const frameworkSignals = detectFrameworkSignals(files, options.target);
|
|
3679
4022
|
const dependencySignals = detectDependencySignals(files, options.target);
|
|
3680
|
-
const memoryHashPath =
|
|
4023
|
+
const memoryHashPath = join8(intelDir, "memory.hash.json");
|
|
3681
4024
|
let memoryStatus = "MISSING";
|
|
3682
4025
|
let memoryTime = "N/A";
|
|
3683
|
-
if (
|
|
4026
|
+
if (existsSync8(memoryHashPath)) {
|
|
3684
4027
|
try {
|
|
3685
|
-
const memObj = JSON.parse(
|
|
4028
|
+
const memObj = JSON.parse(readFileSync9(memoryHashPath, "utf8"));
|
|
3686
4029
|
memoryTime = memObj.generated_at || "N/A";
|
|
3687
4030
|
const diff = diffMemory(options.target);
|
|
3688
4031
|
if (diff) {
|
|
@@ -3692,25 +4035,25 @@ function handleHandoffBuild(options) {
|
|
|
3692
4035
|
memoryStatus = "CORRUPT";
|
|
3693
4036
|
}
|
|
3694
4037
|
}
|
|
3695
|
-
const feedbackPath =
|
|
4038
|
+
const feedbackPath = join8(intelDir, "feedback-log.jsonl");
|
|
3696
4039
|
let feedbackCount = 0;
|
|
3697
|
-
if (
|
|
4040
|
+
if (existsSync8(feedbackPath)) {
|
|
3698
4041
|
try {
|
|
3699
|
-
feedbackCount =
|
|
4042
|
+
feedbackCount = readFileSync9(feedbackPath, "utf8").trim().split(/\r?\n/).filter((l) => l.trim() !== "").length;
|
|
3700
4043
|
} catch (e) {
|
|
3701
4044
|
}
|
|
3702
4045
|
}
|
|
3703
|
-
const rulesPath =
|
|
3704
|
-
const rulesStatus =
|
|
3705
|
-
const proposalsDir =
|
|
4046
|
+
const rulesPath = join8(intelDir, "learning-rules.md");
|
|
4047
|
+
const rulesStatus = existsSync8(rulesPath) ? "PRESENT" : "MISSING";
|
|
4048
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
3706
4049
|
let pendingCount = 0;
|
|
3707
4050
|
let approvedCount = 0;
|
|
3708
4051
|
let rejectedCount = 0;
|
|
3709
|
-
if (
|
|
4052
|
+
if (existsSync8(proposalsDir)) {
|
|
3710
4053
|
try {
|
|
3711
4054
|
const propFiles = readdirSync(proposalsDir).filter((f) => f.startsWith("proposal-") && f.endsWith(".md"));
|
|
3712
4055
|
propFiles.forEach((file) => {
|
|
3713
|
-
const content =
|
|
4056
|
+
const content = readFileSync9(join8(proposalsDir, file), "utf8");
|
|
3714
4057
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3715
4058
|
if (fmMatch) {
|
|
3716
4059
|
const metadata = parseYaml(fmMatch[1]) || {};
|
|
@@ -3726,12 +4069,12 @@ function handleHandoffBuild(options) {
|
|
|
3726
4069
|
} catch (e) {
|
|
3727
4070
|
}
|
|
3728
4071
|
}
|
|
3729
|
-
const applyLogPath =
|
|
4072
|
+
const applyLogPath = join8(proposalsDir, "apply-log.jsonl");
|
|
3730
4073
|
let applyLogCount = 0;
|
|
3731
4074
|
let lastApplyId = "None";
|
|
3732
|
-
if (
|
|
4075
|
+
if (existsSync8(applyLogPath)) {
|
|
3733
4076
|
try {
|
|
3734
|
-
const lines =
|
|
4077
|
+
const lines = readFileSync9(applyLogPath, "utf8").trim().split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
3735
4078
|
applyLogCount = lines.length;
|
|
3736
4079
|
if (applyLogCount > 0) {
|
|
3737
4080
|
const lastRecord = JSON.parse(lines[lines.length - 1]);
|
|
@@ -3741,9 +4084,9 @@ function handleHandoffBuild(options) {
|
|
|
3741
4084
|
}
|
|
3742
4085
|
}
|
|
3743
4086
|
let rulesSummary = "No learning rules defined yet.";
|
|
3744
|
-
if (
|
|
4087
|
+
if (existsSync8(rulesPath)) {
|
|
3745
4088
|
try {
|
|
3746
|
-
const rulesContent =
|
|
4089
|
+
const rulesContent = readFileSync9(rulesPath, "utf8");
|
|
3747
4090
|
const lines = rulesContent.split(/\r?\n/);
|
|
3748
4091
|
const summaryLines = [];
|
|
3749
4092
|
for (const line of lines) {
|
|
@@ -3760,7 +4103,7 @@ function handleHandoffBuild(options) {
|
|
|
3760
4103
|
}
|
|
3761
4104
|
}
|
|
3762
4105
|
let recs = "1. Run `npx multimodel-dev-os workflow run repo-health` to check the directory hygiene.\n2. Review pending proposals if any exist.";
|
|
3763
|
-
if (!
|
|
4106
|
+
if (!existsSync8(join8(options.target, ".ai", "config.yaml"))) {
|
|
3764
4107
|
recs = "1. Run `npx multimodel-dev-os init` to bootstrap MultiModel Dev OS.\n2. Run `npx multimodel-dev-os memory build` to initialize codebase memory.";
|
|
3765
4108
|
} else if (memoryStatus === "MISSING") {
|
|
3766
4109
|
recs = "1. Run `npx multimodel-dev-os memory build` to initialize codebase index.\n2. Verify package safety boundaries.";
|
|
@@ -3798,7 +4141,7 @@ ${rulesSummary}
|
|
|
3798
4141
|
${recs}
|
|
3799
4142
|
`;
|
|
3800
4143
|
try {
|
|
3801
|
-
|
|
4144
|
+
writeFileSync4(handoffPath, handoffContent, "utf8");
|
|
3802
4145
|
console.log(`
|
|
3803
4146
|
\u2714 Handoff context built successfully in: .ai/intelligence/handoff.md`);
|
|
3804
4147
|
} catch (e) {
|
|
@@ -3806,13 +4149,13 @@ ${recs}
|
|
|
3806
4149
|
}
|
|
3807
4150
|
}
|
|
3808
4151
|
function handleHandoffShow(options) {
|
|
3809
|
-
const handoffPath =
|
|
3810
|
-
if (!
|
|
4152
|
+
const handoffPath = join8(options.target, ".ai", "intelligence", "handoff.md");
|
|
4153
|
+
if (!existsSync8(handoffPath)) {
|
|
3811
4154
|
console.log("No compiled handoff file exists. Building first...");
|
|
3812
4155
|
handleHandoffBuild(options);
|
|
3813
4156
|
}
|
|
3814
4157
|
try {
|
|
3815
|
-
const content =
|
|
4158
|
+
const content = readFileSync9(handoffPath, "utf8");
|
|
3816
4159
|
console.log("\n" + content);
|
|
3817
4160
|
} catch (e) {
|
|
3818
4161
|
console.error(`\x1B[31mError reading handoff: ${e.message}\x1B[0m`);
|
|
@@ -3827,8 +4170,8 @@ function handleDoctorIntelligence(options) {
|
|
|
3827
4170
|
console.warn(` \x1B[33m[WARNING]\x1B[0m ${msg}`);
|
|
3828
4171
|
warnings++;
|
|
3829
4172
|
};
|
|
3830
|
-
const memoryHashPath =
|
|
3831
|
-
if (!
|
|
4173
|
+
const memoryHashPath = join8(options.target, ".ai", "intelligence", "memory.hash.json");
|
|
4174
|
+
if (!existsSync8(memoryHashPath)) {
|
|
3832
4175
|
warn("Memory hash index (.ai/intelligence/memory.hash.json) is MISSING. Run `memory build` first.");
|
|
3833
4176
|
} else {
|
|
3834
4177
|
try {
|
|
@@ -3842,23 +4185,23 @@ function handleDoctorIntelligence(options) {
|
|
|
3842
4185
|
warn("Failed to diff memory index.");
|
|
3843
4186
|
}
|
|
3844
4187
|
}
|
|
3845
|
-
const feedbackPath =
|
|
3846
|
-
if (!
|
|
4188
|
+
const feedbackPath = join8(options.target, ".ai", "intelligence", "feedback-log.jsonl");
|
|
4189
|
+
if (!existsSync8(feedbackPath)) {
|
|
3847
4190
|
warn("Feedback log (.ai/intelligence/feedback-log.jsonl) is MISSING.");
|
|
3848
4191
|
}
|
|
3849
|
-
const rulesPath =
|
|
3850
|
-
if (!
|
|
4192
|
+
const rulesPath = join8(options.target, ".ai", "intelligence", "learning-rules.md");
|
|
4193
|
+
if (!existsSync8(rulesPath)) {
|
|
3851
4194
|
warn("Learning rules (.ai/intelligence/learning-rules.md) are MISSING. Run `feedback summarize` to compile logs.");
|
|
3852
4195
|
}
|
|
3853
|
-
const proposalsDir =
|
|
3854
|
-
if (!
|
|
4196
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
4197
|
+
if (!existsSync8(proposalsDir)) {
|
|
3855
4198
|
warn("Proposals directory (.ai/proposals) is MISSING.");
|
|
3856
4199
|
} else {
|
|
3857
4200
|
try {
|
|
3858
4201
|
const files = readdirSync(proposalsDir).filter((f) => f.startsWith("proposal-") && f.endsWith(".md"));
|
|
3859
4202
|
let pending = 0;
|
|
3860
4203
|
files.forEach((file) => {
|
|
3861
|
-
const content =
|
|
4204
|
+
const content = readFileSync9(join8(proposalsDir, file), "utf8");
|
|
3862
4205
|
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3863
4206
|
if (fmMatch) {
|
|
3864
4207
|
const metadata = parseYaml(fmMatch[1]) || {};
|
|
@@ -3873,13 +4216,13 @@ function handleDoctorIntelligence(options) {
|
|
|
3873
4216
|
} catch (e) {
|
|
3874
4217
|
}
|
|
3875
4218
|
}
|
|
3876
|
-
const applyLogPath =
|
|
3877
|
-
if (!
|
|
4219
|
+
const applyLogPath = join8(options.target, ".ai", "proposals", "apply-log.jsonl");
|
|
4220
|
+
if (!existsSync8(applyLogPath)) {
|
|
3878
4221
|
warn("Apply audit log (.ai/proposals/apply-log.jsonl) is MISSING.");
|
|
3879
4222
|
}
|
|
3880
|
-
const gitignorePath =
|
|
3881
|
-
if (
|
|
3882
|
-
const gitignoreContent =
|
|
4223
|
+
const gitignorePath = join8(options.target, ".gitignore");
|
|
4224
|
+
if (existsSync8(gitignorePath)) {
|
|
4225
|
+
const gitignoreContent = readFileSync9(gitignorePath, "utf8");
|
|
3883
4226
|
const checkIgnore = (pattern) => {
|
|
3884
4227
|
if (!gitignoreContent.includes(pattern)) {
|
|
3885
4228
|
warn(`.gitignore is missing rules ignoring: ${pattern}`);
|
|
@@ -3893,9 +4236,9 @@ function handleDoctorIntelligence(options) {
|
|
|
3893
4236
|
} else {
|
|
3894
4237
|
warn(".gitignore file is missing in target root.");
|
|
3895
4238
|
}
|
|
3896
|
-
if (
|
|
4239
|
+
if (existsSync8(memoryHashPath)) {
|
|
3897
4240
|
try {
|
|
3898
|
-
const memObj = JSON.parse(
|
|
4241
|
+
const memObj = JSON.parse(readFileSync9(memoryHashPath, "utf8"));
|
|
3899
4242
|
const fingerprints = memObj.file_fingerprints || {};
|
|
3900
4243
|
Object.keys(fingerprints).forEach((file) => {
|
|
3901
4244
|
const name = file.toLowerCase();
|
|
@@ -3955,7 +4298,7 @@ function getAnalysis(target) {
|
|
|
3955
4298
|
repoType = "docs";
|
|
3956
4299
|
} else if (files.some((f) => f.relPath === "package.json")) {
|
|
3957
4300
|
try {
|
|
3958
|
-
const pkg = JSON.parse(
|
|
4301
|
+
const pkg = JSON.parse(readFileSync9(join8(target, "package.json"), "utf8"));
|
|
3959
4302
|
if (pkg.main && (pkg.main.includes("dist/") || pkg.main.includes("lib/"))) {
|
|
3960
4303
|
repoType = "library";
|
|
3961
4304
|
}
|
|
@@ -3976,7 +4319,7 @@ function getAnalysis(target) {
|
|
|
3976
4319
|
const packageScripts = [];
|
|
3977
4320
|
if (files.some((f) => f.relPath === "package.json")) {
|
|
3978
4321
|
try {
|
|
3979
|
-
const pkg = JSON.parse(
|
|
4322
|
+
const pkg = JSON.parse(readFileSync9(join8(target, "package.json"), "utf8"));
|
|
3980
4323
|
if (pkg.scripts) {
|
|
3981
4324
|
Object.keys(pkg.scripts).forEach((k) => packageScripts.push(k));
|
|
3982
4325
|
}
|
|
@@ -3984,8 +4327,8 @@ function getAnalysis(target) {
|
|
|
3984
4327
|
}
|
|
3985
4328
|
}
|
|
3986
4329
|
const githubWorkflows = [];
|
|
3987
|
-
const githubDir =
|
|
3988
|
-
if (
|
|
4330
|
+
const githubDir = join8(target, ".github", "workflows");
|
|
4331
|
+
if (existsSync8(githubDir)) {
|
|
3989
4332
|
try {
|
|
3990
4333
|
readdirSync(githubDir).forEach((f) => {
|
|
3991
4334
|
if (f.endsWith(".yml") || f.endsWith(".yaml"))
|
|
@@ -4093,8 +4436,8 @@ function handleOnboardPlan(options) {
|
|
|
4093
4436
|
console.log("==================================================");
|
|
4094
4437
|
const analysis = getAnalysis(options.target);
|
|
4095
4438
|
const rec = getRecommendation(analysis);
|
|
4096
|
-
const planPath =
|
|
4097
|
-
const reportPath =
|
|
4439
|
+
const planPath = join8(options.target, ".ai", "intelligence", "onboarding.plan.json");
|
|
4440
|
+
const reportPath = join8(options.target, ".ai", "intelligence", "onboarding.report.md");
|
|
4098
4441
|
const plannedFiles = [
|
|
4099
4442
|
{ action: "CREATE", path: "AGENTS.md", source_template: `examples/${rec.template}/AGENTS.md` },
|
|
4100
4443
|
{ action: "CREATE", path: "MEMORY.md", source_template: `examples/${rec.template}/MEMORY.md` },
|
|
@@ -4170,13 +4513,13 @@ function handleOnboardPlan(options) {
|
|
|
4170
4513
|
reportMd += `\`\`\`
|
|
4171
4514
|
`;
|
|
4172
4515
|
try {
|
|
4173
|
-
const intelDir =
|
|
4174
|
-
if (!options.dryRun && !
|
|
4175
|
-
|
|
4516
|
+
const intelDir = join8(options.target, ".ai", "intelligence");
|
|
4517
|
+
if (!options.dryRun && !existsSync8(intelDir)) {
|
|
4518
|
+
mkdirSync3(intelDir, { recursive: true });
|
|
4176
4519
|
}
|
|
4177
4520
|
if (!options.dryRun) {
|
|
4178
|
-
|
|
4179
|
-
|
|
4521
|
+
writeFileSync4(planPath, JSON.stringify(planData, null, 2), "utf8");
|
|
4522
|
+
writeFileSync4(reportPath, reportMd, "utf8");
|
|
4180
4523
|
}
|
|
4181
4524
|
console.log(` [SUCCESS] Onboarding plan generated:`);
|
|
4182
4525
|
console.log(` - Plan JSON: .ai/intelligence/onboarding.plan.json`);
|
|
@@ -4194,14 +4537,14 @@ function handleOnboardApply(options) {
|
|
|
4194
4537
|
console.log("Example: node bin/multimodel-dev-os.js onboard apply --approved");
|
|
4195
4538
|
process.exit(1);
|
|
4196
4539
|
}
|
|
4197
|
-
const planPath =
|
|
4198
|
-
if (!
|
|
4540
|
+
const planPath = join8(options.target, ".ai", "intelligence", "onboarding.plan.json");
|
|
4541
|
+
if (!existsSync8(planPath)) {
|
|
4199
4542
|
console.error('\x1B[31mError: Onboarding plan not found. Run "npx multimodel-dev-os onboard plan" first.\x1B[0m');
|
|
4200
4543
|
process.exit(1);
|
|
4201
4544
|
}
|
|
4202
4545
|
let plan;
|
|
4203
4546
|
try {
|
|
4204
|
-
plan = JSON.parse(
|
|
4547
|
+
plan = JSON.parse(readFileSync9(planPath, "utf8"));
|
|
4205
4548
|
} catch (e) {
|
|
4206
4549
|
console.error(`\x1B[31mError reading plan JSON: ${e.message}\x1B[0m`);
|
|
4207
4550
|
process.exit(1);
|
|
@@ -4215,23 +4558,23 @@ function handleOnboardApply(options) {
|
|
|
4215
4558
|
plan.planned_files.forEach((f) => {
|
|
4216
4559
|
let srcFile;
|
|
4217
4560
|
if (f.source_template === "RUNBOOK.md") {
|
|
4218
|
-
srcFile =
|
|
4561
|
+
srcFile = join8(sourceRoot, "RUNBOOK.md");
|
|
4219
4562
|
} else {
|
|
4220
|
-
srcFile =
|
|
4563
|
+
srcFile = join8(sourceRoot, f.source_template);
|
|
4221
4564
|
}
|
|
4222
4565
|
operations.push({ dest: f.path, src: srcFile });
|
|
4223
4566
|
});
|
|
4224
|
-
const templateDir =
|
|
4225
|
-
const templateAiDir =
|
|
4226
|
-
if (
|
|
4567
|
+
const templateDir = join8(sourceRoot, "examples", template);
|
|
4568
|
+
const templateAiDir = join8(templateDir, ".ai");
|
|
4569
|
+
if (existsSync8(templateAiDir) && !options.caveman) {
|
|
4227
4570
|
const subdirs = ["context", "skills"];
|
|
4228
4571
|
subdirs.forEach((sub) => {
|
|
4229
|
-
const subPath =
|
|
4230
|
-
if (
|
|
4572
|
+
const subPath = join8(templateAiDir, sub);
|
|
4573
|
+
if (existsSync8(subPath)) {
|
|
4231
4574
|
readdirSync(subPath).forEach((file) => {
|
|
4232
4575
|
operations.push({
|
|
4233
|
-
dest:
|
|
4234
|
-
src:
|
|
4576
|
+
dest: join8(".ai", sub, file),
|
|
4577
|
+
src: join8(subPath, file)
|
|
4235
4578
|
});
|
|
4236
4579
|
});
|
|
4237
4580
|
}
|
|
@@ -4239,17 +4582,17 @@ function handleOnboardApply(options) {
|
|
|
4239
4582
|
}
|
|
4240
4583
|
const globalAiSubdirs = ["context", "agents", "skills", "prompts", "checks", "templates", "session-logs", "registries", "proposals", "intelligence"];
|
|
4241
4584
|
globalAiSubdirs.forEach((sub) => {
|
|
4242
|
-
const globalPath =
|
|
4243
|
-
if (
|
|
4585
|
+
const globalPath = join8(sourceRoot, ".ai", sub);
|
|
4586
|
+
if (existsSync8(globalPath)) {
|
|
4244
4587
|
readdirSync(globalPath).forEach((file) => {
|
|
4245
|
-
const destRel =
|
|
4588
|
+
const destRel = join8(".ai", sub, file);
|
|
4246
4589
|
if (!operations.some((op) => op.dest === destRel)) {
|
|
4247
4590
|
if (options.caveman && (sub === "context" || sub === "skills" || sub === "prompts" || sub === "checks")) {
|
|
4248
4591
|
return;
|
|
4249
4592
|
}
|
|
4250
4593
|
operations.push({
|
|
4251
4594
|
dest: destRel,
|
|
4252
|
-
src:
|
|
4595
|
+
src: join8(globalPath, file)
|
|
4253
4596
|
});
|
|
4254
4597
|
}
|
|
4255
4598
|
});
|
|
@@ -4259,16 +4602,16 @@ function handleOnboardApply(options) {
|
|
|
4259
4602
|
let skippedCount = 0;
|
|
4260
4603
|
let updatedCount = 0;
|
|
4261
4604
|
operations.forEach((op) => {
|
|
4262
|
-
const destPath =
|
|
4263
|
-
const destDir =
|
|
4264
|
-
if (
|
|
4605
|
+
const destPath = join8(options.target, op.dest);
|
|
4606
|
+
const destDir = dirname4(destPath);
|
|
4607
|
+
if (existsSync8(destPath)) {
|
|
4265
4608
|
if (options.force) {
|
|
4266
4609
|
if (!options.dryRun) {
|
|
4267
4610
|
const backupPath = destPath + ".bak";
|
|
4268
|
-
|
|
4269
|
-
if (!
|
|
4270
|
-
|
|
4271
|
-
|
|
4611
|
+
writeFileSync4(backupPath, readFileSync9(destPath));
|
|
4612
|
+
if (!existsSync8(destDir))
|
|
4613
|
+
mkdirSync3(destDir, { recursive: true });
|
|
4614
|
+
writeFileSync4(destPath, readFileSync9(op.src));
|
|
4272
4615
|
console.log(` \x1B[33mOVERWRITE (BACKUP CREATED):\x1B[0m ${op.dest} -> ${op.dest}.bak`);
|
|
4273
4616
|
} else {
|
|
4274
4617
|
console.log(` \x1B[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1B[0m ${op.dest}`);
|
|
@@ -4280,9 +4623,9 @@ function handleOnboardApply(options) {
|
|
|
4280
4623
|
}
|
|
4281
4624
|
} else {
|
|
4282
4625
|
if (!options.dryRun) {
|
|
4283
|
-
if (!
|
|
4284
|
-
|
|
4285
|
-
|
|
4626
|
+
if (!existsSync8(destDir))
|
|
4627
|
+
mkdirSync3(destDir, { recursive: true });
|
|
4628
|
+
writeFileSync4(destPath, readFileSync9(op.src));
|
|
4286
4629
|
console.log(` \x1B[32mCREATE:\x1B[0m ${op.dest}`);
|
|
4287
4630
|
} else {
|
|
4288
4631
|
console.log(` \x1B[36m[DRY-RUN] WOULD CREATE:\x1B[0m ${op.dest}`);
|
|
@@ -4307,8 +4650,8 @@ function handleOnboardStatus(options) {
|
|
|
4307
4650
|
];
|
|
4308
4651
|
let presentCount = 0;
|
|
4309
4652
|
crucialFiles.forEach((f) => {
|
|
4310
|
-
const fullPath =
|
|
4311
|
-
const exists =
|
|
4653
|
+
const fullPath = join8(options.target, f);
|
|
4654
|
+
const exists = existsSync8(fullPath);
|
|
4312
4655
|
if (exists)
|
|
4313
4656
|
presentCount++;
|
|
4314
4657
|
console.log(` [${exists ? "\u2714" : " "}] ${f}`);
|
|
@@ -4325,10 +4668,10 @@ function handleOnboardStatus(options) {
|
|
|
4325
4668
|
}
|
|
4326
4669
|
}
|
|
4327
4670
|
function getEnabledAdapters(target) {
|
|
4328
|
-
const configPath =
|
|
4329
|
-
if (
|
|
4671
|
+
const configPath = join8(target, ".ai", "config.yaml");
|
|
4672
|
+
if (existsSync8(configPath)) {
|
|
4330
4673
|
try {
|
|
4331
|
-
const config = parseYaml(
|
|
4674
|
+
const config = parseYaml(readFileSync9(configPath, "utf8")) || {};
|
|
4332
4675
|
return config.adapters || {};
|
|
4333
4676
|
} catch (e) {
|
|
4334
4677
|
}
|
|
@@ -4344,7 +4687,7 @@ function handleAdapterStatus(options) {
|
|
|
4344
4687
|
const a = ADAPTERS[name];
|
|
4345
4688
|
const isEnabled = enabled[name] || false;
|
|
4346
4689
|
const rulesFile = a.rules_file;
|
|
4347
|
-
const exists =
|
|
4690
|
+
const exists = existsSync8(join8(options.target, rulesFile));
|
|
4348
4691
|
let statusStr = "\x1B[31mMISSING\x1B[0m";
|
|
4349
4692
|
if (exists) {
|
|
4350
4693
|
statusStr = "\x1B[32mINSTALLED\x1B[0m";
|
|
@@ -4403,15 +4746,15 @@ function handleAdapterDiff(aName, options) {
|
|
|
4403
4746
|
}
|
|
4404
4747
|
adaptersToDiff.forEach((name) => {
|
|
4405
4748
|
const a = ADAPTERS[name];
|
|
4406
|
-
const srcFile =
|
|
4407
|
-
const destFile =
|
|
4408
|
-
if (!
|
|
4749
|
+
const srcFile = join8(sourceRoot, "adapters", name, a.rules_file);
|
|
4750
|
+
const destFile = join8(options.target, a.rules_file);
|
|
4751
|
+
if (!existsSync8(srcFile)) {
|
|
4409
4752
|
console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
|
|
4410
4753
|
return;
|
|
4411
4754
|
}
|
|
4412
|
-
const srcContent =
|
|
4413
|
-
if (
|
|
4414
|
-
const destContent =
|
|
4755
|
+
const srcContent = readFileSync9(srcFile, "utf8");
|
|
4756
|
+
if (existsSync8(destFile)) {
|
|
4757
|
+
const destContent = readFileSync9(destFile, "utf8");
|
|
4415
4758
|
printDiff(srcContent, destContent, a.rules_file);
|
|
4416
4759
|
} else {
|
|
4417
4760
|
console.log(`
|
|
@@ -4450,21 +4793,21 @@ function handleAdapterSync(aName, options) {
|
|
|
4450
4793
|
console.log("==================================================");
|
|
4451
4794
|
adaptersToSync.forEach((name) => {
|
|
4452
4795
|
const a = ADAPTERS[name];
|
|
4453
|
-
const srcFile =
|
|
4454
|
-
const destFile =
|
|
4455
|
-
const destDir =
|
|
4456
|
-
if (!
|
|
4796
|
+
const srcFile = join8(sourceRoot, "adapters", name, a.rules_file);
|
|
4797
|
+
const destFile = join8(options.target, a.rules_file);
|
|
4798
|
+
const destDir = dirname4(destFile);
|
|
4799
|
+
if (!existsSync8(srcFile)) {
|
|
4457
4800
|
console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
|
|
4458
4801
|
return;
|
|
4459
4802
|
}
|
|
4460
|
-
if (
|
|
4803
|
+
if (existsSync8(destFile)) {
|
|
4461
4804
|
if (options.force) {
|
|
4462
4805
|
if (!options.dryRun) {
|
|
4463
4806
|
const backupPath = destFile + ".bak";
|
|
4464
|
-
|
|
4465
|
-
if (!
|
|
4466
|
-
|
|
4467
|
-
|
|
4807
|
+
writeFileSync4(backupPath, readFileSync9(destFile));
|
|
4808
|
+
if (!existsSync8(destDir))
|
|
4809
|
+
mkdirSync3(destDir, { recursive: true });
|
|
4810
|
+
writeFileSync4(destFile, readFileSync9(srcFile));
|
|
4468
4811
|
console.log(` \x1B[33mOVERWRITE (BACKUP CREATED):\x1B[0m ${a.rules_file} -> ${a.rules_file}.bak`);
|
|
4469
4812
|
} else {
|
|
4470
4813
|
console.log(` \x1B[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1B[0m ${a.rules_file}`);
|
|
@@ -4474,9 +4817,9 @@ function handleAdapterSync(aName, options) {
|
|
|
4474
4817
|
}
|
|
4475
4818
|
} else {
|
|
4476
4819
|
if (!options.dryRun) {
|
|
4477
|
-
if (!
|
|
4478
|
-
|
|
4479
|
-
|
|
4820
|
+
if (!existsSync8(destDir))
|
|
4821
|
+
mkdirSync3(destDir, { recursive: true });
|
|
4822
|
+
writeFileSync4(destFile, readFileSync9(srcFile));
|
|
4480
4823
|
console.log(` \x1B[32mCREATE:\x1B[0m ${a.rules_file}`);
|
|
4481
4824
|
} else {
|
|
4482
4825
|
console.log(` \x1B[36m[DRY-RUN] WOULD CREATE:\x1B[0m ${a.rules_file}`);
|
|
@@ -4501,29 +4844,29 @@ function handleDoctorOnboarding(options) {
|
|
|
4501
4844
|
"RUNBOOK.md"
|
|
4502
4845
|
];
|
|
4503
4846
|
crucialFiles.forEach((f) => {
|
|
4504
|
-
if (!
|
|
4847
|
+
if (!existsSync8(join8(options.target, f))) {
|
|
4505
4848
|
warn(`Crucial onboarding file '${f}' is missing from project root.`);
|
|
4506
4849
|
}
|
|
4507
4850
|
});
|
|
4508
|
-
const configPath =
|
|
4509
|
-
if (!
|
|
4851
|
+
const configPath = join8(options.target, ".ai", "config.yaml");
|
|
4852
|
+
if (!existsSync8(configPath)) {
|
|
4510
4853
|
warn("MultiModel Dev OS configuration file (.ai/config.yaml) is missing.");
|
|
4511
4854
|
}
|
|
4512
|
-
const registriesDir =
|
|
4513
|
-
if (!
|
|
4855
|
+
const registriesDir = join8(options.target, ".ai", "registries");
|
|
4856
|
+
if (!existsSync8(registriesDir)) {
|
|
4514
4857
|
warn("Registries directory (.ai/registries) is missing.");
|
|
4515
4858
|
}
|
|
4516
|
-
const proposalsDir =
|
|
4517
|
-
if (!
|
|
4859
|
+
const proposalsDir = join8(options.target, ".ai", "proposals");
|
|
4860
|
+
if (!existsSync8(proposalsDir)) {
|
|
4518
4861
|
warn("Proposals directory (.ai/proposals) is missing.");
|
|
4519
4862
|
}
|
|
4520
|
-
const intelligenceDir =
|
|
4521
|
-
if (!
|
|
4863
|
+
const intelligenceDir = join8(options.target, ".ai", "intelligence");
|
|
4864
|
+
if (!existsSync8(intelligenceDir)) {
|
|
4522
4865
|
warn("Intelligence directory (.ai/intelligence) is missing.");
|
|
4523
4866
|
}
|
|
4524
|
-
const gitignorePath =
|
|
4525
|
-
if (
|
|
4526
|
-
const gitignoreContent =
|
|
4867
|
+
const gitignorePath = join8(options.target, ".gitignore");
|
|
4868
|
+
if (existsSync8(gitignorePath)) {
|
|
4869
|
+
const gitignoreContent = readFileSync9(gitignorePath, "utf8");
|
|
4527
4870
|
const checkIgnore = (pattern) => {
|
|
4528
4871
|
if (!gitignoreContent.includes(pattern)) {
|
|
4529
4872
|
warn(`Generated runtime file '${pattern}' is not ignored in .gitignore.`);
|
|
@@ -4687,7 +5030,7 @@ function handleDashboard(options) {
|
|
|
4687
5030
|
\x1B[36mRunning Command:\x1B[0m npx multimodel-dev-os ${cmdStr}${targetFlag}`);
|
|
4688
5031
|
console.log("--------------------------------------------------\n");
|
|
4689
5032
|
try {
|
|
4690
|
-
const cliPath =
|
|
5033
|
+
const cliPath = join8(sourceRoot, "bin", "multimodel-dev-os.js");
|
|
4691
5034
|
execSync(`node "${cliPath}" ${cmdStr} --target "${options.target}"`, { stdio: "inherit" });
|
|
4692
5035
|
} catch (e) {
|
|
4693
5036
|
console.error(`
|
|
@@ -4722,13 +5065,13 @@ function handleDashboard(options) {
|
|
|
4722
5065
|
showMenu(mainMenu, "MultiModel Dev OS Command Center");
|
|
4723
5066
|
}
|
|
4724
5067
|
function getPluginsDir(targetDir) {
|
|
4725
|
-
return
|
|
5068
|
+
return join8(targetDir, ".ai", "plugins");
|
|
4726
5069
|
}
|
|
4727
5070
|
function handlePluginList(options) {
|
|
4728
5071
|
const pluginsDir = getPluginsDir(options.target);
|
|
4729
|
-
const rawRelPath = relative(process.cwd(),
|
|
5072
|
+
const rawRelPath = relative(process.cwd(), join8(sourceRoot, ".ai", "plugins", "plugin.example.yaml")).replace(/\\/g, "/");
|
|
4730
5073
|
const examplePath = rawRelPath.startsWith(".") ? rawRelPath : `./${rawRelPath}`;
|
|
4731
|
-
if (!
|
|
5074
|
+
if (!existsSync8(pluginsDir)) {
|
|
4732
5075
|
if (options.json) {
|
|
4733
5076
|
console.log("[]");
|
|
4734
5077
|
return;
|
|
@@ -4749,7 +5092,7 @@ function handlePluginList(options) {
|
|
|
4749
5092
|
const plugins = [];
|
|
4750
5093
|
files.forEach((f) => {
|
|
4751
5094
|
try {
|
|
4752
|
-
const p = parseYaml(
|
|
5095
|
+
const p = parseYaml(readFileSync9(join8(pluginsDir, f), "utf8"));
|
|
4753
5096
|
if (p && p.name && p.slug) {
|
|
4754
5097
|
plugins.push(p);
|
|
4755
5098
|
}
|
|
@@ -4783,11 +5126,11 @@ function handlePluginShow(slug, options) {
|
|
|
4783
5126
|
}
|
|
4784
5127
|
const pluginsDir = getPluginsDir(options.target);
|
|
4785
5128
|
let p = null;
|
|
4786
|
-
if (
|
|
5129
|
+
if (existsSync8(pluginsDir)) {
|
|
4787
5130
|
const files = readdirSync(pluginsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
4788
5131
|
for (const f of files) {
|
|
4789
5132
|
try {
|
|
4790
|
-
const parsed = parseYaml(
|
|
5133
|
+
const parsed = parseYaml(readFileSync9(join8(pluginsDir, f), "utf8"));
|
|
4791
5134
|
if (parsed && parsed.slug === slug) {
|
|
4792
5135
|
p = parsed;
|
|
4793
5136
|
break;
|
|
@@ -4836,7 +5179,7 @@ function handlePluginShow(slug, options) {
|
|
|
4836
5179
|
}
|
|
4837
5180
|
function handlePluginValidate(pluginPath, options) {
|
|
4838
5181
|
const fullPath = resolve3(process.cwd(), pluginPath);
|
|
4839
|
-
if (!
|
|
5182
|
+
if (!existsSync8(fullPath)) {
|
|
4840
5183
|
console.error(`\x1B[31mError: Plugin file not found at: ${pluginPath}\x1B[0m`);
|
|
4841
5184
|
process.exit(1);
|
|
4842
5185
|
}
|
|
@@ -4846,7 +5189,7 @@ function handlePluginValidate(pluginPath, options) {
|
|
|
4846
5189
|
let errors = 0;
|
|
4847
5190
|
let plugin = null;
|
|
4848
5191
|
try {
|
|
4849
|
-
plugin = parseYaml(
|
|
5192
|
+
plugin = parseYaml(readFileSync9(fullPath, "utf8"));
|
|
4850
5193
|
} catch (e) {
|
|
4851
5194
|
console.error(` \x1B[31m\u2717 [SYNTAX] Failed to parse YAML: ${e.message}\x1B[0m`);
|
|
4852
5195
|
errors++;
|
|
@@ -4979,7 +5322,7 @@ function handlePluginValidate(pluginPath, options) {
|
|
|
4979
5322
|
}
|
|
4980
5323
|
function handlePluginInstall(pluginPath, options) {
|
|
4981
5324
|
const fullPath = resolve3(process.cwd(), pluginPath);
|
|
4982
|
-
if (!
|
|
5325
|
+
if (!existsSync8(fullPath)) {
|
|
4983
5326
|
console.error(`\x1B[31mError: Plugin file not found at: ${pluginPath}\x1B[0m`);
|
|
4984
5327
|
process.exit(1);
|
|
4985
5328
|
}
|
|
@@ -4989,16 +5332,16 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
4989
5332
|
process.exit(1);
|
|
4990
5333
|
}
|
|
4991
5334
|
const policy = loadRegistryPolicy(options.target || process.cwd());
|
|
4992
|
-
const pluginContent =
|
|
5335
|
+
const pluginContent = readFileSync9(fullPath, "utf8");
|
|
4993
5336
|
const plugin = parseYaml(pluginContent);
|
|
4994
5337
|
const slug = plugin.slug;
|
|
4995
|
-
const sourceDir =
|
|
5338
|
+
const sourceDir = dirname4(fullPath);
|
|
4996
5339
|
console.log(`
|
|
4997
5340
|
\u{1F4E5} \x1B[34mInstalling Plugin: ${plugin.name} [slug: ${slug}]\x1B[0m`);
|
|
4998
5341
|
const filesToCopy = [];
|
|
4999
5342
|
filesToCopy.push({
|
|
5000
5343
|
src: fullPath,
|
|
5001
|
-
dest:
|
|
5344
|
+
dest: join8(".ai", "plugins", `${slug}.yaml`),
|
|
5002
5345
|
description: "Plugin Manifest"
|
|
5003
5346
|
});
|
|
5004
5347
|
if (Array.isArray(plugin.allowed_file_patterns)) {
|
|
@@ -5016,8 +5359,8 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
5016
5359
|
console.error(`\x1B[31mError: File extension '${ext}' for asset '${pattern}' is not allowed by policy. Installation aborted.\x1B[0m`);
|
|
5017
5360
|
process.exit(1);
|
|
5018
5361
|
}
|
|
5019
|
-
const srcFile =
|
|
5020
|
-
if (
|
|
5362
|
+
const srcFile = join8(sourceDir, normPattern);
|
|
5363
|
+
if (existsSync8(srcFile) && statSync(srcFile).isFile()) {
|
|
5021
5364
|
filesToCopy.push({
|
|
5022
5365
|
src: srcFile,
|
|
5023
5366
|
dest: normPattern,
|
|
@@ -5032,7 +5375,7 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
5032
5375
|
}
|
|
5033
5376
|
let totalSize = 0;
|
|
5034
5377
|
filesToCopy.forEach((item) => {
|
|
5035
|
-
if (
|
|
5378
|
+
if (existsSync8(item.src)) {
|
|
5036
5379
|
totalSize += statSync(item.src).size;
|
|
5037
5380
|
}
|
|
5038
5381
|
});
|
|
@@ -5042,8 +5385,8 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
5042
5385
|
}
|
|
5043
5386
|
let conflicts = false;
|
|
5044
5387
|
filesToCopy.forEach((item) => {
|
|
5045
|
-
const destPath =
|
|
5046
|
-
if (
|
|
5388
|
+
const destPath = join8(options.target, item.dest);
|
|
5389
|
+
if (existsSync8(destPath)) {
|
|
5047
5390
|
if (!options.force) {
|
|
5048
5391
|
console.error(` \x1B[31mConflict:\x1B[0m File already exists at destination: ${item.dest}`);
|
|
5049
5392
|
conflicts = true;
|
|
@@ -5063,7 +5406,7 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
5063
5406
|
console.log(`
|
|
5064
5407
|
\x1B[33mPlanned Installation Actions:\x1B[0m`);
|
|
5065
5408
|
filesToCopy.forEach((item) => {
|
|
5066
|
-
const exists =
|
|
5409
|
+
const exists = existsSync8(join8(options.target, item.dest));
|
|
5067
5410
|
const suffix = exists ? " \x1B[33m(will overwrite)\x1B[0m" : "";
|
|
5068
5411
|
console.log(` - \x1B[36m[WOULD COPY]\x1B[0m ${item.src} -> ${item.dest}${suffix}`);
|
|
5069
5412
|
});
|
|
@@ -5073,17 +5416,17 @@ function handlePluginInstall(pluginPath, options) {
|
|
|
5073
5416
|
process.exit(1);
|
|
5074
5417
|
}
|
|
5075
5418
|
filesToCopy.forEach((item) => {
|
|
5076
|
-
const destPath =
|
|
5077
|
-
const destDir =
|
|
5078
|
-
if (!
|
|
5079
|
-
|
|
5419
|
+
const destPath = join8(options.target, item.dest);
|
|
5420
|
+
const destDir = dirname4(destPath);
|
|
5421
|
+
if (!existsSync8(destDir)) {
|
|
5422
|
+
mkdirSync3(destDir, { recursive: true });
|
|
5080
5423
|
}
|
|
5081
|
-
if (
|
|
5424
|
+
if (existsSync8(destPath)) {
|
|
5082
5425
|
const bakPath = `${destPath}.bak`;
|
|
5083
|
-
|
|
5426
|
+
writeFileSync4(bakPath, readFileSync9(destPath));
|
|
5084
5427
|
console.log(` \x1B[33mBACKUP:\x1B[0m Created backup: ${item.dest}.bak`);
|
|
5085
5428
|
}
|
|
5086
|
-
|
|
5429
|
+
writeFileSync4(destPath, readFileSync9(item.src));
|
|
5087
5430
|
console.log(` \x1B[32mCOPY:\x1B[0m ${item.dest}`);
|
|
5088
5431
|
});
|
|
5089
5432
|
console.log(`
|
|
@@ -5112,7 +5455,7 @@ function handlePluginStatus(options) {
|
|
|
5112
5455
|
console.log(`
|
|
5113
5456
|
\u{1F50C} \x1B[36mAuditing Plugins Status in: ${options.target}\x1B[0m`);
|
|
5114
5457
|
console.log("==================================================");
|
|
5115
|
-
if (!
|
|
5458
|
+
if (!existsSync8(pluginsDir)) {
|
|
5116
5459
|
console.log(" No plugins directory found. 0 plugins installed.\n");
|
|
5117
5460
|
return;
|
|
5118
5461
|
}
|
|
@@ -5127,8 +5470,8 @@ function handlePluginStatus(options) {
|
|
|
5127
5470
|
}
|
|
5128
5471
|
files.forEach((f) => {
|
|
5129
5472
|
try {
|
|
5130
|
-
const pPath =
|
|
5131
|
-
const p = parseYaml(
|
|
5473
|
+
const pPath = join8(pluginsDir, f);
|
|
5474
|
+
const p = parseYaml(readFileSync9(pPath, "utf8"));
|
|
5132
5475
|
if (p && p.name) {
|
|
5133
5476
|
console.log(`
|
|
5134
5477
|
* \x1B[32m${p.name}\x1B[0m (v${p.version || "1.0.0"})`);
|
|
@@ -5136,8 +5479,8 @@ function handlePluginStatus(options) {
|
|
|
5136
5479
|
let presentCount = 0;
|
|
5137
5480
|
if (Array.isArray(p.allowed_file_patterns)) {
|
|
5138
5481
|
p.allowed_file_patterns.forEach((pat) => {
|
|
5139
|
-
const destPath =
|
|
5140
|
-
if (
|
|
5482
|
+
const destPath = join8(options.target, pat);
|
|
5483
|
+
if (existsSync8(destPath) && statSync(destPath).isFile()) {
|
|
5141
5484
|
presentCount++;
|
|
5142
5485
|
} else {
|
|
5143
5486
|
missingCount++;
|
|
@@ -5153,8 +5496,8 @@ function handlePluginStatus(options) {
|
|
|
5153
5496
|
console.log(` Status: \x1B[33mIncomplete\x1B[0m (${presentCount}/${total} assets present, ${missingCount} missing)`);
|
|
5154
5497
|
console.log(` Missing Assets:`);
|
|
5155
5498
|
p.allowed_file_patterns.forEach((pat) => {
|
|
5156
|
-
const destPath =
|
|
5157
|
-
if (!
|
|
5499
|
+
const destPath = join8(options.target, pat);
|
|
5500
|
+
if (!existsSync8(destPath) || !statSync(destPath).isFile()) {
|
|
5158
5501
|
console.log(` \x1B[31m\u2717\x1B[0m ${pat}`);
|
|
5159
5502
|
}
|
|
5160
5503
|
});
|
|
@@ -5184,12 +5527,12 @@ function handleCatalogList(options) {
|
|
|
5184
5527
|
}
|
|
5185
5528
|
const installedSlugs = /* @__PURE__ */ new Set();
|
|
5186
5529
|
const pluginsDir = getPluginsDir(options.target);
|
|
5187
|
-
if (
|
|
5530
|
+
if (existsSync8(pluginsDir)) {
|
|
5188
5531
|
try {
|
|
5189
5532
|
const files = readdirSync(pluginsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
5190
5533
|
files.forEach((f) => {
|
|
5191
5534
|
try {
|
|
5192
|
-
const parsed = parseYaml(
|
|
5535
|
+
const parsed = parseYaml(readFileSync9(join8(pluginsDir, f), "utf8"));
|
|
5193
5536
|
if (parsed && parsed.slug) {
|
|
5194
5537
|
installedSlugs.add(parsed.slug);
|
|
5195
5538
|
}
|
|
@@ -5303,9 +5646,9 @@ function handleCatalogInstall(slug, options) {
|
|
|
5303
5646
|
const policy = loadRegistryPolicy(options.target || process.cwd());
|
|
5304
5647
|
let srcPath;
|
|
5305
5648
|
if (p._source === "bundled") {
|
|
5306
|
-
srcPath =
|
|
5649
|
+
srcPath = join8(sourceRoot, ".ai", "plugins", "catalog", `${slug}.yaml`);
|
|
5307
5650
|
} else if (p._source === "local") {
|
|
5308
|
-
srcPath =
|
|
5651
|
+
srcPath = join8(options.target || process.cwd(), ".ai", "plugins", "catalog", `${slug}.yaml`);
|
|
5309
5652
|
} else if (p._source && p._source.startsWith("remote:")) {
|
|
5310
5653
|
const regName = p._source.substring(7);
|
|
5311
5654
|
const sources = loadRegistrySources();
|
|
@@ -5318,11 +5661,11 @@ function handleCatalogInstall(slug, options) {
|
|
|
5318
5661
|
process.exit(1);
|
|
5319
5662
|
}
|
|
5320
5663
|
}
|
|
5321
|
-
srcPath =
|
|
5664
|
+
srcPath = join8(sourceRoot, ".ai", "registry-cache", regName, "catalog", `${slug}.yaml`);
|
|
5322
5665
|
} else {
|
|
5323
|
-
srcPath =
|
|
5666
|
+
srcPath = join8(sourceRoot, ".ai", "plugins", "catalog", `${slug}.yaml`);
|
|
5324
5667
|
}
|
|
5325
|
-
if (!
|
|
5668
|
+
if (!existsSync8(srcPath)) {
|
|
5326
5669
|
console.error(`\x1B[31mError: Packed plugin manifest not found at: ${srcPath}\x1B[0m`);
|
|
5327
5670
|
process.exit(1);
|
|
5328
5671
|
}
|
|
@@ -5341,19 +5684,19 @@ function handleCatalogStatus(options) {
|
|
|
5341
5684
|
}
|
|
5342
5685
|
plugins.forEach((p) => {
|
|
5343
5686
|
const slug = p.slug;
|
|
5344
|
-
const destManifest =
|
|
5345
|
-
if (!
|
|
5687
|
+
const destManifest = join8(pluginsDir, `${slug}.yaml`);
|
|
5688
|
+
if (!existsSync8(destManifest)) {
|
|
5346
5689
|
console.log(` - \x1B[33m${p.name}\x1B[0m (v${p.version}): \x1B[90mNot installed\x1B[0m`);
|
|
5347
5690
|
console.log(` Install via: \x1B[36mnpx multimodel-dev-os catalog install ${slug} --approved\x1B[0m`);
|
|
5348
5691
|
} else {
|
|
5349
5692
|
let missingCount = 0;
|
|
5350
5693
|
let presentCount = 0;
|
|
5351
5694
|
try {
|
|
5352
|
-
const targetP = parseYaml(
|
|
5695
|
+
const targetP = parseYaml(readFileSync9(destManifest, "utf8"));
|
|
5353
5696
|
if (Array.isArray(targetP.allowed_file_patterns)) {
|
|
5354
5697
|
targetP.allowed_file_patterns.forEach((pat) => {
|
|
5355
|
-
const destPath =
|
|
5356
|
-
if (
|
|
5698
|
+
const destPath = join8(options.target, pat);
|
|
5699
|
+
if (existsSync8(destPath) && statSync(destPath).isFile()) {
|
|
5357
5700
|
presentCount++;
|
|
5358
5701
|
} else {
|
|
5359
5702
|
missingCount++;
|
|
@@ -5510,22 +5853,28 @@ function handleRegistryList(options) {
|
|
|
5510
5853
|
console.log("==================================================");
|
|
5511
5854
|
console.log(`Policy Status: allow_remote_registries = \x1B[${policy.allow_remote_registries ? "32mtrue" : "33mfalse"}\x1B[0m (Remote registries are disabled by default for safety)
|
|
5512
5855
|
`);
|
|
5856
|
+
const lockfile = loadRegistryLockfile(options.target || process.cwd());
|
|
5513
5857
|
sources.forEach((s) => {
|
|
5514
5858
|
const status = s.enabled ? "\x1B[32m\u25CF enabled\x1B[0m" : "\x1B[90m\u25CB disabled\x1B[0m";
|
|
5515
5859
|
const label = s.name === "bundled" ? "bundled" : s.type === "local" ? `local:${s.name}` : `remote:${s.name}`;
|
|
5516
|
-
|
|
5860
|
+
const lockEntry = lockfile.entries[s.name];
|
|
5861
|
+
const lockBadge = lockEntry ? lockEntry.signature ? " \x1B[32m[signed]\x1B[0m" : " \x1B[33m[unsigned]\x1B[0m" : " \x1B[90m[no lockfile entry]\x1B[0m";
|
|
5862
|
+
console.log(` \x1B[32m${s.name}\x1B[0m [${label}] ${status}${lockBadge}`);
|
|
5517
5863
|
console.log(` type: ${s.type}`);
|
|
5518
5864
|
console.log(` url: ${s.url}`);
|
|
5519
5865
|
console.log(` trust_level: ${s.trust_level}`);
|
|
5520
5866
|
console.log(` safety_policy: ${s.safety_policy}`);
|
|
5521
5867
|
console.log(` checksum: ${s.checksum_required ? "required (SHA-256 integrity)" : "not required"}`);
|
|
5522
|
-
console.log(` signature: ${s.signature_required ? "required" : "not required
|
|
5868
|
+
console.log(` signature: ${s.signature_required ? "required (HMAC-SHA256)" : "not required"}`);
|
|
5523
5869
|
if (s.last_synced_at)
|
|
5524
5870
|
console.log(` last_synced: ${s.last_synced_at}`);
|
|
5871
|
+
if (lockEntry)
|
|
5872
|
+
console.log(` lockfile: synced ${lockEntry.synced_at}, hash ${lockEntry.catalog_sha256.slice(0, 16)}...`);
|
|
5525
5873
|
});
|
|
5526
5874
|
console.log("\nUse \x1B[36mregistry show <name>\x1B[0m to view detailed source configuration.");
|
|
5527
5875
|
console.log("Use \x1B[36mregistry status\x1B[0m to see policy states and cache health.");
|
|
5528
|
-
console.log("Use \x1B[36mregistry verify <name>\x1B[0m to perform integrity checks
|
|
5876
|
+
console.log("Use \x1B[36mregistry verify <name>\x1B[0m to perform integrity checks.");
|
|
5877
|
+
console.log("Use \x1B[36mregistry lock\x1B[0m to inspect the provenance lockfile.\n");
|
|
5529
5878
|
}
|
|
5530
5879
|
function handleRegistryAdd(name, url, options) {
|
|
5531
5880
|
const policy = loadRegistryPolicy(options.target);
|
|
@@ -5610,14 +5959,14 @@ Run with --approved to apply:
|
|
|
5610
5959
|
}
|
|
5611
5960
|
sources.splice(idx, 1);
|
|
5612
5961
|
saveRegistrySources(sources);
|
|
5613
|
-
const cacheDir =
|
|
5614
|
-
if (
|
|
5962
|
+
const cacheDir = join8(sourceRoot, ".ai", "registry-cache", name);
|
|
5963
|
+
if (existsSync8(cacheDir)) {
|
|
5615
5964
|
try {
|
|
5616
5965
|
const files = readdirSync(cacheDir);
|
|
5617
5966
|
files.forEach((f) => {
|
|
5618
|
-
const fp =
|
|
5967
|
+
const fp = join8(cacheDir, f);
|
|
5619
5968
|
if (statSync(fp).isFile()) {
|
|
5620
|
-
|
|
5969
|
+
writeFileSync4(fp, "");
|
|
5621
5970
|
}
|
|
5622
5971
|
});
|
|
5623
5972
|
} catch (e) {
|
|
@@ -5626,7 +5975,7 @@ Run with --approved to apply:
|
|
|
5626
5975
|
console.log(`
|
|
5627
5976
|
\x1B[32m\u2714 Registry '${name}' removed successfully.\x1B[0m`);
|
|
5628
5977
|
console.log(` Source entry removed from .ai/registries/sources.yaml`);
|
|
5629
|
-
if (
|
|
5978
|
+
if (existsSync8(cacheDir)) {
|
|
5630
5979
|
console.log(` Cache directory cleared: .ai/registry-cache/${name}/`);
|
|
5631
5980
|
}
|
|
5632
5981
|
console.log("");
|
|
@@ -5687,9 +6036,9 @@ To execute this sync operation, run:`);
|
|
|
5687
6036
|
`);
|
|
5688
6037
|
process.exit(1);
|
|
5689
6038
|
}
|
|
5690
|
-
const cacheDir =
|
|
5691
|
-
if (!
|
|
5692
|
-
|
|
6039
|
+
const cacheDir = join8(sourceRoot, ".ai", "registry-cache", name);
|
|
6040
|
+
if (!existsSync8(cacheDir)) {
|
|
6041
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
5693
6042
|
}
|
|
5694
6043
|
console.log(`
|
|
5695
6044
|
\u{1F504} \x1B[36mSyncing Registry: ${name}\x1B[0m`);
|
|
@@ -5698,8 +6047,8 @@ To execute this sync operation, run:`);
|
|
|
5698
6047
|
const catalogUrl = url.endsWith("/") ? `${url}catalog.yaml` : url;
|
|
5699
6048
|
const manifestUrl = catalogUrl.replace(/catalog\.yaml$/, "manifest.json");
|
|
5700
6049
|
try {
|
|
5701
|
-
const catalogDest =
|
|
5702
|
-
const manifestDest =
|
|
6050
|
+
const catalogDest = join8(cacheDir, "catalog.yaml");
|
|
6051
|
+
const manifestDest = join8(cacheDir, "manifest.json");
|
|
5703
6052
|
const fetchUrlSync = (targetUrl) => {
|
|
5704
6053
|
validateRegistryUrl(targetUrl, policy);
|
|
5705
6054
|
const script = `
|
|
@@ -5721,7 +6070,7 @@ To execute this sync operation, run:`);
|
|
|
5721
6070
|
console.log(`Downloading: ${catalogUrl}`);
|
|
5722
6071
|
console.log(` \u2192 .ai/registry-cache/${name}/catalog.yaml ...`);
|
|
5723
6072
|
const catalogData = fetchUrlSync(catalogUrl);
|
|
5724
|
-
|
|
6073
|
+
writeFileSync4(catalogDest, catalogData, "utf8");
|
|
5725
6074
|
const catalogSize = (Buffer.byteLength(catalogData) / 1024).toFixed(1);
|
|
5726
6075
|
console.log(` \u2192 OK (${catalogSize}KB)`);
|
|
5727
6076
|
let manifestData = null;
|
|
@@ -5729,7 +6078,7 @@ To execute this sync operation, run:`);
|
|
|
5729
6078
|
console.log(`Downloading: ${manifestUrl}`);
|
|
5730
6079
|
console.log(` \u2192 .ai/registry-cache/${name}/manifest.json ...`);
|
|
5731
6080
|
manifestData = fetchUrlSync(manifestUrl);
|
|
5732
|
-
|
|
6081
|
+
writeFileSync4(manifestDest, manifestData, "utf8");
|
|
5733
6082
|
const manifestSize = (Buffer.byteLength(manifestData) / 1024).toFixed(1);
|
|
5734
6083
|
console.log(` \u2192 OK (${manifestSize}KB)`);
|
|
5735
6084
|
} catch (e) {
|
|
@@ -5751,9 +6100,9 @@ To execute this sync operation, run:`);
|
|
|
5751
6100
|
for (const [file, hash] of Object.entries(manifestObj.files_hashes)) {
|
|
5752
6101
|
if (file === "catalog.yaml" || file === "manifest.json")
|
|
5753
6102
|
continue;
|
|
5754
|
-
const fileDest =
|
|
6103
|
+
const fileDest = join8(cacheDir, file);
|
|
5755
6104
|
const relativeToCache = relative(cacheDir, fileDest);
|
|
5756
|
-
if (relativeToCache.includes("..") ||
|
|
6105
|
+
if (relativeToCache.includes("..") || isAbsolute2(relativeToCache)) {
|
|
5757
6106
|
console.error(`\x1B[31mError: Safe path violation in manifest files list: ${file}\x1B[0m`);
|
|
5758
6107
|
process.exit(1);
|
|
5759
6108
|
}
|
|
@@ -5765,11 +6114,11 @@ To execute this sync operation, run:`);
|
|
|
5765
6114
|
console.error(`\x1B[31mError: Registry cache size limit exceeded (max: ${policy.max_registry_cache_size_kb}KB).\x1B[0m`);
|
|
5766
6115
|
process.exit(1);
|
|
5767
6116
|
}
|
|
5768
|
-
const fileDir =
|
|
5769
|
-
if (!
|
|
5770
|
-
|
|
6117
|
+
const fileDir = dirname4(fileDest);
|
|
6118
|
+
if (!existsSync8(fileDir)) {
|
|
6119
|
+
mkdirSync3(fileDir, { recursive: true });
|
|
5771
6120
|
}
|
|
5772
|
-
|
|
6121
|
+
writeFileSync4(fileDest, fileData, "utf8");
|
|
5773
6122
|
const fileSize = (Buffer.byteLength(fileData) / 1024).toFixed(1);
|
|
5774
6123
|
console.log(` \u2192 OK (${fileSize}KB)`);
|
|
5775
6124
|
const actualHash = computeSHA256(fileData);
|
|
@@ -5789,7 +6138,7 @@ To execute this sync operation, run:`);
|
|
|
5789
6138
|
}
|
|
5790
6139
|
}
|
|
5791
6140
|
const checksumsJson = JSON.stringify(checksums, null, 2);
|
|
5792
|
-
|
|
6141
|
+
writeFileSync4(join8(cacheDir, "checksums.json"), checksumsJson, "utf8");
|
|
5793
6142
|
console.log(` \u2192 .ai/registry-cache/${name}/checksums.json ... OK`);
|
|
5794
6143
|
if (policy.require_checksum && manifestData) {
|
|
5795
6144
|
try {
|
|
@@ -5811,9 +6160,96 @@ To execute this sync operation, run:`);
|
|
|
5811
6160
|
} catch (e) {
|
|
5812
6161
|
}
|
|
5813
6162
|
}
|
|
5814
|
-
|
|
6163
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6164
|
+
source.last_synced_at = syncedAt;
|
|
5815
6165
|
source.pinned_commit_or_hash = computeSHA256(catalogData);
|
|
5816
6166
|
saveRegistrySources(sources);
|
|
6167
|
+
const catalogHash = computeSHA256(catalogData);
|
|
6168
|
+
const manifestHash = manifestData ? computeSHA256(manifestData) : null;
|
|
6169
|
+
const projectDir = options.target || process.cwd();
|
|
6170
|
+
let signingKey = null;
|
|
6171
|
+
let signature = null;
|
|
6172
|
+
try {
|
|
6173
|
+
signingKey = loadSigningKey(projectDir);
|
|
6174
|
+
} catch (sigKeyErr) {
|
|
6175
|
+
console.log(` \x1B[33mWarning: Signing key error \u2014 ${sigKeyErr.message}\x1B[0m`);
|
|
6176
|
+
}
|
|
6177
|
+
if (signingKey) {
|
|
6178
|
+
try {
|
|
6179
|
+
signature = signPayload(signingKey, catalogHash);
|
|
6180
|
+
console.log(" \x1B[32m\u2713 Catalog signed with project signing key (HMAC-SHA256)\x1B[0m");
|
|
6181
|
+
} catch (signErr) {
|
|
6182
|
+
console.log(` \x1B[33mWarning: Signing failed \u2014 ${signErr.message}\x1B[0m`);
|
|
6183
|
+
}
|
|
6184
|
+
} else {
|
|
6185
|
+
if (policy.require_signature) {
|
|
6186
|
+
console.error(`\x1B[31mError: policy require_signature is true but no signing key found.\x1B[0m`);
|
|
6187
|
+
console.error(` Generate a key with: npx multimodel-dev-os registry keygen --approved`);
|
|
6188
|
+
process.exit(1);
|
|
6189
|
+
}
|
|
6190
|
+
console.log(" \x1B[33m\u26A0 No signing key \u2014 provenance recorded without signature.\x1B[0m");
|
|
6191
|
+
console.log(" Generate a key with: npx multimodel-dev-os registry keygen --approved");
|
|
6192
|
+
}
|
|
6193
|
+
const trustedKeys = loadTrustedKeys(projectDir, policy);
|
|
6194
|
+
let verifyRes = { verified: true, status: "unsigned" };
|
|
6195
|
+
let parsedManifest = null;
|
|
6196
|
+
if (manifestData) {
|
|
6197
|
+
try {
|
|
6198
|
+
parsedManifest = JSON.parse(manifestData);
|
|
6199
|
+
verifyRes = verifySignatureBlock({
|
|
6200
|
+
manifest: parsedManifest,
|
|
6201
|
+
trustedKeys,
|
|
6202
|
+
policy,
|
|
6203
|
+
hmacKey: signingKey,
|
|
6204
|
+
source
|
|
6205
|
+
});
|
|
6206
|
+
} catch (_e) {
|
|
6207
|
+
}
|
|
6208
|
+
} else {
|
|
6209
|
+
if (policy.require_signature || policy.allow_unsigned_remote === false) {
|
|
6210
|
+
verifyRes = { verified: false, error: "Manifest missing but signature is required by policy." };
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
const firstSig = parsedManifest && (parsedManifest.signature || Array.isArray(parsedManifest.signatures) && parsedManifest.signatures[0]);
|
|
6214
|
+
const sigBlock = firstSig && typeof firstSig === "object" ? firstSig : null;
|
|
6215
|
+
let trustedPublisherStatus = "unknown";
|
|
6216
|
+
if (sigBlock && sigBlock.key_id) {
|
|
6217
|
+
const tk = trustedKeys.find((k) => k.key_id === sigBlock.key_id);
|
|
6218
|
+
if (tk) {
|
|
6219
|
+
trustedPublisherStatus = tk.status || "inactive";
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
let trustVerdict = "failed";
|
|
6223
|
+
if (verifyRes.verified) {
|
|
6224
|
+
if (verifyRes.status === "verified") {
|
|
6225
|
+
trustVerdict = "verified";
|
|
6226
|
+
} else {
|
|
6227
|
+
trustVerdict = "unsigned_allowed";
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
const verificationErrors = verifyRes.errors || (verifyRes.error ? [verifyRes.error] : []);
|
|
6231
|
+
const verificationWarnings = verifyRes.warning ? [verifyRes.warning] : [];
|
|
6232
|
+
const lockfile = loadRegistryLockfile(projectDir);
|
|
6233
|
+
updateLockfileEntry(lockfile, name, {
|
|
6234
|
+
url: source.url,
|
|
6235
|
+
synced_at: options.synced_at || syncedAt,
|
|
6236
|
+
// Allow override for test determinism
|
|
6237
|
+
catalog_sha256: catalogHash,
|
|
6238
|
+
manifest_sha256: manifestHash,
|
|
6239
|
+
signature,
|
|
6240
|
+
signature_alg: "hmac-sha256",
|
|
6241
|
+
public_signature_status: verifyRes.status || "unsigned",
|
|
6242
|
+
public_signature_algorithm: sigBlock ? sigBlock.algorithm : null,
|
|
6243
|
+
public_signature_key_id: sigBlock ? sigBlock.key_id : null,
|
|
6244
|
+
trusted_publisher_status: trustedPublisherStatus,
|
|
6245
|
+
trust_store_path: policy.trusted_keys_file || ".ai/registries/trusted-keys.yaml",
|
|
6246
|
+
trust_verdict: trustVerdict,
|
|
6247
|
+
lockfile_verdict: "verified",
|
|
6248
|
+
verification_errors: verificationErrors,
|
|
6249
|
+
verification_warnings: verificationWarnings
|
|
6250
|
+
});
|
|
6251
|
+
saveRegistryLockfile(projectDir, lockfile);
|
|
6252
|
+
console.log(` \x1B[32m\u2713 Provenance lockfile updated: .ai/registry-lock.json\x1B[0m`);
|
|
5817
6253
|
let pluginCount = 0;
|
|
5818
6254
|
try {
|
|
5819
6255
|
const catParsed = parseYaml(catalogData);
|
|
@@ -5825,11 +6261,13 @@ To execute this sync operation, run:`);
|
|
|
5825
6261
|
console.log(` Cache location: .ai/registry-cache/${name}/`);
|
|
5826
6262
|
console.log(` Plugins cached: ${pluginCount} entries`);
|
|
5827
6263
|
console.log(` Checksum status: VERIFIED (SHA256)`);
|
|
5828
|
-
console.log(`
|
|
6264
|
+
console.log(` Provenance: ${signature ? "SIGNED (HMAC-SHA256)" : "Unsigned (no signing key)"}`);
|
|
6265
|
+
console.log(` Last synced: ${syncedAt}`);
|
|
5829
6266
|
console.log(`
|
|
5830
6267
|
Next steps:`);
|
|
5831
6268
|
console.log(` \u2022 Browse: npx multimodel-dev-os catalog list --source remote:${name}`);
|
|
5832
6269
|
console.log(` \u2022 Verify: npx multimodel-dev-os registry verify ${name}`);
|
|
6270
|
+
console.log(` \u2022 Lock: npx multimodel-dev-os registry lock`);
|
|
5833
6271
|
console.log(` \u2022 Install: npx multimodel-dev-os catalog install <slug> --approved
|
|
5834
6272
|
`);
|
|
5835
6273
|
} catch (e) {
|
|
@@ -5847,28 +6285,58 @@ function handleRegistryStatus(options) {
|
|
|
5847
6285
|
const sources = loadRegistrySources();
|
|
5848
6286
|
const policy = loadRegistryPolicy(options.target);
|
|
5849
6287
|
if (options.json) {
|
|
5850
|
-
console.log(JSON.stringify({ sources, policy
|
|
6288
|
+
console.log(JSON.stringify({ sources, policy }, null, 2));
|
|
5851
6289
|
return;
|
|
5852
6290
|
}
|
|
6291
|
+
const projectDir = options.target || process.cwd();
|
|
6292
|
+
let signingKeyStatus = "\x1B[90mnot configured\x1B[0m";
|
|
6293
|
+
try {
|
|
6294
|
+
const sk = loadSigningKey(projectDir);
|
|
6295
|
+
signingKeyStatus = sk ? `\x1B[32mconfigured\x1B[0m (${getSigningKeyPath(projectDir)})` : "\x1B[90mnot configured\x1B[0m";
|
|
6296
|
+
} catch (e) {
|
|
6297
|
+
signingKeyStatus = `\x1B[31merror: ${e.message}\x1B[0m`;
|
|
6298
|
+
}
|
|
6299
|
+
const lockfile = loadRegistryLockfile(projectDir);
|
|
6300
|
+
const lockfileEntryCount = Object.keys(lockfile.entries).length;
|
|
6301
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
6302
|
+
const lockfileStatus = existsSync8(lockfilePath) ? `\x1B[32mpresent\x1B[0m (${lockfileEntryCount} entr${lockfileEntryCount === 1 ? "y" : "ies"})` : "\x1B[90mnot present\x1B[0m";
|
|
5853
6303
|
console.log(`
|
|
5854
6304
|
\u{1F4CA} \x1B[36mRegistry Status [v${version}]\x1B[0m`);
|
|
5855
6305
|
console.log("==================================================");
|
|
5856
6306
|
console.log(`\x1B[33mPolicy State:\x1B[0m`);
|
|
5857
|
-
console.log(` allow_remote_registries:
|
|
5858
|
-
console.log(` require_checksum:
|
|
5859
|
-
console.log(` require_signature:
|
|
5860
|
-
console.log(`
|
|
5861
|
-
console.log(`
|
|
5862
|
-
console.log(`
|
|
5863
|
-
console.log(`
|
|
6307
|
+
console.log(` allow_remote_registries: \x1B[${policy.allow_remote_registries ? "32mtrue" : "33mfalse"}\x1B[0m (Disabled by default)`);
|
|
6308
|
+
console.log(` require_checksum: ${policy.require_checksum ? "\x1B[32mtrue\x1B[0m (SHA256 integrity enforced)" : "\x1B[33mfalse\x1B[0m"}`);
|
|
6309
|
+
console.log(` require_signature: ${policy.require_signature ? "\x1B[32mtrue\x1B[0m (HMAC-SHA256 enforced)" : "\x1B[90mfalse\x1B[0m"}`);
|
|
6310
|
+
console.log(` require_lockfile_on_verify: ${policy.require_lockfile_on_verify ? "\x1B[32mtrue\x1B[0m" : "\x1B[90mfalse\x1B[0m"}`);
|
|
6311
|
+
console.log(` allow_untrusted_install: ${policy.allow_untrusted_install ? "\x1B[33mtrue\x1B[0m" : "\x1B[32mfalse\x1B[0m (secured)"}`);
|
|
6312
|
+
console.log(` allow_unsigned_local: ${policy.allow_unsigned_local ? "\x1B[32mtrue\x1B[0m" : "\x1B[33mfalse\x1B[0m"}`);
|
|
6313
|
+
console.log(` allow_unsigned_bundled: ${policy.allow_unsigned_bundled ? "\x1B[32mtrue\x1B[0m" : "\x1B[33mfalse\x1B[0m"}`);
|
|
6314
|
+
console.log(` allow_unsigned_remote: ${policy.allow_unsigned_remote ? "\x1B[32mtrue\x1B[0m" : "\x1B[33mfalse\x1B[0m"}`);
|
|
6315
|
+
console.log(` require_trusted_publisher: ${policy.require_trusted_publisher ? "\x1B[32mtrue\x1B[0m" : "\x1B[90mfalse\x1B[0m"}`);
|
|
6316
|
+
console.log(` provenance_required: ${policy.provenance_required ? "\x1B[32mtrue\x1B[0m" : "\x1B[90mfalse\x1B[0m"}`);
|
|
6317
|
+
console.log(` trusted_keys_file: \x1B[36m${policy.trusted_keys_file}\x1B[0m`);
|
|
6318
|
+
console.log(` allowed_signature_algs: \x1B[36m${(policy.allowed_signature_algorithms || []).join(", ")}\x1B[0m`);
|
|
6319
|
+
console.log(` max_plugin_files: ${policy.max_plugin_files}`);
|
|
6320
|
+
console.log(` max_plugin_size_kb: ${policy.max_plugin_size_kb}KB`);
|
|
6321
|
+
console.log(` max_registry_cache_size: ${policy.max_registry_cache_size_kb}KB`);
|
|
6322
|
+
console.log(`
|
|
6323
|
+
\x1B[33mSigning & Provenance:\x1B[0m`);
|
|
6324
|
+
console.log(` Signing key: ${signingKeyStatus}`);
|
|
6325
|
+
console.log(` Lockfile: ${lockfileStatus}`);
|
|
6326
|
+
if (lockfileEntryCount > 0) {
|
|
6327
|
+
Object.entries(lockfile.entries).forEach(([rName, entry]) => {
|
|
6328
|
+
const sigBadge = entry.signature ? "\x1B[32m[signed]\x1B[0m" : "\x1B[33m[unsigned]\x1B[0m";
|
|
6329
|
+
console.log(` ${rName}: ${sigBadge} synced ${entry.synced_at || "unknown"}`);
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
5864
6332
|
console.log(`
|
|
5865
6333
|
\x1B[33mSources:\x1B[0m`);
|
|
5866
6334
|
sources.forEach((s) => {
|
|
5867
6335
|
const status = s.enabled ? "\x1B[32m\u25CF enabled\x1B[0m" : "\x1B[90m\u25CB disabled\x1B[0m";
|
|
5868
6336
|
const label = s.name === "bundled" ? "bundled" : s.type === "local" ? `local:${s.name}` : `remote:${s.name}`;
|
|
5869
6337
|
const synced = s.last_synced_at ? `synced: ${s.last_synced_at}` : "never synced";
|
|
5870
|
-
const cacheDir =
|
|
5871
|
-
const hasCache = s.type !== "local" &&
|
|
6338
|
+
const cacheDir = join8(sourceRoot, ".ai", "registry-cache", s.name);
|
|
6339
|
+
const hasCache = s.type !== "local" && existsSync8(cacheDir);
|
|
5872
6340
|
console.log(` ${s.name} ${status} [${label}] (${s.type}, ${s.trust_level})`);
|
|
5873
6341
|
if (s.type !== "local") {
|
|
5874
6342
|
console.log(` URL: ${s.url}`);
|
|
@@ -5884,87 +6352,335 @@ function handleRegistryVerify(name, options) {
|
|
|
5884
6352
|
console.log(`
|
|
5885
6353
|
\u{1F50D} \x1B[36mVerifying Registry: ${name}\x1B[0m`);
|
|
5886
6354
|
console.log("==================================================");
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
if (!existsSync5(catalogPath)) {
|
|
5890
|
-
console.error("\x1B[31mError: Bundled catalog.yaml not found.\x1B[0m");
|
|
5891
|
-
process.exit(1);
|
|
5892
|
-
}
|
|
5893
|
-
const content = readFileSync6(catalogPath, "utf8");
|
|
5894
|
-
const hash = computeSHA256(content);
|
|
5895
|
-
console.log(` Verification Type: Local verification (no network required)`);
|
|
5896
|
-
console.log(` File: .ai/plugins/catalog.yaml`);
|
|
5897
|
-
console.log(` SHA256 Checksum: ${hash}`);
|
|
5898
|
-
console.log(` Status: \x1B[32m\u2713 Present and readable (Integrity Verified)\x1B[0m`);
|
|
5899
|
-
try {
|
|
5900
|
-
const parsed = parseYaml(content);
|
|
5901
|
-
const pluginCount = ((parsed.catalog || {}).plugins || []).length;
|
|
5902
|
-
console.log(` Plugins: ${pluginCount} entries parsed successfully`);
|
|
5903
|
-
console.log(`
|
|
5904
|
-
\x1B[32m\u2714 Bundled registry verification passed. (Offline & Secure)\x1B[0m
|
|
5905
|
-
`);
|
|
5906
|
-
} catch (e) {
|
|
5907
|
-
console.error(`
|
|
5908
|
-
\x1B[31m\u2717 Bundled registry verification failed: ${e.message}\x1B[0m
|
|
5909
|
-
`);
|
|
5910
|
-
process.exit(1);
|
|
5911
|
-
}
|
|
5912
|
-
return;
|
|
5913
|
-
}
|
|
6355
|
+
const projectDir = options.target || process.cwd();
|
|
6356
|
+
const policy = loadRegistryPolicy(projectDir);
|
|
5914
6357
|
const sources = loadRegistrySources();
|
|
5915
6358
|
const source = sources.find((s) => s.name === name);
|
|
5916
|
-
|
|
5917
|
-
|
|
6359
|
+
let isBundled = name === "bundled";
|
|
6360
|
+
let isLocal = source ? source.type === "local" : false;
|
|
6361
|
+
let isRemote = source ? source.type === "remote" : false;
|
|
6362
|
+
let url = source ? source.url : isBundled ? ".ai/plugins/catalog.yaml" : null;
|
|
6363
|
+
if (!source && !isBundled) {
|
|
6364
|
+
console.error(`\x1B[31mError: Registry '${name}' is not configured.\x1B[0m`);
|
|
6365
|
+
process.exit(1);
|
|
6366
|
+
}
|
|
6367
|
+
let urlValidationStatus = "N/A";
|
|
6368
|
+
if (isRemote) {
|
|
5918
6369
|
try {
|
|
5919
|
-
validateRegistryUrl(
|
|
6370
|
+
validateRegistryUrl(url, policy);
|
|
6371
|
+
urlValidationStatus = "\x1B[32m\u2713 Valid HTTPS\x1B[0m";
|
|
5920
6372
|
} catch (err) {
|
|
5921
|
-
|
|
5922
|
-
process.exit(1);
|
|
6373
|
+
urlValidationStatus = `\x1B[31m\u2717 Invalid: ${err.message}\x1B[0m`;
|
|
5923
6374
|
}
|
|
6375
|
+
} else {
|
|
6376
|
+
urlValidationStatus = "\x1B[32m\u2713 Valid Local Path\x1B[0m";
|
|
5924
6377
|
}
|
|
5925
|
-
|
|
5926
|
-
if (
|
|
6378
|
+
let cacheDir;
|
|
6379
|
+
if (isBundled) {
|
|
6380
|
+
cacheDir = join8(sourceRoot, ".ai", "plugins");
|
|
6381
|
+
} else {
|
|
6382
|
+
cacheDir = join8(sourceRoot, ".ai", "registry-cache", name);
|
|
6383
|
+
}
|
|
6384
|
+
const catalogDest = join8(cacheDir, "catalog.yaml");
|
|
6385
|
+
const manifestDest = join8(cacheDir, "manifest.json");
|
|
6386
|
+
const checksumPath = join8(cacheDir, "checksums.json");
|
|
6387
|
+
if (!isBundled && !existsSync8(cacheDir)) {
|
|
5927
6388
|
console.error(`\x1B[31mError: No cache found for registry '${name}'. Run registry sync first.\x1B[0m`);
|
|
5928
6389
|
process.exit(1);
|
|
5929
6390
|
}
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
console.error(`\x1B[31mError: No checksums.json found in cache for '${name}'.\x1B[0m`);
|
|
6391
|
+
if (isBundled && !existsSync8(catalogDest)) {
|
|
6392
|
+
console.error(`\x1B[31mError: Bundled catalog.yaml not found.\x1B[0m`);
|
|
5933
6393
|
process.exit(1);
|
|
5934
6394
|
}
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6395
|
+
let catalogContent = "";
|
|
6396
|
+
let catalogHash = "N/A";
|
|
6397
|
+
if (existsSync8(catalogDest)) {
|
|
6398
|
+
catalogContent = readFileSync9(catalogDest, "utf8");
|
|
6399
|
+
catalogHash = computeSHA256(catalogContent);
|
|
6400
|
+
}
|
|
6401
|
+
let manifestObj = null;
|
|
6402
|
+
let manifestHash = "N/A";
|
|
6403
|
+
if (existsSync8(manifestDest)) {
|
|
6404
|
+
const manifestData = readFileSync9(manifestDest, "utf8");
|
|
6405
|
+
manifestHash = computeSHA256(manifestData);
|
|
6406
|
+
try {
|
|
6407
|
+
manifestObj = JSON.parse(manifestData);
|
|
6408
|
+
} catch (e) {
|
|
6409
|
+
console.warn(`\x1B[33mWarning: Failed to parse manifest.json: ${e.message}\x1B[0m`);
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
6412
|
+
let integrityVerified = true;
|
|
6413
|
+
if (!isBundled) {
|
|
6414
|
+
if (!existsSync8(checksumPath)) {
|
|
6415
|
+
console.log(` \x1B[33m\u26A0 Checksums: Missing checksums.json in cache\x1B[0m`);
|
|
6416
|
+
integrityVerified = false;
|
|
6417
|
+
} else {
|
|
6418
|
+
try {
|
|
6419
|
+
const checksums = JSON.parse(readFileSync9(checksumPath, "utf8"));
|
|
6420
|
+
Object.entries(checksums).forEach(([file, expectedHash]) => {
|
|
6421
|
+
const filePath = join8(cacheDir, file);
|
|
6422
|
+
if (!existsSync8(filePath)) {
|
|
6423
|
+
console.log(` \x1B[31m\u2717 File missing in cache: ${file}\x1B[0m`);
|
|
6424
|
+
integrityVerified = false;
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
const content = readFileSync9(filePath, "utf8");
|
|
6428
|
+
const actualHash = `sha256:${computeSHA256(content)}`;
|
|
6429
|
+
if (actualHash === expectedHash) {
|
|
6430
|
+
console.log(` \x1B[32m\u2713 ${file}: VERIFIED (Integrity check matched via SHA-256)\x1B[0m`);
|
|
6431
|
+
} else {
|
|
6432
|
+
console.log(` \x1B[31m\u2717 ${file}: MISMATCH\x1B[0m`);
|
|
6433
|
+
console.log(` Expected: ${expectedHash}`);
|
|
6434
|
+
console.log(` Actual: ${actualHash}`);
|
|
6435
|
+
integrityVerified = false;
|
|
6436
|
+
}
|
|
6437
|
+
});
|
|
6438
|
+
} catch (e) {
|
|
6439
|
+
console.log(` \x1B[31m\u2717 Integrity: Failed to verify checksums: ${e.message}\x1B[0m`);
|
|
6440
|
+
integrityVerified = false;
|
|
5944
6441
|
}
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
const lockfile = loadRegistryLockfile(projectDir);
|
|
6445
|
+
const lockEntry = lockfile.entries[name];
|
|
6446
|
+
let lockfileStatus = "N/A";
|
|
6447
|
+
let provenanceStatus = "N/A";
|
|
6448
|
+
let lockfileVerdict = "N/A";
|
|
6449
|
+
if (!isBundled) {
|
|
6450
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
6451
|
+
lockfileStatus = existsSync8(lockfilePath) ? `\x1B[32mpresent\x1B[0m` : `\x1B[33mmissing\x1B[0m`;
|
|
6452
|
+
if (!lockEntry) {
|
|
6453
|
+
if (policy.require_lockfile_on_verify) {
|
|
6454
|
+
provenanceStatus = `\x1B[31m\u2717 Failed (require_lockfile_on_verify is true but entry missing)\x1B[0m`;
|
|
6455
|
+
lockfileVerdict = "Failed";
|
|
6456
|
+
} else {
|
|
6457
|
+
provenanceStatus = `\x1B[33m\u26A0 Missing provenance entry (no sync lock)\x1B[0m`;
|
|
6458
|
+
lockfileVerdict = "Missing";
|
|
6459
|
+
}
|
|
6460
|
+
} else {
|
|
6461
|
+
let isProvMatch = true;
|
|
6462
|
+
if (catalogHash !== lockEntry.catalog_sha256) {
|
|
6463
|
+
isProvMatch = false;
|
|
6464
|
+
console.log(` \x1B[31m\u2717 Lockfile catalog hash mismatch: Expected ${lockEntry.catalog_sha256}, got ${catalogHash}\x1B[0m`);
|
|
6465
|
+
}
|
|
6466
|
+
if (manifestHash !== "N/A" && lockEntry.manifest_sha256 && manifestHash !== lockEntry.manifest_sha256) {
|
|
6467
|
+
isProvMatch = false;
|
|
6468
|
+
console.log(` \x1B[31m\u2717 Lockfile manifest hash mismatch: Expected ${lockEntry.manifest_sha256}, got ${manifestHash}\x1B[0m`);
|
|
6469
|
+
}
|
|
6470
|
+
if (isProvMatch) {
|
|
6471
|
+
provenanceStatus = `\x1B[32m\u2713 Matched lockfile entry\x1B[0m`;
|
|
6472
|
+
lockfileVerdict = "Verified";
|
|
5949
6473
|
} else {
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
console.log(` Actual: ${actualHash}`);
|
|
5953
|
-
allPassed = false;
|
|
6474
|
+
provenanceStatus = `\x1B[31m\u2717 Tampering detected: hashes do not match lockfile\x1B[0m`;
|
|
6475
|
+
lockfileVerdict = "Tampered";
|
|
5954
6476
|
}
|
|
6477
|
+
}
|
|
6478
|
+
} else {
|
|
6479
|
+
lockfileStatus = "N/A (Bundled)";
|
|
6480
|
+
provenanceStatus = "\x1B[32m\u2713 Implicit Trust\x1B[0m";
|
|
6481
|
+
lockfileVerdict = "Verified";
|
|
6482
|
+
}
|
|
6483
|
+
const trustedKeys = loadTrustedKeys(projectDir, policy);
|
|
6484
|
+
let hmacKey = null;
|
|
6485
|
+
try {
|
|
6486
|
+
hmacKey = loadSigningKey(projectDir);
|
|
6487
|
+
} catch (_e) {
|
|
6488
|
+
}
|
|
6489
|
+
let signatureAlgorithm = "None";
|
|
6490
|
+
let signatureKeyId = "None";
|
|
6491
|
+
let trustedPublisherStatus = "N/A";
|
|
6492
|
+
let signatureValidity = "N/A";
|
|
6493
|
+
let signatureResult = { verified: true, status: "unsigned" };
|
|
6494
|
+
if (manifestObj) {
|
|
6495
|
+
signatureResult = verifySignatureBlock({
|
|
6496
|
+
manifest: manifestObj,
|
|
6497
|
+
trustedKeys,
|
|
6498
|
+
policy,
|
|
6499
|
+
hmacKey,
|
|
6500
|
+
source: source || { name: "bundled", type: "local" }
|
|
5955
6501
|
});
|
|
5956
|
-
|
|
5957
|
-
|
|
6502
|
+
const signatureBlocks = [];
|
|
6503
|
+
if (manifestObj.signature && typeof manifestObj.signature === "object") {
|
|
6504
|
+
signatureBlocks.push(manifestObj.signature);
|
|
6505
|
+
}
|
|
6506
|
+
if (Array.isArray(manifestObj.signatures)) {
|
|
6507
|
+
signatureBlocks.push(...manifestObj.signatures);
|
|
6508
|
+
}
|
|
6509
|
+
if (signatureBlocks.length > 0) {
|
|
6510
|
+
const firstSig = signatureBlocks[0];
|
|
6511
|
+
signatureAlgorithm = firstSig.algorithm || "unknown";
|
|
6512
|
+
signatureKeyId = firstSig.key_id || "unknown";
|
|
6513
|
+
const tk = trustedKeys.find((k) => k.key_id === signatureKeyId);
|
|
6514
|
+
if (tk) {
|
|
6515
|
+
trustedPublisherStatus = tk.status === "active" ? `\x1B[32m\u2713 Trusted (${tk.name})\x1B[0m` : `\x1B[31m\u2717 ${tk.status} (${tk.name})\x1B[0m`;
|
|
6516
|
+
} else {
|
|
6517
|
+
trustedPublisherStatus = `\x1B[33m\u26A0 Unknown key_id (Not in trust store)\x1B[0m`;
|
|
6518
|
+
}
|
|
6519
|
+
if (signatureResult.verified) {
|
|
6520
|
+
signatureValidity = `\x1B[32m\u2713 Valid Signature\x1B[0m`;
|
|
6521
|
+
} else {
|
|
6522
|
+
const errorMsg = signatureResult.errors ? signatureResult.errors.join(", ") : signatureResult.error || "signature verification failed";
|
|
6523
|
+
signatureValidity = `\x1B[31m\u2717 Invalid Signature (${errorMsg})\x1B[0m`;
|
|
6524
|
+
}
|
|
6525
|
+
} else {
|
|
6526
|
+
if (policy.require_signature || isRemote && policy.allow_unsigned_remote === false) {
|
|
6527
|
+
signatureValidity = `\x1B[31m\u2717 Missing Signature (Enforced by policy)\x1B[0m`;
|
|
6528
|
+
} else {
|
|
6529
|
+
signatureValidity = `\x1B[90mUnsigned\x1B[0m`;
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
} else {
|
|
6533
|
+
if (!isBundled && (policy.require_signature || isRemote && policy.allow_unsigned_remote === false)) {
|
|
6534
|
+
signatureResult = { verified: false, error: "Manifest missing but signature is required by policy." };
|
|
6535
|
+
signatureValidity = `\x1B[31m\u2717 Manifest missing (Enforced by policy)\x1B[0m`;
|
|
6536
|
+
} else {
|
|
6537
|
+
signatureValidity = `\x1B[90mUnsigned (No manifest)\x1B[0m`;
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
console.log(` Source Type: ${isBundled ? "bundled" : source.type}`);
|
|
6541
|
+
console.log(` Source URL/Path: ${url}`);
|
|
6542
|
+
console.log(` URL Validation: ${urlValidationStatus}`);
|
|
6543
|
+
console.log(` Manifest SHA256: ${manifestHash}`);
|
|
6544
|
+
console.log(` Catalog SHA256: ${catalogHash}`);
|
|
6545
|
+
console.log(` Lockfile Status: ${lockfileStatus}`);
|
|
6546
|
+
console.log(` Provenance Status: ${provenanceStatus}`);
|
|
6547
|
+
console.log(` Signature Alg: ${signatureAlgorithm}`);
|
|
6548
|
+
console.log(` Signature Key ID: ${signatureKeyId}`);
|
|
6549
|
+
console.log(` Trusted Publisher: ${trustedPublisherStatus}`);
|
|
6550
|
+
console.log(` Signature Validity: ${signatureValidity}`);
|
|
6551
|
+
let finalVerdict = "\u2717 Failed";
|
|
6552
|
+
let passed = true;
|
|
6553
|
+
if (!integrityVerified)
|
|
6554
|
+
passed = false;
|
|
6555
|
+
if (!isBundled && lockfileVerdict === "Failed")
|
|
6556
|
+
passed = false;
|
|
6557
|
+
if (!isBundled && lockfileVerdict === "Tampered")
|
|
6558
|
+
passed = false;
|
|
6559
|
+
if (!signatureResult.verified)
|
|
6560
|
+
passed = false;
|
|
6561
|
+
if (passed) {
|
|
6562
|
+
if (signatureResult.status === "verified") {
|
|
6563
|
+
finalVerdict = `\x1B[32m\u2713 Verified (Signature matches trusted key)\x1B[0m`;
|
|
6564
|
+
} else if (isBundled || isLocal) {
|
|
6565
|
+
finalVerdict = `\x1B[32m\u2713 Verified (Implicit local trust)\x1B[0m`;
|
|
6566
|
+
} else {
|
|
6567
|
+
finalVerdict = `\x1B[33m\u26A0 Unsigned (Allowed by policy)\x1B[0m`;
|
|
6568
|
+
}
|
|
6569
|
+
} else {
|
|
6570
|
+
const reason = !integrityVerified ? "Integrity check failed" : lockfileVerdict === "Tampered" ? "Lockfile tampering detected" : signatureResult.error || signatureResult.errors && signatureResult.errors.join(", ") || "Signature verification failed";
|
|
6571
|
+
finalVerdict = `\x1B[31m\u2717 Failed (${reason})\x1B[0m`;
|
|
6572
|
+
}
|
|
6573
|
+
console.log(` Final Trust: ${finalVerdict}`);
|
|
6574
|
+
console.log("==================================================");
|
|
6575
|
+
try {
|
|
6576
|
+
const parsed = parseYaml(catalogContent);
|
|
6577
|
+
const pluginCount = ((parsed.catalog || {}).plugins || []).length;
|
|
6578
|
+
console.log(` Plugins Parsed: ${pluginCount} entries`);
|
|
6579
|
+
} catch (e) {
|
|
6580
|
+
console.error(`\x1B[31m\u2717 Catalog parsing failed: ${e.message}\x1B[0m`);
|
|
6581
|
+
process.exit(1);
|
|
6582
|
+
}
|
|
6583
|
+
const verdict = createTrustVerdict({
|
|
6584
|
+
source: name,
|
|
6585
|
+
source_type: isBundled ? "bundled" : source ? source.type : "remote",
|
|
6586
|
+
manifest_hash_status: manifestHash !== "N/A" ? lockEntry && manifestHash === lockEntry.manifest_sha256 ? "verified" : manifestObj ? "unverified" : "missing" : "N/A",
|
|
6587
|
+
catalog_hash_status: catalogHash !== "N/A" ? lockEntry && catalogHash === lockEntry.catalog_sha256 ? "verified" : "unverified" : "N/A",
|
|
6588
|
+
lockfile_status: isBundled ? "N/A" : lockEntry ? "present" : "missing",
|
|
6589
|
+
provenance_status: isBundled ? "N/A" : lockEntry ? lockfileVerdict === "Verified" ? "matched" : lockfileVerdict === "Tampered" ? "mismatch" : "missing" : "N/A",
|
|
6590
|
+
signature_status: signatureResult.status || "unsigned",
|
|
6591
|
+
trusted_publisher_status: signatureResult.status === "verified" ? "trusted" : "N/A",
|
|
6592
|
+
errors: signatureResult.errors || (signatureResult.error ? [signatureResult.error] : []),
|
|
6593
|
+
warnings: signatureResult.warning ? [signatureResult.warning] : [],
|
|
6594
|
+
final_status: passed ? signatureResult.status === "verified" ? "trusted" : isBundled || isLocal ? "trusted" : "warning" : "untrusted"
|
|
6595
|
+
});
|
|
6596
|
+
if (!isBundled && lockEntry) {
|
|
6597
|
+
lockEntry.trust_verdict = passed ? signatureResult.status === "verified" ? "verified" : "unsigned_allowed" : "failed";
|
|
6598
|
+
lockEntry.lockfile_verdict = lockfileVerdict.toLowerCase();
|
|
6599
|
+
lockEntry.verification_errors = verdict.errors;
|
|
6600
|
+
lockEntry.verification_warnings = verdict.warnings;
|
|
6601
|
+
lockEntry.verdict = verdict;
|
|
6602
|
+
saveRegistryLockfile(projectDir, lockfile);
|
|
6603
|
+
}
|
|
6604
|
+
if (passed) {
|
|
6605
|
+
console.log(`
|
|
5958
6606
|
\x1B[32m\u2714 Registry '${name}' verification passed.\x1B[0m
|
|
5959
6607
|
`);
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
\x1B[31m\u2717 Registry '${name}' verification failed
|
|
6608
|
+
} else {
|
|
6609
|
+
console.error(`
|
|
6610
|
+
\x1B[31m\u2717 Registry '${name}' verification failed.\x1B[0m
|
|
5963
6611
|
`);
|
|
5964
|
-
|
|
6612
|
+
process.exit(1);
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
function handleRegistryTrustList(options) {
|
|
6616
|
+
const projectDir = options.target || process.cwd();
|
|
6617
|
+
const policy = loadRegistryPolicy(projectDir);
|
|
6618
|
+
const keys = loadTrustedKeys(projectDir, policy);
|
|
6619
|
+
console.log(`
|
|
6620
|
+
\u{1F511} \x1B[36mRegistry Trust Store \u2014 Trusted Keys\x1B[0m`);
|
|
6621
|
+
console.log("==================================================");
|
|
6622
|
+
console.log(`Trust Store Path: \x1B[36m${policy.trusted_keys_file || ".ai/registries/trusted-keys.yaml"}\x1B[0m`);
|
|
6623
|
+
console.log(`Total Keys: ${keys.length}
|
|
6624
|
+
`);
|
|
6625
|
+
if (keys.length === 0) {
|
|
6626
|
+
console.log(" No trusted keys configured.");
|
|
6627
|
+
} else {
|
|
6628
|
+
keys.forEach((k) => {
|
|
6629
|
+
const statusBadge = k.status === "active" ? "\x1B[32m\u25CF active\x1B[0m" : `\x1B[31m\u25CB ${k.status}\x1B[0m`;
|
|
6630
|
+
console.log(` * \x1B[33m${k.key_id}\x1B[0m [${statusBadge}]`);
|
|
6631
|
+
console.log(` Publisher: ${k.name}`);
|
|
6632
|
+
console.log(` Algorithm: ${k.algorithm}`);
|
|
6633
|
+
console.log(` Scopes: ${(k.scopes || []).join(", ")}`);
|
|
6634
|
+
});
|
|
6635
|
+
}
|
|
6636
|
+
console.log("");
|
|
6637
|
+
}
|
|
6638
|
+
function handleRegistryTrustShow(keyId, options) {
|
|
6639
|
+
const projectDir = options.target || process.cwd();
|
|
6640
|
+
const policy = loadRegistryPolicy(projectDir);
|
|
6641
|
+
const keys = loadTrustedKeys(projectDir, policy);
|
|
6642
|
+
const k = keys.find((key) => key.key_id === keyId);
|
|
6643
|
+
if (!k) {
|
|
6644
|
+
console.error(`\x1B[31mError: Trusted key '${keyId}' not found in the trust store.\x1B[0m`);
|
|
6645
|
+
process.exit(1);
|
|
6646
|
+
}
|
|
6647
|
+
console.log(`
|
|
6648
|
+
\u{1F511} \x1B[36mTrusted Key: ${keyId}\x1B[0m`);
|
|
6649
|
+
console.log("==================================================");
|
|
6650
|
+
console.log(`\x1B[33mKey ID:\x1B[0m ${k.key_id}`);
|
|
6651
|
+
console.log(`\x1B[33mPublisher:\x1B[0m ${k.name}`);
|
|
6652
|
+
console.log(`\x1B[33mAlgorithm:\x1B[0m ${k.algorithm}`);
|
|
6653
|
+
console.log(`\x1B[33mStatus:\x1B[0m ${k.status === "active" ? "\x1B[32mactive\x1B[0m" : `\x1B[31m${k.status}\x1B[0m`}`);
|
|
6654
|
+
console.log(`\x1B[33mScopes:\x1B[0m ${(k.scopes || []).join(", ")}`);
|
|
6655
|
+
console.log(`\x1B[33mPublic Key:\x1B[0m
|
|
6656
|
+
${k.public_key.trim()}`);
|
|
6657
|
+
console.log("");
|
|
6658
|
+
}
|
|
6659
|
+
function handleRegistryTrustVerify(options) {
|
|
6660
|
+
const projectDir = options.target || process.cwd();
|
|
6661
|
+
const policy = loadRegistryPolicy(projectDir);
|
|
6662
|
+
const keys = loadTrustedKeys(projectDir, policy);
|
|
6663
|
+
console.log(`
|
|
6664
|
+
\u{1F511} \x1B[36mVerifying Trust Store Integrity...\x1B[0m`);
|
|
6665
|
+
console.log("==================================================");
|
|
6666
|
+
let passed = true;
|
|
6667
|
+
keys.forEach((k) => {
|
|
6668
|
+
try {
|
|
6669
|
+
normalizePublicKey(k.public_key);
|
|
6670
|
+
console.log(` \x1B[32m\u2713\x1B[0m Key '${k.key_id}' public key format is valid.`);
|
|
6671
|
+
} catch (e) {
|
|
6672
|
+
console.log(` \x1B[31m\u2717\x1B[0m Key '${k.key_id}' public key format error: ${e.message}`);
|
|
6673
|
+
passed = false;
|
|
5965
6674
|
}
|
|
5966
|
-
}
|
|
5967
|
-
|
|
6675
|
+
});
|
|
6676
|
+
if (passed) {
|
|
6677
|
+
console.log(`
|
|
6678
|
+
\x1B[32m\u2714 Trust store verification passed.\x1B[0m
|
|
6679
|
+
`);
|
|
6680
|
+
} else {
|
|
6681
|
+
console.error(`
|
|
6682
|
+
\x1B[31m\u2717 Trust store verification failed.\x1B[0m
|
|
6683
|
+
`);
|
|
5968
6684
|
process.exit(1);
|
|
5969
6685
|
}
|
|
5970
6686
|
}
|
|
@@ -6012,12 +6728,12 @@ function handleRegistryShow(name, options) {
|
|
|
6012
6728
|
console.log(`\x1B[33mPinned Hash:\x1B[0m ${source.pinned_commit_or_hash}`);
|
|
6013
6729
|
}
|
|
6014
6730
|
if (source.type !== "local") {
|
|
6015
|
-
const cacheDir =
|
|
6016
|
-
if (
|
|
6017
|
-
const catalogPath =
|
|
6018
|
-
if (
|
|
6731
|
+
const cacheDir = join8(sourceRoot, ".ai", "registry-cache", name);
|
|
6732
|
+
if (existsSync8(cacheDir)) {
|
|
6733
|
+
const catalogPath = join8(cacheDir, "catalog.yaml");
|
|
6734
|
+
if (existsSync8(catalogPath)) {
|
|
6019
6735
|
try {
|
|
6020
|
-
const parsed = parseYaml(
|
|
6736
|
+
const parsed = parseYaml(readFileSync9(catalogPath, "utf8"));
|
|
6021
6737
|
const count = ((parsed.catalog || {}).plugins || []).length;
|
|
6022
6738
|
console.log(`\x1B[33mCached Plugins:\x1B[0m ${count} entries`);
|
|
6023
6739
|
} catch (e) {
|
|
@@ -6043,8 +6759,8 @@ function handleRegistryShow(name, options) {
|
|
|
6043
6759
|
function handleRegistryCacheClear(options) {
|
|
6044
6760
|
if (!options.approved) {
|
|
6045
6761
|
console.error("\x1B[31mError: Cache cannot be cleared without explicit approval. Pass the --approved flag.\x1B[0m");
|
|
6046
|
-
const cacheRoot2 =
|
|
6047
|
-
if (
|
|
6762
|
+
const cacheRoot2 = join8(sourceRoot, ".ai", "registry-cache");
|
|
6763
|
+
if (existsSync8(cacheRoot2)) {
|
|
6048
6764
|
const dirs = readdirSync(cacheRoot2).filter((d) => d !== "README.md");
|
|
6049
6765
|
console.log(`
|
|
6050
6766
|
\x1B[33mPlanned Action:\x1B[0m Clear ${dirs.length} cached registry directories:`);
|
|
@@ -6058,22 +6774,22 @@ Run with --approved to apply:
|
|
|
6058
6774
|
`);
|
|
6059
6775
|
process.exit(1);
|
|
6060
6776
|
}
|
|
6061
|
-
const cacheRoot =
|
|
6062
|
-
if (!
|
|
6777
|
+
const cacheRoot = join8(sourceRoot, ".ai", "registry-cache");
|
|
6778
|
+
if (!existsSync8(cacheRoot)) {
|
|
6063
6779
|
console.log("\n\x1B[33mNo registry cache directory found. Nothing to clear.\x1B[0m\n");
|
|
6064
6780
|
return;
|
|
6065
6781
|
}
|
|
6066
6782
|
const entries = readdirSync(cacheRoot).filter((d) => d !== "README.md");
|
|
6067
6783
|
let cleared = 0;
|
|
6068
6784
|
entries.forEach((d) => {
|
|
6069
|
-
const dirPath =
|
|
6785
|
+
const dirPath = join8(cacheRoot, d);
|
|
6070
6786
|
try {
|
|
6071
6787
|
if (statSync(dirPath).isDirectory()) {
|
|
6072
6788
|
const files = readdirSync(dirPath);
|
|
6073
6789
|
files.forEach((f) => {
|
|
6074
|
-
const fp =
|
|
6790
|
+
const fp = join8(dirPath, f);
|
|
6075
6791
|
if (statSync(fp).isFile()) {
|
|
6076
|
-
|
|
6792
|
+
writeFileSync4(fp, "");
|
|
6077
6793
|
}
|
|
6078
6794
|
});
|
|
6079
6795
|
cleared++;
|
|
@@ -6087,3 +6803,105 @@ Run with --approved to apply:
|
|
|
6087
6803
|
console.log(` Cache root: .ai/registry-cache/
|
|
6088
6804
|
`);
|
|
6089
6805
|
}
|
|
6806
|
+
function handleRegistryKeygen(options) {
|
|
6807
|
+
const projectDir = options.target || process.cwd();
|
|
6808
|
+
const keyPath = getSigningKeyPath(projectDir);
|
|
6809
|
+
console.log(`
|
|
6810
|
+
\u{1F511} \x1B[36mRegistry Signing Key Generator\x1B[0m`);
|
|
6811
|
+
console.log("==================================================");
|
|
6812
|
+
if (!options.approved) {
|
|
6813
|
+
console.error("\x1B[31mError: Signing key generation requires explicit approval. Pass the --approved flag.\x1B[0m");
|
|
6814
|
+
console.log(`
|
|
6815
|
+
\x1B[33mPlanned Action:\x1B[0m Generate a 32-byte random HMAC-SHA256 signing key.`);
|
|
6816
|
+
console.log(` Destination: ${keyPath}`);
|
|
6817
|
+
console.log(` Mode: 0o600 (owner read/write only)`);
|
|
6818
|
+
console.log(`
|
|
6819
|
+
\x1B[33mSecurity Notes:\x1B[0m`);
|
|
6820
|
+
console.log(` \u2022 Add .ai/registry-signing-key to your .gitignore`);
|
|
6821
|
+
console.log(` \u2022 Share the key securely with trusted team members for co-verification`);
|
|
6822
|
+
console.log(` \u2022 The key is used for HMAC-SHA256 signing of catalog checksums only`);
|
|
6823
|
+
console.log(`
|
|
6824
|
+
To generate, run:`);
|
|
6825
|
+
console.log(` \x1B[36mnpx multimodel-dev-os registry keygen --approved\x1B[0m
|
|
6826
|
+
`);
|
|
6827
|
+
process.exit(1);
|
|
6828
|
+
}
|
|
6829
|
+
let existingKey = null;
|
|
6830
|
+
try {
|
|
6831
|
+
existingKey = loadSigningKey(projectDir);
|
|
6832
|
+
} catch (_e) {
|
|
6833
|
+
}
|
|
6834
|
+
if (existingKey && !options.force) {
|
|
6835
|
+
console.error(`\x1B[31mError: A signing key already exists at: ${keyPath}\x1B[0m`);
|
|
6836
|
+
console.log(`
|
|
6837
|
+
To overwrite, run with --force:`);
|
|
6838
|
+
console.log(` \x1B[36mnpx multimodel-dev-os registry keygen --approved --force\x1B[0m`);
|
|
6839
|
+
console.log(`
|
|
6840
|
+
\x1B[33mWarning:\x1B[0m Overwriting will invalidate all existing signatures in the lockfile.
|
|
6841
|
+
`);
|
|
6842
|
+
process.exit(1);
|
|
6843
|
+
}
|
|
6844
|
+
const newKey = generateSigningKey();
|
|
6845
|
+
saveSigningKey(projectDir, newKey);
|
|
6846
|
+
console.log(`
|
|
6847
|
+
\x1B[32m\u2714 Signing key generated successfully!\x1B[0m`);
|
|
6848
|
+
console.log(` Location: ${keyPath}`);
|
|
6849
|
+
console.log(` Mode: 0o600 (restricted permissions)`);
|
|
6850
|
+
console.log(`
|
|
6851
|
+
\x1B[33mNext steps:\x1B[0m`);
|
|
6852
|
+
console.log(` 1. Add to .gitignore: echo '.ai/registry-signing-key' >> .gitignore`);
|
|
6853
|
+
console.log(` 2. Re-sync registries to generate signed lockfile entries:`);
|
|
6854
|
+
console.log(` npx multimodel-dev-os registry sync <name> --approved`);
|
|
6855
|
+
console.log(` 3. Verify signed provenance:`);
|
|
6856
|
+
console.log(` npx multimodel-dev-os registry verify <name>
|
|
6857
|
+
`);
|
|
6858
|
+
}
|
|
6859
|
+
function handleRegistryLock(options) {
|
|
6860
|
+
const projectDir = options.target || process.cwd();
|
|
6861
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
6862
|
+
console.log(`
|
|
6863
|
+
\u{1F512} \x1B[36mRegistry Provenance Lockfile\x1B[0m`);
|
|
6864
|
+
console.log("==================================================");
|
|
6865
|
+
if (!existsSync8(lockfilePath)) {
|
|
6866
|
+
console.log(` \x1B[90mNo lockfile found at: ${lockfilePath}\x1B[0m`);
|
|
6867
|
+
console.log(` Sync a remote registry to create it:`);
|
|
6868
|
+
console.log(` npx multimodel-dev-os registry sync <name> --approved
|
|
6869
|
+
`);
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
6872
|
+
const lockfile = loadRegistryLockfile(projectDir);
|
|
6873
|
+
const entries = Object.entries(lockfile.entries);
|
|
6874
|
+
if (options.json) {
|
|
6875
|
+
console.log(JSON.stringify(lockfile, null, 2));
|
|
6876
|
+
return;
|
|
6877
|
+
}
|
|
6878
|
+
console.log(` Lockfile version: ${lockfile.lockfile_version}`);
|
|
6879
|
+
console.log(` Generated at: ${lockfile.generated_at}`);
|
|
6880
|
+
console.log(` Path: ${lockfilePath}`);
|
|
6881
|
+
console.log(` Entries: ${entries.length}
|
|
6882
|
+
`);
|
|
6883
|
+
if (entries.length === 0) {
|
|
6884
|
+
console.log(` \x1B[90mNo registry entries recorded yet.\x1B[0m`);
|
|
6885
|
+
console.log(` Sync a remote registry to populate:
|
|
6886
|
+
npx multimodel-dev-os registry sync <name> --approved
|
|
6887
|
+
`);
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
entries.forEach(([name, entry]) => {
|
|
6891
|
+
const sigBadge = entry.signature ? `\x1B[32m[SIGNED \u2014 HMAC-SHA256]\x1B[0m` : `\x1B[33m[UNSIGNED]\x1B[0m`;
|
|
6892
|
+
console.log(` \x1B[32m${name}\x1B[0m ${sigBadge}`);
|
|
6893
|
+
console.log(` URL: ${entry.url}`);
|
|
6894
|
+
console.log(` Synced at: ${entry.synced_at}`);
|
|
6895
|
+
console.log(` Catalog SHA-256: ${entry.catalog_sha256}`);
|
|
6896
|
+
if (entry.manifest_sha256) {
|
|
6897
|
+
console.log(` Manifest SHA256: ${entry.manifest_sha256}`);
|
|
6898
|
+
}
|
|
6899
|
+
if (entry.signature) {
|
|
6900
|
+
console.log(` Signature: ${entry.signature.slice(0, 24)}...`);
|
|
6901
|
+
console.log(` Sig algorithm: ${entry.signature_alg}`);
|
|
6902
|
+
}
|
|
6903
|
+
console.log("");
|
|
6904
|
+
});
|
|
6905
|
+
console.log("Use \x1B[36mregistry verify <name>\x1B[0m to re-verify cached files against the lockfile.");
|
|
6906
|
+
console.log("Use \x1B[36mregistry keygen --approved\x1B[0m to generate a signing key for HMAC signatures.\n");
|
|
6907
|
+
}
|