@supercycle/cli 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.
Files changed (77) hide show
  1. package/README.md +107 -0
  2. package/dist/cli.js +9 -0
  3. package/dist/commands/comments/delete.js +48 -0
  4. package/dist/commands/comments/index.js +3 -0
  5. package/dist/commands/conditions/index.js +3 -0
  6. package/dist/commands/conditions/list.js +20 -0
  7. package/dist/commands/custom-fields/definition.js +58 -0
  8. package/dist/commands/custom-fields/definitions.js +25 -0
  9. package/dist/commands/custom-fields/delete.js +45 -0
  10. package/dist/commands/custom-fields/index.js +3 -0
  11. package/dist/commands/custom-fields/set.js +43 -0
  12. package/dist/commands/custom-fields/update.js +19 -0
  13. package/dist/commands/cycles/comment.js +29 -0
  14. package/dist/commands/cycles/fulfil.js +24 -0
  15. package/dist/commands/cycles/get.js +50 -0
  16. package/dist/commands/cycles/index.js +3 -0
  17. package/dist/commands/cycles/list.js +124 -0
  18. package/dist/commands/cycles/overdue.js +25 -0
  19. package/dist/commands/cycles/pack.js +21 -0
  20. package/dist/commands/cycles/reassign.js +28 -0
  21. package/dist/commands/cycles/receive.js +23 -0
  22. package/dist/commands/cycles/reschedule.js +74 -0
  23. package/dist/commands/cycles/return.js +52 -0
  24. package/dist/commands/cycles/tag.js +45 -0
  25. package/dist/commands/cycles/to-fulfill.js +9 -0
  26. package/dist/commands/cycles/to-receive.js +9 -0
  27. package/dist/commands/cycles/today.js +18 -0
  28. package/dist/commands/items/availability.js +75 -0
  29. package/dist/commands/items/comment.js +29 -0
  30. package/dist/commands/items/create.js +57 -0
  31. package/dist/commands/items/get.js +50 -0
  32. package/dist/commands/items/index.js +3 -0
  33. package/dist/commands/items/list.js +25 -0
  34. package/dist/commands/items/update.js +60 -0
  35. package/dist/commands/locations/index.js +3 -0
  36. package/dist/commands/locations/list.js +19 -0
  37. package/dist/commands/login.js +76 -0
  38. package/dist/commands/products/import.js +65 -0
  39. package/dist/commands/products/index.js +3 -0
  40. package/dist/commands/products/list.js +19 -0
  41. package/dist/commands/returns/comment.js +29 -0
  42. package/dist/commands/returns/get.js +50 -0
  43. package/dist/commands/returns/index.js +3 -0
  44. package/dist/commands/returns/list.js +27 -0
  45. package/dist/commands/returns/update.js +68 -0
  46. package/dist/components/CommentResultView.js +40 -0
  47. package/dist/components/CustomFieldResultView.js +46 -0
  48. package/dist/components/CycleActionView.js +36 -0
  49. package/dist/components/CycleDetail.js +60 -0
  50. package/dist/components/CycleTable.js +68 -0
  51. package/dist/components/CyclesListView.js +30 -0
  52. package/dist/components/ErrorMessage.js +13 -0
  53. package/dist/components/ExitError.js +13 -0
  54. package/dist/components/ItemActionView.js +36 -0
  55. package/dist/components/ItemDetail.js +48 -0
  56. package/dist/components/ItemTable.js +58 -0
  57. package/dist/components/ItemsListView.js +30 -0
  58. package/dist/components/ItemsResultView.js +37 -0
  59. package/dist/components/JsonOutput.js +12 -0
  60. package/dist/components/Loading.js +11 -0
  61. package/dist/components/ProductTable.js +49 -0
  62. package/dist/components/ProductsListView.js +30 -0
  63. package/dist/components/ReferenceListView.js +49 -0
  64. package/dist/components/ReturnActionView.js +36 -0
  65. package/dist/components/ReturnDetail.js +55 -0
  66. package/dist/components/ReturnResultView.js +49 -0
  67. package/dist/components/ReturnTable.js +58 -0
  68. package/dist/components/ReturnsListView.js +30 -0
  69. package/dist/components/Status.js +33 -0
  70. package/dist/hooks/useAsync.js +32 -0
  71. package/dist/lib/client.js +302 -0
  72. package/dist/lib/config.js +38 -0
  73. package/dist/lib/format.js +49 -0
  74. package/dist/lib/listViewOptions.js +39 -0
  75. package/dist/lib/resolveClient.js +11 -0
  76. package/dist/lib/types.js +3 -0
  77. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Supercycle CLI
2
+
3
+ A command-line interface for the [Supercycle Admin API](https://docs.supercycle.com/api-reference/admin/introduction), built with Node and [Ink](https://github.com/vadimdemedes/ink).
4
+
5
+ > In the Supercycle UI a **cycle** is modelled as a **rental** in the API. This CLI uses the "cycle" terminology.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js ≥ 18
10
+ - A Supercycle Admin API key (Superest or custom plans — created in Supercycle → Settings → API)
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pnpm install
16
+ pnpm build
17
+ pnpm link --global # optional: makes `supercycle` available globally
18
+ ```
19
+
20
+ During development, run commands with `node dist/cli.js …` (rebuild with `pnpm build`, or `pnpm dev` to watch).
21
+
22
+ Code quality:
23
+
24
+ ```sh
25
+ pnpm fmt # format with oxfmt
26
+ pnpm fmt:check # verify formatting (CI)
27
+ pnpm lint # lint with oxlint
28
+ pnpm lint:fix # lint and auto-fix
29
+ ```
30
+
31
+ ## Authentication
32
+
33
+ ```sh
34
+ supercycle login # prompts for your API key, validates, and stores it
35
+ supercycle login --key sk_… # non-interactive
36
+ ```
37
+
38
+ The key is stored in `~/.config/supercycle/config.json` (mode `600`). You can
39
+ override it with the `SUPERCYCLE_API_KEY` environment variable.
40
+
41
+ ## Usage
42
+
43
+ List cycles, with optional filters:
44
+
45
+ ```sh
46
+ supercycle cycles list
47
+ supercycle cycles list --fulfillment overdue
48
+ supercycle cycles list --search "jane" --limit 20
49
+ supercycle cycles list --rental-start-from 2024-01-01 --rental-start-to 2024-01-31
50
+ supercycle cycles list --updated-since 2024-06-01 # incremental sync
51
+ supercycle cycles list --page <cursor> # next page (cursor from previous output)
52
+ supercycle cycles list --all # auto-follow every page
53
+ supercycle cycles list --json | jq '.data | length'
54
+ ```
55
+
56
+ Retrieve a single cycle:
57
+
58
+ ```sh
59
+ supercycle cycles get 12345
60
+ supercycle cycles get 12345 --include item,timelineEvents
61
+ supercycle cycles get 12345 --json
62
+ ```
63
+
64
+ ### Read shortcuts
65
+
66
+ Pre-filtered views (each accepts `--search`, `--limit`, `--page`, `--all`, `--json`):
67
+
68
+ ```sh
69
+ supercycle cycles to-fulfill # awaiting dispatch (unfulfilled)
70
+ supercycle cycles to-receive # awaiting return (receival due)
71
+ supercycle cycles overdue # overdue returns (--type fulfillment for dispatches)
72
+ supercycle cycles today # scheduled to be received back today
73
+ ```
74
+
75
+ ### Managing cycles (writes)
76
+
77
+ > These update Supercycle only — they do **not** change the linked Shopify order/fulfilment.
78
+ > Most print the updated cycle; add `--json` for raw output.
79
+
80
+ ```sh
81
+ supercycle cycles fulfil 12345 # mark dispatched (now); --at <iso> to backdate
82
+ supercycle cycles receive 12345 # mark received back; --at <iso>
83
+ supercycle cycles pack 12345 --status packed # pending|printed|packed
84
+ supercycle cycles reschedule 12345 --fulfill-at 2024-07-01T09:00:00Z --receive-at 2024-07-10T17:00:00Z
85
+ supercycle cycles reassign 12345 --item-id 678 --reallocate
86
+ supercycle cycles tag 12345 --add "vip,priority" --remove "draft"
87
+ supercycle cycles return 12345 --status awaiting --method collection --collection-date 2024-07-05
88
+ ```
89
+
90
+ ### List filters
91
+
92
+ All `GET /rentals` filters are exposed:
93
+
94
+ - `--search` — title / customer text
95
+ - `--fulfillment`, `--receival` — `due|scheduled|overdue|complete`
96
+ - `--unfulfilled`, `--exclude-cancelled` — booleans
97
+ - `--customer-id`, `--item-id` — numeric
98
+ - `--shopify-order-id`, `--shopify-variant-id`, `--shopify-line-item-id`
99
+ - `--return-order-status` — comma-separated (`requested,expected,received,in_progress,completed,cancelled`)
100
+ - Date ranges (`--*-from` → `gte`, `--*-to` → `lte`, inclusive, ISO dates):
101
+ `--rental-start-from/-to`, `--created-from/-to`, `--receive-from/-to`,
102
+ and `--updated-since` (gte, for incremental sync) / `--updated-to`
103
+ - `--include` — relations to include (e.g. `item`)
104
+ - `--limit` (1–100, default 50), `--page <cursor>`, `--json`
105
+
106
+ Pagination is keyset-based: when more results exist, the output prints a
107
+ `--page <cursor>` hint to fetch the next page.
package/dist/cli.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import Pastel from "pastel";
3
+ const app = new Pastel({
4
+ importMeta: import.meta,
5
+ name: "supercycle",
6
+ version: "0.1.0",
7
+ description: "Command-line interface for the Supercycle Admin API",
8
+ });
9
+ await app.run();
@@ -0,0 +1,48 @@
1
+ import { Text } from "ink";
2
+ import { argument, option } from "pastel";
3
+ import React from "react";
4
+ import zod from "zod";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ import ExitError from "../../components/ExitError.js";
7
+ import JsonOutput from "../../components/JsonOutput.js";
8
+ import Loading from "../../components/Loading.js";
9
+ import { useAsync } from "../../hooks/useAsync.js";
10
+ import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
11
+ export const description = "Delete a timeline comment by ID";
12
+ export const args = zod.tuple([
13
+ zod.string().describe(argument({
14
+ name: "id",
15
+ description: "Numeric ID of the timeline comment (from a resource's timeline events)",
16
+ })),
17
+ ]);
18
+ export const options = zod.object({
19
+ json: zod
20
+ .boolean()
21
+ .default(false)
22
+ .describe(option({ description: "Output raw JSON" })),
23
+ });
24
+ export default function Delete({ args, options }) {
25
+ const [id] = args;
26
+ const resolved = resolveClient();
27
+ const state = useAsync(async () => {
28
+ if (!resolved.client)
29
+ throw new Error(resolved.error);
30
+ await resolved.client.deleteTimelineComment(id);
31
+ return { id, deleted: true };
32
+ }, []);
33
+ if (state.loading) {
34
+ return options.json ? null : React.createElement(Loading, { label: `Deleting comment ${id}…` });
35
+ }
36
+ if (state.error) {
37
+ if (options.json)
38
+ return React.createElement(ExitError, { message: state.error.message });
39
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
40
+ }
41
+ if (options.json) {
42
+ return React.createElement(JsonOutput, { data: state.data });
43
+ }
44
+ return React.createElement(Text, { color: "green" },
45
+ "\u2714 Comment ",
46
+ id,
47
+ " deleted");
48
+ }
@@ -0,0 +1,3 @@
1
+ // Group landing: description only (no default export) so Pastel falls through
2
+ // to Commander's auto-generated help listing the subcommands.
3
+ export const description = "Manage timeline comments";
@@ -0,0 +1,3 @@
1
+ // Group landing: description only (no default export) so Pastel falls through
2
+ // to Commander's auto-generated help listing the subcommands.
3
+ export const description = "List item conditions";
@@ -0,0 +1,20 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import ReferenceListView from "../../components/ReferenceListView.js";
5
+ import { dash, titleCase } from "../../lib/format.js";
6
+ export const description = "List all conditions";
7
+ const COLUMNS = [
8
+ { label: "ID", width: 8, value: (c) => dash(c.id) },
9
+ { label: "Condition", width: 28, value: (c) => dash(c.title) },
10
+ { label: "Severity", value: (c) => titleCase(c.severityKey) },
11
+ ];
12
+ export const options = zod.object({
13
+ json: zod
14
+ .boolean()
15
+ .default(false)
16
+ .describe(option({ description: "Output raw JSON" })),
17
+ });
18
+ export default function List({ options }) {
19
+ return (React.createElement(ReferenceListView, { fetch: (client) => client.listConditions(), columns: COLUMNS, rowKey: (c) => c.id, loadingLabel: "Fetching conditions\u2026", emptyLabel: "No conditions found.", noun: "condition", json: options.json }));
20
+ }
@@ -0,0 +1,58 @@
1
+ import { Box, Text } from "ink";
2
+ import { argument, option } from "pastel";
3
+ import React from "react";
4
+ import zod from "zod";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ import ExitError from "../../components/ExitError.js";
7
+ import JsonOutput from "../../components/JsonOutput.js";
8
+ import Loading from "../../components/Loading.js";
9
+ import { useAsync } from "../../hooks/useAsync.js";
10
+ import { dash, titleCase } from "../../lib/format.js";
11
+ import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
12
+ export const description = "Retrieve a single custom field definition by ID";
13
+ export const args = zod.tuple([
14
+ zod.string().describe(argument({
15
+ name: "id",
16
+ description: "Numeric ID of the custom field definition",
17
+ })),
18
+ ]);
19
+ export const options = zod.object({
20
+ json: zod
21
+ .boolean()
22
+ .default(false)
23
+ .describe(option({ description: "Output raw JSON" })),
24
+ });
25
+ function Field({ label, children }) {
26
+ return (React.createElement(Box, null,
27
+ React.createElement(Box, { width: 18 },
28
+ React.createElement(Text, { dimColor: true }, label)),
29
+ React.createElement(Text, null, children)));
30
+ }
31
+ export default function Definition({ args, options }) {
32
+ const [id] = args;
33
+ const resolved = resolveClient();
34
+ const state = useAsync(async () => {
35
+ if (!resolved.client)
36
+ throw new Error(resolved.error);
37
+ return resolved.client.getCustomFieldDefinition(id);
38
+ }, []);
39
+ if (state.loading) {
40
+ return options.json ? null : React.createElement(Loading, { label: `Fetching definition ${id}…` });
41
+ }
42
+ if (state.error) {
43
+ if (options.json)
44
+ return React.createElement(ExitError, { message: state.error.message });
45
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
46
+ }
47
+ if (options.json) {
48
+ return React.createElement(JsonOutput, { data: state.data });
49
+ }
50
+ const def = state.data;
51
+ return (React.createElement(Box, { flexDirection: "column" },
52
+ React.createElement(Text, { bold: true, underline: true }, def.name),
53
+ React.createElement(Field, { label: "ID" }, String(def.id)),
54
+ React.createElement(Field, { label: "Key" }, dash(def.key)),
55
+ React.createElement(Field, { label: "Owner type" }, titleCase(def.ownerType)),
56
+ React.createElement(Field, { label: "Value type" }, titleCase(def.type)),
57
+ React.createElement(Field, { label: "Groups fulfil." }, def.groupFulfillments ? "yes" : "no")));
58
+ }
@@ -0,0 +1,25 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import ReferenceListView from "../../components/ReferenceListView.js";
5
+ import { dash, titleCase } from "../../lib/format.js";
6
+ export const description = "List custom field definitions for an owner type";
7
+ const COLUMNS = [
8
+ { label: "ID", width: 8, value: (d) => dash(d.id) },
9
+ { label: "Key", width: 24, value: (d) => dash(d.key) },
10
+ { label: "Name", width: 24, value: (d) => dash(d.name) },
11
+ { label: "Type", width: 22, value: (d) => titleCase(d.type) },
12
+ { label: "Group", value: (d) => (d.groupFulfillments ? "yes" : "no") },
13
+ ];
14
+ export const options = zod.object({
15
+ ownerType: zod
16
+ .enum(["item", "rental"])
17
+ .describe(option({ description: "Owner type: item|rental (required)" })),
18
+ json: zod
19
+ .boolean()
20
+ .default(false)
21
+ .describe(option({ description: "Output raw JSON" })),
22
+ });
23
+ export default function Definitions({ options }) {
24
+ return (React.createElement(ReferenceListView, { fetch: (client) => client.listCustomFieldDefinitions({ ownerType: options.ownerType }), columns: COLUMNS, rowKey: (d) => d.id, loadingLabel: `Fetching ${options.ownerType} custom field definitions…`, emptyLabel: "No custom field definitions found.", noun: "definition", json: options.json }));
25
+ }
@@ -0,0 +1,45 @@
1
+ import { Text } from "ink";
2
+ import { argument, option } from "pastel";
3
+ import React from "react";
4
+ import zod from "zod";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ import ExitError from "../../components/ExitError.js";
7
+ import JsonOutput from "../../components/JsonOutput.js";
8
+ import Loading from "../../components/Loading.js";
9
+ import { useAsync } from "../../hooks/useAsync.js";
10
+ import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
11
+ export const description = "Delete a custom field by ID";
12
+ export const args = zod.tuple([
13
+ zod.string().describe(argument({ name: "id", description: "Numeric custom field ID" })),
14
+ ]);
15
+ export const options = zod.object({
16
+ json: zod
17
+ .boolean()
18
+ .default(false)
19
+ .describe(option({ description: "Output raw JSON" })),
20
+ });
21
+ export default function Delete({ args, options }) {
22
+ const [id] = args;
23
+ const resolved = resolveClient();
24
+ const state = useAsync(async () => {
25
+ if (!resolved.client)
26
+ throw new Error(resolved.error);
27
+ await resolved.client.deleteCustomField(id);
28
+ return { id, deleted: true };
29
+ }, []);
30
+ if (state.loading) {
31
+ return options.json ? null : React.createElement(Loading, { label: `Deleting custom field ${id}…` });
32
+ }
33
+ if (state.error) {
34
+ if (options.json)
35
+ return React.createElement(ExitError, { message: state.error.message });
36
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
37
+ }
38
+ if (options.json) {
39
+ return React.createElement(JsonOutput, { data: state.data });
40
+ }
41
+ return React.createElement(Text, { color: "green" },
42
+ "\u2714 Custom field ",
43
+ id,
44
+ " deleted");
45
+ }
@@ -0,0 +1,3 @@
1
+ // Group landing: description only (no default export) so Pastel falls through
2
+ // to Commander's auto-generated help listing the subcommands.
3
+ export const description = "Inspect and manage custom fields";
@@ -0,0 +1,43 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CustomFieldResultView from "../../components/CustomFieldResultView.js";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ export const description = "Set a custom field value on an item or rental";
7
+ export const options = zod.object({
8
+ ownerId: zod
9
+ .number()
10
+ .describe(option({ description: "ID of the item or rental to attach the field to (required)" })),
11
+ value: zod.string().describe(option({ description: "Value to store (required)" })),
12
+ definitionId: zod
13
+ .number()
14
+ .optional()
15
+ .describe(option({ description: "Custom field definition ID (or use --key + --owner-type)" })),
16
+ key: zod
17
+ .string()
18
+ .optional()
19
+ .describe(option({ description: "Definition key (requires --owner-type)" })),
20
+ ownerType: zod
21
+ .enum(["item", "rental"])
22
+ .optional()
23
+ .describe(option({ description: "Owner type: item|rental (requires --key)" })),
24
+ json: zod
25
+ .boolean()
26
+ .default(false)
27
+ .describe(option({ description: "Output raw JSON" })),
28
+ });
29
+ export default function Set({ options }) {
30
+ const hasDefinition = options.definitionId != null;
31
+ const hasKeyPair = Boolean(options.key && options.ownerType);
32
+ if (!hasDefinition && !hasKeyPair) {
33
+ return (React.createElement(ErrorMessage, { message: "Specify the definition with --definition-id, or with --key and --owner-type." }));
34
+ }
35
+ const body = {
36
+ ownerId: options.ownerId,
37
+ value: options.value,
38
+ definitionId: options.definitionId,
39
+ key: hasDefinition ? undefined : options.key,
40
+ ownerType: hasDefinition ? undefined : options.ownerType,
41
+ };
42
+ return (React.createElement(CustomFieldResultView, { loadingLabel: `Setting custom field on ${options.ownerType ?? "owner"} ${options.ownerId}…`, action: (client) => client.createCustomField(body), successLabel: (f) => `Custom field "${f.key}" set`, json: options.json }));
43
+ }
@@ -0,0 +1,19 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CustomFieldResultView from "../../components/CustomFieldResultView.js";
5
+ export const description = "Update an existing custom field's value";
6
+ export const args = zod.tuple([
7
+ zod.string().describe(argument({ name: "id", description: "Numeric custom field ID" })),
8
+ ]);
9
+ export const options = zod.object({
10
+ value: zod.string().describe(option({ description: "New value to store (required)" })),
11
+ json: zod
12
+ .boolean()
13
+ .default(false)
14
+ .describe(option({ description: "Output raw JSON" })),
15
+ });
16
+ export default function Update({ args, options }) {
17
+ const [id] = args;
18
+ return (React.createElement(CustomFieldResultView, { loadingLabel: `Updating custom field ${id}…`, action: (client) => client.updateCustomField(id, options.value), successLabel: (f) => `Custom field "${f.key}" updated`, json: options.json }));
19
+ }
@@ -0,0 +1,29 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CommentResultView from "../../components/CommentResultView.js";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ export const description = "Add a comment to a cycle's timeline";
7
+ export const args = zod.tuple([
8
+ zod.string().describe(argument({ name: "id", description: "Numeric cycle ID" })),
9
+ zod.string().describe(argument({ name: "message", description: "Comment text" })),
10
+ ]);
11
+ export const options = zod.object({
12
+ json: zod
13
+ .boolean()
14
+ .default(false)
15
+ .describe(option({ description: "Output raw JSON" })),
16
+ });
17
+ export default function Comment({ args, options }) {
18
+ const [id, message] = args;
19
+ if (!message.trim()) {
20
+ return React.createElement(ErrorMessage, { message: "Comment message cannot be empty." });
21
+ }
22
+ return (React.createElement(CommentResultView, { loadingLabel: `Adding comment to cycle ${id}…`, action: (client) => client.createTimelineComment({
23
+ timelineEvent: {
24
+ eventableId: Number(id),
25
+ eventableType: "Rental",
26
+ message,
27
+ },
28
+ }), json: options.json }));
29
+ }
@@ -0,0 +1,24 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CycleActionView from "../../components/CycleActionView.js";
5
+ export const description = "Mark a cycle as fulfilled (dispatched)";
6
+ export const args = zod.tuple([
7
+ zod.string().describe(argument({ name: "id", description: "Numeric cycle ID" })),
8
+ ]);
9
+ export const options = zod.object({
10
+ at: zod
11
+ .string()
12
+ .optional()
13
+ .describe(option({ description: "Dispatch time (ISO date-time; defaults to now)" })),
14
+ json: zod
15
+ .boolean()
16
+ .default(false)
17
+ .describe(option({ description: "Output raw JSON" })),
18
+ });
19
+ // Note: this updates Supercycle only — it does not fulfil the Shopify order.
20
+ export default function Fulfil({ args, options }) {
21
+ const [id] = args;
22
+ const fulfilledAt = options.at ?? new Date().toISOString();
23
+ return (React.createElement(CycleActionView, { loadingLabel: `Fulfilling cycle ${id}…`, action: (client) => client.updateCycle(id, { fulfilledAt }), successLabel: (c) => `Cycle ${c.id} marked as fulfilled`, json: options.json }));
24
+ }
@@ -0,0 +1,50 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CycleDetail from "../../components/CycleDetail.js";
5
+ import ErrorMessage from "../../components/ErrorMessage.js";
6
+ import ExitError from "../../components/ExitError.js";
7
+ import JsonOutput from "../../components/JsonOutput.js";
8
+ import Loading from "../../components/Loading.js";
9
+ import { useAsync } from "../../hooks/useAsync.js";
10
+ import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
11
+ export const description = "Retrieve a single cycle by ID";
12
+ export const args = zod.tuple([
13
+ zod.string().describe(argument({
14
+ name: "id",
15
+ description: "Numeric ID of the cycle",
16
+ })),
17
+ ]);
18
+ export const options = zod.object({
19
+ include: zod
20
+ .string()
21
+ .optional()
22
+ .describe(option({
23
+ description: "Relations to include, e.g. item,timelineEvents",
24
+ })),
25
+ json: zod
26
+ .boolean()
27
+ .default(false)
28
+ .describe(option({ description: "Output raw JSON" })),
29
+ });
30
+ export default function Get({ args, options }) {
31
+ const [id] = args;
32
+ const resolved = resolveClient();
33
+ const state = useAsync(async () => {
34
+ if (!resolved.client)
35
+ throw new Error(resolved.error);
36
+ return resolved.client.getCycle(id, { include: options.include });
37
+ }, []);
38
+ if (state.loading) {
39
+ return options.json ? null : React.createElement(Loading, { label: `Fetching cycle ${id}…` });
40
+ }
41
+ if (state.error) {
42
+ if (options.json)
43
+ return React.createElement(ExitError, { message: state.error.message });
44
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
45
+ }
46
+ if (options.json) {
47
+ return React.createElement(JsonOutput, { data: state.data });
48
+ }
49
+ return React.createElement(CycleDetail, { cycle: state.data });
50
+ }
@@ -0,0 +1,3 @@
1
+ // Group landing: description only (no default export) so Pastel falls through
2
+ // to Commander's auto-generated help listing the subcommands.
3
+ export const description = "List, inspect, and manage cycles";
@@ -0,0 +1,124 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CyclesListView from "../../components/CyclesListView.js";
5
+ import { listViewShape, viewFilters } from "../../lib/listViewOptions.js";
6
+ export const description = "List cycles, optionally filtered";
7
+ const progress = zod.enum(["due", "scheduled", "overdue", "complete"]);
8
+ export const options = zod.object({
9
+ ...listViewShape(),
10
+ fulfillment: progress
11
+ .optional()
12
+ .describe(option({ description: "Fulfillment status: due|scheduled|overdue|complete" })),
13
+ receival: progress
14
+ .optional()
15
+ .describe(option({ description: "Receival status: due|scheduled|overdue|complete" })),
16
+ unfulfilled: zod
17
+ .boolean()
18
+ .default(false)
19
+ .describe(option({ description: "Only show unfulfilled cycles" })),
20
+ excludeCancelled: zod
21
+ .boolean()
22
+ .default(false)
23
+ .describe(option({ description: "Omit cancelled cycles" })),
24
+ customerId: zod
25
+ .number()
26
+ .optional()
27
+ .describe(option({ description: "Filter by customer ID" })),
28
+ itemId: zod
29
+ .number()
30
+ .optional()
31
+ .describe(option({ description: "Filter by item ID" })),
32
+ shopifyOrderId: zod
33
+ .string()
34
+ .optional()
35
+ .describe(option({ description: "Filter by Shopify order ID" })),
36
+ shopifyVariantId: zod
37
+ .number()
38
+ .optional()
39
+ .describe(option({ description: "Filter by Shopify variant ID" })),
40
+ shopifyLineItemId: zod
41
+ .number()
42
+ .optional()
43
+ .describe(option({ description: "Filter by Shopify line item ID" })),
44
+ returnOrderStatus: zod
45
+ .string()
46
+ .optional()
47
+ .describe(option({
48
+ description: "Return order status (comma-separated): requested,expected,received,in_progress,completed,cancelled",
49
+ })),
50
+ rentalStartFrom: zod
51
+ .string()
52
+ .optional()
53
+ .describe(option({ description: "Rental start on/after (ISO date)" })),
54
+ rentalStartTo: zod
55
+ .string()
56
+ .optional()
57
+ .describe(option({ description: "Rental start on/before (ISO date)" })),
58
+ createdFrom: zod
59
+ .string()
60
+ .optional()
61
+ .describe(option({ description: "Created on/after (ISO date)" })),
62
+ createdTo: zod
63
+ .string()
64
+ .optional()
65
+ .describe(option({ description: "Created on/before (ISO date)" })),
66
+ updatedSince: zod
67
+ .string()
68
+ .optional()
69
+ .describe(option({ description: "Updated on/after (ISO date), for sync" })),
70
+ updatedTo: zod
71
+ .string()
72
+ .optional()
73
+ .describe(option({ description: "Updated on/before (ISO date)" })),
74
+ receiveFrom: zod
75
+ .string()
76
+ .optional()
77
+ .describe(option({ description: "Scheduled receive on/after (ISO date)" })),
78
+ receiveTo: zod
79
+ .string()
80
+ .optional()
81
+ .describe(option({ description: "Scheduled receive on/before (ISO date)" })),
82
+ include: zod
83
+ .string()
84
+ .optional()
85
+ .describe(option({ description: "Relations to include, e.g. item" })),
86
+ });
87
+ function buildFilters(options) {
88
+ const filters = {
89
+ ...viewFilters(options),
90
+ fulfillmentStatus: options.fulfillment,
91
+ receivalStatus: options.receival,
92
+ customerId: options.customerId,
93
+ itemId: options.itemId,
94
+ shopifyOrderId: options.shopifyOrderId,
95
+ shopifyVariantId: options.shopifyVariantId,
96
+ shopifyLineItemId: options.shopifyLineItemId,
97
+ returnOrderStatus: options.returnOrderStatus,
98
+ include: options.include,
99
+ };
100
+ if (options.unfulfilled)
101
+ filters.unfulfilled = true;
102
+ if (options.excludeCancelled)
103
+ filters.excludeCancelled = true;
104
+ // Date-range filters: --*-from maps to gte (inclusive), --*-to maps to lte.
105
+ if (options.rentalStartFrom || options.rentalStartTo) {
106
+ filters.rentalStart = {
107
+ gte: options.rentalStartFrom,
108
+ lte: options.rentalStartTo,
109
+ };
110
+ }
111
+ if (options.createdFrom || options.createdTo) {
112
+ filters.created = { gte: options.createdFrom, lte: options.createdTo };
113
+ }
114
+ if (options.updatedSince || options.updatedTo) {
115
+ filters.updated = { gte: options.updatedSince, lte: options.updatedTo };
116
+ }
117
+ if (options.receiveFrom || options.receiveTo) {
118
+ filters.receiveAt = { gte: options.receiveFrom, lte: options.receiveTo };
119
+ }
120
+ return filters;
121
+ }
122
+ export default function List({ options }) {
123
+ return React.createElement(CyclesListView, { filters: buildFilters(options), all: options.all, json: options.json });
124
+ }
@@ -0,0 +1,25 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import CyclesListView from "../../components/CyclesListView.js";
5
+ import { listViewShape, viewFilters } from "../../lib/listViewOptions.js";
6
+ export const description = "Overdue cycles (returns by default, or dispatches)";
7
+ export const options = zod.object({
8
+ ...listViewShape(),
9
+ type: zod
10
+ .enum(["receival", "fulfillment"])
11
+ .default("receival")
12
+ .describe(option({
13
+ description: "Which deadline is overdue: receival (returns) or fulfillment (dispatch)",
14
+ })),
15
+ });
16
+ export default function Overdue({ options }) {
17
+ const filters = { ...viewFilters(options) };
18
+ if (options.type === "fulfillment") {
19
+ filters.fulfillmentStatus = "overdue";
20
+ }
21
+ else {
22
+ filters.receivalStatus = "overdue";
23
+ }
24
+ return React.createElement(CyclesListView, { filters: filters, all: options.all, json: options.json });
25
+ }