// Copies the templates into the current project without overwriting anything, and makes sure .storebound/ is ignored. // storebound init [--dir .] [--profile app|game|subscription-app] [--dry] // Profiles (default app): app = utility / productivity app with an optional account and analytics, no ads; // game = free-to-play game with ads and in-app purchases (the templates storebound shipped before 0.5.0); // subscription-app = accounts plus auto-renewable subscriptions. Each profile has its own config, declarations, // App Privacy answers, Data safety section, listing copy and screenshot copy; every answer is a starting point to edit. // Lookup per file: templates/profiles//, then the profile it builds on, then templates/. import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import type { Args } from './lib/args'; import { type AppProfile, PROFILES } from './lib/config'; import { log } from './lib/log'; import { TEMPLATE_TEXT } from './lib/template-files'; import * as rt from './lib/rt'; const T = join(rt.packageRoot(), 'templates'); export const FILES: [string, string][] = [ ['storebound.config.ts', 'storebound.config.ts'], ['listing.md', 'store/listing.md'], ['appstore.md', 'store/appstore.md'], ['shots-copy.json', 'store/shots/copy.json'], ['app-privacy.md', 'store/app-privacy.md'], ['declarations.md', 'store/declarations.md'], ['privacy-policy.html', 'store/privacy-policy.html'], ['credentials.example.json', '.storebound/credentials.json'], ['testers.csv', '.storebound/testers.csv'], ['workflows/release.yml', 'store/workflows/release.yml'], ['workflows/beta.yml', 'store/workflows/beta.yml'], ]; /** Template folders a profile reads from (relative to templates/), most specific first. game = the root templates. */ const CHAIN: Record = { 'game': [''], 'app': ['profiles/app/', ''], 'subscription-app': ['profiles/subscription-app/', 'profiles/app/', ''], }; /** Key (path under templates/) of one template for a profile. Pure: looks it up in the embedded list. */ export function templateKey(profile: AppProfile, name: string): string { for (const dir of CHAIN[profile]) if (TEMPLATE_TEXT[dir + name] !== undefined) return dir + name; throw new Error(`template ${name} not found`); } /** Source file of one template for a profile (in the storebound folder; the compiled sidecar has none). */ export const templateFor = (profile: AppProfile, name: string): string => join(T, ...templateKey(profile, name).split('/')); /** Template text: the embedded copy (the same bytes as the file; tests/apps.test.ts checks it). */ export const templateText = (profile: AppProfile, name: string): string => TEMPLATE_TEXT[templateKey(profile, name)]!; export async function run(args: Args): Promise { const root = resolve(args.str('dir', '.')); const profile = args.str('profile', 'app') as AppProfile; if (!PROFILES.includes(profile)) throw new Error(`--profile ${profile}: one of ${PROFILES.join(', ')}`); const dry = args.has('dry'); log('init', `profile ${profile}${dry ? ' (dry run: nothing is written)' : ''} (templates are starting points: edit every answer to match your app)`); for (const [from, to] of FILES) { const dest = join(root, to); if (relative(root, dest).startsWith('..')) throw new Error(`template target outside the project: ${to}`); if (existsSync(dest)) { log('init', `keep ${to} (exists)`); continue; } if (dry) { log('init', `would create ${to}`); continue; } mkdirSync(dirname(dest), { recursive: true }); writeFileSync(dest, templateText(profile, from)); log('init', `create ${to}`); } const gi = join(root, '.gitignore'); const cur = existsSync(gi) ? readFileSync(gi, 'utf8') : ''; if (dry) { if (!/^\/?\.storebound\/?$/m.test(cur)) log('init', 'would add .storebound/ to .gitignore'); return; } if (!/^\/?\.storebound\/?$/m.test(cur)) { if (cur) appendFileSync(gi, `${cur.endsWith('\n') ? '' : '\n'}# storebound: credentials, fonts, temp files\n.storebound/\n`); else writeFileSync(gi, '# storebound: credentials, fonts, temp files\n.storebound/\n'); log('init', '.gitignore: added .storebound/'); } log('init', 'next: edit storebound.config.ts, store/*.md and .storebound/credentials.json, then `storebound check`; store/workflows/*.yml: storebound flow list'); }