appostle-installer 0.0.1
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/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/appostle-installer.mjs +22 -0
- package/bin/appostle.mjs +19 -0
- package/dist/appostle-installer.js +490 -0
- package/dist/appostle-installer.js.map +7 -0
- package/dist/appostle.js +50398 -0
- package/dist/appostle.js.map +7 -0
- package/dist/assets/silero_vad.onnx +0 -0
- package/dist/shell-integration/zsh/.zshenv +17 -0
- package/dist/shell-integration/zsh/appostle-integration.zsh +17 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oh Lord Agency
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# appostle-installer
|
|
2
|
+
|
|
3
|
+
One-shot installer for the Appostle daemon and the agent CLIs it talks to (Claude Code, Codex, OpenCode, Copilot, Pi). Sets up the daemon on your machine and pairs it with [appostle.app](https://appostle.app), where you control your agents from the web app, mobile app, or any paired client.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx appostle-installer
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
1. Asks which agent CLIs you want (Claude Code, Codex, Copilot, OpenCode, Pi)
|
|
12
|
+
2. Installs each one (idempotent — skips anything already present)
|
|
13
|
+
3. Walks you through each CLI's native auth flow
|
|
14
|
+
4. Installs the daemon runtime (`@appostle/cli` from public npm)
|
|
15
|
+
5. Configures auto-start so the daemon connects to [pair.appostle.app](https://pair.appostle.app) on every boot
|
|
16
|
+
6. Prints a pairing link + QR — paste into [appostle.app](https://appostle.app)
|
|
17
|
+
|
|
18
|
+
## Updating an existing install
|
|
19
|
+
|
|
20
|
+
Already paired and just want a newer daemon? Run the installer with `--update`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx appostle-installer@latest --update
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That skips provider selection, auth, and the pairing step. It only:
|
|
27
|
+
- Reinstalls `@appostle/cli@latest`
|
|
28
|
+
- Re-applies the OS auto-start config (which restarts the running service)
|
|
29
|
+
|
|
30
|
+
Safe to run from cron / a scheduled task for unattended upgrades.
|
|
31
|
+
|
|
32
|
+
## Re-show the pairing link
|
|
33
|
+
|
|
34
|
+
Need to pair another device or browser and don't have the link handy? Run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx appostle-installer@latest --pair
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prints a fresh QR + link for the local daemon and exits. No installs, no service changes — just the pairing artifact.
|
|
41
|
+
|
|
42
|
+
## Other flags
|
|
43
|
+
|
|
44
|
+
| Flag | Effect |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `--yes`, `-y` | Skip every interactive prompt; install daemon only (no providers, no auth, no pairing print). Good for CI / Dockerfile bootstrap. |
|
|
47
|
+
| `--providers=a,b,c` | Pre-pick a comma-separated list of provider IDs (e.g. `claude-code,codex`) and skip the multiselect. |
|
|
48
|
+
| `--no-auth` | Install providers without running their auth flows. Pair with `--providers=…`. |
|
|
49
|
+
| `--update` | Update-only mode (above). Implies non-interactive. |
|
|
50
|
+
| `--pair` | Print a fresh pairing link/QR and exit. Daemon must already be installed. |
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
The daemon is the Appostle runtime (`@appostle/cli`), wired to Appostle's own relay and branded web UI.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Your machine Our infra
|
|
58
|
+
──────────── ─────────
|
|
59
|
+
appostle daemon ──(E2EE WS)──▶ pair.appostle.app (Cloudflare Worker relay)
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
browser ─────────▶ appostle.app (web UI, served from devportal)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Traffic is E2E-encrypted — the relay only forwards ciphertext.
|
|
66
|
+
|
|
67
|
+
## Platform support
|
|
68
|
+
|
|
69
|
+
| OS | Auto-start | Notes |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| macOS | launchd agent at `~/Library/LaunchAgents/app.appostle.daemon.plist` | tested |
|
|
72
|
+
| Linux | systemd user unit at `~/.config/systemd/user/appostle.service` | run `loginctl enable-linger $USER` to keep it up when logged out |
|
|
73
|
+
| Windows | `.vbs` launcher in Startup folder | ⚠️ **beta — unverified**. Code path is written but hasn't been dry-run on a real Windows machine yet. Please [open an issue](https://gitlab.com/ohlord-suite/appostle-daemon-installer/-/issues) if you hit problems. No restart-on-crash; upgrade to a Windows Service later if needed. |
|
|
74
|
+
|
|
75
|
+
## Requirements
|
|
76
|
+
|
|
77
|
+
- Node.js 20+ and npm on PATH. Install from [nodejs.org](https://nodejs.org) if missing.
|
|
78
|
+
- A shell (bash/zsh/PowerShell).
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://gitlab.com/ohlord-suite/appostle-daemon-installer.git
|
|
84
|
+
cd appostle-daemon-installer
|
|
85
|
+
npm install
|
|
86
|
+
npm run dev # live-run without building
|
|
87
|
+
npm run typecheck
|
|
88
|
+
npm run build # compile to dist/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Publishing
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm publish --access public
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or push a `v*` tag to trigger the GitLab CI publish job.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT — but the daemon it installs ([@appostle/cli](https://www.npmjs.com/package/@appostle/cli)) is AGPL-3.0.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point for `npx appostle-installer`.
|
|
3
|
+
|
|
4
|
+
// Defensive Node version check.
|
|
5
|
+
// We rely on `node:util#styleText` (added in Node 20.12) via @clack/core.
|
|
6
|
+
// `engines` in package.json only warns on most installs; this is the hard gate.
|
|
7
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
8
|
+
if (major < 20 || (major === 20 && minor < 12)) {
|
|
9
|
+
console.error(
|
|
10
|
+
`\n appostle-installer requires Node.js >= 20.12.0` +
|
|
11
|
+
`\n Detected: ${process.versions.node}` +
|
|
12
|
+
`\n` +
|
|
13
|
+
`\n Upgrade via your OS package manager, or install Node 22 LTS:` +
|
|
14
|
+
`\n macOS: brew install node@22` +
|
|
15
|
+
`\n Linux: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs` +
|
|
16
|
+
`\n Windows: https://nodejs.org/en/download` +
|
|
17
|
+
`\n`,
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
import("../dist/appostle-installer.js");
|
package/bin/appostle.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point for the `appostle` CLI binary that ships inside
|
|
3
|
+
// appostle-installer. Same node version gate as appostle-installer.
|
|
4
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
5
|
+
if (major < 20 || (major === 20 && minor < 12)) {
|
|
6
|
+
console.error(
|
|
7
|
+
`\n appostle requires Node.js >= 20.12.0` +
|
|
8
|
+
`\n Detected: ${process.versions.node}` +
|
|
9
|
+
`\n` +
|
|
10
|
+
`\n Upgrade via your OS package manager, or install Node 22 LTS:` +
|
|
11
|
+
`\n macOS: brew install node@22` +
|
|
12
|
+
`\n Linux: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs` +
|
|
13
|
+
`\n Windows: https://nodejs.org/en/download` +
|
|
14
|
+
`\n`,
|
|
15
|
+
);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
import("../dist/appostle.js");
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { createRequire as __ac_createRequire } from "node:module";
|
|
2
|
+
const require = __ac_createRequire(import.meta.url);
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
var config_exports = {};
|
|
15
|
+
__export(config_exports, {
|
|
16
|
+
APPOSTLE_RELAY_ENDPOINT: () => APPOSTLE_RELAY_ENDPOINT,
|
|
17
|
+
APPOSTLE_WEB_URL: () => APPOSTLE_WEB_URL,
|
|
18
|
+
INSTALLER_PACKAGE: () => INSTALLER_PACKAGE
|
|
19
|
+
});
|
|
20
|
+
var APPOSTLE_RELAY_ENDPOINT, APPOSTLE_WEB_URL, INSTALLER_PACKAGE;
|
|
21
|
+
var init_config = __esm({
|
|
22
|
+
"src/config.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
APPOSTLE_RELAY_ENDPOINT = "pair.appostle.app:443";
|
|
25
|
+
APPOSTLE_WEB_URL = "https://appostle.app";
|
|
26
|
+
INSTALLER_PACKAGE = "appostle-installer";
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
init_config();
|
|
32
|
+
import {
|
|
33
|
+
cancel,
|
|
34
|
+
confirm,
|
|
35
|
+
intro,
|
|
36
|
+
isCancel,
|
|
37
|
+
log,
|
|
38
|
+
multiselect,
|
|
39
|
+
note,
|
|
40
|
+
outro,
|
|
41
|
+
spinner
|
|
42
|
+
} from "@clack/prompts";
|
|
43
|
+
import chalk from "chalk";
|
|
44
|
+
|
|
45
|
+
// src/daemon-setup.ts
|
|
46
|
+
init_config();
|
|
47
|
+
|
|
48
|
+
// src/util.ts
|
|
49
|
+
import { execa } from "execa";
|
|
50
|
+
import { accessSync, constants } from "node:fs";
|
|
51
|
+
import { platform } from "node:os";
|
|
52
|
+
import which from "which";
|
|
53
|
+
function currentPlatform() {
|
|
54
|
+
const p = platform();
|
|
55
|
+
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
56
|
+
throw new Error(`Unsupported platform: ${p}`);
|
|
57
|
+
}
|
|
58
|
+
async function has(binary) {
|
|
59
|
+
try {
|
|
60
|
+
await which(binary);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function resolveBinary(binary) {
|
|
67
|
+
try {
|
|
68
|
+
return await which(binary);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runInteractive(command, args = [], options = {}) {
|
|
74
|
+
await execa(command, args, { stdio: "inherit", ...options });
|
|
75
|
+
}
|
|
76
|
+
var cachedNeedsSudo = null;
|
|
77
|
+
async function npmGlobalNeedsSudo() {
|
|
78
|
+
if (cachedNeedsSudo !== null) return cachedNeedsSudo;
|
|
79
|
+
if (currentPlatform() === "win32") return cachedNeedsSudo = false;
|
|
80
|
+
if (process.getuid?.() === 0) return cachedNeedsSudo = false;
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execa("npm", ["prefix", "-g"]);
|
|
83
|
+
const prefix = stdout.trim();
|
|
84
|
+
if (!prefix) return cachedNeedsSudo = false;
|
|
85
|
+
accessSync(prefix, constants.W_OK);
|
|
86
|
+
return cachedNeedsSudo = false;
|
|
87
|
+
} catch {
|
|
88
|
+
return cachedNeedsSudo = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function npmInstallGlobal(packages) {
|
|
92
|
+
const useSudo = await npmGlobalNeedsSudo();
|
|
93
|
+
const args = ["install", "-g", ...packages];
|
|
94
|
+
if (useSudo) {
|
|
95
|
+
await runInteractive("sudo", ["-E", "npm", ...args]);
|
|
96
|
+
} else {
|
|
97
|
+
await runInteractive("npm", args);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/daemon-setup.ts
|
|
102
|
+
async function installAppostleGlobal() {
|
|
103
|
+
await npmInstallGlobal([`${INSTALLER_PACKAGE}@latest`]);
|
|
104
|
+
}
|
|
105
|
+
async function installAutostart() {
|
|
106
|
+
const os = currentPlatform();
|
|
107
|
+
if (os === "darwin") return installLaunchd();
|
|
108
|
+
if (os === "linux") return installSystemdUser();
|
|
109
|
+
if (os === "win32") return installWindowsStartup();
|
|
110
|
+
}
|
|
111
|
+
async function installLaunchd() {
|
|
112
|
+
const fs = await import("node:fs/promises");
|
|
113
|
+
const path = await import("node:path");
|
|
114
|
+
const osHome = (await import("node:os")).homedir();
|
|
115
|
+
const plistPath = path.join(osHome, "Library", "LaunchAgents", "agency.ohlord.appostle.plist");
|
|
116
|
+
const logPath = path.join(osHome, "Library", "Logs", "appostle.log");
|
|
117
|
+
const appostleBin = await resolveBinary("appostle") ?? "/usr/local/bin/appostle";
|
|
118
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
119
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
120
|
+
<plist version="1.0">
|
|
121
|
+
<dict>
|
|
122
|
+
<key>Label</key><string>agency.ohlord.appostle</string>
|
|
123
|
+
<key>ProgramArguments</key>
|
|
124
|
+
<array>
|
|
125
|
+
<string>${appostleBin}</string>
|
|
126
|
+
<string>daemon</string>
|
|
127
|
+
<string>start</string>
|
|
128
|
+
<string>--foreground</string>
|
|
129
|
+
</array>
|
|
130
|
+
<key>RunAtLoad</key><true/>
|
|
131
|
+
<key>KeepAlive</key><true/>
|
|
132
|
+
<key>StandardOutPath</key><string>${logPath}</string>
|
|
133
|
+
<key>StandardErrorPath</key><string>${logPath}</string>
|
|
134
|
+
<key>EnvironmentVariables</key>
|
|
135
|
+
<dict>
|
|
136
|
+
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
137
|
+
<key>HOME</key><string>${osHome}</string>
|
|
138
|
+
<key>NODE_ENV</key><string>production</string>
|
|
139
|
+
<key>APPOSTLE_RELAY_ENDPOINT</key><string>${APPOSTLE_RELAY_ENDPOINT}</string>
|
|
140
|
+
</dict>
|
|
141
|
+
</dict>
|
|
142
|
+
</plist>
|
|
143
|
+
`;
|
|
144
|
+
await fs.mkdir(path.dirname(plistPath), { recursive: true });
|
|
145
|
+
await fs.writeFile(plistPath, plist);
|
|
146
|
+
await writeAppostleConfig(osHome);
|
|
147
|
+
try {
|
|
148
|
+
await runInteractive("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
await runInteractive("launchctl", ["load", plistPath]);
|
|
152
|
+
}
|
|
153
|
+
async function writeAppostleConfig(osHome) {
|
|
154
|
+
const fs = await import("node:fs/promises");
|
|
155
|
+
const path = await import("node:path");
|
|
156
|
+
const configDir = path.join(osHome, ".appostle");
|
|
157
|
+
const configPath = path.join(configDir, "config.json");
|
|
158
|
+
const config = {
|
|
159
|
+
version: 1,
|
|
160
|
+
daemon: {
|
|
161
|
+
listen: "127.0.0.1:6767",
|
|
162
|
+
cors: { allowedOrigins: [APPOSTLE_WEB_URL] },
|
|
163
|
+
relay: { enabled: true, endpoint: APPOSTLE_RELAY_ENDPOINT }
|
|
164
|
+
},
|
|
165
|
+
app: { baseUrl: APPOSTLE_WEB_URL }
|
|
166
|
+
};
|
|
167
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
168
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
169
|
+
}
|
|
170
|
+
async function installSystemdUser() {
|
|
171
|
+
const fs = await import("node:fs/promises");
|
|
172
|
+
const path = await import("node:path");
|
|
173
|
+
const osHome = (await import("node:os")).homedir();
|
|
174
|
+
const unitDir = path.join(osHome, ".config", "systemd", "user");
|
|
175
|
+
const unitPath = path.join(unitDir, "appostle.service");
|
|
176
|
+
const appostleBin = await resolveBinary("appostle") ?? "/usr/bin/appostle";
|
|
177
|
+
const logPath = path.join(osHome, ".local", "state", "appostle", "daemon.log");
|
|
178
|
+
const unit = `[Unit]
|
|
179
|
+
Description=Appostle Daemon
|
|
180
|
+
After=network.target
|
|
181
|
+
|
|
182
|
+
[Service]
|
|
183
|
+
Type=simple
|
|
184
|
+
ExecStart=${appostleBin} daemon start --foreground
|
|
185
|
+
Restart=on-failure
|
|
186
|
+
RestartSec=10
|
|
187
|
+
Environment=NODE_ENV=production
|
|
188
|
+
Environment=HOME=${osHome}
|
|
189
|
+
Environment=APPOSTLE_RELAY_ENDPOINT=${APPOSTLE_RELAY_ENDPOINT}
|
|
190
|
+
StandardOutput=append:${logPath}
|
|
191
|
+
StandardError=append:${logPath}
|
|
192
|
+
|
|
193
|
+
[Install]
|
|
194
|
+
WantedBy=default.target
|
|
195
|
+
`;
|
|
196
|
+
await fs.mkdir(unitDir, { recursive: true });
|
|
197
|
+
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
198
|
+
await fs.writeFile(unitPath, unit);
|
|
199
|
+
await writeAppostleConfig(osHome);
|
|
200
|
+
await runInteractive("systemctl", ["--user", "daemon-reload"]);
|
|
201
|
+
await runInteractive("systemctl", ["--user", "enable", "--now", "appostle.service"]);
|
|
202
|
+
}
|
|
203
|
+
async function installWindowsStartup() {
|
|
204
|
+
const fs = await import("node:fs/promises");
|
|
205
|
+
const path = await import("node:path");
|
|
206
|
+
const osHome = (await import("node:os")).homedir();
|
|
207
|
+
const appostleBin = await resolveBinary("appostle") ?? "appostle.cmd";
|
|
208
|
+
const logDir = path.join(osHome, "AppData", "Local", "Appostle");
|
|
209
|
+
const logPath = path.join(logDir, "daemon.log");
|
|
210
|
+
const batPath = path.join(logDir, "appostle-daemon.bat");
|
|
211
|
+
const vbsPath = path.join(
|
|
212
|
+
osHome,
|
|
213
|
+
"AppData",
|
|
214
|
+
"Roaming",
|
|
215
|
+
"Microsoft",
|
|
216
|
+
"Windows",
|
|
217
|
+
"Start Menu",
|
|
218
|
+
"Programs",
|
|
219
|
+
"Startup",
|
|
220
|
+
"appostle.vbs"
|
|
221
|
+
);
|
|
222
|
+
const bat = `@echo off
|
|
223
|
+
set NODE_ENV=production
|
|
224
|
+
set APPOSTLE_RELAY_ENDPOINT=${APPOSTLE_RELAY_ENDPOINT}
|
|
225
|
+
"${appostleBin}" daemon start --foreground >> "${logPath}" 2>&1
|
|
226
|
+
`;
|
|
227
|
+
const vbs = `Set WshShell = CreateObject("WScript.Shell")
|
|
228
|
+
WshShell.Run """${batPath}""", 0, False
|
|
229
|
+
`;
|
|
230
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
231
|
+
await fs.mkdir(path.dirname(vbsPath), { recursive: true });
|
|
232
|
+
await fs.writeFile(batPath, bat);
|
|
233
|
+
await fs.writeFile(vbsPath, vbs);
|
|
234
|
+
await writeAppostleConfig(osHome);
|
|
235
|
+
await runInteractive("wscript", [vbsPath]);
|
|
236
|
+
}
|
|
237
|
+
async function printPairingLink() {
|
|
238
|
+
const { execa: execa2 } = await import("execa");
|
|
239
|
+
const { APPOSTLE_WEB_URL: APPOSTLE_WEB_URL2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
240
|
+
const result = await execa2("appostle", ["daemon", "pair"], { reject: false });
|
|
241
|
+
const raw = (typeof result.stdout === "string" ? result.stdout : "") + (typeof result.stderr === "string" ? result.stderr : "");
|
|
242
|
+
const rewritten = raw.replace(/https?:\/\/[^/]+\/#offer=/g, `${APPOSTLE_WEB_URL2}/#offer=`);
|
|
243
|
+
process.stdout.write(rewritten);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/providers.ts
|
|
247
|
+
var claude = {
|
|
248
|
+
id: "claude",
|
|
249
|
+
label: "Claude Code",
|
|
250
|
+
description: "Anthropic's agent CLI (MCP, streaming, deep reasoning)",
|
|
251
|
+
async detect() {
|
|
252
|
+
return has("claude");
|
|
253
|
+
},
|
|
254
|
+
async install() {
|
|
255
|
+
await npmInstallGlobal(["@anthropic-ai/claude-code@latest"]);
|
|
256
|
+
},
|
|
257
|
+
async auth() {
|
|
258
|
+
await runInteractive("claude", ["/login"]);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var codex = {
|
|
262
|
+
id: "codex",
|
|
263
|
+
label: "Codex",
|
|
264
|
+
description: "OpenAI's Codex workspace agent",
|
|
265
|
+
async detect() {
|
|
266
|
+
return has("codex");
|
|
267
|
+
},
|
|
268
|
+
async install() {
|
|
269
|
+
await npmInstallGlobal(["@openai/codex@latest"]);
|
|
270
|
+
},
|
|
271
|
+
async auth() {
|
|
272
|
+
await runInteractive("codex", ["login"]);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var copilot = {
|
|
276
|
+
id: "copilot",
|
|
277
|
+
label: "GitHub Copilot",
|
|
278
|
+
description: "Standalone Copilot CLI with ACP support",
|
|
279
|
+
async detect() {
|
|
280
|
+
return has("copilot");
|
|
281
|
+
},
|
|
282
|
+
async install() {
|
|
283
|
+
await npmInstallGlobal(["@github/copilot@latest"]);
|
|
284
|
+
},
|
|
285
|
+
async auth() {
|
|
286
|
+
await runInteractive("copilot", ["auth", "login"]);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var opencode = {
|
|
290
|
+
id: "opencode",
|
|
291
|
+
label: "OpenCode",
|
|
292
|
+
description: "Open-source multi-provider coding assistant",
|
|
293
|
+
async detect() {
|
|
294
|
+
return has("opencode");
|
|
295
|
+
},
|
|
296
|
+
async install() {
|
|
297
|
+
await npmInstallGlobal(["opencode-ai@latest"]);
|
|
298
|
+
},
|
|
299
|
+
async auth() {
|
|
300
|
+
await runInteractive("opencode", ["auth", "login"]);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
var pi = {
|
|
304
|
+
id: "pi",
|
|
305
|
+
label: "Pi",
|
|
306
|
+
description: "Minimal terminal-based agent with multi-provider LLM support",
|
|
307
|
+
async detect() {
|
|
308
|
+
return has("pi");
|
|
309
|
+
},
|
|
310
|
+
async install() {
|
|
311
|
+
await npmInstallGlobal(["@mariozechner/pi-coding-agent@latest"]);
|
|
312
|
+
},
|
|
313
|
+
async auth() {
|
|
314
|
+
await runInteractive("pi", ["login"]);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var PROVIDERS = [claude, codex, copilot, opencode, pi];
|
|
318
|
+
|
|
319
|
+
// src/index.ts
|
|
320
|
+
function parseArgs() {
|
|
321
|
+
const args = process.argv.slice(2);
|
|
322
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
323
|
+
const noAuth = args.includes("--no-auth");
|
|
324
|
+
const update = args.includes("--update");
|
|
325
|
+
const pair = args.includes("--pair");
|
|
326
|
+
const providersArg = args.find((a) => a.startsWith("--providers="));
|
|
327
|
+
const providerIds = providersArg ? providersArg.replace("--providers=", "").split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
328
|
+
return { yes, providerIds, noAuth, update, pair };
|
|
329
|
+
}
|
|
330
|
+
async function main() {
|
|
331
|
+
const { yes, providerIds, noAuth, update, pair } = parseArgs();
|
|
332
|
+
intro(
|
|
333
|
+
chalk.magentaBright("Appostle") + chalk.dim(pair ? " \u2014 pairing link" : update ? " \u2014 daemon updater" : " \u2014 daemon installer")
|
|
334
|
+
);
|
|
335
|
+
const os = currentPlatform();
|
|
336
|
+
log.info(`Detected OS: ${os}`);
|
|
337
|
+
if (!await has("npm")) {
|
|
338
|
+
log.error("`npm` not found on PATH. Install Node.js from https://nodejs.org and re-run.");
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
if (pair) {
|
|
342
|
+
note(`Paste the pairing link below into ${APPOSTLE_WEB_URL} (or scan the QR).`, "Pair");
|
|
343
|
+
try {
|
|
344
|
+
await printPairingLink();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log.error(`Could not generate pairing link: ${err.message}`);
|
|
347
|
+
log.info("Is the daemon installed? If not, run `npx appostle-installer` first.");
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
outro(chalk.green("Done."));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (update) {
|
|
354
|
+
const us = spinner();
|
|
355
|
+
us.start("Updating Appostle (npm install -g appostle-installer@latest)\u2026");
|
|
356
|
+
try {
|
|
357
|
+
await installAppostleGlobal();
|
|
358
|
+
us.stop("Daemon package up to date");
|
|
359
|
+
} catch (err) {
|
|
360
|
+
us.stop("Daemon update failed");
|
|
361
|
+
log.error(err.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
us.start(`Re-applying auto-start (${os}) to restart service\u2026`);
|
|
365
|
+
try {
|
|
366
|
+
await installAutostart();
|
|
367
|
+
us.stop("Auto-start refreshed; service restarted");
|
|
368
|
+
} catch (err) {
|
|
369
|
+
us.stop("Auto-start refresh failed");
|
|
370
|
+
log.error(err.message);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
outro(chalk.green("Update complete."));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
let chosen = [];
|
|
377
|
+
if (yes && providerIds.length === 0) {
|
|
378
|
+
log.info("--yes: skipping provider selection");
|
|
379
|
+
} else if (providerIds.length > 0) {
|
|
380
|
+
chosen = PROVIDERS.filter((p) => providerIds.includes(p.id));
|
|
381
|
+
const unknown = providerIds.filter((id) => !PROVIDERS.find((p) => p.id === id));
|
|
382
|
+
if (unknown.length > 0) log.warn(`Unknown provider ids ignored: ${unknown.join(", ")}`);
|
|
383
|
+
log.info(`Providers to install: ${chosen.map((p) => p.label).join(", ")}`);
|
|
384
|
+
} else {
|
|
385
|
+
const selection = await multiselect({
|
|
386
|
+
message: "Which agent CLIs do you want installed?",
|
|
387
|
+
required: false,
|
|
388
|
+
options: PROVIDERS.map((p) => ({
|
|
389
|
+
value: p.id,
|
|
390
|
+
label: p.label,
|
|
391
|
+
hint: p.description
|
|
392
|
+
}))
|
|
393
|
+
});
|
|
394
|
+
if (isCancel(selection)) {
|
|
395
|
+
cancel("Cancelled.");
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
chosen = PROVIDERS.filter((p) => selection.includes(p.id));
|
|
399
|
+
}
|
|
400
|
+
for (const provider of chosen) {
|
|
401
|
+
const s2 = spinner();
|
|
402
|
+
s2.start(`Checking ${provider.label}\u2026`);
|
|
403
|
+
const alreadyInstalled = await provider.detect();
|
|
404
|
+
s2.stop(
|
|
405
|
+
alreadyInstalled ? `${provider.label} already installed` : `${provider.label} not found \u2014 installing`
|
|
406
|
+
);
|
|
407
|
+
if (!alreadyInstalled) {
|
|
408
|
+
try {
|
|
409
|
+
await provider.install();
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log.error(`${provider.label} install failed: ${err.message}`);
|
|
412
|
+
if (yes || noAuth) {
|
|
413
|
+
log.warn(`Skipping ${provider.label} due to --yes/--no-auth`);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const keepGoing = await confirm({
|
|
417
|
+
message: `Skip ${provider.label} and continue?`,
|
|
418
|
+
initialValue: true
|
|
419
|
+
});
|
|
420
|
+
if (isCancel(keepGoing) || !keepGoing) {
|
|
421
|
+
cancel("Aborting.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (yes || noAuth) {
|
|
428
|
+
log.info(`Skipping ${provider.label} auth (--no-auth / --yes)`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const doAuth = await confirm({
|
|
432
|
+
message: `Authenticate ${provider.label} now?`,
|
|
433
|
+
initialValue: true
|
|
434
|
+
});
|
|
435
|
+
if (isCancel(doAuth)) {
|
|
436
|
+
cancel("Cancelled.");
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
if (doAuth) {
|
|
440
|
+
try {
|
|
441
|
+
await provider.auth();
|
|
442
|
+
} catch (err) {
|
|
443
|
+
log.warn(
|
|
444
|
+
`${provider.label} auth returned non-zero \u2014 retry later with its own CLI. (${err.message})`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
log.info(`Skipping ${provider.label} auth \u2014 you can run it later.`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (!yes) {
|
|
452
|
+
note(
|
|
453
|
+
"Installing Appostle globally (daemon + appostle CLI ship in the same package).",
|
|
454
|
+
"Daemon"
|
|
455
|
+
);
|
|
456
|
+
const proceed = await confirm({ message: "Continue?", initialValue: true });
|
|
457
|
+
if (isCancel(proceed) || !proceed) {
|
|
458
|
+
cancel("Stopped before daemon install. Re-run any time.");
|
|
459
|
+
process.exit(0);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const s = spinner();
|
|
463
|
+
s.start("Installing daemon (npm install -g appostle-installer)\u2026");
|
|
464
|
+
try {
|
|
465
|
+
await installAppostleGlobal();
|
|
466
|
+
s.stop("Daemon + appostle CLI installed");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
s.stop("Install failed");
|
|
469
|
+
log.error(err.message);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
s.start(`Configuring auto-start (${os})\u2026`);
|
|
473
|
+
await installAutostart();
|
|
474
|
+
s.stop("Auto-start configured");
|
|
475
|
+
if (os === "linux") {
|
|
476
|
+
log.info(
|
|
477
|
+
"Tip: run `loginctl enable-linger $USER` if you want the daemon to stay up when you're logged out."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (!yes) {
|
|
481
|
+
note(`Paste the pairing link below into ${APPOSTLE_WEB_URL} (or scan the QR).`, "Pair");
|
|
482
|
+
}
|
|
483
|
+
await printPairingLink();
|
|
484
|
+
outro(chalk.green(`Done. Open ${APPOSTLE_WEB_URL}`));
|
|
485
|
+
}
|
|
486
|
+
main().catch((err) => {
|
|
487
|
+
console.error(chalk.red("\nInstaller failed:"), err);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
});
|
|
490
|
+
//# sourceMappingURL=appostle-installer.js.map
|