@fargowealth/fargo-ai-cli 0.7.1 → 0.7.2

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2
4
+
5
+ - Added `fargo lego deploy --help` with the complete Worker Static Assets
6
+ contract and an executable full-stack example.
7
+ - Worker deploys now fail early for manual `.fargo-assets/`, frontend
8
+ `index.html` files inside `--deploy-dir`, overlapping module/assets
9
+ directories, and asset options missing `--assets-dir`.
10
+ - Dry-run deployment summaries now show manifest schema version and separate
11
+ Worker-module and Static Asset file counts.
12
+
3
13
  ## 0.7.1
4
14
 
5
15
  - Added Worker Static Assets packaging with explicit asset directory, binding,
package/README.md CHANGED
@@ -9,7 +9,7 @@ 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.1
12
+ npm install -g @fargowealth/fargo-ai-cli@0.7.2
13
13
  fargo version
14
14
  ```
15
15
 
@@ -108,6 +108,18 @@ boolean flag when only selected routes should invoke the Worker first. The
108
108
  hosted LEGO publisher uploads the assets and attaches the binding; Cloudflare
109
109
  credentials remain server-side.
110
110
 
111
+ `.fargo-assets/` is an internal archive directory reserved for Fargo CLI. Do
112
+ not create it, copy frontend files into it, or place browser output inside the
113
+ Worker `--deploy-dir`. Always provide the original frontend build directory
114
+ through `--assets-dir`; the CLI packages it under `.fargo-assets/` and emits the
115
+ required schema v2 metadata. Worker deploys fail early when they contain a
116
+ manual `.fargo-assets/`, a root `index.html`, overlapping deploy/assets
117
+ directories, or asset options without `--assets-dir`.
118
+
119
+ Run `fargo lego deploy --help` for the complete option list and a full-stack
120
+ Worker example. A dry run reports the manifest schema version and separate
121
+ Worker-module and Static Asset file counts.
122
+
111
123
  Pass `--source-exclude <gitignore-pattern>` once or multiple times when a
112
124
  release command must make additional source exclusions explicit. Secure
113
125
  defaults already exclude `node_modules/`, build output, credentials, and common
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Agent-neutral Fargo AI CLI, MCP client, and LEGO project tooling",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -15,6 +15,14 @@ export async function runCli(args, options = {}) {
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: optionValue(args, "--assets-dir"),
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"),
@@ -129,6 +139,82 @@ export async function runCli(args, options = {}) {
129
139
  }
130
140
  }
131
141
 
142
+ const ROOT_HELP = `Usage: fargo <command> [options]
143
+
144
+ Commands:
145
+ auth login|device|status|logout
146
+ mcp serve|tools|call
147
+ lego snapshot upload|list|download
148
+ lego deploy
149
+ lego d1 migrate plan|apply
150
+ workbuddy install
151
+ version
152
+
153
+ Run "fargo lego deploy --help" for deployment options.
154
+ `;
155
+
156
+ const LEGO_DEPLOY_HELP = `Usage: fargo lego deploy [options]
157
+
158
+ Required:
159
+ --app <appId> LEGO App ID
160
+ --deploy-dir <directory> Built Pages files or Worker modules
161
+ --target <pages|worker> Deployment target
162
+ --version <version> Release version
163
+ --name <name> Release name
164
+ --notes <text> Release notes (or --notes-file <path>)
165
+
166
+ Source and execution:
167
+ --source <directory> Source snapshot root (default: .)
168
+ --source-exclude <pattern> Repeatable source exclusion
169
+ --entrypoint <path> Default: index.html for Pages, index.js for Worker
170
+ --skip-source Do not upload a source snapshot
171
+ --dry-run Validate and summarize without network writes
172
+
173
+ Worker Static Assets:
174
+ --assets-dir <directory> Built browser assets; required for Worker frontends
175
+ --assets-binding <name> Assets binding (default: ASSETS)
176
+ --assets-html-handling <mode> auto-trailing-slash, force-trailing-slash,
177
+ drop-trailing-slash, or none
178
+ --assets-not-found-handling <mode>
179
+ none, 404-page, or single-page-application
180
+ --assets-run-worker-first Run the Worker before static asset routing
181
+ --assets-run-worker-first-route <glob>
182
+ Repeatable Worker-first route
183
+
184
+ Example:
185
+ fargo lego deploy --app app_xxx --source . \\
186
+ --deploy-dir .build/worker --target worker --entrypoint index.js \\
187
+ --assets-dir dist --assets-binding ASSETS \\
188
+ --assets-not-found-handling single-page-application \\
189
+ --assets-run-worker-first --version 1.4.0 --name "Full-stack release" \\
190
+ --notes-file CHANGELOG.md
191
+
192
+ .fargo-assets/ is an internal archive directory reserved for Fargo CLI.
193
+ Do not create or copy it manually; always pass browser output with --assets-dir.
194
+ `;
195
+
196
+ function isHelpOption(args) {
197
+ return args.includes("--help") || args.includes("-h");
198
+ }
199
+
200
+ function assertAssetsDirectoryProvided(args, assetsDirectory) {
201
+ const assetOptions = [
202
+ "--assets-binding",
203
+ "--assets-dir",
204
+ "--assets-html-handling",
205
+ "--assets-not-found-handling",
206
+ "--assets-run-worker-first",
207
+ "--assets-run-worker-first-route"
208
+ ];
209
+ if (!assetsDirectory && assetOptions.some((name) => hasOption(args, name))) {
210
+ throw new Error("Worker Static Assets options require --assets-dir <directory>; .fargo-assets/ is reserved for Fargo CLI and must not be created manually");
211
+ }
212
+ }
213
+
214
+ function hasOption(args, name) {
215
+ return args.some((value) => value === name || value.startsWith(`${name}=`));
216
+ }
217
+
132
218
  async function authorizeWithPkce(args, stdout, store, options) {
133
219
  const browserMode = optionValue(args, "--open-browser") ?? "auto";
134
220
  await authorizePkce(store, {
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "0.7.1";
1
+ export const CLI_VERSION = "0.7.2";
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
  }