@instafy/cli 0.1.8-staging.373 → 0.1.8-staging.375

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/auth.js CHANGED
@@ -35,6 +35,26 @@ function normalizeToken(raw) {
35
35
  }
36
36
  return trimmed;
37
37
  }
38
+ async function loginWithPassword(params) {
39
+ const response = await fetch(`${params.supabaseUrl}/auth/v1/token?grant_type=password`, {
40
+ method: "POST",
41
+ headers: {
42
+ apikey: params.supabaseAnonKey,
43
+ "content-type": "application/json",
44
+ },
45
+ body: JSON.stringify({ email: params.email, password: params.password }),
46
+ });
47
+ if (!response.ok) {
48
+ const text = await response.text().catch(() => "");
49
+ throw new Error(`Supabase login failed (${response.status}): ${text}`);
50
+ }
51
+ const body = (await response.json());
52
+ const accessToken = typeof body["access_token"] === "string" ? body["access_token"] : null;
53
+ if (!accessToken) {
54
+ throw new Error("Supabase login response missing access_token");
55
+ }
56
+ return accessToken;
57
+ }
38
58
  function looksLikeLocalControllerUrl(controllerUrl) {
39
59
  try {
40
60
  const parsed = new URL(controllerUrl);
@@ -261,9 +281,29 @@ export async function login(options) {
261
281
  }));
262
282
  return;
263
283
  }
264
- let callbackServer = null;
284
+ const existing = resolveUserAccessToken({ profile });
265
285
  const provided = normalizeToken(options.token ?? null);
266
- if (!provided) {
286
+ let token = provided;
287
+ let usedPasswordGrant = false;
288
+ if (!token) {
289
+ const email = normalizeToken(options.email ?? null) ?? normalizeToken(process.env["INSTAFY_LOGIN_EMAIL"] ?? null);
290
+ const password = normalizeToken(options.password ?? null) ??
291
+ normalizeToken(process.env["INSTAFY_LOGIN_PASSWORD"] ?? null);
292
+ if (email && password) {
293
+ const supabaseUrl = normalizeUrl(process.env["SUPABASE_URL"] ?? null) ??
294
+ normalizeUrl(process.env["VITE_SUPABASE_URL"] ?? null) ??
295
+ normalizeUrl(process.env["SUPABASE_PROJECT_URL"] ?? null);
296
+ const supabaseAnonKey = normalizeToken(process.env["SUPABASE_ANON_KEY"] ?? null) ??
297
+ normalizeToken(process.env["VITE_SUPABASE_ANON_KEY"] ?? null);
298
+ if (!supabaseUrl || !supabaseAnonKey) {
299
+ throw new Error("Email/password login requires Supabase env.\n\nSet:\n- SUPABASE_URL + SUPABASE_ANON_KEY (or VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY)\n\nThen retry: instafy login --email <email> --password <password>");
300
+ }
301
+ token = await loginWithPassword({ supabaseUrl, supabaseAnonKey, email, password });
302
+ usedPasswordGrant = true;
303
+ }
304
+ }
305
+ let callbackServer = null;
306
+ if (!token) {
267
307
  try {
268
308
  callbackServer = await startCliLoginCallbackServer();
269
309
  url.searchParams.set("cliCallbackUrl", callbackServer.callbackUrl);
@@ -275,19 +315,23 @@ export async function login(options) {
275
315
  }
276
316
  console.log(kleur.green("Instafy CLI login"));
277
317
  console.log("");
278
- console.log("1) Open this URL in your browser:");
279
- console.log(kleur.cyan(url.toString()));
280
- console.log("");
281
- if (callbackServer) {
282
- console.log("2) Sign in — this terminal should continue automatically.");
283
- console.log(kleur.gray("If it doesn't, copy the token shown on that page and paste it here."));
318
+ if (!token) {
319
+ console.log("1) Open this URL in your browser:");
320
+ console.log(kleur.cyan(url.toString()));
321
+ console.log("");
322
+ if (callbackServer) {
323
+ console.log("2) Sign in this terminal should continue automatically.");
324
+ console.log(kleur.gray("If it doesn't, copy the token shown on that page and paste it here."));
325
+ }
326
+ else {
327
+ console.log("2) After you sign in, copy the token shown on that page.");
328
+ }
329
+ console.log("");
284
330
  }
285
- else {
286
- console.log("2) After you sign in, copy the token shown on that page.");
331
+ else if (usedPasswordGrant) {
332
+ console.log(kleur.gray("Authenticated via email/password."));
333
+ console.log("");
287
334
  }
288
- console.log("");
289
- const existing = resolveUserAccessToken({ profile });
290
- let token = provided;
291
335
  if (!token && callbackServer) {
292
336
  if (input.isTTY) {
293
337
  console.log(kleur.gray("Waiting for browser login…"));
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ program
43
43
  .option("--server-url <url>", "Instafy server/controller URL")
44
44
  .option("--profile <name>", "Save token under a named profile (multi-account support)")
45
45
  .option("--token <token>", "Provide token directly (skips prompt)")
46
+ .option("--email <email>", "Email for non-interactive login (requires SUPABASE_URL + SUPABASE_ANON_KEY)")
47
+ .option("--password <password>", "Password for non-interactive login (requires SUPABASE_URL + SUPABASE_ANON_KEY)")
46
48
  .option("--no-git-setup", "Do not configure git credential helper")
47
49
  .option("--no-store", "Do not save token to ~/.instafy/config.json")
48
50
  .option("--json", "Output JSON")
@@ -52,6 +54,8 @@ program
52
54
  controllerUrl: opts.serverUrl,
53
55
  studioUrl: opts.studioUrl,
54
56
  token: opts.token,
57
+ email: opts.email,
58
+ password: opts.password,
55
59
  gitSetup: opts.gitSetup,
56
60
  noStore: opts.store === false,
57
61
  profile: opts.profile,
package/dist/project.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
- import { createInterface } from "node:readline/promises";
5
- import { emitKeypressEvents } from "node:readline";
6
- import { stdin as input, stdout as output } from "node:process";
4
+ import { stdin as input } from "node:process";
7
5
  import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, } from "./config.js";
8
6
  import { formatAuthRequiredError } from "./errors.js";
9
7
  import { findProjectManifest } from "./project-manifest.js";
8
+ let promptsModule = null;
9
+ async function loadPrompts() {
10
+ promptsModule ?? (promptsModule = import("@clack/prompts"));
11
+ return promptsModule;
12
+ }
10
13
  async function fetchOrganizations(controllerUrl, token) {
11
14
  const response = await fetch(`${controllerUrl}/orgs`, {
12
15
  headers: {
@@ -33,143 +36,6 @@ async function fetchOrgProjects(controllerUrl, token, orgId) {
33
36
  const body = (await response.json());
34
37
  return Array.isArray(body.projects) ? body.projects : [];
35
38
  }
36
- async function promptSelectIndex(params) {
37
- const ttyInput = input;
38
- const isRawModeAvailable = typeof ttyInput.setRawMode === "function";
39
- const canUseArrowKeys = Boolean(input.isTTY && output.isTTY && isRawModeAvailable && process.env.TERM !== "dumb");
40
- if (!canUseArrowKeys) {
41
- const rl = createInterface({ input, output });
42
- try {
43
- while (true) {
44
- const raw = (await rl.question(`Select option (default ${params.defaultIndex}): `)).trim();
45
- if (!raw) {
46
- return params.defaultIndex;
47
- }
48
- const selected = Number(raw);
49
- if (!Number.isFinite(selected) || selected < 0 || selected >= params.options.length) {
50
- console.log(kleur.red("Invalid selection."));
51
- continue;
52
- }
53
- return selected;
54
- }
55
- }
56
- finally {
57
- rl.close();
58
- }
59
- }
60
- return await new Promise((resolve, reject) => {
61
- const options = params.options;
62
- const defaultIndex = Math.min(Math.max(params.defaultIndex, 0), Math.max(0, options.length - 1));
63
- let selectedIndex = defaultIndex;
64
- let renderedLineCount = 0;
65
- let typedDigits = "";
66
- let typedResetTimeout = null;
67
- let completed = false;
68
- const previousRawMode = Boolean(ttyInput.isRaw);
69
- const render = (hintOverride) => {
70
- const hint = hintOverride ?? "Use ↑/↓ to move, Enter to select, or type a number.";
71
- const lines = [
72
- kleur.yellow(`${params.title}:`),
73
- ...options.map((label, index) => {
74
- const prefix = index === selectedIndex ? kleur.cyan(">") : " ";
75
- const number = kleur.gray(`${index})`);
76
- return ` ${prefix} ${number} ${label}`;
77
- }),
78
- kleur.gray(hint),
79
- ];
80
- if (renderedLineCount > 0) {
81
- output.write(`\x1b[${renderedLineCount}A`);
82
- }
83
- for (const line of lines) {
84
- output.write(`\x1b[2K\r${line}\n`);
85
- }
86
- renderedLineCount = lines.length;
87
- };
88
- const cleanup = () => {
89
- if (completed)
90
- return;
91
- completed = true;
92
- input.off("keypress", onKeypress);
93
- if (typedResetTimeout) {
94
- clearTimeout(typedResetTimeout);
95
- typedResetTimeout = null;
96
- }
97
- try {
98
- ttyInput.setRawMode(previousRawMode);
99
- }
100
- catch {
101
- // ignore
102
- }
103
- output.write("\x1b[?25h"); // show cursor
104
- output.write("\n");
105
- };
106
- const onKeypress = (_chunk, key) => {
107
- if (!key)
108
- return;
109
- if (key.ctrl && key.name === "c") {
110
- cleanup();
111
- reject(new Error("Cancelled."));
112
- return;
113
- }
114
- if (_chunk && /^[0-9]$/.test(_chunk)) {
115
- typedDigits += _chunk;
116
- if (typedResetTimeout) {
117
- clearTimeout(typedResetTimeout);
118
- }
119
- typedResetTimeout = setTimeout(() => {
120
- typedDigits = "";
121
- render();
122
- }, 1200);
123
- const candidate = Number(typedDigits);
124
- if (Number.isFinite(candidate) && candidate >= 0 && candidate < options.length) {
125
- selectedIndex = candidate;
126
- }
127
- render(typedDigits ? `Number: ${typedDigits}` : undefined);
128
- return;
129
- }
130
- if (key.name === "backspace") {
131
- typedDigits = typedDigits.slice(0, -1);
132
- render(typedDigits ? `Number: ${typedDigits}` : undefined);
133
- return;
134
- }
135
- if (key.name === "up") {
136
- typedDigits = "";
137
- selectedIndex = selectedIndex <= 0 ? options.length - 1 : selectedIndex - 1;
138
- render();
139
- return;
140
- }
141
- if (key.name === "down") {
142
- typedDigits = "";
143
- selectedIndex = selectedIndex >= options.length - 1 ? 0 : selectedIndex + 1;
144
- render();
145
- return;
146
- }
147
- if (key.name === "return" || key.name === "enter") {
148
- if (typedDigits) {
149
- const candidate = Number(typedDigits);
150
- if (Number.isFinite(candidate) && candidate >= 0 && candidate < options.length) {
151
- selectedIndex = candidate;
152
- }
153
- }
154
- const picked = selectedIndex;
155
- cleanup();
156
- resolve(picked);
157
- }
158
- };
159
- try {
160
- emitKeypressEvents(input);
161
- ttyInput.setRawMode(true);
162
- output.write("\x1b[?25l"); // hide cursor
163
- input.on("keypress", onKeypress);
164
- input.resume();
165
- render();
166
- }
167
- catch (error) {
168
- cleanup();
169
- reject(error instanceof Error ? error : new Error(String(error)));
170
- }
171
- });
172
- }
173
39
  async function createOrganization(controllerUrl, token, payload) {
174
40
  const response = await fetch(`${controllerUrl}/orgs`, {
175
41
  method: "POST",
@@ -198,19 +64,32 @@ async function resolveOrg(controllerUrl, token, options) {
198
64
  const orgName = options.orgName?.trim() || null;
199
65
  const studioUrl = resolveConfiguredStudioUrl({ profile: options.profile ?? null }) ?? "https://staging.instafy.dev";
200
66
  const studioOrgUrl = `${studioUrl.replace(/\/$/, "")}/studio?panel=settings`;
201
- const allowInteractive = input.isTTY && options.json !== true;
202
- async function promptAndCreateOrg(rl) {
203
- const enteredName = (await rl.question("Organization name (default: Personal): ")).trim();
204
- const chosenName = enteredName || "Personal";
205
- const enteredSlug = (await rl.question("Organization slug (optional): ")).trim();
67
+ const allowInteractive = Boolean(input.isTTY && process.stdout.isTTY && options.json !== true && process.env.CI !== "true");
68
+ async function promptAndCreateOrg() {
69
+ const { isCancel, text } = await loadPrompts();
70
+ const enteredName = await text({
71
+ message: "Organization name",
72
+ defaultValue: "Personal",
73
+ });
74
+ if (isCancel(enteredName)) {
75
+ throw new Error("Cancelled.");
76
+ }
77
+ const chosenName = String(enteredName).trim() || "Personal";
78
+ const enteredSlug = await text({
79
+ message: "Organization slug (optional)",
80
+ });
81
+ if (isCancel(enteredSlug)) {
82
+ throw new Error("Cancelled.");
83
+ }
84
+ const chosenSlug = String(enteredSlug).trim();
206
85
  const payload = {
207
86
  orgName: chosenName,
208
87
  };
209
88
  if (options.ownerUserId) {
210
89
  payload.ownerUserId = options.ownerUserId;
211
90
  }
212
- if (enteredSlug) {
213
- payload.orgSlug = enteredSlug;
91
+ if (chosenSlug) {
92
+ payload.orgSlug = chosenSlug;
214
93
  }
215
94
  const created = await createOrganization(controllerUrl, token, payload);
216
95
  return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
@@ -226,21 +105,19 @@ async function resolveOrg(controllerUrl, token, options) {
226
105
  const orgs = await fetchOrganizations(controllerUrl, token);
227
106
  if (orgs.length === 0) {
228
107
  if (allowInteractive) {
108
+ const { confirm, isCancel } = await loadPrompts();
229
109
  console.log(kleur.yellow("No organizations found for this account."));
230
110
  console.log("");
231
111
  console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
232
112
  console.log("");
233
- const rl = createInterface({ input, output });
234
- try {
235
- const confirm = (await rl.question("Create a new organization now? (Y/n): ")).trim().toLowerCase();
236
- if (confirm && ["n", "no"].includes(confirm)) {
237
- throw new Error("No organization selected.");
238
- }
239
- return await promptAndCreateOrg(rl);
240
- }
241
- finally {
242
- rl.close();
113
+ const shouldCreate = await confirm({
114
+ message: "Create a new organization now?",
115
+ initialValue: true,
116
+ });
117
+ if (isCancel(shouldCreate) || !shouldCreate) {
118
+ throw new Error("No organization selected.");
243
119
  }
120
+ return await promptAndCreateOrg();
244
121
  }
245
122
  throw new Error(`No organizations found.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
246
123
  }
@@ -248,48 +125,53 @@ async function resolveOrg(controllerUrl, token, options) {
248
125
  return { orgId: orgs[0].id, orgName: orgs[0].name ?? null };
249
126
  }
250
127
  if (allowInteractive) {
251
- const options = [
252
- "+ Create a new organization",
253
- ...orgs.map((org) => `${org.name}${org.slug ? ` · ${org.slug}` : ""} (${org.id})`),
254
- ];
255
- const pickedIndex = await promptSelectIndex({
256
- title: "Choose an organization for this project",
257
- options,
258
- defaultIndex: 1,
128
+ const { isCancel, select } = await loadPrompts();
129
+ const selection = await select({
130
+ message: "Choose an organization for this project",
131
+ options: [
132
+ { value: "__create__", label: "+ Create a new organization" },
133
+ ...orgs.map((org) => ({
134
+ value: org.id,
135
+ label: org.name,
136
+ hint: `${org.slug ? `${org.slug} · ` : ""}${org.id}`,
137
+ })),
138
+ ],
139
+ initialValue: orgs[0].id,
259
140
  });
260
- if (pickedIndex === 0) {
261
- const rl = createInterface({ input, output });
262
- try {
263
- return await promptAndCreateOrg(rl);
264
- }
265
- finally {
266
- rl.close();
267
- }
141
+ if (isCancel(selection)) {
142
+ throw new Error("Cancelled.");
143
+ }
144
+ if (selection === "__create__") {
145
+ return await promptAndCreateOrg();
146
+ }
147
+ const pickedOrg = orgs.find((org) => org.id === selection);
148
+ if (!pickedOrg) {
149
+ throw new Error("Selected organization not found.");
268
150
  }
269
- const pickedOrg = orgs[pickedIndex - 1];
270
151
  return { orgId: pickedOrg.id, orgName: pickedOrg.name ?? null };
271
152
  }
272
153
  throw new Error("Multiple organizations found.\n\nNext:\n- instafy org list\n- instafy project init --org-id <uuid>");
273
154
  }
274
155
  if (orgSlug && !orgName) {
275
156
  if (allowInteractive) {
276
- const rl = createInterface({ input, output });
277
- try {
278
- console.log(kleur.yellow("Organization slug did not match an existing org."));
279
- console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
280
- console.log("");
281
- const enteredName = (await rl.question("Organization name (default: Personal): ")).trim();
282
- const chosenName = enteredName || "Personal";
283
- const payload = { orgName: chosenName, orgSlug };
284
- if (options.ownerUserId) {
285
- payload.ownerUserId = options.ownerUserId;
286
- }
287
- const created = await createOrganization(controllerUrl, token, payload);
288
- return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
157
+ const { isCancel, text } = await loadPrompts();
158
+ console.log(kleur.yellow("Organization slug did not match an existing org."));
159
+ console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
160
+ console.log("");
161
+ const enteredName = await text({
162
+ message: "Organization name",
163
+ defaultValue: "Personal",
164
+ });
165
+ if (isCancel(enteredName)) {
166
+ throw new Error("Cancelled.");
289
167
  }
290
- finally {
291
- rl.close();
168
+ const chosenName = String(enteredName).trim() || "Personal";
169
+ const payload = { orgName: chosenName, orgSlug };
170
+ if (options.ownerUserId) {
171
+ payload.ownerUserId = options.ownerUserId;
292
172
  }
173
+ const created = await createOrganization(controllerUrl, token, payload);
174
+ return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
293
175
  }
294
176
  throw new Error(`Organization slug "${orgSlug}" did not match an existing org, and org name is required to create one.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\" --org-slug "${orgSlug}"`);
295
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.8-staging.373",
3
+ "version": "0.1.8-staging.375",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -24,6 +24,7 @@
24
24
  "test:live": "pnpm build && TUNNEL_E2E_LIVE=1 vitest run test/tunnel-live.e2e.spec.ts"
25
25
  },
26
26
  "dependencies": {
27
+ "@clack/prompts": "^0.11.0",
27
28
  "commander": "^12.1.0",
28
29
  "kleur": "^4.1.5"
29
30
  },