openpalm 0.11.0-beta.3 → 0.11.0-beta.6

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",
3
- "version": "0.11.0-beta.3",
3
+ "version": "0.11.0-beta.6",
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
6
  "description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
@@ -16,8 +16,8 @@
16
16
  "start": "bun run src/main.ts",
17
17
  "test": "bun test",
18
18
  "test:e2e": "npx playwright test",
19
- "wizard:dev": "bun run src/main.ts install --no-start --force",
20
- "_build_note": "Run 'bun run ui:build:tar' from repo root before any build:* target (Bun does not run prebuild hooks)",
19
+ "wizard:local": "bun run src/main.ts install --no-start --force",
20
+ "_build_note": "CI runs 'bun run ui:build:tar' from repo root before any build:* target. The prebuild hook below is npm-only; Bun does not run lifecycle hooks.",
21
21
  "prebuild": "cd ../ui && npm run build && npm run build:tar",
22
22
  "build": "bun build src/main.ts --compile --outfile dist/openpalm-cli",
23
23
  "build:linux-x64": "bun build src/main.ts --compile --target=bun-linux-x64 --outfile dist/openpalm-cli-linux-x64",
@@ -27,8 +27,11 @@
27
27
  "build:windows-x64": "bun build src/main.ts --compile --target=bun-windows-x64 --outfile dist/openpalm-cli-windows-x64.exe",
28
28
  "build:windows-arm64": "bun build src/main.ts --compile --target=bun-windows-arm64 --outfile dist/openpalm-cli-windows-arm64.exe"
29
29
  },
30
+ "engines": {
31
+ "bun": ">=1.0.0"
32
+ },
30
33
  "dependencies": {
31
- "@openpalm/lib": "0.11.0-beta.1",
34
+ "@openpalm/lib": ">=0.11.0-beta.5 <1.0.0",
32
35
  "citty": "^0.2.1",
33
36
  "yaml": "^2.8.0"
34
37
  }
package/src/lib/env.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
- import { mkdirSync } from 'node:fs';
3
- import { reconcileStackEnvImageTag, resolveRequestedImageTag } from '@openpalm/lib';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { reconcileStackEnvImageTag, resolveRequestedImageTag, resolveOperatorIds, hasUsableOperatorId } from '@openpalm/lib';
4
4
  import { defaultDockerSock } from './paths.ts';
5
5
 
6
6
  /**
@@ -32,21 +32,25 @@ export async function ensureStackEnv(
32
32
  const explicitImageTag = imageTagOverride ?? process.env.OP_IMAGE_TAG;
33
33
  const hasExplicitImageTag = explicitImageTag !== undefined && explicitImageTag !== '';
34
34
  mkdirSync(stackDir, { recursive: true });
35
+ // Operator UID/GID — auto-detect from OP_HOME owner (or process UID).
36
+ // Returns null on Windows; in that case we omit OP_UID/OP_GID and let
37
+ // compose fall back to its `${OP_UID:-1000}` default (containers run
38
+ // in WSL2 Linux where this doesn't matter).
39
+ const ids = resolveOperatorIds(homeDir);
35
40
  if (!(await Bun.file(systemEnvPath).exists())) {
36
41
  const defaultImageTag = hasExplicitImageTag
37
42
  ? explicitImageTag
38
43
  : (resolveRequestedImageTag(repoRef) || 'latest');
44
+ const idLines = ids ? `OP_UID=${ids.uid}\nOP_GID=${ids.gid}\n` : '';
39
45
  const content = `# OpenPalm System Environment — system-managed, do not edit
40
46
  OP_HOME=${homeDir}
41
47
  OP_WORK_DIR=${workDir}
42
- OP_UID=${process.getuid?.() ?? 1000}
43
- OP_GID=${process.getgid?.() ?? 1000}
44
- OP_IMAGE_NAMESPACE=${process.env.OP_IMAGE_NAMESPACE || 'openpalm'}
48
+ ${idLines}OP_IMAGE_NAMESPACE=${process.env.OP_IMAGE_NAMESPACE || 'openpalm'}
45
49
  OP_IMAGE_TAG=${defaultImageTag}
46
50
  `;
47
51
  await Bun.write(systemEnvPath, content);
48
52
  } else {
49
- const current = await Bun.file(systemEnvPath).text();
53
+ let current = await Bun.file(systemEnvPath).text();
50
54
  const reconciled = reconcileStackEnvImageTag(
51
55
  current,
52
56
  repoRef,
@@ -54,6 +58,54 @@ OP_IMAGE_TAG=${defaultImageTag}
54
58
  );
55
59
  if (reconciled !== current) {
56
60
  await Bun.write(systemEnvPath, reconciled);
61
+ current = reconciled;
57
62
  }
63
+
64
+ // Backfill OP_UID/OP_GID for installs created by older CLI versions
65
+ // that hard-coded 1000. Only fill missing/zero values — never override
66
+ // a value the operator may have set explicitly.
67
+ if (ids) {
68
+ backfillOperatorIds(systemEnvPath, current, ids);
69
+ }
70
+ }
71
+ }
72
+
73
+ function readEnvKey(content: string, key: string): string | undefined {
74
+ // Minimal key=value reader for OP_UID/OP_GID. Stack.env is system-managed
75
+ // and these lines are always plain numerics (no quotes, no interpolation),
76
+ // so this avoids pulling in a full env parser.
77
+ const re = new RegExp(`^\\s*${key}\\s*=\\s*(.*?)\\s*$`, 'm');
78
+ const m = content.match(re);
79
+ return m?.[1];
80
+ }
81
+
82
+ function backfillOperatorIds(
83
+ path: string,
84
+ current: string,
85
+ ids: { uid: number; gid: number },
86
+ ): void {
87
+ const parsed: Record<string, string> = {};
88
+ const uidValue = readEnvKey(current, 'OP_UID');
89
+ const gidValue = readEnvKey(current, 'OP_GID');
90
+ if (uidValue !== undefined) parsed.OP_UID = uidValue;
91
+ if (gidValue !== undefined) parsed.OP_GID = gidValue;
92
+
93
+ const patches: Array<[string, string]> = [];
94
+ if (!hasUsableOperatorId(parsed, 'OP_UID')) patches.push(['OP_UID', String(ids.uid)]);
95
+ if (!hasUsableOperatorId(parsed, 'OP_GID')) patches.push(['OP_GID', String(ids.gid)]);
96
+ if (patches.length === 0) return;
97
+
98
+ let next = current;
99
+ for (const [key, value] of patches) {
100
+ const re = new RegExp(`^${key}=.*$`, 'm');
101
+ if (re.test(next)) {
102
+ next = next.replace(re, `${key}=${value}`);
103
+ } else {
104
+ next = (next.endsWith('\n') ? next : next + '\n') + `${key}=${value}\n`;
105
+ }
106
+ }
107
+ if (next !== current) {
108
+ writeFileSync(path, next);
58
109
  }
59
110
  }
111
+
package/src/main.ts CHANGED
@@ -10,7 +10,7 @@ export type { HostInfo } from './lib/host-info.ts';
10
10
 
11
11
  const SUBCOMMAND_NAMES = new Set([
12
12
  'install', 'uninstall', 'update', 'self-update', 'addon',
13
- 'start', 'stop', 'restart', 'logs', 'status', 'service',
13
+ 'start', 'stop', 'restart', 'logs', 'status',
14
14
  'validate', 'scan', 'rollback', 'automations',
15
15
  '--help', '-h', 'help',
16
16
  ]);
@@ -114,7 +114,6 @@ export const mainCommand = defineCommand({
114
114
  restart: () => import('./commands/restart.ts').then((m) => m.default),
115
115
  logs: () => import('./commands/logs.ts').then((m) => m.default),
116
116
  status: () => import('./commands/status.ts').then((m) => m.default),
117
- service: () => import('./commands/service.ts').then((m) => m.default),
118
117
  validate: () => import('./commands/validate.ts').then((m) => m.default),
119
118
  scan: () => import('./commands/scan.ts').then((m) => m.default),
120
119
  rollback: () => import('./commands/rollback.ts').then((m) => m.default),
@@ -1,76 +0,0 @@
1
- import { defineCommand } from 'citty';
2
- import { buildManagedServices } from '@openpalm/lib';
3
- import { ensureValidState } from '../lib/cli-state.ts';
4
- import { runComposeWithPreflight, runComposeReadOnly } from '../lib/cli-compose.ts';
5
- import { runLogsAction } from './logs.ts';
6
- import { runStartAction } from './start.ts';
7
- import { runStopAction } from './stop.ts';
8
- import { runRestartAction } from './restart.ts';
9
-
10
- const startCmd = defineCommand({
11
- meta: { name: 'start', description: 'Start services' },
12
- args: {
13
- services: { type: 'positional', description: 'Service names', required: false },
14
- },
15
- async run({ args }) { await runStartAction(args._ ?? []); },
16
- });
17
-
18
- const stopCmd = defineCommand({
19
- meta: { name: 'stop', description: 'Stop services' },
20
- args: {
21
- services: { type: 'positional', description: 'Service names', required: false },
22
- },
23
- async run({ args }) { await runStopAction(args._ ?? []); },
24
- });
25
-
26
- const restartCmd = defineCommand({
27
- meta: { name: 'restart', description: 'Restart services' },
28
- args: {
29
- services: { type: 'positional', description: 'Service names', required: false },
30
- },
31
- async run({ args }) { await runRestartAction(args._ ?? []); },
32
- });
33
-
34
- const logsCmd = defineCommand({
35
- meta: { name: 'logs', description: 'View service logs' },
36
- args: {
37
- services: { type: 'positional', description: 'Service names', required: false },
38
- },
39
- async run({ args }) { await runLogsAction(args._ ?? []); },
40
- });
41
-
42
- const updateCmd = defineCommand({
43
- meta: { name: 'update', description: 'Pull latest images' },
44
- async run() {
45
- const state = ensureValidState();
46
- const managedServices = await buildManagedServices(state);
47
- console.log('Pulling latest images...');
48
- await runComposeWithPreflight(state, ['pull', ...managedServices]);
49
- console.log('Recreating containers...');
50
- await runComposeWithPreflight(state, ['up', '-d', '--force-recreate', ...managedServices]);
51
- console.log('Update complete.');
52
- },
53
- });
54
-
55
- const statusCmd = defineCommand({
56
- meta: { name: 'status', description: 'Show container status' },
57
- async run() {
58
- const state = ensureValidState();
59
- await runComposeReadOnly(state, ['ps', '--format', 'table']);
60
- },
61
- });
62
-
63
- export default defineCommand({
64
- meta: {
65
- name: 'service',
66
- description: 'Service lifecycle operations (start|stop|restart|logs|update|status)',
67
- },
68
- subCommands: {
69
- start: startCmd,
70
- stop: stopCmd,
71
- restart: restartCmd,
72
- logs: logsCmd,
73
- update: updateCmd,
74
- status: statusCmd,
75
- },
76
- });