@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 +57 -13
- package/dist/index.js +4 -0
- package/dist/project.js +71 -189
- package/package.json +2 -1
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
|
-
|
|
284
|
+
const existing = resolveUserAccessToken({ profile });
|
|
265
285
|
const provided = normalizeToken(options.token ?? null);
|
|
266
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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("
|
|
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 {
|
|
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.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
|
},
|