@geekmidas/cli 0.54.0 → 1.0.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/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2223 -606
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2200 -583
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
- package/src/deploy/__tests__/sniffer.spec.ts +4 -20
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +77 -55
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
|
-
import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-
|
|
3
|
-
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-DAxG3_H2.mjs";
|
|
3
|
+
import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-C6awcFBx.mjs";
|
|
4
|
+
import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-DT1dSxIx.mjs";
|
|
5
|
+
import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, OpenApiTsGenerator, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-BMFmLnX6.mjs";
|
|
6
|
+
import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-BMW6yLu3.mjs";
|
|
7
|
+
import { DokployApi } from "./dokploy-api-7k3t7_zd.mjs";
|
|
8
|
+
import { encryptSecrets } from "./encryption-JtMsiGNp.mjs";
|
|
9
|
+
import { CachedStateProvider } from "./CachedStateProvider-DVyKfaMm.mjs";
|
|
10
|
+
import { generateReactQueryCommand } from "./openapi-react-query-DGEkD39r.mjs";
|
|
9
11
|
import { createRequire } from "node:module";
|
|
10
|
-
import { copyFileSync, existsSync,
|
|
12
|
+
import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
11
13
|
import { basename, dirname, join, parse, relative, resolve } from "node:path";
|
|
12
14
|
import { Command } from "commander";
|
|
13
15
|
import { stdin, stdout } from "node:process";
|
|
14
16
|
import * as readline from "node:readline/promises";
|
|
15
17
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
18
|
import { execSync, spawn } from "node:child_process";
|
|
18
19
|
import { createServer } from "node:net";
|
|
19
20
|
import chokidar from "chokidar";
|
|
20
21
|
import { config } from "dotenv";
|
|
21
22
|
import fg from "fast-glob";
|
|
22
23
|
import { Cron } from "@geekmidas/constructs/crons";
|
|
23
|
-
import { Endpoint } from "@geekmidas/constructs/endpoints";
|
|
24
24
|
import { Function } from "@geekmidas/constructs/functions";
|
|
25
25
|
import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
26
26
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -75,6 +75,9 @@ var repository = {
|
|
|
75
75
|
};
|
|
76
76
|
var dependencies = {
|
|
77
77
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
78
|
+
"@aws-sdk/client-route-53": "~3.971.0",
|
|
79
|
+
"@aws-sdk/client-ssm": "~3.971.0",
|
|
80
|
+
"@aws-sdk/credential-providers": "~3.971.0",
|
|
78
81
|
"@geekmidas/constructs": "workspace:~",
|
|
79
82
|
"@geekmidas/envkit": "workspace:~",
|
|
80
83
|
"@geekmidas/errors": "workspace:~",
|
|
@@ -88,7 +91,8 @@ var dependencies = {
|
|
|
88
91
|
"lodash.kebabcase": "^4.1.1",
|
|
89
92
|
"openapi-typescript": "^7.4.2",
|
|
90
93
|
"pg": "~8.17.1",
|
|
91
|
-
"prompts": "~2.4.2"
|
|
94
|
+
"prompts": "~2.4.2",
|
|
95
|
+
"tsx": "~4.20.3"
|
|
92
96
|
};
|
|
93
97
|
var devDependencies = {
|
|
94
98
|
"@geekmidas/testkit": "workspace:*",
|
|
@@ -118,138 +122,6 @@ var package_default = {
|
|
|
118
122
|
peerDependenciesMeta
|
|
119
123
|
};
|
|
120
124
|
|
|
121
|
-
//#endregion
|
|
122
|
-
//#region src/auth/credentials.ts
|
|
123
|
-
/**
|
|
124
|
-
* Get the path to the credentials directory
|
|
125
|
-
*/
|
|
126
|
-
function getCredentialsDir(options) {
|
|
127
|
-
const root = options?.root ?? homedir();
|
|
128
|
-
return join(root, ".gkm");
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Get the path to the credentials file
|
|
132
|
-
*/
|
|
133
|
-
function getCredentialsPath(options) {
|
|
134
|
-
return join(getCredentialsDir(options), "credentials.json");
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Ensure the credentials directory exists
|
|
138
|
-
*/
|
|
139
|
-
function ensureCredentialsDir(options) {
|
|
140
|
-
const dir = getCredentialsDir(options);
|
|
141
|
-
if (!existsSync(dir)) mkdirSync(dir, {
|
|
142
|
-
recursive: true,
|
|
143
|
-
mode: 448
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Read stored credentials from disk
|
|
148
|
-
*/
|
|
149
|
-
async function readCredentials(options) {
|
|
150
|
-
const path = getCredentialsPath(options);
|
|
151
|
-
if (!existsSync(path)) return {};
|
|
152
|
-
try {
|
|
153
|
-
const content = await readFile(path, "utf-8");
|
|
154
|
-
return JSON.parse(content);
|
|
155
|
-
} catch {
|
|
156
|
-
return {};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Write credentials to disk
|
|
161
|
-
*/
|
|
162
|
-
async function writeCredentials(credentials, options) {
|
|
163
|
-
ensureCredentialsDir(options);
|
|
164
|
-
const path = getCredentialsPath(options);
|
|
165
|
-
await writeFile(path, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Store Dokploy credentials
|
|
169
|
-
*/
|
|
170
|
-
async function storeDokployCredentials(token, endpoint, options) {
|
|
171
|
-
const credentials = await readCredentials(options);
|
|
172
|
-
credentials.dokploy = {
|
|
173
|
-
token,
|
|
174
|
-
endpoint,
|
|
175
|
-
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
176
|
-
};
|
|
177
|
-
await writeCredentials(credentials, options);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Get stored Dokploy credentials
|
|
181
|
-
*/
|
|
182
|
-
async function getDokployCredentials(options) {
|
|
183
|
-
const credentials = await readCredentials(options);
|
|
184
|
-
if (!credentials.dokploy) return null;
|
|
185
|
-
return {
|
|
186
|
-
token: credentials.dokploy.token,
|
|
187
|
-
endpoint: credentials.dokploy.endpoint,
|
|
188
|
-
registryId: credentials.dokploy.registryId
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Remove Dokploy credentials
|
|
193
|
-
*/
|
|
194
|
-
async function removeDokployCredentials(options) {
|
|
195
|
-
const credentials = await readCredentials(options);
|
|
196
|
-
if (!credentials.dokploy) return false;
|
|
197
|
-
delete credentials.dokploy;
|
|
198
|
-
await writeCredentials(credentials, options);
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Get Dokploy API token, checking stored credentials first, then environment
|
|
203
|
-
*/
|
|
204
|
-
async function getDokployToken(options) {
|
|
205
|
-
const envToken = process.env.DOKPLOY_API_TOKEN;
|
|
206
|
-
if (envToken) return envToken;
|
|
207
|
-
const stored = await getDokployCredentials(options);
|
|
208
|
-
if (stored) return stored.token;
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Store Dokploy registry ID
|
|
213
|
-
*/
|
|
214
|
-
async function storeDokployRegistryId(registryId, options) {
|
|
215
|
-
const credentials = await readCredentials(options);
|
|
216
|
-
if (!credentials.dokploy) throw new Error("Dokploy credentials not found. Run \"gkm login --service dokploy\" first.");
|
|
217
|
-
credentials.dokploy.registryId = registryId;
|
|
218
|
-
await writeCredentials(credentials, options);
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Get Dokploy registry ID from stored credentials
|
|
222
|
-
*/
|
|
223
|
-
async function getDokployRegistryId(options) {
|
|
224
|
-
const stored = await getDokployCredentials(options);
|
|
225
|
-
return stored?.registryId ?? void 0;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Store Hostinger API token
|
|
229
|
-
*
|
|
230
|
-
* @param token - API token from hpanel.hostinger.com/profile/api
|
|
231
|
-
*/
|
|
232
|
-
async function storeHostingerToken(token, options) {
|
|
233
|
-
const credentials = await readCredentials(options);
|
|
234
|
-
credentials.hostinger = {
|
|
235
|
-
token,
|
|
236
|
-
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
237
|
-
};
|
|
238
|
-
await writeCredentials(credentials, options);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get stored Hostinger API token
|
|
242
|
-
*
|
|
243
|
-
* Checks environment variable first (HOSTINGER_API_TOKEN),
|
|
244
|
-
* then falls back to stored credentials.
|
|
245
|
-
*/
|
|
246
|
-
async function getHostingerToken(options) {
|
|
247
|
-
const envToken = process.env.HOSTINGER_API_TOKEN;
|
|
248
|
-
if (envToken) return envToken;
|
|
249
|
-
const credentials = await readCredentials(options);
|
|
250
|
-
return credentials.hostinger?.token ?? null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
125
|
//#endregion
|
|
254
126
|
//#region src/auth/index.ts
|
|
255
127
|
const logger$11 = console;
|
|
@@ -257,7 +129,7 @@ const logger$11 = console;
|
|
|
257
129
|
* Validate Dokploy token by making a test API call
|
|
258
130
|
*/
|
|
259
131
|
async function validateDokployToken(endpoint, token) {
|
|
260
|
-
const { DokployApi: DokployApi$1 } = await import("./dokploy-api-
|
|
132
|
+
const { DokployApi: DokployApi$1 } = await import("./dokploy-api-CHa8G51l.mjs");
|
|
261
133
|
const api = new DokployApi$1({
|
|
262
134
|
baseUrl: endpoint,
|
|
263
135
|
token
|
|
@@ -2152,7 +2024,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
2152
2024
|
let masterKey;
|
|
2153
2025
|
if (context.production?.bundle && !skipBundle) {
|
|
2154
2026
|
logger$7.log(`\n📦 Bundling production server...`);
|
|
2155
|
-
const { bundleServer } = await import("./bundler-
|
|
2027
|
+
const { bundleServer } = await import("./bundler-BqTN5Dj5.mjs");
|
|
2156
2028
|
const allConstructs = [
|
|
2157
2029
|
...endpoints.map((e) => e.construct),
|
|
2158
2030
|
...functions.map((f) => f.construct),
|
|
@@ -2280,37 +2152,6 @@ function getAppOutputPath(workspace, _appName, app) {
|
|
|
2280
2152
|
//#endregion
|
|
2281
2153
|
//#region src/deploy/state.ts
|
|
2282
2154
|
/**
|
|
2283
|
-
* Get the state file path for a stage
|
|
2284
|
-
*/
|
|
2285
|
-
function getStateFilePath(workspaceRoot, stage) {
|
|
2286
|
-
return join(workspaceRoot, ".gkm", `deploy-${stage}.json`);
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* Read the deploy state for a stage
|
|
2290
|
-
* Returns null if state file doesn't exist
|
|
2291
|
-
*/
|
|
2292
|
-
async function readStageState(workspaceRoot, stage) {
|
|
2293
|
-
const filePath = getStateFilePath(workspaceRoot, stage);
|
|
2294
|
-
try {
|
|
2295
|
-
const content = await readFile(filePath, "utf-8");
|
|
2296
|
-
return JSON.parse(content);
|
|
2297
|
-
} catch (error) {
|
|
2298
|
-
if (error.code === "ENOENT") return null;
|
|
2299
|
-
console.warn(`Warning: Could not read deploy state: ${error}`);
|
|
2300
|
-
return null;
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
/**
|
|
2304
|
-
* Write the deploy state for a stage
|
|
2305
|
-
*/
|
|
2306
|
-
async function writeStageState(workspaceRoot, stage, state) {
|
|
2307
|
-
const filePath = getStateFilePath(workspaceRoot, stage);
|
|
2308
|
-
const dir = join(workspaceRoot, ".gkm");
|
|
2309
|
-
await mkdir(dir, { recursive: true });
|
|
2310
|
-
state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2311
|
-
await writeFile(filePath, JSON.stringify(state, null, 2));
|
|
2312
|
-
}
|
|
2313
|
-
/**
|
|
2314
2155
|
* Create a new empty state for a stage
|
|
2315
2156
|
*/
|
|
2316
2157
|
function createEmptyState(stage, environmentId) {
|
|
@@ -2405,155 +2246,90 @@ function isDnsVerified(state, hostname, serverIp) {
|
|
|
2405
2246
|
}
|
|
2406
2247
|
|
|
2407
2248
|
//#endregion
|
|
2408
|
-
//#region src/deploy/dns/
|
|
2249
|
+
//#region src/deploy/dns/DnsProvider.ts
|
|
2409
2250
|
/**
|
|
2410
|
-
*
|
|
2251
|
+
* Check if value is a DnsProvider implementation.
|
|
2252
|
+
*/
|
|
2253
|
+
function isDnsProvider(value) {
|
|
2254
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function";
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Create a DNS provider based on configuration.
|
|
2411
2258
|
*
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
2259
|
+
* - 'hostinger': HostingerProvider
|
|
2260
|
+
* - 'route53': Route53Provider
|
|
2261
|
+
* - 'manual': Returns null (user handles DNS)
|
|
2262
|
+
* - Custom: Use provided DnsProvider implementation
|
|
2263
|
+
*/
|
|
2264
|
+
async function createDnsProvider(options) {
|
|
2265
|
+
const { config: config$1 } = options;
|
|
2266
|
+
if (config$1.provider === "manual") return null;
|
|
2267
|
+
if (isDnsProvider(config$1.provider)) return config$1.provider;
|
|
2268
|
+
const provider = config$1.provider;
|
|
2269
|
+
if (provider === "hostinger") {
|
|
2270
|
+
const { HostingerProvider } = await import("./HostingerProvider-DqUq6e9i.mjs");
|
|
2271
|
+
return new HostingerProvider();
|
|
2272
|
+
}
|
|
2273
|
+
if (provider === "route53") {
|
|
2274
|
+
const { Route53Provider } = await import("./Route53Provider-KUAX3vz9.mjs");
|
|
2275
|
+
const route53Config = config$1;
|
|
2276
|
+
return new Route53Provider({
|
|
2277
|
+
region: route53Config.region,
|
|
2278
|
+
profile: route53Config.profile,
|
|
2279
|
+
hostedZoneId: route53Config.hostedZoneId
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
if (provider === "cloudflare") throw new Error("Cloudflare DNS provider not yet implemented");
|
|
2283
|
+
throw new Error(`Unknown DNS provider: ${JSON.stringify(config$1)}`);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
//#endregion
|
|
2287
|
+
//#region src/deploy/dns/index.ts
|
|
2288
|
+
const logger$6 = console;
|
|
2289
|
+
/**
|
|
2290
|
+
* Check if DNS config is legacy format (single domain with `domain` property)
|
|
2414
2291
|
*/
|
|
2415
|
-
|
|
2292
|
+
function isLegacyDnsConfig(config$1) {
|
|
2293
|
+
return typeof config$1 === "object" && config$1 !== null && "provider" in config$1 && "domain" in config$1;
|
|
2294
|
+
}
|
|
2416
2295
|
/**
|
|
2417
|
-
*
|
|
2296
|
+
* Normalize DNS config to new multi-domain format
|
|
2418
2297
|
*/
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
this.statusText = statusText;
|
|
2424
|
-
this.errors = errors;
|
|
2425
|
-
this.name = "HostingerApiError";
|
|
2298
|
+
function normalizeDnsConfig(config$1) {
|
|
2299
|
+
if (isLegacyDnsConfig(config$1)) {
|
|
2300
|
+
const { domain,...providerConfig } = config$1;
|
|
2301
|
+
return { [domain]: providerConfig };
|
|
2426
2302
|
}
|
|
2427
|
-
|
|
2303
|
+
return config$1;
|
|
2304
|
+
}
|
|
2428
2305
|
/**
|
|
2429
|
-
*
|
|
2306
|
+
* Find the root domain for a hostname from available DNS configs
|
|
2430
2307
|
*
|
|
2431
2308
|
* @example
|
|
2432
|
-
*
|
|
2433
|
-
*
|
|
2434
|
-
*
|
|
2435
|
-
* // Get all records for a domain
|
|
2436
|
-
* const records = await api.getRecords('traflabs.io');
|
|
2437
|
-
*
|
|
2438
|
-
* // Create/update records
|
|
2439
|
-
* await api.upsertRecords('traflabs.io', [
|
|
2440
|
-
* { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }
|
|
2441
|
-
* ]);
|
|
2442
|
-
* ```
|
|
2309
|
+
* findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
|
|
2310
|
+
* // Returns 'geekmidas.com'
|
|
2443
2311
|
*/
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
},
|
|
2460
|
-
body: body ? JSON.stringify(body) : void 0
|
|
2461
|
-
});
|
|
2462
|
-
if (!response.ok) {
|
|
2463
|
-
let errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;
|
|
2464
|
-
let errors;
|
|
2465
|
-
try {
|
|
2466
|
-
const errorBody = await response.json();
|
|
2467
|
-
if (errorBody.message) errorMessage = `Hostinger API error: ${errorBody.message}`;
|
|
2468
|
-
errors = errorBody.errors;
|
|
2469
|
-
} catch {}
|
|
2470
|
-
throw new HostingerApiError(errorMessage, response.status, response.statusText, errors);
|
|
2312
|
+
function findRootDomain(hostname, dnsConfig) {
|
|
2313
|
+
const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
|
|
2314
|
+
for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Group hostnames by their root domain
|
|
2319
|
+
*/
|
|
2320
|
+
function groupHostnamesByDomain(appHostnames, dnsConfig) {
|
|
2321
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2322
|
+
for (const [appName, hostname] of appHostnames) {
|
|
2323
|
+
const rootDomain = findRootDomain(hostname, dnsConfig);
|
|
2324
|
+
if (!rootDomain) {
|
|
2325
|
+
logger$6.log(` ⚠ No DNS config found for hostname: ${hostname}`);
|
|
2326
|
+
continue;
|
|
2471
2327
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
return JSON.parse(text);
|
|
2475
|
-
}
|
|
2476
|
-
/**
|
|
2477
|
-
* Get all DNS records for a domain
|
|
2478
|
-
*
|
|
2479
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2480
|
-
*/
|
|
2481
|
-
async getRecords(domain) {
|
|
2482
|
-
const response = await this.request("GET", `/api/dns/v1/zones/${domain}`);
|
|
2483
|
-
return response.data || [];
|
|
2484
|
-
}
|
|
2485
|
-
/**
|
|
2486
|
-
* Create or update DNS records
|
|
2487
|
-
*
|
|
2488
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2489
|
-
* @param records - Records to create/update
|
|
2490
|
-
* @param overwrite - If true, replaces all existing records. If false, merges with existing.
|
|
2491
|
-
*/
|
|
2492
|
-
async upsertRecords(domain, records, overwrite = false) {
|
|
2493
|
-
await this.request("PUT", `/api/dns/v1/zones/${domain}`, {
|
|
2494
|
-
overwrite,
|
|
2495
|
-
zone: records
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
/**
|
|
2499
|
-
* Validate DNS records before applying
|
|
2500
|
-
*
|
|
2501
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2502
|
-
* @param records - Records to validate
|
|
2503
|
-
* @returns true if valid, throws if invalid
|
|
2504
|
-
*/
|
|
2505
|
-
async validateRecords(domain, records) {
|
|
2506
|
-
await this.request("POST", `/api/dns/v1/zones/${domain}/validate`, {
|
|
2507
|
-
overwrite: false,
|
|
2508
|
-
zone: records
|
|
2509
|
-
});
|
|
2510
|
-
return true;
|
|
2511
|
-
}
|
|
2512
|
-
/**
|
|
2513
|
-
* Delete specific DNS records
|
|
2514
|
-
*
|
|
2515
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2516
|
-
* @param filters - Filters to match records for deletion
|
|
2517
|
-
*/
|
|
2518
|
-
async deleteRecords(domain, filters) {
|
|
2519
|
-
await this.request("DELETE", `/api/dns/v1/zones/${domain}`, { filters });
|
|
2520
|
-
}
|
|
2521
|
-
/**
|
|
2522
|
-
* Check if a specific record exists
|
|
2523
|
-
*
|
|
2524
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2525
|
-
* @param name - Subdomain name (e.g., 'api.joemoer')
|
|
2526
|
-
* @param type - Record type (e.g., 'A')
|
|
2527
|
-
*/
|
|
2528
|
-
async recordExists(domain, name$1, type$1 = "A") {
|
|
2529
|
-
const records = await this.getRecords(domain);
|
|
2530
|
-
return records.some((r) => r.name === name$1 && r.type === type$1);
|
|
2531
|
-
}
|
|
2532
|
-
/**
|
|
2533
|
-
* Create a single A record if it doesn't exist
|
|
2534
|
-
*
|
|
2535
|
-
* @param domain - Root domain (e.g., 'traflabs.io')
|
|
2536
|
-
* @param subdomain - Subdomain name (e.g., 'api.joemoer')
|
|
2537
|
-
* @param ip - IP address to point to
|
|
2538
|
-
* @param ttl - TTL in seconds (default: 300)
|
|
2539
|
-
* @returns true if created, false if already exists
|
|
2540
|
-
*/
|
|
2541
|
-
async createARecordIfNotExists(domain, subdomain, ip, ttl = 300) {
|
|
2542
|
-
const exists = await this.recordExists(domain, subdomain, "A");
|
|
2543
|
-
if (exists) return false;
|
|
2544
|
-
await this.upsertRecords(domain, [{
|
|
2545
|
-
name: subdomain,
|
|
2546
|
-
type: "A",
|
|
2547
|
-
ttl,
|
|
2548
|
-
records: [{ content: ip }]
|
|
2549
|
-
}]);
|
|
2550
|
-
return true;
|
|
2328
|
+
if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
|
|
2329
|
+
grouped.get(rootDomain).set(appName, hostname);
|
|
2551
2330
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
//#endregion
|
|
2555
|
-
//#region src/deploy/dns/index.ts
|
|
2556
|
-
const logger$6 = console;
|
|
2331
|
+
return grouped;
|
|
2332
|
+
}
|
|
2557
2333
|
/**
|
|
2558
2334
|
* Resolve IP address from a hostname
|
|
2559
2335
|
*/
|
|
@@ -2625,127 +2401,91 @@ function printDnsRecordsSimple(records, rootDomain) {
|
|
|
2625
2401
|
logger$6.log("");
|
|
2626
2402
|
}
|
|
2627
2403
|
/**
|
|
2628
|
-
*
|
|
2629
|
-
*/
|
|
2630
|
-
async function promptForToken(message) {
|
|
2631
|
-
const { stdin: stdin$1, stdout: stdout$1 } = await import("node:process");
|
|
2632
|
-
if (!stdin$1.isTTY) throw new Error("Interactive input required for Hostinger token.");
|
|
2633
|
-
stdout$1.write(message);
|
|
2634
|
-
return new Promise((resolve$1) => {
|
|
2635
|
-
let value = "";
|
|
2636
|
-
const onData = (char) => {
|
|
2637
|
-
const c = char.toString();
|
|
2638
|
-
if (c === "\n" || c === "\r") {
|
|
2639
|
-
stdin$1.setRawMode(false);
|
|
2640
|
-
stdin$1.pause();
|
|
2641
|
-
stdin$1.removeListener("data", onData);
|
|
2642
|
-
stdout$1.write("\n");
|
|
2643
|
-
resolve$1(value);
|
|
2644
|
-
} else if (c === "") {
|
|
2645
|
-
stdin$1.setRawMode(false);
|
|
2646
|
-
stdin$1.pause();
|
|
2647
|
-
stdout$1.write("\n");
|
|
2648
|
-
process.exit(1);
|
|
2649
|
-
} else if (c === "" || c === "\b") {
|
|
2650
|
-
if (value.length > 0) value = value.slice(0, -1);
|
|
2651
|
-
} else value += c;
|
|
2652
|
-
};
|
|
2653
|
-
stdin$1.setRawMode(true);
|
|
2654
|
-
stdin$1.resume();
|
|
2655
|
-
stdin$1.on("data", onData);
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
/**
|
|
2659
|
-
* Create DNS records using the configured provider
|
|
2660
|
-
*/
|
|
2661
|
-
async function createDnsRecords(records, dnsConfig) {
|
|
2662
|
-
const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
|
|
2663
|
-
if (provider === "manual") return records.map((r) => ({
|
|
2664
|
-
...r,
|
|
2665
|
-
created: false,
|
|
2666
|
-
existed: false
|
|
2667
|
-
}));
|
|
2668
|
-
if (provider === "hostinger") return createHostingerRecords(records, rootDomain, ttl);
|
|
2669
|
-
if (provider === "cloudflare") {
|
|
2670
|
-
logger$6.log(" ⚠ Cloudflare DNS integration not yet implemented");
|
|
2671
|
-
return records.map((r) => ({
|
|
2672
|
-
...r,
|
|
2673
|
-
error: "Cloudflare not implemented"
|
|
2674
|
-
}));
|
|
2675
|
-
}
|
|
2676
|
-
return records;
|
|
2677
|
-
}
|
|
2678
|
-
/**
|
|
2679
|
-
* Create DNS records at Hostinger
|
|
2404
|
+
* Create DNS records for a single domain using its configured provider
|
|
2680
2405
|
*/
|
|
2681
|
-
async function
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
logger$6.log("\n 📋 Hostinger API token not found.");
|
|
2685
|
-
logger$6.log(" Get your token from: https://hpanel.hostinger.com/profile/api\n");
|
|
2686
|
-
try {
|
|
2687
|
-
token = await promptForToken(" Hostinger API Token: ");
|
|
2688
|
-
await storeHostingerToken(token);
|
|
2689
|
-
logger$6.log(" ✓ Token saved");
|
|
2690
|
-
} catch {
|
|
2691
|
-
logger$6.log(" ⚠ Could not get token, skipping DNS creation");
|
|
2692
|
-
return records.map((r) => ({
|
|
2693
|
-
...r,
|
|
2694
|
-
error: "No API token"
|
|
2695
|
-
}));
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
const api = new HostingerApi(token);
|
|
2699
|
-
const results = [];
|
|
2700
|
-
let existingRecords = [];
|
|
2406
|
+
async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
|
|
2407
|
+
const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
|
|
2408
|
+
let provider;
|
|
2701
2409
|
try {
|
|
2702
|
-
|
|
2410
|
+
provider = await createDnsProvider({ config: providerConfig });
|
|
2703
2411
|
} catch (error) {
|
|
2704
2412
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2705
|
-
logger$6.log(` ⚠ Failed to
|
|
2413
|
+
logger$6.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
|
|
2706
2414
|
return records.map((r) => ({
|
|
2707
2415
|
...r,
|
|
2708
2416
|
error: message
|
|
2709
2417
|
}));
|
|
2710
2418
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2419
|
+
if (!provider) return records.map((r) => ({
|
|
2420
|
+
...r,
|
|
2421
|
+
created: false,
|
|
2422
|
+
existed: false
|
|
2423
|
+
}));
|
|
2424
|
+
const results = [];
|
|
2425
|
+
const upsertRecords = records.map((r) => ({
|
|
2426
|
+
name: r.subdomain,
|
|
2427
|
+
type: r.type,
|
|
2428
|
+
ttl,
|
|
2429
|
+
value: r.value
|
|
2430
|
+
}));
|
|
2431
|
+
try {
|
|
2432
|
+
const upsertResults = await provider.upsertRecords(rootDomain, upsertRecords);
|
|
2433
|
+
for (const [i, record] of records.entries()) {
|
|
2434
|
+
const result = upsertResults[i];
|
|
2435
|
+
if (!result) {
|
|
2436
|
+
results.push({
|
|
2437
|
+
hostname: record.hostname,
|
|
2438
|
+
subdomain: record.subdomain,
|
|
2439
|
+
type: record.type,
|
|
2440
|
+
value: record.value,
|
|
2441
|
+
appName: record.appName,
|
|
2442
|
+
error: "No result returned from provider"
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (result.unchanged) results.push({
|
|
2447
|
+
hostname: record.hostname,
|
|
2448
|
+
subdomain: record.subdomain,
|
|
2449
|
+
type: record.type,
|
|
2450
|
+
value: record.value,
|
|
2451
|
+
appName: record.appName,
|
|
2716
2452
|
existed: true,
|
|
2717
2453
|
created: false
|
|
2718
2454
|
});
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
}]);
|
|
2728
|
-
results.push({
|
|
2729
|
-
...record,
|
|
2730
|
-
created: true,
|
|
2731
|
-
existed: false
|
|
2732
|
-
});
|
|
2733
|
-
} catch (error) {
|
|
2734
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2735
|
-
results.push({
|
|
2736
|
-
...record,
|
|
2737
|
-
error: message
|
|
2455
|
+
else results.push({
|
|
2456
|
+
hostname: record.hostname,
|
|
2457
|
+
subdomain: record.subdomain,
|
|
2458
|
+
type: record.type,
|
|
2459
|
+
value: record.value,
|
|
2460
|
+
appName: record.appName,
|
|
2461
|
+
created: result.created,
|
|
2462
|
+
existed: !result.created
|
|
2738
2463
|
});
|
|
2739
2464
|
}
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2467
|
+
logger$6.log(` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`);
|
|
2468
|
+
return records.map((r) => ({
|
|
2469
|
+
hostname: r.hostname,
|
|
2470
|
+
subdomain: r.subdomain,
|
|
2471
|
+
type: r.type,
|
|
2472
|
+
value: r.value,
|
|
2473
|
+
appName: r.appName,
|
|
2474
|
+
error: message
|
|
2475
|
+
}));
|
|
2740
2476
|
}
|
|
2741
2477
|
return results;
|
|
2742
2478
|
}
|
|
2743
2479
|
/**
|
|
2744
2480
|
* Main DNS orchestration function for deployments
|
|
2481
|
+
*
|
|
2482
|
+
* Supports both legacy single-domain format and new multi-domain format:
|
|
2483
|
+
* - Legacy: { provider: 'hostinger', domain: 'example.com' }
|
|
2484
|
+
* - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
|
|
2745
2485
|
*/
|
|
2746
2486
|
async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
|
|
2747
2487
|
if (!dnsConfig) return null;
|
|
2748
|
-
const
|
|
2488
|
+
const normalizedConfig = normalizeDnsConfig(dnsConfig);
|
|
2749
2489
|
logger$6.log("\n🌐 Setting up DNS records...");
|
|
2750
2490
|
let serverIp;
|
|
2751
2491
|
try {
|
|
@@ -2757,31 +2497,43 @@ async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
|
|
|
2757
2497
|
logger$6.log(` ⚠ Failed to resolve server IP: ${message}`);
|
|
2758
2498
|
return null;
|
|
2759
2499
|
}
|
|
2760
|
-
const
|
|
2761
|
-
if (
|
|
2762
|
-
logger$6.log(" No DNS records needed");
|
|
2500
|
+
const groupedHostnames = groupHostnamesByDomain(appHostnames, normalizedConfig);
|
|
2501
|
+
if (groupedHostnames.size === 0) {
|
|
2502
|
+
logger$6.log(" No DNS records needed (no hostnames match configured domains)");
|
|
2763
2503
|
return {
|
|
2764
2504
|
records: [],
|
|
2765
2505
|
success: true,
|
|
2766
2506
|
serverIp
|
|
2767
2507
|
};
|
|
2768
2508
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2509
|
+
const allRecords = [];
|
|
2510
|
+
let hasFailures = false;
|
|
2511
|
+
for (const [rootDomain, domainHostnames] of groupedHostnames) {
|
|
2512
|
+
const providerConfig = normalizedConfig[rootDomain];
|
|
2513
|
+
if (!providerConfig) {
|
|
2514
|
+
logger$6.log(` ⚠ No provider config for ${rootDomain}`);
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
const providerName = typeof providerConfig.provider === "string" ? providerConfig.provider : "custom";
|
|
2518
|
+
const requiredRecords = generateRequiredRecords(domainHostnames, rootDomain, serverIp);
|
|
2519
|
+
if (requiredRecords.length === 0) continue;
|
|
2520
|
+
logger$6.log(` Creating DNS records for ${rootDomain} (${providerName})...`);
|
|
2521
|
+
const domainRecords = await createDnsRecordsForDomain(requiredRecords, rootDomain, providerConfig);
|
|
2522
|
+
allRecords.push(...domainRecords);
|
|
2523
|
+
const created = domainRecords.filter((r) => r.created).length;
|
|
2524
|
+
const existed = domainRecords.filter((r) => r.existed).length;
|
|
2525
|
+
const failed = domainRecords.filter((r) => r.error).length;
|
|
2526
|
+
if (created > 0) logger$6.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
|
|
2527
|
+
if (existed > 0) logger$6.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
|
|
2528
|
+
if (failed > 0) {
|
|
2529
|
+
logger$6.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
|
|
2530
|
+
hasFailures = true;
|
|
2531
|
+
}
|
|
2532
|
+
printDnsRecordsTable(domainRecords, rootDomain);
|
|
2533
|
+
if (providerConfig.provider === "manual" || failed > 0) printDnsRecordsSimple(domainRecords.filter((r) => !r.created && !r.existed), rootDomain);
|
|
2534
|
+
}
|
|
2783
2535
|
return {
|
|
2784
|
-
records:
|
|
2536
|
+
records: allRecords,
|
|
2785
2537
|
success: !hasFailures,
|
|
2786
2538
|
serverIp
|
|
2787
2539
|
};
|
|
@@ -4756,6 +4508,48 @@ async function deployListCommand(options) {
|
|
|
4756
4508
|
}
|
|
4757
4509
|
}
|
|
4758
4510
|
|
|
4511
|
+
//#endregion
|
|
4512
|
+
//#region src/deploy/StateProvider.ts
|
|
4513
|
+
/**
|
|
4514
|
+
* Check if value is a StateProvider implementation.
|
|
4515
|
+
*/
|
|
4516
|
+
function isStateProvider(value) {
|
|
4517
|
+
return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
|
|
4518
|
+
}
|
|
4519
|
+
/**
|
|
4520
|
+
* Create a state provider based on configuration.
|
|
4521
|
+
*
|
|
4522
|
+
* - 'local': LocalStateProvider (default)
|
|
4523
|
+
* - 'ssm': CachedStateProvider with SSM as source of truth
|
|
4524
|
+
* - Custom: Use provided StateProvider implementation
|
|
4525
|
+
*/
|
|
4526
|
+
async function createStateProvider(options) {
|
|
4527
|
+
const { config: config$1, workspaceRoot, workspaceName } = options;
|
|
4528
|
+
if (!config$1) {
|
|
4529
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
|
|
4530
|
+
return new LocalStateProvider(workspaceRoot);
|
|
4531
|
+
}
|
|
4532
|
+
if (isStateProvider(config$1.provider)) return config$1.provider;
|
|
4533
|
+
const provider = config$1.provider;
|
|
4534
|
+
if (provider === "local") {
|
|
4535
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
|
|
4536
|
+
return new LocalStateProvider(workspaceRoot);
|
|
4537
|
+
}
|
|
4538
|
+
if (provider === "ssm") {
|
|
4539
|
+
if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
|
|
4540
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
|
|
4541
|
+
const { SSMStateProvider } = await import("./SSMStateProvider-C4wp4AZe.mjs");
|
|
4542
|
+
const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-OiFUGr7p.mjs");
|
|
4543
|
+
const local = new LocalStateProvider(workspaceRoot);
|
|
4544
|
+
const ssm = new SSMStateProvider({
|
|
4545
|
+
workspaceName,
|
|
4546
|
+
region: config$1.region
|
|
4547
|
+
});
|
|
4548
|
+
return new CachedStateProvider$1(ssm, local);
|
|
4549
|
+
}
|
|
4550
|
+
throw new Error(`Unknown state provider: ${JSON.stringify(config$1)}`);
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4759
4553
|
//#endregion
|
|
4760
4554
|
//#region src/deploy/secrets.ts
|
|
4761
4555
|
/**
|
|
@@ -4858,10 +4652,12 @@ function generateSecretsReport(encryptedApps, sniffedApps) {
|
|
|
4858
4652
|
const __filename = fileURLToPath(import.meta.url);
|
|
4859
4653
|
const __dirname = dirname(__filename);
|
|
4860
4654
|
/**
|
|
4861
|
-
*
|
|
4655
|
+
* Resolve the tsx package path from the CLI package's dependencies.
|
|
4656
|
+
* This ensures tsx is available regardless of whether the target project has it installed.
|
|
4862
4657
|
*/
|
|
4863
|
-
function
|
|
4864
|
-
|
|
4658
|
+
function resolveTsxPath() {
|
|
4659
|
+
const require$2 = createRequire(import.meta.url);
|
|
4660
|
+
return require$2.resolve("tsx");
|
|
4865
4661
|
}
|
|
4866
4662
|
/**
|
|
4867
4663
|
* Resolve the path to a sniffer helper file.
|
|
@@ -5022,7 +4818,9 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
|
|
|
5022
4818
|
*
|
|
5023
4819
|
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
5024
4820
|
* use services. Each service's register() method accesses environment variables.
|
|
5025
|
-
*
|
|
4821
|
+
*
|
|
4822
|
+
* This runs in a subprocess with tsx loader to properly handle TypeScript
|
|
4823
|
+
* compilation and path alias resolution (e.g., `src/...` imports).
|
|
5026
4824
|
*
|
|
5027
4825
|
* @param routes - Glob pattern(s) for route files
|
|
5028
4826
|
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
@@ -5031,32 +4829,63 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
|
|
|
5031
4829
|
*/
|
|
5032
4830
|
async function sniffRouteFiles(routes, appPath, workspacePath) {
|
|
5033
4831
|
const fullAppPath = resolve(workspacePath, appPath);
|
|
5034
|
-
const
|
|
5035
|
-
const
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
4832
|
+
const workerPath = resolveSnifferFile("sniffer-routes-worker");
|
|
4833
|
+
const tsxPath = resolveTsxPath();
|
|
4834
|
+
const routesArray = Array.isArray(routes) ? routes : [routes];
|
|
4835
|
+
const pattern = routesArray[0];
|
|
4836
|
+
if (!pattern) return {
|
|
4837
|
+
envVars: [],
|
|
4838
|
+
error: new Error("No route patterns provided")
|
|
4839
|
+
};
|
|
4840
|
+
return new Promise((resolvePromise) => {
|
|
4841
|
+
const child = spawn("node", [
|
|
4842
|
+
"--import",
|
|
4843
|
+
tsxPath,
|
|
4844
|
+
workerPath,
|
|
4845
|
+
fullAppPath,
|
|
4846
|
+
pattern
|
|
4847
|
+
], {
|
|
5039
4848
|
cwd: fullAppPath,
|
|
5040
|
-
|
|
4849
|
+
stdio: [
|
|
4850
|
+
"ignore",
|
|
4851
|
+
"pipe",
|
|
4852
|
+
"pipe"
|
|
4853
|
+
],
|
|
4854
|
+
env: { ...process.env }
|
|
5041
4855
|
});
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
4856
|
+
let stdout$1 = "";
|
|
4857
|
+
let stderr = "";
|
|
4858
|
+
child.stdout.on("data", (data) => {
|
|
4859
|
+
stdout$1 += data.toString();
|
|
4860
|
+
});
|
|
4861
|
+
child.stderr.on("data", (data) => {
|
|
4862
|
+
stderr += data.toString();
|
|
4863
|
+
});
|
|
4864
|
+
child.on("close", (code) => {
|
|
4865
|
+
if (stderr) stderr.split("\n").filter((line) => line.trim()).forEach((line) => console.warn(line));
|
|
4866
|
+
try {
|
|
4867
|
+
const jsonMatch = stdout$1.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
4868
|
+
if (jsonMatch) {
|
|
4869
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
4870
|
+
resolvePromise({
|
|
4871
|
+
envVars: result.envVars || [],
|
|
4872
|
+
error: result.error ? new Error(result.error) : void 0
|
|
4873
|
+
});
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
} catch {}
|
|
4877
|
+
resolvePromise({
|
|
4878
|
+
envVars: [],
|
|
4879
|
+
error: new Error(`Failed to sniff route files (exit code ${code}): ${stderr || stdout$1 || "No output"}`)
|
|
4880
|
+
});
|
|
4881
|
+
});
|
|
4882
|
+
child.on("error", (err) => {
|
|
4883
|
+
resolvePromise({
|
|
4884
|
+
envVars: [],
|
|
4885
|
+
error: err
|
|
4886
|
+
});
|
|
4887
|
+
});
|
|
4888
|
+
});
|
|
5060
4889
|
}
|
|
5061
4890
|
/**
|
|
5062
4891
|
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
@@ -5668,7 +5497,12 @@ async function workspaceDeployCommand(workspace, options) {
|
|
|
5668
5497
|
logger$1.log(` ✓ Created project: ${project.projectId}`);
|
|
5669
5498
|
}
|
|
5670
5499
|
logger$1.log("\n📋 Loading deploy state...");
|
|
5671
|
-
|
|
5500
|
+
const stateProvider = await createStateProvider({
|
|
5501
|
+
config: workspace.state,
|
|
5502
|
+
workspaceRoot: workspace.root,
|
|
5503
|
+
workspaceName: workspace.name
|
|
5504
|
+
});
|
|
5505
|
+
let state = await stateProvider.read(stage);
|
|
5672
5506
|
if (state) {
|
|
5673
5507
|
logger$1.log(` Found existing state for stage "${stage}"`);
|
|
5674
5508
|
if (state.environmentId !== environmentId) {
|
|
@@ -6003,14 +5837,14 @@ async function workspaceDeployCommand(workspace, options) {
|
|
|
6003
5837
|
}
|
|
6004
5838
|
}
|
|
6005
5839
|
logger$1.log("\n📋 Saving deploy state...");
|
|
6006
|
-
await
|
|
6007
|
-
logger$1.log(
|
|
5840
|
+
await stateProvider.write(stage, state);
|
|
5841
|
+
logger$1.log(" ✓ State saved");
|
|
6008
5842
|
const dnsConfig = workspace.deploy.dns;
|
|
6009
5843
|
if (dnsConfig && appHostnames.size > 0) {
|
|
6010
5844
|
const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
|
|
6011
5845
|
if (dnsResult?.serverIp && appHostnames.size > 0) {
|
|
6012
5846
|
await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
|
|
6013
|
-
await
|
|
5847
|
+
await stateProvider.write(stage, state);
|
|
6014
5848
|
}
|
|
6015
5849
|
if (dnsResult?.success && appHostnames.size > 0) {
|
|
6016
5850
|
logger$1.log("\n🔒 Validating domains for SSL certificates...");
|
|
@@ -6079,7 +5913,7 @@ async function deployCommand(options) {
|
|
|
6079
5913
|
dokployConfig = setupResult.config;
|
|
6080
5914
|
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
6081
5915
|
if (setupResult.serviceUrls) {
|
|
6082
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-
|
|
5916
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-D8XzjVaO.mjs");
|
|
6083
5917
|
let secrets = await readStageSecrets$1(stage);
|
|
6084
5918
|
if (!secrets) {
|
|
6085
5919
|
logger$1.log(` Creating secrets file for stage "${stage}"...`);
|
|
@@ -6169,36 +6003,204 @@ async function deployCommand(options) {
|
|
|
6169
6003
|
}
|
|
6170
6004
|
|
|
6171
6005
|
//#endregion
|
|
6172
|
-
//#region src/
|
|
6006
|
+
//#region src/deploy/state-commands.ts
|
|
6173
6007
|
/**
|
|
6174
|
-
*
|
|
6175
|
-
*
|
|
6008
|
+
* Pull state from remote to local.
|
|
6009
|
+
* `gkm state:pull --stage=<stage>`
|
|
6176
6010
|
*/
|
|
6177
|
-
function
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
port: 5432,
|
|
6185
|
-
username: "app",
|
|
6186
|
-
database: "app"
|
|
6187
|
-
},
|
|
6188
|
-
redis: {
|
|
6189
|
-
host: "redis",
|
|
6190
|
-
port: 6379,
|
|
6191
|
-
username: "default"
|
|
6192
|
-
},
|
|
6193
|
-
rabbitmq: {
|
|
6194
|
-
host: "rabbitmq",
|
|
6195
|
-
port: 5672,
|
|
6196
|
-
username: "app",
|
|
6197
|
-
vhost: "/"
|
|
6011
|
+
async function statePullCommand(options) {
|
|
6012
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
6013
|
+
if (!workspace.state || workspace.state.provider === "local") {
|
|
6014
|
+
console.error("No remote state provider configured.");
|
|
6015
|
+
console.error("Add a remote provider in gkm.config.ts:");
|
|
6016
|
+
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
6017
|
+
process.exit(1);
|
|
6198
6018
|
}
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6019
|
+
const provider = await createStateProvider({
|
|
6020
|
+
config: workspace.state,
|
|
6021
|
+
workspaceRoot: workspace.root,
|
|
6022
|
+
workspaceName: workspace.name
|
|
6023
|
+
});
|
|
6024
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
6025
|
+
console.error("State provider does not support pull operation.");
|
|
6026
|
+
process.exit(1);
|
|
6027
|
+
}
|
|
6028
|
+
console.log(`Pulling state for stage: ${options.stage}...`);
|
|
6029
|
+
const state = await provider.pull(options.stage);
|
|
6030
|
+
if (state) {
|
|
6031
|
+
console.log("State pulled successfully.");
|
|
6032
|
+
printStateSummary(state);
|
|
6033
|
+
} else console.log("No remote state found for this stage.");
|
|
6034
|
+
}
|
|
6035
|
+
/**
|
|
6036
|
+
* Push local state to remote.
|
|
6037
|
+
* `gkm state:push --stage=<stage>`
|
|
6038
|
+
*/
|
|
6039
|
+
async function statePushCommand(options) {
|
|
6040
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
6041
|
+
if (!workspace.state || workspace.state.provider === "local") {
|
|
6042
|
+
console.error("No remote state provider configured.");
|
|
6043
|
+
console.error("Add a remote provider in gkm.config.ts:");
|
|
6044
|
+
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
6045
|
+
process.exit(1);
|
|
6046
|
+
}
|
|
6047
|
+
const provider = await createStateProvider({
|
|
6048
|
+
config: workspace.state,
|
|
6049
|
+
workspaceRoot: workspace.root,
|
|
6050
|
+
workspaceName: workspace.name
|
|
6051
|
+
});
|
|
6052
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
6053
|
+
console.error("State provider does not support push operation.");
|
|
6054
|
+
process.exit(1);
|
|
6055
|
+
}
|
|
6056
|
+
console.log(`Pushing state for stage: ${options.stage}...`);
|
|
6057
|
+
const state = await provider.push(options.stage);
|
|
6058
|
+
if (state) {
|
|
6059
|
+
console.log("State pushed successfully.");
|
|
6060
|
+
printStateSummary(state);
|
|
6061
|
+
} else console.log("No local state found for this stage.");
|
|
6062
|
+
}
|
|
6063
|
+
/**
|
|
6064
|
+
* Show current state.
|
|
6065
|
+
* `gkm state:show --stage=<stage>`
|
|
6066
|
+
*/
|
|
6067
|
+
async function stateShowCommand(options) {
|
|
6068
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
6069
|
+
const provider = await createStateProvider({
|
|
6070
|
+
config: workspace.state,
|
|
6071
|
+
workspaceRoot: workspace.root,
|
|
6072
|
+
workspaceName: workspace.name
|
|
6073
|
+
});
|
|
6074
|
+
const state = await provider.read(options.stage);
|
|
6075
|
+
if (!state) {
|
|
6076
|
+
console.log(`No state found for stage: ${options.stage}`);
|
|
6077
|
+
return;
|
|
6078
|
+
}
|
|
6079
|
+
if (options.json) console.log(JSON.stringify(state, null, 2));
|
|
6080
|
+
else printStateDetails(state);
|
|
6081
|
+
}
|
|
6082
|
+
/**
|
|
6083
|
+
* Compare local and remote state.
|
|
6084
|
+
* `gkm state:diff --stage=<stage>`
|
|
6085
|
+
*/
|
|
6086
|
+
async function stateDiffCommand(options) {
|
|
6087
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
6088
|
+
if (!workspace.state || workspace.state.provider === "local") {
|
|
6089
|
+
console.error("No remote state provider configured.");
|
|
6090
|
+
console.error("Diff requires a remote provider to compare against.");
|
|
6091
|
+
process.exit(1);
|
|
6092
|
+
}
|
|
6093
|
+
const provider = await createStateProvider({
|
|
6094
|
+
config: workspace.state,
|
|
6095
|
+
workspaceRoot: workspace.root,
|
|
6096
|
+
workspaceName: workspace.name
|
|
6097
|
+
});
|
|
6098
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
6099
|
+
console.error("State provider does not support diff operation.");
|
|
6100
|
+
process.exit(1);
|
|
6101
|
+
}
|
|
6102
|
+
console.log(`Comparing state for stage: ${options.stage}...\n`);
|
|
6103
|
+
const { local, remote } = await provider.diff(options.stage);
|
|
6104
|
+
if (!local && !remote) {
|
|
6105
|
+
console.log("No state found (local or remote).");
|
|
6106
|
+
return;
|
|
6107
|
+
}
|
|
6108
|
+
if (!local) console.log("Local: (none)");
|
|
6109
|
+
else console.log(`Local: Last deployed ${local.lastDeployedAt}`);
|
|
6110
|
+
if (!remote) console.log("Remote: (none)");
|
|
6111
|
+
else console.log(`Remote: Last deployed ${remote.lastDeployedAt}`);
|
|
6112
|
+
console.log("");
|
|
6113
|
+
const localApps = local?.applications ?? {};
|
|
6114
|
+
const remoteApps = remote?.applications ?? {};
|
|
6115
|
+
const allApps = new Set([...Object.keys(localApps), ...Object.keys(remoteApps)]);
|
|
6116
|
+
if (allApps.size > 0) {
|
|
6117
|
+
console.log("Applications:");
|
|
6118
|
+
for (const app of allApps) {
|
|
6119
|
+
const localId = localApps[app];
|
|
6120
|
+
const remoteId = remoteApps[app];
|
|
6121
|
+
if (localId === remoteId) console.log(` ${app}: ${localId ?? "(none)"}`);
|
|
6122
|
+
else if (!localId) console.log(` ${app}: (none) -> ${remoteId} [REMOTE ONLY]`);
|
|
6123
|
+
else if (!remoteId) console.log(` ${app}: ${localId} -> (none) [LOCAL ONLY]`);
|
|
6124
|
+
else console.log(` ${app}: ${localId} (local) != ${remoteId} (remote) [MISMATCH]`);
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
const localServices = local?.services ?? {};
|
|
6128
|
+
const remoteServices = remote?.services ?? {};
|
|
6129
|
+
if (Object.keys(localServices).length > 0 || Object.keys(remoteServices).length > 0) {
|
|
6130
|
+
console.log("\nServices:");
|
|
6131
|
+
const serviceKeys = new Set([...Object.keys(localServices), ...Object.keys(remoteServices)]);
|
|
6132
|
+
for (const key of serviceKeys) {
|
|
6133
|
+
const localVal = localServices[key];
|
|
6134
|
+
const remoteVal = remoteServices[key];
|
|
6135
|
+
if (localVal === remoteVal) console.log(` ${key}: ${localVal ?? "(none)"}`);
|
|
6136
|
+
else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
function printStateSummary(state) {
|
|
6141
|
+
const appCount = Object.keys(state.applications).length;
|
|
6142
|
+
const hasPostgres = !!state.services.postgresId;
|
|
6143
|
+
const hasRedis = !!state.services.redisId;
|
|
6144
|
+
console.log(` Stage: ${state.stage}`);
|
|
6145
|
+
console.log(` Applications: ${appCount}`);
|
|
6146
|
+
console.log(` Postgres: ${hasPostgres ? "configured" : "none"}`);
|
|
6147
|
+
console.log(` Redis: ${hasRedis ? "configured" : "none"}`);
|
|
6148
|
+
console.log(` Last deployed: ${state.lastDeployedAt}`);
|
|
6149
|
+
}
|
|
6150
|
+
function printStateDetails(state) {
|
|
6151
|
+
console.log(`Stage: ${state.stage}`);
|
|
6152
|
+
console.log(`Environment ID: ${state.environmentId}`);
|
|
6153
|
+
console.log(`Last Deployed: ${state.lastDeployedAt}`);
|
|
6154
|
+
console.log("");
|
|
6155
|
+
console.log("Applications:");
|
|
6156
|
+
const apps = Object.entries(state.applications);
|
|
6157
|
+
if (apps.length === 0) console.log(" (none)");
|
|
6158
|
+
else for (const [name$1, id] of apps) console.log(` ${name$1}: ${id}`);
|
|
6159
|
+
console.log("");
|
|
6160
|
+
console.log("Services:");
|
|
6161
|
+
if (!state.services.postgresId && !state.services.redisId) console.log(" (none)");
|
|
6162
|
+
else {
|
|
6163
|
+
if (state.services.postgresId) console.log(` Postgres: ${state.services.postgresId}`);
|
|
6164
|
+
if (state.services.redisId) console.log(` Redis: ${state.services.redisId}`);
|
|
6165
|
+
}
|
|
6166
|
+
if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
|
|
6167
|
+
console.log("");
|
|
6168
|
+
console.log("DNS Verified:");
|
|
6169
|
+
for (const [hostname, info] of Object.entries(state.dnsVerified)) console.log(` ${hostname}: ${info.serverIp} (${info.verifiedAt})`);
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
//#endregion
|
|
6174
|
+
//#region src/secrets/generator.ts
|
|
6175
|
+
/**
|
|
6176
|
+
* Generate a secure random password using URL-safe base64 characters.
|
|
6177
|
+
* @param length Password length (default: 32)
|
|
6178
|
+
*/
|
|
6179
|
+
function generateSecurePassword(length = 32) {
|
|
6180
|
+
return randomBytes(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
|
|
6181
|
+
}
|
|
6182
|
+
/** Default service configurations */
|
|
6183
|
+
const SERVICE_DEFAULTS = {
|
|
6184
|
+
postgres: {
|
|
6185
|
+
host: "postgres",
|
|
6186
|
+
port: 5432,
|
|
6187
|
+
username: "app",
|
|
6188
|
+
database: "app"
|
|
6189
|
+
},
|
|
6190
|
+
redis: {
|
|
6191
|
+
host: "redis",
|
|
6192
|
+
port: 6379,
|
|
6193
|
+
username: "default"
|
|
6194
|
+
},
|
|
6195
|
+
rabbitmq: {
|
|
6196
|
+
host: "rabbitmq",
|
|
6197
|
+
port: 5672,
|
|
6198
|
+
username: "app",
|
|
6199
|
+
vhost: "/"
|
|
6200
|
+
}
|
|
6201
|
+
};
|
|
6202
|
+
/**
|
|
6203
|
+
* Generate credentials for a specific service.
|
|
6202
6204
|
*/
|
|
6203
6205
|
function generateServiceCredentials(service) {
|
|
6204
6206
|
const defaults = SERVICE_DEFAULTS[service];
|
|
@@ -6374,7 +6376,10 @@ function generateAuthAppFiles(options) {
|
|
|
6374
6376
|
compilerOptions: {
|
|
6375
6377
|
noEmit: true,
|
|
6376
6378
|
baseUrl: ".",
|
|
6377
|
-
paths: {
|
|
6379
|
+
paths: {
|
|
6380
|
+
"~/*": ["./src/*"],
|
|
6381
|
+
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
6382
|
+
}
|
|
6378
6383
|
},
|
|
6379
6384
|
include: ["src/**/*.ts"],
|
|
6380
6385
|
exclude: ["node_modules", "dist"]
|
|
@@ -6584,7 +6589,10 @@ export default defineConfig({
|
|
|
6584
6589
|
compilerOptions: {
|
|
6585
6590
|
noEmit: true,
|
|
6586
6591
|
baseUrl: ".",
|
|
6587
|
-
paths: {
|
|
6592
|
+
paths: {
|
|
6593
|
+
"~/*": ["./src/*"],
|
|
6594
|
+
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
6595
|
+
}
|
|
6588
6596
|
},
|
|
6589
6597
|
include: ["src/**/*.ts"],
|
|
6590
6598
|
exclude: ["node_modules", "dist"]
|
|
@@ -6704,7 +6712,10 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
6704
6712
|
compilerOptions: {
|
|
6705
6713
|
noEmit: true,
|
|
6706
6714
|
baseUrl: ".",
|
|
6707
|
-
paths: {
|
|
6715
|
+
paths: {
|
|
6716
|
+
"~/*": ["./src/*"],
|
|
6717
|
+
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
6718
|
+
}
|
|
6708
6719
|
},
|
|
6709
6720
|
include: ["src/**/*.ts"],
|
|
6710
6721
|
exclude: ["node_modules", "dist"]
|
|
@@ -7002,7 +7013,11 @@ function generateModelsPackage(options) {
|
|
|
7002
7013
|
// Common Schemas
|
|
7003
7014
|
// ============================================
|
|
7004
7015
|
|
|
7005
|
-
export const IdSchema = z.
|
|
7016
|
+
export const IdSchema = z.uuid();
|
|
7017
|
+
|
|
7018
|
+
export const IdParamsSchema = z.object({
|
|
7019
|
+
id: IdSchema,
|
|
7020
|
+
});
|
|
7006
7021
|
|
|
7007
7022
|
export const TimestampsSchema = z.object({
|
|
7008
7023
|
createdAt: z.coerce.date(),
|
|
@@ -7028,6 +7043,7 @@ export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =
|
|
|
7028
7043
|
// ============================================
|
|
7029
7044
|
|
|
7030
7045
|
export type Id = z.infer<typeof IdSchema>;
|
|
7046
|
+
export type IdParams = z.infer<typeof IdParamsSchema>;
|
|
7031
7047
|
export type Timestamps = z.infer<typeof TimestampsSchema>;
|
|
7032
7048
|
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
7033
7049
|
`;
|
|
@@ -7120,6 +7136,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
7120
7136
|
lint: "biome lint .",
|
|
7121
7137
|
fmt: "biome format . --write",
|
|
7122
7138
|
"fmt:check": "biome format .",
|
|
7139
|
+
...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
|
|
7123
7140
|
...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
|
|
7124
7141
|
},
|
|
7125
7142
|
dependencies: { zod: "~4.1.0" },
|
|
@@ -7515,7 +7532,20 @@ export const config = envParser
|
|
|
7515
7532
|
},
|
|
7516
7533
|
{
|
|
7517
7534
|
path: getRoutePath("health.ts"),
|
|
7518
|
-
content: `import {
|
|
7535
|
+
content: monorepo ? `import { z } from 'zod';
|
|
7536
|
+
import { publicRouter } from '~/router';
|
|
7537
|
+
|
|
7538
|
+
export const healthEndpoint = publicRouter
|
|
7539
|
+
.get('/health')
|
|
7540
|
+
.output(z.object({
|
|
7541
|
+
status: z.string(),
|
|
7542
|
+
timestamp: z.string(),
|
|
7543
|
+
}))
|
|
7544
|
+
.handle(async () => ({
|
|
7545
|
+
status: 'ok',
|
|
7546
|
+
timestamp: new Date().toISOString(),
|
|
7547
|
+
}));
|
|
7548
|
+
` : `import { e } from '@geekmidas/constructs/endpoints';
|
|
7519
7549
|
import { z } from 'zod';
|
|
7520
7550
|
|
|
7521
7551
|
export const healthEndpoint = e
|
|
@@ -7568,12 +7598,12 @@ export const listUsersEndpoint = e
|
|
|
7568
7598
|
{
|
|
7569
7599
|
path: getRoutePath("users/get.ts"),
|
|
7570
7600
|
content: modelsImport ? `import { e } from '@geekmidas/constructs/endpoints';
|
|
7571
|
-
import {
|
|
7601
|
+
import { IdParamsSchema } from '${modelsImport}/common';
|
|
7572
7602
|
import { UserResponseSchema } from '${modelsImport}/user';
|
|
7573
7603
|
|
|
7574
7604
|
export const getUserEndpoint = e
|
|
7575
7605
|
.get('/users/:id')
|
|
7576
|
-
.params(
|
|
7606
|
+
.params(IdParamsSchema)
|
|
7577
7607
|
.output(UserResponseSchema)
|
|
7578
7608
|
.handle(async ({ params }) => ({
|
|
7579
7609
|
id: params.id,
|
|
@@ -7599,6 +7629,91 @@ export const getUserEndpoint = e
|
|
|
7599
7629
|
`
|
|
7600
7630
|
}
|
|
7601
7631
|
];
|
|
7632
|
+
if (options.monorepo) {
|
|
7633
|
+
files.push({
|
|
7634
|
+
path: "src/services/auth.ts",
|
|
7635
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
7636
|
+
|
|
7637
|
+
export interface Session {
|
|
7638
|
+
user: {
|
|
7639
|
+
id: string;
|
|
7640
|
+
email: string;
|
|
7641
|
+
name: string;
|
|
7642
|
+
};
|
|
7643
|
+
}
|
|
7644
|
+
|
|
7645
|
+
export interface AuthClient {
|
|
7646
|
+
getSession: (cookie: string) => Promise<Session | null>;
|
|
7647
|
+
}
|
|
7648
|
+
|
|
7649
|
+
export const authService = {
|
|
7650
|
+
serviceName: 'auth' as const,
|
|
7651
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
7652
|
+
const logger = context.getLogger();
|
|
7653
|
+
|
|
7654
|
+
const config = envParser
|
|
7655
|
+
.create((get) => ({
|
|
7656
|
+
url: get('AUTH_URL').string(),
|
|
7657
|
+
}))
|
|
7658
|
+
.parse();
|
|
7659
|
+
|
|
7660
|
+
logger.info({ authUrl: config.url }, 'Auth service configured');
|
|
7661
|
+
|
|
7662
|
+
return {
|
|
7663
|
+
getSession: async (cookie: string): Promise<Session | null> => {
|
|
7664
|
+
const res = await fetch(\`\${config.url}/api/auth/get-session\`, {
|
|
7665
|
+
headers: { cookie },
|
|
7666
|
+
});
|
|
7667
|
+
if (!res.ok) return null;
|
|
7668
|
+
return res.json();
|
|
7669
|
+
},
|
|
7670
|
+
};
|
|
7671
|
+
},
|
|
7672
|
+
} satisfies Service<'auth', AuthClient>;
|
|
7673
|
+
`
|
|
7674
|
+
});
|
|
7675
|
+
files.push({
|
|
7676
|
+
path: "src/router.ts",
|
|
7677
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
7678
|
+
import { UnauthorizedError } from '@geekmidas/errors';
|
|
7679
|
+
import { authService, type Session } from './services/auth.js';
|
|
7680
|
+
import { logger } from './config/logger.js';
|
|
7681
|
+
|
|
7682
|
+
// Public router - no auth required
|
|
7683
|
+
export const publicRouter = e.logger(logger);
|
|
7684
|
+
|
|
7685
|
+
// Router with auth service available (but session not enforced)
|
|
7686
|
+
export const r = publicRouter.services([authService]);
|
|
7687
|
+
|
|
7688
|
+
// Session router - requires active session, throws if not authenticated
|
|
7689
|
+
export const sessionRouter = r.session<Session>(async ({ services, header }) => {
|
|
7690
|
+
const cookie = header('cookie') || '';
|
|
7691
|
+
const session = await services.auth.getSession(cookie);
|
|
7692
|
+
|
|
7693
|
+
if (!session?.user) {
|
|
7694
|
+
throw new UnauthorizedError('No active session');
|
|
7695
|
+
}
|
|
7696
|
+
|
|
7697
|
+
return session;
|
|
7698
|
+
});
|
|
7699
|
+
`
|
|
7700
|
+
});
|
|
7701
|
+
files.push({
|
|
7702
|
+
path: getRoutePath("profile.ts"),
|
|
7703
|
+
content: `import { z } from 'zod';
|
|
7704
|
+
import { sessionRouter } from '~/router';
|
|
7705
|
+
|
|
7706
|
+
export const profileEndpoint = sessionRouter
|
|
7707
|
+
.get('/profile')
|
|
7708
|
+
.output(z.object({
|
|
7709
|
+
id: z.string(),
|
|
7710
|
+
email: z.string(),
|
|
7711
|
+
name: z.string(),
|
|
7712
|
+
}))
|
|
7713
|
+
.handle(async ({ session }) => session.user);
|
|
7714
|
+
`
|
|
7715
|
+
});
|
|
7716
|
+
}
|
|
7602
7717
|
if (options.database) files.push({
|
|
7603
7718
|
path: "src/services/database.ts",
|
|
7604
7719
|
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
@@ -8361,6 +8476,1419 @@ function generateSourceFiles(options, template) {
|
|
|
8361
8476
|
return template.files(options);
|
|
8362
8477
|
}
|
|
8363
8478
|
|
|
8479
|
+
//#endregion
|
|
8480
|
+
//#region src/init/generators/ui.ts
|
|
8481
|
+
/**
|
|
8482
|
+
* Generate UI package files for fullstack template
|
|
8483
|
+
* Based on @geekmidas/ui with shadcn/ui, Tailwind CSS v4, and Storybook
|
|
8484
|
+
*/
|
|
8485
|
+
function generateUiPackageFiles(options) {
|
|
8486
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
8487
|
+
const packageName = `@${options.name}/ui`;
|
|
8488
|
+
const packageJson = {
|
|
8489
|
+
name: packageName,
|
|
8490
|
+
version: "0.0.1",
|
|
8491
|
+
private: true,
|
|
8492
|
+
type: "module",
|
|
8493
|
+
exports: {
|
|
8494
|
+
".": "./src/index.ts",
|
|
8495
|
+
"./components": "./src/components/index.ts",
|
|
8496
|
+
"./lib/utils": "./src/lib/utils.ts",
|
|
8497
|
+
"./styles": "./src/styles/globals.css"
|
|
8498
|
+
},
|
|
8499
|
+
scripts: {
|
|
8500
|
+
"ts:check": "tsc --noEmit",
|
|
8501
|
+
storybook: "storybook dev -p 6006",
|
|
8502
|
+
"build:storybook": "storybook build -o dist/storybook"
|
|
8503
|
+
},
|
|
8504
|
+
dependencies: {
|
|
8505
|
+
"@radix-ui/react-dialog": "~1.1.4",
|
|
8506
|
+
"@radix-ui/react-label": "~2.1.2",
|
|
8507
|
+
"@radix-ui/react-separator": "~1.1.2",
|
|
8508
|
+
"@radix-ui/react-slot": "~1.2.4",
|
|
8509
|
+
"@radix-ui/react-tabs": "~1.1.2",
|
|
8510
|
+
"@radix-ui/react-tooltip": "~1.1.6",
|
|
8511
|
+
"class-variance-authority": "~0.7.1",
|
|
8512
|
+
clsx: "^2.1.1",
|
|
8513
|
+
"lucide-react": "~0.562.0",
|
|
8514
|
+
"tailwind-merge": "~3.4.0"
|
|
8515
|
+
},
|
|
8516
|
+
devDependencies: {
|
|
8517
|
+
"@storybook/addon-a11y": "^8.4.7",
|
|
8518
|
+
"@storybook/addon-essentials": "^8.4.7",
|
|
8519
|
+
"@storybook/addon-interactions": "^8.4.7",
|
|
8520
|
+
"@storybook/react": "^8.4.7",
|
|
8521
|
+
"@storybook/react-vite": "^8.4.7",
|
|
8522
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
8523
|
+
"@types/react": "^19.0.0",
|
|
8524
|
+
"@types/react-dom": "^19.0.0",
|
|
8525
|
+
react: "^19.0.0",
|
|
8526
|
+
"react-dom": "^19.0.0",
|
|
8527
|
+
storybook: "^8.4.7",
|
|
8528
|
+
tailwindcss: "^4.0.0",
|
|
8529
|
+
typescript: "^5.8.2",
|
|
8530
|
+
vite: "^6.0.0"
|
|
8531
|
+
},
|
|
8532
|
+
peerDependencies: {
|
|
8533
|
+
react: ">=18.0.0",
|
|
8534
|
+
"react-dom": ">=18.0.0",
|
|
8535
|
+
tailwindcss: ">=4.0.0"
|
|
8536
|
+
}
|
|
8537
|
+
};
|
|
8538
|
+
const tsConfig = {
|
|
8539
|
+
extends: "../../tsconfig.json",
|
|
8540
|
+
compilerOptions: {
|
|
8541
|
+
jsx: "react-jsx",
|
|
8542
|
+
lib: [
|
|
8543
|
+
"ES2023",
|
|
8544
|
+
"DOM",
|
|
8545
|
+
"DOM.Iterable"
|
|
8546
|
+
],
|
|
8547
|
+
noEmit: true,
|
|
8548
|
+
baseUrl: ".",
|
|
8549
|
+
paths: { "~/*": ["./src/*"] }
|
|
8550
|
+
},
|
|
8551
|
+
include: ["src/**/*"],
|
|
8552
|
+
exclude: [
|
|
8553
|
+
"node_modules",
|
|
8554
|
+
"dist",
|
|
8555
|
+
"**/*.stories.tsx"
|
|
8556
|
+
]
|
|
8557
|
+
};
|
|
8558
|
+
const componentsJson = {
|
|
8559
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
8560
|
+
style: "new-york",
|
|
8561
|
+
rsc: false,
|
|
8562
|
+
tsx: true,
|
|
8563
|
+
tailwind: {
|
|
8564
|
+
config: "",
|
|
8565
|
+
css: "src/styles/globals.css",
|
|
8566
|
+
baseColor: "neutral",
|
|
8567
|
+
cssVariables: true,
|
|
8568
|
+
prefix: ""
|
|
8569
|
+
},
|
|
8570
|
+
aliases: {
|
|
8571
|
+
components: "~/components",
|
|
8572
|
+
utils: "~/lib/utils",
|
|
8573
|
+
ui: "~/components/ui",
|
|
8574
|
+
lib: "~/lib",
|
|
8575
|
+
hooks: "~/hooks"
|
|
8576
|
+
},
|
|
8577
|
+
iconLibrary: "lucide"
|
|
8578
|
+
};
|
|
8579
|
+
const storybookMain = `import type { StorybookConfig } from '@storybook/react-vite';
|
|
8580
|
+
|
|
8581
|
+
const config: StorybookConfig = {
|
|
8582
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
8583
|
+
addons: [
|
|
8584
|
+
'@storybook/addon-essentials',
|
|
8585
|
+
'@storybook/addon-interactions',
|
|
8586
|
+
'@storybook/addon-a11y',
|
|
8587
|
+
],
|
|
8588
|
+
framework: {
|
|
8589
|
+
name: '@storybook/react-vite',
|
|
8590
|
+
options: {},
|
|
8591
|
+
},
|
|
8592
|
+
docs: {
|
|
8593
|
+
autodocs: 'tag',
|
|
8594
|
+
},
|
|
8595
|
+
viteFinal: async (config) => {
|
|
8596
|
+
// Add Tailwind CSS v4 plugin
|
|
8597
|
+
const tailwindcss = await import('@tailwindcss/vite');
|
|
8598
|
+
config.plugins = config.plugins || [];
|
|
8599
|
+
config.plugins.push(tailwindcss.default());
|
|
8600
|
+
return config;
|
|
8601
|
+
},
|
|
8602
|
+
};
|
|
8603
|
+
|
|
8604
|
+
export default config;
|
|
8605
|
+
`;
|
|
8606
|
+
const storybookPreview = `import type { Preview } from '@storybook/react';
|
|
8607
|
+
import '../src/styles/globals.css';
|
|
8608
|
+
|
|
8609
|
+
const preview: Preview = {
|
|
8610
|
+
parameters: {
|
|
8611
|
+
backgrounds: {
|
|
8612
|
+
default: 'dark',
|
|
8613
|
+
values: [
|
|
8614
|
+
{ name: 'dark', value: '#171717' },
|
|
8615
|
+
{ name: 'surface', value: '#1c1c1c' },
|
|
8616
|
+
{ name: 'light', value: '#fafafa' },
|
|
8617
|
+
],
|
|
8618
|
+
},
|
|
8619
|
+
controls: {
|
|
8620
|
+
matchers: {
|
|
8621
|
+
color: /(background|color)$/i,
|
|
8622
|
+
date: /Date$/i,
|
|
8623
|
+
},
|
|
8624
|
+
},
|
|
8625
|
+
layout: 'centered',
|
|
8626
|
+
},
|
|
8627
|
+
};
|
|
8628
|
+
|
|
8629
|
+
export default preview;
|
|
8630
|
+
`;
|
|
8631
|
+
const globalsCss = `@import "tailwindcss";
|
|
8632
|
+
|
|
8633
|
+
@theme {
|
|
8634
|
+
--color-background: hsl(var(--background));
|
|
8635
|
+
--color-foreground: hsl(var(--foreground));
|
|
8636
|
+
--color-card: hsl(var(--card));
|
|
8637
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
8638
|
+
--color-popover: hsl(var(--popover));
|
|
8639
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
8640
|
+
--color-primary: hsl(var(--primary));
|
|
8641
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
8642
|
+
--color-secondary: hsl(var(--secondary));
|
|
8643
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
8644
|
+
--color-muted: hsl(var(--muted));
|
|
8645
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
8646
|
+
--color-accent: hsl(var(--accent));
|
|
8647
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
8648
|
+
--color-destructive: hsl(var(--destructive));
|
|
8649
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
8650
|
+
--color-border: hsl(var(--border));
|
|
8651
|
+
--color-input: hsl(var(--input));
|
|
8652
|
+
--color-ring: hsl(var(--ring));
|
|
8653
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
8654
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
8655
|
+
--radius-lg: var(--radius);
|
|
8656
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
8657
|
+
}
|
|
8658
|
+
|
|
8659
|
+
@layer base {
|
|
8660
|
+
:root {
|
|
8661
|
+
--background: 0 0% 100%;
|
|
8662
|
+
--foreground: 0 0% 3.9%;
|
|
8663
|
+
--card: 0 0% 100%;
|
|
8664
|
+
--card-foreground: 0 0% 3.9%;
|
|
8665
|
+
--popover: 0 0% 100%;
|
|
8666
|
+
--popover-foreground: 0 0% 3.9%;
|
|
8667
|
+
--primary: 160 84% 39%;
|
|
8668
|
+
--primary-foreground: 0 0% 98%;
|
|
8669
|
+
--secondary: 0 0% 96.1%;
|
|
8670
|
+
--secondary-foreground: 0 0% 9%;
|
|
8671
|
+
--muted: 0 0% 96.1%;
|
|
8672
|
+
--muted-foreground: 0 0% 45.1%;
|
|
8673
|
+
--accent: 0 0% 96.1%;
|
|
8674
|
+
--accent-foreground: 0 0% 9%;
|
|
8675
|
+
--destructive: 0 84.2% 60.2%;
|
|
8676
|
+
--destructive-foreground: 0 0% 98%;
|
|
8677
|
+
--border: 0 0% 89.8%;
|
|
8678
|
+
--input: 0 0% 89.8%;
|
|
8679
|
+
--ring: 160 84% 39%;
|
|
8680
|
+
--radius: 0.5rem;
|
|
8681
|
+
}
|
|
8682
|
+
|
|
8683
|
+
.dark {
|
|
8684
|
+
--background: 0 0% 9%;
|
|
8685
|
+
--foreground: 0 0% 98%;
|
|
8686
|
+
--card: 0 0% 11%;
|
|
8687
|
+
--card-foreground: 0 0% 98%;
|
|
8688
|
+
--popover: 0 0% 11%;
|
|
8689
|
+
--popover-foreground: 0 0% 98%;
|
|
8690
|
+
--primary: 160 84% 52%;
|
|
8691
|
+
--primary-foreground: 0 0% 9%;
|
|
8692
|
+
--secondary: 0 0% 15%;
|
|
8693
|
+
--secondary-foreground: 0 0% 98%;
|
|
8694
|
+
--muted: 0 0% 15%;
|
|
8695
|
+
--muted-foreground: 0 0% 64%;
|
|
8696
|
+
--accent: 0 0% 15%;
|
|
8697
|
+
--accent-foreground: 0 0% 98%;
|
|
8698
|
+
--destructive: 0 62.8% 50.6%;
|
|
8699
|
+
--destructive-foreground: 0 0% 98%;
|
|
8700
|
+
--border: 0 0% 18%;
|
|
8701
|
+
--input: 0 0% 18%;
|
|
8702
|
+
--ring: 160 84% 52%;
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
@layer base {
|
|
8707
|
+
* {
|
|
8708
|
+
@apply border-border;
|
|
8709
|
+
}
|
|
8710
|
+
body {
|
|
8711
|
+
@apply bg-background text-foreground;
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
`;
|
|
8715
|
+
const utilsTs = `import { type ClassValue, clsx } from 'clsx';
|
|
8716
|
+
import { twMerge } from 'tailwind-merge';
|
|
8717
|
+
|
|
8718
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8719
|
+
return twMerge(clsx(inputs));
|
|
8720
|
+
}
|
|
8721
|
+
`;
|
|
8722
|
+
const buttonTsx = `import { Slot } from '@radix-ui/react-slot';
|
|
8723
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
8724
|
+
import * as React from 'react';
|
|
8725
|
+
|
|
8726
|
+
import { cn } from '~/lib/utils';
|
|
8727
|
+
|
|
8728
|
+
const buttonVariants = cva(
|
|
8729
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
8730
|
+
{
|
|
8731
|
+
variants: {
|
|
8732
|
+
variant: {
|
|
8733
|
+
default:
|
|
8734
|
+
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
8735
|
+
destructive:
|
|
8736
|
+
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
|
8737
|
+
outline:
|
|
8738
|
+
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
8739
|
+
secondary:
|
|
8740
|
+
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
|
8741
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
8742
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
8743
|
+
},
|
|
8744
|
+
size: {
|
|
8745
|
+
default: 'h-9 px-4 py-2',
|
|
8746
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
8747
|
+
lg: 'h-10 rounded-md px-8',
|
|
8748
|
+
icon: 'h-9 w-9',
|
|
8749
|
+
},
|
|
8750
|
+
},
|
|
8751
|
+
defaultVariants: {
|
|
8752
|
+
variant: 'default',
|
|
8753
|
+
size: 'default',
|
|
8754
|
+
},
|
|
8755
|
+
},
|
|
8756
|
+
);
|
|
8757
|
+
|
|
8758
|
+
export interface ButtonProps
|
|
8759
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8760
|
+
VariantProps<typeof buttonVariants> {
|
|
8761
|
+
asChild?: boolean;
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8764
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
8765
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
8766
|
+
const Comp = asChild ? Slot : 'button';
|
|
8767
|
+
return (
|
|
8768
|
+
<Comp
|
|
8769
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
8770
|
+
ref={ref}
|
|
8771
|
+
{...props}
|
|
8772
|
+
/>
|
|
8773
|
+
);
|
|
8774
|
+
},
|
|
8775
|
+
);
|
|
8776
|
+
Button.displayName = 'Button';
|
|
8777
|
+
|
|
8778
|
+
export { Button, buttonVariants };
|
|
8779
|
+
`;
|
|
8780
|
+
const buttonStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
8781
|
+
import { Button } from '.';
|
|
8782
|
+
|
|
8783
|
+
const meta: Meta<typeof Button> = {
|
|
8784
|
+
title: 'Components/Button',
|
|
8785
|
+
component: Button,
|
|
8786
|
+
tags: ['autodocs'],
|
|
8787
|
+
argTypes: {
|
|
8788
|
+
variant: {
|
|
8789
|
+
control: 'select',
|
|
8790
|
+
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
|
|
8791
|
+
},
|
|
8792
|
+
size: {
|
|
8793
|
+
control: 'select',
|
|
8794
|
+
options: ['default', 'sm', 'lg', 'icon'],
|
|
8795
|
+
},
|
|
8796
|
+
},
|
|
8797
|
+
};
|
|
8798
|
+
|
|
8799
|
+
export default meta;
|
|
8800
|
+
type Story = StoryObj<typeof Button>;
|
|
8801
|
+
|
|
8802
|
+
export const Default: Story = {
|
|
8803
|
+
args: {
|
|
8804
|
+
children: 'Button',
|
|
8805
|
+
variant: 'default',
|
|
8806
|
+
size: 'default',
|
|
8807
|
+
},
|
|
8808
|
+
};
|
|
8809
|
+
|
|
8810
|
+
export const Secondary: Story = {
|
|
8811
|
+
args: {
|
|
8812
|
+
children: 'Secondary',
|
|
8813
|
+
variant: 'secondary',
|
|
8814
|
+
},
|
|
8815
|
+
};
|
|
8816
|
+
|
|
8817
|
+
export const Destructive: Story = {
|
|
8818
|
+
args: {
|
|
8819
|
+
children: 'Destructive',
|
|
8820
|
+
variant: 'destructive',
|
|
8821
|
+
},
|
|
8822
|
+
};
|
|
8823
|
+
|
|
8824
|
+
export const Outline: Story = {
|
|
8825
|
+
args: {
|
|
8826
|
+
children: 'Outline',
|
|
8827
|
+
variant: 'outline',
|
|
8828
|
+
},
|
|
8829
|
+
};
|
|
8830
|
+
|
|
8831
|
+
export const Ghost: Story = {
|
|
8832
|
+
args: {
|
|
8833
|
+
children: 'Ghost',
|
|
8834
|
+
variant: 'ghost',
|
|
8835
|
+
},
|
|
8836
|
+
};
|
|
8837
|
+
|
|
8838
|
+
export const Link: Story = {
|
|
8839
|
+
args: {
|
|
8840
|
+
children: 'Link',
|
|
8841
|
+
variant: 'link',
|
|
8842
|
+
},
|
|
8843
|
+
};
|
|
8844
|
+
`;
|
|
8845
|
+
const inputTsx = `import * as React from 'react';
|
|
8846
|
+
|
|
8847
|
+
import { cn } from '~/lib/utils';
|
|
8848
|
+
|
|
8849
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
8850
|
+
({ className, type, ...props }, ref) => {
|
|
8851
|
+
return (
|
|
8852
|
+
<input
|
|
8853
|
+
type={type}
|
|
8854
|
+
className={cn(
|
|
8855
|
+
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
8856
|
+
className,
|
|
8857
|
+
)}
|
|
8858
|
+
ref={ref}
|
|
8859
|
+
{...props}
|
|
8860
|
+
/>
|
|
8861
|
+
);
|
|
8862
|
+
},
|
|
8863
|
+
);
|
|
8864
|
+
Input.displayName = 'Input';
|
|
8865
|
+
|
|
8866
|
+
export { Input };
|
|
8867
|
+
`;
|
|
8868
|
+
const cardTsx = `import * as React from 'react';
|
|
8869
|
+
|
|
8870
|
+
import { cn } from '~/lib/utils';
|
|
8871
|
+
|
|
8872
|
+
const Card = React.forwardRef<
|
|
8873
|
+
HTMLDivElement,
|
|
8874
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8875
|
+
>(({ className, ...props }, ref) => (
|
|
8876
|
+
<div
|
|
8877
|
+
ref={ref}
|
|
8878
|
+
className={cn(
|
|
8879
|
+
'rounded-xl border bg-card text-card-foreground shadow',
|
|
8880
|
+
className,
|
|
8881
|
+
)}
|
|
8882
|
+
{...props}
|
|
8883
|
+
/>
|
|
8884
|
+
));
|
|
8885
|
+
Card.displayName = 'Card';
|
|
8886
|
+
|
|
8887
|
+
const CardHeader = React.forwardRef<
|
|
8888
|
+
HTMLDivElement,
|
|
8889
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8890
|
+
>(({ className, ...props }, ref) => (
|
|
8891
|
+
<div
|
|
8892
|
+
ref={ref}
|
|
8893
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
8894
|
+
{...props}
|
|
8895
|
+
/>
|
|
8896
|
+
));
|
|
8897
|
+
CardHeader.displayName = 'CardHeader';
|
|
8898
|
+
|
|
8899
|
+
const CardTitle = React.forwardRef<
|
|
8900
|
+
HTMLDivElement,
|
|
8901
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8902
|
+
>(({ className, ...props }, ref) => (
|
|
8903
|
+
<div
|
|
8904
|
+
ref={ref}
|
|
8905
|
+
className={cn('font-semibold leading-none tracking-tight', className)}
|
|
8906
|
+
{...props}
|
|
8907
|
+
/>
|
|
8908
|
+
));
|
|
8909
|
+
CardTitle.displayName = 'CardTitle';
|
|
8910
|
+
|
|
8911
|
+
const CardDescription = React.forwardRef<
|
|
8912
|
+
HTMLDivElement,
|
|
8913
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8914
|
+
>(({ className, ...props }, ref) => (
|
|
8915
|
+
<div
|
|
8916
|
+
ref={ref}
|
|
8917
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
8918
|
+
{...props}
|
|
8919
|
+
/>
|
|
8920
|
+
));
|
|
8921
|
+
CardDescription.displayName = 'CardDescription';
|
|
8922
|
+
|
|
8923
|
+
const CardContent = React.forwardRef<
|
|
8924
|
+
HTMLDivElement,
|
|
8925
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8926
|
+
>(({ className, ...props }, ref) => (
|
|
8927
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
8928
|
+
));
|
|
8929
|
+
CardContent.displayName = 'CardContent';
|
|
8930
|
+
|
|
8931
|
+
const CardFooter = React.forwardRef<
|
|
8932
|
+
HTMLDivElement,
|
|
8933
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8934
|
+
>(({ className, ...props }, ref) => (
|
|
8935
|
+
<div
|
|
8936
|
+
ref={ref}
|
|
8937
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
8938
|
+
{...props}
|
|
8939
|
+
/>
|
|
8940
|
+
));
|
|
8941
|
+
CardFooter.displayName = 'CardFooter';
|
|
8942
|
+
|
|
8943
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
8944
|
+
`;
|
|
8945
|
+
const inputStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
8946
|
+
import { Input } from '.';
|
|
8947
|
+
|
|
8948
|
+
const meta: Meta<typeof Input> = {
|
|
8949
|
+
title: 'Components/Input',
|
|
8950
|
+
component: Input,
|
|
8951
|
+
tags: ['autodocs'],
|
|
8952
|
+
argTypes: {
|
|
8953
|
+
type: {
|
|
8954
|
+
control: 'select',
|
|
8955
|
+
options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url'],
|
|
8956
|
+
},
|
|
8957
|
+
disabled: {
|
|
8958
|
+
control: 'boolean',
|
|
8959
|
+
},
|
|
8960
|
+
},
|
|
8961
|
+
};
|
|
8962
|
+
|
|
8963
|
+
export default meta;
|
|
8964
|
+
type Story = StoryObj<typeof Input>;
|
|
8965
|
+
|
|
8966
|
+
export const Default: Story = {
|
|
8967
|
+
args: {
|
|
8968
|
+
placeholder: 'Enter text...',
|
|
8969
|
+
},
|
|
8970
|
+
};
|
|
8971
|
+
|
|
8972
|
+
export const Email: Story = {
|
|
8973
|
+
args: {
|
|
8974
|
+
type: 'email',
|
|
8975
|
+
placeholder: 'Enter email...',
|
|
8976
|
+
},
|
|
8977
|
+
};
|
|
8978
|
+
|
|
8979
|
+
export const Password: Story = {
|
|
8980
|
+
args: {
|
|
8981
|
+
type: 'password',
|
|
8982
|
+
placeholder: 'Enter password...',
|
|
8983
|
+
},
|
|
8984
|
+
};
|
|
8985
|
+
|
|
8986
|
+
export const Disabled: Story = {
|
|
8987
|
+
args: {
|
|
8988
|
+
placeholder: 'Disabled input',
|
|
8989
|
+
disabled: true,
|
|
8990
|
+
},
|
|
8991
|
+
};
|
|
8992
|
+
|
|
8993
|
+
export const WithValue: Story = {
|
|
8994
|
+
args: {
|
|
8995
|
+
defaultValue: 'Hello World',
|
|
8996
|
+
},
|
|
8997
|
+
};
|
|
8998
|
+
`;
|
|
8999
|
+
const cardStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9000
|
+
import { Button } from '../button';
|
|
9001
|
+
import {
|
|
9002
|
+
Card,
|
|
9003
|
+
CardContent,
|
|
9004
|
+
CardDescription,
|
|
9005
|
+
CardFooter,
|
|
9006
|
+
CardHeader,
|
|
9007
|
+
CardTitle,
|
|
9008
|
+
} from '.';
|
|
9009
|
+
import { Input } from '../input';
|
|
9010
|
+
|
|
9011
|
+
const meta: Meta<typeof Card> = {
|
|
9012
|
+
title: 'Components/Card',
|
|
9013
|
+
component: Card,
|
|
9014
|
+
tags: ['autodocs'],
|
|
9015
|
+
};
|
|
9016
|
+
|
|
9017
|
+
export default meta;
|
|
9018
|
+
type Story = StoryObj<typeof Card>;
|
|
9019
|
+
|
|
9020
|
+
export const Default: Story = {
|
|
9021
|
+
render: () => (
|
|
9022
|
+
<Card className="w-[350px]">
|
|
9023
|
+
<CardHeader>
|
|
9024
|
+
<CardTitle>Card Title</CardTitle>
|
|
9025
|
+
<CardDescription>Card description goes here.</CardDescription>
|
|
9026
|
+
</CardHeader>
|
|
9027
|
+
<CardContent>
|
|
9028
|
+
<p>Card content goes here.</p>
|
|
9029
|
+
</CardContent>
|
|
9030
|
+
</Card>
|
|
9031
|
+
),
|
|
9032
|
+
};
|
|
9033
|
+
|
|
9034
|
+
export const WithFooter: Story = {
|
|
9035
|
+
render: () => (
|
|
9036
|
+
<Card className="w-[350px]">
|
|
9037
|
+
<CardHeader>
|
|
9038
|
+
<CardTitle>Create Account</CardTitle>
|
|
9039
|
+
<CardDescription>Enter your details below.</CardDescription>
|
|
9040
|
+
</CardHeader>
|
|
9041
|
+
<CardContent className="space-y-4">
|
|
9042
|
+
<Input placeholder="Email" type="email" />
|
|
9043
|
+
<Input placeholder="Password" type="password" />
|
|
9044
|
+
</CardContent>
|
|
9045
|
+
<CardFooter className="flex justify-between">
|
|
9046
|
+
<Button variant="outline">Cancel</Button>
|
|
9047
|
+
<Button>Create</Button>
|
|
9048
|
+
</CardFooter>
|
|
9049
|
+
</Card>
|
|
9050
|
+
),
|
|
9051
|
+
};
|
|
9052
|
+
|
|
9053
|
+
export const Simple: Story = {
|
|
9054
|
+
render: () => (
|
|
9055
|
+
<Card className="w-[350px] p-6">
|
|
9056
|
+
<p>Simple card with just content.</p>
|
|
9057
|
+
</Card>
|
|
9058
|
+
),
|
|
9059
|
+
};
|
|
9060
|
+
`;
|
|
9061
|
+
const labelTsx = `import * as LabelPrimitive from '@radix-ui/react-label';
|
|
9062
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
9063
|
+
import * as React from 'react';
|
|
9064
|
+
|
|
9065
|
+
import { cn } from '~/lib/utils';
|
|
9066
|
+
|
|
9067
|
+
const labelVariants = cva(
|
|
9068
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
9069
|
+
);
|
|
9070
|
+
|
|
9071
|
+
const Label = React.forwardRef<
|
|
9072
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
9073
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
9074
|
+
VariantProps<typeof labelVariants>
|
|
9075
|
+
>(({ className, ...props }, ref) => (
|
|
9076
|
+
<LabelPrimitive.Root
|
|
9077
|
+
ref={ref}
|
|
9078
|
+
className={cn(labelVariants(), className)}
|
|
9079
|
+
{...props}
|
|
9080
|
+
/>
|
|
9081
|
+
));
|
|
9082
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
9083
|
+
|
|
9084
|
+
export { Label };
|
|
9085
|
+
`;
|
|
9086
|
+
const labelStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9087
|
+
import { Input } from '../input';
|
|
9088
|
+
import { Label } from '.';
|
|
9089
|
+
|
|
9090
|
+
const meta: Meta<typeof Label> = {
|
|
9091
|
+
title: 'Components/Label',
|
|
9092
|
+
component: Label,
|
|
9093
|
+
tags: ['autodocs'],
|
|
9094
|
+
};
|
|
9095
|
+
|
|
9096
|
+
export default meta;
|
|
9097
|
+
type Story = StoryObj<typeof Label>;
|
|
9098
|
+
|
|
9099
|
+
export const Default: Story = {
|
|
9100
|
+
args: {
|
|
9101
|
+
children: 'Label',
|
|
9102
|
+
},
|
|
9103
|
+
};
|
|
9104
|
+
|
|
9105
|
+
export const WithInput: Story = {
|
|
9106
|
+
render: () => (
|
|
9107
|
+
<div className="grid w-full max-w-sm items-center gap-1.5">
|
|
9108
|
+
<Label htmlFor="email">Email</Label>
|
|
9109
|
+
<Input type="email" id="email" placeholder="Email" />
|
|
9110
|
+
</div>
|
|
9111
|
+
),
|
|
9112
|
+
};
|
|
9113
|
+
|
|
9114
|
+
export const Disabled: Story = {
|
|
9115
|
+
render: () => (
|
|
9116
|
+
<div className="grid w-full max-w-sm items-center gap-1.5">
|
|
9117
|
+
<Label htmlFor="disabled" className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
9118
|
+
Disabled
|
|
9119
|
+
</Label>
|
|
9120
|
+
<Input type="text" id="disabled" placeholder="Disabled" disabled className="peer" />
|
|
9121
|
+
</div>
|
|
9122
|
+
),
|
|
9123
|
+
};
|
|
9124
|
+
`;
|
|
9125
|
+
const badgeTsx = `import { cva, type VariantProps } from 'class-variance-authority';
|
|
9126
|
+
import * as React from 'react';
|
|
9127
|
+
|
|
9128
|
+
import { cn } from '~/lib/utils';
|
|
9129
|
+
|
|
9130
|
+
const badgeVariants = cva(
|
|
9131
|
+
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
9132
|
+
{
|
|
9133
|
+
variants: {
|
|
9134
|
+
variant: {
|
|
9135
|
+
default:
|
|
9136
|
+
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
|
9137
|
+
secondary:
|
|
9138
|
+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
9139
|
+
destructive:
|
|
9140
|
+
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
|
9141
|
+
outline: 'text-foreground',
|
|
9142
|
+
},
|
|
9143
|
+
},
|
|
9144
|
+
defaultVariants: {
|
|
9145
|
+
variant: 'default',
|
|
9146
|
+
},
|
|
9147
|
+
},
|
|
9148
|
+
);
|
|
9149
|
+
|
|
9150
|
+
export interface BadgeProps
|
|
9151
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
9152
|
+
VariantProps<typeof badgeVariants> {}
|
|
9153
|
+
|
|
9154
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
9155
|
+
return (
|
|
9156
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
9157
|
+
);
|
|
9158
|
+
}
|
|
9159
|
+
|
|
9160
|
+
export { Badge, badgeVariants };
|
|
9161
|
+
`;
|
|
9162
|
+
const badgeStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9163
|
+
import { Badge } from '.';
|
|
9164
|
+
|
|
9165
|
+
const meta: Meta<typeof Badge> = {
|
|
9166
|
+
title: 'Components/Badge',
|
|
9167
|
+
component: Badge,
|
|
9168
|
+
tags: ['autodocs'],
|
|
9169
|
+
argTypes: {
|
|
9170
|
+
variant: {
|
|
9171
|
+
control: 'select',
|
|
9172
|
+
options: ['default', 'secondary', 'destructive', 'outline'],
|
|
9173
|
+
},
|
|
9174
|
+
},
|
|
9175
|
+
};
|
|
9176
|
+
|
|
9177
|
+
export default meta;
|
|
9178
|
+
type Story = StoryObj<typeof Badge>;
|
|
9179
|
+
|
|
9180
|
+
export const Default: Story = {
|
|
9181
|
+
args: {
|
|
9182
|
+
children: 'Badge',
|
|
9183
|
+
variant: 'default',
|
|
9184
|
+
},
|
|
9185
|
+
};
|
|
9186
|
+
|
|
9187
|
+
export const Secondary: Story = {
|
|
9188
|
+
args: {
|
|
9189
|
+
children: 'Secondary',
|
|
9190
|
+
variant: 'secondary',
|
|
9191
|
+
},
|
|
9192
|
+
};
|
|
9193
|
+
|
|
9194
|
+
export const Destructive: Story = {
|
|
9195
|
+
args: {
|
|
9196
|
+
children: 'Destructive',
|
|
9197
|
+
variant: 'destructive',
|
|
9198
|
+
},
|
|
9199
|
+
};
|
|
9200
|
+
|
|
9201
|
+
export const Outline: Story = {
|
|
9202
|
+
args: {
|
|
9203
|
+
children: 'Outline',
|
|
9204
|
+
variant: 'outline',
|
|
9205
|
+
},
|
|
9206
|
+
};
|
|
9207
|
+
`;
|
|
9208
|
+
const separatorTsx = `import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
9209
|
+
import * as React from 'react';
|
|
9210
|
+
|
|
9211
|
+
import { cn } from '~/lib/utils';
|
|
9212
|
+
|
|
9213
|
+
const Separator = React.forwardRef<
|
|
9214
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
9215
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
9216
|
+
>(
|
|
9217
|
+
(
|
|
9218
|
+
{ className, orientation = 'horizontal', decorative = true, ...props },
|
|
9219
|
+
ref,
|
|
9220
|
+
) => (
|
|
9221
|
+
<SeparatorPrimitive.Root
|
|
9222
|
+
ref={ref}
|
|
9223
|
+
decorative={decorative}
|
|
9224
|
+
orientation={orientation}
|
|
9225
|
+
className={cn(
|
|
9226
|
+
'shrink-0 bg-border',
|
|
9227
|
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
|
9228
|
+
className,
|
|
9229
|
+
)}
|
|
9230
|
+
{...props}
|
|
9231
|
+
/>
|
|
9232
|
+
),
|
|
9233
|
+
);
|
|
9234
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
9235
|
+
|
|
9236
|
+
export { Separator };
|
|
9237
|
+
`;
|
|
9238
|
+
const separatorStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9239
|
+
import { Separator } from '.';
|
|
9240
|
+
|
|
9241
|
+
const meta: Meta<typeof Separator> = {
|
|
9242
|
+
title: 'Components/Separator',
|
|
9243
|
+
component: Separator,
|
|
9244
|
+
tags: ['autodocs'],
|
|
9245
|
+
argTypes: {
|
|
9246
|
+
orientation: {
|
|
9247
|
+
control: 'select',
|
|
9248
|
+
options: ['horizontal', 'vertical'],
|
|
9249
|
+
},
|
|
9250
|
+
},
|
|
9251
|
+
};
|
|
9252
|
+
|
|
9253
|
+
export default meta;
|
|
9254
|
+
type Story = StoryObj<typeof Separator>;
|
|
9255
|
+
|
|
9256
|
+
export const Horizontal: Story = {
|
|
9257
|
+
render: () => (
|
|
9258
|
+
<div className="w-[300px]">
|
|
9259
|
+
<div className="space-y-1">
|
|
9260
|
+
<h4 className="text-sm font-medium leading-none">Radix Primitives</h4>
|
|
9261
|
+
<p className="text-sm text-muted-foreground">
|
|
9262
|
+
An open-source UI component library.
|
|
9263
|
+
</p>
|
|
9264
|
+
</div>
|
|
9265
|
+
<Separator className="my-4" />
|
|
9266
|
+
<div className="flex h-5 items-center space-x-4 text-sm">
|
|
9267
|
+
<div>Blog</div>
|
|
9268
|
+
<Separator orientation="vertical" />
|
|
9269
|
+
<div>Docs</div>
|
|
9270
|
+
<Separator orientation="vertical" />
|
|
9271
|
+
<div>Source</div>
|
|
9272
|
+
</div>
|
|
9273
|
+
</div>
|
|
9274
|
+
),
|
|
9275
|
+
};
|
|
9276
|
+
|
|
9277
|
+
export const Vertical: Story = {
|
|
9278
|
+
render: () => (
|
|
9279
|
+
<div className="flex h-5 items-center space-x-4 text-sm">
|
|
9280
|
+
<div>Blog</div>
|
|
9281
|
+
<Separator orientation="vertical" />
|
|
9282
|
+
<div>Docs</div>
|
|
9283
|
+
<Separator orientation="vertical" />
|
|
9284
|
+
<div>Source</div>
|
|
9285
|
+
</div>
|
|
9286
|
+
),
|
|
9287
|
+
};
|
|
9288
|
+
`;
|
|
9289
|
+
const tabsTsx = `import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
9290
|
+
import * as React from 'react';
|
|
9291
|
+
|
|
9292
|
+
import { cn } from '~/lib/utils';
|
|
9293
|
+
|
|
9294
|
+
const Tabs = TabsPrimitive.Root;
|
|
9295
|
+
|
|
9296
|
+
const TabsList = React.forwardRef<
|
|
9297
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
9298
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
9299
|
+
>(({ className, ...props }, ref) => (
|
|
9300
|
+
<TabsPrimitive.List
|
|
9301
|
+
ref={ref}
|
|
9302
|
+
className={cn(
|
|
9303
|
+
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
|
9304
|
+
className,
|
|
9305
|
+
)}
|
|
9306
|
+
{...props}
|
|
9307
|
+
/>
|
|
9308
|
+
));
|
|
9309
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
9310
|
+
|
|
9311
|
+
const TabsTrigger = React.forwardRef<
|
|
9312
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
9313
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
9314
|
+
>(({ className, ...props }, ref) => (
|
|
9315
|
+
<TabsPrimitive.Trigger
|
|
9316
|
+
ref={ref}
|
|
9317
|
+
className={cn(
|
|
9318
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
|
9319
|
+
className,
|
|
9320
|
+
)}
|
|
9321
|
+
{...props}
|
|
9322
|
+
/>
|
|
9323
|
+
));
|
|
9324
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
9325
|
+
|
|
9326
|
+
const TabsContent = React.forwardRef<
|
|
9327
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
9328
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
9329
|
+
>(({ className, ...props }, ref) => (
|
|
9330
|
+
<TabsPrimitive.Content
|
|
9331
|
+
ref={ref}
|
|
9332
|
+
className={cn(
|
|
9333
|
+
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
9334
|
+
className,
|
|
9335
|
+
)}
|
|
9336
|
+
{...props}
|
|
9337
|
+
/>
|
|
9338
|
+
));
|
|
9339
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
9340
|
+
|
|
9341
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
9342
|
+
`;
|
|
9343
|
+
const tabsStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9344
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '.';
|
|
9345
|
+
import { Button } from '../button';
|
|
9346
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../card';
|
|
9347
|
+
import { Input } from '../input';
|
|
9348
|
+
import { Label } from '../label';
|
|
9349
|
+
|
|
9350
|
+
const meta: Meta<typeof Tabs> = {
|
|
9351
|
+
title: 'Components/Tabs',
|
|
9352
|
+
component: Tabs,
|
|
9353
|
+
tags: ['autodocs'],
|
|
9354
|
+
};
|
|
9355
|
+
|
|
9356
|
+
export default meta;
|
|
9357
|
+
type Story = StoryObj<typeof Tabs>;
|
|
9358
|
+
|
|
9359
|
+
export const Default: Story = {
|
|
9360
|
+
render: () => (
|
|
9361
|
+
<Tabs defaultValue="account" className="w-[400px]">
|
|
9362
|
+
<TabsList>
|
|
9363
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
9364
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
9365
|
+
</TabsList>
|
|
9366
|
+
<TabsContent value="account">
|
|
9367
|
+
<Card>
|
|
9368
|
+
<CardHeader>
|
|
9369
|
+
<CardTitle>Account</CardTitle>
|
|
9370
|
+
<CardDescription>
|
|
9371
|
+
Make changes to your account here. Click save when you're done.
|
|
9372
|
+
</CardDescription>
|
|
9373
|
+
</CardHeader>
|
|
9374
|
+
<CardContent className="space-y-2">
|
|
9375
|
+
<div className="space-y-1">
|
|
9376
|
+
<Label htmlFor="name">Name</Label>
|
|
9377
|
+
<Input id="name" defaultValue="Pedro Duarte" />
|
|
9378
|
+
</div>
|
|
9379
|
+
<div className="space-y-1">
|
|
9380
|
+
<Label htmlFor="username">Username</Label>
|
|
9381
|
+
<Input id="username" defaultValue="@peduarte" />
|
|
9382
|
+
</div>
|
|
9383
|
+
</CardContent>
|
|
9384
|
+
<CardFooter>
|
|
9385
|
+
<Button>Save changes</Button>
|
|
9386
|
+
</CardFooter>
|
|
9387
|
+
</Card>
|
|
9388
|
+
</TabsContent>
|
|
9389
|
+
<TabsContent value="password">
|
|
9390
|
+
<Card>
|
|
9391
|
+
<CardHeader>
|
|
9392
|
+
<CardTitle>Password</CardTitle>
|
|
9393
|
+
<CardDescription>
|
|
9394
|
+
Change your password here. After saving, you'll be logged out.
|
|
9395
|
+
</CardDescription>
|
|
9396
|
+
</CardHeader>
|
|
9397
|
+
<CardContent className="space-y-2">
|
|
9398
|
+
<div className="space-y-1">
|
|
9399
|
+
<Label htmlFor="current">Current password</Label>
|
|
9400
|
+
<Input id="current" type="password" />
|
|
9401
|
+
</div>
|
|
9402
|
+
<div className="space-y-1">
|
|
9403
|
+
<Label htmlFor="new">New password</Label>
|
|
9404
|
+
<Input id="new" type="password" />
|
|
9405
|
+
</div>
|
|
9406
|
+
</CardContent>
|
|
9407
|
+
<CardFooter>
|
|
9408
|
+
<Button>Save password</Button>
|
|
9409
|
+
</CardFooter>
|
|
9410
|
+
</Card>
|
|
9411
|
+
</TabsContent>
|
|
9412
|
+
</Tabs>
|
|
9413
|
+
),
|
|
9414
|
+
};
|
|
9415
|
+
`;
|
|
9416
|
+
const tooltipTsx = `import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
9417
|
+
import * as React from 'react';
|
|
9418
|
+
|
|
9419
|
+
import { cn } from '~/lib/utils';
|
|
9420
|
+
|
|
9421
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
9422
|
+
|
|
9423
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
9424
|
+
|
|
9425
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
9426
|
+
|
|
9427
|
+
const TooltipContent = React.forwardRef<
|
|
9428
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
9429
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
9430
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
9431
|
+
<TooltipPrimitive.Portal>
|
|
9432
|
+
<TooltipPrimitive.Content
|
|
9433
|
+
ref={ref}
|
|
9434
|
+
sideOffset={sideOffset}
|
|
9435
|
+
className={cn(
|
|
9436
|
+
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
9437
|
+
className,
|
|
9438
|
+
)}
|
|
9439
|
+
{...props}
|
|
9440
|
+
/>
|
|
9441
|
+
</TooltipPrimitive.Portal>
|
|
9442
|
+
));
|
|
9443
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
9444
|
+
|
|
9445
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
9446
|
+
`;
|
|
9447
|
+
const tooltipStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9448
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.';
|
|
9449
|
+
import { Button } from '../button';
|
|
9450
|
+
|
|
9451
|
+
const meta: Meta<typeof Tooltip> = {
|
|
9452
|
+
title: 'Components/Tooltip',
|
|
9453
|
+
component: Tooltip,
|
|
9454
|
+
tags: ['autodocs'],
|
|
9455
|
+
decorators: [
|
|
9456
|
+
(Story) => (
|
|
9457
|
+
<TooltipProvider>
|
|
9458
|
+
<Story />
|
|
9459
|
+
</TooltipProvider>
|
|
9460
|
+
),
|
|
9461
|
+
],
|
|
9462
|
+
};
|
|
9463
|
+
|
|
9464
|
+
export default meta;
|
|
9465
|
+
type Story = StoryObj<typeof Tooltip>;
|
|
9466
|
+
|
|
9467
|
+
export const Default: Story = {
|
|
9468
|
+
render: () => (
|
|
9469
|
+
<Tooltip>
|
|
9470
|
+
<TooltipTrigger asChild>
|
|
9471
|
+
<Button variant="outline">Hover me</Button>
|
|
9472
|
+
</TooltipTrigger>
|
|
9473
|
+
<TooltipContent>
|
|
9474
|
+
<p>Add to library</p>
|
|
9475
|
+
</TooltipContent>
|
|
9476
|
+
</Tooltip>
|
|
9477
|
+
),
|
|
9478
|
+
};
|
|
9479
|
+
|
|
9480
|
+
export const Positions: Story = {
|
|
9481
|
+
render: () => (
|
|
9482
|
+
<div className="flex gap-4">
|
|
9483
|
+
<Tooltip>
|
|
9484
|
+
<TooltipTrigger asChild>
|
|
9485
|
+
<Button variant="outline">Top</Button>
|
|
9486
|
+
</TooltipTrigger>
|
|
9487
|
+
<TooltipContent side="top">
|
|
9488
|
+
<p>Top tooltip</p>
|
|
9489
|
+
</TooltipContent>
|
|
9490
|
+
</Tooltip>
|
|
9491
|
+
<Tooltip>
|
|
9492
|
+
<TooltipTrigger asChild>
|
|
9493
|
+
<Button variant="outline">Bottom</Button>
|
|
9494
|
+
</TooltipTrigger>
|
|
9495
|
+
<TooltipContent side="bottom">
|
|
9496
|
+
<p>Bottom tooltip</p>
|
|
9497
|
+
</TooltipContent>
|
|
9498
|
+
</Tooltip>
|
|
9499
|
+
<Tooltip>
|
|
9500
|
+
<TooltipTrigger asChild>
|
|
9501
|
+
<Button variant="outline">Left</Button>
|
|
9502
|
+
</TooltipTrigger>
|
|
9503
|
+
<TooltipContent side="left">
|
|
9504
|
+
<p>Left tooltip</p>
|
|
9505
|
+
</TooltipContent>
|
|
9506
|
+
</Tooltip>
|
|
9507
|
+
<Tooltip>
|
|
9508
|
+
<TooltipTrigger asChild>
|
|
9509
|
+
<Button variant="outline">Right</Button>
|
|
9510
|
+
</TooltipTrigger>
|
|
9511
|
+
<TooltipContent side="right">
|
|
9512
|
+
<p>Right tooltip</p>
|
|
9513
|
+
</TooltipContent>
|
|
9514
|
+
</Tooltip>
|
|
9515
|
+
</div>
|
|
9516
|
+
),
|
|
9517
|
+
};
|
|
9518
|
+
`;
|
|
9519
|
+
const dialogTsx = `import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
9520
|
+
import { X } from 'lucide-react';
|
|
9521
|
+
import * as React from 'react';
|
|
9522
|
+
|
|
9523
|
+
import { cn } from '~/lib/utils';
|
|
9524
|
+
|
|
9525
|
+
const Dialog = DialogPrimitive.Root;
|
|
9526
|
+
|
|
9527
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
9528
|
+
|
|
9529
|
+
const DialogPortal = DialogPrimitive.Portal;
|
|
9530
|
+
|
|
9531
|
+
const DialogClose = DialogPrimitive.Close;
|
|
9532
|
+
|
|
9533
|
+
const DialogOverlay = React.forwardRef<
|
|
9534
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
9535
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
9536
|
+
>(({ className, ...props }, ref) => (
|
|
9537
|
+
<DialogPrimitive.Overlay
|
|
9538
|
+
ref={ref}
|
|
9539
|
+
className={cn(
|
|
9540
|
+
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
9541
|
+
className,
|
|
9542
|
+
)}
|
|
9543
|
+
{...props}
|
|
9544
|
+
/>
|
|
9545
|
+
));
|
|
9546
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
9547
|
+
|
|
9548
|
+
const DialogContent = React.forwardRef<
|
|
9549
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
9550
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
9551
|
+
>(({ className, children, ...props }, ref) => (
|
|
9552
|
+
<DialogPortal>
|
|
9553
|
+
<DialogOverlay />
|
|
9554
|
+
<DialogPrimitive.Content
|
|
9555
|
+
ref={ref}
|
|
9556
|
+
className={cn(
|
|
9557
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
9558
|
+
className,
|
|
9559
|
+
)}
|
|
9560
|
+
{...props}
|
|
9561
|
+
>
|
|
9562
|
+
{children}
|
|
9563
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
9564
|
+
<X className="h-4 w-4" />
|
|
9565
|
+
<span className="sr-only">Close</span>
|
|
9566
|
+
</DialogPrimitive.Close>
|
|
9567
|
+
</DialogPrimitive.Content>
|
|
9568
|
+
</DialogPortal>
|
|
9569
|
+
));
|
|
9570
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
9571
|
+
|
|
9572
|
+
const DialogHeader = ({
|
|
9573
|
+
className,
|
|
9574
|
+
...props
|
|
9575
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
9576
|
+
<div
|
|
9577
|
+
className={cn(
|
|
9578
|
+
'flex flex-col space-y-1.5 text-center sm:text-left',
|
|
9579
|
+
className,
|
|
9580
|
+
)}
|
|
9581
|
+
{...props}
|
|
9582
|
+
/>
|
|
9583
|
+
);
|
|
9584
|
+
DialogHeader.displayName = 'DialogHeader';
|
|
9585
|
+
|
|
9586
|
+
const DialogFooter = ({
|
|
9587
|
+
className,
|
|
9588
|
+
...props
|
|
9589
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
9590
|
+
<div
|
|
9591
|
+
className={cn(
|
|
9592
|
+
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
|
9593
|
+
className,
|
|
9594
|
+
)}
|
|
9595
|
+
{...props}
|
|
9596
|
+
/>
|
|
9597
|
+
);
|
|
9598
|
+
DialogFooter.displayName = 'DialogFooter';
|
|
9599
|
+
|
|
9600
|
+
const DialogTitle = React.forwardRef<
|
|
9601
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
9602
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
9603
|
+
>(({ className, ...props }, ref) => (
|
|
9604
|
+
<DialogPrimitive.Title
|
|
9605
|
+
ref={ref}
|
|
9606
|
+
className={cn(
|
|
9607
|
+
'text-lg font-semibold leading-none tracking-tight',
|
|
9608
|
+
className,
|
|
9609
|
+
)}
|
|
9610
|
+
{...props}
|
|
9611
|
+
/>
|
|
9612
|
+
));
|
|
9613
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
9614
|
+
|
|
9615
|
+
const DialogDescription = React.forwardRef<
|
|
9616
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
9617
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
9618
|
+
>(({ className, ...props }, ref) => (
|
|
9619
|
+
<DialogPrimitive.Description
|
|
9620
|
+
ref={ref}
|
|
9621
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
9622
|
+
{...props}
|
|
9623
|
+
/>
|
|
9624
|
+
));
|
|
9625
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
9626
|
+
|
|
9627
|
+
export {
|
|
9628
|
+
Dialog,
|
|
9629
|
+
DialogPortal,
|
|
9630
|
+
DialogOverlay,
|
|
9631
|
+
DialogTrigger,
|
|
9632
|
+
DialogClose,
|
|
9633
|
+
DialogContent,
|
|
9634
|
+
DialogHeader,
|
|
9635
|
+
DialogFooter,
|
|
9636
|
+
DialogTitle,
|
|
9637
|
+
DialogDescription,
|
|
9638
|
+
};
|
|
9639
|
+
`;
|
|
9640
|
+
const dialogStories = `import type { Meta, StoryObj } from '@storybook/react';
|
|
9641
|
+
import {
|
|
9642
|
+
Dialog,
|
|
9643
|
+
DialogContent,
|
|
9644
|
+
DialogDescription,
|
|
9645
|
+
DialogFooter,
|
|
9646
|
+
DialogHeader,
|
|
9647
|
+
DialogTitle,
|
|
9648
|
+
DialogTrigger,
|
|
9649
|
+
} from '.';
|
|
9650
|
+
import { Button } from '../button';
|
|
9651
|
+
import { Input } from '../input';
|
|
9652
|
+
import { Label } from '../label';
|
|
9653
|
+
|
|
9654
|
+
const meta: Meta<typeof Dialog> = {
|
|
9655
|
+
title: 'Components/Dialog',
|
|
9656
|
+
component: Dialog,
|
|
9657
|
+
tags: ['autodocs'],
|
|
9658
|
+
};
|
|
9659
|
+
|
|
9660
|
+
export default meta;
|
|
9661
|
+
type Story = StoryObj<typeof Dialog>;
|
|
9662
|
+
|
|
9663
|
+
export const Default: Story = {
|
|
9664
|
+
render: () => (
|
|
9665
|
+
<Dialog>
|
|
9666
|
+
<DialogTrigger asChild>
|
|
9667
|
+
<Button variant="outline">Edit Profile</Button>
|
|
9668
|
+
</DialogTrigger>
|
|
9669
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
9670
|
+
<DialogHeader>
|
|
9671
|
+
<DialogTitle>Edit profile</DialogTitle>
|
|
9672
|
+
<DialogDescription>
|
|
9673
|
+
Make changes to your profile here. Click save when you're done.
|
|
9674
|
+
</DialogDescription>
|
|
9675
|
+
</DialogHeader>
|
|
9676
|
+
<div className="grid gap-4 py-4">
|
|
9677
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
9678
|
+
<Label htmlFor="name" className="text-right">
|
|
9679
|
+
Name
|
|
9680
|
+
</Label>
|
|
9681
|
+
<Input id="name" defaultValue="Pedro Duarte" className="col-span-3" />
|
|
9682
|
+
</div>
|
|
9683
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
9684
|
+
<Label htmlFor="username" className="text-right">
|
|
9685
|
+
Username
|
|
9686
|
+
</Label>
|
|
9687
|
+
<Input id="username" defaultValue="@peduarte" className="col-span-3" />
|
|
9688
|
+
</div>
|
|
9689
|
+
</div>
|
|
9690
|
+
<DialogFooter>
|
|
9691
|
+
<Button type="submit">Save changes</Button>
|
|
9692
|
+
</DialogFooter>
|
|
9693
|
+
</DialogContent>
|
|
9694
|
+
</Dialog>
|
|
9695
|
+
),
|
|
9696
|
+
};
|
|
9697
|
+
|
|
9698
|
+
export const Alert: Story = {
|
|
9699
|
+
render: () => (
|
|
9700
|
+
<Dialog>
|
|
9701
|
+
<DialogTrigger asChild>
|
|
9702
|
+
<Button variant="destructive">Delete Account</Button>
|
|
9703
|
+
</DialogTrigger>
|
|
9704
|
+
<DialogContent>
|
|
9705
|
+
<DialogHeader>
|
|
9706
|
+
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
9707
|
+
<DialogDescription>
|
|
9708
|
+
This action cannot be undone. This will permanently delete your
|
|
9709
|
+
account and remove your data from our servers.
|
|
9710
|
+
</DialogDescription>
|
|
9711
|
+
</DialogHeader>
|
|
9712
|
+
<DialogFooter>
|
|
9713
|
+
<Button variant="outline">Cancel</Button>
|
|
9714
|
+
<Button variant="destructive">Delete</Button>
|
|
9715
|
+
</DialogFooter>
|
|
9716
|
+
</DialogContent>
|
|
9717
|
+
</Dialog>
|
|
9718
|
+
),
|
|
9719
|
+
};
|
|
9720
|
+
`;
|
|
9721
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
|
|
9722
|
+
export { Input } from './input';
|
|
9723
|
+
export {
|
|
9724
|
+
Card,
|
|
9725
|
+
CardHeader,
|
|
9726
|
+
CardFooter,
|
|
9727
|
+
CardTitle,
|
|
9728
|
+
CardDescription,
|
|
9729
|
+
CardContent,
|
|
9730
|
+
} from './card';
|
|
9731
|
+
export { Label } from './label';
|
|
9732
|
+
export { Badge, type BadgeProps, badgeVariants } from './badge';
|
|
9733
|
+
export { Separator } from './separator';
|
|
9734
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
|
|
9735
|
+
export {
|
|
9736
|
+
Tooltip,
|
|
9737
|
+
TooltipTrigger,
|
|
9738
|
+
TooltipContent,
|
|
9739
|
+
TooltipProvider,
|
|
9740
|
+
} from './tooltip';
|
|
9741
|
+
export {
|
|
9742
|
+
Dialog,
|
|
9743
|
+
DialogPortal,
|
|
9744
|
+
DialogOverlay,
|
|
9745
|
+
DialogTrigger,
|
|
9746
|
+
DialogClose,
|
|
9747
|
+
DialogContent,
|
|
9748
|
+
DialogHeader,
|
|
9749
|
+
DialogFooter,
|
|
9750
|
+
DialogTitle,
|
|
9751
|
+
DialogDescription,
|
|
9752
|
+
} from './dialog';
|
|
9753
|
+
`;
|
|
9754
|
+
const buttonIndexTsx = buttonTsx;
|
|
9755
|
+
const inputIndexTsx = inputTsx;
|
|
9756
|
+
const cardIndexTsx = cardTsx;
|
|
9757
|
+
const componentsIndex = `export * from './ui';
|
|
9758
|
+
`;
|
|
9759
|
+
const indexTs = `// @${options.name}/ui - Shared UI component library
|
|
9760
|
+
|
|
9761
|
+
// shadcn/ui components
|
|
9762
|
+
export * from './components';
|
|
9763
|
+
|
|
9764
|
+
// Utilities
|
|
9765
|
+
export { cn } from './lib/utils';
|
|
9766
|
+
`;
|
|
9767
|
+
const gitignore = `node_modules/
|
|
9768
|
+
dist/
|
|
9769
|
+
storybook-static/
|
|
9770
|
+
*.log
|
|
9771
|
+
`;
|
|
9772
|
+
return [
|
|
9773
|
+
{
|
|
9774
|
+
path: "packages/ui/package.json",
|
|
9775
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
9776
|
+
},
|
|
9777
|
+
{
|
|
9778
|
+
path: "packages/ui/tsconfig.json",
|
|
9779
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
9780
|
+
},
|
|
9781
|
+
{
|
|
9782
|
+
path: "packages/ui/components.json",
|
|
9783
|
+
content: `${JSON.stringify(componentsJson, null, 2)}\n`
|
|
9784
|
+
},
|
|
9785
|
+
{
|
|
9786
|
+
path: "packages/ui/.storybook/main.ts",
|
|
9787
|
+
content: storybookMain
|
|
9788
|
+
},
|
|
9789
|
+
{
|
|
9790
|
+
path: "packages/ui/.storybook/preview.ts",
|
|
9791
|
+
content: storybookPreview
|
|
9792
|
+
},
|
|
9793
|
+
{
|
|
9794
|
+
path: "packages/ui/src/styles/globals.css",
|
|
9795
|
+
content: globalsCss
|
|
9796
|
+
},
|
|
9797
|
+
{
|
|
9798
|
+
path: "packages/ui/src/lib/utils.ts",
|
|
9799
|
+
content: utilsTs
|
|
9800
|
+
},
|
|
9801
|
+
{
|
|
9802
|
+
path: "packages/ui/src/components/ui/button/index.tsx",
|
|
9803
|
+
content: buttonIndexTsx
|
|
9804
|
+
},
|
|
9805
|
+
{
|
|
9806
|
+
path: "packages/ui/src/components/ui/button/button.stories.tsx",
|
|
9807
|
+
content: buttonStories
|
|
9808
|
+
},
|
|
9809
|
+
{
|
|
9810
|
+
path: "packages/ui/src/components/ui/input/index.tsx",
|
|
9811
|
+
content: inputIndexTsx
|
|
9812
|
+
},
|
|
9813
|
+
{
|
|
9814
|
+
path: "packages/ui/src/components/ui/input/input.stories.tsx",
|
|
9815
|
+
content: inputStories
|
|
9816
|
+
},
|
|
9817
|
+
{
|
|
9818
|
+
path: "packages/ui/src/components/ui/card/index.tsx",
|
|
9819
|
+
content: cardIndexTsx
|
|
9820
|
+
},
|
|
9821
|
+
{
|
|
9822
|
+
path: "packages/ui/src/components/ui/card/card.stories.tsx",
|
|
9823
|
+
content: cardStories
|
|
9824
|
+
},
|
|
9825
|
+
{
|
|
9826
|
+
path: "packages/ui/src/components/ui/label/index.tsx",
|
|
9827
|
+
content: labelTsx
|
|
9828
|
+
},
|
|
9829
|
+
{
|
|
9830
|
+
path: "packages/ui/src/components/ui/label/label.stories.tsx",
|
|
9831
|
+
content: labelStories
|
|
9832
|
+
},
|
|
9833
|
+
{
|
|
9834
|
+
path: "packages/ui/src/components/ui/badge/index.tsx",
|
|
9835
|
+
content: badgeTsx
|
|
9836
|
+
},
|
|
9837
|
+
{
|
|
9838
|
+
path: "packages/ui/src/components/ui/badge/badge.stories.tsx",
|
|
9839
|
+
content: badgeStories
|
|
9840
|
+
},
|
|
9841
|
+
{
|
|
9842
|
+
path: "packages/ui/src/components/ui/separator/index.tsx",
|
|
9843
|
+
content: separatorTsx
|
|
9844
|
+
},
|
|
9845
|
+
{
|
|
9846
|
+
path: "packages/ui/src/components/ui/separator/separator.stories.tsx",
|
|
9847
|
+
content: separatorStories
|
|
9848
|
+
},
|
|
9849
|
+
{
|
|
9850
|
+
path: "packages/ui/src/components/ui/tabs/index.tsx",
|
|
9851
|
+
content: tabsTsx
|
|
9852
|
+
},
|
|
9853
|
+
{
|
|
9854
|
+
path: "packages/ui/src/components/ui/tabs/tabs.stories.tsx",
|
|
9855
|
+
content: tabsStories
|
|
9856
|
+
},
|
|
9857
|
+
{
|
|
9858
|
+
path: "packages/ui/src/components/ui/tooltip/index.tsx",
|
|
9859
|
+
content: tooltipTsx
|
|
9860
|
+
},
|
|
9861
|
+
{
|
|
9862
|
+
path: "packages/ui/src/components/ui/tooltip/tooltip.stories.tsx",
|
|
9863
|
+
content: tooltipStories
|
|
9864
|
+
},
|
|
9865
|
+
{
|
|
9866
|
+
path: "packages/ui/src/components/ui/dialog/index.tsx",
|
|
9867
|
+
content: dialogTsx
|
|
9868
|
+
},
|
|
9869
|
+
{
|
|
9870
|
+
path: "packages/ui/src/components/ui/dialog/dialog.stories.tsx",
|
|
9871
|
+
content: dialogStories
|
|
9872
|
+
},
|
|
9873
|
+
{
|
|
9874
|
+
path: "packages/ui/src/components/ui/index.ts",
|
|
9875
|
+
content: componentsUiIndex
|
|
9876
|
+
},
|
|
9877
|
+
{
|
|
9878
|
+
path: "packages/ui/src/components/index.ts",
|
|
9879
|
+
content: componentsIndex
|
|
9880
|
+
},
|
|
9881
|
+
{
|
|
9882
|
+
path: "packages/ui/src/index.ts",
|
|
9883
|
+
content: indexTs
|
|
9884
|
+
},
|
|
9885
|
+
{
|
|
9886
|
+
path: "packages/ui/.gitignore",
|
|
9887
|
+
content: gitignore
|
|
9888
|
+
}
|
|
9889
|
+
];
|
|
9890
|
+
}
|
|
9891
|
+
|
|
8364
9892
|
//#endregion
|
|
8365
9893
|
//#region src/init/generators/web.ts
|
|
8366
9894
|
/**
|
|
@@ -8370,29 +9898,35 @@ function generateWebAppFiles(options) {
|
|
|
8370
9898
|
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
8371
9899
|
const packageName = `@${options.name}/web`;
|
|
8372
9900
|
const modelsPackage = `@${options.name}/models`;
|
|
9901
|
+
const uiPackage = `@${options.name}/ui`;
|
|
8373
9902
|
const packageJson = {
|
|
8374
9903
|
name: packageName,
|
|
8375
9904
|
version: "0.0.1",
|
|
8376
9905
|
private: true,
|
|
8377
9906
|
type: "module",
|
|
8378
9907
|
scripts: {
|
|
8379
|
-
dev: "next dev
|
|
8380
|
-
build: "next build",
|
|
9908
|
+
dev: "gkm exec -- next dev --turbopack",
|
|
9909
|
+
build: "gkm exec -- next build",
|
|
8381
9910
|
start: "next start",
|
|
8382
9911
|
typecheck: "tsc --noEmit"
|
|
8383
9912
|
},
|
|
8384
9913
|
dependencies: {
|
|
8385
9914
|
[modelsPackage]: "workspace:*",
|
|
9915
|
+
[uiPackage]: "workspace:*",
|
|
8386
9916
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
8387
9917
|
"@tanstack/react-query": "~5.80.0",
|
|
9918
|
+
"better-auth": "~1.2.0",
|
|
8388
9919
|
next: "~16.1.0",
|
|
8389
9920
|
react: "~19.2.0",
|
|
8390
9921
|
"react-dom": "~19.2.0"
|
|
8391
9922
|
},
|
|
8392
9923
|
devDependencies: {
|
|
9924
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
9925
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
8393
9926
|
"@types/node": "~22.0.0",
|
|
8394
9927
|
"@types/react": "~19.0.0",
|
|
8395
9928
|
"@types/react-dom": "~19.0.0",
|
|
9929
|
+
tailwindcss: "^4.0.0",
|
|
8396
9930
|
typescript: "~5.8.2"
|
|
8397
9931
|
}
|
|
8398
9932
|
};
|
|
@@ -8401,10 +9935,16 @@ function generateWebAppFiles(options) {
|
|
|
8401
9935
|
const nextConfig: NextConfig = {
|
|
8402
9936
|
output: 'standalone',
|
|
8403
9937
|
reactStrictMode: true,
|
|
8404
|
-
transpilePackages: ['${modelsPackage}'],
|
|
9938
|
+
transpilePackages: ['${modelsPackage}', '${uiPackage}'],
|
|
8405
9939
|
};
|
|
8406
9940
|
|
|
8407
9941
|
export default nextConfig;
|
|
9942
|
+
`;
|
|
9943
|
+
const postcssConfig = `export default {
|
|
9944
|
+
plugins: {
|
|
9945
|
+
'@tailwindcss/postcss': {},
|
|
9946
|
+
},
|
|
9947
|
+
};
|
|
8408
9948
|
`;
|
|
8409
9949
|
const tsConfig = {
|
|
8410
9950
|
extends: "../../tsconfig.json",
|
|
@@ -8427,9 +9967,11 @@ export default nextConfig;
|
|
|
8427
9967
|
incremental: true,
|
|
8428
9968
|
plugins: [{ name: "next" }],
|
|
8429
9969
|
paths: {
|
|
8430
|
-
"
|
|
9970
|
+
"~/*": ["./src/*"],
|
|
8431
9971
|
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
8432
|
-
[`${modelsPackage}/*`]: ["../../packages/models/src/*"]
|
|
9972
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
9973
|
+
[`${uiPackage}`]: ["../../packages/ui/src"],
|
|
9974
|
+
[`${uiPackage}/*`]: ["../../packages/ui/src/*"]
|
|
8433
9975
|
},
|
|
8434
9976
|
baseUrl: "."
|
|
8435
9977
|
},
|
|
@@ -8441,68 +9983,66 @@ export default nextConfig;
|
|
|
8441
9983
|
],
|
|
8442
9984
|
exclude: ["node_modules"]
|
|
8443
9985
|
};
|
|
9986
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
9987
|
+
|
|
9988
|
+
function makeQueryClient() {
|
|
9989
|
+
return new QueryClient({
|
|
9990
|
+
defaultOptions: {
|
|
9991
|
+
queries: {
|
|
9992
|
+
staleTime: 60 * 1000,
|
|
9993
|
+
},
|
|
9994
|
+
},
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
|
|
9998
|
+
let browserQueryClient: QueryClient | undefined = undefined;
|
|
9999
|
+
|
|
10000
|
+
export function getQueryClient() {
|
|
10001
|
+
if (typeof window === 'undefined') {
|
|
10002
|
+
// Server: always make a new query client
|
|
10003
|
+
return makeQueryClient();
|
|
10004
|
+
}
|
|
10005
|
+
// Browser: reuse existing query client
|
|
10006
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
10007
|
+
return browserQueryClient;
|
|
10008
|
+
}
|
|
10009
|
+
`;
|
|
10010
|
+
const authClientTs = `import { createAuthClient } from 'better-auth/react';
|
|
10011
|
+
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
10012
|
+
|
|
10013
|
+
export const authClient = createAuthClient({
|
|
10014
|
+
baseURL: process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3002',
|
|
10015
|
+
plugins: [magicLinkClient()],
|
|
10016
|
+
});
|
|
10017
|
+
|
|
10018
|
+
export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
|
|
10019
|
+
`;
|
|
8444
10020
|
const providersTsx = `'use client';
|
|
8445
10021
|
|
|
8446
|
-
import {
|
|
8447
|
-
import {
|
|
10022
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
10023
|
+
import { getQueryClient } from '~/lib/query-client';
|
|
8448
10024
|
|
|
8449
10025
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
8450
|
-
const
|
|
8451
|
-
() =>
|
|
8452
|
-
new QueryClient({
|
|
8453
|
-
defaultOptions: {
|
|
8454
|
-
queries: {
|
|
8455
|
-
staleTime: 60 * 1000,
|
|
8456
|
-
},
|
|
8457
|
-
},
|
|
8458
|
-
}),
|
|
8459
|
-
);
|
|
10026
|
+
const queryClient = getQueryClient();
|
|
8460
10027
|
|
|
8461
10028
|
return (
|
|
8462
10029
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
8463
10030
|
);
|
|
8464
10031
|
}
|
|
8465
10032
|
`;
|
|
8466
|
-
const apiIndexTs = `import {
|
|
8467
|
-
import {
|
|
8468
|
-
|
|
8469
|
-
// TODO: Run 'gkm openapi' to generate typed paths from your API
|
|
8470
|
-
// This is a placeholder that will be replaced by the generated openapi.ts
|
|
8471
|
-
interface paths {
|
|
8472
|
-
'/health': {
|
|
8473
|
-
get: {
|
|
8474
|
-
responses: {
|
|
8475
|
-
200: {
|
|
8476
|
-
content: {
|
|
8477
|
-
'application/json': { status: string; timestamp: string };
|
|
8478
|
-
};
|
|
8479
|
-
};
|
|
8480
|
-
};
|
|
8481
|
-
};
|
|
8482
|
-
};
|
|
8483
|
-
'/users': {
|
|
8484
|
-
get: {
|
|
8485
|
-
responses: {
|
|
8486
|
-
200: {
|
|
8487
|
-
content: {
|
|
8488
|
-
'application/json': { users: Array<{ id: string; name: string }> };
|
|
8489
|
-
};
|
|
8490
|
-
};
|
|
8491
|
-
};
|
|
8492
|
-
};
|
|
8493
|
-
};
|
|
8494
|
-
}
|
|
8495
|
-
|
|
8496
|
-
const baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
|
8497
|
-
|
|
8498
|
-
const fetcher = new TypedFetcher<paths>({ baseURL });
|
|
10033
|
+
const apiIndexTs = `import { createApi } from './openapi';
|
|
10034
|
+
import { getQueryClient } from '~/lib/query-client';
|
|
8499
10035
|
|
|
8500
|
-
const
|
|
8501
|
-
|
|
8502
|
-
|
|
10036
|
+
export const api = createApi({
|
|
10037
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
|
|
10038
|
+
queryClient: getQueryClient(),
|
|
10039
|
+
});
|
|
10040
|
+
`;
|
|
10041
|
+
const globalsCss = `@import '${uiPackage}/styles';
|
|
8503
10042
|
`;
|
|
8504
10043
|
const layoutTsx = `import type { Metadata } from 'next';
|
|
8505
10044
|
import { Providers } from './providers';
|
|
10045
|
+
import './globals.css';
|
|
8506
10046
|
|
|
8507
10047
|
export const metadata: Metadata = {
|
|
8508
10048
|
title: '${options.name}',
|
|
@@ -8523,42 +10063,59 @@ export default function RootLayout({
|
|
|
8523
10063
|
);
|
|
8524
10064
|
}
|
|
8525
10065
|
`;
|
|
8526
|
-
const pageTsx = `import { api } from '
|
|
10066
|
+
const pageTsx = `import { api } from '~/api';
|
|
10067
|
+
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
|
|
8527
10068
|
|
|
8528
10069
|
export default async function Home() {
|
|
8529
10070
|
// Type-safe API call using the generated client
|
|
8530
10071
|
const health = await api('GET /health').catch(() => null);
|
|
8531
10072
|
|
|
8532
10073
|
return (
|
|
8533
|
-
<main
|
|
8534
|
-
<
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
</
|
|
8555
|
-
|
|
10074
|
+
<main className="min-h-screen bg-background p-8">
|
|
10075
|
+
<div className="mx-auto max-w-4xl space-y-8">
|
|
10076
|
+
<div className="space-y-2">
|
|
10077
|
+
<h1 className="text-4xl font-bold tracking-tight">Welcome to ${options.name}</h1>
|
|
10078
|
+
<p className="text-muted-foreground">Your fullstack application is ready.</p>
|
|
10079
|
+
</div>
|
|
10080
|
+
|
|
10081
|
+
<Card>
|
|
10082
|
+
<CardHeader>
|
|
10083
|
+
<CardTitle>API Status</CardTitle>
|
|
10084
|
+
<CardDescription>Connection to your backend API</CardDescription>
|
|
10085
|
+
</CardHeader>
|
|
10086
|
+
<CardContent>
|
|
10087
|
+
{health ? (
|
|
10088
|
+
<pre className="rounded-lg bg-muted p-4 text-sm">
|
|
10089
|
+
{JSON.stringify(health, null, 2)}
|
|
10090
|
+
</pre>
|
|
10091
|
+
) : (
|
|
10092
|
+
<p className="text-destructive">Unable to connect to API</p>
|
|
10093
|
+
)}
|
|
10094
|
+
</CardContent>
|
|
10095
|
+
</Card>
|
|
10096
|
+
|
|
10097
|
+
<Card>
|
|
10098
|
+
<CardHeader>
|
|
10099
|
+
<CardTitle>Next Steps</CardTitle>
|
|
10100
|
+
<CardDescription>Get started with your project</CardDescription>
|
|
10101
|
+
</CardHeader>
|
|
10102
|
+
<CardContent className="space-y-4">
|
|
10103
|
+
<ul className="list-inside list-disc space-y-2 text-muted-foreground">
|
|
10104
|
+
<li>Run <code className="rounded bg-muted px-1">gkm openapi</code> to generate typed API client</li>
|
|
10105
|
+
<li>Edit <code className="rounded bg-muted px-1">apps/web/src/app/page.tsx</code> to customize this page</li>
|
|
10106
|
+
<li>Add API routes in <code className="rounded bg-muted px-1">apps/api/src/endpoints/</code></li>
|
|
10107
|
+
<li>Add UI components with <code className="rounded bg-muted px-1">npx shadcn@latest add</code> in packages/ui</li>
|
|
10108
|
+
</ul>
|
|
10109
|
+
<div className="flex gap-4">
|
|
10110
|
+
<Button>Get Started</Button>
|
|
10111
|
+
<Button variant="outline">Documentation</Button>
|
|
10112
|
+
</div>
|
|
10113
|
+
</CardContent>
|
|
10114
|
+
</Card>
|
|
10115
|
+
</div>
|
|
8556
10116
|
</main>
|
|
8557
10117
|
);
|
|
8558
10118
|
}
|
|
8559
|
-
`;
|
|
8560
|
-
const envLocal = `# API URL for client-side requests
|
|
8561
|
-
NEXT_PUBLIC_API_URL=http://localhost:3000
|
|
8562
10119
|
`;
|
|
8563
10120
|
const gitignore = `.next/
|
|
8564
10121
|
node_modules/
|
|
@@ -8574,10 +10131,18 @@ node_modules/
|
|
|
8574
10131
|
path: "apps/web/next.config.ts",
|
|
8575
10132
|
content: nextConfig
|
|
8576
10133
|
},
|
|
10134
|
+
{
|
|
10135
|
+
path: "apps/web/postcss.config.mjs",
|
|
10136
|
+
content: postcssConfig
|
|
10137
|
+
},
|
|
8577
10138
|
{
|
|
8578
10139
|
path: "apps/web/tsconfig.json",
|
|
8579
10140
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
8580
10141
|
},
|
|
10142
|
+
{
|
|
10143
|
+
path: "apps/web/src/app/globals.css",
|
|
10144
|
+
content: globalsCss
|
|
10145
|
+
},
|
|
8581
10146
|
{
|
|
8582
10147
|
path: "apps/web/src/app/layout.tsx",
|
|
8583
10148
|
content: layoutTsx
|
|
@@ -8591,12 +10156,16 @@ node_modules/
|
|
|
8591
10156
|
content: pageTsx
|
|
8592
10157
|
},
|
|
8593
10158
|
{
|
|
8594
|
-
path: "apps/web/src/
|
|
8595
|
-
content:
|
|
10159
|
+
path: "apps/web/src/lib/query-client.ts",
|
|
10160
|
+
content: queryClientTs
|
|
10161
|
+
},
|
|
10162
|
+
{
|
|
10163
|
+
path: "apps/web/src/lib/auth-client.ts",
|
|
10164
|
+
content: authClientTs
|
|
8596
10165
|
},
|
|
8597
10166
|
{
|
|
8598
|
-
path: "apps/web
|
|
8599
|
-
content:
|
|
10167
|
+
path: "apps/web/src/api/index.ts",
|
|
10168
|
+
content: apiIndexTs
|
|
8600
10169
|
},
|
|
8601
10170
|
{
|
|
8602
10171
|
path: "apps/web/.gitignore",
|
|
@@ -8835,6 +10404,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
8835
10404
|
const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
|
|
8836
10405
|
const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
|
|
8837
10406
|
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
10407
|
+
const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
|
|
8838
10408
|
for (const { path, content } of rootFiles) {
|
|
8839
10409
|
const fullPath = join(targetDir, path);
|
|
8840
10410
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
@@ -8860,6 +10430,11 @@ async function initCommand(projectName, options = {}) {
|
|
|
8860
10430
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
8861
10431
|
await writeFile(fullPath, content);
|
|
8862
10432
|
}
|
|
10433
|
+
for (const { path, content } of uiPackageFiles) {
|
|
10434
|
+
const fullPath = join(targetDir, path);
|
|
10435
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
10436
|
+
await writeFile(fullPath, content);
|
|
10437
|
+
}
|
|
8863
10438
|
console.log("🔐 Initializing encrypted secrets...\n");
|
|
8864
10439
|
const secretServices = [];
|
|
8865
10440
|
if (services.db) secretServices.push("postgres");
|
|
@@ -8879,6 +10454,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
8879
10454
|
customSecrets[passwordKey] = app.password;
|
|
8880
10455
|
}
|
|
8881
10456
|
customSecrets.AUTH_PORT = "3002";
|
|
10457
|
+
customSecrets.AUTH_URL = "http://localhost:3002";
|
|
8882
10458
|
customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
8883
10459
|
customSecrets.BETTER_AUTH_URL = "http://localhost:3002";
|
|
8884
10460
|
customSecrets.BETTER_AUTH_TRUSTED_ORIGINS = "http://localhost:3000,http://localhost:3001";
|
|
@@ -8955,7 +10531,8 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
8955
10531
|
console.log(` │ └── web/ # Next.js frontend`);
|
|
8956
10532
|
}
|
|
8957
10533
|
console.log(` ├── packages/`);
|
|
8958
|
-
console.log(` │
|
|
10534
|
+
console.log(` │ ├── models/ # Shared Zod schemas`);
|
|
10535
|
+
if (isFullstackTemplate(options.template)) console.log(` │ └── ui/ # Shared UI components`);
|
|
8959
10536
|
console.log(` ├── .gkm/secrets/ # Encrypted secrets`);
|
|
8960
10537
|
console.log(` ├── gkm.config.ts # Workspace config`);
|
|
8961
10538
|
console.log(` └── turbo.json # Turbo config`);
|
|
@@ -8971,7 +10548,7 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
8971
10548
|
console.log(` ${getRunCommand(pkgManager, "deploy")}`);
|
|
8972
10549
|
console.log("");
|
|
8973
10550
|
}
|
|
8974
|
-
console.log("📚 Documentation: https://
|
|
10551
|
+
console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
|
|
8975
10552
|
console.log("");
|
|
8976
10553
|
}
|
|
8977
10554
|
|
|
@@ -9523,6 +11100,46 @@ program.command("whoami").description("Show current authentication status").acti
|
|
|
9523
11100
|
process.exit(1);
|
|
9524
11101
|
}
|
|
9525
11102
|
});
|
|
11103
|
+
program.command("state:pull").description("Pull deployment state from remote to local").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
11104
|
+
try {
|
|
11105
|
+
const globalOptions = program.opts();
|
|
11106
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
11107
|
+
await statePullCommand(options);
|
|
11108
|
+
} catch (error) {
|
|
11109
|
+
console.error(error instanceof Error ? error.message : "Command failed");
|
|
11110
|
+
process.exit(1);
|
|
11111
|
+
}
|
|
11112
|
+
});
|
|
11113
|
+
program.command("state:push").description("Push deployment state from local to remote").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
11114
|
+
try {
|
|
11115
|
+
const globalOptions = program.opts();
|
|
11116
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
11117
|
+
await statePushCommand(options);
|
|
11118
|
+
} catch (error) {
|
|
11119
|
+
console.error(error instanceof Error ? error.message : "Command failed");
|
|
11120
|
+
process.exit(1);
|
|
11121
|
+
}
|
|
11122
|
+
});
|
|
11123
|
+
program.command("state:show").description("Show deployment state for a stage").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--json", "Output as JSON").action(async (options) => {
|
|
11124
|
+
try {
|
|
11125
|
+
const globalOptions = program.opts();
|
|
11126
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
11127
|
+
await stateShowCommand(options);
|
|
11128
|
+
} catch (error) {
|
|
11129
|
+
console.error(error instanceof Error ? error.message : "Command failed");
|
|
11130
|
+
process.exit(1);
|
|
11131
|
+
}
|
|
11132
|
+
});
|
|
11133
|
+
program.command("state:diff").description("Compare local and remote deployment state").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
|
|
11134
|
+
try {
|
|
11135
|
+
const globalOptions = program.opts();
|
|
11136
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
11137
|
+
await stateDiffCommand(options);
|
|
11138
|
+
} catch (error) {
|
|
11139
|
+
console.error(error instanceof Error ? error.message : "Command failed");
|
|
11140
|
+
process.exit(1);
|
|
11141
|
+
}
|
|
11142
|
+
});
|
|
9526
11143
|
program.parse();
|
|
9527
11144
|
|
|
9528
11145
|
//#endregion
|