liftbay 0.5.1 → 0.7.0
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/index.js +905 -547
- package/package.json +2 -2
- package/skills/liftbay/SKILL.md +39 -17
package/dist/index.js
CHANGED
|
@@ -26526,6 +26526,26 @@ function emit(result) {
|
|
|
26526
26526
|
var cliName = () => process.env.LIFTBAY_CLI_NAME ?? (/[\\/]_npx[\\/]/.test(process.argv[1] ?? "") || process.env.npm_command === "exec" ? "npx liftbay" : "liftbay");
|
|
26527
26527
|
var cmd = (command) => command.replace(/^liftbay\b/, cliName());
|
|
26528
26528
|
var withCliName = (text2) => cliName() === "liftbay" ? text2 : text2.replace(/`liftbay /g, `\`${cliName()} `);
|
|
26529
|
+
function isTransient(e) {
|
|
26530
|
+
if (e instanceof LiftbayApiError) return e.status >= 500 || e.status === 429;
|
|
26531
|
+
const err = e;
|
|
26532
|
+
const code = err?.cause?.code ?? err?.code ?? "";
|
|
26533
|
+
return err?.name === "TypeError" && err.message === "fetch failed" || /^(ECONN|EADDR|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|ENETUNREACH|EHOSTUNREACH|UND_ERR)/.test(code);
|
|
26534
|
+
}
|
|
26535
|
+
async function poll(fn, onRetry) {
|
|
26536
|
+
const until = Date.now() + 5 * 6e4;
|
|
26537
|
+
for (let attempt = 1; ; attempt++) {
|
|
26538
|
+
try {
|
|
26539
|
+
return await fn();
|
|
26540
|
+
} catch (e) {
|
|
26541
|
+
if (!isTransient(e) || Date.now() > until) throw e;
|
|
26542
|
+
if (attempt === 1) process.stderr.write(`${import_picocolors.default.yellow("!")} Connection to Liftbay lost; retrying\u2026
|
|
26543
|
+
`);
|
|
26544
|
+
onRetry?.(attempt);
|
|
26545
|
+
await new Promise((r2) => setTimeout(r2, Math.min(15e3, 1e3 * 2 ** Math.min(attempt, 4))));
|
|
26546
|
+
}
|
|
26547
|
+
}
|
|
26548
|
+
}
|
|
26529
26549
|
function reportError(e) {
|
|
26530
26550
|
if (json) {
|
|
26531
26551
|
const error = e instanceof LiftbayApiError ? { code: e.code, message: e.message, status: e.status, fields: e.fields } : e instanceof CliError ? { code: "cli_error", message: e.message } : { code: "internal", message: e instanceof Error ? e.message : String(e) };
|
|
@@ -26541,15 +26561,19 @@ function reportError(e) {
|
|
|
26541
26561
|
} else console.error(e);
|
|
26542
26562
|
process.exit(1);
|
|
26543
26563
|
}
|
|
26544
|
-
var
|
|
26564
|
+
var promptsOff = false;
|
|
26565
|
+
function disablePrompts() {
|
|
26566
|
+
promptsOff = true;
|
|
26567
|
+
}
|
|
26568
|
+
var isInteractive = () => !json && !promptsOff && !!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI;
|
|
26545
26569
|
function needInteractive(instead) {
|
|
26546
26570
|
if (!isInteractive()) fail(instead);
|
|
26547
26571
|
}
|
|
26548
26572
|
|
|
26549
26573
|
// src/commands/auth.ts
|
|
26550
|
-
import { spawn } from "child_process";
|
|
26551
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
26552
|
-
import { join as
|
|
26574
|
+
import { spawn as spawn2 } from "child_process";
|
|
26575
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
26576
|
+
import { join as join4 } from "path";
|
|
26553
26577
|
|
|
26554
26578
|
// node_modules/@clack/core/dist/index.mjs
|
|
26555
26579
|
import { styleText } from "util";
|
|
@@ -27886,29 +27910,279 @@ ${i2}
|
|
|
27886
27910
|
}).prompt();
|
|
27887
27911
|
|
|
27888
27912
|
// src/commands/auth.ts
|
|
27913
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
27914
|
+
|
|
27915
|
+
// src/commands/link.ts
|
|
27889
27916
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
27917
|
+
|
|
27918
|
+
// src/project.ts
|
|
27919
|
+
import { execFile, spawn } from "child_process";
|
|
27920
|
+
import { promisify } from "util";
|
|
27921
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
27922
|
+
import { join as join3 } from "path";
|
|
27923
|
+
var execFileAsync = promisify(execFile);
|
|
27924
|
+
var CONFIG_FILE = "liftbay.json";
|
|
27925
|
+
function readConfig(cwd = process.cwd()) {
|
|
27926
|
+
const path2 = join3(cwd, CONFIG_FILE);
|
|
27927
|
+
return existsSync3(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : null;
|
|
27928
|
+
}
|
|
27929
|
+
function requireConfig(cwd = process.cwd()) {
|
|
27930
|
+
return readConfig(cwd) ?? fail(`No ${CONFIG_FILE} here. Run \`liftbay init\` in your app's folder.`);
|
|
27931
|
+
}
|
|
27932
|
+
function writeConfig(config, cwd = process.cwd()) {
|
|
27933
|
+
writeFileSync2(join3(cwd, CONFIG_FILE), `${JSON.stringify({ $schema: "https://liftbay.dev/schema/liftbay.json", ...config }, null, 2)}
|
|
27934
|
+
`);
|
|
27935
|
+
}
|
|
27936
|
+
function readJson(path2) {
|
|
27937
|
+
return existsSync3(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : null;
|
|
27938
|
+
}
|
|
27939
|
+
function packageManager(cwd = process.cwd()) {
|
|
27940
|
+
if (existsSync3(join3(cwd, "bun.lock")) || existsSync3(join3(cwd, "bun.lockb"))) return "bun";
|
|
27941
|
+
if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
27942
|
+
if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
|
|
27943
|
+
return "npm";
|
|
27944
|
+
}
|
|
27945
|
+
async function output(cmd2, args, cwd = process.cwd()) {
|
|
27946
|
+
const { stdout: stdout2 } = await execFileAsync(cmd2, args, { cwd, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
27947
|
+
return stdout2;
|
|
27948
|
+
}
|
|
27949
|
+
async function expoConfig(cwd = process.cwd()) {
|
|
27950
|
+
return JSON.parse(await output("npx", ["expo", "config", "--json", "--type", "public"], cwd));
|
|
27951
|
+
}
|
|
27952
|
+
async function detectApp(cwd = process.cwd()) {
|
|
27953
|
+
const pkg = readJson(join3(cwd, "package.json"));
|
|
27954
|
+
if (!pkg) fail("No package.json here. Run this in your React Native app's folder.");
|
|
27955
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
27956
|
+
if (!deps["react-native"]) fail("This doesn't look like a React Native app (react-native is not a dependency).");
|
|
27957
|
+
const hasExpo = !!deps.expo;
|
|
27958
|
+
let name = pkg.name ?? "app";
|
|
27959
|
+
let slug = name;
|
|
27960
|
+
let version = pkg.version ?? null;
|
|
27961
|
+
let bundleId = null;
|
|
27962
|
+
let androidPackage = null;
|
|
27963
|
+
if (hasExpo) {
|
|
27964
|
+
const cfg = await expoConfig(cwd);
|
|
27965
|
+
name = cfg.name ?? name;
|
|
27966
|
+
slug = cfg.slug ?? slug;
|
|
27967
|
+
version = cfg.version ?? version;
|
|
27968
|
+
bundleId = cfg.ios?.bundleIdentifier ?? null;
|
|
27969
|
+
androidPackage = cfg.android?.package ?? null;
|
|
27970
|
+
} else {
|
|
27971
|
+
const gradle = join3(cwd, "android/app/build.gradle");
|
|
27972
|
+
if (existsSync3(gradle)) androidPackage = readFileSync2(gradle, "utf8").match(/applicationId\s+["']([^"']+)["']/)?.[1] ?? null;
|
|
27973
|
+
}
|
|
27974
|
+
return {
|
|
27975
|
+
framework: hasExpo ? "expo" : "react-native",
|
|
27976
|
+
name,
|
|
27977
|
+
slug: String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "app",
|
|
27978
|
+
version,
|
|
27979
|
+
bundleId,
|
|
27980
|
+
androidPackage,
|
|
27981
|
+
hasExpoModules: hasExpo,
|
|
27982
|
+
hasExpoUpdates: !!deps["expo-updates"],
|
|
27983
|
+
packageManager: packageManager(cwd),
|
|
27984
|
+
appJson: existsSync3(join3(cwd, "app.json"))
|
|
27985
|
+
};
|
|
27986
|
+
}
|
|
27987
|
+
async function run(cmd2, args, cwd = process.cwd(), env2 = {}) {
|
|
27988
|
+
const code = await new Promise((resolve4, reject) => {
|
|
27989
|
+
const child = spawn(cmd2, args, { cwd, stdio: "inherit", env: { ...process.env, ...env2 } });
|
|
27990
|
+
child.on("error", reject);
|
|
27991
|
+
child.on("close", resolve4);
|
|
27992
|
+
});
|
|
27993
|
+
if (code !== 0) fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
27994
|
+
}
|
|
27995
|
+
async function runQuiet(label, cmd2, args, cwd = process.cwd()) {
|
|
27996
|
+
const spin = spinner();
|
|
27997
|
+
spin.start(label);
|
|
27998
|
+
const lines = [];
|
|
27999
|
+
const code = await new Promise((resolve4, reject) => {
|
|
28000
|
+
const child = spawn(cmd2, args, {
|
|
28001
|
+
cwd,
|
|
28002
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28003
|
+
env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false" }
|
|
28004
|
+
});
|
|
28005
|
+
const keep = (chunk) => lines.push(...chunk.toString().split(/\r?\n/).filter(Boolean));
|
|
28006
|
+
child.stdout.on("data", keep);
|
|
28007
|
+
child.stderr.on("data", keep);
|
|
28008
|
+
child.on("error", reject);
|
|
28009
|
+
child.on("close", resolve4);
|
|
28010
|
+
});
|
|
28011
|
+
if (code === 0) return spin.stop(label.replace(/^Installing/, "Installed").replace(/^Setting up/, "Set up"));
|
|
28012
|
+
spin.error(`${label} failed`);
|
|
28013
|
+
console.error(lines.slice(-40).join("\n"));
|
|
28014
|
+
fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28015
|
+
}
|
|
28016
|
+
function readAppJson(cwd = process.cwd()) {
|
|
28017
|
+
return readJson(join3(cwd, "app.json"));
|
|
28018
|
+
}
|
|
28019
|
+
function writeAppJson(data, cwd = process.cwd()) {
|
|
28020
|
+
writeFileSync2(join3(cwd, "app.json"), `${JSON.stringify(data, null, 2)}
|
|
28021
|
+
`);
|
|
28022
|
+
}
|
|
28023
|
+
|
|
28024
|
+
// src/commands/link.ts
|
|
28025
|
+
function cancelled(value) {
|
|
28026
|
+
if (isCancel(value)) {
|
|
28027
|
+
cancel("Cancelled");
|
|
28028
|
+
process.exit(0);
|
|
28029
|
+
}
|
|
28030
|
+
return value;
|
|
28031
|
+
}
|
|
28032
|
+
var slugifyProject = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
28033
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
28034
|
+
async function createFirstOrg(lb, given) {
|
|
28035
|
+
const me = await unwrap(lb.GET("/v1/auth/me"));
|
|
28036
|
+
const suggested = given ?? (me.user.name?.trim() || me.user.email.split("@")[0]);
|
|
28037
|
+
const name = isInteractive() && !given ? cancelled(await text({ message: "Name your organization", initialValue: suggested, validate: (v) => slugify(v ?? "").length >= 2 ? void 0 : "At least 2 letters or digits" })) : suggested;
|
|
28038
|
+
const base = slugify(name).padEnd(2, "0");
|
|
28039
|
+
for (const slug of [base, `${base.slice(0, 35)}-${Math.random().toString(36).slice(2, 6)}`]) {
|
|
28040
|
+
const res = await lb.POST("/v1/orgs", { body: { name: name.slice(0, 64), slug } });
|
|
28041
|
+
if (res.data) {
|
|
28042
|
+
log.success(`Created organization ${import_picocolors2.default.bold(res.data.name)} ${import_picocolors2.default.dim(`(liftbay.dev/${res.data.slug})`)}`);
|
|
28043
|
+
return res.data;
|
|
28044
|
+
}
|
|
28045
|
+
if (res.response.status !== 409) await unwrap(Promise.resolve(res));
|
|
28046
|
+
}
|
|
28047
|
+
return fail("Couldn't pick a free organization URL. Pass --org <slug>.");
|
|
28048
|
+
}
|
|
28049
|
+
async function ensureLinked(app, opts) {
|
|
28050
|
+
const existing = readConfig();
|
|
28051
|
+
if (existing) {
|
|
28052
|
+
log.success(`Linked to ${import_picocolors2.default.bold(`${existing.org}/${existing.project}`)}`);
|
|
28053
|
+
return existing;
|
|
28054
|
+
}
|
|
28055
|
+
const lb = client();
|
|
28056
|
+
const orgs = await unwrap(lb.GET("/v1/orgs"));
|
|
28057
|
+
let org;
|
|
28058
|
+
if (opts.newOrg || !orgs.length) {
|
|
28059
|
+
const created = await createFirstOrg(lb, opts.newOrg ?? opts.org);
|
|
28060
|
+
orgs.push(created);
|
|
28061
|
+
org = created.slug;
|
|
28062
|
+
}
|
|
28063
|
+
org ??= opts.org ?? (orgs.length === 1 ? orgs[0].slug : void 0);
|
|
28064
|
+
if (opts.org && !orgs.some((o) => o.slug === org)) fail(`You're not in an organization "${opts.org}" (${orgs.map((o) => o.slug).join(", ")}). To create one, pass --new-org "<name>".`);
|
|
28065
|
+
if (!org) {
|
|
28066
|
+
if (!isInteractive()) fail(`Several organizations available; pass --org (${orgs.map((o) => o.slug).join(", ")})`);
|
|
28067
|
+
org = cancelled(await select({ message: "Organization", options: orgs.map((o) => ({ value: o.slug, label: o.name, hint: o.slug })) }));
|
|
28068
|
+
}
|
|
28069
|
+
const projects = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org } } }));
|
|
28070
|
+
let projectName = opts.projectName;
|
|
28071
|
+
let slug = opts.project ?? (projectName ? slugifyProject(projectName) : void 0);
|
|
28072
|
+
if (!slug) {
|
|
28073
|
+
const match = projects.find((pr) => pr.slug === app.slug);
|
|
28074
|
+
if (opts.yes || !isInteractive()) slug = match?.slug ?? app.slug;
|
|
28075
|
+
else {
|
|
28076
|
+
const taken = projects.some((pr) => pr.slug === app.slug);
|
|
28077
|
+
const choice = cancelled(
|
|
28078
|
+
await select({
|
|
28079
|
+
message: "Liftbay project",
|
|
28080
|
+
options: [
|
|
28081
|
+
...taken ? [] : [{ value: "__new__", label: `Create "${app.name}"`, hint: `new project \xB7 liftbay.dev/${org}/${app.slug}` }],
|
|
28082
|
+
{ value: "__named__", label: "Create a project with another name" },
|
|
28083
|
+
...projects.map((pr) => ({ value: pr.slug, label: pr.name, hint: `existing \xB7 ${pr.slug}` }))
|
|
28084
|
+
],
|
|
28085
|
+
initialValue: match?.slug ?? (taken ? "__named__" : "__new__")
|
|
28086
|
+
})
|
|
28087
|
+
);
|
|
28088
|
+
if (choice === "__named__") {
|
|
28089
|
+
projectName = cancelled(
|
|
28090
|
+
await text({
|
|
28091
|
+
message: "Project name",
|
|
28092
|
+
initialValue: app.name,
|
|
28093
|
+
validate: (v) => {
|
|
28094
|
+
const s = slugifyProject(v ?? "");
|
|
28095
|
+
if (s.length < 2) return "At least 2 letters or digits";
|
|
28096
|
+
if (projects.some((pr) => pr.slug === s)) return `${s} already exists: pick it from the list instead`;
|
|
28097
|
+
}
|
|
28098
|
+
})
|
|
28099
|
+
);
|
|
28100
|
+
slug = slugifyProject(projectName);
|
|
28101
|
+
} else slug = choice === "__new__" ? app.slug : choice;
|
|
28102
|
+
}
|
|
28103
|
+
}
|
|
28104
|
+
let project2 = projects.find((pr) => pr.slug === slug);
|
|
28105
|
+
if (!project2) {
|
|
28106
|
+
project2 = await unwrap(
|
|
28107
|
+
lb.POST("/v1/orgs/{org}/projects", {
|
|
28108
|
+
params: { path: { org } },
|
|
28109
|
+
body: { name: projectName ?? app.name, slug, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }
|
|
28110
|
+
})
|
|
28111
|
+
);
|
|
28112
|
+
log.success(`Created project ${import_picocolors2.default.bold(`${org}/${project2.slug}`)} with development, preview and production environments`);
|
|
28113
|
+
} else {
|
|
28114
|
+
log.success(`Linked to existing project ${import_picocolors2.default.bold(`${org}/${project2.slug}`)}`);
|
|
28115
|
+
}
|
|
28116
|
+
const config = { org, project: project2.slug, projectId: project2.id };
|
|
28117
|
+
writeConfig(config);
|
|
28118
|
+
log.info(`Wrote ${import_picocolors2.default.cyan(cmd("liftbay.json"))} ${import_picocolors2.default.dim("(commit it: it links this repository to the project)")}`);
|
|
28119
|
+
return config;
|
|
28120
|
+
}
|
|
28121
|
+
|
|
28122
|
+
// src/commands/auth.ts
|
|
27890
28123
|
var sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
27891
28124
|
function openBrowser(url) {
|
|
27892
28125
|
const cmd2 = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
27893
28126
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
27894
28127
|
try {
|
|
27895
|
-
|
|
28128
|
+
spawn2(cmd2, args, { stdio: "ignore", detached: true }).unref();
|
|
27896
28129
|
} catch {
|
|
27897
28130
|
}
|
|
27898
28131
|
}
|
|
27899
|
-
var PENDING =
|
|
28132
|
+
var PENDING = join4(CONFIG_DIR, "pending-login.json");
|
|
27900
28133
|
function readPending(api) {
|
|
27901
28134
|
try {
|
|
27902
|
-
const pending = JSON.parse(
|
|
28135
|
+
const pending = JSON.parse(readFileSync3(PENDING, "utf8"));
|
|
27903
28136
|
return pending.apiUrl === api && Date.parse(pending.expiresAt) > Date.now() + 5e3 ? pending : null;
|
|
27904
28137
|
} catch {
|
|
27905
28138
|
return null;
|
|
27906
28139
|
}
|
|
27907
28140
|
}
|
|
27908
28141
|
var clearPending = () => rmSync2(PENDING, { force: true });
|
|
28142
|
+
async function loginWithEmail(opts, api, dashboard) {
|
|
28143
|
+
const anon = createLiftbayClient({ baseUrl: api });
|
|
28144
|
+
const email = opts.email.trim();
|
|
28145
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) fail(`"${email}" isn't an email address`);
|
|
28146
|
+
let code = opts.code?.replace(/\s/g, "");
|
|
28147
|
+
if (!code) {
|
|
28148
|
+
await unwrap(anon.POST("/v1/auth/email/start", { body: { email } }));
|
|
28149
|
+
if (!isInteractive()) {
|
|
28150
|
+
log.info(`Sent a 6-digit sign-in code to ${import_picocolors3.default.bold(email)}.`);
|
|
28151
|
+
return {
|
|
28152
|
+
status: "code_sent",
|
|
28153
|
+
email,
|
|
28154
|
+
next: `Ask the person for the 6-digit code from the email, then run: liftbay login --email ${email} --code <code>`
|
|
28155
|
+
};
|
|
28156
|
+
}
|
|
28157
|
+
log.info(`Sent a 6-digit code to ${import_picocolors3.default.bold(email)}. ${import_picocolors3.default.dim("New to Liftbay? Signing in creates your account.")}`);
|
|
28158
|
+
code = cancelled(await text({ message: "Code from the email", validate: (v) => /^\d{6}$/.test((v ?? "").replace(/\s/g, "")) ? void 0 : "Enter the 6-digit code" })).replace(/\s/g, "");
|
|
28159
|
+
}
|
|
28160
|
+
const res = await unwrap(anon.POST("/v1/auth/email/token", { body: { email, code } }));
|
|
28161
|
+
clearPending();
|
|
28162
|
+
saveCredentials({ apiUrl: api, dashboardUrl: dashboard, token: res.token, email: res.user.email });
|
|
28163
|
+
log.success(`Signed in as ${import_picocolors3.default.bold(res.user.email)}`);
|
|
28164
|
+
return { status: "signed_in", user: res.user };
|
|
28165
|
+
}
|
|
27909
28166
|
async function login(opts) {
|
|
27910
28167
|
const api = opts.apiUrl ?? apiUrl();
|
|
27911
28168
|
const dashboard = opts.dashboardUrl ?? dashboardUrl();
|
|
28169
|
+
if (opts.code && !opts.email) fail("Pass --email with --code");
|
|
28170
|
+
if (opts.email) return loginWithEmail(opts, api, dashboard);
|
|
28171
|
+
if (isInteractive() && !readPending(api)) {
|
|
28172
|
+
const how = cancelled(
|
|
28173
|
+
await select({
|
|
28174
|
+
message: "How do you want to sign in?",
|
|
28175
|
+
options: [
|
|
28176
|
+
{ value: "browser", label: "In the browser", hint: "Google, GitHub or email" },
|
|
28177
|
+
{ value: "email", label: "With a code sent to my email", hint: "all in this terminal" }
|
|
28178
|
+
]
|
|
28179
|
+
})
|
|
28180
|
+
);
|
|
28181
|
+
if (how === "email") {
|
|
28182
|
+
const email = cancelled(await text({ message: "Email", validate: (v) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((v ?? "").trim()) ? void 0 : "Enter your email address" }));
|
|
28183
|
+
return loginWithEmail({ ...opts, email }, api, dashboard);
|
|
28184
|
+
}
|
|
28185
|
+
}
|
|
27912
28186
|
const anon = createLiftbayClient({ baseUrl: api });
|
|
27913
28187
|
let pending = readPending(api);
|
|
27914
28188
|
if (!pending) {
|
|
@@ -27923,15 +28197,15 @@ async function login(opts) {
|
|
|
27923
28197
|
interval: start.interval
|
|
27924
28198
|
};
|
|
27925
28199
|
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
27926
|
-
|
|
28200
|
+
writeFileSync3(PENDING, JSON.stringify(pending), { mode: 384 });
|
|
27927
28201
|
if (opts.open !== false) openBrowser(pending.url);
|
|
27928
28202
|
}
|
|
27929
|
-
note(`${
|
|
28203
|
+
note(`${import_picocolors3.default.bold(pending.userCode)}
|
|
27930
28204
|
|
|
27931
|
-
${
|
|
28205
|
+
${import_picocolors3.default.dim("Confirm this code in your browser:")}
|
|
27932
28206
|
${pending.url}`, "Sign in");
|
|
27933
28207
|
if (opts.wait === false) {
|
|
27934
|
-
log.info(`Approve it in the browser, then run ${
|
|
28208
|
+
log.info(`Approve it in the browser, then run ${import_picocolors3.default.cyan(cmd("liftbay login"))} again to finish.`);
|
|
27935
28209
|
return { status: "pending", userCode: pending.userCode, url: pending.url, expiresAt: pending.expiresAt, next: "Approve in the browser, then run `liftbay login` to finish signing in" };
|
|
27936
28210
|
}
|
|
27937
28211
|
const spin = spinner();
|
|
@@ -27941,10 +28215,10 @@ ${pending.url}`, "Sign in");
|
|
|
27941
28215
|
const res = await unwrap(anon.POST("/v1/auth/device/token", { body: { deviceCode: pending.deviceCode } }));
|
|
27942
28216
|
clearPending();
|
|
27943
28217
|
saveCredentials({ apiUrl: api, dashboardUrl: pending.dashboardUrl, token: res.token, email: res.user.email });
|
|
27944
|
-
spin.stop(`Signed in as ${
|
|
28218
|
+
spin.stop(`Signed in as ${import_picocolors3.default.bold(res.user.email)}`);
|
|
27945
28219
|
return { status: "signed_in", user: res.user };
|
|
27946
28220
|
} catch (e) {
|
|
27947
|
-
if (e instanceof LiftbayApiError && e.code === "authorization_pending") {
|
|
28221
|
+
if (e instanceof LiftbayApiError && e.code === "authorization_pending" || isTransient(e)) {
|
|
27948
28222
|
await sleep(pending.interval * 1e3);
|
|
27949
28223
|
continue;
|
|
27950
28224
|
}
|
|
@@ -27975,8 +28249,8 @@ async function whoami() {
|
|
|
27975
28249
|
return { kind: "org_token", orgs: orgs.map((o) => ({ slug: o.slug, name: o.name })) };
|
|
27976
28250
|
}
|
|
27977
28251
|
const me = await unwrap(client().GET("/v1/auth/me"));
|
|
27978
|
-
console.log(`${
|
|
27979
|
-
for (const m of me.memberships) console.log(` ${m.orgSlug.padEnd(24)} ${
|
|
28252
|
+
console.log(`${import_picocolors3.default.bold(me.user.email)}${me.user.name ? ` (${me.user.name})` : ""}`);
|
|
28253
|
+
for (const m of me.memberships) console.log(` ${m.orgSlug.padEnd(24)} ${import_picocolors3.default.dim(m.role)}`);
|
|
27980
28254
|
return { kind: "user", user: me.user, memberships: me.memberships };
|
|
27981
28255
|
}
|
|
27982
28256
|
async function ensureSignedIn(opts) {
|
|
@@ -27984,13 +28258,15 @@ async function ensureSignedIn(opts) {
|
|
|
27984
28258
|
try {
|
|
27985
28259
|
if (process.env.LIFTBAY_TOKEN) return;
|
|
27986
28260
|
const me = await unwrap(client().GET("/v1/auth/me"));
|
|
27987
|
-
log.success(`Signed in as ${
|
|
28261
|
+
log.success(`Signed in as ${import_picocolors3.default.bold(me.user.email)}`);
|
|
27988
28262
|
return;
|
|
27989
28263
|
} catch (e) {
|
|
27990
28264
|
if (!(e instanceof LiftbayApiError && e.status === 401)) throw e;
|
|
27991
28265
|
}
|
|
27992
28266
|
}
|
|
27993
|
-
if (!isInteractive())
|
|
28267
|
+
if (!isInteractive()) {
|
|
28268
|
+
fail("Not signed in. Sign in with an email code: `liftbay login --email <address>`, then `liftbay login --email <address> --code <code>` (or approve in the browser: `liftbay login --no-wait`, then `liftbay login`; or set LIFTBAY_TOKEN). Then run this again.");
|
|
28269
|
+
}
|
|
27994
28270
|
await login(opts);
|
|
27995
28271
|
}
|
|
27996
28272
|
|
|
@@ -27998,33 +28274,217 @@ async function ensureSignedIn(opts) {
|
|
|
27998
28274
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
27999
28275
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
28000
28276
|
import { execFileSync } from "child_process";
|
|
28277
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
28278
|
+
import { dirname as dirname3 } from "path";
|
|
28001
28279
|
|
|
28002
|
-
// src/
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28280
|
+
// src/generated/workflow.ts
|
|
28281
|
+
var WORKFLOW_YAML = `# Liftbay builds on your own GitHub Actions minutes.
|
|
28282
|
+
# Liftbay starts this workflow; it does nothing on your own pushes or pull requests. With this file on the
|
|
28283
|
+
# default branch, Liftbay uses workflow_dispatch. Otherwise (a branch you're trying out), Liftbay points a
|
|
28284
|
+
# temporary liftbay/build/<platform>/<build id> branch at your commit, which runs this file from that commit,
|
|
28285
|
+
# and deletes it once the build has started.
|
|
28286
|
+
# The build gets no repository secrets: the runner exchanges this run's GitHub OIDC token for a
|
|
28287
|
+
# one-time Liftbay job token, checked against this repository and this workflow file.
|
|
28288
|
+
# Docs: https://github.com/liftbay/runner
|
|
28289
|
+
name: Liftbay build
|
|
28290
|
+
run-name: Liftbay build \${{ inputs.build_id || github.ref_name }}
|
|
28006
28291
|
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28292
|
+
on:
|
|
28293
|
+
workflow_dispatch:
|
|
28294
|
+
inputs:
|
|
28295
|
+
build_id:
|
|
28296
|
+
description: Liftbay build id
|
|
28297
|
+
required: true
|
|
28298
|
+
type: string
|
|
28299
|
+
platform:
|
|
28300
|
+
description: Platform
|
|
28301
|
+
required: true
|
|
28302
|
+
type: choice
|
|
28303
|
+
options: [android, ios]
|
|
28304
|
+
api_url:
|
|
28305
|
+
description: Liftbay API
|
|
28306
|
+
required: false
|
|
28307
|
+
type: string
|
|
28308
|
+
default: https://api.liftbay.dev
|
|
28309
|
+
push:
|
|
28310
|
+
branches: ["liftbay/build/**"]
|
|
28012
28311
|
|
|
28013
|
-
|
|
28014
|
-
|
|
28015
|
-
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
|
|
28019
|
-
|
|
28020
|
-
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
|
|
28312
|
+
permissions:
|
|
28313
|
+
contents: read
|
|
28314
|
+
id-token: write
|
|
28315
|
+
|
|
28316
|
+
env:
|
|
28317
|
+
# Pinned runner release. The archive's SHA-256 is checked before the runner starts.
|
|
28318
|
+
# Release notes and provenance: https://github.com/liftbay/runner/releases/tag/v0.1.2
|
|
28319
|
+
RUNNER_VERSION: "0.1.2"
|
|
28320
|
+
RUNNER_SHA256_LINUX_AMD64: "9b37bfe534b6dc40f81182bdb9dc46461a672f2c28f048743dc8f13f630909b1"
|
|
28321
|
+
RUNNER_SHA256_DARWIN_ARM64: "301bf09358a47b48df0cdbd6f60edbac1227e4d7f61e037959d4009ed6ae7f27"
|
|
28322
|
+
|
|
28323
|
+
jobs:
|
|
28324
|
+
android:
|
|
28325
|
+
if: inputs.platform == 'android' || startsWith(github.ref_name, 'liftbay/build/android/')
|
|
28326
|
+
runs-on: ubuntu-latest
|
|
28327
|
+
timeout-minutes: 60
|
|
28328
|
+
steps:
|
|
28329
|
+
# GitHub's Linux runners start with about 14 GB free; a React Native release build (NDKs, CMake, four
|
|
28330
|
+
# architectures, Gradle caches) can use more. Remove preinstalled toolchains no app build uses.
|
|
28331
|
+
- name: Free disk space
|
|
28332
|
+
run: |
|
|
28333
|
+
df -h / | tail -1
|
|
28334
|
+
sudo rm -rf /usr/share/dotnet /usr/local/.ghcup /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/share/swift /usr/local/lib/node_modules /usr/local/share/powershell /usr/local/share/chromium /opt/microsoft /opt/google/chrome || true
|
|
28335
|
+
sudo docker image prune --all --force >/dev/null 2>&1 || true
|
|
28336
|
+
df -h / | tail -1
|
|
28337
|
+
|
|
28338
|
+
- uses: actions/checkout@v4
|
|
28339
|
+
|
|
28340
|
+
- uses: actions/setup-java@v4
|
|
28341
|
+
with:
|
|
28342
|
+
distribution: temurin
|
|
28343
|
+
java-version: "17"
|
|
28344
|
+
|
|
28345
|
+
- uses: actions/setup-node@v4
|
|
28346
|
+
with:
|
|
28347
|
+
node-version: "22"
|
|
28348
|
+
|
|
28349
|
+
- name: Cache JavaScript dependencies
|
|
28350
|
+
uses: actions/cache@v4
|
|
28351
|
+
with:
|
|
28352
|
+
path: |
|
|
28353
|
+
node_modules
|
|
28354
|
+
~/.npm
|
|
28355
|
+
~/.cache/yarn
|
|
28356
|
+
~/.local/share/pnpm/store
|
|
28357
|
+
~/.bun/install/cache
|
|
28358
|
+
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28359
|
+
restore-keys: js-\${{ runner.os }}-
|
|
28360
|
+
|
|
28361
|
+
- name: Cache Gradle
|
|
28362
|
+
uses: actions/cache@v4
|
|
28363
|
+
with:
|
|
28364
|
+
path: |
|
|
28365
|
+
~/.gradle/caches
|
|
28366
|
+
~/.gradle/wrapper
|
|
28367
|
+
key: gradle-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
|
28368
|
+
restore-keys: gradle-\${{ runner.os }}-
|
|
28369
|
+
|
|
28370
|
+
- name: Install the Liftbay runner
|
|
28371
|
+
env:
|
|
28372
|
+
# Bring your own runner: set the repository variables LIFTBAY_RUNNER_URL (a
|
|
28373
|
+
# liftbay-runner tar.gz you built) and LIFTBAY_RUNNER_SHA256.
|
|
28374
|
+
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL }}
|
|
28375
|
+
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256 || env.RUNNER_SHA256_LINUX_AMD64 }}
|
|
28376
|
+
run: |
|
|
28377
|
+
set -euo pipefail
|
|
28378
|
+
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_linux_amd64.tar.gz}"
|
|
28379
|
+
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
28380
|
+
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | sha256sum -c -
|
|
28381
|
+
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
28382
|
+
"$RUNNER_TEMP/liftbay-runner" version
|
|
28383
|
+
|
|
28384
|
+
- name: Build
|
|
28385
|
+
env:
|
|
28386
|
+
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
28387
|
+
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
28388
|
+
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
28389
|
+
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
28390
|
+
|
|
28391
|
+
ios:
|
|
28392
|
+
if: inputs.platform == 'ios' || startsWith(github.ref_name, 'liftbay/build/ios/')
|
|
28393
|
+
runs-on: macos-15
|
|
28394
|
+
timeout-minutes: 90
|
|
28395
|
+
steps:
|
|
28396
|
+
- uses: actions/checkout@v4
|
|
28397
|
+
|
|
28398
|
+
- uses: maxim-lobanov/setup-xcode@v1
|
|
28399
|
+
with:
|
|
28400
|
+
xcode-version: latest-stable
|
|
28401
|
+
|
|
28402
|
+
- uses: actions/setup-node@v4
|
|
28403
|
+
with:
|
|
28404
|
+
node-version: "22"
|
|
28405
|
+
|
|
28406
|
+
- name: Cache JavaScript dependencies
|
|
28407
|
+
uses: actions/cache@v4
|
|
28408
|
+
with:
|
|
28409
|
+
path: |
|
|
28410
|
+
node_modules
|
|
28411
|
+
~/.npm
|
|
28412
|
+
~/Library/Caches/Yarn
|
|
28413
|
+
~/Library/pnpm/store
|
|
28414
|
+
~/.bun/install/cache
|
|
28415
|
+
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28416
|
+
restore-keys: js-\${{ runner.os }}-
|
|
28417
|
+
|
|
28418
|
+
# Only the download caches: a restored ios/Pods would make expo prebuild see a half project.
|
|
28419
|
+
- name: Cache CocoaPods
|
|
28420
|
+
uses: actions/cache@v4
|
|
28421
|
+
with:
|
|
28422
|
+
path: |
|
|
28423
|
+
~/Library/Caches/CocoaPods
|
|
28424
|
+
~/.cocoapods
|
|
28425
|
+
key: pods-\${{ runner.os }}-\${{ hashFiles('ios/Podfile.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28426
|
+
restore-keys: pods-\${{ runner.os }}-
|
|
28427
|
+
|
|
28428
|
+
- name: Install the Liftbay runner
|
|
28429
|
+
env:
|
|
28430
|
+
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL_MACOS }}
|
|
28431
|
+
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256_MACOS || env.RUNNER_SHA256_DARWIN_ARM64 }}
|
|
28432
|
+
run: |
|
|
28433
|
+
set -euo pipefail
|
|
28434
|
+
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_darwin_arm64.tar.gz}"
|
|
28435
|
+
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
28436
|
+
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | shasum -a 256 -c -
|
|
28437
|
+
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
28438
|
+
"$RUNNER_TEMP/liftbay-runner" version
|
|
28439
|
+
|
|
28440
|
+
- name: Build
|
|
28441
|
+
env:
|
|
28442
|
+
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
28443
|
+
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
28444
|
+
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
28445
|
+
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
28446
|
+
`;
|
|
28447
|
+
|
|
28448
|
+
// src/workflow.ts
|
|
28449
|
+
var WORKFLOW_PATH = ".github/workflows/liftbay.yml";
|
|
28450
|
+
async function currentWorkflow() {
|
|
28451
|
+
try {
|
|
28452
|
+
const res = await fetch(`${apiUrl()}/v1/github/workflow`, { signal: AbortSignal.timeout(5e3) });
|
|
28453
|
+
if (res.ok) {
|
|
28454
|
+
const body = await res.json();
|
|
28455
|
+
if (typeof body.content === "string" && body.content.includes("liftbay")) return body.content;
|
|
28456
|
+
}
|
|
28457
|
+
} catch {
|
|
28458
|
+
}
|
|
28459
|
+
return WORKFLOW_YAML;
|
|
28460
|
+
}
|
|
28461
|
+
|
|
28462
|
+
// src/commands/credentials.ts
|
|
28463
|
+
import { chmodSync as chmodSync3, existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
28464
|
+
import { basename, dirname as dirname2, extname, join as join6, resolve } from "path";
|
|
28465
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
28466
|
+
|
|
28467
|
+
// src/apple/session.ts
|
|
28468
|
+
import { createHash as createHash3 } from "crypto";
|
|
28469
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
28470
|
+
import { homedir as homedir2 } from "os";
|
|
28471
|
+
import { join as join5 } from "path";
|
|
28472
|
+
|
|
28473
|
+
// src/apple/cookies.ts
|
|
28474
|
+
var CookieJar = class {
|
|
28475
|
+
cookies = [];
|
|
28476
|
+
constructor(saved = []) {
|
|
28477
|
+
this.cookies = saved.filter((c2) => !isExpired(c2));
|
|
28478
|
+
}
|
|
28479
|
+
/** Stores every Set-Cookie of a response from `url`. */
|
|
28480
|
+
store(url, setCookies, now = Date.now()) {
|
|
28481
|
+
const { hostname, pathname } = new URL(url);
|
|
28482
|
+
for (const header of setCookies) {
|
|
28483
|
+
const c2 = parse(header, hostname, pathname, now);
|
|
28484
|
+
if (!c2 || !domainMatches(hostname, c2)) continue;
|
|
28485
|
+
this.cookies = this.cookies.filter((o) => !(o.name === c2.name && o.domain === c2.domain && o.path === c2.path));
|
|
28486
|
+
if (!isExpired(c2, now)) this.cookies.push(c2);
|
|
28487
|
+
}
|
|
28028
28488
|
}
|
|
28029
28489
|
/** The Cookie header for a request to `url`, or "" when none apply. */
|
|
28030
28490
|
header(url, now = Date.now()) {
|
|
@@ -28186,9 +28646,9 @@ var IDMSA = "https://idmsa.apple.com/appleauth";
|
|
|
28186
28646
|
var ASC = "https://appstoreconnect.apple.com";
|
|
28187
28647
|
var PORTALS = ["https://developer.apple.com", "https://developer-mdn.apple.com"];
|
|
28188
28648
|
var MAX_CODE_ATTEMPTS = 3;
|
|
28189
|
-
var sessionDir = (configDir) => configDir ??
|
|
28649
|
+
var sessionDir = (configDir) => configDir ?? join5(process.env.LIFTBAY_CONFIG_DIR ?? join5(homedir2(), ".liftbay"), "apple");
|
|
28190
28650
|
var accountName = (appleId) => appleId.trim().toLowerCase();
|
|
28191
|
-
var sessionFile = (appleId, configDir) =>
|
|
28651
|
+
var sessionFile = (appleId, configDir) => join5(sessionDir(configDir), `${createHash3("sha256").update(accountName(appleId)).digest("hex")}.json`);
|
|
28192
28652
|
var b64 = (b) => Buffer.from(b).toString("base64");
|
|
28193
28653
|
function serviceError(data) {
|
|
28194
28654
|
if (!data || typeof data !== "object") return null;
|
|
@@ -28211,9 +28671,9 @@ var AppleSession = class _AppleSession {
|
|
|
28211
28671
|
/** Signs in (SRP + hashcash + 2FA), reusing a saved session from ~/.liftbay/apple/<sha256(appleId)>.json (0600) when still valid. */
|
|
28212
28672
|
static async signIn(appleId, prompts, opts = {}) {
|
|
28213
28673
|
const file2 = sessionFile(appleId, opts.configDir);
|
|
28214
|
-
if (
|
|
28674
|
+
if (existsSync4(file2)) {
|
|
28215
28675
|
try {
|
|
28216
|
-
const saved = JSON.parse(
|
|
28676
|
+
const saved = JSON.parse(readFileSync4(file2, "utf8"));
|
|
28217
28677
|
const restored = new _AppleSession(appleId, new CookieJar(saved.cookies), opts);
|
|
28218
28678
|
if (await restored.fetchOlympus()) {
|
|
28219
28679
|
opts.log?.(`Using the saved Apple session for ${appleId}`);
|
|
@@ -28242,7 +28702,7 @@ var AppleSession = class _AppleSession {
|
|
|
28242
28702
|
const dir = sessionDir(this.opts.configDir);
|
|
28243
28703
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
28244
28704
|
const file2 = sessionFile(this.appleId, this.opts.configDir);
|
|
28245
|
-
|
|
28705
|
+
writeFileSync4(file2, JSON.stringify({ appleId: accountName(this.appleId), cookies: this.jar.toJSON() }), { mode: 384 });
|
|
28246
28706
|
chmodSync2(file2, 384);
|
|
28247
28707
|
}
|
|
28248
28708
|
async login(password2, prompts) {
|
|
@@ -28474,115 +28934,14 @@ function apiKeyError(r2) {
|
|
|
28474
28934
|
return new AppleAuthError("unexpected", `App Store Connect refused to create an API key (${r2.status})${text2.trim() ? `: ${text2.trim()}` : ""}`);
|
|
28475
28935
|
}
|
|
28476
28936
|
|
|
28477
|
-
// src/project.ts
|
|
28478
|
-
import { execFile, spawn as spawn2 } from "child_process";
|
|
28479
|
-
import { promisify } from "util";
|
|
28480
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
28481
|
-
import { join as join5 } from "path";
|
|
28482
|
-
var execFileAsync = promisify(execFile);
|
|
28483
|
-
var CONFIG_FILE = "liftbay.json";
|
|
28484
|
-
function readConfig(cwd = process.cwd()) {
|
|
28485
|
-
const path2 = join5(cwd, CONFIG_FILE);
|
|
28486
|
-
return existsSync4(path2) ? JSON.parse(readFileSync4(path2, "utf8")) : null;
|
|
28487
|
-
}
|
|
28488
|
-
function requireConfig(cwd = process.cwd()) {
|
|
28489
|
-
return readConfig(cwd) ?? fail(`No ${CONFIG_FILE} here. Run \`liftbay init\` in your app's folder.`);
|
|
28490
|
-
}
|
|
28491
|
-
function writeConfig(config, cwd = process.cwd()) {
|
|
28492
|
-
writeFileSync4(join5(cwd, CONFIG_FILE), `${JSON.stringify({ $schema: "https://liftbay.dev/schema/liftbay.json", ...config }, null, 2)}
|
|
28493
|
-
`);
|
|
28494
|
-
}
|
|
28495
|
-
function readJson(path2) {
|
|
28496
|
-
return existsSync4(path2) ? JSON.parse(readFileSync4(path2, "utf8")) : null;
|
|
28497
|
-
}
|
|
28498
|
-
function packageManager(cwd = process.cwd()) {
|
|
28499
|
-
if (existsSync4(join5(cwd, "bun.lock")) || existsSync4(join5(cwd, "bun.lockb"))) return "bun";
|
|
28500
|
-
if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
28501
|
-
if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
|
|
28502
|
-
return "npm";
|
|
28503
|
-
}
|
|
28504
|
-
async function output(cmd2, args, cwd = process.cwd()) {
|
|
28505
|
-
const { stdout: stdout2 } = await execFileAsync(cmd2, args, { cwd, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
28506
|
-
return stdout2;
|
|
28507
|
-
}
|
|
28508
|
-
async function expoConfig(cwd = process.cwd()) {
|
|
28509
|
-
return JSON.parse(await output("npx", ["expo", "config", "--json", "--type", "public"], cwd));
|
|
28510
|
-
}
|
|
28511
|
-
async function detectApp(cwd = process.cwd()) {
|
|
28512
|
-
const pkg = readJson(join5(cwd, "package.json"));
|
|
28513
|
-
if (!pkg) fail("No package.json here. Run this in your React Native app's folder.");
|
|
28514
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
28515
|
-
if (!deps["react-native"]) fail("This doesn't look like a React Native app (react-native is not a dependency).");
|
|
28516
|
-
const hasExpo = !!deps.expo;
|
|
28517
|
-
let name = pkg.name ?? "app";
|
|
28518
|
-
let slug = name;
|
|
28519
|
-
let version = pkg.version ?? null;
|
|
28520
|
-
let bundleId = null;
|
|
28521
|
-
let androidPackage = null;
|
|
28522
|
-
if (hasExpo) {
|
|
28523
|
-
const cfg = await expoConfig(cwd);
|
|
28524
|
-
name = cfg.name ?? name;
|
|
28525
|
-
slug = cfg.slug ?? slug;
|
|
28526
|
-
version = cfg.version ?? version;
|
|
28527
|
-
bundleId = cfg.ios?.bundleIdentifier ?? null;
|
|
28528
|
-
androidPackage = cfg.android?.package ?? null;
|
|
28529
|
-
} else {
|
|
28530
|
-
const gradle = join5(cwd, "android/app/build.gradle");
|
|
28531
|
-
if (existsSync4(gradle)) androidPackage = readFileSync4(gradle, "utf8").match(/applicationId\s+["']([^"']+)["']/)?.[1] ?? null;
|
|
28532
|
-
}
|
|
28533
|
-
return {
|
|
28534
|
-
framework: hasExpo ? "expo" : "react-native",
|
|
28535
|
-
name,
|
|
28536
|
-
slug: String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "app",
|
|
28537
|
-
version,
|
|
28538
|
-
bundleId,
|
|
28539
|
-
androidPackage,
|
|
28540
|
-
hasExpoModules: hasExpo,
|
|
28541
|
-
hasExpoUpdates: !!deps["expo-updates"],
|
|
28542
|
-
packageManager: packageManager(cwd),
|
|
28543
|
-
appJson: existsSync4(join5(cwd, "app.json"))
|
|
28544
|
-
};
|
|
28545
|
-
}
|
|
28546
|
-
async function run(cmd2, args, cwd = process.cwd(), env2 = {}) {
|
|
28547
|
-
const code = await new Promise((resolve4, reject) => {
|
|
28548
|
-
const child = spawn2(cmd2, args, { cwd, stdio: "inherit", env: { ...process.env, ...env2 } });
|
|
28549
|
-
child.on("error", reject);
|
|
28550
|
-
child.on("close", resolve4);
|
|
28551
|
-
});
|
|
28552
|
-
if (code !== 0) fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28553
|
-
}
|
|
28554
|
-
async function runQuiet(label, cmd2, args, cwd = process.cwd()) {
|
|
28555
|
-
const spin = spinner();
|
|
28556
|
-
spin.start(label);
|
|
28557
|
-
const lines = [];
|
|
28558
|
-
const code = await new Promise((resolve4, reject) => {
|
|
28559
|
-
const child = spawn2(cmd2, args, {
|
|
28560
|
-
cwd,
|
|
28561
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28562
|
-
env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false" }
|
|
28563
|
-
});
|
|
28564
|
-
const keep = (chunk) => lines.push(...chunk.toString().split(/\r?\n/).filter(Boolean));
|
|
28565
|
-
child.stdout.on("data", keep);
|
|
28566
|
-
child.stderr.on("data", keep);
|
|
28567
|
-
child.on("error", reject);
|
|
28568
|
-
child.on("close", resolve4);
|
|
28569
|
-
});
|
|
28570
|
-
if (code === 0) return spin.stop(label.replace(/^Installing/, "Installed").replace(/^Setting up/, "Set up"));
|
|
28571
|
-
spin.error(`${label} failed`);
|
|
28572
|
-
console.error(lines.slice(-40).join("\n"));
|
|
28573
|
-
fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28574
|
-
}
|
|
28575
|
-
function readAppJson(cwd = process.cwd()) {
|
|
28576
|
-
return readJson(join5(cwd, "app.json"));
|
|
28577
|
-
}
|
|
28578
|
-
function writeAppJson(data, cwd = process.cwd()) {
|
|
28579
|
-
writeFileSync4(join5(cwd, "app.json"), `${JSON.stringify(data, null, 2)}
|
|
28580
|
-
`);
|
|
28581
|
-
}
|
|
28582
|
-
|
|
28583
28937
|
// src/prompts.ts
|
|
28584
|
-
var
|
|
28585
|
-
|
|
28938
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
28939
|
+
function fit(text2, reserve = 8) {
|
|
28940
|
+
const width = Math.max(20, (process.stdout.columns || 80) - reserve);
|
|
28941
|
+
const plain = text2.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28942
|
+
return plain.length <= width ? text2 : `${plain.slice(0, width - 1)}\u2026`;
|
|
28943
|
+
}
|
|
28944
|
+
var symbol2 = (state) => state === "submit" ? import_picocolors4.default.green("\u25C7") : state === "cancel" ? import_picocolors4.default.red("\u25A0") : state === "error" ? import_picocolors4.default.yellow("\u25B2") : import_picocolors4.default.cyan("\u25C6");
|
|
28586
28945
|
function checkboxes(opts) {
|
|
28587
28946
|
const required = opts.required ?? true;
|
|
28588
28947
|
return new a({
|
|
@@ -28594,121 +28953,28 @@ function checkboxes(opts) {
|
|
|
28594
28953
|
},
|
|
28595
28954
|
render() {
|
|
28596
28955
|
const selected = this.value ?? [];
|
|
28597
|
-
const bar = this.state === "error" ?
|
|
28598
|
-
const title = `${
|
|
28956
|
+
const bar = this.state === "error" ? import_picocolors4.default.yellow("\u2502") : this.state === "submit" || this.state === "cancel" ? import_picocolors4.default.gray("\u2502") : import_picocolors4.default.cyan("\u2502");
|
|
28957
|
+
const title = `${import_picocolors4.default.gray("\u2502")}
|
|
28599
28958
|
${symbol2(this.state)} ${opts.message}
|
|
28600
28959
|
`;
|
|
28601
28960
|
if (this.state === "submit" || this.state === "cancel") {
|
|
28602
28961
|
const chosen = this.options.filter((o) => selected.includes(o.value)).map((o) => o.label);
|
|
28603
28962
|
const text2 = chosen.join(", ") || "none";
|
|
28604
|
-
return `${title}${bar} ${this.state === "cancel" ?
|
|
28963
|
+
return `${title}${bar} ${this.state === "cancel" ? import_picocolors4.default.strikethrough(import_picocolors4.default.dim(text2)) : import_picocolors4.default.dim(text2)}`;
|
|
28605
28964
|
}
|
|
28606
|
-
const lines = this.options.map((o, i2) => {
|
|
28607
|
-
const on = selected.includes(o.value);
|
|
28608
|
-
const active = i2 === this.cursor;
|
|
28609
|
-
const box = on ?
|
|
28610
|
-
const label = active ?
|
|
28611
|
-
return `${bar} ${box} ${label}${o.hint ? ` ${
|
|
28612
|
-
});
|
|
28613
|
-
const footer = this.state === "error" ? `${
|
|
28614
|
-
return `${title}${lines.join("\n")}
|
|
28615
|
-
${footer}
|
|
28616
|
-
`;
|
|
28617
|
-
}
|
|
28618
|
-
}).prompt();
|
|
28619
|
-
}
|
|
28620
|
-
|
|
28621
|
-
// src/commands/link.ts
|
|
28622
|
-
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
28623
|
-
function cancelled(value) {
|
|
28624
|
-
if (isCancel(value)) {
|
|
28625
|
-
cancel("Cancelled");
|
|
28626
|
-
process.exit(0);
|
|
28627
|
-
}
|
|
28628
|
-
return value;
|
|
28629
|
-
}
|
|
28630
|
-
var slugifyProject = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
28631
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
28632
|
-
async function createFirstOrg(lb, given) {
|
|
28633
|
-
const me = await unwrap(lb.GET("/v1/auth/me"));
|
|
28634
|
-
const suggested = given ?? (me.user.name?.trim() || me.user.email.split("@")[0]);
|
|
28635
|
-
const name = isInteractive() && !given ? cancelled(await text({ message: "Name your organization", initialValue: suggested, validate: (v) => slugify(v ?? "").length >= 2 ? void 0 : "At least 2 letters or digits" })) : suggested;
|
|
28636
|
-
const base = slugify(name).padEnd(2, "0");
|
|
28637
|
-
for (const slug of [base, `${base.slice(0, 35)}-${Math.random().toString(36).slice(2, 6)}`]) {
|
|
28638
|
-
const res = await lb.POST("/v1/orgs", { body: { name: name.slice(0, 64), slug } });
|
|
28639
|
-
if (res.data) {
|
|
28640
|
-
log.success(`Created organization ${import_picocolors4.default.bold(res.data.name)} ${import_picocolors4.default.dim(`(liftbay.dev/${res.data.slug})`)}`);
|
|
28641
|
-
return res.data;
|
|
28642
|
-
}
|
|
28643
|
-
if (res.response.status !== 409) await unwrap(Promise.resolve(res));
|
|
28644
|
-
}
|
|
28645
|
-
return fail("Couldn't pick a free organization URL. Pass --org <slug>.");
|
|
28646
|
-
}
|
|
28647
|
-
async function ensureLinked(app, opts) {
|
|
28648
|
-
const existing = readConfig();
|
|
28649
|
-
if (existing) {
|
|
28650
|
-
log.success(`Linked to ${import_picocolors4.default.bold(`${existing.org}/${existing.project}`)}`);
|
|
28651
|
-
return existing;
|
|
28652
|
-
}
|
|
28653
|
-
const lb = client();
|
|
28654
|
-
const orgs = await unwrap(lb.GET("/v1/orgs"));
|
|
28655
|
-
if (!orgs.length) orgs.push(await createFirstOrg(lb, opts.org));
|
|
28656
|
-
let org = opts.org ?? (orgs.length === 1 ? orgs[0].slug : void 0);
|
|
28657
|
-
if (!org) {
|
|
28658
|
-
if (!isInteractive()) fail(`Several organizations available; pass --org (${orgs.map((o) => o.slug).join(", ")})`);
|
|
28659
|
-
org = cancelled(await select({ message: "Organization", options: orgs.map((o) => ({ value: o.slug, label: o.name, hint: o.slug })) }));
|
|
28660
|
-
}
|
|
28661
|
-
const projects = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org } } }));
|
|
28662
|
-
let slug = opts.project;
|
|
28663
|
-
let projectName;
|
|
28664
|
-
if (!slug) {
|
|
28665
|
-
const match = projects.find((pr) => pr.slug === app.slug);
|
|
28666
|
-
if (opts.yes || !isInteractive()) slug = match?.slug ?? app.slug;
|
|
28667
|
-
else {
|
|
28668
|
-
const taken = projects.some((pr) => pr.slug === app.slug);
|
|
28669
|
-
const choice = cancelled(
|
|
28670
|
-
await select({
|
|
28671
|
-
message: "Liftbay project",
|
|
28672
|
-
options: [
|
|
28673
|
-
...taken ? [] : [{ value: "__new__", label: `Create "${app.name}"`, hint: `new project \xB7 liftbay.dev/${org}/${app.slug}` }],
|
|
28674
|
-
{ value: "__named__", label: "Create a project with another name" },
|
|
28675
|
-
...projects.map((pr) => ({ value: pr.slug, label: pr.name, hint: `existing \xB7 ${pr.slug}` }))
|
|
28676
|
-
],
|
|
28677
|
-
initialValue: match?.slug ?? (taken ? "__named__" : "__new__")
|
|
28678
|
-
})
|
|
28679
|
-
);
|
|
28680
|
-
if (choice === "__named__") {
|
|
28681
|
-
projectName = cancelled(
|
|
28682
|
-
await text({
|
|
28683
|
-
message: "Project name",
|
|
28684
|
-
initialValue: app.name,
|
|
28685
|
-
validate: (v) => {
|
|
28686
|
-
const s = slugifyProject(v ?? "");
|
|
28687
|
-
if (s.length < 2) return "At least 2 letters or digits";
|
|
28688
|
-
if (projects.some((pr) => pr.slug === s)) return `${s} already exists: pick it from the list instead`;
|
|
28689
|
-
}
|
|
28690
|
-
})
|
|
28691
|
-
);
|
|
28692
|
-
slug = slugifyProject(projectName);
|
|
28693
|
-
} else slug = choice === "__new__" ? app.slug : choice;
|
|
28694
|
-
}
|
|
28695
|
-
}
|
|
28696
|
-
let project2 = projects.find((pr) => pr.slug === slug);
|
|
28697
|
-
if (!project2) {
|
|
28698
|
-
project2 = await unwrap(
|
|
28699
|
-
lb.POST("/v1/orgs/{org}/projects", {
|
|
28700
|
-
params: { path: { org } },
|
|
28701
|
-
body: { name: projectName ?? app.name, slug, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }
|
|
28702
|
-
})
|
|
28703
|
-
);
|
|
28704
|
-
log.success(`Created project ${import_picocolors4.default.bold(`${org}/${project2.slug}`)} with development, preview and production environments`);
|
|
28705
|
-
} else {
|
|
28706
|
-
log.success(`Linked to existing project ${import_picocolors4.default.bold(`${org}/${project2.slug}`)}`);
|
|
28707
|
-
}
|
|
28708
|
-
const config = { org, project: project2.slug, projectId: project2.id };
|
|
28709
|
-
writeConfig(config);
|
|
28710
|
-
log.info(`Wrote ${import_picocolors4.default.cyan(cmd("liftbay.json"))} ${import_picocolors4.default.dim("(commit it: it links this repository to the project)")}`);
|
|
28711
|
-
return config;
|
|
28965
|
+
const lines = this.options.map((o, i2) => {
|
|
28966
|
+
const on = selected.includes(o.value);
|
|
28967
|
+
const active = i2 === this.cursor;
|
|
28968
|
+
const box = on ? import_picocolors4.default.green("[\u2714]") : active ? import_picocolors4.default.cyan("[ ]") : import_picocolors4.default.dim("[ ]");
|
|
28969
|
+
const label = active ? import_picocolors4.default.bold(o.label) : o.label;
|
|
28970
|
+
return `${bar} ${box} ${label}${o.hint ? ` ${import_picocolors4.default.dim(`(${o.hint})`)}` : ""}`;
|
|
28971
|
+
});
|
|
28972
|
+
const footer = this.state === "error" ? `${import_picocolors4.default.yellow("\u2514")} ${import_picocolors4.default.yellow(this.error)}` : `${import_picocolors4.default.cyan("\u2514")} ${import_picocolors4.default.dim("\u2191/\u2193 move \xB7 space select \xB7 enter confirm")}`;
|
|
28973
|
+
return `${title}${lines.join("\n")}
|
|
28974
|
+
${footer}
|
|
28975
|
+
`;
|
|
28976
|
+
}
|
|
28977
|
+
}).prompt();
|
|
28712
28978
|
}
|
|
28713
28979
|
|
|
28714
28980
|
// src/commands/credentials.ts
|
|
@@ -28983,11 +29249,11 @@ async function status(lb, config, refresh = true) {
|
|
|
28983
29249
|
}
|
|
28984
29250
|
async function waitFor(lb, config, message, done) {
|
|
28985
29251
|
const spin = spinner();
|
|
28986
|
-
spin.start(
|
|
29252
|
+
spin.start(fit(message));
|
|
28987
29253
|
const until = Date.now() + WAIT_MINUTES * 6e4;
|
|
28988
29254
|
while (Date.now() < until) {
|
|
28989
29255
|
await sleep2(4e3);
|
|
28990
|
-
const s = await status(lb, config);
|
|
29256
|
+
const s = await poll(() => status(lb, config));
|
|
28991
29257
|
if (done(s)) {
|
|
28992
29258
|
spin.stop(message.replace(/^Waiting for /, "Done: "));
|
|
28993
29259
|
return s;
|
|
@@ -28998,27 +29264,36 @@ async function waitFor(lb, config, message, done) {
|
|
|
28998
29264
|
}
|
|
28999
29265
|
async function waitForInstall(lb, config, attempt) {
|
|
29000
29266
|
const spin = spinner();
|
|
29001
|
-
spin.start(
|
|
29267
|
+
spin.start(fit("Waiting for the GitHub App to be installed"));
|
|
29268
|
+
const explain = (text2, spinnerText) => {
|
|
29269
|
+
spin.clear();
|
|
29270
|
+
log.info(text2);
|
|
29271
|
+
spin.start(fit(spinnerText));
|
|
29272
|
+
};
|
|
29273
|
+
let noRepoAccessShown = false;
|
|
29002
29274
|
const until = Date.now() + WAIT_MINUTES * 6e4;
|
|
29003
29275
|
let last = "waiting";
|
|
29004
29276
|
while (Date.now() < until) {
|
|
29005
29277
|
await sleep2(3e3);
|
|
29006
|
-
const a2 = attempt ? (await lb.GET("/v1/orgs/{org}/github/install/{attempt}", { params: { path: { org: config.org, attempt } } })).data : void 0;
|
|
29278
|
+
const a2 = attempt ? (await poll(() => lb.GET("/v1/orgs/{org}/github/install/{attempt}", { params: { path: { org: config.org, attempt } } }))).data : void 0;
|
|
29007
29279
|
if (a2 && a2.status !== last) {
|
|
29008
29280
|
last = a2.status;
|
|
29009
|
-
if (a2.status === "needs_sign_in")
|
|
29281
|
+
if (a2.status === "needs_sign_in") explain("GitHub is done. Sign in to Liftbay in that browser to finish linking.", "Waiting for you to sign in to Liftbay in the browser");
|
|
29010
29282
|
else if (a2.status !== "waiting" && a2.status !== "connected") {
|
|
29011
29283
|
spin.error("GitHub didn't connect");
|
|
29012
29284
|
return { ok: false, message: a2.message };
|
|
29013
29285
|
}
|
|
29014
29286
|
}
|
|
29015
|
-
const s = await status(lb, config);
|
|
29287
|
+
const s = await poll(() => status(lb, config));
|
|
29016
29288
|
if (s.connected) {
|
|
29017
29289
|
spin.stop(`GitHub App installed for ${s.repo}`);
|
|
29018
29290
|
return { ok: true, status: s };
|
|
29019
29291
|
}
|
|
29020
29292
|
if (a2?.status === "connected") {
|
|
29021
|
-
|
|
29293
|
+
if (!noRepoAccessShown) {
|
|
29294
|
+
noRepoAccessShown = true;
|
|
29295
|
+
explain(`Installed on ${s.repo.split("/")[0]}, but without access to ${s.repo}. On GitHub, Configure the app and select the repository.`, "Waiting for access to the repository");
|
|
29296
|
+
}
|
|
29022
29297
|
}
|
|
29023
29298
|
}
|
|
29024
29299
|
spin.error("Stopped waiting");
|
|
@@ -29156,9 +29431,91 @@ function pushState(branch) {
|
|
|
29156
29431
|
return { upstream: true, ahead: 0 };
|
|
29157
29432
|
}
|
|
29158
29433
|
}
|
|
29434
|
+
var minutes = (sec) => sec ? sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s` : "";
|
|
29435
|
+
function activity(b) {
|
|
29436
|
+
if (b.status === "queued") return b.engine === "xcode-cloud" ? "Waiting for Xcode Cloud" : "Waiting for a runner";
|
|
29437
|
+
if (b.status === "preparing") return "Starting";
|
|
29438
|
+
if (b.status === "uploading") return "Uploading";
|
|
29439
|
+
return b.phases.find((ph) => ph.status === "running")?.name ?? "Building";
|
|
29440
|
+
}
|
|
29441
|
+
async function followSteps(lb, path2, started) {
|
|
29442
|
+
const tag = (b) => started.length > 1 ? `${b.platform === "ios" ? "iOS" : "Android"} \xB7 ` : "";
|
|
29443
|
+
const shown = new Map(started.map((b) => [b.number, /* @__PURE__ */ new Set()]));
|
|
29444
|
+
const latest = new Map(started.map((b) => [b.number, b]));
|
|
29445
|
+
const runnerSeen = /* @__PURE__ */ new Set();
|
|
29446
|
+
const spin = spinner({ withGuide: false });
|
|
29447
|
+
spin.start(fit(started.map((b) => `${tag(b)}${activity(b)}`).join(" ")));
|
|
29448
|
+
const say = (line) => {
|
|
29449
|
+
spin.clear();
|
|
29450
|
+
process.stdout.write(`${import_picocolors7.default.gray("\u2502")} ${line}
|
|
29451
|
+
`);
|
|
29452
|
+
spin.start(fit(started.map((b) => `${tag(b)}${activity(latest.get(b.number))}`).join(" ")));
|
|
29453
|
+
};
|
|
29454
|
+
for (; ; ) {
|
|
29455
|
+
for (const b of started) {
|
|
29456
|
+
const prev = latest.get(b.number);
|
|
29457
|
+
if (FINISHED.has(prev.status)) continue;
|
|
29458
|
+
const fresh = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29459
|
+
latest.set(b.number, fresh);
|
|
29460
|
+
if (fresh.status !== "queued" && !runnerSeen.has(b.number)) {
|
|
29461
|
+
runnerSeen.add(b.number);
|
|
29462
|
+
say(`${import_picocolors7.default.green("\u2714")} ${tag(b)}${fresh.engine === "xcode-cloud" ? "Xcode Cloud started" : "Runner started"}`);
|
|
29463
|
+
}
|
|
29464
|
+
for (const ph of fresh.phases) {
|
|
29465
|
+
if (ph.status === "running" || ph.status === "queued" || shown.get(b.number).has(ph.name)) continue;
|
|
29466
|
+
shown.get(b.number).add(ph.name);
|
|
29467
|
+
const mark = ph.status === "succeeded" ? import_picocolors7.default.green("\u2714") : ph.status === "cancelled" ? import_picocolors7.default.yellow("\u25A0") : import_picocolors7.default.red("\u2716");
|
|
29468
|
+
say(`${mark} ${tag(b)}${ph.name} ${import_picocolors7.default.dim(minutes(ph.durationSec))}`);
|
|
29469
|
+
}
|
|
29470
|
+
}
|
|
29471
|
+
const all = [...latest.values()];
|
|
29472
|
+
if (all.every((b) => FINISHED.has(b.status))) {
|
|
29473
|
+
spin.stop(all.every((b) => b.status === "succeeded") ? "Finished" : "Finished with problems");
|
|
29474
|
+
return all;
|
|
29475
|
+
}
|
|
29476
|
+
spin.message(fit(all.map((b) => `${tag(b)}${FINISHED.has(b.status) ? b.status : activity(b)}`).join(" ")));
|
|
29477
|
+
await sleep3(3e3);
|
|
29478
|
+
}
|
|
29479
|
+
}
|
|
29480
|
+
async function followLogs(lb, path2, started) {
|
|
29481
|
+
const prefix = started.length > 1;
|
|
29482
|
+
return Promise.all(
|
|
29483
|
+
started.map(async (b) => {
|
|
29484
|
+
let after = 0;
|
|
29485
|
+
let phase = null;
|
|
29486
|
+
for (; ; ) {
|
|
29487
|
+
const page = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } })));
|
|
29488
|
+
for (const l2 of page.lines) {
|
|
29489
|
+
const tag = prefix ? import_picocolors7.default.dim(`${b.platform.padEnd(8)}`) : "";
|
|
29490
|
+
if (l2.phase && l2.phase !== phase) {
|
|
29491
|
+
phase = l2.phase;
|
|
29492
|
+
console.log(`${tag}${import_picocolors7.default.cyan(`==> ${phase}`)}`);
|
|
29493
|
+
}
|
|
29494
|
+
console.log(`${tag}${l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : l2.stream === "system" ? import_picocolors7.default.dim(l2.text) : l2.text}`);
|
|
29495
|
+
}
|
|
29496
|
+
after = page.nextSeq;
|
|
29497
|
+
if (page.complete) break;
|
|
29498
|
+
if (!page.lines.length) {
|
|
29499
|
+
const fresh = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29500
|
+
if (FINISHED.has(fresh.status) && fresh.logLineCount <= after) break;
|
|
29501
|
+
await sleep3(2e3);
|
|
29502
|
+
}
|
|
29503
|
+
}
|
|
29504
|
+
return poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29505
|
+
})
|
|
29506
|
+
);
|
|
29507
|
+
}
|
|
29508
|
+
async function printLogTail(lb, path2, b, tagged, count = 30) {
|
|
29509
|
+
const after = Math.max(0, b.logLineCount - count);
|
|
29510
|
+
const page = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } })).catch(() => null);
|
|
29511
|
+
if (!page?.lines.length) return;
|
|
29512
|
+
log.error(`${tagged ? `${b.platform === "ios" ? "iOS" : "Android"} \xB7 ` : ""}build #${b.number} failed. The end of its log:`);
|
|
29513
|
+
console.log(page.lines.slice(-count).map((l2) => l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : import_picocolors7.default.dim(l2.text)).join("\n"));
|
|
29514
|
+
log.info(`Whole log: ${import_picocolors7.default.cyan(cmd(`liftbay builds logs ${b.number}`))}`);
|
|
29515
|
+
}
|
|
29159
29516
|
var SETUP_FILES = ["liftbay.json", ".github/workflows/liftbay.yml", "app.json", "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb"];
|
|
29160
29517
|
function git(args) {
|
|
29161
|
-
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).
|
|
29518
|
+
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trimEnd();
|
|
29162
29519
|
}
|
|
29163
29520
|
function gitLoud(args) {
|
|
29164
29521
|
try {
|
|
@@ -29168,6 +29525,20 @@ function gitLoud(args) {
|
|
|
29168
29525
|
}
|
|
29169
29526
|
}
|
|
29170
29527
|
async function readyToBuild(branch, interactive) {
|
|
29528
|
+
const wanted = await currentWorkflow();
|
|
29529
|
+
const present = existsSync6(WORKFLOW_PATH) ? readFileSync6(WORKFLOW_PATH, "utf8") : null;
|
|
29530
|
+
if (present !== wanted) {
|
|
29531
|
+
const what = present === null ? `${WORKFLOW_PATH} isn't in this repository` : `${WORKFLOW_PATH} is out of date`;
|
|
29532
|
+
if (!interactive) fail(`${what}. Update it with \`liftbay ci init --yes\`, then commit and push it.`);
|
|
29533
|
+
log.warn(`${what}${present === null ? "" : " (Liftbay changed how builds run)"}.`);
|
|
29534
|
+
if (cancelled(await confirm({ message: present === null ? "Add it?" : "Update it?", initialValue: true }))) {
|
|
29535
|
+
mkdirSync4(dirname3(WORKFLOW_PATH), { recursive: true });
|
|
29536
|
+
writeFileSync6(WORKFLOW_PATH, wanted);
|
|
29537
|
+
log.success(`${present === null ? "Added" : "Updated"} ${WORKFLOW_PATH}`);
|
|
29538
|
+
} else if (present === null) {
|
|
29539
|
+
fail(`Builds need ${WORKFLOW_PATH} on ${branch}. Run \`liftbay ci init\` to add it.`);
|
|
29540
|
+
}
|
|
29541
|
+
}
|
|
29171
29542
|
const changed = git(["status", "--porcelain", "--", ...SETUP_FILES]).split("\n").filter(Boolean).map((l2) => l2.slice(3).replace(/^"|"$/g, ""));
|
|
29172
29543
|
if (changed.some((f) => f === "liftbay.json" || f.startsWith(".github/"))) {
|
|
29173
29544
|
const files = changed.filter((f) => f !== "" && SETUP_FILES.some((s) => f === s || f.startsWith(".github/")));
|
|
@@ -29323,32 +29694,8 @@ async function build(opts) {
|
|
|
29323
29694
|
outro(`Queued. Follow along in the dashboard, or with ${import_picocolors7.default.cyan(cmd("liftbay builds view <number>"))}.`);
|
|
29324
29695
|
return { builds: started.map(summary) };
|
|
29325
29696
|
}
|
|
29326
|
-
const
|
|
29327
|
-
const results
|
|
29328
|
-
started.map(async (b) => {
|
|
29329
|
-
let after = 0;
|
|
29330
|
-
let phase = null;
|
|
29331
|
-
for (; ; ) {
|
|
29332
|
-
const page = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } }));
|
|
29333
|
-
for (const l2 of page.lines) {
|
|
29334
|
-
const tag = prefix ? import_picocolors7.default.dim(`${b.platform.padEnd(8)}`) : "";
|
|
29335
|
-
if (l2.phase && l2.phase !== phase) {
|
|
29336
|
-
phase = l2.phase;
|
|
29337
|
-
console.log(`${tag}${import_picocolors7.default.cyan(`==> ${phase}`)}`);
|
|
29338
|
-
}
|
|
29339
|
-
console.log(`${tag}${l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : l2.stream === "system" ? import_picocolors7.default.dim(l2.text) : l2.text}`);
|
|
29340
|
-
}
|
|
29341
|
-
after = page.nextSeq;
|
|
29342
|
-
if (page.complete) break;
|
|
29343
|
-
if (!page.lines.length) {
|
|
29344
|
-
const fresh = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } }));
|
|
29345
|
-
if (FINISHED.has(fresh.status) && fresh.logLineCount <= after) break;
|
|
29346
|
-
await sleep3(2e3);
|
|
29347
|
-
}
|
|
29348
|
-
}
|
|
29349
|
-
return unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } }));
|
|
29350
|
-
})
|
|
29351
|
-
);
|
|
29697
|
+
const results = opts.logs ? await followLogs(lb, path2, started) : await followSteps(lb, path2, started);
|
|
29698
|
+
for (const r2 of results.filter((r3) => r3.status === "failed")) await printLogTail(lb, path2, r2, started.length > 1);
|
|
29352
29699
|
const out = [];
|
|
29353
29700
|
for (const r2 of results) {
|
|
29354
29701
|
let installUrl = null;
|
|
@@ -29375,7 +29722,7 @@ async function build(opts) {
|
|
|
29375
29722
|
|
|
29376
29723
|
// src/commands/builds.ts
|
|
29377
29724
|
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
29378
|
-
import { createWriteStream, mkdirSync as
|
|
29725
|
+
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
29379
29726
|
import { join as join7, resolve as resolve2 } from "path";
|
|
29380
29727
|
import { Readable } from "stream";
|
|
29381
29728
|
import { pipeline } from "stream/promises";
|
|
@@ -29476,7 +29823,7 @@ async function buildsDownload(number, opts) {
|
|
|
29476
29823
|
const ready = b.artifacts.filter((a2) => a2.status === "ready" && (!opts.kind || a2.kind === opts.kind));
|
|
29477
29824
|
if (!ready.length) fail(opts.kind ? `Build #${b.number} has no ready ${opts.kind} artifact` : `Build #${b.number} has no ready artifacts (${b.status})`);
|
|
29478
29825
|
const dir = resolve2(opts.out ?? ".");
|
|
29479
|
-
|
|
29826
|
+
mkdirSync5(dir, { recursive: true });
|
|
29480
29827
|
const files = [];
|
|
29481
29828
|
for (const a2 of ready) {
|
|
29482
29829
|
const link = await unwrap(
|
|
@@ -29544,7 +29891,7 @@ async function xcodeCloudConnect(opts) {
|
|
|
29544
29891
|
last = s.step;
|
|
29545
29892
|
if (!wait || s.step === "apple_key") fail(s.step === "apple_key" ? s.message : "Not connected yet. Run this again once the step above is done.");
|
|
29546
29893
|
const spin = spinner();
|
|
29547
|
-
spin.start("Waiting for that step (checking every 10 seconds
|
|
29894
|
+
spin.start(fit("Waiting for that step (checking every 10 seconds)"));
|
|
29548
29895
|
await sleep5(1e4);
|
|
29549
29896
|
spin.stop("Checking again");
|
|
29550
29897
|
}
|
|
@@ -29563,7 +29910,7 @@ async function xcodeCloudDisconnect() {
|
|
|
29563
29910
|
}
|
|
29564
29911
|
|
|
29565
29912
|
// src/commands/submit.ts
|
|
29566
|
-
import { readFileSync as
|
|
29913
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
29567
29914
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
29568
29915
|
var TRACKS2 = ["internal", "alpha", "beta", "production"];
|
|
29569
29916
|
var DONE = /* @__PURE__ */ new Set(["succeeded", "failed"]);
|
|
@@ -29593,7 +29940,7 @@ async function submit(opts) {
|
|
|
29593
29940
|
queued.map(async (s) => {
|
|
29594
29941
|
let shown = 0;
|
|
29595
29942
|
for (; ; ) {
|
|
29596
|
-
const d = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/submissions/{submission}", { params: { path: { ...path2, submission: s.id } } }));
|
|
29943
|
+
const d = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/submissions/{submission}", { params: { path: { ...path2, submission: s.id } } })));
|
|
29597
29944
|
for (const e of d.events.slice(shown)) log.message(`${import_picocolors10.default.dim(s.platform.padEnd(8))}${e.text}`);
|
|
29598
29945
|
shown = d.events.length;
|
|
29599
29946
|
if (DONE.has(d.status)) return d;
|
|
@@ -29623,7 +29970,7 @@ async function credentialsAndroidCommand(opts) {
|
|
|
29623
29970
|
}
|
|
29624
29971
|
let json2;
|
|
29625
29972
|
try {
|
|
29626
|
-
json2 =
|
|
29973
|
+
json2 = readFileSync7(opts.playKey.replace(/^~/, process.env.HOME ?? "~"), "utf8");
|
|
29627
29974
|
} catch {
|
|
29628
29975
|
fail(`Can't read ${opts.playKey}`);
|
|
29629
29976
|
}
|
|
@@ -29674,7 +30021,7 @@ ${reg.url}`, "Scan with the iPhone's camera, or open the link in Safari on it");
|
|
|
29674
30021
|
const until = Date.parse(reg.expiresAt);
|
|
29675
30022
|
while (Date.now() < until) {
|
|
29676
30023
|
await sleep7(2500);
|
|
29677
|
-
const status2 = await unwrap(lb.GET("/v1/orgs/{org}/devices/registrations/{registration}", { params: { path: { ...org, registration: reg.id } } }));
|
|
30024
|
+
const status2 = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/devices/registrations/{registration}", { params: { path: { ...org, registration: reg.id } } })));
|
|
29678
30025
|
if (status2.device) {
|
|
29679
30026
|
spin.stop(`Registered ${import_picocolors11.default.bold(status2.device.name)} ${import_picocolors11.default.dim(`${status2.device.model ?? ""} ${status2.device.udid}`)}`);
|
|
29680
30027
|
await nextStep(lb);
|
|
@@ -29703,171 +30050,16 @@ async function deviceRemove(id) {
|
|
|
29703
30050
|
}
|
|
29704
30051
|
|
|
29705
30052
|
// src/commands/setup.ts
|
|
29706
|
-
import { existsSync as
|
|
29707
|
-
import { dirname as
|
|
29708
|
-
var
|
|
29709
|
-
|
|
29710
|
-
// src/generated/workflow.ts
|
|
29711
|
-
var WORKFLOW_YAML = `# Liftbay builds on your own GitHub Actions minutes.
|
|
29712
|
-
# Liftbay starts this workflow; it does nothing on your own pushes or pull requests. With this file on the
|
|
29713
|
-
# default branch, Liftbay uses workflow_dispatch. Otherwise (a branch you're trying out), Liftbay points a
|
|
29714
|
-
# temporary liftbay/build/<platform>/<build id> branch at your commit, which runs this file from that commit,
|
|
29715
|
-
# and deletes it once the build has started.
|
|
29716
|
-
# The build gets no repository secrets: the runner exchanges this run's GitHub OIDC token for a
|
|
29717
|
-
# one-time Liftbay job token, checked against this repository and this workflow file.
|
|
29718
|
-
# Docs: https://github.com/liftbay/runner
|
|
29719
|
-
name: Liftbay build
|
|
29720
|
-
run-name: Liftbay build \${{ inputs.build_id || github.ref_name }}
|
|
29721
|
-
|
|
29722
|
-
on:
|
|
29723
|
-
workflow_dispatch:
|
|
29724
|
-
inputs:
|
|
29725
|
-
build_id:
|
|
29726
|
-
description: Liftbay build id
|
|
29727
|
-
required: true
|
|
29728
|
-
type: string
|
|
29729
|
-
platform:
|
|
29730
|
-
description: Platform
|
|
29731
|
-
required: true
|
|
29732
|
-
type: choice
|
|
29733
|
-
options: [android, ios]
|
|
29734
|
-
api_url:
|
|
29735
|
-
description: Liftbay API
|
|
29736
|
-
required: false
|
|
29737
|
-
type: string
|
|
29738
|
-
default: https://api.liftbay.dev
|
|
29739
|
-
push:
|
|
29740
|
-
branches: ["liftbay/build/**"]
|
|
29741
|
-
|
|
29742
|
-
permissions:
|
|
29743
|
-
contents: read
|
|
29744
|
-
id-token: write
|
|
29745
|
-
|
|
29746
|
-
env:
|
|
29747
|
-
# Pinned runner release. The archive's SHA-256 is checked before the runner starts.
|
|
29748
|
-
# Release notes and provenance: https://github.com/liftbay/runner/releases/tag/v0.1.2
|
|
29749
|
-
RUNNER_VERSION: "0.1.2"
|
|
29750
|
-
RUNNER_SHA256_LINUX_AMD64: "9b37bfe534b6dc40f81182bdb9dc46461a672f2c28f048743dc8f13f630909b1"
|
|
29751
|
-
RUNNER_SHA256_DARWIN_ARM64: "301bf09358a47b48df0cdbd6f60edbac1227e4d7f61e037959d4009ed6ae7f27"
|
|
29752
|
-
|
|
29753
|
-
jobs:
|
|
29754
|
-
android:
|
|
29755
|
-
if: inputs.platform == 'android' || startsWith(github.ref_name, 'liftbay/build/android/')
|
|
29756
|
-
runs-on: ubuntu-latest
|
|
29757
|
-
timeout-minutes: 60
|
|
29758
|
-
steps:
|
|
29759
|
-
- uses: actions/checkout@v4
|
|
29760
|
-
|
|
29761
|
-
- uses: actions/setup-java@v4
|
|
29762
|
-
with:
|
|
29763
|
-
distribution: temurin
|
|
29764
|
-
java-version: "17"
|
|
29765
|
-
|
|
29766
|
-
- uses: actions/setup-node@v4
|
|
29767
|
-
with:
|
|
29768
|
-
node-version: "22"
|
|
29769
|
-
|
|
29770
|
-
- name: Cache JavaScript dependencies
|
|
29771
|
-
uses: actions/cache@v4
|
|
29772
|
-
with:
|
|
29773
|
-
path: |
|
|
29774
|
-
node_modules
|
|
29775
|
-
~/.npm
|
|
29776
|
-
~/.cache/yarn
|
|
29777
|
-
~/.local/share/pnpm/store
|
|
29778
|
-
~/.bun/install/cache
|
|
29779
|
-
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29780
|
-
restore-keys: js-\${{ runner.os }}-
|
|
29781
|
-
|
|
29782
|
-
- name: Cache Gradle
|
|
29783
|
-
uses: actions/cache@v4
|
|
29784
|
-
with:
|
|
29785
|
-
path: |
|
|
29786
|
-
~/.gradle/caches
|
|
29787
|
-
~/.gradle/wrapper
|
|
29788
|
-
key: gradle-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
|
29789
|
-
restore-keys: gradle-\${{ runner.os }}-
|
|
29790
|
-
|
|
29791
|
-
- name: Install the Liftbay runner
|
|
29792
|
-
env:
|
|
29793
|
-
# Bring your own runner: set the repository variables LIFTBAY_RUNNER_URL (a
|
|
29794
|
-
# liftbay-runner tar.gz you built) and LIFTBAY_RUNNER_SHA256.
|
|
29795
|
-
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL }}
|
|
29796
|
-
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256 || env.RUNNER_SHA256_LINUX_AMD64 }}
|
|
29797
|
-
run: |
|
|
29798
|
-
set -euo pipefail
|
|
29799
|
-
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_linux_amd64.tar.gz}"
|
|
29800
|
-
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
29801
|
-
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | sha256sum -c -
|
|
29802
|
-
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
29803
|
-
"$RUNNER_TEMP/liftbay-runner" version
|
|
29804
|
-
|
|
29805
|
-
- name: Build
|
|
29806
|
-
env:
|
|
29807
|
-
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
29808
|
-
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
29809
|
-
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
29810
|
-
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
29811
|
-
|
|
29812
|
-
ios:
|
|
29813
|
-
if: inputs.platform == 'ios' || startsWith(github.ref_name, 'liftbay/build/ios/')
|
|
29814
|
-
runs-on: macos-15
|
|
29815
|
-
timeout-minutes: 90
|
|
29816
|
-
steps:
|
|
29817
|
-
- uses: actions/checkout@v4
|
|
29818
|
-
|
|
29819
|
-
- uses: maxim-lobanov/setup-xcode@v1
|
|
29820
|
-
with:
|
|
29821
|
-
xcode-version: latest-stable
|
|
29822
|
-
|
|
29823
|
-
- uses: actions/setup-node@v4
|
|
29824
|
-
with:
|
|
29825
|
-
node-version: "22"
|
|
29826
|
-
|
|
29827
|
-
- name: Cache JavaScript dependencies
|
|
29828
|
-
uses: actions/cache@v4
|
|
29829
|
-
with:
|
|
29830
|
-
path: |
|
|
29831
|
-
node_modules
|
|
29832
|
-
~/.npm
|
|
29833
|
-
~/Library/Caches/Yarn
|
|
29834
|
-
~/Library/pnpm/store
|
|
29835
|
-
~/.bun/install/cache
|
|
29836
|
-
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29837
|
-
restore-keys: js-\${{ runner.os }}-
|
|
29838
|
-
|
|
29839
|
-
# Only the download caches: a restored ios/Pods would make expo prebuild see a half project.
|
|
29840
|
-
- name: Cache CocoaPods
|
|
29841
|
-
uses: actions/cache@v4
|
|
29842
|
-
with:
|
|
29843
|
-
path: |
|
|
29844
|
-
~/Library/Caches/CocoaPods
|
|
29845
|
-
~/.cocoapods
|
|
29846
|
-
key: pods-\${{ runner.os }}-\${{ hashFiles('ios/Podfile.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29847
|
-
restore-keys: pods-\${{ runner.os }}-
|
|
29848
|
-
|
|
29849
|
-
- name: Install the Liftbay runner
|
|
29850
|
-
env:
|
|
29851
|
-
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL_MACOS }}
|
|
29852
|
-
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256_MACOS || env.RUNNER_SHA256_DARWIN_ARM64 }}
|
|
29853
|
-
run: |
|
|
29854
|
-
set -euo pipefail
|
|
29855
|
-
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_darwin_arm64.tar.gz}"
|
|
29856
|
-
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
29857
|
-
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | shasum -a 256 -c -
|
|
29858
|
-
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
29859
|
-
"$RUNNER_TEMP/liftbay-runner" version
|
|
30053
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
30054
|
+
import { dirname as dirname4 } from "path";
|
|
30055
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
29860
30056
|
|
|
29861
|
-
|
|
29862
|
-
|
|
29863
|
-
|
|
29864
|
-
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
29865
|
-
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
29866
|
-
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
29867
|
-
`;
|
|
30057
|
+
// src/commands/answers.ts
|
|
30058
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
30059
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
29868
30060
|
|
|
29869
30061
|
// src/commands/env.ts
|
|
29870
|
-
import { existsSync as
|
|
30062
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
29871
30063
|
import { basename as basename2, join as join8, resolve as resolve3 } from "path";
|
|
29872
30064
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
29873
30065
|
var VISIBILITIES = ["plain", "sensitive", "secret"];
|
|
@@ -29911,10 +30103,10 @@ function parseVisibility(v) {
|
|
|
29911
30103
|
var slugify2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "default";
|
|
29912
30104
|
function readEasProfiles(cwd = process.cwd()) {
|
|
29913
30105
|
const path2 = join8(cwd, "eas.json");
|
|
29914
|
-
if (!
|
|
30106
|
+
if (!existsSync7(path2)) return null;
|
|
29915
30107
|
let eas;
|
|
29916
30108
|
try {
|
|
29917
|
-
eas = JSON.parse(
|
|
30109
|
+
eas = JSON.parse(readFileSync8(path2, "utf8"));
|
|
29918
30110
|
} catch {
|
|
29919
30111
|
fail("eas.json isn't valid JSON");
|
|
29920
30112
|
}
|
|
@@ -30035,7 +30227,7 @@ async function envImport(files, opts) {
|
|
|
30035
30227
|
const results = [];
|
|
30036
30228
|
for (const file2 of files) {
|
|
30037
30229
|
const abs = resolve3(file2.replace(/^~/, process.env.HOME ?? "~"));
|
|
30038
|
-
if (!
|
|
30230
|
+
if (!existsSync7(abs)) fail(`No file at ${file2}`);
|
|
30039
30231
|
if (basename2(abs) === "eas.json") {
|
|
30040
30232
|
const profiles = readEasProfiles(resolve3(abs, ".."));
|
|
30041
30233
|
if (!profiles.length) fail("eas.json has no build profiles");
|
|
@@ -30053,7 +30245,7 @@ async function envImport(files, opts) {
|
|
|
30053
30245
|
}
|
|
30054
30246
|
continue;
|
|
30055
30247
|
}
|
|
30056
|
-
const { vars, invalid } = parseEnv(
|
|
30248
|
+
const { vars, invalid } = parseEnv(readFileSync8(abs, "utf8"));
|
|
30057
30249
|
if (invalid.length) log.warn(`${file2}: skipped unreadable line${invalid.length > 1 ? "s" : ""} ${invalid.join(", ")}`);
|
|
30058
30250
|
const { slug } = await pickEnvironment(lb, path2, opts.environment, file2);
|
|
30059
30251
|
const r2 = await importVariables(lb, path2, slug, vars, { visibility, overwrite });
|
|
@@ -30072,13 +30264,165 @@ async function envExport(opts) {
|
|
|
30072
30264
|
`);
|
|
30073
30265
|
return { environment: slug, content };
|
|
30074
30266
|
}
|
|
30075
|
-
if (
|
|
30076
|
-
|
|
30267
|
+
if (existsSync7(opts.out) && !opts.force) fail(`${opts.out} exists. Pass --force to overwrite it.`);
|
|
30268
|
+
writeFileSync7(opts.out, `${content.trimEnd()}
|
|
30077
30269
|
`, { mode: 384 });
|
|
30078
30270
|
log.success(`Wrote ${opts.out} ${import_picocolors12.default.dim("(secret values are not exported)")}`);
|
|
30079
30271
|
return { environment: slug, file: resolve3(opts.out) };
|
|
30080
30272
|
}
|
|
30081
30273
|
|
|
30274
|
+
// src/commands/answers.ts
|
|
30275
|
+
var KEYS = ["org", "newOrg", "project", "newProject", "platforms", "iosEngine", "eas", "ota", "runtimePolicy", "envFiles"];
|
|
30276
|
+
function readAnswers(path2) {
|
|
30277
|
+
if (!existsSync8(path2)) fail(`No answers file at ${path2}`);
|
|
30278
|
+
let a2;
|
|
30279
|
+
try {
|
|
30280
|
+
a2 = JSON.parse(readFileSync9(path2, "utf8"));
|
|
30281
|
+
} catch {
|
|
30282
|
+
fail(`${path2} isn't valid JSON`);
|
|
30283
|
+
}
|
|
30284
|
+
const problems = [];
|
|
30285
|
+
const unknown = Object.keys(a2).filter((k2) => !KEYS.includes(k2));
|
|
30286
|
+
if (unknown.length) problems.push(`unknown field${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} (allowed: ${KEYS.join(", ")})`);
|
|
30287
|
+
const str = (k2) => a2[k2] !== void 0 && (typeof a2[k2] !== "string" || !a2[k2].trim()) && problems.push(`${k2} must be a non-empty string`);
|
|
30288
|
+
const bool = (k2) => a2[k2] !== void 0 && typeof a2[k2] !== "boolean" && problems.push(`${k2} must be true or false`);
|
|
30289
|
+
const oneOf = (k2, values) => a2[k2] !== void 0 && !values.includes(a2[k2]) && problems.push(`${k2} must be ${values.join(" or ")}`);
|
|
30290
|
+
["org", "newOrg", "project", "newProject"].forEach(str);
|
|
30291
|
+
["eas", "ota"].forEach(bool);
|
|
30292
|
+
oneOf("iosEngine", ["github-actions", "xcode-cloud"]);
|
|
30293
|
+
oneOf("runtimePolicy", ["appVersion", "fingerprint"]);
|
|
30294
|
+
if (a2.org && a2.newOrg) problems.push("give org or newOrg, not both");
|
|
30295
|
+
if (a2.project && a2.newProject) problems.push("give project or newProject, not both");
|
|
30296
|
+
if (a2.platforms !== void 0 && (!Array.isArray(a2.platforms) || !a2.platforms.length || !a2.platforms.every((x) => x === "ios" || x === "android"))) {
|
|
30297
|
+
problems.push(`platforms must be a list of "ios" and/or "android"`);
|
|
30298
|
+
}
|
|
30299
|
+
if (a2.envFiles !== void 0) {
|
|
30300
|
+
const ok = typeof a2.envFiles === "object" && a2.envFiles !== null && Object.values(a2.envFiles).every((v) => Array.isArray(v) && v.every((f) => typeof f === "string"));
|
|
30301
|
+
if (!ok) problems.push(`envFiles must look like { "production": [".env.production"] }`);
|
|
30302
|
+
else for (const f of Object.values(a2.envFiles).flat()) if (!existsSync8(f)) problems.push(`envFiles: no file at ${f}`);
|
|
30303
|
+
}
|
|
30304
|
+
if (problems.length) fail(`${path2}:
|
|
30305
|
+
${problems.join("\n ")}`);
|
|
30306
|
+
return a2;
|
|
30307
|
+
}
|
|
30308
|
+
function answersToOptions(a2) {
|
|
30309
|
+
return {
|
|
30310
|
+
org: a2.org,
|
|
30311
|
+
newOrg: a2.newOrg,
|
|
30312
|
+
project: a2.project,
|
|
30313
|
+
projectName: a2.newProject,
|
|
30314
|
+
platforms: a2.platforms?.join(","),
|
|
30315
|
+
iosEngine: a2.iosEngine,
|
|
30316
|
+
eas: a2.eas,
|
|
30317
|
+
ota: a2.ota,
|
|
30318
|
+
runtimePolicy: a2.runtimePolicy,
|
|
30319
|
+
yes: true
|
|
30320
|
+
};
|
|
30321
|
+
}
|
|
30322
|
+
async function initQuestions() {
|
|
30323
|
+
const app = await detectApp();
|
|
30324
|
+
const linked = readConfig();
|
|
30325
|
+
const signedIn = !!(process.env.LIFTBAY_TOKEN || readCredentials());
|
|
30326
|
+
let orgs = [];
|
|
30327
|
+
const projects = {};
|
|
30328
|
+
if (signedIn) {
|
|
30329
|
+
const lb = client();
|
|
30330
|
+
orgs = await unwrap(lb.GET("/v1/orgs")).catch(() => []);
|
|
30331
|
+
for (const o of orgs) projects[o.slug] = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org: o.slug } } })).catch(() => []);
|
|
30332
|
+
}
|
|
30333
|
+
const eas = readEasProfiles();
|
|
30334
|
+
const envFiles = readdirSync3(".").filter((f) => /^\.env(\..+)?$/.test(f) && !/\.(example|sample|template)$/.test(f));
|
|
30335
|
+
const policy = readAppJson()?.expo?.runtimeVersion?.policy;
|
|
30336
|
+
const questions = [];
|
|
30337
|
+
if (!linked) {
|
|
30338
|
+
questions.push(
|
|
30339
|
+
orgs.length ? {
|
|
30340
|
+
id: "org",
|
|
30341
|
+
question: "Which Liftbay organization should own this app?",
|
|
30342
|
+
type: "choice",
|
|
30343
|
+
choices: orgs.map((o) => ({ value: o.slug, label: o.name })),
|
|
30344
|
+
default: orgs.length === 1 ? orgs[0].slug : void 0,
|
|
30345
|
+
note: "Or answer newOrg with a name to create another organization."
|
|
30346
|
+
} : {
|
|
30347
|
+
id: "newOrg",
|
|
30348
|
+
question: "What should the new Liftbay organization be called?",
|
|
30349
|
+
type: "text",
|
|
30350
|
+
note: signedIn ? "This account has no organization yet; init creates it." : "Not signed in yet, so existing organizations can't be listed. Sign in first to see them."
|
|
30351
|
+
},
|
|
30352
|
+
{
|
|
30353
|
+
id: "project",
|
|
30354
|
+
question: "Use an existing Liftbay project, or create a new one?",
|
|
30355
|
+
type: "choice",
|
|
30356
|
+
choices: Object.values(projects).flat().map((pr) => ({ value: pr.slug, label: pr.name })),
|
|
30357
|
+
default: void 0,
|
|
30358
|
+
note: `For a new project, answer newProject with its name instead (suggested: "${app.name}").`
|
|
30359
|
+
}
|
|
30360
|
+
);
|
|
30361
|
+
}
|
|
30362
|
+
questions.push({ id: "platforms", question: "Which platforms should Liftbay build?", type: "multi", choices: [{ value: "ios", label: "iOS" }, { value: "android", label: "Android" }], default: ["ios", "android"] });
|
|
30363
|
+
questions.push({
|
|
30364
|
+
id: "iosEngine",
|
|
30365
|
+
question: "Where should iOS store builds run by default?",
|
|
30366
|
+
type: "choice",
|
|
30367
|
+
when: "platforms includes ios",
|
|
30368
|
+
choices: [
|
|
30369
|
+
{ value: "github-actions", label: "GitHub Actions (macOS runners on your GitHub minutes; ready now)" },
|
|
30370
|
+
{ value: "xcode-cloud", label: "Xcode Cloud (25 free hours a month with the Apple Developer Program; one-time setup after iOS signing)" }
|
|
30371
|
+
],
|
|
30372
|
+
default: "github-actions"
|
|
30373
|
+
});
|
|
30374
|
+
if (eas?.length) {
|
|
30375
|
+
questions.push({
|
|
30376
|
+
id: "eas",
|
|
30377
|
+
question: `Use eas.json's build profiles (${eas.map((e) => e.name).join(", ")}), their channels and env variables?`,
|
|
30378
|
+
type: "boolean",
|
|
30379
|
+
default: true
|
|
30380
|
+
});
|
|
30381
|
+
}
|
|
30382
|
+
questions.push({ id: "ota", question: "Set up over-the-air updates (expo-updates)?", type: "boolean", default: true });
|
|
30383
|
+
questions.push({
|
|
30384
|
+
id: "runtimePolicy",
|
|
30385
|
+
question: "Which installed apps can receive an update?",
|
|
30386
|
+
type: "choice",
|
|
30387
|
+
when: "ota is true",
|
|
30388
|
+
choices: [
|
|
30389
|
+
{ value: "appVersion", label: "Same app version (bump the version when native code changes)" },
|
|
30390
|
+
{ value: "fingerprint", label: "Same native fingerprint (computed from native code and config)" }
|
|
30391
|
+
],
|
|
30392
|
+
default: policy === "fingerprint" ? "fingerprint" : "appVersion"
|
|
30393
|
+
});
|
|
30394
|
+
if (envFiles.length) {
|
|
30395
|
+
questions.push({
|
|
30396
|
+
id: "envFiles",
|
|
30397
|
+
question: `Which of these files should go into which environment? ${envFiles.join(", ")}`,
|
|
30398
|
+
type: "files",
|
|
30399
|
+
default: {},
|
|
30400
|
+
note: 'Like { "production": [".env.production"], "preview": [".env.preview"] }. Files like .env or .env.local often hold local development values: ask before sending them.'
|
|
30401
|
+
});
|
|
30402
|
+
}
|
|
30403
|
+
const example = {
|
|
30404
|
+
...linked ? {} : orgs.length ? { org: orgs[0].slug } : { newOrg: "Acme" },
|
|
30405
|
+
...linked ? {} : { newProject: app.name },
|
|
30406
|
+
platforms: ["ios", "android"],
|
|
30407
|
+
iosEngine: "github-actions",
|
|
30408
|
+
...eas?.length ? { eas: true } : {},
|
|
30409
|
+
ota: true,
|
|
30410
|
+
runtimePolicy: "appVersion"
|
|
30411
|
+
};
|
|
30412
|
+
const next = `Ask the person these questions, write their answers to a JSON file, then run: ${cmd("liftbay init --answers <file> --json")}`;
|
|
30413
|
+
intro(import_picocolors13.default.bgCyan(import_picocolors13.default.black(" liftbay init --questions ")));
|
|
30414
|
+
log.info(`${import_picocolors13.default.bold(app.name)} \xB7 ${app.framework === "expo" ? "Expo" : "React Native"}${app.bundleId ? ` \xB7 ${app.bundleId}` : ""}${linked ? ` \xB7 linked to ${linked.org}/${linked.project}` : ""}`);
|
|
30415
|
+
if (!signedIn) log.warn(`Not signed in: ${cmd("liftbay login")} first to list organizations and projects.`);
|
|
30416
|
+
for (const q of questions) {
|
|
30417
|
+
const choices = q.choices?.length ? `
|
|
30418
|
+
${q.choices.map((c2) => ` ${import_picocolors13.default.cyan(c2.value)} ${import_picocolors13.default.dim(c2.label)}`).join("\n")}` : "";
|
|
30419
|
+
log.message(`${import_picocolors13.default.bold(q.id)}${q.when ? import_picocolors13.default.dim(` (if ${q.when})`) : ""}: ${q.question}${choices}${q.note ? `
|
|
30420
|
+
${import_picocolors13.default.dim(q.note)}` : ""}`);
|
|
30421
|
+
}
|
|
30422
|
+
outro(next);
|
|
30423
|
+
return { app: { name: app.name, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }, linked: linked ? { org: linked.org, project: linked.project } : null, signedIn, questions, example, next };
|
|
30424
|
+
}
|
|
30425
|
+
|
|
30082
30426
|
// src/commands/setup.ts
|
|
30083
30427
|
var DEFAULT_PROFILES = {
|
|
30084
30428
|
development: { environment: "development", channel: "development", distribution: "internal" },
|
|
@@ -30121,11 +30465,11 @@ async function ciInit(app, config, opts) {
|
|
|
30121
30465
|
})
|
|
30122
30466
|
);
|
|
30123
30467
|
}
|
|
30124
|
-
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${
|
|
30125
|
-
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${
|
|
30468
|
+
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${import_picocolors14.default.cyan("expo.ios.bundleIdentifier")} in app.json before the first iOS build.`);
|
|
30469
|
+
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${import_picocolors14.default.cyan("expo.android.package")} in app.json before the first Android build.`);
|
|
30126
30470
|
const path2 = { org: config.org, project: config.project };
|
|
30127
30471
|
const repo = await githubRepo();
|
|
30128
|
-
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${
|
|
30472
|
+
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${import_picocolors14.default.cyan(cmd("liftbay ci init"))} again.`);
|
|
30129
30473
|
await unwrap(
|
|
30130
30474
|
lb.PATCH("/v1/orgs/{org}/projects/{project}", {
|
|
30131
30475
|
params: { path: path2 },
|
|
@@ -30153,22 +30497,22 @@ async function ciInit(app, config, opts) {
|
|
|
30153
30497
|
printImport(r2);
|
|
30154
30498
|
imported.push(r2);
|
|
30155
30499
|
}
|
|
30156
|
-
log.info(
|
|
30157
|
-
log.info(
|
|
30500
|
+
log.info(import_picocolors14.default.dim("EXPO_PUBLIC_* values are plain (they're in the app bundle); names like *_TOKEN or *_SECRET are secret; the rest sensitive."));
|
|
30501
|
+
log.info(import_picocolors14.default.dim(`Variables kept on expo.dev (EAS environment variables) aren't in eas.json: ${import_picocolors14.default.cyan("npx eas env:pull --environment production")}, then ${import_picocolors14.default.cyan(cmd("liftbay env import .env.local --environment production"))}.`));
|
|
30158
30502
|
}
|
|
30159
30503
|
const profiles = Object.fromEntries(Object.entries(base).map(([name, prof]) => [name, { ...prof, platforms }]));
|
|
30160
30504
|
if (iosEngine) await unwrap(lb.PATCH("/v1/orgs/{org}/projects/{project}", { params: { path: path2 }, body: { iosEngine } }));
|
|
30161
30505
|
const next = { ...config, build: { profiles, triggers: config.build?.triggers ?? [] } };
|
|
30162
30506
|
writeConfig(next);
|
|
30163
30507
|
note(
|
|
30164
|
-
Object.entries(profiles).map(([name, prof]) => `${
|
|
30508
|
+
Object.entries(profiles).map(([name, prof]) => `${import_picocolors14.default.bold(name.padEnd(12))} env ${prof.environment.padEnd(12)} ${prof.distribution === "store" ? "store" : "internal"} \xB7 ${prof.platforms.join(", ")}`).join("\n") + (iosEngine ? `
|
|
30165
30509
|
|
|
30166
30510
|
iOS store builds run on ${iosEngine === "xcode-cloud" ? "Xcode Cloud" : "GitHub Actions"} (project setting)` : ""),
|
|
30167
30511
|
"Build profiles"
|
|
30168
30512
|
);
|
|
30169
30513
|
if (iosEngine === "xcode-cloud") {
|
|
30170
30514
|
log.info(
|
|
30171
|
-
`Xcode Cloud makes iOS store builds (TestFlight); internal and simulator builds stay on GitHub Actions. It needs iOS signing first: ${
|
|
30515
|
+
`Xcode Cloud makes iOS store builds (TestFlight); internal and simulator builds stay on GitHub Actions. It needs iOS signing first: ${import_picocolors14.default.cyan(cmd("liftbay credentials ios"))}, then ${import_picocolors14.default.cyan(cmd("liftbay xcode-cloud connect"))}.`
|
|
30172
30516
|
);
|
|
30173
30517
|
}
|
|
30174
30518
|
lastImport = imported;
|
|
@@ -30183,12 +30527,11 @@ async function githubStep() {
|
|
|
30183
30527
|
} catch (e) {
|
|
30184
30528
|
if (!(e instanceof CliError)) throw e;
|
|
30185
30529
|
log.warn(e.message);
|
|
30186
|
-
log.info(`Finish any time with ${
|
|
30530
|
+
log.info(`Finish any time with ${import_picocolors14.default.cyan(cmd("liftbay github connect"))}; ${cmd("liftbay build")} checks it too.`);
|
|
30187
30531
|
return { error: e.message };
|
|
30188
30532
|
}
|
|
30189
30533
|
}
|
|
30190
30534
|
var lastImport = [];
|
|
30191
|
-
var WORKFLOW_PATH = ".github/workflows/liftbay.yml";
|
|
30192
30535
|
async function githubRepo() {
|
|
30193
30536
|
try {
|
|
30194
30537
|
const url = (await output("git", ["remote", "get-url", "origin"])).trim();
|
|
@@ -30198,16 +30541,17 @@ async function githubRepo() {
|
|
|
30198
30541
|
}
|
|
30199
30542
|
}
|
|
30200
30543
|
async function writeWorkflow(opts) {
|
|
30201
|
-
const
|
|
30202
|
-
|
|
30544
|
+
const wanted = await currentWorkflow();
|
|
30545
|
+
const current = existsSync9(WORKFLOW_PATH) ? readFileSync10(WORKFLOW_PATH, "utf8") : null;
|
|
30546
|
+
if (current === wanted) return;
|
|
30203
30547
|
if (current !== null) {
|
|
30204
30548
|
const replace = opts.yes || isInteractive() && cancelled(await confirm({ message: `${WORKFLOW_PATH} differs from the current Liftbay workflow. Replace it?` }));
|
|
30205
30549
|
if (!replace) return log.info(`Kept your ${WORKFLOW_PATH}.`);
|
|
30206
30550
|
}
|
|
30207
|
-
|
|
30208
|
-
|
|
30209
|
-
log.success(`Wrote ${
|
|
30210
|
-
log.info(
|
|
30551
|
+
mkdirSync6(dirname4(WORKFLOW_PATH), { recursive: true });
|
|
30552
|
+
writeFileSync8(WORKFLOW_PATH, wanted);
|
|
30553
|
+
log.success(`Wrote ${import_picocolors14.default.cyan(WORKFLOW_PATH)}. Commit and push it: Liftbay starts it for every build, on your GitHub Actions minutes.`);
|
|
30554
|
+
log.info(import_picocolors14.default.dim("GitHub counts macOS minutes about 10\xD7 faster than Linux; on a private repo with GitHub Free that is roughly 10 iOS builds a month."));
|
|
30211
30555
|
}
|
|
30212
30556
|
var pluginSpec = () => process.env.LIFTBAY_EXPO_PLUGIN_SPEC ?? "@liftbay/expo-plugin";
|
|
30213
30557
|
async function addDependency(app, spec, label) {
|
|
@@ -30246,12 +30590,12 @@ async function otaInit(app, config, opts) {
|
|
|
30246
30590
|
if (!env2) fail(`Environment ${prof.environment} not found; run \`liftbay ci init\` first`);
|
|
30247
30591
|
if (!existing) {
|
|
30248
30592
|
await unwrap(lb.POST("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 }, body: { name: prof.channel, environmentId: env2.id, protocols: ["expo"] } }));
|
|
30249
|
-
log.success(`Created channel ${
|
|
30593
|
+
log.success(`Created channel ${import_picocolors14.default.bold(prof.channel)} \u2192 ${prof.environment}`);
|
|
30250
30594
|
} else if (!existing.protocols.includes("expo")) {
|
|
30251
30595
|
await unwrap(
|
|
30252
30596
|
lb.PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}", { params: { path: { ...path2, channel: prof.channel } }, body: { protocols: [...existing.protocols, "expo"] } })
|
|
30253
30597
|
);
|
|
30254
|
-
log.success(`Enabled expo-updates on channel ${
|
|
30598
|
+
log.success(`Enabled expo-updates on channel ${import_picocolors14.default.bold(prof.channel)}`);
|
|
30255
30599
|
}
|
|
30256
30600
|
}
|
|
30257
30601
|
channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
@@ -30271,24 +30615,24 @@ runtimeVersion: { "policy": "${policy}" }`, "Add to your app config");
|
|
|
30271
30615
|
else plugins.push(entry);
|
|
30272
30616
|
expo.runtimeVersion = { policy };
|
|
30273
30617
|
writeAppJson(appJson);
|
|
30274
|
-
log.success(`Updated ${
|
|
30618
|
+
log.success(`Updated ${import_picocolors14.default.cyan("app.json")}: Liftbay plugin, runtimeVersion policy "${policy}"`);
|
|
30275
30619
|
}
|
|
30276
30620
|
const next = { ...config, updates: { client: "expo-updates", runtimeVersion: { policy } } };
|
|
30277
30621
|
writeConfig(next);
|
|
30278
30622
|
return next;
|
|
30279
30623
|
}
|
|
30280
30624
|
async function ciInitCommand(opts) {
|
|
30281
|
-
intro(
|
|
30625
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay ci init ")));
|
|
30282
30626
|
await ensureSignedIn(opts);
|
|
30283
30627
|
const app = await detectApp();
|
|
30284
30628
|
const config = await ensureLinked(app, opts);
|
|
30285
30629
|
const next = await ciInit(app, config, opts);
|
|
30286
30630
|
const github2 = await githubStep();
|
|
30287
|
-
outro(`Builds are configured. Start one with ${
|
|
30631
|
+
outro(`Builds are configured. Start one with ${import_picocolors14.default.cyan(cmd("liftbay build"))}`);
|
|
30288
30632
|
return { config: next, imported: lastImport, github: github2 };
|
|
30289
30633
|
}
|
|
30290
30634
|
async function otaInitCommand(opts) {
|
|
30291
|
-
intro(
|
|
30635
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay ota init ")));
|
|
30292
30636
|
await ensureSignedIn(opts);
|
|
30293
30637
|
const app = await detectApp();
|
|
30294
30638
|
const config = await ensureLinked(app, opts);
|
|
@@ -30299,37 +30643,49 @@ async function otaInitCommand(opts) {
|
|
|
30299
30643
|
var nextStepsOta = () => `OTA updates are configured.
|
|
30300
30644
|
|
|
30301
30645
|
1. Build a new binary once (the update URL and channel are baked in)
|
|
30302
|
-
2. Ship JS changes with ${
|
|
30646
|
+
2. Ship JS changes with ${import_picocolors14.default.cyan(cmd("liftbay update publish --channel production"))}`;
|
|
30303
30647
|
async function initCommand(opts) {
|
|
30304
|
-
|
|
30648
|
+
if (opts.questions) return initQuestions();
|
|
30649
|
+
let envFiles;
|
|
30650
|
+
if (opts.answers) {
|
|
30651
|
+
const answers = readAnswers(opts.answers);
|
|
30652
|
+
Object.assign(opts, answersToOptions(answers));
|
|
30653
|
+
envFiles = answers.envFiles;
|
|
30654
|
+
disablePrompts();
|
|
30655
|
+
}
|
|
30656
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay init ")));
|
|
30305
30657
|
await ensureSignedIn(opts);
|
|
30306
30658
|
const app = await detectApp();
|
|
30307
30659
|
log.info(
|
|
30308
|
-
`${
|
|
30660
|
+
`${import_picocolors14.default.bold(app.name)}: ${app.framework === "expo" ? "Expo" : "React Native"}${app.version ? ` ${app.version}` : ""}${app.bundleId ? ` \xB7 ${app.bundleId}` : ""}${app.androidPackage && app.androidPackage !== app.bundleId ? ` \xB7 ${app.androidPackage}` : ""}`
|
|
30309
30661
|
);
|
|
30310
30662
|
let config = await ensureLinked(app, opts);
|
|
30311
|
-
log.step(
|
|
30663
|
+
log.step(import_picocolors14.default.bold("1/3 Builds"));
|
|
30312
30664
|
config = await ciInit(app, config, opts);
|
|
30313
|
-
log.step(
|
|
30665
|
+
log.step(import_picocolors14.default.bold("2/3 OTA updates"));
|
|
30314
30666
|
const wantOta = opts.ota ?? (opts.yes || !isInteractive() ? true : cancelled(await confirm({ message: "Set up over-the-air updates with expo-updates?", initialValue: true })));
|
|
30315
30667
|
if (wantOta) config = await otaInit(await detectApp(), config, opts);
|
|
30316
|
-
else log.info(`Skipped. Run ${
|
|
30317
|
-
|
|
30668
|
+
else log.info(`Skipped. Run ${import_picocolors14.default.cyan(cmd("liftbay ota init"))} any time.`);
|
|
30669
|
+
const imported = [];
|
|
30670
|
+
for (const [environment, files] of Object.entries(envFiles ?? {})) {
|
|
30671
|
+
if (files.length) imported.push(...(await envImport(files, { environment })).imported);
|
|
30672
|
+
}
|
|
30673
|
+
log.step(import_picocolors14.default.bold("3/3 GitHub"));
|
|
30318
30674
|
const github2 = await githubStep();
|
|
30319
30675
|
outro(
|
|
30320
30676
|
[
|
|
30321
|
-
`Done. ${
|
|
30677
|
+
`Done. ${import_picocolors14.default.dim(`${dashboardUrl()}/${config.org}/${config.project}`)}`,
|
|
30322
30678
|
"",
|
|
30323
30679
|
" Next:",
|
|
30324
|
-
` 1. Build: ${
|
|
30325
|
-
...wantOta ? [` 2. Update: ${
|
|
30326
|
-
...cmd("liftbay") !== "liftbay" ? ["",
|
|
30680
|
+
` 1. Build: ${import_picocolors14.default.cyan(cmd("liftbay build"))} ${import_picocolors14.default.dim("(offers to commit the setup and push this branch)")}`,
|
|
30681
|
+
...wantOta ? [` 2. Update: ${import_picocolors14.default.cyan(cmd("liftbay update publish --channel preview"))} ${import_picocolors14.default.dim("(after a build from this setup is installed)")}`] : [],
|
|
30682
|
+
...cmd("liftbay") !== "liftbay" ? ["", import_picocolors14.default.dim(` Tip: ${import_picocolors14.default.cyan("npm i -g liftbay")} to type ${import_picocolors14.default.cyan("liftbay")} instead of ${import_picocolors14.default.cyan("npx liftbay")}.`)] : []
|
|
30327
30683
|
].join("\n")
|
|
30328
30684
|
);
|
|
30329
30685
|
return {
|
|
30330
30686
|
github: github2,
|
|
30331
30687
|
config,
|
|
30332
|
-
imported: lastImport,
|
|
30688
|
+
imported: [...lastImport, ...imported],
|
|
30333
30689
|
url: `${dashboardUrl()}/${config.org}/${config.project}`,
|
|
30334
30690
|
next: ["Commit liftbay.json, .github/workflows/liftbay.yml and app.json, and push", "liftbay build --profile preview", ...wantOta ? ["liftbay update publish --channel production"] : []]
|
|
30335
30691
|
};
|
|
@@ -30338,17 +30694,17 @@ async function initCommand(opts) {
|
|
|
30338
30694
|
// src/commands/update.ts
|
|
30339
30695
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
30340
30696
|
import { createHash as createHash4 } from "crypto";
|
|
30341
|
-
import { mkdtempSync, readFileSync as
|
|
30697
|
+
import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
|
|
30342
30698
|
import { tmpdir } from "os";
|
|
30343
30699
|
import { join as join9 } from "path";
|
|
30344
|
-
var
|
|
30700
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
30345
30701
|
|
|
30346
30702
|
// src/commands/signing.ts
|
|
30347
30703
|
import { createPublicKey, createSign, randomBytes as randomBytes2 } from "crypto";
|
|
30348
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
30349
|
-
import { dirname as
|
|
30704
|
+
import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
30705
|
+
import { dirname as dirname5, relative } from "path";
|
|
30350
30706
|
var import_node_forge = __toESM(require_lib2(), 1);
|
|
30351
|
-
var
|
|
30707
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
30352
30708
|
var DEFAULT_PRIVATE_KEY = "keys/liftbay-private-key.pem";
|
|
30353
30709
|
var DEFAULT_PUBLIC_KEY = "certs/liftbay-public-key.pem";
|
|
30354
30710
|
var DEFAULT_CERTIFICATE = "certs/liftbay-certificate.pem";
|
|
@@ -30378,18 +30734,18 @@ function generateSigningCertificate(commonName, years = 10) {
|
|
|
30378
30734
|
var signRsaSha256 = (privateKeyPem, data) => createSign("RSA-SHA256").update(data).sign(privateKeyPem, "base64");
|
|
30379
30735
|
function loadPrivateKey(path2) {
|
|
30380
30736
|
const read = (file2, label) => {
|
|
30381
|
-
if (!
|
|
30382
|
-
return { pem:
|
|
30737
|
+
if (!existsSync10(file2)) fail(`No private key at ${file2} (${label})`);
|
|
30738
|
+
return { pem: readFileSync11(file2, "utf8"), source: file2 };
|
|
30383
30739
|
};
|
|
30384
30740
|
if (path2) return read(path2, "--private-key");
|
|
30385
30741
|
const env2 = process.env.LIFTBAY_UPDATE_PRIVATE_KEY;
|
|
30386
30742
|
if (env2) return env2.includes("PRIVATE KEY") ? { pem: env2.replace(/\\n/g, "\n"), source: "LIFTBAY_UPDATE_PRIVATE_KEY" } : read(env2, "LIFTBAY_UPDATE_PRIVATE_KEY");
|
|
30387
|
-
return
|
|
30743
|
+
return existsSync10(DEFAULT_PRIVATE_KEY) ? { pem: readFileSync11(DEFAULT_PRIVATE_KEY, "utf8"), source: DEFAULT_PRIVATE_KEY } : null;
|
|
30388
30744
|
}
|
|
30389
30745
|
function ensureIgnored(entry) {
|
|
30390
|
-
const current =
|
|
30746
|
+
const current = existsSync10(".gitignore") ? readFileSync11(".gitignore", "utf8") : "";
|
|
30391
30747
|
if (current.split("\n").some((l2) => l2.trim() === entry || l2.trim() === entry.replace(/\/$/, ""))) return false;
|
|
30392
|
-
|
|
30748
|
+
writeFileSync9(".gitignore", `${current}${current && !current.endsWith("\n") ? "\n" : ""}
|
|
30393
30749
|
# Liftbay update signing key: never commit it
|
|
30394
30750
|
${entry}
|
|
30395
30751
|
`);
|
|
@@ -30411,7 +30767,7 @@ async function signingInit(opts) {
|
|
|
30411
30767
|
const config = requireConfig();
|
|
30412
30768
|
const lb = client();
|
|
30413
30769
|
const path2 = { org: config.org, project: config.project };
|
|
30414
|
-
intro(
|
|
30770
|
+
intro(import_picocolors15.default.bgCyan(import_picocolors15.default.black(" liftbay update signing init ")));
|
|
30415
30771
|
const { signing: signing2 } = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/updates/signing", { params: { path: path2 } }));
|
|
30416
30772
|
if (signing2 && !opts.force) {
|
|
30417
30773
|
fail(
|
|
@@ -30420,16 +30776,16 @@ async function signingInit(opts) {
|
|
|
30420
30776
|
}
|
|
30421
30777
|
const privateKeyPath = opts.privateKey ?? DEFAULT_PRIVATE_KEY;
|
|
30422
30778
|
const certificatePath = opts.certificate ?? DEFAULT_CERTIFICATE;
|
|
30423
|
-
if (
|
|
30779
|
+
if (existsSync10(privateKeyPath) && !opts.force) fail(`${privateKeyPath} already exists. Pass --force to overwrite it, or --private-key for another path.`);
|
|
30424
30780
|
const generated = generateSigningCertificate(opts.commonName ?? config.project);
|
|
30425
|
-
for (const dir of /* @__PURE__ */ new Set([
|
|
30426
|
-
|
|
30781
|
+
for (const dir of /* @__PURE__ */ new Set([dirname5(privateKeyPath), dirname5(certificatePath), dirname5(DEFAULT_PUBLIC_KEY)])) mkdirSync7(dir, { recursive: true });
|
|
30782
|
+
writeFileSync9(privateKeyPath, generated.privateKeyPem, { mode: 384 });
|
|
30427
30783
|
chmodSync4(privateKeyPath, 384);
|
|
30428
|
-
|
|
30429
|
-
|
|
30430
|
-
if (ensureIgnored(`${
|
|
30431
|
-
log.success(`Private key: ${
|
|
30432
|
-
Certificate: ${
|
|
30784
|
+
writeFileSync9(certificatePath, generated.certificatePem);
|
|
30785
|
+
writeFileSync9(DEFAULT_PUBLIC_KEY, generated.publicKeyPem);
|
|
30786
|
+
if (ensureIgnored(`${dirname5(relative(".", privateKeyPath))}/`)) log.info(`Added ${import_picocolors15.default.cyan(`${dirname5(privateKeyPath)}/`)} to .gitignore`);
|
|
30787
|
+
log.success(`Private key: ${import_picocolors15.default.cyan(privateKeyPath)} (stays on this machine)
|
|
30788
|
+
Certificate: ${import_picocolors15.default.cyan(certificatePath)} (commit it; it's embedded in your app)`);
|
|
30433
30789
|
await unwrap(
|
|
30434
30790
|
lb.PUT("/v1/orgs/{org}/projects/{project}/updates/signing", {
|
|
30435
30791
|
params: { path: path2 },
|
|
@@ -30437,7 +30793,7 @@ async function signingInit(opts) {
|
|
|
30437
30793
|
})
|
|
30438
30794
|
);
|
|
30439
30795
|
log.success("Certificate saved to Liftbay");
|
|
30440
|
-
if (configureAppJson(certificatePath)) log.success(`Updated ${
|
|
30796
|
+
if (configureAppJson(certificatePath)) log.success(`Updated ${import_picocolors15.default.cyan("app.json")}: the Liftbay plugin embeds the certificate in your app`);
|
|
30441
30797
|
else note(`plugins: [["@liftbay/expo-plugin", { "codeSigningCertificate": "./${certificatePath}" }]]`, "Add to your app config");
|
|
30442
30798
|
const channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
30443
30799
|
const signedOnly = !!channels2.length && (opts.yes || !isInteractive() || cancelled(await confirm({ message: "Require signed updates on every channel? Unsigned publishes will be refused.", initialValue: true })));
|
|
@@ -30450,12 +30806,12 @@ async function signingInit(opts) {
|
|
|
30450
30806
|
note(
|
|
30451
30807
|
[
|
|
30452
30808
|
`1. Rebuild your apps once: the certificate is part of the binary.`,
|
|
30453
|
-
`2. ${
|
|
30454
|
-
`3. In CI, store the private key as a secret named ${
|
|
30809
|
+
`2. ${import_picocolors15.default.cyan(cmd("liftbay update publish"))} signs automatically with ${privateKeyPath}.`,
|
|
30810
|
+
`3. In CI, store the private key as a secret named ${import_picocolors15.default.cyan("LIFTBAY_UPDATE_PRIVATE_KEY")}.`,
|
|
30455
30811
|
`4. Back the key up somewhere safe. If it's lost, apps need a rebuild with a new certificate.`,
|
|
30456
30812
|
``,
|
|
30457
|
-
`hot-updater: set ${
|
|
30458
|
-
`CodePush: set ${
|
|
30813
|
+
`hot-updater: set ${import_picocolors15.default.cyan("HOT_UPDATER_PUBLIC_KEY")} (iOS Info.plist) / ${import_picocolors15.default.cyan("com.hotupdater.PUBLIC_KEY")} (Android) to ${DEFAULT_PUBLIC_KEY}.`,
|
|
30814
|
+
`CodePush: set ${import_picocolors15.default.cyan("CodePushPublicKey")} (iOS) / ${import_picocolors15.default.cyan("CodePushPublicKey")} string resource (Android) to ${DEFAULT_PUBLIC_KEY}.`
|
|
30459
30815
|
].join("\n"),
|
|
30460
30816
|
"Next"
|
|
30461
30817
|
);
|
|
@@ -30486,13 +30842,13 @@ var contentTypes = {
|
|
|
30486
30842
|
mp4: "video/mp4"
|
|
30487
30843
|
};
|
|
30488
30844
|
function file(dir, rel, virtualPath, contentType) {
|
|
30489
|
-
const data =
|
|
30845
|
+
const data = readFileSync12(join9(dir, rel));
|
|
30490
30846
|
return { path: virtualPath, abs: join9(dir, rel), sha256: createHash4("sha256").update(data).digest("hex"), sizeBytes: data.length, contentType };
|
|
30491
30847
|
}
|
|
30492
30848
|
async function exportBundles(platforms) {
|
|
30493
30849
|
const out = mkdtempSync(join9(tmpdir(), "liftbay-export-"));
|
|
30494
30850
|
await run("npx", ["expo", "export", "--output-dir", out, "--dump-assetmap", ...platforms.flatMap((pl) => ["--platform", pl])]);
|
|
30495
|
-
const meta = JSON.parse(
|
|
30851
|
+
const meta = JSON.parse(readFileSync12(join9(out, "metadata.json"), "utf8"));
|
|
30496
30852
|
const bundles = {};
|
|
30497
30853
|
for (const pl of platforms) {
|
|
30498
30854
|
const m = meta.fileMetadata[pl];
|
|
@@ -30531,7 +30887,7 @@ async function publish(opts) {
|
|
|
30531
30887
|
return null;
|
|
30532
30888
|
}
|
|
30533
30889
|
})();
|
|
30534
|
-
intro(
|
|
30890
|
+
intro(import_picocolors16.default.bgCyan(import_picocolors16.default.black(" liftbay update publish ")));
|
|
30535
30891
|
if (!platforms.every((x) => x === "ios" || x === "android")) fail("--platform must be ios, android or all");
|
|
30536
30892
|
log.step(`Exporting ${platforms.join(" and ")} bundles`);
|
|
30537
30893
|
const { out, bundles } = await exportBundles(platforms);
|
|
@@ -30552,14 +30908,14 @@ async function publish(opts) {
|
|
|
30552
30908
|
let done = 0;
|
|
30553
30909
|
for (const m of prep.missing) {
|
|
30554
30910
|
const f = bySha.get(m.sha256);
|
|
30555
|
-
spin.message(`Uploading ${++done}/${prep.missing.length}: ${f.path}`);
|
|
30556
|
-
const res = await fetch(m.uploadUrl, { method: "PUT", body:
|
|
30911
|
+
spin.message(fit(`Uploading ${++done}/${prep.missing.length}: ${f.path}`));
|
|
30912
|
+
const res = await fetch(m.uploadUrl, { method: "PUT", body: readFileSync12(f.abs), headers: { "Content-Type": f.contentType } });
|
|
30557
30913
|
if (!res.ok) fail(`Upload failed for ${f.path} (${res.status})`);
|
|
30558
30914
|
}
|
|
30559
30915
|
spin.stop(`${all.length} files: ${prep.missing.length} uploaded, ${prep.alreadyStored} already stored`);
|
|
30560
30916
|
const key = loadPrivateKey(opts.privateKey);
|
|
30561
30917
|
const published = [];
|
|
30562
|
-
if (key) log.info(`Signing with ${
|
|
30918
|
+
if (key) log.info(`Signing with ${import_picocolors16.default.cyan(key.source)}`);
|
|
30563
30919
|
for (const [runtime, group] of groups) {
|
|
30564
30920
|
const body = {
|
|
30565
30921
|
message,
|
|
@@ -30585,11 +30941,11 @@ async function publish(opts) {
|
|
|
30585
30941
|
if (notice) log.warn(notice);
|
|
30586
30942
|
published.push({ id: release.id, channel, runtimeVersion: runtime, platforms: group, rolloutPercent: release.rolloutPercent, signed: release.signed, message, notice });
|
|
30587
30943
|
log.success(
|
|
30588
|
-
`Published to ${
|
|
30944
|
+
`Published to ${import_picocolors16.default.bold(channel)} \xB7 runtime ${runtime} \xB7 ${group.join(", ")}${release.rolloutPercent < 100 ? ` \xB7 ${release.rolloutPercent}% rollout` : ""}${release.signed ? " \xB7 signed" : ""}`
|
|
30589
30945
|
);
|
|
30590
30946
|
}
|
|
30591
30947
|
const url = `${dashboardUrl()}/${config.org}/${config.project}/updates/${channel}`;
|
|
30592
|
-
outro(
|
|
30948
|
+
outro(import_picocolors16.default.dim(url));
|
|
30593
30949
|
return { releases: published, url };
|
|
30594
30950
|
} finally {
|
|
30595
30951
|
rmSync4(out, { recursive: true, force: true });
|
|
@@ -30601,8 +30957,8 @@ async function list(opts) {
|
|
|
30601
30957
|
const releases = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases", { params: { path: { org: config.org, project: config.project, channel } } }));
|
|
30602
30958
|
if (!releases.length) console.log(`No releases on ${channel} yet.`);
|
|
30603
30959
|
for (const r2 of releases.slice(0, 20)) {
|
|
30604
|
-
const status2 = r2.status === "active" ?
|
|
30605
|
-
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status2.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${
|
|
30960
|
+
const status2 = r2.status === "active" ? import_picocolors16.default.green("active") : r2.status === "rolled_back" ? import_picocolors16.default.red("rolled back") : import_picocolors16.default.dim("superseded");
|
|
30961
|
+
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status2.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${import_picocolors16.default.dim(r2.runtimeVersion.padEnd(10))} ${r2.message}`);
|
|
30606
30962
|
}
|
|
30607
30963
|
return { channel, releases: releases.slice(0, 20) };
|
|
30608
30964
|
}
|
|
@@ -30617,9 +30973,9 @@ async function rollback(opts) {
|
|
|
30617
30973
|
const { path: path2, active } = await activeRelease(opts.channel ?? "production");
|
|
30618
30974
|
const request = client().POST("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollback", { params: { path: { ...path2, release: active.id } } });
|
|
30619
30975
|
const res = await unwrap(request);
|
|
30620
|
-
console.log(`${
|
|
30976
|
+
console.log(`${import_picocolors16.default.green("\u2714")} Rolled back "${active.message}". ${res.active ? "The previous release is active again." : "Devices return to the bundle built into the app."}`);
|
|
30621
30977
|
const notice = (await request).response.headers.get("Liftbay-Notice");
|
|
30622
|
-
if (notice) console.log(`${
|
|
30978
|
+
if (notice) console.log(`${import_picocolors16.default.yellow("!")} ${notice}`);
|
|
30623
30979
|
return { rolledBack: { id: active.id, message: active.message }, active: res.active, notice };
|
|
30624
30980
|
}
|
|
30625
30981
|
async function rollout(percent, opts) {
|
|
@@ -30627,18 +30983,18 @@ async function rollout(percent, opts) {
|
|
|
30627
30983
|
const r2 = await unwrap(
|
|
30628
30984
|
client().PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollout", { params: { path: { ...path2, release: active.id } }, body: { rolloutPercent: Number(percent) } })
|
|
30629
30985
|
);
|
|
30630
|
-
console.log(`${
|
|
30986
|
+
console.log(`${import_picocolors16.default.green("\u2714")} "${r2.message}" now rolls out to ${r2.rolloutPercent}%`);
|
|
30631
30987
|
return { id: r2.id, message: r2.message, rolloutPercent: r2.rolloutPercent };
|
|
30632
30988
|
}
|
|
30633
30989
|
async function channels() {
|
|
30634
30990
|
const config = requireConfig();
|
|
30635
30991
|
const list2 = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: { org: config.org, project: config.project } } }));
|
|
30636
|
-
for (const c2 of list2) console.log(`${
|
|
30992
|
+
for (const c2 of list2) console.log(`${import_picocolors16.default.bold(c2.name.padEnd(16))} env ${c2.environmentSlug.padEnd(12)} ${c2.protocols.join(", ").padEnd(32)} ${import_picocolors16.default.dim(c2.currentRelease?.message ?? "no active release")}`);
|
|
30637
30993
|
return { channels: list2 };
|
|
30638
30994
|
}
|
|
30639
30995
|
|
|
30640
30996
|
// src/index.ts
|
|
30641
|
-
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.
|
|
30997
|
+
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.7.0").option("--json", "print the result as JSON on stdout (progress goes to stderr; never prompts). Also LIFTBAY_JSON=1").hook("preAction", () => {
|
|
30642
30998
|
if (program2.opts().json || process.env.LIFTBAY_JSON === "1") enableJson();
|
|
30643
30999
|
});
|
|
30644
31000
|
var wrap = (fn) => async (...args) => {
|
|
@@ -30649,13 +31005,15 @@ var wrap = (fn) => async (...args) => {
|
|
|
30649
31005
|
}
|
|
30650
31006
|
};
|
|
30651
31007
|
var setupOptions = (cmd2) => cmd2.option("--org <slug>", "organization").option("--project <slug>", "project slug (created if it doesn't exist)").option("--platforms <list>", "ios, android or all").option("--runtime-policy <policy>", "appVersion or fingerprint").option("--ios-engine <engine>", "project setting: where iOS store builds run, github-actions or xcode-cloud").option("--no-eas", "don't use eas.json's build profiles and env, even if there is one").option("-y, --yes", "accept defaults, no prompts").option("--api-url <url>", "API URL (for self-hosted or local development)").option("--dashboard-url <url>", "dashboard URL");
|
|
30652
|
-
program2.command("login").description("Sign in through the browser").option("--api-url <url>", "API URL").option("--dashboard-url <url>", "dashboard URL").option("--no-open", "don't open the browser").option("--no-wait", "start the sign-in and exit; run `liftbay login` again after approving to finish (for agents)").action(wrap(login));
|
|
31008
|
+
program2.command("login").description("Sign in through the browser").option("--api-url <url>", "API URL").option("--dashboard-url <url>", "dashboard URL").option("--no-open", "don't open the browser").option("--no-wait", "start the sign-in and exit; run `liftbay login` again after approving to finish (for agents)").option("--email <address>", "sign in with a code sent to this email, in the terminal (creates the account the first time)").option("--code <code>", "with --email: the 6-digit code from the email").action(wrap(login));
|
|
30653
31009
|
program2.command("logout").description("Sign out and remove the saved token").action(wrap(logout));
|
|
30654
31010
|
program2.command("whoami").description("Show the signed-in user and organizations").action(wrap(whoami));
|
|
30655
|
-
setupOptions(
|
|
31011
|
+
setupOptions(
|
|
31012
|
+
program2.command("init").description("Set up this app: link the project, builds, then OTA updates").option("--no-ota", "skip OTA updates").option("--new-org <name>", "create an organization with this name").option("--project-name <name>", "create the project with this name").option("--questions", "list every question init would ask, with the choices for this app, and exit (for agents: interview the person)").option("--answers <file>", "run with the answers in this JSON file, without any prompts (see --questions)")
|
|
31013
|
+
).action(wrap(initCommand));
|
|
30656
31014
|
var ci = program2.command("ci").description("Builds");
|
|
30657
31015
|
setupOptions(ci.command("init").description("Set up build profiles, environments and app identifiers")).action(wrap(ciInitCommand));
|
|
30658
|
-
program2.command("build").description("Build on your GitHub Actions (Android on Linux, iOS on macOS) and follow the logs").option("--platform <platform>", "ios, android or all (asked in a terminal; otherwise the profile's platforms)").option("--profile <name>", "build profile from liftbay.json (asked in a terminal; otherwise production)").option("--ref <ref>", "branch, tag or commit to build (default: current branch)").option("-m, --message <text>", "build message").option("--simulator", "iOS: build for the simulator instead of a signed .ipa").option("--no-wait", "queue and exit without following the logs").option("--submit", "submit store builds when they succeed: TestFlight, or a Google Play track").option("--track <track>", "Play track for --submit: internal, alpha, beta or production").option("--engine <engine>", "where this iOS store build runs: github-actions or xcode-cloud (asked in a terminal; otherwise the project's setting)").action(wrap(build));
|
|
31016
|
+
program2.command("build").description("Build on your GitHub Actions (Android on Linux, iOS on macOS) and follow the logs").option("--platform <platform>", "ios, android or all (asked in a terminal; otherwise the profile's platforms)").option("--profile <name>", "build profile from liftbay.json (asked in a terminal; otherwise production)").option("--ref <ref>", "branch, tag or commit to build (default: current branch)").option("-m, --message <text>", "build message").option("--simulator", "iOS: build for the simulator instead of a signed .ipa").option("--no-wait", "queue and exit without following the build").option("--logs", "show the whole build log as it runs, instead of just the steps").option("--submit", "submit store builds when they succeed: TestFlight, or a Google Play track").option("--track <track>", "Play track for --submit: internal, alpha, beta or production").option("--engine <engine>", "where this iOS store build runs: github-actions or xcode-cloud (asked in a terminal; otherwise the project's setting)").action(wrap(build));
|
|
30659
31017
|
var builds = program2.command("builds").description("Look at builds: list, status, logs, artifacts");
|
|
30660
31018
|
builds.command("list").description("Recent builds, newest first").option("--platform <platform>", "ios or android").option("--status <list>", "comma-separated: queued, preparing, running, uploading, succeeded, failed, cancelled").option("--limit <n>", "how many", "20").action(wrap(buildsList));
|
|
30661
31019
|
builds.command("view <number>").description("Status, phases and artifacts of one build").action(wrap(buildsView));
|