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 +1 -1
- package/data/rules/google-play.yml +1 -1
- package/dist/{chunk-3AIOX537.js → chunk-EKW5Y74B.js} +1 -1
- package/dist/{chunk-RKHEDQIO.js → chunk-MABS4QNP.js} +1 -1
- package/dist/{chunk-5TUBDRZQ.js → chunk-ZHZ3ML4B.js} +3 -3
- package/dist/{flutter-Q36WBCAC.js → flutter-C75PQYKY.js} +2 -2
- package/dist/index.js +38 -18
- package/dist/{native-AVSHQSRP.js → native-WVYZZZLW.js} +2 -2
- package/package.json +1 -1
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-
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|
package/dist/index.js
CHANGED
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
readExpoConfig,
|
|
10
10
|
readPackageJson,
|
|
11
11
|
readPubspec
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZHZ3ML4B.js";
|
|
13
13
|
import {
|
|
14
14
|
analyzeNativeAndroidProject,
|
|
15
15
|
analyzeNativeIosProject
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-EKW5Y74B.js";
|
|
17
17
|
import {
|
|
18
18
|
APPSHIP_VERSION
|
|
19
|
-
} from "./chunk-
|
|
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", "
|
|
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.
|
|
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-
|
|
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,
|
|
1288
|
+
async function generateWithRetry(provider, basePrompt, jsonSchema, parse8, validate, system = SYSTEM_PROMPT) {
|
|
1289
1289
|
let prompt = basePrompt;
|
|
1290
|
-
let listing =
|
|
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 =
|
|
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
|
|
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(...
|
|
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
|
|
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(
|
|
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-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-EKW5Y74B.js";
|
|
8
|
+
import "./chunk-MABS4QNP.js";
|
|
9
9
|
export {
|
|
10
10
|
analyzeNativeAndroidProject,
|
|
11
11
|
analyzeNativeIosProject,
|
package/package.json
CHANGED