@niro53/store-tools 0.1.0
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/bin/cli.js +17 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +207 -0
- package/dist/config.d.ts +234 -0
- package/dist/config.js +71 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.js +85 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.js +122 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.js +109 -0
- package/package.json +50 -0
- package/templates/helpers.js +105 -0
- package/templates/styles.css +1243 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const distEntry = join(__dirname, '..', 'dist', 'cli.js');
|
|
8
|
+
const srcEntry = join(__dirname, '..', 'src', 'cli.ts');
|
|
9
|
+
|
|
10
|
+
if (existsSync(distEntry)) {
|
|
11
|
+
await import(distEntry);
|
|
12
|
+
} else if (existsSync(srcEntry)) {
|
|
13
|
+
await import(srcEntry);
|
|
14
|
+
} else {
|
|
15
|
+
console.error('✗ @niro/store-tools: no compiled dist/cli.js and no src/cli.ts. Run `pnpm build` first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
// ── Resolve monorepo root + app dir from slug ─────────────────────────────────
|
|
8
|
+
function findMonorepoRoot(start) {
|
|
9
|
+
let dir = start;
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = readFileSync(join(dir, 'package.json'), 'utf-8');
|
|
13
|
+
const pkg = JSON.parse(raw);
|
|
14
|
+
if (pkg.workspaces)
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
catch { /* keep walking up */ }
|
|
18
|
+
const parent = resolve(dir, '..');
|
|
19
|
+
if (parent === dir)
|
|
20
|
+
break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
return start;
|
|
24
|
+
}
|
|
25
|
+
async function loadConfig(appDir) {
|
|
26
|
+
const cfgPath = join(appDir, 'store.config.ts');
|
|
27
|
+
try {
|
|
28
|
+
const mod = await import(pathToFileURL(cfgPath).href);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
30
|
+
return (mod.default ?? mod);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error(`✗ Could not load ${cfgPath}: ${err.message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function resolveAppDir(opts) {
|
|
38
|
+
// 1) Explicit --app-dir <path> wins (standalone consumers outside the monorepo).
|
|
39
|
+
if (opts.appDir) {
|
|
40
|
+
const dir = resolve(process.cwd(), opts.appDir);
|
|
41
|
+
try {
|
|
42
|
+
await readFile(join(dir, 'store.config.ts'), 'utf-8');
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error(`✗ No store.config.ts found at ${dir}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 2) --app <slug>: monorepo lookup (apps/<slug>).
|
|
51
|
+
if (opts.app) {
|
|
52
|
+
const root = findMonorepoRoot(process.cwd());
|
|
53
|
+
const appDir = join(root, 'apps', opts.app);
|
|
54
|
+
try {
|
|
55
|
+
await readFile(join(appDir, 'store.config.ts'), 'utf-8');
|
|
56
|
+
return appDir;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error(`✗ No store.config.ts found at ${appDir}`);
|
|
60
|
+
console.error(` Run "pnpm store validate --app ${opts.app}" to see errors.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 3) Fallback: walk up from cwd looking for store.config.ts (standalone repos).
|
|
65
|
+
let dir = process.cwd();
|
|
66
|
+
for (let i = 0; i < 8; i++) {
|
|
67
|
+
try {
|
|
68
|
+
await readFile(join(dir, 'store.config.ts'), 'utf-8');
|
|
69
|
+
return dir;
|
|
70
|
+
}
|
|
71
|
+
catch { /* keep walking */ }
|
|
72
|
+
const parent = resolve(dir, '..');
|
|
73
|
+
if (parent === dir)
|
|
74
|
+
break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
console.error('✗ No store.config.ts found. Pass --app <slug> (monorepo) or --app-dir <path> (standalone).');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
81
|
+
const program = new Command('niro-store').version('0.0.1');
|
|
82
|
+
program
|
|
83
|
+
.command('render')
|
|
84
|
+
.description('Render PNG screenshots for App Store and/or Play Store')
|
|
85
|
+
.option('--app <slug>', 'App slug (maps to apps/<slug> in monorepo)')
|
|
86
|
+
.option('--app-dir <path>', 'App directory (standalone consumers; defaults to cwd)')
|
|
87
|
+
.option('--locale <locales>', 'Comma-separated locales (default: all in config)')
|
|
88
|
+
.option('--scene <scenes>', 'Comma-separated scene out-names (default: all)')
|
|
89
|
+
.option('--platform <platforms>', 'appstore,play (default: both)')
|
|
90
|
+
.action(async (opts) => {
|
|
91
|
+
const { renderScreenshots } = await import('./render.js');
|
|
92
|
+
const appDir = await resolveAppDir({ ...(opts.app ? { app: opts.app } : {}), ...(opts.appDir ? { appDir: opts.appDir } : {}) });
|
|
93
|
+
const config = await loadConfig(appDir);
|
|
94
|
+
await renderScreenshots({
|
|
95
|
+
appDir,
|
|
96
|
+
config,
|
|
97
|
+
...(opts.locale ? { locales: opts.locale.split(',').map((s) => s.trim()) } : {}),
|
|
98
|
+
...(opts.scene ? { scenes: opts.scene.split(',').map((s) => s.trim()) } : {}),
|
|
99
|
+
...(opts.platform ? { platforms: opts.platform.split(',').map((s) => s.trim()) } : {}),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
program
|
|
103
|
+
.command('metadata')
|
|
104
|
+
.description('Sync store.config.ts → fastlane/metadata/*/ (title, desc, keywords …)')
|
|
105
|
+
.option('--app <slug>', 'App slug (monorepo)')
|
|
106
|
+
.option('--app-dir <path>', 'App directory (standalone; defaults to cwd)')
|
|
107
|
+
.option('--platform <platforms>', 'appstore,play (default: both)')
|
|
108
|
+
.action(async (opts) => {
|
|
109
|
+
const { syncMetadata } = await import('./metadata.js');
|
|
110
|
+
const appDir = await resolveAppDir({ ...(opts.app ? { app: opts.app } : {}), ...(opts.appDir ? { appDir: opts.appDir } : {}) });
|
|
111
|
+
const config = await loadConfig(appDir);
|
|
112
|
+
await syncMetadata({
|
|
113
|
+
appDir,
|
|
114
|
+
config,
|
|
115
|
+
...(opts.platform ? { platforms: opts.platform.split(',').map((s) => s.trim()) } : {}),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command('validate')
|
|
120
|
+
.description('Validate store.config.ts (length limits, locale completeness, scene files)')
|
|
121
|
+
.option('--app <slug>', 'Validate a single app by slug (monorepo)')
|
|
122
|
+
.option('--app-dir <path>', 'Validate an app directory (standalone; defaults to cwd)')
|
|
123
|
+
.option('--all', 'Validate all apps in apps/ (monorepo only)')
|
|
124
|
+
.option('--strict', 'Exit 1 even for warnings (missing optional fields)')
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
const { validateConfig, validateAll } = await import('./validate.js');
|
|
127
|
+
const root = findMonorepoRoot(process.cwd());
|
|
128
|
+
if (opts.all) {
|
|
129
|
+
const { readdir } = await import('node:fs/promises');
|
|
130
|
+
const appsDir = join(root, 'apps');
|
|
131
|
+
const { access: fsAccess } = await import('node:fs/promises');
|
|
132
|
+
const slugs = await readdir(appsDir).catch(() => []);
|
|
133
|
+
const entries = [];
|
|
134
|
+
for (const slug of slugs) {
|
|
135
|
+
const appDir = join(appsDir, slug);
|
|
136
|
+
const cfgPath = join(appDir, 'store.config.ts');
|
|
137
|
+
try {
|
|
138
|
+
await fsAccess(cfgPath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const config = await loadConfig(appDir);
|
|
145
|
+
entries.push({ appDir, config });
|
|
146
|
+
}
|
|
147
|
+
catch { /* malformed config — loadConfig already printed the error */ }
|
|
148
|
+
}
|
|
149
|
+
const ok = await validateAll(entries, { checkSceneFiles: true });
|
|
150
|
+
if (!ok)
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const appDir = await resolveAppDir({ ...(opts.app ? { app: opts.app } : {}), ...(opts.appDir ? { appDir: opts.appDir } : {}) });
|
|
155
|
+
const config = await loadConfig(appDir);
|
|
156
|
+
const result = await validateConfig(appDir, config, { checkSceneFiles: true });
|
|
157
|
+
for (const err of result.errors) {
|
|
158
|
+
const isHard = !['shortDescription', 'subtitle', 'releaseNotes', 'appleId', 'itcTeamId'].some((k) => err.field.includes(k));
|
|
159
|
+
console.log(`${isHard ? ' ✗' : ' ⚠'} ${err.field}: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
if (!result.ok || (opts.strict && result.errors.length > 0))
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
program
|
|
166
|
+
.command('upload <platform>')
|
|
167
|
+
.description('Upload to App Store (ios) or Play Store (android)')
|
|
168
|
+
.option('--app <slug>', 'App slug (monorepo)')
|
|
169
|
+
.option('--app-dir <path>', 'App directory (standalone; defaults to cwd)')
|
|
170
|
+
.option('--track <track>', 'internal | alpha | beta | production (Play) or beta | production (iOS)', 'internal')
|
|
171
|
+
.action(async (platform, opts) => {
|
|
172
|
+
const { execSync } = await import('node:child_process');
|
|
173
|
+
const appDir = await resolveAppDir({ ...(opts.app ? { app: opts.app } : {}), ...(opts.appDir ? { appDir: opts.appDir } : {}) });
|
|
174
|
+
const config = await loadConfig(appDir);
|
|
175
|
+
const { syncMetadata } = await import('./metadata.js');
|
|
176
|
+
const { renderScreenshots } = await import('./render.js');
|
|
177
|
+
await syncMetadata({ appDir, config });
|
|
178
|
+
await renderScreenshots({ appDir, config });
|
|
179
|
+
const fastlaneDir = join(appDir, platform === 'ios' ? 'ios/App' : 'android');
|
|
180
|
+
const lane = opts.track === 'production' ? 'release' : 'beta';
|
|
181
|
+
const envString = `APP_SLUG=${config.slug} APP_IDENTIFIER=${config.bundleId}`;
|
|
182
|
+
console.log(`\n→ bundle exec fastlane ${platform === 'ios' ? 'ios' : 'android'} ${lane}\n`);
|
|
183
|
+
execSync(`${envString} bundle exec fastlane ${platform === 'ios' ? 'ios' : 'android'} ${lane}`, { cwd: fastlaneDir, stdio: 'inherit' });
|
|
184
|
+
});
|
|
185
|
+
program
|
|
186
|
+
.command('promote')
|
|
187
|
+
.description('Promote an existing Play Store track to a higher track')
|
|
188
|
+
.option('--app <slug>', 'App slug (monorepo)')
|
|
189
|
+
.option('--app-dir <path>', 'App directory (standalone; defaults to cwd)')
|
|
190
|
+
.option('--from <track>', 'Source track', 'internal')
|
|
191
|
+
.option('--to <track>', 'Target track', 'production')
|
|
192
|
+
.option('--rollout <pct>', 'Rollout fraction (0.0–1.0)', '0.1')
|
|
193
|
+
.action(async (opts) => {
|
|
194
|
+
const { execSync } = await import('node:child_process');
|
|
195
|
+
const appDir = await resolveAppDir({ ...(opts.app ? { app: opts.app } : {}), ...(opts.appDir ? { appDir: opts.appDir } : {}) });
|
|
196
|
+
const config = await loadConfig(appDir);
|
|
197
|
+
const fastlaneDir = join(appDir, 'android');
|
|
198
|
+
const envString = [
|
|
199
|
+
`APP_SLUG=${config.slug}`,
|
|
200
|
+
`APP_IDENTIFIER=${config.bundleId}`,
|
|
201
|
+
`PROMOTE_FROM=${opts.from}`,
|
|
202
|
+
`PROMOTE_TO=${opts.to}`,
|
|
203
|
+
`PROMOTE_ROLLOUT=${opts.rollout}`,
|
|
204
|
+
].join(' ');
|
|
205
|
+
execSync(`${envString} bundle exec fastlane android promote`, { cwd: fastlaneDir, stdio: 'inherit' });
|
|
206
|
+
});
|
|
207
|
+
program.parse(process.argv);
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export declare const SceneSchema: z.ZodObject<{
|
|
3
|
+
html: z.ZodString;
|
|
4
|
+
out: z.ZodString;
|
|
5
|
+
kind: z.ZodDefault<z.ZodEnum<["screen", "feature"]>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
html: string;
|
|
8
|
+
out: string;
|
|
9
|
+
kind: "screen" | "feature";
|
|
10
|
+
}, {
|
|
11
|
+
html: string;
|
|
12
|
+
out: string;
|
|
13
|
+
kind?: "screen" | "feature" | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type Scene = z.infer<typeof SceneSchema>;
|
|
16
|
+
export declare const LocaleMetaSchema: z.ZodObject<{
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
description: z.ZodString;
|
|
19
|
+
releaseNotes: z.ZodOptional<z.ZodString>;
|
|
20
|
+
privacyUrl: z.ZodString;
|
|
21
|
+
supportUrl: z.ZodString;
|
|
22
|
+
subtitle: z.ZodOptional<z.ZodString>;
|
|
23
|
+
keywords: z.ZodOptional<z.ZodString>;
|
|
24
|
+
promotionalText: z.ZodOptional<z.ZodString>;
|
|
25
|
+
shortDescription: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
privacyUrl: string;
|
|
30
|
+
supportUrl: string;
|
|
31
|
+
releaseNotes?: string | undefined;
|
|
32
|
+
subtitle?: string | undefined;
|
|
33
|
+
keywords?: string | undefined;
|
|
34
|
+
promotionalText?: string | undefined;
|
|
35
|
+
shortDescription?: string | undefined;
|
|
36
|
+
}, {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
privacyUrl: string;
|
|
40
|
+
supportUrl: string;
|
|
41
|
+
releaseNotes?: string | undefined;
|
|
42
|
+
subtitle?: string | undefined;
|
|
43
|
+
keywords?: string | undefined;
|
|
44
|
+
promotionalText?: string | undefined;
|
|
45
|
+
shortDescription?: string | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
export type LocaleMeta = z.infer<typeof LocaleMetaSchema>;
|
|
48
|
+
export declare const AppStoreConfigSchema: z.ZodOptional<z.ZodObject<{
|
|
49
|
+
primaryCategory: z.ZodOptional<z.ZodString>;
|
|
50
|
+
secondaryCategory: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
primaryCategory?: string | undefined;
|
|
53
|
+
secondaryCategory?: string | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
primaryCategory?: string | undefined;
|
|
56
|
+
secondaryCategory?: string | undefined;
|
|
57
|
+
}>>;
|
|
58
|
+
export declare const PlayConfigSchema: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
category: z.ZodOptional<z.ZodString>;
|
|
60
|
+
contentRating: z.ZodDefault<z.ZodEnum<["EVERYONE", "EVERYONE_10_PLUS", "TEEN", "MATURE_17_PLUS"]>>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
contentRating: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS";
|
|
63
|
+
category?: string | undefined;
|
|
64
|
+
}, {
|
|
65
|
+
category?: string | undefined;
|
|
66
|
+
contentRating?: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS" | undefined;
|
|
67
|
+
}>>;
|
|
68
|
+
export declare const StoreConfigSchema: z.ZodObject<{
|
|
69
|
+
slug: z.ZodString;
|
|
70
|
+
bundleId: z.ZodString;
|
|
71
|
+
appleId: z.ZodOptional<z.ZodString>;
|
|
72
|
+
itcTeamId: z.ZodOptional<z.ZodString>;
|
|
73
|
+
locales: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
74
|
+
name: z.ZodString;
|
|
75
|
+
description: z.ZodString;
|
|
76
|
+
releaseNotes: z.ZodOptional<z.ZodString>;
|
|
77
|
+
privacyUrl: z.ZodString;
|
|
78
|
+
supportUrl: z.ZodString;
|
|
79
|
+
subtitle: z.ZodOptional<z.ZodString>;
|
|
80
|
+
keywords: z.ZodOptional<z.ZodString>;
|
|
81
|
+
promotionalText: z.ZodOptional<z.ZodString>;
|
|
82
|
+
shortDescription: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
privacyUrl: string;
|
|
87
|
+
supportUrl: string;
|
|
88
|
+
releaseNotes?: string | undefined;
|
|
89
|
+
subtitle?: string | undefined;
|
|
90
|
+
keywords?: string | undefined;
|
|
91
|
+
promotionalText?: string | undefined;
|
|
92
|
+
shortDescription?: string | undefined;
|
|
93
|
+
}, {
|
|
94
|
+
name: string;
|
|
95
|
+
description: string;
|
|
96
|
+
privacyUrl: string;
|
|
97
|
+
supportUrl: string;
|
|
98
|
+
releaseNotes?: string | undefined;
|
|
99
|
+
subtitle?: string | undefined;
|
|
100
|
+
keywords?: string | undefined;
|
|
101
|
+
promotionalText?: string | undefined;
|
|
102
|
+
shortDescription?: string | undefined;
|
|
103
|
+
}>>, Record<string, {
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
privacyUrl: string;
|
|
107
|
+
supportUrl: string;
|
|
108
|
+
releaseNotes?: string | undefined;
|
|
109
|
+
subtitle?: string | undefined;
|
|
110
|
+
keywords?: string | undefined;
|
|
111
|
+
promotionalText?: string | undefined;
|
|
112
|
+
shortDescription?: string | undefined;
|
|
113
|
+
}>, Record<string, {
|
|
114
|
+
name: string;
|
|
115
|
+
description: string;
|
|
116
|
+
privacyUrl: string;
|
|
117
|
+
supportUrl: string;
|
|
118
|
+
releaseNotes?: string | undefined;
|
|
119
|
+
subtitle?: string | undefined;
|
|
120
|
+
keywords?: string | undefined;
|
|
121
|
+
promotionalText?: string | undefined;
|
|
122
|
+
shortDescription?: string | undefined;
|
|
123
|
+
}>>;
|
|
124
|
+
scenes: z.ZodEffects<z.ZodArray<z.ZodObject<{
|
|
125
|
+
html: z.ZodString;
|
|
126
|
+
out: z.ZodString;
|
|
127
|
+
kind: z.ZodDefault<z.ZodEnum<["screen", "feature"]>>;
|
|
128
|
+
}, "strip", z.ZodTypeAny, {
|
|
129
|
+
html: string;
|
|
130
|
+
out: string;
|
|
131
|
+
kind: "screen" | "feature";
|
|
132
|
+
}, {
|
|
133
|
+
html: string;
|
|
134
|
+
out: string;
|
|
135
|
+
kind?: "screen" | "feature" | undefined;
|
|
136
|
+
}>, "many">, {
|
|
137
|
+
html: string;
|
|
138
|
+
out: string;
|
|
139
|
+
kind: "screen" | "feature";
|
|
140
|
+
}[], {
|
|
141
|
+
html: string;
|
|
142
|
+
out: string;
|
|
143
|
+
kind?: "screen" | "feature" | undefined;
|
|
144
|
+
}[]>;
|
|
145
|
+
appstore: z.ZodOptional<z.ZodObject<{
|
|
146
|
+
primaryCategory: z.ZodOptional<z.ZodString>;
|
|
147
|
+
secondaryCategory: z.ZodOptional<z.ZodString>;
|
|
148
|
+
}, "strip", z.ZodTypeAny, {
|
|
149
|
+
primaryCategory?: string | undefined;
|
|
150
|
+
secondaryCategory?: string | undefined;
|
|
151
|
+
}, {
|
|
152
|
+
primaryCategory?: string | undefined;
|
|
153
|
+
secondaryCategory?: string | undefined;
|
|
154
|
+
}>>;
|
|
155
|
+
play: z.ZodOptional<z.ZodObject<{
|
|
156
|
+
category: z.ZodOptional<z.ZodString>;
|
|
157
|
+
contentRating: z.ZodDefault<z.ZodEnum<["EVERYONE", "EVERYONE_10_PLUS", "TEEN", "MATURE_17_PLUS"]>>;
|
|
158
|
+
}, "strip", z.ZodTypeAny, {
|
|
159
|
+
contentRating: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS";
|
|
160
|
+
category?: string | undefined;
|
|
161
|
+
}, {
|
|
162
|
+
category?: string | undefined;
|
|
163
|
+
contentRating?: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS" | undefined;
|
|
164
|
+
}>>;
|
|
165
|
+
}, "strip", z.ZodTypeAny, {
|
|
166
|
+
slug: string;
|
|
167
|
+
bundleId: string;
|
|
168
|
+
locales: Record<string, {
|
|
169
|
+
name: string;
|
|
170
|
+
description: string;
|
|
171
|
+
privacyUrl: string;
|
|
172
|
+
supportUrl: string;
|
|
173
|
+
releaseNotes?: string | undefined;
|
|
174
|
+
subtitle?: string | undefined;
|
|
175
|
+
keywords?: string | undefined;
|
|
176
|
+
promotionalText?: string | undefined;
|
|
177
|
+
shortDescription?: string | undefined;
|
|
178
|
+
}>;
|
|
179
|
+
scenes: {
|
|
180
|
+
html: string;
|
|
181
|
+
out: string;
|
|
182
|
+
kind: "screen" | "feature";
|
|
183
|
+
}[];
|
|
184
|
+
appleId?: string | undefined;
|
|
185
|
+
itcTeamId?: string | undefined;
|
|
186
|
+
appstore?: {
|
|
187
|
+
primaryCategory?: string | undefined;
|
|
188
|
+
secondaryCategory?: string | undefined;
|
|
189
|
+
} | undefined;
|
|
190
|
+
play?: {
|
|
191
|
+
contentRating: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS";
|
|
192
|
+
category?: string | undefined;
|
|
193
|
+
} | undefined;
|
|
194
|
+
}, {
|
|
195
|
+
slug: string;
|
|
196
|
+
bundleId: string;
|
|
197
|
+
locales: Record<string, {
|
|
198
|
+
name: string;
|
|
199
|
+
description: string;
|
|
200
|
+
privacyUrl: string;
|
|
201
|
+
supportUrl: string;
|
|
202
|
+
releaseNotes?: string | undefined;
|
|
203
|
+
subtitle?: string | undefined;
|
|
204
|
+
keywords?: string | undefined;
|
|
205
|
+
promotionalText?: string | undefined;
|
|
206
|
+
shortDescription?: string | undefined;
|
|
207
|
+
}>;
|
|
208
|
+
scenes: {
|
|
209
|
+
html: string;
|
|
210
|
+
out: string;
|
|
211
|
+
kind?: "screen" | "feature" | undefined;
|
|
212
|
+
}[];
|
|
213
|
+
appleId?: string | undefined;
|
|
214
|
+
itcTeamId?: string | undefined;
|
|
215
|
+
appstore?: {
|
|
216
|
+
primaryCategory?: string | undefined;
|
|
217
|
+
secondaryCategory?: string | undefined;
|
|
218
|
+
} | undefined;
|
|
219
|
+
play?: {
|
|
220
|
+
category?: string | undefined;
|
|
221
|
+
contentRating?: "EVERYONE" | "EVERYONE_10_PLUS" | "TEEN" | "MATURE_17_PLUS" | undefined;
|
|
222
|
+
} | undefined;
|
|
223
|
+
}>;
|
|
224
|
+
export type StoreConfig = z.infer<typeof StoreConfigSchema>;
|
|
225
|
+
/**
|
|
226
|
+
* Typed identity helper — validates at call-site (Zod parse) and infers the
|
|
227
|
+
* StoreConfig type so IDEs surface field completion and inline errors.
|
|
228
|
+
*
|
|
229
|
+
* Usage (apps/<slug>/store.config.ts):
|
|
230
|
+
*
|
|
231
|
+
* import { defineStoreConfig } from '@niro/store-tools';
|
|
232
|
+
* export default defineStoreConfig({ … });
|
|
233
|
+
*/
|
|
234
|
+
export declare function defineStoreConfig(raw: unknown): StoreConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
// ── Locale string limits (Apple / Google Play maximums) ──────────────────────
|
|
3
|
+
const appStoreLimits = {
|
|
4
|
+
name: 30,
|
|
5
|
+
subtitle: 30,
|
|
6
|
+
keywords: 100,
|
|
7
|
+
promotionalText: 170,
|
|
8
|
+
description: 4000,
|
|
9
|
+
releaseNotes: 4000,
|
|
10
|
+
};
|
|
11
|
+
const playLimits = {
|
|
12
|
+
name: 50,
|
|
13
|
+
shortDescription: 80,
|
|
14
|
+
description: 4000,
|
|
15
|
+
releaseNotes: 500,
|
|
16
|
+
};
|
|
17
|
+
// ── Scene definition ──────────────────────────────────────────────────────────
|
|
18
|
+
export const SceneSchema = z.object({
|
|
19
|
+
html: z.string().endsWith('.html'),
|
|
20
|
+
out: z.string().min(1),
|
|
21
|
+
kind: z.enum(['screen', 'feature']).default('screen'),
|
|
22
|
+
});
|
|
23
|
+
// ── Per-locale metadata ───────────────────────────────────────────────────────
|
|
24
|
+
export const LocaleMetaSchema = z.object({
|
|
25
|
+
// Shared App Store + Play Store
|
|
26
|
+
name: z.string().max(appStoreLimits.name),
|
|
27
|
+
description: z.string().max(appStoreLimits.description),
|
|
28
|
+
releaseNotes: z.string().max(appStoreLimits.description).optional(),
|
|
29
|
+
privacyUrl: z.string().url(),
|
|
30
|
+
supportUrl: z.string().url(),
|
|
31
|
+
// App Store only
|
|
32
|
+
subtitle: z.string().max(appStoreLimits.subtitle).optional(),
|
|
33
|
+
keywords: z.string().max(appStoreLimits.keywords).optional(),
|
|
34
|
+
promotionalText: z.string().max(appStoreLimits.promotionalText).optional(),
|
|
35
|
+
// Play Store only
|
|
36
|
+
shortDescription: z.string().max(playLimits.shortDescription).optional(),
|
|
37
|
+
});
|
|
38
|
+
// ── Platform-specific overrides ───────────────────────────────────────────────
|
|
39
|
+
export const AppStoreConfigSchema = z.object({
|
|
40
|
+
primaryCategory: z.string().optional(),
|
|
41
|
+
secondaryCategory: z.string().optional(),
|
|
42
|
+
}).optional();
|
|
43
|
+
export const PlayConfigSchema = z.object({
|
|
44
|
+
category: z.string().optional(),
|
|
45
|
+
contentRating: z.enum(['EVERYONE', 'EVERYONE_10_PLUS', 'TEEN', 'MATURE_17_PLUS']).default('EVERYONE'),
|
|
46
|
+
}).optional();
|
|
47
|
+
// ── Top-level StoreConfig ─────────────────────────────────────────────────────
|
|
48
|
+
export const StoreConfigSchema = z.object({
|
|
49
|
+
slug: z.string().min(1).regex(/^[a-z0-9-]+$/, 'slug must be kebab-case'),
|
|
50
|
+
bundleId: z.string().min(1),
|
|
51
|
+
// Apple App Store Connect
|
|
52
|
+
appleId: z.string().optional(),
|
|
53
|
+
itcTeamId: z.string().optional(),
|
|
54
|
+
// Locales: at least one entry; each key is the internal locale code (en, cs, pl …)
|
|
55
|
+
locales: z.record(z.string().min(2).max(5), LocaleMetaSchema).refine((loc) => Object.keys(loc).length >= 1, 'at least one locale is required'),
|
|
56
|
+
scenes: z.array(SceneSchema).min(1).max(10).refine((scenes) => scenes.filter((s) => s.kind === 'feature').length <= 1, 'at most one feature-graphic scene allowed'),
|
|
57
|
+
appstore: AppStoreConfigSchema,
|
|
58
|
+
play: PlayConfigSchema,
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Typed identity helper — validates at call-site (Zod parse) and infers the
|
|
62
|
+
* StoreConfig type so IDEs surface field completion and inline errors.
|
|
63
|
+
*
|
|
64
|
+
* Usage (apps/<slug>/store.config.ts):
|
|
65
|
+
*
|
|
66
|
+
* import { defineStoreConfig } from '@niro/store-tools';
|
|
67
|
+
* export default defineStoreConfig({ … });
|
|
68
|
+
*/
|
|
69
|
+
export function defineStoreConfig(raw) {
|
|
70
|
+
return StoreConfigSchema.parse(raw);
|
|
71
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { defineStoreConfig, StoreConfigSchema } from './config.js';
|
|
2
|
+
export type { StoreConfig, LocaleMeta, Scene } from './config.js';
|
|
3
|
+
export { renderScreenshots } from './render.js';
|
|
4
|
+
export type { RenderOptions } from './render.js';
|
|
5
|
+
export { syncMetadata } from './metadata.js';
|
|
6
|
+
export type { MetadataOptions } from './metadata.js';
|
|
7
|
+
export { validateConfig, validateAll } from './validate.js';
|
|
8
|
+
export type { ValidationResult, ValidationError } from './validate.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StoreConfig } from './config.js';
|
|
2
|
+
export interface MetadataOptions {
|
|
3
|
+
/** Absolute path to the app directory (e.g. /path/to/apps/autoskola-ai) */
|
|
4
|
+
appDir: string;
|
|
5
|
+
config: StoreConfig;
|
|
6
|
+
platforms?: ('appstore' | 'play')[];
|
|
7
|
+
}
|
|
8
|
+
export declare function syncMetadata({ appDir, config, platforms, }: MetadataOptions): Promise<void>;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// ── Locale code maps ──────────────────────────────────────────────────────────
|
|
4
|
+
// internal → { appstore, play }
|
|
5
|
+
const LOCALE_MAP = {
|
|
6
|
+
en: { appstore: 'en-US', play: 'en-US' },
|
|
7
|
+
cs: { appstore: 'cs', play: 'cs-CZ' },
|
|
8
|
+
pl: { appstore: 'pl', play: 'pl-PL' },
|
|
9
|
+
de: { appstore: 'de-DE', play: 'de-DE' },
|
|
10
|
+
sk: { appstore: 'sk', play: 'sk' },
|
|
11
|
+
fr: { appstore: 'fr-FR', play: 'fr-FR' },
|
|
12
|
+
hu: { appstore: 'hu', play: 'hu' },
|
|
13
|
+
};
|
|
14
|
+
function toAppStoreLocale(internal) {
|
|
15
|
+
return LOCALE_MAP[internal]?.appstore ?? internal;
|
|
16
|
+
}
|
|
17
|
+
function toPlayLocale(internal) {
|
|
18
|
+
return LOCALE_MAP[internal]?.play ?? internal;
|
|
19
|
+
}
|
|
20
|
+
// ── Write helpers ─────────────────────────────────────────────────────────────
|
|
21
|
+
async function write(dir, filename, content) {
|
|
22
|
+
await mkdir(dir, { recursive: true });
|
|
23
|
+
await writeFile(join(dir, filename), content.trim() + '\n', 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
// ── App Store metadata writer ─────────────────────────────────────────────────
|
|
26
|
+
async function writeAppStoreMeta(appDir, internalLocale, meta) {
|
|
27
|
+
const dir = join(appDir, 'fastlane', 'metadata', toAppStoreLocale(internalLocale));
|
|
28
|
+
const tasks = [
|
|
29
|
+
write(dir, 'name.txt', meta.name),
|
|
30
|
+
write(dir, 'description.txt', meta.description),
|
|
31
|
+
write(dir, 'privacy_url.txt', meta.privacyUrl),
|
|
32
|
+
write(dir, 'support_url.txt', meta.supportUrl),
|
|
33
|
+
];
|
|
34
|
+
if (meta.subtitle)
|
|
35
|
+
tasks.push(write(dir, 'subtitle.txt', meta.subtitle));
|
|
36
|
+
if (meta.keywords)
|
|
37
|
+
tasks.push(write(dir, 'keywords.txt', meta.keywords));
|
|
38
|
+
if (meta.promotionalText)
|
|
39
|
+
tasks.push(write(dir, 'promotional_text.txt', meta.promotionalText));
|
|
40
|
+
if (meta.releaseNotes)
|
|
41
|
+
tasks.push(write(dir, 'release_notes.txt', meta.releaseNotes));
|
|
42
|
+
await Promise.all(tasks);
|
|
43
|
+
}
|
|
44
|
+
// ── Play Store metadata writer ────────────────────────────────────────────────
|
|
45
|
+
async function writePlayMeta(appDir, internalLocale, meta) {
|
|
46
|
+
const dir = join(appDir, 'fastlane', 'metadata', 'android', toPlayLocale(internalLocale));
|
|
47
|
+
const tasks = [
|
|
48
|
+
write(dir, 'title.txt', meta.name),
|
|
49
|
+
write(dir, 'full_description.txt', meta.description),
|
|
50
|
+
write(dir, 'privacy_url.txt', meta.privacyUrl),
|
|
51
|
+
];
|
|
52
|
+
if (meta.shortDescription)
|
|
53
|
+
tasks.push(write(dir, 'short_description.txt', meta.shortDescription));
|
|
54
|
+
if (meta.releaseNotes)
|
|
55
|
+
tasks.push(write(dir, 'changelogs/default.txt', meta.releaseNotes));
|
|
56
|
+
await Promise.all(tasks);
|
|
57
|
+
}
|
|
58
|
+
// ── Appfile writer ────────────────────────────────────────────────────────────
|
|
59
|
+
async function writeAppfile(appDir, config) {
|
|
60
|
+
const appfile = join(appDir, 'fastlane', 'Appfile');
|
|
61
|
+
const lines = [
|
|
62
|
+
`app_identifier("${config.bundleId}")`,
|
|
63
|
+
config.appleId ? `apple_id("${config.appleId}")` : '',
|
|
64
|
+
config.itcTeamId ? `itc_team_id("${config.itcTeamId}")` : '',
|
|
65
|
+
config.itcTeamId ? `team_id("${config.itcTeamId}")` : '',
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
await mkdir(join(appDir, 'fastlane'), { recursive: true });
|
|
68
|
+
await writeFile(appfile, lines.join('\n') + '\n', 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
export async function syncMetadata({ appDir, config, platforms = ['appstore', 'play'], }) {
|
|
71
|
+
const writes = [];
|
|
72
|
+
for (const [locale, meta] of Object.entries(config.locales)) {
|
|
73
|
+
if (platforms.includes('appstore')) {
|
|
74
|
+
writes.push(writeAppStoreMeta(appDir, locale, meta));
|
|
75
|
+
}
|
|
76
|
+
if (platforms.includes('play')) {
|
|
77
|
+
writes.push(writePlayMeta(appDir, locale, meta));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (platforms.includes('appstore')) {
|
|
81
|
+
writes.push(writeAppfile(appDir, config));
|
|
82
|
+
}
|
|
83
|
+
await Promise.all(writes);
|
|
84
|
+
console.log(`✓ metadata synced (${Object.keys(config.locales).join(', ')}) → ${appDir}/fastlane/`);
|
|
85
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StoreConfig } from './config.js';
|
|
2
|
+
export interface RenderOptions {
|
|
3
|
+
/** Absolute path to the app directory (e.g. /repo/apps/autoskola-ai) */
|
|
4
|
+
appDir: string;
|
|
5
|
+
config: StoreConfig;
|
|
6
|
+
/** If provided, only render these locales. Default: all locales in config. */
|
|
7
|
+
locales?: string[];
|
|
8
|
+
/** If provided, only render these scene slugs (out field). Default: all. */
|
|
9
|
+
scenes?: string[];
|
|
10
|
+
platforms?: ('appstore' | 'play')[];
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function renderScreenshots({ appDir, config, locales, scenes: sceneFilter, platforms, verbose, }: RenderOptions): Promise<void>;
|