openpalm 0.9.1 → 0.9.2-rc2
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 +6 -2
- package/src/commands/install.ts +275 -0
- package/src/commands/logs.ts +25 -0
- package/src/commands/restart.ts +71 -0
- package/src/commands/scan.ts +48 -0
- package/src/commands/service.ts +93 -0
- package/src/commands/start.ts +79 -0
- package/src/commands/status.ts +23 -0
- package/src/commands/stop.ts +70 -0
- package/src/commands/uninstall.ts +37 -0
- package/src/commands/update.ts +32 -0
- package/src/commands/validate.ts +47 -0
- package/src/lib/admin.ts +107 -0
- package/src/lib/docker.ts +107 -0
- package/src/lib/env.ts +196 -0
- package/src/lib/host-info.ts +47 -0
- package/src/lib/paths.ts +27 -0
- package/src/lib/staging.ts +72 -0
- package/src/lib/varlock.ts +132 -0
- package/src/main.test.ts +142 -29
- package/src/main.ts +33 -954
- 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/README.md
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
# @openpalm/cli
|
|
2
2
|
|
|
3
|
-
Bun CLI for bootstrapping and managing an OpenPalm installation.
|
|
3
|
+
Bun CLI for bootstrapping and managing an OpenPalm installation. The CLI is the primary orchestrator -- all commands work without the admin container. When admin is running, commands optionally delegate to the admin API.
|
|
4
|
+
|
|
5
|
+
## Self-Sufficient Mode
|
|
6
|
+
|
|
7
|
+
The CLI operates directly against Docker Compose without requiring an admin container:
|
|
8
|
+
|
|
9
|
+
- **Install** -- creates XDG dirs, downloads assets, serves the setup wizard locally via `Bun.serve()`, stages artifacts, starts core services
|
|
10
|
+
- **All lifecycle commands** -- stage artifacts from `@openpalm/lib` using `FilesystemAssetProvider`, then run Docker Compose directly
|
|
11
|
+
- **Admin delegation** -- the `install` command checks for a running admin and delegates if reachable. Other commands operate directly via Docker Compose.
|
|
12
|
+
|
|
13
|
+
The admin container is optional. Use `--with-admin` to include the admin UI profile.
|
|
4
14
|
|
|
5
15
|
## Commands
|
|
6
16
|
|
|
7
17
|
| Command | Description |
|
|
8
18
|
|---|---|
|
|
9
|
-
| `openpalm install` | Bootstrap XDG dirs, download assets,
|
|
19
|
+
| `openpalm install` | Bootstrap XDG dirs, download assets, run setup wizard, start core services |
|
|
10
20
|
| `openpalm uninstall` | Stop and remove the stack (preserves config and data) |
|
|
11
21
|
| `openpalm update` | Pull latest images and recreate containers |
|
|
12
22
|
| `openpalm start [svc...]` | Start all or named services |
|
|
23
|
+
| `openpalm start --with-admin` | Start all services including admin UI and docker-socket-proxy |
|
|
13
24
|
| `openpalm stop [svc...]` | Stop all or named services |
|
|
14
25
|
| `openpalm restart [svc...]` | Restart all or named services |
|
|
15
26
|
| `openpalm logs [svc...]` | Tail last 100 log lines |
|
|
16
27
|
| `openpalm status` | Show container status |
|
|
17
|
-
| `openpalm service <sub> [svc...]` | Alias
|
|
28
|
+
| `openpalm service <sub> [svc...]` | Alias -- subcommands: `start`, `stop`, `restart`, `logs`, `status`, `update` |
|
|
29
|
+
| `openpalm validate` | Validate `secrets.env` against the schema (requires prior install) |
|
|
30
|
+
| `openpalm scan` | Scan for leaked secrets in config files |
|
|
18
31
|
|
|
19
32
|
### Install options
|
|
20
33
|
|
|
21
|
-
`--force` skip "already installed" check, `--version TAG` install a specific ref (default
|
|
34
|
+
`--force` skip "already installed" check, `--version TAG` install a specific ref (default: current CLI version), `--no-start` prepare files only, `--no-open` skip browser launch.
|
|
35
|
+
|
|
36
|
+
### Admin profile
|
|
37
|
+
|
|
38
|
+
Admin and docker-socket-proxy use Docker Compose profiles. They start only when explicitly requested:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
openpalm start --with-admin # Start core + admin profile
|
|
42
|
+
openpalm start admin # Start admin service specifically
|
|
43
|
+
openpalm stop admin # Stop admin service specifically
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Setup Wizard
|
|
47
|
+
|
|
48
|
+
On first install, the CLI serves a setup wizard on port 8100 via `Bun.serve()`. The wizard runs entirely in the browser (vanilla HTML/JS) and calls `performSetup()` from `@openpalm/lib` to write secrets, connection profiles, memory config, and stage artifacts. No admin container is involved.
|
|
22
49
|
|
|
23
|
-
## Environment
|
|
50
|
+
## Environment Variables
|
|
24
51
|
|
|
25
52
|
| Variable | Default | Purpose |
|
|
26
53
|
|---|---|---|
|
|
@@ -28,20 +55,21 @@ Bun CLI for bootstrapping and managing an OpenPalm installation. Handles the ini
|
|
|
28
55
|
| `OPENPALM_DATA_HOME` | `~/.local/share/openpalm` | Persistent service data |
|
|
29
56
|
| `OPENPALM_STATE_HOME` | `~/.local/state/openpalm` | Assembled runtime |
|
|
30
57
|
| `OPENPALM_WORK_DIR` | `~/openpalm` | Assistant working directory |
|
|
31
|
-
| `OPENPALM_ADMIN_API_URL` | `http://localhost:8100` | Admin API endpoint |
|
|
58
|
+
| `OPENPALM_ADMIN_API_URL` | `http://localhost:8100` | Admin API endpoint (for optional delegation) |
|
|
32
59
|
| `OPENPALM_ADMIN_TOKEN` | (from `secrets.env`) | Admin API auth token |
|
|
33
60
|
|
|
34
|
-
## How
|
|
61
|
+
## How It Works
|
|
35
62
|
|
|
36
|
-
1. **Bootstrap** (
|
|
37
|
-
2. **Running stack**
|
|
63
|
+
1. **Bootstrap** (first install) -- creates XDG directory tree, downloads core assets from GitHub, seeds `secrets.env` and `stack.env`, serves setup wizard, stages artifacts via `@openpalm/lib`, starts core services via `docker compose up`
|
|
64
|
+
2. **Running stack** -- commands stage artifacts locally using `FilesystemAssetProvider`, then execute Docker Compose directly.
|
|
65
|
+
3. **Admin absent** -- all commands work identically. Admin is never required for any operation.
|
|
38
66
|
|
|
39
|
-
Follows the file-assembly principle: copies whole files, never renders templates. See [`
|
|
67
|
+
Follows the file-assembly principle: copies whole files, never renders templates. See [`core-principles.md`](../../docs/technical/core-principles.md).
|
|
40
68
|
|
|
41
69
|
## Building
|
|
42
70
|
|
|
43
71
|
```bash
|
|
44
|
-
bun run build # Current platform
|
|
72
|
+
bun run build # Current platform -> dist/openpalm-cli
|
|
45
73
|
bun run build:linux-x64 # Cross-compile (also: linux-arm64, darwin-x64, darwin-arm64, windows-x64, windows-arm64)
|
|
46
74
|
```
|
|
47
75
|
|
|
@@ -53,4 +81,4 @@ bun run start -- install --no-start
|
|
|
53
81
|
bun test
|
|
54
82
|
```
|
|
55
83
|
|
|
56
|
-
See also: [`scripts/
|
|
84
|
+
See also: [`scripts/setup.sh`](../../scripts/setup.sh) (shell-based installer).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-only launcher for the CLI setup wizard server.
|
|
3
|
+
*
|
|
4
|
+
* Called from Playwright tests as a child process:
|
|
5
|
+
* bun run packages/cli/e2e/start-wizard-server.ts <port>
|
|
6
|
+
*
|
|
7
|
+
* Starts the wizard server on the given port with a temp config directory
|
|
8
|
+
* so tests do not affect real dev state. Prints "WIZARD_READY:<port>" to
|
|
9
|
+
* stdout when listening, which the Playwright test waits for.
|
|
10
|
+
*/
|
|
11
|
+
import { createSetupServer } from "../src/setup-wizard/server.ts";
|
|
12
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import type { CoreAssetProvider } from "@openpalm/lib";
|
|
14
|
+
|
|
15
|
+
const port = parseInt(Bun.argv[2] || "18100", 10);
|
|
16
|
+
const tmpBase = `/tmp/openpalm-wizard-test-${port}`;
|
|
17
|
+
|
|
18
|
+
// Create minimal directory structure so the server can start.
|
|
19
|
+
// API endpoints that need real files are mocked at the browser level
|
|
20
|
+
// by Playwright's page.route(), so these dirs just prevent crashes.
|
|
21
|
+
mkdirSync(`${tmpBase}/config`, { recursive: true });
|
|
22
|
+
mkdirSync(`${tmpBase}/data`, { recursive: true });
|
|
23
|
+
mkdirSync(`${tmpBase}/state/artifacts`, { recursive: true });
|
|
24
|
+
|
|
25
|
+
writeFileSync(`${tmpBase}/config/secrets.env`, "# test\n");
|
|
26
|
+
writeFileSync(`${tmpBase}/state/artifacts/stack.env`, "OPENPALM_SETUP_COMPLETE=false\n");
|
|
27
|
+
|
|
28
|
+
// No-op asset provider — mocked tests intercept API calls before they
|
|
29
|
+
// reach performSetup(), so these methods are never invoked.
|
|
30
|
+
const noopAssetProvider: CoreAssetProvider = {
|
|
31
|
+
coreCompose: () => "",
|
|
32
|
+
caddyfile: () => "",
|
|
33
|
+
ollamaCompose: () => "",
|
|
34
|
+
agentsMd: () => "",
|
|
35
|
+
opencodeConfig: () => "",
|
|
36
|
+
adminOpencodeConfig: () => "",
|
|
37
|
+
secretsSchema: () => "",
|
|
38
|
+
stackSchema: () => "",
|
|
39
|
+
cleanupLogs: () => "",
|
|
40
|
+
cleanupData: () => "",
|
|
41
|
+
validateConfig: () => "",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Override state/config home so the server doesn't touch real dirs.
|
|
45
|
+
process.env.OPENPALM_CONFIG_HOME = `${tmpBase}/config`;
|
|
46
|
+
process.env.OPENPALM_STATE_HOME = `${tmpBase}/state`;
|
|
47
|
+
process.env.OPENPALM_DATA_HOME = `${tmpBase}/data`;
|
|
48
|
+
|
|
49
|
+
const { server } = createSetupServer(port, {
|
|
50
|
+
configDir: `${tmpBase}/config`,
|
|
51
|
+
assetProvider: noopAssetProvider,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(`WIZARD_READY:${port}`);
|
|
55
|
+
|
|
56
|
+
// Keep alive until killed
|
|
57
|
+
process.on("SIGTERM", () => {
|
|
58
|
+
server.stop();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
});
|
|
61
|
+
process.on("SIGINT", () => {
|
|
62
|
+
server.stop();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpalm",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2-rc2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
|
|
7
|
-
|
|
7
|
+
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/itlackey/openpalm.git",
|
|
10
10
|
"directory": "packages/cli"
|
|
@@ -22,5 +22,9 @@
|
|
|
22
22
|
"build:darwin-arm64": "bun build src/main.ts --compile --target=bun-darwin-arm64 --outfile dist/openpalm-cli-darwin-arm64",
|
|
23
23
|
"build:windows-x64": "bun build src/main.ts --compile --target=bun-windows-x64 --outfile dist/openpalm-cli-windows-x64.exe",
|
|
24
24
|
"build:windows-arm64": "bun build src/main.ts --compile --target=bun-windows-arm64 --outfile dist/openpalm-cli-windows-arm64.exe"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@openpalm/lib": "workspace:*",
|
|
28
|
+
"citty": "^0.2.1"
|
|
25
29
|
}
|
|
26
30
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
import cliPkg from '../../package.json' with { type: 'json' };
|
|
5
|
+
import { defaultConfigHome, defaultDataHome, defaultStateHome, defaultWorkDir } from '../lib/paths.ts';
|
|
6
|
+
import { ensureSecrets, ensureStackEnv } from '../lib/env.ts';
|
|
7
|
+
import { isAdminReachable, adminRequest } from '../lib/admin.ts';
|
|
8
|
+
import { ensureDirectoryTree, fetchAsset, runDockerCompose, openBrowser } from '../lib/docker.ts';
|
|
9
|
+
import { ensureOpenCodeConfig, ensureOpenCodeSystemConfig, ensureAdminOpenCodeConfig, FilesystemAssetProvider } from '@openpalm/lib';
|
|
10
|
+
import { ensureVarlock, prepareVarlockDir } from '../lib/varlock.ts';
|
|
11
|
+
import { detectHostInfo } from '../lib/host-info.ts';
|
|
12
|
+
import { loadAdminToken } from '../lib/env.ts';
|
|
13
|
+
import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
|
|
14
|
+
import { createSetupServer } from '../setup-wizard/server.ts';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_INSTALL_REF = cliPkg.version ? `v${cliPkg.version}` : 'main';
|
|
17
|
+
const SETUP_WIZARD_PORT = 8100;
|
|
18
|
+
|
|
19
|
+
export default defineCommand({
|
|
20
|
+
meta: {
|
|
21
|
+
name: 'install',
|
|
22
|
+
description: 'Bootstrap XDG dirs, download assets, run setup wizard, start core services',
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
force: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'Skip "already installed" check',
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
version: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Install specific release ref (default: current CLI version)',
|
|
33
|
+
default: DEFAULT_INSTALL_REF,
|
|
34
|
+
},
|
|
35
|
+
start: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Start services after install (use --no-start to skip)',
|
|
38
|
+
default: true,
|
|
39
|
+
},
|
|
40
|
+
open: {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
description: 'Open browser after install (use --no-open to skip)',
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
async run({ args }) {
|
|
47
|
+
// If the stack is already running AND we have a valid admin token,
|
|
48
|
+
// delegate to the admin API. Otherwise fall through to bootstrap.
|
|
49
|
+
if (await isAdminReachable()) {
|
|
50
|
+
const token = await loadAdminToken();
|
|
51
|
+
if (token) {
|
|
52
|
+
console.log(JSON.stringify(await adminRequest('/admin/install', { method: 'POST' }), null, 2));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// No token available — fall through to bootstrap install which doesn't need auth.
|
|
56
|
+
console.warn('Stack is running but no admin token is configured. Proceeding with bootstrap install.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await bootstrapInstall({
|
|
60
|
+
force: args.force,
|
|
61
|
+
version: args.version,
|
|
62
|
+
noStart: !args.start,
|
|
63
|
+
noOpen: !args.open,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
type InstallOptions = {
|
|
69
|
+
force: boolean;
|
|
70
|
+
version: string;
|
|
71
|
+
noStart: boolean;
|
|
72
|
+
noOpen: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export async function bootstrapInstall(options: InstallOptions): Promise<void> {
|
|
76
|
+
if (!Bun.which('docker')) {
|
|
77
|
+
throw new Error('Docker is not installed. Install Docker first: https://docs.docker.com/get-docker/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const dockerInfo = Bun.spawn(['docker', 'info'], { stdout: 'ignore', stderr: 'ignore' });
|
|
81
|
+
if ((await dockerInfo.exited) !== 0) {
|
|
82
|
+
throw new Error('Docker is not running (or current user lacks permission). Start Docker and retry.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const composeVersion = Bun.spawn(['docker', 'compose', 'version'], { stdout: 'ignore', stderr: 'ignore' });
|
|
86
|
+
if ((await composeVersion.exited) !== 0) {
|
|
87
|
+
throw new Error('Docker Compose v2 is required. Install it: https://docs.docker.com/compose/install/');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const configHome = defaultConfigHome();
|
|
91
|
+
const dataHome = defaultDataHome();
|
|
92
|
+
const stateHome = defaultStateHome();
|
|
93
|
+
const workDir = defaultWorkDir();
|
|
94
|
+
|
|
95
|
+
const secretsPath = join(configHome, 'secrets.env');
|
|
96
|
+
const updateMode = await Bun.file(secretsPath).exists();
|
|
97
|
+
if (updateMode && !options.force) {
|
|
98
|
+
throw new Error('OpenPalm appears to already be installed. Re-run install with --force to continue.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await ensureDirectoryTree(configHome, dataHome, stateHome, workDir);
|
|
102
|
+
|
|
103
|
+
// Detect host system info (non-fatal)
|
|
104
|
+
try {
|
|
105
|
+
const hostInfo = await detectHostInfo();
|
|
106
|
+
await Bun.write(join(dataHome, 'host.json'), JSON.stringify(hostInfo, null, 2) + '\n');
|
|
107
|
+
} catch {
|
|
108
|
+
// Host detection failure is non-fatal
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const composeContent = await fetchAsset(options.version, 'docker-compose.yml');
|
|
112
|
+
const caddyContent = await fetchAsset(options.version, 'Caddyfile');
|
|
113
|
+
await Bun.write(join(dataHome, 'docker-compose.yml'), composeContent);
|
|
114
|
+
await Bun.write(join(dataHome, 'caddy', 'Caddyfile'), caddyContent);
|
|
115
|
+
await Bun.write(join(stateHome, 'artifacts', 'docker-compose.yml'), composeContent);
|
|
116
|
+
await Bun.write(join(stateHome, 'artifacts', 'Caddyfile'), caddyContent);
|
|
117
|
+
|
|
118
|
+
// Download schemas to both DATA_HOME (for FilesystemAssetProvider) and STATE_HOME (for varlock validation)
|
|
119
|
+
const secretsSchemaContent = await fetchAsset(options.version, 'secrets.env.schema');
|
|
120
|
+
const stackSchemaContent = await fetchAsset(options.version, 'stack.env.schema');
|
|
121
|
+
await Bun.write(join(dataHome, 'secrets.env.schema'), secretsSchemaContent);
|
|
122
|
+
await Bun.write(join(dataHome, 'stack.env.schema'), stackSchemaContent);
|
|
123
|
+
await Bun.write(join(stateHome, 'artifacts', 'secrets.env.schema'), secretsSchemaContent);
|
|
124
|
+
await Bun.write(join(stateHome, 'artifacts', 'stack.env.schema'), stackSchemaContent);
|
|
125
|
+
|
|
126
|
+
// Download remaining assets needed by FilesystemAssetProvider
|
|
127
|
+
const assetFiles: Array<{ remote: string; localPath: string }> = [
|
|
128
|
+
{ remote: 'ollama.yml', localPath: join(dataHome, 'ollama.yml') },
|
|
129
|
+
{ remote: 'AGENTS.md', localPath: join(dataHome, 'assistant', 'AGENTS.md') },
|
|
130
|
+
{ remote: 'opencode.jsonc', localPath: join(dataHome, 'assistant', 'opencode.jsonc') },
|
|
131
|
+
{ remote: 'admin-opencode.jsonc', localPath: join(dataHome, 'admin', 'opencode.jsonc') },
|
|
132
|
+
{ remote: 'cleanup-logs.yml', localPath: join(dataHome, 'automations', 'cleanup-logs.yml') },
|
|
133
|
+
{ remote: 'cleanup-data.yml', localPath: join(dataHome, 'automations', 'cleanup-data.yml') },
|
|
134
|
+
{ remote: 'validate-config.yml', localPath: join(dataHome, 'automations', 'validate-config.yml') },
|
|
135
|
+
];
|
|
136
|
+
await Promise.all(
|
|
137
|
+
assetFiles.map(async ({ remote, localPath }) => {
|
|
138
|
+
try {
|
|
139
|
+
const content = await fetchAsset(options.version, remote);
|
|
140
|
+
await Bun.write(localPath, content);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.warn(`Warning: could not download asset '${remote}': ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
await ensureSecrets(configHome);
|
|
148
|
+
await ensureStackEnv(configHome, dataHome, stateHome, workDir, options.version);
|
|
149
|
+
// Seed OpenCode config — non-fatal since performSetup() also seeds these
|
|
150
|
+
try {
|
|
151
|
+
const fsAssets = new FilesystemAssetProvider(dataHome);
|
|
152
|
+
ensureOpenCodeConfig();
|
|
153
|
+
ensureOpenCodeSystemConfig(fsAssets);
|
|
154
|
+
ensureAdminOpenCodeConfig(fsAssets);
|
|
155
|
+
} catch {
|
|
156
|
+
// Assets may not be available yet on first install; performSetup() will retry
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Non-fatal validation
|
|
160
|
+
try {
|
|
161
|
+
const varlockBin = await ensureVarlock(stateHome);
|
|
162
|
+
const schemaPath = join(stateHome, 'artifacts', 'secrets.env.schema');
|
|
163
|
+
const envPath = join(configHome, 'secrets.env');
|
|
164
|
+
if (await Bun.file(schemaPath).exists()) {
|
|
165
|
+
const tmpDir = await prepareVarlockDir(schemaPath, envPath);
|
|
166
|
+
try {
|
|
167
|
+
const proc = Bun.spawn([varlockBin, 'load', '--path', `${tmpDir}/`], {
|
|
168
|
+
stdout: 'ignore',
|
|
169
|
+
stderr: 'ignore',
|
|
170
|
+
});
|
|
171
|
+
const code = await proc.exited;
|
|
172
|
+
if (code === 0) {
|
|
173
|
+
console.log('Configuration validated.');
|
|
174
|
+
} else {
|
|
175
|
+
console.warn('Configuration has validation warnings (non-fatal on first install).');
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Varlock install/execution failures are non-fatal during install
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.noStart) {
|
|
186
|
+
console.log('OpenPalm files prepared. Run `openpalm start` to start services.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Setup Wizard ──────────────────────────────────────────────────────
|
|
191
|
+
// First-time install: serve the setup wizard locally and wait for user
|
|
192
|
+
// to complete it. The wizard calls performSetup() from @openpalm/lib
|
|
193
|
+
// which writes secrets, connection profiles, memory config, and stages
|
|
194
|
+
// all artifacts. No admin container needed.
|
|
195
|
+
|
|
196
|
+
if (!updateMode) {
|
|
197
|
+
console.log('Starting setup wizard...');
|
|
198
|
+
|
|
199
|
+
const wizard = createSetupServer(SETUP_WIZARD_PORT, { configDir: configHome });
|
|
200
|
+
const wizardUrl = `http://localhost:${wizard.server.port}/setup`;
|
|
201
|
+
console.log(`Setup wizard running at ${wizardUrl}`);
|
|
202
|
+
|
|
203
|
+
if (!options.noOpen) {
|
|
204
|
+
await openBrowser(wizardUrl);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Block until user completes the wizard
|
|
208
|
+
const result = await wizard.waitForComplete();
|
|
209
|
+
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
wizard.stop();
|
|
212
|
+
throw new Error(`Setup failed: ${result.error ?? 'unknown error'}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log('Setup complete. Starting services...');
|
|
216
|
+
|
|
217
|
+
// Keep wizard server running for deploy status polling from the browser.
|
|
218
|
+
// Stage artifacts and start services while the wizard shows progress.
|
|
219
|
+
try {
|
|
220
|
+
const state = await ensureStagedState();
|
|
221
|
+
const composeArgs = fullComposeArgs(state);
|
|
222
|
+
const managedServices = buildManagedServiceNames(state);
|
|
223
|
+
|
|
224
|
+
wizard.updateDeployStatus(
|
|
225
|
+
managedServices.map(s => ({ service: s, status: 'pending', label: 'Waiting...' })),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await runDockerCompose([...composeArgs, 'pull', ...managedServices]).catch(() => {
|
|
229
|
+
// Pull failure is non-fatal — images may already be cached
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
wizard.updateDeployStatus(
|
|
233
|
+
managedServices.map(s => ({ service: s, status: 'pulling', label: 'Starting...' })),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await runDockerCompose([...composeArgs, 'up', '-d', ...managedServices]);
|
|
237
|
+
|
|
238
|
+
wizard.markAllRunning();
|
|
239
|
+
|
|
240
|
+
console.log(JSON.stringify({
|
|
241
|
+
ok: true,
|
|
242
|
+
mode: 'install',
|
|
243
|
+
services: managedServices,
|
|
244
|
+
}, null, 2));
|
|
245
|
+
|
|
246
|
+
// Give the browser a moment to poll the final status, then stop
|
|
247
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
248
|
+
} catch (err) {
|
|
249
|
+
wizard.setDeployError(String(err));
|
|
250
|
+
// Keep server alive briefly so user can see the error
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
252
|
+
throw err;
|
|
253
|
+
} finally {
|
|
254
|
+
wizard.stop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Start Core Services (update mode — no wizard) ────────────────────
|
|
261
|
+
// Stage artifacts and start all managed services directly via Docker
|
|
262
|
+
// Compose. No admin container required for lifecycle operations.
|
|
263
|
+
|
|
264
|
+
const state = await ensureStagedState();
|
|
265
|
+
const composeArgs = fullComposeArgs(state);
|
|
266
|
+
const managedServices = buildManagedServiceNames(state);
|
|
267
|
+
|
|
268
|
+
await runDockerCompose([...composeArgs, 'up', '-d', ...managedServices]);
|
|
269
|
+
|
|
270
|
+
console.log(JSON.stringify({
|
|
271
|
+
ok: true,
|
|
272
|
+
mode: 'update',
|
|
273
|
+
services: managedServices,
|
|
274
|
+
}, null, 2));
|
|
275
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
3
|
+
import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
|
|
4
|
+
|
|
5
|
+
export async function runLogsAction(services: string[]): Promise<void> {
|
|
6
|
+
const state = await ensureStagedState();
|
|
7
|
+
await runDockerCompose([...fullComposeArgs(state), 'logs', '--tail', '100', ...services]);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'logs',
|
|
13
|
+
description: 'Tail last 100 log lines for services',
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
services: {
|
|
17
|
+
type: 'positional',
|
|
18
|
+
description: 'Service names (omit for all)',
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async run({ args }) {
|
|
23
|
+
await runLogsAction(args._ ?? []);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { tryAdminRequest, getServiceNames } from '../lib/admin.ts';
|
|
3
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
4
|
+
import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
|
|
5
|
+
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: 'restart',
|
|
9
|
+
description: 'Restart services (all or named)',
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
services: {
|
|
13
|
+
type: 'positional',
|
|
14
|
+
description: 'Service names to restart (omit for all)',
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const services = args._ ?? [];
|
|
20
|
+
await runRestartAction(services);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function runRestartAction(services: string[]): Promise<void> {
|
|
25
|
+
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
|
|
45
|
+
const state = await ensureStagedState();
|
|
46
|
+
const managedServices = buildManagedServiceNames(state);
|
|
47
|
+
await runDockerCompose([...fullComposeArgs(state), 'restart', ...managedServices]);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Restart specific services
|
|
52
|
+
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
|
+
const state = await ensureStagedState();
|
|
64
|
+
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
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { defaultConfigHome, defaultStateHome } from '../lib/paths.ts';
|
|
5
|
+
import { ensureVarlock, prepareVarlockDir } from '../lib/varlock.ts';
|
|
6
|
+
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'scan',
|
|
10
|
+
description: 'Scan codebase for leaked secrets (requires local secrets.env)',
|
|
11
|
+
},
|
|
12
|
+
async run() {
|
|
13
|
+
const stateHome = defaultStateHome();
|
|
14
|
+
const configHome = defaultConfigHome();
|
|
15
|
+
|
|
16
|
+
const schemaPath = join(stateHome, 'artifacts', 'secrets.env.schema');
|
|
17
|
+
const envPath = join(configHome, 'secrets.env');
|
|
18
|
+
|
|
19
|
+
if (!(await Bun.file(schemaPath).exists())) {
|
|
20
|
+
console.error(
|
|
21
|
+
`Error: secrets.env.schema not found at ${schemaPath}.\nRun 'openpalm install' first to stage schema files.`,
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!(await Bun.file(envPath).exists())) {
|
|
27
|
+
console.error(
|
|
28
|
+
`Error: secrets.env not found at ${envPath}.\nRun 'openpalm install' first.`,
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const varlockBin = await ensureVarlock(stateHome);
|
|
34
|
+
|
|
35
|
+
const tmpDir = await prepareVarlockDir(schemaPath, envPath);
|
|
36
|
+
let exitCode = 1;
|
|
37
|
+
try {
|
|
38
|
+
const proc = Bun.spawn([varlockBin, 'scan', '--path', `${tmpDir}/`], {
|
|
39
|
+
stdout: 'inherit',
|
|
40
|
+
stderr: 'inherit',
|
|
41
|
+
});
|
|
42
|
+
exitCode = await proc.exited;
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
process.exit(exitCode);
|
|
47
|
+
},
|
|
48
|
+
});
|