@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.
Files changed (67) hide show
  1. package/README.md +19 -0
  2. package/VERSION +1 -0
  3. package/bin/storewright.mjs +62 -0
  4. package/contracts/action-registry.json +175 -0
  5. package/contracts/capability-registry.json +63 -0
  6. package/contracts/workflow-manifest.json +207 -0
  7. package/lib/cli/storewright-cli.mjs +259 -0
  8. package/lib/internal/launch-envelope.mjs +223 -0
  9. package/lib/internal/multi-agent-contracts.mjs +137 -0
  10. package/lib/internal/operation-ledger.mjs +190 -0
  11. package/lib/internal/pricing/default-preview-pricing.mjs +181 -0
  12. package/lib/internal/run-state-helpers.mjs +313 -0
  13. package/lib/internal/shopify-operation-adapter.mjs +456 -0
  14. package/package.json +38 -0
  15. package/schemas/action-registry.schema.json +11 -0
  16. package/schemas/agent-report.schema.json +14 -0
  17. package/schemas/approval-grant.schema.json +16 -0
  18. package/schemas/base-theme-report.schema.json +25 -0
  19. package/schemas/brand-identity.schema.json +142 -0
  20. package/schemas/capability-registry.schema.json +11 -0
  21. package/schemas/competitor-audit.schema.json +38 -0
  22. package/schemas/design-direction.schema.json +64 -0
  23. package/schemas/external-operation.schema.json +34 -0
  24. package/schemas/intake-blocked-report.schema.json +76 -0
  25. package/schemas/launch-envelope.schema.json +25 -0
  26. package/schemas/launch-readiness.schema.json +73 -0
  27. package/schemas/media-file-inspection-report.schema.json +223 -0
  28. package/schemas/media-manifest.schema.json +84 -0
  29. package/schemas/merchandising-brief.schema.json +27 -0
  30. package/schemas/normalized-product-catalog.schema.json +42 -0
  31. package/schemas/product-content-generation-input.schema.json +40 -0
  32. package/schemas/product-content-generation-output.schema.json +43 -0
  33. package/schemas/raw-product-candidates.schema.json +32 -0
  34. package/schemas/shopify-access-preflight-report.schema.json +213 -0
  35. package/schemas/shopify-content-sync-report.schema.json +190 -0
  36. package/schemas/shopify-media-map.schema.json +87 -0
  37. package/schemas/shopify-media-upload-report.schema.json +96 -0
  38. package/schemas/shopify-operation-request.schema.json +81 -0
  39. package/schemas/shopify-preflight-report.schema.json +187 -0
  40. package/schemas/store-blueprint.schema.json +112 -0
  41. package/schemas/store-content-generation-output.schema.json +102 -0
  42. package/schemas/store-intake.schema.json +205 -0
  43. package/schemas/store-ops-plan.schema.json +82 -0
  44. package/schemas/storefront-preview-review.schema.json +227 -0
  45. package/schemas/supplier-access-report.schema.json +36 -0
  46. package/schemas/supplier-extraction-report.schema.json +185 -0
  47. package/schemas/theme-build-report.schema.json +43 -0
  48. package/schemas/theme-code-change-summary.schema.json +65 -0
  49. package/schemas/theme-plan.schema.json +26 -0
  50. package/schemas/theme-push-report.schema.json +151 -0
  51. package/schemas/theme-workspace-validation-report.schema.json +61 -0
  52. package/schemas/workflow-manifest.schema.json +29 -0
  53. package/scripts/audit-run-state.mjs +472 -0
  54. package/scripts/execute-shopify-operation.mjs +190 -0
  55. package/scripts/generate-image-assets-openai.mjs +342 -0
  56. package/scripts/generate-media-assets.mjs +121 -0
  57. package/scripts/init-run-state.mjs +69 -0
  58. package/scripts/inspect-media-files.mjs +334 -0
  59. package/scripts/prepare-launch-envelope.mjs +47 -0
  60. package/scripts/shopify-access-preflight.mjs +432 -0
  61. package/scripts/upload-shopify-media.mjs +831 -0
  62. package/scripts/validate-agent-report.mjs +46 -0
  63. package/scripts/validate-artifact.mjs +196 -0
  64. package/scripts/validate-launch-envelope.mjs +50 -0
  65. package/scripts/validate-registries.mjs +50 -0
  66. package/scripts/validate-workflow-manifest.mjs +38 -0
  67. package/scripts/version.mjs +192 -0
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const defaultDimensionThreshold = 300;
9
+
10
+ function parseArgs(argv) {
11
+ const args = {};
12
+ for (let index = 0; index < argv.length; index += 1) {
13
+ const arg = argv[index];
14
+ if (arg === "--run-dir") {
15
+ args.runDir = argv[index + 1];
16
+ index += 1;
17
+ } else if (arg === "--supplier-extraction-report" || arg === "--supplier-extraction") {
18
+ args.supplierExtractionPath = argv[index + 1];
19
+ index += 1;
20
+ } else if (arg === "--media-manifest" || arg === "--manifest") {
21
+ args.mediaManifestPath = argv[index + 1];
22
+ index += 1;
23
+ } else if (arg === "--output-report") {
24
+ args.outputReportPath = argv[index + 1];
25
+ index += 1;
26
+ } else if (arg === "--dimension-threshold") {
27
+ args.dimensionThreshold = Number.parseInt(argv[index + 1], 10);
28
+ index += 1;
29
+ }
30
+ }
31
+ return args;
32
+ }
33
+
34
+ function resolveRunPath(runDir, filePath) {
35
+ if (!filePath || isAbsolute(filePath)) return filePath;
36
+ return join(runDir, filePath);
37
+ }
38
+
39
+ async function readJsonIfPresent(filePath) {
40
+ if (!filePath) return null;
41
+ return JSON.parse(await readFile(filePath, "utf8"));
42
+ }
43
+
44
+ function sourceArtifactName(filePath, fallback) {
45
+ return filePath?.split("/").at(-1) ?? fallback;
46
+ }
47
+
48
+ function collectSupplierAssets(report, sourceArtifact) {
49
+ const assets = [];
50
+ for (const item of report?.supplierExtraction?.items ?? []) {
51
+ const evidence = item.localMediaEvidence ?? [];
52
+ if (evidence.length > 0) {
53
+ evidence.forEach((media, index) => {
54
+ assets.push({
55
+ assetRef: `${item.candidateId ?? "candidate"}:${index + 1}`,
56
+ candidateId: item.candidateId,
57
+ sourceArtifact,
58
+ sourceUrl: media.sourceUrl,
59
+ localPath: media.localPath,
60
+ declaredBytes: media.bytes
61
+ });
62
+ });
63
+ continue;
64
+ }
65
+
66
+ for (const [index, localPath] of (item.localMediaPaths ?? []).entries()) {
67
+ assets.push({
68
+ assetRef: `${item.candidateId ?? "candidate"}:${index + 1}`,
69
+ candidateId: item.candidateId,
70
+ sourceArtifact,
71
+ sourceUrl: item.imageSignals?.[index],
72
+ localPath
73
+ });
74
+ }
75
+ }
76
+ return assets;
77
+ }
78
+
79
+ function collectManifestAssets(manifest, sourceArtifact) {
80
+ return (manifest?.media?.assets ?? []).map((asset, index) => ({
81
+ assetRef: asset.assetId ?? `manifest:${index + 1}`,
82
+ candidateId: asset.candidateId,
83
+ sourceArtifact,
84
+ sourceUrl: asset.provenance?.sourceUrl,
85
+ localPath: asset.localPath,
86
+ declaredBytes: asset.provenance?.bytes
87
+ }));
88
+ }
89
+
90
+ function sha256(bytes) {
91
+ return createHash("sha256").update(bytes).digest("hex");
92
+ }
93
+
94
+ function readUInt24LE(buffer, offset) {
95
+ return buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16);
96
+ }
97
+
98
+ function inspectPng(buffer) {
99
+ const signature = "89504e470d0a1a0a";
100
+ if (buffer.length < 24 || buffer.subarray(0, 8).toString("hex") !== signature) return null;
101
+ return {
102
+ detectedMimeType: "image/png",
103
+ width: buffer.readUInt32BE(16),
104
+ height: buffer.readUInt32BE(20)
105
+ };
106
+ }
107
+
108
+ function inspectJpeg(buffer) {
109
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return null;
110
+ let offset = 2;
111
+ while (offset + 9 < buffer.length) {
112
+ if (buffer[offset] !== 0xff) {
113
+ offset += 1;
114
+ continue;
115
+ }
116
+ const marker = buffer[offset + 1];
117
+ const length = buffer.readUInt16BE(offset + 2);
118
+ const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
119
+ if (isStartOfFrame) {
120
+ return {
121
+ detectedMimeType: "image/jpeg",
122
+ height: buffer.readUInt16BE(offset + 5),
123
+ width: buffer.readUInt16BE(offset + 7)
124
+ };
125
+ }
126
+ if (length < 2) break;
127
+ offset += 2 + length;
128
+ }
129
+ return { detectedMimeType: "image/jpeg" };
130
+ }
131
+
132
+ function inspectWebp(buffer) {
133
+ if (
134
+ buffer.length < 30 ||
135
+ buffer.subarray(0, 4).toString("ascii") !== "RIFF" ||
136
+ buffer.subarray(8, 12).toString("ascii") !== "WEBP"
137
+ ) {
138
+ return null;
139
+ }
140
+
141
+ const chunk = buffer.subarray(12, 16).toString("ascii");
142
+ if (chunk === "VP8X" && buffer.length >= 30) {
143
+ return {
144
+ detectedMimeType: "image/webp",
145
+ width: readUInt24LE(buffer, 24) + 1,
146
+ height: readUInt24LE(buffer, 27) + 1
147
+ };
148
+ }
149
+ if (chunk === "VP8 " && buffer.length >= 30 && buffer[23] === 0x9d && buffer[24] === 0x01 && buffer[25] === 0x2a) {
150
+ return {
151
+ detectedMimeType: "image/webp",
152
+ width: buffer.readUInt16LE(26) & 0x3fff,
153
+ height: buffer.readUInt16LE(28) & 0x3fff
154
+ };
155
+ }
156
+ if (chunk === "VP8L" && buffer.length >= 25 && buffer[20] === 0x2f) {
157
+ const bits = buffer.readUInt32LE(21);
158
+ return {
159
+ detectedMimeType: "image/webp",
160
+ width: (bits & 0x3fff) + 1,
161
+ height: ((bits >> 14) & 0x3fff) + 1
162
+ };
163
+ }
164
+ return { detectedMimeType: "image/webp" };
165
+ }
166
+
167
+ function inspectImageHeader(buffer) {
168
+ return inspectPng(buffer) ?? inspectJpeg(buffer) ?? inspectWebp(buffer) ?? {};
169
+ }
170
+
171
+ function isPlatformOrShopAssetUrl(sourceUrl) {
172
+ if (!sourceUrl) return false;
173
+ return [
174
+ /gtms\d*\.alicdn\.com/i,
175
+ /\/tps\//i,
176
+ /-tps-\d+-\d+/i,
177
+ /\/s\.gif(?:$|\?)/i,
178
+ /avatar/i,
179
+ /shopmanager/i,
180
+ /logo/i,
181
+ /\/tfs\//i
182
+ ].some((pattern) => pattern.test(sourceUrl));
183
+ }
184
+
185
+ function addFinding(asset, code, evidence) {
186
+ asset.findingCodes.push(code);
187
+ asset.findings.push({ code, evidence });
188
+ }
189
+
190
+ async function inspectAsset(asset, { runDir, dimensionThreshold }) {
191
+ const inspected = {
192
+ assetRef: asset.assetRef,
193
+ ...(asset.candidateId ? { candidateId: asset.candidateId } : {}),
194
+ ...(asset.sourceArtifact ? { sourceArtifact: asset.sourceArtifact } : {}),
195
+ ...(asset.sourceUrl ? { sourceUrl: asset.sourceUrl } : {}),
196
+ localPath: asset.localPath,
197
+ fileExists: false,
198
+ findingCodes: [],
199
+ findings: []
200
+ };
201
+
202
+ if (!asset.sourceUrl) {
203
+ addFinding(inspected, "missing-source-url", "No sourceUrl was present in the input artifact.");
204
+ } else if (isPlatformOrShopAssetUrl(asset.sourceUrl)) {
205
+ addFinding(inspected, "platform-or-shop-asset-url", asset.sourceUrl);
206
+ }
207
+
208
+ const absolutePath = resolveRunPath(runDir, asset.localPath);
209
+ if (!absolutePath || !existsSync(absolutePath)) {
210
+ addFinding(inspected, "missing-local-file", asset.localPath ?? "");
211
+ return inspected;
212
+ }
213
+
214
+ const bytes = await readFile(absolutePath);
215
+ const header = inspectImageHeader(bytes);
216
+ inspected.fileExists = true;
217
+ inspected.bytes = bytes.length;
218
+ inspected.sha256 = sha256(bytes);
219
+ if (header.detectedMimeType) inspected.detectedMimeType = header.detectedMimeType;
220
+ if (Number.isFinite(header.width)) inspected.width = header.width;
221
+ if (Number.isFinite(header.height)) inspected.height = header.height;
222
+
223
+ if (!header.detectedMimeType) {
224
+ addFinding(inspected, "unsupported-image-header", asset.localPath);
225
+ }
226
+ if (
227
+ Number.isFinite(header.width) &&
228
+ Number.isFinite(header.height) &&
229
+ (header.width < dimensionThreshold || header.height < dimensionThreshold)
230
+ ) {
231
+ addFinding(inspected, "below-dimension-threshold", `${header.width}x${header.height} < ${dimensionThreshold}x${dimensionThreshold}`);
232
+ }
233
+
234
+ return inspected;
235
+ }
236
+
237
+ function applyDuplicateFindings(assets) {
238
+ const byHash = new Map();
239
+ for (const asset of assets) {
240
+ if (!asset.sha256) continue;
241
+ const group = byHash.get(asset.sha256) ?? [];
242
+ group.push(asset);
243
+ byHash.set(asset.sha256, group);
244
+ }
245
+
246
+ const duplicateHashGroups = [];
247
+ for (const [hash, group] of byHash) {
248
+ const candidateIds = [...new Set(group.map((asset) => asset.candidateId).filter(Boolean))];
249
+ if (group.length < 2 || candidateIds.length < 2) continue;
250
+ const groupId = `duplicate-${duplicateHashGroups.length + 1}`;
251
+ duplicateHashGroups.push({
252
+ groupId,
253
+ sha256: hash,
254
+ assetRefs: group.map((asset) => asset.assetRef),
255
+ candidateIds
256
+ });
257
+ for (const asset of group) {
258
+ asset.duplicateHashGroupId = groupId;
259
+ addFinding(asset, "duplicate-across-candidates", groupId);
260
+ }
261
+ }
262
+
263
+ return duplicateHashGroups;
264
+ }
265
+
266
+ export async function inspectMediaFiles({
267
+ runDir,
268
+ supplierExtractionPath,
269
+ mediaManifestPath,
270
+ outputReportPath,
271
+ dimensionThreshold = defaultDimensionThreshold
272
+ }) {
273
+ if (!runDir) throw new Error("Missing --run-dir argument");
274
+ if (!supplierExtractionPath && !mediaManifestPath) {
275
+ throw new Error("Missing media input artifact: pass --supplier-extraction-report or --media-manifest");
276
+ }
277
+
278
+ const supplierExtraction = await readJsonIfPresent(supplierExtractionPath);
279
+ const mediaManifest = await readJsonIfPresent(mediaManifestPath);
280
+ const sourceInputArtifacts = [
281
+ ...(supplierExtractionPath ? [supplierExtractionPath] : []),
282
+ ...(mediaManifestPath ? [mediaManifestPath] : [])
283
+ ];
284
+ const runId = supplierExtraction?.runId ?? mediaManifest?.runId ?? "unknown";
285
+ const inputAssets = [
286
+ ...collectSupplierAssets(supplierExtraction, sourceArtifactName(supplierExtractionPath, "supplier-extraction-report.json")),
287
+ ...collectManifestAssets(mediaManifest, sourceArtifactName(mediaManifestPath, "media-manifest.json"))
288
+ ];
289
+ const assets = [];
290
+ for (const asset of inputAssets) {
291
+ assets.push(await inspectAsset(asset, { runDir, dimensionThreshold }));
292
+ }
293
+ const duplicateHashGroups = applyDuplicateFindings(assets);
294
+
295
+ const report = {
296
+ schemaVersion: "1.0.0",
297
+ runId,
298
+ authoring: {
299
+ mode: "worker-authored",
300
+ createdBy: "media-file-inspection-adapter",
301
+ sourceInputArtifacts
302
+ },
303
+ mediaFileInspection: {
304
+ dimensionThreshold,
305
+ summary: {
306
+ assetCount: assets.length,
307
+ missingFileCount: assets.filter((asset) => asset.findingCodes.includes("missing-local-file")).length,
308
+ duplicateHashGroupCount: duplicateHashGroups.length,
309
+ findingCount: assets.reduce((total, asset) => total + asset.findingCodes.length, 0)
310
+ },
311
+ duplicateHashGroups,
312
+ assets
313
+ }
314
+ };
315
+
316
+ const resolvedOutputPath = outputReportPath ?? join(runDir, "media-file-inspection-report.json");
317
+ await mkdir(dirname(resolvedOutputPath), { recursive: true });
318
+ await writeFile(resolvedOutputPath, `${JSON.stringify(report, null, 2)}\n`);
319
+ return { outputReportPath: resolvedOutputPath, assetCount: assets.length };
320
+ }
321
+
322
+ async function main() {
323
+ try {
324
+ const result = await inspectMediaFiles(parseArgs(process.argv.slice(2)));
325
+ console.log(JSON.stringify(result));
326
+ } catch (error) {
327
+ console.error(error.message);
328
+ process.exitCode = 1;
329
+ }
330
+ }
331
+
332
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
333
+ await main();
334
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { createLaunchEnvelope } from "../lib/internal/launch-envelope.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 === "--input") {
14
+ options.inputPath = value;
15
+ index += 1;
16
+ } else if (arg === "--run-dir") {
17
+ options.runDir = value;
18
+ index += 1;
19
+ } else if (arg === "--package-root") {
20
+ options.packageRoot = value;
21
+ index += 1;
22
+ }
23
+ }
24
+ return options;
25
+ }
26
+
27
+ export async function prepareLaunchEnvelope({ packageRoot = join(import.meta.dirname, ".."), runDir, inputPath }) {
28
+ if (!inputPath) {
29
+ throw new Error("Missing --input launch envelope request JSON");
30
+ }
31
+ const request = JSON.parse(await readFile(inputPath, "utf8"));
32
+ return createLaunchEnvelope({ packageRoot, ...request, runDir: runDir ?? request.runDir });
33
+ }
34
+
35
+ async function main() {
36
+ try {
37
+ const result = await prepareLaunchEnvelope(parseArgs(process.argv.slice(2)));
38
+ console.log(JSON.stringify(result, null, 2));
39
+ } catch (error) {
40
+ console.error(error.message);
41
+ process.exitCode = 1;
42
+ }
43
+ }
44
+
45
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
46
+ await main();
47
+ }