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 +19 -26
- 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,43 +1,36 @@
|
|
|
1
1
|
# inflight-cli
|
|
2
2
|
|
|
3
|
-
The official CLI for [Inflight](https://inflight.co)
|
|
3
|
+
The official CLI for [Inflight](https://inflight.co).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
### Run locally
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
10
|
+
cd apps/cli
|
|
11
|
+
bun run src/index.ts <command>
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### Login
|
|
14
|
-
|
|
15
|
-
Authenticate with your Inflight account:
|
|
14
|
+
### Run against staging
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
|
-
inflight
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
### Share a version
|
|
20
|
+
For the `preview` command, also set `INFLIGHT_SHARE_API_URL`.
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
### Build
|
|
26
23
|
|
|
27
24
|
```bash
|
|
28
|
-
|
|
25
|
+
bun run build
|
|
29
26
|
```
|
|
30
27
|
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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 |
|
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();
|
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
|
|
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;
|