@fluid-app/fluid-cli-assets 0.1.628 → 0.1.630

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/dist/index.mjs CHANGED
@@ -1,42 +1,27 @@
1
- import { createDomainCommand, declareInputs, findProjectConfig, getAuthToken } from "@fluid-app/fluid-cli";
1
+ import { createDomainCommand, declareInputs, findProjectConfig, getAuthToken, uploadToDam } from "@fluid-app/fluid-cli";
2
2
  import "commander";
3
3
  import { basename } from "node:path";
4
4
  //#region src/commands/upload.ts
5
- const UPLOAD_BASE = (process.env["FLUID_UPLOAD_BASE"] ?? "https://upload.fluid.app").replace(/\/$/, "");
6
5
  function registerUpload(parent, ctx) {
7
6
  declareInputs(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) {
8
7
  const token = resolveToken(this.parent?.opts() ?? {});
9
8
  if (!token) throw new Error("No API token found. Run `fluid login` or provide --token <token>.");
10
9
  if (!filePath && !opts.url) throw new Error("Provide a file path or --url <url>. Run `fluid dam upload --help` for usage.");
11
10
  if (filePath && opts.url) throw new Error("Provide either a file path OR --url, not both.");
12
- const form = new FormData();
13
- if (opts.folder) form.append("folder", opts.folder);
14
- if (opts.uniqueFilename === false) form.append("useUniqueFileName", "false");
15
- if (opts.url) form.append("external_asset_url", opts.url);
16
- else if (filePath) {
17
- const bytes = ctx.inputs.readFile(filePath);
18
- const name = basename(filePath);
19
- form.append("fileName", name);
20
- form.append("file", new Blob([new Uint8Array(bytes)]), name);
21
- }
22
- if (opts.name) form.append("name", opts.name);
23
- if (opts.description) form.append("description", opts.description);
24
- if (opts.tags) form.append("tags", opts.tags);
25
- if (opts.createMedia) form.append("create_media", "true");
26
- if (ctx.verbose) process.stderr.write(`POST ${UPLOAD_BASE}/upload\n`);
27
- const resp = await fetch(`${UPLOAD_BASE}/upload`, {
28
- method: "POST",
29
- headers: { Authorization: `Bearer ${token}` },
30
- body: form
11
+ const body = await uploadToDam({
12
+ token,
13
+ ...filePath ? { file: {
14
+ bytes: ctx.inputs.readFile(filePath),
15
+ name: basename(filePath)
16
+ } } : { url: opts.url },
17
+ name: opts.name,
18
+ description: opts.description,
19
+ tags: opts.tags,
20
+ folder: opts.folder,
21
+ uniqueFilename: opts.uniqueFilename,
22
+ createMedia: opts.createMedia,
23
+ onRequest: ctx.verbose ? (line) => process.stderr.write(`${line}\n`) : void 0
31
24
  });
32
- if (!resp.ok) {
33
- let detail = "";
34
- try {
35
- detail = await resp.text();
36
- } catch {}
37
- throw new Error(`Upload failed (HTTP ${resp.status})${detail ? `: ${detail.slice(0, 400)}` : ""}`);
38
- }
39
- const body = await resp.json();
40
25
  ctx.output(body);
41
26
  }), (_opts, args) => args[0] ? [{
42
27
  kind: "file",
@@ -12,3 +12,5 @@ export function findProjectConfig(): null {
12
12
 
13
13
  export * from "../../core/src/inputs/index";
14
14
  export { createInputsReader } from "../../core/src/inputs/reader";
15
+ // Dependency-free, so the real request is what the tests drive.
16
+ export * from "../../core/src/dam/upload";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-assets",
3
- "version": "0.1.628",
3
+ "version": "0.1.630",
4
4
  "description": "Fluid CLI commands for digital asset management (DAM)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@fluid-app/fluid-cli": "0.1.652",
20
+ "@fluid-app/fluid-cli": "0.1.654",
21
21
  "commander": "^12.0.0"
22
22
  },
23
23
  "devDependencies": {
@@ -9,6 +9,9 @@
9
9
  * - Positional `<file>` path → multipart upload from disk.
10
10
  * - `--url <url>` → service-side fetch + upload.
11
11
  *
12
+ * The request itself is `uploadToDam` from the core CLI, which
13
+ * `fluid sites media sync` sends its files through too.
14
+ *
12
15
  * Auth comes from the same --token / --profile / env / active-profile
13
16
  * resolution path the rest of the CLI uses; we re-implement the
14
17
  * resolve here instead of going through `ctx.getClient()` because
@@ -21,19 +24,10 @@ import {
21
24
  declareInputs,
22
25
  findProjectConfig,
23
26
  getAuthToken,
27
+ uploadToDam,
24
28
  } from "@fluid-app/fluid-cli";
25
29
  import type { CommandContext } from "@fluid-app/fluid-cli";
26
30
 
27
- // Hard-coded — the bearer token we forward is the user's Fluid API
28
- // token, so the destination MUST be a Fluid-controlled host or any
29
- // `--upload-base evil.example` would exfiltrate it. If you're running the
30
- // upload service locally, point at it via the
31
- // FLUID_UPLOAD_BASE env var instead (read at process start, not
32
- // from an end-user-typed argv string).
33
- const UPLOAD_BASE = (
34
- process.env["FLUID_UPLOAD_BASE"] ?? "https://upload.fluid.app"
35
- ).replace(/\/$/, "");
36
-
37
31
  interface UploadOptions {
38
32
  url?: string;
39
33
  name?: string;
@@ -98,61 +92,27 @@ export function registerUpload(parent: Command, ctx: CommandContext): void {
98
92
  throw new Error("Provide either a file path OR --url, not both.");
99
93
  }
100
94
 
101
- // Multipart order matters for streaming uploads (>~100 MB).
102
- // The service streams `file` straight through to ImageKit, so
103
- // any field ImageKit needs (`fileName`, `folder`,
104
- // `useUniqueFileName`) must arrive BEFORE the file part.
105
- // Fields consumed after upload (`name`, `description`, `tags`,
106
- // `create_media`) can sit anywhere — we put them last for
107
- // readability.
108
- const form = new FormData();
109
- if (opts.folder) form.append("folder", opts.folder);
110
- if (opts.uniqueFilename === false) {
111
- form.append("useUniqueFileName", "false");
112
- }
113
-
114
- if (opts.url) {
115
- form.append("external_asset_url", opts.url);
116
- } else if (filePath) {
117
- const bytes = ctx.inputs.readFile(filePath);
118
- const name = basename(filePath);
119
- form.append("fileName", name);
120
- // Wrap as a Blob so the global fetch's multipart serializer
121
- // emits a proper Content-Disposition with filename. Node 20+
122
- // global FormData accepts (Blob, filename) pairs.
123
- form.append("file", new Blob([new Uint8Array(bytes)]), name);
124
- }
125
-
126
- if (opts.name) form.append("name", opts.name);
127
- if (opts.description) form.append("description", opts.description);
128
- if (opts.tags) form.append("tags", opts.tags);
129
- if (opts.createMedia) form.append("create_media", "true");
130
-
131
- if (ctx.verbose) {
132
- process.stderr.write(`POST ${UPLOAD_BASE}/upload\n`);
133
- }
134
-
135
- const resp = await fetch(`${UPLOAD_BASE}/upload`, {
136
- method: "POST",
137
- headers: { Authorization: `Bearer ${token}` },
138
- body: form,
95
+ const body = await uploadToDam({
96
+ token,
97
+ // Exactly one of the two was given, checked above.
98
+ ...(filePath
99
+ ? {
100
+ file: {
101
+ bytes: ctx.inputs.readFile(filePath),
102
+ name: basename(filePath),
103
+ },
104
+ }
105
+ : { url: opts.url }),
106
+ name: opts.name,
107
+ description: opts.description,
108
+ tags: opts.tags,
109
+ folder: opts.folder,
110
+ uniqueFilename: opts.uniqueFilename,
111
+ createMedia: opts.createMedia,
112
+ onRequest: ctx.verbose
113
+ ? (line) => process.stderr.write(`${line}\n`)
114
+ : undefined,
139
115
  });
140
-
141
- if (!resp.ok) {
142
- let detail = "";
143
- try {
144
- detail = await resp.text();
145
- } catch {
146
- // No body or unreadable — error message stays generic.
147
- }
148
- throw new Error(
149
- `Upload failed (HTTP ${resp.status})${
150
- detail ? `: ${detail.slice(0, 400)}` : ""
151
- }`,
152
- );
153
- }
154
-
155
- const body: unknown = await resp.json();
156
116
  ctx.output(body);
157
117
  });
158
118
  declareInputs(upload, (_opts, args) =>