shop-cli 0.1.1 → 0.1.3
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/dist/cli/approvalRequired.d.ts +30 -0
- package/dist/cli/approvalRequired.js +87 -0
- package/dist/cli/errors.d.ts +4 -1
- package/dist/cli/errors.js +3 -1
- package/dist/cli/router.js +3 -0
- package/dist/cli/verbs/collections.js +2 -2
- package/dist/cli/verbs/graphql.js +2 -0
- package/dist/cli/verbs/products.js +6 -6
- package/dist/cli/verbs/reverse-deliveries.js +6 -1
- package/dist/cli/verbs/reverse-fulfillment-orders.js +41 -16
- package/dist/cli.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OutputFormat } from './output';
|
|
2
|
+
type ApprovalRequiredAction = Record<string, unknown> & {
|
|
3
|
+
type?: unknown;
|
|
4
|
+
status?: unknown;
|
|
5
|
+
id?: unknown;
|
|
6
|
+
executed?: unknown;
|
|
7
|
+
contentType?: unknown;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type ApprovalRequiredInfo = {
|
|
11
|
+
code: 'APPROVAL_REQUIRED' | 'ACTION_REQUIRED';
|
|
12
|
+
action: ApprovalRequiredAction;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Generic GraphQL proxy contract for deferred execution.
|
|
16
|
+
*
|
|
17
|
+
* When a proxy requires human approval before executing an operation, it may return a
|
|
18
|
+
* standard GraphQL response with an `errors[]` entry that includes:
|
|
19
|
+
* - `error.extensions.code === "APPROVAL_REQUIRED"` (also accepts "ACTION_REQUIRED")
|
|
20
|
+
* - `error.extensions.action` describing the approval workflow
|
|
21
|
+
*
|
|
22
|
+
* In this case, the operation has NOT executed yet. shop-cli surfaces a purpose-built
|
|
23
|
+
* error payload and exits with code 3.
|
|
24
|
+
*/
|
|
25
|
+
export declare const findApprovalRequired: (errors: unknown[] | undefined | null) => ApprovalRequiredInfo | undefined;
|
|
26
|
+
export declare const maybeThrowApprovalRequired: ({ format, errors, }: {
|
|
27
|
+
format: OutputFormat;
|
|
28
|
+
errors: unknown[] | undefined | null;
|
|
29
|
+
}) => void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var approvalRequired_exports = {};
|
|
20
|
+
__export(approvalRequired_exports, {
|
|
21
|
+
findApprovalRequired: () => findApprovalRequired,
|
|
22
|
+
maybeThrowApprovalRequired: () => maybeThrowApprovalRequired
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(approvalRequired_exports);
|
|
25
|
+
var import_output = require("./output");
|
|
26
|
+
var import_errors = require("./errors");
|
|
27
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
const findApprovalRequired = (errors) => {
|
|
29
|
+
if (!Array.isArray(errors)) return void 0;
|
|
30
|
+
for (const err of errors) {
|
|
31
|
+
if (!isPlainObject(err)) continue;
|
|
32
|
+
const extensions = err.extensions;
|
|
33
|
+
if (!isPlainObject(extensions)) continue;
|
|
34
|
+
const code = extensions.code;
|
|
35
|
+
if (code !== "APPROVAL_REQUIRED" && code !== "ACTION_REQUIRED") continue;
|
|
36
|
+
const action = extensions.action;
|
|
37
|
+
if (!isPlainObject(action)) continue;
|
|
38
|
+
if (action.type !== "approval") continue;
|
|
39
|
+
if (action.status !== "pending") continue;
|
|
40
|
+
if (action.executed !== false) continue;
|
|
41
|
+
if (typeof action.id !== "string" || !action.id.trim()) continue;
|
|
42
|
+
if (typeof action.body !== "string" || !action.body.trim()) continue;
|
|
43
|
+
return { code, action };
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
};
|
|
47
|
+
const writeHumanApprovalMessage = ({ action }) => {
|
|
48
|
+
const id = String(action.id);
|
|
49
|
+
const body = typeof action.body === "string" ? action.body.trim() : "";
|
|
50
|
+
const lines = [
|
|
51
|
+
"Approval required: this operation was NOT executed yet.",
|
|
52
|
+
`Approval id: ${id}`,
|
|
53
|
+
"Execution will happen after approval is granted (out-of-band)."
|
|
54
|
+
];
|
|
55
|
+
if (body) {
|
|
56
|
+
lines.push("", body);
|
|
57
|
+
}
|
|
58
|
+
process.stderr.write(lines.join("\n").trimEnd() + "\n");
|
|
59
|
+
};
|
|
60
|
+
const maybeThrowApprovalRequired = ({
|
|
61
|
+
format,
|
|
62
|
+
errors
|
|
63
|
+
}) => {
|
|
64
|
+
const approval = findApprovalRequired(errors);
|
|
65
|
+
if (!approval) return;
|
|
66
|
+
if (format === "json" || format === "jsonl") {
|
|
67
|
+
(0, import_output.printJsonError)(
|
|
68
|
+
{
|
|
69
|
+
ok: false,
|
|
70
|
+
error: {
|
|
71
|
+
code: approval.code,
|
|
72
|
+
action: approval.action
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
format === "json"
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
writeHumanApprovalMessage(approval);
|
|
79
|
+
}
|
|
80
|
+
throw new import_errors.CliError("", 3, { silent: true });
|
|
81
|
+
};
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
findApprovalRequired,
|
|
85
|
+
maybeThrowApprovalRequired
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=approvalRequired.js.map
|
package/dist/cli/errors.d.ts
CHANGED
package/dist/cli/errors.js
CHANGED
|
@@ -23,9 +23,11 @@ __export(errors_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(errors_exports);
|
|
24
24
|
class CliError extends Error {
|
|
25
25
|
exitCode;
|
|
26
|
-
|
|
26
|
+
silent;
|
|
27
|
+
constructor(message, exitCode = 1, { silent = false } = {}) {
|
|
27
28
|
super(message);
|
|
28
29
|
this.exitCode = exitCode;
|
|
30
|
+
this.silent = silent;
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/cli/router.js
CHANGED
|
@@ -29,6 +29,7 @@ var import_admin_2026_04 = require("../generated/admin-2026-04");
|
|
|
29
29
|
var import_admin_2026_042 = require("../generated/admin-2026-04");
|
|
30
30
|
var import_errors = require("./errors");
|
|
31
31
|
var import_output = require("./output");
|
|
32
|
+
var import_approvalRequired = require("./approvalRequired");
|
|
32
33
|
var import_introspection = require("./introspection");
|
|
33
34
|
var import_format = require("./introspection/format");
|
|
34
35
|
var import_resources = require("./introspection/resources");
|
|
@@ -442,6 +443,7 @@ const runQuery = async (ctx, request) => {
|
|
|
442
443
|
return await ctx.client.query(request);
|
|
443
444
|
} catch (err) {
|
|
444
445
|
if (err instanceof import_admin_2026_042.GenqlError) {
|
|
446
|
+
(0, import_approvalRequired.maybeThrowApprovalRequired)({ format: ctx.format, errors: err.errors });
|
|
445
447
|
if (ctx.warnMissingAccessToken && hasNotAuthorizedError(err)) {
|
|
446
448
|
console.error("SHOPIFY_ACCESS_TOKEN not set");
|
|
447
449
|
}
|
|
@@ -461,6 +463,7 @@ const runMutation = async (ctx, request) => {
|
|
|
461
463
|
return await ctx.client.mutation(request);
|
|
462
464
|
} catch (err) {
|
|
463
465
|
if (err instanceof import_admin_2026_042.GenqlError) {
|
|
466
|
+
(0, import_approvalRequired.maybeThrowApprovalRequired)({ format: ctx.format, errors: err.errors });
|
|
464
467
|
if (ctx.warnMissingAccessToken && hasNotAuthorizedError(err)) {
|
|
465
468
|
console.error("SHOPIFY_ACCESS_TOKEN not set");
|
|
466
469
|
}
|
|
@@ -447,7 +447,7 @@ const runCollections = async ({
|
|
|
447
447
|
const pos = Number(item.slice(idx + 1).trim());
|
|
448
448
|
if (!productId) throw new import_errors.CliError("--move productId cannot be empty", 2);
|
|
449
449
|
if (!Number.isFinite(pos) || pos < 0) throw new import_errors.CliError("--move newPosition must be a non-negative number", 2);
|
|
450
|
-
parsedMoves.push({ id: (0, import_gid.coerceGid)(productId, "Product"), newPosition: Math.floor(pos) });
|
|
450
|
+
parsedMoves.push({ id: (0, import_gid.coerceGid)(productId, "Product"), newPosition: String(Math.floor(pos)) });
|
|
451
451
|
}
|
|
452
452
|
moves = parsedMoves;
|
|
453
453
|
}
|
|
@@ -460,7 +460,7 @@ const runCollections = async ({
|
|
|
460
460
|
if (typeof mid !== "string" || !mid.trim()) throw new import_errors.CliError(`moves[${i}].id is required`, 2);
|
|
461
461
|
const pos = Number(newPosition);
|
|
462
462
|
if (!Number.isFinite(pos) || pos < 0) throw new import_errors.CliError(`moves[${i}].newPosition must be a non-negative number`, 2);
|
|
463
|
-
return { id: mid.startsWith("gid://") ? mid : (0, import_gid.coerceGid)(mid, "Product"), newPosition: Math.floor(pos) };
|
|
463
|
+
return { id: mid.startsWith("gid://") ? mid : (0, import_gid.coerceGid)(mid, "Product"), newPosition: String(Math.floor(pos)) };
|
|
464
464
|
});
|
|
465
465
|
const result = await (0, import_router.runMutation)(ctx, {
|
|
466
466
|
collectionReorderProducts: {
|
|
@@ -26,6 +26,7 @@ var import_node_fs = require("node:fs");
|
|
|
26
26
|
var import_errors = require("../errors");
|
|
27
27
|
var import_output = require("../output");
|
|
28
28
|
var import_router = require("../router");
|
|
29
|
+
var import_approvalRequired = require("../approvalRequired");
|
|
29
30
|
var import_adminClient = require("../../adminClient");
|
|
30
31
|
var import_graphqlValidator = require("../../graphqlValidator");
|
|
31
32
|
const printHelp = () => {
|
|
@@ -272,6 +273,7 @@ const runGraphQL = async ({
|
|
|
272
273
|
throw new import_errors.CliError(`GraphQL request failed: ${err.message}`, 1);
|
|
273
274
|
}
|
|
274
275
|
if (response.errors && response.errors.length > 0) {
|
|
276
|
+
(0, import_approvalRequired.maybeThrowApprovalRequired)({ format: ctx.format, errors: response.errors });
|
|
275
277
|
(0, import_output.printJsonError)({ errors: response.errors, data: response.data }, ctx.format !== "raw");
|
|
276
278
|
throw new import_errors.CliError("GraphQL request returned errors", 1);
|
|
277
279
|
}
|
|
@@ -2262,13 +2262,13 @@ Missing <verb> for "products ${verb}"`, 2);
|
|
|
2262
2262
|
const raw = args.move;
|
|
2263
2263
|
const parsedMoves = [];
|
|
2264
2264
|
for (const item of raw) {
|
|
2265
|
-
const
|
|
2266
|
-
if (
|
|
2267
|
-
const mediaId =
|
|
2268
|
-
const pos = Number(
|
|
2265
|
+
const idx = item.lastIndexOf(":");
|
|
2266
|
+
if (idx <= 0 || idx === item.length - 1) throw new import_errors.CliError("--move must be <mediaId>:<newPosition>", 2);
|
|
2267
|
+
const mediaId = item.slice(0, idx).trim();
|
|
2268
|
+
const pos = Number(item.slice(idx + 1).trim());
|
|
2269
2269
|
if (!mediaId) throw new import_errors.CliError("--move mediaId cannot be empty", 2);
|
|
2270
2270
|
if (!Number.isFinite(pos) || pos < 0) throw new import_errors.CliError("--move newPosition must be a non-negative number", 2);
|
|
2271
|
-
parsedMoves.push({ id: normalizeMediaId(mediaId), newPosition: Math.floor(pos) });
|
|
2271
|
+
parsedMoves.push({ id: normalizeMediaId(mediaId), newPosition: String(Math.floor(pos)) });
|
|
2272
2272
|
}
|
|
2273
2273
|
moves = parsedMoves;
|
|
2274
2274
|
}
|
|
@@ -2285,7 +2285,7 @@ Missing <verb> for "products ${verb}"`, 2);
|
|
|
2285
2285
|
if (!Number.isFinite(pos) || pos < 0) {
|
|
2286
2286
|
throw new import_errors.CliError(`moves[${i}].newPosition must be a non-negative number`, 2);
|
|
2287
2287
|
}
|
|
2288
|
-
return { id: normalizeMediaId(id2), newPosition: Math.floor(pos) };
|
|
2288
|
+
return { id: normalizeMediaId(id2), newPosition: String(Math.floor(pos)) };
|
|
2289
2289
|
});
|
|
2290
2290
|
const result = await (0, import_router.runMutation)(ctx, {
|
|
2291
2291
|
productReorderMedia: {
|
|
@@ -60,7 +60,12 @@ const getReverseDeliverySelection = (view) => {
|
|
|
60
60
|
return reverseDeliverySummarySelection;
|
|
61
61
|
};
|
|
62
62
|
const parseLineItem = (value) => {
|
|
63
|
-
const
|
|
63
|
+
const idx = value.lastIndexOf(":");
|
|
64
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
65
|
+
throw new import_errors.CliError("--line-item must be <reverseFulfillmentOrderLineItemId>:<quantity>", 2);
|
|
66
|
+
}
|
|
67
|
+
const id = value.slice(0, idx).trim();
|
|
68
|
+
const qtyRaw = value.slice(idx + 1).trim();
|
|
64
69
|
if (!id || !qtyRaw) throw new import_errors.CliError("--line-item must be <reverseFulfillmentOrderLineItemId>:<quantity>", 2);
|
|
65
70
|
const quantity = Number(qtyRaw);
|
|
66
71
|
if (!Number.isFinite(quantity) || !Number.isInteger(quantity) || quantity <= 0) {
|
|
@@ -61,24 +61,49 @@ const getReverseFulfillmentOrderSelection = (view) => {
|
|
|
61
61
|
return reverseFulfillmentOrderSummarySelection;
|
|
62
62
|
};
|
|
63
63
|
const parseDisposition = (value) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const raw = value.trim();
|
|
65
|
+
const usage = () => new import_errors.CliError("--disposition must be <reverseFulfillmentOrderLineItemId>:<quantity>:<dispositionType>[:<locationId>]", 2);
|
|
66
|
+
const splitOnLastColonOrThrow = (s) => {
|
|
67
|
+
const idx = s.lastIndexOf(":");
|
|
68
|
+
if (idx <= 0 || idx === s.length - 1) throw usage();
|
|
69
|
+
return [s.slice(0, idx), s.slice(idx + 1)];
|
|
70
|
+
};
|
|
71
|
+
const parseCore = (core) => {
|
|
72
|
+
const [rest, dispositionTypeRaw] = splitOnLastColonOrThrow(core);
|
|
73
|
+
const [idRaw, quantityRaw] = splitOnLastColonOrThrow(rest);
|
|
74
|
+
const reverseFulfillmentOrderLineItemId = idRaw.trim();
|
|
75
|
+
const dispositionType = dispositionTypeRaw.trim();
|
|
76
|
+
const quantity = Number(quantityRaw.trim());
|
|
77
|
+
if (!reverseFulfillmentOrderLineItemId || !dispositionType) throw usage();
|
|
78
|
+
if (!Number.isFinite(quantity) || !Number.isInteger(quantity) || quantity <= 0) {
|
|
79
|
+
throw new import_errors.CliError("--disposition quantity must be a positive integer", 2);
|
|
80
|
+
}
|
|
81
|
+
return { reverseFulfillmentOrderLineItemId, quantity, dispositionType };
|
|
82
|
+
};
|
|
83
|
+
let firstErr;
|
|
84
|
+
try {
|
|
85
|
+
return parseCore(raw);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
firstErr = err;
|
|
67
88
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
const gidIdx = raw.lastIndexOf("gid://");
|
|
90
|
+
if (gidIdx > 0) {
|
|
91
|
+
const before = raw.slice(0, gidIdx);
|
|
92
|
+
const colonIdx = before.lastIndexOf(":");
|
|
93
|
+
if (colonIdx >= 0 && before.slice(colonIdx + 1).trim() === "") {
|
|
94
|
+
const core = raw.slice(0, colonIdx).trim();
|
|
95
|
+
const locationId = raw.slice(gidIdx).trim();
|
|
96
|
+
if (locationId) return { ...parseCore(core), locationId };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const [core, locationIdRaw] = splitOnLastColonOrThrow(raw);
|
|
101
|
+
const locationId = locationIdRaw.trim();
|
|
102
|
+
if (!locationId) throw usage();
|
|
103
|
+
return { ...parseCore(core.trim()), locationId };
|
|
104
|
+
} catch {
|
|
105
|
+
throw firstErr;
|
|
75
106
|
}
|
|
76
|
-
return {
|
|
77
|
-
reverseFulfillmentOrderLineItemId,
|
|
78
|
-
quantity,
|
|
79
|
-
dispositionType,
|
|
80
|
-
...locationId ? { locationId } : {}
|
|
81
|
-
};
|
|
82
107
|
};
|
|
83
108
|
const runReverseFulfillmentOrders = async ({
|
|
84
109
|
ctx,
|
package/dist/cli.js
CHANGED
|
@@ -224,12 +224,12 @@ Missing <verb> for "${resource}"`, 2);
|
|
|
224
224
|
See help:
|
|
225
225
|
${command} ${resource} ${verb} --help`;
|
|
226
226
|
}
|
|
227
|
-
throw new import_errors.CliError(message, err.exitCode);
|
|
227
|
+
throw new import_errors.CliError(message, err.exitCode, { silent: err.silent });
|
|
228
228
|
}
|
|
229
229
|
};
|
|
230
230
|
main().catch((err) => {
|
|
231
231
|
if (err instanceof import_errors.CliError) {
|
|
232
|
-
console.error(err.message);
|
|
232
|
+
if (!err.silent) console.error(err.message);
|
|
233
233
|
process.exit(err.exitCode);
|
|
234
234
|
}
|
|
235
235
|
console.error(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shop-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "An agent-friendly command-line interface for managing Shopify stores",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -63,4 +63,4 @@
|
|
|
63
63
|
"typescript": "^5.9.3",
|
|
64
64
|
"vitest": "^4.0.18"
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|