@htgabriel/release-kit 0.1.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 ADDED
@@ -0,0 +1,1881 @@
1
+ // src/exec/run.ts
2
+ import crossSpawn from "cross-spawn";
3
+ var ReleaseKitError = class extends Error {
4
+ code;
5
+ constructor(message, { code = 1 } = {}) {
6
+ super(message);
7
+ this.name = "ReleaseKitError";
8
+ this.code = code;
9
+ }
10
+ };
11
+ function fail(message, code = 1) {
12
+ throw new ReleaseKitError(message, { code });
13
+ }
14
+ function formatCommand(command, args, env = {}) {
15
+ const exported = Object.entries(env).filter(([key, value]) => value !== void 0 && (key === "APP_VARIANT" || key.startsWith("EAS_"))).map(([key, value]) => `${key}=${value}`).join(" ");
16
+ const body = [command, ...args].join(" ");
17
+ return exported ? `${exported} ${body}` : body;
18
+ }
19
+ var defaultRun = (command, args, { cwd, env, inherit = false, dryRun = false } = {}) => {
20
+ const extraEnv = env ?? {};
21
+ if (dryRun) {
22
+ return {
23
+ status: 0,
24
+ signal: null,
25
+ stdout: "",
26
+ stderr: "",
27
+ dryRun: true,
28
+ printed: formatCommand(command, args, extraEnv)
29
+ };
30
+ }
31
+ const result = crossSpawn.sync(command, args, {
32
+ cwd,
33
+ env: { ...process.env, ...extraEnv },
34
+ encoding: "utf8",
35
+ stdio: inherit ? "inherit" : "pipe",
36
+ maxBuffer: 32 * 1024 * 1024
37
+ });
38
+ return {
39
+ status: result.status ?? 1,
40
+ signal: result.signal ?? null,
41
+ stdout: result.stdout ?? "",
42
+ stderr: result.stderr ?? "",
43
+ error: result.error
44
+ };
45
+ };
46
+ function requireSuccess(result, label) {
47
+ if (result.dryRun) return result;
48
+ if (result.status === 0) return result;
49
+ const how = result.signal ? `sinal ${result.signal}` : `exit ${result.status}`;
50
+ const detail = (result.stderr || result.stdout || result.error?.message || "").trim();
51
+ fail(`${label} falhou (${how}).${detail ? `
52
+ ${detail}` : ""}`);
53
+ }
54
+
55
+ // src/ui/log.ts
56
+ import pc from "picocolors";
57
+ function createLogger(options = {}) {
58
+ const write = options.write ?? ((line) => console.error(line));
59
+ const silent = Boolean(options.json);
60
+ const emit = (line) => {
61
+ if (silent) return;
62
+ write(line);
63
+ };
64
+ return {
65
+ info: (message) => emit(message),
66
+ warn: (message) => emit(pc.yellow(`! ${message}`)),
67
+ error: (message) => emit(pc.red(`\u2717 ${message}`)),
68
+ success: (message) => emit(pc.green(`\u2713 ${message}`)),
69
+ step: (message) => emit(pc.cyan(`\u2192 ${message}`)),
70
+ debug: (message) => {
71
+ if (!options.verbose) return;
72
+ emit(pc.dim(message));
73
+ }
74
+ };
75
+ }
76
+
77
+ // src/context.ts
78
+ function createContext(overrides) {
79
+ const env = overrides.env ?? process.env;
80
+ const json = Boolean(overrides.json);
81
+ return {
82
+ repoRoot: overrides.repoRoot,
83
+ appDir: overrides.appDir ?? overrides.repoRoot,
84
+ releaseDir: overrides.releaseDir ?? `${overrides.repoRoot}/.release`,
85
+ env,
86
+ dryRun: Boolean(overrides.dryRun),
87
+ json,
88
+ verbose: Boolean(overrides.verbose),
89
+ nonInteractive: overrides.nonInteractive ?? !process.stdin.isTTY,
90
+ run: overrides.run ?? defaultRun,
91
+ log: overrides.log ?? createLogger({ json, verbose: overrides.verbose }),
92
+ confirm: overrides.confirm
93
+ };
94
+ }
95
+
96
+ // src/config/schema.ts
97
+ import { z } from "zod";
98
+ var platformSchema = z.enum(["android", "ios"]);
99
+ var versionSourceSchema = z.enum(["app.json", "package.json"]);
100
+ var gitPolicySchema = z.object({
101
+ releaseBranch: z.string().default("main"),
102
+ tagFormat: z.string().default("v{version}"),
103
+ requireClean: z.boolean().default(true),
104
+ requireSyncedWithRemote: z.boolean().default(true),
105
+ remote: z.string().default("origin")
106
+ });
107
+ var iosChannelSchema = z.object({
108
+ bundleId: z.string(),
109
+ ascAppId: z.string().optional(),
110
+ appleTeamId: z.string().optional(),
111
+ phasedRelease: z.boolean().default(false),
112
+ automaticRelease: z.boolean().default(false),
113
+ copyright: z.string().optional()
114
+ });
115
+ var androidChannelSchema = z.object({
116
+ package: z.string(),
117
+ testTrack: z.string().default("internal"),
118
+ productionTrack: z.string().default("production"),
119
+ initialRollout: z.number().min(0).max(1).default(1)
120
+ });
121
+ var otaSchema = z.object({
122
+ channel: z.string()
123
+ });
124
+ var channelSchema = z.object({
125
+ /** Canal protegido (produção): exige --i-know-this-is-production ou confirmação digitando a versão. */
126
+ protected: z.boolean().default(false),
127
+ /** Se false, o canal nunca envia à loja (ex: build local de sideload). */
128
+ store: z.boolean().default(true),
129
+ buildProfile: z.string(),
130
+ submitProfile: z.string().optional(),
131
+ /** Nome do "environment" do EAS (eas.json build.<profile>.environment) — usado nos checks de fingerprint/Sentry. */
132
+ easEnvironment: z.string().optional(),
133
+ /** Se o build profile já reusa o build number/versionCode (eas.json autoIncrement). */
134
+ autoIncrement: z.boolean().default(true),
135
+ /** Profiles com upload de sourcemap desligado (ex: SENTRY_DISABLE_AUTO_UPLOAD) não exigem token no doctor. */
136
+ sentryUploadDisabled: z.boolean().default(false),
137
+ env: z.record(z.string(), z.string()).default({}),
138
+ ios: iosChannelSchema.optional(),
139
+ android: androidChannelSchema.optional(),
140
+ ota: otaSchema.optional()
141
+ });
142
+ var toolchainBaselineSchema = z.object({
143
+ easCli: z.object({ min: z.string(), tested: z.string() }).optional(),
144
+ node: z.object({ min: z.string(), tested: z.string() }).optional(),
145
+ jdk: z.object({ min: z.string(), tested: z.string() }).optional(),
146
+ xcode: z.object({ min: z.string(), tested: z.string() }).optional(),
147
+ cocoapods: z.object({ min: z.string(), tested: z.string() }).optional()
148
+ });
149
+ var doctorConfigSchema = z.object({
150
+ sentry: z.boolean().default(false),
151
+ expoDoctor: z.boolean().default(true)
152
+ });
153
+ var releaseKitConfigSchema = z.object({
154
+ $schema: z.string().optional(),
155
+ appDir: z.string().default("."),
156
+ /** Conta/organização esperada no `eas whoami` (opcional; reforça o doctor). */
157
+ easOwner: z.string().optional(),
158
+ platforms: z.array(platformSchema).min(1),
159
+ locales: z.array(z.string()).min(1).default(["pt-BR"]),
160
+ notesDir: z.string().default("release-notes"),
161
+ version: z.object({ source: versionSourceSchema.default("app.json") }).default({ source: "app.json" }),
162
+ git: gitPolicySchema.default({}),
163
+ channels: z.record(z.string(), channelSchema).refine((channels) => Object.keys(channels).length > 0, {
164
+ message: 'defina ao menos um canal em "channels"'
165
+ }),
166
+ toolchain: toolchainBaselineSchema.default({}),
167
+ doctor: doctorConfigSchema.default({})
168
+ });
169
+ function parseConfig(input) {
170
+ return releaseKitConfigSchema.parse(input);
171
+ }
172
+ function channelOf(config, name) {
173
+ const channel = config.channels[name];
174
+ if (!channel) {
175
+ const available = Object.keys(config.channels).join(", ");
176
+ throw new Error(`canal desconhecido: "${name}". Canais dispon\xEDveis: ${available}`);
177
+ }
178
+ return channel;
179
+ }
180
+ function protectedChannelNames(config) {
181
+ return Object.entries(config.channels).filter(([, channel]) => channel.protected).map(([name]) => name);
182
+ }
183
+
184
+ // src/config/load.ts
185
+ import { existsSync, readFileSync, writeFileSync } from "fs";
186
+ import { join } from "path";
187
+ import { z as z2 } from "zod";
188
+ var CONFIG_FILENAME = "release-kit.config.json";
189
+ function configPath(repoRoot) {
190
+ return join(repoRoot, CONFIG_FILENAME);
191
+ }
192
+ function configExists(repoRoot) {
193
+ return existsSync(configPath(repoRoot));
194
+ }
195
+ function loadConfig(repoRoot) {
196
+ const path = configPath(repoRoot);
197
+ if (!existsSync(path)) {
198
+ fail(
199
+ `${CONFIG_FILENAME} n\xE3o encontrado em ${repoRoot}. Rode \`release-kit init\` para criar a configura\xE7\xE3o.`
200
+ );
201
+ }
202
+ let raw;
203
+ try {
204
+ raw = JSON.parse(readFileSync(path, "utf8"));
205
+ } catch (error) {
206
+ fail(`${CONFIG_FILENAME} n\xE3o \xE9 um JSON v\xE1lido: ${error.message}`);
207
+ }
208
+ try {
209
+ return parseConfig(raw);
210
+ } catch (error) {
211
+ if (error instanceof z2.ZodError) {
212
+ const issues = error.issues.map((issue) => ` - ${issue.path.join(".") || "(raiz)"}: ${issue.message}`);
213
+ fail(`${CONFIG_FILENAME} inv\xE1lido:
214
+ ${issues.join("\n")}`);
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+ function writeConfig(repoRoot, config) {
220
+ const path = configPath(repoRoot);
221
+ const withSchema = { $schema: "./node_modules/@htgabriel/release-kit/schema.json", ...config };
222
+ writeFileSync(path, `${JSON.stringify(withSchema, null, 2)}
223
+ `);
224
+ return path;
225
+ }
226
+
227
+ // src/git/policy.ts
228
+ function git(ctx, args, { allowFail = false } = {}) {
229
+ const result = ctx.run("git", args, { cwd: ctx.repoRoot, inherit: false });
230
+ if (!allowFail) requireSuccess(result, `git ${args.join(" ")}`);
231
+ return result;
232
+ }
233
+ function gitPorcelain(ctx) {
234
+ return git(ctx, ["status", "--porcelain"]).stdout;
235
+ }
236
+ function isWorkingTreeClean(ctx, ignoreDirs = [".release/"]) {
237
+ const dirty = gitPorcelain(ctx).split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).filter((line) => !ignoreDirs.some((dir) => line.endsWith(` ${dir}`) || line.includes(` ${dir}`)));
238
+ return dirty.length === 0;
239
+ }
240
+ function dirtyFiles(ctx, ignoreDirs = [".release/"]) {
241
+ return gitPorcelain(ctx).split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).filter((line) => !ignoreDirs.some((dir) => line.endsWith(` ${dir}`) || line.includes(` ${dir}`)));
242
+ }
243
+ function currentBranch(ctx) {
244
+ return git(ctx, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout.trim();
245
+ }
246
+ function headSha(ctx) {
247
+ return git(ctx, ["rev-parse", "HEAD"]).stdout.trim();
248
+ }
249
+ function remoteSha(ctx, remote, branch) {
250
+ const result = git(ctx, ["rev-parse", `${remote}/${branch}`], { allowFail: true });
251
+ if (result.status !== 0) return null;
252
+ return result.stdout.trim();
253
+ }
254
+ function tagShaFormatted(ctx, tagFormat, version) {
255
+ const tag = renderTag(tagFormat, version);
256
+ const result = git(ctx, ["rev-parse", `${tag}^{commit}`], { allowFail: true });
257
+ if (result.status !== 0) return null;
258
+ return result.stdout.trim();
259
+ }
260
+ function lastTagMatching(ctx, tagFormat) {
261
+ const pattern = tagGlob(tagFormat);
262
+ const result = git(ctx, ["tag", "--list", pattern, "--sort=-creatordate"], { allowFail: true });
263
+ const [first] = (result.stdout || "").split("\n").map((line) => line.trim()).filter(Boolean);
264
+ return first ?? null;
265
+ }
266
+ function aheadBehind(ctx, remote, branch) {
267
+ const upstream = git(ctx, ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`], { allowFail: true });
268
+ if (upstream.status !== 0) return null;
269
+ const counts = git(ctx, ["rev-list", "--left-right", "--count", `HEAD...${remote}/${branch}`], {
270
+ allowFail: true
271
+ });
272
+ if (counts.status !== 0) return null;
273
+ const [ahead = "0", behind = "0"] = counts.stdout.trim().split(/\s+/);
274
+ return { ahead: Number(ahead), behind: Number(behind) };
275
+ }
276
+ function renderTag(tagFormat, version) {
277
+ return tagFormat.replace("{version}", version);
278
+ }
279
+ function tagGlob(tagFormat) {
280
+ return tagFormat.replace("{version}", "*");
281
+ }
282
+ function checkGitPolicy(ctx, policy, { version, isProtectedChannel }) {
283
+ const checks = [];
284
+ const branch = currentBranch(ctx);
285
+ if (policy.requireClean) {
286
+ const dirty = dirtyFiles(ctx);
287
+ if (dirty.length) {
288
+ checks.push({ name: "git-clean", status: "fail", message: `working tree suja:
289
+ ${dirty.join("\n")}` });
290
+ } else {
291
+ checks.push({ name: "git-clean", status: "pass", message: "working tree limpa" });
292
+ }
293
+ }
294
+ if (isProtectedChannel) {
295
+ if (branch !== policy.releaseBranch) {
296
+ checks.push({
297
+ name: "git-branch",
298
+ status: "fail",
299
+ message: `canal protegido exige branch ${policy.releaseBranch} (atual: ${branch}).`
300
+ });
301
+ } else {
302
+ checks.push({ name: "git-branch", status: "pass", message: `branch ${branch}` });
303
+ }
304
+ const head = headSha(ctx);
305
+ const expectedTag = renderTag(policy.tagFormat, version);
306
+ const tagSha = tagShaFormatted(ctx, policy.tagFormat, version);
307
+ if (!tagSha) {
308
+ checks.push({ name: "git-tag", status: "fail", message: `tag ${expectedTag} n\xE3o existe (vers\xE3o atual: ${version}).` });
309
+ } else if (tagSha !== head) {
310
+ checks.push({
311
+ name: "git-tag",
312
+ status: "fail",
313
+ message: `tag ${expectedTag} (${tagSha.slice(0, 8)}) n\xE3o aponta para HEAD (${head.slice(0, 8)}).`
314
+ });
315
+ } else {
316
+ checks.push({ name: "git-tag", status: "pass", message: `${expectedTag} == HEAD` });
317
+ }
318
+ if (policy.requireSyncedWithRemote) {
319
+ const remote = remoteSha(ctx, policy.remote, policy.releaseBranch);
320
+ if (!remote) {
321
+ checks.push({
322
+ name: "git-sync",
323
+ status: "fail",
324
+ message: `${policy.remote}/${policy.releaseBranch} ausente. Rode git fetch ${policy.remote} ${policy.releaseBranch}.`
325
+ });
326
+ } else if (remote !== head) {
327
+ checks.push({
328
+ name: "git-sync",
329
+ status: "fail",
330
+ message: `HEAD (${head.slice(0, 8)}) \u2260 ${policy.remote}/${policy.releaseBranch} (${remote.slice(0, 8)}).`
331
+ });
332
+ } else {
333
+ checks.push({ name: "git-sync", status: "pass", message: `HEAD == ${policy.remote}/${policy.releaseBranch}` });
334
+ }
335
+ }
336
+ return checks;
337
+ }
338
+ if (policy.requireSyncedWithRemote) {
339
+ const status = aheadBehind(ctx, policy.remote, branch);
340
+ if (!status) {
341
+ checks.push({ name: "git-sync", status: "fail", message: `branch ${branch} sem upstream. Rode git push -u.` });
342
+ } else if (status.behind > 0) {
343
+ checks.push({ name: "git-sync", status: "fail", message: `branch atrasada ${status.behind} commit(s) em rela\xE7\xE3o ao upstream.` });
344
+ } else if (status.ahead > 0) {
345
+ checks.push({ name: "git-sync", status: "fail", message: `branch adiantada ${status.ahead} commit(s) \u2014 fa\xE7a push antes de buildar.` });
346
+ } else {
347
+ checks.push({ name: "git-sync", status: "pass", message: "sincronizada com o upstream" });
348
+ }
349
+ }
350
+ return checks;
351
+ }
352
+
353
+ // src/git/commits.ts
354
+ var CONVENTIONAL_RE = /^(\w+)(?:\(([^)]+)\))?!?:\s*(.+)$/;
355
+ function parseSubject(subject) {
356
+ const match = subject.match(CONVENTIONAL_RE);
357
+ if (!match) return { type: null, scope: null, description: subject };
358
+ const [, type, scope, description] = match;
359
+ return { type: type ?? null, scope: scope ?? null, description: description ?? subject };
360
+ }
361
+ function commitsSinceLastTag(ctx, tagFormat) {
362
+ const lastTag = lastTagMatching(ctx, tagFormat);
363
+ const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
364
+ const result = git(ctx, ["log", range, "--pretty=format:%h %s"], { allowFail: true });
365
+ if (result.status !== 0) return [];
366
+ return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
367
+ const [sha, ...rest] = line.split(" ");
368
+ const subject = rest.join(" ");
369
+ const parsed = parseSubject(subject);
370
+ return { sha: sha ?? "", subject, ...parsed };
371
+ });
372
+ }
373
+ var USER_FACING_TYPES = /* @__PURE__ */ new Set(["feat", "fix"]);
374
+ function userFacingCommits(commits) {
375
+ return commits.filter((commit) => commit.type && USER_FACING_TYPES.has(commit.type));
376
+ }
377
+
378
+ // src/project/version.ts
379
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
380
+ import { join as join2 } from "path";
381
+ import semver from "semver";
382
+ function loadVersionFile(appDir, source) {
383
+ const path = join2(appDir, source);
384
+ if (!existsSync2(path)) fail(`${source} n\xE3o encontrado em ${appDir}.`);
385
+ const raw = readFileSync2(path, "utf8");
386
+ const json = JSON.parse(raw);
387
+ if (source === "app.json") {
388
+ return {
389
+ path,
390
+ json,
391
+ get: () => {
392
+ const expo = json.expo;
393
+ const version = expo?.version;
394
+ if (typeof version !== "string") fail(`app.json n\xE3o tem expo.version.`);
395
+ return version;
396
+ },
397
+ set: (next) => {
398
+ const expo = json.expo ?? {};
399
+ expo.version = next;
400
+ json.expo = expo;
401
+ }
402
+ };
403
+ }
404
+ return {
405
+ path,
406
+ json,
407
+ get: () => {
408
+ const version = json.version;
409
+ if (typeof version !== "string") fail(`package.json n\xE3o tem "version".`);
410
+ return version;
411
+ },
412
+ set: (next) => {
413
+ json.version = next;
414
+ }
415
+ };
416
+ }
417
+ function readAppVersion(appDir, config) {
418
+ return loadVersionFile(appDir, config.version.source).get();
419
+ }
420
+ function nextVersion(current, bump) {
421
+ if (bump === "patch" || bump === "minor" || bump === "major") {
422
+ const next = semver.inc(current, bump);
423
+ if (!next) fail(`n\xE3o foi poss\xEDvel incrementar "${bump}" a partir de ${current}.`);
424
+ return next;
425
+ }
426
+ if (!semver.valid(bump)) fail(`vers\xE3o inv\xE1lida: "${bump}". Use patch | minor | major | x.y.z.`);
427
+ if (semver.lte(bump, current)) {
428
+ fail(`${bump} n\xE3o \xE9 maior que a vers\xE3o atual (${current}).`);
429
+ }
430
+ return bump;
431
+ }
432
+ function writeAppVersion(appDir, config, next) {
433
+ const file = loadVersionFile(appDir, config.version.source);
434
+ file.set(next);
435
+ writeFileSync2(file.path, `${JSON.stringify(file.json, null, " ")}
436
+ `);
437
+ return file.path;
438
+ }
439
+
440
+ // src/doctor/checks.ts
441
+ import { existsSync as existsSync4 } from "fs";
442
+ import { join as join4 } from "path";
443
+
444
+ // src/project/detect.ts
445
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
446
+ import { join as join3 } from "path";
447
+ var LOCKFILES = {
448
+ yarn: "yarn.lock",
449
+ pnpm: "pnpm-lock.yaml",
450
+ npm: "package-lock.json"
451
+ };
452
+ function detectPackageManager(repoRoot) {
453
+ for (const [pm, lockfile] of Object.entries(LOCKFILES)) {
454
+ if (existsSync3(join3(repoRoot, lockfile))) return pm;
455
+ }
456
+ return "npm";
457
+ }
458
+ function readEasJson(appDir) {
459
+ const path = join3(appDir, "eas.json");
460
+ if (!existsSync3(path)) return null;
461
+ try {
462
+ return JSON.parse(readFileSync3(path, "utf8"));
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+
468
+ // src/eas/cli.ts
469
+ function eas(ctx, args, { inherit = false, env = {}, cwd } = {}) {
470
+ return ctx.run("eas", args, {
471
+ cwd: cwd ?? ctx.appDir,
472
+ env,
473
+ inherit,
474
+ dryRun: ctx.dryRun
475
+ });
476
+ }
477
+ function parseRemoteVersion(stdout) {
478
+ const text = String(stdout || "").trim();
479
+ const jsonStart = text.indexOf("{");
480
+ if (jsonStart === -1) return { versionCode: null, buildNumber: null, raw: text };
481
+ try {
482
+ const parsed = JSON.parse(text.slice(jsonStart));
483
+ return {
484
+ versionCode: parsed.versionCode != null ? String(parsed.versionCode) : null,
485
+ buildNumber: parsed.buildNumber != null ? String(parsed.buildNumber) : null,
486
+ raw: parsed
487
+ };
488
+ } catch {
489
+ return { versionCode: null, buildNumber: null, raw: text };
490
+ }
491
+ }
492
+ function projectRemoteVersion(ctx, { platform, buildProfile }) {
493
+ const args = ["build:version:get", "--non-interactive", "--json", "-e", buildProfile, "-p", platform];
494
+ const result = eas(ctx, args);
495
+ if (ctx.dryRun) return { versionCode: null, buildNumber: null, raw: null, dryRun: true };
496
+ requireSuccess(result, "eas build:version:get");
497
+ return parseRemoteVersion(result.stdout);
498
+ }
499
+ function expectedBuildNumber(current, { autoIncrement, platform }) {
500
+ const key = platform === "ios" ? "buildNumber" : "versionCode";
501
+ const currentValue = current[key];
502
+ if (!currentValue) return { key, current: null, expected: null };
503
+ if (!autoIncrement) return { key, current: currentValue, expected: currentValue };
504
+ const numeric = Number(currentValue);
505
+ if (!Number.isFinite(numeric)) return { key, current: currentValue, expected: null };
506
+ return { key, current: currentValue, expected: String(numeric + 1) };
507
+ }
508
+ function easWhoami(ctx) {
509
+ const result = eas(ctx, ["whoami"]);
510
+ if (ctx.dryRun) return { ok: true, account: null, raw: "[dry-run]" };
511
+ if (result.status !== 0) return { ok: false, account: null, raw: result.stderr || result.stdout };
512
+ const account = result.stdout.trim().split("\n").pop() ?? null;
513
+ return { ok: true, account, raw: result.stdout };
514
+ }
515
+ function easEnvList(ctx, environment) {
516
+ return eas(ctx, ["env:list", environment, "--format", "short"]);
517
+ }
518
+ function easEnvListHasKey(ctx, environment, key) {
519
+ const result = easEnvList(ctx, environment);
520
+ if (ctx.dryRun) return true;
521
+ if (result.status !== 0) return false;
522
+ const pattern = new RegExp(`(^|\\n)${key}(\\n|$)`);
523
+ return pattern.test(`${result.stdout}
524
+ ${result.stderr}`);
525
+ }
526
+ function easFingerprintGenerate(ctx, environment) {
527
+ return eas(ctx, ["fingerprint:generate", "--non-interactive", "--json", "--environment", environment]);
528
+ }
529
+ function easFingerprintCompare(ctx, environment, buildId) {
530
+ return eas(ctx, [
531
+ "fingerprint:compare",
532
+ "--non-interactive",
533
+ "--json",
534
+ "--build-id",
535
+ buildId,
536
+ "--environment",
537
+ environment
538
+ ]);
539
+ }
540
+ function easBuild(ctx, opts) {
541
+ const args = ["build", "-p", opts.platform, "--profile", opts.buildProfile, "--non-interactive"];
542
+ if (opts.local) args.push("--local");
543
+ if (opts.output) args.push("--output", opts.output);
544
+ return eas(ctx, args, { inherit: true, env: opts.env });
545
+ }
546
+ function easSubmit(ctx, opts) {
547
+ const args = ["submit", "-p", opts.platform, "--profile", opts.submitProfile, "--path", opts.path, "--non-interactive"];
548
+ return eas(ctx, args, { inherit: true, env: opts.env });
549
+ }
550
+ function easUpdate(ctx, opts) {
551
+ const args = ["update", "--channel", opts.channel, "--non-interactive"];
552
+ if (opts.message) args.push("--message", opts.message);
553
+ if (opts.platform) args.push("--platform", opts.platform);
554
+ return eas(ctx, args, { inherit: true, env: opts.env });
555
+ }
556
+
557
+ // src/doctor/toolchain.ts
558
+ function parseVersion(input) {
559
+ const match = String(input ?? "").replace(/^v/i, "").match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
560
+ if (!match) return null;
561
+ return { major: Number(match[1]), minor: Number(match[2] || 0), patch: Number(match[3] || 0) };
562
+ }
563
+ function cmpVersion(a, b) {
564
+ const pa = parseVersion(a);
565
+ const pb = parseVersion(b);
566
+ if (!pa || !pb) return 0;
567
+ if (pa.major !== pb.major) return pa.major - pb.major;
568
+ if (pa.minor !== pb.minor) return pa.minor - pb.minor;
569
+ return pa.patch - pb.patch;
570
+ }
571
+ function collectToolchain(ctx) {
572
+ const capture = (command, args, pick) => {
573
+ const result = ctx.run(command, args, { inherit: false });
574
+ if (result.status !== 0) return null;
575
+ return pick(`${result.stdout}
576
+ ${result.stderr}`);
577
+ };
578
+ const firstMatch = (text, re) => text.match(re)?.[1] ?? null;
579
+ return {
580
+ easCli: capture("eas", ["--version"], (t) => firstMatch(t, /eas-cli\/(\d+\.\d+\.\d+)/)),
581
+ node: capture("node", ["-v"], (t) => firstMatch(t, /v?(\d+\.\d+\.\d+)/)),
582
+ yarn: capture("yarn", ["-v"], (t) => firstMatch(t, /(\d+\.\d+\.\d+)/)),
583
+ jdk: capture("java", ["-version"], (t) => firstMatch(t, /version "(\d+(?:\.\d+\.\d+)?)/)),
584
+ xcode: capture("xcodebuild", ["-version"], (t) => firstMatch(t, /Xcode (\d+\.\d+)/)),
585
+ xcodeBuild: capture("xcodebuild", ["-version"], (t) => firstMatch(t, /Build version (\S+)/)),
586
+ cocoapods: capture("pod", ["--version"], (t) => firstMatch(t, /(\d+\.\d+\.\d+)/)),
587
+ ndkHome: ctx.env.ANDROID_NDK_HOME || null,
588
+ host: `${process.platform}-${process.arch}`
589
+ };
590
+ }
591
+
592
+ // src/doctor/checks.ts
593
+ var EXPO_PUBLIC_PREFIX = "EXPO_PUBLIC_";
594
+ function check(name, status, message) {
595
+ return { name, status, message };
596
+ }
597
+ function assertShellClean(ctx, checks) {
598
+ const leaked = Object.keys(ctx.env).filter((key) => key.startsWith(EXPO_PUBLIC_PREFIX) && ctx.env[key]);
599
+ if (leaked.length) {
600
+ checks.push(
601
+ check(
602
+ "shell-env",
603
+ "fail",
604
+ `shell tem ${leaked.join(", ")} exportado. Em build local isso vence as env vars do EAS. Unset antes de continuar.`
605
+ )
606
+ );
607
+ return;
608
+ }
609
+ checks.push(check("shell-env", "pass", "nenhum EXPO_PUBLIC_* no shell"));
610
+ }
611
+ function assertSentry(ctx, checks, channel) {
612
+ if (channel?.sentryUploadDisabled) {
613
+ checks.push(check("sentry", "pass", "canal desliga o upload de sourcemap; token n\xE3o \xE9 exigido."));
614
+ return;
615
+ }
616
+ const inShell = Boolean(ctx.env.SENTRY_AUTH_TOKEN);
617
+ if (!channel && !inShell) {
618
+ checks.push(check("sentry", "skip", "passe --channel para verificar SENTRY_AUTH_TOKEN no environment EAS."));
619
+ return;
620
+ }
621
+ if (inShell) {
622
+ checks.push(
623
+ check(
624
+ "sentry",
625
+ "warn",
626
+ "SENTRY_AUTH_TOKEN est\xE1 no shell e, em build local, vence o token do EAS. Prefira unset e deixe o environment EAS (Sensitive) injetar."
627
+ )
628
+ );
629
+ return;
630
+ }
631
+ const environment = channel?.easEnvironment;
632
+ if (environment && easEnvListHasKey(ctx, environment, "SENTRY_AUTH_TOKEN")) {
633
+ checks.push(check("sentry", "pass", `SENTRY_AUTH_TOKEN no environment EAS ${environment}.`));
634
+ return;
635
+ }
636
+ checks.push(
637
+ check(
638
+ "sentry",
639
+ "fail",
640
+ `SENTRY_AUTH_TOKEN ausente no shell e no environment EAS ${environment || "(defina easEnvironment no canal)"}.`
641
+ )
642
+ );
643
+ }
644
+ function assertEasAuth(ctx, checks, easOwner) {
645
+ const result = easWhoami(ctx);
646
+ if (ctx.dryRun) {
647
+ checks.push(check("eas-auth", "pass", "[dry-run] eas whoami"));
648
+ return;
649
+ }
650
+ if (!result.ok) {
651
+ checks.push(check("eas-auth", "fail", `eas whoami falhou. Rode \`eas login\` ou defina EXPO_TOKEN.
652
+ ${result.raw}`));
653
+ return;
654
+ }
655
+ if (easOwner && !result.raw.includes(easOwner)) {
656
+ checks.push(check("eas-auth", "fail", `eas whoami n\xE3o listou a conta ${easOwner}.`));
657
+ return;
658
+ }
659
+ checks.push(check("eas-auth", "pass", `autenticado${result.account ? ` como ${result.account}` : ""}`));
660
+ }
661
+ function assertToolchain(ctx, checks, config, { platform, allowToolchainDrift }) {
662
+ const baseline = config.toolchain;
663
+ const actual = collectToolchain(ctx);
664
+ const drift = [];
665
+ const compare = (name, spec, value, { platformOnly } = {}) => {
666
+ if (!spec) {
667
+ checks.push(check(`toolchain-${name}`, "skip", "sem baseline configurado"));
668
+ return;
669
+ }
670
+ if (platformOnly && platform && platform !== platformOnly) {
671
+ checks.push(check(`toolchain-${name}`, "skip", `n\xE3o exigido para --platform ${platform}`));
672
+ return;
673
+ }
674
+ if (!value) {
675
+ checks.push(check(`toolchain-${name}`, "fail", `${name} n\xE3o encontrado no PATH.`));
676
+ return;
677
+ }
678
+ if (spec.min && cmpVersion(value, spec.min) < 0) {
679
+ checks.push(check(`toolchain-${name}`, "fail", `${name} ${value} < m\xEDnimo ${spec.min}.`));
680
+ return;
681
+ }
682
+ if (spec.tested && cmpVersion(value, spec.tested) > 0) {
683
+ drift.push(`${name} ${value} > testado ${spec.tested}`);
684
+ checks.push(
685
+ check(
686
+ `toolchain-${name}`,
687
+ allowToolchainDrift ? "warn" : "fail",
688
+ `${name} ${value} est\xE1 acima do testado (${spec.tested}). Passe --allow-toolchain-drift para continuar.`
689
+ )
690
+ );
691
+ return;
692
+ }
693
+ checks.push(check(`toolchain-${name}`, "pass", `${name} ${value}`));
694
+ };
695
+ compare("eas-cli", baseline.easCli, actual.easCli);
696
+ compare("node", baseline.node, actual.node);
697
+ compare("jdk", baseline.jdk, actual.jdk, { platformOnly: "android" });
698
+ compare("xcode", baseline.xcode, actual.xcode, { platformOnly: "ios" });
699
+ compare("cocoapods", baseline.cocoapods, actual.cocoapods, { platformOnly: "ios" });
700
+ if (!platform || platform === "android") {
701
+ if (!ctx.env.JAVA_HOME) {
702
+ checks.push(check("java-home", "fail", "JAVA_HOME n\xE3o est\xE1 definido."));
703
+ } else {
704
+ checks.push(check("java-home", "pass", ctx.env.JAVA_HOME));
705
+ }
706
+ if (!ctx.env.ANDROID_HOME && !ctx.env.ANDROID_SDK_ROOT) {
707
+ checks.push(check("android-sdk", "fail", "ANDROID_HOME / ANDROID_SDK_ROOT ausente."));
708
+ } else {
709
+ checks.push(check("android-sdk", "pass", ctx.env.ANDROID_HOME || ctx.env.ANDROID_SDK_ROOT || ""));
710
+ }
711
+ }
712
+ if (platform === "ios" && process.platform !== "darwin") {
713
+ checks.push(check("platform-ios", "fail", "build local de iOS s\xF3 funciona no macOS. Use --cloud ou remova --platform ios."));
714
+ } else if (platform === "android" && process.platform === "win32") {
715
+ checks.push(
716
+ check(
717
+ "platform-android",
718
+ "warn",
719
+ "build local de Android no Windows n\xE3o \xE9 suportado pelo EAS fora do WSL2. Use --cloud ou rode dentro do WSL2."
720
+ )
721
+ );
722
+ }
723
+ return { actual, drift, toolchainDrift: drift.length > 0 };
724
+ }
725
+ function assertLockfileClean(ctx, checks) {
726
+ const pm = detectPackageManager(ctx.repoRoot);
727
+ const lockfile = { yarn: "yarn.lock", pnpm: "pnpm-lock.yaml", npm: "package-lock.json" }[pm];
728
+ const diff = ctx.run("git", ["diff", "--quiet", "HEAD", "--", lockfile], { cwd: ctx.repoRoot, inherit: false });
729
+ if (diff.status !== 0) {
730
+ checks.push(check("lockfile", "fail", `${lockfile} diverge do HEAD. Commit ou restaure antes do build (lockfile congelado).`));
731
+ return;
732
+ }
733
+ checks.push(check("lockfile", "pass", `${lockfile} igual ao HEAD`));
734
+ }
735
+ function assertRemoteVersion(ctx, checks, { platform, channel }) {
736
+ const remote = projectRemoteVersion(ctx, { platform, buildProfile: channel.buildProfile });
737
+ if (remote.dryRun) {
738
+ checks.push(check("remote-version", "pass", "[dry-run] eas build:version:get"));
739
+ return;
740
+ }
741
+ const projection = expectedBuildNumber(remote, { autoIncrement: channel.autoIncrement, platform });
742
+ if (!channel.autoIncrement) {
743
+ checks.push(
744
+ check(
745
+ "remote-version",
746
+ "pass",
747
+ `${projection.key} remoto atual: ${projection.current} (autoIncrement desligado; este build reutiliza o n\xFAmero).`
748
+ )
749
+ );
750
+ return;
751
+ }
752
+ checks.push(
753
+ check(
754
+ "remote-version",
755
+ "pass",
756
+ `${projection.key} remoto atual: ${projection.current} \u2192 esperado neste build: ${projection.expected} (s\xF3 \xE9 reservado quando o EAS iniciar o build).`
757
+ )
758
+ );
759
+ }
760
+ function assertFingerprint(ctx, checks, { channel, deep, buildId }) {
761
+ const environment = channel.easEnvironment;
762
+ if (!environment) {
763
+ checks.push(check("fingerprint", "skip", "canal sem easEnvironment configurado."));
764
+ return;
765
+ }
766
+ if (!deep) {
767
+ checks.push(check("fingerprint", "pass", `compara\xE7\xE3o contra build da nuvem s\xF3 em --deep (environment ${environment}).`));
768
+ return;
769
+ }
770
+ if (ctx.dryRun) {
771
+ checks.push(check("fingerprint", "pass", `[dry-run] eas fingerprint:generate --environment ${environment}`));
772
+ return;
773
+ }
774
+ const generated = easFingerprintGenerate(ctx, environment);
775
+ if (generated.status !== 0) {
776
+ checks.push(check("fingerprint", "fail", `eas fingerprint:generate falhou.
777
+ ${generated.stderr || generated.stdout}`));
778
+ return;
779
+ }
780
+ if (!buildId) {
781
+ checks.push(
782
+ check(
783
+ "fingerprint",
784
+ "pass",
785
+ `fingerprint local gerado. Para comparar com a nuvem: eas fingerprint:compare --build-id <id> --environment ${environment}`
786
+ )
787
+ );
788
+ return;
789
+ }
790
+ const compared = easFingerprintCompare(ctx, environment, buildId);
791
+ if (compared.status !== 0) {
792
+ checks.push(check("fingerprint", "fail", `eas fingerprint:compare falhou.
793
+ ${compared.stderr || compared.stdout}`));
794
+ return;
795
+ }
796
+ checks.push(check("fingerprint", "pass", `fingerprint local vs build ${buildId}`));
797
+ }
798
+ function assertExpoDoctor(ctx, checks, config) {
799
+ if (!config.doctor.expoDoctor) {
800
+ checks.push(check("expo-doctor", "skip", "desligado em doctor.expoDoctor"));
801
+ return;
802
+ }
803
+ if (ctx.dryRun) {
804
+ checks.push(check("expo-doctor", "pass", "[dry-run] npx expo-doctor"));
805
+ return;
806
+ }
807
+ const result = ctx.run("npx", ["expo-doctor"], { cwd: ctx.appDir, inherit: false });
808
+ if (result.status !== 0) {
809
+ const detail = (result.stdout || result.stderr || "").trim();
810
+ checks.push(
811
+ check("expo-doctor", "fail", `expo-doctor saiu com c\xF3digo ${result.status}. Atualize as depend\xEAncias com \`npx expo install\`.${detail ? `
812
+ ${detail}` : ""}`)
813
+ );
814
+ return;
815
+ }
816
+ checks.push(check("expo-doctor", "pass", "expo-doctor ok"));
817
+ }
818
+ function assertEasJsonExists(ctx) {
819
+ if (!existsSync4(join4(ctx.appDir, "eas.json"))) {
820
+ throw new Error(`eas.json n\xE3o encontrado em ${ctx.appDir}. Rode \`eas init\` ou \`release-kit init\`.`);
821
+ }
822
+ }
823
+
824
+ // src/doctor/run.ts
825
+ function runDoctor(ctx, config, opts) {
826
+ assertEasJsonExists(ctx);
827
+ const channel = opts.channel ? channelOf(config, opts.channel) : null;
828
+ if (opts.platform && !config.platforms.includes(opts.platform)) {
829
+ fail(`--platform ${opts.platform} n\xE3o est\xE1 em config.platforms (${config.platforms.join(", ")}).`);
830
+ }
831
+ const checks = [];
832
+ const version = readAppVersion(ctx.appDir, config);
833
+ checks.push(...checkGitPolicy(ctx, config.git, { version, isProtectedChannel: Boolean(channel?.protected) }));
834
+ assertShellClean(ctx, checks);
835
+ if (config.doctor.sentry) {
836
+ assertSentry(ctx, checks, channel);
837
+ } else {
838
+ checks.push({ name: "sentry", status: "skip", message: "desligado em doctor.sentry" });
839
+ }
840
+ assertEasAuth(ctx, checks, config.easOwner);
841
+ const toolchain = assertToolchain(ctx, checks, config, {
842
+ platform: opts.platform,
843
+ allowToolchainDrift: opts.allowToolchainDrift
844
+ });
845
+ assertLockfileClean(ctx, checks);
846
+ if (channel && opts.platform) {
847
+ assertRemoteVersion(ctx, checks, { platform: opts.platform, channel });
848
+ assertFingerprint(ctx, checks, { channel, deep: opts.deep, buildId: opts.buildId });
849
+ } else {
850
+ checks.push({ name: "remote-version", status: "skip", message: "passe --channel e --platform para consultar versionCode/buildNumber" });
851
+ checks.push({ name: "fingerprint", status: "skip", message: "passe --channel para avaliar runtimeVersion" });
852
+ }
853
+ assertExpoDoctor(ctx, checks, config);
854
+ const failed = checks.filter((item) => item.status === "fail");
855
+ return {
856
+ ok: failed.length === 0,
857
+ checks,
858
+ toolchainDrift: toolchain.toolchainDrift
859
+ };
860
+ }
861
+ function requireDoctorPass(report) {
862
+ if (report.ok) return;
863
+ const failed = report.checks.filter((item) => item.status === "fail");
864
+ const summary = failed.map((item) => `- ${item.name}: ${item.message}`).join("\n");
865
+ fail(`doctor falhou:
866
+ ${summary}`);
867
+ }
868
+
869
+ // src/release/build.ts
870
+ import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, unlinkSync } from "fs";
871
+ import { dirname, join as join6, resolve } from "path";
872
+
873
+ // src/eas/profiles.ts
874
+ function resolveBuildProfileChain(easJson, profileName) {
875
+ const build = easJson.build ?? {};
876
+ const profile = build[profileName];
877
+ if (!profile) fail(`build profile "${profileName}" n\xE3o existe no eas.json.`);
878
+ const chain = [profile];
879
+ let cursor = profile;
880
+ const seen = /* @__PURE__ */ new Set([profileName]);
881
+ while (cursor?.extends) {
882
+ const parentName = cursor.extends;
883
+ if (seen.has(parentName)) break;
884
+ seen.add(parentName);
885
+ const parent = build[parentName];
886
+ if (!parent) break;
887
+ chain.push(parent);
888
+ cursor = parent;
889
+ }
890
+ const merged = {};
891
+ for (const step of chain.slice().reverse()) {
892
+ Object.assign(merged, step);
893
+ if (step.android) merged.android = { ...merged.android, ...step.android };
894
+ if (step.ios) merged.ios = { ...merged.ios, ...step.ios };
895
+ }
896
+ return merged;
897
+ }
898
+ function androidBuildTypeExt(easJson, profileName) {
899
+ const resolved = resolveBuildProfileChain(easJson, profileName);
900
+ return resolved.android?.buildType === "apk" ? "apk" : "aab";
901
+ }
902
+
903
+ // src/release/gate.ts
904
+ function assertChannelOptIn(ctx, config, channel, opts) {
905
+ if (!channel.protected) return;
906
+ if (opts.iKnowThisIsProduction) return;
907
+ const version = readAppVersion(ctx.appDir, config);
908
+ if (opts.confirmVersion && opts.confirmVersion.trim() === version) return;
909
+ fail(
910
+ `canal protegido: passe --i-know-this-is-production (n\xE3o-interativo) ou confirme digitando a vers\xE3o exata (${version}). \`release-kit\` sem argumentos nunca resolve para um canal protegido.`
911
+ );
912
+ }
913
+
914
+ // src/release/metadata.ts
915
+ import { createHash } from "crypto";
916
+ import { existsSync as existsSync5, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
917
+ import { join as join5 } from "path";
918
+ function sha256File(path) {
919
+ return createHash("sha256").update(readFileSync4(path)).digest("hex");
920
+ }
921
+ function gitInfo(ctx) {
922
+ const sha = git(ctx, ["rev-parse", "HEAD"]).stdout.trim();
923
+ const branch = git(ctx, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout.trim();
924
+ const tagResult = git(ctx, ["tag", "--points-at", "HEAD"], { allowFail: true });
925
+ const tag = (tagResult.stdout || "").split("\n").map((line) => line.trim()).find((line) => /^v\d/.test(line)) || "";
926
+ return { sha, branch, tag };
927
+ }
928
+ function writeMetadata(dir, payload) {
929
+ mkdirSync(dir, { recursive: true });
930
+ writeFileSync3(join5(dir, "metadata.json"), `${JSON.stringify(payload, null, 2)}
931
+ `);
932
+ return payload;
933
+ }
934
+ function findLatestMetadata(releaseDir, { channel, platform } = {}) {
935
+ if (!existsSync5(releaseDir)) fail(`Nenhum artefato em ${releaseDir}. Rode \`release-kit build\` primeiro.`);
936
+ const entries = readdirSync2(releaseDir).map((name) => {
937
+ const dir = join5(releaseDir, name);
938
+ const metaPath = join5(dir, "metadata.json");
939
+ if (!statSync(dir).isDirectory() || !existsSync5(metaPath)) return null;
940
+ const metadata = JSON.parse(readFileSync4(metaPath, "utf8"));
941
+ if (platform && metadata.platform !== platform) return null;
942
+ if (channel && metadata.channel !== channel) return null;
943
+ return { dir, metaPath, metadata, createdAt: metadata.createdAt || "" };
944
+ }).filter((entry) => entry !== null).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
945
+ const [first] = entries;
946
+ if (!first) {
947
+ fail(`Nenhum metadata.json${platform ? ` para ${platform}` : ""}${channel ? ` (canal ${channel})` : ""} em ${releaseDir}.`);
948
+ }
949
+ return first;
950
+ }
951
+
952
+ // src/release/build.ts
953
+ function artifactExt(ctx, platform, buildProfile) {
954
+ if (platform === "ios") return "ipa";
955
+ const easJson = readEasJson(ctx.appDir);
956
+ if (!easJson) fail(`eas.json n\xE3o encontrado em ${ctx.appDir}.`);
957
+ return androidBuildTypeExt(easJson, buildProfile);
958
+ }
959
+ function buildRelease(ctx, config, opts) {
960
+ const channel = channelOf(config, opts.channel);
961
+ assertChannelOptIn(ctx, config, channel, opts);
962
+ if (!config.platforms.includes(opts.platform)) {
963
+ fail(`--platform ${opts.platform} n\xE3o est\xE1 em config.platforms.`);
964
+ }
965
+ const ext = artifactExt(ctx, opts.platform, channel.buildProfile);
966
+ const staging = opts.output ? resolve(ctx.repoRoot, opts.output) : join6(ctx.releaseDir, `_building-${opts.platform}-${opts.channel}.${ext}`);
967
+ const local = !opts.cloud;
968
+ if (ctx.dryRun) {
969
+ const env = channel.env;
970
+ const command = formatCommand("eas", ["build", "-p", opts.platform, "--profile", channel.buildProfile, ...local ? ["--local"] : [], "--output", staging], env);
971
+ ctx.log.info(`[dry-run] cd ${ctx.appDir} && ${command}`);
972
+ return { dryRun: true, command, staging };
973
+ }
974
+ mkdirSync2(dirname(staging), { recursive: true });
975
+ ctx.log.step(`eas build ${local ? "--local" : "(cloud)"} (${opts.platform}/${opts.channel})`);
976
+ requireSuccess(
977
+ easBuild(ctx, { platform: opts.platform, buildProfile: channel.buildProfile, local, output: local ? staging : void 0, env: channel.env }),
978
+ "eas build"
979
+ );
980
+ if (local && !existsSync6(staging)) fail(`Build concluiu sem artefato em ${staging}.`);
981
+ const remote = projectRemoteVersion(ctx, { platform: opts.platform, buildProfile: channel.buildProfile });
982
+ const version = readAppVersion(ctx.appDir, config);
983
+ const versionCode = remote.versionCode;
984
+ const buildNumber = opts.platform === "ios" ? remote.buildNumber : remote.versionCode;
985
+ const git2 = gitInfo(ctx);
986
+ const dirName = `${opts.platform}-${opts.channel}-${version}-${buildNumber || "unknown"}`;
987
+ const finalDir = join6(ctx.releaseDir, dirName);
988
+ mkdirSync2(finalDir, { recursive: true });
989
+ const finalPath = local ? join6(finalDir, `app.${ext}`) : "";
990
+ if (local && resolve(staging) !== resolve(finalPath)) {
991
+ copyFileSync(staging, finalPath);
992
+ if (staging.includes("_building-")) {
993
+ try {
994
+ unlinkSync(staging);
995
+ } catch {
996
+ }
997
+ }
998
+ }
999
+ const metadata = writeMetadata(finalDir, {
1000
+ platform: opts.platform,
1001
+ channel: opts.channel,
1002
+ buildProfile: channel.buildProfile,
1003
+ version,
1004
+ buildNumber,
1005
+ versionCode,
1006
+ artifactPath: finalPath,
1007
+ sha256: local && existsSync6(finalPath) ? sha256File(finalPath) : "",
1008
+ commitSha: git2.sha,
1009
+ branch: git2.branch,
1010
+ tag: git2.tag,
1011
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1012
+ toolchain: collectToolchain(ctx),
1013
+ toolchainDrift: Boolean(opts.toolchainDrift)
1014
+ });
1015
+ if (local) {
1016
+ ctx.log.success(`artefato ${finalPath}`);
1017
+ } else {
1018
+ ctx.log.success("build enviado ao EAS Build Cloud \u2014 use `eas build:list` para acompanhar e baixar o artefato.");
1019
+ }
1020
+ ctx.log.success(`metadata ${join6(finalDir, "metadata.json")}`);
1021
+ return { artifactPath: finalPath, metadata };
1022
+ }
1023
+
1024
+ // src/release/submit.ts
1025
+ import { existsSync as existsSync7 } from "fs";
1026
+ import { resolve as resolve2 } from "path";
1027
+ function inferPlatformFromArtifact(artifactPath) {
1028
+ const lower = artifactPath.toLowerCase();
1029
+ if (lower.endsWith(".apk")) {
1030
+ fail("Submit Android exige AAB. O profile de sideload n\xE3o vai \xE0 loja; gere o bin\xE1rio com um profile de release/production.");
1031
+ }
1032
+ if (lower.endsWith(".aab")) return "android";
1033
+ if (lower.endsWith(".ipa")) return "ios";
1034
+ fail("submit --path exige um arquivo .aab ou .ipa.");
1035
+ }
1036
+ function submitRelease(ctx, config, opts) {
1037
+ const channel = channelOf(config, opts.channel);
1038
+ assertChannelOptIn(ctx, config, channel, opts);
1039
+ if (!channel.store) {
1040
+ fail(`canal "${opts.channel}" n\xE3o envia \xE0 loja (store: false).`);
1041
+ }
1042
+ const submitProfile = channel.submitProfile ?? channel.buildProfile;
1043
+ let artifactPath;
1044
+ let platform;
1045
+ if (opts.path) {
1046
+ artifactPath = resolve2(ctx.repoRoot, opts.path);
1047
+ if (!existsSync7(artifactPath)) fail(`Artefato n\xE3o encontrado: ${artifactPath}.`);
1048
+ platform = inferPlatformFromArtifact(artifactPath);
1049
+ if (opts.platform && opts.platform !== platform) {
1050
+ fail(`--platform ${opts.platform} n\xE3o corresponde ao arquivo (${platform}).`);
1051
+ }
1052
+ } else {
1053
+ const entry = findLatestMetadata(ctx.releaseDir, { channel: opts.channel, platform: opts.platform });
1054
+ const metadata = entry.metadata;
1055
+ artifactPath = metadata.artifactPath;
1056
+ platform = metadata.platform;
1057
+ if (!artifactPath || !existsSync7(artifactPath)) {
1058
+ fail(`Artefato n\xE3o encontrado: ${artifactPath || "(ausente no metadata)"}. Builds na nuvem n\xE3o t\xEAm artefato local \u2014 use --path ap\xF3s baixar.`);
1059
+ }
1060
+ }
1061
+ if (platform === "android" && !artifactPath.toLowerCase().endsWith(".aab")) {
1062
+ fail("Submit Android exige AAB.");
1063
+ }
1064
+ if (ctx.dryRun) {
1065
+ const command = formatCommand("eas", ["submit", "-p", platform, "--profile", submitProfile, "--path", artifactPath], channel.env);
1066
+ ctx.log.info(`[dry-run] cd ${ctx.appDir} && ${command}`);
1067
+ return { dryRun: true, command, submitProfile, artifactPath };
1068
+ }
1069
+ ctx.log.step(`eas submit --path (${platform}/${submitProfile})`);
1070
+ requireSuccess(easSubmit(ctx, { platform, submitProfile, path: artifactPath, env: channel.env }), "eas submit");
1071
+ return { submitProfile, artifactPath };
1072
+ }
1073
+
1074
+ // src/release/ship.ts
1075
+ function shipRelease(ctx, config, opts) {
1076
+ const channel = channelOf(config, opts.channel);
1077
+ if (!channel.store) {
1078
+ fail(`canal "${opts.channel}" n\xE3o envia \xE0 loja (store: false). Use \`release-kit build\` direto para gerar o artefato de sideload.`);
1079
+ }
1080
+ let toolchainDrift = opts.toolchainDrift;
1081
+ if (!opts.skipDoctor) {
1082
+ const report = runDoctor(ctx, config, {
1083
+ channel: opts.channel,
1084
+ platform: opts.platform,
1085
+ allowToolchainDrift: opts.allowToolchainDrift
1086
+ });
1087
+ requireDoctorPass(report);
1088
+ toolchainDrift = report.toolchainDrift;
1089
+ }
1090
+ const built = buildRelease(ctx, config, { ...opts, toolchainDrift });
1091
+ if (ctx.dryRun) return { dryRun: true, artifactPath: built.artifactPath };
1092
+ const submitted = submitRelease(ctx, config, {
1093
+ channel: opts.channel,
1094
+ platform: opts.platform,
1095
+ latest: true,
1096
+ iKnowThisIsProduction: opts.iKnowThisIsProduction,
1097
+ confirmVersion: opts.confirmVersion
1098
+ });
1099
+ return { artifactPath: built.artifactPath, submit: submitted };
1100
+ }
1101
+
1102
+ // src/release/version.ts
1103
+ import { relative } from "path";
1104
+ function bumpVersion(ctx, config, opts) {
1105
+ if (!isWorkingTreeClean(ctx)) {
1106
+ fail("working tree suja. Commit ou descarte as mudan\xE7as antes de rodar `release-kit version`.");
1107
+ }
1108
+ const current = readAppVersion(ctx.appDir, config);
1109
+ const next = nextVersion(current, opts.bump);
1110
+ const tag = renderTag(config.git.tagFormat, next);
1111
+ if (ctx.dryRun) {
1112
+ ctx.log.info(`[dry-run] ${current} \u2192 ${next} (tag ${tag})`);
1113
+ return { from: current, to: next, tag, dryRun: true };
1114
+ }
1115
+ const path = writeAppVersion(ctx.appDir, config, next);
1116
+ const relPath = relative(ctx.repoRoot, path);
1117
+ git(ctx, ["add", relPath]);
1118
+ git(ctx, ["commit", "-m", `chore(release): ${tag}`]);
1119
+ git(ctx, ["tag", tag]);
1120
+ ctx.log.success(`${current} \u2192 ${next} (commit + tag ${tag})`);
1121
+ if (opts.push) {
1122
+ requireSuccess(git(ctx, ["push", config.git.remote, config.git.releaseBranch]), "git push");
1123
+ requireSuccess(git(ctx, ["push", config.git.remote, tag]), "git push tag");
1124
+ ctx.log.success(`push ${config.git.remote} ${config.git.releaseBranch} + tag ${tag}`);
1125
+ }
1126
+ return { from: current, to: next, tag };
1127
+ }
1128
+
1129
+ // src/release/notes.ts
1130
+ import { existsSync as existsSync9 } from "fs";
1131
+
1132
+ // src/notes/draft.ts
1133
+ function draftNotes(commits, version) {
1134
+ const facing = userFacingCommits(commits);
1135
+ if (facing.length === 0) {
1136
+ return `Novidades da vers\xE3o ${version}
1137
+
1138
+ (sem commits feat/fix desde a \xFAltima tag \u2014 escreva as notas manualmente)
1139
+ `;
1140
+ }
1141
+ const features = facing.filter((c) => c.type === "feat");
1142
+ const fixes = facing.filter((c) => c.type === "fix");
1143
+ const lines = [`Novidades da vers\xE3o ${version}`, ""];
1144
+ if (features.length) {
1145
+ lines.push("Novidades:");
1146
+ for (const commit of features) lines.push(`- ${capitalize(commit.description)}`);
1147
+ lines.push("");
1148
+ }
1149
+ if (fixes.length) {
1150
+ lines.push("Corre\xE7\xF5es:");
1151
+ for (const commit of fixes) lines.push(`- ${capitalize(commit.description)}`);
1152
+ lines.push("");
1153
+ }
1154
+ lines.push("# Revise este rascunho antes de publicar \u2014 ele vem direto dos commits (feat/fix) e n\xE3o foi reescrito em linguagem de usu\xE1rio.");
1155
+ return lines.join("\n");
1156
+ }
1157
+ function capitalize(text) {
1158
+ if (!text) return text;
1159
+ return text[0].toUpperCase() + text.slice(1);
1160
+ }
1161
+
1162
+ // src/notes/editor.ts
1163
+ function defaultEditor() {
1164
+ if (process.platform === "win32") return "notepad";
1165
+ return "nano";
1166
+ }
1167
+ function openInEditor(ctx, path) {
1168
+ const editor = ctx.env.EDITOR || ctx.env.VISUAL || defaultEditor();
1169
+ const [command, ...args] = editor.split(" ");
1170
+ if (!command) return;
1171
+ requireSuccess(ctx.run(command, [...args, path], { inherit: true }), `${editor} ${path}`);
1172
+ }
1173
+
1174
+ // src/notes/limits.ts
1175
+ var PLAY_NOTES_MAX = 500;
1176
+ var APPLE_WHATS_NEW_MAX = 4e3;
1177
+ function notesMaxFor(target) {
1178
+ return target === "play" ? PLAY_NOTES_MAX : APPLE_WHATS_NEW_MAX;
1179
+ }
1180
+ function assertNotesWithinLimit(text, target, locale) {
1181
+ const max = notesMaxFor(target);
1182
+ if (text.length > max) {
1183
+ fail(`Notas ${target} (${locale}) t\xEAm ${text.length} caracteres (m\xE1ximo ${max}).`);
1184
+ }
1185
+ if (!text.trim()) {
1186
+ fail(`Notas ${target} (${locale}) est\xE3o vazias.`);
1187
+ }
1188
+ }
1189
+
1190
+ // src/notes/paths.ts
1191
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1192
+ import { join as join7 } from "path";
1193
+ function notesDirFor(appDir, notesDir, version) {
1194
+ return join7(appDir, notesDir, version);
1195
+ }
1196
+ function notesFilePath(appDir, notesDir, version, locale, target) {
1197
+ return join7(notesDirFor(appDir, notesDir, version), `${locale}.${target}.txt`);
1198
+ }
1199
+ function readNotesFile(path) {
1200
+ if (!existsSync8(path)) fail(`Notas n\xE3o encontradas: ${path}. Rode \`release-kit notes\` primeiro.`);
1201
+ const text = readFileSync5(path, "utf8").trim();
1202
+ if (!text) fail(`Notas vazias em ${path}.`);
1203
+ return text;
1204
+ }
1205
+ function writeNotesFile(path, text) {
1206
+ mkdirSync3(join7(path, ".."), { recursive: true });
1207
+ writeFileSync4(path, `${text.trim()}
1208
+ `);
1209
+ }
1210
+
1211
+ // src/release/notes.ts
1212
+ function targetsFor(config) {
1213
+ const targets = [];
1214
+ if (config.platforms.includes("ios")) targets.push("ios");
1215
+ if (config.platforms.includes("android")) targets.push("play");
1216
+ return targets;
1217
+ }
1218
+ function draftAndEditNotes(ctx, config, opts) {
1219
+ const version = opts.version ?? readAppVersion(ctx.appDir, config);
1220
+ const commits = commitsSinceLastTag(ctx, config.git.tagFormat);
1221
+ const targets = targetsFor(config);
1222
+ const files = [];
1223
+ for (const locale of config.locales) {
1224
+ for (const target of targets) {
1225
+ const path = notesFilePath(ctx.appDir, config.notesDir, version, locale, target);
1226
+ const isNew = !existsSync9(path);
1227
+ if (isNew) {
1228
+ writeNotesFile(path, draftNotes(commits, version));
1229
+ ctx.log.step(`rascunho criado: ${path}`);
1230
+ }
1231
+ if (!opts.skipEditor && (isNew || opts.edit)) {
1232
+ openInEditor(ctx, path);
1233
+ }
1234
+ const text = readNotesFile(path);
1235
+ assertNotesWithinLimit(text, target, locale);
1236
+ files.push({ path, target, locale });
1237
+ }
1238
+ }
1239
+ return { version, files };
1240
+ }
1241
+
1242
+ // src/stores/credentials.ts
1243
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
1244
+
1245
+ // src/env/store.ts
1246
+ import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync5 } from "fs";
1247
+ import { homedir, tmpdir } from "os";
1248
+ import { basename, join as join8 } from "path";
1249
+ import dotenv from "dotenv";
1250
+ function materializeSecretContent(content, filename) {
1251
+ const dir = mkdtempSync(join8(tmpdir(), "release-kit-"));
1252
+ const path = join8(dir, filename);
1253
+ writeFileSync5(path, content);
1254
+ try {
1255
+ chmodSync(path, 384);
1256
+ } catch {
1257
+ }
1258
+ return {
1259
+ path,
1260
+ cleanup: () => rmSync(dir, { recursive: true, force: true })
1261
+ };
1262
+ }
1263
+ function resolveFileCredential(env, pathKey, contentKey, filename) {
1264
+ const directPath = env[pathKey];
1265
+ if (directPath) return { path: directPath, cleanup: () => {
1266
+ } };
1267
+ const content = env[contentKey];
1268
+ if (content) return materializeSecretContent(content, filename);
1269
+ return null;
1270
+ }
1271
+
1272
+ // src/stores/apple/auth.ts
1273
+ import { createPrivateKey, createSign } from "crypto";
1274
+ function base64url(input) {
1275
+ const buffer = typeof input === "string" ? Buffer.from(input) : input;
1276
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1277
+ }
1278
+ function createAppStoreConnectToken(credentials, now = Date.now()) {
1279
+ const iat = Math.floor(now / 1e3);
1280
+ const exp = iat + 19 * 60;
1281
+ const header = base64url(JSON.stringify({ alg: "ES256", kid: credentials.keyId, typ: "JWT" }));
1282
+ const payload = base64url(
1283
+ JSON.stringify({
1284
+ iss: credentials.issuerId,
1285
+ iat,
1286
+ exp,
1287
+ aud: "appstoreconnect-v1"
1288
+ })
1289
+ );
1290
+ const signingInput = `${header}.${payload}`;
1291
+ const key = createPrivateKey({ key: credentials.privateKeyPem, format: "pem" });
1292
+ const signer = createSign("SHA256");
1293
+ signer.update(signingInput);
1294
+ signer.end();
1295
+ const signature = signer.sign({ key, dsaEncoding: "ieee-p1363" });
1296
+ return `${signingInput}.${base64url(signature)}`;
1297
+ }
1298
+
1299
+ // src/stores/apple/client.ts
1300
+ var APP_STORE_CONNECT_BASE = "https://api.appstoreconnect.apple.com/v1";
1301
+ var AppStoreConnectClient = class {
1302
+ credentials;
1303
+ baseUrl;
1304
+ fetchImpl;
1305
+ constructor(options) {
1306
+ this.credentials = options.credentials;
1307
+ this.baseUrl = options.baseUrl ?? APP_STORE_CONNECT_BASE;
1308
+ this.fetchImpl = options.fetchImpl ?? fetch;
1309
+ }
1310
+ token() {
1311
+ return createAppStoreConnectToken(this.credentials);
1312
+ }
1313
+ async request(method, path, body) {
1314
+ const url = path.startsWith("http") ? path : `${this.baseUrl}${path}`;
1315
+ const response = await this.fetchImpl(url, {
1316
+ method,
1317
+ headers: {
1318
+ Authorization: `Bearer ${this.token()}`,
1319
+ "Content-Type": "application/json"
1320
+ },
1321
+ body: body ? JSON.stringify(body) : void 0
1322
+ });
1323
+ const text = await response.text();
1324
+ if (!response.ok) {
1325
+ fail(`App Store Connect API ${method} ${path} \u2192 ${response.status}
1326
+ ${text}`);
1327
+ }
1328
+ if (!text) return void 0;
1329
+ return JSON.parse(text);
1330
+ }
1331
+ get(path) {
1332
+ return this.request("GET", path);
1333
+ }
1334
+ post(path, body) {
1335
+ return this.request("POST", path, body);
1336
+ }
1337
+ patch(path, body) {
1338
+ return this.request("PATCH", path, body);
1339
+ }
1340
+ };
1341
+
1342
+ // src/stores/google/auth.ts
1343
+ import { createSign as createSign2 } from "crypto";
1344
+ function parseServiceAccountJson(raw) {
1345
+ let parsed;
1346
+ try {
1347
+ parsed = JSON.parse(raw);
1348
+ } catch (error) {
1349
+ fail(`JSON da service account inv\xE1lido: ${error.message}`);
1350
+ }
1351
+ const account = parsed;
1352
+ if (!account.client_email || !account.private_key) {
1353
+ fail("JSON da service account sem client_email/private_key.");
1354
+ }
1355
+ return account;
1356
+ }
1357
+ function base64url2(input) {
1358
+ const buffer = typeof input === "string" ? Buffer.from(input) : input;
1359
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1360
+ }
1361
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
1362
+ var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
1363
+ async function fetchAccessToken(account, fetchImpl = fetch, now = Date.now()) {
1364
+ const iat = Math.floor(now / 1e3);
1365
+ const exp = iat + 60 * 60;
1366
+ const header = base64url2(JSON.stringify({ alg: "RS256", typ: "JWT" }));
1367
+ const payload = base64url2(
1368
+ JSON.stringify({
1369
+ iss: account.client_email,
1370
+ scope: ANDROID_PUBLISHER_SCOPE,
1371
+ aud: TOKEN_URL,
1372
+ iat,
1373
+ exp
1374
+ })
1375
+ );
1376
+ const signingInput = `${header}.${payload}`;
1377
+ const signer = createSign2("RSA-SHA256");
1378
+ signer.update(signingInput);
1379
+ signer.end();
1380
+ const signature = signer.sign(account.private_key);
1381
+ const assertion = `${signingInput}.${base64url2(signature)}`;
1382
+ const response = await fetchImpl(TOKEN_URL, {
1383
+ method: "POST",
1384
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1385
+ body: new URLSearchParams({
1386
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
1387
+ assertion
1388
+ }).toString()
1389
+ });
1390
+ const text = await response.text();
1391
+ if (!response.ok) fail(`Falha ao obter access_token do Google: ${response.status}
1392
+ ${text}`);
1393
+ const json = JSON.parse(text);
1394
+ if (!json.access_token) fail("Resposta do Google sem access_token.");
1395
+ return json.access_token;
1396
+ }
1397
+
1398
+ // src/stores/google/client.ts
1399
+ var ANDROID_PUBLISHER_BASE = "https://androidpublisher.googleapis.com/androidpublisher/v3";
1400
+ var AndroidPublisherClient = class {
1401
+ account;
1402
+ baseUrl;
1403
+ fetchImpl;
1404
+ cachedToken = null;
1405
+ constructor(options) {
1406
+ this.account = options.account;
1407
+ this.baseUrl = options.baseUrl ?? ANDROID_PUBLISHER_BASE;
1408
+ this.fetchImpl = options.fetchImpl ?? fetch;
1409
+ }
1410
+ async token() {
1411
+ if (!this.cachedToken) {
1412
+ this.cachedToken = await fetchAccessToken(this.account, this.fetchImpl);
1413
+ }
1414
+ return this.cachedToken;
1415
+ }
1416
+ async request(method, path, body) {
1417
+ const token = await this.token();
1418
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
1419
+ method,
1420
+ headers: {
1421
+ Authorization: `Bearer ${token}`,
1422
+ "Content-Type": "application/json"
1423
+ },
1424
+ body: body ? JSON.stringify(body) : void 0
1425
+ });
1426
+ const text = await response.text();
1427
+ if (!response.ok) fail(`Android Publisher API ${method} ${path} \u2192 ${response.status}
1428
+ ${text}`);
1429
+ if (!text) return void 0;
1430
+ return JSON.parse(text);
1431
+ }
1432
+ get(path) {
1433
+ return this.request("GET", path);
1434
+ }
1435
+ put(path, body) {
1436
+ return this.request("PUT", path, body);
1437
+ }
1438
+ post(path, body) {
1439
+ return this.request("POST", path, body);
1440
+ }
1441
+ delete(path) {
1442
+ return this.request("DELETE", path);
1443
+ }
1444
+ };
1445
+
1446
+ // src/stores/credentials.ts
1447
+ function createAppleClient(env) {
1448
+ const keyId = env.ASC_KEY_ID;
1449
+ const issuerId = env.ASC_ISSUER_ID;
1450
+ if (!keyId || !issuerId) {
1451
+ fail("Credenciais App Store Connect ausentes: ASC_KEY_ID, ASC_ISSUER_ID. Rode `release-kit credentials apple`.");
1452
+ }
1453
+ const resolved = resolveFileCredential(env, "ASC_KEY_PATH", "ASC_KEY_P8_CONTENT", "AuthKey.p8");
1454
+ if (!resolved) {
1455
+ fail("Credencial ASC_KEY_PATH (ou ASC_KEY_P8_CONTENT em CI) ausente. Rode `release-kit credentials apple`.");
1456
+ }
1457
+ if (!existsSync11(resolved.path)) fail(`ASC_KEY_PATH n\xE3o existe: ${resolved.path}`);
1458
+ const privateKeyPem = readFileSync7(resolved.path, "utf8");
1459
+ const client = new AppStoreConnectClient({ credentials: { keyId, issuerId, privateKeyPem } });
1460
+ return Object.assign(client, { cleanup: resolved.cleanup });
1461
+ }
1462
+ function createGoogleClient(env) {
1463
+ const resolved = resolveFileCredential(env, "GOOGLE_PLAY_JSON_KEY", "GOOGLE_PLAY_JSON_CONTENT", "play-service-account.json");
1464
+ if (!resolved) {
1465
+ fail(
1466
+ "Credencial GOOGLE_PLAY_JSON_KEY (ou GOOGLE_PLAY_JSON_CONTENT em CI) ausente. Rode `release-kit credentials google`."
1467
+ );
1468
+ }
1469
+ if (!existsSync11(resolved.path)) fail(`GOOGLE_PLAY_JSON_KEY n\xE3o existe: ${resolved.path}`);
1470
+ const account = parseServiceAccountJson(readFileSync7(resolved.path, "utf8"));
1471
+ const client = new AndroidPublisherClient({ account });
1472
+ return Object.assign(client, { cleanup: resolved.cleanup });
1473
+ }
1474
+
1475
+ // src/stores/google/tracks.ts
1476
+ async function withEdit(client, packageName, fn) {
1477
+ const edit = await client.post(`/applications/${packageName}/edits`);
1478
+ try {
1479
+ const result = await fn(edit.id);
1480
+ await client.post(`/applications/${packageName}/edits/${edit.id}:commit`);
1481
+ return result;
1482
+ } catch (error) {
1483
+ await client.delete(`/applications/${packageName}/edits/${edit.id}`).catch(() => {
1484
+ });
1485
+ throw error;
1486
+ }
1487
+ }
1488
+ async function withReadOnlyEdit(client, packageName, fn) {
1489
+ const edit = await client.post(`/applications/${packageName}/edits`);
1490
+ try {
1491
+ return await fn(edit.id);
1492
+ } finally {
1493
+ await client.delete(`/applications/${packageName}/edits/${edit.id}`).catch(() => {
1494
+ });
1495
+ }
1496
+ }
1497
+ function findTargetRelease(releases, versionCode) {
1498
+ if (versionCode) {
1499
+ return releases.find((release) => (release.versionCodes ?? []).includes(String(versionCode)));
1500
+ }
1501
+ return releases.find((release) => release.status === "completed") ?? releases[0];
1502
+ }
1503
+ async function promoteRelease(client, input) {
1504
+ return withEdit(client, input.packageName, async (editId) => {
1505
+ const source = await client.get(`/applications/${input.packageName}/edits/${editId}/tracks/${input.fromTrack}`);
1506
+ const releases = source.releases ?? [];
1507
+ const target = findTargetRelease(releases);
1508
+ if (!target || !target.versionCodes?.length) {
1509
+ fail(`faixa ${input.fromTrack} sem release para promover.`);
1510
+ }
1511
+ const fraction = input.initialRollout ?? 1;
1512
+ const status = fraction < 1 ? "inProgress" : "completed";
1513
+ const releaseNotes = input.notesByLocale ? Object.entries(input.notesByLocale).map(([language, text]) => ({ language, text })) : target.releaseNotes;
1514
+ const newRelease = {
1515
+ versionCodes: target.versionCodes,
1516
+ status,
1517
+ releaseNotes,
1518
+ ...status === "inProgress" ? { userFraction: fraction } : {}
1519
+ };
1520
+ await client.put(`/applications/${input.packageName}/edits/${editId}/tracks/${input.toTrack}`, {
1521
+ track: input.toTrack,
1522
+ releases: [newRelease]
1523
+ });
1524
+ return newRelease;
1525
+ });
1526
+ }
1527
+ async function setRollout(client, packageName, track, target) {
1528
+ return withEdit(client, packageName, async (editId) => {
1529
+ const current = await client.get(`/applications/${packageName}/edits/${editId}/tracks/${track}`);
1530
+ const releases = current.releases ?? [];
1531
+ const active = releases.find((release) => release.status === "inProgress") ?? releases[releases.length - 1];
1532
+ if (!active) fail(`faixa ${track} sem release em rollout.`);
1533
+ if (target === "complete") {
1534
+ active.status = "completed";
1535
+ delete active.userFraction;
1536
+ } else if (target === "halt") {
1537
+ active.status = "halted";
1538
+ } else {
1539
+ if (target <= 0 || target > 1) fail(`--to precisa ser um valor entre 0 e 1 (ou complete/halt).`);
1540
+ active.status = "inProgress";
1541
+ active.userFraction = target;
1542
+ }
1543
+ await client.put(`/applications/${packageName}/edits/${editId}/tracks/${track}`, { track, releases });
1544
+ return active;
1545
+ });
1546
+ }
1547
+ async function getTrackStatus(client, packageName, track) {
1548
+ return withReadOnlyEdit(
1549
+ client,
1550
+ packageName,
1551
+ (editId) => client.get(`/applications/${packageName}/edits/${editId}/tracks/${track}`)
1552
+ );
1553
+ }
1554
+
1555
+ // src/stores/apple/apps.ts
1556
+ async function findAppByBundleId(client, bundleId) {
1557
+ const result = await client.get(`/apps?filter[bundleId]=${encodeURIComponent(bundleId)}`);
1558
+ const [app] = result.data;
1559
+ if (!app) return null;
1560
+ return { id: app.id, name: app.attributes.name };
1561
+ }
1562
+ async function requireAppByBundleId(client, bundleId) {
1563
+ const app = await findAppByBundleId(client, bundleId);
1564
+ if (!app) fail(`App com bundleId ${bundleId} n\xE3o encontrado no App Store Connect (chave sem acesso ao app, ou app ainda n\xE3o criado).`);
1565
+ return app;
1566
+ }
1567
+
1568
+ // src/stores/apple/review.ts
1569
+ var IN_PROGRESS_STATES = /* @__PURE__ */ new Set(["READY_FOR_REVIEW", "WAITING_FOR_REVIEW", "IN_REVIEW", "PROCESSING"]);
1570
+ var TERMINAL_STATES = /* @__PURE__ */ new Set(["COMPLETE", "CANCELED"]);
1571
+ async function submitForReview(client, input) {
1572
+ const app = await requireAppByBundleId(client, input.bundleId);
1573
+ const inProgress = await findInProgressReviewSubmission(client, app.id);
1574
+ if (inProgress) {
1575
+ return {
1576
+ appId: app.id,
1577
+ appStoreVersionId: "",
1578
+ buildId: "",
1579
+ submissionId: inProgress.id,
1580
+ state: inProgress.attributes.state,
1581
+ alreadyInProgress: true
1582
+ };
1583
+ }
1584
+ const version = await ensureAppStoreVersion(client, app.id, input.versionString);
1585
+ if (input.copyright && !version.attributes.copyright) {
1586
+ await client.patch(`/appStoreVersions/${version.id}`, {
1587
+ data: { id: version.id, type: "appStoreVersions", attributes: { copyright: input.copyright } }
1588
+ });
1589
+ }
1590
+ await client.patch(`/appStoreVersions/${version.id}`, {
1591
+ data: {
1592
+ id: version.id,
1593
+ type: "appStoreVersions",
1594
+ attributes: { releaseType: input.automaticRelease ? "AFTER_APPROVAL" : "MANUAL" }
1595
+ }
1596
+ });
1597
+ const build = await findValidBuild(client, app.id, input.versionString);
1598
+ if (!build) fail(`Nenhum build VALID no TestFlight para a vers\xE3o ${input.versionString}.`);
1599
+ await client.request("POST", `/appStoreVersions/${version.id}/relationships/build`, {
1600
+ data: { id: build.id, type: "builds" }
1601
+ });
1602
+ if (input.phasedRelease) {
1603
+ await client.post("/appStoreVersionPhasedReleases", {
1604
+ data: {
1605
+ type: "appStoreVersionPhasedReleases",
1606
+ relationships: { appStoreVersion: { data: { id: version.id, type: "appStoreVersions" } } }
1607
+ }
1608
+ });
1609
+ }
1610
+ for (const [locale, whatsNew] of Object.entries(input.whatsNewByLocale)) {
1611
+ const localization = await findLocalization(client, version.id, locale);
1612
+ if (!localization) fail(`Localization ${locale} ausente na vers\xE3o ${input.versionString} do App Store Connect.`);
1613
+ await client.patch(`/appStoreVersionLocalizations/${localization.id}`, {
1614
+ data: { id: localization.id, type: "appStoreVersionLocalizations", attributes: { whatsNew } }
1615
+ });
1616
+ }
1617
+ const submission = await createReviewSubmission(client, app.id);
1618
+ await client.post("/reviewSubmissionItems", {
1619
+ data: {
1620
+ type: "reviewSubmissionItems",
1621
+ relationships: {
1622
+ reviewSubmission: { data: { id: submission.id, type: "reviewSubmissions" } },
1623
+ appStoreVersion: { data: { id: version.id, type: "appStoreVersions" } }
1624
+ }
1625
+ }
1626
+ });
1627
+ const submitted = await client.patch(`/reviewSubmissions/${submission.id}`, {
1628
+ data: { id: submission.id, type: "reviewSubmissions", attributes: { submitted: true } }
1629
+ });
1630
+ return {
1631
+ appId: app.id,
1632
+ appStoreVersionId: version.id,
1633
+ buildId: build.id,
1634
+ submissionId: submission.id,
1635
+ state: submitted.data.attributes.state,
1636
+ alreadyInProgress: false
1637
+ };
1638
+ }
1639
+ async function getIosStatus(client, bundleId, versionString) {
1640
+ const app = await requireAppByBundleId(client, bundleId);
1641
+ const version = await findAppStoreVersion(client, app.id, versionString);
1642
+ const inProgress = await findInProgressReviewSubmission(client, app.id);
1643
+ return {
1644
+ appId: app.id,
1645
+ versionString,
1646
+ appStoreState: version?.attributes.appStoreState ?? null,
1647
+ reviewSubmissionState: inProgress?.attributes.state ?? null
1648
+ };
1649
+ }
1650
+ async function findInProgressReviewSubmission(client, appId) {
1651
+ const result = await client.get(
1652
+ `/apps/${appId}/reviewSubmissions?filter[platform]=IOS`
1653
+ );
1654
+ const active = result.data.find(
1655
+ (item) => IN_PROGRESS_STATES.has(item.attributes.state) && !TERMINAL_STATES.has(item.attributes.state)
1656
+ );
1657
+ return active ?? null;
1658
+ }
1659
+ async function ensureAppStoreVersion(client, appId, versionString) {
1660
+ const existing = await findAppStoreVersion(client, appId, versionString);
1661
+ if (existing) return existing;
1662
+ const created = await client.post("/appStoreVersions", {
1663
+ data: {
1664
+ type: "appStoreVersions",
1665
+ attributes: { platform: "IOS", versionString },
1666
+ relationships: { app: { data: { id: appId, type: "apps" } } }
1667
+ }
1668
+ });
1669
+ return { id: created.data.id, attributes: created.data.attributes };
1670
+ }
1671
+ async function findAppStoreVersion(client, appId, versionString) {
1672
+ const result = await client.get(
1673
+ `/apps/${appId}/appStoreVersions?filter[versionString]=${encodeURIComponent(versionString)}&filter[platform]=IOS`
1674
+ );
1675
+ const [version] = result.data;
1676
+ return version ? { id: version.id, attributes: version.attributes } : null;
1677
+ }
1678
+ async function findValidBuild(client, appId, versionString) {
1679
+ const result = await client.get(
1680
+ `/builds?filter[app]=${appId}&filter[preReleaseVersion.version]=${encodeURIComponent(versionString)}&sort=-uploadedDate&limit=20`
1681
+ );
1682
+ const valid = result.data.find((item) => item.attributes.processingState === "VALID");
1683
+ const build = valid ?? result.data[0];
1684
+ return build ? { id: build.id, attributes: build.attributes } : null;
1685
+ }
1686
+ async function findLocalization(client, versionId, locale) {
1687
+ const result = await client.get(
1688
+ `/appStoreVersions/${versionId}/appStoreVersionLocalizations`
1689
+ );
1690
+ const match = result.data.find((item) => item.attributes.locale === locale);
1691
+ return match ? { id: match.id, attributes: match.attributes } : null;
1692
+ }
1693
+ async function createReviewSubmission(client, appId) {
1694
+ const created = await client.post("/reviewSubmissions", {
1695
+ data: {
1696
+ type: "reviewSubmissions",
1697
+ attributes: { platform: "IOS" },
1698
+ relationships: { app: { data: { id: appId, type: "apps" } } }
1699
+ }
1700
+ });
1701
+ return { id: created.data.id, attributes: created.data.attributes };
1702
+ }
1703
+
1704
+ // src/release/promote.ts
1705
+ function notesByLocale(ctx, config, version, target) {
1706
+ const result = {};
1707
+ for (const locale of config.locales) {
1708
+ const path = notesFilePath(ctx.appDir, config.notesDir, version, locale, target);
1709
+ result[locale] = readNotesFile(path);
1710
+ }
1711
+ return result;
1712
+ }
1713
+ async function promoteRelease2(ctx, config, opts) {
1714
+ const channel = channelOf(config, opts.channel);
1715
+ assertChannelOptIn(ctx, config, channel, opts);
1716
+ const version = readAppVersion(ctx.appDir, config);
1717
+ const result = {};
1718
+ const wantsAndroid = (!opts.platform || opts.platform === "android") && channel.android;
1719
+ const wantsIos = (!opts.platform || opts.platform === "ios") && channel.ios;
1720
+ if (!wantsAndroid && !wantsIos) fail("nenhuma plataforma configurada no canal para promover.");
1721
+ if (ctx.dryRun) {
1722
+ if (wantsAndroid) ctx.log.info(`[dry-run] Play: promover ${channel.android.testTrack} \u2192 ${channel.android.productionTrack} (${channel.android.package})`);
1723
+ if (wantsIos) ctx.log.info(`[dry-run] Apple: criar Review Submission para ${channel.ios.bundleId} v${version}`);
1724
+ return result;
1725
+ }
1726
+ if (wantsAndroid) {
1727
+ const android = channel.android;
1728
+ const google = createGoogleClient(ctx.env);
1729
+ try {
1730
+ const notes = notesByLocale(ctx, config, version, "play");
1731
+ ctx.log.step(`Play: promovendo ${android.testTrack} \u2192 ${android.productionTrack}`);
1732
+ const release = await promoteRelease(google, {
1733
+ packageName: android.package,
1734
+ fromTrack: android.testTrack,
1735
+ toTrack: android.productionTrack,
1736
+ notesByLocale: notes,
1737
+ initialRollout: android.initialRollout
1738
+ });
1739
+ ctx.log.success(`Play: ${android.productionTrack} status=${release.status} versionCodes=${(release.versionCodes ?? []).join(",")}`);
1740
+ result.android = { versionCodes: release.versionCodes, status: release.status };
1741
+ } finally {
1742
+ google.cleanup();
1743
+ }
1744
+ }
1745
+ if (wantsIos) {
1746
+ const ios = channel.ios;
1747
+ const apple = createAppleClient(ctx.env);
1748
+ try {
1749
+ const notes = notesByLocale(ctx, config, version, "ios");
1750
+ ctx.log.step(`Apple: enviando Review Submission para ${ios.bundleId} v${version}`);
1751
+ const submission = await submitForReview(apple, {
1752
+ bundleId: ios.bundleId,
1753
+ versionString: version,
1754
+ whatsNewByLocale: notes,
1755
+ automaticRelease: ios.automaticRelease,
1756
+ phasedRelease: ios.phasedRelease,
1757
+ copyright: ios.copyright
1758
+ });
1759
+ if (submission.alreadyInProgress) {
1760
+ ctx.log.warn(`Apple: j\xE1 existe submission em an\xE1lise (${submission.submissionId}, state=${submission.state}).`);
1761
+ } else {
1762
+ ctx.log.success(`Apple: submission ${submission.submissionId} enviada (state=${submission.state}).`);
1763
+ }
1764
+ result.ios = { submissionId: submission.submissionId, state: submission.state, alreadyInProgress: submission.alreadyInProgress };
1765
+ } finally {
1766
+ apple.cleanup();
1767
+ }
1768
+ }
1769
+ return result;
1770
+ }
1771
+
1772
+ // src/release/rollout.ts
1773
+ async function rolloutRelease(ctx, config, opts) {
1774
+ if (opts.platform !== "android") {
1775
+ fail("rollout percentual s\xF3 \xE9 suportado para Android nesta vers\xE3o. iOS: acompanhe o phased release no App Store Connect.");
1776
+ }
1777
+ const channel = channelOf(config, opts.channel);
1778
+ assertChannelOptIn(ctx, config, channel, opts);
1779
+ if (!channel.android) fail(`canal "${opts.channel}" n\xE3o tem configura\xE7\xE3o android.`);
1780
+ if (ctx.dryRun) {
1781
+ ctx.log.info(`[dry-run] Play: ${channel.android.productionTrack} \u2192 ${opts.to}`);
1782
+ return { dryRun: true };
1783
+ }
1784
+ const google = createGoogleClient(ctx.env);
1785
+ try {
1786
+ const release = await setRollout(google, channel.android.package, channel.android.productionTrack, opts.to);
1787
+ ctx.log.success(`Play: ${channel.android.productionTrack} agora ${release.status}${release.userFraction ? ` (${Math.round(release.userFraction * 100)}%)` : ""}`);
1788
+ return release;
1789
+ } finally {
1790
+ google.cleanup();
1791
+ }
1792
+ }
1793
+
1794
+ // src/release/status.ts
1795
+ async function statusOf(ctx, config, opts) {
1796
+ const channel = channelOf(config, opts.channel);
1797
+ const version = readAppVersion(ctx.appDir, config);
1798
+ const result = { version };
1799
+ if (channel.android) {
1800
+ const google = createGoogleClient(ctx.env);
1801
+ try {
1802
+ const track = await getTrackStatus(google, channel.android.package, channel.android.productionTrack);
1803
+ result.android = {
1804
+ track: channel.android.productionTrack,
1805
+ releases: (track.releases ?? []).map((release) => ({
1806
+ status: release.status,
1807
+ versionCodes: release.versionCodes,
1808
+ userFraction: release.userFraction
1809
+ }))
1810
+ };
1811
+ } finally {
1812
+ google.cleanup();
1813
+ }
1814
+ }
1815
+ if (channel.ios) {
1816
+ const apple = createAppleClient(ctx.env);
1817
+ try {
1818
+ const status = await getIosStatus(apple, channel.ios.bundleId, version);
1819
+ result.ios = { appStoreState: status.appStoreState, reviewSubmissionState: status.reviewSubmissionState };
1820
+ } finally {
1821
+ apple.cleanup();
1822
+ }
1823
+ }
1824
+ return result;
1825
+ }
1826
+
1827
+ // src/release/update.ts
1828
+ function updateRelease(ctx, config, opts) {
1829
+ const channel = channelOf(config, opts.channel);
1830
+ assertChannelOptIn(ctx, config, channel, opts);
1831
+ if (!channel.ota) {
1832
+ fail(`canal "${opts.channel}" n\xE3o tem "ota.channel" configurado.`);
1833
+ }
1834
+ if (ctx.dryRun) {
1835
+ const command = formatCommand("eas", ["update", "--channel", channel.ota.channel, "--message", opts.message ?? "(sem mensagem)"], channel.env);
1836
+ ctx.log.info(`[dry-run] cd ${ctx.appDir} && ${command}`);
1837
+ return { dryRun: true, command, channel: channel.ota.channel };
1838
+ }
1839
+ ctx.log.step(`eas update (channel ${channel.ota.channel})`);
1840
+ requireSuccess(
1841
+ easUpdate(ctx, { channel: channel.ota.channel, message: opts.message, platform: opts.platform, env: channel.env }),
1842
+ "eas update"
1843
+ );
1844
+ return { channel: channel.ota.channel };
1845
+ }
1846
+ export {
1847
+ APPLE_WHATS_NEW_MAX,
1848
+ PLAY_NOTES_MAX,
1849
+ assertChannelOptIn,
1850
+ assertNotesWithinLimit,
1851
+ buildRelease,
1852
+ bumpVersion,
1853
+ channelOf,
1854
+ checkGitPolicy,
1855
+ commitsSinceLastTag,
1856
+ configExists,
1857
+ createAppStoreConnectToken,
1858
+ createContext,
1859
+ draftAndEditNotes,
1860
+ draftNotes,
1861
+ fetchAccessToken,
1862
+ loadConfig,
1863
+ nextVersion,
1864
+ notesMaxFor,
1865
+ parseConfig,
1866
+ parseServiceAccountJson,
1867
+ promoteRelease2 as promoteRelease,
1868
+ protectedChannelNames,
1869
+ readAppVersion,
1870
+ releaseKitConfigSchema,
1871
+ rolloutRelease,
1872
+ runDoctor,
1873
+ shipRelease,
1874
+ statusOf,
1875
+ submitRelease,
1876
+ updateRelease,
1877
+ userFacingCommits,
1878
+ writeAppVersion,
1879
+ writeConfig
1880
+ };
1881
+ //# sourceMappingURL=index.js.map