@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.
- package/dist/project.js +71 -189
- 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 {
|
|
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(
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
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 (
|
|
213
|
-
payload.orgSlug =
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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.
|
|
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
|
},
|