openpalm 0.10.2 → 0.11.0-beta.2
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 +11 -19
- package/package.json +4 -2
- package/src/commands/addon.ts +5 -4
- package/src/commands/automations.ts +63 -0
- package/src/commands/install.ts +98 -280
- package/src/commands/logs.ts +1 -1
- package/src/commands/restart.ts +5 -4
- package/src/commands/rollback.ts +4 -3
- package/src/commands/scan.ts +66 -32
- package/src/commands/service.ts +5 -4
- package/src/commands/start.ts +5 -4
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +2 -4
- package/src/commands/uninstall.ts +3 -5
- package/src/commands/update.ts +19 -2
- package/src/commands/validate.ts +16 -34
- package/src/install-flow.test.ts +153 -154
- package/src/lib/admin-skills/index.test.ts +70 -0
- package/src/lib/admin-skills/index.ts +113 -0
- package/src/lib/browser.ts +20 -0
- package/src/lib/cli-compose.ts +2 -20
- package/src/lib/cli-state.ts +1 -1
- package/src/lib/docker.ts +8 -214
- package/src/lib/env.ts +12 -83
- package/src/lib/io.ts +130 -0
- package/src/lib/opencode-subprocess.ts +14 -6
- package/src/lib/paths.ts +2 -2
- package/src/lib/ui-server.ts +150 -0
- package/src/main.test.ts +76 -173
- package/src/main.ts +131 -7
- package/e2e/start-wizard-server.ts +0 -59
- package/src/commands/admin.ts +0 -43
- package/src/commands/install-services.test.ts +0 -13
- package/src/commands/install-services.ts +0 -9
- package/src/commands/upgrade.ts +0 -12
- package/src/lib/embedded-assets.ts +0 -115
- package/src/lib/varlock.ts +0 -126
- package/src/setup-wizard/index.html +0 -321
- package/src/setup-wizard/server-errors.test.ts +0 -418
- package/src/setup-wizard/server-integration.test.ts +0 -511
- package/src/setup-wizard/server.test.ts +0 -508
- package/src/setup-wizard/server.ts +0 -342
- package/src/setup-wizard/wizard-renderers.js +0 -1294
- package/src/setup-wizard/wizard-state.js +0 -346
- package/src/setup-wizard/wizard-validators.js +0 -81
- package/src/setup-wizard/wizard.css +0 -1611
- package/src/setup-wizard/wizard.js +0 -613
package/src/main.ts
CHANGED
|
@@ -1,12 +1,90 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { defineCommand, runCommand, runMain } from 'citty';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import cliPkg from '../package.json' with { type: 'json' };
|
|
5
|
+
import { resolveConfigDir } from '@openpalm/lib';
|
|
4
6
|
|
|
5
7
|
// Re-export public API used by tests and external consumers
|
|
6
8
|
export { detectHostInfo } from './lib/host-info.ts';
|
|
7
9
|
export type { HostInfo } from './lib/host-info.ts';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
const SUBCOMMAND_NAMES = new Set([
|
|
12
|
+
'install', 'uninstall', 'update', 'self-update', 'addon',
|
|
13
|
+
'start', 'stop', 'restart', 'logs', 'status', 'service',
|
|
14
|
+
'validate', 'scan', 'rollback', 'automations',
|
|
15
|
+
'--help', '-h', 'help',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
interface BareRunOpts {
|
|
19
|
+
port?: number;
|
|
20
|
+
open?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Probe the assistant container's healthcheck to decide whether the stack
|
|
25
|
+
* is already up. We hit the assistant's published host port (default 3800,
|
|
26
|
+
* overridable via OP_ASSISTANT_PORT) rather than introspect Docker so this
|
|
27
|
+
* works without docker socket access and respects whatever overrides are
|
|
28
|
+
* active.
|
|
29
|
+
*/
|
|
30
|
+
async function isAssistantHealthy(): Promise<boolean> {
|
|
31
|
+
const port = process.env.OP_ASSISTANT_PORT ?? '3800';
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
34
|
+
signal: AbortSignal.timeout(1500),
|
|
35
|
+
});
|
|
36
|
+
return res.ok;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Smart default: `openpalm` (no subcommand) detects state and does the
|
|
44
|
+
* right thing automatically.
|
|
45
|
+
*
|
|
46
|
+
* - Not installed → runs the install flow (seeds OP_HOME, spawns wizard)
|
|
47
|
+
* - Installed, stack down → starts the stack
|
|
48
|
+
* - Installed, stack up → starts the UI host server (foreground)
|
|
49
|
+
*
|
|
50
|
+
* The UI server runs in the foreground until SIGINT/SIGTERM. This is
|
|
51
|
+
* the canonical way to "run OpenPalm" — no separate `ui`/`admin`
|
|
52
|
+
* subcommand.
|
|
53
|
+
*/
|
|
54
|
+
async function autoRun(opts: BareRunOpts = {}): Promise<void> {
|
|
55
|
+
const stackEnv = join(resolveConfigDir(), 'stack', 'stack.env');
|
|
56
|
+
const isInstalled = await Bun.file(stackEnv).exists();
|
|
57
|
+
|
|
58
|
+
if (!isInstalled) {
|
|
59
|
+
const { bootstrapInstall, resolveDefaultInstallRef } = await import('./commands/install.ts') as any;
|
|
60
|
+
const version: string = typeof resolveDefaultInstallRef === 'function'
|
|
61
|
+
? await resolveDefaultInstallRef()
|
|
62
|
+
: (cliPkg.version ? `v${cliPkg.version}` : 'main');
|
|
63
|
+
await bootstrapInstall({
|
|
64
|
+
force: false,
|
|
65
|
+
version,
|
|
66
|
+
noStart: false,
|
|
67
|
+
noOpen: opts.open === false,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure the stack is up. Skip when the assistant is already healthy —
|
|
73
|
+
// calling `docker compose up -d` would otherwise recreate containers
|
|
74
|
+
// (when compose config differs, e.g. dev overlays add port bindings)
|
|
75
|
+
// and tear down test/dev port mappings.
|
|
76
|
+
const stackAlreadyUp = await isAssistantHealthy();
|
|
77
|
+
if (!stackAlreadyUp) {
|
|
78
|
+
const { runStartAction } = await import('./commands/start.ts');
|
|
79
|
+
await runStartAction([]).catch((err) => {
|
|
80
|
+
console.warn(`Warning: failed to ensure stack is running: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Start the UI host server in the foreground (blocks until SIGINT/SIGTERM).
|
|
85
|
+
const { startUIServer } = await import('./lib/ui-server.ts');
|
|
86
|
+
await startUIServer({ port: opts.port, open: opts.open });
|
|
87
|
+
}
|
|
10
88
|
|
|
11
89
|
export const mainCommand = defineCommand({
|
|
12
90
|
meta: {
|
|
@@ -14,14 +92,23 @@ export const mainCommand = defineCommand({
|
|
|
14
92
|
version: cliPkg.version,
|
|
15
93
|
description: 'OpenPalm CLI — install and manage a self-hosted OpenPalm stack',
|
|
16
94
|
},
|
|
95
|
+
args: {
|
|
96
|
+
port: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'UI server port (default: 3880 or OP_HOST_UI_PORT)',
|
|
99
|
+
},
|
|
100
|
+
open: {
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
description: 'Open browser after start (use --no-open to skip)',
|
|
103
|
+
default: true,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
17
106
|
subCommands: {
|
|
18
107
|
install: () => import('./commands/install.ts').then((m) => m.default),
|
|
19
108
|
uninstall: () => import('./commands/uninstall.ts').then((m) => m.default),
|
|
20
109
|
update: () => import('./commands/update.ts').then((m) => m.default),
|
|
21
|
-
upgrade: () => import('./commands/upgrade.ts').then((m) => m.default),
|
|
22
110
|
'self-update': () => import('./commands/self-update.ts').then((m) => m.default),
|
|
23
111
|
addon: () => import('./commands/addon.ts').then((m) => m.default),
|
|
24
|
-
admin: () => import('./commands/admin.ts').then((m) => m.default),
|
|
25
112
|
start: () => import('./commands/start.ts').then((m) => m.default),
|
|
26
113
|
stop: () => import('./commands/stop.ts').then((m) => m.default),
|
|
27
114
|
restart: () => import('./commands/restart.ts').then((m) => m.default),
|
|
@@ -31,18 +118,55 @@ export const mainCommand = defineCommand({
|
|
|
31
118
|
validate: () => import('./commands/validate.ts').then((m) => m.default),
|
|
32
119
|
scan: () => import('./commands/scan.ts').then((m) => m.default),
|
|
33
120
|
rollback: () => import('./commands/rollback.ts').then((m) => m.default),
|
|
121
|
+
automations: () => import('./commands/automations.ts').then((m) => m.default),
|
|
34
122
|
},
|
|
35
123
|
});
|
|
36
124
|
|
|
125
|
+
/** Parse `--port`/`--no-open` from a bare-command argv. */
|
|
126
|
+
function parseBareArgs(argv: string[]): BareRunOpts {
|
|
127
|
+
const opts: BareRunOpts = {};
|
|
128
|
+
for (let i = 0; i < argv.length; i++) {
|
|
129
|
+
if (argv[i] === '--port' && argv[i + 1]) {
|
|
130
|
+
opts.port = Number(argv[++i]);
|
|
131
|
+
} else if (argv[i]?.startsWith('--port=')) {
|
|
132
|
+
opts.port = Number(argv[i]!.split('=')[1]);
|
|
133
|
+
} else if (argv[i] === '--no-open') {
|
|
134
|
+
opts.open = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return opts;
|
|
138
|
+
}
|
|
139
|
+
|
|
37
140
|
/**
|
|
38
141
|
* Programmatic entry point for tests and embedding.
|
|
39
|
-
*
|
|
40
|
-
* and
|
|
142
|
+
*
|
|
143
|
+
* No-subcommand behaviour: autoRun() detects state and does the right thing.
|
|
144
|
+
* Subcommand: route through citty.
|
|
41
145
|
*/
|
|
42
146
|
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
147
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) {
|
|
148
|
+
console.log(cliPkg.version);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const hasSubcommand = argv.length > 0 && SUBCOMMAND_NAMES.has(argv[0]!);
|
|
153
|
+
if (!hasSubcommand) {
|
|
154
|
+
await autoRun(parseBareArgs(argv));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
43
158
|
await runCommand(mainCommand, { rawArgs: argv });
|
|
44
159
|
}
|
|
45
160
|
|
|
46
161
|
if (import.meta.main) {
|
|
47
|
-
|
|
162
|
+
const argv = process.argv.slice(2);
|
|
163
|
+
if (argv.length === 0 || !SUBCOMMAND_NAMES.has(argv[0]!)) {
|
|
164
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
165
|
+
console.log(cliPkg.version);
|
|
166
|
+
} else {
|
|
167
|
+
await autoRun(parseBareArgs(argv));
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
await runMain(mainCommand);
|
|
171
|
+
}
|
|
48
172
|
}
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
|
|
14
|
-
const port = parseInt(Bun.argv[2] || "18100", 10);
|
|
15
|
-
const tmpBase = `/tmp/openpalm-wizard-test-${port}`;
|
|
16
|
-
|
|
17
|
-
// Create minimal directory structure so the server can start.
|
|
18
|
-
// API endpoints that need real files are mocked at the browser level
|
|
19
|
-
// by Playwright's page.route(), so these dirs just prevent crashes.
|
|
20
|
-
mkdirSync(`${tmpBase}/config`, { recursive: true });
|
|
21
|
-
mkdirSync(`${tmpBase}/config/automations`, { recursive: true });
|
|
22
|
-
mkdirSync(`${tmpBase}/data`, { recursive: true });
|
|
23
|
-
mkdirSync(`${tmpBase}/data/assistant`, { recursive: true });
|
|
24
|
-
mkdirSync(`${tmpBase}/registry/automations`, { recursive: true });
|
|
25
|
-
mkdirSync(`${tmpBase}/stack`, { recursive: true });
|
|
26
|
-
mkdirSync(`${tmpBase}/vault/stack`, { recursive: true });
|
|
27
|
-
mkdirSync(`${tmpBase}/vault/user`, { recursive: true });
|
|
28
|
-
|
|
29
|
-
writeFileSync(`${tmpBase}/vault/stack/stack.env`, "OP_SETUP_COMPLETE=false\n");
|
|
30
|
-
writeFileSync(`${tmpBase}/vault/user/user.env`, "# test\n");
|
|
31
|
-
|
|
32
|
-
// Seed minimal asset files so performSetup() can read them if invoked
|
|
33
|
-
writeFileSync(`${tmpBase}/stack/core.compose.yml`, "services:\n admin:\n image: admin:latest\n");
|
|
34
|
-
writeFileSync(`${tmpBase}/data/assistant/opencode.jsonc`, '{"$schema":"https://opencode.ai/config.json"}\n');
|
|
35
|
-
writeFileSync(`${tmpBase}/data/assistant/AGENTS.md`, "# Agents\n");
|
|
36
|
-
writeFileSync(`${tmpBase}/vault/user/user.env.schema`, "OP_ADMIN_TOKEN=string\n");
|
|
37
|
-
writeFileSync(`${tmpBase}/vault/stack/stack.env.schema`, "OP_IMAGE_TAG=string\n");
|
|
38
|
-
writeFileSync(`${tmpBase}/registry/automations/cleanup-logs.yml`, "name: cleanup-logs\nschedule: daily\n");
|
|
39
|
-
writeFileSync(`${tmpBase}/registry/automations/cleanup-data.yml`, "name: cleanup-data\nschedule: weekly\n");
|
|
40
|
-
writeFileSync(`${tmpBase}/registry/automations/validate-config.yml`, "name: validate-config\nschedule: hourly\n");
|
|
41
|
-
|
|
42
|
-
// Override state/config home so the server doesn't touch real dirs.
|
|
43
|
-
process.env.OP_HOME = tmpBase;
|
|
44
|
-
|
|
45
|
-
const { server } = createSetupServer(port, {
|
|
46
|
-
configDir: `${tmpBase}/config`,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
console.log(`WIZARD_READY:${port}`);
|
|
50
|
-
|
|
51
|
-
// Keep alive until killed
|
|
52
|
-
process.on("SIGTERM", () => {
|
|
53
|
-
server.stop();
|
|
54
|
-
process.exit(0);
|
|
55
|
-
});
|
|
56
|
-
process.on("SIGINT", () => {
|
|
57
|
-
server.stop();
|
|
58
|
-
process.exit(0);
|
|
59
|
-
});
|
package/src/commands/admin.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { listEnabledAddonIds } from '@openpalm/lib';
|
|
3
|
-
import { ensureValidState } from '../lib/cli-state.ts';
|
|
4
|
-
import { runAddonDisableAction, runAddonEnableAction } from './addon.ts';
|
|
5
|
-
|
|
6
|
-
async function runAdminStatusAction(): Promise<void> {
|
|
7
|
-
const state = ensureValidState();
|
|
8
|
-
const enabled = listEnabledAddonIds(state.homeDir).includes('admin');
|
|
9
|
-
console.log(enabled ? 'Admin addon is enabled.' : 'Admin addon is disabled.');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const enableCmd = defineCommand({
|
|
13
|
-
meta: { name: 'enable', description: 'Enable the admin addon' },
|
|
14
|
-
async run() {
|
|
15
|
-
await runAddonEnableAction('admin');
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const disableCmd = defineCommand({
|
|
20
|
-
meta: { name: 'disable', description: 'Disable the admin addon' },
|
|
21
|
-
async run() {
|
|
22
|
-
await runAddonDisableAction('admin');
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const statusCmd = defineCommand({
|
|
27
|
-
meta: { name: 'status', description: 'Show whether the admin addon is enabled' },
|
|
28
|
-
async run() {
|
|
29
|
-
await runAdminStatusAction();
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export default defineCommand({
|
|
34
|
-
meta: {
|
|
35
|
-
name: 'admin',
|
|
36
|
-
description: 'Enable, disable, or inspect the admin addon',
|
|
37
|
-
},
|
|
38
|
-
subCommands: {
|
|
39
|
-
enable: enableCmd,
|
|
40
|
-
disable: disableCmd,
|
|
41
|
-
status: statusCmd,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { buildDeployStatusEntries } from './install-services.ts';
|
|
3
|
-
|
|
4
|
-
describe('install service helpers', () => {
|
|
5
|
-
it('builds deploy status entries for the install service list', () => {
|
|
6
|
-
const services = ['memory', 'assistant'];
|
|
7
|
-
|
|
8
|
-
expect(buildDeployStatusEntries(services, 'pending', 'Waiting...')).toEqual([
|
|
9
|
-
{ service: 'memory', status: 'pending', label: 'Waiting...' },
|
|
10
|
-
{ service: 'assistant', status: 'pending', label: 'Waiting...' },
|
|
11
|
-
]);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type DeployStatusState = 'pending' | 'pulling' | 'error';
|
|
2
|
-
|
|
3
|
-
export function buildDeployStatusEntries(
|
|
4
|
-
services: string[],
|
|
5
|
-
status: DeployStatusState,
|
|
6
|
-
label: string,
|
|
7
|
-
): Array<{ service: string; status: DeployStatusState; label: string }> {
|
|
8
|
-
return services.map(service => ({ service, status, label }));
|
|
9
|
-
}
|
package/src/commands/upgrade.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { defineCommand } from 'citty';
|
|
2
|
-
import { runUpgradeAction } from './update.ts';
|
|
3
|
-
|
|
4
|
-
export default defineCommand({
|
|
5
|
-
meta: {
|
|
6
|
-
name: 'upgrade',
|
|
7
|
-
description: 'Refresh stack assets, pull latest images, and recreate containers',
|
|
8
|
-
},
|
|
9
|
-
async run() {
|
|
10
|
-
await runUpgradeAction();
|
|
11
|
-
},
|
|
12
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core assets embedded at build time via Bun text imports.
|
|
3
|
-
*
|
|
4
|
-
* Source of truth is .openpalm/ at the repo root. Bun inlines the file
|
|
5
|
-
* contents at compile time so they're available in compiled binaries
|
|
6
|
-
* without downloading from GitHub.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// @ts-ignore — Bun text import
|
|
10
|
-
import coreCompose from "../../../../.openpalm/stack/core.compose.yml" with { type: "text" };
|
|
11
|
-
// @ts-ignore — Bun text import
|
|
12
|
-
import userEnvSchema from "../../../../.openpalm/vault/user/user.env.schema" with { type: "text" };
|
|
13
|
-
// @ts-ignore — Bun text import
|
|
14
|
-
import stackEnvSchema from "../../../../.openpalm/vault/stack/stack.env.schema" with { type: "text" };
|
|
15
|
-
|
|
16
|
-
// Addon compose files
|
|
17
|
-
// @ts-ignore — Bun text import
|
|
18
|
-
import adminCompose from "../../../../.openpalm/registry/addons/admin/compose.yml" with { type: "text" };
|
|
19
|
-
// @ts-ignore — Bun text import
|
|
20
|
-
import adminSchema from "../../../../.openpalm/registry/addons/admin/.env.schema" with { type: "text" };
|
|
21
|
-
// @ts-ignore — Bun text import
|
|
22
|
-
import chatCompose from "../../../../.openpalm/registry/addons/chat/compose.yml" with { type: "text" };
|
|
23
|
-
// @ts-ignore — Bun text import
|
|
24
|
-
import chatSchema from "../../../../.openpalm/registry/addons/chat/.env.schema" with { type: "text" };
|
|
25
|
-
// @ts-ignore — Bun text import
|
|
26
|
-
import apiCompose from "../../../../.openpalm/registry/addons/api/compose.yml" with { type: "text" };
|
|
27
|
-
// @ts-ignore — Bun text import
|
|
28
|
-
import apiSchema from "../../../../.openpalm/registry/addons/api/.env.schema" with { type: "text" };
|
|
29
|
-
// @ts-ignore — Bun text import
|
|
30
|
-
import discordCompose from "../../../../.openpalm/registry/addons/discord/compose.yml" with { type: "text" };
|
|
31
|
-
// @ts-ignore — Bun text import
|
|
32
|
-
import discordSchema from "../../../../.openpalm/registry/addons/discord/.env.schema" with { type: "text" };
|
|
33
|
-
// @ts-ignore — Bun text import
|
|
34
|
-
import slackCompose from "../../../../.openpalm/registry/addons/slack/compose.yml" with { type: "text" };
|
|
35
|
-
// @ts-ignore — Bun text import
|
|
36
|
-
import slackSchema from "../../../../.openpalm/registry/addons/slack/.env.schema" with { type: "text" };
|
|
37
|
-
// @ts-ignore — Bun text import
|
|
38
|
-
import ollamaCompose from "../../../../.openpalm/registry/addons/ollama/compose.yml" with { type: "text" };
|
|
39
|
-
// @ts-ignore — Bun text import
|
|
40
|
-
import ollamaSchema from "../../../../.openpalm/registry/addons/ollama/.env.schema" with { type: "text" };
|
|
41
|
-
// @ts-ignore — Bun text import
|
|
42
|
-
import voiceCompose from "../../../../.openpalm/registry/addons/voice/compose.yml" with { type: "text" };
|
|
43
|
-
// @ts-ignore — Bun text import
|
|
44
|
-
import voiceSchema from "../../../../.openpalm/registry/addons/voice/.env.schema" with { type: "text" };
|
|
45
|
-
// @ts-ignore — Bun text import
|
|
46
|
-
import openvikingCompose from "../../../../.openpalm/registry/addons/openviking/compose.yml" with { type: "text" };
|
|
47
|
-
// @ts-ignore — Bun text import
|
|
48
|
-
import openvikingSchema from "../../../../.openpalm/registry/addons/openviking/.env.schema" with { type: "text" };
|
|
49
|
-
// @ts-ignore — Bun text import
|
|
50
|
-
import openvikingConfig from "../../../../.openpalm/registry/addons/openviking/config/ov.conf" with { type: "text" };
|
|
51
|
-
// @ts-ignore — Bun text import
|
|
52
|
-
import memoryConfigTemplate from "../../../../.openpalm/config/memory/memory.conf.json" with { type: "text" };
|
|
53
|
-
// @ts-ignore — Bun text import
|
|
54
|
-
import cleanupLogsAutomation from "../../../../.openpalm/registry/automations/cleanup-logs.yml" with { type: "text" };
|
|
55
|
-
// @ts-ignore — Bun text import
|
|
56
|
-
import cleanupDataAutomation from "../../../../.openpalm/registry/automations/cleanup-data.yml" with { type: "text" };
|
|
57
|
-
// @ts-ignore — Bun text import
|
|
58
|
-
import validateConfigAutomation from "../../../../.openpalm/registry/automations/validate-config.yml" with { type: "text" };
|
|
59
|
-
// @ts-ignore — Bun text import
|
|
60
|
-
import healthCheckAutomation from "../../../../.openpalm/registry/automations/health-check.yml" with { type: "text" };
|
|
61
|
-
// @ts-ignore — Bun text import
|
|
62
|
-
import promptAssistantAutomation from "../../../../.openpalm/registry/automations/prompt-assistant.yml" with { type: "text" };
|
|
63
|
-
// @ts-ignore — Bun text import
|
|
64
|
-
import updateContainersAutomation from "../../../../.openpalm/registry/automations/update-containers.yml" with { type: "text" };
|
|
65
|
-
// @ts-ignore — Bun text import
|
|
66
|
-
import assistantDailyBriefingAutomation from "../../../../.openpalm/registry/automations/assistant-daily-briefing.yml" with { type: "text" };
|
|
67
|
-
|
|
68
|
-
export const EMBEDDED_ASSETS: Record<string, string> = {
|
|
69
|
-
"stack/core.compose.yml": coreCompose,
|
|
70
|
-
"registry/addons/admin/compose.yml": adminCompose,
|
|
71
|
-
"registry/addons/admin/.env.schema": adminSchema,
|
|
72
|
-
"registry/addons/chat/compose.yml": chatCompose,
|
|
73
|
-
"registry/addons/chat/.env.schema": chatSchema,
|
|
74
|
-
"registry/addons/api/compose.yml": apiCompose,
|
|
75
|
-
"registry/addons/api/.env.schema": apiSchema,
|
|
76
|
-
"registry/addons/discord/compose.yml": discordCompose,
|
|
77
|
-
"registry/addons/discord/.env.schema": discordSchema,
|
|
78
|
-
"registry/addons/slack/compose.yml": slackCompose,
|
|
79
|
-
"registry/addons/slack/.env.schema": slackSchema,
|
|
80
|
-
"registry/addons/ollama/compose.yml": ollamaCompose,
|
|
81
|
-
"registry/addons/ollama/.env.schema": ollamaSchema,
|
|
82
|
-
"registry/addons/voice/compose.yml": voiceCompose,
|
|
83
|
-
"registry/addons/voice/.env.schema": voiceSchema,
|
|
84
|
-
"registry/addons/openviking/compose.yml": openvikingCompose,
|
|
85
|
-
"registry/addons/openviking/.env.schema": openvikingSchema,
|
|
86
|
-
"registry/addons/openviking/config/ov.conf": openvikingConfig,
|
|
87
|
-
"config/memory/memory.conf.json": memoryConfigTemplate,
|
|
88
|
-
"registry/automations/cleanup-logs.yml": cleanupLogsAutomation,
|
|
89
|
-
"registry/automations/cleanup-data.yml": cleanupDataAutomation,
|
|
90
|
-
"registry/automations/validate-config.yml": validateConfigAutomation,
|
|
91
|
-
"registry/automations/health-check.yml": healthCheckAutomation,
|
|
92
|
-
"registry/automations/prompt-assistant.yml": promptAssistantAutomation,
|
|
93
|
-
"registry/automations/update-containers.yml": updateContainersAutomation,
|
|
94
|
-
"registry/automations/assistant-daily-briefing.yml": assistantDailyBriefingAutomation,
|
|
95
|
-
"vault/user/user.env.schema": userEnvSchema,
|
|
96
|
-
"vault/stack/stack.env.schema": stackEnvSchema,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Seed critical assets from embedded content (compiled into the Bun binary).
|
|
101
|
-
* Only writes files that don't already exist — never overwrites user edits.
|
|
102
|
-
*
|
|
103
|
-
* CLI-only — the admin reads assets from the filesystem at runtime.
|
|
104
|
-
*/
|
|
105
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
106
|
-
import { dirname, join } from "node:path";
|
|
107
|
-
|
|
108
|
-
export function seedEmbeddedAssets(homeDir: string): void {
|
|
109
|
-
for (const [relPath, content] of Object.entries(EMBEDDED_ASSETS)) {
|
|
110
|
-
const targetPath = join(homeDir, relPath);
|
|
111
|
-
if (existsSync(targetPath)) continue;
|
|
112
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
113
|
-
writeFileSync(targetPath, content);
|
|
114
|
-
}
|
|
115
|
-
}
|
package/src/lib/varlock.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { copyFile, mkdir, mkdtemp, unlink } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const VARLOCK_VERSION = '0.4.0';
|
|
6
|
-
|
|
7
|
-
const VARLOCK_CHECKSUMS: Record<string, string> = {
|
|
8
|
-
'varlock-linux-x64.tar.gz': '820295b271cece2679b2b9701b5285ce39354fc2f35797365fa36c70125f51ab',
|
|
9
|
-
'varlock-linux-arm64.tar.gz': 'e830baaa901b6389ecf281bdd2449bfaf7586e91fd3a7a038ec06f78e6fa92f8',
|
|
10
|
-
'varlock-macos-x64.tar.gz': 'e6abf0d97da8ff7c98b0e9044a8b71f48fbf74a0d7bfc2543a81575a07b7a03b',
|
|
11
|
-
'varlock-macos-arm64.tar.gz': '228e4c2666b9fa50a83a8713a848e7a0f0044d7fd7c9d441d43e6ebccad2f4a3',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function varlockArtifactName(): string {
|
|
15
|
-
const platformMap: Record<string, string> = {
|
|
16
|
-
linux: 'linux',
|
|
17
|
-
darwin: 'macos',
|
|
18
|
-
};
|
|
19
|
-
const archMap: Record<string, string> = {
|
|
20
|
-
x64: 'x64',
|
|
21
|
-
arm64: 'arm64',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const os = platformMap[process.platform];
|
|
25
|
-
const arch = archMap[process.arch];
|
|
26
|
-
|
|
27
|
-
if (!os || !arch) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Unsupported platform/arch for varlock: ${process.platform}/${process.arch}. ` +
|
|
30
|
-
`Supported: linux/x64, linux/arm64, darwin/x64, darwin/arm64.`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return `varlock-${os}-${arch}.tar.gz`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Co-locate a schema and env file in a temp directory so varlock can discover them.
|
|
39
|
-
*/
|
|
40
|
-
export async function prepareVarlockDir(schemaPath: string, envPath: string): Promise<string> {
|
|
41
|
-
const dir = await mkdtemp(join(tmpdir(), 'varlock-'));
|
|
42
|
-
await copyFile(schemaPath, join(dir, '.env.schema'));
|
|
43
|
-
await copyFile(envPath, join(dir, '.env'));
|
|
44
|
-
return dir;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Downloads varlock binary and caches it in ~/.cache/openpalm/bin/.
|
|
49
|
-
* Skips download if binary already exists.
|
|
50
|
-
*/
|
|
51
|
-
export async function ensureVarlock(): Promise<string> {
|
|
52
|
-
const { resolveCacheHome } = await import('@openpalm/lib');
|
|
53
|
-
const binDir = join(resolveCacheHome(), 'bin');
|
|
54
|
-
const varlockBin = join(binDir, 'varlock');
|
|
55
|
-
|
|
56
|
-
if (await Bun.file(varlockBin).exists()) {
|
|
57
|
-
return varlockBin;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await mkdir(binDir, { recursive: true });
|
|
61
|
-
|
|
62
|
-
const artifact = varlockArtifactName();
|
|
63
|
-
const expectedHash = VARLOCK_CHECKSUMS[artifact];
|
|
64
|
-
if (!expectedHash) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`No SHA-256 checksum on record for ${artifact}. ` +
|
|
67
|
-
`Cannot verify download integrity.`,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const tarballUrl = `https://github.com/dmno-dev/varlock/releases/download/varlock%40${VARLOCK_VERSION}/${artifact}`;
|
|
72
|
-
const tarballPath = join(binDir, 'varlock.tar.gz');
|
|
73
|
-
|
|
74
|
-
const response = await fetch(tarballUrl, { signal: AbortSignal.timeout(60_000) });
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
throw new Error(`Failed to download varlock tarball (HTTP ${response.status} ${response.statusText})`);
|
|
77
|
-
}
|
|
78
|
-
await Bun.write(tarballPath, response);
|
|
79
|
-
|
|
80
|
-
const hasher = new Bun.CryptoHasher('sha256');
|
|
81
|
-
hasher.update(await Bun.file(tarballPath).arrayBuffer());
|
|
82
|
-
const actualHash = hasher.digest('hex');
|
|
83
|
-
if (actualHash !== expectedHash) {
|
|
84
|
-
try { await unlink(tarballPath); } catch { /* best effort */ }
|
|
85
|
-
throw new Error(
|
|
86
|
-
`varlock tarball SHA-256 verification failed — download may be corrupted.\n` +
|
|
87
|
-
` Expected: ${expectedHash}\n` +
|
|
88
|
-
` Actual: ${actualHash}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const extractProc = Bun.spawn(
|
|
93
|
-
['tar', 'xzf', tarballPath, '--strip-components=1', '-C', binDir],
|
|
94
|
-
{
|
|
95
|
-
env: { ...process.env, HOME: process.env.HOME ?? '' },
|
|
96
|
-
stdout: 'inherit',
|
|
97
|
-
stderr: 'inherit',
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
const extractCode = await extractProc.exited;
|
|
101
|
-
if (extractCode !== 0) {
|
|
102
|
-
throw new Error(`Failed to extract varlock tarball (tar exited with code ${extractCode})`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try { await unlink(tarballPath); } catch { /* best effort */ }
|
|
106
|
-
|
|
107
|
-
const chmodProc = Bun.spawn(['chmod', '+x', varlockBin]);
|
|
108
|
-
const chmodCode = await chmodProc.exited;
|
|
109
|
-
if (chmodCode !== 0) {
|
|
110
|
-
throw new Error(`chmod +x failed for varlock binary (exit code ${chmodCode})`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// macOS: clear quarantine flag and ad-hoc codesign so Gatekeeper does not kill the binary
|
|
114
|
-
if (process.platform === 'darwin') {
|
|
115
|
-
const xattr = Bun.spawn(['xattr', '-cr', varlockBin], { stdout: 'ignore', stderr: 'ignore' });
|
|
116
|
-
await xattr.exited;
|
|
117
|
-
const codesign = Bun.spawn(['codesign', '--force', '--sign', '-', varlockBin], { stdout: 'ignore', stderr: 'ignore' });
|
|
118
|
-
await codesign.exited;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!(await Bun.file(varlockBin).exists())) {
|
|
122
|
-
throw new Error(`varlock binary not found at ${varlockBin} after install`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return varlockBin;
|
|
126
|
-
}
|