@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.
@@ -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
+ }
@@ -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
+ }
@@ -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,4 @@
1
+ import { defineConfig } from 'vite';
2
+ import { kratosAdminVite } from '@maxal_studio/kratosjs/vite';
3
+
4
+ export default defineConfig(kratosAdminVite());
@@ -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,4 @@
1
+ import { defineConfig } from 'vite';
2
+ import { kratosAdminVite } from '@maxal_studio/kratosjs/vite';
3
+
4
+ export default defineConfig(kratosAdminVite());
@@ -0,0 +1,2 @@
1
+ // Server entry: plugin class (+ any builder classes used in resources)
2
+ export { {{PluginName}}Plugin } from './{{PluginName}}Plugin';
@@ -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
+ }