openpalm 0.9.4 → 0.9.5
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/package.json +2 -1
- package/src/commands/install.ts +124 -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/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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpalm",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"build:windows-arm64": "bun build src/main.ts --compile --target=bun-windows-arm64 --outfile dist/openpalm-cli-windows-arm64.exe"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@openpalm/lib": "workspace:*",
|
|
27
28
|
"citty": "^0.2.1"
|
|
28
29
|
}
|
|
29
30
|
}
|
package/src/commands/install.ts
CHANGED
|
@@ -4,18 +4,22 @@ import { rm } from 'node:fs/promises';
|
|
|
4
4
|
import cliPkg from '../../package.json' with { type: 'json' };
|
|
5
5
|
import { defaultConfigHome, defaultDataHome, defaultStateHome, defaultWorkDir } from '../lib/paths.ts';
|
|
6
6
|
import { ensureSecrets, ensureStackEnv } from '../lib/env.ts';
|
|
7
|
-
import {
|
|
8
|
-
import { ensureDirectoryTree, fetchAsset, runDockerCompose,
|
|
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';
|
|
9
10
|
import { ensureVarlock, prepareVarlockDir } from '../lib/varlock.ts';
|
|
10
11
|
import { detectHostInfo } from '../lib/host-info.ts';
|
|
11
12
|
import { loadAdminToken } from '../lib/env.ts';
|
|
13
|
+
import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
|
|
14
|
+
import { createSetupServer } from '../setup-wizard/server.ts';
|
|
12
15
|
|
|
13
16
|
const DEFAULT_INSTALL_REF = cliPkg.version ? `v${cliPkg.version}` : 'main';
|
|
17
|
+
const SETUP_WIZARD_PORT = 8100;
|
|
14
18
|
|
|
15
19
|
export default defineCommand({
|
|
16
20
|
meta: {
|
|
17
21
|
name: 'install',
|
|
18
|
-
description: 'Bootstrap XDG dirs, download assets,
|
|
22
|
+
description: 'Bootstrap XDG dirs, download assets, run setup wizard, start core services',
|
|
19
23
|
},
|
|
20
24
|
args: {
|
|
21
25
|
force: {
|
|
@@ -42,7 +46,7 @@ export default defineCommand({
|
|
|
42
46
|
async run({ args }) {
|
|
43
47
|
// If the stack is already running AND we have a valid admin token,
|
|
44
48
|
// delegate to the admin API. Otherwise fall through to bootstrap.
|
|
45
|
-
if (await
|
|
49
|
+
if (await isAdminReachable()) {
|
|
46
50
|
const token = await loadAdminToken();
|
|
47
51
|
if (token) {
|
|
48
52
|
console.log(JSON.stringify(await adminRequest('/admin/install', { method: 'POST' }), null, 2));
|
|
@@ -111,15 +115,46 @@ export async function bootstrapInstall(options: InstallOptions): Promise<void> {
|
|
|
111
115
|
await Bun.write(join(stateHome, 'artifacts', 'docker-compose.yml'), composeContent);
|
|
112
116
|
await Bun.write(join(stateHome, 'artifacts', 'Caddyfile'), caddyContent);
|
|
113
117
|
|
|
118
|
+
// Download schemas to both DATA_HOME (for FilesystemAssetProvider) and STATE_HOME (for varlock validation)
|
|
114
119
|
const secretsSchemaContent = await fetchAsset(options.version, 'secrets.env.schema');
|
|
115
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);
|
|
116
123
|
await Bun.write(join(stateHome, 'artifacts', 'secrets.env.schema'), secretsSchemaContent);
|
|
117
124
|
await Bun.write(join(stateHome, 'artifacts', 'stack.env.schema'), stackSchemaContent);
|
|
118
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
|
+
|
|
119
147
|
await ensureSecrets(configHome);
|
|
120
148
|
await ensureStackEnv(configHome, dataHome, stateHome, workDir, options.version);
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
|
123
158
|
|
|
124
159
|
// Non-fatal validation
|
|
125
160
|
try {
|
|
@@ -152,19 +187,89 @@ export async function bootstrapInstall(options: InstallOptions): Promise<void> {
|
|
|
152
187
|
return;
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
258
|
}
|
|
168
259
|
|
|
169
|
-
|
|
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));
|
|
170
275
|
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
2
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
3
|
+
import { ensureStagedState, fullComposeArgs } from '../lib/staging.ts';
|
|
3
4
|
|
|
4
5
|
export async function runLogsAction(services: string[]): Promise<void> {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
...composeProjectArgs(),
|
|
8
|
-
'logs',
|
|
9
|
-
'--tail',
|
|
10
|
-
'100',
|
|
11
|
-
...services,
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const proc = Bun.spawn(['docker', ...composeArgs], { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' });
|
|
15
|
-
const exitCode = await proc.exited;
|
|
16
|
-
if (exitCode !== 0) {
|
|
17
|
-
throw new Error(`Docker compose logs command failed (exit code ${exitCode})`);
|
|
18
|
-
}
|
|
6
|
+
const state = await ensureStagedState();
|
|
7
|
+
await runDockerCompose([...fullComposeArgs(state), 'logs', '--tail', '100', ...services]);
|
|
19
8
|
}
|
|
20
9
|
|
|
21
10
|
export default defineCommand({
|
package/src/commands/restart.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
2
|
+
import { tryAdminRequest, getServiceNames } 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: {
|
|
@@ -21,23 +23,49 @@ export default defineCommand({
|
|
|
21
23
|
|
|
22
24
|
export async function runRestartAction(services: string[]): Promise<void> {
|
|
23
25
|
if (services.length === 0) {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
32
42
|
}
|
|
43
|
+
|
|
44
|
+
// Direct compose restart
|
|
45
|
+
const state = await ensureStagedState();
|
|
46
|
+
const managedServices = buildManagedServiceNames(state);
|
|
47
|
+
await runDockerCompose([...fullComposeArgs(state), 'restart', ...managedServices]);
|
|
33
48
|
return;
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
// Restart specific services
|
|
36
52
|
for (const service of services) {
|
|
37
|
-
const
|
|
53
|
+
const adminResult = await tryAdminRequest('/admin/containers/restart', {
|
|
38
54
|
method: 'POST',
|
|
39
55
|
body: JSON.stringify({ service }),
|
|
40
56
|
});
|
|
41
|
-
|
|
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
|
+
}
|
|
42
70
|
}
|
|
43
71
|
}
|
package/src/commands/service.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
|
import { runLogsAction } from './logs.ts';
|
|
4
6
|
import { runStartAction } from './start.ts';
|
|
5
7
|
import { runStopAction } from './stop.ts';
|
|
@@ -40,14 +42,38 @@ const logsCmd = defineCommand({
|
|
|
40
42
|
const updateCmd = defineCommand({
|
|
41
43
|
meta: { name: 'update', description: 'Pull latest images' },
|
|
42
44
|
async run() {
|
|
43
|
-
|
|
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
|
+
const state = await ensureStagedState();
|
|
54
|
+
const composeArgs = fullComposeArgs(state);
|
|
55
|
+
const managedServices = buildManagedServiceNames(state);
|
|
56
|
+
console.log('Pulling latest images...');
|
|
57
|
+
await runDockerCompose([...composeArgs, 'pull', ...managedServices]);
|
|
58
|
+
console.log('Recreating containers...');
|
|
59
|
+
await runDockerCompose([...composeArgs, 'up', '-d', '--force-recreate', ...managedServices]);
|
|
60
|
+
console.log('Update complete.');
|
|
44
61
|
},
|
|
45
62
|
});
|
|
46
63
|
|
|
47
64
|
const statusCmd = defineCommand({
|
|
48
65
|
meta: { name: 'status', description: 'Show container status' },
|
|
49
66
|
async run() {
|
|
50
|
-
|
|
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
|
+
const state = await ensureStagedState();
|
|
76
|
+
await runDockerCompose([...fullComposeArgs(state), 'ps', '--format', 'table']);
|
|
51
77
|
},
|
|
52
78
|
});
|
|
53
79
|
|
package/src/commands/start.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { tryAdminRequest } from '../lib/admin.ts';
|
|
3
|
+
import { runDockerCompose } from '../lib/docker.ts';
|
|
4
|
+
import { ensureStagedState, fullComposeArgs, buildManagedServiceNames } from '../lib/staging.ts';
|
|
5
5
|
|
|
6
6
|
export default defineCommand({
|
|
7
7
|
meta: {
|
|
@@ -14,35 +14,66 @@ export default defineCommand({
|
|
|
14
14
|
description: 'Service names to start (omit for all)',
|
|
15
15
|
required: false,
|
|
16
16
|
},
|
|
17
|
+
'with-admin': {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
description: 'Include admin UI and docker-socket-proxy',
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
17
22
|
},
|
|
18
23
|
async run({ args }) {
|
|
19
24
|
const services = args._ ?? [];
|
|
20
|
-
|
|
25
|
+
const withAdmin = args['with-admin'] ?? false;
|
|
26
|
+
await runStartAction(services, { withAdmin });
|
|
21
27
|
},
|
|
22
28
|
});
|
|
23
29
|
|
|
24
|
-
export async function runStartAction(
|
|
30
|
+
export async function runStartAction(
|
|
31
|
+
services: string[],
|
|
32
|
+
opts?: { withAdmin?: boolean },
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const withAdmin = opts?.withAdmin ?? false;
|
|
35
|
+
|
|
25
36
|
if (services.length === 0) {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const token = await loadAdminToken();
|
|
31
|
-
if (running && token) {
|
|
32
|
-
console.log(JSON.stringify(await adminRequest('/admin/install', { method: 'POST' }), null, 2));
|
|
37
|
+
// Try admin delegation first (gives admin's scheduler/audit a chance to observe)
|
|
38
|
+
const adminResult = await tryAdminRequest('/admin/install', { method: 'POST' });
|
|
39
|
+
if (adminResult !== null) {
|
|
40
|
+
console.log(JSON.stringify(adminResult, null, 2));
|
|
33
41
|
return;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
// Direct
|
|
37
|
-
await
|
|
44
|
+
// Direct compose — stage artifacts and start managed services
|
|
45
|
+
const state = await ensureStagedState();
|
|
46
|
+
const composeArgs = fullComposeArgs(state);
|
|
47
|
+
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
|
+
}
|
|
38
55
|
return;
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
// Start specific services — try admin first, fall back to direct compose
|
|
41
59
|
for (const service of services) {
|
|
42
|
-
const
|
|
60
|
+
const adminResult = await tryAdminRequest('/admin/containers/up', {
|
|
43
61
|
method: 'POST',
|
|
44
62
|
body: JSON.stringify({ service }),
|
|
45
63
|
});
|
|
46
|
-
|
|
64
|
+
if (adminResult !== null) {
|
|
65
|
+
console.log(JSON.stringify(adminResult, null, 2));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Direct compose for specific service
|
|
70
|
+
const state = await ensureStagedState();
|
|
71
|
+
const composeArgs = fullComposeArgs(state);
|
|
72
|
+
// If starting admin explicitly, include the admin profile
|
|
73
|
+
if (service === 'admin' || service === 'docker-socket-proxy') {
|
|
74
|
+
await runDockerCompose([...composeArgs, '--profile', 'admin', 'up', '-d', service]);
|
|
75
|
+
} else {
|
|
76
|
+
await runDockerCompose([...composeArgs, 'up', '-d', service]);
|
|
77
|
+
}
|
|
47
78
|
}
|
|
48
79
|
}
|
package/src/commands/status.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 } from '../lib/staging.ts';
|
|
3
5
|
|
|
4
6
|
export default defineCommand({
|
|
5
7
|
meta: {
|
|
@@ -7,6 +9,15 @@ export default defineCommand({
|
|
|
7
9
|
description: 'Show container status',
|
|
8
10
|
},
|
|
9
11
|
async run() {
|
|
10
|
-
|
|
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
|
+
const state = await ensureStagedState();
|
|
21
|
+
await runDockerCompose([...fullComposeArgs(state), 'ps', '--format', 'table']);
|
|
11
22
|
},
|
|
12
23
|
});
|
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
|
}
|