@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 +12 -12
- package/dist/commands/workspace/index.js +1 -1
- package/dist/lib/workspace.d.ts +12 -11
- package/dist/lib/workspace.js +45 -60
- package/oclif.manifest.json +2 -2
- package/package.json +4 -2
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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',
|
package/dist/lib/workspace.d.ts
CHANGED
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
* │ copy on disk) │ deltas only │ │
|
|
13
13
|
* └─────────────────────┘ └──────────────────────────────┘
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
99
|
-
* the
|
|
100
|
-
*
|
|
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). */
|
package/dist/lib/workspace.js
CHANGED
|
@@ -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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
|
116
|
-
'if command -v
|
|
117
|
-
`
|
|
118
|
-
|
|
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
|
-
?
|
|
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
|
|
155
|
-
* the
|
|
156
|
-
*
|
|
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) =>
|
|
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' });
|
package/oclif.manifest.json
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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"
|