@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
@@ -0,0 +1,76 @@
1
+ import { Box, Text, useApp } from "ink";
2
+ import TextInput from "ink-text-input";
3
+ import { option } from "pastel";
4
+ import React, { useState, useEffect } from "react";
5
+ import zod from "zod";
6
+ import ErrorMessage from "../components/ErrorMessage.js";
7
+ import Loading from "../components/Loading.js";
8
+ import { SupercycleClient } from "../lib/client.js";
9
+ import { saveConfig, getBaseUrl, configPath } from "../lib/config.js";
10
+ export const description = "Store and validate your Supercycle API key";
11
+ export const options = zod.object({
12
+ key: zod
13
+ .string()
14
+ .optional()
15
+ .describe(option({
16
+ description: "API key (skips the interactive prompt)",
17
+ })),
18
+ });
19
+ export default function Login({ options }) {
20
+ const { exit } = useApp();
21
+ const [value, setValue] = useState("");
22
+ const [phase, setPhase] = useState(options.key ? { name: "validating" } : { name: "prompt" });
23
+ async function validateAndSave(key) {
24
+ const trimmed = key.trim();
25
+ if (!trimmed) {
26
+ setPhase({ name: "error", message: "No API key provided." });
27
+ return;
28
+ }
29
+ setPhase({ name: "validating" });
30
+ const client = new SupercycleClient({
31
+ token: trimmed,
32
+ baseUrl: getBaseUrl(),
33
+ });
34
+ try {
35
+ await client.listCycles({ limit: 1 });
36
+ saveConfig({ apiKey: trimmed, baseUrl: getBaseUrl() });
37
+ setPhase({ name: "done" });
38
+ }
39
+ catch (error) {
40
+ setPhase({
41
+ name: "error",
42
+ message: error instanceof Error ? error.message : String(error),
43
+ });
44
+ }
45
+ }
46
+ // If a key was passed as a flag, validate it immediately.
47
+ useEffect(() => {
48
+ if (options.key) {
49
+ void validateAndSave(options.key);
50
+ }
51
+ }, [options]);
52
+ // Exit (with the right code) once we reach a terminal phase.
53
+ useEffect(() => {
54
+ if (phase.name === "done") {
55
+ exit();
56
+ }
57
+ else if (phase.name === "error") {
58
+ process.exitCode = 1;
59
+ exit();
60
+ }
61
+ }, [phase, exit]);
62
+ if (phase.name === "prompt") {
63
+ return (React.createElement(Box, null,
64
+ React.createElement(Text, null, "Enter your Supercycle API key: "),
65
+ React.createElement(TextInput, { value: value, onChange: setValue, onSubmit: validateAndSave, mask: "*" })));
66
+ }
67
+ if (phase.name === "validating") {
68
+ return React.createElement(Loading, { label: "Validating API key\u2026" });
69
+ }
70
+ if (phase.name === "error") {
71
+ return React.createElement(ErrorMessage, { message: phase.message });
72
+ }
73
+ return React.createElement(Text, { color: "green" },
74
+ "\u2714 API key saved to ",
75
+ configPath());
76
+ }
@@ -0,0 +1,65 @@
1
+ import { Box, Text } from "ink";
2
+ import { 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 = "Import Shopify products into Supercycle by ID";
12
+ export const options = zod.object({
13
+ ids: zod
14
+ .string()
15
+ .describe(option({ description: "Comma-separated Shopify product IDs to import (required)" })),
16
+ json: zod
17
+ .boolean()
18
+ .default(false)
19
+ .describe(option({ description: "Output raw JSON" })),
20
+ });
21
+ function parseIds(raw) {
22
+ const ids = [];
23
+ for (const part of raw.split(",")) {
24
+ const trimmed = part.trim();
25
+ if (!trimmed)
26
+ continue;
27
+ const id = Number(trimmed);
28
+ if (!Number.isInteger(id) || id <= 0)
29
+ return null;
30
+ ids.push(id);
31
+ }
32
+ return ids.length > 0 ? ids : null;
33
+ }
34
+ export default function Import({ options }) {
35
+ const ids = parseIds(options.ids);
36
+ const resolved = resolveClient();
37
+ const state = useAsync(async () => {
38
+ if (!ids)
39
+ throw new Error("Provide one or more numeric product IDs via --ids.");
40
+ if (!resolved.client)
41
+ throw new Error(resolved.error);
42
+ return resolved.client.importProducts(ids);
43
+ }, []);
44
+ if (state.loading) {
45
+ return options.json ? null : React.createElement(Loading, { label: "Importing products\u2026" });
46
+ }
47
+ if (state.error) {
48
+ if (options.json)
49
+ return React.createElement(ExitError, { message: state.error.message });
50
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
51
+ }
52
+ if (options.json) {
53
+ return React.createElement(JsonOutput, { data: state.data });
54
+ }
55
+ const imported = state.data.productIds ?? [];
56
+ return (React.createElement(Box, { flexDirection: "column" },
57
+ React.createElement(Text, { color: "green" },
58
+ "\u2714 Imported ",
59
+ imported.length,
60
+ " product",
61
+ imported.length === 1 ? "" : "s"),
62
+ imported.length > 0 ? React.createElement(Text, { dimColor: true },
63
+ "IDs: ",
64
+ imported.join(", ")) : null));
65
+ }
@@ -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 and import products";
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import zod from "zod";
3
+ import ProductsListView from "../../components/ProductsListView.js";
4
+ import { listViewShape } from "../../lib/listViewOptions.js";
5
+ export const description = "List products, optionally filtered by search";
6
+ export const options = zod.object({
7
+ ...listViewShape(),
8
+ });
9
+ function buildFilters(options) {
10
+ const view = options;
11
+ return {
12
+ search: view.search,
13
+ limit: view.limit,
14
+ page: view.page,
15
+ };
16
+ }
17
+ export default function List({ options }) {
18
+ return React.createElement(ProductsListView, { filters: buildFilters(options), all: options.all, 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 return's timeline";
7
+ export const args = zod.tuple([
8
+ zod.string().describe(argument({ name: "id", description: "Numeric return 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 return ${id}…`, action: (client) => client.createTimelineComment({
23
+ timelineEvent: {
24
+ eventableId: Number(id),
25
+ eventableType: "ReturnOrder",
26
+ message,
27
+ },
28
+ }), json: options.json }));
29
+ }
@@ -0,0 +1,50 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import ErrorMessage from "../../components/ErrorMessage.js";
5
+ import ExitError from "../../components/ExitError.js";
6
+ import JsonOutput from "../../components/JsonOutput.js";
7
+ import Loading from "../../components/Loading.js";
8
+ import ReturnDetail from "../../components/ReturnDetail.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 return by ID";
12
+ export const args = zod.tuple([
13
+ zod.string().describe(argument({
14
+ name: "id",
15
+ description: "Numeric ID of the return",
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. 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.getReturn(id, { include: options.include });
37
+ }, []);
38
+ if (state.loading) {
39
+ return options.json ? null : React.createElement(Loading, { label: `Fetching return ${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(ReturnDetail, { order: 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 update returns";
@@ -0,0 +1,27 @@
1
+ import { option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import ReturnsListView from "../../components/ReturnsListView.js";
5
+ import { listViewShape } from "../../lib/listViewOptions.js";
6
+ export const description = "List returns, optionally filtered";
7
+ export const options = zod.object({
8
+ ...listViewShape(),
9
+ status: zod
10
+ .string()
11
+ .optional()
12
+ .describe(option({
13
+ description: "Return status (comma-separated): requested,expected,received,in_progress,completed,cancelled",
14
+ })),
15
+ });
16
+ function buildFilters(options) {
17
+ const view = options;
18
+ return {
19
+ search: view.search,
20
+ limit: view.limit,
21
+ page: view.page,
22
+ status: options.status,
23
+ };
24
+ }
25
+ export default function List({ options }) {
26
+ return React.createElement(ReturnsListView, { filters: buildFilters(options), all: options.all, json: options.json });
27
+ }
@@ -0,0 +1,68 @@
1
+ import { argument, option } from "pastel";
2
+ import React from "react";
3
+ import zod from "zod";
4
+ import ErrorMessage from "../../components/ErrorMessage.js";
5
+ import ReturnActionView from "../../components/ReturnActionView.js";
6
+ export const description = "Update a return's status and/or line statuses";
7
+ const lineStatus = ["awaiting", "received", "missing"];
8
+ export const args = zod.tuple([
9
+ zod.string().describe(argument({ name: "id", description: "Numeric return ID" })),
10
+ ]);
11
+ export const options = zod.object({
12
+ status: zod
13
+ .string()
14
+ .optional()
15
+ .describe(option({
16
+ description: "New return status: requested|expected|received|in_progress|completed|cancelled",
17
+ })),
18
+ line: zod
19
+ .string()
20
+ .optional()
21
+ .describe(option({
22
+ description: 'Set line statuses as "lineId:status" pairs, comma-separated (status: awaiting|received|missing). Line IDs come from `returns get <id>`.',
23
+ })),
24
+ json: zod
25
+ .boolean()
26
+ .default(false)
27
+ .describe(option({ description: "Output raw JSON" })),
28
+ });
29
+ // Parse a "12:received,13:missing" string into returnLines entries.
30
+ function parseLines(raw) {
31
+ const lines = [];
32
+ for (const part of raw.split(",")) {
33
+ const trimmed = part.trim();
34
+ if (!trimmed)
35
+ continue;
36
+ const [idPart, statusPart] = trimmed.split(":");
37
+ const id = Number(idPart);
38
+ if (!Number.isInteger(id) || id <= 0) {
39
+ return { ok: false, error: `Invalid line ID in "${trimmed}". Expected "lineId:status".` };
40
+ }
41
+ if (!statusPart || !lineStatus.includes(statusPart)) {
42
+ return {
43
+ ok: false,
44
+ error: `Invalid status in "${trimmed}". Expected one of: ${lineStatus.join(", ")}.`,
45
+ };
46
+ }
47
+ lines.push({ id, status: statusPart });
48
+ }
49
+ return { ok: true, lines };
50
+ }
51
+ export default function Update({ args, options }) {
52
+ const [id] = args;
53
+ if (!options.status && !options.line) {
54
+ return React.createElement(ErrorMessage, { message: "Nothing to update. Pass --status and/or --line." });
55
+ }
56
+ const body = {};
57
+ if (options.status)
58
+ body.status = options.status;
59
+ if (options.line) {
60
+ const parsed = parseLines(options.line);
61
+ if (!parsed.ok) {
62
+ return React.createElement(ErrorMessage, { message: parsed.error });
63
+ }
64
+ if (parsed.lines.length > 0)
65
+ body.returnLines = parsed.lines;
66
+ }
67
+ return (React.createElement(ReturnActionView, { loadingLabel: `Updating return ${id}…`, action: (client) => client.updateReturn(id, body), successLabel: (o) => `Return #${o.sequentialId ?? o.id} updated`, json: options.json }));
68
+ }
@@ -0,0 +1,40 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { useAsync } from "../hooks/useAsync.js";
4
+ import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
5
+ import ErrorMessage from "./ErrorMessage.js";
6
+ import ExitError from "./ExitError.js";
7
+ import JsonOutput from "./JsonOutput.js";
8
+ import Loading from "./Loading.js";
9
+ // Runs a create-timeline-comment action and reports the new comment.
10
+ export default function CommentResultView({ action, loadingLabel = "Adding comment…", json, }) {
11
+ const resolved = resolveClient();
12
+ const state = useAsync(async () => {
13
+ if (!resolved.client)
14
+ throw new Error(resolved.error);
15
+ return action(resolved.client);
16
+ }, []);
17
+ if (state.loading) {
18
+ return json ? null : React.createElement(Loading, { label: loadingLabel });
19
+ }
20
+ if (state.error) {
21
+ if (json)
22
+ return React.createElement(ExitError, { message: state.error.message });
23
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
24
+ }
25
+ if (json) {
26
+ return React.createElement(JsonOutput, { data: state.data });
27
+ }
28
+ const comment = state.data;
29
+ return (React.createElement(Box, { flexDirection: "column" },
30
+ React.createElement(Text, { color: "green" },
31
+ "\u2714 Comment added to ",
32
+ comment.eventableType,
33
+ " ",
34
+ comment.eventableId,
35
+ " (comment ID ",
36
+ comment.id,
37
+ ")"),
38
+ React.createElement(Box, { marginTop: 1 },
39
+ React.createElement(Text, { dimColor: true }, comment.message))));
40
+ }
@@ -0,0 +1,46 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { useAsync } from "../hooks/useAsync.js";
4
+ import { dash } from "../lib/format.js";
5
+ import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
6
+ import ErrorMessage from "./ErrorMessage.js";
7
+ import ExitError from "./ExitError.js";
8
+ import JsonOutput from "./JsonOutput.js";
9
+ import Loading from "./Loading.js";
10
+ // Runs a custom-field create/update action, then shows a confirmation line plus
11
+ // the resulting field's key/value/owner.
12
+ export default function CustomFieldResultView({ action, successLabel, loadingLabel = "Saving custom field…", json, }) {
13
+ const resolved = resolveClient();
14
+ const state = useAsync(async () => {
15
+ if (!resolved.client)
16
+ throw new Error(resolved.error);
17
+ return action(resolved.client);
18
+ }, []);
19
+ if (state.loading) {
20
+ return json ? null : React.createElement(Loading, { label: loadingLabel });
21
+ }
22
+ if (state.error) {
23
+ if (json)
24
+ return React.createElement(ExitError, { message: state.error.message });
25
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
26
+ }
27
+ if (json) {
28
+ return React.createElement(JsonOutput, { data: state.data });
29
+ }
30
+ const field = state.data;
31
+ return (React.createElement(Box, { flexDirection: "column" },
32
+ React.createElement(Text, { color: "green" },
33
+ "\u2714 ",
34
+ successLabel(field)),
35
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
36
+ React.createElement(Text, { dimColor: true },
37
+ dash(field.key),
38
+ " = ",
39
+ dash(field.value)),
40
+ React.createElement(Text, { dimColor: true },
41
+ field.ownerType,
42
+ " ",
43
+ field.ownerId,
44
+ " \u00B7 field ID ",
45
+ field.id))));
46
+ }
@@ -0,0 +1,36 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { useAsync } from "../hooks/useAsync.js";
4
+ import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
5
+ import CycleDetail from "./CycleDetail.js";
6
+ import ErrorMessage from "./ErrorMessage.js";
7
+ import ExitError from "./ExitError.js";
8
+ import JsonOutput from "./JsonOutput.js";
9
+ import Loading from "./Loading.js";
10
+ // Runs a single cycle-mutating action, then shows a confirmation line plus the
11
+ // updated cycle (or raw JSON). Used by fulfil/receive/pack/reschedule/etc.
12
+ export default function CycleActionView({ action, successLabel, loadingLabel = "Updating cycle…", json, }) {
13
+ const resolved = resolveClient();
14
+ const state = useAsync(async () => {
15
+ if (!resolved.client)
16
+ throw new Error(resolved.error);
17
+ return action(resolved.client);
18
+ }, []);
19
+ if (state.loading) {
20
+ return json ? null : React.createElement(Loading, { label: loadingLabel });
21
+ }
22
+ if (state.error) {
23
+ if (json)
24
+ return React.createElement(ExitError, { message: state.error.message });
25
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
26
+ }
27
+ if (json) {
28
+ return React.createElement(JsonOutput, { data: state.data });
29
+ }
30
+ return (React.createElement(Box, { flexDirection: "column" },
31
+ React.createElement(Text, { color: "green" },
32
+ "\u2714 ",
33
+ successLabel(state.data)),
34
+ React.createElement(Box, { marginTop: 1 },
35
+ React.createElement(CycleDetail, { cycle: state.data }))));
36
+ }
@@ -0,0 +1,60 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { customerName, formatDateTime, titleCase, dash, EMPTY } from "../lib/format.js";
4
+ import Status from "./Status.js";
5
+ function Section({ title, children }) {
6
+ return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
7
+ React.createElement(Text, { bold: true, underline: true }, title),
8
+ children));
9
+ }
10
+ function Field({ label, children }) {
11
+ return (React.createElement(Box, null,
12
+ React.createElement(Box, { width: 18 },
13
+ React.createElement(Text, { dimColor: true }, label)),
14
+ React.createElement(Box, null, typeof children === "string" ? React.createElement(Text, null, children) : children)));
15
+ }
16
+ export default function CycleDetail({ cycle }) {
17
+ const { item, timelineEvents, tags, customFields } = cycle;
18
+ return (React.createElement(Box, { flexDirection: "column" },
19
+ React.createElement(Section, { title: `Cycle #${cycle.sequentialId ?? cycle.id}` },
20
+ React.createElement(Field, { label: "ID" }, String(cycle.id)),
21
+ React.createElement(Field, { label: "Order" }, dash(cycle.shopifyOrderName)),
22
+ React.createElement(Field, { label: "Method" }, titleCase(cycle.methodType)),
23
+ React.createElement(Field, { label: "Status" },
24
+ React.createElement(Status, { value: cycle.status })),
25
+ React.createElement(Field, { label: "Fulfillment" },
26
+ React.createElement(Status, { value: cycle.fulfillmentStatus })),
27
+ React.createElement(Field, { label: "Receival" },
28
+ React.createElement(Status, { value: cycle.receivalStatus })),
29
+ React.createElement(Field, { label: "Packing" },
30
+ React.createElement(Status, { value: cycle.packingStatus }))),
31
+ React.createElement(Section, { title: "Customer" },
32
+ React.createElement(Field, { label: "Name" }, customerName(cycle.customer)),
33
+ React.createElement(Field, { label: "Email" }, dash(cycle.customer?.email))),
34
+ item ? (React.createElement(Section, { title: "Item" },
35
+ React.createElement(Field, { label: "Product" }, dash(item.productTitle)),
36
+ React.createElement(Field, { label: "Variant" }, dash(item.variantTitle)),
37
+ React.createElement(Field, { label: "Serial" }, dash(item.serial)),
38
+ React.createElement(Field, { label: "Visibility" },
39
+ React.createElement(Status, { value: item.visibility })),
40
+ React.createElement(Field, { label: "Pick location" }, dash(item.pickLocation)))) : null,
41
+ React.createElement(Section, { title: "Dates" },
42
+ React.createElement(Field, { label: "Fulfill due" }, formatDateTime(cycle.fulfillAt)),
43
+ React.createElement(Field, { label: "Fulfilled" }, formatDateTime(cycle.fulfilledAt)),
44
+ React.createElement(Field, { label: "Rental start" }, formatDateTime(cycle.rentalStart)),
45
+ React.createElement(Field, { label: "Rental end" }, formatDateTime(cycle.rentalEnd)),
46
+ React.createElement(Field, { label: "Receive due" }, formatDateTime(cycle.receiveAt)),
47
+ React.createElement(Field, { label: "Received" }, formatDateTime(cycle.receivedAt)),
48
+ React.createElement(Field, { label: "Restock by" }, formatDateTime(cycle.restockBy)),
49
+ React.createElement(Field, { label: "Created" }, formatDateTime(cycle.createdAt)),
50
+ React.createElement(Field, { label: "Updated" }, formatDateTime(cycle.updatedAt))),
51
+ tags && tags.length > 0 ? (React.createElement(Section, { title: "Tags" },
52
+ React.createElement(Text, null, tags.join(", ")))) : null,
53
+ customFields && customFields.length > 0 ? (React.createElement(Section, { title: "Custom fields" }, customFields.map((field) => (React.createElement(Field, { key: field.id, label: field.key }, dash(field.value)))))) : null,
54
+ timelineEvents && timelineEvents.length > 0 ? (React.createElement(Section, { title: "Timeline" }, timelineEvents.map((event) => (React.createElement(Box, { key: event.id },
55
+ React.createElement(Box, { width: 20 },
56
+ React.createElement(Text, { dimColor: true, wrap: "truncate" }, formatDateTime(event.createdAt))),
57
+ React.createElement(Box, { width: 28 },
58
+ React.createElement(Text, { wrap: "truncate" }, titleCase(event.eventType))),
59
+ React.createElement(Text, { dimColor: true }, event.author ?? EMPTY)))))) : null));
60
+ }
@@ -0,0 +1,68 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { customerName, formatDate, titleCase, dash } from "../lib/format.js";
4
+ import Status from "./Status.js";
5
+ const COLUMNS = {
6
+ id: 10,
7
+ cycle: 7,
8
+ order: 10,
9
+ customer: 18,
10
+ method: 12,
11
+ status: 11,
12
+ fulfillment: 12,
13
+ receival: 10,
14
+ start: 12,
15
+ };
16
+ // `noShrink` keeps a column at full width even on narrow terminals — used for
17
+ // the identifier columns so they stay readable for follow-up commands.
18
+ function Cell({ width, noShrink, children, }) {
19
+ return (React.createElement(Box, { width: width, marginRight: 1, flexShrink: noShrink ? 0 : 1 }, children));
20
+ }
21
+ function HeaderCell({ width, label, noShrink, }) {
22
+ return (React.createElement(Cell, { width: width, noShrink: noShrink },
23
+ React.createElement(Text, { bold: true, dimColor: true, wrap: "truncate" }, label)));
24
+ }
25
+ export default function CycleTable({ cycles, nextPage }) {
26
+ if (cycles.length === 0) {
27
+ return React.createElement(Text, { dimColor: true }, "No cycles matched your filters.");
28
+ }
29
+ return (React.createElement(Box, { flexDirection: "column" },
30
+ React.createElement(Box, null,
31
+ React.createElement(HeaderCell, { width: COLUMNS.id, label: "ID", noShrink: true }),
32
+ React.createElement(HeaderCell, { width: COLUMNS.cycle, label: "Cycle #", noShrink: true }),
33
+ React.createElement(HeaderCell, { width: COLUMNS.order, label: "Order" }),
34
+ React.createElement(HeaderCell, { width: COLUMNS.customer, label: "Customer" }),
35
+ React.createElement(HeaderCell, { width: COLUMNS.method, label: "Method" }),
36
+ React.createElement(HeaderCell, { width: COLUMNS.status, label: "Status" }),
37
+ React.createElement(HeaderCell, { width: COLUMNS.fulfillment, label: "Fulfillment" }),
38
+ React.createElement(HeaderCell, { width: COLUMNS.receival, label: "Receival" }),
39
+ React.createElement(HeaderCell, { width: COLUMNS.start, label: "Rental start" })),
40
+ cycles.map((cycle) => (React.createElement(Box, { key: cycle.id },
41
+ React.createElement(Cell, { width: COLUMNS.id, noShrink: true },
42
+ React.createElement(Text, { wrap: "truncate" }, cycle.id)),
43
+ React.createElement(Cell, { width: COLUMNS.cycle, noShrink: true },
44
+ React.createElement(Text, { dimColor: true, wrap: "truncate" }, cycle.sequentialId == null ? "—" : `#${cycle.sequentialId}`)),
45
+ React.createElement(Cell, { width: COLUMNS.order },
46
+ React.createElement(Text, { wrap: "truncate" }, dash(cycle.shopifyOrderName))),
47
+ React.createElement(Cell, { width: COLUMNS.customer },
48
+ React.createElement(Text, { wrap: "truncate" }, customerName(cycle.customer))),
49
+ React.createElement(Cell, { width: COLUMNS.method },
50
+ React.createElement(Text, { wrap: "truncate" }, titleCase(cycle.methodType))),
51
+ React.createElement(Cell, { width: COLUMNS.status },
52
+ React.createElement(Status, { value: cycle.status })),
53
+ React.createElement(Cell, { width: COLUMNS.fulfillment },
54
+ React.createElement(Status, { value: cycle.fulfillmentStatus })),
55
+ React.createElement(Cell, { width: COLUMNS.receival },
56
+ React.createElement(Status, { value: cycle.receivalStatus })),
57
+ React.createElement(Cell, { width: COLUMNS.start },
58
+ React.createElement(Text, { wrap: "truncate" }, formatDate(cycle.rentalStart)))))),
59
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
60
+ React.createElement(Text, { dimColor: true },
61
+ cycles.length,
62
+ " cycle",
63
+ cycles.length === 1 ? "" : "s",
64
+ " shown \u00B7 use the ID column with `cycles get <id>`"),
65
+ nextPage ? React.createElement(Text, { dimColor: true },
66
+ "Next page: --page ",
67
+ nextPage) : null)));
68
+ }
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { useAsync } from "../hooks/useAsync.js";
3
+ import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
4
+ import CycleTable from "./CycleTable.js";
5
+ import ErrorMessage from "./ErrorMessage.js";
6
+ import ExitError from "./ExitError.js";
7
+ import JsonOutput from "./JsonOutput.js";
8
+ import Loading from "./Loading.js";
9
+ // Fetch a list of cycles (optionally following every page) and render it as a
10
+ // table or raw JSON. Shared by `list` and the read-convenience commands.
11
+ export default function CyclesListView({ filters, all, json }) {
12
+ const resolved = resolveClient();
13
+ const state = useAsync(async () => {
14
+ if (!resolved.client)
15
+ throw new Error(resolved.error);
16
+ return all ? resolved.client.listAllCycles(filters) : resolved.client.listCycles(filters);
17
+ }, []);
18
+ if (state.loading) {
19
+ return json ? null : React.createElement(Loading, { label: "Fetching cycles\u2026" });
20
+ }
21
+ if (state.error) {
22
+ if (json)
23
+ return React.createElement(ExitError, { message: state.error.message });
24
+ return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
25
+ }
26
+ if (json) {
27
+ return React.createElement(JsonOutput, { data: state.data });
28
+ }
29
+ return React.createElement(CycleTable, { cycles: state.data.data, nextPage: state.data.nextPage });
30
+ }
@@ -0,0 +1,13 @@
1
+ import { Box, Text } from "ink";
2
+ import React, { useEffect } from "react";
3
+ export default function ErrorMessage({ message, hint }) {
4
+ // Rendering an error always means the command failed.
5
+ useEffect(() => {
6
+ process.exitCode = 1;
7
+ }, []);
8
+ return (React.createElement(Box, { flexDirection: "column" },
9
+ React.createElement(Text, { color: "red" },
10
+ "\u2716 ",
11
+ message),
12
+ hint ? React.createElement(Text, { dimColor: true }, hint) : null));
13
+ }