@vibeshiphq/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # VibeShip CLI
2
+
3
+ Public CLI for initializing the private VibeShip starter and installing VibeShip Pilot for guided setup.
4
+
5
+ ```bash
6
+ npm install -g @vibeshiphq/cli
7
+ vibeship login
8
+ vibeship init my-app
9
+ cd my-app
10
+ vibeship pilot install
11
+ ```
12
+
13
+ The CLI does not embed proprietary setup runbooks. It authenticates the user, checks starter/Pilot entitlement through VibeShip, clones the private starter, and writes local Codex MCP config for Pilot.
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ vibeship login # Authenticate this machine with VibeShip
19
+ vibeship logout # Remove local CLI auth
20
+ vibeship whoami # Show account and entitlement status
21
+ vibeship doctor # Inspect auth, project, and Pilot config
22
+ vibeship init [targetDir] # Clone the private starter into a new app
23
+ vibeship pilot install # Install Pilot MCP config into .codex/config.toml
24
+ vibeship pilot status # Show Pilot subscription and project config status
25
+ ```
26
+
27
+ Configuration is stored at `~/.vibeship/config.json`. The default production API is `https://www.vibeship.today`; override it with `VIBESHIP_API_URL` or `--api-url` when dogfooding against a local internal app.
28
+
29
+ Pilot MCP defaults to `https://pilot.vibeship.today/mcp`; override it with `VIBESHIP_PILOT_MCP_URL` or `--mcp-url`.
30
+
31
+ ## Development
32
+
33
+ ```bash
34
+ pnpm install
35
+ pnpm check
36
+ pnpm dev -- --help
37
+ ```
38
+
39
+ ## Local Dogfooding
40
+
41
+ Run the internal app first:
42
+
43
+ ```bash
44
+ cd ~/projects/vibeship-workspace/internal
45
+ pnpm dev
46
+ ```
47
+
48
+ Then log in from the CLI repo:
49
+
50
+ ```bash
51
+ cd ~/projects/vibeship-workspace/cli
52
+ pnpm dev login
53
+ ```
54
+
55
+ When invoked through the `dev` script, the CLI uses `http://localhost:3000` as the VibeShip API URL. A built CLI uses production defaults.
56
+
57
+ For offline CLI UI work:
58
+
59
+ ```bash
60
+ VIBESHIP_CLI_OFFLINE=1 pnpm dev -- whoami
61
+ ```
62
+
63
+ ## Publishing
64
+
65
+ Before publishing:
66
+
67
+ ```bash
68
+ pnpm check
69
+ npm pack --dry-run
70
+ ```
71
+
72
+ The package publishes only `dist` and this README. `prepack` runs a clean TypeScript build so stale local artifacts are not included.
package/dist/api.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { AuthState } from "./config.js";
2
+ export type EntitlementStatus = {
3
+ email?: string;
4
+ starterAccess: boolean;
5
+ pilotActive: boolean;
6
+ entitlements: string[];
7
+ };
8
+ export declare function fetchEntitlements({ apiUrl, auth, }: {
9
+ apiUrl: string;
10
+ auth: AuthState;
11
+ }): Promise<EntitlementStatus>;
package/dist/api.js ADDED
@@ -0,0 +1,44 @@
1
+ export async function fetchEntitlements({ apiUrl, auth, }) {
2
+ if (process.env.VIBESHIP_CLI_OFFLINE === "1") {
3
+ return {
4
+ email: auth.email,
5
+ starterAccess: true,
6
+ pilotActive: true,
7
+ entitlements: ["license:starter", "pilot:active"],
8
+ };
9
+ }
10
+ const url = new URL("/api/cli/entitlements", apiUrl);
11
+ let response;
12
+ try {
13
+ response = await fetch(url, {
14
+ method: "POST",
15
+ headers: {
16
+ authorization: `Bearer ${auth.token}`,
17
+ "content-type": "application/json",
18
+ },
19
+ body: JSON.stringify({}),
20
+ });
21
+ }
22
+ catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ throw new Error(`Could not reach VibeShip at ${url.origin}: ${message}`);
25
+ }
26
+ if (!response.ok) {
27
+ throw new Error(`Entitlement check failed with HTTP ${response.status}${await responseSuffix(response)}.`);
28
+ }
29
+ return (await response.json());
30
+ }
31
+ async function responseSuffix(response) {
32
+ const text = (await response.text()).trim();
33
+ if (!text) {
34
+ return "";
35
+ }
36
+ try {
37
+ const json = JSON.parse(text);
38
+ const message = json.error ?? json.message;
39
+ return typeof message === "string" ? `: ${message}` : "";
40
+ }
41
+ catch {
42
+ return `: ${text.slice(0, 180)}`;
43
+ }
44
+ }
@@ -0,0 +1,8 @@
1
+ export declare function projectCodexConfigPath(projectDir: string): string;
2
+ export declare function pilotCodexConfig({ mcpUrl, }: {
3
+ mcpUrl: string;
4
+ }): string;
5
+ export declare function installPilotCodexConfig({ projectDir, mcpUrl, }: {
6
+ projectDir: string;
7
+ mcpUrl: string;
8
+ }): string;
package/dist/codex.js ADDED
@@ -0,0 +1,25 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function projectCodexConfigPath(projectDir) {
4
+ return path.join(projectDir, ".codex", "config.toml");
5
+ }
6
+ export function pilotCodexConfig({ mcpUrl, }) {
7
+ return `[mcp_servers.vibeship-pilot]
8
+ url = "${mcpUrl}"
9
+ bearer_token_env_var = "VIBESHIP_PILOT_TOKEN"
10
+ default_tools_approval_mode = "prompt"
11
+ `;
12
+ }
13
+ export function installPilotCodexConfig({ projectDir, mcpUrl, }) {
14
+ const file = projectCodexConfigPath(projectDir);
15
+ fs.mkdirSync(path.dirname(file), { recursive: true });
16
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
17
+ const marker = "[mcp_servers.vibeship-pilot]";
18
+ const next = existing.includes(marker)
19
+ ? existing.replace(/\[mcp_servers\.vibeship-pilot\][\s\S]*?(?=\n\[|$)/, pilotCodexConfig({ mcpUrl }).trimEnd())
20
+ : `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${pilotCodexConfig({
21
+ mcpUrl,
22
+ }).trimEnd()}\n`;
23
+ fs.writeFileSync(file, next);
24
+ return file;
25
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ declare const authStateSchema: z.ZodObject<{
3
+ token: z.ZodString;
4
+ email: z.ZodOptional<z.ZodString>;
5
+ clerkUserId: z.ZodOptional<z.ZodString>;
6
+ expiresAt: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ export type AuthState = z.infer<typeof authStateSchema>;
9
+ export type CliConfig = {
10
+ auth?: AuthState;
11
+ apiUrl?: string;
12
+ pilotMcpUrl?: string;
13
+ };
14
+ export declare function configDir(): string;
15
+ export declare function configPath(): string;
16
+ export declare function readConfig(): CliConfig;
17
+ export declare function writeConfig(config: CliConfig): void;
18
+ export declare function clearAuth(): void;
19
+ export declare function requireAuth(config?: CliConfig): AuthState;
20
+ export declare function defaultApiUrl(): string;
21
+ export declare function defaultPilotMcpUrl(): string;
22
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ const authStateSchema = z.object({
6
+ token: z.string().min(1),
7
+ email: z.string().email().optional(),
8
+ clerkUserId: z.string().optional(),
9
+ expiresAt: z.string().optional(),
10
+ });
11
+ const configSchema = z.object({
12
+ auth: authStateSchema.optional(),
13
+ apiUrl: z.string().url().optional(),
14
+ pilotMcpUrl: z.string().url().optional(),
15
+ });
16
+ export function configDir() {
17
+ return path.join(os.homedir(), ".vibeship");
18
+ }
19
+ export function configPath() {
20
+ return path.join(configDir(), "config.json");
21
+ }
22
+ export function readConfig() {
23
+ const file = configPath();
24
+ if (!fs.existsSync(file)) {
25
+ return {};
26
+ }
27
+ return configSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
28
+ }
29
+ export function writeConfig(config) {
30
+ fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
31
+ fs.writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`, {
32
+ mode: 0o600,
33
+ });
34
+ }
35
+ export function clearAuth() {
36
+ const current = readConfig();
37
+ delete current.auth;
38
+ writeConfig(current);
39
+ }
40
+ export function requireAuth(config = readConfig()) {
41
+ if (!config.auth?.token) {
42
+ throw new Error("Run `vibeship login` before using this command.");
43
+ }
44
+ return config.auth;
45
+ }
46
+ export function defaultApiUrl() {
47
+ if (process.env.VIBESHIP_API_URL) {
48
+ return process.env.VIBESHIP_API_URL;
49
+ }
50
+ if (process.env.npm_lifecycle_event === "dev") {
51
+ return "http://localhost:3000";
52
+ }
53
+ return "https://www.vibeship.today";
54
+ }
55
+ export function defaultPilotMcpUrl() {
56
+ return process.env.VIBESHIP_PILOT_MCP_URL ?? "https://pilot.vibeship.today/mcp";
57
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./program.js";
3
+ import { renderError } from "./ui.js";
4
+ run(process.argv).catch((error) => {
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ renderError("VibeShip CLI failed", message, recoveryActions(message));
7
+ process.exitCode = 1;
8
+ });
9
+ function recoveryActions(message) {
10
+ if (message.includes("Run `vibeship login`")) {
11
+ return ["vibeship login"];
12
+ }
13
+ if (message.includes("starter access")) {
14
+ return [
15
+ "Confirm this account has starter access at https://www.vibeship.today.",
16
+ "Then run vibeship login again.",
17
+ ];
18
+ }
19
+ if (message.includes("Pilot subscription")) {
20
+ return [
21
+ "Confirm this account has an active Pilot subscription at https://www.vibeship.today.",
22
+ "Then run vibeship login again.",
23
+ ];
24
+ }
25
+ if (message.includes("Could not reach VibeShip")) {
26
+ return [
27
+ "Check your network connection.",
28
+ "Use --api-url or VIBESHIP_API_URL if you are targeting a local app.",
29
+ ];
30
+ }
31
+ if (message.includes("HTTP 401") || message.includes("HTTP 403")) {
32
+ return ["vibeship login", "vibeship whoami"];
33
+ }
34
+ if (message.includes("already exists and is not empty")) {
35
+ return ["Choose a new directory: vibeship init my-app"];
36
+ }
37
+ return ["Run with --help to inspect command options."];
38
+ }
@@ -0,0 +1,19 @@
1
+ export type BrowserLoginResult = {
2
+ token: string;
3
+ email?: string;
4
+ expiresAt?: string;
5
+ };
6
+ export type BrowserLoginSession = {
7
+ loginUrl: string;
8
+ waitForResult: Promise<BrowserLoginResult>;
9
+ close: () => Promise<void>;
10
+ };
11
+ export declare function createCliLoginUrl({ apiUrl, redirectUri, state, }: {
12
+ apiUrl: string;
13
+ redirectUri: string;
14
+ state: string;
15
+ }): string;
16
+ export declare function startBrowserLogin({ apiUrl, timeoutMs, }: {
17
+ apiUrl: string;
18
+ timeoutMs?: number;
19
+ }): Promise<BrowserLoginSession>;
package/dist/login.js ADDED
@@ -0,0 +1,71 @@
1
+ import crypto from "node:crypto";
2
+ import http from "node:http";
3
+ export function createCliLoginUrl({ apiUrl, redirectUri, state, }) {
4
+ const url = new URL("/cli/login", apiUrl);
5
+ url.searchParams.set("redirect_uri", redirectUri);
6
+ url.searchParams.set("state", state);
7
+ return url.toString();
8
+ }
9
+ function html(message) {
10
+ return `<!doctype html><html><head><meta charset="utf-8"><title>VibeShip CLI</title></head><body><main style="font-family: system-ui, sans-serif; max-width: 42rem; margin: 4rem auto;"><h1>${message}</h1><p>You can return to your terminal.</p></main></body></html>`;
11
+ }
12
+ function writeHtml(response, statusCode, message) {
13
+ response.writeHead(statusCode, { "content-type": "text/html; charset=utf-8" });
14
+ response.end(html(message));
15
+ }
16
+ export async function startBrowserLogin({ apiUrl, timeoutMs = 120_000, }) {
17
+ const state = crypto.randomBytes(24).toString("base64url");
18
+ let settle;
19
+ const waitForResult = new Promise((resolve, reject) => {
20
+ settle = { resolve, reject };
21
+ });
22
+ const server = http.createServer((request, response) => {
23
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
24
+ if (requestUrl.pathname !== "/callback") {
25
+ writeHtml(response, 404, "Unknown VibeShip CLI callback");
26
+ return;
27
+ }
28
+ if (requestUrl.searchParams.get("state") !== state) {
29
+ writeHtml(response, 400, "Invalid VibeShip CLI login state");
30
+ settle?.reject(new Error("CLI login returned an invalid state."));
31
+ return;
32
+ }
33
+ const token = requestUrl.searchParams.get("token");
34
+ if (!token) {
35
+ writeHtml(response, 400, "VibeShip CLI login did not return a token");
36
+ settle?.reject(new Error("CLI login did not return a token."));
37
+ return;
38
+ }
39
+ writeHtml(response, 200, "VibeShip CLI login complete");
40
+ settle?.resolve({
41
+ token,
42
+ email: requestUrl.searchParams.get("email") ?? undefined,
43
+ expiresAt: requestUrl.searchParams.get("expires_at") ?? undefined,
44
+ });
45
+ });
46
+ await new Promise((resolve, reject) => {
47
+ server.once("error", reject);
48
+ server.listen(0, "127.0.0.1", () => resolve());
49
+ });
50
+ const address = server.address();
51
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
52
+ const timeout = setTimeout(() => {
53
+ settle?.reject(new Error("Timed out waiting for browser login."));
54
+ void closeServer(server);
55
+ }, timeoutMs);
56
+ waitForResult.finally(() => clearTimeout(timeout)).catch(() => undefined);
57
+ return {
58
+ loginUrl: createCliLoginUrl({ apiUrl, redirectUri, state }),
59
+ waitForResult,
60
+ close: () => closeServer(server),
61
+ };
62
+ }
63
+ function closeServer(server) {
64
+ return new Promise((resolve, reject) => {
65
+ if (!server.listening) {
66
+ resolve();
67
+ return;
68
+ }
69
+ server.close((error) => (error ? reject(error) : resolve()));
70
+ });
71
+ }
@@ -0,0 +1,2 @@
1
+ export declare function run(argv: string[]): Promise<void>;
2
+ export declare function normalizeArgv(argv: string[]): string[];
@@ -0,0 +1,256 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+ import { execa } from "execa";
5
+ import open from "open";
6
+ import { fetchEntitlements } from "./api.js";
7
+ import { clearAuth, defaultApiUrl, defaultPilotMcpUrl, readConfig, requireAuth, writeConfig, } from "./config.js";
8
+ import { installPilotCodexConfig } from "./codex.js";
9
+ import { startBrowserLogin } from "./login.js";
10
+ import { line, renderDone, renderInfo } from "./ui.js";
11
+ const STARTER_REPO = "git@github.com:vibeshiphq/vibeship-starter.git";
12
+ async function commandLogin(options) {
13
+ const config = readConfig();
14
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
15
+ if (!options.token) {
16
+ const login = await startBrowserLogin({ apiUrl });
17
+ renderInfo("Browser login", [
18
+ line("api", apiUrl),
19
+ line("callback", "listening on 127.0.0.1"),
20
+ ]);
21
+ try {
22
+ await open(login.loginUrl);
23
+ }
24
+ catch {
25
+ renderInfo("Open this URL", [line("url", login.loginUrl)], ["Complete the browser flow to return a CLI token."]);
26
+ }
27
+ const result = await login.waitForResult.finally(() => login.close());
28
+ writeConfig({
29
+ ...config,
30
+ apiUrl,
31
+ auth: {
32
+ token: result.token,
33
+ email: result.email ?? options.email,
34
+ expiresAt: result.expiresAt,
35
+ },
36
+ });
37
+ renderDone("Logged in", [
38
+ line("config", "~/.vibeship/config.json"),
39
+ line("api", apiUrl),
40
+ line("email", result.email ?? options.email ?? "unknown"),
41
+ line("expires", result.expiresAt ?? "unknown", "muted"),
42
+ ]);
43
+ return;
44
+ }
45
+ writeConfig({
46
+ ...config,
47
+ apiUrl,
48
+ auth: {
49
+ token: options.token,
50
+ email: options.email,
51
+ },
52
+ });
53
+ renderDone("Logged in", [
54
+ line("config", "~/.vibeship/config.json"),
55
+ line("api", apiUrl),
56
+ ]);
57
+ }
58
+ async function commandWhoami(options = {}) {
59
+ const config = readConfig();
60
+ const auth = requireAuth(config);
61
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
62
+ const status = await fetchEntitlements({
63
+ apiUrl,
64
+ auth,
65
+ });
66
+ renderDone("Account", [
67
+ line("email", status.email ?? auth.email ?? "unknown"),
68
+ line("api", apiUrl, "muted"),
69
+ line("starter", status.starterAccess ? "ready" : "missing", status.starterAccess ? "success" : "warning"),
70
+ line("pilot", status.pilotActive ? "active" : "inactive", status.pilotActive ? "success" : "warning"),
71
+ ]);
72
+ }
73
+ async function commandDoctor(options) {
74
+ const config = readConfig();
75
+ const projectDir = path.resolve(options.projectDir ?? process.cwd());
76
+ const marker = path.join(projectDir, ".vibeship", "project.json");
77
+ const codexConfig = path.join(projectDir, ".codex", "config.toml");
78
+ const hasAuth = Boolean(config.auth?.token);
79
+ const hasProject = fs.existsSync(marker);
80
+ const hasPilotConfig = fs.existsSync(codexConfig);
81
+ renderInfo("Doctor", [
82
+ line("auth", hasAuth ? "present" : "missing", hasAuth ? "success" : "warning"),
83
+ line("api", options.apiUrl ?? config.apiUrl ?? defaultApiUrl(), "muted"),
84
+ line("project", hasProject ? "vibeship starter" : "not detected", hasProject ? "success" : "warning"),
85
+ line("pilot config", hasPilotConfig ? codexConfig : "not installed", hasPilotConfig ? "success" : "warning"),
86
+ ], doctorActions({ hasAuth, hasProject, hasPilotConfig, projectDir }));
87
+ }
88
+ async function cloneStarter({ targetDir, localStarter, }) {
89
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
90
+ throw new Error(`${targetDir} already exists and is not empty.`);
91
+ }
92
+ if (localStarter) {
93
+ await execa("cp", ["-R", `${path.resolve(localStarter)}/.`, targetDir]);
94
+ return;
95
+ }
96
+ await execa("git", ["clone", STARTER_REPO, targetDir]);
97
+ }
98
+ async function commandInit(options) {
99
+ const config = readConfig();
100
+ const auth = requireAuth(config);
101
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
102
+ const status = await fetchEntitlements({
103
+ apiUrl,
104
+ auth,
105
+ });
106
+ if (!status.starterAccess) {
107
+ throw new Error("This account does not have VibeShip starter access.");
108
+ }
109
+ const targetDir = path.resolve(options.dir ?? options.targetDir ?? "vibeship-app");
110
+ renderInfo("Initializing starter", [
111
+ line("directory", targetDir),
112
+ line("source", options.localStarter ? path.resolve(options.localStarter) : STARTER_REPO),
113
+ line("install", options.skipInstall ? "skipped" : "pnpm install"),
114
+ ]);
115
+ await cloneStarter({ targetDir, localStarter: options.localStarter });
116
+ if (!options.skipInstall) {
117
+ await execa("pnpm", ["install"], { cwd: targetDir, stdio: "inherit" });
118
+ }
119
+ renderDone("Starter initialized", [line("directory", targetDir)], [`cd ${targetDir}`, "vibeship pilot install"]);
120
+ }
121
+ async function commandPilotInstall(options) {
122
+ const config = readConfig();
123
+ const auth = requireAuth(config);
124
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
125
+ const status = await fetchEntitlements({
126
+ apiUrl,
127
+ auth,
128
+ });
129
+ if (!status.pilotActive) {
130
+ throw new Error("This account does not have an active VibeShip Pilot subscription.");
131
+ }
132
+ const projectDir = path.resolve(options.projectDir ?? process.cwd());
133
+ const mcpUrl = options.mcpUrl ?? config.pilotMcpUrl ?? defaultPilotMcpUrl();
134
+ const file = installPilotCodexConfig({ projectDir, mcpUrl });
135
+ renderDone("Pilot installed", [
136
+ line("project", projectDir),
137
+ line("config", file),
138
+ line("mcp", mcpUrl),
139
+ ], [
140
+ "export VIBESHIP_PILOT_TOKEN=$(vibeship whoami --token-only)",
141
+ "Open Codex in this project and use VibeShip Pilot.",
142
+ ]);
143
+ }
144
+ async function commandPilotStatus(options) {
145
+ const config = readConfig();
146
+ const auth = requireAuth(config);
147
+ const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
148
+ const status = await fetchEntitlements({
149
+ apiUrl,
150
+ auth,
151
+ });
152
+ const projectDir = path.resolve(options.projectDir ?? process.cwd());
153
+ const codexConfig = path.join(projectDir, ".codex", "config.toml");
154
+ const hasPilotConfig = fs.existsSync(codexConfig);
155
+ renderDone("Pilot status", [
156
+ line("subscription", status.pilotActive ? "active" : "inactive", status.pilotActive ? "success" : "warning"),
157
+ line("project", projectDir),
158
+ line("config", hasPilotConfig ? codexConfig : "missing", hasPilotConfig ? "success" : "warning"),
159
+ ]);
160
+ }
161
+ export async function run(argv) {
162
+ const program = new Command();
163
+ program
164
+ .name("vibeship")
165
+ .description("Initialize VibeShip starter apps and install VibeShip Pilot.")
166
+ .version("0.1.0")
167
+ .showHelpAfterError()
168
+ .showSuggestionAfterError()
169
+ .configureHelp({ sortSubcommands: true })
170
+ .addHelpText("after", `
171
+ Examples:
172
+ $ vibeship login
173
+ $ vibeship init my-app
174
+ $ vibeship pilot install --project-dir ./my-app
175
+ $ vibeship doctor
176
+
177
+ Environment:
178
+ VIBESHIP_API_URL Override the VibeShip API URL.
179
+ VIBESHIP_PILOT_MCP_URL Override the Pilot MCP URL.
180
+ VIBESHIP_CLI_OFFLINE=1 Use local entitlement fixtures for development.
181
+ `);
182
+ program
183
+ .command("login")
184
+ .description("Authenticate this machine with VibeShip.")
185
+ .option("--token <token>", "CLI token issued by VibeShip")
186
+ .option("--email <email>", "Email to store with a development token")
187
+ .option("--api-url <url>", "VibeShip API URL")
188
+ .action(commandLogin);
189
+ program.command("logout").action(() => {
190
+ clearAuth();
191
+ renderDone("Logged out", [line("config", "~/.vibeship/config.json")]);
192
+ });
193
+ program
194
+ .command("whoami")
195
+ .description("Show the current VibeShip account and entitlement status.")
196
+ .option("--token-only", "Print the stored CLI token only")
197
+ .option("--api-url <url>", "VibeShip API URL")
198
+ .action((options) => {
199
+ if (options.tokenOnly) {
200
+ process.stdout.write(`${requireAuth(readConfig()).token}\n`);
201
+ return;
202
+ }
203
+ return commandWhoami(options);
204
+ });
205
+ program
206
+ .command("doctor")
207
+ .description("Inspect auth, project, and Pilot config for this directory.")
208
+ .option("--project-dir <dir>", "Project directory", process.cwd())
209
+ .option("--api-url <url>", "VibeShip API URL")
210
+ .action(commandDoctor);
211
+ program
212
+ .command("init [targetDir]")
213
+ .description("Clone the private starter into a new app directory.")
214
+ .option("--dir <dir>", "Target directory")
215
+ .option("--local-starter <dir>", "Use a local starter checkout")
216
+ .option("--skip-install", "Skip pnpm install")
217
+ .option("--api-url <url>", "VibeShip API URL")
218
+ .action((targetDir, options) => commandInit({ ...options, targetDir }));
219
+ const pilot = program.command("pilot").description("Manage VibeShip Pilot setup.");
220
+ pilot
221
+ .command("install")
222
+ .description("Install Pilot MCP config into a Codex project.")
223
+ .option("--project-dir <dir>", "Project directory", process.cwd())
224
+ .option("--mcp-url <url>", "Pilot MCP URL")
225
+ .option("--api-url <url>", "VibeShip API URL")
226
+ .action(commandPilotInstall);
227
+ pilot
228
+ .command("status")
229
+ .description("Show Pilot subscription and project config status.")
230
+ .option("--project-dir <dir>", "Project directory", process.cwd())
231
+ .option("--api-url <url>", "VibeShip API URL")
232
+ .action(commandPilotStatus);
233
+ await program.parseAsync(normalizeArgv(argv));
234
+ }
235
+ export function normalizeArgv(argv) {
236
+ const [runtime, script, ...args] = argv;
237
+ if (args[0] === "--") {
238
+ return [runtime, script, ...args.slice(1)];
239
+ }
240
+ return argv;
241
+ }
242
+ function doctorActions({ hasAuth, hasProject, hasPilotConfig, projectDir, }) {
243
+ const actions = [];
244
+ if (!hasAuth) {
245
+ actions.push("vibeship login");
246
+ }
247
+ if (!hasProject) {
248
+ actions.push("Run from a starter app, or create one with vibeship init my-app.");
249
+ }
250
+ if (!hasPilotConfig) {
251
+ actions.push(projectDir === process.cwd()
252
+ ? "vibeship pilot install"
253
+ : `vibeship pilot install --project-dir ${projectDir}`);
254
+ }
255
+ return actions;
256
+ }
package/dist/ui.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ export type DetailTone = "default" | "success" | "warning" | "danger" | "muted";
3
+ export type DetailLine = {
4
+ label: string;
5
+ value: string;
6
+ tone?: DetailTone;
7
+ };
8
+ type PanelProps = {
9
+ title: string;
10
+ subtitle?: string;
11
+ details?: DetailLine[];
12
+ actions?: string[];
13
+ tone?: DetailTone;
14
+ };
15
+ export declare function line(label: string, value: string, tone?: DetailTone): DetailLine;
16
+ export declare function Logo({ subtitle }: {
17
+ subtitle?: string;
18
+ }): React.JSX.Element;
19
+ export declare function StatusLine({ label, value, tone, }: {
20
+ label: string;
21
+ value: string;
22
+ tone?: DetailTone;
23
+ }): React.JSX.Element;
24
+ export declare function Panel({ title, subtitle, details, actions, tone, }: PanelProps): React.JSX.Element;
25
+ export declare function renderPanel(props: PanelProps): void;
26
+ export declare function renderDone(title: string, details: DetailLine[] | Array<[string, string]>, actions?: string[]): void;
27
+ export declare function renderInfo(title: string, details?: DetailLine[] | Array<[string, string]>, actions?: string[]): void;
28
+ export declare function renderError(title: string, message: string, actions?: string[]): void;
29
+ export declare function normalizeDetails(details: DetailLine[] | Array<[string, string]>): DetailLine[];
30
+ export {};
package/dist/ui.js ADDED
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, render } from "ink";
3
+ const toneColor = {
4
+ default: undefined,
5
+ success: "green",
6
+ warning: "yellow",
7
+ danger: "red",
8
+ muted: "gray",
9
+ };
10
+ export function line(label, value, tone = "default") {
11
+ return { label, value, tone };
12
+ }
13
+ export function Logo({ subtitle }) {
14
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyanBright", bold: true, children: "VibeShip" }), subtitle ? _jsx(Text, { color: "gray", children: subtitle }) : null] }));
15
+ }
16
+ export function StatusLine({ label, value, tone, }) {
17
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [label.padEnd(14), " "] }), _jsx(Text, { color: toneColor[tone ?? "default"], children: value })] }));
18
+ }
19
+ export function Panel({ title, subtitle, details = [], actions = [], tone = "default", }) {
20
+ const titleColor = toneColor[tone] ?? "white";
21
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(Logo, { subtitle: subtitle }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: titleColor, bold: true, children: title }), details.map((detail) => (_jsx(StatusLine, { label: detail.label, value: detail.value, tone: detail.tone }, detail.label))), actions.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Next steps" }), actions.map((action) => (_jsx(Text, { children: action }, action)))] })) : null] })] }));
22
+ }
23
+ export function renderPanel(props) {
24
+ const instance = render(_jsx(Panel, { title: props.title, subtitle: props.subtitle, details: props.details, actions: props.actions, tone: props.tone }));
25
+ instance.unmount();
26
+ }
27
+ export function renderDone(title, details, actions = []) {
28
+ renderPanel({
29
+ title,
30
+ subtitle: "Command complete",
31
+ details: normalizeDetails(details),
32
+ actions,
33
+ tone: "success",
34
+ });
35
+ }
36
+ export function renderInfo(title, details = [], actions = []) {
37
+ renderPanel({
38
+ title,
39
+ subtitle: "Working",
40
+ details: normalizeDetails(details),
41
+ actions,
42
+ });
43
+ }
44
+ export function renderError(title, message, actions = []) {
45
+ renderPanel({
46
+ title,
47
+ subtitle: "Command failed",
48
+ details: [line("error", message, "danger")],
49
+ actions,
50
+ tone: "danger",
51
+ });
52
+ }
53
+ export function normalizeDetails(details) {
54
+ return details.map((detail) => Array.isArray(detail) ? line(detail[0], detail[1]) : detail);
55
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@vibeshiphq/cli",
3
+ "version": "0.1.0",
4
+ "description": "VibeShip CLI for starter initialization and Pilot setup.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "homepage": "https://www.vibeship.today",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+ssh://git@github.com/vibeshiphq/vibeship-cli.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/vibeshiphq/vibeship-cli/issues"
14
+ },
15
+ "keywords": [
16
+ "vibeship",
17
+ "cli",
18
+ "starter",
19
+ "codex",
20
+ "mcp"
21
+ ],
22
+ "packageManager": "pnpm@9.15.0",
23
+ "main": "dist/index.js",
24
+ "exports": "./dist/index.js",
25
+ "bin": {
26
+ "vibeship": "dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsx src/index.ts",
37
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
38
+ "build": "pnpm clean && tsc -p tsconfig.json",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "check": "pnpm typecheck && pnpm test",
42
+ "prepack": "pnpm build"
43
+ },
44
+ "dependencies": {
45
+ "commander": "^14.0.2",
46
+ "execa": "^9.6.1",
47
+ "ink": "^6.5.0",
48
+ "open": "^10.2.0",
49
+ "react": "^19.2.4",
50
+ "zod": "^4.4.3"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.19.25",
54
+ "@types/react": "^19.2.7",
55
+ "tsx": "^4.22.4",
56
+ "typescript": "^5.9.3",
57
+ "vitest": "^4.1.8"
58
+ },
59
+ "engines": {
60
+ "node": ">=20"
61
+ }
62
+ }