@geekmidas/cli 0.17.0 → 0.19.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 (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2644 -564
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2639 -564
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.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 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +9 -4
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +223 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -0
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
@@ -8,17 +8,25 @@ import {
8
8
  } from '../auth';
9
9
  import { storeDokployRegistryId } from '../auth/credentials';
10
10
  import { buildCommand } from '../build/index';
11
- import { type GkmConfig, loadConfig } from '../config';
11
+ import { type GkmConfig, loadConfig, loadWorkspaceConfig } from '../config';
12
+ import {
13
+ getAppBuildOrder,
14
+ getDeployTargetError,
15
+ isDeployTargetSupported,
16
+ } from '../workspace/index.js';
17
+ import type { NormalizedWorkspace } from '../workspace/types.js';
12
18
  import { deployDocker, resolveDockerConfig } from './docker';
13
19
  import { deployDokploy } from './dokploy';
14
- import { DokployApi } from './dokploy-api';
20
+ import { DokployApi, type DokployApplication } from './dokploy-api';
15
21
  import { updateConfig } from './init';
16
22
  import type {
23
+ AppDeployResult,
17
24
  DeployOptions,
18
25
  DeployProvider,
19
26
  DeployResult,
20
27
  DockerDeployConfig,
21
28
  DokployDeployConfig,
29
+ WorkspaceDeployResult,
22
30
  } from './types';
23
31
 
24
32
  const logger = console;
@@ -549,18 +557,432 @@ export function generateTag(stage: string): string {
549
557
  return `${stage}-${timestamp}`;
550
558
  }
551
559
 
560
+ /**
561
+ * 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
566
+ * @internal Exported for testing
567
+ */
568
+ export async function workspaceDeployCommand(
569
+ workspace: NormalizedWorkspace,
570
+ options: DeployOptions,
571
+ ): Promise<WorkspaceDeployResult> {
572
+ const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
573
+
574
+ if (provider !== 'dokploy') {
575
+ throw new Error(
576
+ `Workspace deployment only supports Dokploy. Got: ${provider}`,
577
+ );
578
+ }
579
+
580
+ logger.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
581
+ logger.log(` Stage: ${stage}`);
582
+
583
+ // Generate tag if not provided
584
+ const imageTag = tag ?? generateTag(stage);
585
+ logger.log(` Tag: ${imageTag}`);
586
+
587
+ // Get apps to deploy in dependency order
588
+ const buildOrder = getAppBuildOrder(workspace);
589
+
590
+ // Filter to selected apps if specified
591
+ let appsToDeployNames = buildOrder;
592
+ if (selectedApps && selectedApps.length > 0) {
593
+ // Validate selected apps exist
594
+ const invalidApps = selectedApps.filter((name) => !workspace.apps[name]);
595
+ if (invalidApps.length > 0) {
596
+ throw new Error(
597
+ `Unknown apps: ${invalidApps.join(', ')}\n` +
598
+ `Available apps: ${Object.keys(workspace.apps).join(', ')}`,
599
+ );
600
+ }
601
+ // Keep only selected apps, but maintain dependency order
602
+ appsToDeployNames = buildOrder.filter((name) =>
603
+ selectedApps.includes(name),
604
+ );
605
+ logger.log(` Deploying apps: ${appsToDeployNames.join(', ')}`);
606
+ } else {
607
+ logger.log(` Deploying all apps: ${appsToDeployNames.join(', ')}`);
608
+ }
609
+
610
+ // 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
+ const dokployApps = appsToDeployNames.filter((name) => {
614
+ const app = workspace.apps[name]!;
615
+ const target = app.resolvedDeployTarget;
616
+ if (!isDeployTargetSupported(target)) {
617
+ logger.log(
618
+ ` ⚠️ Skipping ${name}: ${getDeployTargetError(target, name)}`,
619
+ );
620
+ return false;
621
+ }
622
+ return true;
623
+ });
624
+
625
+ if (dokployApps.length === 0) {
626
+ throw new Error(
627
+ 'No apps to deploy. All selected apps have unsupported deploy targets.',
628
+ );
629
+ }
630
+
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
+ );
638
+ }
639
+
640
+ appsToDeployNames = dokployApps;
641
+
642
+ // Ensure we have Dokploy credentials
643
+ let creds = await getDokployCredentials();
644
+ if (!creds) {
645
+ logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
646
+ const endpoint = await prompt(
647
+ 'Dokploy URL (e.g., https://dokploy.example.com): ',
648
+ );
649
+ const normalizedEndpoint = endpoint.replace(/\/$/, '');
650
+
651
+ try {
652
+ new URL(normalizedEndpoint);
653
+ } catch {
654
+ throw new Error('Invalid URL format');
655
+ }
656
+
657
+ logger.log(
658
+ `\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
659
+ );
660
+ const token = await prompt('API Token: ', true);
661
+
662
+ logger.log('\nValidating credentials...');
663
+ const isValid = await validateDokployToken(normalizedEndpoint, token);
664
+ if (!isValid) {
665
+ throw new Error('Invalid credentials. Please check your token.');
666
+ }
667
+
668
+ await storeDokployCredentials(token, normalizedEndpoint);
669
+ creds = { token, endpoint: normalizedEndpoint };
670
+ logger.log('✓ Credentials saved');
671
+ }
672
+
673
+ const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
674
+
675
+ // Find or create project for the workspace
676
+ logger.log('\n📁 Setting up Dokploy project...');
677
+ const projectName = workspace.name;
678
+ const projects = await api.listProjects();
679
+ let project = projects.find(
680
+ (p) => p.name.toLowerCase() === projectName.toLowerCase(),
681
+ );
682
+
683
+ let environmentId: string;
684
+
685
+ if (project) {
686
+ logger.log(` Found existing project: ${project.name}`);
687
+ // Get or create environment for stage
688
+ const projectDetails = await api.getProject(project.projectId);
689
+ const environments = projectDetails.environments ?? [];
690
+ const matchingEnv = environments.find(
691
+ (e) => e.name.toLowerCase() === stage.toLowerCase(),
692
+ );
693
+ if (matchingEnv) {
694
+ environmentId = matchingEnv.environmentId;
695
+ logger.log(` Using environment: ${matchingEnv.name}`);
696
+ } else {
697
+ logger.log(` Creating "${stage}" environment...`);
698
+ const env = await api.createEnvironment(project.projectId, stage);
699
+ environmentId = env.environmentId;
700
+ logger.log(` ✓ Created environment: ${stage}`);
701
+ }
702
+ } else {
703
+ logger.log(` Creating project: ${projectName}`);
704
+ const result = await api.createProject(projectName);
705
+ project = result.project;
706
+ // Create environment for stage if different from default
707
+ if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
708
+ logger.log(` Creating "${stage}" environment...`);
709
+ const env = await api.createEnvironment(project.projectId, stage);
710
+ environmentId = env.environmentId;
711
+ } else {
712
+ environmentId = result.environment.environmentId;
713
+ }
714
+ logger.log(` ✓ Created project: ${project.projectId}`);
715
+ }
716
+
717
+ // Get or set up registry
718
+ logger.log('\n🐳 Checking registry...');
719
+ let registryId = await getDokployRegistryId();
720
+ const registry = workspace.deploy.dokploy?.registry;
721
+
722
+ if (registryId) {
723
+ try {
724
+ const reg = await api.getRegistry(registryId);
725
+ logger.log(` Using registry: ${reg.registryName}`);
726
+ } catch {
727
+ logger.log(' ⚠ Stored registry not found, clearing...');
728
+ registryId = undefined;
729
+ await storeDokployRegistryId('');
730
+ }
731
+ }
732
+
733
+ if (!registryId) {
734
+ const registries = await api.listRegistries();
735
+ if (registries.length > 0) {
736
+ // Use first available registry
737
+ registryId = registries[0]!.registryId;
738
+ await storeDokployRegistryId(registryId);
739
+ logger.log(` Using registry: ${registries[0]!.registryName}`);
740
+ } else if (registry) {
741
+ logger.log(" No registries found in Dokploy. Let's create one.");
742
+ logger.log(` Registry URL: ${registry}`);
743
+
744
+ const username = await prompt('Registry username: ');
745
+ const password = await prompt('Registry password/token: ', true);
746
+
747
+ const reg = await api.createRegistry(
748
+ 'Default Registry',
749
+ registry,
750
+ username,
751
+ password,
752
+ );
753
+ registryId = reg.registryId;
754
+ await storeDokployRegistryId(registryId);
755
+ logger.log(` ✓ Registry created: ${registryId}`);
756
+ } else {
757
+ logger.log(
758
+ ' ⚠ No registry configured. Set deploy.dokploy.registry in workspace config',
759
+ );
760
+ }
761
+ }
762
+
763
+ // Provision infrastructure services if configured
764
+ const services = workspace.services;
765
+ const dockerServices = {
766
+ postgres: services.db !== undefined && services.db !== false,
767
+ redis: services.cache !== undefined && services.cache !== false,
768
+ };
769
+
770
+ if (dockerServices.postgres || dockerServices.redis) {
771
+ logger.log('\n🔧 Provisioning infrastructure services...');
772
+ await provisionServices(
773
+ api,
774
+ project.projectId,
775
+ environmentId,
776
+ workspace.name,
777
+ dockerServices,
778
+ );
779
+ }
780
+
781
+ // Track deployed app URLs for environment variable injection
782
+ const deployedAppUrls: Record<string, string> = {};
783
+
784
+ // Deploy apps in dependency order
785
+ logger.log('\n📦 Deploying applications...');
786
+ const results: AppDeployResult[] = [];
787
+
788
+ for (const appName of appsToDeployNames) {
789
+ const app = workspace.apps[appName]!;
790
+ const appPath = app.path;
791
+
792
+ logger.log(
793
+ `\n ${app.type === 'backend' ? '⚙️' : '🌐'} Deploying ${appName}...`,
794
+ );
795
+
796
+ try {
797
+ // Find or create application in Dokploy
798
+ const dokployAppName = `${workspace.name}-${appName}`;
799
+ let application: DokployApplication | undefined;
800
+
801
+ 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;
822
+ }
823
+ }
824
+
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}`;
831
+
832
+ try {
833
+ process.chdir(fullAppPath);
834
+ await buildCommand({
835
+ provider: 'server',
836
+ production: true,
837
+ stage,
838
+ });
839
+ } finally {
840
+ process.chdir(originalCwd);
841
+ }
842
+ }
843
+
844
+ // Build Docker image
845
+ const imageName = `${workspace.name}-${appName}`;
846
+ const imageRef = registry
847
+ ? `${registry}/${imageName}:${imageTag}`
848
+ : `${imageName}:${imageTag}`;
849
+
850
+ logger.log(` Building Docker image: ${imageRef}`);
851
+
852
+ await deployDocker({
853
+ stage,
854
+ tag: imageTag,
855
+ skipPush: false,
856
+ config: {
857
+ registry,
858
+ imageName,
859
+ },
860
+ });
861
+
862
+ // Prepare environment variables
863
+ const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
864
+
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}`);
870
+ }
871
+ }
872
+
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}`,
883
+ );
884
+ }
885
+ }
886
+
887
+ // Configure application in Dokploy
888
+ if (application) {
889
+ // Save Docker provider config
890
+ await api.saveDockerProvider(application.applicationId, imageRef, {
891
+ registryId,
892
+ });
893
+
894
+ // Save environment variables
895
+ await api.saveApplicationEnv(
896
+ application.applicationId,
897
+ envVars.join('\n'),
898
+ );
899
+
900
+ // Deploy
901
+ logger.log(` Deploying to Dokploy...`);
902
+ await api.deployApplication(application.applicationId);
903
+
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;
908
+
909
+ results.push({
910
+ appName,
911
+ type: app.type,
912
+ success: true,
913
+ applicationId: application.applicationId,
914
+ imageRef,
915
+ });
916
+
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;
922
+
923
+ results.push({
924
+ appName,
925
+ type: app.type,
926
+ success: true,
927
+ imageRef,
928
+ });
929
+
930
+ logger.log(` ✓ ${appName} image pushed (app already exists)`);
931
+ }
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
+ }
943
+ }
944
+
945
+ // Summary
946
+ const successCount = results.filter((r) => r.success).length;
947
+ const failedCount = results.filter((r) => !r.success).length;
948
+
949
+ logger.log(`\n${'─'.repeat(50)}`);
950
+ logger.log(`\n✅ Workspace deployment complete!`);
951
+ logger.log(` Project: ${project.projectId}`);
952
+ logger.log(` Successful: ${successCount}`);
953
+ if (failedCount > 0) {
954
+ logger.log(` Failed: ${failedCount}`);
955
+ }
956
+
957
+ return {
958
+ apps: results,
959
+ projectId: project.projectId,
960
+ successCount,
961
+ failedCount,
962
+ };
963
+ }
964
+
552
965
  /**
553
966
  * Main deploy command
554
967
  */
555
968
  export async function deployCommand(
556
969
  options: DeployOptions,
557
- ): Promise<DeployResult> {
970
+ ): Promise<DeployResult | WorkspaceDeployResult> {
558
971
  const { provider, stage, tag, skipPush, skipBuild } = options;
559
972
 
973
+ // Load config with workspace detection
974
+ const loadedConfig = await loadWorkspaceConfig();
975
+
976
+ // Route to workspace deploy mode for multi-app workspaces
977
+ if (loadedConfig.type === 'workspace') {
978
+ logger.log('📦 Detected workspace configuration');
979
+ return workspaceDeployCommand(loadedConfig.workspace, options);
980
+ }
981
+
560
982
  logger.log(`\n🚀 Deploying to ${provider}...`);
561
983
  logger.log(` Stage: ${stage}`);
562
984
 
563
- // Load config
985
+ // Single-app mode - use existing logic
564
986
  const config = await loadConfig();
565
987
 
566
988
  // Generate tag if not provided
@@ -13,6 +13,8 @@ export interface DeployOptions {
13
13
  skipPush?: boolean;
14
14
  /** Skip building (use existing build) */
15
15
  skipBuild?: boolean;
16
+ /** Specific apps to deploy (workspace mode only, default: all) */
17
+ apps?: string[];
16
18
  }
17
19
 
18
20
  /** Result from a deployment */
@@ -27,6 +29,36 @@ export interface DeployResult {
27
29
  url?: string;
28
30
  }
29
31
 
32
+ /** Result for a single app deployment in workspace mode */
33
+ export interface AppDeployResult {
34
+ /** App name */
35
+ appName: string;
36
+ /** App type */
37
+ type: 'backend' | 'frontend';
38
+ /** Whether deployment succeeded */
39
+ success: boolean;
40
+ /** Dokploy application ID */
41
+ applicationId?: string;
42
+ /** Docker image reference */
43
+ imageRef?: string;
44
+ /** Deployment URL */
45
+ url?: string;
46
+ /** Error message if failed */
47
+ error?: string;
48
+ }
49
+
50
+ /** Result from workspace deployment */
51
+ export interface WorkspaceDeployResult {
52
+ /** Results for each app */
53
+ apps: AppDeployResult[];
54
+ /** Dokploy project ID */
55
+ projectId: string;
56
+ /** Total number of successful deployments */
57
+ successCount: number;
58
+ /** Total number of failed deployments */
59
+ failedCount: number;
60
+ }
61
+
30
62
  /** Docker provider configuration */
31
63
  export interface DockerDeployConfig {
32
64
  /** Container registry URL */