inflight-cli 0.1.1 → 1.0.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 CHANGED
@@ -18,17 +18,24 @@ Authenticate with your Inflight account:
18
18
  inflight login
19
19
  ```
20
20
 
21
- This opens a browser window to complete authentication. Your credentials are stored in `~/.inflight/config.json`.
21
+ This opens a browser window to complete authentication. Your credentials are stored in `~/Library/Application Support/co.inflight.cli/auth.json` (macOS) or `~/.local/share/co.inflight.cli/auth.json` (Linux).
22
22
 
23
- ### Create a version
23
+ ### Share a version
24
24
 
25
- Create a new draft version for the current project:
25
+ Share a new design version for the current project:
26
26
 
27
27
  ```bash
28
- inflight create
28
+ inflight share
29
29
  ```
30
30
 
31
- You'll be prompted for a title (defaults to your current git branch name) and a staging URL. Git context (branch, commit, diff) is automatically captured if you're in a git repo.
31
+ This will:
32
+ 1. Detect your git branch and commit info automatically
33
+ 2. Ask where your staging URL is hosted (Vercel or paste a URL)
34
+ 3. Create a version on Inflight and open it in your browser
35
+
36
+ On first run, you'll be prompted to link your Vercel project and select a workspace. These are saved locally so subsequent runs are fast.
37
+
38
+ Git context (branch, commit, diff) is captured automatically and used to generate a version title and feedback questions.
32
39
 
33
40
  ## Requirements
34
41
 
@@ -1,3 +1 @@
1
- export declare function createCommand(opts: {
2
- title?: string;
3
- }): Promise<void>;
1
+ export declare function createCommand(): Promise<void>;
@@ -1,23 +1,23 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
- import { readGlobalConfig, writeGlobalConfig } from "../lib/config.js";
3
+ import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
4
  import { getGitInfo, isGitRepo } from "../lib/git.js";
5
- import { detectStagingUrl } from "../lib/vercel.js";
5
+ import { providers } from "../providers/index.js";
6
6
  import { apiGetMe, apiCreateVersion } from "../lib/api.js";
7
- export async function createCommand(opts) {
7
+ export async function createCommand() {
8
8
  const cwd = process.cwd();
9
- const config = readGlobalConfig();
10
- if (!config) {
9
+ const auth = readGlobalAuth();
10
+ if (!auth) {
11
11
  p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
12
12
  process.exit(1);
13
13
  }
14
- p.intro(pc.bgBlue(pc.white(" inflight create ")));
15
- // Resolve workspace — lazy, cached after first use
16
- let workspaceId = config.workspaceId;
14
+ p.intro(pc.bgBlue(pc.white(" inflight share ")));
15
+ // Resolve workspace — read from .inflight/workspace.json, prompt if not linked
16
+ let workspaceId = readWorkspaceConfig(cwd)?.workspaceId;
17
17
  if (!workspaceId) {
18
18
  const spinner = p.spinner();
19
19
  spinner.start("Loading workspaces...");
20
- const me = await apiGetMe(config.apiKey, config.apiUrl).catch((e) => {
20
+ const me = await apiGetMe(auth.apiKey, auth.apiUrl).catch((e) => {
21
21
  spinner.stop("Failed.");
22
22
  p.log.error(e.message);
23
23
  process.exit(1);
@@ -42,31 +42,34 @@ export async function createCommand(opts) {
42
42
  }
43
43
  workspaceId = selected;
44
44
  }
45
- writeGlobalConfig({ ...config, workspaceId });
45
+ writeWorkspaceConfig(cwd, { workspaceId });
46
46
  }
47
47
  // Git info
48
48
  const gitInfo = isGitRepo(cwd)
49
49
  ? getGitInfo(cwd)
50
50
  : { branch: null, commitShort: null, commitFull: null, commitMessage: null, remoteUrl: null, isDirty: false, diff: null };
51
- // Title defaults to branch name
52
- const defaultTitle = gitInfo.branch ?? "Untitled";
53
- const titleInput = opts.title ??
54
- (await p.text({
55
- message: "Version title",
56
- placeholder: defaultTitle,
57
- defaultValue: defaultTitle,
58
- }));
59
- if (p.isCancel(titleInput)) {
51
+ // Title is auto-generated server-side from the diff/branch/commit
52
+ const title = gitInfo.branch ?? "Untitled";
53
+ // Staging URL user picks provider
54
+ const providerChoice = await p.select({
55
+ message: "Where is your staging URL hosted?",
56
+ options: [
57
+ ...providers.map((prov) => ({ value: prov.id, label: prov.label })),
58
+ { value: "manual", label: "Paste a URL" },
59
+ ],
60
+ });
61
+ if (p.isCancel(providerChoice)) {
60
62
  p.cancel("Cancelled.");
61
63
  process.exit(0);
62
64
  }
63
- const title = titleInput;
64
- // Staging URL auto-detected from env or prompted
65
- let stagingUrl = detectStagingUrl();
66
- if (stagingUrl) {
67
- p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
65
+ const providerSpinner = p.spinner();
66
+ const provider = providers.find((prov) => prov.id === providerChoice);
67
+ let stagingUrl;
68
+ if (provider) {
69
+ stagingUrl = (await provider.resolve(cwd, gitInfo, providerSpinner)) ?? undefined;
68
70
  }
69
- else {
71
+ // Manual input (or fallback if provider returned no URL)
72
+ if (!stagingUrl) {
70
73
  const input = await p.text({
71
74
  message: "Staging URL",
72
75
  placeholder: "https://my-branch.vercel.app",
@@ -90,8 +93,8 @@ export async function createCommand(opts) {
90
93
  const spinner = p.spinner();
91
94
  spinner.start("Creating version...");
92
95
  const result = await apiCreateVersion({
93
- apiKey: config.apiKey,
94
- apiUrl: config.apiUrl,
96
+ apiKey: auth.apiKey,
97
+ apiUrl: auth.apiUrl,
95
98
  workspaceId,
96
99
  title,
97
100
  stagingUrl,
@@ -1,7 +1,7 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { createServer } from "http";
4
- import { writeGlobalConfig } from "../lib/config.js";
4
+ import { writeGlobalAuth } from "../lib/config.js";
5
5
  import { apiGetMe } from "../lib/api.js";
6
6
  const DEFAULT_API_URL = process.env.INFLIGHT_API_URL ?? "https://api.inflight.co";
7
7
  const WEB_URL = process.env.INFLIGHT_WEB_URL ?? "https://inflight.co";
@@ -18,7 +18,7 @@ export async function loginCommand() {
18
18
  process.exit(1);
19
19
  });
20
20
  spinner.stop(`Authenticated as ${pc.bold(me.name)}`);
21
- writeGlobalConfig({ apiKey, apiUrl });
21
+ writeGlobalAuth({ apiKey, apiUrl });
22
22
  p.outro(pc.green("✓ Logged in successfully"));
23
23
  process.exit(0);
24
24
  }
@@ -52,9 +52,10 @@ function browserAuth() {
52
52
  const { default: open } = await import("open");
53
53
  await open(authUrl);
54
54
  });
55
- setTimeout(() => {
55
+ const timeoutId = setTimeout(() => {
56
56
  server.close();
57
57
  reject(new Error("Authentication timed out after 5 minutes"));
58
58
  }, 5 * 60 * 1000);
59
+ server.once("close", () => clearTimeout(timeoutId));
59
60
  });
60
61
  }
@@ -0,0 +1 @@
1
+ export declare function shareCommand(): Promise<void>;
@@ -0,0 +1,106 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
+ import { getGitInfo, isGitRepo } from "../lib/git.js";
5
+ import { providers } from "../providers/index.js";
6
+ import { apiGetMe, apiCreateVersion } from "../lib/api.js";
7
+ export async function shareCommand() {
8
+ const cwd = process.cwd();
9
+ const auth = readGlobalAuth();
10
+ if (!auth) {
11
+ p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
12
+ process.exit(1);
13
+ }
14
+ p.intro(pc.bgBlue(pc.white(" inflight share ")));
15
+ // Resolve workspace — read from .inflight/workspace.json, prompt if not linked
16
+ let workspaceId = readWorkspaceConfig(cwd)?.workspaceId;
17
+ if (!workspaceId) {
18
+ const spinner = p.spinner();
19
+ spinner.start("Loading workspaces...");
20
+ const me = await apiGetMe(auth.apiKey, auth.apiUrl).catch((e) => {
21
+ spinner.stop("Failed.");
22
+ p.log.error(e.message);
23
+ process.exit(1);
24
+ });
25
+ spinner.stop("");
26
+ if (me.workspaces.length === 0) {
27
+ p.log.error("No workspaces found. Create one at inflight.co first.");
28
+ process.exit(1);
29
+ }
30
+ else if (me.workspaces.length === 1) {
31
+ workspaceId = me.workspaces[0].id;
32
+ p.log.info(`Workspace: ${pc.bold(me.workspaces[0].name)}`);
33
+ }
34
+ else {
35
+ const selected = await p.select({
36
+ message: "Select a workspace",
37
+ options: me.workspaces.map((w) => ({ value: w.id, label: w.name })),
38
+ });
39
+ if (p.isCancel(selected)) {
40
+ p.cancel("Cancelled.");
41
+ process.exit(0);
42
+ }
43
+ workspaceId = selected;
44
+ }
45
+ writeWorkspaceConfig(cwd, { workspaceId });
46
+ }
47
+ // Git info
48
+ const gitInfo = isGitRepo(cwd)
49
+ ? getGitInfo(cwd)
50
+ : { branch: null, commitShort: null, commitFull: null, commitMessage: null, remoteUrl: null, isDirty: false, diff: null };
51
+ // Staging URL — user picks provider
52
+ const providerChoice = await p.select({
53
+ message: "Where is your staging URL hosted?",
54
+ options: [
55
+ ...providers.map((prov) => ({ value: prov.id, label: prov.label })),
56
+ { value: "manual", label: "Paste a URL" },
57
+ ],
58
+ });
59
+ if (p.isCancel(providerChoice)) {
60
+ p.cancel("Cancelled.");
61
+ process.exit(0);
62
+ }
63
+ const providerSpinner = p.spinner();
64
+ const provider = providers.find((prov) => prov.id === providerChoice);
65
+ let stagingUrl;
66
+ if (provider) {
67
+ stagingUrl = (await provider.resolve(cwd, gitInfo, providerSpinner)) ?? undefined;
68
+ }
69
+ // Manual input (or fallback if provider returned no URL)
70
+ if (!stagingUrl) {
71
+ const input = await p.text({
72
+ message: "Staging URL",
73
+ placeholder: "https://my-branch.vercel.app",
74
+ validate: (v) => {
75
+ if (!v)
76
+ return "Staging URL is required";
77
+ try {
78
+ new URL(v);
79
+ }
80
+ catch {
81
+ return "Must be a valid URL (include https://)";
82
+ }
83
+ },
84
+ });
85
+ if (p.isCancel(input)) {
86
+ p.cancel("Cancelled.");
87
+ process.exit(0);
88
+ }
89
+ stagingUrl = input;
90
+ }
91
+ const result = await apiCreateVersion({
92
+ apiKey: auth.apiKey,
93
+ apiUrl: auth.apiUrl,
94
+ workspaceId,
95
+ stagingUrl,
96
+ gitInfo,
97
+ }).catch((e) => {
98
+ p.log.error(e.message);
99
+ process.exit(1);
100
+ });
101
+ p.note(result.inflightUrl, "Your Inflight version");
102
+ p.outro(pc.green("✓ Done") + " — opening Inflight...");
103
+ const { default: open } = await import("open");
104
+ await open(result.inflightUrl);
105
+ process.exit(0);
106
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { loginCommand } from "./commands/login.js";
4
- import { createCommand } from "./commands/create.js";
4
+ import { shareCommand } from "./commands/share.js";
5
5
  const program = new Command();
6
6
  program
7
7
  .name("inflight")
@@ -12,8 +12,7 @@ program
12
12
  .description("Authenticate with your Inflight account")
13
13
  .action(loginCommand);
14
14
  program
15
- .command("create")
16
- .description("Create a new draft version for this project")
17
- .option("-t, --title <title>", "Version title")
18
- .action((opts) => createCommand({ title: opts.title }));
15
+ .command("share")
16
+ .description("Share a new version for this project")
17
+ .action(() => shareCommand());
19
18
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -16,7 +16,6 @@ export declare function apiCreateVersion(opts: {
16
16
  apiKey: string;
17
17
  apiUrl: string;
18
18
  workspaceId: string;
19
- title: string;
20
19
  stagingUrl: string;
21
20
  gitInfo: GitInfo;
22
21
  }): Promise<CreateVersionResult>;
package/dist/lib/api.js CHANGED
@@ -15,7 +15,6 @@ export async function apiCreateVersion(opts) {
15
15
  },
16
16
  body: JSON.stringify({
17
17
  workspaceId: opts.workspaceId,
18
- title: opts.title,
19
18
  stagingUrl: opts.stagingUrl,
20
19
  gitInfo: opts.gitInfo,
21
20
  }),
@@ -1,7 +1,11 @@
1
- export interface GlobalConfig {
1
+ export interface GlobalAuth {
2
2
  apiKey: string;
3
3
  apiUrl: string;
4
- workspaceId?: string;
5
4
  }
6
- export declare function readGlobalConfig(): GlobalConfig | null;
7
- export declare function writeGlobalConfig(config: GlobalConfig): void;
5
+ export declare function readGlobalAuth(): GlobalAuth | null;
6
+ export declare function writeGlobalAuth(auth: GlobalAuth): void;
7
+ export interface WorkspaceConfig {
8
+ workspaceId: string;
9
+ }
10
+ export declare function readWorkspaceConfig(cwd: string): WorkspaceConfig | null;
11
+ export declare function writeWorkspaceConfig(cwd: string, config: WorkspaceConfig): void;
@@ -1,22 +1,61 @@
1
- import { homedir } from "os";
1
+ import { homedir, platform } from "os";
2
2
  import { join } from "path";
3
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
4
- const CONFIG_DIR = join(homedir(), ".inflight");
5
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
- const DEFAULT_API_URL = process.env.INFLIGHT_API_URL ?? "https://api-staging.inflight.co";
7
- export function readGlobalConfig() {
8
- if (!existsSync(CONFIG_FILE))
4
+ function getGlobalConfigDir() {
5
+ if (platform() === "darwin") {
6
+ return join(homedir(), "Library", "Application Support", "co.inflight.cli");
7
+ }
8
+ if (platform() === "win32") {
9
+ return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "co.inflight.cli");
10
+ }
11
+ return join(homedir(), ".local", "share", "co.inflight.cli");
12
+ }
13
+ const AUTH_FILE = join(getGlobalConfigDir(), "auth.json");
14
+ const DEFAULT_API_URL = process.env.INFLIGHT_API_URL ?? "https://api.inflight.co";
15
+ export function readGlobalAuth() {
16
+ if (!existsSync(AUTH_FILE))
17
+ return null;
18
+ try {
19
+ const auth = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
20
+ return { ...auth, apiUrl: DEFAULT_API_URL };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export function writeGlobalAuth(auth) {
27
+ mkdirSync(getGlobalConfigDir(), { recursive: true });
28
+ writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
29
+ }
30
+ // --- Project-level config (per-directory, like .vercel/project.json) ---
31
+ const WORKSPACE_FILE = ".inflight/workspace.json";
32
+ export function readWorkspaceConfig(cwd) {
33
+ const file = join(cwd, WORKSPACE_FILE);
34
+ if (!existsSync(file))
9
35
  return null;
10
36
  try {
11
- const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
12
- // Allow env var to override stored API URL (useful for local dev)
13
- return { ...config, apiUrl: DEFAULT_API_URL };
37
+ return JSON.parse(readFileSync(file, "utf-8"));
14
38
  }
15
39
  catch {
16
40
  return null;
17
41
  }
18
42
  }
19
- export function writeGlobalConfig(config) {
20
- mkdirSync(CONFIG_DIR, { recursive: true });
21
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
43
+ export function writeWorkspaceConfig(cwd, config) {
44
+ const dir = join(cwd, ".inflight");
45
+ mkdirSync(dir, { recursive: true });
46
+ writeFileSync(join(cwd, WORKSPACE_FILE), JSON.stringify(config, null, 2));
47
+ addToGitignore(cwd);
48
+ }
49
+ function addToGitignore(cwd) {
50
+ const gitignorePath = join(cwd, ".gitignore");
51
+ const entry = ".inflight";
52
+ try {
53
+ const contents = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
54
+ if (!contents.split("\n").some((line) => line.trim() === entry)) {
55
+ writeFileSync(gitignorePath, contents + (contents.endsWith("\n") ? "" : "\n") + entry + "\n");
56
+ }
57
+ }
58
+ catch {
59
+ // non-fatal
60
+ }
22
61
  }
@@ -1,5 +1,26 @@
1
+ export declare function isVercelLoggedIn(): Promise<boolean>;
2
+ export declare function isVercelLinked(cwd: string): boolean;
3
+ /** Opens browser-based Vercel login — suppresses CLI output, browser opens automatically. */
4
+ export declare function vercelLogin(): boolean;
5
+ export interface VercelTeam {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ }
10
+ export interface VercelProject {
11
+ id: string;
12
+ name: string;
13
+ }
14
+ export declare function getVercelTeams(): Promise<VercelTeam[]>;
15
+ export declare function getVercelProjects(teamId: string): Promise<VercelProject[]>;
16
+ /** Links the directory to a specific Vercel project silently. */
17
+ export declare function vercelLink(cwd: string, projectId: string): Promise<boolean>;
18
+ export interface VercelDeployment {
19
+ url: string;
20
+ label: string;
21
+ }
1
22
  /**
2
- * Detect staging URL from hosting provider environment variables.
3
- * Only works when running inside a CI/CD build environment.
23
+ * Fetches Vercel deployments for the current commit SHA.
24
+ * Falls back to recent deployments if no SHA match is found.
4
25
  */
5
- export declare function detectStagingUrl(): string | null;
26
+ export declare function getVercelDeployments(cwd: string, commitSha: string | null, branch: string | null): Promise<VercelDeployment[]>;
@@ -1,18 +1,177 @@
1
+ import { execSync, spawnSync, spawn } from "child_process";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
1
5
  /**
2
- * Detect staging URL from hosting provider environment variables.
3
- * Only works when running inside a CI/CD build environment.
6
+ * Returns the vercel command to use the installed CLI if available,
7
+ * otherwise falls back to npx so users don't need a global install.
4
8
  */
5
- export function detectStagingUrl() {
6
- // Vercel preview deployments
7
- if (process.env.VERCEL_URL) {
8
- return `https://${process.env.VERCEL_URL}`;
9
+ function getVercelCmd() {
10
+ try {
11
+ execSync("vercel --version", { stdio: "pipe" });
12
+ return { cmd: "vercel", args: [] };
9
13
  }
10
- // Netlify deploy previews
11
- if (process.env.DEPLOY_PRIME_URL) {
12
- return process.env.DEPLOY_PRIME_URL;
14
+ catch {
15
+ return { cmd: "npx", args: ["--yes", "vercel"] };
13
16
  }
14
- if (process.env.DEPLOY_URL) {
15
- return process.env.DEPLOY_URL;
17
+ }
18
+ export function isVercelLoggedIn() {
19
+ const { cmd, args } = getVercelCmd();
20
+ return new Promise((resolve) => {
21
+ const child = spawn(cmd, [...args, "whoami"], { stdio: "pipe" });
22
+ child.on("close", (code) => resolve(code === 0));
23
+ child.on("error", () => resolve(false));
24
+ });
25
+ }
26
+ export function isVercelLinked(cwd) {
27
+ return existsSync(join(cwd, ".vercel", "project.json"));
28
+ }
29
+ /** Opens browser-based Vercel login — suppresses CLI output, browser opens automatically. */
30
+ export function vercelLogin() {
31
+ const { cmd, args } = getVercelCmd();
32
+ const result = spawnSync(cmd, [...args, "login"], { stdio: "pipe" });
33
+ return result.status === 0;
34
+ }
35
+ export async function getVercelTeams() {
36
+ const token = getVercelAuthToken();
37
+ if (!token)
38
+ return [];
39
+ const res = await fetch("https://api.vercel.com/v2/teams", {
40
+ headers: { Authorization: `Bearer ${token}` },
41
+ });
42
+ if (!res.ok)
43
+ return [];
44
+ const data = (await res.json());
45
+ return data.teams;
46
+ }
47
+ export async function getVercelProjects(teamId) {
48
+ const token = getVercelAuthToken();
49
+ if (!token)
50
+ return [];
51
+ const params = new URLSearchParams({ teamId, limit: "100" });
52
+ const res = await fetch(`https://api.vercel.com/v10/projects?${params}`, {
53
+ headers: { Authorization: `Bearer ${token}` },
54
+ });
55
+ if (!res.ok)
56
+ return [];
57
+ const data = (await res.json());
58
+ return data.projects;
59
+ }
60
+ /** Links the directory to a specific Vercel project silently. */
61
+ export function vercelLink(cwd, projectId) {
62
+ const { cmd, args } = getVercelCmd();
63
+ return new Promise((resolve) => {
64
+ const child = spawn(cmd, [...args, "link", "--yes", "--project", projectId], {
65
+ stdio: "pipe",
66
+ cwd,
67
+ });
68
+ child.on("close", (code) => resolve(code === 0));
69
+ child.on("error", () => resolve(false));
70
+ });
71
+ }
72
+ /** Reads the auth token saved by the Vercel CLI after login. */
73
+ function getVercelAuthToken() {
74
+ const candidates = [
75
+ join(homedir(), "Library", "Application Support", "com.vercel.cli", "auth.json"), // macOS
76
+ join(homedir(), ".local", "share", "com.vercel.cli", "auth.json"), // Linux
77
+ join(homedir(), ".config", "vercel", "auth.json"), // older versions
78
+ ];
79
+ for (const p of candidates) {
80
+ if (existsSync(p)) {
81
+ try {
82
+ const data = JSON.parse(readFileSync(p, "utf-8"));
83
+ return data.token ?? null;
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ }
16
89
  }
17
90
  return null;
18
91
  }
92
+ /** Reads the project + team IDs from `.vercel/project.json`. */
93
+ function getVercelProjectConfig(cwd) {
94
+ const configPath = join(cwd, ".vercel", "project.json");
95
+ if (!existsSync(configPath))
96
+ return null;
97
+ try {
98
+ return JSON.parse(readFileSync(configPath, "utf-8"));
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ /**
105
+ * Fetches Vercel deployments for the current commit SHA.
106
+ * Falls back to recent deployments if no SHA match is found.
107
+ */
108
+ export async function getVercelDeployments(cwd, commitSha, branch) {
109
+ const token = getVercelAuthToken();
110
+ const project = getVercelProjectConfig(cwd);
111
+ if (!token || !project)
112
+ return [];
113
+ async function fetchDeployments(extraParams) {
114
+ const params = new URLSearchParams({
115
+ projectId: project.projectId,
116
+ teamId: project.orgId,
117
+ limit: "1",
118
+ state: "READY",
119
+ ...extraParams,
120
+ });
121
+ const res = await fetch(`https://api.vercel.com/v6/deployments?${params}`, {
122
+ headers: { Authorization: `Bearer ${token}` },
123
+ });
124
+ if (!res.ok)
125
+ return [];
126
+ const data = (await res.json());
127
+ return data.deployments;
128
+ }
129
+ async function fetchAutomaticAliases(uid) {
130
+ const params = new URLSearchParams({ teamId: project.orgId });
131
+ const res = await fetch(`https://api.vercel.com/v13/deployments/${uid}?${params}`, {
132
+ headers: { Authorization: `Bearer ${token}` },
133
+ });
134
+ if (!res.ok)
135
+ return [];
136
+ const data = (await res.json());
137
+ return data.automaticAliases ?? [];
138
+ }
139
+ // Fetch commit deployment and latest branch deployment in parallel
140
+ const [commitDeployments, branchDeployments] = await Promise.all([
141
+ commitSha ? fetchDeployments({ sha: commitSha }) : Promise.resolve([]),
142
+ branch ? fetchDeployments({ branch }) : Promise.resolve([]),
143
+ ]);
144
+ const results = [];
145
+ const seenUrls = new Set();
146
+ // Branch alias URL from automaticAliases (auto-updates with each push)
147
+ const branchDeploy = branchDeployments[0];
148
+ if (branchDeploy) {
149
+ const aliases = await fetchAutomaticAliases(branchDeploy.uid);
150
+ for (const alias of aliases) {
151
+ const url = `https://${alias}`;
152
+ if (!seenUrls.has(url)) {
153
+ results.push({ url, label: `branch: ${branch} (${alias})` });
154
+ seenUrls.add(url);
155
+ }
156
+ }
157
+ }
158
+ // Commit-specific URL
159
+ const commitDeploy = commitDeployments[0];
160
+ if (commitDeploy) {
161
+ const url = `https://${commitDeploy.url}`;
162
+ if (!seenUrls.has(url)) {
163
+ results.push({ url, label: `commit: ${commitSha?.slice(0, 7)} (${commitDeploy.url})` });
164
+ seenUrls.add(url);
165
+ }
166
+ }
167
+ // Fallback: most recent deployment
168
+ if (results.length === 0) {
169
+ const recent = await fetchDeployments({});
170
+ const d = recent[0];
171
+ if (d) {
172
+ const b = d.meta?.githubCommitRef ?? d.meta?.gitlabCommitRef ?? d.meta?.bitbucketCommitRef;
173
+ results.push({ url: `https://${d.url}`, label: b ? `${b} (${d.url})` : d.url });
174
+ }
175
+ }
176
+ return results;
177
+ }
@@ -0,0 +1,9 @@
1
+ import * as p from "@clack/prompts";
2
+ import type { GitInfo } from "../lib/git.js";
3
+ export type ProviderSpinner = ReturnType<typeof p.spinner>;
4
+ export interface Provider {
5
+ id: string;
6
+ label: string;
7
+ resolve: (cwd: string, gitInfo: GitInfo, spinner: ProviderSpinner) => Promise<string | null>;
8
+ }
9
+ export declare const providers: Provider[];
@@ -0,0 +1,4 @@
1
+ import { resolveVercelUrl } from "./vercel.js";
2
+ export const providers = [
3
+ { id: "vercel", label: "Vercel", resolve: resolveVercelUrl },
4
+ ];
@@ -0,0 +1,3 @@
1
+ import type { ProviderSpinner } from "./index.js";
2
+ import type { GitInfo } from "../lib/git.js";
3
+ export declare function resolveVercelUrl(cwd: string, gitInfo: GitInfo, spinner: ProviderSpinner): Promise<string | null>;
@@ -0,0 +1,78 @@
1
+ import * as p from "@clack/prompts";
2
+ import { isVercelLoggedIn, isVercelLinked, vercelLogin, vercelLink, getVercelTeams, getVercelProjects, getVercelDeployments, } from "../lib/vercel.js";
3
+ export async function resolveVercelUrl(cwd, gitInfo, spinner) {
4
+ // Ensure logged in
5
+ spinner.start("Checking Vercel...");
6
+ const loggedIn = await isVercelLoggedIn();
7
+ if (!loggedIn) {
8
+ spinner.stop("Opening browser to log in to Vercel...");
9
+ const ok = vercelLogin();
10
+ if (!ok) {
11
+ p.log.error("Vercel login failed.");
12
+ process.exit(1);
13
+ }
14
+ }
15
+ else {
16
+ spinner.stop("Vercel ready");
17
+ }
18
+ // Ensure project is linked
19
+ if (!isVercelLinked(cwd)) {
20
+ const teams = await getVercelTeams();
21
+ if (teams.length === 0) {
22
+ p.log.error("No Vercel teams found.");
23
+ process.exit(1);
24
+ }
25
+ let teamId;
26
+ if (teams.length === 1) {
27
+ teamId = teams[0].id;
28
+ }
29
+ else {
30
+ const selectedTeam = await p.select({
31
+ message: "Select a Vercel team",
32
+ options: teams.map((t) => ({ value: t.id, label: t.name })),
33
+ });
34
+ if (p.isCancel(selectedTeam)) {
35
+ p.cancel("Cancelled.");
36
+ process.exit(0);
37
+ }
38
+ teamId = selectedTeam;
39
+ }
40
+ const projects = await getVercelProjects(teamId);
41
+ if (projects.length === 0) {
42
+ p.log.error("No Vercel projects found.");
43
+ process.exit(1);
44
+ }
45
+ const selectedProject = await p.select({
46
+ message: "Select a Vercel project",
47
+ options: projects.map((proj) => ({ value: proj.id, label: proj.name })),
48
+ });
49
+ if (p.isCancel(selectedProject)) {
50
+ p.cancel("Cancelled.");
51
+ process.exit(0);
52
+ }
53
+ spinner.start("Linking to Vercel project...");
54
+ const ok = await vercelLink(cwd, selectedProject);
55
+ spinner.stop(ok ? "Linked to Vercel project" : "");
56
+ if (!ok) {
57
+ p.log.error("Vercel link failed.");
58
+ process.exit(1);
59
+ }
60
+ }
61
+ // Fetch deployments
62
+ spinner.start("Fetching Vercel deployments...");
63
+ const deployments = await getVercelDeployments(cwd, gitInfo.commitFull, gitInfo.branch);
64
+ spinner.stop("Fetched Vercel deployments");
65
+ if (deployments.length === 0) {
66
+ p.log.warn("No Vercel deployments found. Paste a URL instead.");
67
+ return null;
68
+ }
69
+ const selected = await p.select({
70
+ message: "Select a deployment",
71
+ options: deployments.map((d) => ({ value: d.url, label: d.label })),
72
+ });
73
+ if (p.isCancel(selected)) {
74
+ p.cancel("Cancelled.");
75
+ process.exit(0);
76
+ }
77
+ return selected;
78
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "inflight-cli",
3
- "version": "0.1.1",
4
- "description": "Inflight CLI create and share design versions from the terminal",
3
+ "version": "1.0.1",
4
+ "description": "Get feedback directly on your staging URL",
5
5
  "bin": {
6
6
  "inflight": "dist/index.js"
7
7
  },
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
- "license": "MIT",
13
+ "license": "UNLICENSED",
14
14
  "homepage": "https://inflight.co",
15
15
  "scripts": {
16
16
  "build": "tsc && chmod +x dist/index.js",