openpalm 0.9.7 → 0.9.9

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.
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest, getServiceNames } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
5
4
 
@@ -23,25 +22,7 @@ export default defineCommand({
23
22
 
24
23
  export async function runRestartAction(services: string[]): Promise<void> {
25
24
  if (services.length === 0) {
26
- // Try admin delegation first
27
- const adminResult = await tryAdminRequest('/admin/containers/list');
28
- if (adminResult !== null) {
29
- const serviceNames = getServiceNames(adminResult);
30
- for (const service of serviceNames) {
31
- const result = await tryAdminRequest('/admin/containers/restart', {
32
- method: 'POST',
33
- body: JSON.stringify({ service }),
34
- });
35
- if (result !== null) {
36
- console.log(JSON.stringify(result, null, 2));
37
- } else {
38
- console.warn(`Warning: failed to restart ${service} via admin API`);
39
- }
40
- }
41
- return;
42
- }
43
-
44
- // Direct compose restart
25
+ // Restart all managed services (admin included if enabled)
45
26
  const state = await ensureStagedState();
46
27
  const managedServices = buildManagedServiceNames(state);
47
28
  await runDockerCompose([...fullComposeArgs(state), 'restart', ...managedServices]);
@@ -50,22 +31,8 @@ export async function runRestartAction(services: string[]): Promise<void> {
50
31
 
51
32
  // Restart specific services
52
33
  for (const service of services) {
53
- const adminResult = await tryAdminRequest('/admin/containers/restart', {
54
- method: 'POST',
55
- body: JSON.stringify({ service }),
56
- });
57
- if (adminResult !== null) {
58
- console.log(JSON.stringify(adminResult, null, 2));
59
- continue;
60
- }
61
-
62
- // Direct compose restart for specific service
63
34
  const state = await ensureStagedState();
64
35
  const composeArgs = fullComposeArgs(state);
65
- if (service === 'admin' || service === 'docker-socket-proxy') {
66
- await runDockerCompose([...composeArgs, '--profile', 'admin', 'restart', service]);
67
- } else {
68
- await runDockerCompose([...composeArgs, 'restart', service]);
69
- }
36
+ await runDockerCompose([...composeArgs, 'restart', service]);
70
37
  }
71
38
  }
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
5
4
  import { runLogsAction } from './logs.ts';
@@ -42,14 +41,6 @@ const logsCmd = defineCommand({
42
41
  const updateCmd = defineCommand({
43
42
  meta: { name: 'update', description: 'Pull latest images' },
44
43
  async run() {
45
- // Try admin delegation first
46
- const adminResult = await tryAdminRequest('/admin/containers/pull', { method: 'POST' });
47
- if (adminResult !== null) {
48
- console.log(JSON.stringify(adminResult, null, 2));
49
- return;
50
- }
51
-
52
- // Direct compose pull + recreate (scoped to managed services)
53
44
  const state = await ensureStagedState();
54
45
  const composeArgs = fullComposeArgs(state);
55
46
  const managedServices = buildManagedServiceNames(state);
@@ -64,14 +55,6 @@ const updateCmd = defineCommand({
64
55
  const statusCmd = defineCommand({
65
56
  meta: { name: 'status', description: 'Show container status' },
66
57
  async run() {
67
- // Try admin delegation first
68
- const adminResult = await tryAdminRequest('/admin/containers/list');
69
- if (adminResult !== null) {
70
- console.log(JSON.stringify(adminResult, null, 2));
71
- return;
72
- }
73
-
74
- // Direct compose ps
75
58
  const state = await ensureStagedState();
76
59
  await runDockerCompose([...fullComposeArgs(state), 'ps', '--format', 'table']);
77
60
  },
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
5
4
 
@@ -14,66 +13,29 @@ export default defineCommand({
14
13
  description: 'Service names to start (omit for all)',
15
14
  required: false,
16
15
  },
17
- 'with-admin': {
18
- type: 'boolean',
19
- description: 'Include admin UI and docker-socket-proxy (use --no-with-admin to skip)',
20
- default: true,
21
- },
22
16
  },
23
17
  async run({ args }) {
24
18
  const services = args._ ?? [];
25
- const withAdmin = args['with-admin'] ?? false;
26
- await runStartAction(services, { withAdmin });
19
+ await runStartAction(services);
27
20
  },
28
21
  });
29
22
 
30
23
  export async function runStartAction(
31
24
  services: string[],
32
- opts?: { withAdmin?: boolean },
33
25
  ): Promise<void> {
34
- const withAdmin = opts?.withAdmin ?? false;
35
-
36
26
  if (services.length === 0) {
37
- // Try admin delegation first (gives admin's scheduler/audit a chance to observe)
38
- const adminResult = await tryAdminRequest('/admin/install', { method: 'POST' });
39
- if (adminResult !== null) {
40
- console.log(JSON.stringify(adminResult, null, 2));
41
- return;
42
- }
43
-
44
- // Direct compose — stage artifacts and start managed services
27
+ // Stage artifacts and start all managed services (admin included if enabled)
45
28
  const state = await ensureStagedState();
46
29
  const composeArgs = fullComposeArgs(state);
47
30
  const managedServices = buildManagedServiceNames(state);
48
-
49
- if (withAdmin) {
50
- // Include the admin profile — starts admin + docker-socket-proxy
51
- await runDockerCompose([...composeArgs, '--profile', 'admin', 'up', '-d', ...managedServices, 'admin', 'docker-socket-proxy']);
52
- } else {
53
- await runDockerCompose([...composeArgs, 'up', '-d', ...managedServices]);
54
- }
31
+ await runDockerCompose([...composeArgs, 'up', '-d', ...managedServices]);
55
32
  return;
56
33
  }
57
34
 
58
- // Start specific services — try admin first, fall back to direct compose
35
+ // Start specific services
59
36
  for (const service of services) {
60
- const adminResult = await tryAdminRequest('/admin/containers/up', {
61
- method: 'POST',
62
- body: JSON.stringify({ service }),
63
- });
64
- if (adminResult !== null) {
65
- console.log(JSON.stringify(adminResult, null, 2));
66
- continue;
67
- }
68
-
69
- // Direct compose for specific service
70
37
  const state = await ensureStagedState();
71
38
  const composeArgs = fullComposeArgs(state);
72
- // If starting admin explicitly, include the admin profile
73
- if (service === 'admin' || service === 'docker-socket-proxy') {
74
- await runDockerCompose([...composeArgs, '--profile', 'admin', 'up', '-d', service]);
75
- } else {
76
- await runDockerCompose([...composeArgs, 'up', '-d', service]);
77
- }
39
+ await runDockerCompose([...composeArgs, 'up', '-d', service]);
78
40
  }
79
41
  }
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
5
4
 
@@ -9,14 +8,6 @@ export default defineCommand({
9
8
  description: 'Show container status',
10
9
  },
11
10
  async run() {
12
- // Try admin delegation first for richer output
13
- const adminResult = await tryAdminRequest('/admin/containers/list');
14
- if (adminResult !== null) {
15
- console.log(JSON.stringify(adminResult, null, 2));
16
- return;
17
- }
18
-
19
- // Direct compose ps
20
11
  const state = await ensureStagedState();
21
12
  await runDockerCompose([...fullComposeArgs(state), 'ps', '--format', 'table']);
22
13
  },
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest, getServiceNames } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
5
4
 
@@ -23,48 +22,17 @@ export default defineCommand({
23
22
 
24
23
  export async function runStopAction(services: string[]): Promise<void> {
25
24
  if (services.length === 0) {
26
- // Try admin delegation stop each managed container
27
- const adminResult = await tryAdminRequest('/admin/containers/list');
28
- if (adminResult !== null) {
29
- const serviceNames = getServiceNames(adminResult);
30
- for (const service of serviceNames) {
31
- const result = await tryAdminRequest('/admin/containers/down', {
32
- method: 'POST',
33
- body: JSON.stringify({ service }),
34
- });
35
- if (result !== null) {
36
- console.log(JSON.stringify(result, null, 2));
37
- } else {
38
- console.warn(`Warning: failed to stop ${service} via admin API`);
39
- }
40
- }
41
- return;
42
- }
43
-
44
- // Direct compose down — include admin profile to tear down all services
25
+ // Compose file list includes admin.yml when admin is enabled,
26
+ // so `down` tears down all services including admin/caddy/socket-proxy.
45
27
  const state = await ensureStagedState();
46
- await runDockerCompose([...fullComposeArgs(state), '--profile', 'admin', 'down']);
28
+ await runDockerCompose([...fullComposeArgs(state), 'down']);
47
29
  return;
48
30
  }
49
31
 
50
32
  // Stop specific services
51
33
  for (const service of services) {
52
- const adminResult = await tryAdminRequest('/admin/containers/down', {
53
- method: 'POST',
54
- body: JSON.stringify({ service }),
55
- });
56
- if (adminResult !== null) {
57
- console.log(JSON.stringify(adminResult, null, 2));
58
- continue;
59
- }
60
-
61
- // Direct compose stop for specific service
62
34
  const state = await ensureStagedState();
63
35
  const composeArgs = fullComposeArgs(state);
64
- if (service === 'admin' || service === 'docker-socket-proxy') {
65
- await runDockerCompose([...composeArgs, '--profile', 'admin', 'stop', service]);
66
- } else {
67
- await runDockerCompose([...composeArgs, 'stop', service]);
68
- }
36
+ await runDockerCompose([...composeArgs, 'stop', service]);
69
37
  }
70
38
  }
@@ -1,7 +1,8 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest } from '../lib/admin.ts';
2
+ import { rmSync } from 'node:fs';
3
3
  import { runDockerCompose } from '../lib/docker.ts';
4
4
  import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
5
+ import { resolveConfigHome, resolveDataHome, resolveStateHome } from '@openpalm/lib';
5
6
 
6
7
  export default defineCommand({
7
8
  meta: {
@@ -14,24 +15,32 @@ export default defineCommand({
14
15
  description: 'Also remove Docker volumes',
15
16
  default: false,
16
17
  },
18
+ purge: {
19
+ type: 'boolean',
20
+ description: 'Remove all OpenPalm XDG directories (config, data, state)',
21
+ default: false,
22
+ },
17
23
  },
18
24
  async run({ args }) {
19
- // Try admin delegation first
20
- const adminResult = await tryAdminRequest('/admin/uninstall', { method: 'POST' });
21
- if (adminResult !== null) {
22
- console.log(JSON.stringify(adminResult, null, 2));
23
- return;
24
- }
25
-
26
- // Direct compose down — include admin profile to tear down all services
25
+ // Compose file list includes admin.yml when admin is enabled,
26
+ // so `down` tears down all services including admin/caddy/socket-proxy.
27
27
  const state = await ensureStagedState();
28
28
  const composeArgs = fullComposeArgs(state);
29
- const downArgs = args.volumes ? ['down', '-v'] : ['down'];
30
- await runDockerCompose([...composeArgs, '--profile', 'admin', ...downArgs]);
29
+ const downArgs = args.volumes || args.purge ? ['down', '-v'] : ['down'];
30
+ await runDockerCompose([...composeArgs, ...downArgs]);
31
31
 
32
- console.log('OpenPalm stack stopped and removed.');
33
- if (!args.volumes) {
34
- console.log('Config and data directories are preserved. Use --volumes to also remove Docker volumes.');
32
+ if (args.purge) {
33
+ const dirs = [resolveConfigHome(), resolveDataHome(), resolveStateHome()];
34
+ for (const dir of dirs) {
35
+ console.log(`Removing ${dir}`);
36
+ rmSync(dir, { recursive: true, force: true });
37
+ }
38
+ console.log('OpenPalm stack and all data removed.');
39
+ } else {
40
+ console.log('OpenPalm stack stopped and removed.');
41
+ if (!args.volumes) {
42
+ console.log('Config and data directories are preserved. Use --purge to remove everything.');
43
+ }
35
44
  }
36
45
  },
37
46
  });
@@ -1,5 +1,4 @@
1
1
  import { defineCommand } from 'citty';
2
- import { tryAdminRequest } from '../lib/admin.ts';
3
2
  import { runDockerCompose } from '../lib/docker.ts';
4
3
  import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
5
4
 
@@ -9,14 +8,6 @@ export default defineCommand({
9
8
  description: 'Pull latest images and recreate containers',
10
9
  },
11
10
  async run() {
12
- // Try admin delegation first
13
- const adminResult = await tryAdminRequest('/admin/containers/pull', { method: 'POST' });
14
- if (adminResult !== null) {
15
- console.log(JSON.stringify(adminResult, null, 2));
16
- return;
17
- }
18
-
19
- // Direct compose: pull + recreate
20
11
  const state = await ensureStagedState();
21
12
  const composeArgs = fullComposeArgs(state);
22
13
  const managedServices = buildManagedServiceNames(state);
package/src/lib/docker.ts CHANGED
@@ -27,6 +27,23 @@ export async function ensureDirectoryTree(
27
27
  }
28
28
  }
29
29
 
30
+ /**
31
+ * Fetches a URL with retries and exponential backoff. Only retries on 5xx or network errors.
32
+ */
33
+ async function fetchWithRetry(url: string, retries = 3): Promise<Response> {
34
+ for (let i = 0; i < retries; i++) {
35
+ try {
36
+ const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
37
+ if (res.ok || res.status < 500) return res;
38
+ if (i < retries - 1) await Bun.sleep(200 * 2 ** i);
39
+ } catch (err) {
40
+ if (i === retries - 1) throw err;
41
+ await Bun.sleep(200 * 2 ** i);
42
+ }
43
+ }
44
+ throw new Error(`Failed to fetch ${url} after ${retries} attempts`);
45
+ }
46
+
30
47
  /**
31
48
  * Downloads an asset from a GitHub release, falling back to raw.githubusercontent.com.
32
49
  */
@@ -34,15 +51,15 @@ export async function fetchAsset(repoRef: string, filename: string): Promise<str
34
51
  const releaseUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${repoRef}/${filename}`;
35
52
  const rawUrl = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${repoRef}/assets/${filename}`;
36
53
 
37
- const releaseResponse = await fetch(releaseUrl, { signal: AbortSignal.timeout(30000) });
38
- if (releaseResponse.ok) {
39
- return await releaseResponse.text();
54
+ try {
55
+ const releaseResponse = await fetchWithRetry(releaseUrl);
56
+ if (releaseResponse.ok) return await releaseResponse.text();
57
+ } catch {
58
+ // Fall through to raw URL
40
59
  }
41
60
 
42
- const rawResponse = await fetch(rawUrl, { signal: AbortSignal.timeout(30000) });
43
- if (rawResponse.ok) {
44
- return await rawResponse.text();
45
- }
61
+ const rawResponse = await fetchWithRetry(rawUrl);
62
+ if (rawResponse.ok) return await rawResponse.text();
46
63
 
47
64
  throw new Error(`Failed to download ${filename} from ${repoRef}`);
48
65
  }
@@ -90,6 +107,7 @@ export async function runDockerComposeCapture(args: string[]): Promise<string> {
90
107
  * Opens a URL in the user's default browser. Best-effort, never throws.
91
108
  */
92
109
  export async function openBrowser(url: string): Promise<void> {
110
+ console.log(`Opening ${url} in your browser...`);
93
111
  const platform = process.platform;
94
112
  try {
95
113
  if (platform === 'darwin') {
package/src/lib/env.ts CHANGED
@@ -1,62 +1,7 @@
1
- import { basename, dirname, join } from 'node:path';
2
- import { defaultConfigHome, defaultDockerSock } from './paths.ts';
3
-
4
- const EXPORT_ENV_PREFIX = 'export ';
5
-
6
- /**
7
- * Loads the admin token from environment variables or secrets.env file.
8
- * Checks OPENPALM_ADMIN_TOKEN first, then ADMIN_TOKEN (legacy), then reads from
9
- * CONFIG_HOME/secrets.env. Returns empty string if no token is found.
10
- */
11
- export async function loadAdminToken(): Promise<string> {
12
- if (process.env.OPENPALM_ADMIN_TOKEN) {
13
- return process.env.OPENPALM_ADMIN_TOKEN;
14
- }
15
-
16
- if (process.env.ADMIN_TOKEN) {
17
- return process.env.ADMIN_TOKEN;
18
- }
19
-
20
- const configHome = defaultConfigHome();
21
- const secretsPaths = [join(configHome, 'secrets.env')];
22
- if (basename(configHome) === 'openpalm') {
23
- secretsPaths.push(join(dirname(configHome), 'secrets.env'));
24
- }
25
-
26
- for (const secretsPath of secretsPaths) {
27
- const token = await readTokenFromFile(secretsPath, 'OPENPALM_ADMIN_TOKEN');
28
- if (token) return token;
29
- const legacyToken = await readTokenFromFile(secretsPath, 'ADMIN_TOKEN');
30
- if (legacyToken) return legacyToken;
31
- }
32
-
33
- return '';
34
- }
35
-
36
- /**
37
- * Reads a specific key from an env file. Handles `export` prefix and quoted values.
38
- */
39
- async function readTokenFromFile(secretsPath: string, key: string): Promise<string | null> {
40
- try {
41
- const text = await Bun.file(secretsPath).text();
42
- for (const rawLine of text.split('\n')) {
43
- const line = rawLine.trim();
44
- if (!line || line.startsWith('#')) continue;
45
- const lineWithoutExportPrefix = line.startsWith(EXPORT_ENV_PREFIX)
46
- ? line.slice(EXPORT_ENV_PREFIX.length).trimStart()
47
- : line;
48
- const [lineKey, ...rest] = lineWithoutExportPrefix.split('=');
49
- if (lineKey !== key) continue;
50
- const value = unwrapQuotedEnvValue(rest.join('=').trim());
51
- if (!value) return null;
52
- return value;
53
- }
54
- } catch {
55
- // Best effort only.
56
- }
57
-
58
- return null;
59
- }
1
+ import { join, dirname } from 'node:path';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { defaultDockerSock } from './paths.ts';
60
5
 
61
6
  export function unwrapQuotedEnvValue(value: string): string {
62
7
  const isDoubleQuoted = value.startsWith('"') && value.endsWith('"');
@@ -143,6 +88,7 @@ export OPENAI_BASE_URL=
143
88
 
144
89
  # Memory
145
90
  export MEMORY_USER_ID=${userId}
91
+ export MEMORY_AUTH_TOKEN=${randomBytes(32).toString('hex')}
146
92
  `;
147
93
 
148
94
  await Bun.write(secretsPath, content);
@@ -189,6 +135,7 @@ OPENPALM_IMAGE_TAG=${defaultImageTag}
189
135
  await Bun.write(dataStackEnv, reconciled);
190
136
  }
191
137
  }
138
+ mkdirSync(dirname(stagedStackEnv), { recursive: true });
192
139
  await Bun.write(stagedStackEnv, Bun.file(dataStackEnv));
193
140
 
194
141
  const stateSecrets = join(stateHome, 'artifacts', 'secrets.env');
package/src/lib/paths.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Path resolution — re-exports from @openpalm/lib with CLI-specific additions.
3
3
  */
4
+ import { existsSync } from 'node:fs';
4
5
  import { homedir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
  import {
@@ -19,7 +20,16 @@ export { resolveStateHome as defaultStateHome };
19
20
  // CLI-specific paths (not in lib)
20
21
  export function defaultDockerSock(): string {
21
22
  if (process.env.OPENPALM_DOCKER_SOCK) return process.env.OPENPALM_DOCKER_SOCK;
22
- return IS_WINDOWS ? '//./pipe/docker_engine' : '/var/run/docker.sock';
23
+ if (IS_WINDOWS) return '//./pipe/docker_engine';
24
+
25
+ const home = homedir();
26
+ const candidates = [
27
+ '/var/run/docker.sock',
28
+ join(home, '.orbstack/run/docker.sock'),
29
+ join(home, '.colima/default/docker.sock'),
30
+ join(home, '.rd/docker.sock'),
31
+ ];
32
+ return candidates.find((p) => existsSync(p)) ?? candidates[0];
23
33
  }
24
34
 
25
35
  export function defaultWorkDir(): string {
@@ -22,9 +22,9 @@ import { defaultDataHome } from './paths.ts';
22
22
  /**
23
23
  * Ensure all artifacts are staged from CONFIG_HOME/DATA_HOME to STATE_HOME.
24
24
  *
25
- * This is the CLI-side equivalent of the admin's staging pipeline.
26
- * It uses FilesystemAssetProvider (reads core assets from DATA_HOME,
27
- * persisted by the install command) rather than Vite bundle imports.
25
+ * Uses FilesystemAssetProvider (reads core assets from DATA_HOME,
26
+ * persisted by the install command) to assemble compose files, env
27
+ * files, and Caddyfile into STATE_HOME/artifacts for Docker Compose.
28
28
  *
29
29
  * Returns a ControlPlaneState usable with fullComposeArgs().
30
30
  */