@docyrus/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.
- package/README.md +249 -0
- package/dist/cli.js +2925 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +793 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2925 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { RestApiClient, AuthenticationError as AuthenticationError$1 } from '@docyrus/api-client';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import { URL, fileURLToPath } from 'url';
|
|
6
|
+
import { randomBytes, createDecipheriv, createCipheriv, scryptSync, createHash } from 'crypto';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, constants as constants$1 } from 'fs';
|
|
9
|
+
import { join, resolve, dirname } from 'path';
|
|
10
|
+
import { homedir, arch, release, platform, hostname, userInfo } from 'os';
|
|
11
|
+
import Conf from 'conf';
|
|
12
|
+
import chalk4 from 'chalk';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import { select, input, password } from '@inquirer/prompts';
|
|
15
|
+
import { exec } from 'child_process';
|
|
16
|
+
import { promisify } from 'util';
|
|
17
|
+
import { access, constants, writeFile, readFile, mkdir, cp, rm, readdir } from 'fs/promises';
|
|
18
|
+
import { downloadTemplate } from 'giget';
|
|
19
|
+
import { generateFromOpenAPI } from '@docyrus/tanstack-db-generator';
|
|
20
|
+
|
|
21
|
+
// src/config/constants.ts
|
|
22
|
+
var DOCYRUS_API_URL = "https://alpha-api.docyrus.com";
|
|
23
|
+
var OAUTH_CLIENT_ID = "90565525-8283-4881-82a9-8613eb82ae27";
|
|
24
|
+
var OAUTH_SCOPES = "offline_access Read.All Users.Read Users.Read.All DS.Read.All".split(" ");
|
|
25
|
+
var OAUTH_CALLBACK_PORT_MIN = 9876;
|
|
26
|
+
var OAUTH_CALLBACK_PORT_MAX = 9899;
|
|
27
|
+
var OAUTH_REDIRECT_URI = (port) => `http://localhost:${port}/callback`;
|
|
28
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
29
|
+
var KEYCHAIN_SERVICE = "docyrus-cli";
|
|
30
|
+
var CONFIG_DIR = ".docyrus";
|
|
31
|
+
var CREDENTIALS_FILE = "credentials.enc";
|
|
32
|
+
var TOKEN_KEYS = {
|
|
33
|
+
ACCESS_TOKEN: "accessToken",
|
|
34
|
+
REFRESH_TOKEN: "refreshToken",
|
|
35
|
+
USER_EMAIL: "userEmail",
|
|
36
|
+
GITHUB_TOKEN: "githubToken"
|
|
37
|
+
// For private template access - persists after logout
|
|
38
|
+
};
|
|
39
|
+
var CLI_NAME = "docyrus";
|
|
40
|
+
var CLI_VERSION = "0.0.1";
|
|
41
|
+
var NPM_PACKAGE_NAME = "docyrus";
|
|
42
|
+
|
|
43
|
+
// src/utils/errors.ts
|
|
44
|
+
var CliError = class extends Error {
|
|
45
|
+
exitCode;
|
|
46
|
+
suggestion;
|
|
47
|
+
constructor(message, exitCode = 1, suggestion) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "CliError";
|
|
50
|
+
this.exitCode = exitCode;
|
|
51
|
+
this.suggestion = suggestion;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var AuthenticationError = class extends CliError {
|
|
55
|
+
constructor(message = "Authentication failed", suggestion) {
|
|
56
|
+
super(message, 1, suggestion || "Please check your credentials and try again.");
|
|
57
|
+
this.name = "AuthenticationError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var NotLoggedInError = class extends CliError {
|
|
61
|
+
constructor(message = "You are not logged in.") {
|
|
62
|
+
super(message, 1, "Run `docyrus login` to authenticate.");
|
|
63
|
+
this.name = "NotLoggedInError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var NetworkError = class extends CliError {
|
|
67
|
+
constructor(message = "Network request failed", suggestion) {
|
|
68
|
+
super(message, 1, suggestion || "Please check your internet connection and try again.");
|
|
69
|
+
this.name = "NetworkError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var TimeoutError = class extends CliError {
|
|
73
|
+
constructor(message = "Operation timed out") {
|
|
74
|
+
super(message, 1, "Please try again.");
|
|
75
|
+
this.name = "TimeoutError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var OAuthError = class extends CliError {
|
|
79
|
+
constructor(message, suggestion) {
|
|
80
|
+
super(message, 1, suggestion || "Please try again or use email/password login.");
|
|
81
|
+
this.name = "OAuthError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var TemplateError = class extends CliError {
|
|
85
|
+
constructor(message) {
|
|
86
|
+
super(message, 1, "Check your internet connection and try again.");
|
|
87
|
+
this.name = "TemplateError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var ProjectExistsError = class extends CliError {
|
|
91
|
+
constructor(projectName) {
|
|
92
|
+
super(
|
|
93
|
+
`Directory "${projectName}" already exists.`,
|
|
94
|
+
1,
|
|
95
|
+
"Choose a different project name or delete the existing directory."
|
|
96
|
+
);
|
|
97
|
+
this.name = "ProjectExistsError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var ConflictingFlagsError = class extends CliError {
|
|
101
|
+
constructor(flag1, flag2) {
|
|
102
|
+
super(
|
|
103
|
+
`Cannot use ${flag1} and ${flag2} together.`,
|
|
104
|
+
1,
|
|
105
|
+
"Please use only one option from each group."
|
|
106
|
+
);
|
|
107
|
+
this.name = "ConflictingFlagsError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/auth/credential-auth.ts
|
|
112
|
+
function getApiClient() {
|
|
113
|
+
return new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
114
|
+
}
|
|
115
|
+
async function loginWithCredentials(credentials) {
|
|
116
|
+
const client = getApiClient();
|
|
117
|
+
try {
|
|
118
|
+
const response = await client.post("/v1/auth/token", {
|
|
119
|
+
email: credentials.email,
|
|
120
|
+
password: credentials.password
|
|
121
|
+
});
|
|
122
|
+
const tokens = {
|
|
123
|
+
accessToken: response.data.access_token,
|
|
124
|
+
refreshToken: response.data.refresh_token,
|
|
125
|
+
expiresIn: response.data.expires_in
|
|
126
|
+
};
|
|
127
|
+
if (tokens.expiresIn) {
|
|
128
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
tokens,
|
|
132
|
+
user: response.data.user
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof AuthenticationError$1) {
|
|
136
|
+
throw new AuthenticationError("Invalid email or password.");
|
|
137
|
+
}
|
|
138
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
139
|
+
throw new NetworkError();
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function getUserInfo(accessToken) {
|
|
145
|
+
const client = new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
146
|
+
await client.setAccessToken(accessToken);
|
|
147
|
+
try {
|
|
148
|
+
const response = await client.get("/v1/users/me");
|
|
149
|
+
return response.data;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof AuthenticationError$1) {
|
|
152
|
+
throw new AuthenticationError("Session expired. Please log in again.");
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function refreshAccessToken(refreshToken) {
|
|
158
|
+
const tokenUrl = `${DOCYRUS_API_URL}/v1/oauth2/token`;
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(tokenUrl, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
164
|
+
},
|
|
165
|
+
body: new URLSearchParams({
|
|
166
|
+
grant_type: "refresh_token",
|
|
167
|
+
client_id: OAUTH_CLIENT_ID,
|
|
168
|
+
refresh_token: refreshToken
|
|
169
|
+
}).toString()
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const errorData = await response.json().catch(() => ({}));
|
|
173
|
+
throw new AuthenticationError(
|
|
174
|
+
errorData.error_description || "Failed to refresh token"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
const tokens = {
|
|
179
|
+
accessToken: data.access_token,
|
|
180
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
181
|
+
expiresIn: data.expires_in,
|
|
182
|
+
tokenType: data.token_type
|
|
183
|
+
};
|
|
184
|
+
if (tokens.expiresIn) {
|
|
185
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
186
|
+
}
|
|
187
|
+
return tokens;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof AuthenticationError) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
193
|
+
throw new NetworkError();
|
|
194
|
+
}
|
|
195
|
+
throw new AuthenticationError("Session expired. Please log in again.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
var SUCCESS_HTML = `
|
|
199
|
+
<!DOCTYPE html>
|
|
200
|
+
<html>
|
|
201
|
+
<head>
|
|
202
|
+
<title>Authentication Successful - Docyrus CLI</title>
|
|
203
|
+
<style>
|
|
204
|
+
* { box-sizing: border-box; }
|
|
205
|
+
body {
|
|
206
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
207
|
+
display: flex;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
align-items: center;
|
|
210
|
+
min-height: 100vh;
|
|
211
|
+
margin: 0;
|
|
212
|
+
background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);
|
|
213
|
+
}
|
|
214
|
+
.container {
|
|
215
|
+
width: 100%;
|
|
216
|
+
max-width: 400px;
|
|
217
|
+
padding: 32px;
|
|
218
|
+
background: white;
|
|
219
|
+
border-radius: 16px;
|
|
220
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
221
|
+
text-align: center;
|
|
222
|
+
}
|
|
223
|
+
.logo {
|
|
224
|
+
margin-bottom: 24px;
|
|
225
|
+
}
|
|
226
|
+
.logo svg {
|
|
227
|
+
height: 40px;
|
|
228
|
+
width: auto;
|
|
229
|
+
}
|
|
230
|
+
h1 {
|
|
231
|
+
margin: 0 0 8px;
|
|
232
|
+
font-size: 20px;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
color: #1e293b;
|
|
235
|
+
}
|
|
236
|
+
p {
|
|
237
|
+
margin: 0;
|
|
238
|
+
color: #64748b;
|
|
239
|
+
font-size: 14px;
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>
|
|
244
|
+
<div class="container">
|
|
245
|
+
<div class="logo">
|
|
246
|
+
<svg viewBox="0 0 1325.62 253.55" xmlns="http://www.w3.org/2000/svg">
|
|
247
|
+
<path fill="#dc2626" d="M169.13,133.22l-.07.06v29.48c0,7.81-6.35,14.17-14.16,14.17h-29.48c-2.54,0-4.9-.68-6.96-1.85l-33.13,33.14c2.16,3.21,3.43,7.09,3.43,11.21,0,5.53-2.22,10.54-5.84,14.17-3.62,3.62-8.64,5.84-14.17,5.84s-10.55-2.22-14.17-5.84c-3.62-3.63-5.84-8.64-5.84-14.17,0-11.06,8.96-20.08,20.07-20.08,4.22,0,8.09,1.29,11.3,3.48l33.11-33.11c-1.22-2.07-1.91-4.46-1.91-7.02v-29.48c0-3.24,1.11-6.26,2.97-8.65l-27.31-37.16H19.96V15.5h71.85v65.82l28.44,38.71c1.62-.65,3.39-.98,5.23-.98h29.48c7.81,0,14.17,6.36,14.17,14.17Z"/>
|
|
248
|
+
<path fill="#1c1c1c" d="M241.24,60.57h48.58c12.19,0,23,2.42,32.44,7.25,9.44,4.83,16.77,11.61,21.98,20.34,5.21,8.73,7.82,18.75,7.82,30.05s-2.61,21.32-7.82,30.05c-5.22,8.73-12.54,15.51-21.98,20.34-9.44,4.83-20.25,7.25-32.44,7.25h-48.58V60.57ZM288.83,161.51c9.33,0,17.54-1.81,24.62-5.43,7.08-3.62,12.54-8.7,16.38-15.23,3.84-6.53,5.76-14.08,5.76-22.64s-1.92-16.11-5.76-22.64c-3.84-6.53-9.3-11.61-16.38-15.23-7.08-3.62-15.29-5.43-24.62-5.43h-31.12v86.61h31.12Z"/>
|
|
249
|
+
<path fill="#1c1c1c" d="M430.77,169.5c-9.33-5.1-16.66-12.16-21.98-21.16-5.33-9-7.99-19.04-7.99-30.13s2.66-21.13,7.99-30.13c5.32-9,12.65-16.06,21.98-21.16,9.33-5.1,19.81-7.66,31.45-7.66s21.96,2.55,31.29,7.66c9.33,5.1,16.63,12.13,21.9,21.08,5.27,8.95,7.9,19.02,7.9,30.22s-2.63,21.27-7.9,30.22c-5.27,8.95-12.57,15.97-21.9,21.08-9.33,5.1-19.76,7.66-31.29,7.66s-22.12-2.55-31.45-7.66ZM485.03,156.74c6.75-3.84,12.07-9.14,15.97-15.89,3.9-6.75,5.85-14.3,5.85-22.64s-1.95-15.89-5.85-22.64c-3.9-6.75-9.22-12.05-15.97-15.89-6.75-3.84-14.35-5.76-22.81-5.76s-16.11,1.92-22.97,5.76c-6.86,3.84-12.24,9.14-16.14,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.28,12.05,16.14,15.89,6.86,3.84,14.52,5.76,22.97,5.76s16.06-1.92,22.81-5.76Z"/>
|
|
250
|
+
<path fill="#1c1c1c" d="M601.78,169.5c-9.28-5.1-16.55-12.13-21.82-21.08-5.27-8.95-7.9-19.02-7.9-30.22s2.63-21.27,7.9-30.22c5.27-8.95,12.57-15.97,21.9-21.08,9.33-5.1,19.76-7.66,31.29-7.66,9,0,17.23,1.51,24.7,4.53,7.46,3.02,13.83,7.49,19.1,13.42l-10.7,10.37c-8.67-9.11-19.49-13.67-32.44-13.67-8.56,0-16.3,1.92-23.22,5.76-6.92,3.84-12.32,9.14-16.22,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.3,12.05,16.22,15.89,6.92,3.84,14.66,5.76,23.22,5.76,12.84,0,23.66-4.61,32.44-13.83l10.7,10.37c-5.27,5.93-11.67,10.43-19.18,13.5-7.52,3.07-15.78,4.61-24.78,4.61-11.53,0-21.93-2.55-31.2-7.66Z"/>
|
|
251
|
+
<path fill="#1c1c1c" d="M775.59,135.99v39.85h-16.3v-40.18l-45.78-75.09h17.62l36.89,60.76,37.05-60.76h16.3l-45.78,75.42Z"/>
|
|
252
|
+
<path fill="#1c1c1c" d="M946.01,175.84l-24.87-35.4c-3.07.22-5.49.33-7.25.33h-28.49v35.07h-16.47V60.57h44.95c14.93,0,26.68,3.57,35.24,10.7,8.56,7.14,12.84,16.96,12.84,29.48,0,8.89-2.2,16.47-6.59,22.72-4.39,6.26-10.65,10.81-18.77,13.67l27.33,38.7h-17.95ZM937.29,120.02c5.49-4.5,8.23-10.92,8.23-19.27s-2.75-14.74-8.23-19.18c-5.49-4.45-13.45-6.67-23.88-6.67h-27.99v51.87h27.99c10.43,0,18.39-2.25,23.88-6.75Z"/>
|
|
253
|
+
<path fill="#1c1c1c" d="M1033.45,163.98c-8.56-8.78-12.84-21.41-12.84-37.87V60.57h16.47v64.88c0,24.7,10.81,37.05,32.44,37.05,10.54,0,18.61-3.05,24.21-9.14s8.4-15.4,8.4-27.91V60.57h15.97v65.54c0,16.58-4.28,29.23-12.84,37.96-8.56,8.73-20.53,13.09-35.9,13.09s-27.33-4.39-35.9-13.17Z"/>
|
|
254
|
+
<path fill="#1c1c1c" d="M1193.26,173.12c-8.07-2.69-14.41-6.18-19.02-10.46l6.09-12.84c4.39,3.95,10.02,7.16,16.88,9.63,6.86,2.47,13.91,3.7,21.16,3.7,9.55,0,16.69-1.62,21.41-4.86,4.72-3.24,7.08-7.55,7.08-12.93,0-3.95-1.29-7.16-3.87-9.63-2.58-2.47-5.76-4.36-9.55-5.68s-9.14-2.8-16.05-4.45c-8.67-2.08-15.67-4.17-21-6.26-5.33-2.08-9.88-5.29-13.67-9.63-3.79-4.34-5.68-10.18-5.68-17.54,0-6.15,1.62-11.69,4.86-16.63,3.24-4.94,8.15-8.89,14.74-11.86,6.59-2.96,14.76-4.45,24.54-4.45,6.81,0,13.5.88,20.09,2.63,6.59,1.76,12.24,4.28,16.96,7.57l-5.43,13.17c-4.83-3.07-9.99-5.41-15.48-7-5.49-1.59-10.87-2.39-16.14-2.39-9.33,0-16.33,1.7-21,5.1-4.67,3.4-7,7.8-7,13.17,0,3.95,1.32,7.16,3.95,9.63,2.63,2.47,5.9,4.39,9.8,5.76,3.9,1.37,9.19,2.83,15.89,4.36,8.67,2.09,15.64,4.17,20.91,6.26,5.27,2.09,9.8,5.27,13.58,9.55,3.79,4.28,5.68,10.04,5.68,17.29,0,6.04-1.65,11.55-4.94,16.55-3.29,5-8.29,8.95-14.99,11.86-6.7,2.91-14.93,4.36-24.7,4.36-8.67,0-17.04-1.34-25.11-4.03Z"/>
|
|
255
|
+
</svg>
|
|
256
|
+
</div>
|
|
257
|
+
<h1>Authentication Successful</h1>
|
|
258
|
+
<p>You can close this tab and return to the CLI.</p>
|
|
259
|
+
</div>
|
|
260
|
+
</body>
|
|
261
|
+
</html>
|
|
262
|
+
`;
|
|
263
|
+
var ERROR_HTML = (message) => `
|
|
264
|
+
<!DOCTYPE html>
|
|
265
|
+
<html>
|
|
266
|
+
<head>
|
|
267
|
+
<title>Authentication Failed - Docyrus CLI</title>
|
|
268
|
+
<style>
|
|
269
|
+
* { box-sizing: border-box; }
|
|
270
|
+
body {
|
|
271
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
272
|
+
display: flex;
|
|
273
|
+
justify-content: center;
|
|
274
|
+
align-items: center;
|
|
275
|
+
min-height: 100vh;
|
|
276
|
+
margin: 0;
|
|
277
|
+
background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);
|
|
278
|
+
}
|
|
279
|
+
.container {
|
|
280
|
+
width: 100%;
|
|
281
|
+
max-width: 400px;
|
|
282
|
+
padding: 32px;
|
|
283
|
+
background: white;
|
|
284
|
+
border-radius: 16px;
|
|
285
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
286
|
+
text-align: center;
|
|
287
|
+
}
|
|
288
|
+
.logo {
|
|
289
|
+
margin-bottom: 24px;
|
|
290
|
+
}
|
|
291
|
+
.logo svg {
|
|
292
|
+
height: 40px;
|
|
293
|
+
width: auto;
|
|
294
|
+
}
|
|
295
|
+
h1 {
|
|
296
|
+
margin: 0 0 8px;
|
|
297
|
+
font-size: 20px;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
color: #1e293b;
|
|
300
|
+
}
|
|
301
|
+
p {
|
|
302
|
+
margin: 0;
|
|
303
|
+
color: #64748b;
|
|
304
|
+
font-size: 14px;
|
|
305
|
+
}
|
|
306
|
+
.error-message {
|
|
307
|
+
margin-top: 16px;
|
|
308
|
+
padding: 12px;
|
|
309
|
+
background: #fef2f2;
|
|
310
|
+
border-radius: 8px;
|
|
311
|
+
color: #dc2626;
|
|
312
|
+
font-size: 13px;
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
315
|
+
</head>
|
|
316
|
+
<body>
|
|
317
|
+
<div class="container">
|
|
318
|
+
<div class="logo">
|
|
319
|
+
<svg viewBox="0 0 1325.62 253.55" xmlns="http://www.w3.org/2000/svg">
|
|
320
|
+
<path fill="#dc2626" d="M169.13,133.22l-.07.06v29.48c0,7.81-6.35,14.17-14.16,14.17h-29.48c-2.54,0-4.9-.68-6.96-1.85l-33.13,33.14c2.16,3.21,3.43,7.09,3.43,11.21,0,5.53-2.22,10.54-5.84,14.17-3.62,3.62-8.64,5.84-14.17,5.84s-10.55-2.22-14.17-5.84c-3.62-3.63-5.84-8.64-5.84-14.17,0-11.06,8.96-20.08,20.07-20.08,4.22,0,8.09,1.29,11.3,3.48l33.11-33.11c-1.22-2.07-1.91-4.46-1.91-7.02v-29.48c0-3.24,1.11-6.26,2.97-8.65l-27.31-37.16H19.96V15.5h71.85v65.82l28.44,38.71c1.62-.65,3.39-.98,5.23-.98h29.48c7.81,0,14.17,6.36,14.17,14.17Z"/>
|
|
321
|
+
<path fill="#1c1c1c" d="M241.24,60.57h48.58c12.19,0,23,2.42,32.44,7.25,9.44,4.83,16.77,11.61,21.98,20.34,5.21,8.73,7.82,18.75,7.82,30.05s-2.61,21.32-7.82,30.05c-5.22,8.73-12.54,15.51-21.98,20.34-9.44,4.83-20.25,7.25-32.44,7.25h-48.58V60.57ZM288.83,161.51c9.33,0,17.54-1.81,24.62-5.43,7.08-3.62,12.54-8.7,16.38-15.23,3.84-6.53,5.76-14.08,5.76-22.64s-1.92-16.11-5.76-22.64c-3.84-6.53-9.3-11.61-16.38-15.23-7.08-3.62-15.29-5.43-24.62-5.43h-31.12v86.61h31.12Z"/>
|
|
322
|
+
<path fill="#1c1c1c" d="M430.77,169.5c-9.33-5.1-16.66-12.16-21.98-21.16-5.33-9-7.99-19.04-7.99-30.13s2.66-21.13,7.99-30.13c5.32-9,12.65-16.06,21.98-21.16,9.33-5.1,19.81-7.66,31.45-7.66s21.96,2.55,31.29,7.66c9.33,5.1,16.63,12.13,21.9,21.08,5.27,8.95,7.9,19.02,7.9,30.22s-2.63,21.27-7.9,30.22c-5.27,8.95-12.57,15.97-21.9,21.08-9.33,5.1-19.76,7.66-31.29,7.66s-22.12-2.55-31.45-7.66ZM485.03,156.74c6.75-3.84,12.07-9.14,15.97-15.89,3.9-6.75,5.85-14.3,5.85-22.64s-1.95-15.89-5.85-22.64c-3.9-6.75-9.22-12.05-15.97-15.89-6.75-3.84-14.35-5.76-22.81-5.76s-16.11,1.92-22.97,5.76c-6.86,3.84-12.24,9.14-16.14,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.28,12.05,16.14,15.89,6.86,3.84,14.52,5.76,22.97,5.76s16.06-1.92,22.81-5.76Z"/>
|
|
323
|
+
<path fill="#1c1c1c" d="M601.78,169.5c-9.28-5.1-16.55-12.13-21.82-21.08-5.27-8.95-7.9-19.02-7.9-30.22s2.63-21.27,7.9-30.22c5.27-8.95,12.57-15.97,21.9-21.08,9.33-5.1,19.76-7.66,31.29-7.66,9,0,17.23,1.51,24.7,4.53,7.46,3.02,13.83,7.49,19.1,13.42l-10.7,10.37c-8.67-9.11-19.49-13.67-32.44-13.67-8.56,0-16.3,1.92-23.22,5.76-6.92,3.84-12.32,9.14-16.22,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.3,12.05,16.22,15.89,6.92,3.84,14.66,5.76,23.22,5.76,12.84,0,23.66-4.61,32.44-13.83l10.7,10.37c-5.27,5.93-11.67,10.43-19.18,13.5-7.52,3.07-15.78,4.61-24.78,4.61-11.53,0-21.93-2.55-31.2-7.66Z"/>
|
|
324
|
+
<path fill="#1c1c1c" d="M775.59,135.99v39.85h-16.3v-40.18l-45.78-75.09h17.62l36.89,60.76,37.05-60.76h16.3l-45.78,75.42Z"/>
|
|
325
|
+
<path fill="#1c1c1c" d="M946.01,175.84l-24.87-35.4c-3.07.22-5.49.33-7.25.33h-28.49v35.07h-16.47V60.57h44.95c14.93,0,26.68,3.57,35.24,10.7,8.56,7.14,12.84,16.96,12.84,29.48,0,8.89-2.2,16.47-6.59,22.72-4.39,6.26-10.65,10.81-18.77,13.67l27.33,38.7h-17.95ZM937.29,120.02c5.49-4.5,8.23-10.92,8.23-19.27s-2.75-14.74-8.23-19.18c-5.49-4.45-13.45-6.67-23.88-6.67h-27.99v51.87h27.99c10.43,0,18.39-2.25,23.88-6.75Z"/>
|
|
326
|
+
<path fill="#1c1c1c" d="M1033.45,163.98c-8.56-8.78-12.84-21.41-12.84-37.87V60.57h16.47v64.88c0,24.7,10.81,37.05,32.44,37.05,10.54,0,18.61-3.05,24.21-9.14s8.4-15.4,8.4-27.91V60.57h15.97v65.54c0,16.58-4.28,29.23-12.84,37.96-8.56,8.73-20.53,13.09-35.9,13.09s-27.33-4.39-35.9-13.17Z"/>
|
|
327
|
+
<path fill="#1c1c1c" d="M1193.26,173.12c-8.07-2.69-14.41-6.18-19.02-10.46l6.09-12.84c4.39,3.95,10.02,7.16,16.88,9.63,6.86,2.47,13.91,3.7,21.16,3.7,9.55,0,16.69-1.62,21.41-4.86,4.72-3.24,7.08-7.55,7.08-12.93,0-3.95-1.29-7.16-3.87-9.63-2.58-2.47-5.76-4.36-9.55-5.68s-9.14-2.8-16.05-4.45c-8.67-2.08-15.67-4.17-21-6.26-5.33-2.08-9.88-5.29-13.67-9.63-3.79-4.34-5.68-10.18-5.68-17.54,0-6.15,1.62-11.69,4.86-16.63,3.24-4.94,8.15-8.89,14.74-11.86,6.59-2.96,14.76-4.45,24.54-4.45,6.81,0,13.5.88,20.09,2.63,6.59,1.76,12.24,4.28,16.96,7.57l-5.43,13.17c-4.83-3.07-9.99-5.41-15.48-7-5.49-1.59-10.87-2.39-16.14-2.39-9.33,0-16.33,1.7-21,5.1-4.67,3.4-7,7.8-7,13.17,0,3.95,1.32,7.16,3.95,9.63,2.63,2.47,5.9,4.39,9.8,5.76,3.9,1.37,9.19,2.83,15.89,4.36,8.67,2.09,15.64,4.17,20.91,6.26,5.27,2.09,9.8,5.27,13.58,9.55,3.79,4.28,5.68,10.04,5.68,17.29,0,6.04-1.65,11.55-4.94,16.55-3.29,5-8.29,8.95-14.99,11.86-6.7,2.91-14.93,4.36-24.7,4.36-8.67,0-17.04-1.34-25.11-4.03Z"/>
|
|
328
|
+
</svg>
|
|
329
|
+
</div>
|
|
330
|
+
<h1>Authentication Failed</h1>
|
|
331
|
+
<p>Something went wrong during authentication.</p>
|
|
332
|
+
<div class="error-message">${message}</div>
|
|
333
|
+
</div>
|
|
334
|
+
</body>
|
|
335
|
+
</html>
|
|
336
|
+
`;
|
|
337
|
+
function generateState() {
|
|
338
|
+
return randomBytes(32).toString("hex");
|
|
339
|
+
}
|
|
340
|
+
function generateCodeVerifier() {
|
|
341
|
+
return randomBytes(32).toString("base64url");
|
|
342
|
+
}
|
|
343
|
+
async function generateCodeChallenge(verifier) {
|
|
344
|
+
const encoder = new TextEncoder();
|
|
345
|
+
const data = encoder.encode(verifier);
|
|
346
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
347
|
+
return Buffer.from(hashBuffer).toString("base64url");
|
|
348
|
+
}
|
|
349
|
+
async function findAvailablePort() {
|
|
350
|
+
return new Promise((resolve3, reject) => {
|
|
351
|
+
const tryPort = (port) => {
|
|
352
|
+
if (port > OAUTH_CALLBACK_PORT_MAX) {
|
|
353
|
+
reject(new OAuthError("Could not find an available port for OAuth callback."));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const server = createServer();
|
|
357
|
+
server.listen(port, "127.0.0.1");
|
|
358
|
+
server.once("listening", () => {
|
|
359
|
+
server.close(() => resolve3(port));
|
|
360
|
+
});
|
|
361
|
+
server.once("error", () => {
|
|
362
|
+
tryPort(port + 1);
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
tryPort(OAUTH_CALLBACK_PORT_MIN);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function startCallbackServer(expectedState) {
|
|
369
|
+
const port = await findAvailablePort();
|
|
370
|
+
let resolveResult;
|
|
371
|
+
let rejectResult;
|
|
372
|
+
const resultPromise = new Promise((resolve3, reject) => {
|
|
373
|
+
resolveResult = resolve3;
|
|
374
|
+
rejectResult = reject;
|
|
375
|
+
});
|
|
376
|
+
const timeoutId = setTimeout(() => {
|
|
377
|
+
rejectResult(new TimeoutError("OAuth authentication timed out."));
|
|
378
|
+
}, OAUTH_TIMEOUT_MS);
|
|
379
|
+
const server = createServer((req, res) => {
|
|
380
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
381
|
+
if (url.pathname === "/callback") {
|
|
382
|
+
const code = url.searchParams.get("code");
|
|
383
|
+
const state2 = url.searchParams.get("state");
|
|
384
|
+
const error = url.searchParams.get("error");
|
|
385
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
386
|
+
if (error) {
|
|
387
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
388
|
+
res.end(ERROR_HTML(errorDescription || error));
|
|
389
|
+
rejectResult(new OAuthError(errorDescription || error));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!code || !state2) {
|
|
393
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
394
|
+
res.end(ERROR_HTML("Missing authorization code or state."));
|
|
395
|
+
rejectResult(new OAuthError("Missing authorization code or state."));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (state2 !== expectedState) {
|
|
399
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
400
|
+
res.end(ERROR_HTML("Invalid state parameter. Possible CSRF attack."));
|
|
401
|
+
rejectResult(new OAuthError("Invalid state parameter."));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
405
|
+
res.end(SUCCESS_HTML);
|
|
406
|
+
resolveResult({ code, state: state2 });
|
|
407
|
+
} else {
|
|
408
|
+
res.writeHead(404);
|
|
409
|
+
res.end("Not Found");
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
server.listen(port, "127.0.0.1");
|
|
413
|
+
const cleanup = () => {
|
|
414
|
+
clearTimeout(timeoutId);
|
|
415
|
+
server.close();
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
server,
|
|
419
|
+
port,
|
|
420
|
+
resultPromise,
|
|
421
|
+
cleanup
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async function openBrowser(url) {
|
|
425
|
+
await open(url);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/auth/oauth-auth.ts
|
|
429
|
+
function getOAuthConfig(port) {
|
|
430
|
+
return {
|
|
431
|
+
authorizationUrl: `${DOCYRUS_API_URL}/v1/oauth2/authorize`,
|
|
432
|
+
tokenUrl: `${DOCYRUS_API_URL}/v1/oauth2/token`,
|
|
433
|
+
clientId: OAUTH_CLIENT_ID,
|
|
434
|
+
redirectUri: OAUTH_REDIRECT_URI(port),
|
|
435
|
+
scopes: OAUTH_SCOPES
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function buildAuthorizationUrl(config, state2, codeChallenge) {
|
|
439
|
+
const params = new URLSearchParams({
|
|
440
|
+
response_type: "code",
|
|
441
|
+
client_id: config.clientId,
|
|
442
|
+
redirect_uri: config.redirectUri,
|
|
443
|
+
scope: config.scopes.join(" "),
|
|
444
|
+
state: state2,
|
|
445
|
+
code_challenge: codeChallenge,
|
|
446
|
+
code_challenge_method: "S256"
|
|
447
|
+
});
|
|
448
|
+
return `${config.authorizationUrl}?${params.toString()}`;
|
|
449
|
+
}
|
|
450
|
+
async function exchangeCodeForTokens(config, code, codeVerifier) {
|
|
451
|
+
const response = await fetch(config.tokenUrl, {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: {
|
|
454
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
455
|
+
},
|
|
456
|
+
body: new URLSearchParams({
|
|
457
|
+
grant_type: "authorization_code",
|
|
458
|
+
client_id: config.clientId,
|
|
459
|
+
code,
|
|
460
|
+
redirect_uri: config.redirectUri,
|
|
461
|
+
code_verifier: codeVerifier
|
|
462
|
+
}).toString()
|
|
463
|
+
});
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
const errorText = await response.text();
|
|
466
|
+
throw new OAuthError(`Failed to exchange authorization code: ${errorText}`);
|
|
467
|
+
}
|
|
468
|
+
const data = await response.json();
|
|
469
|
+
const tokens = {
|
|
470
|
+
accessToken: data.access_token,
|
|
471
|
+
refreshToken: data.refresh_token,
|
|
472
|
+
expiresIn: data.expires_in,
|
|
473
|
+
tokenType: data.token_type
|
|
474
|
+
};
|
|
475
|
+
if (tokens.expiresIn) {
|
|
476
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
477
|
+
}
|
|
478
|
+
return tokens;
|
|
479
|
+
}
|
|
480
|
+
async function getUserInfoFromToken(accessToken) {
|
|
481
|
+
const client = new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
482
|
+
await client.setAccessToken(accessToken);
|
|
483
|
+
try {
|
|
484
|
+
return await client.get("/v1/users/me");
|
|
485
|
+
} catch {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function loginWithOAuth(options = {}) {
|
|
490
|
+
const state2 = generateState();
|
|
491
|
+
const codeVerifier = generateCodeVerifier();
|
|
492
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
493
|
+
const { port, resultPromise, cleanup } = await startCallbackServer(state2);
|
|
494
|
+
const config = getOAuthConfig(port);
|
|
495
|
+
const authUrl = buildAuthorizationUrl(config, state2, codeChallenge);
|
|
496
|
+
try {
|
|
497
|
+
options.onOpeningBrowser?.();
|
|
498
|
+
await openBrowser(authUrl);
|
|
499
|
+
options.onWaitingForAuth?.(authUrl);
|
|
500
|
+
const { code } = await resultPromise;
|
|
501
|
+
const tokens = await exchangeCodeForTokens(config, code, codeVerifier);
|
|
502
|
+
const user = await getUserInfoFromToken(tokens.accessToken);
|
|
503
|
+
return { tokens, user };
|
|
504
|
+
} finally {
|
|
505
|
+
cleanup();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function getHomeDir() {
|
|
509
|
+
return homedir();
|
|
510
|
+
}
|
|
511
|
+
function getConfigDir() {
|
|
512
|
+
return join(getHomeDir(), CONFIG_DIR);
|
|
513
|
+
}
|
|
514
|
+
var ALGORITHM = "aes-256-gcm";
|
|
515
|
+
var KEY_LENGTH = 32;
|
|
516
|
+
var IV_LENGTH = 16;
|
|
517
|
+
var AUTH_TAG_LENGTH = 16;
|
|
518
|
+
var SALT_LENGTH = 32;
|
|
519
|
+
function getMachineId() {
|
|
520
|
+
const parts = [
|
|
521
|
+
hostname(),
|
|
522
|
+
userInfo().username,
|
|
523
|
+
process.platform,
|
|
524
|
+
process.arch
|
|
525
|
+
];
|
|
526
|
+
return createHash("sha256").update(parts.join("-")).digest("hex");
|
|
527
|
+
}
|
|
528
|
+
function deriveKey(salt) {
|
|
529
|
+
const machineId = getMachineId();
|
|
530
|
+
return scryptSync(machineId, salt, KEY_LENGTH);
|
|
531
|
+
}
|
|
532
|
+
function encrypt(data) {
|
|
533
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
534
|
+
const key = deriveKey(salt);
|
|
535
|
+
const iv = randomBytes(IV_LENGTH);
|
|
536
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
537
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
538
|
+
const authTag = cipher.getAuthTag();
|
|
539
|
+
return [
|
|
540
|
+
salt.toString("base64"),
|
|
541
|
+
iv.toString("base64"),
|
|
542
|
+
authTag.toString("base64"),
|
|
543
|
+
encrypted.toString("base64")
|
|
544
|
+
].join(":");
|
|
545
|
+
}
|
|
546
|
+
function decrypt(encryptedData) {
|
|
547
|
+
const parts = encryptedData.split(":");
|
|
548
|
+
if (parts.length !== 4) {
|
|
549
|
+
throw new Error("Invalid encrypted data format");
|
|
550
|
+
}
|
|
551
|
+
const [
|
|
552
|
+
saltB64,
|
|
553
|
+
ivB64,
|
|
554
|
+
authTagB64,
|
|
555
|
+
dataB64
|
|
556
|
+
] = parts;
|
|
557
|
+
const salt = Buffer.from(saltB64, "base64");
|
|
558
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
559
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
560
|
+
const encrypted = Buffer.from(dataB64, "base64");
|
|
561
|
+
const key = deriveKey(salt);
|
|
562
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
563
|
+
decipher.setAuthTag(authTag);
|
|
564
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
565
|
+
return decrypted.toString("utf8");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/storage/file-storage.ts
|
|
569
|
+
var FileStorage = class {
|
|
570
|
+
filePath;
|
|
571
|
+
constructor() {
|
|
572
|
+
const configDir = getConfigDir();
|
|
573
|
+
this.filePath = join(configDir, CREDENTIALS_FILE);
|
|
574
|
+
}
|
|
575
|
+
ensureConfigDir() {
|
|
576
|
+
const configDir = getConfigDir();
|
|
577
|
+
if (!existsSync(configDir)) {
|
|
578
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
readData() {
|
|
582
|
+
if (!existsSync(this.filePath)) {
|
|
583
|
+
return {};
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const encrypted = readFileSync(this.filePath, "utf8");
|
|
587
|
+
const decrypted = decrypt(encrypted);
|
|
588
|
+
return JSON.parse(decrypted);
|
|
589
|
+
} catch {
|
|
590
|
+
return {};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
writeData(data) {
|
|
594
|
+
this.ensureConfigDir();
|
|
595
|
+
const json = JSON.stringify(data);
|
|
596
|
+
const encrypted = encrypt(json);
|
|
597
|
+
writeFileSync(this.filePath, encrypted, { mode: 384 });
|
|
598
|
+
}
|
|
599
|
+
async get(key) {
|
|
600
|
+
const data = this.readData();
|
|
601
|
+
return data[key] ?? null;
|
|
602
|
+
}
|
|
603
|
+
async set(key, value) {
|
|
604
|
+
const data = this.readData();
|
|
605
|
+
data[key] = value;
|
|
606
|
+
this.writeData(data);
|
|
607
|
+
}
|
|
608
|
+
async delete(key) {
|
|
609
|
+
const data = this.readData();
|
|
610
|
+
delete data[key];
|
|
611
|
+
if (Object.keys(data).length === 0) {
|
|
612
|
+
this.clear();
|
|
613
|
+
} else {
|
|
614
|
+
this.writeData(data);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async clear() {
|
|
618
|
+
if (existsSync(this.filePath)) {
|
|
619
|
+
unlinkSync(this.filePath);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async has(key) {
|
|
623
|
+
const data = this.readData();
|
|
624
|
+
return key in data;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/storage/keychain-storage.ts
|
|
629
|
+
var keytar = null;
|
|
630
|
+
async function getKeytar() {
|
|
631
|
+
if (keytar !== null) {
|
|
632
|
+
return keytar;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
keytar = await import('keytar');
|
|
636
|
+
return keytar;
|
|
637
|
+
} catch {
|
|
638
|
+
keytar = null;
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
var KeychainStorage = class {
|
|
643
|
+
service;
|
|
644
|
+
constructor(service = KEYCHAIN_SERVICE) {
|
|
645
|
+
this.service = service;
|
|
646
|
+
}
|
|
647
|
+
async isAvailable() {
|
|
648
|
+
const kt = await getKeytar();
|
|
649
|
+
if (!kt) return false;
|
|
650
|
+
try {
|
|
651
|
+
await kt.findCredentials(this.service);
|
|
652
|
+
return true;
|
|
653
|
+
} catch {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async get(key) {
|
|
658
|
+
const kt = await getKeytar();
|
|
659
|
+
if (!kt) return null;
|
|
660
|
+
try {
|
|
661
|
+
return await kt.getPassword(this.service, key);
|
|
662
|
+
} catch {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async set(key, value) {
|
|
667
|
+
const kt = await getKeytar();
|
|
668
|
+
if (!kt) return false;
|
|
669
|
+
try {
|
|
670
|
+
await kt.setPassword(this.service, key, value);
|
|
671
|
+
return true;
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async delete(key) {
|
|
677
|
+
const kt = await getKeytar();
|
|
678
|
+
if (!kt) return false;
|
|
679
|
+
try {
|
|
680
|
+
return await kt.deletePassword(this.service, key);
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async clear() {
|
|
686
|
+
const kt = await getKeytar();
|
|
687
|
+
if (!kt) return;
|
|
688
|
+
try {
|
|
689
|
+
const credentials = await kt.findCredentials(this.service);
|
|
690
|
+
for (const cred of credentials) {
|
|
691
|
+
await kt.deletePassword(this.service, cred.account);
|
|
692
|
+
}
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/storage/cli-token-manager.ts
|
|
699
|
+
var CliTokenManager = class {
|
|
700
|
+
keychain;
|
|
701
|
+
fileStorage;
|
|
702
|
+
keychainAvailable = null;
|
|
703
|
+
constructor() {
|
|
704
|
+
this.keychain = new KeychainStorage();
|
|
705
|
+
this.fileStorage = new FileStorage();
|
|
706
|
+
}
|
|
707
|
+
async useKeychain() {
|
|
708
|
+
if (this.keychainAvailable === null) {
|
|
709
|
+
this.keychainAvailable = await this.keychain.isAvailable();
|
|
710
|
+
}
|
|
711
|
+
return this.keychainAvailable;
|
|
712
|
+
}
|
|
713
|
+
async getToken() {
|
|
714
|
+
if (await this.useKeychain()) {
|
|
715
|
+
const token = await this.keychain.get(TOKEN_KEYS.ACCESS_TOKEN);
|
|
716
|
+
if (token) return token;
|
|
717
|
+
}
|
|
718
|
+
return await this.fileStorage.get(TOKEN_KEYS.ACCESS_TOKEN);
|
|
719
|
+
}
|
|
720
|
+
async setToken(token) {
|
|
721
|
+
if (await this.useKeychain()) {
|
|
722
|
+
const success = await this.keychain.set(TOKEN_KEYS.ACCESS_TOKEN, token);
|
|
723
|
+
if (success) return;
|
|
724
|
+
}
|
|
725
|
+
await this.fileStorage.set(TOKEN_KEYS.ACCESS_TOKEN, token);
|
|
726
|
+
}
|
|
727
|
+
async clearToken() {
|
|
728
|
+
await this.keychain.delete(TOKEN_KEYS.ACCESS_TOKEN);
|
|
729
|
+
await this.fileStorage.delete(TOKEN_KEYS.ACCESS_TOKEN);
|
|
730
|
+
}
|
|
731
|
+
async getRefreshToken() {
|
|
732
|
+
if (await this.useKeychain()) {
|
|
733
|
+
const token = await this.keychain.get(TOKEN_KEYS.REFRESH_TOKEN);
|
|
734
|
+
if (token) return token;
|
|
735
|
+
}
|
|
736
|
+
return await this.fileStorage.get(TOKEN_KEYS.REFRESH_TOKEN);
|
|
737
|
+
}
|
|
738
|
+
async setRefreshToken(token) {
|
|
739
|
+
if (await this.useKeychain()) {
|
|
740
|
+
const success = await this.keychain.set(TOKEN_KEYS.REFRESH_TOKEN, token);
|
|
741
|
+
if (success) return;
|
|
742
|
+
}
|
|
743
|
+
await this.fileStorage.set(TOKEN_KEYS.REFRESH_TOKEN, token);
|
|
744
|
+
}
|
|
745
|
+
async clearRefreshToken() {
|
|
746
|
+
await this.keychain.delete(TOKEN_KEYS.REFRESH_TOKEN);
|
|
747
|
+
await this.fileStorage.delete(TOKEN_KEYS.REFRESH_TOKEN);
|
|
748
|
+
}
|
|
749
|
+
async getUserEmail() {
|
|
750
|
+
if (await this.useKeychain()) {
|
|
751
|
+
const email = await this.keychain.get(TOKEN_KEYS.USER_EMAIL);
|
|
752
|
+
if (email) return email;
|
|
753
|
+
}
|
|
754
|
+
return await this.fileStorage.get(TOKEN_KEYS.USER_EMAIL);
|
|
755
|
+
}
|
|
756
|
+
async setUserEmail(email) {
|
|
757
|
+
if (await this.useKeychain()) {
|
|
758
|
+
const success = await this.keychain.set(TOKEN_KEYS.USER_EMAIL, email);
|
|
759
|
+
if (success) return;
|
|
760
|
+
}
|
|
761
|
+
await this.fileStorage.set(TOKEN_KEYS.USER_EMAIL, email);
|
|
762
|
+
}
|
|
763
|
+
// GitHub token for private template access (persists after logout)
|
|
764
|
+
async getGithubToken() {
|
|
765
|
+
if (await this.useKeychain()) {
|
|
766
|
+
const token = await this.keychain.get(TOKEN_KEYS.GITHUB_TOKEN);
|
|
767
|
+
if (token) return token;
|
|
768
|
+
}
|
|
769
|
+
return await this.fileStorage.get(TOKEN_KEYS.GITHUB_TOKEN);
|
|
770
|
+
}
|
|
771
|
+
async setGithubToken(token) {
|
|
772
|
+
if (await this.useKeychain()) {
|
|
773
|
+
const success = await this.keychain.set(TOKEN_KEYS.GITHUB_TOKEN, token);
|
|
774
|
+
if (success) return;
|
|
775
|
+
}
|
|
776
|
+
await this.fileStorage.set(TOKEN_KEYS.GITHUB_TOKEN, token);
|
|
777
|
+
}
|
|
778
|
+
async clearAll() {
|
|
779
|
+
const githubToken = await this.getGithubToken();
|
|
780
|
+
await this.keychain.clear();
|
|
781
|
+
await this.fileStorage.clear();
|
|
782
|
+
if (githubToken) {
|
|
783
|
+
await this.setGithubToken(githubToken);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async isLoggedIn() {
|
|
787
|
+
const token = await this.getToken();
|
|
788
|
+
return token !== null;
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
var instance = null;
|
|
792
|
+
function getTokenManager() {
|
|
793
|
+
if (!instance) {
|
|
794
|
+
instance = new CliTokenManager();
|
|
795
|
+
}
|
|
796
|
+
return instance;
|
|
797
|
+
}
|
|
798
|
+
var defaults = {
|
|
799
|
+
telemetryEnabled: true
|
|
800
|
+
};
|
|
801
|
+
var ConfigManager = class {
|
|
802
|
+
config;
|
|
803
|
+
constructor() {
|
|
804
|
+
this.config = new Conf({
|
|
805
|
+
projectName: CLI_NAME,
|
|
806
|
+
defaults
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
get(key) {
|
|
810
|
+
return this.config.get(key);
|
|
811
|
+
}
|
|
812
|
+
set(key, value) {
|
|
813
|
+
this.config.set(key, value);
|
|
814
|
+
}
|
|
815
|
+
delete(key) {
|
|
816
|
+
this.config.delete(key);
|
|
817
|
+
}
|
|
818
|
+
getAll() {
|
|
819
|
+
return this.config.store;
|
|
820
|
+
}
|
|
821
|
+
clear() {
|
|
822
|
+
this.config.clear();
|
|
823
|
+
}
|
|
824
|
+
get path() {
|
|
825
|
+
return this.config.path;
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var configManager = new ConfigManager();
|
|
829
|
+
var logger = {
|
|
830
|
+
success(message) {
|
|
831
|
+
console.info(chalk4.green("\u2713"), message);
|
|
832
|
+
},
|
|
833
|
+
error(message) {
|
|
834
|
+
console.error(chalk4.red("\u2717"), message);
|
|
835
|
+
},
|
|
836
|
+
warn(message) {
|
|
837
|
+
console.warn(chalk4.yellow("\u26A0"), message);
|
|
838
|
+
},
|
|
839
|
+
info(message) {
|
|
840
|
+
console.info(chalk4.blue("\u2139"), message);
|
|
841
|
+
},
|
|
842
|
+
debug(message) {
|
|
843
|
+
if (process.env.DEBUG) {
|
|
844
|
+
console.debug(chalk4.gray("\u22EF"), chalk4.gray(message));
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
log(message) {
|
|
848
|
+
console.info(message);
|
|
849
|
+
},
|
|
850
|
+
newline() {
|
|
851
|
+
console.info();
|
|
852
|
+
},
|
|
853
|
+
dim(message) {
|
|
854
|
+
console.info(chalk4.dim(message));
|
|
855
|
+
},
|
|
856
|
+
bold(message) {
|
|
857
|
+
console.info(chalk4.bold(message));
|
|
858
|
+
},
|
|
859
|
+
link(url) {
|
|
860
|
+
return chalk4.cyan.underline(url);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
var state = {
|
|
864
|
+
format: "text",
|
|
865
|
+
data: {}
|
|
866
|
+
};
|
|
867
|
+
var output = {
|
|
868
|
+
setFormat(format) {
|
|
869
|
+
state.format = format;
|
|
870
|
+
},
|
|
871
|
+
getFormat() {
|
|
872
|
+
return state.format;
|
|
873
|
+
},
|
|
874
|
+
isJson() {
|
|
875
|
+
return state.format === "json";
|
|
876
|
+
},
|
|
877
|
+
// Accumulate data for JSON output
|
|
878
|
+
set(key, value) {
|
|
879
|
+
state.data[key] = value;
|
|
880
|
+
},
|
|
881
|
+
// Clear accumulated data
|
|
882
|
+
clear() {
|
|
883
|
+
state.data = {};
|
|
884
|
+
},
|
|
885
|
+
// Print success message or accumulate for JSON
|
|
886
|
+
success(message, data) {
|
|
887
|
+
if (state.format === "json") {
|
|
888
|
+
state.data = {
|
|
889
|
+
...state.data,
|
|
890
|
+
...data,
|
|
891
|
+
success: true,
|
|
892
|
+
message
|
|
893
|
+
};
|
|
894
|
+
} else {
|
|
895
|
+
console.info(chalk4.green("\u2713"), message);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
// Print error message or accumulate for JSON
|
|
899
|
+
error(message, data) {
|
|
900
|
+
if (state.format === "json") {
|
|
901
|
+
state.data = {
|
|
902
|
+
...state.data,
|
|
903
|
+
...data,
|
|
904
|
+
success: false,
|
|
905
|
+
error: message
|
|
906
|
+
};
|
|
907
|
+
} else {
|
|
908
|
+
console.error(chalk4.red("\u2717"), message);
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
// Print warning message
|
|
912
|
+
warn(message) {
|
|
913
|
+
if (state.format === "json") {
|
|
914
|
+
state.data.warning = message;
|
|
915
|
+
} else {
|
|
916
|
+
console.warn(chalk4.yellow("\u26A0"), message);
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
// Print info message (only in text mode)
|
|
920
|
+
info(message) {
|
|
921
|
+
if (state.format !== "json") {
|
|
922
|
+
console.info(chalk4.blue("\u2139"), message);
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
// Print dim message (only in text mode)
|
|
926
|
+
dim(message) {
|
|
927
|
+
if (state.format !== "json") {
|
|
928
|
+
console.info(chalk4.dim(message));
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
// Print the final JSON output
|
|
932
|
+
flush() {
|
|
933
|
+
if (state.format === "json" && Object.keys(state.data).length > 0) {
|
|
934
|
+
console.info(JSON.stringify(state.data, null, 2));
|
|
935
|
+
state.data = {};
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
// Direct JSON output (for commands that return structured data)
|
|
939
|
+
json(data) {
|
|
940
|
+
if (state.format === "json") {
|
|
941
|
+
console.info(JSON.stringify(data, null, 2));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
// src/ui/messages.ts
|
|
947
|
+
var MESSAGES = {
|
|
948
|
+
// Login
|
|
949
|
+
LOGIN_SUCCESS: (email) => `Successfully logged in as ${email}`,
|
|
950
|
+
LOGIN_FAILED: "Login failed. Please check your credentials.",
|
|
951
|
+
LOGIN_PROMPT_EMAIL: "Email:",
|
|
952
|
+
LOGIN_PROMPT_PASSWORD: "Password:",
|
|
953
|
+
LOGIN_AUTHENTICATING: "Authenticating...",
|
|
954
|
+
// SSO
|
|
955
|
+
SSO_OPENING_BROWSER: "Opening browser for authentication...",
|
|
956
|
+
SSO_WAITING: "Waiting for browser authentication...",
|
|
957
|
+
SSO_SUCCESS: "Browser authentication successful!",
|
|
958
|
+
SSO_TIMEOUT: "Authentication timed out. Please try again.",
|
|
959
|
+
SSO_CANCELLED: "Authentication cancelled.",
|
|
960
|
+
// Logout
|
|
961
|
+
LOGOUT_SUCCESS: "Successfully logged out.",
|
|
962
|
+
LOGOUT_CONFIRM: "Are you sure you want to log out?",
|
|
963
|
+
LOGOUT_IN_PROGRESS: "Logging out...",
|
|
964
|
+
// Whoami
|
|
965
|
+
WHOAMI_NOT_LOGGED_IN: "You are not logged in.",
|
|
966
|
+
WHOAMI_SUGGESTION: "Run `docyrus login` to authenticate.",
|
|
967
|
+
WHOAMI_FETCHING: "Fetching user info...",
|
|
968
|
+
// General
|
|
969
|
+
NOT_LOGGED_IN: "You are not logged in.",
|
|
970
|
+
NETWORK_ERROR: "Network request failed. Please check your internet connection.",
|
|
971
|
+
UNKNOWN_ERROR: "An unexpected error occurred.",
|
|
972
|
+
// Errors
|
|
973
|
+
INVALID_CREDENTIALS: "Invalid email or password.",
|
|
974
|
+
SESSION_EXPIRED: "Your session has expired. Please log in again.",
|
|
975
|
+
SERVER_ERROR: "Server error. Please try again later.",
|
|
976
|
+
// Create command
|
|
977
|
+
CREATE_SELECT_FRAMEWORK: "Select a framework:",
|
|
978
|
+
CREATE_SELECT_UI_LIBRARY: "Select UI library:",
|
|
979
|
+
CREATE_SELECT_LINTER: "Select linter/formatter:",
|
|
980
|
+
CREATE_PROJECT_NAME: "Project name:",
|
|
981
|
+
CREATE_SELECT_PACKAGE_MANAGER: "Select package manager:",
|
|
982
|
+
CREATE_CUSTOMIZE_ALIAS: "Customize import alias? (default: @/)",
|
|
983
|
+
CREATE_CUSTOM_ALIAS: "Enter import alias prefix (must start with @):",
|
|
984
|
+
CREATE_SETTING_UP: "Creating project...",
|
|
985
|
+
CREATE_DOWNLOADING: "Downloading project template...",
|
|
986
|
+
CREATE_COPYING_LOCAL: "Copying local template...",
|
|
987
|
+
CREATE_CONFIGURING_LINTER: (linter) => `Configuring ${linter}...`,
|
|
988
|
+
CREATE_CONFIGURING_ALIAS: (alias) => `Configuring import alias (${alias}/)...`,
|
|
989
|
+
CREATE_INSTALLING: "Installing dependencies...",
|
|
990
|
+
CREATE_SUCCESS: "Project created successfully!",
|
|
991
|
+
CREATE_DOWNLOAD_OPENAPI: "Download OpenAPI spec for code generation?",
|
|
992
|
+
CREATE_DOWNLOADING_OPENAPI: "Downloading OpenAPI specification...",
|
|
993
|
+
CREATE_OPENAPI_SUCCESS: (path) => `OpenAPI spec saved to ${path}`,
|
|
994
|
+
// Linter Setup
|
|
995
|
+
LINTER_SETUP_TITLE: "Setting up Linter...",
|
|
996
|
+
// State Management
|
|
997
|
+
CREATE_SELECT_STATE_MANAGEMENT: "Select state management:",
|
|
998
|
+
STATE_MANAGEMENT_SETUP_TITLE: "Setting up State Management...",
|
|
999
|
+
// OpenAPI Setup
|
|
1000
|
+
OPENAPI_SETUP_TITLE: "Setting up API...",
|
|
1001
|
+
// API Client
|
|
1002
|
+
API_CLIENT_DOCS: "API Client: https://www.npmjs.com/package/@docyrus/api-client",
|
|
1003
|
+
// UI Library Setup
|
|
1004
|
+
UI_SETUP_TITLE: "Setting up UI Library...",
|
|
1005
|
+
UI_MULTIPLE_LIBRARIES: "Cannot use multiple UI libraries together",
|
|
1006
|
+
// Docyrus Token (for private template access)
|
|
1007
|
+
DOCYRUS_TOKEN_REQUIRED: "Docyrus token required for template download",
|
|
1008
|
+
DOCYRUS_TOKEN_PROMPT: "Enter Docyrus token:",
|
|
1009
|
+
DOCYRUS_TOKEN_SAVED: "Docyrus token saved successfully",
|
|
1010
|
+
DOCYRUS_TOKEN_INVALID: "Invalid Docyrus token. Please check and try again."
|
|
1011
|
+
};
|
|
1012
|
+
function createSpinner(text) {
|
|
1013
|
+
return ora({
|
|
1014
|
+
text,
|
|
1015
|
+
spinner: "dots"
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
async function withSpinner(text, fn, options) {
|
|
1019
|
+
if (options?.silent) {
|
|
1020
|
+
return fn();
|
|
1021
|
+
}
|
|
1022
|
+
const spinner = createSpinner(text).start();
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await fn();
|
|
1025
|
+
if (options?.successText) {
|
|
1026
|
+
spinner.succeed(options.successText);
|
|
1027
|
+
} else {
|
|
1028
|
+
spinner.stop();
|
|
1029
|
+
}
|
|
1030
|
+
return result;
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (options?.failText) {
|
|
1033
|
+
spinner.fail(options.failText);
|
|
1034
|
+
} else {
|
|
1035
|
+
spinner.stop();
|
|
1036
|
+
}
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function promptEmail(defaultValue) {
|
|
1041
|
+
return input({
|
|
1042
|
+
message: MESSAGES.LOGIN_PROMPT_EMAIL,
|
|
1043
|
+
default: defaultValue,
|
|
1044
|
+
validate: (value) => {
|
|
1045
|
+
if (!value || !value.includes("@")) {
|
|
1046
|
+
return "Please enter a valid email address.";
|
|
1047
|
+
}
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
async function promptPassword() {
|
|
1053
|
+
return password({
|
|
1054
|
+
message: MESSAGES.LOGIN_PROMPT_PASSWORD,
|
|
1055
|
+
mask: "*",
|
|
1056
|
+
validate: (value) => {
|
|
1057
|
+
if (!value || value.length < 1) {
|
|
1058
|
+
return "Please enter your password.";
|
|
1059
|
+
}
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
({
|
|
1065
|
+
pending: chalk4.dim("\u25CB"),
|
|
1066
|
+
in_progress: chalk4.cyan("\u25D0"),
|
|
1067
|
+
completed: chalk4.green("\u2713"),
|
|
1068
|
+
failed: chalk4.red("\u2717")
|
|
1069
|
+
});
|
|
1070
|
+
function createSimpleProgress() {
|
|
1071
|
+
let spinner = null;
|
|
1072
|
+
return (step, current, total) => {
|
|
1073
|
+
if (spinner) {
|
|
1074
|
+
spinner.succeed();
|
|
1075
|
+
spinner = null;
|
|
1076
|
+
}
|
|
1077
|
+
const text = current && total ? `${step} (${current}/${total})` : step;
|
|
1078
|
+
spinner = ora({
|
|
1079
|
+
text,
|
|
1080
|
+
color: "cyan"
|
|
1081
|
+
}).start();
|
|
1082
|
+
if (current && total && current === total) {
|
|
1083
|
+
spinner.succeed();
|
|
1084
|
+
spinner = null;
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/commands/login.ts
|
|
1090
|
+
async function handleCredentialLogin(email) {
|
|
1091
|
+
const lastEmail = configManager.get("lastLoginEmail");
|
|
1092
|
+
const userEmail = email || await promptEmail(lastEmail);
|
|
1093
|
+
const password4 = await promptPassword();
|
|
1094
|
+
const result = await withSpinner(
|
|
1095
|
+
MESSAGES.LOGIN_AUTHENTICATING,
|
|
1096
|
+
() => loginWithCredentials({ email: userEmail, password: password4 })
|
|
1097
|
+
);
|
|
1098
|
+
const tokenManager = getTokenManager();
|
|
1099
|
+
await tokenManager.setToken(result.tokens.accessToken);
|
|
1100
|
+
if (result.tokens.refreshToken) {
|
|
1101
|
+
await tokenManager.setRefreshToken(result.tokens.refreshToken);
|
|
1102
|
+
}
|
|
1103
|
+
const finalEmail = result.user?.email || userEmail;
|
|
1104
|
+
await tokenManager.setUserEmail(finalEmail);
|
|
1105
|
+
configManager.set("lastLoginEmail", finalEmail);
|
|
1106
|
+
output.success(MESSAGES.LOGIN_SUCCESS(finalEmail), { email: finalEmail });
|
|
1107
|
+
}
|
|
1108
|
+
async function handleSsoLogin() {
|
|
1109
|
+
const result = await loginWithOAuth({
|
|
1110
|
+
onOpeningBrowser: () => {
|
|
1111
|
+
logger.info(MESSAGES.SSO_OPENING_BROWSER);
|
|
1112
|
+
},
|
|
1113
|
+
onWaitingForAuth: (url) => {
|
|
1114
|
+
logger.dim(` ${url}`);
|
|
1115
|
+
logger.info(MESSAGES.SSO_WAITING);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
const tokenManager = getTokenManager();
|
|
1119
|
+
await tokenManager.setToken(result.tokens.accessToken);
|
|
1120
|
+
if (result.tokens.refreshToken) {
|
|
1121
|
+
await tokenManager.setRefreshToken(result.tokens.refreshToken);
|
|
1122
|
+
}
|
|
1123
|
+
let email = result.user?.email;
|
|
1124
|
+
if (!email && result.tokens.accessToken) {
|
|
1125
|
+
try {
|
|
1126
|
+
const userInfo2 = await getUserInfo(result.tokens.accessToken);
|
|
1127
|
+
({ email } = userInfo2);
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (email) {
|
|
1132
|
+
await tokenManager.setUserEmail(email);
|
|
1133
|
+
configManager.set("lastLoginEmail", email);
|
|
1134
|
+
output.success(MESSAGES.LOGIN_SUCCESS(email), { email });
|
|
1135
|
+
} else {
|
|
1136
|
+
output.success("Successfully logged in.");
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function registerLoginCommand(program2) {
|
|
1140
|
+
program2.command("login").description("Log in to Docyrus (uses browser-based SSO by default)").option("-e, --email [email]", "Log in with email/password instead of SSO").action(async (options) => {
|
|
1141
|
+
try {
|
|
1142
|
+
const tokenManager = getTokenManager();
|
|
1143
|
+
if (await tokenManager.isLoggedIn()) {
|
|
1144
|
+
const email = await tokenManager.getUserEmail();
|
|
1145
|
+
output.warn(`Already logged in${email ? ` as ${email}` : ""}. Run \`docyrus logout\` first to log in as a different user.`);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (options.email !== void 0) {
|
|
1149
|
+
const emailValue = typeof options.email === "string" ? options.email : void 0;
|
|
1150
|
+
await handleCredentialLogin(emailValue);
|
|
1151
|
+
} else {
|
|
1152
|
+
await handleSsoLogin();
|
|
1153
|
+
}
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
if (error instanceof AuthenticationError) {
|
|
1156
|
+
output.error(error.message);
|
|
1157
|
+
if (error.suggestion && !output.isJson()) {
|
|
1158
|
+
logger.dim(error.suggestion);
|
|
1159
|
+
}
|
|
1160
|
+
process.exit(error.exitCode);
|
|
1161
|
+
}
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/commands/logout.ts
|
|
1168
|
+
function registerLogoutCommand(program2) {
|
|
1169
|
+
program2.command("logout").description("Log out from Docyrus").action(async () => {
|
|
1170
|
+
const tokenManager = getTokenManager();
|
|
1171
|
+
if (!await tokenManager.isLoggedIn()) {
|
|
1172
|
+
output.warn(MESSAGES.WHOAMI_NOT_LOGGED_IN);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const email = await tokenManager.getUserEmail();
|
|
1176
|
+
await withSpinner(
|
|
1177
|
+
MESSAGES.LOGOUT_IN_PROGRESS,
|
|
1178
|
+
async () => {
|
|
1179
|
+
await tokenManager.clearAll();
|
|
1180
|
+
},
|
|
1181
|
+
{ silent: output.isJson() }
|
|
1182
|
+
);
|
|
1183
|
+
output.success(MESSAGES.LOGOUT_SUCCESS, email ? { email } : void 0);
|
|
1184
|
+
if (email && !output.isJson()) {
|
|
1185
|
+
logger.dim(`Logged out from ${email}`);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/utils/auth-guard.ts
|
|
1191
|
+
function decodeJwtPayload(token) {
|
|
1192
|
+
try {
|
|
1193
|
+
const parts = token.split(".");
|
|
1194
|
+
if (parts.length !== 3) return null;
|
|
1195
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
1196
|
+
return JSON.parse(payload);
|
|
1197
|
+
} catch {
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function isTokenExpired(token) {
|
|
1202
|
+
const payload = decodeJwtPayload(token);
|
|
1203
|
+
if (!payload?.exp) return false;
|
|
1204
|
+
const expirationTime = payload.exp * 1e3;
|
|
1205
|
+
const now = Date.now();
|
|
1206
|
+
return now >= expirationTime - 3e4;
|
|
1207
|
+
}
|
|
1208
|
+
async function requireAuth() {
|
|
1209
|
+
const tokenManager = getTokenManager();
|
|
1210
|
+
if (!await tokenManager.isLoggedIn()) {
|
|
1211
|
+
throw new NotLoggedInError();
|
|
1212
|
+
}
|
|
1213
|
+
let token = await tokenManager.getToken();
|
|
1214
|
+
if (!token) {
|
|
1215
|
+
throw new NotLoggedInError();
|
|
1216
|
+
}
|
|
1217
|
+
if (isTokenExpired(token)) {
|
|
1218
|
+
const refreshToken = await tokenManager.getRefreshToken();
|
|
1219
|
+
if (!refreshToken) {
|
|
1220
|
+
await tokenManager.clearAll();
|
|
1221
|
+
throw new NotLoggedInError("Your session has expired. Please log in again.");
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
const newTokens = await refreshAccessToken(refreshToken);
|
|
1225
|
+
await tokenManager.setToken(newTokens.accessToken);
|
|
1226
|
+
if (newTokens.refreshToken) {
|
|
1227
|
+
await tokenManager.setRefreshToken(newTokens.refreshToken);
|
|
1228
|
+
}
|
|
1229
|
+
token = newTokens.accessToken;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
await tokenManager.clearAll();
|
|
1232
|
+
if (error instanceof AuthenticationError) {
|
|
1233
|
+
throw new NotLoggedInError("Your session has expired. Please log in again.");
|
|
1234
|
+
}
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return token;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/commands/whoami.ts
|
|
1242
|
+
function registerWhoamiCommand(program2) {
|
|
1243
|
+
program2.command("whoami").description("Display the current logged-in user").action(async () => {
|
|
1244
|
+
const tokenManager = getTokenManager();
|
|
1245
|
+
const token = await requireAuth();
|
|
1246
|
+
try {
|
|
1247
|
+
const user = await withSpinner(
|
|
1248
|
+
MESSAGES.WHOAMI_FETCHING,
|
|
1249
|
+
() => getUserInfo(token),
|
|
1250
|
+
{ silent: output.isJson() }
|
|
1251
|
+
);
|
|
1252
|
+
if (output.isJson()) {
|
|
1253
|
+
output.set("user", {
|
|
1254
|
+
email: user.email,
|
|
1255
|
+
firstname: user.firstname,
|
|
1256
|
+
lastname: user.lastname
|
|
1257
|
+
});
|
|
1258
|
+
} else {
|
|
1259
|
+
logger.newline();
|
|
1260
|
+
logger.log(`Email: ${user.email}`);
|
|
1261
|
+
if (user.firstname || user.lastname) {
|
|
1262
|
+
const fullName = [user.firstname, user.lastname].filter(Boolean).join(" ");
|
|
1263
|
+
logger.log(`Name: ${fullName}`);
|
|
1264
|
+
}
|
|
1265
|
+
logger.newline();
|
|
1266
|
+
}
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
const cachedEmail = await tokenManager.getUserEmail();
|
|
1269
|
+
if (cachedEmail) {
|
|
1270
|
+
if (output.isJson()) {
|
|
1271
|
+
output.set("user", { email: cachedEmail, cached: true });
|
|
1272
|
+
} else {
|
|
1273
|
+
logger.newline();
|
|
1274
|
+
logger.log(`Email: ${cachedEmail}`);
|
|
1275
|
+
logger.dim("(Unable to fetch full profile from server)");
|
|
1276
|
+
logger.newline();
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
var ESLINT_CONFIGS = {
|
|
1285
|
+
react: {
|
|
1286
|
+
imports: ["baseConfig", "reactConfig"],
|
|
1287
|
+
configs: ["...baseConfig", "...reactConfig"]
|
|
1288
|
+
},
|
|
1289
|
+
nextjs: {
|
|
1290
|
+
imports: [
|
|
1291
|
+
"baseConfig",
|
|
1292
|
+
"reactConfig",
|
|
1293
|
+
"nextjsConfig"
|
|
1294
|
+
],
|
|
1295
|
+
configs: [
|
|
1296
|
+
"...baseConfig",
|
|
1297
|
+
"...reactConfig",
|
|
1298
|
+
"...nextjsConfig"
|
|
1299
|
+
]
|
|
1300
|
+
},
|
|
1301
|
+
vue: {
|
|
1302
|
+
imports: ["baseConfig", "vueConfig"],
|
|
1303
|
+
configs: ["...baseConfig", "...vueConfig"]
|
|
1304
|
+
},
|
|
1305
|
+
electron: {
|
|
1306
|
+
imports: ["baseConfig", "electronConfig"],
|
|
1307
|
+
configs: ["...baseConfig", "...electronConfig"]
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
function generateEslintConfig(framework) {
|
|
1311
|
+
const { imports, configs } = ESLINT_CONFIGS[framework];
|
|
1312
|
+
return `import { ${imports.join(", ")} } from '@docyrus/rules/eslint';
|
|
1313
|
+
|
|
1314
|
+
export default [${configs.join(", ")}];
|
|
1315
|
+
`;
|
|
1316
|
+
}
|
|
1317
|
+
function generateBiomeConfig(framework) {
|
|
1318
|
+
return `${JSON.stringify({ extends: [`@docyrus/rules/biome/${framework}`] }, null, 2)}
|
|
1319
|
+
`;
|
|
1320
|
+
}
|
|
1321
|
+
async function applyLinterConfig(targetDir, framework, linter, onProgress) {
|
|
1322
|
+
if (linter === "none") return;
|
|
1323
|
+
const linterName = linter === "eslint" ? "ESLint" : "Biome";
|
|
1324
|
+
onProgress(`Generating ${linterName} config...`, 1, 2);
|
|
1325
|
+
if (linter === "eslint") {
|
|
1326
|
+
await writeFile(join(targetDir, "eslint.config.mjs"), generateEslintConfig(framework));
|
|
1327
|
+
} else {
|
|
1328
|
+
await writeFile(join(targetDir, "biome.json"), generateBiomeConfig(framework));
|
|
1329
|
+
}
|
|
1330
|
+
onProgress("Updating package.json...", 2, 2);
|
|
1331
|
+
const targetPkgPath = join(targetDir, "package.json");
|
|
1332
|
+
const targetPkg = JSON.parse(await readFile(targetPkgPath, "utf-8"));
|
|
1333
|
+
let devDependencies;
|
|
1334
|
+
let scripts;
|
|
1335
|
+
if (linter === "eslint") {
|
|
1336
|
+
devDependencies = { "@docyrus/rules": "latest", eslint: "latest" };
|
|
1337
|
+
scripts = { lint: "eslint .", format: "eslint --fix ." };
|
|
1338
|
+
} else {
|
|
1339
|
+
devDependencies = { "@docyrus/rules": "latest", "@biomejs/biome": "latest" };
|
|
1340
|
+
scripts = { lint: "biome check .", format: "biome check --fix ." };
|
|
1341
|
+
}
|
|
1342
|
+
targetPkg.devDependencies = {
|
|
1343
|
+
...targetPkg.devDependencies,
|
|
1344
|
+
...devDependencies
|
|
1345
|
+
};
|
|
1346
|
+
targetPkg.scripts = {
|
|
1347
|
+
...targetPkg.scripts,
|
|
1348
|
+
...scripts
|
|
1349
|
+
};
|
|
1350
|
+
await writeFile(targetPkgPath, JSON.stringify(targetPkg, null, 2));
|
|
1351
|
+
}
|
|
1352
|
+
async function applyAliasConfig(targetDir, framework, aliasPrefix) {
|
|
1353
|
+
if (aliasPrefix === "@" || aliasPrefix === "@/") return;
|
|
1354
|
+
const config = {
|
|
1355
|
+
prefix: aliasPrefix,
|
|
1356
|
+
pattern: `${aliasPrefix}/*`,
|
|
1357
|
+
replacement: "./src/*"
|
|
1358
|
+
};
|
|
1359
|
+
await updateTsConfig(targetDir, config);
|
|
1360
|
+
if (framework === "react" || framework === "vue" || framework === "electron") {
|
|
1361
|
+
await updateViteConfig(targetDir, config);
|
|
1362
|
+
}
|
|
1363
|
+
await updateImports(targetDir, aliasPrefix, framework);
|
|
1364
|
+
}
|
|
1365
|
+
async function updateTsConfig(targetDir, config) {
|
|
1366
|
+
const tsconfigPath = join(targetDir, "tsconfig.json");
|
|
1367
|
+
const tsconfig = JSON.parse(await readFile(tsconfigPath, "utf-8"));
|
|
1368
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
1369
|
+
delete tsconfig.compilerOptions.paths["@/*"];
|
|
1370
|
+
tsconfig.compilerOptions.paths[config.pattern] = [config.replacement];
|
|
1371
|
+
}
|
|
1372
|
+
await writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
1373
|
+
}
|
|
1374
|
+
async function updateViteConfig(targetDir, config) {
|
|
1375
|
+
const vitePath = join(targetDir, "vite.config.ts");
|
|
1376
|
+
try {
|
|
1377
|
+
let content = await readFile(vitePath, "utf-8");
|
|
1378
|
+
content = content.replace(
|
|
1379
|
+
/'@':\s*resolve\(/g,
|
|
1380
|
+
`'${config.prefix}': resolve(`
|
|
1381
|
+
);
|
|
1382
|
+
await writeFile(vitePath, content);
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
async function updateImports(targetDir, newPrefix, framework) {
|
|
1387
|
+
const srcDir = join(targetDir, "src");
|
|
1388
|
+
try {
|
|
1389
|
+
await updateImportsRecursive(srcDir, newPrefix, framework);
|
|
1390
|
+
} catch {
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function getFileExtensions(framework) {
|
|
1394
|
+
switch (framework) {
|
|
1395
|
+
case "vue":
|
|
1396
|
+
return [
|
|
1397
|
+
".ts",
|
|
1398
|
+
".tsx",
|
|
1399
|
+
".vue"
|
|
1400
|
+
];
|
|
1401
|
+
default:
|
|
1402
|
+
return [".ts", ".tsx"];
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async function updateImportsRecursive(dir, newPrefix, framework) {
|
|
1406
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1407
|
+
const extensions = getFileExtensions(framework);
|
|
1408
|
+
for (const entry of entries) {
|
|
1409
|
+
const fullPath = join(dir, entry.name);
|
|
1410
|
+
if (entry.isDirectory()) {
|
|
1411
|
+
await updateImportsRecursive(fullPath, newPrefix, framework);
|
|
1412
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
1413
|
+
const content = await readFile(fullPath, "utf-8");
|
|
1414
|
+
const updatedContent = content.replace(
|
|
1415
|
+
/from\s+['"]@\//g,
|
|
1416
|
+
`from '${newPrefix}/`
|
|
1417
|
+
);
|
|
1418
|
+
if (updatedContent !== content) {
|
|
1419
|
+
await writeFile(fullPath, updatedContent);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function createAuthenticatedClient(accessToken) {
|
|
1425
|
+
const apiUrl = configManager.get("apiUrl") || DOCYRUS_API_URL;
|
|
1426
|
+
const client = new RestApiClient({ baseURL: apiUrl });
|
|
1427
|
+
client.setAccessToken(accessToken);
|
|
1428
|
+
return client;
|
|
1429
|
+
}
|
|
1430
|
+
async function downloadOpenApiSpec(accessToken, targetDir, filename = "openapi.json") {
|
|
1431
|
+
const client = createAuthenticatedClient(accessToken);
|
|
1432
|
+
const spec = await client.get("/v1/api/openapi.json");
|
|
1433
|
+
const targetPath = join(targetDir, filename);
|
|
1434
|
+
const targetDirPath = dirname(targetPath);
|
|
1435
|
+
await mkdir(targetDirPath, { recursive: true });
|
|
1436
|
+
await writeFile(targetPath, JSON.stringify(spec, null, 2));
|
|
1437
|
+
return targetPath;
|
|
1438
|
+
}
|
|
1439
|
+
async function applyUIVariants(targetDir, uiLibrary) {
|
|
1440
|
+
const variantsDir = join(targetDir, "__ui-variants__");
|
|
1441
|
+
try {
|
|
1442
|
+
await access(variantsDir, constants$1.F_OK);
|
|
1443
|
+
} catch {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (uiLibrary !== "none") {
|
|
1447
|
+
const libraryDir = join(variantsDir, uiLibrary);
|
|
1448
|
+
try {
|
|
1449
|
+
await access(libraryDir, constants$1.F_OK);
|
|
1450
|
+
await cp(libraryDir, targetDir, { recursive: true, force: true });
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
await rm(variantsDir, { recursive: true, force: true });
|
|
1455
|
+
}
|
|
1456
|
+
var execAsync = promisify(exec);
|
|
1457
|
+
function parseEnvArray(value) {
|
|
1458
|
+
if (!value) {
|
|
1459
|
+
throw new Error("Missing environment variable");
|
|
1460
|
+
}
|
|
1461
|
+
return value.split(",").map((s) => s.trim());
|
|
1462
|
+
}
|
|
1463
|
+
var SHADCN_BASE_COLORS = parseEnvArray("zinc,slate,neutral,stone,gray");
|
|
1464
|
+
parseEnvArray("vega,nova,maia,lyra,mira");
|
|
1465
|
+
var DICEUI_COMPONENTS = parseEnvArray("action-bar,avatar-group,badge-overflow,checkbox-group,circular-progress,color-picker,color-swatch,combobox,compare-slider,cropper,editable,file-upload,gauge,kanban,key-value,listbox,mask-input,media-player,mention,phone-input,qr-code,rating,relative-time-card,responsive-dialog,scroll-spy,scroller,segmented-input,sortable,speed-dial,stack,stat,status,stepper,swap,tags-input,time-picker,timeline,tour");
|
|
1466
|
+
var UI_LIBRARY_COMPATIBILITY = {
|
|
1467
|
+
nextjs: [
|
|
1468
|
+
"shadcn",
|
|
1469
|
+
"diceui",
|
|
1470
|
+
"heroui",
|
|
1471
|
+
"none"
|
|
1472
|
+
],
|
|
1473
|
+
react: [
|
|
1474
|
+
"shadcn",
|
|
1475
|
+
"diceui",
|
|
1476
|
+
"heroui",
|
|
1477
|
+
"none"
|
|
1478
|
+
],
|
|
1479
|
+
vue: ["shadcn-vue", "none"],
|
|
1480
|
+
electron: [
|
|
1481
|
+
"shadcn",
|
|
1482
|
+
"diceui",
|
|
1483
|
+
"none"
|
|
1484
|
+
]
|
|
1485
|
+
};
|
|
1486
|
+
function isUILibraryCompatible(framework, uiLibrary) {
|
|
1487
|
+
return UI_LIBRARY_COMPATIBILITY[framework].includes(uiLibrary);
|
|
1488
|
+
}
|
|
1489
|
+
function getUILibraryErrorMessage(framework, uiLibrary) {
|
|
1490
|
+
switch (uiLibrary) {
|
|
1491
|
+
case "heroui":
|
|
1492
|
+
if (framework === "electron") {
|
|
1493
|
+
return "HeroUI is not supported with Electron due to framer-motion compatibility issues. Use --shadcn or --diceui instead";
|
|
1494
|
+
}
|
|
1495
|
+
return `HeroUI only supports Next.js and React frameworks. Current: ${framework}`;
|
|
1496
|
+
case "shadcn":
|
|
1497
|
+
case "diceui":
|
|
1498
|
+
if (framework === "vue") {
|
|
1499
|
+
return "Use --shadcn-vue for Vue projects instead of --shadcn or --diceui";
|
|
1500
|
+
}
|
|
1501
|
+
return `shadcn/ui is not supported with ${framework}`;
|
|
1502
|
+
case "shadcn-vue":
|
|
1503
|
+
return `shadcn-vue is only for Vue projects. Use --shadcn or --diceui for ${framework}`;
|
|
1504
|
+
default:
|
|
1505
|
+
return `${uiLibrary} is not compatible with ${framework}`;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function setupShadcn(targetDir, packageManager, style, baseColor, onProgress) {
|
|
1509
|
+
const dlx = getDlxCommand(packageManager);
|
|
1510
|
+
onProgress("Initializing shadcn/ui (Base UI)...", 1, 4);
|
|
1511
|
+
await execAsync(`${dlx} shadcn@latest init --yes --defaults -b ${baseColor}`, {
|
|
1512
|
+
cwd: targetDir,
|
|
1513
|
+
timeout: 3e5
|
|
1514
|
+
});
|
|
1515
|
+
onProgress("Migrating to Radix UI...", 2, 4);
|
|
1516
|
+
await execAsync(`${dlx} shadcn@latest migrate radix --yes`, {
|
|
1517
|
+
cwd: targetDir,
|
|
1518
|
+
timeout: 12e4
|
|
1519
|
+
});
|
|
1520
|
+
onProgress("Updating style configuration...", 3, 4);
|
|
1521
|
+
const componentsJsonPath = join(targetDir, "components.json");
|
|
1522
|
+
const componentsJson = JSON.parse(await readFile(componentsJsonPath, "utf-8"));
|
|
1523
|
+
componentsJson.style = `radix-${style}`;
|
|
1524
|
+
await writeFile(componentsJsonPath, JSON.stringify(componentsJson, null, 2));
|
|
1525
|
+
onProgress("Installing shadcn components...", 4, 4);
|
|
1526
|
+
await execAsync(`${dlx} shadcn@latest add --all --yes --overwrite`, {
|
|
1527
|
+
cwd: targetDir,
|
|
1528
|
+
timeout: 3e5
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
async function setupDiceUI(targetDir, packageManager, style, baseColor, onProgress) {
|
|
1532
|
+
const dlx = getDlxCommand(packageManager);
|
|
1533
|
+
onProgress("Initializing shadcn/ui (Base UI)...", 1, 5);
|
|
1534
|
+
await execAsync(`${dlx} shadcn@latest init --yes --defaults -b ${baseColor}`, {
|
|
1535
|
+
cwd: targetDir,
|
|
1536
|
+
timeout: 3e5
|
|
1537
|
+
});
|
|
1538
|
+
onProgress("Migrating to Radix UI...", 2, 5);
|
|
1539
|
+
await execAsync(`${dlx} shadcn@latest migrate radix --yes`, {
|
|
1540
|
+
cwd: targetDir,
|
|
1541
|
+
timeout: 12e4
|
|
1542
|
+
});
|
|
1543
|
+
onProgress("Updating style configuration...", 3, 5);
|
|
1544
|
+
const componentsJsonPath = join(targetDir, "components.json");
|
|
1545
|
+
const componentsJson = JSON.parse(await readFile(componentsJsonPath, "utf-8"));
|
|
1546
|
+
componentsJson.style = `radix-${style}`;
|
|
1547
|
+
await writeFile(componentsJsonPath, JSON.stringify(componentsJson, null, 2));
|
|
1548
|
+
onProgress("Installing shadcn components...", 4, 5);
|
|
1549
|
+
await execAsync(`${dlx} shadcn@latest add --all --yes --overwrite`, {
|
|
1550
|
+
cwd: targetDir,
|
|
1551
|
+
timeout: 3e5
|
|
1552
|
+
});
|
|
1553
|
+
onProgress("Installing DiceUI components...", 5, 5);
|
|
1554
|
+
for (const component of DICEUI_COMPONENTS) {
|
|
1555
|
+
await execAsync(`${dlx} shadcn@latest add https://shadcn-extension.vercel.app/r/diceui/${component}.json --yes --overwrite`, {
|
|
1556
|
+
cwd: targetDir,
|
|
1557
|
+
timeout: 6e4
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function setupShadcnVue(targetDir, packageManager, onProgress) {
|
|
1562
|
+
const dlx = getDlxCommand(packageManager);
|
|
1563
|
+
onProgress("Initializing shadcn-vue...", 1, 2);
|
|
1564
|
+
await execAsync(`${dlx} shadcn-vue@latest init -y -d`, {
|
|
1565
|
+
cwd: targetDir,
|
|
1566
|
+
timeout: 12e4
|
|
1567
|
+
});
|
|
1568
|
+
onProgress("Installing shadcn-vue components...", 2, 2);
|
|
1569
|
+
await execAsync(`${dlx} shadcn-vue@latest add -a -y -o`, {
|
|
1570
|
+
cwd: targetDir,
|
|
1571
|
+
timeout: 3e5
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
async function setupHeroUI(targetDir, framework, packageManager, onProgress) {
|
|
1575
|
+
onProgress("Adding HeroUI dependencies...", 1, 3);
|
|
1576
|
+
const installCmd = getInstallCommand(packageManager);
|
|
1577
|
+
await execAsync(`${installCmd} @heroui/react framer-motion`, {
|
|
1578
|
+
cwd: targetDir,
|
|
1579
|
+
timeout: 12e4
|
|
1580
|
+
});
|
|
1581
|
+
onProgress("Configuring styles...", 2, 3);
|
|
1582
|
+
await writeHeroUIStyles(targetDir);
|
|
1583
|
+
onProgress("Adding HeroUIProvider...", 3, 3);
|
|
1584
|
+
await addHeroUIProvider(targetDir, framework);
|
|
1585
|
+
}
|
|
1586
|
+
async function setupUILibrary(targetDir, framework, uiLibrary, packageManager, onProgress, style, baseColor) {
|
|
1587
|
+
if (uiLibrary === "none") {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (!isUILibraryCompatible(framework, uiLibrary)) {
|
|
1591
|
+
throw new Error(getUILibraryErrorMessage(framework, uiLibrary));
|
|
1592
|
+
}
|
|
1593
|
+
switch (uiLibrary) {
|
|
1594
|
+
case "shadcn":
|
|
1595
|
+
await setupShadcn(
|
|
1596
|
+
targetDir,
|
|
1597
|
+
packageManager,
|
|
1598
|
+
style ?? "vega",
|
|
1599
|
+
baseColor ?? "zinc",
|
|
1600
|
+
onProgress
|
|
1601
|
+
);
|
|
1602
|
+
break;
|
|
1603
|
+
case "diceui":
|
|
1604
|
+
await setupDiceUI(
|
|
1605
|
+
targetDir,
|
|
1606
|
+
packageManager,
|
|
1607
|
+
style ?? "vega",
|
|
1608
|
+
baseColor ?? "zinc",
|
|
1609
|
+
onProgress
|
|
1610
|
+
);
|
|
1611
|
+
break;
|
|
1612
|
+
case "shadcn-vue":
|
|
1613
|
+
await setupShadcnVue(targetDir, packageManager, onProgress);
|
|
1614
|
+
break;
|
|
1615
|
+
case "heroui":
|
|
1616
|
+
await setupHeroUI(targetDir, framework, packageManager, onProgress);
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
function getDlxCommand(packageManager) {
|
|
1621
|
+
switch (packageManager) {
|
|
1622
|
+
case "pnpm":
|
|
1623
|
+
return "pnpx";
|
|
1624
|
+
case "yarn":
|
|
1625
|
+
return "yarn dlx";
|
|
1626
|
+
case "bun":
|
|
1627
|
+
return "bunx";
|
|
1628
|
+
default:
|
|
1629
|
+
return "npx";
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function getInstallCommand(packageManager) {
|
|
1633
|
+
switch (packageManager) {
|
|
1634
|
+
case "pnpm":
|
|
1635
|
+
return "pnpm add";
|
|
1636
|
+
case "yarn":
|
|
1637
|
+
return "yarn add";
|
|
1638
|
+
case "bun":
|
|
1639
|
+
return "bun add";
|
|
1640
|
+
default:
|
|
1641
|
+
return "npm install";
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function writeHeroUIStyles(targetDir) {
|
|
1645
|
+
const possibleCssPaths = [join(targetDir, "src/styles/globals.css"), join(targetDir, "src/styles/global.css")];
|
|
1646
|
+
const css = `@import "tailwindcss";
|
|
1647
|
+
@plugin "@heroui/react/tailwind";
|
|
1648
|
+
`;
|
|
1649
|
+
for (const cssPath of possibleCssPaths) {
|
|
1650
|
+
try {
|
|
1651
|
+
await readFile(cssPath, "utf-8");
|
|
1652
|
+
await writeFile(cssPath, css, "utf-8");
|
|
1653
|
+
return;
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
async function addHeroUIProvider(targetDir, framework) {
|
|
1659
|
+
if (framework === "nextjs") {
|
|
1660
|
+
await addHeroUIProviderNextjs(targetDir);
|
|
1661
|
+
} else {
|
|
1662
|
+
await addHeroUIProviderReact(targetDir);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async function addHeroUIProviderNextjs(targetDir) {
|
|
1666
|
+
const providersPath = join(targetDir, "src/app/providers.tsx");
|
|
1667
|
+
const providersContent = `'use client';
|
|
1668
|
+
|
|
1669
|
+
import { HeroUIProvider } from '@heroui/react';
|
|
1670
|
+
|
|
1671
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
1672
|
+
return <HeroUIProvider>{children}</HeroUIProvider>;
|
|
1673
|
+
}
|
|
1674
|
+
`;
|
|
1675
|
+
await writeFile(providersPath, providersContent, "utf-8");
|
|
1676
|
+
const layoutPath = join(targetDir, "src/app/layout.tsx");
|
|
1677
|
+
try {
|
|
1678
|
+
let layoutContent = await readFile(layoutPath, "utf-8");
|
|
1679
|
+
if (!layoutContent.includes("Providers")) {
|
|
1680
|
+
layoutContent = layoutContent.replace(
|
|
1681
|
+
/^(import.*\n)+/m,
|
|
1682
|
+
(match) => `${match}import { Providers } from './providers';
|
|
1683
|
+
`
|
|
1684
|
+
);
|
|
1685
|
+
layoutContent = layoutContent.replace(
|
|
1686
|
+
/(<body[^>]*>)([\s\S]*?)({children})([\s\S]*?)(<\/body>)/,
|
|
1687
|
+
"$1$2<Providers>$3</Providers>$4$5"
|
|
1688
|
+
);
|
|
1689
|
+
await writeFile(layoutPath, layoutContent, "utf-8");
|
|
1690
|
+
}
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
async function addHeroUIProviderReact(targetDir) {
|
|
1695
|
+
const mainPath = join(targetDir, "src/main.tsx");
|
|
1696
|
+
try {
|
|
1697
|
+
let content = await readFile(mainPath, "utf-8");
|
|
1698
|
+
if (!content.includes("HeroUIProvider")) {
|
|
1699
|
+
content = content.replace(
|
|
1700
|
+
/^(import.*\n)+/m,
|
|
1701
|
+
(match) => `${match}import { HeroUIProvider } from '@heroui/react';
|
|
1702
|
+
`
|
|
1703
|
+
);
|
|
1704
|
+
content = content.replace(
|
|
1705
|
+
/(<App\s*\/>)/,
|
|
1706
|
+
"<HeroUIProvider>\n $1\n </HeroUIProvider>"
|
|
1707
|
+
);
|
|
1708
|
+
await writeFile(mainPath, content, "utf-8");
|
|
1709
|
+
}
|
|
1710
|
+
} catch {
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
var STATE_MANAGEMENT_VERSIONS = {
|
|
1714
|
+
zustand: { name: "zustand", version: "5.0.11" },
|
|
1715
|
+
"tanstack-query": { name: "@tanstack/react-query", version: "5.90.20" },
|
|
1716
|
+
"tanstack-vue-query": { name: "@tanstack/vue-query", version: "5.92.9" }
|
|
1717
|
+
};
|
|
1718
|
+
function getCompatibleStateManagement(framework) {
|
|
1719
|
+
switch (framework) {
|
|
1720
|
+
case "react":
|
|
1721
|
+
case "nextjs":
|
|
1722
|
+
case "electron":
|
|
1723
|
+
return [
|
|
1724
|
+
"zustand",
|
|
1725
|
+
"tanstack-query",
|
|
1726
|
+
"none"
|
|
1727
|
+
];
|
|
1728
|
+
case "vue":
|
|
1729
|
+
return ["tanstack-vue-query", "none"];
|
|
1730
|
+
default:
|
|
1731
|
+
return [];
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
async function setupStateManagement(targetDir, framework, stateManagement, onProgress) {
|
|
1735
|
+
if (stateManagement === "none") return;
|
|
1736
|
+
const totalSteps = stateManagement === "zustand" ? 2 : 3;
|
|
1737
|
+
onProgress("Adding dependency...", 1, totalSteps);
|
|
1738
|
+
const pkg = STATE_MANAGEMENT_VERSIONS[stateManagement];
|
|
1739
|
+
await addDependencyToPackageJson(targetDir, pkg.name, pkg.version);
|
|
1740
|
+
onProgress("Creating configuration...", 2, totalSteps);
|
|
1741
|
+
switch (stateManagement) {
|
|
1742
|
+
case "zustand":
|
|
1743
|
+
await createZustandStore(targetDir);
|
|
1744
|
+
break;
|
|
1745
|
+
case "tanstack-query":
|
|
1746
|
+
await createReactQueryClient(targetDir);
|
|
1747
|
+
break;
|
|
1748
|
+
case "tanstack-vue-query":
|
|
1749
|
+
await createVueQueryConfig(targetDir);
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
if (stateManagement !== "zustand") {
|
|
1753
|
+
onProgress("Configuring provider...", 3, totalSteps);
|
|
1754
|
+
if (stateManagement === "tanstack-query") {
|
|
1755
|
+
await wrapReactQueryProvider(targetDir, framework);
|
|
1756
|
+
} else if (stateManagement === "tanstack-vue-query") {
|
|
1757
|
+
await wrapVueQueryPlugin(targetDir);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async function addDependencyToPackageJson(targetDir, name, version) {
|
|
1762
|
+
const pkgPath = join(targetDir, "package.json");
|
|
1763
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
1764
|
+
pkg.dependencies = pkg.dependencies ?? {};
|
|
1765
|
+
pkg.dependencies[name] = version;
|
|
1766
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
|
|
1767
|
+
}
|
|
1768
|
+
async function createZustandStore(targetDir) {
|
|
1769
|
+
const storePath = join(targetDir, "src/store/app-store.ts");
|
|
1770
|
+
await mkdir(dirname(storePath), { recursive: true });
|
|
1771
|
+
await writeFile(storePath, `import { create } from 'zustand';
|
|
1772
|
+
|
|
1773
|
+
interface AppState {
|
|
1774
|
+
count: number;
|
|
1775
|
+
increment: () => void;
|
|
1776
|
+
decrement: () => void;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
1780
|
+
count: 0,
|
|
1781
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
1782
|
+
decrement: () => set((state) => ({ count: state.count - 1 }))
|
|
1783
|
+
}));
|
|
1784
|
+
`, "utf-8");
|
|
1785
|
+
}
|
|
1786
|
+
async function createReactQueryClient(targetDir) {
|
|
1787
|
+
const clientPath = join(targetDir, "src/lib/query-client.ts");
|
|
1788
|
+
await mkdir(dirname(clientPath), { recursive: true });
|
|
1789
|
+
await writeFile(clientPath, `import { QueryClient } from '@tanstack/react-query';
|
|
1790
|
+
|
|
1791
|
+
export const queryClient = new QueryClient({
|
|
1792
|
+
defaultOptions: {
|
|
1793
|
+
queries: {
|
|
1794
|
+
staleTime: 60 * 1000,
|
|
1795
|
+
retry: 1
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
`, "utf-8");
|
|
1800
|
+
}
|
|
1801
|
+
async function createVueQueryConfig(targetDir) {
|
|
1802
|
+
const configPath = join(targetDir, "src/lib/query-client.ts");
|
|
1803
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1804
|
+
await writeFile(configPath, `import type { VueQueryPluginOptions } from '@tanstack/vue-query';
|
|
1805
|
+
|
|
1806
|
+
export const vueQueryOptions: VueQueryPluginOptions = {
|
|
1807
|
+
queryClientConfig: {
|
|
1808
|
+
defaultOptions: {
|
|
1809
|
+
queries: {
|
|
1810
|
+
staleTime: 60 * 1000,
|
|
1811
|
+
retry: 1
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
`, "utf-8");
|
|
1817
|
+
}
|
|
1818
|
+
async function wrapReactQueryProvider(targetDir, framework) {
|
|
1819
|
+
if (framework === "nextjs") {
|
|
1820
|
+
await wrapNextjsQueryProvider(targetDir);
|
|
1821
|
+
} else {
|
|
1822
|
+
await wrapReactEntryQueryProvider(targetDir, framework);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
async function wrapNextjsQueryProvider(targetDir) {
|
|
1826
|
+
const providersPath = join(targetDir, "src/app/providers.tsx");
|
|
1827
|
+
await writeFile(providersPath, `'use client';
|
|
1828
|
+
|
|
1829
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
1830
|
+
|
|
1831
|
+
import { queryClient } from '@/lib/query-client';
|
|
1832
|
+
|
|
1833
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
1834
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
1835
|
+
}
|
|
1836
|
+
`, "utf-8");
|
|
1837
|
+
const layoutPath = join(targetDir, "src/app/layout.tsx");
|
|
1838
|
+
try {
|
|
1839
|
+
let content = await readFile(layoutPath, "utf-8");
|
|
1840
|
+
if (!content.includes("Providers")) {
|
|
1841
|
+
content = content.replace(
|
|
1842
|
+
/^(import\s.*\n)+/m,
|
|
1843
|
+
(match) => `${match}import { Providers } from './providers';
|
|
1844
|
+
`
|
|
1845
|
+
);
|
|
1846
|
+
content = content.replace(
|
|
1847
|
+
/\{children\}/,
|
|
1848
|
+
"<Providers>{children}</Providers>"
|
|
1849
|
+
);
|
|
1850
|
+
await writeFile(layoutPath, content, "utf-8");
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async function wrapReactEntryQueryProvider(targetDir, framework) {
|
|
1856
|
+
const mainPath = join(targetDir, "src/main.tsx");
|
|
1857
|
+
try {
|
|
1858
|
+
let content = await readFile(mainPath, "utf-8");
|
|
1859
|
+
if (content.includes("QueryClientProvider")) return;
|
|
1860
|
+
content = content.replace(
|
|
1861
|
+
/^(import\s.*\n)+/m,
|
|
1862
|
+
(match) => `${match}import { QueryClientProvider } from '@tanstack/react-query';
|
|
1863
|
+
import { queryClient } from './lib/query-client';
|
|
1864
|
+
`
|
|
1865
|
+
);
|
|
1866
|
+
if (framework === "electron") {
|
|
1867
|
+
content = content.replace(
|
|
1868
|
+
/(<BrowserRouter>)/,
|
|
1869
|
+
"<QueryClientProvider client={queryClient}>\n $1"
|
|
1870
|
+
);
|
|
1871
|
+
content = content.replace(
|
|
1872
|
+
/(<\/BrowserRouter>)/,
|
|
1873
|
+
"$1\n </QueryClientProvider>"
|
|
1874
|
+
);
|
|
1875
|
+
} else {
|
|
1876
|
+
content = content.replace(
|
|
1877
|
+
/(<App\s*\/>)/,
|
|
1878
|
+
"<QueryClientProvider client={queryClient}>\n $1\n </QueryClientProvider>"
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
await writeFile(mainPath, content, "utf-8");
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
async function wrapVueQueryPlugin(targetDir) {
|
|
1886
|
+
const mainPath = join(targetDir, "src/main.ts");
|
|
1887
|
+
try {
|
|
1888
|
+
let content = await readFile(mainPath, "utf-8");
|
|
1889
|
+
if (content.includes("VueQueryPlugin")) return;
|
|
1890
|
+
content = content.replace(
|
|
1891
|
+
/^(import\s.*\n)+/m,
|
|
1892
|
+
(match) => `${match}import { VueQueryPlugin } from '@tanstack/vue-query';
|
|
1893
|
+
import { vueQueryOptions } from './lib/query-client';
|
|
1894
|
+
`
|
|
1895
|
+
);
|
|
1896
|
+
content = content.replace(
|
|
1897
|
+
/app\.use\(router\)/,
|
|
1898
|
+
"app.use(VueQueryPlugin, vueQueryOptions);\napp.use(router)"
|
|
1899
|
+
);
|
|
1900
|
+
await writeFile(mainPath, content, "utf-8");
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/commands/create.ts
|
|
1906
|
+
var execAsync2 = promisify(exec);
|
|
1907
|
+
function validateConflictingFlags(options) {
|
|
1908
|
+
const frameworkFlags = [
|
|
1909
|
+
options.nextjs,
|
|
1910
|
+
options.react,
|
|
1911
|
+
options.vue,
|
|
1912
|
+
options.electron
|
|
1913
|
+
].filter(Boolean);
|
|
1914
|
+
if (frameworkFlags.length > 1) {
|
|
1915
|
+
throw new ConflictingFlagsError("--nextjs, --react, --vue, --electron", "(multiple frameworks)");
|
|
1916
|
+
}
|
|
1917
|
+
const uiLibraryFlags = [
|
|
1918
|
+
options.shadcn,
|
|
1919
|
+
options.diceui,
|
|
1920
|
+
options.shadcnVue,
|
|
1921
|
+
options.heroui
|
|
1922
|
+
].filter(Boolean);
|
|
1923
|
+
if (uiLibraryFlags.length > 1) {
|
|
1924
|
+
throw new ConflictingFlagsError(
|
|
1925
|
+
"--shadcn, --diceui, --shadcn-vue, --heroui",
|
|
1926
|
+
MESSAGES.UI_MULTIPLE_LIBRARIES
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
const stateFlags = [
|
|
1930
|
+
options.zustand,
|
|
1931
|
+
options.tanstackQuery,
|
|
1932
|
+
options.tanstackVueQuery
|
|
1933
|
+
].filter(Boolean);
|
|
1934
|
+
if (stateFlags.length > 1) {
|
|
1935
|
+
throw new ConflictingFlagsError("--zustand, --tanstack-query, --tanstack-vue-query", "(multiple state management options)");
|
|
1936
|
+
}
|
|
1937
|
+
const linterFlags = [
|
|
1938
|
+
options.eslint,
|
|
1939
|
+
options.biome,
|
|
1940
|
+
options.noLinter
|
|
1941
|
+
].filter(Boolean);
|
|
1942
|
+
if (linterFlags.length > 1) {
|
|
1943
|
+
throw new ConflictingFlagsError("--eslint, --biome, --no-linter", "(multiple linter options)");
|
|
1944
|
+
}
|
|
1945
|
+
const pmFlags = [
|
|
1946
|
+
options.pnpm,
|
|
1947
|
+
options.npm,
|
|
1948
|
+
options.yarn,
|
|
1949
|
+
options.bun
|
|
1950
|
+
].filter(Boolean);
|
|
1951
|
+
if (pmFlags.length > 1) {
|
|
1952
|
+
throw new ConflictingFlagsError("--pnpm, --npm, --yarn, --bun", "(multiple package managers)");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function validateFrameworkUILibrary(framework, uiLibrary) {
|
|
1956
|
+
if (uiLibrary === "none") return;
|
|
1957
|
+
if (!isUILibraryCompatible(framework, uiLibrary)) {
|
|
1958
|
+
throw new ConflictingFlagsError(
|
|
1959
|
+
`--${framework}`,
|
|
1960
|
+
getUILibraryErrorMessage(framework, uiLibrary)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function getFrameworkFromFlags(options) {
|
|
1965
|
+
if (options.nextjs) return "nextjs";
|
|
1966
|
+
if (options.react) return "react";
|
|
1967
|
+
if (options.vue) return "vue";
|
|
1968
|
+
if (options.electron) return "electron";
|
|
1969
|
+
return void 0;
|
|
1970
|
+
}
|
|
1971
|
+
function getUiLibraryFromFlags(options) {
|
|
1972
|
+
if (options.shadcn) return "shadcn";
|
|
1973
|
+
if (options.diceui) return "diceui";
|
|
1974
|
+
if (options.shadcnVue) return "shadcn-vue";
|
|
1975
|
+
if (options.heroui) return "heroui";
|
|
1976
|
+
return void 0;
|
|
1977
|
+
}
|
|
1978
|
+
function getLinterFromFlags(options) {
|
|
1979
|
+
if (options.eslint) return "eslint";
|
|
1980
|
+
if (options.biome) return "biome";
|
|
1981
|
+
if (options.noLinter) return "none";
|
|
1982
|
+
return void 0;
|
|
1983
|
+
}
|
|
1984
|
+
function getPackageManagerFromFlags(options) {
|
|
1985
|
+
if (options.pnpm) return "pnpm";
|
|
1986
|
+
if (options.npm) return "npm";
|
|
1987
|
+
if (options.yarn) return "yarn";
|
|
1988
|
+
if (options.bun) return "bun";
|
|
1989
|
+
return void 0;
|
|
1990
|
+
}
|
|
1991
|
+
function getStateManagementFromFlags(options) {
|
|
1992
|
+
if (options.zustand) return "zustand";
|
|
1993
|
+
if (options.tanstackQuery) return "tanstack-query";
|
|
1994
|
+
if (options.tanstackVueQuery) return "tanstack-vue-query";
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
var FRAMEWORKS = [
|
|
1998
|
+
{ name: "nextjs", label: "Next.js (App Router)", description: "Next.js 16+, TypeScript, Tailwind v4" },
|
|
1999
|
+
{ name: "react", label: "React + Vite", description: "React 19+, Vite, TypeScript, Tailwind v4" },
|
|
2000
|
+
{ name: "vue", label: "Vue + Vite", description: "Vue 3.5+, Vite, TypeScript, Tailwind v4" },
|
|
2001
|
+
{ name: "electron", label: "Electron + React", description: "Desktop app, React 19+, Vite, Tailwind v4" }
|
|
2002
|
+
];
|
|
2003
|
+
var UI_LIBRARIES_NEXTJS = [
|
|
2004
|
+
{ name: "shadcn", label: "shadcn/ui (Recommended)", description: "Beautifully designed components from shadcn" },
|
|
2005
|
+
{ name: "diceui", label: "DiceUI", description: "shadcn/ui + 5 premium components" },
|
|
2006
|
+
{ name: "heroui", label: "HeroUI", description: "Modern React UI library" },
|
|
2007
|
+
{ name: "none", label: "None (Plain Tailwind)", description: "Just Tailwind CSS, no component library" }
|
|
2008
|
+
];
|
|
2009
|
+
var UI_LIBRARIES_REACT = [
|
|
2010
|
+
{ name: "shadcn", label: "shadcn/ui (Recommended)", description: "Beautifully designed components from shadcn" },
|
|
2011
|
+
{ name: "diceui", label: "DiceUI", description: "shadcn/ui + 5 premium components" },
|
|
2012
|
+
{ name: "heroui", label: "HeroUI", description: "Modern React UI library" },
|
|
2013
|
+
{ name: "none", label: "None (Plain Tailwind)", description: "Just Tailwind CSS, no component library" }
|
|
2014
|
+
];
|
|
2015
|
+
var UI_LIBRARIES_VUE = [{ name: "shadcn-vue", label: "shadcn-vue (Recommended)", description: "Vue port of shadcn/ui" }, { name: "none", label: "None (Plain Tailwind)", description: "Just Tailwind CSS, no component library" }];
|
|
2016
|
+
var UI_LIBRARIES_ELECTRON = [
|
|
2017
|
+
{ name: "shadcn", label: "shadcn/ui", description: "Beautifully designed components from shadcn" },
|
|
2018
|
+
{ name: "diceui", label: "DiceUI", description: "shadcn/ui + 5 premium components" },
|
|
2019
|
+
{ name: "none", label: "None (Plain Tailwind)", description: "Just Tailwind CSS, no component library" }
|
|
2020
|
+
];
|
|
2021
|
+
function getUiLibrariesForFramework(framework) {
|
|
2022
|
+
switch (framework) {
|
|
2023
|
+
case "nextjs":
|
|
2024
|
+
return UI_LIBRARIES_NEXTJS;
|
|
2025
|
+
case "react":
|
|
2026
|
+
return UI_LIBRARIES_REACT;
|
|
2027
|
+
case "vue":
|
|
2028
|
+
return UI_LIBRARIES_VUE;
|
|
2029
|
+
case "electron":
|
|
2030
|
+
return UI_LIBRARIES_ELECTRON;
|
|
2031
|
+
default:
|
|
2032
|
+
return UI_LIBRARIES_REACT;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
var STATE_MGMT_REACT = [
|
|
2036
|
+
{ name: "zustand", label: "Zustand (Recommended)", description: "Lightweight client state" },
|
|
2037
|
+
{ name: "tanstack-query", label: "TanStack Query", description: "Server state & data fetching" },
|
|
2038
|
+
{ name: "none", label: "None", description: "No state management" }
|
|
2039
|
+
];
|
|
2040
|
+
var STATE_MGMT_VUE = [{ name: "tanstack-vue-query", label: "TanStack Vue Query (Recommended)", description: "Server state & data fetching" }, { name: "none", label: "None", description: "No state management" }];
|
|
2041
|
+
function getStateManagementForFramework(framework) {
|
|
2042
|
+
switch (framework) {
|
|
2043
|
+
case "vue":
|
|
2044
|
+
return STATE_MGMT_VUE;
|
|
2045
|
+
default:
|
|
2046
|
+
return STATE_MGMT_REACT;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
var SHADCN_STYLE_CHOICES = [
|
|
2050
|
+
{ name: "vega", label: "Vega (Recommended)", description: "Classic shadcn/ui design" },
|
|
2051
|
+
{ name: "nova", label: "Nova", description: "Compact layouts" },
|
|
2052
|
+
{ name: "maia", label: "Maia", description: "Soft and rounded" },
|
|
2053
|
+
{ name: "lyra", label: "Lyra", description: "Boxy and sharp" },
|
|
2054
|
+
{ name: "mira", label: "Mira", description: "Dense interfaces" }
|
|
2055
|
+
];
|
|
2056
|
+
var SHADCN_BASE_COLOR_CHOICES = [
|
|
2057
|
+
{ name: "zinc", label: "Zinc (Default)", description: "Cool neutral gray" },
|
|
2058
|
+
{ name: "slate", label: "Slate", description: "Blue-tinted gray" },
|
|
2059
|
+
{ name: "neutral", label: "Neutral", description: "Pure gray" },
|
|
2060
|
+
{ name: "stone", label: "Stone", description: "Warm gray" },
|
|
2061
|
+
{ name: "gray", label: "Gray", description: "Balanced gray" }
|
|
2062
|
+
];
|
|
2063
|
+
var LINTERS = [
|
|
2064
|
+
{ name: "eslint", label: "ESLint (Recommended)", description: "Industry standard, widely supported" },
|
|
2065
|
+
{ name: "biome", label: "Biome", description: "Fast, all-in-one linter and formatter" },
|
|
2066
|
+
{ name: "none", label: "None", description: "No linter, configure your own later" }
|
|
2067
|
+
];
|
|
2068
|
+
var PACKAGE_MANAGERS = [
|
|
2069
|
+
{ name: "pnpm", label: "pnpm (Recommended)", description: "Fast, disk space efficient" },
|
|
2070
|
+
{ name: "npm", label: "npm", description: "Node.js default package manager" },
|
|
2071
|
+
{ name: "yarn", label: "yarn", description: "Fast, reliable, and secure" },
|
|
2072
|
+
{ name: "bun", label: "bun", description: "All-in-one JavaScript runtime" }
|
|
2073
|
+
];
|
|
2074
|
+
var TEMPLATE_REPO_BASE = "Docyrus/docyrus-devkit/packages/cli-templates";
|
|
2075
|
+
function getTemplateRepo(framework) {
|
|
2076
|
+
return `${TEMPLATE_REPO_BASE}/${framework}`;
|
|
2077
|
+
}
|
|
2078
|
+
async function directoryExists(path) {
|
|
2079
|
+
try {
|
|
2080
|
+
await access(path, constants.F_OK);
|
|
2081
|
+
return true;
|
|
2082
|
+
} catch {
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function registerCreateCommand(program2) {
|
|
2087
|
+
program2.command("create [name]").description("Create a new Docyrus project from template").option("-p, --path <path>", "Target directory path").option("--nextjs", "Use Next.js framework").option("--react", "Use React + Vite framework").option("--vue", "Use Vue + Vite framework").option("--electron", "Use Electron + React framework (desktop app)").option("--shadcn", "Use shadcn/ui (Next.js, React, Electron)").option("--diceui", "Use DiceUI (Next.js, React, Electron)").option("--shadcn-vue", "Use shadcn-vue (Vue)").option("--heroui", "Use HeroUI (Next.js, React)").option("--zustand", "Use Zustand for state management (React, Next.js, Electron)").option("--tanstack-query", "Use TanStack Query for state management (React, Next.js, Electron)").option("--tanstack-vue-query", "Use TanStack Vue Query for state management (Vue)").option("--base-color <color>", "Base color theme for shadcn (zinc, slate, neutral, stone, gray)").option("--eslint", "Use ESLint for linting").option("--biome", "Use Biome for linting").option("--no-linter", "Skip linter configuration").option("--alias <prefix>", "Custom import alias prefix (must start with @)").option("--pnpm", "Use pnpm package manager").option("--npm", "Use npm package manager").option("--yarn", "Use yarn package manager").option("--bun", "Use bun package manager").option("--local", "Use local templates instead of downloading from GitHub (development only)").addHelpText("after", `
|
|
2088
|
+
UI Library Compatibility:
|
|
2089
|
+
Next.js \u2192 shadcn, diceui, heroui
|
|
2090
|
+
React \u2192 shadcn, diceui, heroui
|
|
2091
|
+
Vue \u2192 shadcn-vue
|
|
2092
|
+
Electron \u2192 shadcn, diceui
|
|
2093
|
+
|
|
2094
|
+
State Management:
|
|
2095
|
+
React / Next.js / Electron \u2192 zustand, tanstack-query
|
|
2096
|
+
Vue \u2192 tanstack-vue-query
|
|
2097
|
+
|
|
2098
|
+
Examples:
|
|
2099
|
+
$ docyrus create my-app Interactive mode
|
|
2100
|
+
$ docyrus create my-app --nextjs Next.js + plain Tailwind
|
|
2101
|
+
$ docyrus create my-app --nextjs --shadcn Next.js + shadcn/ui
|
|
2102
|
+
$ docyrus create my-app --react --shadcn --zustand React + shadcn + Zustand
|
|
2103
|
+
$ docyrus create my-app --react --tanstack-query React + TanStack Query
|
|
2104
|
+
$ docyrus create my-app --vue --shadcn-vue Vue + shadcn-vue
|
|
2105
|
+
$ docyrus create my-app --vue --tanstack-vue-query Vue + TanStack Vue Query
|
|
2106
|
+
$ docyrus create my-app --electron --shadcn --zustand Electron + shadcn + Zustand
|
|
2107
|
+
`).action(async (name, options) => {
|
|
2108
|
+
const accessToken = await requireAuth();
|
|
2109
|
+
if (options) {
|
|
2110
|
+
validateConflictingFlags(options);
|
|
2111
|
+
}
|
|
2112
|
+
const frameworkFromFlags = options ? getFrameworkFromFlags(options) : void 0;
|
|
2113
|
+
const uiLibraryFromFlags = options ? getUiLibraryFromFlags(options) : void 0;
|
|
2114
|
+
const linterFromFlags = options ? getLinterFromFlags(options) : void 0;
|
|
2115
|
+
const packageManagerFromFlags = options ? getPackageManagerFromFlags(options) : void 0;
|
|
2116
|
+
const aliasFromFlags = options?.alias;
|
|
2117
|
+
const framework = frameworkFromFlags ?? await select({
|
|
2118
|
+
message: MESSAGES.CREATE_SELECT_FRAMEWORK,
|
|
2119
|
+
choices: FRAMEWORKS.map((f) => ({
|
|
2120
|
+
name: f.label,
|
|
2121
|
+
value: f.name
|
|
2122
|
+
}))
|
|
2123
|
+
});
|
|
2124
|
+
let uiLibrary;
|
|
2125
|
+
const availableUiLibraries = getUiLibrariesForFramework(framework);
|
|
2126
|
+
if (uiLibraryFromFlags) {
|
|
2127
|
+
validateFrameworkUILibrary(framework, uiLibraryFromFlags);
|
|
2128
|
+
uiLibrary = uiLibraryFromFlags;
|
|
2129
|
+
} else if (frameworkFromFlags) {
|
|
2130
|
+
uiLibrary = "none";
|
|
2131
|
+
} else {
|
|
2132
|
+
uiLibrary = await select({
|
|
2133
|
+
message: MESSAGES.CREATE_SELECT_UI_LIBRARY,
|
|
2134
|
+
choices: availableUiLibraries.map((u) => ({
|
|
2135
|
+
name: u.label,
|
|
2136
|
+
value: u.name
|
|
2137
|
+
}))
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
let shadcnStyle = "vega";
|
|
2141
|
+
let baseColor = "zinc";
|
|
2142
|
+
if (uiLibrary === "shadcn" || uiLibrary === "diceui") {
|
|
2143
|
+
if (!frameworkFromFlags) {
|
|
2144
|
+
shadcnStyle = await select({
|
|
2145
|
+
message: "Select component style:",
|
|
2146
|
+
choices: SHADCN_STYLE_CHOICES.map((c) => ({
|
|
2147
|
+
name: c.label,
|
|
2148
|
+
value: c.name,
|
|
2149
|
+
description: c.description
|
|
2150
|
+
}))
|
|
2151
|
+
});
|
|
2152
|
+
baseColor = await select({
|
|
2153
|
+
message: "Select base color:",
|
|
2154
|
+
choices: SHADCN_BASE_COLOR_CHOICES.map((c) => ({
|
|
2155
|
+
name: c.label,
|
|
2156
|
+
value: c.name,
|
|
2157
|
+
description: c.description
|
|
2158
|
+
}))
|
|
2159
|
+
});
|
|
2160
|
+
} else {
|
|
2161
|
+
const validColors = SHADCN_BASE_COLORS;
|
|
2162
|
+
if (options?.baseColor) {
|
|
2163
|
+
if (!validColors.includes(options.baseColor)) {
|
|
2164
|
+
throw new ConflictingFlagsError("--base-color", `must be one of: ${validColors.join(", ")}`);
|
|
2165
|
+
}
|
|
2166
|
+
baseColor = options.baseColor;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
const stateManagementFromFlags = options ? getStateManagementFromFlags(options) : void 0;
|
|
2171
|
+
const stateMgmtChoices = getStateManagementForFramework(framework);
|
|
2172
|
+
let stateManagement = "none";
|
|
2173
|
+
if (stateMgmtChoices) {
|
|
2174
|
+
if (stateManagementFromFlags) {
|
|
2175
|
+
const compatible = getCompatibleStateManagement(framework);
|
|
2176
|
+
if (!compatible.includes(stateManagementFromFlags)) {
|
|
2177
|
+
throw new ConflictingFlagsError(
|
|
2178
|
+
`--${stateManagementFromFlags}`,
|
|
2179
|
+
`not compatible with ${framework}`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
stateManagement = stateManagementFromFlags;
|
|
2183
|
+
} else if (!frameworkFromFlags) {
|
|
2184
|
+
stateManagement = await select({
|
|
2185
|
+
message: MESSAGES.CREATE_SELECT_STATE_MANAGEMENT,
|
|
2186
|
+
choices: stateMgmtChoices.map((s) => ({
|
|
2187
|
+
name: s.label,
|
|
2188
|
+
value: s.name,
|
|
2189
|
+
description: s.description
|
|
2190
|
+
}))
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
const linter = linterFromFlags ?? await select({
|
|
2195
|
+
message: MESSAGES.CREATE_SELECT_LINTER,
|
|
2196
|
+
choices: LINTERS.map((l) => ({
|
|
2197
|
+
name: l.label,
|
|
2198
|
+
value: l.name
|
|
2199
|
+
}))
|
|
2200
|
+
});
|
|
2201
|
+
let projectName = name;
|
|
2202
|
+
if (!projectName) {
|
|
2203
|
+
projectName = await input({
|
|
2204
|
+
message: MESSAGES.CREATE_PROJECT_NAME,
|
|
2205
|
+
default: `my-${framework}-app`,
|
|
2206
|
+
validate: (value) => {
|
|
2207
|
+
if (!value.trim()) return "Project name is required";
|
|
2208
|
+
if (!/^[a-z0-9-]+$/.test(value)) return "Only lowercase letters, numbers and hyphens allowed";
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
let aliasPrefix = "@";
|
|
2214
|
+
if (aliasFromFlags) {
|
|
2215
|
+
if (!aliasFromFlags.startsWith("@")) {
|
|
2216
|
+
throw new ConflictingFlagsError("--alias", "value must start with @");
|
|
2217
|
+
}
|
|
2218
|
+
if (!/^@[a-zA-Z0-9]*$/.test(aliasFromFlags)) {
|
|
2219
|
+
throw new ConflictingFlagsError("--alias", "only @, @app, @src format allowed");
|
|
2220
|
+
}
|
|
2221
|
+
aliasPrefix = aliasFromFlags;
|
|
2222
|
+
} else {
|
|
2223
|
+
const customizeAlias = await select({
|
|
2224
|
+
message: MESSAGES.CREATE_CUSTOMIZE_ALIAS,
|
|
2225
|
+
choices: [{ name: "No, use @/ (Recommended)", value: false }, { name: "Yes, customize", value: true }]
|
|
2226
|
+
});
|
|
2227
|
+
if (customizeAlias) {
|
|
2228
|
+
aliasPrefix = await input({
|
|
2229
|
+
message: MESSAGES.CREATE_CUSTOM_ALIAS,
|
|
2230
|
+
default: "@",
|
|
2231
|
+
validate: (value) => {
|
|
2232
|
+
if (!value.trim()) return "Alias prefix is required";
|
|
2233
|
+
if (!value.startsWith("@")) return "Alias must start with @";
|
|
2234
|
+
if (!/^@[a-zA-Z0-9]*$/.test(value)) return "Only @, @app, @src format allowed";
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const packageManager = packageManagerFromFlags ?? await select({
|
|
2241
|
+
message: MESSAGES.CREATE_SELECT_PACKAGE_MANAGER,
|
|
2242
|
+
choices: PACKAGE_MANAGERS.map((pm) => ({
|
|
2243
|
+
name: pm.label,
|
|
2244
|
+
value: pm.name
|
|
2245
|
+
}))
|
|
2246
|
+
});
|
|
2247
|
+
const targetDir = options?.path ? join(options.path, projectName) : projectName;
|
|
2248
|
+
if (await directoryExists(targetDir)) {
|
|
2249
|
+
throw new ProjectExistsError(projectName);
|
|
2250
|
+
}
|
|
2251
|
+
const isLocalMode = options?.local === true;
|
|
2252
|
+
const templateRepo = getTemplateRepo(framework);
|
|
2253
|
+
logger.newline();
|
|
2254
|
+
logger.log(MESSAGES.CREATE_SETTING_UP);
|
|
2255
|
+
logger.newline();
|
|
2256
|
+
if (isLocalMode) {
|
|
2257
|
+
await withSpinner(
|
|
2258
|
+
MESSAGES.CREATE_COPYING_LOCAL,
|
|
2259
|
+
async () => {
|
|
2260
|
+
const cliPackageDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
2261
|
+
const localTemplateDir = resolve(cliPackageDir, "..", "cli-templates", framework);
|
|
2262
|
+
try {
|
|
2263
|
+
await access(localTemplateDir, constants.F_OK);
|
|
2264
|
+
} catch {
|
|
2265
|
+
throw new TemplateError(
|
|
2266
|
+
`Local template not found at: ${localTemplateDir}
|
|
2267
|
+
--local requires running from the monorepo development environment.`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
await cp(localTemplateDir, targetDir, { recursive: true });
|
|
2271
|
+
const pkgJsonPath = join(targetDir, "package.json");
|
|
2272
|
+
const pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
2273
|
+
const packagesDir = resolve(cliPackageDir, "..");
|
|
2274
|
+
for (const depType of ["dependencies", "devDependencies"]) {
|
|
2275
|
+
const deps = pkgJson[depType];
|
|
2276
|
+
if (!deps) continue;
|
|
2277
|
+
for (const [name2, version] of Object.entries(deps)) {
|
|
2278
|
+
if (name2.startsWith("@docyrus/") && version === "latest") {
|
|
2279
|
+
const pkgName = name2.replace("@docyrus/", "");
|
|
2280
|
+
const localPkgDir = resolve(packagesDir, pkgName);
|
|
2281
|
+
try {
|
|
2282
|
+
await access(localPkgDir, constants.F_OK);
|
|
2283
|
+
deps[name2] = `file:${localPkgDir}`;
|
|
2284
|
+
} catch {
|
|
2285
|
+
logger.warn(`Local package not found for ${name2}, keeping "latest"`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
await writeFile(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}
|
|
2291
|
+
`);
|
|
2292
|
+
}
|
|
2293
|
+
);
|
|
2294
|
+
} else {
|
|
2295
|
+
const tokenManager = getTokenManager();
|
|
2296
|
+
let githubToken = await tokenManager.getGithubToken();
|
|
2297
|
+
if (!githubToken) {
|
|
2298
|
+
logger.log(MESSAGES.DOCYRUS_TOKEN_REQUIRED);
|
|
2299
|
+
githubToken = await password({
|
|
2300
|
+
message: MESSAGES.DOCYRUS_TOKEN_PROMPT,
|
|
2301
|
+
mask: "*"
|
|
2302
|
+
});
|
|
2303
|
+
await tokenManager.setGithubToken(githubToken);
|
|
2304
|
+
logger.success(MESSAGES.DOCYRUS_TOKEN_SAVED);
|
|
2305
|
+
}
|
|
2306
|
+
await withSpinner(
|
|
2307
|
+
MESSAGES.CREATE_DOWNLOADING,
|
|
2308
|
+
async () => {
|
|
2309
|
+
const gigetCachePath = resolve(homedir(), ".cache/giget/gh/Docyrus-docyrus-devkit");
|
|
2310
|
+
if (existsSync(gigetCachePath)) {
|
|
2311
|
+
await rm(gigetCachePath, { recursive: true, force: true });
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
await downloadTemplate(`gh:${templateRepo}`, {
|
|
2315
|
+
dir: targetDir,
|
|
2316
|
+
install: false,
|
|
2317
|
+
auth: githubToken
|
|
2318
|
+
});
|
|
2319
|
+
} catch (error) {
|
|
2320
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2321
|
+
if (errorMessage.includes("401") || errorMessage.includes("403")) {
|
|
2322
|
+
await tokenManager.setGithubToken("");
|
|
2323
|
+
throw new TemplateError(
|
|
2324
|
+
`${MESSAGES.DOCYRUS_TOKEN_INVALID}
|
|
2325
|
+
${errorMessage}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
if (errorMessage.includes("404")) {
|
|
2329
|
+
throw new TemplateError(
|
|
2330
|
+
`Template repository not found or token lacks access.
|
|
2331
|
+
Ensure your Docyrus token has correct permissions.
|
|
2332
|
+
${errorMessage}`
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
throw new TemplateError(`Failed to download template: ${errorMessage}`);
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
await access(join(targetDir, "package.json"), constants.F_OK);
|
|
2339
|
+
} catch {
|
|
2340
|
+
throw new TemplateError(
|
|
2341
|
+
"Template download failed \u2014 no files were downloaded.\nThis usually means your Docyrus token is expired or invalid.\nRun: docyrus config --token"
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
if (aliasPrefix !== "@" && aliasPrefix !== "@/") {
|
|
2348
|
+
await withSpinner(
|
|
2349
|
+
MESSAGES.CREATE_CONFIGURING_ALIAS(aliasPrefix),
|
|
2350
|
+
async () => {
|
|
2351
|
+
await applyAliasConfig(targetDir, framework, aliasPrefix);
|
|
2352
|
+
}
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
if (linter !== "none") {
|
|
2356
|
+
logger.newline();
|
|
2357
|
+
logger.log(MESSAGES.LINTER_SETUP_TITLE);
|
|
2358
|
+
const linterProgress = createSimpleProgress();
|
|
2359
|
+
try {
|
|
2360
|
+
await applyLinterConfig(targetDir, framework, linter, linterProgress);
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
throw new TemplateError(
|
|
2363
|
+
`Failed to configure linter: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (stateManagement !== "none") {
|
|
2368
|
+
logger.newline();
|
|
2369
|
+
logger.log(MESSAGES.STATE_MANAGEMENT_SETUP_TITLE);
|
|
2370
|
+
const stateProgress = createSimpleProgress();
|
|
2371
|
+
try {
|
|
2372
|
+
await setupStateManagement(
|
|
2373
|
+
targetDir,
|
|
2374
|
+
framework,
|
|
2375
|
+
stateManagement,
|
|
2376
|
+
stateProgress
|
|
2377
|
+
);
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
throw new TemplateError(
|
|
2380
|
+
`Failed to setup state management: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
await withSpinner(
|
|
2385
|
+
MESSAGES.CREATE_INSTALLING,
|
|
2386
|
+
async () => {
|
|
2387
|
+
try {
|
|
2388
|
+
await execAsync2(`${packageManager} install`, { cwd: targetDir });
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
throw new TemplateError(
|
|
2391
|
+
`Failed to install dependencies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
);
|
|
2396
|
+
if (uiLibrary !== "none") {
|
|
2397
|
+
logger.newline();
|
|
2398
|
+
logger.log(MESSAGES.UI_SETUP_TITLE);
|
|
2399
|
+
const progressCallback = createSimpleProgress();
|
|
2400
|
+
try {
|
|
2401
|
+
await setupUILibrary(
|
|
2402
|
+
targetDir,
|
|
2403
|
+
framework,
|
|
2404
|
+
uiLibrary,
|
|
2405
|
+
packageManager,
|
|
2406
|
+
progressCallback,
|
|
2407
|
+
shadcnStyle,
|
|
2408
|
+
baseColor
|
|
2409
|
+
);
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
throw new TemplateError(
|
|
2412
|
+
`Failed to setup UI library: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
await withSpinner(
|
|
2417
|
+
"Applying page variants...",
|
|
2418
|
+
async () => applyUIVariants(targetDir, uiLibrary)
|
|
2419
|
+
);
|
|
2420
|
+
logger.newline();
|
|
2421
|
+
logger.log(MESSAGES.OPENAPI_SETUP_TITLE);
|
|
2422
|
+
try {
|
|
2423
|
+
const savedPath = await withSpinner(
|
|
2424
|
+
"Downloading OpenAPI spec...",
|
|
2425
|
+
async () => downloadOpenApiSpec(accessToken, targetDir, "openapi-spec.json"),
|
|
2426
|
+
{ successText: "OpenAPI spec downloaded" }
|
|
2427
|
+
);
|
|
2428
|
+
logger.dim(MESSAGES.CREATE_OPENAPI_SUCCESS(savedPath));
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
logger.warn(`Failed to download OpenAPI spec: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2431
|
+
}
|
|
2432
|
+
logger.newline();
|
|
2433
|
+
logger.success(MESSAGES.CREATE_SUCCESS);
|
|
2434
|
+
logger.newline();
|
|
2435
|
+
logger.log(` cd ${projectName}`);
|
|
2436
|
+
logger.log(` ${packageManager} run dev`);
|
|
2437
|
+
logger.newline();
|
|
2438
|
+
logger.dim(MESSAGES.API_CLIENT_DOCS);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
function registerGenerateCommand(program2) {
|
|
2442
|
+
const generate = program2.command("generate").description("Code generation commands");
|
|
2443
|
+
generate.command("api-spec").description("Download OpenAPI specification from Docyrus API").option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
|
|
2444
|
+
const token = await requireAuth();
|
|
2445
|
+
const outputPath = resolve(options.output);
|
|
2446
|
+
if (!output.isJson()) {
|
|
2447
|
+
logger.info("Downloading OpenAPI specification...");
|
|
2448
|
+
}
|
|
2449
|
+
try {
|
|
2450
|
+
const savedPath = await withSpinner(
|
|
2451
|
+
"Fetching OpenAPI spec from API...",
|
|
2452
|
+
async () => downloadOpenApiSpec(token, dirname(outputPath), outputPath.split("/").pop()),
|
|
2453
|
+
{ silent: output.isJson() }
|
|
2454
|
+
);
|
|
2455
|
+
output.success("OpenAPI specification downloaded!", { path: savedPath });
|
|
2456
|
+
if (!output.isJson()) {
|
|
2457
|
+
logger.dim(`Saved to: ${savedPath}`);
|
|
2458
|
+
}
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2461
|
+
output.error(`Failed to download OpenAPI spec: ${message}`);
|
|
2462
|
+
process.exit(1);
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
generate.command("db").description("Generate TanStack Query collections from OpenAPI spec").argument("[spec]", "Path to OpenAPI specification file (JSON)").option("-o, --output <dir>", "Output directory (defaults to src in same folder as spec)").option("-w, --watch", "Watch for changes in the spec file").action(async (specPath, options) => {
|
|
2466
|
+
const resolvedPath = findSpecFile(specPath);
|
|
2467
|
+
if (!resolvedPath) {
|
|
2468
|
+
output.error("OpenAPI spec file not found.");
|
|
2469
|
+
if (!output.isJson()) {
|
|
2470
|
+
logger.dim("Provide a path: docyrus generate db <spec-file>");
|
|
2471
|
+
logger.dim("Or create one of: openapi.json, api.json, spec.json");
|
|
2472
|
+
}
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
if (!existsSync(resolvedPath)) {
|
|
2476
|
+
output.error(`OpenAPI spec file not found: ${resolvedPath}`);
|
|
2477
|
+
process.exit(1);
|
|
2478
|
+
}
|
|
2479
|
+
const outputDir = options.output || join(dirname(resolvedPath), "src");
|
|
2480
|
+
if (!output.isJson()) {
|
|
2481
|
+
logger.info("TanStack DB Generator");
|
|
2482
|
+
logger.dim(` Spec: ${resolvedPath}`);
|
|
2483
|
+
logger.dim(` Output: ${outputDir}`);
|
|
2484
|
+
logger.newline();
|
|
2485
|
+
}
|
|
2486
|
+
try {
|
|
2487
|
+
const specContent = readFileSync(resolvedPath, "utf-8");
|
|
2488
|
+
const spec = JSON.parse(specContent);
|
|
2489
|
+
await withSpinner(
|
|
2490
|
+
"Generating TanStack Query collections...",
|
|
2491
|
+
async () => {
|
|
2492
|
+
await generateFromOpenAPI(spec, outputDir);
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
silent: output.isJson(),
|
|
2496
|
+
successText: "Generation completed!"
|
|
2497
|
+
}
|
|
2498
|
+
);
|
|
2499
|
+
output.success("TanStack Query collections generated successfully!", {
|
|
2500
|
+
spec: resolvedPath,
|
|
2501
|
+
output: outputDir
|
|
2502
|
+
});
|
|
2503
|
+
if (options.watch) {
|
|
2504
|
+
if (!output.isJson()) {
|
|
2505
|
+
logger.info("Watch mode is not yet implemented.");
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2510
|
+
output.error(`Generation failed: ${message}`);
|
|
2511
|
+
process.exit(1);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
function findSpecFile(specPath) {
|
|
2516
|
+
if (specPath) {
|
|
2517
|
+
return resolve(specPath);
|
|
2518
|
+
}
|
|
2519
|
+
const defaultSpec = resolve("openapi.json");
|
|
2520
|
+
if (existsSync(defaultSpec)) {
|
|
2521
|
+
return defaultSpec;
|
|
2522
|
+
}
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
var execAsync3 = promisify(exec);
|
|
2526
|
+
async function getLatestVersion() {
|
|
2527
|
+
try {
|
|
2528
|
+
const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}`);
|
|
2529
|
+
if (!response.ok) return null;
|
|
2530
|
+
const data = await response.json();
|
|
2531
|
+
return data["dist-tags"]?.latest || null;
|
|
2532
|
+
} catch {
|
|
2533
|
+
return null;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
function isNewerVersion(current, latest) {
|
|
2537
|
+
const currentParts = current.replace(/^v/, "").split(".").map(Number);
|
|
2538
|
+
const latestParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
2539
|
+
for (let i = 0; i < 3; i++) {
|
|
2540
|
+
const curr = currentParts[i] || 0;
|
|
2541
|
+
const lat = latestParts[i] || 0;
|
|
2542
|
+
if (lat > curr) return true;
|
|
2543
|
+
if (lat < curr) return false;
|
|
2544
|
+
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
async function detectPackageManager() {
|
|
2548
|
+
const npmExecpath = process.env.npm_execpath || "";
|
|
2549
|
+
if (npmExecpath.includes("pnpm")) return "pnpm";
|
|
2550
|
+
if (npmExecpath.includes("yarn")) return "yarn";
|
|
2551
|
+
if (npmExecpath.includes("bun")) return "bun";
|
|
2552
|
+
try {
|
|
2553
|
+
await execAsync3("pnpm --version");
|
|
2554
|
+
return "pnpm";
|
|
2555
|
+
} catch {
|
|
2556
|
+
}
|
|
2557
|
+
try {
|
|
2558
|
+
await execAsync3("yarn --version");
|
|
2559
|
+
return "yarn";
|
|
2560
|
+
} catch {
|
|
2561
|
+
}
|
|
2562
|
+
return "npm";
|
|
2563
|
+
}
|
|
2564
|
+
function registerUpgradeCommand(program2) {
|
|
2565
|
+
program2.command("upgrade").description("Upgrade Docyrus CLI to the latest version").action(async () => {
|
|
2566
|
+
const latestVersion = await withSpinner(
|
|
2567
|
+
"Checking for updates...",
|
|
2568
|
+
getLatestVersion,
|
|
2569
|
+
{ silent: output.isJson() }
|
|
2570
|
+
);
|
|
2571
|
+
if (!latestVersion) {
|
|
2572
|
+
output.error("Failed to check for updates. Please try again later.");
|
|
2573
|
+
process.exit(1);
|
|
2574
|
+
}
|
|
2575
|
+
if (!isNewerVersion(CLI_VERSION, latestVersion)) {
|
|
2576
|
+
output.success(`You're already on the latest version (${CLI_VERSION})`);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (!output.isJson()) {
|
|
2580
|
+
logger.info(`New version available: ${CLI_VERSION} \u2192 ${latestVersion}`);
|
|
2581
|
+
logger.newline();
|
|
2582
|
+
}
|
|
2583
|
+
const pm = await detectPackageManager();
|
|
2584
|
+
const commands = {
|
|
2585
|
+
npm: `npm install -g ${NPM_PACKAGE_NAME}@latest`,
|
|
2586
|
+
pnpm: `pnpm add -g ${NPM_PACKAGE_NAME}@latest`,
|
|
2587
|
+
yarn: `yarn global add ${NPM_PACKAGE_NAME}@latest`,
|
|
2588
|
+
bun: `bun add -g ${NPM_PACKAGE_NAME}@latest`
|
|
2589
|
+
};
|
|
2590
|
+
const command = commands[pm];
|
|
2591
|
+
try {
|
|
2592
|
+
await withSpinner(
|
|
2593
|
+
`Upgrading via ${pm}...`,
|
|
2594
|
+
async () => {
|
|
2595
|
+
await execAsync3(command);
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
silent: output.isJson(),
|
|
2599
|
+
successText: "Upgrade completed!"
|
|
2600
|
+
}
|
|
2601
|
+
);
|
|
2602
|
+
output.success(`Successfully upgraded to v${latestVersion}`, {
|
|
2603
|
+
previousVersion: CLI_VERSION,
|
|
2604
|
+
newVersion: latestVersion,
|
|
2605
|
+
packageManager: pm
|
|
2606
|
+
});
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2609
|
+
output.error(`Upgrade failed: ${message}`);
|
|
2610
|
+
if (!output.isJson()) {
|
|
2611
|
+
logger.dim(`Try manually: ${command}`);
|
|
2612
|
+
}
|
|
2613
|
+
process.exit(1);
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// src/commands/completion.ts
|
|
2619
|
+
var BASH_COMPLETION = `
|
|
2620
|
+
###-begin-${CLI_NAME}-completions-###
|
|
2621
|
+
_${CLI_NAME}_completions() {
|
|
2622
|
+
local cur_word args type_list
|
|
2623
|
+
cur_word="\${COMP_WORDS[COMP_CWORD]}"
|
|
2624
|
+
args=("\${COMP_WORDS[@]}")
|
|
2625
|
+
|
|
2626
|
+
# Commands
|
|
2627
|
+
type_list="login logout whoami create generate upgrade completion info help"
|
|
2628
|
+
|
|
2629
|
+
# Subcommands for generate
|
|
2630
|
+
if [[ \${args[1]} == "generate" ]]; then
|
|
2631
|
+
type_list="db"
|
|
2632
|
+
fi
|
|
2633
|
+
|
|
2634
|
+
COMPREPLY=($(compgen -W "\${type_list}" -- \${cur_word}))
|
|
2635
|
+
return 0
|
|
2636
|
+
}
|
|
2637
|
+
complete -F _${CLI_NAME}_completions ${CLI_NAME}
|
|
2638
|
+
###-end-${CLI_NAME}-completions-###
|
|
2639
|
+
`.trim();
|
|
2640
|
+
var ZSH_COMPLETION = `
|
|
2641
|
+
###-begin-${CLI_NAME}-completions-###
|
|
2642
|
+
_${CLI_NAME}() {
|
|
2643
|
+
local -a commands
|
|
2644
|
+
commands=(
|
|
2645
|
+
'login:Log in to Docyrus'
|
|
2646
|
+
'logout:Log out from Docyrus'
|
|
2647
|
+
'whoami:Display the current logged-in user'
|
|
2648
|
+
'create:Create a new Docyrus project from template'
|
|
2649
|
+
'generate:Code generation commands'
|
|
2650
|
+
'upgrade:Upgrade Docyrus CLI to the latest version'
|
|
2651
|
+
'completion:Generate shell completion script'
|
|
2652
|
+
'info:Display CLI and environment information'
|
|
2653
|
+
'help:Display help for command'
|
|
2654
|
+
)
|
|
2655
|
+
|
|
2656
|
+
local -a generate_commands
|
|
2657
|
+
generate_commands=(
|
|
2658
|
+
'db:Generate TanStack Query collections from OpenAPI spec'
|
|
2659
|
+
)
|
|
2660
|
+
|
|
2661
|
+
_arguments -C \\
|
|
2662
|
+
'1: :->command' \\
|
|
2663
|
+
'*::arg:->args'
|
|
2664
|
+
|
|
2665
|
+
case "$state" in
|
|
2666
|
+
command)
|
|
2667
|
+
_describe 'command' commands
|
|
2668
|
+
;;
|
|
2669
|
+
args)
|
|
2670
|
+
case $words[1] in
|
|
2671
|
+
generate)
|
|
2672
|
+
_describe 'subcommand' generate_commands
|
|
2673
|
+
;;
|
|
2674
|
+
esac
|
|
2675
|
+
;;
|
|
2676
|
+
esac
|
|
2677
|
+
}
|
|
2678
|
+
compdef _${CLI_NAME} ${CLI_NAME}
|
|
2679
|
+
###-end-${CLI_NAME}-completions-###
|
|
2680
|
+
`.trim();
|
|
2681
|
+
var FISH_COMPLETION = `
|
|
2682
|
+
###-begin-${CLI_NAME}-completions-###
|
|
2683
|
+
complete -c ${CLI_NAME} -f
|
|
2684
|
+
|
|
2685
|
+
# Commands
|
|
2686
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "login" -d "Log in to Docyrus"
|
|
2687
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "logout" -d "Log out from Docyrus"
|
|
2688
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "whoami" -d "Display the current logged-in user"
|
|
2689
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "create" -d "Create a new Docyrus project"
|
|
2690
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "generate" -d "Code generation commands"
|
|
2691
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "upgrade" -d "Upgrade Docyrus CLI"
|
|
2692
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "completion" -d "Generate shell completion"
|
|
2693
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "info" -d "Display CLI and environment information"
|
|
2694
|
+
complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "help" -d "Display help"
|
|
2695
|
+
|
|
2696
|
+
# generate subcommands
|
|
2697
|
+
complete -c ${CLI_NAME} -n "__fish_seen_subcommand_from generate" -a "db" -d "Generate TanStack Query collections"
|
|
2698
|
+
###-end-${CLI_NAME}-completions-###
|
|
2699
|
+
`.trim();
|
|
2700
|
+
function registerCompletionCommand(program2) {
|
|
2701
|
+
program2.command("completion").description("Generate shell completion script").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
|
|
2702
|
+
const shellLower = shell.toLowerCase();
|
|
2703
|
+
let script;
|
|
2704
|
+
let instructions;
|
|
2705
|
+
switch (shellLower) {
|
|
2706
|
+
case "bash":
|
|
2707
|
+
script = BASH_COMPLETION;
|
|
2708
|
+
instructions = `Add to ~/.bashrc:
|
|
2709
|
+
eval "$(${CLI_NAME} completion bash)"`;
|
|
2710
|
+
break;
|
|
2711
|
+
case "zsh":
|
|
2712
|
+
script = ZSH_COMPLETION;
|
|
2713
|
+
instructions = `Add to ~/.zshrc:
|
|
2714
|
+
eval "$(${CLI_NAME} completion zsh)"`;
|
|
2715
|
+
break;
|
|
2716
|
+
case "fish":
|
|
2717
|
+
script = FISH_COMPLETION;
|
|
2718
|
+
instructions = `Save to ~/.config/fish/completions/${CLI_NAME}.fish:
|
|
2719
|
+
${CLI_NAME} completion fish > ~/.config/fish/completions/${CLI_NAME}.fish`;
|
|
2720
|
+
break;
|
|
2721
|
+
default:
|
|
2722
|
+
output.error(`Unknown shell: ${shell}`);
|
|
2723
|
+
if (!output.isJson()) {
|
|
2724
|
+
logger.dim("Supported shells: bash, zsh, fish");
|
|
2725
|
+
}
|
|
2726
|
+
process.exit(1);
|
|
2727
|
+
}
|
|
2728
|
+
if (output.isJson()) {
|
|
2729
|
+
output.set("shell", shellLower);
|
|
2730
|
+
output.set("script", script);
|
|
2731
|
+
output.set("instructions", instructions);
|
|
2732
|
+
} else {
|
|
2733
|
+
console.warn(script);
|
|
2734
|
+
logger.dim(instructions);
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
function registerInfoCommand(program2) {
|
|
2739
|
+
program2.command("info").description("Display CLI and environment information").action(async () => {
|
|
2740
|
+
const tokenManager = getTokenManager();
|
|
2741
|
+
const isLoggedIn = await tokenManager.isLoggedIn();
|
|
2742
|
+
const email = isLoggedIn ? await tokenManager.getUserEmail() : void 0;
|
|
2743
|
+
const info = {
|
|
2744
|
+
cli: {
|
|
2745
|
+
name: CLI_NAME,
|
|
2746
|
+
version: CLI_VERSION
|
|
2747
|
+
},
|
|
2748
|
+
node: {
|
|
2749
|
+
version: process.version
|
|
2750
|
+
},
|
|
2751
|
+
os: {
|
|
2752
|
+
platform: platform(),
|
|
2753
|
+
release: release(),
|
|
2754
|
+
arch: arch()
|
|
2755
|
+
},
|
|
2756
|
+
environment: {
|
|
2757
|
+
apiUrl: DOCYRUS_API_URL,
|
|
2758
|
+
configDir: join(homedir(), ".docyrus")
|
|
2759
|
+
},
|
|
2760
|
+
auth: {
|
|
2761
|
+
loggedIn: isLoggedIn,
|
|
2762
|
+
...email && { email }
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
if (output.isJson()) {
|
|
2766
|
+
output.set("info", info);
|
|
2767
|
+
} else {
|
|
2768
|
+
logger.bold(`${CLI_NAME} v${CLI_VERSION}`);
|
|
2769
|
+
logger.newline();
|
|
2770
|
+
logger.log("System:");
|
|
2771
|
+
logger.dim(` Node.js: ${info.node.version}`);
|
|
2772
|
+
logger.dim(` OS: ${info.os.platform} ${info.os.release} (${info.os.arch})`);
|
|
2773
|
+
logger.newline();
|
|
2774
|
+
logger.log("Environment:");
|
|
2775
|
+
logger.dim(` API URL: ${info.environment.apiUrl}`);
|
|
2776
|
+
logger.dim(` Config: ${info.environment.configDir}`);
|
|
2777
|
+
logger.newline();
|
|
2778
|
+
logger.log("Authentication:");
|
|
2779
|
+
if (info.auth.loggedIn) {
|
|
2780
|
+
logger.dim(` Status: Logged in`);
|
|
2781
|
+
if (info.auth.email) {
|
|
2782
|
+
logger.dim(` Email: ${info.auth.email}`);
|
|
2783
|
+
}
|
|
2784
|
+
} else {
|
|
2785
|
+
logger.dim(` Status: Not logged in`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
function registerConfigCommand(program2) {
|
|
2791
|
+
program2.command("config").description("Manage CLI configuration").option("--token", "Update Docyrus token for private template access").option("--show", "Show current configuration").action(async (options) => {
|
|
2792
|
+
const tokenManager = getTokenManager();
|
|
2793
|
+
if (options.show) {
|
|
2794
|
+
const githubToken2 = await tokenManager.getGithubToken();
|
|
2795
|
+
const isLoggedIn2 = await tokenManager.isLoggedIn();
|
|
2796
|
+
const email2 = await tokenManager.getUserEmail();
|
|
2797
|
+
logger.log("Current configuration:");
|
|
2798
|
+
logger.newline();
|
|
2799
|
+
logger.log(` Docyrus Login: ${isLoggedIn2 ? `\u2713 ${email2}` : "\u2717 Not logged in"}`);
|
|
2800
|
+
logger.log(` Docyrus Token: ${githubToken2 ? "\u2713 Configured" : "\u2717 Not configured"}`);
|
|
2801
|
+
logger.newline();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
if (options.token) {
|
|
2805
|
+
const currentToken = await tokenManager.getGithubToken();
|
|
2806
|
+
if (currentToken) {
|
|
2807
|
+
logger.log("Current Docyrus token will be replaced.");
|
|
2808
|
+
}
|
|
2809
|
+
const newToken = await password({
|
|
2810
|
+
message: MESSAGES.DOCYRUS_TOKEN_PROMPT,
|
|
2811
|
+
mask: "*"
|
|
2812
|
+
});
|
|
2813
|
+
if (newToken) {
|
|
2814
|
+
await tokenManager.setGithubToken(newToken);
|
|
2815
|
+
logger.success(MESSAGES.DOCYRUS_TOKEN_SAVED);
|
|
2816
|
+
} else {
|
|
2817
|
+
logger.warn("No token provided. Token not updated.");
|
|
2818
|
+
}
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
const githubToken = await tokenManager.getGithubToken();
|
|
2822
|
+
const isLoggedIn = await tokenManager.isLoggedIn();
|
|
2823
|
+
const email = await tokenManager.getUserEmail();
|
|
2824
|
+
logger.log("Current configuration:");
|
|
2825
|
+
logger.newline();
|
|
2826
|
+
logger.log(` Docyrus Login: ${isLoggedIn ? `\u2713 ${email}` : "\u2717 Not logged in"}`);
|
|
2827
|
+
logger.log(` Docyrus Token: ${githubToken ? "\u2713 Configured" : "\u2717 Not configured"}`);
|
|
2828
|
+
logger.newline();
|
|
2829
|
+
logger.log("To update token: docyrus config --token");
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// src/commands/index.ts
|
|
2834
|
+
function registerCommands(program2) {
|
|
2835
|
+
registerLoginCommand(program2);
|
|
2836
|
+
registerLogoutCommand(program2);
|
|
2837
|
+
registerWhoamiCommand(program2);
|
|
2838
|
+
registerCreateCommand(program2);
|
|
2839
|
+
registerGenerateCommand(program2);
|
|
2840
|
+
registerUpgradeCommand(program2);
|
|
2841
|
+
registerCompletionCommand(program2);
|
|
2842
|
+
registerInfoCommand(program2);
|
|
2843
|
+
registerConfigCommand(program2);
|
|
2844
|
+
}
|
|
2845
|
+
function isNewerVersion2(current, latest) {
|
|
2846
|
+
const currentParts = current.replace(/^v/, "").split(".").map(Number);
|
|
2847
|
+
const latestParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
2848
|
+
for (let i = 0; i < 3; i++) {
|
|
2849
|
+
const curr = currentParts[i] || 0;
|
|
2850
|
+
const lat = latestParts[i] || 0;
|
|
2851
|
+
if (lat > curr) return true;
|
|
2852
|
+
if (lat < curr) return false;
|
|
2853
|
+
}
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
async function checkForUpdates(packageName, currentVersion) {
|
|
2857
|
+
try {
|
|
2858
|
+
const controller = new AbortController();
|
|
2859
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
2860
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}`, {
|
|
2861
|
+
signal: controller.signal,
|
|
2862
|
+
headers: { Accept: "application/json" }
|
|
2863
|
+
});
|
|
2864
|
+
clearTimeout(timeout);
|
|
2865
|
+
if (!response.ok) return;
|
|
2866
|
+
const data = await response.json();
|
|
2867
|
+
const latestVersion = data["dist-tags"]?.latest;
|
|
2868
|
+
if (latestVersion && isNewerVersion2(currentVersion, latestVersion)) {
|
|
2869
|
+
console.info();
|
|
2870
|
+
console.info(chalk4.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
2871
|
+
console.info(chalk4.yellow("\u2502") + chalk4.bold(" Update available! ") + chalk4.dim(`${currentVersion} \u2192 `) + chalk4.green(latestVersion) + chalk4.yellow(" \u2502"));
|
|
2872
|
+
console.info(chalk4.yellow("\u2502") + chalk4.dim(` Run `) + chalk4.cyan(`npm i -g ${packageName}`) + chalk4.dim(" to update") + chalk4.yellow(" \u2502"));
|
|
2873
|
+
console.info(chalk4.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
2874
|
+
console.info();
|
|
2875
|
+
}
|
|
2876
|
+
} catch {
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// src/cli.ts
|
|
2881
|
+
var program = new Command();
|
|
2882
|
+
program.name(CLI_NAME).description("Docyrus CLI - Authentication and project management tools").version(CLI_VERSION, "-v, --version", "Display version number").option("--json", "Output results in JSON format").hook("preAction", (thisCommand) => {
|
|
2883
|
+
const opts = thisCommand.opts();
|
|
2884
|
+
if (opts.json) {
|
|
2885
|
+
output.setFormat("json");
|
|
2886
|
+
}
|
|
2887
|
+
}).hook("postAction", () => {
|
|
2888
|
+
output.flush();
|
|
2889
|
+
});
|
|
2890
|
+
registerCommands(program);
|
|
2891
|
+
program.configureOutput({
|
|
2892
|
+
outputError: (str, write) => write(str)
|
|
2893
|
+
});
|
|
2894
|
+
async function main() {
|
|
2895
|
+
try {
|
|
2896
|
+
if (!process.argv.includes("--json")) {
|
|
2897
|
+
checkForUpdates(NPM_PACKAGE_NAME, CLI_VERSION).catch(() => {
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
await program.parseAsync(process.argv);
|
|
2901
|
+
} catch (error) {
|
|
2902
|
+
if (error instanceof CliError) {
|
|
2903
|
+
output.error(error.message);
|
|
2904
|
+
if (error.suggestion && !output.isJson()) {
|
|
2905
|
+
logger.dim(error.suggestion);
|
|
2906
|
+
}
|
|
2907
|
+
output.flush();
|
|
2908
|
+
process.exit(error.exitCode);
|
|
2909
|
+
}
|
|
2910
|
+
if (error instanceof Error) {
|
|
2911
|
+
output.error(error.message);
|
|
2912
|
+
if (process.env.DEBUG) {
|
|
2913
|
+
console.error(error.stack);
|
|
2914
|
+
}
|
|
2915
|
+
output.flush();
|
|
2916
|
+
process.exit(1);
|
|
2917
|
+
}
|
|
2918
|
+
output.error("An unexpected error occurred.");
|
|
2919
|
+
output.flush();
|
|
2920
|
+
process.exit(1);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
main();
|
|
2924
|
+
//# sourceMappingURL=cli.js.map
|
|
2925
|
+
//# sourceMappingURL=cli.js.map
|