@storewright/cli 0.14.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/README.md +19 -0
- package/VERSION +1 -0
- package/bin/storewright.mjs +62 -0
- package/contracts/action-registry.json +175 -0
- package/contracts/capability-registry.json +63 -0
- package/contracts/workflow-manifest.json +207 -0
- package/lib/cli/storewright-cli.mjs +259 -0
- package/lib/internal/launch-envelope.mjs +223 -0
- package/lib/internal/multi-agent-contracts.mjs +137 -0
- package/lib/internal/operation-ledger.mjs +190 -0
- package/lib/internal/pricing/default-preview-pricing.mjs +181 -0
- package/lib/internal/run-state-helpers.mjs +313 -0
- package/lib/internal/shopify-operation-adapter.mjs +456 -0
- package/package.json +38 -0
- package/schemas/action-registry.schema.json +11 -0
- package/schemas/agent-report.schema.json +14 -0
- package/schemas/approval-grant.schema.json +16 -0
- package/schemas/base-theme-report.schema.json +25 -0
- package/schemas/brand-identity.schema.json +142 -0
- package/schemas/capability-registry.schema.json +11 -0
- package/schemas/competitor-audit.schema.json +38 -0
- package/schemas/design-direction.schema.json +64 -0
- package/schemas/external-operation.schema.json +34 -0
- package/schemas/intake-blocked-report.schema.json +76 -0
- package/schemas/launch-envelope.schema.json +25 -0
- package/schemas/launch-readiness.schema.json +73 -0
- package/schemas/media-file-inspection-report.schema.json +223 -0
- package/schemas/media-manifest.schema.json +84 -0
- package/schemas/merchandising-brief.schema.json +27 -0
- package/schemas/normalized-product-catalog.schema.json +42 -0
- package/schemas/product-content-generation-input.schema.json +40 -0
- package/schemas/product-content-generation-output.schema.json +43 -0
- package/schemas/raw-product-candidates.schema.json +32 -0
- package/schemas/shopify-access-preflight-report.schema.json +213 -0
- package/schemas/shopify-content-sync-report.schema.json +190 -0
- package/schemas/shopify-media-map.schema.json +87 -0
- package/schemas/shopify-media-upload-report.schema.json +96 -0
- package/schemas/shopify-operation-request.schema.json +81 -0
- package/schemas/shopify-preflight-report.schema.json +187 -0
- package/schemas/store-blueprint.schema.json +112 -0
- package/schemas/store-content-generation-output.schema.json +102 -0
- package/schemas/store-intake.schema.json +205 -0
- package/schemas/store-ops-plan.schema.json +82 -0
- package/schemas/storefront-preview-review.schema.json +227 -0
- package/schemas/supplier-access-report.schema.json +36 -0
- package/schemas/supplier-extraction-report.schema.json +185 -0
- package/schemas/theme-build-report.schema.json +43 -0
- package/schemas/theme-code-change-summary.schema.json +65 -0
- package/schemas/theme-plan.schema.json +26 -0
- package/schemas/theme-push-report.schema.json +151 -0
- package/schemas/theme-workspace-validation-report.schema.json +61 -0
- package/schemas/workflow-manifest.schema.json +29 -0
- package/scripts/audit-run-state.mjs +472 -0
- package/scripts/execute-shopify-operation.mjs +190 -0
- package/scripts/generate-image-assets-openai.mjs +342 -0
- package/scripts/generate-media-assets.mjs +121 -0
- package/scripts/init-run-state.mjs +69 -0
- package/scripts/inspect-media-files.mjs +334 -0
- package/scripts/prepare-launch-envelope.mjs +47 -0
- package/scripts/shopify-access-preflight.mjs +432 -0
- package/scripts/upload-shopify-media.mjs +831 -0
- package/scripts/validate-agent-report.mjs +46 -0
- package/scripts/validate-artifact.mjs +196 -0
- package/scripts/validate-launch-envelope.mjs +50 -0
- package/scripts/validate-registries.mjs +50 -0
- package/scripts/validate-workflow-manifest.mjs +38 -0
- package/scripts/version.mjs +192 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const defaultApiKeyEnv = "OPENAI_API_KEY";
|
|
7
|
+
const defaultModel = "gpt-image-1.5";
|
|
8
|
+
const defaultSize = "1536x1024";
|
|
9
|
+
const defaultQuality = "auto";
|
|
10
|
+
const defaultOutputFormat = "png";
|
|
11
|
+
const openAiImagesEndpoint = "https://api.openai.com/v1/images/generations";
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = {};
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const arg = argv[index];
|
|
17
|
+
if (arg === "--spec") {
|
|
18
|
+
args.specPath = argv[index + 1];
|
|
19
|
+
index += 1;
|
|
20
|
+
} else if (arg === "--output") {
|
|
21
|
+
args.outputPath = argv[index + 1];
|
|
22
|
+
index += 1;
|
|
23
|
+
} else if (arg === "--run-dir") {
|
|
24
|
+
args.runDir = argv[index + 1];
|
|
25
|
+
index += 1;
|
|
26
|
+
} else if (arg === "--env-file") {
|
|
27
|
+
args.envFilePath = argv[index + 1];
|
|
28
|
+
index += 1;
|
|
29
|
+
} else if (arg === "--api-key-env") {
|
|
30
|
+
args.apiKeyEnv = argv[index + 1];
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveRunPath(runDir, filePath) {
|
|
38
|
+
if (!filePath || isAbsolute(filePath)) {
|
|
39
|
+
return filePath;
|
|
40
|
+
}
|
|
41
|
+
return join(runDir, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseEnvFile(source) {
|
|
45
|
+
const values = {};
|
|
46
|
+
for (const [index, rawLine] of source.split(/\r?\n/).entries()) {
|
|
47
|
+
const line = rawLine.trim();
|
|
48
|
+
if (!line || line.startsWith("#")) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith("export ")) {
|
|
52
|
+
throw new Error(`Unsupported env-file syntax on line ${index + 1}`);
|
|
53
|
+
}
|
|
54
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new Error(`Invalid env-file line ${index + 1}`);
|
|
57
|
+
}
|
|
58
|
+
let value = match[2].trim();
|
|
59
|
+
if (
|
|
60
|
+
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
61
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
62
|
+
) {
|
|
63
|
+
value = value.slice(1, -1);
|
|
64
|
+
}
|
|
65
|
+
values[match[1]] = value;
|
|
66
|
+
}
|
|
67
|
+
return values;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readEnvFile({ envFilePath, cwd }) {
|
|
71
|
+
if (!envFilePath) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
const resolvedPath = isAbsolute(envFilePath) ? envFilePath : join(cwd, envFilePath);
|
|
75
|
+
return parseEnvFile(await readFile(resolvedPath, "utf8"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function openAiAssets(spec) {
|
|
79
|
+
const assets = spec.mediaGeneration?.assets ?? spec.media?.assets ?? [];
|
|
80
|
+
return assets.filter((asset) => asset.generation?.provider === "openai");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function redact(value, secrets) {
|
|
84
|
+
let redacted = String(value ?? "");
|
|
85
|
+
for (const secret of secrets) {
|
|
86
|
+
if (secret) {
|
|
87
|
+
redacted = redacted.split(secret).join("[redacted]");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return redacted;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizeApiError({ payload, status, secrets }) {
|
|
94
|
+
const error = payload?.error ?? {};
|
|
95
|
+
return {
|
|
96
|
+
status,
|
|
97
|
+
code: redact(error.code ?? `http_${status}`, secrets),
|
|
98
|
+
message: redact(error.message ?? `OpenAI image generation failed with HTTP ${status}`, secrets)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assetResultBase(asset, model) {
|
|
103
|
+
return {
|
|
104
|
+
assetId: asset.assetId,
|
|
105
|
+
...(asset.slotId ? { slotId: asset.slotId } : {}),
|
|
106
|
+
...(asset.candidateId ? { candidateId: asset.candidateId } : {}),
|
|
107
|
+
localPath: asset.localPath,
|
|
108
|
+
model
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generationDetails(asset) {
|
|
113
|
+
const generation = asset.generation ?? {};
|
|
114
|
+
return {
|
|
115
|
+
prompt: generation.prompt,
|
|
116
|
+
model: generation.model ?? defaultModel,
|
|
117
|
+
size: generation.size ?? defaultSize,
|
|
118
|
+
quality: generation.quality ?? defaultQuality,
|
|
119
|
+
outputFormat: generation.outputFormat ?? defaultOutputFormat
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function skippedMissingCredentialResult(asset, apiKeyEnv) {
|
|
124
|
+
const details = generationDetails(asset);
|
|
125
|
+
return {
|
|
126
|
+
...assetResultBase(asset, details.model),
|
|
127
|
+
prompt: details.prompt,
|
|
128
|
+
size: details.size,
|
|
129
|
+
quality: details.quality,
|
|
130
|
+
outputFormat: details.outputFormat,
|
|
131
|
+
apiKeyEnv,
|
|
132
|
+
credentialStatus: "missing",
|
|
133
|
+
status: "skipped",
|
|
134
|
+
reasonCode: "missing_credential",
|
|
135
|
+
warning: `OpenAI credential missing, generation skipped. Prompt: ${details.prompt ?? ""}`.trim()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function skippedGenerationFailureResult({ asset, reasonCode, warning, error }) {
|
|
140
|
+
const details = generationDetails(asset);
|
|
141
|
+
return {
|
|
142
|
+
...assetResultBase(asset, details.model),
|
|
143
|
+
prompt: details.prompt,
|
|
144
|
+
size: details.size,
|
|
145
|
+
quality: details.quality,
|
|
146
|
+
outputFormat: details.outputFormat,
|
|
147
|
+
status: "skipped",
|
|
148
|
+
reasonCode,
|
|
149
|
+
warning,
|
|
150
|
+
...(error ? { error } : {})
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function missingPromptResult(asset) {
|
|
155
|
+
const details = generationDetails(asset);
|
|
156
|
+
return {
|
|
157
|
+
...assetResultBase(asset, details.model),
|
|
158
|
+
prompt: details.prompt,
|
|
159
|
+
size: details.size,
|
|
160
|
+
quality: details.quality,
|
|
161
|
+
outputFormat: details.outputFormat,
|
|
162
|
+
status: "error",
|
|
163
|
+
reasonCode: "missing_prompt",
|
|
164
|
+
error: {
|
|
165
|
+
code: "missing_prompt",
|
|
166
|
+
message: "Missing OpenAI image generation prompt"
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function readResponseJson(response) {
|
|
172
|
+
try {
|
|
173
|
+
return await response.json();
|
|
174
|
+
} catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sanitizeThrownError({ error, secrets }) {
|
|
180
|
+
return {
|
|
181
|
+
code: "fetch_error",
|
|
182
|
+
message: redact(error?.message ?? "OpenAI image generation request failed", secrets)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function generateAsset({ asset, apiKey, fetchImpl, runDir }) {
|
|
187
|
+
const generation = asset.generation ?? {};
|
|
188
|
+
const model = generation.model ?? defaultModel;
|
|
189
|
+
const prompt = generation.prompt;
|
|
190
|
+
const secrets = [apiKey];
|
|
191
|
+
|
|
192
|
+
if (!prompt) {
|
|
193
|
+
return missingPromptResult(asset);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const body = {
|
|
197
|
+
model,
|
|
198
|
+
prompt,
|
|
199
|
+
size: generation.size ?? defaultSize,
|
|
200
|
+
quality: generation.quality ?? defaultQuality,
|
|
201
|
+
output_format: generation.outputFormat ?? defaultOutputFormat
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let response;
|
|
205
|
+
try {
|
|
206
|
+
response = await fetchImpl(openAiImagesEndpoint, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
Authorization: `Bearer ${apiKey}`,
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify(body)
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return skippedGenerationFailureResult({
|
|
216
|
+
asset,
|
|
217
|
+
reasonCode: "fetch_error",
|
|
218
|
+
warning: "OpenAI image generation request failed, generation skipped.",
|
|
219
|
+
error: sanitizeThrownError({ error, secrets })
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const payload = await readResponseJson(response);
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
return skippedGenerationFailureResult({
|
|
226
|
+
asset,
|
|
227
|
+
reasonCode: "api_error",
|
|
228
|
+
warning: "OpenAI image generation API error, generation skipped.",
|
|
229
|
+
error: sanitizeApiError({ payload, status: response.status, secrets })
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const imageBase64 = payload.data?.[0]?.b64_json;
|
|
234
|
+
if (!imageBase64) {
|
|
235
|
+
return skippedGenerationFailureResult({
|
|
236
|
+
asset,
|
|
237
|
+
reasonCode: "missing_b64_json",
|
|
238
|
+
warning: "OpenAI image generation response missing image data, generation skipped.",
|
|
239
|
+
error: {
|
|
240
|
+
code: "missing_b64_json",
|
|
241
|
+
message: "OpenAI image generation response did not include b64_json"
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const absoluteLocalPath = resolveRunPath(runDir, asset.localPath);
|
|
247
|
+
await mkdir(dirname(absoluteLocalPath), { recursive: true });
|
|
248
|
+
await writeFile(absoluteLocalPath, Buffer.from(imageBase64, "base64"));
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...assetResultBase(asset, model),
|
|
252
|
+
status: "generated",
|
|
253
|
+
usage: payload.usage ?? {}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function writeReport({ outputPath, runDir, report }) {
|
|
258
|
+
const resolvedOutputPath = outputPath ?? join(runDir, "openai-image-generation-report.json");
|
|
259
|
+
await mkdir(dirname(resolvedOutputPath), { recursive: true });
|
|
260
|
+
await writeFile(resolvedOutputPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
261
|
+
return resolvedOutputPath;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function generateOpenAiImageAssets({
|
|
265
|
+
specPath,
|
|
266
|
+
outputPath,
|
|
267
|
+
runDir,
|
|
268
|
+
envFilePath,
|
|
269
|
+
apiKeyEnv,
|
|
270
|
+
cwd = process.cwd(),
|
|
271
|
+
processEnv = process.env,
|
|
272
|
+
fetchImpl = globalThis.fetch
|
|
273
|
+
}) {
|
|
274
|
+
if (!specPath) {
|
|
275
|
+
throw new Error("Missing --spec argument");
|
|
276
|
+
}
|
|
277
|
+
if (typeof fetchImpl !== "function") {
|
|
278
|
+
throw new Error("A fetch implementation is required");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const resolvedRunDir = runDir ?? dirname(specPath);
|
|
282
|
+
const spec = JSON.parse(await readFile(specPath, "utf8"));
|
|
283
|
+
const assets = openAiAssets(spec);
|
|
284
|
+
const requestedApiKeyEnv = apiKeyEnv ?? assets[0]?.generation?.apiKeyEnv ?? defaultApiKeyEnv;
|
|
285
|
+
const envFileValues = await readEnvFile({ envFilePath, cwd });
|
|
286
|
+
const apiKey = processEnv[requestedApiKeyEnv] ?? envFileValues[requestedApiKeyEnv];
|
|
287
|
+
const credentialStatus = apiKey ? "present" : "missing";
|
|
288
|
+
|
|
289
|
+
const report = {
|
|
290
|
+
schemaVersion: "1.0.0",
|
|
291
|
+
runId: spec.runId,
|
|
292
|
+
authoring: {
|
|
293
|
+
mode: "worker-authored",
|
|
294
|
+
createdBy: "media-production-worker",
|
|
295
|
+
sourceInputArtifacts: spec.authoring?.sourceInputArtifacts ?? ["media-generation-spec.json"]
|
|
296
|
+
},
|
|
297
|
+
openaiImageGeneration: {
|
|
298
|
+
apiKeyEnv: requestedApiKeyEnv,
|
|
299
|
+
credentialStatus,
|
|
300
|
+
envFileUsed: Boolean(envFilePath),
|
|
301
|
+
assets: []
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (!apiKey) {
|
|
306
|
+
report.openaiImageGeneration.assets = assets.map((asset) => (
|
|
307
|
+
asset.generation?.prompt
|
|
308
|
+
? skippedMissingCredentialResult(asset, requestedApiKeyEnv)
|
|
309
|
+
: missingPromptResult(asset)
|
|
310
|
+
));
|
|
311
|
+
const resolvedOutputPath = await writeReport({ outputPath, runDir: resolvedRunDir, report });
|
|
312
|
+
const ok = report.openaiImageGeneration.assets.every((asset) => asset.status === "skipped");
|
|
313
|
+
return { ok, outputPath: resolvedOutputPath, assetCount: assets.length };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const asset of assets) {
|
|
317
|
+
report.openaiImageGeneration.assets.push(await generateAsset({ asset, apiKey, fetchImpl, runDir: resolvedRunDir }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ok = report.openaiImageGeneration.assets.every((asset) => (
|
|
321
|
+
asset.status === "generated" || asset.status === "skipped"
|
|
322
|
+
));
|
|
323
|
+
const resolvedOutputPath = await writeReport({ outputPath, runDir: resolvedRunDir, report });
|
|
324
|
+
return { ok, outputPath: resolvedOutputPath, assetCount: assets.length };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function main() {
|
|
328
|
+
try {
|
|
329
|
+
const result = await generateOpenAiImageAssets(parseArgs(process.argv.slice(2)));
|
|
330
|
+
console.log(JSON.stringify(result));
|
|
331
|
+
if (!result.ok) {
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error(error.message);
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
341
|
+
await main();
|
|
342
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const args = {};
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const arg = argv[index];
|
|
11
|
+
if (arg === "--spec") {
|
|
12
|
+
args.specPath = argv[index + 1];
|
|
13
|
+
index += 1;
|
|
14
|
+
} else if (arg === "--output") {
|
|
15
|
+
args.outputPath = argv[index + 1];
|
|
16
|
+
index += 1;
|
|
17
|
+
} else if (arg === "--run-dir") {
|
|
18
|
+
args.runDir = argv[index + 1];
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return args;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveRunPath(runDir, filePath) {
|
|
26
|
+
if (!filePath || isAbsolute(filePath)) {
|
|
27
|
+
return filePath;
|
|
28
|
+
}
|
|
29
|
+
return join(runDir, filePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodeDataUri(dataUri) {
|
|
33
|
+
const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(dataUri);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error("Invalid data URI");
|
|
36
|
+
}
|
|
37
|
+
return Buffer.from(decodeURIComponent(match[3]), match[2] ? "base64" : "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function materializeAsset({ asset, runDir }) {
|
|
41
|
+
const localPath = asset.localPath;
|
|
42
|
+
if (!localPath) {
|
|
43
|
+
return { ...asset, status: "generation-required", warnings: ["missing localPath"] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const absoluteLocalPath = resolveRunPath(runDir, localPath);
|
|
47
|
+
await mkdir(dirname(absoluteLocalPath), { recursive: true });
|
|
48
|
+
|
|
49
|
+
if (asset.dataUri) {
|
|
50
|
+
await writeFile(absoluteLocalPath, decodeDataUri(asset.dataUri));
|
|
51
|
+
} else if (asset.sourceLocalPath) {
|
|
52
|
+
await copyFile(resolveRunPath(runDir, asset.sourceLocalPath), absoluteLocalPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fileExists = existsSync(absoluteLocalPath);
|
|
56
|
+
const warnings = [...(asset.warnings ?? [])];
|
|
57
|
+
let status = asset.status;
|
|
58
|
+
|
|
59
|
+
if (fileExists) {
|
|
60
|
+
status = status === "generation-required" ? "ready-for-upload" : (status ?? "ready-for-upload");
|
|
61
|
+
} else if (status === "ready-for-upload" || status === "source-downloaded" || status === "uploaded") {
|
|
62
|
+
status = "upload-blocked";
|
|
63
|
+
warnings.push(`localPath does not exist: ${localPath}`);
|
|
64
|
+
} else {
|
|
65
|
+
status ??= "generation-required";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { dataUri, sourceLocalPath, generation, ...manifestAsset } = asset;
|
|
69
|
+
return {
|
|
70
|
+
...manifestAsset,
|
|
71
|
+
status,
|
|
72
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function generateMediaAssets({ specPath, outputPath, runDir }) {
|
|
77
|
+
if (!specPath) {
|
|
78
|
+
throw new Error("Missing --spec argument");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolvedRunDir = runDir ?? dirname(specPath);
|
|
82
|
+
const spec = JSON.parse(await readFile(specPath, "utf8"));
|
|
83
|
+
const assets = spec.mediaGeneration?.assets ?? spec.media?.assets ?? [];
|
|
84
|
+
const manifestAssets = [];
|
|
85
|
+
|
|
86
|
+
for (const asset of assets) {
|
|
87
|
+
manifestAssets.push(await materializeAsset({ asset, runDir: resolvedRunDir }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const manifest = {
|
|
91
|
+
schemaVersion: "1.0.0",
|
|
92
|
+
runId: spec.runId,
|
|
93
|
+
authoring: {
|
|
94
|
+
mode: "worker-authored",
|
|
95
|
+
createdBy: "media-production-worker",
|
|
96
|
+
sourceInputArtifacts: spec.authoring?.sourceInputArtifacts ?? ["media-generation-spec.json"]
|
|
97
|
+
},
|
|
98
|
+
media: {
|
|
99
|
+
assets: manifestAssets
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolvedOutputPath = outputPath ?? join(resolvedRunDir, "media-manifest.json");
|
|
104
|
+
await mkdir(dirname(resolvedOutputPath), { recursive: true });
|
|
105
|
+
await writeFile(resolvedOutputPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
106
|
+
return { outputPath: resolvedOutputPath, assetCount: manifestAssets.length };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
try {
|
|
111
|
+
const result = await generateMediaAssets(parseArgs(process.argv.slice(2)));
|
|
112
|
+
console.log(JSON.stringify(result));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(error.message);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
120
|
+
await main();
|
|
121
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { writeInitialProgress } from "../lib/internal/run-state-helpers.mjs";
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const options = {};
|
|
10
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
11
|
+
const arg = argv[index];
|
|
12
|
+
const value = argv[index + 1];
|
|
13
|
+
if (arg === "--help" || arg === "-h") {
|
|
14
|
+
options.help = true;
|
|
15
|
+
} else if (arg === "--workspace-dir") {
|
|
16
|
+
options.workspaceDir = value;
|
|
17
|
+
index += 1;
|
|
18
|
+
} else if (arg === "--run-id") {
|
|
19
|
+
options.runId = value;
|
|
20
|
+
index += 1;
|
|
21
|
+
} else if (arg === "--run-dir") {
|
|
22
|
+
options.runDir = value;
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
return [
|
|
31
|
+
"Usage: init-run-state.mjs [--workspace-dir <path>] [--run-id <id>] [--run-dir <path>]",
|
|
32
|
+
"",
|
|
33
|
+
"Initializes .storewright/runs/<run-id>/PROGRESS.md before Stage 1."
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function timestampRunId(date = new Date()) {
|
|
38
|
+
return date.toISOString().replace(/[:.]/g, "").replace("T", "-").replace("Z", "Z");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function initRunState(options = {}) {
|
|
42
|
+
const workspaceDir = options.workspaceDir ?? process.cwd();
|
|
43
|
+
const runId = options.runId ?? timestampRunId();
|
|
44
|
+
const runDir = options.runDir ?? join(workspaceDir, ".storewright", "runs", runId);
|
|
45
|
+
await mkdir(runDir, { recursive: true });
|
|
46
|
+
const progressPath = await writeInitialProgress({ runDir, runId });
|
|
47
|
+
return { runId, runDir, progressPath };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
try {
|
|
52
|
+
const options = parseArgs(process.argv.slice(2));
|
|
53
|
+
if (options.help) {
|
|
54
|
+
console.log(usage());
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const result = await initRunState(options);
|
|
58
|
+
console.log(`Run ID: ${result.runId}`);
|
|
59
|
+
console.log(`Run directory: ${result.runDir}`);
|
|
60
|
+
console.log(`Progress: ${result.progressPath}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(error.message);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
68
|
+
await main();
|
|
69
|
+
}
|