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

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.
Files changed (2) hide show
  1. package/dist/project.js +71 -189
  2. package/package.json +2 -1
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.374",
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
  },