@geekmidas/cli 0.18.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.
- package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2639 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2634 -563
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +8 -3
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/src/deploy/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
985
|
+
// Single-app mode - use existing logic
|
|
564
986
|
const config = await loadConfig();
|
|
565
987
|
|
|
566
988
|
// Generate tag if not provided
|
package/src/deploy/types.ts
CHANGED
|
@@ -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 */
|