orcasynth 1.3.0 → 1.4.0
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 +82 -34
- package/dist/cli/index.js +40 -2
- package/dist/cli/install/index.js +39 -2
- package/dist/cli/menu.js +12 -30
- package/dist/cli/setup.js +56 -17
- package/dist/cli/setupWizard.js +65 -16
- package/dist/cli/systemd.js +24 -0
- package/dist/cli/update.js +30 -1
- package/package.json +1 -1
- package/web-dist/.next/BUILD_ID +1 -1
- package/web-dist/.next/build-manifest.json +3 -3
- package/web-dist/.next/server/app/_global-error.html +1 -1
- package/web-dist/.next/server/app/_global-error.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.html +1 -1
- package/web-dist/.next/server/app/_not-found.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web-dist/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.html +1 -1
- package/web-dist/.next/server/app/account.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/account/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/account.segments/account.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.html +1 -1
- package/web-dist/.next/server/app/dash.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/dash/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/dash.segments/dash.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.html +1 -1
- package/web-dist/.next/server/app/escalations.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/escalations/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/escalations.segments/escalations.segment.rsc +1 -1
- package/web-dist/.next/server/app/index.html +1 -1
- package/web-dist/.next/server/app/index.rsc +1 -1
- package/web-dist/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.html +1 -1
- package/web-dist/.next/server/app/kanban.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/kanban/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/kanban.segments/kanban.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.html +1 -1
- package/web-dist/.next/server/app/onboarding.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.html +1 -1
- package/web-dist/.next/server/app/projects.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/projects/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/projects.segments/projects.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.html +1 -1
- package/web-dist/.next/server/app/sessions.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/sessions/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/sessions.segments/sessions.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.html +1 -1
- package/web-dist/.next/server/app/settings.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.html +1 -1
- package/web-dist/.next/server/app/tasks.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.html +1 -1
- package/web-dist/.next/server/app/timeline.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/timeline/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/timeline.segments/timeline.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.html +1 -1
- package/web-dist/.next/server/app/users.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/_full.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/_head.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/_index.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/_tree.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/users/__PAGE__.segment.rsc +1 -1
- package/web-dist/.next/server/app/users.segments/users.segment.rsc +1 -1
- package/web-dist/.next/server/chunks/[root-of-the-server]__1wxxtv8._.js +1 -1
- package/web-dist/.next/server/middleware-build-manifest.js +3 -3
- package/web-dist/.next/server/pages/404.html +1 -1
- package/web-dist/.next/server/pages/500.html +1 -1
- /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_buildManifest.js +0 -0
- /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_clientMiddlewareManifest.js +0 -0
- /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
**Control autonomous coding agents — without losing control.**
|
|
6
6
|
|
|
7
|
-
Plan work, launch isolated coding agents, watch every session, and step in
|
|
8
|
-
before risky
|
|
7
|
+
Plan the work, launch isolated coding agents, watch every session live, and step in
|
|
8
|
+
before a risky change ever reaches your codebase.
|
|
9
9
|
|
|
10
10
|
`Plan · Dispatch · Observe · Intervene`
|
|
11
11
|
|
|
12
|
-
Orcasynth is a self-hosted daemon that
|
|
13
|
-
Codex) in isolated `tmux` sessions — with a REST API, a CLI,
|
|
12
|
+
Orcasynth is a self-hosted daemon that orchestrates autonomous coding agents
|
|
13
|
+
(Claude Code, OpenCode, Codex) in isolated `tmux` sessions — with a REST API, a CLI,
|
|
14
|
+
and a real-time Next.js web UI. No SaaS, no lock-in: your machine, your agents, your code.
|
|
14
15
|
|
|
15
16
|
[](https://github.com/dragocz1995/orcasynth/actions/workflows/ci.yml)
|
|
16
17
|
[](./LICENSE)
|
|
@@ -21,32 +22,51 @@ Codex) in isolated `tmux` sessions — with a REST API, a CLI, and a real-time w
|
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
25
|
+
## Why Orcasynth
|
|
26
|
+
|
|
27
|
+
Coding agents are powerful but messy to run at scale: one terminal per agent, no shared
|
|
28
|
+
view of what's happening, and no safety net when an agent decides to `rm -rf` something.
|
|
29
|
+
|
|
30
|
+
Orcasynth puts a control plane in front of them. Hand it a goal and it plans the work,
|
|
31
|
+
spawns the right agent for each step in its own `tmux` session, streams every keystroke to
|
|
32
|
+
your browser, and gates dangerous actions behind a human when you want it to. When you
|
|
33
|
+
trust it more, you turn the autonomy up; when you trust it less, you turn it down.
|
|
34
|
+
|
|
24
35
|
## What it does
|
|
25
36
|
|
|
26
|
-
- **Autopilot planning.** Give the Pilot a goal
|
|
27
|
-
phases,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **
|
|
43
|
-
|
|
37
|
+
- **Autopilot planning.** Give the Pilot a goal and an LLM decomposes it into ordered
|
|
38
|
+
phases, chains them by dependency, and can name an agent per phase. Phases only start
|
|
39
|
+
once the phases they depend on are done.
|
|
40
|
+
- **Per-model descriptions & per-phase model selection.** Write a capability description
|
|
41
|
+
for each model in Settings, flip on "Autopilot picks the model," and the planner chooses
|
|
42
|
+
the best-suited model for each phase from those descriptions — validated against your
|
|
43
|
+
allow-list, falling back to the default on anything invalid.
|
|
44
|
+
- **Agent-agnostic spawning.** Runs Claude Code, OpenCode, or Codex in isolated `tmux`
|
|
45
|
+
sessions, configurable per task. Each agent receives the task context and closes its own
|
|
46
|
+
task when it's done.
|
|
47
|
+
- **Autonomy levels (L0–L3).** Choose how much rope each mission gets — from
|
|
48
|
+
**L0 · Recommend** (plan only, nothing runs until you approve) through **L1 · Assist**
|
|
49
|
+
and **L2 · Pilot** to **L3 · Auto** (full autonomy). The overseer's decision engine
|
|
50
|
+
auto-clears agent permission prompts when confidence is high and the action is safe, and
|
|
51
|
+
escalates anything destructive or uncertain to a human. Operations like `rm -rf`, dropping
|
|
52
|
+
tables, force-pushes, or touching `.env` always escalate, whatever the level.
|
|
53
|
+
- **Live web UI with one-click intervention.** Tasks, a kanban board with a calendar,
|
|
54
|
+
missions with phase progress, a timeline, and real-time `tmux` session previews you can
|
|
55
|
+
jump into and take over. Full EN/CS internationalization built in.
|
|
56
|
+
- **Self-healing.** A stuck-session detector revives agents that die without closing out
|
|
57
|
+
(and blocks the task after repeated failures instead of crash-looping). A janitor sweeps
|
|
58
|
+
up finished sessions. Live token and cost usage is shown per run.
|
|
59
|
+
- **Multi-user RBAC.** Admin and member roles, per-project assignments, per-user model
|
|
60
|
+
allow-lists, profiles and avatars, and a first-run onboarding that needs no login until
|
|
61
|
+
the first admin is created.
|
|
62
|
+
- **Self-hosted & lightweight.** A single SQLite-backed daemon (Hono + SSE) plus a Next.js
|
|
63
|
+
front end. No external services required beyond your own LLM provider.
|
|
44
64
|
|
|
45
65
|
## Screenshots
|
|
46
66
|
|
|
47
67
|
<div align="center">
|
|
48
68
|
|
|
49
|
-
**Dashboard** — live agents, active missions, autopilot spotlight, and recent outcomes at a glance.
|
|
69
|
+
**Dashboard** — live agents, active missions, the autopilot spotlight, and recent outcomes at a glance.
|
|
50
70
|
|
|
51
71
|

|
|
52
72
|
|
|
@@ -54,7 +74,7 @@ Codex) in isolated `tmux` sessions — with a REST API, a CLI, and a real-time w
|
|
|
54
74
|
|
|
55
75
|
| | |
|
|
56
76
|
|---|---|
|
|
57
|
-
| **Tasks** — list + detail with live agent output and token usage.  | **Kanban** — open / in-progress / blocked / closed, with mission progress.  |
|
|
77
|
+
| **Tasks** — list + detail with live agent output and token usage.  | **Kanban** — open / in-progress / blocked / closed, with mission progress and a calendar.  |
|
|
58
78
|
| **Missions** — phase graph and task flow for an autopilot run (folded into Tasks).  | **Timeline** — a live activity feed across tasks, missions, and signals.  |
|
|
59
79
|
| **Sessions** — real-time `tmux` agent previews with one-click intervention.  | **Terminal** — the full agent TUI, including human-in-the-loop approvals.  |
|
|
60
80
|
| **Projects** — a built-in Monaco editor with the project file tree.  | **Settings** — model presets & descriptions, providers, autopilot, and defaults.  |
|
|
@@ -86,8 +106,8 @@ orca update # update to the latest release from npm
|
|
|
86
106
|
```
|
|
87
107
|
|
|
88
108
|
Requires **Node ≥ 22** and **tmux**. On first run, `orca` walks you through a quick
|
|
89
|
-
setup — admin account, LLM provider + API key, default model. Your data (config,
|
|
90
|
-
SQLite database and logs) lives in **`~/.config/orca/`** and survives every update.
|
|
109
|
+
setup — admin account, LLM provider + API key, and a default model. Your data (config,
|
|
110
|
+
the SQLite database, and logs) lives in **`~/.config/orca/`** and survives every update.
|
|
91
111
|
|
|
92
112
|
Then open <http://localhost:4500> and sign in.
|
|
93
113
|
|
|
@@ -111,12 +131,36 @@ npm start -- -p 4500
|
|
|
111
131
|
Open <http://localhost:4500> and sign in. Configure your LLM provider and models in
|
|
112
132
|
**Settings → Autopilot / Models**, then create a task or engage an autopilot mission.
|
|
113
133
|
|
|
114
|
-
The CLI auto-starts
|
|
134
|
+
The CLI talks to the daemon over the REST API and auto-starts it if it isn't running:
|
|
115
135
|
|
|
116
136
|
```bash
|
|
117
|
-
node dist/cli/index.js ls
|
|
118
|
-
node dist/cli/index.js close <id>
|
|
137
|
+
node dist/cli/index.js ls # list tasks
|
|
138
|
+
node dist/cli/index.js close <id> # close a task
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
|
|
119
143
|
```
|
|
144
|
+
goal
|
|
145
|
+
│
|
|
146
|
+
▼
|
|
147
|
+
┌───────────┐ phases + deps ┌─────────────┐ spawn ┌──────────────┐
|
|
148
|
+
│ Pilot │ ─────────────────► │ Overseer │ ─────────► │ Agent (tmux) │
|
|
149
|
+
│ (planner) │ │ (scheduler, │ │ Claude Code / │
|
|
150
|
+
└───────────┘ │ decisions) │ ◄───────── │ OpenCode / │
|
|
151
|
+
└─────────────┘ signals │ Codex │
|
|
152
|
+
│ └──────────────┘
|
|
153
|
+
│ escalate
|
|
154
|
+
▼
|
|
155
|
+
human-in-the-loop
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The **Pilot** decomposes a goal into a dependency-ordered set of phases. The **Overseer**
|
|
159
|
+
schedules ready phases, spawns the right **Agent** for each one in its own `tmux` session,
|
|
160
|
+
and watches the output. A deriver reads each session and emits signals — `working`,
|
|
161
|
+
`needs_input`, `complete`. When an agent hits a permission prompt, the decision engine
|
|
162
|
+
either clears it automatically (high confidence, non-destructive, within the mission's
|
|
163
|
+
autonomy level) or escalates it to a human.
|
|
120
164
|
|
|
121
165
|
## Architecture
|
|
122
166
|
|
|
@@ -125,12 +169,14 @@ is a thin client over the REST API + SSE event stream.
|
|
|
125
169
|
|
|
126
170
|
| Layer | What lives there |
|
|
127
171
|
|-------|------------------|
|
|
128
|
-
| `src/store` | SQLite stores (tasks, missions, agents, config, users) |
|
|
129
|
-
| `src/overseer` | mission engine,
|
|
172
|
+
| `src/store` | SQLite stores (tasks, missions, agents, config, users, projects, events) via `better-sqlite3` |
|
|
173
|
+
| `src/overseer` | mission engine, planner, scheduler, decision engine, stuck-detector, janitor |
|
|
130
174
|
| `src/spawn` · `src/tmux` | agent command building + tmux driver |
|
|
131
|
-
| `src/deriver` | derives signals from agent output (working /
|
|
132
|
-
| `src/
|
|
133
|
-
| `
|
|
175
|
+
| `src/deriver` | derives signals from agent output (`working` / `needs_input` / `complete`) |
|
|
176
|
+
| `src/integrations` | per-executor token/cost usage extraction |
|
|
177
|
+
| `src/api` | Hono REST server + SSE event bus |
|
|
178
|
+
| `src/cli` · `src/daemon` | the `orca` CLI and the daemon entrypoint |
|
|
179
|
+
| `web/modules` | feature modules (tasks, kanban, sessions, timeline, projects, settings, …) |
|
|
134
180
|
|
|
135
181
|
See [`docs/`](./docs) for the [API](./docs/API.md), [architecture](./docs/ARCHITECTURE.md),
|
|
136
182
|
[concepts](./docs/CONCEPTS.md), [CLI](./docs/CLI.md), and [development](./docs/DEVELOPMENT.md) guides.
|
|
@@ -158,3 +204,5 @@ Star the repo if you find it useful — it helps others discover the project.
|
|
|
158
204
|
## License
|
|
159
205
|
|
|
160
206
|
[MIT](./LICENSE)
|
|
207
|
+
</content>
|
|
208
|
+
</invoke>
|
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,45 @@ import { OrcaClient } from './client.js';
|
|
|
7
7
|
import { defaultLifecycleDeps, runLifecycle } from './commands.js';
|
|
8
8
|
import { menu } from './menu.js';
|
|
9
9
|
const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
|
|
10
|
-
const USAGE =
|
|
10
|
+
const USAGE = "usage: orca [command] [options] — run `orca --help` for the full command list";
|
|
11
|
+
/** The full, grouped help shown for `orca --help`. Kept as a function so the version is interpolated. */
|
|
12
|
+
function helpText(version) {
|
|
13
|
+
return `🐋 orca ${version} — control plane for autonomous coding agents
|
|
14
|
+
|
|
15
|
+
USAGE
|
|
16
|
+
orca open the interactive launcher menu (in a terminal)
|
|
17
|
+
orca <command> [options]
|
|
18
|
+
|
|
19
|
+
SETUP
|
|
20
|
+
install provision orca as a service: systemd units, a reverse proxy
|
|
21
|
+
and the first admin (run as root). See \`orca install --help\`.
|
|
22
|
+
|
|
23
|
+
SERVICE
|
|
24
|
+
up start the daemon (:4400) and web UI (:4500) in the background
|
|
25
|
+
down stop the daemon and web UI
|
|
26
|
+
status show which services are running and healthy
|
|
27
|
+
update update to the latest npm release and restart in place
|
|
28
|
+
|
|
29
|
+
TASKS
|
|
30
|
+
ls list all tasks (JSON)
|
|
31
|
+
ready list tasks ready to run (JSON)
|
|
32
|
+
sessions list live agent sessions (JSON)
|
|
33
|
+
close <id> [options] close a task
|
|
34
|
+
--summary "<text>" closing note
|
|
35
|
+
--outcome ok|fail record the outcome
|
|
36
|
+
|
|
37
|
+
AGENT-FACING (invoked by running agents — rarely needed by hand)
|
|
38
|
+
plan submit --phases '<json>' submit an autopilot plan (needs ORCA_PLAN_JOB)
|
|
39
|
+
overseer poll wait for the next decision (needs ORCA_MISSION)
|
|
40
|
+
overseer decide --id <id> … resolve a decision: --approve | --escalate | --choice <optionId>
|
|
41
|
+
[--confidence <0..1>] [--rationale "<text>"]
|
|
42
|
+
|
|
43
|
+
OPTIONS
|
|
44
|
+
-h, --help show this help
|
|
45
|
+
-v, --version print the version
|
|
46
|
+
|
|
47
|
+
Docs & issues: https://github.com/dragocz1995/orcasynth`;
|
|
48
|
+
}
|
|
11
49
|
/** Commands that talk to the daemon API — only these justify auto-starting it. Everything else
|
|
12
50
|
* (help, unknown verbs) must NOT spawn a daemon: a stray detached daemon squats the port and starves
|
|
13
51
|
* the systemd-managed one into a restart loop. */
|
|
@@ -160,7 +198,7 @@ async function main() {
|
|
|
160
198
|
}
|
|
161
199
|
// Help / bare non-TTY invocation: print usage and stop. Must NOT fall through to ensureDaemon.
|
|
162
200
|
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
|
|
163
|
-
console.log(
|
|
201
|
+
console.log(helpText(version));
|
|
164
202
|
return;
|
|
165
203
|
}
|
|
166
204
|
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
@@ -7,7 +7,7 @@ import { ensureServiceUser, userHome } from './serviceUser.js';
|
|
|
7
7
|
import { detectAgentClis, installCommand } from './agentClis.js';
|
|
8
8
|
import { daemonUnit, webUnit } from './systemdUnits.js';
|
|
9
9
|
import { detectProxy, nginxVhost, apacheVhost, certbotCommand } from './proxy.js';
|
|
10
|
-
import { applySetup, buildSetupPlan, isFirstRun } from '../setup.js';
|
|
10
|
+
import { applySetup, buildSetupPlan, defaultExecForCli, isFirstRun } from '../setup.js';
|
|
11
11
|
import { runSetupWizard } from '../setupWizard.js';
|
|
12
12
|
import { INSTALL_INFO_PATH, serializeInstallInfo } from '../installInfo.js';
|
|
13
13
|
const DAEMON_PORT = Number(process.env.ORCA_PORT ?? 4400);
|
|
@@ -237,8 +237,12 @@ async function planFromArgs(r, args) {
|
|
|
237
237
|
: agentsRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
238
238
|
const adminUser = flag(args, '--admin-user');
|
|
239
239
|
const adminPass = flag(args, '--admin-pass');
|
|
240
|
+
// `--autopilot-cli <claude|opencode|codex>` runs autopilot through an agent CLI (no API key);
|
|
241
|
+
// otherwise the --llm-* flags configure the hosted-API engine.
|
|
242
|
+
const autopilotCli = flag(args, '--autopilot-cli');
|
|
243
|
+
const pilotExec = autopilotCli ? defaultExecForCli(autopilotCli, flag(args, '--autopilot-model')) : undefined;
|
|
240
244
|
const admin = adminUser && adminPass
|
|
241
|
-
? { username: adminUser, password: adminPass, apiUrl: flag(args, '--llm-url') ?? 'https://api.openai.com/v1', apiKey: flag(args, '--llm-key') ?? '', model: flag(args, '--llm-model') ?? 'gpt-4o-mini' }
|
|
245
|
+
? { username: adminUser, password: adminPass, pilotExec, apiUrl: flag(args, '--llm-url') ?? 'https://api.openai.com/v1', apiKey: flag(args, '--llm-key') ?? '', model: flag(args, '--llm-model') ?? 'gpt-4o-mini' }
|
|
242
246
|
: null;
|
|
243
247
|
return {
|
|
244
248
|
installTmux: !args.includes('--no-tmux'),
|
|
@@ -378,7 +382,40 @@ function planSummary(plan) {
|
|
|
378
382
|
}
|
|
379
383
|
/** `orca install` — provision a fresh Debian/Ubuntu box. Run as root. Pass `--unattended` (with flags)
|
|
380
384
|
* for a non-interactive install; otherwise an interactive wizard collects every answer. */
|
|
385
|
+
const INSTALL_HELP = `🐋 orca install — provision a fresh Debian/Ubuntu box as an orca service (run as root)
|
|
386
|
+
|
|
387
|
+
USAGE
|
|
388
|
+
orca install interactive wizard (recommended)
|
|
389
|
+
orca install --unattended [options]
|
|
390
|
+
|
|
391
|
+
OPTIONS
|
|
392
|
+
--unattended run non-interactively from the flags below
|
|
393
|
+
--user <name> service user that runs the agents (default: orca)
|
|
394
|
+
--agents <list> agent CLIs to install: all | none | claude,opencode,codex
|
|
395
|
+
--no-tmux skip installing tmux
|
|
396
|
+
|
|
397
|
+
Deployment (pick one; default is localhost):
|
|
398
|
+
--domain <host> serve on a domain behind a reverse proxy (+ Let's Encrypt HTTPS)
|
|
399
|
+
--ip <addr> | --host <addr> serve directly on the public IP and port (no proxy)
|
|
400
|
+
--localhost bind to localhost only
|
|
401
|
+
--proxy <nginx|apache|none> reverse proxy to configure for --domain
|
|
402
|
+
--email <addr> contact email for Let's Encrypt renewal notices
|
|
403
|
+
|
|
404
|
+
First admin + autopilot:
|
|
405
|
+
--admin-user <name> create the first admin account
|
|
406
|
+
--admin-pass <pass> admin password
|
|
407
|
+
--autopilot-cli <cli> run autopilot through an agent CLI (claude|opencode|codex) — no API key
|
|
408
|
+
--autopilot-model <spec> model for --autopilot-cli opencode (e.g. anthropic/claude-sonnet-4-5)
|
|
409
|
+
--llm-url <url> hosted-API engine: base URL (default: https://api.openai.com/v1)
|
|
410
|
+
--llm-key <key> hosted-API engine: API key
|
|
411
|
+
--llm-model <name> hosted-API engine: model (default: gpt-4o-mini)
|
|
412
|
+
|
|
413
|
+
-h, --help show this help`;
|
|
381
414
|
export async function install(args = []) {
|
|
415
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
416
|
+
console.log(INSTALL_HELP);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
382
419
|
const r = realRunner();
|
|
383
420
|
const unattended = args.includes('--unattended');
|
|
384
421
|
p.intro(`🐋 orca install${unattended ? ' (unattended)' : ''}`);
|
package/dist/cli/menu.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import { status } from './launcher.js';
|
|
4
4
|
import { defaultLifecycleDeps, formatStatus, runLifecycle } from './commands.js';
|
|
5
5
|
import { isFirstRun } from './setup.js';
|
|
6
6
|
import { runSetupWizard } from './setupWizard.js';
|
|
7
7
|
import { readInstallInfo } from './installInfo.js';
|
|
8
|
+
import { update } from './update.js';
|
|
9
|
+
import { SERVICES, runCmd, systemctl, servicesActive } from './systemd.js';
|
|
8
10
|
const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
|
|
9
|
-
const SERVICES = ['orca-daemon', 'orca-web'];
|
|
10
11
|
/** Open a URL in the user's default browser, cross-platform, fire-and-forget. */
|
|
11
12
|
function openUrl(url) {
|
|
12
13
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
@@ -16,26 +17,6 @@ function openUrl(url) {
|
|
|
16
17
|
}
|
|
17
18
|
catch { /* headless box — ignore */ }
|
|
18
19
|
}
|
|
19
|
-
/** Run a command, resolving its stdout/exit code (never rejects). */
|
|
20
|
-
function run(cmd, args) {
|
|
21
|
-
return new Promise((resolve) => {
|
|
22
|
-
execFile(cmd, args, (err, stdout) => {
|
|
23
|
-
const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
|
|
24
|
-
resolve({ code, stdout: stdout?.toString() ?? '' });
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/** systemctl, transparently via sudo when we aren't root (so a non-root operator still works). */
|
|
29
|
-
async function systemctl(...args) {
|
|
30
|
-
const asRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
31
|
-
return asRoot ? run('systemctl', args) : run('sudo', ['systemctl', ...args]);
|
|
32
|
-
}
|
|
33
|
-
/** Whether both ORCA units report active. */
|
|
34
|
-
async function servicesActive() {
|
|
35
|
-
const r = await systemctl('is-active', ...SERVICES);
|
|
36
|
-
const states = r.stdout.trim().split('\n');
|
|
37
|
-
return states.length > 0 && states.every((s) => s.trim() === 'active');
|
|
38
|
-
}
|
|
39
20
|
/** Launcher menu for a systemd-provisioned box (`orca install`): drives the units via systemctl and
|
|
40
21
|
* shows the real public URL the operator chose — never spawns a second, port-conflicting daemon. */
|
|
41
22
|
async function systemdMenu(info, version) {
|
|
@@ -68,20 +49,21 @@ async function systemdMenu(info, version) {
|
|
|
68
49
|
continue;
|
|
69
50
|
}
|
|
70
51
|
if (action === 'logs') {
|
|
71
|
-
const r = await
|
|
52
|
+
const r = await runCmd('journalctl', ['-u', 'orca-daemon', '-n', '20', '--no-pager']);
|
|
72
53
|
p.note(r.stdout.trim() || '(no logs — try: journalctl -u orca-daemon)', 'orca-daemon');
|
|
73
54
|
continue;
|
|
74
55
|
}
|
|
75
56
|
if (action === 'update') {
|
|
76
57
|
const s = p.spinner();
|
|
77
|
-
s.start('
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
58
|
+
s.start('Checking npm for a newer version…');
|
|
59
|
+
try {
|
|
60
|
+
// Shared updater: self-locating npm --prefix + systemd-aware restart (same path as `orca update`).
|
|
61
|
+
const r = await update(process.env, { current: version });
|
|
62
|
+
s.stop(r.updated ? `Updated ${r.from} → ${r.to} and restarted.` : `Already on the latest version (${r.to}).`);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
s.stop(`Update failed: ${e.message}`);
|
|
82
66
|
}
|
|
83
|
-
await systemctl('restart', ...SERVICES);
|
|
84
|
-
s.stop('Updated and restarted.');
|
|
85
67
|
continue;
|
|
86
68
|
}
|
|
87
69
|
// start | stop | restart
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** First-run wizard logic, kept pure/injectable so the menu shell stays thin. All persistence goes
|
|
2
2
|
* through the daemon's own HTTP API (POST /users, POST /auth/login, PUT /config) — the single source
|
|
3
3
|
* of truth — rather than writing the DB directly, so there is no parallel config path. */
|
|
4
|
+
/** Autopilot CLIs that can drive missions without an API key, in recommended order. Mirrors the agent
|
|
5
|
+
* programs the daemon knows about (src/shared/execs.ts). */
|
|
6
|
+
const AUTOPILOT_CLIS = ['claude', 'opencode', 'codex'];
|
|
7
|
+
/** Default autopilot exec spec for a detected agent CLI — a well-formed `<prefix>:<model>` spec that
|
|
8
|
+
* resolveExecutor routes to the right program (so it passes the daemon's allow-list guard without
|
|
9
|
+
* needing a custom model entry). opencode is provider-agnostic, so its model comes from the caller. */
|
|
10
|
+
export function defaultExecForCli(cli, opencodeModel = 'anthropic/claude-sonnet-4-5') {
|
|
11
|
+
switch (cli) {
|
|
12
|
+
case 'claude': return 'claude:sonnet';
|
|
13
|
+
case 'codex': return 'codex:gpt-5.5';
|
|
14
|
+
case 'opencode': return `opencode:${opencodeModel}`;
|
|
15
|
+
default: return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
/** True when the daemon has no users yet — the open setup window during which the wizard may create
|
|
5
19
|
* the first admin and save the provider/key. */
|
|
6
20
|
export async function isFirstRun(fetchFn, base) {
|
|
@@ -8,33 +22,58 @@ export async function isFirstRun(fetchFn, base) {
|
|
|
8
22
|
const body = await r.json();
|
|
9
23
|
return body.needsSetup === true;
|
|
10
24
|
}
|
|
11
|
-
/** Pure mapper: wizard answers → the API payloads.
|
|
12
|
-
*
|
|
25
|
+
/** Pure mapper: wizard answers → the API payloads. With a pilotExec the autopilot runs through an
|
|
26
|
+
* agent CLI (same exec for pilot and overseer) and no API key is sent; otherwise a blank apiKey is
|
|
27
|
+
* omitted so we never overwrite an existing key with an empty string. */
|
|
13
28
|
export function buildSetupPlan(a) {
|
|
14
|
-
const autopilot =
|
|
15
|
-
|
|
29
|
+
const autopilot = a.pilotExec
|
|
30
|
+
? { pilotExec: a.pilotExec, overseerExec: a.pilotExec }
|
|
31
|
+
: { model: a.model, apiUrl: a.apiUrl };
|
|
32
|
+
if (!a.pilotExec && a.apiKey)
|
|
16
33
|
autopilot.apiKey = a.apiKey;
|
|
17
34
|
return { user: { username: a.username, password: a.password }, config: { autopilot } };
|
|
18
35
|
}
|
|
19
|
-
/** Create the admin (open during setup)
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
method: path === '/config' ? 'PUT' : 'POST',
|
|
25
|
-
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
26
|
-
body: JSON.stringify(body),
|
|
36
|
+
/** Create the admin (open during setup) and log in for a bearer token. The first user created is
|
|
37
|
+
* automatically the admin (userStore.create), so subsequent authenticated calls succeed. */
|
|
38
|
+
export async function createAdmin(fetchFn, base, user) {
|
|
39
|
+
const post = (path, body) => fetchFn(`${base}${path}`, {
|
|
40
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
|
|
27
41
|
});
|
|
28
|
-
const created = await post('/users',
|
|
42
|
+
const created = await post('/users', user);
|
|
29
43
|
if (!created.ok)
|
|
30
44
|
throw new Error(`setup: creating the admin failed (${created.status})`);
|
|
31
|
-
const login = await post('/auth/login',
|
|
45
|
+
const login = await post('/auth/login', user);
|
|
32
46
|
if (!login.ok)
|
|
33
47
|
throw new Error(`setup: login failed (${login.status})`);
|
|
34
48
|
const { token } = await login.json();
|
|
35
49
|
if (!token)
|
|
36
50
|
throw new Error('setup: login returned no token');
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
/** Persist the config patch with an admin bearer token. */
|
|
54
|
+
export async function saveConfig(fetchFn, base, token, config) {
|
|
55
|
+
const r = await fetchFn(`${base}/config`, {
|
|
56
|
+
method: 'PUT', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify(config),
|
|
57
|
+
});
|
|
58
|
+
if (!r.ok)
|
|
59
|
+
throw new Error(`setup: saving config failed (${r.status})`);
|
|
60
|
+
}
|
|
61
|
+
/** Ask the daemon which autopilot-capable agent CLIs are installed & functional for the SERVICE USER
|
|
62
|
+
* (the daemon detects on its own PATH, which is who actually runs the agents), returned in
|
|
63
|
+
* recommended order. Requires an admin bearer token. Returns [] on any failure — callers fall back
|
|
64
|
+
* to the API-key engine. */
|
|
65
|
+
export async function fetchAvailableClis(fetchFn, base, token) {
|
|
66
|
+
const r = await fetchFn(`${base}/integrations/cli-status`, { headers: { authorization: `Bearer ${token}` } });
|
|
67
|
+
if (!r.ok)
|
|
68
|
+
return [];
|
|
69
|
+
const body = await r.json();
|
|
70
|
+
const functional = new Set((body.tools ?? []).filter((t) => t.functional).map((t) => t.name));
|
|
71
|
+
return AUTOPILOT_CLIS.filter((c) => functional.has(c));
|
|
72
|
+
}
|
|
73
|
+
/** Create the admin, log in for a bearer token, then save the config. Kept for the non-interactive
|
|
74
|
+
* (unattended) install path; the interactive wizard creates the admin earlier so it can probe the
|
|
75
|
+
* daemon for installed CLIs before choosing the autopilot engine. */
|
|
76
|
+
export async function applySetup(fetchFn, base, plan) {
|
|
77
|
+
const token = await createAdmin(fetchFn, base, plan.user);
|
|
78
|
+
await saveConfig(fetchFn, base, token, plan.config);
|
|
40
79
|
}
|
package/dist/cli/setupWizard.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import {
|
|
2
|
+
import { createAdmin, saveConfig, fetchAvailableClis, defaultExecForCli } from './setup.js';
|
|
3
3
|
const PROVIDERS = {
|
|
4
4
|
OpenAI: 'https://api.openai.com/v1',
|
|
5
5
|
Anthropic: 'https://api.anthropic.com/v1',
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* cancelled. Throws only on an API failure (caller reports it). */
|
|
11
|
-
export async function runSetupWizard(base) {
|
|
12
|
-
const username = await p.text({ message: 'Admin username', initialValue: 'admin' });
|
|
13
|
-
if (p.isCancel(username))
|
|
14
|
-
return null;
|
|
15
|
-
const password = await p.password({ message: 'Admin password', validate: (v) => ((v ?? '').length < 4 ? 'At least 4 characters' : undefined) });
|
|
16
|
-
if (p.isCancel(password))
|
|
17
|
-
return null;
|
|
7
|
+
const CLI_LABEL = { claude: 'Claude Code', opencode: 'OpenCode', codex: 'Codex' };
|
|
8
|
+
/** Configure the hosted-API (relay) autopilot engine: provider URL + key + default model. */
|
|
9
|
+
async function chooseApiEngine() {
|
|
18
10
|
const choice = await p.select({
|
|
19
11
|
message: 'LLM provider',
|
|
20
12
|
options: [...Object.keys(PROVIDERS).map((k) => ({ value: k, label: k })), { value: 'Custom', label: 'Custom (enter URL)' }],
|
|
@@ -34,16 +26,73 @@ export async function runSetupWizard(base) {
|
|
|
34
26
|
const model = await p.text({ message: 'Default model', initialValue: 'gpt-4o-mini' });
|
|
35
27
|
if (p.isCancel(model))
|
|
36
28
|
return null;
|
|
37
|
-
const
|
|
29
|
+
const patch = { model, apiUrl };
|
|
30
|
+
if (apiKey)
|
|
31
|
+
patch.apiKey = apiKey;
|
|
32
|
+
return patch;
|
|
33
|
+
}
|
|
34
|
+
/** Pick the autopilot engine: an installed agent CLI (no API key — recommended) or a hosted API key.
|
|
35
|
+
* `clis` are the agent CLIs the daemon found installed for the service user, in recommended order. */
|
|
36
|
+
async function chooseAutopilot(clis) {
|
|
37
|
+
const options = [
|
|
38
|
+
...clis.map((c, i) => ({ value: `cli:${c}`, label: `${CLI_LABEL[c] ?? c} CLI`, hint: i === 0 ? 'no API key — recommended' : 'no API key' })),
|
|
39
|
+
{ value: 'apikey', label: 'LLM API key', hint: clis.length ? 'use a hosted model via an API key' : 'recommended' },
|
|
40
|
+
{ value: 'skip', label: 'Skip for now', hint: 'configure later in the web UI' },
|
|
41
|
+
];
|
|
42
|
+
const choice = await p.select({ message: 'How should Autopilot plan and oversee missions?', options });
|
|
43
|
+
if (p.isCancel(choice) || choice === 'skip')
|
|
44
|
+
return null;
|
|
45
|
+
if (choice === 'apikey')
|
|
46
|
+
return chooseApiEngine();
|
|
47
|
+
const cli = choice.slice('cli:'.length);
|
|
48
|
+
// opencode is provider-agnostic — ask which model it should use (it must already be authenticated).
|
|
49
|
+
let opencodeModel;
|
|
50
|
+
if (cli === 'opencode') {
|
|
51
|
+
const m = await p.text({ message: 'OpenCode model for autopilot', placeholder: 'provider/model', initialValue: 'anthropic/claude-sonnet-4-5' });
|
|
52
|
+
if (p.isCancel(m))
|
|
53
|
+
return null;
|
|
54
|
+
opencodeModel = m.trim() || undefined;
|
|
55
|
+
}
|
|
56
|
+
const exec = defaultExecForCli(cli, opencodeModel);
|
|
57
|
+
return { pilotExec: exec, overseerExec: exec };
|
|
58
|
+
}
|
|
59
|
+
/** Interactive first-run wizard: create the admin, then let the operator pick the autopilot engine —
|
|
60
|
+
* an installed agent CLI (no API key) or an LLM API key — and persist it through the daemon API at
|
|
61
|
+
* `base`. The admin is created up front so the CLI-detection probe can authenticate (which engines
|
|
62
|
+
* are available is only knowable as the service user). Shared by the launcher menu and `orca
|
|
63
|
+
* install`. Returns the admin credentials on success (so the caller can run a login smoke test), or
|
|
64
|
+
* null if the operator cancelled before any account was created. Throws only on an API failure. */
|
|
65
|
+
export async function runSetupWizard(base) {
|
|
66
|
+
const username = await p.text({ message: 'Admin username', initialValue: 'admin' });
|
|
67
|
+
if (p.isCancel(username))
|
|
68
|
+
return null;
|
|
69
|
+
const password = await p.password({ message: 'Admin password', validate: (v) => ((v ?? '').length < 4 ? 'At least 4 characters' : undefined) });
|
|
70
|
+
if (p.isCancel(password))
|
|
71
|
+
return null;
|
|
38
72
|
const s = p.spinner();
|
|
39
|
-
s.start('
|
|
73
|
+
s.start('Creating admin…');
|
|
74
|
+
let token;
|
|
40
75
|
try {
|
|
41
|
-
await
|
|
76
|
+
token = await createAdmin(fetch, base, { username, password });
|
|
42
77
|
s.stop('Admin account created.');
|
|
43
|
-
return { username, password };
|
|
44
78
|
}
|
|
45
79
|
catch (e) {
|
|
46
80
|
s.stop(`Setup failed: ${e.message}`);
|
|
47
81
|
throw e;
|
|
48
82
|
}
|
|
83
|
+
const clis = await fetchAvailableClis(fetch, base, token);
|
|
84
|
+
const autopilot = await chooseAutopilot(clis);
|
|
85
|
+
if (autopilot) {
|
|
86
|
+
const s2 = p.spinner();
|
|
87
|
+
s2.start('Saving autopilot settings…');
|
|
88
|
+
try {
|
|
89
|
+
await saveConfig(fetch, base, token, { autopilot });
|
|
90
|
+
s2.stop(autopilot.pilotExec ? 'Autopilot will run through your agent CLI — no API key needed.' : 'Autopilot configured.');
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
// The admin already exists and is usable; autopilot can be configured later in the web UI.
|
|
94
|
+
s2.stop(`Saving autopilot settings failed: ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { username, password };
|
|
49
98
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/** The two units `orca install` provisions. Shared so the menu and the updater drive the same names. */
|
|
3
|
+
export const SERVICES = ['orca-daemon', 'orca-web'];
|
|
4
|
+
/** Run a command, resolving its exit code + stdout (never rejects). */
|
|
5
|
+
export function runCmd(cmd, args) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
execFile(cmd, args, (err, stdout) => {
|
|
8
|
+
const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
|
|
9
|
+
resolve({ code, stdout: stdout?.toString() ?? '' });
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** systemctl, transparently via sudo when we aren't root (so a non-root operator — e.g. the services'
|
|
14
|
+
* own www-data with passwordless sudo — still manages the units). */
|
|
15
|
+
export function systemctl(...args) {
|
|
16
|
+
const asRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
17
|
+
return asRoot ? runCmd('systemctl', args) : runCmd('sudo', ['systemctl', ...args]);
|
|
18
|
+
}
|
|
19
|
+
/** Whether all ORCA units report active. */
|
|
20
|
+
export async function servicesActive() {
|
|
21
|
+
const r = await systemctl('is-active', ...SERVICES);
|
|
22
|
+
const states = r.stdout.trim().split('\n');
|
|
23
|
+
return states.length > 0 && states.every((s) => s.trim() === 'active');
|
|
24
|
+
}
|