@fluid-app/fluid-cli-portal 0.1.37 → 0.1.38
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 +81 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +374 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/templates/base/.gitignore.template +1 -0
package/dist/index.mjs
CHANGED
|
@@ -5,12 +5,13 @@ 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,297 @@ 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
|
+
//#endregion
|
|
579
|
+
//#region src/utils/scaffold-github.ts
|
|
580
|
+
/**
|
|
581
|
+
* GitHub-template scaffolding for `fluid portal create`.
|
|
582
|
+
*
|
|
583
|
+
* The canonical starter lives at github.com/Fluid-WeCommerce/portal-template
|
|
584
|
+
* as a **fully rendered, runnable** project (GitHub's generate-from-template
|
|
585
|
+
* copies files verbatim, so it can't contain `{{handlebars}}` placeholders).
|
|
586
|
+
* The project name is baked in as a distinctive token (CANONICAL_PROJECT_NAME);
|
|
587
|
+
* after cloning we re-substitute it with the user's chosen app name.
|
|
588
|
+
*
|
|
589
|
+
* This is the primary create path. When git or the network is unavailable
|
|
590
|
+
* the command falls back to the bundled template (see create.ts), so
|
|
591
|
+
* scaffolding never hard-fails offline.
|
|
592
|
+
*/
|
|
593
|
+
/** Public HTTPS URL of the portal starter template repo. */
|
|
594
|
+
const TEMPLATE_REPO_URL = "https://github.com/Fluid-WeCommerce/portal-template.git";
|
|
595
|
+
/**
|
|
596
|
+
* The project name baked into the rendered template repo. `fluid portal
|
|
597
|
+
* create` replaces every occurrence with the user's app name after
|
|
598
|
+
* cloning. Must match the name the template was rendered with — keep in
|
|
599
|
+
* sync with the render used to publish the repo.
|
|
600
|
+
*/
|
|
601
|
+
const CANONICAL_PROJECT_NAME = "fluid-portal-starter";
|
|
602
|
+
/**
|
|
603
|
+
* Files/dirs we never rewrite when re-substituting the project name.
|
|
604
|
+
* The lockfile keys packages by import path (`.`), not the root name, so
|
|
605
|
+
* touching it would only risk corruption for zero benefit.
|
|
606
|
+
*/
|
|
607
|
+
const SUBSTITUTION_SKIP = new Set([
|
|
608
|
+
".git",
|
|
609
|
+
"node_modules",
|
|
610
|
+
"pnpm-lock.yaml"
|
|
611
|
+
]);
|
|
612
|
+
/** Extensions we treat as text and safe to rewrite. Everything else is
|
|
613
|
+
* left byte-for-byte (avoids mangling any binary asset a future template
|
|
614
|
+
* might carry). */
|
|
615
|
+
const TEXT_EXTENSIONS = new Set([
|
|
616
|
+
".ts",
|
|
617
|
+
".tsx",
|
|
618
|
+
".js",
|
|
619
|
+
".jsx",
|
|
620
|
+
".mjs",
|
|
621
|
+
".cjs",
|
|
622
|
+
".json",
|
|
623
|
+
".md",
|
|
624
|
+
".yml",
|
|
625
|
+
".yaml",
|
|
626
|
+
".css",
|
|
627
|
+
".html",
|
|
628
|
+
".txt"
|
|
629
|
+
]);
|
|
630
|
+
function hasTextExtension(file) {
|
|
631
|
+
const dot = file.lastIndexOf(".");
|
|
632
|
+
if (dot < 0) return false;
|
|
633
|
+
return TEXT_EXTENSIONS.has(file.slice(dot));
|
|
634
|
+
}
|
|
635
|
+
/** Recursively list absolute file paths, skipping SUBSTITUTION_SKIP dirs. */
|
|
636
|
+
async function listFiles(dir) {
|
|
637
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
638
|
+
const files = [];
|
|
639
|
+
for (const entry of entries) {
|
|
640
|
+
if (SUBSTITUTION_SKIP.has(entry.name)) continue;
|
|
641
|
+
const full = join(dir, entry.name);
|
|
642
|
+
if (entry.isDirectory()) files.push(...await listFiles(full));
|
|
643
|
+
else files.push(full);
|
|
644
|
+
}
|
|
645
|
+
return files;
|
|
646
|
+
}
|
|
647
|
+
/** Escape a string for use in a RegExp. */
|
|
648
|
+
function escapeRegExp(value) {
|
|
649
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Replace every occurrence of the canonical project name with `appName`
|
|
653
|
+
* across the cloned tree's text files. Only files that actually contain
|
|
654
|
+
* the token are rewritten.
|
|
655
|
+
*/
|
|
656
|
+
async function rewriteProjectName(targetPath, appName) {
|
|
657
|
+
if (appName === "fluid-portal-starter") return;
|
|
658
|
+
const token = new RegExp(escapeRegExp(CANONICAL_PROJECT_NAME), "g");
|
|
659
|
+
const files = await listFiles(targetPath);
|
|
660
|
+
for (const file of files) {
|
|
661
|
+
if (!hasTextExtension(file)) continue;
|
|
662
|
+
const content = await readFile(file, "utf-8");
|
|
663
|
+
if (!content.includes("fluid-portal-starter")) continue;
|
|
664
|
+
await writeFile(file, content.replace(token, () => appName), "utf-8");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Rewrite `.fluidrc` with the chosen CLI profile (the template ships an
|
|
668
|
+
* empty profile). No-op when the file is absent. */
|
|
669
|
+
async function writeFluidrcProfile(targetPath, profileName) {
|
|
670
|
+
const fluidrcPath = join(targetPath, ".fluidrc");
|
|
671
|
+
if (!existsSync(fluidrcPath)) return;
|
|
672
|
+
await writeFile(fluidrcPath, JSON.stringify({ profile: profileName }, null, 2) + "\n", "utf-8");
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Clone the GitHub starter template into `targetPath`, discard its git
|
|
676
|
+
* history, re-substitute the project name, wire `.fluidrc`, and copy
|
|
677
|
+
* `.env.example` → `.env`.
|
|
678
|
+
*
|
|
679
|
+
* Throws (GitError or otherwise) on any failure — the caller decides
|
|
680
|
+
* whether to fall back to the bundled template.
|
|
681
|
+
*/
|
|
682
|
+
async function scaffoldFromGitHub(options) {
|
|
683
|
+
const { targetPath, appName, profileName } = options;
|
|
684
|
+
await gitClone(options.repoUrl ?? "https://github.com/Fluid-WeCommerce/portal-template.git", targetPath, { depth: 1 });
|
|
685
|
+
await rm(join(targetPath, ".git"), {
|
|
686
|
+
recursive: true,
|
|
687
|
+
force: true
|
|
688
|
+
});
|
|
689
|
+
await rewriteProjectName(targetPath, appName);
|
|
690
|
+
await writeFluidrcProfile(targetPath, profileName);
|
|
691
|
+
const envExample = join(targetPath, ".env.example");
|
|
692
|
+
try {
|
|
693
|
+
if ((await stat(envExample)).isFile()) await copyFile(envExample, join(targetPath, ".env"));
|
|
694
|
+
} catch {}
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
407
697
|
//#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) => {
|
|
698
|
+
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
699
|
try {
|
|
410
700
|
console.log();
|
|
411
701
|
console.log(chalk.bold("Creating a new Fluid portal application"));
|
|
@@ -426,65 +716,39 @@ const createCommand = new Command("create").description("Create a new Fluid port
|
|
|
426
716
|
process.exit(0);
|
|
427
717
|
}
|
|
428
718
|
console.log();
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
719
|
+
const isLocal = !!options.local;
|
|
720
|
+
const gitReady = !(isLocal || options.template === false || config.selectedPages.length > 0) && await isGitAvailable();
|
|
721
|
+
let scaffolded = false;
|
|
722
|
+
if (gitReady) {
|
|
723
|
+
const spinner = ora("Fetching starter template from GitHub...").start();
|
|
724
|
+
try {
|
|
725
|
+
await scaffoldFromGitHub({
|
|
726
|
+
targetPath,
|
|
727
|
+
appName: config.name,
|
|
728
|
+
profileName: config.profileName
|
|
729
|
+
});
|
|
730
|
+
spinner.succeed("Fetched starter template");
|
|
731
|
+
scaffolded = true;
|
|
732
|
+
} catch (error) {
|
|
733
|
+
spinner.warn("Couldn't fetch the GitHub template — using the bundled starter");
|
|
734
|
+
const reason = error instanceof GitError ? error.message.split("\n")[0] : error instanceof Error ? error.message : String(error);
|
|
735
|
+
console.log(chalk.dim(` (${reason}; source: ${TEMPLATE_REPO_URL})`));
|
|
736
|
+
try {
|
|
737
|
+
await rm(targetPath, {
|
|
738
|
+
recursive: true,
|
|
739
|
+
force: true
|
|
740
|
+
});
|
|
741
|
+
} catch {}
|
|
448
742
|
}
|
|
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
743
|
}
|
|
744
|
+
if (!scaffolded) await scaffoldFromBundled(targetPath, config, isLocal);
|
|
481
745
|
if (config.installDeps) {
|
|
482
|
-
|
|
746
|
+
const installSpinner = ora("Installing dependencies with pnpm...").start();
|
|
483
747
|
try {
|
|
484
748
|
await installDependencies(targetPath);
|
|
485
|
-
|
|
749
|
+
installSpinner.succeed("Installed dependencies");
|
|
486
750
|
} catch {
|
|
487
|
-
|
|
751
|
+
installSpinner.fail("Failed to install dependencies");
|
|
488
752
|
console.log();
|
|
489
753
|
console.log(chalk.yellow("You can try installing dependencies manually:"));
|
|
490
754
|
console.log(chalk.cyan(` cd ${appName}`));
|
|
@@ -517,6 +781,59 @@ const createCommand = new Command("create").description("Create a new Fluid port
|
|
|
517
781
|
process.exit(1);
|
|
518
782
|
}
|
|
519
783
|
});
|
|
784
|
+
/**
|
|
785
|
+
* Scaffold from the bundled base + overlay templates (Handlebars). Used
|
|
786
|
+
* when the GitHub template is skipped/unavailable, or with `--local`
|
|
787
|
+
* (which rewrites SDK/core deps to workspace `file:`/`link:` paths so the
|
|
788
|
+
* generated project resolves unpublished monorepo source).
|
|
789
|
+
*
|
|
790
|
+
* Throws on failure; the caller reports and exits.
|
|
791
|
+
*/
|
|
792
|
+
async function scaffoldFromBundled(targetPath, config, isLocal) {
|
|
793
|
+
const templatePaths = getTemplatePaths("starter");
|
|
794
|
+
if (!await directoryExists(templatePaths.base)) throw new Error("Base template not found");
|
|
795
|
+
if (!await directoryExists(templatePaths.overlay)) throw new Error("Starter template not found");
|
|
796
|
+
let sdkVersion;
|
|
797
|
+
let localCoreVersion;
|
|
798
|
+
const cliVersion = await getCliVersion();
|
|
799
|
+
if (isLocal) {
|
|
800
|
+
const monorepoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
801
|
+
const sdkPath = join(monorepoRoot, "packages", "portal", "sdk");
|
|
802
|
+
const corePath = join(monorepoRoot, "packages", "portal", "core");
|
|
803
|
+
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");
|
|
804
|
+
sdkVersion = getLocalPackageLinkVersion(targetPath, sdkPath);
|
|
805
|
+
localCoreVersion = getLocalPackageLinkVersion(targetPath, corePath);
|
|
806
|
+
console.log(chalk.cyan(" Using local packages (--local mode)"));
|
|
807
|
+
} else sdkVersion = await getSdkVersion();
|
|
808
|
+
const spinner = ora("Creating project directory...").start();
|
|
809
|
+
try {
|
|
810
|
+
await createDirectory(targetPath);
|
|
811
|
+
spinner.succeed("Created project directory");
|
|
812
|
+
} catch (error) {
|
|
813
|
+
spinner.fail("Failed to create project directory");
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
const templateVariables = {
|
|
817
|
+
projectName: config.name,
|
|
818
|
+
sdkVersion,
|
|
819
|
+
localCoreVersion,
|
|
820
|
+
cliVersion,
|
|
821
|
+
selectedPages: config.selectedPages,
|
|
822
|
+
hasSelectedPages: config.selectedPages.length > 0,
|
|
823
|
+
profileName: config.profileName
|
|
824
|
+
};
|
|
825
|
+
spinner.start("Copying template files...");
|
|
826
|
+
try {
|
|
827
|
+
await copyTemplate(templatePaths.base, targetPath, templateVariables);
|
|
828
|
+
await copyTemplate(templatePaths.overlay, targetPath, templateVariables);
|
|
829
|
+
const envExamplePath = join(targetPath, ".env.example");
|
|
830
|
+
if (await fileExists(envExamplePath)) await copyFile(envExamplePath, join(targetPath, ".env"));
|
|
831
|
+
spinner.succeed("Copied template files");
|
|
832
|
+
} catch (error) {
|
|
833
|
+
spinner.fail("Failed to copy template files");
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
520
837
|
//#endregion
|
|
521
838
|
//#region src/commands/dev.ts
|
|
522
839
|
/**
|
|
@@ -5311,6 +5628,6 @@ const plugin = {
|
|
|
5311
5628
|
}
|
|
5312
5629
|
};
|
|
5313
5630
|
//#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 };
|
|
5631
|
+
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, collectWidgetPackageArtifacts, completeWidgetPackageUpload, computeArtifactMetadata, computeFileHash, copyTemplate, copyTemplateSafe, createAuthenticatedWidgetPackageClient, createCommand, createDirectory, createDirectorySafe, createPublishManifestMetadata, createWidgetPackageEntrySource, createWidgetPackagePublishRequest, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getArtifactContentType, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, getWidgetPackageCompleteUploadRoute, getWidgetPackageUploadSessionRoute, gitClone, gitInit, gitMissingError, installDependencies, isGitAvailable, loadSourceWidgetPackages, normalizeWidgetPackageCompleteResponse, normalizeWidgetPackageUploadSession, pathExists, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, promptProjectConfig, publishWidgetPackageVersion, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, requestWidgetPackageUploadSession, resolveIdToSlug, resolvePortalWidgetSourceConfig, resolveSlugToId, rewriteProjectName, runPackageManager, scaffoldFromGitHub, sha256, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, uploadWidgetPackageArtifacts, validateCrossReferences, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
|
|
5315
5632
|
|
|
5316
5633
|
//# sourceMappingURL=index.mjs.map
|