inflight-cli 2.6.0 → 2.8.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 +56 -33
- package/dist/commands/setup.d.ts +5 -1
- package/dist/commands/setup.js +75 -62
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.js +256 -56
- package/dist/index.js +13 -2
- package/dist/lib/agent.d.ts +25 -0
- package/dist/lib/agent.js +31 -0
- package/dist/lib/framework.js +7 -1
- package/dist/lib/netlify.d.ts +4 -1
- package/dist/lib/netlify.js +9 -4
- package/dist/lib/resolve-workspace.d.ts +15 -0
- package/dist/lib/resolve-workspace.js +77 -0
- package/dist/lib/vercel.d.ts +4 -1
- package/dist/lib/vercel.js +9 -4
- package/dist/providers/netlify.js +76 -52
- package/dist/providers/vercel.js +67 -45
- package/package.json +3 -3
package/dist/commands/share.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import { readGlobalAuth
|
|
4
|
-
import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, } from "../lib/git.js";
|
|
3
|
+
import { readGlobalAuth } from "../lib/config.js";
|
|
4
|
+
import { getGitInfo, getGitSyncState, generateCommitMessage, commitAndPush, pushBranch, hasCommitsAhead, getGitRoot, } from "../lib/git.js";
|
|
5
5
|
import open from "open";
|
|
6
6
|
import { providers } from "../providers/index.js";
|
|
7
7
|
import { apiGetMe, apiCreateVersion, apiGetRecentProjects } from "../lib/api.js";
|
|
8
8
|
import { scrollableSelect } from "../lib/scrollable-select.js";
|
|
9
|
+
import { isAgent, agentSuccess, agentActionRequired, agentError } from "../lib/agent.js";
|
|
10
|
+
import { resolveWorkspace } from "../lib/resolve-workspace.js";
|
|
11
|
+
import { readLocalVercelProject } from "../lib/vercel.js";
|
|
12
|
+
import { readLocalNetlifySite } from "../lib/netlify.js";
|
|
9
13
|
function formatRelativeTime(timestampMs) {
|
|
10
14
|
const seconds = Math.floor((Date.now() - timestampMs) / 1000);
|
|
11
15
|
if (seconds < 60)
|
|
@@ -32,12 +36,29 @@ function isValidHostedUrl(url) {
|
|
|
32
36
|
return false;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds a next_command string carrying forward relevant opts from the current run.
|
|
41
|
+
*/
|
|
42
|
+
function buildNextCommand(base, opts) {
|
|
43
|
+
const parts = [base];
|
|
44
|
+
if (opts.workspace)
|
|
45
|
+
parts.push(`--workspace ${opts.workspace}`);
|
|
46
|
+
if (opts.provider)
|
|
47
|
+
parts.push(`--provider ${opts.provider}`);
|
|
48
|
+
if (opts.deployment)
|
|
49
|
+
parts.push(`--deployment ${opts.deployment}`);
|
|
50
|
+
if (opts.project)
|
|
51
|
+
parts.push(`--project ${opts.project}`);
|
|
52
|
+
if (opts.override)
|
|
53
|
+
parts.push("--override");
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
35
56
|
/**
|
|
36
57
|
* Checks local git state and prompts the user to commit/push if needed.
|
|
37
58
|
* Returns { justPushed: true } if changes were pushed, { justPushed: false } otherwise.
|
|
38
59
|
* Provider-agnostic — works for Vercel, Netlify, or any future provider.
|
|
39
60
|
*/
|
|
40
|
-
async function checkAndSyncGit(cwd) {
|
|
61
|
+
async function checkAndSyncGit(cwd, opts = {}) {
|
|
41
62
|
const state = getGitSyncState(cwd);
|
|
42
63
|
if (state.status === "clean" || state.status === "detached") {
|
|
43
64
|
return { justPushed: false };
|
|
@@ -74,6 +95,17 @@ async function checkAndSyncGit(cwd) {
|
|
|
74
95
|
if (state.changedFiles.length > maxFiles) {
|
|
75
96
|
lines.push(` ${pc.dim(`... and ${state.changedFiles.length - maxFiles} more`)}`);
|
|
76
97
|
}
|
|
98
|
+
if (isAgent) {
|
|
99
|
+
agentActionRequired({
|
|
100
|
+
type: "git_uncommitted",
|
|
101
|
+
message: "You have uncommitted changes. Your deployment won't include them.",
|
|
102
|
+
choices: [
|
|
103
|
+
{ id: "commit_push", label: "Commit and push these changes" },
|
|
104
|
+
{ id: "continue", label: "Continue without committing" },
|
|
105
|
+
],
|
|
106
|
+
nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
77
109
|
lines.push("", pc.yellow("Your deployment won't include these changes."));
|
|
78
110
|
p.log.warn("You have uncommitted changes:\n" + lines.join("\n"));
|
|
79
111
|
const action = await p.select({
|
|
@@ -112,7 +144,12 @@ async function checkAndSyncGit(cwd) {
|
|
|
112
144
|
return { justPushed: true };
|
|
113
145
|
}
|
|
114
146
|
catch (e) {
|
|
115
|
-
|
|
147
|
+
const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
|
|
148
|
+
spinner.stop("Push failed.");
|
|
149
|
+
if (stderr)
|
|
150
|
+
p.log.message(pc.dim(stderr));
|
|
151
|
+
else if (e instanceof Error)
|
|
152
|
+
p.log.message(pc.dim(e.message));
|
|
116
153
|
p.log.warn("Continuing with existing deployment.");
|
|
117
154
|
return { justPushed: false };
|
|
118
155
|
}
|
|
@@ -123,6 +160,17 @@ async function checkAndSyncGit(cwd) {
|
|
|
123
160
|
if (state.unpushedCommits.length > 5) {
|
|
124
161
|
commitLines.push(` ${pc.dim(`... and ${state.unpushedCommits.length - 5} more`)}`);
|
|
125
162
|
}
|
|
163
|
+
if (isAgent) {
|
|
164
|
+
agentActionRequired({
|
|
165
|
+
type: "git_unpushed",
|
|
166
|
+
message: "You have unpushed commits. Your deployment won't include them.",
|
|
167
|
+
choices: [
|
|
168
|
+
{ id: "push", label: "Push these commits" },
|
|
169
|
+
{ id: "continue", label: "Continue without pushing" },
|
|
170
|
+
],
|
|
171
|
+
nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
126
174
|
commitLines.push("", pc.yellow("Your deployment won't include these commits."));
|
|
127
175
|
p.log.warn("You have unpushed commits:\n" + commitLines.join("\n"));
|
|
128
176
|
const action = await p.select({
|
|
@@ -147,7 +195,12 @@ async function checkAndSyncGit(cwd) {
|
|
|
147
195
|
return { justPushed: true };
|
|
148
196
|
}
|
|
149
197
|
catch (e) {
|
|
150
|
-
|
|
198
|
+
const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
|
|
199
|
+
spinner.stop("Push failed.");
|
|
200
|
+
if (stderr)
|
|
201
|
+
p.log.message(pc.dim(stderr));
|
|
202
|
+
else if (e instanceof Error)
|
|
203
|
+
p.log.message(pc.dim(e.message));
|
|
151
204
|
p.log.warn("Continuing with existing deployment.");
|
|
152
205
|
return { justPushed: false };
|
|
153
206
|
}
|
|
@@ -160,6 +213,17 @@ async function checkAndSyncGit(cwd) {
|
|
|
160
213
|
p.log.info(`Branch ${pc.bold(branch)} has no new commits — using existing deployments.`);
|
|
161
214
|
return { justPushed: false };
|
|
162
215
|
}
|
|
216
|
+
if (isAgent) {
|
|
217
|
+
agentActionRequired({
|
|
218
|
+
type: "git_no_remote",
|
|
219
|
+
message: `Branch "${branch}" hasn't been pushed yet — no deployment exists.`,
|
|
220
|
+
choices: [
|
|
221
|
+
{ id: "push", label: "Push to create a deployment" },
|
|
222
|
+
{ id: "continue", label: "Continue without pushing" },
|
|
223
|
+
],
|
|
224
|
+
nextCommand: buildNextCommand("inflight share --skip-git-check", opts),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
163
227
|
p.log.warn(`Branch ${pc.bold(branch)} hasn't been pushed yet — no deployment exists.`);
|
|
164
228
|
const confirm = await p.confirm({
|
|
165
229
|
message: "Push to create a deployment?",
|
|
@@ -179,18 +243,182 @@ async function checkAndSyncGit(cwd) {
|
|
|
179
243
|
return { justPushed: true };
|
|
180
244
|
}
|
|
181
245
|
catch (e) {
|
|
182
|
-
|
|
246
|
+
const stderr = e && typeof e === "object" && "stderr" in e ? String(e.stderr).trim() : "";
|
|
247
|
+
spinner.stop("Push failed.");
|
|
248
|
+
if (stderr)
|
|
249
|
+
p.log.message(pc.dim(stderr));
|
|
250
|
+
else if (e instanceof Error)
|
|
251
|
+
p.log.message(pc.dim(e.message));
|
|
183
252
|
return { justPushed: false };
|
|
184
253
|
}
|
|
185
254
|
}
|
|
186
255
|
return { justPushed: false };
|
|
187
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Agent mode share flow.
|
|
259
|
+
* Mirrors every human prompt with action_required JSON.
|
|
260
|
+
* Polling runs normally (no spinners).
|
|
261
|
+
* Git is NOT touched — if there's dirty state, action_required is returned
|
|
262
|
+
* and the agent handles git itself, then re-runs with --skip-git-check.
|
|
263
|
+
*/
|
|
264
|
+
async function agentShareFlow(cwd, apiKey, workspaceId, opts) {
|
|
265
|
+
// ── Git sync — report state, don't touch git ──
|
|
266
|
+
if (!opts.skipGitCheck) {
|
|
267
|
+
// checkAndSyncGit will call agentActionRequired and exit if git is dirty
|
|
268
|
+
await checkAndSyncGit(cwd, opts);
|
|
269
|
+
// If we reach here, git is clean
|
|
270
|
+
}
|
|
271
|
+
const gitInfo = getGitInfo(cwd);
|
|
272
|
+
// ── Resolve staging URL ──
|
|
273
|
+
let stagingUrl;
|
|
274
|
+
if (opts.deployment) {
|
|
275
|
+
// --deployment flag provided
|
|
276
|
+
stagingUrl = opts.deployment;
|
|
277
|
+
if (!stagingUrl.startsWith("http"))
|
|
278
|
+
stagingUrl = `https://${stagingUrl}`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// Determine provider
|
|
282
|
+
let providerId = opts.provider;
|
|
283
|
+
if (!providerId) {
|
|
284
|
+
// Auto-detect from local config files
|
|
285
|
+
const gitRoot = getGitRoot(cwd);
|
|
286
|
+
if (gitRoot && readLocalVercelProject(gitRoot)) {
|
|
287
|
+
providerId = "vercel";
|
|
288
|
+
}
|
|
289
|
+
else if (gitRoot && readLocalNetlifySite(gitRoot)) {
|
|
290
|
+
providerId = "netlify";
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
agentActionRequired({
|
|
294
|
+
type: "choose_provider",
|
|
295
|
+
message: "Could not auto-detect deployment provider.",
|
|
296
|
+
choices: [
|
|
297
|
+
{ id: "vercel", label: "Vercel" },
|
|
298
|
+
{ id: "netlify", label: "Netlify" },
|
|
299
|
+
],
|
|
300
|
+
nextCommand: "inflight share --skip-git-check --provider <ID>",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const provider = providers.find((prov) => prov.id === providerId);
|
|
305
|
+
if (!provider) {
|
|
306
|
+
agentError({
|
|
307
|
+
type: "invalid_provider",
|
|
308
|
+
message: `Unknown provider "${providerId}". Use "vercel" or "netlify".`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
stagingUrl = (await provider.resolve(cwd, gitInfo, {})) ?? undefined;
|
|
312
|
+
if (stagingUrl && !stagingUrl.startsWith("http")) {
|
|
313
|
+
stagingUrl = `https://${stagingUrl}`;
|
|
314
|
+
}
|
|
315
|
+
if (!stagingUrl) {
|
|
316
|
+
agentError({
|
|
317
|
+
type: "no_deployment",
|
|
318
|
+
message: "Could not find a deployment URL. Provide one with --url.",
|
|
319
|
+
suggestion: "inflight share --url <staging-url>",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── Resolve project ──
|
|
324
|
+
let selectedProjectId;
|
|
325
|
+
let overrideVersionId;
|
|
326
|
+
if (opts.project) {
|
|
327
|
+
if (opts.project !== "new") {
|
|
328
|
+
selectedProjectId = opts.project;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
|
|
333
|
+
projects: [],
|
|
334
|
+
}));
|
|
335
|
+
if (projects.length > 0) {
|
|
336
|
+
const currentBranch = gitInfo.branch;
|
|
337
|
+
agentActionRequired({
|
|
338
|
+
type: "choose_project",
|
|
339
|
+
message: "Select a project or create new.",
|
|
340
|
+
choices: [
|
|
341
|
+
{ id: "new", label: "Start a new project" },
|
|
342
|
+
...projects.map((proj) => ({
|
|
343
|
+
id: proj.projectId,
|
|
344
|
+
label: proj.latestVersion.title,
|
|
345
|
+
hint: [
|
|
346
|
+
proj.latestVersion.branch === currentBranch ? "current branch" : proj.latestVersion.branch,
|
|
347
|
+
proj.latestVersion.branch === currentBranch ? "(recommended)" : undefined,
|
|
348
|
+
]
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
.join(" ") || undefined,
|
|
351
|
+
})),
|
|
352
|
+
],
|
|
353
|
+
nextCommand: `inflight share --skip-git-check --deployment ${stagingUrl} --project <ID>`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// ── Resolve override vs new version ──
|
|
358
|
+
if (selectedProjectId && !opts.override) {
|
|
359
|
+
const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
|
|
360
|
+
projects: [],
|
|
361
|
+
}));
|
|
362
|
+
const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
|
|
363
|
+
if (selectedProject && selectedProject.latestVersion.commentCount === 0) {
|
|
364
|
+
agentActionRequired({
|
|
365
|
+
type: "choose_override",
|
|
366
|
+
message: `"${selectedProject.latestVersion.title}" has no feedback yet.`,
|
|
367
|
+
choices: [
|
|
368
|
+
{
|
|
369
|
+
id: "override",
|
|
370
|
+
label: "Update its staging URL",
|
|
371
|
+
hint: `replace with ${new URL(stagingUrl).hostname}`,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: "new_version",
|
|
375
|
+
label: "Add a new version",
|
|
376
|
+
hint: "keep both in version history",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
nextCommand: `inflight share --skip-git-check --deployment ${stagingUrl} --project ${selectedProjectId} --override`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (opts.override && selectedProjectId) {
|
|
384
|
+
const { projects } = await apiGetRecentProjects(apiKey, workspaceId, 20).catch(() => ({
|
|
385
|
+
projects: [],
|
|
386
|
+
}));
|
|
387
|
+
const selectedProject = projects.find((proj) => proj.projectId === selectedProjectId);
|
|
388
|
+
if (selectedProject) {
|
|
389
|
+
overrideVersionId = selectedProject.latestVersion.id;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// ── Create version ──
|
|
393
|
+
const result = await apiCreateVersion({
|
|
394
|
+
apiKey,
|
|
395
|
+
workspaceId,
|
|
396
|
+
stagingUrl: stagingUrl,
|
|
397
|
+
gitInfo,
|
|
398
|
+
...(selectedProjectId && { projectId: selectedProjectId }),
|
|
399
|
+
...(overrideVersionId && { overrideVersionId }),
|
|
400
|
+
}).catch((e) => {
|
|
401
|
+
agentError({ type: "create_failed", message: e.message });
|
|
402
|
+
});
|
|
403
|
+
agentSuccess({
|
|
404
|
+
stagingUrl,
|
|
405
|
+
...result,
|
|
406
|
+
isOverride: !!overrideVersionId,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
188
409
|
export async function shareCommand(opts = {}) {
|
|
189
410
|
const cwd = process.cwd();
|
|
190
411
|
// TODO: Add a step to login if not authenticated
|
|
191
412
|
// ── Step 1: Auth ──
|
|
192
413
|
const auth = readGlobalAuth();
|
|
193
414
|
if (!auth) {
|
|
415
|
+
if (isAgent) {
|
|
416
|
+
agentError({
|
|
417
|
+
type: "not_authenticated",
|
|
418
|
+
message: "Not logged in. Run inflight setup first.",
|
|
419
|
+
suggestion: "inflight setup",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
194
422
|
if (opts.json) {
|
|
195
423
|
console.log(JSON.stringify({ error: "not_authenticated", message: "Not logged in. Run inflight setup first." }));
|
|
196
424
|
}
|
|
@@ -202,6 +430,8 @@ export async function shareCommand(opts = {}) {
|
|
|
202
430
|
let gitInfo = getGitInfo(cwd);
|
|
203
431
|
// ── Step 2: Resolve workspace ──
|
|
204
432
|
const me = await apiGetMe(auth.apiKey).catch((e) => {
|
|
433
|
+
if (isAgent)
|
|
434
|
+
agentError({ type: "api_error", message: e.message });
|
|
205
435
|
if (opts.json) {
|
|
206
436
|
console.log(JSON.stringify({ error: "api_error", message: e.message }));
|
|
207
437
|
}
|
|
@@ -210,50 +440,10 @@ export async function shareCommand(opts = {}) {
|
|
|
210
440
|
}
|
|
211
441
|
process.exit(1);
|
|
212
442
|
});
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (savedWorkspace) {
|
|
218
|
-
workspaceId = savedWorkspace.id;
|
|
219
|
-
}
|
|
220
|
-
else if (workspaces.length === 0) {
|
|
221
|
-
if (opts.json) {
|
|
222
|
-
console.log(JSON.stringify({
|
|
223
|
-
error: "no_workspaces",
|
|
224
|
-
message: "No workspaces found. Create one at inflight.co first.",
|
|
225
|
-
}));
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
229
|
-
}
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
else if (workspaces.length === 1) {
|
|
233
|
-
workspaceId = workspaces[0].id;
|
|
234
|
-
if (!opts.json)
|
|
235
|
-
p.log.success(`Workspace: ${pc.bold(workspaces[0].name)}`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
if (opts.json) {
|
|
239
|
-
console.log(JSON.stringify({
|
|
240
|
-
error: "no_workspace_set",
|
|
241
|
-
message: "Multiple workspaces found. Run 'inflight workspace --set=ID' first.",
|
|
242
|
-
workspaces: workspaces.map((w) => ({ id: w.id, name: w.name })),
|
|
243
|
-
}));
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
const selected = await p.select({
|
|
247
|
-
message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
|
|
248
|
-
options: workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
249
|
-
});
|
|
250
|
-
if (p.isCancel(selected)) {
|
|
251
|
-
p.cancel("Cancelled.");
|
|
252
|
-
process.exit(0);
|
|
253
|
-
}
|
|
254
|
-
workspaceId = selected;
|
|
255
|
-
}
|
|
256
|
-
writeWorkspaceConfig({ workspaceId });
|
|
443
|
+
const workspaceId = await resolveWorkspace(me.workspaces, {
|
|
444
|
+
explicitId: opts.workspace,
|
|
445
|
+
commandForNext: "inflight share",
|
|
446
|
+
});
|
|
257
447
|
// ── Fast path: URL provided (agent / scripting) ──
|
|
258
448
|
if (opts.url) {
|
|
259
449
|
let stagingUrl = opts.url;
|
|
@@ -264,6 +454,8 @@ export async function shareCommand(opts = {}) {
|
|
|
264
454
|
const message = stagingUrl.includes("localhost")
|
|
265
455
|
? "Inflight needs a hosted URL — localhost isn't accessible to your team. Deploy to Vercel, Netlify, or another hosting provider first."
|
|
266
456
|
: "Must be a hosted URL with a domain (e.g., my-branch.vercel.app)";
|
|
457
|
+
if (isAgent)
|
|
458
|
+
agentError({ type: "invalid_url", message });
|
|
267
459
|
if (opts.json) {
|
|
268
460
|
console.log(JSON.stringify({ error: "invalid_url", message }));
|
|
269
461
|
}
|
|
@@ -277,7 +469,10 @@ export async function shareCommand(opts = {}) {
|
|
|
277
469
|
workspaceId,
|
|
278
470
|
stagingUrl,
|
|
279
471
|
gitInfo,
|
|
472
|
+
...(opts.project && opts.project !== "new" && { projectId: opts.project }),
|
|
280
473
|
}).catch((e) => {
|
|
474
|
+
if (isAgent)
|
|
475
|
+
agentError({ type: "create_failed", message: e.message });
|
|
281
476
|
if (opts.json) {
|
|
282
477
|
console.log(JSON.stringify({ error: "create_failed", message: e.message }));
|
|
283
478
|
}
|
|
@@ -286,6 +481,8 @@ export async function shareCommand(opts = {}) {
|
|
|
286
481
|
}
|
|
287
482
|
process.exit(1);
|
|
288
483
|
});
|
|
484
|
+
if (isAgent)
|
|
485
|
+
agentSuccess({ stagingUrl, ...result });
|
|
289
486
|
if (opts.json) {
|
|
290
487
|
console.log(JSON.stringify({ success: true, stagingUrl, ...result }));
|
|
291
488
|
}
|
|
@@ -296,6 +493,11 @@ export async function shareCommand(opts = {}) {
|
|
|
296
493
|
await open(stagingUrl);
|
|
297
494
|
return;
|
|
298
495
|
}
|
|
496
|
+
// ── Agent mode: structured flow with action_required for every choice ──
|
|
497
|
+
if (isAgent) {
|
|
498
|
+
await agentShareFlow(cwd, auth.apiKey, workspaceId, opts);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
299
501
|
// Resolve staging URL
|
|
300
502
|
const providerChoice = await p.select({
|
|
301
503
|
message: "Where is your staging URL hosted?",
|
|
@@ -366,17 +568,15 @@ export async function shareCommand(opts = {}) {
|
|
|
366
568
|
const projectChoice = await scrollableSelect({
|
|
367
569
|
message: "Create a new project in Inflight, or update an existing one?",
|
|
368
570
|
maxItems: 5,
|
|
571
|
+
...(branchMatch && { initialValue: branchMatch.projectId }),
|
|
369
572
|
options: [
|
|
370
573
|
{ value: "new", label: "Start a new project" },
|
|
371
574
|
...sortedProjects.map((proj) => ({
|
|
372
575
|
value: proj.projectId,
|
|
373
576
|
label: `"${proj.latestVersion.title}"`,
|
|
374
|
-
hint:
|
|
375
|
-
formatRelativeTime(proj.latestVersion.createdAt)
|
|
376
|
-
proj.latestVersion.
|
|
377
|
-
]
|
|
378
|
-
.filter(Boolean)
|
|
379
|
-
.join(" · "),
|
|
577
|
+
hint: proj.latestVersion.branch === currentBranch
|
|
578
|
+
? `${formatRelativeTime(proj.latestVersion.createdAt)} · current branch (${currentBranch})`
|
|
579
|
+
: formatRelativeTime(proj.latestVersion.createdAt),
|
|
380
580
|
})),
|
|
381
581
|
],
|
|
382
582
|
});
|
package/dist/index.js
CHANGED
|
@@ -18,13 +18,24 @@ program
|
|
|
18
18
|
.description("Get feedback directly on your staging URL")
|
|
19
19
|
.version(version)
|
|
20
20
|
.enablePositionalOptions();
|
|
21
|
-
program
|
|
21
|
+
program
|
|
22
|
+
.command("setup")
|
|
23
|
+
.description("Set up Inflight in your project")
|
|
24
|
+
.option("--json", "Output as JSON (auto-enabled for non-TTY)")
|
|
25
|
+
.option("--workspace <id>", "Workspace ID (skip selection)")
|
|
26
|
+
.action((opts) => setupCommand(opts));
|
|
22
27
|
program.command("login").description("Authenticate with your Inflight account").action(loginCommand);
|
|
23
28
|
program
|
|
24
29
|
.command("share")
|
|
25
30
|
.description("Get feedback on your staging URL")
|
|
26
31
|
.option("--url <url>", "Staging URL (skips provider selection)")
|
|
27
|
-
.option("--json", "Output result as JSON")
|
|
32
|
+
.option("--json", "Output result as JSON (auto-enabled for non-TTY)")
|
|
33
|
+
.option("--workspace <id>", "Workspace ID (skip selection)")
|
|
34
|
+
.option("--project <id>", "Project ID, or 'new' to create")
|
|
35
|
+
.option("--provider <id>", "Deployment provider: vercel, netlify")
|
|
36
|
+
.option("--deployment <url>", "Specific deployment URL")
|
|
37
|
+
.option("--override", "Override latest version instead of creating new")
|
|
38
|
+
.option("--skip-git-check", "Skip git state check (use after agent handled git)")
|
|
28
39
|
.action((opts) => shareCommand(opts));
|
|
29
40
|
program
|
|
30
41
|
.command("workspace")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent mode detection and structured JSON output.
|
|
3
|
+
*
|
|
4
|
+
* When !process.stdout.isTTY (piped output, agent calling CLI),
|
|
5
|
+
* all interactive prompts are replaced with auto-resolution or
|
|
6
|
+
* structured JSON responses the agent can parse and act on.
|
|
7
|
+
*/
|
|
8
|
+
export declare const isAgent: boolean;
|
|
9
|
+
export interface AgentChoice {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function agentSuccess(data: Record<string, unknown>): never;
|
|
15
|
+
export declare function agentActionRequired(opts: {
|
|
16
|
+
type: string;
|
|
17
|
+
message: string;
|
|
18
|
+
choices: AgentChoice[];
|
|
19
|
+
nextCommand: string;
|
|
20
|
+
}): never;
|
|
21
|
+
export declare function agentError(opts: {
|
|
22
|
+
type: string;
|
|
23
|
+
message: string;
|
|
24
|
+
suggestion?: string;
|
|
25
|
+
}): never;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent mode detection and structured JSON output.
|
|
3
|
+
*
|
|
4
|
+
* When !process.stdout.isTTY (piped output, agent calling CLI),
|
|
5
|
+
* all interactive prompts are replaced with auto-resolution or
|
|
6
|
+
* structured JSON responses the agent can parse and act on.
|
|
7
|
+
*/
|
|
8
|
+
export const isAgent = !process.stdout.isTTY;
|
|
9
|
+
export function agentSuccess(data) {
|
|
10
|
+
console.log(JSON.stringify({ status: "success", data }));
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
export function agentActionRequired(opts) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
status: "action_required",
|
|
16
|
+
type: opts.type,
|
|
17
|
+
message: opts.message,
|
|
18
|
+
choices: opts.choices,
|
|
19
|
+
next_command: opts.nextCommand,
|
|
20
|
+
}));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
export function agentError(opts) {
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
status: "error",
|
|
26
|
+
type: opts.type,
|
|
27
|
+
message: opts.message,
|
|
28
|
+
...(opts.suggestion && { suggestion: opts.suggestion }),
|
|
29
|
+
}));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
package/dist/lib/framework.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getGitRoot } from "./git.js";
|
|
|
5
5
|
const CANDIDATE_PATTERNS = [
|
|
6
6
|
"package.json",
|
|
7
7
|
"index.html",
|
|
8
|
+
"src/index.html",
|
|
8
9
|
"app/layout.tsx",
|
|
9
10
|
"app/layout.jsx",
|
|
10
11
|
"src/app/layout.tsx",
|
|
@@ -16,8 +17,13 @@ const CANDIDATE_PATTERNS = [
|
|
|
16
17
|
"app/root.tsx",
|
|
17
18
|
"app/root.jsx",
|
|
18
19
|
"src/app.html",
|
|
20
|
+
"src/routes/+layout.svelte",
|
|
19
21
|
"nuxt.config.ts",
|
|
20
22
|
"nuxt.config.js",
|
|
23
|
+
"app.vue",
|
|
24
|
+
"layouts/default.vue",
|
|
25
|
+
"gatsby-ssr.tsx",
|
|
26
|
+
"gatsby-ssr.jsx",
|
|
21
27
|
"src/layouts/Layout.astro",
|
|
22
28
|
"src/layouts/BaseLayout.astro",
|
|
23
29
|
];
|
|
@@ -55,7 +61,7 @@ export function gatherProjectContext(cwd) {
|
|
|
55
61
|
fileTree.push(relative(root, join(dir, entry)));
|
|
56
62
|
}
|
|
57
63
|
// Also list key subdirectories
|
|
58
|
-
for (const sub of ["app", "src", "src/app", "src/layouts", "pages", "src/pages"]) {
|
|
64
|
+
for (const sub of ["app", "src", "src/app", "src/layouts", "src/routes", "pages", "src/pages", "layouts"]) {
|
|
59
65
|
const subDir = join(dir, sub);
|
|
60
66
|
if (!existsSync(subDir))
|
|
61
67
|
continue;
|
package/dist/lib/netlify.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export declare function ensureNetlifyCli(log?: (msg: string) => void): Promise<
|
|
1
|
+
export declare function ensureNetlifyCli(log?: (msg: string) => void): Promise<{
|
|
2
|
+
ok: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}>;
|
|
2
5
|
/**
|
|
3
6
|
* Gets the Netlify auth token non-interactively.
|
|
4
7
|
* Priority: NETLIFY_AUTH_TOKEN env var → CLI config file.
|
package/dist/lib/netlify.js
CHANGED
|
@@ -16,14 +16,19 @@ function hasNetlifyCli() {
|
|
|
16
16
|
}
|
|
17
17
|
export async function ensureNetlifyCli(log) {
|
|
18
18
|
if (hasNetlifyCli())
|
|
19
|
-
return true;
|
|
19
|
+
return { ok: true };
|
|
20
20
|
log?.("Installing Netlify CLI...");
|
|
21
21
|
try {
|
|
22
22
|
await execAsync("npm install -g netlify-cli");
|
|
23
|
-
return true;
|
|
23
|
+
return { ok: true };
|
|
24
24
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
25
|
+
catch (e) {
|
|
26
|
+
const stderr = e && typeof e === "object" && "stderr" in e && typeof e.stderr === "string"
|
|
27
|
+
? e.stderr
|
|
28
|
+
: e instanceof Error
|
|
29
|
+
? e.message
|
|
30
|
+
: "Unknown error";
|
|
31
|
+
return { ok: false, error: stderr };
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Workspace } from "./api.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the active workspace ID.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. Explicit --workspace flag
|
|
7
|
+
* 2. Saved config from prior session
|
|
8
|
+
* 3. Single workspace → auto-select
|
|
9
|
+
* 4. Multiple → action_required (agent) or p.select (human)
|
|
10
|
+
* 5. Zero → error
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveWorkspace(workspaces: Workspace[], opts?: {
|
|
13
|
+
explicitId?: string;
|
|
14
|
+
commandForNext?: string;
|
|
15
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// apps/cli/src/lib/resolve-workspace.ts
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { isAgent, agentActionRequired, agentError } from "./agent.js";
|
|
5
|
+
import { readWorkspaceConfig, writeWorkspaceConfig } from "./config.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the active workspace ID.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. Explicit --workspace flag
|
|
11
|
+
* 2. Saved config from prior session
|
|
12
|
+
* 3. Single workspace → auto-select
|
|
13
|
+
* 4. Multiple → action_required (agent) or p.select (human)
|
|
14
|
+
* 5. Zero → error
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveWorkspace(workspaces, opts = {}) {
|
|
17
|
+
const { explicitId, commandForNext = "inflight share" } = opts;
|
|
18
|
+
// Explicit flag
|
|
19
|
+
if (explicitId) {
|
|
20
|
+
const match = workspaces.find((w) => w.id === explicitId);
|
|
21
|
+
if (!match) {
|
|
22
|
+
if (isAgent) {
|
|
23
|
+
agentError({
|
|
24
|
+
type: "invalid_workspace",
|
|
25
|
+
message: `Workspace "${explicitId}" not found.`,
|
|
26
|
+
suggestion: `Valid IDs: ${workspaces.map((w) => w.id).join(", ")}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
p.log.error(`Workspace "${explicitId}" not found.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
writeWorkspaceConfig({ workspaceId: match.id });
|
|
33
|
+
return match.id;
|
|
34
|
+
}
|
|
35
|
+
// Saved config — return silently (avoids duplicate log when setup calls share)
|
|
36
|
+
const savedConfig = readWorkspaceConfig();
|
|
37
|
+
const savedWorkspace = savedConfig ? workspaces.find((w) => w.id === savedConfig.workspaceId) : null;
|
|
38
|
+
if (savedWorkspace) {
|
|
39
|
+
return savedWorkspace.id;
|
|
40
|
+
}
|
|
41
|
+
// Zero
|
|
42
|
+
if (workspaces.length === 0) {
|
|
43
|
+
if (isAgent) {
|
|
44
|
+
agentError({ type: "no_workspaces", message: "No workspaces found. Create one at inflight.co first." });
|
|
45
|
+
}
|
|
46
|
+
p.log.error("No workspaces found. Create one at " + pc.cyan("inflight.co") + " first.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// Single → auto-select
|
|
50
|
+
if (workspaces.length === 1) {
|
|
51
|
+
const ws = workspaces[0];
|
|
52
|
+
writeWorkspaceConfig({ workspaceId: ws.id });
|
|
53
|
+
if (!isAgent)
|
|
54
|
+
p.log.success(`Workspace: ${pc.bold(ws.name)}`);
|
|
55
|
+
return ws.id;
|
|
56
|
+
}
|
|
57
|
+
// Multiple
|
|
58
|
+
if (isAgent) {
|
|
59
|
+
agentActionRequired({
|
|
60
|
+
type: "choose_workspace",
|
|
61
|
+
message: "Multiple workspaces found. Re-run with --workspace <id>.",
|
|
62
|
+
choices: workspaces.map((w) => ({ id: w.id, label: w.name })),
|
|
63
|
+
nextCommand: `${commandForNext} --workspace <ID>`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const selected = await p.select({
|
|
67
|
+
message: "Select a workspace " + pc.dim("(change anytime with inflight workspace)"),
|
|
68
|
+
options: workspaces.map((w) => ({ value: w.id, label: w.name })),
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(selected)) {
|
|
71
|
+
p.cancel("Cancelled.");
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
const workspaceId = selected;
|
|
75
|
+
writeWorkspaceConfig({ workspaceId });
|
|
76
|
+
return workspaceId;
|
|
77
|
+
}
|