inflight-cli 0.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/dist/commands/create.d.ts +3 -0
- package/dist/commands/create.js +108 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -0
- package/dist/lib/api.d.ts +22 -0
- package/dist/lib/api.js +28 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +22 -0
- package/dist/lib/git.d.ts +11 -0
- package/dist/lib/git.js +72 -0
- package/dist/lib/vercel.d.ts +5 -0
- package/dist/lib/vercel.js +18 -0
- package/package.json +30 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { readGlobalConfig, writeGlobalConfig } from "../lib/config.js";
|
|
4
|
+
import { getGitInfo, isGitRepo } from "../lib/git.js";
|
|
5
|
+
import { detectStagingUrl } from "../lib/vercel.js";
|
|
6
|
+
import { apiGetMe, apiCreateVersion } from "../lib/api.js";
|
|
7
|
+
export async function createCommand(opts) {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const config = readGlobalConfig();
|
|
10
|
+
if (!config) {
|
|
11
|
+
p.log.error("Not logged in. Run " + pc.cyan("inflight login") + " first.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
p.intro(pc.bgBlue(pc.white(" inflight create ")));
|
|
15
|
+
// Resolve workspace — lazy, cached after first use
|
|
16
|
+
let workspaceId = config.workspaceId;
|
|
17
|
+
if (!workspaceId) {
|
|
18
|
+
const spinner = p.spinner();
|
|
19
|
+
spinner.start("Loading workspaces...");
|
|
20
|
+
const me = await apiGetMe(config.apiKey, config.apiUrl).catch((e) => {
|
|
21
|
+
spinner.stop("Failed.");
|
|
22
|
+
p.log.error(e.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
spinner.stop("");
|
|
26
|
+
if (me.workspaces.length === 0) {
|
|
27
|
+
p.log.error("No workspaces found. Create one at inflight.co first.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
else if (me.workspaces.length === 1) {
|
|
31
|
+
workspaceId = me.workspaces[0].id;
|
|
32
|
+
p.log.info(`Workspace: ${pc.bold(me.workspaces[0].name)}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const selected = await p.select({
|
|
36
|
+
message: "Select a workspace",
|
|
37
|
+
options: me.workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
38
|
+
});
|
|
39
|
+
if (p.isCancel(selected)) {
|
|
40
|
+
p.cancel("Cancelled.");
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
workspaceId = selected;
|
|
44
|
+
}
|
|
45
|
+
writeGlobalConfig({ ...config, workspaceId });
|
|
46
|
+
}
|
|
47
|
+
// Git info
|
|
48
|
+
const gitInfo = isGitRepo(cwd)
|
|
49
|
+
? getGitInfo(cwd)
|
|
50
|
+
: { branch: null, commitShort: null, commitFull: null, commitMessage: null, remoteUrl: null, isDirty: false, diff: null };
|
|
51
|
+
// Title defaults to branch name
|
|
52
|
+
const defaultTitle = gitInfo.branch ?? "Untitled";
|
|
53
|
+
const titleInput = opts.title ??
|
|
54
|
+
(await p.text({
|
|
55
|
+
message: "Version title",
|
|
56
|
+
placeholder: defaultTitle,
|
|
57
|
+
defaultValue: defaultTitle,
|
|
58
|
+
}));
|
|
59
|
+
if (p.isCancel(titleInput)) {
|
|
60
|
+
p.cancel("Cancelled.");
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
const title = titleInput;
|
|
64
|
+
// Staging URL — auto-detected from env or prompted
|
|
65
|
+
let stagingUrl = detectStagingUrl();
|
|
66
|
+
if (stagingUrl) {
|
|
67
|
+
p.log.info(`Staging URL: ${pc.cyan(stagingUrl)}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const input = await p.text({
|
|
71
|
+
message: "Staging URL",
|
|
72
|
+
placeholder: "https://my-branch.vercel.app",
|
|
73
|
+
validate: (v) => {
|
|
74
|
+
if (!v)
|
|
75
|
+
return "Staging URL is required";
|
|
76
|
+
try {
|
|
77
|
+
new URL(v);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return "Must be a valid URL (include https://)";
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (p.isCancel(input)) {
|
|
85
|
+
p.cancel("Cancelled.");
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
stagingUrl = input;
|
|
89
|
+
}
|
|
90
|
+
const spinner = p.spinner();
|
|
91
|
+
spinner.start("Creating version...");
|
|
92
|
+
const result = await apiCreateVersion({
|
|
93
|
+
apiKey: config.apiKey,
|
|
94
|
+
apiUrl: config.apiUrl,
|
|
95
|
+
workspaceId,
|
|
96
|
+
title,
|
|
97
|
+
stagingUrl,
|
|
98
|
+
gitInfo,
|
|
99
|
+
}).catch((e) => {
|
|
100
|
+
spinner.stop("Failed.");
|
|
101
|
+
p.log.error(e.message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
spinner.stop("Version created");
|
|
105
|
+
p.note(result.inflightUrl, "Your Inflight version");
|
|
106
|
+
p.outro(pc.green("✓ Done") + " — open Inflight to add feedback and publish when ready");
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loginCommand(): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import { writeGlobalConfig } from "../lib/config.js";
|
|
5
|
+
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";
|
|
8
|
+
export async function loginCommand() {
|
|
9
|
+
p.intro(pc.bgBlue(pc.white(" inflight login ")));
|
|
10
|
+
p.log.info("Opening browser to authenticate with Inflight...");
|
|
11
|
+
const apiKey = await browserAuth();
|
|
12
|
+
const apiUrl = DEFAULT_API_URL;
|
|
13
|
+
const spinner = p.spinner();
|
|
14
|
+
spinner.start("Validating...");
|
|
15
|
+
const me = await apiGetMe(apiKey, apiUrl).catch((e) => {
|
|
16
|
+
spinner.stop("Validation failed.");
|
|
17
|
+
p.log.error(e.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
spinner.stop(`Authenticated as ${pc.bold(me.name)}`);
|
|
21
|
+
writeGlobalConfig({ apiKey, apiUrl });
|
|
22
|
+
p.outro(pc.green("✓ Logged in successfully"));
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
function browserAuth() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const server = createServer((req, res) => {
|
|
28
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
29
|
+
const key = url.searchParams.get("api_key");
|
|
30
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
31
|
+
res.end(`
|
|
32
|
+
<html><body style="font-family:sans-serif;text-align:center;padding:60px">
|
|
33
|
+
<h2>Authenticated!</h2>
|
|
34
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
35
|
+
<script>window.close()</script>
|
|
36
|
+
</body></html>
|
|
37
|
+
`);
|
|
38
|
+
server.close();
|
|
39
|
+
if (key) {
|
|
40
|
+
resolve(key);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
reject(new Error("No API key received from browser"));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
server.listen(0, "127.0.0.1").unref();
|
|
47
|
+
server.once("listening", async () => {
|
|
48
|
+
const port = server.address().port;
|
|
49
|
+
const callbackUrl = `http://127.0.0.1:${port}`;
|
|
50
|
+
const authUrl = `${WEB_URL}/cli/connect?callback=${encodeURIComponent(callbackUrl)}`;
|
|
51
|
+
p.log.info(`Opening ${pc.cyan(authUrl)}`);
|
|
52
|
+
const { default: open } = await import("open");
|
|
53
|
+
await open(authUrl);
|
|
54
|
+
});
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
server.close();
|
|
57
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
58
|
+
}, 5 * 60 * 1000);
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { loginCommand } from "./commands/login.js";
|
|
4
|
+
import { createCommand } from "./commands/create.js";
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name("inflight")
|
|
8
|
+
.description("Create and share design versions from the terminal")
|
|
9
|
+
.version("0.1.0");
|
|
10
|
+
program
|
|
11
|
+
.command("login")
|
|
12
|
+
.description("Authenticate with your Inflight account")
|
|
13
|
+
.action(loginCommand);
|
|
14
|
+
program
|
|
15
|
+
.command("create")
|
|
16
|
+
.description("Create a new draft version for this project")
|
|
17
|
+
.option("-t, --title <title>", "Version title")
|
|
18
|
+
.action((opts) => createCommand({ title: opts.title }));
|
|
19
|
+
program.parse();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GitInfo } from "./git.js";
|
|
2
|
+
export interface Workspace {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CreateVersionResult {
|
|
7
|
+
projectId: string;
|
|
8
|
+
versionId: string;
|
|
9
|
+
inflightUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function apiGetMe(apiKey: string, apiUrl: string): Promise<{
|
|
12
|
+
name: string;
|
|
13
|
+
workspaces: Workspace[];
|
|
14
|
+
}>;
|
|
15
|
+
export declare function apiCreateVersion(opts: {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
apiUrl: string;
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
stagingUrl: string;
|
|
21
|
+
gitInfo: GitInfo;
|
|
22
|
+
}): Promise<CreateVersionResult>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function apiGetMe(apiKey, apiUrl) {
|
|
2
|
+
const res = await fetch(`${apiUrl}/api/cli/me`, {
|
|
3
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
4
|
+
});
|
|
5
|
+
if (!res.ok)
|
|
6
|
+
throw new Error("Invalid API key");
|
|
7
|
+
return res.json();
|
|
8
|
+
}
|
|
9
|
+
export async function apiCreateVersion(opts) {
|
|
10
|
+
const res = await fetch(`${opts.apiUrl}/api/cli/version/create`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
workspaceId: opts.workspaceId,
|
|
18
|
+
title: opts.title,
|
|
19
|
+
stagingUrl: opts.stagingUrl,
|
|
20
|
+
gitInfo: opts.gitInfo,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
25
|
+
throw new Error(body.error ?? `API error ${res.status}`);
|
|
26
|
+
}
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".inflight");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
const DEFAULT_API_URL = process.env.INFLIGHT_API_URL ?? "https://api.inflight.co";
|
|
7
|
+
export function readGlobalConfig() {
|
|
8
|
+
if (!existsSync(CONFIG_FILE))
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
12
|
+
// Allow env var to override stored API URL (useful for local dev)
|
|
13
|
+
return { ...config, apiUrl: DEFAULT_API_URL };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeGlobalConfig(config) {
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface GitInfo {
|
|
2
|
+
branch: string | null;
|
|
3
|
+
commitShort: string | null;
|
|
4
|
+
commitFull: string | null;
|
|
5
|
+
commitMessage: string | null;
|
|
6
|
+
remoteUrl: string | null;
|
|
7
|
+
isDirty: boolean;
|
|
8
|
+
diff: string | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function getGitInfo(cwd: string): GitInfo;
|
|
11
|
+
export declare function isGitRepo(cwd: string): boolean;
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
function run(cmd, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
return execSync(cmd, { cwd, stdio: ["pipe", "pipe", "pipe"] })
|
|
5
|
+
.toString()
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// Files matching these patterns are stripped from the diff before sending to GPT.
|
|
13
|
+
// They consume token budget without providing useful feedback signal.
|
|
14
|
+
const EXCLUDED_FILE_PATTERNS = [
|
|
15
|
+
/^(dist|build|\.next|out)\//,
|
|
16
|
+
/\/dist\//,
|
|
17
|
+
/\/build\//,
|
|
18
|
+
/\.min\.(js|css)$/,
|
|
19
|
+
/public\/.*\.js$/, // minified embed bundles, compiled assets
|
|
20
|
+
/^(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb|bun\.lock)$/,
|
|
21
|
+
/\.snap$/,
|
|
22
|
+
// Generated type files — large, no feedback signal
|
|
23
|
+
/supabase\.ts$/,
|
|
24
|
+
];
|
|
25
|
+
function isExcludedFile(filePath) {
|
|
26
|
+
return EXCLUDED_FILE_PATTERNS.some((re) => re.test(filePath));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Strip hunks for excluded files from a unified diff string.
|
|
30
|
+
* Each file section starts with "diff --git a/<path> b/<path>".
|
|
31
|
+
*/
|
|
32
|
+
function filterDiff(raw) {
|
|
33
|
+
// Split on file boundaries, keeping the delimiter
|
|
34
|
+
const sections = raw.split(/(?=^diff --git )/m);
|
|
35
|
+
return sections
|
|
36
|
+
.filter((section) => {
|
|
37
|
+
const match = section.match(/^diff --git a\/.+ b\/(.+)/);
|
|
38
|
+
if (!match)
|
|
39
|
+
return true; // keep preamble / unknown sections
|
|
40
|
+
return !isExcludedFile(match[1]);
|
|
41
|
+
})
|
|
42
|
+
.join("");
|
|
43
|
+
}
|
|
44
|
+
function getDiff(cwd) {
|
|
45
|
+
// Try to find merge base with origin/main or origin/master
|
|
46
|
+
const mergeBase = run("git merge-base origin/main HEAD", cwd) ?? run("git merge-base origin/master HEAD", cwd);
|
|
47
|
+
const raw = mergeBase
|
|
48
|
+
? run(`git diff ${mergeBase}..HEAD`, cwd)
|
|
49
|
+
: run("git diff HEAD~1..HEAD", cwd);
|
|
50
|
+
if (!raw)
|
|
51
|
+
return null;
|
|
52
|
+
const filtered = filterDiff(raw);
|
|
53
|
+
if (!filtered.trim())
|
|
54
|
+
return null;
|
|
55
|
+
// Cap at 100k chars for truly enormous diffs
|
|
56
|
+
const MAX_DIFF_CHARS = 100_000;
|
|
57
|
+
return filtered.length > MAX_DIFF_CHARS ? filtered.slice(0, MAX_DIFF_CHARS) : filtered;
|
|
58
|
+
}
|
|
59
|
+
export function getGitInfo(cwd) {
|
|
60
|
+
return {
|
|
61
|
+
branch: run("git rev-parse --abbrev-ref HEAD", cwd),
|
|
62
|
+
commitShort: run("git rev-parse --short HEAD", cwd),
|
|
63
|
+
commitFull: run("git rev-parse HEAD", cwd),
|
|
64
|
+
commitMessage: run("git log -1 --pretty=%s", cwd),
|
|
65
|
+
remoteUrl: run("git remote get-url origin", cwd),
|
|
66
|
+
isDirty: run("git status --porcelain", cwd) !== "",
|
|
67
|
+
diff: getDiff(cwd),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function isGitRepo(cwd) {
|
|
71
|
+
return run("git rev-parse --git-dir", cwd) !== null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect staging URL from hosting provider environment variables.
|
|
3
|
+
* Only works when running inside a CI/CD build environment.
|
|
4
|
+
*/
|
|
5
|
+
export function detectStagingUrl() {
|
|
6
|
+
// Vercel preview deployments
|
|
7
|
+
if (process.env.VERCEL_URL) {
|
|
8
|
+
return `https://${process.env.VERCEL_URL}`;
|
|
9
|
+
}
|
|
10
|
+
// Netlify deploy previews
|
|
11
|
+
if (process.env.DEPLOY_PRIME_URL) {
|
|
12
|
+
return process.env.DEPLOY_PRIME_URL;
|
|
13
|
+
}
|
|
14
|
+
if (process.env.DEPLOY_URL) {
|
|
15
|
+
return process.env.DEPLOY_URL;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inflight-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inflight CLI — create and share design versions from the terminal",
|
|
5
|
+
"bin": {
|
|
6
|
+
"inflight": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://inflight.co",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
17
|
+
"dev": "tsx src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@clack/prompts": "^0.8.2",
|
|
21
|
+
"commander": "^12.1.0",
|
|
22
|
+
"open": "^11.0.0",
|
|
23
|
+
"picocolors": "^1.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"tsx": "^4.19.0",
|
|
28
|
+
"typescript": "^5.6.0"
|
|
29
|
+
}
|
|
30
|
+
}
|