mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Merge pull request #2 from getcompanion-ai/daemon
daemon with coding agent
This commit is contained in:
commit
fa208bca73
7 changed files with 610 additions and 279 deletions
56
README.md
56
README.md
|
|
@ -39,21 +39,24 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.m
|
||||||
|
|
||||||
### Public (binary)
|
### Public (binary)
|
||||||
|
|
||||||
Use this for users on production machines where you don't want to expose source:
|
Use this for users on production machines where you don't want to expose source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run:
|
Install everything and keep it always-on (recommended for new devices):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
co-mono
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --daemon --start
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer downloads the latest release archive, writes a launcher to
|
This installer:
|
||||||
`~/.local/bin/co-mono`, and creates a private agent settings directory at
|
- Downloads the latest release (or falls back to source when needed),
|
||||||
`~/.co-mono/agent/settings.json` with remote packages.
|
- writes `~/.local/bin/co-mono` launcher,
|
||||||
|
- populates `~/.co-mono/agent/settings.json` with package list,
|
||||||
|
- installs packages (if `npm` is available),
|
||||||
|
- and can install a user systemd service for `co-mono daemon` so it stays alive.
|
||||||
|
|
||||||
Preinstalled package sources are:
|
Preinstalled package sources are:
|
||||||
|
|
||||||
|
|
@ -65,41 +68,19 @@ Preinstalled package sources are:
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
If `npm` is available, it also tries to install these packages during install.
|
If `npm` is available, it also installs these packages during install.
|
||||||
|
|
||||||
If a release has not been published yet, the installer can fallback to a source checkout automatically. Set this explicitly with:
|
If no release asset is found, the installer falls back to source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CO_MONO_FALLBACK_TO_SOURCE=1 curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash
|
CO_MONO_FALLBACK_TO_SOURCE=0 \
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --daemon --start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Keep it running
|
`public-install.sh` options:
|
||||||
|
|
||||||
Start and keep `co-mono` alive with your process supervisor of choice (systemd, launchd, supervisor, Docker, etc).
|
|
||||||
|
|
||||||
For public installs, a minimal systemd user service is:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.config/systemd/user
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --help
|
||||||
cat > ~/.config/systemd/user/co-mono.service <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=co-mono
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Environment=PI_CODING_AGENT_DIR=%h/.co-mono/agent
|
|
||||||
Environment=CO_MONO_AGENT_DIR=%h/.co-mono/agent
|
|
||||||
ExecStart=%h/.local/bin/co-mono
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user enable --now co-mono
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local (source)
|
### Local (source)
|
||||||
|
|
@ -116,11 +97,10 @@ Run:
|
||||||
./co-mono
|
./co-mono
|
||||||
```
|
```
|
||||||
|
|
||||||
Run with built-in runtime watchdog:
|
Run in background with extensions active:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CO_MONO_RUNTIME_COMMAND="python -m http.server 8765" \
|
./co-mono daemon
|
||||||
./co-mono --with-runtime-daemon
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For a user systemd setup, create `~/.config/systemd/user/co-mono.service` with:
|
For a user systemd setup, create `~/.config/systemd/user/co-mono.service` with:
|
||||||
|
|
@ -134,7 +114,7 @@ After=network-online.target
|
||||||
Type=simple
|
Type=simple
|
||||||
Environment=PI_CODING_AGENT_DIR=%h/.co-mono/agent
|
Environment=PI_CODING_AGENT_DIR=%h/.co-mono/agent
|
||||||
Environment=CO_MONO_AGENT_DIR=%h/.co-mono/agent
|
Environment=CO_MONO_AGENT_DIR=%h/.co-mono/agent
|
||||||
ExecStart=/absolute/path/to/repo/co-mono --with-runtime-daemon
|
ExecStart=/absolute/path/to/repo/co-mono daemon
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,7 @@ pi config # Enable/disable package resources
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| (default) | Interactive mode |
|
| (default) | Interactive mode |
|
||||||
| `-p`, `--print` | Print response and exit |
|
| `-p`, `--print` | Print response and exit |
|
||||||
|
| `daemon` | Start a long-lived, non-interactive daemon that keeps extensions running |
|
||||||
| `--mode json` | Output all events as JSON lines (see [docs/json.md](docs/json.md)) |
|
| `--mode json` | Output all events as JSON lines (see [docs/json.md](docs/json.md)) |
|
||||||
| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) |
|
| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) |
|
||||||
| `--export <in> [out]` | Export session to HTML |
|
| `--export <in> [out]` | Export session to HTML |
|
||||||
|
|
|
||||||
|
|
@ -184,9 +184,10 @@ ${chalk.bold("Usage:")}
|
||||||
|
|
||||||
${chalk.bold("Commands:")}
|
${chalk.bold("Commands:")}
|
||||||
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
||||||
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
||||||
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
||||||
${APP_NAME} list List installed extensions from settings
|
${APP_NAME} list List installed extensions from settings
|
||||||
|
${APP_NAME} daemon Run in long-lived daemon mode (extensions stay active)
|
||||||
${APP_NAME} config Open TUI to enable/disable package resources
|
${APP_NAME} config Open TUI to enable/disable package resources
|
||||||
${APP_NAME} <command> --help Show help for install/remove/update/list
|
${APP_NAME} <command> --help Show help for install/remove/update/list
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { SettingsManager } from "./core/settings-manager.js";
|
||||||
import { printTimings, time } from "./core/timings.js";
|
import { printTimings, time } from "./core/timings.js";
|
||||||
import { allTools } from "./core/tools/index.js";
|
import { allTools } from "./core/tools/index.js";
|
||||||
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
||||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
import { type DaemonModeOptions, InteractiveMode, runDaemonMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,6 +79,19 @@ interface PackageCommandOptions {
|
||||||
invalidOption?: string;
|
invalidOption?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printDaemonHelp(): void {
|
||||||
|
console.log(`${chalk.bold("Usage:")}
|
||||||
|
${APP_NAME} daemon [options] [messages...]
|
||||||
|
|
||||||
|
Run pi as a long-lived daemon (non-interactive) with extensions enabled.
|
||||||
|
Messages passed as positional args are sent once at startup.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--list-models [search] List available models and exit
|
||||||
|
--help, -h Show this help
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
function getPackageCommandUsage(command: PackageCommand): string {
|
function getPackageCommandUsage(command: PackageCommand): string {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "install":
|
case "install":
|
||||||
|
|
@ -540,6 +553,8 @@ async function handleConfigCommand(args: string[]): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(args: string[]) {
|
export async function main(args: string[]) {
|
||||||
|
const isDaemonCommand = args[0] === "daemon";
|
||||||
|
const parsedArgs = isDaemonCommand ? args.slice(1) : args;
|
||||||
const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE);
|
||||||
if (offlineMode) {
|
if (offlineMode) {
|
||||||
process.env.PI_OFFLINE = "1";
|
process.env.PI_OFFLINE = "1";
|
||||||
|
|
@ -558,7 +573,7 @@ export async function main(args: string[]) {
|
||||||
const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
|
const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
|
||||||
|
|
||||||
// First pass: parse args to get --extension paths
|
// First pass: parse args to get --extension paths
|
||||||
const firstPass = parseArgs(args);
|
const firstPass = parseArgs(parsedArgs);
|
||||||
|
|
||||||
// Early load extensions to discover their CLI flags
|
// Early load extensions to discover their CLI flags
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
@ -606,7 +621,7 @@ export async function main(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: parse args with extension flags
|
// Second pass: parse args with extension flags
|
||||||
const parsed = parseArgs(args, extensionFlags);
|
const parsed = parseArgs(parsedArgs, extensionFlags);
|
||||||
|
|
||||||
// Pass flag values to extensions via runtime
|
// Pass flag values to extensions via runtime
|
||||||
for (const [name, value] of parsed.unknownFlags) {
|
for (const [name, value] of parsed.unknownFlags) {
|
||||||
|
|
@ -619,7 +634,11 @@ export async function main(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.help) {
|
if (parsed.help) {
|
||||||
printHelp();
|
if (isDaemonCommand) {
|
||||||
|
printDaemonHelp();
|
||||||
|
} else {
|
||||||
|
printHelp();
|
||||||
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -629,8 +648,13 @@ export async function main(args: string[]) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
|
if (isDaemonCommand && parsed.mode === "rpc") {
|
||||||
if (parsed.mode !== "rpc") {
|
console.error(chalk.red("Cannot use --mode rpc with the daemon command."));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read piped stdin content (if any) - skip for daemon and RPC modes
|
||||||
|
if (!isDaemonCommand && parsed.mode !== "rpc") {
|
||||||
const stdinContent = await readPipedStdin();
|
const stdinContent = await readPipedStdin();
|
||||||
if (stdinContent !== undefined) {
|
if (stdinContent !== undefined) {
|
||||||
// Force print mode since interactive mode requires a TTY for keyboard input
|
// Force print mode since interactive mode requires a TTY for keyboard input
|
||||||
|
|
@ -660,7 +684,7 @@ export async function main(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
||||||
const isInteractive = !parsed.print && parsed.mode === undefined;
|
const isInteractive = !isDaemonCommand && !parsed.print && parsed.mode === undefined;
|
||||||
const mode = parsed.mode || "text";
|
const mode = parsed.mode || "text";
|
||||||
initTheme(settingsManager.getTheme(), isInteractive);
|
initTheme(settingsManager.getTheme(), isInteractive);
|
||||||
|
|
||||||
|
|
@ -765,6 +789,13 @@ export async function main(args: string[]) {
|
||||||
verbose: parsed.verbose,
|
verbose: parsed.verbose,
|
||||||
});
|
});
|
||||||
await mode.run();
|
await mode.run();
|
||||||
|
} else if (isDaemonCommand) {
|
||||||
|
const daemonOptions: DaemonModeOptions = {
|
||||||
|
initialMessage,
|
||||||
|
initialImages,
|
||||||
|
messages: parsed.messages,
|
||||||
|
};
|
||||||
|
await runDaemonMode(session, daemonOptions);
|
||||||
} else {
|
} else {
|
||||||
await runPrintMode(session, {
|
await runPrintMode(session, {
|
||||||
mode,
|
mode,
|
||||||
|
|
|
||||||
143
packages/coding-agent/src/modes/daemon-mode.ts
Normal file
143
packages/coding-agent/src/modes/daemon-mode.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* Daemon mode (always-on background execution).
|
||||||
|
*
|
||||||
|
* Starts agent extensions, accepts messages from extension sources
|
||||||
|
* (webhooks, queues, Telegram/Slack gateways, etc.), and stays alive
|
||||||
|
* until explicitly stopped.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
|
import type { AgentSession } from "../core/agent-session.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for daemon mode.
|
||||||
|
*/
|
||||||
|
export interface DaemonModeOptions {
|
||||||
|
/** First message to send at startup (can include @file content expansion by caller). */
|
||||||
|
initialMessage?: string;
|
||||||
|
/** Images to attach to the startup message. */
|
||||||
|
initialImages?: ImageContent[];
|
||||||
|
/** Additional startup messages (sent after initialMessage, one by one). */
|
||||||
|
messages?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommandContextActions(session: AgentSession) {
|
||||||
|
return {
|
||||||
|
waitForIdle: () => session.agent.waitForIdle(),
|
||||||
|
newSession: async (options?: {
|
||||||
|
parentSession?: string;
|
||||||
|
setup?: (sessionManager: typeof session.sessionManager) => Promise<void> | void;
|
||||||
|
}) => {
|
||||||
|
const success = await session.newSession({ parentSession: options?.parentSession });
|
||||||
|
if (success && options?.setup) {
|
||||||
|
await options.setup(session.sessionManager);
|
||||||
|
}
|
||||||
|
return { cancelled: !success };
|
||||||
|
},
|
||||||
|
fork: async (entryId: string) => {
|
||||||
|
const result = await session.fork(entryId);
|
||||||
|
return { cancelled: result.cancelled };
|
||||||
|
},
|
||||||
|
navigateTree: async (
|
||||||
|
targetId: string,
|
||||||
|
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
||||||
|
) => {
|
||||||
|
const result = await session.navigateTree(targetId, {
|
||||||
|
summarize: options?.summarize,
|
||||||
|
customInstructions: options?.customInstructions,
|
||||||
|
replaceInstructions: options?.replaceInstructions,
|
||||||
|
label: options?.label,
|
||||||
|
});
|
||||||
|
return { cancelled: result.cancelled };
|
||||||
|
},
|
||||||
|
switchSession: async (sessionPath: string) => {
|
||||||
|
const success = await session.switchSession(sessionPath);
|
||||||
|
return { cancelled: !success };
|
||||||
|
},
|
||||||
|
reload: async () => {
|
||||||
|
await session.reload();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run in daemon mode.
|
||||||
|
* Stays alive indefinitely unless stopped by signal or extension trigger.
|
||||||
|
*/
|
||||||
|
export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise<never> {
|
||||||
|
const { initialMessage, initialImages, messages = [] } = options;
|
||||||
|
let isShuttingDown = false;
|
||||||
|
let resolveReady: () => void = () => {};
|
||||||
|
const ready = new Promise<void>((resolve) => {
|
||||||
|
resolveReady = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shutdown = async (reason: "signal" | "extension"): Promise<void> => {
|
||||||
|
if (isShuttingDown) return;
|
||||||
|
isShuttingDown = true;
|
||||||
|
|
||||||
|
console.error(`[co-mono-daemon] shutdown requested: ${reason}`);
|
||||||
|
|
||||||
|
const runner = session.extensionRunner;
|
||||||
|
if (runner?.hasHandlers("session_shutdown")) {
|
||||||
|
await runner.emit({ type: "session_shutdown" });
|
||||||
|
}
|
||||||
|
|
||||||
|
session.dispose();
|
||||||
|
resolveReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShutdownSignal = (signal: NodeJS.Signals) => {
|
||||||
|
void shutdown("signal").catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`[co-mono-daemon] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
process.once("SIGINT", () => handleShutdownSignal("SIGINT"));
|
||||||
|
process.once("SIGTERM", () => handleShutdownSignal("SIGTERM"));
|
||||||
|
process.once("SIGQUIT", () => handleShutdownSignal("SIGQUIT"));
|
||||||
|
process.once("SIGHUP", () => handleShutdownSignal("SIGHUP"));
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (error) => {
|
||||||
|
console.error(`[co-mono-daemon] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.bindExtensions({
|
||||||
|
commandContextActions: createCommandContextActions(session),
|
||||||
|
shutdownHandler: () => {
|
||||||
|
void shutdown("extension").catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`[co-mono-daemon] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(`Extension error (${err.extensionPath}): ${err.error}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit structured events to stderr for supervisor logs.
|
||||||
|
session.subscribe((event) => {
|
||||||
|
console.error(
|
||||||
|
JSON.stringify({ type: event.type, sessionId: session.sessionId, messageCount: session.messages.length }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startup probes/messages.
|
||||||
|
if (initialMessage) {
|
||||||
|
await session.prompt(initialMessage, { images: initialImages });
|
||||||
|
}
|
||||||
|
for (const message of messages) {
|
||||||
|
await session.prompt(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[co-mono-daemon] startup complete (session=${session.sessionId ?? "unknown"})`);
|
||||||
|
|
||||||
|
// Keep process alive forever.
|
||||||
|
await ready;
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* Run modes for the coding agent.
|
* Run modes for the coding agent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { type DaemonModeOptions, runDaemonMode } from "./daemon-mode.js";
|
||||||
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js";
|
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js";
|
||||||
export { type PrintModeOptions, runPrintMode } from "./print-mode.js";
|
export { type PrintModeOptions, runPrintMode } from "./print-mode.js";
|
||||||
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js";
|
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js";
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,19 @@
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Defaults
|
||||||
REPO="${CO_MONO_REPO:-getcompanion-ai/co-mono}"
|
REPO="${CO_MONO_REPO:-getcompanion-ai/co-mono}"
|
||||||
VERSION="${CO_MONO_VERSION:-latest}"
|
VERSION="${CO_MONO_VERSION:-latest}"
|
||||||
INSTALL_DIR="${CO_MONO_INSTALL_DIR:-$HOME/.co-mono}"
|
INSTALL_DIR="${CO_MONO_INSTALL_DIR:-$HOME/.co-mono}"
|
||||||
BIN_DIR="${CO_MONO_BIN_DIR:-$HOME/.local/bin}"
|
BIN_DIR="${CO_MONO_BIN_DIR:-$HOME/.local/bin}"
|
||||||
AGENT_DIR="${CO_MONO_AGENT_DIR:-$INSTALL_DIR/agent}"
|
AGENT_DIR="${CO_MONO_AGENT_DIR:-$INSTALL_DIR/agent}"
|
||||||
RUN_INSTALL_PACKAGES="${CO_MONO_INSTALL_PACKAGES:-1}"
|
SERVICE_NAME="${CO_MONO_SERVICE_NAME:-co-mono}"
|
||||||
SKIP_REINSTALL="${CO_MONO_SKIP_REINSTALL:-0}"
|
|
||||||
INSTALL_RUNTIME_DAEMON="${CO_MONO_INSTALL_RUNTIME_DAEMON:-0}"
|
|
||||||
FALLBACK_TO_SOURCE="${CO_MONO_FALLBACK_TO_SOURCE:-1}"
|
FALLBACK_TO_SOURCE="${CO_MONO_FALLBACK_TO_SOURCE:-1}"
|
||||||
|
SKIP_REINSTALL="${CO_MONO_SKIP_REINSTALL:-0}"
|
||||||
|
RUN_INSTALL_PACKAGES="${CO_MONO_INSTALL_PACKAGES:-1}"
|
||||||
|
SETUP_DAEMON="${CO_MONO_SETUP_DAEMON:-0}"
|
||||||
|
START_DAEMON="${CO_MONO_START_DAEMON:-0}"
|
||||||
|
SKIP_SERVICE="${CO_MONO_SKIP_SERVICE:-0}"
|
||||||
|
|
||||||
DEFAULT_PACKAGES=(
|
DEFAULT_PACKAGES=(
|
||||||
"npm:@e9n/pi-channels"
|
"npm:@e9n/pi-channels"
|
||||||
|
|
@ -18,6 +22,9 @@ DEFAULT_PACKAGES=(
|
||||||
"npm:pi-teams"
|
"npm:pi-teams"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
declare -a EXTRA_PACKAGES=()
|
||||||
|
USE_DEFAULT_PACKAGES=1
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "==> $*"
|
echo "==> $*"
|
||||||
}
|
}
|
||||||
|
|
@ -27,26 +34,142 @@ fail() {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
need() {
|
has() {
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
command -v "$1" >/dev/null 2>&1
|
||||||
fail "required tool not found: $1"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
need tar
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash
|
||||||
|
bash public-install.sh [options]
|
||||||
|
|
||||||
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
|
Options:
|
||||||
|
--repo <owner/repo> Override GitHub repo for install (default: getcompanion-ai/co-mono)
|
||||||
|
--version <tag>|latest Release tag to install (default: latest)
|
||||||
|
--install-dir <path> Target directory for release contents (default: ~/.co-mono)
|
||||||
|
--bin-dir <path> Directory for co-mono launcher (default: ~/.local/bin)
|
||||||
|
--agent-dir <path> Agent config directory (default: <install-dir>/agent)
|
||||||
|
--package <pkg> Add package to installation list (repeatable)
|
||||||
|
--no-default-packages Skip default packages list
|
||||||
|
--skip-packages Skip package installation step
|
||||||
|
--daemon Install user systemd service for long-lived mode
|
||||||
|
--start Start service after install (implies --daemon)
|
||||||
|
--skip-daemon Force skip service setup/start
|
||||||
|
--fallback-to-source <0|1> Allow source fallback when release is unavailable
|
||||||
|
--skip-reinstall Keep existing install directory
|
||||||
|
--help
|
||||||
|
|
||||||
|
Env vars:
|
||||||
|
CO_MONO_INSTALL_PACKAGES=0/1
|
||||||
|
CO_MONO_SETUP_DAEMON=0/1
|
||||||
|
CO_MONO_START_DAEMON=0/1
|
||||||
|
CO_MONO_FALLBACK_TO_SOURCE=0/1
|
||||||
|
CO_MONO_SKIP_REINSTALL=1
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! has tar; then
|
||||||
|
fail "required tool not found: tar"
|
||||||
|
fi
|
||||||
|
if ! has curl && ! has wget; then
|
||||||
fail "required tool not found: curl or wget"
|
fail "required tool not found: curl or wget"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v git >/dev/null 2>&1; then
|
while [[ $# -gt 0 ]]; do
|
||||||
log "git not found; this is fine unless package install is triggered"
|
case "$1" in
|
||||||
|
--repo)
|
||||||
|
REPO="${2:?missing repo value}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
VERSION="${2:?missing version value}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--install-dir)
|
||||||
|
INSTALL_DIR="${2:?missing install dir}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bin-dir)
|
||||||
|
BIN_DIR="${2:?missing bin dir}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--agent-dir)
|
||||||
|
AGENT_DIR="${2:?missing agent dir}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--package)
|
||||||
|
EXTRA_PACKAGES+=("${2:?missing package}")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-default-packages)
|
||||||
|
USE_DEFAULT_PACKAGES=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-packages)
|
||||||
|
RUN_INSTALL_PACKAGES=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--daemon)
|
||||||
|
SETUP_DAEMON=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--start)
|
||||||
|
START_DAEMON=1
|
||||||
|
SETUP_DAEMON=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-daemon)
|
||||||
|
SETUP_DAEMON=0
|
||||||
|
START_DAEMON=0
|
||||||
|
SKIP_SERVICE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--fallback-to-source)
|
||||||
|
FALLBACK_TO_SOURCE="${2:?missing fallback value}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-reinstall)
|
||||||
|
SKIP_REINSTALL=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$FALLBACK_TO_SOURCE" != "0" && "$FALLBACK_TO_SOURCE" != "1" ]]; then
|
||||||
|
fail "CO_MONO_FALLBACK_TO_SOURCE must be 0 or 1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -d "$INSTALL_DIR" && "${SKIP_REINSTALL}" != "1" ]]; then
|
if [[ -d "$INSTALL_DIR" && "$SKIP_REINSTALL" != "1" ]]; then
|
||||||
rm -rf "$INSTALL_DIR"
|
rm -rf "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${SERVICE_NAME:-}" ]]; then
|
||||||
|
SERVICE_NAME="co-mono"
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
local url="$1"
|
||||||
|
local out="$2"
|
||||||
|
if has curl; then
|
||||||
|
curl -fsSL "$url" -o "$out"
|
||||||
|
else
|
||||||
|
wget -qO "$out" "$url"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
detect_platform() {
|
detect_platform() {
|
||||||
local os
|
local os
|
||||||
local arch
|
local arch
|
||||||
|
|
@ -57,9 +180,7 @@ detect_platform() {
|
||||||
case "$os" in
|
case "$os" in
|
||||||
darwin) os="darwin" ;;
|
darwin) os="darwin" ;;
|
||||||
linux) os="linux" ;;
|
linux) os="linux" ;;
|
||||||
mingw*|msys*|cygwin*)
|
mingw*|msys*|cygwin*) os="windows" ;;
|
||||||
os="windows"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
fail "unsupported OS: $os"
|
fail "unsupported OS: $os"
|
||||||
;;
|
;;
|
||||||
|
|
@ -80,73 +201,222 @@ detect_platform() {
|
||||||
PLATFORM="${os}-${arch}"
|
PLATFORM="${os}-${arch}"
|
||||||
}
|
}
|
||||||
|
|
||||||
download_json() {
|
|
||||||
local url="$1"
|
|
||||||
local out="$2"
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
curl -fsSL "$url" -o "$out"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -qO "$out" "$url"
|
|
||||||
else
|
|
||||||
fail "neither curl nor wget is available"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_release_tag() {
|
resolve_release_tag() {
|
||||||
if [[ "$VERSION" != latest ]]; then
|
if [[ "$VERSION" != "latest" ]]; then
|
||||||
echo "$VERSION"
|
echo "$VERSION"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local api_json
|
local api_json
|
||||||
api_json="$(mktemp)"
|
api_json="$(mktemp)"
|
||||||
if ! download_json "https://api.github.com/repos/${REPO}/releases/latest" "$api_json"; then
|
if ! download_file "https://api.github.com/repos/${REPO}/releases/latest" "$api_json"; then
|
||||||
rm -f "$api_json"
|
rm -f "$api_json"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
TAG="$(awk -F '"tag_name": "' 'index($0, "\"tag_name\"") { split($2, a, "\""); print a[1] }' "$api_json" | head -n 1)"
|
|
||||||
|
local tag
|
||||||
|
if has jq; then
|
||||||
|
tag="$(jq -r '.tag_name // empty' "$api_json")"
|
||||||
|
else
|
||||||
|
tag="$(awk '/"tag_name":/ { gsub(/[",]/, "", $3); print $3; exit }' "$api_json")"
|
||||||
|
fi
|
||||||
rm -f "$api_json"
|
rm -f "$api_json"
|
||||||
|
|
||||||
if [[ -z "$TAG" ]]; then
|
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||||
fail "could not determine latest tag from GitHub API"
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_assets() {
|
||||||
|
if [[ "$PLATFORM" == "windows"* ]]; then
|
||||||
|
echo "pi-${PLATFORM}.zip"
|
||||||
|
echo "co-mono-${PLATFORM}.zip"
|
||||||
|
else
|
||||||
|
echo "pi-${PLATFORM}.tar.gz"
|
||||||
|
echo "co-mono-${PLATFORM}.tar.gz"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_archive() {
|
||||||
|
local archive="$1"
|
||||||
|
local out_dir="$2"
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
if [[ "$archive" == *.zip ]]; then
|
||||||
|
if ! has unzip; then
|
||||||
|
fail "unzip required for zip archive"
|
||||||
|
fi
|
||||||
|
unzip -q "$archive" -d "$out_dir"
|
||||||
|
else
|
||||||
|
tar -xzf "$archive" -C "$out_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_packages() {
|
||||||
|
local -a packages=()
|
||||||
|
if [[ "$USE_DEFAULT_PACKAGES" == "1" ]]; then
|
||||||
|
packages=("${DEFAULT_PACKAGES[@]}")
|
||||||
|
fi
|
||||||
|
if [[ "${#EXTRA_PACKAGES[@]}" -gt 0 ]]; then
|
||||||
|
packages+=("${EXTRA_PACKAGES[@]}")
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${packages[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_launcher() {
|
||||||
|
local output="$1"
|
||||||
|
local runtime_dir="$2"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$output")"
|
||||||
|
cat > "$output" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export CO_MONO_AGENT_DIR="${AGENT_DIR}"
|
||||||
|
export PI_CODING_AGENT_DIR="${AGENT_DIR}"
|
||||||
|
|
||||||
|
exec "${runtime_dir}" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_agent_settings() {
|
||||||
|
mkdir -p "$AGENT_DIR"
|
||||||
|
local settings_file="$AGENT_DIR/settings.json"
|
||||||
|
if [[ -f "$settings_file" ]]; then
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$TAG"
|
local -a packages
|
||||||
|
readarray -t packages < <(collect_packages)
|
||||||
|
if [[ "${#packages[@]}" -eq 0 ]]; then
|
||||||
|
cat > "$settings_file" <<'EOF'
|
||||||
|
{
|
||||||
|
"packages": []
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "{"
|
||||||
|
echo ' "packages": ['
|
||||||
|
} > "$settings_file"
|
||||||
|
local idx=0
|
||||||
|
local total="${#packages[@]}"
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
local suffix=""
|
||||||
|
if [[ "$idx" -lt $((total - 1)) ]]; then
|
||||||
|
suffix=","
|
||||||
|
fi
|
||||||
|
printf ' "%s"%s\n' "$package" "$suffix" >> "$settings_file"
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
{
|
||||||
|
echo " ]"
|
||||||
|
echo "}"
|
||||||
|
} >> "$settings_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_packages() {
|
||||||
|
if [[ "$RUN_INSTALL_PACKAGES" != "1" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! has npm; then
|
||||||
|
log "npm not found. Skipping package installation."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r package; do
|
||||||
|
[[ -z "$package" ]] && continue
|
||||||
|
if "$BIN_DIR/co-mono" install "$package" >/dev/null 2>&1; then
|
||||||
|
log "Installed package: $package"
|
||||||
|
else
|
||||||
|
log "Could not install ${package} now. It will install on first run when available."
|
||||||
|
fi
|
||||||
|
done < <(collect_packages)
|
||||||
|
}
|
||||||
|
|
||||||
|
write_service_file() {
|
||||||
|
if ! has systemctl; then
|
||||||
|
log "systemctl unavailable; skipping service setup."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$HOME/.config/systemd/user"
|
||||||
|
local service_path="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
|
||||||
|
cat > "$service_path" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=co-mono background agent
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment=CO_MONO_AGENT_DIR=${AGENT_DIR}
|
||||||
|
Environment=PI_CODING_AGENT_DIR=${AGENT_DIR}
|
||||||
|
ExecStart=${BIN_DIR}/co-mono daemon
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
log "service file: $service_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_daemon_service() {
|
||||||
|
if ! has systemctl; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now "${SERVICE_NAME}.service"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_next_steps() {
|
||||||
|
echo
|
||||||
|
log "Installed to: $INSTALL_DIR"
|
||||||
|
log "Launcher: $BIN_DIR/co-mono"
|
||||||
|
echo
|
||||||
|
echo "Run in terminal:"
|
||||||
|
echo " co-mono"
|
||||||
|
echo
|
||||||
|
echo "Run always-on:"
|
||||||
|
echo " co-mono daemon"
|
||||||
|
echo
|
||||||
|
if [[ "$SETUP_DAEMON" == "1" ]] && [[ "$SKIP_SERVICE" == "0" ]]; then
|
||||||
|
echo "Service:"
|
||||||
|
echo " systemctl --user status ${SERVICE_NAME}"
|
||||||
|
echo " systemctl --user restart ${SERVICE_NAME}"
|
||||||
|
echo
|
||||||
|
echo "Service logs:"
|
||||||
|
echo " journalctl --user -u ${SERVICE_NAME} -f"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap_from_source() {
|
bootstrap_from_source() {
|
||||||
local source_dir="$INSTALL_DIR/source"
|
if ! has git; then
|
||||||
local ref="$1"
|
|
||||||
|
|
||||||
if ! command -v node >/dev/null 2>&1; then
|
|
||||||
fail "Node.js is required for source fallback. Install nodejs first."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1; then
|
|
||||||
fail "npm is required for source fallback. Install npm first."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v git >/dev/null 2>&1; then
|
|
||||||
fail "git is required for source fallback."
|
fail "git is required for source fallback."
|
||||||
fi
|
fi
|
||||||
|
if ! has node; then
|
||||||
|
fail "node is required for source fallback."
|
||||||
|
fi
|
||||||
|
if ! has npm; then
|
||||||
|
fail "npm is required for source fallback."
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
local source_dir="$INSTALL_DIR/source"
|
||||||
if [[ -d "$source_dir" && "${SKIP_REINSTALL}" != "1" ]]; then
|
local ref="${1:-main}"
|
||||||
|
|
||||||
|
if [[ -d "$source_dir" && "$SKIP_REINSTALL" != "1" ]]; then
|
||||||
rm -rf "$source_dir"
|
rm -rf "$source_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$source_dir" ]]; then
|
if [[ ! -d "$source_dir" ]]; then
|
||||||
log "Cloning ${REPO} (${ref})"
|
log "Cloning ${REPO}@${ref}"
|
||||||
git clone --depth 1 --branch "$ref" "https://github.com/${REPO}.git" "$source_dir"
|
git clone --depth 1 --branch "$ref" "https://github.com/${REPO}.git" "$source_dir"
|
||||||
else
|
|
||||||
log "Updating existing source checkout at $source_dir"
|
|
||||||
git -C "$source_dir" fetch --depth 1 origin "$ref"
|
|
||||||
git -C "$source_dir" checkout "$ref"
|
|
||||||
git -C "$source_dir" pull --ff-only origin "$ref"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Running local install for source checkout"
|
log "Running source install"
|
||||||
(
|
(
|
||||||
cd "$source_dir"
|
cd "$source_dir"
|
||||||
CO_MONO_AGENT_DIR="$AGENT_DIR" \
|
CO_MONO_AGENT_DIR="$AGENT_DIR" \
|
||||||
|
|
@ -155,209 +425,113 @@ bootstrap_from_source() {
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ ! -x "$source_dir/co-mono" ]]; then
|
if [[ ! -x "$source_dir/co-mono" ]]; then
|
||||||
fail "co-mono source launcher was not created in fallback checkout."
|
fail "co-mono executable not found in source checkout."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
write_source_launcher "$source_dir"
|
write_launcher "$BIN_DIR/co-mono" "$source_dir/co-mono"
|
||||||
export CO_MONO_BIN_PATH="$source_dir/co-mono"
|
ensure_agent_settings
|
||||||
maybe_install_packages
|
install_packages
|
||||||
print_next_steps_source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write_source_launcher() {
|
install_from_release() {
|
||||||
local source_dir="$1"
|
local tag="$1"
|
||||||
mkdir -p "$BIN_DIR"
|
|
||||||
local launcher="$BIN_DIR/co-mono"
|
|
||||||
cat > "$launcher" <<EOF
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$source_dir"
|
|
||||||
AGENT_DIR="$AGENT_DIR"
|
|
||||||
|
|
||||||
export CO_MONO_AGENT_DIR="${AGENT_DIR}"
|
|
||||||
export PI_CODING_AGENT_DIR="${AGENT_DIR}"
|
|
||||||
|
|
||||||
cd "\$ROOT_DIR"
|
|
||||||
exec "\$ROOT_DIR/co-mono" "\$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$launcher"
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_asset() {
|
|
||||||
if [[ "$PLATFORM" == windows-* ]]; then
|
|
||||||
echo "pi-${PLATFORM}.zip"
|
|
||||||
else
|
|
||||||
echo "pi-${PLATFORM}.tar.gz"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_archive() {
|
|
||||||
local archive="$1"
|
|
||||||
local out_dir="$2"
|
|
||||||
|
|
||||||
mkdir -p "$out_dir"
|
|
||||||
if [[ "$archive" == *.zip ]]; then
|
|
||||||
if ! command -v unzip >/dev/null 2>&1; then
|
|
||||||
fail "unzip not found for windows archive"
|
|
||||||
fi
|
|
||||||
unzip -q "$archive" -d "$out_dir"
|
|
||||||
else
|
|
||||||
tar -xzf "$archive" -C "$out_dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_agent_settings() {
|
|
||||||
mkdir -p "$AGENT_DIR"
|
|
||||||
|
|
||||||
local settings_file="$AGENT_DIR/settings.json"
|
|
||||||
if [[ -f "$settings_file" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$settings_file" <<'EOF'
|
|
||||||
{
|
|
||||||
"packages": [
|
|
||||||
"npm:@e9n/pi-channels",
|
|
||||||
"npm:pi-memory-md",
|
|
||||||
"npm:pi-teams"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_install_packages() {
|
|
||||||
if [[ "$RUN_INSTALL_PACKAGES" == "0" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1; then
|
|
||||||
log "npm not found. Skipping package installation (settings.json was still written)."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
for pkg in "${DEFAULT_PACKAGES[@]}"; do
|
|
||||||
if [[ -n "$CO_MONO_BIN_PATH" ]]; then
|
|
||||||
log "Installing package: $pkg"
|
|
||||||
if ! PI_CODING_AGENT_DIR="$AGENT_DIR" CO_MONO_AGENT_DIR="$AGENT_DIR" "$CO_MONO_BIN_PATH" install "$pkg" >/dev/null 2>&1; then
|
|
||||||
log "Could not install $pkg now. It will be installed on first run if network/API access is available."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
write_launcher() {
|
|
||||||
mkdir -p "$BIN_DIR"
|
|
||||||
local launcher="$BIN_DIR/co-mono"
|
|
||||||
cat > "$launcher" <<EOF
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$INSTALL_DIR"
|
|
||||||
AGENT_DIR="$AGENT_DIR"
|
|
||||||
|
|
||||||
export CO_MONO_AGENT_DIR="${AGENT_DIR}"
|
|
||||||
export PI_CODING_AGENT_DIR="${AGENT_DIR}"
|
|
||||||
|
|
||||||
exec "\$ROOT_DIR/pi" "\$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$launcher"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_next_steps() {
|
|
||||||
echo
|
|
||||||
log "Installed co-mono to: $INSTALL_DIR"
|
|
||||||
log "Launcher created: $BIN_DIR/co-mono"
|
|
||||||
echo
|
|
||||||
echo "Add to PATH if needed:"
|
|
||||||
echo " export PATH=\"$BIN_DIR:$PATH\""
|
|
||||||
echo
|
|
||||||
echo "Run:"
|
|
||||||
echo " co-mono"
|
|
||||||
echo
|
|
||||||
echo "You can override settings directory with PI_CODING_AGENT_DIR/CO_MONO_AGENT_DIR."
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
print_next_steps_source() {
|
|
||||||
echo
|
|
||||||
log "Installed co-mono from source checkout: $INSTALL_DIR/source"
|
|
||||||
log "Launcher created: $BIN_DIR/co-mono"
|
|
||||||
echo
|
|
||||||
echo "Add to PATH if needed:"
|
|
||||||
echo " export PATH=\"$BIN_DIR:$PATH\""
|
|
||||||
echo
|
|
||||||
echo "Run:"
|
|
||||||
echo " co-mono"
|
|
||||||
echo
|
|
||||||
echo "Source fallback mode uses node/npm and local scripts."
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
detect_platform
|
detect_platform
|
||||||
if ! TAG="$(resolve_release_tag)"; then
|
local workdir
|
||||||
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
|
||||||
log "No release found in GitHub. Falling back to source install."
|
|
||||||
bootstrap_from_source "main"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fail "could not determine latest release tag from GitHub API. Set CO_MONO_FALLBACK_TO_SOURCE=1 to fallback to source."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$TAG" ]]; then
|
|
||||||
fail "resolved release tag was empty."
|
|
||||||
fi
|
|
||||||
|
|
||||||
local asset
|
local asset
|
||||||
local url
|
local url
|
||||||
local archive
|
local archive
|
||||||
local workdir
|
local downloaded=0
|
||||||
|
|
||||||
asset="$(platform_asset)"
|
|
||||||
url="https://github.com/${REPO}/releases/download/${TAG}/${asset}"
|
|
||||||
|
|
||||||
workdir="$(mktemp -d)"
|
workdir="$(mktemp -d)"
|
||||||
trap 'rm -rf "$workdir"' EXIT
|
|
||||||
|
|
||||||
archive="$workdir/$asset"
|
archive="$workdir/$asset"
|
||||||
log "Downloading ${REPO} ${TAG} (${PLATFORM})"
|
while IFS= read -r asset; do
|
||||||
if ! download_json "$url" "$archive"; then
|
url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
|
||||||
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
archive="$workdir/$asset"
|
||||||
log "Release asset not available for ${TAG} (${PLATFORM}). Falling back to source."
|
log "Trying asset: ${asset}"
|
||||||
bootstrap_from_source "${TAG}"
|
if download_file "$url" "$archive"; then
|
||||||
return
|
downloaded=1
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
fail "release asset not found: $url"
|
done < <(platform_assets)
|
||||||
|
|
||||||
|
if [[ "$downloaded" == "0" ]]; then
|
||||||
|
rm -rf "$workdir"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Extracting archive"
|
log "Extracting archive"
|
||||||
extract_archive "$archive" "$workdir"
|
extract_archive "$archive" "$workdir"
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
local release_dir
|
||||||
|
local install_binary
|
||||||
if [[ -d "$workdir/pi" ]]; then
|
if [[ -d "$workdir/pi" ]]; then
|
||||||
rm -rf "$INSTALL_DIR"/*
|
release_dir="$workdir/pi"
|
||||||
cp -R "$workdir/pi/." "$INSTALL_DIR/"
|
elif [[ -d "$workdir/co-mono" ]]; then
|
||||||
|
release_dir="$workdir/co-mono"
|
||||||
|
elif [[ -f "$workdir/pi" ]]; then
|
||||||
|
release_dir="$workdir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${release_dir:-}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
rm -rf "$INSTALL_DIR"/*
|
||||||
|
cp -R "$release_dir/." "$INSTALL_DIR/"
|
||||||
|
|
||||||
|
if [[ -x "$INSTALL_DIR/pi" ]]; then
|
||||||
|
install_binary="$INSTALL_DIR/pi"
|
||||||
|
elif [[ -x "$INSTALL_DIR/co-mono" ]]; then
|
||||||
|
install_binary="$INSTALL_DIR/co-mono"
|
||||||
else
|
else
|
||||||
fail "release asset did not contain expected pi directory"
|
return 1
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -x "$INSTALL_DIR/pi" ]]; then
|
|
||||||
fail "co-mono binary not found at $INSTALL_DIR/pi"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Runtime launcher with fixed agent dir env.
|
||||||
|
write_launcher "$INSTALL_DIR/co-mono" "$install_binary"
|
||||||
|
write_launcher "$BIN_DIR/co-mono" "$INSTALL_DIR/co-mono"
|
||||||
ensure_agent_settings
|
ensure_agent_settings
|
||||||
write_launcher
|
install_packages
|
||||||
|
rm -rf "$workdir"
|
||||||
|
}
|
||||||
|
|
||||||
export CO_MONO_BIN_PATH="$INSTALL_DIR/pi"
|
main() {
|
||||||
maybe_install_packages
|
local tag
|
||||||
|
if ! tag="$(resolve_release_tag)"; then
|
||||||
if [[ "$INSTALL_RUNTIME_DAEMON" == "1" ]]; then
|
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
||||||
log "Runtime-daemon helper is not bundled in this binary distribution."
|
log "Could not resolve release tag. Falling back to source."
|
||||||
log "Use a process supervisor or an external runtime wrapper to keep services up."
|
bootstrap_from_source "main"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fail "could not resolve latest release tag from GitHub API"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_next_steps
|
if [[ -n "$tag" ]]; then
|
||||||
|
if ! install_from_release "$tag"; then
|
||||||
|
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
||||||
|
log "Release install failed. Falling back to source."
|
||||||
|
if [[ "$VERSION" == "latest" ]]; then
|
||||||
|
bootstrap_from_source "main"
|
||||||
|
else
|
||||||
|
bootstrap_from_source "$VERSION"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fail "release asset unavailable: ${tag}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "release tag empty."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main
|
main
|
||||||
|
print_next_steps
|
||||||
|
|
||||||
|
if [[ "$SETUP_DAEMON" == "1" && "$SKIP_SERVICE" == "0" ]]; then
|
||||||
|
if write_service_file; then
|
||||||
|
if [[ "$START_DAEMON" == "1" ]]; then
|
||||||
|
start_daemon_service
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue