@geekmidas/cli 0.39.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 (77) 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 +674 -122
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +653 -101
  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/deploy/__tests__/domain.spec.ts +231 -0
  58. package/src/deploy/__tests__/secrets.spec.ts +300 -0
  59. package/src/deploy/__tests__/sniffer.spec.ts +221 -0
  60. package/src/deploy/docker.ts +40 -11
  61. package/src/deploy/dokploy-api.ts +99 -0
  62. package/src/deploy/domain.ts +125 -0
  63. package/src/deploy/index.ts +366 -148
  64. package/src/deploy/secrets.ts +182 -0
  65. package/src/deploy/sniffer.ts +180 -0
  66. package/src/dev/index.ts +11 -0
  67. package/src/docker/index.ts +17 -2
  68. package/src/docker/templates.ts +171 -1
  69. package/src/init/versions.ts +2 -2
  70. package/src/workspace/index.ts +2 -0
  71. package/src/workspace/schema.ts +32 -6
  72. package/src/workspace/types.ts +64 -2
  73. package/tsconfig.tsbuildinfo +1 -1
  74. package/dist/dokploy-api-B0w17y4_.mjs +0 -3
  75. package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
  76. package/dist/index-C7TkoYmt.d.mts.map +0 -1
  77. 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,172 +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> = {};
783
-
784
- // Build the entire workspace once (not per-app to avoid Turbo/Next.js lock conflicts)
785
- if (!skipBuild) {
786
- logger.log('\n🏗️ Building workspace...');
787
- try {
788
- await buildCommand({
789
- provider: 'server',
790
- production: true,
791
- stage,
792
- });
793
- logger.log(' ✓ Workspace build complete');
794
- } catch (error) {
795
- const message = error instanceof Error ? error.message : 'Unknown error';
796
- logger.log(` ✗ Workspace build failed: ${message}`);
797
- throw error;
798
- }
799
- }
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
+ );
800
829
 
801
- // Deploy apps in dependency order
802
- logger.log('\n📦 Deploying applications...');
830
+ // Track deployed app public URLs for frontend builds
831
+ const publicUrls: Record<string, string> = {};
803
832
  const results: AppDeployResult[] = [];
833
+ const dokployConfig = workspace.deploy.dokploy;
804
834
 
805
- for (const appName of appsToDeployNames) {
806
- const app = workspace.apps[appName]!;
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...');
807
840
 
808
- logger.log(
809
- `\n ${app.type === 'backend' ? '⚙️' : '🌐'} Deploying ${appName}...`,
810
- );
841
+ for (const appName of backendApps) {
842
+ const app = workspace.apps[appName]!;
811
843
 
812
- try {
813
- // Find or create application in Dokploy
814
- const dokployAppName = `${workspace.name}-${appName}`;
815
- let application: DokployApplication | undefined;
844
+ logger.log(`\n ⚙️ Deploying ${appName}...`);
816
845
 
817
846
  try {
818
- // Try to find existing application (Dokploy doesn't have a direct lookup)
819
- // We'll create a new one and handle the error if it exists
820
- application = await api.createApplication(
821
- dokployAppName,
822
- project.projectId,
823
- environmentId,
824
- );
825
- logger.log(` Created application: ${application.applicationId}`);
826
- } catch (error) {
827
- const message =
828
- error instanceof Error ? error.message : 'Unknown error';
829
- if (
830
- message.includes('already exists') ||
831
- message.includes('duplicate')
832
- ) {
833
- logger.log(` Application already exists`);
834
- // For now, we'll continue without the applicationId
835
- // In a real implementation, we'd need to list and find the app
836
- } else {
837
- 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
+ }
838
869
  }
839
- }
840
870
 
841
- // Note: Workspace was already built once at the start of deployment
842
- // to avoid Turbo/Next.js lock conflicts from concurrent builds
871
+ // Get encrypted secrets for this app
872
+ const appSecrets = encryptedSecrets.get(appName);
873
+ const buildArgs: string[] = [];
843
874
 
844
- // Build Docker image
845
- const imageName = `${workspace.name}-${appName}`;
846
- const imageRef = registry
847
- ? `${registry}/${imageName}:${imageTag}`
848
- : `${imageName}:${imageTag}`;
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`);
881
+ }
849
882
 
850
- logger.log(` Building Docker image: ${imageRef}`);
883
+ // Build Docker image with encrypted secrets
884
+ const imageName = `${workspace.name}-${appName}`;
885
+ const imageRef = registry
886
+ ? `${registry}/${imageName}:${imageTag}`
887
+ : `${imageName}:${imageTag}`;
851
888
 
852
- await deployDocker({
853
- stage,
854
- tag: imageTag,
855
- skipPush: false,
856
- config: {
857
- registry,
858
- imageName,
859
- appName, // Pass appName for Dockerfile.{appName} selection
860
- },
861
- });
889
+ logger.log(` Building Docker image: ${imageRef}`);
890
+
891
+ await deployDocker({
892
+ stage,
893
+ tag: imageTag,
894
+ skipPush: false,
895
+ config: {
896
+ registry,
897
+ imageName,
898
+ appName,
899
+ },
900
+ buildArgs,
901
+ });
862
902
 
863
- // Prepare environment variables
864
- 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}`];
865
905
 
866
- // Add dependency URLs
867
- for (const dep of app.dependencies) {
868
- const depUrl = deployedAppUrls[dep];
869
- if (depUrl) {
870
- 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}`);
871
909
  }
872
- }
873
910
 
874
- // Add infrastructure URLs for backend apps
875
- if (app.type === 'backend') {
876
- if (dockerServices.postgres) {
877
- envVars.push(
878
- `DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`,
879
- );
880
- }
881
- if (dockerServices.redis) {
882
- envVars.push(
883
- `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'),
884
920
  );
921
+
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
+ }
952
+
953
+ results.push({
954
+ appName,
955
+ type: app.type,
956
+ success: true,
957
+ applicationId: application.applicationId,
958
+ imageRef,
959
+ });
960
+
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
+ });
973
+
974
+ logger.log(` ✓ ${appName} image pushed (app already exists)`);
885
975
  }
886
- }
976
+ } catch (error) {
977
+ const message = error instanceof Error ? error.message : 'Unknown error';
978
+ logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
887
979
 
888
- // Configure application in Dokploy
889
- if (application) {
890
- // Save Docker provider config
891
- await api.saveDockerProvider(application.applicationId, imageRef, {
892
- registryId,
980
+ results.push({
981
+ appName,
982
+ type: app.type,
983
+ success: false,
984
+ error: message,
893
985
  });
894
986
 
895
- // Save environment variables
896
- await api.saveApplicationEnv(
897
- application.applicationId,
898
- envVars.join('\n'),
987
+ // Abort on backend failure to prevent incomplete deployment
988
+ throw new Error(
989
+ `Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
899
990
  );
991
+ }
992
+ }
993
+ }
900
994
 
901
- // Deploy
902
- logger.log(` Deploying to Dokploy...`);
903
- await api.deployApplication(application.applicationId);
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...');
904
1000
 
905
- // Track this app's URL for dependent apps
906
- // Dokploy uses the appName as the internal hostname
907
- const appUrl = `http://${dokployAppName}:${app.port}`;
908
- deployedAppUrls[appName] = appUrl;
1001
+ for (const appName of frontendApps) {
1002
+ const app = workspace.apps[appName]!;
909
1003
 
910
- results.push({
911
- appName,
912
- type: app.type,
913
- success: true,
914
- applicationId: application.applicationId,
915
- imageRef,
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),
916
1057
  });
917
1058
 
918
- logger.log(` ✓ ${appName} deployed successfully`);
919
- } else {
920
- // Application already exists, just track it
921
- const appUrl = `http://${dokployAppName}:${app.port}`;
922
- deployedAppUrls[appName] = appUrl;
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}`);
923
1142
 
924
1143
  results.push({
925
1144
  appName,
926
1145
  type: app.type,
927
- success: true,
928
- imageRef,
1146
+ success: false,
1147
+ error: message,
929
1148
  });
930
-
931
- logger.log(` ✓ ${appName} image pushed (app already exists)`);
1149
+ // Don't abort on frontend failures - continue with other frontends
932
1150
  }
933
- } catch (error) {
934
- const message = error instanceof Error ? error.message : 'Unknown error';
935
- logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
936
-
937
- results.push({
938
- appName,
939
- type: app.type,
940
- success: false,
941
- error: message,
942
- });
943
1151
  }
944
1152
  }
945
1153
 
1154
+ // ==================================================================
946
1155
  // Summary
1156
+ // ==================================================================
947
1157
  const successCount = results.filter((r) => r.success).length;
948
1158
  const failedCount = results.filter((r) => !r.success).length;
949
1159
 
@@ -955,6 +1165,14 @@ export async function workspaceDeployCommand(
955
1165
  logger.log(` Failed: ${failedCount}`);
956
1166
  }
957
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
+
958
1176
  return {
959
1177
  apps: results,
960
1178
  projectId: project.projectId,