@fluid-app/fluid-cli-assets 0.1.11 → 0.1.12

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @fluid-app/fluid-cli-assets@0.1.11 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/assets
2
+ > @fluid-app/fluid-cli-assets@0.1.12 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/assets
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,9 +8,11 @@
8
8
  ℹ target: node24
9
9
  ℹ tsconfig: tsconfig.json
10
10
  ℹ Build start
11
- ℹ dist/index.mjs  8.76 kB │ gzip: 1.28 kB
12
- ℹ dist/index.mjs.map 17.08 kB │ gzip: 2.44 kB
11
+ ℹ dist/index.mjs 12.11 kB │ gzip: 2.54 kB
12
+ ℹ dist/index.mjs.map 26.57 kB │ gzip: 5.83 kB
13
13
  ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
14
  ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
- ℹ 4 files, total: 26.15 kB
16
- ✔ Build complete in 2685ms
15
+ ℹ 4 files, total: 38.99 kB
16
+ ✔ Build complete in 5682ms
17
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
18
+
@@ -1,4 +1,4 @@
1
1
 
2
- > @fluid-app/fluid-cli-assets@0.1.11 typecheck /home/runner/_work/fluid-mono/fluid-mono/packages/cli/assets
2
+ > @fluid-app/fluid-cli-assets@0.1.12 typecheck /home/runner/_work/fluid-mono/fluid-mono/packages/cli/assets
3
3
  > tsgo --noEmit
4
4
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;cAKM,MAAA,EAAQ,WAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;cAMM,MAAA,EAAQ,WAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,67 @@
1
- import { createDomainCommand } from "@fluid-app/fluid-cli";
1
+ import { createDomainCommand, findProjectConfig, getAuthToken } from "@fluid-app/fluid-cli";
2
2
  import "commander";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { basename } from "node:path";
5
+ //#region src/commands/upload.ts
6
+ const UPLOAD_BASE = (process.env["FLUID_UPLOAD_BASE"] ?? "https://upload.fluid.app").replace(/\/$/, "");
7
+ function registerUpload(parent, ctx) {
8
+ parent.command("upload [file]").description("Upload a file or URL to the Fluid DAM (via upload.fluid.app)").option("--url <url>", "Fetch from an external URL instead of attaching a local file").option("--name <name>", "Display name for the DAM asset").option("--description <description>", "Asset description").option("--tags <tags>", "Comma-separated tags").option("--folder <folder>", "ImageKit folder override").option("--no-unique-filename", "Disable ImageKit's useUniqueFileName (default: enabled)").option("--create-media", "Also create a Fluid Media resource (active, share, publish-now)").action(async function(filePath, opts) {
9
+ const token = resolveToken(this.parent?.opts() ?? {});
10
+ if (!token) throw new Error("No API token found. Run `fluid login` or provide --token <token>.");
11
+ if (!filePath && !opts.url) throw new Error("Provide a file path or --url <url>. Run `fluid dam upload --help` for usage.");
12
+ if (filePath && opts.url) throw new Error("Provide either a file path OR --url, not both.");
13
+ const form = new FormData();
14
+ if (opts.folder) form.append("folder", opts.folder);
15
+ if (opts.uniqueFilename === false) form.append("useUniqueFileName", "false");
16
+ if (opts.url) form.append("external_asset_url", opts.url);
17
+ else if (filePath) {
18
+ await stat(filePath);
19
+ const bytes = await readFile(filePath);
20
+ const name = basename(filePath);
21
+ form.append("fileName", name);
22
+ form.append("file", new Blob([bytes]), name);
23
+ }
24
+ if (opts.name) form.append("name", opts.name);
25
+ if (opts.description) form.append("description", opts.description);
26
+ if (opts.tags) form.append("tags", opts.tags);
27
+ if (opts.createMedia) form.append("create_media", "true");
28
+ if (ctx.verbose) process.stderr.write(`POST ${UPLOAD_BASE}/upload\n`);
29
+ const resp = await fetch(`${UPLOAD_BASE}/upload`, {
30
+ method: "POST",
31
+ headers: { Authorization: `Bearer ${token}` },
32
+ body: form
33
+ });
34
+ if (!resp.ok) {
35
+ let detail = "";
36
+ try {
37
+ detail = await resp.text();
38
+ } catch {}
39
+ throw new Error(`Upload failed (HTTP ${resp.status})${detail ? `: ${detail.slice(0, 400)}` : ""}`);
40
+ }
41
+ const body = await resp.json();
42
+ ctx.output(body);
43
+ });
44
+ }
45
+ /**
46
+ * Token resolution mirrors `createCommandContext.resolveToken` from
47
+ * @fluid-app/fluid-cli: explicit flag → --profile lookup → env vars
48
+ * → project-local `.fluidrc` → global active profile. Re-implemented
49
+ * here instead of imported because the helper isn't exported, and
50
+ * the duplication is tiny.
51
+ */
52
+ function resolveToken(opts) {
53
+ if (opts.token) return opts.token;
54
+ if (opts.profile) return getAuthToken(opts.profile);
55
+ const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"];
56
+ if (envToken) return envToken;
57
+ const projectConfig = findProjectConfig(process.cwd());
58
+ if (projectConfig?.profile) {
59
+ const rcToken = getAuthToken(projectConfig.profile);
60
+ if (rcToken) return rcToken;
61
+ }
62
+ return getAuthToken();
63
+ }
64
+ //#endregion
3
65
  //#region src/generated/primary.ts
4
66
  function registerPrimary(parent, ctx) {
5
67
  parent.command("get-asset-paths <code>").description("Lists asset paths for an existing asset").action(async (code) => {
@@ -164,7 +226,12 @@ const plugin = {
164
226
  name: "@fluid-app/fluid-cli-assets",
165
227
  version: "0.1.0",
166
228
  register(ctx) {
167
- ctx.program.addCommand(createDomainCommand("assets", "Manage Fluid digital assets (DAM)", registerAllCommands));
229
+ const assets = createDomainCommand("assets", "Manage Fluid digital assets (DAM)", (parent, cmdCtx) => {
230
+ registerAllCommands(parent, cmdCtx);
231
+ registerUpload(parent, cmdCtx);
232
+ });
233
+ assets.alias("dam");
234
+ ctx.program.addCommand(assets);
168
235
  }
169
236
  };
170
237
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/generated/primary.ts","../src/generated/index.ts","../src/index.ts"],"sourcesContent":["// primary.ts — AUTO-GENERATED, DO NOT EDIT\nimport { Command } from \"commander\";\nimport type { CommandContext } from \"@fluid-app/fluid-cli\";\n\nexport function registerPrimary(parent: Command, ctx: CommandContext): void {\n parent\n .command(\"get-asset-paths <code>\")\n .description(\"Lists asset paths for an existing asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/asset_paths` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/asset_paths`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-asset-paths <code>\")\n .description(\"Creates asset paths for an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/${code}/asset_paths` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/${code}/asset_paths`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"delete-asset-path <code> <id>\")\n .description(\"Destroys an asset path\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"DELETE \" + `/api/dam/assets/${code}/asset_paths/${id}` + \"\\n\",\n );\n const result = await client.delete(\n `/api/dam/assets/${code}/asset_paths/${id}`,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"create-backfill-imagekit\")\n .description(\"Creates a DAM asset from ImageKit upload metadata\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/backfill_imagekit` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/backfill_imagekit`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"create-asset-invalid-mixed-payload\")\n .description(\"Errors when multiple asset types are provided\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose) process.stderr.write(\"POST \" + `/api/dam/assets` + \"\\n\");\n const result = await client.post(`/api/dam/assets`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"get-asset <code>\")\n .description(\"Retrieves an asset by code\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"GET \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.get(`/api/dam/assets/${code}`);\n ctx.output(result);\n });\n\n parent\n .command(\"update-asset <code>\")\n .description(\"Updates an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\"PUT \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.put(`/api/dam/assets/${code}`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"delete-asset <code>\")\n .description(\"Deletes an asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"DELETE \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.delete(`/api/dam/assets/${code}`);\n ctx.output(result);\n });\n\n parent\n .command(\"discard-asset <code>\")\n .description(\"Discard an asset (soft delete)\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"PATCH \" + `/api/dam/assets/${code}/discard` + \"\\n\",\n );\n const result = await client.patch(`/api/dam/assets/${code}/discard`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-imagekit-auth\")\n .description(\n \"Creates ImageKit authentication credentials for large file uploads\",\n )\n .action(async () => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"POST \" + `/api/dam/assets/imagekit_auth` + \"\\n\");\n const result = await client.post(`/api/dam/assets/imagekit_auth`);\n ctx.output(result);\n });\n\n parent\n .command(\"query-assets\")\n .description(\"Query DAM assets using tree paths and tags\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose) process.stderr.write(\"POST \" + `/api/dam/query` + \"\\n\");\n const result = await client.post(`/api/dam/query`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"list-variants <code>\")\n .description(\"Returns all variants for an existing asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/variants` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/variants`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-variants <code>\")\n .description(\"Creates variants for an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/${code}/variants` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/${code}/variants`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"get-variant <code> <id>\")\n .description(\"Returns a specific variant for an existing asset\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/variants/${id}`);\n ctx.output(result);\n });\n\n parent\n .command(\"update-variant <code> <id>\")\n .description(\"Updates tags for a specific variant\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, id, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"PUT \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.put(\n `/api/dam/assets/${code}/variants/${id}`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"delete-variant <code> <id>\")\n .description(\"Destroys a variant for an existing asset\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"DELETE \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.delete(\n `/api/dam/assets/${code}/variants/${id}`,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"discard-variant <asset-code> <id>\")\n .description(\"Discards a variant for an existing asset (soft delete)\")\n .action(async (assetCode, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"PATCH \" +\n `/api/dam/assets/${assetCode}/variants/${id}/discard` +\n \"\\n\",\n );\n const result = await client.patch(\n `/api/dam/assets/${assetCode}/variants/${id}/discard`,\n );\n ctx.output(result);\n });\n}\n","// index.ts — AUTO-GENERATED, DO NOT EDIT\nimport { Command } from \"commander\";\nimport type { CommandContext } from \"@fluid-app/fluid-cli\";\n\nimport { registerPrimary } from \"./primary.js\";\n\nexport function registerAllCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n registerPrimary(parent, ctx);\n}\n","import type { FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { createDomainCommand } from \"@fluid-app/fluid-cli\";\n\nimport { registerAllCommands } from \"./generated/index.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-assets\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(\n createDomainCommand(\n \"assets\",\n \"Manage Fluid digital assets (DAM)\",\n registerAllCommands,\n ),\n );\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/commands/upload.ts","../src/generated/primary.ts","../src/generated/index.ts","../src/index.ts"],"sourcesContent":["/**\n * `fluid assets upload` / `fluid dam upload` — wraps the\n * `upload.fluid.app/upload` Cloud Run service (../file-upload-\n * serverless) so users can drop a file or external URL onto the\n * Fluid DAM in one step instead of stitching together the three\n * underlying API calls (imagekit_auth → ImageKit → backfill_imagekit).\n *\n * Two input modes:\n * - Positional `<file>` path → multipart upload from disk.\n * - `--url <url>` → service-side fetch + upload.\n *\n * Auth comes from the same --token / --profile / env / active-profile\n * resolution path the rest of the CLI uses; we re-implement the\n * resolve here instead of going through `ctx.getClient()` because\n * the DAM upload service lives on a different host than the\n * regular Fluid API and we just need the bearer to forward.\n */\nimport { Command } from \"commander\";\nimport { readFile, stat } from \"node:fs/promises\";\nimport { basename } from \"node:path\";\nimport { findProjectConfig, getAuthToken } from \"@fluid-app/fluid-cli\";\nimport type { CommandContext } from \"@fluid-app/fluid-cli\";\n\n// Hard-coded — the bearer token we forward is the user's Fluid API\n// token, so the destination MUST be a Fluid-controlled host or any\n// `--upload-base evil.example` would exfiltrate it. We previously\n// shipped a `--upload-base` override for local-dev convenience; if\n// you're running the upload service locally, point at it via the\n// FLUID_UPLOAD_BASE env var instead (read at process start, not\n// from an end-user-typed argv string).\nconst UPLOAD_BASE = (\n process.env[\"FLUID_UPLOAD_BASE\"] ?? \"https://upload.fluid.app\"\n).replace(/\\/$/, \"\");\n\ninterface UploadOptions {\n url?: string;\n name?: string;\n description?: string;\n tags?: string;\n folder?: string;\n /**\n * commander maps `--no-unique-filename` onto `uniqueFilename: false`.\n * Default is true so the service keeps its existing behavior unless\n * the user explicitly opts out.\n */\n uniqueFilename: boolean;\n createMedia?: boolean;\n}\n\nexport function registerUpload(parent: Command, ctx: CommandContext): void {\n parent\n .command(\"upload [file]\")\n .description(\"Upload a file or URL to the Fluid DAM (via upload.fluid.app)\")\n .option(\n \"--url <url>\",\n \"Fetch from an external URL instead of attaching a local file\",\n )\n .option(\"--name <name>\", \"Display name for the DAM asset\")\n .option(\"--description <description>\", \"Asset description\")\n .option(\"--tags <tags>\", \"Comma-separated tags\")\n .option(\"--folder <folder>\", \"ImageKit folder override\")\n .option(\n \"--no-unique-filename\",\n \"Disable ImageKit's useUniqueFileName (default: enabled)\",\n )\n .option(\n \"--create-media\",\n \"Also create a Fluid Media resource (active, share, publish-now)\",\n )\n .action(async function (\n this: Command,\n filePath: string | undefined,\n opts: UploadOptions,\n ) {\n // --token / --profile live on the parent `assets` domain\n // command (createDomainCommand sets them there). Reach up to\n // pick them up here so the user can write\n // fluid dam --token X upload ./photo.jpg\n const parentOpts = (this.parent?.opts() ?? {}) as {\n token?: string;\n profile?: string;\n };\n const token = resolveToken(parentOpts);\n if (!token) {\n throw new Error(\n \"No API token found. Run `fluid login` or provide --token <token>.\",\n );\n }\n\n if (!filePath && !opts.url) {\n throw new Error(\n \"Provide a file path or --url <url>. Run `fluid dam upload --help` for usage.\",\n );\n }\n if (filePath && opts.url) {\n throw new Error(\"Provide either a file path OR --url, not both.\");\n }\n\n // Multipart order matters for streaming uploads (>~100 MB).\n // The service streams `file` straight through to ImageKit, so\n // any field ImageKit needs (`fileName`, `folder`,\n // `useUniqueFileName`) must arrive BEFORE the file part.\n // Fields consumed after upload (`name`, `description`, `tags`,\n // `create_media`) can sit anywhere — we put them last for\n // readability.\n const form = new FormData();\n if (opts.folder) form.append(\"folder\", opts.folder);\n if (opts.uniqueFilename === false) {\n form.append(\"useUniqueFileName\", \"false\");\n }\n\n if (opts.url) {\n form.append(\"external_asset_url\", opts.url);\n } else if (filePath) {\n // Validate first — `readFile` errors are friendlier than the\n // service returning a generic 500 for a missing file.\n await stat(filePath);\n const bytes = await readFile(filePath);\n const name = basename(filePath);\n form.append(\"fileName\", name);\n // Wrap as a Blob so the global fetch's multipart serializer\n // emits a proper Content-Disposition with filename. Node 20+\n // global FormData accepts (Blob, filename) pairs.\n form.append(\"file\", new Blob([bytes]), name);\n }\n\n if (opts.name) form.append(\"name\", opts.name);\n if (opts.description) form.append(\"description\", opts.description);\n if (opts.tags) form.append(\"tags\", opts.tags);\n if (opts.createMedia) form.append(\"create_media\", \"true\");\n\n if (ctx.verbose) {\n process.stderr.write(`POST ${UPLOAD_BASE}/upload\\n`);\n }\n\n const resp = await fetch(`${UPLOAD_BASE}/upload`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: form,\n });\n\n if (!resp.ok) {\n let detail = \"\";\n try {\n detail = await resp.text();\n } catch {\n // No body or unreadable — error message stays generic.\n }\n throw new Error(\n `Upload failed (HTTP ${resp.status})${\n detail ? `: ${detail.slice(0, 400)}` : \"\"\n }`,\n );\n }\n\n const body: unknown = await resp.json();\n ctx.output(body);\n });\n}\n\n/**\n * Token resolution mirrors `createCommandContext.resolveToken` from\n * @fluid-app/fluid-cli: explicit flag → --profile lookup → env vars\n * → project-local `.fluidrc` → global active profile. Re-implemented\n * here instead of imported because the helper isn't exported, and\n * the duplication is tiny.\n */\nfunction resolveToken(opts: {\n token?: string;\n profile?: string;\n}): string | null {\n if (opts.token) return opts.token;\n if (opts.profile) return getAuthToken(opts.profile);\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n const projectConfig = findProjectConfig(process.cwd());\n if (projectConfig?.profile) {\n const rcToken = getAuthToken(projectConfig.profile);\n if (rcToken) return rcToken;\n }\n return getAuthToken();\n}\n","// primary.ts — AUTO-GENERATED, DO NOT EDIT\nimport { Command } from \"commander\";\nimport type { CommandContext } from \"@fluid-app/fluid-cli\";\n\nexport function registerPrimary(parent: Command, ctx: CommandContext): void {\n parent\n .command(\"get-asset-paths <code>\")\n .description(\"Lists asset paths for an existing asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/asset_paths` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/asset_paths`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-asset-paths <code>\")\n .description(\"Creates asset paths for an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/${code}/asset_paths` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/${code}/asset_paths`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"delete-asset-path <code> <id>\")\n .description(\"Destroys an asset path\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"DELETE \" + `/api/dam/assets/${code}/asset_paths/${id}` + \"\\n\",\n );\n const result = await client.delete(\n `/api/dam/assets/${code}/asset_paths/${id}`,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"create-backfill-imagekit\")\n .description(\"Creates a DAM asset from ImageKit upload metadata\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/backfill_imagekit` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/backfill_imagekit`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"create-asset-invalid-mixed-payload\")\n .description(\"Errors when multiple asset types are provided\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose) process.stderr.write(\"POST \" + `/api/dam/assets` + \"\\n\");\n const result = await client.post(`/api/dam/assets`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"get-asset <code>\")\n .description(\"Retrieves an asset by code\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"GET \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.get(`/api/dam/assets/${code}`);\n ctx.output(result);\n });\n\n parent\n .command(\"update-asset <code>\")\n .description(\"Updates an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\"PUT \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.put(`/api/dam/assets/${code}`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"delete-asset <code>\")\n .description(\"Deletes an asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"DELETE \" + `/api/dam/assets/${code}` + \"\\n\");\n const result = await client.delete(`/api/dam/assets/${code}`);\n ctx.output(result);\n });\n\n parent\n .command(\"discard-asset <code>\")\n .description(\"Discard an asset (soft delete)\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"PATCH \" + `/api/dam/assets/${code}/discard` + \"\\n\",\n );\n const result = await client.patch(`/api/dam/assets/${code}/discard`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-imagekit-auth\")\n .description(\n \"Creates ImageKit authentication credentials for large file uploads\",\n )\n .action(async () => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\"POST \" + `/api/dam/assets/imagekit_auth` + \"\\n\");\n const result = await client.post(`/api/dam/assets/imagekit_auth`);\n ctx.output(result);\n });\n\n parent\n .command(\"query-assets\")\n .description(\"Query DAM assets using tree paths and tags\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose) process.stderr.write(\"POST \" + `/api/dam/query` + \"\\n\");\n const result = await client.post(`/api/dam/query`, body);\n ctx.output(result);\n });\n\n parent\n .command(\"list-variants <code>\")\n .description(\"Returns all variants for an existing asset\")\n .action(async (code) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/variants` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/variants`);\n ctx.output(result);\n });\n\n parent\n .command(\"create-variants <code>\")\n .description(\"Creates variants for an existing asset\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"POST \" + `/api/dam/assets/${code}/variants` + \"\\n\",\n );\n const result = await client.post(\n `/api/dam/assets/${code}/variants`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"get-variant <code> <id>\")\n .description(\"Returns a specific variant for an existing asset\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"GET \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.get(`/api/dam/assets/${code}/variants/${id}`);\n ctx.output(result);\n });\n\n parent\n .command(\"update-variant <code> <id>\")\n .description(\"Updates tags for a specific variant\")\n .option(\"--body <json>\", \"Request body as JSON string\")\n .option(\"--body-file <path>\", \"Read request body from file\")\n .action(async (code, id, opts) => {\n const client = await ctx.getClient();\n let body: unknown;\n if (opts.bodyFile) {\n const { readFileSync } = await import(\"fs\");\n body = ctx.parseBody(readFileSync(opts.bodyFile, \"utf-8\"));\n } else if (opts.body) {\n body = ctx.parseBody(opts.body);\n }\n if (ctx.verbose)\n process.stderr.write(\n \"PUT \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.put(\n `/api/dam/assets/${code}/variants/${id}`,\n body,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"delete-variant <code> <id>\")\n .description(\"Destroys a variant for an existing asset\")\n .action(async (code, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"DELETE \" + `/api/dam/assets/${code}/variants/${id}` + \"\\n\",\n );\n const result = await client.delete(\n `/api/dam/assets/${code}/variants/${id}`,\n );\n ctx.output(result);\n });\n\n parent\n .command(\"discard-variant <asset-code> <id>\")\n .description(\"Discards a variant for an existing asset (soft delete)\")\n .action(async (assetCode, id) => {\n const client = await ctx.getClient();\n if (ctx.verbose)\n process.stderr.write(\n \"PATCH \" +\n `/api/dam/assets/${assetCode}/variants/${id}/discard` +\n \"\\n\",\n );\n const result = await client.patch(\n `/api/dam/assets/${assetCode}/variants/${id}/discard`,\n );\n ctx.output(result);\n });\n}\n","// index.ts — AUTO-GENERATED, DO NOT EDIT\nimport { Command } from \"commander\";\nimport type { CommandContext } from \"@fluid-app/fluid-cli\";\n\nimport { registerPrimary } from \"./primary.js\";\n\nexport function registerAllCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n registerPrimary(parent, ctx);\n}\n","import type { FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { createDomainCommand } from \"@fluid-app/fluid-cli\";\n\nimport { registerUpload } from \"./commands/upload.js\";\nimport { registerAllCommands } from \"./generated/index.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-assets\",\n version: \"0.1.0\",\n register(ctx) {\n const assets = createDomainCommand(\n \"assets\",\n \"Manage Fluid digital assets (DAM)\",\n (parent, cmdCtx) => {\n registerAllCommands(parent, cmdCtx);\n // Hand-written ergonomic command on top of the generated\n // OpenAPI surface. Wraps upload.fluid.app/upload so users\n // can `fluid dam upload ./photo.jpg` instead of manually\n // coordinating the three-step ImageKit + backfill flow.\n registerUpload(parent, cmdCtx);\n },\n );\n // `dam` is the colloquial nickname for this domain — both\n // `fluid assets <cmd>` and `fluid dam <cmd>` route to the same\n // command tree.\n assets.alias(\"dam\");\n ctx.program.addCommand(assets);\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-assets",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Fluid CLI commands for digital asset management (DAM)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -21,7 +21,7 @@
21
21
  "@fluid-app/fluid-cli": "0.1.12"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^24",
24
+ "@types/node": "24.10.12",
25
25
  "tsdown": "^0.21.0",
26
26
  "tsx": "^4.21.0",
27
27
  "typescript": "^5",
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `fluid assets upload` / `fluid dam upload` — wraps the
3
+ * `upload.fluid.app/upload` Cloud Run service (../file-upload-
4
+ * serverless) so users can drop a file or external URL onto the
5
+ * Fluid DAM in one step instead of stitching together the three
6
+ * underlying API calls (imagekit_auth → ImageKit → backfill_imagekit).
7
+ *
8
+ * Two input modes:
9
+ * - Positional `<file>` path → multipart upload from disk.
10
+ * - `--url <url>` → service-side fetch + upload.
11
+ *
12
+ * Auth comes from the same --token / --profile / env / active-profile
13
+ * resolution path the rest of the CLI uses; we re-implement the
14
+ * resolve here instead of going through `ctx.getClient()` because
15
+ * the DAM upload service lives on a different host than the
16
+ * regular Fluid API and we just need the bearer to forward.
17
+ */
18
+ import { Command } from "commander";
19
+ import { readFile, stat } from "node:fs/promises";
20
+ import { basename } from "node:path";
21
+ import { findProjectConfig, getAuthToken } from "@fluid-app/fluid-cli";
22
+ import type { CommandContext } from "@fluid-app/fluid-cli";
23
+
24
+ // Hard-coded — the bearer token we forward is the user's Fluid API
25
+ // token, so the destination MUST be a Fluid-controlled host or any
26
+ // `--upload-base evil.example` would exfiltrate it. We previously
27
+ // shipped a `--upload-base` override for local-dev convenience; if
28
+ // you're running the upload service locally, point at it via the
29
+ // FLUID_UPLOAD_BASE env var instead (read at process start, not
30
+ // from an end-user-typed argv string).
31
+ const UPLOAD_BASE = (
32
+ process.env["FLUID_UPLOAD_BASE"] ?? "https://upload.fluid.app"
33
+ ).replace(/\/$/, "");
34
+
35
+ interface UploadOptions {
36
+ url?: string;
37
+ name?: string;
38
+ description?: string;
39
+ tags?: string;
40
+ folder?: string;
41
+ /**
42
+ * commander maps `--no-unique-filename` onto `uniqueFilename: false`.
43
+ * Default is true so the service keeps its existing behavior unless
44
+ * the user explicitly opts out.
45
+ */
46
+ uniqueFilename: boolean;
47
+ createMedia?: boolean;
48
+ }
49
+
50
+ export function registerUpload(parent: Command, ctx: CommandContext): void {
51
+ parent
52
+ .command("upload [file]")
53
+ .description("Upload a file or URL to the Fluid DAM (via upload.fluid.app)")
54
+ .option(
55
+ "--url <url>",
56
+ "Fetch from an external URL instead of attaching a local file",
57
+ )
58
+ .option("--name <name>", "Display name for the DAM asset")
59
+ .option("--description <description>", "Asset description")
60
+ .option("--tags <tags>", "Comma-separated tags")
61
+ .option("--folder <folder>", "ImageKit folder override")
62
+ .option(
63
+ "--no-unique-filename",
64
+ "Disable ImageKit's useUniqueFileName (default: enabled)",
65
+ )
66
+ .option(
67
+ "--create-media",
68
+ "Also create a Fluid Media resource (active, share, publish-now)",
69
+ )
70
+ .action(async function (
71
+ this: Command,
72
+ filePath: string | undefined,
73
+ opts: UploadOptions,
74
+ ) {
75
+ // --token / --profile live on the parent `assets` domain
76
+ // command (createDomainCommand sets them there). Reach up to
77
+ // pick them up here so the user can write
78
+ // fluid dam --token X upload ./photo.jpg
79
+ const parentOpts = (this.parent?.opts() ?? {}) as {
80
+ token?: string;
81
+ profile?: string;
82
+ };
83
+ const token = resolveToken(parentOpts);
84
+ if (!token) {
85
+ throw new Error(
86
+ "No API token found. Run `fluid login` or provide --token <token>.",
87
+ );
88
+ }
89
+
90
+ if (!filePath && !opts.url) {
91
+ throw new Error(
92
+ "Provide a file path or --url <url>. Run `fluid dam upload --help` for usage.",
93
+ );
94
+ }
95
+ if (filePath && opts.url) {
96
+ throw new Error("Provide either a file path OR --url, not both.");
97
+ }
98
+
99
+ // Multipart order matters for streaming uploads (>~100 MB).
100
+ // The service streams `file` straight through to ImageKit, so
101
+ // any field ImageKit needs (`fileName`, `folder`,
102
+ // `useUniqueFileName`) must arrive BEFORE the file part.
103
+ // Fields consumed after upload (`name`, `description`, `tags`,
104
+ // `create_media`) can sit anywhere — we put them last for
105
+ // readability.
106
+ const form = new FormData();
107
+ if (opts.folder) form.append("folder", opts.folder);
108
+ if (opts.uniqueFilename === false) {
109
+ form.append("useUniqueFileName", "false");
110
+ }
111
+
112
+ if (opts.url) {
113
+ form.append("external_asset_url", opts.url);
114
+ } else if (filePath) {
115
+ // Validate first — `readFile` errors are friendlier than the
116
+ // service returning a generic 500 for a missing file.
117
+ await stat(filePath);
118
+ const bytes = await readFile(filePath);
119
+ const name = basename(filePath);
120
+ form.append("fileName", name);
121
+ // Wrap as a Blob so the global fetch's multipart serializer
122
+ // emits a proper Content-Disposition with filename. Node 20+
123
+ // global FormData accepts (Blob, filename) pairs.
124
+ form.append("file", new Blob([bytes]), name);
125
+ }
126
+
127
+ if (opts.name) form.append("name", opts.name);
128
+ if (opts.description) form.append("description", opts.description);
129
+ if (opts.tags) form.append("tags", opts.tags);
130
+ if (opts.createMedia) form.append("create_media", "true");
131
+
132
+ if (ctx.verbose) {
133
+ process.stderr.write(`POST ${UPLOAD_BASE}/upload\n`);
134
+ }
135
+
136
+ const resp = await fetch(`${UPLOAD_BASE}/upload`, {
137
+ method: "POST",
138
+ headers: { Authorization: `Bearer ${token}` },
139
+ body: form,
140
+ });
141
+
142
+ if (!resp.ok) {
143
+ let detail = "";
144
+ try {
145
+ detail = await resp.text();
146
+ } catch {
147
+ // No body or unreadable — error message stays generic.
148
+ }
149
+ throw new Error(
150
+ `Upload failed (HTTP ${resp.status})${
151
+ detail ? `: ${detail.slice(0, 400)}` : ""
152
+ }`,
153
+ );
154
+ }
155
+
156
+ const body: unknown = await resp.json();
157
+ ctx.output(body);
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Token resolution mirrors `createCommandContext.resolveToken` from
163
+ * @fluid-app/fluid-cli: explicit flag → --profile lookup → env vars
164
+ * → project-local `.fluidrc` → global active profile. Re-implemented
165
+ * here instead of imported because the helper isn't exported, and
166
+ * the duplication is tiny.
167
+ */
168
+ function resolveToken(opts: {
169
+ token?: string;
170
+ profile?: string;
171
+ }): string | null {
172
+ if (opts.token) return opts.token;
173
+ if (opts.profile) return getAuthToken(opts.profile);
174
+ const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"];
175
+ if (envToken) return envToken;
176
+ const projectConfig = findProjectConfig(process.cwd());
177
+ if (projectConfig?.profile) {
178
+ const rcToken = getAuthToken(projectConfig.profile);
179
+ if (rcToken) return rcToken;
180
+ }
181
+ return getAuthToken();
182
+ }
package/src/index.ts CHANGED
@@ -1,19 +1,30 @@
1
1
  import type { FluidPlugin } from "@fluid-app/fluid-cli";
2
2
  import { createDomainCommand } from "@fluid-app/fluid-cli";
3
3
 
4
+ import { registerUpload } from "./commands/upload.js";
4
5
  import { registerAllCommands } from "./generated/index.js";
5
6
 
6
7
  const plugin: FluidPlugin = {
7
8
  name: "@fluid-app/fluid-cli-assets",
8
9
  version: "0.1.0",
9
10
  register(ctx) {
10
- ctx.program.addCommand(
11
- createDomainCommand(
12
- "assets",
13
- "Manage Fluid digital assets (DAM)",
14
- registerAllCommands,
15
- ),
11
+ const assets = createDomainCommand(
12
+ "assets",
13
+ "Manage Fluid digital assets (DAM)",
14
+ (parent, cmdCtx) => {
15
+ registerAllCommands(parent, cmdCtx);
16
+ // Hand-written ergonomic command on top of the generated
17
+ // OpenAPI surface. Wraps upload.fluid.app/upload so users
18
+ // can `fluid dam upload ./photo.jpg` instead of manually
19
+ // coordinating the three-step ImageKit + backfill flow.
20
+ registerUpload(parent, cmdCtx);
21
+ },
16
22
  );
23
+ // `dam` is the colloquial nickname for this domain — both
24
+ // `fluid assets <cmd>` and `fluid dam <cmd>` route to the same
25
+ // command tree.
26
+ assets.alias("dam");
27
+ ctx.program.addCommand(assets);
17
28
  },
18
29
  };
19
30