@remixhq/cli 0.1.11
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/LICENSE +22 -0
- package/README.md +15 -0
- package/dist/cli.js +3054 -0
- package/dist/cli.js.map +1 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3054 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import pc12 from "picocolors";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
|
|
8
|
+
// src/commands/login.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
|
|
11
|
+
// src/errors/cliError.ts
|
|
12
|
+
import { CliError, RemixError } from "@remixhq/core/errors";
|
|
13
|
+
|
|
14
|
+
// src/config/config.ts
|
|
15
|
+
import { configSchema, resolveConfig } from "@remixhq/core/config";
|
|
16
|
+
|
|
17
|
+
// src/services/oauthCallbackServer.ts
|
|
18
|
+
import http from "http";
|
|
19
|
+
async function startOAuthCallbackServer(params) {
|
|
20
|
+
const timeoutMs = params?.timeoutMs ?? 3 * 6e4;
|
|
21
|
+
const requestedPort = params?.port ?? null;
|
|
22
|
+
let resolveCode = null;
|
|
23
|
+
let rejectCode = null;
|
|
24
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
25
|
+
resolveCode = resolve;
|
|
26
|
+
rejectCode = reject;
|
|
27
|
+
});
|
|
28
|
+
const server = http.createServer((req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const base = `http://127.0.0.1`;
|
|
31
|
+
const u = new URL(req.url ?? "/", base);
|
|
32
|
+
const errorDescription = u.searchParams.get("error_description");
|
|
33
|
+
const error = u.searchParams.get("error");
|
|
34
|
+
const code = u.searchParams.get("code");
|
|
35
|
+
if (errorDescription || error) {
|
|
36
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
37
|
+
res.end(
|
|
38
|
+
`<html><body><h3>Sign-in failed</h3><p>${escapeHtml(errorDescription ?? error ?? "Unknown error")}</p></body></html>`
|
|
39
|
+
);
|
|
40
|
+
rejectCode?.(new CliError("Sign-in failed.", { exitCode: 1, hint: errorDescription ?? error ?? void 0 }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!code) {
|
|
44
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
45
|
+
res.end(`<html><body><h3>Missing code</h3><p>No authorization code was provided.</p></body></html>`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
49
|
+
res.end(
|
|
50
|
+
`<html><body><h3>Sign-in complete</h3><p>You can close this tab and return to the terminal.</p></body></html>`
|
|
51
|
+
);
|
|
52
|
+
resolveCode?.(code);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
rejectCode?.(err);
|
|
55
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
56
|
+
res.end("Internal error.");
|
|
57
|
+
} finally {
|
|
58
|
+
try {
|
|
59
|
+
server.close();
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const listenPort = typeof requestedPort === "number" ? requestedPort : 0;
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
server.once("error", reject);
|
|
67
|
+
server.listen(listenPort, "127.0.0.1", () => resolve());
|
|
68
|
+
});
|
|
69
|
+
const addr = server.address();
|
|
70
|
+
if (!addr || typeof addr === "string") {
|
|
71
|
+
server.close();
|
|
72
|
+
throw new CliError("Failed to start the local callback server.", { exitCode: 1 });
|
|
73
|
+
}
|
|
74
|
+
const redirectTo = `http://127.0.0.1:${addr.port}/callback`;
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
rejectCode?.(
|
|
77
|
+
new CliError("Sign-in timed out.", {
|
|
78
|
+
exitCode: 1,
|
|
79
|
+
hint: "Try running `remix login` again."
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
try {
|
|
83
|
+
server.close();
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}, timeoutMs).unref();
|
|
87
|
+
const waitForCode = async () => {
|
|
88
|
+
try {
|
|
89
|
+
return await codePromise;
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const close = async () => {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
await new Promise((resolve) => server.close(() => resolve())).catch(() => {
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
return { redirectTo, waitForCode, close };
|
|
100
|
+
}
|
|
101
|
+
function escapeHtml(input) {
|
|
102
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/utils/browser.ts
|
|
106
|
+
import { execa } from "execa";
|
|
107
|
+
async function openBrowser(url) {
|
|
108
|
+
const platform = process.platform;
|
|
109
|
+
try {
|
|
110
|
+
if (platform === "darwin") {
|
|
111
|
+
await execa("open", [url], { stdio: "ignore" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (platform === "win32") {
|
|
115
|
+
await execa("cmd", ["/c", "start", "", url], { stdio: "ignore", windowsHide: true });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await execa("xdg-open", [url], { stdio: "ignore" });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw new CliError("Could not open a browser automatically.", {
|
|
121
|
+
exitCode: 1,
|
|
122
|
+
hint: `Open this URL in a browser:
|
|
123
|
+
${url}
|
|
124
|
+
|
|
125
|
+
${err?.message ?? String(err)}`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/clients/supabase.ts
|
|
131
|
+
import { createSupabaseAuthHelpers } from "@remixhq/core/auth";
|
|
132
|
+
|
|
133
|
+
// src/services/sessionStore.ts
|
|
134
|
+
import fs2 from "fs/promises";
|
|
135
|
+
import os from "os";
|
|
136
|
+
import path2 from "path";
|
|
137
|
+
import { storedSessionSchema } from "@remixhq/core/auth";
|
|
138
|
+
|
|
139
|
+
// src/utils/fs.ts
|
|
140
|
+
import fs from "fs/promises";
|
|
141
|
+
import path from "path";
|
|
142
|
+
import fse from "fs-extra";
|
|
143
|
+
async function findAvailableDirPath(preferredDir) {
|
|
144
|
+
const preferredExists = await fse.pathExists(preferredDir);
|
|
145
|
+
if (!preferredExists) return preferredDir;
|
|
146
|
+
const parent = path.dirname(preferredDir);
|
|
147
|
+
const base = path.basename(preferredDir);
|
|
148
|
+
for (let i = 2; i <= 1e3; i++) {
|
|
149
|
+
const candidate = path.join(parent, `${base}-${i}`);
|
|
150
|
+
if (!await fse.pathExists(candidate)) return candidate;
|
|
151
|
+
}
|
|
152
|
+
throw new CliError("No available output directory name.", {
|
|
153
|
+
exitCode: 2,
|
|
154
|
+
hint: `Tried ${base}-2 through ${base}-1000 under ${parent}.`
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function ensureEmptyDir(dir) {
|
|
158
|
+
const exists2 = await fse.pathExists(dir);
|
|
159
|
+
if (exists2) {
|
|
160
|
+
throw new CliError("Output directory already exists.", {
|
|
161
|
+
hint: `Choose a new --out directory or delete: ${dir}`,
|
|
162
|
+
exitCode: 2
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
await fse.mkdirp(dir);
|
|
166
|
+
}
|
|
167
|
+
async function writeJsonAtomic(filePath, value) {
|
|
168
|
+
const dir = path.dirname(filePath);
|
|
169
|
+
await fse.mkdirp(dir);
|
|
170
|
+
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
171
|
+
await fs.writeFile(tmp, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
172
|
+
await fs.rename(tmp, filePath);
|
|
173
|
+
}
|
|
174
|
+
async function writeTextAtomic(filePath, contents) {
|
|
175
|
+
const dir = path.dirname(filePath);
|
|
176
|
+
await fse.mkdirp(dir);
|
|
177
|
+
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
178
|
+
await fs.writeFile(tmp, contents, "utf8");
|
|
179
|
+
await fs.rename(tmp, filePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/services/sessionStore.ts
|
|
183
|
+
var KEYTAR_SERVICE = "remix-cli";
|
|
184
|
+
var KEYTAR_ACCOUNT = "default";
|
|
185
|
+
function xdgConfigHome() {
|
|
186
|
+
const v = process.env.XDG_CONFIG_HOME;
|
|
187
|
+
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
188
|
+
return path2.join(os.homedir(), ".config");
|
|
189
|
+
}
|
|
190
|
+
function sessionFilePath() {
|
|
191
|
+
return path2.join(xdgConfigHome(), "remix", "session.json");
|
|
192
|
+
}
|
|
193
|
+
async function maybeLoadKeytar() {
|
|
194
|
+
try {
|
|
195
|
+
const mod = await import("keytar");
|
|
196
|
+
const candidates = [mod?.default, mod].filter(Boolean);
|
|
197
|
+
for (const c of candidates) {
|
|
198
|
+
if (c && typeof c.getPassword === "function" && typeof c.setPassword === "function" && typeof c.deletePassword === "function") {
|
|
199
|
+
return c;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function ensurePathPermissions(filePath) {
|
|
208
|
+
const dir = path2.dirname(filePath);
|
|
209
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
210
|
+
try {
|
|
211
|
+
await fs2.chmod(dir, 448);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await fs2.chmod(filePath, 384);
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function writeSessionFileFallback(session) {
|
|
220
|
+
const fp = sessionFilePath();
|
|
221
|
+
await writeJsonAtomic(fp, session);
|
|
222
|
+
await ensurePathPermissions(fp);
|
|
223
|
+
}
|
|
224
|
+
async function getSession() {
|
|
225
|
+
const keytar = await maybeLoadKeytar();
|
|
226
|
+
if (keytar) {
|
|
227
|
+
const raw2 = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
228
|
+
if (!raw2) return null;
|
|
229
|
+
try {
|
|
230
|
+
const parsed = storedSessionSchema.safeParse(JSON.parse(raw2));
|
|
231
|
+
if (!parsed.success) return null;
|
|
232
|
+
return parsed.data;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const fp = sessionFilePath();
|
|
238
|
+
const raw = await fs2.readFile(fp, "utf8").catch(() => null);
|
|
239
|
+
if (!raw) return null;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
242
|
+
if (!parsed.success) return null;
|
|
243
|
+
await ensurePathPermissions(fp);
|
|
244
|
+
return parsed.data;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function setSession(session) {
|
|
250
|
+
const parsed = storedSessionSchema.safeParse(session);
|
|
251
|
+
if (!parsed.success) {
|
|
252
|
+
throw new CliError("Session data is invalid and was not stored.", { exitCode: 1 });
|
|
253
|
+
}
|
|
254
|
+
const keytar = await maybeLoadKeytar();
|
|
255
|
+
if (keytar) {
|
|
256
|
+
await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, JSON.stringify(parsed.data));
|
|
257
|
+
}
|
|
258
|
+
await writeSessionFileFallback(parsed.data);
|
|
259
|
+
}
|
|
260
|
+
async function clearSession() {
|
|
261
|
+
const keytar = await maybeLoadKeytar();
|
|
262
|
+
if (keytar) {
|
|
263
|
+
await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
264
|
+
}
|
|
265
|
+
const fp = sessionFilePath();
|
|
266
|
+
await fs2.rm(fp, { force: true }).catch(() => {
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/clients/api.ts
|
|
271
|
+
import {
|
|
272
|
+
createApiClient as createCoreApiClient,
|
|
273
|
+
createStoredSessionTokenProvider,
|
|
274
|
+
createSupabaseAuthHelpers as createSupabaseAuthHelpers2
|
|
275
|
+
} from "@remixhq/core";
|
|
276
|
+
function createApiClient(config, opts) {
|
|
277
|
+
const tokenProvider = createStoredSessionTokenProvider({
|
|
278
|
+
config,
|
|
279
|
+
sessionStore: { getSession, setSession },
|
|
280
|
+
refreshStoredSession: async ({ config: refreshConfig, session }) => {
|
|
281
|
+
const supabase = createSupabaseAuthHelpers2(refreshConfig);
|
|
282
|
+
return supabase.refreshWithStoredSession({ session });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return createCoreApiClient(config, {
|
|
286
|
+
apiKey: opts?.apiKey,
|
|
287
|
+
tokenProvider
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/commands/login.ts
|
|
292
|
+
function registerLoginCommand(program) {
|
|
293
|
+
program.command("login").description("Sign in to Remix").option("--no-browser", "Do not open a browser; print the URL").option("--port <port>", "Local callback port (default: random available port)").option("--yes", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
294
|
+
const config = await resolveConfig({
|
|
295
|
+
yes: Boolean(opts.yes)
|
|
296
|
+
});
|
|
297
|
+
const portRaw = typeof opts.port === "string" ? opts.port.trim() : "";
|
|
298
|
+
let port = null;
|
|
299
|
+
if (portRaw) {
|
|
300
|
+
const parsed = Number(portRaw);
|
|
301
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
302
|
+
throw new CliError("Invalid --port value", { exitCode: 2, hint: "Example: --port 8085" });
|
|
303
|
+
}
|
|
304
|
+
port = parsed;
|
|
305
|
+
}
|
|
306
|
+
const server = await startOAuthCallbackServer({ port });
|
|
307
|
+
try {
|
|
308
|
+
const supabase = createSupabaseAuthHelpers(config);
|
|
309
|
+
const { url } = await supabase.startGoogleLogin({ redirectTo: server.redirectTo });
|
|
310
|
+
const shouldOpenBrowser = Boolean(opts.browser ?? true);
|
|
311
|
+
if (shouldOpenBrowser) {
|
|
312
|
+
try {
|
|
313
|
+
await openBrowser(url);
|
|
314
|
+
console.log(pc.dim("Opened a browser for sign-in."));
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error(pc.yellow("Could not open a browser automatically."));
|
|
317
|
+
console.error(pc.dim(err?.message ?? String(err)));
|
|
318
|
+
console.log(`Open this URL in a browser:
|
|
319
|
+
${url}`);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`Open this URL to sign in:
|
|
323
|
+
${url}`);
|
|
324
|
+
}
|
|
325
|
+
const code = await server.waitForCode();
|
|
326
|
+
const session = await supabase.exchangeCode({ code });
|
|
327
|
+
await setSession(session);
|
|
328
|
+
let me = null;
|
|
329
|
+
try {
|
|
330
|
+
const api = createApiClient(config);
|
|
331
|
+
me = await api.getMe();
|
|
332
|
+
} catch (err) {
|
|
333
|
+
await clearSession().catch(() => {
|
|
334
|
+
});
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
if (opts.json) {
|
|
338
|
+
console.log(JSON.stringify({ success: true, me }, null, 2));
|
|
339
|
+
} else {
|
|
340
|
+
const profile = me?.responseObject;
|
|
341
|
+
const label = profile?.email ? `${profile.email}` : profile?.id ? `user ${profile.id}` : "user";
|
|
342
|
+
console.log(pc.green(`Signed in as ${label}.`));
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
await server.close();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/commands/logout.ts
|
|
351
|
+
import pc2 from "picocolors";
|
|
352
|
+
function registerLogoutCommand(program) {
|
|
353
|
+
program.command("logout").description("Clear the local Remix session").option("--json", "Output JSON", false).action(async (opts) => {
|
|
354
|
+
await clearSession();
|
|
355
|
+
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
356
|
+
if (opts.json) {
|
|
357
|
+
console.log(JSON.stringify({ success: true, cleared: true, envTokenPresent: Boolean(envToken) }, null, 2));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(pc2.green("Signed out."));
|
|
361
|
+
if (envToken) {
|
|
362
|
+
console.log(pc2.dim("Note: COMERGE_ACCESS_TOKEN is set and will still authenticate requests."));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/commands/whoami.ts
|
|
368
|
+
import pc3 from "picocolors";
|
|
369
|
+
function registerWhoamiCommand(program) {
|
|
370
|
+
program.command("whoami").description("Show the current Remix user").option("--yes", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
371
|
+
const config = await resolveConfig({
|
|
372
|
+
yes: Boolean(opts.yes)
|
|
373
|
+
});
|
|
374
|
+
const api = createApiClient(config);
|
|
375
|
+
const me = await api.getMe();
|
|
376
|
+
if (opts.json) {
|
|
377
|
+
console.log(JSON.stringify(me, null, 2));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const profile = me?.responseObject;
|
|
381
|
+
const label = profile?.email ? `${profile.email}` : profile?.id ? `user ${profile.id}` : "unknown";
|
|
382
|
+
console.log(pc3.green(`Signed in as ${label}.`));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/commands/init.ts
|
|
387
|
+
import pc9 from "picocolors";
|
|
388
|
+
|
|
389
|
+
// src/services/initLocal.ts
|
|
390
|
+
import os5 from "os";
|
|
391
|
+
import pc8 from "picocolors";
|
|
392
|
+
import prompts from "prompts";
|
|
393
|
+
|
|
394
|
+
// src/services/ensureAuth.ts
|
|
395
|
+
import pc4 from "picocolors";
|
|
396
|
+
|
|
397
|
+
// src/utils/tty.ts
|
|
398
|
+
function isInteractive() {
|
|
399
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/services/ensureAuth.ts
|
|
403
|
+
async function validateBackendSession(params) {
|
|
404
|
+
const cfg = await resolveConfig({ yes: Boolean(params?.yes) });
|
|
405
|
+
const api = createApiClient(cfg);
|
|
406
|
+
await api.getMe();
|
|
407
|
+
}
|
|
408
|
+
async function ensureAuth(params) {
|
|
409
|
+
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
410
|
+
if (typeof envToken === "string" && envToken.trim().length > 0) return;
|
|
411
|
+
const existing = await getSession();
|
|
412
|
+
if (existing) {
|
|
413
|
+
try {
|
|
414
|
+
await validateBackendSession({ yes: params?.yes });
|
|
415
|
+
return;
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const interactive = isInteractive();
|
|
420
|
+
const yes = Boolean(params?.yes);
|
|
421
|
+
if (!interactive || yes) {
|
|
422
|
+
throw new CliError("Not signed in.", {
|
|
423
|
+
exitCode: 2,
|
|
424
|
+
hint: "Run `remix login`, or set COMERGE_ACCESS_TOKEN for CI."
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const cfg = await resolveConfig({ yes: Boolean(params?.yes) });
|
|
428
|
+
const server = await startOAuthCallbackServer({ port: null });
|
|
429
|
+
try {
|
|
430
|
+
const supabase = createSupabaseAuthHelpers(cfg);
|
|
431
|
+
const { url } = await supabase.startGoogleLogin({ redirectTo: server.redirectTo });
|
|
432
|
+
try {
|
|
433
|
+
await openBrowser(url);
|
|
434
|
+
if (!params?.json) console.log(pc4.dim("Opened a browser for sign-in."));
|
|
435
|
+
} catch {
|
|
436
|
+
if (!params?.json) {
|
|
437
|
+
console.log(pc4.yellow("Could not open a browser automatically."));
|
|
438
|
+
console.log(`Open this URL in a browser:
|
|
439
|
+
${url}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const code = await server.waitForCode();
|
|
443
|
+
const session = await supabase.exchangeCode({ code });
|
|
444
|
+
await setSession(session);
|
|
445
|
+
try {
|
|
446
|
+
await validateBackendSession({ yes: params?.yes });
|
|
447
|
+
} catch (err) {
|
|
448
|
+
await clearSession().catch(() => {
|
|
449
|
+
});
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
452
|
+
} finally {
|
|
453
|
+
await server.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/services/importLocal.ts
|
|
458
|
+
import path6 from "path";
|
|
459
|
+
import pc5 from "picocolors";
|
|
460
|
+
|
|
461
|
+
// src/utils/projectDetect.ts
|
|
462
|
+
import fs3 from "fs/promises";
|
|
463
|
+
import path3 from "path";
|
|
464
|
+
async function fileExists(p) {
|
|
465
|
+
try {
|
|
466
|
+
const st = await fs3.stat(p);
|
|
467
|
+
return st.isFile();
|
|
468
|
+
} catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function findExpoProjectRoot(startDir) {
|
|
473
|
+
let cur = path3.resolve(startDir);
|
|
474
|
+
while (true) {
|
|
475
|
+
const pkg = path3.join(cur, "package.json");
|
|
476
|
+
if (await fileExists(pkg)) {
|
|
477
|
+
const hasAppJson = await fileExists(path3.join(cur, "app.json"));
|
|
478
|
+
const hasAppConfig = await fileExists(path3.join(cur, "app.config.js")) || await fileExists(path3.join(cur, "app.config.ts"));
|
|
479
|
+
if (!hasAppJson && !hasAppConfig) {
|
|
480
|
+
} else {
|
|
481
|
+
return cur;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const parent = path3.dirname(cur);
|
|
485
|
+
if (parent === cur) break;
|
|
486
|
+
cur = parent;
|
|
487
|
+
}
|
|
488
|
+
throw new CliError("Not inside an Expo project.", {
|
|
489
|
+
hint: "Run this command from the root of your Expo project.",
|
|
490
|
+
exitCode: 2
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/utils/gitFiles.ts
|
|
495
|
+
import { execa as execa2 } from "execa";
|
|
496
|
+
async function canUseGit(cwd) {
|
|
497
|
+
try {
|
|
498
|
+
const res = await execa2("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "ignore" });
|
|
499
|
+
return res.exitCode === 0;
|
|
500
|
+
} catch {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function parseGitLsFilesZ(buf) {
|
|
505
|
+
return buf.toString("utf8").split("\0").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
506
|
+
}
|
|
507
|
+
async function listFilesGit(params) {
|
|
508
|
+
const args = ["ls-files", "-z", "--cached", "--others", "--exclude-standard"];
|
|
509
|
+
if (params.pathspec) {
|
|
510
|
+
args.push("--", params.pathspec);
|
|
511
|
+
}
|
|
512
|
+
const res = await execa2("git", args, { cwd: params.cwd, stdout: "pipe", stderr: "ignore" });
|
|
513
|
+
return parseGitLsFilesZ(Buffer.from(res.stdout ?? "", "utf8"));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/utils/fileSelection.ts
|
|
517
|
+
import fs4 from "fs/promises";
|
|
518
|
+
import path4 from "path";
|
|
519
|
+
import fg from "fast-glob";
|
|
520
|
+
import ignore from "ignore";
|
|
521
|
+
var BUILTIN_IGNORES = [
|
|
522
|
+
"**/node_modules/**",
|
|
523
|
+
"**/.git/**",
|
|
524
|
+
"**/.expo/**",
|
|
525
|
+
"**/dist/**",
|
|
526
|
+
"**/build/**",
|
|
527
|
+
"**/coverage/**",
|
|
528
|
+
"**/.turbo/**",
|
|
529
|
+
"**/.next/**",
|
|
530
|
+
"**/ios/build/**",
|
|
531
|
+
"**/android/build/**",
|
|
532
|
+
"**/.pnpm-store/**",
|
|
533
|
+
"**/remix-shell*/**"
|
|
534
|
+
];
|
|
535
|
+
function normalizeRel(p) {
|
|
536
|
+
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
537
|
+
}
|
|
538
|
+
async function readGitignore(root) {
|
|
539
|
+
const fp = path4.join(root, ".gitignore");
|
|
540
|
+
return await fs4.readFile(fp, "utf8").catch(() => "");
|
|
541
|
+
}
|
|
542
|
+
async function listFilesFs(params) {
|
|
543
|
+
const ig = ignore();
|
|
544
|
+
const raw = await readGitignore(params.root);
|
|
545
|
+
if (raw) ig.add(raw);
|
|
546
|
+
const entries = await fg(["**/*"], {
|
|
547
|
+
cwd: params.root,
|
|
548
|
+
onlyFiles: true,
|
|
549
|
+
dot: true,
|
|
550
|
+
followSymbolicLinks: false,
|
|
551
|
+
unique: true,
|
|
552
|
+
ignore: [...BUILTIN_IGNORES]
|
|
553
|
+
});
|
|
554
|
+
const filtered = entries.filter((p) => {
|
|
555
|
+
const rel = normalizeRel(p);
|
|
556
|
+
if (!params.includeDotenv && path4.posix.basename(rel).startsWith(".env")) return false;
|
|
557
|
+
return !ig.ignores(rel);
|
|
558
|
+
});
|
|
559
|
+
return filtered.map(normalizeRel);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/utils/zip.ts
|
|
563
|
+
import crypto from "crypto";
|
|
564
|
+
import fs5 from "fs";
|
|
565
|
+
import fsp from "fs/promises";
|
|
566
|
+
import os2 from "os";
|
|
567
|
+
import path5 from "path";
|
|
568
|
+
import archiver from "archiver";
|
|
569
|
+
var MAX_IMPORT_ZIP_BYTES = 50 * 1024 * 1024;
|
|
570
|
+
var MAX_IMPORT_FILE_COUNT = 25e3;
|
|
571
|
+
function normalizeRel2(p) {
|
|
572
|
+
return p.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+/, "");
|
|
573
|
+
}
|
|
574
|
+
function validateRelPath(rel) {
|
|
575
|
+
const p = normalizeRel2(rel);
|
|
576
|
+
if (!p) throw new CliError("Invalid file path.", { exitCode: 1 });
|
|
577
|
+
if (p.startsWith("../") || p.includes("/../") || p === "..") {
|
|
578
|
+
throw new CliError("Refusing to add a path traversal entry.", { exitCode: 1, hint: p });
|
|
579
|
+
}
|
|
580
|
+
if (p.startsWith("/")) {
|
|
581
|
+
throw new CliError("Refusing to add an absolute path.", { exitCode: 1, hint: p });
|
|
582
|
+
}
|
|
583
|
+
return p;
|
|
584
|
+
}
|
|
585
|
+
async function computeStats(params) {
|
|
586
|
+
const largestN = params.largestN ?? 10;
|
|
587
|
+
if (params.files.length > MAX_IMPORT_FILE_COUNT) {
|
|
588
|
+
throw new CliError("Too many files to import.", {
|
|
589
|
+
exitCode: 2,
|
|
590
|
+
hint: `File count ${params.files.length} exceeds limit ${MAX_IMPORT_FILE_COUNT}. Use --path or add ignores.`
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
let totalBytes = 0;
|
|
594
|
+
const largest = [];
|
|
595
|
+
for (const rel0 of params.files) {
|
|
596
|
+
const rel = validateRelPath(rel0);
|
|
597
|
+
const abs = path5.join(params.root, rel);
|
|
598
|
+
const st = await fsp.stat(abs);
|
|
599
|
+
if (!st.isFile()) continue;
|
|
600
|
+
const bytes = st.size;
|
|
601
|
+
totalBytes += bytes;
|
|
602
|
+
if (largestN > 0) {
|
|
603
|
+
largest.push({ path: rel, bytes });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
largest.sort((a, b) => b.bytes - a.bytes);
|
|
607
|
+
return {
|
|
608
|
+
fileCount: params.files.length,
|
|
609
|
+
totalBytes,
|
|
610
|
+
largestFiles: largest.slice(0, largestN)
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function createZip(params) {
|
|
614
|
+
const zipName = (params.zipName ?? "import.zip").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
615
|
+
const tmpDir = await fsp.mkdtemp(path5.join(os2.tmpdir(), "remix-import-"));
|
|
616
|
+
const zipPath = path5.join(tmpDir, zipName);
|
|
617
|
+
await new Promise((resolve, reject) => {
|
|
618
|
+
const output = fs5.createWriteStream(zipPath);
|
|
619
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
620
|
+
output.on("close", () => resolve());
|
|
621
|
+
output.on("error", (err) => reject(err));
|
|
622
|
+
archive.on("warning", (err) => {
|
|
623
|
+
reject(err);
|
|
624
|
+
});
|
|
625
|
+
archive.on("error", (err) => reject(err));
|
|
626
|
+
archive.pipe(output);
|
|
627
|
+
for (const rel0 of params.files) {
|
|
628
|
+
const rel = validateRelPath(rel0);
|
|
629
|
+
const abs = path5.join(params.root, rel);
|
|
630
|
+
archive.file(abs, { name: rel });
|
|
631
|
+
}
|
|
632
|
+
void archive.finalize();
|
|
633
|
+
});
|
|
634
|
+
const st = await fsp.stat(zipPath);
|
|
635
|
+
if (st.size > MAX_IMPORT_ZIP_BYTES) {
|
|
636
|
+
throw new CliError("Archive exceeds the upload size limit.", {
|
|
637
|
+
exitCode: 2,
|
|
638
|
+
hint: `Zip size ${st.size} bytes exceeds limit ${MAX_IMPORT_ZIP_BYTES} bytes. Use --path or add ignores.`
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return { zipPath };
|
|
642
|
+
}
|
|
643
|
+
async function sha256FileHex(filePath) {
|
|
644
|
+
const hash = crypto.createHash("sha256");
|
|
645
|
+
await new Promise((resolve, reject) => {
|
|
646
|
+
const s = fs5.createReadStream(filePath);
|
|
647
|
+
s.on("data", (chunk) => hash.update(chunk));
|
|
648
|
+
s.on("error", reject);
|
|
649
|
+
s.on("end", () => resolve());
|
|
650
|
+
});
|
|
651
|
+
return hash.digest("hex");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/clients/upload.ts
|
|
655
|
+
import fs6 from "fs";
|
|
656
|
+
import { PassThrough } from "stream";
|
|
657
|
+
async function uploadPresigned(params) {
|
|
658
|
+
const st = await fs6.promises.stat(params.filePath).catch(() => null);
|
|
659
|
+
if (!st || !st.isFile()) throw new CliError("Upload file not found.", { exitCode: 2 });
|
|
660
|
+
const totalBytes = st.size;
|
|
661
|
+
const fileStream = fs6.createReadStream(params.filePath);
|
|
662
|
+
const pass = new PassThrough();
|
|
663
|
+
let sent = 0;
|
|
664
|
+
fileStream.on("data", (chunk) => {
|
|
665
|
+
sent += chunk.length;
|
|
666
|
+
params.onProgress?.({ sentBytes: sent, totalBytes });
|
|
667
|
+
});
|
|
668
|
+
fileStream.on("error", (err) => pass.destroy(err));
|
|
669
|
+
fileStream.pipe(pass);
|
|
670
|
+
const res = await fetch(params.uploadUrl, {
|
|
671
|
+
method: "PUT",
|
|
672
|
+
headers: params.headers,
|
|
673
|
+
body: pass,
|
|
674
|
+
duplex: "half"
|
|
675
|
+
});
|
|
676
|
+
if (!res.ok) {
|
|
677
|
+
const text = await res.text().catch(() => "");
|
|
678
|
+
throw new CliError("Upload failed.", {
|
|
679
|
+
exitCode: 1,
|
|
680
|
+
hint: `Status: ${res.status}
|
|
681
|
+
${text}`.trim() || null
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/services/importLocal.ts
|
|
687
|
+
var SAFE_PATH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
688
|
+
var COMERGE_SHELL_SEGMENT_RE = /(^|\/)remix-shell[^/]*(\/|$)/;
|
|
689
|
+
function sleep(ms) {
|
|
690
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
691
|
+
}
|
|
692
|
+
function formatBytes(bytes) {
|
|
693
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
694
|
+
let v = bytes;
|
|
695
|
+
let i = 0;
|
|
696
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
697
|
+
v /= 1024;
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
701
|
+
}
|
|
702
|
+
function deriveAppName(projectRoot) {
|
|
703
|
+
const base = path6.basename(projectRoot);
|
|
704
|
+
return base || "Imported App";
|
|
705
|
+
}
|
|
706
|
+
function validateOptionalSubdir(subdir) {
|
|
707
|
+
if (!subdir) return null;
|
|
708
|
+
const s = subdir.trim().replace(/^\/+/, "").replace(/\\/g, "/");
|
|
709
|
+
if (!s) return null;
|
|
710
|
+
if (!SAFE_PATH_RE.test(s) || s.includes("..")) {
|
|
711
|
+
throw new CliError("Invalid --path value.", {
|
|
712
|
+
exitCode: 2,
|
|
713
|
+
hint: "Use a safe subdirectory such as packages/mobile-app (letters, numbers, ., _, -, / only)."
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return s;
|
|
717
|
+
}
|
|
718
|
+
function getAppStatus(appResp) {
|
|
719
|
+
const obj = appResp?.responseObject;
|
|
720
|
+
const status = typeof obj?.status === "string" ? obj.status : null;
|
|
721
|
+
const statusError = typeof obj?.statusError === "string" ? obj.statusError : null;
|
|
722
|
+
return { status, statusError };
|
|
723
|
+
}
|
|
724
|
+
async function importLocal(params) {
|
|
725
|
+
const noWait = Boolean(params.noWait);
|
|
726
|
+
const maxWaitMs = 1200 * 1e3;
|
|
727
|
+
const apiKey = String(params.apiKey || "").trim();
|
|
728
|
+
if (!apiKey && !params.dryRun) {
|
|
729
|
+
throw new CliError("Missing client key.", { exitCode: 2, hint: "Sign in and rerun `remix init`." });
|
|
730
|
+
}
|
|
731
|
+
const cfg = await resolveConfig({ yes: Boolean(params.yes) });
|
|
732
|
+
const api = createApiClient(cfg, { apiKey });
|
|
733
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
734
|
+
const subdir = validateOptionalSubdir(params.subdir);
|
|
735
|
+
const importRoot = subdir ? path6.join(projectRoot, subdir) : projectRoot;
|
|
736
|
+
const appName = (params.appName ?? "").trim() || deriveAppName(projectRoot);
|
|
737
|
+
const log = (msg) => {
|
|
738
|
+
if (params.json) return;
|
|
739
|
+
console.log(msg);
|
|
740
|
+
};
|
|
741
|
+
log(pc5.dim(`Project: ${projectRoot}`));
|
|
742
|
+
if (subdir) log(pc5.dim(`Import path: ${subdir}`));
|
|
743
|
+
let files = [];
|
|
744
|
+
const useGit = await canUseGit(projectRoot);
|
|
745
|
+
if (useGit) {
|
|
746
|
+
const gitPaths = await listFilesGit({ cwd: projectRoot, pathspec: subdir ?? null });
|
|
747
|
+
files = gitPaths.filter((p) => !COMERGE_SHELL_SEGMENT_RE.test(p.replace(/\\/g, "/")));
|
|
748
|
+
if (!params.includeDotenv) {
|
|
749
|
+
files = files.filter((p) => !path6.posix.basename(p).startsWith(".env"));
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
files = await listFilesFs({ root: importRoot, includeDotenv: params.includeDotenv });
|
|
753
|
+
if (subdir) {
|
|
754
|
+
files = files.map((p) => path6.posix.join(subdir, p));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (files.length === 0) {
|
|
758
|
+
throw new CliError("No files found to upload.", {
|
|
759
|
+
exitCode: 2,
|
|
760
|
+
hint: "Check .gitignore or try without --path."
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const stats = await computeStats({ root: projectRoot, files, largestN: 10 });
|
|
764
|
+
log(pc5.dim(`Files: ${stats.fileCount} Total: ${formatBytes(stats.totalBytes)}`));
|
|
765
|
+
if (params.dryRun) {
|
|
766
|
+
const top = stats.largestFiles.slice(0, 10);
|
|
767
|
+
if (top.length > 0) {
|
|
768
|
+
log(pc5.dim("Largest files:"));
|
|
769
|
+
for (const f of top) {
|
|
770
|
+
log(pc5.dim(` ${formatBytes(f.bytes)} ${f.path}`));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return { uploadId: "dry-run", appId: "dry-run", status: "dry-run" };
|
|
774
|
+
}
|
|
775
|
+
log(pc5.dim("Creating archive..."));
|
|
776
|
+
const { zipPath } = await createZip({ root: projectRoot, files, zipName: "import.zip" });
|
|
777
|
+
const zipSha = await sha256FileHex(zipPath);
|
|
778
|
+
const zipSize = (await (await import("fs/promises")).stat(zipPath)).size;
|
|
779
|
+
log(pc5.dim("Requesting upload URL..."));
|
|
780
|
+
const presignResp = await api.presignImportUpload({
|
|
781
|
+
file: {
|
|
782
|
+
name: "import.zip",
|
|
783
|
+
mimeType: "application/zip",
|
|
784
|
+
size: zipSize,
|
|
785
|
+
checksumSha256: zipSha
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
const presign = presignResp?.responseObject;
|
|
789
|
+
const uploadId = String(presign?.uploadId ?? "");
|
|
790
|
+
const uploadUrl = String(presign?.uploadUrl ?? "");
|
|
791
|
+
const headers = presign?.headers ?? {};
|
|
792
|
+
if (!uploadId || !uploadUrl) {
|
|
793
|
+
throw new CliError("Upload URL response is missing required fields.", {
|
|
794
|
+
exitCode: 1,
|
|
795
|
+
hint: JSON.stringify(presignResp, null, 2)
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
log(pc5.dim("Uploading archive..."));
|
|
799
|
+
let lastPct = -1;
|
|
800
|
+
await uploadPresigned({
|
|
801
|
+
uploadUrl,
|
|
802
|
+
headers,
|
|
803
|
+
filePath: zipPath,
|
|
804
|
+
onProgress: ({ sentBytes, totalBytes }) => {
|
|
805
|
+
if (params.json) return;
|
|
806
|
+
const pct = totalBytes > 0 ? Math.floor(sentBytes / totalBytes * 100) : 0;
|
|
807
|
+
if (pct !== lastPct && (pct % 5 === 0 || pct === 100)) {
|
|
808
|
+
lastPct = pct;
|
|
809
|
+
process.stdout.write(pc5.dim(`Upload ${pct}%\r`));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
if (!params.json) process.stdout.write("\n");
|
|
814
|
+
log(pc5.dim("Starting import..."));
|
|
815
|
+
const importResp = await api.importFromUpload({
|
|
816
|
+
uploadId,
|
|
817
|
+
appName,
|
|
818
|
+
path: subdir ?? void 0
|
|
819
|
+
});
|
|
820
|
+
const importObj = importResp?.responseObject;
|
|
821
|
+
const appId = String(importObj?.appId ?? "");
|
|
822
|
+
const projectId = importObj?.projectId ? String(importObj.projectId) : void 0;
|
|
823
|
+
const threadId = importObj?.threadId ? String(importObj.threadId) : void 0;
|
|
824
|
+
if (!appId) {
|
|
825
|
+
throw new CliError("Import response is missing the app ID.", {
|
|
826
|
+
exitCode: 1,
|
|
827
|
+
hint: JSON.stringify(importResp, null, 2)
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (noWait) {
|
|
831
|
+
return { uploadId, appId, projectId, threadId, status: "accepted" };
|
|
832
|
+
}
|
|
833
|
+
log(pc5.dim("Waiting for import to finish..."));
|
|
834
|
+
const startedAt = Date.now();
|
|
835
|
+
let delay = 2e3;
|
|
836
|
+
let lastStatus = null;
|
|
837
|
+
while (Date.now() - startedAt < maxWaitMs) {
|
|
838
|
+
const appResp = await api.getApp(appId);
|
|
839
|
+
const { status, statusError } = getAppStatus(appResp);
|
|
840
|
+
if (status && status !== lastStatus) {
|
|
841
|
+
lastStatus = status;
|
|
842
|
+
log(pc5.dim(`Status: ${status}`));
|
|
843
|
+
}
|
|
844
|
+
if (status === "ready") {
|
|
845
|
+
return { uploadId, appId, projectId, threadId, status };
|
|
846
|
+
}
|
|
847
|
+
if (status === "error") {
|
|
848
|
+
throw new CliError("Import failed.", { exitCode: 1, hint: statusError ?? "App status is error." });
|
|
849
|
+
}
|
|
850
|
+
await sleep(delay);
|
|
851
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
852
|
+
}
|
|
853
|
+
throw new CliError("Timed out waiting for import.", {
|
|
854
|
+
exitCode: 1,
|
|
855
|
+
hint: "Check app status with: remix import local --no-wait"
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/commands/shellInit.ts
|
|
860
|
+
import path10 from "path";
|
|
861
|
+
import os3 from "os";
|
|
862
|
+
import pc7 from "picocolors";
|
|
863
|
+
import fse4 from "fs-extra";
|
|
864
|
+
|
|
865
|
+
// src/utils/packageJson.ts
|
|
866
|
+
import fs7 from "fs/promises";
|
|
867
|
+
async function readPackageJson(projectRoot) {
|
|
868
|
+
const raw = await fs7.readFile(`${projectRoot}/package.json`, "utf8").catch(() => null);
|
|
869
|
+
if (!raw) {
|
|
870
|
+
throw new CliError("package.json not found.", { exitCode: 2 });
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
return JSON.parse(raw);
|
|
874
|
+
} catch {
|
|
875
|
+
throw new CliError("Failed to parse package.json.", { exitCode: 2 });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
var STUDIO_PEERS = [
|
|
879
|
+
"@callstack/liquid-glass",
|
|
880
|
+
"@supabase/supabase-js",
|
|
881
|
+
"@gorhom/bottom-sheet",
|
|
882
|
+
"expo-file-system",
|
|
883
|
+
"expo-haptics",
|
|
884
|
+
"expo-linear-gradient",
|
|
885
|
+
"lucide-react-native",
|
|
886
|
+
"react-native-gesture-handler",
|
|
887
|
+
"react-native-reanimated",
|
|
888
|
+
"react-native-safe-area-context",
|
|
889
|
+
"react-native-svg",
|
|
890
|
+
"react-native-view-shot"
|
|
891
|
+
];
|
|
892
|
+
function buildShellPackageJson(params) {
|
|
893
|
+
const orig = params.original;
|
|
894
|
+
const warnings = [];
|
|
895
|
+
const info = [];
|
|
896
|
+
const dependencies = { ...orig.dependencies ?? {} };
|
|
897
|
+
const devDependencies = { ...orig.devDependencies ?? {} };
|
|
898
|
+
const main2 = "expo-router/entry";
|
|
899
|
+
dependencies["@remixhq/expo-studio"] = "latest";
|
|
900
|
+
dependencies["@remixhq/expo-runtime"] = "latest";
|
|
901
|
+
if (!dependencies["expo-router"] && !devDependencies["expo-router"]) {
|
|
902
|
+
dependencies["expo-router"] = "latest";
|
|
903
|
+
warnings.push("Added missing dependency expo-router@latest.");
|
|
904
|
+
}
|
|
905
|
+
for (const dep of STUDIO_PEERS) {
|
|
906
|
+
if (dependencies[dep] || devDependencies[dep]) continue;
|
|
907
|
+
dependencies[dep] = "latest";
|
|
908
|
+
info.push(`Added missing peer dependency ${dep}@latest.`);
|
|
909
|
+
}
|
|
910
|
+
const pkg = {
|
|
911
|
+
...orig,
|
|
912
|
+
name: orig.name ? `${orig.name}-remix-shell` : "remix-shell",
|
|
913
|
+
private: true,
|
|
914
|
+
main: main2,
|
|
915
|
+
scripts: {
|
|
916
|
+
dev: "expo start -c",
|
|
917
|
+
ios: "expo start -c --ios",
|
|
918
|
+
android: "expo start -c --android",
|
|
919
|
+
web: "expo start -c --web",
|
|
920
|
+
...orig.scripts ?? {}
|
|
921
|
+
},
|
|
922
|
+
dependencies,
|
|
923
|
+
devDependencies
|
|
924
|
+
};
|
|
925
|
+
return { pkg, warnings, info };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/utils/templates.ts
|
|
929
|
+
function shellLayoutTsx() {
|
|
930
|
+
return `import { Stack } from 'expo-router';
|
|
931
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
932
|
+
|
|
933
|
+
export default function Layout() {
|
|
934
|
+
return (
|
|
935
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
936
|
+
<Stack screenOptions={{ headerShown: false }} />
|
|
937
|
+
</GestureHandlerRootView>
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
function shellIndexTsx() {
|
|
943
|
+
return `import * as React from 'react';
|
|
944
|
+
import { View } from 'react-native';
|
|
945
|
+
import { Stack } from 'expo-router';
|
|
946
|
+
import { RemixStudio } from '@remixhq/expo-studio';
|
|
947
|
+
|
|
948
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
949
|
+
// @ts-ignore
|
|
950
|
+
import config from '../remix.config.json';
|
|
951
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
952
|
+
// @ts-ignore
|
|
953
|
+
import embeddedMeta from '../assets/remix/base.meta.json';
|
|
954
|
+
|
|
955
|
+
export default function Index() {
|
|
956
|
+
const appId = String((config as any)?.appId || '');
|
|
957
|
+
const appKey = String((config as any)?.appKey || 'MicroMain');
|
|
958
|
+
const clientKey = String((config as any)?.clientKey || '');
|
|
959
|
+
const embeddedBaseBundles = {
|
|
960
|
+
ios: {
|
|
961
|
+
module: require('../assets/remix/base.ios.jsbundle'),
|
|
962
|
+
assetsModule: require('../assets/remix/base.ios.assets.zip'),
|
|
963
|
+
meta: (embeddedMeta as any)?.ios?.bundle,
|
|
964
|
+
assetsMeta: (embeddedMeta as any)?.ios?.assets,
|
|
965
|
+
},
|
|
966
|
+
android: {
|
|
967
|
+
module: require('../assets/remix/base.android.jsbundle'),
|
|
968
|
+
assetsModule: require('../assets/remix/base.android.assets.zip'),
|
|
969
|
+
meta: (embeddedMeta as any)?.android?.bundle,
|
|
970
|
+
assetsMeta: (embeddedMeta as any)?.android?.assets,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
return (
|
|
974
|
+
<>
|
|
975
|
+
<Stack.Screen options={{ headerShown: false }} />
|
|
976
|
+
<View style={{ flex: 1 }}>
|
|
977
|
+
{appId ? (
|
|
978
|
+
<RemixStudio appId={appId} clientKey={clientKey} appKey={appKey} embeddedBaseBundles={embeddedBaseBundles} />
|
|
979
|
+
) : null}
|
|
980
|
+
</View>
|
|
981
|
+
</>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
`;
|
|
985
|
+
}
|
|
986
|
+
function shellBabelConfigJs() {
|
|
987
|
+
return `module.exports = function (api) {
|
|
988
|
+
api.cache(true);
|
|
989
|
+
return {
|
|
990
|
+
presets: ['babel-preset-expo'],
|
|
991
|
+
plugins: ['react-native-reanimated/plugin'],
|
|
992
|
+
};
|
|
993
|
+
};
|
|
994
|
+
`;
|
|
995
|
+
}
|
|
996
|
+
function shellMetroConfigJs() {
|
|
997
|
+
return `const { getDefaultConfig } = require('expo/metro-config');
|
|
998
|
+
|
|
999
|
+
const config = getDefaultConfig(__dirname);
|
|
1000
|
+
const resolver = config.resolver || {};
|
|
1001
|
+
const assetExts = resolver.assetExts || [];
|
|
1002
|
+
if (!assetExts.includes('jsbundle')) {
|
|
1003
|
+
resolver.assetExts = [...assetExts, 'jsbundle'];
|
|
1004
|
+
}
|
|
1005
|
+
if (!assetExts.includes('zip')) {
|
|
1006
|
+
resolver.assetExts = [...assetExts, 'zip'];
|
|
1007
|
+
}
|
|
1008
|
+
config.resolver = resolver;
|
|
1009
|
+
|
|
1010
|
+
module.exports = config;
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/utils/packageManager.ts
|
|
1015
|
+
import fs8 from "fs/promises";
|
|
1016
|
+
async function exists(p) {
|
|
1017
|
+
try {
|
|
1018
|
+
await fs8.stat(p);
|
|
1019
|
+
return true;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
async function detectPackageManager(projectRoot) {
|
|
1025
|
+
if (await exists(`${projectRoot}/pnpm-lock.yaml`)) return "pnpm";
|
|
1026
|
+
if (await exists(`${projectRoot}/yarn.lock`)) return "yarn";
|
|
1027
|
+
if (await exists(`${projectRoot}/package-lock.json`)) return "npm";
|
|
1028
|
+
if (await exists(`${projectRoot}/bun.lockb`)) return "bun";
|
|
1029
|
+
return "npm";
|
|
1030
|
+
}
|
|
1031
|
+
function installCommand(pm) {
|
|
1032
|
+
switch (pm) {
|
|
1033
|
+
case "pnpm":
|
|
1034
|
+
return { cmd: "pnpm", args: ["install"] };
|
|
1035
|
+
case "yarn":
|
|
1036
|
+
return { cmd: "yarn", args: ["install"] };
|
|
1037
|
+
case "bun":
|
|
1038
|
+
return { cmd: "bun", args: ["install"] };
|
|
1039
|
+
case "npm":
|
|
1040
|
+
default:
|
|
1041
|
+
return { cmd: "npm", args: ["install"] };
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/commands/shellInit.ts
|
|
1046
|
+
import { execa as execa3 } from "execa";
|
|
1047
|
+
|
|
1048
|
+
// src/utils/copyProject.ts
|
|
1049
|
+
import path7 from "path";
|
|
1050
|
+
import fse2 from "fs-extra";
|
|
1051
|
+
var ALWAYS_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1052
|
+
"node_modules",
|
|
1053
|
+
".git",
|
|
1054
|
+
".expo",
|
|
1055
|
+
"dist",
|
|
1056
|
+
"build"
|
|
1057
|
+
]);
|
|
1058
|
+
function normalizeRel3(relPath) {
|
|
1059
|
+
return relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1060
|
+
}
|
|
1061
|
+
function shouldExclude(relPath) {
|
|
1062
|
+
const rel = normalizeRel3(relPath);
|
|
1063
|
+
const parts = rel.split("/").filter(Boolean);
|
|
1064
|
+
const top = parts[0] ?? "";
|
|
1065
|
+
if (!top) return false;
|
|
1066
|
+
if (ALWAYS_EXCLUDE_DIRS.has(top)) return true;
|
|
1067
|
+
if (top.startsWith("remix-shell")) return true;
|
|
1068
|
+
if (rel.startsWith("ios/build/") || rel === "ios/build") return true;
|
|
1069
|
+
if (rel.startsWith("android/build/") || rel === "android/build") return true;
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
async function copyProject(params) {
|
|
1073
|
+
const src = path7.resolve(params.projectRoot);
|
|
1074
|
+
const dest = path7.resolve(params.outRoot);
|
|
1075
|
+
const srcExists = await fse2.pathExists(src);
|
|
1076
|
+
if (!srcExists) {
|
|
1077
|
+
throw new CliError("Project root not found.", { exitCode: 2 });
|
|
1078
|
+
}
|
|
1079
|
+
await fse2.copy(src, dest, {
|
|
1080
|
+
dereference: true,
|
|
1081
|
+
preserveTimestamps: true,
|
|
1082
|
+
filter: (srcPath) => {
|
|
1083
|
+
const rel = path7.relative(src, srcPath);
|
|
1084
|
+
if (!rel) return true;
|
|
1085
|
+
return !shouldExclude(rel);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/utils/stripProject.ts
|
|
1091
|
+
import path8 from "path";
|
|
1092
|
+
import fse3 from "fs-extra";
|
|
1093
|
+
var DEFAULT_STRIP_DIRS = ["app", "src", "components", "lib", "hooks", "providers"];
|
|
1094
|
+
async function stripProject(params) {
|
|
1095
|
+
const dirs = params.dirs ?? DEFAULT_STRIP_DIRS;
|
|
1096
|
+
await Promise.all(
|
|
1097
|
+
dirs.map(async (d) => {
|
|
1098
|
+
const p = path8.join(params.outRoot, d);
|
|
1099
|
+
const exists2 = await fse3.pathExists(p);
|
|
1100
|
+
if (!exists2) return;
|
|
1101
|
+
await fse3.remove(p);
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/commands/shellInit.ts
|
|
1107
|
+
import fs12 from "fs/promises";
|
|
1108
|
+
|
|
1109
|
+
// src/utils/appJsonPatch.ts
|
|
1110
|
+
import fs9 from "fs/promises";
|
|
1111
|
+
function pluginName(entry) {
|
|
1112
|
+
if (Array.isArray(entry)) return typeof entry[0] === "string" ? entry[0] : null;
|
|
1113
|
+
return typeof entry === "string" ? entry : null;
|
|
1114
|
+
}
|
|
1115
|
+
async function ensureRemixShellPlugins(appJsonPath) {
|
|
1116
|
+
let raw;
|
|
1117
|
+
try {
|
|
1118
|
+
raw = await fs9.readFile(appJsonPath, "utf8");
|
|
1119
|
+
} catch {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
let parsed;
|
|
1123
|
+
try {
|
|
1124
|
+
parsed = JSON.parse(raw);
|
|
1125
|
+
} catch {
|
|
1126
|
+
throw new CliError("Failed to parse app.json in the generated shell.", { exitCode: 2 });
|
|
1127
|
+
}
|
|
1128
|
+
const expo = parsed.expo;
|
|
1129
|
+
if (!expo || typeof expo !== "object") return false;
|
|
1130
|
+
const plugins = Array.isArray(expo.plugins) ? [...expo.plugins] : [];
|
|
1131
|
+
const routerEntry = plugins.find((p) => pluginName(p) === "expo-router");
|
|
1132
|
+
const runtimeEntry = plugins.find((p) => pluginName(p) === "@remixhq/expo-runtime");
|
|
1133
|
+
const needsRouter = !routerEntry;
|
|
1134
|
+
const needsRuntime = !runtimeEntry;
|
|
1135
|
+
if (!needsRouter && !needsRuntime) return false;
|
|
1136
|
+
const rest = plugins.filter((p) => {
|
|
1137
|
+
const name = pluginName(p);
|
|
1138
|
+
return name !== "expo-router" && name !== "@remixhq/expo-runtime";
|
|
1139
|
+
});
|
|
1140
|
+
expo.plugins = [routerEntry ?? "expo-router", runtimeEntry ?? "@remixhq/expo-runtime", ...rest];
|
|
1141
|
+
await fs9.writeFile(appJsonPath, JSON.stringify({ expo }, null, 2) + "\n", "utf8");
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/utils/reanimated.ts
|
|
1146
|
+
import fs10 from "fs/promises";
|
|
1147
|
+
async function ensureReanimatedBabelPlugin(babelConfigPath) {
|
|
1148
|
+
const raw = await fs10.readFile(babelConfigPath, "utf8").catch(() => null);
|
|
1149
|
+
if (!raw) return false;
|
|
1150
|
+
if (raw.includes("react-native-reanimated/plugin")) return false;
|
|
1151
|
+
const patched = patchBabelConfigAddReanimatedPlugin(raw);
|
|
1152
|
+
if (patched === raw) return false;
|
|
1153
|
+
await fs10.writeFile(babelConfigPath, patched, "utf8");
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
function patchBabelConfigAddReanimatedPlugin(raw) {
|
|
1157
|
+
const pluginsMatch = /(^|\n)([ \t]*)plugins\s*:\s*\[/m.exec(raw);
|
|
1158
|
+
if (pluginsMatch) {
|
|
1159
|
+
const matchIdx = pluginsMatch.index + pluginsMatch[1].length;
|
|
1160
|
+
const indent = pluginsMatch[2] ?? "";
|
|
1161
|
+
const bracketIdx = raw.indexOf("[", matchIdx);
|
|
1162
|
+
if (bracketIdx >= 0) {
|
|
1163
|
+
const closeIdx = findMatchingBracket(raw, bracketIdx, "[", "]");
|
|
1164
|
+
if (closeIdx >= 0) {
|
|
1165
|
+
const inner = raw.slice(bracketIdx + 1, closeIdx);
|
|
1166
|
+
const innerTrim = inner.trim();
|
|
1167
|
+
const innerTrimEnd = inner.replace(/[ \t\r\n]+$/g, "");
|
|
1168
|
+
const elementIndent = indent + " ";
|
|
1169
|
+
let insert = "";
|
|
1170
|
+
if (!innerTrim) {
|
|
1171
|
+
insert = `
|
|
1172
|
+
${elementIndent}'react-native-reanimated/plugin'
|
|
1173
|
+
${indent}`;
|
|
1174
|
+
} else if (innerTrimEnd.endsWith(",")) {
|
|
1175
|
+
insert = `
|
|
1176
|
+
${elementIndent}'react-native-reanimated/plugin'`;
|
|
1177
|
+
} else {
|
|
1178
|
+
insert = `,
|
|
1179
|
+
${elementIndent}'react-native-reanimated/plugin'`;
|
|
1180
|
+
}
|
|
1181
|
+
return raw.slice(0, closeIdx) + insert + raw.slice(closeIdx);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const presetsMatch = /(^|\n)([ \t]*)presets\s*:\s*\[[^\]]*\]\s*,?/m.exec(raw);
|
|
1186
|
+
if (presetsMatch) {
|
|
1187
|
+
const indent = presetsMatch[2] ?? "";
|
|
1188
|
+
const insertAt = presetsMatch.index + presetsMatch[0].length;
|
|
1189
|
+
const insertion = `
|
|
1190
|
+
${indent}// Required by react-native-reanimated. Must be listed last.
|
|
1191
|
+
${indent}plugins: ['react-native-reanimated/plugin'],`;
|
|
1192
|
+
return raw.slice(0, insertAt) + insertion + raw.slice(insertAt);
|
|
1193
|
+
}
|
|
1194
|
+
return raw;
|
|
1195
|
+
}
|
|
1196
|
+
function findMatchingBracket(text, openIdx, open, close) {
|
|
1197
|
+
let depth = 0;
|
|
1198
|
+
let inSingle = false;
|
|
1199
|
+
let inDouble = false;
|
|
1200
|
+
let inTemplate = false;
|
|
1201
|
+
let inLineComment = false;
|
|
1202
|
+
let inBlockComment = false;
|
|
1203
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
1204
|
+
const ch = text[i];
|
|
1205
|
+
const next = text[i + 1];
|
|
1206
|
+
if (inLineComment) {
|
|
1207
|
+
if (ch === "\n") inLineComment = false;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (inBlockComment) {
|
|
1211
|
+
if (ch === "*" && next === "/") {
|
|
1212
|
+
inBlockComment = false;
|
|
1213
|
+
i++;
|
|
1214
|
+
}
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (!inSingle && !inDouble && !inTemplate) {
|
|
1218
|
+
if (ch === "/" && next === "/") {
|
|
1219
|
+
inLineComment = true;
|
|
1220
|
+
i++;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (ch === "/" && next === "*") {
|
|
1224
|
+
inBlockComment = true;
|
|
1225
|
+
i++;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (!inDouble && !inTemplate && ch === "'" && text[i - 1] !== "\\") {
|
|
1230
|
+
inSingle = !inSingle;
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (!inSingle && !inTemplate && ch === `"` && text[i - 1] !== "\\") {
|
|
1234
|
+
inDouble = !inDouble;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (!inSingle && !inDouble && ch === "`" && text[i - 1] !== "\\") {
|
|
1238
|
+
inTemplate = !inTemplate;
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
1242
|
+
if (ch === open) depth++;
|
|
1243
|
+
if (ch === close) {
|
|
1244
|
+
depth--;
|
|
1245
|
+
if (depth === 0) return i;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return -1;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/services/bundles.ts
|
|
1252
|
+
import fs11 from "fs";
|
|
1253
|
+
import fsPromises from "fs/promises";
|
|
1254
|
+
import path9 from "path";
|
|
1255
|
+
import { Readable } from "stream";
|
|
1256
|
+
import { pipeline } from "stream/promises";
|
|
1257
|
+
import pc6 from "picocolors";
|
|
1258
|
+
function sleep2(ms) {
|
|
1259
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1260
|
+
}
|
|
1261
|
+
function isRetryableNetworkError(e) {
|
|
1262
|
+
const err = e;
|
|
1263
|
+
const code = typeof err?.code === "string" ? err.code : "";
|
|
1264
|
+
const message = typeof err?.message === "string" ? err.message : "";
|
|
1265
|
+
if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
|
|
1266
|
+
if (message.toLowerCase().includes("network error")) return true;
|
|
1267
|
+
if (message.toLowerCase().includes("timeout")) return true;
|
|
1268
|
+
const status = typeof err?.response?.status === "number" ? err.response.status : void 0;
|
|
1269
|
+
if (status && (status === 429 || status >= 500)) return true;
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
async function withRetry(fn, opts) {
|
|
1273
|
+
let lastErr = null;
|
|
1274
|
+
for (let attempt = 1; attempt <= opts.attempts; attempt += 1) {
|
|
1275
|
+
try {
|
|
1276
|
+
return await fn();
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
lastErr = e;
|
|
1279
|
+
const retryable = isRetryableNetworkError(e);
|
|
1280
|
+
if (!retryable || attempt >= opts.attempts) {
|
|
1281
|
+
throw e;
|
|
1282
|
+
}
|
|
1283
|
+
const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
|
|
1284
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
1285
|
+
await sleep2(exp + jitter);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
throw lastErr;
|
|
1289
|
+
}
|
|
1290
|
+
function unwrapResponseObject(resp, label) {
|
|
1291
|
+
const obj = resp?.responseObject;
|
|
1292
|
+
if (obj === void 0 || obj === null) {
|
|
1293
|
+
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
1294
|
+
throw new CliError(message, { exitCode: 1, hint: resp ? JSON.stringify(resp, null, 2) : null });
|
|
1295
|
+
}
|
|
1296
|
+
return obj;
|
|
1297
|
+
}
|
|
1298
|
+
async function pollBundle(api, appId, bundleId, opts) {
|
|
1299
|
+
const start = Date.now();
|
|
1300
|
+
while (true) {
|
|
1301
|
+
try {
|
|
1302
|
+
const bundleResp = await api.getBundle(appId, bundleId);
|
|
1303
|
+
const bundle = unwrapResponseObject(bundleResp, "bundle");
|
|
1304
|
+
if (bundle.status === "succeeded" || bundle.status === "failed") return bundle;
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
if (!isRetryableNetworkError(e)) {
|
|
1307
|
+
throw e;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (Date.now() - start > opts.timeoutMs) {
|
|
1311
|
+
throw new Error("Bundle build timed out.");
|
|
1312
|
+
}
|
|
1313
|
+
await sleep2(opts.intervalMs);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function downloadToFile(url, targetPath) {
|
|
1317
|
+
const dir = path9.dirname(targetPath);
|
|
1318
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
1319
|
+
const tmpPath = `${targetPath}.tmp-${Date.now()}`;
|
|
1320
|
+
await withRetry(
|
|
1321
|
+
async () => {
|
|
1322
|
+
const res = await fetch(url);
|
|
1323
|
+
if (!res.ok) {
|
|
1324
|
+
throw new Error(`Download failed (status ${res.status})`);
|
|
1325
|
+
}
|
|
1326
|
+
if (!res.body) {
|
|
1327
|
+
throw new Error("Download response has no body.");
|
|
1328
|
+
}
|
|
1329
|
+
const body = Readable.fromWeb(res.body);
|
|
1330
|
+
const out = fs11.createWriteStream(tmpPath);
|
|
1331
|
+
await pipeline(body, out);
|
|
1332
|
+
},
|
|
1333
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1334
|
+
);
|
|
1335
|
+
const stat = await fsPromises.stat(tmpPath);
|
|
1336
|
+
if (!stat.size || stat.size <= 0) {
|
|
1337
|
+
await fsPromises.unlink(tmpPath).catch(() => {
|
|
1338
|
+
});
|
|
1339
|
+
throw new Error("Downloaded bundle is empty.");
|
|
1340
|
+
}
|
|
1341
|
+
await fsPromises.rename(tmpPath, targetPath);
|
|
1342
|
+
}
|
|
1343
|
+
async function downloadBundle(api, appId, platform, targetPath) {
|
|
1344
|
+
const initiateResp = await api.initiateBundle(appId, {
|
|
1345
|
+
platform,
|
|
1346
|
+
idempotencyKey: `${appId}:head:${platform}`
|
|
1347
|
+
});
|
|
1348
|
+
const initiate = unwrapResponseObject(initiateResp, "bundle initiate");
|
|
1349
|
+
const finalBundle = initiate.status === "succeeded" || initiate.status === "failed" ? initiate : await pollBundle(api, appId, initiate.id, { timeoutMs: 3 * 60 * 1e3, intervalMs: 1200 });
|
|
1350
|
+
if (finalBundle.status === "failed") {
|
|
1351
|
+
throw new Error("Bundle build failed.");
|
|
1352
|
+
}
|
|
1353
|
+
const signedResp = await api.getBundleDownloadUrl(appId, finalBundle.id, { redirect: false });
|
|
1354
|
+
const signed = unwrapResponseObject(signedResp, "bundle download url");
|
|
1355
|
+
const url = String(signed.url || "").trim();
|
|
1356
|
+
if (!url) throw new Error("Download URL is missing.");
|
|
1357
|
+
await downloadToFile(url, targetPath);
|
|
1358
|
+
const fingerprint = finalBundle.checksumSha256 ?? `id:${finalBundle.id}`;
|
|
1359
|
+
const meta = {
|
|
1360
|
+
fingerprint,
|
|
1361
|
+
bundleId: finalBundle.id,
|
|
1362
|
+
checksumSha256: finalBundle.checksumSha256 ?? null,
|
|
1363
|
+
size: finalBundle.size ?? null,
|
|
1364
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1365
|
+
};
|
|
1366
|
+
return { platform, bundlePath: targetPath, bundleId: finalBundle.id, meta };
|
|
1367
|
+
}
|
|
1368
|
+
async function downloadBundleAssets(api, appId, bundleId, targetPath) {
|
|
1369
|
+
const signedResp = await api.getBundleAssetsDownloadUrl(appId, bundleId, {
|
|
1370
|
+
redirect: false,
|
|
1371
|
+
kind: "metro-assets"
|
|
1372
|
+
});
|
|
1373
|
+
const signed = unwrapResponseObject(signedResp, "bundle assets download url");
|
|
1374
|
+
const url = String(signed.url || "").trim();
|
|
1375
|
+
if (!url) throw new Error("Assets download URL is missing.");
|
|
1376
|
+
await downloadToFile(url, targetPath);
|
|
1377
|
+
const stat = await fsPromises.stat(targetPath);
|
|
1378
|
+
const checksumSha256 = await sha256FileHex(targetPath);
|
|
1379
|
+
return {
|
|
1380
|
+
checksumSha256,
|
|
1381
|
+
size: stat.size ?? null,
|
|
1382
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
async function downloadEmbeddedBundles(params) {
|
|
1386
|
+
const log = params.log ?? (() => {
|
|
1387
|
+
});
|
|
1388
|
+
const baseDir = path9.join(params.outRoot, "assets", "remix");
|
|
1389
|
+
const iosPath = path9.join(baseDir, "base.ios.jsbundle");
|
|
1390
|
+
const androidPath = path9.join(baseDir, "base.android.jsbundle");
|
|
1391
|
+
const iosAssetsPath = path9.join(baseDir, "base.ios.assets.zip");
|
|
1392
|
+
const androidAssetsPath = path9.join(baseDir, "base.android.assets.zip");
|
|
1393
|
+
log(pc6.dim("Downloading base bundles for shell..."));
|
|
1394
|
+
const results = await Promise.allSettled([
|
|
1395
|
+
(async () => {
|
|
1396
|
+
const bundle = await downloadBundle(params.api, params.appId, "ios", iosPath);
|
|
1397
|
+
let assets;
|
|
1398
|
+
try {
|
|
1399
|
+
assets = await downloadBundleAssets(params.api, params.appId, bundle.bundleId, iosAssetsPath);
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
log(pc6.yellow(`Warning: failed to download iOS assets (${e?.message ?? e})`));
|
|
1402
|
+
}
|
|
1403
|
+
return { platform: "ios", meta: { bundle: bundle.meta, ...assets ? { assets } : {} } };
|
|
1404
|
+
})(),
|
|
1405
|
+
(async () => {
|
|
1406
|
+
const bundle = await downloadBundle(params.api, params.appId, "android", androidPath);
|
|
1407
|
+
let assets;
|
|
1408
|
+
try {
|
|
1409
|
+
assets = await downloadBundleAssets(params.api, params.appId, bundle.bundleId, androidAssetsPath);
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
log(pc6.yellow(`Warning: failed to download Android assets (${e?.message ?? e})`));
|
|
1412
|
+
}
|
|
1413
|
+
return { platform: "android", meta: { bundle: bundle.meta, ...assets ? { assets } : {} } };
|
|
1414
|
+
})()
|
|
1415
|
+
]);
|
|
1416
|
+
const meta = {};
|
|
1417
|
+
for (const res of results) {
|
|
1418
|
+
if (res.status === "fulfilled") {
|
|
1419
|
+
meta[res.value.platform] = res.value.meta;
|
|
1420
|
+
} else {
|
|
1421
|
+
log(pc6.yellow(`Warning: failed to download ${res.reason?.message ?? res.reason}`));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
await fsPromises.mkdir(baseDir, { recursive: true });
|
|
1425
|
+
await fsPromises.writeFile(path9.join(baseDir, "base.meta.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
1426
|
+
return meta;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/commands/shellInit.ts
|
|
1430
|
+
async function shellInit(params) {
|
|
1431
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
1432
|
+
const outDirDefault = params.outDir || "remix-shell";
|
|
1433
|
+
let outDir = outDirDefault;
|
|
1434
|
+
let appId = params.appId ?? "";
|
|
1435
|
+
let apiKey = params.apiKey ?? "";
|
|
1436
|
+
let appKey = params.appKey ?? "MicroMain";
|
|
1437
|
+
appId = String(appId || "").trim();
|
|
1438
|
+
apiKey = String(apiKey || "").trim();
|
|
1439
|
+
if (!appId) {
|
|
1440
|
+
throw new CliError("Missing app ID.", {
|
|
1441
|
+
hint: "Pass --app-id <uuid>.",
|
|
1442
|
+
exitCode: 2
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
if (!apiKey) {
|
|
1446
|
+
throw new CliError("Missing client key.", {
|
|
1447
|
+
hint: "Pass --api-key <key>.",
|
|
1448
|
+
exitCode: 2
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
const requestedOutputPath = path10.resolve(projectRoot, outDir);
|
|
1452
|
+
const outputPath = await findAvailableDirPath(requestedOutputPath);
|
|
1453
|
+
console.log(pc7.dim(`Project: ${projectRoot}`));
|
|
1454
|
+
if (outputPath !== requestedOutputPath) {
|
|
1455
|
+
console.log(pc7.dim(`Output: ${requestedOutputPath} (already exists)`));
|
|
1456
|
+
console.log(pc7.dim(`Using: ${outputPath}`));
|
|
1457
|
+
} else {
|
|
1458
|
+
console.log(pc7.dim(`Output: ${outputPath}`));
|
|
1459
|
+
}
|
|
1460
|
+
const outputIsInsideProject = outputPath === projectRoot || outputPath.startsWith(projectRoot + path10.sep);
|
|
1461
|
+
const stagingPath = outputIsInsideProject ? await fs12.mkdtemp(path10.join(os3.tmpdir(), "remix-shell-")) : outputPath;
|
|
1462
|
+
let movedToFinal = false;
|
|
1463
|
+
try {
|
|
1464
|
+
if (!outputIsInsideProject) {
|
|
1465
|
+
await ensureEmptyDir(stagingPath);
|
|
1466
|
+
}
|
|
1467
|
+
await copyProject({ projectRoot, outRoot: stagingPath });
|
|
1468
|
+
await stripProject({ outRoot: stagingPath });
|
|
1469
|
+
try {
|
|
1470
|
+
const didPatch = await ensureRemixShellPlugins(path10.join(stagingPath, "app.json"));
|
|
1471
|
+
if (didPatch) {
|
|
1472
|
+
console.log(pc7.dim("Updated app.json with required Remix shell plugins."));
|
|
1473
|
+
}
|
|
1474
|
+
} catch {
|
|
1475
|
+
}
|
|
1476
|
+
await writeJsonAtomic(path10.join(stagingPath, "remix.config.json"), {
|
|
1477
|
+
appId,
|
|
1478
|
+
appKey: appKey || "MicroMain",
|
|
1479
|
+
clientKey: apiKey
|
|
1480
|
+
});
|
|
1481
|
+
try {
|
|
1482
|
+
const cfg = await resolveConfig({ yes: true });
|
|
1483
|
+
const api = createApiClient(cfg, { apiKey });
|
|
1484
|
+
await downloadEmbeddedBundles({
|
|
1485
|
+
api,
|
|
1486
|
+
appId,
|
|
1487
|
+
outRoot: stagingPath,
|
|
1488
|
+
log: (msg) => console.log(msg)
|
|
1489
|
+
});
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
console.log(pc7.yellow(`Warning: failed to embed base bundles: ${e instanceof Error ? e.message : String(e)}`));
|
|
1492
|
+
}
|
|
1493
|
+
await writeTextAtomic(path10.join(stagingPath, "app/_layout.tsx"), shellLayoutTsx());
|
|
1494
|
+
await writeTextAtomic(path10.join(stagingPath, "app/index.tsx"), shellIndexTsx());
|
|
1495
|
+
await ensureTextFile(path10.join(stagingPath, "babel.config.js"), shellBabelConfigJs());
|
|
1496
|
+
await ensureTextFile(path10.join(stagingPath, "metro.config.js"), shellMetroConfigJs());
|
|
1497
|
+
try {
|
|
1498
|
+
const didPatch = await ensureReanimatedBabelPlugin(path10.join(stagingPath, "babel.config.js"));
|
|
1499
|
+
if (didPatch) console.log(pc7.dim("Updated babel.config.js to include react-native-reanimated/plugin."));
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
await ensureTextFile(
|
|
1503
|
+
path10.join(stagingPath, "tsconfig.json"),
|
|
1504
|
+
JSON.stringify(
|
|
1505
|
+
{
|
|
1506
|
+
extends: "expo/tsconfig.base",
|
|
1507
|
+
compilerOptions: { strict: true, resolveJsonModule: true }
|
|
1508
|
+
},
|
|
1509
|
+
null,
|
|
1510
|
+
2
|
|
1511
|
+
) + "\n"
|
|
1512
|
+
);
|
|
1513
|
+
const originalPkg = await readPackageJson(stagingPath);
|
|
1514
|
+
const { pkg: shellPkg, warnings, info } = buildShellPackageJson({
|
|
1515
|
+
original: originalPkg
|
|
1516
|
+
});
|
|
1517
|
+
await writeJsonAtomic(path10.join(stagingPath, "package.json"), shellPkg);
|
|
1518
|
+
for (const w of warnings) console.log(pc7.yellow(`Warning: ${w}`));
|
|
1519
|
+
for (const i of info) console.log(pc7.dim(`Info: ${i}`));
|
|
1520
|
+
if (outputIsInsideProject) {
|
|
1521
|
+
await fse4.move(stagingPath, outputPath, { overwrite: false });
|
|
1522
|
+
movedToFinal = true;
|
|
1523
|
+
}
|
|
1524
|
+
const finalPath = outputIsInsideProject ? outputPath : stagingPath;
|
|
1525
|
+
const pm = params.packageManager ?? await detectPackageManager(projectRoot);
|
|
1526
|
+
const { cmd, args } = installCommand(pm);
|
|
1527
|
+
console.log(pc7.dim(`Installing dependencies with ${cmd}...`));
|
|
1528
|
+
const res = await execa3(cmd, args, { cwd: finalPath, stdio: "inherit" });
|
|
1529
|
+
if (res.exitCode !== 0) {
|
|
1530
|
+
throw new CliError("Dependency installation failed.", { exitCode: res.exitCode ?? 1 });
|
|
1531
|
+
}
|
|
1532
|
+
console.log(pc7.dim("Running Expo prebuild..."));
|
|
1533
|
+
const prebuild = await execa3("npx", ["expo", "prebuild"], { cwd: finalPath, stdio: "inherit" });
|
|
1534
|
+
if (prebuild.exitCode !== 0) {
|
|
1535
|
+
throw new CliError("Expo prebuild failed.", { exitCode: prebuild.exitCode ?? 1 });
|
|
1536
|
+
}
|
|
1537
|
+
console.log(pc7.green("Remix has been installed successfully."));
|
|
1538
|
+
console.log(pc7.dim("You can now open the app from the My apps page in the Remix app."));
|
|
1539
|
+
} finally {
|
|
1540
|
+
if (outputIsInsideProject && !movedToFinal) {
|
|
1541
|
+
try {
|
|
1542
|
+
await fse4.remove(stagingPath);
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
async function ensureTextFile(filePath, contents) {
|
|
1549
|
+
try {
|
|
1550
|
+
await fs12.access(filePath);
|
|
1551
|
+
return;
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
await writeTextAtomic(filePath, contents);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/utils/appDefaults.ts
|
|
1558
|
+
import fs13 from "fs/promises";
|
|
1559
|
+
import path11 from "path";
|
|
1560
|
+
import fg2 from "fast-glob";
|
|
1561
|
+
import { execa as execa4 } from "execa";
|
|
1562
|
+
function normalizeString(value) {
|
|
1563
|
+
if (typeof value !== "string") return null;
|
|
1564
|
+
const trimmed = value.trim();
|
|
1565
|
+
return trimmed ? trimmed : null;
|
|
1566
|
+
}
|
|
1567
|
+
function normalizeSlug(value) {
|
|
1568
|
+
if (!value) return null;
|
|
1569
|
+
const cleaned = value.trim().toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1570
|
+
return cleaned || null;
|
|
1571
|
+
}
|
|
1572
|
+
function suggestBundleId(slug) {
|
|
1573
|
+
if (!slug) return null;
|
|
1574
|
+
return `com.example.${slug}`;
|
|
1575
|
+
}
|
|
1576
|
+
function derivePlatforms(config, iosBundleId, androidPackageName) {
|
|
1577
|
+
const platforms = Array.isArray(config.platforms) ? config.platforms.map(String) : null;
|
|
1578
|
+
const iosEnabled = platforms ? platforms.includes("ios") : iosBundleId ? true : null;
|
|
1579
|
+
const androidEnabled = platforms ? platforms.includes("android") : androidPackageName ? true : null;
|
|
1580
|
+
return { iosEnabled, androidEnabled };
|
|
1581
|
+
}
|
|
1582
|
+
async function loadExpoConfig(projectRoot) {
|
|
1583
|
+
try {
|
|
1584
|
+
const res = await execa4("npx", ["expo", "config", "--json"], { cwd: projectRoot });
|
|
1585
|
+
const parsed = JSON.parse(res.stdout);
|
|
1586
|
+
if (parsed && typeof parsed === "object" && "expo" in parsed) {
|
|
1587
|
+
return parsed.expo ?? {};
|
|
1588
|
+
}
|
|
1589
|
+
return parsed;
|
|
1590
|
+
} catch {
|
|
1591
|
+
}
|
|
1592
|
+
const appJsonPath = path11.join(projectRoot, "app.json");
|
|
1593
|
+
const rawAppJson = await fs13.readFile(appJsonPath, "utf8").catch(() => null);
|
|
1594
|
+
if (!rawAppJson) return {};
|
|
1595
|
+
try {
|
|
1596
|
+
const parsed = JSON.parse(rawAppJson);
|
|
1597
|
+
if (parsed && typeof parsed === "object" && "expo" in parsed) {
|
|
1598
|
+
return parsed.expo ?? {};
|
|
1599
|
+
}
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
return {};
|
|
1603
|
+
}
|
|
1604
|
+
async function findIosBundleId(projectRoot) {
|
|
1605
|
+
const pbxprojMatches = await fg2("ios/**/*.xcodeproj/project.pbxproj", {
|
|
1606
|
+
cwd: projectRoot,
|
|
1607
|
+
absolute: true,
|
|
1608
|
+
suppressErrors: true
|
|
1609
|
+
});
|
|
1610
|
+
for (const file of pbxprojMatches) {
|
|
1611
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1612
|
+
if (!raw) continue;
|
|
1613
|
+
const match = raw.match(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([A-Za-z0-9.\-]+)\s*;/);
|
|
1614
|
+
if (match && match[1]) return match[1];
|
|
1615
|
+
}
|
|
1616
|
+
const plistMatches = await fg2("ios/**/Info.plist", {
|
|
1617
|
+
cwd: projectRoot,
|
|
1618
|
+
absolute: true,
|
|
1619
|
+
suppressErrors: true
|
|
1620
|
+
});
|
|
1621
|
+
for (const file of plistMatches) {
|
|
1622
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1623
|
+
if (!raw) continue;
|
|
1624
|
+
const match = raw.match(/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/);
|
|
1625
|
+
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
async function findAndroidPackageName(projectRoot) {
|
|
1630
|
+
const gradlePaths = ["android/app/build.gradle", "android/app/build.gradle.kts"];
|
|
1631
|
+
for (const rel of gradlePaths) {
|
|
1632
|
+
const file = path11.join(projectRoot, rel);
|
|
1633
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1634
|
+
if (!raw) continue;
|
|
1635
|
+
const match = raw.match(/applicationId\s+["']([^"']+)["']/) || raw.match(/applicationId\s*=\s*["']([^"']+)["']/);
|
|
1636
|
+
if (match && match[1]) return match[1];
|
|
1637
|
+
}
|
|
1638
|
+
const manifestMatches = await fg2("android/**/AndroidManifest.xml", {
|
|
1639
|
+
cwd: projectRoot,
|
|
1640
|
+
absolute: true,
|
|
1641
|
+
suppressErrors: true
|
|
1642
|
+
});
|
|
1643
|
+
for (const file of manifestMatches) {
|
|
1644
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1645
|
+
if (!raw) continue;
|
|
1646
|
+
const match = raw.match(/<manifest[^>]*\spackage="([^"]+)"/);
|
|
1647
|
+
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
1648
|
+
}
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
async function inferExpoAppDefaults(projectRoot) {
|
|
1652
|
+
const expoConfig = await loadExpoConfig(projectRoot);
|
|
1653
|
+
const name = normalizeString(expoConfig.name);
|
|
1654
|
+
const slug = normalizeSlug(normalizeString(expoConfig.slug));
|
|
1655
|
+
const iosBundleId = normalizeString(expoConfig.ios?.bundleIdentifier);
|
|
1656
|
+
const androidPackageName = normalizeString(expoConfig.android?.package);
|
|
1657
|
+
const platforms = derivePlatforms(expoConfig, iosBundleId, androidPackageName);
|
|
1658
|
+
let fallbackName = name;
|
|
1659
|
+
if (!fallbackName) {
|
|
1660
|
+
const pkg = await readPackageJson(projectRoot).catch(() => null);
|
|
1661
|
+
fallbackName = normalizeString(pkg?.name ?? null);
|
|
1662
|
+
}
|
|
1663
|
+
if (!fallbackName) {
|
|
1664
|
+
fallbackName = normalizeString(path11.basename(projectRoot));
|
|
1665
|
+
}
|
|
1666
|
+
const resolvedSlug = slug ?? normalizeSlug(fallbackName ?? null) ?? normalizeSlug(path11.basename(projectRoot));
|
|
1667
|
+
const suggestedBundleId = suggestBundleId(resolvedSlug);
|
|
1668
|
+
const nativeIosBundleId = iosBundleId ?? await findIosBundleId(projectRoot);
|
|
1669
|
+
const nativeAndroidPackage = androidPackageName ?? await findAndroidPackageName(projectRoot);
|
|
1670
|
+
return {
|
|
1671
|
+
name: fallbackName,
|
|
1672
|
+
slug: resolvedSlug,
|
|
1673
|
+
iosBundleId: nativeIosBundleId,
|
|
1674
|
+
androidPackageName: nativeAndroidPackage,
|
|
1675
|
+
iosEnabled: platforms.iosEnabled,
|
|
1676
|
+
androidEnabled: platforms.androidEnabled,
|
|
1677
|
+
suggestedBundleId
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/services/apiKeyStore.ts
|
|
1682
|
+
import fs14 from "fs/promises";
|
|
1683
|
+
import os4 from "os";
|
|
1684
|
+
import path12 from "path";
|
|
1685
|
+
var KEYTAR_SERVICE2 = "remix-cli-api-keys";
|
|
1686
|
+
function xdgConfigHome2() {
|
|
1687
|
+
const v = process.env.XDG_CONFIG_HOME;
|
|
1688
|
+
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
1689
|
+
return path12.join(os4.homedir(), ".config");
|
|
1690
|
+
}
|
|
1691
|
+
function apiKeyFilePath() {
|
|
1692
|
+
return path12.join(xdgConfigHome2(), "remix", "api-keys.json");
|
|
1693
|
+
}
|
|
1694
|
+
async function ensurePathPermissions2(filePath) {
|
|
1695
|
+
const dir = path12.dirname(filePath);
|
|
1696
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
1697
|
+
try {
|
|
1698
|
+
await fs14.chmod(dir, 448);
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
await fs14.chmod(filePath, 384);
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function maybeLoadKeytar2() {
|
|
1707
|
+
try {
|
|
1708
|
+
const mod = await import("keytar");
|
|
1709
|
+
const candidates = [mod?.default, mod].filter(Boolean);
|
|
1710
|
+
for (const c of candidates) {
|
|
1711
|
+
if (c && typeof c.getPassword === "function" && typeof c.setPassword === "function") {
|
|
1712
|
+
return c;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return null;
|
|
1716
|
+
} catch {
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function keyId(orgId, clientAppId) {
|
|
1721
|
+
return `${orgId}:${clientAppId}`;
|
|
1722
|
+
}
|
|
1723
|
+
async function readKeyFile() {
|
|
1724
|
+
const fp = apiKeyFilePath();
|
|
1725
|
+
const raw = await fs14.readFile(fp, "utf8").catch(() => null);
|
|
1726
|
+
if (!raw) return {};
|
|
1727
|
+
try {
|
|
1728
|
+
const parsed = JSON.parse(raw);
|
|
1729
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
1730
|
+
return parsed;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return {};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function getStoredApiKey(orgId, clientAppId) {
|
|
1736
|
+
const account = keyId(orgId, clientAppId);
|
|
1737
|
+
const keytar = await maybeLoadKeytar2();
|
|
1738
|
+
if (keytar) {
|
|
1739
|
+
const raw = await keytar.getPassword(KEYTAR_SERVICE2, account);
|
|
1740
|
+
return raw && raw.trim() ? raw.trim() : null;
|
|
1741
|
+
}
|
|
1742
|
+
const map = await readKeyFile();
|
|
1743
|
+
const key = map[account];
|
|
1744
|
+
return key && key.trim() ? key.trim() : null;
|
|
1745
|
+
}
|
|
1746
|
+
async function setStoredApiKey(orgId, clientAppId, apiKey) {
|
|
1747
|
+
const account = keyId(orgId, clientAppId);
|
|
1748
|
+
const trimmed = String(apiKey || "").trim();
|
|
1749
|
+
if (!trimmed) {
|
|
1750
|
+
throw new CliError("API key is empty and was not stored.", { exitCode: 1 });
|
|
1751
|
+
}
|
|
1752
|
+
const keytar = await maybeLoadKeytar2();
|
|
1753
|
+
if (keytar) {
|
|
1754
|
+
await keytar.setPassword(KEYTAR_SERVICE2, account, trimmed);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const fp = apiKeyFilePath();
|
|
1758
|
+
const map = await readKeyFile();
|
|
1759
|
+
map[account] = trimmed;
|
|
1760
|
+
await writeJsonAtomic(fp, map);
|
|
1761
|
+
await ensurePathPermissions2(fp);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// src/services/initLocal.ts
|
|
1765
|
+
function unwrapResponseObject2(resp, label) {
|
|
1766
|
+
const obj = resp?.responseObject;
|
|
1767
|
+
if (obj === void 0 || obj === null) {
|
|
1768
|
+
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
1769
|
+
throw new CliError(message, { exitCode: 1, hint: resp ? JSON.stringify(resp, null, 2) : null });
|
|
1770
|
+
}
|
|
1771
|
+
return obj;
|
|
1772
|
+
}
|
|
1773
|
+
async function selectOrganization(orgs, yes) {
|
|
1774
|
+
if (orgs.length === 0) {
|
|
1775
|
+
throw new CliError("No organizations found for your account.", { exitCode: 1 });
|
|
1776
|
+
}
|
|
1777
|
+
if (orgs.length === 1) return orgs[0];
|
|
1778
|
+
if (yes || !isInteractive()) {
|
|
1779
|
+
throw new CliError("Multiple organizations found. Re-run without --yes to select one.", { exitCode: 2 });
|
|
1780
|
+
}
|
|
1781
|
+
const res = await prompts(
|
|
1782
|
+
[
|
|
1783
|
+
{
|
|
1784
|
+
type: "select",
|
|
1785
|
+
name: "orgId",
|
|
1786
|
+
message: "Select an organization",
|
|
1787
|
+
choices: orgs.map((org) => ({
|
|
1788
|
+
title: org.name ? `${org.name} (${org.id})` : org.id,
|
|
1789
|
+
value: org.id
|
|
1790
|
+
}))
|
|
1791
|
+
}
|
|
1792
|
+
],
|
|
1793
|
+
{
|
|
1794
|
+
onCancel: () => {
|
|
1795
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
const selected = orgs.find((org) => org.id === res.orgId);
|
|
1800
|
+
if (!selected) throw new CliError("Invalid organization selection.", { exitCode: 1 });
|
|
1801
|
+
return selected;
|
|
1802
|
+
}
|
|
1803
|
+
async function selectClientApp(apps, yes) {
|
|
1804
|
+
if (apps.length === 0) {
|
|
1805
|
+
if (yes || !isInteractive()) {
|
|
1806
|
+
return { app: null };
|
|
1807
|
+
}
|
|
1808
|
+
console.log(
|
|
1809
|
+
pc8.dim(
|
|
1810
|
+
"No apps found. Create one here or in the Remix dashboard at dashboard.remix.one."
|
|
1811
|
+
)
|
|
1812
|
+
);
|
|
1813
|
+
const res2 = await prompts(
|
|
1814
|
+
[
|
|
1815
|
+
{
|
|
1816
|
+
type: "select",
|
|
1817
|
+
name: "appId",
|
|
1818
|
+
message: "Select an app or create a new one:",
|
|
1819
|
+
choices: [{ title: "Create a new app", value: "__create_new__" }]
|
|
1820
|
+
}
|
|
1821
|
+
],
|
|
1822
|
+
{
|
|
1823
|
+
onCancel: () => {
|
|
1824
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
);
|
|
1828
|
+
if (res2.appId === "__create_new__") return { app: null };
|
|
1829
|
+
throw new CliError("Invalid app selection.", { exitCode: 1 });
|
|
1830
|
+
}
|
|
1831
|
+
if (yes || !isInteractive()) {
|
|
1832
|
+
throw new CliError("Multiple apps found. Re-run without --yes to select one.", { exitCode: 2 });
|
|
1833
|
+
}
|
|
1834
|
+
const res = await prompts(
|
|
1835
|
+
[
|
|
1836
|
+
{
|
|
1837
|
+
type: "select",
|
|
1838
|
+
name: "appId",
|
|
1839
|
+
message: "Select an app or create a new one:",
|
|
1840
|
+
choices: [
|
|
1841
|
+
...apps.map((app) => ({
|
|
1842
|
+
title: app.name ? `${app.name} (${app.id})` : app.id,
|
|
1843
|
+
value: app.id
|
|
1844
|
+
})),
|
|
1845
|
+
{ title: "Create a new app", value: "__create_new__" }
|
|
1846
|
+
]
|
|
1847
|
+
}
|
|
1848
|
+
],
|
|
1849
|
+
{
|
|
1850
|
+
onCancel: () => {
|
|
1851
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
);
|
|
1855
|
+
if (res.appId === "__create_new__") return { app: null };
|
|
1856
|
+
const selected = apps.find((app) => app.id === res.appId);
|
|
1857
|
+
if (!selected) throw new CliError("Invalid app selection.", { exitCode: 1 });
|
|
1858
|
+
return { app: selected };
|
|
1859
|
+
}
|
|
1860
|
+
async function promptNewAppDetails(projectRoot, yes) {
|
|
1861
|
+
const defaults = await inferExpoAppDefaults(projectRoot);
|
|
1862
|
+
const defaultName = defaults.name || "My App";
|
|
1863
|
+
const iosEnabled = true;
|
|
1864
|
+
const androidEnabled = true;
|
|
1865
|
+
if (yes || !isInteractive()) {
|
|
1866
|
+
return {
|
|
1867
|
+
name: defaultName,
|
|
1868
|
+
iosEnabled,
|
|
1869
|
+
androidEnabled,
|
|
1870
|
+
iosBundleId: defaults.iosBundleId || defaults.suggestedBundleId || "com.example.app",
|
|
1871
|
+
androidPackageName: defaults.androidPackageName || defaults.suggestedBundleId || "com.example.app"
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
const answers = await prompts(
|
|
1875
|
+
[
|
|
1876
|
+
{
|
|
1877
|
+
type: "text",
|
|
1878
|
+
name: "name",
|
|
1879
|
+
message: "App name",
|
|
1880
|
+
initial: defaultName,
|
|
1881
|
+
validate: (v) => String(v || "").trim() ? true : "App name is required."
|
|
1882
|
+
}
|
|
1883
|
+
],
|
|
1884
|
+
{
|
|
1885
|
+
onCancel: () => {
|
|
1886
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
);
|
|
1890
|
+
const bundlePrompts = [
|
|
1891
|
+
iosEnabled ? {
|
|
1892
|
+
type: "text",
|
|
1893
|
+
name: "iosBundleId",
|
|
1894
|
+
message: "iOS bundle ID",
|
|
1895
|
+
initial: defaults.iosBundleId || defaults.suggestedBundleId || "com.example.app",
|
|
1896
|
+
validate: (v) => String(v || "").trim() ? true : "iOS bundle ID is required."
|
|
1897
|
+
} : null,
|
|
1898
|
+
androidEnabled ? {
|
|
1899
|
+
type: "text",
|
|
1900
|
+
name: "androidPackageName",
|
|
1901
|
+
message: "Android package name",
|
|
1902
|
+
initial: defaults.androidPackageName || defaults.suggestedBundleId || "com.example.app",
|
|
1903
|
+
validate: (v) => String(v || "").trim() ? true : "Android package name is required."
|
|
1904
|
+
} : null
|
|
1905
|
+
].filter(Boolean);
|
|
1906
|
+
const bundleAnswers = await prompts(bundlePrompts, {
|
|
1907
|
+
onCancel: () => {
|
|
1908
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
return {
|
|
1912
|
+
name: String(answers.name || defaultName).trim(),
|
|
1913
|
+
iosEnabled,
|
|
1914
|
+
androidEnabled,
|
|
1915
|
+
iosBundleId: iosEnabled ? String(bundleAnswers.iosBundleId || "").trim() : null,
|
|
1916
|
+
androidPackageName: androidEnabled ? String(bundleAnswers.androidPackageName || "").trim() : null
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function buildKeyName(params) {
|
|
1920
|
+
const rawUser = params.email && params.email.includes("@") ? params.email.split("@")[0] : params.id || "user";
|
|
1921
|
+
const user = rawUser.trim().replace(/\s+/g, "-");
|
|
1922
|
+
const hostname = os5.hostname().trim().replace(/\s+/g, "-");
|
|
1923
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
1924
|
+
return `cli_${user}_${hostname}_${ts}`;
|
|
1925
|
+
}
|
|
1926
|
+
async function initLocal(params) {
|
|
1927
|
+
await ensureAuth({ yes: params.yes, json: params.json });
|
|
1928
|
+
const cfg = await resolveConfig({ yes: params.yes });
|
|
1929
|
+
const api = createApiClient(cfg);
|
|
1930
|
+
const appKey = String(params.appKey || "MicroMain").trim() || "MicroMain";
|
|
1931
|
+
try {
|
|
1932
|
+
await api.autoEnableDeveloper();
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
const orgs = unwrapResponseObject2(await api.listOrganizations(), "organizations");
|
|
1936
|
+
const selectedOrg = await selectOrganization(orgs, params.yes);
|
|
1937
|
+
const apps = unwrapResponseObject2(
|
|
1938
|
+
await api.listClientApps({ orgId: selectedOrg.id }),
|
|
1939
|
+
"client apps"
|
|
1940
|
+
);
|
|
1941
|
+
const selectedAppResult = await selectClientApp(apps, params.yes);
|
|
1942
|
+
let clientApp = selectedAppResult.app;
|
|
1943
|
+
let importAppName = clientApp?.name ?? null;
|
|
1944
|
+
if (!clientApp) {
|
|
1945
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
1946
|
+
const details = await promptNewAppDetails(projectRoot, params.yes);
|
|
1947
|
+
const createdResp = await api.createClientApp({
|
|
1948
|
+
orgId: selectedOrg.id,
|
|
1949
|
+
name: details.name,
|
|
1950
|
+
type: "wrapper",
|
|
1951
|
+
environment: "production",
|
|
1952
|
+
platform: "expo",
|
|
1953
|
+
iosBundleId: details.iosEnabled ? details.iosBundleId ?? void 0 : void 0,
|
|
1954
|
+
androidPackageName: details.androidEnabled ? details.androidPackageName ?? void 0 : void 0
|
|
1955
|
+
});
|
|
1956
|
+
clientApp = unwrapResponseObject2(createdResp, "client app");
|
|
1957
|
+
importAppName = details.name;
|
|
1958
|
+
}
|
|
1959
|
+
const clientAppId = String(clientApp.id || "").trim();
|
|
1960
|
+
if (!clientAppId) {
|
|
1961
|
+
throw new CliError("Missing client app ID.", { exitCode: 1 });
|
|
1962
|
+
}
|
|
1963
|
+
let apiKey = await getStoredApiKey(selectedOrg.id, clientAppId);
|
|
1964
|
+
if (!apiKey) {
|
|
1965
|
+
const me = unwrapResponseObject2(await api.getMe(), "user");
|
|
1966
|
+
const keyName = buildKeyName({ email: me.email ?? null, id: me.id ?? null });
|
|
1967
|
+
const createdKeyResp = await api.createClientAppKey(clientAppId, { name: keyName });
|
|
1968
|
+
const keyPayload = unwrapResponseObject2(createdKeyResp, "client key");
|
|
1969
|
+
apiKey = String(keyPayload.key || "").trim();
|
|
1970
|
+
if (!apiKey) {
|
|
1971
|
+
throw new CliError("Server did not return a client key.", { exitCode: 1 });
|
|
1972
|
+
}
|
|
1973
|
+
await setStoredApiKey(selectedOrg.id, clientAppId, apiKey);
|
|
1974
|
+
}
|
|
1975
|
+
if (!params.json) console.log(pc8.dim("Importing project..."));
|
|
1976
|
+
const imported = await importLocal({
|
|
1977
|
+
apiKey,
|
|
1978
|
+
yes: params.yes,
|
|
1979
|
+
appName: importAppName,
|
|
1980
|
+
subdir: params.subdir,
|
|
1981
|
+
json: params.json,
|
|
1982
|
+
includeDotenv: params.includeDotenv,
|
|
1983
|
+
dryRun: false
|
|
1984
|
+
});
|
|
1985
|
+
if (!params.json) console.log(pc8.dim("Generating shell app..."));
|
|
1986
|
+
await shellInit({
|
|
1987
|
+
outDir: params.outDir,
|
|
1988
|
+
appId: imported.appId,
|
|
1989
|
+
apiKey,
|
|
1990
|
+
appKey,
|
|
1991
|
+
packageManager: params.packageManager
|
|
1992
|
+
});
|
|
1993
|
+
return {
|
|
1994
|
+
appId: imported.appId,
|
|
1995
|
+
uploadId: imported.uploadId,
|
|
1996
|
+
projectId: imported.projectId,
|
|
1997
|
+
threadId: imported.threadId,
|
|
1998
|
+
status: imported.status,
|
|
1999
|
+
shellOutDir: params.outDir
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/commands/init.ts
|
|
2004
|
+
function registerInitCommands(program) {
|
|
2005
|
+
const init = program.command("init").description("Import into Remix and generate the shell wrapper");
|
|
2006
|
+
init.description("Sign in if needed, import the local Expo project, then generate the shell wrapper").option("--out <dir>", "Output directory for the shell wrapper", "remix-shell").option("--package-manager <pm>", "Override package manager detection (pnpm|npm|yarn|bun)").option("--path <subdir>", "Optional subdirectory to import").option("--no-include-dotenv", "Exclude .env* files from the upload").option("--yes", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2007
|
+
const res = await initLocal({
|
|
2008
|
+
appKey: "MicroMain",
|
|
2009
|
+
outDir: opts.out ? String(opts.out) : "remix-shell",
|
|
2010
|
+
packageManager: opts.packageManager ? String(opts.packageManager) : null,
|
|
2011
|
+
subdir: opts.path ? String(opts.path) : null,
|
|
2012
|
+
includeDotenv: Boolean(opts.includeDotenv),
|
|
2013
|
+
yes: Boolean(opts.yes),
|
|
2014
|
+
json: Boolean(opts.json)
|
|
2015
|
+
});
|
|
2016
|
+
if (opts.json) {
|
|
2017
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
console.log(pc9.green(`Completed. appId=${res.appId}`));
|
|
2021
|
+
if (res.status) console.log(pc9.dim(`Status: ${res.status}`));
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/services/collab.ts
|
|
2026
|
+
import fs15 from "fs/promises";
|
|
2027
|
+
import path13 from "path";
|
|
2028
|
+
import pc10 from "picocolors";
|
|
2029
|
+
import {
|
|
2030
|
+
collabAdd as collabAddCore,
|
|
2031
|
+
collabRecordTurn as collabRecordTurnCore,
|
|
2032
|
+
collabApprove as collabApproveCore,
|
|
2033
|
+
collabInbox as collabInboxCore,
|
|
2034
|
+
collabInit as collabInitCore,
|
|
2035
|
+
collabInvite as collabInviteCore,
|
|
2036
|
+
collabList as collabListCore,
|
|
2037
|
+
collabReconcile as collabReconcileCore,
|
|
2038
|
+
collabReject as collabRejectCore,
|
|
2039
|
+
collabRemix as collabRemixCore,
|
|
2040
|
+
collabRequestMerge as collabRequestMergeCore,
|
|
2041
|
+
collabStatus as collabStatusCore,
|
|
2042
|
+
collabSync as collabSyncCore,
|
|
2043
|
+
collabSyncUpstream as collabSyncUpstreamCore,
|
|
2044
|
+
collabView as collabViewCore
|
|
2045
|
+
} from "@remixhq/core/collab";
|
|
2046
|
+
async function readStdin() {
|
|
2047
|
+
const chunks = [];
|
|
2048
|
+
for await (const chunk of process.stdin) {
|
|
2049
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
2050
|
+
}
|
|
2051
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2052
|
+
}
|
|
2053
|
+
function logMaybe(json, message) {
|
|
2054
|
+
if (!json) console.log(message);
|
|
2055
|
+
}
|
|
2056
|
+
function collectResultWarnings(value) {
|
|
2057
|
+
if (!value || typeof value !== "object") return [];
|
|
2058
|
+
const warnings = value.warnings;
|
|
2059
|
+
if (!Array.isArray(warnings)) return [];
|
|
2060
|
+
return warnings.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
2061
|
+
}
|
|
2062
|
+
function printResultWarnings(json, value) {
|
|
2063
|
+
if (json) return;
|
|
2064
|
+
for (const warning of collectResultWarnings(value)) {
|
|
2065
|
+
console.log(pc10.yellow(warning));
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
async function createCollabApi(params) {
|
|
2069
|
+
await ensureAuth({ yes: params.yes, json: params.json });
|
|
2070
|
+
const cfg = await resolveConfig({ yes: params.yes });
|
|
2071
|
+
return createApiClient(cfg);
|
|
2072
|
+
}
|
|
2073
|
+
async function resolveBoundCollabConfig(cwd) {
|
|
2074
|
+
let current = path13.resolve(cwd);
|
|
2075
|
+
while (true) {
|
|
2076
|
+
const candidate = path13.join(current, ".remix", "config.json");
|
|
2077
|
+
try {
|
|
2078
|
+
const raw = await fs15.readFile(candidate, "utf8");
|
|
2079
|
+
const parsed = JSON.parse(raw);
|
|
2080
|
+
if (parsed.schemaVersion === 1 && parsed.projectId && parsed.currentAppId) {
|
|
2081
|
+
return {
|
|
2082
|
+
schemaVersion: 1,
|
|
2083
|
+
projectId: parsed.projectId,
|
|
2084
|
+
currentAppId: parsed.currentAppId
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
const parent = path13.dirname(current);
|
|
2090
|
+
if (parent === current) return null;
|
|
2091
|
+
current = parent;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
async function resolveInviteTargetId(params) {
|
|
2095
|
+
if (params.targetId?.trim()) return params.targetId.trim();
|
|
2096
|
+
const binding = await resolveBoundCollabConfig(params.cwd);
|
|
2097
|
+
if (!binding) {
|
|
2098
|
+
throw new CliError("Repository is not bound to Remix and no explicit target id was provided.", { exitCode: 2 });
|
|
2099
|
+
}
|
|
2100
|
+
if (params.scope === "project") return binding.projectId;
|
|
2101
|
+
if (params.scope === "app") return binding.currentAppId;
|
|
2102
|
+
const project = await params.api.getProject(binding.projectId);
|
|
2103
|
+
const organizationId = project.responseObject?.organizationId ?? null;
|
|
2104
|
+
if (!organizationId) {
|
|
2105
|
+
throw new CliError("Could not resolve the organization for the current repository binding.", { exitCode: 2 });
|
|
2106
|
+
}
|
|
2107
|
+
return organizationId;
|
|
2108
|
+
}
|
|
2109
|
+
async function runInviteList(params) {
|
|
2110
|
+
const response = params.scope === "organization" ? await params.api.listOrganizationInvites(params.targetId) : params.scope === "project" ? await params.api.listProjectInvites(params.targetId) : await params.api.listAppInvites(params.targetId);
|
|
2111
|
+
return response.responseObject ?? response;
|
|
2112
|
+
}
|
|
2113
|
+
async function runInviteResend(params) {
|
|
2114
|
+
const response = params.scope === "organization" ? await params.api.resendOrganizationInvite(params.targetId, params.inviteId, { ttlDays: params.ttlDays }) : params.scope === "project" ? await params.api.resendProjectInvite(params.targetId, params.inviteId, { ttlDays: params.ttlDays }) : await params.api.resendAppInvite(params.targetId, params.inviteId, { ttlDays: params.ttlDays });
|
|
2115
|
+
return response.responseObject ?? response;
|
|
2116
|
+
}
|
|
2117
|
+
async function runInviteRevoke(params) {
|
|
2118
|
+
const response = params.scope === "organization" ? await params.api.revokeOrganizationInvite(params.targetId, params.inviteId) : params.scope === "project" ? await params.api.revokeProjectInvite(params.targetId, params.inviteId) : await params.api.revokeAppInvite(params.targetId, params.inviteId);
|
|
2119
|
+
return response.responseObject ?? response;
|
|
2120
|
+
}
|
|
2121
|
+
async function collabInit(params) {
|
|
2122
|
+
const api = await createCollabApi(params);
|
|
2123
|
+
const result = await collabInitCore({
|
|
2124
|
+
api,
|
|
2125
|
+
cwd: params.cwd,
|
|
2126
|
+
appName: params.appName,
|
|
2127
|
+
forceNew: params.forceNew
|
|
2128
|
+
});
|
|
2129
|
+
if (!params.json) {
|
|
2130
|
+
if (result.reused) {
|
|
2131
|
+
console.log(pc10.green(`Bound existing Remix project. appId=${result.appId}`));
|
|
2132
|
+
} else {
|
|
2133
|
+
console.log(pc10.green(`Initialized repository in Remix. appId=${result.appId}`));
|
|
2134
|
+
}
|
|
2135
|
+
printResultWarnings(params.json, result);
|
|
2136
|
+
}
|
|
2137
|
+
return result;
|
|
2138
|
+
}
|
|
2139
|
+
async function collabList(params) {
|
|
2140
|
+
const api = await createCollabApi(params);
|
|
2141
|
+
const result = await collabListCore({ api });
|
|
2142
|
+
if (!params.json) {
|
|
2143
|
+
for (const app of result.apps) {
|
|
2144
|
+
console.log(`${app.id} ${app.name} status=${app.status} project=${app.projectId}${app.forkedFromAppId ? ` forkOf=${app.forkedFromAppId}` : ""}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return result;
|
|
2148
|
+
}
|
|
2149
|
+
async function collabStatus(params) {
|
|
2150
|
+
const localStatus = await collabStatusCore({
|
|
2151
|
+
api: null,
|
|
2152
|
+
cwd: params.cwd
|
|
2153
|
+
});
|
|
2154
|
+
let result = localStatus;
|
|
2155
|
+
if (localStatus.repo.isGitRepo && localStatus.binding.isBound) {
|
|
2156
|
+
try {
|
|
2157
|
+
result = await collabStatusCore({
|
|
2158
|
+
api: await createCollabApi(params),
|
|
2159
|
+
cwd: params.cwd
|
|
2160
|
+
});
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
const detail = err instanceof CliError ? [err.message, err.hint].filter(Boolean).join("\n\n") || "Remote collab checks are unavailable." : err instanceof Error ? err.message || "Remote collab checks are unavailable." : "Remote collab checks are unavailable.";
|
|
2163
|
+
result = {
|
|
2164
|
+
...localStatus,
|
|
2165
|
+
remote: {
|
|
2166
|
+
...localStatus.remote,
|
|
2167
|
+
error: detail
|
|
2168
|
+
},
|
|
2169
|
+
sync: {
|
|
2170
|
+
...localStatus.sync,
|
|
2171
|
+
error: detail,
|
|
2172
|
+
blockedReasons: Array.from(/* @__PURE__ */ new Set([...localStatus.sync.blockedReasons, "remote_error"]))
|
|
2173
|
+
},
|
|
2174
|
+
reconcile: {
|
|
2175
|
+
...localStatus.reconcile,
|
|
2176
|
+
error: detail,
|
|
2177
|
+
blockedReasons: Array.from(/* @__PURE__ */ new Set([...localStatus.reconcile.blockedReasons, "remote_error"]))
|
|
2178
|
+
},
|
|
2179
|
+
recommendedAction: "no_action",
|
|
2180
|
+
warnings: Array.from(new Set([...localStatus.warnings, detail].filter(Boolean)))
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (!params.json) {
|
|
2185
|
+
printCollabStatus(result);
|
|
2186
|
+
}
|
|
2187
|
+
return result;
|
|
2188
|
+
}
|
|
2189
|
+
function printCollabStatus(result) {
|
|
2190
|
+
if (!result.repo.isGitRepo) {
|
|
2191
|
+
console.log(pc10.yellow("Repository: not inside a git repository"));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
console.log(pc10.bold("Repository"));
|
|
2195
|
+
console.log(`${result.repo.repoRoot}`);
|
|
2196
|
+
console.log(`branch=${result.repo.branch ?? "(detached)"}`);
|
|
2197
|
+
console.log(`branchMismatch=${result.repo.branchMismatch ? "yes" : "no"}`);
|
|
2198
|
+
console.log(`head=${result.repo.headCommitHash ?? "unknown"}`);
|
|
2199
|
+
console.log(
|
|
2200
|
+
`worktree=${result.repo.worktree.isClean ? "clean" : "dirty"} entries=${result.repo.worktree.entryCount} tracked=${result.repo.worktree.hasTrackedChanges ? "yes" : "no"} untracked=${result.repo.worktree.hasUntrackedFiles ? "yes" : "no"}`
|
|
2201
|
+
);
|
|
2202
|
+
console.log("");
|
|
2203
|
+
console.log(pc10.bold("Binding"));
|
|
2204
|
+
if (!result.binding.isBound) {
|
|
2205
|
+
console.log("bound=no");
|
|
2206
|
+
} else {
|
|
2207
|
+
console.log(`bound=yes appId=${result.binding.currentAppId} projectId=${result.binding.projectId}`);
|
|
2208
|
+
console.log(`remix=${result.binding.isRemix ? "yes" : "no"} upstreamAppId=${result.binding.upstreamAppId}`);
|
|
2209
|
+
console.log(`preferredBranch=${result.binding.preferredBranch ?? "(unset)"} defaultBranch=${result.binding.defaultBranch ?? "(unknown)"}`);
|
|
2210
|
+
}
|
|
2211
|
+
console.log("");
|
|
2212
|
+
console.log(pc10.bold("Remote"));
|
|
2213
|
+
console.log(`appStatus=${result.remote.appStatus ?? "unknown"}`);
|
|
2214
|
+
console.log(
|
|
2215
|
+
`incomingOpenMergeRequests=${result.remote.incomingOpenMergeRequestCount ?? "unknown"} outgoingOpenMergeRequests=${result.remote.outgoingOpenMergeRequestCount ?? "unknown"}`
|
|
2216
|
+
);
|
|
2217
|
+
console.log("");
|
|
2218
|
+
console.log(pc10.bold("Preflight"));
|
|
2219
|
+
console.log(
|
|
2220
|
+
`sync.status=${result.sync.status} canApply=${result.sync.canApply ? "yes" : "no"} blocked=${result.sync.blockedReasons.join(",") || "none"}`
|
|
2221
|
+
);
|
|
2222
|
+
console.log(
|
|
2223
|
+
`reconcile.status=${result.reconcile.status} canApply=${result.reconcile.canApply ? "yes" : "no"} blocked=${result.reconcile.blockedReasons.join(",") || "none"}`
|
|
2224
|
+
);
|
|
2225
|
+
console.log("");
|
|
2226
|
+
console.log(pc10.bold("Recommendation"));
|
|
2227
|
+
console.log(result.recommendedAction);
|
|
2228
|
+
if (result.warnings.length > 0) {
|
|
2229
|
+
console.log("");
|
|
2230
|
+
console.log(pc10.bold("Warnings"));
|
|
2231
|
+
for (const warning of result.warnings) {
|
|
2232
|
+
console.log(`- ${warning}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async function collabRemix(params) {
|
|
2237
|
+
const api = await createCollabApi(params);
|
|
2238
|
+
const result = await collabRemixCore({
|
|
2239
|
+
api,
|
|
2240
|
+
cwd: params.cwd,
|
|
2241
|
+
outputDir: params.outputDir ?? null,
|
|
2242
|
+
appId: params.appId,
|
|
2243
|
+
name: params.name
|
|
2244
|
+
});
|
|
2245
|
+
if (!params.json) console.log(pc10.green(`Created remix. appId=${result.appId} path=${result.repoRoot}`));
|
|
2246
|
+
return result;
|
|
2247
|
+
}
|
|
2248
|
+
async function collabAdd(params) {
|
|
2249
|
+
if (params.promptStdin && params.diffStdin) {
|
|
2250
|
+
throw new CliError("Cannot read both prompt and diff from stdin in one invocation.", {
|
|
2251
|
+
exitCode: 2,
|
|
2252
|
+
hint: "Use `--prompt-file` or `--diff-file` for one of the inputs."
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
let prompt = params.prompt?.trim() || "";
|
|
2256
|
+
if (params.promptFile) prompt = (await fs15.readFile(path13.resolve(params.promptFile), "utf8")).trim();
|
|
2257
|
+
if (params.promptStdin) prompt = (await readStdin()).trim();
|
|
2258
|
+
if (!prompt) throw new CliError("Prompt is required.", { exitCode: 2 });
|
|
2259
|
+
let assistantResponse = params.assistantResponse?.trim() || "";
|
|
2260
|
+
if (params.assistantResponseFile) {
|
|
2261
|
+
assistantResponse = (await fs15.readFile(path13.resolve(params.assistantResponseFile), "utf8")).trim();
|
|
2262
|
+
}
|
|
2263
|
+
let diff = null;
|
|
2264
|
+
let diffSource = "worktree";
|
|
2265
|
+
if (params.diffFile) {
|
|
2266
|
+
diff = await fs15.readFile(path13.resolve(params.diffFile), "utf8");
|
|
2267
|
+
diffSource = "external";
|
|
2268
|
+
} else if (params.diffStdin) {
|
|
2269
|
+
diff = await readStdin();
|
|
2270
|
+
diffSource = "external";
|
|
2271
|
+
}
|
|
2272
|
+
const api = await createCollabApi(params);
|
|
2273
|
+
const result = await collabAddCore({
|
|
2274
|
+
api,
|
|
2275
|
+
cwd: params.cwd,
|
|
2276
|
+
prompt,
|
|
2277
|
+
assistantResponse: assistantResponse || null,
|
|
2278
|
+
diff,
|
|
2279
|
+
diffSource,
|
|
2280
|
+
sync: params.sync,
|
|
2281
|
+
allowBranchMismatch: params.allowBranchMismatch,
|
|
2282
|
+
idempotencyKey: params.idempotencyKey,
|
|
2283
|
+
actor: {
|
|
2284
|
+
type: process.env.COMERGE_AGENT_TYPE || "agent",
|
|
2285
|
+
name: process.env.COMERGE_AGENT_NAME || void 0,
|
|
2286
|
+
version: process.env.COMERGE_AGENT_VERSION || void 0,
|
|
2287
|
+
provider: process.env.COMERGE_AGENT_PROVIDER || void 0
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
if (!params.json) {
|
|
2291
|
+
console.log(pc10.green(`Recorded change step. id=${result.id}`));
|
|
2292
|
+
if (params.sync !== false && diffSource === "external") {
|
|
2293
|
+
console.log(
|
|
2294
|
+
pc10.yellow(
|
|
2295
|
+
"Skipping automatic local discard+sync because the submitted diff came from --diff-file/--diff-stdin and may not match the current worktree."
|
|
2296
|
+
)
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
printResultWarnings(params.json, result);
|
|
2300
|
+
}
|
|
2301
|
+
return result;
|
|
2302
|
+
}
|
|
2303
|
+
async function collabRecordTurn(params) {
|
|
2304
|
+
let prompt = params.prompt?.trim() || "";
|
|
2305
|
+
if (params.promptFile) prompt = (await fs15.readFile(path13.resolve(params.promptFile), "utf8")).trim();
|
|
2306
|
+
if (params.promptStdin) prompt = (await readStdin()).trim();
|
|
2307
|
+
if (!prompt) throw new CliError("Prompt is required.", { exitCode: 2 });
|
|
2308
|
+
let assistantResponse = params.assistantResponse?.trim() || "";
|
|
2309
|
+
if (params.assistantResponseFile) {
|
|
2310
|
+
assistantResponse = (await fs15.readFile(path13.resolve(params.assistantResponseFile), "utf8")).trim();
|
|
2311
|
+
}
|
|
2312
|
+
if (!assistantResponse) {
|
|
2313
|
+
throw new CliError("Assistant response is required.", {
|
|
2314
|
+
exitCode: 2,
|
|
2315
|
+
hint: "Pass `--assistant-response` or `--assistant-response-file`."
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
const api = await createCollabApi(params);
|
|
2319
|
+
const result = await collabRecordTurnCore({
|
|
2320
|
+
api,
|
|
2321
|
+
cwd: params.cwd,
|
|
2322
|
+
prompt,
|
|
2323
|
+
assistantResponse,
|
|
2324
|
+
allowBranchMismatch: params.allowBranchMismatch,
|
|
2325
|
+
idempotencyKey: params.idempotencyKey,
|
|
2326
|
+
actor: {
|
|
2327
|
+
type: process.env.COMERGE_AGENT_TYPE || "agent",
|
|
2328
|
+
name: process.env.COMERGE_AGENT_NAME || void 0,
|
|
2329
|
+
version: process.env.COMERGE_AGENT_VERSION || void 0,
|
|
2330
|
+
provider: process.env.COMERGE_AGENT_PROVIDER || void 0
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
if (!params.json) {
|
|
2334
|
+
console.log(pc10.green(`Recorded collaboration turn. id=${result.id}`));
|
|
2335
|
+
}
|
|
2336
|
+
return result;
|
|
2337
|
+
}
|
|
2338
|
+
async function collabSync(params) {
|
|
2339
|
+
const api = await createCollabApi(params);
|
|
2340
|
+
const result = await collabSyncCore({
|
|
2341
|
+
api,
|
|
2342
|
+
cwd: params.cwd,
|
|
2343
|
+
dryRun: params.dryRun,
|
|
2344
|
+
allowBranchMismatch: params.allowBranchMismatch
|
|
2345
|
+
});
|
|
2346
|
+
if (!params.json) {
|
|
2347
|
+
if (result.status === "up_to_date") {
|
|
2348
|
+
logMaybe(params.json, pc10.green(`Already in sync. head=${result.targetCommitHash}`));
|
|
2349
|
+
} else if (result.dryRun) {
|
|
2350
|
+
logMaybe(
|
|
2351
|
+
params.json,
|
|
2352
|
+
pc10.green(
|
|
2353
|
+
`Fast-forward preview ready. target=${result.targetCommitHash} files=${result.stats.changedFilesCount} +${result.stats.insertions} -${result.stats.deletions}`
|
|
2354
|
+
)
|
|
2355
|
+
);
|
|
2356
|
+
} else if (result.applied) {
|
|
2357
|
+
logMaybe(
|
|
2358
|
+
params.json,
|
|
2359
|
+
pc10.green(
|
|
2360
|
+
`Fast-forward synced local repository. branch=${result.branch} head=${result.localCommitHash} files=${result.stats.changedFilesCount}`
|
|
2361
|
+
)
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
printResultWarnings(params.json, result);
|
|
2365
|
+
}
|
|
2366
|
+
return result;
|
|
2367
|
+
}
|
|
2368
|
+
async function collabSyncUpstream(params) {
|
|
2369
|
+
const api = await createCollabApi(params);
|
|
2370
|
+
const result = await collabSyncUpstreamCore({
|
|
2371
|
+
api,
|
|
2372
|
+
cwd: params.cwd
|
|
2373
|
+
});
|
|
2374
|
+
if (!params.json) {
|
|
2375
|
+
if (result.status === "up-to-date") {
|
|
2376
|
+
logMaybe(params.json, pc10.green("Upstream is already synced for the current remix."));
|
|
2377
|
+
} else {
|
|
2378
|
+
logMaybe(
|
|
2379
|
+
params.json,
|
|
2380
|
+
pc10.green(
|
|
2381
|
+
`Upstream sync completed for remix. appId=${result.appId} mergeRequestId=${result.mergeRequestId ?? "unknown"}`
|
|
2382
|
+
)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
printResultWarnings(params.json, result);
|
|
2386
|
+
}
|
|
2387
|
+
return result;
|
|
2388
|
+
}
|
|
2389
|
+
async function collabReconcile(params) {
|
|
2390
|
+
const api = await createCollabApi(params);
|
|
2391
|
+
const result = await collabReconcileCore({
|
|
2392
|
+
api,
|
|
2393
|
+
cwd: params.cwd,
|
|
2394
|
+
dryRun: params.dryRun,
|
|
2395
|
+
allowBranchMismatch: params.allowBranchMismatch
|
|
2396
|
+
});
|
|
2397
|
+
if (!params.json) {
|
|
2398
|
+
if (result.status === "ready_to_reconcile" && result.dryRun) {
|
|
2399
|
+
logMaybe(params.json, pc10.green(`Reconcile preview ready. target=${result.targetHeadCommitHash} local=${result.localHeadCommitHash}`));
|
|
2400
|
+
} else if (result.status === "succeeded" && result.applied) {
|
|
2401
|
+
logMaybe(
|
|
2402
|
+
params.json,
|
|
2403
|
+
pc10.green(
|
|
2404
|
+
`Reconciled local repository. branch=${result.branch} head=${result.localCommitHash} backup=${result.backupBranchName}`
|
|
2405
|
+
)
|
|
2406
|
+
);
|
|
2407
|
+
} else if (result.targetCommitHash && !result.dryRun && result.applied) {
|
|
2408
|
+
logMaybe(
|
|
2409
|
+
params.json,
|
|
2410
|
+
pc10.green(
|
|
2411
|
+
`Fast-forward synced local repository. branch=${result.branch} head=${result.localCommitHash} files=${result.stats.changedFilesCount}`
|
|
2412
|
+
)
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
printResultWarnings(params.json, result);
|
|
2416
|
+
}
|
|
2417
|
+
return result;
|
|
2418
|
+
}
|
|
2419
|
+
async function collabRequestMerge(params) {
|
|
2420
|
+
const api = await createCollabApi(params);
|
|
2421
|
+
const result = await collabRequestMergeCore({
|
|
2422
|
+
api,
|
|
2423
|
+
cwd: params.cwd
|
|
2424
|
+
});
|
|
2425
|
+
if (!params.json) console.log(pc10.green(`Opened merge request. id=${result.id}`));
|
|
2426
|
+
return result;
|
|
2427
|
+
}
|
|
2428
|
+
async function collabInbox(params) {
|
|
2429
|
+
const api = await createCollabApi(params);
|
|
2430
|
+
const result = await collabInboxCore({ api });
|
|
2431
|
+
if (!params.json) {
|
|
2432
|
+
const items = Array.isArray(result.mergeRequests) ? result.mergeRequests : result.mergeRequests ? Object.values(result.mergeRequests).flat() : [];
|
|
2433
|
+
for (const mr of items) {
|
|
2434
|
+
console.log(`${mr.id} ${mr.status} ${mr.title ?? "(untitled)"}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return result;
|
|
2438
|
+
}
|
|
2439
|
+
async function collabView(params) {
|
|
2440
|
+
const api = await createCollabApi(params);
|
|
2441
|
+
const result = await collabViewCore({
|
|
2442
|
+
api,
|
|
2443
|
+
mrId: params.mrId
|
|
2444
|
+
});
|
|
2445
|
+
if (!params.json) {
|
|
2446
|
+
console.log(pc10.bold(`${result.mergeRequest?.title ?? "Merge request"} (${result.mergeRequest?.status ?? "unknown"})`));
|
|
2447
|
+
console.log(result.unifiedDiff ?? "");
|
|
2448
|
+
}
|
|
2449
|
+
return result;
|
|
2450
|
+
}
|
|
2451
|
+
async function collabApprove(params) {
|
|
2452
|
+
const mode = params.remoteOnly === params.syncTargetRepo ? null : params.remoteOnly ? "remote-only" : "sync-target-repo";
|
|
2453
|
+
if (!mode) {
|
|
2454
|
+
throw new CliError("Choose exactly one approval mode.", {
|
|
2455
|
+
exitCode: 2,
|
|
2456
|
+
hint: "Pass either `--remote-only` or `--sync-target-repo`."
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
const api = await createCollabApi(params);
|
|
2460
|
+
const result = await collabApproveCore({
|
|
2461
|
+
api,
|
|
2462
|
+
mrId: params.mrId,
|
|
2463
|
+
mode,
|
|
2464
|
+
cwd: params.cwd,
|
|
2465
|
+
allowBranchMismatch: params.allowBranchMismatch
|
|
2466
|
+
});
|
|
2467
|
+
if (!params.json) {
|
|
2468
|
+
if (result.mode === "remote-only") {
|
|
2469
|
+
console.log(pc10.green(`Merge approval completed remotely. id=${result.mergeRequestId} status=${result.terminalStatus}`));
|
|
2470
|
+
} else {
|
|
2471
|
+
console.log(
|
|
2472
|
+
pc10.green(
|
|
2473
|
+
`Merge approval completed and synced local repo. id=${result.mergeRequestId} branch=${result.localSync?.branch} head=${result.localSync?.localCommitHash}`
|
|
2474
|
+
)
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
printResultWarnings(params.json, result);
|
|
2478
|
+
}
|
|
2479
|
+
return result;
|
|
2480
|
+
}
|
|
2481
|
+
async function collabReject(params) {
|
|
2482
|
+
const api = await createCollabApi(params);
|
|
2483
|
+
const result = await collabRejectCore({
|
|
2484
|
+
api,
|
|
2485
|
+
mrId: params.mrId
|
|
2486
|
+
});
|
|
2487
|
+
if (!params.json) console.log(pc10.green(`Rejected merge request. id=${result.id}`));
|
|
2488
|
+
return result;
|
|
2489
|
+
}
|
|
2490
|
+
async function collabInvite(params) {
|
|
2491
|
+
const api = await createCollabApi(params);
|
|
2492
|
+
const result = await collabInviteCore({
|
|
2493
|
+
api,
|
|
2494
|
+
cwd: params.cwd,
|
|
2495
|
+
email: params.email,
|
|
2496
|
+
role: params.role,
|
|
2497
|
+
ttlDays: params.ttlDays,
|
|
2498
|
+
scope: params.scope ?? "project",
|
|
2499
|
+
targetId: params.targetId ?? null
|
|
2500
|
+
});
|
|
2501
|
+
if (!params.json) {
|
|
2502
|
+
const inviteId = result.id ?? "unknown";
|
|
2503
|
+
console.log(pc10.green(`Created ${result.scopeType} invite. id=${inviteId}`));
|
|
2504
|
+
}
|
|
2505
|
+
return result;
|
|
2506
|
+
}
|
|
2507
|
+
async function collabInviteList(params) {
|
|
2508
|
+
const api = await createCollabApi(params);
|
|
2509
|
+
const targetId = await resolveInviteTargetId({
|
|
2510
|
+
api,
|
|
2511
|
+
cwd: params.cwd,
|
|
2512
|
+
scope: params.scope,
|
|
2513
|
+
targetId: params.targetId
|
|
2514
|
+
});
|
|
2515
|
+
const result = await runInviteList({
|
|
2516
|
+
api,
|
|
2517
|
+
scope: params.scope,
|
|
2518
|
+
targetId
|
|
2519
|
+
});
|
|
2520
|
+
if (!params.json && Array.isArray(result)) {
|
|
2521
|
+
for (const invite of result) {
|
|
2522
|
+
console.log(
|
|
2523
|
+
`${String(invite.id ?? "")} ${String(invite.email ?? "")} role=${String(invite.role ?? "")} state=${String(
|
|
2524
|
+
invite.state ?? ""
|
|
2525
|
+
)}`
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return result;
|
|
2530
|
+
}
|
|
2531
|
+
async function collabInviteResend(params) {
|
|
2532
|
+
const api = await createCollabApi(params);
|
|
2533
|
+
const targetId = await resolveInviteTargetId({
|
|
2534
|
+
api,
|
|
2535
|
+
cwd: params.cwd,
|
|
2536
|
+
scope: params.scope,
|
|
2537
|
+
targetId: params.targetId
|
|
2538
|
+
});
|
|
2539
|
+
const result = await runInviteResend({
|
|
2540
|
+
api,
|
|
2541
|
+
scope: params.scope,
|
|
2542
|
+
targetId,
|
|
2543
|
+
inviteId: params.inviteId,
|
|
2544
|
+
ttlDays: params.ttlDays
|
|
2545
|
+
});
|
|
2546
|
+
if (!params.json) console.log(pc10.green(`Resent ${params.scope} invite. id=${params.inviteId}`));
|
|
2547
|
+
return result;
|
|
2548
|
+
}
|
|
2549
|
+
async function collabInviteRevoke(params) {
|
|
2550
|
+
const api = await createCollabApi(params);
|
|
2551
|
+
const targetId = await resolveInviteTargetId({
|
|
2552
|
+
api,
|
|
2553
|
+
cwd: params.cwd,
|
|
2554
|
+
scope: params.scope,
|
|
2555
|
+
targetId: params.targetId
|
|
2556
|
+
});
|
|
2557
|
+
const result = await runInviteRevoke({
|
|
2558
|
+
api,
|
|
2559
|
+
scope: params.scope,
|
|
2560
|
+
targetId,
|
|
2561
|
+
inviteId: params.inviteId
|
|
2562
|
+
});
|
|
2563
|
+
if (!params.json) console.log(pc10.green(`Revoked ${params.scope} invite. id=${params.inviteId}`));
|
|
2564
|
+
return result;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/services/memory.ts
|
|
2568
|
+
import pc11 from "picocolors";
|
|
2569
|
+
import { readCollabBinding } from "@remixhq/core/binding";
|
|
2570
|
+
import { findGitRoot } from "@remixhq/core/repo";
|
|
2571
|
+
async function createMemoryApi(params) {
|
|
2572
|
+
await ensureAuth({ yes: params.yes, json: params.json });
|
|
2573
|
+
const cfg = await resolveConfig({ yes: params.yes });
|
|
2574
|
+
return createApiClient(cfg);
|
|
2575
|
+
}
|
|
2576
|
+
function unwrapResponseObject3(resp, label) {
|
|
2577
|
+
const obj = resp?.responseObject;
|
|
2578
|
+
if (obj === void 0 || obj === null) {
|
|
2579
|
+
throw new CliError(typeof resp?.message === "string" && resp.message.trim() ? resp.message : `Missing ${label} response.`, {
|
|
2580
|
+
exitCode: 1
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
return obj;
|
|
2584
|
+
}
|
|
2585
|
+
async function maybeFindGitRoot(cwd) {
|
|
2586
|
+
try {
|
|
2587
|
+
return await findGitRoot(cwd);
|
|
2588
|
+
} catch {
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
async function resolveMemoryTarget(params) {
|
|
2593
|
+
const explicitAppId = params.appId?.trim();
|
|
2594
|
+
if (explicitAppId) {
|
|
2595
|
+
return {
|
|
2596
|
+
appId: explicitAppId,
|
|
2597
|
+
repoRoot: await maybeFindGitRoot(params.cwd)
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
2601
|
+
const binding = await readCollabBinding(repoRoot);
|
|
2602
|
+
if (!binding) {
|
|
2603
|
+
throw new CliError("Repository is not bound to Remix.", {
|
|
2604
|
+
exitCode: 2,
|
|
2605
|
+
hint: "Run `remix collab init` in this repository, or pass `--app-id` for a direct memory read."
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
appId: binding.currentAppId,
|
|
2610
|
+
repoRoot
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
function truncateText(value, maxChars = 180) {
|
|
2614
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
2615
|
+
if (normalized.length <= maxChars) return normalized;
|
|
2616
|
+
return `${normalized.slice(0, maxChars - 3)}...`;
|
|
2617
|
+
}
|
|
2618
|
+
function compactJson(value, maxChars = 180) {
|
|
2619
|
+
try {
|
|
2620
|
+
return truncateText(JSON.stringify(value));
|
|
2621
|
+
} catch {
|
|
2622
|
+
return "[unserializable preview]";
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
function previewSummary(preview) {
|
|
2626
|
+
const candidateKeys = ["title", "summary", "prompt", "assistantResponse", "description", "statusError", "message"];
|
|
2627
|
+
for (const key of candidateKeys) {
|
|
2628
|
+
const value = preview[key];
|
|
2629
|
+
if (typeof value === "string" && value.trim()) {
|
|
2630
|
+
return truncateText(value);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (Object.keys(preview).length === 0) return null;
|
|
2634
|
+
return compactJson(preview);
|
|
2635
|
+
}
|
|
2636
|
+
function lineageSummary(item) {
|
|
2637
|
+
const pairs = [
|
|
2638
|
+
["thread", item.threadId],
|
|
2639
|
+
["commit", item.lineage.commitId],
|
|
2640
|
+
["mergeRequest", item.lineage.mergeRequestId],
|
|
2641
|
+
["branch", item.lineage.branch]
|
|
2642
|
+
].filter(([, value]) => typeof value === "string" && value);
|
|
2643
|
+
if (pairs.length === 0) return null;
|
|
2644
|
+
return pairs.map(([label, value]) => `${label}=${value}`).join(" ");
|
|
2645
|
+
}
|
|
2646
|
+
function printMemoryItems(items, opts) {
|
|
2647
|
+
if (items.length === 0) {
|
|
2648
|
+
console.log(pc11.dim("No memory items found."));
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
for (const item of items) {
|
|
2652
|
+
const search = "search" in item ? item.search : null;
|
|
2653
|
+
const score = opts?.includeScore && search ? ` score=${search.score.toFixed(4)}` : "";
|
|
2654
|
+
console.log(
|
|
2655
|
+
`${pc11.bold(item.kind)} id=${item.id} status=${item.status ?? "unknown"} createdAt=${item.createdAt}${score}`
|
|
2656
|
+
);
|
|
2657
|
+
const preview = previewSummary(item.preview);
|
|
2658
|
+
if (preview) console.log(`preview=${preview}`);
|
|
2659
|
+
const lineage = lineageSummary(item);
|
|
2660
|
+
if (lineage) console.log(lineage);
|
|
2661
|
+
if (search && search.matchedOn.length > 0) console.log(`matchedOn=${search.matchedOn.join(",")}`);
|
|
2662
|
+
console.log("");
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
function printSummary(summary, target) {
|
|
2666
|
+
console.log(pc11.bold("App"));
|
|
2667
|
+
console.log(`appId=${summary.app.id}`);
|
|
2668
|
+
console.log(`status=${summary.app.status ?? "unknown"} threadId=${summary.app.threadId ?? "none"}`);
|
|
2669
|
+
console.log(`headCommitId=${summary.app.headCommitId ?? "none"} headCommitHash=${summary.app.headCommitHash ?? "none"}`);
|
|
2670
|
+
if (target.repoRoot) console.log(`repoRoot=${target.repoRoot}`);
|
|
2671
|
+
console.log("");
|
|
2672
|
+
console.log(pc11.bold("Source"));
|
|
2673
|
+
if (!summary.source) {
|
|
2674
|
+
console.log("none");
|
|
2675
|
+
} else {
|
|
2676
|
+
console.log(`remoteUrl=${summary.source.remoteUrl ?? "none"}`);
|
|
2677
|
+
console.log(`defaultBranch=${summary.source.defaultBranch ?? "none"} repoFingerprint=${summary.source.repoFingerprint ?? "none"}`);
|
|
2678
|
+
console.log(`sourceType=${summary.source.sourceType ?? "none"} updatedAt=${summary.source.updatedAt}`);
|
|
2679
|
+
}
|
|
2680
|
+
console.log("");
|
|
2681
|
+
console.log(pc11.bold("Counts"));
|
|
2682
|
+
console.log(
|
|
2683
|
+
`collabTurns=${summary.counts.collabTurnCount} changeSteps=${summary.counts.changeStepCount} mergeRequests=${summary.counts.mergeRequestCount} reconciles=${summary.counts.reconcileCount}`
|
|
2684
|
+
);
|
|
2685
|
+
console.log(
|
|
2686
|
+
`openMergeRequests=${summary.counts.openMergeRequestCount} pendingChangeSteps=${summary.counts.pendingChangeStepCount} failedChangeSteps=${summary.counts.failedChangeStepCount}`
|
|
2687
|
+
);
|
|
2688
|
+
console.log("");
|
|
2689
|
+
console.log(pc11.bold("Latest Reconcile"));
|
|
2690
|
+
if (!summary.latestReconcile) {
|
|
2691
|
+
console.log("none");
|
|
2692
|
+
} else {
|
|
2693
|
+
console.log(`id=${summary.latestReconcile.id} status=${summary.latestReconcile.status ?? "unknown"} createdAt=${summary.latestReconcile.createdAt}`);
|
|
2694
|
+
}
|
|
2695
|
+
console.log("");
|
|
2696
|
+
console.log(pc11.bold("Recent"));
|
|
2697
|
+
console.log(`collabTurns=${summary.recent.collabTurns.length}`);
|
|
2698
|
+
console.log(`changeSteps=${summary.recent.changeSteps.length}`);
|
|
2699
|
+
console.log(`mergeRequests=${summary.recent.mergeRequests.length}`);
|
|
2700
|
+
console.log(`reconciles=${summary.recent.reconciles.length}`);
|
|
2701
|
+
}
|
|
2702
|
+
function printPagination(pagination) {
|
|
2703
|
+
console.log(pc11.dim(`limit=${pagination.limit} offset=${pagination.offset} hasMore=${pagination.hasMore ? "yes" : "no"}`));
|
|
2704
|
+
}
|
|
2705
|
+
async function memorySummary(params) {
|
|
2706
|
+
const target = await resolveMemoryTarget(params);
|
|
2707
|
+
const api = await createMemoryApi(params);
|
|
2708
|
+
const resp = await api.getAgentMemorySummary(target.appId);
|
|
2709
|
+
const result = unwrapResponseObject3(resp, "agent memory summary");
|
|
2710
|
+
if (!params.json) {
|
|
2711
|
+
printSummary(result, target);
|
|
2712
|
+
}
|
|
2713
|
+
return result;
|
|
2714
|
+
}
|
|
2715
|
+
async function memorySearch(params) {
|
|
2716
|
+
const target = await resolveMemoryTarget(params);
|
|
2717
|
+
const api = await createMemoryApi(params);
|
|
2718
|
+
const resp = await api.searchAgentMemory(target.appId, {
|
|
2719
|
+
q: params.query,
|
|
2720
|
+
kinds: params.kinds,
|
|
2721
|
+
limit: params.limit,
|
|
2722
|
+
offset: params.offset,
|
|
2723
|
+
createdAfter: params.createdAfter ?? void 0,
|
|
2724
|
+
createdBefore: params.createdBefore ?? void 0
|
|
2725
|
+
});
|
|
2726
|
+
const result = unwrapResponseObject3(resp, "agent memory search");
|
|
2727
|
+
if (!params.json) {
|
|
2728
|
+
printMemoryItems(result.items, { includeScore: true });
|
|
2729
|
+
printPagination(result.pagination);
|
|
2730
|
+
}
|
|
2731
|
+
return result;
|
|
2732
|
+
}
|
|
2733
|
+
async function memoryTimeline(params) {
|
|
2734
|
+
const target = await resolveMemoryTarget(params);
|
|
2735
|
+
const api = await createMemoryApi(params);
|
|
2736
|
+
const resp = await api.listAgentMemoryTimeline(target.appId, {
|
|
2737
|
+
kinds: params.kinds,
|
|
2738
|
+
limit: params.limit,
|
|
2739
|
+
offset: params.offset,
|
|
2740
|
+
createdAfter: params.createdAfter ?? void 0,
|
|
2741
|
+
createdBefore: params.createdBefore ?? void 0
|
|
2742
|
+
});
|
|
2743
|
+
const result = unwrapResponseObject3(resp, "agent memory timeline");
|
|
2744
|
+
if (!params.json) {
|
|
2745
|
+
printMemoryItems(result.items);
|
|
2746
|
+
printPagination(result.pagination);
|
|
2747
|
+
}
|
|
2748
|
+
return result;
|
|
2749
|
+
}
|
|
2750
|
+
async function memoryDiff(params) {
|
|
2751
|
+
const target = await resolveMemoryTarget(params);
|
|
2752
|
+
const api = await createMemoryApi(params);
|
|
2753
|
+
const resp = await api.getChangeStepDiff(target.appId, params.changeStepId);
|
|
2754
|
+
const result = unwrapResponseObject3(resp, "change step diff");
|
|
2755
|
+
if (!params.json) {
|
|
2756
|
+
console.log(pc11.bold("Change Step Diff"));
|
|
2757
|
+
console.log(`changeStepId=${result.changeStepId} appId=${result.appId}`);
|
|
2758
|
+
console.log(`diffSha256=${result.diffSha256} encoding=${result.encoding} contentType=${result.contentType}`);
|
|
2759
|
+
console.log("");
|
|
2760
|
+
process.stdout.write(result.diff.endsWith("\n") ? result.diff : `${result.diff}
|
|
2761
|
+
`);
|
|
2762
|
+
}
|
|
2763
|
+
return result;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// src/commands/collab.ts
|
|
2767
|
+
function parseInteger(value, label) {
|
|
2768
|
+
const parsed = Number.parseInt(value, 10);
|
|
2769
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2770
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
2771
|
+
}
|
|
2772
|
+
return parsed;
|
|
2773
|
+
}
|
|
2774
|
+
function collectKinds(value, previous) {
|
|
2775
|
+
return [...previous, String(value)];
|
|
2776
|
+
}
|
|
2777
|
+
function toKinds(values) {
|
|
2778
|
+
if (!Array.isArray(values) || values.length === 0) return void 0;
|
|
2779
|
+
return values.map((value) => String(value));
|
|
2780
|
+
}
|
|
2781
|
+
function parseInviteScope(value) {
|
|
2782
|
+
const normalized = String(value).trim().toLowerCase();
|
|
2783
|
+
if (normalized === "org" || normalized === "organization") return "organization";
|
|
2784
|
+
if (normalized === "project") return "project";
|
|
2785
|
+
if (normalized === "app") return "app";
|
|
2786
|
+
throw new Error("scope must be one of: organization, project, app");
|
|
2787
|
+
}
|
|
2788
|
+
function registerCollabCommands(program) {
|
|
2789
|
+
const collab = program.command("collab").description("Generic collaboration workflow for any software repository");
|
|
2790
|
+
const memory = collab.command("memory").description("Inspect collaboration memory for the current bound repository or an explicit app");
|
|
2791
|
+
collab.command("init").description("Import the current repository into Remix").option("--name <name>", "Override the imported app name").option("--cwd <path>", "Working directory for repository detection").option("--force-new", "Create a new binding even if a matching project exists", false).option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2792
|
+
const result = await collabInit({
|
|
2793
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2794
|
+
appName: opts.name ? String(opts.name) : null,
|
|
2795
|
+
forceNew: Boolean(opts.forceNew),
|
|
2796
|
+
json: Boolean(opts.json),
|
|
2797
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2798
|
+
});
|
|
2799
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2800
|
+
});
|
|
2801
|
+
collab.command("list").description("List apps available in Remix").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2802
|
+
const result = await collabList({ json: Boolean(opts.json), yes: Boolean(opts.yes || opts.nonInteractive) });
|
|
2803
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2804
|
+
});
|
|
2805
|
+
collab.command("status").description("Summarize binding, worktree, merge request, and sync/reconcile readiness").option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2806
|
+
const result = await collabStatus({
|
|
2807
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2808
|
+
json: Boolean(opts.json),
|
|
2809
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2810
|
+
});
|
|
2811
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2812
|
+
});
|
|
2813
|
+
memory.command("summary").description("Summarize app memory state and recent collaboration activity").option("--cwd <path>", "Working directory for repository detection").option("--app-id <id>", "Read memory for a specific app instead of the current bound repository").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2814
|
+
const result = await memorySummary({
|
|
2815
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2816
|
+
appId: opts.appId ? String(opts.appId) : null,
|
|
2817
|
+
json: Boolean(opts.json),
|
|
2818
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2819
|
+
});
|
|
2820
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2821
|
+
});
|
|
2822
|
+
memory.command("search <query...>").description("Search collaboration memory for historical context").option("--cwd <path>", "Working directory for repository detection").option("--app-id <id>", "Read memory for a specific app instead of the current bound repository").option("--kind <kind>", "Filter by memory kind", collectKinds, []).option("--limit <n>", "Maximum number of results", (value) => parseInteger(value, "limit")).option("--offset <n>", "Pagination offset", (value) => parseInteger(value, "offset")).option("--created-after <iso8601>", "Only include items created at or after this timestamp").option("--created-before <iso8601>", "Only include items created at or before this timestamp").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (query, opts) => {
|
|
2823
|
+
const result = await memorySearch({
|
|
2824
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2825
|
+
appId: opts.appId ? String(opts.appId) : null,
|
|
2826
|
+
query: Array.isArray(query) ? query.join(" ") : String(query),
|
|
2827
|
+
kinds: toKinds(opts.kind),
|
|
2828
|
+
limit: typeof opts.limit === "number" ? opts.limit : void 0,
|
|
2829
|
+
offset: typeof opts.offset === "number" ? opts.offset : void 0,
|
|
2830
|
+
createdAfter: opts.createdAfter ? String(opts.createdAfter) : null,
|
|
2831
|
+
createdBefore: opts.createdBefore ? String(opts.createdBefore) : null,
|
|
2832
|
+
json: Boolean(opts.json),
|
|
2833
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2834
|
+
});
|
|
2835
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2836
|
+
});
|
|
2837
|
+
memory.command("timeline").description("List recent collaboration memory items chronologically").option("--cwd <path>", "Working directory for repository detection").option("--app-id <id>", "Read memory for a specific app instead of the current bound repository").option("--kind <kind>", "Filter by memory kind", collectKinds, []).option("--limit <n>", "Maximum number of results", (value) => parseInteger(value, "limit")).option("--offset <n>", "Pagination offset", (value) => parseInteger(value, "offset")).option("--created-after <iso8601>", "Only include items created at or after this timestamp").option("--created-before <iso8601>", "Only include items created at or before this timestamp").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2838
|
+
const result = await memoryTimeline({
|
|
2839
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2840
|
+
appId: opts.appId ? String(opts.appId) : null,
|
|
2841
|
+
kinds: toKinds(opts.kind),
|
|
2842
|
+
limit: typeof opts.limit === "number" ? opts.limit : void 0,
|
|
2843
|
+
offset: typeof opts.offset === "number" ? opts.offset : void 0,
|
|
2844
|
+
createdAfter: opts.createdAfter ? String(opts.createdAfter) : null,
|
|
2845
|
+
createdBefore: opts.createdBefore ? String(opts.createdBefore) : null,
|
|
2846
|
+
json: Boolean(opts.json),
|
|
2847
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2848
|
+
});
|
|
2849
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2850
|
+
});
|
|
2851
|
+
memory.command("diff <changeStepId>").description("Fetch the full stored diff for a specific change step").option("--cwd <path>", "Working directory for repository detection").option("--app-id <id>", "Read memory for a specific app instead of the current bound repository").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (changeStepId, opts) => {
|
|
2852
|
+
const result = await memoryDiff({
|
|
2853
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2854
|
+
appId: opts.appId ? String(opts.appId) : null,
|
|
2855
|
+
changeStepId: String(changeStepId),
|
|
2856
|
+
json: Boolean(opts.json),
|
|
2857
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2858
|
+
});
|
|
2859
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2860
|
+
});
|
|
2861
|
+
collab.command("remix <appId>").description("Create a remix/fork and materialize a new local checkout").option("--name <name>", "Optional name for the remix").option("--output-dir <path>", "Exact destination path for the new remix checkout").option("--cwd <path>", "Parent directory where the new remix checkout will be created when --output-dir is not provided").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (appId, opts) => {
|
|
2862
|
+
const result = await collabRemix({
|
|
2863
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2864
|
+
outputDir: opts.outputDir ? String(opts.outputDir) : null,
|
|
2865
|
+
appId: appId ? String(appId) : null,
|
|
2866
|
+
name: opts.name ? String(opts.name) : null,
|
|
2867
|
+
json: Boolean(opts.json),
|
|
2868
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2869
|
+
});
|
|
2870
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2871
|
+
});
|
|
2872
|
+
collab.command("add [prompt...]").description("Record one collaboration change step").option("--assistant-response <text>", "Attach the final assistant response to the change step").option("--assistant-response-file <path>", "Read the assistant response from a file").option("--prompt-file <path>", "Read the prompt from a file").option("--prompt-stdin", "Read the prompt from stdin", false).option("--diff-file <path>", "Read the unified diff from a file").option("--diff-stdin", "Read the unified diff from stdin", false).option("--no-sync", "Do not auto-discard local tracked changes and sync after a successful add").option("--allow-branch-mismatch", "Allow running from a branch that does not match the checkout's preferred Remix branch", false).option("--cwd <path>", "Working directory for repository detection").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (prompt, opts) => {
|
|
2873
|
+
const result = await collabAdd({
|
|
2874
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2875
|
+
prompt: Array.isArray(prompt) ? prompt.join(" ") : prompt ? String(prompt) : null,
|
|
2876
|
+
assistantResponse: opts.assistantResponse ? String(opts.assistantResponse) : null,
|
|
2877
|
+
assistantResponseFile: opts.assistantResponseFile ? String(opts.assistantResponseFile) : null,
|
|
2878
|
+
promptFile: opts.promptFile ? String(opts.promptFile) : null,
|
|
2879
|
+
promptStdin: Boolean(opts.promptStdin),
|
|
2880
|
+
diffFile: opts.diffFile ? String(opts.diffFile) : null,
|
|
2881
|
+
diffStdin: Boolean(opts.diffStdin),
|
|
2882
|
+
sync: Boolean(opts.sync),
|
|
2883
|
+
allowBranchMismatch: Boolean(opts.allowBranchMismatch),
|
|
2884
|
+
idempotencyKey: opts.idempotencyKey ? String(opts.idempotencyKey) : null,
|
|
2885
|
+
json: Boolean(opts.json),
|
|
2886
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2887
|
+
});
|
|
2888
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2889
|
+
});
|
|
2890
|
+
collab.command("record-turn [prompt...]").description("Record one no-diff collaboration turn with prompt and assistant response").option("--assistant-response <text>", "Assistant response text").option("--assistant-response-file <path>", "Read the assistant response from a file").option("--prompt-file <path>", "Read the prompt from a file").option("--prompt-stdin", "Read the prompt from stdin", false).option("--allow-branch-mismatch", "Allow running from a branch that does not match the checkout's preferred Remix branch", false).option("--cwd <path>", "Working directory for repository detection").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (prompt, opts) => {
|
|
2891
|
+
const result = await collabRecordTurn({
|
|
2892
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2893
|
+
prompt: Array.isArray(prompt) ? prompt.join(" ") : prompt ? String(prompt) : null,
|
|
2894
|
+
assistantResponse: opts.assistantResponse ? String(opts.assistantResponse) : null,
|
|
2895
|
+
assistantResponseFile: opts.assistantResponseFile ? String(opts.assistantResponseFile) : null,
|
|
2896
|
+
promptFile: opts.promptFile ? String(opts.promptFile) : null,
|
|
2897
|
+
promptStdin: Boolean(opts.promptStdin),
|
|
2898
|
+
allowBranchMismatch: Boolean(opts.allowBranchMismatch),
|
|
2899
|
+
idempotencyKey: opts.idempotencyKey ? String(opts.idempotencyKey) : null,
|
|
2900
|
+
json: Boolean(opts.json),
|
|
2901
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2902
|
+
});
|
|
2903
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2904
|
+
});
|
|
2905
|
+
collab.command("sync").description("Sync the current local repository to the bound Remix app state").option("--cwd <path>", "Working directory for repository detection").option("--dry-run", "Preview sync status without applying changes", false).option("--allow-branch-mismatch", "Allow running from a branch that does not match the checkout's preferred Remix branch", false).option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2906
|
+
const result = await collabSync({
|
|
2907
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2908
|
+
dryRun: Boolean(opts.dryRun),
|
|
2909
|
+
allowBranchMismatch: Boolean(opts.allowBranchMismatch),
|
|
2910
|
+
json: Boolean(opts.json),
|
|
2911
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2912
|
+
});
|
|
2913
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2914
|
+
});
|
|
2915
|
+
collab.command("reconcile").description("Reconcile divergent local history against the bound Remix app").option("--cwd <path>", "Working directory for repository detection").option("--dry-run", "Preview reconcile readiness without applying changes", false).option("--allow-branch-mismatch", "Allow running from a branch that does not match the checkout's preferred Remix branch", false).option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2916
|
+
const result = await collabReconcile({
|
|
2917
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2918
|
+
dryRun: Boolean(opts.dryRun),
|
|
2919
|
+
allowBranchMismatch: Boolean(opts.allowBranchMismatch),
|
|
2920
|
+
json: Boolean(opts.json),
|
|
2921
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2922
|
+
});
|
|
2923
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2924
|
+
});
|
|
2925
|
+
collab.command("sync-upstream").description("Sync upstream changes into the current remix and update the local checkout").option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2926
|
+
const result = await collabSyncUpstream({
|
|
2927
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2928
|
+
json: Boolean(opts.json),
|
|
2929
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2930
|
+
});
|
|
2931
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2932
|
+
});
|
|
2933
|
+
collab.command("request-merge").description("Open a merge request from the current remix to its upstream app").option("--yes", "Run non-interactively", false).option("--cwd <path>", "Working directory for repository detection").option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2934
|
+
const result = await collabRequestMerge({
|
|
2935
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2936
|
+
json: Boolean(opts.json),
|
|
2937
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2938
|
+
});
|
|
2939
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2940
|
+
});
|
|
2941
|
+
collab.command("inbox").description("List open merge requests available for review").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2942
|
+
const result = await collabInbox({ json: Boolean(opts.json), yes: Boolean(opts.yes || opts.nonInteractive) });
|
|
2943
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2944
|
+
});
|
|
2945
|
+
collab.command("view <mrId>").description("View merge request prompts and diffs").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (mrId, opts) => {
|
|
2946
|
+
const result = await collabView({
|
|
2947
|
+
mrId: String(mrId),
|
|
2948
|
+
json: Boolean(opts.json),
|
|
2949
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2950
|
+
});
|
|
2951
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2952
|
+
});
|
|
2953
|
+
collab.command("approve <mrId>").description("Approve a merge request").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--remote-only", "Approve remotely and wait for merge completion without touching the local repo", false).option("--sync-target-repo", "Approve, wait for merge completion, and sync the current target repo checkout", false).option("--allow-branch-mismatch", "Allow syncing the target repo from a branch that does not match the checkout's preferred Remix branch", false).option("--json", "Output JSON", false).action(async (mrId, opts) => {
|
|
2954
|
+
const result = await collabApprove({
|
|
2955
|
+
mrId: String(mrId),
|
|
2956
|
+
cwd: process.cwd(),
|
|
2957
|
+
remoteOnly: Boolean(opts.remoteOnly),
|
|
2958
|
+
syncTargetRepo: Boolean(opts.syncTargetRepo),
|
|
2959
|
+
allowBranchMismatch: Boolean(opts.allowBranchMismatch),
|
|
2960
|
+
json: Boolean(opts.json),
|
|
2961
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2962
|
+
});
|
|
2963
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2964
|
+
});
|
|
2965
|
+
collab.command("reject <mrId>").description("Reject a merge request").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (mrId, opts) => {
|
|
2966
|
+
const result = await collabReject({
|
|
2967
|
+
mrId: String(mrId),
|
|
2968
|
+
json: Boolean(opts.json),
|
|
2969
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2970
|
+
});
|
|
2971
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2972
|
+
});
|
|
2973
|
+
collab.command("invite <email>").description("Invite a collaborator to an organization, project, or app").option("--scope <scope>", "Invitation scope (organization|project|app)", parseInviteScope, "project").option("--target-id <id>", "Explicit organization/project/app id instead of using the current repository binding").option("--role <role>", "Role for the invite").option("--ttl-days <days>", "Invitation lifetime in days", (value) => parseInteger(value, "ttl-days")).option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (email, opts) => {
|
|
2974
|
+
const result = await collabInvite({
|
|
2975
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2976
|
+
email: String(email),
|
|
2977
|
+
role: opts.role ? String(opts.role) : null,
|
|
2978
|
+
ttlDays: typeof opts.ttlDays === "number" ? opts.ttlDays : void 0,
|
|
2979
|
+
scope: parseInviteScope(String(opts.scope ?? "project")),
|
|
2980
|
+
targetId: opts.targetId ? String(opts.targetId) : null,
|
|
2981
|
+
json: Boolean(opts.json),
|
|
2982
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2983
|
+
});
|
|
2984
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2985
|
+
});
|
|
2986
|
+
const inviteAdmin = collab.command("invites").description("List, resend, or revoke invitations");
|
|
2987
|
+
inviteAdmin.command("list").description("List invitations for an organization, project, or app").option("--scope <scope>", "Invitation scope (organization|project|app)", parseInviteScope, "project").option("--target-id <id>", "Explicit organization/project/app id instead of using the current repository binding").option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
2988
|
+
const result = await collabInviteList({
|
|
2989
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
2990
|
+
scope: parseInviteScope(String(opts.scope ?? "project")),
|
|
2991
|
+
targetId: opts.targetId ? String(opts.targetId) : null,
|
|
2992
|
+
json: Boolean(opts.json),
|
|
2993
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
2994
|
+
});
|
|
2995
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
2996
|
+
});
|
|
2997
|
+
inviteAdmin.command("resend <inviteId>").description("Resend an invitation").option("--scope <scope>", "Invitation scope (organization|project|app)", parseInviteScope, "project").option("--target-id <id>", "Explicit organization/project/app id instead of using the current repository binding").option("--ttl-days <days>", "Invitation lifetime in days", (value) => parseInteger(value, "ttl-days")).option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (inviteId, opts) => {
|
|
2998
|
+
const result = await collabInviteResend({
|
|
2999
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
3000
|
+
scope: parseInviteScope(String(opts.scope ?? "project")),
|
|
3001
|
+
inviteId: String(inviteId),
|
|
3002
|
+
targetId: opts.targetId ? String(opts.targetId) : null,
|
|
3003
|
+
ttlDays: typeof opts.ttlDays === "number" ? opts.ttlDays : void 0,
|
|
3004
|
+
json: Boolean(opts.json),
|
|
3005
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
3006
|
+
});
|
|
3007
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
3008
|
+
});
|
|
3009
|
+
inviteAdmin.command("revoke <inviteId>").description("Revoke an invitation").option("--scope <scope>", "Invitation scope (organization|project|app)", parseInviteScope, "project").option("--target-id <id>", "Explicit organization/project/app id instead of using the current repository binding").option("--cwd <path>", "Working directory for repository detection").option("--yes", "Run non-interactively", false).option("--non-interactive", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (inviteId, opts) => {
|
|
3010
|
+
const result = await collabInviteRevoke({
|
|
3011
|
+
cwd: opts.cwd ? String(opts.cwd) : process.cwd(),
|
|
3012
|
+
scope: parseInviteScope(String(opts.scope ?? "project")),
|
|
3013
|
+
inviteId: String(inviteId),
|
|
3014
|
+
targetId: opts.targetId ? String(opts.targetId) : null,
|
|
3015
|
+
json: Boolean(opts.json),
|
|
3016
|
+
yes: Boolean(opts.yes || opts.nonInteractive)
|
|
3017
|
+
});
|
|
3018
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/cli.ts
|
|
3023
|
+
var require2 = createRequire(import.meta.url);
|
|
3024
|
+
var { version } = require2("../package.json");
|
|
3025
|
+
async function main(argv) {
|
|
3026
|
+
const program = new Command();
|
|
3027
|
+
program.name("remix").description("Remix CLI").version(version);
|
|
3028
|
+
registerLoginCommand(program);
|
|
3029
|
+
registerWhoamiCommand(program);
|
|
3030
|
+
registerLogoutCommand(program);
|
|
3031
|
+
registerInitCommands(program);
|
|
3032
|
+
registerCollabCommands(program);
|
|
3033
|
+
program.configureOutput({
|
|
3034
|
+
outputError: (str, write) => write(pc12.red(str))
|
|
3035
|
+
});
|
|
3036
|
+
try {
|
|
3037
|
+
await program.parseAsync(argv);
|
|
3038
|
+
} catch (err) {
|
|
3039
|
+
const e = err;
|
|
3040
|
+
if (e instanceof CliError) {
|
|
3041
|
+
console.error(pc12.red(`Error: ${e.message}`));
|
|
3042
|
+
if (e.hint) console.error(pc12.dim(e.hint));
|
|
3043
|
+
process.exitCode = e.exitCode;
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
console.error(pc12.red(`Unexpected error: ${e?.message ?? String(e)}`));
|
|
3047
|
+
process.exitCode = 1;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
void main(process.argv);
|
|
3051
|
+
export {
|
|
3052
|
+
main
|
|
3053
|
+
};
|
|
3054
|
+
//# sourceMappingURL=cli.js.map
|