gutterpress 0.10.4 → 0.10.5-beta.1
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/README.md +7 -3
- package/dist/api/index.d.ts +7 -3
- package/dist/api/index.js +24 -2
- package/dist/{audit-6gsbjcyp.js → audit-9pf96y1w.js} +4 -4
- package/dist/{build-8gx7gkgw.js → build-jw9kynts.js} +4 -4
- package/dist/{cli-7hay1r0h.js → cli-0t4zfevc.js} +1 -1
- package/dist/{cli-zddg7r27.js → cli-5w9y6r2f.js} +928 -74
- package/dist/{cli-h617210r.js → cli-nb902265.js} +1 -1
- package/dist/{cli-d613m9n5.js → cli-xewqry0j.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-mrqpw7g3.js → doctor-6vmxh7mz.js} +2 -2
- package/dist/{engine-dzrzyyew.js → engine-fwe8djyn.js} +1 -1
- package/dist/{engine-pta3yeyt.js → engine-mtyjc4v6.js} +2 -2
- package/dist/{index-1z8a090c.js → index-3xv7vwv2.js} +972 -120
- package/dist/{index-cm4dywtk.js → index-ja0p4w5f.js} +1 -1
- package/dist/{index-p4yq14qw.js → index-k5hcp8wj.js} +1 -1
- package/dist/index.js +25 -3
- package/dist/lib/markdown/gutterpress-css.d.ts +10 -2
- package/dist/lib/open-path.d.ts +27 -0
- package/dist/lib/publish/connect-google.d.ts +34 -0
- package/dist/lib/publish/connect.d.ts +23 -0
- package/dist/lib/publish/google-auth.d.ts +104 -0
- package/dist/lib/publish/google-drive.d.ts +88 -0
- package/dist/lib/publish/google-errors.d.ts +49 -0
- package/dist/lib/publish/providers/gdrive.d.ts +25 -0
- package/dist/lib/publish/run-publish.d.ts +14 -1
- package/dist/lib/publish/types.d.ts +46 -2
- package/dist/lib/remote-auth/token-store.d.ts +1 -1
- package/dist/{lint-gmymvnm3.js → lint-kzvbrn3d.js} +4 -4
- package/dist/{manifest.schema-084rxtwp.json → manifest.schema-hn6ac0ae.json} +25 -0
- package/dist/{new-hw7s7jne.js → new-cyna8tyn.js} +4 -4
- package/dist/{plugin-0qtfgjr2.js → plugin-mpc5v8sr.js} +4 -4
- package/dist/{preflight-j99dpj5e.js → preflight-fc178as2.js} +4 -4
- package/dist/{preview-sc3rcrkh.js → preview-qnbbtyqb.js} +4 -4
- package/dist/{publish-xwrwvrh0.js → publish-gmgpyh4n.js} +58 -9
- package/dist/schema/manifest.types.d.ts +20 -0
- package/dist/{validate-evzfg9m6.js → validate-0t3vjpmk.js} +4 -4
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
resolveChromiumExecutable,
|
|
18
18
|
run,
|
|
19
19
|
spawnCapture
|
|
20
|
-
} from "./index-
|
|
20
|
+
} from "./index-k5hcp8wj.js";
|
|
21
21
|
import {
|
|
22
22
|
gitFs,
|
|
23
23
|
gitScopeFor,
|
|
@@ -3978,7 +3978,7 @@ import git from "isomorphic-git";
|
|
|
3978
3978
|
// package.json
|
|
3979
3979
|
var package_default = {
|
|
3980
3980
|
name: "gutterpress",
|
|
3981
|
-
version: "0.10.
|
|
3981
|
+
version: "0.10.5-beta.1",
|
|
3982
3982
|
description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
|
|
3983
3983
|
author: "itlackey",
|
|
3984
3984
|
license: "MPL-2.0",
|
|
@@ -8032,7 +8032,7 @@ class PdfOutput {
|
|
|
8032
8032
|
const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
|
|
8033
8033
|
await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
|
|
8034
8034
|
log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
|
|
8035
|
-
const { buildNativePdf } = await import("./engine-
|
|
8035
|
+
const { buildNativePdf } = await import("./engine-mtyjc4v6.js");
|
|
8036
8036
|
const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
|
|
8037
8037
|
title: config.title,
|
|
8038
8038
|
author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
|
|
@@ -8747,24 +8747,24 @@ import { WebSocket, WebSocketServer } from "ws";
|
|
|
8747
8747
|
|
|
8748
8748
|
// src/lib/open-path.ts
|
|
8749
8749
|
import { spawn } from "node:child_process";
|
|
8750
|
+
function buildOpenPathSpawnSpec(filePath, platform = process.platform) {
|
|
8751
|
+
switch (platform) {
|
|
8752
|
+
case "darwin":
|
|
8753
|
+
return { cmd: "open", args: [filePath], options: { detached: true, stdio: "ignore" } };
|
|
8754
|
+
case "win32":
|
|
8755
|
+
return {
|
|
8756
|
+
cmd: "cmd",
|
|
8757
|
+
args: ["/c", "start", '""', '"' + filePath + '"'],
|
|
8758
|
+
options: { detached: true, stdio: "ignore", windowsVerbatimArguments: true }
|
|
8759
|
+
};
|
|
8760
|
+
default:
|
|
8761
|
+
return { cmd: "xdg-open", args: [filePath], options: { detached: true, stdio: "ignore" } };
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8750
8764
|
function openPath(filePath) {
|
|
8751
8765
|
return new Promise((resolve9, reject) => {
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
switch (process.platform) {
|
|
8755
|
-
case "darwin":
|
|
8756
|
-
cmd = "open";
|
|
8757
|
-
args = [filePath];
|
|
8758
|
-
break;
|
|
8759
|
-
case "win32":
|
|
8760
|
-
cmd = "cmd";
|
|
8761
|
-
args = ["/c", "start", "", filePath];
|
|
8762
|
-
break;
|
|
8763
|
-
default:
|
|
8764
|
-
cmd = "xdg-open";
|
|
8765
|
-
args = [filePath];
|
|
8766
|
-
}
|
|
8767
|
-
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
8766
|
+
const { cmd, args, options } = buildOpenPathSpawnSpec(filePath);
|
|
8767
|
+
const child = spawn(cmd, args, options);
|
|
8768
8768
|
child.once("error", reject);
|
|
8769
8769
|
child.once("spawn", () => {
|
|
8770
8770
|
child.unref();
|
|
@@ -12225,16 +12225,371 @@ function githubApiHeaders(token) {
|
|
|
12225
12225
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
12226
12226
|
};
|
|
12227
12227
|
}
|
|
12228
|
+
// src/lib/publish/google-auth.ts
|
|
12229
|
+
import crypto from "node:crypto";
|
|
12230
|
+
import http2 from "node:http";
|
|
12231
|
+
|
|
12232
|
+
// src/lib/publish/google-errors.ts
|
|
12233
|
+
function redactQueryCredentials(text) {
|
|
12234
|
+
return text.replace(/([?&](?:access_token|key)=)[^&\s"']+/gi, "$1(redacted)");
|
|
12235
|
+
}
|
|
12236
|
+
function parseGoogleApiError(status, body) {
|
|
12237
|
+
const err = body?.error;
|
|
12238
|
+
if (!err || typeof err !== "object")
|
|
12239
|
+
return { status };
|
|
12240
|
+
const e = err;
|
|
12241
|
+
const first = Array.isArray(e.errors) ? e.errors[0] : undefined;
|
|
12242
|
+
const reason = typeof first?.reason === "string" && first.reason ? first.reason : typeof e.status === "string" && e.status ? e.status : undefined;
|
|
12243
|
+
const message = typeof e.message === "string" && e.message.trim() ? redactQueryCredentials(e.message.trim()) : undefined;
|
|
12244
|
+
return { status, ...reason ? { reason } : {}, ...message ? { message } : {} };
|
|
12245
|
+
}
|
|
12246
|
+
async function readGoogleApiError(res) {
|
|
12247
|
+
const body = await res.json().catch(() => {
|
|
12248
|
+
return;
|
|
12249
|
+
});
|
|
12250
|
+
return parseGoogleApiError(res.status, body);
|
|
12251
|
+
}
|
|
12252
|
+
var RATE_LIMIT_REASONS = /^(dailyLimitExceeded|userRateLimitExceeded|rateLimitExceeded|quotaExceeded)$/i;
|
|
12253
|
+
var PERMISSION_REASONS = /^(insufficientPermissions|insufficientFilePermissions|forbidden|PERMISSION_DENIED)$/i;
|
|
12254
|
+
function hintFor(info2) {
|
|
12255
|
+
const reason = info2.reason ?? "";
|
|
12256
|
+
if (/^accessNotConfigured$/i.test(reason)) {
|
|
12257
|
+
return "Google Drive publishing isn't fully set up on this build: the Google Drive API isn't enabled for the app's Google Cloud project, so a maintainer needs to enable it (the link is in Google's message).";
|
|
12258
|
+
}
|
|
12259
|
+
if (/^storageQuotaExceeded$/i.test(reason)) {
|
|
12260
|
+
return "Your Google Drive is full — free up space (or choose a different folder) and try again.";
|
|
12261
|
+
}
|
|
12262
|
+
if (RATE_LIMIT_REASONS.test(reason)) {
|
|
12263
|
+
return "Google Drive is rate-limiting requests right now. Wait a minute and try again.";
|
|
12264
|
+
}
|
|
12265
|
+
if (PERMISSION_REASONS.test(reason) || info2.status === 401) {
|
|
12266
|
+
return "This Google Drive connection doesn't have permission for that — disconnect and connect Google Drive again to re-approve access.";
|
|
12267
|
+
}
|
|
12268
|
+
return;
|
|
12269
|
+
}
|
|
12270
|
+
function googleApiFailure(what, info2) {
|
|
12271
|
+
const parts = [`${what} (HTTP ${info2.status}${info2.reason ? `, ${info2.reason}` : ""}).`];
|
|
12272
|
+
const hint = hintFor(info2);
|
|
12273
|
+
if (hint)
|
|
12274
|
+
parts.push(hint);
|
|
12275
|
+
if (info2.message)
|
|
12276
|
+
parts.push(`Google said: "${info2.message}"`);
|
|
12277
|
+
let text = parts.join(" ");
|
|
12278
|
+
if (!/\bgoogle\b/i.test(text))
|
|
12279
|
+
text = `Google Drive: ${text}`;
|
|
12280
|
+
return new FriendlyHttpError(text);
|
|
12281
|
+
}
|
|
12282
|
+
|
|
12283
|
+
// src/lib/publish/google-auth.ts
|
|
12284
|
+
var GDRIVE_HOST = "gdrive";
|
|
12285
|
+
var GOOGLE_OAUTH_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
12286
|
+
var AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
12287
|
+
var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
12288
|
+
var GOOGLE_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
|
|
12289
|
+
var DRIVE_ABOUT_URL = "https://www.googleapis.com/drive/v3/about?fields=user(emailAddress)";
|
|
12290
|
+
var REQUEST_TIMEOUT_MS2 = 15000;
|
|
12291
|
+
var DEFAULT_FLOW_TIMEOUT_MS = 5 * 60000;
|
|
12292
|
+
var OFFLINE_MESSAGE2 = "Couldn't reach Google. Check your connection and try again.";
|
|
12293
|
+
var DEFAULT_GOOGLE_CLIENT_ID = "621278203862-2t35qditgvfh0pgosguqu2encf9c2h9j.apps.googleusercontent.com";
|
|
12294
|
+
var DEFAULT_GOOGLE_CLIENT_SECRET = "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";
|
|
12295
|
+
function resolveClientValue(explicit, env, embedded) {
|
|
12296
|
+
if (explicit !== undefined)
|
|
12297
|
+
return explicit.trim();
|
|
12298
|
+
if (env !== undefined)
|
|
12299
|
+
return env.trim();
|
|
12300
|
+
return embedded;
|
|
12301
|
+
}
|
|
12302
|
+
function resolveGoogleClientId(explicit) {
|
|
12303
|
+
return resolveClientValue(explicit, process.env.GUTTERPRESS_GOOGLE_CLIENT_ID, DEFAULT_GOOGLE_CLIENT_ID);
|
|
12304
|
+
}
|
|
12305
|
+
function resolveGoogleClientSecret(explicit) {
|
|
12306
|
+
return resolveClientValue(explicit, process.env.GUTTERPRESS_GOOGLE_CLIENT_SECRET, DEFAULT_GOOGLE_CLIENT_SECRET);
|
|
12307
|
+
}
|
|
12308
|
+
var GOOGLE_NOT_CONFIGURED_MESSAGE = "Google Drive publishing isn't configured on this build yet. Set GUTTERPRESS_GOOGLE_CLIENT_ID and GUTTERPRESS_GOOGLE_CLIENT_SECRET to enable it.";
|
|
12309
|
+
function requireGoogleClientCredentials(clientIdOpt, clientSecretOpt) {
|
|
12310
|
+
const clientId = resolveGoogleClientId(clientIdOpt);
|
|
12311
|
+
const clientSecret = resolveGoogleClientSecret(clientSecretOpt);
|
|
12312
|
+
if (!clientId || !clientSecret) {
|
|
12313
|
+
throw new Error(GOOGLE_NOT_CONFIGURED_MESSAGE);
|
|
12314
|
+
}
|
|
12315
|
+
return { clientId, clientSecret };
|
|
12316
|
+
}
|
|
12317
|
+
var RECONNECT_MESSAGE = "Your Google Drive connection expired or was revoked. Connect Google Drive again.";
|
|
12318
|
+
var DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
12319
|
+
var DRIVE_PERMISSION_NOT_GRANTED_MESSAGE = "Google sign-in finished, but it didn't include the Google Drive permission, so Gutterpress can't create or see any files. Connect Google Drive again and allow it.";
|
|
12320
|
+
function grantedScopesInclude(scope, wanted) {
|
|
12321
|
+
if (scope === undefined)
|
|
12322
|
+
return true;
|
|
12323
|
+
return scope.split(/\s+/).includes(wanted);
|
|
12324
|
+
}
|
|
12325
|
+
function b64url(buf) {
|
|
12326
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
12327
|
+
}
|
|
12328
|
+
function pkceChallengeFromVerifier(verifier) {
|
|
12329
|
+
return b64url(crypto.createHash("sha256").update(verifier).digest());
|
|
12330
|
+
}
|
|
12331
|
+
async function safeFetch2(fetchImpl, url, init, timeoutMs = REQUEST_TIMEOUT_MS2) {
|
|
12332
|
+
return withFetchTimeout({ timeoutMs, signal: init.signal ?? undefined, offlineMessage: OFFLINE_MESSAGE2 }, (signal) => fetchImpl(url, { ...init, signal }));
|
|
12333
|
+
}
|
|
12334
|
+
function friendlyTokenError(status, body) {
|
|
12335
|
+
const code = body?.error;
|
|
12336
|
+
if (code === "invalid_grant")
|
|
12337
|
+
return new Error(RECONNECT_MESSAGE);
|
|
12338
|
+
if (code === "invalid_client") {
|
|
12339
|
+
return new Error("Google rejected the app's sign-in credentials. This build's Google OAuth client id/secret may be misconfigured.");
|
|
12340
|
+
}
|
|
12341
|
+
return new FriendlyHttpError(`Google sign-in failed (HTTP ${status}${code ? `: ${code}` : ""}). Please try again.`);
|
|
12342
|
+
}
|
|
12343
|
+
var SUCCESS_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>You're connected</h2><p>Return to Gutterpress — you can close this tab.</p></body></html>`;
|
|
12344
|
+
var FAILURE_HTML = `<html><body style="font:16px system-ui;padding:3rem"><h2>Sign-in didn't complete</h2><p>Return to Gutterpress and try again.</p></body></html>`;
|
|
12345
|
+
function waitForCallback(server, expectedState, redirectUri, timeoutMs, signal) {
|
|
12346
|
+
return new Promise((resolve9, reject) => {
|
|
12347
|
+
let settled = false;
|
|
12348
|
+
const cleanup = () => {
|
|
12349
|
+
clearTimeout(timer);
|
|
12350
|
+
server.removeListener("request", onRequest);
|
|
12351
|
+
signal?.removeEventListener("abort", onAbort);
|
|
12352
|
+
server.close();
|
|
12353
|
+
server.closeIdleConnections();
|
|
12354
|
+
};
|
|
12355
|
+
const finishResolve = (value) => {
|
|
12356
|
+
if (settled)
|
|
12357
|
+
return;
|
|
12358
|
+
settled = true;
|
|
12359
|
+
cleanup();
|
|
12360
|
+
resolve9(value);
|
|
12361
|
+
};
|
|
12362
|
+
const finishReject = (err) => {
|
|
12363
|
+
if (settled)
|
|
12364
|
+
return;
|
|
12365
|
+
settled = true;
|
|
12366
|
+
cleanup();
|
|
12367
|
+
reject(err);
|
|
12368
|
+
};
|
|
12369
|
+
const onRequest = (req, res) => {
|
|
12370
|
+
let url;
|
|
12371
|
+
try {
|
|
12372
|
+
url = new URL(req.url ?? "/", redirectUri);
|
|
12373
|
+
} catch {
|
|
12374
|
+
res.writeHead(400).end();
|
|
12375
|
+
return;
|
|
12376
|
+
}
|
|
12377
|
+
const err = url.searchParams.get("error");
|
|
12378
|
+
const gotState = url.searchParams.get("state");
|
|
12379
|
+
const code = url.searchParams.get("code");
|
|
12380
|
+
if (!err && !code) {
|
|
12381
|
+
res.writeHead(204, { Connection: "close" }).end();
|
|
12382
|
+
return;
|
|
12383
|
+
}
|
|
12384
|
+
const fail = (message) => {
|
|
12385
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" }).end(FAILURE_HTML);
|
|
12386
|
+
finishReject(new Error(message));
|
|
12387
|
+
};
|
|
12388
|
+
if (err) {
|
|
12389
|
+
fail("Google sign-in was declined. You can connect Google Drive again whenever you're ready.");
|
|
12390
|
+
return;
|
|
12391
|
+
}
|
|
12392
|
+
if (gotState !== expectedState) {
|
|
12393
|
+
fail("Google sign-in failed a security check (state mismatch). Connect Google Drive again.");
|
|
12394
|
+
return;
|
|
12395
|
+
}
|
|
12396
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" }).end(SUCCESS_HTML);
|
|
12397
|
+
finishResolve({ code });
|
|
12398
|
+
};
|
|
12399
|
+
const onAbort = () => finishReject(new Error("Google sign-in was canceled."));
|
|
12400
|
+
const timer = setTimeout(() => finishReject(new Error("Google sign-in timed out waiting for the browser. Try again, or use GDRIVE_REFRESH_TOKEN for headless/CI use.")), timeoutMs);
|
|
12401
|
+
server.on("request", onRequest);
|
|
12402
|
+
if (signal) {
|
|
12403
|
+
if (signal.aborted)
|
|
12404
|
+
return onAbort();
|
|
12405
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12406
|
+
}
|
|
12407
|
+
});
|
|
12408
|
+
}
|
|
12409
|
+
|
|
12410
|
+
class GoogleAuthProvider {
|
|
12411
|
+
clientIdOpt;
|
|
12412
|
+
clientSecretOpt;
|
|
12413
|
+
fetchImpl;
|
|
12414
|
+
openBrowser;
|
|
12415
|
+
timeoutMs;
|
|
12416
|
+
bindPort;
|
|
12417
|
+
constructor(options = {}) {
|
|
12418
|
+
this.clientIdOpt = options.clientId;
|
|
12419
|
+
this.clientSecretOpt = options.clientSecret;
|
|
12420
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
12421
|
+
this.openBrowser = options.openBrowser ?? openPath;
|
|
12422
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
12423
|
+
this.bindPort = options.port ?? 0;
|
|
12424
|
+
}
|
|
12425
|
+
async connect(callbacks) {
|
|
12426
|
+
const { clientId, clientSecret } = requireGoogleClientCredentials(this.clientIdOpt, this.clientSecretOpt);
|
|
12427
|
+
const { signal } = callbacks;
|
|
12428
|
+
const verifier = b64url(crypto.randomBytes(32));
|
|
12429
|
+
const state = b64url(crypto.randomBytes(16));
|
|
12430
|
+
const challenge = pkceChallengeFromVerifier(verifier);
|
|
12431
|
+
const server = http2.createServer();
|
|
12432
|
+
await new Promise((resolve9, reject) => {
|
|
12433
|
+
const onListenError = (err) => {
|
|
12434
|
+
reject(new Error(`Couldn't start the local sign-in listener: ${err.message}`));
|
|
12435
|
+
};
|
|
12436
|
+
server.once("error", onListenError);
|
|
12437
|
+
server.listen(this.bindPort, "127.0.0.1", () => {
|
|
12438
|
+
server.removeListener("error", onListenError);
|
|
12439
|
+
resolve9();
|
|
12440
|
+
});
|
|
12441
|
+
});
|
|
12442
|
+
const address = server.address();
|
|
12443
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
12444
|
+
const redirectUri = `http://127.0.0.1:${port}`;
|
|
12445
|
+
const authUrl = `${AUTHORIZATION_ENDPOINT}?` + new URLSearchParams({
|
|
12446
|
+
client_id: clientId,
|
|
12447
|
+
redirect_uri: redirectUri,
|
|
12448
|
+
response_type: "code",
|
|
12449
|
+
scope: GOOGLE_OAUTH_SCOPE,
|
|
12450
|
+
code_challenge: challenge,
|
|
12451
|
+
code_challenge_method: "S256",
|
|
12452
|
+
state,
|
|
12453
|
+
access_type: "offline",
|
|
12454
|
+
prompt: "consent"
|
|
12455
|
+
}).toString();
|
|
12456
|
+
callbacks.onAuthUrl(authUrl);
|
|
12457
|
+
this.openBrowser(authUrl).catch(() => {});
|
|
12458
|
+
const { code } = await waitForCallback(server, state, redirectUri, this.timeoutMs, signal);
|
|
12459
|
+
const tokenRes = await safeFetch2(this.fetchImpl, TOKEN_ENDPOINT, {
|
|
12460
|
+
method: "POST",
|
|
12461
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
12462
|
+
body: new URLSearchParams({
|
|
12463
|
+
grant_type: "authorization_code",
|
|
12464
|
+
code,
|
|
12465
|
+
client_id: clientId,
|
|
12466
|
+
client_secret: clientSecret,
|
|
12467
|
+
redirect_uri: redirectUri,
|
|
12468
|
+
code_verifier: verifier
|
|
12469
|
+
}).toString(),
|
|
12470
|
+
signal
|
|
12471
|
+
});
|
|
12472
|
+
const tokenBody = await tokenRes.json().catch(() => ({}));
|
|
12473
|
+
if (!tokenRes.ok || !tokenBody.access_token) {
|
|
12474
|
+
throw friendlyTokenError(tokenRes.status, tokenBody);
|
|
12475
|
+
}
|
|
12476
|
+
if (!tokenBody.refresh_token) {
|
|
12477
|
+
throw new Error("Google didn't return a refresh token. Try connecting again — if this keeps happening, revoke Gutterpress's access at myaccount.google.com/permissions and reconnect.");
|
|
12478
|
+
}
|
|
12479
|
+
if (!grantedScopesInclude(tokenBody.scope, DRIVE_FILE_SCOPE)) {
|
|
12480
|
+
throw new Error(DRIVE_PERMISSION_NOT_GRANTED_MESSAGE);
|
|
12481
|
+
}
|
|
12482
|
+
const email = await this.fetchEmail(tokenBody.access_token);
|
|
12483
|
+
return {
|
|
12484
|
+
host: GDRIVE_HOST,
|
|
12485
|
+
kind: "google-oauth",
|
|
12486
|
+
token: tokenBody.refresh_token,
|
|
12487
|
+
...email ? { username: email, label: `Google Drive — ${email}` } : { label: "Google Drive" },
|
|
12488
|
+
createdAt: Date.now()
|
|
12489
|
+
};
|
|
12490
|
+
}
|
|
12491
|
+
async fetchEmail(accessToken) {
|
|
12492
|
+
let res;
|
|
12493
|
+
try {
|
|
12494
|
+
res = await safeFetch2(this.fetchImpl, DRIVE_ABOUT_URL, {
|
|
12495
|
+
method: "GET",
|
|
12496
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
12497
|
+
});
|
|
12498
|
+
} catch {
|
|
12499
|
+
return;
|
|
12500
|
+
}
|
|
12501
|
+
if (res.status === 401 || res.status === 403) {
|
|
12502
|
+
throw googleApiFailure("Google Drive rejected the new connection", await readGoogleApiError(res));
|
|
12503
|
+
}
|
|
12504
|
+
if (!res.ok)
|
|
12505
|
+
return;
|
|
12506
|
+
try {
|
|
12507
|
+
const body = await res.json();
|
|
12508
|
+
return body.user?.emailAddress;
|
|
12509
|
+
} catch {
|
|
12510
|
+
return;
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12514
|
+
async function revokeGoogleCredential(refreshToken, options = {}) {
|
|
12515
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
12516
|
+
try {
|
|
12517
|
+
await withFetchTimeout({ timeoutMs: 1e4, offlineMessage: OFFLINE_MESSAGE2 }, (signal) => fetchImpl(GOOGLE_REVOKE_ENDPOINT, {
|
|
12518
|
+
method: "POST",
|
|
12519
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
12520
|
+
body: new URLSearchParams({ token: refreshToken }).toString(),
|
|
12521
|
+
signal
|
|
12522
|
+
}));
|
|
12523
|
+
} catch {}
|
|
12524
|
+
}
|
|
12525
|
+
// src/lib/publish/types.ts
|
|
12526
|
+
function publishCredentialKey(host, account) {
|
|
12527
|
+
const a = (account ?? "").trim();
|
|
12528
|
+
return a ? `${host}#${a}` : host;
|
|
12529
|
+
}
|
|
12530
|
+
async function resolvePublishCredential(info2, deps, account = deps.credentialAccount) {
|
|
12531
|
+
const env = deps.env ?? process.env;
|
|
12532
|
+
const fromEnv = info2.credential.envVar ? env[info2.credential.envVar]?.trim() : undefined;
|
|
12533
|
+
if (fromEnv) {
|
|
12534
|
+
return {
|
|
12535
|
+
credential: {
|
|
12536
|
+
host: info2.credential.host,
|
|
12537
|
+
kind: "token",
|
|
12538
|
+
token: fromEnv,
|
|
12539
|
+
createdAt: 0
|
|
12540
|
+
},
|
|
12541
|
+
source: "env"
|
|
12542
|
+
};
|
|
12543
|
+
}
|
|
12544
|
+
const stored = await deps.tokenStore.get(publishCredentialKey(info2.credential.host, account));
|
|
12545
|
+
return stored ? { credential: stored, source: "store" } : null;
|
|
12546
|
+
}
|
|
12547
|
+
async function publishConnectionStatus(info2, deps, account = deps.credentialAccount) {
|
|
12548
|
+
if (!info2.credential.required)
|
|
12549
|
+
return { connected: true };
|
|
12550
|
+
const resolved = await resolvePublishCredential(info2, deps, account);
|
|
12551
|
+
return resolved ? { connected: true, source: resolved.source } : { connected: false };
|
|
12552
|
+
}
|
|
12553
|
+
async function listPublishAccounts(info2, deps) {
|
|
12554
|
+
const wantHost = info2.credential.host.trim().toLowerCase();
|
|
12555
|
+
const all = await deps.tokenStore.list();
|
|
12556
|
+
return all.filter((c) => c.host.trim().toLowerCase() === wantHost).map((c) => ({
|
|
12557
|
+
account: (c.username ?? "").trim(),
|
|
12558
|
+
label: c.label ?? (c.username || info2.label),
|
|
12559
|
+
createdAt: c.createdAt
|
|
12560
|
+
}));
|
|
12561
|
+
}
|
|
12562
|
+
|
|
12563
|
+
// src/lib/publish/connect-google.ts
|
|
12564
|
+
async function connectGoogleDrive(options, deps, callbacks) {
|
|
12565
|
+
const provider = new GoogleAuthProvider({
|
|
12566
|
+
clientId: options.clientId,
|
|
12567
|
+
clientSecret: options.clientSecret,
|
|
12568
|
+
fetchImpl: deps.fetch,
|
|
12569
|
+
...options.openBrowser ? { openBrowser: options.openBrowser } : {}
|
|
12570
|
+
});
|
|
12571
|
+
const credential = await provider.connect(callbacks);
|
|
12572
|
+
const email = credential.username;
|
|
12573
|
+
const account = (options.account ?? "").trim();
|
|
12574
|
+
const key = publishCredentialKey(GDRIVE_HOST, account);
|
|
12575
|
+
const { username: _accountEmailAsUsername, ...credentialWithoutUsername } = credential;
|
|
12576
|
+
await deps.tokenStore.set(key, {
|
|
12577
|
+
...credentialWithoutUsername,
|
|
12578
|
+
...account ? { username: account } : {},
|
|
12579
|
+
label: account ? `${account} (${email ?? "Google Drive"})` : credential.label
|
|
12580
|
+
});
|
|
12581
|
+
return { connected: true, ...email ? { email } : {} };
|
|
12582
|
+
}
|
|
12228
12583
|
// src/lib/remote-auth/github-repos.ts
|
|
12229
12584
|
var API_BASE2 = "https://api.github.com";
|
|
12230
|
-
var
|
|
12585
|
+
var REQUEST_TIMEOUT_MS3 = 15000;
|
|
12231
12586
|
var PER_PAGE = 100;
|
|
12232
12587
|
var MAX_PAGES = 50;
|
|
12233
|
-
var
|
|
12588
|
+
var RECONNECT_MESSAGE2 = "Your GitHub connection has expired. Reconnect GitHub and try again.";
|
|
12234
12589
|
async function apiGet(fetchImpl, url, token) {
|
|
12235
|
-
const res = await withFetchTimeout({ timeoutMs:
|
|
12590
|
+
const res = await withFetchTimeout({ timeoutMs: REQUEST_TIMEOUT_MS3, offlineMessage: OFFLINE_MESSAGE }, (signal) => fetchImpl(url, { method: "GET", headers: githubApiHeaders(token), signal }));
|
|
12236
12591
|
if (res.status === 401 || res.status === 403) {
|
|
12237
|
-
throw new Error(
|
|
12592
|
+
throw new Error(RECONNECT_MESSAGE2);
|
|
12238
12593
|
}
|
|
12239
12594
|
if (!res.ok) {
|
|
12240
12595
|
throw new Error(`GitHub returned an unexpected error (HTTP ${res.status}). Please try again.`);
|
|
@@ -12344,7 +12699,7 @@ function isSmallBody(body) {
|
|
|
12344
12699
|
function gitTimeoutError(what, ms) {
|
|
12345
12700
|
return new Error(`Git network operation timed out after ${Math.round(ms / 1000)}s (${what}); ` + `couldn't reach the remote (ETIMEDOUT).`);
|
|
12346
12701
|
}
|
|
12347
|
-
function withIdleTimeout(
|
|
12702
|
+
function withIdleTimeout(http3, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GIT_HTTP_UPLOAD_TIMEOUT_MS) {
|
|
12348
12703
|
return {
|
|
12349
12704
|
async request(options) {
|
|
12350
12705
|
let timer;
|
|
@@ -12373,7 +12728,7 @@ function withIdleTimeout(http2, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
|
|
|
12373
12728
|
arm(isLargeUpload ? uploadMs : idleMs, isLargeUpload ? "the upload did not complete" : "the remote did not respond");
|
|
12374
12729
|
let res;
|
|
12375
12730
|
try {
|
|
12376
|
-
res = await race(
|
|
12731
|
+
res = await race(http3.request(options));
|
|
12377
12732
|
} finally {
|
|
12378
12733
|
disarm();
|
|
12379
12734
|
}
|
|
@@ -12582,12 +12937,12 @@ async function guardTrackingRef(dir, ref, cache2, fn) {
|
|
|
12582
12937
|
throw e;
|
|
12583
12938
|
}
|
|
12584
12939
|
}
|
|
12585
|
-
async function fetchRemoteTip(dir, branch, transport,
|
|
12940
|
+
async function fetchRemoteTip(dir, branch, transport, http3, cache2) {
|
|
12586
12941
|
try {
|
|
12587
12942
|
const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
|
|
12588
12943
|
const result = await guardTrackingRef(dir, trackingRef, cache2, () => git2.fetch({
|
|
12589
12944
|
fs: gitFs,
|
|
12590
|
-
http:
|
|
12945
|
+
http: http3,
|
|
12591
12946
|
dir,
|
|
12592
12947
|
cache: cache2,
|
|
12593
12948
|
remote: transport.remote,
|
|
@@ -13202,7 +13557,7 @@ function defaultSleep2(ms) {
|
|
|
13202
13557
|
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
13203
13558
|
}
|
|
13204
13559
|
async function syncProject(options) {
|
|
13205
|
-
const
|
|
13560
|
+
const http3 = options.httpClient ?? defaultGitHttp;
|
|
13206
13561
|
const push = options.push ?? true;
|
|
13207
13562
|
const attempts = Math.max(1, options.retry?.attempts ?? DEFAULT_SYNC_RETRY.attempts);
|
|
13208
13563
|
const backoffMs = Math.max(0, options.retry?.backoffMs ?? DEFAULT_SYNC_RETRY.backoffMs);
|
|
@@ -13239,7 +13594,7 @@ async function syncProject(options) {
|
|
|
13239
13594
|
}
|
|
13240
13595
|
for (let attempt = 0;attempt < attempts; attempt++) {
|
|
13241
13596
|
logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
|
|
13242
|
-
const remoteTip = await fetchRemoteTip(dir, branch, transport,
|
|
13597
|
+
const remoteTip = await fetchRemoteTip(dir, branch, transport, http3, cache2);
|
|
13243
13598
|
let localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
|
|
13244
13599
|
if (push || remoteTip !== null && remoteTip !== localTip) {
|
|
13245
13600
|
const lateSnapshot = await snapshotBeforeAction({
|
|
@@ -13312,7 +13667,7 @@ async function syncProject(options) {
|
|
|
13312
13667
|
try {
|
|
13313
13668
|
await git6.push({
|
|
13314
13669
|
fs: gitFs,
|
|
13315
|
-
http:
|
|
13670
|
+
http: http3,
|
|
13316
13671
|
dir,
|
|
13317
13672
|
cache: cache2,
|
|
13318
13673
|
remote: transport.remote,
|
|
@@ -13351,8 +13706,8 @@ async function syncProject(options) {
|
|
|
13351
13706
|
});
|
|
13352
13707
|
}
|
|
13353
13708
|
// src/lib/publish/run-publish.ts
|
|
13354
|
-
import { readdir as
|
|
13355
|
-
import
|
|
13709
|
+
import { readdir as readdir12, stat as stat11 } from "node:fs/promises";
|
|
13710
|
+
import path27 from "node:path";
|
|
13356
13711
|
|
|
13357
13712
|
// src/lib/publish/command-runner.ts
|
|
13358
13713
|
var CAPTURE_LIMIT = 64 * 1024;
|
|
@@ -13409,44 +13764,6 @@ async function commandExists(cmd, runCommand = defaultCommandRunner, env) {
|
|
|
13409
13764
|
}
|
|
13410
13765
|
}
|
|
13411
13766
|
|
|
13412
|
-
// src/lib/publish/types.ts
|
|
13413
|
-
function publishCredentialKey(host, account) {
|
|
13414
|
-
const a = (account ?? "").trim();
|
|
13415
|
-
return a ? `${host}#${a}` : host;
|
|
13416
|
-
}
|
|
13417
|
-
async function resolvePublishCredential(info2, deps, account = deps.credentialAccount) {
|
|
13418
|
-
const env = deps.env ?? process.env;
|
|
13419
|
-
const fromEnv = info2.credential.envVar ? env[info2.credential.envVar]?.trim() : undefined;
|
|
13420
|
-
if (fromEnv) {
|
|
13421
|
-
return {
|
|
13422
|
-
credential: {
|
|
13423
|
-
host: info2.credential.host,
|
|
13424
|
-
kind: "token",
|
|
13425
|
-
token: fromEnv,
|
|
13426
|
-
createdAt: 0
|
|
13427
|
-
},
|
|
13428
|
-
source: "env"
|
|
13429
|
-
};
|
|
13430
|
-
}
|
|
13431
|
-
const stored = await deps.tokenStore.get(publishCredentialKey(info2.credential.host, account));
|
|
13432
|
-
return stored ? { credential: stored, source: "store" } : null;
|
|
13433
|
-
}
|
|
13434
|
-
async function publishConnectionStatus(info2, deps, account = deps.credentialAccount) {
|
|
13435
|
-
if (!info2.credential.required)
|
|
13436
|
-
return { connected: true };
|
|
13437
|
-
const resolved = await resolvePublishCredential(info2, deps, account);
|
|
13438
|
-
return resolved ? { connected: true, source: resolved.source } : { connected: false };
|
|
13439
|
-
}
|
|
13440
|
-
async function listPublishAccounts(info2, deps) {
|
|
13441
|
-
const wantHost = info2.credential.host.trim().toLowerCase();
|
|
13442
|
-
const all = await deps.tokenStore.list();
|
|
13443
|
-
return all.filter((c) => c.host.trim().toLowerCase() === wantHost).map((c) => ({
|
|
13444
|
-
account: (c.username ?? "").trim(),
|
|
13445
|
-
label: c.label ?? (c.username || info2.label),
|
|
13446
|
-
createdAt: c.createdAt
|
|
13447
|
-
}));
|
|
13448
|
-
}
|
|
13449
|
-
|
|
13450
13767
|
// src/lib/publish/providers/azure-swa.ts
|
|
13451
13768
|
var AZURE_SWA_HOST = "azure-swa";
|
|
13452
13769
|
var info2 = {
|
|
@@ -13619,9 +13936,521 @@ var drivethrurpgProvider = {
|
|
|
13619
13936
|
}
|
|
13620
13937
|
};
|
|
13621
13938
|
|
|
13622
|
-
// src/lib/publish/
|
|
13623
|
-
import {
|
|
13939
|
+
// src/lib/publish/providers/gdrive.ts
|
|
13940
|
+
import { mkdtemp as mkdtemp4, readFile as readFile31, readdir as readdir11, rm as rm9, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
|
|
13941
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
13624
13942
|
import path25 from "node:path";
|
|
13943
|
+
import { Zip, ZipDeflate } from "fflate";
|
|
13944
|
+
|
|
13945
|
+
// src/lib/publish/google-drive.ts
|
|
13946
|
+
import { open as open3 } from "node:fs/promises";
|
|
13947
|
+
var TOKEN_ENDPOINT2 = "https://oauth2.googleapis.com/token";
|
|
13948
|
+
var DRIVE_API_BASE = "https://www.googleapis.com/drive/v3";
|
|
13949
|
+
var DRIVE_UPLOAD_BASE = "https://www.googleapis.com/upload/drive/v3/files";
|
|
13950
|
+
var METADATA_TIMEOUT_MS2 = 30000;
|
|
13951
|
+
var TOKEN_REFRESH_TIMEOUT_MS = 15000;
|
|
13952
|
+
var CHUNK_TIMEOUT_MS = 120000;
|
|
13953
|
+
var OFFLINE_MESSAGE3 = "Couldn't reach Google Drive. Check your connection and try again.";
|
|
13954
|
+
async function driveFetch(fetchImpl, url, init, timeoutMs = METADATA_TIMEOUT_MS2) {
|
|
13955
|
+
return withFetchTimeout({ timeoutMs, offlineMessage: OFFLINE_MESSAGE3 }, (signal) => fetchImpl(url, { ...init, signal }));
|
|
13956
|
+
}
|
|
13957
|
+
function authHeaders(accessToken) {
|
|
13958
|
+
return { Authorization: `Bearer ${accessToken}` };
|
|
13959
|
+
}
|
|
13960
|
+
async function driveFailure(res, what) {
|
|
13961
|
+
return googleApiFailure(what, await readGoogleApiError(res));
|
|
13962
|
+
}
|
|
13963
|
+
function escapeDriveQueryValue(value) {
|
|
13964
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
13965
|
+
}
|
|
13966
|
+
async function refreshAccessToken(fetchImpl, params) {
|
|
13967
|
+
const res = await driveFetch(fetchImpl, TOKEN_ENDPOINT2, {
|
|
13968
|
+
method: "POST",
|
|
13969
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
13970
|
+
body: new URLSearchParams({
|
|
13971
|
+
grant_type: "refresh_token",
|
|
13972
|
+
refresh_token: params.refreshToken,
|
|
13973
|
+
client_id: params.clientId,
|
|
13974
|
+
client_secret: params.clientSecret
|
|
13975
|
+
}).toString()
|
|
13976
|
+
}, TOKEN_REFRESH_TIMEOUT_MS);
|
|
13977
|
+
const body = await res.json().catch(() => ({}));
|
|
13978
|
+
if (!res.ok || !body.access_token) {
|
|
13979
|
+
if (body.error === "invalid_grant")
|
|
13980
|
+
throw new Error(RECONNECT_MESSAGE);
|
|
13981
|
+
throw new FriendlyHttpError(`Google rejected the Drive connection (HTTP ${res.status}${body.error ? `: ${body.error}` : ""}). ${RECONNECT_MESSAGE}`);
|
|
13982
|
+
}
|
|
13983
|
+
return { accessToken: body.access_token, expiresIn: body.expires_in ?? 3600 };
|
|
13984
|
+
}
|
|
13985
|
+
async function driveAbout(fetchImpl, accessToken) {
|
|
13986
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/about?fields=user(emailAddress),storageQuota`, {
|
|
13987
|
+
method: "GET",
|
|
13988
|
+
headers: authHeaders(accessToken)
|
|
13989
|
+
});
|
|
13990
|
+
if (!res.ok)
|
|
13991
|
+
throw await driveFailure(res, "Couldn't read your Google Drive account info");
|
|
13992
|
+
const body = await res.json();
|
|
13993
|
+
const limit = body.storageQuota?.limit != null ? Number(body.storageQuota.limit) : null;
|
|
13994
|
+
const usage = Number(body.storageQuota?.usage ?? 0);
|
|
13995
|
+
return {
|
|
13996
|
+
email: body.user?.emailAddress,
|
|
13997
|
+
quota: {
|
|
13998
|
+
limitBytes: limit,
|
|
13999
|
+
usageBytes: usage,
|
|
14000
|
+
freeBytes: limit != null ? Math.max(0, limit - usage) : null
|
|
14001
|
+
}
|
|
14002
|
+
};
|
|
14003
|
+
}
|
|
14004
|
+
var FOLDER_MIME = "application/vnd.google-apps.folder";
|
|
14005
|
+
async function listFolders(fetchImpl, accessToken) {
|
|
14006
|
+
const q = `mimeType='${FOLDER_MIME}' and trashed=false`;
|
|
14007
|
+
const all = [];
|
|
14008
|
+
let pageToken;
|
|
14009
|
+
do {
|
|
14010
|
+
const params = new URLSearchParams({
|
|
14011
|
+
q,
|
|
14012
|
+
fields: "nextPageToken,files(id,name)",
|
|
14013
|
+
orderBy: "modifiedTime desc",
|
|
14014
|
+
pageSize: "100"
|
|
14015
|
+
});
|
|
14016
|
+
if (pageToken)
|
|
14017
|
+
params.set("pageToken", pageToken);
|
|
14018
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/files?${params.toString()}`, {
|
|
14019
|
+
method: "GET",
|
|
14020
|
+
headers: authHeaders(accessToken)
|
|
14021
|
+
});
|
|
14022
|
+
if (!res.ok)
|
|
14023
|
+
throw await driveFailure(res, "Couldn't list Google Drive folders");
|
|
14024
|
+
const body = await res.json();
|
|
14025
|
+
all.push(...body.files ?? []);
|
|
14026
|
+
pageToken = body.nextPageToken;
|
|
14027
|
+
} while (pageToken);
|
|
14028
|
+
return all;
|
|
14029
|
+
}
|
|
14030
|
+
async function getFolderById(fetchImpl, accessToken, folderId) {
|
|
14031
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/files/${encodeURIComponent(folderId)}?fields=id,name,trashed,mimeType`, {
|
|
14032
|
+
method: "GET",
|
|
14033
|
+
headers: authHeaders(accessToken)
|
|
14034
|
+
});
|
|
14035
|
+
if (res.status === 404)
|
|
14036
|
+
return null;
|
|
14037
|
+
if (!res.ok)
|
|
14038
|
+
throw await driveFailure(res, "Couldn't look up the Google Drive folder");
|
|
14039
|
+
const body = await res.json();
|
|
14040
|
+
if (body.trashed || body.mimeType !== FOLDER_MIME)
|
|
14041
|
+
return null;
|
|
14042
|
+
return { id: body.id, name: body.name };
|
|
14043
|
+
}
|
|
14044
|
+
async function createFolder(fetchImpl, accessToken, name) {
|
|
14045
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/files?fields=id,name`, {
|
|
14046
|
+
method: "POST",
|
|
14047
|
+
headers: { ...authHeaders(accessToken), "Content-Type": "application/json" },
|
|
14048
|
+
body: JSON.stringify({ name, mimeType: FOLDER_MIME })
|
|
14049
|
+
});
|
|
14050
|
+
if (!res.ok)
|
|
14051
|
+
throw await driveFailure(res, `Couldn't create the Google Drive folder "${name}"`);
|
|
14052
|
+
return await res.json();
|
|
14053
|
+
}
|
|
14054
|
+
async function findFolderByName(fetchImpl, accessToken, name) {
|
|
14055
|
+
const q = `name='${escapeDriveQueryValue(name)}' and mimeType='${FOLDER_MIME}' and trashed=false`;
|
|
14056
|
+
const params = new URLSearchParams({
|
|
14057
|
+
q,
|
|
14058
|
+
fields: "files(id,name)",
|
|
14059
|
+
orderBy: "modifiedTime desc",
|
|
14060
|
+
pageSize: "1"
|
|
14061
|
+
});
|
|
14062
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/files?${params.toString()}`, {
|
|
14063
|
+
method: "GET",
|
|
14064
|
+
headers: authHeaders(accessToken)
|
|
14065
|
+
});
|
|
14066
|
+
if (!res.ok)
|
|
14067
|
+
throw await driveFailure(res, `Couldn't look up the Google Drive folder "${name}"`);
|
|
14068
|
+
const body = await res.json();
|
|
14069
|
+
return body.files?.[0] ?? null;
|
|
14070
|
+
}
|
|
14071
|
+
async function ensureFolder(fetchImpl, accessToken, name) {
|
|
14072
|
+
const found = await findFolderByName(fetchImpl, accessToken, name);
|
|
14073
|
+
if (found)
|
|
14074
|
+
return found;
|
|
14075
|
+
return createFolder(fetchImpl, accessToken, name);
|
|
14076
|
+
}
|
|
14077
|
+
async function findFileInFolder(fetchImpl, accessToken, folderId, name) {
|
|
14078
|
+
const q = `name='${escapeDriveQueryValue(name)}' and '${escapeDriveQueryValue(folderId)}' in parents and trashed=false`;
|
|
14079
|
+
const res = await driveFetch(fetchImpl, `${DRIVE_API_BASE}/files?q=${encodeURIComponent(q)}&fields=files(id,name,webViewLink)&pageSize=1`, { method: "GET", headers: authHeaders(accessToken) });
|
|
14080
|
+
if (!res.ok)
|
|
14081
|
+
throw await driveFailure(res, "Couldn't search the Google Drive folder");
|
|
14082
|
+
const body = await res.json();
|
|
14083
|
+
return body.files?.[0] ?? null;
|
|
14084
|
+
}
|
|
14085
|
+
var RESUMABLE_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
14086
|
+
var MAX_CHUNK_RETRIES = 3;
|
|
14087
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
14088
|
+
function defaultSleep3(ms) {
|
|
14089
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
14090
|
+
}
|
|
14091
|
+
async function startResumableSession(fetchImpl, accessToken, opts) {
|
|
14092
|
+
const mimeType = opts.mimeType ?? "application/pdf";
|
|
14093
|
+
const url = opts.fileId ? `${DRIVE_UPLOAD_BASE}/${encodeURIComponent(opts.fileId)}?uploadType=resumable&fields=id,name,webViewLink` : `${DRIVE_UPLOAD_BASE}?uploadType=resumable&fields=id,name,webViewLink`;
|
|
14094
|
+
const res = await driveFetch(fetchImpl, url, {
|
|
14095
|
+
method: opts.fileId ? "PATCH" : "POST",
|
|
14096
|
+
headers: {
|
|
14097
|
+
...authHeaders(accessToken),
|
|
14098
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
14099
|
+
"X-Upload-Content-Type": mimeType,
|
|
14100
|
+
"X-Upload-Content-Length": String(opts.totalBytes)
|
|
14101
|
+
},
|
|
14102
|
+
body: JSON.stringify(opts.fileId ? {} : { name: opts.name, parents: opts.parentFolderId ? [opts.parentFolderId] : undefined })
|
|
14103
|
+
});
|
|
14104
|
+
const session = res.headers.get("location");
|
|
14105
|
+
if (!res.ok || !session)
|
|
14106
|
+
throw await driveFailure(res, "Couldn't start the Google Drive upload");
|
|
14107
|
+
return session;
|
|
14108
|
+
}
|
|
14109
|
+
async function queryUploadStatus(fetchImpl, sessionUrl, total) {
|
|
14110
|
+
let res;
|
|
14111
|
+
try {
|
|
14112
|
+
res = await driveFetch(fetchImpl, sessionUrl, { method: "PUT", headers: { "Content-Range": `bytes */${total}` } }, CHUNK_TIMEOUT_MS);
|
|
14113
|
+
} catch {
|
|
14114
|
+
return null;
|
|
14115
|
+
}
|
|
14116
|
+
if (res.status === 308) {
|
|
14117
|
+
const range = res.headers.get("range");
|
|
14118
|
+
return { kind: "progress", receivedBytes: range ? Number(range.split("-")[1]) + 1 : 0 };
|
|
14119
|
+
}
|
|
14120
|
+
if (res.status === 200 || res.status === 201) {
|
|
14121
|
+
return { kind: "done", file: await res.json() };
|
|
14122
|
+
}
|
|
14123
|
+
return null;
|
|
14124
|
+
}
|
|
14125
|
+
async function putChunkWithRetry(fetchImpl, sessionUrl, buf, start, total, maxRetries, sleep) {
|
|
14126
|
+
let attempt = 0;
|
|
14127
|
+
for (;; ) {
|
|
14128
|
+
let res;
|
|
14129
|
+
try {
|
|
14130
|
+
res = await driveFetch(fetchImpl, sessionUrl, {
|
|
14131
|
+
method: "PUT",
|
|
14132
|
+
headers: {
|
|
14133
|
+
"Content-Range": `bytes ${start}-${start + buf.length - 1}/${total}`,
|
|
14134
|
+
"Content-Length": String(buf.length)
|
|
14135
|
+
},
|
|
14136
|
+
body: buf
|
|
14137
|
+
}, CHUNK_TIMEOUT_MS);
|
|
14138
|
+
} catch (e) {
|
|
14139
|
+
if (attempt >= maxRetries)
|
|
14140
|
+
throw e;
|
|
14141
|
+
attempt++;
|
|
14142
|
+
const status = await queryUploadStatus(fetchImpl, sessionUrl, total);
|
|
14143
|
+
if (status?.kind === "done")
|
|
14144
|
+
return { done: true, file: status.file };
|
|
14145
|
+
if (status?.kind === "progress" && status.receivedBytes > start) {
|
|
14146
|
+
const chunkEnd = start + buf.length;
|
|
14147
|
+
if (status.receivedBytes >= chunkEnd) {
|
|
14148
|
+
return { done: false, nextOffset: status.receivedBytes };
|
|
14149
|
+
}
|
|
14150
|
+
buf = buf.subarray(status.receivedBytes - start);
|
|
14151
|
+
start = status.receivedBytes;
|
|
14152
|
+
}
|
|
14153
|
+
await sleep(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
14154
|
+
continue;
|
|
14155
|
+
}
|
|
14156
|
+
if (res.status === 308) {
|
|
14157
|
+
const range = res.headers.get("range");
|
|
14158
|
+
const nextOffset = range ? Number(range.split("-")[1]) + 1 : start + buf.length;
|
|
14159
|
+
return { done: false, nextOffset };
|
|
14160
|
+
}
|
|
14161
|
+
if (res.status === 200 || res.status === 201) {
|
|
14162
|
+
return { done: true, file: await res.json() };
|
|
14163
|
+
}
|
|
14164
|
+
if ((res.status === 429 || res.status >= 500) && attempt < maxRetries) {
|
|
14165
|
+
attempt++;
|
|
14166
|
+
const retryAfter = res.headers.get("retry-after");
|
|
14167
|
+
const delayMs = retryAfter ? Number(retryAfter) * 1000 : RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
14168
|
+
await sleep(Number.isFinite(delayMs) && delayMs > 0 ? delayMs : RETRY_BASE_DELAY_MS);
|
|
14169
|
+
continue;
|
|
14170
|
+
}
|
|
14171
|
+
throw await driveFailure(res, "Google Drive upload failed");
|
|
14172
|
+
}
|
|
14173
|
+
}
|
|
14174
|
+
async function resumableUpload(fetchImpl, accessToken, opts) {
|
|
14175
|
+
const chunkSize = opts.chunkSize ?? RESUMABLE_CHUNK_SIZE;
|
|
14176
|
+
const maxRetries = opts.maxRetriesPerChunk ?? MAX_CHUNK_RETRIES;
|
|
14177
|
+
const sleep = opts.sleepImpl ?? defaultSleep3;
|
|
14178
|
+
const sessionUrl = await startResumableSession(fetchImpl, accessToken, opts);
|
|
14179
|
+
const fh = await open3(opts.filePath, "r");
|
|
14180
|
+
try {
|
|
14181
|
+
let offset = 0;
|
|
14182
|
+
for (;; ) {
|
|
14183
|
+
const len = Math.min(chunkSize, opts.totalBytes - offset);
|
|
14184
|
+
const buf = Buffer.allocUnsafe(len);
|
|
14185
|
+
const { bytesRead } = await fh.read(buf, 0, len, offset);
|
|
14186
|
+
if (bytesRead !== len) {
|
|
14187
|
+
throw new FriendlyHttpError(`Couldn't read "${opts.filePath}" while uploading to Google Drive: expected ${len} bytes at offset ${offset} but only read ${bytesRead}. The file may have changed on disk during publish.`);
|
|
14188
|
+
}
|
|
14189
|
+
const result = await putChunkWithRetry(fetchImpl, sessionUrl, buf, offset, opts.totalBytes, maxRetries, sleep);
|
|
14190
|
+
if (result.done) {
|
|
14191
|
+
opts.onProgress?.(opts.totalBytes, opts.totalBytes);
|
|
14192
|
+
return result.file;
|
|
14193
|
+
}
|
|
14194
|
+
offset = result.nextOffset;
|
|
14195
|
+
opts.onProgress?.(Math.min(offset, opts.totalBytes), opts.totalBytes);
|
|
14196
|
+
if (offset >= opts.totalBytes) {
|
|
14197
|
+
throw new FriendlyHttpError("Google Drive accepted every byte but never confirmed the upload. Try publishing again.");
|
|
14198
|
+
}
|
|
14199
|
+
}
|
|
14200
|
+
} finally {
|
|
14201
|
+
await fh.close();
|
|
14202
|
+
}
|
|
14203
|
+
}
|
|
14204
|
+
|
|
14205
|
+
// src/lib/publish/providers/gdrive.ts
|
|
14206
|
+
var DEFAULT_FOLDER_NAME = "Gutterpress";
|
|
14207
|
+
var NOT_CONNECTED_MESSAGE = "Google Drive isn't connected. Run `gutterpress publish --provider gdrive --connect` (or set GDRIVE_REFRESH_TOKEN) first.";
|
|
14208
|
+
var info4 = {
|
|
14209
|
+
id: "gdrive",
|
|
14210
|
+
label: "Google Drive",
|
|
14211
|
+
kind: "api",
|
|
14212
|
+
format: "pdf",
|
|
14213
|
+
formats: ["pdf", "html"],
|
|
14214
|
+
description: "Upload the finished PDF (or the website export, zipped) to a folder in your Google Drive — publishing again updates the same file, so shared links stay current. Drive delivers files, not live websites; use Azure Static Web Apps to publish the HTML export as a site.",
|
|
14215
|
+
configFields: [{ key: "folder", label: "Drive folder", placeholder: DEFAULT_FOLDER_NAME }],
|
|
14216
|
+
credential: {
|
|
14217
|
+
required: true,
|
|
14218
|
+
host: GDRIVE_HOST,
|
|
14219
|
+
envVar: "GDRIVE_REFRESH_TOKEN",
|
|
14220
|
+
connect: "oauth",
|
|
14221
|
+
hint: "Click Connect Google Drive and approve in your browser — nothing to paste."
|
|
14222
|
+
},
|
|
14223
|
+
destinations: { label: "Folder", canCreate: true }
|
|
14224
|
+
};
|
|
14225
|
+
function readConfig(req) {
|
|
14226
|
+
const cfg = req.config;
|
|
14227
|
+
return {
|
|
14228
|
+
folder: cfg.folder?.trim() || undefined,
|
|
14229
|
+
folderId: cfg.folderId?.trim() || undefined
|
|
14230
|
+
};
|
|
14231
|
+
}
|
|
14232
|
+
function formatBytes(bytes) {
|
|
14233
|
+
if (bytes < 1024 * 1024)
|
|
14234
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
14235
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
14236
|
+
}
|
|
14237
|
+
async function mintAccessToken(resolved, fetchImpl) {
|
|
14238
|
+
const { clientId, clientSecret } = requireGoogleClientCredentials();
|
|
14239
|
+
const { accessToken } = await refreshAccessToken(fetchImpl, {
|
|
14240
|
+
clientId,
|
|
14241
|
+
clientSecret,
|
|
14242
|
+
refreshToken: resolved.credential.token
|
|
14243
|
+
});
|
|
14244
|
+
return accessToken;
|
|
14245
|
+
}
|
|
14246
|
+
async function getAccessToken(req) {
|
|
14247
|
+
const resolved = await resolvePublishCredential(info4, req.deps);
|
|
14248
|
+
if (!resolved)
|
|
14249
|
+
throw new Error(NOT_CONNECTED_MESSAGE);
|
|
14250
|
+
const accessToken = await mintAccessToken(resolved, req.deps.fetch ?? globalThis.fetch);
|
|
14251
|
+
return { accessToken, source: resolved.source };
|
|
14252
|
+
}
|
|
14253
|
+
async function resolveFolder(fetchImpl, accessToken, cfg) {
|
|
14254
|
+
if (cfg.folderId) {
|
|
14255
|
+
const folder = await getFolderById(fetchImpl, accessToken, cfg.folderId);
|
|
14256
|
+
if (!folder) {
|
|
14257
|
+
throw new Error(`The Drive folder recorded in publish.gdrive.folderId ("${cfg.folderId}") can't be found — it may have been trashed or the id copied wrong. Pick the folder again.`);
|
|
14258
|
+
}
|
|
14259
|
+
return folder;
|
|
14260
|
+
}
|
|
14261
|
+
return ensureFolder(fetchImpl, accessToken, cfg.folder || DEFAULT_FOLDER_NAME);
|
|
14262
|
+
}
|
|
14263
|
+
function folderUrl(id) {
|
|
14264
|
+
return `https://drive.google.com/drive/folders/${id}`;
|
|
14265
|
+
}
|
|
14266
|
+
function toDestination(folder) {
|
|
14267
|
+
return { id: folder.id, title: folder.name, url: folderUrl(folder.id) };
|
|
14268
|
+
}
|
|
14269
|
+
var ZIP_CHUNK_BYTES = 64 * 1024;
|
|
14270
|
+
function yieldToEventLoop() {
|
|
14271
|
+
return new Promise((resolve9) => setImmediate(resolve9));
|
|
14272
|
+
}
|
|
14273
|
+
function zipEntriesNonBlocking(entries, opts) {
|
|
14274
|
+
return new Promise((resolve9, reject) => {
|
|
14275
|
+
const chunks = [];
|
|
14276
|
+
let total = 0;
|
|
14277
|
+
let settled = false;
|
|
14278
|
+
const archive = new Zip((err, chunk2, final) => {
|
|
14279
|
+
if (settled)
|
|
14280
|
+
return;
|
|
14281
|
+
if (err) {
|
|
14282
|
+
settled = true;
|
|
14283
|
+
reject(err);
|
|
14284
|
+
return;
|
|
14285
|
+
}
|
|
14286
|
+
if (chunk2) {
|
|
14287
|
+
chunks.push(chunk2);
|
|
14288
|
+
total += chunk2.length;
|
|
14289
|
+
}
|
|
14290
|
+
if (final) {
|
|
14291
|
+
settled = true;
|
|
14292
|
+
const out = new Uint8Array(total);
|
|
14293
|
+
let offset = 0;
|
|
14294
|
+
for (const c of chunks) {
|
|
14295
|
+
out.set(c, offset);
|
|
14296
|
+
offset += c.length;
|
|
14297
|
+
}
|
|
14298
|
+
resolve9(out);
|
|
14299
|
+
}
|
|
14300
|
+
});
|
|
14301
|
+
(async () => {
|
|
14302
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
14303
|
+
const stream = new ZipDeflate(name, opts);
|
|
14304
|
+
archive.add(stream);
|
|
14305
|
+
if (data.length === 0) {
|
|
14306
|
+
stream.push(new Uint8Array(0), true);
|
|
14307
|
+
continue;
|
|
14308
|
+
}
|
|
14309
|
+
for (let offset = 0;offset < data.length; offset += ZIP_CHUNK_BYTES) {
|
|
14310
|
+
const end = Math.min(offset + ZIP_CHUNK_BYTES, data.length);
|
|
14311
|
+
stream.push(data.subarray(offset, end), end >= data.length);
|
|
14312
|
+
await yieldToEventLoop();
|
|
14313
|
+
}
|
|
14314
|
+
}
|
|
14315
|
+
archive.end();
|
|
14316
|
+
})().catch((e) => {
|
|
14317
|
+
if (!settled) {
|
|
14318
|
+
settled = true;
|
|
14319
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
14320
|
+
}
|
|
14321
|
+
});
|
|
14322
|
+
});
|
|
14323
|
+
}
|
|
14324
|
+
async function collectZipEntries(root, dir, out, onWarn) {
|
|
14325
|
+
const entries = await readdir11(dir, { withFileTypes: true });
|
|
14326
|
+
for (const entry of entries) {
|
|
14327
|
+
const full = path25.join(dir, entry.name);
|
|
14328
|
+
if (entry.isDirectory()) {
|
|
14329
|
+
await collectZipEntries(root, full, out, onWarn);
|
|
14330
|
+
} else if (entry.isFile()) {
|
|
14331
|
+
const rel = path25.relative(root, full).split(path25.sep).join("/");
|
|
14332
|
+
out[rel] = await readFile31(full);
|
|
14333
|
+
} else if (entry.isSymbolicLink()) {
|
|
14334
|
+
const resolved = await stat9(full).catch(() => null);
|
|
14335
|
+
if (resolved?.isFile()) {
|
|
14336
|
+
const rel = path25.relative(root, full).split(path25.sep).join("/");
|
|
14337
|
+
out[rel] = await readFile31(full);
|
|
14338
|
+
} else if (resolved?.isDirectory()) {
|
|
14339
|
+
await collectZipEntries(root, full, out, onWarn);
|
|
14340
|
+
} else {
|
|
14341
|
+
onWarn?.(`Skipped "${path25.relative(root, full)}" in the website export — it's a broken symlink.`);
|
|
14342
|
+
}
|
|
14343
|
+
} else {
|
|
14344
|
+
onWarn?.(`Skipped "${path25.relative(root, full)}" in the website export — not a file, folder, or symlink.`);
|
|
14345
|
+
}
|
|
14346
|
+
}
|
|
14347
|
+
}
|
|
14348
|
+
async function zipHtmlExport(exportDir, title, onWarn, writeArchiveImpl = writeFile14) {
|
|
14349
|
+
const entries = {};
|
|
14350
|
+
await collectZipEntries(exportDir, exportDir, entries, onWarn);
|
|
14351
|
+
const fileName = `${bookSlug(title)}-website.zip`;
|
|
14352
|
+
const tmpRoot = await mkdtemp4(path25.join(tmpdir4(), "gutterpress-gdrive-"));
|
|
14353
|
+
try {
|
|
14354
|
+
const filePath = path25.join(tmpRoot, fileName);
|
|
14355
|
+
const zipped = await zipEntriesNonBlocking(entries, { level: 6 });
|
|
14356
|
+
await writeArchiveImpl(filePath, zipped);
|
|
14357
|
+
return {
|
|
14358
|
+
filePath,
|
|
14359
|
+
fileName,
|
|
14360
|
+
mimeType: "application/zip",
|
|
14361
|
+
cleanup: () => rm9(tmpRoot, { recursive: true, force: true })
|
|
14362
|
+
};
|
|
14363
|
+
} catch (e) {
|
|
14364
|
+
await rm9(tmpRoot, { recursive: true, force: true }).catch(() => {});
|
|
14365
|
+
throw e;
|
|
14366
|
+
}
|
|
14367
|
+
}
|
|
14368
|
+
var gdriveProvider = {
|
|
14369
|
+
info: info4,
|
|
14370
|
+
async authenticate(req) {
|
|
14371
|
+
const resolved = await resolvePublishCredential(info4, req.deps);
|
|
14372
|
+
if (!resolved)
|
|
14373
|
+
return { ok: false, message: NOT_CONNECTED_MESSAGE };
|
|
14374
|
+
try {
|
|
14375
|
+
const accessToken = await mintAccessToken(resolved, req.deps.fetch ?? globalThis.fetch);
|
|
14376
|
+
await driveAbout(req.deps.fetch ?? globalThis.fetch, accessToken);
|
|
14377
|
+
return { ok: true, source: resolved.source };
|
|
14378
|
+
} catch (e) {
|
|
14379
|
+
return { ok: false, source: resolved.source, message: e instanceof Error ? e.message : String(e) };
|
|
14380
|
+
}
|
|
14381
|
+
},
|
|
14382
|
+
async preflight(_req) {
|
|
14383
|
+
return [];
|
|
14384
|
+
},
|
|
14385
|
+
async listDestinations(req) {
|
|
14386
|
+
const { accessToken } = await getAccessToken(req);
|
|
14387
|
+
const folders = await listFolders(req.deps.fetch ?? globalThis.fetch, accessToken);
|
|
14388
|
+
return folders.map(toDestination);
|
|
14389
|
+
},
|
|
14390
|
+
async createDestination(req, name) {
|
|
14391
|
+
const { accessToken } = await getAccessToken(req);
|
|
14392
|
+
const folder = await createFolder(req.deps.fetch ?? globalThis.fetch, accessToken, name);
|
|
14393
|
+
return toDestination(folder);
|
|
14394
|
+
},
|
|
14395
|
+
async upload(req) {
|
|
14396
|
+
const fetchImpl = req.deps.fetch ?? globalThis.fetch;
|
|
14397
|
+
const cfg = readConfig(req);
|
|
14398
|
+
const { accessToken } = await getAccessToken(req);
|
|
14399
|
+
const isHtml = req.artifact.format === "html";
|
|
14400
|
+
const source = isHtml ? await zipHtmlExport(req.artifact.path, req.project.title, (msg) => req.deps.onProgress?.(msg)) : {
|
|
14401
|
+
filePath: req.artifact.path,
|
|
14402
|
+
fileName: path25.basename(req.artifact.path),
|
|
14403
|
+
mimeType: "application/pdf",
|
|
14404
|
+
cleanup: async () => {}
|
|
14405
|
+
};
|
|
14406
|
+
try {
|
|
14407
|
+
const artifactStat = await stat9(source.filePath);
|
|
14408
|
+
const about = await driveAbout(fetchImpl, accessToken);
|
|
14409
|
+
if (about.quota.limitBytes != null && about.quota.freeBytes != null) {
|
|
14410
|
+
if (artifactStat.size > about.quota.freeBytes) {
|
|
14411
|
+
throw new Error(`Your Google Drive is full — this ${isHtml ? "website export" : "PDF"} needs ${formatBytes(artifactStat.size)} but only ${formatBytes(about.quota.freeBytes)} is free.`);
|
|
14412
|
+
}
|
|
14413
|
+
}
|
|
14414
|
+
req.deps.onProgress?.("Resolving the Drive folder…");
|
|
14415
|
+
const folder = await resolveFolder(fetchImpl, accessToken, cfg);
|
|
14416
|
+
const fileName = source.fileName;
|
|
14417
|
+
const existing = await findFileInFolder(fetchImpl, accessToken, folder.id, fileName);
|
|
14418
|
+
req.deps.onProgress?.(existing ? `Updating "${fileName}" in "${folder.name}"…` : `Uploading "${fileName}" to "${folder.name}"…`);
|
|
14419
|
+
let lastReported = -1;
|
|
14420
|
+
const file = await resumableUpload(fetchImpl, accessToken, {
|
|
14421
|
+
...existing ? { fileId: existing.id } : {},
|
|
14422
|
+
name: fileName,
|
|
14423
|
+
parentFolderId: folder.id,
|
|
14424
|
+
filePath: source.filePath,
|
|
14425
|
+
totalBytes: artifactStat.size,
|
|
14426
|
+
mimeType: source.mimeType,
|
|
14427
|
+
onProgress: (uploaded, total) => {
|
|
14428
|
+
const pct = total > 0 ? Math.floor(uploaded / total * 100) : 100;
|
|
14429
|
+
if (pct !== lastReported) {
|
|
14430
|
+
lastReported = pct;
|
|
14431
|
+
req.deps.onProgress?.(`Uploaded ${formatBytes(uploaded)} of ${formatBytes(total)} (${pct}%)…`);
|
|
14432
|
+
}
|
|
14433
|
+
}
|
|
14434
|
+
});
|
|
14435
|
+
return {
|
|
14436
|
+
kind: "published",
|
|
14437
|
+
url: file.webViewLink,
|
|
14438
|
+
detail: `Uploaded "${fileName}" to the "${folder.name}" folder in your Google Drive${existing ? " (updated the existing file)" : ""}.`,
|
|
14439
|
+
followUp: [
|
|
14440
|
+
"To share it, open it in Drive and use the Share button — Gutterpress never changes who can see your files.",
|
|
14441
|
+
isHtml ? "Google Drive stores files, not live websites — download and unzip it to view the export, or publish it as a real site with the Azure Static Web Apps provider." : undefined,
|
|
14442
|
+
cfg.folderId ? undefined : `Tip (CLI): record the folder id in the manifest (publish.gdrive.folderId: "${folder.id}") so renaming the folder in Drive can never break publishing.`
|
|
14443
|
+
].filter((s) => !!s)
|
|
14444
|
+
};
|
|
14445
|
+
} finally {
|
|
14446
|
+
await source.cleanup();
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
};
|
|
14450
|
+
|
|
14451
|
+
// src/lib/publish/butler.ts
|
|
14452
|
+
import { chmod as chmod2, mkdir as mkdir17, stat as stat10, writeFile as writeFile15 } from "node:fs/promises";
|
|
14453
|
+
import path26 from "node:path";
|
|
13625
14454
|
import { unzipSync as unzipSync2 } from "fflate";
|
|
13626
14455
|
var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
|
|
13627
14456
|
function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
|
|
@@ -13641,7 +14470,7 @@ function butlerBinaryName(platform2 = process.platform) {
|
|
|
13641
14470
|
}
|
|
13642
14471
|
async function fileExists2(p) {
|
|
13643
14472
|
try {
|
|
13644
|
-
return (await
|
|
14473
|
+
return (await stat10(p)).isFile();
|
|
13645
14474
|
} catch {
|
|
13646
14475
|
return false;
|
|
13647
14476
|
}
|
|
@@ -13658,8 +14487,8 @@ async function ensureButler(deps) {
|
|
|
13658
14487
|
}
|
|
13659
14488
|
if (await commandExists("butler", runCommand, deps.env))
|
|
13660
14489
|
return "butler";
|
|
13661
|
-
const cacheDir =
|
|
13662
|
-
const cached =
|
|
14490
|
+
const cacheDir = path26.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
|
|
14491
|
+
const cached = path26.join(cacheDir, butlerBinaryName());
|
|
13663
14492
|
if (await fileExists2(cached))
|
|
13664
14493
|
return cached;
|
|
13665
14494
|
const channel = butlerBrothChannel();
|
|
@@ -13686,9 +14515,9 @@ async function ensureButler(deps) {
|
|
|
13686
14515
|
for (const [name, data] of Object.entries(files)) {
|
|
13687
14516
|
if (name.endsWith("/"))
|
|
13688
14517
|
continue;
|
|
13689
|
-
const target =
|
|
13690
|
-
await
|
|
13691
|
-
if (
|
|
14518
|
+
const target = path26.join(cacheDir, path26.basename(name));
|
|
14519
|
+
await writeFile15(target, data);
|
|
14520
|
+
if (path26.basename(name) === butlerBinaryName()) {
|
|
13692
14521
|
await chmod2(target, 493).catch(() => {});
|
|
13693
14522
|
foundBinary = true;
|
|
13694
14523
|
}
|
|
@@ -13704,7 +14533,7 @@ async function ensureButler(deps) {
|
|
|
13704
14533
|
var ITCH_HOST = "itch.io";
|
|
13705
14534
|
var TARGET_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
13706
14535
|
var ITCH_API_TIMEOUT_MS = 15000;
|
|
13707
|
-
var
|
|
14536
|
+
var info5 = {
|
|
13708
14537
|
id: "itch",
|
|
13709
14538
|
label: "itch.io",
|
|
13710
14539
|
kind: "api",
|
|
@@ -13722,7 +14551,7 @@ var info4 = {
|
|
|
13722
14551
|
hint: "Create an API key under itch.io → Settings → API keys, then paste it here."
|
|
13723
14552
|
}
|
|
13724
14553
|
};
|
|
13725
|
-
function
|
|
14554
|
+
function readConfig2(req) {
|
|
13726
14555
|
const cfg = req.config;
|
|
13727
14556
|
return {
|
|
13728
14557
|
target: (cfg.target ?? "").trim(),
|
|
@@ -13734,9 +14563,9 @@ function itchProjectUrl(target) {
|
|
|
13734
14563
|
return `https://${user}.itch.io/${game}`;
|
|
13735
14564
|
}
|
|
13736
14565
|
var itchProvider = {
|
|
13737
|
-
info:
|
|
14566
|
+
info: info5,
|
|
13738
14567
|
async authenticate(req) {
|
|
13739
|
-
const resolved = await resolvePublishCredential(
|
|
14568
|
+
const resolved = await resolvePublishCredential(info5, req.deps);
|
|
13740
14569
|
if (!resolved) {
|
|
13741
14570
|
return {
|
|
13742
14571
|
ok: false,
|
|
@@ -13779,7 +14608,7 @@ var itchProvider = {
|
|
|
13779
14608
|
},
|
|
13780
14609
|
async preflight(req) {
|
|
13781
14610
|
const issues = [];
|
|
13782
|
-
const { target, channel } =
|
|
14611
|
+
const { target, channel } = readConfig2(req);
|
|
13783
14612
|
if (!target) {
|
|
13784
14613
|
issues.push({
|
|
13785
14614
|
severity: "error",
|
|
@@ -13803,8 +14632,8 @@ var itchProvider = {
|
|
|
13803
14632
|
return issues;
|
|
13804
14633
|
},
|
|
13805
14634
|
async upload(req) {
|
|
13806
|
-
const { target, channel } =
|
|
13807
|
-
const resolved = await resolvePublishCredential(
|
|
14635
|
+
const { target, channel } = readConfig2(req);
|
|
14636
|
+
const resolved = await resolvePublishCredential(info5, req.deps);
|
|
13808
14637
|
if (!resolved) {
|
|
13809
14638
|
throw new Error("No itch.io API key found. Connect itch.io (or set BUTLER_API_KEY) first.");
|
|
13810
14639
|
}
|
|
@@ -13832,7 +14661,7 @@ ${tail}` : ""}`);
|
|
|
13832
14661
|
|
|
13833
14662
|
// src/lib/publish/providers/kdp.ts
|
|
13834
14663
|
var KDP_URL = "https://kdp.amazon.com/en_US/bookshelf";
|
|
13835
|
-
var
|
|
14664
|
+
var info6 = {
|
|
13836
14665
|
id: "kdp",
|
|
13837
14666
|
label: "Amazon KDP",
|
|
13838
14667
|
kind: "guided",
|
|
@@ -13842,7 +14671,7 @@ var info5 = {
|
|
|
13842
14671
|
credential: { required: false, host: "kdp.amazon.com" }
|
|
13843
14672
|
};
|
|
13844
14673
|
var kdpProvider = {
|
|
13845
|
-
info:
|
|
14674
|
+
info: info6,
|
|
13846
14675
|
async authenticate() {
|
|
13847
14676
|
return { ok: true };
|
|
13848
14677
|
},
|
|
@@ -13897,7 +14726,7 @@ function requireValidShop(shop) {
|
|
|
13897
14726
|
}
|
|
13898
14727
|
return shop;
|
|
13899
14728
|
}
|
|
13900
|
-
var
|
|
14729
|
+
var info7 = {
|
|
13901
14730
|
id: "shopify",
|
|
13902
14731
|
label: "Shopify",
|
|
13903
14732
|
kind: "api",
|
|
@@ -13920,7 +14749,7 @@ var info6 = {
|
|
|
13920
14749
|
hint: "Create a custom app in Shopify admin (Settings → Apps → Develop apps) with write_products scope, then paste its Admin API access token."
|
|
13921
14750
|
}
|
|
13922
14751
|
};
|
|
13923
|
-
function
|
|
14752
|
+
function readConfig3(req) {
|
|
13924
14753
|
const cfg = req.config;
|
|
13925
14754
|
return {
|
|
13926
14755
|
shop: (cfg.shop ?? "").trim().toLowerCase(),
|
|
@@ -13929,9 +14758,9 @@ function readConfig2(req) {
|
|
|
13929
14758
|
};
|
|
13930
14759
|
}
|
|
13931
14760
|
async function adminGraphQL(req, query, variables = {}) {
|
|
13932
|
-
const { shop: rawShop, apiVersion } =
|
|
14761
|
+
const { shop: rawShop, apiVersion } = readConfig3(req);
|
|
13933
14762
|
const shop = requireValidShop(rawShop);
|
|
13934
|
-
const resolved = await resolvePublishCredential(
|
|
14763
|
+
const resolved = await resolvePublishCredential(info7, req.deps);
|
|
13935
14764
|
if (!resolved) {
|
|
13936
14765
|
throw new Error("No Shopify access token found. Connect Shopify (or set SHOPIFY_ADMIN_TOKEN) first.");
|
|
13937
14766
|
}
|
|
@@ -13982,9 +14811,9 @@ function userErrorsToMessage(result) {
|
|
|
13982
14811
|
return errs?.length ? errs[0].message : null;
|
|
13983
14812
|
}
|
|
13984
14813
|
var shopifyProvider = {
|
|
13985
|
-
info:
|
|
14814
|
+
info: info7,
|
|
13986
14815
|
async authenticate(req) {
|
|
13987
|
-
const resolved = await resolvePublishCredential(
|
|
14816
|
+
const resolved = await resolvePublishCredential(info7, req.deps);
|
|
13988
14817
|
if (!resolved) {
|
|
13989
14818
|
return {
|
|
13990
14819
|
ok: false,
|
|
@@ -14004,7 +14833,7 @@ var shopifyProvider = {
|
|
|
14004
14833
|
},
|
|
14005
14834
|
async preflight(req) {
|
|
14006
14835
|
const issues = [];
|
|
14007
|
-
const { shop } =
|
|
14836
|
+
const { shop } = readConfig3(req);
|
|
14008
14837
|
if (!shop) {
|
|
14009
14838
|
issues.push({
|
|
14010
14839
|
severity: "error",
|
|
@@ -14028,13 +14857,13 @@ var shopifyProvider = {
|
|
|
14028
14857
|
return issues;
|
|
14029
14858
|
},
|
|
14030
14859
|
async listProducts(req) {
|
|
14031
|
-
const { shop } =
|
|
14860
|
+
const { shop } = readConfig3(req);
|
|
14032
14861
|
const data = await adminGraphQL(req, `{ products(first: 25, sortKey: UPDATED_AT, reverse: true) { nodes { ${PRODUCT_FIELDS} } } }`);
|
|
14033
14862
|
const nodes = data.products?.nodes ?? [];
|
|
14034
14863
|
return nodes.map((n) => toProduct(shop, n));
|
|
14035
14864
|
},
|
|
14036
14865
|
async updateListing(req, productId, metadata) {
|
|
14037
|
-
const { shop } =
|
|
14866
|
+
const { shop } = readConfig3(req);
|
|
14038
14867
|
const data = await adminGraphQL(req, `mutation($product: ProductUpdateInput!) {
|
|
14039
14868
|
productUpdate(product: $product) {
|
|
14040
14869
|
product { ${PRODUCT_FIELDS} }
|
|
@@ -14055,7 +14884,7 @@ var shopifyProvider = {
|
|
|
14055
14884
|
return toProduct(shop, result.product);
|
|
14056
14885
|
},
|
|
14057
14886
|
async upload(req) {
|
|
14058
|
-
const { shop, productId } =
|
|
14887
|
+
const { shop, productId } = readConfig3(req);
|
|
14059
14888
|
const title = req.project.title || "Untitled";
|
|
14060
14889
|
let product;
|
|
14061
14890
|
if (productId) {
|
|
@@ -14101,7 +14930,8 @@ var PROVIDERS = {
|
|
|
14101
14930
|
drivethrurpg: drivethrurpgProvider,
|
|
14102
14931
|
kdp: kdpProvider,
|
|
14103
14932
|
"azure-swa": azureSwaProvider,
|
|
14104
|
-
shopify: shopifyProvider
|
|
14933
|
+
shopify: shopifyProvider,
|
|
14934
|
+
gdrive: gdriveProvider
|
|
14105
14935
|
};
|
|
14106
14936
|
function listPublishProviders() {
|
|
14107
14937
|
return Object.values(PROVIDERS).map((p) => p.info);
|
|
@@ -14116,6 +14946,15 @@ function publishProviderFor(id) {
|
|
|
14116
14946
|
}
|
|
14117
14947
|
|
|
14118
14948
|
// src/lib/publish/run-publish.ts
|
|
14949
|
+
function resolvePublishFormat(info8, providerConfig) {
|
|
14950
|
+
if (info8.formats && info8.formats.length > 0) {
|
|
14951
|
+
const configured = typeof providerConfig.format === "string" ? providerConfig.format.trim() : "";
|
|
14952
|
+
if (configured && info8.formats.includes(configured)) {
|
|
14953
|
+
return configured;
|
|
14954
|
+
}
|
|
14955
|
+
}
|
|
14956
|
+
return info8.format;
|
|
14957
|
+
}
|
|
14119
14958
|
async function resolvePublishRequest(options, deps) {
|
|
14120
14959
|
const provider = publishProviderFor(options.providerId);
|
|
14121
14960
|
const { manifest, manifestDir } = await loadManifestWithPath(options.manifestPath ?? options.projectDir, { explicit: options.manifestPath !== undefined });
|
|
@@ -14126,10 +14965,11 @@ async function resolvePublishRequest(options, deps) {
|
|
|
14126
14965
|
const effectiveAccount = bookAccount || deps.credentialAccount;
|
|
14127
14966
|
const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
|
|
14128
14967
|
const outDir = resolveOutputDir(manifestDir, config.title);
|
|
14129
|
-
const
|
|
14968
|
+
const effectiveFormat = resolvePublishFormat(provider.info, providerConfig);
|
|
14969
|
+
const defaultArtifact = effectiveFormat === "pdf" ? path27.join(outDir, artifactName(config.title, "pdf")) : outDir;
|
|
14130
14970
|
const artifact = {
|
|
14131
|
-
path: options.artifactPath ?
|
|
14132
|
-
format:
|
|
14971
|
+
path: options.artifactPath ? path27.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
|
|
14972
|
+
format: effectiveFormat
|
|
14133
14973
|
};
|
|
14134
14974
|
return {
|
|
14135
14975
|
project: {
|
|
@@ -14146,7 +14986,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
|
|
|
14146
14986
|
var HTML_HINT = "Build the website export first (gutterpress build --format html).";
|
|
14147
14987
|
async function artifactIssues(artifact) {
|
|
14148
14988
|
try {
|
|
14149
|
-
const s = await
|
|
14989
|
+
const s = await stat11(artifact.path);
|
|
14150
14990
|
if (artifact.format === "pdf" && !s.isFile()) {
|
|
14151
14991
|
return [
|
|
14152
14992
|
{
|
|
@@ -14192,7 +15032,7 @@ async function artifactIssues(artifact) {
|
|
|
14192
15032
|
var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
|
|
14193
15033
|
async function htmlDirIssues(dir) {
|
|
14194
15034
|
const issues = [];
|
|
14195
|
-
const isThere = async (rel) =>
|
|
15035
|
+
const isThere = async (rel) => stat11(path27.join(dir, rel)).then(() => true, () => false);
|
|
14196
15036
|
if (!await isThere(BOOK_HTML)) {
|
|
14197
15037
|
issues.push({
|
|
14198
15038
|
severity: "error",
|
|
@@ -14200,13 +15040,13 @@ async function htmlDirIssues(dir) {
|
|
|
14200
15040
|
message: `${dir} has no ${BOOK_HTML} — it isn't an HTML export. ${HTML_HINT}`
|
|
14201
15041
|
});
|
|
14202
15042
|
}
|
|
14203
|
-
const entries = await
|
|
15043
|
+
const entries = await readdir12(dir).catch(() => []);
|
|
14204
15044
|
const extras = entries.filter((name) => name.toLowerCase().endsWith(".pdf") || name === BUILD_FINGERPRINT_FILENAME);
|
|
14205
15045
|
if (extras.length > 0) {
|
|
14206
15046
|
issues.push({
|
|
14207
15047
|
severity: "warning",
|
|
14208
15048
|
id: "publish/html-dir-extras",
|
|
14209
|
-
message: `${dir} also contains ${extras.join(" and ")} — everything in the folder
|
|
15049
|
+
message: `${dir} also contains ${extras.join(" and ")} — everything in the folder gets bundled and published along with the website. ` + "Use a dedicated output folder for the website (gutterpress build --format html --out <dir>) if that isn't intended."
|
|
14210
15050
|
});
|
|
14211
15051
|
}
|
|
14212
15052
|
return issues;
|
|
@@ -14262,14 +15102,17 @@ function overlayStore(inner, key, candidate) {
|
|
|
14262
15102
|
}
|
|
14263
15103
|
async function connectPublishProvider(options, deps) {
|
|
14264
15104
|
const provider = publishProviderFor(options.providerId);
|
|
14265
|
-
const
|
|
14266
|
-
if (!
|
|
14267
|
-
throw new Error(`${
|
|
15105
|
+
const info8 = provider.info;
|
|
15106
|
+
if (!info8.credential.required) {
|
|
15107
|
+
throw new Error(`${info8.label} needs no API key — just publish when you're ready.`);
|
|
15108
|
+
}
|
|
15109
|
+
if (info8.credential.connect === "oauth") {
|
|
15110
|
+
throw new Error(`${info8.label} connects through your browser, not a pasted key — run ` + `"gutterpress publish --provider ${info8.id} --connect" or use the desktop app's Connect button.`);
|
|
14268
15111
|
}
|
|
14269
15112
|
const token = options.token.trim();
|
|
14270
15113
|
if (!token)
|
|
14271
15114
|
throw new Error("Paste an API key first.");
|
|
14272
|
-
const host =
|
|
15115
|
+
const host = info8.credential.host;
|
|
14273
15116
|
const account = (options.account ?? "").trim();
|
|
14274
15117
|
const key = publishCredentialKey(host, account);
|
|
14275
15118
|
const candidate = {
|
|
@@ -14277,12 +15120,12 @@ async function connectPublishProvider(options, deps) {
|
|
|
14277
15120
|
kind: "token",
|
|
14278
15121
|
token,
|
|
14279
15122
|
...account ? { username: account } : {},
|
|
14280
|
-
label: account ||
|
|
15123
|
+
label: account || info8.label,
|
|
14281
15124
|
createdAt: Date.now()
|
|
14282
15125
|
};
|
|
14283
15126
|
const req = await resolvePublishRequest({
|
|
14284
15127
|
projectDir: options.projectDir,
|
|
14285
|
-
providerId:
|
|
15128
|
+
providerId: info8.id,
|
|
14286
15129
|
...options.manifestPath ? { manifestPath: options.manifestPath } : {}
|
|
14287
15130
|
}, deps);
|
|
14288
15131
|
const trialDeps = {
|
|
@@ -14293,23 +15136,32 @@ async function connectPublishProvider(options, deps) {
|
|
|
14293
15136
|
};
|
|
14294
15137
|
const auth = await provider.authenticate({ ...req, deps: trialDeps });
|
|
14295
15138
|
if (!auth.ok) {
|
|
14296
|
-
throw new Error(auth.message ?? `${
|
|
15139
|
+
throw new Error(auth.message ?? `${info8.label} didn't accept that key.`);
|
|
14297
15140
|
}
|
|
14298
15141
|
await deps.tokenStore.set(key, candidate);
|
|
14299
|
-
return { connected: true, providerId:
|
|
15142
|
+
return { connected: true, providerId: info8.id };
|
|
15143
|
+
}
|
|
15144
|
+
async function disconnectPublishCredential(key, deps, options = {}) {
|
|
15145
|
+
const existing = await deps.tokenStore.get(key);
|
|
15146
|
+
await deps.tokenStore.delete(key);
|
|
15147
|
+
if (existing?.kind === "google-oauth") {
|
|
15148
|
+
const revoke = revokeGoogleCredential(existing.token, { fetchImpl: deps.fetch });
|
|
15149
|
+
if (options.awaitRevoke)
|
|
15150
|
+
await revoke;
|
|
15151
|
+
}
|
|
14300
15152
|
}
|
|
14301
15153
|
// src/lib/publish/selections.ts
|
|
14302
|
-
import { chmod as chmod3, mkdir as mkdir18, readFile as
|
|
14303
|
-
import
|
|
15154
|
+
import { chmod as chmod3, mkdir as mkdir18, readFile as readFile32, writeFile as writeFile16 } from "node:fs/promises";
|
|
15155
|
+
import path28 from "node:path";
|
|
14304
15156
|
class PublishSelectionsStore {
|
|
14305
15157
|
filePath;
|
|
14306
15158
|
queue = Promise.resolve();
|
|
14307
15159
|
constructor(filePath) {
|
|
14308
|
-
this.filePath = filePath ??
|
|
15160
|
+
this.filePath = filePath ?? path28.join(defaultConfigDir(), "publish-selections.json");
|
|
14309
15161
|
}
|
|
14310
15162
|
async read() {
|
|
14311
15163
|
try {
|
|
14312
|
-
const raw = await
|
|
15164
|
+
const raw = await readFile32(this.filePath, "utf8");
|
|
14313
15165
|
const parsed = JSON.parse(raw);
|
|
14314
15166
|
if (parsed && typeof parsed === "object") {
|
|
14315
15167
|
return {
|
|
@@ -14322,8 +15174,8 @@ class PublishSelectionsStore {
|
|
|
14322
15174
|
return { version: 1, global: {}, projects: {} };
|
|
14323
15175
|
}
|
|
14324
15176
|
async write(data) {
|
|
14325
|
-
await mkdir18(
|
|
14326
|
-
await
|
|
15177
|
+
await mkdir18(path28.dirname(this.filePath), { recursive: true });
|
|
15178
|
+
await writeFile16(this.filePath, JSON.stringify(data, null, 2), {
|
|
14327
15179
|
encoding: "utf8",
|
|
14328
15180
|
mode: 384
|
|
14329
15181
|
});
|
|
@@ -14425,4 +15277,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
|
|
|
14425
15277
|
await writeManifestDoc(file, doc);
|
|
14426
15278
|
return settingsFromDoc(doc);
|
|
14427
15279
|
}
|
|
14428
|
-
export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject,
|
|
15280
|
+
export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
|