@geekmidas/cli 0.38.0 → 0.40.0

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.
Files changed (80) hide show
  1. package/dist/{bundler-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
  2. package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
  3. package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
  4. package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
  5. package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
  6. package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
  7. package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
  8. package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
  9. package/dist/config.cjs +2 -2
  10. package/dist/config.d.cts +1 -1
  11. package/dist/config.d.mts +2 -2
  12. package/dist/config.mjs +2 -2
  13. package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
  14. package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
  15. package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
  16. package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
  17. package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
  18. package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
  19. package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
  20. package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
  21. package/dist/encryption-Biq0EZ4m.cjs +4 -0
  22. package/dist/encryption-CQXBZGkt.mjs +3 -0
  23. package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
  24. package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
  25. package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
  26. package/dist/index-CXa3odEw.d.mts.map +1 -0
  27. package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
  28. package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
  29. package/dist/index.cjs +787 -145
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +767 -125
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
  34. package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
  35. package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
  36. package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
  38. package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
  39. package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
  40. package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -3
  44. package/dist/openapi.d.mts +1 -1
  45. package/dist/openapi.mjs +3 -3
  46. package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
  47. package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
  48. package/dist/workspace/index.cjs +1 -1
  49. package/dist/workspace/index.d.cts +2 -2
  50. package/dist/workspace/index.d.mts +3 -3
  51. package/dist/workspace/index.mjs +1 -1
  52. package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
  53. package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
  54. package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
  55. package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
  56. package/package.json +5 -5
  57. package/src/build/index.ts +23 -6
  58. package/src/deploy/__tests__/domain.spec.ts +231 -0
  59. package/src/deploy/__tests__/secrets.spec.ts +300 -0
  60. package/src/deploy/__tests__/sniffer.spec.ts +221 -0
  61. package/src/deploy/docker.ts +58 -29
  62. package/src/deploy/dokploy-api.ts +99 -0
  63. package/src/deploy/domain.ts +125 -0
  64. package/src/deploy/index.ts +364 -145
  65. package/src/deploy/secrets.ts +182 -0
  66. package/src/deploy/sniffer.ts +180 -0
  67. package/src/dev/index.ts +155 -9
  68. package/src/docker/index.ts +17 -2
  69. package/src/docker/templates.ts +171 -1
  70. package/src/index.ts +18 -1
  71. package/src/init/generators/auth.ts +2 -0
  72. package/src/init/versions.ts +2 -2
  73. package/src/workspace/index.ts +2 -0
  74. package/src/workspace/schema.ts +32 -6
  75. package/src/workspace/types.ts +64 -2
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/dist/dokploy-api-B0w17y4_.mjs +0 -3
  78. package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
  79. package/dist/index-C7TkoYmt.d.mts.map +0 -1
  80. package/dist/index-CpchsC9w.d.cts.map +0 -1
@@ -9,6 +9,7 @@ import {
9
9
  import { storeDokployRegistryId } from '../auth/credentials';
10
10
  import { buildCommand } from '../build/index';
11
11
  import { type GkmConfig, loadConfig, loadWorkspaceConfig } from '../config';
12
+ import { readStageSecrets } from '../secrets/storage.js';
12
13
  import {
13
14
  getAppBuildOrder,
14
15
  getDeployTargetError,
@@ -18,7 +19,18 @@ import type { NormalizedWorkspace } from '../workspace/types.js';
18
19
  import { deployDocker, resolveDockerConfig } from './docker';
19
20
  import { deployDokploy } from './dokploy';
20
21
  import { DokployApi, type DokployApplication } from './dokploy-api';
22
+ import {
23
+ generatePublicUrlBuildArgs,
24
+ getPublicUrlArgNames,
25
+ isMainFrontendApp,
26
+ resolveHost,
27
+ } from './domain.js';
21
28
  import { updateConfig } from './init';
29
+ import {
30
+ generateSecretsReport,
31
+ prepareSecretsForAllApps,
32
+ } from './secrets.js';
33
+ import { sniffAllApps } from './sniffer.js';
22
34
  import type {
23
35
  AppDeployResult,
24
36
  DeployOptions,
@@ -559,17 +571,23 @@ export function generateTag(stage: string): string {
559
571
 
560
572
  /**
561
573
  * Deploy all apps in a workspace to Dokploy.
562
- * - Workspace maps to one Dokploy project
563
- * - Each app maps to one Dokploy application
564
- * - Deploys in dependency order (backends before dependent frontends)
565
- * - Syncs environment variables including {APP_NAME}_URL
574
+ *
575
+ * Two-phase orchestration:
576
+ * - PHASE 1: Deploy backend apps (with encrypted secrets)
577
+ * - PHASE 2: Deploy frontend apps (with public URLs from backends)
578
+ *
579
+ * Security model:
580
+ * - Backend apps get encrypted secrets embedded at build time
581
+ * - Only GKM_MASTER_KEY is injected as Dokploy env var
582
+ * - Frontend apps get public URLs baked in at build time (no secrets)
583
+ *
566
584
  * @internal Exported for testing
567
585
  */
568
586
  export async function workspaceDeployCommand(
569
587
  workspace: NormalizedWorkspace,
570
588
  options: DeployOptions,
571
589
  ): Promise<WorkspaceDeployResult> {
572
- const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
590
+ const { provider, stage, tag, apps: selectedApps } = options;
573
591
 
574
592
  if (provider !== 'dokploy') {
575
593
  throw new Error(
@@ -608,8 +626,6 @@ export async function workspaceDeployCommand(
608
626
  }
609
627
 
610
628
  // Filter apps by deploy target
611
- // In Phase 1, only 'dokploy' is supported. Other targets should have been
612
- // caught at config validation, but we handle it gracefully here too.
613
629
  const dokployApps = appsToDeployNames.filter((name) => {
614
630
  const app = workspace.apps[name]!;
615
631
  const target = app.resolvedDeployTarget;
@@ -628,18 +644,44 @@ export async function workspaceDeployCommand(
628
644
  );
629
645
  }
630
646
 
631
- if (dokployApps.length !== appsToDeployNames.length) {
632
- const skipped = appsToDeployNames.filter(
633
- (name) => !dokployApps.includes(name),
634
- );
635
- logger.log(
636
- ` 📌 ${skipped.length} app(s) skipped due to unsupported targets`,
637
- );
647
+ appsToDeployNames = dokployApps;
648
+
649
+ // ==================================================================
650
+ // PREFLIGHT: Load secrets and sniff environment requirements
651
+ // ==================================================================
652
+ logger.log('\n🔐 Loading secrets and analyzing environment requirements...');
653
+
654
+ // Load secrets for this stage
655
+ const stageSecrets = await readStageSecrets(stage, workspace.root);
656
+ if (!stageSecrets) {
657
+ logger.log(` ⚠️ No secrets found for stage "${stage}"`);
658
+ logger.log(` Run "gkm secrets:init --stage ${stage}" to create secrets`);
638
659
  }
639
660
 
640
- appsToDeployNames = dokployApps;
661
+ // Sniff environment variables for all apps
662
+ const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
663
+
664
+ // Prepare encrypted secrets for backend apps
665
+ const encryptedSecrets = stageSecrets
666
+ ? prepareSecretsForAllApps(stageSecrets, sniffedApps)
667
+ : new Map();
668
+
669
+ // Report on secrets preparation
670
+ if (stageSecrets) {
671
+ const report = generateSecretsReport(encryptedSecrets, sniffedApps);
672
+ if (report.appsWithSecrets.length > 0) {
673
+ logger.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(', ')}`);
674
+ }
675
+ if (report.appsWithMissingSecrets.length > 0) {
676
+ for (const { appName, missing } of report.appsWithMissingSecrets) {
677
+ logger.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(', ')}`);
678
+ }
679
+ }
680
+ }
641
681
 
642
- // Ensure we have Dokploy credentials
682
+ // ==================================================================
683
+ // SETUP: Credentials, Project, Registry
684
+ // ==================================================================
643
685
  let creds = await getDokployCredentials();
644
686
  if (!creds) {
645
687
  logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
@@ -684,7 +726,6 @@ export async function workspaceDeployCommand(
684
726
 
685
727
  if (project) {
686
728
  logger.log(` Found existing project: ${project.name}`);
687
- // Get or create environment for stage
688
729
  const projectDetails = await api.getProject(project.projectId);
689
730
  const environments = projectDetails.environments ?? [];
690
731
  const matchingEnv = environments.find(
@@ -703,7 +744,6 @@ export async function workspaceDeployCommand(
703
744
  logger.log(` Creating project: ${projectName}`);
704
745
  const result = await api.createProject(projectName);
705
746
  project = result.project;
706
- // Create environment for stage if different from default
707
747
  if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
708
748
  logger.log(` Creating "${stage}" environment...`);
709
749
  const env = await api.createEnvironment(project.projectId, stage);
@@ -733,7 +773,6 @@ export async function workspaceDeployCommand(
733
773
  if (!registryId) {
734
774
  const registries = await api.listRegistries();
735
775
  if (registries.length > 0) {
736
- // Use first available registry
737
776
  registryId = registries[0]!.registryId;
738
777
  await storeDokployRegistryId(registryId);
739
778
  logger.log(` Using registry: ${registries[0]!.registryName}`);
@@ -778,171 +817,343 @@ export async function workspaceDeployCommand(
778
817
  );
779
818
  }
780
819
 
781
- // Track deployed app URLs for environment variable injection
782
- const deployedAppUrls: Record<string, string> = {};
820
+ // ==================================================================
821
+ // Separate apps by type for two-phase deployment
822
+ // ==================================================================
823
+ const backendApps = appsToDeployNames.filter(
824
+ (name) => workspace.apps[name]!.type === 'backend',
825
+ );
826
+ const frontendApps = appsToDeployNames.filter(
827
+ (name) => workspace.apps[name]!.type === 'frontend',
828
+ );
783
829
 
784
- // Deploy apps in dependency order
785
- logger.log('\n📦 Deploying applications...');
830
+ // Track deployed app public URLs for frontend builds
831
+ const publicUrls: Record<string, string> = {};
786
832
  const results: AppDeployResult[] = [];
833
+ const dokployConfig = workspace.deploy.dokploy;
787
834
 
788
- for (const appName of appsToDeployNames) {
789
- const app = workspace.apps[appName]!;
790
- const appPath = app.path;
835
+ // ==================================================================
836
+ // PHASE 1: Deploy backend apps (with encrypted secrets)
837
+ // ==================================================================
838
+ if (backendApps.length > 0) {
839
+ logger.log('\n📦 PHASE 1: Deploying backend applications...');
791
840
 
792
- logger.log(
793
- `\n ${app.type === 'backend' ? '⚙️' : '🌐'} Deploying ${appName}...`,
794
- );
841
+ for (const appName of backendApps) {
842
+ const app = workspace.apps[appName]!;
795
843
 
796
- try {
797
- // Find or create application in Dokploy
798
- const dokployAppName = `${workspace.name}-${appName}`;
799
- let application: DokployApplication | undefined;
844
+ logger.log(`\n ⚙️ Deploying ${appName}...`);
800
845
 
801
846
  try {
802
- // Try to find existing application (Dokploy doesn't have a direct lookup)
803
- // We'll create a new one and handle the error if it exists
804
- application = await api.createApplication(
805
- dokployAppName,
806
- project.projectId,
807
- environmentId,
808
- );
809
- logger.log(` Created application: ${application.applicationId}`);
810
- } catch (error) {
811
- const message =
812
- error instanceof Error ? error.message : 'Unknown error';
813
- if (
814
- message.includes('already exists') ||
815
- message.includes('duplicate')
816
- ) {
817
- logger.log(` Application already exists`);
818
- // For now, we'll continue without the applicationId
819
- // In a real implementation, we'd need to list and find the app
820
- } else {
821
- throw error;
847
+ const dokployAppName = `${workspace.name}-${appName}`;
848
+ let application: DokployApplication | undefined;
849
+
850
+ // Create or find application
851
+ try {
852
+ application = await api.createApplication(
853
+ dokployAppName,
854
+ project.projectId,
855
+ environmentId,
856
+ );
857
+ logger.log(` Created application: ${application.applicationId}`);
858
+ } catch (error) {
859
+ const message =
860
+ error instanceof Error ? error.message : 'Unknown error';
861
+ if (
862
+ message.includes('already exists') ||
863
+ message.includes('duplicate')
864
+ ) {
865
+ logger.log(` Application already exists`);
866
+ } else {
867
+ throw error;
868
+ }
822
869
  }
823
- }
824
870
 
825
- // Build the app if not skipped
826
- if (!skipBuild) {
827
- logger.log(` Building ${appName}...`);
828
- // For workspace, we need to build from the app directory
829
- const originalCwd = process.cwd();
830
- const fullAppPath = `${workspace.root}/${appPath}`;
871
+ // Get encrypted secrets for this app
872
+ const appSecrets = encryptedSecrets.get(appName);
873
+ const buildArgs: string[] = [];
831
874
 
832
- try {
833
- process.chdir(fullAppPath);
834
- await buildCommand({
835
- provider: 'server',
836
- production: true,
837
- stage,
838
- });
839
- } finally {
840
- process.chdir(originalCwd);
875
+ if (appSecrets && appSecrets.secretCount > 0) {
876
+ buildArgs.push(
877
+ `GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`,
878
+ );
879
+ buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
880
+ logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
841
881
  }
842
- }
843
882
 
844
- // Build Docker image
845
- const imageName = `${workspace.name}-${appName}`;
846
- const imageRef = registry
847
- ? `${registry}/${imageName}:${imageTag}`
848
- : `${imageName}:${imageTag}`;
883
+ // Build Docker image with encrypted secrets
884
+ const imageName = `${workspace.name}-${appName}`;
885
+ const imageRef = registry
886
+ ? `${registry}/${imageName}:${imageTag}`
887
+ : `${imageName}:${imageTag}`;
849
888
 
850
- logger.log(` Building Docker image: ${imageRef}`);
889
+ logger.log(` Building Docker image: ${imageRef}`);
851
890
 
852
- await deployDocker({
853
- stage,
854
- tag: imageTag,
855
- skipPush: false,
856
- config: {
857
- registry,
858
- imageName,
859
- },
860
- });
891
+ await deployDocker({
892
+ stage,
893
+ tag: imageTag,
894
+ skipPush: false,
895
+ config: {
896
+ registry,
897
+ imageName,
898
+ appName,
899
+ },
900
+ buildArgs,
901
+ });
861
902
 
862
- // Prepare environment variables
863
- const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
903
+ // Prepare environment variables - ONLY inject GKM_MASTER_KEY
904
+ const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
864
905
 
865
- // Add dependency URLs
866
- for (const dep of app.dependencies) {
867
- const depUrl = deployedAppUrls[dep];
868
- if (depUrl) {
869
- envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
906
+ // Add master key for runtime decryption (NOT plain secrets)
907
+ if (appSecrets && appSecrets.masterKey) {
908
+ envVars.push(`GKM_MASTER_KEY=${appSecrets.masterKey}`);
870
909
  }
871
- }
872
910
 
873
- // Add infrastructure URLs for backend apps
874
- if (app.type === 'backend') {
875
- if (dockerServices.postgres) {
876
- envVars.push(
877
- `DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`,
878
- );
879
- }
880
- if (dockerServices.redis) {
881
- envVars.push(
882
- `REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`,
911
+ // Configure and deploy application in Dokploy
912
+ if (application) {
913
+ await api.saveDockerProvider(application.applicationId, imageRef, {
914
+ registryId,
915
+ });
916
+
917
+ await api.saveApplicationEnv(
918
+ application.applicationId,
919
+ envVars.join('\n'),
883
920
  );
884
- }
885
- }
886
921
 
887
- // Configure application in Dokploy
888
- if (application) {
889
- // Save Docker provider config
890
- await api.saveDockerProvider(application.applicationId, imageRef, {
891
- registryId,
892
- });
922
+ logger.log(` Deploying to Dokploy...`);
923
+ await api.deployApplication(application.applicationId);
924
+
925
+ // Create domain for this app
926
+ try {
927
+ const host = resolveHost(
928
+ appName,
929
+ app,
930
+ stage,
931
+ dokployConfig,
932
+ false, // Backend apps are not main frontend
933
+ );
934
+
935
+ await api.createDomain({
936
+ host,
937
+ port: app.port,
938
+ https: true,
939
+ certificateType: 'letsencrypt',
940
+ applicationId: application.applicationId,
941
+ });
942
+
943
+ const publicUrl = `https://${host}`;
944
+ publicUrls[appName] = publicUrl;
945
+ logger.log(` ✓ Domain: ${publicUrl}`);
946
+ } catch (domainError) {
947
+ // Domain might already exist, try to get public URL anyway
948
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
949
+ publicUrls[appName] = `https://${host}`;
950
+ logger.log(` ℹ Domain already configured: https://${host}`);
951
+ }
893
952
 
894
- // Save environment variables
895
- await api.saveApplicationEnv(
896
- application.applicationId,
897
- envVars.join('\n'),
898
- );
953
+ results.push({
954
+ appName,
955
+ type: app.type,
956
+ success: true,
957
+ applicationId: application.applicationId,
958
+ imageRef,
959
+ });
899
960
 
900
- // Deploy
901
- logger.log(` Deploying to Dokploy...`);
902
- await api.deployApplication(application.applicationId);
961
+ logger.log(` ✓ ${appName} deployed successfully`);
962
+ } else {
963
+ // Application already exists
964
+ const host = resolveHost(appName, app, stage, dokployConfig, false);
965
+ publicUrls[appName] = `https://${host}`;
966
+
967
+ results.push({
968
+ appName,
969
+ type: app.type,
970
+ success: true,
971
+ imageRef,
972
+ });
903
973
 
904
- // Track this app's URL for dependent apps
905
- // Dokploy uses the appName as the internal hostname
906
- const appUrl = `http://${dokployAppName}:${app.port}`;
907
- deployedAppUrls[appName] = appUrl;
974
+ logger.log(` ✓ ${appName} image pushed (app already exists)`);
975
+ }
976
+ } catch (error) {
977
+ const message = error instanceof Error ? error.message : 'Unknown error';
978
+ logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
908
979
 
909
980
  results.push({
910
981
  appName,
911
982
  type: app.type,
912
- success: true,
913
- applicationId: application.applicationId,
914
- imageRef,
983
+ success: false,
984
+ error: message,
915
985
  });
916
986
 
917
- logger.log(` ✓ ${appName} deployed successfully`);
918
- } else {
919
- // Application already exists, just track it
920
- const appUrl = `http://${dokployAppName}:${app.port}`;
921
- deployedAppUrls[appName] = appUrl;
987
+ // Abort on backend failure to prevent incomplete deployment
988
+ throw new Error(
989
+ `Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
990
+ );
991
+ }
992
+ }
993
+ }
994
+
995
+ // ==================================================================
996
+ // PHASE 2: Deploy frontend apps (with public URLs from backends)
997
+ // ==================================================================
998
+ if (frontendApps.length > 0) {
999
+ logger.log('\n🌐 PHASE 2: Deploying frontend applications...');
1000
+
1001
+ for (const appName of frontendApps) {
1002
+ const app = workspace.apps[appName]!;
1003
+
1004
+ logger.log(`\n 🌐 Deploying ${appName}...`);
1005
+
1006
+ try {
1007
+ const dokployAppName = `${workspace.name}-${appName}`;
1008
+ let application: DokployApplication | undefined;
1009
+
1010
+ // Create or find application
1011
+ try {
1012
+ application = await api.createApplication(
1013
+ dokployAppName,
1014
+ project.projectId,
1015
+ environmentId,
1016
+ );
1017
+ logger.log(` Created application: ${application.applicationId}`);
1018
+ } catch (error) {
1019
+ const message =
1020
+ error instanceof Error ? error.message : 'Unknown error';
1021
+ if (
1022
+ message.includes('already exists') ||
1023
+ message.includes('duplicate')
1024
+ ) {
1025
+ logger.log(` Application already exists`);
1026
+ } else {
1027
+ throw error;
1028
+ }
1029
+ }
1030
+
1031
+ // Generate public URL build args from dependencies
1032
+ const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
1033
+ if (buildArgs.length > 0) {
1034
+ logger.log(` Public URLs: ${buildArgs.join(', ')}`);
1035
+ }
1036
+
1037
+ // Build Docker image with public URLs
1038
+ const imageName = `${workspace.name}-${appName}`;
1039
+ const imageRef = registry
1040
+ ? `${registry}/${imageName}:${imageTag}`
1041
+ : `${imageName}:${imageTag}`;
1042
+
1043
+ logger.log(` Building Docker image: ${imageRef}`);
1044
+
1045
+ await deployDocker({
1046
+ stage,
1047
+ tag: imageTag,
1048
+ skipPush: false,
1049
+ config: {
1050
+ registry,
1051
+ imageName,
1052
+ appName,
1053
+ },
1054
+ buildArgs,
1055
+ // Pass public URL arg names for Dockerfile generation
1056
+ publicUrlArgs: getPublicUrlArgNames(app),
1057
+ });
1058
+
1059
+ // Prepare environment variables - no secrets needed
1060
+ const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
1061
+
1062
+ // Configure and deploy application in Dokploy
1063
+ if (application) {
1064
+ await api.saveDockerProvider(application.applicationId, imageRef, {
1065
+ registryId,
1066
+ });
1067
+
1068
+ await api.saveApplicationEnv(
1069
+ application.applicationId,
1070
+ envVars.join('\n'),
1071
+ );
1072
+
1073
+ logger.log(` Deploying to Dokploy...`);
1074
+ await api.deployApplication(application.applicationId);
1075
+
1076
+ // Create domain for this app
1077
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1078
+ try {
1079
+ const host = resolveHost(
1080
+ appName,
1081
+ app,
1082
+ stage,
1083
+ dokployConfig,
1084
+ isMainFrontend,
1085
+ );
1086
+
1087
+ await api.createDomain({
1088
+ host,
1089
+ port: app.port,
1090
+ https: true,
1091
+ certificateType: 'letsencrypt',
1092
+ applicationId: application.applicationId,
1093
+ });
1094
+
1095
+ const publicUrl = `https://${host}`;
1096
+ publicUrls[appName] = publicUrl;
1097
+ logger.log(` ✓ Domain: ${publicUrl}`);
1098
+ } catch (domainError) {
1099
+ const host = resolveHost(
1100
+ appName,
1101
+ app,
1102
+ stage,
1103
+ dokployConfig,
1104
+ isMainFrontend,
1105
+ );
1106
+ publicUrls[appName] = `https://${host}`;
1107
+ logger.log(` ℹ Domain already configured: https://${host}`);
1108
+ }
1109
+
1110
+ results.push({
1111
+ appName,
1112
+ type: app.type,
1113
+ success: true,
1114
+ applicationId: application.applicationId,
1115
+ imageRef,
1116
+ });
1117
+
1118
+ logger.log(` ✓ ${appName} deployed successfully`);
1119
+ } else {
1120
+ const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
1121
+ const host = resolveHost(
1122
+ appName,
1123
+ app,
1124
+ stage,
1125
+ dokployConfig,
1126
+ isMainFrontend,
1127
+ );
1128
+ publicUrls[appName] = `https://${host}`;
1129
+
1130
+ results.push({
1131
+ appName,
1132
+ type: app.type,
1133
+ success: true,
1134
+ imageRef,
1135
+ });
1136
+
1137
+ logger.log(` ✓ ${appName} image pushed (app already exists)`);
1138
+ }
1139
+ } catch (error) {
1140
+ const message = error instanceof Error ? error.message : 'Unknown error';
1141
+ logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
922
1142
 
923
1143
  results.push({
924
1144
  appName,
925
1145
  type: app.type,
926
- success: true,
927
- imageRef,
1146
+ success: false,
1147
+ error: message,
928
1148
  });
929
-
930
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1149
+ // Don't abort on frontend failures - continue with other frontends
931
1150
  }
932
- } catch (error) {
933
- const message = error instanceof Error ? error.message : 'Unknown error';
934
- logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
935
-
936
- results.push({
937
- appName,
938
- type: app.type,
939
- success: false,
940
- error: message,
941
- });
942
1151
  }
943
1152
  }
944
1153
 
1154
+ // ==================================================================
945
1155
  // Summary
1156
+ // ==================================================================
946
1157
  const successCount = results.filter((r) => r.success).length;
947
1158
  const failedCount = results.filter((r) => !r.success).length;
948
1159
 
@@ -954,6 +1165,14 @@ export async function workspaceDeployCommand(
954
1165
  logger.log(` Failed: ${failedCount}`);
955
1166
  }
956
1167
 
1168
+ // Print deployed URLs
1169
+ if (Object.keys(publicUrls).length > 0) {
1170
+ logger.log('\n 📡 Deployed URLs:');
1171
+ for (const [name, url] of Object.entries(publicUrls)) {
1172
+ logger.log(` ${name}: ${url}`);
1173
+ }
1174
+ }
1175
+
957
1176
  return {
958
1177
  apps: results,
959
1178
  projectId: project.projectId,