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.
- package/bin/openpalm.js +5 -0
- package/package.json +4 -2
- package/playwright.config.ts +16 -0
- package/src/commands/install-file.test.ts +306 -0
- package/src/commands/install-services.test.ts +12 -7
- package/src/commands/install-services.ts +1 -1
- package/src/commands/install.ts +113 -30
- package/src/commands/restart.ts +2 -35
- package/src/commands/service.ts +0 -17
- package/src/commands/start.ts +5 -43
- package/src/commands/status.ts +0 -9
- package/src/commands/stop.ts +4 -36
- package/src/commands/uninstall.ts +23 -14
- package/src/commands/update.ts +0 -9
- package/src/lib/docker.ts +25 -7
- package/src/lib/env.ts +6 -59
- package/src/lib/paths.ts +11 -1
- package/src/lib/staging.ts +3 -3
- package/src/main.test.ts +67 -83
- package/src/main.ts +1 -1
- package/src/setup-wizard/index.html +114 -180
- package/src/setup-wizard/server-errors.test.ts +429 -0
- package/src/setup-wizard/server-integration.test.ts +511 -0
- package/src/setup-wizard/server.test.ts +6 -6
- package/src/setup-wizard/server.ts +17 -5
- package/src/setup-wizard/standalone.ts +166 -0
- package/src/setup-wizard/wizard.css +892 -299
- package/src/setup-wizard/wizard.js +1172 -559
- package/src/lib/admin.ts +0 -107
package/src/commands/restart.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/service.ts
CHANGED
|
@@ -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
|
},
|
package/src/commands/start.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
},
|
package/src/commands/stop.ts
CHANGED
|
@@ -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
|
-
//
|
|
27
|
-
|
|
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), '
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
20
|
-
|
|
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,
|
|
29
|
+
const downArgs = args.volumes || args.purge ? ['down', '-v'] : ['down'];
|
|
30
|
+
await runDockerCompose([...composeArgs, ...downArgs]);
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
});
|
package/src/commands/update.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 {
|
package/src/lib/staging.ts
CHANGED
|
@@ -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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
*/
|