@fluid-app/fluid-cli-portal 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -8,25 +8,14 @@ import prompts from "prompts";
8
8
  import { existsSync, readFileSync, readdirSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import Handlebars from "handlebars";
11
- import { failure, getActiveProfile, getAuthToken, getErrorMessage, success } from "@fluid-app/fluid-cli";
11
+ import { failure, getActiveProfile, getAuthToken, success } from "@fluid-app/fluid-cli";
12
12
  import { execa } from "execa";
13
13
  import fs from "fs-extra";
14
- import path$1 from "path";
15
- import { config } from "dotenv";
16
14
  //#region src/types.ts
17
15
  /**
18
16
  * Available project templates
19
17
  */
20
- const TEMPLATES = {
21
- starter: "starter",
22
- fullstack: "fullstack"
23
- };
24
- /**
25
- * Type guard to check if a string is a valid template name
26
- */
27
- function isTemplateName(value) {
28
- return Object.values(TEMPLATES).includes(value);
29
- }
18
+ const TEMPLATES = { starter: "starter" };
30
19
  //#endregion
31
20
  //#region src/utils/prompts.ts
32
21
  /**
@@ -40,20 +29,6 @@ const OPTIONAL_PAGE_TEMPLATES = [];
40
29
  */
41
30
  async function promptProjectConfig(projectName, options) {
42
31
  const questions = [];
43
- if (!options.template) questions.push({
44
- type: "select",
45
- name: "template",
46
- message: "Select a project template",
47
- choices: [{
48
- title: "Starter",
49
- value: TEMPLATES.starter,
50
- description: "Frontend-only (React + Vite + Tailwind + portal-sdk)"
51
- }, {
52
- title: "Fullstack",
53
- value: TEMPLATES.fullstack,
54
- description: "Frontend + API server (Hono + Drizzle + SQLite)"
55
- }]
56
- });
57
32
  if (OPTIONAL_PAGE_TEMPLATES.length > 0) questions.push({
58
33
  type: "multiselect",
59
34
  name: "selectedPages",
@@ -75,36 +50,25 @@ async function promptProjectConfig(projectName, options) {
75
50
  message: "Install dependencies?",
76
51
  initial: true
77
52
  });
78
- if (!process.stdin.isTTY && questions.length > 0) {
79
- const templateRaw = options.template;
80
- return {
81
- name: projectName,
82
- template: isTemplateName(templateRaw ?? "") ? templateRaw : TEMPLATES.starter,
83
- installDeps: false,
84
- selectedPages: []
85
- };
86
- }
87
- if (questions.length === 0) {
88
- const templateRaw = options.template;
89
- return {
90
- name: projectName,
91
- template: isTemplateName(templateRaw ?? "") ? templateRaw : TEMPLATES.starter,
92
- installDeps: options.skipInstall ? false : true,
93
- selectedPages: []
94
- };
95
- }
53
+ if (!process.stdin.isTTY && questions.length > 0) return {
54
+ name: projectName,
55
+ installDeps: false,
56
+ selectedPages: []
57
+ };
58
+ if (questions.length === 0) return {
59
+ name: projectName,
60
+ installDeps: options.skipInstall ? false : true,
61
+ selectedPages: []
62
+ };
96
63
  let cancelled = false;
97
64
  const response = await prompts(questions, { onCancel: () => {
98
65
  cancelled = true;
99
66
  return false;
100
67
  } });
101
68
  if (cancelled) return null;
102
- const templateRaw = options.template ?? response.template;
103
- const template = isTemplateName(templateRaw) ? templateRaw : TEMPLATES.starter;
104
69
  const selectedPages = response.selectedPages ?? [];
105
70
  return {
106
71
  name: projectName,
107
- template,
108
72
  installDeps: options.skipInstall ? false : response.installDeps ?? true,
109
73
  selectedPages
110
74
  };
@@ -358,7 +322,7 @@ async function installDependencies(cwd) {
358
322
  }
359
323
  //#endregion
360
324
  //#region src/commands/create.ts
361
- const createCommand = new Command("create").description("Create a new Fluid portal application").argument("<app-name>", "Name of the application to create").option("-t, --template <template>", "Project template (starter, fullstack)").option("--skip-install", "Skip dependency installation").option("-o, --output-dir <dir>", "Directory to create the project in (defaults to cwd)").action(async (appName, options) => {
325
+ 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)").action(async (appName, options) => {
362
326
  try {
363
327
  console.log();
364
328
  console.log(chalk.bold("Creating a new Fluid portal application"));
@@ -379,13 +343,13 @@ const createCommand = new Command("create").description("Create a new Fluid port
379
343
  process.exit(0);
380
344
  }
381
345
  console.log();
382
- const templatePaths = getTemplatePaths(config.template);
346
+ const templatePaths = getTemplatePaths("starter");
383
347
  if (!await directoryExists(templatePaths.base)) {
384
348
  console.error(chalk.red("Error: Base template not found"));
385
349
  process.exit(1);
386
350
  }
387
351
  if (!await directoryExists(templatePaths.overlay)) {
388
- console.error(chalk.red(`Error: Template "${config.template}" not found`));
352
+ console.error(chalk.red("Error: Starter template not found"));
389
353
  process.exit(1);
390
354
  }
391
355
  const sdkVersion = await getSdkVersion();
@@ -435,11 +399,9 @@ const createCommand = new Command("create").description("Create a new Fluid port
435
399
  const cdPath = options.outputDir ? targetPath : appName;
436
400
  console.log(chalk.cyan(` cd ${cdPath}`));
437
401
  if (!config.installDeps) console.log(chalk.cyan(" pnpm install"));
438
- if (config.template === TEMPLATES.fullstack) console.log(chalk.cyan(` ${getRunCommand("db:push")}`) + " # create the database");
439
402
  console.log(chalk.cyan(` ${getRunCommand("dev")}`));
440
403
  console.log();
441
404
  console.log("Then open " + chalk.cyan("http://localhost:5173") + " in your browser.");
442
- if (config.template === TEMPLATES.fullstack) console.log("API server: " + chalk.cyan("http://localhost:5173/api/health"));
443
405
  console.log(chalk.dim(" (port may differ if 5173 is in use — check the dev server output)"));
444
406
  console.log();
445
407
  console.log("Edit " + chalk.cyan("src/portal.config.ts") + " to customize your navigation.");
@@ -572,1544 +534,6 @@ const buildCommand = new Command("build").description("Build the application for
572
534
  }
573
535
  });
574
536
  //#endregion
575
- //#region src/utils/turso.ts
576
- const TURSO_API_BASE = "https://api.turso.tech/v1";
577
- const TURSO_ERROR = {
578
- MISSING_TOKEN: {
579
- code: "MISSING_TOKEN",
580
- message: "TURSO_API_TOKEN environment variable is not set"
581
- },
582
- MISSING_ORG: {
583
- code: "MISSING_ORG",
584
- message: "TURSO_ORG environment variable is not set"
585
- },
586
- GROUP_CREATION_FAILED: {
587
- code: "GROUP_CREATION_FAILED",
588
- message: "Failed to create database group"
589
- },
590
- DATABASE_CREATION_FAILED: {
591
- code: "DATABASE_CREATION_FAILED",
592
- message: "Failed to create database"
593
- },
594
- TOKEN_CREATION_FAILED: {
595
- code: "TOKEN_CREATION_FAILED",
596
- message: "Failed to create database auth token"
597
- },
598
- DATABASE_DELETION_FAILED: {
599
- code: "DATABASE_DELETION_FAILED",
600
- message: "Failed to delete database"
601
- },
602
- INVALID_LOCATION: {
603
- code: "INVALID_LOCATION",
604
- message: "Invalid database location"
605
- },
606
- LOCATIONS_FETCH_FAILED: {
607
- code: "LOCATIONS_FETCH_FAILED",
608
- message: "Failed to fetch available Turso locations"
609
- },
610
- TURSO_CLI_NOT_FOUND: {
611
- code: "TURSO_CLI_NOT_FOUND",
612
- message: "Turso CLI is not installed"
613
- },
614
- TURSO_CLI_NOT_AUTHENTICATED: {
615
- code: "TURSO_CLI_NOT_AUTHENTICATED",
616
- message: "Turso CLI is not authenticated"
617
- },
618
- TURSO_NO_ORGS: {
619
- code: "TURSO_NO_ORGS",
620
- message: "No organizations found in Turso CLI"
621
- }
622
- };
623
- /**
624
- * Create a Turso error from a constant and optional details
625
- */
626
- function createTursoError(template, details) {
627
- return {
628
- code: template.code,
629
- message: template.message,
630
- details
631
- };
632
- }
633
- /**
634
- * Validate that required Turso environment variables are present
635
- */
636
- function validateTursoConfig() {
637
- const apiToken = process.env.TURSO_API_TOKEN;
638
- if (!apiToken) return failure(createTursoError(TURSO_ERROR.MISSING_TOKEN));
639
- const org = process.env.TURSO_ORG;
640
- if (!org) return failure(createTursoError(TURSO_ERROR.MISSING_ORG));
641
- return success({
642
- apiToken,
643
- org
644
- });
645
- }
646
- /**
647
- * Check if the Turso CLI is installed and authenticated.
648
- * Runs `turso auth whoami` which verifies both in a single call.
649
- */
650
- async function isTursoCliAvailable() {
651
- try {
652
- await execa("turso", ["auth", "whoami"], { stdio: "pipe" });
653
- return true;
654
- } catch {
655
- return false;
656
- }
657
- }
658
- /**
659
- * Get the Turso platform API token from the CLI.
660
- * Runs `turso auth token` which outputs the token to stdout.
661
- */
662
- async function getTursoCliToken() {
663
- try {
664
- const { stdout } = await execa("turso", ["auth", "token"], { stdio: "pipe" });
665
- const token = stdout.trim().split("\n").filter(Boolean).pop()?.trim() ?? "";
666
- if (!token) return failure(createTursoError(TURSO_ERROR.TURSO_CLI_NOT_AUTHENTICATED, "turso auth token returned an empty value. Run: turso auth login"));
667
- return success(token);
668
- } catch {
669
- return failure(createTursoError(TURSO_ERROR.TURSO_CLI_NOT_AUTHENTICATED, "Failed to get token from Turso CLI. Run: turso auth login"));
670
- }
671
- }
672
- /**
673
- * Parse the tabular output of `turso org list`.
674
- *
675
- * Example input:
676
- * Name Slug Type
677
- * My Org my-org personal (current)
678
- * Team Org team-org team
679
- *
680
- * Exported for unit testing.
681
- */
682
- function parseOrgList(stdout) {
683
- const lines = stdout.trim().split("\n");
684
- if (lines.length <= 1) return [];
685
- return lines.slice(1).reduce((orgs, line) => {
686
- const trimmed = line.trim();
687
- if (!trimmed) return orgs;
688
- const columns = trimmed.split(/\s{2,}/);
689
- if (columns.length < 2) return orgs;
690
- const name = columns[0].trim();
691
- const slug = columns[1].trim().replace(/\s*\(current\)/, "");
692
- const isCurrent = trimmed.includes("(current)");
693
- if (name && slug) orgs.push({
694
- name,
695
- slug,
696
- isCurrent
697
- });
698
- return orgs;
699
- }, []);
700
- }
701
- /**
702
- * Get the list of Turso organizations from the CLI.
703
- */
704
- async function getTursoCliOrgs() {
705
- try {
706
- const { stdout } = await execa("turso", ["org", "list"], { stdio: "pipe" });
707
- const orgs = parseOrgList(stdout);
708
- if (orgs.length === 0) return failure(createTursoError(TURSO_ERROR.TURSO_NO_ORGS, "No organizations found. Create one at https://turso.tech"));
709
- return success(orgs);
710
- } catch {
711
- return failure(createTursoError(TURSO_ERROR.TURSO_NO_ORGS, "Failed to list organizations from Turso CLI."));
712
- }
713
- }
714
- const TURSO_AUTH_HELP = [
715
- "Option 1 — Turso CLI (recommended for local dev):",
716
- " curl -sSfL https://get.tur.so/install.sh | bash",
717
- " turso auth login",
718
- "",
719
- "Option 2 — Environment variables (CI/CD):",
720
- " export TURSO_API_TOKEN=your_token_here",
721
- " export TURSO_ORG=your_org_name",
722
- "",
723
- "Get your API token at: https://turso.tech/app/settings/api-tokens"
724
- ].join("\n");
725
- /**
726
- * Resolve Turso credentials from the best available source.
727
- *
728
- * Priority:
729
- * 1. Environment variables (TURSO_API_TOKEN + TURSO_ORG) — immediate, for CI/CD
730
- * 2. Turso CLI (turso auth token + turso org list) — interactive, for local dev
731
- * 3. Fail with instructions for both options
732
- *
733
- * @param tursoOrgOverride - Optional org slug from --turso-org flag (skips interactive selection)
734
- */
735
- async function resolveTursoConfig(tursoOrgOverride) {
736
- const envToken = process.env.TURSO_API_TOKEN;
737
- const envOrg = process.env.TURSO_ORG;
738
- if (envToken && envOrg) return success({
739
- apiToken: envToken,
740
- org: envOrg,
741
- source: "env"
742
- });
743
- if (!await isTursoCliAvailable()) return failure(createTursoError(TURSO_ERROR.TURSO_CLI_NOT_FOUND, `No Turso credentials found.\n\n${TURSO_AUTH_HELP}`));
744
- const tokenResult = await getTursoCliToken();
745
- if (!tokenResult.success) return tokenResult;
746
- const apiToken = tokenResult.value;
747
- if (tursoOrgOverride) return success({
748
- apiToken,
749
- org: tursoOrgOverride,
750
- source: "cli"
751
- });
752
- const orgsResult = await getTursoCliOrgs();
753
- if (!orgsResult.success) return orgsResult;
754
- const orgs = orgsResult.value;
755
- if (orgs.length === 1) return success({
756
- apiToken,
757
- org: orgs[0].slug,
758
- source: "cli"
759
- });
760
- const fluidOrg = orgs.find((o) => o.slug === "fluid");
761
- if (fluidOrg) return success({
762
- apiToken,
763
- org: fluidOrg.slug,
764
- source: "cli"
765
- });
766
- const currentOrg = orgs.find((o) => o.isCurrent);
767
- if (currentOrg) return success({
768
- apiToken,
769
- org: currentOrg.slug,
770
- source: "cli"
771
- });
772
- const { orgSlug } = await prompts({
773
- type: "select",
774
- name: "orgSlug",
775
- message: "Which Turso organization?",
776
- choices: orgs.map((o) => ({
777
- title: `${o.name} (${o.slug})`,
778
- value: o.slug
779
- }))
780
- });
781
- if (!orgSlug) return failure(createTursoError(TURSO_ERROR.TURSO_NO_ORGS, "Organization selection cancelled."));
782
- return success({
783
- apiToken,
784
- org: orgSlug,
785
- source: "cli"
786
- });
787
- }
788
- /**
789
- * Build standard headers for Turso API requests
790
- */
791
- function buildHeaders(apiToken) {
792
- return {
793
- Authorization: `Bearer ${apiToken}`,
794
- "Content-Type": "application/json"
795
- };
796
- }
797
- /**
798
- * Fetch available Turso database locations.
799
- * Returns a map of location ID → description (e.g., "aws-us-east-1" → "US East (N. Virginia)").
800
- */
801
- async function fetchLocations(config) {
802
- try {
803
- const controller = new AbortController();
804
- const timeout = setTimeout(() => controller.abort(), 3e4);
805
- const response = await fetch(`${TURSO_API_BASE}/locations`, {
806
- method: "GET",
807
- headers: buildHeaders(config.apiToken),
808
- signal: controller.signal
809
- });
810
- clearTimeout(timeout);
811
- if (!response.ok) {
812
- const body = await response.text();
813
- return failure(createTursoError(TURSO_ERROR.LOCATIONS_FETCH_FAILED, `HTTP ${response.status}: ${body}`));
814
- }
815
- return success((await response.json()).locations);
816
- } catch (err) {
817
- const message = err instanceof Error ? err.message : String(err);
818
- return failure(createTursoError(TURSO_ERROR.LOCATIONS_FETCH_FAILED, message));
819
- }
820
- }
821
- /**
822
- * Validate that a location string is a known Turso location.
823
- * On failure, returns an error listing all valid locations.
824
- */
825
- async function validateLocation(config, location) {
826
- const locationsResult = await fetchLocations(config);
827
- if (!locationsResult.success) return locationsResult;
828
- const locations = locationsResult.value;
829
- if (location in locations) return success(void 0);
830
- const validLocations = Object.entries(locations).map(([id, desc]) => ` ${id} — ${desc}`).join("\n");
831
- return failure(createTursoError(TURSO_ERROR.INVALID_LOCATION, `"${location}" is not a valid Turso location.\n\nAvailable locations:\n${validLocations}`));
832
- }
833
- /**
834
- * Ensure a default database group exists in the Turso organization.
835
- * Creates the group if it does not exist; treats 409 (conflict) as success
836
- * since it means the group already exists.
837
- */
838
- async function ensureGroup(config, location = "aws-us-east-1") {
839
- try {
840
- const listController = new AbortController();
841
- const listTimeout = setTimeout(() => listController.abort(), 3e4);
842
- const listResponse = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/groups`, {
843
- method: "GET",
844
- headers: buildHeaders(config.apiToken),
845
- signal: listController.signal
846
- });
847
- clearTimeout(listTimeout);
848
- if (listResponse.ok) {
849
- if ((await listResponse.json()).groups.some((g) => g.name === "default")) return success(void 0);
850
- }
851
- const createController = new AbortController();
852
- const createTimeout = setTimeout(() => createController.abort(), 3e4);
853
- const createResponse = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/groups`, {
854
- method: "POST",
855
- headers: buildHeaders(config.apiToken),
856
- body: JSON.stringify({
857
- name: "default",
858
- location
859
- }),
860
- signal: createController.signal
861
- });
862
- clearTimeout(createTimeout);
863
- if (createResponse.ok || createResponse.status === 409) return success(void 0);
864
- const body = await createResponse.text();
865
- return failure(createTursoError(TURSO_ERROR.GROUP_CREATION_FAILED, `HTTP ${createResponse.status}: ${body}`));
866
- } catch (err) {
867
- const message = err instanceof Error ? err.message : String(err);
868
- return failure(createTursoError(TURSO_ERROR.GROUP_CREATION_FAILED, message));
869
- }
870
- }
871
- /**
872
- * Create a new Turso database in the default group.
873
- * If the database already exists (409), fetches its info via GET instead.
874
- * Returns the database name and hostname.
875
- */
876
- async function createDatabase(config, name) {
877
- try {
878
- const controller = new AbortController();
879
- const timeout = setTimeout(() => controller.abort(), 3e4);
880
- const response = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/databases`, {
881
- method: "POST",
882
- headers: buildHeaders(config.apiToken),
883
- body: JSON.stringify({
884
- name,
885
- group: "default"
886
- }),
887
- signal: controller.signal
888
- });
889
- clearTimeout(timeout);
890
- if (response.ok) {
891
- const data = await response.json();
892
- if (!data.database) return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, "Unexpected API response: missing database object"));
893
- return success({
894
- name: data.database.Name ?? data.database.name ?? name,
895
- hostname: data.database.Hostname ?? data.database.hostname ?? "",
896
- isNew: true
897
- });
898
- }
899
- if (response.status === 409) {
900
- const existing = await getDatabaseInfo(config, name);
901
- if (!existing.success) return existing;
902
- return success({
903
- ...existing.value,
904
- isNew: false
905
- });
906
- }
907
- const body = await response.text();
908
- return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, `HTTP ${response.status}: ${body}`));
909
- } catch (err) {
910
- const message = err instanceof Error ? err.message : String(err);
911
- return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, message));
912
- }
913
- }
914
- /**
915
- * Fetch existing database info by name
916
- */
917
- async function getDatabaseInfo(config, name) {
918
- try {
919
- const controller = new AbortController();
920
- const timeout = setTimeout(() => controller.abort(), 3e4);
921
- const response = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/databases/${name}`, {
922
- method: "GET",
923
- headers: buildHeaders(config.apiToken),
924
- signal: controller.signal
925
- });
926
- clearTimeout(timeout);
927
- if (!response.ok) {
928
- const body = await response.text();
929
- return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, `Failed to fetch existing database info: HTTP ${response.status}: ${body}`));
930
- }
931
- const data = await response.json();
932
- if (!data.database) return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, "Unexpected API response: missing database object"));
933
- return success({
934
- name: data.database.Name ?? data.database.name ?? name,
935
- hostname: data.database.Hostname ?? data.database.hostname ?? ""
936
- });
937
- } catch (err) {
938
- const message = err instanceof Error ? err.message : String(err);
939
- return failure(createTursoError(TURSO_ERROR.DATABASE_CREATION_FAILED, `Failed to fetch existing database info: ${message}`));
940
- }
941
- }
942
- /**
943
- * Delete a Turso database by name.
944
- * Returns void on success, or a TursoError on failure.
945
- */
946
- async function deleteDatabase(config, name) {
947
- try {
948
- const controller = new AbortController();
949
- const timeout = setTimeout(() => controller.abort(), 3e4);
950
- const response = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/databases/${name}`, {
951
- method: "DELETE",
952
- headers: buildHeaders(config.apiToken),
953
- signal: controller.signal
954
- });
955
- clearTimeout(timeout);
956
- if (response.ok || response.status === 404) return success(void 0);
957
- const body = await response.text();
958
- return failure(createTursoError(TURSO_ERROR.DATABASE_DELETION_FAILED, `HTTP ${response.status}: ${body}`));
959
- } catch (err) {
960
- const message = err instanceof Error ? err.message : String(err);
961
- return failure(createTursoError(TURSO_ERROR.DATABASE_DELETION_FAILED, message));
962
- }
963
- }
964
- /**
965
- * Create an auth token for a Turso database.
966
- * Returns the JWT token string used for database connections.
967
- */
968
- async function createDatabaseToken(config, dbName) {
969
- try {
970
- const controller = new AbortController();
971
- const timeout = setTimeout(() => controller.abort(), 3e4);
972
- const response = await fetch(`${TURSO_API_BASE}/organizations/${config.org}/databases/${dbName}/auth/tokens`, {
973
- method: "POST",
974
- headers: buildHeaders(config.apiToken),
975
- signal: controller.signal
976
- });
977
- clearTimeout(timeout);
978
- if (!response.ok) {
979
- const body = await response.text();
980
- return failure(createTursoError(TURSO_ERROR.TOKEN_CREATION_FAILED, `HTTP ${response.status}: ${body}`));
981
- }
982
- const data = await response.json();
983
- if (!data.jwt) return failure(createTursoError(TURSO_ERROR.TOKEN_CREATION_FAILED, "Unexpected API response: missing jwt field"));
984
- return success(data.jwt);
985
- } catch (err) {
986
- const message = err instanceof Error ? err.message : String(err);
987
- return failure(createTursoError(TURSO_ERROR.TOKEN_CREATION_FAILED, message));
988
- }
989
- }
990
- /**
991
- * Provision a complete Turso database for a project.
992
- *
993
- * Orchestrates the full flow:
994
- * 1. Ensure a default group exists
995
- * 2. Create (or retrieve) the database
996
- * 3. Generate an auth token
997
- *
998
- * Calls progress callbacks at each step so callers can display status.
999
- */
1000
- async function provisionDatabase(config, projectName, location, callbacks) {
1001
- if (location) {
1002
- const locationResult = await validateLocation(config, location);
1003
- if (!locationResult.success) return locationResult;
1004
- }
1005
- callbacks?.onGroupCreating?.();
1006
- const groupResult = await ensureGroup(config, location);
1007
- if (!groupResult.success) return groupResult;
1008
- callbacks?.onGroupReady?.();
1009
- callbacks?.onDatabaseCreating?.(projectName);
1010
- const dbResult = await createDatabase(config, projectName);
1011
- if (!dbResult.success) return dbResult;
1012
- const { name: databaseName, hostname, isNew } = dbResult.value;
1013
- callbacks?.onDatabaseReady?.(databaseName);
1014
- callbacks?.onTokenCreating?.();
1015
- const tokenResult = await createDatabaseToken(config, databaseName);
1016
- if (!tokenResult.success) return tokenResult;
1017
- const authToken = tokenResult.value;
1018
- callbacks?.onTokenReady?.();
1019
- return success({
1020
- url: `libsql://${hostname}`,
1021
- authToken,
1022
- databaseName,
1023
- hostname,
1024
- isNew
1025
- });
1026
- }
1027
- //#endregion
1028
- //#region src/utils/cloud-run.ts
1029
- /**
1030
- * Cloud Run error codes and default messages
1031
- */
1032
- const CLOUD_RUN_ERRORS = {
1033
- GCLOUD_NOT_INSTALLED: {
1034
- code: "GCLOUD_NOT_INSTALLED",
1035
- message: "gcloud CLI is not installed. Install it from https://cloud.google.com/sdk/docs/install"
1036
- },
1037
- GCLOUD_NOT_AUTHENTICATED: {
1038
- code: "GCLOUD_NOT_AUTHENTICATED",
1039
- message: "gcloud CLI is not authenticated. Run 'gcloud auth login' to authenticate"
1040
- },
1041
- NO_GCP_PROJECT: {
1042
- code: "NO_GCP_PROJECT",
1043
- message: "No GCP project configured. Run 'gcloud config set project PROJECT_ID' or pass --gcp-project"
1044
- },
1045
- DEPLOY_FAILED: {
1046
- code: "DEPLOY_FAILED",
1047
- message: "Cloud Run deployment failed"
1048
- },
1049
- SERVICE_DELETION_FAILED: {
1050
- code: "SERVICE_DELETION_FAILED",
1051
- message: "Cloud Run service deletion failed"
1052
- }
1053
- };
1054
- /**
1055
- * Create a CloudRunError from a constant and optional details
1056
- */
1057
- function createCloudRunError(template, details) {
1058
- return {
1059
- code: template.code,
1060
- message: template.message,
1061
- details
1062
- };
1063
- }
1064
- /**
1065
- * Build a KEY=VALUE string from an env vars record using gcloud's custom
1066
- * delimiter syntax to avoid issues with values containing commas.
1067
- * Prefix with `^::^` and join entries with `::` instead of `,`.
1068
- */
1069
- function buildEnvVarsString(envVars) {
1070
- return `^::^${Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("::")}`;
1071
- }
1072
- /**
1073
- * Validate that the gcloud CLI is installed
1074
- */
1075
- async function validateGcloudInstalled() {
1076
- try {
1077
- await execa("gcloud", ["--version"], {
1078
- stdio: "pipe",
1079
- timeout: 6e4
1080
- });
1081
- return success(void 0);
1082
- } catch {
1083
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.GCLOUD_NOT_INSTALLED));
1084
- }
1085
- }
1086
- /**
1087
- * Validate that the gcloud CLI has an active authenticated account
1088
- */
1089
- async function validateGcloudAuth() {
1090
- try {
1091
- const { stdout } = await execa("gcloud", [
1092
- "auth",
1093
- "list",
1094
- "--format=json"
1095
- ], {
1096
- stdio: "pipe",
1097
- timeout: 6e4
1098
- });
1099
- if (!JSON.parse(stdout).some((account) => account.status === "ACTIVE")) return failure(createCloudRunError(CLOUD_RUN_ERRORS.GCLOUD_NOT_AUTHENTICATED));
1100
- return success(void 0);
1101
- } catch {
1102
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.GCLOUD_NOT_AUTHENTICATED, "Failed to check gcloud authentication status"));
1103
- }
1104
- }
1105
- /**
1106
- * Get the currently configured GCP project from gcloud config
1107
- */
1108
- async function getGcpProject() {
1109
- try {
1110
- const { stdout } = await execa("gcloud", [
1111
- "config",
1112
- "get-value",
1113
- "project"
1114
- ], {
1115
- stdio: "pipe",
1116
- timeout: 6e4
1117
- });
1118
- const project = stdout.trim();
1119
- if (!project || project === "(unset)") return failure(createCloudRunError(CLOUD_RUN_ERRORS.NO_GCP_PROJECT));
1120
- return success(project);
1121
- } catch {
1122
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.NO_GCP_PROJECT, "Failed to read GCP project from gcloud config"));
1123
- }
1124
- }
1125
- /**
1126
- * Fetch the last 30 lines of the most recent Cloud Build log.
1127
- * Returns `undefined` on any failure — this is best-effort diagnostic info.
1128
- */
1129
- async function fetchRecentBuildLogs(gcpProject, region) {
1130
- try {
1131
- const { stdout: buildId } = await execa("gcloud", [
1132
- "builds",
1133
- "list",
1134
- "--limit=1",
1135
- "--project",
1136
- gcpProject,
1137
- "--region",
1138
- region,
1139
- "--format=value(id)"
1140
- ], {
1141
- stdio: "pipe",
1142
- timeout: 6e4
1143
- });
1144
- const trimmedId = buildId.trim();
1145
- if (!trimmedId) return void 0;
1146
- const { stdout: logText } = await execa("gcloud", [
1147
- "builds",
1148
- "log",
1149
- trimmedId,
1150
- "--project",
1151
- gcpProject,
1152
- "--region",
1153
- region
1154
- ], {
1155
- stdio: "pipe",
1156
- timeout: 6e4
1157
- });
1158
- return logText.split("\n").slice(-30).join("\n");
1159
- } catch {
1160
- return;
1161
- }
1162
- }
1163
- /**
1164
- * Retrieve the service URL via `gcloud run services describe`.
1165
- * Used as a fallback when the deploy command does not return parseable JSON.
1166
- */
1167
- async function getServiceUrl(serviceName, gcpProject, region) {
1168
- try {
1169
- const { stdout } = await execa("gcloud", [
1170
- "run",
1171
- "services",
1172
- "describe",
1173
- serviceName,
1174
- "--project",
1175
- gcpProject,
1176
- "--region",
1177
- region,
1178
- "--format=json"
1179
- ], {
1180
- stdio: "pipe",
1181
- timeout: 6e4
1182
- });
1183
- const url = JSON.parse(stdout).status?.url;
1184
- if (!url) return failure(createCloudRunError(CLOUD_RUN_ERRORS.DEPLOY_FAILED, "Service deployed but no URL found in service description"));
1185
- return success(url);
1186
- } catch (err) {
1187
- const details = err instanceof Error ? err.message : "Unknown error describing service";
1188
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.DEPLOY_FAILED, details));
1189
- }
1190
- }
1191
- /**
1192
- * Deploy to Cloud Run using `gcloud run deploy --source`
1193
- *
1194
- * This builds the container from source and deploys it in a single step.
1195
- * Progress is reported through optional callbacks.
1196
- */
1197
- async function deployToCloudRun(config, callbacks) {
1198
- callbacks?.onValidating?.();
1199
- const installCheck = await validateGcloudInstalled();
1200
- if (!installCheck.success) return failure(installCheck.error);
1201
- const authCheck = await validateGcloudAuth();
1202
- if (!authCheck.success) return failure(authCheck.error);
1203
- const args = [
1204
- "run",
1205
- "deploy",
1206
- config.serviceName,
1207
- "--source",
1208
- config.sourceDir,
1209
- "--project",
1210
- config.gcpProject,
1211
- "--region",
1212
- config.region,
1213
- ...config.requireAuth ? [] : ["--allow-unauthenticated"],
1214
- "--quiet",
1215
- "--format=json"
1216
- ];
1217
- const envVarsString = buildEnvVarsString(config.envVars);
1218
- if (envVarsString) args.push("--set-env-vars", envVarsString);
1219
- callbacks?.onDeploying?.();
1220
- try {
1221
- const { stdout } = await execa("gcloud", args, {
1222
- stdio: "pipe",
1223
- timeout: 3e5
1224
- });
1225
- let url;
1226
- try {
1227
- url = JSON.parse(stdout).status?.url;
1228
- } catch {}
1229
- if (!url) {
1230
- const fallbackResult = await getServiceUrl(config.serviceName, config.gcpProject, config.region);
1231
- if (!fallbackResult.success) return failure(fallbackResult.error);
1232
- url = fallbackResult.value;
1233
- }
1234
- const result = {
1235
- url,
1236
- serviceName: config.serviceName,
1237
- region: config.region,
1238
- gcpProject: config.gcpProject
1239
- };
1240
- callbacks?.onDeployComplete?.(url);
1241
- return success(result);
1242
- } catch (err) {
1243
- const execaError = err;
1244
- let details = execaError.stderr ?? execaError.message ?? String(err);
1245
- const buildLogs = await fetchRecentBuildLogs(config.gcpProject, config.region);
1246
- if (buildLogs) details += "\n\n── Recent Cloud Build logs ─────────────────────\n" + buildLogs;
1247
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.DEPLOY_FAILED, details));
1248
- }
1249
- }
1250
- /**
1251
- * Delete a Cloud Run service using `gcloud run services delete`.
1252
- */
1253
- async function deleteCloudRunService(config) {
1254
- try {
1255
- await execa("gcloud", [
1256
- "run",
1257
- "services",
1258
- "delete",
1259
- config.serviceName,
1260
- "--region",
1261
- config.region,
1262
- "--project",
1263
- config.gcpProject,
1264
- "--quiet"
1265
- ], {
1266
- stdio: "pipe",
1267
- timeout: 6e4
1268
- });
1269
- return success(void 0);
1270
- } catch (err) {
1271
- const execaError = err;
1272
- return failure(createCloudRunError(CLOUD_RUN_ERRORS.SERVICE_DELETION_FAILED, execaError.stderr ?? execaError.message ?? String(err)));
1273
- }
1274
- }
1275
- //#endregion
1276
- //#region src/utils/fluid-api.ts
1277
- const FLUID_API_BASE$1 = "https://api.fluid.app";
1278
- const FLUID_API_ERROR = {
1279
- MISSING_API_KEY: {
1280
- code: "MISSING_API_KEY",
1281
- message: "FLUID_COMPANY_API_KEY is not set"
1282
- },
1283
- INVALID_API_KEY: {
1284
- code: "INVALID_API_KEY",
1285
- message: "FLUID_COMPANY_API_KEY is invalid or expired"
1286
- },
1287
- API_UNREACHABLE: {
1288
- code: "API_UNREACHABLE",
1289
- message: "Could not reach the Fluid API"
1290
- }
1291
- };
1292
- /**
1293
- * Create a Fluid API error from a constant and optional details
1294
- */
1295
- function createFluidApiError(template, details) {
1296
- return {
1297
- code: template.code,
1298
- message: template.message,
1299
- details
1300
- };
1301
- }
1302
- /**
1303
- * Resolve and validate the Fluid API key.
1304
- *
1305
- * Priority:
1306
- * 1. `apiKeyOverride` parameter (from --fluid-company-api-key flag)
1307
- * 2. FLUID_COMPANY_API_KEY environment variable
1308
- * 3. Interactive hidden-input prompt
1309
- * 4. Fail with instructions if all sources exhausted
1310
- *
1311
- * Once resolved, validates against the Fluid API (GET /api/company/v1/companies/me).
1312
- *
1313
- * @param apiKeyOverride - Optional API key from CLI flag (skips env + prompt)
1314
- */
1315
- async function resolveFluidApiKey(apiKeyOverride) {
1316
- if (apiKeyOverride) return validateFluidApiKey(apiKeyOverride);
1317
- const envKey = process.env.FLUID_COMPANY_API_KEY;
1318
- if (envKey) return validateFluidApiKey(envKey);
1319
- const { apiKey } = await prompts({
1320
- type: "password",
1321
- name: "apiKey",
1322
- message: "Enter your Fluid company API key (FLUID_COMPANY_API_KEY)"
1323
- });
1324
- if (!apiKey) return failure(createFluidApiError(FLUID_API_ERROR.MISSING_API_KEY, "Set FLUID_COMPANY_API_KEY in your .env file or pass --fluid-company-api-key <key>."));
1325
- return validateFluidApiKey(apiKey);
1326
- }
1327
- /**
1328
- * Validate a Fluid API key by calling the companies/me endpoint.
1329
- *
1330
- * - 200 → extract company name, return success
1331
- * - 401/403 → invalid or expired key
1332
- * - Network error → API unreachable
1333
- */
1334
- async function validateFluidApiKey(apiKey) {
1335
- try {
1336
- const response = await fetch(`${FLUID_API_BASE$1}/api/company/v1/companies/me`, {
1337
- method: "GET",
1338
- headers: {
1339
- Authorization: `Bearer ${apiKey}`,
1340
- "Content-Type": "application/json"
1341
- }
1342
- });
1343
- if (response.ok) return success({
1344
- name: (await response.json()).data.company.name,
1345
- apiKey
1346
- });
1347
- if (response.status === 401 || response.status === 403) return failure(createFluidApiError(FLUID_API_ERROR.INVALID_API_KEY, `HTTP ${response.status}: Check that your FLUID_COMPANY_API_KEY is a valid, non-expired company token.`));
1348
- const body = await response.text();
1349
- return failure(createFluidApiError(FLUID_API_ERROR.API_UNREACHABLE, `HTTP ${response.status}: ${body}`));
1350
- } catch (err) {
1351
- const message = err instanceof Error ? err.message : String(err);
1352
- return failure(createFluidApiError(FLUID_API_ERROR.API_UNREACHABLE, message));
1353
- }
1354
- }
1355
- //#endregion
1356
- //#region src/utils/project.ts
1357
- /**
1358
- * Read project name from package.json
1359
- */
1360
- async function getProjectName(cwd) {
1361
- const packageJsonPath = path$1.join(cwd, "package.json");
1362
- if (await fs.pathExists(packageJsonPath)) return (await fs.readJson(packageJsonPath)).name;
1363
- }
1364
- /**
1365
- * Sanitize a project name into a valid Cloud Run service name.
1366
- * Lowercase, alphanumeric, and hyphens only.
1367
- */
1368
- function sanitizeServiceName(projectName) {
1369
- return projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "");
1370
- }
1371
- //#endregion
1372
- //#region src/utils/extract-navigation.ts
1373
- /**
1374
- * Navigation extraction utility
1375
- *
1376
- * Extracts the `navigation` export from a project's navigation.config.ts
1377
- * by writing a minimal wrapper script that imports from navigation.config.ts
1378
- * and serializes the result to stdout, then running it with tsx.
1379
- *
1380
- * This avoids transitive dependency resolution — navigation.config.ts must
1381
- * be self-contained static data with no import statements.
1382
- */
1383
- const EXTRACT_FILENAME = "__fluid_extract_nav.ts";
1384
- /**
1385
- * Extract the `navigation` export from a project's navigation.config.ts.
1386
- *
1387
- * Reads the config file, validates it contains no import statements,
1388
- * writes a minimal wrapper that imports and serializes navigation,
1389
- * and runs it with tsx. This avoids needing project dependencies
1390
- * (React, etc.) to be installed.
1391
- *
1392
- * The temp file is always cleaned up.
1393
- * Returns null if no navigation export exists.
1394
- *
1395
- * @param projectDir - The project root directory containing src/navigation.config.ts
1396
- */
1397
- async function extractNavigation(projectDir) {
1398
- const configPath = path$1.join(projectDir, "src", "navigation.config.ts");
1399
- const extractFile = path$1.join(projectDir, EXTRACT_FILENAME);
1400
- try {
1401
- const configSource = await fs.readFile(configPath, "utf-8");
1402
- if (!/export\s+(const|let)\s+navigation\s*=/.test(configSource)) return success(null);
1403
- if (/^\s*import\s/m.test(configSource)) return failure({
1404
- code: "EXTRACTION_FAILED",
1405
- message: "navigation.config.ts must not contain import statements — it should only export static data",
1406
- details: "Move all imports to portal.config.ts. navigation.config.ts must be self-contained."
1407
- });
1408
- const wrapperScript = [`import { navigation } from "./src/navigation.config.ts";`, `console.log(JSON.stringify(navigation ?? null));`].join("\n");
1409
- await fs.writeFile(extractFile, wrapperScript, "utf-8");
1410
- const output = (await execa("npx", ["tsx", EXTRACT_FILENAME], {
1411
- cwd: projectDir,
1412
- stdio: "pipe",
1413
- env: {
1414
- ...process.env,
1415
- NODE_ENV: "production"
1416
- }
1417
- })).stdout.trim();
1418
- if (!output || output === "null") return success(null);
1419
- try {
1420
- const parsed = JSON.parse(output);
1421
- if (!Array.isArray(parsed)) return failure({
1422
- code: "INVALID_FORMAT",
1423
- message: "navigation export is not an array",
1424
- details: `Expected an array, got: ${typeof parsed}`
1425
- });
1426
- return success(parsed);
1427
- } catch {
1428
- return failure({
1429
- code: "INVALID_FORMAT",
1430
- message: "Failed to parse navigation output as JSON",
1431
- details: output.slice(0, 200)
1432
- });
1433
- }
1434
- } catch (err) {
1435
- const error = err;
1436
- return failure({
1437
- code: "EXTRACTION_FAILED",
1438
- message: "Failed to extract navigation from navigation.config.ts",
1439
- details: error.stderr ?? error.message ?? String(err)
1440
- });
1441
- } finally {
1442
- await fs.remove(extractFile).catch(() => {});
1443
- }
1444
- }
1445
- //#endregion
1446
- //#region src/utils/navigation-sync.ts
1447
- /**
1448
- * Navigation sync utility
1449
- *
1450
- * Reconciles code-defined navigation items from portal.config.ts
1451
- * against the Fluid OS API. Only manages items with source: "code",
1452
- * leaving user-created and system items untouched.
1453
- */
1454
- const FLUID_API_BASE = "https://api.fluid.app";
1455
- async function apiGet(apiKey, path) {
1456
- const response = await fetch(`${FLUID_API_BASE}${path}`, {
1457
- method: "GET",
1458
- headers: {
1459
- Authorization: `Bearer ${apiKey}`,
1460
- "Content-Type": "application/json"
1461
- }
1462
- });
1463
- if (!response.ok) {
1464
- const body = await response.text();
1465
- throw new Error(`GET ${path} failed (${response.status}): ${body}`);
1466
- }
1467
- return response.json();
1468
- }
1469
- async function apiPost(apiKey, path, body) {
1470
- const response = await fetch(`${FLUID_API_BASE}${path}`, {
1471
- method: "POST",
1472
- headers: {
1473
- Authorization: `Bearer ${apiKey}`,
1474
- "Content-Type": "application/json"
1475
- },
1476
- body: JSON.stringify(body)
1477
- });
1478
- if (!response.ok) {
1479
- const text = await response.text();
1480
- throw new Error(`POST ${path} failed (${response.status}): ${text}`);
1481
- }
1482
- return response.json();
1483
- }
1484
- async function apiPut(apiKey, path, body) {
1485
- const response = await fetch(`${FLUID_API_BASE}${path}`, {
1486
- method: "PUT",
1487
- headers: {
1488
- Authorization: `Bearer ${apiKey}`,
1489
- "Content-Type": "application/json"
1490
- },
1491
- body: JSON.stringify(body)
1492
- });
1493
- if (!response.ok) {
1494
- const text = await response.text();
1495
- throw new Error(`PUT ${path} failed (${response.status}): ${text}`);
1496
- }
1497
- return response.json();
1498
- }
1499
- async function apiDelete(apiKey, path) {
1500
- const response = await fetch(`${FLUID_API_BASE}${path}`, {
1501
- method: "DELETE",
1502
- headers: {
1503
- Authorization: `Bearer ${apiKey}`,
1504
- "Content-Type": "application/json"
1505
- }
1506
- });
1507
- if (!response.ok) {
1508
- const text = await response.text();
1509
- throw new Error(`DELETE ${path} failed (${response.status}): ${text}`);
1510
- }
1511
- }
1512
- async function discoverContext(apiKey) {
1513
- let definitionId;
1514
- try {
1515
- const active = (await apiGet(apiKey, "/api/company/fluid_os/definitions")).definitions.find((d) => d.active);
1516
- if (!active) return failure({
1517
- code: "NO_DEFINITION",
1518
- message: "No active Fluid OS definition found",
1519
- details: "Create and activate a definition in the Fluid admin dashboard first."
1520
- });
1521
- definitionId = active.id;
1522
- } catch (err) {
1523
- return failure({
1524
- code: "API_ERROR",
1525
- message: "Failed to fetch Fluid OS definitions",
1526
- details: err instanceof Error ? err.message : String(err)
1527
- });
1528
- }
1529
- let navigationId;
1530
- try {
1531
- const navs = await apiGet(apiKey, `/api/company/fluid_os/definitions/${definitionId}/navigations`);
1532
- const webNav = navs.navigations.find((n) => n.name.toLowerCase().includes("web")) ?? navs.navigations[0];
1533
- if (!webNav) return failure({
1534
- code: "NO_NAVIGATION",
1535
- message: "No navigation found for the active definition",
1536
- details: "The active definition has no navigations. Create one in the Fluid admin dashboard."
1537
- });
1538
- navigationId = webNav.id;
1539
- } catch (err) {
1540
- return failure({
1541
- code: "API_ERROR",
1542
- message: "Failed to fetch navigations",
1543
- details: err instanceof Error ? err.message : String(err)
1544
- });
1545
- }
1546
- return success({
1547
- apiKey,
1548
- definitionId,
1549
- navigationId
1550
- });
1551
- }
1552
- /**
1553
- * Build a lookup key for matching code items to API items.
1554
- * Items with a slug are keyed by slug; headers (no slug) are keyed by "header:{label}".
1555
- */
1556
- function itemKey(item) {
1557
- if (item.slug) return item.slug;
1558
- return `header:${item.label ?? ""}`;
1559
- }
1560
- /**
1561
- * Recursively delete an API navigation item and all its source: "code" descendants
1562
- * in post-order (deepest children first, then the item itself).
1563
- */
1564
- async function deleteItemRecursive(apiKey, basePath, item, stats) {
1565
- const codeChildren = (item.children ?? []).filter((c) => c.source === "code");
1566
- for (const child of codeChildren) await deleteItemRecursive(apiKey, basePath, child, stats);
1567
- await apiDelete(apiKey, `${basePath}/${item.id}`);
1568
- stats.deleted++;
1569
- }
1570
- /**
1571
- * Sync a level of code-defined navigation items against existing API items.
1572
- * Recurses for children.
1573
- */
1574
- async function syncLevel(ctx, codeItems, existingItems, parentId, stats) {
1575
- const existingByKey = /* @__PURE__ */ new Map();
1576
- for (const item of existingItems) if (item.source === "code") existingByKey.set(itemKey(item), item);
1577
- const basePath = `/api/company/fluid_os/definitions/${ctx.definitionId}/navigations/${ctx.navigationId}/navigation_items`;
1578
- const matched = /* @__PURE__ */ new Set();
1579
- for (let i = 0; i < codeItems.length; i++) {
1580
- const codeItem = codeItems[i];
1581
- const key = itemKey(codeItem);
1582
- const existing = existingByKey.get(key);
1583
- matched.add(key);
1584
- if (existing) {
1585
- if (existing.label !== codeItem.label || existing.icon !== (codeItem.icon ?? null) || existing.position !== i + 1) {
1586
- await apiPut(ctx.apiKey, `${basePath}/${existing.id}`, { navigation_item: {
1587
- label: codeItem.label,
1588
- icon: codeItem.icon ?? null,
1589
- position: i + 1
1590
- } });
1591
- stats.updated++;
1592
- }
1593
- if (codeItem.children?.length) await syncLevel(ctx, codeItem.children, existing.children ?? [], existing.id, stats);
1594
- } else {
1595
- const response = await apiPost(ctx.apiKey, basePath, { navigation_item: {
1596
- label: codeItem.label,
1597
- slug: codeItem.slug ?? null,
1598
- icon: codeItem.icon ?? null,
1599
- position: i + 1,
1600
- parent_id: parentId,
1601
- source: "code"
1602
- } });
1603
- stats.created++;
1604
- if (codeItem.children?.length) {
1605
- const newId = response.navigation_item.id;
1606
- await syncLevel(ctx, codeItem.children, [], newId, stats);
1607
- }
1608
- }
1609
- }
1610
- for (const [key, item] of existingByKey) if (!matched.has(key)) await deleteItemRecursive(ctx.apiKey, basePath, item, stats);
1611
- }
1612
- /**
1613
- * Sync code-defined navigation items to the Fluid OS API.
1614
- *
1615
- * @param apiKey - Valid Fluid company API key
1616
- * @param codeItems - Navigation items from portal.config.ts
1617
- */
1618
- async function syncNavigation(apiKey, codeItems) {
1619
- const contextResult = await discoverContext(apiKey);
1620
- if (!contextResult.success) return contextResult;
1621
- const ctx = {
1622
- apiBase: FLUID_API_BASE,
1623
- ...contextResult.value
1624
- };
1625
- let existingItems;
1626
- try {
1627
- existingItems = (await apiGet(ctx.apiKey, `/api/company/fluid_os/definitions/${ctx.definitionId}/navigations/${ctx.navigationId}/navigation_items`)).navigation_items;
1628
- } catch (err) {
1629
- return failure({
1630
- code: "API_ERROR",
1631
- message: "Failed to fetch existing navigation items",
1632
- details: err instanceof Error ? err.message : String(err)
1633
- });
1634
- }
1635
- const stats = {
1636
- created: 0,
1637
- updated: 0,
1638
- deleted: 0
1639
- };
1640
- try {
1641
- await syncLevel(ctx, codeItems, existingItems, null, stats);
1642
- } catch (err) {
1643
- return failure({
1644
- code: "SYNC_FAILED",
1645
- message: "Navigation sync failed during reconciliation",
1646
- details: err instanceof Error ? err.message : String(err)
1647
- });
1648
- }
1649
- return success(stats);
1650
- }
1651
- //#endregion
1652
- //#region src/commands/deploy.ts
1653
- /**
1654
- * Detect if the project is a fullstack template (has server entry)
1655
- */
1656
- async function isFullstackProject(cwd) {
1657
- return fs.pathExists(path$1.join(cwd, "src", "server", "index.ts"));
1658
- }
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) => {
1660
- const cwd = process.cwd();
1661
- config({ path: path$1.join(cwd, ".env") });
1662
- console.log();
1663
- console.log(chalk.blue.bold("Fluid Deploy") + chalk.gray(" (Cloud Run + Turso)"));
1664
- console.log();
1665
- if (!await isFullstackProject(cwd)) {
1666
- console.log(chalk.red("Error:") + " This project is not a fullstack template.");
1667
- console.log();
1668
- console.log(chalk.yellow("fluid deploy") + " only supports fullstack projects with a Hono API server.");
1669
- console.log();
1670
- console.log("For static sites (starter template), deploy the " + chalk.cyan("dist/") + " folder to:");
1671
- console.log(" - Firebase Hosting: " + chalk.gray("https://firebase.google.com/docs/hosting"));
1672
- console.log(" - Cloud Storage: " + chalk.gray("https://cloud.google.com/storage/docs/hosting-static-website"));
1673
- console.log(" - Vercel: " + chalk.gray("https://vercel.com"));
1674
- console.log(" - Netlify: " + chalk.gray("https://netlify.com"));
1675
- console.log();
1676
- process.exit(1);
1677
- }
1678
- const spinner = ora();
1679
- spinner.start("Validating Fluid API key...");
1680
- const fluidResult = await resolveFluidApiKey(options.fluidCompanyApiKey);
1681
- if (!fluidResult.success) {
1682
- spinner.fail("Fluid API key validation failed");
1683
- console.log();
1684
- console.log(chalk.red("Error:") + " " + fluidResult.error.message);
1685
- if (fluidResult.error.details) {
1686
- console.log();
1687
- console.log(fluidResult.error.details);
1688
- }
1689
- console.log();
1690
- process.exit(1);
1691
- }
1692
- spinner.succeed(`Fluid company: ${chalk.cyan(fluidResult.value.name)}`);
1693
- if (!await fs.pathExists(path$1.join(cwd, "Dockerfile"))) {
1694
- console.log(chalk.red("Error:") + " No Dockerfile found in current directory.");
1695
- console.log();
1696
- console.log("Fullstack projects created with the latest template include a Dockerfile.");
1697
- console.log("If you upgraded from an older template, add a Dockerfile to your project.");
1698
- console.log();
1699
- process.exit(1);
1700
- }
1701
- spinner.start("Checking gcloud CLI...");
1702
- const gcloudResult = await validateGcloudInstalled();
1703
- if (!gcloudResult.success) {
1704
- spinner.fail("gcloud CLI not found");
1705
- console.log();
1706
- console.log(chalk.red("Error:") + " " + gcloudResult.error.message);
1707
- console.log();
1708
- console.log("Install the Google Cloud SDK: " + chalk.cyan("https://cloud.google.com/sdk/docs/install"));
1709
- console.log();
1710
- process.exit(1);
1711
- }
1712
- const authResult = await validateGcloudAuth();
1713
- if (!authResult.success) {
1714
- spinner.fail("gcloud not authenticated");
1715
- console.log();
1716
- console.log(chalk.red("Error:") + " " + authResult.error.message);
1717
- console.log();
1718
- console.log("Run " + chalk.cyan("gcloud auth login") + " to authenticate.");
1719
- console.log();
1720
- process.exit(1);
1721
- }
1722
- spinner.succeed("gcloud CLI ready");
1723
- let gcpProject = options.gcpProject;
1724
- if (!gcpProject) {
1725
- spinner.start("Detecting GCP project...");
1726
- const projectResult = await getGcpProject();
1727
- if (!projectResult.success) {
1728
- spinner.fail("No GCP project configured");
1729
- console.log();
1730
- console.log(chalk.red("Error:") + " " + projectResult.error.message);
1731
- console.log();
1732
- console.log("Either:");
1733
- console.log(" - Run " + chalk.cyan("gcloud config set project PROJECT_ID"));
1734
- console.log(" - Use the " + chalk.cyan("--gcp-project <id>") + " flag");
1735
- console.log();
1736
- process.exit(1);
1737
- }
1738
- gcpProject = projectResult.value;
1739
- spinner.succeed(`GCP project: ${chalk.cyan(gcpProject)}`);
1740
- }
1741
- spinner.start("Resolving Turso credentials...");
1742
- const tursoConfigResult = await resolveTursoConfig(options.tursoOrg);
1743
- if (!tursoConfigResult.success) {
1744
- spinner.fail("Turso credentials not found");
1745
- console.log();
1746
- console.log(chalk.red("Error:") + " " + tursoConfigResult.error.message);
1747
- if (tursoConfigResult.error.details) {
1748
- console.log();
1749
- console.log(tursoConfigResult.error.details);
1750
- }
1751
- console.log();
1752
- process.exit(1);
1753
- }
1754
- const tursoConfig = tursoConfigResult.value;
1755
- const sourceLabel = tursoConfig.source === "env" ? "environment variables" : "Turso CLI";
1756
- spinner.succeed(`Turso: authenticated via ${sourceLabel}`);
1757
- const projectName = options.project ?? await getProjectName(cwd);
1758
- if (!projectName) {
1759
- console.log(chalk.red("Error:") + " Could not determine project name.");
1760
- console.log();
1761
- console.log("Either:");
1762
- console.log(" - Add a " + chalk.cyan("name") + " field to package.json");
1763
- console.log(" - Use the " + chalk.cyan("--project <name>") + " flag");
1764
- console.log();
1765
- process.exit(1);
1766
- }
1767
- const serviceName = sanitizeServiceName(projectName);
1768
- if (!serviceName) {
1769
- console.log(chalk.red("Error:") + " Project name sanitizes to an empty service name.");
1770
- console.log();
1771
- console.log("Use the " + chalk.cyan("--project <name>") + " flag to provide a valid service name.");
1772
- console.log();
1773
- process.exit(1);
1774
- }
1775
- const region = options.region ?? "us-central1";
1776
- console.log();
1777
- console.log(chalk.gray("Service: ") + chalk.white(serviceName));
1778
- console.log(chalk.gray("Region: ") + chalk.white(region));
1779
- console.log(chalk.gray("GCP Project: ") + chalk.white(gcpProject));
1780
- console.log(chalk.gray("Turso Org: ") + chalk.white(tursoConfig.org) + chalk.gray(` (via ${sourceLabel})`));
1781
- console.log();
1782
- if (!options.skipLocalBuild) {
1783
- let dockerAvailable = false;
1784
- try {
1785
- await execa("docker", ["--version"], { stdio: "pipe" });
1786
- dockerAvailable = true;
1787
- } catch {
1788
- spinner.warn("Docker not found — skipping local build check");
1789
- }
1790
- if (dockerAvailable) {
1791
- spinner.start("Running local Docker build check...");
1792
- try {
1793
- await execa("docker", [
1794
- "build",
1795
- "-t",
1796
- `${serviceName}-check`,
1797
- "."
1798
- ], {
1799
- cwd,
1800
- stdio: "pipe"
1801
- });
1802
- spinner.succeed("Local Docker build passed");
1803
- } catch (buildErr) {
1804
- spinner.fail("Local Docker build failed");
1805
- const buildError = buildErr;
1806
- if (buildError.stderr) {
1807
- console.log();
1808
- console.log(chalk.gray(buildError.stderr));
1809
- }
1810
- console.log();
1811
- console.log(chalk.red("Fix the Dockerfile errors above before deploying."));
1812
- console.log(chalk.gray("Tip: Use --skip-local-build to bypass this check."));
1813
- console.log();
1814
- process.exit(1);
1815
- }
1816
- console.log();
1817
- }
1818
- }
1819
- try {
1820
- const dbResult = await provisionDatabase(tursoConfig, serviceName, options.dbRegion, {
1821
- onGroupCreating: () => {
1822
- spinner.start("Ensuring Turso database group...");
1823
- },
1824
- onGroupReady: () => {
1825
- spinner.succeed("Database group ready");
1826
- },
1827
- onDatabaseCreating: (name) => {
1828
- spinner.start(`Creating database "${name}"...`);
1829
- },
1830
- onDatabaseReady: (name) => {
1831
- spinner.succeed(`Database "${name}" ready`);
1832
- },
1833
- onTokenCreating: () => {
1834
- spinner.start("Creating database auth token...");
1835
- },
1836
- onTokenReady: () => {
1837
- spinner.succeed("Auth token created");
1838
- }
1839
- });
1840
- if (!dbResult.success) {
1841
- spinner.fail("Turso provisioning failed");
1842
- console.log();
1843
- console.log(chalk.red("Error:") + " " + dbResult.error.message);
1844
- if (dbResult.error.details) console.log(chalk.gray("Details: ") + dbResult.error.details);
1845
- console.log();
1846
- process.exit(1);
1847
- }
1848
- const database = dbResult.value;
1849
- console.log();
1850
- console.log(chalk.gray("Database URL: ") + chalk.cyan(database.url));
1851
- console.log();
1852
- const deployResult = await deployToCloudRun({
1853
- gcpProject,
1854
- region,
1855
- serviceName,
1856
- sourceDir: cwd,
1857
- requireAuth: options.requireAuth,
1858
- envVars: {
1859
- DATABASE_URL: database.url,
1860
- DATABASE_AUTH_TOKEN: database.authToken,
1861
- NODE_ENV: "production",
1862
- FLUID_COMPANY_API_KEY: fluidResult.value.apiKey
1863
- }
1864
- }, {
1865
- onValidating: () => {
1866
- spinner.start("Preparing Cloud Run deployment...");
1867
- },
1868
- onDeploying: () => {
1869
- spinner.text = "Deploying to Cloud Run (this may take 2-5 minutes)...";
1870
- },
1871
- onDeployComplete: () => {
1872
- spinner.succeed("Deployed to Cloud Run");
1873
- }
1874
- });
1875
- if (!deployResult.success) {
1876
- spinner.fail("Cloud Run deployment failed");
1877
- console.log();
1878
- console.log(chalk.red("Error:") + " " + deployResult.error.message);
1879
- if (deployResult.error.details) console.log(chalk.gray("Details: ") + deployResult.error.details);
1880
- console.log();
1881
- process.exit(1);
1882
- }
1883
- console.log();
1884
- console.log(chalk.green.bold("Deployed successfully!"));
1885
- console.log();
1886
- console.log(chalk.gray("Service URL: ") + chalk.cyan(deployResult.value.url));
1887
- console.log(chalk.gray("Database: ") + chalk.cyan(database.databaseName));
1888
- console.log(chalk.gray("Region: ") + chalk.cyan(deployResult.value.region));
1889
- console.log(chalk.gray("GCP Project: ") + chalk.cyan(deployResult.value.gcpProject));
1890
- console.log();
1891
- if (!options.skipNavSync) {
1892
- const configPath = path$1.join(cwd, "src", "navigation.config.ts");
1893
- if (await fs.pathExists(configPath)) {
1894
- const navSpinner = ora("Extracting navigation from navigation.config.ts...").start();
1895
- const extractResult = await extractNavigation(cwd);
1896
- if (extractResult.success && extractResult.value != null) {
1897
- const navItems = extractResult.value;
1898
- navSpinner.text = `Syncing ${navItems.length} navigation item(s)...`;
1899
- const syncResult = await syncNavigation(fluidResult.value.apiKey, navItems);
1900
- if (syncResult.success) {
1901
- const { created, updated, deleted } = syncResult.value;
1902
- const parts = [];
1903
- if (created > 0) parts.push(`${created} created`);
1904
- if (updated > 0) parts.push(`${updated} updated`);
1905
- if (deleted > 0) parts.push(`${deleted} deleted`);
1906
- if (parts.length > 0) navSpinner.succeed(`Navigation synced (${parts.join(", ")})`);
1907
- else navSpinner.succeed("Navigation up to date");
1908
- } else {
1909
- navSpinner.warn("Navigation sync failed (deploy succeeded)");
1910
- console.log(chalk.yellow(" Warning: ") + syncResult.error.message);
1911
- if (syncResult.error.details) console.log(chalk.gray(" Details: ") + syncResult.error.details);
1912
- }
1913
- } else if (extractResult.success && extractResult.value == null) navSpinner.info("No navigation export found in navigation.config.ts — skipping sync");
1914
- else {
1915
- navSpinner.warn("Could not extract navigation (deploy succeeded)");
1916
- if (!extractResult.success && extractResult.error.details) console.log(chalk.gray(" Details: ") + extractResult.error.details);
1917
- }
1918
- }
1919
- }
1920
- const serviceUrl = deployResult.value.url;
1921
- const healthSpinner = ora("Running health check...").start();
1922
- try {
1923
- const controller = new AbortController();
1924
- const timeout = setTimeout(() => controller.abort(), 3e4);
1925
- const healthRes = await fetch(`${serviceUrl}/api/health`, { signal: controller.signal });
1926
- clearTimeout(timeout);
1927
- if (healthRes.ok) healthSpinner.succeed("Health check passed");
1928
- else healthSpinner.warn("Health check returned non-200 (service may still be starting)");
1929
- } catch {
1930
- healthSpinner.warn("Health check timed out (cold start may take a moment)");
1931
- }
1932
- if (options.migrate || database.isNew) {
1933
- if (database.isNew && !options.migrate) {
1934
- console.log();
1935
- console.log(chalk.blue("New database") + " — running migrations automatically...");
1936
- }
1937
- const migrateCmd = getRunCommand("db:push");
1938
- const migrateSpinner = ora(`Running migrations (${migrateCmd})...`).start();
1939
- try {
1940
- const [pmBin, ...pmArgs] = migrateCmd.split(" ");
1941
- await execa(pmBin, pmArgs, {
1942
- cwd,
1943
- stdio: "pipe",
1944
- env: {
1945
- ...process.env,
1946
- DATABASE_URL: database.url,
1947
- DATABASE_AUTH_TOKEN: database.authToken
1948
- }
1949
- });
1950
- migrateSpinner.succeed("Database migrations complete");
1951
- } catch (migrateErr) {
1952
- const migrateError = migrateErr;
1953
- migrateSpinner.warn("Database migration failed (deploy succeeded)");
1954
- console.log();
1955
- console.log(chalk.yellow("Migration error:") + " " + (migrateError.stderr ?? migrateError.message ?? String(migrateErr)));
1956
- console.log();
1957
- console.log("Run migrations manually:");
1958
- console.log(` DATABASE_URL=${database.url} DATABASE_AUTH_TOKEN=<token> ${migrateCmd}`);
1959
- }
1960
- } else {
1961
- console.log();
1962
- console.log(chalk.yellow("Important:") + " Run database migrations against your production database:");
1963
- console.log(` DATABASE_URL=${database.url} DATABASE_AUTH_TOKEN=<token> pnpm db:push`);
1964
- console.log(chalk.gray("Tip: Use --migrate to run migrations automatically."));
1965
- }
1966
- console.log();
1967
- } catch (error) {
1968
- spinner.fail("Deployment failed");
1969
- console.log();
1970
- const errorMessage = getErrorMessage(error);
1971
- console.log(chalk.red("Error:") + " " + errorMessage);
1972
- console.log();
1973
- process.exit(1);
1974
- }
1975
- });
1976
- //#endregion
1977
- //#region src/commands/destroy.ts
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) => {
1979
- const cwd = process.cwd();
1980
- config({ path: path$1.join(cwd, ".env") });
1981
- console.log();
1982
- console.log(chalk.red.bold("Fluid Destroy") + chalk.gray(" (Cloud Run + Turso)"));
1983
- console.log();
1984
- const spinner = ora();
1985
- spinner.start("Validating Fluid API key...");
1986
- const fluidResult = await resolveFluidApiKey(options.fluidCompanyApiKey);
1987
- if (!fluidResult.success) {
1988
- spinner.fail("Fluid API key validation failed");
1989
- console.log();
1990
- console.log(chalk.red("Error:") + " " + fluidResult.error.message);
1991
- if (fluidResult.error.details) {
1992
- console.log();
1993
- console.log(fluidResult.error.details);
1994
- }
1995
- console.log();
1996
- process.exit(1);
1997
- }
1998
- spinner.succeed(`Fluid company: ${chalk.cyan(fluidResult.value.name)}`);
1999
- spinner.start("Checking gcloud CLI...");
2000
- const gcloudResult = await validateGcloudInstalled();
2001
- if (!gcloudResult.success) {
2002
- spinner.fail("gcloud CLI not found");
2003
- console.log();
2004
- console.log(chalk.red("Error:") + " " + gcloudResult.error.message);
2005
- console.log();
2006
- process.exit(1);
2007
- }
2008
- const authResult = await validateGcloudAuth();
2009
- if (!authResult.success) {
2010
- spinner.fail("gcloud not authenticated");
2011
- console.log();
2012
- console.log(chalk.red("Error:") + " " + authResult.error.message);
2013
- console.log();
2014
- process.exit(1);
2015
- }
2016
- spinner.succeed("gcloud CLI ready");
2017
- let gcpProject = options.gcpProject;
2018
- if (!gcpProject) {
2019
- spinner.start("Detecting GCP project...");
2020
- const projectResult = await getGcpProject();
2021
- if (!projectResult.success) {
2022
- spinner.fail("No GCP project configured");
2023
- console.log();
2024
- console.log(chalk.red("Error:") + " " + projectResult.error.message);
2025
- console.log();
2026
- process.exit(1);
2027
- }
2028
- gcpProject = projectResult.value;
2029
- spinner.succeed(`GCP project: ${chalk.cyan(gcpProject)}`);
2030
- }
2031
- spinner.start("Resolving Turso credentials...");
2032
- const tursoConfigResult = await resolveTursoConfig(options.tursoOrg);
2033
- if (!tursoConfigResult.success) {
2034
- spinner.fail("Turso credentials not found");
2035
- console.log();
2036
- console.log(chalk.red("Error:") + " " + tursoConfigResult.error.message);
2037
- if (tursoConfigResult.error.details) {
2038
- console.log();
2039
- console.log(tursoConfigResult.error.details);
2040
- }
2041
- console.log();
2042
- process.exit(1);
2043
- }
2044
- const tursoConfig = tursoConfigResult.value;
2045
- spinner.succeed("Turso credentials resolved");
2046
- const projectName = options.project ?? await getProjectName(cwd);
2047
- if (!projectName) {
2048
- console.log(chalk.red("Error:") + " Could not determine project name.");
2049
- console.log();
2050
- console.log("Either:");
2051
- console.log(" - Add a " + chalk.cyan("name") + " field to package.json");
2052
- console.log(" - Use the " + chalk.cyan("--project <name>") + " flag");
2053
- console.log();
2054
- process.exit(1);
2055
- }
2056
- const serviceName = sanitizeServiceName(projectName);
2057
- if (!serviceName) {
2058
- console.log(chalk.red("Error:") + " Project name sanitizes to an empty service name.");
2059
- console.log();
2060
- console.log("Use the " + chalk.cyan("--project <name>") + " flag to provide a valid service name.");
2061
- console.log();
2062
- process.exit(1);
2063
- }
2064
- const region = options.region ?? "us-central1";
2065
- console.log();
2066
- console.log(chalk.yellow("The following resources will be destroyed:"));
2067
- console.log();
2068
- console.log(chalk.gray(" Cloud Run service: ") + chalk.white(serviceName));
2069
- console.log(chalk.gray(" Region: ") + chalk.white(region));
2070
- console.log(chalk.gray(" GCP Project: ") + chalk.white(gcpProject));
2071
- console.log(chalk.gray(" Turso database: ") + chalk.white(serviceName));
2072
- console.log(chalk.gray(" Turso org: ") + chalk.white(tursoConfig.org));
2073
- console.log();
2074
- if (!options.yes) {
2075
- const { confirmed } = await prompts({
2076
- type: "confirm",
2077
- name: "confirmed",
2078
- message: "Are you sure you want to destroy these resources?",
2079
- initial: false
2080
- });
2081
- if (!confirmed) {
2082
- console.log();
2083
- console.log(chalk.gray("Destroy cancelled."));
2084
- console.log();
2085
- return;
2086
- }
2087
- }
2088
- console.log();
2089
- spinner.start(`Deleting Cloud Run service "${serviceName}"...`);
2090
- const deleteServiceResult = await deleteCloudRunService({
2091
- serviceName,
2092
- gcpProject,
2093
- region
2094
- });
2095
- if (!deleteServiceResult.success) {
2096
- spinner.warn("Cloud Run service deletion failed");
2097
- console.log(chalk.yellow("Warning:") + " " + deleteServiceResult.error.message);
2098
- if (deleteServiceResult.error.details) console.log(chalk.gray("Details: ") + deleteServiceResult.error.details);
2099
- } else spinner.succeed("Cloud Run service deleted");
2100
- spinner.start(`Deleting Turso database "${serviceName}"...`);
2101
- const deleteDbResult = await deleteDatabase(tursoConfig, serviceName);
2102
- if (!deleteDbResult.success) {
2103
- spinner.warn("Turso database deletion failed");
2104
- console.log(chalk.yellow("Warning:") + " " + deleteDbResult.error.message);
2105
- if (deleteDbResult.error.details) console.log(chalk.gray("Details: ") + deleteDbResult.error.details);
2106
- } else spinner.succeed("Turso database deleted");
2107
- console.log();
2108
- if (deleteServiceResult.success && deleteDbResult.success) console.log(chalk.green.bold("All resources destroyed successfully."));
2109
- else console.log(chalk.yellow.bold("Destroy completed with warnings.") + " Some resources may need manual cleanup.");
2110
- console.log();
2111
- });
2112
- //#endregion
2113
537
  //#region src/utils/push-validation.ts
2114
538
  /**
2115
539
  * Cross-reference validation and change categorization utilities for the push command.
@@ -3503,19 +1927,17 @@ const versionCommand = new Command("version").description("Manage portal definit
3503
1927
  /**
3504
1928
  * @fluid-app/fluid-cli-portal
3505
1929
  *
3506
- * Fluid CLI plugin for building and deploying portal applications.
1930
+ * Fluid CLI plugin for building portal applications.
3507
1931
  * Auto-discovered by @fluid-app/fluid-cli via the fluid-cli-* naming convention.
3508
1932
  */
3509
1933
  const plugin = {
3510
1934
  name: "fluid-cli-portal",
3511
1935
  version: "0.1.0",
3512
1936
  async register(ctx) {
3513
- const portal = new Command("portal").description("Build, develop, and deploy portal applications");
1937
+ const portal = new Command("portal").description("Build and develop portal applications");
3514
1938
  portal.addCommand(createCommand);
3515
1939
  portal.addCommand(devCommand);
3516
1940
  portal.addCommand(buildCommand);
3517
- portal.addCommand(deployCommand);
3518
- portal.addCommand(destroyCommand);
3519
1941
  portal.addCommand(pullCommand);
3520
1942
  portal.addCommand(pushCommand);
3521
1943
  portal.addCommand(widgetCommand);
@@ -3525,6 +1947,6 @@ const plugin = {
3525
1947
  }
3526
1948
  };
3527
1949
  //#endregion
3528
- 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 };
1950
+ export { FILE_SYSTEM_ERRORS, TEMPLATES, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, plugin as default, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, fileExists, getInstallCommand, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, installDependencies, pathExists, promptProjectConfig, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolveSlugToId, runPackageManager, slugFromPath, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeMappings, writeSnapshot };
3529
1951
 
3530
1952
  //# sourceMappingURL=index.mjs.map