@fluid-app/fluid-cli-portal 0.1.51 → 0.1.53

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