inflight-cli 1.1.5 → 2.0.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/dist/commands/login.js +10 -9
- package/dist/commands/logout.js +0 -1
- package/dist/commands/preview.js +2 -3
- package/dist/commands/reset.d.ts +1 -0
- package/dist/commands/reset.js +17 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +168 -0
- package/dist/commands/share.d.ts +6 -1
- package/dist/commands/share.js +44 -13
- package/dist/commands/vercel.d.ts +2 -0
- package/dist/commands/vercel.js +169 -0
- package/dist/commands/workspace.js +2 -4
- package/dist/commands/workspaces.d.ts +4 -0
- package/dist/commands/workspaces.js +81 -0
- package/dist/index.js +20 -3
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +17 -0
- package/dist/lib/config.d.ts +12 -2
- package/dist/lib/config.js +29 -19
- package/dist/lib/framework.d.ts +17 -0
- package/dist/lib/framework.js +154 -0
- package/dist/lib/skill.d.ts +5 -0
- package/dist/lib/skill.js +22 -0
- package/dist/lib/vercel.d.ts +21 -11
- package/dist/lib/vercel.js +107 -83
- package/dist/providers/vercel.d.ts +6 -0
- package/dist/providers/vercel.js +67 -55
- package/package.json +3 -2
package/dist/commands/login.js
CHANGED
|
@@ -6,16 +6,14 @@ import { API_URL, WEB_URL } from "../lib/env.js";
|
|
|
6
6
|
const POLL_INTERVAL_MS = 2000;
|
|
7
7
|
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
8
|
export async function loginCommand() {
|
|
9
|
-
p.intro(pc.bgBlue(pc.white(" inflight login ")));
|
|
10
9
|
const existingAuth = readGlobalAuth();
|
|
11
10
|
if (existingAuth) {
|
|
12
11
|
const spinner = p.spinner();
|
|
13
12
|
spinner.start("Checking existing session...");
|
|
14
13
|
const me = await apiGetMe(existingAuth.apiKey).catch(() => null);
|
|
15
14
|
if (me?.email) {
|
|
16
|
-
spinner.stop(
|
|
17
|
-
|
|
18
|
-
process.exit(0);
|
|
15
|
+
spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
|
|
16
|
+
return;
|
|
19
17
|
}
|
|
20
18
|
spinner.stop("Session expired — re-authenticating...");
|
|
21
19
|
}
|
|
@@ -44,10 +42,8 @@ export async function loginCommand() {
|
|
|
44
42
|
p.log.error("No email associated with this account. Please sign up at inflight.co first.");
|
|
45
43
|
process.exit(1);
|
|
46
44
|
}
|
|
47
|
-
spinner.stop(`Authenticated as ${pc.bold(me.email)}`);
|
|
48
45
|
writeGlobalAuth({ apiKey });
|
|
49
|
-
|
|
50
|
-
process.exit(0);
|
|
46
|
+
spinner.stop(pc.green(`✓ Logged in as ${pc.bold(me.email)}`));
|
|
51
47
|
}
|
|
52
48
|
async function pollForApiKey(sessionId) {
|
|
53
49
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
@@ -56,11 +52,16 @@ async function pollForApiKey(sessionId) {
|
|
|
56
52
|
const res = await fetch(`${API_URL}/api/cli/poll?session_id=${sessionId}`);
|
|
57
53
|
if (res.ok) {
|
|
58
54
|
const { api_key } = (await res.json());
|
|
59
|
-
|
|
55
|
+
if (api_key)
|
|
56
|
+
return api_key;
|
|
60
57
|
}
|
|
58
|
+
// 404 = not ready yet, keep polling
|
|
59
|
+
// 500+ = server error, stop
|
|
60
|
+
if (res.status >= 500)
|
|
61
|
+
return null;
|
|
61
62
|
}
|
|
62
63
|
catch {
|
|
63
|
-
// Network error — keep polling
|
|
64
|
+
// Network error (offline, DNS, etc.) — keep polling
|
|
64
65
|
}
|
|
65
66
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
66
67
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -2,7 +2,6 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { readGlobalAuth, clearGlobalAuth } from "../lib/config.js";
|
|
4
4
|
export async function logoutCommand() {
|
|
5
|
-
p.intro(pc.bgBlue(pc.white(" inflight logout ")));
|
|
6
5
|
const auth = readGlobalAuth();
|
|
7
6
|
if (!auth) {
|
|
8
7
|
p.outro(pc.yellow("You're not logged in"));
|
package/dist/commands/preview.js
CHANGED
|
@@ -8,7 +8,6 @@ import { createProgressHandler } from "../lib/progress.js";
|
|
|
8
8
|
import { apiGetMe } from "../lib/api.js";
|
|
9
9
|
export async function previewCommand(opts) {
|
|
10
10
|
const cwd = process.cwd();
|
|
11
|
-
p.intro(pc.bgMagenta(pc.white(" inflight preview ")));
|
|
12
11
|
// ── Step 1: Auth ──
|
|
13
12
|
const auth = readGlobalAuth();
|
|
14
13
|
if (!auth) {
|
|
@@ -31,7 +30,7 @@ export async function previewCommand(opts) {
|
|
|
31
30
|
process.exit(1);
|
|
32
31
|
}
|
|
33
32
|
// ── Step 4: Workspace ──
|
|
34
|
-
let workspaceId = readWorkspaceConfig(
|
|
33
|
+
let workspaceId = readWorkspaceConfig()?.workspaceId;
|
|
35
34
|
if (!workspaceId) {
|
|
36
35
|
spinner.start("Loading workspaces...");
|
|
37
36
|
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
@@ -60,7 +59,7 @@ export async function previewCommand(opts) {
|
|
|
60
59
|
}
|
|
61
60
|
workspaceId = selected;
|
|
62
61
|
}
|
|
63
|
-
writeWorkspaceConfig(
|
|
62
|
+
writeWorkspaceConfig({ workspaceId });
|
|
64
63
|
}
|
|
65
64
|
// ── Step 5: Show diff summary and select scope ──
|
|
66
65
|
const baseBranch = getDefaultBranch(cwd);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resetCommand(): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { clearGlobalAuth, clearWorkspaceConfig, clearVercelConfig } from "../lib/config.js";
|
|
4
|
+
export async function resetCommand() {
|
|
5
|
+
const confirm = await p.confirm({
|
|
6
|
+
message: "This will clear all Inflight auth and workspace config. Continue?",
|
|
7
|
+
initialValue: false,
|
|
8
|
+
});
|
|
9
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
10
|
+
p.cancel("Cancelled.");
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
clearGlobalAuth();
|
|
14
|
+
clearWorkspaceConfig();
|
|
15
|
+
clearVercelConfig();
|
|
16
|
+
p.log.success(pc.green("All Inflight config cleared."));
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setupCommand(): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { execSync, exec } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
import { readGlobalAuth, writeWorkspaceConfig } from "../lib/config.js";
|
|
7
|
+
import { apiGetMe, apiDetectWidgetLocation } from "../lib/api.js";
|
|
8
|
+
import { loginCommand } from "./login.js";
|
|
9
|
+
import { shareCommand } from "./share.js";
|
|
10
|
+
import { gatherProjectContext, hasInflightWidget, insertWidgetScript } from "../lib/framework.js";
|
|
11
|
+
import { isGitRepo } from "../lib/git.js";
|
|
12
|
+
import { installSkill } from "../lib/skill.js";
|
|
13
|
+
async function ensureGlobalInstall() {
|
|
14
|
+
try {
|
|
15
|
+
await execAsync("inflight --version");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
try {
|
|
19
|
+
await execAsync("npm install -g inflight-cli");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Non-fatal — they can still use npx
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function setupCommand() {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
// ── Step 1: Install CLI globally + agent skill ──
|
|
29
|
+
p.log.step("Installing Inflight CLI and agent skill...");
|
|
30
|
+
await Promise.all([ensureGlobalInstall(), installSkill()]);
|
|
31
|
+
// ── Step 2: Authenticate ──
|
|
32
|
+
let auth = readGlobalAuth();
|
|
33
|
+
if (!auth) {
|
|
34
|
+
await loginCommand();
|
|
35
|
+
auth = readGlobalAuth();
|
|
36
|
+
if (!auth) {
|
|
37
|
+
p.log.error("Login failed.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const me = await apiGetMe(auth.apiKey).catch(() => null);
|
|
43
|
+
if (me?.email) {
|
|
44
|
+
p.log.success(`Logged in as ${pc.bold(me.email)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ── Step 3: Resolve workspace ──
|
|
48
|
+
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
49
|
+
p.log.error(e.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
const workspaces = me.workspaces;
|
|
53
|
+
let workspaceId;
|
|
54
|
+
if (workspaces.length === 0) {
|
|
55
|
+
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
else if (workspaces.length === 1) {
|
|
59
|
+
workspaceId = workspaces[0].id;
|
|
60
|
+
p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const selected = await p.select({
|
|
64
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
|
|
65
|
+
options: workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(selected)) {
|
|
68
|
+
p.cancel("Cancelled.");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
workspaceId = selected;
|
|
72
|
+
}
|
|
73
|
+
writeWorkspaceConfig({ workspaceId });
|
|
74
|
+
const widgetId = workspaces.find((w) => w.id === workspaceId)?.widgetId;
|
|
75
|
+
if (!widgetId) {
|
|
76
|
+
p.log.error("Could not find widget ID for this workspace.");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
// ── Step 4: Add widget script tag ──
|
|
80
|
+
if (hasInflightWidget(cwd)) {
|
|
81
|
+
// Already present — move on silently
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const spinner = p.spinner();
|
|
85
|
+
spinner.start("Detecting framework...");
|
|
86
|
+
const context = gatherProjectContext(cwd);
|
|
87
|
+
const location = await apiDetectWidgetLocation({
|
|
88
|
+
apiKey: auth.apiKey,
|
|
89
|
+
fileTree: context.fileTree,
|
|
90
|
+
fileContents: context.fileContents,
|
|
91
|
+
});
|
|
92
|
+
let inserted = false;
|
|
93
|
+
if (location.file && location.insertAfter && location.confidence === "high") {
|
|
94
|
+
const result = insertWidgetScript(cwd, location.file, location.insertAfter, widgetId);
|
|
95
|
+
if (result) {
|
|
96
|
+
spinner.stop(`Detected ${pc.bold(location.framework ?? "framework")} — widget script tag added to ${pc.cyan(location.file)}`);
|
|
97
|
+
inserted = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
spinner.stop("Could not auto-detect where to add the widget.");
|
|
102
|
+
}
|
|
103
|
+
if (!inserted) {
|
|
104
|
+
p.log.message(`Add this snippet to your root HTML layout, just before ${pc.cyan("</body>")}:\n\n` +
|
|
105
|
+
pc.dim(` <script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`));
|
|
106
|
+
await p.text({
|
|
107
|
+
message: "Press enter when you've added it",
|
|
108
|
+
defaultValue: "",
|
|
109
|
+
placeholder: "",
|
|
110
|
+
});
|
|
111
|
+
if (!hasInflightWidget(cwd)) {
|
|
112
|
+
const skip = await p.confirm({
|
|
113
|
+
message: "Widget script not detected. Continue anyway?",
|
|
114
|
+
initialValue: false,
|
|
115
|
+
});
|
|
116
|
+
if (p.isCancel(skip) || !skip) {
|
|
117
|
+
p.cancel("Add the widget script and run setup again.");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── Step 5: Commit and push (only files containing the widget script) ──
|
|
124
|
+
if (isGitRepo(cwd)) {
|
|
125
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { cwd, encoding: "utf-8" }).trim();
|
|
126
|
+
let widgetFiles;
|
|
127
|
+
try {
|
|
128
|
+
const grepResult = execSync('git diff --name-only | xargs grep -l "inflight.co/widget.js" 2>/dev/null', {
|
|
129
|
+
cwd: gitRoot,
|
|
130
|
+
encoding: "utf-8",
|
|
131
|
+
}).trim();
|
|
132
|
+
widgetFiles = grepResult ? grepResult.split("\n") : [];
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
widgetFiles = [];
|
|
136
|
+
}
|
|
137
|
+
if (widgetFiles.length > 0) {
|
|
138
|
+
const fileList = widgetFiles.map((f) => pc.cyan(f)).join(", ");
|
|
139
|
+
const shouldCommit = await p.confirm({ message: `Commit and push ${fileList}?`, initialValue: true });
|
|
140
|
+
if (!p.isCancel(shouldCommit) && shouldCommit) {
|
|
141
|
+
try {
|
|
142
|
+
for (const file of widgetFiles) {
|
|
143
|
+
execSync(`git add "${file}"`, { cwd: gitRoot, stdio: "pipe" });
|
|
144
|
+
}
|
|
145
|
+
execSync('git commit -m "Add Inflight feedback widget script tag"', {
|
|
146
|
+
cwd: gitRoot,
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
});
|
|
149
|
+
p.log.success("Changes committed.");
|
|
150
|
+
const spinner = p.spinner();
|
|
151
|
+
spinner.start("Pushing...");
|
|
152
|
+
execSync("git push", { cwd: gitRoot, stdio: "pipe" });
|
|
153
|
+
spinner.stop("Pushed. When sharing, pick a deployment that includes the script tag.");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
p.log.warn("Could not push. Push manually before sharing.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
p.log.info("Remember to push your changes. The widget won't show on deployments made before the script tag was added.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Done ──
|
|
165
|
+
p.log.success(pc.green("Setup complete!") + " Run " + pc.cyan("inflight share") + " anytime to share your staging URL.");
|
|
166
|
+
// ── Step 6: Share ──
|
|
167
|
+
await shareCommand({ workspace: workspaceId });
|
|
168
|
+
}
|
package/dist/commands/share.d.ts
CHANGED
package/dist/commands/share.js
CHANGED
|
@@ -4,23 +4,58 @@ import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../li
|
|
|
4
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
|
-
export async function shareCommand() {
|
|
7
|
+
export async function shareCommand(opts = {}) {
|
|
8
8
|
const cwd = process.cwd();
|
|
9
9
|
const auth = readGlobalAuth();
|
|
10
10
|
if (!auth) {
|
|
11
|
-
|
|
11
|
+
if (opts.json) {
|
|
12
|
+
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight login first." }));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
|
|
16
|
+
}
|
|
12
17
|
process.exit(1);
|
|
13
18
|
}
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
19
|
+
const gitInfo = getGitInfo(cwd);
|
|
20
|
+
// ── Fast path: all inputs provided (agent / scripting) ──
|
|
21
|
+
if (opts.url && opts.workspace) {
|
|
22
|
+
let stagingUrl = opts.url;
|
|
23
|
+
if (!stagingUrl.startsWith("http")) {
|
|
24
|
+
stagingUrl = `https://${stagingUrl}`;
|
|
25
|
+
}
|
|
26
|
+
const result = await apiCreateVersion({
|
|
27
|
+
apiKey: auth.apiKey,
|
|
28
|
+
workspaceId: opts.workspace,
|
|
29
|
+
stagingUrl,
|
|
30
|
+
gitInfo,
|
|
31
|
+
}).catch((e) => {
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify({ error: "create_failed", message: e.message }));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
p.log.error(e.message);
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
|
|
45
|
+
p.outro(pc.green("✓ Inflight added to your staging URL"));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// ── Interactive path: prompt for missing inputs ──
|
|
50
|
+
// Resolve workspace
|
|
51
|
+
let workspaceId = opts.workspace ?? readWorkspaceConfig()?.workspaceId;
|
|
17
52
|
if (!workspaceId) {
|
|
18
53
|
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
19
54
|
p.log.error(e.message);
|
|
20
55
|
process.exit(1);
|
|
21
56
|
});
|
|
22
57
|
if (me.workspaces.length === 0) {
|
|
23
|
-
p.log.error("No workspaces found. Create one at inflight.co first.");
|
|
58
|
+
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
24
59
|
process.exit(1);
|
|
25
60
|
}
|
|
26
61
|
else if (me.workspaces.length === 1) {
|
|
@@ -38,10 +73,9 @@ export async function shareCommand() {
|
|
|
38
73
|
}
|
|
39
74
|
workspaceId = selected;
|
|
40
75
|
}
|
|
41
|
-
writeWorkspaceConfig(
|
|
76
|
+
writeWorkspaceConfig({ workspaceId });
|
|
42
77
|
}
|
|
43
|
-
|
|
44
|
-
// Staging URL — user picks provider
|
|
78
|
+
// Resolve staging URL
|
|
45
79
|
const providerChoice = await p.select({
|
|
46
80
|
message: "Where is your staging URL hosted?",
|
|
47
81
|
options: [
|
|
@@ -53,12 +87,11 @@ export async function shareCommand() {
|
|
|
53
87
|
p.cancel("Cancelled.");
|
|
54
88
|
process.exit(0);
|
|
55
89
|
}
|
|
56
|
-
const provider = providers.find((prov) => prov.id === providerChoice);
|
|
57
90
|
let stagingUrl;
|
|
91
|
+
const provider = providers.find((prov) => prov.id === providerChoice);
|
|
58
92
|
if (provider) {
|
|
59
93
|
stagingUrl = (await provider.resolve(cwd, gitInfo)) ?? undefined;
|
|
60
94
|
}
|
|
61
|
-
// Manual input (or fallback if provider returned no URL)
|
|
62
95
|
if (!stagingUrl) {
|
|
63
96
|
const input = await p.text({
|
|
64
97
|
message: "Staging URL",
|
|
@@ -80,7 +113,6 @@ export async function shareCommand() {
|
|
|
80
113
|
}
|
|
81
114
|
stagingUrl = input.trim();
|
|
82
115
|
}
|
|
83
|
-
// Ensure protocol for any source (Vercel returns bare hostnames)
|
|
84
116
|
if (!stagingUrl.startsWith("http")) {
|
|
85
117
|
stagingUrl = `https://${stagingUrl}`;
|
|
86
118
|
}
|
|
@@ -97,5 +129,4 @@ export async function shareCommand() {
|
|
|
97
129
|
p.outro(pc.green("✓ Inflight added to your staging URL") + " — opening in browser...");
|
|
98
130
|
const { default: open } = await import("open");
|
|
99
131
|
await open(stagingUrl);
|
|
100
|
-
process.exit(0);
|
|
101
132
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { readVercelConfig, writeVercelConfig } from "../lib/config.js";
|
|
4
|
+
import { ensureVercelCli, ensureVercelAuth, getVercelToken, getVercelTeams, getVercelProjects, getRecentDeployments, getBranchAliasUrl, } from "../lib/vercel.js";
|
|
5
|
+
import { pickVercelProject } from "../providers/vercel.js";
|
|
6
|
+
// --- Action handlers ---
|
|
7
|
+
async function vercelSetup(opts) {
|
|
8
|
+
// Fast path: non-interactive set (for agents)
|
|
9
|
+
if (opts.team && opts.project) {
|
|
10
|
+
const token = requireVercelToken();
|
|
11
|
+
const teams = await getVercelTeams(token);
|
|
12
|
+
const team = teams.find((t) => t.id === opts.team);
|
|
13
|
+
if (!team) {
|
|
14
|
+
const msg = `Team '${opts.team}' not found. Available: ${teams.map((t) => `${t.name} (${t.id})`).join(", ")}`;
|
|
15
|
+
if (opts.json) {
|
|
16
|
+
console.log(JSON.stringify({ error: "team_not_found", message: msg }));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
p.log.error(msg);
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const projects = await getVercelProjects(token, opts.team);
|
|
24
|
+
const project = projects.find((proj) => proj.id === opts.project);
|
|
25
|
+
if (!project) {
|
|
26
|
+
const msg = `Project '${opts.project}' not found on team '${team.name}'.`;
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
console.log(JSON.stringify({ error: "project_not_found", message: msg }));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
p.log.error(msg);
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
writeVercelConfig({ teamId: team.id, teamName: team.name, projectId: project.id, projectName: project.name });
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
saved: true,
|
|
39
|
+
teamId: team.id,
|
|
40
|
+
teamName: team.name,
|
|
41
|
+
projectId: project.id,
|
|
42
|
+
projectName: project.name,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
p.log.success(`Saved! Using ${pc.bold(project.name)} on ${pc.bold(team.name)}.`);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Interactive path
|
|
51
|
+
const cliOk = await ensureVercelCli((msg) => p.log.step(msg));
|
|
52
|
+
if (!cliOk) {
|
|
53
|
+
p.log.error("Failed to install Vercel CLI. Install manually: " + pc.cyan("npm install -g vercel"));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
p.log.step("Checking Vercel authentication...");
|
|
57
|
+
const token = await ensureVercelAuth();
|
|
58
|
+
if (!token) {
|
|
59
|
+
p.log.error("Vercel login failed.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const current = readVercelConfig();
|
|
63
|
+
if (current) {
|
|
64
|
+
p.log.info(`Current: ${pc.bold(current.projectName)} on ${pc.bold(current.teamName)}`);
|
|
65
|
+
}
|
|
66
|
+
const config = await pickVercelProject(token);
|
|
67
|
+
if (!config)
|
|
68
|
+
process.exit(1);
|
|
69
|
+
p.log.success(`Saved! ${pc.cyan("inflight share")} will now use ${pc.bold(config.projectName)} on ${pc.bold(config.teamName)}.`);
|
|
70
|
+
}
|
|
71
|
+
async function listTeams() {
|
|
72
|
+
const token = requireVercelToken();
|
|
73
|
+
const teams = await getVercelTeams(token);
|
|
74
|
+
console.log(JSON.stringify(teams));
|
|
75
|
+
}
|
|
76
|
+
async function listProjects(opts) {
|
|
77
|
+
const token = requireVercelToken();
|
|
78
|
+
const projects = await getVercelProjects(token, opts.team);
|
|
79
|
+
console.log(JSON.stringify(projects));
|
|
80
|
+
}
|
|
81
|
+
async function listDeployments(opts) {
|
|
82
|
+
const token = requireVercelToken();
|
|
83
|
+
let teamId = opts.team;
|
|
84
|
+
let projectId = opts.project;
|
|
85
|
+
if (!teamId || !projectId) {
|
|
86
|
+
const config = readVercelConfig();
|
|
87
|
+
if (!config) {
|
|
88
|
+
console.log(JSON.stringify({
|
|
89
|
+
error: "vercel_not_configured",
|
|
90
|
+
message: "No Vercel project configured. Run 'inflight vercel --team=TEAM_ID --project=PROJECT_ID --json' first.",
|
|
91
|
+
}));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
teamId = teamId ?? config.teamId;
|
|
95
|
+
projectId = projectId ?? config.projectId;
|
|
96
|
+
}
|
|
97
|
+
const deployments = await getRecentDeployments(token, teamId, projectId, {
|
|
98
|
+
limit: parseInt(opts.limit ?? "10"),
|
|
99
|
+
branch: opts.branch,
|
|
100
|
+
});
|
|
101
|
+
console.log(JSON.stringify(deployments));
|
|
102
|
+
}
|
|
103
|
+
async function branchUrl(opts) {
|
|
104
|
+
const token = requireVercelToken();
|
|
105
|
+
let teamId = opts.team;
|
|
106
|
+
let projectId = opts.project;
|
|
107
|
+
if (!teamId || !projectId) {
|
|
108
|
+
const config = readVercelConfig();
|
|
109
|
+
if (!config) {
|
|
110
|
+
console.log(JSON.stringify({
|
|
111
|
+
error: "vercel_not_configured",
|
|
112
|
+
message: "No Vercel project configured. Run 'inflight vercel --team=TEAM_ID --project=PROJECT_ID --json' first.",
|
|
113
|
+
}));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
teamId = teamId ?? config.teamId;
|
|
117
|
+
projectId = projectId ?? config.projectId;
|
|
118
|
+
}
|
|
119
|
+
const url = await getBranchAliasUrl(token, teamId, projectId, opts.branch);
|
|
120
|
+
if (url) {
|
|
121
|
+
console.log(JSON.stringify({ url, branch: opts.branch }));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(JSON.stringify({ url: null, branch: opts.branch, message: "No deployment found for this branch." }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Gets token silently, exits with JSON error if unavailable. */
|
|
128
|
+
function requireVercelToken() {
|
|
129
|
+
const token = getVercelToken();
|
|
130
|
+
if (!token) {
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
error: "vercel_not_authenticated",
|
|
133
|
+
message: "Vercel auth expired or missing. Run 'inflight vercel' or 'vercel login' first.",
|
|
134
|
+
}));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
return token;
|
|
138
|
+
}
|
|
139
|
+
// --- Command registration ---
|
|
140
|
+
export function registerVercelCommand(program) {
|
|
141
|
+
const vercel = program
|
|
142
|
+
.command("vercel")
|
|
143
|
+
.description("Set up or change your Vercel project")
|
|
144
|
+
.option("--team <id>", "Vercel team ID (non-interactive)")
|
|
145
|
+
.option("--project <id>", "Vercel project ID (non-interactive)")
|
|
146
|
+
.option("--json", "Output as JSON (non-interactive)")
|
|
147
|
+
.action(vercelSetup);
|
|
148
|
+
vercel.command("teams").description("List Vercel teams (JSON)").action(listTeams);
|
|
149
|
+
vercel
|
|
150
|
+
.command("projects")
|
|
151
|
+
.description("List projects for a Vercel team (JSON)")
|
|
152
|
+
.requiredOption("--team <id>", "Vercel team ID")
|
|
153
|
+
.action(listProjects);
|
|
154
|
+
vercel
|
|
155
|
+
.command("branch-url")
|
|
156
|
+
.description("Get the stable branch preview URL (JSON)")
|
|
157
|
+
.requiredOption("--branch <name>", "Git branch name")
|
|
158
|
+
.option("--team <id>", "Vercel team ID (reads from saved config if omitted)")
|
|
159
|
+
.option("--project <id>", "Vercel project ID (reads from saved config if omitted)")
|
|
160
|
+
.action(branchUrl);
|
|
161
|
+
vercel
|
|
162
|
+
.command("deployments")
|
|
163
|
+
.description("List recent deployments (JSON)")
|
|
164
|
+
.option("--team <id>", "Vercel team ID (reads from saved config if omitted)")
|
|
165
|
+
.option("--project <id>", "Vercel project ID (reads from saved config if omitted)")
|
|
166
|
+
.option("--branch <name>", "Filter by git branch")
|
|
167
|
+
.option("--limit <n>", "Number of deployments", "10")
|
|
168
|
+
.action(listDeployments);
|
|
169
|
+
}
|
|
@@ -3,14 +3,12 @@ import pc from "picocolors";
|
|
|
3
3
|
import { readGlobalAuth, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/config.js";
|
|
4
4
|
import { apiGetMe } from "../lib/api.js";
|
|
5
5
|
export async function workspaceCommand() {
|
|
6
|
-
const cwd = process.cwd();
|
|
7
|
-
p.intro(pc.bgCyan(pc.white(" inflight workspace ")));
|
|
8
6
|
const auth = readGlobalAuth();
|
|
9
7
|
if (!auth) {
|
|
10
8
|
p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
|
|
11
9
|
process.exit(1);
|
|
12
10
|
}
|
|
13
|
-
const current = readWorkspaceConfig(
|
|
11
|
+
const current = readWorkspaceConfig();
|
|
14
12
|
const spinner = p.spinner();
|
|
15
13
|
spinner.start("Loading workspaces...");
|
|
16
14
|
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
@@ -44,7 +42,7 @@ export async function workspaceCommand() {
|
|
|
44
42
|
process.exit(0);
|
|
45
43
|
}
|
|
46
44
|
const workspaceId = selected;
|
|
47
|
-
writeWorkspaceConfig(
|
|
45
|
+
writeWorkspaceConfig({ workspaceId });
|
|
48
46
|
const name = workspaces.find((w) => w.id === workspaceId)?.name ?? workspaceId;
|
|
49
47
|
p.outro(pc.green(`Workspace set to ${pc.bold(name)}`));
|
|
50
48
|
process.exit(0);
|
|
@@ -0,0 +1,81 @@
|
|
|
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 workspacesCommand(opts) {
|
|
6
|
+
const auth = readGlobalAuth();
|
|
7
|
+
if (!auth) {
|
|
8
|
+
if (opts.json) {
|
|
9
|
+
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight login first." }));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// ── Fetch workspaces ──
|
|
16
|
+
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify({ error: "api_error", message: e.message }));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
p.log.error(e.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
const workspaces = me.workspaces;
|
|
25
|
+
// ── Set active workspace ──
|
|
26
|
+
if (opts.set) {
|
|
27
|
+
const workspace = workspaces.find((w) => w.id === opts.set);
|
|
28
|
+
if (!workspace) {
|
|
29
|
+
const msg = `Workspace '${opts.set}' not found.`;
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
console.log(JSON.stringify({ error: "workspace_not_found", message: msg, available: workspaces.map((w) => ({ id: w.id, name: w.name })) }));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
p.log.error(msg);
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
writeWorkspaceConfig({ workspaceId: workspace.id });
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
console.log(JSON.stringify({ success: true, active: workspace.id, name: workspace.name }));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
p.outro(pc.green(`Workspace set to ${pc.bold(workspace.name)}`));
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// ── JSON mode: list + active ──
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
const active = readWorkspaceConfig()?.workspaceId ?? null;
|
|
50
|
+
console.log(JSON.stringify({ active, workspaces }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// ── Interactive mode: select workspace ──
|
|
54
|
+
if (workspaces.length === 0) {
|
|
55
|
+
p.log.error("No workspaces found. Create one at inflight.co");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const currentWorkspaceId = readWorkspaceConfig()?.workspaceId;
|
|
59
|
+
if (currentWorkspaceId) {
|
|
60
|
+
const currentWs = workspaces.find((w) => w.id === currentWorkspaceId);
|
|
61
|
+
if (currentWs) {
|
|
62
|
+
p.log.info(`Current workspace: ${pc.bold(currentWs.name)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const selected = await p.select({
|
|
66
|
+
message: "Select a workspace",
|
|
67
|
+
options: workspaces.map((w) => ({
|
|
68
|
+
value: w.id,
|
|
69
|
+
label: w.name,
|
|
70
|
+
hint: currentWorkspaceId === w.id ? "current" : undefined,
|
|
71
|
+
})),
|
|
72
|
+
});
|
|
73
|
+
if (p.isCancel(selected)) {
|
|
74
|
+
p.cancel("Cancelled.");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const workspaceId = selected;
|
|
78
|
+
writeWorkspaceConfig({ workspaceId });
|
|
79
|
+
const name = workspaces.find((w) => w.id === workspaceId)?.name ?? workspaceId;
|
|
80
|
+
p.outro(pc.green(`Workspace set to ${pc.bold(name)}`));
|
|
81
|
+
}
|