preflightlaunch 0.2.1 → 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.
@@ -18839,7 +18839,8 @@ var config = new Conf({
18839
18839
  userId: { type: "string" },
18840
18840
  email: { type: "string" },
18841
18841
  hasRunBefore: { type: "boolean", default: false },
18842
- lastScannedPath: { type: "string" }
18842
+ lastScannedPath: { type: "string" },
18843
+ ascConnected: { type: "boolean", default: false }
18843
18844
  }
18844
18845
  });
18845
18846
  try {
@@ -18856,7 +18857,8 @@ function getConfig() {
18856
18857
  userId: config.get("userId"),
18857
18858
  email: config.get("email"),
18858
18859
  hasRunBefore: config.get("hasRunBefore") || false,
18859
- lastScannedPath: config.get("lastScannedPath")
18860
+ lastScannedPath: config.get("lastScannedPath"),
18861
+ ascConnected: config.get("ascConnected") || false
18860
18862
  };
18861
18863
  }
18862
18864
  function setTokens(accessToken, refreshToken) {
@@ -18888,6 +18890,12 @@ function setLastScannedPath(path5) {
18888
18890
  function getLastScannedPath() {
18889
18891
  return config.get("lastScannedPath");
18890
18892
  }
18893
+ function getAscConnected() {
18894
+ return config.get("ascConnected") || false;
18895
+ }
18896
+ function setAscConnected(connected) {
18897
+ config.set("ascConnected", connected);
18898
+ }
18891
18899
 
18892
18900
  // ../../node_modules/@supabase/supabase-js/dist/index.mjs
18893
18901
  init_esm_shims();
@@ -32293,11 +32301,66 @@ function ora(options) {
32293
32301
  return new Ora(options);
32294
32302
  }
32295
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
+
32296
32359
  // src/ui/spinner.ts
32297
32360
  function createSpinner(text2) {
32298
32361
  return ora({
32299
32362
  text: text2,
32300
- color: "cyan",
32363
+ color: "yellow",
32301
32364
  spinner: "dots"
32302
32365
  });
32303
32366
  }
@@ -33352,61 +33415,6 @@ ${l}
33352
33415
  }
33353
33416
  } }).prompt();
33354
33417
 
33355
- // src/ui/theme.ts
33356
- init_esm_shims();
33357
- var brand = source_default.bold.cyan;
33358
- var brandDim = source_default.cyan;
33359
- var heading = source_default.bold.white;
33360
- var subtext = source_default.dim;
33361
- var muted = source_default.gray;
33362
- var ok = source_default.green;
33363
- var okBold = source_default.bold.green;
33364
- var warning = source_default.yellow;
33365
- var warningBold = source_default.bold.yellow;
33366
- var critical = source_default.red;
33367
- var criticalBold = source_default.bold.red;
33368
- var info = source_default.blue;
33369
- var infoBold = source_default.bold.blue;
33370
- var icons = {
33371
- check: ok("\u2714"),
33372
- cross: critical("\u2716"),
33373
- warn: warning("!"),
33374
- info: info("i"),
33375
- bullet: "\u25CF",
33376
- circle: "\u25CB",
33377
- arrow: "\u2192",
33378
- block: "\u2588",
33379
- blockDim: source_default.dim("\u2591"),
33380
- file: "\u{1F4C4}",
33381
- image: "\u{1F5BC}",
33382
- plane: "\u{1F6EB}"
33383
- };
33384
- function scoreBar(score, width = 20) {
33385
- const filled = Math.round(score / 100 * width);
33386
- const empty = width - filled;
33387
- let color;
33388
- let label;
33389
- if (score >= 80) {
33390
- color = ok;
33391
- label = "READY";
33392
- } else if (score >= 60) {
33393
- color = warning;
33394
- label = "NEEDS ATTENTION";
33395
- } else {
33396
- color = critical;
33397
- label = "AT RISK";
33398
- }
33399
- return `${score}/100 ${color(icons.block.repeat(filled))}${icons.blockDim.repeat(empty)} ${color(label)}`;
33400
- }
33401
- function formatBytes(bytes) {
33402
- if (bytes < 1024) return `${bytes} B`;
33403
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
33404
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
33405
- }
33406
- var APP_VERSION = "0.2.0";
33407
- var APP_NAME = "Preflight";
33408
- var APP_TAGLINE = "App Store Review Scanner";
33409
-
33410
33418
  // src/ui/report.ts
33411
33419
  function renderReport(report, items) {
33412
33420
  console.log();
@@ -33428,7 +33436,7 @@ function renderReport(report, items) {
33428
33436
  for (const item of criticals) {
33429
33437
  console.log(` ${critical(item.title)}`);
33430
33438
  if (item.fix_suggestion) {
33431
- console.log(` ${source_default.cyan(icons.arrow)} ${item.fix_suggestion}`);
33439
+ console.log(` ${brandDim(icons.arrow)} ${item.fix_suggestion}`);
33432
33440
  }
33433
33441
  if (item.description && item.description !== item.title) {
33434
33442
  console.log(` ${subtext(item.description)}`);
@@ -33442,7 +33450,7 @@ function renderReport(report, items) {
33442
33450
  for (const item of warnings) {
33443
33451
  console.log(` ${warning(item.title)}`);
33444
33452
  if (item.fix_suggestion) {
33445
- console.log(` ${source_default.cyan(icons.arrow)} ${item.fix_suggestion}`);
33453
+ console.log(` ${brandDim(icons.arrow)} ${item.fix_suggestion}`);
33446
33454
  }
33447
33455
  console.log();
33448
33456
  }
@@ -33804,7 +33812,8 @@ var KNOWN_COMMANDS = [
33804
33812
  { name: "status", description: "Check analysis status" },
33805
33813
  { name: "report", description: "View analysis report" },
33806
33814
  { name: "history", description: "List past submissions" },
33807
- { name: "setup", description: "Run guided setup" }
33815
+ { name: "setup", description: "Run guided setup" },
33816
+ { name: "asc", description: "App Store Connect integration" }
33808
33817
  ];
33809
33818
  function levenshtein(a, b) {
33810
33819
  const matrix = [];
@@ -33845,7 +33854,7 @@ function handleUnknownCommand(cmdName) {
33845
33854
  console.log();
33846
33855
  console.log(source_default.dim(" Did you mean:"));
33847
33856
  for (const s of suggestions) {
33848
- 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)}`);
33849
33858
  }
33850
33859
  }
33851
33860
  console.log();
@@ -33918,7 +33927,10 @@ var FEATURE_ITEMS = [
33918
33927
  { value: "thirdPartyLogin", label: "Sign in with Apple / Google", hint: "Social login" },
33919
33928
  { value: "aiContent", label: "AI-Generated Content", hint: "ChatGPT, DALL-E, etc." },
33920
33929
  { value: "healthClaims", label: "Health / Medical Advice", hint: "Diagnosis, treatment" },
33921
- { value: "crypto", label: "Crypto / NFTs", hint: "Buy, sell, trade" }
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" }
33922
33934
  ];
33923
33935
  function calculateAgeRating(answers) {
33924
33936
  if (answers.prolongedViolence === 2 || answers.sexualContent === 2 || answers.gamblingSimulated === 2 || answers.gamblingContests > 0) {
@@ -33932,25 +33944,28 @@ function calculateAgeRating(answers) {
33932
33944
  }
33933
33945
  return "4+";
33934
33946
  }
33935
- async function collectAppDetails(projectName) {
33936
- const skipGate = await select({
33937
- message: "App Details (you can always add these later on the web)",
33938
- options: [
33939
- { value: "fill", label: "Fill in now", hint: "Name, description, keywords, category" },
33940
- { value: "skip", label: "Skip for now", hint: "Just use the project name" }
33941
- ]
33942
- });
33943
- if (skipGate === null) return null;
33944
- if (skipGate === "skip") {
33945
- return {
33946
- appName: projectName,
33947
- signInRequired: false
33948
- };
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
+ }
33949
33963
  }
33964
+ const defaultName = defaults?.appName || projectName;
33950
33965
  const appName = await text({
33951
33966
  message: "App Name",
33952
- placeholder: projectName,
33953
- defaultValue: projectName,
33967
+ placeholder: defaultName,
33968
+ defaultValue: defaultName,
33954
33969
  validate: (val) => {
33955
33970
  if (!val?.trim()) return "App name is required";
33956
33971
  }
@@ -33958,12 +33973,14 @@ async function collectAppDetails(projectName) {
33958
33973
  if (appName === null) return null;
33959
33974
  const description = await text({
33960
33975
  message: "Description (press Enter to skip)",
33961
- placeholder: "Describe your app as it appears in the App Store"
33976
+ placeholder: "Describe your app as it appears in the App Store",
33977
+ ...defaults?.description ? { defaultValue: defaults.description } : {}
33962
33978
  });
33963
33979
  if (description === null) return null;
33964
33980
  const keywords = await text({
33965
33981
  message: "Keywords (press Enter to skip)",
33966
33982
  placeholder: "Comma-separated, 100 chars max",
33983
+ ...defaults?.keywords ? { defaultValue: defaults.keywords } : {},
33967
33984
  validate: (val) => {
33968
33985
  if (val && val.length > 100) return "Keywords must be 100 characters or less";
33969
33986
  }
@@ -33972,6 +33989,7 @@ async function collectAppDetails(projectName) {
33972
33989
  const promotionalText = await text({
33973
33990
  message: "Promotional Text (press Enter to skip)",
33974
33991
  placeholder: "Short promotional text, 170 chars max",
33992
+ ...defaults?.promotionalText ? { defaultValue: defaults.promotionalText } : {},
33975
33993
  validate: (val) => {
33976
33994
  if (val && val.length > 170) return "Promotional text must be 170 characters or less";
33977
33995
  }
@@ -33983,20 +34001,26 @@ async function collectAppDetails(projectName) {
33983
34001
  ];
33984
34002
  const category = await select({
33985
34003
  message: "Primary Category",
33986
- options: categoryOptions
34004
+ options: categoryOptions,
34005
+ ...defaults?.category ? { initialValue: defaults.category } : {}
33987
34006
  });
33988
34007
  if (category === null) return null;
33989
34008
  const supportUrl = await text({
33990
34009
  message: "Support URL (press Enter to skip)",
33991
- placeholder: "https://example.com/support"
34010
+ placeholder: "https://example.com/support",
34011
+ ...defaults?.supportUrl ? { defaultValue: defaults.supportUrl } : {}
33992
34012
  });
33993
34013
  if (supportUrl === null) return null;
33994
34014
  const marketingUrl = await text({
33995
34015
  message: "Marketing URL (press Enter to skip)",
33996
- placeholder: "https://example.com"
34016
+ placeholder: "https://example.com",
34017
+ ...defaults?.marketingUrl ? { defaultValue: defaults.marketingUrl } : {}
33997
34018
  });
33998
34019
  if (marketingUrl === null) return null;
33999
- const signInRequired = await confirm("Does your app require sign-in for review?", false);
34020
+ const signInRequired = await confirm(
34021
+ "Does your app require sign-in for review?",
34022
+ defaults?.signInRequired ?? false
34023
+ );
34000
34024
  if (signInRequired === null) return null;
34001
34025
  let demoUsername;
34002
34026
  let demoPassword;
@@ -34004,6 +34028,7 @@ async function collectAppDetails(projectName) {
34004
34028
  const email = await text({
34005
34029
  message: "Demo Email",
34006
34030
  placeholder: "test@example.com",
34031
+ ...defaults?.demoUsername ? { defaultValue: defaults.demoUsername } : {},
34007
34032
  validate: (val) => {
34008
34033
  if (!val?.trim()) return "Demo email is required when sign-in is required";
34009
34034
  }
@@ -34144,7 +34169,10 @@ async function collectFeatureChecklist() {
34144
34169
  thirdPartyLogin: selectedFeatures.includes("thirdPartyLogin"),
34145
34170
  aiContent: selectedFeatures.includes("aiContent"),
34146
34171
  healthClaims: selectedFeatures.includes("healthClaims"),
34147
- crypto: selectedFeatures.includes("crypto")
34172
+ crypto: selectedFeatures.includes("crypto"),
34173
+ miniApps: selectedFeatures.includes("miniApps"),
34174
+ euDistribution: selectedFeatures.includes("euDistribution"),
34175
+ externalPayments: selectedFeatures.includes("externalPayments")
34148
34176
  };
34149
34177
  if (checklist.login) {
34150
34178
  const hasAccountDeletion = await confirm(
@@ -34162,9 +34190,41 @@ async function collectFeatureChecklist() {
34162
34190
  if (hasRestorePurchases === null) return null;
34163
34191
  checklist.restorePurchases = hasRestorePurchases;
34164
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
+ }
34165
34225
  return checklist;
34166
34226
  }
34167
- async function collectCompliance() {
34227
+ async function collectCompliance(defaults) {
34168
34228
  const ageResult = await collectAgeRating();
34169
34229
  if (ageResult === null) return null;
34170
34230
  const privacyResult = await collectPrivacyData();
@@ -34261,7 +34321,77 @@ async function creditPreCheck() {
34261
34321
  log.warning("Still waiting for credits. You can try again later.");
34262
34322
  return false;
34263
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
+ }
34264
34393
  async function submitCommand(path5, options = {}, fromMenu = false) {
34394
+ const draftState = {};
34265
34395
  if (!isLoggedIn()) {
34266
34396
  if (fromMenu) {
34267
34397
  log.error("Not logged in.");
@@ -34298,6 +34428,7 @@ async function submitCommand(path5, options = {}, fromMenu = false) {
34298
34428
  const detected = scanProject(dir);
34299
34429
  const projectName = detected.projectName || "Unknown App";
34300
34430
  let appName = options.appName || projectName;
34431
+ draftState.appName = appName;
34301
34432
  if (options.plist) detected.infoPlist = resolve3(options.plist);
34302
34433
  if (options.manifest) detected.privacyManifest = resolve3(options.manifest);
34303
34434
  if (options.ipa) detected.ipa = resolve3(options.ipa);
@@ -34335,13 +34466,34 @@ async function submitCommand(path5, options = {}, fromMenu = false) {
34335
34466
  { value: "full", label: "Full review (add app details + compliance info)", hint: "More thorough" }
34336
34467
  ]
34337
34468
  });
34338
- if (reviewType === null) return;
34469
+ if (reviewType === null) {
34470
+ if (fromMenu) await offerDraftSave(draftState);
34471
+ return;
34472
+ }
34339
34473
  if (reviewType === "full") {
34340
34474
  appDetails = await collectAppDetails(projectName);
34341
- if (appDetails === null) return;
34475
+ if (appDetails === null) {
34476
+ if (fromMenu) await offerDraftSave(draftState);
34477
+ return;
34478
+ }
34342
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);
34343
34491
  compliance = await collectCompliance();
34344
- if (compliance === null) return;
34492
+ if (compliance === null) {
34493
+ if (fromMenu) await offerDraftSave(draftState);
34494
+ return;
34495
+ }
34496
+ draftState.compliance = compliance;
34345
34497
  }
34346
34498
  }
34347
34499
  if (fromMenu) {
@@ -34354,7 +34506,10 @@ async function submitCommand(path5, options = {}, fromMenu = false) {
34354
34506
  { value: "cancel", label: "Cancel", hint: "Back to menu" }
34355
34507
  ]
34356
34508
  });
34357
- if (action === null || action === "cancel") return;
34509
+ if (action === null || action === "cancel") {
34510
+ if (fromMenu) await offerDraftSave(draftState);
34511
+ return;
34512
+ }
34358
34513
  } else {
34359
34514
  if (!fromMenu) {
34360
34515
  intro(`Submit ${appName} for analysis`);
@@ -34490,8 +34645,15 @@ async function submitCommand(path5, options = {}, fromMenu = false) {
34490
34645
  log.error("Could not finalize after multiple attempts. Your files are saved -- try again later.");
34491
34646
  return;
34492
34647
  }
34493
- analyzeSpinner.text = "AI review in progress...";
34648
+ analyzeSpinner.text = "AI review in progress... (press Esc to stop waiting)";
34494
34649
  const reportData = await pollForReport(submissionId, analyzeSpinner);
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
+ }
34495
34657
  analyzeSpinner.succeed("Analysis complete!");
34496
34658
  if (reportData.status === "complete" && reportData.data) {
34497
34659
  if (options.json) {
@@ -34526,6 +34688,186 @@ async function submitCommand(path5, options = {}, fromMenu = false) {
34526
34688
  if (!fromMenu) process.exitCode = 1;
34527
34689
  }
34528
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"}`);
34869
+ }
34870
+ }
34529
34871
  function buildSummary(appName, dir, files, compliance) {
34530
34872
  const home = __require("os").homedir();
34531
34873
  const shortDir = dir.startsWith(home) ? "~" + dir.slice(home.length) : dir;
@@ -34553,42 +34895,71 @@ function getFileSize(filePath) {
34553
34895
  async function pollForReport(submissionId, spinner2, maxAttempts = 60, interval = 5e3) {
34554
34896
  let consecutiveFailures = 0;
34555
34897
  const startTime = Date.now();
34556
- for (let i = 0; i < maxAttempts; i++) {
34557
- await new Promise((r) => setTimeout(r, interval));
34558
- const res = await apiRequest(`/api/submissions/${submissionId}`);
34559
- if (!res.ok) {
34560
- consecutiveFailures++;
34561
- if (res.status === 401) {
34562
- throw new Error("Session expired. Please run `preflight login` to re-authenticate.");
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));
34563
34914
  }
34564
- if (consecutiveFailures >= 3) {
34565
- 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;
34566
34926
  }
34567
- continue;
34568
- }
34569
- consecutiveFailures = 0;
34570
- const data = await res.json();
34571
- const submission = data.data;
34572
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
34573
- const status = submission.status || "analyzing";
34574
- spinner2.text = `AI review in progress... (${elapsed}s elapsed)`;
34575
- if (submission.status === "complete") {
34576
- if (submission.report_id) {
34577
- const reportRes = await apiRequest(`/api/reports/${submission.report_id}`);
34578
- const reportData = await reportRes.json();
34579
- 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" };
34580
34942
  }
34581
- return { status: "failed" };
34582
34943
  }
34583
- if (submission.status === "failed") {
34584
- 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();
34585
34950
  }
34586
34951
  }
34587
- return { status: "timeout" };
34588
34952
  }
34589
34953
 
34590
34954
  export {
34591
34955
  source_default,
34956
+ brand,
34957
+ brandDim,
34958
+ subtext,
34959
+ ok,
34960
+ warning,
34961
+ critical,
34962
+ icons,
34592
34963
  createSpinner,
34593
34964
  success,
34594
34965
  error,
@@ -34599,27 +34970,24 @@ export {
34599
34970
  hasRunBefore,
34600
34971
  markAsRun,
34601
34972
  setLastScannedPath,
34973
+ setAscConnected,
34602
34974
  loginWithBrowser,
34603
34975
  logout,
34604
34976
  apiRequest,
34605
34977
  scanProject,
34606
- brand,
34607
- subtext,
34608
- ok,
34609
- warning,
34610
- critical,
34611
- icons,
34612
34978
  intro,
34613
34979
  tip,
34614
34980
  renderHeader,
34615
34981
  select,
34616
34982
  confirm,
34983
+ text,
34617
34984
  spinner,
34618
34985
  log,
34619
34986
  interactiveProjectSelect,
34620
34987
  renderReport,
34621
34988
  renderReportJson,
34622
34989
  handleUnknownCommand,
34623
- submitCommand
34990
+ submitCommand,
34991
+ resumeSubmitCommand
34624
34992
  };
34625
- //# sourceMappingURL=chunk-PMKDGQCB.js.map
34993
+ //# sourceMappingURL=chunk-26P7VL2P.js.map
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  DEFAULT_API_URL,
6
6
  apiRequest,
7
7
  brand,
8
+ brandDim,
8
9
  clearAuth,
9
10
  confirm,
10
11
  createSpinner,
@@ -25,17 +26,20 @@ import {
25
26
  renderHeader,
26
27
  renderReport,
27
28
  renderReportJson,
29
+ resumeSubmitCommand,
28
30
  scanProject,
29
31
  select,
32
+ setAscConnected,
30
33
  setLastScannedPath,
31
34
  source_default,
32
35
  spinner,
33
36
  submitCommand,
34
37
  subtext,
35
38
  success,
39
+ text,
36
40
  tip,
37
41
  warning
38
- } from "./chunk-PMKDGQCB.js";
42
+ } from "./chunk-26P7VL2P.js";
39
43
  import {
40
44
  __commonJS,
41
45
  __require,
@@ -3094,9 +3098,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3094
3098
  helpWidth: context.helpWidth,
3095
3099
  outputHasColors: context.hasColors
3096
3100
  });
3097
- const text = helper.formatHelp(this, helper);
3098
- if (context.hasColors) return text;
3099
- return this._outputConfiguration.stripColor(text);
3101
+ const text2 = helper.formatHelp(this, helper);
3102
+ if (context.hasColors) return text2;
3103
+ return this._outputConfiguration.stripColor(text2);
3100
3104
  }
3101
3105
  /**
3102
3106
  * @typedef HelpContext
@@ -3250,7 +3254,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3250
3254
  * @param {(string | Function)} text - string to add, or a function returning a string
3251
3255
  * @return {Command} `this` command for chaining
3252
3256
  */
3253
- addHelpText(position, text) {
3257
+ addHelpText(position, text2) {
3254
3258
  const allowedValues = ["beforeAll", "before", "after", "afterAll"];
3255
3259
  if (!allowedValues.includes(position)) {
3256
3260
  throw new Error(`Unexpected value for position to addHelpText.
@@ -3259,10 +3263,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
3259
3263
  const helpEvent = `${position}Help`;
3260
3264
  this.on(helpEvent, (context) => {
3261
3265
  let helpStr;
3262
- if (typeof text === "function") {
3263
- helpStr = text({ error: context.error, command: context.command });
3266
+ if (typeof text2 === "function") {
3267
+ helpStr = text2({ error: context.error, command: context.command });
3264
3268
  } else {
3265
- helpStr = text;
3269
+ helpStr = text2;
3266
3270
  }
3267
3271
  if (helpStr) {
3268
3272
  context.write(`${helpStr}
@@ -3773,11 +3777,11 @@ var require_utils = __commonJS({
3773
3777
  }
3774
3778
  return output;
3775
3779
  }
3776
- function hyperlink(url, text) {
3780
+ function hyperlink(url, text2) {
3777
3781
  const OSC = "\x1B]";
3778
3782
  const BEL = "\x07";
3779
3783
  const SEP = ";";
3780
- return [OSC, "8", SEP, SEP, url || text, BEL, text, OSC, "8", SEP, SEP, BEL].join("");
3784
+ return [OSC, "8", SEP, SEP, url || text2, BEL, text2, OSC, "8", SEP, SEP, BEL].join("");
3781
3785
  }
3782
3786
  module.exports = {
3783
3787
  strlen,
@@ -3978,10 +3982,10 @@ var require_trap = __commonJS({
3978
3982
  "../../node_modules/@colors/colors/lib/custom/trap.js"(exports, module) {
3979
3983
  "use strict";
3980
3984
  init_esm_shims();
3981
- module["exports"] = function runTheTrap(text, options) {
3985
+ module["exports"] = function runTheTrap(text2, options) {
3982
3986
  var result = "";
3983
- text = text || "Run the trap, drop the bass";
3984
- text = text.split("");
3987
+ text2 = text2 || "Run the trap, drop the bass";
3988
+ text2 = text2.split("");
3985
3989
  var trap = {
3986
3990
  a: ["@", "\u0104", "\u023A", "\u0245", "\u0394", "\u039B", "\u0414"],
3987
3991
  b: ["\xDF", "\u0181", "\u0243", "\u026E", "\u03B2", "\u0E3F"],
@@ -4029,7 +4033,7 @@ var require_trap = __commonJS({
4029
4033
  y: ["\xA5", "\u04B0", "\u04CB"],
4030
4034
  z: ["\u01B5", "\u0240"]
4031
4035
  };
4032
- text.forEach(function(c) {
4036
+ text2.forEach(function(c) {
4033
4037
  c = c.toLowerCase();
4034
4038
  var chars = trap[c] || [" "];
4035
4039
  var rand = Math.floor(Math.random() * chars.length);
@@ -4049,8 +4053,8 @@ var require_zalgo = __commonJS({
4049
4053
  "../../node_modules/@colors/colors/lib/custom/zalgo.js"(exports, module) {
4050
4054
  "use strict";
4051
4055
  init_esm_shims();
4052
- module["exports"] = function zalgo(text, options) {
4053
- text = text || " he is here ";
4056
+ module["exports"] = function zalgo(text2, options) {
4057
+ text2 = text2 || " he is here ";
4054
4058
  var soul = {
4055
4059
  "up": [
4056
4060
  "\u030D",
@@ -4183,7 +4187,7 @@ var require_zalgo = __commonJS({
4183
4187
  });
4184
4188
  return bool;
4185
4189
  }
4186
- function heComes(text2, options2) {
4190
+ function heComes(text3, options2) {
4187
4191
  var result = "";
4188
4192
  var counts;
4189
4193
  var l;
@@ -4192,12 +4196,12 @@ var require_zalgo = __commonJS({
4192
4196
  options2["mid"] = typeof options2["mid"] !== "undefined" ? options2["mid"] : true;
4193
4197
  options2["down"] = typeof options2["down"] !== "undefined" ? options2["down"] : true;
4194
4198
  options2["size"] = typeof options2["size"] !== "undefined" ? options2["size"] : "maxi";
4195
- text2 = text2.split("");
4196
- for (l in text2) {
4199
+ text3 = text3.split("");
4200
+ for (l in text3) {
4197
4201
  if (isChar(l)) {
4198
4202
  continue;
4199
4203
  }
4200
- result = result + text2[l];
4204
+ result = result + text3[l];
4201
4205
  counts = { "up": 0, "down": 0, "mid": 0 };
4202
4206
  switch (options2.size) {
4203
4207
  case "mini":
@@ -4228,7 +4232,7 @@ var require_zalgo = __commonJS({
4228
4232
  }
4229
4233
  return result;
4230
4234
  }
4231
- return heComes(text, options);
4235
+ return heComes(text2, options);
4232
4236
  };
4233
4237
  }
4234
4238
  });
@@ -5266,7 +5270,7 @@ async function whoamiCommand() {
5266
5270
  }
5267
5271
  console.log();
5268
5272
  console.log(source_default.bold(" Account"));
5269
- console.log(` Email: ${source_default.cyan(data.user.email)}`);
5273
+ console.log(` Email: ${brandDim(data.user.email)}`);
5270
5274
  console.log(` ID: ${source_default.dim(data.user.id)}`);
5271
5275
  if (data.user.credits != null) {
5272
5276
  console.log(` Credits: ${source_default.green(data.user.credits)}`);
@@ -5385,7 +5389,7 @@ async function scanCommand(path) {
5385
5389
  ]
5386
5390
  });
5387
5391
  if (next === "submit") {
5388
- const { submitCommand: submitCommand2 } = await import("./submit-HBKAOG3Y.js");
5392
+ const { submitCommand: submitCommand2 } = await import("./submit-HEQTSQL5.js");
5389
5393
  await submitCommand2(dir, {});
5390
5394
  } else {
5391
5395
  tip(`Run ${brand("preflight submit")} anytime to get AI-powered fix instructions.`);
@@ -5598,11 +5602,12 @@ async function interactiveHistory() {
5598
5602
  month: "short",
5599
5603
  day: "numeric"
5600
5604
  });
5601
- const statusLabel = sub2.status === "complete" ? "Ready" : sub2.status === "failed" ? "Failed" : sub2.status === "analyzing" ? "Analyzing..." : sub2.status;
5605
+ const statusLabel = sub2.status === "complete" ? "Ready" : sub2.status === "failed" ? "Failed" : sub2.status === "analyzing" ? "Analyzing..." : sub2.status === "draft" ? "Draft" : sub2.status;
5606
+ const hint = sub2.status === "complete" ? "View report" : sub2.status === "draft" ? "Resume draft" : "";
5602
5607
  return {
5603
5608
  value: sub2.id,
5604
5609
  label: `${sub2.app_name || "Unknown"} - ${statusLabel} (${date})`,
5605
- hint: sub2.status === "complete" ? "View report" : ""
5610
+ hint
5606
5611
  };
5607
5612
  });
5608
5613
  options.push({
@@ -5617,6 +5622,32 @@ async function interactiveHistory() {
5617
5622
  if (selected === null || selected === "__back__") return;
5618
5623
  const sub = submissions.find((s2) => s2.id === selected);
5619
5624
  if (!sub) return;
5625
+ if (sub.status === "draft") {
5626
+ const draftAction = await select({
5627
+ message: `Draft: ${sub.app_name || "Unknown"}`,
5628
+ options: [
5629
+ { value: "resume", label: "Resume Draft", hint: "Continue where you left off" },
5630
+ { value: "back", label: "Back to list" }
5631
+ ]
5632
+ });
5633
+ if (draftAction === null || draftAction === "back") continue;
5634
+ const draftSpinner = spinner();
5635
+ draftSpinner.start("Loading draft...");
5636
+ try {
5637
+ const draftRes = await apiRequest(`/api/submissions/${sub.id}`);
5638
+ const draftData = await draftRes.json();
5639
+ draftSpinner.stop("Draft loaded");
5640
+ if (draftRes.ok && draftData.data) {
5641
+ await resumeSubmitCommand(draftData.data);
5642
+ return;
5643
+ } else {
5644
+ log.error("Could not load this draft.");
5645
+ }
5646
+ } catch {
5647
+ draftSpinner.stop("Failed to load draft");
5648
+ }
5649
+ continue;
5650
+ }
5620
5651
  if (sub.status !== "complete" || !sub.report_id) {
5621
5652
  if (sub.status === "analyzing") {
5622
5653
  log.info("This review is still being analyzed. Check back in a few minutes.");
@@ -5738,9 +5769,185 @@ async function setupCommand() {
5738
5769
  await runOnboarding();
5739
5770
  }
5740
5771
 
5772
+ // src/commands/asc.ts
5773
+ init_esm_shims();
5774
+ import { readFileSync, existsSync } from "fs";
5775
+ import { resolve as resolve2 } from "path";
5776
+ async function ascConnectCommand() {
5777
+ log.step("Connect to App Store Connect");
5778
+ console.log(subtext(" You'll need your API key from App Store Connect > Users and Access > Integrations."));
5779
+ console.log();
5780
+ const keyId = await text({
5781
+ message: "Key ID",
5782
+ placeholder: "e.g. ABC123DEF4",
5783
+ validate: (val) => {
5784
+ if (!val?.trim()) return "Key ID is required";
5785
+ }
5786
+ });
5787
+ if (keyId === null) return;
5788
+ const issuerId = await text({
5789
+ message: "Issuer ID",
5790
+ placeholder: "e.g. 12345678-1234-1234-1234-123456789012",
5791
+ validate: (val) => {
5792
+ if (!val?.trim()) return "Issuer ID is required";
5793
+ }
5794
+ });
5795
+ if (issuerId === null) return;
5796
+ const p8Path = await text({
5797
+ message: "Path to .p8 private key file",
5798
+ placeholder: "~/Downloads/AuthKey_ABC123DEF4.p8",
5799
+ validate: (val) => {
5800
+ if (!val?.trim()) return "File path is required";
5801
+ const resolved = resolve2(val.replace(/^~/, process.env.HOME || ""));
5802
+ if (!existsSync(resolved)) return `File not found: ${resolved}`;
5803
+ if (!resolved.endsWith(".p8")) return "File must be a .p8 key file";
5804
+ }
5805
+ });
5806
+ if (p8Path === null) return;
5807
+ const resolvedP8 = resolve2(p8Path.replace(/^~/, process.env.HOME || ""));
5808
+ let privateKey;
5809
+ try {
5810
+ privateKey = readFileSync(resolvedP8, "utf-8");
5811
+ } catch {
5812
+ log.error("Could not read .p8 file.");
5813
+ return;
5814
+ }
5815
+ const s = spinner();
5816
+ s.start("Connecting to App Store Connect...");
5817
+ try {
5818
+ const res = await apiRequest("/api/asc/connect", {
5819
+ method: "POST",
5820
+ body: JSON.stringify({
5821
+ keyId: keyId.trim(),
5822
+ issuerId: issuerId.trim(),
5823
+ privateKey
5824
+ })
5825
+ });
5826
+ const data = await res.json();
5827
+ if (!res.ok) {
5828
+ s.stop("Connection failed");
5829
+ log.error(data.message || "Failed to connect to App Store Connect");
5830
+ return;
5831
+ }
5832
+ s.stop("Connected to App Store Connect!");
5833
+ if (data.apps && data.apps.length > 0) {
5834
+ console.log();
5835
+ log.success(`Team: ${data.teamName || "Your team"}`);
5836
+ console.log();
5837
+ const appOptions = data.apps.map((app) => ({
5838
+ value: app.id,
5839
+ label: app.name,
5840
+ hint: app.bundleId || ""
5841
+ }));
5842
+ const selectedApp = await select({
5843
+ message: "Select your app",
5844
+ options: appOptions
5845
+ });
5846
+ if (selectedApp !== null) {
5847
+ const autofillSpinner = spinner();
5848
+ autofillSpinner.start("Saving app selection...");
5849
+ const autofillRes = await apiRequest("/api/asc/autofill", {
5850
+ method: "POST",
5851
+ body: JSON.stringify({ appId: selectedApp })
5852
+ });
5853
+ if (autofillRes.ok) {
5854
+ const autofillData = await autofillRes.json();
5855
+ autofillSpinner.stop("App selected");
5856
+ log.success(`Connected: ${autofillData.app_name || "App"} is ready for autofill.`);
5857
+ } else {
5858
+ autofillSpinner.stop("App selection saved");
5859
+ }
5860
+ }
5861
+ } else {
5862
+ log.success("Connected! Your App Store Connect data is now available for autofill.");
5863
+ }
5864
+ setAscConnected(true);
5865
+ console.log();
5866
+ } catch (err) {
5867
+ s.stop("Connection failed");
5868
+ log.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
5869
+ }
5870
+ }
5871
+ async function ascStatusCommand() {
5872
+ const s = createSpinner("Checking App Store Connect status...");
5873
+ s.start();
5874
+ try {
5875
+ const res = await apiRequest("/api/asc/connect");
5876
+ const data = await res.json();
5877
+ s.stop();
5878
+ if (!res.ok) {
5879
+ log.error(data.message || "Could not check status");
5880
+ return;
5881
+ }
5882
+ console.log();
5883
+ console.log(brand(" App Store Connect"));
5884
+ console.log();
5885
+ if (data.connected) {
5886
+ console.log(` Status: ${brandDim("Connected")}`);
5887
+ if (data.appName) console.log(` App: ${data.appName}`);
5888
+ if (data.keyId) console.log(` Key ID: ${data.keyId.slice(0, 3)}${"*".repeat(Math.max(0, data.keyId.length - 3))}`);
5889
+ } else {
5890
+ console.log(` Status: ${subtext("Not connected")}`);
5891
+ console.log();
5892
+ console.log(subtext(` Run ${brand("preflight asc connect")} to set up.`));
5893
+ }
5894
+ console.log();
5895
+ } catch (err) {
5896
+ s.stop();
5897
+ log.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
5898
+ }
5899
+ }
5900
+ async function ascDisconnectCommand() {
5901
+ const confirmed = await confirm("Disconnect from App Store Connect?", false);
5902
+ if (confirmed === null || !confirmed) return;
5903
+ const s = createSpinner("Disconnecting...");
5904
+ s.start();
5905
+ try {
5906
+ const res = await apiRequest("/api/asc/connect", {
5907
+ method: "DELETE"
5908
+ });
5909
+ s.stop();
5910
+ if (res.ok) {
5911
+ setAscConnected(false);
5912
+ log.success("Disconnected from App Store Connect.");
5913
+ } else {
5914
+ const data = await res.json();
5915
+ log.error(data.message || "Could not disconnect");
5916
+ }
5917
+ } catch (err) {
5918
+ s.stop();
5919
+ log.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
5920
+ }
5921
+ }
5922
+ async function ascInteractiveMenu() {
5923
+ while (true) {
5924
+ const choice = await select({
5925
+ message: "App Store Connect",
5926
+ options: [
5927
+ { value: "connect", label: "Connect", hint: "Set up API key" },
5928
+ { value: "status", label: "View Status", hint: "Check connection" },
5929
+ { value: "disconnect", label: "Disconnect", hint: "Remove API key" },
5930
+ { value: "back", label: "Back to menu" }
5931
+ ]
5932
+ });
5933
+ if (choice === null || choice === "back") return;
5934
+ switch (choice) {
5935
+ case "connect":
5936
+ await ascConnectCommand();
5937
+ break;
5938
+ case "status":
5939
+ await ascStatusCommand();
5940
+ break;
5941
+ case "disconnect":
5942
+ await ascDisconnectCommand();
5943
+ break;
5944
+ }
5945
+ }
5946
+ }
5947
+
5741
5948
  // src/index.ts
5742
5949
  var program2 = new Command();
5743
- program2.name("preflight").description("Preflight - App Store Review Scanner").version("0.2.1");
5950
+ program2.name("preflight").description("Preflight - App Store Review Scanner").version("0.2.2");
5744
5951
  program2.command("login").description("Log in to Preflight (opens browser)").action(loginCommand);
5745
5952
  program2.command("logout").description("Log out and clear stored credentials").action(logoutCommand);
5746
5953
  program2.command("whoami").description("Show current user and credit balance").action(whoamiCommand);
@@ -5751,6 +5958,10 @@ program2.command("report [id]").description("View full analysis report").option(
5751
5958
  program2.command("history").description("List past submissions").option("--json", "Output as JSON").action(historyCommand);
5752
5959
  program2.command("credits").description("Show credit balance").action(creditsCommand);
5753
5960
  program2.command("setup").description("Run guided setup (can be re-run anytime)").action(setupCommand);
5961
+ var ascCmd = program2.command("asc").description("App Store Connect integration");
5962
+ ascCmd.command("connect").description("Connect your App Store Connect account").action(ascConnectCommand);
5963
+ ascCmd.command("status").description("Check App Store Connect connection status").action(ascStatusCommand);
5964
+ ascCmd.command("disconnect").description("Disconnect from App Store Connect").action(ascDisconnectCommand);
5754
5965
  program2.on("command:*", (operands) => {
5755
5966
  handleUnknownCommand(operands[0]);
5756
5967
  process.exitCode = 1;
@@ -5793,6 +6004,7 @@ async function interactiveMenu() {
5793
6004
  { value: "review", label: "New Review", hint: "Scan your app for App Store issues" },
5794
6005
  { value: "history", label: "View Reviews", hint: "See your past review reports" },
5795
6006
  { value: "buy", label: "Buy Credits", hint: "Get more credits at preflightlaunch.com" },
6007
+ { value: "asc", label: "App Store Connect", hint: "Connect your ASC account for autofill" },
5796
6008
  { value: "logout", label: "Log Out" }
5797
6009
  ]
5798
6010
  });
@@ -5816,6 +6028,9 @@ async function interactiveMenu() {
5816
6028
  await new Promise((r) => setTimeout(r, 2e3));
5817
6029
  cachedCredits = await fetchCredits();
5818
6030
  break;
6031
+ case "asc":
6032
+ await ascInteractiveMenu();
6033
+ break;
5819
6034
  case "logout":
5820
6035
  clearAuth();
5821
6036
  const authenticated = await showAuthScreen();
@@ -2,10 +2,12 @@
2
2
  import { createRequire } from "node:module";
3
3
  const require = createRequire(import.meta.url);
4
4
  import {
5
+ resumeSubmitCommand,
5
6
  submitCommand
6
- } from "./chunk-PMKDGQCB.js";
7
+ } from "./chunk-26P7VL2P.js";
7
8
  import "./chunk-45JYNMSU.js";
8
9
  export {
10
+ resumeSubmitCommand,
9
11
  submitCommand
10
12
  };
11
- //# sourceMappingURL=submit-HBKAOG3Y.js.map
13
+ //# sourceMappingURL=submit-HEQTSQL5.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preflightlaunch",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Preflight CLI - App Store Review Scanner from your terminal",
6
6
  "bin": {