@openpalm/lib 0.11.0-beta.6 → 0.11.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpalm/lib",
3
- "version": "0.11.0-beta.6",
3
+ "version": "0.11.0-beta.8",
4
4
  "license": "MPL-2.0",
5
5
  "type": "module",
6
6
  "description": "Shared control-plane library for OpenPalm — lifecycle, staging, secrets, channels, connections, scheduler",
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "bun test"
12
12
  },
13
+ "types": "./src/index.ts",
13
14
  "exports": {
14
15
  ".": "./src/index.ts",
15
16
  "./provider-constants": "./src/provider-constants.ts",
@@ -87,7 +87,8 @@ export function writeSystemEnv(state: ControlPlaneState): void {
87
87
  sectionHeader: "# ── Admin-managed ──────────────────────────────────────────────────"
88
88
  });
89
89
 
90
- writeFileSync(systemEnvPath, content);
90
+ writeFileSync(systemEnvPath, content, { mode: 0o600 });
91
+ chmodSync(systemEnvPath, 0o600);
91
92
  }
92
93
 
93
94
  function generateFallbackSystemEnv(state: ControlPlaneState): string {
@@ -95,11 +95,16 @@ function resolveAssetVersion(): string {
95
95
  }
96
96
  const VERSION = resolveAssetVersion();
97
97
 
98
- // Persona files (openpalm.md, system.md) and stash seeds are intentionally NOT
99
- // in this list they are user-customizable and use seedAssistantPersonaFiles()
100
- // / seedStashAssets() which never overwrite existing files (user edits win).
98
+ // Persona files (openpalm.md, system.md), stash seeds, and user-editable config
99
+ // files are intentionally NOT in this list. They are seeded once (never
100
+ // overwritten) via seedOpenPalmDir (skipExisting) or SEEDED_ASSETS below.
101
101
  const MANAGED_ASSETS: { relPath: string; githubFilename: string }[] = [
102
- { relPath: "config/stack/core.compose.yml", githubFilename: ".openpalm/config/stack/core.compose.yml" },
102
+ { relPath: "config/stack/core.compose.yml", githubFilename: ".openpalm/config/stack/core.compose.yml" },
103
+ ];
104
+
105
+ // Seeded once — written only when the file does not exist yet.
106
+ // User edits always win; upgrade never touches these files.
107
+ const SEEDED_ASSETS: { relPath: string; githubFilename: string }[] = [
103
108
  { relPath: "config/assistant/opencode.jsonc", githubFilename: ".openpalm/config/assistant/opencode.jsonc" },
104
109
  ];
105
110
 
@@ -149,6 +154,16 @@ export async function refreshCoreAssets(): Promise<{
149
154
  updated.push(asset.relPath);
150
155
  }
151
156
 
157
+ // Seed user-editable assets only when missing — never overwrite.
158
+ for (const asset of SEEDED_ASSETS) {
159
+ const targetPath = join(homeDir, asset.relPath);
160
+ if (existsSync(targetPath)) continue;
161
+ const freshContent = await downloadAsset(asset.githubFilename);
162
+ mkdirSync(dirname(targetPath), { recursive: true });
163
+ writeFileSync(targetPath, freshContent);
164
+ updated.push(asset.relPath);
165
+ }
166
+
152
167
  return { backupDir, updated };
153
168
  }
154
169
 
@@ -1,5 +1,5 @@
1
1
  import { parse as dotenvParse } from 'dotenv';
2
- import { readFileSync, existsSync } from 'node:fs';
2
+ import { readFileSync, existsSync, copyFileSync } from 'node:fs';
3
3
 
4
4
  export function parseEnvContent(content: string): Record<string, string> {
5
5
  return dotenvParse(content);
@@ -10,6 +10,9 @@ export function parseEnvFile(filePath: string): Record<string, string> {
10
10
  try {
11
11
  return dotenvParse(readFileSync(filePath, 'utf-8'));
12
12
  } catch {
13
+ // File is unreadable or malformed — back it up before returning empty so
14
+ // the next write doesn't silently discard all existing values.
15
+ try { copyFileSync(filePath, `${filePath}.corrupt-${Date.now()}`); } catch { /* best-effort */ }
13
16
  return {};
14
17
  }
15
18
  }
@@ -300,6 +300,24 @@ export async function performUpgrade(state: ControlPlaneState): Promise<UpgradeR
300
300
  };
301
301
  }
302
302
 
303
+ /**
304
+ * Set a specific image tag in stack.env then pull images and restart containers.
305
+ * Used by the admin "set version" action — skips the auto-detect step in performUpgrade.
306
+ */
307
+ export async function applyTagChange(state: ControlPlaneState, tag: string): Promise<UpgradeResult> {
308
+ const stackEnvPath = `${state.stackDir}/stack.env`;
309
+ const currentContent = existsSync(stackEnvPath) ? readFileSync(stackEnvPath, "utf-8") : "";
310
+ writeFileSync(stackEnvPath, mergeEnvContent(currentContent, { OP_IMAGE_TAG: tag }, { uncomment: true }));
311
+ const upgradeResult = await applyUpgrade(state);
312
+ return {
313
+ imageTag: tag,
314
+ namespace: "openpalm",
315
+ backupDir: upgradeResult.backupDir,
316
+ assetsUpdated: upgradeResult.updated,
317
+ restarted: upgradeResult.restarted,
318
+ };
319
+ }
320
+
303
321
  export function buildComposeFileList(state: ControlPlaneState): string[] {
304
322
  return discoverStackOverlays(state.stackDir);
305
323
  }
@@ -241,11 +241,17 @@ function parseChecksumsFile(content: string): Map<string, string> {
241
241
  return map;
242
242
  }
243
243
 
244
- export async function seedUiBuild(repoRef: string, stateDir: string): Promise<void> {
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> {
245
251
  const uiDir = join(stateDir, 'ui');
246
252
  mkdirSync(uiDir, { recursive: true });
247
253
 
248
- const local = resolveLocalUiBuild();
254
+ const local = options?.forceRemote ? null : resolveLocalUiBuild();
249
255
  if (local) {
250
256
  logger.debug('seeding UI build from local source', { src: local });
251
257
  copyTree(local, uiDir);
@@ -284,6 +290,9 @@ export async function seedUiBuild(repoRef: string, stateDir: string): Promise<vo
284
290
 
285
291
  writeFileSync(tmpTar, tarData);
286
292
 
293
+ // Clear stale files before extracting so old build files don't persist
294
+ rmSync(uiDir, { recursive: true, force: true });
295
+ mkdirSync(uiDir, { recursive: true });
287
296
  // Cross-platform extraction via the `tar` npm package — no shell dependency
288
297
  await tarExtract({ file: tmpTar, cwd: uiDir, strip: 1 });
289
298
  writeFileSync(join(uiDir, 'version.txt'), repoRef.replace(/^v/, ''));
package/src/index.ts CHANGED
@@ -181,6 +181,7 @@ export {
181
181
  applyUninstall,
182
182
  applyUpgrade,
183
183
  performUpgrade,
184
+ applyTagChange,
184
185
  updateStackEnvToLatestImageTag,
185
186
  buildComposeFileList,
186
187
  buildManagedServices,
@@ -308,4 +309,5 @@ export {
308
309
  resolveUiBuildDir,
309
310
  seedUiBuild,
310
311
  checkAndUpdateUiBuild,
312
+ readCurrentUiBuildVersion,
311
313
  } from "./control-plane/ui-assets.js";