@firenet-designs/fnd-cli 2.4.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 +194 -57
- package/bin/dev.js +1 -1
- package/dist/commands/alt-text.d.ts +105 -0
- package/dist/commands/alt-text.js +616 -0
- package/dist/commands/backfill-project.js +1 -1
- package/dist/commands/create-project.js +48 -5
- package/dist/commands/workspace/index.d.ts +19 -2
- package/dist/commands/workspace/index.js +171 -56
- package/dist/lib/alt-text.d.ts +87 -0
- package/dist/lib/alt-text.js +196 -0
- package/dist/lib/image-filter.d.ts +43 -0
- package/dist/lib/image-filter.js +71 -0
- 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/webflow.d.ts +80 -0
- package/dist/lib/webflow.js +122 -0
- package/dist/lib/workspace.d.ts +29 -10
- package/dist/lib/workspace.js +74 -39
- package/oclif.manifest.json +162 -78
- package/package.json +21 -10
- package/dist/commands/workspace/cleanup.d.ts +0 -14
- package/dist/commands/workspace/cleanup.js +0 -84
- 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
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,142 @@
|
|
|
1
1
|
{
|
|
2
2
|
"commands": {
|
|
3
|
+
"alt-text": {
|
|
4
|
+
"aliases": [
|
|
5
|
+
"caption"
|
|
6
|
+
],
|
|
7
|
+
"args": {},
|
|
8
|
+
"description": "Generate alt text for a site's images with a local Ollama vision model and write it back.\n\nPick a platform with --platform (webflow or shopify). For Webflow, walks the site asset library and (with --cms) the image fields of CMS collection items and PATCHes each description back through the Data API. For Shopify, walks the store's file library and writes alt text back through the Shopify CLI (which owns the auth — no --api-key). Images are fetched and described one at a time; nothing but the platform API calls leaves your network.\n\nWebflow CMS writes go to STAGING, so publish the site in Webflow to make them live. Any required flag you omit is prompted for.",
|
|
9
|
+
"examples": [
|
|
10
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --api-key <key> --site-id <id> --ollama-host http://localhost:11434 --ollama-model qwen3-vl:8b",
|
|
11
|
+
"<%= config.bin %> <%= command.id %> --platform shopify --site-id mystore",
|
|
12
|
+
"<%= config.bin %> <%= command.id %> --platform shopify --site-id mystore --dry",
|
|
13
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --skip --limit 20",
|
|
14
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --cms --select",
|
|
15
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --cms --only products --only sku",
|
|
16
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --output-stats ./alt-text-run.md",
|
|
17
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --filter \"fileSize>=sizes.KB(100) && width>=100 && height>=100\"",
|
|
18
|
+
"<%= config.bin %> <%= command.id %> --platform webflow --filter \"type !== 'svg' && !url.includes('/icons/')\""
|
|
19
|
+
],
|
|
20
|
+
"flags": {
|
|
21
|
+
"api-key": {
|
|
22
|
+
"description": "Webflow API token (site-scoped). Webflow only — Shopify auth comes from the Shopify CLI. Prompted for if omitted.",
|
|
23
|
+
"name": "api-key",
|
|
24
|
+
"hasDynamicHelp": false,
|
|
25
|
+
"multiple": false,
|
|
26
|
+
"type": "option"
|
|
27
|
+
},
|
|
28
|
+
"cms": {
|
|
29
|
+
"description": "also add alt text to the images in CMS collection items. Webflow only. Off by default — only the site asset library is walked.",
|
|
30
|
+
"name": "cms",
|
|
31
|
+
"allowNo": false,
|
|
32
|
+
"type": "boolean"
|
|
33
|
+
},
|
|
34
|
+
"dry": {
|
|
35
|
+
"description": "report how many images the run would caption without describing or writing anything. Every image is still downloaded and run through --filter so the count is accurate, but nothing reaches the model. The --ollama-host/--ollama-model and --output-stats flags are ignored in this mode, so --dry can be added to (and removed from) a full command without changing the rest.",
|
|
36
|
+
"name": "dry",
|
|
37
|
+
"allowNo": false,
|
|
38
|
+
"type": "boolean"
|
|
39
|
+
},
|
|
40
|
+
"filter": {
|
|
41
|
+
"description": "a JavaScript expression deciding which images are worth describing, e.g. \"fileSize>=sizes.KB(100) && width>=100 && height>=100\". Available: fileSize (bytes), width, height (pixels), url, type (\"webp\", \"png\", \"jpeg\", \"svg\", …), and sizes.KB/MB/GB helpers. Images that fail it are skipped and reported separately. Evaluated after the download, since dimensions can't be known before it.",
|
|
42
|
+
"name": "filter",
|
|
43
|
+
"hasDynamicHelp": false,
|
|
44
|
+
"multiple": false,
|
|
45
|
+
"type": "option"
|
|
46
|
+
},
|
|
47
|
+
"limit": {
|
|
48
|
+
"description": "stop after this many images have been transcribed (counted across assets and CMS together)",
|
|
49
|
+
"name": "limit",
|
|
50
|
+
"hasDynamicHelp": false,
|
|
51
|
+
"multiple": false,
|
|
52
|
+
"type": "option"
|
|
53
|
+
},
|
|
54
|
+
"ollama-host": {
|
|
55
|
+
"description": "base URL of the Ollama server. Prompted for if omitted.",
|
|
56
|
+
"name": "ollama-host",
|
|
57
|
+
"hasDynamicHelp": false,
|
|
58
|
+
"multiple": false,
|
|
59
|
+
"type": "option"
|
|
60
|
+
},
|
|
61
|
+
"ollama-model": {
|
|
62
|
+
"description": "the model used to describe the images. Omit it to pick from the vision-capable models pulled on the host.",
|
|
63
|
+
"name": "ollama-model",
|
|
64
|
+
"hasDynamicHelp": false,
|
|
65
|
+
"multiple": false,
|
|
66
|
+
"type": "option"
|
|
67
|
+
},
|
|
68
|
+
"only": {
|
|
69
|
+
"dependsOn": [
|
|
70
|
+
"cms"
|
|
71
|
+
],
|
|
72
|
+
"description": "only touch these CMS collections, matched case-insensitively against a collection's name or slug. Repeat the flag for more than one.",
|
|
73
|
+
"exclusive": [
|
|
74
|
+
"select"
|
|
75
|
+
],
|
|
76
|
+
"name": "only",
|
|
77
|
+
"hasDynamicHelp": false,
|
|
78
|
+
"multiple": true,
|
|
79
|
+
"type": "option"
|
|
80
|
+
},
|
|
81
|
+
"output-stats": {
|
|
82
|
+
"description": "also write the run stats to this file as Markdown: totals, a row per image (url, description, tokens, time, size), and the failures. Parent directories are created.",
|
|
83
|
+
"name": "output-stats",
|
|
84
|
+
"hasDynamicHelp": false,
|
|
85
|
+
"multiple": false,
|
|
86
|
+
"type": "option"
|
|
87
|
+
},
|
|
88
|
+
"platform": {
|
|
89
|
+
"description": "which platform to run against. Prompted for if omitted.",
|
|
90
|
+
"name": "platform",
|
|
91
|
+
"hasDynamicHelp": false,
|
|
92
|
+
"multiple": false,
|
|
93
|
+
"options": [
|
|
94
|
+
"webflow",
|
|
95
|
+
"shopify"
|
|
96
|
+
],
|
|
97
|
+
"type": "option"
|
|
98
|
+
},
|
|
99
|
+
"select": {
|
|
100
|
+
"dependsOn": [
|
|
101
|
+
"cms"
|
|
102
|
+
],
|
|
103
|
+
"description": "fetch the CMS collections and pick which ones to process interactively",
|
|
104
|
+
"exclusive": [
|
|
105
|
+
"only"
|
|
106
|
+
],
|
|
107
|
+
"name": "select",
|
|
108
|
+
"allowNo": false,
|
|
109
|
+
"type": "boolean"
|
|
110
|
+
},
|
|
111
|
+
"site-id": {
|
|
112
|
+
"description": "Webflow site ID, or the Shopify store (mystore or mystore.myshopify.com). Prompted for if omitted.",
|
|
113
|
+
"name": "site-id",
|
|
114
|
+
"hasDynamicHelp": false,
|
|
115
|
+
"multiple": false,
|
|
116
|
+
"type": "option"
|
|
117
|
+
},
|
|
118
|
+
"skip": {
|
|
119
|
+
"description": "leave images that already have alt text alone instead of overwriting them",
|
|
120
|
+
"name": "skip",
|
|
121
|
+
"allowNo": false,
|
|
122
|
+
"type": "boolean"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"hasDynamicHelp": false,
|
|
126
|
+
"hiddenAliases": [],
|
|
127
|
+
"id": "alt-text",
|
|
128
|
+
"pluginAlias": "@firenet-designs/fnd-cli",
|
|
129
|
+
"pluginName": "@firenet-designs/fnd-cli",
|
|
130
|
+
"pluginType": "core",
|
|
131
|
+
"strict": true,
|
|
132
|
+
"enableJsonFlag": false,
|
|
133
|
+
"isESM": true,
|
|
134
|
+
"relativePath": [
|
|
135
|
+
"dist",
|
|
136
|
+
"commands",
|
|
137
|
+
"alt-text.js"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
3
140
|
"backfill-project": {
|
|
4
141
|
"aliases": [],
|
|
5
142
|
"args": {
|
|
@@ -80,7 +217,7 @@
|
|
|
80
217
|
"required": false
|
|
81
218
|
}
|
|
82
219
|
},
|
|
83
|
-
"description": "Scaffold a new client project: git on branch production, ignore files, Shopify theme pull, a Claude-generated CLAUDE.md, then a private GitHub repo under the FireNet-Designs org.\n\nRequires the claude CLI (npm install -g @anthropic-ai/claude-code) for the CLAUDE.md step.\n\nGitHub auth comes from YOUR environment — run `gh auth login` once, or export GH_TOKEN in your shell profile. This CLI never stores credentials. Override the org with FND_GH_ORG.",
|
|
220
|
+
"description": "Scaffold a new client project: git on branch production, ignore files, Shopify theme pull, a Claude-generated CLAUDE.md, then a private GitHub repo under the FireNet-Designs org with production and staging branches pushed.\n\nRequires the claude CLI (npm install -g @anthropic-ai/claude-code) for the CLAUDE.md step.\n\nGitHub auth comes from YOUR environment — run `gh auth login` once, or export GH_TOKEN in your shell profile. This CLI never stores credentials. Override the org with FND_GH_ORG.",
|
|
84
221
|
"examples": [
|
|
85
222
|
"<%= config.bin %> <%= command.id %>",
|
|
86
223
|
"<%= config.bin %> <%= command.id %> my-store",
|
|
@@ -183,76 +320,6 @@
|
|
|
183
320
|
"token.js"
|
|
184
321
|
]
|
|
185
322
|
},
|
|
186
|
-
"workspace:cleanup": {
|
|
187
|
-
"aliases": [],
|
|
188
|
-
"args": {},
|
|
189
|
-
"description": "Tear down a leftover workspace — use this if a `workspace` session dropped before it could clean up after itself.\n\nTerminates any Mutagen sync sessions this machine started for the directory. Pass --devtools or --rpc to also strip the matching MCP entries from the remote (only do this if the dropped session used those flags). With no --remote-dir, it targets the same path `workspace` would use for the current directory. The synced files themselves are left in place unless you pass --delete-remote-dir.",
|
|
190
|
-
"examples": [
|
|
191
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host",
|
|
192
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --remote-dir /home/fnd/cole/fnd-cli",
|
|
193
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools",
|
|
194
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --rpc",
|
|
195
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir"
|
|
196
|
-
],
|
|
197
|
-
"flags": {
|
|
198
|
-
"delete-remote-dir": {
|
|
199
|
-
"description": "also delete the remote workspace directory (by default the synced files are left in place)",
|
|
200
|
-
"name": "delete-remote-dir",
|
|
201
|
-
"allowNo": false,
|
|
202
|
-
"type": "boolean"
|
|
203
|
-
},
|
|
204
|
-
"devtools": {
|
|
205
|
-
"description": "also strip the chrome-devtools MCP entry from the remote (only if the dropped session used --devtools)",
|
|
206
|
-
"name": "devtools",
|
|
207
|
-
"allowNo": false,
|
|
208
|
-
"type": "boolean"
|
|
209
|
-
},
|
|
210
|
-
"remote-base": {
|
|
211
|
-
"description": "base dir on the remote, used to derive the default remote directory path",
|
|
212
|
-
"name": "remote-base",
|
|
213
|
-
"default": "/home/fnd",
|
|
214
|
-
"hasDynamicHelp": false,
|
|
215
|
-
"multiple": false,
|
|
216
|
-
"type": "option"
|
|
217
|
-
},
|
|
218
|
-
"remote-dir": {
|
|
219
|
-
"description": "exact remote directory to target (defaults to the current dir mapping)",
|
|
220
|
-
"name": "remote-dir",
|
|
221
|
-
"hasDynamicHelp": false,
|
|
222
|
-
"multiple": false,
|
|
223
|
-
"type": "option"
|
|
224
|
-
},
|
|
225
|
-
"rpc": {
|
|
226
|
-
"description": "also strip the local-shell MCP entry from the remote (only if the dropped session used --rpc)",
|
|
227
|
-
"name": "rpc",
|
|
228
|
-
"allowNo": false,
|
|
229
|
-
"type": "boolean"
|
|
230
|
-
},
|
|
231
|
-
"ssh": {
|
|
232
|
-
"description": "remote to connect to, as user@host",
|
|
233
|
-
"name": "ssh",
|
|
234
|
-
"required": true,
|
|
235
|
-
"hasDynamicHelp": false,
|
|
236
|
-
"multiple": false,
|
|
237
|
-
"type": "option"
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
"hasDynamicHelp": false,
|
|
241
|
-
"hiddenAliases": [],
|
|
242
|
-
"id": "workspace:cleanup",
|
|
243
|
-
"pluginAlias": "@firenet-designs/fnd-cli",
|
|
244
|
-
"pluginName": "@firenet-designs/fnd-cli",
|
|
245
|
-
"pluginType": "core",
|
|
246
|
-
"strict": true,
|
|
247
|
-
"enableJsonFlag": false,
|
|
248
|
-
"isESM": true,
|
|
249
|
-
"relativePath": [
|
|
250
|
-
"dist",
|
|
251
|
-
"commands",
|
|
252
|
-
"workspace",
|
|
253
|
-
"cleanup.js"
|
|
254
|
-
]
|
|
255
|
-
},
|
|
256
323
|
"workspace": {
|
|
257
324
|
"aliases": [],
|
|
258
325
|
"args": {},
|
|
@@ -264,11 +331,21 @@
|
|
|
264
331
|
"<%= config.bin %> <%= command.id %> --ssh user@host --ignore-vcs",
|
|
265
332
|
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9222",
|
|
266
333
|
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9333:9222",
|
|
267
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --
|
|
268
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --
|
|
269
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-
|
|
334
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool shopify-file-upload --site-id mystore",
|
|
335
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool shopify-file-upload --with-tool shopify-file-search --with-tool \"shopify-file-delete[ask]\" --site-id mystore",
|
|
336
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool shopify-file-upload --with-tool shopify-file-replace --with-tool shopify-file-delete --site-id mystore.myshopify.com",
|
|
337
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool \"shopify-execute[scopes=read_products+read_orders]\" --site-id mystore",
|
|
338
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool \"shopify-execute[ask,scopes=all]\" --site-id mystore",
|
|
339
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir",
|
|
340
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --with-tool shopify-file-upload --site-id mystore --cleanup"
|
|
270
341
|
],
|
|
271
342
|
"flags": {
|
|
343
|
+
"cleanup": {
|
|
344
|
+
"description": "tear down a leftover session instead of opening one — don't connect, just terminate this directory's Mutagen sync and (using whatever other flags are set) strip the remote --devtools/--with-tool MCP entries and, with --delete-remote-dir, remove the remote dir. Re-run your original command with --cleanup appended after a session that dropped without cleaning up.",
|
|
345
|
+
"name": "cleanup",
|
|
346
|
+
"allowNo": false,
|
|
347
|
+
"type": "boolean"
|
|
348
|
+
},
|
|
272
349
|
"delete-remote-dir": {
|
|
273
350
|
"description": "on exit, delete the remote workspace directory instead of leaving the synced copy in place",
|
|
274
351
|
"name": "delete-remote-dir",
|
|
@@ -296,9 +373,9 @@
|
|
|
296
373
|
"multiple": false,
|
|
297
374
|
"type": "option"
|
|
298
375
|
},
|
|
299
|
-
"
|
|
300
|
-
"description": "
|
|
301
|
-
"name": "
|
|
376
|
+
"site-id": {
|
|
377
|
+
"description": "the Shopify store (mystore or mystore.myshopify.com) the Shopify --with-tool tools operate on. Required when any Shopify tool (shopify-file-* or shopify-execute) is selected; fixing the store here is a safety boundary — the remote AI cannot target another store.",
|
|
378
|
+
"name": "site-id",
|
|
302
379
|
"hasDynamicHelp": false,
|
|
303
380
|
"multiple": false,
|
|
304
381
|
"type": "option"
|
|
@@ -321,6 +398,13 @@
|
|
|
321
398
|
"hasDynamicHelp": false,
|
|
322
399
|
"multiple": false,
|
|
323
400
|
"type": "option"
|
|
401
|
+
},
|
|
402
|
+
"with-tool": {
|
|
403
|
+
"description": "expose an fnd tool to Claude on the remote via a loopback MCP server on THIS machine (the one running fnd workspace), reached through a reverse tunnel. Repeatable. Value is <name> or <name>[options]; available: shopify-execute[<scopes=all|<list>[,ask]>], shopify-file-delete[[<ask>]], shopify-file-replace[[<ask>]], shopify-file-search[[<ask>]], shopify-file-upload[[<ask>]]. e.g. shopify-file-upload (with --site-id mystore) lets the remote AI upload files from the workspace into that Shopify store using your local Shopify CLI; shopify-file-replace and shopify-file-delete add in-place replace and delete; shopify-file-search looks up files by name/size/type/url (metadata only, no image bytes) to dedupe before upload or find oversized images; shopify-execute runs arbitrary Admin GraphQL and REQUIRES a scopes option — shopify-execute[scopes=all] for broad access or shopify-execute[scopes=read_products+write_orders] for an explicit set (which the store is then trimmed to, revoking anything extra, so the AI is held to least privilege). Append ,ask (e.g. shopify-file-delete[ask], shopify-execute[ask,scopes=all]) to make that tool prompt for confirmation before EVERY call, even under the remote's auto-accept/bypass permissions — use it to gate the powerful/destructive tools. Prerequisites (auth, scopes) are checked and set up before connecting.",
|
|
404
|
+
"name": "with-tool",
|
|
405
|
+
"hasDynamicHelp": false,
|
|
406
|
+
"multiple": true,
|
|
407
|
+
"type": "option"
|
|
324
408
|
}
|
|
325
409
|
},
|
|
326
410
|
"hasDynamicHelp": false,
|
|
@@ -340,5 +424,5 @@
|
|
|
340
424
|
]
|
|
341
425
|
}
|
|
342
426
|
},
|
|
343
|
-
"version": "2.
|
|
427
|
+
"version": "2.7.0"
|
|
344
428
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firenet-designs/fnd-cli",
|
|
3
3
|
"description": "A new CLI generated with oclif",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0",
|
|
5
5
|
"author": "Cole Denslow",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Justin Schellenberg"
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
},
|
|
12
12
|
"bugs": "https://github.com/FireNet-Designs/fnd-cli/issues",
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@inquirer/prompts": "^7.10.1",
|
|
14
15
|
"@oclif/core": "^4",
|
|
15
16
|
"@oclif/plugin-help": "^6",
|
|
16
17
|
"@oclif/plugin-plugins": "^5",
|
|
18
|
+
"@oclif/plugin-warn-if-update-available": "^3.1.71",
|
|
19
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
17
20
|
"chalk": "^5.6.2",
|
|
21
|
+
"ollama": "^0.6.3",
|
|
18
22
|
"ora": "^9.3.0",
|
|
23
|
+
"sharp": "^0.34.5",
|
|
19
24
|
"simple-git": "^3.32.2",
|
|
20
25
|
"zod": "^4.4.3"
|
|
21
26
|
},
|
|
@@ -49,6 +54,12 @@
|
|
|
49
54
|
"./oclif.manifest.json"
|
|
50
55
|
],
|
|
51
56
|
"homepage": "https://github.com/FireNet-Designs/fnd-cli",
|
|
57
|
+
"imports": {
|
|
58
|
+
"#*": {
|
|
59
|
+
"development": "./src/*",
|
|
60
|
+
"default": "./dist/*"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
52
63
|
"keywords": [
|
|
53
64
|
"oclif"
|
|
54
65
|
],
|
|
@@ -63,14 +74,14 @@
|
|
|
63
74
|
"bin": "fnd",
|
|
64
75
|
"dirname": "fnd-cli",
|
|
65
76
|
"commands": "./dist/commands",
|
|
66
|
-
"hooks": {
|
|
67
|
-
"init": [
|
|
68
|
-
"./dist/hooks/init/check-for-updates"
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
77
|
"plugins": [
|
|
72
|
-
"@oclif/plugin-help"
|
|
78
|
+
"@oclif/plugin-help",
|
|
79
|
+
"@oclif/plugin-warn-if-update-available"
|
|
73
80
|
],
|
|
81
|
+
"warn-if-update-available": {
|
|
82
|
+
"timeoutInDays": 1,
|
|
83
|
+
"message": "💡 Version <%= chalk.greenBright(latest) %> available! Run <%= chalk.greenBright('npm i -g ' + config.name) %> to update to the latest version."
|
|
84
|
+
},
|
|
74
85
|
"topicSeparator": " ",
|
|
75
86
|
"topics": {
|
|
76
87
|
"hello": {
|
|
@@ -86,13 +97,13 @@
|
|
|
86
97
|
"url": "git+https://github.com/FireNet-Designs/fnd-cli.git"
|
|
87
98
|
},
|
|
88
99
|
"scripts": {
|
|
89
|
-
"build": "shx rm -rf dist && tsc -b",
|
|
100
|
+
"build": "shx rm -rf dist && tsc -b && shx cp -r src/lib/shopify/graphql dist/lib/shopify/graphql",
|
|
90
101
|
"lint": "eslint",
|
|
91
102
|
"pack": "npm run prepack && npm run postpack",
|
|
92
103
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
93
104
|
"posttest": "npm run lint",
|
|
94
|
-
"prepack": "oclif manifest && oclif readme",
|
|
95
|
-
"version": "oclif readme",
|
|
105
|
+
"prepack": "oclif manifest && oclif readme && node scripts/strip-os-from-readme.mjs",
|
|
106
|
+
"version": "oclif readme && node scripts/strip-os-from-readme.mjs",
|
|
96
107
|
"prerelease": "npm run build && npm run prepack",
|
|
97
108
|
"release": "npm run build && npm run prepack && npm publish"
|
|
98
109
|
},
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
export default class WorkspaceCleanup extends Command {
|
|
3
|
-
static description: string;
|
|
4
|
-
static examples: string[];
|
|
5
|
-
static flags: {
|
|
6
|
-
'delete-remote-dir': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
devtools: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
-
'remote-base': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
'remote-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
rpc: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
-
ssh: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
};
|
|
13
|
-
run(): Promise<void>;
|
|
14
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { basename } from 'node:path';
|
|
4
|
-
import { buildContext, buildMutagenTerminateSelectorArgs, DEFAULT_MOUNT_BASE, hasMutagen, hasSshClient, parseSshTarget, runMutagen, runRemoteCleanup, slugify, } from '../../lib/workspace.js';
|
|
5
|
-
export default class WorkspaceCleanup extends Command {
|
|
6
|
-
static description = 'Tear down a leftover workspace — use this if a `workspace` session dropped before it could clean up after itself.\n\nTerminates any Mutagen sync sessions this machine started for the directory. Pass --devtools or --rpc to also strip the matching MCP entries from the remote (only do this if the dropped session used those flags). With no --remote-dir, it targets the same path `workspace` would use for the current directory. The synced files themselves are left in place unless you pass --delete-remote-dir.';
|
|
7
|
-
static examples = [
|
|
8
|
-
'<%= config.bin %> <%= command.id %> --ssh user@host',
|
|
9
|
-
'<%= config.bin %> <%= command.id %> --ssh user@host --remote-dir /home/fnd/cole/fnd-cli',
|
|
10
|
-
'<%= config.bin %> <%= command.id %> --ssh user@host --devtools',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> --ssh user@host --rpc',
|
|
12
|
-
'<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir',
|
|
13
|
-
];
|
|
14
|
-
static flags = {
|
|
15
|
-
'delete-remote-dir': Flags.boolean({
|
|
16
|
-
default: false,
|
|
17
|
-
description: 'also delete the remote workspace directory (by default the synced files are left in place)',
|
|
18
|
-
}),
|
|
19
|
-
devtools: Flags.boolean({
|
|
20
|
-
default: false,
|
|
21
|
-
description: 'also strip the chrome-devtools MCP entry from the remote (only if the dropped session used --devtools)',
|
|
22
|
-
}),
|
|
23
|
-
'remote-base': Flags.string({
|
|
24
|
-
default: DEFAULT_MOUNT_BASE,
|
|
25
|
-
description: 'base dir on the remote, used to derive the default remote directory path',
|
|
26
|
-
}),
|
|
27
|
-
'remote-dir': Flags.string({
|
|
28
|
-
description: 'exact remote directory to target (defaults to the current dir mapping)',
|
|
29
|
-
}),
|
|
30
|
-
rpc: Flags.boolean({
|
|
31
|
-
default: false,
|
|
32
|
-
description: 'also strip the local-shell MCP entry from the remote (only if the dropped session used --rpc)',
|
|
33
|
-
}),
|
|
34
|
-
ssh: Flags.string({
|
|
35
|
-
description: 'remote to connect to, as user@host',
|
|
36
|
-
required: true,
|
|
37
|
-
}),
|
|
38
|
-
};
|
|
39
|
-
async run() {
|
|
40
|
-
const { flags } = await this.parse(WorkspaceCleanup);
|
|
41
|
-
if (!hasSshClient()) {
|
|
42
|
-
this.error('No `ssh` client found on PATH. Install OpenSSH client and try again.', { code: '1' });
|
|
43
|
-
}
|
|
44
|
-
const target = parseSshTarget(flags.ssh);
|
|
45
|
-
const target2 = `${target.user}@${target.host}`;
|
|
46
|
-
const remoteDir = flags['remote-dir'] ??
|
|
47
|
-
buildContext({ cwd: process.cwd(), remoteBase: flags['remote-base'] }).remoteDir;
|
|
48
|
-
const slug = slugify(basename(remoteDir));
|
|
49
|
-
this.log(chalk.bold('Cleaning up workspace'));
|
|
50
|
-
this.log(` ${chalk.dim('remote:')} ${target2}`);
|
|
51
|
-
this.log(` ${chalk.dim('remote dir:')} ${remoteDir}`);
|
|
52
|
-
this.log('');
|
|
53
|
-
// Terminate any lingering sync sessions for this directory (a local Mutagen op).
|
|
54
|
-
if (hasMutagen()) {
|
|
55
|
-
this.log(chalk.dim('Terminating any leftover Mutagen sync sessions…'));
|
|
56
|
-
await runMutagen(buildMutagenTerminateSelectorArgs(slug)).catch(() => 1);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
this.log(chalk.yellow('Mutagen CLI not found on PATH — skipping sync termination.'));
|
|
60
|
-
}
|
|
61
|
-
// Strip any MCP config this workspace left on the remote (only when asked),
|
|
62
|
-
// and optionally delete the synced directory itself.
|
|
63
|
-
if (flags.devtools) {
|
|
64
|
-
this.log(chalk.dim('Removing any leftover chrome-devtools MCP config on the remote…'));
|
|
65
|
-
}
|
|
66
|
-
if (flags.rpc) {
|
|
67
|
-
this.log(chalk.dim('Removing any leftover local-shell MCP config on the remote…'));
|
|
68
|
-
}
|
|
69
|
-
if (flags['delete-remote-dir']) {
|
|
70
|
-
this.log(chalk.dim('Deleting the remote workspace directory…'));
|
|
71
|
-
}
|
|
72
|
-
const code = await runRemoteCleanup(target2, remoteDir, {
|
|
73
|
-
deleteRemoteDir: flags['delete-remote-dir'],
|
|
74
|
-
removeDevtoolsMcp: flags.devtools,
|
|
75
|
-
removeRpcMcp: flags.rpc,
|
|
76
|
-
});
|
|
77
|
-
if (code === 0) {
|
|
78
|
-
this.log(chalk.green('✓ Done.'));
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
this.error(`Remote cleanup ssh session exited with code ${code}.`, { code: '1' });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { exec } from 'node:child_process';
|
|
3
|
-
const hook = async function (opts) {
|
|
4
|
-
const newestVersion = await new Promise((resolve, reject) => {
|
|
5
|
-
exec(`npm view @firenet-designs/fnd-cli version`, (error, stdout) => {
|
|
6
|
-
if (error)
|
|
7
|
-
return reject(error);
|
|
8
|
-
resolve(stdout.trim());
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
if (newestVersion !== opts.config.pjson.version) {
|
|
12
|
-
console.log(chalk.yellow('💡 Version', chalk.green(newestVersion), 'available! Run', chalk.green('`npm i -g @firenet-designs/fnd-cli`'), 'to update to the latest version!\n'));
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
export default hook;
|
package/dist/lib/kv-flag.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
type KvShape = Record<string, z.ZodType>;
|
|
3
|
-
/** Usage string for the schema, required keys first: `a=<...>,b=<...>[,c=<...>]`. */
|
|
4
|
-
export declare const kvUsage: (schema: z.ZodObject<KvShape>) => string;
|
|
5
|
-
/** Example flag value built from each field's example meta (falls back to its hint), required keys first. */
|
|
6
|
-
export declare const kvExample: (schema: z.ZodObject<KvShape>, opts?: {
|
|
7
|
-
requiredOnly?: boolean;
|
|
8
|
-
}) => string;
|
|
9
|
-
/**
|
|
10
|
-
* Parse a key=value flag string against the schema. Keys are case-insensitive
|
|
11
|
-
* and order-independent; unknown keys, duplicate keys, and missing required
|
|
12
|
-
* keys are hard errors naming the flag and its usage.
|
|
13
|
-
*/
|
|
14
|
-
export declare const parseKvFlag: <T extends KvShape>(flag: string, raw: string, schema: z.ZodObject<T>) => z.output<z.ZodObject<T>>;
|
|
15
|
-
export {};
|
package/dist/lib/kv-flag.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const fieldMeta = (field) => (field.meta() ?? {});
|
|
2
|
-
const isOptional = (field) => field.def.type === 'default' || field.def.type === 'optional';
|
|
3
|
-
/** Unwrap default/optional/preprocess wrappers down to the schema describing the value. */
|
|
4
|
-
const unwrap = (field) => {
|
|
5
|
-
const { def } = field;
|
|
6
|
-
if (def.type === 'default' || def.type === 'optional') {
|
|
7
|
-
return unwrap(def.innerType);
|
|
8
|
-
}
|
|
9
|
-
if (def.type === 'pipe') {
|
|
10
|
-
return unwrap(def.out);
|
|
11
|
-
}
|
|
12
|
-
return field;
|
|
13
|
-
};
|
|
14
|
-
/** The <placeholder> for a field's value: explicit hint meta, enum options, or "value". */
|
|
15
|
-
const fieldHint = (field) => {
|
|
16
|
-
const { hint } = fieldMeta(field);
|
|
17
|
-
if (hint)
|
|
18
|
-
return hint;
|
|
19
|
-
const inner = unwrap(field);
|
|
20
|
-
if (inner.def.type === 'enum') {
|
|
21
|
-
return Object.values(inner.def.entries).join('|');
|
|
22
|
-
}
|
|
23
|
-
return 'value';
|
|
24
|
-
};
|
|
25
|
-
/** Usage string for the schema, required keys first: `a=<...>,b=<...>[,c=<...>]`. */
|
|
26
|
-
export const kvUsage = (schema) => {
|
|
27
|
-
const fields = Object.entries(schema.shape);
|
|
28
|
-
const required = fields.filter(([, f]) => !isOptional(f));
|
|
29
|
-
const optional = fields.filter(([, f]) => isOptional(f));
|
|
30
|
-
const pair = ([key, field]) => `${key}=<${fieldHint(field)}>`;
|
|
31
|
-
return required.map((f) => pair(f)).join(',') + optional.map((f) => `[,${pair(f)}]`).join('');
|
|
32
|
-
};
|
|
33
|
-
/** Example flag value built from each field's example meta (falls back to its hint), required keys first. */
|
|
34
|
-
export const kvExample = (schema, opts) => {
|
|
35
|
-
const fields = Object.entries(schema.shape).filter(([, field]) => !(opts?.requiredOnly && isOptional(field)));
|
|
36
|
-
return [...fields.filter(([, f]) => !isOptional(f)), ...fields.filter(([, f]) => isOptional(f))]
|
|
37
|
-
.map(([key, field]) => `${key}=${fieldMeta(field).example ?? fieldHint(field)}`)
|
|
38
|
-
.join(',');
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Parse a key=value flag string against the schema. Keys are case-insensitive
|
|
42
|
-
* and order-independent; unknown keys, duplicate keys, and missing required
|
|
43
|
-
* keys are hard errors naming the flag and its usage.
|
|
44
|
-
*/
|
|
45
|
-
export const parseKvFlag = (flag, raw, schema) => {
|
|
46
|
-
const usage = `${flag} takes comma-separated key=value pairs: ${kvUsage(schema)}`;
|
|
47
|
-
const known = Object.keys(schema.shape);
|
|
48
|
-
const pairs = new Map();
|
|
49
|
-
for (const part of raw.trim().split(',')) {
|
|
50
|
-
const eq = part.indexOf('=');
|
|
51
|
-
if (eq === -1) {
|
|
52
|
-
throw new Error(`${usage} (got "${raw}")`);
|
|
53
|
-
}
|
|
54
|
-
const key = part.slice(0, eq).trim().toLowerCase();
|
|
55
|
-
if (!known.includes(key)) {
|
|
56
|
-
throw new Error(`${flag} has no "${key}" option. ${usage}`);
|
|
57
|
-
}
|
|
58
|
-
if (pairs.has(key)) {
|
|
59
|
-
throw new Error(`${flag} "${key}" was given more than once (got "${raw}")`);
|
|
60
|
-
}
|
|
61
|
-
pairs.set(key, part.slice(eq + 1).trim());
|
|
62
|
-
}
|
|
63
|
-
for (const key of known) {
|
|
64
|
-
if (!isOptional(schema.shape[key]) && !pairs.has(key)) {
|
|
65
|
-
throw new Error(`${flag} needs "${key}". ${usage}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const result = schema.safeParse(Object.fromEntries(pairs));
|
|
69
|
-
if (!result.success) {
|
|
70
|
-
const issue = result.error.issues[0];
|
|
71
|
-
const at = issue.path.join('.');
|
|
72
|
-
throw new Error(`${flag}${at ? ` ${at}` : ''}: ${issue.message}. ${usage}`);
|
|
73
|
-
}
|
|
74
|
-
return result.data;
|
|
75
|
-
};
|
package/dist/lib/rpc.d.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { PortPair } from './workspace.js';
|
|
3
|
-
/**
|
|
4
|
-
* The --rpc local-command server.
|
|
5
|
-
*
|
|
6
|
-
* Topology: this machine (the one that ran `fnd workspace`) runs a tiny MCP
|
|
7
|
-
* server — Streamable HTTP transport, implemented on node:http with no
|
|
8
|
-
* dependencies — bound to 127.0.0.1:<local>. The workspace's `ssh -R` reverse
|
|
9
|
-
* tunnel exposes it on the REMOTE at 127.0.0.1:<remote>, where the `claude` CLI
|
|
10
|
-
* registers it as an HTTP MCP server. When the AI on the remote calls the
|
|
11
|
-
* `run_local_command` tool, the command executes HERE, on the calling machine,
|
|
12
|
-
* under the shell chosen in the flag.
|
|
13
|
-
*
|
|
14
|
-
* The server binds loopback only; the sole way in from outside is the reverse
|
|
15
|
-
* tunnel, which lives exactly as long as the ssh session.
|
|
16
|
-
*/
|
|
17
|
-
declare const ShellSchema: z.ZodEnum<{
|
|
18
|
-
zsh: "zsh";
|
|
19
|
-
bash: "bash";
|
|
20
|
-
batch: "batch";
|
|
21
|
-
powershell: "powershell";
|
|
22
|
-
sh: "sh";
|
|
23
|
-
}>;
|
|
24
|
-
export declare const RPC_SHELLS: ("zsh" | "bash" | "batch" | "powershell" | "sh")[];
|
|
25
|
-
export type RpcShell = z.infer<typeof ShellSchema>;
|
|
26
|
-
/** Usage and example strings for the --rpc flag, derived from the schema. */
|
|
27
|
-
export declare const RPC_FLAG_USAGE: string;
|
|
28
|
-
export declare const RPC_FLAG_EXAMPLES: {
|
|
29
|
-
full: string;
|
|
30
|
-
required: string;
|
|
31
|
-
};
|
|
32
|
-
export interface RpcConfig {
|
|
33
|
-
/** local = port the server binds on this machine; remote = port opened on the workspace host via `ssh -R`. */
|
|
34
|
-
ports: PortPair;
|
|
35
|
-
/** Whether the shell loads its startup files (rc/profile). POSIX shells run with -i, powershell without -NoProfile. */
|
|
36
|
-
profile: boolean;
|
|
37
|
-
/** Shell used to execute commands on this machine. */
|
|
38
|
-
shell: RpcShell;
|
|
39
|
-
}
|
|
40
|
-
/** Handle for a running RPC server. */
|
|
41
|
-
export interface RpcServer {
|
|
42
|
-
close: () => Promise<void>;
|
|
43
|
-
}
|
|
44
|
-
export declare const RPC_TOOL_NAME = "run_local_command";
|
|
45
|
-
/**
|
|
46
|
-
* The shell `fnd workspace` was called from: the parent process when it is a
|
|
47
|
-
* supported shell, else $SHELL, else undefined (e.g. invoked from a script).
|
|
48
|
-
*/
|
|
49
|
-
export declare const detectCallingShell: () => RpcShell | undefined;
|
|
50
|
-
/**
|
|
51
|
-
* Parse the --rpc value: comma-separated key=value pairs per RpcFlagSchema.
|
|
52
|
-
* port=<port> | port=<remote>:<local> — required; `local` is this machine
|
|
53
|
-
* (where commands run), `remote` the port opened on the workspace host.
|
|
54
|
-
* The single-port form uses the same port on both ends.
|
|
55
|
-
* shell=<bash|batch|powershell|sh|zsh> — optional, defaults to the shell
|
|
56
|
-
* `fnd workspace` was called from.
|
|
57
|
-
* profile=<true|1|false|0> — optional, default true: the shell loads its
|
|
58
|
-
* startup files (rc/profile), so tools like nvm are available.
|
|
59
|
-
*/
|
|
60
|
-
export declare const parseRpcFlag: (raw: string) => RpcConfig;
|
|
61
|
-
/** True if the chosen shell is runnable on this machine. */
|
|
62
|
-
export declare const hasLocalShell: (shell: RpcShell, profile: boolean) => boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Start the local RPC (MCP) server on 127.0.0.1:<ports.local>. Commands execute
|
|
65
|
-
* with `cwd` as their working directory — the local side of the workspace sync.
|
|
66
|
-
* Resolves once the port is bound; rejects if binding fails (e.g. port in use).
|
|
67
|
-
*/
|
|
68
|
-
export declare const startRpcServer: (config: RpcConfig, cwd: string) => Promise<RpcServer>;
|
|
69
|
-
export {};
|