@wafflebase/cli 0.3.7 → 0.4.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/dist/commands/docs.js +14 -0
- package/dist/config/config.d.ts +4 -1
- package/dist/config/config.js +12 -4
- package/dist/docs/image-fetcher.d.ts +37 -0
- package/dist/docs/image-fetcher.js +38 -0
- package/dist/docs/pdf-export.d.ts +10 -1
- package/dist/docs/pdf-export.js +5 -1
- package/package.json +2 -2
package/dist/commands/docs.js
CHANGED
|
@@ -5,6 +5,7 @@ import { printDryRun } from '../client/dry-run.js';
|
|
|
5
5
|
import { parseContentFormat, runDocsContent } from '../docs/content.js';
|
|
6
6
|
import { exportPdf } from '../docs/pdf-export.js';
|
|
7
7
|
import { exportDocx } from '../docs/docx-export.js';
|
|
8
|
+
import { createImageFetcher } from '../docs/image-fetcher.js';
|
|
8
9
|
import { parsePageRange } from '../docs/page-range.js';
|
|
9
10
|
import { writeBinary } from '../output/binary.js';
|
|
10
11
|
import { runDocsImport } from '../docs/import.js';
|
|
@@ -224,6 +225,15 @@ export function registerDocsCommand(program) {
|
|
|
224
225
|
throw new Error(`HTTP ${res.status}`);
|
|
225
226
|
}
|
|
226
227
|
const fetchedDoc = res.data;
|
|
228
|
+
// Image inlines reference URLs that PdfExporter and DocxExporter
|
|
229
|
+
// can't resolve on their own. The fetcher walks each unique
|
|
230
|
+
// `src`, GETs it (via `globalThis.fetch`), and hands back a
|
|
231
|
+
// Blob. Relative URLs (`/images/<id>`) resolve against the
|
|
232
|
+
// configured server base so the same doc round-trips between
|
|
233
|
+
// the editor (browser-relative) and the CLI.
|
|
234
|
+
const imageFetcher = createImageFetcher({
|
|
235
|
+
serverBase: getConfig(opts).server,
|
|
236
|
+
});
|
|
227
237
|
let bytes;
|
|
228
238
|
if (format === 'pdf') {
|
|
229
239
|
// Pagination uses the FontkitMeasurer's coarse Latin estimate
|
|
@@ -238,6 +248,7 @@ export function registerDocsCommand(program) {
|
|
|
238
248
|
// see `extractPages` in pdf-export.ts.
|
|
239
249
|
const fullPdf = await exportPdf(fetchedDoc, {
|
|
240
250
|
includeHeaderFooter: local.includeHeaderFooter,
|
|
251
|
+
imageFetcher,
|
|
241
252
|
});
|
|
242
253
|
const total = await pdfPageCount(fullPdf);
|
|
243
254
|
pageRange = parsePageRange(local.pages, total);
|
|
@@ -248,11 +259,13 @@ export function registerDocsCommand(program) {
|
|
|
248
259
|
bytes = await exportPdf(fetchedDoc, {
|
|
249
260
|
includeHeaderFooter: local.includeHeaderFooter,
|
|
250
261
|
pages: pageRange,
|
|
262
|
+
imageFetcher,
|
|
251
263
|
});
|
|
252
264
|
}
|
|
253
265
|
else {
|
|
254
266
|
bytes = await exportPdf(fetchedDoc, {
|
|
255
267
|
includeHeaderFooter: local.includeHeaderFooter,
|
|
268
|
+
imageFetcher,
|
|
256
269
|
});
|
|
257
270
|
}
|
|
258
271
|
}
|
|
@@ -262,6 +275,7 @@ export function registerDocsCommand(program) {
|
|
|
262
275
|
}
|
|
263
276
|
bytes = await exportDocx(fetchedDoc, {
|
|
264
277
|
includeHeaderFooter: local.includeHeaderFooter,
|
|
278
|
+
imageFetcher,
|
|
265
279
|
});
|
|
266
280
|
}
|
|
267
281
|
writeBinary(bytes, file, { force: local.force, quiet: opts.quiet });
|
package/dist/config/config.d.ts
CHANGED
|
@@ -23,7 +23,10 @@ export declare function getConfigPath(): string;
|
|
|
23
23
|
*
|
|
24
24
|
* Auth resolution order:
|
|
25
25
|
* 1. Flag/env `--api-key` or `WAFFLEBASE_API_KEY` → API key auth
|
|
26
|
-
* 2. Session `~/.wafflebase/session.json`
|
|
26
|
+
* 2. Session `~/.wafflebase/session.json` with both tokens → JWT auth
|
|
27
|
+
* (the access token may already be past its `expiresAt`; the HTTP
|
|
28
|
+
* client refreshes it on the first 401 and falls back to a clear
|
|
29
|
+
* SESSION_EXPIRED error when the refresh token is also dead)
|
|
27
30
|
* 3. Config profile `api-key` → API key auth
|
|
28
31
|
* 4. None → empty (commands will fail)
|
|
29
32
|
*/
|
package/dist/config/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
|
-
import { loadSession
|
|
5
|
+
import { loadSession } from './session.js';
|
|
6
6
|
export const DEFAULT_SERVER = 'https://api.wafflebase.io';
|
|
7
7
|
/**
|
|
8
8
|
* Migrate config from old path (~/.config/wafflebase/config.yaml) to new path
|
|
@@ -40,7 +40,10 @@ export function getConfigPath() {
|
|
|
40
40
|
*
|
|
41
41
|
* Auth resolution order:
|
|
42
42
|
* 1. Flag/env `--api-key` or `WAFFLEBASE_API_KEY` → API key auth
|
|
43
|
-
* 2. Session `~/.wafflebase/session.json`
|
|
43
|
+
* 2. Session `~/.wafflebase/session.json` with both tokens → JWT auth
|
|
44
|
+
* (the access token may already be past its `expiresAt`; the HTTP
|
|
45
|
+
* client refreshes it on the first 401 and falls back to a clear
|
|
46
|
+
* SESSION_EXPIRED error when the refresh token is also dead)
|
|
44
47
|
* 3. Config profile `api-key` → API key auth
|
|
45
48
|
* 4. None → empty (commands will fail)
|
|
46
49
|
*/
|
|
@@ -62,9 +65,14 @@ export function resolveConfig(flags) {
|
|
|
62
65
|
authMode: 'api-key',
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
|
-
// Step 2: Try session
|
|
68
|
+
// Step 2: Try session — accept it whenever both tokens are present.
|
|
69
|
+
// Gating on `isSessionExpired` here would silently drop the session
|
|
70
|
+
// (and its `activeWorkspace`) the moment the access token expired,
|
|
71
|
+
// turning every command into a `/workspaces//...` 404. Letting the
|
|
72
|
+
// HTTP client see the expired access token + refresh token lets its
|
|
73
|
+
// 401 → refresh → retry path do its job.
|
|
66
74
|
const session = loadSession();
|
|
67
|
-
if (session &&
|
|
75
|
+
if (session && session.accessToken && session.refreshToken) {
|
|
68
76
|
return {
|
|
69
77
|
server: flags.server ??
|
|
70
78
|
process.env.WAFFLEBASE_SERVER ??
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DocxImageFetcher } from '@wafflebase/docs';
|
|
2
|
+
export interface ImageFetcherOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the Wafflebase API server. Used to resolve image
|
|
5
|
+
* inlines whose `src` is a server-relative path (`/images/<id>`).
|
|
6
|
+
* Absolute URLs (`http(s)://...`, `data:`, `blob:`, ...) pass through
|
|
7
|
+
* untouched so docs imported from external sources keep working.
|
|
8
|
+
*/
|
|
9
|
+
serverBase: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional `fetch` override — kept as a seam for tests. Defaults to
|
|
12
|
+
* the global `fetch`. The signature matches the WHATWG fetch so a
|
|
13
|
+
* stub can be a plain async function without pulling DOM types in.
|
|
14
|
+
*/
|
|
15
|
+
fetch?: typeof globalThis.fetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a possibly-relative image URL against the configured server
|
|
19
|
+
* base. Mirrors `resolveImageUrl` in
|
|
20
|
+
* `packages/frontend/src/app/docs/export-utils.ts` so the CLI and
|
|
21
|
+
* browser agree on what counts as "absolute". The shared rule: any URL
|
|
22
|
+
* that starts with a scheme (`http:`, `https:`, `data:`, `blob:`,
|
|
23
|
+
* `file:`, ...) is left alone; everything else gets prefixed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveImageUrl(url: string, serverBase: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build an `ImageFetcher` for the CLI export pipelines. The fetcher
|
|
28
|
+
* downloads each unique image inline `src` once, returning a Blob the
|
|
29
|
+
* exporter can embed verbatim (PDF) or stream into the DOCX zip.
|
|
30
|
+
*
|
|
31
|
+
* Backend's `GET /images/:id` is publicly readable, so we don't attach
|
|
32
|
+
* an Authorization header — sending JWT cookies via the CLI isn't
|
|
33
|
+
* possible anyway. Relative URLs resolve against `serverBase`; absolute
|
|
34
|
+
* URLs (e.g., the canonical `https://api.wafflebase.io/images/...`
|
|
35
|
+
* surfaced by `imageFetcher required` errors) pass through.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createImageFetcher(opts: ImageFetcherOptions): DocxImageFetcher;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a possibly-relative image URL against the configured server
|
|
3
|
+
* base. Mirrors `resolveImageUrl` in
|
|
4
|
+
* `packages/frontend/src/app/docs/export-utils.ts` so the CLI and
|
|
5
|
+
* browser agree on what counts as "absolute". The shared rule: any URL
|
|
6
|
+
* that starts with a scheme (`http:`, `https:`, `data:`, `blob:`,
|
|
7
|
+
* `file:`, ...) is left alone; everything else gets prefixed.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveImageUrl(url, serverBase) {
|
|
10
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
11
|
+
return url;
|
|
12
|
+
const base = serverBase.replace(/\/$/, '');
|
|
13
|
+
if (!base)
|
|
14
|
+
return url;
|
|
15
|
+
return `${base}${url.startsWith('/') ? url : `/${url}`}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build an `ImageFetcher` for the CLI export pipelines. The fetcher
|
|
19
|
+
* downloads each unique image inline `src` once, returning a Blob the
|
|
20
|
+
* exporter can embed verbatim (PDF) or stream into the DOCX zip.
|
|
21
|
+
*
|
|
22
|
+
* Backend's `GET /images/:id` is publicly readable, so we don't attach
|
|
23
|
+
* an Authorization header — sending JWT cookies via the CLI isn't
|
|
24
|
+
* possible anyway. Relative URLs resolve against `serverBase`; absolute
|
|
25
|
+
* URLs (e.g., the canonical `https://api.wafflebase.io/images/...`
|
|
26
|
+
* surfaced by `imageFetcher required` errors) pass through.
|
|
27
|
+
*/
|
|
28
|
+
export function createImageFetcher(opts) {
|
|
29
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
30
|
+
return async (url) => {
|
|
31
|
+
const resolved = resolveImageUrl(url, opts.serverBase);
|
|
32
|
+
const res = await fetchImpl(resolved);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`Image fetch failed: ${res.status} ${res.statusText} for ${resolved}`);
|
|
35
|
+
}
|
|
36
|
+
return res.blob();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Document, type PdfFontsOptions } from '@wafflebase/docs';
|
|
1
|
+
import { type Document, type DocxImageFetcher, type PdfFontsOptions } from '@wafflebase/docs';
|
|
2
2
|
import type { PageRange } from './page-range.js';
|
|
3
3
|
/**
|
|
4
4
|
* Options for the CLI PDF export pipeline.
|
|
@@ -21,6 +21,15 @@ export interface CliPdfExportOptions {
|
|
|
21
21
|
* and to leave room for a future "strip header/footer" mode. */
|
|
22
22
|
includeHeaderFooter?: boolean;
|
|
23
23
|
fontSources?: PdfFontsOptions['sources'];
|
|
24
|
+
/** Resolve and download image inlines so they can be embedded in the
|
|
25
|
+
* PDF. Required when the document contains image inlines —
|
|
26
|
+
* `PdfExporter` throws otherwise to avoid silently dropping content.
|
|
27
|
+
* The CLI command builds one via `createImageFetcher` from the
|
|
28
|
+
* configured server base; tests inject a stub that returns
|
|
29
|
+
* in-memory bytes. Typed as `DocxImageFetcher` (a structural
|
|
30
|
+
* `(url) => Promise<Blob>` alias) since the bare `ImageFetcher`
|
|
31
|
+
* symbol from `pdf-image-painter` isn't re-exported. */
|
|
32
|
+
imageFetcher?: DocxImageFetcher;
|
|
24
33
|
}
|
|
25
34
|
/**
|
|
26
35
|
* Render `doc` to a PDF byte buffer using the same `PdfExporter` the
|
package/dist/docs/pdf-export.js
CHANGED
|
@@ -19,7 +19,11 @@ export async function exportPdf(doc, opts = {}) {
|
|
|
19
19
|
const fonts = new PdfFonts({ sources: opts.fontSources });
|
|
20
20
|
const measurer = new FontkitMeasurer();
|
|
21
21
|
await preloadKoreanFonts(fonts, measurer, usage);
|
|
22
|
-
const blob = await PdfExporter.export(doc, {
|
|
22
|
+
const blob = await PdfExporter.export(doc, {
|
|
23
|
+
measurer,
|
|
24
|
+
fonts,
|
|
25
|
+
imageFetcher: opts.imageFetcher,
|
|
26
|
+
});
|
|
23
27
|
const fullBytes = new Uint8Array(await blob.arrayBuffer());
|
|
24
28
|
if (!opts.pages || opts.pages.pages.size === 0) {
|
|
25
29
|
return fullBytes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafflebase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for Wafflebase spreadsheet API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"open": "^11.0.0",
|
|
21
21
|
"pdf-lib": "^1.17.1",
|
|
22
22
|
"yaml": "^2.0.0",
|
|
23
|
-
"@wafflebase/docs": "0.
|
|
23
|
+
"@wafflebase/docs": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/fontkit": "^2.0.0",
|