@firenet-designs/fnd-cli 2.3.3 → 2.6.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.
@@ -1,5 +1,150 @@
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\nWalks the Webflow site asset library and (with --cms) the image fields of CMS collection items, describes every image that needs alt text, and PATCHes the description back. Images are fetched and described one at a time — a single local model gains nothing from concurrency, and Webflow rate-limits. Nothing leaves your network except the Webflow API calls.\n\nCMS 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 %> --webflow --api-key <key> --site-id <id> --ollama-host http://localhost:11434 --ollama-model qwen3-vl:8b",
11
+ "<%= config.bin %> <%= command.id %> --webflow --skip --limit 20",
12
+ "<%= config.bin %> <%= command.id %> --webflow --cms --select",
13
+ "<%= config.bin %> <%= command.id %> --webflow --cms --only products --only sku",
14
+ "<%= config.bin %> <%= command.id %> --webflow --output-stats ./alt-text-run.md",
15
+ "<%= config.bin %> <%= command.id %> --webflow --filter \"fileSize>=sizes.KB(100) && width>=100 && height>=100\"",
16
+ "<%= config.bin %> <%= command.id %> --webflow --filter \"type !== 'svg' && !url.includes('/icons/')\""
17
+ ],
18
+ "flags": {
19
+ "api-key": {
20
+ "dependsOn": [
21
+ "webflow"
22
+ ],
23
+ "description": "Webflow API token (site-scoped). Prompted for if omitted.",
24
+ "name": "api-key",
25
+ "hasDynamicHelp": false,
26
+ "multiple": false,
27
+ "type": "option"
28
+ },
29
+ "cms": {
30
+ "dependsOn": [
31
+ "webflow"
32
+ ],
33
+ "description": "also add alt text to the images in CMS collection items. Off by default — only the site asset library is walked.",
34
+ "name": "cms",
35
+ "allowNo": false,
36
+ "type": "boolean"
37
+ },
38
+ "filter": {
39
+ "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.",
40
+ "name": "filter",
41
+ "hasDynamicHelp": false,
42
+ "multiple": false,
43
+ "type": "option"
44
+ },
45
+ "limit": {
46
+ "description": "stop after this many images have been transcribed (counted across assets and CMS together)",
47
+ "name": "limit",
48
+ "hasDynamicHelp": false,
49
+ "multiple": false,
50
+ "type": "option"
51
+ },
52
+ "ollama-host": {
53
+ "description": "base URL of the Ollama server. Prompted for if omitted.",
54
+ "name": "ollama-host",
55
+ "hasDynamicHelp": false,
56
+ "multiple": false,
57
+ "type": "option"
58
+ },
59
+ "ollama-model": {
60
+ "description": "the model used to describe the images. Omit it to pick from the vision-capable models pulled on the host.",
61
+ "name": "ollama-model",
62
+ "hasDynamicHelp": false,
63
+ "multiple": false,
64
+ "type": "option"
65
+ },
66
+ "only": {
67
+ "dependsOn": [
68
+ "cms"
69
+ ],
70
+ "description": "only touch these CMS collections, matched case-insensitively against a collection's name or slug. Repeat the flag for more than one.",
71
+ "exclusive": [
72
+ "select"
73
+ ],
74
+ "name": "only",
75
+ "hasDynamicHelp": false,
76
+ "multiple": true,
77
+ "type": "option"
78
+ },
79
+ "output-stats": {
80
+ "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.",
81
+ "name": "output-stats",
82
+ "hasDynamicHelp": false,
83
+ "multiple": false,
84
+ "type": "option"
85
+ },
86
+ "select": {
87
+ "dependsOn": [
88
+ "cms"
89
+ ],
90
+ "description": "fetch the CMS collections and pick which ones to process interactively",
91
+ "exclusive": [
92
+ "only"
93
+ ],
94
+ "name": "select",
95
+ "allowNo": false,
96
+ "type": "boolean"
97
+ },
98
+ "shopify": {
99
+ "description": "run against a Shopify store (not implemented yet)",
100
+ "exclusive": [
101
+ "webflow"
102
+ ],
103
+ "name": "shopify",
104
+ "allowNo": false,
105
+ "type": "boolean"
106
+ },
107
+ "site-id": {
108
+ "dependsOn": [
109
+ "webflow"
110
+ ],
111
+ "description": "Webflow site ID. Prompted for if omitted.",
112
+ "name": "site-id",
113
+ "hasDynamicHelp": false,
114
+ "multiple": false,
115
+ "type": "option"
116
+ },
117
+ "skip": {
118
+ "description": "leave images that already have alt text alone instead of overwriting them",
119
+ "name": "skip",
120
+ "allowNo": false,
121
+ "type": "boolean"
122
+ },
123
+ "webflow": {
124
+ "description": "run against a Webflow site",
125
+ "exclusive": [
126
+ "shopify"
127
+ ],
128
+ "name": "webflow",
129
+ "allowNo": false,
130
+ "type": "boolean"
131
+ }
132
+ },
133
+ "hasDynamicHelp": false,
134
+ "hiddenAliases": [],
135
+ "id": "alt-text",
136
+ "pluginAlias": "@firenet-designs/fnd-cli",
137
+ "pluginName": "@firenet-designs/fnd-cli",
138
+ "pluginType": "core",
139
+ "strict": true,
140
+ "enableJsonFlag": false,
141
+ "isESM": true,
142
+ "relativePath": [
143
+ "dist",
144
+ "commands",
145
+ "alt-text.js"
146
+ ]
147
+ },
3
148
  "backfill-project": {
4
149
  "aliases": [],
5
150
  "args": {
@@ -80,7 +225,7 @@
80
225
  "required": false
81
226
  }
82
227
  },
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.",
228
+ "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
229
  "examples": [
85
230
  "<%= config.bin %> <%= command.id %>",
86
231
  "<%= config.bin %> <%= command.id %> my-store",
@@ -183,69 +328,6 @@
183
328
  "token.js"
184
329
  ]
185
330
  },
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 to also strip the matching chrome-devtools MCP entry from the remote (only do this if the dropped session used --devtools). 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 --delete-remote-dir"
195
- ],
196
- "flags": {
197
- "delete-remote-dir": {
198
- "description": "also delete the remote workspace directory (by default the synced files are left in place)",
199
- "name": "delete-remote-dir",
200
- "allowNo": false,
201
- "type": "boolean"
202
- },
203
- "devtools": {
204
- "description": "also strip the chrome-devtools MCP entry from the remote (only if the dropped session used --devtools)",
205
- "name": "devtools",
206
- "allowNo": false,
207
- "type": "boolean"
208
- },
209
- "remote-base": {
210
- "description": "base dir on the remote, used to derive the default remote directory path",
211
- "name": "remote-base",
212
- "default": "/home/fnd",
213
- "hasDynamicHelp": false,
214
- "multiple": false,
215
- "type": "option"
216
- },
217
- "remote-dir": {
218
- "description": "exact remote directory to target (defaults to the current dir mapping)",
219
- "name": "remote-dir",
220
- "hasDynamicHelp": false,
221
- "multiple": false,
222
- "type": "option"
223
- },
224
- "ssh": {
225
- "description": "remote to connect to, as user@host",
226
- "name": "ssh",
227
- "required": true,
228
- "hasDynamicHelp": false,
229
- "multiple": false,
230
- "type": "option"
231
- }
232
- },
233
- "hasDynamicHelp": false,
234
- "hiddenAliases": [],
235
- "id": "workspace:cleanup",
236
- "pluginAlias": "@firenet-designs/fnd-cli",
237
- "pluginName": "@firenet-designs/fnd-cli",
238
- "pluginType": "core",
239
- "strict": true,
240
- "enableJsonFlag": false,
241
- "isESM": true,
242
- "relativePath": [
243
- "dist",
244
- "commands",
245
- "workspace",
246
- "cleanup.js"
247
- ]
248
- },
249
331
  "workspace": {
250
332
  "aliases": [],
251
333
  "args": {},
@@ -254,11 +336,21 @@
254
336
  "<%= config.bin %> <%= command.id %> --ssh user@203.0.113.4",
255
337
  "<%= config.bin %> <%= command.id %> --ssh user@host --source local",
256
338
  "<%= config.bin %> <%= command.id %> --ssh user@host --remote-base /home/fnd",
339
+ "<%= config.bin %> <%= command.id %> --ssh user@host --ignore-vcs",
257
340
  "<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9222",
258
341
  "<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9333:9222",
259
- "<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir"
342
+ "<%= config.bin %> <%= command.id %> --ssh user@host --rpc port=7777:7700",
343
+ "<%= config.bin %> <%= command.id %> --ssh user@host --rpc port=7777:7700,profile=false,shell=zsh",
344
+ "<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir",
345
+ "<%= config.bin %> <%= command.id %> --ssh user@host --rpc 7700 --cleanup"
260
346
  ],
261
347
  "flags": {
348
+ "cleanup": {
349
+ "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/--rpc 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.",
350
+ "name": "cleanup",
351
+ "allowNo": false,
352
+ "type": "boolean"
353
+ },
262
354
  "delete-remote-dir": {
263
355
  "description": "on exit, delete the remote workspace directory instead of leaving the synced copy in place",
264
356
  "name": "delete-remote-dir",
@@ -272,6 +364,12 @@
272
364
  "multiple": false,
273
365
  "type": "option"
274
366
  },
367
+ "ignore-vcs": {
368
+ "description": "don't sync paths matched by the project's .gitignore files (node_modules, build output, …) so each side keeps its own platform-specific artifacts. Every .gitignore in the tree is honoured relative to its directory, like git does. The .git directory itself still syncs, so the remote stays a working repo.",
369
+ "name": "ignore-vcs",
370
+ "allowNo": false,
371
+ "type": "boolean"
372
+ },
275
373
  "remote-base": {
276
374
  "description": "base dir on the remote; the workspace lands at <base>/<local-user>/<dir-name>",
277
375
  "name": "remote-base",
@@ -280,6 +378,13 @@
280
378
  "multiple": false,
281
379
  "type": "option"
282
380
  },
381
+ "rpc": {
382
+ "description": "expose a run_local_command MCP tool to Claude on the remote that executes commands back on THIS machine (the one running fnd workspace). Value is port=<port|remote:local>[,profile=<true|1|false|0>][,shell=<bash|batch|powershell|sh|zsh>] — port opens a reverse tunnel (ssh -R <remote>:localhost:<local>) to a command server started here; shell defaults to the shell fnd workspace was called from; profile (default true) controls whether the shell loads its startup files — with it on, POSIX shells run interactively (-i) so rc files like ~/.bashrc or ~/.zshrc are sourced and tools such as nvm work.",
383
+ "name": "rpc",
384
+ "hasDynamicHelp": false,
385
+ "multiple": false,
386
+ "type": "option"
387
+ },
283
388
  "source": {
284
389
  "description": "which side wins on conflict: \"remote\" = this server (where the workspace shell runs), \"local\" = the machine you ran fnd workspace from. Omit to flag conflicts instead of auto-resolving them.",
285
390
  "name": "source",
@@ -317,5 +422,5 @@
317
422
  ]
318
423
  }
319
424
  },
320
- "version": "2.3.3"
425
+ "version": "2.6.0"
321
426
  }
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.3.3",
4
+ "version": "2.6.0",
5
5
  "author": "Cole Denslow",
6
6
  "contributors": [
7
7
  "Justin Schellenberg"
@@ -11,12 +11,17 @@
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
+ "@resvg/resvg-js": "^2.6.2",
17
19
  "chalk": "^5.6.2",
20
+ "ollama": "^0.6.3",
18
21
  "ora": "^9.3.0",
19
- "simple-git": "^3.32.2"
22
+ "sharp": "^0.34.5",
23
+ "simple-git": "^3.32.2",
24
+ "zod": "^4.4.3"
20
25
  },
21
26
  "devDependencies": {
22
27
  "@eslint/compat": "^1",
@@ -91,7 +96,7 @@
91
96
  "postpack": "shx rm -f oclif.manifest.json",
92
97
  "posttest": "npm run lint",
93
98
  "prepack": "oclif manifest && oclif readme",
94
- "version": "oclif readme && git add README.md",
99
+ "version": "oclif readme",
95
100
  "prerelease": "npm run build && npm run prepack",
96
101
  "release": "npm run build && npm run prepack && npm publish"
97
102
  },
@@ -1,13 +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
- ssh: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- };
12
- run(): Promise<void>;
13
- }
@@ -1,75 +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 to also strip the matching chrome-devtools MCP entry from the remote (only do this if the dropped session used --devtools). 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 --delete-remote-dir',
12
- ];
13
- static flags = {
14
- 'delete-remote-dir': Flags.boolean({
15
- default: false,
16
- description: 'also delete the remote workspace directory (by default the synced files are left in place)',
17
- }),
18
- devtools: Flags.boolean({
19
- default: false,
20
- description: 'also strip the chrome-devtools MCP entry from the remote (only if the dropped session used --devtools)',
21
- }),
22
- 'remote-base': Flags.string({
23
- default: DEFAULT_MOUNT_BASE,
24
- description: 'base dir on the remote, used to derive the default remote directory path',
25
- }),
26
- 'remote-dir': Flags.string({
27
- description: 'exact remote directory to target (defaults to the current dir mapping)',
28
- }),
29
- ssh: Flags.string({
30
- description: 'remote to connect to, as user@host',
31
- required: true,
32
- }),
33
- };
34
- async run() {
35
- const { flags } = await this.parse(WorkspaceCleanup);
36
- if (!hasSshClient()) {
37
- this.error('No `ssh` client found on PATH. Install OpenSSH client and try again.', { code: '1' });
38
- }
39
- const target = parseSshTarget(flags.ssh);
40
- const target2 = `${target.user}@${target.host}`;
41
- const remoteDir = flags['remote-dir'] ??
42
- buildContext({ cwd: process.cwd(), remoteBase: flags['remote-base'] }).remoteDir;
43
- const slug = slugify(basename(remoteDir));
44
- this.log(chalk.bold('Cleaning up workspace'));
45
- this.log(` ${chalk.dim('remote:')} ${target2}`);
46
- this.log(` ${chalk.dim('remote dir:')} ${remoteDir}`);
47
- this.log('');
48
- // Terminate any lingering sync sessions for this directory (a local Mutagen op).
49
- if (hasMutagen()) {
50
- this.log(chalk.dim('Terminating any leftover Mutagen sync sessions…'));
51
- await runMutagen(buildMutagenTerminateSelectorArgs(slug)).catch(() => 1);
52
- }
53
- else {
54
- this.log(chalk.yellow('Mutagen CLI not found on PATH — skipping sync termination.'));
55
- }
56
- // Strip any chrome-devtools MCP config this workspace left on the remote
57
- // (only when asked), and optionally delete the synced directory itself.
58
- if (flags.devtools) {
59
- this.log(chalk.dim('Removing any leftover chrome-devtools MCP config on the remote…'));
60
- }
61
- if (flags['delete-remote-dir']) {
62
- this.log(chalk.dim('Deleting the remote workspace directory…'));
63
- }
64
- const code = await runRemoteCleanup(target2, remoteDir, {
65
- deleteRemoteDir: flags['delete-remote-dir'],
66
- removeDevtoolsMcp: flags.devtools,
67
- });
68
- if (code === 0) {
69
- this.log(chalk.green('✓ Done.'));
70
- }
71
- else {
72
- this.error(`Remote cleanup ssh session exited with code ${code}.`, { code: '1' });
73
- }
74
- }
75
- }