@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,432 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ const WORKFLOW_SCOPES = [
5
+ "read_files",
6
+ "write_files",
7
+ "read_themes",
8
+ "write_themes",
9
+ "read_products",
10
+ "write_products",
11
+ "read_publications",
12
+ "write_publications",
13
+ "read_content",
14
+ "write_content",
15
+ "read_online_store_navigation",
16
+ "write_online_store_navigation"
17
+ ];
18
+
19
+ const SCOPE_PROBE_QUERY =
20
+ "query StorewrightScopeProbe { currentAppInstallation { accessScopes { handle } } }";
21
+
22
+ const SHOP_QUERY = "query StorewrightPreflight { shop { id name myshopifyDomain } }";
23
+
24
+ const INSTALLER_COMMANDS = {
25
+ npm: "install -g @shopify/cli@latest",
26
+ pnpm: "install -g @shopify/cli@latest",
27
+ yarn: "global add @shopify/cli@latest",
28
+ brew: "tap shopify/shopify && brew install shopify-cli"
29
+ };
30
+
31
+ const DEFAULT_COMMAND_TIMEOUT_MS = 60_000;
32
+ const DEFAULT_AUTH_TIMEOUT_MS = 300_000;
33
+
34
+ function usage() {
35
+ return "Usage: shopify-access-preflight.mjs --store <myshopify-domain> [--command-timeout-ms <ms>] [--auth-timeout-ms <ms>]";
36
+ }
37
+
38
+ function parsePositiveInteger(value, name) {
39
+ if (!/^[1-9]\d*$/.test(String(value ?? ""))) {
40
+ return { error: `${name} must be a positive integer.` };
41
+ }
42
+ const parsed = Number.parseInt(value, 10);
43
+ if (!Number.isSafeInteger(parsed)) {
44
+ return { error: `${name} must be a positive integer.` };
45
+ }
46
+ return { value: parsed };
47
+ }
48
+
49
+ function parseArgs(argv) {
50
+ let store = "";
51
+ let commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS;
52
+ let authTimeoutMs = DEFAULT_AUTH_TIMEOUT_MS;
53
+ for (let i = 0; i < argv.length; i += 1) {
54
+ const arg = argv[i];
55
+ if (arg === "--store") {
56
+ store = argv[i + 1] ?? "";
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg === "--command-timeout-ms") {
61
+ const parsed = parsePositiveInteger(argv[i + 1], "--command-timeout-ms");
62
+ if (parsed.error) return parsed;
63
+ commandTimeoutMs = parsed.value;
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (arg === "--auth-timeout-ms") {
68
+ const parsed = parsePositiveInteger(argv[i + 1], "--auth-timeout-ms");
69
+ if (parsed.error) return parsed;
70
+ authTimeoutMs = parsed.value;
71
+ i += 1;
72
+ continue;
73
+ }
74
+ if (arg === "-h" || arg === "--help") {
75
+ return { help: true, store, commandTimeoutMs, authTimeoutMs };
76
+ }
77
+ return { error: `Unknown argument: ${arg}` };
78
+ }
79
+ if (!store) return { error: "Missing required --store argument." };
80
+ return { store, commandTimeoutMs, authTimeoutMs };
81
+ }
82
+
83
+ function parseJson(text) {
84
+ const raw = String(text ?? "").trim();
85
+ try {
86
+ return JSON.parse(raw);
87
+ } catch {
88
+ let lastParsed = null;
89
+ for (let start = 0; start < raw.length; start += 1) {
90
+ const opener = raw[start];
91
+ if (opener !== "{" && opener !== "[") continue;
92
+ const closer = opener === "{" ? "}" : "]";
93
+ let depth = 0;
94
+ let inString = false;
95
+ let escaped = false;
96
+ for (let index = start; index < raw.length; index += 1) {
97
+ const char = raw[index];
98
+ if (inString) {
99
+ if (escaped) {
100
+ escaped = false;
101
+ } else if (char === "\\") {
102
+ escaped = true;
103
+ } else if (char === "\"") {
104
+ inString = false;
105
+ }
106
+ continue;
107
+ }
108
+ if (char === "\"") {
109
+ inString = true;
110
+ } else if (char === opener) {
111
+ depth += 1;
112
+ } else if (char === closer) {
113
+ depth -= 1;
114
+ if (depth === 0) {
115
+ try {
116
+ lastParsed = JSON.parse(raw.slice(start, index + 1));
117
+ } catch {
118
+ // Keep scanning for the final valid JSON payload in noisy CLI output.
119
+ }
120
+ start = index;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ }
126
+ return lastParsed;
127
+ }
128
+ }
129
+
130
+ function collectScopes(value, scopes = new Set()) {
131
+ if (Array.isArray(value)) {
132
+ for (const item of value) collectScopes(item, scopes);
133
+ return scopes;
134
+ }
135
+ if (value && typeof value === "object") {
136
+ for (const item of Object.values(value)) collectScopes(item, scopes);
137
+ return scopes;
138
+ }
139
+ if (typeof value === "string") {
140
+ for (const part of value.split(/[,\s]+/)) {
141
+ if (/^[a-z]+_[a-z_]+$/.test(part)) scopes.add(part);
142
+ }
143
+ }
144
+ return scopes;
145
+ }
146
+
147
+ function effectiveScopes(rawScopes) {
148
+ const scopes = new Set(rawScopes);
149
+ for (const scope of rawScopes) {
150
+ if (scope.startsWith("write_")) {
151
+ scopes.add(scope.replace(/^write_/, "read_"));
152
+ }
153
+ }
154
+ return [...scopes].sort();
155
+ }
156
+
157
+ function runShopify(args, { timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS } = {}) {
158
+ const result = spawnSync("shopify", args, {
159
+ encoding: "utf8",
160
+ timeout: timeoutMs,
161
+ killSignal: "SIGTERM"
162
+ });
163
+ return processResult(["shopify", ...args], result, { timeoutMs });
164
+ }
165
+
166
+ function runShell(command, { timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS } = {}) {
167
+ const result = spawnSync("/bin/sh", ["-lc", command], {
168
+ encoding: "utf8",
169
+ timeout: timeoutMs,
170
+ killSignal: "SIGTERM"
171
+ });
172
+ return processResult(["/bin/sh", "-lc", command], result, { timeoutMs });
173
+ }
174
+
175
+ function commandSignals(commandParts, stdout, stderr, timedOut) {
176
+ const combinedOutput = `${stdout}\n${stderr}`;
177
+ const commandText = commandParts.join(" ");
178
+ const blockerHints = [];
179
+ if (/shopify store auth\b/.test(commandText) && timedOut) {
180
+ blockerHints.push("shopify-auth-timeout");
181
+ }
182
+ if (/Client network socket disconnected|ECONNRESET|ETIMEDOUT|ENOTFOUND|TLS connection/i.test(combinedOutput)) {
183
+ blockerHints.push("shopify-network-error");
184
+ }
185
+ return { blockerHints };
186
+ }
187
+
188
+ function processResult(commandParts, result, { timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS } = {}) {
189
+ const timedOut = result.error?.code === "ETIMEDOUT";
190
+ const exitCode = timedOut ? 1 : result.status ?? 1;
191
+ const parsedJson = parseJson(result.stdout);
192
+ const stdout = result.stdout ?? "";
193
+ const stderr = result.stderr ?? "";
194
+ const signals = commandSignals(commandParts, stdout, stderr, timedOut);
195
+ return {
196
+ command: commandParts.join(" "),
197
+ exitCode,
198
+ commandResult: timedOut ? "timeout" : exitCode === 0 ? "ok" : "error",
199
+ timedOut,
200
+ signal: result.signal ?? null,
201
+ timeoutMs,
202
+ stdout,
203
+ stderr,
204
+ parsedJson,
205
+ blockerHints: signals.blockerHints
206
+ };
207
+ }
208
+
209
+ function skippedCommand(command) {
210
+ return {
211
+ command,
212
+ exitCode: null,
213
+ commandResult: "skipped",
214
+ stdout: "",
215
+ stderr: "",
216
+ parsedJson: null
217
+ };
218
+ }
219
+
220
+ function commandSummary(command) {
221
+ return {
222
+ command: command.command,
223
+ exitCode: command.exitCode,
224
+ commandResult: command.commandResult,
225
+ timedOut: command.timedOut ?? false,
226
+ signal: command.signal ?? null,
227
+ timeoutMs: command.timeoutMs ?? null,
228
+ stdoutSummary: command.stdout.slice(0, 500),
229
+ stderrSummary: command.stderr.slice(0, 500),
230
+ blockerHints: command.blockerHints ?? []
231
+ };
232
+ }
233
+
234
+ function detectExecutable(name, { timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS } = {}) {
235
+ const lookup = runShell(`command -v ${name}`, { timeoutMs });
236
+ return {
237
+ name,
238
+ lookup,
239
+ path: lookup.commandResult === "ok" ? lookup.stdout.trim() : null
240
+ };
241
+ }
242
+
243
+ function createShopifyCliEvidence({ commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS } = {}) {
244
+ const shopify = detectExecutable("shopify", { timeoutMs: commandTimeoutMs });
245
+ const version = shopify.path ? runShopify(["version"], { timeoutMs: commandTimeoutMs }) : skippedCommand("shopify version");
246
+ const installerLookups = ["npm", "pnpm", "yarn", "brew"].map((name) => detectExecutable(name, { timeoutMs: commandTimeoutMs }));
247
+ const availableInstallers = installerLookups
248
+ .filter((installer) => installer.path)
249
+ .map((installer) => ({ name: installer.name, path: installer.path }));
250
+
251
+ return {
252
+ status: shopify.path ? "available" : "missing",
253
+ path: shopify.path,
254
+ version: commandSummary(version),
255
+ availableInstallers,
256
+ installCandidates: shopify.path
257
+ ? []
258
+ : availableInstallers.map((installer) => ({
259
+ name: installer.name,
260
+ command: `${installer.path} ${INSTALLER_COMMANDS[installer.name]}`
261
+ })),
262
+ commands: [
263
+ { name: "shopifyCliLookup", ...commandSummary(shopify.lookup) },
264
+ { name: "shopifyCliVersion", ...commandSummary(version) },
265
+ ...installerLookups.map((installer) => ({
266
+ name: `installerLookup:${installer.name}`,
267
+ ...commandSummary(installer.lookup)
268
+ }))
269
+ ]
270
+ };
271
+ }
272
+
273
+ function missingScopes(requestedScopes, scopes) {
274
+ const available = new Set(scopes);
275
+ return requestedScopes.filter((scope) => !available.has(scope));
276
+ }
277
+
278
+ function isRetryableScopeProbeFailure(command) {
279
+ return command.timedOut || /aborted before it completed|timed out|timeout|ECONNRESET|ETIMEDOUT/i.test(command.stderr);
280
+ }
281
+
282
+ function isAuthRequiredScopeProbeFailure(command) {
283
+ return /run .*store auth|no stored store auth|authentication required|missing .*scope|requires .*scope|access denied/i.test(command.stderr);
284
+ }
285
+
286
+ function run({
287
+ store,
288
+ requestedScopes = WORKFLOW_SCOPES,
289
+ commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS,
290
+ authTimeoutMs = DEFAULT_AUTH_TIMEOUT_MS
291
+ }) {
292
+ const shopifyCli = createShopifyCliEvidence({ commandTimeoutMs });
293
+ if (shopifyCli.status === "missing") {
294
+ return {
295
+ schemaVersion: "1.0.0",
296
+ store,
297
+ requestedScopes,
298
+ shopifyCli,
299
+ rawCurrentScopes: [],
300
+ effectiveCurrentScopes: [],
301
+ auth: {
302
+ action: "skipped",
303
+ commandResult: "skipped",
304
+ scopeProbeResult: "skipped",
305
+ rawVerifiedScopes: [],
306
+ verifiedScopes: [],
307
+ missingScopes: requestedScopes,
308
+ missingScopesBeforeAuth: requestedScopes
309
+ },
310
+ storeAccess: {
311
+ commandResult: "skipped",
312
+ exitCode: null,
313
+ themeCount: null
314
+ },
315
+ adminGraphqlAccess: {
316
+ commandResult: "skipped",
317
+ exitCode: null,
318
+ shop: null
319
+ },
320
+ commands: shopifyCli.commands
321
+ };
322
+ }
323
+
324
+ const currentScopeProbeCommands = [
325
+ runShopify(["store", "execute", "--store", store, "--query", SCOPE_PROBE_QUERY, "--json"], { timeoutMs: commandTimeoutMs })
326
+ ];
327
+ if (isRetryableScopeProbeFailure(currentScopeProbeCommands[0])) {
328
+ currentScopeProbeCommands.push(
329
+ runShopify(["store", "execute", "--store", store, "--query", SCOPE_PROBE_QUERY, "--json"], { timeoutMs: commandTimeoutMs })
330
+ );
331
+ }
332
+ const currentScopeProbe = currentScopeProbeCommands.at(-1);
333
+ const scopeProbeSucceeded = currentScopeProbe.commandResult === "ok";
334
+ const scopeProbeAuthRequired = !scopeProbeSucceeded && isAuthRequiredScopeProbeFailure(currentScopeProbe);
335
+ const rawCurrentScopes = scopeProbeSucceeded
336
+ ? [...collectScopes(currentScopeProbe.parsedJson)].sort()
337
+ : [];
338
+ const effectiveCurrentScopes = effectiveScopes(rawCurrentScopes);
339
+ const missingBeforeAuth = missingScopes(requestedScopes, effectiveCurrentScopes);
340
+ let authAction = "skipped";
341
+ let authCommand = null;
342
+ let rawVerifiedScopes = rawCurrentScopes;
343
+ let verifiedScopes = effectiveCurrentScopes;
344
+
345
+ if (missingBeforeAuth.length > 0 && (scopeProbeSucceeded || scopeProbeAuthRequired)) {
346
+ authAction = "requested";
347
+ authCommand = runShopify([
348
+ "store",
349
+ "auth",
350
+ "--store",
351
+ store,
352
+ "--scopes",
353
+ requestedScopes.join(","),
354
+ "--json"
355
+ ], { timeoutMs: authTimeoutMs });
356
+ rawVerifiedScopes = [...collectScopes(authCommand.parsedJson)].sort();
357
+ verifiedScopes = effectiveScopes(rawVerifiedScopes);
358
+ }
359
+
360
+ const themeList = runShopify(["theme", "list", "--store", store, "--json"], { timeoutMs: commandTimeoutMs });
361
+ const adminGraphql = runShopify([
362
+ "store",
363
+ "execute",
364
+ "--store",
365
+ store,
366
+ "--query",
367
+ SHOP_QUERY,
368
+ "--json"
369
+ ], { timeoutMs: commandTimeoutMs });
370
+
371
+ return {
372
+ schemaVersion: "1.0.0",
373
+ store,
374
+ requestedScopes,
375
+ shopifyCli,
376
+ rawCurrentScopes,
377
+ effectiveCurrentScopes,
378
+ auth: {
379
+ action: authAction,
380
+ commandResult: authCommand?.commandResult ?? "skipped",
381
+ approvalState: authCommand
382
+ ? authCommand.commandResult === "ok"
383
+ ? "approved"
384
+ : authCommand.timedOut
385
+ ? "timeout"
386
+ : "unknown"
387
+ : "not-needed",
388
+ scopeProbeResult: currentScopeProbe.commandResult,
389
+ rawVerifiedScopes,
390
+ verifiedScopes,
391
+ missingScopes: missingScopes(requestedScopes, verifiedScopes),
392
+ missingScopesBeforeAuth: missingBeforeAuth
393
+ },
394
+ storeAccess: {
395
+ commandResult: themeList.commandResult,
396
+ exitCode: themeList.exitCode,
397
+ themeCount: Array.isArray(themeList.parsedJson) ? themeList.parsedJson.length : null,
398
+ blockerId: null
399
+ },
400
+ adminGraphqlAccess: {
401
+ commandResult: adminGraphql.commandResult,
402
+ exitCode: adminGraphql.exitCode,
403
+ shop: adminGraphql.parsedJson?.data?.shop ?? adminGraphql.parsedJson?.shop ?? null
404
+ },
405
+ commands: [
406
+ ...shopifyCli.commands,
407
+ ...currentScopeProbeCommands.map((command, index) => ({
408
+ name: index === 0 ? "currentScopeProbe" : `currentScopeProbeRetry${index}`,
409
+ ...commandSummary(command)
410
+ })),
411
+ ...(authAction === "requested" ? [{ name: "workflowScopeAuth", ...commandSummary(authCommand) }] : []),
412
+ { name: "themeList", ...commandSummary(themeList) },
413
+ { name: "adminGraphqlRead", ...commandSummary(adminGraphql) }
414
+ ]
415
+ };
416
+ }
417
+
418
+ const parsed = parseArgs(process.argv.slice(2));
419
+ if (parsed.help) {
420
+ console.log(usage());
421
+ process.exit(0);
422
+ }
423
+ if (parsed.error) {
424
+ console.error(parsed.error);
425
+ process.exit(2);
426
+ }
427
+
428
+ process.stdout.write(`${JSON.stringify(run({
429
+ store: parsed.store,
430
+ commandTimeoutMs: parsed.commandTimeoutMs,
431
+ authTimeoutMs: parsed.authTimeoutMs
432
+ }), null, 2)}\n`);