Path: @/
- Agent-oriented CLI (
nori-shopify) for the Shopify Admin GraphQL API: shop info, orders, customers, products, inventory, discounts, ShopifyQL analytics, and a raw GraphQL passthrough escape hatch. - All output is pretty-printed JSON; all parameters are flags; no colors, no interactivity. Help text and error messages always print the source directory so an agent can read the implementation when confused.
- The behavioral spec lives in @/APPLICATION_SPEC.md — it is the deeper reference for command-to-GraphQL mappings, required access scopes, and error-layer semantics.
- Independent git repo inside the tilework-tech workspace, built so AI agents in Nori sessions can read and manage arbitrary Shopify stores. Intended for npm publication as
nori-shopify-cli(binnori-shopify); not yet published. - Structurally mirrors the sibling nori-luma-cli repo, which served as the template: same dependency-injection program factory, Output abstraction, agentic help/error conventions, and mock-service test harness.
- Talks to exactly one external system: the Shopify Admin GraphQL endpoint at
https://{shop}.myshopify.com/admin/api/{version}/graphql.json, authenticated with a custom-app Admin token (shpat_...) via theX-Shopify-Access-Tokenheader. - Configuration comes entirely from environment variables (
SHOPIFY_SHOP,SHOPIFY_ADMIN_TOKEN, optionalSHOPIFY_API_VERSION, default pinned in @/src/config.ts). There is no config file or stored state.
src/index.ts (entry, only try/catch)
└─ loadConfig (src/config.ts) ← env vars
└─ createShopifyService (src/services/shopify.ts) ← fetch → Shopify Admin GraphQL
└─ createProgram(shopify, out) (src/program.ts) ← commander wiring
└─ src/commands/*.ts ← one file per command group
- @/src/index.ts is the entry point.
--help,--version, and bare invocation are detected before config loading and run with a placeholder config, so help works without credentials. The top-level try/catch is the only error boundary: it printsString(err)to stderr and sets exit code 1. All intermediate layers let errors bubble. - Dependency injection:
createProgram(shopify, out)in @/src/program.ts takes theShopifyServiceinterface and anOutputinterface (write/error/setExitCode, defined in @/src/output.ts). Tests drive the real commander program with a mock service and capture output without spawning processes. configureCommandOutputin @/src/program.ts recursively configures every command and subcommand: colors disabled, writeOut/writeErr routed throughOutput, errors append "Look at the source at {sourceDir}", help appends "Source: {sourceDir}", plus showHelpAfterError and showSuggestionAfterError. This is the agentic-CLI pattern — failures always tell the agent where the source lives.- @/src/services/shopify.ts holds both the
ShopifyServiceinterface (with all result and param types) andcreateShopifyService. It normalizesSHOPIFY_SHOP(bare subdomain, domain, or https URL all accepted) to a myshopify.com endpoint and POSTs GraphQL documents with embedded field-selection constants. - Command files in @/src/commands/ (e.g. orders, products, graphql) translate flags to service params and
JSON.stringifythe result. Shared flag parsers (parseIntStrict,parseJSON, etc.) live in @/src/parse.ts;toGidin @/src/gid.ts wraps bare numeric ids into full GIDs (123→gid://shopify/Order/123) before the service call, while full GIDs pass through unchanged. - Wire-shape mapping: GraphQL connection sub-lists (order lineItems, product variants, inventory levels) come back as
{nodes: [...]}and are flattened to plain arrays before output.discountNodesreturns a union of code/automatic discount types via inline fragments; these are flattened to a uniformDiscountNodewhosediscountTypeis the GraphQL__typename. - Validation that is business logic rather than wire format lives in the command layer:
discounts create-basic-codein @/src/commands/discounts.ts enforces exactly-one-of--percentage/--amount, requires--currencywith--amount, and converts--percentage 10to the fractional0.1the API expects.
- Shopify returns most errors as HTTP 200. The service checks three layers in @/src/services/shopify.ts: HTTP non-2xx status; top-level
errors[]in the JSON body (messages joined,extensions.codelikeTHROTTLEDappended); and mutationuserErrors[](mapped tofield.path: messageand thrown). Null single-object lookups (e.g.order(id)returning null) are converted to "not found" errors rather than printingnull. - The API version is pinned via
SHOPIFY_API_VERSION(default2026-04in @/src/config.ts). Shopify versions quarterly; field deprecations are version-dependent.Customer.emailis deprecated, so the CLI readsdefaultEmailAddress/defaultPhoneNumberinstead. - Scope-dependent behavior surfaces as API errors, not CLI errors: orders older than 60 days need the
read_all_ordersscope, andanalytics query(theshopifyqlQueryfield) needsread_reports. ShopifyQL parse errors come back in-band in theparseErrorsfield with exit code 0 — they are data, not thrown errors. - Test architecture in @/tests/:
createMockShopifyServicein @/tests/helpers.ts is a stateful in-memory implementation (Maps pluslast*Paramsrecording) andrunCommandruns the real commander program withexitOverride, so command tests assert exact stdout/stderr/exit codes. @/tests/services/shopify.test.ts stubs global fetch to assert exact wire shapes (endpoint URL, headers, GraphQL variables). @/tests/cli-startup.test.ts spawns the CLI via tsx to verify help works without credentials. - The
graphqlcommand (@/src/commands/graphql.ts) is a deliberate escape hatch: any Admin API operation not covered by the typed commands can be run raw, returning the unwrappeddatapayload.
Created and maintained by Nori.