bank20baht-cli 0.0.1 → 0.0.2
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/application/generate.d.ts +33 -0
- package/dist/application/generate.js +91 -0
- package/dist/cli.js +82 -29
- package/dist/domain/form-dsl.d.ts +20 -0
- package/dist/domain/form-dsl.js +90 -0
- package/dist/domain/generate-plan.d.ts +23 -0
- package/dist/domain/generate-plan.js +60 -0
- package/dist/infrastructure/fs-reader.d.ts +5 -0
- package/dist/infrastructure/fs-reader.js +14 -0
- package/package.json +3 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FileWriter, Logger } from './ports.js';
|
|
2
|
+
/** Read-only fs the generators need: existence checks + bahtui.json. */
|
|
3
|
+
export interface FileReader {
|
|
4
|
+
exists(path: string): boolean;
|
|
5
|
+
read(path: string): string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface GenerateDeps {
|
|
8
|
+
reader: FileReader;
|
|
9
|
+
writer: FileWriter;
|
|
10
|
+
log: Logger;
|
|
11
|
+
}
|
|
12
|
+
export declare class GenerateError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export interface InitInput {
|
|
15
|
+
root: string;
|
|
16
|
+
frontend: 'react' | null;
|
|
17
|
+
backend: 'elysia' | null;
|
|
18
|
+
dryRun: boolean;
|
|
19
|
+
force: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** `bahtui init` (PRD P2): bahtui.json for an existing repo, paths detected. */
|
|
22
|
+
export declare function runInit(input: InitInput, deps: GenerateDeps): void;
|
|
23
|
+
export interface GenerateInput {
|
|
24
|
+
root: string;
|
|
25
|
+
kind: 'model' | 'form';
|
|
26
|
+
/** `<domain>` for model, `<domain>/<name>` for form. */
|
|
27
|
+
target: string;
|
|
28
|
+
fields?: string;
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
force: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** `bahtui g model|form` (PRD P3). Engine-validates before any write. */
|
|
33
|
+
export declare function runGenerate(input: GenerateInput, deps: GenerateDeps): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { buildFormDefinition, FormDslError } from '../domain/form-dsl.js';
|
|
2
|
+
import { DEFAULT_CONFIG, modelDir, planForm, planInit, planModel, validateDomainName, } from '../domain/generate-plan.js';
|
|
3
|
+
export class GenerateError extends Error {
|
|
4
|
+
}
|
|
5
|
+
function loadConfig(root, reader) {
|
|
6
|
+
const raw = reader.read(`${root}/bahtui.json`);
|
|
7
|
+
if (raw === null) {
|
|
8
|
+
throw new GenerateError('No bahtui.json here — run "bahtui init" (or create) first.');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new GenerateError('bahtui.json is not valid JSON.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function emit(blueprint, dryRun, deps) {
|
|
18
|
+
if (dryRun) {
|
|
19
|
+
deps.log.info(`dry-run — would write in ${blueprint.root}:`);
|
|
20
|
+
for (const f of blueprint.files)
|
|
21
|
+
deps.log.info(` ${f.path}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
deps.writer.writeFiles(blueprint);
|
|
25
|
+
for (const f of blueprint.files)
|
|
26
|
+
deps.log.step(f.path);
|
|
27
|
+
}
|
|
28
|
+
/** `bahtui init` (PRD P2): bahtui.json for an existing repo, paths detected. */
|
|
29
|
+
export function runInit(input, deps) {
|
|
30
|
+
if (deps.reader.exists(`${input.root}/bahtui.json`) && !input.force && !input.dryRun) {
|
|
31
|
+
throw new GenerateError('bahtui.json already exists — use --force to overwrite.');
|
|
32
|
+
}
|
|
33
|
+
const config = {
|
|
34
|
+
...DEFAULT_CONFIG,
|
|
35
|
+
frontend: input.frontend,
|
|
36
|
+
backend: input.backend,
|
|
37
|
+
apps: {
|
|
38
|
+
...(deps.reader.exists(`${input.root}/apps/web`) ? { web: 'apps/web' } : {}),
|
|
39
|
+
...(deps.reader.exists(`${input.root}/apps/api`) ? { api: 'apps/api' } : {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
if (input.backend === null)
|
|
43
|
+
delete config.edenClient;
|
|
44
|
+
emit(planInit(input.root, config), input.dryRun, deps);
|
|
45
|
+
if (!input.dryRun)
|
|
46
|
+
deps.log.info('bahtui.json written — `bahtui g model <domain>` is ready.');
|
|
47
|
+
}
|
|
48
|
+
/** `bahtui g model|form` (PRD P3). Engine-validates before any write. */
|
|
49
|
+
export function runGenerate(input, deps) {
|
|
50
|
+
const config = loadConfig(input.root, deps.reader);
|
|
51
|
+
if (input.kind === 'model') {
|
|
52
|
+
const domain = input.target;
|
|
53
|
+
const errors = validateDomainName(domain);
|
|
54
|
+
if (errors.length > 0)
|
|
55
|
+
throw new GenerateError(errors.join(' '));
|
|
56
|
+
const dir = `${input.root}/${modelDir(config, domain)}`;
|
|
57
|
+
if (deps.reader.exists(dir) && !input.force) {
|
|
58
|
+
throw new GenerateError(`${modelDir(config, domain)} already exists — use --force.`);
|
|
59
|
+
}
|
|
60
|
+
emit(planModel(input.root, config, domain), input.dryRun, deps);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// form: <domain>/<name>
|
|
64
|
+
const [domain, name, ...rest] = input.target.split('/');
|
|
65
|
+
if (!domain || !name || rest.length > 0) {
|
|
66
|
+
throw new GenerateError('g form needs <domain>/<name>, e.g. "ticket/create-ticket".');
|
|
67
|
+
}
|
|
68
|
+
const nameErrors = [...validateDomainName(domain), ...validateDomainName(name)];
|
|
69
|
+
if (nameErrors.length > 0)
|
|
70
|
+
throw new GenerateError(nameErrors.join(' '));
|
|
71
|
+
if (!input.fields) {
|
|
72
|
+
throw new GenerateError('g form needs --fields, e.g. --fields "title:text:required,qty:number:min=1".');
|
|
73
|
+
}
|
|
74
|
+
if (!deps.reader.exists(`${input.root}/${modelDir(config, domain)}`)) {
|
|
75
|
+
throw new GenerateError(`${modelDir(config, domain)} does not exist — run "bahtui g model ${domain}" first.`);
|
|
76
|
+
}
|
|
77
|
+
let form;
|
|
78
|
+
try {
|
|
79
|
+
form = buildFormDefinition(domain, name, input.fields); // invalid spec = zero files
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof FormDslError)
|
|
83
|
+
throw new GenerateError(err.message);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
const filePath = `${input.root}/${modelDir(config, domain)}/src/${name}.form.json`;
|
|
87
|
+
if (deps.reader.exists(filePath) && !input.force) {
|
|
88
|
+
throw new GenerateError(`${name}.form.json already exists — use --force.`);
|
|
89
|
+
}
|
|
90
|
+
emit(planForm(input.root, config, domain, name, form), input.dryRun, deps);
|
|
91
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -3,20 +3,31 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { runCreate, CreateError } from './application/create.js';
|
|
6
|
+
import { runGenerate, runInit, GenerateError } from './application/generate.js';
|
|
6
7
|
import { DiskTemplateSource } from './infrastructure/template-source.js';
|
|
7
8
|
import { HbsRenderer } from './infrastructure/hbs-renderer.js';
|
|
8
9
|
import { DiskFileWriter } from './infrastructure/fs-writer.js';
|
|
10
|
+
import { DiskFileReader } from './infrastructure/fs-reader.js';
|
|
9
11
|
import { ShellRunner } from './infrastructure/shell-runner.js';
|
|
10
12
|
const HELP = `bahtui — bun-workspace DDD scaffolding (React + Elysia + Eden)
|
|
11
13
|
|
|
12
14
|
Usage:
|
|
13
|
-
bahtui create <name> [options]
|
|
15
|
+
bahtui create <name> [options] scaffold a new monorepo in ./<name>
|
|
16
|
+
bahtui init [options] write bahtui.json into an existing repo
|
|
17
|
+
bahtui g model <domain> libs/domain/<d>/model package
|
|
18
|
+
bahtui g form <domain>/<name> --fields "key:type:mod,…"
|
|
19
|
+
engine-validated FormDefinition JSON
|
|
20
|
+
Field DSL:
|
|
21
|
+
title:text:required,priority:dropdown:options=low|medium|high,qty:number:min=1
|
|
14
22
|
|
|
15
23
|
Options:
|
|
24
|
+
--fields <dsl> g form: the field list (required)
|
|
25
|
+
--frontend <react|none> init: default react
|
|
26
|
+
--backend <elysia|none> init: default elysia
|
|
16
27
|
--dry-run print what would be generated, write nothing
|
|
17
|
-
--no-install skip "bun install" (also skips the initial commit)
|
|
18
|
-
--no-git skip git init + initial commit
|
|
19
|
-
--force
|
|
28
|
+
--no-install create: skip "bun install" (also skips the initial commit)
|
|
29
|
+
--no-git create: skip git init + initial commit
|
|
30
|
+
--force overwrite existing files/dirs
|
|
20
31
|
-h, --help this help
|
|
21
32
|
-v, --version print version
|
|
22
33
|
`;
|
|
@@ -29,10 +40,23 @@ const log = {
|
|
|
29
40
|
step: (m) => console.log(`• ${m}`),
|
|
30
41
|
error: (m) => console.error(`✖ ${m}`),
|
|
31
42
|
};
|
|
32
|
-
|
|
43
|
+
/** Value-carrying flags (--flag value) pulled out before positional parsing. */
|
|
44
|
+
function takeOption(argv, flag) {
|
|
45
|
+
const i = argv.indexOf(flag);
|
|
46
|
+
if (i === -1)
|
|
47
|
+
return undefined;
|
|
48
|
+
const value = argv[i + 1];
|
|
49
|
+
argv.splice(i, value !== undefined && !value.startsWith('-') ? 2 : 1);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function main(rawArgv) {
|
|
53
|
+
const argv = [...rawArgv];
|
|
54
|
+
const fields = takeOption(argv, '--fields');
|
|
55
|
+
const frontend = takeOption(argv, '--frontend');
|
|
56
|
+
const backend = takeOption(argv, '--backend');
|
|
33
57
|
const args = argv.filter((a) => !a.startsWith('-'));
|
|
34
58
|
const flags = new Set(argv.filter((a) => a.startsWith('-')));
|
|
35
|
-
const [command, name] = args;
|
|
59
|
+
const [command, name, sub] = args;
|
|
36
60
|
if (flags.has('-v') || flags.has('--version')) {
|
|
37
61
|
log.info(version());
|
|
38
62
|
return 0;
|
|
@@ -41,34 +65,63 @@ function main(argv) {
|
|
|
41
65
|
log.info(HELP);
|
|
42
66
|
return command === undefined && !flags.has('-h') && !flags.has('--help') ? 1 : 0;
|
|
43
67
|
}
|
|
44
|
-
|
|
68
|
+
const dryRun = flags.has('--dry-run');
|
|
69
|
+
const force = flags.has('--force');
|
|
70
|
+
const generateDeps = {
|
|
71
|
+
reader: new DiskFileReader(),
|
|
72
|
+
writer: new DiskFileWriter(),
|
|
73
|
+
log,
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
if (command === 'create') {
|
|
77
|
+
if (!name) {
|
|
78
|
+
log.error('create needs a project name: bahtui create <name>');
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
runCreate({
|
|
82
|
+
name,
|
|
83
|
+
targetDir: resolve(process.cwd(), name),
|
|
84
|
+
install: !flags.has('--no-install'),
|
|
85
|
+
git: !flags.has('--no-git'),
|
|
86
|
+
dryRun,
|
|
87
|
+
force,
|
|
88
|
+
}, {
|
|
89
|
+
templates: new DiskTemplateSource(),
|
|
90
|
+
renderer: new HbsRenderer(),
|
|
91
|
+
writer: new DiskFileWriter(),
|
|
92
|
+
runner: new ShellRunner(),
|
|
93
|
+
log,
|
|
94
|
+
});
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (command === 'init') {
|
|
98
|
+
runInit({
|
|
99
|
+
root: process.cwd(),
|
|
100
|
+
frontend: frontend === 'none' ? null : 'react',
|
|
101
|
+
backend: backend === 'none' ? null : 'elysia',
|
|
102
|
+
dryRun,
|
|
103
|
+
force,
|
|
104
|
+
}, generateDeps);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
if (command === 'g') {
|
|
108
|
+
if (name !== 'model' && name !== 'form') {
|
|
109
|
+
log.error('Unknown generator — bahtui g model <domain> | g form <domain>/<name>.');
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
if (!sub) {
|
|
113
|
+
log.error(`g ${name} needs a target: bahtui g ${name} <${name === 'model' ? 'domain' : 'domain/name'}>`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
runGenerate({ root: process.cwd(), kind: name, target: sub, fields, dryRun, force }, generateDeps);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
45
119
|
log.error(`Unknown command "${command}".`);
|
|
46
120
|
log.info(HELP);
|
|
47
121
|
return 1;
|
|
48
122
|
}
|
|
49
|
-
if (!name) {
|
|
50
|
-
log.error('create needs a project name: bahtui create <name>');
|
|
51
|
-
return 1;
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
runCreate({
|
|
55
|
-
name,
|
|
56
|
-
targetDir: resolve(process.cwd(), name),
|
|
57
|
-
install: !flags.has('--no-install'),
|
|
58
|
-
git: !flags.has('--no-git'),
|
|
59
|
-
dryRun: flags.has('--dry-run'),
|
|
60
|
-
force: flags.has('--force'),
|
|
61
|
-
}, {
|
|
62
|
-
templates: new DiskTemplateSource(),
|
|
63
|
-
renderer: new HbsRenderer(),
|
|
64
|
-
writer: new DiskFileWriter(),
|
|
65
|
-
runner: new ShellRunner(),
|
|
66
|
-
log,
|
|
67
|
-
});
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
70
123
|
catch (err) {
|
|
71
|
-
log.error(err instanceof CreateError ? err.message : String(err));
|
|
124
|
+
log.error(err instanceof CreateError || err instanceof GenerateError ? err.message : String(err));
|
|
72
125
|
return 1;
|
|
73
126
|
}
|
|
74
127
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FormDefinition } from 'bank20baht-validator';
|
|
2
|
+
export interface DslField {
|
|
3
|
+
order: number;
|
|
4
|
+
type: string;
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
options?: Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}>;
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
flag?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseFieldsDsl(dsl: string): DslField[];
|
|
17
|
+
/** DSL -> FormDefinition, engine-validated. Throws FormDslError on any problem. */
|
|
18
|
+
export declare function buildFormDefinition(domain: string, name: string, dsl: string): FormDefinition;
|
|
19
|
+
export declare class FormDslError extends Error {
|
|
20
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parseSchema } from 'bank20baht-validator';
|
|
2
|
+
/**
|
|
3
|
+
* Compact field DSL for `bahtui g form` (spec 04-adjacent, PRD P3):
|
|
4
|
+
*
|
|
5
|
+
* --fields "title:text:required,priority:dropdown:options=low|medium|high,qty:number"
|
|
6
|
+
*
|
|
7
|
+
* field = key ':' type (':' modifier)*
|
|
8
|
+
* mods = 'required' | 'options=a|b|c' | 'min=N' | 'max=N' | 'flag=NAME'
|
|
9
|
+
*
|
|
10
|
+
* The result is engine-validated (parseSchema) BEFORE any file is written —
|
|
11
|
+
* an invalid spec exits 1 with zero files (the P1 guarantee, kept).
|
|
12
|
+
*/
|
|
13
|
+
const FIELD_TYPES = new Set([
|
|
14
|
+
'text',
|
|
15
|
+
'textarea',
|
|
16
|
+
'number',
|
|
17
|
+
'email',
|
|
18
|
+
'dropdown',
|
|
19
|
+
'multiselect',
|
|
20
|
+
'checkbox',
|
|
21
|
+
'radio',
|
|
22
|
+
'toggle',
|
|
23
|
+
'date',
|
|
24
|
+
'file',
|
|
25
|
+
]);
|
|
26
|
+
const KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
27
|
+
const titleCase = (key) => key
|
|
28
|
+
.split('_')
|
|
29
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
|
|
30
|
+
.join(' ');
|
|
31
|
+
export function parseFieldsDsl(dsl) {
|
|
32
|
+
const fields = [];
|
|
33
|
+
const parts = dsl
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((p) => p.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
if (parts.length === 0)
|
|
38
|
+
throw new FormDslError('No fields given — pass --fields "key:type,…".');
|
|
39
|
+
for (const [i, part] of parts.entries()) {
|
|
40
|
+
const [key, type, ...mods] = part.split(':').map((s) => s.trim());
|
|
41
|
+
if (!key || !KEY_PATTERN.test(key)) {
|
|
42
|
+
throw new FormDslError(`Bad field key ${JSON.stringify(key ?? '')} in "${part}".`);
|
|
43
|
+
}
|
|
44
|
+
if (!type || !FIELD_TYPES.has(type)) {
|
|
45
|
+
throw new FormDslError(`Unknown field type ${JSON.stringify(type ?? '')} in "${part}" — one of: ${[...FIELD_TYPES].join(', ')}.`);
|
|
46
|
+
}
|
|
47
|
+
const field = { order: i + 1, type, key, label: titleCase(key) };
|
|
48
|
+
for (const mod of mods) {
|
|
49
|
+
if (mod === 'required')
|
|
50
|
+
field.required = true;
|
|
51
|
+
else if (mod.startsWith('options=')) {
|
|
52
|
+
field.options = mod
|
|
53
|
+
.slice('options='.length)
|
|
54
|
+
.split('|')
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((o) => ({ key: o, label: titleCase(o) }));
|
|
57
|
+
}
|
|
58
|
+
else if (mod.startsWith('min='))
|
|
59
|
+
field.min = Number(mod.slice(4));
|
|
60
|
+
else if (mod.startsWith('max='))
|
|
61
|
+
field.max = Number(mod.slice(4));
|
|
62
|
+
else if (mod.startsWith('flag='))
|
|
63
|
+
field.flag = mod.slice(5);
|
|
64
|
+
else
|
|
65
|
+
throw new FormDslError(`Unknown modifier ${JSON.stringify(mod)} in "${part}".`);
|
|
66
|
+
}
|
|
67
|
+
fields.push(field);
|
|
68
|
+
}
|
|
69
|
+
const keys = fields.map((f) => f.key);
|
|
70
|
+
if (new Set(keys).size !== keys.length) {
|
|
71
|
+
throw new FormDslError('Field keys must be unique.');
|
|
72
|
+
}
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
/** DSL -> FormDefinition, engine-validated. Throws FormDslError on any problem. */
|
|
76
|
+
export function buildFormDefinition(domain, name, dsl) {
|
|
77
|
+
const form = {
|
|
78
|
+
formId: `${domain}-${name}`,
|
|
79
|
+
formName: titleCase(name.replace(/-/g, '_')).replace(/ /g, '_'),
|
|
80
|
+
field_data: parseFieldsDsl(dsl),
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
return parseSchema(JSON.stringify(form));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new FormDslError(`Engine rejected the form spec: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export class FormDslError extends Error {
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FormDefinition } from 'bank20baht-validator';
|
|
2
|
+
import type { Blueprint } from './blueprint.js';
|
|
3
|
+
/** bahtui.json — written by create/init, read by every `g` command (PRD). */
|
|
4
|
+
export interface BahtuiConfig {
|
|
5
|
+
apps: {
|
|
6
|
+
web?: string;
|
|
7
|
+
api?: string;
|
|
8
|
+
};
|
|
9
|
+
libsDir: string;
|
|
10
|
+
frontend: 'react' | null;
|
|
11
|
+
backend: 'elysia' | null;
|
|
12
|
+
edenClient?: string;
|
|
13
|
+
wrapperPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_CONFIG: BahtuiConfig;
|
|
16
|
+
export declare function validateDomainName(domain: string): string[];
|
|
17
|
+
/** `bahtui init` — write bahtui.json into an existing repo (root = cwd). */
|
|
18
|
+
export declare function planInit(root: string, config: BahtuiConfig): Blueprint;
|
|
19
|
+
export declare function modelDir(config: BahtuiConfig, domain: string): string;
|
|
20
|
+
/** `bahtui g model <domain>` — the framework-free domain model lib (PRD). */
|
|
21
|
+
export declare function planModel(root: string, config: BahtuiConfig, domain: string): Blueprint;
|
|
22
|
+
/** `bahtui g form <domain>/<name>` — engine-validated FormDefinition JSON. */
|
|
23
|
+
export declare function planForm(root: string, config: BahtuiConfig, domain: string, name: string, form: FormDefinition): Blueprint;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const DEFAULT_CONFIG = {
|
|
2
|
+
apps: { web: 'apps/web', api: 'apps/api' },
|
|
3
|
+
libsDir: 'libs',
|
|
4
|
+
frontend: 'react',
|
|
5
|
+
backend: 'elysia',
|
|
6
|
+
edenClient: 'libs/shared/eden',
|
|
7
|
+
};
|
|
8
|
+
const DOMAIN_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
9
|
+
export function validateDomainName(domain) {
|
|
10
|
+
return DOMAIN_PATTERN.test(domain)
|
|
11
|
+
? []
|
|
12
|
+
: [`"${domain}" is not a valid domain name — lowercase letters, digits, dashes.`];
|
|
13
|
+
}
|
|
14
|
+
/** `bahtui init` — write bahtui.json into an existing repo (root = cwd). */
|
|
15
|
+
export function planInit(root, config) {
|
|
16
|
+
return {
|
|
17
|
+
root,
|
|
18
|
+
files: [{ path: 'bahtui.json', contents: JSON.stringify(config, null, 2) + '\n' }],
|
|
19
|
+
postCommands: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function modelDir(config, domain) {
|
|
23
|
+
return `${config.libsDir}/domain/${domain}/model`;
|
|
24
|
+
}
|
|
25
|
+
/** `bahtui g model <domain>` — the framework-free domain model lib (PRD). */
|
|
26
|
+
export function planModel(root, config, domain) {
|
|
27
|
+
const dir = modelDir(config, domain);
|
|
28
|
+
const pkg = {
|
|
29
|
+
name: `domain-${domain}-model`,
|
|
30
|
+
version: '0.0.0',
|
|
31
|
+
private: true,
|
|
32
|
+
type: 'module',
|
|
33
|
+
exports: { '.': './src/index.ts' },
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
root,
|
|
37
|
+
files: [
|
|
38
|
+
{ path: `${dir}/package.json`, contents: JSON.stringify(pkg, null, 2) + '\n' },
|
|
39
|
+
{
|
|
40
|
+
path: `${dir}/src/index.ts`,
|
|
41
|
+
contents: `// ${domain} domain model — entities and form JSON live here.\n// NO react, NO elysia in this package (dependency rule: everything points AT model).\nexport {};\n`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
postCommands: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** `bahtui g form <domain>/<name>` — engine-validated FormDefinition JSON. */
|
|
48
|
+
export function planForm(root, config, domain, name, form) {
|
|
49
|
+
const dir = modelDir(config, domain);
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
files: [
|
|
53
|
+
{
|
|
54
|
+
path: `${dir}/src/${name}.form.json`,
|
|
55
|
+
contents: JSON.stringify(form, null, 2) + '\n',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
postCommands: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scaffold bun-workspace DDD monorepos (React + Elysia + Eden) and bahtui generators.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"test:watch": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"handlebars": "^4.7.8"
|
|
43
|
+
"handlebars": "^4.7.8",
|
|
44
|
+
"bank20baht-validator": "^0.0.5"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/node": "^22",
|