@firenet-designs/fnd-cli 2.6.0 → 2.7.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 (54) hide show
  1. package/README.md +103 -63
  2. package/bin/dev.js +1 -1
  3. package/dist/commands/alt-text.d.ts +64 -15
  4. package/dist/commands/alt-text.js +277 -65
  5. package/dist/commands/backfill-project.js +1 -1
  6. package/dist/commands/create-project.js +1 -1
  7. package/dist/commands/workspace/index.d.ts +3 -2
  8. package/dist/commands/workspace/index.js +96 -49
  9. package/dist/lib/alt-text.d.ts +33 -2
  10. package/dist/lib/alt-text.js +56 -4
  11. package/dist/lib/mcp/bracket-args.d.ts +37 -0
  12. package/dist/lib/mcp/bracket-args.js +65 -0
  13. package/dist/lib/mcp/define-tool.d.ts +52 -0
  14. package/dist/lib/mcp/define-tool.js +2 -0
  15. package/dist/lib/mcp/registry.d.ts +38 -0
  16. package/dist/lib/mcp/registry.js +98 -0
  17. package/dist/lib/mcp/server.d.ts +66 -0
  18. package/dist/lib/mcp/server.js +176 -0
  19. package/dist/lib/mcp/tools/shopify-common.d.ts +139 -0
  20. package/dist/lib/mcp/tools/shopify-common.js +167 -0
  21. package/dist/lib/mcp/tools/shopify-execute.d.ts +2 -0
  22. package/dist/lib/mcp/tools/shopify-execute.js +105 -0
  23. package/dist/lib/mcp/tools/shopify-file-delete.d.ts +2 -0
  24. package/dist/lib/mcp/tools/shopify-file-delete.js +49 -0
  25. package/dist/lib/mcp/tools/shopify-file-replace.d.ts +2 -0
  26. package/dist/lib/mcp/tools/shopify-file-replace.js +79 -0
  27. package/dist/lib/mcp/tools/shopify-file-search.d.ts +2 -0
  28. package/dist/lib/mcp/tools/shopify-file-search.js +199 -0
  29. package/dist/lib/mcp/tools/shopify-file-upload.d.ts +2 -0
  30. package/dist/lib/mcp/tools/shopify-file-upload.js +76 -0
  31. package/dist/lib/shopify/graphql/AccessScopes.graphql +7 -0
  32. package/dist/lib/shopify/graphql/CurrentBulkOperation.graphql +8 -0
  33. package/dist/lib/shopify/graphql/FileCreate.graphql +25 -0
  34. package/dist/lib/shopify/graphql/FileDelete.graphql +11 -0
  35. package/dist/lib/shopify/graphql/FileReplace.graphql +26 -0
  36. package/dist/lib/shopify/graphql/FileStatus.graphql +19 -0
  37. package/dist/lib/shopify/graphql/FilesBulkQuery.graphql +27 -0
  38. package/dist/lib/shopify/graphql/ProductsBulkQuery.graphql +27 -0
  39. package/dist/lib/shopify/graphql/SearchFiles.graphql +36 -0
  40. package/dist/lib/shopify/graphql/StagedUploadsCreate.graphql +20 -0
  41. package/dist/lib/shopify/graphql/StartBulkQuery.graphql +16 -0
  42. package/dist/lib/shopify/graphql/UpdateFileAlt.graphql +9 -0
  43. package/dist/lib/shopify/shopify.d.ts +228 -0
  44. package/dist/lib/shopify/shopify.js +662 -0
  45. package/dist/lib/workspace.d.ts +19 -8
  46. package/dist/lib/workspace.js +13 -13
  47. package/oclif.manifest.json +48 -46
  48. package/package.json +17 -10
  49. package/dist/hooks/init/check-for-updates.d.ts +0 -3
  50. package/dist/hooks/init/check-for-updates.js +0 -15
  51. package/dist/lib/kv-flag.d.ts +0 -15
  52. package/dist/lib/kv-flag.js +0 -75
  53. package/dist/lib/rpc.d.ts +0 -69
  54. package/dist/lib/rpc.js +0 -313
@@ -0,0 +1,27 @@
1
+ # The body of the product→image bulk query — the selection run in bulk, passed
2
+ # as the $query variable to StartBulkQuery (never via --query-file). Bulk allows
3
+ # two levels of nested connections, which is exactly products -> media; the
4
+ # MediaImage id on each media row is joined back to its product by __parentId in
5
+ # the JSONL result. No `first`/`after` — bulk expands the connections itself.
6
+ {
7
+ products {
8
+ edges {
9
+ node {
10
+ id
11
+ title
12
+ productType
13
+ vendor
14
+ tags
15
+ media {
16
+ edges {
17
+ node {
18
+ ... on MediaImage {
19
+ id
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,36 @@
1
+ # Page through the file library returning metadata only (no bytes) so a search
2
+ # can check for an existing file or find oversized images cheaply. `query` is a
3
+ # Shopify file-search string (e.g. filename:"hero", media_type:IMAGE) or null for
4
+ # everything. Pull the fields from whichever concrete type applies — MediaImage
5
+ # for images (dimensions + originalSource.fileSize), GenericFile otherwise
6
+ # (originalFileSize).
7
+ query SearchFiles($query: String, $first: Int!, $after: String, $sortKey: FileSortKeys, $reverse: Boolean) {
8
+ files(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
9
+ edges {
10
+ node {
11
+ id
12
+ alt
13
+ ... on MediaImage {
14
+ mimeType
15
+ image {
16
+ url
17
+ width
18
+ height
19
+ }
20
+ originalSource {
21
+ fileSize
22
+ }
23
+ }
24
+ ... on GenericFile {
25
+ mimeType
26
+ url
27
+ originalFileSize
28
+ }
29
+ }
30
+ }
31
+ pageInfo {
32
+ hasNextPage
33
+ endCursor
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,20 @@
1
+ # Reserve a staging slot for a file the CLI is about to upload. Shopify hands
2
+ # back a short-lived upload `url` (Google Cloud Storage) plus the form
3
+ # `parameters` that authorize the POST, and the `resourceUrl` to hand to
4
+ # fileCreate once the bytes have landed. Counts as a mutation to the CLI.
5
+ mutation StagedUploadsCreate($input: [StagedUploadInput!]!) {
6
+ stagedUploadsCreate(input: $input) {
7
+ stagedTargets {
8
+ url
9
+ resourceUrl
10
+ parameters {
11
+ name
12
+ value
13
+ }
14
+ }
15
+ userErrors {
16
+ field
17
+ message
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,16 @@
1
+ # Kick off a bulk query export (the file library, or the product→image map). The
2
+ # query to run in bulk is passed as the $query variable, so it needs no escaping
3
+ # here. Counts as a mutation to the CLI (--allow-mutations) even though it only
4
+ # reads. Only one bulk query can run per store at a time, so callers run these
5
+ # one after another.
6
+ mutation StartBulkQuery($query: String!) {
7
+ bulkOperationRunQuery(query: $query) {
8
+ bulkOperation {
9
+ id
10
+ }
11
+ userErrors {
12
+ field
13
+ message
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ # Write one file's alt text.
2
+ mutation UpdateFileAlt($id: ID!, $alt: String!) {
3
+ fileUpdate(files: [{ id: $id, alt: $alt }]) {
4
+ userErrors {
5
+ field
6
+ message
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Shopify support for `fnd alt-text`.
3
+ *
4
+ * The Webflow path talks to the Data API directly with a bearer token we hold.
5
+ * The Shopify path deliberately does NOT: it shells out to the user's own
6
+ * Shopify CLI (`shopify store execute` / `shopify store auth`), which owns the
7
+ * OAuth session and the token. We never see, store, or print a credential —
8
+ * which is exactly why `alt-text` takes no --api-key for Shopify.
9
+ *
10
+ * Files (the Shopify asset library) are read and captioned through Admin
11
+ * GraphQL, run via `shopify store execute --json`. GraphQL lives under graphql/,
12
+ * never string-interpolated here: named operations (one per file — see docPath)
13
+ * go to the CLI with --query-file and their parameters with --variables, while
14
+ * bulk query bodies (readBulkQuery) are handed to StartBulkQuery as a variable.
15
+ * Both the file list and the product→image map are read with bulk operations —
16
+ * one streamed JSONL each, no pagination and no per-minute throttle.
17
+ *
18
+ * Every CLI call has stderr routed to /dev/null (stdio 'ignore') on purpose:
19
+ * with --json the meaningful output is on stdout, and the CLI's progress/spinner
20
+ * chatter on stderr would otherwise corrupt what we try to JSON.parse.
21
+ */
22
+ /**
23
+ * The scopes the run needs: list files (read) and set their alt text (write),
24
+ * plus read products so a file can be captioned with the context of the product
25
+ * it's attached to. Nothing here should grow without a matching need — the point
26
+ * of asking for the least is that a client store grants the least.
27
+ */
28
+ export declare const REQUIRED_SCOPES: readonly ["read_files", "write_files", "read_products"];
29
+ /**
30
+ * A --dry run only lists the files to count them — it never writes alt text and
31
+ * never maps product context — so read_files is the whole ask. Keeping this
32
+ * minimal means a dry run works against a store that only ever granted the read
33
+ * scope, and doesn't provoke an auth prompt for write access it won't use.
34
+ */
35
+ export declare const DRY_RUN_SCOPES: readonly ["read_files"];
36
+ /**
37
+ * Writing to the store's asset library — uploading, replacing a file's bytes in
38
+ * place, or deleting a file — only needs write access to files. `write_files`
39
+ * subsumes `read_files` (see missingScopes), so this single scope is the whole
40
+ * ask — the least a store has to grant for any of the Shopify `--with-tool` tools.
41
+ */
42
+ export declare const FILE_WRITE_SCOPES: readonly ["write_files"];
43
+ /**
44
+ * The `shopify-file-search` workspace tool only reads file metadata, so
45
+ * `read_files` is its whole ask — the least a store grants for a read-only
46
+ * search, and (unlike the write tools) it never provokes a write-scope prompt.
47
+ */
48
+ export declare const FILE_READ_SCOPES: readonly ["read_files"];
49
+ /**
50
+ * Which of `required` the installation is still missing, given the scopes it
51
+ * already `granted`. A write scope subsumes its read counterpart — a store that
52
+ * granted `write_files` can read files too — so `write_files` satisfies a
53
+ * `read_files` requirement without read_files being listed separately.
54
+ */
55
+ export declare const missingScopes: (granted: string[], required: readonly string[]) => string[];
56
+ /**
57
+ * One image file from the store's asset library. `alt` is null until it's set.
58
+ *
59
+ * `mimeType`, `width`, `height` and `fileSize` are the metadata Shopify already
60
+ * knows, carried so a --dry run can filter on them without downloading the
61
+ * image. Any of them is null when the MediaImage is still processing (or the
62
+ * connection omitted it), which is the caller's signal to fall back to the
63
+ * download; a real run ignores them and measures the bytes it fetches anyway.
64
+ */
65
+ export interface ShopifyImageFile {
66
+ alt: null | string;
67
+ fileSize: null | number;
68
+ height: null | number;
69
+ id: string;
70
+ mimeType: null | string;
71
+ name: string;
72
+ url: string;
73
+ width: null | number;
74
+ }
75
+ /** `mystore`, `mystore.myshopify.com`, or `https://mystore.myshopify.com/` -> `mystore.myshopify.com`. */
76
+ export declare const normalizeStore: (raw: string) => string;
77
+ /**
78
+ * Run an ARBITRARY Admin GraphQL document through the user's Shopify CLI and hand
79
+ * back the CLI's raw JSON output as text. This backs the `shopify-execute`
80
+ * workspace tool, whose whole point is to let the AI run a query it composed —
81
+ * unlike `execute`, the document isn't one of our bundled named operations, so it
82
+ * can't go through `--query-file <bundled>`; instead it's written to a throwaway
83
+ * temp file the CLI reads (and which we always delete). `store execute` refuses
84
+ * mutations unless `--allow-mutations`, so a mutation must set `mutate: true`.
85
+ *
86
+ * Deliberately never throws: the AI needs to SEE the failure text to fix its own
87
+ * query, so a non-zero exit or a GraphQL `errors` payload comes back as
88
+ * `{ ok: false, output }` (the message / the response body) rather than an
89
+ * exception. On success `output` is the response JSON verbatim.
90
+ */
91
+ export declare const runStoreExecute: (bin: string, store: string, opts: {
92
+ mutate?: boolean;
93
+ query: string;
94
+ variables?: Record<string, unknown>;
95
+ }) => {
96
+ ok: boolean;
97
+ output: string;
98
+ };
99
+ /**
100
+ * The scopes the store's app installation currently grants.
101
+ *
102
+ * `authenticated` is false when `shopify store execute` exits non-zero — the
103
+ * task's contract for "this store needs `shopify store auth`". A store that IS
104
+ * authenticated but is missing a scope still comes back authenticated, just with
105
+ * that scope absent from the list, so the caller can tell the two apart.
106
+ */
107
+ export declare const getInstalledScopes: (bin: string, store: string) => {
108
+ authenticated: boolean;
109
+ scopes: string[];
110
+ };
111
+ /**
112
+ * Grant `scopes` to the store via `shopify store auth`. This both authenticates
113
+ * a store that never was and adds any missing scopes to one that already is, so
114
+ * the caller always passes the full required set.
115
+ *
116
+ * stdin/stdout are inherited so the user can complete the browser/login flow the
117
+ * CLI may open; only stderr is suppressed, matching the task's `2> /dev/null`.
118
+ * Returns whether the CLI exited cleanly.
119
+ */
120
+ export declare const authenticate: (bin: string, store: string, scopes: readonly string[]) => boolean;
121
+ /**
122
+ * Every image in the store's asset library, streamed from a single **bulk
123
+ * operation** rather than paginated — one JSONL export, no per-minute throttle,
124
+ * however large the library.
125
+ *
126
+ * `media_type:IMAGE` keeps videos, 3D models and generic files out. `alt` and
127
+ * `id` are on the File interface; the CDN `url`, `mimeType`, dimensions and
128
+ * `originalSource.fileSize` are on the MediaImage. The metadata rides along so a
129
+ * --dry run can filter without downloading (see ShopifyImageFile). A bulk query
130
+ * takes no variables, so — unlike the old paged query — it can't @skip those
131
+ * fields for a real run; they're a few scalars per row in a stream a real run
132
+ * ignores anyway (it measures the bytes it downloads instead).
133
+ *
134
+ * @yields each image file.
135
+ */
136
+ export declare function getImageFiles(bin: string, store: string): AsyncGenerator<ShopifyImageFile>;
137
+ /**
138
+ * A `FileSortKeys` member — the server-side order a scan streams in, so the
139
+ * `limit` early-break yields the true top-N in that order (e.g. size-desc streams
140
+ * the biggest first, so "find large images" is authoritative, not just the first
141
+ * page re-sorted). Values per the Admin schema's FileSortKeys enum.
142
+ */
143
+ export type FileSortKey = 'CREATED_AT' | 'FILENAME' | 'ORIGINAL_UPLOAD_SIZE' | 'UPDATED_AT';
144
+ export declare function searchFiles(bin: string, store: string, opts: {
145
+ pageSize?: number;
146
+ query: null | string;
147
+ reverse?: boolean;
148
+ sortKey?: FileSortKey;
149
+ }): AsyncGenerator<ShopifyImageFile>;
150
+ /** How a store product describes itself — the context a file's pixels can't. */
151
+ export interface ProductContext {
152
+ productType: string;
153
+ tags: string[];
154
+ title: string;
155
+ vendor: string;
156
+ }
157
+ /**
158
+ * Map every product image to the product it belongs to, so a file can be
159
+ * captioned knowing it's (say) snowboard wax rather than a candle.
160
+ *
161
+ * The public Admin API has no field on a MediaImage pointing back to its
162
+ * product, so the relationship is walked from the product side via a **bulk
163
+ * operation** (see ProductsBulkQuery): the whole catalog streams into one JSONL
164
+ * file, which is the right tool when the map has to cover every product at once.
165
+ * The result is folded into a `{mediaImageId -> product}` lookup.
166
+ *
167
+ * An image shared by several products keeps the first product that claims it —
168
+ * a corner case whose only cost is a slightly-off hint, never a wrong caption.
169
+ */
170
+ export declare const getImageProductContext: (bin: string, store: string) => Promise<Map<string, ProductContext>>;
171
+ /** A one-line context hint for the prompt, skipping whatever fields are empty. */
172
+ export declare const formatProductContext: (context: ProductContext) => string;
173
+ /** Set one file's alt text. Throws on any userErrors the mutation reports. */
174
+ export declare const updateFileAlt: (bin: string, store: string, id: string, alt: string) => void;
175
+ /**
176
+ * The outcome of an upload — everything an AI needs to then USE the file, e.g.
177
+ * wire it into a theme setting during theme development:
178
+ * id — the Admin API GID (gid://shopify/MediaImage/…).
179
+ * url — the public CDN url, once processing finishes (null if it hasn't).
180
+ * reference — the `shopify://shop_images/<filename>` value a theme `image_picker`
181
+ * setting stores; the form to drop into settings JSON / liquid.
182
+ * filename — the name the file actually landed under (Shopify may de-dupe it).
183
+ * status — the file's processing status (READY once the url is live).
184
+ */
185
+ export interface UploadResult {
186
+ alt: null | string;
187
+ filename: null | string;
188
+ id: string;
189
+ reference: null | string;
190
+ status: null | string;
191
+ url: null | string;
192
+ }
193
+ /**
194
+ * Upload a local file into the store's asset library, in the three steps the
195
+ * Admin API requires (staged slot → POST bytes → register), then wait for it to
196
+ * finish processing so the returned url/reference are usable. The bytes are read
197
+ * from `path` on THIS machine (the caller); the GraphQL steps go through the
198
+ * user's Shopify CLI like every other operation here — no token ever passes
199
+ * through us. Throws with the CLI's or userErrors' reason on any failure.
200
+ *
201
+ * The result carries everything needed to then USE the file (e.g. reference it
202
+ * from a theme image_picker setting): the GID, CDN url, filename, and the
203
+ * `shopify://shop_images/…` reference. url/reference are null if processing
204
+ * hadn't finished within the wait window — the file still exists; poll later.
205
+ */
206
+ export declare const uploadFile: (bin: string, store: string, opts: {
207
+ alt?: string;
208
+ filename?: string;
209
+ path: string;
210
+ }) => Promise<UploadResult>;
211
+ /**
212
+ * Replace an existing file's contents with a new local file, in place — the same
213
+ * three-step staging as uploadFile, but the final step is fileUpdate on an
214
+ * existing `id` instead of fileCreate, so the file keeps its GID and (typically)
215
+ * its filename/theme reference. The new bytes are read from `path` on THIS
216
+ * machine; every GraphQL step goes through the user's Shopify CLI. Returns the
217
+ * updated file's id/url/reference/status, polling until processing finishes.
218
+ */
219
+ export declare const replaceFile: (bin: string, store: string, opts: {
220
+ alt?: string;
221
+ id: string;
222
+ path: string;
223
+ }) => Promise<UploadResult>;
224
+ /**
225
+ * Delete a file from the store's asset library by GID, returning the id Shopify
226
+ * reports as actually removed. Throws on any userErrors the mutation reports.
227
+ */
228
+ export declare const deleteFile: (bin: string, store: string, id: string) => string;