appship-cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ appship rules update # fetch the latest policy rules & SDK signatures
51
51
 
52
52
  Store policies change between releases, so the doctor rules and SDK signature database are plain data files that can be refreshed without a new CLI version: `appship rules update` downloads them from this repository into `~/.appship/data-cache` (schema-validated before anything is written; nothing partial is ever observable), `rules status` shows what is in use, and `rules reset` returns to the bundled files. If a cached file ever stops parsing (e.g. you run a much newer rule format with an old CLI), appship silently falls back to its bundled data.
53
53
 
54
- `generate`, `localize`, and `review analyze` call an AI provider — Anthropic by default (`ANTHROPIC_API_KEY` or `ant auth login`), and any OpenAI-compatible endpoint via `ai.provider` in appship.yml: `gemini` (free keys at [aistudio.google.com](https://aistudio.google.com/apikey), `GEMINI_API_KEY`, default model `gemini-2.5-flash`), `openai` (`OPENAI_API_KEY`), `ollama` (local, set `ai.model`), or `openai-compatible` (set `ai.base_url` + `ai.model` — works with OpenRouter, Groq, and self-hosted servers). Store character limits are validated with retry regardless of provider. `init` and `doctor` run fully offline. Translations go through the same validation as generation; App Review notes are copied, not translated.
54
+ `generate`, `localize`, and `review analyze` call an AI provider — Anthropic by default (`ANTHROPIC_API_KEY` or `ant auth login`), and any OpenAI-compatible endpoint via `ai.provider` in appship.yml: `gemini` (free keys at [aistudio.google.com](https://aistudio.google.com/apikey), `GEMINI_API_KEY`, default model `gemini-3.6-flash`), `openai` (`OPENAI_API_KEY`), `ollama` (local, set `ai.model`), or `openai-compatible` (set `ai.base_url` + `ai.model` — works with OpenRouter, Groq, and self-hosted servers). Store character limits are validated with retry regardless of provider. `init` and `doctor` run fully offline. Translations go through the same validation as generation; App Review notes are copied, not translated.
55
55
 
56
56
  Supported projects: React Native CLI, Expo (managed), Flutter, native iOS (`.xcodeproj` at the root), and native Android (`settings.gradle` + `app/` module) — identity and permissions are read from native files (`Info.plist`, `AndroidManifest.xml`, `project.pbxproj`, `build.gradle`), from the Expo config (`app.json` or a dynamic `app.config.ts`/`.js`/`.cjs`/`.mjs`, evaluated by the project's Expo config loader), or from `pubspec.yaml`. SDK detection matches npm packages, Dart/Flutter packages, CocoaPods, and Gradle Maven coordinates, plus source patterns in JS/TS, Dart, Swift, Kotlin, and Java.
57
57
 
@@ -37,7 +37,7 @@
37
37
  category: consistency
38
38
  severity: error
39
39
  target: google-play/data-safety.yml
40
- check: { type: not_contains, value: "confirm_before_submitting" }
40
+ check: { type: yaml_no_confirm_markers }
41
41
  message: "data-safety.yml still contains confirm_before_submitting markers — resolve them before filling the Play Console form"
42
42
  pass_message: "Data Safety confirm markers resolved"
43
43
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  APPSHIP_VERSION
4
- } from "./chunk-RKHEDQIO.js";
4
+ } from "./chunk-MABS4QNP.js";
5
5
 
6
6
  // src/core/project/native.ts
7
7
  import { readFile } from "fs/promises";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var APPSHIP_VERSION = "0.1.3";
4
+ var APPSHIP_VERSION = "0.1.4";
5
5
 
6
6
  export {
7
7
  APPSHIP_VERSION
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  APPSHIP_VERSION
4
- } from "./chunk-RKHEDQIO.js";
4
+ } from "./chunk-MABS4QNP.js";
5
5
 
6
6
  // src/core/project/flutter.ts
7
7
  import { readFile as readFile4 } from "fs/promises";
@@ -32,11 +32,11 @@ async function detectProjectType(projectRoot) {
32
32
  if (pkg && "react-native" in deps) {
33
33
  return "react-native";
34
34
  }
35
- const { readPubspec: readPubspec2, isFlutterPubspec: isFlutterPubspec2 } = await import("./flutter-Q36WBCAC.js");
35
+ const { readPubspec: readPubspec2, isFlutterPubspec: isFlutterPubspec2 } = await import("./flutter-C75PQYKY.js");
36
36
  if (isFlutterPubspec2(await readPubspec2(projectRoot))) {
37
37
  return "flutter";
38
38
  }
39
- const { findRootPbxproj, hasNativeAndroidLayout } = await import("./native-AVSHQSRP.js");
39
+ const { findRootPbxproj, hasNativeAndroidLayout } = await import("./native-WVYZZZLW.js");
40
40
  const { existsSync: existsSync2 } = await import("fs");
41
41
  if (hasNativeAndroidLayout(projectRoot, existsSync2)) {
42
42
  return "native-android";
@@ -3,8 +3,8 @@ import {
3
3
  analyzeFlutterProject,
4
4
  isFlutterPubspec,
5
5
  readPubspec
6
- } from "./chunk-5TUBDRZQ.js";
7
- import "./chunk-RKHEDQIO.js";
6
+ } from "./chunk-ZHZ3ML4B.js";
7
+ import "./chunk-MABS4QNP.js";
8
8
  export {
9
9
  analyzeFlutterProject,
10
10
  isFlutterPubspec,
package/dist/index.js CHANGED
@@ -9,14 +9,14 @@ import {
9
9
  readExpoConfig,
10
10
  readPackageJson,
11
11
  readPubspec
12
- } from "./chunk-5TUBDRZQ.js";
12
+ } from "./chunk-ZHZ3ML4B.js";
13
13
  import {
14
14
  analyzeNativeAndroidProject,
15
15
  analyzeNativeIosProject
16
- } from "./chunk-3AIOX537.js";
16
+ } from "./chunk-EKW5Y74B.js";
17
17
  import {
18
18
  APPSHIP_VERSION
19
- } from "./chunk-RKHEDQIO.js";
19
+ } from "./chunk-MABS4QNP.js";
20
20
  import {
21
21
  UploadError,
22
22
  assertUploadLane,
@@ -95,7 +95,7 @@ import { parse as parse2 } from "yaml";
95
95
  import { z as z2 } from "zod";
96
96
  var storeSchema = z2.enum(["app-store", "google-play"]);
97
97
  var checkSchema = z2.object({
98
- type: z2.enum(["max_length", "file_exists", "not_contains", "no_emoji"]),
98
+ type: z2.enum(["max_length", "file_exists", "not_contains", "no_emoji", "yaml_no_confirm_markers"]),
99
99
  value: z2.union([z2.string(), z2.number()]).optional()
100
100
  });
101
101
  var ruleSchema = z2.object({
@@ -636,12 +636,12 @@ function defaultAnswers(scan) {
636
636
  };
637
637
  }
638
638
  function buildConfig(scan, answers, existing) {
639
- const bundleId = scan.project.ios?.bundleId ?? null;
640
- const packageName = scan.project.android?.packageName ?? null;
639
+ const bundleId = scan.project.ios?.bundleId ?? existing?.platforms.ios?.bundle_id ?? null;
640
+ const packageName = scan.project.android?.packageName ?? existing?.platforms.android?.package_name ?? null;
641
641
  const locales = answers.locales.length > 0 ? answers.locales : ["en-US"];
642
642
  return appshipConfigSchema.parse({
643
643
  project: {
644
- name: scan.project.appName ?? "My App",
644
+ name: scan.project.appName ?? existing?.project.name ?? "My App",
645
645
  description: answers.description || DESCRIPTION_PLACEHOLDER,
646
646
  audience: answers.audience,
647
647
  ...answers.requiresLogin === null ? {} : { requires_login: answers.requiresLogin },
@@ -798,7 +798,7 @@ function answersFromExisting(scan, existing) {
798
798
  locales: existing.stores.locales
799
799
  };
800
800
  }
801
- var initCommand = new Command("init").description("Analyze the project and create appship.yml and the .appship/ folder").option("--force", "ignore existing appship.yml and re-initialize from scratch").option("--non-interactive", "skip interactive questions (unconfirmed findings are left for doctor)").action(async (options) => {
801
+ var initCommand = new Command("init").description("Analyze the project and create appship.yml and the .appship/ folder").option("--force", "rewrite generated analysis and refresh detected project fields, preserving user settings").option("--reset-config", "discard existing appship.yml values and initialize configuration defaults").option("--non-interactive", "skip interactive questions (unconfirmed findings are left for doctor)").action(async (options) => {
802
802
  const projectRoot = process.cwd();
803
803
  let scan;
804
804
  try {
@@ -814,7 +814,7 @@ var initCommand = new Command("init").description("Analyze the project and creat
814
814
  printDetectionSummary(scan);
815
815
  console.log();
816
816
  let existing = null;
817
- if (!options.force) {
817
+ if (!options.resetConfig) {
818
818
  try {
819
819
  existing = await loadConfig(projectRoot);
820
820
  console.log(pc.dim(`Existing ${CONFIG_FILENAME} found \u2014 using it as defaults.`));
@@ -1055,7 +1055,7 @@ function createOpenAICompatibleProvider(config) {
1055
1055
  return new OpenAICompatibleProvider({
1056
1056
  name: "gemini",
1057
1057
  baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
1058
- model: config.model ?? "gemini-2.5-flash",
1058
+ model: config.model ?? "gemini-3.6-flash",
1059
1059
  apiKeyEnv: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
1060
1060
  keyHint: "Set GEMINI_API_KEY \u2014 free keys are available at https://aistudio.google.com/apikey."
1061
1061
  });
@@ -1285,13 +1285,13 @@ function buildPrompt(payload, locale, store) {
1285
1285
  Project summary (the only source of truth about the app):
1286
1286
  ` + JSON.stringify(payload, null, 2);
1287
1287
  }
1288
- async function generateWithRetry(provider, basePrompt, jsonSchema, parse7, validate, system = SYSTEM_PROMPT) {
1288
+ async function generateWithRetry(provider, basePrompt, jsonSchema, parse8, validate, system = SYSTEM_PROMPT) {
1289
1289
  let prompt = basePrompt;
1290
- let listing = parse7(await provider.generateObject({ system, prompt, jsonSchema }));
1290
+ let listing = parse8(await provider.generateObject({ system, prompt, jsonSchema }));
1291
1291
  let violations = validate(listing);
1292
1292
  for (let attempt = 0; violations.length > 0 && attempt < MAX_RETRIES; attempt++) {
1293
1293
  prompt = basePrompt + "\n\nYour previous attempt violated these constraints \u2014 fix them and regenerate:\n" + violations.map((v) => `- ${v.message}`).join("\n") + "\n\nFor character-limit violations: character limits are hard store rules, not suggestions. Count the characters of your replacement before answering and make it comfortably shorter than the limit (aim for ~10% under) \u2014 cutting words is better than exceeding the limit.";
1294
- listing = parse7(await provider.generateObject({ system, prompt, jsonSchema }));
1294
+ listing = parse8(await provider.generateObject({ system, prompt, jsonSchema }));
1295
1295
  violations = validate(listing);
1296
1296
  }
1297
1297
  return { listing, violations };
@@ -1496,6 +1496,9 @@ var SCREENSHOT_JSON_SCHEMA = {
1496
1496
  }
1497
1497
  }
1498
1498
  };
1499
+ function legalDocsFor(config) {
1500
+ return config.project.requires_login === false ? LEGAL_DOCS.filter((spec) => spec.filename !== "account-deletion.md") : LEGAL_DOCS;
1501
+ }
1499
1502
  async function detectScreens(projectRoot) {
1500
1503
  const files = await glob3(["src/screens/*.{tsx,jsx,ts,js}", "app/screens/*.{tsx,jsx,ts,js}"], {
1501
1504
  cwd: projectRoot,
@@ -1575,7 +1578,7 @@ async function runGenerate(projectRoot, config, scan, provider, options = {}) {
1575
1578
  add("google-play/data-safety.yml", renderDataSafetyYaml(scan.privacyReport));
1576
1579
  add("google-play/content-rating.yml", renderContentRatingYaml(config));
1577
1580
  }
1578
- for (const spec of LEGAL_DOCS) {
1581
+ for (const spec of legalDocsFor(config)) {
1579
1582
  const doc = await generateLegalDoc(provider, spec, payload, scan.privacyReport);
1580
1583
  add(`legal/${spec.filename}`, doc.endsWith("\n") ? doc : doc + "\n");
1581
1584
  }
@@ -1615,7 +1618,7 @@ function planArtifacts(config, options = {}) {
1615
1618
  if (wantAndroid) {
1616
1619
  paths.push(".appship/google-play/data-safety.yml", ".appship/google-play/content-rating.yml");
1617
1620
  }
1618
- paths.push(...LEGAL_DOCS.map((d) => `.appship/legal/${d.filename}`));
1621
+ paths.push(...legalDocsFor(config).map((d) => `.appship/legal/${d.filename}`));
1619
1622
  if (wantIos) paths.push(".appship/app-store/screenshots/screenshot-plan.yml");
1620
1623
  if (wantAndroid) paths.push(".appship/google-play/screenshots/screenshot-plan.yml");
1621
1624
  if (wantIos) paths.push(".appship/checklist/app-store.md");
@@ -1779,7 +1782,14 @@ import { existsSync as existsSync4 } from "fs";
1779
1782
  import { readFile as readFile8 } from "fs/promises";
1780
1783
  import { join as join11 } from "path";
1781
1784
  import glob4 from "fast-glob";
1785
+ import { parse as parse6 } from "yaml";
1782
1786
  var EMOJI_PATTERN2 = new RegExp("\\p{Extended_Pictographic}", "u");
1787
+ function hasConfirmMarker(value) {
1788
+ if (Array.isArray(value)) return value.some(hasConfirmMarker);
1789
+ if (!value || typeof value !== "object") return false;
1790
+ const record = value;
1791
+ return Object.hasOwn(record, "confirm_before_submitting") || Object.values(record).some(hasConfirmMarker);
1792
+ }
1783
1793
  function humanize(id) {
1784
1794
  return id.replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase());
1785
1795
  }
@@ -1825,7 +1835,9 @@ async function evaluateRule(rule, ctx, findings) {
1825
1835
  const target = join11(appshipDir, String(rule.check.value));
1826
1836
  return existsSync4(target) ? pass() : fail();
1827
1837
  }
1828
- const targets = rule.target ? await glob4(rule.target, { cwd: appshipDir }) : [];
1838
+ const targets = (rule.target ? await glob4(rule.target, { cwd: appshipDir }) : []).filter(
1839
+ (file) => !(file === "legal/account-deletion.md" && !ctx.config.project.requires_login && !findings.has("login_detected"))
1840
+ );
1829
1841
  if (targets.length === 0) {
1830
1842
  return skip("no generated files to check (run appship generate)");
1831
1843
  }
@@ -1850,6 +1862,14 @@ async function evaluateRule(rule, ctx, findings) {
1850
1862
  return fail(file);
1851
1863
  }
1852
1864
  break;
1865
+ case "yaml_no_confirm_markers": {
1866
+ try {
1867
+ if (hasConfirmMarker(parse6(content))) return fail(file);
1868
+ } catch {
1869
+ return fail(`${file}: invalid YAML`);
1870
+ }
1871
+ break;
1872
+ }
1853
1873
  }
1854
1874
  }
1855
1875
  return pass();
@@ -2925,7 +2945,7 @@ import pc7 from "picocolors";
2925
2945
  import { mkdir as mkdir8, readdir as readdir3, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
2926
2946
  import { existsSync as existsSync10 } from "fs";
2927
2947
  import { join as join18 } from "path";
2928
- import { parse as parse6 } from "yaml";
2948
+ import { parse as parse7 } from "yaml";
2929
2949
  import { z as z8 } from "zod";
2930
2950
  var ScreenshotsError = class extends Error {
2931
2951
  };
@@ -2950,7 +2970,7 @@ async function loadScreenshotPlan(projectRoot) {
2950
2970
  for (const location of PLAN_LOCATIONS) {
2951
2971
  const path = join18(projectRoot, location);
2952
2972
  if (!existsSync10(path)) continue;
2953
- const parsed = planSchema.safeParse(parse6(await readFile11(path, "utf8")));
2973
+ const parsed = planSchema.safeParse(parse7(await readFile11(path, "utf8")));
2954
2974
  if (!parsed.success) {
2955
2975
  throw new ScreenshotsError(`Invalid screenshot plan (${location}): ${parsed.error.message}`);
2956
2976
  }
@@ -4,8 +4,8 @@ import {
4
4
  analyzeNativeIosProject,
5
5
  findRootPbxproj,
6
6
  hasNativeAndroidLayout
7
- } from "./chunk-3AIOX537.js";
8
- import "./chunk-RKHEDQIO.js";
7
+ } from "./chunk-EKW5Y74B.js";
8
+ import "./chunk-MABS4QNP.js";
9
9
  export {
10
10
  analyzeNativeAndroidProject,
11
11
  analyzeNativeIosProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appship-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Open-source AI release assistant that analyzes your mobile app and prepares everything required for App Store and Google Play submission",
5
5
  "repository": {
6
6
  "type": "git",