inflight-cli 1.1.4 → 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/index.js CHANGED
@@ -3,15 +3,25 @@ import { Command } from "commander";
3
3
  import updateNotifier from "update-notifier";
4
4
  import { loginCommand } from "./commands/login.js";
5
5
  import { shareCommand } from "./commands/share.js";
6
- import { workspaceCommand } from "./commands/workspace.js";
7
6
  import { logoutCommand } from "./commands/logout.js";
7
+ import { resetCommand } from "./commands/reset.js";
8
+ import { workspacesCommand } from "./commands/workspaces.js";
9
+ import { registerVercelCommand } from "./commands/vercel.js";
10
+ import { setupCommand } from "./commands/setup.js";
8
11
  import pkg from "../package.json" with { type: "json" };
9
12
  const { version } = pkg;
10
13
  updateNotifier({ pkg }).notify();
11
14
  const program = new Command();
12
15
  program.name("inflight").description("Get feedback directly on your staging URL").version(version);
16
+ program.command("setup").description("Set up Inflight in your project").action(setupCommand);
13
17
  program.command("login").description("Authenticate with your Inflight account").action(loginCommand);
14
- program.command("share").description("Get feedback on your staging URL").action(shareCommand);
18
+ program
19
+ .command("share")
20
+ .description("Get feedback on your staging URL")
21
+ .option("--url <url>", "Staging URL (skips provider selection)")
22
+ .option("--workspace <id>", "Workspace ID (skips workspace selection)")
23
+ .option("--json", "Output result as JSON")
24
+ .action((opts) => shareCommand(opts));
15
25
  // program
16
26
  // .command("preview")
17
27
  // .description("Preview a live component from your code")
@@ -19,6 +29,13 @@ program.command("share").description("Get feedback on your staging URL").action(
19
29
  // .option("--scope <mode>", "Skip scope prompt: branch, uncommitted, staged")
20
30
  // .option("--no-open", "Don't open result in browser")
21
31
  // .action((opts) => previewCommand(opts));
22
- program.command("workspace").description("Switch the active inflight workspace").action(workspaceCommand);
32
+ program
33
+ .command("workspaces")
34
+ .description("List, select, or set your workspaces")
35
+ .option("--json", "Output as JSON")
36
+ .option("--set <id>", "Set the active workspace")
37
+ .action((opts) => workspacesCommand(opts));
38
+ registerVercelCommand(program);
23
39
  program.command("logout").description("Disconnect your Inflight account").action(logoutCommand);
40
+ program.command("reset").description("Clear all Inflight auth and config").action(resetCommand);
24
41
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { GitInfo } from "./git.js";
2
2
  export interface Workspace {
3
3
  id: string;
4
4
  name: string;
5
+ widgetId: string;
5
6
  }
6
7
  export interface CreateVersionResult {
7
8
  projectId: string;
@@ -19,3 +20,14 @@ export declare function apiCreateVersion(opts: {
19
20
  stagingUrl: string;
20
21
  gitInfo: GitInfo;
21
22
  }): Promise<CreateVersionResult>;
23
+ export interface WidgetLocationResult {
24
+ file: string | null;
25
+ insertAfter: string | null;
26
+ framework: string | null;
27
+ confidence: "high" | "low";
28
+ }
29
+ export declare function apiDetectWidgetLocation(opts: {
30
+ apiKey: string;
31
+ fileTree: string[];
32
+ fileContents: Record<string, string>;
33
+ }): Promise<WidgetLocationResult>;
package/dist/lib/api.js CHANGED
@@ -26,3 +26,20 @@ export async function apiCreateVersion(opts) {
26
26
  }
27
27
  return res.json();
28
28
  }
29
+ export async function apiDetectWidgetLocation(opts) {
30
+ const res = await fetch(`${API_URL}/api/cli/detect-widget-location`, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${opts.apiKey}`,
35
+ },
36
+ body: JSON.stringify({
37
+ fileTree: opts.fileTree,
38
+ fileContents: opts.fileContents,
39
+ }),
40
+ });
41
+ if (!res.ok) {
42
+ return { file: null, insertAfter: null, framework: null, confidence: "low" };
43
+ }
44
+ return res.json();
45
+ }
@@ -7,5 +7,15 @@ export declare function clearGlobalAuth(): void;
7
7
  export interface WorkspaceConfig {
8
8
  workspaceId: string;
9
9
  }
10
- export declare function readWorkspaceConfig(cwd: string): WorkspaceConfig | null;
11
- export declare function writeWorkspaceConfig(cwd: string, config: WorkspaceConfig): void;
10
+ export declare function readWorkspaceConfig(): WorkspaceConfig | null;
11
+ export declare function clearWorkspaceConfig(): void;
12
+ export declare function writeWorkspaceConfig(config: WorkspaceConfig): void;
13
+ export interface VercelConfig {
14
+ teamId: string;
15
+ teamName: string;
16
+ projectId: string;
17
+ projectName: string;
18
+ }
19
+ export declare function readVercelConfig(): VercelConfig | null;
20
+ export declare function writeVercelConfig(config: VercelConfig): void;
21
+ export declare function clearVercelConfig(): void;
@@ -30,35 +30,45 @@ export function clearGlobalAuth() {
30
30
  unlinkSync(AUTH_FILE);
31
31
  }
32
32
  }
33
- // --- Project-level config (per-directory, like .vercel/project.json) ---
34
- const WORKSPACE_FILE = ".inflight/workspace.json";
35
- export function readWorkspaceConfig(cwd) {
36
- const file = join(cwd, WORKSPACE_FILE);
37
- if (!existsSync(file))
33
+ // --- Workspace config (global, persists across directories) ---
34
+ const WORKSPACE_FILE = join(getGlobalConfigDir(), "workspace.json");
35
+ export function readWorkspaceConfig() {
36
+ if (!existsSync(WORKSPACE_FILE))
38
37
  return null;
39
38
  try {
40
- return JSON.parse(readFileSync(file, "utf-8"));
39
+ return JSON.parse(readFileSync(WORKSPACE_FILE, "utf-8"));
41
40
  }
42
41
  catch {
43
42
  return null;
44
43
  }
45
44
  }
46
- export function writeWorkspaceConfig(cwd, config) {
47
- const dir = join(cwd, ".inflight");
48
- mkdirSync(dir, { recursive: true });
49
- writeFileSync(join(cwd, WORKSPACE_FILE), JSON.stringify(config, null, 2));
50
- addToGitignore(cwd);
45
+ export function clearWorkspaceConfig() {
46
+ if (existsSync(WORKSPACE_FILE)) {
47
+ unlinkSync(WORKSPACE_FILE);
48
+ }
49
+ }
50
+ export function writeWorkspaceConfig(config) {
51
+ mkdirSync(getGlobalConfigDir(), { recursive: true });
52
+ writeFileSync(WORKSPACE_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
51
53
  }
52
- function addToGitignore(cwd) {
53
- const gitignorePath = join(cwd, ".gitignore");
54
- const entry = ".inflight";
54
+ // --- Vercel config (global, persists across directories) ---
55
+ const VERCEL_FILE = join(getGlobalConfigDir(), "vercel.json");
56
+ export function readVercelConfig() {
57
+ if (!existsSync(VERCEL_FILE))
58
+ return null;
55
59
  try {
56
- const contents = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
57
- if (!contents.split("\n").some((line) => line.trim() === entry)) {
58
- writeFileSync(gitignorePath, contents + (contents.endsWith("\n") ? "" : "\n") + entry + "\n");
59
- }
60
+ return JSON.parse(readFileSync(VERCEL_FILE, "utf-8"));
60
61
  }
61
62
  catch {
62
- // non-fatal
63
+ return null;
64
+ }
65
+ }
66
+ export function writeVercelConfig(config) {
67
+ mkdirSync(getGlobalConfigDir(), { recursive: true });
68
+ writeFileSync(VERCEL_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
69
+ }
70
+ export function clearVercelConfig() {
71
+ if (existsSync(VERCEL_FILE)) {
72
+ unlinkSync(VERCEL_FILE);
63
73
  }
64
74
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Gathers a lightweight file tree and candidate file contents for AI detection.
3
+ * Scans cwd and common monorepo subdirectories.
4
+ */
5
+ export declare function gatherProjectContext(cwd: string): {
6
+ fileTree: string[];
7
+ fileContents: Record<string, string>;
8
+ };
9
+ /**
10
+ * Returns true if any candidate file in the project already has the Inflight widget script.
11
+ */
12
+ export declare function hasInflightWidget(cwd: string): boolean;
13
+ /**
14
+ * Inserts the script tag into a file at the specified location.
15
+ * Returns the file path that was modified, or null if insertion failed.
16
+ */
17
+ export declare function insertWidgetScript(cwd: string, file: string, insertAfter: string, widgetId: string): string | null;
@@ -0,0 +1,154 @@
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
2
+ import { join, relative } from "path";
3
+ /** File patterns that are likely layout/root files where a script tag belongs */
4
+ const CANDIDATE_PATTERNS = [
5
+ "package.json",
6
+ "index.html",
7
+ "app/layout.tsx",
8
+ "app/layout.jsx",
9
+ "src/app/layout.tsx",
10
+ "src/app/layout.jsx",
11
+ "pages/_document.tsx",
12
+ "pages/_document.jsx",
13
+ "src/pages/_document.tsx",
14
+ "src/pages/_document.jsx",
15
+ "app/root.tsx",
16
+ "app/root.jsx",
17
+ "src/app.html",
18
+ "nuxt.config.ts",
19
+ "nuxt.config.js",
20
+ "src/layouts/Layout.astro",
21
+ "src/layouts/BaseLayout.astro",
22
+ ];
23
+ /**
24
+ * Gathers a lightweight file tree and candidate file contents for AI detection.
25
+ * Scans cwd and common monorepo subdirectories.
26
+ */
27
+ export function gatherProjectContext(cwd) {
28
+ const fileTree = [];
29
+ const fileContents = {};
30
+ const dirsToScan = [cwd];
31
+ // If monorepo, add common subdirectories
32
+ for (const dir of ["apps", "packages", "projects", "services", "libs"]) {
33
+ const baseDir = join(cwd, dir);
34
+ if (!existsSync(baseDir))
35
+ continue;
36
+ try {
37
+ for (const entry of readdirSync(baseDir)) {
38
+ const fullPath = join(baseDir, entry);
39
+ if (statSync(fullPath).isDirectory()) {
40
+ dirsToScan.push(fullPath);
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ }
48
+ for (const dir of dirsToScan) {
49
+ // Add shallow file listing (1 level deep + key subdirs)
50
+ try {
51
+ for (const entry of readdirSync(dir)) {
52
+ fileTree.push(relative(cwd, join(dir, entry)));
53
+ }
54
+ // Also list key subdirectories
55
+ for (const sub of ["app", "src", "src/app", "src/layouts", "pages", "src/pages"]) {
56
+ const subDir = join(dir, sub);
57
+ if (!existsSync(subDir))
58
+ continue;
59
+ for (const entry of readdirSync(subDir)) {
60
+ fileTree.push(relative(cwd, join(subDir, entry)));
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ // Read candidate files
68
+ for (const pattern of CANDIDATE_PATTERNS) {
69
+ const filePath = join(dir, pattern);
70
+ const relPath = relative(cwd, filePath);
71
+ if (existsSync(filePath) && !fileContents[relPath]) {
72
+ try {
73
+ const content = readFileSync(filePath, "utf-8");
74
+ // Truncate large files to save tokens
75
+ fileContents[relPath] = content.length > 3000 ? content.slice(0, 3000) + "\n... (truncated)" : content;
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ }
81
+ }
82
+ // Also check for astro layouts dynamically
83
+ const layoutsDir = join(dir, "src", "layouts");
84
+ if (existsSync(layoutsDir)) {
85
+ try {
86
+ for (const f of readdirSync(layoutsDir).filter((f) => f.endsWith(".astro"))) {
87
+ const filePath = join(layoutsDir, f);
88
+ const relPath = relative(cwd, filePath);
89
+ if (!fileContents[relPath]) {
90
+ const content = readFileSync(filePath, "utf-8");
91
+ fileContents[relPath] = content.length > 3000 ? content.slice(0, 3000) + "\n... (truncated)" : content;
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // skip
97
+ }
98
+ }
99
+ }
100
+ return { fileTree, fileContents };
101
+ }
102
+ /**
103
+ * Returns true if any candidate file in the project already has the Inflight widget script.
104
+ */
105
+ export function hasInflightWidget(cwd) {
106
+ const { fileContents } = gatherProjectContext(cwd);
107
+ return Object.values(fileContents).some((content) => content.includes("inflight.co/widget.js"));
108
+ }
109
+ /**
110
+ * Inserts the script tag into a file at the specified location.
111
+ * Returns the file path that was modified, or null if insertion failed.
112
+ */
113
+ export function insertWidgetScript(cwd, file, insertAfter, widgetId) {
114
+ try {
115
+ const filePath = join(cwd, file);
116
+ const content = readFileSync(filePath, "utf-8");
117
+ if (content.includes("inflight.co/widget.js")) {
118
+ return file; // Already present
119
+ }
120
+ const isJsx = file.endsWith(".tsx") || file.endsWith(".jsx");
121
+ const tag = isJsx
122
+ ? `<script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async />`
123
+ : `<script src="https://www.inflight.co/widget.js" data-workspace="${widgetId}" async></script>`;
124
+ let newContent;
125
+ if (insertAfter === "</body>") {
126
+ const lines = content.split("\n");
127
+ const bodyIdx = lines.findIndex((line) => line.includes("</body>"));
128
+ const bodyIndent = bodyIdx >= 0 ? (lines[bodyIdx].match(/^(\s*)/)?.[1] ?? "") : "";
129
+ // Use indentation of the nearest non-empty sibling line before </body>
130
+ let tagIndent = bodyIndent + "\t"; // fallback: one tab deeper than </body>
131
+ for (let i = bodyIdx - 1; i >= 0; i--) {
132
+ if (lines[i].trim()) {
133
+ tagIndent = lines[i].match(/^(\s*)/)?.[1] ?? tagIndent;
134
+ break;
135
+ }
136
+ }
137
+ newContent = content.replace("</body>", `${tagIndent}${tag}\n${bodyIndent}</body>`);
138
+ }
139
+ else {
140
+ const markerLine = content.split("\n").find((line) => line.includes(insertAfter));
141
+ const indent = markerLine?.match(/^(\s*)/)?.[1] ?? "";
142
+ const markerIndex = content.indexOf(insertAfter);
143
+ if (markerIndex === -1)
144
+ return null;
145
+ const insertPos = markerIndex + insertAfter.length;
146
+ newContent = content.slice(0, insertPos) + "\n" + indent + tag + content.slice(insertPos);
147
+ }
148
+ writeFileSync(filePath, newContent);
149
+ return file;
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Installs the Inflight skill for all detected AI agents using the skills CLI.
3
+ * Skips if already installed. Non-fatal — setup continues even if this fails.
4
+ */
5
+ export declare function installSkill(): Promise<boolean>;
@@ -0,0 +1,22 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+ const execAsync = promisify(exec);
7
+ /**
8
+ * Installs the Inflight skill for all detected AI agents using the skills CLI.
9
+ * Skips if already installed. Non-fatal — setup continues even if this fails.
10
+ */
11
+ export async function installSkill() {
12
+ if (existsSync(join(homedir(), ".claude", "skills", "inflight", "SKILL.md"))) {
13
+ return true;
14
+ }
15
+ try {
16
+ await execAsync("npx skills add inflightsoftware/skills -y -g");
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
@@ -1,9 +1,16 @@
1
- export declare function isVercelLoggedIn(): boolean;
2
- export declare function isVercelLinked(cwd: string): boolean;
3
- /** Opens browser-based Vercel login — suppresses CLI output, browser opens automatically. */
4
- export declare function vercelLogin(): boolean;
5
- /** Links the directory to a specific Vercel project silently. */
6
- export declare function vercelLink(cwd: string, projectId: string): Promise<boolean>;
1
+ /** Ensures the Vercel CLI is available — installs globally if missing. */
2
+ export declare function ensureVercelCli(log?: (msg: string) => void): Promise<boolean>;
3
+ /**
4
+ * Gets a valid Vercel token. Refreshes silently via `vercel whoami` if expired.
5
+ * Does NOT prompt for login returns null if no valid token available.
6
+ */
7
+ export declare function getVercelToken(): string | null;
8
+ /**
9
+ * Ensures a valid Vercel auth token is available.
10
+ * Checks existing token → refreshes if expired → opens browser login if needed.
11
+ * For interactive commands only.
12
+ */
13
+ export declare function ensureVercelAuth(): Promise<string | null>;
7
14
  export interface VercelTeam {
8
15
  id: string;
9
16
  name: string;
@@ -13,8 +20,6 @@ export interface VercelProject {
13
20
  id: string;
14
21
  name: string;
15
22
  }
16
- export declare function getVercelTeams(): Promise<VercelTeam[]>;
17
- export declare function getVercelProjects(teamId: string): Promise<VercelProject[]>;
18
23
  export interface VercelDeployment {
19
24
  url: string;
20
25
  branch: string | null;
@@ -22,12 +27,17 @@ export interface VercelDeployment {
22
27
  commitMessage: string | null;
23
28
  createdAt: number;
24
29
  }
30
+ export declare function getVercelTeams(token: string): Promise<VercelTeam[]>;
31
+ export declare function getVercelProjects(token: string, teamId: string): Promise<VercelProject[]>;
25
32
  /**
26
33
  * Fetches the branch alias URL (stable, auto-updates with each push).
27
34
  * Returns null if no deployment exists for this branch.
28
35
  */
29
- export declare function getBranchAliasUrl(cwd: string, branch: string | null): Promise<string | null>;
36
+ export declare function getBranchAliasUrl(token: string, teamId: string, projectId: string, branch: string | null): Promise<string | null>;
30
37
  /**
31
- * Fetches recent deployments for the project.
38
+ * Fetches recent deployments for a project.
32
39
  */
33
- export declare function getRecentDeployments(cwd: string, limit?: number): Promise<VercelDeployment[]>;
40
+ export declare function getRecentDeployments(token: string, teamId: string, projectId: string, opts?: {
41
+ limit?: number;
42
+ branch?: string;
43
+ }): Promise<VercelDeployment[]>;