storemeta 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/.env.release.example +8 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/auth/apple/credential-state.js +14 -0
- package/dist/auth/apple/jwt.js +56 -0
- package/dist/auth/apple/load-credentials.js +36 -0
- package/dist/auth/credential-state.js +6 -0
- package/dist/auth/google/credential-state.js +10 -0
- package/dist/auth/google/load-credentials.js +18 -0
- package/dist/auth/google/service-account-auth.js +25 -0
- package/dist/auth/redact.js +17 -0
- package/dist/cli/auth-check.js +46 -0
- package/dist/cli/config-doctor.js +40 -0
- package/dist/cli/context.js +23 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/exit-code.js +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/locales-list.js +37 -0
- package/dist/cli/metadata-diff.js +57 -0
- package/dist/cli/metadata-pull.js +52 -0
- package/dist/cli/metadata-push.js +95 -0
- package/dist/cli/render.js +57 -0
- package/dist/cli/result-types.js +1 -0
- package/dist/cli/scaffold.js +76 -0
- package/dist/cli/screenshots-diff.js +65 -0
- package/dist/cli/screenshots-pull.js +72 -0
- package/dist/cli/screenshots-push.js +96 -0
- package/dist/cli/validate.js +74 -0
- package/dist/cli.js +222 -0
- package/dist/config/apps.js +21 -0
- package/dist/config/load-config.js +30 -0
- package/dist/config/schema.js +97 -0
- package/dist/config/select-app.js +10 -0
- package/dist/config/select-platforms.js +17 -0
- package/dist/config/single-app.js +16 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate-base-dirs.js +33 -0
- package/dist/config/validate-credential-env-names.js +32 -0
- package/dist/config/validate-platform-identifiers.js +24 -0
- package/dist/formats/load-metadata.js +38 -0
- package/dist/formats/metadata-types.js +1 -0
- package/dist/formats/screenshot-types.js +1 -0
- package/dist/formats/serialize-metadata.js +20 -0
- package/dist/locales/groups.js +12 -0
- package/dist/locales/map.js +15 -0
- package/dist/locales/normalize.js +22 -0
- package/dist/locales/types.js +1 -0
- package/dist/locales/validate-groups.js +14 -0
- package/dist/platforms/apple/client.js +109 -0
- package/dist/platforms/apple/metadata/pull.js +120 -0
- package/dist/platforms/apple/metadata/push.js +272 -0
- package/dist/platforms/apple/metadata/types.js +1 -0
- package/dist/platforms/apple/screenshots/pull.js +160 -0
- package/dist/platforms/apple/screenshots/push.js +418 -0
- package/dist/platforms/google/client.js +42 -0
- package/dist/platforms/google/edits.js +38 -0
- package/dist/platforms/google/metadata/pull.js +23 -0
- package/dist/platforms/google/metadata/push.js +73 -0
- package/dist/platforms/google/metadata/types.js +1 -0
- package/dist/platforms/google/screenshots/pull.js +129 -0
- package/dist/platforms/google/screenshots/push.js +252 -0
- package/dist/platforms/google/screenshots/types.js +7 -0
- package/dist/validation/metadata/apple.js +31 -0
- package/dist/validation/metadata/files.js +58 -0
- package/dist/validation/metadata/google.js +46 -0
- package/dist/validation/screenshots/extensions.js +17 -0
- package/dist/validation/screenshots/files.js +55 -0
- package/dist/validation/screenshots/folders.js +41 -0
- package/dist/validation/screenshots/order.js +25 -0
- package/dist/writers/ensure-directory.js +16 -0
- package/dist/writers/order-screenshots.js +19 -0
- package/dist/writers/resolve-screenshot-path.js +8 -0
- package/dist/writers/resolve-within-base-dir.js +17 -0
- package/dist/writers/write-metadata.js +17 -0
- package/docs/API_EDITABLE_METADATA_SURFACE.md +319 -0
- package/docs/AUTH_SETUP.md +67 -0
- package/docs/DOCUMENTATION.md +285 -0
- package/docs/PROJECT_PLAN.md +687 -0
- package/docs/RELEASE_VERIFICATION.md +70 -0
- package/docs/TODO.md +308 -0
- package/examples/README.md +8 -0
- package/examples/metadata/apple/en-US.yml +11 -0
- package/examples/metadata/apple/tr.yml +11 -0
- package/examples/metadata/google/en-US.yml +6 -0
- package/examples/metadata/google/tr.yml +6 -0
- package/examples/screenshots/apple/en-US/APP_IPHONE_65/1.png +1 -0
- package/examples/screenshots/apple/tr/APP_IPHONE_65/1.png +1 -0
- package/examples/screenshots/google/en-US/phoneScreenshots/1.png +1 -0
- package/examples/screenshots/google/tr/phoneScreenshots/1.png +1 -0
- package/examples/storemeta.yml +29 -0
- package/package.json +55 -0
- package/storemeta.release.example.yml +27 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
import { runAuthCheckCommand } from "./cli/auth-check.js";
|
|
4
|
+
import { runConfigDoctorCommand } from "./cli/config-doctor.js";
|
|
5
|
+
import { runInitCommand } from "./cli/init.js";
|
|
6
|
+
import { applyCommandSummaryExitCode } from "./cli/exit-code.js";
|
|
7
|
+
import { runLocalesListCommand } from "./cli/locales-list.js";
|
|
8
|
+
import { runMetadataDiffCommand } from "./cli/metadata-diff.js";
|
|
9
|
+
import { runMetadataPullCommand } from "./cli/metadata-pull.js";
|
|
10
|
+
import { runMetadataPushCommand } from "./cli/metadata-push.js";
|
|
11
|
+
import { renderCommandError, renderCommandSummary } from "./cli/render.js";
|
|
12
|
+
import { runScaffoldCommand } from "./cli/scaffold.js";
|
|
13
|
+
import { runScreenshotsDiffCommand } from "./cli/screenshots-diff.js";
|
|
14
|
+
import { runScreenshotsPullCommand } from "./cli/screenshots-pull.js";
|
|
15
|
+
import { runScreenshotsPushCommand } from "./cli/screenshots-push.js";
|
|
16
|
+
import { runValidateCommand } from "./cli/validate.js";
|
|
17
|
+
export function buildCliProgram() {
|
|
18
|
+
const program = new Command()
|
|
19
|
+
.name("storemeta")
|
|
20
|
+
.description("CLI for pulling, validating, and pushing App Store Connect and Google Play metadata and screenshots.")
|
|
21
|
+
.option("--config <path>", "Path to the root storemeta config file")
|
|
22
|
+
.option("--app <id>", "Configured app identifier")
|
|
23
|
+
.addOption(new Option("--platform <platform>", "Target platform")
|
|
24
|
+
.choices(["apple", "google", "all"]))
|
|
25
|
+
.option("--locale <code>", "Target locale code")
|
|
26
|
+
.option("--dry-run", "Preview changes without applying them")
|
|
27
|
+
.option("--replace", "Delete existing remote screenshots before uploading new ones")
|
|
28
|
+
.option("--verbose", "Enable verbose output");
|
|
29
|
+
const metadataCommand = program
|
|
30
|
+
.command("metadata")
|
|
31
|
+
.description("Metadata commands");
|
|
32
|
+
const screenshotsCommand = program
|
|
33
|
+
.command("screenshots")
|
|
34
|
+
.description("Screenshot commands");
|
|
35
|
+
const authCommand = program.command("auth").description("Auth commands");
|
|
36
|
+
const configCommand = program.command("config").description("Config commands");
|
|
37
|
+
const localesCommand = program.command("locales").description("Locale commands");
|
|
38
|
+
program
|
|
39
|
+
.command("init")
|
|
40
|
+
.description("Create a starter storemeta config")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const options = program.opts();
|
|
43
|
+
const configPath = await runInitCommand(options.config);
|
|
44
|
+
const summary = await runScaffoldCommand({
|
|
45
|
+
config: configPath,
|
|
46
|
+
app: options.app,
|
|
47
|
+
platform: options.platform,
|
|
48
|
+
});
|
|
49
|
+
console.log([
|
|
50
|
+
"Created starter storemeta project.",
|
|
51
|
+
"Next steps:",
|
|
52
|
+
"1. Fill app identifiers in storemeta.yml.",
|
|
53
|
+
"2. Create Apple App Store Connect API credentials and Google Play service account credentials.",
|
|
54
|
+
"3. Export the credential environment variables named in storemeta.yml.",
|
|
55
|
+
"4. Run storemeta auth check, then storemeta validate.",
|
|
56
|
+
"See docs/AUTH_SETUP.md for credential setup.",
|
|
57
|
+
].join("\n"));
|
|
58
|
+
console.log(renderCommandSummary(summary));
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command("scaffold")
|
|
62
|
+
.description("Create missing metadata files and screenshot folders from config")
|
|
63
|
+
.action(async () => {
|
|
64
|
+
const options = program.opts();
|
|
65
|
+
const summary = await runScaffoldCommand({
|
|
66
|
+
config: options.config,
|
|
67
|
+
app: options.app,
|
|
68
|
+
platform: options.platform,
|
|
69
|
+
});
|
|
70
|
+
console.log(renderCommandSummary(summary));
|
|
71
|
+
applyCommandSummaryExitCode(summary);
|
|
72
|
+
});
|
|
73
|
+
program
|
|
74
|
+
.command("validate")
|
|
75
|
+
.description("Validate the storemeta project configuration")
|
|
76
|
+
.action(async () => {
|
|
77
|
+
const options = program.opts();
|
|
78
|
+
const summary = await runValidateCommand({
|
|
79
|
+
config: options.config,
|
|
80
|
+
app: options.app,
|
|
81
|
+
platform: options.platform,
|
|
82
|
+
});
|
|
83
|
+
console.log(renderCommandSummary(summary));
|
|
84
|
+
applyCommandSummaryExitCode(summary);
|
|
85
|
+
});
|
|
86
|
+
metadataCommand
|
|
87
|
+
.command("pull")
|
|
88
|
+
.description("Pull remote metadata into the local project")
|
|
89
|
+
.action(async () => {
|
|
90
|
+
const options = program.opts();
|
|
91
|
+
await runMetadataPullCommand({
|
|
92
|
+
config: options.config,
|
|
93
|
+
app: options.app,
|
|
94
|
+
locale: options.locale,
|
|
95
|
+
platform: options.platform,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
metadataCommand
|
|
99
|
+
.command("push")
|
|
100
|
+
.description("Push local metadata to the store")
|
|
101
|
+
.action(async () => {
|
|
102
|
+
const options = program.opts();
|
|
103
|
+
const summary = await runMetadataPushCommand({
|
|
104
|
+
config: options.config,
|
|
105
|
+
app: options.app,
|
|
106
|
+
platform: options.platform,
|
|
107
|
+
locale: options.locale,
|
|
108
|
+
dryRun: options.dryRun,
|
|
109
|
+
});
|
|
110
|
+
console.log(renderCommandSummary(summary));
|
|
111
|
+
applyCommandSummaryExitCode(summary);
|
|
112
|
+
});
|
|
113
|
+
metadataCommand
|
|
114
|
+
.command("diff")
|
|
115
|
+
.description("Compare configured metadata locales with local metadata files")
|
|
116
|
+
.action(async () => {
|
|
117
|
+
const options = program.opts();
|
|
118
|
+
const summary = await runMetadataDiffCommand({
|
|
119
|
+
config: options.config,
|
|
120
|
+
app: options.app,
|
|
121
|
+
platform: options.platform,
|
|
122
|
+
});
|
|
123
|
+
console.log(renderCommandSummary(summary));
|
|
124
|
+
applyCommandSummaryExitCode(summary);
|
|
125
|
+
});
|
|
126
|
+
authCommand
|
|
127
|
+
.command("check")
|
|
128
|
+
.description("Check configured credential environment variables")
|
|
129
|
+
.action(async () => {
|
|
130
|
+
const options = program.opts();
|
|
131
|
+
const summary = await runAuthCheckCommand({
|
|
132
|
+
config: options.config,
|
|
133
|
+
app: options.app,
|
|
134
|
+
platform: options.platform,
|
|
135
|
+
});
|
|
136
|
+
console.log(renderCommandSummary(summary));
|
|
137
|
+
applyCommandSummaryExitCode(summary);
|
|
138
|
+
});
|
|
139
|
+
configCommand
|
|
140
|
+
.command("doctor")
|
|
141
|
+
.description("Inspect resolved config, app, directories, and platforms")
|
|
142
|
+
.action(async () => {
|
|
143
|
+
const options = program.opts();
|
|
144
|
+
const summary = await runConfigDoctorCommand({
|
|
145
|
+
config: options.config,
|
|
146
|
+
app: options.app,
|
|
147
|
+
platform: options.platform,
|
|
148
|
+
});
|
|
149
|
+
console.log(renderCommandSummary(summary));
|
|
150
|
+
applyCommandSummaryExitCode(summary);
|
|
151
|
+
});
|
|
152
|
+
localesCommand
|
|
153
|
+
.command("list")
|
|
154
|
+
.description("List configured default locales")
|
|
155
|
+
.action(async () => {
|
|
156
|
+
const options = program.opts();
|
|
157
|
+
const summary = await runLocalesListCommand({
|
|
158
|
+
config: options.config,
|
|
159
|
+
app: options.app,
|
|
160
|
+
platform: options.platform,
|
|
161
|
+
});
|
|
162
|
+
console.log(renderCommandSummary(summary));
|
|
163
|
+
applyCommandSummaryExitCode(summary);
|
|
164
|
+
});
|
|
165
|
+
screenshotsCommand
|
|
166
|
+
.command("pull")
|
|
167
|
+
.description("Pull remote screenshots into the local project")
|
|
168
|
+
.action(async () => {
|
|
169
|
+
const options = program.opts();
|
|
170
|
+
const summary = await runScreenshotsPullCommand({
|
|
171
|
+
config: options.config,
|
|
172
|
+
app: options.app,
|
|
173
|
+
platform: options.platform,
|
|
174
|
+
locale: options.locale,
|
|
175
|
+
});
|
|
176
|
+
console.log(renderCommandSummary(summary));
|
|
177
|
+
applyCommandSummaryExitCode(summary);
|
|
178
|
+
});
|
|
179
|
+
screenshotsCommand
|
|
180
|
+
.command("diff")
|
|
181
|
+
.description("Compare configured screenshot locales with local screenshot folders")
|
|
182
|
+
.action(async () => {
|
|
183
|
+
const options = program.opts();
|
|
184
|
+
const summary = await runScreenshotsDiffCommand({
|
|
185
|
+
config: options.config,
|
|
186
|
+
app: options.app,
|
|
187
|
+
platform: options.platform,
|
|
188
|
+
});
|
|
189
|
+
console.log(renderCommandSummary(summary));
|
|
190
|
+
applyCommandSummaryExitCode(summary);
|
|
191
|
+
});
|
|
192
|
+
screenshotsCommand
|
|
193
|
+
.command("push")
|
|
194
|
+
.description("Push local screenshots to the store")
|
|
195
|
+
.action(async () => {
|
|
196
|
+
const options = program.opts();
|
|
197
|
+
const summary = await runScreenshotsPushCommand({
|
|
198
|
+
config: options.config,
|
|
199
|
+
app: options.app,
|
|
200
|
+
platform: options.platform,
|
|
201
|
+
locale: options.locale,
|
|
202
|
+
dryRun: options.dryRun,
|
|
203
|
+
replace: options.replace,
|
|
204
|
+
});
|
|
205
|
+
console.log(renderCommandSummary(summary));
|
|
206
|
+
applyCommandSummaryExitCode(summary);
|
|
207
|
+
});
|
|
208
|
+
return program;
|
|
209
|
+
}
|
|
210
|
+
export async function runCli(argv = process.argv) {
|
|
211
|
+
const program = buildCliProgram();
|
|
212
|
+
await program.parseAsync(argv);
|
|
213
|
+
}
|
|
214
|
+
export function hasVerboseFlag(argv) {
|
|
215
|
+
return argv.includes("--verbose");
|
|
216
|
+
}
|
|
217
|
+
void runCli().catch((error) => {
|
|
218
|
+
console.error(renderCommandError(error, {
|
|
219
|
+
verbose: hasVerboseFlag(process.argv),
|
|
220
|
+
}));
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function listConfiguredApps(config) {
|
|
2
|
+
return Object.entries(config.apps)
|
|
3
|
+
.sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
|
|
4
|
+
.map(([id, settings]) => ({
|
|
5
|
+
id,
|
|
6
|
+
settings,
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
export function getConfiguredApp(config, appId) {
|
|
10
|
+
const settings = config.apps[appId];
|
|
11
|
+
if (settings === undefined) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
id: appId,
|
|
16
|
+
settings,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function hasConfiguredApp(config, appId) {
|
|
20
|
+
return getConfiguredApp(config, appId) !== undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
5
|
+
export const DEFAULT_CONFIG_FILE = "storemeta.yml";
|
|
6
|
+
export function resolveConfigPath(configPath) {
|
|
7
|
+
return resolve(configPath ?? DEFAULT_CONFIG_FILE);
|
|
8
|
+
}
|
|
9
|
+
export async function loadConfigFile(configPath) {
|
|
10
|
+
const resolvedPath = resolveConfigPath(configPath);
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(resolvedPath, "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch (cause) {
|
|
16
|
+
throw new StoremetaError("FILESYSTEM_ERROR", `Failed to read config file at ${resolvedPath}`, { cause });
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = YAML.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
throw new StoremetaError("CONFIG_ERROR", `Failed to parse YAML config at ${resolvedPath}`, { cause });
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
path: resolvedPath,
|
|
27
|
+
raw,
|
|
28
|
+
parsed,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
3
|
+
const metadataFormatSchema = z.literal("yaml");
|
|
4
|
+
const projectSettingsSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
name: z.string().min(1),
|
|
7
|
+
defaultApp: z.string().min(1),
|
|
8
|
+
})
|
|
9
|
+
.strict();
|
|
10
|
+
const metadataSettingsSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
baseDir: z.string().min(1),
|
|
13
|
+
format: metadataFormatSchema,
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
const screenshotSettingsSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
baseDir: z.string().min(1),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
const appleCredentialsSettingsSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
issuerIdEnv: z.string().min(1),
|
|
24
|
+
keyIdEnv: z.string().min(1),
|
|
25
|
+
privateKeyPathEnv: z.string().min(1),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
const googleCredentialsSettingsSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
serviceAccountPathEnv: z.string().min(1),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const localeSettingsSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
default: z.array(z.string().min(1)).optional(),
|
|
36
|
+
map: z.record(z.string(), z.string().min(1)).optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
const screenshotGroupSettingsSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
locales: z.array(z.string().min(1)).min(1),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
const googleScreenshotSettingsSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
groups: z.record(z.string(), screenshotGroupSettingsSchema).optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
const appleAppSettingsSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
appId: z.string().min(1),
|
|
52
|
+
credentials: appleCredentialsSettingsSchema,
|
|
53
|
+
locales: localeSettingsSchema.optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
const googleAppSettingsSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
packageName: z.string().min(1),
|
|
59
|
+
credentials: googleCredentialsSettingsSchema,
|
|
60
|
+
locales: localeSettingsSchema.optional(),
|
|
61
|
+
screenshots: googleScreenshotSettingsSchema.optional(),
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
64
|
+
const appSettingsSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
metadata: metadataSettingsSchema,
|
|
67
|
+
screenshots: screenshotSettingsSchema,
|
|
68
|
+
apple: appleAppSettingsSchema.optional(),
|
|
69
|
+
google: googleAppSettingsSchema.optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict();
|
|
72
|
+
export const storemetaConfigSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
project: projectSettingsSchema,
|
|
76
|
+
apps: z
|
|
77
|
+
.record(z.string().min(1), appSettingsSchema)
|
|
78
|
+
.refine((apps) => Object.keys(apps).length > 0, {
|
|
79
|
+
message: "At least one app must be configured",
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
.strict();
|
|
83
|
+
function formatSchemaIssues(error) {
|
|
84
|
+
return error.issues
|
|
85
|
+
.map((issue) => {
|
|
86
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
87
|
+
return `${path}: ${issue.message}`;
|
|
88
|
+
})
|
|
89
|
+
.join("; ");
|
|
90
|
+
}
|
|
91
|
+
export function validateRootConfig(config) {
|
|
92
|
+
const result = storemetaConfigSchema.safeParse(config);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
throw new StoremetaError("CONFIG_ERROR", `Config schema validation failed: ${formatSchemaIssues(result.error)}`, { cause: result.error });
|
|
95
|
+
}
|
|
96
|
+
return result.data;
|
|
97
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
import { getConfiguredApp } from "./apps.js";
|
|
3
|
+
export function selectConfiguredApp(config, selectedAppId) {
|
|
4
|
+
const targetAppId = selectedAppId ?? config.project.defaultApp;
|
|
5
|
+
const app = getConfiguredApp(config, targetAppId);
|
|
6
|
+
if (app === undefined) {
|
|
7
|
+
throw new StoremetaError("CONFIG_ERROR", `Configured app "${targetAppId}" was not found`);
|
|
8
|
+
}
|
|
9
|
+
return app;
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
export function resolveSelectedPlatforms(app, selectedPlatform) {
|
|
3
|
+
const configuredPlatforms = [];
|
|
4
|
+
if (app.settings.apple !== undefined) {
|
|
5
|
+
configuredPlatforms.push("apple");
|
|
6
|
+
}
|
|
7
|
+
if (app.settings.google !== undefined) {
|
|
8
|
+
configuredPlatforms.push("google");
|
|
9
|
+
}
|
|
10
|
+
if (selectedPlatform === undefined || selectedPlatform === "all") {
|
|
11
|
+
return configuredPlatforms;
|
|
12
|
+
}
|
|
13
|
+
if (!configuredPlatforms.includes(selectedPlatform)) {
|
|
14
|
+
throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not support platform "${selectedPlatform}"`);
|
|
15
|
+
}
|
|
16
|
+
return [selectedPlatform];
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
import { listConfiguredApps } from "./apps.js";
|
|
3
|
+
export function hasSingleConfiguredApp(config) {
|
|
4
|
+
return listConfiguredApps(config).length === 1;
|
|
5
|
+
}
|
|
6
|
+
export function getSingleConfiguredApp(config) {
|
|
7
|
+
const [app] = listConfiguredApps(config);
|
|
8
|
+
return hasSingleConfiguredApp(config) ? app : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function requireSingleConfiguredApp(config) {
|
|
11
|
+
const app = getSingleConfiguredApp(config);
|
|
12
|
+
if (app === undefined) {
|
|
13
|
+
throw new StoremetaError("CONFIG_ERROR", "Expected exactly one configured app");
|
|
14
|
+
}
|
|
15
|
+
return app;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
2
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
3
|
+
import { listConfiguredApps } from "./apps.js";
|
|
4
|
+
function isAbsolutePath(value) {
|
|
5
|
+
return posix.isAbsolute(value) || win32.isAbsolute(value);
|
|
6
|
+
}
|
|
7
|
+
function normalizeRelativePath(value) {
|
|
8
|
+
return posix.normalize(value.replaceAll("\\", "/"));
|
|
9
|
+
}
|
|
10
|
+
function isSafeRelativeDirectoryPath(value) {
|
|
11
|
+
if (value.trim().length === 0 || isAbsolutePath(value)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizeRelativePath(value);
|
|
15
|
+
if (normalized === "." || normalized === "..") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return !normalized.startsWith("../");
|
|
19
|
+
}
|
|
20
|
+
export function validateBaseDirectoryPaths(config) {
|
|
21
|
+
const issues = [];
|
|
22
|
+
for (const app of listConfiguredApps(config)) {
|
|
23
|
+
if (!isSafeRelativeDirectoryPath(app.settings.metadata.baseDir)) {
|
|
24
|
+
issues.push(`apps.${app.id}.metadata.baseDir: must be a safe relative path`);
|
|
25
|
+
}
|
|
26
|
+
if (!isSafeRelativeDirectoryPath(app.settings.screenshots.baseDir)) {
|
|
27
|
+
issues.push(`apps.${app.id}.screenshots.baseDir: must be a safe relative path`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (issues.length > 0) {
|
|
31
|
+
throw new StoremetaError("CONFIG_ERROR", `Base directory validation failed: ${issues.join("; ")}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
import { listConfiguredApps } from "./apps.js";
|
|
3
|
+
const ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
|
+
function isValidEnvVarName(value) {
|
|
5
|
+
return ENV_VAR_NAME_PATTERN.test(value);
|
|
6
|
+
}
|
|
7
|
+
export function validateCredentialEnvVarNames(config) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
for (const app of listConfiguredApps(config)) {
|
|
10
|
+
if (app.settings.apple !== undefined) {
|
|
11
|
+
const { credentials } = app.settings.apple;
|
|
12
|
+
if (!isValidEnvVarName(credentials.issuerIdEnv)) {
|
|
13
|
+
issues.push(`apps.${app.id}.apple.credentials.issuerIdEnv: invalid env var name`);
|
|
14
|
+
}
|
|
15
|
+
if (!isValidEnvVarName(credentials.keyIdEnv)) {
|
|
16
|
+
issues.push(`apps.${app.id}.apple.credentials.keyIdEnv: invalid env var name`);
|
|
17
|
+
}
|
|
18
|
+
if (!isValidEnvVarName(credentials.privateKeyPathEnv)) {
|
|
19
|
+
issues.push(`apps.${app.id}.apple.credentials.privateKeyPathEnv: invalid env var name`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (app.settings.google !== undefined) {
|
|
23
|
+
const { credentials } = app.settings.google;
|
|
24
|
+
if (!isValidEnvVarName(credentials.serviceAccountPathEnv)) {
|
|
25
|
+
issues.push(`apps.${app.id}.google.credentials.serviceAccountPathEnv: invalid env var name`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (issues.length > 0) {
|
|
30
|
+
throw new StoremetaError("CONFIG_ERROR", `Credential environment variable validation failed: ${issues.join("; ")}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
import { listConfiguredApps } from "./apps.js";
|
|
3
|
+
function hasText(value) {
|
|
4
|
+
return value !== undefined && value.trim().length > 0;
|
|
5
|
+
}
|
|
6
|
+
export function validateRequiredPlatformIdentifiers(config) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
for (const app of listConfiguredApps(config)) {
|
|
9
|
+
const { apple, google } = app.settings;
|
|
10
|
+
if (apple === undefined && google === undefined) {
|
|
11
|
+
issues.push(`apps.${app.id}: at least one platform must be configured`);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (apple !== undefined && !hasText(apple.appId)) {
|
|
15
|
+
issues.push(`apps.${app.id}.apple.appId: required`);
|
|
16
|
+
}
|
|
17
|
+
if (google !== undefined && !hasText(google.packageName)) {
|
|
18
|
+
issues.push(`apps.${app.id}.google.packageName: required`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (issues.length > 0) {
|
|
22
|
+
throw new StoremetaError("CONFIG_ERROR", `Platform identifier validation failed: ${issues.join("; ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
5
|
+
async function loadMetadataFileForExtension(filePath, expectedExtension) {
|
|
6
|
+
const resolvedPath = resolve(filePath);
|
|
7
|
+
if (extname(resolvedPath).toLowerCase() !== expectedExtension) {
|
|
8
|
+
throw new StoremetaError("FILESYSTEM_ERROR", `Expected a ${expectedExtension} metadata file: ${resolvedPath}`);
|
|
9
|
+
}
|
|
10
|
+
let raw;
|
|
11
|
+
try {
|
|
12
|
+
raw = await readFile(resolvedPath, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch (cause) {
|
|
15
|
+
throw new StoremetaError("FILESYSTEM_ERROR", `Failed to read metadata file at ${resolvedPath}`, { cause });
|
|
16
|
+
}
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = YAML.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
throw new StoremetaError("CONFIG_ERROR", `Failed to parse YAML metadata at ${resolvedPath}`, { cause });
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
path: resolvedPath,
|
|
26
|
+
raw,
|
|
27
|
+
parsed,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function loadYmlMetadataFile(filePath) {
|
|
31
|
+
return loadMetadataFileForExtension(filePath, ".yml");
|
|
32
|
+
}
|
|
33
|
+
export async function loadYamlMetadataFile(filePath) {
|
|
34
|
+
return loadMetadataFileForExtension(filePath, ".yaml");
|
|
35
|
+
}
|
|
36
|
+
export async function loadMarkdownMetadataFile(filePath) {
|
|
37
|
+
return loadMetadataFileForExtension(filePath, ".md");
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
function sortMetadataValue(value) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
return value.map(sortMetadataValue);
|
|
5
|
+
}
|
|
6
|
+
if (value !== null && typeof value === "object") {
|
|
7
|
+
return Object.fromEntries(Object.entries(value)
|
|
8
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
9
|
+
.map(([key, childValue]) => [key, sortMetadataValue(childValue)]));
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function serializeMetadataDocument(document) {
|
|
14
|
+
const serialized = YAML.stringify(sortMetadataValue(document), {
|
|
15
|
+
indent: 2,
|
|
16
|
+
lineWidth: 0,
|
|
17
|
+
minContentWidth: 0,
|
|
18
|
+
});
|
|
19
|
+
return serialized.endsWith("\n") ? serialized : `${serialized}\n`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { normalizeLocaleCodes } from "./normalize.js";
|
|
2
|
+
export function listScreenshotGroups(settings) {
|
|
3
|
+
return Object.entries(settings?.groups ?? {})
|
|
4
|
+
.sort(([leftName], [rightName]) => leftName.localeCompare(rightName))
|
|
5
|
+
.map(([name, group]) => ({
|
|
6
|
+
name,
|
|
7
|
+
locales: normalizeLocaleCodes(group.locales),
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
export function getScreenshotGroup(settings, groupName) {
|
|
11
|
+
return listScreenshotGroups(settings).find((group) => group.name === groupName);
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { normalizeLocaleCode } from "./normalize.js";
|
|
2
|
+
function normalizeLocaleMap(localeSettings) {
|
|
3
|
+
return Object.fromEntries(Object.entries(localeSettings?.map ?? {}).map(([sourceLocale, targetLocale]) => [
|
|
4
|
+
normalizeLocaleCode(sourceLocale),
|
|
5
|
+
normalizeLocaleCode(targetLocale),
|
|
6
|
+
]));
|
|
7
|
+
}
|
|
8
|
+
export function mapLocaleCode(locale, localeSettings) {
|
|
9
|
+
const normalizedLocale = normalizeLocaleCode(locale);
|
|
10
|
+
const localeMap = normalizeLocaleMap(localeSettings);
|
|
11
|
+
return localeMap[normalizedLocale] ?? normalizedLocale;
|
|
12
|
+
}
|
|
13
|
+
export function mapLocaleCodes(locales, localeSettings) {
|
|
14
|
+
return locales.map((locale) => mapLocaleCode(locale, localeSettings));
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function normalizeLocaleSegment(segment, index) {
|
|
2
|
+
if (index === 0) {
|
|
3
|
+
return segment.toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
if (segment.length === 4) {
|
|
6
|
+
return `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1).toLowerCase()}`;
|
|
7
|
+
}
|
|
8
|
+
if (/^[A-Za-z]{2}$/.test(segment) || /^\d{3}$/.test(segment)) {
|
|
9
|
+
return segment.toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
return segment.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
export function normalizeLocaleCode(locale) {
|
|
14
|
+
const trimmed = locale.trim().replaceAll("_", "-");
|
|
15
|
+
const segments = trimmed.split("-").filter((segment) => segment.length > 0);
|
|
16
|
+
return segments
|
|
17
|
+
.map((segment, index) => normalizeLocaleSegment(segment, index))
|
|
18
|
+
.join("-");
|
|
19
|
+
}
|
|
20
|
+
export function normalizeLocaleCodes(locales) {
|
|
21
|
+
return locales.map(normalizeLocaleCode);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StoremetaError } from "../cli/errors.js";
|
|
2
|
+
import { listScreenshotGroups } from "./groups.js";
|
|
3
|
+
export function validateScreenshotGroups(settings) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
for (const group of listScreenshotGroups(settings)) {
|
|
6
|
+
const uniqueLocales = new Set(group.locales);
|
|
7
|
+
if (uniqueLocales.size !== group.locales.length) {
|
|
8
|
+
issues.push(`screenshots.groups.${group.name}: contains duplicate locales after normalization`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (issues.length > 0) {
|
|
12
|
+
throw new StoremetaError("VALIDATION_ERROR", `Screenshot group validation failed: ${issues.join("; ")}`);
|
|
13
|
+
}
|
|
14
|
+
}
|