@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.
- package/README.md +103 -63
- package/bin/dev.js +1 -1
- package/dist/commands/alt-text.d.ts +64 -15
- package/dist/commands/alt-text.js +277 -65
- package/dist/commands/backfill-project.js +1 -1
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/workspace/index.d.ts +3 -2
- package/dist/commands/workspace/index.js +96 -49
- package/dist/lib/alt-text.d.ts +33 -2
- package/dist/lib/alt-text.js +56 -4
- package/dist/lib/mcp/bracket-args.d.ts +37 -0
- package/dist/lib/mcp/bracket-args.js +65 -0
- package/dist/lib/mcp/define-tool.d.ts +52 -0
- package/dist/lib/mcp/define-tool.js +2 -0
- package/dist/lib/mcp/registry.d.ts +38 -0
- package/dist/lib/mcp/registry.js +98 -0
- package/dist/lib/mcp/server.d.ts +66 -0
- package/dist/lib/mcp/server.js +176 -0
- package/dist/lib/mcp/tools/shopify-common.d.ts +139 -0
- package/dist/lib/mcp/tools/shopify-common.js +167 -0
- package/dist/lib/mcp/tools/shopify-execute.d.ts +2 -0
- package/dist/lib/mcp/tools/shopify-execute.js +105 -0
- package/dist/lib/mcp/tools/shopify-file-delete.d.ts +2 -0
- package/dist/lib/mcp/tools/shopify-file-delete.js +49 -0
- package/dist/lib/mcp/tools/shopify-file-replace.d.ts +2 -0
- package/dist/lib/mcp/tools/shopify-file-replace.js +79 -0
- package/dist/lib/mcp/tools/shopify-file-search.d.ts +2 -0
- package/dist/lib/mcp/tools/shopify-file-search.js +199 -0
- package/dist/lib/mcp/tools/shopify-file-upload.d.ts +2 -0
- package/dist/lib/mcp/tools/shopify-file-upload.js +76 -0
- package/dist/lib/shopify/graphql/AccessScopes.graphql +7 -0
- package/dist/lib/shopify/graphql/CurrentBulkOperation.graphql +8 -0
- package/dist/lib/shopify/graphql/FileCreate.graphql +25 -0
- package/dist/lib/shopify/graphql/FileDelete.graphql +11 -0
- package/dist/lib/shopify/graphql/FileReplace.graphql +26 -0
- package/dist/lib/shopify/graphql/FileStatus.graphql +19 -0
- package/dist/lib/shopify/graphql/FilesBulkQuery.graphql +27 -0
- package/dist/lib/shopify/graphql/ProductsBulkQuery.graphql +27 -0
- package/dist/lib/shopify/graphql/SearchFiles.graphql +36 -0
- package/dist/lib/shopify/graphql/StagedUploadsCreate.graphql +20 -0
- package/dist/lib/shopify/graphql/StartBulkQuery.graphql +16 -0
- package/dist/lib/shopify/graphql/UpdateFileAlt.graphql +9 -0
- package/dist/lib/shopify/shopify.d.ts +228 -0
- package/dist/lib/shopify/shopify.js +662 -0
- package/dist/lib/workspace.d.ts +19 -8
- package/dist/lib/workspace.js +13 -13
- package/oclif.manifest.json +48 -46
- package/package.json +17 -10
- package/dist/hooks/init/check-for-updates.d.ts +0 -3
- package/dist/hooks/init/check-for-updates.js +0 -15
- package/dist/lib/kv-flag.d.ts +0 -15
- package/dist/lib/kv-flag.js +0 -75
- package/dist/lib/rpc.d.ts +0 -69
- 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,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;
|