@firenet-designs/fnd-cli 2.3.3 → 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 +54 -24
- package/dist/commands/workspace/cleanup.d.ts +1 -0
- package/dist/commands/workspace/cleanup.js +12 -3
- package/dist/commands/workspace/index.d.ts +2 -0
- package/dist/commands/workspace/index.js +61 -15
- 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 +54 -12
- package/dist/lib/workspace.js +140 -18
- package/oclif.manifest.json +25 -2
- 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,14 +225,18 @@ 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',
|
|
@@ -133,16 +251,17 @@ export const runRemoteCleanup = (target, remoteDir, opts = {}) => new Promise((r
|
|
|
133
251
|
/**
|
|
134
252
|
* The remote-side teardown script. Runs from inside the workspace dir so the
|
|
135
253
|
* `claude` CLI's local scope resolves to the right project. The chrome-devtools
|
|
136
|
-
* MCP
|
|
137
|
-
* workspace registered
|
|
138
|
-
* MCP config. The synced files themselves are left in
|
|
139
|
-
* copy, not a mount — unless `deleteRemoteDir` is set,
|
|
140
|
-
* workspace dir is removed after any MCP config is stripped.
|
|
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.
|
|
141
259
|
*/
|
|
142
260
|
export const buildCleanupScript = (remoteDir, opts = {}) => [
|
|
143
261
|
`DIR=${shQuote(remoteDir)}`,
|
|
144
262
|
'cd "$DIR" 2>/dev/null || { echo "Nothing to clean up: $DIR is gone." >&2; exit 0; }',
|
|
145
|
-
...(opts.removeDevtoolsMcp ?
|
|
263
|
+
...(opts.removeDevtoolsMcp ? claudeMcpRemoveScript(DEVTOOLS_MCP_NAME) : []),
|
|
264
|
+
...(opts.removeRpcMcp ? claudeMcpRemoveScript(RPC_MCP_NAME) : []),
|
|
146
265
|
// Remove the workspace dir last: cd out first so we don't rm the cwd out from
|
|
147
266
|
// under the shell, then delete it. Only when explicitly requested.
|
|
148
267
|
...(opts.deleteRemoteDir
|
|
@@ -169,6 +288,8 @@ export const hasMutagen = () => {
|
|
|
169
288
|
* the mode switches to two-way-resolved (alpha always wins conflicts), so
|
|
170
289
|
* `--source remote` puts the server first and `--source local` puts this
|
|
171
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.
|
|
172
293
|
*/
|
|
173
294
|
export const buildMutagenCreateArgs = (ctx, target) => {
|
|
174
295
|
const local = ctx.localCwd;
|
|
@@ -184,6 +305,7 @@ export const buildMutagenCreateArgs = (ctx, target) => {
|
|
|
184
305
|
'--label=managed-by=fnd-workspace',
|
|
185
306
|
`--label=dir=${slugify(ctx.localDirName)}`,
|
|
186
307
|
`--sync-mode=${syncMode}`,
|
|
308
|
+
...(ctx.ignores ?? []).map((p) => `--ignore=${p}`),
|
|
187
309
|
alpha,
|
|
188
310
|
beta,
|
|
189
311
|
];
|
package/oclif.manifest.json
CHANGED
|
@@ -186,11 +186,12 @@
|
|
|
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. Pass --devtools to also strip the matching
|
|
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
192
|
"<%= config.bin %> <%= command.id %> --ssh user@host --remote-dir /home/fnd/cole/fnd-cli",
|
|
193
193
|
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools",
|
|
194
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --rpc",
|
|
194
195
|
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir"
|
|
195
196
|
],
|
|
196
197
|
"flags": {
|
|
@@ -221,6 +222,12 @@
|
|
|
221
222
|
"multiple": false,
|
|
222
223
|
"type": "option"
|
|
223
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
|
+
},
|
|
224
231
|
"ssh": {
|
|
225
232
|
"description": "remote to connect to, as user@host",
|
|
226
233
|
"name": "ssh",
|
|
@@ -254,8 +261,11 @@
|
|
|
254
261
|
"<%= config.bin %> <%= command.id %> --ssh user@203.0.113.4",
|
|
255
262
|
"<%= config.bin %> <%= command.id %> --ssh user@host --source local",
|
|
256
263
|
"<%= config.bin %> <%= command.id %> --ssh user@host --remote-base /home/fnd",
|
|
264
|
+
"<%= config.bin %> <%= command.id %> --ssh user@host --ignore-vcs",
|
|
257
265
|
"<%= config.bin %> <%= command.id %> --ssh user@host --devtools 9222",
|
|
258
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",
|
|
259
269
|
"<%= config.bin %> <%= command.id %> --ssh user@host --delete-remote-dir"
|
|
260
270
|
],
|
|
261
271
|
"flags": {
|
|
@@ -272,6 +282,12 @@
|
|
|
272
282
|
"multiple": false,
|
|
273
283
|
"type": "option"
|
|
274
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
|
+
},
|
|
275
291
|
"remote-base": {
|
|
276
292
|
"description": "base dir on the remote; the workspace lands at <base>/<local-user>/<dir-name>",
|
|
277
293
|
"name": "remote-base",
|
|
@@ -280,6 +296,13 @@
|
|
|
280
296
|
"multiple": false,
|
|
281
297
|
"type": "option"
|
|
282
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
|
+
},
|
|
283
306
|
"source": {
|
|
284
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.",
|
|
285
308
|
"name": "source",
|
|
@@ -317,5 +340,5 @@
|
|
|
317
340
|
]
|
|
318
341
|
}
|
|
319
342
|
},
|
|
320
|
-
"version": "2.
|
|
343
|
+
"version": "2.4.0"
|
|
321
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
|
},
|