@instafy/cli 0.1.8-staging.371 → 0.1.8-staging.373
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 +225 -106
- package/package.json +1 -1
package/dist/project.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { emitKeypressEvents } from "node:readline";
|
|
5
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
7
|
import { getInstafyProfileConfigPath, resolveConfiguredStudioUrl, resolveControllerUrl, resolveUserAccessToken, } from "./config.js";
|
|
7
8
|
import { formatAuthRequiredError } from "./errors.js";
|
|
@@ -32,6 +33,163 @@ async function fetchOrgProjects(controllerUrl, token, orgId) {
|
|
|
32
33
|
const body = (await response.json());
|
|
33
34
|
return Array.isArray(body.projects) ? body.projects : [];
|
|
34
35
|
}
|
|
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
|
+
async function createOrganization(controllerUrl, token, payload) {
|
|
174
|
+
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: {
|
|
177
|
+
authorization: `Bearer ${token}`,
|
|
178
|
+
"content-type": "application/json",
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(payload),
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
const text = await response.text().catch(() => "");
|
|
184
|
+
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
185
|
+
}
|
|
186
|
+
const json = (await response.json());
|
|
187
|
+
const orgId = json.org_id ?? json.orgId ?? json.id;
|
|
188
|
+
if (!orgId) {
|
|
189
|
+
throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
|
|
190
|
+
}
|
|
191
|
+
return { orgId, orgName: json.org_name ?? json.orgName ?? null };
|
|
192
|
+
}
|
|
35
193
|
async function resolveOrg(controllerUrl, token, options) {
|
|
36
194
|
if (options.orgId) {
|
|
37
195
|
return { orgId: options.orgId, orgName: options.orgName ?? null };
|
|
@@ -40,6 +198,23 @@ async function resolveOrg(controllerUrl, token, options) {
|
|
|
40
198
|
const orgName = options.orgName?.trim() || null;
|
|
41
199
|
const studioUrl = resolveConfiguredStudioUrl({ profile: options.profile ?? null }) ?? "https://staging.instafy.dev";
|
|
42
200
|
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();
|
|
206
|
+
const payload = {
|
|
207
|
+
orgName: chosenName,
|
|
208
|
+
};
|
|
209
|
+
if (options.ownerUserId) {
|
|
210
|
+
payload.ownerUserId = options.ownerUserId;
|
|
211
|
+
}
|
|
212
|
+
if (enteredSlug) {
|
|
213
|
+
payload.orgSlug = enteredSlug;
|
|
214
|
+
}
|
|
215
|
+
const created = await createOrganization(controllerUrl, token, payload);
|
|
216
|
+
return { orgId: created.orgId, orgName: created.orgName ?? chosenName };
|
|
217
|
+
}
|
|
43
218
|
if (orgSlug) {
|
|
44
219
|
const orgs = await fetchOrganizations(controllerUrl, token);
|
|
45
220
|
const matches = orgs.filter((org) => org.slug === orgSlug);
|
|
@@ -49,11 +224,8 @@ async function resolveOrg(controllerUrl, token, options) {
|
|
|
49
224
|
}
|
|
50
225
|
if (!orgSlug && !orgName) {
|
|
51
226
|
const orgs = await fetchOrganizations(controllerUrl, token);
|
|
52
|
-
if (orgs.length === 1) {
|
|
53
|
-
return { orgId: orgs[0].id, orgName: orgs[0].name ?? null };
|
|
54
|
-
}
|
|
55
227
|
if (orgs.length === 0) {
|
|
56
|
-
if (
|
|
228
|
+
if (allowInteractive) {
|
|
57
229
|
console.log(kleur.yellow("No organizations found for this account."));
|
|
58
230
|
console.log("");
|
|
59
231
|
console.log(`Create one in Studio: ${kleur.cyan(studioOrgUrl)}`);
|
|
@@ -64,34 +236,7 @@ async function resolveOrg(controllerUrl, token, options) {
|
|
|
64
236
|
if (confirm && ["n", "no"].includes(confirm)) {
|
|
65
237
|
throw new Error("No organization selected.");
|
|
66
238
|
}
|
|
67
|
-
|
|
68
|
-
const chosenName = enteredName || "Personal";
|
|
69
|
-
const enteredSlug = (await rl.question("Organization slug (optional): ")).trim();
|
|
70
|
-
const payload = {
|
|
71
|
-
orgName: chosenName,
|
|
72
|
-
ownerUserId: options.ownerUserId,
|
|
73
|
-
};
|
|
74
|
-
if (enteredSlug) {
|
|
75
|
-
payload.orgSlug = enteredSlug;
|
|
76
|
-
}
|
|
77
|
-
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
authorization: `Bearer ${token}`,
|
|
81
|
-
"content-type": "application/json",
|
|
82
|
-
},
|
|
83
|
-
body: JSON.stringify(payload),
|
|
84
|
-
});
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
const text = await response.text().catch(() => "");
|
|
87
|
-
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
88
|
-
}
|
|
89
|
-
const json = (await response.json());
|
|
90
|
-
const createdId = json.org_id ?? json.orgId ?? json.id;
|
|
91
|
-
if (!createdId) {
|
|
92
|
-
throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
|
|
93
|
-
}
|
|
94
|
-
return { orgId: createdId, orgName: json.org_name ?? json.orgName ?? chosenName };
|
|
239
|
+
return await promptAndCreateOrg(rl);
|
|
95
240
|
}
|
|
96
241
|
finally {
|
|
97
242
|
rl.close();
|
|
@@ -99,96 +244,70 @@ async function resolveOrg(controllerUrl, token, options) {
|
|
|
99
244
|
}
|
|
100
245
|
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\"`);
|
|
101
246
|
}
|
|
102
|
-
if (orgs.length
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
247
|
+
if (orgs.length === 1) {
|
|
248
|
+
return { orgId: orgs[0].id, orgName: orgs[0].name ?? null };
|
|
249
|
+
}
|
|
250
|
+
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,
|
|
259
|
+
});
|
|
260
|
+
if (pickedIndex === 0) {
|
|
111
261
|
const rl = createInterface({ input, output });
|
|
112
262
|
try {
|
|
113
|
-
|
|
114
|
-
const answer = (await rl.question("Select org number: ")).trim();
|
|
115
|
-
const selected = Number(answer);
|
|
116
|
-
if (selected === 0) {
|
|
117
|
-
const enteredName = (await rl.question("Organization name (default: Personal): ")).trim();
|
|
118
|
-
const chosenName = enteredName || "Personal";
|
|
119
|
-
const enteredSlug = (await rl.question("Organization slug (optional): ")).trim();
|
|
120
|
-
const payload = {
|
|
121
|
-
orgName: chosenName,
|
|
122
|
-
ownerUserId: options.ownerUserId,
|
|
123
|
-
};
|
|
124
|
-
if (enteredSlug) {
|
|
125
|
-
payload.orgSlug = enteredSlug;
|
|
126
|
-
}
|
|
127
|
-
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
128
|
-
method: "POST",
|
|
129
|
-
headers: {
|
|
130
|
-
authorization: `Bearer ${token}`,
|
|
131
|
-
"content-type": "application/json",
|
|
132
|
-
},
|
|
133
|
-
body: JSON.stringify(payload),
|
|
134
|
-
});
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
const text = await response.text().catch(() => "");
|
|
137
|
-
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
138
|
-
}
|
|
139
|
-
const json = (await response.json());
|
|
140
|
-
const createdId = json.org_id ?? json.orgId ?? json.id;
|
|
141
|
-
if (!createdId) {
|
|
142
|
-
throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
|
|
143
|
-
}
|
|
144
|
-
return { orgId: createdId, orgName: json.org_name ?? json.orgName ?? chosenName };
|
|
145
|
-
}
|
|
146
|
-
if (!Number.isFinite(selected) || selected < 1 || selected > orgs.length) {
|
|
147
|
-
console.log(kleur.red("Invalid selection."));
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const picked = orgs[selected - 1];
|
|
151
|
-
return { orgId: picked.id, orgName: picked.name ?? null };
|
|
152
|
-
}
|
|
263
|
+
return await promptAndCreateOrg(rl);
|
|
153
264
|
}
|
|
154
265
|
finally {
|
|
155
266
|
rl.close();
|
|
156
267
|
}
|
|
157
268
|
}
|
|
158
|
-
|
|
269
|
+
const pickedOrg = orgs[pickedIndex - 1];
|
|
270
|
+
return { orgId: pickedOrg.id, orgName: pickedOrg.name ?? null };
|
|
271
|
+
}
|
|
272
|
+
throw new Error("Multiple organizations found.\n\nNext:\n- instafy org list\n- instafy project init --org-id <uuid>");
|
|
273
|
+
}
|
|
274
|
+
if (orgSlug && !orgName) {
|
|
275
|
+
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 };
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
rl.close();
|
|
292
|
+
}
|
|
159
293
|
}
|
|
294
|
+
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}"`);
|
|
160
295
|
}
|
|
161
296
|
const payload = {};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
null;
|
|
165
|
-
if (!resolvedName) {
|
|
166
|
-
throw new Error(`Organization name is required.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
|
|
297
|
+
if (orgName) {
|
|
298
|
+
payload.orgName = orgName;
|
|
167
299
|
}
|
|
168
|
-
|
|
169
|
-
if (orgSlug)
|
|
300
|
+
if (orgSlug) {
|
|
170
301
|
payload.orgSlug = orgSlug;
|
|
302
|
+
}
|
|
171
303
|
if (options.ownerUserId) {
|
|
172
304
|
payload.ownerUserId = options.ownerUserId;
|
|
173
305
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
headers: {
|
|
177
|
-
authorization: `Bearer ${token}`,
|
|
178
|
-
"content-type": "application/json",
|
|
179
|
-
},
|
|
180
|
-
body: JSON.stringify(payload),
|
|
181
|
-
});
|
|
182
|
-
if (!response.ok) {
|
|
183
|
-
const text = await response.text().catch(() => "");
|
|
184
|
-
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
185
|
-
}
|
|
186
|
-
const json = (await response.json());
|
|
187
|
-
const orgId = json.org_id ?? json.orgId ?? json.id;
|
|
188
|
-
if (!orgId) {
|
|
189
|
-
throw new Error(`Organization creation response missing org id (expected org_id/orgId/id). Raw: ${JSON.stringify(json)}`);
|
|
306
|
+
if (!payload.orgName) {
|
|
307
|
+
throw new Error(`Organization name is required.\n\nNext:\n- Create an org in Studio: ${studioOrgUrl}\n- Or rerun: instafy project init --org-name \"My Org\"`);
|
|
190
308
|
}
|
|
191
|
-
|
|
309
|
+
const created = await createOrganization(controllerUrl, token, payload);
|
|
310
|
+
return { orgId: created.orgId, orgName: created.orgName ?? orgName ?? null };
|
|
192
311
|
}
|
|
193
312
|
export async function listProjects(options) {
|
|
194
313
|
const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
|
package/package.json
CHANGED