@fluid-app/fluid-cli-portal 0.1.37 → 0.1.39
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.d.mts +225 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +975 -61
- package/dist/index.mjs.map +1 -1
- package/dist/{pull-zrTaJuSb.mjs → pull-CG5dELOX.mjs} +2 -2
- package/dist/{pull-zrTaJuSb.mjs.map → pull-CG5dELOX.mjs.map} +1 -1
- package/package.json +4 -4
- package/templates/base/.gitignore.template +1 -0
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { A as fluid_os_v0_create_fluid_osversion, B as
|
|
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-CG5dELOX.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
6
|
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import { failure, findProjectConfig, getActiveProfile, getAuthToken, listProfileNames, readConfig, success } from "@fluid-app/fluid-cli";
|
|
10
10
|
import prompts from "prompts";
|
|
11
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
12
12
|
import Handlebars from "handlebars";
|
|
13
13
|
import { execa } from "execa";
|
|
14
|
+
import os from "node:os";
|
|
14
15
|
import fs from "fs-extra";
|
|
15
16
|
import path$1 from "path";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
@@ -404,8 +405,583 @@ async function installDependencies(cwd) {
|
|
|
404
405
|
await runPackageManager(["install"], cwd);
|
|
405
406
|
}
|
|
406
407
|
//#endregion
|
|
408
|
+
//#region src/lib/git.ts
|
|
409
|
+
/**
|
|
410
|
+
* Git wrapper for the portal CLI — shells out to the system `git` via
|
|
411
|
+
* execa. We deliberately do NOT bundle git or use a pure-JS impl; users
|
|
412
|
+
* are expected to have it installed (the same stance as `fluid mist`).
|
|
413
|
+
*
|
|
414
|
+
* Security: a token-bearing `remote_url` (minted by the portal
|
|
415
|
+
* `git_credential` endpoint) must never be logged, persisted, *or*
|
|
416
|
+
* exposed in the process table. The token is stripped from the URL and
|
|
417
|
+
* handed to git through a GIT_ASKPASS script via env vars
|
|
418
|
+
* (FLUID_PORTAL_GIT_USER / FLUID_PORTAL_GIT_PASS) — so it never appears
|
|
419
|
+
* in argv (visible to `ps aux` / `/proc/<pid>/cmdline`) and is never
|
|
420
|
+
* written to `.git/config`.
|
|
421
|
+
*
|
|
422
|
+
* This mirrors `packages/cli/mist/src/lib/git.ts`. Kept portal-local for
|
|
423
|
+
* now; if the widget/mist tracks extract a shared git lib we should
|
|
424
|
+
* consume it instead (see overnight/track2-portal.md).
|
|
425
|
+
*/
|
|
426
|
+
var GitError = class extends Error {
|
|
427
|
+
exitCode;
|
|
428
|
+
constructor(message, exitCode = 1) {
|
|
429
|
+
super(message);
|
|
430
|
+
this.name = "GitError";
|
|
431
|
+
this.exitCode = exitCode;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* execa throws ENOENT when the git binary itself is missing — the single
|
|
436
|
+
* most common failure on non-developer machines. Translate it into an
|
|
437
|
+
* actionable message. Returns null for every other failure so call sites
|
|
438
|
+
* keep their specific messages.
|
|
439
|
+
*/
|
|
440
|
+
function gitMissingError(err) {
|
|
441
|
+
if (err?.code !== "ENOENT") return null;
|
|
442
|
+
return new GitError("Git isn't installed (or isn't on your PATH), so this command can't run.\n Install it, then try again:\n macOS: xcode-select --install (or `brew install git`)\n Windows: https://git-scm.com/download/win\n Linux: use your package manager, e.g. `apt install git`", 127);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Is `git` available on this machine? Cheap, never throws — used to
|
|
446
|
+
* decide whether to take the GitHub-template path or fall back to the
|
|
447
|
+
* bundled scaffold.
|
|
448
|
+
*/
|
|
449
|
+
async function isGitAvailable() {
|
|
450
|
+
try {
|
|
451
|
+
await execa("git", ["--version"]);
|
|
452
|
+
return true;
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Confirm git is installed, throwing the friendly message otherwise. */
|
|
458
|
+
async function ensureGitAvailable() {
|
|
459
|
+
if (!await isGitAvailable()) throw new GitError("Git isn't installed (or isn't on your PATH), so this command can't run.\n Install it from https://git-scm.com/downloads (or `brew install git`), then try again.");
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Split a token-bearing remote URL into the userless URL + creds. Keeps
|
|
463
|
+
* the token off git's argv: we pass `cleanUrl` to git and feed
|
|
464
|
+
* `username` / `password` back through GIT_ASKPASS env vars.
|
|
465
|
+
*
|
|
466
|
+
* If the URL has no embedded credentials, returns empty strings — fine
|
|
467
|
+
* for public repos where git won't invoke askpass at all.
|
|
468
|
+
*/
|
|
469
|
+
function splitRemoteCredentials(remoteUrl) {
|
|
470
|
+
const u = new URL(remoteUrl);
|
|
471
|
+
const username = decodeURIComponent(u.username);
|
|
472
|
+
const password = decodeURIComponent(u.password);
|
|
473
|
+
u.username = "";
|
|
474
|
+
u.password = "";
|
|
475
|
+
return {
|
|
476
|
+
cleanUrl: u.toString(),
|
|
477
|
+
username,
|
|
478
|
+
password
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Write a stub GIT_ASKPASS shell script to a per-process tmpdir and
|
|
483
|
+
* return its path. The script contains no secret — it echoes back
|
|
484
|
+
* whichever env var (FLUID_PORTAL_GIT_USER / FLUID_PORTAL_GIT_PASS)
|
|
485
|
+
* matches git's prompt. The credentials only live in the spawned git
|
|
486
|
+
* process's env for the duration of the call.
|
|
487
|
+
*
|
|
488
|
+
* One POSIX script for all platforms: Git for Windows executes
|
|
489
|
+
* GIT_ASKPASS through its bundled sh, so a plain sh script works
|
|
490
|
+
* everywhere. Idempotent: subsequent calls reuse the same path.
|
|
491
|
+
*/
|
|
492
|
+
let askpassPath = null;
|
|
493
|
+
function ensureAskpassScript() {
|
|
494
|
+
if (askpassPath && existsSync(askpassPath)) return askpassPath;
|
|
495
|
+
const dir = path.join(os.tmpdir(), `fluid-portal-askpass-${process.pid}`);
|
|
496
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
497
|
+
recursive: true,
|
|
498
|
+
mode: 448
|
|
499
|
+
});
|
|
500
|
+
const p = path.join(dir, "askpass.sh");
|
|
501
|
+
writeFileSync(p, `#!/bin/sh\ncase "$1" in\n Username*) printf '%s' "$FLUID_PORTAL_GIT_USER" ;;\n *) printf '%s' "$FLUID_PORTAL_GIT_PASS" ;;\nesac\n`, { mode: 448 });
|
|
502
|
+
chmodSync(p, 448);
|
|
503
|
+
askpassPath = p;
|
|
504
|
+
return askpassPath;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Build the spawn env + cleaned URL for a credentialed git invocation.
|
|
508
|
+
* - `cleanUrl`: URL with userinfo stripped — safe to pass as argv.
|
|
509
|
+
* - `env`: parent env plus GIT_ASKPASS + FLUID_PORTAL_GIT_USER/PASS.
|
|
510
|
+
* GIT_TERMINAL_PROMPT=0 prevents the TTY-prompt fallback if the
|
|
511
|
+
* credentials are empty (auth should fail fast, not hang).
|
|
512
|
+
*/
|
|
513
|
+
function gitAuthSpawn(remoteUrl) {
|
|
514
|
+
const { cleanUrl, username, password } = splitRemoteCredentials(remoteUrl);
|
|
515
|
+
return {
|
|
516
|
+
cleanUrl,
|
|
517
|
+
env: {
|
|
518
|
+
...process.env,
|
|
519
|
+
GIT_ASKPASS: ensureAskpassScript(),
|
|
520
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
521
|
+
FLUID_PORTAL_GIT_USER: username,
|
|
522
|
+
FLUID_PORTAL_GIT_PASS: password
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/** Map an execa failure to a GitError, preferring the friendly
|
|
527
|
+
* git-missing message when the binary itself is absent. */
|
|
528
|
+
function toGitError(err, verb) {
|
|
529
|
+
const missing = gitMissingError(err);
|
|
530
|
+
if (missing) return missing;
|
|
531
|
+
const e = err;
|
|
532
|
+
return new GitError(`git ${verb} failed${e.exitCode != null ? ` (exit ${e.exitCode})` : ""}`, e.exitCode ?? 1);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* `git clone <cleanUrl> <targetDir>`. When `remoteUrl` carries a token,
|
|
536
|
+
* gitAuthSpawn strips it from the URL and re-injects it via GIT_ASKPASS
|
|
537
|
+
* so it never appears in argv. `credential.helper=` stops git from
|
|
538
|
+
* caching what askpass returned to any system credential store.
|
|
539
|
+
*
|
|
540
|
+
* `depth` shallow-clones (used by the create flow, which discards `.git`
|
|
541
|
+
* afterwards anyway).
|
|
542
|
+
*/
|
|
543
|
+
async function gitClone(remoteUrl, targetDir, opts = {}) {
|
|
544
|
+
const { cleanUrl, env } = gitAuthSpawn(remoteUrl);
|
|
545
|
+
const depthArgs = opts.depth ? ["--depth", String(opts.depth)] : [];
|
|
546
|
+
try {
|
|
547
|
+
await execa("git", [
|
|
548
|
+
"-c",
|
|
549
|
+
"credential.helper=",
|
|
550
|
+
"clone",
|
|
551
|
+
"--quiet",
|
|
552
|
+
...depthArgs,
|
|
553
|
+
cleanUrl,
|
|
554
|
+
targetDir
|
|
555
|
+
], {
|
|
556
|
+
env,
|
|
557
|
+
stdio: [
|
|
558
|
+
"ignore",
|
|
559
|
+
"inherit",
|
|
560
|
+
"inherit"
|
|
561
|
+
]
|
|
562
|
+
});
|
|
563
|
+
} catch (err) {
|
|
564
|
+
throw toGitError(err, "clone");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** Initialize a fresh repo (used after scaffolding so the project is
|
|
568
|
+
* git-ready). Best-effort: never throws — a scaffold without a repo is
|
|
569
|
+
* still usable, and a missing git binary is handled by the caller. */
|
|
570
|
+
async function gitInit(cwd) {
|
|
571
|
+
try {
|
|
572
|
+
await execa("git", ["init", "--quiet"], { cwd });
|
|
573
|
+
return true;
|
|
574
|
+
} catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** Is `cwd` inside a git work tree? Cheap, never throws. */
|
|
579
|
+
async function isGitRepo(cwd) {
|
|
580
|
+
try {
|
|
581
|
+
const { stdout } = await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
582
|
+
return stdout.trim() === "true";
|
|
583
|
+
} catch {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Read `git remote get-url origin` (or null if no origin / not a repo). */
|
|
588
|
+
async function getOriginUrl(cwd) {
|
|
589
|
+
try {
|
|
590
|
+
const { stdout } = await execa("git", [
|
|
591
|
+
"remote",
|
|
592
|
+
"get-url",
|
|
593
|
+
"origin"
|
|
594
|
+
], { cwd });
|
|
595
|
+
return stdout.trim();
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Are there uncommitted changes (working tree or index)? `git status
|
|
602
|
+
* --porcelain` is empty when clean. Returns false (never throws) when
|
|
603
|
+
* `cwd` isn't a repo.
|
|
604
|
+
*/
|
|
605
|
+
async function hasUncommittedChanges(cwd) {
|
|
606
|
+
try {
|
|
607
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
|
|
608
|
+
return stdout.trim().length > 0;
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Fetch `branch` from a token-bearing remote URL, writing FETCH_HEAD so
|
|
615
|
+
* `gitAheadBehindVsFetchHead` is meaningful. Token comes through
|
|
616
|
+
* GIT_ASKPASS, never argv or `.git/config`.
|
|
617
|
+
*/
|
|
618
|
+
async function gitFetch(cwd, remoteUrl, branch = "main") {
|
|
619
|
+
const { cleanUrl, env } = gitAuthSpawn(remoteUrl);
|
|
620
|
+
try {
|
|
621
|
+
await execa("git", [
|
|
622
|
+
"-c",
|
|
623
|
+
"credential.helper=",
|
|
624
|
+
"fetch",
|
|
625
|
+
"--quiet",
|
|
626
|
+
cleanUrl,
|
|
627
|
+
branch
|
|
628
|
+
], {
|
|
629
|
+
cwd,
|
|
630
|
+
env
|
|
631
|
+
});
|
|
632
|
+
} catch (err) {
|
|
633
|
+
throw toGitError(err, "fetch");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Count commits in local HEAD vs FETCH_HEAD (after `gitFetch`). Returns
|
|
638
|
+
* `{ahead, behind}`. `0/0` means "in sync" (or no FETCH_HEAD yet).
|
|
639
|
+
*/
|
|
640
|
+
async function gitAheadBehindVsFetchHead(cwd) {
|
|
641
|
+
try {
|
|
642
|
+
const { stdout } = await execa("git", [
|
|
643
|
+
"rev-list",
|
|
644
|
+
"--left-right",
|
|
645
|
+
"--count",
|
|
646
|
+
"HEAD...FETCH_HEAD"
|
|
647
|
+
], { cwd });
|
|
648
|
+
const [a, b] = stdout.trim().split(/\s+/);
|
|
649
|
+
const ahead = Number.parseInt(a ?? "0", 10);
|
|
650
|
+
const behind = Number.parseInt(b ?? "0", 10);
|
|
651
|
+
return {
|
|
652
|
+
ahead: Number.isFinite(ahead) ? ahead : 0,
|
|
653
|
+
behind: Number.isFinite(behind) ? behind : 0
|
|
654
|
+
};
|
|
655
|
+
} catch {
|
|
656
|
+
return {
|
|
657
|
+
ahead: 0,
|
|
658
|
+
behind: 0
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Does `cwd` have any commits yet? (false right after `git init`.) */
|
|
663
|
+
async function hasCommits(cwd) {
|
|
664
|
+
try {
|
|
665
|
+
await execa("git", [
|
|
666
|
+
"rev-parse",
|
|
667
|
+
"--verify",
|
|
668
|
+
"HEAD"
|
|
669
|
+
], { cwd });
|
|
670
|
+
return true;
|
|
671
|
+
} catch {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Does `branch` exist on the remote? Throws on transport/auth failure so
|
|
677
|
+
* a network blip is never mistaken for an empty remote (which would skip
|
|
678
|
+
* drift detection). Token via GIT_ASKPASS, never argv or .git/config.
|
|
679
|
+
*/
|
|
680
|
+
async function gitRemoteBranchExists(cwd, remoteUrl, branch = "main") {
|
|
681
|
+
const { cleanUrl, env } = gitAuthSpawn(remoteUrl);
|
|
682
|
+
try {
|
|
683
|
+
const { stdout } = await execa("git", [
|
|
684
|
+
"-c",
|
|
685
|
+
"credential.helper=",
|
|
686
|
+
"ls-remote",
|
|
687
|
+
"--heads",
|
|
688
|
+
cleanUrl,
|
|
689
|
+
`refs/heads/${branch}`
|
|
690
|
+
], {
|
|
691
|
+
cwd,
|
|
692
|
+
env
|
|
693
|
+
});
|
|
694
|
+
return stdout.trim().length > 0;
|
|
695
|
+
} catch (err) {
|
|
696
|
+
throw toGitError(err, "ls-remote");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Adopt `FETCH_HEAD` as the current (unborn) branch's base: point the
|
|
701
|
+
* branch ref at it and load its tree into the index, leaving the working
|
|
702
|
+
* tree untouched. The next auto-commit then records the local project
|
|
703
|
+
* tree as a child of the remote history — local files win, remote-only
|
|
704
|
+
* files are recorded as deletions — so the push fast-forwards instead of
|
|
705
|
+
* being rejected for unrelated history. Only call when `hasCommits` is
|
|
706
|
+
* false; with local commits it would silently rewrite the branch ref.
|
|
707
|
+
* Mirrors the widget CLI's first-push adoption (the backend provisions
|
|
708
|
+
* repos with an auto-init/template commit, so a fresh root commit is
|
|
709
|
+
* always unrelated history).
|
|
710
|
+
*
|
|
711
|
+
* Returns the number of remote commits adopted.
|
|
712
|
+
*/
|
|
713
|
+
async function gitAdoptFetchHead(cwd) {
|
|
714
|
+
try {
|
|
715
|
+
await execa("git", [
|
|
716
|
+
"reset",
|
|
717
|
+
"--mixed",
|
|
718
|
+
"FETCH_HEAD"
|
|
719
|
+
], { cwd });
|
|
720
|
+
const { stdout } = await execa("git", [
|
|
721
|
+
"rev-list",
|
|
722
|
+
"--count",
|
|
723
|
+
"FETCH_HEAD"
|
|
724
|
+
], { cwd });
|
|
725
|
+
const count = Number.parseInt(stdout.trim(), 10);
|
|
726
|
+
return Number.isFinite(count) ? count : 0;
|
|
727
|
+
} catch (err) {
|
|
728
|
+
throw toGitError(err, "adopt remote history");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Normalize a git remote URL to `host/owner/repo` for identity
|
|
733
|
+
* comparison — strips credentials, protocol, `.git`, and trailing
|
|
734
|
+
* slashes so a token-bearing minted URL compares equal to the stored
|
|
735
|
+
* origin. Returns null when the URL doesn't parse.
|
|
736
|
+
*/
|
|
737
|
+
function normalizedRepoIdentity(remoteUrl) {
|
|
738
|
+
const scp = remoteUrl.match(/^(?:[^@/\s]+@)?([^:/\s]+):([^/].*)$/);
|
|
739
|
+
if (scp?.[1] && scp[2] && !remoteUrl.includes("://")) {
|
|
740
|
+
const path = scp[2].replace(/\.git$/, "").replace(/\/+$/, "");
|
|
741
|
+
return `${scp[1]}/${path}`.toLowerCase();
|
|
742
|
+
}
|
|
743
|
+
if (remoteUrl.startsWith("/") || remoteUrl.startsWith(".")) return remoteUrl.replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
744
|
+
try {
|
|
745
|
+
const u = new URL(remoteUrl);
|
|
746
|
+
const path = u.pathname.replace(/\.git$/, "").replace(/\/+$/, "").replace(/^\/+/, "");
|
|
747
|
+
return `${u.host}/${path}`.toLowerCase();
|
|
748
|
+
} catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Do HEAD and FETCH_HEAD share any common ancestor? False for unrelated
|
|
754
|
+
* histories — the guard that keeps a whole-tree auto-commit of the WRONG
|
|
755
|
+
* repository from being pushed to a portal's remote. Never throws.
|
|
756
|
+
*/
|
|
757
|
+
async function hasCommonAncestorWithFetchHead(cwd) {
|
|
758
|
+
try {
|
|
759
|
+
await execa("git", [
|
|
760
|
+
"merge-base",
|
|
761
|
+
"HEAD",
|
|
762
|
+
"FETCH_HEAD"
|
|
763
|
+
], { cwd });
|
|
764
|
+
return true;
|
|
765
|
+
} catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/** Fast-forward HEAD to FETCH_HEAD. Fails (non-zero exit) if not
|
|
770
|
+
* fast-forwardable — callers check the ahead-count first. */
|
|
771
|
+
async function gitMergeFastForward(cwd) {
|
|
772
|
+
try {
|
|
773
|
+
await execa("git", [
|
|
774
|
+
"merge",
|
|
775
|
+
"--ff-only",
|
|
776
|
+
"FETCH_HEAD"
|
|
777
|
+
], { cwd });
|
|
778
|
+
} catch (err) {
|
|
779
|
+
throw toGitError(err, "fast-forward merge");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* `git add -A && git commit -m <message>`.
|
|
784
|
+
*
|
|
785
|
+
* `git add -A` intentionally stages the whole work tree: a portal's
|
|
786
|
+
* provisioned repo tracks the entire project (it's seeded from the
|
|
787
|
+
* starter template), and the "publish my changes" UX expects every edit
|
|
788
|
+
* to ship — the same stance as `fluid mist push`. The exclusion boundary
|
|
789
|
+
* is the project's `.gitignore` (the starter ignores `.env`,
|
|
790
|
+
* `node_modules/`, `dist/`, `.portal-sync/`, logs, `*.tsbuildinfo`), so
|
|
791
|
+
* generated output and local secrets stay out of the commit.
|
|
792
|
+
*
|
|
793
|
+
* Uses the repo's configured identity, else a synthetic Fluid bot
|
|
794
|
+
* identity so the commit isn't rejected. "nothing to commit" is treated
|
|
795
|
+
* as a no-op, not an error.
|
|
796
|
+
*/
|
|
797
|
+
async function gitAutoCommit(cwd, message) {
|
|
798
|
+
try {
|
|
799
|
+
await execa("git", ["add", "-A"], { cwd });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
throw toGitError(err, "add");
|
|
802
|
+
}
|
|
803
|
+
let hasName = false;
|
|
804
|
+
let hasEmail = false;
|
|
805
|
+
try {
|
|
806
|
+
hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
|
|
807
|
+
} catch {}
|
|
808
|
+
try {
|
|
809
|
+
hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
|
|
810
|
+
} catch {}
|
|
811
|
+
const identityArgs = [...hasName ? [] : ["-c", "user.name=Fluid Portal CLI"], ...hasEmail ? [] : ["-c", "user.email=portal@fluid.app"]];
|
|
812
|
+
try {
|
|
813
|
+
await execa("git", [
|
|
814
|
+
...identityArgs,
|
|
815
|
+
"commit",
|
|
816
|
+
"-m",
|
|
817
|
+
message
|
|
818
|
+
], {
|
|
819
|
+
cwd,
|
|
820
|
+
stdio: [
|
|
821
|
+
"ignore",
|
|
822
|
+
"inherit",
|
|
823
|
+
"pipe"
|
|
824
|
+
]
|
|
825
|
+
});
|
|
826
|
+
} catch (err) {
|
|
827
|
+
const missing = gitMissingError(err);
|
|
828
|
+
if (missing) throw missing;
|
|
829
|
+
const e = err;
|
|
830
|
+
const streams = `${typeof e.stdout === "string" ? e.stdout : ""}\n${typeof e.stderr === "string" ? e.stderr : ""}`;
|
|
831
|
+
if (/nothing to commit/i.test(streams)) return;
|
|
832
|
+
throw toGitError(err, "commit");
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* `git push <cleanUrl> HEAD:<branch>` — credentials supplied via
|
|
837
|
+
* GIT_ASKPASS, never argv. `http.…/.extraheader=` clears any cached
|
|
838
|
+
* Authorization header a CI-style config might inject.
|
|
839
|
+
*/
|
|
840
|
+
async function gitPush(cwd, remoteUrl, branch = "main") {
|
|
841
|
+
const { cleanUrl, env } = gitAuthSpawn(remoteUrl);
|
|
842
|
+
try {
|
|
843
|
+
await execa("git", [
|
|
844
|
+
"-c",
|
|
845
|
+
"credential.helper=",
|
|
846
|
+
"-c",
|
|
847
|
+
"http.https://github.com/.extraheader=",
|
|
848
|
+
"push",
|
|
849
|
+
cleanUrl,
|
|
850
|
+
`HEAD:${branch}`
|
|
851
|
+
], {
|
|
852
|
+
cwd,
|
|
853
|
+
env,
|
|
854
|
+
stdio: [
|
|
855
|
+
"ignore",
|
|
856
|
+
"inherit",
|
|
857
|
+
"inherit"
|
|
858
|
+
]
|
|
859
|
+
});
|
|
860
|
+
} catch (err) {
|
|
861
|
+
throw toGitError(err, "push");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/utils/scaffold-github.ts
|
|
866
|
+
/**
|
|
867
|
+
* GitHub-template scaffolding for `fluid portal create`.
|
|
868
|
+
*
|
|
869
|
+
* The canonical starter lives at github.com/Fluid-WeCommerce/portal-template
|
|
870
|
+
* as a **fully rendered, runnable** project (GitHub's generate-from-template
|
|
871
|
+
* copies files verbatim, so it can't contain `{{handlebars}}` placeholders).
|
|
872
|
+
* The project name is baked in as a distinctive token (CANONICAL_PROJECT_NAME);
|
|
873
|
+
* after cloning we re-substitute it with the user's chosen app name.
|
|
874
|
+
*
|
|
875
|
+
* This is the primary create path. When git or the network is unavailable
|
|
876
|
+
* the command falls back to the bundled template (see create.ts), so
|
|
877
|
+
* scaffolding never hard-fails offline.
|
|
878
|
+
*/
|
|
879
|
+
/** Public HTTPS URL of the portal starter template repo. */
|
|
880
|
+
const TEMPLATE_REPO_URL = "https://github.com/Fluid-WeCommerce/portal-template.git";
|
|
881
|
+
/**
|
|
882
|
+
* The project name baked into the rendered template repo. `fluid portal
|
|
883
|
+
* create` replaces every occurrence with the user's app name after
|
|
884
|
+
* cloning. Must match the name the template was rendered with — keep in
|
|
885
|
+
* sync with the render used to publish the repo.
|
|
886
|
+
*/
|
|
887
|
+
const CANONICAL_PROJECT_NAME = "fluid-portal-starter";
|
|
888
|
+
/**
|
|
889
|
+
* Files/dirs we never rewrite when re-substituting the project name.
|
|
890
|
+
* The lockfile keys packages by import path (`.`), not the root name, so
|
|
891
|
+
* touching it would only risk corruption for zero benefit.
|
|
892
|
+
*/
|
|
893
|
+
const SUBSTITUTION_SKIP = new Set([
|
|
894
|
+
".git",
|
|
895
|
+
"node_modules",
|
|
896
|
+
"pnpm-lock.yaml"
|
|
897
|
+
]);
|
|
898
|
+
/** Extensions we treat as text and safe to rewrite. Everything else is
|
|
899
|
+
* left byte-for-byte (avoids mangling any binary asset a future template
|
|
900
|
+
* might carry). */
|
|
901
|
+
const TEXT_EXTENSIONS = new Set([
|
|
902
|
+
".ts",
|
|
903
|
+
".tsx",
|
|
904
|
+
".js",
|
|
905
|
+
".jsx",
|
|
906
|
+
".mjs",
|
|
907
|
+
".cjs",
|
|
908
|
+
".json",
|
|
909
|
+
".md",
|
|
910
|
+
".yml",
|
|
911
|
+
".yaml",
|
|
912
|
+
".css",
|
|
913
|
+
".html",
|
|
914
|
+
".txt"
|
|
915
|
+
]);
|
|
916
|
+
function hasTextExtension(file) {
|
|
917
|
+
const dot = file.lastIndexOf(".");
|
|
918
|
+
if (dot < 0) return false;
|
|
919
|
+
return TEXT_EXTENSIONS.has(file.slice(dot));
|
|
920
|
+
}
|
|
921
|
+
/** Recursively list absolute file paths, skipping SUBSTITUTION_SKIP dirs. */
|
|
922
|
+
async function listFiles(dir) {
|
|
923
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
924
|
+
const files = [];
|
|
925
|
+
for (const entry of entries) {
|
|
926
|
+
if (SUBSTITUTION_SKIP.has(entry.name)) continue;
|
|
927
|
+
const full = join(dir, entry.name);
|
|
928
|
+
if (entry.isDirectory()) files.push(...await listFiles(full));
|
|
929
|
+
else files.push(full);
|
|
930
|
+
}
|
|
931
|
+
return files;
|
|
932
|
+
}
|
|
933
|
+
/** Escape a string for use in a RegExp. */
|
|
934
|
+
function escapeRegExp(value) {
|
|
935
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Replace every occurrence of the canonical project name with `appName`
|
|
939
|
+
* across the cloned tree's text files. Only files that actually contain
|
|
940
|
+
* the token are rewritten.
|
|
941
|
+
*/
|
|
942
|
+
async function rewriteProjectName(targetPath, appName) {
|
|
943
|
+
if (appName === "fluid-portal-starter") return;
|
|
944
|
+
const token = new RegExp(escapeRegExp(CANONICAL_PROJECT_NAME), "g");
|
|
945
|
+
const files = await listFiles(targetPath);
|
|
946
|
+
for (const file of files) {
|
|
947
|
+
if (!hasTextExtension(file)) continue;
|
|
948
|
+
const content = await readFile(file, "utf-8");
|
|
949
|
+
if (!content.includes("fluid-portal-starter")) continue;
|
|
950
|
+
await writeFile(file, content.replace(token, () => appName), "utf-8");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** Rewrite `.fluidrc` with the chosen CLI profile (the template ships an
|
|
954
|
+
* empty profile). No-op when the file is absent. */
|
|
955
|
+
async function writeFluidrcProfile(targetPath, profileName) {
|
|
956
|
+
const fluidrcPath = join(targetPath, ".fluidrc");
|
|
957
|
+
if (!existsSync(fluidrcPath)) return;
|
|
958
|
+
await writeFile(fluidrcPath, JSON.stringify({ profile: profileName }, null, 2) + "\n", "utf-8");
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Clone the GitHub starter template into `targetPath`, discard its git
|
|
962
|
+
* history, re-substitute the project name, wire `.fluidrc`, and copy
|
|
963
|
+
* `.env.example` → `.env`.
|
|
964
|
+
*
|
|
965
|
+
* Throws (GitError or otherwise) on any failure — the caller decides
|
|
966
|
+
* whether to fall back to the bundled template.
|
|
967
|
+
*/
|
|
968
|
+
async function scaffoldFromGitHub(options) {
|
|
969
|
+
const { targetPath, appName, profileName } = options;
|
|
970
|
+
await gitClone(options.repoUrl ?? "https://github.com/Fluid-WeCommerce/portal-template.git", targetPath, { depth: 1 });
|
|
971
|
+
await rm(join(targetPath, ".git"), {
|
|
972
|
+
recursive: true,
|
|
973
|
+
force: true
|
|
974
|
+
});
|
|
975
|
+
await rewriteProjectName(targetPath, appName);
|
|
976
|
+
await writeFluidrcProfile(targetPath, profileName);
|
|
977
|
+
const envExample = join(targetPath, ".env.example");
|
|
978
|
+
try {
|
|
979
|
+
if ((await stat(envExample)).isFile()) await copyFile(envExample, join(targetPath, ".env"));
|
|
980
|
+
} catch {}
|
|
981
|
+
}
|
|
982
|
+
//#endregion
|
|
407
983
|
//#region src/commands/create.ts
|
|
408
|
-
const createCommand = new Command("create").description("Create a new Fluid portal application").argument("<app-name>", "Name of the application to create").option("--skip-install", "Skip dependency installation").option("-o, --output-dir <dir>", "Directory to create the project in (defaults to cwd)").option("--local", "Use local monorepo packages via file: links (for development testing)").action(async (appName, options) => {
|
|
984
|
+
const createCommand = new Command("create").description("Create a new Fluid portal application").argument("<app-name>", "Name of the application to create").option("--skip-install", "Skip dependency installation").option("-o, --output-dir <dir>", "Directory to create the project in (defaults to cwd)").option("--local", "Use local monorepo packages via file: links (for development testing)").option("--no-template", "Skip the GitHub starter template; scaffold from the bundled template").action(async (appName, options) => {
|
|
409
985
|
try {
|
|
410
986
|
console.log();
|
|
411
987
|
console.log(chalk.bold("Creating a new Fluid portal application"));
|
|
@@ -426,65 +1002,39 @@ const createCommand = new Command("create").description("Create a new Fluid port
|
|
|
426
1002
|
process.exit(0);
|
|
427
1003
|
}
|
|
428
1004
|
console.log();
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1005
|
+
const isLocal = !!options.local;
|
|
1006
|
+
const gitReady = !(isLocal || options.template === false || config.selectedPages.length > 0) && await isGitAvailable();
|
|
1007
|
+
let scaffolded = false;
|
|
1008
|
+
if (gitReady) {
|
|
1009
|
+
const spinner = ora("Fetching starter template from GitHub...").start();
|
|
1010
|
+
try {
|
|
1011
|
+
await scaffoldFromGitHub({
|
|
1012
|
+
targetPath,
|
|
1013
|
+
appName: config.name,
|
|
1014
|
+
profileName: config.profileName
|
|
1015
|
+
});
|
|
1016
|
+
spinner.succeed("Fetched starter template");
|
|
1017
|
+
scaffolded = true;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
spinner.warn("Couldn't fetch the GitHub template — using the bundled starter");
|
|
1020
|
+
const reason = error instanceof GitError ? error.message.split("\n")[0] : error instanceof Error ? error.message : String(error);
|
|
1021
|
+
console.log(chalk.dim(` (${reason}; source: ${TEMPLATE_REPO_URL})`));
|
|
1022
|
+
try {
|
|
1023
|
+
await rm(targetPath, {
|
|
1024
|
+
recursive: true,
|
|
1025
|
+
force: true
|
|
1026
|
+
});
|
|
1027
|
+
} catch {}
|
|
448
1028
|
}
|
|
449
|
-
sdkVersion = getLocalPackageLinkVersion(targetPath, sdkPath);
|
|
450
|
-
localCoreVersion = getLocalPackageLinkVersion(targetPath, corePath);
|
|
451
|
-
console.log(chalk.cyan(" Using local packages (--local mode)"));
|
|
452
|
-
} else sdkVersion = await getSdkVersion();
|
|
453
|
-
const spinner = ora("Creating project directory...").start();
|
|
454
|
-
try {
|
|
455
|
-
await createDirectory(targetPath);
|
|
456
|
-
spinner.succeed("Created project directory");
|
|
457
|
-
} catch (error) {
|
|
458
|
-
spinner.fail("Failed to create project directory");
|
|
459
|
-
throw error;
|
|
460
|
-
}
|
|
461
|
-
const templateVariables = {
|
|
462
|
-
projectName: config.name,
|
|
463
|
-
sdkVersion,
|
|
464
|
-
localCoreVersion,
|
|
465
|
-
cliVersion,
|
|
466
|
-
selectedPages: config.selectedPages,
|
|
467
|
-
hasSelectedPages: config.selectedPages.length > 0,
|
|
468
|
-
profileName: config.profileName
|
|
469
|
-
};
|
|
470
|
-
spinner.start("Copying template files...");
|
|
471
|
-
try {
|
|
472
|
-
await copyTemplate(templatePaths.base, targetPath, templateVariables);
|
|
473
|
-
await copyTemplate(templatePaths.overlay, targetPath, templateVariables);
|
|
474
|
-
const envExamplePath = join(targetPath, ".env.example");
|
|
475
|
-
if (await fileExists(envExamplePath)) await copyFile(envExamplePath, join(targetPath, ".env"));
|
|
476
|
-
spinner.succeed("Copied template files");
|
|
477
|
-
} catch (error) {
|
|
478
|
-
spinner.fail("Failed to copy template files");
|
|
479
|
-
throw error;
|
|
480
1029
|
}
|
|
1030
|
+
if (!scaffolded) await scaffoldFromBundled(targetPath, config, isLocal);
|
|
481
1031
|
if (config.installDeps) {
|
|
482
|
-
|
|
1032
|
+
const installSpinner = ora("Installing dependencies with pnpm...").start();
|
|
483
1033
|
try {
|
|
484
1034
|
await installDependencies(targetPath);
|
|
485
|
-
|
|
1035
|
+
installSpinner.succeed("Installed dependencies");
|
|
486
1036
|
} catch {
|
|
487
|
-
|
|
1037
|
+
installSpinner.fail("Failed to install dependencies");
|
|
488
1038
|
console.log();
|
|
489
1039
|
console.log(chalk.yellow("You can try installing dependencies manually:"));
|
|
490
1040
|
console.log(chalk.cyan(` cd ${appName}`));
|
|
@@ -517,6 +1067,59 @@ const createCommand = new Command("create").description("Create a new Fluid port
|
|
|
517
1067
|
process.exit(1);
|
|
518
1068
|
}
|
|
519
1069
|
});
|
|
1070
|
+
/**
|
|
1071
|
+
* Scaffold from the bundled base + overlay templates (Handlebars). Used
|
|
1072
|
+
* when the GitHub template is skipped/unavailable, or with `--local`
|
|
1073
|
+
* (which rewrites SDK/core deps to workspace `file:`/`link:` paths so the
|
|
1074
|
+
* generated project resolves unpublished monorepo source).
|
|
1075
|
+
*
|
|
1076
|
+
* Throws on failure; the caller reports and exits.
|
|
1077
|
+
*/
|
|
1078
|
+
async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
1079
|
+
const templatePaths = getTemplatePaths("starter");
|
|
1080
|
+
if (!await directoryExists(templatePaths.base)) throw new Error("Base template not found");
|
|
1081
|
+
if (!await directoryExists(templatePaths.overlay)) throw new Error("Starter template not found");
|
|
1082
|
+
let sdkVersion;
|
|
1083
|
+
let localCoreVersion;
|
|
1084
|
+
const cliVersion = await getCliVersion();
|
|
1085
|
+
if (isLocal) {
|
|
1086
|
+
const monorepoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
1087
|
+
const sdkPath = join(monorepoRoot, "packages", "portal", "sdk");
|
|
1088
|
+
const corePath = join(monorepoRoot, "packages", "portal", "core");
|
|
1089
|
+
if (!await directoryExists(sdkPath) || !await directoryExists(corePath)) throw new Error("--local requires running from within the fluid-mono monorepo\n Could not find packages/portal/sdk or packages/portal/core");
|
|
1090
|
+
sdkVersion = getLocalPackageLinkVersion(targetPath, sdkPath);
|
|
1091
|
+
localCoreVersion = getLocalPackageLinkVersion(targetPath, corePath);
|
|
1092
|
+
console.log(chalk.cyan(" Using local packages (--local mode)"));
|
|
1093
|
+
} else sdkVersion = await getSdkVersion();
|
|
1094
|
+
const spinner = ora("Creating project directory...").start();
|
|
1095
|
+
try {
|
|
1096
|
+
await createDirectory(targetPath);
|
|
1097
|
+
spinner.succeed("Created project directory");
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
spinner.fail("Failed to create project directory");
|
|
1100
|
+
throw error;
|
|
1101
|
+
}
|
|
1102
|
+
const templateVariables = {
|
|
1103
|
+
projectName: config.name,
|
|
1104
|
+
sdkVersion,
|
|
1105
|
+
localCoreVersion,
|
|
1106
|
+
cliVersion,
|
|
1107
|
+
selectedPages: config.selectedPages,
|
|
1108
|
+
hasSelectedPages: config.selectedPages.length > 0,
|
|
1109
|
+
profileName: config.profileName
|
|
1110
|
+
};
|
|
1111
|
+
spinner.start("Copying template files...");
|
|
1112
|
+
try {
|
|
1113
|
+
await copyTemplate(templatePaths.base, targetPath, templateVariables);
|
|
1114
|
+
await copyTemplate(templatePaths.overlay, targetPath, templateVariables);
|
|
1115
|
+
const envExamplePath = join(targetPath, ".env.example");
|
|
1116
|
+
if (await fileExists(envExamplePath)) await copyFile(envExamplePath, join(targetPath, ".env"));
|
|
1117
|
+
spinner.succeed("Copied template files");
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
spinner.fail("Failed to copy template files");
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
520
1123
|
//#endregion
|
|
521
1124
|
//#region src/commands/dev.ts
|
|
522
1125
|
/**
|
|
@@ -546,7 +1149,7 @@ async function autoPull(cwd) {
|
|
|
546
1149
|
console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
|
|
547
1150
|
console.log();
|
|
548
1151
|
try {
|
|
549
|
-
const { pullCommand } = await import("./pull-
|
|
1152
|
+
const { pullCommand } = await import("./pull-CG5dELOX.mjs").then((n) => n.n);
|
|
550
1153
|
await pullCommand.parseAsync([], { from: "user" });
|
|
551
1154
|
return hasPortalContent(cwd);
|
|
552
1155
|
} catch (err) {
|
|
@@ -1224,6 +1827,181 @@ function validateNavigationItems(items, file, validScreenSlugs, errors) {
|
|
|
1224
1827
|
}
|
|
1225
1828
|
}
|
|
1226
1829
|
//#endregion
|
|
1830
|
+
//#region src/api/git-credential.ts
|
|
1831
|
+
/**
|
|
1832
|
+
* Build the portal git-credential path for a definition id.
|
|
1833
|
+
*
|
|
1834
|
+
* Matches GitSync Phase 006 (fluid-commerce/fluid #19596): a member
|
|
1835
|
+
* route on the company-scoped definitions resource —
|
|
1836
|
+
* `POST /api/company/fluid_os/definitions/:id/git_credential`.
|
|
1837
|
+
*/
|
|
1838
|
+
function portalGitCredentialPath(definitionId) {
|
|
1839
|
+
return `/api/company/fluid_os/definitions/${definitionId}/git_credential`;
|
|
1840
|
+
}
|
|
1841
|
+
function extractErrorReason(body, fallback) {
|
|
1842
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
1843
|
+
const error = body.error;
|
|
1844
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
1845
|
+
const message = error.message;
|
|
1846
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return fallback;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Request a git credential for the portal definition's provisioned repo.
|
|
1853
|
+
*
|
|
1854
|
+
* Never throws — every failure maps to an `unavailable`/`error` outcome
|
|
1855
|
+
* so the invisible-git sync is strictly best-effort.
|
|
1856
|
+
*/
|
|
1857
|
+
async function createPortalGitCredential(options) {
|
|
1858
|
+
const { baseUrl, token, definitionId } = options;
|
|
1859
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1860
|
+
const url = `${baseUrl.replace(/\/$/, "")}${portalGitCredentialPath(definitionId)}`;
|
|
1861
|
+
let resp;
|
|
1862
|
+
try {
|
|
1863
|
+
resp = await fetchImpl(url, {
|
|
1864
|
+
method: "POST",
|
|
1865
|
+
headers: {
|
|
1866
|
+
Authorization: `Bearer ${token}`,
|
|
1867
|
+
Accept: "application/json"
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
return {
|
|
1872
|
+
status: "error",
|
|
1873
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (resp.status === 404) return {
|
|
1877
|
+
status: "unavailable",
|
|
1878
|
+
reason: "git sync isn't available for portals on this Fluid API yet"
|
|
1879
|
+
};
|
|
1880
|
+
const body = await resp.json().catch(() => null);
|
|
1881
|
+
if (!resp.ok) {
|
|
1882
|
+
const reason = extractErrorReason(body, `git credential request failed (HTTP ${resp.status})`);
|
|
1883
|
+
if (resp.status === 409) return {
|
|
1884
|
+
status: "unavailable",
|
|
1885
|
+
reason
|
|
1886
|
+
};
|
|
1887
|
+
return {
|
|
1888
|
+
status: "error",
|
|
1889
|
+
reason
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
const credential = body?.git_credential;
|
|
1893
|
+
if (!credential?.remote_url) return {
|
|
1894
|
+
status: "error",
|
|
1895
|
+
reason: "git credential response missing remote_url"
|
|
1896
|
+
};
|
|
1897
|
+
return {
|
|
1898
|
+
status: "ok",
|
|
1899
|
+
credential
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
//#endregion
|
|
1903
|
+
//#region src/lib/git-sync.ts
|
|
1904
|
+
/**
|
|
1905
|
+
* Invisible-git sync for `fluid portal push`.
|
|
1906
|
+
*
|
|
1907
|
+
* Before the normal content push, we best-effort mirror the local portal
|
|
1908
|
+
* project to its provisioned GitHub repo: fetch → fast-forward-pull if a
|
|
1909
|
+
* teammate pushed → auto-commit working-tree changes → push HEAD:main.
|
|
1910
|
+
* This mirrors `fluid mist push`, but is STRICTLY non-fatal: any problem
|
|
1911
|
+
* (no git, no repo, credential endpoint missing, diverged history,
|
|
1912
|
+
* network blip) degrades to a skip so the content push always proceeds.
|
|
1913
|
+
*/
|
|
1914
|
+
/**
|
|
1915
|
+
* Run the best-effort git sync. Returns a result describing what
|
|
1916
|
+
* happened; never throws. Callers surface `skipped` reasons as a dim
|
|
1917
|
+
* note and continue.
|
|
1918
|
+
*/
|
|
1919
|
+
async function runPortalGitSync(options) {
|
|
1920
|
+
const { cwd, baseUrl, token, definitionId, commitMessage } = options;
|
|
1921
|
+
const branch = options.branch ?? "main";
|
|
1922
|
+
if (!await isGitRepo(cwd) && !await gitInit(cwd)) return {
|
|
1923
|
+
status: "skipped",
|
|
1924
|
+
reason: "not a git repository"
|
|
1925
|
+
};
|
|
1926
|
+
const outcome = await createPortalGitCredential({
|
|
1927
|
+
baseUrl,
|
|
1928
|
+
token,
|
|
1929
|
+
definitionId,
|
|
1930
|
+
fetchImpl: options.fetchImpl
|
|
1931
|
+
});
|
|
1932
|
+
if (outcome.status !== "ok") return {
|
|
1933
|
+
status: "skipped",
|
|
1934
|
+
reason: outcome.reason
|
|
1935
|
+
};
|
|
1936
|
+
const remoteUrl = outcome.credential.remote_url;
|
|
1937
|
+
const origin = await getOriginUrl(cwd);
|
|
1938
|
+
if (origin) {
|
|
1939
|
+
const originId = normalizedRepoIdentity(origin);
|
|
1940
|
+
const mintedId = normalizedRepoIdentity(remoteUrl);
|
|
1941
|
+
if (mintedId && originId !== mintedId) return {
|
|
1942
|
+
status: "skipped",
|
|
1943
|
+
reason: `this directory's git origin (${originId ?? origin}) is not the portal's provisioned repository (${mintedId}) — are you in the right project directory?`
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
try {
|
|
1947
|
+
if (!await hasCommits(cwd)) {
|
|
1948
|
+
if (await gitRemoteBranchExists(cwd, remoteUrl, branch)) {
|
|
1949
|
+
await gitFetch(cwd, remoteUrl, branch);
|
|
1950
|
+
await gitAdoptFetchHead(cwd);
|
|
1951
|
+
}
|
|
1952
|
+
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage);
|
|
1953
|
+
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1954
|
+
}
|
|
1955
|
+
await gitFetch(cwd, remoteUrl, branch);
|
|
1956
|
+
if (!await hasCommonAncestorWithFetchHead(cwd)) return {
|
|
1957
|
+
status: "skipped",
|
|
1958
|
+
reason: "this repository's history is unrelated to the portal's provisioned repository — are you in the right project directory?"
|
|
1959
|
+
};
|
|
1960
|
+
const { ahead, behind } = await gitAheadBehindVsFetchHead(cwd);
|
|
1961
|
+
if (behind > 0 && ahead > 0) return {
|
|
1962
|
+
status: "skipped",
|
|
1963
|
+
reason: "local branch and origin have diverged — run `git pull --rebase` and retry"
|
|
1964
|
+
};
|
|
1965
|
+
if (behind > 0) {
|
|
1966
|
+
if (await hasUncommittedChanges(cwd)) return {
|
|
1967
|
+
status: "skipped",
|
|
1968
|
+
reason: "origin has newer commits and you have uncommitted changes — run `git pull` and retry"
|
|
1969
|
+
};
|
|
1970
|
+
await gitMergeFastForward(cwd);
|
|
1971
|
+
}
|
|
1972
|
+
if (await hasUncommittedChanges(cwd)) await gitAutoCommit(cwd, commitMessage);
|
|
1973
|
+
return await pushCommitted(cwd, remoteUrl, branch);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
return {
|
|
1976
|
+
status: "skipped",
|
|
1977
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* The push gets its own failure boundary: by this point the auto-commit
|
|
1983
|
+
* (and any fast-forward) has already mutated local git state. A push
|
|
1984
|
+
* failure here is FATAL to the content push (matching `fluid widget
|
|
1985
|
+
* push`): publishing artifacts whose source never reached the repo is
|
|
1986
|
+
* exactly the drift the invisible-git model exists to prevent. The
|
|
1987
|
+
* local commit is durable — rerunning push finds the branch ahead and
|
|
1988
|
+
* retries the sync.
|
|
1989
|
+
*/
|
|
1990
|
+
async function pushCommitted(cwd, remoteUrl, branch) {
|
|
1991
|
+
try {
|
|
1992
|
+
await gitPush(cwd, remoteUrl, branch);
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
return {
|
|
1995
|
+
status: "failed",
|
|
1996
|
+
reason: `your changes are committed locally but couldn't be pushed (${err instanceof Error ? err.message : String(err)}) — nothing was published; rerun \`fluid portal push\` to retry`
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
return {
|
|
2000
|
+
status: "synced",
|
|
2001
|
+
pushed: true
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
//#endregion
|
|
1227
2005
|
//#region src/commands/push.ts
|
|
1228
2006
|
/**
|
|
1229
2007
|
* `fluid portal push` command
|
|
@@ -1247,7 +2025,7 @@ function toApiComponentTree(tree) {
|
|
|
1247
2025
|
/**
|
|
1248
2026
|
* Create an authenticated FetchClient using the stored CLI profile.
|
|
1249
2027
|
*/
|
|
1250
|
-
function createClient$
|
|
2028
|
+
function createClient$2() {
|
|
1251
2029
|
const token = getAuthToken();
|
|
1252
2030
|
if (!token) {
|
|
1253
2031
|
const profile = getActiveProfile();
|
|
@@ -1269,6 +2047,32 @@ function enrichedErrorMessage(err) {
|
|
|
1269
2047
|
return msg;
|
|
1270
2048
|
}
|
|
1271
2049
|
/**
|
|
2050
|
+
* Best-effort "invisible git" sync: commit the local project and push it
|
|
2051
|
+
* to the portal's provisioned GitHub repo before the content push. Never
|
|
2052
|
+
* throws — every failure is reported as a dim note and swallowed so the
|
|
2053
|
+
* content push always runs.
|
|
2054
|
+
*/
|
|
2055
|
+
async function runInvisibleGitSync(cwd, definitionId, definitionName) {
|
|
2056
|
+
const token = getAuthToken();
|
|
2057
|
+
if (!token) return;
|
|
2058
|
+
const baseUrl = process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
2059
|
+
const commitMessage = `Portal push: ${definitionName} · ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")}`;
|
|
2060
|
+
const spinner = ora("Syncing project to git…").start();
|
|
2061
|
+
const result = await runPortalGitSync({
|
|
2062
|
+
cwd,
|
|
2063
|
+
baseUrl,
|
|
2064
|
+
token,
|
|
2065
|
+
definitionId,
|
|
2066
|
+
commitMessage
|
|
2067
|
+
});
|
|
2068
|
+
if (result.status === "synced") spinner.succeed("Synced project to git");
|
|
2069
|
+
else if (result.status === "failed") {
|
|
2070
|
+
spinner.fail("Git sync failed");
|
|
2071
|
+
throw new Error(result.reason);
|
|
2072
|
+
} else spinner.info(chalk.gray(`Skipped git sync — ${result.reason}`));
|
|
2073
|
+
console.log();
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
1272
2076
|
* Read and parse a JSON file from the portal directory.
|
|
1273
2077
|
*/
|
|
1274
2078
|
async function readPortalFile(portalDir, relativePath) {
|
|
@@ -1876,6 +2680,7 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
1876
2680
|
const definitionName = snapshot.definition;
|
|
1877
2681
|
console.log(chalk.gray("Definition: ") + chalk.white(definitionName) + chalk.gray(` (ID: ${definitionId})`));
|
|
1878
2682
|
console.log();
|
|
2683
|
+
await runInvisibleGitSync(cwd, definitionId, definitionName);
|
|
1879
2684
|
const spinner = ora();
|
|
1880
2685
|
spinner.start("Detecting changes...");
|
|
1881
2686
|
const diff = await diffAgainstSnapshot(portalDir, snapshot);
|
|
@@ -1917,7 +2722,7 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
1917
2722
|
spinner.start("Authenticating...");
|
|
1918
2723
|
let client;
|
|
1919
2724
|
try {
|
|
1920
|
-
client = createClient$
|
|
2725
|
+
client = createClient$2();
|
|
1921
2726
|
spinner.succeed("Authenticated");
|
|
1922
2727
|
} catch (err) {
|
|
1923
2728
|
spinner.fail("Authentication failed");
|
|
@@ -2005,6 +2810,114 @@ const pushCommand = new Command("push").description("Push local portal content c
|
|
|
2005
2810
|
console.log();
|
|
2006
2811
|
});
|
|
2007
2812
|
//#endregion
|
|
2813
|
+
//#region src/commands/clone.ts
|
|
2814
|
+
/**
|
|
2815
|
+
* `fluid portal clone [app]` — mints a fresh git credential for a Fluid
|
|
2816
|
+
* OS portal definition and clones its provisioned GitHub repo into
|
|
2817
|
+
* `./<slug>/`. Mirrors `fluid mist clone` (credential → clone), but
|
|
2818
|
+
* against the portal git-credential endpoint.
|
|
2819
|
+
*
|
|
2820
|
+
* The credential endpoint is part of GitSync phase 006; until it ships
|
|
2821
|
+
* the request 404s and we print a friendly "not available yet" message
|
|
2822
|
+
* instead of a raw git error.
|
|
2823
|
+
*/
|
|
2824
|
+
function getBaseUrl() {
|
|
2825
|
+
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
2826
|
+
}
|
|
2827
|
+
function createClient$1(token) {
|
|
2828
|
+
return createFetchClient({
|
|
2829
|
+
baseUrl: getBaseUrl(),
|
|
2830
|
+
getAuthToken: () => token
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
/** Select a definition by --app name, else interactively. */
|
|
2834
|
+
async function selectDefinition(client, appName) {
|
|
2835
|
+
const definitions = [];
|
|
2836
|
+
let page = 1;
|
|
2837
|
+
while (true) {
|
|
2838
|
+
const response = await fluid_os_v0_list_fluid_osdefinitions(client, {
|
|
2839
|
+
page,
|
|
2840
|
+
per_page: 500
|
|
2841
|
+
});
|
|
2842
|
+
const batch = response.definitions ?? [];
|
|
2843
|
+
definitions.push(...batch);
|
|
2844
|
+
const totalPages = response.meta?.total_pages ?? 1;
|
|
2845
|
+
if (page >= totalPages || batch.length === 0) break;
|
|
2846
|
+
page++;
|
|
2847
|
+
}
|
|
2848
|
+
if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
|
|
2849
|
+
if (appName) {
|
|
2850
|
+
const match = definitions.find((d) => d.name?.toLowerCase() === appName.toLowerCase());
|
|
2851
|
+
if (!match || match.id == null) {
|
|
2852
|
+
const available = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
2853
|
+
throw new Error(`No definition found matching "${appName}". Available: ${available}`);
|
|
2854
|
+
}
|
|
2855
|
+
return {
|
|
2856
|
+
id: match.id,
|
|
2857
|
+
name: match.name ?? `portal-${match.id}`
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
const { definitionId } = await prompts({
|
|
2861
|
+
type: "select",
|
|
2862
|
+
name: "definitionId",
|
|
2863
|
+
message: "Select a Fluid OS definition to clone",
|
|
2864
|
+
choices: definitions.filter((d) => d.id != null).map((d) => ({
|
|
2865
|
+
title: d.name ?? "(unnamed)",
|
|
2866
|
+
value: d.id
|
|
2867
|
+
}))
|
|
2868
|
+
});
|
|
2869
|
+
if (definitionId == null) throw new Error("No definition selected.");
|
|
2870
|
+
const selected = definitions.find((d) => d.id === definitionId);
|
|
2871
|
+
if (!selected || selected.id == null) throw new Error("Selected definition not found.");
|
|
2872
|
+
return {
|
|
2873
|
+
id: selected.id,
|
|
2874
|
+
name: selected.name ?? `portal-${selected.id}`
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
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) => {
|
|
2878
|
+
try {
|
|
2879
|
+
await ensureGitAvailable();
|
|
2880
|
+
const token = getAuthToken();
|
|
2881
|
+
if (!token) {
|
|
2882
|
+
const profile = getActiveProfile();
|
|
2883
|
+
throw new Error(profile ? `No auth token for profile ${chalk.cyan(profile.name)}. Run ${chalk.cyan("fluid login")}.` : `Not logged in. Run ${chalk.cyan("fluid login")} first.`);
|
|
2884
|
+
}
|
|
2885
|
+
const client = createClient$1(token);
|
|
2886
|
+
const spinner = ora("Resolving definition…").start();
|
|
2887
|
+
const definition = await selectDefinition(client, options.app ?? appArg);
|
|
2888
|
+
spinner.text = "Minting git credential…";
|
|
2889
|
+
const outcome = await createPortalGitCredential({
|
|
2890
|
+
baseUrl: getBaseUrl(),
|
|
2891
|
+
token,
|
|
2892
|
+
definitionId: definition.id
|
|
2893
|
+
});
|
|
2894
|
+
if (outcome.status !== "ok") {
|
|
2895
|
+
spinner.warn(`Can't clone — ${outcome.reason}`);
|
|
2896
|
+
process.exit(1);
|
|
2897
|
+
}
|
|
2898
|
+
const slug = deriveSlug(definition.name);
|
|
2899
|
+
const targetDir = resolve(options.dir ?? slug);
|
|
2900
|
+
if (existsSync(targetDir)) {
|
|
2901
|
+
spinner.fail(`Directory ${targetDir} already exists. Pass --dir or remove it first.`);
|
|
2902
|
+
process.exit(1);
|
|
2903
|
+
}
|
|
2904
|
+
spinner.text = `Cloning into ${slug}/…`;
|
|
2905
|
+
try {
|
|
2906
|
+
await gitClone(outcome.credential.remote_url, targetDir);
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
spinner.fail("git clone failed");
|
|
2909
|
+
throw err;
|
|
2910
|
+
}
|
|
2911
|
+
spinner.succeed(`Cloned into ${chalk.cyan(targetDir)}`);
|
|
2912
|
+
console.log(chalk.dim(` cd ${slug}`));
|
|
2913
|
+
console.log(chalk.dim(` ${chalk.cyan("fluid portal pull")} to sync content`));
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
console.log();
|
|
2916
|
+
console.log(chalk.red("Error:") + " " + (error instanceof Error ? error.message : String(error)));
|
|
2917
|
+
process.exit(1);
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
//#endregion
|
|
2008
2921
|
//#region src/utils/widget-helpers.ts
|
|
2009
2922
|
/**
|
|
2010
2923
|
* Pure helper functions for widget scaffolding.
|
|
@@ -5303,6 +6216,7 @@ const plugin = {
|
|
|
5303
6216
|
portal.addCommand(buildCommand);
|
|
5304
6217
|
portal.addCommand(pullCommand);
|
|
5305
6218
|
portal.addCommand(pushCommand);
|
|
6219
|
+
portal.addCommand(cloneCommand);
|
|
5306
6220
|
portal.addCommand(deployCommand);
|
|
5307
6221
|
portal.addCommand(widgetCommand);
|
|
5308
6222
|
portal.addCommand(doctorCommand);
|
|
@@ -5311,6 +6225,6 @@ const plugin = {
|
|
|
5311
6225
|
}
|
|
5312
6226
|
};
|
|
5313
6227
|
//#endregion
|
|
5314
|
-
export { FILE_SYSTEM_ERRORS, TEMPLATES, WIDGET_PACKAGE_BUILDER_VERSION, WidgetPackageUploadError, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSharedWidgetPackage, buildSnapshot, buildThemeIdToSlugMap, buildWidgetPackageCompleteUploadPayload, buildWidgetPackageDescriptor, buildWidgetPackageUploadSessionPayload, categorizeChanges, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPublishManifestMetadata, createWidgetPackageEntrySource, createWidgetPackagePublishRequest, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, fileExists, getArtifactContentType, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, getWidgetPackageCompleteUploadRoute, getWidgetPackageUploadSessionRoute, installDependencies, loadSourceWidgetPackages, normalizeWidgetPackageCompleteResponse, normalizeWidgetPackageUploadSession, pathExists, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, promptProjectConfig, publishWidgetPackageVersion, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, requestWidgetPackageUploadSession, resolveIdToSlug, resolvePortalWidgetSourceConfig, resolveSlugToId, runPackageManager, sha256, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, uploadWidgetPackageArtifacts, validateCrossReferences, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir, versionCommand, widgetCommand, writeFileSafe, writeMappings, writeSnapshot };
|
|
6228
|
+
export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, WIDGET_PACKAGE_BUILDER_VERSION, WidgetPackageUploadError, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSharedWidgetPackage, buildSnapshot, buildThemeIdToSlugMap, buildWidgetPackageCompleteUploadPayload, buildWidgetPackageDescriptor, buildWidgetPackageUploadSessionPayload, categorizeChanges, cloneCommand, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, 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 };
|
|
5315
6229
|
|
|
5316
6230
|
//# sourceMappingURL=index.mjs.map
|