0.7.0: version (package, plugin and extension manifests), changelog section with summary; check no longer counts owner-only App content steps (IARC Terms, demo account typing) as missing config answers; the check shot shows a ready project (init plus icons, apple.name, App content answers, demo account): all good

This commit is contained in:
CRTkafa 2026-10-13 04:19:39 +08:00
parent 36a8bf9625
commit ba8cfd2594
11 changed files with 69 additions and 19 deletions

View file

@ -9,7 +9,7 @@
"name": "storebound",
"source": ".",
"description": "Take a mobile app or game from nothing to Submit for review on both stores: skills for every store step and questionnaire plus the storebound CLI and MCP server.",
"version": "0.6.0",
"version": "0.7.0",
"category": "development",
"tags": [
"mobile",

View file

@ -1,6 +1,6 @@
{
"name": "storebound",
"version": "0.6.0",
"version": "0.7.0",
"description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire (Data safety, App access and demo accounts, account deletion, IARC, target audience, ads, App Privacy, age rating, export compliance), privacy, icons, screenshots and previews, validate, TestFlight, release management, and after launch: crash vitals, review replies, price sheets, store analytics, Product Page Optimization. Agent Skills plus a Bun CLI and MCP server (storebound); you press Submit. CRTkafa's own toolchain, open-sourced.",
"author": {
"name": "CRTkafa"

View file

@ -1,6 +1,6 @@
{
"name": "storebound",
"version": "0.6.0",
"version": "0.7.0",
"description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire (Data safety, App access and demo accounts, account deletion, IARC, target audience, ads, App Privacy, age rating, export compliance), privacy, icons, screenshots and previews, validate, TestFlight, release management, and after launch: crash vitals, review replies, price sheets, store analytics, Product Page Optimization. Agent Skills plus a Bun CLI and MCP server (storebound); you press Submit. CRTkafa's own toolchain, open-sourced.",
"author": {
"name": "CRTkafa"

View file

@ -1,6 +1,17 @@
# Changelog
## Unreleased
## 0.7.0 · 2026-10-13
Agent-first: the AI does the whole job ("do it, don't ask") and the owner presses only the final public buttons, with
every compliance question asked once, in one batch. Consoles without an API get `console plan` / `console verify`
(start URL, steps, answer sheet, never-press list) for App Privacy, Play App content, IARC, Create app and the first
production release. The desktop app sends for review itself (dry-run plan first). The CLI runs on Node 20+
(`npx storebound`). Plus many store fixes from real releases (IAP resubmits, Invalid Binary, Data safety, uploads).
- Fix (`check`): "N Play App content answer(s) are not in the config" no longer counts the two steps that stay the
owner's however complete the config is (ticking the IARC Terms of Use, typing the demo account's username and
password), so a fully answered project checks clean. The site's `check` shot (`tools/site-term-shots.ts`) now shows
that ready project: a fresh `init` plus icon files, `apple.name`, the App content answers and the demo account.
- The CLI runs on Node 20+ (owner decision, 2026-10-13): `npx storebound init` works, `bunx storebound` keeps working.
The npm package's `bin` is one Node ESM bundle, `dist/storebound.mjs` (built by `prepack` with

View file

@ -506,7 +506,10 @@ export function playConsoleReminders(config: ShipConfig): { warnings: string[];
const cat = playCategoryOf(config);
if (!cat) warnings.push('play.category is not set: Play\'s App category stays "Not selected" (the Play API cannot set it, so `play listing` does not). Set play.category (a Play category name, e.g. "Strategy"), then set it in Store settings with `storebound console plan play-store-settings`');
else hints.push(`Play App category "${cat.category}" (${cat.source}) is a console step, not an API one: \`storebound console plan play-store-settings\``);
const ask = consolePlan('play-app-content', config).askOwner;
// only answers the config can hold: the IARC Terms box (an agreement the owner ticks) and the demo account's
// username / password (the owner types them from .storebound/credentials.json; check reports a missing one itself)
// stay owner steps however complete the config is, so they never count here
const ask = consolePlan('play-app-content', config).answers.filter(a => a.status === 'ask-owner' && !a.id.endsWith('/credentials'));
if (ask.length) warnings.push(`${ask.length} Play App content answer(s) are not in the config: ask the owner in one batch at the start (\`storebound console plan play-app-content\` lists them), then write them into the config`);
if (config.products?.some(p => p.randomRewards)) hints.push('products with randomRewards (loot boxes): ClassInd (Brazil) rated such a game 18 on a real IARC run; other authorities barely moved');
return { warnings, hints };

View file

@ -1,6 +1,6 @@
{
"name": "storebound",
"version": "0.6.0",
"version": "0.7.0",
"description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire (Data safety, App access and demo accounts, account deletion, IARC, target audience, ads, App Privacy, age rating, export compliance), privacy, icons, screenshots and previews, validate, TestFlight, release management, and after launch: crash vitals, review replies, price sheets, store analytics, Product Page Optimization. Agent Skills plus a Bun CLI and MCP server (storebound); you press Submit. CRTkafa's own toolchain, open-sourced.",
"contextFileName": "GEMINI.md",
"mcpServers": {

View file

@ -1,6 +1,6 @@
{
"name": "storebound",
"version": "0.6.0",
"version": "0.7.0",
"desktopVersion": "0.1.0",
"description": "Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every store questionnaire (incl. account deletion and App Review demo accounts), privacy, icons, screenshots and previews, TestFlight. After launch: crash vitals, review replies, price sheets, analytics and Product Page Optimization. One config, a CLI (Node 20+ or Bun), an MCP server and Agent Skills for your AI coding agent.",
"license": "MIT",

View file

@ -1,15 +1,8 @@
$ storebound check
[check] config ok: storebound.config.ts
[check] play listing: 2 languages
[check] hint: the App Store name is "Example App" in every locale: App Store names are unique, so if it is taken App Store Connect refuses the record ("already being used"). Then set apple.name with a short suffix, e.g. "Example App: <tagline>" (30 characters at most); app.name stays the Play and on-device name
[check] appstore listing: 2 locales
[check] data safety: 768 CSV rows
[check] shots copy: 2 languages x 4 scenes
[check] hint: Play App category "Productivity" (apple.info.categories (PRODUCTIVITY)) is a console step, not an API one: `storebound console plan play-store-settings`
[check] WARN 31 Play App content answer(s) are not in the config: ask the owner in one batch at the start (`storebound console plan play-app-content` lists them), then write them into the config
- App Review needs a demo account (sign-in required) · add reviewDemoAccount to .storebound/credentials.json · review-access skill
- missing file: store/icon/icon.png
- missing file: store/icon/foreground.png
- missing file: store/icon/background.png
error: 4 problems
[check] all good

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -2,7 +2,7 @@
// the in-page helper and the config fields behind them. Pure; the facts were verified on a real Play Console
// (2026-10-12); every id here is a placeholder.
import { describe, expect, test } from 'bun:test';
import { type ShipConfig, validateConfig, validateExtras } from '../cli/lib/config';
import { IARC_QUESTIONS, type ShipConfig, validateConfig, validateExtras } from '../cli/lib/config';
import { consoleAnswers, PLAN_FORM_IDS, PLAN_ONLY_FORM_IDS } from '../cli/lib/console-answers';
import { formDef, forbiddenTick } from '../cli/lib/console-forms';
import { consolePlan, IN_PAGE_LOOP, playConsoleReminders, planText } from '../cli/lib/console-plan';
@ -281,6 +281,13 @@ describe('verify, check, validate, config and MCP for the new forms', () => {
expect(set.hints.some(h => h.includes('ClassInd'))).toBe(true);
expect(set.warnings.some(w => /Play App content answer\(s\) are not in the config/.test(w))).toBe(true);
expect(playConsoleReminders({ ...base, play: undefined } as ShipConfig)).toEqual({ warnings: [], hints: [] });
// every config answer given: the IARC Terms agreement and the demo account typing stay owner steps, no warning
const answered = { ...base, reviewAccess: { signInRequired: true, instructions: 'Sign in with the demo account.' }, console: { ...base.console,
contentRating: { category: 'other', answers: Object.fromEntries(IARC_QUESTIONS.map(q => [q, false])), details: { miscellaneous: { naziSymbols: false, koreaHistory: false, terrorism: false, crimes: false } } },
targetAudience: { ages: ['18-over'], appealsToChildren: false, containsAds: false },
appContent: { government: false, financialFeatures: false, healthFeatures: false } } } as ShipConfig;
expect(consolePlan('play-app-content', answered).askOwner.length).toBeGreaterThan(0);
expect(playConsoleReminders(answered).warnings.filter(w => /App content answer/.test(w))).toEqual([]);
});
test('validate: the category reminder (ok with one, warn without), nothing when not passed', () => {

View file

@ -13,7 +13,7 @@
// bun tools/site-term-shots.ts --text only the .txt files (no browser)
// bun tools/site-term-shots.ts scan check only these shots
import { generateKeyPairSync } from 'node:crypto';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { closeBrowser, launch } from '../cli/shots/browser';
@ -74,6 +74,38 @@ const PRODUCTS = ` products: [
],
dataSafety: {`;
/** What the owner adds to a fresh `storebound init` project before the first release, so that `check` passes:
* the icon artwork (examples/demo-game's real icon files), the App Store name with a suffix, the Play App content
* answers (the batch `console plan play-app-content` asks for) and the App Review demo account in
* .storebound/credentials.json (placeholder values). Returns a function that puts the project back as init wrote it. */
export function makeReady(dir: string): () => void {
const cfg = join(dir, 'storebound.config.ts'), creds = join(dir, '.storebound', 'credentials.json');
const src = readFileSync(cfg, 'utf8'), credSrc = readFileSync(creds, 'utf8');
for (const anchor of [" listing: 'store/appstore.md',\n", ' renders: ']) if (!src.includes(anchor)) throw new Error(`the init config has no ${JSON.stringify(anchor)} line`);
writeFileSync(cfg, src
.replace(" listing: 'store/appstore.md',\n", " listing: 'store/appstore.md',\n name: 'Example App: Lists & Notes',\n")
.replace(' renders: ', `${READY_CONSOLE}\n renders: `));
const c = JSON.parse(credSrc) as Record<string, unknown>;
writeFileSync(creds, JSON.stringify({ ...c, reviewDemoAccount: { username: 'demo@example.com', password: 'placeholder-demo-password' } }, null, 2));
const icons = join(dir, 'store', 'icon');
mkdirSync(icons, { recursive: true });
for (const f of ['icon.png', 'foreground.png', 'background.png']) copyFileSync(join(DEMO, 'store', 'icon', f), join(icons, f));
return () => { writeFileSync(cfg, src); writeFileSync(creds, credSrc); rmSync(icons, { recursive: true, force: true }); };
}
/** The Play App content answers of a plain utility app without ads, for everyone 18+ (config `console`). */
const READY_CONSOLE = ` console: {
contentRating: {
category: 'other',
answers: { violence: false, fear: false, sexuality: false, language: false, controlledSubstances: false, crudeHumor: false, gambling: false,
simulatedGambling: false, lootBoxes: false, userInteraction: false, sharesLocation: false, digitalPurchases: false, unrestrictedInternet: false },
details: { miscellaneous: { naziSymbols: false, koreaHistory: false, terrorism: false, crimes: false } },
},
targetAudience: { ages: ['18-over'], appealsToChildren: false, containsAds: false },
appContent: { government: false, financialFeatures: false, healthFeatures: false },
},
`;
// --- cleaning --------------------------------------------------------------------------------------------------------
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
@ -171,8 +203,12 @@ const SHOTS: Shot[] = [
},
{
name: 'check', cmd: 'storebound check', title: 'my-app',
codes: [1], markers: ['config ok', /^error: \d+ problems?$/],
run: async p => ({ ...await run(['check'], p.dir), paths: [[p.dir, '']] }),
codes: [0], markers: ['config ok', 'all good'],
run: async p => {
const restore = makeReady(p.dir);
try { return { ...await run(['check'], p.dir, { REVIEW_DEMO_USER: '', REVIEW_DEMO_PASSWORD: '' }), paths: [[p.dir, '']] }; }
finally { restore(); }
},
image: lines => head(lines.map(short), 24, 'lines'),
},
{