@firenet-designs/fnd-cli 2.3.2 → 2.4.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 +64 -21
- package/dist/commands/workspace/cleanup.d.ts +3 -0
- package/dist/commands/workspace/cleanup.js +32 -4
- package/dist/commands/workspace/index.d.ts +3 -0
- package/dist/commands/workspace/index.js +80 -18
- package/dist/hooks/init/check-for-updates.js +1 -1
- package/dist/lib/kv-flag.d.ts +15 -0
- package/dist/lib/kv-flag.js +75 -0
- package/dist/lib/rpc.d.ts +69 -0
- package/dist/lib/rpc.js +313 -0
- package/dist/lib/workspace.d.ts +66 -14
- package/dist/lib/workspace.js +154 -21
- package/oclif.manifest.json +48 -4
- package/package.json +4 -3
package/dist/lib/workspace.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { randomInt } from 'node:crypto';
|
|
3
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { userInfo } from 'node:os';
|
|
4
|
-
import { basename } from 'node:path';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
5
6
|
export const DEFAULT_MOUNT_BASE = '/home/fnd';
|
|
6
7
|
/** Parse a `user@host` string, throwing a friendly error otherwise. */
|
|
7
8
|
export const parseSshTarget = (raw) => {
|
|
@@ -23,15 +24,15 @@ export const slugify = (value) => value
|
|
|
23
24
|
/** A unique Mutagen session name for a workspace on the given local directory. */
|
|
24
25
|
export const buildSyncName = (dirName) => `fnd-ws-${slugify(dirName)}-${randomInt(0, 1_000_000).toString(36)}`;
|
|
25
26
|
/**
|
|
26
|
-
* Parse
|
|
27
|
-
* `remote:local`, where `local` is the caller's machine
|
|
28
|
-
*
|
|
27
|
+
* Parse a reverse-tunnel port value. Accepts `port` (same port on both ends) or
|
|
28
|
+
* `remote:local`, where `local` is the caller's machine and `remote` is the
|
|
29
|
+
* port opened on the workspace host. `flag` names the flag in error messages.
|
|
29
30
|
*/
|
|
30
|
-
export const
|
|
31
|
+
export const parsePortPair = (raw, flag) => {
|
|
31
32
|
const toPort = (value) => {
|
|
32
33
|
const n = Number(value);
|
|
33
34
|
if (!Number.isInteger(n) || n < 1 || n > 65_535) {
|
|
34
|
-
throw new Error(
|
|
35
|
+
throw new Error(`${flag} port must be an integer 1-65535 (got "${value}")`);
|
|
35
36
|
}
|
|
36
37
|
return n;
|
|
37
38
|
};
|
|
@@ -43,7 +44,7 @@ export const parseDevtoolsPort = (raw) => {
|
|
|
43
44
|
if (parts.length === 2) {
|
|
44
45
|
return { local: toPort(parts[1]), remote: toPort(parts[0]) };
|
|
45
46
|
}
|
|
46
|
-
throw new Error(
|
|
47
|
+
throw new Error(`${flag} port must be "port" or "remote:local" (got "${raw}")`);
|
|
47
48
|
};
|
|
48
49
|
/** Build the immutable facts for a workspace session from the local environment + flags. */
|
|
49
50
|
export const buildContext = (opts) => {
|
|
@@ -54,18 +55,108 @@ export const buildContext = (opts) => {
|
|
|
54
55
|
const remoteDir = `${base}/${localUser}/${localDirName}`;
|
|
55
56
|
return {
|
|
56
57
|
devtools: opts.devtools,
|
|
58
|
+
ignores: opts.ignoreVcs ? collectVcsIgnores(localCwd) : undefined,
|
|
57
59
|
localCwd,
|
|
58
60
|
localDirName,
|
|
59
61
|
localUser,
|
|
60
62
|
remoteDir,
|
|
63
|
+
rpc: opts.rpc,
|
|
61
64
|
source: opts.source,
|
|
62
65
|
syncName: buildSyncName(localDirName),
|
|
63
66
|
};
|
|
64
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Translate one .gitignore line into Mutagen ignore patterns. `base` is the
|
|
70
|
+
* directory holding the .gitignore, as a posix path relative to the sync root
|
|
71
|
+
* ('' for the root file). Mutagen's syntax already matches gitignore's for the
|
|
72
|
+
* pieces that pass through untouched (`*`, `**`, `?`, `[...]`, `!` negation,
|
|
73
|
+
* trailing `/` for directory-only) — what needs translating is scope:
|
|
74
|
+
*
|
|
75
|
+
* - A pattern with a slash is anchored to the .gitignore's own directory, so it
|
|
76
|
+
* becomes an absolute pattern under `base` (`/dist` in src/.gitignore →
|
|
77
|
+
* `/src/dist`).
|
|
78
|
+
* - A slashless pattern matches at any depth AT OR BELOW `base`. At the root
|
|
79
|
+
* that is exactly Mutagen's unanchored behavior, so it passes through as-is;
|
|
80
|
+
* under a subdirectory it becomes `/base/p` plus `/base/**\/p` (both forms, so
|
|
81
|
+
* the match doesn't depend on `**` matching zero segments).
|
|
82
|
+
*
|
|
83
|
+
* Returns [] for blanks and comments.
|
|
84
|
+
*/
|
|
85
|
+
export const translateGitignoreLine = (line, base) => {
|
|
86
|
+
// Trailing whitespace is meaningless unless backslash-escaped.
|
|
87
|
+
let p = line.replace(/(?<![\\])[ \t]+$/, '');
|
|
88
|
+
if (p === '' || p.startsWith('#'))
|
|
89
|
+
return [];
|
|
90
|
+
let negated = false;
|
|
91
|
+
if (p.startsWith('!')) {
|
|
92
|
+
negated = true;
|
|
93
|
+
p = p.slice(1);
|
|
94
|
+
}
|
|
95
|
+
else if (p.startsWith(String.raw `\!`) || p.startsWith(String.raw `\#`)) {
|
|
96
|
+
p = p.slice(1);
|
|
97
|
+
}
|
|
98
|
+
let dirOnly = false;
|
|
99
|
+
if (p.endsWith('/')) {
|
|
100
|
+
dirOnly = true;
|
|
101
|
+
p = p.replace(/\/+$/, '');
|
|
102
|
+
}
|
|
103
|
+
const anchored = p.includes('/');
|
|
104
|
+
p = p.replace(/^\/+/, '');
|
|
105
|
+
if (p === '')
|
|
106
|
+
return [];
|
|
107
|
+
const withBase = (rel) => `/${[base, rel].filter(Boolean).join('/')}`;
|
|
108
|
+
const targets = anchored ? [withBase(p)] : base === '' ? [p] : [withBase(p), withBase(`**/${p}`)];
|
|
109
|
+
return targets.map((t) => `${negated ? '!' : ''}${t}${dirOnly ? '/' : ''}`);
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Walk the project and turn every .gitignore into Mutagen ignore patterns, each
|
|
113
|
+
* resolved relative to the directory of the .gitignore that declared it. Files
|
|
114
|
+
* are ordered root-first so deeper .gitignore patterns come later — Mutagen
|
|
115
|
+
* gives later patterns precedence, which mirrors git. `.git` and `node_modules`
|
|
116
|
+
* are never descended into (git does not consult .gitignore files inside
|
|
117
|
+
* ignored or metadata directories), and symlinked directories are skipped to
|
|
118
|
+
* avoid cycles.
|
|
119
|
+
*/
|
|
120
|
+
export const collectVcsIgnores = (rootDir) => {
|
|
121
|
+
const found = [];
|
|
122
|
+
const walk = (dir, base) => {
|
|
123
|
+
let entries;
|
|
124
|
+
try {
|
|
125
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return; // unreadable dir — nothing to collect there
|
|
129
|
+
}
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
133
|
+
continue;
|
|
134
|
+
walk(join(dir, entry.name), base === '' ? entry.name : `${base}/${entry.name}`);
|
|
135
|
+
}
|
|
136
|
+
else if (entry.name === '.gitignore') {
|
|
137
|
+
found.push({ base });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
walk(rootDir, '');
|
|
142
|
+
found.sort((a, b) => a.base.split('/').filter(Boolean).length - b.base.split('/').filter(Boolean).length);
|
|
143
|
+
return found.flatMap(({ base }) => {
|
|
144
|
+
let content;
|
|
145
|
+
try {
|
|
146
|
+
content = readFileSync(join(rootDir, ...base.split('/').filter(Boolean), '.gitignore'), 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return content.split('\n').flatMap((line) => translateGitignoreLine(line, base));
|
|
152
|
+
});
|
|
153
|
+
};
|
|
65
154
|
/** POSIX single-quote a string so it can be embedded safely in the remote shell script. */
|
|
66
155
|
export const shQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
67
156
|
/** MCP server name registered for the workspace's chrome-devtools tunnel. */
|
|
68
157
|
const DEVTOOLS_MCP_NAME = 'chrome-devtools';
|
|
158
|
+
/** MCP server name registered for the --rpc local-command tunnel. */
|
|
159
|
+
const RPC_MCP_NAME = 'local-shell';
|
|
69
160
|
/**
|
|
70
161
|
* Bash lines (run on the REMOTE, from inside the workspace dir) that register the
|
|
71
162
|
* chrome-devtools MCP with the `claude` CLI. Local scope keys off the current
|
|
@@ -91,14 +182,37 @@ const claudeDevtoolsAddScript = (remotePort, okMessage) => {
|
|
|
91
182
|
return [`"\${SHELL:-bash}" -lc ${shQuote(body)}`];
|
|
92
183
|
};
|
|
93
184
|
/**
|
|
94
|
-
* Bash lines (run on the REMOTE, from inside the workspace dir) that
|
|
95
|
-
*
|
|
185
|
+
* Bash lines (run on the REMOTE, from inside the workspace dir) that register the
|
|
186
|
+
* --rpc local-shell MCP with the `claude` CLI as a Streamable HTTP server. The
|
|
187
|
+
* URL points at the reverse-tunnelled port, which the workspace's `ssh -R`
|
|
188
|
+
* forwards back to the RPC server on the calling machine. Same login-shell and
|
|
189
|
+
* idempotency reasoning as `claudeDevtoolsAddScript`.
|
|
190
|
+
*/
|
|
191
|
+
const claudeRpcAddScript = (remotePort, okMessage) => {
|
|
192
|
+
const body = [
|
|
193
|
+
'if command -v claude >/dev/null 2>&1; then',
|
|
194
|
+
` claude mcp remove ${RPC_MCP_NAME} >/dev/null 2>&1 || true`,
|
|
195
|
+
` claude mcp add --transport http ${RPC_MCP_NAME} http://127.0.0.1:${remotePort}/mcp`,
|
|
196
|
+
` echo ${shQuote(okMessage)}`,
|
|
197
|
+
'else',
|
|
198
|
+
' echo "WARNING: claude CLI not found on the remote; skipped local-shell MCP config." >&2',
|
|
199
|
+
'fi',
|
|
200
|
+
].join('\n');
|
|
201
|
+
return [`"\${SHELL:-bash}" -lc ${shQuote(body)}`];
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Bash lines (run on the REMOTE, from inside the workspace dir) that remove an
|
|
205
|
+
* MCP entry this workspace registered. Local scope keys off the current
|
|
96
206
|
* directory, so it targets only the workspace project. No-op if the entry or the
|
|
97
207
|
* claude CLI is absent. Runs under a login shell for the same PATH reason as
|
|
98
208
|
* `claudeDevtoolsAddScript`.
|
|
99
209
|
*/
|
|
100
|
-
const
|
|
101
|
-
const body = [
|
|
210
|
+
const claudeMcpRemoveScript = (name) => {
|
|
211
|
+
const body = [
|
|
212
|
+
'if command -v claude >/dev/null 2>&1; then',
|
|
213
|
+
` claude mcp remove ${name}`,
|
|
214
|
+
'fi'
|
|
215
|
+
].join('\n');
|
|
102
216
|
return [`"\${SHELL:-bash}" -lc ${shQuote(body)}`];
|
|
103
217
|
};
|
|
104
218
|
/**
|
|
@@ -111,35 +225,51 @@ export const buildRemoteScript = (ctx) => {
|
|
|
111
225
|
const devtoolsSetup = ctx.devtools
|
|
112
226
|
? claudeDevtoolsAddScript(ctx.devtools.remote, `Configured chrome-devtools MCP for this workspace (browser via 127.0.0.1:${ctx.devtools.remote}).`)
|
|
113
227
|
: [];
|
|
228
|
+
const rpcSetup = ctx.rpc
|
|
229
|
+
? claudeRpcAddScript(ctx.rpc.ports.remote, `Configured local-shell MCP for this workspace (runs ${ctx.rpc.shell} commands on the calling machine via 127.0.0.1:${ctx.rpc.ports.remote}).`)
|
|
230
|
+
: [];
|
|
114
231
|
return [
|
|
115
232
|
'set -u',
|
|
116
233
|
`DIR=${dir}`,
|
|
117
234
|
// Mutagen creates the sync root, but ensure it exists so `cd` never races it.
|
|
118
235
|
'mkdir -p "$DIR" || { echo "ERROR: could not create $DIR" >&2; exit 1; }',
|
|
119
236
|
'cd "$DIR" || { echo "ERROR: could not enter $DIR" >&2; exit 1; }',
|
|
120
|
-
// Register the
|
|
237
|
+
// Register the MCPs from inside $DIR: `claude mcp add` local scope keys off cwd.
|
|
121
238
|
...devtoolsSetup,
|
|
239
|
+
...rpcSetup,
|
|
122
240
|
'echo "Workspace ready at $DIR — files sync in the background (exit to stop syncing)."',
|
|
123
241
|
// eslint-disable-next-line no-template-curly-in-string -- shell parameter expansion, not a JS template
|
|
124
242
|
'"${SHELL:-bash}" -l',
|
|
125
243
|
].join('\n');
|
|
126
244
|
};
|
|
127
|
-
/** Run the remote-side teardown
|
|
128
|
-
export const runRemoteCleanup = (target, remoteDir) => new Promise((resolve, reject) => {
|
|
129
|
-
const child = spawn('ssh', [target, buildCleanupScript(remoteDir)], { stdio: 'inherit' });
|
|
245
|
+
/** Run the remote-side teardown over a fresh ssh connection. */
|
|
246
|
+
export const runRemoteCleanup = (target, remoteDir, opts = {}) => new Promise((resolve, reject) => {
|
|
247
|
+
const child = spawn('ssh', [target, buildCleanupScript(remoteDir, opts)], { stdio: 'inherit' });
|
|
130
248
|
child.once('error', reject);
|
|
131
249
|
child.once('close', (code) => resolve(code ?? 0));
|
|
132
250
|
});
|
|
133
251
|
/**
|
|
134
|
-
* The remote-side teardown script
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
252
|
+
* The remote-side teardown script. Runs from inside the workspace dir so the
|
|
253
|
+
* `claude` CLI's local scope resolves to the right project. The chrome-devtools
|
|
254
|
+
* and local-shell MCP entries are stripped only when their flags are set — i.e.
|
|
255
|
+
* when the workspace registered them via --devtools / --rpc; without them we
|
|
256
|
+
* never touch the user's MCP config. The synced files themselves are left in
|
|
257
|
+
* place — they are a real copy, not a mount — unless `deleteRemoteDir` is set,
|
|
258
|
+
* in which case the workspace dir is removed after any MCP config is stripped.
|
|
138
259
|
*/
|
|
139
|
-
export const buildCleanupScript = (remoteDir) => [
|
|
260
|
+
export const buildCleanupScript = (remoteDir, opts = {}) => [
|
|
140
261
|
`DIR=${shQuote(remoteDir)}`,
|
|
141
262
|
'cd "$DIR" 2>/dev/null || { echo "Nothing to clean up: $DIR is gone." >&2; exit 0; }',
|
|
142
|
-
...
|
|
263
|
+
...(opts.removeDevtoolsMcp ? claudeMcpRemoveScript(DEVTOOLS_MCP_NAME) : []),
|
|
264
|
+
...(opts.removeRpcMcp ? claudeMcpRemoveScript(RPC_MCP_NAME) : []),
|
|
265
|
+
// Remove the workspace dir last: cd out first so we don't rm the cwd out from
|
|
266
|
+
// under the shell, then delete it. Only when explicitly requested.
|
|
267
|
+
...(opts.deleteRemoteDir
|
|
268
|
+
? [
|
|
269
|
+
'cd / || exit 1',
|
|
270
|
+
'rm -rf "$DIR" && echo "Removed remote dir $DIR." || { echo "ERROR: could not remove $DIR" >&2; exit 1; }',
|
|
271
|
+
]
|
|
272
|
+
: []),
|
|
143
273
|
].join('\n');
|
|
144
274
|
/** True if an `ssh` client is on PATH (works on Windows, macOS, Linux). */
|
|
145
275
|
export const hasSshClient = () => {
|
|
@@ -158,6 +288,8 @@ export const hasMutagen = () => {
|
|
|
158
288
|
* the mode switches to two-way-resolved (alpha always wins conflicts), so
|
|
159
289
|
* `--source remote` puts the server first and `--source local` puts this
|
|
160
290
|
* machine first. Labels let `workspace cleanup` find and terminate orphans.
|
|
291
|
+
* `ctx.ignores` (from --ignore-vcs) excludes gitignored paths from the sync so
|
|
292
|
+
* each side keeps its own build artifacts and platform-specific binaries.
|
|
161
293
|
*/
|
|
162
294
|
export const buildMutagenCreateArgs = (ctx, target) => {
|
|
163
295
|
const local = ctx.localCwd;
|
|
@@ -173,6 +305,7 @@ export const buildMutagenCreateArgs = (ctx, target) => {
|
|
|
173
305
|
'--label=managed-by=fnd-workspace',
|
|
174
306
|
`--label=dir=${slugify(ctx.localDirName)}`,
|
|
175
307
|
`--sync-mode=${syncMode}`,
|
|
308
|
+
...(ctx.ignores ?? []).map((p) => `--ignore=${p}`),
|
|
176
309
|
alpha,
|
|
177
310
|
beta,
|
|
178
311
|
];
|
package/oclif.manifest.json
CHANGED
|
@@ -186,12 +186,27 @@
|
|
|
186
186
|
"workspace:cleanup": {
|
|
187
187
|
"aliases": [],
|
|
188
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
|
|
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
190
|
"examples": [
|
|
191
191
|
"<%= config.bin %> <%= command.id %> --ssh user@host",
|
|
192
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --remote-dir /home/fnd/cole/fnd-cli"
|
|
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"
|
|
193
196
|
],
|
|
194
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
|
+
},
|
|
195
210
|
"remote-base": {
|
|
196
211
|
"description": "base dir on the remote, used to derive the default remote directory path",
|
|
197
212
|
"name": "remote-base",
|
|
@@ -207,6 +222,12 @@
|
|
|
207
222
|
"multiple": false,
|
|
208
223
|
"type": "option"
|
|
209
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
|
+
},
|
|
210
231
|
"ssh": {
|
|
211
232
|
"description": "remote to connect to, as user@host",
|
|
212
233
|
"name": "ssh",
|
|
@@ -240,10 +261,20 @@
|
|
|
240
261
|
"<%= config.bin %> <%= command.id %> --ssh user@203.0.113.4",
|
|
241
262
|
"<%= config.bin %> <%= command.id %> --ssh user@host --source local",
|
|
242
263
|
"<%= config.bin %> <%= command.id %> --ssh user@host --remote-base /home/fnd",
|
|
264
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --ignore-vcs",
|
|
243
265
|
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9222",
|
|
244
|
-
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9333:9222"
|
|
266
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9333:9222",
|
|
267
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --rpc port=7777:7700",
|
|
268
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --rpc port=7777:7700,profile=false,shell=zsh",
|
|
269
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir"
|
|
245
270
|
],
|
|
246
271
|
"flags": {
|
|
272
|
+
"delete-remote-dir": {
|
|
273
|
+
"description": "on exit, delete the remote workspace directory instead of leaving the synced copy in place",
|
|
274
|
+
"name": "delete-remote-dir",
|
|
275
|
+
"allowNo": false,
|
|
276
|
+
"type": "boolean"
|
|
277
|
+
},
|
|
247
278
|
"devtools": {
|
|
248
279
|
"description": "expose your LOCAL browser to Claude on the remote via the chrome-devtools MCP. Value is \"port\" (same port both ends) or \"remote:local\" (local = this machine, where the browser runs). Your browser must already be listening with --remote-debugging-port=<local>.",
|
|
249
280
|
"name": "devtools",
|
|
@@ -251,6 +282,12 @@
|
|
|
251
282
|
"multiple": false,
|
|
252
283
|
"type": "option"
|
|
253
284
|
},
|
|
285
|
+
"ignore-vcs": {
|
|
286
|
+
"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.",
|
|
287
|
+
"name": "ignore-vcs",
|
|
288
|
+
"allowNo": false,
|
|
289
|
+
"type": "boolean"
|
|
290
|
+
},
|
|
254
291
|
"remote-base": {
|
|
255
292
|
"description": "base dir on the remote; the workspace lands at <base>/<local-user>/<dir-name>",
|
|
256
293
|
"name": "remote-base",
|
|
@@ -259,6 +296,13 @@
|
|
|
259
296
|
"multiple": false,
|
|
260
297
|
"type": "option"
|
|
261
298
|
},
|
|
299
|
+
"rpc": {
|
|
300
|
+
"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.",
|
|
301
|
+
"name": "rpc",
|
|
302
|
+
"hasDynamicHelp": false,
|
|
303
|
+
"multiple": false,
|
|
304
|
+
"type": "option"
|
|
305
|
+
},
|
|
262
306
|
"source": {
|
|
263
307
|
"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.",
|
|
264
308
|
"name": "source",
|
|
@@ -296,5 +340,5 @@
|
|
|
296
340
|
]
|
|
297
341
|
}
|
|
298
342
|
},
|
|
299
|
-
"version": "2.
|
|
343
|
+
"version": "2.4.0"
|
|
300
344
|
}
|
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.4.0",
|
|
5
5
|
"author": "Cole Denslow",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Justin Schellenberg"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"@oclif/plugin-plugins": "^5",
|
|
17
17
|
"chalk": "^5.6.2",
|
|
18
18
|
"ora": "^9.3.0",
|
|
19
|
-
"simple-git": "^3.32.2"
|
|
19
|
+
"simple-git": "^3.32.2",
|
|
20
|
+
"zod": "^4.4.3"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@eslint/compat": "^1",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
92
93
|
"posttest": "npm run lint",
|
|
93
94
|
"prepack": "oclif manifest && oclif readme",
|
|
94
|
-
"version": "oclif readme
|
|
95
|
+
"version": "oclif readme",
|
|
95
96
|
"prerelease": "npm run build && npm run prepack",
|
|
96
97
|
"release": "npm run build && npm run prepack && npm publish"
|
|
97
98
|
},
|