@hypersonic-js/cli 0.2.0 β 0.2.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/README.md +13 -0
- package/dist/package.json +6 -6
- package/dist/src/commands/new/generate-files.d.ts +98 -0
- package/dist/src/commands/new/generate-files.d.ts.map +1 -0
- package/dist/src/commands/new/generate-files.js +79 -0
- package/dist/src/commands/new/generate-files.js.map +1 -0
- package/dist/src/commands/new/index.d.ts +31 -0
- package/dist/src/commands/new/index.d.ts.map +1 -0
- package/dist/src/commands/new/index.js +102 -0
- package/dist/src/commands/new/index.js.map +1 -0
- package/dist/src/commands/new/run-setup.d.ts +34 -0
- package/dist/src/commands/new/run-setup.d.ts.map +1 -0
- package/dist/src/commands/new/run-setup.js +70 -0
- package/dist/src/commands/new/run-setup.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/templates/new/.env.example +2 -0
- package/dist/templates/new/_env +2 -0
- package/dist/templates/new/eslint.config.js +17 -0
- package/dist/templates/new/hypersonic.config.ts +17 -0
- package/dist/templates/new/package.json +39 -0
- package/dist/templates/new/prisma/schema.prisma +71 -0
- package/dist/templates/new/prisma.config.ts +18 -0
- package/dist/templates/new/resources/css/app.css +1 -0
- package/dist/templates/new/resources/js/Pages/Auth/Login.tsx +96 -0
- package/dist/templates/new/resources/js/Pages/Auth/Register.tsx +107 -0
- package/dist/templates/new/resources/js/Pages/Welcome.tsx +54 -0
- package/dist/templates/new/resources/js/app.tsx +15 -0
- package/dist/templates/new/resources/js/lib/auth-client.ts +5 -0
- package/dist/templates/new/server.ts +71 -0
- package/dist/templates/new/src/middleware.ts +27 -0
- package/dist/templates/new/src/types.ts +17 -0
- package/dist/templates/new/tsconfig.json +14 -0
- package/dist/templates/new/types.ts +17 -0
- package/dist/templates/new/vite.config.ts +13 -0
- package/package.json +8 -8
package/README.md
ADDED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypersonic-js/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Hypersonic.js CLI β developer tooling for the Hypersonic framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": "
|
|
23
|
+
"node": ">=24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "tsc",
|
|
26
|
+
"build": "tsc && node --input-type=module --eval \"import{cpSync}from'node:fs';cpSync('templates','dist/templates',{recursive:true})\"",
|
|
27
27
|
"test": "vitest run",
|
|
28
28
|
"test:coverage": "vitest run --coverage",
|
|
29
29
|
"type-check": "tsc --noEmit",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"@prisma/adapter-pg": "7.8.0",
|
|
42
42
|
"@prisma/client": "7.8.0",
|
|
43
43
|
"@types/node": "25.9.3",
|
|
44
|
-
"@vitest/coverage-v8": "4.1.
|
|
45
|
-
"better-auth": "1.6.
|
|
44
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
45
|
+
"better-auth": "1.6.19",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.9"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface GenerateFilesOptions {
|
|
2
|
+
projectDir: string;
|
|
3
|
+
projectName: string;
|
|
4
|
+
secret: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WrittenFile {
|
|
7
|
+
/** Destination path relative to the project root (e.g. 'prisma/schema.prisma'). */
|
|
8
|
+
dest: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Injectable I/O deps so unit tests never touch the real filesystem.
|
|
12
|
+
* templatesDir lets tests point at a fake templates directory.
|
|
13
|
+
*/
|
|
14
|
+
export interface GenerateFilesDeps {
|
|
15
|
+
readFile: (filePath: string) => string;
|
|
16
|
+
mkdir: (dirPath: string) => void;
|
|
17
|
+
writeFile: (filePath: string, content: string) => void;
|
|
18
|
+
templatesDir: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Static list of every file the `new` command writes.
|
|
22
|
+
*
|
|
23
|
+
* `src` β filename inside templates/new/ (relative, may differ from dest)
|
|
24
|
+
* `dest` β filename written into the project (relative to projectDir)
|
|
25
|
+
*
|
|
26
|
+
* The only srcβdest rename is `_env` β `.env`: the root .gitignore has a bare
|
|
27
|
+
* `.env` pattern that matches files at any depth, so the template is stored
|
|
28
|
+
* under a neutral name and renamed on write.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TEMPLATE_FILES: readonly [{
|
|
31
|
+
readonly src: "package.json";
|
|
32
|
+
readonly dest: "package.json";
|
|
33
|
+
}, {
|
|
34
|
+
readonly src: "hypersonic.config.ts";
|
|
35
|
+
readonly dest: "hypersonic.config.ts";
|
|
36
|
+
}, {
|
|
37
|
+
readonly src: "_env";
|
|
38
|
+
readonly dest: ".env";
|
|
39
|
+
}, {
|
|
40
|
+
readonly src: ".env.example";
|
|
41
|
+
readonly dest: ".env.example";
|
|
42
|
+
}, {
|
|
43
|
+
readonly src: ".gitignore";
|
|
44
|
+
readonly dest: ".gitignore";
|
|
45
|
+
}, {
|
|
46
|
+
readonly src: "tsconfig.json";
|
|
47
|
+
readonly dest: "tsconfig.json";
|
|
48
|
+
}, {
|
|
49
|
+
readonly src: "eslint.config.js";
|
|
50
|
+
readonly dest: "eslint.config.js";
|
|
51
|
+
}, {
|
|
52
|
+
readonly src: "vite.config.ts";
|
|
53
|
+
readonly dest: "vite.config.ts";
|
|
54
|
+
}, {
|
|
55
|
+
readonly src: "prisma/schema.prisma";
|
|
56
|
+
readonly dest: "prisma/schema.prisma";
|
|
57
|
+
}, {
|
|
58
|
+
readonly src: "prisma.config.ts";
|
|
59
|
+
readonly dest: "prisma.config.ts";
|
|
60
|
+
}, {
|
|
61
|
+
readonly src: "server.ts";
|
|
62
|
+
readonly dest: "server.ts";
|
|
63
|
+
}, {
|
|
64
|
+
readonly src: "src/types.ts";
|
|
65
|
+
readonly dest: "src/types.ts";
|
|
66
|
+
}, {
|
|
67
|
+
readonly src: "src/middleware.ts";
|
|
68
|
+
readonly dest: "src/middleware.ts";
|
|
69
|
+
}, {
|
|
70
|
+
readonly src: "resources/css/app.css";
|
|
71
|
+
readonly dest: "resources/css/app.css";
|
|
72
|
+
}, {
|
|
73
|
+
readonly src: "resources/js/app.tsx";
|
|
74
|
+
readonly dest: "resources/js/app.tsx";
|
|
75
|
+
}, {
|
|
76
|
+
readonly src: "resources/js/lib/auth-client.ts";
|
|
77
|
+
readonly dest: "resources/js/lib/auth-client.ts";
|
|
78
|
+
}, {
|
|
79
|
+
readonly src: "resources/js/Pages/Welcome.tsx";
|
|
80
|
+
readonly dest: "resources/js/Pages/Welcome.tsx";
|
|
81
|
+
}, {
|
|
82
|
+
readonly src: "resources/js/Pages/Auth/Login.tsx";
|
|
83
|
+
readonly dest: "resources/js/Pages/Auth/Login.tsx";
|
|
84
|
+
}, {
|
|
85
|
+
readonly src: "resources/js/Pages/Auth/Register.tsx";
|
|
86
|
+
readonly dest: "resources/js/Pages/Auth/Register.tsx";
|
|
87
|
+
}];
|
|
88
|
+
/**
|
|
89
|
+
* Replaces every `{{KEY}}` placeholder in `content` with the corresponding
|
|
90
|
+
* value from `vars`. Unknown placeholders are left untouched.
|
|
91
|
+
*/
|
|
92
|
+
export declare function applySubstitutions(content: string, vars: Record<string, string>): string;
|
|
93
|
+
/**
|
|
94
|
+
* Reads every template file, applies placeholder substitution, and writes it
|
|
95
|
+
* into the target project directory.
|
|
96
|
+
*/
|
|
97
|
+
export declare function generateFiles(opts: GenerateFilesOptions, deps?: GenerateFilesDeps): Promise<WrittenFile[]>;
|
|
98
|
+
//# sourceMappingURL=generate-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../../../src/commands/new/generate-files.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACtD,YAAY,EAAE,MAAM,CAAA;CACrB;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBjB,CAAA;AAIV;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,MAAM,CAIR;AAkBD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,oBAAoB,EAC1B,IAAI,GAAE,iBAAqC,GAC1C,OAAO,CAAC,WAAW,EAAE,CAAC,CAyBxB"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// ββ Template file manifest βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
5
|
+
/**
|
|
6
|
+
* Static list of every file the `new` command writes.
|
|
7
|
+
*
|
|
8
|
+
* `src` β filename inside templates/new/ (relative, may differ from dest)
|
|
9
|
+
* `dest` β filename written into the project (relative to projectDir)
|
|
10
|
+
*
|
|
11
|
+
* The only srcβdest rename is `_env` β `.env`: the root .gitignore has a bare
|
|
12
|
+
* `.env` pattern that matches files at any depth, so the template is stored
|
|
13
|
+
* under a neutral name and renamed on write.
|
|
14
|
+
*/
|
|
15
|
+
export const TEMPLATE_FILES = [
|
|
16
|
+
{ src: 'package.json', dest: 'package.json' },
|
|
17
|
+
{ src: 'hypersonic.config.ts', dest: 'hypersonic.config.ts' },
|
|
18
|
+
{ src: '_env', dest: '.env' },
|
|
19
|
+
{ src: '.env.example', dest: '.env.example' },
|
|
20
|
+
{ src: '.gitignore', dest: '.gitignore' },
|
|
21
|
+
{ src: 'tsconfig.json', dest: 'tsconfig.json' },
|
|
22
|
+
{ src: 'eslint.config.js', dest: 'eslint.config.js' },
|
|
23
|
+
{ src: 'vite.config.ts', dest: 'vite.config.ts' },
|
|
24
|
+
{ src: 'prisma/schema.prisma', dest: 'prisma/schema.prisma' },
|
|
25
|
+
{ src: 'prisma.config.ts', dest: 'prisma.config.ts' },
|
|
26
|
+
{ src: 'server.ts', dest: 'server.ts' },
|
|
27
|
+
{ src: 'src/types.ts', dest: 'src/types.ts' },
|
|
28
|
+
{ src: 'src/middleware.ts', dest: 'src/middleware.ts' },
|
|
29
|
+
{ src: 'resources/css/app.css', dest: 'resources/css/app.css' },
|
|
30
|
+
{ src: 'resources/js/app.tsx', dest: 'resources/js/app.tsx' },
|
|
31
|
+
{ src: 'resources/js/lib/auth-client.ts', dest: 'resources/js/lib/auth-client.ts' },
|
|
32
|
+
{ src: 'resources/js/Pages/Welcome.tsx', dest: 'resources/js/Pages/Welcome.tsx' },
|
|
33
|
+
{ src: 'resources/js/Pages/Auth/Login.tsx', dest: 'resources/js/Pages/Auth/Login.tsx' },
|
|
34
|
+
{ src: 'resources/js/Pages/Auth/Register.tsx', dest: 'resources/js/Pages/Auth/Register.tsx' },
|
|
35
|
+
];
|
|
36
|
+
// ββ Substitution βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
37
|
+
/**
|
|
38
|
+
* Replaces every `{{KEY}}` placeholder in `content` with the corresponding
|
|
39
|
+
* value from `vars`. Unknown placeholders are left untouched.
|
|
40
|
+
*/
|
|
41
|
+
export function applySubstitutions(content, vars) {
|
|
42
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => key in vars ? (vars[key] ?? match) : match);
|
|
43
|
+
}
|
|
44
|
+
// ββ Default deps βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
45
|
+
function makeDefaultDeps() {
|
|
46
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
47
|
+
const __dirname = dirname(__filename);
|
|
48
|
+
return {
|
|
49
|
+
readFile: (filePath) => readFileSync(filePath, 'utf-8'),
|
|
50
|
+
mkdir: (dirPath) => mkdirSync(dirPath, { recursive: true }),
|
|
51
|
+
writeFile: (filePath, content) => writeFileSync(filePath, content, 'utf-8'),
|
|
52
|
+
templatesDir: join(__dirname, '../../../templates/new'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ββ generateFiles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
56
|
+
/**
|
|
57
|
+
* Reads every template file, applies placeholder substitution, and writes it
|
|
58
|
+
* into the target project directory.
|
|
59
|
+
*/
|
|
60
|
+
export async function generateFiles(opts, deps = makeDefaultDeps()) {
|
|
61
|
+
const { projectDir, projectName, secret } = opts;
|
|
62
|
+
const { readFile, mkdir, writeFile, templatesDir } = deps;
|
|
63
|
+
const vars = {
|
|
64
|
+
PROJECT_NAME: projectName,
|
|
65
|
+
SECRET: secret,
|
|
66
|
+
};
|
|
67
|
+
const written = [];
|
|
68
|
+
for (const { src, dest } of TEMPLATE_FILES) {
|
|
69
|
+
const srcPath = join(templatesDir, src);
|
|
70
|
+
const destPath = join(projectDir, dest);
|
|
71
|
+
const raw = readFile(srcPath);
|
|
72
|
+
const content = applySubstitutions(raw, vars);
|
|
73
|
+
mkdir(dirname(destPath));
|
|
74
|
+
writeFile(destPath, content);
|
|
75
|
+
written.push({ dest });
|
|
76
|
+
}
|
|
77
|
+
return written;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=generate-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-files.js","sourceRoot":"","sources":["../../../../src/commands/new/generate-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type PromptFn } from '../../utils/prompt.js';
|
|
3
|
+
import { generateFiles } from './generate-files.js';
|
|
4
|
+
import { runSetup } from './run-setup.js';
|
|
5
|
+
export interface NewCommandDeps {
|
|
6
|
+
prompt: PromptFn;
|
|
7
|
+
readdirSync: (path: string) => string[];
|
|
8
|
+
mkdirSync: (path: string, opts: {
|
|
9
|
+
recursive: boolean;
|
|
10
|
+
}) => void;
|
|
11
|
+
generateFiles: typeof generateFiles;
|
|
12
|
+
runSetup: typeof runSetup;
|
|
13
|
+
randomBytes: (size: number) => Buffer;
|
|
14
|
+
cwd: () => string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registers the `hypersonic new` top-level command.
|
|
18
|
+
*
|
|
19
|
+
* Fully interactive β no arguments accepted. Prompts:
|
|
20
|
+
* 1. Directory choice (new subdirectory | current directory)
|
|
21
|
+
* 2. Warning + confirm if current directory is not empty (current dir only)
|
|
22
|
+
* 3. Project name (always required)
|
|
23
|
+
* 4. Warning + confirm if new subdirectory already exists (new dir only)
|
|
24
|
+
*
|
|
25
|
+
* Then generates all project files, runs npm install, runs Prisma migrations,
|
|
26
|
+
* scaffolds the admin dashboard, generates admin metadata, and finally runs
|
|
27
|
+
* `hypersonic admin create-admin` interactively so the user creates their
|
|
28
|
+
* first admin account before the command exits.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerNewCommand(program: Command, deps?: NewCommandDeps): void;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/new/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAIzC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,QAAQ,CAAA;IAChB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;IACvC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/D,aAAa,EAAE,OAAO,aAAa,CAAA;IACnC,QAAQ,EAAE,OAAO,QAAQ,CAAA;IACzB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAkBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE,cAA2B,GAChC,IAAI,CAoFN"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readdirSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { prompt as defaultPrompt } from '../../utils/prompt.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { generateFiles } from './generate-files.js';
|
|
7
|
+
import { runSetup } from './run-setup.js';
|
|
8
|
+
// ββ Dependency loader ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
9
|
+
function loadDeps() {
|
|
10
|
+
return {
|
|
11
|
+
prompt: defaultPrompt,
|
|
12
|
+
readdirSync,
|
|
13
|
+
mkdirSync: (p, opts) => mkdirSync(p, opts),
|
|
14
|
+
generateFiles,
|
|
15
|
+
runSetup,
|
|
16
|
+
randomBytes,
|
|
17
|
+
cwd: () => process.cwd(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// ββ Registration βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
21
|
+
/**
|
|
22
|
+
* Registers the `hypersonic new` top-level command.
|
|
23
|
+
*
|
|
24
|
+
* Fully interactive β no arguments accepted. Prompts:
|
|
25
|
+
* 1. Directory choice (new subdirectory | current directory)
|
|
26
|
+
* 2. Warning + confirm if current directory is not empty (current dir only)
|
|
27
|
+
* 3. Project name (always required)
|
|
28
|
+
* 4. Warning + confirm if new subdirectory already exists (new dir only)
|
|
29
|
+
*
|
|
30
|
+
* Then generates all project files, runs npm install, runs Prisma migrations,
|
|
31
|
+
* scaffolds the admin dashboard, generates admin metadata, and finally runs
|
|
32
|
+
* `hypersonic admin create-admin` interactively so the user creates their
|
|
33
|
+
* first admin account before the command exits.
|
|
34
|
+
*/
|
|
35
|
+
export function registerNewCommand(program, deps = loadDeps()) {
|
|
36
|
+
program
|
|
37
|
+
.command('new')
|
|
38
|
+
.description('Scaffold a new Hypersonic.js project interactively')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
const { prompt, readdirSync: readdir, mkdirSync: mkdir, generateFiles: doGenerateFiles, runSetup: doRunSetup, randomBytes: rb, cwd, } = deps;
|
|
41
|
+
// ββ 1. Directory choice ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
logger.info('Where would you like to create your project?');
|
|
43
|
+
logger.info(' 1. Create a new directory');
|
|
44
|
+
logger.info(' 2. Use current directory');
|
|
45
|
+
const dirChoice = await prompt('Choice [1]: ');
|
|
46
|
+
const useCurrentDir = dirChoice.trim() === '2';
|
|
47
|
+
// ββ 2. Warn if current directory is not empty ββββββββββββββββββββββββ
|
|
48
|
+
if (useCurrentDir) {
|
|
49
|
+
const existing = readdir(cwd());
|
|
50
|
+
if (existing.length > 0) {
|
|
51
|
+
logger.warn(`Current directory is not empty (${existing.length} file(s) found).`);
|
|
52
|
+
const confirm = await prompt('Continue anyway? [y/N]: ');
|
|
53
|
+
if (confirm.trim().toLowerCase() !== 'y') {
|
|
54
|
+
logger.info('Aborted.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ββ 3. Project name (mandatory) ββββββββββββββββββββββββββββββββββββββ
|
|
60
|
+
const rawName = await prompt('Project name: ');
|
|
61
|
+
const projectName = rawName.trim();
|
|
62
|
+
if (!projectName) {
|
|
63
|
+
logger.error('Project name is required.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
// ββ 4. Resolve project directory βββββββββββββββββββββββββββββββββββββ
|
|
67
|
+
const projectDir = useCurrentDir ? cwd() : join(cwd(), projectName);
|
|
68
|
+
if (!useCurrentDir) {
|
|
69
|
+
// Guard: warn if the target subdirectory already exists and is non-empty.
|
|
70
|
+
// A thrown error means the directory does not exist yet β that is fine.
|
|
71
|
+
try {
|
|
72
|
+
const existing = readdir(projectDir);
|
|
73
|
+
if (existing.length > 0) {
|
|
74
|
+
logger.warn(`Directory "${projectName}" already exists and is not empty (${existing.length} file(s) found).`);
|
|
75
|
+
const confirm = await prompt('Continue anyway? [y/N]: ');
|
|
76
|
+
if (confirm.trim().toLowerCase() !== 'y') {
|
|
77
|
+
logger.info('Aborted.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Directory does not exist yet β mkdirSync will create it below.
|
|
84
|
+
}
|
|
85
|
+
mkdir(projectDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
// ββ 5. Generate project files βββββββββββββββββββββββββββββββββββββββββ
|
|
88
|
+
const secret = rb(32).toString('hex');
|
|
89
|
+
logger.info(`\nCreating project in ${projectDir}β¦`);
|
|
90
|
+
await doGenerateFiles({ projectDir, projectName, secret });
|
|
91
|
+
logger.success('Project files written.');
|
|
92
|
+
// ββ 6. Run setup steps ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
93
|
+
await doRunSetup({ projectDir });
|
|
94
|
+
// ββ 7. Done βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
95
|
+
logger.success('\nYour project is ready!');
|
|
96
|
+
if (!useCurrentDir) {
|
|
97
|
+
logger.info(`\n cd ${projectName}`);
|
|
98
|
+
}
|
|
99
|
+
logger.info(' npm run dev\n');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/commands/new/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface RunSetupOptions {
|
|
2
|
+
projectDir: string;
|
|
3
|
+
}
|
|
4
|
+
/** Runs a shell command with the given working directory. */
|
|
5
|
+
export type ExecFn = (command: string, cwd: string) => void;
|
|
6
|
+
export interface RunSetupDeps {
|
|
7
|
+
exec: ExecFn;
|
|
8
|
+
scaffoldAdmin: (opts: {
|
|
9
|
+
targetDir: string;
|
|
10
|
+
force: boolean;
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
written: string[];
|
|
13
|
+
skipped: string[];
|
|
14
|
+
}>;
|
|
15
|
+
generateAdminMeta: (schemaPath: string, outputPath: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs all post-generation setup steps in order:
|
|
19
|
+
*
|
|
20
|
+
* 1. npm install β installs project dependencies
|
|
21
|
+
* 2. prisma migrate dev β creates the SQLite database and runs migrations
|
|
22
|
+
* 3. prisma generate β generates the Prisma client from the schema
|
|
23
|
+
* 4. admin scaffold β copies admin React page components into the project
|
|
24
|
+
* 5. admin generate-meta β generates prisma/admin-meta.json from the schema
|
|
25
|
+
* 6. admin create-admin β interactive: the user creates their admin account
|
|
26
|
+
*
|
|
27
|
+
* Steps 1β3 run as child processes (stdio: inherit so the user sees output).
|
|
28
|
+
* Steps 4β5 call the existing CLI functions directly to avoid a second spawn.
|
|
29
|
+
* Step 6 spawns `hypersonic admin create-admin` in the project directory so
|
|
30
|
+
* it resolves its own node_modules (better-auth, @prisma/client, etc.) from
|
|
31
|
+
* the newly-installed project rather than from the CLI's install location.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runSetup(opts: RunSetupOptions, deps?: RunSetupDeps): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=run-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-setup.d.ts","sourceRoot":"","sources":["../../../../src/commands/new/run-setup.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,6DAA6D;AAC7D,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;AAE3D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC;QACtE,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,EAAE,MAAM,EAAE,CAAA;KAClB,CAAC,CAAA;IACF,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7E;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,eAAe,EACrB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { scaffoldAdmin } from '@hypersonic-js/admin';
|
|
5
|
+
import { runGenerateMeta } from '../admin/generate-meta.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
// ββ Dependency loader ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
8
|
+
async function loadDeps() {
|
|
9
|
+
const { getDMMF } = await import('@prisma/get-dmmf');
|
|
10
|
+
return {
|
|
11
|
+
exec: (command, cwd) => execSync(command, { cwd, stdio: 'inherit' }),
|
|
12
|
+
scaffoldAdmin,
|
|
13
|
+
generateAdminMeta: async (schemaPath, outputPath) => {
|
|
14
|
+
await runGenerateMeta({ schema: schemaPath, output: outputPath }, {
|
|
15
|
+
getDMMF,
|
|
16
|
+
readFile: (p) => readFileSync(p, 'utf-8'),
|
|
17
|
+
writeFile: (p, c) => writeFileSync(p, c),
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
23
|
+
/**
|
|
24
|
+
* Runs all post-generation setup steps in order:
|
|
25
|
+
*
|
|
26
|
+
* 1. npm install β installs project dependencies
|
|
27
|
+
* 2. prisma migrate dev β creates the SQLite database and runs migrations
|
|
28
|
+
* 3. prisma generate β generates the Prisma client from the schema
|
|
29
|
+
* 4. admin scaffold β copies admin React page components into the project
|
|
30
|
+
* 5. admin generate-meta β generates prisma/admin-meta.json from the schema
|
|
31
|
+
* 6. admin create-admin β interactive: the user creates their admin account
|
|
32
|
+
*
|
|
33
|
+
* Steps 1β3 run as child processes (stdio: inherit so the user sees output).
|
|
34
|
+
* Steps 4β5 call the existing CLI functions directly to avoid a second spawn.
|
|
35
|
+
* Step 6 spawns `hypersonic admin create-admin` in the project directory so
|
|
36
|
+
* it resolves its own node_modules (better-auth, @prisma/client, etc.) from
|
|
37
|
+
* the newly-installed project rather than from the CLI's install location.
|
|
38
|
+
*/
|
|
39
|
+
export async function runSetup(opts, deps) {
|
|
40
|
+
// `await` cannot be used in a default parameter expression of an async
|
|
41
|
+
// function β it is a syntax error. Resolve lazily inside the body instead.
|
|
42
|
+
const { exec, scaffoldAdmin: doScaffold, generateAdminMeta } = deps ?? (await loadDeps());
|
|
43
|
+
const { projectDir } = opts;
|
|
44
|
+
const pagesDir = join(projectDir, 'resources/js/Pages');
|
|
45
|
+
const schemaPath = join(projectDir, 'prisma/schema.prisma');
|
|
46
|
+
const metaPath = join(projectDir, 'prisma/admin-meta.json');
|
|
47
|
+
// ββ 1. Install dependencies ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
48
|
+
logger.info('Installing dependenciesβ¦');
|
|
49
|
+
exec('npm install', projectDir);
|
|
50
|
+
// ββ 2. Run database migrations βββββββββββββββββββββββββββββββββββββββββββ
|
|
51
|
+
logger.info('Running database migrationsβ¦');
|
|
52
|
+
exec('npx prisma migrate dev --name init', projectDir);
|
|
53
|
+
// ββ 3. Generate Prisma client ββββββββββββββββββββββββββββββββββββββββββββ
|
|
54
|
+
logger.info('Generating Prisma clientβ¦');
|
|
55
|
+
exec('npx prisma generate', projectDir);
|
|
56
|
+
// ββ 4. Scaffold admin pages ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
57
|
+
logger.info('Scaffolding admin pagesβ¦');
|
|
58
|
+
const scaffoldResult = await doScaffold({ targetDir: pagesDir, force: false });
|
|
59
|
+
for (const file of scaffoldResult.written) {
|
|
60
|
+
logger.success(`Written resources/js/Pages/Admin/${file}`);
|
|
61
|
+
}
|
|
62
|
+
// ββ 5. Generate admin metadata βββββββββββββββββββββββββββββββββββββββββββ
|
|
63
|
+
logger.info('Generating admin metadataβ¦');
|
|
64
|
+
await generateAdminMeta(schemaPath, metaPath);
|
|
65
|
+
logger.success('Admin meta written to prisma/admin-meta.json');
|
|
66
|
+
// ββ 6. Create admin user (interactive subprocess) ββββββββββββββββββββββββ
|
|
67
|
+
logger.info('Creating your admin accountβ¦');
|
|
68
|
+
exec('npx hypersonic admin create-admin', projectDir);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=run-setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-setup.js","sourceRoot":"","sources":["../../../../src/commands/new/run-setup.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKnC,eAAO,MAAM,WAAW,EAAE,MAAoB,CAAA;AAE9C;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAUvC"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { registerAdminCommands } from './commands/admin/index.js';
|
|
3
|
+
import { registerNewCommand } from './commands/new/index.js';
|
|
3
4
|
import pkg from '../package.json' with { type: 'json' };
|
|
4
5
|
export const CLI_VERSION = pkg.version;
|
|
5
6
|
/**
|
|
@@ -13,6 +14,7 @@ export function createProgram() {
|
|
|
13
14
|
.description('Hypersonic.js framework CLI')
|
|
14
15
|
.version(CLI_VERSION, '-v, --version', 'Print the CLI version');
|
|
15
16
|
registerAdminCommands(program);
|
|
17
|
+
registerNewCommand(program);
|
|
16
18
|
return program;
|
|
17
19
|
}
|
|
18
20
|
//# sourceMappingURL=index.js.map
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAEvD,MAAM,CAAC,MAAM,WAAW,GAAW,GAAG,CAAC,OAAO,CAAA;AAE9C;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE;SAC1B,IAAI,CAAC,YAAY,CAAC;SAClB,WAAW,CAAC,6BAA6B,CAAC;SAC1C,OAAO,CAAC,WAAW,EAAE,eAAe,EAAE,uBAAuB,CAAC,CAAA;IAEjE,qBAAqB,CAAC,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAEvD,MAAM,CAAC,MAAM,WAAW,GAAW,GAAG,CAAC,OAAO,CAAA;AAE9C;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE;SAC1B,IAAI,CAAC,YAAY,CAAC;SAClB,WAAW,CAAC,6BAA6B,CAAC;SAC1C,OAAO,CAAC,WAAW,EAAE,eAAe,EAAE,uBAAuB,CAAC,CAAA;IAEjE,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAE3B,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
{ ignores: ['public/', 'node_modules/'] },
|
|
7
|
+
{
|
|
8
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
9
|
+
files: ['**/*.{ts,tsx}'],
|
|
10
|
+
plugins: {
|
|
11
|
+
'react-hooks': reactHooks,
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
...reactHooks.configs.recommended.rules,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from '@hypersonic-js/complete'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
server: {
|
|
5
|
+
port: 3000,
|
|
6
|
+
host: 'localhost',
|
|
7
|
+
},
|
|
8
|
+
database: {
|
|
9
|
+
provider: 'sqlite',
|
|
10
|
+
},
|
|
11
|
+
auth: {
|
|
12
|
+
trustedOrigins: ['http://localhost:3000'],
|
|
13
|
+
},
|
|
14
|
+
inertia: {
|
|
15
|
+
ssr: false,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "node --experimental-strip-types server.ts",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"db:migrate": "prisma migrate dev",
|
|
10
|
+
"db:generate": "prisma generate"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@hypersonic-js/complete": "0.2.0",
|
|
14
|
+
"@prisma/adapter-better-sqlite3": "7.8.0",
|
|
15
|
+
"better-sqlite3": "12.11.1",
|
|
16
|
+
"dotenv": "17.4.2",
|
|
17
|
+
"@prisma/client": "7.8.0",
|
|
18
|
+
"better-auth": "1.6.19"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/js": "10.0.1",
|
|
22
|
+
"@hypersonic-js/cli": "0.2.0",
|
|
23
|
+
"@inertiajs/react": "3.4.0",
|
|
24
|
+
"@tailwindcss/vite": "4.3.1",
|
|
25
|
+
"@types/better-sqlite3": "7.6.13",
|
|
26
|
+
"@types/node": "25.9.3",
|
|
27
|
+
"@types/react": "19.2.17",
|
|
28
|
+
"@types/react-dom": "19.2.3",
|
|
29
|
+
"eslint": "10.5.0",
|
|
30
|
+
"eslint-plugin-react-hooks": "7.1.1",
|
|
31
|
+
"prisma": "7.8.0",
|
|
32
|
+
"react": "19.2.7",
|
|
33
|
+
"react-dom": "19.2.7",
|
|
34
|
+
"tailwindcss": "4.3.1",
|
|
35
|
+
"typescript": "6.0.3",
|
|
36
|
+
"typescript-eslint": "8.61.1",
|
|
37
|
+
"vite": "8.0.16"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "sqlite"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ββ Required by Better Auth ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
10
|
+
|
|
11
|
+
model User {
|
|
12
|
+
id String @id
|
|
13
|
+
name String
|
|
14
|
+
email String @unique
|
|
15
|
+
emailVerified Boolean
|
|
16
|
+
image String?
|
|
17
|
+
createdAt DateTime
|
|
18
|
+
updatedAt DateTime
|
|
19
|
+
// Better Auth admin plugin fields
|
|
20
|
+
role Role @default(user)
|
|
21
|
+
banned Boolean @default(false)
|
|
22
|
+
banReason String?
|
|
23
|
+
banExpires DateTime?
|
|
24
|
+
sessions Session[]
|
|
25
|
+
accounts Account[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
enum Role {
|
|
29
|
+
user
|
|
30
|
+
admin
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
model Session {
|
|
34
|
+
id String @id
|
|
35
|
+
expiresAt DateTime
|
|
36
|
+
token String @unique
|
|
37
|
+
createdAt DateTime
|
|
38
|
+
updatedAt DateTime
|
|
39
|
+
ipAddress String?
|
|
40
|
+
userAgent String?
|
|
41
|
+
userId String
|
|
42
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
model Account {
|
|
46
|
+
id String @id
|
|
47
|
+
accountId String
|
|
48
|
+
providerId String
|
|
49
|
+
userId String
|
|
50
|
+
accessToken String?
|
|
51
|
+
refreshToken String?
|
|
52
|
+
idToken String?
|
|
53
|
+
accessTokenExpiresAt DateTime?
|
|
54
|
+
refreshTokenExpiresAt DateTime?
|
|
55
|
+
scope String?
|
|
56
|
+
password String?
|
|
57
|
+
createdAt DateTime
|
|
58
|
+
updatedAt DateTime
|
|
59
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
model Verification {
|
|
63
|
+
id String @id
|
|
64
|
+
identifier String
|
|
65
|
+
value String
|
|
66
|
+
expiresAt DateTime
|
|
67
|
+
createdAt DateTime?
|
|
68
|
+
updatedAt DateTime?
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ββ Your models go here ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import { defineConfig } from 'prisma/config'
|
|
3
|
+
|
|
4
|
+
if (!process.env.DATABASE_URL) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
'DATABASE_URL is not defined. Please add it to your .env file before running Prisma commands.',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
schema: 'prisma/schema.prisma',
|
|
12
|
+
migrations: {
|
|
13
|
+
path: 'prisma/migrations',
|
|
14
|
+
},
|
|
15
|
+
datasource: {
|
|
16
|
+
url: process.env.DATABASE_URL,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { router } from '@inertiajs/react'
|
|
3
|
+
import { authClient } from '../../lib/auth-client'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
error?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Login({ error }: Props) {
|
|
10
|
+
const [email, setEmail] = useState('')
|
|
11
|
+
const [password, setPassword] = useState('')
|
|
12
|
+
const [submitting, setSubmitting] = useState(false)
|
|
13
|
+
const [formError, setFormError] = useState(error ?? '')
|
|
14
|
+
|
|
15
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
setSubmitting(true)
|
|
18
|
+
setFormError('')
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await authClient.signIn.email({
|
|
22
|
+
email,
|
|
23
|
+
password,
|
|
24
|
+
callbackURL: '/',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (result.error) {
|
|
28
|
+
setFormError(result.error.message ?? 'Invalid email or password')
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
router.visit('/')
|
|
33
|
+
} catch {
|
|
34
|
+
setFormError('An unexpected error occurred. Please try again.')
|
|
35
|
+
} finally {
|
|
36
|
+
setSubmitting(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
42
|
+
<div className="w-full max-w-sm bg-white rounded-2xl shadow p-8">
|
|
43
|
+
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Sign in</h1>
|
|
44
|
+
|
|
45
|
+
{formError && (
|
|
46
|
+
<p className="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
47
|
+
{formError}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
52
|
+
<div>
|
|
53
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
54
|
+
Email
|
|
55
|
+
</label>
|
|
56
|
+
<input
|
|
57
|
+
type="email"
|
|
58
|
+
value={email}
|
|
59
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
60
|
+
required
|
|
61
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div>
|
|
66
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
67
|
+
Password
|
|
68
|
+
</label>
|
|
69
|
+
<input
|
|
70
|
+
type="password"
|
|
71
|
+
value={password}
|
|
72
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
73
|
+
required
|
|
74
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
type="submit"
|
|
80
|
+
disabled={submitting}
|
|
81
|
+
className="w-full rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
|
|
82
|
+
>
|
|
83
|
+
{submitting ? 'Signing inβ¦' : 'Sign in'}
|
|
84
|
+
</button>
|
|
85
|
+
</form>
|
|
86
|
+
|
|
87
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
88
|
+
No account?{' '}
|
|
89
|
+
<a href="/register" className="text-indigo-600 hover:underline">
|
|
90
|
+
Create one
|
|
91
|
+
</a>
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { router } from '@inertiajs/react'
|
|
3
|
+
import { authClient } from '../../lib/auth-client'
|
|
4
|
+
|
|
5
|
+
export default function Register() {
|
|
6
|
+
const [name, setName] = useState('')
|
|
7
|
+
const [email, setEmail] = useState('')
|
|
8
|
+
const [password, setPassword] = useState('')
|
|
9
|
+
const [submitting, setSubmitting] = useState(false)
|
|
10
|
+
const [formError, setFormError] = useState('')
|
|
11
|
+
|
|
12
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
13
|
+
e.preventDefault()
|
|
14
|
+
setSubmitting(true)
|
|
15
|
+
setFormError('')
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await authClient.signUp.email({
|
|
19
|
+
name,
|
|
20
|
+
email,
|
|
21
|
+
password,
|
|
22
|
+
callbackURL: '/',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (result.error) {
|
|
26
|
+
setFormError(result.error.message ?? 'Could not create account')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
router.visit('/')
|
|
31
|
+
} catch {
|
|
32
|
+
setFormError('An unexpected error occurred. Please try again.')
|
|
33
|
+
} finally {
|
|
34
|
+
setSubmitting(false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
40
|
+
<div className="w-full max-w-sm bg-white rounded-2xl shadow p-8">
|
|
41
|
+
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Create account</h1>
|
|
42
|
+
|
|
43
|
+
{formError && (
|
|
44
|
+
<p className="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
45
|
+
{formError}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
50
|
+
<div>
|
|
51
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
52
|
+
Name
|
|
53
|
+
</label>
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
value={name}
|
|
57
|
+
onChange={(e) => setName(e.target.value)}
|
|
58
|
+
required
|
|
59
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
65
|
+
Email
|
|
66
|
+
</label>
|
|
67
|
+
<input
|
|
68
|
+
type="email"
|
|
69
|
+
value={email}
|
|
70
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
71
|
+
required
|
|
72
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div>
|
|
77
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
78
|
+
Password
|
|
79
|
+
</label>
|
|
80
|
+
<input
|
|
81
|
+
type="password"
|
|
82
|
+
value={password}
|
|
83
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
84
|
+
required
|
|
85
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<button
|
|
90
|
+
type="submit"
|
|
91
|
+
disabled={submitting}
|
|
92
|
+
className="w-full rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
|
|
93
|
+
>
|
|
94
|
+
{submitting ? 'Creating accountβ¦' : 'Create account'}
|
|
95
|
+
</button>
|
|
96
|
+
</form>
|
|
97
|
+
|
|
98
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
99
|
+
Already have an account?{' '}
|
|
100
|
+
<a href="/login" className="text-indigo-600 hover:underline">
|
|
101
|
+
Sign in
|
|
102
|
+
</a>
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
interface Route {
|
|
2
|
+
path: string
|
|
3
|
+
description: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
routes: Route[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function Welcome({ routes }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-8">
|
|
13
|
+
<div className="max-w-lg w-full">
|
|
14
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
|
15
|
+
Welcome to Hypersonic.js
|
|
16
|
+
</h1>
|
|
17
|
+
<p className="text-gray-500 mb-8">
|
|
18
|
+
Your app is running. Here's where to go next.
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 divide-y divide-gray-100 mb-8">
|
|
22
|
+
{routes.map((route) => (
|
|
23
|
+
<a
|
|
24
|
+
key={route.path}
|
|
25
|
+
href={route.path}
|
|
26
|
+
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 transition-colors"
|
|
27
|
+
>
|
|
28
|
+
<div>
|
|
29
|
+
<p className="font-mono text-sm font-medium text-gray-900">
|
|
30
|
+
{route.path}
|
|
31
|
+
</p>
|
|
32
|
+
<p className="text-sm text-gray-500">{route.description}</p>
|
|
33
|
+
</div>
|
|
34
|
+
<span className="text-gray-300">→</span>
|
|
35
|
+
</a>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<p className="text-center text-sm text-gray-400">
|
|
40
|
+
Read the{' '}
|
|
41
|
+
<a
|
|
42
|
+
href="https://hypersonic-js.com/guide/"
|
|
43
|
+
className="text-indigo-600 hover:underline"
|
|
44
|
+
target="_blank"
|
|
45
|
+
rel="noreferrer"
|
|
46
|
+
>
|
|
47
|
+
documentation
|
|
48
|
+
</a>{' '}
|
|
49
|
+
to learn more.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createInertiaApp } from '@inertiajs/react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import '../css/app.css'
|
|
4
|
+
|
|
5
|
+
createInertiaApp({
|
|
6
|
+
resolve: (name) => {
|
|
7
|
+
const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true })
|
|
8
|
+
const page = pages[`./Pages/${name}.tsx`]
|
|
9
|
+
if (!page) throw new Error(`Inertia page not found: ${name}`)
|
|
10
|
+
return page as never
|
|
11
|
+
},
|
|
12
|
+
setup({ el, App, props }) {
|
|
13
|
+
createRoot(el).render(<App {...props} />)
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import type { PrismaClient as PrismaClientType } from '@prisma/client'
|
|
4
|
+
import {
|
|
5
|
+
createApp,
|
|
6
|
+
loadConfig,
|
|
7
|
+
createDatabaseAdapter,
|
|
8
|
+
createInertiaErrorHandler,
|
|
9
|
+
mountAdmin,
|
|
10
|
+
} from '@hypersonic-js/complete'
|
|
11
|
+
import type { AdminModelMeta } from '@hypersonic-js/complete'
|
|
12
|
+
import { createAuthGuard } from './src/middleware.ts'
|
|
13
|
+
|
|
14
|
+
// PrismaClient is CommonJS β use createRequire to load it in an ESM context.
|
|
15
|
+
const require = createRequire(import.meta.url)
|
|
16
|
+
const adminMeta = require('./prisma/admin-meta.json') as AdminModelMeta[]
|
|
17
|
+
const { PrismaClient } = require('@prisma/client') as {
|
|
18
|
+
PrismaClient: typeof PrismaClientType
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { config, env } = await loadConfig()
|
|
22
|
+
|
|
23
|
+
// Prisma v7 requires a driver adapter β never instantiate PrismaClient bare.
|
|
24
|
+
const adapter = await createDatabaseAdapter(config.database.provider, env.DATABASE_URL)
|
|
25
|
+
const prisma = new PrismaClient({ adapter })
|
|
26
|
+
|
|
27
|
+
const app = await createApp({ config, env, prisma })
|
|
28
|
+
|
|
29
|
+
// ββ Auth guard (use on any route you want to protect) ββββββββββββββββββββββ
|
|
30
|
+
|
|
31
|
+
const requireAuth = createAuthGuard(app.auth)
|
|
32
|
+
|
|
33
|
+
// ββ Public routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
34
|
+
|
|
35
|
+
app.express.get('/', (_req, res) => {
|
|
36
|
+
res.inertia!('Welcome', {
|
|
37
|
+
routes: [
|
|
38
|
+
{ path: '/login', description: 'Sign in to your account' },
|
|
39
|
+
{ path: '/register', description: 'Create a new account' },
|
|
40
|
+
{ path: '/admin', description: 'Admin dashboard (admin role required and required to /login first)' },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
app.express.get('/login', (_req, res) => {
|
|
46
|
+
res.inertia!('Auth/Login', {})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
app.express.get('/register', (_req, res) => {
|
|
50
|
+
res.inertia!('Auth/Register', {})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// ββ Protected routes β example βββββββββββββββββββββββββββββββββββββββββββββ
|
|
54
|
+
// app.express.get('/dashboard', requireAuth, (req, res) => {
|
|
55
|
+
// res.inertia!('Dashboard', { user: req.sessionUser })
|
|
56
|
+
// })
|
|
57
|
+
|
|
58
|
+
// ββ Admin βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
59
|
+
|
|
60
|
+
mountAdmin(app.express, prisma, {
|
|
61
|
+
meta: adminMeta,
|
|
62
|
+
auth: app.auth,
|
|
63
|
+
logger: app.logger,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
app.express.use(createInertiaErrorHandler())
|
|
67
|
+
|
|
68
|
+
await app.start()
|
|
69
|
+
console.log(`Listening on http://${config.server.host}:${config.server.port}`)
|
|
70
|
+
|
|
71
|
+
export { requireAuth }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Response, NextFunction, RequestHandler } from 'express'
|
|
2
|
+
import type { AuthLike, AuthRequest } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a middleware that checks for a valid Better Auth session.
|
|
6
|
+
* Redirects to /login when the session is absent.
|
|
7
|
+
* Attaches session.user to req.sessionUser on success.
|
|
8
|
+
*/
|
|
9
|
+
export function createAuthGuard(auth: AuthLike): RequestHandler {
|
|
10
|
+
return async function requireAuth(
|
|
11
|
+
req: AuthRequest,
|
|
12
|
+
res: Response,
|
|
13
|
+
next: NextFunction,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const session = await auth.api.getSession({
|
|
16
|
+
headers: req.headers as unknown as Headers,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if (!session) {
|
|
20
|
+
res.redirect('/login')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
req.sessionUser = session.user
|
|
25
|
+
next()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Request } from 'express'
|
|
2
|
+
|
|
3
|
+
export interface SessionUser {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
email: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuthRequest extends Request {
|
|
10
|
+
sessionUser?: SessionUser
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthLike {
|
|
14
|
+
api: {
|
|
15
|
+
getSession(opts: { headers: unknown }): Promise<{ user: SessionUser } | null>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["vite/client"],
|
|
10
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
13
|
+
"exclude": ["node_modules", "public"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Request } from 'express'
|
|
2
|
+
|
|
3
|
+
export interface SessionUser {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
email: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuthRequest extends Request {
|
|
10
|
+
sessionUser?: SessionUser
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthLike {
|
|
14
|
+
api: {
|
|
15
|
+
getSession(opts: { headers: unknown }): Promise<{ user: SessionUser } | null>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tailwindcss()],
|
|
6
|
+
build: {
|
|
7
|
+
outDir: 'public',
|
|
8
|
+
manifest: true,
|
|
9
|
+
rollupOptions: {
|
|
10
|
+
input: 'resources/js/app.tsx',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypersonic-js/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Hypersonic.js CLI β developer tooling for the Hypersonic framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,27 +20,27 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": "
|
|
23
|
+
"node": ">=24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"commander": "15.0.0",
|
|
27
27
|
"dotenv": "17.4.2",
|
|
28
28
|
"picocolors": "1.1.1",
|
|
29
29
|
"@prisma/get-dmmf": "7.8.0",
|
|
30
|
-
"@hypersonic-js/admin": "0.2.
|
|
30
|
+
"@hypersonic-js/admin": "0.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@prisma/adapter-pg": "7.8.0",
|
|
34
34
|
"@prisma/client": "7.8.0",
|
|
35
35
|
"@types/node": "25.9.3",
|
|
36
|
-
"@vitest/coverage-v8": "4.1.
|
|
37
|
-
"better-auth": "1.6.
|
|
36
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
37
|
+
"better-auth": "1.6.19",
|
|
38
38
|
"typescript": "6.0.3",
|
|
39
|
-
"vitest": "4.1.
|
|
40
|
-
"@hypersonic-js/core": "0.2.
|
|
39
|
+
"vitest": "4.1.9",
|
|
40
|
+
"@hypersonic-js/core": "0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"build": "tsc",
|
|
43
|
+
"build": "tsc && node --input-type=module --eval \"import{cpSync}from'node:fs';cpSync('templates','dist/templates',{recursive:true})\"",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:coverage": "vitest run --coverage",
|
|
46
46
|
"type-check": "tsc --noEmit",
|