preflightlaunch 0.2.0 → 0.2.2

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.
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  const require = createRequire(import.meta.url);
4
4
  import {
5
5
  __commonJS,
6
+ __require,
6
7
  __toESM,
7
8
  init_esm_shims,
8
9
  open_default
@@ -2980,7 +2981,7 @@ var require_compile = __commonJS({
2980
2981
  const schOrFunc = root.refs[ref];
2981
2982
  if (schOrFunc)
2982
2983
  return schOrFunc;
2983
- let _sch = resolve3.call(this, root, ref);
2984
+ let _sch = resolve4.call(this, root, ref);
2984
2985
  if (_sch === void 0) {
2985
2986
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2986
2987
  const { schemaId } = this.opts;
@@ -3007,7 +3008,7 @@ var require_compile = __commonJS({
3007
3008
  function sameSchemaEnv(s1, s2) {
3008
3009
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3010
  }
3010
- function resolve3(root, ref) {
3011
+ function resolve4(root, ref) {
3011
3012
  let sch;
3012
3013
  while (typeof (sch = this.refs[ref]) == "string")
3013
3014
  ref = sch;
@@ -3585,7 +3586,7 @@ var require_fast_uri = __commonJS({
3585
3586
  }
3586
3587
  return uri;
3587
3588
  }
3588
- function resolve3(baseURI, relativeURI, options) {
3589
+ function resolve4(baseURI, relativeURI, options) {
3589
3590
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3590
3591
  const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
3591
3592
  schemelessOptions.skipEscape = true;
@@ -3812,7 +3813,7 @@ var require_fast_uri = __commonJS({
3812
3813
  var fastUri = {
3813
3814
  SCHEMES,
3814
3815
  normalize,
3815
- resolve: resolve3,
3816
+ resolve: resolve4,
3816
3817
  resolveComponent,
3817
3818
  equal,
3818
3819
  serialize,
@@ -10245,7 +10246,7 @@ var require_compile2 = __commonJS({
10245
10246
  const schOrFunc = root.refs[ref];
10246
10247
  if (schOrFunc)
10247
10248
  return schOrFunc;
10248
- let _sch = resolve3.call(this, root, ref);
10249
+ let _sch = resolve4.call(this, root, ref);
10249
10250
  if (_sch === void 0) {
10250
10251
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
10251
10252
  const { schemaId } = this.opts;
@@ -10272,7 +10273,7 @@ var require_compile2 = __commonJS({
10272
10273
  function sameSchemaEnv(s1, s2) {
10273
10274
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
10274
10275
  }
10275
- function resolve3(root, ref) {
10276
+ function resolve4(root, ref) {
10276
10277
  let sch;
10277
10278
  while (typeof (sch = this.refs[ref]) == "string")
10278
10279
  ref = sch;
@@ -17430,8 +17431,8 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
17430
17431
  var source_default = chalk;
17431
17432
 
17432
17433
  // src/commands/submit.ts
17433
- import { readFileSync, statSync as statSync2 } from "fs";
17434
- import { basename as basename3, resolve as resolve2 } from "path";
17434
+ import { readFileSync, statSync as statSync3 } from "fs";
17435
+ import { basename as basename3, resolve as resolve3 } from "path";
17435
17436
 
17436
17437
  // src/lib/scanner.ts
17437
17438
  init_esm_shims();
@@ -17867,7 +17868,7 @@ var retryifyAsync = (fn, options) => {
17867
17868
  throw error2;
17868
17869
  const delay = Math.round(interval * Math.random());
17869
17870
  if (delay > 0) {
17870
- const delayPromise = new Promise((resolve3) => setTimeout(resolve3, delay));
17871
+ const delayPromise = new Promise((resolve4) => setTimeout(resolve4, delay));
17871
17872
  return delayPromise.then(() => attempt.apply(void 0, args));
17872
17873
  } else {
17873
17874
  return attempt.apply(void 0, args);
@@ -18669,8 +18670,8 @@ var Conf = class {
18669
18670
  }
18670
18671
  try {
18671
18672
  const initializationVector = data.slice(0, 16);
18672
- const password = crypto2.pbkdf2Sync(this.#encryptionKey, initializationVector.toString(), 1e4, 32, "sha512");
18673
- const decipher = crypto2.createDecipheriv(encryptionAlgorithm, password, initializationVector);
18673
+ const password2 = crypto2.pbkdf2Sync(this.#encryptionKey, initializationVector.toString(), 1e4, 32, "sha512");
18674
+ const decipher = crypto2.createDecipheriv(encryptionAlgorithm, password2, initializationVector);
18674
18675
  const slice = data.slice(17);
18675
18676
  const dataUpdate = typeof slice === "string" ? stringToUint8Array(slice) : slice;
18676
18677
  return uint8ArrayToString(concatUint8Arrays([decipher.update(dataUpdate), decipher.final()]));
@@ -18714,8 +18715,8 @@ var Conf = class {
18714
18715
  let data = this._serialize(value);
18715
18716
  if (this.#encryptionKey) {
18716
18717
  const initializationVector = crypto2.randomBytes(16);
18717
- const password = crypto2.pbkdf2Sync(this.#encryptionKey, initializationVector.toString(), 1e4, 32, "sha512");
18718
- const cipher = crypto2.createCipheriv(encryptionAlgorithm, password, initializationVector);
18718
+ const password2 = crypto2.pbkdf2Sync(this.#encryptionKey, initializationVector.toString(), 1e4, 32, "sha512");
18719
+ const cipher = crypto2.createCipheriv(encryptionAlgorithm, password2, initializationVector);
18719
18720
  data = concatUint8Arrays([initializationVector, stringToUint8Array(":"), cipher.update(stringToUint8Array(data)), cipher.final()]);
18720
18721
  }
18721
18722
  if (process8.env.SNAP) {
@@ -18821,28 +18822,43 @@ var Conf = class {
18821
18822
  }
18822
18823
  };
18823
18824
 
18825
+ // src/lib/constants.ts
18826
+ init_esm_shims();
18827
+ var SUPABASE_URL = "https://cfqzdyktjhkalfrmcgmw.supabase.co";
18828
+ var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNmcXpkeWt0amhrYWxmcm1jZ213Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzM4MjYsImV4cCI6MjA4NDg0OTgyNn0.O1bPUNHw7kzpWecAyT4Pizh2ITRSal3PJsrUIkZY04o";
18829
+ var CALLBACK_PORT = 54321;
18830
+ var DEFAULT_API_URL = "https://preflightlaunch.com";
18831
+
18824
18832
  // src/lib/config.ts
18825
18833
  var config = new Conf({
18826
18834
  projectName: "preflight",
18827
18835
  schema: {
18828
18836
  accessToken: { type: "string" },
18829
18837
  refreshToken: { type: "string" },
18830
- apiUrl: { type: "string", default: "https://preflight.dev" },
18838
+ apiUrl: { type: "string", default: DEFAULT_API_URL },
18831
18839
  userId: { type: "string" },
18832
18840
  email: { type: "string" },
18833
18841
  hasRunBefore: { type: "boolean", default: false },
18834
- lastScannedPath: { type: "string" }
18842
+ lastScannedPath: { type: "string" },
18843
+ ascConnected: { type: "boolean", default: false }
18835
18844
  }
18836
18845
  });
18846
+ try {
18847
+ if (config.get("apiUrl") === "https://preflight.dev") {
18848
+ config.set("apiUrl", DEFAULT_API_URL);
18849
+ }
18850
+ } catch {
18851
+ }
18837
18852
  function getConfig() {
18838
18853
  return {
18839
18854
  accessToken: config.get("accessToken"),
18840
18855
  refreshToken: config.get("refreshToken"),
18841
- apiUrl: config.get("apiUrl") || "https://preflight.dev",
18856
+ apiUrl: config.get("apiUrl") || DEFAULT_API_URL,
18842
18857
  userId: config.get("userId"),
18843
18858
  email: config.get("email"),
18844
18859
  hasRunBefore: config.get("hasRunBefore") || false,
18845
- lastScannedPath: config.get("lastScannedPath")
18860
+ lastScannedPath: config.get("lastScannedPath"),
18861
+ ascConnected: config.get("ascConnected") || false
18846
18862
  };
18847
18863
  }
18848
18864
  function setTokens(accessToken, refreshToken) {
@@ -18874,6 +18890,12 @@ function setLastScannedPath(path5) {
18874
18890
  function getLastScannedPath() {
18875
18891
  return config.get("lastScannedPath");
18876
18892
  }
18893
+ function getAscConnected() {
18894
+ return config.get("ascConnected") || false;
18895
+ }
18896
+ function setAscConnected(connected) {
18897
+ config.set("ascConnected", connected);
18898
+ }
18877
18899
 
18878
18900
  // ../../node_modules/@supabase/supabase-js/dist/index.mjs
18879
18901
  init_esm_shims();
@@ -18899,11 +18921,11 @@ function __rest(s, e) {
18899
18921
  }
18900
18922
  function __awaiter(thisArg, _arguments, P3, generator) {
18901
18923
  function adopt(value) {
18902
- return value instanceof P3 ? value : new P3(function(resolve3) {
18903
- resolve3(value);
18924
+ return value instanceof P3 ? value : new P3(function(resolve4) {
18925
+ resolve4(value);
18904
18926
  });
18905
18927
  }
18906
- return new (P3 || (P3 = Promise))(function(resolve3, reject) {
18928
+ return new (P3 || (P3 = Promise))(function(resolve4, reject) {
18907
18929
  function fulfilled(value) {
18908
18930
  try {
18909
18931
  step(generator.next(value));
@@ -18919,7 +18941,7 @@ function __awaiter(thisArg, _arguments, P3, generator) {
18919
18941
  }
18920
18942
  }
18921
18943
  function step(result) {
18922
- result.done ? resolve3(result.value) : adopt(result.value).then(fulfilled, rejected);
18944
+ result.done ? resolve4(result.value) : adopt(result.value).then(fulfilled, rejected);
18923
18945
  }
18924
18946
  step((generator = generator.apply(thisArg, _arguments || [])).next());
18925
18947
  });
@@ -21472,15 +21494,15 @@ var RealtimeChannel = class _RealtimeChannel {
21472
21494
  }
21473
21495
  }
21474
21496
  } else {
21475
- return new Promise((resolve3) => {
21497
+ return new Promise((resolve4) => {
21476
21498
  var _a2, _b2, _c;
21477
21499
  const push = this._push(args.type, args, opts.timeout || this.timeout);
21478
21500
  if (args.type === "broadcast" && !((_c = (_b2 = (_a2 = this.params) === null || _a2 === void 0 ? void 0 : _a2.config) === null || _b2 === void 0 ? void 0 : _b2.broadcast) === null || _c === void 0 ? void 0 : _c.ack)) {
21479
- resolve3("ok");
21501
+ resolve4("ok");
21480
21502
  }
21481
- push.receive("ok", () => resolve3("ok"));
21482
- push.receive("error", () => resolve3("error"));
21483
- push.receive("timeout", () => resolve3("timed out"));
21503
+ push.receive("ok", () => resolve4("ok"));
21504
+ push.receive("error", () => resolve4("error"));
21505
+ push.receive("timeout", () => resolve4("timed out"));
21484
21506
  });
21485
21507
  }
21486
21508
  }
@@ -21508,16 +21530,16 @@ var RealtimeChannel = class _RealtimeChannel {
21508
21530
  };
21509
21531
  this.joinPush.destroy();
21510
21532
  let leavePush = null;
21511
- return new Promise((resolve3) => {
21533
+ return new Promise((resolve4) => {
21512
21534
  leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
21513
21535
  leavePush.receive("ok", () => {
21514
21536
  onClose();
21515
- resolve3("ok");
21537
+ resolve4("ok");
21516
21538
  }).receive("timeout", () => {
21517
21539
  onClose();
21518
- resolve3("timed out");
21540
+ resolve4("timed out");
21519
21541
  }).receive("error", () => {
21520
- resolve3("error");
21542
+ resolve4("error");
21521
21543
  });
21522
21544
  leavePush.send();
21523
21545
  if (!this._canPush()) {
@@ -23217,7 +23239,7 @@ var _getRequestParams = (method, options, parameters, body) => {
23217
23239
  return _objectSpread2(_objectSpread2({}, params), parameters);
23218
23240
  };
23219
23241
  async function _handleRequest(fetcher, method, url, options, parameters, body, namespace) {
23220
- return new Promise((resolve3, reject) => {
23242
+ return new Promise((resolve4, reject) => {
23221
23243
  fetcher(url, _getRequestParams(method, options, parameters, body)).then((result) => {
23222
23244
  if (!result.ok) throw result;
23223
23245
  if (options === null || options === void 0 ? void 0 : options.noResolveJson) return result;
@@ -23227,7 +23249,7 @@ async function _handleRequest(fetcher, method, url, options, parameters, body, n
23227
23249
  if (!contentType || !contentType.includes("application/json")) return {};
23228
23250
  }
23229
23251
  return result.json();
23230
- }).then((data) => resolve3(data)).catch((error2) => handleError(error2, reject, options, namespace));
23252
+ }).then((data) => resolve4(data)).catch((error2) => handleError(error2, reject, options, namespace));
23231
23253
  });
23232
23254
  }
23233
23255
  function createFetchApi(namespace = "storage") {
@@ -27724,7 +27746,7 @@ var GoTrueClient = class _GoTrueClient {
27724
27746
  try {
27725
27747
  let res;
27726
27748
  if ("email" in credentials) {
27727
- const { email, password, options } = credentials;
27749
+ const { email, password: password2, options } = credentials;
27728
27750
  let codeChallenge = null;
27729
27751
  let codeChallengeMethod = null;
27730
27752
  if (this.flowType === "pkce") {
@@ -27736,7 +27758,7 @@ var GoTrueClient = class _GoTrueClient {
27736
27758
  redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo,
27737
27759
  body: {
27738
27760
  email,
27739
- password,
27761
+ password: password2,
27740
27762
  data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {},
27741
27763
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
27742
27764
  code_challenge: codeChallenge,
@@ -27745,12 +27767,12 @@ var GoTrueClient = class _GoTrueClient {
27745
27767
  xform: _sessionResponse
27746
27768
  });
27747
27769
  } else if ("phone" in credentials) {
27748
- const { phone, password, options } = credentials;
27770
+ const { phone, password: password2, options } = credentials;
27749
27771
  res = await _request(this.fetch, "POST", `${this.url}/signup`, {
27750
27772
  headers: this.headers,
27751
27773
  body: {
27752
27774
  phone,
27753
- password,
27775
+ password: password2,
27754
27776
  data: (_b = options === null || options === void 0 ? void 0 : options.data) !== null && _b !== void 0 ? _b : {},
27755
27777
  channel: (_c = options === null || options === void 0 ? void 0 : options.channel) !== null && _c !== void 0 ? _c : "sms",
27756
27778
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }
@@ -27792,23 +27814,23 @@ var GoTrueClient = class _GoTrueClient {
27792
27814
  try {
27793
27815
  let res;
27794
27816
  if ("email" in credentials) {
27795
- const { email, password, options } = credentials;
27817
+ const { email, password: password2, options } = credentials;
27796
27818
  res = await _request(this.fetch, "POST", `${this.url}/token?grant_type=password`, {
27797
27819
  headers: this.headers,
27798
27820
  body: {
27799
27821
  email,
27800
- password,
27822
+ password: password2,
27801
27823
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }
27802
27824
  },
27803
27825
  xform: _sessionResponsePassword
27804
27826
  });
27805
27827
  } else if ("phone" in credentials) {
27806
- const { phone, password, options } = credentials;
27828
+ const { phone, password: password2, options } = credentials;
27807
27829
  res = await _request(this.fetch, "POST", `${this.url}/token?grant_type=password`, {
27808
27830
  headers: this.headers,
27809
27831
  body: {
27810
27832
  phone,
27811
- password,
27833
+ password: password2,
27812
27834
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken }
27813
27835
  },
27814
27836
  xform: _sessionResponsePassword
@@ -30308,13 +30330,6 @@ function shouldShowDeprecationWarning() {
30308
30330
  }
30309
30331
  if (shouldShowDeprecationWarning()) console.warn("\u26A0\uFE0F Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
30310
30332
 
30311
- // src/lib/constants.ts
30312
- init_esm_shims();
30313
- var SUPABASE_URL = "https://cfqzdyktjhkalfrmcgmw.supabase.co";
30314
- var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNmcXpkeWt0amhrYWxmcm1jZ213Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNzM4MjYsImV4cCI6MjA4NDg0OTgyNn0.O1bPUNHw7kzpWecAyT4Pizh2ITRSal3PJsrUIkZY04o";
30315
- var CALLBACK_PORT = 54321;
30316
- var DEFAULT_API_URL = "https://preflightlaunch.com";
30317
-
30318
30333
  // src/lib/api-client.ts
30319
30334
  var API_TIMEOUT_MS = 3e4;
30320
30335
  async function refreshSession() {
@@ -30379,10 +30394,14 @@ function decodeJwtPayload(token) {
30379
30394
  const decoded = Buffer.from(payload, "base64url").toString("utf-8");
30380
30395
  return JSON.parse(decoded);
30381
30396
  }
30382
- async function loginWithBrowser() {
30397
+ async function loginWithBrowser(mode = "login") {
30383
30398
  const { apiUrl } = getConfig();
30384
30399
  const baseUrl = apiUrl || DEFAULT_API_URL;
30385
- return new Promise((resolve3) => {
30400
+ if (mode === "signup") {
30401
+ await open_default(`${baseUrl}/auth/signup`);
30402
+ return null;
30403
+ }
30404
+ return new Promise((resolve4) => {
30386
30405
  const server = http.createServer(async (req, res) => {
30387
30406
  const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
30388
30407
  if (url.pathname === "/callback") {
@@ -30393,7 +30412,7 @@ async function loginWithBrowser() {
30393
30412
  res.writeHead(400, { "Content-Type": "text/html" });
30394
30413
  res.end(htmlPage("Login failed", errorMsg));
30395
30414
  server.close();
30396
- resolve3(null);
30415
+ resolve4(null);
30397
30416
  return;
30398
30417
  }
30399
30418
  try {
@@ -30405,12 +30424,12 @@ async function loginWithBrowser() {
30405
30424
  res.writeHead(200, { "Content-Type": "text/html" });
30406
30425
  res.end(htmlPage("Logged in to Preflight!", "You can close this tab and return to your terminal."));
30407
30426
  server.close();
30408
- resolve3({ email });
30427
+ resolve4({ email });
30409
30428
  } catch (err) {
30410
30429
  res.writeHead(500, { "Content-Type": "text/html" });
30411
30430
  res.end(htmlPage("Login failed", "Could not process authentication tokens."));
30412
30431
  server.close();
30413
- resolve3(null);
30432
+ resolve4(null);
30414
30433
  }
30415
30434
  } else {
30416
30435
  res.writeHead(404, { "Content-Type": "text/plain" });
@@ -30423,7 +30442,7 @@ async function loginWithBrowser() {
30423
30442
  } else {
30424
30443
  console.error(`Server error: ${err.message}`);
30425
30444
  }
30426
- resolve3(null);
30445
+ resolve4(null);
30427
30446
  });
30428
30447
  server.listen(CALLBACK_PORT, () => {
30429
30448
  const redirectTo = encodeURIComponent(`http://localhost:${CALLBACK_PORT}/callback`);
@@ -30432,7 +30451,7 @@ async function loginWithBrowser() {
30432
30451
  });
30433
30452
  setTimeout(() => {
30434
30453
  server.close();
30435
- resolve3(null);
30454
+ resolve4(null);
30436
30455
  }, 5 * 60 * 1e3);
30437
30456
  });
30438
30457
  }
@@ -32282,11 +32301,66 @@ function ora(options) {
32282
32301
  return new Ora(options);
32283
32302
  }
32284
32303
 
32304
+ // src/ui/theme.ts
32305
+ init_esm_shims();
32306
+ var brand = source_default.bold.hex("#E8700A");
32307
+ var brandDim = source_default.hex("#E8700A");
32308
+ var heading = source_default.bold.white;
32309
+ var subtext = source_default.dim;
32310
+ var muted = source_default.gray;
32311
+ var ok = source_default.green;
32312
+ var okBold = source_default.bold.green;
32313
+ var warning = source_default.yellow;
32314
+ var warningBold = source_default.bold.yellow;
32315
+ var critical = source_default.red;
32316
+ var criticalBold = source_default.bold.red;
32317
+ var info = source_default.hex("#E8700A");
32318
+ var infoBold = source_default.bold.hex("#E8700A");
32319
+ var icons = {
32320
+ check: ok("\u2714"),
32321
+ cross: critical("\u2716"),
32322
+ warn: warning("!"),
32323
+ info: info("i"),
32324
+ bullet: "\u25CF",
32325
+ circle: "\u25CB",
32326
+ arrow: "\u2192",
32327
+ block: "\u2588",
32328
+ blockDim: source_default.dim("\u2591"),
32329
+ file: "\u{1F4C4}",
32330
+ image: "\u{1F5BC}",
32331
+ plane: "\u{1F6EB}"
32332
+ };
32333
+ function scoreBar(score, width = 20) {
32334
+ const filled = Math.round(score / 100 * width);
32335
+ const empty = width - filled;
32336
+ let color;
32337
+ let label;
32338
+ if (score >= 80) {
32339
+ color = ok;
32340
+ label = "READY";
32341
+ } else if (score >= 60) {
32342
+ color = warning;
32343
+ label = "NEEDS ATTENTION";
32344
+ } else {
32345
+ color = critical;
32346
+ label = "AT RISK";
32347
+ }
32348
+ return `${score}/100 ${color(icons.block.repeat(filled))}${icons.blockDim.repeat(empty)} ${color(label)}`;
32349
+ }
32350
+ function formatBytes(bytes) {
32351
+ if (bytes < 1024) return `${bytes} B`;
32352
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
32353
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
32354
+ }
32355
+ var APP_VERSION = "0.2.0";
32356
+ var APP_NAME = "Preflight";
32357
+ var APP_TAGLINE = "App Store Review Scanner";
32358
+
32285
32359
  // src/ui/spinner.ts
32286
32360
  function createSpinner(text2) {
32287
32361
  return ora({
32288
32362
  text: text2,
32289
- color: "cyan",
32363
+ color: "yellow",
32290
32364
  spinner: "dots"
32291
32365
  });
32292
32366
  }
@@ -32673,6 +32747,76 @@ function D(t2, e, s) {
32673
32747
  const i = t2 + e, r = Math.max(s.length - 1, 0), n = i < 0 ? r : i > r ? 0 : i;
32674
32748
  return s[n].disabled ? D(n, e < 0 ? -1 : 1, s) : n;
32675
32749
  }
32750
+ var Mt = class extends x {
32751
+ options;
32752
+ cursor = 0;
32753
+ get _value() {
32754
+ return this.options[this.cursor].value;
32755
+ }
32756
+ get _enabledOptions() {
32757
+ return this.options.filter((e) => e.disabled !== true);
32758
+ }
32759
+ toggleAll() {
32760
+ const e = this._enabledOptions, s = this.value !== void 0 && this.value.length === e.length;
32761
+ this.value = s ? [] : e.map((i) => i.value);
32762
+ }
32763
+ toggleInvert() {
32764
+ const e = this.value;
32765
+ if (!e) return;
32766
+ const s = this._enabledOptions.filter((i) => !e.includes(i.value));
32767
+ this.value = s.map((i) => i.value);
32768
+ }
32769
+ toggleValue() {
32770
+ this.value === void 0 && (this.value = []);
32771
+ const e = this.value.includes(this._value);
32772
+ this.value = e ? this.value.filter((s) => s !== this._value) : [...this.value, this._value];
32773
+ }
32774
+ constructor(e) {
32775
+ super(e, false), this.options = e.options, this.value = [...e.initialValues ?? []];
32776
+ const s = Math.max(this.options.findIndex(({ value: i }) => i === e.cursorAt), 0);
32777
+ this.cursor = this.options[s].disabled ? D(s, 1, this.options) : s, this.on("key", (i) => {
32778
+ i === "a" && this.toggleAll(), i === "i" && this.toggleInvert();
32779
+ }), this.on("cursor", (i) => {
32780
+ switch (i) {
32781
+ case "left":
32782
+ case "up":
32783
+ this.cursor = D(this.cursor, -1, this.options);
32784
+ break;
32785
+ case "down":
32786
+ case "right":
32787
+ this.cursor = D(this.cursor, 1, this.options);
32788
+ break;
32789
+ case "space":
32790
+ this.toggleValue();
32791
+ break;
32792
+ }
32793
+ });
32794
+ }
32795
+ };
32796
+ var Lt = class extends x {
32797
+ _mask = "\u2022";
32798
+ get cursor() {
32799
+ return this._cursor;
32800
+ }
32801
+ get masked() {
32802
+ return this.userInput.replaceAll(/./g, this._mask);
32803
+ }
32804
+ get userInputWithCursor() {
32805
+ if (this.state === "submit" || this.state === "cancel") return this.masked;
32806
+ const e = this.userInput;
32807
+ if (this.cursor >= e.length) return `${this.masked}${import_picocolors.default.inverse(import_picocolors.default.hidden("_"))}`;
32808
+ const s = this.masked, i = s.slice(0, this.cursor), r = s.slice(this.cursor);
32809
+ return `${i}${import_picocolors.default.inverse(r[0])}${r.slice(1)}`;
32810
+ }
32811
+ clear() {
32812
+ this._clearUserInput();
32813
+ }
32814
+ constructor({ mask: e, ...s }) {
32815
+ super(s), this._mask = e ?? "\u2022", this.on("userInput", (i) => {
32816
+ this._setValue(i);
32817
+ });
32818
+ }
32819
+ };
32676
32820
  var Wt = class extends x {
32677
32821
  options;
32678
32822
  cursor = 0;
@@ -33042,6 +33186,105 @@ ${import_picocolors2.default.gray(x2)} ${e}
33042
33186
 
33043
33187
  `);
33044
33188
  };
33189
+ var Q2 = (e, r) => e.split(`
33190
+ `).map((s) => r(s)).join(`
33191
+ `);
33192
+ var Lt2 = (e) => {
33193
+ const r = (i, n) => {
33194
+ const o = i.label ?? String(i.value);
33195
+ return n === "disabled" ? `${import_picocolors2.default.gray(z2)} ${Q2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.gray(u)))}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint ?? "disabled"})`)}` : ""}` : n === "active" ? `${import_picocolors2.default.cyan(te)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : n === "selected" ? `${import_picocolors2.default.green(G2)} ${Q2(o, import_picocolors2.default.dim)}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : n === "cancelled" ? `${Q2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.dim(u)))}` : n === "active-selected" ? `${import_picocolors2.default.green(G2)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : n === "submitted" ? `${Q2(o, import_picocolors2.default.dim)}` : `${import_picocolors2.default.dim(z2)} ${Q2(o, import_picocolors2.default.dim)}`;
33196
+ }, s = e.required ?? true;
33197
+ return new Mt({ options: e.options, signal: e.signal, input: e.input, output: e.output, initialValues: e.initialValues, required: s, cursorAt: e.cursorAt, validate(i) {
33198
+ if (s && (i === void 0 || i.length === 0)) return `Please select at least one option.
33199
+ ${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
33200
+ }, render() {
33201
+ const i = Bt(e.output, e.message, `${Ee(this.state)} `, `${N2(this.state)} `), n = `${import_picocolors2.default.gray(h)}
33202
+ ${i}
33203
+ `, o = this.value ?? [], u = (l, a) => {
33204
+ if (l.disabled) return r(l, "disabled");
33205
+ const d = o.includes(l.value);
33206
+ return a && d ? r(l, "active-selected") : d ? r(l, "selected") : r(l, a ? "active" : "inactive");
33207
+ };
33208
+ switch (this.state) {
33209
+ case "submit": {
33210
+ const l = this.options.filter(({ value: d }) => o.includes(d)).map((d) => r(d, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none"), a = Bt(e.output, l, `${import_picocolors2.default.gray(h)} `);
33211
+ return `${n}${a}`;
33212
+ }
33213
+ case "cancel": {
33214
+ const l = this.options.filter(({ value: d }) => o.includes(d)).map((d) => r(d, "cancelled")).join(import_picocolors2.default.dim(", "));
33215
+ if (l.trim() === "") return `${n}${import_picocolors2.default.gray(h)}`;
33216
+ const a = Bt(e.output, l, `${import_picocolors2.default.gray(h)} `);
33217
+ return `${n}${a}
33218
+ ${import_picocolors2.default.gray(h)}`;
33219
+ }
33220
+ case "error": {
33221
+ const l = `${import_picocolors2.default.yellow(h)} `, a = this.error.split(`
33222
+ `).map((E, p) => p === 0 ? `${import_picocolors2.default.yellow(x2)} ${import_picocolors2.default.yellow(E)}` : ` ${E}`).join(`
33223
+ `), d = n.split(`
33224
+ `).length, g = a.split(`
33225
+ `).length + 1;
33226
+ return `${n}${l}${J2({ output: e.output, options: this.options, cursor: this.cursor, maxItems: e.maxItems, columnPadding: l.length, rowPadding: d + g, style: u }).join(`
33227
+ ${l}`)}
33228
+ ${a}
33229
+ `;
33230
+ }
33231
+ default: {
33232
+ const l = `${import_picocolors2.default.cyan(h)} `, a = n.split(`
33233
+ `).length;
33234
+ return `${n}${l}${J2({ output: e.output, options: this.options, cursor: this.cursor, maxItems: e.maxItems, columnPadding: l.length, rowPadding: a + 2, style: u }).join(`
33235
+ ${l}`)}
33236
+ ${import_picocolors2.default.cyan(x2)}
33237
+ `;
33238
+ }
33239
+ }
33240
+ } }).prompt();
33241
+ };
33242
+ var jt = (e) => import_picocolors2.default.dim(e);
33243
+ var Vt2 = (e, r, s) => {
33244
+ const i = { hard: true, trim: false }, n = q2(e, r, i).split(`
33245
+ `), o = n.reduce((a, d) => Math.max(M2(d), a), 0), u = n.map(s).reduce((a, d) => Math.max(M2(d), a), 0), l = r - (u - o);
33246
+ return q2(e, l, i);
33247
+ };
33248
+ var kt2 = (e = "", r = "", s) => {
33249
+ const i = s?.output ?? P2.stdout, n = (s?.withGuide ?? _.withGuide) !== false, o = s?.format ?? jt, u = ["", ...Vt2(e, rt(i) - 6, o).split(`
33250
+ `).map(o), ""], l = M2(r), a = Math.max(u.reduce((p, y2) => {
33251
+ const $ = M2(y2);
33252
+ return $ > p ? $ : p;
33253
+ }, 0), l) + 2, d = u.map((p) => `${import_picocolors2.default.gray(h)} ${p}${" ".repeat(a - M2(p))}${import_picocolors2.default.gray(h)}`).join(`
33254
+ `), g = n ? `${import_picocolors2.default.gray(h)}
33255
+ ` : "", E = n ? Ne : pe;
33256
+ i.write(`${g}${import_picocolors2.default.green(k2)} ${import_picocolors2.default.reset(r)} ${import_picocolors2.default.gray(se.repeat(Math.max(a - l - 1, 1)) + he)}
33257
+ ${d}
33258
+ ${import_picocolors2.default.gray(E + se.repeat(a + 2) + me)}
33259
+ `);
33260
+ };
33261
+ var Gt = (e) => new Lt({ validate: e.validate, mask: e.mask ?? Pe, signal: e.signal, input: e.input, output: e.output, render() {
33262
+ const r = `${import_picocolors2.default.gray(h)}
33263
+ ${N2(this.state)} ${e.message}
33264
+ `, s = this.userInputWithCursor, i = this.masked;
33265
+ switch (this.state) {
33266
+ case "error": {
33267
+ const n = i ? ` ${i}` : "";
33268
+ return e.clearOnError && this.clear(), `${r.trim()}
33269
+ ${import_picocolors2.default.yellow(h)}${n}
33270
+ ${import_picocolors2.default.yellow(x2)} ${import_picocolors2.default.yellow(this.error)}
33271
+ `;
33272
+ }
33273
+ case "submit": {
33274
+ const n = i ? ` ${import_picocolors2.default.dim(i)}` : "";
33275
+ return `${r}${import_picocolors2.default.gray(h)}${n}`;
33276
+ }
33277
+ case "cancel": {
33278
+ const n = i ? ` ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(i))}` : "";
33279
+ return `${r}${import_picocolors2.default.gray(h)}${n}${i ? `
33280
+ ${import_picocolors2.default.gray(h)}` : ""}`;
33281
+ }
33282
+ default:
33283
+ return `${r}${import_picocolors2.default.cyan(h)} ${s}
33284
+ ${import_picocolors2.default.cyan(x2)}
33285
+ `;
33286
+ }
33287
+ } }).prompt();
33045
33288
  var Ut = import_picocolors2.default.magenta;
33046
33289
  var Ie = ({ indicator: e = "dots", onCancel: r, output: s = process.stdout, cancelMessage: i, errorMessage: n, frames: o = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: u = ee ? 80 : 120, signal: l, ...a } = {}) => {
33047
33290
  const d = ue();
@@ -33172,61 +33415,6 @@ ${l}
33172
33415
  }
33173
33416
  } }).prompt();
33174
33417
 
33175
- // src/ui/theme.ts
33176
- init_esm_shims();
33177
- var brand = source_default.bold.cyan;
33178
- var brandDim = source_default.cyan;
33179
- var heading = source_default.bold.white;
33180
- var subtext = source_default.dim;
33181
- var muted = source_default.gray;
33182
- var ok = source_default.green;
33183
- var okBold = source_default.bold.green;
33184
- var warning = source_default.yellow;
33185
- var warningBold = source_default.bold.yellow;
33186
- var critical = source_default.red;
33187
- var criticalBold = source_default.bold.red;
33188
- var info = source_default.blue;
33189
- var infoBold = source_default.bold.blue;
33190
- var icons = {
33191
- check: ok("\u2714"),
33192
- cross: critical("\u2716"),
33193
- warn: warning("!"),
33194
- info: info("i"),
33195
- bullet: "\u25CF",
33196
- circle: "\u25CB",
33197
- arrow: "\u2192",
33198
- block: "\u2588",
33199
- blockDim: source_default.dim("\u2591"),
33200
- file: "\u{1F4C4}",
33201
- image: "\u{1F5BC}",
33202
- plane: "\u{1F6EB}"
33203
- };
33204
- function scoreBar(score, width = 20) {
33205
- const filled = Math.round(score / 100 * width);
33206
- const empty = width - filled;
33207
- let color;
33208
- let label;
33209
- if (score >= 80) {
33210
- color = ok;
33211
- label = "READY";
33212
- } else if (score >= 60) {
33213
- color = warning;
33214
- label = "NEEDS ATTENTION";
33215
- } else {
33216
- color = critical;
33217
- label = "AT RISK";
33218
- }
33219
- return `${score}/100 ${color(icons.block.repeat(filled))}${icons.blockDim.repeat(empty)} ${color(label)}`;
33220
- }
33221
- function formatBytes(bytes) {
33222
- if (bytes < 1024) return `${bytes} B`;
33223
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
33224
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
33225
- }
33226
- var APP_VERSION = "0.2.0";
33227
- var APP_NAME = "Preflight";
33228
- var APP_TAGLINE = "App Store Review Scanner";
33229
-
33230
33418
  // src/ui/report.ts
33231
33419
  function renderReport(report, items) {
33232
33420
  console.log();
@@ -33248,7 +33436,7 @@ function renderReport(report, items) {
33248
33436
  for (const item of criticals) {
33249
33437
  console.log(` ${critical(item.title)}`);
33250
33438
  if (item.fix_suggestion) {
33251
- console.log(` ${source_default.cyan(icons.arrow)} ${item.fix_suggestion}`);
33439
+ console.log(` ${brandDim(icons.arrow)} ${item.fix_suggestion}`);
33252
33440
  }
33253
33441
  if (item.description && item.description !== item.title) {
33254
33442
  console.log(` ${subtext(item.description)}`);
@@ -33262,7 +33450,7 @@ function renderReport(report, items) {
33262
33450
  for (const item of warnings) {
33263
33451
  console.log(` ${warning(item.title)}`);
33264
33452
  if (item.fix_suggestion) {
33265
- console.log(` ${source_default.cyan(icons.arrow)} ${item.fix_suggestion}`);
33453
+ console.log(` ${brandDim(icons.arrow)} ${item.fix_suggestion}`);
33266
33454
  }
33267
33455
  console.log();
33268
33456
  }
@@ -33312,9 +33500,10 @@ function renderReportJson(report, items) {
33312
33500
 
33313
33501
  // src/lib/project-finder.ts
33314
33502
  init_esm_shims();
33315
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
33316
- import { join as join2, basename as basename2 } from "path";
33317
- import { homedir as homedir2 } from "os";
33503
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
33504
+ import { execFileSync } from "child_process";
33505
+ import { join as join2, basename as basename2, dirname, resolve as resolve2 } from "path";
33506
+ import { homedir as homedir2, platform } from "os";
33318
33507
 
33319
33508
  // src/ui/interactive.ts
33320
33509
  init_esm_shims();
@@ -33324,24 +33513,24 @@ function intro(title) {
33324
33513
  R2.info(title);
33325
33514
  }
33326
33515
  }
33327
- function showTagline() {
33328
- R2.message(`${APP_TAGLINE}
33329
- Catch rejection reasons before Apple does.`);
33330
- }
33331
- function brandSplash() {
33332
- console.log();
33333
- console.log(brand(` ${APP_NAME.split("").join(" ")}`));
33334
- console.log();
33335
- console.log(subtext(` ${APP_TAGLINE}`));
33336
- console.log(subtext(` v${APP_VERSION}`));
33337
- console.log();
33338
- }
33339
33516
  function outro(message) {
33340
33517
  Wt2(message || "Done!");
33341
33518
  }
33342
33519
  function tip(message) {
33343
33520
  console.log();
33344
- console.log(subtext(` \u{1F4A1} Tip: ${message}`));
33521
+ console.log(subtext(` Tip: ${message}`));
33522
+ console.log();
33523
+ }
33524
+ function renderHeader(email, credits) {
33525
+ clearScreen();
33526
+ console.log();
33527
+ console.log(brand(` ${APP_NAME.split("").map((c) => c.toUpperCase()).join(" ")}`));
33528
+ if (email) {
33529
+ const creditDisplay = credits !== void 0 ? credits < 100 ? source_default.yellow(`${credits} credits`) : `${credits} credits` : "";
33530
+ console.log(subtext(` ${email}${creditDisplay ? ` \xB7 ${creditDisplay}` : ""}`));
33531
+ } else {
33532
+ console.log(subtext(` ${APP_TAGLINE}`));
33533
+ }
33345
33534
  console.log();
33346
33535
  }
33347
33536
  async function select(opts) {
@@ -33350,7 +33539,17 @@ async function select(opts) {
33350
33539
  options: opts.options
33351
33540
  });
33352
33541
  if (Ct(result)) {
33353
- Pt("Cancelled.");
33542
+ return null;
33543
+ }
33544
+ return result;
33545
+ }
33546
+ async function multiselect(opts) {
33547
+ const result = await Lt2({
33548
+ message: opts.message,
33549
+ options: opts.options,
33550
+ required: opts.required ?? false
33551
+ });
33552
+ if (Ct(result)) {
33354
33553
  return null;
33355
33554
  }
33356
33555
  return result;
@@ -33358,7 +33557,6 @@ async function select(opts) {
33358
33557
  async function confirm(message, initialValue = true) {
33359
33558
  const result = await Mt2({ message, initialValue });
33360
33559
  if (Ct(result)) {
33361
- Pt("Cancelled.");
33362
33560
  return null;
33363
33561
  }
33364
33562
  return result;
@@ -33366,7 +33564,13 @@ async function confirm(message, initialValue = true) {
33366
33564
  async function text(opts) {
33367
33565
  const result = await Qt(opts);
33368
33566
  if (Ct(result)) {
33369
- Pt("Cancelled.");
33567
+ return null;
33568
+ }
33569
+ return result;
33570
+ }
33571
+ async function password(opts) {
33572
+ const result = await Gt({ message: opts.message, validate: opts.validate });
33573
+ if (Ct(result)) {
33370
33574
  return null;
33371
33575
  }
33372
33576
  return result;
@@ -33375,6 +33579,14 @@ function spinner() {
33375
33579
  return Ie();
33376
33580
  }
33377
33581
  var log = R2;
33582
+ function clearScreen() {
33583
+ if (process.stdout.isTTY) {
33584
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
33585
+ }
33586
+ }
33587
+ function note(message, title) {
33588
+ kt2(message, title);
33589
+ }
33378
33590
 
33379
33591
  // src/lib/project-finder.ts
33380
33592
  var SEARCH_DIRS = [
@@ -33386,6 +33598,45 @@ var SEARCH_DIRS = [
33386
33598
  "code",
33387
33599
  "dev"
33388
33600
  ];
33601
+ var NOISE_PATTERNS = [
33602
+ "/Pods/",
33603
+ "/DerivedData/",
33604
+ "/Carthage/",
33605
+ "/build/",
33606
+ "/.build/",
33607
+ "/SourcePackages/",
33608
+ "/Library/Developer/",
33609
+ "/.Trash/"
33610
+ ];
33611
+ function spotlightSearch() {
33612
+ if (platform() !== "darwin") return [];
33613
+ try {
33614
+ const output = execFileSync("mdfind", [
33615
+ 'kMDItemFSName == "*.xcodeproj" || kMDItemFSName == "*.xcworkspace"'
33616
+ ], { encoding: "utf-8", timeout: 5e3 });
33617
+ const lines = output.trim().split("\n").filter(Boolean);
33618
+ const seen = /* @__PURE__ */ new Set();
33619
+ const projects = [];
33620
+ for (const fullPath of lines) {
33621
+ if (NOISE_PATTERNS.some((pattern) => fullPath.includes(pattern))) continue;
33622
+ const projectDir = dirname(fullPath);
33623
+ if (seen.has(projectDir)) continue;
33624
+ seen.add(projectDir);
33625
+ const type = fullPath.endsWith(".xcworkspace") ? "xcworkspace" : "xcodeproj";
33626
+ const name = basename2(fullPath).replace(/\.(xcodeproj|xcworkspace)$/, "");
33627
+ let modifiedAt;
33628
+ try {
33629
+ modifiedAt = statSync2(fullPath).mtimeMs;
33630
+ } catch {
33631
+ }
33632
+ projects.push({ name, path: projectDir, type, fullPath, modifiedAt });
33633
+ }
33634
+ projects.sort((a, b) => (b.modifiedAt ?? 0) - (a.modifiedAt ?? 0));
33635
+ return projects;
33636
+ } catch {
33637
+ return [];
33638
+ }
33639
+ }
33389
33640
  function findXcodeProjects(extraDirs = []) {
33390
33641
  const home = homedir2();
33391
33642
  const projects = [];
@@ -33447,9 +33698,23 @@ function findProjectInDir(dir) {
33447
33698
  }
33448
33699
  return null;
33449
33700
  }
33701
+ function browseWithFinder() {
33702
+ if (platform() !== "darwin") return null;
33703
+ try {
33704
+ const result = execFileSync("osascript", [
33705
+ "-e",
33706
+ 'POSIX path of (choose folder with prompt "Select your Xcode project folder")'
33707
+ ], { encoding: "utf-8", timeout: 12e4 });
33708
+ const path5 = result.trim();
33709
+ return path5.endsWith("/") ? path5.slice(0, -1) : path5;
33710
+ } catch {
33711
+ return null;
33712
+ }
33713
+ }
33450
33714
  function buildProjectChoices(lastScannedPath) {
33451
33715
  const choices = [];
33452
33716
  const cwd = process.cwd();
33717
+ const addedPaths = /* @__PURE__ */ new Set();
33453
33718
  if (lastScannedPath && existsSync2(lastScannedPath)) {
33454
33719
  const proj = findProjectInDir(lastScannedPath);
33455
33720
  if (proj) {
@@ -33458,24 +33723,37 @@ function buildProjectChoices(lastScannedPath) {
33458
33723
  label: `${proj.name} (last scanned)`,
33459
33724
  hint: shortenPath(lastScannedPath)
33460
33725
  });
33726
+ addedPaths.add(lastScannedPath);
33461
33727
  }
33462
33728
  }
33463
33729
  const cwdProj = findProjectInDir(cwd);
33464
- if (cwdProj && cwd !== lastScannedPath) {
33730
+ if (cwdProj && !addedPaths.has(cwd)) {
33465
33731
  choices.push({
33466
33732
  value: cwd,
33467
33733
  label: cwdProj.name,
33468
- hint: `Current directory - ${shortenPath(cwd)}`
33734
+ hint: `Current directory`
33469
33735
  });
33736
+ addedPaths.add(cwd);
33470
33737
  }
33471
- const found = findXcodeProjects();
33472
- for (const proj of found.slice(0, 5)) {
33473
- if (proj.path === lastScannedPath || proj.path === cwd) continue;
33738
+ let found = spotlightSearch();
33739
+ if (found.length === 0) {
33740
+ found = findXcodeProjects();
33741
+ }
33742
+ for (const proj of found.slice(0, 8)) {
33743
+ if (addedPaths.has(proj.path)) continue;
33474
33744
  choices.push({
33475
33745
  value: proj.path,
33476
33746
  label: proj.name,
33477
33747
  hint: shortenPath(proj.path)
33478
33748
  });
33749
+ addedPaths.add(proj.path);
33750
+ }
33751
+ if (platform() === "darwin") {
33752
+ choices.push({
33753
+ value: "__finder__",
33754
+ label: "Open Finder to pick a folder",
33755
+ hint: "Browse with macOS file picker"
33756
+ });
33479
33757
  }
33480
33758
  choices.push({
33481
33759
  value: "__manual__",
@@ -33486,15 +33764,20 @@ function buildProjectChoices(lastScannedPath) {
33486
33764
  }
33487
33765
  async function interactiveProjectSelect() {
33488
33766
  const choices = buildProjectChoices(getLastScannedPath());
33489
- if (choices.length <= 1) {
33490
- log.info("No Xcode projects found in common locations.");
33491
- return promptForManualPath();
33767
+ const realProjects = choices.filter((c) => !c.value.startsWith("__"));
33768
+ if (realProjects.length === 0) {
33769
+ log.info("No Xcode projects found on your Mac.");
33492
33770
  }
33493
33771
  const selected = await select({
33494
33772
  message: "Where's your Xcode project?",
33495
33773
  options: choices
33496
33774
  });
33497
33775
  if (selected === null) return null;
33776
+ if (selected === "__finder__") {
33777
+ const finderPath = browseWithFinder();
33778
+ if (!finderPath) return null;
33779
+ return finderPath;
33780
+ }
33498
33781
  if (selected === "__manual__") return promptForManualPath();
33499
33782
  return selected;
33500
33783
  }
@@ -33504,6 +33787,8 @@ async function promptForManualPath() {
33504
33787
  placeholder: "./MyApp",
33505
33788
  validate: (val) => {
33506
33789
  if (!val?.trim()) return "Path is required";
33790
+ const resolved = resolve2(val.trim());
33791
+ if (!existsSync2(resolved)) return "Directory not found";
33507
33792
  }
33508
33793
  });
33509
33794
  }
@@ -33527,7 +33812,8 @@ var KNOWN_COMMANDS = [
33527
33812
  { name: "status", description: "Check analysis status" },
33528
33813
  { name: "report", description: "View analysis report" },
33529
33814
  { name: "history", description: "List past submissions" },
33530
- { name: "setup", description: "Run guided setup" }
33815
+ { name: "setup", description: "Run guided setup" },
33816
+ { name: "asc", description: "App Store Connect integration" }
33531
33817
  ];
33532
33818
  function levenshtein(a, b) {
33533
33819
  const matrix = [];
@@ -33568,7 +33854,7 @@ function handleUnknownCommand(cmdName) {
33568
33854
  console.log();
33569
33855
  console.log(source_default.dim(" Did you mean:"));
33570
33856
  for (const s of suggestions) {
33571
- console.log(` ${source_default.cyan("\u2192")} ${brand(`preflight ${s.name}`)} ${source_default.dim(s.description)}`);
33857
+ console.log(` ${brandDim("\u2192")} ${brand(`preflight ${s.name}`)} ${source_default.dim(s.description)}`);
33572
33858
  }
33573
33859
  }
33574
33860
  console.log();
@@ -33590,14 +33876,532 @@ async function promptLogin() {
33590
33876
  return result === "login";
33591
33877
  }
33592
33878
 
33879
+ // src/lib/submission-questions.ts
33880
+ init_esm_shims();
33881
+ var CATEGORIES = [
33882
+ "Business",
33883
+ "Developer Tools",
33884
+ "Education",
33885
+ "Finance",
33886
+ "Health & Fitness",
33887
+ "Lifestyle",
33888
+ "Productivity",
33889
+ "Social Networking",
33890
+ "Utilities"
33891
+ ];
33892
+ var AGE_RATING_CONTENT_TYPES = [
33893
+ { value: "cartoonViolence", label: "Cartoon violence", hint: "e.g., Tom & Jerry style" },
33894
+ { value: "realisticViolence", label: "Realistic violence", hint: "e.g., combat games" },
33895
+ { value: "prolongedViolence", label: "Graphic/intense violence", hint: "e.g., gore, torture" },
33896
+ { value: "sexualContent", label: "Sexual content or nudity", hint: "e.g., explicit images" },
33897
+ { value: "matureSuggestive", label: "Mature or suggestive themes", hint: "e.g., dating, romance" },
33898
+ { value: "profanity", label: "Swearing or crude humor", hint: "e.g., curse words" },
33899
+ { value: "alcoholDrugs", label: "Alcohol, tobacco, or drugs", hint: "e.g., drinking scenes" },
33900
+ { value: "gamblingSimulated", label: "Gambling (no real money)", hint: "e.g., casino games with fake chips" },
33901
+ { value: "horrorFear", label: "Horror or scary content", hint: "e.g., jump scares" },
33902
+ { value: "medicalTreatment", label: "Medical or health advice", hint: "e.g., treatment suggestions" },
33903
+ { value: "gamblingContests", label: "Real money gambling or paid contests", hint: "e.g., betting, fantasy sports" }
33904
+ ];
33905
+ var PRIVACY_DATA_TYPES = [
33906
+ { value: "contact", label: "Contact Info", hint: "Name, email, phone, address" },
33907
+ { value: "health", label: "Health & Fitness", hint: "Workouts, steps, medical info" },
33908
+ { value: "financial", label: "Financial Info", hint: "Credit cards, bank details" },
33909
+ { value: "location", label: "Location", hint: "GPS, location data" },
33910
+ { value: "sensitive", label: "Sensitive Info", hint: "Race, religion, politics" },
33911
+ { value: "contacts", label: "Contacts", hint: "Phone contacts, address book" },
33912
+ { value: "content", label: "User Content", hint: "Photos, videos, posts" },
33913
+ { value: "browsing", label: "Browsing History", hint: "Websites visited" },
33914
+ { value: "search", label: "Search History", hint: "In-app searches" },
33915
+ { value: "identifiers", label: "Identifiers", hint: "User ID, device ID" },
33916
+ { value: "purchases", label: "Purchases", hint: "Purchase history" },
33917
+ { value: "usage", label: "Usage Data", hint: "Button taps, feature usage" },
33918
+ { value: "diagnostics", label: "Diagnostics", hint: "Crash reports (Firebase, Sentry)" },
33919
+ { value: "other", label: "Other Data", hint: "Anything else not listed" }
33920
+ ];
33921
+ var FEATURE_ITEMS = [
33922
+ { value: "ugc", label: "User Posts & Uploads", hint: "Comments, photos, sharing" },
33923
+ { value: "login", label: "Account / Login", hint: "Sign up or log in required" },
33924
+ { value: "iap", label: "Pay to Unlock Features", hint: "One-time purchases" },
33925
+ { value: "subscriptions", label: "Subscription / Recurring Payment", hint: "Weekly, monthly, yearly" },
33926
+ { value: "ads", label: "Ads in Your App", hint: "Banner, video, or sponsored" },
33927
+ { value: "thirdPartyLogin", label: "Sign in with Apple / Google", hint: "Social login" },
33928
+ { value: "aiContent", label: "AI-Generated Content", hint: "ChatGPT, DALL-E, etc." },
33929
+ { value: "healthClaims", label: "Health / Medical Advice", hint: "Diagnosis, treatment" },
33930
+ { value: "crypto", label: "Crypto / NFTs", hint: "Buy, sell, trade" },
33931
+ { value: "miniApps", label: "Mini Apps / Plugins", hint: "Hosts mini games, chatbots, or plugins" },
33932
+ { value: "euDistribution", label: "Available in the EU", hint: "Distributed on EU App Store" },
33933
+ { value: "externalPayments", label: "External Payment Links (US)", hint: "Links to pay outside Apple" }
33934
+ ];
33935
+ function calculateAgeRating(answers) {
33936
+ if (answers.prolongedViolence === 2 || answers.sexualContent === 2 || answers.gamblingSimulated === 2 || answers.gamblingContests > 0) {
33937
+ return "17+";
33938
+ }
33939
+ if (answers.realisticViolence > 0 || answers.sexualContent > 0 || answers.matureSuggestive === 2 || answers.alcoholDrugs === 2 || answers.gamblingSimulated > 0) {
33940
+ return "12+";
33941
+ }
33942
+ if (answers.cartoonViolence === 2 || answers.matureSuggestive > 0 || answers.profanity === 2 || answers.horrorFear === 2) {
33943
+ return "9+";
33944
+ }
33945
+ return "4+";
33946
+ }
33947
+ async function collectAppDetails(projectName, defaults) {
33948
+ if (!defaults) {
33949
+ const skipGate = await select({
33950
+ message: "App Details (you can always add these later on the web)",
33951
+ options: [
33952
+ { value: "fill", label: "Fill in now", hint: "Name, description, keywords, category" },
33953
+ { value: "skip", label: "Skip for now", hint: "Just use the project name" }
33954
+ ]
33955
+ });
33956
+ if (skipGate === null) return null;
33957
+ if (skipGate === "skip") {
33958
+ return {
33959
+ appName: projectName,
33960
+ signInRequired: false
33961
+ };
33962
+ }
33963
+ }
33964
+ const defaultName = defaults?.appName || projectName;
33965
+ const appName = await text({
33966
+ message: "App Name",
33967
+ placeholder: defaultName,
33968
+ defaultValue: defaultName,
33969
+ validate: (val) => {
33970
+ if (!val?.trim()) return "App name is required";
33971
+ }
33972
+ });
33973
+ if (appName === null) return null;
33974
+ const description = await text({
33975
+ message: "Description (press Enter to skip)",
33976
+ placeholder: "Describe your app as it appears in the App Store",
33977
+ ...defaults?.description ? { defaultValue: defaults.description } : {}
33978
+ });
33979
+ if (description === null) return null;
33980
+ const keywords = await text({
33981
+ message: "Keywords (press Enter to skip)",
33982
+ placeholder: "Comma-separated, 100 chars max",
33983
+ ...defaults?.keywords ? { defaultValue: defaults.keywords } : {},
33984
+ validate: (val) => {
33985
+ if (val && val.length > 100) return "Keywords must be 100 characters or less";
33986
+ }
33987
+ });
33988
+ if (keywords === null) return null;
33989
+ const promotionalText = await text({
33990
+ message: "Promotional Text (press Enter to skip)",
33991
+ placeholder: "Short promotional text, 170 chars max",
33992
+ ...defaults?.promotionalText ? { defaultValue: defaults.promotionalText } : {},
33993
+ validate: (val) => {
33994
+ if (val && val.length > 170) return "Promotional text must be 170 characters or less";
33995
+ }
33996
+ });
33997
+ if (promotionalText === null) return null;
33998
+ const categoryOptions = [
33999
+ { value: "__skip__", label: "Skip", hint: "Choose later" },
34000
+ ...CATEGORIES.map((c) => ({ value: c, label: c }))
34001
+ ];
34002
+ const category = await select({
34003
+ message: "Primary Category",
34004
+ options: categoryOptions,
34005
+ ...defaults?.category ? { initialValue: defaults.category } : {}
34006
+ });
34007
+ if (category === null) return null;
34008
+ const supportUrl = await text({
34009
+ message: "Support URL (press Enter to skip)",
34010
+ placeholder: "https://example.com/support",
34011
+ ...defaults?.supportUrl ? { defaultValue: defaults.supportUrl } : {}
34012
+ });
34013
+ if (supportUrl === null) return null;
34014
+ const marketingUrl = await text({
34015
+ message: "Marketing URL (press Enter to skip)",
34016
+ placeholder: "https://example.com",
34017
+ ...defaults?.marketingUrl ? { defaultValue: defaults.marketingUrl } : {}
34018
+ });
34019
+ if (marketingUrl === null) return null;
34020
+ const signInRequired = await confirm(
34021
+ "Does your app require sign-in for review?",
34022
+ defaults?.signInRequired ?? false
34023
+ );
34024
+ if (signInRequired === null) return null;
34025
+ let demoUsername;
34026
+ let demoPassword;
34027
+ if (signInRequired) {
34028
+ const email = await text({
34029
+ message: "Demo Email",
34030
+ placeholder: "test@example.com",
34031
+ ...defaults?.demoUsername ? { defaultValue: defaults.demoUsername } : {},
34032
+ validate: (val) => {
34033
+ if (!val?.trim()) return "Demo email is required when sign-in is required";
34034
+ }
34035
+ });
34036
+ if (email === null) return null;
34037
+ demoUsername = email;
34038
+ const pass = await password({
34039
+ message: "Demo Password",
34040
+ validate: (val) => {
34041
+ if (!val?.trim()) return "Demo password is required when sign-in is required";
34042
+ }
34043
+ });
34044
+ if (pass === null) return null;
34045
+ demoPassword = pass;
34046
+ }
34047
+ return {
34048
+ appName: appName.trim(),
34049
+ description: description?.trim() || void 0,
34050
+ keywords: keywords?.trim() || void 0,
34051
+ category: category === "__skip__" ? void 0 : category,
34052
+ supportUrl: supportUrl?.trim() || void 0,
34053
+ promotionalText: promotionalText?.trim() || void 0,
34054
+ marketingUrl: marketingUrl?.trim() || void 0,
34055
+ signInRequired,
34056
+ demoUsername,
34057
+ demoPassword
34058
+ };
34059
+ }
34060
+ async function collectAgeRating() {
34061
+ log.step(subtext("Step 1 of 3: Age Rating"));
34062
+ const defaultAnswers = {
34063
+ cartoonViolence: 0,
34064
+ realisticViolence: 0,
34065
+ prolongedViolence: 0,
34066
+ sexualContent: 0,
34067
+ matureSuggestive: 0,
34068
+ profanity: 0,
34069
+ alcoholDrugs: 0,
34070
+ gamblingSimulated: 0,
34071
+ horrorFear: 0,
34072
+ medicalTreatment: 0,
34073
+ gamblingContests: 0,
34074
+ unrestrictedWebAccess: false,
34075
+ madeForKids: false
34076
+ };
34077
+ const hasMatureContent = await confirm(
34078
+ "Does your app contain any mature content? (violence, sexual content, drugs, gambling, horror)",
34079
+ false
34080
+ );
34081
+ if (hasMatureContent === null) return null;
34082
+ if (!hasMatureContent) {
34083
+ const rating = calculateAgeRating(defaultAnswers);
34084
+ log.success(`Age Rating: ${rating} (no mature content)`);
34085
+ const looksRight = await confirm("Does that look right?", true);
34086
+ if (looksRight === null) return null;
34087
+ if (!looksRight) {
34088
+ return collectAgeRatingDetailed(defaultAnswers);
34089
+ }
34090
+ return { answers: defaultAnswers, rating };
34091
+ }
34092
+ return collectAgeRatingDetailed(defaultAnswers);
34093
+ }
34094
+ async function collectAgeRatingDetailed(answers) {
34095
+ const selectedTypes = await multiselect({
34096
+ message: "Which types of content does your app contain? (Space to select, Enter to confirm)",
34097
+ options: AGE_RATING_CONTENT_TYPES
34098
+ });
34099
+ if (selectedTypes === null) return null;
34100
+ const updatedAnswers = { ...answers };
34101
+ for (const typeKey of selectedTypes) {
34102
+ const typeInfo = AGE_RATING_CONTENT_TYPES.find((t2) => t2.value === typeKey);
34103
+ if (!typeInfo) continue;
34104
+ const severity = await select({
34105
+ message: `${typeInfo.label}: how much?`,
34106
+ options: [
34107
+ { value: "1", label: "A little", hint: "Minor/occasional" },
34108
+ { value: "2", label: "A lot", hint: "Frequent/prominent" }
34109
+ ]
34110
+ });
34111
+ if (severity === null) return null;
34112
+ updatedAnswers[typeKey] = parseInt(severity);
34113
+ }
34114
+ const webAccess = await confirm("Can users browse any website in your app? (e.g., in-app browser)", false);
34115
+ if (webAccess === null) return null;
34116
+ updatedAnswers.unrestrictedWebAccess = webAccess;
34117
+ const madeForKids = await confirm("Is this app designed specifically for kids under 13?", false);
34118
+ if (madeForKids === null) return null;
34119
+ updatedAnswers.madeForKids = madeForKids;
34120
+ const rating = calculateAgeRating(updatedAnswers);
34121
+ log.success(`Age Rating: ${rating}`);
34122
+ return { answers: updatedAnswers, rating };
34123
+ }
34124
+ async function collectPrivacyData() {
34125
+ log.step(subtext("Step 2 of 3: Privacy & Data"));
34126
+ const collectsData = await confirm("Does your app collect any user data?", false);
34127
+ if (collectsData === null) return null;
34128
+ const emptyData = Object.fromEntries(
34129
+ PRIVACY_DATA_TYPES.map((t2) => [t2.value, { collected: false, linked: false }])
34130
+ );
34131
+ if (!collectsData) {
34132
+ return { data: emptyData, tracking: false };
34133
+ }
34134
+ const collectedTypes = await multiselect({
34135
+ message: "What data does your app collect? (Space to select, Enter to confirm)",
34136
+ options: PRIVACY_DATA_TYPES
34137
+ });
34138
+ if (collectedTypes === null) return null;
34139
+ const data = { ...emptyData };
34140
+ for (const typeKey of collectedTypes) {
34141
+ data[typeKey] = { collected: true, linked: false };
34142
+ const linked = await confirm(
34143
+ `Can you tie ${PRIVACY_DATA_TYPES.find((t2) => t2.value === typeKey)?.label || typeKey} to a specific person? (e.g., through their account)`,
34144
+ false
34145
+ );
34146
+ if (linked === null) return null;
34147
+ data[typeKey].linked = linked;
34148
+ }
34149
+ const tracking = await confirm(
34150
+ "Does your app use data to track users across other companies' apps and websites?",
34151
+ false
34152
+ );
34153
+ if (tracking === null) return null;
34154
+ return { data, tracking };
34155
+ }
34156
+ async function collectFeatureChecklist() {
34157
+ log.step(subtext("Step 3 of 3: Features"));
34158
+ const selectedFeatures = await multiselect({
34159
+ message: "Which features does your app include? (Space to select, Enter to confirm)",
34160
+ options: FEATURE_ITEMS
34161
+ });
34162
+ if (selectedFeatures === null) return null;
34163
+ const checklist = {
34164
+ ugc: selectedFeatures.includes("ugc"),
34165
+ login: selectedFeatures.includes("login"),
34166
+ iap: selectedFeatures.includes("iap"),
34167
+ subscriptions: selectedFeatures.includes("subscriptions"),
34168
+ ads: selectedFeatures.includes("ads"),
34169
+ thirdPartyLogin: selectedFeatures.includes("thirdPartyLogin"),
34170
+ aiContent: selectedFeatures.includes("aiContent"),
34171
+ healthClaims: selectedFeatures.includes("healthClaims"),
34172
+ crypto: selectedFeatures.includes("crypto"),
34173
+ miniApps: selectedFeatures.includes("miniApps"),
34174
+ euDistribution: selectedFeatures.includes("euDistribution"),
34175
+ externalPayments: selectedFeatures.includes("externalPayments")
34176
+ };
34177
+ if (checklist.login) {
34178
+ const hasAccountDeletion = await confirm(
34179
+ "Does your app have an account deletion button? (Apple requires this!)",
34180
+ false
34181
+ );
34182
+ if (hasAccountDeletion === null) return null;
34183
+ checklist.accountDeletion = hasAccountDeletion;
34184
+ }
34185
+ if (checklist.iap || checklist.subscriptions) {
34186
+ const hasRestorePurchases = await confirm(
34187
+ 'Does your app have a "Restore Purchases" button? (Apple requires this!)',
34188
+ false
34189
+ );
34190
+ if (hasRestorePurchases === null) return null;
34191
+ checklist.restorePurchases = hasRestorePurchases;
34192
+ }
34193
+ if (checklist.ugc) {
34194
+ const hasCreatorAgeGate = await confirm(
34195
+ "Do you verify content creators are 13+ (or local minimum age)?",
34196
+ false
34197
+ );
34198
+ if (hasCreatorAgeGate === null) return null;
34199
+ checklist.creatorAgeGate = hasCreatorAgeGate;
34200
+ }
34201
+ if (checklist.miniApps) {
34202
+ const miniAppsReviewed = await confirm(
34203
+ "Have all mini apps/plugins been individually submitted for Apple review?",
34204
+ false
34205
+ );
34206
+ if (miniAppsReviewed === null) return null;
34207
+ checklist.miniAppsReviewed = miniAppsReviewed;
34208
+ }
34209
+ if (checklist.euDistribution) {
34210
+ const euTraderDeclared = await confirm(
34211
+ "Have you declared your trader status in App Store Connect? (EU DSA requirement)",
34212
+ false
34213
+ );
34214
+ if (euTraderDeclared === null) return null;
34215
+ checklist.euTraderDeclared = euTraderDeclared;
34216
+ }
34217
+ if (checklist.externalPayments) {
34218
+ const externalLinkCompliant = await confirm(
34219
+ "Do you use StoreKit External Link Account API with Apple's disclosure sheet?",
34220
+ false
34221
+ );
34222
+ if (externalLinkCompliant === null) return null;
34223
+ checklist.externalLinkCompliant = externalLinkCompliant;
34224
+ }
34225
+ return checklist;
34226
+ }
34227
+ async function collectCompliance(defaults) {
34228
+ const ageResult = await collectAgeRating();
34229
+ if (ageResult === null) return null;
34230
+ const privacyResult = await collectPrivacyData();
34231
+ if (privacyResult === null) return null;
34232
+ const checklistResult = await collectFeatureChecklist();
34233
+ if (checklistResult === null) return null;
34234
+ return {
34235
+ ageRatingAnswers: ageResult.answers,
34236
+ ageRating: ageResult.rating,
34237
+ privacyDeclarations: privacyResult,
34238
+ checklist: checklistResult
34239
+ };
34240
+ }
34241
+ function formatComplianceForApi(compliance) {
34242
+ return {
34243
+ age_rating: compliance.ageRatingAnswers,
34244
+ age_rating_result: compliance.ageRating,
34245
+ privacy_declarations: {
34246
+ data: compliance.privacyDeclarations.data,
34247
+ tracking: compliance.privacyDeclarations.tracking
34248
+ },
34249
+ checklist: compliance.checklist
34250
+ };
34251
+ }
34252
+ function formatComplianceSummary(compliance) {
34253
+ const lines = [];
34254
+ lines.push(` Age Rating: ${compliance.ageRating}`);
34255
+ const collectedTypes = Object.entries(compliance.privacyDeclarations.data).filter(([_2, v]) => v.collected).map(([k3, _2]) => {
34256
+ const typeInfo = PRIVACY_DATA_TYPES.find((t2) => t2.value === k3);
34257
+ return typeInfo?.label || k3;
34258
+ });
34259
+ if (collectedTypes.length > 0) {
34260
+ lines.push(` Privacy: ${collectedTypes.join(", ")}`);
34261
+ } else {
34262
+ lines.push(` Privacy: No data collected`);
34263
+ }
34264
+ const enabledFeatures = Object.entries(compliance.checklist).filter(([_2, v]) => v === true).map(([k3, _2]) => {
34265
+ const featureInfo = FEATURE_ITEMS.find((f) => f.value === k3);
34266
+ return featureInfo?.label || k3;
34267
+ });
34268
+ if (enabledFeatures.length > 0) {
34269
+ lines.push(` Features: ${enabledFeatures.join(", ")}`);
34270
+ } else {
34271
+ lines.push(` Features: None selected`);
34272
+ }
34273
+ return lines;
34274
+ }
34275
+
33593
34276
  // src/commands/submit.ts
33594
- async function submitCommand(path5, options = {}) {
34277
+ async function openUrl(url) {
34278
+ try {
34279
+ const open = (await import("./open-A77P4RC4.js")).default;
34280
+ await open(url);
34281
+ } catch {
34282
+ console.log(subtext(` Visit: ${url}`));
34283
+ }
34284
+ }
34285
+ async function fetchCredits() {
34286
+ try {
34287
+ const res = await apiRequest("/api/credits");
34288
+ if (!res.ok) return null;
34289
+ const data = await res.json();
34290
+ return data.credits ?? null;
34291
+ } catch {
34292
+ return null;
34293
+ }
34294
+ }
34295
+ async function creditPreCheck() {
34296
+ const credits = await fetchCredits();
34297
+ if (credits === null) {
34298
+ log.warning("Could not verify credit balance. Proceeding anyway.");
34299
+ return true;
34300
+ }
34301
+ if (credits >= 100) return true;
34302
+ log.warning(`You need 100 credits for a review. You currently have ${credits}.`);
34303
+ console.log();
34304
+ const wantsBuy = await confirm("Would you like to buy more credits?");
34305
+ if (wantsBuy === null || !wantsBuy) return false;
34306
+ await openUrl("https://preflightlaunch.com/pricing");
34307
+ log.info("Opened pricing page in browser.");
34308
+ console.log();
34309
+ log.info(subtext("Waiting for credits... Press Enter to check now, or Esc to cancel."));
34310
+ let attempts = 0;
34311
+ const maxAttempts = 60;
34312
+ while (attempts < maxAttempts) {
34313
+ await new Promise((r) => setTimeout(r, 1e4));
34314
+ attempts++;
34315
+ const newCredits = await fetchCredits();
34316
+ if (newCredits !== null && newCredits >= 100) {
34317
+ log.success(`Credits updated! You now have ${newCredits} credits.`);
34318
+ return true;
34319
+ }
34320
+ }
34321
+ log.warning("Still waiting for credits. You can try again later.");
34322
+ return false;
34323
+ }
34324
+ async function offerDraftSave(state) {
34325
+ if (!state.appName) return;
34326
+ const save = await confirm("Save your progress as a draft?", true);
34327
+ if (save === null || !save) return;
34328
+ const s = spinner();
34329
+ s.start("Saving draft...");
34330
+ try {
34331
+ const body = { app_name: state.appName };
34332
+ if (state.description) body.description = state.description;
34333
+ if (state.keywords) body.keywords = state.keywords;
34334
+ if (state.category) body.category = state.category;
34335
+ if (state.supportUrl) body.support_url = state.supportUrl;
34336
+ if (state.promotionalText) body.promotional_text = state.promotionalText;
34337
+ if (state.marketingUrl) body.marketing_url = state.marketingUrl;
34338
+ if (state.signInRequired != null) body.sign_in_required = state.signInRequired;
34339
+ if (state.demoUsername) body.demo_username = state.demoUsername;
34340
+ if (state.demoPassword) body.demo_password = state.demoPassword;
34341
+ if (state.compliance) Object.assign(body, formatComplianceForApi(state.compliance));
34342
+ const res = await apiRequest("/api/submissions", {
34343
+ method: "POST",
34344
+ body: JSON.stringify(body)
34345
+ });
34346
+ s.stop(res.ok ? "Draft saved" : "Could not save draft");
34347
+ if (res.ok) {
34348
+ log.success("Draft saved. Resume it from View Reviews anytime.");
34349
+ }
34350
+ } catch {
34351
+ s.stop("Could not save draft");
34352
+ }
34353
+ }
34354
+ async function offerAscAutofill(appDetails) {
34355
+ const ascConnected = getAscConnected();
34356
+ if (!ascConnected) return appDetails;
34357
+ try {
34358
+ const statusRes = await apiRequest("/api/asc/connect");
34359
+ if (!statusRes.ok) return appDetails;
34360
+ const statusData = await statusRes.json();
34361
+ if (!statusData.connected || !statusData.appId) return appDetails;
34362
+ const useAutofill = await confirm(
34363
+ `Autofill from App Store Connect? (${statusData.appName || "Connected app"})`,
34364
+ true
34365
+ );
34366
+ if (useAutofill === null || !useAutofill) return appDetails;
34367
+ const s = spinner();
34368
+ s.start("Fetching from App Store Connect...");
34369
+ const autofillRes = await apiRequest("/api/asc/autofill", {
34370
+ method: "POST",
34371
+ body: JSON.stringify({ appId: statusData.appId })
34372
+ });
34373
+ const autofillData = await autofillRes.json();
34374
+ s.stop(autofillRes.ok ? "Autofill complete" : "Autofill failed");
34375
+ if (autofillRes.ok && autofillData) {
34376
+ return {
34377
+ appName: appDetails.appName || autofillData.app_name || appDetails.appName,
34378
+ description: appDetails.description || autofillData.description,
34379
+ keywords: appDetails.keywords || autofillData.keywords,
34380
+ category: appDetails.category || autofillData.category,
34381
+ supportUrl: appDetails.supportUrl || autofillData.support_url,
34382
+ promotionalText: appDetails.promotionalText || autofillData.promotional_text,
34383
+ marketingUrl: appDetails.marketingUrl || autofillData.marketing_url,
34384
+ signInRequired: appDetails.signInRequired,
34385
+ demoUsername: appDetails.demoUsername,
34386
+ demoPassword: appDetails.demoPassword
34387
+ };
34388
+ }
34389
+ } catch {
34390
+ }
34391
+ return appDetails;
34392
+ }
34393
+ async function submitCommand(path5, options = {}, fromMenu = false) {
34394
+ const draftState = {};
33595
34395
  if (!isLoggedIn()) {
34396
+ if (fromMenu) {
34397
+ log.error("Not logged in.");
34398
+ return;
34399
+ }
33596
34400
  const wantsLogin = await promptLogin();
33597
34401
  if (wantsLogin) {
33598
34402
  const s = spinner();
33599
34403
  s.start("Opening browser...");
33600
- const result = await loginWithBrowser();
34404
+ const result = await loginWithBrowser("login");
33601
34405
  if (result) {
33602
34406
  s.stop(`Logged in as ${result.email}`);
33603
34407
  } else {
@@ -33610,18 +34414,24 @@ async function submitCommand(path5, options = {}) {
33610
34414
  return;
33611
34415
  }
33612
34416
  }
34417
+ if (fromMenu) {
34418
+ const hasCredits = await creditPreCheck();
34419
+ if (!hasCredits) return;
34420
+ }
33613
34421
  if (!path5) {
33614
34422
  const resolvedPath = await interactiveProjectSelect();
33615
34423
  if (!resolvedPath) return;
33616
34424
  path5 = resolvedPath;
33617
34425
  }
33618
- const dir = resolve2(path5);
34426
+ const dir = resolve3(path5);
33619
34427
  setLastScannedPath(dir);
33620
34428
  const detected = scanProject(dir);
33621
- const appName = options.appName || detected.projectName || "Unknown App";
33622
- if (options.plist) detected.infoPlist = resolve2(options.plist);
33623
- if (options.manifest) detected.privacyManifest = resolve2(options.manifest);
33624
- if (options.ipa) detected.ipa = resolve2(options.ipa);
34429
+ const projectName = detected.projectName || "Unknown App";
34430
+ let appName = options.appName || projectName;
34431
+ draftState.appName = appName;
34432
+ if (options.plist) detected.infoPlist = resolve3(options.plist);
34433
+ if (options.manifest) detected.privacyManifest = resolve3(options.manifest);
34434
+ if (options.ipa) detected.ipa = resolve3(options.ipa);
33625
34435
  const filesToUpload = [];
33626
34436
  if (detected.infoPlist) {
33627
34437
  filesToUpload.push({ type: "plist", filename: "Info.plist", path: detected.infoPlist });
@@ -33641,39 +34451,118 @@ async function submitCommand(path5, options = {}) {
33641
34451
  });
33642
34452
  }
33643
34453
  if (filesToUpload.length === 0) {
33644
- log.warning("No files to upload. Use --plist, --manifest, --ipa, or --screenshots flags.");
34454
+ log.warning("No files to upload. Make sure you're pointing to an Xcode project directory.");
34455
+ if (fromMenu) return;
34456
+ log.info(subtext("Use --plist, --manifest, --ipa, or --screenshots flags to specify files manually."));
33645
34457
  return;
33646
34458
  }
33647
- intro(`Submit ${appName} for analysis`);
33648
- const fileLines = filesToUpload.map((f) => {
33649
- const size = getFileSize(f.path);
33650
- const icon = f.type === "screenshot" ? icons.image : icons.file;
33651
- return ` ${icon} ${f.filename} ${subtext(`(${formatBytes(size)})`)}`;
33652
- });
33653
- log.message(source_default.bold("Files to upload:") + "\n" + fileLines.join("\n"));
33654
- const shouldContinue = await confirm("This will use 1 credit. Continue?");
33655
- if (shouldContinue === null) return;
33656
- if (!shouldContinue) {
33657
- outro("Submission cancelled.");
33658
- return;
34459
+ let appDetails = null;
34460
+ let compliance = null;
34461
+ if (fromMenu) {
34462
+ const reviewType = await select({
34463
+ message: "What would you like to include in your review?",
34464
+ options: [
34465
+ { value: "quick", label: "Quick review (just analyze my project files)", hint: "Fastest option" },
34466
+ { value: "full", label: "Full review (add app details + compliance info)", hint: "More thorough" }
34467
+ ]
34468
+ });
34469
+ if (reviewType === null) {
34470
+ if (fromMenu) await offerDraftSave(draftState);
34471
+ return;
34472
+ }
34473
+ if (reviewType === "full") {
34474
+ appDetails = await collectAppDetails(projectName);
34475
+ if (appDetails === null) {
34476
+ if (fromMenu) await offerDraftSave(draftState);
34477
+ return;
34478
+ }
34479
+ appName = appDetails.appName;
34480
+ draftState.appName = appName;
34481
+ draftState.description = appDetails.description;
34482
+ draftState.keywords = appDetails.keywords;
34483
+ draftState.category = appDetails.category;
34484
+ draftState.supportUrl = appDetails.supportUrl;
34485
+ draftState.promotionalText = appDetails.promotionalText;
34486
+ draftState.marketingUrl = appDetails.marketingUrl;
34487
+ draftState.signInRequired = appDetails.signInRequired;
34488
+ draftState.demoUsername = appDetails.demoUsername;
34489
+ draftState.demoPassword = appDetails.demoPassword;
34490
+ appDetails = await offerAscAutofill(appDetails);
34491
+ compliance = await collectCompliance();
34492
+ if (compliance === null) {
34493
+ if (fromMenu) await offerDraftSave(draftState);
34494
+ return;
34495
+ }
34496
+ draftState.compliance = compliance;
34497
+ }
34498
+ }
34499
+ if (fromMenu) {
34500
+ console.log();
34501
+ note(buildSummary(appName, dir, filesToUpload, compliance), "Review Summary");
34502
+ const action = await select({
34503
+ message: `Submit review? (100 credits)`,
34504
+ options: [
34505
+ { value: "submit", label: "Submit review", hint: "100 credits will be deducted" },
34506
+ { value: "cancel", label: "Cancel", hint: "Back to menu" }
34507
+ ]
34508
+ });
34509
+ if (action === null || action === "cancel") {
34510
+ if (fromMenu) await offerDraftSave(draftState);
34511
+ return;
34512
+ }
34513
+ } else {
34514
+ if (!fromMenu) {
34515
+ intro(`Submit ${appName} for analysis`);
34516
+ const fileLines = filesToUpload.map((f) => {
34517
+ const size = getFileSize(f.path);
34518
+ const icon = f.type === "screenshot" ? icons.image : icons.file;
34519
+ return ` ${icon} ${f.filename} ${subtext(`(${formatBytes(size)})`)}`;
34520
+ });
34521
+ log.message(source_default.bold("Files to upload:") + "\n" + fileLines.join("\n"));
34522
+ const shouldContinue = await confirm("This will use 100 credits. Continue?");
34523
+ if (shouldContinue === null || !shouldContinue) {
34524
+ outro("Submission cancelled.");
34525
+ return;
34526
+ }
34527
+ }
33659
34528
  }
34529
+ log.info(subtext("Reviews usually take 1-3 minutes."));
34530
+ console.log();
33660
34531
  const spinner2 = createSpinner("Creating submission...");
33661
34532
  spinner2.start();
34533
+ let activeSpinner = spinner2;
33662
34534
  try {
34535
+ const submissionBody = { app_name: appName };
34536
+ if (appDetails) {
34537
+ if (appDetails.description) submissionBody.description = appDetails.description;
34538
+ if (appDetails.keywords) submissionBody.keywords = appDetails.keywords;
34539
+ if (appDetails.category) submissionBody.category = appDetails.category;
34540
+ if (appDetails.supportUrl) submissionBody.support_url = appDetails.supportUrl;
34541
+ if (appDetails.promotionalText) submissionBody.promotional_text = appDetails.promotionalText;
34542
+ if (appDetails.marketingUrl) submissionBody.marketing_url = appDetails.marketingUrl;
34543
+ submissionBody.sign_in_required = appDetails.signInRequired;
34544
+ if (appDetails.demoUsername) submissionBody.demo_username = appDetails.demoUsername;
34545
+ if (appDetails.demoPassword) submissionBody.demo_password = appDetails.demoPassword;
34546
+ }
34547
+ if (compliance) {
34548
+ Object.assign(submissionBody, formatComplianceForApi(compliance));
34549
+ }
33663
34550
  const createRes = await apiRequest("/api/submissions", {
33664
34551
  method: "POST",
33665
- body: JSON.stringify({ app_name: appName })
34552
+ body: JSON.stringify(submissionBody)
33666
34553
  });
33667
34554
  const createData = await createRes.json();
33668
34555
  if (!createRes.ok) {
33669
34556
  spinner2.stop();
33670
34557
  log.error(createData.message || "Failed to create submission");
33671
- process.exit(1);
34558
+ if (!fromMenu) process.exitCode = 1;
34559
+ return;
33672
34560
  }
33673
34561
  const submissionId = createData.submissionId;
33674
34562
  spinner2.succeed("Submission created");
33675
34563
  const uploadSpinner = createSpinner("Getting upload URLs...");
33676
34564
  uploadSpinner.start();
34565
+ activeSpinner = uploadSpinner;
33677
34566
  const urlsRes = await apiRequest(`/api/submissions/${submissionId}/upload-urls`, {
33678
34567
  method: "POST",
33679
34568
  body: JSON.stringify({
@@ -33688,7 +34577,8 @@ async function submitCommand(path5, options = {}) {
33688
34577
  if (!urlsRes.ok) {
33689
34578
  uploadSpinner.stop();
33690
34579
  log.error(urlsData.message || "Failed to get upload URLs");
33691
- process.exit(1);
34580
+ if (!fromMenu) process.exitCode = 1;
34581
+ return;
33692
34582
  }
33693
34583
  for (let i = 0; i < urlsData.urls.length; i++) {
33694
34584
  const urlInfo = urlsData.urls[i];
@@ -33704,35 +34594,67 @@ async function submitCommand(path5, options = {}) {
33704
34594
  if (!uploadRes.ok) {
33705
34595
  uploadSpinner.stop();
33706
34596
  log.error(`Failed to upload ${fileInfo.filename}: HTTP ${uploadRes.status} ${uploadRes.statusText}`);
33707
- process.exit(1);
34597
+ if (!fromMenu) process.exitCode = 1;
34598
+ return;
33708
34599
  }
33709
34600
  }
33710
34601
  uploadSpinner.succeed("Files uploaded");
33711
34602
  const analyzeSpinner = createSpinner("Starting analysis...");
33712
34603
  analyzeSpinner.start();
33713
- const finalizeRes = await apiRequest(`/api/submissions/${submissionId}/finalize`, {
33714
- method: "POST",
33715
- body: JSON.stringify({
33716
- files: filesToUpload.map((f) => ({
33717
- type: f.type,
33718
- index: f.index
33719
- }))
33720
- })
33721
- });
33722
- const finalizeData = await finalizeRes.json();
33723
- if (!finalizeRes.ok) {
33724
- analyzeSpinner.stop();
33725
- if (finalizeRes.status === 402) {
33726
- log.error(`Insufficient credits. Need ${finalizeData.required}, have ${finalizeData.credits}.`);
33727
- console.log(subtext(" Purchase credits at https://preflightlaunch.com/pricing"));
34604
+ activeSpinner = analyzeSpinner;
34605
+ const finalizePayload = {
34606
+ files: filesToUpload.map((f) => ({
34607
+ type: f.type,
34608
+ index: f.index
34609
+ }))
34610
+ };
34611
+ let finalizeSuccess = false;
34612
+ let maxFinalizeRetries = 3;
34613
+ while (!finalizeSuccess && maxFinalizeRetries > 0) {
34614
+ const finalizeRes = await apiRequest(`/api/submissions/${submissionId}/finalize`, {
34615
+ method: "POST",
34616
+ body: JSON.stringify(finalizePayload)
34617
+ });
34618
+ const finalizeData = await finalizeRes.json();
34619
+ if (finalizeRes.ok) {
34620
+ finalizeSuccess = true;
34621
+ } else if (finalizeRes.status === 402) {
34622
+ analyzeSpinner.stop();
34623
+ log.warning(`Not enough credits. Need ${finalizeData.required ?? 100}, have ${finalizeData.credits ?? 0}.`);
34624
+ console.log();
34625
+ const wantsBuy = await confirm("Would you like to buy more credits?");
34626
+ if (wantsBuy === null || !wantsBuy) return;
34627
+ await openUrl("https://preflightlaunch.com/pricing");
34628
+ log.info("Opened pricing page. Press Enter when you've purchased credits.");
34629
+ const ready = await confirm("Ready to continue?");
34630
+ if (ready === null || !ready) return;
34631
+ await new Promise((r) => setTimeout(r, 3e3));
34632
+ analyzeSpinner.start();
34633
+ activeSpinner = analyzeSpinner;
34634
+ analyzeSpinner.text = "Retrying analysis...";
34635
+ maxFinalizeRetries--;
33728
34636
  } else {
34637
+ analyzeSpinner.stop();
33729
34638
  log.error(finalizeData.message || "Failed to finalize submission");
34639
+ if (!fromMenu) process.exitCode = 1;
34640
+ return;
33730
34641
  }
33731
- process.exit(1);
33732
34642
  }
33733
- analyzeSpinner.text = "AI review in progress...";
34643
+ if (!finalizeSuccess) {
34644
+ analyzeSpinner.stop();
34645
+ log.error("Could not finalize after multiple attempts. Your files are saved -- try again later.");
34646
+ return;
34647
+ }
34648
+ analyzeSpinner.text = "AI review in progress... (press Esc to stop waiting)";
33734
34649
  const reportData = await pollForReport(submissionId, analyzeSpinner);
33735
- analyzeSpinner.stop();
34650
+ if (reportData.status === "cancelled") {
34651
+ analyzeSpinner.stop();
34652
+ log.info("Analysis continues in the background.");
34653
+ console.log(subtext(` Check status with ${brand(`preflight status ${submissionId}`)} or from View Reviews.`));
34654
+ console.log();
34655
+ return;
34656
+ }
34657
+ analyzeSpinner.succeed("Analysis complete!");
33736
34658
  if (reportData.status === "complete" && reportData.data) {
33737
34659
  if (options.json) {
33738
34660
  console.log(JSON.stringify(reportData.data, null, 2));
@@ -33740,111 +34662,332 @@ async function submitCommand(path5, options = {}) {
33740
34662
  renderReport(reportData.data.report, reportData.data.items);
33741
34663
  console.log(subtext(` Full report: https://preflightlaunch.com/report/${reportData.data.report.id}`));
33742
34664
  console.log();
33743
- const next = await select({
33744
- message: "What next?",
33745
- options: [
33746
- { value: "open", label: "Open full report in browser" },
33747
- { value: "another", label: "Submit a different app" },
33748
- { value: "done", label: "Done" }
33749
- ]
33750
- });
33751
- if (next === "open") {
33752
- const open = (await import("./open-A77P4RC4.js")).default;
33753
- await open(`https://preflightlaunch.com/report/${reportData.data.report.id}`);
33754
- } else if (next === "another") {
33755
- await submitCommand();
34665
+ if (!fromMenu) {
34666
+ const next = await select({
34667
+ message: "What next?",
34668
+ options: [
34669
+ { value: "open", label: "Open full report in browser" },
34670
+ { value: "done", label: "Done" }
34671
+ ]
34672
+ });
34673
+ if (next === "open") {
34674
+ await openUrl(`https://preflightlaunch.com/report/${reportData.data.report.id}`);
34675
+ }
33756
34676
  }
33757
34677
  }
33758
34678
  } else if (reportData.status === "failed") {
33759
34679
  log.error("Analysis failed. Please try submitting again or contact support.");
33760
- process.exit(1);
34680
+ if (!fromMenu) process.exitCode = 1;
33761
34681
  } else {
33762
34682
  log.warning("Analysis is still running. Check status with:");
33763
34683
  console.log(subtext(` preflight status ${submissionId}`));
33764
34684
  }
33765
34685
  } catch (err) {
33766
- spinner2.stop();
34686
+ activeSpinner.stop();
33767
34687
  log.error(`Submit failed: ${err instanceof Error ? err.message : "Unknown error"}`);
33768
- process.exit(1);
34688
+ if (!fromMenu) process.exitCode = 1;
34689
+ }
34690
+ }
34691
+ async function resumeSubmitCommand(draft) {
34692
+ const submissionId = draft.id;
34693
+ log.step(`Resuming draft: ${draft.app_name || "Unknown"}`);
34694
+ console.log();
34695
+ const lastPath = draft.project_path;
34696
+ let path5;
34697
+ if (lastPath) {
34698
+ const useLast = await confirm(`Use previous project path? (${lastPath})`, true);
34699
+ if (useLast === null) return;
34700
+ if (useLast) path5 = lastPath;
34701
+ }
34702
+ if (!path5) {
34703
+ const resolvedPath = await interactiveProjectSelect();
34704
+ if (!resolvedPath) return;
34705
+ path5 = resolvedPath;
34706
+ }
34707
+ const dir = resolve3(path5);
34708
+ setLastScannedPath(dir);
34709
+ const detected = scanProject(dir);
34710
+ const projectName = detected.projectName || "Unknown App";
34711
+ const filesToUpload = [];
34712
+ if (detected.infoPlist) {
34713
+ filesToUpload.push({ type: "plist", filename: "Info.plist", path: detected.infoPlist });
34714
+ }
34715
+ if (detected.privacyManifest) {
34716
+ filesToUpload.push({ type: "manifest", filename: "PrivacyInfo.xcprivacy", path: detected.privacyManifest });
34717
+ }
34718
+ if (detected.ipa) {
34719
+ filesToUpload.push({ type: "ipa", filename: basename3(detected.ipa), path: detected.ipa });
34720
+ }
34721
+ for (let i = 0; i < Math.min(detected.screenshots.length, 10); i++) {
34722
+ filesToUpload.push({
34723
+ type: "screenshot",
34724
+ index: i,
34725
+ filename: basename3(detected.screenshots[i]),
34726
+ path: detected.screenshots[i]
34727
+ });
34728
+ }
34729
+ if (filesToUpload.length === 0) {
34730
+ log.warning("No files found. Make sure you're pointing to an Xcode project directory.");
34731
+ return;
34732
+ }
34733
+ const draftDefaults = {
34734
+ appName: draft.app_name || projectName,
34735
+ description: draft.description,
34736
+ keywords: draft.keywords,
34737
+ category: draft.category,
34738
+ supportUrl: draft.support_url,
34739
+ promotionalText: draft.promotional_text,
34740
+ marketingUrl: draft.marketing_url,
34741
+ signInRequired: draft.sign_in_required ?? false,
34742
+ demoUsername: draft.demo_username,
34743
+ demoPassword: draft.demo_password
34744
+ };
34745
+ const appDetails = await collectAppDetails(projectName, draftDefaults);
34746
+ if (appDetails === null) return;
34747
+ const appName = appDetails.appName;
34748
+ const compliance = await collectCompliance();
34749
+ if (compliance === null) return;
34750
+ console.log();
34751
+ note(buildSummary(appName, dir, filesToUpload, compliance), "Review Summary");
34752
+ const action = await select({
34753
+ message: `Submit review? (100 credits)`,
34754
+ options: [
34755
+ { value: "submit", label: "Submit review", hint: "100 credits will be deducted" },
34756
+ { value: "cancel", label: "Cancel", hint: "Back to menu" }
34757
+ ]
34758
+ });
34759
+ if (action === null || action === "cancel") return;
34760
+ log.info(subtext("Reviews usually take 1-3 minutes."));
34761
+ console.log();
34762
+ const spinner2 = createSpinner("Updating submission...");
34763
+ spinner2.start();
34764
+ let activeSpinner = spinner2;
34765
+ try {
34766
+ const submissionBody = {
34767
+ submission_id: submissionId,
34768
+ app_name: appName
34769
+ };
34770
+ if (appDetails.description) submissionBody.description = appDetails.description;
34771
+ if (appDetails.keywords) submissionBody.keywords = appDetails.keywords;
34772
+ if (appDetails.category) submissionBody.category = appDetails.category;
34773
+ if (appDetails.supportUrl) submissionBody.support_url = appDetails.supportUrl;
34774
+ if (appDetails.promotionalText) submissionBody.promotional_text = appDetails.promotionalText;
34775
+ if (appDetails.marketingUrl) submissionBody.marketing_url = appDetails.marketingUrl;
34776
+ submissionBody.sign_in_required = appDetails.signInRequired;
34777
+ if (appDetails.demoUsername) submissionBody.demo_username = appDetails.demoUsername;
34778
+ if (appDetails.demoPassword) submissionBody.demo_password = appDetails.demoPassword;
34779
+ if (compliance) {
34780
+ Object.assign(submissionBody, formatComplianceForApi(compliance));
34781
+ }
34782
+ const createRes = await apiRequest("/api/submissions", {
34783
+ method: "POST",
34784
+ body: JSON.stringify(submissionBody)
34785
+ });
34786
+ const createData = await createRes.json();
34787
+ if (!createRes.ok) {
34788
+ spinner2.stop();
34789
+ log.error(createData.message || "Failed to update submission");
34790
+ return;
34791
+ }
34792
+ const finalId = createData.submissionId || submissionId;
34793
+ spinner2.succeed("Submission updated");
34794
+ const uploadSpinner = createSpinner("Getting upload URLs...");
34795
+ uploadSpinner.start();
34796
+ activeSpinner = uploadSpinner;
34797
+ const urlsRes = await apiRequest(`/api/submissions/${finalId}/upload-urls`, {
34798
+ method: "POST",
34799
+ body: JSON.stringify({
34800
+ files: filesToUpload.map((f) => ({
34801
+ type: f.type,
34802
+ index: f.index,
34803
+ filename: f.filename
34804
+ }))
34805
+ })
34806
+ });
34807
+ const urlsData = await urlsRes.json();
34808
+ if (!urlsRes.ok) {
34809
+ uploadSpinner.stop();
34810
+ log.error(urlsData.message || "Failed to get upload URLs");
34811
+ return;
34812
+ }
34813
+ for (let i = 0; i < urlsData.urls.length; i++) {
34814
+ const urlInfo = urlsData.urls[i];
34815
+ const fileInfo = filesToUpload[i];
34816
+ const fileBuffer = readFileSync(fileInfo.path);
34817
+ const fileSize = fileBuffer.length;
34818
+ uploadSpinner.text = `Uploading ${fileInfo.filename} (${formatBytes(fileSize)})...`;
34819
+ const uploadRes = await fetch(urlInfo.signedUrl, {
34820
+ method: "PUT",
34821
+ body: fileBuffer,
34822
+ headers: { "Content-Type": "application/octet-stream" }
34823
+ });
34824
+ if (!uploadRes.ok) {
34825
+ uploadSpinner.stop();
34826
+ log.error(`Failed to upload ${fileInfo.filename}: HTTP ${uploadRes.status} ${uploadRes.statusText}`);
34827
+ return;
34828
+ }
34829
+ }
34830
+ uploadSpinner.succeed("Files uploaded");
34831
+ const analyzeSpinner = createSpinner("Starting analysis...");
34832
+ analyzeSpinner.start();
34833
+ activeSpinner = analyzeSpinner;
34834
+ const finalizeRes = await apiRequest(`/api/submissions/${finalId}/finalize`, {
34835
+ method: "POST",
34836
+ body: JSON.stringify({
34837
+ files: filesToUpload.map((f) => ({ type: f.type, index: f.index }))
34838
+ })
34839
+ });
34840
+ const finalizeData = await finalizeRes.json();
34841
+ if (!finalizeRes.ok) {
34842
+ analyzeSpinner.stop();
34843
+ log.error(finalizeData.message || "Failed to start analysis");
34844
+ return;
34845
+ }
34846
+ analyzeSpinner.text = "AI review in progress... (press Esc to stop waiting)";
34847
+ const reportData = await pollForReport(finalId, analyzeSpinner);
34848
+ if (reportData.status === "cancelled") {
34849
+ analyzeSpinner.stop();
34850
+ log.info("Analysis continues in the background.");
34851
+ console.log(subtext(` Check status with ${brand(`preflight status ${finalId}`)} or from View Reviews.`));
34852
+ console.log();
34853
+ return;
34854
+ }
34855
+ analyzeSpinner.succeed("Analysis complete!");
34856
+ if (reportData.status === "complete" && reportData.data) {
34857
+ renderReport(reportData.data.report, reportData.data.items);
34858
+ console.log(subtext(` Full report: https://preflightlaunch.com/report/${reportData.data.report.id}`));
34859
+ console.log();
34860
+ } else if (reportData.status === "failed") {
34861
+ log.error("Analysis failed. Please try submitting again or contact support.");
34862
+ } else {
34863
+ log.warning("Analysis is still running. Check status with:");
34864
+ console.log(subtext(` preflight status ${finalId}`));
34865
+ }
34866
+ } catch (err) {
34867
+ activeSpinner.stop();
34868
+ log.error(`Resume failed: ${err instanceof Error ? err.message : "Unknown error"}`);
33769
34869
  }
33770
34870
  }
34871
+ function buildSummary(appName, dir, files, compliance) {
34872
+ const home = __require("os").homedir();
34873
+ const shortDir = dir.startsWith(home) ? "~" + dir.slice(home.length) : dir;
34874
+ const fileTypes = files.map((f) => f.filename).join(", ");
34875
+ const screenshotCount = files.filter((f) => f.type === "screenshot").length;
34876
+ let summary = `App: ${appName}
34877
+ `;
34878
+ summary += `Project: ${shortDir}
34879
+ `;
34880
+ summary += `Files: ${fileTypes}${screenshotCount > 0 ? ` (${screenshotCount} screenshots)` : ""}
34881
+ `;
34882
+ if (compliance) {
34883
+ const complianceLines = formatComplianceSummary(compliance);
34884
+ summary += complianceLines.map((l) => l.trim()).join("\n");
34885
+ }
34886
+ return summary;
34887
+ }
33771
34888
  function getFileSize(filePath) {
33772
34889
  try {
33773
- return statSync2(filePath).size;
34890
+ return statSync3(filePath).size;
33774
34891
  } catch {
33775
34892
  return 0;
33776
34893
  }
33777
34894
  }
33778
34895
  async function pollForReport(submissionId, spinner2, maxAttempts = 60, interval = 5e3) {
33779
34896
  let consecutiveFailures = 0;
33780
- for (let i = 0; i < maxAttempts; i++) {
33781
- await new Promise((r) => setTimeout(r, interval));
33782
- const res = await apiRequest(`/api/submissions/${submissionId}`);
33783
- if (!res.ok) {
33784
- consecutiveFailures++;
33785
- if (res.status === 401) {
33786
- throw new Error("Session expired. Please run `preflight login` to re-authenticate.");
34897
+ const startTime = Date.now();
34898
+ let cancelled = false;
34899
+ const onKeypress = (data) => {
34900
+ if (data[0] === 27 || data[0] === 3) {
34901
+ cancelled = true;
34902
+ }
34903
+ };
34904
+ if (process.stdin.isTTY) {
34905
+ process.stdin.setRawMode(true);
34906
+ process.stdin.resume();
34907
+ process.stdin.on("data", onKeypress);
34908
+ }
34909
+ try {
34910
+ for (let i = 0; i < maxAttempts; i++) {
34911
+ for (let w3 = 0; w3 < interval / 500; w3++) {
34912
+ if (cancelled) return { status: "cancelled" };
34913
+ await new Promise((r) => setTimeout(r, 500));
33787
34914
  }
33788
- if (consecutiveFailures >= 3) {
33789
- throw new Error(`Polling failed after 3 consecutive errors (last status: HTTP ${res.status})`);
34915
+ if (cancelled) return { status: "cancelled" };
34916
+ const res = await apiRequest(`/api/submissions/${submissionId}`);
34917
+ if (!res.ok) {
34918
+ consecutiveFailures++;
34919
+ if (res.status === 401) {
34920
+ throw new Error("Session expired. Please run `preflight login` to re-authenticate.");
34921
+ }
34922
+ if (consecutiveFailures >= 3) {
34923
+ throw new Error(`Polling failed after 3 consecutive errors (last status: HTTP ${res.status})`);
34924
+ }
34925
+ continue;
33790
34926
  }
33791
- continue;
33792
- }
33793
- consecutiveFailures = 0;
33794
- const data = await res.json();
33795
- const submission = data.data;
33796
- const stages = ["Files uploaded", "Metadata validated", "AI review in progress...", "Generating report"];
33797
- const stageIdx = Math.min(Math.floor((i + 1) / maxAttempts * stages.length), stages.length - 1);
33798
- spinner2.text = `${stages[stageIdx]} (${Math.min((i + 1) * 3, 95)}%)`;
33799
- if (submission.status === "complete") {
33800
- if (submission.report_id) {
33801
- const reportRes = await apiRequest(`/api/reports/${submission.report_id}`);
33802
- const reportData = await reportRes.json();
33803
- return { status: "complete", data: reportData };
34927
+ consecutiveFailures = 0;
34928
+ const data = await res.json();
34929
+ const submission = data.data;
34930
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
34931
+ spinner2.text = `AI review in progress... (${elapsed}s elapsed, press Esc to stop waiting)`;
34932
+ if (submission.status === "complete") {
34933
+ if (submission.report_id) {
34934
+ const reportRes = await apiRequest(`/api/reports/${submission.report_id}`);
34935
+ const reportData = await reportRes.json();
34936
+ return { status: "complete", data: reportData };
34937
+ }
34938
+ return { status: "failed" };
34939
+ }
34940
+ if (submission.status === "failed") {
34941
+ return { status: "failed" };
33804
34942
  }
33805
- return { status: "failed" };
33806
34943
  }
33807
- if (submission.status === "failed") {
33808
- return { status: "failed" };
34944
+ return { status: "timeout" };
34945
+ } finally {
34946
+ if (process.stdin.isTTY) {
34947
+ process.stdin.removeListener("data", onKeypress);
34948
+ process.stdin.setRawMode(false);
34949
+ process.stdin.pause();
33809
34950
  }
33810
34951
  }
33811
- return { status: "timeout" };
33812
34952
  }
33813
34953
 
33814
34954
  export {
33815
34955
  source_default,
34956
+ brand,
34957
+ brandDim,
34958
+ subtext,
34959
+ ok,
34960
+ warning,
34961
+ critical,
34962
+ icons,
33816
34963
  createSpinner,
33817
34964
  success,
33818
34965
  error,
34966
+ DEFAULT_API_URL,
33819
34967
  getConfig,
34968
+ clearAuth,
33820
34969
  isLoggedIn,
33821
34970
  hasRunBefore,
33822
34971
  markAsRun,
33823
34972
  setLastScannedPath,
34973
+ setAscConnected,
33824
34974
  loginWithBrowser,
33825
34975
  logout,
33826
34976
  apiRequest,
33827
34977
  scanProject,
33828
- brand,
33829
- subtext,
33830
- ok,
33831
- warning,
33832
- critical,
33833
- icons,
33834
34978
  intro,
33835
- showTagline,
33836
- brandSplash,
33837
- outro,
33838
34979
  tip,
34980
+ renderHeader,
33839
34981
  select,
34982
+ confirm,
34983
+ text,
33840
34984
  spinner,
33841
34985
  log,
33842
- findXcodeProjects,
33843
- findProjectInDir,
33844
34986
  interactiveProjectSelect,
33845
34987
  renderReport,
33846
34988
  renderReportJson,
33847
34989
  handleUnknownCommand,
33848
- submitCommand
34990
+ submitCommand,
34991
+ resumeSubmitCommand
33849
34992
  };
33850
- //# sourceMappingURL=chunk-X5CBMYPG.js.map
34993
+ //# sourceMappingURL=chunk-26P7VL2P.js.map