@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 +72 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +44 -0
- package/dist/codex.d.ts +8 -0
- package/dist/codex.js +25 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/login.d.ts +19 -0
- package/dist/login.js +71 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +256 -0
- package/dist/ui.d.ts +30 -0
- package/dist/ui.js +55 -0
- package/package.json +62 -0
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
|
+
}
|
package/dist/codex.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
+
}
|
package/dist/login.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/program.js
ADDED
|
@@ -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
|
+
}
|