@vibeshiphq/cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/api.d.ts +26 -1
- package/dist/api.js +79 -2
- package/dist/codex.d.ts +2 -2
- package/dist/codex.js +7 -7
- package/dist/config.d.ts +2 -2
- package/dist/config.js +3 -3
- package/dist/index.js +9 -2
- package/dist/program.d.ts +21 -0
- package/dist/program.js +169 -44
- package/dist/project-config.d.ts +31 -0
- package/dist/project-config.js +75 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# VibeShip CLI
|
|
2
2
|
|
|
3
|
-
Public CLI for initializing the private VibeShip starter and installing VibeShip
|
|
3
|
+
Public CLI for initializing the private VibeShip starter and installing VibeShip Hyperdrive for guided setup.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @vibeshiphq/cli
|
|
7
7
|
vibeship login
|
|
8
8
|
vibeship init my-app
|
|
9
9
|
cd my-app
|
|
10
|
-
vibeship
|
|
10
|
+
vibeship hyperdrive install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The CLI does not embed proprietary setup runbooks. It authenticates the user, checks starter/
|
|
13
|
+
The CLI does not embed proprietary setup runbooks. It authenticates the user, checks starter/Hyperdrive entitlement through VibeShip, clones the private starter, and writes local Codex MCP config for Hyperdrive.
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
@@ -18,15 +18,20 @@ The CLI does not embed proprietary setup runbooks. It authenticates the user, ch
|
|
|
18
18
|
vibeship login # Authenticate this machine with VibeShip
|
|
19
19
|
vibeship logout # Remove local CLI auth
|
|
20
20
|
vibeship whoami # Show account and entitlement status
|
|
21
|
-
vibeship doctor # Inspect auth, project, and
|
|
21
|
+
vibeship doctor # Inspect auth, project, and Hyperdrive config
|
|
22
22
|
vibeship init [targetDir] # Clone the private starter into a new app
|
|
23
|
-
vibeship
|
|
24
|
-
vibeship
|
|
23
|
+
vibeship hyperdrive install # Install Hyperdrive MCP config into .codex/config.toml
|
|
24
|
+
vibeship hyperdrive status # Show Hyperdrive subscription, config, and MCP reachability
|
|
25
25
|
```
|
|
26
26
|
|
|
27
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
28
|
|
|
29
|
-
|
|
29
|
+
Hyperdrive MCP defaults to `https://hyperdrive.vibeship.today/mcp`; override it with
|
|
30
|
+
`VIBESHIP_HYPERDRIVE_MCP_URL` or `--mcp-url`. `vibeship hyperdrive status` calls Hyperdrive's
|
|
31
|
+
`vibeship_hyperdrive_status` MCP tool with your stored CLI token and reports whether
|
|
32
|
+
the server accepted the token.
|
|
33
|
+
|
|
34
|
+
`vibeship init` checks your VibeShip entitlement first, then tries to clone the private starter over SSH and falls back to HTTPS. If GitHub access fails after entitlement approval, connect the Polar GitHub repository access benefit in the VibeShip customer portal, verify access to `vibeshiphq/vibeship-starter`, and rerun `vibeship init`. Support can provide `--repo-url` or `VIBESHIP_STARTER_REPO_URL` for temporary clone overrides.
|
|
30
35
|
|
|
31
36
|
## Development
|
|
32
37
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import type { AuthState } from "./config.js";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
export type EntitlementStatus = {
|
|
3
4
|
email?: string;
|
|
4
5
|
starterAccess: boolean;
|
|
5
|
-
|
|
6
|
+
hyperdriveActive: boolean;
|
|
6
7
|
entitlements: string[];
|
|
7
8
|
};
|
|
9
|
+
declare const hyperdriveMcpStatusSchema: z.ZodObject<{
|
|
10
|
+
authenticated: z.ZodBoolean;
|
|
11
|
+
subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
12
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
13
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
14
|
+
entitlements: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
15
|
+
tokenExpiresAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
16
|
+
subscriptionStatus: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
17
|
+
server: z.ZodObject<{
|
|
18
|
+
name: z.ZodString;
|
|
19
|
+
version: z.ZodString;
|
|
20
|
+
protocolVersion: z.ZodString;
|
|
21
|
+
authority: z.ZodString;
|
|
22
|
+
}, z.core.$loose>;
|
|
23
|
+
providers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
24
|
+
rateLimit: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
25
|
+
}, z.core.$loose>;
|
|
26
|
+
export type HyperdriveMcpStatus = z.infer<typeof hyperdriveMcpStatusSchema>;
|
|
8
27
|
export declare function fetchEntitlements({ apiUrl, auth, }: {
|
|
9
28
|
apiUrl: string;
|
|
10
29
|
auth: AuthState;
|
|
11
30
|
}): Promise<EntitlementStatus>;
|
|
31
|
+
export declare function fetchHyperdriveMcpStatus({ mcpUrl, auth, fetchImpl, }: {
|
|
32
|
+
mcpUrl: string;
|
|
33
|
+
auth: AuthState;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
}): Promise<HyperdriveMcpStatus>;
|
|
36
|
+
export {};
|
package/dist/api.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const hyperdriveMcpStatusSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
authenticated: z.boolean(),
|
|
5
|
+
subject: z.string().nullable().optional(),
|
|
6
|
+
email: z.string().nullable().optional(),
|
|
7
|
+
reason: z.string().nullable().optional(),
|
|
8
|
+
entitlements: z.array(z.string()).default([]),
|
|
9
|
+
tokenExpiresAt: z.string().nullable().optional(),
|
|
10
|
+
subscriptionStatus: z.string().nullable().optional(),
|
|
11
|
+
server: z
|
|
12
|
+
.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
version: z.string(),
|
|
15
|
+
protocolVersion: z.string(),
|
|
16
|
+
authority: z.string(),
|
|
17
|
+
})
|
|
18
|
+
.passthrough(),
|
|
19
|
+
providers: z.array(z.string()).default([]),
|
|
20
|
+
rateLimit: z.unknown().nullable().optional(),
|
|
21
|
+
})
|
|
22
|
+
.passthrough();
|
|
1
23
|
export async function fetchEntitlements({ apiUrl, auth, }) {
|
|
2
24
|
if (process.env.VIBESHIP_CLI_OFFLINE === "1") {
|
|
3
25
|
return {
|
|
4
26
|
email: auth.email,
|
|
5
27
|
starterAccess: true,
|
|
6
|
-
|
|
7
|
-
entitlements: ["license:starter", "
|
|
28
|
+
hyperdriveActive: true,
|
|
29
|
+
entitlements: ["license:starter", "hyperdrive:active"],
|
|
8
30
|
};
|
|
9
31
|
}
|
|
10
32
|
const url = new URL("/api/cli/entitlements", apiUrl);
|
|
@@ -28,6 +50,61 @@ export async function fetchEntitlements({ apiUrl, auth, }) {
|
|
|
28
50
|
}
|
|
29
51
|
return (await response.json());
|
|
30
52
|
}
|
|
53
|
+
export async function fetchHyperdriveMcpStatus({ mcpUrl, auth, fetchImpl = fetch, }) {
|
|
54
|
+
const url = new URL(mcpUrl);
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetchImpl(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
authorization: `Bearer ${auth.token}`,
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
accept: "application/json",
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: "vibeship-cli-status",
|
|
67
|
+
method: "tools/call",
|
|
68
|
+
params: {
|
|
69
|
+
name: "vibeship_hyperdrive_status",
|
|
70
|
+
arguments: {},
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new Error(`Could not reach Hyperdrive MCP at ${url.origin}: ${message}`);
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Hyperdrive MCP check failed with HTTP ${response.status}${await responseSuffix(response)}.`);
|
|
81
|
+
}
|
|
82
|
+
const body = (await response.json());
|
|
83
|
+
if (body.error) {
|
|
84
|
+
const message = typeof body.error.message === "string"
|
|
85
|
+
? body.error.message
|
|
86
|
+
: "JSON-RPC error";
|
|
87
|
+
throw new Error(`Hyperdrive MCP check failed: ${message}`);
|
|
88
|
+
}
|
|
89
|
+
const status = body.result?.structuredContent ??
|
|
90
|
+
parseTextContent(body.result?.content?.find((item) => item.type === "text"));
|
|
91
|
+
const parsed = hyperdriveMcpStatusSchema.safeParse(status);
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
throw new Error("Hyperdrive MCP status response was not recognized.");
|
|
94
|
+
}
|
|
95
|
+
return parsed.data;
|
|
96
|
+
}
|
|
97
|
+
function parseTextContent(item) {
|
|
98
|
+
if (typeof item?.text !== "string") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(item.text);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
31
108
|
async function responseSuffix(response) {
|
|
32
109
|
const text = (await response.text()).trim();
|
|
33
110
|
if (!text) {
|
package/dist/codex.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export declare function projectCodexConfigPath(projectDir: string): string;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function hyperdriveCodexConfig({ mcpUrl, }: {
|
|
3
3
|
mcpUrl: string;
|
|
4
4
|
}): string;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function installHyperdriveCodexConfig({ projectDir, mcpUrl, }: {
|
|
6
6
|
projectDir: string;
|
|
7
7
|
mcpUrl: string;
|
|
8
8
|
}): string;
|
package/dist/codex.js
CHANGED
|
@@ -3,21 +3,21 @@ import path from "node:path";
|
|
|
3
3
|
export function projectCodexConfigPath(projectDir) {
|
|
4
4
|
return path.join(projectDir, ".codex", "config.toml");
|
|
5
5
|
}
|
|
6
|
-
export function
|
|
7
|
-
return `[mcp_servers.vibeship-
|
|
6
|
+
export function hyperdriveCodexConfig({ mcpUrl, }) {
|
|
7
|
+
return `[mcp_servers.vibeship-hyperdrive]
|
|
8
8
|
url = "${mcpUrl}"
|
|
9
|
-
bearer_token_env_var = "
|
|
9
|
+
bearer_token_env_var = "VIBESHIP_HYPERDRIVE_TOKEN"
|
|
10
10
|
default_tools_approval_mode = "prompt"
|
|
11
11
|
`;
|
|
12
12
|
}
|
|
13
|
-
export function
|
|
13
|
+
export function installHyperdriveCodexConfig({ projectDir, mcpUrl, }) {
|
|
14
14
|
const file = projectCodexConfigPath(projectDir);
|
|
15
15
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
16
16
|
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
17
|
-
const marker = "[mcp_servers.vibeship-
|
|
17
|
+
const marker = "[mcp_servers.vibeship-hyperdrive]";
|
|
18
18
|
const next = existing.includes(marker)
|
|
19
|
-
? existing.replace(/\[mcp_servers\.vibeship-
|
|
20
|
-
: `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${
|
|
19
|
+
? existing.replace(/\[mcp_servers\.vibeship-hyperdrive\][\s\S]*?(?=\n\[|$)/, hyperdriveCodexConfig({ mcpUrl }).trimEnd())
|
|
20
|
+
: `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${hyperdriveCodexConfig({
|
|
21
21
|
mcpUrl,
|
|
22
22
|
}).trimEnd()}\n`;
|
|
23
23
|
fs.writeFileSync(file, next);
|
package/dist/config.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type AuthState = z.infer<typeof authStateSchema>;
|
|
|
9
9
|
export type CliConfig = {
|
|
10
10
|
auth?: AuthState;
|
|
11
11
|
apiUrl?: string;
|
|
12
|
-
|
|
12
|
+
hyperdriveMcpUrl?: string;
|
|
13
13
|
};
|
|
14
14
|
export declare function configDir(): string;
|
|
15
15
|
export declare function configPath(): string;
|
|
@@ -18,5 +18,5 @@ export declare function writeConfig(config: CliConfig): void;
|
|
|
18
18
|
export declare function clearAuth(): void;
|
|
19
19
|
export declare function requireAuth(config?: CliConfig): AuthState;
|
|
20
20
|
export declare function defaultApiUrl(): string;
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function defaultHyperdriveMcpUrl(): string;
|
|
22
22
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,7 @@ const authStateSchema = z.object({
|
|
|
11
11
|
const configSchema = z.object({
|
|
12
12
|
auth: authStateSchema.optional(),
|
|
13
13
|
apiUrl: z.string().url().optional(),
|
|
14
|
-
|
|
14
|
+
hyperdriveMcpUrl: z.string().url().optional(),
|
|
15
15
|
});
|
|
16
16
|
export function configDir() {
|
|
17
17
|
return path.join(os.homedir(), ".vibeship");
|
|
@@ -52,6 +52,6 @@ export function defaultApiUrl() {
|
|
|
52
52
|
}
|
|
53
53
|
return "https://www.vibeship.today";
|
|
54
54
|
}
|
|
55
|
-
export function
|
|
56
|
-
return process.env.
|
|
55
|
+
export function defaultHyperdriveMcpUrl() {
|
|
56
|
+
return process.env.VIBESHIP_HYPERDRIVE_MCP_URL ?? "https://hyperdrive.vibeship.today/mcp";
|
|
57
57
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,9 @@ function recoveryActions(message) {
|
|
|
16
16
|
"Then run vibeship login again.",
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
|
-
if (message.includes("
|
|
19
|
+
if (message.includes("Hyperdrive subscription")) {
|
|
20
20
|
return [
|
|
21
|
-
"Confirm this account has an active
|
|
21
|
+
"Confirm this account has an active Hyperdrive subscription at https://www.vibeship.today.",
|
|
22
22
|
"Then run vibeship login again.",
|
|
23
23
|
];
|
|
24
24
|
}
|
|
@@ -34,5 +34,12 @@ function recoveryActions(message) {
|
|
|
34
34
|
if (message.includes("already exists and is not empty")) {
|
|
35
35
|
return ["Choose a new directory: vibeship init my-app"];
|
|
36
36
|
}
|
|
37
|
+
if (message.includes("Could not clone the VibeShip starter")) {
|
|
38
|
+
return [
|
|
39
|
+
"Confirm the Polar GitHub repository access benefit is connected to your GitHub account.",
|
|
40
|
+
"Check SSH with `ssh -T git@github.com` or HTTPS auth with `gh auth status`.",
|
|
41
|
+
"Use --repo-url or VIBESHIP_STARTER_REPO_URL if support gave you a temporary clone URL.",
|
|
42
|
+
];
|
|
43
|
+
}
|
|
37
44
|
return ["Run with --help to inspect command options."];
|
|
38
45
|
}
|
package/dist/program.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
+
import { type SetupConfig } from "./project-config.js";
|
|
2
|
+
type CloneSource = {
|
|
3
|
+
label: string;
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
type CloneFailure = CloneSource & {
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function starterCloneSources(overrideUrl?: string): CloneSource[];
|
|
10
|
+
export declare function starterCloneRecoveryActions(failures: CloneFailure[]): string[];
|
|
11
|
+
export declare function shouldPromptForSetup(options: {
|
|
12
|
+
workflow?: string;
|
|
13
|
+
integrations?: string;
|
|
14
|
+
interactive?: boolean;
|
|
15
|
+
}): boolean;
|
|
16
|
+
export declare function resolveInitSetupConfig(options: {
|
|
17
|
+
workflow?: string;
|
|
18
|
+
integrations?: string;
|
|
19
|
+
interactive?: boolean;
|
|
20
|
+
}): Promise<SetupConfig>;
|
|
1
21
|
export declare function run(argv: string[]): Promise<void>;
|
|
2
22
|
export declare function normalizeArgv(argv: string[]): string[];
|
|
23
|
+
export {};
|
package/dist/program.js
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { execa } from "execa";
|
|
5
6
|
import open from "open";
|
|
6
|
-
import { fetchEntitlements } from "./api.js";
|
|
7
|
-
import { clearAuth, defaultApiUrl,
|
|
8
|
-
import {
|
|
7
|
+
import { fetchEntitlements, fetchHyperdriveMcpStatus } from "./api.js";
|
|
8
|
+
import { clearAuth, defaultApiUrl, defaultHyperdriveMcpUrl, readConfig, requireAuth, writeConfig, } from "./config.js";
|
|
9
|
+
import { installHyperdriveCodexConfig } from "./codex.js";
|
|
9
10
|
import { startBrowserLogin } from "./login.js";
|
|
11
|
+
import { parseIntegrations, parseWorkflow, setupConfigForWorkflow, setupIntegrations, writeProjectSetupConfig, } from "./project-config.js";
|
|
10
12
|
import { line, renderDone, renderInfo } from "./ui.js";
|
|
11
|
-
const
|
|
13
|
+
const STARTER_REPO_SSH = "git@github.com:vibeshiphq/vibeship-starter.git";
|
|
14
|
+
const STARTER_REPO_HTTPS = "https://github.com/vibeshiphq/vibeship-starter.git";
|
|
15
|
+
export function starterCloneSources(overrideUrl) {
|
|
16
|
+
const configured = overrideUrl ?? process.env.VIBESHIP_STARTER_REPO_URL?.trim();
|
|
17
|
+
if (configured) {
|
|
18
|
+
return [{ label: "custom", url: configured }];
|
|
19
|
+
}
|
|
20
|
+
return [
|
|
21
|
+
{ label: "ssh", url: STARTER_REPO_SSH },
|
|
22
|
+
{ label: "https", url: STARTER_REPO_HTTPS },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function cloneErrorMessage(error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
return String(error);
|
|
30
|
+
}
|
|
31
|
+
export function starterCloneRecoveryActions(failures) {
|
|
32
|
+
const attempted = failures
|
|
33
|
+
.map((failure) => `${failure.label}: ${failure.url}`)
|
|
34
|
+
.join("; ");
|
|
35
|
+
return [
|
|
36
|
+
`Clone attempts failed (${attempted}).`,
|
|
37
|
+
"Open the Polar customer portal from VibeShip and confirm the GitHub repository access benefit is connected to the right GitHub account.",
|
|
38
|
+
"Confirm that account can access vibeshiphq/vibeship-starter in GitHub.",
|
|
39
|
+
"For SSH, run `ssh -T git@github.com` and confirm your key is accepted.",
|
|
40
|
+
"For HTTPS, run `gh auth status` or sign in to GitHub in your credential manager.",
|
|
41
|
+
"If access was just granted, wait a minute and rerun `vibeship init`.",
|
|
42
|
+
];
|
|
43
|
+
}
|
|
12
44
|
async function commandLogin(options) {
|
|
13
45
|
const config = readConfig();
|
|
14
46
|
const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
|
|
@@ -67,7 +99,7 @@ async function commandWhoami(options = {}) {
|
|
|
67
99
|
line("email", status.email ?? auth.email ?? "unknown"),
|
|
68
100
|
line("api", apiUrl, "muted"),
|
|
69
101
|
line("starter", status.starterAccess ? "ready" : "missing", status.starterAccess ? "success" : "warning"),
|
|
70
|
-
line("
|
|
102
|
+
line("hyperdrive", status.hyperdriveActive ? "active" : "inactive", status.hyperdriveActive ? "success" : "warning"),
|
|
71
103
|
]);
|
|
72
104
|
}
|
|
73
105
|
async function commandDoctor(options) {
|
|
@@ -77,15 +109,15 @@ async function commandDoctor(options) {
|
|
|
77
109
|
const codexConfig = path.join(projectDir, ".codex", "config.toml");
|
|
78
110
|
const hasAuth = Boolean(config.auth?.token);
|
|
79
111
|
const hasProject = fs.existsSync(marker);
|
|
80
|
-
const
|
|
112
|
+
const hasHyperdriveConfig = fs.existsSync(codexConfig);
|
|
81
113
|
renderInfo("Doctor", [
|
|
82
114
|
line("auth", hasAuth ? "present" : "missing", hasAuth ? "success" : "warning"),
|
|
83
115
|
line("api", options.apiUrl ?? config.apiUrl ?? defaultApiUrl(), "muted"),
|
|
84
116
|
line("project", hasProject ? "vibeship starter" : "not detected", hasProject ? "success" : "warning"),
|
|
85
|
-
line("
|
|
86
|
-
], doctorActions({ hasAuth, hasProject,
|
|
117
|
+
line("hyperdrive config", hasHyperdriveConfig ? codexConfig : "not installed", hasHyperdriveConfig ? "success" : "warning"),
|
|
118
|
+
], doctorActions({ hasAuth, hasProject, hasHyperdriveConfig, projectDir }));
|
|
87
119
|
}
|
|
88
|
-
async function cloneStarter({ targetDir, localStarter, }) {
|
|
120
|
+
async function cloneStarter({ targetDir, localStarter, repoUrl, }) {
|
|
89
121
|
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
90
122
|
throw new Error(`${targetDir} already exists and is not empty.`);
|
|
91
123
|
}
|
|
@@ -93,7 +125,66 @@ async function cloneStarter({ targetDir, localStarter, }) {
|
|
|
93
125
|
await execa("cp", ["-R", `${path.resolve(localStarter)}/.`, targetDir]);
|
|
94
126
|
return;
|
|
95
127
|
}
|
|
96
|
-
|
|
128
|
+
const failures = [];
|
|
129
|
+
for (const source of starterCloneSources(repoUrl)) {
|
|
130
|
+
try {
|
|
131
|
+
await execa("git", ["clone", source.url, targetDir]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
failures.push({ ...source, message: cloneErrorMessage(error) });
|
|
136
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error([
|
|
140
|
+
"Could not clone the VibeShip starter after entitlement approval.",
|
|
141
|
+
...starterCloneRecoveryActions(failures),
|
|
142
|
+
"Raw git errors:",
|
|
143
|
+
...failures.map((failure) => `- ${failure.label}: ${failure.message.slice(0, 500)}`),
|
|
144
|
+
].join("\n"));
|
|
145
|
+
}
|
|
146
|
+
export function shouldPromptForSetup(options) {
|
|
147
|
+
if (options.interactive === false) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return (!options.workflow &&
|
|
151
|
+
!options.integrations &&
|
|
152
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
153
|
+
}
|
|
154
|
+
function parseIntegrationAnswer(answer) {
|
|
155
|
+
return parseIntegrations(answer.trim() || undefined);
|
|
156
|
+
}
|
|
157
|
+
async function promptForSetupConfig() {
|
|
158
|
+
const rl = readline.createInterface({
|
|
159
|
+
input: process.stdin,
|
|
160
|
+
output: process.stdout,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
const workflowAnswer = await rl.question("Setup workflow [1 local-first, 2 launch-ready] (1): ");
|
|
164
|
+
const workflow = workflowAnswer.trim() === "2" || workflowAnswer.trim() === "launch-ready"
|
|
165
|
+
? "launch-ready"
|
|
166
|
+
: "local-first";
|
|
167
|
+
const defaultList = setupIntegrations
|
|
168
|
+
.filter((integration) => ["clerk", "convex", "polar", "resend", "vercel"].includes(integration))
|
|
169
|
+
.join(",");
|
|
170
|
+
const integrationsAnswer = await rl.question(`Enabled integrations (${defaultList}): `);
|
|
171
|
+
return setupConfigForWorkflow({
|
|
172
|
+
workflow,
|
|
173
|
+
integrations: parseIntegrationAnswer(integrationsAnswer) ?? undefined,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
rl.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function resolveInitSetupConfig(options) {
|
|
181
|
+
if (shouldPromptForSetup(options)) {
|
|
182
|
+
return promptForSetupConfig();
|
|
183
|
+
}
|
|
184
|
+
return setupConfigForWorkflow({
|
|
185
|
+
workflow: parseWorkflow(options.workflow),
|
|
186
|
+
integrations: parseIntegrations(options.integrations),
|
|
187
|
+
});
|
|
97
188
|
}
|
|
98
189
|
async function commandInit(options) {
|
|
99
190
|
const config = readConfig();
|
|
@@ -107,18 +198,33 @@ async function commandInit(options) {
|
|
|
107
198
|
throw new Error("This account does not have VibeShip starter access.");
|
|
108
199
|
}
|
|
109
200
|
const targetDir = path.resolve(options.dir ?? options.targetDir ?? "vibeship-app");
|
|
201
|
+
const setup = await resolveInitSetupConfig({
|
|
202
|
+
workflow: options.workflow,
|
|
203
|
+
integrations: options.integrations,
|
|
204
|
+
});
|
|
110
205
|
renderInfo("Initializing starter", [
|
|
111
206
|
line("directory", targetDir),
|
|
112
|
-
line("source", options.localStarter
|
|
207
|
+
line("source", options.localStarter
|
|
208
|
+
? path.resolve(options.localStarter)
|
|
209
|
+
: starterCloneSources(options.repoUrl)
|
|
210
|
+
.map((source) => source.url)
|
|
211
|
+
.join(" then ")),
|
|
113
212
|
line("install", options.skipInstall ? "skipped" : "pnpm install"),
|
|
213
|
+
line("workflow", setup.workflow),
|
|
214
|
+
line("environments", setup.environments.join(", ")),
|
|
114
215
|
]);
|
|
115
|
-
await cloneStarter({
|
|
216
|
+
await cloneStarter({
|
|
217
|
+
targetDir,
|
|
218
|
+
localStarter: options.localStarter,
|
|
219
|
+
repoUrl: options.repoUrl,
|
|
220
|
+
});
|
|
221
|
+
writeProjectSetupConfig({ projectDir: targetDir, setup });
|
|
116
222
|
if (!options.skipInstall) {
|
|
117
223
|
await execa("pnpm", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
118
224
|
}
|
|
119
|
-
renderDone("Starter initialized", [line("directory", targetDir)], [`cd ${targetDir}`, "vibeship
|
|
225
|
+
renderDone("Starter initialized", [line("directory", targetDir)], [`cd ${targetDir}`, "vibeship hyperdrive install"]);
|
|
120
226
|
}
|
|
121
|
-
async function
|
|
227
|
+
async function commandHyperdriveInstall(options) {
|
|
122
228
|
const config = readConfig();
|
|
123
229
|
const auth = requireAuth(config);
|
|
124
230
|
const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
|
|
@@ -126,22 +232,22 @@ async function commandPilotInstall(options) {
|
|
|
126
232
|
apiUrl,
|
|
127
233
|
auth,
|
|
128
234
|
});
|
|
129
|
-
if (!status.
|
|
130
|
-
throw new Error("This account does not have an active VibeShip
|
|
235
|
+
if (!status.hyperdriveActive) {
|
|
236
|
+
throw new Error("This account does not have an active VibeShip Hyperdrive subscription.");
|
|
131
237
|
}
|
|
132
238
|
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
133
|
-
const mcpUrl = options.mcpUrl ?? config.
|
|
134
|
-
const file =
|
|
135
|
-
renderDone("
|
|
239
|
+
const mcpUrl = options.mcpUrl ?? config.hyperdriveMcpUrl ?? defaultHyperdriveMcpUrl();
|
|
240
|
+
const file = installHyperdriveCodexConfig({ projectDir, mcpUrl });
|
|
241
|
+
renderDone("Hyperdrive installed", [
|
|
136
242
|
line("project", projectDir),
|
|
137
243
|
line("config", file),
|
|
138
244
|
line("mcp", mcpUrl),
|
|
139
245
|
], [
|
|
140
|
-
"export
|
|
141
|
-
"Open Codex in this project and use VibeShip
|
|
246
|
+
"export VIBESHIP_HYPERDRIVE_TOKEN=$(vibeship whoami --token-only)",
|
|
247
|
+
"Open Codex in this project and use VibeShip Hyperdrive.",
|
|
142
248
|
]);
|
|
143
249
|
}
|
|
144
|
-
async function
|
|
250
|
+
async function commandHyperdriveStatus(options) {
|
|
145
251
|
const config = readConfig();
|
|
146
252
|
const auth = requireAuth(config);
|
|
147
253
|
const apiUrl = options.apiUrl ?? config.apiUrl ?? defaultApiUrl();
|
|
@@ -151,19 +257,32 @@ async function commandPilotStatus(options) {
|
|
|
151
257
|
});
|
|
152
258
|
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
153
259
|
const codexConfig = path.join(projectDir, ".codex", "config.toml");
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
260
|
+
const hasHyperdriveConfig = fs.existsSync(codexConfig);
|
|
261
|
+
const mcpUrl = options.mcpUrl ?? config.hyperdriveMcpUrl ?? defaultHyperdriveMcpUrl();
|
|
262
|
+
const details = [
|
|
263
|
+
line("subscription", status.hyperdriveActive ? "active" : "inactive", status.hyperdriveActive ? "success" : "warning"),
|
|
157
264
|
line("project", projectDir),
|
|
158
|
-
line("config",
|
|
159
|
-
|
|
265
|
+
line("config", hasHyperdriveConfig ? codexConfig : "missing", hasHyperdriveConfig ? "success" : "warning"),
|
|
266
|
+
line("mcp", mcpUrl, "muted"),
|
|
267
|
+
];
|
|
268
|
+
try {
|
|
269
|
+
const hyperdriveStatus = await fetchHyperdriveMcpStatus({ mcpUrl, auth });
|
|
270
|
+
details.push(line("mcp auth", hyperdriveStatus.authenticated
|
|
271
|
+
? "accepted"
|
|
272
|
+
: `rejected: ${hyperdriveStatus.reason ?? "unknown"}`, hyperdriveStatus.authenticated ? "success" : "warning"), line("server", `${hyperdriveStatus.server.name}@${hyperdriveStatus.server.version}`, "muted"));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
details.push(line("mcp check", message.slice(0, 140), "warning"));
|
|
277
|
+
}
|
|
278
|
+
renderDone("Hyperdrive status", details);
|
|
160
279
|
}
|
|
161
280
|
export async function run(argv) {
|
|
162
281
|
const program = new Command();
|
|
163
282
|
program
|
|
164
283
|
.name("vibeship")
|
|
165
|
-
.description("Initialize VibeShip starter apps and install VibeShip
|
|
166
|
-
.version("0.1
|
|
284
|
+
.description("Initialize VibeShip starter apps and install VibeShip Hyperdrive.")
|
|
285
|
+
.version("0.2.1")
|
|
167
286
|
.showHelpAfterError()
|
|
168
287
|
.showSuggestionAfterError()
|
|
169
288
|
.configureHelp({ sortSubcommands: true })
|
|
@@ -171,12 +290,14 @@ export async function run(argv) {
|
|
|
171
290
|
Examples:
|
|
172
291
|
$ vibeship login
|
|
173
292
|
$ vibeship init my-app
|
|
174
|
-
$ vibeship
|
|
293
|
+
$ vibeship init my-app --workflow local-first --integrations clerk,convex,polar,resend,vercel
|
|
294
|
+
$ vibeship hyperdrive install --project-dir ./my-app
|
|
175
295
|
$ vibeship doctor
|
|
176
296
|
|
|
177
297
|
Environment:
|
|
178
298
|
VIBESHIP_API_URL Override the VibeShip API URL.
|
|
179
|
-
|
|
299
|
+
VIBESHIP_HYPERDRIVE_MCP_URL Override the Hyperdrive MCP URL.
|
|
300
|
+
VIBESHIP_STARTER_REPO_URL Override the starter repository clone URL.
|
|
180
301
|
VIBESHIP_CLI_OFFLINE=1 Use local entitlement fixtures for development.
|
|
181
302
|
`);
|
|
182
303
|
program
|
|
@@ -204,7 +325,7 @@ Environment:
|
|
|
204
325
|
});
|
|
205
326
|
program
|
|
206
327
|
.command("doctor")
|
|
207
|
-
.description("Inspect auth, project, and
|
|
328
|
+
.description("Inspect auth, project, and Hyperdrive config for this directory.")
|
|
208
329
|
.option("--project-dir <dir>", "Project directory", process.cwd())
|
|
209
330
|
.option("--api-url <url>", "VibeShip API URL")
|
|
210
331
|
.action(commandDoctor);
|
|
@@ -213,23 +334,27 @@ Environment:
|
|
|
213
334
|
.description("Clone the private starter into a new app directory.")
|
|
214
335
|
.option("--dir <dir>", "Target directory")
|
|
215
336
|
.option("--local-starter <dir>", "Use a local starter checkout")
|
|
337
|
+
.option("--repo-url <url>", "Override the starter repository clone URL")
|
|
216
338
|
.option("--skip-install", "Skip pnpm install")
|
|
217
339
|
.option("--api-url <url>", "VibeShip API URL")
|
|
340
|
+
.option("--workflow <workflow>", "Setup workflow: local-first or launch-ready")
|
|
341
|
+
.option("--integrations <list>", "Comma-separated setup integrations to enable")
|
|
218
342
|
.action((targetDir, options) => commandInit({ ...options, targetDir }));
|
|
219
|
-
const
|
|
220
|
-
|
|
343
|
+
const hyperdrive = program.command("hyperdrive").description("Manage VibeShip Hyperdrive setup.");
|
|
344
|
+
hyperdrive
|
|
221
345
|
.command("install")
|
|
222
|
-
.description("Install
|
|
346
|
+
.description("Install Hyperdrive MCP config into a Codex project.")
|
|
223
347
|
.option("--project-dir <dir>", "Project directory", process.cwd())
|
|
224
|
-
.option("--mcp-url <url>", "
|
|
348
|
+
.option("--mcp-url <url>", "Hyperdrive MCP URL")
|
|
225
349
|
.option("--api-url <url>", "VibeShip API URL")
|
|
226
|
-
.action(
|
|
227
|
-
|
|
350
|
+
.action(commandHyperdriveInstall);
|
|
351
|
+
hyperdrive
|
|
228
352
|
.command("status")
|
|
229
|
-
.description("Show
|
|
353
|
+
.description("Show Hyperdrive subscription and project config status.")
|
|
230
354
|
.option("--project-dir <dir>", "Project directory", process.cwd())
|
|
231
355
|
.option("--api-url <url>", "VibeShip API URL")
|
|
232
|
-
.
|
|
356
|
+
.option("--mcp-url <url>", "Hyperdrive MCP URL")
|
|
357
|
+
.action(commandHyperdriveStatus);
|
|
233
358
|
await program.parseAsync(normalizeArgv(argv));
|
|
234
359
|
}
|
|
235
360
|
export function normalizeArgv(argv) {
|
|
@@ -239,7 +364,7 @@ export function normalizeArgv(argv) {
|
|
|
239
364
|
}
|
|
240
365
|
return argv;
|
|
241
366
|
}
|
|
242
|
-
function doctorActions({ hasAuth, hasProject,
|
|
367
|
+
function doctorActions({ hasAuth, hasProject, hasHyperdriveConfig, projectDir, }) {
|
|
243
368
|
const actions = [];
|
|
244
369
|
if (!hasAuth) {
|
|
245
370
|
actions.push("vibeship login");
|
|
@@ -247,10 +372,10 @@ function doctorActions({ hasAuth, hasProject, hasPilotConfig, projectDir, }) {
|
|
|
247
372
|
if (!hasProject) {
|
|
248
373
|
actions.push("Run from a starter app, or create one with vibeship init my-app.");
|
|
249
374
|
}
|
|
250
|
-
if (!
|
|
375
|
+
if (!hasHyperdriveConfig) {
|
|
251
376
|
actions.push(projectDir === process.cwd()
|
|
252
|
-
? "vibeship
|
|
253
|
-
: `vibeship
|
|
377
|
+
? "vibeship hyperdrive install"
|
|
378
|
+
: `vibeship hyperdrive install --project-dir ${projectDir}`);
|
|
254
379
|
}
|
|
255
380
|
return actions;
|
|
256
381
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const setupWorkflows: readonly ["local-first", "launch-ready"];
|
|
2
|
+
export declare const setupEnvironments: readonly ["development", "preview", "production"];
|
|
3
|
+
export declare const setupIntegrations: readonly ["clerk", "convex", "polar", "resend", "posthog", "vercel", "calCom"];
|
|
4
|
+
export type SetupWorkflow = (typeof setupWorkflows)[number];
|
|
5
|
+
export type SetupEnvironment = (typeof setupEnvironments)[number];
|
|
6
|
+
export type SetupIntegration = (typeof setupIntegrations)[number];
|
|
7
|
+
export type SetupConfig = {
|
|
8
|
+
workflow: SetupWorkflow;
|
|
9
|
+
environments: SetupEnvironment[];
|
|
10
|
+
integrations: Record<SetupIntegration, boolean>;
|
|
11
|
+
};
|
|
12
|
+
export declare function defaultIntegrations(): {
|
|
13
|
+
clerk: true;
|
|
14
|
+
convex: true;
|
|
15
|
+
polar: true;
|
|
16
|
+
resend: true;
|
|
17
|
+
posthog: false;
|
|
18
|
+
vercel: true;
|
|
19
|
+
calCom: false;
|
|
20
|
+
};
|
|
21
|
+
export declare function environmentsForWorkflow(workflow: SetupWorkflow): SetupEnvironment[];
|
|
22
|
+
export declare function setupConfigForWorkflow({ workflow, integrations, }: {
|
|
23
|
+
workflow: SetupWorkflow;
|
|
24
|
+
integrations?: SetupIntegration[];
|
|
25
|
+
}): SetupConfig;
|
|
26
|
+
export declare function parseWorkflow(value: string | undefined): SetupWorkflow;
|
|
27
|
+
export declare function parseIntegrations(value: string | undefined): ("clerk" | "convex" | "polar" | "resend" | "posthog" | "vercel" | "calCom")[] | undefined;
|
|
28
|
+
export declare function writeProjectSetupConfig({ projectDir, setup, }: {
|
|
29
|
+
projectDir: string;
|
|
30
|
+
setup: SetupConfig;
|
|
31
|
+
}): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const setupWorkflows = ["local-first", "launch-ready"];
|
|
5
|
+
export const setupEnvironments = [
|
|
6
|
+
"development",
|
|
7
|
+
"preview",
|
|
8
|
+
"production",
|
|
9
|
+
];
|
|
10
|
+
export const setupIntegrations = [
|
|
11
|
+
"clerk",
|
|
12
|
+
"convex",
|
|
13
|
+
"polar",
|
|
14
|
+
"resend",
|
|
15
|
+
"posthog",
|
|
16
|
+
"vercel",
|
|
17
|
+
"calCom",
|
|
18
|
+
];
|
|
19
|
+
const workflowSchema = z.enum(setupWorkflows);
|
|
20
|
+
const integrationsSchema = z
|
|
21
|
+
.array(z.enum(setupIntegrations))
|
|
22
|
+
.min(1, "Select at least one integration.");
|
|
23
|
+
export function defaultIntegrations() {
|
|
24
|
+
return {
|
|
25
|
+
clerk: true,
|
|
26
|
+
convex: true,
|
|
27
|
+
polar: true,
|
|
28
|
+
resend: true,
|
|
29
|
+
posthog: false,
|
|
30
|
+
vercel: true,
|
|
31
|
+
calCom: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function environmentsForWorkflow(workflow) {
|
|
35
|
+
return workflow === "local-first"
|
|
36
|
+
? ["development"]
|
|
37
|
+
: ["development", "preview", "production"];
|
|
38
|
+
}
|
|
39
|
+
export function setupConfigForWorkflow({ workflow, integrations, }) {
|
|
40
|
+
const enabled = {
|
|
41
|
+
...defaultIntegrations(),
|
|
42
|
+
};
|
|
43
|
+
if (integrations) {
|
|
44
|
+
for (const integration of setupIntegrations) {
|
|
45
|
+
enabled[integration] = integrations.includes(integration);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
workflow,
|
|
50
|
+
environments: environmentsForWorkflow(workflow),
|
|
51
|
+
integrations: enabled,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function parseWorkflow(value) {
|
|
55
|
+
return workflowSchema.parse(value ?? "local-first");
|
|
56
|
+
}
|
|
57
|
+
export function parseIntegrations(value) {
|
|
58
|
+
if (!value) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const parsed = value
|
|
62
|
+
.split(",")
|
|
63
|
+
.map((entry) => entry.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
return integrationsSchema.parse(parsed);
|
|
66
|
+
}
|
|
67
|
+
export function writeProjectSetupConfig({ projectDir, setup, }) {
|
|
68
|
+
const vibeshipDir = path.join(projectDir, ".vibeship");
|
|
69
|
+
const marker = path.join(vibeshipDir, "project.json");
|
|
70
|
+
const existing = fs.existsSync(marker)
|
|
71
|
+
? JSON.parse(fs.readFileSync(marker, "utf8"))
|
|
72
|
+
: {};
|
|
73
|
+
fs.mkdirSync(vibeshipDir, { recursive: true });
|
|
74
|
+
fs.writeFileSync(marker, `${JSON.stringify({ ...existing, setup }, null, 2)}\n`);
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeshiphq/cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "VibeShip CLI for starter initialization and
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "VibeShip CLI for starter initialization and Hyperdrive setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://www.vibeship.today",
|