@fargowealth/fargo-ai-cli 0.7.1 → 0.7.3
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/CHANGELOG.md +18 -0
- package/README.md +32 -15
- package/package.json +1 -1
- package/src/agent-install.mjs +241 -0
- package/src/cli.mjs +112 -17
- package/src/constants.mjs +1 -1
- package/src/deployment-artifact.mjs +28 -1
- package/src/lego-deploy-artifacts.mjs +8 -0
- package/src/workbuddy-install.mjs +9 -126
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.3
|
|
4
|
+
|
|
5
|
+
- Added one-command Agent installation through `fargo agent install --agent`
|
|
6
|
+
for WorkBuddy, Claude Code, Codex, and Cursor.
|
|
7
|
+
- The installer completes CLI-managed OAuth + PKCE, writes a local stdio MCP
|
|
8
|
+
connection, and reuses the same private rotating credential on later runs.
|
|
9
|
+
- Kept `fargo workbuddy install` as a compatibility alias.
|
|
10
|
+
|
|
11
|
+
## 0.7.2
|
|
12
|
+
|
|
13
|
+
- Added `fargo lego deploy --help` with the complete Worker Static Assets
|
|
14
|
+
contract and an executable full-stack example.
|
|
15
|
+
- Worker deploys now fail early for manual `.fargo-assets/`, frontend
|
|
16
|
+
`index.html` files inside `--deploy-dir`, overlapping module/assets
|
|
17
|
+
directories, and asset options missing `--assets-dir`.
|
|
18
|
+
- Dry-run deployment summaries now show manifest schema version and separate
|
|
19
|
+
Worker-module and Static Asset file counts.
|
|
20
|
+
|
|
3
21
|
## 0.7.1
|
|
4
22
|
|
|
5
23
|
- Added Worker Static Assets packaging with explicit asset directory, binding,
|
package/README.md
CHANGED
|
@@ -9,23 +9,25 @@ Requires Node.js 20.19 or newer.
|
|
|
9
9
|
## Install or upgrade
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install -g @fargowealth/fargo-ai-cli@0.7.
|
|
12
|
+
npm install -g @fargowealth/fargo-ai-cli@0.7.3
|
|
13
13
|
fargo version
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
only when an Agent first needs local files:
|
|
16
|
+
Install the Fargo MCP connection with one command for the selected Agent:
|
|
18
17
|
|
|
19
18
|
```sh
|
|
20
|
-
fargo
|
|
21
|
-
fargo
|
|
22
|
-
fargo
|
|
19
|
+
npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=codex --open-browser=always --output=jsonl
|
|
20
|
+
npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=claude-code --open-browser=always --output=jsonl
|
|
21
|
+
npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=cursor --open-browser=always --output=jsonl
|
|
22
|
+
npx -y @fargowealth/fargo-ai-cli@latest agent install --agent=workbuddy --open-browser=always --output=jsonl
|
|
23
23
|
fargo auth status
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
Authorization Code + PKCE over an ephemeral IPv4 loopback callback
|
|
28
|
-
|
|
26
|
+
Run only the line for the Agent being configured. The command completes OAuth
|
|
27
|
+
Authorization Code + PKCE over an ephemeral IPv4 loopback callback, writes the
|
|
28
|
+
Agent's user-level local stdio MCP entry, and reports `installed=true`. It is
|
|
29
|
+
normally run once; the rotating credential is then refreshed automatically and
|
|
30
|
+
reused by both MCP and local CLI operations.
|
|
29
31
|
|
|
30
32
|
## MCP boundary
|
|
31
33
|
|
|
@@ -34,9 +36,10 @@ validates state, uses PKCE S256, closes the listener, and never prints tokens.
|
|
|
34
36
|
- CLI owns local walking, exclusions, ZIP creation, hashing, binary transfer,
|
|
35
37
|
deploy composition, and migration-file ordering.
|
|
36
38
|
- CLI has no generic R2 or Cloudflare administration commands.
|
|
37
|
-
- `fargo mcp serve` is the local stdio bridge for WorkBuddy
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
- `fargo mcp serve` is the local stdio bridge for WorkBuddy, Claude Code,
|
|
40
|
+
Codex, and Cursor.
|
|
41
|
+
- `fargo mcp tools` and `fargo mcp call` support scripts and one-shot
|
|
42
|
+
diagnostics without starting an MCP host.
|
|
40
43
|
|
|
41
44
|
```sh
|
|
42
45
|
fargo mcp tools
|
|
@@ -108,6 +111,18 @@ boolean flag when only selected routes should invoke the Worker first. The
|
|
|
108
111
|
hosted LEGO publisher uploads the assets and attaches the binding; Cloudflare
|
|
109
112
|
credentials remain server-side.
|
|
110
113
|
|
|
114
|
+
`.fargo-assets/` is an internal archive directory reserved for Fargo CLI. Do
|
|
115
|
+
not create it, copy frontend files into it, or place browser output inside the
|
|
116
|
+
Worker `--deploy-dir`. Always provide the original frontend build directory
|
|
117
|
+
through `--assets-dir`; the CLI packages it under `.fargo-assets/` and emits the
|
|
118
|
+
required schema v2 metadata. Worker deploys fail early when they contain a
|
|
119
|
+
manual `.fargo-assets/`, a root `index.html`, overlapping deploy/assets
|
|
120
|
+
directories, or asset options without `--assets-dir`.
|
|
121
|
+
|
|
122
|
+
Run `fargo lego deploy --help` for the complete option list and a full-stack
|
|
123
|
+
Worker example. A dry run reports the manifest schema version and separate
|
|
124
|
+
Worker-module and Static Asset file counts.
|
|
125
|
+
|
|
111
126
|
Pass `--source-exclude <gitignore-pattern>` once or multiple times when a
|
|
112
127
|
release command must make additional source exclusions explicit. Secure
|
|
113
128
|
defaults already exclude `node_modules/`, build output, credentials, and common
|
|
@@ -163,14 +178,16 @@ blocked. `--table-prefix` is a local assertion and the service rejects it if it
|
|
|
163
178
|
does not match the App's authoritative prefix. Migration execution remains
|
|
164
179
|
server-side and is never coupled to deploy.
|
|
165
180
|
|
|
166
|
-
##
|
|
181
|
+
## Compatibility and repair
|
|
167
182
|
|
|
168
183
|
```sh
|
|
169
184
|
npx -y @fargowealth/fargo-ai-cli@latest workbuddy install --open-browser=always --output=jsonl
|
|
170
185
|
```
|
|
171
186
|
|
|
172
|
-
The
|
|
173
|
-
|
|
187
|
+
The legacy WorkBuddy command is an alias for the unified Agent installer.
|
|
188
|
+
Installers preserve unrelated MCP servers. Rerun the unified command only to
|
|
189
|
+
upgrade or repair a Fargo entry. Device Authorization Grant remains an explicit
|
|
190
|
+
fallback:
|
|
174
191
|
|
|
175
192
|
```sh
|
|
176
193
|
fargo auth device --open-browser=always --output=jsonl
|
package/package.json
CHANGED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { normalizeAgentType } from "./auth.mjs";
|
|
7
|
+
import { CLI_VERSION } from "./constants.mjs";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = "@fargowealth/fargo-ai-cli";
|
|
10
|
+
const SERVER_NAME = "fargo-ai";
|
|
11
|
+
|
|
12
|
+
export async function installAgentMcp(rawAgent, options = {}) {
|
|
13
|
+
const agent = normalizeAgentType(rawAgent);
|
|
14
|
+
const platform = options.platform ?? process.platform;
|
|
15
|
+
const env = options.env ?? process.env;
|
|
16
|
+
const homeDirectory = options.home ?? homedir();
|
|
17
|
+
const packageSpec = `${PACKAGE_NAME}@${options.packageVersion ?? CLI_VERSION}`;
|
|
18
|
+
const npxCommand = options.npxCommand ?? (platform === "darwin"
|
|
19
|
+
? await resolveNpxCommand({ env, execPath: options.execPath, platform })
|
|
20
|
+
: "npx");
|
|
21
|
+
const server = mcpServerConfig({ agent, env, homeDirectory, npxCommand, packageSpec, platform });
|
|
22
|
+
|
|
23
|
+
if (agent === "workbuddy" || agent === "cursor") {
|
|
24
|
+
return installJsonMcp(agent, server, {
|
|
25
|
+
...options,
|
|
26
|
+
configPath: options.configPath ?? defaultAgentConfigPath(agent, homeDirectory)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return installHostCliMcp(agent, server, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function installWorkBuddyMcp(options = {}) {
|
|
34
|
+
return installAgentMcp("workbuddy", options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function defaultAgentConfigPath(rawAgent, homeDirectory = homedir()) {
|
|
38
|
+
const agent = normalizeAgentType(rawAgent);
|
|
39
|
+
if (agent === "workbuddy") return join(homeDirectory, ".workbuddy-ai", "mcp.json");
|
|
40
|
+
if (agent === "cursor") return join(homeDirectory, ".cursor", "mcp.json");
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function resolveNpxCommand(options = {}) {
|
|
45
|
+
const platform = options.platform ?? process.platform;
|
|
46
|
+
const env = options.env ?? process.env;
|
|
47
|
+
const execPath = options.execPath ?? process.execPath;
|
|
48
|
+
const executableName = platform === "win32" ? "npx.cmd" : "npx";
|
|
49
|
+
const pathDelimiter = platform === "win32" ? ";" : ":";
|
|
50
|
+
const candidates = [
|
|
51
|
+
join(dirname(execPath), executableName),
|
|
52
|
+
...String(env.PATH ?? env.Path ?? env.path ?? "").split(pathDelimiter).filter(Boolean).map((entry) => join(entry, executableName))
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const absoluteCandidate = isAbsolute(candidate) ? candidate : resolve(candidate);
|
|
56
|
+
try {
|
|
57
|
+
await access(absoluteCandidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
58
|
+
return absoluteCandidate;
|
|
59
|
+
} catch {
|
|
60
|
+
// Continue until an executable visible to the GUI host is found.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw new Error("Unable to locate an absolute npx executable for the Agent MCP configuration");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mcpServerConfig({ agent, env, homeDirectory, npxCommand, packageSpec, platform }) {
|
|
67
|
+
const credentialDirectory = platform === "win32"
|
|
68
|
+
? join(env.APPDATA || homeDirectory, "Fargo AI")
|
|
69
|
+
: join(homeDirectory, ".fargo");
|
|
70
|
+
const serverEnv = {
|
|
71
|
+
FARGO_CREDENTIAL_PATH: join(credentialDirectory, "credentials.json"),
|
|
72
|
+
FARGO_CREDENTIAL_STORE: "file",
|
|
73
|
+
FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock")
|
|
74
|
+
};
|
|
75
|
+
if (platform === "darwin") serverEnv.PATH = buildMacPath(npxCommand);
|
|
76
|
+
if (platform === "win32") {
|
|
77
|
+
const windowsPath = env.PATH ?? env.Path ?? env.path;
|
|
78
|
+
if (windowsPath) serverEnv.PATH = windowsPath;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
agent,
|
|
82
|
+
args: ["-y", packageSpec, "mcp", "serve"],
|
|
83
|
+
command: npxCommand,
|
|
84
|
+
env: serverEnv,
|
|
85
|
+
packageSpec
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function installJsonMcp(agent, server, options) {
|
|
90
|
+
const config = await readConfig(options.configPath, agent);
|
|
91
|
+
const nextServer = {
|
|
92
|
+
...(agent === "workbuddy" ? { type: "stdio" } : {}),
|
|
93
|
+
command: server.command,
|
|
94
|
+
args: server.args,
|
|
95
|
+
...(agent === "workbuddy" ? { timeout: 60000 } : {}),
|
|
96
|
+
env: server.env
|
|
97
|
+
};
|
|
98
|
+
const previousServer = config.mcpServers?.[SERVER_NAME];
|
|
99
|
+
const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);
|
|
100
|
+
|
|
101
|
+
if (changed) {
|
|
102
|
+
config.mcpServers = { ...(config.mcpServers ?? {}), [SERVER_NAME]: nextServer };
|
|
103
|
+
await writeConfig(options.configPath, config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return installationResult(agent, server.packageSpec, changed, {
|
|
107
|
+
config_path: options.configPath,
|
|
108
|
+
trust_required: agent === "workbuddy"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function installHostCliMcp(agent, server, options) {
|
|
113
|
+
const command = agent === "codex" ? "codex" : "claude";
|
|
114
|
+
const removeArgs = agent === "codex"
|
|
115
|
+
? ["mcp", "remove", SERVER_NAME]
|
|
116
|
+
: ["mcp", "remove", "--scope", "user", SERVER_NAME];
|
|
117
|
+
const removeResult = await runHostCommand(command, removeArgs, options);
|
|
118
|
+
if (removeResult.exitCode !== 0 && !isMissingServer(removeResult)) {
|
|
119
|
+
throw new Error(`${agentLabel(agent)} could not replace the existing ${SERVER_NAME} MCP entry: ${commandError(removeResult)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const environmentArgs = Object.entries(server.env).flatMap(([key, value]) => agent === "codex"
|
|
123
|
+
? ["--env", `${key}=${value}`]
|
|
124
|
+
: ["-e", `${key}=${value}`]);
|
|
125
|
+
const addArgs = agent === "codex"
|
|
126
|
+
? ["mcp", "add", ...environmentArgs, SERVER_NAME, "--", server.command, ...server.args]
|
|
127
|
+
: ["mcp", "add", "--transport", "stdio", "--scope", "user", SERVER_NAME, ...environmentArgs, "--", server.command, ...server.args];
|
|
128
|
+
const addResult = await runHostCommand(command, addArgs, options);
|
|
129
|
+
if (addResult.exitCode !== 0) {
|
|
130
|
+
throw new Error(`${agentLabel(agent)} could not add the ${SERVER_NAME} CLI MCP entry: ${commandError(addResult)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return installationResult(agent, server.packageSpec, true, {
|
|
134
|
+
configured_by: command,
|
|
135
|
+
trust_required: false
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runHostCommand(command, args, options) {
|
|
140
|
+
if (options.runCommand) return normalizeCommandResult(await options.runCommand(command, args));
|
|
141
|
+
return new Promise((resolveResult, reject) => {
|
|
142
|
+
const child = spawn(command, args, { env: options.env ?? process.env, windowsHide: true });
|
|
143
|
+
let stdout = "";
|
|
144
|
+
let stderr = "";
|
|
145
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
146
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
147
|
+
child.on("error", (error) => {
|
|
148
|
+
if (error?.code === "ENOENT") {
|
|
149
|
+
reject(new Error(`Required ${agentCommandLabel(command)} command '${command}' was not found; run the installer from that Agent environment`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
reject(error);
|
|
153
|
+
});
|
|
154
|
+
child.on("close", (exitCode) => resolveResult({ exitCode: exitCode ?? 1, stderr, stdout }));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeCommandResult(result) {
|
|
159
|
+
return {
|
|
160
|
+
exitCode: Number(result?.exitCode ?? result?.code ?? 0),
|
|
161
|
+
stderr: String(result?.stderr ?? ""),
|
|
162
|
+
stdout: String(result?.stdout ?? "")
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isMissingServer(result) {
|
|
167
|
+
return /not found|no .*mcp server|does not exist|unknown server/i.test(`${result.stdout}\n${result.stderr}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function commandError(result) {
|
|
171
|
+
return (result.stderr || result.stdout || `exit ${result.exitCode}`).trim().slice(0, 300);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function installationResult(agent, packageSpec, changed, extra) {
|
|
175
|
+
return {
|
|
176
|
+
type: "agent_installation_complete",
|
|
177
|
+
agent,
|
|
178
|
+
installed: true,
|
|
179
|
+
changed,
|
|
180
|
+
server_name: SERVER_NAME,
|
|
181
|
+
package: packageSpec,
|
|
182
|
+
...extra,
|
|
183
|
+
next_action: agent === "workbuddy"
|
|
184
|
+
? "Open WorkBuddy Custom Connectors, trust and enable fargo-ai, then start a new task."
|
|
185
|
+
: `Restart ${agentLabel(agent)} or start a new Agent session, then call ping_who_am_i.`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function agentLabel(agent) {
|
|
190
|
+
if (agent === "claude-code") return "Claude Code";
|
|
191
|
+
if (agent === "codex") return "Codex";
|
|
192
|
+
if (agent === "cursor") return "Cursor";
|
|
193
|
+
return "WorkBuddy";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function agentCommandLabel(command) {
|
|
197
|
+
return command === "claude" ? "Claude Code" : "Codex";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildMacPath(npxCommand) {
|
|
201
|
+
return [...new Set([
|
|
202
|
+
dirname(npxCommand),
|
|
203
|
+
"/opt/homebrew/bin",
|
|
204
|
+
"/usr/local/bin",
|
|
205
|
+
"/usr/bin",
|
|
206
|
+
"/bin",
|
|
207
|
+
"/usr/sbin",
|
|
208
|
+
"/sbin"
|
|
209
|
+
])].join(":");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function readConfig(configPath, agent) {
|
|
213
|
+
let text;
|
|
214
|
+
try {
|
|
215
|
+
text = await readFile(configPath, "utf8");
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error?.code === "ENOENT") return {};
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (!text.trim()) return {};
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(text);
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error(`${agentLabel(agent)} MCP config is not valid JSON: ${configPath}`);
|
|
226
|
+
}
|
|
227
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
228
|
+
throw new Error(`${agentLabel(agent)} MCP config must contain a JSON object: ${configPath}`);
|
|
229
|
+
}
|
|
230
|
+
if (parsed.mcpServers !== undefined && (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers))) {
|
|
231
|
+
throw new Error(`${agentLabel(agent)} mcpServers must be a JSON object: ${configPath}`);
|
|
232
|
+
}
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function writeConfig(configPath, config) {
|
|
237
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
238
|
+
const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
239
|
+
await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
240
|
+
await rename(temporaryPath, configPath);
|
|
241
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -8,13 +8,21 @@ import { deployLocalLegoApp } from "./lego-deploy.mjs";
|
|
|
8
8
|
import { applyLegoMigrations, planLegoMigrations } from "./lego-migrations.mjs";
|
|
9
9
|
import { callMcpTool, listMcpTools } from "./mcp-client.mjs";
|
|
10
10
|
import { runMcpProxy } from "./mcp-proxy.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { installAgentMcp } from "./agent-install.mjs";
|
|
12
12
|
|
|
13
13
|
export async function runCli(args, options = {}) {
|
|
14
14
|
const stdout = options.stdout ?? process.stdout;
|
|
15
15
|
const stderr = options.stderr ?? process.stderr;
|
|
16
16
|
const store = options.store ?? createCredentialStore(options.storeOptions);
|
|
17
17
|
try {
|
|
18
|
+
if (isHelpOption(args) && args[0] === "lego" && args[1] === "deploy") {
|
|
19
|
+
stdout.write(LEGO_DEPLOY_HELP);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if ((isHelpOption(args) && args.length === 1) || (args[0] === "help" && args.length === 1)) {
|
|
23
|
+
stdout.write(ROOT_HELP);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
18
26
|
if (args[0] === "version" || args[0] === "--version" || args[0] === "-v") {
|
|
19
27
|
stdout.write(`fargo-cli version ${CLI_VERSION}\n`);
|
|
20
28
|
return;
|
|
@@ -59,6 +67,8 @@ export async function runCli(args, options = {}) {
|
|
|
59
67
|
}
|
|
60
68
|
if (args[0] === "lego" && args[1] === "deploy") {
|
|
61
69
|
const appId = requiredOption(args, "--app");
|
|
70
|
+
const assetsDirectory = optionValue(args, "--assets-dir")?.trim();
|
|
71
|
+
assertAssetsDirectoryProvided(args, assetsDirectory);
|
|
62
72
|
const deployDir = requiredOption(args, "--deploy-dir");
|
|
63
73
|
const target = requiredOption(args, "--target");
|
|
64
74
|
const version = requiredVersionOption(args, "--version", 80);
|
|
@@ -68,7 +78,7 @@ export async function runCli(args, options = {}) {
|
|
|
68
78
|
const result = await deployLocalLegoApp(store, {
|
|
69
79
|
appId,
|
|
70
80
|
assetsBinding: optionValue(args, "--assets-binding"),
|
|
71
|
-
assetsDirectory
|
|
81
|
+
assetsDirectory,
|
|
72
82
|
assetsHtmlHandling: optionValue(args, "--assets-html-handling"),
|
|
73
83
|
assetsNotFoundHandling: optionValue(args, "--assets-not-found-handling"),
|
|
74
84
|
assetsRunWorkerFirst: workerFirstRoutes.length > 0 ? workerFirstRoutes : args.includes("--assets-run-worker-first"),
|
|
@@ -104,24 +114,15 @@ export async function runCli(args, options = {}) {
|
|
|
104
114
|
}
|
|
105
115
|
throw new Error("Usage: fargo lego d1 migrate plan|apply --app <appId> [--dir migrations] [--table-prefix app_prefix_] [--allow-data-migrations]");
|
|
106
116
|
}
|
|
117
|
+
if (args[0] === "agent" && args[1] === "install") {
|
|
118
|
+
await installAgentConnection(args, requiredOption(args, "--agent"), stdout, store, options);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
107
121
|
if (args[0] === "workbuddy" && args[1] === "install") {
|
|
108
|
-
|
|
109
|
-
if (credential) {
|
|
110
|
-
try {
|
|
111
|
-
credential = await currentCredential(store, options);
|
|
112
|
-
} catch {
|
|
113
|
-
await store.clear();
|
|
114
|
-
credential = undefined;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (credential?.authorization_method !== "authorization_code_pkce") {
|
|
118
|
-
credential = undefined;
|
|
119
|
-
}
|
|
120
|
-
if (!credential?.refresh_token) await authorizeWithPkce(args, stdout, store, { ...options, agentType: "workbuddy" });
|
|
121
|
-
stdout.write(`${JSON.stringify(await installWorkBuddyMcp(options))}\n`);
|
|
122
|
+
await installAgentConnection(args, "workbuddy", stdout, store, options);
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
|
-
throw new Error("Usage: fargo auth login|device|status|logout | fargo mcp serve|tools|call | fargo lego snapshot upload|list|download | fargo lego deploy | fargo lego d1 migrate plan|apply | fargo workbuddy install | fargo version");
|
|
125
|
+
throw new Error("Usage: fargo auth login|device|status|logout | fargo agent install --agent <agent> | fargo mcp serve|tools|call | fargo lego snapshot upload|list|download | fargo lego deploy | fargo lego d1 migrate plan|apply | fargo workbuddy install | fargo version");
|
|
125
126
|
} catch (error) {
|
|
126
127
|
stderr.write(`fargo: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
127
128
|
if (options.throwOnError) throw error;
|
|
@@ -129,6 +130,100 @@ export async function runCli(args, options = {}) {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
const ROOT_HELP = `Usage: fargo <command> [options]
|
|
134
|
+
|
|
135
|
+
Commands:
|
|
136
|
+
auth login|device|status|logout
|
|
137
|
+
agent install --agent <codex|claude-code|cursor|workbuddy>
|
|
138
|
+
mcp serve|tools|call
|
|
139
|
+
lego snapshot upload|list|download
|
|
140
|
+
lego deploy
|
|
141
|
+
lego d1 migrate plan|apply
|
|
142
|
+
workbuddy install
|
|
143
|
+
version
|
|
144
|
+
|
|
145
|
+
Run "fargo lego deploy --help" for deployment options.
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const LEGO_DEPLOY_HELP = `Usage: fargo lego deploy [options]
|
|
149
|
+
|
|
150
|
+
Required:
|
|
151
|
+
--app <appId> LEGO App ID
|
|
152
|
+
--deploy-dir <directory> Built Pages files or Worker modules
|
|
153
|
+
--target <pages|worker> Deployment target
|
|
154
|
+
--version <version> Release version
|
|
155
|
+
--name <name> Release name
|
|
156
|
+
--notes <text> Release notes (or --notes-file <path>)
|
|
157
|
+
|
|
158
|
+
Source and execution:
|
|
159
|
+
--source <directory> Source snapshot root (default: .)
|
|
160
|
+
--source-exclude <pattern> Repeatable source exclusion
|
|
161
|
+
--entrypoint <path> Default: index.html for Pages, index.js for Worker
|
|
162
|
+
--skip-source Do not upload a source snapshot
|
|
163
|
+
--dry-run Validate and summarize without network writes
|
|
164
|
+
|
|
165
|
+
Worker Static Assets:
|
|
166
|
+
--assets-dir <directory> Built browser assets; required for Worker frontends
|
|
167
|
+
--assets-binding <name> Assets binding (default: ASSETS)
|
|
168
|
+
--assets-html-handling <mode> auto-trailing-slash, force-trailing-slash,
|
|
169
|
+
drop-trailing-slash, or none
|
|
170
|
+
--assets-not-found-handling <mode>
|
|
171
|
+
none, 404-page, or single-page-application
|
|
172
|
+
--assets-run-worker-first Run the Worker before static asset routing
|
|
173
|
+
--assets-run-worker-first-route <glob>
|
|
174
|
+
Repeatable Worker-first route
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
fargo lego deploy --app app_xxx --source . \\
|
|
178
|
+
--deploy-dir .build/worker --target worker --entrypoint index.js \\
|
|
179
|
+
--assets-dir dist --assets-binding ASSETS \\
|
|
180
|
+
--assets-not-found-handling single-page-application \\
|
|
181
|
+
--assets-run-worker-first --version 1.4.0 --name "Full-stack release" \\
|
|
182
|
+
--notes-file CHANGELOG.md
|
|
183
|
+
|
|
184
|
+
.fargo-assets/ is an internal archive directory reserved for Fargo CLI.
|
|
185
|
+
Do not create or copy it manually; always pass browser output with --assets-dir.
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
function isHelpOption(args) {
|
|
189
|
+
return args.includes("--help") || args.includes("-h");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function installAgentConnection(args, agentType, stdout, store, options) {
|
|
193
|
+
let credential = await store.load();
|
|
194
|
+
if (credential) {
|
|
195
|
+
try {
|
|
196
|
+
credential = await currentCredential(store, options);
|
|
197
|
+
} catch {
|
|
198
|
+
await store.clear();
|
|
199
|
+
credential = undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (credential?.authorization_method !== "authorization_code_pkce") credential = undefined;
|
|
203
|
+
const authorizationReused = Boolean(credential?.refresh_token);
|
|
204
|
+
if (!authorizationReused) await authorizeWithPkce(args, stdout, store, { ...options, agentType });
|
|
205
|
+
const installation = await installAgentMcp(agentType, options);
|
|
206
|
+
stdout.write(`${JSON.stringify({ ...installation, authorization_reused: authorizationReused })}\n`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertAssetsDirectoryProvided(args, assetsDirectory) {
|
|
210
|
+
const assetOptions = [
|
|
211
|
+
"--assets-binding",
|
|
212
|
+
"--assets-dir",
|
|
213
|
+
"--assets-html-handling",
|
|
214
|
+
"--assets-not-found-handling",
|
|
215
|
+
"--assets-run-worker-first",
|
|
216
|
+
"--assets-run-worker-first-route"
|
|
217
|
+
];
|
|
218
|
+
if (!assetsDirectory && assetOptions.some((name) => hasOption(args, name))) {
|
|
219
|
+
throw new Error("Worker Static Assets options require --assets-dir <directory>; .fargo-assets/ is reserved for Fargo CLI and must not be created manually");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hasOption(args, name) {
|
|
224
|
+
return args.some((value) => value === name || value.startsWith(`${name}=`));
|
|
225
|
+
}
|
|
226
|
+
|
|
132
227
|
async function authorizeWithPkce(args, stdout, store, options) {
|
|
133
228
|
const browserMode = optionValue(args, "--open-browser") ?? "auto";
|
|
134
229
|
await authorizePkce(store, {
|
package/src/constants.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.7.
|
|
1
|
+
export const CLI_VERSION = "0.7.3";
|
|
2
2
|
export const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
3
3
|
export const DEFAULT_AUTH_URL = "https://auth.fargowealth.app";
|
|
4
4
|
export const DEFAULT_API_URL = "https://mcp.fargowealth.app";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstat, opendir, readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, extname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { strToU8, zipSync } from "fflate";
|
|
5
5
|
import ignore from "ignore";
|
|
6
6
|
|
|
@@ -38,6 +38,7 @@ export async function createDeploymentArtifact(deployDirectory, options = {}) {
|
|
|
38
38
|
const target = deploymentTarget(options.target);
|
|
39
39
|
const entrypoint = normalizePath(options.entrypoint || (target === "pages" ? "index.html" : "index.js"));
|
|
40
40
|
const assets = normalizeWorkerAssets(options.assets, target);
|
|
41
|
+
await assertReservedArchiveDirectoryAbsent(root);
|
|
41
42
|
const matcher = ignore().add(DEFAULT_IGNORE_RULES);
|
|
42
43
|
const customRules = await readOptionalText(join(root, ".legodeployignore"));
|
|
43
44
|
if (customRules) matcher.add(customRules);
|
|
@@ -45,12 +46,18 @@ export async function createDeploymentArtifact(deployDirectory, options = {}) {
|
|
|
45
46
|
const candidates = [];
|
|
46
47
|
const skippedSymlinks = [];
|
|
47
48
|
await walk(root, "", matcher, "");
|
|
49
|
+
if (target === "worker" && candidates.some((file) => file.path === "index.html")) {
|
|
50
|
+
throw new Error("Worker deployment output contains browser entry file 'index.html'. Keep Worker modules in --deploy-dir and pass frontend output separately with --assets-dir <directory>");
|
|
51
|
+
}
|
|
48
52
|
if (assets) {
|
|
49
53
|
const assetsRoot = resolve(assets.sourceDirectory);
|
|
50
54
|
const assetsRootStat = await lstat(assetsRoot).catch(() => undefined);
|
|
51
55
|
if (!assetsRootStat?.isDirectory()) {
|
|
52
56
|
throw new Error(`Worker assets output is not a directory: ${assetsRoot}`);
|
|
53
57
|
}
|
|
58
|
+
if (directoriesOverlap(root, assetsRoot)) {
|
|
59
|
+
throw new Error("--deploy-dir and --assets-dir must be separate, non-overlapping directories so browser assets are not uploaded as Worker modules");
|
|
60
|
+
}
|
|
54
61
|
const assetsMatcher = ignore().add(DEFAULT_IGNORE_RULES);
|
|
55
62
|
const assetsIgnore = await readOptionalText(join(assetsRoot, ".assetsignore"));
|
|
56
63
|
if (assetsIgnore) assetsMatcher.add(assetsIgnore);
|
|
@@ -161,6 +168,26 @@ export async function createDeploymentArtifact(deployDirectory, options = {}) {
|
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
async function assertReservedArchiveDirectoryAbsent(root) {
|
|
172
|
+
const reservedPath = join(root, WORKER_ASSET_ARCHIVE_DIRECTORY);
|
|
173
|
+
const reservedStat = await lstat(reservedPath).catch((error) => {
|
|
174
|
+
if (error?.code === "ENOENT") return undefined;
|
|
175
|
+
throw error;
|
|
176
|
+
});
|
|
177
|
+
if (reservedStat) {
|
|
178
|
+
throw new Error("Deployment output contains reserved path '.fargo-assets'. Do not create it manually; pass browser output with --assets-dir <directory> and Fargo CLI will package it");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function directoriesOverlap(left, right) {
|
|
183
|
+
return isSameOrInside(left, right) || isSameOrInside(right, left);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isSameOrInside(parent, candidate) {
|
|
187
|
+
const path = relative(parent, candidate);
|
|
188
|
+
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
function normalizeWorkerAssets(value, target) {
|
|
165
192
|
if (!value?.sourceDirectory) return undefined;
|
|
166
193
|
if (target !== "worker") throw new Error("Static assets are supported only for Worker deployments");
|
|
@@ -27,6 +27,10 @@ export async function uploadLegoDeployArtifact(store, input, options = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function deploymentArtifactSummary(artifact) {
|
|
30
|
+
const assetPrefix = artifact.assets ? `${artifact.assets.directory}/` : undefined;
|
|
31
|
+
const staticAssetFileCount = assetPrefix
|
|
32
|
+
? artifact.manifest.files.filter((file) => file.path.startsWith(assetPrefix)).length
|
|
33
|
+
: 0;
|
|
30
34
|
return {
|
|
31
35
|
archiveName: artifact.archiveName,
|
|
32
36
|
archiveSha256: artifact.archiveSha256,
|
|
@@ -35,9 +39,13 @@ export function deploymentArtifactSummary(artifact) {
|
|
|
35
39
|
contentSha256: artifact.contentSha256,
|
|
36
40
|
contentSizeBytes: artifact.contentSizeBytes,
|
|
37
41
|
entrypoint: artifact.entrypoint,
|
|
42
|
+
fileBreakdown: artifact.target === "worker"
|
|
43
|
+
? { staticAssets: staticAssetFileCount, workerModules: artifact.fileCount - staticAssetFileCount }
|
|
44
|
+
: { pagesFiles: artifact.fileCount },
|
|
38
45
|
fileCount: artifact.fileCount,
|
|
39
46
|
root: artifact.root,
|
|
40
47
|
skippedSymlinkCount: artifact.skippedSymlinkCount,
|
|
48
|
+
schemaVersion: artifact.manifest.schemaVersion,
|
|
41
49
|
target: artifact.target
|
|
42
50
|
};
|
|
43
51
|
}
|
|
@@ -1,129 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
-
import { CLI_VERSION } from "./constants.mjs";
|
|
1
|
+
import { defaultAgentConfigPath } from "./agent-install.mjs";
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
export {
|
|
4
|
+
defaultAgentConfigPath,
|
|
5
|
+
installAgentMcp,
|
|
6
|
+
installWorkBuddyMcp,
|
|
7
|
+
resolveNpxCommand
|
|
8
|
+
} from "./agent-install.mjs";
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
const env = options.env ?? process.env;
|
|
12
|
-
const homeDirectory = options.home ?? homedir();
|
|
13
|
-
const configPath = options.configPath
|
|
14
|
-
?? env.FARGO_WORKBUDDY_MCP_CONFIG
|
|
15
|
-
?? defaultWorkBuddyConfigPath(homeDirectory);
|
|
16
|
-
const packageSpec = `${PACKAGE_NAME}@${options.packageVersion ?? CLI_VERSION}`;
|
|
17
|
-
const config = await readConfig(configPath);
|
|
18
|
-
const npxCommand = options.npxCommand ?? (platform === "darwin"
|
|
19
|
-
? await resolveNpxCommand({ env, execPath: options.execPath, platform })
|
|
20
|
-
: "npx");
|
|
21
|
-
const nextServer = {
|
|
22
|
-
type: "stdio",
|
|
23
|
-
command: npxCommand,
|
|
24
|
-
args: ["-y", packageSpec, "mcp", "serve"],
|
|
25
|
-
timeout: 60000
|
|
26
|
-
};
|
|
27
|
-
if (platform === "darwin" || platform === "win32") {
|
|
28
|
-
const credentialDirectory = platform === "win32"
|
|
29
|
-
? join(env.APPDATA || homeDirectory, "Fargo AI")
|
|
30
|
-
: join(homeDirectory, ".fargo");
|
|
31
|
-
nextServer.env = {
|
|
32
|
-
FARGO_CREDENTIAL_PATH: join(credentialDirectory, "credentials.json"),
|
|
33
|
-
FARGO_CREDENTIAL_STORE: "file",
|
|
34
|
-
FARGO_REFRESH_LOCK_PATH: join(credentialDirectory, "oauth-refresh.lock")
|
|
35
|
-
};
|
|
36
|
-
if (platform === "darwin") nextServer.env.PATH = buildMacPath(npxCommand);
|
|
37
|
-
if (platform === "win32") {
|
|
38
|
-
const windowsPath = env.PATH ?? env.Path ?? env.path;
|
|
39
|
-
if (windowsPath) nextServer.env.PATH = windowsPath;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const previousServer = config.mcpServers?.["fargo-ai"];
|
|
43
|
-
const changed = JSON.stringify(previousServer) !== JSON.stringify(nextServer);
|
|
44
|
-
|
|
45
|
-
if (changed) {
|
|
46
|
-
config.mcpServers = { ...(config.mcpServers ?? {}), "fargo-ai": nextServer };
|
|
47
|
-
await writeConfig(configPath, config);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
type: "workbuddy_installation_complete",
|
|
52
|
-
installed: true,
|
|
53
|
-
changed,
|
|
54
|
-
config_path: configPath,
|
|
55
|
-
server_name: "fargo-ai",
|
|
56
|
-
package: packageSpec,
|
|
57
|
-
trust_required: true,
|
|
58
|
-
next_action: "Open WorkBuddy MCP management, trust and enable fargo-ai, then reload MCP or start a new task."
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function resolveNpxCommand(options = {}) {
|
|
63
|
-
const platform = options.platform ?? process.platform;
|
|
64
|
-
const env = options.env ?? process.env;
|
|
65
|
-
const execPath = options.execPath ?? process.execPath;
|
|
66
|
-
const executableName = platform === "win32" ? "npx.cmd" : "npx";
|
|
67
|
-
const pathDelimiter = platform === "win32" ? ";" : ":";
|
|
68
|
-
const candidates = [
|
|
69
|
-
join(dirname(execPath), executableName),
|
|
70
|
-
...String(env.PATH ?? "").split(pathDelimiter).filter(Boolean).map((entry) => join(entry, executableName))
|
|
71
|
-
];
|
|
72
|
-
for (const candidate of candidates) {
|
|
73
|
-
const absoluteCandidate = isAbsolute(candidate) ? candidate : resolve(candidate);
|
|
74
|
-
try {
|
|
75
|
-
await access(absoluteCandidate, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
76
|
-
return absoluteCandidate;
|
|
77
|
-
} catch {
|
|
78
|
-
// Keep searching for an executable that will also be visible to the GUI host.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
throw new Error("Unable to locate an absolute npx executable for WorkBuddy");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildMacPath(npxCommand) {
|
|
85
|
-
return [...new Set([
|
|
86
|
-
dirname(npxCommand),
|
|
87
|
-
"/opt/homebrew/bin",
|
|
88
|
-
"/usr/local/bin",
|
|
89
|
-
"/usr/bin",
|
|
90
|
-
"/bin",
|
|
91
|
-
"/usr/sbin",
|
|
92
|
-
"/sbin"
|
|
93
|
-
])].join(":");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function defaultWorkBuddyConfigPath(homeDirectory = homedir()) {
|
|
97
|
-
return join(homeDirectory, ".workbuddy-ai", "mcp.json");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function readConfig(configPath) {
|
|
101
|
-
let text;
|
|
102
|
-
try {
|
|
103
|
-
text = await readFile(configPath, "utf8");
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (error?.code === "ENOENT") return {};
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
if (!text.trim()) return {};
|
|
109
|
-
let parsed;
|
|
110
|
-
try {
|
|
111
|
-
parsed = JSON.parse(text);
|
|
112
|
-
} catch {
|
|
113
|
-
throw new Error(`WorkBuddy MCP config is not valid JSON: ${configPath}`);
|
|
114
|
-
}
|
|
115
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
116
|
-
throw new Error(`WorkBuddy MCP config must contain a JSON object: ${configPath}`);
|
|
117
|
-
}
|
|
118
|
-
if (parsed.mcpServers !== undefined && (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers))) {
|
|
119
|
-
throw new Error(`WorkBuddy mcpServers must be a JSON object: ${configPath}`);
|
|
120
|
-
}
|
|
121
|
-
return parsed;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function writeConfig(configPath, config) {
|
|
125
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
126
|
-
const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
127
|
-
await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
128
|
-
await rename(temporaryPath, configPath);
|
|
10
|
+
export function defaultWorkBuddyConfigPath(homeDirectory) {
|
|
11
|
+
return defaultAgentConfigPath("workbuddy", homeDirectory);
|
|
129
12
|
}
|