inflight-cli 1.0.4 → 1.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 CHANGED
@@ -1,43 +1,36 @@
1
1
  # inflight-cli
2
2
 
3
- The official CLI for [Inflight](https://inflight.co) — create and share design versions from the terminal.
3
+ The official CLI for [Inflight](https://inflight.co).
4
4
 
5
- ## Installation
5
+ ## Development
6
+
7
+ ### Run locally
6
8
 
7
9
  ```bash
8
- npm install -g inflight-cli
10
+ cd apps/cli
11
+ bun run src/index.ts <command>
9
12
  ```
10
13
 
11
- ## Usage
12
-
13
- ### Login
14
-
15
- Authenticate with your Inflight account:
14
+ ### Run against staging
16
15
 
17
16
  ```bash
18
- inflight login
17
+ INFLIGHT_API_URL=https://staging-api.inflight.co INFLIGHT_WEB_URL=https://staging.inflight.co bun run src/index.ts <command>
19
18
  ```
20
19
 
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
-
23
- ### Share a version
20
+ For the `preview` command, also set `INFLIGHT_SHARE_API_URL`.
24
21
 
25
- Share a new design version for the current project:
22
+ ### Build
26
23
 
27
24
  ```bash
28
- inflight share
25
+ bun run build
29
26
  ```
30
27
 
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.
39
-
40
- ## Requirements
28
+ ### Commands
41
29
 
42
- - Node.js 18+
43
- - A free [Inflight](https://inflight.co) account
30
+ | Command | Description |
31
+ | ----------- | ---------------------------------------- |
32
+ | `login` | Authenticate with your Inflight account |
33
+ | `logout` | Disconnect your Inflight account |
34
+ | `share` | Get feedback on your staging URL |
35
+ | `preview` | Preview a live component from your code |
36
+ | `workspace` | Switch the workspace for this project |
@@ -1,73 +1,68 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
- import { createServer } from "http";
4
3
  import { readGlobalAuth, writeGlobalAuth } from "../lib/config.js";
5
4
  import { apiGetMe } from "../lib/api.js";
6
- const DEFAULT_API_URL = process.env.INFLIGHT_API_URL ?? "https://api.inflight.co";
7
- const WEB_URL = process.env.INFLIGHT_WEB_URL ?? "https://inflight.co";
5
+ import { API_URL, WEB_URL } from "../lib/env.js";
6
+ const POLL_INTERVAL_MS = 2000;
7
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000;
8
8
  export async function loginCommand() {
9
9
  p.intro(pc.bgBlue(pc.white(" inflight login ")));
10
10
  const existingAuth = readGlobalAuth();
11
11
  if (existingAuth) {
12
12
  const spinner = p.spinner();
13
13
  spinner.start("Checking existing session...");
14
- const me = await apiGetMe(existingAuth.apiKey, existingAuth.apiUrl).catch(() => null);
15
- if (me) {
16
- spinner.stop(`Already logged in as ${pc.bold(me.name)}`);
14
+ const me = await apiGetMe(existingAuth.apiKey).catch(() => null);
15
+ if (me?.email) {
16
+ spinner.stop(`Already logged in as ${pc.bold(me.email)}`);
17
17
  p.outro(pc.green("✓ You're already logged in"));
18
18
  process.exit(0);
19
19
  }
20
20
  spinner.stop("Session expired — re-authenticating...");
21
21
  }
22
+ const sessionId = crypto.randomUUID();
23
+ const authUrl = `${WEB_URL}/cli/connect?session_id=${sessionId}`;
22
24
  p.log.info("Opening browser to authenticate with Inflight...");
23
- const apiKey = await browserAuth();
24
- const apiUrl = DEFAULT_API_URL;
25
+ p.log.info(`Opening ${pc.cyan(authUrl)}`);
26
+ const { default: open } = await import("open");
27
+ await open(authUrl);
25
28
  const spinner = p.spinner();
26
- spinner.start("Validating...");
27
- const me = await apiGetMe(apiKey, apiUrl).catch((e) => {
29
+ spinner.start("Waiting for browser authentication...");
30
+ const apiKey = await pollForApiKey(sessionId);
31
+ if (!apiKey) {
32
+ spinner.stop("Authentication timed out.");
33
+ p.log.error("No response after 5 minutes. Please try again.");
34
+ process.exit(1);
35
+ }
36
+ spinner.message("Validating...");
37
+ const me = await apiGetMe(apiKey).catch((e) => {
28
38
  spinner.stop("Validation failed.");
29
39
  p.log.error(e.message);
30
40
  process.exit(1);
31
41
  });
32
- spinner.stop(`Authenticated as ${pc.bold(me.name)}`);
33
- writeGlobalAuth({ apiKey, apiUrl });
42
+ if (!me.email) {
43
+ spinner.stop("Validation failed.");
44
+ p.log.error("No email associated with this account. Please sign up at inflight.co first.");
45
+ process.exit(1);
46
+ }
47
+ spinner.stop(`Authenticated as ${pc.bold(me.email)}`);
48
+ writeGlobalAuth({ apiKey });
34
49
  p.outro(pc.green("✓ Logged in successfully"));
35
50
  process.exit(0);
36
51
  }
37
- function browserAuth() {
38
- return new Promise((resolve, reject) => {
39
- const server = createServer((req, res) => {
40
- const url = new URL(req.url ?? "/", "http://localhost");
41
- const key = url.searchParams.get("api_key");
42
- res.writeHead(200, { "Content-Type": "text/html" });
43
- res.end(`
44
- <html><body style="font-family:sans-serif;text-align:center;padding:60px">
45
- <h2>Authenticated!</h2>
46
- <p>You can close this tab and return to the terminal.</p>
47
- <script>window.close()</script>
48
- </body></html>
49
- `);
50
- server.close();
51
- if (key) {
52
- resolve(key);
52
+ async function pollForApiKey(sessionId) {
53
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
54
+ while (Date.now() < deadline) {
55
+ try {
56
+ const res = await fetch(`${API_URL}/api/cli/poll?session_id=${sessionId}`);
57
+ if (res.ok) {
58
+ const { api_key } = (await res.json());
59
+ return api_key;
53
60
  }
54
- else {
55
- reject(new Error("No API key received from browser"));
56
- }
57
- });
58
- server.listen(0, "127.0.0.1").unref();
59
- server.once("listening", async () => {
60
- const port = server.address().port;
61
- const callbackUrl = `http://127.0.0.1:${port}`;
62
- const authUrl = `${WEB_URL}/cli/connect?callback=${encodeURIComponent(callbackUrl)}`;
63
- p.log.info(`Opening ${pc.cyan(authUrl)}`);
64
- const { default: open } = await import("open");
65
- await open(authUrl);
66
- });
67
- const timeoutId = setTimeout(() => {
68
- server.close();
69
- reject(new Error("Authentication timed out after 5 minutes"));
70
- }, 5 * 60 * 1000);
71
- server.once("close", () => clearTimeout(timeoutId));
72
- });
61
+ }
62
+ catch {
63
+ // Network error — keep polling
64
+ }
65
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
66
+ }
67
+ return null;
73
68
  }
@@ -0,0 +1 @@
1
+ export declare function logoutCommand(): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { readGlobalAuth, clearGlobalAuth } from "../lib/config.js";
4
+ export async function logoutCommand() {
5
+ p.intro(pc.bgBlue(pc.white(" inflight logout ")));
6
+ const auth = readGlobalAuth();
7
+ if (!auth) {
8
+ p.outro(pc.yellow("You're not logged in"));
9
+ process.exit(0);
10
+ }
11
+ clearGlobalAuth();
12
+ p.outro(pc.green("✓ Logged out successfully"));
13
+ process.exit(0);
14
+ }
@@ -0,0 +1,7 @@
1
+ interface ShareOptions {
2
+ message?: string;
3
+ scope?: string;
4
+ open?: boolean;
5
+ }
6
+ export declare function previewCommand(opts: ShareOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,256 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
+ import { isGitRepo, getGitDiffResult, getDefaultBranch, getRemoteUrl, parseDiffStat } from "../lib/git.js";
5
+ import { readProjectFiles, calculateTotalSize, getFileCount, formatSize, needsChunkedUpload } from "../lib/files.js";
6
+ import { shareProject, shareChunked, lookupExistingProject, shareApiHealthCheck } from "../lib/share-api.js";
7
+ import { createProgressHandler } from "../lib/progress.js";
8
+ import { apiGetMe } from "../lib/api.js";
9
+ export async function previewCommand(opts) {
10
+ const cwd = process.cwd();
11
+ p.intro(pc.bgMagenta(pc.white(" inflight preview ")));
12
+ // ── Step 1: Auth ──
13
+ const auth = readGlobalAuth();
14
+ if (!auth) {
15
+ p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
16
+ process.exit(1);
17
+ }
18
+ // ── Step 2: Health check ──
19
+ const spinner = p.spinner();
20
+ spinner.start("Connecting to Inflight...");
21
+ const healthy = await shareApiHealthCheck();
22
+ if (!healthy) {
23
+ spinner.stop("Connection failed.");
24
+ p.log.error("Couldn't reach Inflight servers. Check your internet connection and try again.");
25
+ process.exit(1);
26
+ }
27
+ spinner.stop("Connected to Inflight.");
28
+ // ── Step 3: Git check ──
29
+ if (!isGitRepo(cwd)) {
30
+ p.log.error("This folder isn't a git repo. Make sure you're in the right project directory.");
31
+ process.exit(1);
32
+ }
33
+ // ── Step 4: Workspace ──
34
+ let workspaceId = readWorkspaceConfig(cwd)?.workspaceId;
35
+ if (!workspaceId) {
36
+ spinner.start("Loading workspaces...");
37
+ const me = await apiGetMe(auth.apiKey).catch((e) => {
38
+ spinner.stop("Failed.");
39
+ p.log.error(e.message);
40
+ process.exit(1);
41
+ });
42
+ spinner.stop("");
43
+ const workspaces = me.workspaces;
44
+ if (workspaces.length === 0) {
45
+ p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
46
+ process.exit(1);
47
+ }
48
+ else if (workspaces.length === 1) {
49
+ workspaceId = workspaces[0].id;
50
+ p.log.info(`Workspace: ${pc.bold(workspaces[0].name)}`);
51
+ }
52
+ else {
53
+ const selected = await p.select({
54
+ message: "Select a workspace",
55
+ options: workspaces.map((w) => ({ value: w.id, label: w.name })),
56
+ });
57
+ if (p.isCancel(selected)) {
58
+ p.cancel("Cancelled.");
59
+ process.exit(0);
60
+ }
61
+ workspaceId = selected;
62
+ }
63
+ writeWorkspaceConfig(cwd, { workspaceId });
64
+ }
65
+ // ── Step 5: Show diff summary and select scope ──
66
+ const baseBranch = getDefaultBranch(cwd);
67
+ const branchDiff = getGitDiffResult(cwd, { mode: "branch" });
68
+ if (branchDiff) {
69
+ const fileChanges = parseDiffStat(branchDiff.diffStat);
70
+ const fileCount = fileChanges.length;
71
+ p.log.info(`Found ${pc.bold(String(fileCount))} changed file${fileCount !== 1 ? "s" : ""} on ${pc.cyan(branchDiff.currentBranch)} (vs ${baseBranch}):`);
72
+ for (const { file, insertions, deletions } of fileChanges.slice(0, 15)) {
73
+ const parts = [];
74
+ if (insertions > 0)
75
+ parts.push(pc.green(`+${insertions}`));
76
+ if (deletions > 0)
77
+ parts.push(pc.red(`-${deletions}`));
78
+ p.log.message(` ${file} ${parts.join(", ")}`);
79
+ }
80
+ if (fileChanges.length > 15) {
81
+ p.log.message(pc.dim(` ... and ${fileChanges.length - 15} more`));
82
+ }
83
+ }
84
+ else {
85
+ p.log.warning("No branch diff found against " + pc.cyan(baseBranch) + ". You may want to share uncommitted changes.");
86
+ }
87
+ // Scope selection
88
+ let scope;
89
+ const VALID_MODES = ["branch", "uncommitted", "staged", "commits", "files"];
90
+ if (opts.scope) {
91
+ if (!VALID_MODES.includes(opts.scope)) {
92
+ p.log.error(`Invalid scope "${opts.scope}". Valid options: ${VALID_MODES.join(", ")}`);
93
+ process.exit(1);
94
+ }
95
+ scope = { mode: opts.scope };
96
+ }
97
+ else {
98
+ const scopeChoice = await p.select({
99
+ message: "What would you like to share?",
100
+ options: [
101
+ { value: "branch", label: `All branch changes${branchDiff ? ` (${parseDiffStat(branchDiff.diffStat).length} files)` : ""}` },
102
+ { value: "uncommitted", label: "Only uncommitted changes" },
103
+ { value: "staged", label: "Only staged changes" },
104
+ { value: "commits", label: "Last N commits" },
105
+ { value: "files", label: "Pick specific files" },
106
+ ],
107
+ });
108
+ if (p.isCancel(scopeChoice)) {
109
+ p.cancel("Cancelled.");
110
+ process.exit(0);
111
+ }
112
+ scope = { mode: scopeChoice };
113
+ // Follow-up prompts for commits and files modes
114
+ if (scope.mode === "commits") {
115
+ const count = await p.text({
116
+ message: "How many commits?",
117
+ placeholder: "1",
118
+ validate: (v) => {
119
+ const n = parseInt(v, 10);
120
+ if (isNaN(n) || n < 1)
121
+ return "Enter a positive number";
122
+ },
123
+ });
124
+ if (p.isCancel(count)) {
125
+ p.cancel("Cancelled.");
126
+ process.exit(0);
127
+ }
128
+ scope.commitCount = parseInt(count, 10);
129
+ }
130
+ if (scope.mode === "files" && branchDiff) {
131
+ const fileChanges = parseDiffStat(branchDiff.diffStat);
132
+ if (fileChanges.length === 0) {
133
+ p.log.warning("No changed files found. Falling back to full branch diff.");
134
+ scope = { mode: "branch" };
135
+ }
136
+ else {
137
+ const selectedFiles = await p.multiselect({
138
+ message: "Select files to share",
139
+ options: fileChanges.map(({ file, insertions, deletions }) => {
140
+ const parts = [];
141
+ if (insertions > 0)
142
+ parts.push(pc.green(`+${insertions}`));
143
+ if (deletions > 0)
144
+ parts.push(pc.red(`-${deletions}`));
145
+ return { value: file, label: `${file} ${parts.join(", ")}` };
146
+ }),
147
+ required: true,
148
+ });
149
+ if (p.isCancel(selectedFiles)) {
150
+ p.cancel("Cancelled.");
151
+ process.exit(0);
152
+ }
153
+ scope.paths = selectedFiles;
154
+ }
155
+ }
156
+ }
157
+ // Get the actual diff for the selected scope
158
+ const diffResult = getGitDiffResult(cwd, scope);
159
+ // ── Step 6: Existing project check ──
160
+ const remoteUrl = getRemoteUrl(cwd);
161
+ let existingProjectId;
162
+ if (remoteUrl) {
163
+ spinner.start("Checking for existing shares...");
164
+ const existing = await lookupExistingProject(auth.apiKey, remoteUrl, workspaceId);
165
+ spinner.stop("");
166
+ if (existing) {
167
+ const nextVersion = existing.versionCount + 1;
168
+ const choice = await p.select({
169
+ message: `This repo was shared before as "${pc.bold(existing.name)}" (${existing.versionCount} version${existing.versionCount !== 1 ? "s" : ""}).`,
170
+ options: [
171
+ { value: "add", label: `Add as V${nextVersion} to "${existing.name}"` },
172
+ { value: "new", label: "Create a new project" },
173
+ ],
174
+ });
175
+ if (p.isCancel(choice)) {
176
+ p.cancel("Cancelled.");
177
+ process.exit(0);
178
+ }
179
+ if (choice === "add") {
180
+ existingProjectId = existing.id;
181
+ }
182
+ }
183
+ }
184
+ // ── Step 7: User intent ──
185
+ let userIntent = opts.message;
186
+ if (!userIntent) {
187
+ const intentInput = await p.text({
188
+ message: "Describe what you're sharing (optional, press Enter to skip)",
189
+ placeholder: "e.g. the new header on the landing page",
190
+ });
191
+ if (p.isCancel(intentInput)) {
192
+ p.cancel("Cancelled.");
193
+ process.exit(0);
194
+ }
195
+ const trimmed = String(intentInput ?? "").trim();
196
+ if (trimmed)
197
+ userIntent = trimmed;
198
+ }
199
+ // ── Step 8: File collection ──
200
+ spinner.start("Reading project files...");
201
+ const files = readProjectFiles(cwd);
202
+ const fileCount = getFileCount(files);
203
+ const totalSize = calculateTotalSize(files);
204
+ spinner.stop(`Read ${pc.bold(String(fileCount))} files (${formatSize(totalSize)})`);
205
+ // ── Step 9: Upload + SSE progress ──
206
+ const toFriendlyMessage = createProgressHandler();
207
+ const shareSpinner = p.spinner();
208
+ shareSpinner.start("Uploading to Inflight...");
209
+ const request = {
210
+ files,
211
+ gitDiff: diffResult ?? {
212
+ diff: "",
213
+ diffStat: "",
214
+ baseBranch,
215
+ currentBranch: branchDiff?.currentBranch ?? "unknown",
216
+ },
217
+ workspaceId,
218
+ existingProjectId,
219
+ gitUrl: remoteUrl ?? undefined,
220
+ userIntent,
221
+ };
222
+ const onProgress = (percentage, step) => {
223
+ const friendly = toFriendlyMessage(percentage, step);
224
+ if (friendly) {
225
+ shareSpinner.message(friendly);
226
+ }
227
+ };
228
+ try {
229
+ const chunked = needsChunkedUpload(files);
230
+ const result = chunked
231
+ ? await shareChunked(request, auth.apiKey, onProgress)
232
+ : await shareProject(request, auth.apiKey, onProgress);
233
+ shareSpinner.stop(pc.green("Share complete!"));
234
+ // ── Step 10: Result ──
235
+ const lines = [
236
+ `${pc.bold("Preview:")} ${pc.cyan(result.previewUrl)}`,
237
+ `${pc.bold("Inflight:")} ${pc.cyan(result.inflightUrl)}`,
238
+ ];
239
+ p.note(lines.join("\n"), "Your share is live");
240
+ if (result.diffSummary?.summary) {
241
+ p.log.info(pc.dim(result.diffSummary.summary));
242
+ }
243
+ p.outro(pc.green("Done!") + " — opening in browser...");
244
+ if (opts.open !== false) {
245
+ const { default: open } = await import("open");
246
+ await open(result.inflightUrl);
247
+ }
248
+ }
249
+ catch (e) {
250
+ const message = e instanceof Error ? e.message : String(e);
251
+ shareSpinner.stop("Failed.");
252
+ p.log.error(`Something went wrong: ${message}`);
253
+ process.exit(1);
254
+ }
255
+ process.exit(0);
256
+ }
@@ -1,7 +1,7 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
- import { getGitInfo, isGitRepo } from "../lib/git.js";
4
+ import { getGitInfo } from "../lib/git.js";
5
5
  import { providers } from "../providers/index.js";
6
6
  import { apiGetMe, apiCreateVersion } from "../lib/api.js";
7
7
  export async function shareCommand() {
@@ -15,14 +15,10 @@ export async function shareCommand() {
15
15
  // Resolve workspace — read from .inflight/workspace.json, prompt if not linked
16
16
  let workspaceId = readWorkspaceConfig(cwd)?.workspaceId;
17
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.");
18
+ const me = await apiGetMe(auth.apiKey).catch((e) => {
22
19
  p.log.error(e.message);
23
20
  process.exit(1);
24
21
  });
25
- spinner.stop("");
26
22
  if (me.workspaces.length === 0) {
27
23
  p.log.error("No workspaces found. Create one at inflight.co first.");
28
24
  process.exit(1);
@@ -44,10 +40,7 @@ export async function shareCommand() {
44
40
  }
45
41
  writeWorkspaceConfig(cwd, { workspaceId });
46
42
  }
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 };
43
+ const gitInfo = getGitInfo(cwd);
51
44
  // Staging URL — user picks provider
52
45
  const providerChoice = await p.select({
53
46
  message: "Where is your staging URL hosted?",
@@ -60,25 +53,24 @@ export async function shareCommand() {
60
53
  p.cancel("Cancelled.");
61
54
  process.exit(0);
62
55
  }
63
- const providerSpinner = p.spinner();
64
56
  const provider = providers.find((prov) => prov.id === providerChoice);
65
57
  let stagingUrl;
66
58
  if (provider) {
67
- stagingUrl = (await provider.resolve(cwd, gitInfo, providerSpinner)) ?? undefined;
59
+ stagingUrl = (await provider.resolve(cwd, gitInfo)) ?? undefined;
68
60
  }
69
61
  // Manual input (or fallback if provider returned no URL)
70
62
  if (!stagingUrl) {
71
63
  const input = await p.text({
72
64
  message: "Staging URL",
73
- placeholder: "https://my-branch.vercel.app",
65
+ placeholder: "my-branch.vercel.app",
74
66
  validate: (v) => {
75
67
  if (!v)
76
68
  return "Staging URL is required";
77
69
  try {
78
- new URL(v);
70
+ new URL(v.startsWith("http") ? v : `https://${v}`);
79
71
  }
80
72
  catch {
81
- return "Must be a valid URL (include https://)";
73
+ return "Must be a valid URL";
82
74
  }
83
75
  },
84
76
  });
@@ -86,11 +78,14 @@ export async function shareCommand() {
86
78
  p.cancel("Cancelled.");
87
79
  process.exit(0);
88
80
  }
89
- stagingUrl = input;
81
+ stagingUrl = input.trim();
82
+ }
83
+ // Ensure protocol for any source (Vercel returns bare hostnames)
84
+ if (!stagingUrl.startsWith("http")) {
85
+ stagingUrl = `https://${stagingUrl}`;
90
86
  }
91
87
  const result = await apiCreateVersion({
92
88
  apiKey: auth.apiKey,
93
- apiUrl: auth.apiUrl,
94
89
  workspaceId,
95
90
  stagingUrl,
96
91
  gitInfo,
@@ -98,8 +93,8 @@ export async function shareCommand() {
98
93
  p.log.error(e.message);
99
94
  process.exit(1);
100
95
  });
101
- p.note(result.inflightUrl, "Your Inflight version");
102
- p.outro(pc.green("✓ Done") + " — opening staging URL...");
96
+ p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
97
+ p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
103
98
  const { default: open } = await import("open");
104
99
  await open(stagingUrl);
105
100
  process.exit(0);
@@ -0,0 +1 @@
1
+ export declare function workspaceCommand(): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
4
+ import { apiGetMe } from "../lib/api.js";
5
+ export async function workspaceCommand() {
6
+ const cwd = process.cwd();
7
+ p.intro(pc.bgCyan(pc.white(" inflight workspace ")));
8
+ const auth = readGlobalAuth();
9
+ if (!auth) {
10
+ p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
11
+ process.exit(1);
12
+ }
13
+ const current = readWorkspaceConfig(cwd);
14
+ const spinner = p.spinner();
15
+ spinner.start("Loading workspaces...");
16
+ const me = await apiGetMe(auth.apiKey).catch((e) => {
17
+ spinner.stop("Failed.");
18
+ p.log.error(e.message);
19
+ process.exit(1);
20
+ });
21
+ spinner.stop("");
22
+ const workspaces = me.workspaces;
23
+ if (workspaces.length === 0) {
24
+ p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
25
+ process.exit(1);
26
+ }
27
+ // Show current workspace if set
28
+ if (current) {
29
+ const currentWs = workspaces.find((w) => w.id === current.workspaceId);
30
+ if (currentWs) {
31
+ p.log.info(`Current workspace: ${pc.bold(currentWs.name)}`);
32
+ }
33
+ }
34
+ const selected = await p.select({
35
+ message: "Select a workspace",
36
+ options: workspaces.map((w) => ({
37
+ value: w.id,
38
+ label: w.name,
39
+ hint: current?.workspaceId === w.id ? "current" : undefined,
40
+ })),
41
+ });
42
+ if (p.isCancel(selected)) {
43
+ p.cancel("Cancelled.");
44
+ process.exit(0);
45
+ }
46
+ const workspaceId = selected;
47
+ writeWorkspaceConfig(cwd, { workspaceId });
48
+ const name = workspaces.find((w) => w.id === workspaceId)?.name ?? workspaceId;
49
+ p.outro(pc.green(`Workspace set to ${pc.bold(name)}`));
50
+ process.exit(0);
51
+ }
package/dist/index.js CHANGED
@@ -2,19 +2,21 @@
2
2
  import { Command } from "commander";
3
3
  import { loginCommand } from "./commands/login.js";
4
4
  import { shareCommand } from "./commands/share.js";
5
+ import { workspaceCommand } from "./commands/workspace.js";
6
+ import { logoutCommand } from "./commands/logout.js";
5
7
  import pkg from "../package.json" with { type: "json" };
6
8
  const { version } = pkg;
7
9
  const program = new Command();
8
- program
9
- .name("inflight")
10
- .description("Get feedback directly on your staging URL")
11
- .version(version);
12
- program
13
- .command("login")
14
- .description("Authenticate with your Inflight account")
15
- .action(loginCommand);
16
- program
17
- .command("share")
18
- .description("Share a new version for this project")
19
- .action(() => shareCommand());
10
+ program.name("inflight").description("Get feedback directly on your staging URL").version(version);
11
+ program.command("login").description("Authenticate with your Inflight account").action(loginCommand);
12
+ program.command("share").description("Get feedback on your staging URL").action(shareCommand);
13
+ // program
14
+ // .command("preview")
15
+ // .description("Preview a live component from your code")
16
+ // .option("-m, --message <message>", "Pre-fill the intent prompt")
17
+ // .option("--scope <mode>", "Skip scope prompt: branch, uncommitted, staged")
18
+ // .option("--no-open", "Don't open result in browser")
19
+ // .action((opts) => previewCommand(opts));
20
+ program.command("workspace").description("Switch the active inflight workspace").action(workspaceCommand);
21
+ program.command("logout").description("Disconnect your Inflight account").action(logoutCommand);
20
22
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -8,13 +8,13 @@ export interface CreateVersionResult {
8
8
  versionId: string;
9
9
  inflightUrl: string;
10
10
  }
11
- export declare function apiGetMe(apiKey: string, apiUrl: string): Promise<{
12
- name: string;
11
+ export declare function apiGetMe(apiKey: string): Promise<{
12
+ name: string | null;
13
+ email: string | null;
13
14
  workspaces: Workspace[];
14
15
  }>;
15
16
  export declare function apiCreateVersion(opts: {
16
17
  apiKey: string;
17
- apiUrl: string;
18
18
  workspaceId: string;
19
19
  stagingUrl: string;
20
20
  gitInfo: GitInfo;