openpalm 0.9.4 → 0.9.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/README.md +40 -12
- package/e2e/start-wizard-server.ts +64 -0
- package/package.json +2 -1
- package/src/commands/install.ts +129 -19
- package/src/commands/logs.ts +4 -15
- package/src/commands/restart.ts +39 -11
- package/src/commands/service.ts +29 -3
- package/src/commands/start.ts +47 -16
- package/src/commands/status.ts +13 -2
- package/src/commands/stop.ts +36 -11
- package/src/commands/uninstall.ts +28 -3
- package/src/commands/update.ts +22 -2
- package/src/lib/admin.ts +27 -2
- package/src/lib/docker.ts +27 -100
- package/src/lib/paths.ts +13 -23
- package/src/lib/staging.ts +72 -0
- package/src/main.test.ts +7 -2
- package/src/setup-wizard/index.html +349 -0
- package/src/setup-wizard/server.test.ts +347 -0
- package/src/setup-wizard/server.ts +297 -0
- package/src/setup-wizard/wizard.css +952 -0
- package/src/setup-wizard/wizard.js +1104 -0
package/src/commands/stop.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { tryAdminRequest, getServiceNames } from '../lib/admin.ts';
|
|
3
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
4
|
+
import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
|
|
5
5
|
|
|
6
6
|
export default defineCommand({
|
|
7
7
|
meta: {
|
|
@@ -23,23 +23,48 @@ export default defineCommand({
|
|
|
23
23
|
|
|
24
24
|
export async function runStopAction(services: string[]): Promise<void> {
|
|
25
25
|
if (services.length === 0) {
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
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
|
+
}
|
|
30
41
|
return;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
// Direct
|
|
34
|
-
await
|
|
44
|
+
// Direct compose down — include admin profile to tear down all services
|
|
45
|
+
const state = await ensureStagedState();
|
|
46
|
+
await runDockerCompose([...fullComposeArgs(state), '--profile', 'admin', 'down']);
|
|
35
47
|
return;
|
|
36
48
|
}
|
|
37
49
|
|
|
50
|
+
// Stop specific services
|
|
38
51
|
for (const service of services) {
|
|
39
|
-
const
|
|
52
|
+
const adminResult = await tryAdminRequest('/admin/containers/down', {
|
|
40
53
|
method: 'POST',
|
|
41
54
|
body: JSON.stringify({ service }),
|
|
42
55
|
});
|
|
43
|
-
|
|
56
|
+
if (adminResult !== null) {
|
|
57
|
+
console.log(JSON.stringify(adminResult, null, 2));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Direct compose stop for specific service
|
|
62
|
+
const state = await ensureStagedState();
|
|
63
|
+
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
|
+
}
|
|
44
69
|
}
|
|
45
70
|
}
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
2
|
+
import { tryAdminRequest } from '../lib/admin.ts';
|
|
3
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
4
|
+
import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
|
|
3
5
|
|
|
4
6
|
export default defineCommand({
|
|
5
7
|
meta: {
|
|
6
8
|
name: 'uninstall',
|
|
7
9
|
description: 'Stop and remove the OpenPalm stack (preserves config and data)',
|
|
8
10
|
},
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
args: {
|
|
12
|
+
volumes: {
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
description: 'Also remove Docker volumes',
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
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
|
|
27
|
+
const state = await ensureStagedState();
|
|
28
|
+
const composeArgs = fullComposeArgs(state);
|
|
29
|
+
const downArgs = args.volumes ? ['down', '-v'] : ['down'];
|
|
30
|
+
await runDockerCompose([...composeArgs, '--profile', 'admin', ...downArgs]);
|
|
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.');
|
|
35
|
+
}
|
|
11
36
|
},
|
|
12
37
|
});
|
package/src/commands/update.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
2
|
+
import { tryAdminRequest } from '../lib/admin.ts';
|
|
3
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
4
|
+
import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
|
|
3
5
|
|
|
4
6
|
export default defineCommand({
|
|
5
7
|
meta: {
|
|
@@ -7,6 +9,24 @@ export default defineCommand({
|
|
|
7
9
|
description: 'Pull latest images and recreate containers',
|
|
8
10
|
},
|
|
9
11
|
async run() {
|
|
10
|
-
|
|
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
|
+
const state = await ensureStagedState();
|
|
21
|
+
const composeArgs = fullComposeArgs(state);
|
|
22
|
+
const managedServices = buildManagedServiceNames(state);
|
|
23
|
+
|
|
24
|
+
console.log('Pulling latest images...');
|
|
25
|
+
await runDockerCompose([...composeArgs, 'pull', ...managedServices]);
|
|
26
|
+
|
|
27
|
+
console.log('Recreating containers...');
|
|
28
|
+
await runDockerCompose([...composeArgs, 'up', '-d', '--force-recreate', ...managedServices]);
|
|
29
|
+
|
|
30
|
+
console.log('Update complete.');
|
|
11
31
|
},
|
|
12
32
|
});
|
package/src/lib/admin.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const ADMIN_URL = process.env.OPENPALM_ADMIN_API_URL || 'http://localhost
|
|
|
5
5
|
/**
|
|
6
6
|
* Returns true if the admin health endpoint is reachable.
|
|
7
7
|
*/
|
|
8
|
-
export async function
|
|
8
|
+
export async function isAdminReachable(): Promise<boolean> {
|
|
9
9
|
try {
|
|
10
10
|
const response = await fetch(`${ADMIN_URL}/health`, {
|
|
11
11
|
method: 'GET',
|
|
@@ -17,6 +17,11 @@ export async function isStackRunning(): Promise<boolean> {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated Use isAdminReachable() instead. Kept for backward compatibility.
|
|
22
|
+
*/
|
|
23
|
+
export const isStackRunning = isAdminReachable;
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Makes an authenticated request to the admin API.
|
|
22
27
|
* Throws if the response is not ok.
|
|
@@ -53,13 +58,33 @@ export async function adminRequest(path: string, init?: RequestInit): Promise<un
|
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Try to delegate an operation to the admin API.
|
|
63
|
+
* Returns the result if admin is reachable and has a valid token.
|
|
64
|
+
* Returns null if admin is unreachable or no token is available.
|
|
65
|
+
* Attempts the request directly — no separate health check round-trip.
|
|
66
|
+
*/
|
|
67
|
+
export async function tryAdminRequest(path: string, init?: RequestInit): Promise<unknown | null> {
|
|
68
|
+
const token = await loadAdminToken();
|
|
69
|
+
if (!token) return null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await adminRequest(path, {
|
|
73
|
+
...init,
|
|
74
|
+
signal: init?.signal ?? AbortSignal.timeout(10_000),
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/**
|
|
57
82
|
* Waits for the admin health endpoint to become healthy (up to 120s).
|
|
58
83
|
*/
|
|
59
84
|
export async function waitForAdminHealthy(): Promise<void> {
|
|
60
85
|
const started = Date.now();
|
|
61
86
|
while (Date.now() - started < 120_000) {
|
|
62
|
-
if (await
|
|
87
|
+
if (await isAdminReachable()) {
|
|
63
88
|
return;
|
|
64
89
|
}
|
|
65
90
|
await Bun.sleep(3000);
|
package/src/lib/docker.ts
CHANGED
|
@@ -1,45 +1,28 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { ensureXdgDirs } from '@openpalm/lib';
|
|
4
4
|
|
|
5
5
|
const REPO_OWNER = 'itlackey';
|
|
6
6
|
const REPO_NAME = 'openpalm';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates the full XDG directory tree required by the stack.
|
|
10
|
+
* Delegates to @openpalm/lib for core dirs, then adds CLI-specific extras.
|
|
10
11
|
*/
|
|
11
12
|
export async function ensureDirectoryTree(
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
_configHome: string,
|
|
14
|
+
_dataHome: string,
|
|
14
15
|
stateHome: string,
|
|
15
16
|
workDir: string,
|
|
16
17
|
): Promise<void> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
dataHome,
|
|
23
|
-
join(dataHome, 'admin'),
|
|
24
|
-
join(dataHome, 'memory'),
|
|
25
|
-
join(dataHome, 'assistant'),
|
|
26
|
-
join(dataHome, 'guardian'),
|
|
27
|
-
join(dataHome, 'caddy'),
|
|
28
|
-
join(dataHome, 'caddy', 'data'),
|
|
29
|
-
join(dataHome, 'caddy', 'config'),
|
|
30
|
-
join(dataHome, 'automations'),
|
|
31
|
-
join(dataHome, 'opencode'),
|
|
32
|
-
stateHome,
|
|
33
|
-
join(stateHome, 'artifacts'),
|
|
34
|
-
join(stateHome, 'audit'),
|
|
35
|
-
join(stateHome, 'artifacts', 'channels'),
|
|
36
|
-
join(stateHome, 'automations'),
|
|
37
|
-
join(stateHome, 'opencode'),
|
|
18
|
+
// Core XDG dirs (CONFIG_HOME, DATA_HOME, STATE_HOME subtrees)
|
|
19
|
+
ensureXdgDirs();
|
|
20
|
+
|
|
21
|
+
// CLI-specific extras not in lib
|
|
22
|
+
for (const dir of [
|
|
38
23
|
join(stateHome, 'bin'),
|
|
39
24
|
workDir,
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
for (const dir of dirs) {
|
|
25
|
+
]) {
|
|
43
26
|
await mkdir(dir, { recursive: true });
|
|
44
27
|
}
|
|
45
28
|
}
|
|
@@ -80,84 +63,28 @@ export async function runDockerCompose(args: string[]): Promise<void> {
|
|
|
80
63
|
}
|
|
81
64
|
|
|
82
65
|
/**
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
export function composeProjectArgs(): string[] {
|
|
86
|
-
const stateHome = defaultStateHome();
|
|
87
|
-
const configHome = defaultConfigHome();
|
|
88
|
-
return [
|
|
89
|
-
'--project-name',
|
|
90
|
-
'openpalm',
|
|
91
|
-
'-f',
|
|
92
|
-
join(stateHome, 'artifacts', 'docker-compose.yml'),
|
|
93
|
-
'--env-file',
|
|
94
|
-
join(configHome, 'secrets.env'),
|
|
95
|
-
'--env-file',
|
|
96
|
-
join(stateHome, 'artifacts', 'stack.env'),
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Ensures the opencode config and system config directories exist with defaults.
|
|
66
|
+
* Runs a `docker compose` command and captures stdout as a string.
|
|
67
|
+
* Throws on non-zero exit.
|
|
102
68
|
*/
|
|
103
|
-
export async function
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function writeIfChanged(path: string, content: string): Promise<void> {
|
|
115
|
-
const file = Bun.file(path);
|
|
116
|
-
if (await file.exists()) {
|
|
117
|
-
const existing = await file.text();
|
|
118
|
-
if (existing === content) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
69
|
+
export async function runDockerComposeCapture(args: string[]): Promise<string> {
|
|
70
|
+
const proc = Bun.spawn(['docker', 'compose', ...args], {
|
|
71
|
+
stdout: 'pipe',
|
|
72
|
+
stderr: 'inherit',
|
|
73
|
+
stdin: 'inherit',
|
|
74
|
+
});
|
|
75
|
+
const output = await new Response(proc.stdout).text();
|
|
76
|
+
const code = await proc.exited;
|
|
77
|
+
if (code !== 0) {
|
|
78
|
+
throw new Error(`docker compose ${args.join(' ')} failed with exit code ${code}`);
|
|
121
79
|
}
|
|
122
|
-
|
|
80
|
+
return output;
|
|
123
81
|
}
|
|
124
82
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await mkdir(opencodeSystemDir, { recursive: true });
|
|
128
|
-
|
|
129
|
-
const systemConfig = join(opencodeSystemDir, 'opencode.jsonc');
|
|
130
|
-
const systemConfigContent =
|
|
131
|
-
JSON.stringify(
|
|
132
|
-
{
|
|
133
|
-
"$schema": "https://opencode.ai/config.json",
|
|
134
|
-
"plugin": ["@openpalm/assistant-tools", "akm-opencode"],
|
|
135
|
-
"permission": {
|
|
136
|
-
"read": {
|
|
137
|
-
"/home/opencode/.local/share/opencode/auth.json": "deny",
|
|
138
|
-
"/home/opencode/.local/share/opencode/mcp-auth.json": "deny"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
null,
|
|
143
|
-
2,
|
|
144
|
-
) + "\n";
|
|
145
|
-
await writeIfChanged(systemConfig, systemConfigContent);
|
|
83
|
+
// composeProjectArgs() removed — use fullComposeArgs(state) from staging.ts instead.
|
|
84
|
+
// That function builds the correct file list including channel overlays and staged env files.
|
|
146
85
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const assetsAgentsPath = join(import.meta.dir, '..', '..', '..', '..', 'assets', 'AGENTS.md');
|
|
150
|
-
let agentsContent: string;
|
|
151
|
-
if (await Bun.file(assetsAgentsPath).exists()) {
|
|
152
|
-
agentsContent = await Bun.file(assetsAgentsPath).text();
|
|
153
|
-
} else {
|
|
154
|
-
agentsContent =
|
|
155
|
-
'# OpenPalm Assistant\n\n' +
|
|
156
|
-
'This file defines the assistant persona.\n' +
|
|
157
|
-
'It is seeded by the CLI on first install and managed by the admin on subsequent updates.\n';
|
|
158
|
-
}
|
|
159
|
-
await writeIfChanged(agentsFile, agentsContent);
|
|
160
|
-
}
|
|
86
|
+
// ensureOpenCodeConfig and ensureOpenCodeSystemConfig are imported from @openpalm/lib.
|
|
87
|
+
// See packages/lib/src/control-plane/secrets.ts and core-assets.ts.
|
|
161
88
|
|
|
162
89
|
/**
|
|
163
90
|
* Opens a URL in the user's default browser. Best-effort, never throws.
|
package/src/lib/paths.ts
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution — re-exports from @openpalm/lib with CLI-specific additions.
|
|
3
|
+
*/
|
|
1
4
|
import { homedir } from 'node:os';
|
|
2
5
|
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
resolveConfigHome,
|
|
8
|
+
resolveDataHome,
|
|
9
|
+
resolveStateHome,
|
|
10
|
+
} from '@openpalm/lib';
|
|
3
11
|
|
|
4
12
|
export const IS_WINDOWS = process.platform === 'win32';
|
|
5
13
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
return join(homedir(), '.config', 'openpalm');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function defaultDataHome(): string {
|
|
15
|
-
if (process.env.OPENPALM_DATA_HOME) return process.env.OPENPALM_DATA_HOME;
|
|
16
|
-
if (IS_WINDOWS) {
|
|
17
|
-
return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'openpalm', 'data');
|
|
18
|
-
}
|
|
19
|
-
return join(homedir(), '.local', 'share', 'openpalm');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function defaultStateHome(): string {
|
|
23
|
-
if (process.env.OPENPALM_STATE_HOME) return process.env.OPENPALM_STATE_HOME;
|
|
24
|
-
if (IS_WINDOWS) {
|
|
25
|
-
return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'openpalm', 'state');
|
|
26
|
-
}
|
|
27
|
-
return join(homedir(), '.local', 'state', 'openpalm');
|
|
28
|
-
}
|
|
14
|
+
// Re-export lib's XDG resolvers under CLI's existing names
|
|
15
|
+
export { resolveConfigHome as defaultConfigHome };
|
|
16
|
+
export { resolveDataHome as defaultDataHome };
|
|
17
|
+
export { resolveStateHome as defaultStateHome };
|
|
29
18
|
|
|
19
|
+
// CLI-specific paths (not in lib)
|
|
30
20
|
export function defaultDockerSock(): string {
|
|
31
21
|
if (process.env.OPENPALM_DOCKER_SOCK) return process.env.OPENPALM_DOCKER_SOCK;
|
|
32
22
|
return IS_WINDOWS ? '//./pipe/docker_engine' : '/var/run/docker.sock';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-side staging pipeline for Docker Compose operations.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to @openpalm/lib for all control-plane logic. The CLI
|
|
5
|
+
* uses FilesystemAssetProvider (reads from DATA_HOME) and
|
|
6
|
+
* FilesystemRegistryProvider (reads from registry dir).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
createState,
|
|
12
|
+
stageArtifacts,
|
|
13
|
+
persistArtifacts,
|
|
14
|
+
buildComposeFileList,
|
|
15
|
+
buildManagedServices,
|
|
16
|
+
buildEnvFiles,
|
|
17
|
+
FilesystemAssetProvider,
|
|
18
|
+
} from '@openpalm/lib';
|
|
19
|
+
import type { ControlPlaneState } from '@openpalm/lib';
|
|
20
|
+
import { defaultDataHome } from './paths.ts';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure all artifacts are staged from CONFIG_HOME/DATA_HOME to STATE_HOME.
|
|
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.
|
|
28
|
+
*
|
|
29
|
+
* Returns a ControlPlaneState usable with fullComposeArgs().
|
|
30
|
+
*/
|
|
31
|
+
export async function ensureStagedState(): Promise<ControlPlaneState> {
|
|
32
|
+
const dataDir = defaultDataHome();
|
|
33
|
+
|
|
34
|
+
// Verify DATA_HOME has core assets (populated by `openpalm install`)
|
|
35
|
+
if (!existsSync(join(dataDir, 'docker-compose.yml'))) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Core assets not found in ${dataDir}. Run 'openpalm install' first.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const assets = new FilesystemAssetProvider(dataDir);
|
|
42
|
+
|
|
43
|
+
const state = createState();
|
|
44
|
+
state.artifacts = stageArtifacts(state, assets);
|
|
45
|
+
persistArtifacts(state, assets);
|
|
46
|
+
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the full list of docker compose CLI arguments for a given state.
|
|
52
|
+
*
|
|
53
|
+
* Returns: ['--project-name', 'openpalm', '-f', '...', '--env-file', '...']
|
|
54
|
+
*/
|
|
55
|
+
export function fullComposeArgs(state: ControlPlaneState): string[] {
|
|
56
|
+
const files = buildComposeFileList(state);
|
|
57
|
+
const envFiles = buildEnvFiles(state);
|
|
58
|
+
|
|
59
|
+
return [
|
|
60
|
+
'--project-name',
|
|
61
|
+
'openpalm',
|
|
62
|
+
...files.flatMap((f) => ['-f', f]),
|
|
63
|
+
...envFiles.filter((f) => existsSync(f)).flatMap((f) => ['--env-file', f]),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the list of managed service names (used for targeted `up` commands).
|
|
69
|
+
*/
|
|
70
|
+
export function buildManagedServiceNames(state: ControlPlaneState): string[] {
|
|
71
|
+
return buildManagedServices(state);
|
|
72
|
+
}
|
package/src/main.test.ts
CHANGED
|
@@ -57,7 +57,7 @@ describe('cli main', () => {
|
|
|
57
57
|
process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('calls containers pull for update', async () => {
|
|
60
|
+
it('calls containers pull for update (via admin when reachable)', async () => {
|
|
61
61
|
const calls: string[] = [];
|
|
62
62
|
process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
|
|
63
63
|
globalThis.fetch = mock(async (input: string | URL) => {
|
|
@@ -68,7 +68,10 @@ describe('cli main', () => {
|
|
|
68
68
|
|
|
69
69
|
await main(['update']);
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// tryAdminRequest attempts directly — no separate health check
|
|
72
|
+
expect(calls).toEqual([
|
|
73
|
+
'http://localhost:8100/admin/containers/pull',
|
|
74
|
+
]);
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
|
|
@@ -85,6 +88,7 @@ describe('cli main', () => {
|
|
|
85
88
|
|
|
86
89
|
await main(['update']);
|
|
87
90
|
|
|
91
|
+
// Direct request — single call with token header
|
|
88
92
|
expect(adminTokens).toEqual(['env-admin-token']);
|
|
89
93
|
});
|
|
90
94
|
|
|
@@ -110,6 +114,7 @@ describe('cli main', () => {
|
|
|
110
114
|
|
|
111
115
|
try {
|
|
112
116
|
await main(['update']);
|
|
117
|
+
// Direct request — single call with legacy token
|
|
113
118
|
expect(adminTokens).toEqual(['legacy-admin-token']);
|
|
114
119
|
} finally {
|
|
115
120
|
rmSync(base, { recursive: true, force: true });
|