@openpalm/lib 0.11.0-beta.10 → 0.11.0-beta.13

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 (54) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/control-plane/akm-user-env.test.ts +113 -0
  4. package/src/control-plane/akm-user-env.ts +144 -0
  5. package/src/control-plane/backup.ts +14 -5
  6. package/src/control-plane/channels.ts +48 -29
  7. package/src/control-plane/cleanup-guardrails.test.ts +1 -1
  8. package/src/control-plane/compose-args.test.ts +90 -31
  9. package/src/control-plane/compose-args.ts +119 -9
  10. package/src/control-plane/config-persistence.ts +87 -133
  11. package/src/control-plane/core-assets.test.ts +9 -9
  12. package/src/control-plane/core-assets.ts +24 -8
  13. package/src/control-plane/docker.ts +15 -14
  14. package/src/control-plane/env.test.ts +10 -10
  15. package/src/control-plane/env.ts +1 -1
  16. package/src/control-plane/extends-support.test.ts +8 -8
  17. package/src/control-plane/home.ts +34 -46
  18. package/src/control-plane/host-opencode.test.ts +82 -10
  19. package/src/control-plane/host-opencode.ts +42 -13
  20. package/src/control-plane/install-edge-cases.test.ts +94 -102
  21. package/src/control-plane/install-lock.ts +7 -7
  22. package/src/control-plane/lifecycle.ts +36 -34
  23. package/src/control-plane/markdown-task.ts +30 -50
  24. package/src/control-plane/paths.ts +62 -42
  25. package/src/control-plane/profile-ids.ts +21 -0
  26. package/src/control-plane/provider-models.ts +3 -3
  27. package/src/control-plane/registry.test.ts +97 -88
  28. package/src/control-plane/registry.ts +142 -110
  29. package/src/control-plane/rollback.ts +8 -38
  30. package/src/control-plane/scheduler.ts +7 -7
  31. package/src/control-plane/secret-audit.test.ts +159 -0
  32. package/src/control-plane/secret-audit.ts +255 -0
  33. package/src/control-plane/secret-mappings.ts +2 -2
  34. package/src/control-plane/secrets-files.test.ts +60 -0
  35. package/src/control-plane/secrets-files.ts +66 -0
  36. package/src/control-plane/secrets.ts +113 -86
  37. package/src/control-plane/setup-config.schema.json +1 -1
  38. package/src/control-plane/setup-status.ts +6 -11
  39. package/src/control-plane/setup.test.ts +60 -40
  40. package/src/control-plane/setup.ts +36 -31
  41. package/src/control-plane/skeleton-guardrail.test.ts +64 -55
  42. package/src/control-plane/spec-to-env.test.ts +22 -17
  43. package/src/control-plane/spec-to-env.ts +7 -2
  44. package/src/control-plane/stack-spec.test.ts +10 -0
  45. package/src/control-plane/stack-spec.ts +28 -1
  46. package/src/control-plane/types.ts +2 -4
  47. package/src/control-plane/ui-assets.ts +60 -58
  48. package/src/control-plane/validate.ts +13 -15
  49. package/src/index.ts +47 -15
  50. package/src/control-plane/akm-vault.test.ts +0 -105
  51. package/src/control-plane/akm-vault.ts +0 -311
  52. package/src/control-plane/migrate-0110.test.ts +0 -177
  53. package/src/control-plane/migrate-0110.ts +0 -99
  54. package/src/control-plane/registry-components.test.ts +0 -391
@@ -12,13 +12,13 @@
12
12
  */
13
13
  import {
14
14
  existsSync, mkdirSync, readdirSync, copyFileSync,
15
- readFileSync, writeFileSync, rmSync, realpathSync, renameSync,
15
+ writeFileSync, rmSync, realpathSync, renameSync,
16
16
  } from 'node:fs';
17
17
  import { join, dirname, relative } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { createHash } from 'node:crypto';
20
20
  import { x as tarExtract } from 'tar';
21
- import { resolveStateDir } from './home.js';
21
+ import { resolveBackupsDir, resolveDataDir } from './home.js';
22
22
  import { createLogger } from '../logger.js';
23
23
 
24
24
  const logger = createLogger('lib:ui-assets');
@@ -87,13 +87,15 @@ export function resolveLocalOpenpalmDir(): string | null {
87
87
  () => process.env.OPENPALM_REPO_ROOT
88
88
  ? join(process.env.OPENPALM_REPO_ROOT, '.openpalm')
89
89
  : null,
90
- // 2. Relative to this source file (dev / bun run)
90
+ // 2. Electron extraResources openpalm-skeleton/ placed alongside the asar
91
+ () => process.env.OPENPALM_SKELETON_DIR ?? null,
92
+ // 3. Relative to this source file (dev / bun run)
91
93
  () => {
92
94
  const meta = fileURLToPath(import.meta.url);
93
95
  if (meta.startsWith('/$bunfs/')) return null;
94
96
  return join(dirname(meta), '..', '..', '..', '..', '.openpalm');
95
97
  },
96
- // 3. Relative to the compiled binary on disk
98
+ // 4. Relative to the compiled binary on disk
97
99
  () => join(dirname(realpathSync(process.execPath)), '..', '..', '..', '.openpalm'),
98
100
  );
99
101
  }
@@ -109,14 +111,12 @@ export async function seedOpenPalmDir(
109
111
  repoRef: string,
110
112
  homeDir: string,
111
113
  _configDir: string,
112
- _stateDir: string,
114
+ _dataDir: string,
113
115
  ): Promise<void> {
114
116
  const local = resolveLocalOpenpalmDir();
115
117
  if (local) {
116
118
  logger.debug('seeding .openpalm from local source', { src: local });
117
119
  copyTree(local, homeDir, { skipExisting: true });
118
- // Registry is system-managed — always refresh so addon overlays stay current.
119
- copyTree(join(local, 'state', 'registry'), join(homeDir, 'state', 'registry'));
120
120
  return;
121
121
  }
122
122
 
@@ -137,8 +137,6 @@ export async function seedOpenPalmDir(
137
137
  const srcOpenpalm = join(tmpDir, '.openpalm');
138
138
  if (!existsSync(srcOpenpalm)) throw new Error('.openpalm/ not found in tarball');
139
139
  copyTree(srcOpenpalm, homeDir, { skipExisting: true });
140
- // Registry is system-managed — always refresh so addon overlays stay current.
141
- copyTree(join(srcOpenpalm, 'state', 'registry'), join(homeDir, 'state', 'registry'));
142
140
  } finally {
143
141
  rmSync(tmpDir, { recursive: true, force: true });
144
142
  }
@@ -179,47 +177,28 @@ export function resolveLocalUiBuild(): string | null {
179
177
  );
180
178
  }
181
179
 
182
- function readUiVersionFile(dir: string): string | null {
183
- try { return readFileSync(join(dir, 'version.txt'), 'utf-8').trim(); } catch { return null; }
184
- }
185
-
186
180
  /**
187
181
  * Resolve the best available UI build directory at runtime.
188
182
  *
189
183
  * Priority:
190
- * 1. OP_HOME/state/ui/ — if its version.txt is NEWER than the bundled build
191
- * 2. Bundled / local build (Electron extraResources, source checkout)
192
- * 3. OP_HOME/state/ui/ — fallback when no bundled build exists
193
- *
194
- * This means GitHub-downloaded updates are applied automatically (disk wins
195
- * when newer), but a fresh AppImage install always works without a download.
184
+ * 1. OP_HOME/data/ui/ — user-installed or auto-updated build
185
+ * 2. Bundled / local build (Electron extraResources, OPENPALM_REPO_ROOT, source checkout)
196
186
  */
197
187
  export function resolveUiBuildDir(): string {
198
- const stateBuild = join(resolveStateDir(), 'ui');
199
- const localBuild = resolveLocalUiBuild();
200
-
201
- if (existsSync(join(stateBuild, 'index.js')) && localBuild) {
202
- const diskVer = readUiVersionFile(stateBuild);
203
- const bundledVer = readUiVersionFile(localBuild);
204
- if (diskVer && bundledVer && compareVersionTags(diskVer, bundledVer) > 0) {
205
- return stateBuild;
206
- }
207
- return localBuild;
208
- }
209
-
210
- if (localBuild) return localBuild;
211
- return stateBuild;
188
+ const dataBuild = join(resolveDataDir(), 'ui');
189
+ if (existsSync(join(dataBuild, 'index.js'))) return dataBuild;
190
+ return resolveLocalUiBuild() ?? dataBuild;
212
191
  }
213
192
 
214
193
  /**
215
- * Install the UI build to OP_HOME/state/ui/.
194
+ * Install the UI build to OP_HOME/data/ui/.
216
195
  *
217
196
  * Copies from local packages/ui/build/ when running from source,
218
197
  * otherwise downloads ui-build.tar.gz from the GitHub release.
219
198
  * Called during install and update; always replaces existing content.
220
199
  *
221
- * state/ui/ is automatically included in backups because
222
- * backupOpenPalmHome() copies all of OP_HOME/state/.
200
+ * data/ui/ is automatically included in backups because
201
+ * backupOpenPalmHome() copies all of OP_HOME/data/.
223
202
  */
224
203
  /** SHA-256 hex digest of arbitrary bytes. */
225
204
  function sha256Hex(data: Uint8Array): string {
@@ -241,21 +220,14 @@ function parseChecksumsFile(content: string): Map<string, string> {
241
220
  return map;
242
221
  }
243
222
 
244
- export function readCurrentUiBuildVersion(stateDir: string): string | null {
245
- const versionFile = join(stateDir, 'ui', 'version.txt');
246
- if (!existsSync(versionFile)) return null;
247
- return readFileSync(versionFile, 'utf-8').trim() || null;
248
- }
249
-
250
- export async function seedUiBuild(repoRef: string, stateDir: string, options?: { forceRemote?: boolean }): Promise<void> {
251
- const uiDir = join(stateDir, 'ui');
223
+ export async function seedUiBuild(repoRef: string, dataDir: string, options?: { forceRemote?: boolean }): Promise<void> {
224
+ const uiDir = join(dataDir, 'ui');
252
225
  mkdirSync(uiDir, { recursive: true });
253
226
 
254
227
  const local = options?.forceRemote ? null : resolveLocalUiBuild();
255
228
  if (local) {
256
229
  logger.debug('seeding UI build from local source', { src: local });
257
230
  copyTree(local, uiDir);
258
- writeFileSync(join(uiDir, 'version.txt'), repoRef.replace(/^v/, ''));
259
231
  return;
260
232
  }
261
233
 
@@ -264,7 +236,7 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
264
236
  const checksumUrl = `${base}/checksums-sha256.txt`;
265
237
  logger.debug('downloading UI build', { url: tarballUrl });
266
238
 
267
- const tmpTar = join(stateDir, '.ui-build.tar.gz.tmp');
239
+ const tmpTar = join(dataDir, '.ui-build.tar.gz.tmp');
268
240
  try {
269
241
  // Download tarball and checksums file in parallel (checksums best-effort)
270
242
  const [tarRes, csRes] = await Promise.all([
@@ -295,7 +267,6 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
295
267
  mkdirSync(uiDir, { recursive: true });
296
268
  // Cross-platform extraction via the `tar` npm package — no shell dependency
297
269
  await tarExtract({ file: tmpTar, cwd: uiDir, strip: 1 });
298
- writeFileSync(join(uiDir, 'version.txt'), repoRef.replace(/^v/, ''));
299
270
  } finally {
300
271
  rmSync(tmpTar, { force: true });
301
272
  }
@@ -305,14 +276,45 @@ export async function seedUiBuild(repoRef: string, stateDir: string, options?: {
305
276
 
306
277
  const GITHUB_API = 'https://api.github.com';
307
278
 
308
- /** Returns 1 if a > b, -1 if a < b, 0 if equal. Strips leading 'v'. */
279
+ /** Returns 1 if a > b, -1 if a < b, 0 if equal. Strips leading 'v'. Handles pre-release tags. */
309
280
  function compareVersionTags(a: string, b: string): number {
310
- const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
311
- const [aM, am, ap] = parse(a);
312
- const [bM, bm, bp] = parse(b);
281
+ const parse = (v: string): [number, number, number, string | null] => {
282
+ const clean = v.replace(/^v/, '');
283
+ const dashIdx = clean.indexOf('-');
284
+ const main = dashIdx === -1 ? clean : clean.slice(0, dashIdx);
285
+ const pre = dashIdx === -1 ? null : clean.slice(dashIdx + 1);
286
+ const parts = main.split('.').map(Number);
287
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0, pre];
288
+ };
289
+ const comparePre = (x: string, y: string): number => {
290
+ const xp = x.split('.');
291
+ const yp = y.split('.');
292
+ for (let i = 0; i < Math.max(xp.length, yp.length); i++) {
293
+ if (i >= xp.length) return -1;
294
+ if (i >= yp.length) return 1;
295
+ const xn = Number(xp[i]);
296
+ const yn = Number(yp[i]);
297
+ const xIsNum = !isNaN(xn);
298
+ const yIsNum = !isNaN(yn);
299
+ if (xIsNum && yIsNum) {
300
+ if (xn !== yn) return xn > yn ? 1 : -1;
301
+ } else if (xIsNum !== yIsNum) {
302
+ return xIsNum ? -1 : 1; // numeric < alphanumeric per semver
303
+ } else {
304
+ if (xp[i] !== yp[i]) return xp[i]! > yp[i]! ? 1 : -1;
305
+ }
306
+ }
307
+ return 0;
308
+ };
309
+ const [aM, am, ap, aPre] = parse(a);
310
+ const [bM, bm, bp, bPre] = parse(b);
313
311
  if (aM !== bM) return aM > bM ? 1 : -1;
314
312
  if (am !== bm) return am > bm ? 1 : -1;
315
313
  if (ap !== bp) return ap > bp ? 1 : -1;
314
+ // Same numeric version: stable > pre-release (semver spec)
315
+ if (aPre === null && bPre !== null) return 1;
316
+ if (aPre !== null && bPre === null) return -1;
317
+ if (aPre !== null && bPre !== null) return comparePre(aPre, bPre);
316
318
  return 0;
317
319
  }
318
320
 
@@ -326,15 +328,15 @@ export interface UiBuildUpdateResult {
326
328
  * Check GitHub for a newer UI build and apply it if one exists.
327
329
  *
328
330
  * When an update is available:
329
- * 1. Move state/ui/ → state/backups/ui-{timestamp}/ (preserves the old build)
330
- * 2. Download ui-build.tar.gz from the latest release and extract to state/ui/
331
+ * 1. Move data/ui/ → data/backups/ui-{timestamp}/ (preserves the old build)
332
+ * 2. Download ui-build.tar.gz from the latest release and extract to data/ui/
331
333
  *
332
334
  * Non-fatal: any network or extraction error returns { updated: false, error }.
333
335
  * The caller should proceed with the existing build on failure.
334
336
  */
335
337
  export async function checkAndUpdateUiBuild(
336
338
  currentVersion: string,
337
- stateDir: string,
339
+ dataDir: string,
338
340
  ): Promise<UiBuildUpdateResult> {
339
341
  try {
340
342
  const res = await fetch(
@@ -365,15 +367,15 @@ export async function checkAndUpdateUiBuild(
365
367
  }
366
368
 
367
369
  // Back up the existing UI build before replacing it
368
- const uiDir = join(stateDir, 'ui');
370
+ const uiDir = join(dataDir, 'ui');
369
371
  if (existsSync(join(uiDir, 'index.js'))) {
370
- const backupDir = join(stateDir, 'backups', `ui-${Date.now()}`);
371
- mkdirSync(join(stateDir, 'backups'), { recursive: true });
372
+ const backupDir = join(resolveBackupsDir(), `ui-${Date.now()}`);
373
+ mkdirSync(resolveBackupsDir(), { recursive: true });
372
374
  renameSync(uiDir, backupDir);
373
375
  logger.debug('backed up UI build before update', { backup: backupDir });
374
376
  }
375
377
 
376
- await seedUiBuild(latestTag, stateDir);
378
+ await seedUiBuild(latestTag, dataDir);
377
379
  logger.debug('UI build updated', { from: currentVersion, to: latestVersion });
378
380
 
379
381
  return { updated: true, latestVersion };
@@ -2,26 +2,26 @@
2
2
  * Runtime configuration validation for the OpenPalm control plane.
3
3
  *
4
4
  * Validation is a presence check on the canonical env keys we expect in
5
- * the live config/stack/stack.env file. The
5
+ * the live config/stack files. The
6
6
  * historical schema files and external validation binary were retired in
7
7
  * #391; everything advisory is surfaced as a non-blocking warning. The
8
8
  * function never shells out and never reads schemas.
9
9
  */
10
10
  import { existsSync } from "node:fs";
11
- import { readStackEnv } from "./secrets.js";
11
+ import { readStackRuntimeEnv } from "./secrets.js";
12
12
  import { getCoreSecretMappings } from "./secret-mappings.js";
13
13
  import type { ControlPlaneState } from "./types.js";
14
14
 
15
15
  // Stack-scoped env keys that must always exist and carry a non-empty value
16
16
  // for the platform to boot. Keep this list small — anything optional
17
17
  // belongs in the warning bucket instead.
18
- const REQUIRED_STACK_KEYS = ["OP_UI_LOGIN_PASSWORD"] as const;
18
+ const REQUIRED_SECRET_KEYS = ["OP_UI_LOGIN_PASSWORD"] as const;
19
19
 
20
20
  /**
21
21
  * Validate the live configuration files.
22
22
  *
23
23
  * Checks:
24
- * 1. config/stack/stack.env exists and carries every required key with a
24
+ * 1. knowledge/env/stack.env exists and carries every required key with a
25
25
  * non-empty value.
26
26
  * 2. Every secret env key in getCoreSecretMappings() is present (key only
27
27
  * — blank values are warned about, never erred on, because operators
@@ -38,32 +38,30 @@ export async function validateProposedState(state: ControlPlaneState): Promise<{
38
38
  const errors: string[] = [];
39
39
  const warnings: string[] = [];
40
40
 
41
- const stackEnvPath = `${state.stackDir}/stack.env`;
41
+ const stackEnvPath = `${state.stashDir}/env/stack.env`;
42
42
 
43
43
  if (!existsSync(stackEnvPath)) {
44
44
  errors.push(`ERROR: stack env file missing at ${stackEnvPath}`);
45
45
  return { ok: false, errors, warnings };
46
46
  }
47
47
 
48
- const stackEnv = readStackEnv(state.stackDir);
49
- const userEnv: Record<string, string> = {};
48
+ const runtimeEnv = readStackRuntimeEnv(state.stackDir);
50
49
 
51
- for (const key of REQUIRED_STACK_KEYS) {
52
- const value = stackEnv[key];
50
+ for (const key of REQUIRED_SECRET_KEYS) {
51
+ const value = runtimeEnv[key];
53
52
  if (!value || value.trim().length === 0) {
54
- errors.push(`ERROR: required key ${key} is missing or empty in config/stack/stack.env`);
53
+ errors.push(`ERROR: required secret ${key} is missing or empty in knowledge/secrets/${key.toLowerCase()}`);
55
54
  }
56
55
  }
57
56
 
58
57
  // Every canonical secret should at least appear as a key somewhere in
59
58
  // the env files so the operator sees the slot. Missing slots warn (not
60
59
  // error) since not every provider is in use on every install.
61
- for (const mapping of getCoreSecretMappings(stackEnv)) {
62
- const inStack = Object.prototype.hasOwnProperty.call(stackEnv, mapping.envKey);
63
- const inUser = Object.prototype.hasOwnProperty.call(userEnv, mapping.envKey);
64
- if (!inStack && !inUser) {
60
+ for (const mapping of getCoreSecretMappings(runtimeEnv)) {
61
+ const inRuntime = Object.prototype.hasOwnProperty.call(runtimeEnv, mapping.envKey);
62
+ if (!inRuntime) {
65
63
  warnings.push(
66
- `WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in config/stack/stack.env`,
64
+ `WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in knowledge/secrets/${mapping.envKey.toLowerCase()}`,
67
65
  );
68
66
  }
69
67
  }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  export {
11
11
  LLM_PROVIDERS,
12
12
  EMBEDDING_DIMS,
13
+ PROVIDER_KEY_MAP,
13
14
  lookupEmbeddingDims,
14
15
  } from "./provider-constants.js";
15
16
 
@@ -75,8 +76,7 @@ export {
75
76
  resolveConfigDir,
76
77
  resolveStashDir,
77
78
  resolveWorkspaceDir,
78
- resolveCacheDir,
79
- resolveStateDir,
79
+ resolveDataDir,
80
80
  resolveStackDir,
81
81
  resolveLogsDir,
82
82
  ensureHomeDirs,
@@ -109,12 +109,36 @@ export {
109
109
  updateSecretsEnv,
110
110
  writeAuthJsonProviderKeys,
111
111
  readStackEnv,
112
+ readStackSecretEnv,
113
+ readStackRuntimeEnv,
114
+ writeStackSecretEnv,
112
115
  patchSecretsEnvFile,
113
116
  maskSecretValue,
114
117
  ensureOpenCodeConfig,
118
+ assertNoSecretLikeStackEnvKeys,
115
119
  } from "./control-plane/secrets.js";
116
- export { migrateAuth0110 } from "./control-plane/migrate-0110.js";
117
- export type { MigrateAuth0110Result } from "./control-plane/migrate-0110.js";
120
+ export {
121
+ resolveSecretsDir,
122
+ secretPath,
123
+ readSecret,
124
+ writeSecret,
125
+ ensureSecret,
126
+ removeSecret,
127
+ listSecretNames,
128
+ } from './control-plane/secrets-files.js';
129
+ export type {
130
+ SecretAuditIssue,
131
+ SecretAuditOptions,
132
+ SecretAuditResult,
133
+ SecretAuditSeverity,
134
+ } from "./control-plane/secret-audit.js";
135
+ export {
136
+ auditComposeSecrets,
137
+ auditFileBasedSecrets,
138
+ auditSecretFilesystem,
139
+ auditStackEnv,
140
+ isSecretLikeKey,
141
+ } from "./control-plane/secret-audit.js";
118
142
  // ── Setup Status ────────────────────────────────────────────────────────
119
143
  export {
120
144
  isSetupComplete,
@@ -156,8 +180,8 @@ export {
156
180
  buildRuntimeFileMeta,
157
181
  writeRuntimeFiles,
158
182
  writeSystemEnv,
159
- readChannelSecrets,
160
- writeChannelSecrets,
183
+ channelSecretName,
184
+ ensureChannelSecret,
161
185
  ensureComposeVolumeTargets,
162
186
  } from "./control-plane/config-persistence.js";
163
187
 
@@ -230,8 +254,16 @@ export { detectLocalProviders } from "./control-plane/model-runner.js";
230
254
  export {
231
255
  buildComposeOptions,
232
256
  buildComposeCliArgs,
257
+ resolveActiveProfiles,
258
+ writeRunScript,
233
259
  } from "./control-plane/compose-args.js";
234
260
 
261
+ export {
262
+ addonProfileId,
263
+ canonicalAddonProfileSelection,
264
+ resolveHardwareProfileVariant,
265
+ } from "./control-plane/profile-ids.js";
266
+
235
267
  // ── Compose Error Parsing ────────────────────────────────────────────────
236
268
  export type { ComposeServiceFailure } from "./control-plane/compose-errors.js";
237
269
  export {
@@ -289,16 +321,17 @@ export {
289
321
  importHostOpenCode,
290
322
  } from "./control-plane/host-opencode.js";
291
323
 
292
- // ── AKM Vault ────────────────────────────────────────────────────────────
324
+ // ── AKM user env (env:user) ──────────────────────────────────────────────
293
325
  export {
294
- AKM_USER_VAULT_REF,
326
+ AKM_USER_ENV_REF,
295
327
  buildAkmEnv,
296
- ensureAkmUserVault,
297
- writeAkmVaultKey,
298
- deleteAkmVaultKey,
299
- readAkmUserVaultFile,
300
- readUserVaultSync,
301
- } from "./control-plane/akm-vault.js";
328
+ ensureAkmUserEnv,
329
+ writeUserEnvKey,
330
+ deleteUserEnvKey,
331
+ readUserEnvFile,
332
+ readUserEnvSync,
333
+ userEnvPathSync,
334
+ } from "./control-plane/akm-user-env.js";
302
335
 
303
336
  // ── UI asset seeding and resolution ─────────────────────────────────────────
304
337
  export type { UiBuildUpdateResult } from "./control-plane/ui-assets.js";
@@ -309,5 +342,4 @@ export {
309
342
  resolveUiBuildDir,
310
343
  seedUiBuild,
311
344
  checkAndUpdateUiBuild,
312
- readCurrentUiBuildVersion,
313
345
  } from "./control-plane/ui-assets.js";
@@ -1,105 +0,0 @@
1
- /**
2
- * Tests for the akm vault helpers. The vault helpers spawn `akm vault set
3
- * <ref> <key>` and feed the secret VALUE on stdin (akm-cli >= 0.8.0).
4
- *
5
- * Tests gate on the akm CLI being on PATH so the suite stays green in
6
- * environments without akm installed.
7
- */
8
- import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test";
9
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
- import { execFileSync } from "node:child_process";
13
- import {
14
- ensureAkmUserVault,
15
- readAkmUserVaultFile,
16
- writeAkmVaultKey,
17
- deleteAkmVaultKey,
18
- AKM_USER_VAULT_REF,
19
- } from "./akm-vault.js";
20
- import type { ControlPlaneState } from "./types.js";
21
-
22
- function makeState(homeDir: string): ControlPlaneState {
23
- return {
24
- homeDir,
25
- configDir: join(homeDir, "config"),
26
- stashDir: join(homeDir, "stash"),
27
- workspaceDir: join(homeDir, "workspace"),
28
- cacheDir: join(homeDir, "cache"),
29
- stateDir: join(homeDir, "state"),
30
- stackDir: join(homeDir, "stack"),
31
- services: {},
32
- artifacts: { compose: "" },
33
- artifactMeta: [],
34
- };
35
- }
36
-
37
- function hasAkmCli(): boolean {
38
- try {
39
- execFileSync("akm", ["--version"], { stdio: "ignore" });
40
- return true;
41
- } catch {
42
- return false;
43
- }
44
- }
45
-
46
- const AKM_AVAILABLE = hasAkmCli();
47
-
48
-
49
- describe("writeAkmVaultKey", () => {
50
- let homeDir: string;
51
- let state: ControlPlaneState;
52
-
53
- beforeEach(() => {
54
- homeDir = mkdtempSync(join(tmpdir(), "openpalm-akm-write-"));
55
- state = makeState(homeDir);
56
- mkdirSync(state.stashDir, { recursive: true });
57
- mkdirSync(`${state.stateDir}/cache/akm`, { recursive: true });
58
- });
59
-
60
- afterEach(() => {
61
- rmSync(homeDir, { recursive: true, force: true });
62
- });
63
-
64
- it.skipIf(!AKM_AVAILABLE)("writes a key via `akm vault set` (stdin mode, no argv leak)", async () => {
65
- const value = "argv-free-secret-9988";
66
- const ok = await writeAkmVaultKey(state, "TOKEN", value);
67
- expect(ok).toBe(true);
68
-
69
- const vaultPath = await ensureAkmUserVault(state);
70
- expect(vaultPath).not.toBeNull();
71
- if (vaultPath) {
72
- const stored = readAkmUserVaultFile(vaultPath);
73
- expect(stored.TOKEN).toBe(value);
74
- }
75
- });
76
-
77
- it.skipIf(!AKM_AVAILABLE)("deleteAkmVaultKey removes a key via `akm vault unset`", async () => {
78
- await writeAkmVaultKey(state, "TOKEN_A", "value-a");
79
- await writeAkmVaultKey(state, "TOKEN_B", "value-b");
80
-
81
- const ok = await deleteAkmVaultKey(state, "TOKEN_A");
82
- expect(ok).toBe(true);
83
-
84
- const vaultPath = await ensureAkmUserVault(state);
85
- if (vaultPath) {
86
- const stored = readAkmUserVaultFile(vaultPath);
87
- expect(stored.TOKEN_A).toBeUndefined();
88
- expect(stored.TOKEN_B).toBe("value-b");
89
- }
90
- });
91
-
92
- it.skipIf(!AKM_AVAILABLE)("deleteAkmVaultKey is idempotent on a missing key", async () => {
93
- // Deleting a key that was never set should not throw — `akm vault unset`
94
- // either exits 0 or emits a "not found" message we tolerate.
95
- const ok = await deleteAkmVaultKey(state, "NEVER_SET_KEY");
96
- expect(ok).toBe(true);
97
- });
98
- });
99
-
100
-
101
- describe("AKM_USER_VAULT_REF", () => {
102
- it("exports the canonical akm ref string", () => {
103
- expect(AKM_USER_VAULT_REF).toBe("vault:user");
104
- });
105
- });