Merge pull request #246 from suraj-markup/feat/issue-245
fix(setup): improve setup.sh prereq validation and add start/spawn pre-flight checks
This commit is contained in:
commit
01a6c56e1b
|
|
@ -304,3 +304,121 @@ describe("spawn command", () => {
|
|||
).rejects.toThrow("process.exit(1)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawn pre-flight checks", () => {
|
||||
it("fails with clear error when tmux is not installed (default runtime)", async () => {
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["node", "test", "spawn", "my-app"]),
|
||||
).rejects.toThrow("process.exit(1)");
|
||||
|
||||
const errors = vi.mocked(console.error).mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(errors).toContain("tmux");
|
||||
// Should not attempt to spawn
|
||||
expect(mockSessionManager.spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips tmux check when runtime is not tmux", async () => {
|
||||
const fakeSession: Session = {
|
||||
id: "app-1",
|
||||
projectId: "my-app",
|
||||
status: "spawning",
|
||||
activity: null,
|
||||
branch: null,
|
||||
issueId: null,
|
||||
pr: null,
|
||||
workspacePath: "/tmp/wt",
|
||||
runtimeHandle: { id: "proc-1", runtimeName: "process", data: {} },
|
||||
agentInfo: null,
|
||||
createdAt: new Date(),
|
||||
lastActivityAt: new Date(),
|
||||
metadata: {},
|
||||
};
|
||||
mockSessionManager.spawn.mockResolvedValue(fakeSession);
|
||||
|
||||
// Set runtime to "process"
|
||||
(mockConfigRef.current as Record<string, unknown>).defaults = {
|
||||
runtime: "process",
|
||||
agent: "claude-code",
|
||||
workspace: "worktree",
|
||||
notifiers: ["desktop"],
|
||||
};
|
||||
|
||||
// exec would fail for tmux but should never be called
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
|
||||
await program.parseAsync(["node", "test", "spawn", "my-app"]);
|
||||
|
||||
expect(mockSessionManager.spawn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("checks gh auth when tracker is github", async () => {
|
||||
const projects = (mockConfigRef.current as Record<string, unknown>).projects as Record<string, Record<string, unknown>>;
|
||||
projects["my-app"].tracker = { plugin: "github" };
|
||||
|
||||
// tmux check passes, gh --version passes, gh auth status fails
|
||||
mockExec
|
||||
.mockResolvedValueOnce({ stdout: "tmux 3.3a", stderr: "" }) // tmux -V
|
||||
.mockResolvedValueOnce({ stdout: "gh version 2.40", stderr: "" }) // gh --version
|
||||
.mockRejectedValueOnce(new Error("not logged in")); // gh auth status
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["node", "test", "spawn", "my-app"]),
|
||||
).rejects.toThrow("process.exit(1)");
|
||||
|
||||
const errors = vi.mocked(console.error).mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(errors).toContain("not authenticated");
|
||||
expect(mockSessionManager.spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips gh auth check when tracker is not github", async () => {
|
||||
const fakeSession: Session = {
|
||||
id: "app-1",
|
||||
projectId: "my-app",
|
||||
status: "spawning",
|
||||
activity: null,
|
||||
branch: null,
|
||||
issueId: null,
|
||||
pr: null,
|
||||
workspacePath: "/tmp/wt",
|
||||
runtimeHandle: { id: "hash-1", runtimeName: "tmux", data: {} },
|
||||
agentInfo: null,
|
||||
createdAt: new Date(),
|
||||
lastActivityAt: new Date(),
|
||||
metadata: {},
|
||||
};
|
||||
mockSessionManager.spawn.mockResolvedValue(fakeSession);
|
||||
|
||||
const projects = (mockConfigRef.current as Record<string, unknown>).projects as Record<string, Record<string, unknown>>;
|
||||
projects["my-app"].tracker = { plugin: "linear" };
|
||||
|
||||
// tmux check passes — gh should never be called
|
||||
mockExec.mockResolvedValue({ stdout: "tmux 3.3a", stderr: "" });
|
||||
|
||||
await program.parseAsync(["node", "test", "spawn", "my-app"]);
|
||||
|
||||
// Should only call tmux -V, not gh
|
||||
expect(mockExec).toHaveBeenCalledWith("tmux", ["-V"]);
|
||||
expect(mockExec).not.toHaveBeenCalledWith("gh", expect.anything());
|
||||
expect(mockSessionManager.spawn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("distinguishes gh not installed from gh not authenticated", async () => {
|
||||
const projects = (mockConfigRef.current as Record<string, unknown>).projects as Record<string, Record<string, unknown>>;
|
||||
projects["my-app"].tracker = { plugin: "github" };
|
||||
|
||||
// tmux passes, gh --version fails (not installed)
|
||||
mockExec
|
||||
.mockResolvedValueOnce({ stdout: "tmux 3.3a", stderr: "" }) // tmux -V
|
||||
.mockRejectedValueOnce(new Error("ENOENT")); // gh --version fails
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["node", "test", "spawn", "my-app"]),
|
||||
).rejects.toThrow("process.exit(1)");
|
||||
|
||||
const errors = vi.mocked(console.error).mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(errors).toContain("not installed");
|
||||
expect(errors).not.toContain("not authenticated");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const { mockExec, mockIsPortAvailable, mockExistsSync } = vi.hoisted(() => ({
|
||||
mockExec: vi.fn(),
|
||||
mockIsPortAvailable: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
exec: mockExec,
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/web-dir.js", () => ({
|
||||
isPortAvailable: mockIsPortAvailable,
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: mockExistsSync,
|
||||
}));
|
||||
|
||||
import { preflight } from "../../src/lib/preflight.js";
|
||||
|
||||
beforeEach(() => {
|
||||
mockExec.mockReset();
|
||||
mockIsPortAvailable.mockReset();
|
||||
mockExistsSync.mockReset();
|
||||
});
|
||||
|
||||
describe("preflight.checkPort", () => {
|
||||
it("passes when port is free", async () => {
|
||||
mockIsPortAvailable.mockResolvedValue(true);
|
||||
await expect(preflight.checkPort(3000)).resolves.toBeUndefined();
|
||||
expect(mockIsPortAvailable).toHaveBeenCalledWith(3000);
|
||||
});
|
||||
|
||||
it("throws when port is in use", async () => {
|
||||
mockIsPortAvailable.mockResolvedValue(false);
|
||||
await expect(preflight.checkPort(3000)).rejects.toThrow(
|
||||
"Port 3000 is already in use",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes port number in error message", async () => {
|
||||
mockIsPortAvailable.mockResolvedValue(false);
|
||||
await expect(preflight.checkPort(8080)).rejects.toThrow("Port 8080");
|
||||
});
|
||||
});
|
||||
|
||||
describe("preflight.checkBuilt", () => {
|
||||
it("passes when node_modules and core dist exist", async () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
await expect(preflight.checkBuilt("/web")).resolves.toBeUndefined();
|
||||
expect(mockExistsSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws 'pnpm install' when node_modules is missing", async () => {
|
||||
// First call checks node_modules/@composio/ao-core — missing
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
await expect(preflight.checkBuilt("/web")).rejects.toThrow(
|
||||
"pnpm install",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws 'pnpm build' when node_modules exists but dist is missing", async () => {
|
||||
// First call: node_modules/@composio/ao-core exists
|
||||
// Second call: dist/index.js does not exist
|
||||
mockExistsSync
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false);
|
||||
await expect(preflight.checkBuilt("/web")).rejects.toThrow(
|
||||
"Packages not built. Run: pnpm build",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("preflight.checkTmux", () => {
|
||||
it("passes when tmux is installed", async () => {
|
||||
mockExec.mockResolvedValue({ stdout: "tmux 3.3a", stderr: "" });
|
||||
await expect(preflight.checkTmux()).resolves.toBeUndefined();
|
||||
expect(mockExec).toHaveBeenCalledWith("tmux", ["-V"]);
|
||||
});
|
||||
|
||||
it("throws when tmux is not installed", async () => {
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
await expect(preflight.checkTmux()).rejects.toThrow(
|
||||
"tmux is not installed",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes install instruction in error", async () => {
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
await expect(preflight.checkTmux()).rejects.toThrow("brew install tmux");
|
||||
});
|
||||
});
|
||||
|
||||
describe("preflight.checkGhAuth", () => {
|
||||
it("passes when gh is installed and authenticated", async () => {
|
||||
mockExec.mockResolvedValue({ stdout: "ok", stderr: "" });
|
||||
await expect(preflight.checkGhAuth()).resolves.toBeUndefined();
|
||||
expect(mockExec).toHaveBeenCalledWith("gh", ["--version"]);
|
||||
expect(mockExec).toHaveBeenCalledWith("gh", ["auth", "status"]);
|
||||
});
|
||||
|
||||
it("throws 'not installed' when gh is missing (ENOENT)", async () => {
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
await expect(preflight.checkGhAuth()).rejects.toThrow(
|
||||
"GitHub CLI (gh) is not installed",
|
||||
);
|
||||
// Should only call --version, not auth status
|
||||
expect(mockExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockExec).toHaveBeenCalledWith("gh", ["--version"]);
|
||||
});
|
||||
|
||||
it("throws 'not authenticated' when gh exists but auth fails", async () => {
|
||||
mockExec
|
||||
.mockResolvedValueOnce({ stdout: "gh version 2.40", stderr: "" }) // --version succeeds
|
||||
.mockRejectedValueOnce(new Error("not logged in")); // auth status fails
|
||||
await expect(preflight.checkGhAuth()).rejects.toThrow(
|
||||
"GitHub CLI is not authenticated",
|
||||
);
|
||||
expect(mockExec).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("includes correct fix instructions for each failure", async () => {
|
||||
// Not installed → install link
|
||||
mockExec.mockRejectedValue(new Error("ENOENT"));
|
||||
await expect(preflight.checkGhAuth()).rejects.toThrow(
|
||||
"https://cli.github.com/",
|
||||
);
|
||||
|
||||
mockExec.mockReset();
|
||||
|
||||
// Not authenticated → auth login
|
||||
mockExec
|
||||
.mockResolvedValueOnce({ stdout: "gh version 2.40", stderr: "" })
|
||||
.mockRejectedValueOnce(new Error("not logged in"));
|
||||
await expect(preflight.checkGhAuth()).rejects.toThrow("gh auth login");
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,23 @@ import { loadConfig, type OrchestratorConfig } from "@composio/ao-core";
|
|||
import { exec } from "../lib/shell.js";
|
||||
import { banner } from "../lib/format.js";
|
||||
import { getSessionManager } from "../lib/create-session-manager.js";
|
||||
import { preflight } from "../lib/preflight.js";
|
||||
|
||||
/**
|
||||
* Run pre-flight checks for a project once, before any sessions are spawned.
|
||||
* Validates runtime and tracker prerequisites so failures surface immediately
|
||||
* rather than repeating per-session in a batch.
|
||||
*/
|
||||
async function runSpawnPreflight(config: OrchestratorConfig, projectId: string): Promise<void> {
|
||||
const project = config.projects[projectId];
|
||||
const runtime = project?.runtime ?? config.defaults.runtime;
|
||||
if (runtime === "tmux") {
|
||||
await preflight.checkTmux();
|
||||
}
|
||||
if (project?.tracker?.plugin === "github") {
|
||||
await preflight.checkGhAuth();
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnSession(
|
||||
config: OrchestratorConfig,
|
||||
|
|
@ -73,9 +90,10 @@ export function registerSpawn(program: Command): void {
|
|||
}
|
||||
|
||||
try {
|
||||
await runSpawnPreflight(config, projectId);
|
||||
await spawnSession(config, projectId, issueId, opts.open, opts.agent);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ ${err}`));
|
||||
console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
|
@ -105,6 +123,14 @@ export function registerBatchSpawn(program: Command): void {
|
|||
console.log(` Issues: ${issues.join(", ")}`);
|
||||
console.log();
|
||||
|
||||
// Pre-flight once before the loop so a missing prerequisite fails fast
|
||||
try {
|
||||
await runSpawnPreflight(config, projectId);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sm = await getSessionManager(config);
|
||||
const created: Array<{ session: string; issue: string }> = [];
|
||||
const skipped: Array<{ issue: string; existing: string }> = [];
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { exec } from "../lib/shell.js";
|
|||
import { getSessionManager } from "../lib/create-session-manager.js";
|
||||
import { findWebDir, buildDashboardEnv } from "../lib/web-dir.js";
|
||||
import { cleanNextCache } from "../lib/dashboard-rebuild.js";
|
||||
import { preflight } from "../lib/preflight.js";
|
||||
|
||||
/**
|
||||
* Resolve project from config.
|
||||
|
|
@ -146,10 +147,13 @@ export function registerStart(program: Command): void {
|
|||
let exists = false; // Track whether orchestrator session already exists
|
||||
|
||||
if (opts?.dashboard !== false) {
|
||||
// Pre-flight: only check port/build when actually starting the dashboard
|
||||
await preflight.checkPort(port);
|
||||
const webDir = findWebDir();
|
||||
if (!existsSync(resolve(webDir, "package.json"))) {
|
||||
throw new Error("Could not find @composio/ao-web package. Run: pnpm install");
|
||||
}
|
||||
await preflight.checkBuilt(webDir);
|
||||
|
||||
if (opts?.rebuild) {
|
||||
await cleanNextCache(webDir);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Pre-flight checks for `ao start` and `ao spawn`.
|
||||
*
|
||||
* Validates runtime prerequisites before entering the main command flow,
|
||||
* giving clear errors instead of cryptic failures.
|
||||
*
|
||||
* All checks throw on failure so callers can catch and handle uniformly.
|
||||
*/
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { isPortAvailable } from "./web-dir.js";
|
||||
import { exec } from "./shell.js";
|
||||
|
||||
/**
|
||||
* Check that the dashboard port is free.
|
||||
* Throws if the port is already in use.
|
||||
*/
|
||||
async function checkPort(port: number): Promise<void> {
|
||||
const free = await isPortAvailable(port);
|
||||
if (!free) {
|
||||
throw new Error(
|
||||
`Port ${port} is already in use. Free it or change 'port' in agent-orchestrator.yaml.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that workspace packages have been compiled (TypeScript → JavaScript).
|
||||
* Verifies @composio/ao-core dist output exists from the web package's
|
||||
* node_modules, since a missing dist/ causes module resolution errors when
|
||||
* starting the dashboard. Works with both `next dev` and `next build`.
|
||||
*/
|
||||
async function checkBuilt(webDir: string): Promise<void> {
|
||||
const nodeModules = resolve(webDir, "node_modules", "@composio", "ao-core");
|
||||
if (!existsSync(nodeModules)) {
|
||||
throw new Error("Dependencies not installed. Run: pnpm install && pnpm build");
|
||||
}
|
||||
const coreEntry = resolve(nodeModules, "dist", "index.js");
|
||||
if (!existsSync(coreEntry)) {
|
||||
throw new Error("Packages not built. Run: pnpm build");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that tmux is installed (required for the default runtime).
|
||||
* Throws if not installed.
|
||||
*/
|
||||
async function checkTmux(): Promise<void> {
|
||||
try {
|
||||
await exec("tmux", ["-V"]);
|
||||
} catch {
|
||||
throw new Error("tmux is not installed. Install it: brew install tmux");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the GitHub CLI is installed and authenticated.
|
||||
* Distinguishes between "not installed" and "not authenticated"
|
||||
* so the user gets the right troubleshooting guidance.
|
||||
*/
|
||||
async function checkGhAuth(): Promise<void> {
|
||||
try {
|
||||
await exec("gh", ["--version"]);
|
||||
} catch {
|
||||
throw new Error("GitHub CLI (gh) is not installed. Install it: https://cli.github.com/");
|
||||
}
|
||||
|
||||
try {
|
||||
await exec("gh", ["auth", "status"]);
|
||||
} catch {
|
||||
throw new Error("GitHub CLI is not authenticated. Run: gh auth login");
|
||||
}
|
||||
}
|
||||
|
||||
export const preflight = {
|
||||
checkPort,
|
||||
checkBuilt,
|
||||
checkTmux,
|
||||
checkGhAuth,
|
||||
};
|
||||
159
scripts/setup.sh
159
scripts/setup.sh
|
|
@ -1,38 +1,167 @@
|
|||
#!/bin/bash
|
||||
# Agent Orchestrator setup script
|
||||
# Installs dependencies, builds packages, and links the CLI globally
|
||||
# Validates prerequisites, installs dependencies, builds packages, and links the CLI globally
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "🤖 Agent Orchestrator Setup"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
echo "Agent Orchestrator Setup"
|
||||
echo ""
|
||||
|
||||
# Check for pnpm
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
echo "❌ pnpm not found. Installing pnpm..."
|
||||
npm install -g pnpm
|
||||
# ─── Hard requirements (exit 1 if missing) ────────────────────────────────────
|
||||
|
||||
# Node.js >= 20
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "ERROR: Node.js is not installed."
|
||||
echo " Install Node.js 20+: https://nodejs.org/en/download"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Installing dependencies..."
|
||||
NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
|
||||
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||
echo "ERROR: Node.js $NODE_MAJOR.x detected, but 20+ is required."
|
||||
echo " Install Node.js 20+: https://nodejs.org/en/download"
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] Node.js $(node --version)"
|
||||
|
||||
# git >= 2.25 (required for worktree support)
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "ERROR: git is not installed."
|
||||
echo " Install git 2.25+: https://git-scm.com/downloads"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GIT_VERSION=$(git --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
|
||||
GIT_MAJOR=$(echo "$GIT_VERSION" | cut -d. -f1)
|
||||
GIT_MINOR=$(echo "$GIT_VERSION" | cut -d. -f2)
|
||||
if [ "$GIT_MAJOR" -lt 2 ] || { [ "$GIT_MAJOR" -eq 2 ] && [ "$GIT_MINOR" -lt 25 ]; }; then
|
||||
echo "ERROR: git $GIT_VERSION detected, but 2.25+ is required (worktree support)."
|
||||
echo " Upgrade git: https://git-scm.com/downloads"
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] git $GIT_VERSION"
|
||||
|
||||
# ─── Soft requirements (warn + offer interactive fix) ─────────────────────────
|
||||
|
||||
# Detect interactive terminal for optional prompts (skip in CI/Docker)
|
||||
INTERACTIVE=false
|
||||
if [ -t 0 ]; then
|
||||
INTERACTIVE=true
|
||||
fi
|
||||
|
||||
# tmux
|
||||
if ! command -v tmux &> /dev/null; then
|
||||
echo ""
|
||||
echo "WARNING: tmux is not installed (default runtime requires it)."
|
||||
if [ "$INTERACTIVE" = true ] && command -v brew &> /dev/null; then
|
||||
read -r -p " Install tmux via Homebrew? [Y/n] " response
|
||||
response=${response:-Y}
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
brew install tmux
|
||||
echo "[ok] tmux installed"
|
||||
else
|
||||
echo " Skipping. Install later: brew install tmux"
|
||||
fi
|
||||
else
|
||||
echo " Install tmux: https://github.com/tmux/tmux/wiki/Installing"
|
||||
fi
|
||||
else
|
||||
echo "[ok] tmux $(tmux -V | grep -oE '[0-9]+\.[0-9a-z]+')"
|
||||
fi
|
||||
|
||||
# gh CLI authentication
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo ""
|
||||
echo "WARNING: GitHub CLI (gh) is not installed."
|
||||
echo " Install: https://cli.github.com/"
|
||||
else
|
||||
if ! gh auth status &> /dev/null; then
|
||||
echo ""
|
||||
echo "WARNING: GitHub CLI is not authenticated."
|
||||
if [ "$INTERACTIVE" = true ]; then
|
||||
read -r -p " Run 'gh auth login' now? [Y/n] " response
|
||||
response=${response:-Y}
|
||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||
gh auth login
|
||||
else
|
||||
echo " Skipping. Authenticate later: gh auth login"
|
||||
fi
|
||||
else
|
||||
echo " Authenticate later: gh auth login"
|
||||
fi
|
||||
else
|
||||
echo "[ok] gh authenticated"
|
||||
fi
|
||||
fi
|
||||
|
||||
# claude CLI
|
||||
if ! command -v claude &> /dev/null; then
|
||||
echo ""
|
||||
echo "WARNING: Claude CLI is not installed (required for claude-code agent)."
|
||||
echo " Install: npm install -g @anthropic-ai/claude-code"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ─── Install pnpm ────────────────────────────────────────────────────────────
|
||||
|
||||
if command -v pnpm &> /dev/null; then
|
||||
echo "[ok] pnpm $(pnpm --version) (already installed)"
|
||||
else
|
||||
echo "Installing pnpm via corepack..."
|
||||
if corepack enable && corepack prepare --activate 2>/dev/null; then
|
||||
echo "[ok] pnpm $(pnpm --version)"
|
||||
else
|
||||
echo " corepack failed (likely permissions), falling back to npm install..."
|
||||
npm install -g pnpm
|
||||
echo "[ok] pnpm $(pnpm --version)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Install, build, link ────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Installing dependencies..."
|
||||
pnpm install
|
||||
|
||||
echo "🧹 Cleaning stale build artifacts..."
|
||||
echo ""
|
||||
echo "Cleaning stale build artifacts..."
|
||||
rm -rf packages/web/.next
|
||||
|
||||
echo "🔨 Building all packages..."
|
||||
echo ""
|
||||
echo "Building all packages..."
|
||||
pnpm build
|
||||
|
||||
echo "🔗 Linking CLI globally..."
|
||||
echo ""
|
||||
echo "Linking CLI globally..."
|
||||
cd packages/cli
|
||||
npm link
|
||||
cd ../..
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# ─── Verify ao is in PATH ────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "✅ Setup complete! The 'ao' command is now available."
|
||||
if command -v ao &> /dev/null; then
|
||||
echo "[ok] 'ao' command is available in PATH"
|
||||
else
|
||||
NPM_BIN="$(npm bin -g 2>/dev/null || npm config get prefix)/bin"
|
||||
echo "WARNING: 'ao' is not in your PATH."
|
||||
echo " Add this to your shell profile (~/.zshrc or ~/.bashrc):"
|
||||
echo ""
|
||||
echo " export PATH=\"$NPM_BIN:\$PATH\""
|
||||
echo ""
|
||||
echo " Then restart your terminal or run: source ~/.zshrc"
|
||||
fi
|
||||
|
||||
# ─── Done ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Setup complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. cd /path/to/your/project"
|
||||
echo " 1. cd /path/to/your-project"
|
||||
echo " 2. ao init --auto"
|
||||
echo " 3. gh auth login"
|
||||
echo " 4. ao start"
|
||||
echo " 3. ao start"
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue