From 2dc20b61de5a6e6679324cb0ca6422a248fc2f93 Mon Sep 17 00:00:00 2001 From: CRT Date: Tue, 27 Oct 2026 17:10:59 +0700 Subject: [PATCH] regenerate embedded skills --- cli/lib/embedded.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/lib/embedded.ts b/cli/lib/embedded.ts index c963dee..6429370 100644 --- a/cli/lib/embedded.ts +++ b/cli/lib/embedded.ts @@ -26,7 +26,7 @@ export const TEMPLATE_TEXT: Record = { "profiles/subscription-app/listing.md": "# Example App · Google Play listing copy\n\nSource for `storebound play listing` (title, short + full description) and `storebound play release` (release notes).\nRules the parser follows:\n- every `## ...` section with a `` line is a listing; one section can serve several Play codes\n- under it, `### ...` headings that contain **title** (optional, defaults to `app.name`), **short**, **full** and\n **what's new**, each followed by one fenced block\n- limits (checked before anything is sent): title 30, short 80, full 4000, what's new 500 characters\nOnly claim what the app really does. No prices, no \"best / #1\", no other apps' names.\n\n---\n\n## English (en-US)\n\n\n### Play short description (max 80)\n```\nWhat the app does for people, in one plain line\n```\n\n### Play full description (max 4000)\n```\nFirst paragraph: the job the app does and why it is easier with it, in two or three sentences.\n\nFEATURES\n• Lists and notes that sync across your devices\n• Share a list with family or friends\n• Export your data or delete your account in Settings, any time\n\nFree to use with up to 3 lists. Example Pro unlocks unlimited lists, sharing and themes: yearly with a 1-week free trial, or monthly. Payment is charged to your store account; the subscription renews automatically unless cancelled at least 24 hours before the end of the period, and you can manage or cancel it in your account settings.\n\nTerms of Use: https://example.com/terms\nPrivacy Policy: https://example.com/privacy\n```\n\n### What's new (max 500)\n```\nFirst release. Thanks for trying it!\n```\n\n## Türkçe (tr-TR)\n\n\n### Play kısa açıklama · short (max 80)\n```\nUygulamanın insanlar için ne yaptığını anlatan sade bir cümle\n```\n\n### Play tam açıklama · full (max 4000)\n```\nİlk paragraf: uygulamanın gördüğü iş ve onunla neden daha kolay olduğu, iki üç cümlede.\n\nÖZELLİKLER\n• Tüm cihazlarında eşitlenen listeler ve notlar\n• Bir listeyi ailen ya da arkadaşlarınla paylaş\n• Verilerini dışa aktar ya da hesabını Ayarlar'dan istediğin an sil\n\n3 listeye kadar ücretsiz. Example Pro sınırsız liste, paylaşım ve tema açar: 1 hafta ücretsiz denemeli yıllık ya da aylık. Ödeme mağaza hesabınızdan alınır; abonelik, dönem bitmeden en az 24 saat önce iptal edilmezse otomatik yenilenir; hesap ayarlarınızdan yönetebilir ya da iptal edebilirsiniz.\n\nKullanım Koşulları: https://example.com/terms\nGizlilik Politikası: https://example.com/privacy\n```\n\n### Yenilikler · what's new (max 500)\n```\nİlk sürüm. Denediğin için teşekkürler!\n```\n", "profiles/subscription-app/storebound.config.ts": "// storebound config for one app (profile \"subscription-app\": accounts plus auto-renewable subscriptions, an\n// analytics SDK, no ads). Every answer below is a starting point: edit it to match your app. Credentials NEVER go\n// here (env vars or .storebound/credentials.json, gitignored). Paths are relative to this file.\n// The type import is optional: it only works when storebound is installed in the project (bun add -d storebound).\nimport type { ShipConfig } from 'storebound';\n\nconst config: ShipConfig = {\n // Every compliance answer below is the TEMPLATE's, not yours. While an area is listed here, check / validate block\n // and nothing sends it to a store. Derive the answers from the code (storebound scan --write; for an app that is\n // already live: storebound onboard --declarations), answer the rest, then remove the area from this list.\n unanswered: ['dataSafety', 'appPrivacy', 'reviewNotes', 'ageRating', 'accounts', 'reviewAccess'],\n\n app: {\n name: 'Example App',\n profile: 'subscription-app',\n copyright: '2026 Example Studio',\n privacyPolicyUrl: 'https://example.com/privacy',\n termsUrl: 'https://example.com/terms', // Terms of Use (EULA): in the app and the metadata (Apple 3.1.2)\n supportUrl: 'https://example.com/support',\n marketingUrl: 'https://example.com',\n website: 'https://example.com',\n contactEmail: 'support@example.com',\n },\n\n // Accounts (account-deletion skill). Remove this block if the app has no sign-up / sign-in at all.\n // Apple 5.1.1(v): deletion must start inside the app. Google Play: in the app AND on a public web page.\n accounts: {\n enabled: true,\n deletionUrl: 'https://example.com/delete-account', // public page, no login needed to request deletion\n inApp: 'Settings > Account > Delete account', // goes into the App Review notes\n // How people sign up: 'username' (a name picked in the app, no password, no email), 'username-password',\n // 'email-password', 'email-link', 'phone', 'oauth', 'other'. `storebound scan` proposes the account data and\n // dataSafety.accountCreation from it; only the 'email-*' methods mean an email address.\n signUp: ['email-password'],\n },\n\n // Sign-in for store reviewers (review-access skill). The demo account itself lives ONLY in\n // .storebound/credentials.json (\"reviewDemoAccount\") or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD.\n reviewAccess: {\n signInRequired: true, // false if every feature works without signing in\n instructions: 'Sign in with the demo account. Sample data is already there; nothing needs a second device.',\n },\n\n // id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale\n languages: [\n { id: 'en', play: 'en-US', apple: 'en-US' },\n { id: 'tr', play: 'tr-TR', apple: 'tr' },\n ],\n\n play: {\n packageName: 'com.example.app',\n defaultLanguage: 'en-US',\n listing: 'store/listing.md',\n icon: 'store/icons/play-512.png',\n },\n\n apple: {\n bundleId: 'com.example.app',\n // name: 'Example: Short Tagline', // App Store name (max 30) when app.name is taken there (\"already being used\"); default app.name\n listing: 'store/appstore.md',\n // Export compliance (export-compliance skill): decide it, then uncomment. false ONLY if the app uses no encryption\n // beyond the OS (HTTPS through the system, standard sign-in).\n // usesNonExemptEncryption: false,\n // ipad: true,\n info: {\n categories: { primary: 'PRODUCTIVITY', secondary: 'UTILITIES' },\n ageRating: {}, // every content question NONE / false; change what applies (user-generated content, web access ...)\n contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',\n free: true,\n reviewNotes: [\n 'Example App keeps lists and notes in sync across devices. Pro (yearly with a 1-week free trial, or monthly) unlocks unlimited lists, sharing and themes.',\n 'Subscriptions: Settings > Pro (or the paywall after the third list). Restore Purchases is on the paywall and in Settings > Pro.',\n 'Privacy policy and Terms of Use: on the paywall and in Settings > About.',\n // With a remote paywall or feature flags (storebound scan warns, 2.3.1) you may add a line like:\n // 'Paywall designs and the prices shown are tested remotely with ; what Pro unlocks does not change after review.',\n // Google Play has no review-notes field: put the same line in reviewAccess.instructions (Play App content >\n // App access, 'Any other information') when the app has a sign-in, and say in the full description what needs\n // a paid subscription (Play: Deceptive behavior, Payments).\n ].join('\\n\\n'),\n },\n },\n\n // Auto-renewable subscriptions (store-subscriptions skill): `storebound apple subscriptions`, `storebound play\n // subscriptions`. Apple: one group, levels rank the plans (1 = highest). Play: one subscription per entry with one\n // auto-renewing base plan. Prices: USD base, Apple equalizes from the USA, Play converts per region; overrides set a\n // region by hand. The disclosure (price per period, renewal, how to cancel, Terms + Privacy links) belongs on the\n // purchase screen and in the descriptions.\n subscriptions: [\n {\n name: 'Example Pro',\n listings: { en: { name: 'Example Pro' }, tr: { name: 'Example Pro' } },\n subscriptions: [\n {\n id: 'com.example.app.pro_yearly', period: 'P1Y', level: 1, usd: 29.99,\n listings: {\n en: { title: 'Pro yearly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },\n tr: { title: 'Pro yıllık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },\n },\n intro: { type: 'free', duration: 'P1W' }, // 1 week free trial for new subscribers\n reviewScreenshot: 'store/iap-review/paywall.png',\n },\n {\n id: 'com.example.app.pro_monthly', period: 'P1M', level: 2, usd: 3.99,\n overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }],\n listings: {\n en: { title: 'Pro monthly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },\n tr: { title: 'Pro aylık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },\n },\n reviewScreenshot: 'store/iap-review/paywall.png',\n },\n ],\n },\n ],\n\n // Google Play Data safety. Types not listed = not collected. Typical app with accounts + an analytics SDK acting as\n // your processor (collected, not shared). Edit to match what your app and its SDKs really do.\n dataSafety: {\n collectsData: true,\n encryptedInTransit: true,\n // Sign-up methods. Google does not publish these ids: compare with Play Console > App content > Data safety >\n // Export to CSV before the first import. The delete-account URL (PSL_ACCOUNT_DELETION_URL) comes from\n // accounts.deletionUrl; `storebound check` names any other row Play requires.\n accountCreation: ['PSL_ACM_USER_ID_PASSWORD'],\n // Can people delete their data without deleting the account (a data-only page)? true needs partialDeletionUrl.\n // Play's \"can users request deletion?\" is answered yes by accounts.deletionUrl. Ask the owner.\n partialDeletion: false,\n data: {\n PSL_EMAIL: { shared: false, collect: ['functionality', 'account'] },\n PSL_USER_ACCOUNT: { shared: false, collect: ['functionality', 'account'] },\n PSL_USER_GENERATED_CONTENT: { shared: false, collect: ['functionality'] },\n PSL_USER_INTERACTION: { shared: false, collect: ['analytics'] },\n PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] },\n PSL_PERFORMANCE_DIAGNOSTICS: { shared: false, collect: ['analytics'] },\n PSL_DEVICE_ID: { shared: false, collect: ['analytics'] },\n PSL_PURCHASE_HISTORY: { shared: false, collect: ['functionality'] }, // subscription status tied to the account\n },\n },\n\n renders: 'store/renders',\n\n shots: {\n style: 'app', // calm gradient, device frame, headline + subline (game: sparkles and badges)\n copy: 'store/shots/copy.json',\n frames: 'store/shots/frames', // /phone/.png, /tablet/.png\n out: 'store/renders',\n // fonts: { title: 'Inter', body: 'Inter' }, // the app style default\n scenes: [\n { id: 'home', palette: 'mist', frames: [{ src: 'home.png' }] },\n { id: 'detail', palette: 'mint', frames: [{ src: 'detail.png' }] },\n { id: 'share', palette: 'sky', frames: [{ src: 'share.png' }] },\n { id: 'privacy', palette: 'slate', frames: [{ src: 'settings.png' }] },\n ],\n feature: { frame: 'home.png', palette: 'mist' },\n // Raw frames from a web build, the iOS Simulator (ios-sim) or an Android emulator (android-emu): sign in to a demo\n // account with realistic sample data, never real user data (store-screenshots skill).\n capture: {\n baseUrl: 'http://127.0.0.1:5173',\n query: 'lang={lang}&demo=1',\n shots: {\n 'home': { path: '/', delayMs: 1000, device: 'both' },\n 'detail': { path: '/lists/1', delayMs: 1000, device: 'both' },\n 'share': { path: '/lists/1/share', delayMs: 1000, device: 'both' },\n 'settings': { path: '/settings', delayMs: 1000, device: 'both' },\n },\n },\n },\n\n icons: {\n composite: 'store/icon/icon.png',\n foreground: 'store/icon/foreground.png',\n background: 'store/icon/background.png',\n out: 'store/icons',\n },\n};\n\nexport default config;\n", "shots-copy.json": "{\n \"en\": {\n \"hero\": { \"title\": [\"Tap. Match.\", \"Repeat!\"], \"badges\": [[\"100+ levels\", \"and more every update\"], [\"Play offline\", \"No account needed\"]] },\n \"play\": { \"eyebrow\": \"Play\", \"title\": [\"Easy to learn,\", \"hard to put down\"], \"sub\": \"One thumb, short rounds, big satisfying moments.\", \"badges\": [[\"Quick rounds\", \"Two minutes each\"], [\"Great!\"]] },\n \"levels\": { \"eyebrow\": \"Worlds\", \"title\": [\"Explore new\", \"worlds\"], \"sub\": \"A fresh look and new rules every ten levels.\", \"badges\": [[\"10 worlds\", \"A new one every 10 levels\"]] },\n \"stars\": { \"eyebrow\": \"Stars\", \"title\": [\"Play smart,\", \"grab 3 stars\"], \"sub\": \"Finish with moves to spare and collect every star.\", \"badges\": [[\"3 stars\"]] }\n },\n \"tr\": {\n \"hero\": { \"title\": [\"Dokun. Eşleştir.\", \"Tekrarla!\"], \"badges\": [[\"100+ bölüm\", \"her güncellemede yenileri\"], [\"İnternetsiz\", \"hesap gerekmez\"]] },\n \"play\": { \"eyebrow\": \"Oyna\", \"title\": [\"Öğrenmesi kolay,\", \"bırakması zor\"], \"sub\": \"Tek parmak, kısa turlar, büyük keyifli anlar.\", \"badges\": [[\"Kısa turlar\", \"Her biri iki dakika\"], [\"Harika!\"]] },\n \"levels\": { \"eyebrow\": \"Dünyalar\", \"title\": [\"Yeni dünyaları\", \"keşfet\"], \"sub\": \"Her on bölümde yeni bir görünüm ve yeni kurallar.\", \"badges\": [[\"10 dünya\", \"Her 10 bölümde yeni biri\"]] },\n \"stars\": { \"eyebrow\": \"Yıldızlar\", \"title\": [\"Akıllı oyna,\", \"3 yıldızı kap\"], \"sub\": \"Hamle artırarak bitir, tüm yıldızları topla.\", \"badges\": [[\"3 yıldız\"]] }\n },\n \"es\": {\n \"hero\": { \"title\": [\"Toca. Combina.\", \"¡Repite!\"], \"badges\": [[\"100+ niveles\", \"y más en cada versión\"], [\"Sin conexión\", \"Sin cuenta\"]] },\n \"play\": { \"eyebrow\": \"Juega\", \"title\": [\"Fácil de aprender,\", \"difícil de soltar\"], \"sub\": \"Un pulgar, rondas cortas, grandes momentos.\", \"badges\": [[\"Rondas cortas\", \"Dos minutos cada una\"], [\"¡Genial!\"]] },\n \"levels\": { \"eyebrow\": \"Mundos\", \"title\": [\"Explora\", \"nuevos mundos\"], \"sub\": \"Un aspecto nuevo y nuevas reglas cada diez niveles.\", \"badges\": [[\"10 mundos\", \"Uno nuevo cada 10 niveles\"]] },\n \"stars\": { \"eyebrow\": \"Estrellas\", \"title\": [\"Juega con cabeza,\", \"logra 3 estrellas\"], \"sub\": \"Termina con movimientos de sobra y consigue todas las estrellas.\", \"badges\": [[\"3 estrellas\"]] }\n },\n \"ja\": {\n \"hero\": { \"title\": [\"タップして、そろえて、\", \"もう一回!\"], \"badges\": [[\"100以上のステージ\", \"アップデートで追加\"], [\"オフラインで遊べる\", \"アカウント不要\"]] },\n \"play\": { \"eyebrow\": \"プレイ\", \"title\": [\"かんたんなのに\", \"やめられない\"], \"sub\": \"片手で遊べる短いラウンド、気持ちいい瞬間がたくさん。\", \"badges\": [[\"短いラウンド\", \"1回2分\"], [\"すごい!\"]] },\n \"levels\": { \"eyebrow\": \"ワールド\", \"title\": [\"新しいワールドを\", \"冒険しよう\"], \"sub\": \"10ステージごとに新しい見た目と新しいルール。\", \"badges\": [[\"10のワールド\", \"10ステージごとに追加\"]] },\n \"stars\": { \"eyebrow\": \"スター\", \"title\": [\"かしこく遊んで\", \"星を3つゲット\"], \"sub\": \"手数を残してクリアし、すべての星を集めよう。\", \"badges\": [[\"星3つ\"]] }\n },\n \"ar\": {\n \"hero\": { \"title\": [\"انقر. طابق.\", \"كرّر!\"], \"badges\": [[\"أكثر من 100 مرحلة\", \"والمزيد مع كل تحديث\"], [\"العب دون اتصال\", \"دون حساب\"]] },\n \"play\": { \"eyebrow\": \"العب\", \"title\": [\"سهلة التعلّم،\", \"صعبة التوقف\"], \"sub\": \"إبهام واحد، جولات قصيرة، ولحظات ممتعة.\", \"badges\": [[\"جولات قصيرة\", \"دقيقتان لكل جولة\"], [\"رائع!\"]] },\n \"levels\": { \"eyebrow\": \"العوالم\", \"title\": [\"استكشف\", \"عوالم جديدة\"], \"sub\": \"شكل جديد وقواعد جديدة كل عشر مراحل.\", \"badges\": [[\"10 عوالم\", \"عالم جديد كل 10 مراحل\"]] },\n \"stars\": { \"eyebrow\": \"النجوم\", \"title\": [\"العب بذكاء،\", \"واجمع 3 نجوم\"], \"sub\": \"أنهِ المرحلة بحركات متبقية واجمع كل النجوم.\", \"badges\": [[\"3 نجوم\"]] }\n }\n}\n", - "storebound.config.ts": "// storebound config for one game. Everything game-specific lives here; credentials NEVER do (they go in env vars or\n// .storebound/credentials.json, which must stay gitignored). Paths are relative to this file.\n// The type import is optional: it only works when storebound is installed in the project (bun add -d ).\nimport type { ShipConfig } from 'storebound';\n\nconst config: ShipConfig = {\n // Every compliance answer below is the TEMPLATE's, not yours. While an area is listed here, check / validate block\n // and nothing sends it to a store. Derive the answers from the code (storebound scan --write; for an app that is\n // already live: storebound onboard --declarations), answer the rest, then remove the area from this list.\n unanswered: ['dataSafety', 'appPrivacy', 'reviewNotes', 'ageRating'],\n\n app: {\n name: 'Example Game',\n profile: 'game',\n copyright: '2026 Example Studio',\n privacyPolicyUrl: 'https://example.com/privacy',\n supportUrl: 'https://example.com',\n marketingUrl: 'https://example.com',\n // Developer website on Play: the same domain must serve /app-ads.txt if the game shows ads\n website: 'https://example.com',\n contactEmail: 'support@example.com',\n },\n\n // id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale\n languages: [\n { id: 'en', play: 'en-US', apple: 'en-US' },\n { id: 'tr', play: 'tr-TR', apple: 'tr' },\n { id: 'es', play: ['es-ES', 'es-419'], apple: 'es-ES' },\n { id: 'ja', play: 'ja-JP', apple: 'ja' },\n { id: 'ar', play: 'ar', apple: 'ar-SA', rtl: true },\n ],\n\n play: {\n packageName: 'com.example.game',\n defaultLanguage: 'en-US',\n listing: 'store/listing.md',\n icon: 'store/icons/play-512.png',\n // read by `storebound scan` (fill in what is true for your account and app):\n // accountType: 'personal', accountCreated: 'YYYY-MM-DD', // new personal accounts: 12 testers x 14 days closed test\n // targetAges: ['13-15', '16-17', '18+'], // App content > Target audience; under 13 = Families policy\n },\n\n apple: {\n bundleId: 'com.example.game',\n // name: 'Example: Short Tagline', // App Store name (max 30) when app.name is taken there (\"already being used\"); default app.name\n // appId: '1234567890', // Apple ID of the app record; looked up by bundle id when left out\n listing: 'store/appstore.md',\n reviewShots: 'store/iap-review',\n // Export compliance (export-compliance skill). false ONLY if the game uses no encryption beyond the OS (HTTPS through\n // the system). Left unset on purpose: decide it, then uncomment. Same answer as ITSAppUsesNonExemptEncryption.\n // usesNonExemptEncryption: false,\n // ipad: true, // the app runs on iPad: 13\" iPad screenshots (2064 x 2752, ipad13) are rendered, checked and uploaded\n // iphoneMedium: true, // optional 6.3\" iPhone set (1206 x 2622, iphone-medium); off: Apple scales the 6.5\" set down\n // iphoneDuo: true, // optional iPhone Duo (foldable) sets, outer 1398 x 2034 + inner 2007 x 2853; rendered and checked, not uploaded until the API takes them\n // screenshotTypes: ['APP_IPHONE_61'], // what `storebound validate` requires per locale (default: medium display, met by a 6.9\" or 6.5\" set; + APP_IPAD_PRO_3GEN_129 with ipad)\n // TestFlight (`storebound apple testflight`, testflight skill). Tester emails live in .storebound/testers.csv only.\n testflight: {\n groups: [\n { name: 'Team', internal: true, allBuilds: true },\n { name: 'Friends' },\n ],\n description: { en: 'Example Game beta: try the new levels and tell us what feels too hard.' },\n },\n info: {\n categories: { primary: 'GAMES', primarySubOne: 'GAMES_CASUAL', primarySubTwo: 'GAMES_PUZZLE', secondary: 'ENTERTAINMENT' },\n ageRating: { advertising: true }, // everything else NONE / false\n contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',\n free: true,\n reviewNotes: [\n 'Single-player game. No account or login is needed; there is no user-generated content or chat.',\n 'In-app purchases: Home > Shop. Restore Purchases is in Settings.',\n 'Ads: optional rewarded videos and interstitials between rounds. The App Tracking Transparency prompt is shown before the ad SDK starts.',\n 'Privacy policy: Settings > Privacy.',\n // With remote config, feature flags or a remotely changed paywall (storebound scan warns, 2.3.1) you may add:\n // 'Shop offers and prices shown come from and change remotely; no feature is switched on or off after review.',\n // Google Play has no review-notes field: put the same line in reviewAccess.instructions (Play App content >\n // App access, 'Any other information') when the app has a sign-in, and say in the full description what needs\n // a paid subscription (Play: Deceptive behavior, Payments).\n ].join('\\n\\n'),\n },\n },\n\n // Price changes later for many products / regions: storebound prices export, edit store/prices.csv, prices import --dry.\n // prices: { csv: 'store/prices.csv', baseTerritory: 'USA' },\n\n // Same ids on both stores. Prices: USD base; Play converts per region, Apple equalizes from the USA. Overrides set a\n // region by hand (Apple needs an existing price point with exactly that value).\n products: [\n {\n id: 'com.example.game.remove_ads', type: 'non_consumable', usd: 2.99,\n overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }],\n listings: {\n en: { title: 'Remove Ads', description: 'Removes interstitial ads for good.' },\n tr: { title: 'Reklamları Kaldır', description: 'Ara reklamları kalıcı olarak kaldırır.' },\n },\n reviewScreenshot: 'store/iap-review/shop.png',\n },\n {\n id: 'com.example.game.coins_small', type: 'consumable', usd: 0.99,\n listings: {\n en: { title: '500 Coins', description: 'A small bag of coins.' },\n tr: { title: '500 Altın', description: 'Küçük bir kese altın.' },\n },\n reviewScreenshot: 'store/iap-review/shop.png',\n },\n ],\n\n // Google Play Data safety. Types not listed = not collected. Purposes: functionality, analytics, communications,\n // fraud, advertising, personalization, account. Typical F2P game with an analytics SDK + ad mediation:\n dataSafety: {\n collectsData: true,\n encryptedInTransit: true,\n // No accounts: PSL_ACM_NONE. A guest account with a name picked on the device and a server token (no password, no\n // email): accounts { enabled: true, signUp: ['username'], ... } and ['PSL_ACM_USER_ID_OTHER_AUTH'] here (Play's\n // \"Username and other authentication\"; `storebound scan` proposes it). With accounts Play also requires the\n // delete-account URL (from accounts.deletionUrl). That page also answers \"can users request that their data is\n // deleted?\" with yes (dataDeletion: 'yes' | 'no' | 'auto90' only for apps without one). partialDeletion: true +\n // partialDeletionUrl when a page deletes data without deleting the account. `storebound check` compares every row with Play's\n // current form (cli/data/play-data-safety-form.csv).\n accountCreation: ['PSL_ACM_NONE'],\n data: {\n PSL_APPROX_LOCATION: { shared: true, collect: ['analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_USER_INTERACTION: { shared: true, collect: ['analytics', 'advertising'], share: ['analytics', 'advertising'] },\n PSL_OTHER_APP_ACTIVITY: { shared: false, collect: ['functionality'] },\n PSL_PERFORMANCE_DIAGNOSTICS: { shared: true, collect: ['analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_DEVICE_ID: { shared: true, collect: ['functionality', 'analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_PURCHASE_HISTORY: { shared: false, collect: ['functionality', 'fraud'] },\n },\n },\n\n // Ads: `storebound validate --play` checks /app-ads.txt for these lines (copy them from your mediation\n // dashboard's app-ads.txt page). Placeholder line below; replace it.\n // ads: { appAdsTxt: ['example-network.com, pub-0000000000000000, DIRECT, 0000000000000000'] },\n\n renders: 'store/renders',\n\n shots: {\n copy: 'store/shots/copy.json',\n frames: 'store/shots/frames', // /phone/.png, /tablet/.png\n out: 'store/renders',\n logo: 'store/shots/logo.png', // optional: hero scene + feature graphic\n // Google Fonts; both cover Latin Extended (tr, pl, cs, ro ...) and Vietnamese. `shots fonts` checks every glyph of the\n // copy; a character the font lacks needs a font that has it or fallback: ['Noto Sans Display'] (else shots check fails).\n fonts: { title: 'Paytone One', body: 'Montserrat' },\n // phone: { w: 440, h: 902, dpr: 3 }, // raw frame size in CSS px (screen below a 54 px status bar)\n // tablet: { w: 1032, h: 1352, dpr: 2 }, // default with apple.ipad (13\" iPad below its 24 px bar), else 800 x 1240\n scenes: [\n { id: 'hero', palette: 'fire', hero: true, over: 34, frames: [{ src: 'hero.png', rotate: -4 }],\n badges: [{ y: 0.42, side: 'l', icon: 'flag', iconBg: 'red', rot: -4 }, { y: 0.66, side: 'r', icon: 'offline', iconBg: 'red', rot: 3 }] },\n { id: 'play', palette: 'violet', frames: [{ src: 'gameplay.png', rotate: 3 }],\n badges: [{ y: 0.3, side: 'l', icon: 'bolt', iconBg: 'gold', rot: -3 }, { y: 0.7, side: 'r', hot: true, rot: 4 }] },\n { id: 'levels', palette: 'sky', frames: [{ src: 'level-b.png', scale: 0.82 }, { src: 'level-a.png', x: -31, dy: 10, scale: 0.7, rotate: -8 }, { src: 'level-c.png', x: 31, dy: 10, scale: 0.7, rotate: 8 }],\n badges: [{ y: 0.66, side: 'r', icon: 'map', iconBg: 'blue', rot: 3 }] },\n { id: 'stars', palette: 'gold', frames: [{ src: 'results.png', rotate: -3 }],\n badges: [{ y: 0.42, side: 'l', stars: true, rot: -3 }] },\n ],\n feature: { frame: 'hero.png', palette: 'fire' },\n capture: {\n baseUrl: 'http://127.0.0.1:5173',\n query: 'lang={lang}&noads=1&seed=1',\n shots: {\n 'hero': { path: '/?level=12', waitFor: \"window.game && game.state === 'playing'\", delayMs: 1500, device: 'both' },\n 'gameplay': { path: '/?level=3', delayMs: 2500, device: 'both' },\n 'level-a': { path: '/?level=20', delayMs: 2000 },\n 'level-b': { path: '/?level=40', delayMs: 2000 },\n 'level-c': { path: '/?level=60', delayMs: 2000 },\n 'results': { path: '/?level=5&result=3', delayMs: 2000, device: 'both' },\n },\n },\n },\n\n icons: {\n composite: 'store/icon/icon.png', // 1024 opaque\n foreground: 'store/icon/foreground.png', // 1024 RGBA, subject on transparent\n background: 'store/icon/background.png', // 1024 opaque\n out: 'store/icons',\n web: 'public/icons',\n // capacitorAssets: 'native/assets',\n // capacitorCommand: 'bunx @capacitor/assets generate --assetPath native/assets',\n // androidRes: 'native/android/app/src/main/res',\n },\n};\n\nexport default config;\n", + "storebound.config.ts": "// storebound config for one game. Everything game-specific lives here; credentials NEVER do (they go in env vars or\n// .storebound/credentials.json, which must stay gitignored). Paths are relative to this file.\n// The type import is optional: it only works when storebound is installed in the project (bun add -d ).\nimport type { ShipConfig } from 'storebound';\n\nconst config: ShipConfig = {\n // Every compliance answer below is the TEMPLATE's, not yours. While an area is listed here, check / validate block\n // and nothing sends it to a store. Derive the answers from the code (storebound scan --write; for an app that is\n // already live: storebound onboard --declarations), answer the rest, then remove the area from this list.\n unanswered: ['dataSafety', 'appPrivacy', 'reviewNotes', 'ageRating'],\n\n app: {\n name: 'Example Game',\n profile: 'game',\n copyright: '2026 Example Studio',\n privacyPolicyUrl: 'https://example.com/privacy',\n supportUrl: 'https://example.com',\n marketingUrl: 'https://example.com',\n // Developer website on Play: the same domain must serve /app-ads.txt if the game shows ads\n website: 'https://example.com',\n contactEmail: 'support@example.com',\n },\n\n // id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale\n languages: [\n { id: 'en', play: 'en-US', apple: 'en-US' },\n { id: 'tr', play: 'tr-TR', apple: 'tr' },\n { id: 'es', play: ['es-ES', 'es-419'], apple: 'es-ES' },\n { id: 'ja', play: 'ja-JP', apple: 'ja' },\n { id: 'ar', play: 'ar', apple: 'ar-SA', rtl: true },\n ],\n\n play: {\n packageName: 'com.example.game',\n defaultLanguage: 'en-US',\n listing: 'store/listing.md',\n icon: 'store/icons/play-512.png',\n // read by `storebound scan` (fill in what is true for your account and app):\n // accountType: 'personal', accountCreated: 'YYYY-MM-DD', // new personal accounts: 12 testers x 14 days closed test\n // targetAges: ['13-15', '16-17', '18+'], // App content > Target audience; under 13 = Families policy\n },\n\n apple: {\n bundleId: 'com.example.game',\n // name: 'Example: Short Tagline', // App Store name (max 30) when app.name is taken there (\"already being used\"); default app.name\n // appId: '1234567890', // Apple ID of the app record; looked up by bundle id when left out\n listing: 'store/appstore.md',\n reviewShots: 'store/iap-review',\n // Export compliance (export-compliance skill). false ONLY if the game uses no encryption beyond the OS (HTTPS through\n // the system). Left unset on purpose: decide it, then uncomment. Same answer as ITSAppUsesNonExemptEncryption.\n // usesNonExemptEncryption: false,\n // ipad: true, // the app runs on iPad: 13\" iPad screenshots (2064 x 2752, ipad13) are rendered, checked and uploaded\n // iphoneMedium: true, // optional 6.3\" iPhone set (1206 x 2622, iphone-medium); off: Apple scales the 6.5\" set down\n // releaseType: \"afterApproval\", // afterApproval | manual | scheduled (+ earliestReleaseDate): how an approved version goes live; unset = new versions MANUAL\n // iphoneDuo: true, // optional iPhone Duo (foldable) sets, outer 1398 x 2034 + inner 2007 x 2853; rendered and checked, not uploaded until the API takes them\n // screenshotTypes: ['APP_IPHONE_61'], // what `storebound validate` requires per locale (default: medium display, met by a 6.9\" or 6.5\" set; + APP_IPAD_PRO_3GEN_129 with ipad)\n // TestFlight (`storebound apple testflight`, testflight skill). Tester emails live in .storebound/testers.csv only.\n testflight: {\n groups: [\n { name: 'Team', internal: true, allBuilds: true },\n { name: 'Friends' },\n ],\n description: { en: 'Example Game beta: try the new levels and tell us what feels too hard.' },\n },\n info: {\n categories: { primary: 'GAMES', primarySubOne: 'GAMES_CASUAL', primarySubTwo: 'GAMES_PUZZLE', secondary: 'ENTERTAINMENT' },\n ageRating: { advertising: true }, // everything else NONE / false\n contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',\n free: true,\n reviewNotes: [\n 'Single-player game. No account or login is needed; there is no user-generated content or chat.',\n 'In-app purchases: Home > Shop. Restore Purchases is in Settings.',\n 'Ads: optional rewarded videos and interstitials between rounds. The App Tracking Transparency prompt is shown before the ad SDK starts.',\n 'Privacy policy: Settings > Privacy.',\n // With remote config, feature flags or a remotely changed paywall (storebound scan warns, 2.3.1) you may add:\n // 'Shop offers and prices shown come from and change remotely; no feature is switched on or off after review.',\n // Google Play has no review-notes field: put the same line in reviewAccess.instructions (Play App content >\n // App access, 'Any other information') when the app has a sign-in, and say in the full description what needs\n // a paid subscription (Play: Deceptive behavior, Payments).\n ].join('\\n\\n'),\n },\n },\n\n // Price changes later for many products / regions: storebound prices export, edit store/prices.csv, prices import --dry.\n // prices: { csv: 'store/prices.csv', baseTerritory: 'USA' },\n\n // Same ids on both stores. Prices: USD base; Play converts per region, Apple equalizes from the USA. Overrides set a\n // region by hand (Apple needs an existing price point with exactly that value).\n products: [\n {\n id: 'com.example.game.remove_ads', type: 'non_consumable', usd: 2.99,\n overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }],\n listings: {\n en: { title: 'Remove Ads', description: 'Removes interstitial ads for good.' },\n tr: { title: 'Reklamları Kaldır', description: 'Ara reklamları kalıcı olarak kaldırır.' },\n },\n reviewScreenshot: 'store/iap-review/shop.png',\n },\n {\n id: 'com.example.game.coins_small', type: 'consumable', usd: 0.99,\n listings: {\n en: { title: '500 Coins', description: 'A small bag of coins.' },\n tr: { title: '500 Altın', description: 'Küçük bir kese altın.' },\n },\n reviewScreenshot: 'store/iap-review/shop.png',\n },\n ],\n\n // Google Play Data safety. Types not listed = not collected. Purposes: functionality, analytics, communications,\n // fraud, advertising, personalization, account. Typical F2P game with an analytics SDK + ad mediation:\n dataSafety: {\n collectsData: true,\n encryptedInTransit: true,\n // No accounts: PSL_ACM_NONE. A guest account with a name picked on the device and a server token (no password, no\n // email): accounts { enabled: true, signUp: ['username'], ... } and ['PSL_ACM_USER_ID_OTHER_AUTH'] here (Play's\n // \"Username and other authentication\"; `storebound scan` proposes it). With accounts Play also requires the\n // delete-account URL (from accounts.deletionUrl). That page also answers \"can users request that their data is\n // deleted?\" with yes (dataDeletion: 'yes' | 'no' | 'auto90' only for apps without one). partialDeletion: true +\n // partialDeletionUrl when a page deletes data without deleting the account. `storebound check` compares every row with Play's\n // current form (cli/data/play-data-safety-form.csv).\n accountCreation: ['PSL_ACM_NONE'],\n data: {\n PSL_APPROX_LOCATION: { shared: true, collect: ['analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_USER_INTERACTION: { shared: true, collect: ['analytics', 'advertising'], share: ['analytics', 'advertising'] },\n PSL_OTHER_APP_ACTIVITY: { shared: false, collect: ['functionality'] },\n PSL_PERFORMANCE_DIAGNOSTICS: { shared: true, collect: ['analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_DEVICE_ID: { shared: true, collect: ['functionality', 'analytics', 'advertising', 'fraud'], share: ['analytics', 'advertising', 'fraud'] },\n PSL_PURCHASE_HISTORY: { shared: false, collect: ['functionality', 'fraud'] },\n },\n },\n\n // Ads: `storebound validate --play` checks /app-ads.txt for these lines (copy them from your mediation\n // dashboard's app-ads.txt page). Placeholder line below; replace it.\n // ads: { appAdsTxt: ['example-network.com, pub-0000000000000000, DIRECT, 0000000000000000'] },\n\n renders: 'store/renders',\n\n shots: {\n copy: 'store/shots/copy.json',\n frames: 'store/shots/frames', // /phone/.png, /tablet/.png\n out: 'store/renders',\n logo: 'store/shots/logo.png', // optional: hero scene + feature graphic\n // Google Fonts; both cover Latin Extended (tr, pl, cs, ro ...) and Vietnamese. `shots fonts` checks every glyph of the\n // copy; a character the font lacks needs a font that has it or fallback: ['Noto Sans Display'] (else shots check fails).\n fonts: { title: 'Paytone One', body: 'Montserrat' },\n // phone: { w: 440, h: 902, dpr: 3 }, // raw frame size in CSS px (screen below a 54 px status bar)\n // tablet: { w: 1032, h: 1352, dpr: 2 }, // default with apple.ipad (13\" iPad below its 24 px bar), else 800 x 1240\n scenes: [\n { id: 'hero', palette: 'fire', hero: true, over: 34, frames: [{ src: 'hero.png', rotate: -4 }],\n badges: [{ y: 0.42, side: 'l', icon: 'flag', iconBg: 'red', rot: -4 }, { y: 0.66, side: 'r', icon: 'offline', iconBg: 'red', rot: 3 }] },\n { id: 'play', palette: 'violet', frames: [{ src: 'gameplay.png', rotate: 3 }],\n badges: [{ y: 0.3, side: 'l', icon: 'bolt', iconBg: 'gold', rot: -3 }, { y: 0.7, side: 'r', hot: true, rot: 4 }] },\n { id: 'levels', palette: 'sky', frames: [{ src: 'level-b.png', scale: 0.82 }, { src: 'level-a.png', x: -31, dy: 10, scale: 0.7, rotate: -8 }, { src: 'level-c.png', x: 31, dy: 10, scale: 0.7, rotate: 8 }],\n badges: [{ y: 0.66, side: 'r', icon: 'map', iconBg: 'blue', rot: 3 }] },\n { id: 'stars', palette: 'gold', frames: [{ src: 'results.png', rotate: -3 }],\n badges: [{ y: 0.42, side: 'l', stars: true, rot: -3 }] },\n ],\n feature: { frame: 'hero.png', palette: 'fire' },\n capture: {\n baseUrl: 'http://127.0.0.1:5173',\n query: 'lang={lang}&noads=1&seed=1',\n shots: {\n 'hero': { path: '/?level=12', waitFor: \"window.game && game.state === 'playing'\", delayMs: 1500, device: 'both' },\n 'gameplay': { path: '/?level=3', delayMs: 2500, device: 'both' },\n 'level-a': { path: '/?level=20', delayMs: 2000 },\n 'level-b': { path: '/?level=40', delayMs: 2000 },\n 'level-c': { path: '/?level=60', delayMs: 2000 },\n 'results': { path: '/?level=5&result=3', delayMs: 2000, device: 'both' },\n },\n },\n },\n\n icons: {\n composite: 'store/icon/icon.png', // 1024 opaque\n foreground: 'store/icon/foreground.png', // 1024 RGBA, subject on transparent\n background: 'store/icon/background.png', // 1024 opaque\n out: 'store/icons',\n web: 'public/icons',\n // capacitorAssets: 'native/assets',\n // capacitorCommand: 'bunx @capacitor/assets generate --assetPath native/assets',\n // androidRes: 'native/android/app/src/main/res',\n },\n};\n\nexport default config;\n", "testers.csv": "email,first_name,last_name,groups\n", "web/appstore.svg": "App Store", "web/googleplay.svg": "Google Play",