@comergehq/cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1750 -1543
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc9 from "picocolors";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
|
|
8
|
-
// src/commands/
|
|
9
|
-
import path5 from "path";
|
|
10
|
-
import os from "os";
|
|
8
|
+
// src/commands/login.ts
|
|
11
9
|
import pc from "picocolors";
|
|
12
|
-
import prompts from "prompts";
|
|
13
|
-
import fse4 from "fs-extra";
|
|
14
10
|
|
|
15
|
-
// src/
|
|
11
|
+
// src/errors/cliError.ts
|
|
16
12
|
var CliError = class extends Error {
|
|
17
13
|
exitCode;
|
|
18
14
|
hint;
|
|
@@ -24,71 +20,245 @@ var CliError = class extends Error {
|
|
|
24
20
|
}
|
|
25
21
|
};
|
|
26
22
|
|
|
27
|
-
// src/
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
23
|
+
// src/config/config.ts
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
var API_URL = "http://localhost:8080";
|
|
26
|
+
var SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
|
|
27
|
+
var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
|
|
28
|
+
function isValidUrl(value) {
|
|
34
29
|
try {
|
|
35
|
-
|
|
30
|
+
new URL(value);
|
|
31
|
+
return true;
|
|
36
32
|
} catch {
|
|
37
|
-
|
|
33
|
+
return false;
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const main2 = "expo-router/entry";
|
|
61
|
-
dependencies["@comergehq/studio"] = params.studioVersion || "latest";
|
|
62
|
-
dependencies["@comergehq/runtime"] = params.studioVersion || "latest";
|
|
63
|
-
if (!dependencies["expo-router"] && !devDependencies["expo-router"]) {
|
|
64
|
-
dependencies["expo-router"] = "latest";
|
|
65
|
-
warnings.push("Added missing dependency expo-router@latest");
|
|
36
|
+
var urlSchema = z.string().min(1).transform((s) => s.trim()).refine((s) => isValidUrl(s), "Invalid URL.").transform((s) => s.replace(/\/+$/, ""));
|
|
37
|
+
var configSchema = z.object({
|
|
38
|
+
apiUrl: urlSchema,
|
|
39
|
+
supabaseUrl: urlSchema,
|
|
40
|
+
supabaseAnonKey: z.string().min(1)
|
|
41
|
+
});
|
|
42
|
+
var cachedConfig = null;
|
|
43
|
+
async function resolveConfig(_opts) {
|
|
44
|
+
if (cachedConfig) return cachedConfig;
|
|
45
|
+
const cfgRaw = {
|
|
46
|
+
apiUrl: API_URL,
|
|
47
|
+
supabaseUrl: SUPABASE_URL,
|
|
48
|
+
supabaseAnonKey: SUPABASE_ANON_KEY
|
|
49
|
+
};
|
|
50
|
+
const parsedCfg = configSchema.safeParse(cfgRaw);
|
|
51
|
+
if (!parsedCfg.success) {
|
|
52
|
+
throw new CliError("Invalid CLI configuration.", {
|
|
53
|
+
exitCode: 1,
|
|
54
|
+
hint: parsedCfg.error.issues.map((i) => `${i.path.join(".") || "config"}: ${i.message}`).join("; ")
|
|
55
|
+
});
|
|
66
56
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
cachedConfig = parsedCfg.data;
|
|
58
|
+
return parsedCfg.data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/services/oauthCallbackServer.ts
|
|
62
|
+
import http from "http";
|
|
63
|
+
async function startOAuthCallbackServer(params) {
|
|
64
|
+
const timeoutMs = params?.timeoutMs ?? 3 * 6e4;
|
|
65
|
+
const requestedPort = params?.port ?? null;
|
|
66
|
+
let resolveCode = null;
|
|
67
|
+
let rejectCode = null;
|
|
68
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
69
|
+
resolveCode = resolve;
|
|
70
|
+
rejectCode = reject;
|
|
71
|
+
});
|
|
72
|
+
const server = http.createServer((req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const base = `http://127.0.0.1`;
|
|
75
|
+
const u = new URL(req.url ?? "/", base);
|
|
76
|
+
const errorDescription = u.searchParams.get("error_description");
|
|
77
|
+
const error = u.searchParams.get("error");
|
|
78
|
+
const code = u.searchParams.get("code");
|
|
79
|
+
if (errorDescription || error) {
|
|
80
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
81
|
+
res.end(
|
|
82
|
+
`<html><body><h3>Sign-in failed</h3><p>${escapeHtml(errorDescription ?? error ?? "Unknown error")}</p></body></html>`
|
|
83
|
+
);
|
|
84
|
+
rejectCode?.(new CliError("Sign-in failed.", { exitCode: 1, hint: errorDescription ?? error ?? void 0 }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!code) {
|
|
88
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
89
|
+
res.end(`<html><body><h3>Missing code</h3><p>No authorization code was provided.</p></body></html>`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
93
|
+
res.end(
|
|
94
|
+
`<html><body><h3>Sign-in complete</h3><p>You can close this tab and return to the terminal.</p></body></html>`
|
|
95
|
+
);
|
|
96
|
+
resolveCode?.(code);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
rejectCode?.(err);
|
|
99
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
100
|
+
res.end("Internal error.");
|
|
101
|
+
} finally {
|
|
102
|
+
try {
|
|
103
|
+
server.close();
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const listenPort = typeof requestedPort === "number" ? requestedPort : 0;
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
server.once("error", reject);
|
|
111
|
+
server.listen(listenPort, "127.0.0.1", () => resolve());
|
|
112
|
+
});
|
|
113
|
+
const addr = server.address();
|
|
114
|
+
if (!addr || typeof addr === "string") {
|
|
115
|
+
server.close();
|
|
116
|
+
throw new CliError("Failed to start the local callback server.", { exitCode: 1 });
|
|
71
117
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
118
|
+
const redirectTo = `http://127.0.0.1:${addr.port}/callback`;
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
rejectCode?.(
|
|
121
|
+
new CliError("Sign-in timed out.", {
|
|
122
|
+
exitCode: 1,
|
|
123
|
+
hint: "Try running `comerge login` again."
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
server.close();
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}, timeoutMs).unref();
|
|
131
|
+
const waitForCode = async () => {
|
|
132
|
+
try {
|
|
133
|
+
return await codePromise;
|
|
134
|
+
} finally {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const close = async () => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
await new Promise((resolve) => server.close(() => resolve())).catch(() => {
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
return { redirectTo, waitForCode, close };
|
|
144
|
+
}
|
|
145
|
+
function escapeHtml(input) {
|
|
146
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/utils/browser.ts
|
|
150
|
+
import { execa } from "execa";
|
|
151
|
+
async function openBrowser(url) {
|
|
152
|
+
const platform = process.platform;
|
|
153
|
+
try {
|
|
154
|
+
if (platform === "darwin") {
|
|
155
|
+
await execa("open", [url], { stdio: "ignore" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (platform === "win32") {
|
|
159
|
+
await execa("cmd", ["/c", "start", "", url], { stdio: "ignore", windowsHide: true });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await execa("xdg-open", [url], { stdio: "ignore" });
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new CliError("Could not open a browser automatically.", {
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
hint: `Open this URL in a browser:
|
|
167
|
+
${url}
|
|
168
|
+
|
|
169
|
+
${err?.message ?? String(err)}`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/clients/supabase.ts
|
|
175
|
+
import { createClient } from "@supabase/supabase-js";
|
|
176
|
+
function createInMemoryStorage() {
|
|
177
|
+
const map = /* @__PURE__ */ new Map();
|
|
178
|
+
return {
|
|
179
|
+
getItem: (k) => map.get(k) ?? null,
|
|
180
|
+
setItem: (k, v) => {
|
|
181
|
+
map.set(k, v);
|
|
83
182
|
},
|
|
84
|
-
|
|
85
|
-
|
|
183
|
+
removeItem: (k) => {
|
|
184
|
+
map.delete(k);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createSupabaseClient(config, storage) {
|
|
189
|
+
return createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
190
|
+
auth: {
|
|
191
|
+
flowType: "pkce",
|
|
192
|
+
persistSession: false,
|
|
193
|
+
autoRefreshToken: false,
|
|
194
|
+
detectSessionInUrl: false,
|
|
195
|
+
storage
|
|
196
|
+
},
|
|
197
|
+
global: {
|
|
198
|
+
headers: {
|
|
199
|
+
"X-Requested-By": "comerge-cli"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function toStoredSession(session) {
|
|
205
|
+
if (!session.access_token || !session.refresh_token || !session.expires_at) {
|
|
206
|
+
throw new CliError("Supabase session is missing required fields.", { exitCode: 1 });
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
access_token: session.access_token,
|
|
210
|
+
refresh_token: session.refresh_token,
|
|
211
|
+
expires_at: session.expires_at,
|
|
212
|
+
token_type: session.token_type ?? void 0,
|
|
213
|
+
user: session.user ? { id: session.user.id, email: session.user.email ?? null } : void 0
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function createSupabaseAuthHelpers(config) {
|
|
217
|
+
const storage = createInMemoryStorage();
|
|
218
|
+
const supabase = createSupabaseClient(config, storage);
|
|
219
|
+
return {
|
|
220
|
+
async startGoogleLogin(params) {
|
|
221
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
222
|
+
provider: "google",
|
|
223
|
+
options: {
|
|
224
|
+
redirectTo: params.redirectTo,
|
|
225
|
+
skipBrowserRedirect: true,
|
|
226
|
+
queryParams: {
|
|
227
|
+
access_type: "offline",
|
|
228
|
+
prompt: "consent"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
if (error) throw error;
|
|
233
|
+
if (!data?.url) throw new CliError("Supabase did not return an OAuth URL.", { exitCode: 1 });
|
|
234
|
+
return { url: data.url };
|
|
235
|
+
},
|
|
236
|
+
async exchangeCode(params) {
|
|
237
|
+
const { data, error } = await supabase.auth.exchangeCodeForSession(params.code);
|
|
238
|
+
if (error) throw error;
|
|
239
|
+
if (!data?.session) throw new CliError("Supabase did not return a session.", { exitCode: 1 });
|
|
240
|
+
return toStoredSession(data.session);
|
|
241
|
+
},
|
|
242
|
+
async refreshWithStoredSession(params) {
|
|
243
|
+
const { data, error } = await supabase.auth.setSession({
|
|
244
|
+
access_token: params.session.access_token,
|
|
245
|
+
refresh_token: params.session.refresh_token
|
|
246
|
+
});
|
|
247
|
+
if (error) throw error;
|
|
248
|
+
if (!data?.session) throw new CliError("No session returned after refresh.", { exitCode: 1 });
|
|
249
|
+
return toStoredSession(data.session);
|
|
250
|
+
}
|
|
86
251
|
};
|
|
87
|
-
return { pkg, warnings, info };
|
|
88
252
|
}
|
|
89
253
|
|
|
90
|
-
// src/
|
|
254
|
+
// src/services/sessionStore.ts
|
|
91
255
|
import fs2 from "fs/promises";
|
|
256
|
+
import os from "os";
|
|
257
|
+
import path2 from "path";
|
|
258
|
+
import { z as z2 } from "zod";
|
|
259
|
+
|
|
260
|
+
// src/utils/fs.ts
|
|
261
|
+
import fs from "fs/promises";
|
|
92
262
|
import path from "path";
|
|
93
263
|
import fse from "fs-extra";
|
|
94
264
|
async function findAvailableDirPath(preferredDir) {
|
|
@@ -100,7 +270,7 @@ async function findAvailableDirPath(preferredDir) {
|
|
|
100
270
|
const candidate = path.join(parent, `${base}-${i}`);
|
|
101
271
|
if (!await fse.pathExists(candidate)) return candidate;
|
|
102
272
|
}
|
|
103
|
-
throw new CliError("
|
|
273
|
+
throw new CliError("No available output directory name.", {
|
|
104
274
|
exitCode: 2,
|
|
105
275
|
hint: `Tried ${base}-2 through ${base}-1000 under ${parent}.`
|
|
106
276
|
});
|
|
@@ -108,7 +278,7 @@ async function findAvailableDirPath(preferredDir) {
|
|
|
108
278
|
async function ensureEmptyDir(dir) {
|
|
109
279
|
const exists2 = await fse.pathExists(dir);
|
|
110
280
|
if (exists2) {
|
|
111
|
-
throw new CliError("Output directory already exists", {
|
|
281
|
+
throw new CliError("Output directory already exists.", {
|
|
112
282
|
hint: `Choose a new --out directory or delete: ${dir}`,
|
|
113
283
|
exitCode: 2
|
|
114
284
|
});
|
|
@@ -119,1636 +289,1688 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
119
289
|
const dir = path.dirname(filePath);
|
|
120
290
|
await fse.mkdirp(dir);
|
|
121
291
|
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
122
|
-
await
|
|
123
|
-
await
|
|
292
|
+
await fs.writeFile(tmp, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
293
|
+
await fs.rename(tmp, filePath);
|
|
124
294
|
}
|
|
125
295
|
async function writeTextAtomic(filePath, contents) {
|
|
126
296
|
const dir = path.dirname(filePath);
|
|
127
297
|
await fse.mkdirp(dir);
|
|
128
298
|
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
129
|
-
await
|
|
130
|
-
await
|
|
299
|
+
await fs.writeFile(tmp, contents, "utf8");
|
|
300
|
+
await fs.rename(tmp, filePath);
|
|
131
301
|
}
|
|
132
302
|
|
|
133
|
-
// src/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const hasAppConfig = await fileExists(path2.join(cur, "app.config.js")) || await fileExists(path2.join(cur, "app.config.ts"));
|
|
151
|
-
if (!hasAppJson && !hasAppConfig) {
|
|
152
|
-
} else {
|
|
153
|
-
return cur;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const parent = path2.dirname(cur);
|
|
157
|
-
if (parent === cur) break;
|
|
158
|
-
cur = parent;
|
|
159
|
-
}
|
|
160
|
-
throw new CliError("Not inside an Expo project", {
|
|
161
|
-
hint: "Run this command from the root of your Expo project repository.",
|
|
162
|
-
exitCode: 2
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// src/lib/templates.ts
|
|
167
|
-
function shellLayoutTsx() {
|
|
168
|
-
return `import { Stack } from 'expo-router';
|
|
169
|
-
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
170
|
-
|
|
171
|
-
export default function Layout() {
|
|
172
|
-
return (
|
|
173
|
-
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
174
|
-
<Stack screenOptions={{ headerShown: false }} />
|
|
175
|
-
</GestureHandlerRootView>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
`;
|
|
179
|
-
}
|
|
180
|
-
function shellIndexTsx() {
|
|
181
|
-
return `import * as React from 'react';
|
|
182
|
-
import { View } from 'react-native';
|
|
183
|
-
import { Stack } from 'expo-router';
|
|
184
|
-
import { ComergeStudio } from '@comergehq/studio';
|
|
185
|
-
|
|
186
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
187
|
-
// @ts-ignore
|
|
188
|
-
import config from '../comerge.config.json';
|
|
189
|
-
|
|
190
|
-
export default function Index() {
|
|
191
|
-
const appId = String((config as any)?.appId || '');
|
|
192
|
-
const appKey = String((config as any)?.appKey || 'MicroMain');
|
|
193
|
-
const apiKey = String((config as any)?.apiKey || '');
|
|
194
|
-
return (
|
|
195
|
-
<>
|
|
196
|
-
<Stack.Screen options={{ headerShown: false }} />
|
|
197
|
-
<View style={{ flex: 1 }}>
|
|
198
|
-
{appId ? <ComergeStudio appId={appId} apiKey={apiKey} appKey={appKey} /> : null}
|
|
199
|
-
</View>
|
|
200
|
-
</>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
`;
|
|
204
|
-
}
|
|
205
|
-
function shellBabelConfigJs() {
|
|
206
|
-
return `module.exports = function (api) {
|
|
207
|
-
api.cache(true);
|
|
208
|
-
return {
|
|
209
|
-
presets: ['babel-preset-expo'],
|
|
210
|
-
plugins: ['react-native-reanimated/plugin'],
|
|
211
|
-
};
|
|
212
|
-
};
|
|
213
|
-
`;
|
|
303
|
+
// src/services/sessionStore.ts
|
|
304
|
+
var storedSessionSchema = z2.object({
|
|
305
|
+
access_token: z2.string().min(1),
|
|
306
|
+
refresh_token: z2.string().min(1),
|
|
307
|
+
expires_at: z2.number().int().positive(),
|
|
308
|
+
token_type: z2.string().min(1).optional(),
|
|
309
|
+
user: z2.object({
|
|
310
|
+
id: z2.string().min(1),
|
|
311
|
+
email: z2.string().email().optional().nullable()
|
|
312
|
+
}).optional()
|
|
313
|
+
});
|
|
314
|
+
var KEYTAR_SERVICE = "comerge-cli";
|
|
315
|
+
var KEYTAR_ACCOUNT = "default";
|
|
316
|
+
function xdgConfigHome() {
|
|
317
|
+
const v = process.env.XDG_CONFIG_HOME;
|
|
318
|
+
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
319
|
+
return path2.join(os.homedir(), ".config");
|
|
214
320
|
}
|
|
215
|
-
function
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
const config = getDefaultConfig(__dirname);
|
|
219
|
-
|
|
220
|
-
module.exports = config;
|
|
221
|
-
`;
|
|
321
|
+
function sessionFilePath() {
|
|
322
|
+
return path2.join(xdgConfigHome(), "comerge", "session.json");
|
|
222
323
|
}
|
|
223
|
-
|
|
224
|
-
// src/lib/packageManager.ts
|
|
225
|
-
import fs4 from "fs/promises";
|
|
226
|
-
async function exists(p) {
|
|
324
|
+
async function maybeLoadKeytar() {
|
|
227
325
|
try {
|
|
228
|
-
await
|
|
229
|
-
|
|
326
|
+
const mod = await import("keytar");
|
|
327
|
+
const candidates = [mod?.default, mod].filter(Boolean);
|
|
328
|
+
for (const c of candidates) {
|
|
329
|
+
if (c && typeof c.getPassword === "function" && typeof c.setPassword === "function" && typeof c.deletePassword === "function") {
|
|
330
|
+
return c;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
230
334
|
} catch {
|
|
231
|
-
return
|
|
335
|
+
return null;
|
|
232
336
|
}
|
|
233
337
|
}
|
|
234
|
-
async function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return { cmd: "pnpm", args: ["install"] };
|
|
245
|
-
case "yarn":
|
|
246
|
-
return { cmd: "yarn", args: ["install"] };
|
|
247
|
-
case "bun":
|
|
248
|
-
return { cmd: "bun", args: ["install"] };
|
|
249
|
-
case "npm":
|
|
250
|
-
default:
|
|
251
|
-
return { cmd: "npm", args: ["install"] };
|
|
338
|
+
async function ensurePathPermissions(filePath) {
|
|
339
|
+
const dir = path2.dirname(filePath);
|
|
340
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
341
|
+
try {
|
|
342
|
+
await fs2.chmod(dir, 448);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await fs2.chmod(filePath, 384);
|
|
347
|
+
} catch {
|
|
252
348
|
}
|
|
253
349
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
350
|
+
async function getSession() {
|
|
351
|
+
const keytar = await maybeLoadKeytar();
|
|
352
|
+
if (keytar) {
|
|
353
|
+
const raw2 = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
354
|
+
if (!raw2) return null;
|
|
355
|
+
try {
|
|
356
|
+
const parsed = storedSessionSchema.safeParse(JSON.parse(raw2));
|
|
357
|
+
if (!parsed.success) return null;
|
|
358
|
+
return parsed.data;
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const fp = sessionFilePath();
|
|
364
|
+
const raw = await fs2.readFile(fp, "utf8").catch(() => null);
|
|
365
|
+
if (!raw) return null;
|
|
366
|
+
try {
|
|
367
|
+
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
368
|
+
if (!parsed.success) return null;
|
|
369
|
+
await ensurePathPermissions(fp);
|
|
370
|
+
return parsed.data;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
270
374
|
}
|
|
271
|
-
function
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
375
|
+
async function setSession(session) {
|
|
376
|
+
const parsed = storedSessionSchema.safeParse(session);
|
|
377
|
+
if (!parsed.success) {
|
|
378
|
+
throw new CliError("Session data is invalid and was not stored.", { exitCode: 1 });
|
|
379
|
+
}
|
|
380
|
+
const keytar = await maybeLoadKeytar();
|
|
381
|
+
if (keytar) {
|
|
382
|
+
await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, JSON.stringify(parsed.data));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const fp = sessionFilePath();
|
|
386
|
+
await writeJsonAtomic(fp, parsed.data);
|
|
387
|
+
await ensurePathPermissions(fp);
|
|
281
388
|
}
|
|
282
|
-
async function
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new CliError("Project root does not exist", { exitCode: 2 });
|
|
389
|
+
async function clearSession() {
|
|
390
|
+
const keytar = await maybeLoadKeytar();
|
|
391
|
+
if (keytar) {
|
|
392
|
+
await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
393
|
+
return;
|
|
288
394
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
preserveTimestamps: true,
|
|
292
|
-
filter: (srcPath) => {
|
|
293
|
-
const rel = path3.relative(src, srcPath);
|
|
294
|
-
if (!rel) return true;
|
|
295
|
-
return !shouldExclude(rel);
|
|
296
|
-
}
|
|
395
|
+
const fp = sessionFilePath();
|
|
396
|
+
await fs2.rm(fp, { force: true }).catch(() => {
|
|
297
397
|
});
|
|
298
398
|
}
|
|
299
399
|
|
|
300
|
-
// src/
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
async function stripProject(params) {
|
|
305
|
-
const dirs = params.dirs ?? DEFAULT_STRIP_DIRS;
|
|
306
|
-
await Promise.all(
|
|
307
|
-
dirs.map(async (d) => {
|
|
308
|
-
const p = path4.join(params.outRoot, d);
|
|
309
|
-
const exists2 = await fse3.pathExists(p);
|
|
310
|
-
if (!exists2) return;
|
|
311
|
-
await fse3.remove(p);
|
|
312
|
-
})
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// src/commands/shellInit.ts
|
|
317
|
-
import fs7 from "fs/promises";
|
|
318
|
-
|
|
319
|
-
// src/lib/appJsonPatch.ts
|
|
320
|
-
import fs5 from "fs/promises";
|
|
321
|
-
function pluginName(entry) {
|
|
322
|
-
if (Array.isArray(entry)) return typeof entry[0] === "string" ? entry[0] : null;
|
|
323
|
-
return typeof entry === "string" ? entry : null;
|
|
400
|
+
// src/clients/api.ts
|
|
401
|
+
function shouldRefreshSoon(session, skewSeconds = 60) {
|
|
402
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
403
|
+
return session.expires_at <= nowSec + skewSeconds;
|
|
324
404
|
}
|
|
325
|
-
async function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
raw = await fs5.readFile(appJsonPath, "utf8");
|
|
329
|
-
} catch {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
let parsed;
|
|
405
|
+
async function readJsonSafe(res) {
|
|
406
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
407
|
+
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
333
408
|
try {
|
|
334
|
-
|
|
409
|
+
return await res.json();
|
|
335
410
|
} catch {
|
|
336
|
-
|
|
411
|
+
return null;
|
|
337
412
|
}
|
|
338
|
-
const expo = parsed.expo;
|
|
339
|
-
if (!expo || typeof expo !== "object") return false;
|
|
340
|
-
const plugins = Array.isArray(expo.plugins) ? [...expo.plugins] : [];
|
|
341
|
-
const routerEntry = plugins.find((p) => pluginName(p) === "expo-router");
|
|
342
|
-
const runtimeEntry = plugins.find((p) => pluginName(p) === "@comergehq/runtime");
|
|
343
|
-
const needsRouter = !routerEntry;
|
|
344
|
-
const needsRuntime = !runtimeEntry;
|
|
345
|
-
if (!needsRouter && !needsRuntime) return false;
|
|
346
|
-
const rest = plugins.filter((p) => {
|
|
347
|
-
const name = pluginName(p);
|
|
348
|
-
return name !== "expo-router" && name !== "@comergehq/runtime";
|
|
349
|
-
});
|
|
350
|
-
expo.plugins = [routerEntry ?? "expo-router", runtimeEntry ?? "@comergehq/runtime", ...rest];
|
|
351
|
-
await fs5.writeFile(appJsonPath, JSON.stringify({ expo }, null, 2) + "\n", "utf8");
|
|
352
|
-
return true;
|
|
353
413
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const raw = await fs6.readFile(babelConfigPath, "utf8").catch(() => null);
|
|
359
|
-
if (!raw) return false;
|
|
360
|
-
if (raw.includes("react-native-reanimated/plugin")) return false;
|
|
361
|
-
const patched = patchBabelConfigAddReanimatedPlugin(raw);
|
|
362
|
-
if (patched === raw) return false;
|
|
363
|
-
await fs6.writeFile(babelConfigPath, patched, "utf8");
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
function patchBabelConfigAddReanimatedPlugin(raw) {
|
|
367
|
-
const pluginsMatch = /(^|\n)([ \t]*)plugins\s*:\s*\[/m.exec(raw);
|
|
368
|
-
if (pluginsMatch) {
|
|
369
|
-
const matchIdx = pluginsMatch.index + pluginsMatch[1].length;
|
|
370
|
-
const indent = pluginsMatch[2] ?? "";
|
|
371
|
-
const bracketIdx = raw.indexOf("[", matchIdx);
|
|
372
|
-
if (bracketIdx >= 0) {
|
|
373
|
-
const closeIdx = findMatchingBracket(raw, bracketIdx, "[", "]");
|
|
374
|
-
if (closeIdx >= 0) {
|
|
375
|
-
const inner = raw.slice(bracketIdx + 1, closeIdx);
|
|
376
|
-
const innerTrim = inner.trim();
|
|
377
|
-
const innerTrimEnd = inner.replace(/[ \t\r\n]+$/g, "");
|
|
378
|
-
const elementIndent = indent + " ";
|
|
379
|
-
let insert = "";
|
|
380
|
-
if (!innerTrim) {
|
|
381
|
-
insert = `
|
|
382
|
-
${elementIndent}'react-native-reanimated/plugin'
|
|
383
|
-
${indent}`;
|
|
384
|
-
} else if (innerTrimEnd.endsWith(",")) {
|
|
385
|
-
insert = `
|
|
386
|
-
${elementIndent}'react-native-reanimated/plugin'`;
|
|
387
|
-
} else {
|
|
388
|
-
insert = `,
|
|
389
|
-
${elementIndent}'react-native-reanimated/plugin'`;
|
|
390
|
-
}
|
|
391
|
-
return raw.slice(0, closeIdx) + insert + raw.slice(closeIdx);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
414
|
+
async function getAuthToken(config) {
|
|
415
|
+
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
416
|
+
if (typeof envToken === "string" && envToken.trim().length > 0) {
|
|
417
|
+
return { token: envToken.trim(), session: null, fromEnv: true };
|
|
394
418
|
}
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
${indent}plugins: ['react-native-reanimated/plugin'],`;
|
|
402
|
-
return raw.slice(0, insertAt) + insertion + raw.slice(insertAt);
|
|
419
|
+
let session = await getSession();
|
|
420
|
+
if (!session) {
|
|
421
|
+
throw new CliError("Not signed in.", {
|
|
422
|
+
exitCode: 2,
|
|
423
|
+
hint: "Run `comerge login`, or set COMERGE_ACCESS_TOKEN for CI."
|
|
424
|
+
});
|
|
403
425
|
}
|
|
404
|
-
|
|
426
|
+
if (shouldRefreshSoon(session)) {
|
|
427
|
+
try {
|
|
428
|
+
const supabase = createSupabaseAuthHelpers(config);
|
|
429
|
+
session = await supabase.refreshWithStoredSession({ session });
|
|
430
|
+
await setSession(session);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
void err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { token: session.access_token, session, fromEnv: false };
|
|
405
436
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
437
|
+
function createApiClient(config, opts) {
|
|
438
|
+
const cfg = config;
|
|
439
|
+
const apiKey = (opts?.apiKey ?? "").trim();
|
|
440
|
+
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
441
|
+
async function request(path12, init) {
|
|
442
|
+
const { token, session, fromEnv } = await getAuthToken(cfg);
|
|
443
|
+
const url = new URL(path12, cfg.apiUrl).toString();
|
|
444
|
+
const doFetch = async (bearer) => {
|
|
445
|
+
const res2 = await fetch(url, {
|
|
446
|
+
...init,
|
|
447
|
+
headers: {
|
|
448
|
+
Accept: "application/json",
|
|
449
|
+
"Content-Type": "application/json",
|
|
450
|
+
...init?.headers ?? {},
|
|
451
|
+
Authorization: `Bearer ${bearer}`,
|
|
452
|
+
...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return res2;
|
|
456
|
+
};
|
|
457
|
+
let res = await doFetch(token);
|
|
458
|
+
if (res.status === 401 && !fromEnv && session?.refresh_token) {
|
|
459
|
+
const supabase = createSupabaseAuthHelpers(cfg);
|
|
460
|
+
const refreshed = await supabase.refreshWithStoredSession({ session });
|
|
461
|
+
await setSession(refreshed);
|
|
462
|
+
res = await doFetch(refreshed.access_token);
|
|
419
463
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
const body = await readJsonSafe(res);
|
|
466
|
+
const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
|
|
467
|
+
throw new CliError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
468
|
+
}
|
|
469
|
+
const json = await readJsonSafe(res);
|
|
470
|
+
return json ?? null;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
getMe: () => request("/v1/me", { method: "GET" }),
|
|
474
|
+
listOrganizations: () => request("/v1/organizations", { method: "GET" }),
|
|
475
|
+
autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
|
|
476
|
+
listClientApps: (params) => {
|
|
477
|
+
const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
|
|
478
|
+
return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
|
|
479
|
+
},
|
|
480
|
+
createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
|
|
481
|
+
createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
|
|
482
|
+
method: "POST",
|
|
483
|
+
body: JSON.stringify(payload ?? {})
|
|
484
|
+
}),
|
|
485
|
+
getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
|
|
486
|
+
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
487
|
+
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) })
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/commands/login.ts
|
|
492
|
+
function registerLoginCommand(program) {
|
|
493
|
+
program.command("login").description("Sign in to Comerge").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) => {
|
|
494
|
+
const config = await resolveConfig({
|
|
495
|
+
yes: Boolean(opts.yes)
|
|
496
|
+
});
|
|
497
|
+
const portRaw = typeof opts.port === "string" ? opts.port.trim() : "";
|
|
498
|
+
let port = null;
|
|
499
|
+
if (portRaw) {
|
|
500
|
+
const parsed = Number(portRaw);
|
|
501
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
502
|
+
throw new CliError("Invalid --port value", { exitCode: 2, hint: "Example: --port 8085" });
|
|
424
503
|
}
|
|
425
|
-
|
|
504
|
+
port = parsed;
|
|
426
505
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
506
|
+
const server = await startOAuthCallbackServer({ port });
|
|
507
|
+
try {
|
|
508
|
+
const supabase = createSupabaseAuthHelpers(config);
|
|
509
|
+
const { url } = await supabase.startGoogleLogin({ redirectTo: server.redirectTo });
|
|
510
|
+
const shouldOpenBrowser = Boolean(opts.browser ?? true);
|
|
511
|
+
if (shouldOpenBrowser) {
|
|
512
|
+
try {
|
|
513
|
+
await openBrowser(url);
|
|
514
|
+
console.log(pc.dim("Opened a browser for sign-in."));
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error(pc.yellow("Could not open a browser automatically."));
|
|
517
|
+
console.error(pc.dim(err?.message ?? String(err)));
|
|
518
|
+
console.log(`Open this URL in a browser:
|
|
519
|
+
${url}`);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
console.log(`Open this URL to sign in:
|
|
523
|
+
${url}`);
|
|
432
524
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
525
|
+
const code = await server.waitForCode();
|
|
526
|
+
const session = await supabase.exchangeCode({ code });
|
|
527
|
+
await setSession(session);
|
|
528
|
+
let me = null;
|
|
529
|
+
try {
|
|
530
|
+
const api = createApiClient(config);
|
|
531
|
+
me = await api.getMe();
|
|
532
|
+
} catch (err) {
|
|
533
|
+
await clearSession().catch(() => {
|
|
534
|
+
});
|
|
535
|
+
throw err;
|
|
437
536
|
}
|
|
537
|
+
if (opts.json) {
|
|
538
|
+
console.log(JSON.stringify({ success: true, me }, null, 2));
|
|
539
|
+
} else {
|
|
540
|
+
const profile = me?.responseObject;
|
|
541
|
+
const label = profile?.email ? `${profile.email}` : profile?.id ? `user ${profile.id}` : "user";
|
|
542
|
+
console.log(pc.green(`Signed in as ${label}.`));
|
|
543
|
+
}
|
|
544
|
+
} finally {
|
|
545
|
+
await server.close();
|
|
438
546
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/commands/logout.ts
|
|
551
|
+
import pc2 from "picocolors";
|
|
552
|
+
function registerLogoutCommand(program) {
|
|
553
|
+
program.command("logout").description("Clear the local Comerge session").option("--json", "Output JSON", false).action(async (opts) => {
|
|
554
|
+
await clearSession();
|
|
555
|
+
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
556
|
+
if (opts.json) {
|
|
557
|
+
console.log(JSON.stringify({ success: true, cleared: true, envTokenPresent: Boolean(envToken) }, null, 2));
|
|
558
|
+
return;
|
|
450
559
|
}
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
depth--;
|
|
455
|
-
if (depth === 0) return i;
|
|
560
|
+
console.log(pc2.green("Signed out."));
|
|
561
|
+
if (envToken) {
|
|
562
|
+
console.log(pc2.dim("Note: COMERGE_ACCESS_TOKEN is set and will still authenticate requests."));
|
|
456
563
|
}
|
|
457
|
-
}
|
|
458
|
-
return -1;
|
|
564
|
+
});
|
|
459
565
|
}
|
|
460
566
|
|
|
461
|
-
// src/commands/
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
let apiKey = params.apiKey ?? "";
|
|
468
|
-
let appKey = params.appKey ?? "MicroMain";
|
|
469
|
-
if (!params.yes) {
|
|
470
|
-
const res = await prompts(
|
|
471
|
-
[
|
|
472
|
-
{
|
|
473
|
-
type: "text",
|
|
474
|
-
name: "outDir",
|
|
475
|
-
message: "Output directory",
|
|
476
|
-
initial: outDir
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
type: "text",
|
|
480
|
-
name: "appKey",
|
|
481
|
-
message: "App key",
|
|
482
|
-
initial: appKey
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
type: "text",
|
|
486
|
-
name: "appId",
|
|
487
|
-
message: "Comerge appId",
|
|
488
|
-
initial: appId,
|
|
489
|
-
validate: (value) => String(value || "").trim().length > 0 ? true : "appId is required"
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
type: "password",
|
|
493
|
-
name: "apiKey",
|
|
494
|
-
message: "Comerge apiKey",
|
|
495
|
-
initial: apiKey,
|
|
496
|
-
validate: (value) => String(value || "").trim().length > 0 ? true : "apiKey is required"
|
|
497
|
-
}
|
|
498
|
-
],
|
|
499
|
-
{
|
|
500
|
-
onCancel: () => {
|
|
501
|
-
throw new CliError("Cancelled", { exitCode: 130 });
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
);
|
|
505
|
-
outDir = String(res.outDir || outDir);
|
|
506
|
-
appKey = String(res.appKey || appKey);
|
|
507
|
-
appId = String(res.appId || appId);
|
|
508
|
-
apiKey = String(res.apiKey || apiKey);
|
|
509
|
-
}
|
|
510
|
-
appId = String(appId || "").trim();
|
|
511
|
-
apiKey = String(apiKey || "").trim();
|
|
512
|
-
if (!appId) {
|
|
513
|
-
throw new CliError("Missing required appId", {
|
|
514
|
-
hint: "Pass --app-id <uuid> (or omit --yes to be prompted).",
|
|
515
|
-
exitCode: 2
|
|
567
|
+
// src/commands/whoami.ts
|
|
568
|
+
import pc3 from "picocolors";
|
|
569
|
+
function registerWhoamiCommand(program) {
|
|
570
|
+
program.command("whoami").description("Show the current Comerge user").option("--yes", "Run non-interactively", false).option("--json", "Output JSON", false).action(async (opts) => {
|
|
571
|
+
const config = await resolveConfig({
|
|
572
|
+
yes: Boolean(opts.yes)
|
|
516
573
|
});
|
|
574
|
+
const api = createApiClient(config);
|
|
575
|
+
const me = await api.getMe();
|
|
576
|
+
if (opts.json) {
|
|
577
|
+
console.log(JSON.stringify(me, null, 2));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const profile = me?.responseObject;
|
|
581
|
+
const label = profile?.email ? `${profile.email}` : profile?.id ? `user ${profile.id}` : "unknown";
|
|
582
|
+
console.log(pc3.green(`Signed in as ${label}.`));
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/commands/init.ts
|
|
587
|
+
import pc8 from "picocolors";
|
|
588
|
+
|
|
589
|
+
// src/services/initLocal.ts
|
|
590
|
+
import os5 from "os";
|
|
591
|
+
import pc7 from "picocolors";
|
|
592
|
+
import prompts from "prompts";
|
|
593
|
+
|
|
594
|
+
// src/services/ensureAuth.ts
|
|
595
|
+
import pc4 from "picocolors";
|
|
596
|
+
|
|
597
|
+
// src/utils/tty.ts
|
|
598
|
+
function isInteractive() {
|
|
599
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/services/ensureAuth.ts
|
|
603
|
+
async function validateBackendSession(params) {
|
|
604
|
+
const cfg = await resolveConfig({ yes: Boolean(params?.yes) });
|
|
605
|
+
const api = createApiClient(cfg);
|
|
606
|
+
await api.getMe();
|
|
607
|
+
}
|
|
608
|
+
async function ensureAuth(params) {
|
|
609
|
+
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
610
|
+
if (typeof envToken === "string" && envToken.trim().length > 0) return;
|
|
611
|
+
const existing = await getSession();
|
|
612
|
+
if (existing) {
|
|
613
|
+
try {
|
|
614
|
+
await validateBackendSession({ yes: params?.yes });
|
|
615
|
+
return;
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
517
618
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
619
|
+
const interactive = isInteractive();
|
|
620
|
+
const yes = Boolean(params?.yes);
|
|
621
|
+
if (!interactive || yes) {
|
|
622
|
+
throw new CliError("Not signed in.", {
|
|
623
|
+
exitCode: 2,
|
|
624
|
+
hint: "Run `comerge login`, or set COMERGE_ACCESS_TOKEN for CI."
|
|
522
625
|
});
|
|
523
626
|
}
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
console.log(pc.dim(`Project: ${projectRoot}`));
|
|
527
|
-
if (outputPath !== requestedOutputPath) {
|
|
528
|
-
console.log(pc.dim(`Output: ${requestedOutputPath} (exists)`));
|
|
529
|
-
console.log(pc.dim(`Using: ${outputPath}`));
|
|
530
|
-
} else {
|
|
531
|
-
console.log(pc.dim(`Output: ${outputPath}`));
|
|
532
|
-
}
|
|
533
|
-
const outputIsInsideProject = outputPath === projectRoot || outputPath.startsWith(projectRoot + path5.sep);
|
|
534
|
-
const stagingPath = outputIsInsideProject ? await fs7.mkdtemp(path5.join(os.tmpdir(), "comerge-shell-")) : outputPath;
|
|
535
|
-
let movedToFinal = false;
|
|
627
|
+
const cfg = await resolveConfig({ yes: Boolean(params?.yes) });
|
|
628
|
+
const server = await startOAuthCallbackServer({ port: null });
|
|
536
629
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
630
|
+
const supabase = createSupabaseAuthHelpers(cfg);
|
|
631
|
+
const { url } = await supabase.startGoogleLogin({ redirectTo: server.redirectTo });
|
|
632
|
+
try {
|
|
633
|
+
await openBrowser(url);
|
|
634
|
+
if (!params?.json) console.log(pc4.dim("Opened a browser for sign-in."));
|
|
635
|
+
} catch {
|
|
636
|
+
if (!params?.json) {
|
|
637
|
+
console.log(pc4.yellow("Could not open a browser automatically."));
|
|
638
|
+
console.log(`Open this URL in a browser:
|
|
639
|
+
${url}`);
|
|
640
|
+
}
|
|
539
641
|
}
|
|
540
|
-
|
|
541
|
-
await
|
|
642
|
+
const code = await server.waitForCode();
|
|
643
|
+
const session = await supabase.exchangeCode({ code });
|
|
644
|
+
await setSession(session);
|
|
542
645
|
try {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
646
|
+
await validateBackendSession({ yes: params?.yes });
|
|
647
|
+
} catch (err) {
|
|
648
|
+
await clearSession().catch(() => {
|
|
649
|
+
});
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
652
|
+
} finally {
|
|
653
|
+
await server.close();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/services/importLocal.ts
|
|
658
|
+
import path6 from "path";
|
|
659
|
+
import pc5 from "picocolors";
|
|
660
|
+
|
|
661
|
+
// src/utils/projectDetect.ts
|
|
662
|
+
import fs3 from "fs/promises";
|
|
663
|
+
import path3 from "path";
|
|
664
|
+
async function fileExists(p) {
|
|
665
|
+
try {
|
|
666
|
+
const st = await fs3.stat(p);
|
|
667
|
+
return st.isFile();
|
|
668
|
+
} catch {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function findExpoProjectRoot(startDir) {
|
|
673
|
+
let cur = path3.resolve(startDir);
|
|
674
|
+
while (true) {
|
|
675
|
+
const pkg = path3.join(cur, "package.json");
|
|
676
|
+
if (await fileExists(pkg)) {
|
|
677
|
+
const hasAppJson = await fileExists(path3.join(cur, "app.json"));
|
|
678
|
+
const hasAppConfig = await fileExists(path3.join(cur, "app.config.js")) || await fileExists(path3.join(cur, "app.config.ts"));
|
|
679
|
+
if (!hasAppJson && !hasAppConfig) {
|
|
680
|
+
} else {
|
|
681
|
+
return cur;
|
|
546
682
|
}
|
|
547
|
-
} catch {
|
|
548
683
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
684
|
+
const parent = path3.dirname(cur);
|
|
685
|
+
if (parent === cur) break;
|
|
686
|
+
cur = parent;
|
|
687
|
+
}
|
|
688
|
+
throw new CliError("Not inside an Expo project.", {
|
|
689
|
+
hint: "Run this command from the root of your Expo project.",
|
|
690
|
+
exitCode: 2
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/utils/gitFiles.ts
|
|
695
|
+
import { execa as execa2 } from "execa";
|
|
696
|
+
async function canUseGit(cwd) {
|
|
697
|
+
try {
|
|
698
|
+
const res = await execa2("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "ignore" });
|
|
699
|
+
return res.exitCode === 0;
|
|
700
|
+
} catch {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function parseGitLsFilesZ(buf) {
|
|
705
|
+
return buf.toString("utf8").split("\0").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
706
|
+
}
|
|
707
|
+
async function listFilesGit(params) {
|
|
708
|
+
const args = ["ls-files", "-z", "--cached", "--others", "--exclude-standard"];
|
|
709
|
+
if (params.pathspec) {
|
|
710
|
+
args.push("--", params.pathspec);
|
|
711
|
+
}
|
|
712
|
+
const res = await execa2("git", args, { cwd: params.cwd, stdout: "pipe", stderr: "ignore" });
|
|
713
|
+
return parseGitLsFilesZ(Buffer.from(res.stdout ?? "", "utf8"));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/utils/fileSelection.ts
|
|
717
|
+
import fs4 from "fs/promises";
|
|
718
|
+
import path4 from "path";
|
|
719
|
+
import fg from "fast-glob";
|
|
720
|
+
import ignore from "ignore";
|
|
721
|
+
var BUILTIN_IGNORES = [
|
|
722
|
+
"**/node_modules/**",
|
|
723
|
+
"**/.git/**",
|
|
724
|
+
"**/.expo/**",
|
|
725
|
+
"**/dist/**",
|
|
726
|
+
"**/build/**",
|
|
727
|
+
"**/coverage/**",
|
|
728
|
+
"**/.turbo/**",
|
|
729
|
+
"**/.next/**",
|
|
730
|
+
"**/ios/build/**",
|
|
731
|
+
"**/android/build/**",
|
|
732
|
+
"**/.pnpm-store/**",
|
|
733
|
+
"**/comerge-shell*/**"
|
|
734
|
+
];
|
|
735
|
+
function normalizeRel(p) {
|
|
736
|
+
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
737
|
+
}
|
|
738
|
+
async function readGitignore(root) {
|
|
739
|
+
const fp = path4.join(root, ".gitignore");
|
|
740
|
+
return await fs4.readFile(fp, "utf8").catch(() => "");
|
|
741
|
+
}
|
|
742
|
+
async function listFilesFs(params) {
|
|
743
|
+
const ig = ignore();
|
|
744
|
+
const raw = await readGitignore(params.root);
|
|
745
|
+
if (raw) ig.add(raw);
|
|
746
|
+
const entries = await fg(["**/*"], {
|
|
747
|
+
cwd: params.root,
|
|
748
|
+
onlyFiles: true,
|
|
749
|
+
dot: true,
|
|
750
|
+
followSymbolicLinks: false,
|
|
751
|
+
unique: true,
|
|
752
|
+
ignore: [...BUILTIN_IGNORES]
|
|
753
|
+
});
|
|
754
|
+
const filtered = entries.filter((p) => {
|
|
755
|
+
const rel = normalizeRel(p);
|
|
756
|
+
if (!params.includeDotenv && path4.posix.basename(rel).startsWith(".env")) return false;
|
|
757
|
+
return !ig.ignores(rel);
|
|
758
|
+
});
|
|
759
|
+
return filtered.map(normalizeRel);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/utils/zip.ts
|
|
763
|
+
import crypto from "crypto";
|
|
764
|
+
import fs5 from "fs";
|
|
765
|
+
import fsp from "fs/promises";
|
|
766
|
+
import os2 from "os";
|
|
767
|
+
import path5 from "path";
|
|
768
|
+
import archiver from "archiver";
|
|
769
|
+
var MAX_IMPORT_ZIP_BYTES = 50 * 1024 * 1024;
|
|
770
|
+
var MAX_IMPORT_FILE_COUNT = 25e3;
|
|
771
|
+
function normalizeRel2(p) {
|
|
772
|
+
return p.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+/, "");
|
|
773
|
+
}
|
|
774
|
+
function validateRelPath(rel) {
|
|
775
|
+
const p = normalizeRel2(rel);
|
|
776
|
+
if (!p) throw new CliError("Invalid file path.", { exitCode: 1 });
|
|
777
|
+
if (p.startsWith("../") || p.includes("/../") || p === "..") {
|
|
778
|
+
throw new CliError("Refusing to add a path traversal entry.", { exitCode: 1, hint: p });
|
|
779
|
+
}
|
|
780
|
+
if (p.startsWith("/")) {
|
|
781
|
+
throw new CliError("Refusing to add an absolute path.", { exitCode: 1, hint: p });
|
|
782
|
+
}
|
|
783
|
+
return p;
|
|
784
|
+
}
|
|
785
|
+
async function computeStats(params) {
|
|
786
|
+
const largestN = params.largestN ?? 10;
|
|
787
|
+
if (params.files.length > MAX_IMPORT_FILE_COUNT) {
|
|
788
|
+
throw new CliError("Too many files to import.", {
|
|
789
|
+
exitCode: 2,
|
|
790
|
+
hint: `File count ${params.files.length} exceeds limit ${MAX_IMPORT_FILE_COUNT}. Use --path or add ignores.`
|
|
553
791
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
792
|
+
}
|
|
793
|
+
let totalBytes = 0;
|
|
794
|
+
const largest = [];
|
|
795
|
+
for (const rel0 of params.files) {
|
|
796
|
+
const rel = validateRelPath(rel0);
|
|
797
|
+
const abs = path5.join(params.root, rel);
|
|
798
|
+
const st = await fsp.stat(abs);
|
|
799
|
+
if (!st.isFile()) continue;
|
|
800
|
+
const bytes = st.size;
|
|
801
|
+
totalBytes += bytes;
|
|
802
|
+
if (largestN > 0) {
|
|
803
|
+
largest.push({ path: rel, bytes });
|
|
562
804
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
805
|
+
}
|
|
806
|
+
largest.sort((a, b) => b.bytes - a.bytes);
|
|
807
|
+
return {
|
|
808
|
+
fileCount: params.files.length,
|
|
809
|
+
totalBytes,
|
|
810
|
+
largestFiles: largest.slice(0, largestN)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function createZip(params) {
|
|
814
|
+
const zipName = (params.zipName ?? "import.zip").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
815
|
+
const tmpDir = await fsp.mkdtemp(path5.join(os2.tmpdir(), "comerge-import-"));
|
|
816
|
+
const zipPath = path5.join(tmpDir, zipName);
|
|
817
|
+
await new Promise((resolve, reject) => {
|
|
818
|
+
const output = fs5.createWriteStream(zipPath);
|
|
819
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
820
|
+
output.on("close", () => resolve());
|
|
821
|
+
output.on("error", (err) => reject(err));
|
|
822
|
+
archive.on("warning", (err) => {
|
|
823
|
+
reject(err);
|
|
578
824
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
for (const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
825
|
+
archive.on("error", (err) => reject(err));
|
|
826
|
+
archive.pipe(output);
|
|
827
|
+
for (const rel0 of params.files) {
|
|
828
|
+
const rel = validateRelPath(rel0);
|
|
829
|
+
const abs = path5.join(params.root, rel);
|
|
830
|
+
archive.file(abs, { name: rel });
|
|
585
831
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
832
|
+
void archive.finalize();
|
|
833
|
+
});
|
|
834
|
+
const st = await fsp.stat(zipPath);
|
|
835
|
+
if (st.size > MAX_IMPORT_ZIP_BYTES) {
|
|
836
|
+
throw new CliError("Archive exceeds the upload size limit.", {
|
|
837
|
+
exitCode: 2,
|
|
838
|
+
hint: `Zip size ${st.size} bytes exceeds limit ${MAX_IMPORT_ZIP_BYTES} bytes. Use --path or add ignores.`
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return { zipPath };
|
|
842
|
+
}
|
|
843
|
+
async function sha256FileHex(filePath) {
|
|
844
|
+
const hash = crypto.createHash("sha256");
|
|
845
|
+
await new Promise((resolve, reject) => {
|
|
846
|
+
const s = fs5.createReadStream(filePath);
|
|
847
|
+
s.on("data", (chunk) => hash.update(chunk));
|
|
848
|
+
s.on("error", reject);
|
|
849
|
+
s.on("end", () => resolve());
|
|
850
|
+
});
|
|
851
|
+
return hash.digest("hex");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/clients/upload.ts
|
|
855
|
+
import fs6 from "fs";
|
|
856
|
+
import { PassThrough } from "stream";
|
|
857
|
+
async function uploadPresigned(params) {
|
|
858
|
+
const st = await fs6.promises.stat(params.filePath).catch(() => null);
|
|
859
|
+
if (!st || !st.isFile()) throw new CliError("Upload file not found.", { exitCode: 2 });
|
|
860
|
+
const totalBytes = st.size;
|
|
861
|
+
const fileStream = fs6.createReadStream(params.filePath);
|
|
862
|
+
const pass = new PassThrough();
|
|
863
|
+
let sent = 0;
|
|
864
|
+
fileStream.on("data", (chunk) => {
|
|
865
|
+
sent += chunk.length;
|
|
866
|
+
params.onProgress?.({ sentBytes: sent, totalBytes });
|
|
867
|
+
});
|
|
868
|
+
fileStream.on("error", (err) => pass.destroy(err));
|
|
869
|
+
fileStream.pipe(pass);
|
|
870
|
+
const res = await fetch(params.uploadUrl, {
|
|
871
|
+
method: "PUT",
|
|
872
|
+
headers: params.headers,
|
|
873
|
+
body: pass,
|
|
874
|
+
duplex: "half"
|
|
875
|
+
});
|
|
876
|
+
if (!res.ok) {
|
|
877
|
+
const text = await res.text().catch(() => "");
|
|
878
|
+
throw new CliError("Upload failed.", {
|
|
879
|
+
exitCode: 1,
|
|
880
|
+
hint: `Status: ${res.status}
|
|
881
|
+
${text}`.trim() || null
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/services/importLocal.ts
|
|
887
|
+
var SAFE_PATH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
888
|
+
var COMERGE_SHELL_SEGMENT_RE = /(^|\/)comerge-shell[^/]*(\/|$)/;
|
|
889
|
+
function sleep(ms) {
|
|
890
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
891
|
+
}
|
|
892
|
+
function formatBytes(bytes) {
|
|
893
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
894
|
+
let v = bytes;
|
|
895
|
+
let i = 0;
|
|
896
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
897
|
+
v /= 1024;
|
|
898
|
+
i++;
|
|
899
|
+
}
|
|
900
|
+
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
901
|
+
}
|
|
902
|
+
function deriveAppName(projectRoot) {
|
|
903
|
+
const base = path6.basename(projectRoot);
|
|
904
|
+
return base || "Imported App";
|
|
905
|
+
}
|
|
906
|
+
function validateOptionalSubdir(subdir) {
|
|
907
|
+
if (!subdir) return null;
|
|
908
|
+
const s = subdir.trim().replace(/^\/+/, "").replace(/\\/g, "/");
|
|
909
|
+
if (!s) return null;
|
|
910
|
+
if (!SAFE_PATH_RE.test(s) || s.includes("..")) {
|
|
911
|
+
throw new CliError("Invalid --path value.", {
|
|
912
|
+
exitCode: 2,
|
|
913
|
+
hint: "Use a safe subdirectory such as packages/mobile-app (letters, numbers, ., _, -, / only)."
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
return s;
|
|
917
|
+
}
|
|
918
|
+
function getAppStatus(appResp) {
|
|
919
|
+
const obj = appResp?.responseObject;
|
|
920
|
+
const status = typeof obj?.status === "string" ? obj.status : null;
|
|
921
|
+
const statusError = typeof obj?.statusError === "string" ? obj.statusError : null;
|
|
922
|
+
return { status, statusError };
|
|
923
|
+
}
|
|
924
|
+
async function importLocal(params) {
|
|
925
|
+
const noWait = Boolean(params.noWait);
|
|
926
|
+
const maxWaitMs = 1200 * 1e3;
|
|
927
|
+
const apiKey = String(params.apiKey || "").trim();
|
|
928
|
+
if (!apiKey && !params.dryRun) {
|
|
929
|
+
throw new CliError("Missing client key.", { exitCode: 2, hint: "Sign in and rerun `comerge init`." });
|
|
930
|
+
}
|
|
931
|
+
const cfg = await resolveConfig({ yes: Boolean(params.yes) });
|
|
932
|
+
const api = createApiClient(cfg, { apiKey });
|
|
933
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
934
|
+
const subdir = validateOptionalSubdir(params.subdir);
|
|
935
|
+
const importRoot = subdir ? path6.join(projectRoot, subdir) : projectRoot;
|
|
936
|
+
const appName = (params.appName ?? "").trim() || deriveAppName(projectRoot);
|
|
937
|
+
const log = (msg) => {
|
|
938
|
+
if (params.json) return;
|
|
939
|
+
console.log(msg);
|
|
940
|
+
};
|
|
941
|
+
log(pc5.dim(`Project: ${projectRoot}`));
|
|
942
|
+
if (subdir) log(pc5.dim(`Import path: ${subdir}`));
|
|
943
|
+
let files = [];
|
|
944
|
+
const useGit = await canUseGit(projectRoot);
|
|
945
|
+
if (useGit) {
|
|
946
|
+
const gitPaths = await listFilesGit({ cwd: projectRoot, pathspec: subdir ?? null });
|
|
947
|
+
files = gitPaths.filter((p) => !COMERGE_SHELL_SEGMENT_RE.test(p.replace(/\\/g, "/")));
|
|
948
|
+
if (!params.includeDotenv) {
|
|
949
|
+
files = files.filter((p) => !path6.posix.basename(p).startsWith(".env"));
|
|
600
950
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
console.log(pc.dim(` cd ${rel}`));
|
|
606
|
-
console.log(pc.dim(` npx expo run:ios`));
|
|
607
|
-
console.log(pc.dim(` npx expo run:android`));
|
|
608
|
-
} finally {
|
|
609
|
-
if (outputIsInsideProject && !movedToFinal) {
|
|
610
|
-
try {
|
|
611
|
-
await fse4.remove(stagingPath);
|
|
612
|
-
} catch {
|
|
613
|
-
}
|
|
951
|
+
} else {
|
|
952
|
+
files = await listFilesFs({ root: importRoot, includeDotenv: params.includeDotenv });
|
|
953
|
+
if (subdir) {
|
|
954
|
+
files = files.map((p) => path6.posix.join(subdir, p));
|
|
614
955
|
}
|
|
615
956
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
await fs7.access(filePath);
|
|
620
|
-
return;
|
|
621
|
-
} catch {
|
|
622
|
-
}
|
|
623
|
-
await writeTextAtomic(filePath, contents);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// src/commands/shell.ts
|
|
627
|
-
function registerShellCommands(program) {
|
|
628
|
-
const shell = program.command("shell").description("Shell app utilities");
|
|
629
|
-
shell.command("init").description("Generate an Expo Router shell app that renders Comerge Studio").option("--out <dir>", "Output directory", "comerge-shell").requiredOption("--app-id <uuid>", "Comerge appId (required)").requiredOption("--api-key <key>", "Comerge apiKey (required)").option("--app-key <key>", "Bundle app key (default MicroMain)", "MicroMain").option("--yes", "Non-interactive; use defaults", false).option("--no-install", "Skip dependency install + expo prebuild").option(
|
|
630
|
-
"--package-manager <pm>",
|
|
631
|
-
"Override package manager detection (pnpm|npm|yarn|bun)"
|
|
632
|
-
).option("--studio-version <ver>", "Version to use for @comergehq/studio", "latest").action(async (opts) => {
|
|
633
|
-
await shellInit({
|
|
634
|
-
outDir: String(opts.out),
|
|
635
|
-
appId: String(opts.appId ?? ""),
|
|
636
|
-
apiKey: String(opts.apiKey ?? ""),
|
|
637
|
-
appKey: String(opts.appKey ?? "MicroMain"),
|
|
638
|
-
yes: Boolean(opts.yes),
|
|
639
|
-
install: Boolean(opts.install),
|
|
640
|
-
packageManager: opts.packageManager ? String(opts.packageManager) : null,
|
|
641
|
-
studioVersion: String(opts.studioVersion ?? "latest")
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// src/commands/login.ts
|
|
647
|
-
import pc2 from "picocolors";
|
|
648
|
-
|
|
649
|
-
// src/lib/config.ts
|
|
650
|
-
import { z } from "zod";
|
|
651
|
-
|
|
652
|
-
// src/lib/apiKey.ts
|
|
653
|
-
import prompts2 from "prompts";
|
|
654
|
-
function isInteractive() {
|
|
655
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
656
|
-
}
|
|
657
|
-
var cachedApiKey = null;
|
|
658
|
-
async function resolveStudioApiKey(params) {
|
|
659
|
-
if (cachedApiKey) return cachedApiKey;
|
|
660
|
-
const fromFlag = (params.flagApiKey ?? "").trim();
|
|
661
|
-
if (fromFlag) {
|
|
662
|
-
cachedApiKey = fromFlag;
|
|
663
|
-
return fromFlag;
|
|
664
|
-
}
|
|
665
|
-
const fromEnv = (process.env.COMERGE_API_KEY ?? "").trim();
|
|
666
|
-
if (fromEnv) {
|
|
667
|
-
cachedApiKey = fromEnv;
|
|
668
|
-
return fromEnv;
|
|
669
|
-
}
|
|
670
|
-
if (params.yes || !isInteractive()) {
|
|
671
|
-
throw new CliError("Missing required API Key", {
|
|
957
|
+
if (files.length === 0) {
|
|
958
|
+
throw new CliError("No files found to upload.", {
|
|
672
959
|
exitCode: 2,
|
|
673
|
-
hint: "
|
|
960
|
+
hint: "Check .gitignore or try without --path."
|
|
674
961
|
});
|
|
675
962
|
}
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
],
|
|
685
|
-
{
|
|
686
|
-
onCancel: () => {
|
|
687
|
-
throw new CliError("Cancelled", { exitCode: 130 });
|
|
963
|
+
const stats = await computeStats({ root: projectRoot, files, largestN: 10 });
|
|
964
|
+
log(pc5.dim(`Files: ${stats.fileCount} Total: ${formatBytes(stats.totalBytes)}`));
|
|
965
|
+
if (params.dryRun) {
|
|
966
|
+
const top = stats.largestFiles.slice(0, 10);
|
|
967
|
+
if (top.length > 0) {
|
|
968
|
+
log(pc5.dim("Largest files:"));
|
|
969
|
+
for (const f of top) {
|
|
970
|
+
log(pc5.dim(` ${formatBytes(f.bytes)} ${f.path}`));
|
|
688
971
|
}
|
|
689
972
|
}
|
|
690
|
-
|
|
691
|
-
const apiKey = String(res.apiKey ?? "").trim();
|
|
692
|
-
if (!apiKey) {
|
|
693
|
-
throw new CliError("Missing required API Key", { exitCode: 2 });
|
|
694
|
-
}
|
|
695
|
-
cachedApiKey = apiKey;
|
|
696
|
-
return apiKey;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// src/lib/config.ts
|
|
700
|
-
var API_URL = "https://comerge.ai";
|
|
701
|
-
var CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
702
|
-
var urlSchema = z.string().min(1).transform((s) => s.trim()).refine((s) => {
|
|
703
|
-
try {
|
|
704
|
-
new URL(s);
|
|
705
|
-
return true;
|
|
706
|
-
} catch {
|
|
707
|
-
return false;
|
|
973
|
+
return { uploadId: "dry-run", appId: "dry-run", status: "dry-run" };
|
|
708
974
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
} catch {
|
|
732
|
-
return null;
|
|
975
|
+
log(pc5.dim("Creating archive..."));
|
|
976
|
+
const { zipPath } = await createZip({ root: projectRoot, files, zipName: "import.zip" });
|
|
977
|
+
const zipSha = await sha256FileHex(zipPath);
|
|
978
|
+
const zipSize = (await (await import("fs/promises")).stat(zipPath)).size;
|
|
979
|
+
log(pc5.dim("Requesting upload URL..."));
|
|
980
|
+
const presignResp = await api.presignImportUpload({
|
|
981
|
+
file: {
|
|
982
|
+
name: "import.zip",
|
|
983
|
+
mimeType: "application/zip",
|
|
984
|
+
size: zipSize,
|
|
985
|
+
checksumSha256: zipSha
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
const presign = presignResp?.responseObject;
|
|
989
|
+
const uploadId = String(presign?.uploadId ?? "");
|
|
990
|
+
const uploadUrl = String(presign?.uploadUrl ?? "");
|
|
991
|
+
const headers = presign?.headers ?? {};
|
|
992
|
+
if (!uploadId || !uploadUrl) {
|
|
993
|
+
throw new CliError("Upload URL response is missing required fields.", {
|
|
994
|
+
exitCode: 1,
|
|
995
|
+
hint: JSON.stringify(presignResp, null, 2)
|
|
996
|
+
});
|
|
733
997
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
Accept: "application/json",
|
|
747
|
-
[CLIENT_KEY_HEADER]: apiKey
|
|
998
|
+
log(pc5.dim("Uploading archive..."));
|
|
999
|
+
let lastPct = -1;
|
|
1000
|
+
await uploadPresigned({
|
|
1001
|
+
uploadUrl,
|
|
1002
|
+
headers,
|
|
1003
|
+
filePath: zipPath,
|
|
1004
|
+
onProgress: ({ sentBytes, totalBytes }) => {
|
|
1005
|
+
if (params.json) return;
|
|
1006
|
+
const pct = totalBytes > 0 ? Math.floor(sentBytes / totalBytes * 100) : 0;
|
|
1007
|
+
if (pct !== lastPct && (pct % 5 === 0 || pct === 100)) {
|
|
1008
|
+
lastPct = pct;
|
|
1009
|
+
process.stdout.write(pc5.dim(`Upload ${pct}%\r`));
|
|
748
1010
|
}
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
if (!params.json) process.stdout.write("\n");
|
|
1014
|
+
log(pc5.dim("Starting import..."));
|
|
1015
|
+
const importResp = await api.importFromUpload({
|
|
1016
|
+
uploadId,
|
|
1017
|
+
appName,
|
|
1018
|
+
path: subdir ?? void 0
|
|
1019
|
+
});
|
|
1020
|
+
const importObj = importResp?.responseObject;
|
|
1021
|
+
const appId = String(importObj?.appId ?? "");
|
|
1022
|
+
const projectId = importObj?.projectId ? String(importObj.projectId) : void 0;
|
|
1023
|
+
const threadId = importObj?.threadId ? String(importObj.threadId) : void 0;
|
|
1024
|
+
if (!appId) {
|
|
1025
|
+
throw new CliError("Import response is missing the app ID.", {
|
|
1026
|
+
exitCode: 1,
|
|
1027
|
+
hint: JSON.stringify(importResp, null, 2)
|
|
749
1028
|
});
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1029
|
+
}
|
|
1030
|
+
if (noWait) {
|
|
1031
|
+
return { uploadId, appId, projectId, threadId, status: "accepted" };
|
|
1032
|
+
}
|
|
1033
|
+
log(pc5.dim("Waiting for import to finish..."));
|
|
1034
|
+
const startedAt = Date.now();
|
|
1035
|
+
let delay = 2e3;
|
|
1036
|
+
let lastStatus = null;
|
|
1037
|
+
while (Date.now() - startedAt < maxWaitMs) {
|
|
1038
|
+
const appResp = await api.getApp(appId);
|
|
1039
|
+
const { status, statusError } = getAppStatus(appResp);
|
|
1040
|
+
if (status && status !== lastStatus) {
|
|
1041
|
+
lastStatus = status;
|
|
1042
|
+
log(pc5.dim(`Status: ${status}`));
|
|
757
1043
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
throw new CliError("Invalid studio-config response", {
|
|
761
|
-
exitCode: 1,
|
|
762
|
-
hint: body ? JSON.stringify(body, null, 2) : null
|
|
763
|
-
});
|
|
1044
|
+
if (status === "ready") {
|
|
1045
|
+
return { uploadId, appId, projectId, threadId, status };
|
|
764
1046
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
supabaseUrl: parsedResp.data.responseObject.url,
|
|
768
|
-
supabaseAnonKey: parsedResp.data.responseObject.anonKey
|
|
769
|
-
};
|
|
770
|
-
const parsedCfg = configSchema.safeParse(cfgRaw);
|
|
771
|
-
if (!parsedCfg.success) {
|
|
772
|
-
throw new CliError("Invalid CLI configuration", {
|
|
773
|
-
exitCode: 1,
|
|
774
|
-
hint: parsedCfg.error.issues.map((i) => `${i.path.join(".") || "config"}: ${i.message}`).join("; ")
|
|
775
|
-
});
|
|
1047
|
+
if (status === "error") {
|
|
1048
|
+
throw new CliError("Import failed.", { exitCode: 1, hint: statusError ?? "App status is error." });
|
|
776
1049
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
1050
|
+
await sleep(delay);
|
|
1051
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
1052
|
+
}
|
|
1053
|
+
throw new CliError("Timed out waiting for import.", {
|
|
1054
|
+
exitCode: 1,
|
|
1055
|
+
hint: "Check app status with: comerge import local --no-wait"
|
|
781
1056
|
});
|
|
782
|
-
return inFlight;
|
|
783
1057
|
}
|
|
784
1058
|
|
|
785
|
-
// src/
|
|
786
|
-
import
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
try {
|
|
798
|
-
const base = `http://127.0.0.1`;
|
|
799
|
-
const u = new URL(req.url ?? "/", base);
|
|
800
|
-
const errorDescription = u.searchParams.get("error_description");
|
|
801
|
-
const error = u.searchParams.get("error");
|
|
802
|
-
const code = u.searchParams.get("code");
|
|
803
|
-
if (errorDescription || error) {
|
|
804
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
805
|
-
res.end(`<html><body><h3>Login failed</h3><p>${escapeHtml(errorDescription ?? error ?? "Unknown error")}</p></body></html>`);
|
|
806
|
-
rejectCode?.(new CliError("Login failed", { exitCode: 1, hint: errorDescription ?? error ?? void 0 }));
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
if (!code) {
|
|
810
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
811
|
-
res.end(`<html><body><h3>Missing code</h3><p>No authorization code was provided.</p></body></html>`);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
815
|
-
res.end(
|
|
816
|
-
`<html><body><h3>Login complete</h3><p>You can close this tab and return to the terminal.</p></body></html>`
|
|
817
|
-
);
|
|
818
|
-
resolveCode?.(code);
|
|
819
|
-
} catch (err) {
|
|
820
|
-
rejectCode?.(err);
|
|
821
|
-
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
822
|
-
res.end("Internal error");
|
|
823
|
-
} finally {
|
|
824
|
-
try {
|
|
825
|
-
server.close();
|
|
826
|
-
} catch {
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
});
|
|
830
|
-
const listenPort = typeof requestedPort === "number" ? requestedPort : 0;
|
|
831
|
-
await new Promise((resolve, reject) => {
|
|
832
|
-
server.once("error", reject);
|
|
833
|
-
server.listen(listenPort, "127.0.0.1", () => resolve());
|
|
834
|
-
});
|
|
835
|
-
const addr = server.address();
|
|
836
|
-
if (!addr || typeof addr === "string") {
|
|
837
|
-
server.close();
|
|
838
|
-
throw new CliError("Failed to start callback server", { exitCode: 1 });
|
|
1059
|
+
// src/commands/shellInit.ts
|
|
1060
|
+
import path9 from "path";
|
|
1061
|
+
import os3 from "os";
|
|
1062
|
+
import pc6 from "picocolors";
|
|
1063
|
+
import fse4 from "fs-extra";
|
|
1064
|
+
|
|
1065
|
+
// src/utils/packageJson.ts
|
|
1066
|
+
import fs7 from "fs/promises";
|
|
1067
|
+
async function readPackageJson(projectRoot) {
|
|
1068
|
+
const raw = await fs7.readFile(`${projectRoot}/package.json`, "utf8").catch(() => null);
|
|
1069
|
+
if (!raw) {
|
|
1070
|
+
throw new CliError("package.json not found.", { exitCode: 2 });
|
|
839
1071
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
return JSON.parse(raw);
|
|
1074
|
+
} catch {
|
|
1075
|
+
throw new CliError("Failed to parse package.json.", { exitCode: 2 });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
var STUDIO_PEERS = [
|
|
1079
|
+
"@callstack/liquid-glass",
|
|
1080
|
+
"@supabase/supabase-js",
|
|
1081
|
+
"@gorhom/bottom-sheet",
|
|
1082
|
+
"expo-file-system",
|
|
1083
|
+
"expo-haptics",
|
|
1084
|
+
"expo-linear-gradient",
|
|
1085
|
+
"lucide-react-native",
|
|
1086
|
+
"react-native-gesture-handler",
|
|
1087
|
+
"react-native-reanimated",
|
|
1088
|
+
"react-native-safe-area-context",
|
|
1089
|
+
"react-native-svg",
|
|
1090
|
+
"react-native-view-shot"
|
|
1091
|
+
];
|
|
1092
|
+
function buildShellPackageJson(params) {
|
|
1093
|
+
const orig = params.original;
|
|
1094
|
+
const warnings = [];
|
|
1095
|
+
const info = [];
|
|
1096
|
+
const dependencies = { ...orig.dependencies ?? {} };
|
|
1097
|
+
const devDependencies = { ...orig.devDependencies ?? {} };
|
|
1098
|
+
const main2 = "expo-router/entry";
|
|
1099
|
+
dependencies["@comergehq/studio"] = "latest";
|
|
1100
|
+
dependencies["@comergehq/runtime"] = "latest";
|
|
1101
|
+
if (!dependencies["expo-router"] && !devDependencies["expo-router"]) {
|
|
1102
|
+
dependencies["expo-router"] = "latest";
|
|
1103
|
+
warnings.push("Added missing dependency expo-router@latest.");
|
|
1104
|
+
}
|
|
1105
|
+
for (const dep of STUDIO_PEERS) {
|
|
1106
|
+
if (dependencies[dep] || devDependencies[dep]) continue;
|
|
1107
|
+
dependencies[dep] = "latest";
|
|
1108
|
+
info.push(`Added missing peer dependency ${dep}@latest.`);
|
|
1109
|
+
}
|
|
1110
|
+
const pkg = {
|
|
1111
|
+
...orig,
|
|
1112
|
+
name: orig.name ? `${orig.name}-comerge-shell` : "comerge-shell",
|
|
1113
|
+
private: true,
|
|
1114
|
+
main: main2,
|
|
1115
|
+
scripts: {
|
|
1116
|
+
dev: "expo start -c",
|
|
1117
|
+
ios: "expo start -c --ios",
|
|
1118
|
+
android: "expo start -c --android",
|
|
1119
|
+
web: "expo start -c --web",
|
|
1120
|
+
...orig.scripts ?? {}
|
|
1121
|
+
},
|
|
1122
|
+
dependencies,
|
|
1123
|
+
devDependencies
|
|
864
1124
|
};
|
|
865
|
-
return {
|
|
866
|
-
}
|
|
867
|
-
function escapeHtml(input) {
|
|
868
|
-
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
1125
|
+
return { pkg, warnings, info };
|
|
869
1126
|
}
|
|
870
1127
|
|
|
871
|
-
// src/
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
if (platform === "darwin") {
|
|
877
|
-
await execa2("open", [url], { stdio: "ignore" });
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
if (platform === "win32") {
|
|
881
|
-
await execa2("cmd", ["/c", "start", "", url], { stdio: "ignore", windowsHide: true });
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
await execa2("xdg-open", [url], { stdio: "ignore" });
|
|
885
|
-
} catch (err) {
|
|
886
|
-
throw new CliError("Failed to open browser automatically", {
|
|
887
|
-
exitCode: 1,
|
|
888
|
-
hint: `Open this URL manually:
|
|
889
|
-
${url}
|
|
1128
|
+
// src/utils/templates.ts
|
|
1129
|
+
function shellLayoutTsx() {
|
|
1130
|
+
return `import { Stack } from 'expo-router';
|
|
1131
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
890
1132
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1133
|
+
export default function Layout() {
|
|
1134
|
+
return (
|
|
1135
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
1136
|
+
<Stack screenOptions={{ headerShown: false }} />
|
|
1137
|
+
</GestureHandlerRootView>
|
|
1138
|
+
);
|
|
894
1139
|
}
|
|
1140
|
+
`;
|
|
1141
|
+
}
|
|
1142
|
+
function shellIndexTsx() {
|
|
1143
|
+
return `import * as React from 'react';
|
|
1144
|
+
import { View } from 'react-native';
|
|
1145
|
+
import { Stack } from 'expo-router';
|
|
1146
|
+
import { ComergeStudio } from '@comergehq/studio';
|
|
895
1147
|
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1148
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1149
|
+
// @ts-ignore
|
|
1150
|
+
import config from '../comerge.config.json';
|
|
1151
|
+
|
|
1152
|
+
export default function Index() {
|
|
1153
|
+
const appId = String((config as any)?.appId || '');
|
|
1154
|
+
const appKey = String((config as any)?.appKey || 'MicroMain');
|
|
1155
|
+
const apiKey = String((config as any)?.apiKey || '');
|
|
1156
|
+
return (
|
|
1157
|
+
<>
|
|
1158
|
+
<Stack.Screen options={{ headerShown: false }} />
|
|
1159
|
+
<View style={{ flex: 1 }}>
|
|
1160
|
+
{appId ? <ComergeStudio appId={appId} apiKey={apiKey} appKey={appKey} /> : null}
|
|
1161
|
+
</View>
|
|
1162
|
+
</>
|
|
1163
|
+
);
|
|
909
1164
|
}
|
|
910
|
-
|
|
911
|
-
return createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
912
|
-
auth: {
|
|
913
|
-
flowType: "pkce",
|
|
914
|
-
persistSession: false,
|
|
915
|
-
autoRefreshToken: false,
|
|
916
|
-
detectSessionInUrl: false,
|
|
917
|
-
storage
|
|
918
|
-
},
|
|
919
|
-
global: {
|
|
920
|
-
headers: {
|
|
921
|
-
"X-Requested-By": "comerge-cli"
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
});
|
|
1165
|
+
`;
|
|
925
1166
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1167
|
+
function shellBabelConfigJs() {
|
|
1168
|
+
return `module.exports = function (api) {
|
|
1169
|
+
api.cache(true);
|
|
930
1170
|
return {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
expires_at: session.expires_at,
|
|
934
|
-
token_type: session.token_type ?? void 0,
|
|
935
|
-
user: session.user ? { id: session.user.id, email: session.user.email ?? null } : void 0
|
|
1171
|
+
presets: ['babel-preset-expo'],
|
|
1172
|
+
plugins: ['react-native-reanimated/plugin'],
|
|
936
1173
|
};
|
|
1174
|
+
};
|
|
1175
|
+
`;
|
|
937
1176
|
}
|
|
938
|
-
function
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
options: {
|
|
946
|
-
redirectTo: params.redirectTo,
|
|
947
|
-
skipBrowserRedirect: true,
|
|
948
|
-
queryParams: {
|
|
949
|
-
access_type: "offline",
|
|
950
|
-
prompt: "consent"
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
if (error) throw error;
|
|
955
|
-
if (!data?.url) throw new CliError("Supabase did not return an OAuth URL", { exitCode: 1 });
|
|
956
|
-
return { url: data.url };
|
|
957
|
-
},
|
|
958
|
-
async exchangeCode(params) {
|
|
959
|
-
const { data, error } = await supabase.auth.exchangeCodeForSession(params.code);
|
|
960
|
-
if (error) throw error;
|
|
961
|
-
if (!data?.session) throw new CliError("No session returned from Supabase", { exitCode: 1 });
|
|
962
|
-
return toStoredSession(data.session);
|
|
963
|
-
},
|
|
964
|
-
async refreshWithStoredSession(params) {
|
|
965
|
-
const { data, error } = await supabase.auth.setSession({
|
|
966
|
-
access_token: params.session.access_token,
|
|
967
|
-
refresh_token: params.session.refresh_token
|
|
968
|
-
});
|
|
969
|
-
if (error) throw error;
|
|
970
|
-
if (!data?.session) throw new CliError("No session returned after refresh", { exitCode: 1 });
|
|
971
|
-
return toStoredSession(data.session);
|
|
972
|
-
}
|
|
973
|
-
};
|
|
1177
|
+
function shellMetroConfigJs() {
|
|
1178
|
+
return `const { getDefaultConfig } = require('expo/metro-config');
|
|
1179
|
+
|
|
1180
|
+
const config = getDefaultConfig(__dirname);
|
|
1181
|
+
|
|
1182
|
+
module.exports = config;
|
|
1183
|
+
`;
|
|
974
1184
|
}
|
|
975
1185
|
|
|
976
|
-
// src/
|
|
1186
|
+
// src/utils/packageManager.ts
|
|
977
1187
|
import fs8 from "fs/promises";
|
|
978
|
-
|
|
979
|
-
import path6 from "path";
|
|
980
|
-
import { z as z2 } from "zod";
|
|
981
|
-
var storedSessionSchema = z2.object({
|
|
982
|
-
access_token: z2.string().min(1),
|
|
983
|
-
refresh_token: z2.string().min(1),
|
|
984
|
-
expires_at: z2.number().int().positive(),
|
|
985
|
-
token_type: z2.string().min(1).optional(),
|
|
986
|
-
user: z2.object({
|
|
987
|
-
id: z2.string().min(1),
|
|
988
|
-
email: z2.string().email().optional().nullable()
|
|
989
|
-
}).optional()
|
|
990
|
-
});
|
|
991
|
-
var KEYTAR_SERVICE = "comerge-cli";
|
|
992
|
-
var KEYTAR_ACCOUNT = "default";
|
|
993
|
-
function xdgConfigHome() {
|
|
994
|
-
const v = process.env.XDG_CONFIG_HOME;
|
|
995
|
-
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
996
|
-
return path6.join(os2.homedir(), ".config");
|
|
997
|
-
}
|
|
998
|
-
function sessionFilePath() {
|
|
999
|
-
return path6.join(xdgConfigHome(), "comerge", "session.json");
|
|
1000
|
-
}
|
|
1001
|
-
async function maybeLoadKeytar() {
|
|
1188
|
+
async function exists(p) {
|
|
1002
1189
|
try {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
for (const c of candidates) {
|
|
1006
|
-
if (c && typeof c.getPassword === "function" && typeof c.setPassword === "function" && typeof c.deletePassword === "function") {
|
|
1007
|
-
return c;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return null;
|
|
1190
|
+
await fs8.stat(p);
|
|
1191
|
+
return true;
|
|
1011
1192
|
} catch {
|
|
1012
|
-
return
|
|
1193
|
+
return false;
|
|
1013
1194
|
}
|
|
1014
1195
|
}
|
|
1015
|
-
async function
|
|
1016
|
-
|
|
1017
|
-
await
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
try {
|
|
1023
|
-
await fs8.chmod(filePath, 384);
|
|
1024
|
-
} catch {
|
|
1025
|
-
}
|
|
1196
|
+
async function detectPackageManager(projectRoot) {
|
|
1197
|
+
if (await exists(`${projectRoot}/pnpm-lock.yaml`)) return "pnpm";
|
|
1198
|
+
if (await exists(`${projectRoot}/yarn.lock`)) return "yarn";
|
|
1199
|
+
if (await exists(`${projectRoot}/package-lock.json`)) return "npm";
|
|
1200
|
+
if (await exists(`${projectRoot}/bun.lockb`)) return "bun";
|
|
1201
|
+
return "npm";
|
|
1026
1202
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
const fp = sessionFilePath();
|
|
1041
|
-
const raw = await fs8.readFile(fp, "utf8").catch(() => null);
|
|
1042
|
-
if (!raw) return null;
|
|
1043
|
-
try {
|
|
1044
|
-
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
1045
|
-
if (!parsed.success) return null;
|
|
1046
|
-
await ensurePathPermissions(fp);
|
|
1047
|
-
return parsed.data;
|
|
1048
|
-
} catch {
|
|
1049
|
-
return null;
|
|
1203
|
+
function installCommand(pm) {
|
|
1204
|
+
switch (pm) {
|
|
1205
|
+
case "pnpm":
|
|
1206
|
+
return { cmd: "pnpm", args: ["install"] };
|
|
1207
|
+
case "yarn":
|
|
1208
|
+
return { cmd: "yarn", args: ["install"] };
|
|
1209
|
+
case "bun":
|
|
1210
|
+
return { cmd: "bun", args: ["install"] };
|
|
1211
|
+
case "npm":
|
|
1212
|
+
default:
|
|
1213
|
+
return { cmd: "npm", args: ["install"] };
|
|
1050
1214
|
}
|
|
1051
1215
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1216
|
+
|
|
1217
|
+
// src/commands/shellInit.ts
|
|
1218
|
+
import { execa as execa3 } from "execa";
|
|
1219
|
+
|
|
1220
|
+
// src/utils/copyProject.ts
|
|
1221
|
+
import path7 from "path";
|
|
1222
|
+
import fse2 from "fs-extra";
|
|
1223
|
+
var ALWAYS_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1224
|
+
"node_modules",
|
|
1225
|
+
".git",
|
|
1226
|
+
".expo",
|
|
1227
|
+
"dist",
|
|
1228
|
+
"build"
|
|
1229
|
+
]);
|
|
1230
|
+
function normalizeRel3(relPath) {
|
|
1231
|
+
return relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1065
1232
|
}
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1233
|
+
function shouldExclude(relPath) {
|
|
1234
|
+
const rel = normalizeRel3(relPath);
|
|
1235
|
+
const parts = rel.split("/").filter(Boolean);
|
|
1236
|
+
const top = parts[0] ?? "";
|
|
1237
|
+
if (!top) return false;
|
|
1238
|
+
if (ALWAYS_EXCLUDE_DIRS.has(top)) return true;
|
|
1239
|
+
if (top.startsWith("comerge-shell")) return true;
|
|
1240
|
+
if (rel.startsWith("ios/build/") || rel === "ios/build") return true;
|
|
1241
|
+
if (rel.startsWith("android/build/") || rel === "android/build") return true;
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
async function copyProject(params) {
|
|
1245
|
+
const src = path7.resolve(params.projectRoot);
|
|
1246
|
+
const dest = path7.resolve(params.outRoot);
|
|
1247
|
+
const srcExists = await fse2.pathExists(src);
|
|
1248
|
+
if (!srcExists) {
|
|
1249
|
+
throw new CliError("Project root not found.", { exitCode: 2 });
|
|
1071
1250
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1251
|
+
await fse2.copy(src, dest, {
|
|
1252
|
+
dereference: true,
|
|
1253
|
+
preserveTimestamps: true,
|
|
1254
|
+
filter: (srcPath) => {
|
|
1255
|
+
const rel = path7.relative(src, srcPath);
|
|
1256
|
+
if (!rel) return true;
|
|
1257
|
+
return !shouldExclude(rel);
|
|
1258
|
+
}
|
|
1074
1259
|
});
|
|
1075
1260
|
}
|
|
1076
1261
|
|
|
1077
|
-
// src/
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1262
|
+
// src/utils/stripProject.ts
|
|
1263
|
+
import path8 from "path";
|
|
1264
|
+
import fse3 from "fs-extra";
|
|
1265
|
+
var DEFAULT_STRIP_DIRS = ["app", "src", "components", "lib", "hooks", "providers"];
|
|
1266
|
+
async function stripProject(params) {
|
|
1267
|
+
const dirs = params.dirs ?? DEFAULT_STRIP_DIRS;
|
|
1268
|
+
await Promise.all(
|
|
1269
|
+
dirs.map(async (d) => {
|
|
1270
|
+
const p = path8.join(params.outRoot, d);
|
|
1271
|
+
const exists2 = await fse3.pathExists(p);
|
|
1272
|
+
if (!exists2) return;
|
|
1273
|
+
await fse3.remove(p);
|
|
1274
|
+
})
|
|
1275
|
+
);
|
|
1081
1276
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1277
|
+
|
|
1278
|
+
// src/commands/shellInit.ts
|
|
1279
|
+
import fs11 from "fs/promises";
|
|
1280
|
+
|
|
1281
|
+
// src/utils/appJsonPatch.ts
|
|
1282
|
+
import fs9 from "fs/promises";
|
|
1283
|
+
function pluginName(entry) {
|
|
1284
|
+
if (Array.isArray(entry)) return typeof entry[0] === "string" ? entry[0] : null;
|
|
1285
|
+
return typeof entry === "string" ? entry : null;
|
|
1286
|
+
}
|
|
1287
|
+
async function ensureComergeShellPlugins(appJsonPath) {
|
|
1288
|
+
let raw;
|
|
1085
1289
|
try {
|
|
1086
|
-
|
|
1290
|
+
raw = await fs9.readFile(appJsonPath, "utf8");
|
|
1087
1291
|
} catch {
|
|
1088
|
-
return
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
async function getAuthToken(config) {
|
|
1092
|
-
const envToken = process.env.COMERGE_ACCESS_TOKEN;
|
|
1093
|
-
if (typeof envToken === "string" && envToken.trim().length > 0) {
|
|
1094
|
-
return { token: envToken.trim(), session: null, fromEnv: true };
|
|
1095
|
-
}
|
|
1096
|
-
let session = await getSession();
|
|
1097
|
-
if (!session) {
|
|
1098
|
-
throw new CliError("Not logged in", {
|
|
1099
|
-
exitCode: 2,
|
|
1100
|
-
hint: "Run `comerge login` first, or set COMERGE_ACCESS_TOKEN for CI."
|
|
1101
|
-
});
|
|
1292
|
+
return false;
|
|
1102
1293
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
} catch (err) {
|
|
1109
|
-
void err;
|
|
1110
|
-
}
|
|
1294
|
+
let parsed;
|
|
1295
|
+
try {
|
|
1296
|
+
parsed = JSON.parse(raw);
|
|
1297
|
+
} catch {
|
|
1298
|
+
throw new CliError("Failed to parse app.json in the generated shell.", { exitCode: 2 });
|
|
1111
1299
|
}
|
|
1112
|
-
|
|
1300
|
+
const expo = parsed.expo;
|
|
1301
|
+
if (!expo || typeof expo !== "object") return false;
|
|
1302
|
+
const plugins = Array.isArray(expo.plugins) ? [...expo.plugins] : [];
|
|
1303
|
+
const routerEntry = plugins.find((p) => pluginName(p) === "expo-router");
|
|
1304
|
+
const runtimeEntry = plugins.find((p) => pluginName(p) === "@comergehq/runtime");
|
|
1305
|
+
const needsRouter = !routerEntry;
|
|
1306
|
+
const needsRuntime = !runtimeEntry;
|
|
1307
|
+
if (!needsRouter && !needsRuntime) return false;
|
|
1308
|
+
const rest = plugins.filter((p) => {
|
|
1309
|
+
const name = pluginName(p);
|
|
1310
|
+
return name !== "expo-router" && name !== "@comergehq/runtime";
|
|
1311
|
+
});
|
|
1312
|
+
expo.plugins = [routerEntry ?? "expo-router", runtimeEntry ?? "@comergehq/runtime", ...rest];
|
|
1313
|
+
await fs9.writeFile(appJsonPath, JSON.stringify({ expo }, null, 2) + "\n", "utf8");
|
|
1314
|
+
return true;
|
|
1113
1315
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1316
|
+
|
|
1317
|
+
// src/utils/reanimated.ts
|
|
1318
|
+
import fs10 from "fs/promises";
|
|
1319
|
+
async function ensureReanimatedBabelPlugin(babelConfigPath) {
|
|
1320
|
+
const raw = await fs10.readFile(babelConfigPath, "utf8").catch(() => null);
|
|
1321
|
+
if (!raw) return false;
|
|
1322
|
+
if (raw.includes("react-native-reanimated/plugin")) return false;
|
|
1323
|
+
const patched = patchBabelConfigAddReanimatedPlugin(raw);
|
|
1324
|
+
if (patched === raw) return false;
|
|
1325
|
+
await fs10.writeFile(babelConfigPath, patched, "utf8");
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
function patchBabelConfigAddReanimatedPlugin(raw) {
|
|
1329
|
+
const pluginsMatch = /(^|\n)([ \t]*)plugins\s*:\s*\[/m.exec(raw);
|
|
1330
|
+
if (pluginsMatch) {
|
|
1331
|
+
const matchIdx = pluginsMatch.index + pluginsMatch[1].length;
|
|
1332
|
+
const indent = pluginsMatch[2] ?? "";
|
|
1333
|
+
const bracketIdx = raw.indexOf("[", matchIdx);
|
|
1334
|
+
if (bracketIdx >= 0) {
|
|
1335
|
+
const closeIdx = findMatchingBracket(raw, bracketIdx, "[", "]");
|
|
1336
|
+
if (closeIdx >= 0) {
|
|
1337
|
+
const inner = raw.slice(bracketIdx + 1, closeIdx);
|
|
1338
|
+
const innerTrim = inner.trim();
|
|
1339
|
+
const innerTrimEnd = inner.replace(/[ \t\r\n]+$/g, "");
|
|
1340
|
+
const elementIndent = indent + " ";
|
|
1341
|
+
let insert = "";
|
|
1342
|
+
if (!innerTrim) {
|
|
1343
|
+
insert = `
|
|
1344
|
+
${elementIndent}'react-native-reanimated/plugin'
|
|
1345
|
+
${indent}`;
|
|
1346
|
+
} else if (innerTrimEnd.endsWith(",")) {
|
|
1347
|
+
insert = `
|
|
1348
|
+
${elementIndent}'react-native-reanimated/plugin'`;
|
|
1349
|
+
} else {
|
|
1350
|
+
insert = `,
|
|
1351
|
+
${elementIndent}'react-native-reanimated/plugin'`;
|
|
1127
1352
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
};
|
|
1131
|
-
let res = await doFetch(token);
|
|
1132
|
-
if (res.status === 401 && !fromEnv && session?.refresh_token) {
|
|
1133
|
-
const supabase = createSupabaseAuthHelpers(cfg);
|
|
1134
|
-
const refreshed = await supabase.refreshWithStoredSession({ session });
|
|
1135
|
-
await setSession(refreshed);
|
|
1136
|
-
res = await doFetch(refreshed.access_token);
|
|
1137
|
-
}
|
|
1138
|
-
if (!res.ok) {
|
|
1139
|
-
const body = await readJsonSafe2(res);
|
|
1140
|
-
const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (${res.status})`;
|
|
1141
|
-
throw new CliError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
1353
|
+
return raw.slice(0, closeIdx) + insert + raw.slice(closeIdx);
|
|
1354
|
+
}
|
|
1142
1355
|
}
|
|
1143
|
-
const json = await readJsonSafe2(res);
|
|
1144
|
-
return json ?? null;
|
|
1145
1356
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1357
|
+
const presetsMatch = /(^|\n)([ \t]*)presets\s*:\s*\[[^\]]*\]\s*,?/m.exec(raw);
|
|
1358
|
+
if (presetsMatch) {
|
|
1359
|
+
const indent = presetsMatch[2] ?? "";
|
|
1360
|
+
const insertAt = presetsMatch.index + presetsMatch[0].length;
|
|
1361
|
+
const insertion = `
|
|
1362
|
+
${indent}// Required by react-native-reanimated. Must be listed last.
|
|
1363
|
+
${indent}plugins: ['react-native-reanimated/plugin'],`;
|
|
1364
|
+
return raw.slice(0, insertAt) + insertion + raw.slice(insertAt);
|
|
1365
|
+
}
|
|
1366
|
+
return raw;
|
|
1152
1367
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
throw new CliError("Invalid --port", { exitCode: 2, hint: "Example: --port 8085" });
|
|
1167
|
-
}
|
|
1168
|
-
port = parsed;
|
|
1368
|
+
function findMatchingBracket(text, openIdx, open, close) {
|
|
1369
|
+
let depth = 0;
|
|
1370
|
+
let inSingle = false;
|
|
1371
|
+
let inDouble = false;
|
|
1372
|
+
let inTemplate = false;
|
|
1373
|
+
let inLineComment = false;
|
|
1374
|
+
let inBlockComment = false;
|
|
1375
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
1376
|
+
const ch = text[i];
|
|
1377
|
+
const next = text[i + 1];
|
|
1378
|
+
if (inLineComment) {
|
|
1379
|
+
if (ch === "\n") inLineComment = false;
|
|
1380
|
+
continue;
|
|
1169
1381
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const shouldOpenBrowser = Boolean(opts.browser ?? true);
|
|
1175
|
-
if (shouldOpenBrowser) {
|
|
1176
|
-
try {
|
|
1177
|
-
await openBrowser(url);
|
|
1178
|
-
console.log(pc2.dim("Opened browser for login..."));
|
|
1179
|
-
} catch (err) {
|
|
1180
|
-
console.error(pc2.yellow("Could not open the browser automatically."));
|
|
1181
|
-
console.error(pc2.dim(err?.message ?? String(err)));
|
|
1182
|
-
console.log(`Open this URL manually:
|
|
1183
|
-
${url}`);
|
|
1184
|
-
}
|
|
1185
|
-
} else {
|
|
1186
|
-
console.log(`Open this URL to login:
|
|
1187
|
-
${url}`);
|
|
1382
|
+
if (inBlockComment) {
|
|
1383
|
+
if (ch === "*" && next === "/") {
|
|
1384
|
+
inBlockComment = false;
|
|
1385
|
+
i++;
|
|
1188
1386
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
await clearSession().catch(() => {
|
|
1198
|
-
});
|
|
1199
|
-
throw err;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (!inSingle && !inDouble && !inTemplate) {
|
|
1390
|
+
if (ch === "/" && next === "/") {
|
|
1391
|
+
inLineComment = true;
|
|
1392
|
+
i++;
|
|
1393
|
+
continue;
|
|
1200
1394
|
}
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const label = profile?.email ? `${profile.email}` : profile?.id ? `user ${profile.id}` : "user";
|
|
1206
|
-
console.log(pc2.green(`Logged in as ${label}.`));
|
|
1395
|
+
if (ch === "/" && next === "*") {
|
|
1396
|
+
inBlockComment = true;
|
|
1397
|
+
i++;
|
|
1398
|
+
continue;
|
|
1207
1399
|
}
|
|
1208
|
-
} finally {
|
|
1209
|
-
await server.close();
|
|
1210
1400
|
}
|
|
1211
|
-
|
|
1401
|
+
if (!inDouble && !inTemplate && ch === "'" && text[i - 1] !== "\\") {
|
|
1402
|
+
inSingle = !inSingle;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (!inSingle && !inTemplate && ch === `"` && text[i - 1] !== "\\") {
|
|
1406
|
+
inDouble = !inDouble;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (!inSingle && !inDouble && ch === "`" && text[i - 1] !== "\\") {
|
|
1410
|
+
inTemplate = !inTemplate;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
1414
|
+
if (ch === open) depth++;
|
|
1415
|
+
if (ch === close) {
|
|
1416
|
+
depth--;
|
|
1417
|
+
if (depth === 0) return i;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return -1;
|
|
1212
1421
|
}
|
|
1213
1422
|
|
|
1214
|
-
// src/commands/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1423
|
+
// src/commands/shellInit.ts
|
|
1424
|
+
async function shellInit(params) {
|
|
1425
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
1426
|
+
const outDirDefault = params.outDir || "comerge-shell";
|
|
1427
|
+
let outDir = outDirDefault;
|
|
1428
|
+
let appId = params.appId ?? "";
|
|
1429
|
+
let apiKey = params.apiKey ?? "";
|
|
1430
|
+
let appKey = params.appKey ?? "MicroMain";
|
|
1431
|
+
appId = String(appId || "").trim();
|
|
1432
|
+
apiKey = String(apiKey || "").trim();
|
|
1433
|
+
if (!appId) {
|
|
1434
|
+
throw new CliError("Missing app ID.", {
|
|
1435
|
+
hint: "Pass --app-id <uuid>.",
|
|
1436
|
+
exitCode: 2
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
if (!apiKey) {
|
|
1440
|
+
throw new CliError("Missing client key.", {
|
|
1441
|
+
hint: "Pass --api-key <key>.",
|
|
1442
|
+
exitCode: 2
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const requestedOutputPath = path9.resolve(projectRoot, outDir);
|
|
1446
|
+
const outputPath = await findAvailableDirPath(requestedOutputPath);
|
|
1447
|
+
console.log(pc6.dim(`Project: ${projectRoot}`));
|
|
1448
|
+
if (outputPath !== requestedOutputPath) {
|
|
1449
|
+
console.log(pc6.dim(`Output: ${requestedOutputPath} (already exists)`));
|
|
1450
|
+
console.log(pc6.dim(`Using: ${outputPath}`));
|
|
1451
|
+
} else {
|
|
1452
|
+
console.log(pc6.dim(`Output: ${outputPath}`));
|
|
1453
|
+
}
|
|
1454
|
+
const outputIsInsideProject = outputPath === projectRoot || outputPath.startsWith(projectRoot + path9.sep);
|
|
1455
|
+
const stagingPath = outputIsInsideProject ? await fs11.mkdtemp(path9.join(os3.tmpdir(), "comerge-shell-")) : outputPath;
|
|
1456
|
+
let movedToFinal = false;
|
|
1457
|
+
try {
|
|
1458
|
+
if (!outputIsInsideProject) {
|
|
1459
|
+
await ensureEmptyDir(stagingPath);
|
|
1223
1460
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1461
|
+
await copyProject({ projectRoot, outRoot: stagingPath });
|
|
1462
|
+
await stripProject({ outRoot: stagingPath });
|
|
1463
|
+
try {
|
|
1464
|
+
const didPatch = await ensureComergeShellPlugins(path9.join(stagingPath, "app.json"));
|
|
1465
|
+
if (didPatch) {
|
|
1466
|
+
console.log(pc6.dim("Updated app.json with required Comerge shell plugins."));
|
|
1467
|
+
}
|
|
1468
|
+
} catch {
|
|
1227
1469
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
import pc4 from "picocolors";
|
|
1233
|
-
function registerWhoamiCommand(program) {
|
|
1234
|
-
program.command("whoami").description("Show the currently authenticated Comerge user").option("--api-key <pk>", "API Key (or set COMERGE_API_KEY)").option("--yes", "Non-interactive; do not prompt", false).option("--json", "Output machine-readable JSON", false).action(async (opts) => {
|
|
1235
|
-
const config = await resolveConfig({
|
|
1236
|
-
apiKey: opts.apiKey ? String(opts.apiKey) : null,
|
|
1237
|
-
yes: Boolean(opts.yes)
|
|
1470
|
+
await writeJsonAtomic(path9.join(stagingPath, "comerge.config.json"), {
|
|
1471
|
+
appId,
|
|
1472
|
+
appKey: appKey || "MicroMain",
|
|
1473
|
+
apiKey
|
|
1238
1474
|
});
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1475
|
+
await writeTextAtomic(path9.join(stagingPath, "app/_layout.tsx"), shellLayoutTsx());
|
|
1476
|
+
await writeTextAtomic(path9.join(stagingPath, "app/index.tsx"), shellIndexTsx());
|
|
1477
|
+
await ensureTextFile(path9.join(stagingPath, "babel.config.js"), shellBabelConfigJs());
|
|
1478
|
+
await ensureTextFile(path9.join(stagingPath, "metro.config.js"), shellMetroConfigJs());
|
|
1479
|
+
try {
|
|
1480
|
+
const didPatch = await ensureReanimatedBabelPlugin(path9.join(stagingPath, "babel.config.js"));
|
|
1481
|
+
if (didPatch) console.log(pc6.dim("Updated babel.config.js to include react-native-reanimated/plugin."));
|
|
1482
|
+
} catch {
|
|
1244
1483
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1484
|
+
await ensureTextFile(
|
|
1485
|
+
path9.join(stagingPath, "tsconfig.json"),
|
|
1486
|
+
JSON.stringify(
|
|
1487
|
+
{
|
|
1488
|
+
extends: "expo/tsconfig.base",
|
|
1489
|
+
compilerOptions: { strict: true, resolveJsonModule: true }
|
|
1490
|
+
},
|
|
1491
|
+
null,
|
|
1492
|
+
2
|
|
1493
|
+
) + "\n"
|
|
1494
|
+
);
|
|
1495
|
+
const originalPkg = await readPackageJson(stagingPath);
|
|
1496
|
+
const { pkg: shellPkg, warnings, info } = buildShellPackageJson({
|
|
1497
|
+
original: originalPkg
|
|
1498
|
+
});
|
|
1499
|
+
await writeJsonAtomic(path9.join(stagingPath, "package.json"), shellPkg);
|
|
1500
|
+
for (const w of warnings) console.log(pc6.yellow(`Warning: ${w}`));
|
|
1501
|
+
for (const i of info) console.log(pc6.dim(`Info: ${i}`));
|
|
1502
|
+
if (outputIsInsideProject) {
|
|
1503
|
+
await fse4.move(stagingPath, outputPath, { overwrite: false });
|
|
1504
|
+
movedToFinal = true;
|
|
1505
|
+
}
|
|
1506
|
+
const finalPath = outputIsInsideProject ? outputPath : stagingPath;
|
|
1507
|
+
const pm = params.packageManager ?? await detectPackageManager(projectRoot);
|
|
1508
|
+
const { cmd, args } = installCommand(pm);
|
|
1509
|
+
console.log(pc6.dim(`Installing dependencies with ${cmd}...`));
|
|
1510
|
+
const res = await execa3(cmd, args, { cwd: finalPath, stdio: "inherit" });
|
|
1511
|
+
if (res.exitCode !== 0) {
|
|
1512
|
+
throw new CliError("Dependency installation failed.", { exitCode: res.exitCode ?? 1 });
|
|
1513
|
+
}
|
|
1514
|
+
console.log(pc6.dim("Running Expo prebuild..."));
|
|
1515
|
+
const prebuild = await execa3("npx", ["expo", "prebuild"], { cwd: finalPath, stdio: "inherit" });
|
|
1516
|
+
if (prebuild.exitCode !== 0) {
|
|
1517
|
+
throw new CliError("Expo prebuild failed.", { exitCode: prebuild.exitCode ?? 1 });
|
|
1518
|
+
}
|
|
1519
|
+
console.log(pc6.green("Comerge has been installed successfully."));
|
|
1520
|
+
console.log(pc6.dim("You can now open the app from the My apps page in the Comerge app."));
|
|
1521
|
+
} finally {
|
|
1522
|
+
if (outputIsInsideProject && !movedToFinal) {
|
|
1523
|
+
try {
|
|
1524
|
+
await fse4.remove(stagingPath);
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1249
1529
|
}
|
|
1250
|
-
|
|
1251
|
-
// src/commands/import.ts
|
|
1252
|
-
import pc6 from "picocolors";
|
|
1253
|
-
|
|
1254
|
-
// src/lib/importLocal.ts
|
|
1255
|
-
import path9 from "path";
|
|
1256
|
-
import pc5 from "picocolors";
|
|
1257
|
-
|
|
1258
|
-
// src/lib/gitFiles.ts
|
|
1259
|
-
import { execa as execa3 } from "execa";
|
|
1260
|
-
async function canUseGit(cwd) {
|
|
1530
|
+
async function ensureTextFile(filePath, contents) {
|
|
1261
1531
|
try {
|
|
1262
|
-
|
|
1263
|
-
return
|
|
1532
|
+
await fs11.access(filePath);
|
|
1533
|
+
return;
|
|
1264
1534
|
} catch {
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
function parseGitLsFilesZ(buf) {
|
|
1269
|
-
return buf.toString("utf8").split("\0").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1270
|
-
}
|
|
1271
|
-
async function listFilesGit(params) {
|
|
1272
|
-
const args = ["ls-files", "-z", "--cached", "--others", "--exclude-standard"];
|
|
1273
|
-
if (params.pathspec) {
|
|
1274
|
-
args.push("--", params.pathspec);
|
|
1275
1535
|
}
|
|
1276
|
-
|
|
1277
|
-
return parseGitLsFilesZ(Buffer.from(res.stdout ?? "", "utf8"));
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// src/lib/fileSelection.ts
|
|
1281
|
-
import fs9 from "fs/promises";
|
|
1282
|
-
import path7 from "path";
|
|
1283
|
-
import fg from "fast-glob";
|
|
1284
|
-
import ignore from "ignore";
|
|
1285
|
-
var BUILTIN_IGNORES = [
|
|
1286
|
-
"**/node_modules/**",
|
|
1287
|
-
"**/.git/**",
|
|
1288
|
-
"**/.expo/**",
|
|
1289
|
-
"**/dist/**",
|
|
1290
|
-
"**/build/**",
|
|
1291
|
-
"**/coverage/**",
|
|
1292
|
-
"**/.turbo/**",
|
|
1293
|
-
"**/.next/**",
|
|
1294
|
-
"**/ios/build/**",
|
|
1295
|
-
"**/android/build/**",
|
|
1296
|
-
"**/.pnpm-store/**",
|
|
1297
|
-
"**/comerge-shell*/**"
|
|
1298
|
-
];
|
|
1299
|
-
function normalizeRel2(p) {
|
|
1300
|
-
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1301
|
-
}
|
|
1302
|
-
async function readGitignore(root) {
|
|
1303
|
-
const fp = path7.join(root, ".gitignore");
|
|
1304
|
-
return await fs9.readFile(fp, "utf8").catch(() => "");
|
|
1305
|
-
}
|
|
1306
|
-
async function listFilesFs(params) {
|
|
1307
|
-
const ig = ignore();
|
|
1308
|
-
const raw = await readGitignore(params.root);
|
|
1309
|
-
if (raw) ig.add(raw);
|
|
1310
|
-
const entries = await fg(["**/*"], {
|
|
1311
|
-
cwd: params.root,
|
|
1312
|
-
onlyFiles: true,
|
|
1313
|
-
dot: true,
|
|
1314
|
-
followSymbolicLinks: false,
|
|
1315
|
-
unique: true,
|
|
1316
|
-
ignore: [...BUILTIN_IGNORES]
|
|
1317
|
-
});
|
|
1318
|
-
const filtered = entries.filter((p) => {
|
|
1319
|
-
const rel = normalizeRel2(p);
|
|
1320
|
-
if (!params.includeDotenv && path7.posix.basename(rel).startsWith(".env")) return false;
|
|
1321
|
-
return !ig.ignores(rel);
|
|
1322
|
-
});
|
|
1323
|
-
return filtered.map(normalizeRel2);
|
|
1536
|
+
await writeTextAtomic(filePath, contents);
|
|
1324
1537
|
}
|
|
1325
1538
|
|
|
1326
|
-
// src/
|
|
1327
|
-
import
|
|
1328
|
-
import
|
|
1329
|
-
import
|
|
1330
|
-
import
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
let totalBytes = 0;
|
|
1358
|
-
const largest = [];
|
|
1359
|
-
for (const rel0 of params.files) {
|
|
1360
|
-
const rel = validateRelPath(rel0);
|
|
1361
|
-
const abs = path8.join(params.root, rel);
|
|
1362
|
-
const st = await fsp.stat(abs);
|
|
1363
|
-
if (!st.isFile()) continue;
|
|
1364
|
-
const bytes = st.size;
|
|
1365
|
-
totalBytes += bytes;
|
|
1366
|
-
if (largestN > 0) {
|
|
1367
|
-
largest.push({ path: rel, bytes });
|
|
1539
|
+
// src/utils/appDefaults.ts
|
|
1540
|
+
import fs12 from "fs/promises";
|
|
1541
|
+
import path10 from "path";
|
|
1542
|
+
import fg2 from "fast-glob";
|
|
1543
|
+
import { execa as execa4 } from "execa";
|
|
1544
|
+
function normalizeString(value) {
|
|
1545
|
+
if (typeof value !== "string") return null;
|
|
1546
|
+
const trimmed = value.trim();
|
|
1547
|
+
return trimmed ? trimmed : null;
|
|
1548
|
+
}
|
|
1549
|
+
function normalizeSlug(value) {
|
|
1550
|
+
if (!value) return null;
|
|
1551
|
+
const cleaned = value.trim().toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1552
|
+
return cleaned || null;
|
|
1553
|
+
}
|
|
1554
|
+
function suggestBundleId(slug) {
|
|
1555
|
+
if (!slug) return null;
|
|
1556
|
+
return `com.example.${slug}`;
|
|
1557
|
+
}
|
|
1558
|
+
function derivePlatforms(config, iosBundleId, androidPackageName) {
|
|
1559
|
+
const platforms = Array.isArray(config.platforms) ? config.platforms.map(String) : null;
|
|
1560
|
+
const iosEnabled = platforms ? platforms.includes("ios") : iosBundleId ? true : null;
|
|
1561
|
+
const androidEnabled = platforms ? platforms.includes("android") : androidPackageName ? true : null;
|
|
1562
|
+
return { iosEnabled, androidEnabled };
|
|
1563
|
+
}
|
|
1564
|
+
async function loadExpoConfig(projectRoot) {
|
|
1565
|
+
try {
|
|
1566
|
+
const res = await execa4("npx", ["expo", "config", "--json"], { cwd: projectRoot });
|
|
1567
|
+
const parsed = JSON.parse(res.stdout);
|
|
1568
|
+
if (parsed && typeof parsed === "object" && "expo" in parsed) {
|
|
1569
|
+
return parsed.expo ?? {};
|
|
1368
1570
|
}
|
|
1571
|
+
return parsed;
|
|
1572
|
+
} catch {
|
|
1369
1573
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
async function createZip(params) {
|
|
1378
|
-
const zipName = (params.zipName ?? "import.zip").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
1379
|
-
const tmpDir = await fsp.mkdtemp(path8.join(os3.tmpdir(), "comerge-import-"));
|
|
1380
|
-
const zipPath = path8.join(tmpDir, zipName);
|
|
1381
|
-
await new Promise((resolve, reject) => {
|
|
1382
|
-
const output = fs10.createWriteStream(zipPath);
|
|
1383
|
-
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
1384
|
-
output.on("close", () => resolve());
|
|
1385
|
-
output.on("error", (err) => reject(err));
|
|
1386
|
-
archive.on("warning", (err) => {
|
|
1387
|
-
reject(err);
|
|
1388
|
-
});
|
|
1389
|
-
archive.on("error", (err) => reject(err));
|
|
1390
|
-
archive.pipe(output);
|
|
1391
|
-
for (const rel0 of params.files) {
|
|
1392
|
-
const rel = validateRelPath(rel0);
|
|
1393
|
-
const abs = path8.join(params.root, rel);
|
|
1394
|
-
archive.file(abs, { name: rel });
|
|
1574
|
+
const appJsonPath = path10.join(projectRoot, "app.json");
|
|
1575
|
+
const rawAppJson = await fs12.readFile(appJsonPath, "utf8").catch(() => null);
|
|
1576
|
+
if (!rawAppJson) return {};
|
|
1577
|
+
try {
|
|
1578
|
+
const parsed = JSON.parse(rawAppJson);
|
|
1579
|
+
if (parsed && typeof parsed === "object" && "expo" in parsed) {
|
|
1580
|
+
return parsed.expo ?? {};
|
|
1395
1581
|
}
|
|
1396
|
-
|
|
1397
|
-
});
|
|
1398
|
-
const st = await fsp.stat(zipPath);
|
|
1399
|
-
if (st.size > MAX_IMPORT_ZIP_BYTES) {
|
|
1400
|
-
throw new CliError("Archive exceeds max upload size", {
|
|
1401
|
-
exitCode: 2,
|
|
1402
|
-
hint: `Zip size ${st.size} bytes exceeds limit ${MAX_IMPORT_ZIP_BYTES} bytes. Use --path or add ignores.`
|
|
1403
|
-
});
|
|
1582
|
+
} catch {
|
|
1404
1583
|
}
|
|
1405
|
-
return {
|
|
1584
|
+
return {};
|
|
1406
1585
|
}
|
|
1407
|
-
async function
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
s.on("error", reject);
|
|
1413
|
-
s.on("end", () => resolve());
|
|
1586
|
+
async function findIosBundleId(projectRoot) {
|
|
1587
|
+
const pbxprojMatches = await fg2("ios/**/*.xcodeproj/project.pbxproj", {
|
|
1588
|
+
cwd: projectRoot,
|
|
1589
|
+
absolute: true,
|
|
1590
|
+
suppressErrors: true
|
|
1414
1591
|
});
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const fileStream = fs11.createReadStream(params.filePath);
|
|
1426
|
-
const pass = new PassThrough();
|
|
1427
|
-
let sent = 0;
|
|
1428
|
-
fileStream.on("data", (chunk) => {
|
|
1429
|
-
sent += chunk.length;
|
|
1430
|
-
params.onProgress?.({ sentBytes: sent, totalBytes });
|
|
1592
|
+
for (const file of pbxprojMatches) {
|
|
1593
|
+
const raw = await fs12.readFile(file, "utf8").catch(() => null);
|
|
1594
|
+
if (!raw) continue;
|
|
1595
|
+
const match = raw.match(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([A-Za-z0-9.\-]+)\s*;/);
|
|
1596
|
+
if (match && match[1]) return match[1];
|
|
1597
|
+
}
|
|
1598
|
+
const plistMatches = await fg2("ios/**/Info.plist", {
|
|
1599
|
+
cwd: projectRoot,
|
|
1600
|
+
absolute: true,
|
|
1601
|
+
suppressErrors: true
|
|
1431
1602
|
});
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1603
|
+
for (const file of plistMatches) {
|
|
1604
|
+
const raw = await fs12.readFile(file, "utf8").catch(() => null);
|
|
1605
|
+
if (!raw) continue;
|
|
1606
|
+
const match = raw.match(/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/);
|
|
1607
|
+
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
async function findAndroidPackageName(projectRoot) {
|
|
1612
|
+
const gradlePaths = ["android/app/build.gradle", "android/app/build.gradle.kts"];
|
|
1613
|
+
for (const rel of gradlePaths) {
|
|
1614
|
+
const file = path10.join(projectRoot, rel);
|
|
1615
|
+
const raw = await fs12.readFile(file, "utf8").catch(() => null);
|
|
1616
|
+
if (!raw) continue;
|
|
1617
|
+
const match = raw.match(/applicationId\s+["']([^"']+)["']/) || raw.match(/applicationId\s*=\s*["']([^"']+)["']/);
|
|
1618
|
+
if (match && match[1]) return match[1];
|
|
1619
|
+
}
|
|
1620
|
+
const manifestMatches = await fg2("android/**/AndroidManifest.xml", {
|
|
1621
|
+
cwd: projectRoot,
|
|
1622
|
+
absolute: true,
|
|
1623
|
+
suppressErrors: true
|
|
1439
1624
|
});
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1625
|
+
for (const file of manifestMatches) {
|
|
1626
|
+
const raw = await fs12.readFile(file, "utf8").catch(() => null);
|
|
1627
|
+
if (!raw) continue;
|
|
1628
|
+
const match = raw.match(/<manifest[^>]*\spackage="([^"]+)"/);
|
|
1629
|
+
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
1630
|
+
}
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
async function inferExpoAppDefaults(projectRoot) {
|
|
1634
|
+
const expoConfig = await loadExpoConfig(projectRoot);
|
|
1635
|
+
const name = normalizeString(expoConfig.name);
|
|
1636
|
+
const slug = normalizeSlug(normalizeString(expoConfig.slug));
|
|
1637
|
+
const iosBundleId = normalizeString(expoConfig.ios?.bundleIdentifier);
|
|
1638
|
+
const androidPackageName = normalizeString(expoConfig.android?.package);
|
|
1639
|
+
const platforms = derivePlatforms(expoConfig, iosBundleId, androidPackageName);
|
|
1640
|
+
let fallbackName = name;
|
|
1641
|
+
if (!fallbackName) {
|
|
1642
|
+
const pkg = await readPackageJson(projectRoot).catch(() => null);
|
|
1643
|
+
fallbackName = normalizeString(pkg?.name ?? null);
|
|
1644
|
+
}
|
|
1645
|
+
if (!fallbackName) {
|
|
1646
|
+
fallbackName = normalizeString(path10.basename(projectRoot));
|
|
1647
|
+
}
|
|
1648
|
+
const resolvedSlug = slug ?? normalizeSlug(fallbackName ?? null) ?? normalizeSlug(path10.basename(projectRoot));
|
|
1649
|
+
const suggestedBundleId = suggestBundleId(resolvedSlug);
|
|
1650
|
+
const nativeIosBundleId = iosBundleId ?? await findIosBundleId(projectRoot);
|
|
1651
|
+
const nativeAndroidPackage = androidPackageName ?? await findAndroidPackageName(projectRoot);
|
|
1652
|
+
return {
|
|
1653
|
+
name: fallbackName,
|
|
1654
|
+
slug: resolvedSlug,
|
|
1655
|
+
iosBundleId: nativeIosBundleId,
|
|
1656
|
+
androidPackageName: nativeAndroidPackage,
|
|
1657
|
+
iosEnabled: platforms.iosEnabled,
|
|
1658
|
+
androidEnabled: platforms.androidEnabled,
|
|
1659
|
+
suggestedBundleId
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/services/apiKeyStore.ts
|
|
1664
|
+
import fs13 from "fs/promises";
|
|
1665
|
+
import os4 from "os";
|
|
1666
|
+
import path11 from "path";
|
|
1667
|
+
var KEYTAR_SERVICE2 = "comerge-cli-api-keys";
|
|
1668
|
+
function xdgConfigHome2() {
|
|
1669
|
+
const v = process.env.XDG_CONFIG_HOME;
|
|
1670
|
+
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
1671
|
+
return path11.join(os4.homedir(), ".config");
|
|
1672
|
+
}
|
|
1673
|
+
function apiKeyFilePath() {
|
|
1674
|
+
return path11.join(xdgConfigHome2(), "comerge", "api-keys.json");
|
|
1675
|
+
}
|
|
1676
|
+
async function ensurePathPermissions2(filePath) {
|
|
1677
|
+
const dir = path11.dirname(filePath);
|
|
1678
|
+
await fs13.mkdir(dir, { recursive: true });
|
|
1679
|
+
try {
|
|
1680
|
+
await fs13.chmod(dir, 448);
|
|
1681
|
+
} catch {
|
|
1682
|
+
}
|
|
1683
|
+
try {
|
|
1684
|
+
await fs13.chmod(filePath, 384);
|
|
1685
|
+
} catch {
|
|
1447
1686
|
}
|
|
1448
1687
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1688
|
+
async function maybeLoadKeytar2() {
|
|
1689
|
+
try {
|
|
1690
|
+
const mod = await import("keytar");
|
|
1691
|
+
const candidates = [mod?.default, mod].filter(Boolean);
|
|
1692
|
+
for (const c of candidates) {
|
|
1693
|
+
if (c && typeof c.getPassword === "function" && typeof c.setPassword === "function") {
|
|
1694
|
+
return c;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
} catch {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1455
1701
|
}
|
|
1456
|
-
function
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1702
|
+
function keyId(orgId, clientAppId) {
|
|
1703
|
+
return `${orgId}:${clientAppId}`;
|
|
1704
|
+
}
|
|
1705
|
+
async function readKeyFile() {
|
|
1706
|
+
const fp = apiKeyFilePath();
|
|
1707
|
+
const raw = await fs13.readFile(fp, "utf8").catch(() => null);
|
|
1708
|
+
if (!raw) return {};
|
|
1709
|
+
try {
|
|
1710
|
+
const parsed = JSON.parse(raw);
|
|
1711
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
1712
|
+
return parsed;
|
|
1713
|
+
} catch {
|
|
1714
|
+
return {};
|
|
1463
1715
|
}
|
|
1464
|
-
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
1465
1716
|
}
|
|
1466
|
-
function
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1717
|
+
async function getStoredApiKey(orgId, clientAppId) {
|
|
1718
|
+
const account = keyId(orgId, clientAppId);
|
|
1719
|
+
const keytar = await maybeLoadKeytar2();
|
|
1720
|
+
if (keytar) {
|
|
1721
|
+
const raw = await keytar.getPassword(KEYTAR_SERVICE2, account);
|
|
1722
|
+
return raw && raw.trim() ? raw.trim() : null;
|
|
1723
|
+
}
|
|
1724
|
+
const map = await readKeyFile();
|
|
1725
|
+
const key = map[account];
|
|
1726
|
+
return key && key.trim() ? key.trim() : null;
|
|
1469
1727
|
}
|
|
1470
|
-
function
|
|
1471
|
-
|
|
1472
|
-
const
|
|
1473
|
-
if (!
|
|
1474
|
-
|
|
1475
|
-
throw new CliError("Invalid --path", {
|
|
1476
|
-
exitCode: 2,
|
|
1477
|
-
hint: "Use a safe subdirectory like packages/mobile-app (letters/numbers/._-/ only)."
|
|
1478
|
-
});
|
|
1728
|
+
async function setStoredApiKey(orgId, clientAppId, apiKey) {
|
|
1729
|
+
const account = keyId(orgId, clientAppId);
|
|
1730
|
+
const trimmed = String(apiKey || "").trim();
|
|
1731
|
+
if (!trimmed) {
|
|
1732
|
+
throw new CliError("API key is empty and was not stored.", { exitCode: 1 });
|
|
1479
1733
|
}
|
|
1480
|
-
|
|
1734
|
+
const keytar = await maybeLoadKeytar2();
|
|
1735
|
+
if (keytar) {
|
|
1736
|
+
await keytar.setPassword(KEYTAR_SERVICE2, account, trimmed);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const fp = apiKeyFilePath();
|
|
1740
|
+
const map = await readKeyFile();
|
|
1741
|
+
map[account] = trimmed;
|
|
1742
|
+
await writeJsonAtomic(fp, map);
|
|
1743
|
+
await ensurePathPermissions2(fp);
|
|
1481
1744
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1745
|
+
|
|
1746
|
+
// src/services/initLocal.ts
|
|
1747
|
+
function unwrapResponseObject(resp, label) {
|
|
1748
|
+
const obj = resp?.responseObject;
|
|
1749
|
+
if (obj === void 0 || obj === null) {
|
|
1750
|
+
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
1751
|
+
throw new CliError(message, { exitCode: 1, hint: resp ? JSON.stringify(resp, null, 2) : null });
|
|
1752
|
+
}
|
|
1753
|
+
return obj;
|
|
1487
1754
|
}
|
|
1488
|
-
async function
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
1492
|
-
const subdir = validateOptionalSubdir(params.subdir);
|
|
1493
|
-
const importRoot = subdir ? path9.join(projectRoot, subdir) : projectRoot;
|
|
1494
|
-
const appName = (params.appName ?? "").trim() || deriveAppName(projectRoot);
|
|
1495
|
-
const log = (msg) => {
|
|
1496
|
-
if (params.json) return;
|
|
1497
|
-
console.log(msg);
|
|
1498
|
-
};
|
|
1499
|
-
log(pc5.dim(`Project: ${projectRoot}`));
|
|
1500
|
-
if (subdir) log(pc5.dim(`Import path: ${subdir}`));
|
|
1501
|
-
let files = [];
|
|
1502
|
-
const useGit = await canUseGit(projectRoot);
|
|
1503
|
-
if (useGit) {
|
|
1504
|
-
const gitPaths = await listFilesGit({ cwd: projectRoot, pathspec: subdir ?? null });
|
|
1505
|
-
files = gitPaths.filter((p) => !COMERGE_SHELL_SEGMENT_RE.test(p.replace(/\\/g, "/")));
|
|
1506
|
-
if (!params.includeDotenv) {
|
|
1507
|
-
files = files.filter((p) => !path9.posix.basename(p).startsWith(".env"));
|
|
1508
|
-
}
|
|
1509
|
-
} else {
|
|
1510
|
-
files = await listFilesFs({ root: importRoot, includeDotenv: params.includeDotenv });
|
|
1511
|
-
if (subdir) {
|
|
1512
|
-
files = files.map((p) => path9.posix.join(subdir, p));
|
|
1513
|
-
}
|
|
1755
|
+
async function selectOrganization(orgs, yes) {
|
|
1756
|
+
if (orgs.length === 0) {
|
|
1757
|
+
throw new CliError("No organizations found for your account.", { exitCode: 1 });
|
|
1514
1758
|
}
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1759
|
+
if (orgs.length === 1) return orgs[0];
|
|
1760
|
+
if (yes || !isInteractive()) {
|
|
1761
|
+
throw new CliError("Multiple organizations found. Re-run without --yes to select one.", { exitCode: 2 });
|
|
1517
1762
|
}
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1763
|
+
const res = await prompts(
|
|
1764
|
+
[
|
|
1765
|
+
{
|
|
1766
|
+
type: "select",
|
|
1767
|
+
name: "orgId",
|
|
1768
|
+
message: "Select an organization",
|
|
1769
|
+
choices: orgs.map((org) => ({
|
|
1770
|
+
title: org.name ? `${org.name} (${org.id})` : org.id,
|
|
1771
|
+
value: org.id
|
|
1772
|
+
}))
|
|
1773
|
+
}
|
|
1774
|
+
],
|
|
1775
|
+
{
|
|
1776
|
+
onCancel: () => {
|
|
1777
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1526
1778
|
}
|
|
1527
1779
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
name: "import.zip",
|
|
1538
|
-
mimeType: "application/zip",
|
|
1539
|
-
size: zipSize,
|
|
1540
|
-
checksumSha256: zipSha
|
|
1780
|
+
);
|
|
1781
|
+
const selected = orgs.find((org) => org.id === res.orgId);
|
|
1782
|
+
if (!selected) throw new CliError("Invalid organization selection.", { exitCode: 1 });
|
|
1783
|
+
return selected;
|
|
1784
|
+
}
|
|
1785
|
+
async function selectClientApp(apps, yes) {
|
|
1786
|
+
if (apps.length === 0) {
|
|
1787
|
+
if (yes || !isInteractive()) {
|
|
1788
|
+
return { app: null };
|
|
1541
1789
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
lastPct = pct;
|
|
1561
|
-
process.stdout.write(pc5.dim(`Upload ${pct}%\r`));
|
|
1790
|
+
console.log(
|
|
1791
|
+
pc7.dim(
|
|
1792
|
+
"No apps found. Create one here or in the Comerge dashboard at dashboard.comerge.ai."
|
|
1793
|
+
)
|
|
1794
|
+
);
|
|
1795
|
+
const res2 = await prompts(
|
|
1796
|
+
[
|
|
1797
|
+
{
|
|
1798
|
+
type: "select",
|
|
1799
|
+
name: "appId",
|
|
1800
|
+
message: "Select an app or create a new one:",
|
|
1801
|
+
choices: [{ title: "Create a new app", value: "__create_new__" }]
|
|
1802
|
+
}
|
|
1803
|
+
],
|
|
1804
|
+
{
|
|
1805
|
+
onCancel: () => {
|
|
1806
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1807
|
+
}
|
|
1562
1808
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
log(pc5.dim("Triggering import..."));
|
|
1567
|
-
const importResp = await api.importFromUpload({
|
|
1568
|
-
uploadId,
|
|
1569
|
-
appName,
|
|
1570
|
-
path: subdir ?? void 0
|
|
1571
|
-
});
|
|
1572
|
-
const importObj = importResp?.responseObject;
|
|
1573
|
-
const appId = String(importObj?.appId ?? "");
|
|
1574
|
-
const projectId = importObj?.projectId ? String(importObj.projectId) : void 0;
|
|
1575
|
-
const threadId = importObj?.threadId ? String(importObj.threadId) : void 0;
|
|
1576
|
-
if (!appId) {
|
|
1577
|
-
throw new CliError("Import response missing appId", { exitCode: 1, hint: JSON.stringify(importResp, null, 2) });
|
|
1809
|
+
);
|
|
1810
|
+
if (res2.appId === "__create_new__") return { app: null };
|
|
1811
|
+
throw new CliError("Invalid app selection.", { exitCode: 1 });
|
|
1578
1812
|
}
|
|
1579
|
-
if (
|
|
1580
|
-
|
|
1813
|
+
if (yes || !isInteractive()) {
|
|
1814
|
+
throw new CliError("Multiple apps found. Re-run without --yes to select one.", { exitCode: 2 });
|
|
1581
1815
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1816
|
+
const res = await prompts(
|
|
1817
|
+
[
|
|
1818
|
+
{
|
|
1819
|
+
type: "select",
|
|
1820
|
+
name: "appId",
|
|
1821
|
+
message: "Select an app or create a new one:",
|
|
1822
|
+
choices: [
|
|
1823
|
+
...apps.map((app) => ({
|
|
1824
|
+
title: app.name ? `${app.name} (${app.id})` : app.id,
|
|
1825
|
+
value: app.id
|
|
1826
|
+
})),
|
|
1827
|
+
{ title: "Create a new app", value: "__create_new__" }
|
|
1828
|
+
]
|
|
1829
|
+
}
|
|
1830
|
+
],
|
|
1831
|
+
{
|
|
1832
|
+
onCancel: () => {
|
|
1833
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1834
|
+
}
|
|
1598
1835
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1836
|
+
);
|
|
1837
|
+
if (res.appId === "__create_new__") return { app: null };
|
|
1838
|
+
const selected = apps.find((app) => app.id === res.appId);
|
|
1839
|
+
if (!selected) throw new CliError("Invalid app selection.", { exitCode: 1 });
|
|
1840
|
+
return { app: selected };
|
|
1841
|
+
}
|
|
1842
|
+
async function promptNewAppDetails(projectRoot, yes) {
|
|
1843
|
+
const defaults = await inferExpoAppDefaults(projectRoot);
|
|
1844
|
+
const defaultName = defaults.name || "My App";
|
|
1845
|
+
const iosEnabled = true;
|
|
1846
|
+
const androidEnabled = true;
|
|
1847
|
+
if (yes || !isInteractive()) {
|
|
1848
|
+
return {
|
|
1849
|
+
name: defaultName,
|
|
1850
|
+
iosEnabled,
|
|
1851
|
+
androidEnabled,
|
|
1852
|
+
iosBundleId: defaults.iosBundleId || defaults.suggestedBundleId || "com.example.app",
|
|
1853
|
+
androidPackageName: defaults.androidPackageName || defaults.suggestedBundleId || "com.example.app"
|
|
1854
|
+
};
|
|
1601
1855
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
const res = await importLocal({
|
|
1617
|
-
apiKey: opts.apiKey ? String(opts.apiKey) : null,
|
|
1618
|
-
yes: Boolean(opts.yes),
|
|
1619
|
-
appName: opts.name ? String(opts.name) : null,
|
|
1620
|
-
subdir: opts.path ? String(opts.path) : null,
|
|
1621
|
-
maxWaitMs: Math.floor(maxWaitSec * 1e3),
|
|
1622
|
-
noWait: Boolean(opts.noWait),
|
|
1623
|
-
json: Boolean(opts.json),
|
|
1624
|
-
includeDotenv: Boolean(opts.includeDotenv),
|
|
1625
|
-
dryRun: Boolean(opts.dryRun)
|
|
1626
|
-
});
|
|
1627
|
-
if (Boolean(opts.json)) {
|
|
1628
|
-
console.log(JSON.stringify(res, null, 2));
|
|
1629
|
-
return;
|
|
1856
|
+
const answers = await prompts(
|
|
1857
|
+
[
|
|
1858
|
+
{
|
|
1859
|
+
type: "text",
|
|
1860
|
+
name: "name",
|
|
1861
|
+
message: "App name",
|
|
1862
|
+
initial: defaultName,
|
|
1863
|
+
validate: (v) => String(v || "").trim() ? true : "App name is required."
|
|
1864
|
+
}
|
|
1865
|
+
],
|
|
1866
|
+
{
|
|
1867
|
+
onCancel: () => {
|
|
1868
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1869
|
+
}
|
|
1630
1870
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1871
|
+
);
|
|
1872
|
+
const bundlePrompts = [
|
|
1873
|
+
iosEnabled ? {
|
|
1874
|
+
type: "text",
|
|
1875
|
+
name: "iosBundleId",
|
|
1876
|
+
message: "iOS bundle ID",
|
|
1877
|
+
initial: defaults.iosBundleId || defaults.suggestedBundleId || "com.example.app",
|
|
1878
|
+
validate: (v) => String(v || "").trim() ? true : "iOS bundle ID is required."
|
|
1879
|
+
} : null,
|
|
1880
|
+
androidEnabled ? {
|
|
1881
|
+
type: "text",
|
|
1882
|
+
name: "androidPackageName",
|
|
1883
|
+
message: "Android package name",
|
|
1884
|
+
initial: defaults.androidPackageName || defaults.suggestedBundleId || "com.example.app",
|
|
1885
|
+
validate: (v) => String(v || "").trim() ? true : "Android package name is required."
|
|
1886
|
+
} : null
|
|
1887
|
+
].filter(Boolean);
|
|
1888
|
+
const bundleAnswers = await prompts(bundlePrompts, {
|
|
1889
|
+
onCancel: () => {
|
|
1890
|
+
throw new CliError("Cancelled by user.", { exitCode: 130 });
|
|
1634
1891
|
}
|
|
1635
1892
|
});
|
|
1893
|
+
return {
|
|
1894
|
+
name: String(answers.name || defaultName).trim(),
|
|
1895
|
+
iosEnabled,
|
|
1896
|
+
androidEnabled,
|
|
1897
|
+
iosBundleId: iosEnabled ? String(bundleAnswers.iosBundleId || "").trim() : null,
|
|
1898
|
+
androidPackageName: androidEnabled ? String(bundleAnswers.androidPackageName || "").trim() : null
|
|
1899
|
+
};
|
|
1636
1900
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
// src/lib/ensureAuth.ts
|
|
1645
|
-
import pc7 from "picocolors";
|
|
1646
|
-
function isInteractive2() {
|
|
1647
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1901
|
+
function buildKeyName(params) {
|
|
1902
|
+
const rawUser = params.email && params.email.includes("@") ? params.email.split("@")[0] : params.id || "user";
|
|
1903
|
+
const user = rawUser.trim().replace(/\s+/g, "-");
|
|
1904
|
+
const hostname = os5.hostname().trim().replace(/\s+/g, "-");
|
|
1905
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
1906
|
+
return `cli_${user}_${hostname}_${ts}`;
|
|
1648
1907
|
}
|
|
1649
|
-
async function
|
|
1650
|
-
|
|
1908
|
+
async function initLocal(params) {
|
|
1909
|
+
await ensureAuth({ yes: params.yes, json: params.json });
|
|
1910
|
+
const cfg = await resolveConfig({ yes: params.yes });
|
|
1651
1911
|
const api = createApiClient(cfg);
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
if (typeof envToken === "string" && envToken.trim().length > 0) return;
|
|
1657
|
-
const existing = await getSession();
|
|
1658
|
-
if (existing) {
|
|
1659
|
-
try {
|
|
1660
|
-
await validateBackendSession({ apiKey: params?.apiKey ?? null, yes: params?.yes });
|
|
1661
|
-
return;
|
|
1662
|
-
} catch {
|
|
1663
|
-
}
|
|
1912
|
+
const appKey = String(params.appKey || "MicroMain").trim() || "MicroMain";
|
|
1913
|
+
try {
|
|
1914
|
+
await api.autoEnableDeveloper();
|
|
1915
|
+
} catch {
|
|
1664
1916
|
}
|
|
1665
|
-
const
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1917
|
+
const orgs = unwrapResponseObject(await api.listOrganizations(), "organizations");
|
|
1918
|
+
const selectedOrg = await selectOrganization(orgs, params.yes);
|
|
1919
|
+
const apps = unwrapResponseObject(
|
|
1920
|
+
await api.listClientApps({ orgId: selectedOrg.id }),
|
|
1921
|
+
"client apps"
|
|
1922
|
+
);
|
|
1923
|
+
const selectedAppResult = await selectClientApp(apps, params.yes);
|
|
1924
|
+
let clientApp = selectedAppResult.app;
|
|
1925
|
+
let importAppName = clientApp?.name ?? null;
|
|
1926
|
+
if (!clientApp) {
|
|
1927
|
+
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
1928
|
+
const details = await promptNewAppDetails(projectRoot, params.yes);
|
|
1929
|
+
const createdResp = await api.createClientApp({
|
|
1930
|
+
orgId: selectedOrg.id,
|
|
1931
|
+
name: details.name,
|
|
1932
|
+
type: "wrapper",
|
|
1933
|
+
environment: "production",
|
|
1934
|
+
platform: "expo",
|
|
1935
|
+
iosBundleId: details.iosEnabled ? details.iosBundleId ?? void 0 : void 0,
|
|
1936
|
+
androidPackageName: details.androidEnabled ? details.androidPackageName ?? void 0 : void 0
|
|
1671
1937
|
});
|
|
1938
|
+
clientApp = unwrapResponseObject(createdResp, "client app");
|
|
1939
|
+
importAppName = details.name;
|
|
1672
1940
|
}
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const supabase = createSupabaseAuthHelpers(cfg);
|
|
1677
|
-
const { url } = await supabase.startGoogleLogin({ redirectTo: server.redirectTo });
|
|
1678
|
-
try {
|
|
1679
|
-
await openBrowser(url);
|
|
1680
|
-
if (!params?.json) console.log(pc7.dim("Opened browser for login..."));
|
|
1681
|
-
} catch {
|
|
1682
|
-
if (!params?.json) {
|
|
1683
|
-
console.log(pc7.yellow("Could not open the browser automatically."));
|
|
1684
|
-
console.log(`Open this URL manually:
|
|
1685
|
-
${url}`);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
const code = await server.waitForCode();
|
|
1689
|
-
const session = await supabase.exchangeCode({ code });
|
|
1690
|
-
await setSession(session);
|
|
1691
|
-
try {
|
|
1692
|
-
await validateBackendSession({ apiKey: params?.apiKey ?? null, yes: params?.yes });
|
|
1693
|
-
} catch (err) {
|
|
1694
|
-
await clearSession().catch(() => {
|
|
1695
|
-
});
|
|
1696
|
-
throw err;
|
|
1697
|
-
}
|
|
1698
|
-
} finally {
|
|
1699
|
-
await server.close();
|
|
1941
|
+
const clientAppId = String(clientApp.id || "").trim();
|
|
1942
|
+
if (!clientAppId) {
|
|
1943
|
+
throw new CliError("Missing client app ID.", { exitCode: 1 });
|
|
1700
1944
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
noWait: true,
|
|
1713
|
-
json: params.json,
|
|
1714
|
-
includeDotenv: params.includeDotenv,
|
|
1715
|
-
dryRun: true
|
|
1716
|
-
});
|
|
1717
|
-
return {
|
|
1718
|
-
appId: res.appId,
|
|
1719
|
-
uploadId: res.uploadId,
|
|
1720
|
-
projectId: res.projectId,
|
|
1721
|
-
threadId: res.threadId,
|
|
1722
|
-
status: res.status,
|
|
1723
|
-
shellOutDir: params.outDir,
|
|
1724
|
-
noWait: params.noWait
|
|
1725
|
-
};
|
|
1945
|
+
let apiKey = await getStoredApiKey(selectedOrg.id, clientAppId);
|
|
1946
|
+
if (!apiKey) {
|
|
1947
|
+
const me = unwrapResponseObject(await api.getMe(), "user");
|
|
1948
|
+
const keyName = buildKeyName({ email: me.email ?? null, id: me.id ?? null });
|
|
1949
|
+
const createdKeyResp = await api.createClientAppKey(clientAppId, { name: keyName });
|
|
1950
|
+
const keyPayload = unwrapResponseObject(createdKeyResp, "client key");
|
|
1951
|
+
apiKey = String(keyPayload.key || "").trim();
|
|
1952
|
+
if (!apiKey) {
|
|
1953
|
+
throw new CliError("Server did not return a client key.", { exitCode: 1 });
|
|
1954
|
+
}
|
|
1955
|
+
await setStoredApiKey(selectedOrg.id, clientAppId, apiKey);
|
|
1726
1956
|
}
|
|
1727
|
-
|
|
1728
|
-
await ensureAuth({ apiKey, yes: params.yes, json: params.json });
|
|
1729
|
-
const appKey = String(params.appKey || "MicroMain").trim() || "MicroMain";
|
|
1730
|
-
if (!params.json) console.log(pc8.dim("Importing project..."));
|
|
1957
|
+
if (!params.json) console.log(pc7.dim("Importing project..."));
|
|
1731
1958
|
const imported = await importLocal({
|
|
1732
1959
|
apiKey,
|
|
1733
1960
|
yes: params.yes,
|
|
1734
|
-
appName:
|
|
1961
|
+
appName: importAppName,
|
|
1735
1962
|
subdir: params.subdir,
|
|
1736
|
-
maxWaitMs: params.maxWaitMs,
|
|
1737
|
-
noWait: params.noWait,
|
|
1738
1963
|
json: params.json,
|
|
1739
1964
|
includeDotenv: params.includeDotenv,
|
|
1740
1965
|
dryRun: false
|
|
1741
1966
|
});
|
|
1742
|
-
if (!params.json) console.log(
|
|
1967
|
+
if (!params.json) console.log(pc7.dim("Generating shell app..."));
|
|
1743
1968
|
await shellInit({
|
|
1744
1969
|
outDir: params.outDir,
|
|
1745
1970
|
appId: imported.appId,
|
|
1746
1971
|
apiKey,
|
|
1747
1972
|
appKey,
|
|
1748
|
-
|
|
1749
|
-
install: params.install,
|
|
1750
|
-
packageManager: params.packageManager,
|
|
1751
|
-
studioVersion: params.studioVersion
|
|
1973
|
+
packageManager: params.packageManager
|
|
1752
1974
|
});
|
|
1753
1975
|
return {
|
|
1754
1976
|
appId: imported.appId,
|
|
@@ -1756,32 +1978,20 @@ async function initLocal(params) {
|
|
|
1756
1978
|
projectId: imported.projectId,
|
|
1757
1979
|
threadId: imported.threadId,
|
|
1758
1980
|
status: imported.status,
|
|
1759
|
-
shellOutDir: params.outDir
|
|
1760
|
-
noWait: params.noWait
|
|
1981
|
+
shellOutDir: params.outDir
|
|
1761
1982
|
};
|
|
1762
1983
|
}
|
|
1763
1984
|
|
|
1764
1985
|
// src/commands/init.ts
|
|
1765
1986
|
function registerInitCommands(program) {
|
|
1766
|
-
const init = program.command("init").description("Import
|
|
1767
|
-
init.
|
|
1768
|
-
const maxWaitSec = Number(String(opts.maxWait ?? "1200").trim());
|
|
1769
|
-
if (!Number.isFinite(maxWaitSec) || maxWaitSec <= 0) {
|
|
1770
|
-
throw new CliError("Invalid --max-wait", { exitCode: 2, hint: "Example: --max-wait 1200" });
|
|
1771
|
-
}
|
|
1987
|
+
const init = program.command("init").description("Import into Comerge and generate the shell wrapper");
|
|
1988
|
+
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", "comerge-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) => {
|
|
1772
1989
|
const res = await initLocal({
|
|
1773
|
-
|
|
1774
|
-
appKey: opts.appKey ? String(opts.appKey) : "MicroMain",
|
|
1990
|
+
appKey: "MicroMain",
|
|
1775
1991
|
outDir: opts.out ? String(opts.out) : "comerge-shell",
|
|
1776
|
-
install: Boolean(opts.install),
|
|
1777
1992
|
packageManager: opts.packageManager ? String(opts.packageManager) : null,
|
|
1778
|
-
studioVersion: opts.studioVersion ? String(opts.studioVersion) : "latest",
|
|
1779
|
-
appName: opts.name ? String(opts.name) : null,
|
|
1780
1993
|
subdir: opts.path ? String(opts.path) : null,
|
|
1781
|
-
maxWaitMs: Math.floor(maxWaitSec * 1e3),
|
|
1782
|
-
noWait: Boolean(opts.noWait),
|
|
1783
1994
|
includeDotenv: Boolean(opts.includeDotenv),
|
|
1784
|
-
dryRun: Boolean(opts.dryRun),
|
|
1785
1995
|
yes: Boolean(opts.yes),
|
|
1786
1996
|
json: Boolean(opts.json)
|
|
1787
1997
|
});
|
|
@@ -1789,9 +1999,8 @@ function registerInitCommands(program) {
|
|
|
1789
1999
|
console.log(JSON.stringify(res, null, 2));
|
|
1790
2000
|
return;
|
|
1791
2001
|
}
|
|
1792
|
-
console.log(
|
|
1793
|
-
if (res.status) console.log(
|
|
1794
|
-
if (res.noWait) console.log(pc9.yellow("Note: --no-wait was used; bundles may still be building."));
|
|
2002
|
+
console.log(pc8.green(`Completed. appId=${res.appId}`));
|
|
2003
|
+
if (res.status) console.log(pc8.dim(`Status: ${res.status}`));
|
|
1795
2004
|
});
|
|
1796
2005
|
}
|
|
1797
2006
|
|
|
@@ -1801,26 +2010,24 @@ var { version } = require2("../package.json");
|
|
|
1801
2010
|
async function main(argv) {
|
|
1802
2011
|
const program = new Command();
|
|
1803
2012
|
program.name("comerge").description("Comerge CLI").version(version);
|
|
1804
|
-
registerShellCommands(program);
|
|
1805
2013
|
registerLoginCommand(program);
|
|
1806
2014
|
registerWhoamiCommand(program);
|
|
1807
2015
|
registerLogoutCommand(program);
|
|
1808
|
-
registerImportCommands(program);
|
|
1809
2016
|
registerInitCommands(program);
|
|
1810
2017
|
program.configureOutput({
|
|
1811
|
-
outputError: (str, write) => write(
|
|
2018
|
+
outputError: (str, write) => write(pc9.red(str))
|
|
1812
2019
|
});
|
|
1813
2020
|
try {
|
|
1814
2021
|
await program.parseAsync(argv);
|
|
1815
2022
|
} catch (err) {
|
|
1816
2023
|
const e = err;
|
|
1817
2024
|
if (e instanceof CliError) {
|
|
1818
|
-
console.error(
|
|
1819
|
-
if (e.hint) console.error(
|
|
2025
|
+
console.error(pc9.red(`Error: ${e.message}`));
|
|
2026
|
+
if (e.hint) console.error(pc9.dim(e.hint));
|
|
1820
2027
|
process.exitCode = e.exitCode;
|
|
1821
2028
|
return;
|
|
1822
2029
|
}
|
|
1823
|
-
console.error(
|
|
2030
|
+
console.error(pc9.red(`Unexpected error: ${e?.message ?? String(e)}`));
|
|
1824
2031
|
process.exitCode = 1;
|
|
1825
2032
|
}
|
|
1826
2033
|
}
|