@fluid-app/fluid-cli-portal 0.1.51 → 0.1.53
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 +3 -0
- package/dist/build-manifest.d.mts +91 -0
- package/dist/build-manifest.d.mts.map +1 -0
- package/dist/build-manifest.mjs +180 -0
- package/dist/build-manifest.mjs.map +1 -0
- package/dist/index.d.mts +234 -519
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1492 -570
- package/dist/index.mjs.map +1 -1
- package/dist/portal-dev-plugin-ByYelgFg.mjs +232 -0
- package/dist/portal-dev-plugin-ByYelgFg.mjs.map +1 -0
- package/dist/{pull-XOsFaqz2.mjs → pull-C3yj9L07.mjs} +44 -16
- package/dist/pull-C3yj9L07.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +12 -56
- package/dist/vite-plugin.d.mts.map +1 -1
- package/dist/vite-plugin.mjs +3 -2
- package/package.json +11 -4
- package/templates/base/.gitignore.template +6 -0
- package/templates/base/.oxlintrc.json +1 -2
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +126 -10
- package/templates/starter/.env.example +4 -3
- package/templates/starter/README.md.template +14 -1
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs +0 -289
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs.map +0 -1
- package/dist/pull-XOsFaqz2.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as fluid_os_v0_create_fluid_ostheme, B as fluid_os_v0_list_fluid_osversions, C as writeMappings, D as fluid_os_v0_create_fluid_osnavigation_item, E as fluid_os_v0_create_fluid_osnavigation, F as fluid_os_v0_delete_fluid_osprofile, G as fluid_os_v0_update_fluid_ostheme, H as fluid_os_v0_update_fluid_osnavigation_item, I as fluid_os_v0_delete_fluid_osscreen, K as fluid_os_v0_update_fluid_osversion, L as fluid_os_v0_delete_fluid_ostheme, M as fluid_os_v0_create_widget_package_version, N as fluid_os_v0_delete_fluid_osnavigation, O as fluid_os_v0_create_fluid_osprofile, P as fluid_os_v0_delete_fluid_osnavigation_item, R as fluid_os_v0_list_fluid_osdefinitions, S as updateMapping, T as fluid_os_v0_complete_widget_package_version_upload, U as fluid_os_v0_update_fluid_osprofile, V as fluid_os_v0_update_fluid_osnavigation, W as fluid_os_v0_update_fluid_osscreen, _ as deriveSlug, a as buildThemeIdToSlugMap, b as resolveIdToSlug, c as transformNavigationItems, d as transformTheme, f as buildSnapshot, g as writeSnapshot, h as readSnapshot, i as buildNavigationIdToSlugMap, j as fluid_os_v0_create_fluid_osversion, k as fluid_os_v0_create_fluid_osscreen, l as transformProfile, m as diffAgainstSnapshot, o as deriveScreenSlug, p as computeFileHash, q as createFetchClient, r as buildIdToSlugMap, s as transformNavigation, t as pullCommand, u as transformScreen, v as readMappings, w as atomicWriteFile, x as resolveSlugToId, y as removeMapping, z as fluid_os_v0_list_fluid_osnavigation_items } from "./pull-C3yj9L07.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import ora from "ora";
|
|
6
|
-
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import {
|
|
8
|
+
import { chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import os, { tmpdir } from "node:os";
|
|
9
11
|
import { failure, findProjectConfig, getActiveProfile, getAuthToken, listProfileNames, readConfig, success } from "@fluid-app/fluid-cli";
|
|
10
12
|
import prompts from "prompts";
|
|
11
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
12
13
|
import Handlebars from "handlebars";
|
|
13
14
|
import { execa } from "execa";
|
|
14
|
-
import
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
15
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
16
16
|
import fs from "fs-extra";
|
|
17
17
|
//#region src/types.ts
|
|
18
18
|
/**
|
|
@@ -95,9 +95,9 @@ async function promptProjectConfig(projectName, options) {
|
|
|
95
95
|
//#endregion
|
|
96
96
|
//#region src/utils/file-system.ts
|
|
97
97
|
const _currentDir$1 = dirname(fileURLToPath(import.meta.url));
|
|
98
|
-
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.
|
|
98
|
+
const EMBEDDED_PORTAL_SDK_VERSION = "0.1.462";
|
|
99
99
|
const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
|
|
100
|
-
const EMBEDDED_CLI_PORTAL_VERSION = "0.1.
|
|
100
|
+
const EMBEDDED_CLI_PORTAL_VERSION = "0.1.53";
|
|
101
101
|
/**
|
|
102
102
|
* Find the package root by walking up from the current directory to the nearest package.json.
|
|
103
103
|
* Works whether running from dist/ (bundled) or src/utils/ (tsx dev mode).
|
|
@@ -296,6 +296,40 @@ async function getCliVersion() {
|
|
|
296
296
|
async function getPortalCliVersion() {
|
|
297
297
|
return getTemplatePackageVersion(["cli", "portal"], EMBEDDED_CLI_PORTAL_VERSION);
|
|
298
298
|
}
|
|
299
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
300
|
+
const NPM_REGISTRY_TIMEOUT_MS = 5e3;
|
|
301
|
+
/**
|
|
302
|
+
* Clamp a scaffold dependency pin to a version that `pnpm install` can
|
|
303
|
+
* actually resolve from the registry.
|
|
304
|
+
*
|
|
305
|
+
* The versions above are whatever this CLI build carries — embedded at build
|
|
306
|
+
* time, or read from the workspace when running from source. Both run ahead
|
|
307
|
+
* of npm whenever the monorepo holds a release bump that has not been
|
|
308
|
+
* published yet (routine for anyone running the CLI from a checkout, e.g.
|
|
309
|
+
* Mist in development), and a scaffold pinned to that version fails to
|
|
310
|
+
* install. Only a definitive registry 404 for the exact version downgrades
|
|
311
|
+
* the pin to the registry's `latest`; a network failure or any other registry
|
|
312
|
+
* response keeps the original version, so offline scaffolds behave exactly as
|
|
313
|
+
* before.
|
|
314
|
+
*
|
|
315
|
+
* Scaffold pins are the ONLY sound callsite: consumers that report which
|
|
316
|
+
* version is running (e.g. widget-package-publish) must not route through
|
|
317
|
+
* this — it would silently misreport an unpublished build as the older
|
|
318
|
+
* published one.
|
|
319
|
+
*/
|
|
320
|
+
async function resolveInstallableTemplateVersion(packageName, version) {
|
|
321
|
+
try {
|
|
322
|
+
if ((await fetch(`${NPM_REGISTRY_URL}/${packageName}/${encodeURIComponent(version)}`, { signal: AbortSignal.timeout(NPM_REGISTRY_TIMEOUT_MS) })).status !== 404) return version;
|
|
323
|
+
const latest = await fetch(`${NPM_REGISTRY_URL}/${packageName}/latest`, { signal: AbortSignal.timeout(NPM_REGISTRY_TIMEOUT_MS) });
|
|
324
|
+
if (!latest.ok) return version;
|
|
325
|
+
const pkg = await latest.json();
|
|
326
|
+
if (typeof pkg.version !== "string") return version;
|
|
327
|
+
console.warn(`${packageName}@${version} is not published yet; the scaffold pins the latest published version ${pkg.version} instead.`);
|
|
328
|
+
return pkg.version;
|
|
329
|
+
} catch {
|
|
330
|
+
return version;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
299
333
|
async function getTemplatePackageVersion(packagePathSegments, embeddedVersion) {
|
|
300
334
|
if (embeddedVersion) return embeddedVersion;
|
|
301
335
|
try {
|
|
@@ -486,6 +520,10 @@ function splitRemoteCredentials(remoteUrl) {
|
|
|
486
520
|
password
|
|
487
521
|
};
|
|
488
522
|
}
|
|
523
|
+
/** Strip credential userinfo from a minted remote URL for safe comparison. */
|
|
524
|
+
function cleanGitRemoteUrl(remoteUrl) {
|
|
525
|
+
return splitRemoteCredentials(remoteUrl).cleanUrl;
|
|
526
|
+
}
|
|
489
527
|
/**
|
|
490
528
|
* Write a stub GIT_ASKPASS shell script to a per-process tmpdir and
|
|
491
529
|
* return its path. The script contains no secret — it echoes back
|
|
@@ -519,9 +557,9 @@ function ensureAskpassScript() {
|
|
|
519
557
|
* credentials are empty (auth should fail fast, not hang).
|
|
520
558
|
*/
|
|
521
559
|
function gitAuthSpawn(remoteUrl) {
|
|
522
|
-
const {
|
|
560
|
+
const { username, password } = splitRemoteCredentials(remoteUrl);
|
|
523
561
|
return {
|
|
524
|
-
cleanUrl,
|
|
562
|
+
cleanUrl: cleanGitRemoteUrl(remoteUrl),
|
|
525
563
|
env: {
|
|
526
564
|
...process.env,
|
|
527
565
|
GIT_ASKPASS: ensureAskpassScript(),
|
|
@@ -791,6 +829,245 @@ async function gitMergeFastForward(cwd) {
|
|
|
791
829
|
}
|
|
792
830
|
}
|
|
793
831
|
/**
|
|
832
|
+
* Local-only state that must never reach a portal's provisioned repo,
|
|
833
|
+
* regardless of which tool created the checkout.
|
|
834
|
+
*
|
|
835
|
+
* `.mist-desktop/` is Mist Desktop's per-machine working state for the
|
|
836
|
+
* project directory it opened — chat transcripts, user attachments,
|
|
837
|
+
* screenshots, source baselines, and a media cache. When Desktop drives
|
|
838
|
+
* `fluid portal push`, that directory sits in the project root, so a
|
|
839
|
+
* whole-tree `git add -A` publishes every private conversation to the
|
|
840
|
+
* portal's GitHub repo. `.mist` is the sibling clone marker (a slug
|
|
841
|
+
* pointer); committing it means a later revert deletes it and breaks
|
|
842
|
+
* slug resolution for every subsequent command.
|
|
843
|
+
*
|
|
844
|
+
* Neither is portal content: the portal's own material lives under
|
|
845
|
+
* `portal/` and is pushed separately by the content push.
|
|
846
|
+
*/
|
|
847
|
+
const LOCAL_ONLY_GIT_PATHS = [".mist", ".mist-desktop/"];
|
|
848
|
+
/**
|
|
849
|
+
* Append {@link LOCAL_ONLY_GIT_PATHS} to `.git/info/exclude` when absent.
|
|
850
|
+
*
|
|
851
|
+
* Why the local exclude and not `.gitignore`: a portal's `.gitignore`
|
|
852
|
+
* ships from the starter template (bundled or the GitHub
|
|
853
|
+
* `portal-template` repo), so a template edit alone would never reach
|
|
854
|
+
* the clones that already exist on people's machines. `.git/info/exclude`
|
|
855
|
+
* is per-checkout, so writing it here — at sync time, on every push —
|
|
856
|
+
* heals a pre-existing clone on its very next push with no template
|
|
857
|
+
* release required. The template is updated too, for fresh scaffolds.
|
|
858
|
+
*
|
|
859
|
+
* Idempotent and best-effort: never throws. A repo with an unusual `.git`
|
|
860
|
+
* layout (missing `info/`, `.git` as a worktree pointer file) just skips
|
|
861
|
+
* the exclude — {@link gitAutoCommit} also cleans these paths out of the
|
|
862
|
+
* index after staging, so the exclude is a convenience, not the only
|
|
863
|
+
* defense.
|
|
864
|
+
*
|
|
865
|
+
* Mirrors `packages/cli/mist/src/lib/git.ts::excludeMistMarkerFromGit`.
|
|
866
|
+
*/
|
|
867
|
+
function excludeLocalStateFromGit(cwd) {
|
|
868
|
+
try {
|
|
869
|
+
const gitDir = path.join(cwd, ".git");
|
|
870
|
+
if (!existsSync(gitDir)) return;
|
|
871
|
+
const infoDir = path.join(gitDir, "info");
|
|
872
|
+
if (!existsSync(infoDir)) mkdirSync(infoDir, { recursive: true });
|
|
873
|
+
const excludePath = path.join(infoDir, "exclude");
|
|
874
|
+
const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
875
|
+
const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
876
|
+
const missing = LOCAL_ONLY_GIT_PATHS.filter((entry) => !existingLines.has(entry));
|
|
877
|
+
if (missing.length === 0) return;
|
|
878
|
+
writeFileSync(excludePath, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
|
|
879
|
+
} catch {}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Absolute path of the repository containing `cwd`, or `cwd` itself when
|
|
883
|
+
* resolution fails (not a repo / git missing — the caller's own git
|
|
884
|
+
* command surfaces the real error).
|
|
885
|
+
*
|
|
886
|
+
* Every local-state defense must anchor here rather than to `cwd`: a push
|
|
887
|
+
* run from a project subdirectory would otherwise look for
|
|
888
|
+
* `<subdir>/.git`, silently skip the exclude write, and then let
|
|
889
|
+
* `git add -A` — which always stages the WHOLE repository — sweep the
|
|
890
|
+
* root `.mist-desktop/` into the commit anyway.
|
|
891
|
+
*/
|
|
892
|
+
async function resolveRepoRoot(cwd) {
|
|
893
|
+
try {
|
|
894
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
895
|
+
const root = stdout.trim();
|
|
896
|
+
if (root.length > 0) return root;
|
|
897
|
+
} catch {}
|
|
898
|
+
return cwd;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Repository-root-anchored {@link excludeLocalStateFromGit}, for callers
|
|
902
|
+
* that only have a possibly-nested `cwd`.
|
|
903
|
+
*
|
|
904
|
+
* Run this BEFORE any working-tree cleanliness check, not just before
|
|
905
|
+
* staging: while `.mist-desktop/` is neither tracked nor excluded, it
|
|
906
|
+
* shows up as untracked in `git status --porcelain`, so a Desktop-driven
|
|
907
|
+
* push reads as "you have uncommitted changes" and the sync skips with
|
|
908
|
+
* advice to `git pull` that the user can't act on. Excluding it first
|
|
909
|
+
* makes the cleanliness check reflect actual portal changes.
|
|
910
|
+
*/
|
|
911
|
+
async function ensureLocalStateExcluded(cwd) {
|
|
912
|
+
excludeLocalStateFromGit(await resolveRepoRoot(cwd));
|
|
913
|
+
}
|
|
914
|
+
/** Paths currently tracked under `.mist-desktop/`, or [] when none. */
|
|
915
|
+
async function trackedLocalStatePaths(repoRoot) {
|
|
916
|
+
try {
|
|
917
|
+
const { stdout } = await execa("git", [
|
|
918
|
+
"ls-files",
|
|
919
|
+
"--",
|
|
920
|
+
".mist-desktop"
|
|
921
|
+
], { cwd: repoRoot });
|
|
922
|
+
return stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
923
|
+
} catch {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Untrack private Mist Desktop state and, if anything was tracked,
|
|
929
|
+
* commit ONLY that removal so the next push deletes it from the remote.
|
|
930
|
+
* Returns true when a purge commit was made.
|
|
931
|
+
*
|
|
932
|
+
* Call this on every sync BEFORE deciding whether there is anything to
|
|
933
|
+
* auto-commit — a clean checkout that already tracks `.mist-desktop/`
|
|
934
|
+
* still needs the removal, and ignore rules alone cannot untrack an
|
|
935
|
+
* existing path.
|
|
936
|
+
*
|
|
937
|
+
* The commit is built through a THROWAWAY index (`GIT_INDEX_FILE`), not
|
|
938
|
+
* the user's: `read-tree HEAD` → `rm --cached` → `write-tree` →
|
|
939
|
+
* `commit-tree` → `update-ref`. A plain `git commit` would capture the
|
|
940
|
+
* user's entire index, so anything they had pre-staged would be swept
|
|
941
|
+
* into the purge commit and pushed — defeating `--no-commit` and
|
|
942
|
+
* mislabelling their work. A pathspec commit (`git commit -- .mist-desktop`)
|
|
943
|
+
* is also wrong: with `--only` git takes the *working tree* content of
|
|
944
|
+
* those paths, and the chats are still on disk, so it would re-add
|
|
945
|
+
* exactly what we are removing.
|
|
946
|
+
*
|
|
947
|
+
* The user's real index is then updated with the same `rm --cached`,
|
|
948
|
+
* touching only this path — so any other staged work stays staged, and
|
|
949
|
+
* the working tree is never modified.
|
|
950
|
+
*/
|
|
951
|
+
async function purgeLocalStateFromIndex(cwd, message) {
|
|
952
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
953
|
+
if ((await trackedLocalStatePaths(repoRoot)).length === 0) return false;
|
|
954
|
+
let head;
|
|
955
|
+
try {
|
|
956
|
+
const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
|
|
957
|
+
head = stdout.trim();
|
|
958
|
+
if (!head) return false;
|
|
959
|
+
} catch {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
const tmpIndex = path.join(os.tmpdir(), `fluid-portal-purge-${process.pid}-${randomUUID()}.index`);
|
|
963
|
+
const indexEnv = {
|
|
964
|
+
...process.env,
|
|
965
|
+
GIT_INDEX_FILE: tmpIndex
|
|
966
|
+
};
|
|
967
|
+
let commitSha;
|
|
968
|
+
try {
|
|
969
|
+
await execa("git", ["read-tree", head], {
|
|
970
|
+
cwd: repoRoot,
|
|
971
|
+
env: indexEnv
|
|
972
|
+
});
|
|
973
|
+
await execa("git", [
|
|
974
|
+
"rm",
|
|
975
|
+
"-q",
|
|
976
|
+
"-r",
|
|
977
|
+
"--cached",
|
|
978
|
+
"--ignore-unmatch",
|
|
979
|
+
"--",
|
|
980
|
+
".mist-desktop"
|
|
981
|
+
], {
|
|
982
|
+
cwd: repoRoot,
|
|
983
|
+
env: indexEnv
|
|
984
|
+
});
|
|
985
|
+
const { stdout: tree } = await execa("git", ["write-tree"], {
|
|
986
|
+
cwd: repoRoot,
|
|
987
|
+
env: indexEnv
|
|
988
|
+
});
|
|
989
|
+
const { stdout: sha } = await execa("git", [
|
|
990
|
+
"commit-tree",
|
|
991
|
+
tree.trim(),
|
|
992
|
+
"-p",
|
|
993
|
+
head,
|
|
994
|
+
"-m",
|
|
995
|
+
message
|
|
996
|
+
], {
|
|
997
|
+
cwd: repoRoot,
|
|
998
|
+
env: {
|
|
999
|
+
...indexEnv,
|
|
1000
|
+
...await identityEnv(repoRoot)
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
commitSha = sha.trim();
|
|
1004
|
+
if (!commitSha) return false;
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
const missing = gitMissingError(err);
|
|
1007
|
+
if (missing) throw missing;
|
|
1008
|
+
throw toGitError(err, "purge local state");
|
|
1009
|
+
} finally {
|
|
1010
|
+
rmSync(tmpIndex, { force: true });
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
await execa("git", [
|
|
1014
|
+
"update-ref",
|
|
1015
|
+
"HEAD",
|
|
1016
|
+
commitSha,
|
|
1017
|
+
head
|
|
1018
|
+
], { cwd: repoRoot });
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
throw toGitError(err, "purge local state");
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
await execa("git", [
|
|
1024
|
+
"rm",
|
|
1025
|
+
"-q",
|
|
1026
|
+
"-r",
|
|
1027
|
+
"--cached",
|
|
1028
|
+
"--ignore-unmatch",
|
|
1029
|
+
"--",
|
|
1030
|
+
".mist-desktop"
|
|
1031
|
+
], { cwd: repoRoot });
|
|
1032
|
+
} catch {}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
/** `GIT_{AUTHOR,COMMITTER}_*` overrides for `commit-tree`, which (unlike
|
|
1036
|
+
* `git commit`) fails outright rather than prompting when the repo has
|
|
1037
|
+
* no identity. Only set what the repo itself doesn't define. */
|
|
1038
|
+
async function identityEnv(cwd) {
|
|
1039
|
+
const env = {};
|
|
1040
|
+
const read = async (key) => {
|
|
1041
|
+
try {
|
|
1042
|
+
return (await execa("git", ["config", key], { cwd })).stdout.trim().length > 0;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
if (!await read("user.name")) {
|
|
1048
|
+
env["GIT_AUTHOR_NAME"] = "Fluid Portal CLI";
|
|
1049
|
+
env["GIT_COMMITTER_NAME"] = "Fluid Portal CLI";
|
|
1050
|
+
}
|
|
1051
|
+
if (!await read("user.email")) {
|
|
1052
|
+
env["GIT_AUTHOR_EMAIL"] = "portal@fluid.app";
|
|
1053
|
+
env["GIT_COMMITTER_EMAIL"] = "portal@fluid.app";
|
|
1054
|
+
}
|
|
1055
|
+
return env;
|
|
1056
|
+
}
|
|
1057
|
+
/** Shared `-c user.*` args: use the repo's identity, else a synthetic
|
|
1058
|
+
* Fluid bot identity so the commit isn't rejected. */
|
|
1059
|
+
async function resolveIdentityArgs(cwd) {
|
|
1060
|
+
let hasName = false;
|
|
1061
|
+
let hasEmail = false;
|
|
1062
|
+
try {
|
|
1063
|
+
hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
|
|
1064
|
+
} catch {}
|
|
1065
|
+
try {
|
|
1066
|
+
hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
|
|
1067
|
+
} catch {}
|
|
1068
|
+
return [...hasName ? [] : ["-c", "user.name=Fluid Portal CLI"], ...hasEmail ? [] : ["-c", "user.email=portal@fluid.app"]];
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
794
1071
|
* `git add -A && git commit -m <message>` (or, with
|
|
795
1072
|
* `{ ignoreRemovals: true }`, `git add --ignore-removal .` — see
|
|
796
1073
|
* GitAutoCommitOptions).
|
|
@@ -803,11 +1080,42 @@ async function gitMergeFastForward(cwd) {
|
|
|
803
1080
|
* `node_modules/`, `dist/`, `.portal-sync/`, logs, `*.tsbuildinfo`), so
|
|
804
1081
|
* generated output and local secrets stay out of the commit.
|
|
805
1082
|
*
|
|
1083
|
+
* That boundary is not enough on its own for tool-local state written
|
|
1084
|
+
* INTO the project directory by whatever opened it — most importantly
|
|
1085
|
+
* Mist Desktop's `.mist-desktop/` (private chats, attachments,
|
|
1086
|
+
* screenshots) and the `.mist` clone marker. A `.gitignore`-only fix
|
|
1087
|
+
* can't reach clones that already exist, so this applies belt AND
|
|
1088
|
+
* suspenders, all anchored to the repository root (see
|
|
1089
|
+
* {@link resolveRepoRoot}):
|
|
1090
|
+
*
|
|
1091
|
+
* 1. {@link excludeLocalStateFromGit} — idempotent `.git/info/exclude`
|
|
1092
|
+
* write, so `git add` never picks the paths up in the first place.
|
|
1093
|
+
* Heals pre-existing clones on their next push.
|
|
1094
|
+
* 2. `git reset -- ':(top).mist' .mist` after staging — unstages the
|
|
1095
|
+
* marker if step 1 silently failed. We deliberately do NOT pass a
|
|
1096
|
+
* `:(exclude)` pathspec to `git add`: when the path is *also*
|
|
1097
|
+
* listed in `.git/info/exclude`, git exits 1 with "paths are
|
|
1098
|
+
* ignored by one of your .gitignore files", turning the normal
|
|
1099
|
+
* (already-healed) case into a hard failure. Add-then-reset is a
|
|
1100
|
+
* no-op when there's nothing staged.
|
|
1101
|
+
* 3. `git rm --cached -r --ignore-unmatch .mist-desktop` — untracks
|
|
1102
|
+
* chat history that an older, unhardened push already committed.
|
|
1103
|
+
* Ignore rules alone cannot untrack an existing path. `--cached`
|
|
1104
|
+
* keeps every file on disk; only the index entry goes away. This
|
|
1105
|
+
* runs even under `ignoreRemovals`, where suppressing deletions
|
|
1106
|
+
* protects remote-only *portal* files — private chats are never
|
|
1107
|
+
* that, and leaving them tracked would keep republishing them.
|
|
1108
|
+
*
|
|
1109
|
+
* Steps 2 and 3 are best-effort and never fail the publish; the commit
|
|
1110
|
+
* itself is what must succeed.
|
|
1111
|
+
*
|
|
806
1112
|
* Uses the repo's configured identity, else a synthetic Fluid bot
|
|
807
1113
|
* identity so the commit isn't rejected. "nothing to commit" is treated
|
|
808
1114
|
* as a no-op, not an error.
|
|
809
1115
|
*/
|
|
810
1116
|
async function gitAutoCommit(cwd, message, options = {}) {
|
|
1117
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
1118
|
+
excludeLocalStateFromGit(repoRoot);
|
|
811
1119
|
const addArgs = options.ignoreRemovals ? [
|
|
812
1120
|
"add",
|
|
813
1121
|
"--ignore-removal",
|
|
@@ -818,15 +1126,27 @@ async function gitAutoCommit(cwd, message, options = {}) {
|
|
|
818
1126
|
} catch (err) {
|
|
819
1127
|
throw toGitError(err, "add");
|
|
820
1128
|
}
|
|
821
|
-
let hasName = false;
|
|
822
|
-
let hasEmail = false;
|
|
823
1129
|
try {
|
|
824
|
-
|
|
1130
|
+
await execa("git", [
|
|
1131
|
+
"reset",
|
|
1132
|
+
"-q",
|
|
1133
|
+
"--",
|
|
1134
|
+
":(top).mist",
|
|
1135
|
+
".mist"
|
|
1136
|
+
], { cwd });
|
|
825
1137
|
} catch {}
|
|
826
1138
|
try {
|
|
827
|
-
|
|
1139
|
+
await execa("git", [
|
|
1140
|
+
"rm",
|
|
1141
|
+
"-q",
|
|
1142
|
+
"-r",
|
|
1143
|
+
"--cached",
|
|
1144
|
+
"--ignore-unmatch",
|
|
1145
|
+
"--",
|
|
1146
|
+
".mist-desktop"
|
|
1147
|
+
], { cwd: repoRoot });
|
|
828
1148
|
} catch {}
|
|
829
|
-
const identityArgs =
|
|
1149
|
+
const identityArgs = await resolveIdentityArgs(cwd);
|
|
830
1150
|
try {
|
|
831
1151
|
await execa("git", [
|
|
832
1152
|
...identityArgs,
|
|
@@ -1055,7 +1375,7 @@ async function createPortalProject(appName, options, config, dependencies = defa
|
|
|
1055
1375
|
}).catch(() => {});
|
|
1056
1376
|
throw error;
|
|
1057
1377
|
}
|
|
1058
|
-
} else await scaffoldFromBundled(targetPath, config, isLocal);
|
|
1378
|
+
} else await scaffoldFromBundled(targetPath, config, isLocal, true);
|
|
1059
1379
|
if (config.installDeps) {
|
|
1060
1380
|
const installSpinner = ora("Installing dependencies with pnpm...").start();
|
|
1061
1381
|
try {
|
|
@@ -1105,13 +1425,13 @@ function formatCdPath(targetPath) {
|
|
|
1105
1425
|
*
|
|
1106
1426
|
* Throws on failure; the caller reports and exits.
|
|
1107
1427
|
*/
|
|
1108
|
-
async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
1428
|
+
async function scaffoldFromBundled(targetPath, config, isLocal, createTarget) {
|
|
1109
1429
|
const templatePaths = getTemplatePaths("starter");
|
|
1110
1430
|
if (!await directoryExists(templatePaths.base)) throw new Error("Base template not found");
|
|
1111
1431
|
if (!await directoryExists(templatePaths.overlay)) throw new Error("Starter template not found");
|
|
1112
1432
|
let sdkVersion;
|
|
1113
1433
|
let localCoreVersion;
|
|
1114
|
-
const [cliVersion, portalCliVersion] = await Promise.all([getCliVersion(), getPortalCliVersion()]);
|
|
1434
|
+
const [cliVersion, portalCliVersion] = await Promise.all([getCliVersion().then((version) => resolveInstallableTemplateVersion("@fluid-app/fluid-cli", version)), getPortalCliVersion().then((version) => resolveInstallableTemplateVersion("@fluid-app/fluid-cli-portal", version))]);
|
|
1115
1435
|
if (isLocal) {
|
|
1116
1436
|
const monorepoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
1117
1437
|
const sdkPath = join(monorepoRoot, "packages", "portal", "sdk");
|
|
@@ -1120,15 +1440,16 @@ async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
|
1120
1440
|
sdkVersion = getLocalPackageLinkVersion(targetPath, sdkPath);
|
|
1121
1441
|
localCoreVersion = getLocalPackageLinkVersion(targetPath, corePath);
|
|
1122
1442
|
console.log(chalk.cyan(" Using local packages (--local mode)"));
|
|
1123
|
-
} else sdkVersion = await getSdkVersion();
|
|
1124
|
-
const spinner = ora("Creating project directory...").start();
|
|
1125
|
-
try {
|
|
1443
|
+
} else sdkVersion = await getSdkVersion().then((version) => resolveInstallableTemplateVersion("@fluid-app/portal-sdk", version));
|
|
1444
|
+
const spinner = ora(createTarget ? "Creating project directory..." : "Preparing provisioned portal checkout...").start();
|
|
1445
|
+
if (createTarget) try {
|
|
1126
1446
|
await createDirectory(targetPath);
|
|
1127
1447
|
spinner.succeed("Created project directory");
|
|
1128
1448
|
} catch (error) {
|
|
1129
1449
|
spinner.fail("Failed to create project directory");
|
|
1130
1450
|
throw error;
|
|
1131
1451
|
}
|
|
1452
|
+
else if (!await directoryExists(targetPath)) throw new Error(`Portal checkout does not exist: ${targetPath}`);
|
|
1132
1453
|
const templateVariables = {
|
|
1133
1454
|
projectName: config.name,
|
|
1134
1455
|
sdkVersion,
|
|
@@ -1151,6 +1472,79 @@ async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
|
1151
1472
|
throw error;
|
|
1152
1473
|
}
|
|
1153
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Add the runnable bundled starter to an existing provisioned Git checkout.
|
|
1477
|
+
* Portal repositories begin with a bootstrap commit, so a first clone may
|
|
1478
|
+
* not contain package.json yet. Rendering into the checkout preserves its
|
|
1479
|
+
* .git directory and gives both the CLI and Mist Desktop a usable project
|
|
1480
|
+
* without creating unrelated local history.
|
|
1481
|
+
*/
|
|
1482
|
+
async function scaffoldPortalProjectIntoExistingDirectory(targetPath, config) {
|
|
1483
|
+
const pendingMarker = join(targetPath, PORTAL_SCAFFOLD_PENDING_MARKER);
|
|
1484
|
+
const stagingRoot = await mkdtemp(join(tmpdir(), "fluid-portal-scaffold-stage-"));
|
|
1485
|
+
const stagedProject = join(stagingRoot, "portal");
|
|
1486
|
+
let completed = false;
|
|
1487
|
+
try {
|
|
1488
|
+
await writeFile(pendingMarker, "Fluid Portal scaffold pending\n");
|
|
1489
|
+
await scaffoldFromBundled(stagedProject, config, false, true);
|
|
1490
|
+
const preserved = await copyMissingScaffoldFiles(stagedProject, targetPath);
|
|
1491
|
+
if (preserved.length > 0) console.log(chalk.dim(` Preserved ${preserved.length} existing repository ${preserved.length === 1 ? "file" : "files"} while adding the runnable scaffold.`));
|
|
1492
|
+
completed = true;
|
|
1493
|
+
} finally {
|
|
1494
|
+
if (completed) await rm(pendingMarker, { force: true });
|
|
1495
|
+
await rm(stagingRoot, {
|
|
1496
|
+
recursive: true,
|
|
1497
|
+
force: true
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const PORTAL_SCAFFOLD_PENDING_MARKER = ".fluid-portal-scaffold-pending";
|
|
1502
|
+
async function copyMissingScaffoldFiles(sourceRoot, targetRoot, relativeDirectory = "") {
|
|
1503
|
+
const preserved = [];
|
|
1504
|
+
const entries = await readdir(join(sourceRoot, relativeDirectory), { withFileTypes: true });
|
|
1505
|
+
for (const entry of entries) {
|
|
1506
|
+
const relativePath = join(relativeDirectory, entry.name);
|
|
1507
|
+
const sourcePath = join(sourceRoot, relativePath);
|
|
1508
|
+
const targetPath = join(targetRoot, relativePath);
|
|
1509
|
+
if (entry.isDirectory()) {
|
|
1510
|
+
if (!await ensureScaffoldDirectory(targetPath)) throw new Error(`Cannot add the runnable Portal scaffold because the repository path "${relativePath}" is not a directory. The cloned repository and Git history were preserved.`);
|
|
1511
|
+
preserved.push(...await copyMissingScaffoldFiles(sourceRoot, targetRoot, relativePath));
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (!entry.isFile()) throw new Error(`Portal scaffold contains an unsupported file type at ${relativePath}.`);
|
|
1515
|
+
try {
|
|
1516
|
+
await copyFile(sourcePath, targetPath, constants.COPYFILE_EXCL);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
if (error.code !== "EEXIST") throw error;
|
|
1519
|
+
preserved.push(relativePath);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return preserved;
|
|
1523
|
+
}
|
|
1524
|
+
async function ensureScaffoldDirectory(targetPath) {
|
|
1525
|
+
try {
|
|
1526
|
+
return (await lstat(targetPath)).isDirectory();
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
const code = error.code;
|
|
1529
|
+
if (code !== "ENOENT") {
|
|
1530
|
+
if (code === "ENOTDIR") return false;
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
await mkdir(targetPath);
|
|
1536
|
+
return true;
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
const code = error.code;
|
|
1539
|
+
if (code !== "EEXIST" && code !== "ENOTDIR") throw error;
|
|
1540
|
+
try {
|
|
1541
|
+
return (await lstat(targetPath)).isDirectory();
|
|
1542
|
+
} catch (statError) {
|
|
1543
|
+
if (statError.code === "ENOTDIR") return false;
|
|
1544
|
+
throw statError;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1154
1548
|
//#endregion
|
|
1155
1549
|
//#region src/commands/dev.ts
|
|
1156
1550
|
/**
|
|
@@ -1164,6 +1558,46 @@ async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
|
1164
1558
|
* or auto-pulls if they are logged in.
|
|
1165
1559
|
*/
|
|
1166
1560
|
const PORTAL_DIR$1 = "portal";
|
|
1561
|
+
const WATCHED_DIRECTORIES = [
|
|
1562
|
+
"portal",
|
|
1563
|
+
"public",
|
|
1564
|
+
"src"
|
|
1565
|
+
];
|
|
1566
|
+
const WATCHED_ROOT_FILES = new Set([
|
|
1567
|
+
"index.html",
|
|
1568
|
+
"package.json",
|
|
1569
|
+
"tsconfig.json",
|
|
1570
|
+
"vite.config.cjs",
|
|
1571
|
+
"vite.config.cts",
|
|
1572
|
+
"vite.config.js",
|
|
1573
|
+
"vite.config.mjs",
|
|
1574
|
+
"vite.config.mts",
|
|
1575
|
+
"vite.config.ts"
|
|
1576
|
+
]);
|
|
1577
|
+
/**
|
|
1578
|
+
* Restrict Vite's root watcher to files that can affect a portal preview.
|
|
1579
|
+
* Directory paths themselves must remain visible to Chokidar so it can
|
|
1580
|
+
* descend into them and discover their files.
|
|
1581
|
+
*/
|
|
1582
|
+
function createPortalWatchIgnored(cwd) {
|
|
1583
|
+
const projectRoot = resolve(cwd);
|
|
1584
|
+
return (filePath) => {
|
|
1585
|
+
const projectPath = relative(projectRoot, isAbsolute(filePath) ? filePath : resolve(projectRoot, filePath));
|
|
1586
|
+
if (projectPath === "") return false;
|
|
1587
|
+
if (projectPath === ".." || projectPath.startsWith(`..${sep}`)) return true;
|
|
1588
|
+
const [topLevelPath] = projectPath.split(sep);
|
|
1589
|
+
if (WATCHED_DIRECTORIES.some((directory) => topLevelPath === directory)) return false;
|
|
1590
|
+
const isEnvironmentFile = projectPath === ".env" || projectPath.startsWith(".env.");
|
|
1591
|
+
return !WATCHED_ROOT_FILES.has(projectPath) && !isEnvironmentFile;
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function portalDevServerOptions(port, exposeToNetwork, cwd = process.cwd()) {
|
|
1595
|
+
return {
|
|
1596
|
+
port,
|
|
1597
|
+
host: exposeToNetwork ? true : void 0,
|
|
1598
|
+
watch: { ignored: createPortalWatchIgnored(cwd) }
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1167
1601
|
/**
|
|
1168
1602
|
* Check if the portal directory exists and has content files.
|
|
1169
1603
|
* Returns true if at minimum `portal/definition.json` exists.
|
|
@@ -1172,6 +1606,29 @@ function hasPortalContent(cwd) {
|
|
|
1172
1606
|
return existsSync(join(cwd, PORTAL_DIR$1, "definition.json"));
|
|
1173
1607
|
}
|
|
1174
1608
|
/**
|
|
1609
|
+
* Preflight check: does `<cwd>/node_modules` actually contain what this
|
|
1610
|
+
* command is about to `import()` (`vite`) and inject into the config
|
|
1611
|
+
* (`@fluid-app/portal-sdk`, pulled in transitively by the vite plugins)?
|
|
1612
|
+
*
|
|
1613
|
+
* Without this, a project whose install failed or was never run (no
|
|
1614
|
+
* `node_modules` at all) produces a cascade of confusing failures instead
|
|
1615
|
+
* of one clear message: `createServer()` below throws "failed to load
|
|
1616
|
+
* config from .../vite.config.ts", we catch that and warn "falling back to
|
|
1617
|
+
* `vite`", and the fallback then dies with a raw
|
|
1618
|
+
* `ERR_MODULE_NOT_FOUND: Cannot find package 'vite'` — never once saying
|
|
1619
|
+
* the actual problem is a missing `pnpm install`.
|
|
1620
|
+
*/
|
|
1621
|
+
function checkDependenciesInstalled(cwd) {
|
|
1622
|
+
const nodeModulesPath = join(cwd, "node_modules");
|
|
1623
|
+
const hasVite = existsSync(join(nodeModulesPath, "vite"));
|
|
1624
|
+
const hasPortalSdk = existsSync(join(nodeModulesPath, "@fluid-app", "portal-sdk"));
|
|
1625
|
+
if (existsSync(nodeModulesPath) && hasVite && hasPortalSdk) return { ok: true };
|
|
1626
|
+
return {
|
|
1627
|
+
ok: false,
|
|
1628
|
+
message: `Dependencies aren't installed in ${cwd} — run \`pnpm install\`, then retry.`
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1175
1632
|
* Attempt to auto-pull portal content by invoking the pull command's action.
|
|
1176
1633
|
* Falls back to a helpful error message if pull is not possible.
|
|
1177
1634
|
*/
|
|
@@ -1180,7 +1637,7 @@ async function autoPull(cwd) {
|
|
|
1180
1637
|
console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
|
|
1181
1638
|
console.log();
|
|
1182
1639
|
try {
|
|
1183
|
-
const { pullCommand } = await import("./pull-
|
|
1640
|
+
const { pullCommand } = await import("./pull-C3yj9L07.mjs").then((n) => n.n);
|
|
1184
1641
|
await pullCommand.parseAsync([], { from: "user" });
|
|
1185
1642
|
return hasPortalContent(cwd);
|
|
1186
1643
|
} catch (err) {
|
|
@@ -1205,6 +1662,11 @@ const devCommand = new Command("dev").description("Start the development server
|
|
|
1205
1662
|
console.error(chalk.yellow("This command must be run from a Fluid project directory"));
|
|
1206
1663
|
process.exit(1);
|
|
1207
1664
|
}
|
|
1665
|
+
const depsCheck = checkDependenciesInstalled(cwd);
|
|
1666
|
+
if (!depsCheck.ok) {
|
|
1667
|
+
console.error(chalk.red(`Error: ${depsCheck.message}`));
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1208
1670
|
if (!hasPortalContent(cwd) && !options.skipPull) {
|
|
1209
1671
|
if (!await autoPull(cwd)) {
|
|
1210
1672
|
console.error(chalk.red("Cannot start dev server without portal content."));
|
|
@@ -1228,16 +1690,13 @@ const devCommand = new Command("dev").description("Start the development server
|
|
|
1228
1690
|
const port = Number.isFinite(parsedPort) ? parsedPort : 5173;
|
|
1229
1691
|
try {
|
|
1230
1692
|
const { createServer } = await import("vite");
|
|
1231
|
-
const { portalDevPlugin } = await import("./portal-dev-plugin-
|
|
1693
|
+
const { portalDevPlugin } = await import("./portal-dev-plugin-ByYelgFg.mjs").then((n) => n.n);
|
|
1232
1694
|
const { backendDevPlugin } = await import("./backend-dev-plugin-B9-S4Is-.mjs").then((n) => n.n);
|
|
1233
1695
|
const server = await createServer({
|
|
1234
1696
|
configFile: viteConfigPath,
|
|
1235
1697
|
envPrefix: ["VITE_", "NEXT_PUBLIC_"],
|
|
1236
1698
|
plugins: [portalDevPlugin(), backendDevPlugin()],
|
|
1237
|
-
server:
|
|
1238
|
-
port,
|
|
1239
|
-
host: options.host ? true : void 0
|
|
1240
|
-
}
|
|
1699
|
+
server: portalDevServerOptions(port, options.host === true, cwd)
|
|
1241
1700
|
});
|
|
1242
1701
|
await server.listen();
|
|
1243
1702
|
server.printUrls();
|
|
@@ -1373,6 +1832,8 @@ function categorizeChanges(diff) {
|
|
|
1373
1832
|
* Validate that all cross-references between local portal files are valid.
|
|
1374
1833
|
*
|
|
1375
1834
|
* Checks:
|
|
1835
|
+
* - New and changed resources contain the required local-file fields
|
|
1836
|
+
* - New and changed themes include the required config object
|
|
1376
1837
|
* - Navigation items' "screen" slugs reference existing screen files or mappings
|
|
1377
1838
|
* - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
|
|
1378
1839
|
* - Profile "themes" slugs reference existing theme files or mappings
|
|
@@ -1385,9 +1846,43 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1385
1846
|
for (const file of changes.screens.deleted) validScreenSlugs.delete(slugFromPath(file));
|
|
1386
1847
|
for (const file of changes.navigations.deleted) validNavSlugs.delete(slugFromPath(file));
|
|
1387
1848
|
for (const file of changes.themes.deleted) validThemeSlugs.delete(slugFromPath(file));
|
|
1849
|
+
const screenFilesToCheck = [...changes.screens.new, ...changes.screens.changed];
|
|
1850
|
+
for (const file of screenFilesToCheck) try {
|
|
1851
|
+
const screen = await readPortalFile$1(portalDir, file);
|
|
1852
|
+
if (typeof screen.name !== "string" || !Array.isArray(screen.component_tree)) errors.push({
|
|
1853
|
+
file,
|
|
1854
|
+
message: "Screen requires a string \"name\" and array \"component_tree\"."
|
|
1855
|
+
});
|
|
1856
|
+
} catch {
|
|
1857
|
+
errors.push({
|
|
1858
|
+
file,
|
|
1859
|
+
message: "Failed to read screen file"
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
const themeFilesToCheck = [...changes.themes.new, ...changes.themes.changed];
|
|
1863
|
+
for (const file of themeFilesToCheck) try {
|
|
1864
|
+
const theme = await readPortalFile$1(portalDir, file);
|
|
1865
|
+
if (typeof theme.name !== "string" || typeof theme.active !== "boolean" || !theme.config || typeof theme.config !== "object" || Array.isArray(theme.config)) errors.push({
|
|
1866
|
+
file,
|
|
1867
|
+
message: "Theme requires a string \"name\", boolean \"active\", and \"config\" object. Copy the complete config shape from a pulled theme instead of creating only name/active fields."
|
|
1868
|
+
});
|
|
1869
|
+
} catch {
|
|
1870
|
+
errors.push({
|
|
1871
|
+
file,
|
|
1872
|
+
message: "Failed to read theme file"
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1388
1875
|
const navFilesToCheck = [...changes.navigations.new, ...changes.navigations.changed];
|
|
1389
1876
|
for (const file of navFilesToCheck) try {
|
|
1390
|
-
|
|
1877
|
+
const nav = await readPortalFile$1(portalDir, file);
|
|
1878
|
+
if (typeof nav.name !== "string" || nav.platform !== "web" && nav.platform !== "mobile" || !Array.isArray(nav.navigation_items)) {
|
|
1879
|
+
errors.push({
|
|
1880
|
+
file,
|
|
1881
|
+
message: "Navigation requires a string \"name\", \"platform\" set to \"web\" or \"mobile\", and a \"navigation_items\" array."
|
|
1882
|
+
});
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
validateNavigationItems(nav.navigation_items, file, validScreenSlugs, errors);
|
|
1391
1886
|
} catch {
|
|
1392
1887
|
errors.push({
|
|
1393
1888
|
file,
|
|
@@ -1397,6 +1892,14 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1397
1892
|
const profileFilesToCheck = [...changes.profiles.new, ...changes.profiles.changed];
|
|
1398
1893
|
for (const file of profileFilesToCheck) try {
|
|
1399
1894
|
const profile = await readPortalFile$1(portalDir, file);
|
|
1895
|
+
const permissions = profile.permissions;
|
|
1896
|
+
if (typeof profile.name !== "string" || typeof profile.default !== "boolean" || !Array.isArray(profile.themes) || !permissions || !Array.isArray(permissions.countries) || !Array.isArray(permissions.ranks) || !Array.isArray(permissions.roles) || !Array.isArray(permissions.platform)) {
|
|
1897
|
+
errors.push({
|
|
1898
|
+
file,
|
|
1899
|
+
message: "Profile requires \"name\", boolean \"default\", a \"themes\" array, and permissions arrays for countries, ranks, roles, and platform."
|
|
1900
|
+
});
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1400
1903
|
if (profile.navigation && !validNavSlugs.has(profile.navigation)) errors.push({
|
|
1401
1904
|
file,
|
|
1402
1905
|
message: `References navigation "${profile.navigation}" which does not exist`
|
|
@@ -1455,15 +1958,71 @@ function validateNavigationItems(items, file, validScreenSlugs, errors) {
|
|
|
1455
1958
|
function portalGitCredentialPath(definitionId) {
|
|
1456
1959
|
return `/api/company/fluid_os/definitions/${definitionId}/git_credential`;
|
|
1457
1960
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1961
|
+
const DEFAULT_RETRY_DELAYS_MS = [
|
|
1962
|
+
500,
|
|
1963
|
+
1e3,
|
|
1964
|
+
2e3,
|
|
1965
|
+
4e3,
|
|
1966
|
+
8e3,
|
|
1967
|
+
1e4,
|
|
1968
|
+
15e3,
|
|
1969
|
+
15e3
|
|
1970
|
+
];
|
|
1971
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
1972
|
+
const RETRYABLE_CONFLICT_TYPES = new Set(["not_provisioned"]);
|
|
1973
|
+
function nonEmptyString(value) {
|
|
1974
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1975
|
+
}
|
|
1976
|
+
function firstString(value) {
|
|
1977
|
+
if (Array.isArray(value)) return nonEmptyString(value[0]);
|
|
1978
|
+
return nonEmptyString(value);
|
|
1979
|
+
}
|
|
1980
|
+
function extractErrorDetails(body, fallback) {
|
|
1981
|
+
if (!body || typeof body !== "object") return { reason: fallback };
|
|
1982
|
+
const envelope = body;
|
|
1983
|
+
const error = envelope.error && typeof envelope.error === "object" ? envelope.error : void 0;
|
|
1984
|
+
const nestedDetails = error?.details && typeof error.details === "object" ? error.details : void 0;
|
|
1985
|
+
const errors = envelope.errors && typeof envelope.errors === "object" ? envelope.errors : void 0;
|
|
1986
|
+
const reason = nonEmptyString(envelope.error_message) ?? nonEmptyString(error?.message) ?? nonEmptyString(envelope.error) ?? fallback;
|
|
1987
|
+
const failureType = firstString(errors?.type) ?? nonEmptyString(nestedDetails?.type);
|
|
1988
|
+
return {
|
|
1989
|
+
reason,
|
|
1990
|
+
...failureType ? { failureType } : {}
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function sleep(delayMs) {
|
|
1994
|
+
return new Promise((resolve) => {
|
|
1995
|
+
setTimeout(resolve, delayMs);
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Retry clone-credential conflicts while the backend provisions or revives
|
|
2000
|
+
* the portal's repository. The first credential request is the trigger for
|
|
2001
|
+
* older definitions with no repo, so a single-shot clone would always fail
|
|
2002
|
+
* even though the repository becomes ready seconds later.
|
|
2003
|
+
*/
|
|
2004
|
+
async function createPortalGitCredentialWithRetry(options) {
|
|
2005
|
+
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
2006
|
+
const sleepImpl = options.sleepImpl ?? sleep;
|
|
2007
|
+
const requestOptions = {
|
|
2008
|
+
baseUrl: options.baseUrl,
|
|
2009
|
+
token: options.token,
|
|
2010
|
+
definitionId: options.definitionId,
|
|
2011
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2012
|
+
...options.requestTimeoutMs !== void 0 ? { requestTimeoutMs: options.requestTimeoutMs } : {}
|
|
2013
|
+
};
|
|
2014
|
+
let outcome = await createPortalGitCredential(requestOptions);
|
|
2015
|
+
for (const [index, delayMs] of retryDelaysMs.entries()) {
|
|
2016
|
+
if (outcome.status !== "unavailable" || !outcome.retryable) return outcome;
|
|
2017
|
+
options.onRetry?.({
|
|
2018
|
+
attempt: index + 2,
|
|
2019
|
+
totalAttempts: retryDelaysMs.length + 1,
|
|
2020
|
+
reason: outcome.reason
|
|
2021
|
+
});
|
|
2022
|
+
await sleepImpl(delayMs);
|
|
2023
|
+
outcome = await createPortalGitCredential(requestOptions);
|
|
1465
2024
|
}
|
|
1466
|
-
return
|
|
2025
|
+
return outcome;
|
|
1467
2026
|
}
|
|
1468
2027
|
/**
|
|
1469
2028
|
* Request a git credential for the portal definition's provisioned repo.
|
|
@@ -1474,36 +2033,54 @@ function extractErrorReason(body, fallback) {
|
|
|
1474
2033
|
async function createPortalGitCredential(options) {
|
|
1475
2034
|
const { baseUrl, token, definitionId } = options;
|
|
1476
2035
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2036
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1477
2037
|
const url = `${baseUrl.replace(/\/$/, "")}${portalGitCredentialPath(definitionId)}`;
|
|
2038
|
+
const abortController = new AbortController();
|
|
2039
|
+
let requestTimedOut = false;
|
|
2040
|
+
const timeout = setTimeout(() => {
|
|
2041
|
+
requestTimedOut = true;
|
|
2042
|
+
abortController.abort();
|
|
2043
|
+
}, requestTimeoutMs);
|
|
1478
2044
|
let resp;
|
|
2045
|
+
let body = null;
|
|
1479
2046
|
try {
|
|
1480
2047
|
resp = await fetchImpl(url, {
|
|
1481
2048
|
method: "POST",
|
|
1482
2049
|
headers: {
|
|
1483
2050
|
Authorization: `Bearer ${token}`,
|
|
1484
2051
|
Accept: "application/json"
|
|
1485
|
-
}
|
|
2052
|
+
},
|
|
2053
|
+
signal: abortController.signal
|
|
1486
2054
|
});
|
|
2055
|
+
if (resp.status !== 404) try {
|
|
2056
|
+
body = await resp.json();
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
if (requestTimedOut) throw err;
|
|
2059
|
+
}
|
|
1487
2060
|
} catch (err) {
|
|
1488
2061
|
return {
|
|
1489
2062
|
status: "error",
|
|
1490
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
2063
|
+
reason: requestTimedOut ? `git credential request timed out after ${requestTimeoutMs}ms` : err instanceof Error ? err.message : String(err)
|
|
1491
2064
|
};
|
|
2065
|
+
} finally {
|
|
2066
|
+
clearTimeout(timeout);
|
|
1492
2067
|
}
|
|
1493
2068
|
if (resp.status === 404) return {
|
|
1494
2069
|
status: "unavailable",
|
|
1495
|
-
reason: "git sync isn't available for portals on this Fluid API yet"
|
|
2070
|
+
reason: "git sync isn't available for portals on this Fluid API yet",
|
|
2071
|
+
retryable: false
|
|
1496
2072
|
};
|
|
1497
|
-
const body = await resp.json().catch(() => null);
|
|
1498
2073
|
if (!resp.ok) {
|
|
1499
|
-
const
|
|
2074
|
+
const details = extractErrorDetails(body, `git credential request failed (HTTP ${resp.status})`);
|
|
1500
2075
|
if (resp.status === 409) return {
|
|
1501
2076
|
status: "unavailable",
|
|
1502
|
-
reason
|
|
2077
|
+
reason: details.reason,
|
|
2078
|
+
retryable: details.failureType !== void 0 && RETRYABLE_CONFLICT_TYPES.has(details.failureType),
|
|
2079
|
+
...details.failureType !== void 0 ? { conflictType: details.failureType } : {}
|
|
1503
2080
|
};
|
|
1504
2081
|
return {
|
|
1505
2082
|
status: "error",
|
|
1506
|
-
reason
|
|
2083
|
+
reason: details.reason
|
|
1507
2084
|
};
|
|
1508
2085
|
}
|
|
1509
2086
|
const credential = body?.git_credential;
|
|
@@ -1528,6 +2105,9 @@ async function createPortalGitCredential(options) {
|
|
|
1528
2105
|
* (no git, no repo, credential endpoint missing, diverged history,
|
|
1529
2106
|
* network blip) degrades to a skip so the content push always proceeds.
|
|
1530
2107
|
*/
|
|
2108
|
+
/** Subject for the index-only commit that untracks leaked Desktop state.
|
|
2109
|
+
* Kept distinct from the publish message so the history explains itself. */
|
|
2110
|
+
const PURGE_COMMIT_MESSAGE = "Remove private Mist Desktop state from the portal repository";
|
|
1531
2111
|
/**
|
|
1532
2112
|
* Run the best-effort git sync. Returns a result describing what
|
|
1533
2113
|
* happened; never throws. Callers surface `skipped` reasons as a dim
|
|
@@ -1540,6 +2120,7 @@ async function runPortalGitSync(options) {
|
|
|
1540
2120
|
status: "skipped",
|
|
1541
2121
|
reason: "not a git repository"
|
|
1542
2122
|
};
|
|
2123
|
+
await ensureLocalStateExcluded(cwd);
|
|
1543
2124
|
const outcome = await createPortalGitCredential({
|
|
1544
2125
|
baseUrl,
|
|
1545
2126
|
token,
|
|
@@ -1566,6 +2147,7 @@ async function runPortalGitSync(options) {
|
|
|
1566
2147
|
await gitFetch(cwd, remoteUrl, branch);
|
|
1567
2148
|
await gitAdoptFetchHead(cwd);
|
|
1568
2149
|
}
|
|
2150
|
+
await purgeLocalStateFromIndex(cwd, PURGE_COMMIT_MESSAGE);
|
|
1569
2151
|
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage, { ignoreRemovals: true });
|
|
1570
2152
|
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1571
2153
|
}
|
|
@@ -1586,6 +2168,7 @@ async function runPortalGitSync(options) {
|
|
|
1586
2168
|
};
|
|
1587
2169
|
await gitMergeFastForward(cwd);
|
|
1588
2170
|
}
|
|
2171
|
+
await purgeLocalStateFromIndex(cwd, PURGE_COMMIT_MESSAGE);
|
|
1589
2172
|
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage);
|
|
1590
2173
|
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1591
2174
|
} catch (err) {
|
|
@@ -1627,6 +2210,9 @@ async function pushCommitted(cwd, remoteUrl, branch) {
|
|
|
1627
2210
|
* Detects changes since the last pull/push via snapshot diffing,
|
|
1628
2211
|
* validates cross-references, and pushes resources in dependency order.
|
|
1629
2212
|
*/
|
|
2213
|
+
var NavigationRemoteStateUnknownError = class extends Error {
|
|
2214
|
+
name = "NavigationRemoteStateUnknownError";
|
|
2215
|
+
};
|
|
1630
2216
|
const PORTAL_DIR = "portal";
|
|
1631
2217
|
const PORTAL_SYNC_DIR$1 = ".portal-sync";
|
|
1632
2218
|
/**
|
|
@@ -1654,6 +2240,28 @@ function createClient$2() {
|
|
|
1654
2240
|
getAuthToken: () => token
|
|
1655
2241
|
});
|
|
1656
2242
|
}
|
|
2243
|
+
async function createAndActivateVersion(client, definitionId) {
|
|
2244
|
+
const versionId = (await fluid_os_v0_create_fluid_osversion(client, definitionId)).version?.id;
|
|
2245
|
+
if (!versionId) throw new Error("the version API returned no version ID");
|
|
2246
|
+
await fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
|
|
2247
|
+
return String(versionId);
|
|
2248
|
+
}
|
|
2249
|
+
async function activateDraft(definitionId, existingClient) {
|
|
2250
|
+
const spinner = ora("Creating and activating a live version...");
|
|
2251
|
+
spinner.start();
|
|
2252
|
+
try {
|
|
2253
|
+
const versionId = await createAndActivateVersion(existingClient ?? createClient$2(), definitionId);
|
|
2254
|
+
spinner.succeed(`Live version ${versionId} activated`);
|
|
2255
|
+
return true;
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
spinner.fail("Could not activate a live version");
|
|
2258
|
+
console.log();
|
|
2259
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
2260
|
+
console.log();
|
|
2261
|
+
process.exitCode = 1;
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
1657
2265
|
/**
|
|
1658
2266
|
* Extract an enriched error message from a caught value.
|
|
1659
2267
|
* Includes structured API error data when available.
|
|
@@ -1663,6 +2271,18 @@ function enrichedErrorMessage(err) {
|
|
|
1663
2271
|
if (err && typeof err === "object" && "data" in err) msg += ` — ${JSON.stringify(err.data)}`;
|
|
1664
2272
|
return msg;
|
|
1665
2273
|
}
|
|
2274
|
+
function navigationPushFailure(err, remoteChangesComplete) {
|
|
2275
|
+
const error = enrichedErrorMessage(err);
|
|
2276
|
+
if (err instanceof NavigationRemoteStateUnknownError) return {
|
|
2277
|
+
advanceSnapshot: true,
|
|
2278
|
+
error
|
|
2279
|
+
};
|
|
2280
|
+
if (!remoteChangesComplete) return { error };
|
|
2281
|
+
return {
|
|
2282
|
+
advanceSnapshot: true,
|
|
2283
|
+
error: `Remote changes were applied before the push failed: ${error}. Run fluid portal pull before editing or pushing again.`
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
1666
2286
|
/**
|
|
1667
2287
|
* Best-effort "invisible git" sync: commit the local project and push it
|
|
1668
2288
|
* to the portal's provisioned GitHub repo before the content push.
|
|
@@ -1957,25 +2577,129 @@ function resolveNavigationItemScreenIds(items, mappings) {
|
|
|
1957
2577
|
* The API reconciliation logic requires a flat list to correctly
|
|
1958
2578
|
* compare against the flat server response.
|
|
1959
2579
|
*/
|
|
1960
|
-
function flattenNavigationItems(items) {
|
|
1961
|
-
const flat = [];
|
|
2580
|
+
function flattenNavigationItems(items, treeParentIndex, flat = []) {
|
|
1962
2581
|
for (const item of items) {
|
|
1963
2582
|
const { children, ...rest } = item;
|
|
1964
|
-
flat.
|
|
1965
|
-
|
|
2583
|
+
const itemIndex = flat.length;
|
|
2584
|
+
flat.push({
|
|
2585
|
+
...rest,
|
|
2586
|
+
treeParentIndex
|
|
2587
|
+
});
|
|
2588
|
+
if (children && children.length > 0) flattenNavigationItems(children, itemIndex, flat);
|
|
2589
|
+
}
|
|
2590
|
+
return flat;
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* The navigation-items index returns root items with descendants nested under
|
|
2594
|
+
* `children`. Flatten descendants first so removals cannot orphan or revisit a
|
|
2595
|
+
* child when its parent is also deleted.
|
|
2596
|
+
*/
|
|
2597
|
+
function flattenServerNavigationItems(items, flat = []) {
|
|
2598
|
+
for (const item of items) {
|
|
2599
|
+
flattenServerNavigationItems(item.children ?? [], flat);
|
|
2600
|
+
flat.push(item);
|
|
1966
2601
|
}
|
|
1967
2602
|
return flat;
|
|
1968
2603
|
}
|
|
2604
|
+
function resolveNavigationItemParentId(item, serverIdByItemIndex) {
|
|
2605
|
+
if (item.treeParentIndex != null) {
|
|
2606
|
+
const serverParentId = serverIdByItemIndex.get(item.treeParentIndex);
|
|
2607
|
+
if (serverParentId == null) throw new Error("Failed to resolve the server ID for a nested navigation item's parent");
|
|
2608
|
+
return serverParentId;
|
|
2609
|
+
}
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
async function resolveCreatedNavigationItemId(client, defId, navigationId, responseId, knownServerIds, requestedItem, file) {
|
|
2613
|
+
if (responseId != null) {
|
|
2614
|
+
knownServerIds.add(responseId);
|
|
2615
|
+
return responseId;
|
|
2616
|
+
}
|
|
2617
|
+
let response;
|
|
2618
|
+
try {
|
|
2619
|
+
response = await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navigationId);
|
|
2620
|
+
} catch (recoveryError) {
|
|
2621
|
+
throw new NavigationRemoteStateUnknownError(`Remote navigation state is unknown because item creation succeeded without returning its required ID, and the recovery read failed: ${enrichedErrorMessage(recoveryError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2622
|
+
}
|
|
2623
|
+
const unseenItems = flattenServerNavigationItems(response.navigation_items ?? []).filter((item) => !knownServerIds.has(item.id));
|
|
2624
|
+
const matchingItems = unseenItems.filter((item) => (item.label ?? "") === requestedItem.label && (item.position ?? 0) === requestedItem.position && (item.icon ?? void 0) === requestedItem.icon && (item.screen_id ?? void 0) === requestedItem.screen_id && (item.slug ?? void 0) === requestedItem.slug && (item.source ?? "user") === (requestedItem.source ?? "user") && (item.parent_id ?? null) === (requestedItem.parent_id ?? null));
|
|
2625
|
+
if (unseenItems.length !== 1 || matchingItems.length !== 1) throw new NavigationRemoteStateUnknownError(`Remote navigation state is unknown because item creation succeeded without returning its required ID, and recovery found ${matchingItems.length} matching items among ${unseenItems.length} new server items. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2626
|
+
const recoveredId = matchingItems[0]?.id;
|
|
2627
|
+
if (recoveredId == null) throw new NavigationRemoteStateUnknownError(`Remote navigation state is unknown because item creation succeeded without returning its required ID. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2628
|
+
knownServerIds.add(recoveredId);
|
|
2629
|
+
return recoveredId;
|
|
2630
|
+
}
|
|
2631
|
+
function assignNavigationItemId(navigation, targetIndex, serverId) {
|
|
2632
|
+
let currentIndex = 0;
|
|
2633
|
+
let previousId;
|
|
2634
|
+
function assign(items) {
|
|
2635
|
+
return items.map((item) => {
|
|
2636
|
+
const itemIndex = currentIndex++;
|
|
2637
|
+
const children = assign(item.children ?? []);
|
|
2638
|
+
if (itemIndex !== targetIndex) return {
|
|
2639
|
+
...item,
|
|
2640
|
+
children
|
|
2641
|
+
};
|
|
2642
|
+
previousId = item.id;
|
|
2643
|
+
return {
|
|
2644
|
+
...item,
|
|
2645
|
+
id: serverId,
|
|
2646
|
+
children
|
|
2647
|
+
};
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
function rewriteParentIds(items) {
|
|
2651
|
+
return items.map((item) => ({
|
|
2652
|
+
...item,
|
|
2653
|
+
parent_id: item.parent_id === previousId ? serverId : item.parent_id,
|
|
2654
|
+
children: rewriteParentIds(item.children ?? [])
|
|
2655
|
+
}));
|
|
2656
|
+
}
|
|
2657
|
+
const navigationItems = assign(navigation.navigation_items);
|
|
2658
|
+
return {
|
|
2659
|
+
...navigation,
|
|
2660
|
+
navigation_items: previousId == null || previousId === serverId ? navigationItems : rewriteParentIds(navigationItems)
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
async function persistCreatedNavigationItemId(client, defId, navigationId, portalDir, file, navigation, itemIndex, serverId) {
|
|
2664
|
+
const updated = assignNavigationItemId(navigation, itemIndex, serverId);
|
|
2665
|
+
try {
|
|
2666
|
+
await atomicWriteFile(join(portalDir, file), JSON.stringify(updated, null, 2) + "\n");
|
|
2667
|
+
} catch (persistError) {
|
|
2668
|
+
try {
|
|
2669
|
+
await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navigationId, serverId);
|
|
2670
|
+
} catch (rollbackError) {
|
|
2671
|
+
throw new NavigationRemoteStateUnknownError(`Failed to persist navigation item ${serverId} locally: ${enrichedErrorMessage(persistError)}. Automatic rollback also failed: ${enrichedErrorMessage(rollbackError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
|
|
2672
|
+
}
|
|
2673
|
+
throw persistError;
|
|
2674
|
+
}
|
|
2675
|
+
return updated;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Replace a pushed navigation file with the authoritative server tree.
|
|
2679
|
+
* New navigation items receive their IDs from the server, so the local file
|
|
2680
|
+
* must be reconciled before the push snapshot hashes it.
|
|
2681
|
+
*/
|
|
2682
|
+
async function reconcileNavigationFile(client, defId, navId, portalDir, file, local, mappings) {
|
|
2683
|
+
const response = await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId);
|
|
2684
|
+
const reconciled = {
|
|
2685
|
+
...local,
|
|
2686
|
+
navigation_items: transformNavigationItems(response.navigation_items ?? [], buildIdToSlugMap(mappings.screens))
|
|
2687
|
+
};
|
|
2688
|
+
await atomicWriteFile(join(portalDir, file), JSON.stringify(reconciled, null, 2) + "\n");
|
|
2689
|
+
}
|
|
1969
2690
|
/**
|
|
1970
2691
|
* Push navigation changes to the API.
|
|
1971
2692
|
*/
|
|
1972
2693
|
async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
1973
2694
|
const results = [];
|
|
1974
2695
|
let currentMappings = mappings;
|
|
1975
|
-
|
|
2696
|
+
const newFiles = changes.new.filter((file) => resolveSlugToId(currentMappings, "navigations", slugFromPath(file)) == null);
|
|
2697
|
+
const changedFiles = [...changes.changed, ...changes.new.filter((file) => resolveSlugToId(currentMappings, "navigations", slugFromPath(file)) != null)];
|
|
2698
|
+
for (const file of newFiles) {
|
|
1976
2699
|
const slug = slugFromPath(file);
|
|
2700
|
+
let remoteChangesComplete = false;
|
|
1977
2701
|
try {
|
|
1978
|
-
|
|
2702
|
+
let local = await readPortalFile(portalDir, file);
|
|
1979
2703
|
const newId = (await fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
|
|
1980
2704
|
name: local.name,
|
|
1981
2705
|
platform: local.platform
|
|
@@ -1983,10 +2707,11 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
1983
2707
|
if (newId != null) {
|
|
1984
2708
|
currentMappings = updateMapping(currentMappings, "navigations", slug, newId);
|
|
1985
2709
|
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const
|
|
2710
|
+
const serverIdByItemIndex = /* @__PURE__ */ new Map();
|
|
2711
|
+
const knownServerIds = /* @__PURE__ */ new Set();
|
|
2712
|
+
for (const [itemIndex, item] of resolvedItems.entries()) {
|
|
2713
|
+
const resolvedParentId = resolveNavigationItemParentId(item, serverIdByItemIndex);
|
|
2714
|
+
const createBody = {
|
|
1990
2715
|
label: item.label ?? "",
|
|
1991
2716
|
position: item.position ?? 0,
|
|
1992
2717
|
icon: item.icon ?? void 0,
|
|
@@ -1994,9 +2719,13 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
1994
2719
|
slug: item.slug ?? void 0,
|
|
1995
2720
|
source: item.source ?? void 0,
|
|
1996
2721
|
parent_id: resolvedParentId
|
|
1997
|
-
}
|
|
1998
|
-
|
|
2722
|
+
};
|
|
2723
|
+
const createdId = await resolveCreatedNavigationItemId(client, defId, newId, (await fluid_os_v0_create_fluid_osnavigation_item(client, defId, newId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
|
|
2724
|
+
local = await persistCreatedNavigationItemId(client, defId, newId, portalDir, file, local, itemIndex, createdId);
|
|
2725
|
+
serverIdByItemIndex.set(itemIndex, createdId);
|
|
1999
2726
|
}
|
|
2727
|
+
remoteChangesComplete = true;
|
|
2728
|
+
await reconcileNavigationFile(client, defId, newId, portalDir, file, local, currentMappings);
|
|
2000
2729
|
}
|
|
2001
2730
|
results.push({
|
|
2002
2731
|
file,
|
|
@@ -2008,7 +2737,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2008
2737
|
file,
|
|
2009
2738
|
action: "created",
|
|
2010
2739
|
success: false,
|
|
2011
|
-
|
|
2740
|
+
...navigationPushFailure(err, remoteChangesComplete)
|
|
2012
2741
|
});
|
|
2013
2742
|
return {
|
|
2014
2743
|
results,
|
|
@@ -2016,7 +2745,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2016
2745
|
};
|
|
2017
2746
|
}
|
|
2018
2747
|
}
|
|
2019
|
-
for (const file of
|
|
2748
|
+
for (const file of changedFiles) {
|
|
2020
2749
|
const slug = slugFromPath(file);
|
|
2021
2750
|
const navId = resolveSlugToId(currentMappings, "navigations", slug);
|
|
2022
2751
|
if (navId == null) {
|
|
@@ -2031,20 +2760,21 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2031
2760
|
mappings: currentMappings
|
|
2032
2761
|
};
|
|
2033
2762
|
}
|
|
2763
|
+
let remoteChangesComplete = false;
|
|
2034
2764
|
try {
|
|
2035
|
-
|
|
2765
|
+
let local = await readPortalFile(portalDir, file);
|
|
2036
2766
|
await fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
|
|
2037
2767
|
name: local.name,
|
|
2038
2768
|
platform: local.platform
|
|
2039
2769
|
} });
|
|
2040
2770
|
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2041
|
-
const serverItems = (await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? [];
|
|
2771
|
+
const serverItems = flattenServerNavigationItems((await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? []);
|
|
2042
2772
|
const serverById = new Map(serverItems.map((s) => [s.id, s]));
|
|
2773
|
+
const knownServerIds = new Set(serverById.keys());
|
|
2043
2774
|
const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
|
|
2044
|
-
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
|
|
2775
|
+
const serverIdByItemIndex = /* @__PURE__ */ new Map();
|
|
2776
|
+
for (const [itemIndex, item] of resolvedItems.entries()) {
|
|
2777
|
+
const resolvedParentId = resolveNavigationItemParentId(item, serverIdByItemIndex);
|
|
2048
2778
|
const body = {
|
|
2049
2779
|
label: item.label,
|
|
2050
2780
|
position: item.position,
|
|
@@ -2054,16 +2784,23 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2054
2784
|
source: item.source ?? void 0,
|
|
2055
2785
|
parent_id: resolvedParentId
|
|
2056
2786
|
};
|
|
2057
|
-
if (item.id && serverById.has(item.id))
|
|
2058
|
-
|
|
2059
|
-
|
|
2787
|
+
if (item.id && serverById.has(item.id)) {
|
|
2788
|
+
await fluid_os_v0_update_fluid_osnavigation_item(client, defId, navId, item.id, { navigation_item: body });
|
|
2789
|
+
serverIdByItemIndex.set(itemIndex, item.id);
|
|
2790
|
+
} else {
|
|
2791
|
+
const createBody = {
|
|
2060
2792
|
...body,
|
|
2061
2793
|
label: body.label ?? "",
|
|
2062
2794
|
position: body.position ?? 0
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2795
|
+
};
|
|
2796
|
+
const createdId = await resolveCreatedNavigationItemId(client, defId, navId, (await fluid_os_v0_create_fluid_osnavigation_item(client, defId, navId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
|
|
2797
|
+
local = await persistCreatedNavigationItemId(client, defId, navId, portalDir, file, local, itemIndex, createdId);
|
|
2798
|
+
serverIdByItemIndex.set(itemIndex, createdId);
|
|
2065
2799
|
}
|
|
2066
2800
|
}
|
|
2801
|
+
for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navId, serverItem.id);
|
|
2802
|
+
remoteChangesComplete = true;
|
|
2803
|
+
await reconcileNavigationFile(client, defId, navId, portalDir, file, local, currentMappings);
|
|
2067
2804
|
results.push({
|
|
2068
2805
|
file,
|
|
2069
2806
|
action: "updated",
|
|
@@ -2074,7 +2811,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2074
2811
|
file,
|
|
2075
2812
|
action: "updated",
|
|
2076
2813
|
success: false,
|
|
2077
|
-
|
|
2814
|
+
...navigationPushFailure(err, remoteChangesComplete)
|
|
2078
2815
|
});
|
|
2079
2816
|
return {
|
|
2080
2817
|
results,
|
|
@@ -2274,7 +3011,7 @@ function printPushReport(results, skippedPhases) {
|
|
|
2274
3011
|
for (const phase of skippedPhases) console.log(chalk.yellow(" " + phase));
|
|
2275
3012
|
}
|
|
2276
3013
|
}
|
|
2277
|
-
const pushCommand = new Command("push").description("Push local portal content changes to the Fluid OS API").option("--yes", "Skip confirmation prompt").action(async (options) => {
|
|
3014
|
+
const pushCommand = new Command("push").description("Push local portal content changes to the Fluid OS API").option("--yes", "Skip confirmation prompt").option("--activate", "Create and activate a live version after a successful push").action(async (options) => {
|
|
2278
3015
|
const cwd = process.cwd();
|
|
2279
3016
|
const portalDir = join(cwd, PORTAL_DIR);
|
|
2280
3017
|
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR$1);
|
|
@@ -2313,6 +3050,10 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2313
3050
|
if (totalChanges === 0) {
|
|
2314
3051
|
spinner.succeed("Nothing to push.");
|
|
2315
3052
|
console.log();
|
|
3053
|
+
if (options.activate) {
|
|
3054
|
+
await activateDraft(definitionId);
|
|
3055
|
+
console.log();
|
|
3056
|
+
}
|
|
2316
3057
|
return;
|
|
2317
3058
|
}
|
|
2318
3059
|
spinner.succeed(`Found ${totalChanges} change(s)`);
|
|
@@ -2334,16 +3075,16 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2334
3075
|
console.log();
|
|
2335
3076
|
}
|
|
2336
3077
|
const changes = categorizeChanges(diff);
|
|
2337
|
-
spinner.start("Validating
|
|
3078
|
+
spinner.start("Validating portal files...");
|
|
2338
3079
|
const validationErrors = await validateCrossReferences(portalDir, mappings, changes);
|
|
2339
3080
|
if (validationErrors.length > 0) {
|
|
2340
|
-
spinner.fail("
|
|
3081
|
+
spinner.fail("Portal file validation failed");
|
|
2341
3082
|
console.log();
|
|
2342
3083
|
for (const err of validationErrors) console.log(chalk.red(" " + err.file + ": ") + err.message);
|
|
2343
3084
|
console.log();
|
|
2344
3085
|
process.exit(1);
|
|
2345
3086
|
}
|
|
2346
|
-
spinner.succeed("
|
|
3087
|
+
spinner.succeed("Portal files valid");
|
|
2347
3088
|
spinner.start("Authenticating...");
|
|
2348
3089
|
let client;
|
|
2349
3090
|
try {
|
|
@@ -2414,10 +3155,10 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2414
3155
|
else spinner.succeed("Phase 3 complete");
|
|
2415
3156
|
}
|
|
2416
3157
|
await writeMappings(portalSyncDir, currentMappings);
|
|
2417
|
-
const
|
|
2418
|
-
if (
|
|
3158
|
+
const filesToSnapshot = new Set(allResults.filter((r) => r.success || r.advanceSnapshot).map((r) => r.file));
|
|
3159
|
+
if (filesToSnapshot.size > 0) {
|
|
2419
3160
|
const updatedHashes = { ...snapshot.files };
|
|
2420
|
-
for (const file of
|
|
3161
|
+
for (const file of filesToSnapshot) {
|
|
2421
3162
|
const fullPath = join(portalDir, file);
|
|
2422
3163
|
if (existsSync(fullPath)) updatedHashes[file] = await computeFileHash(fullPath);
|
|
2423
3164
|
else delete updatedHashes[file];
|
|
@@ -2428,11 +3169,20 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2428
3169
|
});
|
|
2429
3170
|
}
|
|
2430
3171
|
console.log();
|
|
2431
|
-
|
|
3172
|
+
const allSucceeded = allResults.every((r) => r.success);
|
|
3173
|
+
if (allSucceeded && !aborted) console.log(chalk.green.bold("Push complete!"));
|
|
2432
3174
|
else console.log(chalk.yellow.bold("Push completed with issues:"));
|
|
2433
3175
|
console.log();
|
|
2434
3176
|
printPushReport(allResults, skippedPhases);
|
|
2435
3177
|
console.log();
|
|
3178
|
+
if (!allSucceeded || aborted) {
|
|
3179
|
+
process.exitCode = 1;
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
if (options.activate) {
|
|
3183
|
+
await activateDraft(definitionId, client);
|
|
3184
|
+
console.log();
|
|
3185
|
+
}
|
|
2436
3186
|
});
|
|
2437
3187
|
//#endregion
|
|
2438
3188
|
//#region src/commands/clone.ts
|
|
@@ -2442,21 +3192,85 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2442
3192
|
* `./<slug>/`. Mirrors `fluid mist clone` (credential → clone), but
|
|
2443
3193
|
* against the portal git-credential endpoint.
|
|
2444
3194
|
*
|
|
2445
|
-
*
|
|
2446
|
-
* the
|
|
2447
|
-
*
|
|
3195
|
+
* When the credential endpoint reports that no repository is available
|
|
3196
|
+
* (the company's Git integration isn't connected, the repo is still
|
|
3197
|
+
* unprovisioned after the retry budget, or the endpoint isn't deployed),
|
|
3198
|
+
* clone falls back to scaffolding the portal locally from the bundled
|
|
3199
|
+
* starter template instead of failing. Local files plus API sync
|
|
3200
|
+
* (`fluid portal push`/`pull`) are a fully functional portal — Git is
|
|
3201
|
+
* an optional sync channel, not a prerequisite — so the user still
|
|
3202
|
+
* lands in a working project and Git history can attach later.
|
|
2448
3203
|
*/
|
|
2449
3204
|
function getBaseUrl() {
|
|
2450
3205
|
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
2451
3206
|
}
|
|
2452
|
-
function
|
|
3207
|
+
async function removeIncompleteCheckout(targetDir) {
|
|
3208
|
+
try {
|
|
3209
|
+
await rm(targetDir, {
|
|
3210
|
+
recursive: true,
|
|
3211
|
+
force: true
|
|
3212
|
+
});
|
|
3213
|
+
return null;
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
return error instanceof Error ? error.message : String(error);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
function createClient$1(token) {
|
|
2453
3219
|
return createFetchClient({
|
|
2454
3220
|
baseUrl: getBaseUrl(),
|
|
2455
3221
|
getAuthToken: () => token
|
|
2456
3222
|
});
|
|
2457
3223
|
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Human message for the git-sync-unavailable fallback, split by conflict
|
|
3226
|
+
* type. `not_connected` is a durable state (the company's Git integration
|
|
3227
|
+
* isn't connected), so it must not claim that waiting will fix anything;
|
|
3228
|
+
* `not_provisioned` genuinely may resolve later, so it says so.
|
|
3229
|
+
*/
|
|
3230
|
+
function gitSyncUnavailableNotice(outcome) {
|
|
3231
|
+
if (outcome.conflictType === "not_connected") return `This company's Git integration isn't connected, so the portal has no Git repository to clone. Creating the portal locally from the starter template instead — ${chalk.cyan("fluid portal push")}/${chalk.cyan("pull")} sync content without Git, and Git sync can attach once the integration is connected.`;
|
|
3232
|
+
if (outcome.conflictType === "not_provisioned") return "The portal's Git repository isn't provisioned yet. Creating the portal locally from the starter template so you can start now — Git sync can attach once the repository is ready.";
|
|
3233
|
+
return `Git sync isn't available (${outcome.reason}). Creating the portal locally from the starter template instead.`;
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* A git-less fallback scaffold that failed or was interrupted leaves the
|
|
3237
|
+
* pending marker behind with no `.git`. Only that exact shape is safe to
|
|
3238
|
+
* resume — anything else is an unrelated pre-existing directory.
|
|
3239
|
+
*/
|
|
3240
|
+
async function canResumeGitlessPortalScaffold(targetDir) {
|
|
3241
|
+
return await fileExists(join(targetDir, ".fluid-portal-scaffold-pending")) && !existsSync(join(targetDir, ".git"));
|
|
3242
|
+
}
|
|
3243
|
+
async function canResumePortalScaffold(targetDir, credentialRemoteUrl) {
|
|
3244
|
+
if (!await fileExists(join(targetDir, ".fluid-portal-scaffold-pending")) || !await isGitRepo(targetDir)) return false;
|
|
3245
|
+
const origin = await getOriginUrl(targetDir);
|
|
3246
|
+
if (!origin) return false;
|
|
3247
|
+
const expected = cleanGitRemoteUrl(credentialRemoteUrl);
|
|
3248
|
+
return origin.replace(/\/$/, "") === expected.replace(/\/$/, "");
|
|
3249
|
+
}
|
|
3250
|
+
function resolveDefinition(definitions, appName, rawDefinitionId) {
|
|
3251
|
+
if (rawDefinitionId !== void 0) {
|
|
3252
|
+
if (!/^[1-9]\d*$/.test(rawDefinitionId)) throw new Error("--id must be a positive integer");
|
|
3253
|
+
const definitionId = Number.parseInt(rawDefinitionId, 10);
|
|
3254
|
+
const match = definitions.find((definition) => definition.id === definitionId);
|
|
3255
|
+
if (!match) throw new Error(`No definition found with id ${definitionId}.`);
|
|
3256
|
+
return {
|
|
3257
|
+
id: definitionId,
|
|
3258
|
+
name: match.name ?? `portal-${definitionId}`
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
if (!appName) return null;
|
|
3262
|
+
const match = definitions.find((definition) => definition.name?.toLowerCase() === appName.toLowerCase());
|
|
3263
|
+
if (!match || match.id == null) {
|
|
3264
|
+
const available = definitions.map((definition) => chalk.cyan(definition.name ?? "(unnamed)")).join(", ");
|
|
3265
|
+
throw new Error(`No definition found matching "${appName}". Available: ${available}`);
|
|
3266
|
+
}
|
|
3267
|
+
return {
|
|
3268
|
+
id: match.id,
|
|
3269
|
+
name: match.name ?? `portal-${match.id}`
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
2458
3272
|
/** Select a definition by --app name, else interactively. */
|
|
2459
|
-
async function selectDefinition(client, appName) {
|
|
3273
|
+
async function selectDefinition(client, appName, rawDefinitionId) {
|
|
2460
3274
|
const definitions = [];
|
|
2461
3275
|
let page = 1;
|
|
2462
3276
|
while (true) {
|
|
@@ -2471,17 +3285,8 @@ async function selectDefinition(client, appName) {
|
|
|
2471
3285
|
page++;
|
|
2472
3286
|
}
|
|
2473
3287
|
if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
if (!match || match.id == null) {
|
|
2477
|
-
const available = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
2478
|
-
throw new Error(`No definition found matching "${appName}". Available: ${available}`);
|
|
2479
|
-
}
|
|
2480
|
-
return {
|
|
2481
|
-
id: match.id,
|
|
2482
|
-
name: match.name ?? `portal-${match.id}`
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
3288
|
+
const resolved = resolveDefinition(definitions, appName, rawDefinitionId);
|
|
3289
|
+
if (resolved) return resolved;
|
|
2485
3290
|
const { definitionId } = await prompts({
|
|
2486
3291
|
type: "select",
|
|
2487
3292
|
name: "definitionId",
|
|
@@ -2499,9 +3304,8 @@ async function selectDefinition(client, appName) {
|
|
|
2499
3304
|
name: selected.name ?? `portal-${selected.id}`
|
|
2500
3305
|
};
|
|
2501
3306
|
}
|
|
2502
|
-
const cloneCommand = new Command("clone").description("Clone a portal definition's provisioned GitHub repo (mints a fresh token per call)").argument("[app]", "Definition name (prompts to select when omitted)").option("--app <name>", "Definition name (same as the positional argument)").option("--dir <dir>", "Target directory (defaults to ./<slug>/)").action(async (appArg, options) => {
|
|
3307
|
+
const cloneCommand = new Command("clone").description("Clone a portal definition's provisioned GitHub repo (mints a fresh token per call)").argument("[app]", "Definition name (prompts to select when omitted)").option("--app <name>", "Definition name (same as the positional argument)").option("--id <id>", "Definition id (avoids ambiguity when names repeat)").option("--dir <dir>", "Target directory (defaults to ./<slug>/)").action(async (appArg, options) => {
|
|
2503
3308
|
try {
|
|
2504
|
-
await ensureGitAvailable();
|
|
2505
3309
|
const token = getAuthToken();
|
|
2506
3310
|
if (!token) {
|
|
2507
3311
|
const profile = getActiveProfile();
|
|
@@ -2509,32 +3313,99 @@ const cloneCommand = new Command("clone").description("Clone a portal definition
|
|
|
2509
3313
|
}
|
|
2510
3314
|
const client = createClient$1(token);
|
|
2511
3315
|
const spinner = ora("Resolving definition…").start();
|
|
2512
|
-
const definition = await selectDefinition(client, options.app ?? appArg);
|
|
3316
|
+
const definition = await selectDefinition(client, options.app ?? appArg, options.id);
|
|
2513
3317
|
spinner.text = "Minting git credential…";
|
|
2514
|
-
const outcome = await
|
|
3318
|
+
const outcome = await createPortalGitCredentialWithRetry({
|
|
2515
3319
|
baseUrl: getBaseUrl(),
|
|
2516
3320
|
token,
|
|
2517
|
-
definitionId: definition.id
|
|
3321
|
+
definitionId: definition.id,
|
|
3322
|
+
onRetry: ({ attempt, totalAttempts }) => {
|
|
3323
|
+
spinner.text = `Preparing Git repository… retrying (${attempt}/${totalAttempts})`;
|
|
3324
|
+
}
|
|
2518
3325
|
});
|
|
2519
|
-
if (outcome.status
|
|
3326
|
+
if (outcome.status === "error") {
|
|
2520
3327
|
spinner.warn(`Can't clone — ${outcome.reason}`);
|
|
2521
3328
|
process.exit(1);
|
|
2522
3329
|
}
|
|
2523
3330
|
const slug = deriveSlug(definition.name);
|
|
2524
3331
|
const targetDir = resolve(options.dir ?? slug);
|
|
2525
|
-
if (
|
|
3332
|
+
if (outcome.status === "unavailable") {
|
|
3333
|
+
spinner.info(gitSyncUnavailableNotice(outcome));
|
|
3334
|
+
const resumeGitless = existsSync(targetDir) && await canResumeGitlessPortalScaffold(targetDir);
|
|
3335
|
+
if (existsSync(targetDir) && !resumeGitless) {
|
|
3336
|
+
spinner.fail(`Directory ${targetDir} already exists. Pass --dir or remove it first.`);
|
|
3337
|
+
process.exit(1);
|
|
3338
|
+
}
|
|
3339
|
+
if (!resumeGitless) {
|
|
3340
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
3341
|
+
try {
|
|
3342
|
+
await mkdir(targetDir);
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
if (error.code === "EEXIST") {
|
|
3345
|
+
spinner.fail(`Directory ${targetDir} was created by another process. Choose a different --dir or retry after it finishes.`);
|
|
3346
|
+
process.exit(1);
|
|
3347
|
+
}
|
|
3348
|
+
throw error;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
try {
|
|
3352
|
+
spinner.text = resumeGitless ? "Resuming portal scaffold…" : "Scaffolding portal from the starter template…";
|
|
3353
|
+
await scaffoldPortalProjectIntoExistingDirectory(targetDir, {
|
|
3354
|
+
name: slug,
|
|
3355
|
+
installDeps: false,
|
|
3356
|
+
selectedPages: [],
|
|
3357
|
+
profileName: getActiveProfile()?.name ?? ""
|
|
3358
|
+
});
|
|
3359
|
+
} catch (err) {
|
|
3360
|
+
spinner.fail(`Git sync is unavailable and the local scaffold failed at ${targetDir}. Fix the reported path or permission conflict and retry.`);
|
|
3361
|
+
throw err;
|
|
3362
|
+
}
|
|
3363
|
+
spinner.succeed(`Created ${chalk.cyan(targetDir)} from the starter template (no Git history yet)`);
|
|
3364
|
+
console.log(chalk.dim(` cd ${targetDir}`));
|
|
3365
|
+
console.log(chalk.dim(` ${chalk.cyan("fluid portal pull")} to sync content`));
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
await ensureGitAvailable();
|
|
3369
|
+
const resumeScaffold = existsSync(targetDir) && await canResumePortalScaffold(targetDir, outcome.credential.remote_url);
|
|
3370
|
+
if (existsSync(targetDir) && !resumeScaffold) {
|
|
2526
3371
|
spinner.fail(`Directory ${targetDir} already exists. Pass --dir or remove it first.`);
|
|
2527
3372
|
process.exit(1);
|
|
2528
3373
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3374
|
+
if (!resumeScaffold) {
|
|
3375
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
3376
|
+
try {
|
|
3377
|
+
await mkdir(targetDir);
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
if (error.code === "EEXIST") {
|
|
3380
|
+
spinner.fail(`Directory ${targetDir} was created by another process. Choose a different --dir or retry after it finishes.`);
|
|
3381
|
+
process.exit(1);
|
|
3382
|
+
}
|
|
3383
|
+
throw error;
|
|
3384
|
+
}
|
|
3385
|
+
spinner.text = `Cloning into ${slug}/…`;
|
|
3386
|
+
try {
|
|
3387
|
+
await gitClone(outcome.credential.remote_url, targetDir);
|
|
3388
|
+
} catch (err) {
|
|
3389
|
+
const cleanupError = await removeIncompleteCheckout(targetDir);
|
|
3390
|
+
spinner.fail(cleanupError ? `git clone failed, and Fluid could not remove the incomplete checkout: ${cleanupError}` : "git clone failed; removed the incomplete checkout");
|
|
3391
|
+
throw err;
|
|
3392
|
+
}
|
|
3393
|
+
excludeLocalStateFromGit(targetDir);
|
|
3394
|
+
}
|
|
3395
|
+
if (resumeScaffold || !await fileExists(join(targetDir, "package.json"))) try {
|
|
3396
|
+
spinner.text = resumeScaffold ? "Resuming runnable portal scaffold…" : "Adding runnable portal scaffold…";
|
|
3397
|
+
await scaffoldPortalProjectIntoExistingDirectory(targetDir, {
|
|
3398
|
+
name: slug,
|
|
3399
|
+
installDeps: false,
|
|
3400
|
+
selectedPages: [],
|
|
3401
|
+
profileName: getActiveProfile()?.name ?? ""
|
|
3402
|
+
});
|
|
2532
3403
|
} catch (err) {
|
|
2533
|
-
spinner.fail(
|
|
3404
|
+
spinner.fail(`Repository cloned at ${targetDir}, but the runnable scaffold failed. The provisioned Git history and repository files were preserved; fix the reported path or permission conflict and retry the scaffold.`);
|
|
2534
3405
|
throw err;
|
|
2535
3406
|
}
|
|
2536
3407
|
spinner.succeed(`Cloned into ${chalk.cyan(targetDir)}`);
|
|
2537
|
-
console.log(chalk.dim(` cd ${
|
|
3408
|
+
console.log(chalk.dim(` cd ${targetDir}`));
|
|
2538
3409
|
console.log(chalk.dim(` ${chalk.cyan("fluid portal pull")} to sync content`));
|
|
2539
3410
|
} catch (error) {
|
|
2540
3411
|
console.log();
|
|
@@ -2742,206 +3613,19 @@ export const widgetPackage = defineWidgetPackage({
|
|
|
2742
3613
|
}
|
|
2743
3614
|
const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
|
|
2744
3615
|
//#endregion
|
|
2745
|
-
//#region src/
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
} : void 0;
|
|
2757
|
-
}
|
|
2758
|
-
async function loadSourceWidgetPackages(projectDir) {
|
|
2759
|
-
const config = await resolvePortalWidgetSourceConfig(projectDir);
|
|
2760
|
-
if (!config) return success([]);
|
|
2761
|
-
let tempDir;
|
|
2762
|
-
try {
|
|
2763
|
-
tempDir = await createTempDirectory(projectDir);
|
|
2764
|
-
const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
|
|
2765
|
-
const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
|
|
2766
|
-
await fs.writeFile(extractFile, createSourcePackageExtractorScript({
|
|
2767
|
-
projectDir,
|
|
2768
|
-
configPath: config.path
|
|
2769
|
-
}), {
|
|
2770
|
-
encoding: "utf-8",
|
|
2771
|
-
flag: "wx"
|
|
2772
|
-
});
|
|
2773
|
-
await execa(process.execPath, [
|
|
2774
|
-
TSX_CLI_PATH,
|
|
2775
|
-
extractFile,
|
|
2776
|
-
outputFile
|
|
2777
|
-
], {
|
|
2778
|
-
cwd: projectDir,
|
|
2779
|
-
stdio: "pipe",
|
|
2780
|
-
env: {
|
|
2781
|
-
...process.env,
|
|
2782
|
-
NODE_ENV: "production"
|
|
2783
|
-
}
|
|
2784
|
-
});
|
|
2785
|
-
const outputResult = await readSourcePackageExtractorOutput(outputFile);
|
|
2786
|
-
if (!outputResult.success) return outputResult;
|
|
2787
|
-
const parsed = outputResult.value;
|
|
2788
|
-
if (parsed.length === 0) return success([]);
|
|
2789
|
-
return success(parsed);
|
|
2790
|
-
} catch (err) {
|
|
2791
|
-
const error = err;
|
|
2792
|
-
return failure({
|
|
2793
|
-
code: "CONFIG_LOAD_FAILED",
|
|
2794
|
-
message: `Failed to load widget packages from ${config.relativePath}`,
|
|
2795
|
-
details: error.stderr ?? error.message ?? String(err)
|
|
2796
|
-
});
|
|
2797
|
-
} finally {
|
|
2798
|
-
if (tempDir) await fs.remove(tempDir).catch(() => {});
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
async function createTempDirectory(projectDir) {
|
|
2802
|
-
const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
|
|
2803
|
-
try {
|
|
2804
|
-
await fs.ensureDir(projectTmpDir);
|
|
2805
|
-
return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
|
|
2806
|
-
} catch (err) {
|
|
2807
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2808
|
-
throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
async function readSourcePackageExtractorOutput(outputFile) {
|
|
2812
|
-
let output;
|
|
2813
|
-
try {
|
|
2814
|
-
output = await fs.readFile(outputFile, "utf-8");
|
|
2815
|
-
} catch (err) {
|
|
2816
|
-
return failure({
|
|
2817
|
-
code: "CONFIG_LOAD_FAILED",
|
|
2818
|
-
message: "Widget package extractor did not write an output file",
|
|
2819
|
-
details: err instanceof Error ? err.message : String(err)
|
|
2820
|
-
});
|
|
2821
|
-
}
|
|
2822
|
-
let parsed;
|
|
2823
|
-
try {
|
|
2824
|
-
parsed = JSON.parse(output);
|
|
2825
|
-
} catch {
|
|
2826
|
-
return failure({
|
|
2827
|
-
code: "INVALID_FORMAT",
|
|
2828
|
-
message: "Failed to parse widget package output file as JSON",
|
|
2829
|
-
details: `Output was: ${output.slice(0, 200)}`
|
|
2830
|
-
});
|
|
3616
|
+
//#region ../widget-tooling/src/errors.ts
|
|
3617
|
+
var WidgetToolingError = class extends Error {
|
|
3618
|
+
code;
|
|
3619
|
+
details;
|
|
3620
|
+
diagnostics;
|
|
3621
|
+
constructor(code, message, options = {}) {
|
|
3622
|
+
super(message, options);
|
|
3623
|
+
this.name = "WidgetToolingError";
|
|
3624
|
+
this.code = code;
|
|
3625
|
+
this.details = options.details;
|
|
3626
|
+
this.diagnostics = options.diagnostics;
|
|
2831
3627
|
}
|
|
2832
|
-
|
|
2833
|
-
code: "INVALID_FORMAT",
|
|
2834
|
-
message: "Widget package extractor output file had an invalid sentinel"
|
|
2835
|
-
});
|
|
2836
|
-
if (!Array.isArray(parsed.data)) return failure({
|
|
2837
|
-
code: "INVALID_FORMAT",
|
|
2838
|
-
message: "Extracted widget package output is not an array",
|
|
2839
|
-
details: `Expected an array, got: ${typeof parsed.data}`
|
|
2840
|
-
});
|
|
2841
|
-
return success(parsed.data);
|
|
2842
|
-
}
|
|
2843
|
-
function createSourcePackageExtractorScript(options) {
|
|
2844
|
-
return `
|
|
2845
|
-
import fs from "node:fs/promises";
|
|
2846
|
-
import path from "node:path";
|
|
2847
|
-
import { createServer, normalizePath } from "vite";
|
|
2848
|
-
|
|
2849
|
-
const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
|
|
2850
|
-
const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
|
|
2851
|
-
const projectRoot = ${JSON.stringify(options.projectDir)};
|
|
2852
|
-
const widgetConfigPath = ${JSON.stringify(options.configPath)};
|
|
2853
|
-
const outputPath = process.argv[2];
|
|
2854
|
-
if (!outputPath) throw new Error("Missing widget package extractor output path.");
|
|
2855
|
-
|
|
2856
|
-
function toViteModuleId(modulePath) {
|
|
2857
|
-
const relativePath = path.relative(projectRoot, modulePath);
|
|
2858
|
-
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
2859
|
-
return "/" + normalizePath(relativePath);
|
|
2860
|
-
}
|
|
2861
|
-
return normalizePath(modulePath);
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
|
-
const server = await createServer({
|
|
2865
|
-
root: projectRoot,
|
|
2866
|
-
mode: "production",
|
|
2867
|
-
server: { middlewareMode: true },
|
|
2868
|
-
appType: "custom",
|
|
2869
|
-
logLevel: "error",
|
|
2870
|
-
clearScreen: false,
|
|
2871
|
-
resolve: {
|
|
2872
|
-
conditions: ["module", "browser", "development|production"],
|
|
2873
|
-
},
|
|
2874
|
-
ssr: {
|
|
2875
|
-
noExternal: ["@fluid-app/portal-sdk"],
|
|
2876
|
-
resolve: {
|
|
2877
|
-
conditions: ["module", "node", "development|production"],
|
|
2878
|
-
},
|
|
2879
|
-
},
|
|
2880
|
-
});
|
|
2881
|
-
|
|
2882
|
-
try {
|
|
2883
|
-
const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
|
|
2884
|
-
|
|
2885
|
-
function isSourceWidgetPackage(value) {
|
|
2886
|
-
return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
function collectSourceWidgetPackages(mod) {
|
|
2890
|
-
const candidates = [
|
|
2891
|
-
mod.widgetPackage,
|
|
2892
|
-
...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
|
|
2893
|
-
mod.default,
|
|
2894
|
-
];
|
|
2895
|
-
const byPackageId = new Map();
|
|
2896
|
-
for (const candidate of candidates) {
|
|
2897
|
-
if (!isSourceWidgetPackage(candidate)) continue;
|
|
2898
|
-
if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
|
|
2899
|
-
}
|
|
2900
|
-
return Array.from(byPackageId.values());
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
function serializeWidget(widget) {
|
|
2904
|
-
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
|
|
2905
|
-
return widget;
|
|
2906
|
-
}
|
|
2907
|
-
const { component, ...metadata } = widget;
|
|
2908
|
-
return metadata;
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
function serializePackage(sourcePackage) {
|
|
2912
|
-
return {
|
|
2913
|
-
manifestVersion: sourcePackage.manifestVersion,
|
|
2914
|
-
scope: sourcePackage.scope,
|
|
2915
|
-
packageStableId: sourcePackage.packageStableId,
|
|
2916
|
-
packageId: sourcePackage.packageId,
|
|
2917
|
-
packageType: sourcePackage.packageType,
|
|
2918
|
-
version: sourcePackage.version,
|
|
2919
|
-
cssUrls: sourcePackage.cssUrls,
|
|
2920
|
-
widgets: Array.isArray(sourcePackage.widgets)
|
|
2921
|
-
? sourcePackage.widgets.map(serializeWidget)
|
|
2922
|
-
: sourcePackage.widgets,
|
|
2923
|
-
};
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
await fs.writeFile(
|
|
2927
|
-
outputPath,
|
|
2928
|
-
JSON.stringify({
|
|
2929
|
-
sentinel: OUTPUT_SENTINEL,
|
|
2930
|
-
data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
|
|
2931
|
-
}),
|
|
2932
|
-
"utf-8",
|
|
2933
|
-
);
|
|
2934
|
-
} finally {
|
|
2935
|
-
await server.close();
|
|
2936
|
-
}
|
|
2937
|
-
`;
|
|
2938
|
-
}
|
|
2939
|
-
function isRecord$5(value) {
|
|
2940
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2941
|
-
}
|
|
2942
|
-
//#endregion
|
|
2943
|
-
//#region src/utils/widget-package-artifacts.ts
|
|
2944
|
-
const WIDGET_PACKAGE_BUILDER_VERSION = "1";
|
|
3628
|
+
};
|
|
2945
3629
|
const CONTENT_TYPES = {
|
|
2946
3630
|
".css": "text/css",
|
|
2947
3631
|
".js": "application/javascript",
|
|
@@ -2972,7 +3656,7 @@ function createPublishManifestMetadata(options) {
|
|
|
2972
3656
|
return {
|
|
2973
3657
|
packageId: options.packageId,
|
|
2974
3658
|
version: options.version,
|
|
2975
|
-
builderVersion: options.builderVersion ?? "1",
|
|
3659
|
+
builderVersion: options.builderVersion ?? "0.1.0",
|
|
2976
3660
|
cliVersion: options.cliVersion,
|
|
2977
3661
|
runtimeVersion: options.runtimeVersion,
|
|
2978
3662
|
manifestHash: sha256(options.manifestContent),
|
|
@@ -3059,15 +3743,15 @@ function isJsonValueAtDepth(value, ancestors, depth) {
|
|
|
3059
3743
|
if (typeof value !== "object" || depth > 50 || ancestors.has(value)) return false;
|
|
3060
3744
|
ancestors.add(value);
|
|
3061
3745
|
const prototype = Object.getPrototypeOf(value);
|
|
3062
|
-
const isJson = Array.isArray(value) ? value.every((item) => isJsonValueAtDepth(item, ancestors, depth + 1)) : (prototype === Object.prototype || prototype === null) && isRecord$
|
|
3746
|
+
const isJson = Array.isArray(value) ? value.every((item) => isJsonValueAtDepth(item, ancestors, depth + 1)) : (prototype === Object.prototype || prototype === null) && isRecord$5(value) && Object.entries(value).every(([key, item]) => !UNSAFE_JSON_KEYS.has(key) && isJsonValueAtDepth(item, ancestors, depth + 1));
|
|
3063
3747
|
ancestors.delete(value);
|
|
3064
3748
|
return isJson;
|
|
3065
3749
|
}
|
|
3066
|
-
function isRecord$
|
|
3750
|
+
function isRecord$5(value) {
|
|
3067
3751
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3068
3752
|
}
|
|
3069
3753
|
//#endregion
|
|
3070
|
-
//#region src/
|
|
3754
|
+
//#region ../widget-tooling/src/internal/widget-package-validation.ts
|
|
3071
3755
|
const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
|
|
3072
3756
|
const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
|
|
3073
3757
|
const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
|
|
@@ -3106,7 +3790,17 @@ function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
|
|
|
3106
3790
|
path: "packageType",
|
|
3107
3791
|
message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
|
|
3108
3792
|
});
|
|
3793
|
+
else if (options.owner !== void 0 && sourcePackage.packageType !== options.owner) errors.push({
|
|
3794
|
+
code: "UNSUPPORTED_OWNER",
|
|
3795
|
+
path: "packageType",
|
|
3796
|
+
message: `Widget package metadata must set packageType to "${options.owner}" for this workflow.`
|
|
3797
|
+
});
|
|
3109
3798
|
const resolvedOwner = owner === "droplet" ? "droplet" : "company";
|
|
3799
|
+
if (resolvedOwner === "droplet" && !readNonEmptyTrimmedString(sourcePackage.packageStableId)) errors.push({
|
|
3800
|
+
code: "INVALID_PACKAGE_KEY",
|
|
3801
|
+
path: "packageStableId",
|
|
3802
|
+
message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
|
|
3803
|
+
});
|
|
3110
3804
|
const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
|
|
3111
3805
|
const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
|
|
3112
3806
|
if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
|
|
@@ -3227,7 +3921,7 @@ function validationFailure(error) {
|
|
|
3227
3921
|
};
|
|
3228
3922
|
}
|
|
3229
3923
|
function validateSourcePackageShape(value) {
|
|
3230
|
-
if (!isRecord$
|
|
3924
|
+
if (!isRecord$4(value)) return [{
|
|
3231
3925
|
code: "INVALID_SOURCE_PACKAGE",
|
|
3232
3926
|
message: "Widget source package must be a JSON object."
|
|
3233
3927
|
}];
|
|
@@ -3237,6 +3931,11 @@ function validateSourcePackageShape(value) {
|
|
|
3237
3931
|
validateOptionalStringField(value, "scope", "scope", errors);
|
|
3238
3932
|
validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
|
|
3239
3933
|
validateOptionalStringField(value, "packageType", "packageType", errors);
|
|
3934
|
+
if (Object.prototype.hasOwnProperty.call(value, "navigationOrigins")) errors.push({
|
|
3935
|
+
code: "INVALID_SOURCE_PACKAGE",
|
|
3936
|
+
path: "navigationOrigins",
|
|
3937
|
+
message: "navigationOrigins has been removed. Declare the externalNavigation capability and call allowAnchorUrl(url) for each external link."
|
|
3938
|
+
});
|
|
3240
3939
|
if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
|
|
3241
3940
|
code: "INVALID_SOURCE_PACKAGE",
|
|
3242
3941
|
path: "cssUrls",
|
|
@@ -3255,7 +3954,7 @@ function validateSourcePackageShape(value) {
|
|
|
3255
3954
|
}
|
|
3256
3955
|
value.widgets.forEach((widget, index) => {
|
|
3257
3956
|
const widgetPath = `widgets[${index}]`;
|
|
3258
|
-
if (!isRecord$
|
|
3957
|
+
if (!isRecord$4(widget)) {
|
|
3259
3958
|
errors.push({
|
|
3260
3959
|
code: "INVALID_WIDGET_METADATA",
|
|
3261
3960
|
path: widgetPath,
|
|
@@ -3323,7 +4022,7 @@ function validateCssUrl(value, cssUrlPath, errors) {
|
|
|
3323
4022
|
function validateOptionalJsonRecordField(record, key, path, errors) {
|
|
3324
4023
|
const value = record[key];
|
|
3325
4024
|
if (value === void 0) return;
|
|
3326
|
-
if (isRecord$
|
|
4025
|
+
if (isRecord$4(value) && isJsonValue(value)) return;
|
|
3327
4026
|
errors.push({
|
|
3328
4027
|
code: "INVALID_WIDGET_METADATA",
|
|
3329
4028
|
path,
|
|
@@ -3344,7 +4043,7 @@ function validateOptionalCapabilities(record, key, path, errors) {
|
|
|
3344
4043
|
const names = /* @__PURE__ */ new Set();
|
|
3345
4044
|
value.forEach((candidate, index) => {
|
|
3346
4045
|
const capabilityPath = `${path}[${index}]`;
|
|
3347
|
-
if (!isRecord$
|
|
4046
|
+
if (!isRecord$4(candidate)) {
|
|
3348
4047
|
errors.push({
|
|
3349
4048
|
code: "INVALID_WIDGET_METADATA",
|
|
3350
4049
|
path: capabilityPath,
|
|
@@ -3413,7 +4112,7 @@ function normalizeContainer(value) {
|
|
|
3413
4112
|
function normalizeResizable(value) {
|
|
3414
4113
|
if (typeof value === "boolean") return value;
|
|
3415
4114
|
if (value === "horizontal" || value === "vertical" || value === "both") return value;
|
|
3416
|
-
if (!isRecord$
|
|
4115
|
+
if (!isRecord$4(value)) return false;
|
|
3417
4116
|
const horizontal = value.horizontal === true;
|
|
3418
4117
|
const vertical = value.vertical === true;
|
|
3419
4118
|
if (horizontal && vertical) return "both";
|
|
@@ -3456,20 +4155,221 @@ function isUrlSafeWidgetName(value) {
|
|
|
3456
4155
|
function isUrlSafeSemver(value) {
|
|
3457
4156
|
return SEMVER_URL_SAFE_PATTERN.test(value);
|
|
3458
4157
|
}
|
|
3459
|
-
function isRecord$
|
|
4158
|
+
function isRecord$4(value) {
|
|
3460
4159
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3461
4160
|
}
|
|
3462
4161
|
//#endregion
|
|
3463
|
-
//#region src/
|
|
3464
|
-
const
|
|
3465
|
-
const
|
|
3466
|
-
const
|
|
3467
|
-
const
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
4162
|
+
//#region ../widget-tooling/src/internal/widget-package-config.ts
|
|
4163
|
+
const WIDGET_CONFIG_PATH = "src/widgets.config.ts";
|
|
4164
|
+
const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
|
|
4165
|
+
const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
|
|
4166
|
+
const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
|
|
4167
|
+
const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
|
|
4168
|
+
async function resolvePortalWidgetSourceConfig(projectDir) {
|
|
4169
|
+
const candidate = path.join(projectDir, WIDGET_CONFIG_PATH);
|
|
4170
|
+
return await fs.pathExists(candidate) ? {
|
|
4171
|
+
path: candidate,
|
|
4172
|
+
relativePath: WIDGET_CONFIG_PATH
|
|
4173
|
+
} : void 0;
|
|
4174
|
+
}
|
|
4175
|
+
async function loadSourceWidgetPackages(projectDir) {
|
|
4176
|
+
const config = await resolvePortalWidgetSourceConfig(projectDir);
|
|
4177
|
+
if (!config) return success([]);
|
|
4178
|
+
let tempDir;
|
|
4179
|
+
try {
|
|
4180
|
+
tempDir = await createTempDirectory(projectDir);
|
|
4181
|
+
const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
|
|
4182
|
+
const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
|
|
4183
|
+
await fs.writeFile(extractFile, createSourcePackageExtractorScript({
|
|
4184
|
+
projectDir,
|
|
4185
|
+
configPath: config.path
|
|
4186
|
+
}), {
|
|
4187
|
+
encoding: "utf-8",
|
|
4188
|
+
flag: "wx"
|
|
4189
|
+
});
|
|
4190
|
+
await execa(process.execPath, [
|
|
4191
|
+
TSX_CLI_PATH,
|
|
4192
|
+
extractFile,
|
|
4193
|
+
outputFile
|
|
4194
|
+
], {
|
|
4195
|
+
cwd: projectDir,
|
|
4196
|
+
stdio: "pipe",
|
|
4197
|
+
env: {
|
|
4198
|
+
...process.env,
|
|
4199
|
+
NODE_ENV: "production"
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
const outputResult = await readSourcePackageExtractorOutput(outputFile);
|
|
4203
|
+
if (!outputResult.success) return outputResult;
|
|
4204
|
+
const parsed = outputResult.value;
|
|
4205
|
+
if (parsed.length === 0) return success([]);
|
|
4206
|
+
return success(parsed);
|
|
4207
|
+
} catch (err) {
|
|
4208
|
+
const error = err;
|
|
4209
|
+
return failure({
|
|
4210
|
+
code: "CONFIG_LOAD_FAILED",
|
|
4211
|
+
message: `Failed to load widget packages from ${config.relativePath}`,
|
|
4212
|
+
details: error.stderr ?? error.message ?? String(err)
|
|
4213
|
+
});
|
|
4214
|
+
} finally {
|
|
4215
|
+
if (tempDir) await fs.remove(tempDir).catch(() => {});
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
async function createTempDirectory(projectDir) {
|
|
4219
|
+
const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
|
|
4220
|
+
try {
|
|
4221
|
+
await fs.ensureDir(projectTmpDir);
|
|
4222
|
+
return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4225
|
+
throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
async function readSourcePackageExtractorOutput(outputFile) {
|
|
4229
|
+
let output;
|
|
4230
|
+
try {
|
|
4231
|
+
output = await fs.readFile(outputFile, "utf-8");
|
|
4232
|
+
} catch (err) {
|
|
4233
|
+
return failure({
|
|
4234
|
+
code: "CONFIG_LOAD_FAILED",
|
|
4235
|
+
message: "Widget package extractor did not write an output file",
|
|
4236
|
+
details: err instanceof Error ? err.message : String(err)
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
let parsed;
|
|
4240
|
+
try {
|
|
4241
|
+
parsed = JSON.parse(output);
|
|
4242
|
+
} catch {
|
|
4243
|
+
return failure({
|
|
4244
|
+
code: "INVALID_FORMAT",
|
|
4245
|
+
message: "Failed to parse widget package output file as JSON",
|
|
4246
|
+
details: `Output was: ${output.slice(0, 200)}`
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
if (!isRecord$3(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
|
|
4250
|
+
code: "INVALID_FORMAT",
|
|
4251
|
+
message: "Widget package extractor output file had an invalid sentinel"
|
|
4252
|
+
});
|
|
4253
|
+
if (!Array.isArray(parsed.data)) return failure({
|
|
4254
|
+
code: "INVALID_FORMAT",
|
|
4255
|
+
message: "Extracted widget package output is not an array",
|
|
4256
|
+
details: `Expected an array, got: ${typeof parsed.data}`
|
|
4257
|
+
});
|
|
4258
|
+
return success(parsed.data);
|
|
4259
|
+
}
|
|
4260
|
+
function createSourcePackageExtractorScript(options) {
|
|
4261
|
+
return `
|
|
4262
|
+
import fs from "node:fs/promises";
|
|
4263
|
+
import path from "node:path";
|
|
4264
|
+
import { createServer, normalizePath } from "vite";
|
|
4265
|
+
|
|
4266
|
+
const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
|
|
4267
|
+
const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
|
|
4268
|
+
const projectRoot = ${JSON.stringify(options.projectDir)};
|
|
4269
|
+
const widgetConfigPath = ${JSON.stringify(options.configPath)};
|
|
4270
|
+
const outputPath = process.argv[2];
|
|
4271
|
+
if (!outputPath) throw new Error("Missing widget package extractor output path.");
|
|
4272
|
+
|
|
4273
|
+
function toViteModuleId(modulePath) {
|
|
4274
|
+
const relativePath = path.relative(projectRoot, modulePath);
|
|
4275
|
+
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
4276
|
+
return "/" + normalizePath(relativePath);
|
|
4277
|
+
}
|
|
4278
|
+
return normalizePath(modulePath);
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const server = await createServer({
|
|
4282
|
+
root: projectRoot,
|
|
4283
|
+
mode: "production",
|
|
4284
|
+
server: { middlewareMode: true },
|
|
4285
|
+
appType: "custom",
|
|
4286
|
+
logLevel: "error",
|
|
4287
|
+
clearScreen: false,
|
|
4288
|
+
resolve: {
|
|
4289
|
+
conditions: ["module", "browser", "development|production"],
|
|
4290
|
+
},
|
|
4291
|
+
ssr: {
|
|
4292
|
+
noExternal: ["@fluid-app/portal-sdk"],
|
|
4293
|
+
resolve: {
|
|
4294
|
+
conditions: ["module", "node", "development|production"],
|
|
4295
|
+
},
|
|
4296
|
+
},
|
|
4297
|
+
});
|
|
4298
|
+
|
|
4299
|
+
try {
|
|
4300
|
+
const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
|
|
4301
|
+
|
|
4302
|
+
function isSourceWidgetPackage(value) {
|
|
4303
|
+
return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
function collectSourceWidgetPackages(mod) {
|
|
4307
|
+
const candidates = [
|
|
4308
|
+
mod.widgetPackage,
|
|
4309
|
+
...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
|
|
4310
|
+
mod.default,
|
|
4311
|
+
];
|
|
4312
|
+
const byPackageId = new Map();
|
|
4313
|
+
for (const candidate of candidates) {
|
|
4314
|
+
if (!isSourceWidgetPackage(candidate)) continue;
|
|
4315
|
+
if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
|
|
4316
|
+
}
|
|
4317
|
+
return Array.from(byPackageId.values());
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
function serializeWidget(widget) {
|
|
4321
|
+
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
|
|
4322
|
+
return widget;
|
|
4323
|
+
}
|
|
4324
|
+
const { component, ...metadata } = widget;
|
|
4325
|
+
return metadata;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
function serializePackage(sourcePackage) {
|
|
4329
|
+
return {
|
|
4330
|
+
manifestVersion: sourcePackage.manifestVersion,
|
|
4331
|
+
scope: sourcePackage.scope,
|
|
4332
|
+
packageStableId: sourcePackage.packageStableId,
|
|
4333
|
+
packageId: sourcePackage.packageId,
|
|
4334
|
+
packageType: sourcePackage.packageType,
|
|
4335
|
+
version: sourcePackage.version,
|
|
4336
|
+
cssUrls: sourcePackage.cssUrls,
|
|
4337
|
+
...(Object.prototype.hasOwnProperty.call(sourcePackage, "navigationOrigins")
|
|
4338
|
+
? { navigationOrigins: sourcePackage.navigationOrigins }
|
|
4339
|
+
: {}),
|
|
4340
|
+
widgets: Array.isArray(sourcePackage.widgets)
|
|
4341
|
+
? sourcePackage.widgets.map(serializeWidget)
|
|
4342
|
+
: sourcePackage.widgets,
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
await fs.writeFile(
|
|
4347
|
+
outputPath,
|
|
4348
|
+
JSON.stringify({
|
|
4349
|
+
sentinel: OUTPUT_SENTINEL,
|
|
4350
|
+
data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
|
|
4351
|
+
}),
|
|
4352
|
+
"utf-8",
|
|
4353
|
+
);
|
|
4354
|
+
} finally {
|
|
4355
|
+
await server.close();
|
|
4356
|
+
}
|
|
4357
|
+
`;
|
|
4358
|
+
}
|
|
4359
|
+
function isRecord$3(value) {
|
|
4360
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4361
|
+
}
|
|
4362
|
+
//#endregion
|
|
4363
|
+
//#region ../widget-tooling/src/internal/widget-package-builder.ts
|
|
4364
|
+
const DEFAULT_PUBLISH_DIR = ".fluid/widget-dist";
|
|
4365
|
+
const DEFAULT_RUNTIME_VERSION = "1";
|
|
4366
|
+
const TEMP_BUILD_DIR = ".fluid/widget-build";
|
|
4367
|
+
const TEMP_CLI_DIR = ".fluid/tmp";
|
|
4368
|
+
const PROJECT_VITE_CONFIG_CANDIDATES = [
|
|
4369
|
+
"vite.config.ts",
|
|
4370
|
+
"vite.config.mts",
|
|
4371
|
+
"vite.config.js",
|
|
4372
|
+
"vite.config.mjs"
|
|
3473
4373
|
];
|
|
3474
4374
|
const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
|
|
3475
4375
|
const require = createRequire(import.meta.url);
|
|
@@ -3547,7 +4447,7 @@ async function buildSharedWidgetPackage(options) {
|
|
|
3547
4447
|
const publishManifest = createPublishManifestMetadata({
|
|
3548
4448
|
packageId: publishPackageId,
|
|
3549
4449
|
version: validation.value.version,
|
|
3550
|
-
cliVersion:
|
|
4450
|
+
cliVersion: options.cliVersion,
|
|
3551
4451
|
runtimeVersion: DEFAULT_RUNTIME_VERSION,
|
|
3552
4452
|
manifestContent,
|
|
3553
4453
|
artifacts
|
|
@@ -3845,24 +4745,6 @@ function toPosixPath(value) {
|
|
|
3845
4745
|
function stableStringify(value) {
|
|
3846
4746
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
3847
4747
|
}
|
|
3848
|
-
function readCliVersion() {
|
|
3849
|
-
return resolveCliVersionFromPackageMetadata(path.dirname(fileURLToPath(import.meta.url)), require);
|
|
3850
|
-
}
|
|
3851
|
-
function resolveCliVersionFromPackageMetadata(startDir, loadPackageMetadata) {
|
|
3852
|
-
for (const packageJsonPath of candidatePackageJsonPaths(startDir)) {
|
|
3853
|
-
let packageJson;
|
|
3854
|
-
try {
|
|
3855
|
-
packageJson = loadPackageMetadata(packageJsonPath);
|
|
3856
|
-
} catch (error) {
|
|
3857
|
-
if (isModuleResolutionError(error)) continue;
|
|
3858
|
-
throw new Error("Failed to read Fluid CLI package metadata.", { cause: error });
|
|
3859
|
-
}
|
|
3860
|
-
if (!isRecord$2(packageJson) || packageJson.name !== "@fluid-app/fluid-cli-portal") continue;
|
|
3861
|
-
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) throw new Error("Fluid CLI package metadata does not declare a valid version.");
|
|
3862
|
-
return packageJson.version;
|
|
3863
|
-
}
|
|
3864
|
-
throw new Error("Unable to find Fluid CLI package metadata.");
|
|
3865
|
-
}
|
|
3866
4748
|
function resolveViteCliPathFromRequire(moduleRequire) {
|
|
3867
4749
|
try {
|
|
3868
4750
|
const packageJsonPath = moduleRequire.resolve("vite/package.json");
|
|
@@ -3889,41 +4771,187 @@ function isNodeError(value) {
|
|
|
3889
4771
|
function isModuleResolutionError(value) {
|
|
3890
4772
|
return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
|
|
3891
4773
|
}
|
|
3892
|
-
function candidatePackageJsonPaths(startDir) {
|
|
3893
|
-
const paths = [];
|
|
3894
|
-
let current = startDir;
|
|
3895
|
-
while (true) {
|
|
3896
|
-
paths.push(path.join(current, "package.json"));
|
|
3897
|
-
const parent = path.dirname(current);
|
|
3898
|
-
if (parent === current) return paths;
|
|
3899
|
-
current = parent;
|
|
3900
|
-
}
|
|
3901
|
-
}
|
|
3902
4774
|
//#endregion
|
|
3903
|
-
//#region src/
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
4775
|
+
//#region ../widget-tooling/src/internal/widget-package-publish-request.ts
|
|
4776
|
+
async function createWidgetPackagePublishRequest(options) {
|
|
4777
|
+
const manifestContent = await fs.readFile(options.manifestPath);
|
|
4778
|
+
const manifest = parseJsonRecord(manifestContent.toString("utf-8"), options.manifestPath);
|
|
4779
|
+
const publishManifest = await readPublishManifest(options.publishManifestPath);
|
|
4780
|
+
assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
|
|
4781
|
+
assertMetadataMatches({
|
|
4782
|
+
label: "manifestHash",
|
|
4783
|
+
path: "manifest.json",
|
|
4784
|
+
expected: publishManifest.manifestHash,
|
|
4785
|
+
actual: sha256(manifestContent)
|
|
4786
|
+
});
|
|
4787
|
+
const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
|
|
4788
|
+
assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
|
|
4789
|
+
const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
|
|
3913
4790
|
return {
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
4791
|
+
version: publishManifest.version,
|
|
4792
|
+
packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
|
|
4793
|
+
builderVersion: publishManifest.builderVersion,
|
|
4794
|
+
cliVersion: publishManifest.cliVersion,
|
|
4795
|
+
runtimeVersion: publishManifest.runtimeVersion,
|
|
4796
|
+
manifestHash: publishManifest.manifestHash,
|
|
4797
|
+
manifest,
|
|
4798
|
+
artifacts
|
|
3918
4799
|
};
|
|
3919
4800
|
}
|
|
3920
|
-
function
|
|
3921
|
-
return
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4801
|
+
async function readJsonRecord(filePath) {
|
|
4802
|
+
return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
|
|
4803
|
+
}
|
|
4804
|
+
function parseJsonRecord(content, filePath) {
|
|
4805
|
+
const value = JSON.parse(content);
|
|
4806
|
+
if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
|
|
4807
|
+
return value;
|
|
4808
|
+
}
|
|
4809
|
+
async function readPublishManifest(filePath) {
|
|
4810
|
+
const record = await readJsonRecord(filePath);
|
|
4811
|
+
const artifactsValue = record["artifacts"];
|
|
4812
|
+
if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
|
|
4813
|
+
return {
|
|
4814
|
+
packageId: readRequiredString(record, "packageId", filePath),
|
|
4815
|
+
version: readRequiredString(record, "version", filePath),
|
|
4816
|
+
builderVersion: readRequiredString(record, "builderVersion", filePath),
|
|
4817
|
+
cliVersion: readRequiredString(record, "cliVersion", filePath),
|
|
4818
|
+
runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
|
|
4819
|
+
manifestHash: readRequiredString(record, "manifestHash", filePath),
|
|
4820
|
+
artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
function readArtifactMetadata(value, index) {
|
|
4824
|
+
if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
|
|
4825
|
+
return {
|
|
4826
|
+
path: readRequiredString(value, "path", "publish-manifest.json"),
|
|
4827
|
+
sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
|
|
4828
|
+
bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
|
|
4829
|
+
contentType: readRequiredString(value, "contentType", "publish-manifest.json")
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
async function readUploadArtifacts(publishDir, artifacts) {
|
|
4833
|
+
const resolvedPublishDir = path.resolve(publishDir);
|
|
4834
|
+
return Promise.all(artifacts.map(async (artifact) => {
|
|
4835
|
+
const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
|
|
4836
|
+
const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
|
|
4837
|
+
assertArtifactMetadataMatches(artifact, actual);
|
|
4838
|
+
return {
|
|
4839
|
+
...actual,
|
|
4840
|
+
body: await fs.readFile(artifactPath)
|
|
4841
|
+
};
|
|
4842
|
+
}));
|
|
4843
|
+
}
|
|
4844
|
+
function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
|
|
4845
|
+
assertMetadataMatches({
|
|
4846
|
+
label: "packageId",
|
|
4847
|
+
path: "manifest.json",
|
|
4848
|
+
expected: publishManifest.packageId,
|
|
4849
|
+
actual: readRequiredString(manifest, "packageId", manifestPath)
|
|
4850
|
+
});
|
|
4851
|
+
assertMetadataMatches({
|
|
4852
|
+
label: "version",
|
|
4853
|
+
path: "manifest.json",
|
|
4854
|
+
expected: publishManifest.version,
|
|
4855
|
+
actual: readRequiredString(manifest, "version", manifestPath)
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
function assertArtifactInventoryMatches(expected, actual) {
|
|
4859
|
+
const expectedByPath = /* @__PURE__ */ new Map();
|
|
4860
|
+
for (const artifact of expected) {
|
|
4861
|
+
if (expectedByPath.has(artifact.path)) throw new Error(`publish-manifest.json artifacts include duplicate path ${JSON.stringify(artifact.path)}. Rebuild the widget package before publishing.`);
|
|
4862
|
+
expectedByPath.set(artifact.path, artifact);
|
|
4863
|
+
}
|
|
4864
|
+
const actualPaths = /* @__PURE__ */ new Set();
|
|
4865
|
+
for (const actualArtifact of actual) {
|
|
4866
|
+
actualPaths.add(actualArtifact.path);
|
|
4867
|
+
const expectedArtifact = expectedByPath.get(actualArtifact.path);
|
|
4868
|
+
if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
|
|
4869
|
+
assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
|
|
4870
|
+
}
|
|
4871
|
+
for (const expectedArtifact of expected) {
|
|
4872
|
+
if (actualPaths.has(expectedArtifact.path)) continue;
|
|
4873
|
+
throw new Error(`publish-manifest.json artifacts include ${JSON.stringify(expectedArtifact.path)}, but that file is not present on disk. Rebuild the widget package before publishing.`);
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
function assertArtifactMetadataMatches(expected, actual) {
|
|
4877
|
+
assertMetadataMatches({
|
|
4878
|
+
label: "path",
|
|
4879
|
+
path: expected.path,
|
|
4880
|
+
expected: expected.path,
|
|
4881
|
+
actual: actual.path
|
|
4882
|
+
});
|
|
4883
|
+
assertMetadataMatches({
|
|
4884
|
+
label: "sha256",
|
|
4885
|
+
path: expected.path,
|
|
4886
|
+
expected: expected.sha256,
|
|
4887
|
+
actual: actual.sha256
|
|
4888
|
+
});
|
|
4889
|
+
assertMetadataMatches({
|
|
4890
|
+
label: "bytes",
|
|
4891
|
+
path: expected.path,
|
|
4892
|
+
expected: expected.bytes,
|
|
4893
|
+
actual: actual.bytes
|
|
4894
|
+
});
|
|
4895
|
+
assertMetadataMatches({
|
|
4896
|
+
label: "contentType",
|
|
4897
|
+
path: expected.path,
|
|
4898
|
+
expected: expected.contentType,
|
|
4899
|
+
actual: actual.contentType
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4902
|
+
function assertMetadataMatches(options) {
|
|
4903
|
+
if (options.expected === options.actual) return;
|
|
4904
|
+
throw new Error(`publish-manifest.json ${options.label} for ${JSON.stringify(options.path)} does not match the file on disk. Recorded ${JSON.stringify(options.expected)}, computed ${JSON.stringify(options.actual)}. Rebuild the widget package before publishing.`);
|
|
4905
|
+
}
|
|
4906
|
+
function resolveArtifactPath(publishDir, artifactPath) {
|
|
4907
|
+
const resolved = path.resolve(publishDir, artifactPath);
|
|
4908
|
+
const relative = path.relative(publishDir, resolved);
|
|
4909
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
|
|
4910
|
+
return resolved;
|
|
4911
|
+
}
|
|
4912
|
+
function stripOwnerPrefix(packageId, owner) {
|
|
4913
|
+
const prefix = `${owner}.`;
|
|
4914
|
+
if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
|
|
4915
|
+
return packageId;
|
|
4916
|
+
}
|
|
4917
|
+
function readRequiredString(record, key, filePath) {
|
|
4918
|
+
const value = record[key];
|
|
4919
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
4920
|
+
throw new Error(`${path.basename(filePath)} must include string ${key}.`);
|
|
4921
|
+
}
|
|
4922
|
+
function readRequiredNumber(record, key, filePath) {
|
|
4923
|
+
const value = record[key];
|
|
4924
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4925
|
+
throw new Error(`${path.basename(filePath)} must include number ${key}.`);
|
|
4926
|
+
}
|
|
4927
|
+
function isRecord$1(value) {
|
|
4928
|
+
return typeof value === "object" && value !== null;
|
|
4929
|
+
}
|
|
4930
|
+
//#endregion
|
|
4931
|
+
//#region ../widget-tooling/src/internal/widget-package-upload.ts
|
|
4932
|
+
var WidgetPackageUploadError = class extends Error {
|
|
4933
|
+
code;
|
|
4934
|
+
constructor(code, message) {
|
|
4935
|
+
super(message);
|
|
4936
|
+
this.name = "WidgetPackageUploadError";
|
|
4937
|
+
this.code = code;
|
|
4938
|
+
}
|
|
4939
|
+
};
|
|
4940
|
+
function buildWidgetPackageArtifactPayload(artifact) {
|
|
4941
|
+
return {
|
|
4942
|
+
path: artifact.path,
|
|
4943
|
+
sha256: artifact.sha256,
|
|
4944
|
+
bytes: artifact.bytes,
|
|
4945
|
+
contentType: artifact.contentType
|
|
4946
|
+
};
|
|
4947
|
+
}
|
|
4948
|
+
function buildWidgetPackageUploadSessionPayload(request) {
|
|
4949
|
+
return {
|
|
4950
|
+
version: request.version,
|
|
4951
|
+
package_key: request.packageKey,
|
|
4952
|
+
builder_version: request.builderVersion,
|
|
4953
|
+
cli_version: request.cliVersion,
|
|
4954
|
+
runtime_version: request.runtimeVersion,
|
|
3927
4955
|
manifest_hash: request.manifestHash,
|
|
3928
4956
|
manifest: request.manifest,
|
|
3929
4957
|
artifacts: request.artifacts.map(buildWidgetPackageArtifactPayload)
|
|
@@ -4072,10 +5100,10 @@ function mergeUploadHeaders(headers, contentType) {
|
|
|
4072
5100
|
return merged;
|
|
4073
5101
|
}
|
|
4074
5102
|
function requireRecord(value, code, message) {
|
|
4075
|
-
if (!isRecord
|
|
5103
|
+
if (!isRecord(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
|
|
4076
5104
|
return value;
|
|
4077
5105
|
}
|
|
4078
|
-
function isRecord
|
|
5106
|
+
function isRecord(value) {
|
|
4079
5107
|
return typeof value === "object" && value !== null;
|
|
4080
5108
|
}
|
|
4081
5109
|
function requireStringProperty(record, key, code) {
|
|
@@ -4095,7 +5123,7 @@ function requireStringArrayProperty(record, key, code) {
|
|
|
4095
5123
|
}
|
|
4096
5124
|
function formatUnknownError(err) {
|
|
4097
5125
|
const base = err instanceof Error ? err.message : String(err);
|
|
4098
|
-
if (isRecord
|
|
5126
|
+
if (isRecord(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
|
|
4099
5127
|
return base;
|
|
4100
5128
|
}
|
|
4101
5129
|
function safeStringify(value) {
|
|
@@ -4113,73 +5141,100 @@ function safeStringify(value) {
|
|
|
4113
5141
|
}
|
|
4114
5142
|
}
|
|
4115
5143
|
//#endregion
|
|
5144
|
+
//#region ../widget-tooling/src/workflows.ts
|
|
5145
|
+
async function buildWidgetPackage(options) {
|
|
5146
|
+
const result = await buildSharedWidgetPackage({
|
|
5147
|
+
projectDir: options.projectDir,
|
|
5148
|
+
publishDir: options.outDir,
|
|
5149
|
+
owner: options.owner,
|
|
5150
|
+
cliVersion: options.cliVersion
|
|
5151
|
+
});
|
|
5152
|
+
if (result.success) return result.value;
|
|
5153
|
+
throw new WidgetToolingError(result.error.code === "CONFIG_LOAD_FAILED" ? "CONFIG_LOAD_FAILED" : result.error.code === "VALIDATION_FAILED" ? "VALIDATION_FAILED" : result.error.code === "INVALID_PUBLISH_DIR" ? "INVALID_OUTPUT_DIR" : "BUILD_FAILED", result.error.message, { details: result.error.details });
|
|
5154
|
+
}
|
|
5155
|
+
async function publishWidgetPackage(options) {
|
|
5156
|
+
if (options.owner !== options.uploadOwner.kind) throw new WidgetToolingError("OWNER_MISMATCH", `Widget package build owner "${options.owner}" must match upload owner kind "${options.uploadOwner.kind}".`);
|
|
5157
|
+
if (!options.dryRun && !options.client) throw new WidgetToolingError("CLIENT_REQUIRED", "A FetchClient is required when dryRun is false.");
|
|
5158
|
+
const build = await buildWidgetPackage(options);
|
|
5159
|
+
let request;
|
|
5160
|
+
try {
|
|
5161
|
+
request = await createWidgetPackagePublishRequest({
|
|
5162
|
+
publishDir: build.publishDir,
|
|
5163
|
+
manifestPath: build.manifestPath,
|
|
5164
|
+
publishManifestPath: build.publishManifestPath,
|
|
5165
|
+
owner: options.owner
|
|
5166
|
+
});
|
|
5167
|
+
} catch (error) {
|
|
5168
|
+
throw new WidgetToolingError("ARTIFACT_VERIFICATION_FAILED", error instanceof Error ? error.message : "Failed to verify widget package artifacts.", { cause: error });
|
|
5169
|
+
}
|
|
5170
|
+
try {
|
|
5171
|
+
const publish = options.dryRun ? await publishWidgetPackageVersion({
|
|
5172
|
+
owner: options.uploadOwner,
|
|
5173
|
+
request,
|
|
5174
|
+
dryRun: true,
|
|
5175
|
+
fetchImpl: options.fetchImpl
|
|
5176
|
+
}) : await publishWidgetPackageVersion({
|
|
5177
|
+
client: options.client,
|
|
5178
|
+
owner: options.uploadOwner,
|
|
5179
|
+
request,
|
|
5180
|
+
fetchImpl: options.fetchImpl
|
|
5181
|
+
});
|
|
5182
|
+
return {
|
|
5183
|
+
build,
|
|
5184
|
+
request,
|
|
5185
|
+
publish
|
|
5186
|
+
};
|
|
5187
|
+
} catch (error) {
|
|
5188
|
+
if (error instanceof WidgetToolingError) throw error;
|
|
5189
|
+
if (error instanceof WidgetPackageUploadError) throw new WidgetToolingError(toPublicUploadErrorCode(error.code), error.message, { cause: error });
|
|
5190
|
+
throw new WidgetToolingError("PUBLISH_FAILED", error instanceof Error ? error.message : "Failed to publish widget package.", { cause: error });
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
function toPublicUploadErrorCode(code) {
|
|
5194
|
+
switch (code) {
|
|
5195
|
+
case "SESSION_REQUEST_FAILED":
|
|
5196
|
+
case "INVALID_SESSION_RESPONSE":
|
|
5197
|
+
case "MISSING_SIGNED_URL":
|
|
5198
|
+
case "CONTENT_TYPE_MISMATCH":
|
|
5199
|
+
case "ARTIFACT_UPLOAD_FAILED":
|
|
5200
|
+
case "COMPLETE_UPLOAD_FAILED":
|
|
5201
|
+
case "INVALID_COMPLETE_RESPONSE": return code;
|
|
5202
|
+
default: return "PUBLISH_FAILED";
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
//#endregion
|
|
4116
5206
|
//#region src/commands/widget-package-publish.ts
|
|
4117
|
-
async function publishWidgetRuntimeArtifacts(options
|
|
4118
|
-
const buildPackage = dependencies.buildSharedWidgetPackage ?? buildSharedWidgetPackage;
|
|
4119
|
-
const publishVersion = dependencies.publishWidgetPackageVersion ?? publishWidgetPackageVersion;
|
|
5207
|
+
async function publishWidgetRuntimeArtifacts(options) {
|
|
4120
5208
|
const isDryRun = options.dryRun === true;
|
|
4121
5209
|
const client = options.client;
|
|
4122
5210
|
if (!isDryRun && !client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
4123
|
-
const
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
const request = await createWidgetPackagePublishRequest({
|
|
4134
|
-
publishDir: buildResult.value.publishDir,
|
|
4135
|
-
manifestPath: buildResult.value.manifestPath,
|
|
4136
|
-
publishManifestPath: buildResult.value.publishManifestPath,
|
|
4137
|
-
owner: options.buildOwner
|
|
4138
|
-
});
|
|
4139
|
-
let publish;
|
|
4140
|
-
if (isDryRun) publish = await publishVersion({
|
|
4141
|
-
owner: options.uploadOwner,
|
|
4142
|
-
request,
|
|
4143
|
-
dryRun: true
|
|
4144
|
-
});
|
|
4145
|
-
else {
|
|
5211
|
+
const cliVersion = await getPortalCliVersion();
|
|
5212
|
+
try {
|
|
5213
|
+
if (isDryRun) return await publishWidgetPackage({
|
|
5214
|
+
projectDir: options.projectDir,
|
|
5215
|
+
outDir: options.outDir,
|
|
5216
|
+
owner: options.buildOwner,
|
|
5217
|
+
uploadOwner: options.uploadOwner,
|
|
5218
|
+
cliVersion,
|
|
5219
|
+
dryRun: true
|
|
5220
|
+
});
|
|
4146
5221
|
if (!client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
5222
|
+
return await publishWidgetPackage({
|
|
5223
|
+
projectDir: options.projectDir,
|
|
5224
|
+
outDir: options.outDir,
|
|
5225
|
+
owner: options.buildOwner,
|
|
5226
|
+
uploadOwner: options.uploadOwner,
|
|
5227
|
+
cliVersion,
|
|
5228
|
+
client
|
|
4151
5229
|
});
|
|
5230
|
+
} catch (error) {
|
|
5231
|
+
throwWithDetails(error);
|
|
4152
5232
|
}
|
|
4153
|
-
return {
|
|
4154
|
-
build: buildResult.value,
|
|
4155
|
-
request,
|
|
4156
|
-
publish
|
|
4157
|
-
};
|
|
4158
5233
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
|
|
4164
|
-
assertMetadataMatches({
|
|
4165
|
-
label: "manifestHash",
|
|
4166
|
-
path: "manifest.json",
|
|
4167
|
-
expected: publishManifest.manifestHash,
|
|
4168
|
-
actual: sha256(manifestContent)
|
|
4169
|
-
});
|
|
4170
|
-
const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
|
|
4171
|
-
assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
|
|
4172
|
-
const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
|
|
4173
|
-
return {
|
|
4174
|
-
version: publishManifest.version,
|
|
4175
|
-
packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
|
|
4176
|
-
builderVersion: publishManifest.builderVersion,
|
|
4177
|
-
cliVersion: publishManifest.cliVersion,
|
|
4178
|
-
runtimeVersion: publishManifest.runtimeVersion,
|
|
4179
|
-
manifestHash: publishManifest.manifestHash,
|
|
4180
|
-
manifest,
|
|
4181
|
-
artifacts
|
|
4182
|
-
};
|
|
5234
|
+
function throwWithDetails(error) {
|
|
5235
|
+
if (!(error instanceof Error)) throw error;
|
|
5236
|
+
if (!("details" in error) || typeof error.details !== "string" || error.details.length === 0) throw error;
|
|
5237
|
+
throw new Error(`${error.message}\n${error.details}`, { cause: error });
|
|
4183
5238
|
}
|
|
4184
5239
|
function createAuthenticatedWidgetPackageClient() {
|
|
4185
5240
|
const { profile, profileName, token } = resolveAuthenticatedProfile();
|
|
@@ -4233,139 +5288,6 @@ function printDryRunSessionPayload(result) {
|
|
|
4233
5288
|
console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
|
|
4234
5289
|
console.log();
|
|
4235
5290
|
}
|
|
4236
|
-
function validateWidgetPublishOutDir(projectDir, outDir) {
|
|
4237
|
-
validateSharedWidgetPackagePublishDir(projectDir, outDir, { optionName: "--out-dir" });
|
|
4238
|
-
return outDir;
|
|
4239
|
-
}
|
|
4240
|
-
async function readJsonRecord(filePath) {
|
|
4241
|
-
return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
|
|
4242
|
-
}
|
|
4243
|
-
function parseJsonRecord(content, filePath) {
|
|
4244
|
-
const value = JSON.parse(content);
|
|
4245
|
-
if (!isRecord(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
|
|
4246
|
-
return value;
|
|
4247
|
-
}
|
|
4248
|
-
async function readPublishManifest(filePath) {
|
|
4249
|
-
const record = await readJsonRecord(filePath);
|
|
4250
|
-
const artifactsValue = record["artifacts"];
|
|
4251
|
-
if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
|
|
4252
|
-
return {
|
|
4253
|
-
packageId: readRequiredString(record, "packageId", filePath),
|
|
4254
|
-
version: readRequiredString(record, "version", filePath),
|
|
4255
|
-
builderVersion: readRequiredString(record, "builderVersion", filePath),
|
|
4256
|
-
cliVersion: readRequiredString(record, "cliVersion", filePath),
|
|
4257
|
-
runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
|
|
4258
|
-
manifestHash: readRequiredString(record, "manifestHash", filePath),
|
|
4259
|
-
artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
|
|
4260
|
-
};
|
|
4261
|
-
}
|
|
4262
|
-
function readArtifactMetadata(value, index) {
|
|
4263
|
-
if (!isRecord(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
|
|
4264
|
-
return {
|
|
4265
|
-
path: readRequiredString(value, "path", "publish-manifest.json"),
|
|
4266
|
-
sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
|
|
4267
|
-
bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
|
|
4268
|
-
contentType: readRequiredString(value, "contentType", "publish-manifest.json")
|
|
4269
|
-
};
|
|
4270
|
-
}
|
|
4271
|
-
async function readUploadArtifacts(publishDir, artifacts) {
|
|
4272
|
-
const resolvedPublishDir = path.resolve(publishDir);
|
|
4273
|
-
return Promise.all(artifacts.map(async (artifact) => {
|
|
4274
|
-
const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
|
|
4275
|
-
const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
|
|
4276
|
-
assertArtifactMetadataMatches(artifact, actual);
|
|
4277
|
-
return {
|
|
4278
|
-
...actual,
|
|
4279
|
-
body: await fs.readFile(artifactPath)
|
|
4280
|
-
};
|
|
4281
|
-
}));
|
|
4282
|
-
}
|
|
4283
|
-
function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
|
|
4284
|
-
assertMetadataMatches({
|
|
4285
|
-
label: "packageId",
|
|
4286
|
-
path: "manifest.json",
|
|
4287
|
-
expected: publishManifest.packageId,
|
|
4288
|
-
actual: readRequiredString(manifest, "packageId", manifestPath)
|
|
4289
|
-
});
|
|
4290
|
-
assertMetadataMatches({
|
|
4291
|
-
label: "version",
|
|
4292
|
-
path: "manifest.json",
|
|
4293
|
-
expected: publishManifest.version,
|
|
4294
|
-
actual: readRequiredString(manifest, "version", manifestPath)
|
|
4295
|
-
});
|
|
4296
|
-
}
|
|
4297
|
-
function assertArtifactInventoryMatches(expected, actual) {
|
|
4298
|
-
const expectedByPath = /* @__PURE__ */ new Map();
|
|
4299
|
-
for (const artifact of expected) {
|
|
4300
|
-
if (expectedByPath.has(artifact.path)) throw new Error(`publish-manifest.json artifacts include duplicate path ${JSON.stringify(artifact.path)}. Rebuild the widget package before publishing.`);
|
|
4301
|
-
expectedByPath.set(artifact.path, artifact);
|
|
4302
|
-
}
|
|
4303
|
-
const actualPaths = /* @__PURE__ */ new Set();
|
|
4304
|
-
for (const actualArtifact of actual) {
|
|
4305
|
-
actualPaths.add(actualArtifact.path);
|
|
4306
|
-
const expectedArtifact = expectedByPath.get(actualArtifact.path);
|
|
4307
|
-
if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
|
|
4308
|
-
assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
|
|
4309
|
-
}
|
|
4310
|
-
for (const expectedArtifact of expected) {
|
|
4311
|
-
if (actualPaths.has(expectedArtifact.path)) continue;
|
|
4312
|
-
throw new Error(`publish-manifest.json artifacts include ${JSON.stringify(expectedArtifact.path)}, but that file is not present on disk. Rebuild the widget package before publishing.`);
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
function assertArtifactMetadataMatches(expected, actual) {
|
|
4316
|
-
assertMetadataMatches({
|
|
4317
|
-
label: "path",
|
|
4318
|
-
path: expected.path,
|
|
4319
|
-
expected: expected.path,
|
|
4320
|
-
actual: actual.path
|
|
4321
|
-
});
|
|
4322
|
-
assertMetadataMatches({
|
|
4323
|
-
label: "sha256",
|
|
4324
|
-
path: expected.path,
|
|
4325
|
-
expected: expected.sha256,
|
|
4326
|
-
actual: actual.sha256
|
|
4327
|
-
});
|
|
4328
|
-
assertMetadataMatches({
|
|
4329
|
-
label: "bytes",
|
|
4330
|
-
path: expected.path,
|
|
4331
|
-
expected: expected.bytes,
|
|
4332
|
-
actual: actual.bytes
|
|
4333
|
-
});
|
|
4334
|
-
assertMetadataMatches({
|
|
4335
|
-
label: "contentType",
|
|
4336
|
-
path: expected.path,
|
|
4337
|
-
expected: expected.contentType,
|
|
4338
|
-
actual: actual.contentType
|
|
4339
|
-
});
|
|
4340
|
-
}
|
|
4341
|
-
function assertMetadataMatches(options) {
|
|
4342
|
-
if (options.expected === options.actual) return;
|
|
4343
|
-
throw new Error(`publish-manifest.json ${options.label} for ${JSON.stringify(options.path)} does not match the file on disk. Recorded ${JSON.stringify(options.expected)}, computed ${JSON.stringify(options.actual)}. Rebuild the widget package before publishing.`);
|
|
4344
|
-
}
|
|
4345
|
-
function resolveArtifactPath(publishDir, artifactPath) {
|
|
4346
|
-
const resolved = path.resolve(publishDir, artifactPath);
|
|
4347
|
-
const relative = path.relative(publishDir, resolved);
|
|
4348
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
|
|
4349
|
-
return resolved;
|
|
4350
|
-
}
|
|
4351
|
-
function stripOwnerPrefix(packageId, owner) {
|
|
4352
|
-
const prefix = `${owner}.`;
|
|
4353
|
-
if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
|
|
4354
|
-
return packageId;
|
|
4355
|
-
}
|
|
4356
|
-
function readRequiredString(record, key, filePath) {
|
|
4357
|
-
const value = record[key];
|
|
4358
|
-
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
4359
|
-
throw new Error(`${path.basename(filePath)} must include string ${key}.`);
|
|
4360
|
-
}
|
|
4361
|
-
function readRequiredNumber(record, key, filePath) {
|
|
4362
|
-
const value = record[key];
|
|
4363
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4364
|
-
throw new Error(`${path.basename(filePath)} must include number ${key}.`);
|
|
4365
|
-
}
|
|
4366
|
-
function isRecord(value) {
|
|
4367
|
-
return typeof value === "object" && value !== null;
|
|
4368
|
-
}
|
|
4369
5291
|
//#endregion
|
|
4370
5292
|
//#region src/commands/deploy.ts
|
|
4371
5293
|
const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
|
|
@@ -4765,6 +5687,6 @@ const plugin = {
|
|
|
4765
5687
|
}
|
|
4766
5688
|
};
|
|
4767
5689
|
//#endregion
|
|
4768
|
-
export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL,
|
|
5690
|
+
export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, cloneCommand, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPortalProject, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, pathExists, portalGitCredentialPath, promptProjectConfig, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
|
|
4769
5691
|
|
|
4770
5692
|
//# sourceMappingURL=index.mjs.map
|