@fargowealth/fargo-ai-cli 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - Added source-backup history metadata and Portal list/download support for
6
+ immutable source versions, including independent backup-only events.
7
+ - Deployment now requires a version number, version name, and upgrade notes;
8
+ history records show the deployer and expandable notes.
9
+ - Source backup remains the deploy default. Added explicit `--skip-source` for
10
+ deployments that intentionally omit a source archive.
11
+
3
12
  ## 0.6.0
4
13
 
5
14
  - Added `fargo lego deploy` to create a source snapshot and deterministic
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.6.0
12
+ npm install -g @fargowealth/fargo-ai-cli@0.7.0
13
13
  fargo version
14
14
  ```
15
15
 
@@ -47,8 +47,8 @@ fargo mcp call lego_get_app --args-file ./arguments.json
47
47
  ## LEGO source snapshots
48
48
 
49
49
  ```sh
50
- fargo lego snapshot upload --app app_xxx --source . --dry-run
51
- fargo lego snapshot upload --app app_xxx --source . --version v1
50
+ fargo lego snapshot upload --app app_xxx --source . --version 1.4.0 --name "Pre-upgrade backup" --dry-run
51
+ fargo lego snapshot upload --app app_xxx --source . --version 1.4.0 --name "Pre-upgrade backup" --notes "Before migration"
52
52
  fargo lego snapshot list --app app_xxx
53
53
  fargo lego snapshot download --app app_xxx --snapshot snap_xxx
54
54
  ```
@@ -57,6 +57,8 @@ The CLI applies secure defaults plus `.legoignore`, skips symbolic links,
57
57
  creates a deterministic ZIP, calculates SHA-256 digests, and uploads raw bytes
58
58
  over authenticated HTTP. The ZIP includes
59
59
  `.fargo-snapshot/manifest.json`; its default archive limit is 50 MB.
60
+ An upload creates a downloadable source-backup history event without deploying
61
+ or publishing the App.
60
62
 
61
63
  ## LEGO local deploy
62
64
 
@@ -70,11 +72,16 @@ fargo lego deploy \
70
72
  --deploy-dir dist \
71
73
  --target pages \
72
74
  --entrypoint index.html \
73
- --version v1
75
+ --version 1.4.0 \
76
+ --name "Navigation refresh" \
77
+ --notes-file CHANGELOG.md
74
78
  ```
75
79
 
76
80
  For Workers use `--target worker`; the default entrypoint becomes `index.js`.
77
81
  The CLI does not install dependencies, infer a framework, or run the build.
82
+ Version number, version name, and upgrade notes are required. Source upload is
83
+ the default; `--skip-source` is the explicit exception for a deployment that
84
+ must not retain a source archive.
78
85
 
79
86
  The command creates and uploads two immutable archives:
80
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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
@@ -58,14 +58,23 @@ export async function runCli(args, options = {}) {
58
58
  return;
59
59
  }
60
60
  if (args[0] === "lego" && args[1] === "deploy") {
61
+ const appId = requiredOption(args, "--app");
62
+ const deployDir = requiredOption(args, "--deploy-dir");
63
+ const target = requiredOption(args, "--target");
64
+ const version = requiredVersionOption(args, "--version", 80);
65
+ const versionName = requiredTextOption(args, "--name", 160);
66
+ const releaseNotes = await requiredNotesOption(args);
61
67
  const result = await deployLocalLegoApp(store, {
62
- appId: requiredOption(args, "--app"),
63
- deployDir: requiredOption(args, "--deploy-dir"),
68
+ appId,
69
+ deployDir,
64
70
  dryRun: args.includes("--dry-run"),
65
71
  entrypoint: optionValue(args, "--entrypoint"),
72
+ releaseNotes,
73
+ skipSource: args.includes("--skip-source"),
66
74
  source: optionValue(args, "--source") ?? ".",
67
- target: requiredOption(args, "--target"),
68
- version: optionValue(args, "--version")
75
+ target,
76
+ version,
77
+ versionName
69
78
  }, options);
70
79
  stdout.write(`${JSON.stringify(result)}\n`);
71
80
  return;
@@ -138,8 +147,11 @@ async function runLegoSnapshotCommand(args, stdout, store, options) {
138
147
  const result = await uploadLegoSourceSnapshot(store, {
139
148
  appId,
140
149
  dryRun: args.includes("--dry-run"),
150
+ notes: await optionalNotesOption(args),
151
+ purpose: "backup",
141
152
  source: optionValue(args, "--source") ?? ".",
142
- version: optionValue(args, "--version")
153
+ version: requiredVersionOption(args, "--version", 120),
154
+ versionName: requiredTextOption(args, "--name", 160)
143
155
  }, options);
144
156
  stdout.write(`${JSON.stringify(result)}\n`);
145
157
  return;
@@ -160,7 +172,25 @@ async function runLegoSnapshotCommand(args, stdout, store, options) {
160
172
  stdout.write(`${JSON.stringify({ archiveSha256: result.archiveSha256, outputPath, sizeBytes: result.archive.byteLength, snapshotId })}\n`);
161
173
  return;
162
174
  }
163
- throw new Error("Usage: fargo lego snapshot upload|list|download --app <appId>");
175
+ throw new Error("Usage: fargo lego snapshot upload --app <appId> --version <number> --name <name> [--notes TEXT | --notes-file PATH] | list|download --app <appId>");
176
+ }
177
+
178
+ async function requiredNotesOption(args) {
179
+ const notes = await optionalNotesOption(args);
180
+ if (!notes) throw new Error("Missing release notes; pass --notes <text> or --notes-file <path>");
181
+ if (notes.length > 4_000) throw new Error("Release notes must not exceed 4000 characters");
182
+ return notes;
183
+ }
184
+
185
+ async function optionalNotesOption(args) {
186
+ const inline = optionValue(args, "--notes");
187
+ const file = optionValue(args, "--notes-file");
188
+ if (inline !== undefined && file !== undefined) throw new Error("Use only one of --notes or --notes-file");
189
+ const notes = file !== undefined
190
+ ? (await readFile(resolve(file), "utf8")).trim()
191
+ : inline?.trim() ?? "";
192
+ if (notes.length > 4_000) throw new Error("Notes must not exceed 4000 characters");
193
+ return notes;
164
194
  }
165
195
 
166
196
  async function readToolArgs(args) {
@@ -192,7 +222,21 @@ async function pathExists(path) {
192
222
 
193
223
  function requiredOption(args, name) {
194
224
  const value = optionValue(args, name)?.trim();
195
- if (!value) throw new Error(`Missing required option ${name}`);
225
+ if (!value || value.startsWith("--")) throw new Error(`Missing required option ${name}`);
226
+ return value;
227
+ }
228
+
229
+ function requiredTextOption(args, name, maxLength) {
230
+ const value = requiredOption(args, name);
231
+ if (value.length > maxLength) throw new Error(`${name} must not exceed ${maxLength} characters`);
232
+ return value;
233
+ }
234
+
235
+ function requiredVersionOption(args, name, maxLength) {
236
+ const value = requiredTextOption(args, name, maxLength);
237
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
238
+ throw new Error(`${name} must use letters, numbers, dot, underscore, or hyphen`);
239
+ }
196
240
  return value;
197
241
  }
198
242
 
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "0.6.0";
1
+ export const CLI_VERSION = "0.7.0";
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";
@@ -9,25 +9,34 @@ export async function deployLocalLegoApp(store, input, options = {}) {
9
9
  const target = requiredTarget(input.target);
10
10
  const entrypoint = input.entrypoint || (target === "pages" ? "index.html" : "index.js");
11
11
  const [snapshot, artifact] = await Promise.all([
12
- createSourceSnapshot(input.source || ".", options.snapshotOptions),
12
+ input.skipSource ? Promise.resolve(undefined) : createSourceSnapshot(input.source || ".", options.snapshotOptions),
13
13
  createDeploymentArtifact(input.deployDir, { ...options.artifactOptions, target, entrypoint })
14
14
  ]);
15
15
 
16
16
  if (input.dryRun) {
17
17
  return {
18
18
  dryRun: true,
19
- sourceSnapshot: sourceSnapshotSummary(snapshot),
19
+ release: {
20
+ notes: input.releaseNotes,
21
+ version: input.version,
22
+ versionName: input.versionName
23
+ },
24
+ sourceSnapshot: snapshot ? sourceSnapshotSummary(snapshot) : undefined,
25
+ sourceSnapshotSkipped: !snapshot,
20
26
  deployArtifact: deploymentArtifactSummary(artifact)
21
27
  };
22
28
  }
23
29
 
24
- const uploadedSnapshot = await uploadLegoSourceSnapshot(store, {
30
+ const uploadedSnapshot = snapshot ? await uploadLegoSourceSnapshot(store, {
25
31
  appId: input.appId,
32
+ notes: input.releaseNotes,
33
+ purpose: "deployment",
26
34
  source: input.source || ".",
27
- version: input.version
28
- }, { ...options, snapshot });
29
- const sourceSnapshotId = uploadedSnapshot.sourceSnapshot?.id;
30
- if (!sourceSnapshotId) throw new Error("Source snapshot upload did not return sourceSnapshot.id");
35
+ version: input.version,
36
+ versionName: input.versionName
37
+ }, { ...options, snapshot }) : undefined;
38
+ const sourceSnapshotId = uploadedSnapshot?.sourceSnapshot?.id;
39
+ if (snapshot && !sourceSnapshotId) throw new Error("Source snapshot upload did not return sourceSnapshot.id");
31
40
 
32
41
  const uploadedArtifact = await uploadLegoDeployArtifact(store, {
33
42
  appId: input.appId,
@@ -38,18 +47,22 @@ export async function deployLocalLegoApp(store, input, options = {}) {
38
47
  if (!deployArtifactId) throw new Error("Deployment artifact upload did not return deployArtifact.id");
39
48
 
40
49
  const tool = target === "pages" ? "lego_pages_deploy" : "lego_worker_deploy";
41
- const deployment = mcpToolPayload(await callMcpTool(store, tool, {
50
+ const deploymentInput = {
42
51
  appId: input.appId,
43
52
  deployArtifactId,
44
53
  entrypoint: artifact.entrypoint,
45
- sourceSnapshotId,
46
- version: input.version
47
- }, options));
54
+ releaseNotes: input.releaseNotes,
55
+ version: input.version,
56
+ versionName: input.versionName,
57
+ ...(sourceSnapshotId ? { sourceSnapshotId } : {})
58
+ };
59
+ const deployment = mcpToolPayload(await callMcpTool(store, tool, deploymentInput, options));
48
60
 
49
61
  return {
50
62
  appId: input.appId,
51
63
  target,
52
- sourceSnapshot: uploadedSnapshot.sourceSnapshot,
64
+ sourceSnapshot: uploadedSnapshot?.sourceSnapshot,
65
+ sourceSnapshotSkipped: !snapshot,
53
66
  deployArtifact: uploadedArtifact.deployArtifact,
54
67
  deployment: deployment.deployment,
55
68
  sourceVersion: deployment.sourceVersion,
@@ -5,11 +5,26 @@ import { createSourceSnapshot } from "./source-snapshot.mjs";
5
5
  export async function uploadLegoSourceSnapshot(store, input, options = {}) {
6
6
  const snapshot = options.snapshot ?? await createSourceSnapshot(input.source ?? ".", options.snapshotOptions);
7
7
  if (input.dryRun) {
8
- return { dryRun: true, snapshot: snapshotSummary(snapshot) };
8
+ return {
9
+ dryRun: true,
10
+ metadata: {
11
+ notes: input.notes ?? "",
12
+ purpose: input.purpose ?? "backup",
13
+ version: input.version ?? "",
14
+ versionName: input.versionName ?? ""
15
+ },
16
+ snapshot: snapshotSummary(snapshot)
17
+ };
9
18
  }
10
19
 
11
20
  const url = snapshotCollectionUrl(input.appId, options);
12
21
  if (input.version) url.searchParams.set("version", input.version);
22
+ const metadata = Buffer.from(JSON.stringify({
23
+ notes: input.notes ?? "",
24
+ purpose: input.purpose ?? "backup",
25
+ version: input.version ?? "",
26
+ versionName: input.versionName ?? ""
27
+ }), "utf8").toString("base64url");
13
28
  const response = await authenticatedRequest(store, url, {
14
29
  method: "POST",
15
30
  headers: {
@@ -19,6 +34,7 @@ export async function uploadLegoSourceSnapshot(store, input, options = {}) {
19
34
  "X-Fargo-Source-Archive-Sha256": snapshot.archiveSha256,
20
35
  "X-Fargo-Source-Content-Sha256": snapshot.contentSha256,
21
36
  "X-Fargo-Source-File-Count": String(snapshot.fileCount),
37
+ "X-Fargo-Source-Metadata": metadata,
22
38
  "X-Fargo-Source-Size": String(snapshot.sourceSizeBytes)
23
39
  },
24
40
  body: snapshot.archive