diff --git a/cli/lib/desktop-coverage.ts b/cli/lib/desktop-coverage.ts new file mode 100644 index 0000000..4f3c803 --- /dev/null +++ b/cli/lib/desktop-coverage.ts @@ -0,0 +1,138 @@ +// Where every CLI command (cli/commands.ts COMMANDS + PLAIN) lives in the desktop app. One source for +// tools/gen-desktop-coverage.ts (docs/desktop-coverage.md) and the guard test (tests/desktop-coverage.test.ts): a +// command without an entry, or with a method the window never calls, fails the test. +// +// methods: serve.ts methods (`play.listing`, `owner.play_rollout`) or host commands of src-tauri (`host:accounts_list`). +// owner: the owner-only parts of the command; the More tools tab lists them as terminal steps (OWNER_STEPS). + +export type Coverage = + | { where: string; methods: string[]; owner?: string } + | { not: string }; + +/** The More tools tab (apps/desktop/src/screens/tabs/Tools.tsx): one typed form per method, built from its schema + * (`system.methods`). Reads run; writes go through the dry run, the native dialog and the confirm token. */ +export const TOOLBOX: { id: string; methods: string[] }[] = [ + { id: 'build', methods: ['play.build', 'play.upload', 'play.share', 'play.next_version_code', 'play.app_id', 'apple.build', 'apple.upload', 'apple.next_build', + 'apple.signing_status', 'apple.signing_ensure', 'apple.bundle', 'apple.testflight_info', 'apple.testflight_wait'] }, + { id: 'adhoc', methods: ['apple.adhoc_devices', 'apple.adhoc_register', 'apple.adhoc_options', 'apple.adhoc_export'] }, + { id: 'store', methods: ['apple.iap_review', 'apple.iap_review_plan', 'apple.creative_assets', 'apple.creative_assets_verify', 'apple.cpp_list', 'apple.cpp_create', + 'apple.cpp_upload', 'apple.events_list', 'apple.events_create', 'apple.events_upload', 'apple.preorder_status', 'catalog.pull', 'onboard', 'revenuecat.diff', 'prices.ppp'] }, + { id: 'text', methods: ['whats.new', 'whats.new_languages', 'whats.new_check', 'whats.new_apply', 'listing.translate', 'listing.check', 'check.aso', 'aso.rank', + 'aso.competitors', 'aso.suggest'] }, + { id: 'capture', methods: ['shots.capture', 'shots.record', 'shots.check', 'shots.fonts', 'shots.jpeg'] }, + { id: 'quality', methods: ['health.report', 'triage', 'originality', 'privacy.manifest', 'ads.scan', 'web.init', 'web.check'] }, + { id: 'ads', methods: ['ads.apple_status', 'ads.apple_campaigns', 'ads.apple_report'] }, + { id: 'flows', methods: ['flow.list', 'flow.run', 'agent.install'] }, +]; + +/** Owner-only steps without a window button: the More tools tab shows each with its terminal command to copy. */ +export const OWNER_STEPS: { id: string; command: string }[] = [ + { id: 'offer_codes', command: 'storebound apple offer-codes' }, + { id: 'win_back', command: 'storebound apple win-back' }, + { id: 'promoted', command: 'storebound apple promoted --enable' }, + { id: 'preorder', command: 'storebound apple preorder enable --release-date YYYY-MM-DD' }, + { id: 'cpp_submit', command: 'storebound apple cpp submit --name ' }, + { id: 'events_submit', command: 'storebound apple events submit --name ' }, + { id: 'tf_expire', command: 'storebound apple testflight expire --keep 5' }, + { id: 'ads_change', command: 'storebound ads apple bid --campaign --keyword --bid 1.20' }, + { id: 'rc_sync', command: 'storebound revenuecat sync' }, + { id: 'shots_login', command: 'storebound shots login' }, +]; + +const T = 'More tools'; +export const DESKTOP_COVERAGE: Record = { + // PLAIN + 'init': { where: 'App folders: Start an app folder', methods: ['host:project_init'] }, + 'import': { where: 'App folders / first-run guide: Import from fastlane', methods: ['host:project_import_fastlane'] }, + 'install': { where: `${T}: Install for an AI tool`, methods: ['agent.install'] }, + 'mcp': { not: 'the stdio server for AI tools; the window itself is the GUI over the same commands' }, + 'serve': { not: 'the JSON-RPC sidecar the window runs on; nothing to click' }, + 'apps': { where: 'Accounts (store apps per account, key check) and App folders (list, add, remove)', methods: ['host:store_apps_refresh', 'host:account_check', 'host:projects_list', 'host:project_add', 'host:project_remove'] }, + 'desktop': { not: 'opens or installs this window from a terminal' }, + 'accounts': { where: 'Accounts screen (profiles shared with the CLI)', methods: ['host:accounts_list', 'host:account_add_play', 'host:account_add_apple', 'host:account_update'] }, + 'doctor': { where: 'Dependencies screen', methods: ['system.tools', 'host:tools_info'] }, + 'scan': { where: 'Checks tab: Scan, Fix', methods: ['scan', 'scan.write'] }, + 'api': { not: 'raw API calls by operation id: every task has its own screen, and raw writes stay a terminal step the owner types' }, + 'privacy-manifest': { where: `${T}: Quality`, methods: ['privacy.manifest'] }, + 'ws': { where: 'Portfolio, studios and Settings (the workspace of this machine)', methods: ['host:workspace_get', 'host:portfolio_status'] }, + 'release': { where: 'Portfolio release trains and the Releases tab', methods: ['host:portfolio_status', 'apple.status', 'play.status'] }, + 'flow': { where: `${T}: Workflows (plans; owner steps never run)`, methods: ['flow.list', 'flow.run'] }, + 'ads': { where: `${T}: Quality (AdAttributionKit / SKAdNetwork scan)`, methods: ['ads.scan'] }, + // COMMANDS + 'edit': { where: 'Editors on every tab (Save edits)', methods: ['project.editable', 'project.edit'] }, + 'inspect': { where: 'App card, App info, Listings, Store art (offline reads)', methods: ['host:portfolio_status', 'project.declarations', 'project.listings', 'project.listing_text', 'project.icon', 'project.render'] }, + 'check': { where: 'Checks tab', methods: ['check'] }, + 'validate': { where: 'Checks tab', methods: ['validate'] }, + 'report': { where: 'Checks tab: readiness report', methods: ['report'] }, + 'analytics': { where: 'Growth tab: chart, snapshot, setup', methods: ['analytics.compare', 'analytics.snapshot', 'analytics.setup'] }, + 'health': { where: 'Health tab; weekly report in More tools', methods: ['health', 'health.issue', 'health.report'] }, + 'art': { where: 'Store art tab: live art, pull, replace', methods: ['art.live', 'art.thumb', 'art.pull', 'art.replace'] }, + 'onboard': { where: `${T}: Store features`, methods: ['onboard'] }, + 'catalog': { where: 'Catalog tab (parity); pull in More tools', methods: ['catalog.diff', 'catalog.pull'] }, + 'reports': { where: 'Growth tab: sales and finance reports', methods: ['reports'] }, + 'triage': { where: `${T}: Quality`, methods: ['triage'] }, + 'aso': { where: 'Growth tab: ASO audit; rank, competitors, suggest in More tools', methods: ['aso.audit', 'aso.rank', 'aso.competitors', 'aso.suggest'] }, + 'prices': { where: 'Prices tab (export, compare, apply = owner action); PPP sheet in More tools', methods: ['prices.current', 'prices.export', 'prices.diff', 'owner.prices_import', 'prices.ppp'] }, + 'ads apple': { where: `${T}: Apple Ads (status, campaigns, report)`, methods: ['ads.apple_status', 'ads.apple_campaigns', 'ads.apple_report'], owner: 'bid, budget, pause' }, + 'web': { where: `${T}: Quality (init, check)`, methods: ['web.init', 'web.check'] }, + 'revenuecat': { where: `${T}: Store features (diff)`, methods: ['revenuecat.diff'], owner: 'sync' }, + 'console plan': { where: 'App info: Console steps without an API', methods: ['console.plan'] }, + 'console verify': { where: 'App info: Console steps, Check', methods: ['console.verify'] }, + 'console fill': { where: 'App info: Fill in console', methods: ['console.fill'] }, + 'reviews': { where: 'Reviews tab (list, draft, reply, approve; Send = owner action)', methods: ['reviews.list', 'reviews.draft', 'reviews.drafts', 'reviews.reply', 'reviews.approve', 'owner.reply_send'] }, + 'originality': { where: `${T}: Quality`, methods: ['originality'] }, + 'listing': { where: 'Listings tab (live texts, store edits); translate and check in More tools', methods: ['listing.live', 'listing.set', 'listing.translate', 'listing.check'] }, + 'whats-new': { where: `${T}: Text and ASO`, methods: ['whats.new', 'whats.new_languages', 'whats.new_check', 'whats.new_apply'] }, + 'play products': { where: 'Catalog tab', methods: ['play.products', 'owner.prices_play_products'] }, + 'play subscriptions': { where: 'Catalog tab', methods: ['play.subscriptions', 'owner.prices_play_subscriptions'] }, + 'play listing': { where: 'Listings tab (push, pull)', methods: ['play.listing', 'play.listing_pull'] }, + 'play images': { where: 'Listings tab: Play graphics', methods: ['play.images'] }, + 'play data-safety': { where: 'App info tab: Data safety upload', methods: ['play.data_safety'] }, + 'play release': { where: 'Releases tab: Send to production (owner action); testing tracks via play upload in More tools', methods: ['owner.play_production', 'play.upload'] }, + 'play build': { where: `${T}: Build and upload`, methods: ['play.build'] }, + 'play upload': { where: `${T}: Build and upload`, methods: ['play.upload'] }, + 'play status': { where: 'Releases tab: Google Play card', methods: ['play.status'] }, + 'play rollout': { where: 'Releases tab: rollout (owner action)', methods: ['owner.play_rollout'] }, + 'play share': { where: `${T}: Build and upload`, methods: ['play.share'] }, + 'play app-id': { where: `${T}: Build and upload`, methods: ['play.app_id'] }, + 'play next-version-code': { where: `${T}: Build and upload`, methods: ['play.next_version_code'] }, + 'apple bundle': { where: `${T}: Build and upload`, methods: ['apple.bundle'] }, + 'apple iap': { where: 'Catalog tab', methods: ['apple.iap'] }, + 'apple subscriptions': { where: 'Catalog tab', methods: ['apple.subscriptions', 'owner.prices_apple_subscriptions'] }, + 'apple iap-review': { where: `${T}: Store features`, methods: ['apple.iap_review'] }, + 'apple iap-submit': { where: 'Releases tab: Submit in-app purchases (owner action); plan in More tools', methods: ['owner.apple_iap_submit', 'apple.iap_review_plan'] }, + 'apple submit': { where: 'Releases tab: Submit for review (owner action)', methods: ['owner.apple_submit'] }, + 'apple resubmit': { where: 'Releases tab: Resubmit (owner action)', methods: ['owner.apple_resubmit'] }, + 'apple offer-codes': { where: `${T}: owner steps (terminal command)`, methods: [], owner: 'creating campaigns and codes' }, + 'apple win-back': { where: `${T}: owner steps (terminal command)`, methods: [], owner: 'win-back offers go live on their start date' }, + 'apple promoted': { where: `${T}: owner steps (terminal command)`, methods: [], owner: 'promoted purchases on the product page' }, + 'apple preorder': { where: `${T}: Store features (status); enable / end as owner steps`, methods: ['apple.preorder_status'], owner: 'enable, end' }, + 'apple cpp': { where: `${T}: Store features (list, create, upload)`, methods: ['apple.cpp_list', 'apple.cpp_create', 'apple.cpp_upload'], owner: 'submit, visible' }, + 'apple events': { where: `${T}: Store features (list, create, upload)`, methods: ['apple.events_list', 'apple.events_create', 'apple.events_upload'], owner: 'submit' }, + 'apple listing': { where: 'Listings tab (push, pull)', methods: ['apple.listing', 'apple.listing_pull'] }, + 'apple screenshots': { where: 'Listings tab: App Store screenshots', methods: ['apple.screenshots'] }, + 'apple appinfo': { where: 'App info tab: Push', methods: ['apple.appinfo'] }, + 'apple review-details': { where: 'App info tab: App Review details', methods: ['apple.review_details'] }, + 'apple status': { where: 'Releases tab: Send for review, App Store card', methods: ['apple.status'] }, + 'apple creative-assets': { where: `${T}: Store features`, methods: ['apple.creative_assets', 'apple.creative_assets_verify'] }, + 'apple previews': { where: 'Listings tab: App Store previews', methods: ['apple.previews'] }, + 'apple release': { where: 'Releases tab: phased release (owner action)', methods: ['owner.ios_phased'] }, + 'apple signing': { where: `${T}: Build and upload (status, ensure)`, methods: ['apple.signing_status', 'apple.signing_ensure'] }, + 'apple build': { where: `${T}: Build and upload`, methods: ['apple.build'] }, + 'apple upload': { where: `${T}: Build and upload`, methods: ['apple.upload'] }, + 'apple adhoc': { where: `${T}: Ad-hoc devices (devices, register, options, export)`, methods: ['apple.adhoc_devices', 'apple.adhoc_register', 'apple.adhoc_options', 'apple.adhoc_export'] }, + 'apple next-build': { where: `${T}: Build and upload`, methods: ['apple.next_build'] }, + 'apple encryption': { where: 'App info tab: export compliance', methods: ['apple.encryption'] }, + 'apple ppo': { where: 'Growth tab: PPO tests (start / stop = owner actions)', methods: ['apple.ppo_status', 'apple.ppo_create', 'apple.ppo_upload', 'owner.ppo_start', 'owner.ppo_stop'] }, + 'apple testflight': { where: 'TestFlight tab (status, groups, What to Test, distribute, testers = owner action); info and wait in More tools', methods: ['apple.testflight_status', 'apple.testflight_groups', 'apple.testflight_what_to_test', 'apple.testflight_distribute', 'owner.testflight_testers', 'apple.testflight_info', 'apple.testflight_wait'], owner: 'expire' }, + 'icons': { where: 'Store art tab: icons', methods: ['icons'] }, + 'shots capture': { where: `${T}: Captures`, methods: ['shots.capture'] }, + 'shots login': { where: `${T}: owner steps (terminal command)`, methods: [], owner: 'signing in to a demo account by hand in a visible browser' }, + 'shots fonts': { where: `${T}: Captures`, methods: ['shots.fonts'] }, + 'shots render': { where: 'Store art tab: Render', methods: ['shots.render'] }, + 'shots jpeg': { where: `${T}: Captures`, methods: ['shots.jpeg'] }, + 'shots check': { where: `${T}: Captures`, methods: ['shots.check'] }, + 'shots record': { where: `${T}: Captures`, methods: ['shots.record'] }, + 'shots video': { where: 'Store art tab: previews', methods: ['shots.video'] }, + 'shots gallery': { where: 'Store art tab: gallery, review page', methods: ['shots.gallery', 'project.renders'] }, +}; diff --git a/docs/desktop-coverage.md b/docs/desktop-coverage.md new file mode 100644 index 0000000..687ded5 --- /dev/null +++ b/docs/desktop-coverage.md @@ -0,0 +1,103 @@ +# Desktop coverage + +Generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`; do not edit by hand. +Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered" means each method listed exists +(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap. +Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools). + +**92 commands:** 88 covered, 0 gaps, 4 not for the desktop. + +| command | status | where in the window | methods | owner step | +|---|---|---|---|---| +| `init` | covered | App folders: Start an app folder | `host:project_init` | | +| `import` | covered | App folders / first-run guide: Import from fastlane | `host:project_import_fastlane` | | +| `install` | covered | More tools: Install for an AI tool | `agent.install` | | +| `mcp` | not-for-desktop | the stdio server for AI tools; the window itself is the GUI over the same commands | | | +| `serve` | not-for-desktop | the JSON-RPC sidecar the window runs on; nothing to click | | | +| `apps` | covered | Accounts (store apps per account, key check) and App folders (list, add, remove) | `host:store_apps_refresh`, `host:account_check`, `host:projects_list`, `host:project_add`, `host:project_remove` | | +| `desktop` | not-for-desktop | opens or installs this window from a terminal | | | +| `accounts` | covered | Accounts screen (profiles shared with the CLI) | `host:accounts_list`, `host:account_add_play`, `host:account_add_apple`, `host:account_update` | | +| `doctor` | covered | Dependencies screen | `system.tools`, `host:tools_info` | | +| `scan` | covered | Checks tab: Scan, Fix | `scan`, `scan.write` | | +| `api` | not-for-desktop | raw API calls by operation id: every task has its own screen, and raw writes stay a terminal step the owner types | | | +| `privacy-manifest` | covered | More tools: Quality | `privacy.manifest` | | +| `ws` | covered | Portfolio, studios and Settings (the workspace of this machine) | `host:workspace_get`, `host:portfolio_status` | | +| `release` | covered | Portfolio release trains and the Releases tab | `host:portfolio_status`, `apple.status`, `play.status` | | +| `flow` | covered | More tools: Workflows (plans; owner steps never run) | `flow.list`, `flow.run` | | +| `ads` | covered | More tools: Quality (AdAttributionKit / SKAdNetwork scan) | `ads.scan` | | +| `edit` | covered | Editors on every tab (Save edits) | `project.editable`, `project.edit` | | +| `inspect` | covered | App card, App info, Listings, Store art (offline reads) | `host:portfolio_status`, `project.declarations`, `project.listings`, `project.listing_text`, `project.icon`, `project.render` | | +| `check` | covered | Checks tab | `check` | | +| `validate` | covered | Checks tab | `validate` | | +| `report` | covered | Checks tab: readiness report | `report` | | +| `analytics` | covered | Growth tab: chart, snapshot, setup | `analytics.compare`, `analytics.snapshot`, `analytics.setup` | | +| `health` | covered | Health tab; weekly report in More tools | `health`, `health.issue`, `health.report` | | +| `art` | covered | Store art tab: live art, pull, replace | `art.live`, `art.thumb`, `art.pull`, `art.replace` | | +| `onboard` | covered | More tools: Store features | `onboard` | | +| `catalog` | covered | Catalog tab (parity); pull in More tools | `catalog.diff`, `catalog.pull` | | +| `reports` | covered | Growth tab: sales and finance reports | `reports` | | +| `triage` | covered | More tools: Quality | `triage` | | +| `aso` | covered | Growth tab: ASO audit; rank, competitors, suggest in More tools | `aso.audit`, `aso.rank`, `aso.competitors`, `aso.suggest` | | +| `prices` | covered | Prices tab (export, compare, apply = owner action); PPP sheet in More tools | `prices.current`, `prices.export`, `prices.diff`, `owner.prices_import`, `prices.ppp` | | +| `ads apple` | covered | More tools: Apple Ads (status, campaigns, report) | `ads.apple_status`, `ads.apple_campaigns`, `ads.apple_report` | bid, budget, pause | +| `web` | covered | More tools: Quality (init, check) | `web.init`, `web.check` | | +| `revenuecat` | covered | More tools: Store features (diff) | `revenuecat.diff` | sync | +| `console plan` | covered | App info: Console steps without an API | `console.plan` | | +| `console verify` | covered | App info: Console steps, Check | `console.verify` | | +| `console fill` | covered | App info: Fill in console | `console.fill` | | +| `reviews` | covered | Reviews tab (list, draft, reply, approve; Send = owner action) | `reviews.list`, `reviews.draft`, `reviews.drafts`, `reviews.reply`, `reviews.approve`, `owner.reply_send` | | +| `originality` | covered | More tools: Quality | `originality` | | +| `listing` | covered | Listings tab (live texts, store edits); translate and check in More tools | `listing.live`, `listing.set`, `listing.translate`, `listing.check` | | +| `whats-new` | covered | More tools: Text and ASO | `whats.new`, `whats.new_languages`, `whats.new_check`, `whats.new_apply` | | +| `play products` | covered | Catalog tab | `play.products`, `owner.prices_play_products` | | +| `play subscriptions` | covered | Catalog tab | `play.subscriptions`, `owner.prices_play_subscriptions` | | +| `play listing` | covered | Listings tab (push, pull) | `play.listing`, `play.listing_pull` | | +| `play images` | covered | Listings tab: Play graphics | `play.images` | | +| `play data-safety` | covered | App info tab: Data safety upload | `play.data_safety` | | +| `play release` | covered | Releases tab: Send to production (owner action); testing tracks via play upload in More tools | `owner.play_production`, `play.upload` | | +| `play build` | covered | More tools: Build and upload | `play.build` | | +| `play upload` | covered | More tools: Build and upload | `play.upload` | | +| `play status` | covered | Releases tab: Google Play card | `play.status` | | +| `play rollout` | covered | Releases tab: rollout (owner action) | `owner.play_rollout` | | +| `play share` | covered | More tools: Build and upload | `play.share` | | +| `play app-id` | covered | More tools: Build and upload | `play.app_id` | | +| `play next-version-code` | covered | More tools: Build and upload | `play.next_version_code` | | +| `apple bundle` | covered | More tools: Build and upload | `apple.bundle` | | +| `apple iap` | covered | Catalog tab | `apple.iap` | | +| `apple subscriptions` | covered | Catalog tab | `apple.subscriptions`, `owner.prices_apple_subscriptions` | | +| `apple iap-review` | covered | More tools: Store features | `apple.iap_review` | | +| `apple iap-submit` | covered | Releases tab: Submit in-app purchases (owner action); plan in More tools | `owner.apple_iap_submit`, `apple.iap_review_plan` | | +| `apple submit` | covered | Releases tab: Submit for review (owner action) | `owner.apple_submit` | | +| `apple resubmit` | covered | Releases tab: Resubmit (owner action) | `owner.apple_resubmit` | | +| `apple offer-codes` | covered | More tools: owner steps (terminal command) | | creating campaigns and codes | +| `apple win-back` | covered | More tools: owner steps (terminal command) | | win-back offers go live on their start date | +| `apple promoted` | covered | More tools: owner steps (terminal command) | | promoted purchases on the product page | +| `apple preorder` | covered | More tools: Store features (status); enable / end as owner steps | `apple.preorder_status` | enable, end | +| `apple cpp` | covered | More tools: Store features (list, create, upload) | `apple.cpp_list`, `apple.cpp_create`, `apple.cpp_upload` | submit, visible | +| `apple events` | covered | More tools: Store features (list, create, upload) | `apple.events_list`, `apple.events_create`, `apple.events_upload` | submit | +| `apple listing` | covered | Listings tab (push, pull) | `apple.listing`, `apple.listing_pull` | | +| `apple screenshots` | covered | Listings tab: App Store screenshots | `apple.screenshots` | | +| `apple appinfo` | covered | App info tab: Push | `apple.appinfo` | | +| `apple review-details` | covered | App info tab: App Review details | `apple.review_details` | | +| `apple status` | covered | Releases tab: Send for review, App Store card | `apple.status` | | +| `apple creative-assets` | covered | More tools: Store features | `apple.creative_assets`, `apple.creative_assets_verify` | | +| `apple previews` | covered | Listings tab: App Store previews | `apple.previews` | | +| `apple release` | covered | Releases tab: phased release (owner action) | `owner.ios_phased` | | +| `apple signing` | covered | More tools: Build and upload (status, ensure) | `apple.signing_status`, `apple.signing_ensure` | | +| `apple build` | covered | More tools: Build and upload | `apple.build` | | +| `apple upload` | covered | More tools: Build and upload | `apple.upload` | | +| `apple adhoc` | covered | More tools: Ad-hoc devices (devices, register, options, export) | `apple.adhoc_devices`, `apple.adhoc_register`, `apple.adhoc_options`, `apple.adhoc_export` | | +| `apple next-build` | covered | More tools: Build and upload | `apple.next_build` | | +| `apple encryption` | covered | App info tab: export compliance | `apple.encryption` | | +| `apple ppo` | covered | Growth tab: PPO tests (start / stop = owner actions) | `apple.ppo_status`, `apple.ppo_create`, `apple.ppo_upload`, `owner.ppo_start`, `owner.ppo_stop` | | +| `apple testflight` | covered | TestFlight tab (status, groups, What to Test, distribute, testers = owner action); info and wait in More tools | `apple.testflight_status`, `apple.testflight_groups`, `apple.testflight_what_to_test`, `apple.testflight_distribute`, `owner.testflight_testers`, `apple.testflight_info`, `apple.testflight_wait` | expire | +| `icons` | covered | Store art tab: icons | `icons` | | +| `shots capture` | covered | More tools: Captures | `shots.capture` | | +| `shots login` | covered | More tools: owner steps (terminal command) | | signing in to a demo account by hand in a visible browser | +| `shots fonts` | covered | More tools: Captures | `shots.fonts` | | +| `shots render` | covered | Store art tab: Render | `shots.render` | | +| `shots jpeg` | covered | More tools: Captures | `shots.jpeg` | | +| `shots check` | covered | More tools: Captures | `shots.check` | | +| `shots record` | covered | More tools: Captures | `shots.record` | | +| `shots video` | covered | Store art tab: previews | `shots.video` | | +| `shots gallery` | covered | Store art tab: gallery, review page | `shots.gallery`, `project.renders` | | diff --git a/tests/desktop-coverage.test.ts b/tests/desktop-coverage.test.ts new file mode 100644 index 0000000..7c4b686 --- /dev/null +++ b/tests/desktop-coverage.test.ts @@ -0,0 +1,44 @@ +// Every CLI command is reachable in the desktop app, or listed as not for the desktop with a reason +// (cli/lib/desktop-coverage.ts; the same analysis writes docs/desktop-coverage.md). +import { describe, expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { DESKTOP_COVERAGE, OWNER_STEPS, TOOLBOX } from '../cli/lib/desktop-coverage'; +import { describeMethods } from '../cli/serve'; +import { analyze, render } from '../tools/gen-desktop-coverage'; + +/** The only commands that make no sense in a window. Adding one here needs a reason in the coverage map. */ +const NOT_FOR_DESKTOP = ['mcp', 'serve', 'desktop', 'api']; + +describe('desktop coverage', () => { + const { rows, extra } = analyze(); + test('every CLI command has a desktop place (no gaps)', () => { + expect(rows.filter(r => r.status === 'gap').map(r => `${r.command}: ${r.problems.join('; ')}`)).toEqual([]); + expect(extra).toEqual([]); + }); + test('not-for-desktop is short and each has a reason', () => { + const not = rows.filter(r => r.status === 'not-for-desktop'); + expect(not.map(r => r.command).sort()).toEqual([...NOT_FOR_DESKTOP].sort()); + for (const r of not) expect(r.where.length).toBeGreaterThan(20); + }); + test('the More tools tab offers no owner action and no unknown method', () => { + const ms = new Map(describeMethods().map(m => [m.method, m] as const)); + for (const m of TOOLBOX.flatMap(g => g.methods)) { + expect(ms.has(m)).toBe(true); + expect(['read', 'write']).toContain(ms.get(m)!.kind); + } + for (const s of OWNER_STEPS) expect(s.command.startsWith('storebound ')).toBe(true); + for (const [, c] of Object.entries(DESKTOP_COVERAGE)) if ('owner' in c && c.owner) expect(c.owner.length).toBeGreaterThan(2); + }); + test('docs/desktop-coverage.md is generated and current', () => { + expect(readFileSync(join(import.meta.dir, '../docs/desktop-coverage.md'), 'utf8')).toBe(render(rows)); + }); + test('every More tools group and owner step has en and tr strings', () => { + const en = readFileSync(join(import.meta.dir, '../apps/desktop/src/i18n/en.ts'), 'utf8'); + const tr = readFileSync(join(import.meta.dir, '../apps/desktop/src/i18n/tr.ts'), 'utf8'); + for (const k of [...TOOLBOX.map(g => `tools.g.${g.id}`), ...OWNER_STEPS.map(s => `tools.o.${s.id}`)]) { + expect(en).toContain(`'${k}'`); + expect(tr).toContain(`'${k}'`); + } + }); +}); diff --git a/tools/gen-desktop-coverage.ts b/tools/gen-desktop-coverage.ts new file mode 100644 index 0000000..3ee1317 --- /dev/null +++ b/tools/gen-desktop-coverage.ts @@ -0,0 +1,86 @@ +// docs/desktop-coverage.md from cli/lib/desktop-coverage.ts, checked against the code: a command is "covered" only +// when every method it names exists (serve.ts describeMethods(), or a #[tauri::command] of src-tauri) and the window +// calls it (the method quoted in apps/desktop/src, or listed in TOOLBOX, which the More tools tab renders). +// `bun tools/gen-desktop-coverage.ts` writes the page; `--check` exits 1 when it is stale. tests/desktop-coverage.test.ts +// runs analyze() too. +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { COMMANDS, PLAIN } from '../cli/commands'; +import { DESKTOP_COVERAGE, OWNER_STEPS, TOOLBOX } from '../cli/lib/desktop-coverage'; +import { describeMethods } from '../cli/serve'; + +const ROOT = join(import.meta.dir, '..'); +const OUT = join(ROOT, 'docs/desktop-coverage.md'); + +function files(dir: string, out: string[] = []): string[] { + for (const f of readdirSync(dir)) { + const p = join(dir, f); + if (statSync(p).isDirectory()) { if (f !== 'dev' && f !== 'i18n') files(p, out); } else if (/\.(ts|tsx|rs)$/.test(f)) out.push(p); + } + return out; +} + +export interface Row { command: string; status: 'covered' | 'gap' | 'not-for-desktop'; where: string; methods: string[]; owner?: string; problems: string[] } + +export function analyze(): { rows: Row[]; extra: string[] } { + const src = files(join(ROOT, 'apps/desktop/src')).map(f => readFileSync(f, 'utf8')).join('\n'); + const rust = files(join(ROOT, 'apps/desktop/src-tauri/src')).map(f => readFileSync(f, 'utf8')).join('\n'); + const hostCmds = new Set([...rust.matchAll(/#\[tauri::command\][\s\S]{0,200}?fn ([a-z_]+)/g)].map(m => m[1]!)); + const methods = new Set(describeMethods().map(m => m.method)); + const toolboxShown = /\bTOOLBOX\b/.test(src); + const inToolbox = new Set(TOOLBOX.flatMap(g => g.methods)); + const reachable = (m: string): string | null => { + if (m.startsWith('host:')) { + const c = m.slice(5); + if (!hostCmds.has(c)) return `${m}: no such tauri command`; + return src.includes(`'${c}'`) ? null : `${m}: never invoked by the window`; + } + if (!methods.has(m)) return `${m}: no such serve method`; + if (src.includes(`'${m}'`) || src.includes(`"${m}"`)) return null; + if (toolboxShown && inToolbox.has(m)) return null; + return `${m}: no window place calls it`; + }; + const all = [...Object.keys(PLAIN), ...Object.keys(COMMANDS)]; + const rows: Row[] = all.map(command => { + const c = DESKTOP_COVERAGE[command]; + if (!c) return { command, status: 'gap', where: '', methods: [], problems: ['no entry in cli/lib/desktop-coverage.ts'] }; + if ('not' in c) return { command, status: 'not-for-desktop', where: c.not, methods: [], problems: c.not.trim() ? [] : ['not-for-desktop without a reason'] }; + const problems = c.methods.map(reachable).filter((p): p is string => !!p); + if (!c.methods.length && !(c.owner && toolboxShown && OWNER_STEPS.length)) problems.push('no method and no owner step'); + return { command, status: problems.length ? 'gap' : 'covered', where: c.where, methods: c.methods, ...(c.owner ? { owner: c.owner } : {}), problems }; + }); + return { rows, extra: Object.keys(DESKTOP_COVERAGE).filter(k => !all.includes(k)) }; +} + +export function render(rows: Row[]): string { + const n = (s: Row['status']): number => rows.filter(r => r.status === s).length; + const cell = (s: string): string => s.replace(/\|/g, '\\|'); + return [ + '# Desktop coverage', + '', + 'Generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`; do not edit by hand.', + 'Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered" means each method listed exists', + '(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap.', + 'Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools).', + '', + `**${rows.length} commands:** ${n('covered')} covered, ${n('gap')} gaps, ${n('not-for-desktop')} not for the desktop.`, + '', + '| command | status | where in the window | methods | owner step |', + '|---|---|---|---|---|', + ...rows.map(r => `| \`${r.command}\` | ${r.status} | ${cell(r.where || r.problems.join('; '))} | ${r.methods.map(m => `\`${m}\``).join(', ')} | ${cell(r.owner ?? '')} |`), + '', + ].join('\n'); +} + +if (import.meta.main) { + const { rows } = analyze(); + const md = render(rows); + if (process.argv.includes('--check')) { + let cur = ''; + try { cur = readFileSync(OUT, 'utf8'); } catch { /* missing */ } + if (cur !== md) { console.error('docs/desktop-coverage.md is stale: run bun tools/gen-desktop-coverage.ts'); process.exit(1); } + } else writeFileSync(OUT, md); + const gaps = rows.filter(r => r.status === 'gap'); + for (const g of gaps) console.log(`gap: ${g.command}: ${g.problems.join('; ')}`); + console.log(`${rows.length} commands, ${rows.filter(r => r.status === 'covered').length} covered, ${gaps.length} gaps`); +}