@geekmidas/cli 1.5.1 → 1.7.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/CHANGELOG.md +17 -0
- package/dist/{config-BYn5yUt5.cjs → config-6JHOwLCx.cjs} +30 -2
- package/dist/{config-dLNQIvDR.mjs.map → config-6JHOwLCx.cjs.map} +1 -1
- package/dist/{config-dLNQIvDR.mjs → config-DxASSNjr.mjs} +25 -3
- package/dist/{config-BYn5yUt5.cjs.map → config-DxASSNjr.mjs.map} +1 -1
- package/dist/config.cjs +3 -2
- package/dist/config.d.cts +14 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +14 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -3
- package/dist/{index-Bj5VNxEL.d.mts → index-C-KxSGGK.d.mts} +2 -2
- package/dist/{index-Ba21_lNt.d.cts.map → index-C-KxSGGK.d.mts.map} +1 -1
- package/dist/{index-Ba21_lNt.d.cts → index-Cyk2rTyj.d.cts} +2 -2
- package/dist/{index-Bj5VNxEL.d.mts.map → index-Cyk2rTyj.d.cts.map} +1 -1
- package/dist/index.cjs +555 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +519 -103
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CMTyaIJJ.mjs → openapi-BYlyAbH3.mjs} +6 -5
- package/dist/openapi-BYlyAbH3.mjs.map +1 -0
- package/dist/{openapi-CqblwJZ4.cjs → openapi-CnvwSRDU.cjs} +6 -5
- package/dist/openapi-CnvwSRDU.cjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Dy8k7Wru.mjs → workspace-9IQIjwkQ.mjs} +5 -3
- package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
- package/dist/{workspace-DIMnYaYt.cjs → workspace-D2ocAlpl.cjs} +5 -3
- package/dist/workspace-D2ocAlpl.cjs.map +1 -0
- package/package.json +11 -10
- package/src/config.ts +44 -0
- package/src/dev/__tests__/index.spec.ts +490 -0
- package/src/dev/index.ts +313 -18
- package/src/generators/Generator.ts +4 -1
- package/src/init/__tests__/generators.spec.ts +167 -18
- package/src/init/__tests__/init.spec.ts +66 -3
- package/src/init/generators/auth.ts +6 -5
- package/src/init/generators/config.ts +49 -7
- package/src/init/generators/docker.ts +8 -8
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -5
- package/src/init/generators/package.ts +4 -0
- package/src/init/generators/test.ts +133 -0
- package/src/init/generators/ui.ts +13 -12
- package/src/init/generators/web.ts +9 -8
- package/src/init/index.ts +2 -0
- package/src/init/templates/api.ts +6 -6
- package/src/init/templates/minimal.ts +2 -2
- package/src/init/templates/worker.ts +2 -2
- package/src/init/versions.ts +4 -4
- package/src/openapi.ts +6 -2
- package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
- package/src/test/__tests__/api.spec.ts +199 -0
- package/src/test/__tests__/auth.spec.ts +162 -0
- package/src/test/__tests__/index.spec.ts +323 -0
- package/src/test/__tests__/web.spec.ts +210 -0
- package/src/test/index.ts +165 -14
- package/src/workspace/__tests__/index.spec.ts +3 -0
- package/src/workspace/index.ts +4 -2
- package/dist/openapi-CMTyaIJJ.mjs.map +0 -1
- package/dist/openapi-CqblwJZ4.cjs.map +0 -1
- package/dist/workspace-DIMnYaYt.cjs.map +0 -1
- package/dist/workspace-Dy8k7Wru.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { __require } from "./chunk-Duj1WY3L.mjs";
|
|
3
|
-
import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-
|
|
4
|
-
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-
|
|
3
|
+
import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-9IQIjwkQ.mjs";
|
|
4
|
+
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-DxASSNjr.mjs";
|
|
5
5
|
import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
|
|
6
|
-
import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-
|
|
6
|
+
import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-BYlyAbH3.mjs";
|
|
7
7
|
import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-DmCbr6DI.mjs";
|
|
8
8
|
import { DokployApi } from "./dokploy-api-2ldYoN3i.mjs";
|
|
9
9
|
import { encryptSecrets } from "./encryption-BOH5M-f-.mjs";
|
|
@@ -21,6 +21,7 @@ import { createServer } from "node:net";
|
|
|
21
21
|
import chokidar from "chokidar";
|
|
22
22
|
import { config } from "dotenv";
|
|
23
23
|
import fg from "fast-glob";
|
|
24
|
+
import { parse as parse$1 } from "yaml";
|
|
24
25
|
import { Cron } from "@geekmidas/constructs/crons";
|
|
25
26
|
import { Function } from "@geekmidas/constructs/functions";
|
|
26
27
|
import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
@@ -32,7 +33,7 @@ import prompts from "prompts";
|
|
|
32
33
|
|
|
33
34
|
//#region package.json
|
|
34
35
|
var name = "@geekmidas/cli";
|
|
35
|
-
var version = "1.
|
|
36
|
+
var version = "1.6.0";
|
|
36
37
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
37
38
|
var private$1 = false;
|
|
38
39
|
var type = "module";
|
|
@@ -78,11 +79,11 @@ var repository = {
|
|
|
78
79
|
};
|
|
79
80
|
var dependencies = {
|
|
80
81
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
81
|
-
"@aws-sdk/client-iam": "~3.
|
|
82
|
-
"@aws-sdk/client-route-53": "~3.
|
|
83
|
-
"@aws-sdk/client-s3": "~3.
|
|
84
|
-
"@aws-sdk/client-ssm": "~3.
|
|
85
|
-
"@aws-sdk/credential-providers": "~3.
|
|
82
|
+
"@aws-sdk/client-iam": "~3.992.0",
|
|
83
|
+
"@aws-sdk/client-route-53": "~3.992.0",
|
|
84
|
+
"@aws-sdk/client-s3": "~3.992.0",
|
|
85
|
+
"@aws-sdk/client-ssm": "~3.992.0",
|
|
86
|
+
"@aws-sdk/credential-providers": "~3.992.0",
|
|
86
87
|
"@geekmidas/constructs": "workspace:~",
|
|
87
88
|
"@geekmidas/envkit": "workspace:~",
|
|
88
89
|
"@geekmidas/errors": "workspace:~",
|
|
@@ -97,7 +98,8 @@ var dependencies = {
|
|
|
97
98
|
"openapi-typescript": "^7.4.2",
|
|
98
99
|
"pg": "~8.17.1",
|
|
99
100
|
"prompts": "~2.4.2",
|
|
100
|
-
"tsx": "~4.20.3"
|
|
101
|
+
"tsx": "~4.20.3",
|
|
102
|
+
"yaml": "~2.8.2"
|
|
101
103
|
};
|
|
102
104
|
var devDependencies = {
|
|
103
105
|
"@geekmidas/testkit": "workspace:*",
|
|
@@ -775,6 +777,152 @@ async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
|
775
777
|
}
|
|
776
778
|
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
777
779
|
}
|
|
780
|
+
const PORT_STATE_PATH = ".gkm/ports.json";
|
|
781
|
+
/**
|
|
782
|
+
* Parse docker-compose.yml and extract all port mappings that use env var interpolation.
|
|
783
|
+
* Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
|
|
784
|
+
* Fixed port mappings like `'5050:80'` are skipped.
|
|
785
|
+
* @internal Exported for testing
|
|
786
|
+
*/
|
|
787
|
+
function parseComposePortMappings(composePath) {
|
|
788
|
+
if (!existsSync(composePath)) return [];
|
|
789
|
+
const content = readFileSync(composePath, "utf-8");
|
|
790
|
+
const compose = parse$1(content);
|
|
791
|
+
if (!compose?.services) return [];
|
|
792
|
+
const results = [];
|
|
793
|
+
for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
|
|
794
|
+
const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
|
|
795
|
+
if (match?.[1] && match[2] && match[3]) results.push({
|
|
796
|
+
service: serviceName,
|
|
797
|
+
envVar: match[1],
|
|
798
|
+
defaultPort: Number(match[2]),
|
|
799
|
+
containerPort: Number(match[3])
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
return results;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Load saved port state from .gkm/ports.json.
|
|
806
|
+
* @internal Exported for testing
|
|
807
|
+
*/
|
|
808
|
+
async function loadPortState(workspaceRoot) {
|
|
809
|
+
try {
|
|
810
|
+
const raw = await readFile(join(workspaceRoot, PORT_STATE_PATH), "utf-8");
|
|
811
|
+
return JSON.parse(raw);
|
|
812
|
+
} catch {
|
|
813
|
+
return {};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Save port state to .gkm/ports.json.
|
|
818
|
+
* @internal Exported for testing
|
|
819
|
+
*/
|
|
820
|
+
async function savePortState(workspaceRoot, ports) {
|
|
821
|
+
const dir = join(workspaceRoot, ".gkm");
|
|
822
|
+
await mkdir(dir, { recursive: true });
|
|
823
|
+
await writeFile(join(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Check if a project's own Docker container is running and return its host port.
|
|
827
|
+
* Uses `docker compose port` scoped to the project's compose file.
|
|
828
|
+
* @internal Exported for testing
|
|
829
|
+
*/
|
|
830
|
+
function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
831
|
+
try {
|
|
832
|
+
const result = execSync(`docker compose port ${service} ${containerPort}`, {
|
|
833
|
+
cwd: workspaceRoot,
|
|
834
|
+
stdio: "pipe"
|
|
835
|
+
}).toString().trim();
|
|
836
|
+
const match = result.match(/:(\d+)$/);
|
|
837
|
+
return match ? Number(match[1]) : null;
|
|
838
|
+
} catch {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Resolve host ports for Docker services by parsing docker-compose.yml.
|
|
844
|
+
* Priority: running container → saved state → find available port.
|
|
845
|
+
* Persists resolved ports to .gkm/ports.json.
|
|
846
|
+
* @internal Exported for testing
|
|
847
|
+
*/
|
|
848
|
+
async function resolveServicePorts(workspaceRoot) {
|
|
849
|
+
const composePath = join(workspaceRoot, "docker-compose.yml");
|
|
850
|
+
const mappings = parseComposePortMappings(composePath);
|
|
851
|
+
if (mappings.length === 0) return {
|
|
852
|
+
dockerEnv: {},
|
|
853
|
+
ports: {},
|
|
854
|
+
mappings: []
|
|
855
|
+
};
|
|
856
|
+
const savedState = await loadPortState(workspaceRoot);
|
|
857
|
+
const dockerEnv = {};
|
|
858
|
+
const ports = {};
|
|
859
|
+
logger$9.log("\n🔌 Resolving service ports...");
|
|
860
|
+
for (const mapping of mappings) {
|
|
861
|
+
const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
|
|
862
|
+
if (containerPort !== null) {
|
|
863
|
+
ports[mapping.envVar] = containerPort;
|
|
864
|
+
dockerEnv[mapping.envVar] = String(containerPort);
|
|
865
|
+
logger$9.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const savedPort = savedState[mapping.envVar];
|
|
869
|
+
if (savedPort && await isPortAvailable(savedPort)) {
|
|
870
|
+
ports[mapping.envVar] = savedPort;
|
|
871
|
+
dockerEnv[mapping.envVar] = String(savedPort);
|
|
872
|
+
logger$9.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const resolvedPort = await findAvailablePort(mapping.defaultPort);
|
|
876
|
+
ports[mapping.envVar] = resolvedPort;
|
|
877
|
+
dockerEnv[mapping.envVar] = String(resolvedPort);
|
|
878
|
+
if (resolvedPort !== mapping.defaultPort) logger$9.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
|
|
879
|
+
else logger$9.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
|
|
880
|
+
}
|
|
881
|
+
await savePortState(workspaceRoot, ports);
|
|
882
|
+
return {
|
|
883
|
+
dockerEnv,
|
|
884
|
+
ports,
|
|
885
|
+
mappings
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Replace a port in a URL string.
|
|
890
|
+
* Handles both `hostname:port` and `localhost:port` patterns.
|
|
891
|
+
* @internal Exported for testing
|
|
892
|
+
*/
|
|
893
|
+
function replacePortInUrl(url, oldPort, newPort) {
|
|
894
|
+
if (oldPort === newPort) return url;
|
|
895
|
+
return url.replace(new RegExp(`:${oldPort}(?=/|$)`, "g"), `:${newPort}`);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Rewrite connection URLs and port vars in secrets with resolved ports.
|
|
899
|
+
* Uses the parsed compose mappings to determine which default ports to replace.
|
|
900
|
+
* Pure transform — does not modify secrets on disk.
|
|
901
|
+
* @internal Exported for testing
|
|
902
|
+
*/
|
|
903
|
+
function rewriteUrlsWithPorts(secrets, resolvedPorts) {
|
|
904
|
+
const { ports, mappings } = resolvedPorts;
|
|
905
|
+
const result = { ...secrets };
|
|
906
|
+
const portReplacements = [];
|
|
907
|
+
for (const mapping of mappings) {
|
|
908
|
+
const resolved = ports[mapping.envVar];
|
|
909
|
+
if (resolved !== void 0) portReplacements.push({
|
|
910
|
+
defaultPort: mapping.defaultPort,
|
|
911
|
+
resolvedPort: resolved
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
for (const [key, value] of Object.entries(result)) {
|
|
915
|
+
if (!key.endsWith("_PORT")) continue;
|
|
916
|
+
for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
|
|
917
|
+
}
|
|
918
|
+
for (const [key, value] of Object.entries(result)) {
|
|
919
|
+
if (!key.endsWith("_URL") && key !== "DATABASE_URL") continue;
|
|
920
|
+
let rewritten = value;
|
|
921
|
+
for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
|
|
922
|
+
result[key] = rewritten;
|
|
923
|
+
}
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
778
926
|
/**
|
|
779
927
|
* Normalize telescope configuration
|
|
780
928
|
* @internal Exported for testing
|
|
@@ -1002,8 +1150,11 @@ async function devCommand(options) {
|
|
|
1002
1150
|
rebuildTimeout = setTimeout(async () => {
|
|
1003
1151
|
try {
|
|
1004
1152
|
logger$9.log("🔄 Rebuilding...");
|
|
1005
|
-
await buildServer(config$1, buildContext, resolved.providers[0], enableOpenApi, appRoot);
|
|
1006
|
-
if (enableOpenApi) await generateOpenApi(config$1, {
|
|
1153
|
+
await buildServer(config$1, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
|
|
1154
|
+
if (enableOpenApi) await generateOpenApi(config$1, {
|
|
1155
|
+
silent: true,
|
|
1156
|
+
bustCache: true
|
|
1157
|
+
});
|
|
1007
1158
|
logger$9.log("✅ Rebuild complete, restarting server...");
|
|
1008
1159
|
await devServer.restart();
|
|
1009
1160
|
} catch (error) {
|
|
@@ -1156,7 +1307,7 @@ async function loadSecretsForApp(secretsRoot, appName) {
|
|
|
1156
1307
|
* Start docker-compose services for the workspace.
|
|
1157
1308
|
* @internal Exported for testing
|
|
1158
1309
|
*/
|
|
1159
|
-
async function startWorkspaceServices(workspace) {
|
|
1310
|
+
async function startWorkspaceServices(workspace, portEnv) {
|
|
1160
1311
|
const services = workspace.services;
|
|
1161
1312
|
if (!services.db && !services.cache && !services.mail) return;
|
|
1162
1313
|
const servicesToStart = [];
|
|
@@ -1173,7 +1324,11 @@ async function startWorkspaceServices(workspace) {
|
|
|
1173
1324
|
}
|
|
1174
1325
|
execSync(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
1175
1326
|
cwd: workspace.root,
|
|
1176
|
-
stdio: "inherit"
|
|
1327
|
+
stdio: "inherit",
|
|
1328
|
+
env: {
|
|
1329
|
+
...process.env,
|
|
1330
|
+
...portEnv
|
|
1331
|
+
}
|
|
1177
1332
|
});
|
|
1178
1333
|
logger$9.log("✅ Services started");
|
|
1179
1334
|
} catch (error) {
|
|
@@ -1221,8 +1376,9 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1221
1376
|
const copiedCount = clientResults.filter((r) => r.success).length;
|
|
1222
1377
|
if (copiedCount > 0) logger$9.log(`\n📦 Copied ${copiedCount} API client(s)`);
|
|
1223
1378
|
}
|
|
1224
|
-
await
|
|
1225
|
-
|
|
1379
|
+
const resolvedPorts = await resolveServicePorts(workspace.root);
|
|
1380
|
+
await startWorkspaceServices(workspace, resolvedPorts.dockerEnv);
|
|
1381
|
+
const secretsEnv = rewriteUrlsWithPorts(await loadDevSecrets(workspace), resolvedPorts);
|
|
1226
1382
|
if (Object.keys(secretsEnv).length > 0) logger$9.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
1227
1383
|
const dependencyEnv = generateAllDependencyEnvVars(workspace);
|
|
1228
1384
|
if (Object.keys(dependencyEnv).length > 0) {
|
|
@@ -1347,16 +1503,16 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1347
1503
|
});
|
|
1348
1504
|
});
|
|
1349
1505
|
}
|
|
1350
|
-
async function buildServer(config$1, context, provider, enableOpenApi, appRoot = process.cwd()) {
|
|
1506
|
+
async function buildServer(config$1, context, provider, enableOpenApi, appRoot = process.cwd(), bustCache = false) {
|
|
1351
1507
|
const endpointGenerator = new EndpointGenerator();
|
|
1352
1508
|
const functionGenerator = new FunctionGenerator();
|
|
1353
1509
|
const cronGenerator = new CronGenerator();
|
|
1354
1510
|
const subscriberGenerator = new SubscriberGenerator();
|
|
1355
1511
|
const [allEndpoints, allFunctions, allCrons, allSubscribers] = await Promise.all([
|
|
1356
|
-
endpointGenerator.load(config$1.routes, appRoot),
|
|
1357
|
-
config$1.functions ? functionGenerator.load(config$1.functions, appRoot) : [],
|
|
1358
|
-
config$1.crons ? cronGenerator.load(config$1.crons, appRoot) : [],
|
|
1359
|
-
config$1.subscribers ? subscriberGenerator.load(config$1.subscribers, appRoot) : []
|
|
1512
|
+
endpointGenerator.load(config$1.routes, appRoot, bustCache),
|
|
1513
|
+
config$1.functions ? functionGenerator.load(config$1.functions, appRoot, bustCache) : [],
|
|
1514
|
+
config$1.crons ? cronGenerator.load(config$1.crons, appRoot, bustCache) : [],
|
|
1515
|
+
config$1.subscribers ? subscriberGenerator.load(config$1.subscribers, appRoot, bustCache) : []
|
|
1360
1516
|
]);
|
|
1361
1517
|
const outputDir = join(appRoot, ".gkm", provider);
|
|
1362
1518
|
await mkdir(outputDir, { recursive: true });
|
|
@@ -1445,10 +1601,10 @@ async function prepareEntryCredentials(options) {
|
|
|
1445
1601
|
let secretsRoot = cwd;
|
|
1446
1602
|
let appName;
|
|
1447
1603
|
try {
|
|
1448
|
-
const
|
|
1449
|
-
workspaceAppPort =
|
|
1450
|
-
secretsRoot =
|
|
1451
|
-
appName =
|
|
1604
|
+
const appInfo = await loadWorkspaceAppInfo(cwd);
|
|
1605
|
+
workspaceAppPort = appInfo.app.port;
|
|
1606
|
+
secretsRoot = appInfo.workspaceRoot;
|
|
1607
|
+
appName = appInfo.appName;
|
|
1452
1608
|
} catch (error) {
|
|
1453
1609
|
logger$9.log(`⚠️ Could not load workspace config: ${error.message}`);
|
|
1454
1610
|
secretsRoot = findSecretsRoot(cwd);
|
|
@@ -1746,17 +1902,39 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1746
1902
|
if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
|
|
1747
1903
|
const defaultEnv = loadEnvFiles(".env");
|
|
1748
1904
|
if (defaultEnv.loaded.length > 0) logger$9.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1749
|
-
const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
|
|
1905
|
+
const { credentials, secretsJsonPath, appName, secretsRoot } = await prepareEntryCredentials({ cwd });
|
|
1750
1906
|
if (appName) logger$9.log(`📦 App: ${appName}`);
|
|
1751
1907
|
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1752
1908
|
if (secretCount > 0) logger$9.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1909
|
+
const composePath = join(secretsRoot, "docker-compose.yml");
|
|
1910
|
+
const mappings = parseComposePortMappings(composePath);
|
|
1911
|
+
if (mappings.length > 0) {
|
|
1912
|
+
const ports = await loadPortState(secretsRoot);
|
|
1913
|
+
if (Object.keys(ports).length > 0) {
|
|
1914
|
+
const rewritten = rewriteUrlsWithPorts(credentials, {
|
|
1915
|
+
dockerEnv: {},
|
|
1916
|
+
ports,
|
|
1917
|
+
mappings
|
|
1918
|
+
});
|
|
1919
|
+
Object.assign(credentials, rewritten);
|
|
1920
|
+
logger$9.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
try {
|
|
1924
|
+
const appInfo = await loadWorkspaceAppInfo(cwd);
|
|
1925
|
+
if (appInfo.appName) {
|
|
1926
|
+
const depEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
1927
|
+
Object.assign(credentials, depEnv);
|
|
1928
|
+
}
|
|
1929
|
+
} catch {}
|
|
1753
1930
|
const preloadDir = join(cwd, ".gkm");
|
|
1754
1931
|
await mkdir(preloadDir, { recursive: true });
|
|
1755
1932
|
const preloadPath = join(preloadDir, "credentials-preload.ts");
|
|
1756
1933
|
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1757
|
-
const [cmd, ...
|
|
1934
|
+
const [cmd, ...rawArgs] = commandArgs;
|
|
1758
1935
|
if (!cmd) throw new Error("No command specified");
|
|
1759
|
-
|
|
1936
|
+
const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
|
|
1937
|
+
logger$9.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1760
1938
|
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
1761
1939
|
const tsxImport = "--import=tsx";
|
|
1762
1940
|
const preloadImport = `--import=${preloadPath}`;
|
|
@@ -6367,18 +6545,18 @@ const GEEKMIDAS_VERSIONS = {
|
|
|
6367
6545
|
"@geekmidas/audit": "~1.0.0",
|
|
6368
6546
|
"@geekmidas/auth": "~1.0.0",
|
|
6369
6547
|
"@geekmidas/cache": "~1.0.0",
|
|
6370
|
-
"@geekmidas/client": "~
|
|
6548
|
+
"@geekmidas/client": "~2.0.0",
|
|
6371
6549
|
"@geekmidas/cloud": "~1.0.0",
|
|
6372
|
-
"@geekmidas/constructs": "~1.0
|
|
6550
|
+
"@geekmidas/constructs": "~1.1.0",
|
|
6373
6551
|
"@geekmidas/db": "~1.0.0",
|
|
6374
6552
|
"@geekmidas/emailkit": "~1.0.0",
|
|
6375
|
-
"@geekmidas/envkit": "~1.0.
|
|
6553
|
+
"@geekmidas/envkit": "~1.0.3",
|
|
6376
6554
|
"@geekmidas/errors": "~1.0.0",
|
|
6377
6555
|
"@geekmidas/events": "~1.0.0",
|
|
6378
6556
|
"@geekmidas/logger": "~1.0.0",
|
|
6379
6557
|
"@geekmidas/rate-limit": "~1.0.0",
|
|
6380
6558
|
"@geekmidas/schema": "~1.0.0",
|
|
6381
|
-
"@geekmidas/services": "~1.0.
|
|
6559
|
+
"@geekmidas/services": "~1.0.1",
|
|
6382
6560
|
"@geekmidas/storage": "~1.0.0",
|
|
6383
6561
|
"@geekmidas/studio": "~1.0.0",
|
|
6384
6562
|
"@geekmidas/telescope": "~1.0.0",
|
|
@@ -6431,6 +6609,7 @@ function generateAuthAppFiles(options) {
|
|
|
6431
6609
|
extends: "../../tsconfig.json",
|
|
6432
6610
|
compilerOptions: {
|
|
6433
6611
|
noEmit: true,
|
|
6612
|
+
allowImportingTsExtensions: true,
|
|
6434
6613
|
baseUrl: ".",
|
|
6435
6614
|
paths: {
|
|
6436
6615
|
"~/*": ["./src/*"],
|
|
@@ -6461,8 +6640,8 @@ export const logger = createLogger();
|
|
|
6461
6640
|
const authTs = `import { betterAuth } from 'better-auth';
|
|
6462
6641
|
import { magicLink } from 'better-auth/plugins';
|
|
6463
6642
|
import pg from 'pg';
|
|
6464
|
-
import { envParser } from './config/env.
|
|
6465
|
-
import { logger } from './config/logger.
|
|
6643
|
+
import { envParser } from './config/env.ts';
|
|
6644
|
+
import { logger } from './config/logger.ts';
|
|
6466
6645
|
|
|
6467
6646
|
// Parse auth-specific config (no defaults - values from secrets)
|
|
6468
6647
|
const authConfig = envParser
|
|
@@ -6505,9 +6684,9 @@ export type Auth = typeof auth;
|
|
|
6505
6684
|
const indexTs = `import { Hono } from 'hono';
|
|
6506
6685
|
import { cors } from 'hono/cors';
|
|
6507
6686
|
import { serve } from '@hono/node-server';
|
|
6508
|
-
import { auth } from './auth.
|
|
6509
|
-
import { envParser } from './config/env.
|
|
6510
|
-
import { logger } from './config/logger.
|
|
6687
|
+
import { auth } from './auth.ts';
|
|
6688
|
+
import { envParser } from './config/env.ts';
|
|
6689
|
+
import { logger } from './config/logger.ts';
|
|
6511
6690
|
|
|
6512
6691
|
// Parse server config (no defaults - values from secrets)
|
|
6513
6692
|
const serverConfig = envParser
|
|
@@ -6593,6 +6772,20 @@ dist/
|
|
|
6593
6772
|
//#endregion
|
|
6594
6773
|
//#region src/init/generators/config.ts
|
|
6595
6774
|
/**
|
|
6775
|
+
* Vitest config content with globalSetup for database-enabled apps
|
|
6776
|
+
*/
|
|
6777
|
+
const vitestConfigContent = `import { defineConfig } from 'vitest/config';
|
|
6778
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
6779
|
+
|
|
6780
|
+
export default defineConfig({
|
|
6781
|
+
plugins: [tsconfigPaths()],
|
|
6782
|
+
test: {
|
|
6783
|
+
environment: 'node',
|
|
6784
|
+
globalSetup: './test/globalSetup.ts',
|
|
6785
|
+
},
|
|
6786
|
+
});
|
|
6787
|
+
`;
|
|
6788
|
+
/**
|
|
6596
6789
|
* Generate configuration files (gkm.config.ts, tsconfig.json, biome.json, turbo.json)
|
|
6597
6790
|
*/
|
|
6598
6791
|
function generateConfigFiles(options, template) {
|
|
@@ -6644,6 +6837,7 @@ export default defineConfig({
|
|
|
6644
6837
|
extends: "../../tsconfig.json",
|
|
6645
6838
|
compilerOptions: {
|
|
6646
6839
|
noEmit: true,
|
|
6840
|
+
allowImportingTsExtensions: true,
|
|
6647
6841
|
baseUrl: ".",
|
|
6648
6842
|
paths: {
|
|
6649
6843
|
"~/*": ["./src/*"],
|
|
@@ -6663,21 +6857,26 @@ export default defineConfig({
|
|
|
6663
6857
|
skipLibCheck: true,
|
|
6664
6858
|
forceConsistentCasingInFileNames: true,
|
|
6665
6859
|
resolveJsonModule: true,
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
outDir: "./dist",
|
|
6669
|
-
rootDir: "./src"
|
|
6860
|
+
noEmit: true,
|
|
6861
|
+
allowImportingTsExtensions: true
|
|
6670
6862
|
},
|
|
6671
6863
|
include: ["src/**/*.ts"],
|
|
6672
6864
|
exclude: ["node_modules", "dist"]
|
|
6673
6865
|
};
|
|
6674
|
-
if (options.monorepo)
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6866
|
+
if (options.monorepo) {
|
|
6867
|
+
const files$1 = [{
|
|
6868
|
+
path: "gkm.config.ts",
|
|
6869
|
+
content: gkmConfig
|
|
6870
|
+
}, {
|
|
6871
|
+
path: "tsconfig.json",
|
|
6872
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
6873
|
+
}];
|
|
6874
|
+
if (options.database) files$1.push({
|
|
6875
|
+
path: "vitest.config.ts",
|
|
6876
|
+
content: vitestConfigContent
|
|
6877
|
+
});
|
|
6878
|
+
return files$1;
|
|
6879
|
+
}
|
|
6681
6880
|
const biomeConfig = {
|
|
6682
6881
|
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
6683
6882
|
vcs: {
|
|
@@ -6743,7 +6942,7 @@ export default defineConfig({
|
|
|
6743
6942
|
fmt: { outputs: [] }
|
|
6744
6943
|
}
|
|
6745
6944
|
};
|
|
6746
|
-
|
|
6945
|
+
const files = [
|
|
6747
6946
|
{
|
|
6748
6947
|
path: "gkm.config.ts",
|
|
6749
6948
|
content: gkmConfig
|
|
@@ -6761,12 +6960,18 @@ export default defineConfig({
|
|
|
6761
6960
|
content: `${JSON.stringify(turboConfig, null, 2)}\n`
|
|
6762
6961
|
}
|
|
6763
6962
|
];
|
|
6963
|
+
if (options.database) files.push({
|
|
6964
|
+
path: "vitest.config.ts",
|
|
6965
|
+
content: vitestConfigContent
|
|
6966
|
+
});
|
|
6967
|
+
return files;
|
|
6764
6968
|
}
|
|
6765
6969
|
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
6766
6970
|
const tsConfig = {
|
|
6767
6971
|
extends: "../../tsconfig.json",
|
|
6768
6972
|
compilerOptions: {
|
|
6769
6973
|
noEmit: true,
|
|
6974
|
+
allowImportingTsExtensions: true,
|
|
6770
6975
|
baseUrl: ".",
|
|
6771
6976
|
paths: {
|
|
6772
6977
|
"~/*": ["./src/*"],
|
|
@@ -6776,10 +6981,15 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
6776
6981
|
include: ["src/**/*.ts"],
|
|
6777
6982
|
exclude: ["node_modules", "dist"]
|
|
6778
6983
|
};
|
|
6779
|
-
|
|
6984
|
+
const files = [{
|
|
6780
6985
|
path: "tsconfig.json",
|
|
6781
6986
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
6782
6987
|
}];
|
|
6988
|
+
if (options.database) files.push({
|
|
6989
|
+
path: "vitest.config.ts",
|
|
6990
|
+
content: vitestConfigContent
|
|
6991
|
+
});
|
|
6992
|
+
return files;
|
|
6783
6993
|
}
|
|
6784
6994
|
|
|
6785
6995
|
//#endregion
|
|
@@ -6810,7 +7020,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6810
7020
|
POSTGRES_PASSWORD: postgres
|
|
6811
7021
|
POSTGRES_DB: ${options.name.replace(/-/g, "_")}_dev
|
|
6812
7022
|
ports:
|
|
6813
|
-
- '5432:5432'
|
|
7023
|
+
- '\${POSTGRES_HOST_PORT:-5432}:5432'
|
|
6814
7024
|
volumes:
|
|
6815
7025
|
- postgres_data:/var/lib/postgresql/data${initVolume}
|
|
6816
7026
|
healthcheck:
|
|
@@ -6836,7 +7046,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6836
7046
|
container_name: ${options.name}-redis
|
|
6837
7047
|
restart: unless-stopped
|
|
6838
7048
|
ports:
|
|
6839
|
-
- '6379:6379'
|
|
7049
|
+
- '\${REDIS_HOST_PORT:-6379}:6379'
|
|
6840
7050
|
volumes:
|
|
6841
7051
|
- redis_data:/data
|
|
6842
7052
|
healthcheck:
|
|
@@ -6850,7 +7060,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6850
7060
|
container_name: ${options.name}-serverless-redis
|
|
6851
7061
|
restart: unless-stopped
|
|
6852
7062
|
ports:
|
|
6853
|
-
- '8079:80'
|
|
7063
|
+
- '\${SRH_HOST_PORT:-8079}:80'
|
|
6854
7064
|
environment:
|
|
6855
7065
|
SRH_MODE: env
|
|
6856
7066
|
SRH_TOKEN: local_dev_token
|
|
@@ -6865,7 +7075,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6865
7075
|
container_name: ${options.name}-redis
|
|
6866
7076
|
restart: unless-stopped
|
|
6867
7077
|
ports:
|
|
6868
|
-
- '6379:6379'
|
|
7078
|
+
- '\${REDIS_HOST_PORT:-6379}:6379'
|
|
6869
7079
|
volumes:
|
|
6870
7080
|
- redis_data:/data
|
|
6871
7081
|
healthcheck:
|
|
@@ -6881,8 +7091,8 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6881
7091
|
container_name: ${options.name}-rabbitmq
|
|
6882
7092
|
restart: unless-stopped
|
|
6883
7093
|
ports:
|
|
6884
|
-
- '5672:5672'
|
|
6885
|
-
- '15672:15672'
|
|
7094
|
+
- '\${RABBITMQ_HOST_PORT:-5672}:5672'
|
|
7095
|
+
- '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
|
|
6886
7096
|
environment:
|
|
6887
7097
|
RABBITMQ_DEFAULT_USER: guest
|
|
6888
7098
|
RABBITMQ_DEFAULT_PASS: guest
|
|
@@ -6900,8 +7110,8 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
6900
7110
|
container_name: ${options.name}-mailpit
|
|
6901
7111
|
restart: unless-stopped
|
|
6902
7112
|
ports:
|
|
6903
|
-
- '1025:1025'
|
|
6904
|
-
- '8025:8025'
|
|
7113
|
+
- '\${MAILPIT_SMTP_HOST_PORT:-1025}:1025'
|
|
7114
|
+
- '\${MAILPIT_UI_HOST_PORT:-8025}:8025'
|
|
6905
7115
|
environment:
|
|
6906
7116
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
|
6907
7117
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1`);
|
|
@@ -7055,10 +7265,8 @@ function generateModelsPackage(options) {
|
|
|
7055
7265
|
const tsConfig = {
|
|
7056
7266
|
extends: "../../tsconfig.json",
|
|
7057
7267
|
compilerOptions: {
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
outDir: "./dist",
|
|
7061
|
-
rootDir: "./src"
|
|
7268
|
+
noEmit: true,
|
|
7269
|
+
allowImportingTsExtensions: true
|
|
7062
7270
|
},
|
|
7063
7271
|
include: ["src/**/*.ts"],
|
|
7064
7272
|
exclude: ["node_modules", "dist"]
|
|
@@ -7104,7 +7312,7 @@ export type Timestamps = z.infer<typeof TimestampsSchema>;
|
|
|
7104
7312
|
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
7105
7313
|
`;
|
|
7106
7314
|
const userTs = `import { z } from 'zod';
|
|
7107
|
-
import { IdSchema, TimestampsSchema } from './common.
|
|
7315
|
+
import { IdSchema, TimestampsSchema } from './common.ts';
|
|
7108
7316
|
|
|
7109
7317
|
// ============================================
|
|
7110
7318
|
// User Schemas
|
|
@@ -7595,7 +7803,7 @@ export const config = envParser
|
|
|
7595
7803
|
{
|
|
7596
7804
|
path: getRoutePath("health.ts"),
|
|
7597
7805
|
content: monorepo ? `import { z } from 'zod';
|
|
7598
|
-
import { publicRouter } from '~/router';
|
|
7806
|
+
import { publicRouter } from '~/router.ts';
|
|
7599
7807
|
|
|
7600
7808
|
export const healthEndpoint = publicRouter
|
|
7601
7809
|
.get('/health')
|
|
@@ -7738,8 +7946,8 @@ export const authService = {
|
|
|
7738
7946
|
path: "src/router.ts",
|
|
7739
7947
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
7740
7948
|
import { UnauthorizedError } from '@geekmidas/errors';
|
|
7741
|
-
import { authService, type Session } from './services/auth.
|
|
7742
|
-
import { logger } from './config/logger.
|
|
7949
|
+
import { authService, type Session } from './services/auth.ts';
|
|
7950
|
+
import { logger } from './config/logger.ts';
|
|
7743
7951
|
|
|
7744
7952
|
// Public router - no auth required
|
|
7745
7953
|
export const publicRouter = e.logger(logger);
|
|
@@ -7763,7 +7971,7 @@ export const sessionRouter = r.session<Session>(async ({ services, header }) =>
|
|
|
7763
7971
|
files.push({
|
|
7764
7972
|
path: getRoutePath("profile.ts"),
|
|
7765
7973
|
content: `import { z } from 'zod';
|
|
7766
|
-
import { sessionRouter } from '~/router';
|
|
7974
|
+
import { sessionRouter } from '~/router.ts';
|
|
7767
7975
|
|
|
7768
7976
|
export const profileEndpoint = sessionRouter
|
|
7769
7977
|
.get('/profile')
|
|
@@ -7832,8 +8040,8 @@ export const telescope = new Telescope({
|
|
|
7832
8040
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
7833
8041
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
7834
8042
|
import pg from 'pg';
|
|
7835
|
-
import type { Database } from '
|
|
7836
|
-
import { envParser } from '
|
|
8043
|
+
import type { Database } from '~/services/database.ts';
|
|
8044
|
+
import { envParser } from '~/config/env.ts';
|
|
7837
8045
|
|
|
7838
8046
|
// Parse database config for Studio
|
|
7839
8047
|
const studioConfig = envParser
|
|
@@ -8006,8 +8214,8 @@ export const telescope = new Telescope({
|
|
|
8006
8214
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
8007
8215
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
8008
8216
|
import pg from 'pg';
|
|
8009
|
-
import type { Database } from '
|
|
8010
|
-
import { envParser } from '
|
|
8217
|
+
import type { Database } from '~/services/database.ts';
|
|
8218
|
+
import { envParser } from '~/config/env.ts';
|
|
8011
8219
|
|
|
8012
8220
|
// Parse database config for Studio
|
|
8013
8221
|
const studioConfig = envParser
|
|
@@ -8263,7 +8471,7 @@ export type AppEvents =
|
|
|
8263
8471
|
path: "src/events/publisher.ts",
|
|
8264
8472
|
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
8265
8473
|
import { Publisher, type EventPublisher } from '@geekmidas/events';
|
|
8266
|
-
import type { AppEvents } from './types.
|
|
8474
|
+
import type { AppEvents } from './types.ts';
|
|
8267
8475
|
|
|
8268
8476
|
export const eventsPublisherService = {
|
|
8269
8477
|
serviceName: 'events' as const,
|
|
@@ -8290,7 +8498,7 @@ export const eventsPublisherService = {
|
|
|
8290
8498
|
{
|
|
8291
8499
|
path: "src/subscribers/user-events.ts",
|
|
8292
8500
|
content: `import { s } from '@geekmidas/constructs/subscribers';
|
|
8293
|
-
import { eventsPublisherService } from '
|
|
8501
|
+
import { eventsPublisherService } from '~/events/publisher.ts';
|
|
8294
8502
|
|
|
8295
8503
|
export const userEventsSubscriber = s
|
|
8296
8504
|
.publisher(eventsPublisherService)
|
|
@@ -8492,6 +8700,9 @@ function generatePackageJson(options, template) {
|
|
|
8492
8700
|
dependencies$1.kysely = "~0.28.2";
|
|
8493
8701
|
dependencies$1.pg = "~8.16.0";
|
|
8494
8702
|
devDependencies$1["@types/pg"] = "~8.15.0";
|
|
8703
|
+
devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
|
|
8704
|
+
devDependencies$1["@faker-js/faker"] = "~9.8.0";
|
|
8705
|
+
devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
|
|
8495
8706
|
}
|
|
8496
8707
|
if (monorepo) {
|
|
8497
8708
|
delete devDependencies$1["@biomejs/biome"];
|
|
@@ -8538,6 +8749,118 @@ function generateSourceFiles(options, template) {
|
|
|
8538
8749
|
return template.files(options);
|
|
8539
8750
|
}
|
|
8540
8751
|
|
|
8752
|
+
//#endregion
|
|
8753
|
+
//#region src/init/generators/test.ts
|
|
8754
|
+
/**
|
|
8755
|
+
* Generate test infrastructure files when database is enabled.
|
|
8756
|
+
* Includes transaction-isolated test config, global setup with migrations,
|
|
8757
|
+
* factory system with builders/seeds, and an example spec.
|
|
8758
|
+
*/
|
|
8759
|
+
function generateTestFiles(options, _template) {
|
|
8760
|
+
if (!options.database) return [];
|
|
8761
|
+
return [
|
|
8762
|
+
{
|
|
8763
|
+
path: "test/config.ts",
|
|
8764
|
+
content: `import { it as itVitest } from 'vitest';
|
|
8765
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
8766
|
+
import pg from 'pg';
|
|
8767
|
+
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
|
|
8768
|
+
import type { Database } from '~/services/database.ts';
|
|
8769
|
+
|
|
8770
|
+
const connection = new Kysely<Database>({
|
|
8771
|
+
dialect: new PostgresDialect({
|
|
8772
|
+
pool: new pg.Pool({ connectionString: process.env.DATABASE_URL }),
|
|
8773
|
+
}),
|
|
8774
|
+
});
|
|
8775
|
+
|
|
8776
|
+
export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
|
|
8777
|
+
connection,
|
|
8778
|
+
});
|
|
8779
|
+
`
|
|
8780
|
+
},
|
|
8781
|
+
{
|
|
8782
|
+
path: "test/globalSetup.ts",
|
|
8783
|
+
content: `import { Kysely, PostgresDialect } from 'kysely';
|
|
8784
|
+
import pg from 'pg';
|
|
8785
|
+
import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
|
|
8786
|
+
import type { Database } from '~/services/database.ts';
|
|
8787
|
+
|
|
8788
|
+
export async function setup() {
|
|
8789
|
+
const testUrl = process.env.DATABASE_URL;
|
|
8790
|
+
if (!testUrl) throw new Error('DATABASE_URL is required for tests');
|
|
8791
|
+
|
|
8792
|
+
// Run migrations on the test database
|
|
8793
|
+
// (gkm test already rewrites DATABASE_URL to point to the _test database)
|
|
8794
|
+
const db = new Kysely<Database>({
|
|
8795
|
+
dialect: new PostgresDialect({
|
|
8796
|
+
pool: new pg.Pool({ connectionString: testUrl }),
|
|
8797
|
+
}),
|
|
8798
|
+
});
|
|
8799
|
+
|
|
8800
|
+
const migrator = new PostgresKyselyMigrator({
|
|
8801
|
+
db,
|
|
8802
|
+
migrationsPath: './src/migrations',
|
|
8803
|
+
});
|
|
8804
|
+
|
|
8805
|
+
await migrator.migrateToLatest();
|
|
8806
|
+
await db.destroy();
|
|
8807
|
+
}
|
|
8808
|
+
`
|
|
8809
|
+
},
|
|
8810
|
+
{
|
|
8811
|
+
path: "test/factory/index.ts",
|
|
8812
|
+
content: `import type { Kysely } from 'kysely';
|
|
8813
|
+
import { KyselyFactory } from '@geekmidas/testkit/kysely';
|
|
8814
|
+
import type { Database } from '~/services/database.ts';
|
|
8815
|
+
import { usersBuilder } from './users.ts';
|
|
8816
|
+
|
|
8817
|
+
const builders = { users: usersBuilder };
|
|
8818
|
+
const seeds = {};
|
|
8819
|
+
|
|
8820
|
+
export function createFactory(db: Kysely<Database>) {
|
|
8821
|
+
return new KyselyFactory<Database, typeof builders, typeof seeds>(
|
|
8822
|
+
builders,
|
|
8823
|
+
seeds,
|
|
8824
|
+
db,
|
|
8825
|
+
);
|
|
8826
|
+
}
|
|
8827
|
+
|
|
8828
|
+
export type Factory = ReturnType<typeof createFactory>;
|
|
8829
|
+
`
|
|
8830
|
+
},
|
|
8831
|
+
{
|
|
8832
|
+
path: "test/factory/users.ts",
|
|
8833
|
+
content: `import { KyselyFactory } from '@geekmidas/testkit/kysely';
|
|
8834
|
+
import type { Database } from '~/services/database.ts';
|
|
8835
|
+
|
|
8836
|
+
export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
|
|
8837
|
+
'users',
|
|
8838
|
+
({ faker }) => ({
|
|
8839
|
+
id: faker.string.uuid(),
|
|
8840
|
+
name: faker.person.fullName(),
|
|
8841
|
+
email: faker.internet.email(),
|
|
8842
|
+
created_at: new Date(),
|
|
8843
|
+
}),
|
|
8844
|
+
);
|
|
8845
|
+
`
|
|
8846
|
+
},
|
|
8847
|
+
{
|
|
8848
|
+
path: "test/example.spec.ts",
|
|
8849
|
+
content: `import { describe, expect } from 'vitest';
|
|
8850
|
+
import { it } from './config.ts';
|
|
8851
|
+
|
|
8852
|
+
describe('example', () => {
|
|
8853
|
+
it('should have a working test setup', async ({ db }) => {
|
|
8854
|
+
// db is a transaction-wrapped Kysely instance
|
|
8855
|
+
// All changes are automatically rolled back after the test
|
|
8856
|
+
expect(db).toBeDefined();
|
|
8857
|
+
});
|
|
8858
|
+
});
|
|
8859
|
+
`
|
|
8860
|
+
}
|
|
8861
|
+
];
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8541
8864
|
//#endregion
|
|
8542
8865
|
//#region src/init/generators/ui.ts
|
|
8543
8866
|
/**
|
|
@@ -8607,6 +8930,7 @@ function generateUiPackageFiles(options) {
|
|
|
8607
8930
|
"DOM.Iterable"
|
|
8608
8931
|
],
|
|
8609
8932
|
noEmit: true,
|
|
8933
|
+
allowImportingTsExtensions: true,
|
|
8610
8934
|
baseUrl: ".",
|
|
8611
8935
|
paths: { "~/*": ["./src/*"] }
|
|
8612
8936
|
},
|
|
@@ -9780,8 +10104,8 @@ export const Alert: Story = {
|
|
|
9780
10104
|
),
|
|
9781
10105
|
};
|
|
9782
10106
|
`;
|
|
9783
|
-
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
|
|
9784
|
-
export { Input } from './input';
|
|
10107
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
|
|
10108
|
+
export { Input } from './input.tsx';
|
|
9785
10109
|
export {
|
|
9786
10110
|
Card,
|
|
9787
10111
|
CardHeader,
|
|
@@ -9789,17 +10113,17 @@ export {
|
|
|
9789
10113
|
CardTitle,
|
|
9790
10114
|
CardDescription,
|
|
9791
10115
|
CardContent,
|
|
9792
|
-
} from './card';
|
|
9793
|
-
export { Label } from './label';
|
|
9794
|
-
export { Badge, type BadgeProps, badgeVariants } from './badge';
|
|
9795
|
-
export { Separator } from './separator';
|
|
9796
|
-
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
|
|
10116
|
+
} from './card.tsx';
|
|
10117
|
+
export { Label } from './label.tsx';
|
|
10118
|
+
export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
|
|
10119
|
+
export { Separator } from './separator.tsx';
|
|
10120
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
|
|
9797
10121
|
export {
|
|
9798
10122
|
Tooltip,
|
|
9799
10123
|
TooltipTrigger,
|
|
9800
10124
|
TooltipContent,
|
|
9801
10125
|
TooltipProvider,
|
|
9802
|
-
} from './tooltip';
|
|
10126
|
+
} from './tooltip.tsx';
|
|
9803
10127
|
export {
|
|
9804
10128
|
Dialog,
|
|
9805
10129
|
DialogPortal,
|
|
@@ -9811,20 +10135,20 @@ export {
|
|
|
9811
10135
|
DialogFooter,
|
|
9812
10136
|
DialogTitle,
|
|
9813
10137
|
DialogDescription,
|
|
9814
|
-
} from './dialog';
|
|
10138
|
+
} from './dialog.tsx';
|
|
9815
10139
|
`;
|
|
9816
10140
|
const buttonIndexTsx = buttonTsx;
|
|
9817
10141
|
const inputIndexTsx = inputTsx;
|
|
9818
10142
|
const cardIndexTsx = cardTsx;
|
|
9819
|
-
const componentsIndex = `export * from './ui';
|
|
10143
|
+
const componentsIndex = `export * from './ui/index.ts';
|
|
9820
10144
|
`;
|
|
9821
10145
|
const indexTs = `// @${options.name}/ui - Shared UI component library
|
|
9822
10146
|
|
|
9823
10147
|
// shadcn/ui components
|
|
9824
|
-
export * from './components';
|
|
10148
|
+
export * from './components/index.ts';
|
|
9825
10149
|
|
|
9826
10150
|
// Utilities
|
|
9827
|
-
export { cn } from './lib/utils';
|
|
10151
|
+
export { cn } from './lib/utils.ts';
|
|
9828
10152
|
`;
|
|
9829
10153
|
const gitignore = `node_modules/
|
|
9830
10154
|
dist/
|
|
@@ -9967,7 +10291,7 @@ function generateWebAppFiles(options) {
|
|
|
9967
10291
|
private: true,
|
|
9968
10292
|
type: "module",
|
|
9969
10293
|
scripts: {
|
|
9970
|
-
dev: "gkm exec -- next dev --turbopack",
|
|
10294
|
+
dev: "gkm exec -- next dev --turbopack -p $PORT",
|
|
9971
10295
|
build: "gkm exec -- next build",
|
|
9972
10296
|
start: "next start",
|
|
9973
10297
|
typecheck: "tsc --noEmit"
|
|
@@ -10022,6 +10346,7 @@ export default nextConfig;
|
|
|
10022
10346
|
skipLibCheck: true,
|
|
10023
10347
|
strict: true,
|
|
10024
10348
|
noEmit: true,
|
|
10349
|
+
allowImportingTsExtensions: true,
|
|
10025
10350
|
esModuleInterop: true,
|
|
10026
10351
|
module: "ESNext",
|
|
10027
10352
|
moduleResolution: "bundler",
|
|
@@ -10103,7 +10428,7 @@ export const serverConfig = envParser
|
|
|
10103
10428
|
`;
|
|
10104
10429
|
const authClientTs = `import { createAuthClient } from 'better-auth/react';
|
|
10105
10430
|
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
10106
|
-
import { clientConfig } from '~/config/client';
|
|
10431
|
+
import { clientConfig } from '~/config/client.ts';
|
|
10107
10432
|
|
|
10108
10433
|
export const authClient = createAuthClient({
|
|
10109
10434
|
baseURL: clientConfig.authUrl,
|
|
@@ -10115,7 +10440,7 @@ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
|
|
|
10115
10440
|
const providersTsx = `'use client';
|
|
10116
10441
|
|
|
10117
10442
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
10118
|
-
import { getQueryClient } from '~/lib/query-client';
|
|
10443
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
10119
10444
|
|
|
10120
10445
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
10121
10446
|
const queryClient = getQueryClient();
|
|
@@ -10125,9 +10450,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
10125
10450
|
);
|
|
10126
10451
|
}
|
|
10127
10452
|
`;
|
|
10128
|
-
const apiIndexTs = `import { createApi } from './
|
|
10129
|
-
import { getQueryClient } from '~/lib/query-client';
|
|
10130
|
-
import { clientConfig } from '~/config/client';
|
|
10453
|
+
const apiIndexTs = `import { createApi } from './api.ts';
|
|
10454
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
10455
|
+
import { clientConfig } from '~/config/client.ts';
|
|
10131
10456
|
|
|
10132
10457
|
export const api = createApi({
|
|
10133
10458
|
baseURL: clientConfig.apiUrl,
|
|
@@ -10137,7 +10462,7 @@ export const api = createApi({
|
|
|
10137
10462
|
const globalsCss = `@import '${uiPackage}/styles';
|
|
10138
10463
|
`;
|
|
10139
10464
|
const layoutTsx = `import type { Metadata } from 'next';
|
|
10140
|
-
import { Providers } from './providers';
|
|
10465
|
+
import { Providers } from './providers.tsx';
|
|
10141
10466
|
import './globals.css';
|
|
10142
10467
|
|
|
10143
10468
|
export const metadata: Metadata = {
|
|
@@ -10159,7 +10484,7 @@ export default function RootLayout({
|
|
|
10159
10484
|
);
|
|
10160
10485
|
}
|
|
10161
10486
|
`;
|
|
10162
|
-
const pageTsx = `import { api } from '~/api';
|
|
10487
|
+
const pageTsx = `import { api } from '~/api/index.ts';
|
|
10163
10488
|
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
|
|
10164
10489
|
|
|
10165
10490
|
export default async function Home() {
|
|
@@ -10502,6 +10827,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
10502
10827
|
...generateConfigFiles(templateOptions, baseTemplate),
|
|
10503
10828
|
...generateEnvFiles(templateOptions, baseTemplate),
|
|
10504
10829
|
...generateSourceFiles(templateOptions, baseTemplate),
|
|
10830
|
+
...generateTestFiles(templateOptions, baseTemplate),
|
|
10505
10831
|
...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
|
|
10506
10832
|
] : [];
|
|
10507
10833
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
@@ -10845,23 +11171,61 @@ function maskUrl(url) {
|
|
|
10845
11171
|
//#endregion
|
|
10846
11172
|
//#region src/test/index.ts
|
|
10847
11173
|
/**
|
|
10848
|
-
* Run tests with secrets
|
|
10849
|
-
*
|
|
11174
|
+
* Run tests with secrets, dependency URLs, and .env files loaded.
|
|
11175
|
+
* Environment variables are sniffed to inject only what the app needs.
|
|
10850
11176
|
*/
|
|
10851
11177
|
async function testCommand(options = {}) {
|
|
10852
11178
|
const stage = options.stage ?? "development";
|
|
10853
|
-
|
|
10854
|
-
|
|
11179
|
+
const cwd = process.cwd();
|
|
11180
|
+
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
11181
|
+
const defaultEnv = loadEnvFiles(".env");
|
|
11182
|
+
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
11183
|
+
let secretsEnv = {};
|
|
10855
11184
|
try {
|
|
10856
11185
|
const secrets = await readStageSecrets(stage);
|
|
10857
11186
|
if (secrets) {
|
|
10858
|
-
|
|
10859
|
-
console.log(` Loaded ${Object.keys(
|
|
10860
|
-
} else console.log(` No secrets found for ${stage}
|
|
11187
|
+
secretsEnv = toEmbeddableSecrets(secrets);
|
|
11188
|
+
console.log(` 🔐 Loaded ${Object.keys(secretsEnv).length} secrets from ${stage}`);
|
|
11189
|
+
} else console.log(` No secrets found for ${stage}`);
|
|
10861
11190
|
} catch (error) {
|
|
10862
|
-
if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}
|
|
11191
|
+
if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}`);
|
|
10863
11192
|
else throw error;
|
|
10864
11193
|
}
|
|
11194
|
+
const composePath = join(cwd, "docker-compose.yml");
|
|
11195
|
+
const mappings = parseComposePortMappings(composePath);
|
|
11196
|
+
if (mappings.length > 0) {
|
|
11197
|
+
const ports = await loadPortState(cwd);
|
|
11198
|
+
if (Object.keys(ports).length > 0) {
|
|
11199
|
+
secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
|
|
11200
|
+
dockerEnv: {},
|
|
11201
|
+
ports,
|
|
11202
|
+
mappings
|
|
11203
|
+
});
|
|
11204
|
+
console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
|
|
11208
|
+
await ensureTestDatabase(secretsEnv);
|
|
11209
|
+
let dependencyEnv = {};
|
|
11210
|
+
try {
|
|
11211
|
+
const appInfo = await loadWorkspaceAppInfo(cwd);
|
|
11212
|
+
dependencyEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
11213
|
+
if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
|
|
11214
|
+
const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
|
|
11215
|
+
if (sniffed.requiredEnvVars.length > 0) {
|
|
11216
|
+
const needed = new Set(sniffed.requiredEnvVars);
|
|
11217
|
+
const allEnv = {
|
|
11218
|
+
...secretsEnv,
|
|
11219
|
+
...dependencyEnv
|
|
11220
|
+
};
|
|
11221
|
+
const filteredEnv = {};
|
|
11222
|
+
for (const [key, value] of Object.entries(allEnv)) if (needed.has(key)) filteredEnv[key] = value;
|
|
11223
|
+
secretsEnv = {};
|
|
11224
|
+
dependencyEnv = filteredEnv;
|
|
11225
|
+
console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
|
|
11226
|
+
}
|
|
11227
|
+
} catch {}
|
|
11228
|
+
console.log("");
|
|
10865
11229
|
const args = [];
|
|
10866
11230
|
if (options.run) args.push("run");
|
|
10867
11231
|
else if (options.watch) args.push("--watch");
|
|
@@ -10869,11 +11233,12 @@ async function testCommand(options = {}) {
|
|
|
10869
11233
|
if (options.ui) args.push("--ui");
|
|
10870
11234
|
if (options.pattern) args.push(options.pattern);
|
|
10871
11235
|
const vitestProcess = spawn("npx", ["vitest", ...args], {
|
|
10872
|
-
cwd
|
|
11236
|
+
cwd,
|
|
10873
11237
|
stdio: "inherit",
|
|
10874
11238
|
env: {
|
|
10875
11239
|
...process.env,
|
|
10876
|
-
...
|
|
11240
|
+
...secretsEnv,
|
|
11241
|
+
...dependencyEnv,
|
|
10877
11242
|
NODE_ENV: "test"
|
|
10878
11243
|
}
|
|
10879
11244
|
});
|
|
@@ -10887,6 +11252,57 @@ async function testCommand(options = {}) {
|
|
|
10887
11252
|
});
|
|
10888
11253
|
});
|
|
10889
11254
|
}
|
|
11255
|
+
const TEST_DB_SUFFIX = "_test";
|
|
11256
|
+
/**
|
|
11257
|
+
* Rewrite DATABASE_URL to point to a separate test database.
|
|
11258
|
+
* Appends `_test` to the database name (e.g., `app` -> `app_test`).
|
|
11259
|
+
* @internal Exported for testing
|
|
11260
|
+
*/
|
|
11261
|
+
function rewriteDatabaseUrlForTests(env) {
|
|
11262
|
+
const result = { ...env };
|
|
11263
|
+
for (const key of Object.keys(result)) {
|
|
11264
|
+
if (!key.includes("DATABASE_URL")) continue;
|
|
11265
|
+
const value = result[key];
|
|
11266
|
+
try {
|
|
11267
|
+
const url = new URL(value);
|
|
11268
|
+
const dbName = url.pathname.slice(1);
|
|
11269
|
+
if (dbName && !dbName.endsWith(TEST_DB_SUFFIX)) {
|
|
11270
|
+
url.pathname = `/${dbName}${TEST_DB_SUFFIX}`;
|
|
11271
|
+
result[key] = url.toString();
|
|
11272
|
+
console.log(` 🧪 ${key}: using test database "${dbName}${TEST_DB_SUFFIX}"`);
|
|
11273
|
+
}
|
|
11274
|
+
} catch {}
|
|
11275
|
+
}
|
|
11276
|
+
return result;
|
|
11277
|
+
}
|
|
11278
|
+
/**
|
|
11279
|
+
* Ensure the test database exists by connecting to the default database
|
|
11280
|
+
* and running CREATE DATABASE IF NOT EXISTS.
|
|
11281
|
+
* @internal Exported for testing
|
|
11282
|
+
*/
|
|
11283
|
+
async function ensureTestDatabase(env) {
|
|
11284
|
+
const databaseUrl = env.DATABASE_URL;
|
|
11285
|
+
if (!databaseUrl) return;
|
|
11286
|
+
try {
|
|
11287
|
+
const url = new URL(databaseUrl);
|
|
11288
|
+
const testDbName = url.pathname.slice(1);
|
|
11289
|
+
if (!testDbName) return;
|
|
11290
|
+
url.pathname = "/postgres";
|
|
11291
|
+
const { default: pg } = await import("pg");
|
|
11292
|
+
const client = new pg.Client({ connectionString: url.toString() });
|
|
11293
|
+
await client.connect();
|
|
11294
|
+
try {
|
|
11295
|
+
await client.query(`CREATE DATABASE "${testDbName}"`);
|
|
11296
|
+
console.log(` 📦 Created test database "${testDbName}"`);
|
|
11297
|
+
} catch (err) {
|
|
11298
|
+
if (err.code !== "42P04") throw err;
|
|
11299
|
+
} finally {
|
|
11300
|
+
await client.end();
|
|
11301
|
+
}
|
|
11302
|
+
} catch (err) {
|
|
11303
|
+
console.log(` ⚠️ Could not ensure test database: ${err.message}`);
|
|
11304
|
+
}
|
|
11305
|
+
}
|
|
10890
11306
|
|
|
10891
11307
|
//#endregion
|
|
10892
11308
|
//#region src/index.ts
|