@fluid-app/fluid-cli-portal 0.1.7 → 0.1.9
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 +607 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1474 -35
- package/dist/index.mjs.map +1 -1
- package/dist/pull-p1mSVa5W.mjs +1148 -0
- package/dist/pull-p1mSVa5W.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +63 -0
- package/dist/vite-plugin.d.mts.map +1 -0
- package/dist/vite-plugin.mjs +227 -0
- package/dist/vite-plugin.mjs.map +1 -0
- package/package.json +8 -3
- package/templates/base/.oxlintrc.json +9 -0
- package/templates/base/src/index.css +1 -156
- package/templates/base/src/main.tsx +3 -40
- package/templates/base/src/portal.config.ts +18 -10
- package/templates/base/src/screens/ExampleForm.tsx +4 -11
- package/templates/fullstack/package.json.template +2 -5
- package/templates/fullstack/vite.config.ts +2 -0
- package/templates/starter/package.json.template +3 -1
- package/templates/starter/vite.config.ts +19 -3
- package/templates/base/src/App.tsx +0 -18
- package/templates/fullstack/eslint.config.js +0 -13
- package/templates/fullstack/src/fluid.config.ts.template +0 -62
- package/templates/starter/src/fluid.config.ts.template +0 -62
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { A as deleteFluidOSNavigation, B as updateFluidOSScreen, C as writeMappings, D as createFluidOSScreen, E as createFluidOSProfile, F as listFluidOSNavigationItems, H as updateFluidOSVersion, I as listFluidOSVersions, L as updateFluidOSNavigation, M as deleteFluidOSProfile, N as deleteFluidOSScreen, O as createFluidOSTheme, P as deleteFluidOSTheme, R as updateFluidOSNavigationItem, S as updateMapping, T as createFluidOSNavigationItem, U as createFetchClient, V as updateFluidOSTheme, _ 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 deleteFluidOSNavigationItem, k as createFluidOSVersion, 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 createFluidOSNavigation, x as resolveSlugToId, y as removeMapping, z as updateFluidOSProfile } from "./pull-p1mSVa5W.mjs";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import chalk from "chalk";
|
|
3
4
|
import ora from "ora";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import path, { basename, dirname, join, resolve } from "node:path";
|
|
5
6
|
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import prompts from "prompts";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import Handlebars from "handlebars";
|
|
10
|
-
import { failure, getErrorMessage, success } from "@fluid-app/fluid-cli";
|
|
11
|
+
import { failure, getActiveProfile, getAuthToken, getErrorMessage, success } from "@fluid-app/fluid-cli";
|
|
11
12
|
import { execa } from "execa";
|
|
12
13
|
import fs from "fs-extra";
|
|
13
|
-
import path from "path";
|
|
14
|
+
import path$1 from "path";
|
|
14
15
|
import { config } from "dotenv";
|
|
15
16
|
//#region src/types.ts
|
|
16
17
|
/**
|
|
@@ -110,13 +111,13 @@ async function promptProjectConfig(projectName, options) {
|
|
|
110
111
|
}
|
|
111
112
|
//#endregion
|
|
112
113
|
//#region src/utils/file-system.ts
|
|
113
|
-
const _currentDir = dirname(fileURLToPath(import.meta.url));
|
|
114
|
+
const _currentDir$1 = dirname(fileURLToPath(import.meta.url));
|
|
114
115
|
/**
|
|
115
116
|
* Find the package root by walking up from the current directory to the nearest package.json.
|
|
116
117
|
* Works whether running from dist/ (bundled) or src/utils/ (tsx dev mode).
|
|
117
118
|
*/
|
|
118
119
|
function findPackageRoot() {
|
|
119
|
-
let dir = _currentDir;
|
|
120
|
+
let dir = _currentDir$1;
|
|
120
121
|
while (!existsSync(join(dir, "package.json"))) {
|
|
121
122
|
const parent = dirname(dir);
|
|
122
123
|
if (parent === dir) throw new Error("Could not find package root");
|
|
@@ -450,12 +451,48 @@ const createCommand = new Command("create").description("Create a new Fluid port
|
|
|
450
451
|
process.exit(1);
|
|
451
452
|
}
|
|
452
453
|
});
|
|
453
|
-
function registerCreateCommand(ctx) {
|
|
454
|
-
ctx.program.addCommand(createCommand);
|
|
455
|
-
}
|
|
456
454
|
//#endregion
|
|
457
455
|
//#region src/commands/dev.ts
|
|
458
|
-
|
|
456
|
+
/**
|
|
457
|
+
* `fluid portal dev` command
|
|
458
|
+
*
|
|
459
|
+
* Starts the Vite development server with the portal dev plugin,
|
|
460
|
+
* which intercepts manifest API requests and serves content from
|
|
461
|
+
* the local `portal/` directory.
|
|
462
|
+
*
|
|
463
|
+
* If no `portal/` directory exists, prompts the user to run `fluid portal pull`
|
|
464
|
+
* or auto-pulls if they are logged in.
|
|
465
|
+
*/
|
|
466
|
+
const PORTAL_DIR$1 = "portal";
|
|
467
|
+
/**
|
|
468
|
+
* Check if the portal directory exists and has content files.
|
|
469
|
+
* Returns true if at minimum `portal/definition.json` exists.
|
|
470
|
+
*/
|
|
471
|
+
function hasPortalContent(cwd) {
|
|
472
|
+
return existsSync(join(cwd, PORTAL_DIR$1, "definition.json"));
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Attempt to auto-pull portal content by invoking the pull command's action.
|
|
476
|
+
* Falls back to a helpful error message if pull is not possible.
|
|
477
|
+
*/
|
|
478
|
+
async function autoPull(cwd) {
|
|
479
|
+
console.log();
|
|
480
|
+
console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
|
|
481
|
+
console.log();
|
|
482
|
+
try {
|
|
483
|
+
const { pullCommand } = await import("./pull-p1mSVa5W.mjs").then((n) => n.n);
|
|
484
|
+
await pullCommand.parseAsync([], { from: "user" });
|
|
485
|
+
return hasPortalContent(cwd);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.log();
|
|
488
|
+
console.log(chalk.red("Auto-pull failed: ") + (err instanceof Error ? err.message : String(err)));
|
|
489
|
+
console.log();
|
|
490
|
+
console.log("Run " + chalk.cyan("fluid portal pull") + " manually to set up local content.");
|
|
491
|
+
console.log();
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const devCommand = new Command("dev").description("Start the development server with local portal content serving").option("-p, --port <port>", "Port to run the dev server on", "5173").option("--host", "Expose the dev server to the network").option("--skip-pull", "Skip auto-pull if portal/ directory is missing").action(async (options) => {
|
|
459
496
|
const cwd = process.cwd();
|
|
460
497
|
if (!existsSync(join(cwd, "package.json"))) {
|
|
461
498
|
console.error(chalk.red("Error: No package.json found in current directory"));
|
|
@@ -467,6 +504,19 @@ const devCommand = new Command("dev").description("Start the development server"
|
|
|
467
504
|
console.error(chalk.yellow("This command must be run from a Fluid project directory"));
|
|
468
505
|
process.exit(1);
|
|
469
506
|
}
|
|
507
|
+
if (!hasPortalContent(cwd) && !options.skipPull) {
|
|
508
|
+
if (!await autoPull(cwd)) {
|
|
509
|
+
console.error(chalk.red("Cannot start dev server without portal content."));
|
|
510
|
+
console.error(chalk.yellow("Run " + chalk.cyan("fluid portal pull") + " to download content first."));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (hasPortalContent(cwd)) {
|
|
515
|
+
console.log();
|
|
516
|
+
console.log(chalk.green("Portal dev mode: ") + "local content from " + chalk.cyan("portal/") + " will be served");
|
|
517
|
+
console.log(chalk.gray(" Manifest requests intercepted at /api/fluid_os/definitions/active"));
|
|
518
|
+
console.log(chalk.gray(" File changes in portal/ will trigger a page reload"));
|
|
519
|
+
}
|
|
470
520
|
const viteArgs = ["vite"];
|
|
471
521
|
if (options.port) viteArgs.push("--port", String(options.port));
|
|
472
522
|
if (options.host) viteArgs.push("--host");
|
|
@@ -484,9 +534,6 @@ const devCommand = new Command("dev").description("Start the development server"
|
|
|
484
534
|
process.exit(1);
|
|
485
535
|
}
|
|
486
536
|
});
|
|
487
|
-
function registerDevCommand(ctx) {
|
|
488
|
-
ctx.program.addCommand(devCommand);
|
|
489
|
-
}
|
|
490
537
|
//#endregion
|
|
491
538
|
//#region src/commands/build.ts
|
|
492
539
|
const buildCommand = new Command("build").description("Build the application for production").option("-o, --out-dir <dir>", "Output directory", "dist").action(async (options) => {
|
|
@@ -524,9 +571,6 @@ const buildCommand = new Command("build").description("Build the application for
|
|
|
524
571
|
process.exit(1);
|
|
525
572
|
}
|
|
526
573
|
});
|
|
527
|
-
function registerBuildCommand(ctx) {
|
|
528
|
-
ctx.program.addCommand(buildCommand);
|
|
529
|
-
}
|
|
530
574
|
//#endregion
|
|
531
575
|
//#region src/utils/turso.ts
|
|
532
576
|
const TURSO_API_BASE = "https://api.turso.tech/v1";
|
|
@@ -1314,7 +1358,7 @@ async function validateFluidApiKey(apiKey) {
|
|
|
1314
1358
|
* Read project name from package.json
|
|
1315
1359
|
*/
|
|
1316
1360
|
async function getProjectName(cwd) {
|
|
1317
|
-
const packageJsonPath = path.join(cwd, "package.json");
|
|
1361
|
+
const packageJsonPath = path$1.join(cwd, "package.json");
|
|
1318
1362
|
if (await fs.pathExists(packageJsonPath)) return (await fs.readJson(packageJsonPath)).name;
|
|
1319
1363
|
}
|
|
1320
1364
|
/**
|
|
@@ -1351,8 +1395,8 @@ const EXTRACT_FILENAME = "__fluid_extract_nav.ts";
|
|
|
1351
1395
|
* @param projectDir - The project root directory containing src/navigation.config.ts
|
|
1352
1396
|
*/
|
|
1353
1397
|
async function extractNavigation(projectDir) {
|
|
1354
|
-
const configPath = path.join(projectDir, "src", "navigation.config.ts");
|
|
1355
|
-
const extractFile = path.join(projectDir, EXTRACT_FILENAME);
|
|
1398
|
+
const configPath = path$1.join(projectDir, "src", "navigation.config.ts");
|
|
1399
|
+
const extractFile = path$1.join(projectDir, EXTRACT_FILENAME);
|
|
1356
1400
|
try {
|
|
1357
1401
|
const configSource = await fs.readFile(configPath, "utf-8");
|
|
1358
1402
|
if (!/export\s+(const|let)\s+navigation\s*=/.test(configSource)) return success(null);
|
|
@@ -1610,11 +1654,11 @@ async function syncNavigation(apiKey, codeItems) {
|
|
|
1610
1654
|
* Detect if the project is a fullstack template (has server entry)
|
|
1611
1655
|
*/
|
|
1612
1656
|
async function isFullstackProject(cwd) {
|
|
1613
|
-
return fs.pathExists(path.join(cwd, "src", "server", "index.ts"));
|
|
1657
|
+
return fs.pathExists(path$1.join(cwd, "src", "server", "index.ts"));
|
|
1614
1658
|
}
|
|
1615
1659
|
const deployCommand = new Command("deploy").description("Deploy the fullstack application to Cloud Run + Turso").option("--region <region>", "Cloud Run region", "us-central1").option("--gcp-project <id>", "GCP project ID (default: from gcloud config)").option("-p, --project <name>", "Service name override (default: from package.json)").option("--db-region <location>", "Turso database group location", "aws-us-east-1").option("--require-auth", "Require IAM authentication for the Cloud Run service (default: public)").option("--migrate", "Run database migrations (db:push) after successful deploy").option("--skip-local-build", "Skip the local Docker build check before deploying").option("--turso-org <slug>", "Turso organization slug (skips interactive org selection)").option("--fluid-company-api-key <key>", "Fluid company API key (skips env var lookup and prompt)").option("--skip-nav-sync", "Skip navigation sync from portal.config.ts").action(async (options) => {
|
|
1616
1660
|
const cwd = process.cwd();
|
|
1617
|
-
config({ path: path.join(cwd, ".env") });
|
|
1661
|
+
config({ path: path$1.join(cwd, ".env") });
|
|
1618
1662
|
console.log();
|
|
1619
1663
|
console.log(chalk.blue.bold("Fluid Deploy") + chalk.gray(" (Cloud Run + Turso)"));
|
|
1620
1664
|
console.log();
|
|
@@ -1646,7 +1690,7 @@ const deployCommand = new Command("deploy").description("Deploy the fullstack ap
|
|
|
1646
1690
|
process.exit(1);
|
|
1647
1691
|
}
|
|
1648
1692
|
spinner.succeed(`Fluid company: ${chalk.cyan(fluidResult.value.name)}`);
|
|
1649
|
-
if (!await fs.pathExists(path.join(cwd, "Dockerfile"))) {
|
|
1693
|
+
if (!await fs.pathExists(path$1.join(cwd, "Dockerfile"))) {
|
|
1650
1694
|
console.log(chalk.red("Error:") + " No Dockerfile found in current directory.");
|
|
1651
1695
|
console.log();
|
|
1652
1696
|
console.log("Fullstack projects created with the latest template include a Dockerfile.");
|
|
@@ -1845,7 +1889,7 @@ const deployCommand = new Command("deploy").description("Deploy the fullstack ap
|
|
|
1845
1889
|
console.log(chalk.gray("GCP Project: ") + chalk.cyan(deployResult.value.gcpProject));
|
|
1846
1890
|
console.log();
|
|
1847
1891
|
if (!options.skipNavSync) {
|
|
1848
|
-
const configPath = path.join(cwd, "src", "navigation.config.ts");
|
|
1892
|
+
const configPath = path$1.join(cwd, "src", "navigation.config.ts");
|
|
1849
1893
|
if (await fs.pathExists(configPath)) {
|
|
1850
1894
|
const navSpinner = ora("Extracting navigation from navigation.config.ts...").start();
|
|
1851
1895
|
const extractResult = await extractNavigation(cwd);
|
|
@@ -1929,14 +1973,11 @@ const deployCommand = new Command("deploy").description("Deploy the fullstack ap
|
|
|
1929
1973
|
process.exit(1);
|
|
1930
1974
|
}
|
|
1931
1975
|
});
|
|
1932
|
-
function registerDeployCommand(ctx) {
|
|
1933
|
-
ctx.program.addCommand(deployCommand);
|
|
1934
|
-
}
|
|
1935
1976
|
//#endregion
|
|
1936
1977
|
//#region src/commands/destroy.ts
|
|
1937
1978
|
const destroyCommand = new Command("destroy").description("Tear down deployed Cloud Run service and Turso database").option("--region <region>", "Cloud Run region", "us-central1").option("--gcp-project <id>", "GCP project ID (default: from gcloud config)").option("-p, --project <name>", "Service name override (default: from package.json)").option("--turso-org <slug>", "Turso organization slug (skips interactive org selection)").option("--fluid-company-api-key <key>", "Fluid company API key (skips env var lookup and prompt)").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
1938
1979
|
const cwd = process.cwd();
|
|
1939
|
-
config({ path: path.join(cwd, ".env") });
|
|
1980
|
+
config({ path: path$1.join(cwd, ".env") });
|
|
1940
1981
|
console.log();
|
|
1941
1982
|
console.log(chalk.red.bold("Fluid Destroy") + chalk.gray(" (Cloud Run + Turso)"));
|
|
1942
1983
|
console.log();
|
|
@@ -2068,23 +2109,1421 @@ const destroyCommand = new Command("destroy").description("Tear down deployed Cl
|
|
|
2068
2109
|
else console.log(chalk.yellow.bold("Destroy completed with warnings.") + " Some resources may need manual cleanup.");
|
|
2069
2110
|
console.log();
|
|
2070
2111
|
});
|
|
2071
|
-
|
|
2072
|
-
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region src/utils/push-validation.ts
|
|
2114
|
+
/**
|
|
2115
|
+
* Cross-reference validation and change categorization utilities for the push command.
|
|
2116
|
+
*
|
|
2117
|
+
* Extracted into a standalone utility so that pure logic can be tested
|
|
2118
|
+
* without pulling in CLI dependencies (ora, chalk, prompts, etc.).
|
|
2119
|
+
*/
|
|
2120
|
+
/**
|
|
2121
|
+
* Extract the slug from a file path (e.g., "screens/home.json" -> "home").
|
|
2122
|
+
*/
|
|
2123
|
+
function slugFromPath(filePath) {
|
|
2124
|
+
return basename(filePath, ".json");
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Extract the resource type directory from a file path (e.g., "screens/home.json" -> "screens").
|
|
2128
|
+
*/
|
|
2129
|
+
function resourceTypeFromPath(filePath) {
|
|
2130
|
+
const dir = filePath.split("/")[0];
|
|
2131
|
+
if (dir === "screens" || dir === "themes" || dir === "navigations" || dir === "profiles") return dir;
|
|
2132
|
+
return null;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Read and parse a JSON file from the portal directory.
|
|
2136
|
+
*/
|
|
2137
|
+
async function readPortalFile$1(portalDir, relativePath) {
|
|
2138
|
+
const content = await readFile(join(portalDir, relativePath), "utf-8");
|
|
2139
|
+
return JSON.parse(content);
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Categorize a snapshot diff into resource-type-specific change lists.
|
|
2143
|
+
*/
|
|
2144
|
+
function categorizeChanges(diff) {
|
|
2145
|
+
const result = {
|
|
2146
|
+
screens: {
|
|
2147
|
+
new: [],
|
|
2148
|
+
changed: [],
|
|
2149
|
+
deleted: []
|
|
2150
|
+
},
|
|
2151
|
+
themes: {
|
|
2152
|
+
new: [],
|
|
2153
|
+
changed: [],
|
|
2154
|
+
deleted: []
|
|
2155
|
+
},
|
|
2156
|
+
navigations: {
|
|
2157
|
+
new: [],
|
|
2158
|
+
changed: [],
|
|
2159
|
+
deleted: []
|
|
2160
|
+
},
|
|
2161
|
+
profiles: {
|
|
2162
|
+
new: [],
|
|
2163
|
+
changed: [],
|
|
2164
|
+
deleted: []
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
for (const file of diff.new) {
|
|
2168
|
+
const type = resourceTypeFromPath(file);
|
|
2169
|
+
if (type) result[type].new.push(file);
|
|
2170
|
+
}
|
|
2171
|
+
for (const file of diff.changed) {
|
|
2172
|
+
const type = resourceTypeFromPath(file);
|
|
2173
|
+
if (type) result[type].changed.push(file);
|
|
2174
|
+
}
|
|
2175
|
+
for (const file of diff.deleted) {
|
|
2176
|
+
const type = resourceTypeFromPath(file);
|
|
2177
|
+
if (type) result[type].deleted.push(file);
|
|
2178
|
+
}
|
|
2179
|
+
return result;
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Validate that all cross-references between local portal files are valid.
|
|
2183
|
+
*
|
|
2184
|
+
* Checks:
|
|
2185
|
+
* - Navigation items' "screen" slugs reference existing screen files or mappings
|
|
2186
|
+
* - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
|
|
2187
|
+
* - Profile "themes" slugs reference existing theme files or mappings
|
|
2188
|
+
*/
|
|
2189
|
+
async function validateCrossReferences(portalDir, mappings, changes) {
|
|
2190
|
+
const errors = [];
|
|
2191
|
+
const validScreenSlugs = buildValidSlugsSet(portalDir, "screens", mappings);
|
|
2192
|
+
const validNavSlugs = buildValidSlugsSet(portalDir, "navigations", mappings);
|
|
2193
|
+
const validThemeSlugs = buildValidSlugsSet(portalDir, "themes", mappings);
|
|
2194
|
+
for (const file of changes.screens.deleted) validScreenSlugs.delete(slugFromPath(file));
|
|
2195
|
+
for (const file of changes.navigations.deleted) validNavSlugs.delete(slugFromPath(file));
|
|
2196
|
+
for (const file of changes.themes.deleted) validThemeSlugs.delete(slugFromPath(file));
|
|
2197
|
+
const navFilesToCheck = [...changes.navigations.new, ...changes.navigations.changed];
|
|
2198
|
+
for (const file of navFilesToCheck) try {
|
|
2199
|
+
validateNavigationItems((await readPortalFile$1(portalDir, file)).navigation_items, file, validScreenSlugs, errors);
|
|
2200
|
+
} catch {
|
|
2201
|
+
errors.push({
|
|
2202
|
+
file,
|
|
2203
|
+
message: "Failed to read navigation file"
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
const profileFilesToCheck = [...changes.profiles.new, ...changes.profiles.changed];
|
|
2207
|
+
for (const file of profileFilesToCheck) try {
|
|
2208
|
+
const profile = await readPortalFile$1(portalDir, file);
|
|
2209
|
+
if (profile.navigation && !validNavSlugs.has(profile.navigation)) errors.push({
|
|
2210
|
+
file,
|
|
2211
|
+
message: `References navigation "${profile.navigation}" which does not exist`
|
|
2212
|
+
});
|
|
2213
|
+
if (profile.mobile_navigation && !validNavSlugs.has(profile.mobile_navigation)) errors.push({
|
|
2214
|
+
file,
|
|
2215
|
+
message: `References mobile_navigation "${profile.mobile_navigation}" which does not exist`
|
|
2216
|
+
});
|
|
2217
|
+
for (const themeSlug of profile.themes) if (!validThemeSlugs.has(themeSlug)) errors.push({
|
|
2218
|
+
file,
|
|
2219
|
+
message: `References theme "${themeSlug}" which does not exist`
|
|
2220
|
+
});
|
|
2221
|
+
} catch {
|
|
2222
|
+
errors.push({
|
|
2223
|
+
file,
|
|
2224
|
+
message: "Failed to read profile file"
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
return errors;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Build a set of valid slugs for a resource type by combining
|
|
2231
|
+
* existing mapping slugs with local file slugs on disk.
|
|
2232
|
+
*/
|
|
2233
|
+
function buildValidSlugsSet(portalDir, resourceType, mappings) {
|
|
2234
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2235
|
+
for (const slug of Object.keys(mappings[resourceType])) slugs.add(slug);
|
|
2236
|
+
const dir = join(portalDir, resourceType);
|
|
2237
|
+
if (existsSync(dir)) try {
|
|
2238
|
+
const entries = readdirSync(dir);
|
|
2239
|
+
for (const entry of entries) if (entry.endsWith(".json")) slugs.add(basename(entry, ".json"));
|
|
2240
|
+
} catch {}
|
|
2241
|
+
return slugs;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Recursively validate navigation item screen references.
|
|
2245
|
+
*/
|
|
2246
|
+
function validateNavigationItems(items, file, validScreenSlugs, errors) {
|
|
2247
|
+
for (const item of items) {
|
|
2248
|
+
if (item.screen && !validScreenSlugs.has(item.screen)) errors.push({
|
|
2249
|
+
file,
|
|
2250
|
+
message: `Navigation item "${item.label ?? "(unlabeled)"}" references screen "${item.screen}" which does not exist`
|
|
2251
|
+
});
|
|
2252
|
+
if (item.children && item.children.length > 0) validateNavigationItems(item.children, file, validScreenSlugs, errors);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
//#endregion
|
|
2256
|
+
//#region src/commands/push.ts
|
|
2257
|
+
/**
|
|
2258
|
+
* `fluid portal push` command
|
|
2259
|
+
*
|
|
2260
|
+
* Pushes local portal content changes to the Fluid OS API.
|
|
2261
|
+
* Detects changes since the last pull/push via snapshot diffing,
|
|
2262
|
+
* validates cross-references, and pushes resources in dependency order.
|
|
2263
|
+
*/
|
|
2264
|
+
const PORTAL_DIR = "portal";
|
|
2265
|
+
const PORTAL_SYNC_DIR$1 = ".portal-sync";
|
|
2266
|
+
/**
|
|
2267
|
+
* Convert the local array-form component_tree back to the object
|
|
2268
|
+
* the API expects. The pull command normalizes the API object into
|
|
2269
|
+
* an array for local convenience; this reverses that transformation.
|
|
2270
|
+
*/
|
|
2271
|
+
function toApiComponentTree(tree) {
|
|
2272
|
+
if (tree.length === 0) return null;
|
|
2273
|
+
if (tree.length === 1) return tree[0];
|
|
2274
|
+
return { children: tree };
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Create an authenticated FetchClient using the stored CLI profile.
|
|
2278
|
+
*/
|
|
2279
|
+
function createClient$1() {
|
|
2280
|
+
const token = getAuthToken();
|
|
2281
|
+
if (!token) {
|
|
2282
|
+
const profile = getActiveProfile();
|
|
2283
|
+
if (!profile) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
2284
|
+
throw new Error("No auth token found for profile " + chalk.cyan(profile.name) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
2285
|
+
}
|
|
2286
|
+
return createFetchClient({
|
|
2287
|
+
baseUrl: process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
|
|
2288
|
+
getAuthToken: () => token
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Extract an enriched error message from a caught value.
|
|
2293
|
+
* Includes structured API error data when available.
|
|
2294
|
+
*/
|
|
2295
|
+
function enrichedErrorMessage(err) {
|
|
2296
|
+
let msg = err instanceof Error ? err.message : String(err);
|
|
2297
|
+
if (err && typeof err === "object" && "data" in err) msg += ` — ${JSON.stringify(err.data)}`;
|
|
2298
|
+
return msg;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Read and parse a JSON file from the portal directory.
|
|
2302
|
+
*/
|
|
2303
|
+
async function readPortalFile(portalDir, relativePath) {
|
|
2304
|
+
const content = await readFile(join(portalDir, relativePath), "utf-8");
|
|
2305
|
+
return JSON.parse(content);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Push screen changes to the API.
|
|
2309
|
+
*/
|
|
2310
|
+
async function pushScreens(client, defId, portalDir, changes, mappings) {
|
|
2311
|
+
const results = [];
|
|
2312
|
+
let currentMappings = mappings;
|
|
2313
|
+
for (const file of changes.new) {
|
|
2314
|
+
const slug = slugFromPath(file);
|
|
2315
|
+
try {
|
|
2316
|
+
const local = await readPortalFile(portalDir, file);
|
|
2317
|
+
const newId = (await createFluidOSScreen(client, defId, { screen: {
|
|
2318
|
+
name: local.name,
|
|
2319
|
+
slug,
|
|
2320
|
+
component_tree: toApiComponentTree(local.component_tree)
|
|
2321
|
+
} })).screen?.id;
|
|
2322
|
+
if (newId != null) currentMappings = updateMapping(currentMappings, "screens", slug, newId);
|
|
2323
|
+
results.push({
|
|
2324
|
+
file,
|
|
2325
|
+
action: "created",
|
|
2326
|
+
success: true
|
|
2327
|
+
});
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
results.push({
|
|
2330
|
+
file,
|
|
2331
|
+
action: "created",
|
|
2332
|
+
success: false,
|
|
2333
|
+
error: enrichedErrorMessage(err)
|
|
2334
|
+
});
|
|
2335
|
+
return {
|
|
2336
|
+
results,
|
|
2337
|
+
mappings: currentMappings
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
for (const file of changes.changed) {
|
|
2342
|
+
const slug = slugFromPath(file);
|
|
2343
|
+
const screenId = resolveSlugToId(currentMappings, "screens", slug);
|
|
2344
|
+
if (screenId == null) {
|
|
2345
|
+
results.push({
|
|
2346
|
+
file,
|
|
2347
|
+
action: "updated",
|
|
2348
|
+
success: false,
|
|
2349
|
+
error: `No mapping found for screen slug "${slug}"`
|
|
2350
|
+
});
|
|
2351
|
+
return {
|
|
2352
|
+
results,
|
|
2353
|
+
mappings: currentMappings
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
try {
|
|
2357
|
+
const local = await readPortalFile(portalDir, file);
|
|
2358
|
+
await updateFluidOSScreen(client, defId, screenId, { screen: {
|
|
2359
|
+
name: local.name,
|
|
2360
|
+
slug,
|
|
2361
|
+
component_tree: toApiComponentTree(local.component_tree)
|
|
2362
|
+
} });
|
|
2363
|
+
results.push({
|
|
2364
|
+
file,
|
|
2365
|
+
action: "updated",
|
|
2366
|
+
success: true
|
|
2367
|
+
});
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
results.push({
|
|
2370
|
+
file,
|
|
2371
|
+
action: "updated",
|
|
2372
|
+
success: false,
|
|
2373
|
+
error: enrichedErrorMessage(err)
|
|
2374
|
+
});
|
|
2375
|
+
return {
|
|
2376
|
+
results,
|
|
2377
|
+
mappings: currentMappings
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
for (const file of changes.deleted) {
|
|
2382
|
+
const slug = slugFromPath(file);
|
|
2383
|
+
const screenId = resolveSlugToId(currentMappings, "screens", slug);
|
|
2384
|
+
if (screenId == null) {
|
|
2385
|
+
results.push({
|
|
2386
|
+
file,
|
|
2387
|
+
action: "deleted",
|
|
2388
|
+
success: false,
|
|
2389
|
+
error: `No mapping found for screen slug "${slug}"`
|
|
2390
|
+
});
|
|
2391
|
+
return {
|
|
2392
|
+
results,
|
|
2393
|
+
mappings: currentMappings
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
try {
|
|
2397
|
+
await deleteFluidOSScreen(client, defId, screenId);
|
|
2398
|
+
currentMappings = removeMapping(currentMappings, "screens", slug);
|
|
2399
|
+
results.push({
|
|
2400
|
+
file,
|
|
2401
|
+
action: "deleted",
|
|
2402
|
+
success: true
|
|
2403
|
+
});
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
results.push({
|
|
2406
|
+
file,
|
|
2407
|
+
action: "deleted",
|
|
2408
|
+
success: false,
|
|
2409
|
+
error: enrichedErrorMessage(err)
|
|
2410
|
+
});
|
|
2411
|
+
return {
|
|
2412
|
+
results,
|
|
2413
|
+
mappings: currentMappings
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return {
|
|
2418
|
+
results,
|
|
2419
|
+
mappings: currentMappings
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Push theme changes to the API.
|
|
2424
|
+
*/
|
|
2425
|
+
async function pushThemes(client, defId, portalDir, changes, mappings) {
|
|
2426
|
+
const results = [];
|
|
2427
|
+
let currentMappings = mappings;
|
|
2428
|
+
for (const file of changes.new) {
|
|
2429
|
+
const slug = slugFromPath(file);
|
|
2430
|
+
try {
|
|
2431
|
+
const local = await readPortalFile(portalDir, file);
|
|
2432
|
+
const newId = (await createFluidOSTheme(client, defId, { theme: {
|
|
2433
|
+
name: local.name,
|
|
2434
|
+
active: local.active,
|
|
2435
|
+
config: local.config
|
|
2436
|
+
} })).theme?.id;
|
|
2437
|
+
if (newId != null) currentMappings = updateMapping(currentMappings, "themes", slug, newId);
|
|
2438
|
+
results.push({
|
|
2439
|
+
file,
|
|
2440
|
+
action: "created",
|
|
2441
|
+
success: true
|
|
2442
|
+
});
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
results.push({
|
|
2445
|
+
file,
|
|
2446
|
+
action: "created",
|
|
2447
|
+
success: false,
|
|
2448
|
+
error: enrichedErrorMessage(err)
|
|
2449
|
+
});
|
|
2450
|
+
return {
|
|
2451
|
+
results,
|
|
2452
|
+
mappings: currentMappings
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
for (const file of changes.changed) {
|
|
2457
|
+
const slug = slugFromPath(file);
|
|
2458
|
+
const themeId = resolveSlugToId(currentMappings, "themes", slug);
|
|
2459
|
+
if (themeId == null) {
|
|
2460
|
+
results.push({
|
|
2461
|
+
file,
|
|
2462
|
+
action: "updated",
|
|
2463
|
+
success: false,
|
|
2464
|
+
error: `No mapping found for theme slug "${slug}"`
|
|
2465
|
+
});
|
|
2466
|
+
return {
|
|
2467
|
+
results,
|
|
2468
|
+
mappings: currentMappings
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
try {
|
|
2472
|
+
const local = await readPortalFile(portalDir, file);
|
|
2473
|
+
await updateFluidOSTheme(client, defId, themeId, { theme: {
|
|
2474
|
+
name: local.name,
|
|
2475
|
+
active: local.active,
|
|
2476
|
+
config: local.config
|
|
2477
|
+
} });
|
|
2478
|
+
results.push({
|
|
2479
|
+
file,
|
|
2480
|
+
action: "updated",
|
|
2481
|
+
success: true
|
|
2482
|
+
});
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
results.push({
|
|
2485
|
+
file,
|
|
2486
|
+
action: "updated",
|
|
2487
|
+
success: false,
|
|
2488
|
+
error: enrichedErrorMessage(err)
|
|
2489
|
+
});
|
|
2490
|
+
return {
|
|
2491
|
+
results,
|
|
2492
|
+
mappings: currentMappings
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
for (const file of changes.deleted) {
|
|
2497
|
+
const slug = slugFromPath(file);
|
|
2498
|
+
const themeId = resolveSlugToId(currentMappings, "themes", slug);
|
|
2499
|
+
if (themeId == null) {
|
|
2500
|
+
results.push({
|
|
2501
|
+
file,
|
|
2502
|
+
action: "deleted",
|
|
2503
|
+
success: false,
|
|
2504
|
+
error: `No mapping found for theme slug "${slug}"`
|
|
2505
|
+
});
|
|
2506
|
+
return {
|
|
2507
|
+
results,
|
|
2508
|
+
mappings: currentMappings
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
try {
|
|
2512
|
+
await deleteFluidOSTheme(client, defId, themeId);
|
|
2513
|
+
currentMappings = removeMapping(currentMappings, "themes", slug);
|
|
2514
|
+
results.push({
|
|
2515
|
+
file,
|
|
2516
|
+
action: "deleted",
|
|
2517
|
+
success: true
|
|
2518
|
+
});
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
results.push({
|
|
2521
|
+
file,
|
|
2522
|
+
action: "deleted",
|
|
2523
|
+
success: false,
|
|
2524
|
+
error: enrichedErrorMessage(err)
|
|
2525
|
+
});
|
|
2526
|
+
return {
|
|
2527
|
+
results,
|
|
2528
|
+
mappings: currentMappings
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
results,
|
|
2534
|
+
mappings: currentMappings
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Resolve screen slug references to screen IDs in navigation items.
|
|
2539
|
+
* Returns a new tree with `screen_id` instead of `screen` slug,
|
|
2540
|
+
* shaped to match the FluidOSNavigationItemSyncItem schema.
|
|
2541
|
+
*/
|
|
2542
|
+
function resolveNavigationItemScreenIds(items, mappings) {
|
|
2543
|
+
return items.map((item) => {
|
|
2544
|
+
const screenId = item.screen ? resolveSlugToId(mappings, "screens", item.screen) ?? void 0 : void 0;
|
|
2545
|
+
return {
|
|
2546
|
+
...item.id ? { id: item.id } : {},
|
|
2547
|
+
label: item.label ?? "",
|
|
2548
|
+
position: item.position ?? 0,
|
|
2549
|
+
icon: item.icon,
|
|
2550
|
+
screen_id: screenId ?? null,
|
|
2551
|
+
slug: item.slug,
|
|
2552
|
+
source: item.source ?? "user",
|
|
2553
|
+
parent_id: item.parent_id,
|
|
2554
|
+
children: resolveNavigationItemScreenIds(item.children ?? [], mappings)
|
|
2555
|
+
};
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Flatten a tree of navigation sync items into a flat list.
|
|
2560
|
+
* The API reconciliation logic requires a flat list to correctly
|
|
2561
|
+
* compare against the flat server response.
|
|
2562
|
+
*/
|
|
2563
|
+
function flattenNavigationItems(items) {
|
|
2564
|
+
const flat = [];
|
|
2565
|
+
for (const item of items) {
|
|
2566
|
+
const { children, ...rest } = item;
|
|
2567
|
+
flat.push(rest);
|
|
2568
|
+
if (children && children.length > 0) flat.push(...flattenNavigationItems(children));
|
|
2569
|
+
}
|
|
2570
|
+
return flat;
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Push navigation changes to the API.
|
|
2574
|
+
*/
|
|
2575
|
+
async function pushNavigations(client, defId, portalDir, changes, mappings) {
|
|
2576
|
+
const results = [];
|
|
2577
|
+
let currentMappings = mappings;
|
|
2578
|
+
for (const file of changes.new) {
|
|
2579
|
+
const slug = slugFromPath(file);
|
|
2580
|
+
try {
|
|
2581
|
+
const local = await readPortalFile(portalDir, file);
|
|
2582
|
+
const newId = (await createFluidOSNavigation(client, defId, { navigation: {
|
|
2583
|
+
name: local.name,
|
|
2584
|
+
platform: local.platform
|
|
2585
|
+
} })).navigation?.id;
|
|
2586
|
+
if (newId != null) {
|
|
2587
|
+
currentMappings = updateMapping(currentMappings, "navigations", slug, newId);
|
|
2588
|
+
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2589
|
+
const localToServerId = /* @__PURE__ */ new Map();
|
|
2590
|
+
for (const item of resolvedItems) {
|
|
2591
|
+
const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
|
|
2592
|
+
const created = await createFluidOSNavigationItem(client, defId, newId, { navigation_item: {
|
|
2593
|
+
label: item.label ?? "",
|
|
2594
|
+
position: item.position ?? 0,
|
|
2595
|
+
icon: item.icon ?? void 0,
|
|
2596
|
+
screen_id: item.screen_id ?? void 0,
|
|
2597
|
+
slug: item.slug ?? void 0,
|
|
2598
|
+
source: item.source ?? void 0,
|
|
2599
|
+
parent_id: resolvedParentId
|
|
2600
|
+
} });
|
|
2601
|
+
if (item.id != null && created.navigation_item?.id != null) localToServerId.set(item.id, created.navigation_item.id);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
results.push({
|
|
2605
|
+
file,
|
|
2606
|
+
action: "created",
|
|
2607
|
+
success: true
|
|
2608
|
+
});
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
results.push({
|
|
2611
|
+
file,
|
|
2612
|
+
action: "created",
|
|
2613
|
+
success: false,
|
|
2614
|
+
error: enrichedErrorMessage(err)
|
|
2615
|
+
});
|
|
2616
|
+
return {
|
|
2617
|
+
results,
|
|
2618
|
+
mappings: currentMappings
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
for (const file of changes.changed) {
|
|
2623
|
+
const slug = slugFromPath(file);
|
|
2624
|
+
const navId = resolveSlugToId(currentMappings, "navigations", slug);
|
|
2625
|
+
if (navId == null) {
|
|
2626
|
+
results.push({
|
|
2627
|
+
file,
|
|
2628
|
+
action: "updated",
|
|
2629
|
+
success: false,
|
|
2630
|
+
error: `No mapping found for navigation slug "${slug}"`
|
|
2631
|
+
});
|
|
2632
|
+
return {
|
|
2633
|
+
results,
|
|
2634
|
+
mappings: currentMappings
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
try {
|
|
2638
|
+
const local = await readPortalFile(portalDir, file);
|
|
2639
|
+
await updateFluidOSNavigation(client, defId, navId, { navigation: {
|
|
2640
|
+
name: local.name,
|
|
2641
|
+
platform: local.platform
|
|
2642
|
+
} });
|
|
2643
|
+
const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
|
|
2644
|
+
const serverItems = (await listFluidOSNavigationItems(client, defId, navId)).navigation_items ?? [];
|
|
2645
|
+
const serverById = new Map(serverItems.map((s) => [s.id, s]));
|
|
2646
|
+
const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
|
|
2647
|
+
for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await deleteFluidOSNavigationItem(client, defId, navId, serverItem.id);
|
|
2648
|
+
const localToServerId = /* @__PURE__ */ new Map();
|
|
2649
|
+
for (const item of resolvedItems) {
|
|
2650
|
+
const resolvedParentId = item.parent_id != null ? localToServerId.get(item.parent_id) ?? item.parent_id : void 0;
|
|
2651
|
+
const body = {
|
|
2652
|
+
label: item.label,
|
|
2653
|
+
position: item.position,
|
|
2654
|
+
icon: item.icon ?? void 0,
|
|
2655
|
+
screen_id: item.screen_id ?? void 0,
|
|
2656
|
+
slug: item.slug ?? void 0,
|
|
2657
|
+
source: item.source ?? void 0,
|
|
2658
|
+
parent_id: resolvedParentId
|
|
2659
|
+
};
|
|
2660
|
+
if (item.id && serverById.has(item.id)) await updateFluidOSNavigationItem(client, defId, navId, item.id, { navigation_item: body });
|
|
2661
|
+
else {
|
|
2662
|
+
const created = await createFluidOSNavigationItem(client, defId, navId, { navigation_item: {
|
|
2663
|
+
...body,
|
|
2664
|
+
label: body.label ?? "",
|
|
2665
|
+
position: body.position ?? 0
|
|
2666
|
+
} });
|
|
2667
|
+
if (item.id != null && created.navigation_item?.id != null) localToServerId.set(item.id, created.navigation_item.id);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
results.push({
|
|
2671
|
+
file,
|
|
2672
|
+
action: "updated",
|
|
2673
|
+
success: true
|
|
2674
|
+
});
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
results.push({
|
|
2677
|
+
file,
|
|
2678
|
+
action: "updated",
|
|
2679
|
+
success: false,
|
|
2680
|
+
error: enrichedErrorMessage(err)
|
|
2681
|
+
});
|
|
2682
|
+
return {
|
|
2683
|
+
results,
|
|
2684
|
+
mappings: currentMappings
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
for (const file of changes.deleted) {
|
|
2689
|
+
const slug = slugFromPath(file);
|
|
2690
|
+
const navId = resolveSlugToId(currentMappings, "navigations", slug);
|
|
2691
|
+
if (navId == null) {
|
|
2692
|
+
results.push({
|
|
2693
|
+
file,
|
|
2694
|
+
action: "deleted",
|
|
2695
|
+
success: false,
|
|
2696
|
+
error: `No mapping found for navigation slug "${slug}"`
|
|
2697
|
+
});
|
|
2698
|
+
return {
|
|
2699
|
+
results,
|
|
2700
|
+
mappings: currentMappings
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
try {
|
|
2704
|
+
await deleteFluidOSNavigation(client, defId, navId);
|
|
2705
|
+
currentMappings = removeMapping(currentMappings, "navigations", slug);
|
|
2706
|
+
results.push({
|
|
2707
|
+
file,
|
|
2708
|
+
action: "deleted",
|
|
2709
|
+
success: true
|
|
2710
|
+
});
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
results.push({
|
|
2713
|
+
file,
|
|
2714
|
+
action: "deleted",
|
|
2715
|
+
success: false,
|
|
2716
|
+
error: enrichedErrorMessage(err)
|
|
2717
|
+
});
|
|
2718
|
+
return {
|
|
2719
|
+
results,
|
|
2720
|
+
mappings: currentMappings
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
return {
|
|
2725
|
+
results,
|
|
2726
|
+
mappings: currentMappings
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Push profile changes to the API.
|
|
2731
|
+
*/
|
|
2732
|
+
async function pushProfiles(client, defId, portalDir, changes, mappings) {
|
|
2733
|
+
const results = [];
|
|
2734
|
+
let currentMappings = mappings;
|
|
2735
|
+
for (const file of changes.new) {
|
|
2736
|
+
const slug = slugFromPath(file);
|
|
2737
|
+
try {
|
|
2738
|
+
const newId = (await createFluidOSProfile(client, defId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) })).profile?.id;
|
|
2739
|
+
if (newId != null) currentMappings = updateMapping(currentMappings, "profiles", slug, newId);
|
|
2740
|
+
results.push({
|
|
2741
|
+
file,
|
|
2742
|
+
action: "created",
|
|
2743
|
+
success: true
|
|
2744
|
+
});
|
|
2745
|
+
} catch (err) {
|
|
2746
|
+
results.push({
|
|
2747
|
+
file,
|
|
2748
|
+
action: "created",
|
|
2749
|
+
success: false,
|
|
2750
|
+
error: enrichedErrorMessage(err)
|
|
2751
|
+
});
|
|
2752
|
+
return {
|
|
2753
|
+
results,
|
|
2754
|
+
mappings: currentMappings
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
for (const file of changes.changed) {
|
|
2759
|
+
const slug = slugFromPath(file);
|
|
2760
|
+
const profileId = resolveSlugToId(currentMappings, "profiles", slug);
|
|
2761
|
+
if (profileId == null) {
|
|
2762
|
+
results.push({
|
|
2763
|
+
file,
|
|
2764
|
+
action: "updated",
|
|
2765
|
+
success: false,
|
|
2766
|
+
error: `No mapping found for profile slug "${slug}"`
|
|
2767
|
+
});
|
|
2768
|
+
return {
|
|
2769
|
+
results,
|
|
2770
|
+
mappings: currentMappings
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
try {
|
|
2774
|
+
await updateFluidOSProfile(client, defId, profileId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) });
|
|
2775
|
+
results.push({
|
|
2776
|
+
file,
|
|
2777
|
+
action: "updated",
|
|
2778
|
+
success: true
|
|
2779
|
+
});
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
results.push({
|
|
2782
|
+
file,
|
|
2783
|
+
action: "updated",
|
|
2784
|
+
success: false,
|
|
2785
|
+
error: enrichedErrorMessage(err)
|
|
2786
|
+
});
|
|
2787
|
+
return {
|
|
2788
|
+
results,
|
|
2789
|
+
mappings: currentMappings
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
for (const file of changes.deleted) {
|
|
2794
|
+
const slug = slugFromPath(file);
|
|
2795
|
+
const profileId = resolveSlugToId(currentMappings, "profiles", slug);
|
|
2796
|
+
if (profileId == null) {
|
|
2797
|
+
results.push({
|
|
2798
|
+
file,
|
|
2799
|
+
action: "deleted",
|
|
2800
|
+
success: false,
|
|
2801
|
+
error: `No mapping found for profile slug "${slug}"`
|
|
2802
|
+
});
|
|
2803
|
+
return {
|
|
2804
|
+
results,
|
|
2805
|
+
mappings: currentMappings
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
await deleteFluidOSProfile(client, defId, profileId);
|
|
2810
|
+
currentMappings = removeMapping(currentMappings, "profiles", slug);
|
|
2811
|
+
results.push({
|
|
2812
|
+
file,
|
|
2813
|
+
action: "deleted",
|
|
2814
|
+
success: true
|
|
2815
|
+
});
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
results.push({
|
|
2818
|
+
file,
|
|
2819
|
+
action: "deleted",
|
|
2820
|
+
success: false,
|
|
2821
|
+
error: enrichedErrorMessage(err)
|
|
2822
|
+
});
|
|
2823
|
+
return {
|
|
2824
|
+
results,
|
|
2825
|
+
mappings: currentMappings
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return {
|
|
2830
|
+
results,
|
|
2831
|
+
mappings: currentMappings
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Resolve profile slug references to API IDs for create/update request body.
|
|
2836
|
+
*/
|
|
2837
|
+
function resolveProfileBody(local, mappings) {
|
|
2838
|
+
const body = {
|
|
2839
|
+
name: local.name,
|
|
2840
|
+
default: local.default,
|
|
2841
|
+
permissions: local.permissions
|
|
2842
|
+
};
|
|
2843
|
+
if (local.navigation) {
|
|
2844
|
+
const navId = resolveSlugToId(mappings, "navigations", local.navigation);
|
|
2845
|
+
if (navId != null) body.navigation_id = navId;
|
|
2846
|
+
}
|
|
2847
|
+
if (local.mobile_navigation) {
|
|
2848
|
+
const mobileNavId = resolveSlugToId(mappings, "navigations", local.mobile_navigation);
|
|
2849
|
+
if (mobileNavId != null) body.mobile_navigation_id = mobileNavId;
|
|
2850
|
+
}
|
|
2851
|
+
body.theme_ids = local.themes.map((slug) => resolveSlugToId(mappings, "themes", slug)).filter((id) => id != null);
|
|
2852
|
+
return body;
|
|
2853
|
+
}
|
|
2854
|
+
function printChangesSummary(diff, definitionName) {
|
|
2855
|
+
console.log(chalk.blue("Changes to push for ") + chalk.white.bold(`"${definitionName}"`) + chalk.blue(":"));
|
|
2856
|
+
console.log();
|
|
2857
|
+
if (diff.new.length > 0) console.log(chalk.green(" New: ") + diff.new.join(", "));
|
|
2858
|
+
if (diff.changed.length > 0) console.log(chalk.yellow(" Changed: ") + diff.changed.join(", "));
|
|
2859
|
+
if (diff.deleted.length > 0) console.log(chalk.red(" Deleted: ") + diff.deleted.join(", "));
|
|
2860
|
+
console.log();
|
|
2861
|
+
}
|
|
2862
|
+
function printPushReport(results, skippedPhases) {
|
|
2863
|
+
const succeeded = results.filter((r) => r.success);
|
|
2864
|
+
const failed = results.filter((r) => !r.success);
|
|
2865
|
+
if (succeeded.length > 0) {
|
|
2866
|
+
console.log(chalk.green.bold("Succeeded:"));
|
|
2867
|
+
for (const r of succeeded) console.log(chalk.green(" " + r.action + ": ") + r.file);
|
|
2868
|
+
}
|
|
2869
|
+
if (failed.length > 0) {
|
|
2870
|
+
console.log();
|
|
2871
|
+
console.log(chalk.red.bold("Failed:"));
|
|
2872
|
+
for (const r of failed) console.log(chalk.red(" " + r.action + ": ") + r.file + chalk.gray(" — " + (r.error ?? "Unknown error")));
|
|
2873
|
+
}
|
|
2874
|
+
if (skippedPhases.length > 0) {
|
|
2875
|
+
console.log();
|
|
2876
|
+
console.log(chalk.yellow.bold("Skipped phases:"));
|
|
2877
|
+
for (const phase of skippedPhases) console.log(chalk.yellow(" " + phase));
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
const pushCommand = new Command("push").description("Push local portal content changes to the Fluid OS API").option("--yes", "Skip confirmation prompt").action(async (options) => {
|
|
2881
|
+
const cwd = process.cwd();
|
|
2882
|
+
const portalDir = join(cwd, PORTAL_DIR);
|
|
2883
|
+
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR$1);
|
|
2884
|
+
console.log();
|
|
2885
|
+
console.log(chalk.blue.bold("Fluid Portal Push"));
|
|
2886
|
+
console.log();
|
|
2887
|
+
if (!existsSync(portalDir)) {
|
|
2888
|
+
console.log(chalk.red("Error:") + " No portal/ directory found. Run " + chalk.cyan("fluid portal pull") + " first.");
|
|
2889
|
+
console.log();
|
|
2890
|
+
process.exit(1);
|
|
2891
|
+
}
|
|
2892
|
+
const snapshot = await readSnapshot(portalSyncDir);
|
|
2893
|
+
if (!snapshot) {
|
|
2894
|
+
console.log(chalk.red("Error:") + " No snapshot found in .portal-sync/. Run " + chalk.cyan("fluid portal pull") + " first.");
|
|
2895
|
+
console.log();
|
|
2896
|
+
process.exit(1);
|
|
2897
|
+
}
|
|
2898
|
+
const mappings = await readMappings(portalSyncDir);
|
|
2899
|
+
if (!mappings) {
|
|
2900
|
+
console.log(chalk.red("Error:") + " No mappings found in .portal-sync/. Run " + chalk.cyan("fluid portal pull") + " first.");
|
|
2901
|
+
console.log();
|
|
2902
|
+
process.exit(1);
|
|
2903
|
+
}
|
|
2904
|
+
const definitionId = snapshot.definition_id;
|
|
2905
|
+
const definitionName = snapshot.definition;
|
|
2906
|
+
console.log(chalk.gray("Definition: ") + chalk.white(definitionName) + chalk.gray(` (ID: ${definitionId})`));
|
|
2907
|
+
console.log();
|
|
2908
|
+
const spinner = ora();
|
|
2909
|
+
spinner.start("Detecting changes...");
|
|
2910
|
+
const diff = await diffAgainstSnapshot(portalDir, snapshot);
|
|
2911
|
+
const totalChanges = diff.new.length + diff.changed.length + diff.deleted.length;
|
|
2912
|
+
if (totalChanges === 0) {
|
|
2913
|
+
spinner.succeed("Nothing to push.");
|
|
2914
|
+
console.log();
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
spinner.succeed(`Found ${totalChanges} change(s)`);
|
|
2918
|
+
console.log();
|
|
2919
|
+
printChangesSummary(diff, definitionName);
|
|
2920
|
+
if (!options.yes) {
|
|
2921
|
+
const { confirmed } = await prompts({
|
|
2922
|
+
type: "confirm",
|
|
2923
|
+
name: "confirmed",
|
|
2924
|
+
message: `Push ${totalChanges} change(s) to Fluid OS?`,
|
|
2925
|
+
initial: false
|
|
2926
|
+
});
|
|
2927
|
+
if (!confirmed) {
|
|
2928
|
+
console.log();
|
|
2929
|
+
console.log(chalk.gray("Push cancelled."));
|
|
2930
|
+
console.log();
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
console.log();
|
|
2934
|
+
}
|
|
2935
|
+
const changes = categorizeChanges(diff);
|
|
2936
|
+
spinner.start("Validating cross-references...");
|
|
2937
|
+
const validationErrors = await validateCrossReferences(portalDir, mappings, changes);
|
|
2938
|
+
if (validationErrors.length > 0) {
|
|
2939
|
+
spinner.fail("Cross-reference validation failed");
|
|
2940
|
+
console.log();
|
|
2941
|
+
for (const err of validationErrors) console.log(chalk.red(" " + err.file + ": ") + err.message);
|
|
2942
|
+
console.log();
|
|
2943
|
+
process.exit(1);
|
|
2944
|
+
}
|
|
2945
|
+
spinner.succeed("Cross-references valid");
|
|
2946
|
+
spinner.start("Authenticating...");
|
|
2947
|
+
let client;
|
|
2948
|
+
try {
|
|
2949
|
+
client = createClient$1();
|
|
2950
|
+
spinner.succeed("Authenticated");
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
spinner.fail("Authentication failed");
|
|
2953
|
+
console.log();
|
|
2954
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
2955
|
+
console.log();
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
const allResults = [];
|
|
2959
|
+
const skippedPhases = [];
|
|
2960
|
+
let currentMappings = mappings;
|
|
2961
|
+
let aborted = false;
|
|
2962
|
+
const hasScreenChanges = changes.screens.new.length > 0 || changes.screens.changed.length > 0 || changes.screens.deleted.length > 0;
|
|
2963
|
+
const hasThemeChanges = changes.themes.new.length > 0 || changes.themes.changed.length > 0 || changes.themes.deleted.length > 0;
|
|
2964
|
+
if (hasScreenChanges || hasThemeChanges) {
|
|
2965
|
+
spinner.start("Phase 1: Pushing screens and themes...");
|
|
2966
|
+
const phase1Tasks = [];
|
|
2967
|
+
if (hasScreenChanges) phase1Tasks.push(pushScreens(client, definitionId, portalDir, changes.screens, currentMappings));
|
|
2968
|
+
if (hasThemeChanges) phase1Tasks.push(pushThemes(client, definitionId, portalDir, changes.themes, currentMappings));
|
|
2969
|
+
const phase1Results = await Promise.all(phase1Tasks);
|
|
2970
|
+
for (const result of phase1Results) allResults.push(...result.results);
|
|
2971
|
+
if (hasScreenChanges) currentMappings = {
|
|
2972
|
+
...currentMappings,
|
|
2973
|
+
screens: phase1Results[0].mappings.screens
|
|
2974
|
+
};
|
|
2975
|
+
if (hasThemeChanges) {
|
|
2976
|
+
const idx = hasScreenChanges ? 1 : 0;
|
|
2977
|
+
currentMappings = {
|
|
2978
|
+
...currentMappings,
|
|
2979
|
+
themes: phase1Results[idx].mappings.themes
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
if (phase1Results.some((r) => r.results.some((res) => !res.success))) {
|
|
2983
|
+
spinner.fail("Phase 1 failed");
|
|
2984
|
+
aborted = true;
|
|
2985
|
+
skippedPhases.push("Phase 2: Navigations", "Phase 3: Profiles");
|
|
2986
|
+
} else spinner.succeed("Phase 1 complete");
|
|
2987
|
+
}
|
|
2988
|
+
const hasNavChanges = changes.navigations.new.length > 0 || changes.navigations.changed.length > 0 || changes.navigations.deleted.length > 0;
|
|
2989
|
+
if (!aborted && hasNavChanges) {
|
|
2990
|
+
spinner.start("Phase 2: Pushing navigations...");
|
|
2991
|
+
const navResult = await pushNavigations(client, definitionId, portalDir, changes.navigations, currentMappings);
|
|
2992
|
+
allResults.push(...navResult.results);
|
|
2993
|
+
currentMappings = {
|
|
2994
|
+
...currentMappings,
|
|
2995
|
+
navigations: navResult.mappings.navigations
|
|
2996
|
+
};
|
|
2997
|
+
if (navResult.results.some((r) => !r.success)) {
|
|
2998
|
+
spinner.fail("Phase 2 failed");
|
|
2999
|
+
aborted = true;
|
|
3000
|
+
skippedPhases.push("Phase 3: Profiles");
|
|
3001
|
+
} else spinner.succeed("Phase 2 complete");
|
|
3002
|
+
} else if (aborted && hasNavChanges) {}
|
|
3003
|
+
const hasProfileChanges = changes.profiles.new.length > 0 || changes.profiles.changed.length > 0 || changes.profiles.deleted.length > 0;
|
|
3004
|
+
if (!aborted && hasProfileChanges) {
|
|
3005
|
+
spinner.start("Phase 3: Pushing profiles...");
|
|
3006
|
+
const profileResult = await pushProfiles(client, definitionId, portalDir, changes.profiles, currentMappings);
|
|
3007
|
+
allResults.push(...profileResult.results);
|
|
3008
|
+
currentMappings = {
|
|
3009
|
+
...currentMappings,
|
|
3010
|
+
profiles: profileResult.mappings.profiles
|
|
3011
|
+
};
|
|
3012
|
+
if (profileResult.results.some((r) => !r.success)) spinner.fail("Phase 3 failed");
|
|
3013
|
+
else spinner.succeed("Phase 3 complete");
|
|
3014
|
+
}
|
|
3015
|
+
await writeMappings(portalSyncDir, currentMappings);
|
|
3016
|
+
const successfulFiles = new Set(allResults.filter((r) => r.success).map((r) => r.file));
|
|
3017
|
+
if (successfulFiles.size > 0) {
|
|
3018
|
+
const updatedHashes = { ...snapshot.files };
|
|
3019
|
+
for (const file of successfulFiles) {
|
|
3020
|
+
const fullPath = join(portalDir, file);
|
|
3021
|
+
if (existsSync(fullPath)) updatedHashes[file] = await computeFileHash(fullPath);
|
|
3022
|
+
else delete updatedHashes[file];
|
|
3023
|
+
}
|
|
3024
|
+
await writeSnapshot(portalSyncDir, {
|
|
3025
|
+
...snapshot,
|
|
3026
|
+
files: updatedHashes
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
console.log();
|
|
3030
|
+
if (allResults.every((r) => r.success) && !aborted) console.log(chalk.green.bold("Push complete!"));
|
|
3031
|
+
else console.log(chalk.yellow.bold("Push completed with issues:"));
|
|
3032
|
+
console.log();
|
|
3033
|
+
printPushReport(allResults, skippedPhases);
|
|
3034
|
+
console.log();
|
|
3035
|
+
});
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/utils/widget-helpers.ts
|
|
3038
|
+
/**
|
|
3039
|
+
* Pure helper functions for widget scaffolding.
|
|
3040
|
+
* Extracted from widget-create command for testability.
|
|
3041
|
+
*/
|
|
3042
|
+
/**
|
|
3043
|
+
* Convert kebab-case name to PascalCase widget type.
|
|
3044
|
+
* e.g., "stock-ticker" → "StockTickerWidget"
|
|
3045
|
+
*/
|
|
3046
|
+
function toWidgetType(name) {
|
|
3047
|
+
const pascal = name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
3048
|
+
return pascal.endsWith("Widget") ? pascal : `${pascal}Widget`;
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* Derive the component name from a widget type.
|
|
3052
|
+
* Strips the trailing "Widget" suffix, but only if the result is non-empty
|
|
3053
|
+
* and starts with a letter (not a digit).
|
|
3054
|
+
* e.g., "StockTickerWidget" → "StockTicker"
|
|
3055
|
+
*/
|
|
3056
|
+
function toComponentName(widgetType) {
|
|
3057
|
+
if (widgetType === "Widget") return "Widget";
|
|
3058
|
+
return widgetType.replace(/Widget$/, "") || widgetType;
|
|
3059
|
+
}
|
|
3060
|
+
/**
|
|
3061
|
+
* Convert kebab-case name to a display name.
|
|
3062
|
+
* e.g., "stock-ticker" → "Stock Ticker"
|
|
3063
|
+
*/
|
|
3064
|
+
function toDisplayName(name) {
|
|
3065
|
+
return name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Convert kebab-case to camelCase.
|
|
3069
|
+
* e.g., "stock-ticker" → "stockTicker"
|
|
3070
|
+
*/
|
|
3071
|
+
function toCamelCase(name) {
|
|
3072
|
+
return name.replace(/-./g, (x) => x.charAt(1).toUpperCase());
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Validate a widget name for scaffold.
|
|
3076
|
+
* Must be kebab-case, no trailing/consecutive dashes, no bare "widget".
|
|
3077
|
+
*/
|
|
3078
|
+
function validateWidgetName(name) {
|
|
3079
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) return "Widget name must be kebab-case with no trailing or consecutive dashes (e.g., stock-ticker).";
|
|
3080
|
+
if (name === "widget") return "Widget name \"widget\" is reserved. Use a more descriptive name (e.g., custom-widget).";
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Insert an import line after the last import in a source file.
|
|
3085
|
+
* Skips if the import already exists (prevents duplicates on re-scaffold).
|
|
3086
|
+
* Returns null if no imports exist in the source.
|
|
3087
|
+
*/
|
|
3088
|
+
function insertImport(source, importLine) {
|
|
3089
|
+
if (source.includes(importLine)) return source;
|
|
3090
|
+
const lastImportIndex = source.lastIndexOf("import ");
|
|
3091
|
+
if (lastImportIndex === -1) return null;
|
|
3092
|
+
const lineEnd = source.indexOf("\n", lastImportIndex);
|
|
3093
|
+
if (lineEnd === -1) return source + "\n" + importLine + "\n";
|
|
3094
|
+
return source.slice(0, lineEnd + 1) + importLine + "\n" + source.slice(lineEnd + 1);
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Insert a manifest reference into the customWidgets array.
|
|
3098
|
+
* Preserves developer comments. Skips if already present.
|
|
3099
|
+
* Returns null if the array pattern isn't found.
|
|
3100
|
+
*/
|
|
3101
|
+
function insertIntoCustomWidgets(source, camelName) {
|
|
3102
|
+
let matched = false;
|
|
3103
|
+
const result = source.replace(/export const customWidgets:\s*WidgetManifest\[\]\s*=\s*\[([^\]]*)\]/, (_match, inner) => {
|
|
3104
|
+
matched = true;
|
|
3105
|
+
const lines = inner.split("\n");
|
|
3106
|
+
const existingEntries = [];
|
|
3107
|
+
for (const line of lines) {
|
|
3108
|
+
const trimmed = line.trim();
|
|
3109
|
+
if (trimmed && !trimmed.startsWith("//")) existingEntries.push(trimmed.replace(/,$/, ""));
|
|
3110
|
+
}
|
|
3111
|
+
if (existingEntries.includes(camelName)) return _match;
|
|
3112
|
+
const commentLines = lines.filter((line) => line.trim().startsWith("//")).map((line) => line.trimEnd());
|
|
3113
|
+
return `export const customWidgets: WidgetManifest[] = [${commentLines.length > 0 ? "\n" + commentLines.join("\n") : ""}\n${[...existingEntries, camelName].map((e) => ` ${e},`).join("\n")}\n]`;
|
|
3114
|
+
});
|
|
3115
|
+
return matched ? result : null;
|
|
2073
3116
|
}
|
|
2074
3117
|
//#endregion
|
|
3118
|
+
//#region src/commands/widget-create.ts
|
|
3119
|
+
const createSubcommand = new Command("create").description("Scaffold a new custom widget").argument("<name>", "Widget name in kebab-case (e.g., stock-ticker)").option("-c, --category <category>", "Widget category for palette grouping", "components").action(async (name, options) => {
|
|
3120
|
+
const cwd = process.cwd();
|
|
3121
|
+
const widgetDir = path.join(cwd, "src", "widgets", name);
|
|
3122
|
+
const validationError = validateWidgetName(name);
|
|
3123
|
+
if (validationError) {
|
|
3124
|
+
console.error(chalk.red(`Error: ${validationError}`));
|
|
3125
|
+
process.exit(1);
|
|
3126
|
+
}
|
|
3127
|
+
if (!fs.existsSync(path.join(cwd, "src", "portal.config.ts"))) {
|
|
3128
|
+
console.error(chalk.red("Error: No src/portal.config.ts found."));
|
|
3129
|
+
console.error(chalk.yellow("Run this command from a Fluid portal project root."));
|
|
3130
|
+
process.exit(1);
|
|
3131
|
+
}
|
|
3132
|
+
if (fs.existsSync(widgetDir)) {
|
|
3133
|
+
console.error(chalk.red(`Error: Widget directory already exists: src/widgets/${name}`));
|
|
3134
|
+
process.exit(1);
|
|
3135
|
+
}
|
|
3136
|
+
const widgetType = toWidgetType(name);
|
|
3137
|
+
const componentName = toComponentName(widgetType);
|
|
3138
|
+
const displayName = toDisplayName(name);
|
|
3139
|
+
const category = options.category ?? "components";
|
|
3140
|
+
try {
|
|
3141
|
+
await fs.ensureDir(widgetDir);
|
|
3142
|
+
await fs.writeFile(path.join(widgetDir, "component.tsx"), `interface ${widgetType}Props {
|
|
3143
|
+
title?: string;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
export function ${componentName}({ title = "${displayName}" }: ${widgetType}Props) {
|
|
3147
|
+
return (
|
|
3148
|
+
<div className="p-4">
|
|
3149
|
+
<h3 className="text-lg font-semibold">{title}</h3>
|
|
3150
|
+
<p className="text-sm text-gray-500">
|
|
3151
|
+
Edit this widget in src/widgets/${name}/component.tsx
|
|
3152
|
+
</p>
|
|
3153
|
+
</div>
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
`);
|
|
3157
|
+
await fs.writeFile(path.join(widgetDir, "manifest.ts"), `import type { WidgetManifest } from "@fluid-app/portal-core/registries";
|
|
3158
|
+
import { ${componentName} } from "./component";
|
|
3159
|
+
|
|
3160
|
+
export const manifest: WidgetManifest = {
|
|
3161
|
+
manifestVersion: 1,
|
|
3162
|
+
type: "${widgetType}",
|
|
3163
|
+
component: ${componentName},
|
|
3164
|
+
displayName: "${displayName}",
|
|
3165
|
+
description: "A custom ${displayName.toLowerCase()} widget",
|
|
3166
|
+
icon: "puzzle-piece",
|
|
3167
|
+
category: "${category}",
|
|
3168
|
+
propertySchema: {
|
|
3169
|
+
widgetType: "${widgetType}",
|
|
3170
|
+
displayName: "${displayName}",
|
|
3171
|
+
fields: [{ key: "title", label: "Title", type: "text" }],
|
|
3172
|
+
},
|
|
3173
|
+
defaultProps: {
|
|
3174
|
+
title: "${displayName}",
|
|
3175
|
+
},
|
|
3176
|
+
};
|
|
3177
|
+
`);
|
|
3178
|
+
await fs.writeFile(path.join(widgetDir, "index.ts"), `export { ${componentName} } from "./component";
|
|
3179
|
+
export { manifest } from "./manifest";
|
|
3180
|
+
`);
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
await fs.remove(widgetDir).catch(() => {});
|
|
3183
|
+
console.error(chalk.red("Error: Failed to scaffold widget files."));
|
|
3184
|
+
console.error(err);
|
|
3185
|
+
process.exit(1);
|
|
3186
|
+
}
|
|
3187
|
+
const configPath = path.join(cwd, "src", "portal.config.ts");
|
|
3188
|
+
const camelName = toCamelCase(name);
|
|
3189
|
+
const importLine = `import { manifest as ${camelName} } from "./widgets/${name}";`;
|
|
3190
|
+
const configSource = await fs.readFile(configPath, "utf-8");
|
|
3191
|
+
const withImport = insertImport(configSource, importLine);
|
|
3192
|
+
if (withImport === null) {
|
|
3193
|
+
console.warn(chalk.yellow("Warning: Could not find import statements in portal.config.ts. Add the import manually:"));
|
|
3194
|
+
console.warn(chalk.cyan(` ${importLine}`));
|
|
3195
|
+
}
|
|
3196
|
+
const updated = insertIntoCustomWidgets(withImport ?? configSource, camelName);
|
|
3197
|
+
if (updated === null) {
|
|
3198
|
+
console.warn(chalk.yellow("Warning: Could not find customWidgets array in portal.config.ts. Add the manifest manually:"));
|
|
3199
|
+
console.warn(chalk.cyan(` ${camelName}`));
|
|
3200
|
+
}
|
|
3201
|
+
if (updated !== null) await fs.writeFile(configPath, updated, "utf-8");
|
|
3202
|
+
else if (withImport !== null) await fs.writeFile(configPath, withImport, "utf-8");
|
|
3203
|
+
console.log();
|
|
3204
|
+
console.log(chalk.green("Created widget:") + ` src/widgets/${name}/`);
|
|
3205
|
+
console.log(chalk.gray(" component.tsx — React component"));
|
|
3206
|
+
console.log(chalk.gray(" manifest.ts — WidgetManifest"));
|
|
3207
|
+
console.log(chalk.gray(" index.ts — Re-exports"));
|
|
3208
|
+
console.log();
|
|
3209
|
+
if (updated !== null) {
|
|
3210
|
+
console.log(chalk.green("Registered") + ` in ${chalk.cyan("src/portal.config.ts")}`);
|
|
3211
|
+
console.log();
|
|
3212
|
+
}
|
|
3213
|
+
console.log(chalk.yellow("Next steps:"));
|
|
3214
|
+
console.log(` 1. Edit the component in ${chalk.cyan(`src/widgets/${name}/component.tsx`)}`);
|
|
3215
|
+
console.log(` 2. Customize the manifest fields in ${chalk.cyan(`src/widgets/${name}/manifest.ts`)}`);
|
|
3216
|
+
console.log(` 3. Run ${chalk.cyan("fluid dev")} to preview in the builder`);
|
|
3217
|
+
});
|
|
3218
|
+
const widgetCommand = new Command("widget").description("Manage custom portal widgets").addCommand(createSubcommand);
|
|
3219
|
+
//#endregion
|
|
3220
|
+
//#region src/commands/doctor.ts
|
|
3221
|
+
/** Files that are managed by the SDK and should match the canonical template. */
|
|
3222
|
+
const INFRASTRUCTURE_FILES = [
|
|
3223
|
+
"index.html",
|
|
3224
|
+
"tsconfig.json",
|
|
3225
|
+
"vite.config.ts",
|
|
3226
|
+
".oxlintrc.json"
|
|
3227
|
+
];
|
|
3228
|
+
/** Entry files with expected content patterns after Phase 2 absorption. */
|
|
3229
|
+
const ENTRY_CHECKS = [{
|
|
3230
|
+
file: "src/main.tsx",
|
|
3231
|
+
expectedPattern: "createPortal",
|
|
3232
|
+
hint: "main.tsx should use createPortal() from @fluid-app/portal-sdk. Run \"fluid create\" to see the latest template."
|
|
3233
|
+
}, {
|
|
3234
|
+
file: "src/index.css",
|
|
3235
|
+
expectedPattern: "@fluid-app/portal-sdk/globals.css",
|
|
3236
|
+
hint: "index.css should import @fluid-app/portal-sdk/globals.css instead of inline theme tokens."
|
|
3237
|
+
}];
|
|
3238
|
+
/** Files that should NOT exist after Phase 2 (absorbed into SDK). */
|
|
3239
|
+
const REMOVED_FILES = [{
|
|
3240
|
+
file: "src/App.tsx",
|
|
3241
|
+
hint: "App.tsx is no longer needed — createPortal() handles the AppShell wiring internally."
|
|
3242
|
+
}, {
|
|
3243
|
+
file: "src/fluid.config.ts",
|
|
3244
|
+
hint: "fluid.config.ts is no longer needed — pass config overrides to createPortal() directly."
|
|
3245
|
+
}];
|
|
3246
|
+
function readFileOrNull(filePath) {
|
|
3247
|
+
try {
|
|
3248
|
+
return readFileSync(filePath, "utf-8");
|
|
3249
|
+
} catch {
|
|
3250
|
+
return null;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
const _currentDir = dirname(fileURLToPath(import.meta.url));
|
|
3254
|
+
function findTemplateDir() {
|
|
3255
|
+
let dir = _currentDir;
|
|
3256
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
3257
|
+
const parent = dirname(dir);
|
|
3258
|
+
if (parent === dir) break;
|
|
3259
|
+
dir = parent;
|
|
3260
|
+
}
|
|
3261
|
+
const templateDir = join(dir, "templates");
|
|
3262
|
+
if (existsSync(join(templateDir, "base"))) return templateDir;
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
function checkInfrastructureDrift(cwd, templateDir) {
|
|
3266
|
+
const diagnostics = [];
|
|
3267
|
+
for (const file of INFRASTRUCTURE_FILES) {
|
|
3268
|
+
const portalPath = join(cwd, file);
|
|
3269
|
+
const templatePath = join(templateDir, "base", file);
|
|
3270
|
+
if (!existsSync(portalPath)) {
|
|
3271
|
+
diagnostics.push({
|
|
3272
|
+
file,
|
|
3273
|
+
severity: "warn",
|
|
3274
|
+
message: `Missing file: ${file}`
|
|
3275
|
+
});
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
if (!existsSync(templatePath)) continue;
|
|
3279
|
+
const portalContent = readFileOrNull(portalPath);
|
|
3280
|
+
const templateContent = readFileOrNull(templatePath);
|
|
3281
|
+
if (portalContent !== null && templateContent !== null) {
|
|
3282
|
+
if (portalContent.trim() !== templateContent.trim()) diagnostics.push({
|
|
3283
|
+
file,
|
|
3284
|
+
severity: "warn",
|
|
3285
|
+
message: `Content differs from canonical template. Review and update if needed.`
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return diagnostics;
|
|
3290
|
+
}
|
|
3291
|
+
function checkEntryPatterns(cwd) {
|
|
3292
|
+
const diagnostics = [];
|
|
3293
|
+
for (const check of ENTRY_CHECKS) {
|
|
3294
|
+
const content = readFileOrNull(join(cwd, check.file));
|
|
3295
|
+
if (content === null) {
|
|
3296
|
+
diagnostics.push({
|
|
3297
|
+
file: check.file,
|
|
3298
|
+
severity: "error",
|
|
3299
|
+
message: `Missing file. ${check.hint}`
|
|
3300
|
+
});
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
if (!content.includes(check.expectedPattern)) diagnostics.push({
|
|
3304
|
+
file: check.file,
|
|
3305
|
+
severity: "warn",
|
|
3306
|
+
message: `Does not contain expected pattern "${check.expectedPattern}". ${check.hint}`
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
return diagnostics;
|
|
3310
|
+
}
|
|
3311
|
+
function checkRemovedFiles(cwd) {
|
|
3312
|
+
const diagnostics = [];
|
|
3313
|
+
for (const check of REMOVED_FILES) if (existsSync(join(cwd, check.file))) diagnostics.push({
|
|
3314
|
+
file: check.file,
|
|
3315
|
+
severity: "info",
|
|
3316
|
+
message: `File can be removed. ${check.hint}`
|
|
3317
|
+
});
|
|
3318
|
+
return diagnostics;
|
|
3319
|
+
}
|
|
3320
|
+
function formatDiagnostic(d) {
|
|
3321
|
+
return ` ${d.severity === "error" ? chalk.red("ERROR") : d.severity === "warn" ? chalk.yellow(" WARN") : chalk.blue(" INFO")} ${chalk.bold(d.file)}\n ${chalk.dim(d.message)}`;
|
|
3322
|
+
}
|
|
3323
|
+
const doctorCommand = new Command("doctor").description("Check portal for scaffold drift and report stale infrastructure files").action(async () => {
|
|
3324
|
+
const cwd = process.cwd();
|
|
3325
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
3326
|
+
if (!existsSync(packageJsonPath)) {
|
|
3327
|
+
console.error(chalk.red("Error: No package.json found in current directory"));
|
|
3328
|
+
console.error(chalk.yellow("Make sure you're in a Fluid portal project directory"));
|
|
3329
|
+
process.exit(1);
|
|
3330
|
+
}
|
|
3331
|
+
let packageJson;
|
|
3332
|
+
try {
|
|
3333
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
3334
|
+
} catch {
|
|
3335
|
+
console.error(chalk.red("Error: Could not parse package.json"));
|
|
3336
|
+
console.error(chalk.yellow("Ensure package.json contains valid JSON"));
|
|
3337
|
+
process.exit(1);
|
|
3338
|
+
}
|
|
3339
|
+
if (!{
|
|
3340
|
+
...packageJson.dependencies,
|
|
3341
|
+
...packageJson.devDependencies
|
|
3342
|
+
}["@fluid-app/portal-sdk"]) {
|
|
3343
|
+
console.error(chalk.red("Error: @fluid-app/portal-sdk not found in dependencies"));
|
|
3344
|
+
console.error(chalk.yellow("This command must be run from a Fluid portal project directory"));
|
|
3345
|
+
process.exit(1);
|
|
3346
|
+
}
|
|
3347
|
+
console.log();
|
|
3348
|
+
console.log(chalk.bold("Fluid Portal Doctor"));
|
|
3349
|
+
console.log(chalk.dim("Checking for scaffold drift...\n"));
|
|
3350
|
+
const diagnostics = [];
|
|
3351
|
+
diagnostics.push(...checkEntryPatterns(cwd));
|
|
3352
|
+
diagnostics.push(...checkRemovedFiles(cwd));
|
|
3353
|
+
const templateDir = findTemplateDir();
|
|
3354
|
+
if (templateDir) diagnostics.push(...checkInfrastructureDrift(cwd, templateDir));
|
|
3355
|
+
else console.log(chalk.dim(" (Skipping infrastructure diff — template files not available)\n"));
|
|
3356
|
+
if (diagnostics.length === 0) {
|
|
3357
|
+
console.log(chalk.green(" All clear — no drift detected.\n"));
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
3361
|
+
const warns = diagnostics.filter((d) => d.severity === "warn");
|
|
3362
|
+
const infos = diagnostics.filter((d) => d.severity === "info");
|
|
3363
|
+
for (const d of [
|
|
3364
|
+
...errors,
|
|
3365
|
+
...warns,
|
|
3366
|
+
...infos
|
|
3367
|
+
]) {
|
|
3368
|
+
console.log(formatDiagnostic(d));
|
|
3369
|
+
console.log();
|
|
3370
|
+
}
|
|
3371
|
+
const parts = [];
|
|
3372
|
+
if (errors.length > 0) parts.push(chalk.red(`${errors.length} error(s)`));
|
|
3373
|
+
if (warns.length > 0) parts.push(chalk.yellow(`${warns.length} warning(s)`));
|
|
3374
|
+
if (infos.length > 0) parts.push(chalk.blue(`${infos.length} info`));
|
|
3375
|
+
console.log(` ${parts.join(", ")}\n`);
|
|
3376
|
+
if (errors.length > 0) process.exit(1);
|
|
3377
|
+
});
|
|
3378
|
+
//#endregion
|
|
3379
|
+
//#region src/commands/version.ts
|
|
3380
|
+
const PORTAL_SYNC_DIR = ".portal-sync";
|
|
3381
|
+
function getApiBase() {
|
|
3382
|
+
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
3383
|
+
}
|
|
3384
|
+
function requireToken() {
|
|
3385
|
+
const token = getAuthToken();
|
|
3386
|
+
if (!token) {
|
|
3387
|
+
console.error(chalk.red("Error:") + " Not logged in. Run " + chalk.cyan("`fluid login`") + " first.");
|
|
3388
|
+
process.exit(1);
|
|
3389
|
+
}
|
|
3390
|
+
return token;
|
|
3391
|
+
}
|
|
3392
|
+
function createClient(token) {
|
|
3393
|
+
return createFetchClient({
|
|
3394
|
+
baseUrl: getApiBase(),
|
|
3395
|
+
getAuthToken: () => token
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
async function requireDefinitionId() {
|
|
3399
|
+
const cwd = process.cwd();
|
|
3400
|
+
const mappings = await readMappings(path.join(cwd, PORTAL_SYNC_DIR));
|
|
3401
|
+
if (!mappings) {
|
|
3402
|
+
console.error(chalk.red("Error:") + " No definition pulled. Run " + chalk.cyan("`fluid portal pull`") + " first.");
|
|
3403
|
+
process.exit(1);
|
|
3404
|
+
}
|
|
3405
|
+
return mappings.definition.id;
|
|
3406
|
+
}
|
|
3407
|
+
const createVersionCommand = new Command("create").description("Create a new version (snapshot) of the portal definition").option("--activate", "Activate the version immediately after creation").action(async (options) => {
|
|
3408
|
+
const client = createClient(requireToken());
|
|
3409
|
+
const definitionId = await requireDefinitionId();
|
|
3410
|
+
console.log();
|
|
3411
|
+
console.log(chalk.bold("Creating version..."));
|
|
3412
|
+
let result;
|
|
3413
|
+
try {
|
|
3414
|
+
result = await createFluidOSVersion(client, definitionId);
|
|
3415
|
+
} catch (err) {
|
|
3416
|
+
console.error(chalk.red("Error:") + " Failed to create version — " + (err instanceof Error ? err.message : String(err)));
|
|
3417
|
+
process.exit(1);
|
|
3418
|
+
}
|
|
3419
|
+
const version = result.version;
|
|
3420
|
+
if (!version?.id) {
|
|
3421
|
+
console.error(chalk.red("Error:") + " Failed to create version — unexpected response.");
|
|
3422
|
+
process.exit(1);
|
|
3423
|
+
}
|
|
3424
|
+
console.log();
|
|
3425
|
+
console.log(chalk.green("Version created successfully."));
|
|
3426
|
+
console.log();
|
|
3427
|
+
console.log(chalk.gray("Version ID: ") + chalk.white(version.id));
|
|
3428
|
+
let active = version.active ?? false;
|
|
3429
|
+
if (options.activate) {
|
|
3430
|
+
console.log("Activating version...");
|
|
3431
|
+
try {
|
|
3432
|
+
await updateFluidOSVersion(client, definitionId, version.id, { version: { active: true } });
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
|
|
3435
|
+
process.exit(1);
|
|
3436
|
+
}
|
|
3437
|
+
active = true;
|
|
3438
|
+
}
|
|
3439
|
+
console.log(chalk.gray("Active: ") + (active ? chalk.green("yes") : chalk.gray("no")));
|
|
3440
|
+
console.log();
|
|
3441
|
+
});
|
|
3442
|
+
const listVersionCommand = new Command("list").description("List all versions of the portal definition").action(async () => {
|
|
3443
|
+
const client = createClient(requireToken());
|
|
3444
|
+
const definitionId = await requireDefinitionId();
|
|
3445
|
+
let result;
|
|
3446
|
+
try {
|
|
3447
|
+
result = await listFluidOSVersions(client, definitionId);
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
console.error(chalk.red("Error:") + " Failed to list versions — " + (err instanceof Error ? err.message : String(err)));
|
|
3450
|
+
process.exit(1);
|
|
3451
|
+
}
|
|
3452
|
+
const versions = result.version;
|
|
3453
|
+
if (!Array.isArray(versions) || versions.length === 0) {
|
|
3454
|
+
console.log();
|
|
3455
|
+
console.log(chalk.yellow("No versions found."));
|
|
3456
|
+
console.log("Run " + chalk.cyan("`fluid portal version create`") + " to publish a version.");
|
|
3457
|
+
console.log();
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
console.log();
|
|
3461
|
+
const COL_ID = "Version ID".padEnd(36);
|
|
3462
|
+
const COL_ACT = "Active".padEnd(6);
|
|
3463
|
+
console.log(chalk.gray(` ${COL_ID} ${COL_ACT} Published`));
|
|
3464
|
+
for (const v of versions) {
|
|
3465
|
+
const id = String(v.id).padEnd(36);
|
|
3466
|
+
const active = v.active ? chalk.green("✓") + " " : " ";
|
|
3467
|
+
const published = v.published_at ? new Date(v.published_at).toLocaleString() : "—";
|
|
3468
|
+
console.log(` ${id} ${active} ${published}`);
|
|
3469
|
+
}
|
|
3470
|
+
console.log();
|
|
3471
|
+
});
|
|
3472
|
+
const activateVersionCommand = new Command("activate").description("Activate a specific version, making it the live version").argument("<version-id>", "The version ID to activate").option("-y, --yes", "Skip confirmation prompt").action(async (versionId, options) => {
|
|
3473
|
+
const client = createClient(requireToken());
|
|
3474
|
+
const definitionId = await requireDefinitionId();
|
|
3475
|
+
if (!options.yes) {
|
|
3476
|
+
const { confirm } = await prompts({
|
|
3477
|
+
type: "confirm",
|
|
3478
|
+
name: "confirm",
|
|
3479
|
+
message: `Activate version ${versionId}? This will make it the live version.`,
|
|
3480
|
+
initial: false
|
|
3481
|
+
});
|
|
3482
|
+
if (!confirm) {
|
|
3483
|
+
console.log(chalk.yellow("Aborted."));
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
console.log();
|
|
3488
|
+
console.log(chalk.bold("Activating version..."));
|
|
3489
|
+
try {
|
|
3490
|
+
await updateFluidOSVersion(client, definitionId, versionId, { version: { active: true } });
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
|
|
3493
|
+
process.exit(1);
|
|
3494
|
+
}
|
|
3495
|
+
console.log();
|
|
3496
|
+
console.log(chalk.green("Version " + versionId + " is now active."));
|
|
3497
|
+
console.log();
|
|
3498
|
+
});
|
|
3499
|
+
const versionCommand = new Command("version").description("Manage portal definition versions").addCommand(createVersionCommand).addCommand(listVersionCommand).addCommand(activateVersionCommand);
|
|
3500
|
+
//#endregion
|
|
2075
3501
|
//#region src/index.ts
|
|
3502
|
+
/**
|
|
3503
|
+
* @fluid-app/fluid-cli-portal
|
|
3504
|
+
*
|
|
3505
|
+
* Fluid CLI plugin for building and deploying portal applications.
|
|
3506
|
+
* Auto-discovered by @fluid-app/fluid-cli via the fluid-cli-* naming convention.
|
|
3507
|
+
*/
|
|
2076
3508
|
const plugin = {
|
|
2077
3509
|
name: "fluid-cli-portal",
|
|
2078
3510
|
version: "0.1.0",
|
|
2079
3511
|
async register(ctx) {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
3512
|
+
const portal = new Command("portal").description("Build, develop, and deploy portal applications");
|
|
3513
|
+
portal.addCommand(createCommand);
|
|
3514
|
+
portal.addCommand(devCommand);
|
|
3515
|
+
portal.addCommand(buildCommand);
|
|
3516
|
+
portal.addCommand(deployCommand);
|
|
3517
|
+
portal.addCommand(destroyCommand);
|
|
3518
|
+
portal.addCommand(pullCommand);
|
|
3519
|
+
portal.addCommand(pushCommand);
|
|
3520
|
+
portal.addCommand(widgetCommand);
|
|
3521
|
+
portal.addCommand(doctorCommand);
|
|
3522
|
+
portal.addCommand(versionCommand);
|
|
3523
|
+
ctx.program.addCommand(portal);
|
|
2085
3524
|
}
|
|
2086
3525
|
};
|
|
2087
3526
|
//#endregion
|
|
2088
|
-
export { CLOUD_RUN_ERRORS, FILE_SYSTEM_ERRORS, FLUID_API_ERROR, TEMPLATES, TURSO_ERROR, copyTemplate, copyTemplateSafe, createCommand, createDatabase, createDatabaseToken, createDirectory, createDirectorySafe, plugin as default, deleteCloudRunService, deleteDatabase, deployToCloudRun, destroyCommand, directoryExists, ensureGroup, fetchLocations, fileExists, getGcpProject, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, installDependencies, isTemplateName, parseOrgList, pathExists, promptProjectConfig, provisionDatabase, readFileSafe, resolveFluidApiKey, resolveTursoConfig, runPackageManager, validateFluidApiKey, validateGcloudAuth, validateGcloudInstalled, validateLocation, validateTursoConfig, writeFileSafe };
|
|
3527
|
+
export { CLOUD_RUN_ERRORS, FILE_SYSTEM_ERRORS, FLUID_API_ERROR, TEMPLATES, TURSO_ERROR, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDatabase, createDatabaseToken, createDirectory, createDirectorySafe, plugin as default, deleteCloudRunService, deleteDatabase, deployToCloudRun, deriveScreenSlug, deriveSlug, destroyCommand, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGroup, fetchLocations, fileExists, getGcpProject, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, installDependencies, isTemplateName, parseOrgList, pathExists, promptProjectConfig, provisionDatabase, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveFluidApiKey, resolveIdToSlug, resolveSlugToId, resolveTursoConfig, runPackageManager, slugFromPath, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, validateFluidApiKey, validateGcloudAuth, validateGcloudInstalled, validateLocation, validateTursoConfig, versionCommand, widgetCommand, writeFileSafe, writeMappings, writeSnapshot };
|
|
2089
3528
|
|
|
2090
3529
|
//# sourceMappingURL=index.mjs.map
|