openpalm 0.4.0 → 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 CHANGED
@@ -1,98 +1,84 @@
1
- # openpalm CLI
1
+ # @openpalm/cli
2
2
 
3
- CLI tool for installing, managing, and operating an OpenPalm stack. Published to npm as `openpalm`.
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
4
 
5
- ## Installation
5
+ ## Self-Sufficient Mode
6
6
 
7
- ```bash
8
- npx openpalm install
9
- # or
10
- bunx openpalm install
11
- ```
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.
12
14
 
13
15
  ## Commands
14
16
 
15
17
  | Command | Description |
16
18
  |---|---|
17
- | `install` | Install and start the OpenPalm stack |
18
- | `uninstall` | Stop and remove OpenPalm |
19
- | `update` | Pull latest images and recreate containers |
20
- | `start [service...]` | Start services |
21
- | `stop [service...]` | Stop services |
22
- | `restart [service...]` | Restart services |
23
- | `logs [service...]` | View container logs |
24
- | `status` | Show container status |
25
- | `service <up\|stop\|restart\|logs\|update\|status>` | Domain-based service operations |
26
- | `channel <add\|configure>` | Domain-based channel operations |
27
- | `automation <run\|trigger>` | Domain-based automation operations |
28
- | `extensions <install\|uninstall\|list>` | Manage extensions |
29
- | `dev preflight` | Validate development environment |
30
- | `dev create-channel` | Scaffold a new channel adapter |
31
-
32
- ## Install options
33
-
34
- - `--runtime <docker|podman|orbstack>` — Force container runtime
35
- - `--no-open` — Don't auto-open browser after install
36
- - `--ref <branch|tag>` — Git ref for asset download
19
+ | `openpalm install` | Bootstrap XDG dirs, download assets, run setup wizard, start core services |
20
+ | `openpalm uninstall` | Stop and remove the stack (preserves config and data) |
21
+ | `openpalm update` | Pull latest images and recreate containers |
22
+ | `openpalm start [svc...]` | Start all or named services |
23
+ | `openpalm start --with-admin` | Start all services including admin UI and docker-socket-proxy |
24
+ | `openpalm stop [svc...]` | Stop all or named services |
25
+ | `openpalm restart [svc...]` | Restart all or named services |
26
+ | `openpalm logs [svc...]` | Tail last 100 log lines |
27
+ | `openpalm status` | Show container status |
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 |
37
31
 
38
- ## Building
32
+ ### Install options
39
33
 
40
- Cross-platform compiled binaries:
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.
41
35
 
42
- ```bash
43
- bun run build # Default platform
44
- bun run build:linux-x64 # Linux x64
45
- bun run build:linux-arm64 # Linux ARM64
46
- bun run build:darwin-x64 # macOS x64
47
- bun run build:darwin-arm64 # macOS ARM64
48
- bun run build:windows-x64 # Windows x64
49
- bun run build:windows-arm64 # Windows ARM64
50
- ```
36
+ ### Admin profile
51
37
 
52
- Binaries are output to `dist/`.
53
-
54
- ## Development
38
+ Admin and docker-socket-proxy use Docker Compose profiles. They start only when explicitly requested:
55
39
 
56
40
  ```bash
57
- # Run directly from source
58
- bun run src/main.ts install
59
-
60
- # Run tests
61
- cd packages/cli && bun test
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
62
44
  ```
63
45
 
64
- ## Dependencies
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.
65
49
 
66
- Depends on `@openpalm/lib` (workspace package) for shared utilities like path resolution, runtime detection, and compose generation.
50
+ ## Environment Variables
67
51
 
68
- ## Execution modes (same commands for local and remote admin)
52
+ | Variable | Default | Purpose |
53
+ |---|---|---|
54
+ | `OPENPALM_CONFIG_HOME` | `~/.config/openpalm` | User config + secrets |
55
+ | `OPENPALM_DATA_HOME` | `~/.local/share/openpalm` | Persistent service data |
56
+ | `OPENPALM_STATE_HOME` | `~/.local/state/openpalm` | Assembled runtime |
57
+ | `OPENPALM_WORK_DIR` | `~/openpalm` | Assistant working directory |
58
+ | `OPENPALM_ADMIN_API_URL` | `http://localhost:8100` | Admin API endpoint (for optional delegation) |
59
+ | `OPENPALM_ADMIN_TOKEN` | (from `secrets.env`) | Admin API auth token |
69
60
 
70
- Domain commands automatically choose execution mode:
61
+ ## How It Works
71
62
 
72
- - **Local mode (default):** if admin API env vars are not explicitly set, service commands run locally via compose.
73
- - **Remote admin API mode:** if admin API env vars are set, domain commands call admin over HTTP (`x-admin-token`), so callers do not need Docker socket access.
74
- - **Assistant env fallback:** CLI also reads `${OPENPALM_STATE_HOME}/assistant/.env` for admin URL/token values.
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.
75
66
 
76
- Environment variables (all optional):
67
+ Follows the file-assembly principle: copies whole files, never renders templates. See [`core-principles.md`](../../docs/technical/core-principles.md).
68
+
69
+ ## Building
77
70
 
78
- - `OPENPALM_ADMIN_API_URL` (preferred), `ADMIN_APP_URL`, or `GATEWAY_URL` for admin endpoint resolution.
79
- - `OPENPALM_ADMIN_TOKEN` (preferred) or `ADMIN_TOKEN` for authentication.
80
- - `OPENPALM_ADMIN_TIMEOUT_MS` request timeout (default `15000`).
81
- - `OPENPALM_ALLOW_INSECURE_ADMIN_HTTP=1` to allow public/non-private HTTP URLs (not recommended).
71
+ ```bash
72
+ bun run build # Current platform -> dist/openpalm-cli
73
+ bun run build:linux-x64 # Cross-compile (also: linux-arm64, darwin-x64, darwin-arm64, windows-x64, windows-arm64)
74
+ ```
82
75
 
83
- Examples:
76
+ ## Development
84
77
 
85
78
  ```bash
86
- # Local default service execution
87
- openpalm service restart assistant
88
-
89
- # Remote admin API execution (same command shape)
90
- export OPENPALM_ADMIN_API_URL=http://admin:8100
91
- export OPENPALM_ADMIN_TOKEN=...
92
- openpalm service restart assistant
93
-
94
- # Domain commands for channels/automations
95
- openpalm channel add /path/to/channel.yaml
96
- openpalm channel configure discord --exposure lan
97
- openpalm automation run example-job
79
+ cd packages/cli
80
+ bun run start -- install --no-start
81
+ bun test
98
82
  ```
83
+
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,31 +1,30 @@
1
1
  {
2
2
  "name": "openpalm",
3
- "version": "0.4.0",
4
- "description": "CLI tool for installing and managing an OpenPalm stack",
3
+ "version": "0.9.2-rc2",
5
4
  "type": "module",
6
- "license": "MIT",
5
+ "license": "MPL-2.0",
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"
11
11
  },
12
- "homepage": "https://github.com/itlackey/openpalm",
13
- "keywords": [
14
- "openpalm",
15
- "ai",
16
- "cli",
17
- "docker",
18
- "installer"
19
- ],
20
12
  "bin": {
21
- "openpalm": "./dist/openpalm.js"
13
+ "openpalm": "./src/main.ts"
22
14
  },
23
- "files": [
24
- "dist/**",
25
- "README.md",
26
- "LICENSE"
27
- ],
28
- "engines": {
29
- "bun": ">=1.2.0"
15
+ "scripts": {
16
+ "start": "bun run src/main.ts",
17
+ "test": "bun test",
18
+ "build": "bun build src/main.ts --compile --outfile dist/openpalm-cli",
19
+ "build:linux-x64": "bun build src/main.ts --compile --target=bun-linux-x64 --outfile dist/openpalm-cli-linux-x64",
20
+ "build:linux-arm64": "bun build src/main.ts --compile --target=bun-linux-arm64 --outfile dist/openpalm-cli-linux-arm64",
21
+ "build:darwin-x64": "bun build src/main.ts --compile --target=bun-darwin-x64 --outfile dist/openpalm-cli-darwin-x64",
22
+ "build:darwin-arm64": "bun build src/main.ts --compile --target=bun-darwin-arm64 --outfile dist/openpalm-cli-darwin-arm64",
23
+ "build:windows-x64": "bun build src/main.ts --compile --target=bun-windows-x64 --outfile dist/openpalm-cli-windows-x64.exe",
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"
30
29
  }
31
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
+ }