@heyputer/shell 2.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/.claude/settings.local.json +9 -0
- package/.env.example +3 -0
- package/.github/workflows/npm-build.yml +34 -0
- package/.github/workflows/npm-publish.yml +50 -0
- package/.idx/dev.nix +53 -0
- package/CHANGELOG.md +292 -0
- package/LICENSE.md +21 -0
- package/README.md +18 -0
- package/bin/index.js +258 -0
- package/package.json +58 -0
- package/screenshot.png +0 -0
- package/src/commands/apps.js +356 -0
- package/src/commands/auth.js +195 -0
- package/src/commands/deploy.js +54 -0
- package/src/commands/files.js +1187 -0
- package/src/commands/init.js +322 -0
- package/src/commands/shell.js +61 -0
- package/src/commands/sites.js +169 -0
- package/src/commands/subdomains.js +95 -0
- package/src/commons.js +409 -0
- package/src/crypto.js +9 -0
- package/src/executor.js +386 -0
- package/src/modules/ErrorModule.js +20 -0
- package/src/modules/ProfileModule.js +334 -0
- package/src/modules/PuterModule.js +30 -0
- package/src/utils.js +135 -0
- package/tests/ErrorModule.test.js +42 -0
- package/tests/ProfileModule.test.js +274 -0
- package/tests/PuterModule.test.js +56 -0
- package/tests/apps.test.js +194 -0
- package/tests/commons.test.js +380 -0
- package/tests/deploy.test.js +84 -0
- package/tests/executor.test.js +52 -0
- package/tests/files.test.js +640 -0
- package/tests/login.test.js +206 -0
- package/tests/shell.test.js +184 -0
- package/tests/sites.test.js +67 -0
- package/tests/subdomains.test.js +90 -0
- package/tests/utils.test.js +193 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// external
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import Conf from 'conf';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import {getAuthToken} from "@heyputer/puter.js/src/init.cjs";
|
|
7
|
+
import { puter } from "@heyputer/puter.js";
|
|
8
|
+
|
|
9
|
+
// project
|
|
10
|
+
import { BASE_URL, NULL_UUID, PROJECT_NAME, getHeaders, reconfigureURLs } from '../commons.js'
|
|
11
|
+
|
|
12
|
+
// builtin
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { initPuterModule } from './PuterModule.js';
|
|
16
|
+
|
|
17
|
+
// initializations
|
|
18
|
+
const config = new Conf({ projectName: PROJECT_NAME });
|
|
19
|
+
|
|
20
|
+
let profileModule;
|
|
21
|
+
|
|
22
|
+
function toApiSubdomain(inputUrl) {
|
|
23
|
+
const url = new URL(inputUrl);
|
|
24
|
+
const hostParts = url.hostname.split('.');
|
|
25
|
+
|
|
26
|
+
// Insert 'api' before the domain
|
|
27
|
+
hostParts.splice(-2, 0, 'api');
|
|
28
|
+
url.hostname = hostParts.join('.');
|
|
29
|
+
|
|
30
|
+
let output = url.toString();
|
|
31
|
+
if (output.endsWith('/')) {
|
|
32
|
+
output = output.slice(0, -1);
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class ProfileModule {
|
|
38
|
+
async checkLogin() {
|
|
39
|
+
if (config.get('auth_token')) {
|
|
40
|
+
this.migrateLegacyConfig();
|
|
41
|
+
}
|
|
42
|
+
if (!config.get('selected_profile')) {
|
|
43
|
+
console.log(chalk.cyan('Please login first (or use CTRL+C to exit):'));
|
|
44
|
+
await this.switchProfileWizard();
|
|
45
|
+
// re init with new authToken
|
|
46
|
+
initPuterModule();
|
|
47
|
+
}
|
|
48
|
+
this.applyProfileToGlobals();
|
|
49
|
+
}
|
|
50
|
+
migrateLegacyConfig() {
|
|
51
|
+
const auth_token = config.get('auth_token');
|
|
52
|
+
const username = config.get('username');
|
|
53
|
+
|
|
54
|
+
this.addProfile({
|
|
55
|
+
host: BASE_URL,
|
|
56
|
+
username,
|
|
57
|
+
cwd: `/${username}`,
|
|
58
|
+
token: auth_token,
|
|
59
|
+
uuid: NULL_UUID,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
config.delete('auth_token');
|
|
63
|
+
config.delete('username');
|
|
64
|
+
}
|
|
65
|
+
getDefaultProfile() {
|
|
66
|
+
const auth_token = config.get('auth_token');
|
|
67
|
+
if (!auth_token) return;
|
|
68
|
+
return {
|
|
69
|
+
host: 'puter.com',
|
|
70
|
+
username: config.get('username'),
|
|
71
|
+
token: auth_token,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
getProfiles() {
|
|
75
|
+
const profiles = config.get('profiles') ?? [];
|
|
76
|
+
return profiles;
|
|
77
|
+
}
|
|
78
|
+
addProfile(newProfile) {
|
|
79
|
+
const profiles = [
|
|
80
|
+
...this.getProfiles().filter(p => !p.transient),
|
|
81
|
+
newProfile,
|
|
82
|
+
];
|
|
83
|
+
config.set('profiles', profiles);
|
|
84
|
+
}
|
|
85
|
+
selectProfile(profile) {
|
|
86
|
+
config.set('selected_profile', profile.uuid);
|
|
87
|
+
config.set('username', `${profile.username}`);
|
|
88
|
+
config.set('cwd', `/${profile.username}`);
|
|
89
|
+
this.applyProfileToGlobals(profile);
|
|
90
|
+
}
|
|
91
|
+
getCurrentProfile() {
|
|
92
|
+
const profiles = this.getProfiles();
|
|
93
|
+
const uuid = config.get('selected_profile');
|
|
94
|
+
return profiles.find(p => p.uuid === uuid);
|
|
95
|
+
}
|
|
96
|
+
applyProfileToGlobals(profile) {
|
|
97
|
+
if (!profile) profile = this.getCurrentProfile();
|
|
98
|
+
reconfigureURLs({
|
|
99
|
+
base: profile.host,
|
|
100
|
+
api: toApiSubdomain(profile.host),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
getAuthToken() {
|
|
104
|
+
const uuid = config.get('selected_profile');
|
|
105
|
+
const profiles = this.getProfiles();
|
|
106
|
+
const profile = profiles.find(v => v.uuid === uuid);
|
|
107
|
+
return profile?.token;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async switchProfileWizard(args = {}) {
|
|
111
|
+
const profiles = this.getProfiles();
|
|
112
|
+
if (profiles.length < 1) {
|
|
113
|
+
return this.addProfileWizard(args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const answer = await inquirer.prompt([
|
|
117
|
+
{
|
|
118
|
+
name: 'profile',
|
|
119
|
+
type: 'list',
|
|
120
|
+
message: 'Select a Profile',
|
|
121
|
+
choices: [
|
|
122
|
+
...profiles.map((v, i) => {
|
|
123
|
+
return {
|
|
124
|
+
name: v.name ?? `${v.username}@${v.host}`,
|
|
125
|
+
value: v,
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
128
|
+
{
|
|
129
|
+
name: 'Create New Profile',
|
|
130
|
+
value: 'new',
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
if (answer.profile === 'new') {
|
|
137
|
+
return await this.addProfileWizard(args);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.selectProfile(answer.profile);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async addProfileWizard(args = {}) {
|
|
144
|
+
const host = args.host || 'https://puter.com';
|
|
145
|
+
|
|
146
|
+
if (args.withCredentials) {
|
|
147
|
+
return await this.credentialLogin({ ...args, host });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Browser-based login (default)
|
|
151
|
+
return await this.browserLogin({ ...args, host });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async browserLogin(args) {
|
|
155
|
+
const { host, save } = args;
|
|
156
|
+
const TIMEOUT_MS = 60000; // 1 minute timeout
|
|
157
|
+
let spinner;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
spinner = ora('Opening browser for login...').start();
|
|
161
|
+
|
|
162
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
163
|
+
setTimeout(() => reject(new Error('Login timed out after 60 seconds')), TIMEOUT_MS);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const authToken = await Promise.race([
|
|
167
|
+
getAuthToken(),
|
|
168
|
+
timeoutPromise
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
if (!authToken) {
|
|
172
|
+
spinner.fail(chalk.red('Login failed or was cancelled.'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
spinner.text = 'Fetching user info...';
|
|
177
|
+
|
|
178
|
+
// Set token and fetch user info
|
|
179
|
+
puter.setAuthToken(authToken);
|
|
180
|
+
const userInfo = await puter.auth.getUser();
|
|
181
|
+
|
|
182
|
+
const profileUUID = crypto.randomUUID();
|
|
183
|
+
const profile = {
|
|
184
|
+
host,
|
|
185
|
+
username: userInfo.username,
|
|
186
|
+
cwd: `/${userInfo.username}`,
|
|
187
|
+
token: authToken,
|
|
188
|
+
uuid: profileUUID,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.addProfile(profile);
|
|
192
|
+
this.selectProfile(profile);
|
|
193
|
+
spinner.succeed(chalk.green(`Successfully logged in as ${userInfo.username}!`));
|
|
194
|
+
|
|
195
|
+
// Handle --save option
|
|
196
|
+
this.saveTokenToEnv(authToken, save);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (spinner) {
|
|
199
|
+
spinner.fail(chalk.red(`Failed to login: ${error.message}`));
|
|
200
|
+
} else {
|
|
201
|
+
console.error(chalk.red(`Failed to login: ${error.message}`));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async credentialLogin(args) {
|
|
207
|
+
const { host, save } = args;
|
|
208
|
+
|
|
209
|
+
const answers = await inquirer.prompt([
|
|
210
|
+
{
|
|
211
|
+
type: 'input',
|
|
212
|
+
name: 'username',
|
|
213
|
+
message: 'Username:',
|
|
214
|
+
validate: input => input.length >= 1 || 'Username is required'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'password',
|
|
218
|
+
name: 'password',
|
|
219
|
+
message: 'Password:',
|
|
220
|
+
mask: '*',
|
|
221
|
+
validate: input => input.length >= 1 || 'Password is required'
|
|
222
|
+
}
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
let spinner;
|
|
226
|
+
try {
|
|
227
|
+
spinner = ora('Logging in to Puter...').start();
|
|
228
|
+
|
|
229
|
+
const apiHost = toApiSubdomain(host);
|
|
230
|
+
const response = await fetch(`${apiHost}/login`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: getHeaders(),
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
username: answers.username,
|
|
235
|
+
password: answers.password,
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const data = await response.json();
|
|
240
|
+
|
|
241
|
+
if (data.proceed && data.next_step === 'otp') {
|
|
242
|
+
// Handle 2FA
|
|
243
|
+
spinner.stop();
|
|
244
|
+
const otpAnswer = await inquirer.prompt([
|
|
245
|
+
{
|
|
246
|
+
type: 'input',
|
|
247
|
+
name: 'otp',
|
|
248
|
+
message: 'Enter your 2FA code:',
|
|
249
|
+
validate: input => input.length >= 1 || '2FA code is required'
|
|
250
|
+
}
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
spinner = ora('Verifying 2FA code...').start();
|
|
254
|
+
const otpResponse = await fetch(`${apiHost}/login/otp`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: getHeaders(),
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
token: data.otp_jwt_token,
|
|
259
|
+
code: otpAnswer.otp,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const otpData = await otpResponse.json();
|
|
264
|
+
|
|
265
|
+
if (otpData.token) {
|
|
266
|
+
this.createProfileFromToken(otpData.token, answers.username, host, spinner, save);
|
|
267
|
+
} else {
|
|
268
|
+
spinner.fail(chalk.red('2FA verification failed.'));
|
|
269
|
+
}
|
|
270
|
+
} else if (data.token) {
|
|
271
|
+
this.createProfileFromToken(data.token, answers.username, host, spinner, save);
|
|
272
|
+
} else {
|
|
273
|
+
spinner.fail(chalk.red(data.error?.message || 'Login failed. Please check your credentials.'));
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (spinner) {
|
|
277
|
+
spinner.fail(chalk.red(`Failed to login: ${error.message}`));
|
|
278
|
+
} else {
|
|
279
|
+
console.error(chalk.red(`Failed to login: ${error.message}`));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
createProfileFromToken(token, username, host, spinner, save) {
|
|
285
|
+
const profileUUID = crypto.randomUUID();
|
|
286
|
+
const profile = {
|
|
287
|
+
host,
|
|
288
|
+
username,
|
|
289
|
+
cwd: `/${username}`,
|
|
290
|
+
token,
|
|
291
|
+
uuid: profileUUID,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.addProfile(profile);
|
|
295
|
+
this.selectProfile(profile);
|
|
296
|
+
spinner.succeed(chalk.green(`Successfully logged in as ${username}!`));
|
|
297
|
+
|
|
298
|
+
// Handle --save option
|
|
299
|
+
this.saveTokenToEnv(token, save);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
saveTokenToEnv(token, save) {
|
|
303
|
+
if (!save) return;
|
|
304
|
+
|
|
305
|
+
const localEnvFile = '.env';
|
|
306
|
+
try {
|
|
307
|
+
if (fs.existsSync(localEnvFile)) {
|
|
308
|
+
console.log(chalk.yellow(`File "${localEnvFile}" already exists... Adding token.`));
|
|
309
|
+
fs.appendFileSync(localEnvFile, `\nPUTER_API_KEY="${token}"`, 'utf8');
|
|
310
|
+
} else {
|
|
311
|
+
console.log(chalk.cyan(`Saving token to ${chalk.green(localEnvFile)} file.`));
|
|
312
|
+
fs.writeFileSync(localEnvFile, `PUTER_API_KEY="${token}"`, 'utf8');
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error(chalk.red(`Cannot save token to .env file. Error: ${error.message}`));
|
|
316
|
+
console.log(chalk.cyan(`PUTER_API_KEY="${token}"`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const initProfileModule = () => {
|
|
322
|
+
profileModule = new ProfileModule();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get ProfileModule object
|
|
327
|
+
* @returns {ProfileModule} ProfileModule - ProfileModule Object.
|
|
328
|
+
*/
|
|
329
|
+
export const getProfileModule = () => {
|
|
330
|
+
if (!profileModule) {
|
|
331
|
+
throw new Error("Call initprofileModule() first");
|
|
332
|
+
}
|
|
333
|
+
return profileModule;
|
|
334
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
import { PROJECT_NAME } from "../commons.js";
|
|
3
|
+
import { puter } from "@heyputer/puter.js";
|
|
4
|
+
|
|
5
|
+
const config = new Conf({ projectName: PROJECT_NAME });
|
|
6
|
+
|
|
7
|
+
let puterModule;
|
|
8
|
+
|
|
9
|
+
export const initPuterModule = () => {
|
|
10
|
+
const uuid = config.get("selected_profile");
|
|
11
|
+
const profiles = config.get("profiles") ?? [];
|
|
12
|
+
const profile = profiles.find((v) => v.uuid === uuid);
|
|
13
|
+
const authToken = profile?.token;
|
|
14
|
+
|
|
15
|
+
if (authToken) {
|
|
16
|
+
puter.setAuthToken(authToken);
|
|
17
|
+
puterModule = puter;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get Puter object
|
|
23
|
+
* @returns {puter} puter - Puter Object.
|
|
24
|
+
*/
|
|
25
|
+
export const getPuter = () => {
|
|
26
|
+
if (!puterModule) {
|
|
27
|
+
throw new Error("Call initPuterModule() first");
|
|
28
|
+
}
|
|
29
|
+
return puterModule;
|
|
30
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import yargsParser from 'yargs-parser';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert "2024-10-07T15:03:53.000Z" to "10/7/2024, 15:03:53"
|
|
6
|
+
* @param {Date} value date value
|
|
7
|
+
* @returns formatted date string
|
|
8
|
+
*/
|
|
9
|
+
export function formatDate(value) {
|
|
10
|
+
const date = new Date(value);
|
|
11
|
+
return date.toLocaleString("en-US", {
|
|
12
|
+
year: "numeric",
|
|
13
|
+
month: "2-digit",
|
|
14
|
+
day: "2-digit",
|
|
15
|
+
hour: "2-digit",
|
|
16
|
+
minute: "2-digit",
|
|
17
|
+
second: "2-digit",
|
|
18
|
+
hour12: false,
|
|
19
|
+
timeZone: 'UTC'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format timestamp to date or time
|
|
25
|
+
* @param {number} timestamp value
|
|
26
|
+
* @returns string
|
|
27
|
+
*/
|
|
28
|
+
export function formatDateTime(timestamp) {
|
|
29
|
+
const date = new Date(timestamp * 1000); // Convert to milliseconds
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const diff = now - date;
|
|
32
|
+
if (diff < 86400000) { // Less than 24 hours
|
|
33
|
+
return date.toLocaleTimeString();
|
|
34
|
+
} else {
|
|
35
|
+
return date.toLocaleDateString();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format file size in human readable format
|
|
40
|
+
* @param {number} size File size value
|
|
41
|
+
* @returns string formatted in human readable format
|
|
42
|
+
*/
|
|
43
|
+
export function formatSize(size) {
|
|
44
|
+
if (size === null || size === undefined) return '0';
|
|
45
|
+
if (size === 0) return '0';
|
|
46
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
47
|
+
let unit = 0;
|
|
48
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
49
|
+
size /= 1024;
|
|
50
|
+
unit++;
|
|
51
|
+
}
|
|
52
|
+
return `${size.toFixed(1)} ${units[unit]}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Display non null values in formatted table
|
|
57
|
+
* @param {Object} data Object to display
|
|
58
|
+
* @returns null
|
|
59
|
+
*/
|
|
60
|
+
export function displayNonNullValues(data) {
|
|
61
|
+
if (typeof data !== 'object' || data === null) {
|
|
62
|
+
console.error("Invalid input: Input must be a non-null object.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const tableData = [];
|
|
66
|
+
function flattenObject(obj, parentKey = '') {
|
|
67
|
+
for (const key in obj) {
|
|
68
|
+
const value = obj[key];
|
|
69
|
+
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
70
|
+
if (value !== null) {
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
flattenObject(value, newKey);
|
|
73
|
+
} else {
|
|
74
|
+
tableData.push({ key: newKey, value: value });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
flattenObject(data);
|
|
81
|
+
// Determine max key length for formatting
|
|
82
|
+
const maxKeyLength = tableData.reduce((max, item) => Math.max(max, item.key.length), 0);
|
|
83
|
+
// Format and output the table
|
|
84
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
85
|
+
console.log(chalk.cyan(`| ${'Key'.padEnd(maxKeyLength)} | Value`));
|
|
86
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
87
|
+
tableData.forEach(item => {
|
|
88
|
+
const key = item.key.padEnd(maxKeyLength);
|
|
89
|
+
const value = String(item.value);
|
|
90
|
+
console.log(chalk.green(`| ${chalk.dim(key)} | ${value}`));
|
|
91
|
+
});
|
|
92
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
93
|
+
console.log(chalk.cyan(`You have ${chalk.green(tableData.length)} key/value pair(s).`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse command line arguments including quoted strings
|
|
98
|
+
* @param {string} input Raw command line input
|
|
99
|
+
* @returns {Object} Parsed arguments
|
|
100
|
+
*/
|
|
101
|
+
export function parseArgs(input, options = {}) {
|
|
102
|
+
const result = yargsParser(input, options);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks if a given string is a valid UUID of any version
|
|
108
|
+
* @param {string} uuid - The string to validate.
|
|
109
|
+
* @returns {boolean} - True if the string is a valid UUID, false otherwise.
|
|
110
|
+
*/
|
|
111
|
+
export function isValidAppUuid (uuid) {
|
|
112
|
+
return uuid.startsWith('app-') && is_valid_uuid4(uuid.slice(4));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Checks if a given string is a valid UUID version 4.
|
|
117
|
+
* @param {string} uuid - The string to validate.
|
|
118
|
+
* @returns {boolean} - True if the string is a valid UUID version 4, false otherwise.
|
|
119
|
+
*/
|
|
120
|
+
export function is_valid_uuid4 (uuid) {
|
|
121
|
+
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
122
|
+
return uuidV4Regex.test(uuid);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get system editor
|
|
127
|
+
* @returns {string} - System editor
|
|
128
|
+
* @example
|
|
129
|
+
* getSystemEditor()
|
|
130
|
+
* // => 'nano'
|
|
131
|
+
*/
|
|
132
|
+
export function getSystemEditor() {
|
|
133
|
+
return process.env.EDITOR || process.env.VISUAL ||
|
|
134
|
+
(process.platform === 'win32' ? 'notepad' : 'vi')
|
|
135
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { it, describe, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.spyOn(console, "log").mockImplementation(() => { });
|
|
4
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
5
|
+
|
|
6
|
+
let errors, report, ERROR_BUFFER_LIMIT, showLast;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
vi.resetModules();
|
|
10
|
+
const module = await import("../src/modules/ErrorModule");
|
|
11
|
+
errors = module.errors;
|
|
12
|
+
report = module.report;
|
|
13
|
+
ERROR_BUFFER_LIMIT = module.ERROR_BUFFER_LIMIT;
|
|
14
|
+
showLast = module.showLast;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("report", () => {
|
|
18
|
+
it("should be able to report error", () => {
|
|
19
|
+
report("hehe")
|
|
20
|
+
expect(errors).toHaveLength(1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("should not exceed error buffer limit", () => {
|
|
24
|
+
for (let i = 0; i < 100; i++) {
|
|
25
|
+
report(`error ${i}`)
|
|
26
|
+
}
|
|
27
|
+
expect(errors.length).lessThanOrEqual(ERROR_BUFFER_LIMIT);
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe("showLast", () => {
|
|
32
|
+
it("should not log error if no error exists", () => {
|
|
33
|
+
showLast();
|
|
34
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("No errors to report"));
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should log error if error exists", () => {
|
|
38
|
+
report("hehe")
|
|
39
|
+
showLast();
|
|
40
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("hehe"));
|
|
41
|
+
})
|
|
42
|
+
})
|