@deploily/deploily-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.devcontainer/devcontainer.json +25 -0
  2. package/.devcontainer/docker-compose.yml +18 -0
  3. package/.env.example +8 -0
  4. package/.github/dependabot.yml +12 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/LICENSE +201 -0
  7. package/README.md +105 -0
  8. package/dist/auth/callback.d.ts +3 -0
  9. package/dist/auth/callback.d.ts.map +1 -0
  10. package/dist/auth/callback.js +143 -0
  11. package/dist/auth/callback.js.map +1 -0
  12. package/dist/auth/index.d.ts +23 -0
  13. package/dist/auth/index.d.ts.map +1 -0
  14. package/dist/auth/index.js +173 -0
  15. package/dist/auth/index.js.map +1 -0
  16. package/dist/commands/auth.d.ts +4 -0
  17. package/dist/commands/auth.d.ts.map +1 -0
  18. package/dist/commands/auth.js +90 -0
  19. package/dist/commands/auth.js.map +1 -0
  20. package/dist/config/constants.d.ts +22 -0
  21. package/dist/config/constants.d.ts.map +1 -0
  22. package/dist/config/constants.js +25 -0
  23. package/dist/config/constants.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +61 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/storage/credentials.d.ts +47 -0
  29. package/dist/storage/credentials.d.ts.map +1 -0
  30. package/dist/storage/credentials.js +160 -0
  31. package/dist/storage/credentials.js.map +1 -0
  32. package/dist/utils/pkce.d.ts +5 -0
  33. package/dist/utils/pkce.d.ts.map +1 -0
  34. package/dist/utils/pkce.js +30 -0
  35. package/dist/utils/pkce.js.map +1 -0
  36. package/package.json +37 -0
  37. package/pnpm-workspace.yaml +5 -0
  38. package/src/auth/callback.ts +159 -0
  39. package/src/auth/index.ts +238 -0
  40. package/src/commands/auth.ts +114 -0
  41. package/src/config/constants.ts +28 -0
  42. package/src/index.ts +69 -0
  43. package/src/storage/credentials.ts +188 -0
  44. package/src/types/index.d.ts +35 -0
  45. package/src/utils/pkce.ts +36 -0
  46. package/tsconfig.json +24 -0
@@ -0,0 +1,30 @@
1
+ import crypto from "crypto";
2
+ // Generate a cryptographically secure random string
3
+ export function generateRandomString(length = 32) {
4
+ return crypto.randomBytes(length).toString("hex");
5
+ }
6
+ // Generate PKCE code verifier
7
+ export function generateCodeVerifier() {
8
+ const length = 128;
9
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
10
+ let result = "";
11
+ const randomValues = crypto.getRandomValues(new Uint8Array(length));
12
+ for (let i = 0; i < length; i++) {
13
+ result += charset[randomValues[i] % charset.length];
14
+ }
15
+ return result;
16
+ }
17
+ // Generate PKCE code challenge from verifier
18
+ export function generateCodeChallenge(codeVerifier) {
19
+ const hash = crypto.createHash("sha256").update(codeVerifier).digest();
20
+ return hash
21
+ .toString("base64")
22
+ .replace(/\+/g, "-")
23
+ .replace(/\//g, "_")
24
+ .replace(/=+$/, "");
25
+ }
26
+ // Generate state parameter for CSRF protection
27
+ export function generateState() {
28
+ return generateRandomString(32);
29
+ }
30
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/utils/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAE3B,oDAAoD;AACpD,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE;IACtD,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACnD,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,oBAAoB;IAClC,MAAM,MAAM,GAAG,GAAG,CAAA;IAClB,MAAM,OAAO,GACX,oEAAoE,CAAA;IACtE,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,MAAM,YAAY,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;IAEnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACrD,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,qBAAqB,CAAC,YAAoB;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAA;IACtE,OAAO,IAAI;SACR,QAAQ,CAAC,QAAQ,CAAC;SAClB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACvB,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,aAAa;IAC3B,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAA;AACjC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@deploily/deploily-cli",
3
+ "version": "0.1.0",
4
+ "description": "Deploily CLI - Deploy and manage your applications with ease",
5
+ "license": "APACHE-2.0",
6
+ "keywords": [
7
+ "cli",
8
+ "deployment",
9
+ "deploily"
10
+ ],
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "bin": {
14
+ "deploily": "dist/index.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/index.ts",
19
+ "start": "node dist/index.js"
20
+ },
21
+ "dependencies": {
22
+ "@deploily/deploily-cli": "link:",
23
+ "@hono/node-server": "^2.0.6",
24
+ "axios": "^1.18.1",
25
+ "chalk": "^5.6.2",
26
+ "dotenv": "^17.4.2",
27
+ "open": "^11.0.0"
28
+ },
29
+ "optionalDependencies": {
30
+ "keytar": "^7.9.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.9.3",
34
+ "tsx": "^4.22.4",
35
+ "typescript": "^6.0.3"
36
+ }
37
+ }
@@ -0,0 +1,5 @@
1
+ allowBuilds:
2
+ esbuild: false
3
+ keytar: false
4
+ overrides:
5
+ '@deploily/deploily-cli': 'link:'
@@ -0,0 +1,159 @@
1
+ import http from "http"
2
+ import { URL } from "url"
3
+ import { AUTH_CONFIG } from "../config/constants.js"
4
+ import { CallbackResult } from "../types/index.js"
5
+
6
+ // Start local HTTP server to listen for OAuth callback
7
+ export async function startCallbackServer(
8
+ expectedState: string,
9
+ ): Promise<CallbackResult> {
10
+ return new Promise((resolve, reject) => {
11
+ const server = http.createServer((req, res) => {
12
+ if (!req.url) {
13
+ res.writeHead(400)
14
+ res.end("Bad request")
15
+ return
16
+ }
17
+
18
+ const url = new URL(
19
+ req.url,
20
+ `http://localhost:${AUTH_CONFIG.CALLBACK_PORT}`,
21
+ )
22
+ const code = url.searchParams.get("code")
23
+ const state = url.searchParams.get("state")
24
+ const error = url.searchParams.get("error")
25
+ const errorDescription = url.searchParams.get("error_description")
26
+
27
+ // Handle errors from Keycloak
28
+ if (error) {
29
+ const errorMsg = errorDescription || error
30
+ res.writeHead(400, { "Content-Type": "text/html" })
31
+ res.end(`
32
+ <!DOCTYPE html>
33
+ <html>
34
+ <head>
35
+ <title>Authentication Failed</title>
36
+ <style>
37
+ body {
38
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
39
+ display: flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ height: 100vh;
43
+ margin: 0;
44
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
45
+ }
46
+ .container {
47
+ background: white;
48
+ padding: 40px;
49
+ border-radius: 8px;
50
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
51
+ max-width: 500px;
52
+ text-align: center;
53
+ }
54
+ h1 { color: #e74c3c; margin-top: 0; }
55
+ p { color: #555; line-height: 1.6; }
56
+ code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="container">
61
+ <h1>Authentication Failed</h1>
62
+ <p>Error: <code>${escapeHtml(errorMsg)}</code></p>
63
+ <p>Please try again with: <code>deploily login</code></p>
64
+ </div>
65
+ </body>
66
+ </html>
67
+ `)
68
+ server.close()
69
+ reject(new Error(`Authentication failed: ${errorMsg}`))
70
+ return
71
+ }
72
+
73
+ // Validate callback
74
+ if (!code || !state) {
75
+ res.writeHead(400)
76
+ res.end("Missing code or state parameter")
77
+ server.close()
78
+ reject(new Error("Missing code or state parameter"))
79
+ return
80
+ }
81
+
82
+ // Validate state parameter (CSRF protection)
83
+ if (state !== expectedState) {
84
+ res.writeHead(403)
85
+ res.end("Invalid state parameter")
86
+ server.close()
87
+ reject(new Error("State parameter mismatch"))
88
+ return
89
+ }
90
+
91
+ // Success response
92
+ res.writeHead(200, { "Content-Type": "text/html" })
93
+ res.end(`
94
+ <!DOCTYPE html>
95
+ <html>
96
+ <head>
97
+ <title>Authentication Successful</title>
98
+ <style>
99
+ body {
100
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
101
+ display: flex;
102
+ justify-content: center;
103
+ align-items: center;
104
+ height: 100vh;
105
+ margin: 0;
106
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
107
+ }
108
+ .container {
109
+ background: white;
110
+ padding: 40px;
111
+ border-radius: 8px;
112
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
113
+ max-width: 500px;
114
+ text-align: center;
115
+ }
116
+ h1 { color: #27ae60; margin-top: 0; }
117
+ p { color: #555; line-height: 1.6; }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div class="container">
122
+ <h1>Authentication Successful</h1>
123
+ <p>You have successfully authenticated with Deploily.</p>
124
+ <p>You can close this window and return to your terminal.</p>
125
+ </div>
126
+ </body>
127
+ </html>
128
+ `)
129
+
130
+ server.close()
131
+ resolve({ code, state })
132
+ })
133
+
134
+ server.listen(AUTH_CONFIG.CALLBACK_PORT, "localhost", () => {
135
+ // Server is listening
136
+ })
137
+
138
+ // Set a timeout for the callback
139
+ const timeout = setTimeout(() => {
140
+ server.close()
141
+ reject(new Error("Callback timeout: No response from Keycloak"))
142
+ }, 300000) // 5 minutes
143
+
144
+ server.on("close", () => {
145
+ clearTimeout(timeout)
146
+ })
147
+ })
148
+ }
149
+
150
+ function escapeHtml(text: string): string {
151
+ const map: Record<string, string> = {
152
+ "&": "&amp;",
153
+ "<": "&lt;",
154
+ ">": "&gt;",
155
+ '"': "&quot;",
156
+ "'": "&#039;",
157
+ }
158
+ return text.replace(/[&<>"']/g, (m) => map[m])
159
+ }
@@ -0,0 +1,238 @@
1
+ import axios, { AxiosInstance } from "axios"
2
+ import {
3
+ generateCodeVerifier,
4
+ generateCodeChallenge,
5
+ generateState,
6
+ } from "../utils/pkce.js"
7
+ import { AUTH_CONFIG } from "../config/constants.js"
8
+ import { credentialStorage } from "../storage/credentials.js"
9
+ import {
10
+ AuthContext,
11
+ StoredCredentials,
12
+ TokenResponse,
13
+ UserInfo,
14
+ } from "../types/index.js"
15
+
16
+ class AuthenticationService {
17
+ private client: AxiosInstance
18
+
19
+ constructor() {
20
+ this.client = axios.create({
21
+ timeout: 10000,
22
+ })
23
+ }
24
+
25
+ private buildClientAuthHeaders(): Record<string, string> {
26
+ const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET
27
+
28
+ if (!clientSecret) {
29
+ return {}
30
+ }
31
+
32
+ const clientId = process.env.KEYCLOAK_CLIENT_ID!
33
+ const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString(
34
+ "base64",
35
+ )
36
+
37
+ return {
38
+ Authorization: `Basic ${basicAuth}`,
39
+ }
40
+ }
41
+
42
+ // Generate authorization URL for user login
43
+ generateAuthorizationUrl(): { url: string; context: AuthContext } {
44
+ const state = generateState()
45
+ const codeVerifier = generateCodeVerifier()
46
+ const codeChallenge = generateCodeChallenge(codeVerifier)
47
+
48
+ const params = new URLSearchParams({
49
+ client_id: AUTH_CONFIG.CLIENT_ID,
50
+ response_type: AUTH_CONFIG.RESPONSE_TYPE,
51
+ redirect_uri: AUTH_CONFIG.REDIRECT_URI,
52
+ scope: AUTH_CONFIG.SCOPE,
53
+ code_challenge: codeChallenge,
54
+ code_challenge_method: AUTH_CONFIG.CODE_CHALLENGE_METHOD,
55
+ state,
56
+ })
57
+
58
+ const url = `${AUTH_CONFIG.KEYCLOAK_AUTH_ENDPOINT}?${params.toString()}`
59
+
60
+ return {
61
+ url,
62
+ context: { state, codeVerifier, codeChallenge },
63
+ }
64
+ }
65
+
66
+ // Exchange authorization code for tokens
67
+ async exchangeCodeForToken(
68
+ code: string,
69
+ codeVerifier: string,
70
+ ): Promise<TokenResponse> {
71
+ try {
72
+ const body = new URLSearchParams({
73
+ grant_type: "authorization_code",
74
+ code,
75
+ client_id: process.env.KEYCLOAK_CLIENT_ID!,
76
+ redirect_uri: process.env.KEYCLOAK_REDIRECT_URI!,
77
+ code_verifier: codeVerifier,
78
+ })
79
+
80
+ const response = await this.client.post<TokenResponse>(
81
+ process.env.KEYCLOAK_TOKEN_ENDPOINT!,
82
+ body,
83
+ {
84
+ headers: {
85
+ "Content-Type": "application/x-www-form-urlencoded",
86
+ ...this.buildClientAuthHeaders(),
87
+ },
88
+ },
89
+ )
90
+
91
+ return response.data
92
+ } catch (error) {
93
+ if (axios.isAxiosError(error)) {
94
+ const responseData =
95
+ typeof error.response?.data === "string"
96
+ ? error.response.data
97
+ : JSON.stringify(error.response?.data ?? {})
98
+ const authHint =
99
+ error.response?.status === 401 &&
100
+ responseData.includes("unauthorized_client")
101
+ ? " This usually means the Keycloak client is confidential and KEYCLOAK_CLIENT_SECRET is missing or invalid."
102
+ : ""
103
+
104
+ throw new Error(
105
+ `Failed to exchange code for token: ${error.message}${responseData ? ` (${responseData})` : ""}${authHint}`,
106
+ )
107
+ }
108
+
109
+ throw new Error(
110
+ `Failed to exchange code for token: ${error instanceof Error ? error.message : "Unknown error"}`,
111
+ )
112
+ }
113
+ }
114
+
115
+ // Refresh access token using refresh token
116
+ async refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
117
+ try {
118
+ const body = new URLSearchParams({
119
+ grant_type: "refresh_token",
120
+ refresh_token: refreshToken,
121
+ client_id: process.env.KEYCLOAK_CLIENT_ID!,
122
+ })
123
+
124
+ const response = await this.client.post<TokenResponse>(
125
+ process.env.KEYCLOAK_TOKEN_ENDPOINT!,
126
+ body,
127
+ {
128
+ headers: {
129
+ "Content-Type": "application/x-www-form-urlencoded",
130
+ ...this.buildClientAuthHeaders(),
131
+ },
132
+ },
133
+ )
134
+
135
+ return response.data
136
+ } catch (error) {
137
+ throw new Error(
138
+ `Failed to refresh token: ${error instanceof Error ? error.message : "Unknown error"}`,
139
+ )
140
+ }
141
+ }
142
+
143
+ // Get user information from access token
144
+ async getUserInfo(accessToken: string): Promise<UserInfo> {
145
+ try {
146
+ const response = await this.client.get<UserInfo>(
147
+ process.env.KEYCLOAK_USERINFO_ENDPOINT!,
148
+ {
149
+ headers: {
150
+ Authorization: `Bearer ${accessToken}`,
151
+ },
152
+ },
153
+ )
154
+
155
+ return response.data
156
+ } catch (error) {
157
+ throw new Error(
158
+ `Failed to fetch user info: ${error instanceof Error ? error.message : "Unknown error"}`,
159
+ )
160
+ }
161
+ }
162
+
163
+ // Store tokens after successful authentication
164
+ async storeTokens(token: TokenResponse, userEmail?: string): Promise<void> {
165
+ const expiresAt = Math.floor(Date.now() / 1000) + token.expires_in
166
+
167
+ const credentials: StoredCredentials = {
168
+ access_token: token.access_token,
169
+ refresh_token: token.refresh_token,
170
+ expires_at: expiresAt,
171
+ user_email: userEmail,
172
+ }
173
+
174
+ await credentialStorage.save(credentials)
175
+ }
176
+
177
+ // Get valid access token
178
+ async getValidAccessToken(): Promise<string | null> {
179
+ const credentials = await credentialStorage.load()
180
+ if (!credentials) {
181
+ return null
182
+ }
183
+
184
+ // Check if token is expired
185
+ if (credentials.expires_at < Date.now() / 1000) {
186
+ try {
187
+ const newToken = await this.refreshAccessToken(
188
+ credentials.refresh_token,
189
+ )
190
+ await this.storeTokens(newToken, credentials.user_email)
191
+ return newToken.access_token
192
+ } catch (error) {
193
+ // Token refresh failed, need to re-authenticate
194
+ await credentialStorage.clear()
195
+ return null
196
+ }
197
+ }
198
+
199
+ return credentials.access_token
200
+ }
201
+
202
+ // Logout and clear stored tokens
203
+ async logout(): Promise<void> {
204
+ const credentials = await credentialStorage.load()
205
+
206
+ if (credentials) {
207
+ try {
208
+ const body = new URLSearchParams({
209
+ client_id: process.env.KEYCLOAK_CLIENT_ID!,
210
+ refresh_token: credentials.refresh_token,
211
+ })
212
+
213
+ // Optionally call Keycloak logout endpoint
214
+ await this.client.post(process.env.KEYCLOAK_LOGOUT_ENDPOINT!, body, {
215
+ headers: {
216
+ "Content-Type": "application/x-www-form-urlencoded",
217
+ ...this.buildClientAuthHeaders(),
218
+ },
219
+ })
220
+ } catch (error) {
221
+ // Continue with local logout even if server call fails
222
+ }
223
+ }
224
+
225
+ // Clear local credentials
226
+ await credentialStorage.clear()
227
+ }
228
+
229
+ /**
230
+ * Check if user is authenticated
231
+ */
232
+ async isAuthenticated(): Promise<boolean> {
233
+ const credentials = await credentialStorage.load()
234
+ return credentials !== null
235
+ }
236
+ }
237
+
238
+ export const authService = new AuthenticationService()
@@ -0,0 +1,114 @@
1
+ import open from "open"
2
+ import { authService } from "../auth/index.js"
3
+ import { startCallbackServer } from "../auth/callback.js"
4
+ import { credentialStorage } from "../storage/credentials.js"
5
+ import chalk from "chalk"
6
+
7
+ export async function handleLogin() {
8
+ try {
9
+ // 1. Check if logged in
10
+ const isAuth = await authService.isAuthenticated()
11
+ if (isAuth) {
12
+ console.log("Already logged in. Use 'deploily logout' to log out first.")
13
+ return
14
+ }
15
+ console.log("Opening browser for authentication...")
16
+
17
+ // 2. Generate authorization URL
18
+ const { url, context } = authService.generateAuthorizationUrl()
19
+
20
+ // 3. Start callback server
21
+ const callbackPromise = startCallbackServer(context.state)
22
+
23
+ // 4. Open browser
24
+ await open(url)
25
+
26
+ console.log("Waiting for authentication...")
27
+
28
+ // 5. Wait for callback
29
+ const callback = await callbackPromise
30
+
31
+ // 6. Exchange code for token
32
+ const tokens = await authService.exchangeCodeForToken(
33
+ callback.code,
34
+ context.codeVerifier,
35
+ )
36
+
37
+ // 7. Get user info
38
+ const userInfo = await authService.getUserInfo(tokens.access_token)
39
+
40
+ // 8. Store tokens
41
+ await authService.storeTokens(tokens, userInfo.email)
42
+ console.log("Successfully authenticated")
43
+ console.log(`Welcome, ${userInfo.email}!`)
44
+ } catch (error) {
45
+ console.error("Error occurred while logging in:", error)
46
+ process.exit(1)
47
+ }
48
+ }
49
+
50
+ export async function handleLogout() {
51
+ try {
52
+ const isAuth = await authService.isAuthenticated()
53
+
54
+ if (!isAuth) {
55
+ console.log("You are not logged in.")
56
+ return
57
+ }
58
+
59
+ await authService.logout()
60
+ console.log("Successfully logged out")
61
+ } catch (error) {
62
+ console.error("Error occurred while logging in:", error)
63
+ process.exit(1)
64
+ }
65
+ }
66
+
67
+ export async function handleWhoami() {
68
+ try {
69
+ const isAuth = await authService.isAuthenticated()
70
+
71
+ if (!isAuth) {
72
+ console.log(
73
+ "You are not logged in. Use `deploily login` to authenticate.",
74
+ )
75
+ process.exit(1)
76
+ }
77
+
78
+ // Get stored credentials
79
+ const credentials = await credentialStorage.load()
80
+
81
+ if (!credentials) {
82
+ console.log(
83
+ "You are not logged in. Use `deploily login` to authenticate.",
84
+ )
85
+ process.exit(1)
86
+ }
87
+
88
+ // Get user info from API
89
+ const userInfo = await authService.getUserInfo(credentials.access_token)
90
+
91
+ console.log("Current User:")
92
+ console.log(`├─ Email: ${userInfo.email}`)
93
+ console.log(`├─ Name: ${userInfo.name}`)
94
+ console.log(`├─ Username: ${userInfo.preferred_username}`)
95
+
96
+ // Calculate token expiry
97
+ const expiresAt = new Date(credentials.expires_at * 1000)
98
+ const now = new Date()
99
+ const timeRemaining = Math.floor(
100
+ (credentials.expires_at * 1000 - now.getTime()) / 1000,
101
+ )
102
+
103
+ if (timeRemaining > 0) {
104
+ const hours = Math.floor(timeRemaining / 3600)
105
+ const minutes = Math.floor((timeRemaining % 3600) / 60)
106
+ console.log(chalk.gray(`└─ Token expires in: ${hours}h ${minutes}m`))
107
+ } else {
108
+ console.log(chalk.gray(`└─ Token expires in: Soon (will auto-refresh)`))
109
+ }
110
+ } catch (error) {
111
+ console.error("Error occurred while logging in:", error)
112
+ process.exit(1)
113
+ }
114
+ }
@@ -0,0 +1,28 @@
1
+ import "dotenv/config"
2
+
3
+ export const AUTH_CONFIG = {
4
+ // Keycloak Configuration
5
+ KEYCLOAK_REALM_URL: process.env.KEYCLOAK_REALM_URL!,
6
+ KEYCLOAK_TOKEN_ENDPOINT: process.env.KEYCLOAK_TOKEN_ENDPOINT!,
7
+ KEYCLOAK_AUTH_ENDPOINT: process.env.KEYCLOAK_AUTH_ENDPOINT!,
8
+ KEYCLOAK_LOGOUT_ENDPOINT: process.env.KEYCLOAK_LOGOUT_ENDPOINT!,
9
+ KEYCLOAK_USERINFO_ENDPOINT: process.env.KEYCLOAK_USERINFO_ENDPOINT!,
10
+
11
+ // CLI Configuration
12
+ CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID!,
13
+ CLIENT_SECRET: process.env.KEYCLOAK_CLIENT_SECRET!,
14
+ REDIRECT_URI: process.env.KEYCLOAK_REDIRECT_URI!,
15
+ CALLBACK_PORT: 8976,
16
+ SCOPE: "openid profile email",
17
+ RESPONSE_TYPE: "code",
18
+ CODE_CHALLENGE_METHOD: "S256",
19
+
20
+ // Token Configuration
21
+ TOKEN_EXPIRY_BUFFER: 60,
22
+ }
23
+
24
+ export const STORAGE_CONFIG = {
25
+ SERVICE_NAME: "deploily-cli",
26
+ CONFIG_DIR: "~/.config/deploily",
27
+ CONFIG_FILE: "config.json",
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { handleLogin, handleLogout, handleWhoami } from "./commands/auth.js"
3
+
4
+ const VERSION = "1.0.0" // TODO: read from package.json
5
+
6
+ async function main() {
7
+ const args = process.argv.slice(2)
8
+ const command = args[0]
9
+
10
+ if (!command || command === "--help" || command === "-h") {
11
+ showHelp()
12
+ return
13
+ }
14
+
15
+ if (command === "--version" || command === "-v") {
16
+ console.log(`deploily version ${VERSION}`)
17
+ return
18
+ }
19
+
20
+ try {
21
+ switch (command) {
22
+ case "login":
23
+ await handleLogin()
24
+ break
25
+
26
+ case "logout":
27
+ await handleLogout()
28
+ break
29
+
30
+ case "whoami":
31
+ await handleWhoami()
32
+ break
33
+
34
+ default:
35
+ console.error(`Unknown command: ${command}`)
36
+ showHelp()
37
+ process.exit(1)
38
+ }
39
+ } catch (err) {
40
+ console.error(`Error executing command '${command}':`, err)
41
+ process.exit(1)
42
+ }
43
+ }
44
+
45
+ function showHelp() {
46
+ console.log(`
47
+ Usage:
48
+ deploily [command]
49
+
50
+ Commands:
51
+ login Authenticate with Keycloak
52
+ logout Clear authentication tokens
53
+ whoami Show current user information
54
+
55
+ Options:
56
+ --help, -h Show this help message
57
+ --version Show version number
58
+
59
+ Examples:
60
+ deploily login
61
+ deploily logout
62
+ deploily whoami
63
+ `)
64
+ }
65
+
66
+ main().catch((err) => {
67
+ console.error(err)
68
+ process.exit(1)
69
+ })