@yansirplus/cli 0.5.18 → 0.5.20

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.
@@ -4,6 +4,8 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ import { workspacePackagePaths } from "./lib/workspace-manifest.mjs";
8
+
7
9
  export const installManifestProtocol = "agentos-install-manifest@1";
8
10
  export const localConsumerMarkerName = ".agentos-local.json";
9
11
 
@@ -112,6 +114,57 @@ export const consumerManifestFiles = (consumerRoot) =>
112
114
  (name) => path.join(consumerRoot, name),
113
115
  );
114
116
 
117
+ const packageNameForRoot = (consumerRoot) => {
118
+ const file = path.join(consumerRoot, "package.json");
119
+ if (!fs.existsSync(file)) return undefined;
120
+ const name = readJson(file).name;
121
+ return typeof name === "string" && name.length > 0 ? name : undefined;
122
+ };
123
+
124
+ const consumerWorkspaceLayout = (consumerRoot) => {
125
+ const manifest = path.join(consumerRoot, "pnpm-workspace.yaml");
126
+ const root = {
127
+ kind: "root",
128
+ relativePath: ".",
129
+ consumerRoot,
130
+ packageName: packageNameForRoot(consumerRoot),
131
+ };
132
+ if (!fs.existsSync(manifest)) {
133
+ return { status: "not_workspace", roots: [root] };
134
+ }
135
+ try {
136
+ const roots = [
137
+ root,
138
+ ...workspacePackagePaths(consumerRoot).map((relativePath) => {
139
+ const packageRoot = path.join(consumerRoot, relativePath);
140
+ return {
141
+ kind: "workspace-package",
142
+ relativePath,
143
+ consumerRoot: packageRoot,
144
+ packageName: packageNameForRoot(packageRoot),
145
+ };
146
+ }),
147
+ ];
148
+ return {
149
+ status: "workspace",
150
+ manifestPath: path.relative(consumerRoot, manifest).split(path.sep).join("/"),
151
+ roots,
152
+ };
153
+ } catch (error) {
154
+ return {
155
+ status: "invalid",
156
+ manifestPath: path.relative(consumerRoot, manifest).split(path.sep).join("/"),
157
+ error: error instanceof Error ? error.message : String(error),
158
+ roots: [root],
159
+ };
160
+ }
161
+ };
162
+
163
+ const consumerWorkspaceManifestFiles = (layout) =>
164
+ [...new Set(layout.roots.flatMap((root) => consumerManifestFiles(root.consumerRoot)))].sort(
165
+ (left, right) => left.localeCompare(right),
166
+ );
167
+
115
168
  export const snapshotFiles = (files) =>
116
169
  new Map(files.map((file) => [file, fs.existsSync(file) ? fs.readFileSync(file) : undefined]));
117
170
 
@@ -289,6 +342,9 @@ const fileSpecPath = (spec) => {
289
342
  return spec.slice("file:".length);
290
343
  };
291
344
 
345
+ const optionalFileSpecPath = (spec) =>
346
+ typeof spec === "string" && spec.startsWith("file:") ? spec.slice("file:".length) : undefined;
347
+
292
348
  export const tarballPackageEntries = (manifest) =>
293
349
  Object.entries(manifest.tarballs)
294
350
  .map(([packageName, entry]) => {
@@ -321,6 +377,326 @@ const markerArtifact = (manifestPath, manifest) => ({
321
377
  },
322
378
  });
323
379
 
380
+ const sourcePackageScope = "@agent-os";
381
+
382
+ const releaseNpmScope = (sourceRoot) => {
383
+ if (typeof sourceRoot !== "string") return undefined;
384
+ const manifestPath = path.join(sourceRoot, "package.json");
385
+ if (!fs.existsSync(manifestPath)) return undefined;
386
+ const manifest = readJson(manifestPath);
387
+ return typeof manifest.agentOsRelease?.npmScope === "string"
388
+ ? manifest.agentOsRelease.npmScope
389
+ : undefined;
390
+ };
391
+
392
+ const publicPackageName = (sourceName, npmScope) => {
393
+ if (typeof npmScope !== "string" || !sourceName.startsWith(`${sourcePackageScope}/`)) {
394
+ return sourceName;
395
+ }
396
+ return `${npmScope}/${sourceName.slice(sourcePackageScope.length + 1)}`;
397
+ };
398
+
399
+ const sourceManifestPathForPackage = (packageName, sourceRoot) => {
400
+ if (typeof sourceRoot !== "string") return undefined;
401
+ const surfacePath = path.join(sourceRoot, "docs", "surface.json");
402
+ if (!fs.existsSync(surfacePath)) return undefined;
403
+ const npmScope = releaseNpmScope(sourceRoot);
404
+ const surface = readJson(surfacePath);
405
+ for (const pkg of surface.packages ?? []) {
406
+ if (pkg?.published !== true || typeof pkg.name !== "string" || typeof pkg.path !== "string") {
407
+ continue;
408
+ }
409
+ if (packageName !== pkg.name && packageName !== publicPackageName(pkg.name, npmScope)) {
410
+ continue;
411
+ }
412
+ const manifestPath = path.join(sourceRoot, pkg.path, "package.json");
413
+ return fs.existsSync(manifestPath) ? manifestPath : undefined;
414
+ }
415
+ return undefined;
416
+ };
417
+
418
+ const collectExportTargetStrings = (value, output = []) => {
419
+ if (typeof value === "string") {
420
+ output.push(value);
421
+ return output;
422
+ }
423
+ if (value === null || typeof value !== "object") return output;
424
+ for (const child of Object.values(value)) collectExportTargetStrings(child, output);
425
+ return output;
426
+ };
427
+
428
+ const exportTargetKind = (target) => {
429
+ if (target.startsWith("./src/") && target.endsWith(".ts")) return "source-ts";
430
+ if (target.startsWith("./src/") && target.endsWith(".mjs")) return "source-mjs";
431
+ if (target.startsWith("./dist/") && target.endsWith(".d.ts")) return "dist-dts";
432
+ if (target.startsWith("./dist/") && target.endsWith(".js")) return "dist-js";
433
+ if (target.startsWith("./dist/") && target.endsWith(".mjs")) return "dist-mjs";
434
+ if (target.startsWith("./") && !target.includes("..") && target.endsWith(".json")) {
435
+ return "json-asset";
436
+ }
437
+ if (target.startsWith("./")) return "relative-other";
438
+ return "specifier-other";
439
+ };
440
+
441
+ const exportEntryKind = (targetKinds) => {
442
+ if (targetKinds.length === 0) return "empty";
443
+ if (targetKinds.every((kind) => kind.startsWith("source-"))) return "source-module";
444
+ if (targetKinds.every((kind) => kind.startsWith("dist-"))) return "dist-module";
445
+ if (targetKinds.every((kind) => kind === "json-asset")) return "json-asset";
446
+ return "mixed";
447
+ };
448
+
449
+ const exportSetForManifest = (manifest) => {
450
+ const exportsValue = manifest.exports ?? manifest.main;
451
+ const entries =
452
+ typeof exportsValue === "string"
453
+ ? [[".", exportsValue]]
454
+ : exportsValue !== null && typeof exportsValue === "object"
455
+ ? Object.entries(exportsValue)
456
+ : [];
457
+ return Object.fromEntries(
458
+ entries
459
+ .map(([subpath, value]) => {
460
+ const targets = collectExportTargetStrings(value).sort((left, right) =>
461
+ left.localeCompare(right),
462
+ );
463
+ const targetKinds = [...new Set(targets.map(exportTargetKind))].sort((left, right) =>
464
+ left.localeCompare(right),
465
+ );
466
+ return [
467
+ subpath,
468
+ {
469
+ targetKinds,
470
+ entryKind: exportEntryKind(targetKinds),
471
+ },
472
+ ];
473
+ })
474
+ .sort(([left], [right]) => left.localeCompare(right)),
475
+ );
476
+ };
477
+
478
+ const readPackageJsonProjection = (manifestPath) => {
479
+ if (typeof manifestPath !== "string") return { status: "unavailable" };
480
+ if (!fs.existsSync(manifestPath)) return { status: "missing", manifestPath };
481
+ try {
482
+ const manifest = readJson(manifestPath);
483
+ return {
484
+ status: "available",
485
+ manifestPath,
486
+ packageName: manifest.name,
487
+ exports: exportSetForManifest(manifest),
488
+ };
489
+ } catch (error) {
490
+ return {
491
+ status: "failed",
492
+ manifestPath,
493
+ error: error instanceof Error ? error.message : String(error),
494
+ };
495
+ }
496
+ };
497
+
498
+ const readTarballPackageJsonProjection = (tarball) => {
499
+ if (typeof tarball !== "string" || tarball.length === 0) {
500
+ return { status: "unavailable" };
501
+ }
502
+ if (!fs.existsSync(tarball)) return { status: "missing", tarball };
503
+ const tmp = fs.mkdtempSync(path.join(fs.realpathSync("/tmp"), "agentos-export-tarball-"));
504
+ try {
505
+ const result = spawnSync("tar", ["-xzf", tarball, "-C", tmp, "package/package.json"], {
506
+ encoding: "utf8",
507
+ stdio: ["ignore", "pipe", "pipe"],
508
+ });
509
+ if (result.status !== 0) {
510
+ return {
511
+ status: "failed",
512
+ tarball,
513
+ error: result.stderr.trim() || result.stdout.trim() || `tar exited ${result.status}`,
514
+ };
515
+ }
516
+ const projection = readPackageJsonProjection(path.join(tmp, "package", "package.json"));
517
+ return { ...projection, manifestPath: "package/package.json", tarball };
518
+ } finally {
519
+ fs.rmSync(tmp, { recursive: true, force: true });
520
+ }
521
+ };
522
+
523
+ const sortedExportKeys = (exports) =>
524
+ Object.keys(exports ?? {}).sort((left, right) => left.localeCompare(right));
525
+
526
+ const compareSubpathSets = (leftExports, rightExports) => {
527
+ const left = new Set(sortedExportKeys(leftExports));
528
+ const right = new Set(sortedExportKeys(rightExports));
529
+ return {
530
+ missing: [...left].filter((subpath) => !right.has(subpath)),
531
+ extra: [...right].filter((subpath) => !left.has(subpath)),
532
+ };
533
+ };
534
+
535
+ const expectedPackedKindForSource = (sourceKind) => {
536
+ if (sourceKind === "source-module") return "dist-module";
537
+ return sourceKind;
538
+ };
539
+
540
+ const compareSourcePackedExports = (source, packed) => {
541
+ const subpaths = compareSubpathSets(source.exports, packed.exports);
542
+ const targetKindDrift = [];
543
+ for (const subpath of sortedExportKeys(source.exports)) {
544
+ if (packed.exports?.[subpath] === undefined) continue;
545
+ const expected = expectedPackedKindForSource(source.exports[subpath].entryKind);
546
+ const actual = packed.exports[subpath].entryKind;
547
+ if (actual !== expected) {
548
+ targetKindDrift.push({
549
+ subpath,
550
+ sourceKind: source.exports[subpath].entryKind,
551
+ expectedPackedKind: expected,
552
+ actualPackedKind: actual,
553
+ });
554
+ }
555
+ }
556
+ return {
557
+ status:
558
+ subpaths.missing.length === 0 && subpaths.extra.length === 0 && targetKindDrift.length === 0
559
+ ? "pass"
560
+ : "fail",
561
+ subpaths,
562
+ targetKindDrift,
563
+ };
564
+ };
565
+
566
+ const compareEquivalentExports = (left, right) => {
567
+ const subpaths = compareSubpathSets(left.exports, right.exports);
568
+ const targetKindDrift = [];
569
+ for (const subpath of sortedExportKeys(left.exports)) {
570
+ if (right.exports?.[subpath] === undefined) continue;
571
+ const leftKind = left.exports[subpath].entryKind;
572
+ const rightKind = right.exports[subpath].entryKind;
573
+ if (leftKind !== rightKind) {
574
+ targetKindDrift.push({ subpath, leftKind, rightKind });
575
+ }
576
+ }
577
+ return {
578
+ status:
579
+ subpaths.missing.length === 0 && subpaths.extra.length === 0 && targetKindDrift.length === 0
580
+ ? "pass"
581
+ : "fail",
582
+ subpaths,
583
+ targetKindDrift,
584
+ };
585
+ };
586
+
587
+ const exportFailure = (code, packageName, comparison, detail = {}) => ({
588
+ code,
589
+ packageName,
590
+ comparison,
591
+ ...detail,
592
+ });
593
+
594
+ const exportEquivalenceForPackage = (entry, options = {}) => {
595
+ const source = readPackageJsonProjection(
596
+ sourceManifestPathForPackage(entry.packageName, options.sourceRoot),
597
+ );
598
+ const packed = readTarballPackageJsonProjection(entry.tarball);
599
+ const installed = readPackageJsonProjection(entry.installedManifestPath);
600
+ const failures = [];
601
+ const comparisons = {};
602
+ if (packed.status !== "available") {
603
+ failures.push(
604
+ exportFailure("export_packed_manifest_unavailable", entry.packageName, "packed", {
605
+ status: packed.status,
606
+ error: packed.error,
607
+ }),
608
+ );
609
+ }
610
+ if (entry.installedManifestPath !== undefined && installed.status !== "available") {
611
+ failures.push(
612
+ exportFailure("export_installed_manifest_unavailable", entry.packageName, "installed", {
613
+ status: installed.status,
614
+ error: installed.error,
615
+ }),
616
+ );
617
+ }
618
+ if (source.status === "available" && packed.status === "available") {
619
+ const comparison = compareSourcePackedExports(source, packed);
620
+ comparisons.sourcePacked = comparison;
621
+ if (comparison.subpaths.missing.length > 0 || comparison.subpaths.extra.length > 0) {
622
+ failures.push(
623
+ exportFailure("export_source_packed_subpath_drift", entry.packageName, "source_packed", {
624
+ missingSubpaths: comparison.subpaths.missing,
625
+ extraSubpaths: comparison.subpaths.extra,
626
+ }),
627
+ );
628
+ }
629
+ if (comparison.targetKindDrift.length > 0) {
630
+ failures.push(
631
+ exportFailure(
632
+ "export_source_packed_target_kind_drift",
633
+ entry.packageName,
634
+ "source_packed",
635
+ { targetKindDrift: comparison.targetKindDrift },
636
+ ),
637
+ );
638
+ }
639
+ }
640
+ if (packed.status === "available" && installed.status === "available") {
641
+ const comparison = compareEquivalentExports(packed, installed);
642
+ comparisons.packedInstalled = comparison;
643
+ if (comparison.subpaths.missing.length > 0 || comparison.subpaths.extra.length > 0) {
644
+ failures.push(
645
+ exportFailure(
646
+ "export_packed_installed_subpath_drift",
647
+ entry.packageName,
648
+ "packed_installed",
649
+ {
650
+ missingSubpaths: comparison.subpaths.missing,
651
+ extraSubpaths: comparison.subpaths.extra,
652
+ },
653
+ ),
654
+ );
655
+ }
656
+ if (comparison.targetKindDrift.length > 0) {
657
+ failures.push(
658
+ exportFailure(
659
+ "export_packed_installed_target_kind_drift",
660
+ entry.packageName,
661
+ "packed_installed",
662
+ { targetKindDrift: comparison.targetKindDrift },
663
+ ),
664
+ );
665
+ }
666
+ }
667
+ return {
668
+ packageName: entry.packageName,
669
+ source,
670
+ packed,
671
+ ...(entry.installedManifestPath === undefined ? {} : { installed }),
672
+ comparisons,
673
+ status: failures.length === 0 ? "verified" : "failed",
674
+ failures,
675
+ };
676
+ };
677
+
678
+ export const exportEquivalenceProjection = (entries, options = {}) => {
679
+ const packages = entries
680
+ .map((entry) => exportEquivalenceForPackage(entry, options))
681
+ .sort((left, right) => left.packageName.localeCompare(right.packageName));
682
+ const failures = packages.flatMap((pkg) => pkg.failures);
683
+ return {
684
+ status: packages.length === 0 ? "not_checked" : failures.length === 0 ? "verified" : "failed",
685
+ packagesChecked: packages.length,
686
+ packages,
687
+ failures,
688
+ };
689
+ };
690
+
691
+ export const exportEquivalenceForInstallManifest = (manifest, options = {}) =>
692
+ exportEquivalenceProjection(
693
+ Object.entries(manifest.tarballs ?? {}).map(([packageName, entry]) => ({
694
+ packageName,
695
+ tarball: optionalFileSpecPath(entry?.spec),
696
+ })),
697
+ options,
698
+ );
699
+
324
700
  const packageOverlayRows = (consumerRoot, marker) => {
325
701
  const nodeModules = path.join(consumerRoot, "node_modules");
326
702
  return Object.entries(marker.packages ?? {})
@@ -336,6 +712,7 @@ const packageOverlayRows = (consumerRoot, marker) => {
336
712
  const tarballExists = tarball.length > 0 && fs.existsSync(tarball);
337
713
  const expectedSha = typeof record.sha256 === "string" ? record.sha256 : undefined;
338
714
  const actualSha = tarballExists ? sha256File(tarball) : undefined;
715
+ const requiresSha = marker.artifact?.kind === "install-manifest-overlay";
339
716
  return {
340
717
  packageName,
341
718
  target: record.target,
@@ -343,9 +720,13 @@ const packageOverlayRows = (consumerRoot, marker) => {
343
720
  targetStatus,
344
721
  tarball,
345
722
  tarballStatus: tarballExists
346
- ? expectedSha === undefined || expectedSha === actualSha
347
- ? "verified"
348
- : "sha_mismatch"
723
+ ? expectedSha === undefined
724
+ ? requiresSha
725
+ ? "sha_missing"
726
+ : "verified"
727
+ : expectedSha === actualSha
728
+ ? "verified"
729
+ : "sha_mismatch"
349
730
  : "missing",
350
731
  sha256: expectedSha,
351
732
  };
@@ -457,6 +838,38 @@ const consumerGateIssue = (code, severity, dimension, message, detail = {}) => (
457
838
  const consumerOverlayGate = (status) => {
458
839
  const hardFailures = [];
459
840
  const signals = [];
841
+ if (status.workspaceOverlay?.status === "invalid") {
842
+ hardFailures.push(
843
+ consumerGateIssue(
844
+ "workspace_layout_invalid",
845
+ "hard",
846
+ "workspace_layout",
847
+ "consumer workspace manifest could not be projected",
848
+ {
849
+ manifestPath: status.workspaceOverlay.manifestPath,
850
+ error: status.workspaceOverlay.error,
851
+ },
852
+ ),
853
+ );
854
+ }
855
+ for (const root of status.workspaceOverlay?.roots ?? []) {
856
+ if (root.relativePath === ".") continue;
857
+ if (root.gate?.status !== "pass") {
858
+ hardFailures.push(
859
+ consumerGateIssue(
860
+ "workspace_consumer_root_failed",
861
+ "hard",
862
+ "workspace_resolver",
863
+ `workspace consumer root ${root.relativePath} does not have a passing local overlay`,
864
+ {
865
+ relativePath: root.relativePath,
866
+ packageName: root.packageName,
867
+ gate: root.gate,
868
+ },
869
+ ),
870
+ );
871
+ }
872
+ }
460
873
  if (status.localOverlay.status === "missing") {
461
874
  hardFailures.push(
462
875
  consumerGateIssue(
@@ -468,13 +881,25 @@ const consumerOverlayGate = (status) => {
468
881
  ),
469
882
  );
470
883
  }
471
- if (status.localOverlay.status === "partial") {
884
+ for (const failure of status.packageIntegrity.failures ?? []) {
472
885
  hardFailures.push(
473
886
  consumerGateIssue(
474
- "local_overlay_partial",
887
+ failure.code,
475
888
  "hard",
476
889
  "package_integrity",
477
- "local consumer overlay package installation is partial",
890
+ failure.message ?? `local consumer overlay package integrity failed: ${failure.code}`,
891
+ failure,
892
+ ),
893
+ );
894
+ }
895
+ for (const failure of status.exportEquivalence?.failures ?? []) {
896
+ hardFailures.push(
897
+ consumerGateIssue(
898
+ failure.code,
899
+ "hard",
900
+ "export_equivalence",
901
+ `local consumer overlay export equivalence failed: ${failure.code}`,
902
+ failure,
478
903
  ),
479
904
  );
480
905
  }
@@ -513,41 +938,6 @@ const consumerOverlayGate = (status) => {
513
938
  ),
514
939
  );
515
940
  }
516
- for (const pkg of status.localOverlay.packages ?? []) {
517
- if (pkg.targetStatus === "missing") {
518
- hardFailures.push(
519
- consumerGateIssue(
520
- "local_overlay_package_missing",
521
- "hard",
522
- "package_integrity",
523
- `${pkg.packageName} is missing from the consumer overlay`,
524
- { packageName: pkg.packageName },
525
- ),
526
- );
527
- }
528
- if (pkg.targetStatus === "symlink") {
529
- hardFailures.push(
530
- consumerGateIssue(
531
- "local_overlay_package_symlink",
532
- "hard",
533
- "package_integrity",
534
- `${pkg.packageName} is a symlink, not packed package content`,
535
- { packageName: pkg.packageName },
536
- ),
537
- );
538
- }
539
- if (pkg.tarballStatus !== "verified") {
540
- hardFailures.push(
541
- consumerGateIssue(
542
- "local_overlay_tarball_not_verified",
543
- "hard",
544
- "package_integrity",
545
- `${pkg.packageName} tarball status is ${pkg.tarballStatus}`,
546
- { packageName: pkg.packageName, tarballStatus: pkg.tarballStatus },
547
- ),
548
- );
549
- }
550
- }
551
941
  if (status.npmLatest.status === "not_checked") {
552
942
  signals.push(
553
943
  consumerGateIssue(
@@ -585,7 +975,7 @@ const withConsumerGate = (status) => ({
585
975
  gate: consumerOverlayGate(status),
586
976
  });
587
977
 
588
- export const consumerStatusData = (consumerRoot, options = {}) => {
978
+ const consumerStatusDataForRoot = (consumerRoot, options = {}) => {
589
979
  const metadata = packageMetadata(options.packageRoot);
590
980
  const markerPath = localConsumerMarkerPath(consumerRoot);
591
981
  const currentSource =
@@ -606,6 +996,19 @@ export const consumerStatusData = (consumerRoot, options = {}) => {
606
996
  }
607
997
  const marker = readJson(markerPath);
608
998
  const packages = packageOverlayRows(consumerRoot, marker);
999
+ const exportEquivalence = exportEquivalenceProjection(
1000
+ packages.map((pkg) => ({
1001
+ packageName: pkg.packageName,
1002
+ tarball: pkg.tarball,
1003
+ installedManifestPath: path.join(
1004
+ consumerRoot,
1005
+ "node_modules",
1006
+ ...pkg.packageName.split("/"),
1007
+ "package.json",
1008
+ ),
1009
+ })),
1010
+ { sourceRoot: options.sourceRoot },
1011
+ );
609
1012
  const sourceStatus = overlaySourceStatus(marker, currentSource);
610
1013
  const packageIntegrity = packageIntegrityFor(marker, packages);
611
1014
  const sourceFreshness = sourceFreshnessFor(marker, currentSource);
@@ -623,6 +1026,7 @@ export const consumerStatusData = (consumerRoot, options = {}) => {
623
1026
  packages,
624
1027
  },
625
1028
  packageIntegrity,
1029
+ exportEquivalence,
626
1030
  sourceFreshness,
627
1031
  source: {
628
1032
  ...(currentSource === undefined ? {} : { current: currentSource }),
@@ -643,12 +1047,62 @@ export const consumerStatusData = (consumerRoot, options = {}) => {
643
1047
  });
644
1048
  };
645
1049
 
1050
+ const workspaceRootStatusSummary = (root, status) => ({
1051
+ kind: root.kind,
1052
+ relativePath: root.relativePath,
1053
+ consumerRoot: root.consumerRoot,
1054
+ ...(root.packageName === undefined ? {} : { packageName: root.packageName }),
1055
+ truthMode: status.truthMode,
1056
+ localOverlay: status.localOverlay,
1057
+ packageIntegrity: status.packageIntegrity,
1058
+ exportEquivalence: status.exportEquivalence,
1059
+ sourceFreshness: status.sourceFreshness,
1060
+ packageVersion: status.packageVersion,
1061
+ gate: status.gate,
1062
+ });
1063
+
1064
+ export const consumerStatusData = (consumerRoot, options = {}) => {
1065
+ const status = consumerStatusDataForRoot(consumerRoot, options);
1066
+ if (options.workspace === false) return status;
1067
+ const layout = consumerWorkspaceLayout(consumerRoot);
1068
+ if (layout.status === "not_workspace") return status;
1069
+ const roots =
1070
+ layout.status === "invalid"
1071
+ ? [workspaceRootStatusSummary(layout.roots[0], status)]
1072
+ : layout.roots.map((root) =>
1073
+ workspaceRootStatusSummary(
1074
+ root,
1075
+ root.relativePath === "."
1076
+ ? status
1077
+ : consumerStatusDataForRoot(root.consumerRoot, options),
1078
+ ),
1079
+ );
1080
+ const workspaceStatus =
1081
+ layout.status === "invalid"
1082
+ ? "invalid"
1083
+ : roots.every((root) => root.gate.status === "pass")
1084
+ ? "verified"
1085
+ : "failed";
1086
+ return withConsumerGate({
1087
+ ...status,
1088
+ workspaceOverlay: {
1089
+ status: workspaceStatus,
1090
+ manifestPath: layout.manifestPath,
1091
+ roots,
1092
+ ...(layout.error === undefined ? {} : { error: layout.error }),
1093
+ },
1094
+ });
1095
+ };
1096
+
646
1097
  const printConsumerStatus = (status) => {
647
1098
  console.log(`consumer: ${status.consumerRoot}`);
648
1099
  console.log(`marker: ${status.markerPath}`);
649
1100
  console.log(`truth mode: ${status.truthMode}`);
650
1101
  console.log(`local overlay: ${status.localOverlay.status}`);
651
1102
  console.log(`package integrity: ${status.packageIntegrity.status}`);
1103
+ if (status.exportEquivalence !== undefined) {
1104
+ console.log(`export equivalence: ${status.exportEquivalence.status}`);
1105
+ }
652
1106
  if (status.sourceFreshness !== undefined) {
653
1107
  console.log(`source freshness: ${status.sourceFreshness.status}`);
654
1108
  }
@@ -659,6 +1113,14 @@ const printConsumerStatus = (status) => {
659
1113
  `package version: overlay=${status.packageVersion.overlay ?? "none"} release=${status.packageVersion.release} status=${status.packageVersion.status ?? "none"}`,
660
1114
  );
661
1115
  console.log(`npm latest: ${status.npmLatest.status}`);
1116
+ if (status.workspaceOverlay !== undefined) {
1117
+ console.log(`workspace overlay: ${status.workspaceOverlay.status}`);
1118
+ for (const root of status.workspaceOverlay.roots ?? []) {
1119
+ console.log(
1120
+ `workspace ${root.relativePath}: gate=${root.gate.status} overlay=${root.localOverlay.status}`,
1121
+ );
1122
+ }
1123
+ }
662
1124
  console.log(`gate: ${status.gate.status}`);
663
1125
  for (const pkg of status.localOverlay.packages ?? []) {
664
1126
  console.log(
@@ -695,40 +1157,50 @@ export const installConsumer = async (rawArgs, context = {}) => {
695
1157
  const args = parseArgs(rawArgs);
696
1158
  const consumerRoot = resolveConsumerRoot(positionalArgs(args)[0]);
697
1159
  const manifestPath = await installManifestPathForArgs(args, context);
698
- const snapshot = snapshotFiles(consumerManifestFiles(consumerRoot));
1160
+ const workspaceLayout = consumerWorkspaceLayout(consumerRoot);
1161
+ if (workspaceLayout.status === "invalid") {
1162
+ fail(`${consumerRoot}: ${workspaceLayout.error}`);
1163
+ }
1164
+ const snapshot = snapshotFiles(consumerWorkspaceManifestFiles(workspaceLayout));
699
1165
  const { manifest } = readInstallManifest(manifestPath);
700
1166
  const entries = tarballPackageEntries(manifest);
701
- const nodeModules = nodeModulesRoot(consumerRoot, { install: !boolArg(args, "no-install") });
702
- const packages = {};
703
- for (const entry of entries) {
704
- const target = packageTargetDir(nodeModules, entry.packageName);
705
- unpackTarballInto(entry.tarball, target);
706
- packages[entry.packageName] = {
707
- target: path.relative(consumerRoot, target).split(path.sep).join("/"),
708
- tarball: entry.tarball,
709
- sha256: entry.sha256,
710
- };
711
- }
1167
+ nodeModulesRoot(consumerRoot, { install: !boolArg(args, "no-install") });
712
1168
  const source =
713
1169
  typeof context.sourceRoot === "string"
714
1170
  ? sourceIdentityFor(context.sourceRoot)
715
1171
  : (manifest.source ?? undefined);
716
- writeJson(localConsumerMarkerPath(consumerRoot), {
717
- schemaVersion: 1,
718
- generatedBy: "agentos consumer install",
719
- installedAt: new Date().toISOString(),
720
- consumerRoot,
721
- ...(source === undefined ? {} : { source }),
722
- packageVersion: manifest.version,
723
- artifact: markerArtifact(manifestPath, manifest),
724
- packages,
725
- });
1172
+ const installedAt = new Date().toISOString();
1173
+ for (const root of workspaceLayout.roots) {
1174
+ const nodeModules = path.join(root.consumerRoot, "node_modules");
1175
+ const packages = {};
1176
+ for (const entry of entries) {
1177
+ const target = packageTargetDir(nodeModules, entry.packageName);
1178
+ unpackTarballInto(entry.tarball, target);
1179
+ packages[entry.packageName] = {
1180
+ target: path.relative(root.consumerRoot, target).split(path.sep).join("/"),
1181
+ tarball: entry.tarball,
1182
+ sha256: entry.sha256,
1183
+ };
1184
+ }
1185
+ writeJson(localConsumerMarkerPath(root.consumerRoot), {
1186
+ schemaVersion: 1,
1187
+ generatedBy: "agentos consumer install",
1188
+ installedAt,
1189
+ consumerRoot: root.consumerRoot,
1190
+ ...(source === undefined ? {} : { source }),
1191
+ packageVersion: manifest.version,
1192
+ artifact: markerArtifact(manifestPath, manifest),
1193
+ packages,
1194
+ });
1195
+ }
726
1196
  assertSnapshotUnchanged(snapshot, "agentos consumer install");
727
1197
  const status = consumerStatusData(consumerRoot, { sourceRoot: context.sourceRoot });
728
1198
  if (boolArg(args, "json")) {
729
1199
  console.log(JSON.stringify(status, null, 2));
730
1200
  } else {
731
- console.log(`installed ${entries.length} local agentOS packages into ${consumerRoot}`);
1201
+ console.log(
1202
+ `installed ${entries.length} local agentOS packages into ${workspaceLayout.roots.length} consumer root(s)`,
1203
+ );
732
1204
  console.log(
733
1205
  `wrote ${path.relative(consumerRoot, localConsumerMarkerPath(consumerRoot)).split(path.sep).join("/")}`,
734
1206
  );
@@ -790,7 +1262,14 @@ export const restoreConsumer = (rawArgs) => {
790
1262
  }
791
1263
  fs.rmSync(markerPath, { force: true });
792
1264
  if (!boolArg(args, "no-install")) {
793
- run("npm", ["install"], { cwd: consumerRoot });
1265
+ const installCommand = consumerInstallCommand(consumerRoot);
1266
+ if (installCommand === null) {
1267
+ fail(`${consumerRoot}: no package manager/lockfile was detected for consumer restore`);
1268
+ }
1269
+ run(installCommand.cmd, installCommand.args, {
1270
+ cwd: consumerRoot,
1271
+ env: installCommand.env,
1272
+ });
794
1273
  }
795
1274
  assertSnapshotUnchanged(snapshot, "agentos consumer restore");
796
1275
  const result = { schemaVersion: 1, restoredPackages: packageNames };