@firenet-designs/fnd-cli 2.3.0 → 2.3.1

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 CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @firenet-designs/fnd-cli
20
20
  $ fnd COMMAND
21
21
  running command...
22
22
  $ fnd (--version)
23
- @firenet-designs/fnd-cli/2.3.0 linux-x64 node-v24.18.0
23
+ @firenet-designs/fnd-cli/2.3.1 linux-x64 node-v24.18.0
24
24
  $ fnd --help [COMMAND]
25
25
  USAGE
26
26
  $ fnd COMMAND
@@ -62,7 +62,7 @@ EXAMPLES
62
62
  $ fnd backfill-project my-store "extra hints"
63
63
  ```
64
64
 
65
- _See code: [src/commands/backfill-project.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/backfill-project.ts)_
65
+ _See code: [src/commands/backfill-project.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/backfill-project.ts)_
66
66
 
67
67
  ## `fnd create-app`
68
68
 
@@ -83,7 +83,7 @@ EXAMPLES
83
83
  $ fnd create-app
84
84
  ```
85
85
 
86
- _See code: [src/commands/create-app.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/create-app.ts)_
86
+ _See code: [src/commands/create-app.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/create-app.ts)_
87
87
 
88
88
  ## `fnd create-project [SHOP]`
89
89
 
@@ -113,7 +113,7 @@ EXAMPLES
113
113
  $ fnd create-project my-store "Acme storefront, Klaviyo"
114
114
  ```
115
115
 
116
- _See code: [src/commands/create-project.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/create-project.ts)_
116
+ _See code: [src/commands/create-project.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/create-project.ts)_
117
117
 
118
118
  ## `fnd help [COMMAND]`
119
119
 
@@ -165,7 +165,7 @@ EXAMPLES
165
165
  $ fnd token -s mystore -i <id> -c <secret> --write ./.mcp.json
166
166
  ```
167
167
 
168
- _See code: [src/commands/token.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/token.ts)_
168
+ _See code: [src/commands/token.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/token.ts)_
169
169
 
170
170
  ## `fnd workspace`
171
171
 
@@ -190,11 +190,11 @@ DESCRIPTION
190
190
  Open a remote workspace: two-way sync the current directory to a remote Linux box with Mutagen and drop into a shell
191
191
  there, tearing the sync down on exit.
192
192
 
193
- Both sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native speed
194
- (unlike reverse SSHFS). Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to
195
- auto-resolve them in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and
196
- auto-deploys its agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required.
197
- You need the Mutagen CLI installed on THIS machine.
193
+ Both sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native
194
+ speed. Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to auto-resolve them
195
+ in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and auto-deploys its
196
+ agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required. You need the
197
+ Mutagen CLI installed on THIS machine.
198
198
 
199
199
  EXAMPLES
200
200
  $ fnd workspace --ssh user@203.0.113.4
@@ -208,7 +208,7 @@ EXAMPLES
208
208
  $ fnd workspace --ssh user@host --devtools 9333:9222
209
209
  ```
210
210
 
211
- _See code: [src/commands/workspace/index.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/workspace/index.ts)_
211
+ _See code: [src/commands/workspace/index.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/workspace/index.ts)_
212
212
 
213
213
  ## `fnd workspace cleanup`
214
214
 
@@ -236,5 +236,5 @@ EXAMPLES
236
236
  $ fnd workspace cleanup --ssh user@host --remote-dir /home/fnd/cole/fnd-cli
237
237
  ```
238
238
 
239
- _See code: [src/commands/workspace/cleanup.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.0/src/commands/workspace/cleanup.ts)_
239
+ _See code: [src/commands/workspace/cleanup.ts](https://github.com/FireNet-Designs/fnd-cli/blob/v2.3.1/src/commands/workspace/cleanup.ts)_
240
240
  <!-- commandsstop -->
@@ -3,7 +3,7 @@ import chalk from 'chalk';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { browserDebugInstructions, buildContext, buildMutagenCreateArgs, buildMutagenFlushArgs, buildMutagenTerminateArgs, buildRemoteScript, DEFAULT_MOUNT_BASE, hasMutagen, hasSshClient, isLocalDebugPortLive, mutagenInstallInstructions, parseDevtoolsPort, parseSshTarget, runMutagen, runRemoteCleanup, } from '../../lib/workspace.js';
5
5
  export default class Workspace extends Command {
6
- static description = 'Open a remote workspace: two-way sync the current directory to a remote Linux box with Mutagen and drop into a shell there, tearing the sync down on exit.\n\nBoth sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native speed (unlike reverse SSHFS). Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to auto-resolve them in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and auto-deploys its agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required. You need the Mutagen CLI installed on THIS machine.';
6
+ static description = 'Open a remote workspace: two-way sync the current directory to a remote Linux box with Mutagen and drop into a shell there, tearing the sync down on exit.\n\nBoth sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native speed. Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to auto-resolve them in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and auto-deploys its agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required. You need the Mutagen CLI installed on THIS machine.';
7
7
  static examples = [
8
8
  '<%= config.bin %> <%= command.id %> --ssh user@203.0.113.4',
9
9
  '<%= config.bin %> <%= command.id %> --ssh user@host --source local',
@@ -12,15 +12,15 @@
12
12
  * │ copy on disk) │ deltas only │ │
13
13
  * └─────────────────────┘ └──────────────────────────────┘
14
14
  *
15
- * Unlike the old reverse-SSHFS approach, BOTH sides hold a real copy on local
16
- * disk and Mutagen propagates only the deltas. The remote therefore reads files
17
- * at native disk speed (no per-file network round-trips), which is the whole
18
- * point — lint/build/test on the remote stop being IO-bound on the tunnel.
15
+ * BOTH sides hold a real copy on local disk and Mutagen propagates only the
16
+ * deltas. The remote therefore reads files at native disk speed (no per-file
17
+ * network round-trips), so lint/build/test on the remote are not IO-bound on
18
+ * the tunnel.
19
19
  *
20
- * Mutagen connects the normal direction (local → remote over SSH), so unlike
21
- * SSHFS — we need NO local SSH server, NO reverse tunnel, and NO authorized_keys
22
- * trust dance. Mutagen auto-deploys its agent to the remote over that same SSH
23
- * connection; nothing to install on the remote by hand.
20
+ * Mutagen connects the normal direction (local → remote over SSH), so we need
21
+ * NO local SSH server, NO reverse tunnel, and NO authorized_keys trust dance.
22
+ * Mutagen auto-deploys its agent to the remote over that same SSH connection;
23
+ * nothing to install on the remote by hand.
24
24
  *
25
25
  * The one reverse tunnel that remains is optional and unrelated to files: with
26
26
  * --devtools we open `ssh -R` so the remote's chrome-devtools MCP can reach the
@@ -95,9 +95,10 @@ export declare const buildRemoteScript: (ctx: WorkspaceContext) => string;
95
95
  /** Run the remote-side teardown (strip this project's chrome-devtools MCP) over a fresh ssh connection. */
96
96
  export declare const runRemoteCleanup: (target: string, remoteDir: string) => Promise<number>;
97
97
  /**
98
- * The remote-side teardown script: strip this project's chrome-devtools MCP from
99
- * the remote's ~/.claude.json. A no-op if there is nothing to undo. The synced
100
- * files themselves are left in place they are a real copy, not a mount.
98
+ * The remote-side teardown script: strip this project's chrome-devtools MCP via
99
+ * the `claude` CLI. Runs from inside the workspace dir so local scope resolves to
100
+ * the right project. A no-op if there is nothing to undo. The synced files
101
+ * themselves are left in place — they are a real copy, not a mount.
101
102
  */
102
103
  export declare const buildCleanupScript: (remoteDir: string) => string;
103
104
  /** True if an `ssh` client is on PATH (works on Windows, macOS, Linux). */
@@ -64,64 +64,43 @@ export const buildContext = (opts) => {
64
64
  };
65
65
  /** POSIX single-quote a string so it can be embedded safely in the remote shell script. */
66
66
  export const shQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
67
+ /** MCP server name registered for the workspace's chrome-devtools tunnel. */
68
+ const DEVTOOLS_MCP_NAME = 'chrome-devtools';
67
69
  /**
68
- * Node program (run on the REMOTE) that adds the chrome-devtools MCP to the
69
- * remote's ~/.claude.json under this project (the remote dir). Project-scoped,
70
- * never the top-level mcpServers, so it is not globally exposed. Reads the
71
- * remote dir and remote tunnel port from the environment.
70
+ * Bash lines (run on the REMOTE, from inside the workspace dir) that register the
71
+ * chrome-devtools MCP with the `claude` CLI. Local scope keys off the current
72
+ * directory, so this only affects the workspace project never a global server.
73
+ * A prior entry is cleared first so a re-connect after a crashed session is
74
+ * idempotent. Skips gracefully if the claude CLI is missing.
75
+ *
76
+ * The block runs inside a login shell (`$SHELL -lc`): the outer script arrives as
77
+ * a non-login ssh command whose PATH lacks the node/nvm/volta/Homebrew dirs that
78
+ * login profiles add, so a bare `command -v claude` misses an installed CLI. A
79
+ * login shell reproduces the same PATH the interactive session below gets.
72
80
  */
73
- const claudeDevtoolsAddProgram = () => [
74
- "const fs = require('node:fs');",
75
- "const os = require('node:os');",
76
- "const path = require('node:path');",
77
- "const file = path.join(os.homedir(), '.claude.json');",
78
- 'let data = {};',
79
- "try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { data = {}; }",
80
- 'const mount = process.env.FND_MOUNT;',
81
- 'const port = process.env.FND_DEVTOOLS_PORT;',
82
- 'if (!data.projects) data.projects = {};',
83
- 'if (!data.projects[mount]) data.projects[mount] = {};',
84
- 'const project = data.projects[mount];',
85
- 'if (!project.mcpServers) project.mcpServers = {};',
86
- "project.mcpServers['chrome-devtools'] = { command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest', '--browserUrl', 'http://127.0.0.1:' + port] };",
87
- String.raw `fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');`,
88
- ];
89
- /**
90
- * Node program (run on the REMOTE) that removes the chrome-devtools MCP this
91
- * workspace added for its project from ~/.claude.json, dropping an emptied
92
- * mcpServers map. No-op if the file or entry is absent.
93
- */
94
- const claudeDevtoolsRemoveProgram = () => [
95
- "const fs = require('node:fs');",
96
- "const os = require('node:os');",
97
- "const path = require('node:path');",
98
- "const file = path.join(os.homedir(), '.claude.json');",
99
- 'let data;',
100
- "try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { process.exit(0); }",
101
- 'const mount = process.env.FND_MOUNT;',
102
- 'const project = data.projects && data.projects[mount];',
103
- "if (project && project.mcpServers && project.mcpServers['chrome-devtools']) {",
104
- " delete project.mcpServers['chrome-devtools'];",
105
- ' if (Object.keys(project.mcpServers).length === 0) delete project.mcpServers;',
106
- String.raw ` fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');`,
107
- " console.log('Removed chrome-devtools MCP config for ' + mount + '.');",
108
- '}',
109
- ];
81
+ const claudeDevtoolsAddScript = (remotePort, okMessage) => {
82
+ const body = [
83
+ 'if command -v claude >/dev/null 2>&1; then',
84
+ ` claude mcp remove ${DEVTOOLS_MCP_NAME} >/dev/null 2>&1 || true`,
85
+ ` claude mcp add ${DEVTOOLS_MCP_NAME} -- npx -y chrome-devtools-mcp@latest --browserUrl http://127.0.0.1:${remotePort}`,
86
+ ` echo ${shQuote(okMessage)}`,
87
+ 'else',
88
+ ' echo "WARNING: claude CLI not found on the remote; skipped chrome-devtools MCP config." >&2',
89
+ 'fi',
90
+ ].join('\n');
91
+ return [`"\${SHELL:-bash}" -lc ${shQuote(body)}`];
92
+ };
110
93
  /**
111
- * Wrap a node program in a bash snippet that runs it on the remote with FND_MOUNT
112
- * (and optionally FND_DEVTOOLS_PORT) in the environment, skipping gracefully if
113
- * node is missing.
94
+ * Bash lines (run on the REMOTE, from inside the workspace dir) that remove the
95
+ * chrome-devtools MCP this workspace registered. Local scope keys off the current
96
+ * directory, so it targets only the workspace project. No-op if the entry or the
97
+ * claude CLI is absent. Runs under a login shell for the same PATH reason as
98
+ * `claudeDevtoolsAddScript`.
114
99
  */
115
- const runNodeOnRemote = (program, env, okMessage) => [
116
- 'if command -v node >/dev/null 2>&1; then',
117
- ` ${env} node <<'FND_NODE_EOF'`,
118
- ...program,
119
- 'FND_NODE_EOF',
120
- ...(okMessage ? [` echo ${shQuote(okMessage)}`] : []),
121
- 'else',
122
- ' echo "WARNING: node not found on the remote; skipped chrome-devtools MCP config." >&2',
123
- 'fi',
124
- ];
100
+ const claudeDevtoolsRemoveScript = () => {
101
+ const body = ['if command -v claude >/dev/null 2>&1; then', ` claude mcp remove ${DEVTOOLS_MCP_NAME}`, 'fi'].join('\n');
102
+ return [`"\${SHELL:-bash}" -lc ${shQuote(body)}`];
103
+ };
125
104
  /**
126
105
  * The bash script the remote runs for the interactive session. Mutagen already
127
106
  * keeps the directory in sync, so this just makes sure the directory exists,
@@ -130,15 +109,16 @@ const runNodeOnRemote = (program, env, okMessage) => [
130
109
  export const buildRemoteScript = (ctx) => {
131
110
  const dir = shQuote(ctx.remoteDir);
132
111
  const devtoolsSetup = ctx.devtools
133
- ? runNodeOnRemote(claudeDevtoolsAddProgram(), `FND_MOUNT=${dir} FND_DEVTOOLS_PORT=${ctx.devtools.remote}`, `Configured chrome-devtools MCP for this workspace (browser via 127.0.0.1:${ctx.devtools.remote}).`)
112
+ ? claudeDevtoolsAddScript(ctx.devtools.remote, `Configured chrome-devtools MCP for this workspace (browser via 127.0.0.1:${ctx.devtools.remote}).`)
134
113
  : [];
135
114
  return [
136
115
  'set -u',
137
116
  `DIR=${dir}`,
138
117
  // Mutagen creates the sync root, but ensure it exists so `cd` never races it.
139
118
  'mkdir -p "$DIR" || { echo "ERROR: could not create $DIR" >&2; exit 1; }',
140
- ...devtoolsSetup,
141
119
  'cd "$DIR" || { echo "ERROR: could not enter $DIR" >&2; exit 1; }',
120
+ // Register the MCP from inside $DIR: `claude mcp add` local scope keys off cwd.
121
+ ...devtoolsSetup,
142
122
  'echo "Workspace ready at $DIR — files sync in the background (exit to stop syncing)."',
143
123
  // eslint-disable-next-line no-template-curly-in-string -- shell parameter expansion, not a JS template
144
124
  '"${SHELL:-bash}" -l',
@@ -151,11 +131,16 @@ export const runRemoteCleanup = (target, remoteDir) => new Promise((resolve, rej
151
131
  child.once('close', (code) => resolve(code ?? 0));
152
132
  });
153
133
  /**
154
- * The remote-side teardown script: strip this project's chrome-devtools MCP from
155
- * the remote's ~/.claude.json. A no-op if there is nothing to undo. The synced
156
- * files themselves are left in place they are a real copy, not a mount.
134
+ * The remote-side teardown script: strip this project's chrome-devtools MCP via
135
+ * the `claude` CLI. Runs from inside the workspace dir so local scope resolves to
136
+ * the right project. A no-op if there is nothing to undo. The synced files
137
+ * themselves are left in place — they are a real copy, not a mount.
157
138
  */
158
- export const buildCleanupScript = (remoteDir) => runNodeOnRemote(claudeDevtoolsRemoveProgram(), `FND_MOUNT=${shQuote(remoteDir)}`).join('\n');
139
+ export const buildCleanupScript = (remoteDir) => [
140
+ `DIR=${shQuote(remoteDir)}`,
141
+ 'cd "$DIR" 2>/dev/null || { echo "Nothing to clean up: $DIR is gone." >&2; exit 0; }',
142
+ ...claudeDevtoolsRemoveScript(),
143
+ ].join('\n');
159
144
  /** True if an `ssh` client is on PATH (works on Windows, macOS, Linux). */
160
145
  export const hasSshClient = () => {
161
146
  const result = spawnSync('ssh', ['-V'], { stdio: 'ignore' });
@@ -235,7 +235,7 @@
235
235
  "workspace": {
236
236
  "aliases": [],
237
237
  "args": {},
238
- "description": "Open a remote workspace: two-way sync the current directory to a remote Linux box with Mutagen and drop into a shell there, tearing the sync down on exit.\n\nBoth sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native speed (unlike reverse SSHFS). Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to auto-resolve them in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and auto-deploys its agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required. You need the Mutagen CLI installed on THIS machine.",
238
+ "description": "Open a remote workspace: two-way sync the current directory to a remote Linux box with Mutagen and drop into a shell there, tearing the sync down on exit.\n\nBoth sides keep a real copy on local disk and only deltas cross the network, so the remote reads files at native speed. Conflicting edits on both ends are flagged rather than silently overwritten; pass --source to auto-resolve them in favour of one side. Mutagen connects the normal direction (this machine → remote over SSH) and auto-deploys its agent to the remote, so no local SSH server, reverse tunnel, or authorized_keys trust is required. You need the Mutagen CLI installed on THIS machine.",
239
239
  "examples": [
240
240
  "<%= config.bin %> <%= command.id %> --ssh user@203.0.113.4",
241
241
  "<%= config.bin %> <%= command.id %> --ssh user@host --source local",
@@ -296,5 +296,5 @@
296
296
  ]
297
297
  }
298
298
  },
299
- "version": "2.3.0"
299
+ "version": "2.3.1"
300
300
  }
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.0",
4
+ "version": "2.3.1",
5
5
  "author": "Cole Denslow",
6
6
  "contributors": [
7
7
  "Justin Schellenberg"
@@ -76,7 +76,7 @@
76
76
  "description": "Say hello to the world and others"
77
77
  },
78
78
  "workspace": {
79
- "description": "Open a remote reverse-SSHFS workspace and run Claude on it"
79
+ "description": "Open a remote Mutagen workspace and run Claude on it"
80
80
  }
81
81
  }
82
82
  },
@@ -87,10 +87,12 @@
87
87
  "scripts": {
88
88
  "build": "shx rm -rf dist && tsc -b",
89
89
  "lint": "eslint",
90
+ "pack": "npm run prepack && npm run postpack",
90
91
  "postpack": "shx rm -f oclif.manifest.json",
91
92
  "posttest": "npm run lint",
92
93
  "prepack": "oclif manifest && oclif readme",
93
94
  "version": "oclif readme && git add README.md",
95
+ "prerelease": "npm run build && npm run prepack",
94
96
  "release": "npm run build && npm run prepack && npm publish --access public"
95
97
  },
96
98
  "types": "dist/index.d.ts"