@fluid-app/fluid-cli-portal 0.1.52 → 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 +1467 -573
- 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 +8 -1
- 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.
|
|
@@ -1191,7 +1625,7 @@ function checkDependenciesInstalled(cwd) {
|
|
|
1191
1625
|
if (existsSync(nodeModulesPath) && hasVite && hasPortalSdk) return { ok: true };
|
|
1192
1626
|
return {
|
|
1193
1627
|
ok: false,
|
|
1194
|
-
message: `Dependencies aren't installed in ${cwd} — run \`pnpm install
|
|
1628
|
+
message: `Dependencies aren't installed in ${cwd} — run \`pnpm install\`, then retry.`
|
|
1195
1629
|
};
|
|
1196
1630
|
}
|
|
1197
1631
|
/**
|
|
@@ -1203,7 +1637,7 @@ async function autoPull(cwd) {
|
|
|
1203
1637
|
console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
|
|
1204
1638
|
console.log();
|
|
1205
1639
|
try {
|
|
1206
|
-
const { pullCommand } = await import("./pull-
|
|
1640
|
+
const { pullCommand } = await import("./pull-C3yj9L07.mjs").then((n) => n.n);
|
|
1207
1641
|
await pullCommand.parseAsync([], { from: "user" });
|
|
1208
1642
|
return hasPortalContent(cwd);
|
|
1209
1643
|
} catch (err) {
|
|
@@ -1256,16 +1690,13 @@ const devCommand = new Command("dev").description("Start the development server
|
|
|
1256
1690
|
const port = Number.isFinite(parsedPort) ? parsedPort : 5173;
|
|
1257
1691
|
try {
|
|
1258
1692
|
const { createServer } = await import("vite");
|
|
1259
|
-
const { portalDevPlugin } = await import("./portal-dev-plugin-
|
|
1693
|
+
const { portalDevPlugin } = await import("./portal-dev-plugin-ByYelgFg.mjs").then((n) => n.n);
|
|
1260
1694
|
const { backendDevPlugin } = await import("./backend-dev-plugin-B9-S4Is-.mjs").then((n) => n.n);
|
|
1261
1695
|
const server = await createServer({
|
|
1262
1696
|
configFile: viteConfigPath,
|
|
1263
1697
|
envPrefix: ["VITE_", "NEXT_PUBLIC_"],
|
|
1264
1698
|
plugins: [portalDevPlugin(), backendDevPlugin()],
|
|
1265
|
-
server:
|
|
1266
|
-
port,
|
|
1267
|
-
host: options.host ? true : void 0
|
|
1268
|
-
}
|
|
1699
|
+
server: portalDevServerOptions(port, options.host === true, cwd)
|
|
1269
1700
|
});
|
|
1270
1701
|
await server.listen();
|
|
1271
1702
|
server.printUrls();
|
|
@@ -1401,6 +1832,8 @@ function categorizeChanges(diff) {
|
|
|
1401
1832
|
* Validate that all cross-references between local portal files are valid.
|
|
1402
1833
|
*
|
|
1403
1834
|
* Checks:
|
|
1835
|
+
* - New and changed resources contain the required local-file fields
|
|
1836
|
+
* - New and changed themes include the required config object
|
|
1404
1837
|
* - Navigation items' "screen" slugs reference existing screen files or mappings
|
|
1405
1838
|
* - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
|
|
1406
1839
|
* - Profile "themes" slugs reference existing theme files or mappings
|
|
@@ -1413,9 +1846,43 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1413
1846
|
for (const file of changes.screens.deleted) validScreenSlugs.delete(slugFromPath(file));
|
|
1414
1847
|
for (const file of changes.navigations.deleted) validNavSlugs.delete(slugFromPath(file));
|
|
1415
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
|
+
}
|
|
1416
1875
|
const navFilesToCheck = [...changes.navigations.new, ...changes.navigations.changed];
|
|
1417
1876
|
for (const file of navFilesToCheck) try {
|
|
1418
|
-
|
|
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);
|
|
1419
1886
|
} catch {
|
|
1420
1887
|
errors.push({
|
|
1421
1888
|
file,
|
|
@@ -1425,6 +1892,14 @@ async function validateCrossReferences(portalDir, mappings, changes) {
|
|
|
1425
1892
|
const profileFilesToCheck = [...changes.profiles.new, ...changes.profiles.changed];
|
|
1426
1893
|
for (const file of profileFilesToCheck) try {
|
|
1427
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
|
+
}
|
|
1428
1903
|
if (profile.navigation && !validNavSlugs.has(profile.navigation)) errors.push({
|
|
1429
1904
|
file,
|
|
1430
1905
|
message: `References navigation "${profile.navigation}" which does not exist`
|
|
@@ -1483,15 +1958,71 @@ function validateNavigationItems(items, file, validScreenSlugs, errors) {
|
|
|
1483
1958
|
function portalGitCredentialPath(definitionId) {
|
|
1484
1959
|
return `/api/company/fluid_os/definitions/${definitionId}/git_credential`;
|
|
1485
1960
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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);
|
|
1493
2024
|
}
|
|
1494
|
-
return
|
|
2025
|
+
return outcome;
|
|
1495
2026
|
}
|
|
1496
2027
|
/**
|
|
1497
2028
|
* Request a git credential for the portal definition's provisioned repo.
|
|
@@ -1502,36 +2033,54 @@ function extractErrorReason(body, fallback) {
|
|
|
1502
2033
|
async function createPortalGitCredential(options) {
|
|
1503
2034
|
const { baseUrl, token, definitionId } = options;
|
|
1504
2035
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2036
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1505
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);
|
|
1506
2044
|
let resp;
|
|
2045
|
+
let body = null;
|
|
1507
2046
|
try {
|
|
1508
2047
|
resp = await fetchImpl(url, {
|
|
1509
2048
|
method: "POST",
|
|
1510
2049
|
headers: {
|
|
1511
2050
|
Authorization: `Bearer ${token}`,
|
|
1512
2051
|
Accept: "application/json"
|
|
1513
|
-
}
|
|
2052
|
+
},
|
|
2053
|
+
signal: abortController.signal
|
|
1514
2054
|
});
|
|
2055
|
+
if (resp.status !== 404) try {
|
|
2056
|
+
body = await resp.json();
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
if (requestTimedOut) throw err;
|
|
2059
|
+
}
|
|
1515
2060
|
} catch (err) {
|
|
1516
2061
|
return {
|
|
1517
2062
|
status: "error",
|
|
1518
|
-
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)
|
|
1519
2064
|
};
|
|
2065
|
+
} finally {
|
|
2066
|
+
clearTimeout(timeout);
|
|
1520
2067
|
}
|
|
1521
2068
|
if (resp.status === 404) return {
|
|
1522
2069
|
status: "unavailable",
|
|
1523
|
-
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
|
|
1524
2072
|
};
|
|
1525
|
-
const body = await resp.json().catch(() => null);
|
|
1526
2073
|
if (!resp.ok) {
|
|
1527
|
-
const
|
|
2074
|
+
const details = extractErrorDetails(body, `git credential request failed (HTTP ${resp.status})`);
|
|
1528
2075
|
if (resp.status === 409) return {
|
|
1529
2076
|
status: "unavailable",
|
|
1530
|
-
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 } : {}
|
|
1531
2080
|
};
|
|
1532
2081
|
return {
|
|
1533
2082
|
status: "error",
|
|
1534
|
-
reason
|
|
2083
|
+
reason: details.reason
|
|
1535
2084
|
};
|
|
1536
2085
|
}
|
|
1537
2086
|
const credential = body?.git_credential;
|
|
@@ -1556,6 +2105,9 @@ async function createPortalGitCredential(options) {
|
|
|
1556
2105
|
* (no git, no repo, credential endpoint missing, diverged history,
|
|
1557
2106
|
* network blip) degrades to a skip so the content push always proceeds.
|
|
1558
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";
|
|
1559
2111
|
/**
|
|
1560
2112
|
* Run the best-effort git sync. Returns a result describing what
|
|
1561
2113
|
* happened; never throws. Callers surface `skipped` reasons as a dim
|
|
@@ -1568,6 +2120,7 @@ async function runPortalGitSync(options) {
|
|
|
1568
2120
|
status: "skipped",
|
|
1569
2121
|
reason: "not a git repository"
|
|
1570
2122
|
};
|
|
2123
|
+
await ensureLocalStateExcluded(cwd);
|
|
1571
2124
|
const outcome = await createPortalGitCredential({
|
|
1572
2125
|
baseUrl,
|
|
1573
2126
|
token,
|
|
@@ -1594,6 +2147,7 @@ async function runPortalGitSync(options) {
|
|
|
1594
2147
|
await gitFetch(cwd, remoteUrl, branch);
|
|
1595
2148
|
await gitAdoptFetchHead(cwd);
|
|
1596
2149
|
}
|
|
2150
|
+
await purgeLocalStateFromIndex(cwd, PURGE_COMMIT_MESSAGE);
|
|
1597
2151
|
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage, { ignoreRemovals: true });
|
|
1598
2152
|
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1599
2153
|
}
|
|
@@ -1614,6 +2168,7 @@ async function runPortalGitSync(options) {
|
|
|
1614
2168
|
};
|
|
1615
2169
|
await gitMergeFastForward(cwd);
|
|
1616
2170
|
}
|
|
2171
|
+
await purgeLocalStateFromIndex(cwd, PURGE_COMMIT_MESSAGE);
|
|
1617
2172
|
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage);
|
|
1618
2173
|
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1619
2174
|
} catch (err) {
|
|
@@ -1655,6 +2210,9 @@ async function pushCommitted(cwd, remoteUrl, branch) {
|
|
|
1655
2210
|
* Detects changes since the last pull/push via snapshot diffing,
|
|
1656
2211
|
* validates cross-references, and pushes resources in dependency order.
|
|
1657
2212
|
*/
|
|
2213
|
+
var NavigationRemoteStateUnknownError = class extends Error {
|
|
2214
|
+
name = "NavigationRemoteStateUnknownError";
|
|
2215
|
+
};
|
|
1658
2216
|
const PORTAL_DIR = "portal";
|
|
1659
2217
|
const PORTAL_SYNC_DIR$1 = ".portal-sync";
|
|
1660
2218
|
/**
|
|
@@ -1682,6 +2240,28 @@ function createClient$2() {
|
|
|
1682
2240
|
getAuthToken: () => token
|
|
1683
2241
|
});
|
|
1684
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
|
+
}
|
|
1685
2265
|
/**
|
|
1686
2266
|
* Extract an enriched error message from a caught value.
|
|
1687
2267
|
* Includes structured API error data when available.
|
|
@@ -1691,6 +2271,18 @@ function enrichedErrorMessage(err) {
|
|
|
1691
2271
|
if (err && typeof err === "object" && "data" in err) msg += ` — ${JSON.stringify(err.data)}`;
|
|
1692
2272
|
return msg;
|
|
1693
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
|
+
}
|
|
1694
2286
|
/**
|
|
1695
2287
|
* Best-effort "invisible git" sync: commit the local project and push it
|
|
1696
2288
|
* to the portal's provisioned GitHub repo before the content push.
|
|
@@ -1985,25 +2577,129 @@ function resolveNavigationItemScreenIds(items, mappings) {
|
|
|
1985
2577
|
* The API reconciliation logic requires a flat list to correctly
|
|
1986
2578
|
* compare against the flat server response.
|
|
1987
2579
|
*/
|
|
1988
|
-
function flattenNavigationItems(items) {
|
|
1989
|
-
const flat = [];
|
|
2580
|
+
function flattenNavigationItems(items, treeParentIndex, flat = []) {
|
|
1990
2581
|
for (const item of items) {
|
|
1991
2582
|
const { children, ...rest } = item;
|
|
1992
|
-
flat.
|
|
1993
|
-
|
|
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);
|
|
1994
2601
|
}
|
|
1995
2602
|
return flat;
|
|
1996
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
|
+
}
|
|
1997
2690
|
/**
|
|
1998
2691
|
* Push navigation changes to the API.
|
|
1999
2692
|
*/
|
|
2000
2693
|
async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
2001
2694
|
const results = [];
|
|
2002
2695
|
let currentMappings = mappings;
|
|
2003
|
-
|
|
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) {
|
|
2004
2699
|
const slug = slugFromPath(file);
|
|
2700
|
+
let remoteChangesComplete = false;
|
|
2005
2701
|
try {
|
|
2006
|
-
|
|
2702
|
+
let local = await readPortalFile(portalDir, file);
|
|
2007
2703
|
const newId = (await fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
|
|
2008
2704
|
name: local.name,
|
|
2009
2705
|
platform: local.platform
|
|
@@ -2011,10 +2707,11 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2011
2707
|
if (newId != null) {
|
|
2012
2708
|
currentMappings = updateMapping(currentMappings, "navigations", slug, newId);
|
|
2013
2709
|
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
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 = {
|
|
2018
2715
|
label: item.label ?? "",
|
|
2019
2716
|
position: item.position ?? 0,
|
|
2020
2717
|
icon: item.icon ?? void 0,
|
|
@@ -2022,9 +2719,13 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2022
2719
|
slug: item.slug ?? void 0,
|
|
2023
2720
|
source: item.source ?? void 0,
|
|
2024
2721
|
parent_id: resolvedParentId
|
|
2025
|
-
}
|
|
2026
|
-
|
|
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);
|
|
2027
2726
|
}
|
|
2727
|
+
remoteChangesComplete = true;
|
|
2728
|
+
await reconcileNavigationFile(client, defId, newId, portalDir, file, local, currentMappings);
|
|
2028
2729
|
}
|
|
2029
2730
|
results.push({
|
|
2030
2731
|
file,
|
|
@@ -2036,7 +2737,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2036
2737
|
file,
|
|
2037
2738
|
action: "created",
|
|
2038
2739
|
success: false,
|
|
2039
|
-
|
|
2740
|
+
...navigationPushFailure(err, remoteChangesComplete)
|
|
2040
2741
|
});
|
|
2041
2742
|
return {
|
|
2042
2743
|
results,
|
|
@@ -2044,7 +2745,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2044
2745
|
};
|
|
2045
2746
|
}
|
|
2046
2747
|
}
|
|
2047
|
-
for (const file of
|
|
2748
|
+
for (const file of changedFiles) {
|
|
2048
2749
|
const slug = slugFromPath(file);
|
|
2049
2750
|
const navId = resolveSlugToId(currentMappings, "navigations", slug);
|
|
2050
2751
|
if (navId == null) {
|
|
@@ -2059,20 +2760,21 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2059
2760
|
mappings: currentMappings
|
|
2060
2761
|
};
|
|
2061
2762
|
}
|
|
2763
|
+
let remoteChangesComplete = false;
|
|
2062
2764
|
try {
|
|
2063
|
-
|
|
2765
|
+
let local = await readPortalFile(portalDir, file);
|
|
2064
2766
|
await fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
|
|
2065
2767
|
name: local.name,
|
|
2066
2768
|
platform: local.platform
|
|
2067
2769
|
} });
|
|
2068
2770
|
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2069
|
-
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 ?? []);
|
|
2070
2772
|
const serverById = new Map(serverItems.map((s) => [s.id, s]));
|
|
2773
|
+
const knownServerIds = new Set(serverById.keys());
|
|
2071
2774
|
const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
|
|
2072
|
-
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
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);
|
|
2076
2778
|
const body = {
|
|
2077
2779
|
label: item.label,
|
|
2078
2780
|
position: item.position,
|
|
@@ -2082,16 +2784,23 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2082
2784
|
source: item.source ?? void 0,
|
|
2083
2785
|
parent_id: resolvedParentId
|
|
2084
2786
|
};
|
|
2085
|
-
if (item.id && serverById.has(item.id))
|
|
2086
|
-
|
|
2087
|
-
|
|
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 = {
|
|
2088
2792
|
...body,
|
|
2089
2793
|
label: body.label ?? "",
|
|
2090
2794
|
position: body.position ?? 0
|
|
2091
|
-
}
|
|
2092
|
-
|
|
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);
|
|
2093
2799
|
}
|
|
2094
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);
|
|
2095
2804
|
results.push({
|
|
2096
2805
|
file,
|
|
2097
2806
|
action: "updated",
|
|
@@ -2102,7 +2811,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
|
2102
2811
|
file,
|
|
2103
2812
|
action: "updated",
|
|
2104
2813
|
success: false,
|
|
2105
|
-
|
|
2814
|
+
...navigationPushFailure(err, remoteChangesComplete)
|
|
2106
2815
|
});
|
|
2107
2816
|
return {
|
|
2108
2817
|
results,
|
|
@@ -2302,7 +3011,7 @@ function printPushReport(results, skippedPhases) {
|
|
|
2302
3011
|
for (const phase of skippedPhases) console.log(chalk.yellow(" " + phase));
|
|
2303
3012
|
}
|
|
2304
3013
|
}
|
|
2305
|
-
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) => {
|
|
2306
3015
|
const cwd = process.cwd();
|
|
2307
3016
|
const portalDir = join(cwd, PORTAL_DIR);
|
|
2308
3017
|
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR$1);
|
|
@@ -2341,6 +3050,10 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2341
3050
|
if (totalChanges === 0) {
|
|
2342
3051
|
spinner.succeed("Nothing to push.");
|
|
2343
3052
|
console.log();
|
|
3053
|
+
if (options.activate) {
|
|
3054
|
+
await activateDraft(definitionId);
|
|
3055
|
+
console.log();
|
|
3056
|
+
}
|
|
2344
3057
|
return;
|
|
2345
3058
|
}
|
|
2346
3059
|
spinner.succeed(`Found ${totalChanges} change(s)`);
|
|
@@ -2362,16 +3075,16 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2362
3075
|
console.log();
|
|
2363
3076
|
}
|
|
2364
3077
|
const changes = categorizeChanges(diff);
|
|
2365
|
-
spinner.start("Validating
|
|
3078
|
+
spinner.start("Validating portal files...");
|
|
2366
3079
|
const validationErrors = await validateCrossReferences(portalDir, mappings, changes);
|
|
2367
3080
|
if (validationErrors.length > 0) {
|
|
2368
|
-
spinner.fail("
|
|
3081
|
+
spinner.fail("Portal file validation failed");
|
|
2369
3082
|
console.log();
|
|
2370
3083
|
for (const err of validationErrors) console.log(chalk.red(" " + err.file + ": ") + err.message);
|
|
2371
3084
|
console.log();
|
|
2372
3085
|
process.exit(1);
|
|
2373
3086
|
}
|
|
2374
|
-
spinner.succeed("
|
|
3087
|
+
spinner.succeed("Portal files valid");
|
|
2375
3088
|
spinner.start("Authenticating...");
|
|
2376
3089
|
let client;
|
|
2377
3090
|
try {
|
|
@@ -2442,10 +3155,10 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2442
3155
|
else spinner.succeed("Phase 3 complete");
|
|
2443
3156
|
}
|
|
2444
3157
|
await writeMappings(portalSyncDir, currentMappings);
|
|
2445
|
-
const
|
|
2446
|
-
if (
|
|
3158
|
+
const filesToSnapshot = new Set(allResults.filter((r) => r.success || r.advanceSnapshot).map((r) => r.file));
|
|
3159
|
+
if (filesToSnapshot.size > 0) {
|
|
2447
3160
|
const updatedHashes = { ...snapshot.files };
|
|
2448
|
-
for (const file of
|
|
3161
|
+
for (const file of filesToSnapshot) {
|
|
2449
3162
|
const fullPath = join(portalDir, file);
|
|
2450
3163
|
if (existsSync(fullPath)) updatedHashes[file] = await computeFileHash(fullPath);
|
|
2451
3164
|
else delete updatedHashes[file];
|
|
@@ -2456,11 +3169,20 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2456
3169
|
});
|
|
2457
3170
|
}
|
|
2458
3171
|
console.log();
|
|
2459
|
-
|
|
3172
|
+
const allSucceeded = allResults.every((r) => r.success);
|
|
3173
|
+
if (allSucceeded && !aborted) console.log(chalk.green.bold("Push complete!"));
|
|
2460
3174
|
else console.log(chalk.yellow.bold("Push completed with issues:"));
|
|
2461
3175
|
console.log();
|
|
2462
3176
|
printPushReport(allResults, skippedPhases);
|
|
2463
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
|
+
}
|
|
2464
3186
|
});
|
|
2465
3187
|
//#endregion
|
|
2466
3188
|
//#region src/commands/clone.ts
|
|
@@ -2470,23 +3192,87 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2470
3192
|
* `./<slug>/`. Mirrors `fluid mist clone` (credential → clone), but
|
|
2471
3193
|
* against the portal git-credential endpoint.
|
|
2472
3194
|
*
|
|
2473
|
-
*
|
|
2474
|
-
* the
|
|
2475
|
-
*
|
|
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.
|
|
2476
3203
|
*/
|
|
2477
3204
|
function getBaseUrl() {
|
|
2478
3205
|
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
2479
3206
|
}
|
|
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
|
+
}
|
|
2480
3218
|
function createClient$1(token) {
|
|
2481
3219
|
return createFetchClient({
|
|
2482
3220
|
baseUrl: getBaseUrl(),
|
|
2483
3221
|
getAuthToken: () => token
|
|
2484
3222
|
});
|
|
2485
3223
|
}
|
|
2486
|
-
/**
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
+
}
|
|
3272
|
+
/** Select a definition by --app name, else interactively. */
|
|
3273
|
+
async function selectDefinition(client, appName, rawDefinitionId) {
|
|
3274
|
+
const definitions = [];
|
|
3275
|
+
let page = 1;
|
|
2490
3276
|
while (true) {
|
|
2491
3277
|
const response = await fluid_os_v0_list_fluid_osdefinitions(client, {
|
|
2492
3278
|
page,
|
|
@@ -2499,17 +3285,8 @@ async function selectDefinition(client, appName) {
|
|
|
2499
3285
|
page++;
|
|
2500
3286
|
}
|
|
2501
3287
|
if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
if (!match || match.id == null) {
|
|
2505
|
-
const available = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
2506
|
-
throw new Error(`No definition found matching "${appName}". Available: ${available}`);
|
|
2507
|
-
}
|
|
2508
|
-
return {
|
|
2509
|
-
id: match.id,
|
|
2510
|
-
name: match.name ?? `portal-${match.id}`
|
|
2511
|
-
};
|
|
2512
|
-
}
|
|
3288
|
+
const resolved = resolveDefinition(definitions, appName, rawDefinitionId);
|
|
3289
|
+
if (resolved) return resolved;
|
|
2513
3290
|
const { definitionId } = await prompts({
|
|
2514
3291
|
type: "select",
|
|
2515
3292
|
name: "definitionId",
|
|
@@ -2527,9 +3304,8 @@ async function selectDefinition(client, appName) {
|
|
|
2527
3304
|
name: selected.name ?? `portal-${selected.id}`
|
|
2528
3305
|
};
|
|
2529
3306
|
}
|
|
2530
|
-
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) => {
|
|
2531
3308
|
try {
|
|
2532
|
-
await ensureGitAvailable();
|
|
2533
3309
|
const token = getAuthToken();
|
|
2534
3310
|
if (!token) {
|
|
2535
3311
|
const profile = getActiveProfile();
|
|
@@ -2537,32 +3313,99 @@ const cloneCommand = new Command("clone").description("Clone a portal definition
|
|
|
2537
3313
|
}
|
|
2538
3314
|
const client = createClient$1(token);
|
|
2539
3315
|
const spinner = ora("Resolving definition…").start();
|
|
2540
|
-
const definition = await selectDefinition(client, options.app ?? appArg);
|
|
3316
|
+
const definition = await selectDefinition(client, options.app ?? appArg, options.id);
|
|
2541
3317
|
spinner.text = "Minting git credential…";
|
|
2542
|
-
const outcome = await
|
|
3318
|
+
const outcome = await createPortalGitCredentialWithRetry({
|
|
2543
3319
|
baseUrl: getBaseUrl(),
|
|
2544
3320
|
token,
|
|
2545
|
-
definitionId: definition.id
|
|
3321
|
+
definitionId: definition.id,
|
|
3322
|
+
onRetry: ({ attempt, totalAttempts }) => {
|
|
3323
|
+
spinner.text = `Preparing Git repository… retrying (${attempt}/${totalAttempts})`;
|
|
3324
|
+
}
|
|
2546
3325
|
});
|
|
2547
|
-
if (outcome.status
|
|
3326
|
+
if (outcome.status === "error") {
|
|
2548
3327
|
spinner.warn(`Can't clone — ${outcome.reason}`);
|
|
2549
3328
|
process.exit(1);
|
|
2550
3329
|
}
|
|
2551
3330
|
const slug = deriveSlug(definition.name);
|
|
2552
3331
|
const targetDir = resolve(options.dir ?? slug);
|
|
2553
|
-
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) {
|
|
2554
3371
|
spinner.fail(`Directory ${targetDir} already exists. Pass --dir or remove it first.`);
|
|
2555
3372
|
process.exit(1);
|
|
2556
3373
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
+
});
|
|
2560
3403
|
} catch (err) {
|
|
2561
|
-
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.`);
|
|
2562
3405
|
throw err;
|
|
2563
3406
|
}
|
|
2564
3407
|
spinner.succeed(`Cloned into ${chalk.cyan(targetDir)}`);
|
|
2565
|
-
console.log(chalk.dim(` cd ${
|
|
3408
|
+
console.log(chalk.dim(` cd ${targetDir}`));
|
|
2566
3409
|
console.log(chalk.dim(` ${chalk.cyan("fluid portal pull")} to sync content`));
|
|
2567
3410
|
} catch (error) {
|
|
2568
3411
|
console.log();
|
|
@@ -2770,206 +3613,19 @@ export const widgetPackage = defineWidgetPackage({
|
|
|
2770
3613
|
}
|
|
2771
3614
|
const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
|
|
2772
3615
|
//#endregion
|
|
2773
|
-
//#region src/
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
} : void 0;
|
|
2785
|
-
}
|
|
2786
|
-
async function loadSourceWidgetPackages(projectDir) {
|
|
2787
|
-
const config = await resolvePortalWidgetSourceConfig(projectDir);
|
|
2788
|
-
if (!config) return success([]);
|
|
2789
|
-
let tempDir;
|
|
2790
|
-
try {
|
|
2791
|
-
tempDir = await createTempDirectory(projectDir);
|
|
2792
|
-
const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
|
|
2793
|
-
const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
|
|
2794
|
-
await fs.writeFile(extractFile, createSourcePackageExtractorScript({
|
|
2795
|
-
projectDir,
|
|
2796
|
-
configPath: config.path
|
|
2797
|
-
}), {
|
|
2798
|
-
encoding: "utf-8",
|
|
2799
|
-
flag: "wx"
|
|
2800
|
-
});
|
|
2801
|
-
await execa(process.execPath, [
|
|
2802
|
-
TSX_CLI_PATH,
|
|
2803
|
-
extractFile,
|
|
2804
|
-
outputFile
|
|
2805
|
-
], {
|
|
2806
|
-
cwd: projectDir,
|
|
2807
|
-
stdio: "pipe",
|
|
2808
|
-
env: {
|
|
2809
|
-
...process.env,
|
|
2810
|
-
NODE_ENV: "production"
|
|
2811
|
-
}
|
|
2812
|
-
});
|
|
2813
|
-
const outputResult = await readSourcePackageExtractorOutput(outputFile);
|
|
2814
|
-
if (!outputResult.success) return outputResult;
|
|
2815
|
-
const parsed = outputResult.value;
|
|
2816
|
-
if (parsed.length === 0) return success([]);
|
|
2817
|
-
return success(parsed);
|
|
2818
|
-
} catch (err) {
|
|
2819
|
-
const error = err;
|
|
2820
|
-
return failure({
|
|
2821
|
-
code: "CONFIG_LOAD_FAILED",
|
|
2822
|
-
message: `Failed to load widget packages from ${config.relativePath}`,
|
|
2823
|
-
details: error.stderr ?? error.message ?? String(err)
|
|
2824
|
-
});
|
|
2825
|
-
} finally {
|
|
2826
|
-
if (tempDir) await fs.remove(tempDir).catch(() => {});
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
async function createTempDirectory(projectDir) {
|
|
2830
|
-
const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
|
|
2831
|
-
try {
|
|
2832
|
-
await fs.ensureDir(projectTmpDir);
|
|
2833
|
-
return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
|
|
2834
|
-
} catch (err) {
|
|
2835
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2836
|
-
throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
async function readSourcePackageExtractorOutput(outputFile) {
|
|
2840
|
-
let output;
|
|
2841
|
-
try {
|
|
2842
|
-
output = await fs.readFile(outputFile, "utf-8");
|
|
2843
|
-
} catch (err) {
|
|
2844
|
-
return failure({
|
|
2845
|
-
code: "CONFIG_LOAD_FAILED",
|
|
2846
|
-
message: "Widget package extractor did not write an output file",
|
|
2847
|
-
details: err instanceof Error ? err.message : String(err)
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
let parsed;
|
|
2851
|
-
try {
|
|
2852
|
-
parsed = JSON.parse(output);
|
|
2853
|
-
} catch {
|
|
2854
|
-
return failure({
|
|
2855
|
-
code: "INVALID_FORMAT",
|
|
2856
|
-
message: "Failed to parse widget package output file as JSON",
|
|
2857
|
-
details: `Output was: ${output.slice(0, 200)}`
|
|
2858
|
-
});
|
|
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;
|
|
2859
3627
|
}
|
|
2860
|
-
|
|
2861
|
-
code: "INVALID_FORMAT",
|
|
2862
|
-
message: "Widget package extractor output file had an invalid sentinel"
|
|
2863
|
-
});
|
|
2864
|
-
if (!Array.isArray(parsed.data)) return failure({
|
|
2865
|
-
code: "INVALID_FORMAT",
|
|
2866
|
-
message: "Extracted widget package output is not an array",
|
|
2867
|
-
details: `Expected an array, got: ${typeof parsed.data}`
|
|
2868
|
-
});
|
|
2869
|
-
return success(parsed.data);
|
|
2870
|
-
}
|
|
2871
|
-
function createSourcePackageExtractorScript(options) {
|
|
2872
|
-
return `
|
|
2873
|
-
import fs from "node:fs/promises";
|
|
2874
|
-
import path from "node:path";
|
|
2875
|
-
import { createServer, normalizePath } from "vite";
|
|
2876
|
-
|
|
2877
|
-
const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
|
|
2878
|
-
const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
|
|
2879
|
-
const projectRoot = ${JSON.stringify(options.projectDir)};
|
|
2880
|
-
const widgetConfigPath = ${JSON.stringify(options.configPath)};
|
|
2881
|
-
const outputPath = process.argv[2];
|
|
2882
|
-
if (!outputPath) throw new Error("Missing widget package extractor output path.");
|
|
2883
|
-
|
|
2884
|
-
function toViteModuleId(modulePath) {
|
|
2885
|
-
const relativePath = path.relative(projectRoot, modulePath);
|
|
2886
|
-
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
2887
|
-
return "/" + normalizePath(relativePath);
|
|
2888
|
-
}
|
|
2889
|
-
return normalizePath(modulePath);
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
const server = await createServer({
|
|
2893
|
-
root: projectRoot,
|
|
2894
|
-
mode: "production",
|
|
2895
|
-
server: { middlewareMode: true },
|
|
2896
|
-
appType: "custom",
|
|
2897
|
-
logLevel: "error",
|
|
2898
|
-
clearScreen: false,
|
|
2899
|
-
resolve: {
|
|
2900
|
-
conditions: ["module", "browser", "development|production"],
|
|
2901
|
-
},
|
|
2902
|
-
ssr: {
|
|
2903
|
-
noExternal: ["@fluid-app/portal-sdk"],
|
|
2904
|
-
resolve: {
|
|
2905
|
-
conditions: ["module", "node", "development|production"],
|
|
2906
|
-
},
|
|
2907
|
-
},
|
|
2908
|
-
});
|
|
2909
|
-
|
|
2910
|
-
try {
|
|
2911
|
-
const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
|
|
2912
|
-
|
|
2913
|
-
function isSourceWidgetPackage(value) {
|
|
2914
|
-
return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
function collectSourceWidgetPackages(mod) {
|
|
2918
|
-
const candidates = [
|
|
2919
|
-
mod.widgetPackage,
|
|
2920
|
-
...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
|
|
2921
|
-
mod.default,
|
|
2922
|
-
];
|
|
2923
|
-
const byPackageId = new Map();
|
|
2924
|
-
for (const candidate of candidates) {
|
|
2925
|
-
if (!isSourceWidgetPackage(candidate)) continue;
|
|
2926
|
-
if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
|
|
2927
|
-
}
|
|
2928
|
-
return Array.from(byPackageId.values());
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
function serializeWidget(widget) {
|
|
2932
|
-
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
|
|
2933
|
-
return widget;
|
|
2934
|
-
}
|
|
2935
|
-
const { component, ...metadata } = widget;
|
|
2936
|
-
return metadata;
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
function serializePackage(sourcePackage) {
|
|
2940
|
-
return {
|
|
2941
|
-
manifestVersion: sourcePackage.manifestVersion,
|
|
2942
|
-
scope: sourcePackage.scope,
|
|
2943
|
-
packageStableId: sourcePackage.packageStableId,
|
|
2944
|
-
packageId: sourcePackage.packageId,
|
|
2945
|
-
packageType: sourcePackage.packageType,
|
|
2946
|
-
version: sourcePackage.version,
|
|
2947
|
-
cssUrls: sourcePackage.cssUrls,
|
|
2948
|
-
widgets: Array.isArray(sourcePackage.widgets)
|
|
2949
|
-
? sourcePackage.widgets.map(serializeWidget)
|
|
2950
|
-
: sourcePackage.widgets,
|
|
2951
|
-
};
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
await fs.writeFile(
|
|
2955
|
-
outputPath,
|
|
2956
|
-
JSON.stringify({
|
|
2957
|
-
sentinel: OUTPUT_SENTINEL,
|
|
2958
|
-
data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
|
|
2959
|
-
}),
|
|
2960
|
-
"utf-8",
|
|
2961
|
-
);
|
|
2962
|
-
} finally {
|
|
2963
|
-
await server.close();
|
|
2964
|
-
}
|
|
2965
|
-
`;
|
|
2966
|
-
}
|
|
2967
|
-
function isRecord$5(value) {
|
|
2968
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2969
|
-
}
|
|
2970
|
-
//#endregion
|
|
2971
|
-
//#region src/utils/widget-package-artifacts.ts
|
|
2972
|
-
const WIDGET_PACKAGE_BUILDER_VERSION = "1";
|
|
3628
|
+
};
|
|
2973
3629
|
const CONTENT_TYPES = {
|
|
2974
3630
|
".css": "text/css",
|
|
2975
3631
|
".js": "application/javascript",
|
|
@@ -3000,7 +3656,7 @@ function createPublishManifestMetadata(options) {
|
|
|
3000
3656
|
return {
|
|
3001
3657
|
packageId: options.packageId,
|
|
3002
3658
|
version: options.version,
|
|
3003
|
-
builderVersion: options.builderVersion ?? "1",
|
|
3659
|
+
builderVersion: options.builderVersion ?? "0.1.0",
|
|
3004
3660
|
cliVersion: options.cliVersion,
|
|
3005
3661
|
runtimeVersion: options.runtimeVersion,
|
|
3006
3662
|
manifestHash: sha256(options.manifestContent),
|
|
@@ -3087,15 +3743,15 @@ function isJsonValueAtDepth(value, ancestors, depth) {
|
|
|
3087
3743
|
if (typeof value !== "object" || depth > 50 || ancestors.has(value)) return false;
|
|
3088
3744
|
ancestors.add(value);
|
|
3089
3745
|
const prototype = Object.getPrototypeOf(value);
|
|
3090
|
-
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));
|
|
3091
3747
|
ancestors.delete(value);
|
|
3092
3748
|
return isJson;
|
|
3093
3749
|
}
|
|
3094
|
-
function isRecord$
|
|
3750
|
+
function isRecord$5(value) {
|
|
3095
3751
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3096
3752
|
}
|
|
3097
3753
|
//#endregion
|
|
3098
|
-
//#region src/
|
|
3754
|
+
//#region ../widget-tooling/src/internal/widget-package-validation.ts
|
|
3099
3755
|
const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
|
|
3100
3756
|
const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
|
|
3101
3757
|
const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
|
|
@@ -3134,7 +3790,17 @@ function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
|
|
|
3134
3790
|
path: "packageType",
|
|
3135
3791
|
message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
|
|
3136
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
|
+
});
|
|
3137
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
|
+
});
|
|
3138
3804
|
const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
|
|
3139
3805
|
const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
|
|
3140
3806
|
if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
|
|
@@ -3255,7 +3921,7 @@ function validationFailure(error) {
|
|
|
3255
3921
|
};
|
|
3256
3922
|
}
|
|
3257
3923
|
function validateSourcePackageShape(value) {
|
|
3258
|
-
if (!isRecord$
|
|
3924
|
+
if (!isRecord$4(value)) return [{
|
|
3259
3925
|
code: "INVALID_SOURCE_PACKAGE",
|
|
3260
3926
|
message: "Widget source package must be a JSON object."
|
|
3261
3927
|
}];
|
|
@@ -3265,6 +3931,11 @@ function validateSourcePackageShape(value) {
|
|
|
3265
3931
|
validateOptionalStringField(value, "scope", "scope", errors);
|
|
3266
3932
|
validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
|
|
3267
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
|
+
});
|
|
3268
3939
|
if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
|
|
3269
3940
|
code: "INVALID_SOURCE_PACKAGE",
|
|
3270
3941
|
path: "cssUrls",
|
|
@@ -3283,7 +3954,7 @@ function validateSourcePackageShape(value) {
|
|
|
3283
3954
|
}
|
|
3284
3955
|
value.widgets.forEach((widget, index) => {
|
|
3285
3956
|
const widgetPath = `widgets[${index}]`;
|
|
3286
|
-
if (!isRecord$
|
|
3957
|
+
if (!isRecord$4(widget)) {
|
|
3287
3958
|
errors.push({
|
|
3288
3959
|
code: "INVALID_WIDGET_METADATA",
|
|
3289
3960
|
path: widgetPath,
|
|
@@ -3351,7 +4022,7 @@ function validateCssUrl(value, cssUrlPath, errors) {
|
|
|
3351
4022
|
function validateOptionalJsonRecordField(record, key, path, errors) {
|
|
3352
4023
|
const value = record[key];
|
|
3353
4024
|
if (value === void 0) return;
|
|
3354
|
-
if (isRecord$
|
|
4025
|
+
if (isRecord$4(value) && isJsonValue(value)) return;
|
|
3355
4026
|
errors.push({
|
|
3356
4027
|
code: "INVALID_WIDGET_METADATA",
|
|
3357
4028
|
path,
|
|
@@ -3372,7 +4043,7 @@ function validateOptionalCapabilities(record, key, path, errors) {
|
|
|
3372
4043
|
const names = /* @__PURE__ */ new Set();
|
|
3373
4044
|
value.forEach((candidate, index) => {
|
|
3374
4045
|
const capabilityPath = `${path}[${index}]`;
|
|
3375
|
-
if (!isRecord$
|
|
4046
|
+
if (!isRecord$4(candidate)) {
|
|
3376
4047
|
errors.push({
|
|
3377
4048
|
code: "INVALID_WIDGET_METADATA",
|
|
3378
4049
|
path: capabilityPath,
|
|
@@ -3441,7 +4112,7 @@ function normalizeContainer(value) {
|
|
|
3441
4112
|
function normalizeResizable(value) {
|
|
3442
4113
|
if (typeof value === "boolean") return value;
|
|
3443
4114
|
if (value === "horizontal" || value === "vertical" || value === "both") return value;
|
|
3444
|
-
if (!isRecord$
|
|
4115
|
+
if (!isRecord$4(value)) return false;
|
|
3445
4116
|
const horizontal = value.horizontal === true;
|
|
3446
4117
|
const vertical = value.vertical === true;
|
|
3447
4118
|
if (horizontal && vertical) return "both";
|
|
@@ -3484,24 +4155,225 @@ function isUrlSafeWidgetName(value) {
|
|
|
3484
4155
|
function isUrlSafeSemver(value) {
|
|
3485
4156
|
return SEMVER_URL_SAFE_PATTERN.test(value);
|
|
3486
4157
|
}
|
|
3487
|
-
function isRecord$
|
|
4158
|
+
function isRecord$4(value) {
|
|
3488
4159
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3489
4160
|
}
|
|
3490
4161
|
//#endregion
|
|
3491
|
-
//#region src/
|
|
3492
|
-
const
|
|
3493
|
-
const
|
|
3494
|
-
const
|
|
3495
|
-
const
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
function
|
|
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"
|
|
4373
|
+
];
|
|
4374
|
+
const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
|
|
4375
|
+
const require = createRequire(import.meta.url);
|
|
4376
|
+
function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
|
|
3505
4377
|
const label = options.optionName ?? "publishDir";
|
|
3506
4378
|
if (path.isAbsolute(publishDir)) throw new Error(`${label} must be relative to the project directory.`);
|
|
3507
4379
|
const normalizedPublishDir = normalizePublishDirForValidation(publishDir);
|
|
@@ -3575,7 +4447,7 @@ async function buildSharedWidgetPackage(options) {
|
|
|
3575
4447
|
const publishManifest = createPublishManifestMetadata({
|
|
3576
4448
|
packageId: publishPackageId,
|
|
3577
4449
|
version: validation.value.version,
|
|
3578
|
-
cliVersion:
|
|
4450
|
+
cliVersion: options.cliVersion,
|
|
3579
4451
|
runtimeVersion: DEFAULT_RUNTIME_VERSION,
|
|
3580
4452
|
manifestContent,
|
|
3581
4453
|
artifacts
|
|
@@ -3873,24 +4745,6 @@ function toPosixPath(value) {
|
|
|
3873
4745
|
function stableStringify(value) {
|
|
3874
4746
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
3875
4747
|
}
|
|
3876
|
-
function readCliVersion() {
|
|
3877
|
-
return resolveCliVersionFromPackageMetadata(path.dirname(fileURLToPath(import.meta.url)), require);
|
|
3878
|
-
}
|
|
3879
|
-
function resolveCliVersionFromPackageMetadata(startDir, loadPackageMetadata) {
|
|
3880
|
-
for (const packageJsonPath of candidatePackageJsonPaths(startDir)) {
|
|
3881
|
-
let packageJson;
|
|
3882
|
-
try {
|
|
3883
|
-
packageJson = loadPackageMetadata(packageJsonPath);
|
|
3884
|
-
} catch (error) {
|
|
3885
|
-
if (isModuleResolutionError(error)) continue;
|
|
3886
|
-
throw new Error("Failed to read Fluid CLI package metadata.", { cause: error });
|
|
3887
|
-
}
|
|
3888
|
-
if (!isRecord$2(packageJson) || packageJson.name !== "@fluid-app/fluid-cli-portal") continue;
|
|
3889
|
-
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) throw new Error("Fluid CLI package metadata does not declare a valid version.");
|
|
3890
|
-
return packageJson.version;
|
|
3891
|
-
}
|
|
3892
|
-
throw new Error("Unable to find Fluid CLI package metadata.");
|
|
3893
|
-
}
|
|
3894
4748
|
function resolveViteCliPathFromRequire(moduleRequire) {
|
|
3895
4749
|
try {
|
|
3896
4750
|
const packageJsonPath = moduleRequire.resolve("vite/package.json");
|
|
@@ -3917,37 +4771,183 @@ function isNodeError(value) {
|
|
|
3917
4771
|
function isModuleResolutionError(value) {
|
|
3918
4772
|
return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
|
|
3919
4773
|
}
|
|
3920
|
-
function candidatePackageJsonPaths(startDir) {
|
|
3921
|
-
const paths = [];
|
|
3922
|
-
let current = startDir;
|
|
3923
|
-
while (true) {
|
|
3924
|
-
paths.push(path.join(current, "package.json"));
|
|
3925
|
-
const parent = path.dirname(current);
|
|
3926
|
-
if (parent === current) return paths;
|
|
3927
|
-
current = parent;
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3930
4774
|
//#endregion
|
|
3931
|
-
//#region src/
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
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);
|
|
3941
4790
|
return {
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
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
|
|
3946
4799
|
};
|
|
3947
4800
|
}
|
|
3948
|
-
function
|
|
3949
|
-
return
|
|
3950
|
-
|
|
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,
|
|
3951
4951
|
package_key: request.packageKey,
|
|
3952
4952
|
builder_version: request.builderVersion,
|
|
3953
4953
|
cli_version: request.cliVersion,
|
|
@@ -4100,10 +5100,10 @@ function mergeUploadHeaders(headers, contentType) {
|
|
|
4100
5100
|
return merged;
|
|
4101
5101
|
}
|
|
4102
5102
|
function requireRecord(value, code, message) {
|
|
4103
|
-
if (!isRecord
|
|
5103
|
+
if (!isRecord(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
|
|
4104
5104
|
return value;
|
|
4105
5105
|
}
|
|
4106
|
-
function isRecord
|
|
5106
|
+
function isRecord(value) {
|
|
4107
5107
|
return typeof value === "object" && value !== null;
|
|
4108
5108
|
}
|
|
4109
5109
|
function requireStringProperty(record, key, code) {
|
|
@@ -4123,7 +5123,7 @@ function requireStringArrayProperty(record, key, code) {
|
|
|
4123
5123
|
}
|
|
4124
5124
|
function formatUnknownError(err) {
|
|
4125
5125
|
const base = err instanceof Error ? err.message : String(err);
|
|
4126
|
-
if (isRecord
|
|
5126
|
+
if (isRecord(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
|
|
4127
5127
|
return base;
|
|
4128
5128
|
}
|
|
4129
5129
|
function safeStringify(value) {
|
|
@@ -4141,73 +5141,100 @@ function safeStringify(value) {
|
|
|
4141
5141
|
}
|
|
4142
5142
|
}
|
|
4143
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
|
|
4144
5206
|
//#region src/commands/widget-package-publish.ts
|
|
4145
|
-
async function publishWidgetRuntimeArtifacts(options
|
|
4146
|
-
const buildPackage = dependencies.buildSharedWidgetPackage ?? buildSharedWidgetPackage;
|
|
4147
|
-
const publishVersion = dependencies.publishWidgetPackageVersion ?? publishWidgetPackageVersion;
|
|
5207
|
+
async function publishWidgetRuntimeArtifacts(options) {
|
|
4148
5208
|
const isDryRun = options.dryRun === true;
|
|
4149
5209
|
const client = options.client;
|
|
4150
5210
|
if (!isDryRun && !client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
4151
|
-
const
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
const request = await createWidgetPackagePublishRequest({
|
|
4162
|
-
publishDir: buildResult.value.publishDir,
|
|
4163
|
-
manifestPath: buildResult.value.manifestPath,
|
|
4164
|
-
publishManifestPath: buildResult.value.publishManifestPath,
|
|
4165
|
-
owner: options.buildOwner
|
|
4166
|
-
});
|
|
4167
|
-
let publish;
|
|
4168
|
-
if (isDryRun) publish = await publishVersion({
|
|
4169
|
-
owner: options.uploadOwner,
|
|
4170
|
-
request,
|
|
4171
|
-
dryRun: true
|
|
4172
|
-
});
|
|
4173
|
-
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
|
+
});
|
|
4174
5221
|
if (!client) throw new Error("A FetchClient is required when dryRun is false.");
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
5222
|
+
return await publishWidgetPackage({
|
|
5223
|
+
projectDir: options.projectDir,
|
|
5224
|
+
outDir: options.outDir,
|
|
5225
|
+
owner: options.buildOwner,
|
|
5226
|
+
uploadOwner: options.uploadOwner,
|
|
5227
|
+
cliVersion,
|
|
5228
|
+
client
|
|
4179
5229
|
});
|
|
5230
|
+
} catch (error) {
|
|
5231
|
+
throwWithDetails(error);
|
|
4180
5232
|
}
|
|
4181
|
-
return {
|
|
4182
|
-
build: buildResult.value,
|
|
4183
|
-
request,
|
|
4184
|
-
publish
|
|
4185
|
-
};
|
|
4186
5233
|
}
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
|
|
4192
|
-
assertMetadataMatches({
|
|
4193
|
-
label: "manifestHash",
|
|
4194
|
-
path: "manifest.json",
|
|
4195
|
-
expected: publishManifest.manifestHash,
|
|
4196
|
-
actual: sha256(manifestContent)
|
|
4197
|
-
});
|
|
4198
|
-
const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
|
|
4199
|
-
assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
|
|
4200
|
-
const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
|
|
4201
|
-
return {
|
|
4202
|
-
version: publishManifest.version,
|
|
4203
|
-
packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
|
|
4204
|
-
builderVersion: publishManifest.builderVersion,
|
|
4205
|
-
cliVersion: publishManifest.cliVersion,
|
|
4206
|
-
runtimeVersion: publishManifest.runtimeVersion,
|
|
4207
|
-
manifestHash: publishManifest.manifestHash,
|
|
4208
|
-
manifest,
|
|
4209
|
-
artifacts
|
|
4210
|
-
};
|
|
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 });
|
|
4211
5238
|
}
|
|
4212
5239
|
function createAuthenticatedWidgetPackageClient() {
|
|
4213
5240
|
const { profile, profileName, token } = resolveAuthenticatedProfile();
|
|
@@ -4261,139 +5288,6 @@ function printDryRunSessionPayload(result) {
|
|
|
4261
5288
|
console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
|
|
4262
5289
|
console.log();
|
|
4263
5290
|
}
|
|
4264
|
-
function validateWidgetPublishOutDir(projectDir, outDir) {
|
|
4265
|
-
validateSharedWidgetPackagePublishDir(projectDir, outDir, { optionName: "--out-dir" });
|
|
4266
|
-
return outDir;
|
|
4267
|
-
}
|
|
4268
|
-
async function readJsonRecord(filePath) {
|
|
4269
|
-
return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
|
|
4270
|
-
}
|
|
4271
|
-
function parseJsonRecord(content, filePath) {
|
|
4272
|
-
const value = JSON.parse(content);
|
|
4273
|
-
if (!isRecord(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
|
|
4274
|
-
return value;
|
|
4275
|
-
}
|
|
4276
|
-
async function readPublishManifest(filePath) {
|
|
4277
|
-
const record = await readJsonRecord(filePath);
|
|
4278
|
-
const artifactsValue = record["artifacts"];
|
|
4279
|
-
if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
|
|
4280
|
-
return {
|
|
4281
|
-
packageId: readRequiredString(record, "packageId", filePath),
|
|
4282
|
-
version: readRequiredString(record, "version", filePath),
|
|
4283
|
-
builderVersion: readRequiredString(record, "builderVersion", filePath),
|
|
4284
|
-
cliVersion: readRequiredString(record, "cliVersion", filePath),
|
|
4285
|
-
runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
|
|
4286
|
-
manifestHash: readRequiredString(record, "manifestHash", filePath),
|
|
4287
|
-
artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
|
|
4288
|
-
};
|
|
4289
|
-
}
|
|
4290
|
-
function readArtifactMetadata(value, index) {
|
|
4291
|
-
if (!isRecord(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
|
|
4292
|
-
return {
|
|
4293
|
-
path: readRequiredString(value, "path", "publish-manifest.json"),
|
|
4294
|
-
sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
|
|
4295
|
-
bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
|
|
4296
|
-
contentType: readRequiredString(value, "contentType", "publish-manifest.json")
|
|
4297
|
-
};
|
|
4298
|
-
}
|
|
4299
|
-
async function readUploadArtifacts(publishDir, artifacts) {
|
|
4300
|
-
const resolvedPublishDir = path.resolve(publishDir);
|
|
4301
|
-
return Promise.all(artifacts.map(async (artifact) => {
|
|
4302
|
-
const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
|
|
4303
|
-
const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
|
|
4304
|
-
assertArtifactMetadataMatches(artifact, actual);
|
|
4305
|
-
return {
|
|
4306
|
-
...actual,
|
|
4307
|
-
body: await fs.readFile(artifactPath)
|
|
4308
|
-
};
|
|
4309
|
-
}));
|
|
4310
|
-
}
|
|
4311
|
-
function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
|
|
4312
|
-
assertMetadataMatches({
|
|
4313
|
-
label: "packageId",
|
|
4314
|
-
path: "manifest.json",
|
|
4315
|
-
expected: publishManifest.packageId,
|
|
4316
|
-
actual: readRequiredString(manifest, "packageId", manifestPath)
|
|
4317
|
-
});
|
|
4318
|
-
assertMetadataMatches({
|
|
4319
|
-
label: "version",
|
|
4320
|
-
path: "manifest.json",
|
|
4321
|
-
expected: publishManifest.version,
|
|
4322
|
-
actual: readRequiredString(manifest, "version", manifestPath)
|
|
4323
|
-
});
|
|
4324
|
-
}
|
|
4325
|
-
function assertArtifactInventoryMatches(expected, actual) {
|
|
4326
|
-
const expectedByPath = /* @__PURE__ */ new Map();
|
|
4327
|
-
for (const artifact of expected) {
|
|
4328
|
-
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.`);
|
|
4329
|
-
expectedByPath.set(artifact.path, artifact);
|
|
4330
|
-
}
|
|
4331
|
-
const actualPaths = /* @__PURE__ */ new Set();
|
|
4332
|
-
for (const actualArtifact of actual) {
|
|
4333
|
-
actualPaths.add(actualArtifact.path);
|
|
4334
|
-
const expectedArtifact = expectedByPath.get(actualArtifact.path);
|
|
4335
|
-
if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
|
|
4336
|
-
assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
|
|
4337
|
-
}
|
|
4338
|
-
for (const expectedArtifact of expected) {
|
|
4339
|
-
if (actualPaths.has(expectedArtifact.path)) continue;
|
|
4340
|
-
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.`);
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
function assertArtifactMetadataMatches(expected, actual) {
|
|
4344
|
-
assertMetadataMatches({
|
|
4345
|
-
label: "path",
|
|
4346
|
-
path: expected.path,
|
|
4347
|
-
expected: expected.path,
|
|
4348
|
-
actual: actual.path
|
|
4349
|
-
});
|
|
4350
|
-
assertMetadataMatches({
|
|
4351
|
-
label: "sha256",
|
|
4352
|
-
path: expected.path,
|
|
4353
|
-
expected: expected.sha256,
|
|
4354
|
-
actual: actual.sha256
|
|
4355
|
-
});
|
|
4356
|
-
assertMetadataMatches({
|
|
4357
|
-
label: "bytes",
|
|
4358
|
-
path: expected.path,
|
|
4359
|
-
expected: expected.bytes,
|
|
4360
|
-
actual: actual.bytes
|
|
4361
|
-
});
|
|
4362
|
-
assertMetadataMatches({
|
|
4363
|
-
label: "contentType",
|
|
4364
|
-
path: expected.path,
|
|
4365
|
-
expected: expected.contentType,
|
|
4366
|
-
actual: actual.contentType
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
|
-
function assertMetadataMatches(options) {
|
|
4370
|
-
if (options.expected === options.actual) return;
|
|
4371
|
-
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.`);
|
|
4372
|
-
}
|
|
4373
|
-
function resolveArtifactPath(publishDir, artifactPath) {
|
|
4374
|
-
const resolved = path.resolve(publishDir, artifactPath);
|
|
4375
|
-
const relative = path.relative(publishDir, resolved);
|
|
4376
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
|
|
4377
|
-
return resolved;
|
|
4378
|
-
}
|
|
4379
|
-
function stripOwnerPrefix(packageId, owner) {
|
|
4380
|
-
const prefix = `${owner}.`;
|
|
4381
|
-
if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
|
|
4382
|
-
return packageId;
|
|
4383
|
-
}
|
|
4384
|
-
function readRequiredString(record, key, filePath) {
|
|
4385
|
-
const value = record[key];
|
|
4386
|
-
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
4387
|
-
throw new Error(`${path.basename(filePath)} must include string ${key}.`);
|
|
4388
|
-
}
|
|
4389
|
-
function readRequiredNumber(record, key, filePath) {
|
|
4390
|
-
const value = record[key];
|
|
4391
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4392
|
-
throw new Error(`${path.basename(filePath)} must include number ${key}.`);
|
|
4393
|
-
}
|
|
4394
|
-
function isRecord(value) {
|
|
4395
|
-
return typeof value === "object" && value !== null;
|
|
4396
|
-
}
|
|
4397
5291
|
//#endregion
|
|
4398
5292
|
//#region src/commands/deploy.ts
|
|
4399
5293
|
const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
|
|
@@ -4793,6 +5687,6 @@ const plugin = {
|
|
|
4793
5687
|
}
|
|
4794
5688
|
};
|
|
4795
5689
|
//#endregion
|
|
4796
|
-
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 };
|
|
4797
5691
|
|
|
4798
5692
|
//# sourceMappingURL=index.mjs.map
|