@anaemia/cli 0.0.1
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/LICENSE +201 -0
- package/README.md +3 -0
- package/dist/index.js +362 -0
- package/dist/route-manifest.json +6 -0
- package/dist/scaffold.js +369 -0
- package/dist/utils/casing.js +18 -0
- package/package.json +33 -0
- package/src/index.ts +428 -0
- package/src/scaffold.ts +430 -0
- package/src/utils/casing.ts +20 -0
- package/tsconfig.json +11 -0
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { toCamelCase, toKebabCase, toPascalCase } from "./utils/casing.js";
|
|
4
|
+
export function scaffoldFeature(rawName, appRoot) {
|
|
5
|
+
const folderName = toKebabCase(rawName);
|
|
6
|
+
const componentName = toPascalCase(rawName);
|
|
7
|
+
const isTypeScript = fs.existsSync(path.join(appRoot, "tsconfig.json"));
|
|
8
|
+
const ext = isTypeScript ? "tsx" : "jsx";
|
|
9
|
+
const scriptExt = isTypeScript ? "ts" : "js";
|
|
10
|
+
const featureDir = path.resolve(appRoot, `./src/features/${folderName}`);
|
|
11
|
+
const directories = [path.join(featureDir, "components"), path.join(featureDir, "hooks"), path.join(featureDir, "server")];
|
|
12
|
+
directories.forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
|
|
13
|
+
const componentContent = isTypeScript
|
|
14
|
+
? `import { children, JSX } from "solid-js";
|
|
15
|
+
import styles from "./${componentName}.module.scss";
|
|
16
|
+
|
|
17
|
+
interface ${componentName}Props {
|
|
18
|
+
children?: JSX.Element;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ${componentName}(props: ${componentName}Props) {
|
|
22
|
+
const resolved = children(() => props.children);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div class={styles.wrapper}>
|
|
26
|
+
Welcome to ${componentName}
|
|
27
|
+
{resolved()}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
`
|
|
32
|
+
: `import { children } from "solid-js";
|
|
33
|
+
import styles from "./${componentName}.module.scss";
|
|
34
|
+
|
|
35
|
+
export function ${componentName}(props) {
|
|
36
|
+
const resolved = children(() => props.children);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div class={styles.wrapper}>
|
|
40
|
+
Welcome to ${componentName}
|
|
41
|
+
{resolved()}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
const actionsContent = isTypeScript
|
|
47
|
+
? `import { runOnServer } from "@anaemia/core";
|
|
48
|
+
|
|
49
|
+
export const ${toCamelCase(componentName)}Action = runOnServer(async (input: unknown) => {
|
|
50
|
+
// TODO: implement server-side logic
|
|
51
|
+
return { ok: true };
|
|
52
|
+
});
|
|
53
|
+
`
|
|
54
|
+
: `import { runOnServer } from "@anaemia/core";
|
|
55
|
+
|
|
56
|
+
export const ${toCamelCase(componentName)}Action = runOnServer(async (input) => {
|
|
57
|
+
// TODO: implement server-side logic
|
|
58
|
+
return { ok: true };
|
|
59
|
+
});
|
|
60
|
+
`;
|
|
61
|
+
const hookContent = isTypeScript
|
|
62
|
+
? `import { createSignal } from "solid-js";
|
|
63
|
+
import { ${toCamelCase(componentName)}Action } from "../server/actions";
|
|
64
|
+
|
|
65
|
+
export function use${componentName}() {
|
|
66
|
+
const [data, setData] = createSignal<unknown>(null);
|
|
67
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
68
|
+
const [loading, setLoading] = createSignal(false);
|
|
69
|
+
|
|
70
|
+
const execute = async (input: unknown) => {
|
|
71
|
+
setLoading(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const result = await ${toCamelCase(componentName)}Action(input);
|
|
75
|
+
setData(result);
|
|
76
|
+
return result;
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
setError(err.message ?? "Unknown error");
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return { data, error, loading, execute };
|
|
85
|
+
}
|
|
86
|
+
`
|
|
87
|
+
: `import { createSignal } from "solid-js";
|
|
88
|
+
import { ${toCamelCase(componentName)}Action } from "../server/actions";
|
|
89
|
+
|
|
90
|
+
export function use${componentName}() {
|
|
91
|
+
const [data, setData] = createSignal(null);
|
|
92
|
+
const [error, setError] = createSignal(null);
|
|
93
|
+
const [loading, setLoading] = createSignal(false);
|
|
94
|
+
|
|
95
|
+
const execute = async (input) => {
|
|
96
|
+
setLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
const result = await ${toCamelCase(componentName)}Action(input);
|
|
100
|
+
setData(result);
|
|
101
|
+
return result;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
setError(err.message ?? "Unknown error");
|
|
104
|
+
} finally {
|
|
105
|
+
setLoading(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return { data, error, loading, execute };
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
const indexContent = `export { ${componentName} } from "./components/${componentName}";
|
|
113
|
+
export { use${componentName} } from "./hooks/use${componentName}";
|
|
114
|
+
`;
|
|
115
|
+
fs.writeFileSync(path.join(featureDir, `components/${componentName}.${ext}`), componentContent, "utf8");
|
|
116
|
+
fs.writeFileSync(path.join(featureDir, `components/${componentName}.module.scss`), `.wrapper {\n display: block;\n}\n`, "utf8");
|
|
117
|
+
fs.writeFileSync(path.join(featureDir, `server/actions.${scriptExt}`), actionsContent, "utf8");
|
|
118
|
+
fs.writeFileSync(path.join(featureDir, `hooks/use${componentName}.${scriptExt}`), hookContent, "utf8");
|
|
119
|
+
fs.writeFileSync(path.join(featureDir, `index.${scriptExt}`), indexContent, "utf8");
|
|
120
|
+
console.log("\nšÆ successfully generated feature domain template structure:");
|
|
121
|
+
console.log(` āā src/features/${folderName}/`);
|
|
122
|
+
console.log(` āāā components/`);
|
|
123
|
+
console.log(` ā āāā ${componentName}.${ext}`);
|
|
124
|
+
console.log(` ā āāā ${componentName}.module.scss`);
|
|
125
|
+
console.log(` āāā hooks/`);
|
|
126
|
+
console.log(` āāā server/`);
|
|
127
|
+
console.log(` āāā actions.${scriptExt}\n`);
|
|
128
|
+
}
|
|
129
|
+
export function generateSharedComponent(appRoot, componentName, { logger, pc }) {
|
|
130
|
+
const kebabFolder = toKebabCase(componentName);
|
|
131
|
+
const pascalName = toPascalCase(componentName);
|
|
132
|
+
const isTypeScript = fs.existsSync(path.join(appRoot, "tsconfig.json"));
|
|
133
|
+
const ext = isTypeScript ? "tsx" : "jsx";
|
|
134
|
+
const compDir = path.resolve(appRoot, `./src/shared/components/${kebabFolder}`);
|
|
135
|
+
if (fs.existsSync(compDir)) {
|
|
136
|
+
logger.error(`generation halted: shared UI component folder "${kebabFolder}" already exists.`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
140
|
+
const componentContent = isTypeScript
|
|
141
|
+
? `import { children, JSX } from "solid-js";
|
|
142
|
+
import styles from "./${pascalName}.module.scss";
|
|
143
|
+
|
|
144
|
+
interface ${pascalName}Props {
|
|
145
|
+
children?: JSX.Element;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function ${pascalName}(props: ${pascalName}Props) {
|
|
149
|
+
// Safe evaluation wrapper preserves fine-grained reactive updates
|
|
150
|
+
const resolved = children(() => props.children);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div class={styles.base}>
|
|
154
|
+
{resolved()}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
`
|
|
159
|
+
: `import { children } from "solid-js";
|
|
160
|
+
import styles from "./${pascalName}.module.scss";
|
|
161
|
+
|
|
162
|
+
export function ${pascalName}(props) {
|
|
163
|
+
const resolved = children(() => props.children);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div class={styles.base}>
|
|
167
|
+
{resolved()}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
const cssContent = `.base {
|
|
173
|
+
display: inline-block;
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
fs.writeFileSync(path.join(compDir, `${pascalName}.${ext}`), componentContent, "utf8");
|
|
177
|
+
fs.writeFileSync(path.join(compDir, `${pascalName}.module.scss`), cssContent, "utf8");
|
|
178
|
+
logger.success(`\nš successfully generated global shared component capsule:`);
|
|
179
|
+
console.log(pc.dim(` āā src/shared/components/${kebabFolder}/`));
|
|
180
|
+
console.log(` āāā ${pc.cyan(`${pascalName}.${ext}`)}`);
|
|
181
|
+
console.log(` āāā ${pc.cyan(`${pascalName}.module.scss`)}\n`);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
export function scaffoldPage(rawName, appRoot) {
|
|
185
|
+
const isTypeScript = fs.existsSync(path.join(appRoot, "tsconfig.json"));
|
|
186
|
+
const ext = isTypeScript ? "tsx" : "jsx";
|
|
187
|
+
const scriptExt = isTypeScript ? "ts" : "js";
|
|
188
|
+
// rawName can be nested: "dashboard/settings" -> src/routes/dashboard/settings.tsx
|
|
189
|
+
// or dynamic: "blog/[slug]" -> src/routes/blog/[slug].tsx
|
|
190
|
+
const segments = rawName.replace(/\\/g, "/").split("/");
|
|
191
|
+
const fileName = segments[segments.length - 1];
|
|
192
|
+
const dirSegments = segments.slice(0, -1);
|
|
193
|
+
const routesDir = path.resolve(appRoot, "./src/routes");
|
|
194
|
+
const pageDir = dirSegments.length > 0 ? path.join(routesDir, ...dirSegments) : routesDir;
|
|
195
|
+
const pagePath = path.join(pageDir, `${fileName}.${ext}`);
|
|
196
|
+
const loaderTypePath = path.join(pageDir, `${fileName}.types.${scriptExt}`);
|
|
197
|
+
if (fs.existsSync(pagePath)) {
|
|
198
|
+
console.error(`[anaemia] generation halted: page "${rawName}" already exists at ${pagePath}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
fs.mkdirSync(pageDir, { recursive: true });
|
|
202
|
+
// derive a component name from the file name
|
|
203
|
+
const componentName = toPascalCase(fileName
|
|
204
|
+
.replace(/^\[\.\.\./, "") // strip [...
|
|
205
|
+
.replace(/^\[/, "") // strip [
|
|
206
|
+
.replace(/\]$/, "") // strip ]
|
|
207
|
+
) + "Page";
|
|
208
|
+
// derive the URL pattern for the comment header
|
|
209
|
+
const urlPattern = "/" +
|
|
210
|
+
segments
|
|
211
|
+
.map((s) => {
|
|
212
|
+
if (s.startsWith("[...") && s.endsWith("]"))
|
|
213
|
+
return `*${s.slice(4, -1)}`;
|
|
214
|
+
if (s.startsWith("[") && s.endsWith("]"))
|
|
215
|
+
return `:${s.slice(1, -1)}`;
|
|
216
|
+
return s;
|
|
217
|
+
})
|
|
218
|
+
.join("/");
|
|
219
|
+
const isCatchAll = fileName.startsWith("[...");
|
|
220
|
+
const isDynamic = fileName.startsWith("[") && !isCatchAll;
|
|
221
|
+
const paramName = isDynamic ? fileName.slice(1, -1) : isCatchAll ? fileName.slice(4, -1) : null;
|
|
222
|
+
const typesContent = isTypeScript
|
|
223
|
+
? `export interface ${componentName}LoaderData {
|
|
224
|
+
// TODO: define your loader return shape
|
|
225
|
+
}
|
|
226
|
+
`
|
|
227
|
+
: null;
|
|
228
|
+
const componentContent = isTypeScript
|
|
229
|
+
? `import type { ${componentName}LoaderData } from "./${fileName}.types";
|
|
230
|
+
import { useLoaderData } from "@anaemia/core";
|
|
231
|
+
|
|
232
|
+
// route: ${urlPattern}
|
|
233
|
+
${paramName ? `// param: ${paramName}` : ""}
|
|
234
|
+
|
|
235
|
+
export async function loader({ params${paramName ? `, request` : ""} }: { params: Record<string, string>; request: Request }) {
|
|
236
|
+
// TODO: fetch data here
|
|
237
|
+
return {} satisfies ${componentName}LoaderData;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export default function ${componentName}() {
|
|
241
|
+
const data = useLoaderData<${componentName}LoaderData>();
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<main>
|
|
245
|
+
<h1>${componentName}</h1>
|
|
246
|
+
</main>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
`
|
|
250
|
+
: `import { useLoaderData } from "@anaemia/core";
|
|
251
|
+
|
|
252
|
+
// route: ${urlPattern}
|
|
253
|
+
${paramName ? `// param: ${paramName}` : ""}
|
|
254
|
+
|
|
255
|
+
export async function loader({ params${paramName ? `, request` : ""} }) {
|
|
256
|
+
// TODO: fetch data here
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default function ${componentName}() {
|
|
261
|
+
const data = useLoaderData();
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<main>
|
|
265
|
+
<h1>${componentName}</h1>
|
|
266
|
+
</main>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
`;
|
|
270
|
+
fs.writeFileSync(pagePath, componentContent, "utf8");
|
|
271
|
+
if (isTypeScript && typesContent) {
|
|
272
|
+
fs.writeFileSync(loaderTypePath, typesContent, "utf8");
|
|
273
|
+
}
|
|
274
|
+
console.log("\nš successfully generated page route:");
|
|
275
|
+
console.log(` āā src/routes/${rawName}.${ext}`);
|
|
276
|
+
if (isTypeScript) {
|
|
277
|
+
console.log(` āāā ${fileName}.${ext}`);
|
|
278
|
+
console.log(` āāā ${fileName}.types.${scriptExt}\n`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export function scaffoldHook(rawName, appRoot) {
|
|
282
|
+
const isTypeScript = fs.existsSync(path.join(appRoot, "tsconfig.json"));
|
|
283
|
+
const ext = isTypeScript ? "ts" : "js";
|
|
284
|
+
// rawName can be:
|
|
285
|
+
// "auth/usePermissions" -> adds to existing feature
|
|
286
|
+
// "usePermissions" -> creates in src/shared/hooks
|
|
287
|
+
const segments = rawName.replace(/\\/g, "/").split("/");
|
|
288
|
+
const isFeatureHook = segments.length > 1;
|
|
289
|
+
const rawHookName = segments[segments.length - 1];
|
|
290
|
+
const featureName = isFeatureHook ? segments[0] : null;
|
|
291
|
+
// ensure it starts with "use"
|
|
292
|
+
const hookName = rawHookName.startsWith("use") ? rawHookName : `use${toPascalCase(rawHookName)}`;
|
|
293
|
+
const hookDir = isFeatureHook ? path.resolve(appRoot, `./src/features/${toKebabCase(featureName)}/hooks`) : path.resolve(appRoot, `./src/shared/hooks`);
|
|
294
|
+
const hookPath = path.join(hookDir, `${hookName}.${ext}`);
|
|
295
|
+
if (fs.existsSync(hookPath)) {
|
|
296
|
+
console.error(`[anaemia] generation halted: hook "${hookName}" already exists at ${hookPath}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
if (!fs.existsSync(hookDir)) {
|
|
300
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
// if adding to a feature, check the feature actually exists
|
|
303
|
+
if (isFeatureHook) {
|
|
304
|
+
const featureDir = path.resolve(appRoot, `./src/features/${toKebabCase(featureName)}`);
|
|
305
|
+
if (!fs.existsSync(featureDir)) {
|
|
306
|
+
console.error(`[anaemia] feature "${featureName}" does not exist. Run "create feature:${featureName}" first.`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const hookContent = isTypeScript
|
|
311
|
+
? `import { createSignal, onMount, onCleanup } from "solid-js";
|
|
312
|
+
|
|
313
|
+
interface ${toPascalCase(hookName)}Options {
|
|
314
|
+
// TODO: define options
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface ${toPascalCase(hookName)}Return {
|
|
318
|
+
// TODO: define return shape
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function ${hookName}(options?: ${toPascalCase(hookName)}Options): ${toPascalCase(hookName)}Return {
|
|
322
|
+
const [data, setData] = createSignal<unknown>(null);
|
|
323
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
324
|
+
const [loading, setLoading] = createSignal(false);
|
|
325
|
+
|
|
326
|
+
onMount(() => {
|
|
327
|
+
// TODO: setup side effects
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
onCleanup(() => {
|
|
331
|
+
// TODO: cleanup side effects
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return { data, error, loading };
|
|
335
|
+
}
|
|
336
|
+
`
|
|
337
|
+
: `import { createSignal, onMount, onCleanup } from "solid-js";
|
|
338
|
+
|
|
339
|
+
export function ${hookName}(options) {
|
|
340
|
+
const [data, setData] = createSignal(null);
|
|
341
|
+
const [error, setError] = createSignal(null);
|
|
342
|
+
const [loading, setLoading] = createSignal(false);
|
|
343
|
+
|
|
344
|
+
onMount(() => {
|
|
345
|
+
// TODO: setup side effects
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
onCleanup(() => {
|
|
349
|
+
// TODO: cleanup side effects
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return { data, error, loading };
|
|
353
|
+
}
|
|
354
|
+
`;
|
|
355
|
+
fs.writeFileSync(hookPath, hookContent, "utf8");
|
|
356
|
+
if (isFeatureHook) {
|
|
357
|
+
const indexPath = path.resolve(appRoot, `./src/features/${toKebabCase(featureName)}/index.${ext}`);
|
|
358
|
+
if (fs.existsSync(indexPath)) {
|
|
359
|
+
const existing = fs.readFileSync(indexPath, "utf8");
|
|
360
|
+
const exportLine = `export { ${hookName} } from "./hooks/${hookName}";\n`;
|
|
361
|
+
if (!existing.includes(exportLine)) {
|
|
362
|
+
fs.appendFileSync(indexPath, exportLine, "utf8");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const location = isFeatureHook ? `src/features/${toKebabCase(featureName)}/hooks/${hookName}.${ext}` : `src/shared/hooks/${hookName}.${ext}`;
|
|
367
|
+
console.log("\nšŖ successfully generated hook:");
|
|
368
|
+
console.log(` āā ${location}\n`);
|
|
369
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** converts any string (kebab, snake, camel) to kebab-case (e.g., "user-profile") */
|
|
2
|
+
export function toKebabCase(str) {
|
|
3
|
+
return str
|
|
4
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
5
|
+
.replace(/[\s_]+/g, "-")
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** converts any string to PascalCase (e.g., "UserProfile") */
|
|
9
|
+
export function toPascalCase(str) {
|
|
10
|
+
return toKebabCase(str)
|
|
11
|
+
.split("-")
|
|
12
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
13
|
+
.join("");
|
|
14
|
+
}
|
|
15
|
+
export function toCamelCase(str) {
|
|
16
|
+
const pascal = toPascalCase(str);
|
|
17
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anaemia/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"anaemia": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@rspack/core": "^2.0.4",
|
|
10
|
+
"cac": "^7.0.0",
|
|
11
|
+
"cross-spawn": "^7.0.6",
|
|
12
|
+
"jiti": "^2.7.0",
|
|
13
|
+
"picocolors": "^1.1.1",
|
|
14
|
+
"prompts": "^2.4.2",
|
|
15
|
+
"sucrase": "^3.35.1",
|
|
16
|
+
"ws": "^8.21.0",
|
|
17
|
+
"@anaemia/bundler": "0.0.1",
|
|
18
|
+
"@anaemia/core": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@rspack/dev-server": "^2.0.1",
|
|
22
|
+
"@types/cross-spawn": "^6.0.6",
|
|
23
|
+
"@types/fs-extra": "^11.0.4",
|
|
24
|
+
"@types/node": "^25.9.1",
|
|
25
|
+
"@types/prompts": "^2.4.9",
|
|
26
|
+
"@types/ws": "^8.18.1",
|
|
27
|
+
"fs-extra": "^11.3.5"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch"
|
|
32
|
+
}
|
|
33
|
+
}
|