inflight-cli 1.0.4 → 1.1.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 +35 -18
- package/dist/commands/login.js +42 -47
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +14 -0
- package/dist/commands/preview.d.ts +7 -0
- package/dist/commands/preview.js +256 -0
- package/dist/commands/share.js +14 -19
- package/dist/commands/workspace.d.ts +1 -0
- package/dist/commands/workspace.js +51 -0
- package/dist/index.js +14 -12
- package/dist/lib/api.d.ts +3 -3
- package/dist/lib/api.js +4 -3
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +7 -4
- package/dist/lib/env.d.ts +2 -0
- package/dist/lib/env.js +2 -0
- package/dist/lib/files.d.ts +29 -0
- package/dist/lib/files.js +187 -0
- package/dist/lib/git.d.ts +25 -0
- package/dist/lib/git.js +158 -13
- package/dist/lib/progress.d.ts +18 -0
- package/dist/lib/progress.js +106 -0
- package/dist/lib/share-api.d.ts +48 -0
- package/dist/lib/share-api.js +186 -0
- package/dist/lib/vercel.d.ts +14 -7
- package/dist/lib/vercel.js +92 -105
- package/dist/providers/index.d.ts +1 -3
- package/dist/providers/vercel.d.ts +1 -2
- package/dist/providers/vercel.js +61 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# inflight-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Get feedback directly on your staging URL with [Inflight](https://inflight.co).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,34 +8,51 @@ The official CLI for [Inflight](https://inflight.co) — create and share design
|
|
|
8
8
|
npm install -g inflight-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### Login
|
|
14
|
-
|
|
15
|
-
Authenticate with your Inflight account:
|
|
11
|
+
## Quick Start
|
|
16
12
|
|
|
17
13
|
```bash
|
|
14
|
+
# Log in to your Inflight account
|
|
18
15
|
inflight login
|
|
16
|
+
|
|
17
|
+
# Add Inflight to your staging URL
|
|
18
|
+
inflight share
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
That's it. The CLI will detect your Vercel deployments automatically and open your staging URL with Inflight attached.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## Commands
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### `inflight login`
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Authenticate with your Inflight account. Opens a browser window to sign in.
|
|
28
|
+
|
|
29
|
+
### `inflight logout`
|
|
30
|
+
|
|
31
|
+
Disconnect your Inflight account from this machine.
|
|
32
|
+
|
|
33
|
+
### `inflight share`
|
|
34
|
+
|
|
35
|
+
Add Inflight to a staging URL. The CLI will:
|
|
36
|
+
|
|
37
|
+
1. Detect your Vercel project and show available deployments
|
|
38
|
+
2. Let you pick a branch preview or a specific deployment
|
|
39
|
+
3. Open the staging URL with Inflight attached
|
|
40
|
+
|
|
41
|
+
If you're not using Vercel, you can paste any staging URL manually.
|
|
42
|
+
|
|
43
|
+
### `inflight workspace`
|
|
44
|
+
|
|
45
|
+
Switch the active workspace for the current project directory.
|
|
46
|
+
|
|
47
|
+
## Vercel Integration
|
|
30
48
|
|
|
31
|
-
|
|
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
|
|
49
|
+
The CLI integrates with Vercel to automatically detect your deployments. On first run, it will:
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
1. Check if you're logged in to Vercel (reads your existing Vercel CLI auth)
|
|
52
|
+
2. Link to your Vercel project if not already linked
|
|
53
|
+
3. Show your branch preview URL and recent deployments
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
No extra configuration needed if you already use the Vercel CLI.
|
|
39
56
|
|
|
40
57
|
## Requirements
|
|
41
58
|
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
const
|
|
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
|
|
15
|
-
if (me) {
|
|
16
|
-
spinner.stop(`Already logged in as ${pc.bold(me.
|
|
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
|
-
|
|
24
|
-
const
|
|
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("
|
|
27
|
-
const
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
res.
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,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
|
+
}
|
package/dist/commands/share.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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.
|
|
102
|
-
p.outro(pc.green("✓
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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();
|