@fluid-app/fluid-cli-widget 0.1.27 → 0.1.29
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-JKSTx5pZ.d.mts +199 -0
- package/dist/index-JKSTx5pZ.d.mts.map +1 -0
- package/dist/index.d.mts +3 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1304 -315
- package/dist/index.mjs.map +1 -1
- package/dist/src-_wfDoBLp.mjs +1997 -0
- package/dist/src-_wfDoBLp.mjs.map +1 -0
- package/dist/vite/plugin.d.mts +2 -62
- package/dist/vite/plugin.mjs +2 -200
- package/package.json +8 -5
- package/templates/default/.gitignore.template +6 -0
- package/templates/default/AGENTS.md +15 -0
- package/templates/default/package.json.template +3 -1
- package/templates/default/styles.css +47 -15
- package/dist/vite/plugin.d.mts.map +0 -1
- package/dist/vite/plugin.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { i as fluidOsV0WidgetPackageGitCredentialCreate, l as WidgetToolingError, n as loadAndValidateWidgetPackage, r as publishWidgetPackage$1, t as buildWidgetPackage$1 } from "./src-_wfDoBLp.mjs";
|
|
1
2
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { findProjectConfig, readConfig } from "@fluid-app/fluid-cli";
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { cp, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
3
10
|
import chalk from "chalk";
|
|
4
11
|
import ora from "ora";
|
|
5
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { cp, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
7
|
-
import path from "node:path";
|
|
8
12
|
import { spawn, spawnSync } from "node:child_process";
|
|
9
13
|
import os from "node:os";
|
|
10
|
-
import { execa } from "execa";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
14
|
import prompts from "prompts";
|
|
13
15
|
//#region src/utils/config.ts
|
|
14
16
|
const WIDGET_CONFIG_FILE = "fluid.widget.config.ts";
|
|
@@ -317,22 +319,27 @@ async function validateDropletWidgetProject(projectDir, packageKeyOverride) {
|
|
|
317
319
|
}]);
|
|
318
320
|
const configErrors = await validateRootWidgetConfig(resolvedProjectDir, packageKeyOverride);
|
|
319
321
|
if (configErrors.length > 0) throw new WidgetValidationError(configErrors);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
322
|
+
let validated;
|
|
323
|
+
try {
|
|
324
|
+
validated = (await withRootWidgetConfigBridge(resolvedProjectDir, () => loadAndValidateWidgetPackage({
|
|
325
|
+
projectDir: resolvedProjectDir,
|
|
326
|
+
owner: "droplet"
|
|
327
|
+
}), packageKeyOverride)).validated;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error instanceof WidgetToolingError) {
|
|
330
|
+
if (error.diagnostics) throw new WidgetValidationError(error.diagnostics);
|
|
331
|
+
throw new WidgetValidationError([{
|
|
332
|
+
code: "INVALID_SOURCE_PACKAGE",
|
|
333
|
+
path: sourceConfig.relativePath,
|
|
334
|
+
message: error.details ? `${error.message}\n${error.details}` : error.message
|
|
335
|
+
}]);
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
332
339
|
return {
|
|
333
340
|
projectDir: resolvedProjectDir,
|
|
334
341
|
sourceConfigPath: sourceConfig.relativePath,
|
|
335
|
-
validated
|
|
342
|
+
validated
|
|
336
343
|
};
|
|
337
344
|
}
|
|
338
345
|
async function withRootWidgetConfigBridge(projectDir, callback, packageKeyOverride) {
|
|
@@ -411,29 +418,6 @@ async function validateRootWidgetConfig(projectDir, packageKeyOverride) {
|
|
|
411
418
|
}];
|
|
412
419
|
}
|
|
413
420
|
}
|
|
414
|
-
function validateDropletMetadata(sourcePackages) {
|
|
415
|
-
if (sourcePackages.length !== 1) return [];
|
|
416
|
-
const sourcePackage = sourcePackages[0];
|
|
417
|
-
if (!isRecord$5(sourcePackage)) return [];
|
|
418
|
-
const errors = [];
|
|
419
|
-
if (sourcePackage["packageType"] !== "droplet") errors.push({
|
|
420
|
-
code: "UNSUPPORTED_OWNER",
|
|
421
|
-
path: "packageType",
|
|
422
|
-
message: "Droplet widget packages must set packageType: \"droplet\" in package metadata."
|
|
423
|
-
});
|
|
424
|
-
if (!isNonEmptyString(sourcePackage["packageStableId"])) errors.push({
|
|
425
|
-
code: "INVALID_PACKAGE_KEY",
|
|
426
|
-
path: "packageStableId",
|
|
427
|
-
message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
|
|
428
|
-
});
|
|
429
|
-
const widgets = sourcePackage["widgets"];
|
|
430
|
-
if (Array.isArray(widgets)) widgets.forEach((widget, index) => {
|
|
431
|
-
if (!isRecord$5(widget)) return;
|
|
432
|
-
validateJsonSerializable(widget["propertySchema"], `widgets[${index}].propertySchema`, errors);
|
|
433
|
-
validateJsonSerializable(widget["defaultProps"], `widgets[${index}].defaultProps`, errors);
|
|
434
|
-
});
|
|
435
|
-
return errors;
|
|
436
|
-
}
|
|
437
421
|
function resolvePortalSourceConfig(projectDir) {
|
|
438
422
|
for (const relativePath of SOURCE_CONFIG_CANDIDATES) {
|
|
439
423
|
const candidate = path.join(projectDir, relativePath);
|
|
@@ -443,50 +427,6 @@ function resolvePortalSourceConfig(projectDir) {
|
|
|
443
427
|
};
|
|
444
428
|
}
|
|
445
429
|
}
|
|
446
|
-
function validateJsonSerializable(value, pathLabel, errors) {
|
|
447
|
-
if (value === void 0) return;
|
|
448
|
-
try {
|
|
449
|
-
assertJsonSerializable(value, pathLabel);
|
|
450
|
-
} catch (err) {
|
|
451
|
-
errors.push({
|
|
452
|
-
code: "INVALID_WIDGET_METADATA",
|
|
453
|
-
path: pathLabel,
|
|
454
|
-
message: err instanceof Error ? err.message : String(err)
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
function assertJsonSerializable(value, pathLabel) {
|
|
459
|
-
if (value === null) return;
|
|
460
|
-
const valueType = typeof value;
|
|
461
|
-
if (valueType === "string" || valueType === "boolean") return;
|
|
462
|
-
if (valueType === "number") {
|
|
463
|
-
if (Number.isFinite(value)) return;
|
|
464
|
-
throw new Error(`${pathLabel} must not contain NaN or Infinity.`);
|
|
465
|
-
}
|
|
466
|
-
if (Array.isArray(value)) {
|
|
467
|
-
value.forEach((item, index) => {
|
|
468
|
-
assertJsonSerializable(item, `${pathLabel}[${index}]`);
|
|
469
|
-
});
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
if (isRecord$5(value)) {
|
|
473
|
-
for (const [key, item] of Object.entries(value)) {
|
|
474
|
-
if (item === void 0) throw new Error(`${pathLabel}.${key} must not be undefined.`);
|
|
475
|
-
assertJsonSerializable(item, `${pathLabel}.${key}`);
|
|
476
|
-
}
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
throw new Error(`${pathLabel} must contain only JSON-serializable values.`);
|
|
480
|
-
}
|
|
481
|
-
function appendDetails(message, details) {
|
|
482
|
-
return details ? `${message}\n${details}` : message;
|
|
483
|
-
}
|
|
484
|
-
function isNonEmptyString(value) {
|
|
485
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
486
|
-
}
|
|
487
|
-
function isRecord$5(value) {
|
|
488
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
489
|
-
}
|
|
490
430
|
function isNodeError(error) {
|
|
491
431
|
return error instanceof Error && "code" in error;
|
|
492
432
|
}
|
|
@@ -633,6 +573,138 @@ function isRecord$3(value) {
|
|
|
633
573
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
634
574
|
}
|
|
635
575
|
//#endregion
|
|
576
|
+
//#region src/utils/template.ts
|
|
577
|
+
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.463";
|
|
578
|
+
const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
|
|
579
|
+
const EMBEDDED_CLI_WIDGET_VERSION = "0.1.29";
|
|
580
|
+
function getDefaultTemplatePath() {
|
|
581
|
+
return path.join(findPackageRoot(), "templates", "default");
|
|
582
|
+
}
|
|
583
|
+
async function directoryExists(filePath) {
|
|
584
|
+
try {
|
|
585
|
+
return (await stat(filePath)).isDirectory();
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function copyWidgetTemplate(templatePath, targetPath, variables) {
|
|
591
|
+
const files = await getFiles(templatePath);
|
|
592
|
+
for (const file of files) {
|
|
593
|
+
const sourcePath = path.join(templatePath, file);
|
|
594
|
+
const isTemplate = file.endsWith(".template");
|
|
595
|
+
const outputFiles = getOutputFilenames(file);
|
|
596
|
+
const content = await readFile(sourcePath, "utf-8");
|
|
597
|
+
await writeOutputFiles(targetPath, outputFiles, isTemplate ? renderTemplate(content, variables) : content);
|
|
598
|
+
}
|
|
599
|
+
for (const skillFile of await getSharedTemplateSkillFiles()) await writeOutputFiles(targetPath, getOutputFilenames(skillFile.relativePath), skillFile.content);
|
|
600
|
+
}
|
|
601
|
+
async function getPortalSdkVersion(options = {}) {
|
|
602
|
+
return getWorkspacePackageVersion(["portal", "sdk"], EMBEDDED_PORTAL_SDK_VERSION, options);
|
|
603
|
+
}
|
|
604
|
+
async function getPortalCoreVersion(options = {}) {
|
|
605
|
+
return getWorkspacePackageVersion(["portal", "core"], void 0, options);
|
|
606
|
+
}
|
|
607
|
+
async function getCliVersion(options = {}) {
|
|
608
|
+
return getWorkspacePackageVersion(["cli", "core"], EMBEDDED_CLI_CORE_VERSION, options);
|
|
609
|
+
}
|
|
610
|
+
async function getWidgetCliVersion(options = {}) {
|
|
611
|
+
return getWorkspacePackageVersion(["cli", "widget"], EMBEDDED_CLI_WIDGET_VERSION, options);
|
|
612
|
+
}
|
|
613
|
+
async function getWorkspacePackageVersion(packagePathSegments, embeddedVersion, options) {
|
|
614
|
+
try {
|
|
615
|
+
const packageRoot = findPackageRoot();
|
|
616
|
+
const packagesRoot = path.join(packageRoot, "..", "..");
|
|
617
|
+
const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
|
|
618
|
+
if (options.local) return `link:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot).replace(/\\/g, "/") || "."}`;
|
|
619
|
+
if (embeddedVersion) return embeddedVersion;
|
|
620
|
+
const packageJsonPath = path.join(targetPackageRoot, "package.json");
|
|
621
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
622
|
+
const pkg = JSON.parse(content);
|
|
623
|
+
if (typeof pkg.version !== "string") throw new Error(`Missing version in ${packageJsonPath}`);
|
|
624
|
+
return pkg.version;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (embeddedVersion) return embeddedVersion;
|
|
627
|
+
throw new Error(`Could not resolve the exact template package version for ${packagePathSegments.join("/")}`, { cause: error });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function getFiles(dir, baseDir = dir) {
|
|
631
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
632
|
+
const files = [];
|
|
633
|
+
for (const entry of entries) {
|
|
634
|
+
const fullPath = path.join(dir, entry.name);
|
|
635
|
+
if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
|
|
636
|
+
else files.push(path.relative(baseDir, fullPath));
|
|
637
|
+
}
|
|
638
|
+
return files;
|
|
639
|
+
}
|
|
640
|
+
async function getSharedTemplateSkillFiles() {
|
|
641
|
+
const packageRoot = findFluidCliPackageRoot();
|
|
642
|
+
const skillsRoot = path.join(packageRoot, "template-skills");
|
|
643
|
+
const files = await getFiles(skillsRoot);
|
|
644
|
+
return Promise.all(files.map(async (file) => ({
|
|
645
|
+
relativePath: path.join("skills", file),
|
|
646
|
+
content: await readFile(path.join(skillsRoot, file), "utf-8")
|
|
647
|
+
})));
|
|
648
|
+
}
|
|
649
|
+
function findFluidCliPackageRoot() {
|
|
650
|
+
const workspacePackageRoot = path.join(findPackageRoot(), "..", "core");
|
|
651
|
+
if (existsSync(path.join(workspacePackageRoot, "template-skills"))) return workspacePackageRoot;
|
|
652
|
+
let dir = path.dirname(fileURLToPath(import.meta.resolve("@fluid-app/fluid-cli")));
|
|
653
|
+
while (!existsSync(path.join(dir, "package.json"))) {
|
|
654
|
+
const parent = path.dirname(dir);
|
|
655
|
+
if (parent === dir) throw new Error("Could not find Fluid CLI package root");
|
|
656
|
+
dir = parent;
|
|
657
|
+
}
|
|
658
|
+
return dir;
|
|
659
|
+
}
|
|
660
|
+
function renderTemplate(content, variables) {
|
|
661
|
+
return content.replace(/{{\s*#if\s+([a-zA-Z0-9_]+)\s*}}([\s\S]*?){{\s*\/if\s*}}/g, (match, key, block) => {
|
|
662
|
+
if (!isTemplateVariableKey(key)) return match;
|
|
663
|
+
return variables[key] ? String(block) : "";
|
|
664
|
+
}).replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
|
|
665
|
+
if (isTemplateVariableKey(key)) return variables[key] ?? "";
|
|
666
|
+
return match;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function isTemplateVariableKey(key) {
|
|
670
|
+
return [
|
|
671
|
+
"projectName",
|
|
672
|
+
"widgetScope",
|
|
673
|
+
"widgetPackageVersion",
|
|
674
|
+
"sdkVersion",
|
|
675
|
+
"localCoreVersion",
|
|
676
|
+
"fluidCliVersion",
|
|
677
|
+
"widgetCliVersion",
|
|
678
|
+
"droplet"
|
|
679
|
+
].includes(key);
|
|
680
|
+
}
|
|
681
|
+
async function writeOutputFiles(targetPath, outputFiles, content) {
|
|
682
|
+
for (const outputFile of outputFiles) {
|
|
683
|
+
const destinationPath = path.join(targetPath, outputFile);
|
|
684
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
685
|
+
await writeFile(destinationPath, content, "utf-8");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function getOutputFilenames(filename) {
|
|
689
|
+
const outputFilename = getOutputFilename(filename);
|
|
690
|
+
const normalized = outputFilename.replace(/\\/g, "/");
|
|
691
|
+
if (normalized === "AGENTS.md") return ["AGENTS.md", "CLAUDE.md"];
|
|
692
|
+
if (normalized.startsWith("skills/")) return [path.join(".agents", outputFilename), path.join(".claude", outputFilename)];
|
|
693
|
+
return [outputFilename];
|
|
694
|
+
}
|
|
695
|
+
function getOutputFilename(filename) {
|
|
696
|
+
return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
|
|
697
|
+
}
|
|
698
|
+
function findPackageRoot() {
|
|
699
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
700
|
+
while (!existsSync(path.join(dir, "package.json"))) {
|
|
701
|
+
const parent = path.dirname(dir);
|
|
702
|
+
if (parent === dir) throw new Error("Could not find widget CLI package root");
|
|
703
|
+
dir = parent;
|
|
704
|
+
}
|
|
705
|
+
return dir;
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
636
708
|
//#region src/commands/build.ts
|
|
637
709
|
async function buildWidgetPackage(options) {
|
|
638
710
|
await validateDropletWidgetProject(options.projectDir);
|
|
@@ -640,20 +712,23 @@ async function buildWidgetPackage(options) {
|
|
|
640
712
|
projectDir: options.projectDir,
|
|
641
713
|
scriptName: "typecheck"
|
|
642
714
|
});
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
715
|
+
let buildResult;
|
|
716
|
+
try {
|
|
717
|
+
const cliVersion = await getWidgetCliVersion();
|
|
718
|
+
buildResult = await withRootWidgetConfigBridge(options.projectDir, () => buildWidgetPackage$1({
|
|
719
|
+
projectDir: options.projectDir,
|
|
720
|
+
outDir: options.outDir,
|
|
721
|
+
owner: "droplet",
|
|
722
|
+
cliVersion
|
|
723
|
+
}));
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (error instanceof WidgetToolingError && error.details) throw new Error(`${error.message}\n${error.details}`, { cause: error });
|
|
726
|
+
throw error;
|
|
652
727
|
}
|
|
653
728
|
return {
|
|
654
|
-
outDir: buildResult.
|
|
655
|
-
packageId: buildResult.
|
|
656
|
-
version: buildResult.
|
|
729
|
+
outDir: buildResult.publishDir,
|
|
730
|
+
packageId: buildResult.packageId,
|
|
731
|
+
version: buildResult.version
|
|
657
732
|
};
|
|
658
733
|
}
|
|
659
734
|
const buildCommand = new Command("build").description("Validate and build the current droplet widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_WIDGET_OUT_DIR).option("--skip-typecheck", "Skip package typecheck script when present").action((options) => {
|
|
@@ -675,6 +750,769 @@ const buildCommand = new Command("build").description("Validate and build the cu
|
|
|
675
750
|
});
|
|
676
751
|
});
|
|
677
752
|
//#endregion
|
|
753
|
+
//#region ../../platform/api-client-core/src/api-error-shape.ts
|
|
754
|
+
/**
|
|
755
|
+
* Narrows a parsed JSON error payload to the envelope contract. Arrays and
|
|
756
|
+
* primitives are not envelopes, so they resolve to `null` — their content is
|
|
757
|
+
* still reachable through `ApiError.data`.
|
|
758
|
+
*/
|
|
759
|
+
function toApiErrorBody(value) {
|
|
760
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
761
|
+
return value;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* The single unwrap rule, shared by every producer of `ApiError`.
|
|
765
|
+
*
|
|
766
|
+
* `??` rather than `||` so an explicitly empty `errors` (`""`, `0`, `false`)
|
|
767
|
+
* is preserved instead of silently falling back to the whole envelope.
|
|
768
|
+
*/
|
|
769
|
+
function toApiErrorData(body) {
|
|
770
|
+
if (!body) return null;
|
|
771
|
+
return body.errors ?? body;
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region ../../platform/api-client-core/src/fetch-client.ts
|
|
775
|
+
/**
|
|
776
|
+
* API Error class compatible with fluid-admin's ApiError
|
|
777
|
+
*/
|
|
778
|
+
var ApiError = class ApiError extends Error {
|
|
779
|
+
status;
|
|
780
|
+
/**
|
|
781
|
+
* The unwrapped field-error bag. See `ApiErrorData`; the contract is
|
|
782
|
+
* documented in `docs/api-error-data-contract.md`.
|
|
783
|
+
*/
|
|
784
|
+
data;
|
|
785
|
+
/**
|
|
786
|
+
* The full error response envelope. Read `error_message`, `error`, `status`
|
|
787
|
+
* and nested `errors` from here — `data` has already unwrapped one level.
|
|
788
|
+
*/
|
|
789
|
+
body;
|
|
790
|
+
requestId;
|
|
791
|
+
constructor(message, status, data, requestId, body) {
|
|
792
|
+
super(message);
|
|
793
|
+
this.name = "ApiError";
|
|
794
|
+
this.status = status;
|
|
795
|
+
this.data = data ?? null;
|
|
796
|
+
this.body = body ?? null;
|
|
797
|
+
this.requestId = requestId;
|
|
798
|
+
if ("captureStackTrace" in Error) Error.captureStackTrace(this, ApiError);
|
|
799
|
+
}
|
|
800
|
+
toJSON() {
|
|
801
|
+
return {
|
|
802
|
+
name: this.name,
|
|
803
|
+
message: this.message,
|
|
804
|
+
status: this.status,
|
|
805
|
+
data: this.data,
|
|
806
|
+
body: this.body,
|
|
807
|
+
requestId: this.requestId
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region ../../platform/api-client-core/src/hey-api-error-policy.ts
|
|
813
|
+
function getNonEmptyString(value) {
|
|
814
|
+
if (typeof value !== "string") return;
|
|
815
|
+
const trimmed = value.trim();
|
|
816
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
817
|
+
}
|
|
818
|
+
function getRequestId(response, error) {
|
|
819
|
+
const meta = toApiErrorBody(error?.meta);
|
|
820
|
+
return getNonEmptyString(response.headers.get("x-request-id")) ?? getNonEmptyString(response.headers.get("request-id")) ?? getNonEmptyString(error?.request_id) ?? getNonEmptyString(error?.requestId) ?? getNonEmptyString(meta?.request_id) ?? getNonEmptyString(meta?.requestId);
|
|
821
|
+
}
|
|
822
|
+
function getErrorMessage(error, fallback) {
|
|
823
|
+
const nestedError = toApiErrorBody(error.error);
|
|
824
|
+
return getNonEmptyString(error.message) ?? getNonEmptyString(error.error_message) ?? getNonEmptyString(nestedError?.message) ?? fallback;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Creates an error interceptor compatible with generated Hey API fetch clients.
|
|
828
|
+
* Consumer-specific auth and cancellation policy is supplied through callbacks,
|
|
829
|
+
* keeping this package independent from generated client types.
|
|
830
|
+
*/
|
|
831
|
+
function createHeyApiErrorInterceptor(options = {}) {
|
|
832
|
+
return (error, response, request, requestOptions) => {
|
|
833
|
+
if (!response) {
|
|
834
|
+
if (error instanceof ApiError) return error;
|
|
835
|
+
if (options.shouldPassThroughTransportError?.(error)) return error;
|
|
836
|
+
if (!request) return error;
|
|
837
|
+
return new ApiError(`Network error: ${error instanceof Error ? error.message : "Unknown network error"}`, 0, null);
|
|
838
|
+
}
|
|
839
|
+
if (response.ok && error instanceof SyntaxError) return new ApiError("Failed to parse response as JSON", response.status, null, getRequestId(response, null));
|
|
840
|
+
options.onResponseError?.(response);
|
|
841
|
+
const method = request?.method ?? requestOptions.method ?? "GET";
|
|
842
|
+
const errorRecord = toApiErrorBody(error);
|
|
843
|
+
const requestId = getRequestId(response, errorRecord);
|
|
844
|
+
if (errorRecord) return new ApiError(getErrorMessage(errorRecord, `${method} request failed`), response.status, toApiErrorData(errorRecord), requestId, errorRecord);
|
|
845
|
+
const isJson = response.headers.get("content-type")?.includes("application/json");
|
|
846
|
+
const text = getNonEmptyString(error);
|
|
847
|
+
return new ApiError(isJson && text ? text.slice(0, 200) : `${method} request failed with status ${response.status}`, response.status, isJson ? error : null, requestId, null);
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/bodySerializer.gen.ts
|
|
852
|
+
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
853
|
+
Object.entries({
|
|
854
|
+
$body_: "body",
|
|
855
|
+
$headers_: "headers",
|
|
856
|
+
$path_: "path",
|
|
857
|
+
$query_: "query"
|
|
858
|
+
});
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/serverSentEvents.gen.ts
|
|
861
|
+
function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
|
|
862
|
+
let lastEventId;
|
|
863
|
+
const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
864
|
+
const createStream = async function* () {
|
|
865
|
+
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
866
|
+
let attempt = 0;
|
|
867
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
868
|
+
while (true) {
|
|
869
|
+
if (signal.aborted) break;
|
|
870
|
+
attempt++;
|
|
871
|
+
const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
|
|
872
|
+
if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
|
|
873
|
+
try {
|
|
874
|
+
const requestInit = {
|
|
875
|
+
redirect: "follow",
|
|
876
|
+
...options,
|
|
877
|
+
body: options.serializedBody,
|
|
878
|
+
headers,
|
|
879
|
+
signal
|
|
880
|
+
};
|
|
881
|
+
let request = new Request(url, requestInit);
|
|
882
|
+
if (onRequest) request = await onRequest(url, requestInit);
|
|
883
|
+
const response = await (options.fetch ?? globalThis.fetch)(request);
|
|
884
|
+
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
|
885
|
+
if (!response.body) throw new Error("No body in SSE response");
|
|
886
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
887
|
+
let buffer = "";
|
|
888
|
+
const abortHandler = () => {
|
|
889
|
+
try {
|
|
890
|
+
reader.cancel();
|
|
891
|
+
} catch {}
|
|
892
|
+
};
|
|
893
|
+
signal.addEventListener("abort", abortHandler);
|
|
894
|
+
try {
|
|
895
|
+
while (true) {
|
|
896
|
+
const { done, value } = await reader.read();
|
|
897
|
+
if (done) break;
|
|
898
|
+
buffer += value;
|
|
899
|
+
buffer = buffer.replace(/\r\n?/g, "\n");
|
|
900
|
+
const chunks = buffer.split("\n\n");
|
|
901
|
+
buffer = chunks.pop() ?? "";
|
|
902
|
+
for (const chunk of chunks) {
|
|
903
|
+
const lines = chunk.split("\n");
|
|
904
|
+
const dataLines = [];
|
|
905
|
+
let eventName;
|
|
906
|
+
for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
|
|
907
|
+
else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
|
|
908
|
+
else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
|
|
909
|
+
else if (line.startsWith("retry:")) {
|
|
910
|
+
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
|
911
|
+
if (!Number.isNaN(parsed)) retryDelay = parsed;
|
|
912
|
+
}
|
|
913
|
+
let data;
|
|
914
|
+
let parsedJson = false;
|
|
915
|
+
if (dataLines.length) {
|
|
916
|
+
const rawData = dataLines.join("\n");
|
|
917
|
+
try {
|
|
918
|
+
data = JSON.parse(rawData);
|
|
919
|
+
parsedJson = true;
|
|
920
|
+
} catch {
|
|
921
|
+
data = rawData;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (parsedJson) {
|
|
925
|
+
if (responseValidator) await responseValidator(data);
|
|
926
|
+
if (responseTransformer) data = await responseTransformer(data);
|
|
927
|
+
}
|
|
928
|
+
onSseEvent?.({
|
|
929
|
+
data,
|
|
930
|
+
event: eventName,
|
|
931
|
+
id: lastEventId,
|
|
932
|
+
retry: retryDelay
|
|
933
|
+
});
|
|
934
|
+
if (dataLines.length) yield data;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} finally {
|
|
938
|
+
signal.removeEventListener("abort", abortHandler);
|
|
939
|
+
reader.releaseLock();
|
|
940
|
+
}
|
|
941
|
+
break;
|
|
942
|
+
} catch (error) {
|
|
943
|
+
onSseError?.(error);
|
|
944
|
+
if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
|
|
945
|
+
await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
return { stream: createStream() };
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/pathSerializer.gen.ts
|
|
953
|
+
const separatorArrayExplode = (style) => {
|
|
954
|
+
switch (style) {
|
|
955
|
+
case "label": return ".";
|
|
956
|
+
case "matrix": return ";";
|
|
957
|
+
case "simple": return ",";
|
|
958
|
+
default: return "&";
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const separatorArrayNoExplode = (style) => {
|
|
962
|
+
switch (style) {
|
|
963
|
+
case "form": return ",";
|
|
964
|
+
case "pipeDelimited": return "|";
|
|
965
|
+
case "spaceDelimited": return "%20";
|
|
966
|
+
default: return ",";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
const separatorObjectExplode = (style) => {
|
|
970
|
+
switch (style) {
|
|
971
|
+
case "label": return ".";
|
|
972
|
+
case "matrix": return ";";
|
|
973
|
+
case "simple": return ",";
|
|
974
|
+
default: return "&";
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
|
|
978
|
+
if (!explode) {
|
|
979
|
+
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
|
|
980
|
+
switch (style) {
|
|
981
|
+
case "label": return `.${joinedValues}`;
|
|
982
|
+
case "matrix": return `;${name}=${joinedValues}`;
|
|
983
|
+
case "simple": return joinedValues;
|
|
984
|
+
default: return `${name}=${joinedValues}`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const separator = separatorArrayExplode(style);
|
|
988
|
+
const joinedValues = value.map((v) => {
|
|
989
|
+
if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
|
|
990
|
+
return serializePrimitiveParam({
|
|
991
|
+
allowReserved,
|
|
992
|
+
name,
|
|
993
|
+
value: v
|
|
994
|
+
});
|
|
995
|
+
}).join(separator);
|
|
996
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
997
|
+
};
|
|
998
|
+
const serializePrimitiveParam = ({ allowReserved, name, value }) => {
|
|
999
|
+
if (value === void 0 || value === null) return "";
|
|
1000
|
+
if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
|
|
1001
|
+
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
1002
|
+
};
|
|
1003
|
+
const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
|
|
1004
|
+
if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
1005
|
+
if (style !== "deepObject" && !explode) {
|
|
1006
|
+
let values = [];
|
|
1007
|
+
Object.entries(value).forEach(([key, v]) => {
|
|
1008
|
+
values = [
|
|
1009
|
+
...values,
|
|
1010
|
+
key,
|
|
1011
|
+
allowReserved ? v : encodeURIComponent(v)
|
|
1012
|
+
];
|
|
1013
|
+
});
|
|
1014
|
+
const joinedValues = values.join(",");
|
|
1015
|
+
switch (style) {
|
|
1016
|
+
case "form": return `${name}=${joinedValues}`;
|
|
1017
|
+
case "label": return `.${joinedValues}`;
|
|
1018
|
+
case "matrix": return `;${name}=${joinedValues}`;
|
|
1019
|
+
default: return joinedValues;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const separator = separatorObjectExplode(style);
|
|
1023
|
+
const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
|
|
1024
|
+
allowReserved,
|
|
1025
|
+
name: style === "deepObject" ? `${name}[${key}]` : key,
|
|
1026
|
+
value: v
|
|
1027
|
+
})).join(separator);
|
|
1028
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
1029
|
+
};
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/utils.gen.ts
|
|
1032
|
+
const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
1033
|
+
const defaultPathSerializer = ({ path, url: _url }) => {
|
|
1034
|
+
let url = _url;
|
|
1035
|
+
const matches = _url.match(PATH_PARAM_RE);
|
|
1036
|
+
if (matches) for (const match of matches) {
|
|
1037
|
+
let explode = false;
|
|
1038
|
+
let name = match.substring(1, match.length - 1);
|
|
1039
|
+
let style = "simple";
|
|
1040
|
+
if (name.endsWith("*")) {
|
|
1041
|
+
explode = true;
|
|
1042
|
+
name = name.substring(0, name.length - 1);
|
|
1043
|
+
}
|
|
1044
|
+
if (name.startsWith(".")) {
|
|
1045
|
+
name = name.substring(1);
|
|
1046
|
+
style = "label";
|
|
1047
|
+
} else if (name.startsWith(";")) {
|
|
1048
|
+
name = name.substring(1);
|
|
1049
|
+
style = "matrix";
|
|
1050
|
+
}
|
|
1051
|
+
const value = path[name];
|
|
1052
|
+
if (value === void 0 || value === null) continue;
|
|
1053
|
+
if (Array.isArray(value)) {
|
|
1054
|
+
url = url.replace(match, serializeArrayParam({
|
|
1055
|
+
explode,
|
|
1056
|
+
name,
|
|
1057
|
+
style,
|
|
1058
|
+
value
|
|
1059
|
+
}));
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof value === "object") {
|
|
1063
|
+
url = url.replace(match, serializeObjectParam({
|
|
1064
|
+
explode,
|
|
1065
|
+
name,
|
|
1066
|
+
style,
|
|
1067
|
+
value,
|
|
1068
|
+
valueOnly: true
|
|
1069
|
+
}));
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (style === "matrix") {
|
|
1073
|
+
url = url.replace(match, `;${serializePrimitiveParam({
|
|
1074
|
+
name,
|
|
1075
|
+
value
|
|
1076
|
+
})}`);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
|
|
1080
|
+
url = url.replace(match, replaceValue);
|
|
1081
|
+
}
|
|
1082
|
+
return url;
|
|
1083
|
+
};
|
|
1084
|
+
const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
|
|
1085
|
+
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
|
1086
|
+
let url = (baseUrl ?? "") + pathUrl;
|
|
1087
|
+
if (path) url = defaultPathSerializer({
|
|
1088
|
+
path,
|
|
1089
|
+
url
|
|
1090
|
+
});
|
|
1091
|
+
let search = query ? querySerializer(query) : "";
|
|
1092
|
+
if (search.startsWith("?")) search = search.substring(1);
|
|
1093
|
+
if (search) url += `?${search}`;
|
|
1094
|
+
return url;
|
|
1095
|
+
};
|
|
1096
|
+
function getValidRequestBody(options) {
|
|
1097
|
+
const hasBody = options.body !== void 0;
|
|
1098
|
+
if (hasBody && options.bodySerializer) {
|
|
1099
|
+
if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
|
|
1100
|
+
return options.body !== "" ? options.body : null;
|
|
1101
|
+
}
|
|
1102
|
+
if (hasBody) return options.body;
|
|
1103
|
+
}
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/auth.gen.ts
|
|
1106
|
+
const getAuthToken = async (auth, callback) => {
|
|
1107
|
+
const token = typeof callback === "function" ? await callback(auth) : callback;
|
|
1108
|
+
if (!token) return;
|
|
1109
|
+
if (auth.scheme === "bearer") return `Bearer ${token}`;
|
|
1110
|
+
if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
|
|
1111
|
+
return token;
|
|
1112
|
+
};
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/utils.gen.ts
|
|
1115
|
+
const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
|
|
1116
|
+
const querySerializer = (queryParams) => {
|
|
1117
|
+
const search = [];
|
|
1118
|
+
if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
|
|
1119
|
+
const value = queryParams[name];
|
|
1120
|
+
if (value === void 0 || value === null) continue;
|
|
1121
|
+
const options = parameters[name] || args;
|
|
1122
|
+
if (Array.isArray(value)) {
|
|
1123
|
+
const serializedArray = serializeArrayParam({
|
|
1124
|
+
allowReserved: options.allowReserved,
|
|
1125
|
+
explode: true,
|
|
1126
|
+
name,
|
|
1127
|
+
style: "form",
|
|
1128
|
+
value,
|
|
1129
|
+
...options.array
|
|
1130
|
+
});
|
|
1131
|
+
if (serializedArray) search.push(serializedArray);
|
|
1132
|
+
} else if (typeof value === "object") {
|
|
1133
|
+
const serializedObject = serializeObjectParam({
|
|
1134
|
+
allowReserved: options.allowReserved,
|
|
1135
|
+
explode: true,
|
|
1136
|
+
name,
|
|
1137
|
+
style: "deepObject",
|
|
1138
|
+
value,
|
|
1139
|
+
...options.object
|
|
1140
|
+
});
|
|
1141
|
+
if (serializedObject) search.push(serializedObject);
|
|
1142
|
+
} else {
|
|
1143
|
+
const serializedPrimitive = serializePrimitiveParam({
|
|
1144
|
+
allowReserved: options.allowReserved,
|
|
1145
|
+
name,
|
|
1146
|
+
value
|
|
1147
|
+
});
|
|
1148
|
+
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return search.join("&");
|
|
1152
|
+
};
|
|
1153
|
+
return querySerializer;
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* Infers parseAs value from provided Content-Type header.
|
|
1157
|
+
*/
|
|
1158
|
+
const getParseAs = (contentType) => {
|
|
1159
|
+
if (!contentType) return "stream";
|
|
1160
|
+
const cleanContent = contentType.split(";")[0]?.trim();
|
|
1161
|
+
if (!cleanContent) return;
|
|
1162
|
+
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
|
|
1163
|
+
if (cleanContent === "multipart/form-data") return "formData";
|
|
1164
|
+
if ([
|
|
1165
|
+
"application/",
|
|
1166
|
+
"audio/",
|
|
1167
|
+
"image/",
|
|
1168
|
+
"video/"
|
|
1169
|
+
].some((type) => cleanContent.startsWith(type))) return "blob";
|
|
1170
|
+
if (cleanContent.startsWith("text/")) return "text";
|
|
1171
|
+
};
|
|
1172
|
+
const checkForExistence = (options, name) => {
|
|
1173
|
+
if (!name) return false;
|
|
1174
|
+
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
|
|
1175
|
+
return false;
|
|
1176
|
+
};
|
|
1177
|
+
async function setAuthParams(options) {
|
|
1178
|
+
for (const auth of options.security ?? []) {
|
|
1179
|
+
if (checkForExistence(options, auth.name)) continue;
|
|
1180
|
+
const token = await getAuthToken(auth, options.auth);
|
|
1181
|
+
if (!token) continue;
|
|
1182
|
+
const name = auth.name ?? "Authorization";
|
|
1183
|
+
switch (auth.in) {
|
|
1184
|
+
case "query":
|
|
1185
|
+
if (!options.query) options.query = {};
|
|
1186
|
+
options.query[name] = token;
|
|
1187
|
+
break;
|
|
1188
|
+
case "cookie":
|
|
1189
|
+
options.headers.append("Cookie", `${name}=${token}`);
|
|
1190
|
+
break;
|
|
1191
|
+
default:
|
|
1192
|
+
options.headers.set(name, token);
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const buildUrl = (options) => getUrl({
|
|
1198
|
+
baseUrl: options.baseUrl,
|
|
1199
|
+
path: options.path,
|
|
1200
|
+
query: options.query,
|
|
1201
|
+
querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
|
|
1202
|
+
url: options.url
|
|
1203
|
+
});
|
|
1204
|
+
const mergeConfigs = (a, b) => {
|
|
1205
|
+
const config = {
|
|
1206
|
+
...a,
|
|
1207
|
+
...b
|
|
1208
|
+
};
|
|
1209
|
+
if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
1210
|
+
config.headers = mergeHeaders(a.headers, b.headers);
|
|
1211
|
+
return config;
|
|
1212
|
+
};
|
|
1213
|
+
const headersEntries = (headers) => {
|
|
1214
|
+
const entries = [];
|
|
1215
|
+
headers.forEach((value, key) => {
|
|
1216
|
+
entries.push([key, value]);
|
|
1217
|
+
});
|
|
1218
|
+
return entries;
|
|
1219
|
+
};
|
|
1220
|
+
const mergeHeaders = (...headers) => {
|
|
1221
|
+
const mergedHeaders = new Headers();
|
|
1222
|
+
for (const header of headers) {
|
|
1223
|
+
if (!header) continue;
|
|
1224
|
+
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
|
1225
|
+
for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
|
|
1226
|
+
else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
|
|
1227
|
+
else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
|
|
1228
|
+
}
|
|
1229
|
+
return mergedHeaders;
|
|
1230
|
+
};
|
|
1231
|
+
var Interceptors = class {
|
|
1232
|
+
fns = [];
|
|
1233
|
+
clear() {
|
|
1234
|
+
this.fns = [];
|
|
1235
|
+
}
|
|
1236
|
+
eject(id) {
|
|
1237
|
+
const index = this.getInterceptorIndex(id);
|
|
1238
|
+
if (this.fns[index]) this.fns[index] = null;
|
|
1239
|
+
}
|
|
1240
|
+
exists(id) {
|
|
1241
|
+
const index = this.getInterceptorIndex(id);
|
|
1242
|
+
return Boolean(this.fns[index]);
|
|
1243
|
+
}
|
|
1244
|
+
getInterceptorIndex(id) {
|
|
1245
|
+
if (typeof id === "number") return this.fns[id] ? id : -1;
|
|
1246
|
+
return this.fns.indexOf(id);
|
|
1247
|
+
}
|
|
1248
|
+
update(id, fn) {
|
|
1249
|
+
const index = this.getInterceptorIndex(id);
|
|
1250
|
+
if (this.fns[index]) {
|
|
1251
|
+
this.fns[index] = fn;
|
|
1252
|
+
return id;
|
|
1253
|
+
}
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
use(fn) {
|
|
1257
|
+
this.fns.push(fn);
|
|
1258
|
+
return this.fns.length - 1;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
const createInterceptors = () => ({
|
|
1262
|
+
error: new Interceptors(),
|
|
1263
|
+
request: new Interceptors(),
|
|
1264
|
+
response: new Interceptors()
|
|
1265
|
+
});
|
|
1266
|
+
const defaultQuerySerializer = createQuerySerializer({
|
|
1267
|
+
allowReserved: false,
|
|
1268
|
+
array: {
|
|
1269
|
+
explode: true,
|
|
1270
|
+
style: "form"
|
|
1271
|
+
},
|
|
1272
|
+
object: {
|
|
1273
|
+
explode: true,
|
|
1274
|
+
style: "deepObject"
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
const defaultHeaders = { "Content-Type": "application/json" };
|
|
1278
|
+
const createConfig = (override = {}) => ({
|
|
1279
|
+
...jsonBodySerializer,
|
|
1280
|
+
headers: defaultHeaders,
|
|
1281
|
+
parseAs: "auto",
|
|
1282
|
+
querySerializer: defaultQuerySerializer,
|
|
1283
|
+
...override
|
|
1284
|
+
});
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/client.gen.ts
|
|
1287
|
+
const createClient = (config = {}) => {
|
|
1288
|
+
let _config = mergeConfigs(createConfig(), config);
|
|
1289
|
+
const getConfig = () => ({ ..._config });
|
|
1290
|
+
const setConfig = (config) => {
|
|
1291
|
+
_config = mergeConfigs(_config, config);
|
|
1292
|
+
return getConfig();
|
|
1293
|
+
};
|
|
1294
|
+
const interceptors = createInterceptors();
|
|
1295
|
+
const beforeRequest = async (options) => {
|
|
1296
|
+
const opts = {
|
|
1297
|
+
..._config,
|
|
1298
|
+
...options,
|
|
1299
|
+
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
1300
|
+
headers: mergeHeaders(_config.headers, options.headers),
|
|
1301
|
+
serializedBody: void 0
|
|
1302
|
+
};
|
|
1303
|
+
if (opts.security) await setAuthParams(opts);
|
|
1304
|
+
if (opts.requestValidator) await opts.requestValidator(opts);
|
|
1305
|
+
if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
|
|
1306
|
+
if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
|
|
1307
|
+
const resolvedOpts = opts;
|
|
1308
|
+
return {
|
|
1309
|
+
opts: resolvedOpts,
|
|
1310
|
+
url: buildUrl(resolvedOpts)
|
|
1311
|
+
};
|
|
1312
|
+
};
|
|
1313
|
+
const request = async (options) => {
|
|
1314
|
+
const throwOnError = options.throwOnError ?? _config.throwOnError;
|
|
1315
|
+
const responseStyle = options.responseStyle ?? _config.responseStyle;
|
|
1316
|
+
let request;
|
|
1317
|
+
let response;
|
|
1318
|
+
try {
|
|
1319
|
+
const { opts, url } = await beforeRequest(options);
|
|
1320
|
+
const requestInit = {
|
|
1321
|
+
redirect: "follow",
|
|
1322
|
+
...opts,
|
|
1323
|
+
body: getValidRequestBody(opts)
|
|
1324
|
+
};
|
|
1325
|
+
request = new Request(url, requestInit);
|
|
1326
|
+
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
1327
|
+
const _fetch = opts.fetch;
|
|
1328
|
+
response = await _fetch(request);
|
|
1329
|
+
for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
|
|
1330
|
+
const result = {
|
|
1331
|
+
request,
|
|
1332
|
+
response
|
|
1333
|
+
};
|
|
1334
|
+
if (response.ok) {
|
|
1335
|
+
const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
|
|
1336
|
+
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
|
1337
|
+
let emptyData;
|
|
1338
|
+
switch (parseAs) {
|
|
1339
|
+
case "arrayBuffer":
|
|
1340
|
+
case "blob":
|
|
1341
|
+
case "text":
|
|
1342
|
+
emptyData = await response[parseAs]();
|
|
1343
|
+
break;
|
|
1344
|
+
case "formData":
|
|
1345
|
+
emptyData = new FormData();
|
|
1346
|
+
break;
|
|
1347
|
+
case "stream":
|
|
1348
|
+
emptyData = response.body;
|
|
1349
|
+
break;
|
|
1350
|
+
default:
|
|
1351
|
+
emptyData = {};
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
return opts.responseStyle === "data" ? emptyData : {
|
|
1355
|
+
data: emptyData,
|
|
1356
|
+
...result
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
let data;
|
|
1360
|
+
switch (parseAs) {
|
|
1361
|
+
case "arrayBuffer":
|
|
1362
|
+
case "blob":
|
|
1363
|
+
case "formData":
|
|
1364
|
+
case "text":
|
|
1365
|
+
data = await response[parseAs]();
|
|
1366
|
+
break;
|
|
1367
|
+
case "json": {
|
|
1368
|
+
const text = await response.text();
|
|
1369
|
+
data = text ? JSON.parse(text) : {};
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case "stream": return opts.responseStyle === "data" ? response.body : {
|
|
1373
|
+
data: response.body,
|
|
1374
|
+
...result
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (parseAs === "json") {
|
|
1378
|
+
if (opts.responseValidator) await opts.responseValidator(data);
|
|
1379
|
+
if (opts.responseTransformer) data = await opts.responseTransformer(data);
|
|
1380
|
+
}
|
|
1381
|
+
return opts.responseStyle === "data" ? data : {
|
|
1382
|
+
data,
|
|
1383
|
+
...result
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
const textError = await response.text();
|
|
1387
|
+
let jsonError;
|
|
1388
|
+
try {
|
|
1389
|
+
jsonError = JSON.parse(textError);
|
|
1390
|
+
} catch {}
|
|
1391
|
+
throw jsonError ?? textError;
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
let finalError = error;
|
|
1394
|
+
for (const fn of interceptors.error.fns) if (fn) finalError = await fn(finalError, response, request, options);
|
|
1395
|
+
finalError = finalError || {};
|
|
1396
|
+
if (throwOnError) throw finalError;
|
|
1397
|
+
return responseStyle === "data" ? void 0 : {
|
|
1398
|
+
error: finalError,
|
|
1399
|
+
request,
|
|
1400
|
+
response
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
const makeMethodFn = (method) => (options) => request({
|
|
1405
|
+
...options,
|
|
1406
|
+
method
|
|
1407
|
+
});
|
|
1408
|
+
const makeSseFn = (method) => async (options) => {
|
|
1409
|
+
const { opts, url } = await beforeRequest(options);
|
|
1410
|
+
return createSseClient({
|
|
1411
|
+
...opts,
|
|
1412
|
+
body: opts.body,
|
|
1413
|
+
method,
|
|
1414
|
+
onRequest: async (url, init) => {
|
|
1415
|
+
let request = new Request(url, init);
|
|
1416
|
+
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
1417
|
+
return request;
|
|
1418
|
+
},
|
|
1419
|
+
serializedBody: getValidRequestBody(opts),
|
|
1420
|
+
url
|
|
1421
|
+
});
|
|
1422
|
+
};
|
|
1423
|
+
const _buildUrl = (options) => buildUrl({
|
|
1424
|
+
..._config,
|
|
1425
|
+
...options
|
|
1426
|
+
});
|
|
1427
|
+
return {
|
|
1428
|
+
buildUrl: _buildUrl,
|
|
1429
|
+
connect: makeMethodFn("CONNECT"),
|
|
1430
|
+
delete: makeMethodFn("DELETE"),
|
|
1431
|
+
get: makeMethodFn("GET"),
|
|
1432
|
+
getConfig,
|
|
1433
|
+
head: makeMethodFn("HEAD"),
|
|
1434
|
+
interceptors,
|
|
1435
|
+
options: makeMethodFn("OPTIONS"),
|
|
1436
|
+
patch: makeMethodFn("PATCH"),
|
|
1437
|
+
post: makeMethodFn("POST"),
|
|
1438
|
+
put: makeMethodFn("PUT"),
|
|
1439
|
+
request,
|
|
1440
|
+
setConfig,
|
|
1441
|
+
sse: {
|
|
1442
|
+
connect: makeSseFn("CONNECT"),
|
|
1443
|
+
delete: makeSseFn("DELETE"),
|
|
1444
|
+
get: makeSseFn("GET"),
|
|
1445
|
+
head: makeSseFn("HEAD"),
|
|
1446
|
+
options: makeSseFn("OPTIONS"),
|
|
1447
|
+
patch: makeSseFn("PATCH"),
|
|
1448
|
+
post: makeSseFn("POST"),
|
|
1449
|
+
put: makeSseFn("PUT"),
|
|
1450
|
+
trace: makeSseFn("TRACE")
|
|
1451
|
+
},
|
|
1452
|
+
trace: makeMethodFn("TRACE")
|
|
1453
|
+
};
|
|
1454
|
+
};
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region ../../api-clients/fluidos/src/hey-api-client.ts
|
|
1457
|
+
function isAbortError(error) {
|
|
1458
|
+
return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
1459
|
+
}
|
|
1460
|
+
function createFluidOsHeyApiClient(config) {
|
|
1461
|
+
const client = createClient({
|
|
1462
|
+
auth: async () => await config.getAuthToken?.() ?? void 0,
|
|
1463
|
+
baseUrl: config.baseUrl,
|
|
1464
|
+
fetch: config.fetch,
|
|
1465
|
+
headers: {
|
|
1466
|
+
Accept: "application/json",
|
|
1467
|
+
...config.defaultHeaders
|
|
1468
|
+
},
|
|
1469
|
+
throwOnError: true
|
|
1470
|
+
});
|
|
1471
|
+
client.interceptors.error.use(createHeyApiErrorInterceptor({
|
|
1472
|
+
onResponseError: (response) => {
|
|
1473
|
+
if (response.status === 401) config.onAuthError?.();
|
|
1474
|
+
},
|
|
1475
|
+
shouldPassThroughTransportError: isAbortError
|
|
1476
|
+
}));
|
|
1477
|
+
return client;
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/utils/auth-client.ts
|
|
1481
|
+
function createAuthenticatedClient() {
|
|
1482
|
+
return { client: createAuthenticatedWidgetPackageClient() };
|
|
1483
|
+
}
|
|
1484
|
+
function createAuthenticatedWidgetPackageClient() {
|
|
1485
|
+
const { profile, profileName, token } = resolveAuthenticatedProfile();
|
|
1486
|
+
if (!token) {
|
|
1487
|
+
if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
1488
|
+
throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
1489
|
+
}
|
|
1490
|
+
return createFluidOsHeyApiClient({
|
|
1491
|
+
baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
|
|
1492
|
+
getAuthToken: () => token
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
function resolveAuthenticatedProfile() {
|
|
1496
|
+
const config = readConfig();
|
|
1497
|
+
const projectConfig = findProjectConfig(process.cwd());
|
|
1498
|
+
if (projectConfig?.profile) {
|
|
1499
|
+
const profile = config.profiles[projectConfig.profile];
|
|
1500
|
+
if (profile) return {
|
|
1501
|
+
profile,
|
|
1502
|
+
profileName: projectConfig.profile,
|
|
1503
|
+
token: profile.token
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (!config.activeProfile) return {};
|
|
1507
|
+
const profile = config.profiles[config.activeProfile];
|
|
1508
|
+
if (!profile) return {};
|
|
1509
|
+
return {
|
|
1510
|
+
profile,
|
|
1511
|
+
profileName: config.activeProfile,
|
|
1512
|
+
token: profile.token
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
//#endregion
|
|
678
1516
|
//#region src/lib/git.ts
|
|
679
1517
|
/**
|
|
680
1518
|
* Git wrapper for the Fluid widget CLI — shells out to the system `git`
|
|
@@ -994,50 +1832,268 @@ async function gitAdoptFetchHead(cwd) {
|
|
|
994
1832
|
throw wrapGitError(err, "adopt remote history");
|
|
995
1833
|
}
|
|
996
1834
|
}
|
|
997
|
-
/** Fast-forward `HEAD` to `FETCH_HEAD`. Fails if not fast-forwardable. */
|
|
998
|
-
async function gitMergeFastForward(cwd) {
|
|
1835
|
+
/** Fast-forward `HEAD` to `FETCH_HEAD`. Fails if not fast-forwardable. */
|
|
1836
|
+
async function gitMergeFastForward(cwd) {
|
|
1837
|
+
try {
|
|
1838
|
+
await execa("git", [
|
|
1839
|
+
"merge",
|
|
1840
|
+
"--ff-only",
|
|
1841
|
+
"FETCH_HEAD"
|
|
1842
|
+
], { cwd });
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
throw wrapGitError(err, "fast-forward merge");
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
/** Are there uncommitted changes (working tree or index) in this repo? */
|
|
1848
|
+
async function hasUncommittedChanges(cwd) {
|
|
1849
|
+
try {
|
|
1850
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
|
|
1851
|
+
return stdout.trim().length > 0;
|
|
1852
|
+
} catch {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
/** Stash working-tree + untracked changes with a tagged message. */
|
|
1857
|
+
async function gitStash(cwd, message) {
|
|
1858
|
+
try {
|
|
1859
|
+
await execa("git", [
|
|
1860
|
+
"stash",
|
|
1861
|
+
"push",
|
|
1862
|
+
"--include-untracked",
|
|
1863
|
+
"--message",
|
|
1864
|
+
message
|
|
1865
|
+
], { cwd });
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
throw wrapGitError(err, "stash push");
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
/** Pop the most recent stash. Throws on conflict so callers can surface it. */
|
|
1871
|
+
async function gitStashPop(cwd) {
|
|
1872
|
+
try {
|
|
1873
|
+
await execa("git", ["stash", "pop"], { cwd });
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
const missing = gitMissingError(err);
|
|
1876
|
+
if (missing) throw missing;
|
|
1877
|
+
throw new GitError("Couldn't restore your local changes after pulling — the edits collided with a teammate's. Run `git stash list` and resolve manually.", err.exitCode ?? 1);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Local-only state that must never reach a widget's provisioned repo.
|
|
1882
|
+
*
|
|
1883
|
+
* `.mist-desktop/` is Mist Desktop's per-machine working state for the
|
|
1884
|
+
* project directory it opened — chat transcripts, user attachments,
|
|
1885
|
+
* screenshots, source baselines, media cache. When Desktop drives
|
|
1886
|
+
* `fluid widget push`, that directory sits in the project root, so a
|
|
1887
|
+
* whole-tree `git add -A` publishes every private conversation.
|
|
1888
|
+
* `.mist` is the sibling clone/slug marker.
|
|
1889
|
+
*
|
|
1890
|
+
* Mirrors the portal CLI (`packages/cli/portal/src/lib/git.ts`) and the
|
|
1891
|
+
* Mist CLI (`packages/cli/mist/src/lib/git.ts`).
|
|
1892
|
+
*/
|
|
1893
|
+
const LOCAL_ONLY_GIT_PATHS = [".mist", ".mist-desktop/"];
|
|
1894
|
+
/**
|
|
1895
|
+
* Append {@link LOCAL_ONLY_GIT_PATHS} to `.git/info/exclude` when absent.
|
|
1896
|
+
*
|
|
1897
|
+
* Why the local exclude and not `.gitignore`: `.gitignore` ships from the
|
|
1898
|
+
* widget starter template, so editing it would never reach the clones
|
|
1899
|
+
* already on people's machines. `.git/info/exclude` is per-checkout, so
|
|
1900
|
+
* writing it on every sync heals a pre-existing clone on its next push.
|
|
1901
|
+
*
|
|
1902
|
+
* Idempotent and best-effort: never throws. {@link gitAutoCommit} also
|
|
1903
|
+
* cleans these paths out of the index after staging.
|
|
1904
|
+
*/
|
|
1905
|
+
function excludeLocalStateFromGit(cwd) {
|
|
1906
|
+
try {
|
|
1907
|
+
const gitDir = path.join(cwd, ".git");
|
|
1908
|
+
if (!existsSync(gitDir)) return;
|
|
1909
|
+
const infoDir = path.join(gitDir, "info");
|
|
1910
|
+
if (!existsSync(infoDir)) mkdirSync(infoDir, { recursive: true });
|
|
1911
|
+
const excludePath = path.join(infoDir, "exclude");
|
|
1912
|
+
const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
1913
|
+
const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
1914
|
+
const missing = LOCAL_ONLY_GIT_PATHS.filter((entry) => !existingLines.has(entry));
|
|
1915
|
+
if (missing.length === 0) return;
|
|
1916
|
+
writeFileSync(excludePath, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
|
|
1917
|
+
} catch {}
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Absolute path of the repository containing `cwd`, or `cwd` when
|
|
1921
|
+
* resolution fails. Every local-state defense anchors here: `git add -A`
|
|
1922
|
+
* stages the WHOLE repository, so a push run from a subdirectory would
|
|
1923
|
+
* otherwise look for `<subdir>/.git`, skip the exclude, and sweep the
|
|
1924
|
+
* root `.mist-desktop/` in anyway.
|
|
1925
|
+
*/
|
|
1926
|
+
async function resolveRepoRoot(cwd) {
|
|
1927
|
+
try {
|
|
1928
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
1929
|
+
const root = stdout.trim();
|
|
1930
|
+
if (root.length > 0) return root;
|
|
1931
|
+
} catch {}
|
|
1932
|
+
return cwd;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Repository-root-anchored {@link excludeLocalStateFromGit}. Run before
|
|
1936
|
+
* any working-tree cleanliness check: while `.mist-desktop/` is neither
|
|
1937
|
+
* tracked nor excluded it shows as untracked in `git status --porcelain`,
|
|
1938
|
+
* so a Desktop-driven push needlessly stashes/pops around a pull.
|
|
1939
|
+
*/
|
|
1940
|
+
async function ensureLocalStateExcluded(cwd) {
|
|
1941
|
+
excludeLocalStateFromGit(await resolveRepoRoot(cwd));
|
|
1942
|
+
}
|
|
1943
|
+
/** Paths currently tracked under `.mist-desktop/`, or [] when none. */
|
|
1944
|
+
async function trackedLocalStatePaths(repoRoot) {
|
|
999
1945
|
try {
|
|
1000
|
-
await execa("git", [
|
|
1001
|
-
"
|
|
1002
|
-
"--
|
|
1003
|
-
"
|
|
1004
|
-
], { cwd });
|
|
1005
|
-
|
|
1006
|
-
|
|
1946
|
+
const { stdout } = await execa("git", [
|
|
1947
|
+
"ls-files",
|
|
1948
|
+
"--",
|
|
1949
|
+
".mist-desktop"
|
|
1950
|
+
], { cwd: repoRoot });
|
|
1951
|
+
return stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
1952
|
+
} catch {
|
|
1953
|
+
return [];
|
|
1007
1954
|
}
|
|
1008
1955
|
}
|
|
1009
|
-
/**
|
|
1010
|
-
|
|
1956
|
+
/**
|
|
1957
|
+
* Untrack private Mist Desktop state and, if anything was tracked,
|
|
1958
|
+
* commit ONLY that removal so the next push deletes it from the remote.
|
|
1959
|
+
* Returns true when a purge commit was made.
|
|
1960
|
+
*
|
|
1961
|
+
* Must run regardless of working-tree cleanliness AND of `--no-commit`:
|
|
1962
|
+
* an older CLI could have committed chats to a checkout that is now
|
|
1963
|
+
* clean, and gating on either would skip the cleanup forever, leaving
|
|
1964
|
+
* the chats published.
|
|
1965
|
+
*
|
|
1966
|
+
* The commit is built through a THROWAWAY index (`GIT_INDEX_FILE`), not
|
|
1967
|
+
* the user's: `read-tree HEAD` → `rm --cached` → `write-tree` →
|
|
1968
|
+
* `commit-tree` → `update-ref`. A plain `git commit` would capture the
|
|
1969
|
+
* user's entire index, so anything they had pre-staged would be swept
|
|
1970
|
+
* into the purge commit and pushed — the exact thing `--no-commit`
|
|
1971
|
+
* exists to prevent. A pathspec commit (`git commit -- .mist-desktop`)
|
|
1972
|
+
* is also wrong: with `--only` git takes the *working tree* content of
|
|
1973
|
+
* those paths, and the chats are still on disk, so it would re-add
|
|
1974
|
+
* exactly what we are removing.
|
|
1975
|
+
*
|
|
1976
|
+
* The user's real index then gets the same scoped `rm --cached`, so any
|
|
1977
|
+
* other staged work stays staged and the working tree is never touched.
|
|
1978
|
+
*/
|
|
1979
|
+
async function purgeLocalStateFromIndex(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
|
|
1980
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
1981
|
+
if ((await trackedLocalStatePaths(repoRoot)).length === 0) return false;
|
|
1982
|
+
let head;
|
|
1011
1983
|
try {
|
|
1012
|
-
const { stdout } = await execa("git", ["
|
|
1013
|
-
|
|
1984
|
+
const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
|
|
1985
|
+
head = stdout.trim();
|
|
1986
|
+
if (!head) return false;
|
|
1014
1987
|
} catch {
|
|
1015
1988
|
return false;
|
|
1016
1989
|
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1990
|
+
const tmpIndex = path.join(os.tmpdir(), `fluid-widget-purge-${process.pid}-${randomUUID()}.index`);
|
|
1991
|
+
const indexEnv = {
|
|
1992
|
+
...process.env,
|
|
1993
|
+
GIT_INDEX_FILE: tmpIndex
|
|
1994
|
+
};
|
|
1995
|
+
let commitSha;
|
|
1020
1996
|
try {
|
|
1997
|
+
await execa("git", ["read-tree", head], {
|
|
1998
|
+
cwd: repoRoot,
|
|
1999
|
+
env: indexEnv
|
|
2000
|
+
});
|
|
1021
2001
|
await execa("git", [
|
|
1022
|
-
"
|
|
1023
|
-
"
|
|
1024
|
-
"
|
|
1025
|
-
"--
|
|
2002
|
+
"rm",
|
|
2003
|
+
"-q",
|
|
2004
|
+
"-r",
|
|
2005
|
+
"--cached",
|
|
2006
|
+
"--ignore-unmatch",
|
|
2007
|
+
"--",
|
|
2008
|
+
".mist-desktop"
|
|
2009
|
+
], {
|
|
2010
|
+
cwd: repoRoot,
|
|
2011
|
+
env: indexEnv
|
|
2012
|
+
});
|
|
2013
|
+
const { stdout: tree } = await execa("git", ["write-tree"], {
|
|
2014
|
+
cwd: repoRoot,
|
|
2015
|
+
env: indexEnv
|
|
2016
|
+
});
|
|
2017
|
+
const { stdout: sha } = await execa("git", [
|
|
2018
|
+
"commit-tree",
|
|
2019
|
+
tree.trim(),
|
|
2020
|
+
"-p",
|
|
2021
|
+
head,
|
|
2022
|
+
"-m",
|
|
1026
2023
|
message
|
|
1027
|
-
], {
|
|
2024
|
+
], {
|
|
2025
|
+
cwd: repoRoot,
|
|
2026
|
+
env: {
|
|
2027
|
+
...indexEnv,
|
|
2028
|
+
...await identityEnv(repoRoot, identity)
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
commitSha = sha.trim();
|
|
2032
|
+
if (!commitSha) return false;
|
|
1028
2033
|
} catch (err) {
|
|
1029
|
-
|
|
2034
|
+
const missing = gitMissingError(err);
|
|
2035
|
+
if (missing) throw missing;
|
|
2036
|
+
throw wrapGitError(err, "purge local state");
|
|
2037
|
+
} finally {
|
|
2038
|
+
rmSync(tmpIndex, { force: true });
|
|
1030
2039
|
}
|
|
1031
|
-
}
|
|
1032
|
-
/** Pop the most recent stash. Throws on conflict so callers can surface it. */
|
|
1033
|
-
async function gitStashPop(cwd) {
|
|
1034
2040
|
try {
|
|
1035
|
-
await execa("git", [
|
|
2041
|
+
await execa("git", [
|
|
2042
|
+
"update-ref",
|
|
2043
|
+
"HEAD",
|
|
2044
|
+
commitSha,
|
|
2045
|
+
head
|
|
2046
|
+
], { cwd: repoRoot });
|
|
1036
2047
|
} catch (err) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
2048
|
+
throw wrapGitError(err, "purge local state");
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
await execa("git", [
|
|
2052
|
+
"rm",
|
|
2053
|
+
"-q",
|
|
2054
|
+
"-r",
|
|
2055
|
+
"--cached",
|
|
2056
|
+
"--ignore-unmatch",
|
|
2057
|
+
"--",
|
|
2058
|
+
".mist-desktop"
|
|
2059
|
+
], { cwd: repoRoot });
|
|
2060
|
+
} catch {}
|
|
2061
|
+
return true;
|
|
2062
|
+
}
|
|
2063
|
+
/** `GIT_{AUTHOR,COMMITTER}_*` overrides for `commit-tree`, which (unlike
|
|
2064
|
+
* `git commit`) fails outright rather than prompting when the repo has
|
|
2065
|
+
* no identity. Only set what the repo itself doesn't define. */
|
|
2066
|
+
async function identityEnv(cwd, identity) {
|
|
2067
|
+
const env = {};
|
|
2068
|
+
const has = async (key) => {
|
|
2069
|
+
try {
|
|
2070
|
+
return (await execa("git", ["config", key], { cwd })).stdout.trim().length > 0;
|
|
2071
|
+
} catch {
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
if (!await has("user.name")) {
|
|
2076
|
+
env["GIT_AUTHOR_NAME"] = identity.name;
|
|
2077
|
+
env["GIT_COMMITTER_NAME"] = identity.name;
|
|
2078
|
+
}
|
|
2079
|
+
if (!await has("user.email")) {
|
|
2080
|
+
env["GIT_AUTHOR_EMAIL"] = identity.email;
|
|
2081
|
+
env["GIT_COMMITTER_EMAIL"] = identity.email;
|
|
1040
2082
|
}
|
|
2083
|
+
return env;
|
|
2084
|
+
}
|
|
2085
|
+
/** Shared `-c user.*` args: prefer the repo's configured identity, else
|
|
2086
|
+
* the supplied synthetic one so the commit isn't rejected. */
|
|
2087
|
+
async function resolveIdentityArgs(cwd, identity) {
|
|
2088
|
+
let hasName = false;
|
|
2089
|
+
let hasEmail = false;
|
|
2090
|
+
try {
|
|
2091
|
+
hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
|
|
2092
|
+
} catch {}
|
|
2093
|
+
try {
|
|
2094
|
+
hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
|
|
2095
|
+
} catch {}
|
|
2096
|
+
return [...hasName ? [] : ["-c", `user.name=${identity.name}`], ...hasEmail ? [] : ["-c", `user.email=${identity.email}`]];
|
|
1041
2097
|
}
|
|
1042
2098
|
const DEFAULT_COMMIT_IDENTITY = {
|
|
1043
2099
|
name: "Fluid CLI",
|
|
@@ -1051,22 +2107,54 @@ const DEFAULT_COMMIT_IDENTITY = {
|
|
|
1051
2107
|
* commit per push. If the repo has no configured user.name / user.email we
|
|
1052
2108
|
* pass a synthetic identity so the commit isn't rejected. "nothing to commit"
|
|
1053
2109
|
* is treated as a no-op rather than an error.
|
|
2110
|
+
*
|
|
2111
|
+
* Scope: `add -A` stages the whole work tree, bounded by the project
|
|
2112
|
+
* `.gitignore` — which can't cover tool-local state written INTO the
|
|
2113
|
+
* project by whatever opened it (Mist Desktop's `.mist-desktop/` chats and
|
|
2114
|
+
* the `.mist` marker). Belt and suspenders, all repo-root-anchored:
|
|
2115
|
+
*
|
|
2116
|
+
* 1. {@link excludeLocalStateFromGit} — idempotent `.git/info/exclude`
|
|
2117
|
+
* write so `git add` never sees the paths. Heals old clones.
|
|
2118
|
+
* 2. `git reset -- ':(top).mist' .mist` — unstages the marker if step 1
|
|
2119
|
+
* silently failed. We deliberately do NOT pass `:(exclude)` to
|
|
2120
|
+
* `git add`: when the path is also in `.git/info/exclude`, git exits
|
|
2121
|
+
* 1 with "paths are ignored by one of your .gitignore files", which
|
|
2122
|
+
* would turn the already-healed case into a hard failure.
|
|
2123
|
+
* 3. `git rm --cached -r --ignore-unmatch .mist-desktop` — untracks
|
|
2124
|
+
* chats an older push already committed (ignore rules can't untrack).
|
|
2125
|
+
* `--cached` keeps every file on disk.
|
|
2126
|
+
*
|
|
2127
|
+
* Steps 2 and 3 are best-effort and never fail the push.
|
|
1054
2128
|
*/
|
|
1055
2129
|
async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
|
|
2130
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
2131
|
+
excludeLocalStateFromGit(repoRoot);
|
|
1056
2132
|
try {
|
|
1057
2133
|
await execa("git", ["add", "-A"], { cwd });
|
|
1058
2134
|
} catch (err) {
|
|
1059
2135
|
throw wrapGitError(err, "add");
|
|
1060
2136
|
}
|
|
1061
|
-
let hasName = false;
|
|
1062
|
-
let hasEmail = false;
|
|
1063
2137
|
try {
|
|
1064
|
-
|
|
2138
|
+
await execa("git", [
|
|
2139
|
+
"reset",
|
|
2140
|
+
"-q",
|
|
2141
|
+
"--",
|
|
2142
|
+
":(top).mist",
|
|
2143
|
+
".mist"
|
|
2144
|
+
], { cwd });
|
|
1065
2145
|
} catch {}
|
|
1066
2146
|
try {
|
|
1067
|
-
|
|
2147
|
+
await execa("git", [
|
|
2148
|
+
"rm",
|
|
2149
|
+
"-q",
|
|
2150
|
+
"-r",
|
|
2151
|
+
"--cached",
|
|
2152
|
+
"--ignore-unmatch",
|
|
2153
|
+
"--",
|
|
2154
|
+
".mist-desktop"
|
|
2155
|
+
], { cwd: repoRoot });
|
|
1068
2156
|
} catch {}
|
|
1069
|
-
const identityArgs =
|
|
2157
|
+
const identityArgs = await resolveIdentityArgs(cwd, identity);
|
|
1070
2158
|
try {
|
|
1071
2159
|
await execa("git", [
|
|
1072
2160
|
...identityArgs,
|
|
@@ -1091,6 +2179,10 @@ async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
|
|
|
1091
2179
|
}
|
|
1092
2180
|
//#endregion
|
|
1093
2181
|
//#region src/utils/git-credential.ts
|
|
2182
|
+
function unwrapResponseData(response) {
|
|
2183
|
+
if (response !== null && typeof response === "object" && "data" in response && !("git_credential" in response)) return response.data;
|
|
2184
|
+
return response;
|
|
2185
|
+
}
|
|
1094
2186
|
/**
|
|
1095
2187
|
* Mint a git credential for a widget package.
|
|
1096
2188
|
*
|
|
@@ -1103,8 +2195,13 @@ async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
|
|
|
1103
2195
|
* decide whether to degrade gracefully (see `isGitSyncUnavailable`).
|
|
1104
2196
|
*/
|
|
1105
2197
|
async function mintWidgetGitCredential(options) {
|
|
1106
|
-
const
|
|
1107
|
-
|
|
2198
|
+
const response = unwrapResponseData(options.droplet ? await options.client.post({
|
|
2199
|
+
responseStyle: "data",
|
|
2200
|
+
url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package/git_credential`
|
|
2201
|
+
}) : await fluidOsV0WidgetPackageGitCredentialCreate({
|
|
2202
|
+
client: options.client,
|
|
2203
|
+
responseStyle: "data"
|
|
2204
|
+
}));
|
|
1108
2205
|
const remoteUrl = response?.git_credential?.remote_url;
|
|
1109
2206
|
if (typeof remoteUrl !== "string" || remoteUrl.length === 0) throw new Error("The git credential response did not include a remote URL. Update the Fluid CLI, or contact support if this persists.");
|
|
1110
2207
|
const expiresAtRaw = response.git_credential?.expires_at;
|
|
@@ -1235,138 +2332,6 @@ const cloneCommand = new Command("clone").description("Clone a widget package's
|
|
|
1235
2332
|
console.log(chalk.dim(` cd ${path.relative(process.cwd(), targetDir) || "."}`));
|
|
1236
2333
|
if (options.skipInstall === true) console.log(chalk.dim(` ${installCommand}`));
|
|
1237
2334
|
}));
|
|
1238
|
-
//#endregion
|
|
1239
|
-
//#region src/utils/template.ts
|
|
1240
|
-
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.461";
|
|
1241
|
-
const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
|
|
1242
|
-
const EMBEDDED_CLI_WIDGET_VERSION = "0.1.27";
|
|
1243
|
-
function getDefaultTemplatePath() {
|
|
1244
|
-
return path.join(findPackageRoot(), "templates", "default");
|
|
1245
|
-
}
|
|
1246
|
-
async function directoryExists(filePath) {
|
|
1247
|
-
try {
|
|
1248
|
-
return (await stat(filePath)).isDirectory();
|
|
1249
|
-
} catch {
|
|
1250
|
-
return false;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
async function copyWidgetTemplate(templatePath, targetPath, variables) {
|
|
1254
|
-
const files = await getFiles(templatePath);
|
|
1255
|
-
for (const file of files) {
|
|
1256
|
-
const sourcePath = path.join(templatePath, file);
|
|
1257
|
-
const isTemplate = file.endsWith(".template");
|
|
1258
|
-
const outputFiles = getOutputFilenames(file);
|
|
1259
|
-
const content = await readFile(sourcePath, "utf-8");
|
|
1260
|
-
await writeOutputFiles(targetPath, outputFiles, isTemplate ? renderTemplate(content, variables) : content);
|
|
1261
|
-
}
|
|
1262
|
-
for (const skillFile of await getSharedTemplateSkillFiles()) await writeOutputFiles(targetPath, getOutputFilenames(skillFile.relativePath), skillFile.content);
|
|
1263
|
-
}
|
|
1264
|
-
async function getPortalSdkVersion(options = {}) {
|
|
1265
|
-
return getWorkspacePackageVersion(["portal", "sdk"], EMBEDDED_PORTAL_SDK_VERSION, options);
|
|
1266
|
-
}
|
|
1267
|
-
async function getPortalCoreVersion(options = {}) {
|
|
1268
|
-
return getWorkspacePackageVersion(["portal", "core"], void 0, options);
|
|
1269
|
-
}
|
|
1270
|
-
async function getCliVersion(options = {}) {
|
|
1271
|
-
return getWorkspacePackageVersion(["cli", "core"], EMBEDDED_CLI_CORE_VERSION, options);
|
|
1272
|
-
}
|
|
1273
|
-
async function getWidgetCliVersion(options = {}) {
|
|
1274
|
-
return getWorkspacePackageVersion(["cli", "widget"], EMBEDDED_CLI_WIDGET_VERSION, options);
|
|
1275
|
-
}
|
|
1276
|
-
async function getWorkspacePackageVersion(packagePathSegments, embeddedVersion, options) {
|
|
1277
|
-
try {
|
|
1278
|
-
const packageRoot = findPackageRoot();
|
|
1279
|
-
const packagesRoot = path.join(packageRoot, "..", "..");
|
|
1280
|
-
const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
|
|
1281
|
-
if (options.local) return `link:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot).replace(/\\/g, "/") || "."}`;
|
|
1282
|
-
if (embeddedVersion) return embeddedVersion;
|
|
1283
|
-
const packageJsonPath = path.join(targetPackageRoot, "package.json");
|
|
1284
|
-
const content = await readFile(packageJsonPath, "utf-8");
|
|
1285
|
-
const pkg = JSON.parse(content);
|
|
1286
|
-
if (typeof pkg.version !== "string") throw new Error(`Missing version in ${packageJsonPath}`);
|
|
1287
|
-
return pkg.version;
|
|
1288
|
-
} catch (error) {
|
|
1289
|
-
if (embeddedVersion) return embeddedVersion;
|
|
1290
|
-
throw new Error(`Could not resolve the exact template package version for ${packagePathSegments.join("/")}`, { cause: error });
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
async function getFiles(dir, baseDir = dir) {
|
|
1294
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
1295
|
-
const files = [];
|
|
1296
|
-
for (const entry of entries) {
|
|
1297
|
-
const fullPath = path.join(dir, entry.name);
|
|
1298
|
-
if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
|
|
1299
|
-
else files.push(path.relative(baseDir, fullPath));
|
|
1300
|
-
}
|
|
1301
|
-
return files;
|
|
1302
|
-
}
|
|
1303
|
-
async function getSharedTemplateSkillFiles() {
|
|
1304
|
-
const packageRoot = findFluidCliPackageRoot();
|
|
1305
|
-
const skillsRoot = path.join(packageRoot, "template-skills");
|
|
1306
|
-
const files = await getFiles(skillsRoot);
|
|
1307
|
-
return Promise.all(files.map(async (file) => ({
|
|
1308
|
-
relativePath: path.join("skills", file),
|
|
1309
|
-
content: await readFile(path.join(skillsRoot, file), "utf-8")
|
|
1310
|
-
})));
|
|
1311
|
-
}
|
|
1312
|
-
function findFluidCliPackageRoot() {
|
|
1313
|
-
const workspacePackageRoot = path.join(findPackageRoot(), "..", "core");
|
|
1314
|
-
if (existsSync(path.join(workspacePackageRoot, "template-skills"))) return workspacePackageRoot;
|
|
1315
|
-
let dir = path.dirname(fileURLToPath(import.meta.resolve("@fluid-app/fluid-cli")));
|
|
1316
|
-
while (!existsSync(path.join(dir, "package.json"))) {
|
|
1317
|
-
const parent = path.dirname(dir);
|
|
1318
|
-
if (parent === dir) throw new Error("Could not find Fluid CLI package root");
|
|
1319
|
-
dir = parent;
|
|
1320
|
-
}
|
|
1321
|
-
return dir;
|
|
1322
|
-
}
|
|
1323
|
-
function renderTemplate(content, variables) {
|
|
1324
|
-
return content.replace(/{{\s*#if\s+([a-zA-Z0-9_]+)\s*}}([\s\S]*?){{\s*\/if\s*}}/g, (match, key, block) => {
|
|
1325
|
-
if (!isTemplateVariableKey(key)) return match;
|
|
1326
|
-
return variables[key] ? String(block) : "";
|
|
1327
|
-
}).replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
|
|
1328
|
-
if (isTemplateVariableKey(key)) return variables[key] ?? "";
|
|
1329
|
-
return match;
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
function isTemplateVariableKey(key) {
|
|
1333
|
-
return [
|
|
1334
|
-
"projectName",
|
|
1335
|
-
"widgetScope",
|
|
1336
|
-
"widgetPackageVersion",
|
|
1337
|
-
"sdkVersion",
|
|
1338
|
-
"localCoreVersion",
|
|
1339
|
-
"fluidCliVersion",
|
|
1340
|
-
"widgetCliVersion",
|
|
1341
|
-
"droplet"
|
|
1342
|
-
].includes(key);
|
|
1343
|
-
}
|
|
1344
|
-
async function writeOutputFiles(targetPath, outputFiles, content) {
|
|
1345
|
-
for (const outputFile of outputFiles) {
|
|
1346
|
-
const destinationPath = path.join(targetPath, outputFile);
|
|
1347
|
-
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
1348
|
-
await writeFile(destinationPath, content, "utf-8");
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
function getOutputFilenames(filename) {
|
|
1352
|
-
const outputFilename = getOutputFilename(filename);
|
|
1353
|
-
const normalized = outputFilename.replace(/\\/g, "/");
|
|
1354
|
-
if (normalized === "AGENTS.md") return ["AGENTS.md", "CLAUDE.md"];
|
|
1355
|
-
if (normalized.startsWith("skills/")) return [path.join(".agents", outputFilename), path.join(".claude", outputFilename)];
|
|
1356
|
-
return [outputFilename];
|
|
1357
|
-
}
|
|
1358
|
-
function getOutputFilename(filename) {
|
|
1359
|
-
return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
|
|
1360
|
-
}
|
|
1361
|
-
function findPackageRoot() {
|
|
1362
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1363
|
-
while (!existsSync(path.join(dir, "package.json"))) {
|
|
1364
|
-
const parent = path.dirname(dir);
|
|
1365
|
-
if (parent === dir) throw new Error("Could not find widget CLI package root");
|
|
1366
|
-
dir = parent;
|
|
1367
|
-
}
|
|
1368
|
-
return dir;
|
|
1369
|
-
}
|
|
1370
2335
|
/**
|
|
1371
2336
|
* The template repo to clone. Overridable via `FLUID_WIDGET_TEMPLATE_REPO`
|
|
1372
2337
|
* (read at call time so tests can point at a local repo). Falls back to the
|
|
@@ -1697,11 +2662,6 @@ function isRecord$2(value) {
|
|
|
1697
2662
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1698
2663
|
}
|
|
1699
2664
|
//#endregion
|
|
1700
|
-
//#region src/utils/auth-client.ts
|
|
1701
|
-
function createAuthenticatedClient() {
|
|
1702
|
-
return { client: createAuthenticatedWidgetPackageClient() };
|
|
1703
|
-
}
|
|
1704
|
-
//#endregion
|
|
1705
2665
|
//#region src/utils/droplets.ts
|
|
1706
2666
|
const MAX_DROPLET_PAGES = 100;
|
|
1707
2667
|
async function promptForDroplet(client) {
|
|
@@ -1720,10 +2680,18 @@ async function promptForDroplet(client) {
|
|
|
1720
2680
|
}
|
|
1721
2681
|
async function listDroplets(client) {
|
|
1722
2682
|
try {
|
|
1723
|
-
const firstResponse = await client.get(
|
|
2683
|
+
const firstResponse = await client.get({
|
|
2684
|
+
query: { page: 1 },
|
|
2685
|
+
responseStyle: "data",
|
|
2686
|
+
url: "/api/droplets"
|
|
2687
|
+
});
|
|
1724
2688
|
const totalPages = Math.min(readTotalPages(firstResponse) ?? 1, MAX_DROPLET_PAGES);
|
|
1725
2689
|
const responses = [firstResponse];
|
|
1726
|
-
for (let page = 2; page <= totalPages; page += 1) responses.push(await client.get(
|
|
2690
|
+
for (let page = 2; page <= totalPages; page += 1) responses.push(await client.get({
|
|
2691
|
+
query: { page },
|
|
2692
|
+
responseStyle: "data",
|
|
2693
|
+
url: "/api/droplets"
|
|
2694
|
+
}));
|
|
1727
2695
|
return responses.flatMap(normalizeDropletsResponse);
|
|
1728
2696
|
} catch (err) {
|
|
1729
2697
|
if (isApiLikeError(err)) {
|
|
@@ -1820,10 +2788,17 @@ const linkCommand = new Command("link").description("Link this widget project to
|
|
|
1820
2788
|
//#endregion
|
|
1821
2789
|
//#region src/utils/droplet-api.ts
|
|
1822
2790
|
async function fetchDropletWidgetStatus(options) {
|
|
1823
|
-
return extractArray(await options.client.get(
|
|
2791
|
+
return extractArray(await options.client.get({
|
|
2792
|
+
responseStyle: "data",
|
|
2793
|
+
url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions`
|
|
2794
|
+
})).map(normalizeVersionSummary);
|
|
1824
2795
|
}
|
|
1825
2796
|
async function fetchDropletWidgetLogs(options) {
|
|
1826
|
-
return extractArray(await options.client.get(
|
|
2797
|
+
return extractArray(await options.client.get({
|
|
2798
|
+
query: options.limit ? { limit: options.limit } : void 0,
|
|
2799
|
+
responseStyle: "data",
|
|
2800
|
+
url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions/logs`
|
|
2801
|
+
})).map(normalizeLogEntry);
|
|
1827
2802
|
}
|
|
1828
2803
|
function formatStatusRows(versions) {
|
|
1829
2804
|
if (versions.length === 0) return "No widget package versions found.";
|
|
@@ -1955,29 +2930,7 @@ function parseLimit(value) {
|
|
|
1955
2930
|
}
|
|
1956
2931
|
//#endregion
|
|
1957
2932
|
//#region src/commands/publish.ts
|
|
1958
|
-
async function publishWidgetPackage(options
|
|
1959
|
-
if (dependencies) {
|
|
1960
|
-
await dependencies.buildWidgetPackage({
|
|
1961
|
-
projectDir: options.projectDir,
|
|
1962
|
-
outDir: options.outDir
|
|
1963
|
-
});
|
|
1964
|
-
const dryRun = options.dryRun === true;
|
|
1965
|
-
if (dryRun) return {
|
|
1966
|
-
dryRun,
|
|
1967
|
-
uploaded: false,
|
|
1968
|
-
outDir: options.outDir
|
|
1969
|
-
};
|
|
1970
|
-
await dependencies.uploadWidgetPackage({
|
|
1971
|
-
outDir: options.outDir,
|
|
1972
|
-
droplet: options.droplet,
|
|
1973
|
-
client: options.client
|
|
1974
|
-
});
|
|
1975
|
-
return {
|
|
1976
|
-
dryRun,
|
|
1977
|
-
uploaded: true,
|
|
1978
|
-
outDir: options.outDir
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
2933
|
+
async function publishWidgetPackage(options) {
|
|
1981
2934
|
const droplet = await resolvePublishDroplet(options.projectDir, options.droplet);
|
|
1982
2935
|
await validateDropletWidgetProject(options.projectDir, droplet);
|
|
1983
2936
|
if (options.skipTypecheck !== true) await runPackageScriptIfAvailable({
|
|
@@ -1985,16 +2938,11 @@ async function publishWidgetPackage(options, dependencies) {
|
|
|
1985
2938
|
scriptName: "typecheck"
|
|
1986
2939
|
});
|
|
1987
2940
|
const dryRun = options.dryRun === true;
|
|
1988
|
-
await withRootWidgetConfigBridge(options.projectDir, () =>
|
|
2941
|
+
await withRootWidgetConfigBridge(options.projectDir, () => publishDropletWidgetPackageArtifacts({
|
|
1989
2942
|
projectDir: options.projectDir,
|
|
1990
2943
|
outDir: options.outDir,
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
kind: "droplet",
|
|
1994
|
-
uuid: droplet
|
|
1995
|
-
},
|
|
1996
|
-
dryRun,
|
|
1997
|
-
...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
|
|
2944
|
+
droplet,
|
|
2945
|
+
dryRun
|
|
1998
2946
|
}), droplet);
|
|
1999
2947
|
return {
|
|
2000
2948
|
dryRun,
|
|
@@ -2029,16 +2977,11 @@ const publishCommand = new Command("publish").description("Publish a Fluid widge
|
|
|
2029
2977
|
else typecheckSpinner.info("No typecheck script found; skipping");
|
|
2030
2978
|
}
|
|
2031
2979
|
const publishSpinner = ora(dryRun ? "Building and preparing dry-run upload payload..." : "Building and publishing widget artifacts...").start();
|
|
2032
|
-
const result = await withRootWidgetConfigBridge(projectDir, () =>
|
|
2980
|
+
const result = await withRootWidgetConfigBridge(projectDir, () => publishDropletWidgetPackageArtifacts({
|
|
2033
2981
|
projectDir,
|
|
2034
2982
|
outDir,
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
kind: "droplet",
|
|
2038
|
-
uuid: droplet
|
|
2039
|
-
},
|
|
2040
|
-
dryRun,
|
|
2041
|
-
...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
|
|
2983
|
+
droplet,
|
|
2984
|
+
dryRun
|
|
2042
2985
|
}), droplet);
|
|
2043
2986
|
publishSpinner.succeed(dryRun ? "Prepared dry-run upload payload" : "Published widget package");
|
|
2044
2987
|
if (dryRun) {
|
|
@@ -2064,6 +3007,47 @@ async function resolvePublishDroplet(projectDir, override) {
|
|
|
2064
3007
|
}
|
|
2065
3008
|
throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
|
|
2066
3009
|
}
|
|
3010
|
+
async function publishDropletWidgetPackageArtifacts(options) {
|
|
3011
|
+
const common = {
|
|
3012
|
+
projectDir: options.projectDir,
|
|
3013
|
+
outDir: options.outDir,
|
|
3014
|
+
owner: "droplet",
|
|
3015
|
+
uploadOwner: {
|
|
3016
|
+
kind: "droplet",
|
|
3017
|
+
uuid: options.droplet
|
|
3018
|
+
},
|
|
3019
|
+
cliVersion: await getWidgetCliVersion()
|
|
3020
|
+
};
|
|
3021
|
+
if (options.dryRun) return publishWidgetPackage$1({
|
|
3022
|
+
...common,
|
|
3023
|
+
dryRun: true
|
|
3024
|
+
});
|
|
3025
|
+
return publishWidgetPackage$1({
|
|
3026
|
+
...common,
|
|
3027
|
+
dryRun: false,
|
|
3028
|
+
client: createAuthenticatedWidgetPackageClient()
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
function printRuntimeOnlyNotice() {
|
|
3032
|
+
console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
|
|
3033
|
+
console.log();
|
|
3034
|
+
}
|
|
3035
|
+
function printDryRunSessionPayload(result) {
|
|
3036
|
+
console.log(chalk.bold("Dry-run upload session payload:"));
|
|
3037
|
+
console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
|
|
3038
|
+
console.log();
|
|
3039
|
+
}
|
|
3040
|
+
function printWidgetPublishSummary(options) {
|
|
3041
|
+
console.log();
|
|
3042
|
+
console.log(chalk.green.bold(`${options.title} complete`));
|
|
3043
|
+
console.log();
|
|
3044
|
+
console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
|
|
3045
|
+
console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
|
|
3046
|
+
console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
|
|
3047
|
+
console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
|
|
3048
|
+
console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
|
|
3049
|
+
console.log();
|
|
3050
|
+
}
|
|
2067
3051
|
//#endregion
|
|
2068
3052
|
//#region src/utils/me.ts
|
|
2069
3053
|
function str(value) {
|
|
@@ -2071,7 +3055,10 @@ function str(value) {
|
|
|
2071
3055
|
return null;
|
|
2072
3056
|
}
|
|
2073
3057
|
async function fetchCurrentUser(client) {
|
|
2074
|
-
const raw = await client.get(
|
|
3058
|
+
const raw = await client.get({
|
|
3059
|
+
responseStyle: "data",
|
|
3060
|
+
url: "/api/me"
|
|
3061
|
+
});
|
|
2075
3062
|
const composed = [str(raw.first_name), str(raw.last_name)].filter((part) => part !== null).join(" ").trim();
|
|
2076
3063
|
const idValue = str(raw.public_id) ?? (typeof raw.id === "number" && Number.isFinite(raw.id) ? String(raw.id) : null);
|
|
2077
3064
|
return {
|
|
@@ -2095,6 +3082,7 @@ async function syncWidgetGitRepo(options) {
|
|
|
2095
3082
|
const { cwd, client } = options;
|
|
2096
3083
|
const branch = options.branch ?? "main";
|
|
2097
3084
|
if (!await isGitRepository(cwd)) await gitInit(cwd, branch);
|
|
3085
|
+
await ensureLocalStateExcluded(cwd);
|
|
2098
3086
|
const cred = await mintWidgetGitCredential({
|
|
2099
3087
|
client,
|
|
2100
3088
|
droplet: options.droplet
|
|
@@ -2121,6 +3109,7 @@ async function syncWidgetGitRepo(options) {
|
|
|
2121
3109
|
}
|
|
2122
3110
|
} else pulled = await gitAdoptFetchHead(cwd);
|
|
2123
3111
|
}
|
|
3112
|
+
await purgeLocalStateFromIndex(cwd, "Remove private Mist Desktop state from the widget repository", await resolveCommitIdentity(client));
|
|
2124
3113
|
let committed = false;
|
|
2125
3114
|
if (options.commit !== false && await hasUncommittedChanges(cwd)) {
|
|
2126
3115
|
await gitAutoCommit(cwd, await buildCommitMessage(client, options.message), await resolveCommitIdentity(client));
|