@maxal_studio/kratosjs-cli 1.0.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/dist/commands/init.js +41 -0
- package/dist/commands/new.js +148 -0
- package/dist/commands/plugin.js +111 -0
- package/dist/drivers.js +130 -0
- package/dist/index.js +46 -0
- package/dist/render.js +82 -0
- package/dist/versions.js +74 -0
- package/package.json +38 -0
- package/templates/admin-client/index.html +14 -0
- package/templates/admin-client/src/admin/main.tsx +9 -0
- package/templates/admin-client/vite.config.mts +4 -0
- package/templates/app/.env.example.tmpl +11 -0
- package/templates/app/README.md.tmpl +61 -0
- package/templates/app/assets/icon.png +0 -0
- package/templates/app/index.html +14 -0
- package/templates/app/package.json.tmpl +15 -0
- package/templates/app/src/admin/main.tsx +11 -0
- package/templates/app/src/entities/User.ts.tmpl +31 -0
- package/templates/app/src/hooks/userHooks.ts +51 -0
- package/templates/app/src/index.ts.tmpl +71 -0
- package/templates/app/src/migrations/.gitkeep +0 -0
- package/templates/app/src/resources/UserResource.ts +79 -0
- package/templates/app/src/seedAdminUser.ts +30 -0
- package/templates/app/tsconfig.json +21 -0
- package/templates/app/vite.config.mts +4 -0
- package/templates/plugin/src/index.ts.tmpl +2 -0
- package/templates/plugin/src/{{PluginName}}Plugin.ts.tmpl +25 -0
- package/templates/plugin/tsconfig.server.json +18 -0
- package/templates/plugin-client/src/client/index.ts.tmpl +17 -0
- package/templates/plugin-client/src/client/{{PluginName}}Field.tsx.tmpl +28 -0
- package/templates/plugin-client/tsconfig.client.json +19 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = runInit;
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const render_1 = require("../render");
|
|
9
|
+
/**
|
|
10
|
+
* Scaffold the standard KratosJs admin client entry (index.html, vite config,
|
|
11
|
+
* src/admin/main.tsx) into an existing app. Idempotent: never overwrites.
|
|
12
|
+
*/
|
|
13
|
+
async function runInit() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const result = (0, render_1.renderTemplateTree)((0, render_1.templatePath)('admin-client'), cwd, {});
|
|
16
|
+
if (result.created.length > 0) {
|
|
17
|
+
console.log(picocolors_1.default.green('Created admin client files:'));
|
|
18
|
+
for (const file of result.created) {
|
|
19
|
+
console.log(` ${picocolors_1.default.green('+')} ${file}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (result.skipped.length > 0) {
|
|
23
|
+
console.log(picocolors_1.default.dim('Already present (skipped):'));
|
|
24
|
+
for (const file of result.skipped) {
|
|
25
|
+
console.log(` ${picocolors_1.default.dim('-')} ${file}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (result.created.length === 0 && result.skipped.length === 0) {
|
|
29
|
+
console.log(picocolors_1.default.dim('No files to scaffold.'));
|
|
30
|
+
}
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log('Ensure these dev dependencies are installed in your app:');
|
|
33
|
+
console.log(picocolors_1.default.dim(' @maxal_studio/kratosjs-react react react-dom react-hook-form vite'));
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log('Recommended package.json scripts:');
|
|
36
|
+
console.log(picocolors_1.default.dim(' "dev": "tsx watch src/index.ts"'));
|
|
37
|
+
console.log(picocolors_1.default.dim(' "build": "npm run build:server && npm run build:admin"'));
|
|
38
|
+
console.log(picocolors_1.default.dim(' "build:server": "tsc"'));
|
|
39
|
+
console.log(picocolors_1.default.dim(' "build:admin": "vite build"'));
|
|
40
|
+
console.log(picocolors_1.default.dim(' "start": "NODE_ENV=production node dist/index.js"'));
|
|
41
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runNew = runNew;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
11
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
12
|
+
const drivers_1 = require("../drivers");
|
|
13
|
+
const render_1 = require("../render");
|
|
14
|
+
const versions_1 = require("../versions");
|
|
15
|
+
const MIKRO_ORM_CORE = '^7.1.4';
|
|
16
|
+
/** Resolve the monorepo root (only meaningful with --local). */
|
|
17
|
+
function monorepoRoot() {
|
|
18
|
+
// dist/commands/new.js -> dist -> kratosjs-cli -> packages -> repo root
|
|
19
|
+
return path_1.default.resolve(__dirname, '..', '..', '..', '..');
|
|
20
|
+
}
|
|
21
|
+
function buildDependencies(driver, local) {
|
|
22
|
+
return {
|
|
23
|
+
'@maxal_studio/kratosjs': (0, versions_1.kratosCoreDep)(local),
|
|
24
|
+
'@mikro-orm/core': MIKRO_ORM_CORE,
|
|
25
|
+
...driver.dependencies,
|
|
26
|
+
dotenv: '^17.4.2',
|
|
27
|
+
express: '^5.2.1',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildDevDependencies(local) {
|
|
31
|
+
return {
|
|
32
|
+
'@maxal_studio/kratosjs-react': (0, versions_1.kratosReactDep)(local),
|
|
33
|
+
'@types/node': '^25.9.3',
|
|
34
|
+
react: '^19.2.7',
|
|
35
|
+
'react-dom': '^19.2.7',
|
|
36
|
+
'react-hook-form': '^7.79.0',
|
|
37
|
+
tsx: '^4.22.4',
|
|
38
|
+
typescript: '^5.9.3',
|
|
39
|
+
vite: '^7.3.5',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** JSON.stringify an object and re-indent so it nests under a one-tab key. */
|
|
43
|
+
function jsonBlock(obj) {
|
|
44
|
+
return JSON.stringify(obj, null, '\t')
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map((line, i) => (i === 0 ? line : '\t' + line))
|
|
47
|
+
.join('\n');
|
|
48
|
+
}
|
|
49
|
+
function resolveDriver(value) {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const key = value.toLowerCase();
|
|
54
|
+
return drivers_1.DRIVERS[key] ?? null;
|
|
55
|
+
}
|
|
56
|
+
async function runNew(nameArg, options) {
|
|
57
|
+
const projectName = nameArg ??
|
|
58
|
+
(await (0, prompts_1.input)({
|
|
59
|
+
message: 'Project name:',
|
|
60
|
+
default: 'my-kratosjs-app',
|
|
61
|
+
validate: value => (value.trim().length > 0 ? true : 'Please enter a project name'),
|
|
62
|
+
}));
|
|
63
|
+
const appName = (0, render_1.toKebabCase)(projectName);
|
|
64
|
+
const targetDir = path_1.default.resolve(process.cwd(), appName);
|
|
65
|
+
if (fs_1.default.existsSync(targetDir) && fs_1.default.readdirSync(targetDir).length > 0) {
|
|
66
|
+
console.error(picocolors_1.default.red(`✖ Directory "${appName}" already exists and is not empty.`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
let driver = resolveDriver(options.driver);
|
|
70
|
+
if (options.driver && !driver) {
|
|
71
|
+
console.error(picocolors_1.default.red(`✖ Unknown driver "${options.driver}". Valid drivers: ${drivers_1.DRIVER_KEYS.join('|')}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!driver) {
|
|
75
|
+
const driverKey = await (0, prompts_1.select)({
|
|
76
|
+
message: 'Which database do you want to use?',
|
|
77
|
+
choices: drivers_1.DRIVER_KEYS.map(key => ({ name: drivers_1.DRIVERS[key].label, value: key })),
|
|
78
|
+
});
|
|
79
|
+
driver = drivers_1.DRIVERS[driverKey];
|
|
80
|
+
}
|
|
81
|
+
const local = options.local ?? false;
|
|
82
|
+
const tokens = {
|
|
83
|
+
appName,
|
|
84
|
+
appTitle: `${appName} admin`,
|
|
85
|
+
driverLabel: driver.label,
|
|
86
|
+
driverImport: driver.driverImport,
|
|
87
|
+
migratorImport: driver.migratorImport,
|
|
88
|
+
ormConfig: driver.ormConfig,
|
|
89
|
+
idProps: driver.idProps,
|
|
90
|
+
idInterfaceFields: driver.idInterfaceFields,
|
|
91
|
+
envVars: driver.envVars,
|
|
92
|
+
dependencies: jsonBlock(buildDependencies(driver, local)),
|
|
93
|
+
devDependencies: jsonBlock(buildDevDependencies(local)),
|
|
94
|
+
};
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(picocolors_1.default.cyan(`Creating a new KratosJs app in ${picocolors_1.default.bold(targetDir)}`));
|
|
97
|
+
console.log(picocolors_1.default.dim(` driver: ${driver.label}${local ? ' (local file: links)' : ''}`));
|
|
98
|
+
console.log('');
|
|
99
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
100
|
+
const result = (0, render_1.renderTemplateTree)((0, render_1.templatePath)('app'), targetDir, tokens);
|
|
101
|
+
for (const file of result.created) {
|
|
102
|
+
console.log(` ${picocolors_1.default.green('+')} ${file}`);
|
|
103
|
+
}
|
|
104
|
+
const install = options.install ?? true;
|
|
105
|
+
let built = false;
|
|
106
|
+
if (install) {
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(picocolors_1.default.cyan('Installing dependencies...'));
|
|
109
|
+
let installOk = false;
|
|
110
|
+
try {
|
|
111
|
+
(0, child_process_1.execSync)('npm install', { cwd: local ? monorepoRoot() : targetDir, stdio: 'inherit' });
|
|
112
|
+
installOk = true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
console.error(picocolors_1.default.yellow('⚠ npm install failed — you can run it manually later.'));
|
|
116
|
+
}
|
|
117
|
+
if (installOk) {
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(picocolors_1.default.cyan('Building app...'));
|
|
120
|
+
try {
|
|
121
|
+
(0, child_process_1.execSync)('npm run build', { cwd: targetDir, stdio: 'inherit' });
|
|
122
|
+
built = true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
console.error(picocolors_1.default.yellow('⚠ npm run build failed — you can run it manually later.'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
printNextSteps(appName, install, built);
|
|
130
|
+
}
|
|
131
|
+
function printNextSteps(appName, installed, built) {
|
|
132
|
+
const steps = [
|
|
133
|
+
`cd ${appName}`,
|
|
134
|
+
'cp .env.example .env ' + picocolors_1.default.dim('# then edit DB settings'),
|
|
135
|
+
...(installed ? [] : ['npm install']),
|
|
136
|
+
...(built ? [] : ['npm run build']),
|
|
137
|
+
'npm run dev',
|
|
138
|
+
];
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(picocolors_1.default.green('✔ Done! Next steps:'));
|
|
141
|
+
console.log('');
|
|
142
|
+
for (const step of steps) {
|
|
143
|
+
console.log(` ${picocolors_1.default.bold(step)}`);
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(picocolors_1.default.dim('Login with admin@example.com / password once the server is running.'));
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runPlugin = runPlugin;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
10
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
11
|
+
const render_1 = require("../render");
|
|
12
|
+
const versions_1 = require("../versions");
|
|
13
|
+
function buildPackageJson(pluginName, withClient, local) {
|
|
14
|
+
const pkg = {
|
|
15
|
+
name: `kratosjs-plugin-${pluginName}`,
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
description: `${pluginName} plugin for KratosJs panels`,
|
|
18
|
+
main: './dist/server/index.js',
|
|
19
|
+
types: './dist/server/index.d.ts',
|
|
20
|
+
sideEffects: false,
|
|
21
|
+
exports: {
|
|
22
|
+
'.': {
|
|
23
|
+
types: './dist/server/index.d.ts',
|
|
24
|
+
default: './dist/server/index.js',
|
|
25
|
+
},
|
|
26
|
+
...(withClient
|
|
27
|
+
? {
|
|
28
|
+
'./client': {
|
|
29
|
+
types: './dist/client/index.d.ts',
|
|
30
|
+
default: './dist/client/index.js',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
'./package.json': './package.json',
|
|
35
|
+
},
|
|
36
|
+
files: ['dist'],
|
|
37
|
+
scripts: {
|
|
38
|
+
build: withClient
|
|
39
|
+
? 'npm run clean && tsc -p tsconfig.server.json && tsc -p tsconfig.client.json'
|
|
40
|
+
: 'npm run clean && tsc -p tsconfig.server.json',
|
|
41
|
+
clean: 'rm -rf dist',
|
|
42
|
+
},
|
|
43
|
+
keywords: ['kratosjs', 'kratosjs-plugin'],
|
|
44
|
+
license: 'ISC',
|
|
45
|
+
peerDependencies: withClient
|
|
46
|
+
? {
|
|
47
|
+
'@maxal_studio/kratosjs': (0, versions_1.kratosCoreDep)(local),
|
|
48
|
+
'@maxal_studio/kratosjs-react': (0, versions_1.kratosReactDep)(local),
|
|
49
|
+
react: '^19.0.0',
|
|
50
|
+
'react-hook-form': '^7.0.0',
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
'@maxal_studio/kratosjs': (0, versions_1.kratosCoreDep)(local),
|
|
54
|
+
},
|
|
55
|
+
devDependencies: withClient
|
|
56
|
+
? {
|
|
57
|
+
'@types/react': '^19.2.5',
|
|
58
|
+
typescript: '^5.9.3',
|
|
59
|
+
}
|
|
60
|
+
: {
|
|
61
|
+
typescript: '^5.9.3',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return JSON.stringify(pkg, null, '\t') + '\n';
|
|
65
|
+
}
|
|
66
|
+
async function runPlugin(nameArg, options) {
|
|
67
|
+
const rawName = nameArg ??
|
|
68
|
+
(await (0, prompts_1.input)({
|
|
69
|
+
message: 'Plugin name:',
|
|
70
|
+
default: 'my-plugin',
|
|
71
|
+
validate: value => (value.trim().length > 0 ? true : 'Please enter a plugin name'),
|
|
72
|
+
}));
|
|
73
|
+
const pluginName = (0, render_1.toKebabCase)(rawName);
|
|
74
|
+
const PluginName = (0, render_1.toPascalCase)(rawName);
|
|
75
|
+
const pluginCamel = (0, render_1.toCamelCase)(rawName);
|
|
76
|
+
const withClient = options.client ?? false;
|
|
77
|
+
const targetDir = path_1.default.resolve(process.cwd(), `kratosjs-plugin-${pluginName}`);
|
|
78
|
+
if (fs_1.default.existsSync(targetDir) && fs_1.default.readdirSync(targetDir).length > 0) {
|
|
79
|
+
console.error(picocolors_1.default.red(`✖ Directory "${path_1.default.basename(targetDir)}" already exists and is not empty.`));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const tokens = {
|
|
83
|
+
pluginName,
|
|
84
|
+
PluginName,
|
|
85
|
+
pluginCamel,
|
|
86
|
+
registerBody: withClient
|
|
87
|
+
? `panel.registerCustomField('${pluginName}');`
|
|
88
|
+
: `// e.g. panel.registerCustomField('${pluginName}');`,
|
|
89
|
+
};
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(picocolors_1.default.cyan(`Creating a new KratosJs plugin in ${picocolors_1.default.bold(targetDir)}`));
|
|
92
|
+
console.log(picocolors_1.default.dim(` ${withClient ? 'server + client' : 'server-only'}`));
|
|
93
|
+
console.log('');
|
|
94
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
95
|
+
const result = (0, render_1.renderTemplateTree)((0, render_1.templatePath)('plugin'), targetDir, tokens);
|
|
96
|
+
if (withClient) {
|
|
97
|
+
(0, render_1.renderTemplateTree)((0, render_1.templatePath)('plugin-client'), targetDir, tokens, result);
|
|
98
|
+
}
|
|
99
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, 'package.json'), buildPackageJson(pluginName, withClient, false), 'utf-8');
|
|
100
|
+
result.created.unshift('package.json');
|
|
101
|
+
for (const file of result.created) {
|
|
102
|
+
console.log(` ${picocolors_1.default.green('+')} ${file}`);
|
|
103
|
+
}
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(picocolors_1.default.green('✔ Plugin scaffolded. Next steps:'));
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(` ${picocolors_1.default.bold(`cd ${path_1.default.basename(targetDir)}`)}`);
|
|
108
|
+
console.log(` ${picocolors_1.default.bold('npm install')}`);
|
|
109
|
+
console.log(` ${picocolors_1.default.bold('npm run build')}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
package/dist/drivers.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DRIVER_KEYS = exports.DRIVERS = void 0;
|
|
4
|
+
const MIKRO_ORM_VERSION = '^7.1.4';
|
|
5
|
+
exports.DRIVERS = {
|
|
6
|
+
mysql: {
|
|
7
|
+
key: 'mysql',
|
|
8
|
+
label: 'MySQL',
|
|
9
|
+
kind: 'sql',
|
|
10
|
+
dependencies: {
|
|
11
|
+
'@mikro-orm/mysql': MIKRO_ORM_VERSION,
|
|
12
|
+
'@mikro-orm/migrations': MIKRO_ORM_VERSION,
|
|
13
|
+
},
|
|
14
|
+
driverImport: "import { MySqlDriver } from '@mikro-orm/mysql';",
|
|
15
|
+
migratorImport: "import { Migrator } from '@mikro-orm/migrations';",
|
|
16
|
+
ormConfig: `{
|
|
17
|
+
driver: MySqlDriver,
|
|
18
|
+
host: process.env.DATABASE_HOST || 'localhost',
|
|
19
|
+
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
|
20
|
+
user: process.env.DATABASE_USER || 'root',
|
|
21
|
+
password: process.env.DATABASE_PASSWORD || '',
|
|
22
|
+
dbName: process.env.DATABASE_NAME || 'kratosjs',
|
|
23
|
+
extensions: [Migrator],
|
|
24
|
+
}`,
|
|
25
|
+
idProps: `id: { type: 'number', primary: true, autoincrement: true },`,
|
|
26
|
+
idInterfaceFields: `id: number;`,
|
|
27
|
+
envVars: `DATABASE_HOST="localhost"
|
|
28
|
+
DATABASE_PORT="3306"
|
|
29
|
+
DATABASE_USER="root"
|
|
30
|
+
DATABASE_PASSWORD=""
|
|
31
|
+
DATABASE_NAME="kratosjs"`,
|
|
32
|
+
},
|
|
33
|
+
postgresql: {
|
|
34
|
+
key: 'postgresql',
|
|
35
|
+
label: 'PostgreSQL',
|
|
36
|
+
kind: 'sql',
|
|
37
|
+
dependencies: {
|
|
38
|
+
'@mikro-orm/postgresql': MIKRO_ORM_VERSION,
|
|
39
|
+
'@mikro-orm/migrations': MIKRO_ORM_VERSION,
|
|
40
|
+
},
|
|
41
|
+
driverImport: "import { PostgreSqlDriver } from '@mikro-orm/postgresql';",
|
|
42
|
+
migratorImport: "import { Migrator } from '@mikro-orm/migrations';",
|
|
43
|
+
ormConfig: `{
|
|
44
|
+
driver: PostgreSqlDriver,
|
|
45
|
+
host: process.env.DATABASE_HOST || 'localhost',
|
|
46
|
+
port: parseInt(process.env.DATABASE_PORT || '5432'),
|
|
47
|
+
user: process.env.DATABASE_USER || 'postgres',
|
|
48
|
+
password: process.env.DATABASE_PASSWORD || '',
|
|
49
|
+
dbName: process.env.DATABASE_NAME || 'kratosjs',
|
|
50
|
+
extensions: [Migrator],
|
|
51
|
+
}`,
|
|
52
|
+
idProps: `id: { type: 'number', primary: true, autoincrement: true },`,
|
|
53
|
+
idInterfaceFields: `id: number;`,
|
|
54
|
+
envVars: `DATABASE_HOST="localhost"
|
|
55
|
+
DATABASE_PORT="5432"
|
|
56
|
+
DATABASE_USER="postgres"
|
|
57
|
+
DATABASE_PASSWORD=""
|
|
58
|
+
DATABASE_NAME="kratosjs"`,
|
|
59
|
+
},
|
|
60
|
+
mariadb: {
|
|
61
|
+
key: 'mariadb',
|
|
62
|
+
label: 'MariaDB',
|
|
63
|
+
kind: 'sql',
|
|
64
|
+
dependencies: {
|
|
65
|
+
'@mikro-orm/mariadb': MIKRO_ORM_VERSION,
|
|
66
|
+
'@mikro-orm/migrations': MIKRO_ORM_VERSION,
|
|
67
|
+
},
|
|
68
|
+
driverImport: "import { MariaDbDriver } from '@mikro-orm/mariadb';",
|
|
69
|
+
migratorImport: "import { Migrator } from '@mikro-orm/migrations';",
|
|
70
|
+
ormConfig: `{
|
|
71
|
+
driver: MariaDbDriver,
|
|
72
|
+
host: process.env.DATABASE_HOST || 'localhost',
|
|
73
|
+
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
|
74
|
+
user: process.env.DATABASE_USER || 'root',
|
|
75
|
+
password: process.env.DATABASE_PASSWORD || '',
|
|
76
|
+
dbName: process.env.DATABASE_NAME || 'kratosjs',
|
|
77
|
+
extensions: [Migrator],
|
|
78
|
+
}`,
|
|
79
|
+
idProps: `id: { type: 'number', primary: true, autoincrement: true },`,
|
|
80
|
+
idInterfaceFields: `id: number;`,
|
|
81
|
+
envVars: `DATABASE_HOST="localhost"
|
|
82
|
+
DATABASE_PORT="3306"
|
|
83
|
+
DATABASE_USER="root"
|
|
84
|
+
DATABASE_PASSWORD=""
|
|
85
|
+
DATABASE_NAME="kratosjs"`,
|
|
86
|
+
},
|
|
87
|
+
sqlite: {
|
|
88
|
+
key: 'sqlite',
|
|
89
|
+
label: 'SQLite',
|
|
90
|
+
kind: 'sql',
|
|
91
|
+
dependencies: {
|
|
92
|
+
'@mikro-orm/sqlite': MIKRO_ORM_VERSION,
|
|
93
|
+
'@mikro-orm/migrations': MIKRO_ORM_VERSION,
|
|
94
|
+
},
|
|
95
|
+
driverImport: "import { SqliteDriver } from '@mikro-orm/sqlite';",
|
|
96
|
+
migratorImport: "import { Migrator } from '@mikro-orm/migrations';",
|
|
97
|
+
ormConfig: `{
|
|
98
|
+
driver: SqliteDriver,
|
|
99
|
+
dbName: process.env.DATABASE_NAME || 'kratosjs.sqlite',
|
|
100
|
+
extensions: [Migrator],
|
|
101
|
+
}`,
|
|
102
|
+
idProps: `id: { type: 'number', primary: true, autoincrement: true },`,
|
|
103
|
+
idInterfaceFields: `id: number;`,
|
|
104
|
+
envVars: `DATABASE_NAME="kratosjs.sqlite"`,
|
|
105
|
+
},
|
|
106
|
+
mongo: {
|
|
107
|
+
key: 'mongo',
|
|
108
|
+
label: 'MongoDB',
|
|
109
|
+
kind: 'mongo',
|
|
110
|
+
dependencies: {
|
|
111
|
+
'@mikro-orm/mongodb': MIKRO_ORM_VERSION,
|
|
112
|
+
'@mikro-orm/migrations-mongodb': MIKRO_ORM_VERSION,
|
|
113
|
+
},
|
|
114
|
+
driverImport: "import { MongoDriver } from '@mikro-orm/mongodb';",
|
|
115
|
+
migratorImport: "import { Migrator } from '@mikro-orm/migrations-mongodb';",
|
|
116
|
+
ormConfig: `{
|
|
117
|
+
driver: MongoDriver,
|
|
118
|
+
clientUrl: process.env.DATABASE_URL || 'mongodb://localhost:27017',
|
|
119
|
+
dbName: process.env.DATABASE_NAME || 'kratosjs',
|
|
120
|
+
extensions: [Migrator],
|
|
121
|
+
}`,
|
|
122
|
+
idProps: `_id: { type: 'ObjectId', primary: true },
|
|
123
|
+
id: { type: 'string', serializedPrimaryKey: true },`,
|
|
124
|
+
idInterfaceFields: `_id: any;
|
|
125
|
+
id: string;`,
|
|
126
|
+
envVars: `DATABASE_URL="mongodb://localhost:27017"
|
|
127
|
+
DATABASE_NAME="kratosjs"`,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
exports.DRIVER_KEYS = Object.keys(exports.DRIVERS);
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
const new_1 = require("./commands/new");
|
|
10
|
+
const plugin_1 = require("./commands/plugin");
|
|
11
|
+
const init_1 = require("./commands/init");
|
|
12
|
+
const drivers_1 = require("./drivers");
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
program.name('kratosjs').description('Scaffold KratosJs apps and plugins').version(require('../package.json').version);
|
|
15
|
+
program
|
|
16
|
+
.command('new')
|
|
17
|
+
.argument('[name]', 'project name')
|
|
18
|
+
.description('Create a new KratosJs app')
|
|
19
|
+
.option('--driver <driver>', `database driver (${drivers_1.DRIVER_KEYS.join('|')})`)
|
|
20
|
+
.option('--no-install', 'skip installing dependencies')
|
|
21
|
+
.option('--local', 'use file: links to the monorepo packages (for local testing)')
|
|
22
|
+
.action(async (name, options) => {
|
|
23
|
+
await (0, new_1.runNew)(name, options);
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command('plugin')
|
|
27
|
+
.argument('[name]', 'plugin name')
|
|
28
|
+
.description('Scaffold a standalone KratosJs plugin package')
|
|
29
|
+
.option('--client', 'include a React client entry (custom UI components)')
|
|
30
|
+
.action(async (name, options) => {
|
|
31
|
+
await (0, plugin_1.runPlugin)(name, options);
|
|
32
|
+
});
|
|
33
|
+
program
|
|
34
|
+
.command('init')
|
|
35
|
+
.description('Scaffold the admin client entry into an existing app')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
await (0, init_1.runInit)();
|
|
38
|
+
});
|
|
39
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
40
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
41
|
+
console.log(picocolors_1.default.dim('\nAborted.'));
|
|
42
|
+
process.exit(130);
|
|
43
|
+
}
|
|
44
|
+
console.error(picocolors_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.toPascalCase = toPascalCase;
|
|
7
|
+
exports.toKebabCase = toKebabCase;
|
|
8
|
+
exports.toCamelCase = toCamelCase;
|
|
9
|
+
exports.replaceTokens = replaceTokens;
|
|
10
|
+
exports.renderTemplateTree = renderTemplateTree;
|
|
11
|
+
exports.templatePath = templatePath;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
/** Convert an arbitrary string into PascalCase, e.g. "star rating" -> "StarRating". */
|
|
15
|
+
function toPascalCase(input) {
|
|
16
|
+
return input
|
|
17
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
18
|
+
.trim()
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
21
|
+
.join('');
|
|
22
|
+
}
|
|
23
|
+
/** Convert an arbitrary string into kebab-case, e.g. "StarRating" -> "star-rating". */
|
|
24
|
+
function toKebabCase(input) {
|
|
25
|
+
return input
|
|
26
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
27
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '')
|
|
29
|
+
.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
/** Convert an arbitrary string into camelCase, e.g. "Star Rating" -> "starRating". */
|
|
32
|
+
function toCamelCase(input) {
|
|
33
|
+
const pascal = toPascalCase(input);
|
|
34
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
35
|
+
}
|
|
36
|
+
/** Replace every {{token}} occurrence in a string using the provided map. */
|
|
37
|
+
function replaceTokens(content, tokens) {
|
|
38
|
+
return content.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, key) => {
|
|
39
|
+
return key in tokens ? tokens[key] : match;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Recursively render a template directory into the target directory.
|
|
44
|
+
*
|
|
45
|
+
* - `.tmpl` files have their tokens replaced and the `.tmpl` suffix stripped.
|
|
46
|
+
* - Other files are copied verbatim.
|
|
47
|
+
* - Existing files are skipped (idempotent, never overwrites).
|
|
48
|
+
* - File/directory names also get token replacement (so `{{name}}.ts` works).
|
|
49
|
+
*/
|
|
50
|
+
function renderTemplateTree(templateDir, targetDir, tokens, result = { created: [], skipped: [] }) {
|
|
51
|
+
const entries = fs_1.default.readdirSync(templateDir, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const sourcePath = path_1.default.join(templateDir, entry.name);
|
|
54
|
+
const renderedName = replaceTokens(entry.name.replace(/\.tmpl$/, ''), tokens);
|
|
55
|
+
const targetPath = path_1.default.join(targetDir, renderedName);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
fs_1.default.mkdirSync(targetPath, { recursive: true });
|
|
58
|
+
renderTemplateTree(sourcePath, targetPath, tokens, result);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const relative = path_1.default.relative(targetDir, targetPath);
|
|
62
|
+
if (fs_1.default.existsSync(targetPath)) {
|
|
63
|
+
result.skipped.push(relative);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const isTemplate = entry.name.endsWith('.tmpl');
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
|
|
68
|
+
if (isTemplate) {
|
|
69
|
+
const raw = fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
70
|
+
fs_1.default.writeFileSync(targetPath, replaceTokens(raw, tokens), 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
fs_1.default.copyFileSync(sourcePath, targetPath);
|
|
74
|
+
}
|
|
75
|
+
result.created.push(relative);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/** Resolve a path inside the shipped templates directory. */
|
|
80
|
+
function templatePath(...segments) {
|
|
81
|
+
return path_1.default.join(__dirname, '..', 'templates', ...segments);
|
|
82
|
+
}
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.kratosCoreDep = kratosCoreDep;
|
|
7
|
+
exports.kratosReactDep = kratosReactDep;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
function readVersionFromPackageJson(pkgJsonPath) {
|
|
12
|
+
if (!fs_1.default.existsSync(pkgJsonPath)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
17
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function monorepoRoot() {
|
|
24
|
+
// dist/versions.js -> dist -> kratosjs-cli -> packages -> repo root
|
|
25
|
+
return path_1.default.resolve(__dirname, '..', '..', '..', '..');
|
|
26
|
+
}
|
|
27
|
+
function readMonorepoVersion(relativePath) {
|
|
28
|
+
const pkgPath = relativePath === '.'
|
|
29
|
+
? path_1.default.join(monorepoRoot(), 'package.json')
|
|
30
|
+
: path_1.default.join(monorepoRoot(), relativePath, 'package.json');
|
|
31
|
+
return readVersionFromPackageJson(pkgPath);
|
|
32
|
+
}
|
|
33
|
+
function readInstalledVersion(packageName) {
|
|
34
|
+
try {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const pkgPath = require.resolve(`${packageName}/package.json`);
|
|
37
|
+
return readVersionFromPackageJson(pkgPath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readPublishedVersion(packageName) {
|
|
44
|
+
try {
|
|
45
|
+
return (0, child_process_1.execSync)(`npm view ${packageName} version`, {
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
48
|
+
}).trim();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function toVersionRange(version) {
|
|
55
|
+
return version ? `^${version}` : 'latest';
|
|
56
|
+
}
|
|
57
|
+
/** Resolve a semver range for @maxal_studio/kratosjs. */
|
|
58
|
+
function kratosCoreDep(local) {
|
|
59
|
+
if (local) {
|
|
60
|
+
return 'file:..';
|
|
61
|
+
}
|
|
62
|
+
return toVersionRange(readInstalledVersion('@maxal_studio/kratosjs') ??
|
|
63
|
+
readMonorepoVersion('.') ??
|
|
64
|
+
readPublishedVersion('@maxal_studio/kratosjs'));
|
|
65
|
+
}
|
|
66
|
+
/** Resolve a semver range for @maxal_studio/kratosjs-react. */
|
|
67
|
+
function kratosReactDep(local) {
|
|
68
|
+
if (local) {
|
|
69
|
+
return 'file:../packages/kratosjs-react';
|
|
70
|
+
}
|
|
71
|
+
return toVersionRange(readInstalledVersion('@maxal_studio/kratosjs-react') ??
|
|
72
|
+
readMonorepoVersion('packages/kratosjs-react') ??
|
|
73
|
+
readPublishedVersion('@maxal_studio/kratosjs-react'));
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maxal_studio/kratosjs-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command-line interface for scaffolding KratosJs apps and plugins",
|
|
5
|
+
"bin": {
|
|
6
|
+
"kratosjs": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"templates"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "npm run clean && tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
14
|
+
"clean": "rm -rf dist",
|
|
15
|
+
"lint": "eslint ./src"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"kratosjs",
|
|
19
|
+
"cli",
|
|
20
|
+
"scaffold",
|
|
21
|
+
"generator"
|
|
22
|
+
],
|
|
23
|
+
"author": "MaxAl",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+ssh://git@github.com/maxal-studio/kratosjs.git"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.5.2",
|
|
31
|
+
"commander": "^15.0.0",
|
|
32
|
+
"picocolors": "^1.1.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.9.3",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<!-- VALAJS_PANEL_FAVICON -->
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<!-- VALAJS_PANEL_TITLE -->
|
|
8
|
+
<!-- VALAJS_PANEL_SETTINGS -->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/admin/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mountAdminPanel } from '@maxal_studio/kratosjs-react';
|
|
2
|
+
import '@maxal_studio/kratosjs-react/styles.css';
|
|
3
|
+
|
|
4
|
+
// Import plugin client manifests here, e.g.:
|
|
5
|
+
// import starRating from '@maxal_studio/kratosjs-plugin-star-rating/client';
|
|
6
|
+
|
|
7
|
+
mountAdminPanel({
|
|
8
|
+
plugins: [],
|
|
9
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
PORT="3000"
|
|
2
|
+
|
|
3
|
+
# ------------------------------------------------------------
|
|
4
|
+
# Database configuration ({{driverLabel}})
|
|
5
|
+
# ------------------------------------------------------------
|
|
6
|
+
{{envVars}}
|
|
7
|
+
|
|
8
|
+
# ------------------------------------------------------------
|
|
9
|
+
# Auth
|
|
10
|
+
# ------------------------------------------------------------
|
|
11
|
+
JWT_SECRET="your-secret-key-change-in-production"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# {{appName}}
|
|
2
|
+
|
|
3
|
+
A [KratosJs](https://github.com/maxal-studio/kratosjs) admin panel using **{{driverLabel}}**.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
1. Copy the environment file and adjust the database settings:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cp .env.example .env
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Install dependencies (if you skipped `--install`):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Start the dev server (API + admin client with HMR):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The admin panel will be available at `http://localhost:3000`.
|
|
26
|
+
|
|
27
|
+
Default login (seeded automatically on first boot):
|
|
28
|
+
|
|
29
|
+
- **Email:** `admin@example.com`
|
|
30
|
+
- **Password:** `password`
|
|
31
|
+
|
|
32
|
+
## Project structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/
|
|
36
|
+
index.ts # Panel definition, ORM config, auth, server bootstrap
|
|
37
|
+
entities/User.ts # MikroORM entity (driver-agnostic schema)
|
|
38
|
+
resources/UserResource.ts # Admin resource (form + table) for users
|
|
39
|
+
seedAdminUser.ts # Seeds the demo admin user on first boot
|
|
40
|
+
admin/main.tsx # Admin client entry — register plugin client manifests here
|
|
41
|
+
migrations/ # Generated migrations (mikro-orm migration:create)
|
|
42
|
+
index.html # Admin client HTML shell
|
|
43
|
+
vite.config.mts # Admin client Vite config (kratosAdminVite)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The database schema is created automatically on first boot via `updateSchema: true`.
|
|
47
|
+
When you are ready for versioned migrations, generate them with
|
|
48
|
+
`npx mikro-orm migration:create` and register them on the panel.
|
|
49
|
+
|
|
50
|
+
## Building for production
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run build # compiles the server (tsc) and the admin client (vite build)
|
|
54
|
+
npm start # runs the compiled server with NODE_ENV=production
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Adding a plugin
|
|
58
|
+
|
|
59
|
+
Install a KratosJs plugin package, register its server class in `src/index.ts`
|
|
60
|
+
via `.plugins([...])`, and (if it ships UI) import its client manifest in
|
|
61
|
+
`src/admin/main.tsx`. See the docs for details.
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<!-- VALAJS_PANEL_FAVICON -->
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<!-- VALAJS_PANEL_TITLE -->
|
|
8
|
+
<!-- VALAJS_PANEL_SETTINGS -->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/admin/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{appName}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "A KratosJs admin panel ({{driverLabel}})",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"build": "npm run build:server && npm run build:admin",
|
|
9
|
+
"build:server": "tsc",
|
|
10
|
+
"build:admin": "vite build",
|
|
11
|
+
"start": "NODE_ENV=production node dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {{dependencies}},
|
|
14
|
+
"devDependencies": {{devDependencies}}
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { mountAdminPanel } from '@maxal_studio/kratosjs-react';
|
|
2
|
+
import '@maxal_studio/kratosjs-react/styles.css';
|
|
3
|
+
|
|
4
|
+
// Import plugin client manifests here, e.g.:
|
|
5
|
+
// import starRating from '@maxal_studio/kratosjs-plugin-star-rating/client';
|
|
6
|
+
|
|
7
|
+
// Languages are configured once on the backend (src/index.ts) and injected into
|
|
8
|
+
// the page, so no i18n config is needed here.
|
|
9
|
+
mountAdminPanel({
|
|
10
|
+
plugins: [],
|
|
11
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core';
|
|
2
|
+
|
|
3
|
+
export interface IUser {
|
|
4
|
+
{{idInterfaceFields}}
|
|
5
|
+
firstname: string;
|
|
6
|
+
lastname?: string;
|
|
7
|
+
email: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
phone?: string;
|
|
10
|
+
profileMediaImage?: { key: string; bucket: string; url?: string } | null;
|
|
11
|
+
active: boolean;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User entity ({{driverLabel}})
|
|
17
|
+
*/
|
|
18
|
+
export const User = new EntitySchema<IUser>({
|
|
19
|
+
name: 'User',
|
|
20
|
+
properties: {
|
|
21
|
+
{{idProps}}
|
|
22
|
+
firstname: { type: 'string' },
|
|
23
|
+
lastname: { type: 'string', nullable: true },
|
|
24
|
+
email: { type: 'string', unique: true },
|
|
25
|
+
password: { type: 'string', hidden: true },
|
|
26
|
+
phone: { type: 'string', nullable: true },
|
|
27
|
+
profileMediaImage: { type: 'json', nullable: true },
|
|
28
|
+
active: { type: 'boolean', default: true },
|
|
29
|
+
createdAt: { type: 'Date', onCreate: () => new Date() },
|
|
30
|
+
} as any,
|
|
31
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { hashPassword, type ResourceHooks, type HookContext } from '@maxal_studio/kratosjs';
|
|
2
|
+
|
|
3
|
+
const capitalize = (str: string | undefined): string => {
|
|
4
|
+
if (!str || typeof str !== 'string') return str || '';
|
|
5
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const userHooks: ResourceHooks = {
|
|
9
|
+
beforeCreate: [
|
|
10
|
+
async (ctx: HookContext) => {
|
|
11
|
+
const data = ctx.input.data?.[0];
|
|
12
|
+
if (!data) return;
|
|
13
|
+
|
|
14
|
+
if (data.firstname) {
|
|
15
|
+
data.firstname = capitalize(data.firstname);
|
|
16
|
+
}
|
|
17
|
+
if (data.lastname) {
|
|
18
|
+
data.lastname = capitalize(data.lastname);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
beforeUpdate: [
|
|
23
|
+
async (ctx: HookContext) => {
|
|
24
|
+
const data = ctx.input.data?.[0];
|
|
25
|
+
if (!data) return;
|
|
26
|
+
|
|
27
|
+
if (data.firstname) {
|
|
28
|
+
data.firstname = capitalize(data.firstname);
|
|
29
|
+
}
|
|
30
|
+
if (data.lastname) {
|
|
31
|
+
data.lastname = capitalize(data.lastname);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
// Hash the password AFTER validation, so length rules (e.g. min/max) check the
|
|
36
|
+
// raw password the user typed — not the 60-char bcrypt hash. This handler runs
|
|
37
|
+
// for both create and update. On update, an empty password is dropped so it
|
|
38
|
+
// doesn't overwrite the stored hash.
|
|
39
|
+
afterValidate: [
|
|
40
|
+
async (ctx: HookContext) => {
|
|
41
|
+
const data = ctx.input.data?.[0];
|
|
42
|
+
if (!data) return;
|
|
43
|
+
|
|
44
|
+
if (data.password) {
|
|
45
|
+
data.password = await hashPassword(data.password);
|
|
46
|
+
} else {
|
|
47
|
+
delete data.password;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Panel, LocalMediaAdapter, EmailAuthProvider } from '@maxal_studio/kratosjs';
|
|
4
|
+
{{driverImport}}
|
|
5
|
+
{{migratorImport}}
|
|
6
|
+
import { UserResource } from './resources/UserResource';
|
|
7
|
+
import { User } from './entities/User';
|
|
8
|
+
import { seedAdminUser } from './seedAdminUser';
|
|
9
|
+
|
|
10
|
+
const PORT = parseInt(process.env.PORT || '3000');
|
|
11
|
+
const uploadsPath = path.join(process.cwd(), 'uploads');
|
|
12
|
+
const assetsPath = path.join(process.cwd(), 'assets');
|
|
13
|
+
|
|
14
|
+
const adminPanel = Panel.make('admin')
|
|
15
|
+
.title('{{appTitle}}')
|
|
16
|
+
.favicon('/assets/icon.png')
|
|
17
|
+
.icon('/assets/icon.png')
|
|
18
|
+
.orm(
|
|
19
|
+
{{ormConfig}},
|
|
20
|
+
{ migrate: true, updateSchema: true },
|
|
21
|
+
)
|
|
22
|
+
.mediaAdapters([
|
|
23
|
+
new LocalMediaAdapter({
|
|
24
|
+
name: 'local-uploads',
|
|
25
|
+
uploadPath: uploadsPath,
|
|
26
|
+
publicUrl: `http://localhost:${PORT}/uploads`,
|
|
27
|
+
createDirectories: true,
|
|
28
|
+
isDefault: true,
|
|
29
|
+
}),
|
|
30
|
+
])
|
|
31
|
+
.resources([UserResource])
|
|
32
|
+
.plugins([]);
|
|
33
|
+
|
|
34
|
+
// Multilingual support (optional). This is the single source of truth for
|
|
35
|
+
// languages — the admin client auto-configures itself from what you register here.
|
|
36
|
+
//
|
|
37
|
+
// adminPanel
|
|
38
|
+
// .i18n({ locales: ['en', 'sq'], defaultLocale: 'en', fallbackLocale: 'en' })
|
|
39
|
+
// .registerTranslations('app', {
|
|
40
|
+
// en: { 'users.label': 'Users' },
|
|
41
|
+
// sq: { 'users.label': 'Përdoruesit' },
|
|
42
|
+
// });
|
|
43
|
+
|
|
44
|
+
// Email/password login. With `userEntity` set, `validateCredentials` and `getUserById`
|
|
45
|
+
// are provided by default (look up the user, verify the password, resolve the avatar).
|
|
46
|
+
// To customize, pass your own `validateCredentials` to EmailAuthProvider, add more
|
|
47
|
+
// providers, or pass a `getUserById` here. Map non-standard field names with `userFields`.
|
|
48
|
+
adminPanel.auth({
|
|
49
|
+
jwt: {
|
|
50
|
+
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
|
51
|
+
accessTokenExpiry: '15m',
|
|
52
|
+
refreshTokenExpiry: '7d',
|
|
53
|
+
},
|
|
54
|
+
userEntity: User,
|
|
55
|
+
providers: [new EmailAuthProvider()],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Serve static assets (including panel icon)
|
|
59
|
+
adminPanel.useStatic('/assets', assetsPath);
|
|
60
|
+
|
|
61
|
+
adminPanel
|
|
62
|
+
.start(PORT, async () => {
|
|
63
|
+
await seedAdminUser(adminPanel);
|
|
64
|
+
console.log(`🚀 {{appTitle}} running on http://localhost:${PORT}`);
|
|
65
|
+
console.log(`📊 Admin Panel API: ${adminPanel.getBasePath()}`);
|
|
66
|
+
console.log('🔐 Login: admin@example.com / password');
|
|
67
|
+
})
|
|
68
|
+
.catch((error: unknown) => {
|
|
69
|
+
console.error('Failed to start panel:', error);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseResource,
|
|
3
|
+
FormBuilder,
|
|
4
|
+
TextInput,
|
|
5
|
+
Toggle,
|
|
6
|
+
FileUpload,
|
|
7
|
+
TableBuilder,
|
|
8
|
+
TextColumn,
|
|
9
|
+
ToggleColumn,
|
|
10
|
+
ImageColumn,
|
|
11
|
+
StatsWidget,
|
|
12
|
+
Widget,
|
|
13
|
+
type FormContext,
|
|
14
|
+
} from '@maxal_studio/kratosjs';
|
|
15
|
+
import { User } from '../entities/User';
|
|
16
|
+
import { userHooks } from '../hooks/userHooks';
|
|
17
|
+
|
|
18
|
+
export class UserResource extends BaseResource {
|
|
19
|
+
static slug = 'users';
|
|
20
|
+
|
|
21
|
+
static entity = User;
|
|
22
|
+
|
|
23
|
+
static label = 'User';
|
|
24
|
+
static pluralLabel = 'Users';
|
|
25
|
+
static icon = 'Users';
|
|
26
|
+
static navigationGroup = 'App';
|
|
27
|
+
static navigationSort = 1;
|
|
28
|
+
|
|
29
|
+
static recordTitleAttribute = (record: any) =>
|
|
30
|
+
record.lastname ? `${record.firstname} ${record.lastname}` : record.firstname;
|
|
31
|
+
static recordFeaturedImageAttribute = 'profileMediaImage';
|
|
32
|
+
static globallySearchableAttributes = ['firstname', 'lastname', 'email'];
|
|
33
|
+
|
|
34
|
+
static form() {
|
|
35
|
+
return FormBuilder.make().schema([
|
|
36
|
+
FileUpload.make('profileMediaImage').label('Profile Image').image(),
|
|
37
|
+
TextInput.make('password')
|
|
38
|
+
.label('Password')
|
|
39
|
+
.password()
|
|
40
|
+
.required((context: FormContext) => context?.operation === 'create')
|
|
41
|
+
.min(8)
|
|
42
|
+
.max(50)
|
|
43
|
+
.hidden((context: FormContext) => context?.operation === 'view'),
|
|
44
|
+
TextInput.make('firstname').label('First name').required().min(2).max(50),
|
|
45
|
+
TextInput.make('lastname').label('Last name').max(50),
|
|
46
|
+
TextInput.make('email').label('Email').email().required(),
|
|
47
|
+
TextInput.make('phone').label('Phone Number').placeholder('Enter phone number...'),
|
|
48
|
+
Toggle.make('active').label('Active').default(true),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static table() {
|
|
53
|
+
return TableBuilder.make()
|
|
54
|
+
.columns([
|
|
55
|
+
ImageColumn.make('profileMediaImage').label('Profile').circular(),
|
|
56
|
+
TextColumn.make('firstname').label('First name').sortable().searchable(),
|
|
57
|
+
TextColumn.make('lastname').label('Last name').sortable().searchable(),
|
|
58
|
+
TextColumn.make('email').label('Email').sortable().searchable(),
|
|
59
|
+
ToggleColumn.make('active').label('Active').sortable(),
|
|
60
|
+
TextColumn.make('createdAt').label('Created').sortable().dateTime(),
|
|
61
|
+
])
|
|
62
|
+
.searchable()
|
|
63
|
+
.paginate(10)
|
|
64
|
+
.defaultSort('createdAt', 'desc');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static hooks() {
|
|
68
|
+
return userHooks;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static widgets(): Widget[] {
|
|
72
|
+
return [
|
|
73
|
+
StatsWidget.make('totalUsers')
|
|
74
|
+
.label('Total Users')
|
|
75
|
+
.icon('Users')
|
|
76
|
+
.render(async (em, entity) => em.count(entity, {})),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { hashPassword, type Panel } from '@maxal_studio/kratosjs';
|
|
2
|
+
import { User } from './entities/User';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ADMIN_EMAIL = 'admin@example.com';
|
|
5
|
+
const DEFAULT_ADMIN_PASSWORD = 'password';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure a default admin user exists for local development login.
|
|
9
|
+
*/
|
|
10
|
+
export async function seedAdminUser(panel: Panel): Promise<void> {
|
|
11
|
+
const em = panel.getOrm().em.fork();
|
|
12
|
+
const existing = await em.findOne(User, { email: DEFAULT_ADMIN_EMAIL });
|
|
13
|
+
|
|
14
|
+
if (existing) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hashedPassword = await hashPassword(DEFAULT_ADMIN_PASSWORD);
|
|
19
|
+
const user = em.create(User, {
|
|
20
|
+
firstname: 'Admin',
|
|
21
|
+
email: DEFAULT_ADMIN_EMAIL,
|
|
22
|
+
password: hashedPassword,
|
|
23
|
+
active: true,
|
|
24
|
+
createdAt: new Date(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
em.persist(user);
|
|
28
|
+
await em.flush();
|
|
29
|
+
console.log(`👤 Seeded admin user (${DEFAULT_ADMIN_EMAIL} / ${DEFAULT_ADMIN_PASSWORD})`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"removeComments": true,
|
|
16
|
+
"moduleResolution": "node",
|
|
17
|
+
"allowSyntheticDefaultImports": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "**/*.tsx"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Plugin, Panel } from '@maxal_studio/kratosjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* {{PluginName}} Plugin
|
|
5
|
+
*
|
|
6
|
+
* Implement `register` to extend a panel: register custom component names,
|
|
7
|
+
* add routes, register migrations, or wire up resources.
|
|
8
|
+
*/
|
|
9
|
+
export class {{PluginName}}Plugin extends Plugin {
|
|
10
|
+
getName(): string {
|
|
11
|
+
return '{{pluginName}}';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
register(panel: Panel): void {
|
|
15
|
+
{{registerBody}}
|
|
16
|
+
|
|
17
|
+
// Register the plugin's translations (labels + any frontend UI strings) once,
|
|
18
|
+
// here on the backend, under the plugin namespace. They are injected into the
|
|
19
|
+
// admin page, so the client uses `t('{{pluginName}}:...')` with no extra setup.
|
|
20
|
+
// panel.registerTranslations('{{pluginName}}', {
|
|
21
|
+
// en: { 'hint': 'Tap to rate' },
|
|
22
|
+
// sq: { 'hint': 'Prekni për të vlerësuar' },
|
|
23
|
+
// });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist/server",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "src/client/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { definePluginClient } from '@maxal_studio/kratosjs-react';
|
|
2
|
+
import {{PluginName}}Field from './{{PluginName}}Field';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client manifest. Import this from your app's `src/admin/main.tsx`:
|
|
6
|
+
*
|
|
7
|
+
* import {{pluginCamel}} from 'kratosjs-plugin-{{pluginName}}/client';
|
|
8
|
+
* mountAdminPanel({ plugins: [{{pluginCamel}}] });
|
|
9
|
+
*
|
|
10
|
+
* Translations are NOT declared here — register them on the backend in the
|
|
11
|
+
* plugin's `register()` via `panel.registerTranslations('{{pluginName}}', {...})`.
|
|
12
|
+
* The server injects them into the page and the client picks them up automatically.
|
|
13
|
+
*/
|
|
14
|
+
export default definePluginClient({
|
|
15
|
+
name: '{{pluginName}}',
|
|
16
|
+
fields: { '{{pluginName}}': {{PluginName}}Field },
|
|
17
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { FieldProps, ViewFieldWrapper } from '@maxal_studio/kratosjs-react';
|
|
3
|
+
import { useFormContext } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sample custom field component for the {{PluginName}} plugin.
|
|
7
|
+
* Replace the body with your own UI.
|
|
8
|
+
*/
|
|
9
|
+
export default function {{PluginName}}Field(props: FieldProps) {
|
|
10
|
+
if (props.mode === 'view') {
|
|
11
|
+
return (
|
|
12
|
+
<ViewFieldWrapper label={props.label}>
|
|
13
|
+
<span>{props.value ?? '—'}</span>
|
|
14
|
+
</ViewFieldWrapper>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { register } = useFormContext();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="mb-4">
|
|
22
|
+
{props.label && (
|
|
23
|
+
<label className="block text-sm font-medium kratosjstext-primary mb-2">{props.label}</label>
|
|
24
|
+
)}
|
|
25
|
+
<input type="text" className="w-full rounded border px-3 py-2" {...register(props.name)} />
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"outDir": "./dist/client",
|
|
10
|
+
"rootDir": "./src/client",
|
|
11
|
+
"strict": false,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"isolatedModules": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/client/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|