@pleri/olam-cli 0.1.147 → 0.1.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/agent-stream/agent-sdk-to-chunks.js +276 -0
  2. package/dist/agent-stream/agent-stream-launch.js +348 -0
  3. package/dist/agent-stream/chunks-subscriber-transport.js +262 -0
  4. package/dist/agent-stream/codex-runner.js +188 -0
  5. package/dist/agent-stream/driver-runner.js +347 -0
  6. package/dist/agent-stream/operator-subscription.js +179 -0
  7. package/dist/commands/auth.d.ts.map +1 -1
  8. package/dist/commands/auth.js +26 -1
  9. package/dist/commands/auth.js.map +1 -1
  10. package/dist/commands/create.d.ts.map +1 -1
  11. package/dist/commands/create.js +39 -0
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +54 -3
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +348 -6
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/init.d.ts +46 -0
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +90 -0
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/kg-build.d.ts +23 -0
  22. package/dist/commands/kg-build.d.ts.map +1 -1
  23. package/dist/commands/kg-build.js +104 -2
  24. package/dist/commands/kg-build.js.map +1 -1
  25. package/dist/commands/restart.d.ts +18 -0
  26. package/dist/commands/restart.d.ts.map +1 -0
  27. package/dist/commands/restart.js +113 -0
  28. package/dist/commands/restart.js.map +1 -0
  29. package/dist/commands/services.d.ts +41 -3
  30. package/dist/commands/services.d.ts.map +1 -1
  31. package/dist/commands/services.js +221 -13
  32. package/dist/commands/services.js.map +1 -1
  33. package/dist/commands/setup-linux-gate.d.ts +26 -0
  34. package/dist/commands/setup-linux-gate.d.ts.map +1 -0
  35. package/dist/commands/setup-linux-gate.js +42 -0
  36. package/dist/commands/setup-linux-gate.js.map +1 -0
  37. package/dist/commands/setup-metrics.d.ts +26 -0
  38. package/dist/commands/setup-metrics.d.ts.map +1 -0
  39. package/dist/commands/setup-metrics.js +57 -0
  40. package/dist/commands/setup-metrics.js.map +1 -0
  41. package/dist/commands/setup-phase-5a-skill-source.d.ts +68 -0
  42. package/dist/commands/setup-phase-5a-skill-source.d.ts.map +1 -0
  43. package/dist/commands/setup-phase-5a-skill-source.js +196 -0
  44. package/dist/commands/setup-phase-5a-skill-source.js.map +1 -0
  45. package/dist/commands/setup-phase-5b-project-sweep.d.ts +38 -0
  46. package/dist/commands/setup-phase-5b-project-sweep.d.ts.map +1 -0
  47. package/dist/commands/setup-phase-5b-project-sweep.js +176 -0
  48. package/dist/commands/setup-phase-5b-project-sweep.js.map +1 -0
  49. package/dist/commands/setup.d.ts +19 -0
  50. package/dist/commands/setup.d.ts.map +1 -1
  51. package/dist/commands/setup.js +22 -0
  52. package/dist/commands/setup.js.map +1 -1
  53. package/dist/commands/skills-10x.d.ts +23 -0
  54. package/dist/commands/skills-10x.d.ts.map +1 -0
  55. package/dist/commands/skills-10x.js +308 -0
  56. package/dist/commands/skills-10x.js.map +1 -0
  57. package/dist/commands/substrate-audit-log.d.ts +2 -0
  58. package/dist/commands/substrate-audit-log.d.ts.map +1 -1
  59. package/dist/commands/substrate-audit-log.js +13 -0
  60. package/dist/commands/substrate-audit-log.js.map +1 -1
  61. package/dist/image-digests.json +7 -7
  62. package/dist/index.js +18102 -15234
  63. package/dist/index.js.map +1 -1
  64. package/dist/lib/auth-refresh-kubernetes.d.ts +62 -0
  65. package/dist/lib/auth-refresh-kubernetes.d.ts.map +1 -0
  66. package/dist/lib/auth-refresh-kubernetes.js +127 -0
  67. package/dist/lib/auth-refresh-kubernetes.js.map +1 -0
  68. package/dist/lib/build-if-stale.d.ts +33 -0
  69. package/dist/lib/build-if-stale.d.ts.map +1 -0
  70. package/dist/lib/build-if-stale.js +156 -0
  71. package/dist/lib/build-if-stale.js.map +1 -0
  72. package/dist/lib/bundle-freshness.d.ts +57 -0
  73. package/dist/lib/bundle-freshness.d.ts.map +1 -0
  74. package/dist/lib/bundle-freshness.js +223 -0
  75. package/dist/lib/bundle-freshness.js.map +1 -0
  76. package/dist/lib/bundle-source.d.ts +52 -0
  77. package/dist/lib/bundle-source.d.ts.map +1 -0
  78. package/dist/lib/bundle-source.js +83 -0
  79. package/dist/lib/bundle-source.js.map +1 -0
  80. package/dist/lib/kubectl-wrap.d.ts +6 -0
  81. package/dist/lib/kubectl-wrap.d.ts.map +1 -1
  82. package/dist/lib/kubectl-wrap.js +6 -1
  83. package/dist/lib/kubectl-wrap.js.map +1 -1
  84. package/dist/lib/manifest-refresh.d.ts +42 -1
  85. package/dist/lib/manifest-refresh.d.ts.map +1 -1
  86. package/dist/lib/manifest-refresh.js +83 -7
  87. package/dist/lib/manifest-refresh.js.map +1 -1
  88. package/dist/lib/peripheral-registry.d.ts +36 -0
  89. package/dist/lib/peripheral-registry.d.ts.map +1 -0
  90. package/dist/lib/peripheral-registry.js +55 -0
  91. package/dist/lib/peripheral-registry.js.map +1 -0
  92. package/dist/lib/port-forward.d.ts +67 -0
  93. package/dist/lib/port-forward.d.ts.map +1 -1
  94. package/dist/lib/port-forward.js +153 -0
  95. package/dist/lib/port-forward.js.map +1 -1
  96. package/dist/lib/upgrade-kubernetes.d.ts +52 -12
  97. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
  98. package/dist/lib/upgrade-kubernetes.js +390 -22
  99. package/dist/lib/upgrade-kubernetes.js.map +1 -1
  100. package/dist/mcp-server.js +84 -58
  101. package/host-cp/compose.yaml +6 -0
  102. package/host-cp/k8s/manifests/30-configmap.yaml +6 -0
  103. package/host-cp/k8s/manifests/50-deployment.yaml +46 -9
  104. package/host-cp/k8s/manifests/auth-service/10-serviceaccount.yaml +8 -0
  105. package/host-cp/k8s/manifests/auth-service/20-rbac.yaml +34 -0
  106. package/host-cp/k8s/manifests/auth-service/30-configmap.yaml +24 -0
  107. package/host-cp/k8s/manifests/auth-service/45-pvc.yaml +25 -0
  108. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +117 -0
  109. package/host-cp/k8s/manifests/auth-service/60-service.yaml +21 -0
  110. package/host-cp/k8s/manifests/kg-service/10-serviceaccount.yaml +8 -0
  111. package/host-cp/k8s/manifests/kg-service/20-rbac.yaml +34 -0
  112. package/host-cp/k8s/manifests/kg-service/30-configmap.yaml +18 -0
  113. package/host-cp/k8s/manifests/kg-service/45-pvc.yaml +25 -0
  114. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +108 -0
  115. package/host-cp/k8s/manifests/kg-service/60-service.yaml +21 -0
  116. package/host-cp/k8s/manifests/mcp-auth-service/10-serviceaccount.yaml +8 -0
  117. package/host-cp/k8s/manifests/mcp-auth-service/20-rbac.yaml +34 -0
  118. package/host-cp/k8s/manifests/mcp-auth-service/30-configmap.yaml +18 -0
  119. package/host-cp/k8s/manifests/mcp-auth-service/45-pvc.yaml +25 -0
  120. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +117 -0
  121. package/host-cp/k8s/manifests/mcp-auth-service/60-service.yaml +21 -0
  122. package/host-cp/k8s/manifests/memory-service/10-serviceaccount.yaml +8 -0
  123. package/host-cp/k8s/manifests/memory-service/20-rbac.yaml +34 -0
  124. package/host-cp/k8s/manifests/memory-service/30-configmap.yaml +20 -0
  125. package/host-cp/k8s/manifests/memory-service/45-pvc.yaml +25 -0
  126. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +121 -0
  127. package/host-cp/k8s/manifests/memory-service/60-service.yaml +21 -0
  128. package/host-cp/k8s/templates/auth-service-secret-template.yaml +28 -0
  129. package/host-cp/k8s/templates/kg-service-secret-template.yaml +28 -0
  130. package/host-cp/k8s/templates/mcp-auth-service-secret-template.yaml +28 -0
  131. package/host-cp/k8s/templates/memory-service-secret-template.yaml +29 -0
  132. package/host-cp/src/agent-runtime-trigger.mjs +7 -5
  133. package/host-cp/src/plan-chat-secret.mjs +13 -2
  134. package/host-cp/src/plan-chat-service.mjs +116 -15
  135. package/host-cp/src/server.mjs +23 -11
  136. package/host-cp/src/upgrade-spawner.mjs +10 -5
  137. package/package.json +4 -2
@@ -4,43 +4,61 @@
4
4
  * Phase 1b C2 of olam-host-suite-phase-1b-k3s-beta-flavour (plan
5
5
  * ~/.claude/plans/olam-host-suite-phase-1b-k3s-beta-flavour.md).
6
6
  *
7
+ * Phase 2 Phase C extensions:
8
+ * C1 — step 2.5: in-memory ConfigMap substitution for inter-peripheral K8s DNS URLs (D4)
9
+ * C2 — step 2.6: per-peripheral Secret pre-check (iterates PERIPHERALS; D12 pattern)
10
+ * C3 — step 2.7: CoreDNS warm-up wait + extend steps 3/4/5 to iterate peripherals
11
+ *
12
+ * Phase 2 Phase D extensions:
13
+ * D5 — OLAM_PHASE_2_BETA guard removed; all peripherals deploy unconditionally (Phase 2 GA)
14
+ *
7
15
  * Decisions consumed:
16
+ * D4 — K8s DNS URL form for inter-peripheral service URLs
8
17
  * D10 — context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality check
9
18
  * D12 — Secret pre-check (base64-decode + key-name + placeholder exact match)
10
19
  * D14 — --force-refresh-manifests flag + --accept-security-regression guard
11
20
  * D15 — kubectl rollout status --timeout=90s; state snapshot on failure
12
21
  * D17 — port-forward spawn via flock (spawnPortForward from port-forward.ts)
13
22
  * D22 — probeKubernetesApiReachable pre-flight via kubectl-wrap (5s timeout)
23
+ * D27 — audit log entry (phase2.flag_removed) emitted per upgrade run
14
24
  *
15
- * Step order:
16
- * 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
17
- * 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
18
- * 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
19
- * 3 kubectl apply --context <pinned> -f ~/.olam/k8s/manifests/
20
- * 4 D15 kubectl rollout status --context <pinned> --timeout=90s (95s wrap)
21
- * 5 D17 port-forward spawn via flock
22
- * 6 verify /health returns X-Olam-Engine: kubernetes
23
- * 7 emit upgrade.complete instrumentation event
24
- * 8 success message
25
+ * Step order (Phase D — kubernetes substrate, Phase 2 GA):
26
+ * 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
27
+ * 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
28
+ * 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
29
+ * 2.5 C1 in-memory ConfigMap substitution — K8s DNS URLs for inter-peripheral URLs
30
+ * 2.6 C2 per-peripheral Secret pre-check (iterates PERIPHERALS; unconditional D5)
31
+ * 2.7 C3 CoreDNS warm-up wait (kubectl wait --for=condition=Available deployment/coredns -n kube-system)
32
+ * 3 kubectl apply host-cp manifests + all 4 peripheral manifest dirs (alphabetical)
33
+ * 4 D15 kubectl rollout status (all 5 deployments in parallel)
34
+ * 5 D17 port-forward spawn for host-cp + spawnAllPeripheralPortForwards in parallel
35
+ * 6 verify /health returns X-Olam-Engine: kubernetes
36
+ * 7 emit upgrade.complete instrumentation event
37
+ * 8 success message
25
38
  *
26
39
  * Manifests NOT auto-rolled-back on failure (D15 spec). Operator must
27
40
  * manually intervene; state snapshot is printed on failure.
28
41
  */
42
+ import * as fs from 'node:fs';
29
43
  import { kubectlWrap } from './kubectl-wrap.js';
30
- import { spawnPortForward, probePortForwardLiveness, type PortForwardDeps } from './port-forward.js';
44
+ import { spawnPortForward, spawnAllPeripheralPortForwards, probePortForwardLiveness, type PortForwardDeps, type PeripheralPortForwardDeps } from './port-forward.js';
31
45
  import { emitUpgradeComplete, type EmitOpts } from './instrumentation.js';
32
- import { runManifestRefresh } from './manifest-refresh.js';
46
+ import { runManifestRefresh, seedManifestsFromBundle, type SeedManifestsDeps } from './manifest-refresh.js';
33
47
  export declare const OLAM_K8S_MANIFESTS_DIR: string;
34
48
  export declare const K8S_NAMESPACE = "olam";
35
49
  export declare const HOST_CP_SECRET_NAME = "olam-host-cp-secret";
36
50
  export declare const HOST_CP_DEPLOYMENT_NAME = "olam-host-cp";
37
51
  export declare const PORT_FORWARD_TARGET = "service/olam-host-cp";
38
52
  export declare const HOST_CP_HEALTH_URL = "http://127.0.0.1:19000/health";
53
+ /** Audit log for substrate upgrade events (D18). */
54
+ export declare const SUBSTRATE_AUDIT_LOG: string;
39
55
  export interface UpgradeKubernetesDeps {
40
56
  /** Override kubectl-wrap for tests. */
41
57
  readonly kubectlWrapImpl?: typeof kubectlWrap;
42
58
  /** Override spawnPortForward for tests. */
43
59
  readonly spawnPortForwardImpl?: typeof spawnPortForward;
60
+ /** Override spawnAllPeripheralPortForwards for tests. */
61
+ readonly spawnAllPeripheralPortForwardsImpl?: typeof spawnAllPeripheralPortForwards;
44
62
  /** Override probePortForwardLiveness for tests. */
45
63
  readonly probePortForwardLivenessImpl?: typeof probePortForwardLiveness;
46
64
  /** Override fetch for /health verification in tests. */
@@ -49,16 +67,38 @@ export interface UpgradeKubernetesDeps {
49
67
  readonly emitImpl?: typeof emitUpgradeComplete;
50
68
  /** Override manifest refresh for tests. */
51
69
  readonly manifestRefreshImpl?: typeof runManifestRefresh;
70
+ /** Override manifests seeder for tests. */
71
+ readonly seedManifestsImpl?: typeof seedManifestsFromBundle;
72
+ /** Override seed manifests deps for tests (injectable to seedManifestsFromBundle). */
73
+ readonly seedManifestsDeps?: SeedManifestsDeps;
52
74
  /** Override manifests dir path for tests. */
53
75
  readonly manifestsDir?: string;
54
76
  /** Override PortForwardDeps for tests (passed into spawnPortForward). */
55
77
  readonly portForwardDeps?: PortForwardDeps;
78
+ /** Override PeripheralPortForwardDeps for tests (passed into spawnAllPeripheralPortForwards). */
79
+ readonly peripheralPortForwardDeps?: PeripheralPortForwardDeps;
56
80
  /** Override emit opts for tests. */
57
81
  readonly emitOpts?: EmitOpts;
58
82
  /** Override stdout for tests. */
59
83
  readonly stdout?: NodeJS.WritableStream;
60
84
  /** Override stderr for tests. */
61
85
  readonly stderr?: NodeJS.WritableStream;
86
+ /** Override fs.readFileSync for tests (step 2.5 ConfigMap read). */
87
+ readonly readFileSyncImpl?: typeof fs.readFileSync;
88
+ /** Override Date.now for tests (audit log timestamp). */
89
+ readonly nowImpl?: () => number;
90
+ /**
91
+ * D6: override kubectl config view call for managed-k8s detection.
92
+ * Returns the cluster server URL string, or null when unavailable.
93
+ * Tests inject a mock to avoid real kubectl invocations.
94
+ */
95
+ readonly getClusterServerUrlImpl?: () => Promise<string | null>;
96
+ /**
97
+ * D7: override docker socket accessibility check for inline preflight.
98
+ * Returns true when /var/run/docker.sock is accessible inside the host-cp pod.
99
+ * Tests inject a mock to avoid real kubectl invocations.
100
+ */
101
+ readonly checkDockerSocketImpl?: (context: string) => Promise<boolean>;
62
102
  }
63
103
  export interface UpgradeKubernetesOpts {
64
104
  readonly forceRefreshManifests?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade-kubernetes.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade-kubernetes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAOH,OAAO,EAAE,WAAW,EAAwB,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAA4B,MAAM,uBAAuB,CAAC;AAGrF,eAAO,MAAM,sBAAsB,QAA2C,CAAC;AAC/E,eAAO,MAAM,aAAa,SAAS,CAAC;AACpC,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AACzD,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAclE,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,WAAW,CAAC;IAC9C,2CAA2C;IAC3C,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACxD,mDAAmD;IACnD,QAAQ,CAAC,4BAA4B,CAAC,EAAE,OAAO,wBAAwB,CAAC;IACxE,wDAAwD;IACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5E,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,mBAAmB,CAAC;IAC/C,2CAA2C;IAC3C,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACzD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,iCAAiC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACxC,iCAAiC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAqJD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,GAAE,qBAA0B,EAChC,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,uBAAuB,CAAC,CAgKlC"}
1
+ {"version":3,"file":"upgrade-kubernetes.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade-kubernetes.ts"],"names":[],"mappings":"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"}
@@ -4,53 +4,177 @@
4
4
  * Phase 1b C2 of olam-host-suite-phase-1b-k3s-beta-flavour (plan
5
5
  * ~/.claude/plans/olam-host-suite-phase-1b-k3s-beta-flavour.md).
6
6
  *
7
+ * Phase 2 Phase C extensions:
8
+ * C1 — step 2.5: in-memory ConfigMap substitution for inter-peripheral K8s DNS URLs (D4)
9
+ * C2 — step 2.6: per-peripheral Secret pre-check (iterates PERIPHERALS; D12 pattern)
10
+ * C3 — step 2.7: CoreDNS warm-up wait + extend steps 3/4/5 to iterate peripherals
11
+ *
12
+ * Phase 2 Phase D extensions:
13
+ * D5 — OLAM_PHASE_2_BETA guard removed; all peripherals deploy unconditionally (Phase 2 GA)
14
+ *
7
15
  * Decisions consumed:
16
+ * D4 — K8s DNS URL form for inter-peripheral service URLs
8
17
  * D10 — context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality check
9
18
  * D12 — Secret pre-check (base64-decode + key-name + placeholder exact match)
10
19
  * D14 — --force-refresh-manifests flag + --accept-security-regression guard
11
20
  * D15 — kubectl rollout status --timeout=90s; state snapshot on failure
12
21
  * D17 — port-forward spawn via flock (spawnPortForward from port-forward.ts)
13
22
  * D22 — probeKubernetesApiReachable pre-flight via kubectl-wrap (5s timeout)
23
+ * D27 — audit log entry (phase2.flag_removed) emitted per upgrade run
14
24
  *
15
- * Step order:
16
- * 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
17
- * 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
18
- * 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
19
- * 3 kubectl apply --context <pinned> -f ~/.olam/k8s/manifests/
20
- * 4 D15 kubectl rollout status --context <pinned> --timeout=90s (95s wrap)
21
- * 5 D17 port-forward spawn via flock
22
- * 6 verify /health returns X-Olam-Engine: kubernetes
23
- * 7 emit upgrade.complete instrumentation event
24
- * 8 success message
25
+ * Step order (Phase D — kubernetes substrate, Phase 2 GA):
26
+ * 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
27
+ * 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
28
+ * 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
29
+ * 2.5 C1 in-memory ConfigMap substitution — K8s DNS URLs for inter-peripheral URLs
30
+ * 2.6 C2 per-peripheral Secret pre-check (iterates PERIPHERALS; unconditional D5)
31
+ * 2.7 C3 CoreDNS warm-up wait (kubectl wait --for=condition=Available deployment/coredns -n kube-system)
32
+ * 3 kubectl apply host-cp manifests + all 4 peripheral manifest dirs (alphabetical)
33
+ * 4 D15 kubectl rollout status (all 5 deployments in parallel)
34
+ * 5 D17 port-forward spawn for host-cp + spawnAllPeripheralPortForwards in parallel
35
+ * 6 verify /health returns X-Olam-Engine: kubernetes
36
+ * 7 emit upgrade.complete instrumentation event
37
+ * 8 success message
25
38
  *
26
39
  * Manifests NOT auto-rolled-back on failure (D15 spec). Operator must
27
40
  * manually intervene; state snapshot is printed on failure.
28
41
  */
42
+ import * as fs from 'node:fs';
29
43
  import * as os from 'node:os';
30
44
  import * as path from 'node:path';
45
+ import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
31
46
  import ora from 'ora';
32
47
  import pc from 'picocolors';
33
48
  import { printError, printSuccess, printInfo, printWarning } from '../output.js';
34
49
  import { kubectlWrap } from './kubectl-wrap.js';
35
- import { spawnPortForward, probePortForwardLiveness } from './port-forward.js';
50
+ import { spawnPortForward, spawnAllPeripheralPortForwards, probePortForwardLiveness } from './port-forward.js';
36
51
  import { emitUpgradeComplete } from './instrumentation.js';
37
- import { runManifestRefresh } from './manifest-refresh.js';
38
- import { OLAM_HOME } from './config.js';
52
+ import { runManifestRefresh, seedManifestsFromBundle } from './manifest-refresh.js';
53
+ import { OLAM_HOME, OLAM_STATE_DIR } from './config.js';
54
+ import { PERIPHERALS } from './peripheral-registry.js';
39
55
  export const OLAM_K8S_MANIFESTS_DIR = path.join(OLAM_HOME, 'k8s', 'manifests');
40
56
  export const K8S_NAMESPACE = 'olam';
41
57
  export const HOST_CP_SECRET_NAME = 'olam-host-cp-secret';
42
58
  export const HOST_CP_DEPLOYMENT_NAME = 'olam-host-cp';
43
59
  export const PORT_FORWARD_TARGET = 'service/olam-host-cp';
44
60
  export const HOST_CP_HEALTH_URL = 'http://127.0.0.1:19000/health';
61
+ /** Audit log for substrate upgrade events (D18). */
62
+ export const SUBSTRATE_AUDIT_LOG = path.join(OLAM_STATE_DIR, 'substrate-audit.jsonl');
45
63
  /** Placeholder values that indicate the Secret has not been configured (D12). */
46
64
  const PLACEHOLDER_VALUES = new Set(['OLAM_AUTH_SECRET', 'GH_TOKEN']);
47
65
  /** Required keys in the olam-host-cp-secret (D12). */
48
66
  const REQUIRED_SECRET_KEYS = ['OLAM_AUTH_SECRET', 'GH_TOKEN'];
67
+ /**
68
+ * Peripheral secret metadata: name and required keys with their placeholder patterns.
69
+ * Placeholder pattern: starts with REPLACE_ME_ (as per the template files in
70
+ * packages/host-cp/k8s/templates/).
71
+ */
72
+ const PERIPHERAL_SECRETS = [
73
+ { name: 'auth-service', secretName: 'olam-auth-service-secret', keys: ['OLAM_AUTH_DB_SECRET'] },
74
+ { name: 'mcp-auth-service', secretName: 'olam-mcp-auth-service-secret', keys: ['OLAM_MCP_AUTH_JWT_SECRET'] },
75
+ { name: 'kg-service', secretName: 'olam-kg-service-secret', keys: ['OLAM_KG_BEARER_TOKEN'] },
76
+ { name: 'memory-service', secretName: 'olam-memory-service-secret', keys: ['OLAM_MEMORY_BEARER_SECRET'] },
77
+ ];
78
+ /** K8s cluster DNS base domain for the olam namespace. */
79
+ const K8S_DNS_SUFFIX = 'olam.svc.cluster.local';
80
+ /**
81
+ * Build the K8s in-cluster DNS URL for a peripheral service.
82
+ * Form: http://<k8sServiceName>.olam.svc.cluster.local:<port>
83
+ */
84
+ function buildK8sDnsUrl(k8sServiceName, port) {
85
+ return `http://${k8sServiceName}.${K8S_DNS_SUFFIX}:${port}`;
86
+ }
87
+ /**
88
+ * Append a JSONL audit entry to the substrate audit log (D18).
89
+ * Best-effort: errors are logged to stderr but do not abort the upgrade.
90
+ */
91
+ function appendSubstrateAuditEntry(entry, stderr) {
92
+ try {
93
+ const line = JSON.stringify(entry) + '\n';
94
+ const dir = path.dirname(SUBSTRATE_AUDIT_LOG);
95
+ if (!fs.existsSync(dir))
96
+ fs.mkdirSync(dir, { recursive: true });
97
+ fs.writeFileSync(SUBSTRATE_AUDIT_LOG, line, { encoding: 'utf8', flag: 'a', mode: 0o600 });
98
+ }
99
+ catch (err) {
100
+ stderr.write(`${pc.yellow('[warn]')} could not write substrate audit log: ${err instanceof Error ? err.message : String(err)}\n`);
101
+ }
102
+ }
49
103
  /** D10 — allowed kubectl context patterns. Currently: anything non-empty.
50
104
  * The allowlist is enforced by OLAM_K8S_CONTEXT_ACK strict-equality. */
51
105
  function isContextAllowed(context) {
52
106
  return typeof context === 'string' && context.length > 0;
53
107
  }
108
+ /**
109
+ * D6 — Managed-k8s cluster server URL patterns (Decision #18 active retraction).
110
+ *
111
+ * Matches the cluster server URL (from `kubectl config view --minify`).
112
+ * These patterns indicate managed Kubernetes providers where hostPath
113
+ * /var/run/docker.sock does not reach the operator's docker daemon.
114
+ * Operators on managed k8s must use a local k3d/k3s cluster instead.
115
+ *
116
+ * Pattern sources:
117
+ * - EKS: *.amazonaws.com — AWS API server endpoint
118
+ * - GKE: *.googleapis.com — Google API server; also *.gke.io
119
+ * - AKS: *.azmk8s.io — Azure managed k8s endpoint
120
+ * - DO: *.do-user-*.k8s.ondigitalocean.com — DigitalOcean
121
+ * - Civo: *.civo.com — Civo cloud
122
+ */
123
+ const MANAGED_K8S_URL_PATTERNS = [
124
+ { pattern: /\.amazonaws\.com(:\d+)?$/, provider: 'EKS (Amazon)' },
125
+ { pattern: /\.googleapis\.com(:\d+)?$/, provider: 'GKE (Google)' },
126
+ { pattern: /\.gke\.io(:\d+)?$/, provider: 'GKE (Google)' },
127
+ { pattern: /\.azmk8s\.io(:\d+)?$/, provider: 'AKS (Azure)' },
128
+ { pattern: /\.k8s\.ondigitalocean\.com(:\d+)?$/, provider: 'DOKS (DigitalOcean)' },
129
+ { pattern: /\.civo\.com(:\d+)?$/, provider: 'Civo' },
130
+ ];
131
+ /**
132
+ * Detect whether the active kubectl context is a managed-k8s provider.
133
+ * Returns the detected provider name if managed, null if local/unknown.
134
+ * Fail-open: unrecognized cluster URLs return null (no false positives).
135
+ */
136
+ async function detectManagedK8sProvider(deps) {
137
+ if (deps.getClusterServerUrlImpl) {
138
+ const url = await deps.getClusterServerUrlImpl();
139
+ if (!url)
140
+ return null;
141
+ for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
142
+ if (pattern.test(url))
143
+ return provider;
144
+ }
145
+ return null;
146
+ }
147
+ // Default: run kubectl config view --minify to get the server URL.
148
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
149
+ const result = await wrap(['config', 'view', '--minify', '-o', 'jsonpath={.clusters[0].cluster.server}'], { timeout: 5_000 });
150
+ if (!result.ok || !result.stdout.trim())
151
+ return null;
152
+ const url = result.stdout.trim();
153
+ for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
154
+ if (pattern.test(url))
155
+ return provider;
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * D7 — Check docker socket accessibility inside the host-cp pod.
161
+ * Uses `kubectl exec deploy/olam-host-cp -n olam -- test -S /var/run/docker.sock`.
162
+ * Returns true when socket is accessible; false when absent or unreachable.
163
+ */
164
+ async function checkDockerSocketAccessible(context, deps) {
165
+ if (deps.checkDockerSocketImpl) {
166
+ return deps.checkDockerSocketImpl(context);
167
+ }
168
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
169
+ const result = await wrap([
170
+ '--context', context,
171
+ 'exec', 'deploy/olam-host-cp',
172
+ '-n', 'olam',
173
+ '--',
174
+ 'test', '-S', '/var/run/docker.sock',
175
+ ], { timeout: 10_000 });
176
+ return result.ok;
177
+ }
54
178
  /**
55
179
  * Step 0 — D22: probe Kubernetes API reachability (5s timeout).
56
180
  * Uses kubectl cluster-info with a tight timeout to detect unreachable API server.
@@ -135,6 +259,122 @@ async function waitForRollout(context, deps, stderr) {
135
259
  stderr.write(`\n${pc.yellow('note:')} Manifests are NOT auto-rolled-back. Inspect the state above and remediate manually.\n`);
136
260
  return false;
137
261
  }
262
+ /**
263
+ * Step 2.5 — C1: in-memory ConfigMap substitution for inter-peripheral K8s DNS URLs (D4).
264
+ *
265
+ * Reads the bundled host-cp ConfigMap YAML from manifestsDir/30-configmap.yaml,
266
+ * patches the inter-peripheral URL values to K8s cluster-DNS form, then applies
267
+ * the patched manifest via `kubectl apply -f -` on stdin.
268
+ *
269
+ * Does NOT rewrite any file on disk.
270
+ * Only runs on kubernetes substrate (called after substrate check).
271
+ *
272
+ * Returns null on success; error message string on failure.
273
+ */
274
+ async function applyConfigMapSubstitution(context, manifestsDir, deps) {
275
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
276
+ const readFileSync = deps.readFileSyncImpl ?? fs.readFileSync;
277
+ const configMapPath = path.join(manifestsDir, '30-configmap.yaml');
278
+ let rawYaml;
279
+ try {
280
+ rawYaml = readFileSync(configMapPath, 'utf8');
281
+ }
282
+ catch (err) {
283
+ return `Failed to read ConfigMap at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
284
+ }
285
+ // Parse → patch data values → re-serialize.
286
+ let parsed;
287
+ try {
288
+ parsed = yamlParse(rawYaml);
289
+ }
290
+ catch (err) {
291
+ return `Failed to parse ConfigMap YAML at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
292
+ }
293
+ const data = (parsed['data'] ?? {});
294
+ // Substitute each peripheral's ConfigMap key to K8s DNS form.
295
+ for (const peripheral of PERIPHERALS) {
296
+ data[peripheral.configMapKeyInHostCp] = buildK8sDnsUrl(peripheral.k8sServiceName, peripheral.port);
297
+ }
298
+ parsed['data'] = data;
299
+ const patchedYaml = yamlStringify(parsed);
300
+ // Apply via stdin pipe (D20 compliance: no value in argv).
301
+ const result = await wrap(['--context', context, 'apply', '-f', '-'], { timeout: 30_000, stdin: patchedYaml });
302
+ if (!result.ok) {
303
+ return `kubectl apply (ConfigMap substitution) failed: ${result.stderr.split('\n')[0] ?? ''}`;
304
+ }
305
+ return null;
306
+ }
307
+ /**
308
+ * Step 2.6 — C2: per-peripheral Secret pre-check.
309
+ *
310
+ * Iterates PERIPHERAL_SECRETS; for each, fetches the Secret via kubectl,
311
+ * base64-decodes each value, and refuses if:
312
+ * (a) Secret does not exist → names the peripheral + Secret resource.
313
+ * (b) Any key is missing → names the peripheral + key.
314
+ * (c) Any value starts with REPLACE_ME_ (placeholder not configured) → names key.
315
+ *
316
+ * Mirrors Phase 1b's D12 host-cp Secret pre-check pattern, generalised to N peripherals.
317
+ *
318
+ * Returns null on success; error message string on first failure.
319
+ */
320
+ async function checkPeripheralSecrets(context, deps) {
321
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
322
+ for (const { name, secretName, keys } of PERIPHERAL_SECRETS) {
323
+ const result = await wrap([
324
+ '--context', context,
325
+ 'get', 'secret', secretName,
326
+ '-n', K8S_NAMESPACE,
327
+ '-o', 'json',
328
+ ], { timeout: 15_000 });
329
+ if (!result.ok) {
330
+ return (`Peripheral Secret "${secretName}" (${name}) not found in namespace "${K8S_NAMESPACE}".\n` +
331
+ ` Create it first: kubectl --context ${context} apply -f <your-${name}-secret.yaml>\n` +
332
+ ` ${result.stderr.split('\n')[0] ?? ''}`);
333
+ }
334
+ let secretJson;
335
+ try {
336
+ secretJson = JSON.parse(result.stdout);
337
+ }
338
+ catch {
339
+ return `Failed to parse Secret JSON for "${secretName}" (${name}): ${result.stdout.slice(0, 200)}`;
340
+ }
341
+ const data = secretJson.data ?? {};
342
+ for (const key of keys) {
343
+ if (!(key in data)) {
344
+ return (`Peripheral Secret "${secretName}" (${name}) is missing required key "${key}".\n` +
345
+ ` Keys found: ${Object.keys(data).join(', ') || '(none)'}\n` +
346
+ ` Add the key and re-run.`);
347
+ }
348
+ const b64 = data[key] ?? '';
349
+ const decoded = Buffer.from(b64, 'base64').toString('utf8');
350
+ if (decoded.startsWith('REPLACE_ME_')) {
351
+ return (`Peripheral Secret "${secretName}" (${name}) key "${key}" still holds its placeholder value.\n` +
352
+ ` Replace the placeholder with a real value, then re-run.`);
353
+ }
354
+ }
355
+ }
356
+ return null; // all peripheral secrets verified
357
+ }
358
+ /**
359
+ * Step 2.7 — C3: CoreDNS warm-up wait.
360
+ *
361
+ * Runs `kubectl wait --for=condition=Available deployment/coredns -n kube-system --timeout=30s`
362
+ * before step 3 to ensure DNS resolution is available for peripheral service discovery.
363
+ *
364
+ * Returns true on success; false on timeout or failure.
365
+ */
366
+ async function waitForCoreDns(context, deps) {
367
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
368
+ const result = await wrap([
369
+ '--context', context,
370
+ 'wait',
371
+ '--for=condition=Available',
372
+ 'deployment/coredns',
373
+ '-n', 'kube-system',
374
+ '--timeout=30s',
375
+ ], { timeout: 35_000 });
376
+ return result.ok;
377
+ }
138
378
  /**
139
379
  * Step 6 — verify /health returns X-Olam-Engine: kubernetes.
140
380
  */
@@ -161,6 +401,60 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
161
401
  const stderr = deps.stderr ?? process.stderr;
162
402
  const manifestsDir = deps.manifestsDir ?? OLAM_K8S_MANIFESTS_DIR;
163
403
  const startMs = Date.now();
404
+ // ── Pre-step 0: B5 — seed manifests from bundle if dir absent ────
405
+ // Closes T2/B2: ENOENT at step 2.5 (30-configmap.yaml) on fresh installs.
406
+ // Runs FIRST so manifests are available for all subsequent steps.
407
+ {
408
+ const seedImpl = deps.seedManifestsImpl ?? seedManifestsFromBundle;
409
+ const seedResult = seedImpl(opts.forceRefreshManifests === true, deps.seedManifestsDeps ?? {});
410
+ if (seedResult.seeded) {
411
+ stdout.write(`${pc.dim('[seed]')} ${seedResult.message}\n`);
412
+ }
413
+ else if (!seedResult.skipped) {
414
+ // Hard error: copy failed (permission denied, ENOSPC, etc.).
415
+ stderr.write(`${pc.red('error:')} Manifest seed failed: ${seedResult.error}\n`);
416
+ return { exitCode: 1, summary: 'manifest seed failed' };
417
+ }
418
+ // skipped: dir exists or bundle absent (dev context) — no output, continue.
419
+ }
420
+ // ── Pre-step 0a: D6 — managed-k8s context refusal (Decision #18) ──────
421
+ // Detects managed-k8s cluster server URL patterns and refuses BEFORE any
422
+ // kubectl apply. Fail-open: unrecognized cluster URLs proceed normally.
423
+ {
424
+ const managedProvider = await detectManagedK8sProvider(deps);
425
+ if (managedProvider !== null) {
426
+ stderr.write(`${pc.red('error:')} olam upgrade supports local k3d/k3s clusters only.\n` +
427
+ ` Detected managed-k8s context: ${managedProvider}\n` +
428
+ ` hostPath /var/run/docker.sock is not accessible on managed clusters — the docker\n` +
429
+ ` socket does not reach the operator's host daemon on cloud-managed nodes.\n` +
430
+ ` See Decision #18 and docs/operator/kubernetes-substrate-beta.md\n` +
431
+ ` for supported cluster types and the active retraction of managed-k8s support.\n`);
432
+ return { exitCode: 1, summary: 'managed-k8s context detected (Decision #18 retraction)' };
433
+ }
434
+ }
435
+ // ── Pre-step 0b: D7 — docker socket bind-mount preflight ──────────────
436
+ // Checks that /var/run/docker.sock is accessible inside the host-cp pod
437
+ // BEFORE step 0. Prevents crash-loop UX when the operator forgot the
438
+ // --volume socket bind on k3d cluster create (OQ13 resolution).
439
+ // Note: only meaningful when host-cp is already deployed. On a fresh install
440
+ // the pod doesn't exist yet — kubectl exec will fail gracefully (WARN only).
441
+ {
442
+ const preflightContext = process.env.OLAM_K8S_CONTEXT_ACK ?? 'default';
443
+ const socketAccessible = await checkDockerSocketAccessible(preflightContext, deps);
444
+ if (!socketAccessible) {
445
+ stderr.write(`${pc.yellow('[WARN]')} docker socket not accessible at /var/run/docker.sock inside the host-cp pod.\n` +
446
+ ` This means the k3d cluster was not created with the docker socket bind-mount,\n` +
447
+ ` or host-cp is not yet deployed (first install — this warning is expected).\n` +
448
+ ` For subsequent upgrades, recreate the cluster with:\n` +
449
+ ` k3d cluster create olam-host \\\n` +
450
+ ` --volume /var/run/docker.sock:/var/run/docker.sock@server:* \\\n` +
451
+ ` --volume ~/.config/gh:/host/.config/gh \\\n` +
452
+ ` --volume <olam-repo>:/host/olam \\\n` +
453
+ ` --wait --timeout 90s\n` +
454
+ ` Run olam doctor to check probe 28 (probeDockerSocketBindMount).\n`);
455
+ // WARN only — do not abort. Fresh installs have no pod yet.
456
+ }
457
+ }
164
458
  // ── Step 0: D22 — probe Kubernetes API reachability (5s) ─────────
165
459
  const step0Spinner = ora('Probing Kubernetes API reachability').start();
166
460
  const context = process.env.OLAM_K8S_CONTEXT_ACK ?? '';
@@ -214,9 +508,46 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
214
508
  return { exitCode: 1, summary: 'secret pre-check failed' };
215
509
  }
216
510
  step2Spinner.succeed(`Secret ${HOST_CP_SECRET_NAME} verified`);
511
+ // ── D5: Phase 2 GA — OLAM_PHASE_2_BETA guard removed (D27) ──────
512
+ // Emit phase2.flag_removed audit log entry per upgrade run (D27).
513
+ appendSubstrateAuditEntry({
514
+ ts: new Date(deps.nowImpl ? deps.nowImpl() : Date.now()).toISOString(),
515
+ op: 'upgrade',
516
+ substrate: 'kubernetes',
517
+ phase2: { flag_removed: true },
518
+ }, stderr);
519
+ // ── Step 2.5: C1 — in-memory ConfigMap substitution (K8s DNS URLs) ──
520
+ const step25Spinner = ora('Patching ConfigMap with K8s DNS URLs (C1/D4)').start();
521
+ const configMapError = await applyConfigMapSubstitution(pinnedContext, manifestsDir, deps);
522
+ if (configMapError !== null) {
523
+ step25Spinner.fail('ConfigMap substitution failed');
524
+ stderr.write(`${pc.red('error:')} ${configMapError}\n`);
525
+ return { exitCode: 1, summary: 'configmap substitution failed' };
526
+ }
527
+ step25Spinner.succeed('ConfigMap patched with K8s DNS URLs');
528
+ // ── Step 2.6: C2 — per-peripheral Secret pre-check ──────────────
529
+ const step26Spinner = ora('Checking peripheral Secrets (C2)').start();
530
+ const peripheralSecretError = await checkPeripheralSecrets(pinnedContext, deps);
531
+ if (peripheralSecretError !== null) {
532
+ step26Spinner.fail('Peripheral Secret pre-check failed');
533
+ stderr.write(`${pc.red('error:')} ${peripheralSecretError}\n`);
534
+ return { exitCode: 1, summary: 'peripheral secret pre-check failed' };
535
+ }
536
+ step26Spinner.succeed('All peripheral Secrets verified');
537
+ // ── Step 2.7: C3 — CoreDNS warm-up wait ────────────────────────
538
+ const step27Spinner = ora('Waiting for CoreDNS (C3, 30s timeout)').start();
539
+ const coreDnsOk = await waitForCoreDns(pinnedContext, deps);
540
+ if (!coreDnsOk) {
541
+ step27Spinner.warn('CoreDNS not Available within 30s — continuing (DNS may be degraded)');
542
+ }
543
+ else {
544
+ step27Spinner.succeed('CoreDNS Available');
545
+ }
217
546
  // ── Step 3: kubectl apply ─────────────────────────────────────────
218
547
  const step3Spinner = ora('Applying manifests').start();
219
548
  const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
549
+ // Always apply host-cp manifests (top-level manifests dir).
550
+ // kubectl -f <dir> does NOT recurse by default — peripheral subdirs are ignored here.
220
551
  const applyResult = await wrap(['--context', pinnedContext, 'apply', '-f', manifestsDir], { timeout: 120_000 });
221
552
  if (!applyResult.ok) {
222
553
  step3Spinner.fail('kubectl apply failed');
@@ -224,24 +555,61 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
224
555
  ` ${applyResult.stderr.split('\n').slice(0, 5).join('\n ')}\n`);
225
556
  return { exitCode: 1, summary: 'kubectl apply failed' };
226
557
  }
227
- step3Spinner.succeed('Manifests applied');
558
+ // Apply each peripheral's manifest directory (alphabetical order).
559
+ const peripheralNames = [...PERIPHERALS.map((p) => p.name)].sort();
560
+ for (const name of peripheralNames) {
561
+ const peripheralManifestsDir = path.join(manifestsDir, name);
562
+ const peripheralApplyResult = await wrap(['--context', pinnedContext, 'apply', '-f', peripheralManifestsDir], { timeout: 120_000 });
563
+ if (!peripheralApplyResult.ok) {
564
+ step3Spinner.fail(`kubectl apply failed for peripheral "${name}"`);
565
+ stderr.write(`${pc.red('error:')} kubectl apply failed for peripheral "${name}" (exit ${peripheralApplyResult.exitCode}):\n` +
566
+ ` ${peripheralApplyResult.stderr.split('\n').slice(0, 5).join('\n ')}\n`);
567
+ return { exitCode: 1, summary: `kubectl apply failed for peripheral ${name}` };
568
+ }
569
+ }
570
+ step3Spinner.succeed('All manifests applied (host-cp + 4 peripherals)');
228
571
  // ── Step 4: D15 — kubectl rollout status (90s timeout, 95s wrap) ──
229
- const step4Spinner = ora('Waiting for rollout (90s)').start();
230
- const rolloutOk = await waitForRollout(pinnedContext, deps, stderr);
231
- if (!rolloutOk) {
232
- step4Spinner.fail('Rollout status failed or timed out');
572
+ // Wait for all 5 deployments in parallel (host-cp + 4 peripherals).
573
+ const step4Spinner = ora('Waiting for rollout (all 5 deployments, 90s each)').start();
574
+ const deploymentNames = [
575
+ HOST_CP_DEPLOYMENT_NAME,
576
+ ...PERIPHERALS.map((p) => p.name),
577
+ ];
578
+ const rolloutResults = await Promise.all(deploymentNames.map((deploymentName) => (deps.kubectlWrapImpl ?? kubectlWrap)([
579
+ '--context', pinnedContext,
580
+ 'rollout', 'status',
581
+ `deployment/${deploymentName}`,
582
+ '-n', K8S_NAMESPACE,
583
+ '--timeout=90s',
584
+ ], { timeout: 95_000 })));
585
+ const failedDeployments = deploymentNames.filter((_, i) => !rolloutResults[i]?.ok);
586
+ if (failedDeployments.length > 0) {
587
+ step4Spinner.fail(`Rollout failed for: ${failedDeployments.join(', ')}`);
588
+ // Emit state snapshot for failed deployments (mirrors D15 pattern).
589
+ for (const name of failedDeployments) {
590
+ stderr.write(`${pc.red('error:')} rollout status failed for deployment/${name}.\n`);
591
+ stderr.write(`${pc.dim(`--- kubectl get pods -n ${K8S_NAMESPACE} -o wide ---`)}\n`);
592
+ const podsResult = await wrap(['--context', pinnedContext, 'get', 'pods', '-n', K8S_NAMESPACE, '-o', 'wide'], { timeout: 10_000 });
593
+ stderr.write((podsResult.stdout || podsResult.stderr || '(no output)') + '\n');
594
+ }
595
+ stderr.write(`\n${pc.yellow('note:')} Manifests are NOT auto-rolled-back. Inspect the state above and remediate manually.\n`);
233
596
  return { exitCode: 1, summary: 'rollout status timed out' };
234
597
  }
235
- step4Spinner.succeed('Rollout complete');
598
+ step4Spinner.succeed('All 5 deployments rolled out');
236
599
  // ── Step 5: D17 — port-forward spawn via flock ───────────────────
600
+ // Spawn host-cp and all peripheral port-forwards in parallel.
237
601
  const step5Spinner = ora('Establishing port-forward').start();
238
602
  const pfSpawn = deps.spawnPortForwardImpl ?? spawnPortForward;
239
- const pfResult = await pfSpawn(pinnedContext, K8S_NAMESPACE, PORT_FORWARD_TARGET, 19000, 19000, deps.portForwardDeps ?? {});
603
+ const spawnAllPeripheral = deps.spawnAllPeripheralPortForwardsImpl ?? spawnAllPeripheralPortForwards;
604
+ const [pfResult] = await Promise.all([
605
+ pfSpawn(pinnedContext, K8S_NAMESPACE, PORT_FORWARD_TARGET, 19000, 19000, deps.portForwardDeps ?? {}),
606
+ spawnAllPeripheral(pinnedContext, K8S_NAMESPACE, deps.peripheralPortForwardDeps ?? {}),
607
+ ]);
240
608
  if (pfResult.spawned) {
241
- step5Spinner.succeed(`Port-forward spawned (pid ${pfResult.pid})`);
609
+ step5Spinner.succeed(`Port-forward spawned (pid ${pfResult.pid}) + all peripheral port-forwards`);
242
610
  }
243
611
  else if (pfResult.reason === 'live') {
244
- step5Spinner.succeed('Port-forward already live');
612
+ step5Spinner.succeed('Port-forward already live + peripheral port-forwards spawned');
245
613
  }
246
614
  else {
247
615
  step5Spinner.warn('Port-forward lock held by concurrent caller; skipping spawn');