bank20baht-cli 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nattapong Promthong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -21,7 +21,13 @@ export function runCreate(input, deps) {
21
21
  deps.log.step(`${blueprint.files.length} files written to ${blueprint.root}`);
22
22
  for (const c of blueprint.postCommands) {
23
23
  deps.log.step(c.label);
24
- deps.runner.run(c.cmd, c.args, blueprint.root);
24
+ try {
25
+ deps.runner.run(c.cmd, c.args, blueprint.root);
26
+ }
27
+ catch (err) {
28
+ const retry = `${c.cmd} ${c.args.join(' ')}`;
29
+ throw new CreateError(`${err instanceof Error ? err.message : String(err)}\nproject files are written — cd ${input.name} and rerun: ${retry}`);
30
+ }
25
31
  }
26
32
  deps.log.info('');
27
33
  deps.log.info(`Done. Next:`);
@@ -2,17 +2,47 @@ import { buildFormDefinition, FormDslError } from '../domain/form-dsl.js';
2
2
  import { DEFAULT_CONFIG, modelDir, planForm, planInit, planModel, validateDomainName, } from '../domain/generate-plan.js';
3
3
  export class GenerateError extends Error {
4
4
  }
5
+ /**
6
+ * bahtui.json path-ish fields feed directly into fs paths (modelDir(), eden
7
+ * client wiring). A cloned repo's bahtui.json is untrusted input — reject
8
+ * anything that could escape the project root before it ever reaches a path
9
+ * join (defense in depth alongside DiskFileWriter's own root confinement).
10
+ */
11
+ const SAFE_RELATIVE_PATH = /^[A-Za-z0-9._/-]+$/;
12
+ function validateConfigPath(field, value) {
13
+ if (!SAFE_RELATIVE_PATH.test(value)) {
14
+ throw new GenerateError(`bahtui.json "${field}" contains invalid characters: ${JSON.stringify(value)}.`);
15
+ }
16
+ if (value.startsWith('/')) {
17
+ throw new GenerateError(`bahtui.json "${field}" must be a relative path: ${JSON.stringify(value)}.`);
18
+ }
19
+ if (value.split('/').includes('..')) {
20
+ throw new GenerateError(`bahtui.json "${field}" must not contain ".." segments: ${JSON.stringify(value)}.`);
21
+ }
22
+ }
23
+ function isPlainObject(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
5
26
  function loadConfig(root, reader) {
6
27
  const raw = reader.read(`${root}/bahtui.json`);
7
28
  if (raw === null) {
8
29
  throw new GenerateError('No bahtui.json here — run "bahtui init" (or create) first.');
9
30
  }
31
+ let parsed;
10
32
  try {
11
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
33
+ parsed = JSON.parse(raw);
12
34
  }
13
35
  catch {
14
36
  throw new GenerateError('bahtui.json is not valid JSON.');
15
37
  }
38
+ if (!isPlainObject(parsed)) {
39
+ throw new GenerateError('bahtui.json must contain a JSON object.');
40
+ }
41
+ const config = { ...DEFAULT_CONFIG, ...parsed };
42
+ validateConfigPath('libsDir', config.libsDir);
43
+ if (config.edenClient !== undefined)
44
+ validateConfigPath('edenClient', config.edenClient);
45
+ return config;
16
46
  }
17
47
  function emit(blueprint, dryRun, deps) {
18
48
  if (dryRun) {
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { runCreate, CreateError } from './application/create.js';
6
6
  import { runGenerate, runInit, GenerateError } from './application/generate.js';
7
7
  import { DiskTemplateSource } from './infrastructure/template-source.js';
8
- import { HbsRenderer } from './infrastructure/hbs-renderer.js';
8
+ import { TemplateRenderer } from './infrastructure/template-renderer.js';
9
9
  import { DiskFileWriter } from './infrastructure/fs-writer.js';
10
10
  import { DiskFileReader } from './infrastructure/fs-reader.js';
11
11
  import { ShellRunner } from './infrastructure/shell-runner.js';
@@ -40,13 +40,40 @@ const log = {
40
40
  step: (m) => console.log(`• ${m}`),
41
41
  error: (m) => console.error(`✖ ${m}`),
42
42
  };
43
- /** Value-carrying flags (--flag value) pulled out before positional parsing. */
43
+ /** Boolean flags recognized anywhere on the command line (no value). */
44
+ const KNOWN_FLAGS = new Set([
45
+ '-v',
46
+ '--version',
47
+ '-h',
48
+ '--help',
49
+ '--dry-run',
50
+ '--force',
51
+ '--no-install',
52
+ '--no-git',
53
+ ]);
54
+ /**
55
+ * Value-carrying flags pulled out before positional parsing. Supports both
56
+ * `--flag value` and `--flag=value`. If the token after `--flag value` looks
57
+ * like another flag (starts with `-`), the option is treated as missing
58
+ * rather than swallowing that flag as the value.
59
+ */
44
60
  function takeOption(argv, flag) {
61
+ const eqPrefix = `${flag}=`;
62
+ const eqIndex = argv.findIndex((a) => a.startsWith(eqPrefix));
63
+ if (eqIndex !== -1) {
64
+ const value = argv[eqIndex].slice(eqPrefix.length);
65
+ argv.splice(eqIndex, 1);
66
+ return value;
67
+ }
45
68
  const i = argv.indexOf(flag);
46
69
  if (i === -1)
47
70
  return undefined;
48
71
  const value = argv[i + 1];
49
- argv.splice(i, value !== undefined && !value.startsWith('-') ? 2 : 1);
72
+ if (value === undefined || value.startsWith('-')) {
73
+ argv.splice(i, 1);
74
+ return undefined;
75
+ }
76
+ argv.splice(i, 2);
50
77
  return value;
51
78
  }
52
79
  function main(rawArgv) {
@@ -57,13 +84,29 @@ function main(rawArgv) {
57
84
  const args = argv.filter((a) => !a.startsWith('-'));
58
85
  const flags = new Set(argv.filter((a) => a.startsWith('-')));
59
86
  const [command, name, sub] = args;
87
+ const unknownFlags = [...flags].filter((f) => !KNOWN_FLAGS.has(f));
88
+ if (unknownFlags.length > 0) {
89
+ log.error(`Unknown flag${unknownFlags.length > 1 ? 's' : ''}: ${unknownFlags.join(', ')}`);
90
+ return 1;
91
+ }
60
92
  if (flags.has('-v') || flags.has('--version')) {
61
- log.info(version());
62
- return 0;
93
+ try {
94
+ log.info(version());
95
+ return 0;
96
+ }
97
+ catch (err) {
98
+ log.error(`cannot read package version: ${String(err)}`);
99
+ return 1;
100
+ }
63
101
  }
64
- if (command === undefined || flags.has('-h') || flags.has('--help')) {
102
+ if (flags.has('-h') || flags.has('--help')) {
65
103
  log.info(HELP);
66
- return command === undefined && !flags.has('-h') && !flags.has('--help') ? 1 : 0;
104
+ return 0;
105
+ }
106
+ if (command === undefined) {
107
+ log.error('No command given — see usage below.');
108
+ log.error(HELP);
109
+ return 1;
67
110
  }
68
111
  const dryRun = flags.has('--dry-run');
69
112
  const force = flags.has('--force');
@@ -87,7 +130,7 @@ function main(rawArgv) {
87
130
  force,
88
131
  }, {
89
132
  templates: new DiskTemplateSource(),
90
- renderer: new HbsRenderer(),
133
+ renderer: new TemplateRenderer(),
91
134
  writer: new DiskFileWriter(),
92
135
  runner: new ShellRunner(),
93
136
  log,
@@ -117,7 +160,7 @@ function main(rawArgv) {
117
160
  return 0;
118
161
  }
119
162
  log.error(`Unknown command "${command}".`);
120
- log.info(HELP);
163
+ log.error(HELP);
121
164
  return 1;
122
165
  }
123
166
  catch (err) {
@@ -2,7 +2,7 @@
2
2
  * Template naming conventions (the whole contract between templates/ and here):
3
3
  * - `*.hbs` → rendered with the data below, suffix stripped.
4
4
  * anything else → copied verbatim (so files with `${{ }}` like ci.yml
5
- * never meet handlebars).
5
+ * never meet the renderer).
6
6
  * - leading `_` on a basename → `.` (npm pack silently drops/renames real
7
7
  * .gitignore files inside packages, so templates ship `_gitignore`).
8
8
  */
@@ -28,6 +28,14 @@ const titleCase = (key) => key
28
28
  .split('_')
29
29
  .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
30
30
  .join(' ');
31
+ /** `min=`/`max=` must be a real, non-empty number — never silently coerced to NaN/0. */
32
+ function parseNumberMod(name, raw, part) {
33
+ const n = Number(raw);
34
+ if (raw.trim() === '' || !Number.isFinite(n)) {
35
+ throw new FormDslError(`Bad ${name} value ${JSON.stringify(raw)} in "${part}" — expected a number.`);
36
+ }
37
+ return n;
38
+ }
31
39
  export function parseFieldsDsl(dsl) {
32
40
  const fields = [];
33
41
  const parts = dsl
@@ -49,16 +57,23 @@ export function parseFieldsDsl(dsl) {
49
57
  if (mod === 'required')
50
58
  field.required = true;
51
59
  else if (mod.startsWith('options=')) {
52
- field.options = mod
60
+ const opts = mod
53
61
  .slice('options='.length)
54
62
  .split('|')
55
- .filter(Boolean)
56
- .map((o) => ({ key: o, label: titleCase(o) }));
63
+ .map((o) => o.trim())
64
+ .filter(Boolean);
65
+ if (opts.length === 0) {
66
+ throw new FormDslError(`options modifier needs at least one value in "${part}".`);
67
+ }
68
+ if (new Set(opts).size !== opts.length) {
69
+ throw new FormDslError(`Duplicate option keys in "${part}".`);
70
+ }
71
+ field.options = opts.map((o) => ({ key: o, label: titleCase(o) }));
57
72
  }
58
73
  else if (mod.startsWith('min='))
59
- field.min = Number(mod.slice(4));
74
+ field.min = parseNumberMod('min', mod.slice(4), part);
60
75
  else if (mod.startsWith('max='))
61
- field.max = Number(mod.slice(4));
76
+ field.max = parseNumberMod('max', mod.slice(4), part);
62
77
  else if (mod.startsWith('flag='))
63
78
  field.flag = mod.slice(5);
64
79
  else
@@ -83,7 +98,7 @@ export function buildFormDefinition(domain, name, dsl) {
83
98
  return parseSchema(JSON.stringify(form));
84
99
  }
85
100
  catch (err) {
86
- throw new FormDslError(`Engine rejected the form spec: ${err.message}`);
101
+ throw new FormDslError(`Engine rejected the form spec: ${err instanceof Error ? err.message : String(err)}`);
87
102
  }
88
103
  }
89
104
  export class FormDslError extends Error {
@@ -7,8 +7,13 @@ export class DiskFileReader {
7
7
  try {
8
8
  return readFileSync(path, 'utf8');
9
9
  }
10
- catch {
11
- return null;
10
+ catch (err) {
11
+ // null strictly means "not there" — callers use it to decide the file is
12
+ // absent (e.g. "run bahtui init first"). A permission or IO error must
13
+ // surface, not masquerade as a missing file.
14
+ if (err.code === 'ENOENT')
15
+ return null;
16
+ throw err;
12
17
  }
13
18
  }
14
19
  }
@@ -1,9 +1,12 @@
1
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve, sep } from 'node:path';
3
3
  export class DiskFileWriter {
4
4
  ensureUsableDir(dir, force) {
5
5
  if (!existsSync(dir))
6
6
  return;
7
+ if (!statSync(dir).isDirectory()) {
8
+ throw new Error(`${dir} exists and is not a directory.`);
9
+ }
7
10
  if (readdirSync(dir).length === 0)
8
11
  return;
9
12
  if (force)
@@ -11,8 +14,12 @@ export class DiskFileWriter {
11
14
  throw new Error(`${dir} already exists and is not empty (use --force to write anyway).`);
12
15
  }
13
16
  writeFiles(blueprint) {
17
+ const root = resolve(blueprint.root);
14
18
  for (const file of blueprint.files) {
15
- const abs = join(blueprint.root, file.path);
19
+ const abs = resolve(root, file.path);
20
+ if (abs !== root && !abs.startsWith(root + sep)) {
21
+ throw new Error(`Refusing to write "${file.path}" — it resolves outside the target directory ${root}.`);
22
+ }
16
23
  mkdirSync(dirname(abs), { recursive: true });
17
24
  writeFileSync(abs, file.contents);
18
25
  }
@@ -2,8 +2,12 @@ import { spawnSync } from 'node:child_process';
2
2
  export class ShellRunner {
3
3
  run(cmd, args, cwd) {
4
4
  const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' });
5
- if (result.error)
5
+ if (result.error) {
6
+ if (result.error.code === 'ENOENT') {
7
+ throw new Error(`'${cmd}' not found on PATH — install it first.`);
8
+ }
6
9
  throw result.error;
10
+ }
7
11
  if (result.status !== 0) {
8
12
  throw new Error(`${cmd} ${args.join(' ')} exited with code ${result.status}`);
9
13
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The create template set's only expression form is `{{key}}` substitution —
3
+ * a full template engine (handlebars, ~1.5 MB installed) buys nothing here.
4
+ * Unknown variables throw, matching the old handlebars strict-mode behavior.
5
+ * No escaping: these are code/config files, not HTML — quotes must survive.
6
+ */
7
+ export class TemplateRenderer {
8
+ render(raw, data) {
9
+ return raw.replaceAll(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
10
+ if (!Object.hasOwn(data, key)) {
11
+ throw new Error(`unknown template variable "${key}"`);
12
+ }
13
+ return String(data[key]);
14
+ });
15
+ }
16
+ }
@@ -1,4 +1,4 @@
1
- import { readdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  /** templates/ sits at the package root, next to dist/ — hence ../../ from here. */
@@ -9,6 +9,9 @@ export class DiskTemplateSource {
9
9
  }
10
10
  load(set) {
11
11
  const setRoot = join(this.root, set);
12
+ if (!existsSync(setRoot)) {
13
+ throw new Error(`template set '${set}' missing — reinstall bank20baht-cli`);
14
+ }
12
15
  const entries = readdirSync(setRoot, { recursive: true, withFileTypes: true });
13
16
  const files = [];
14
17
  for (const entry of entries) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bank20baht-cli",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold bun-workspace DDD monorepos (React + Elysia + Eden) and bahtui generators.",
6
6
  "license": "MIT",
@@ -27,6 +27,9 @@
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
+ "engines": {
31
+ "node": ">=20.12"
32
+ },
30
33
  "bin": {
31
34
  "bahtui": "./dist/cli.js"
32
35
  },
@@ -40,8 +43,7 @@
40
43
  "test:watch": "vitest"
41
44
  },
42
45
  "dependencies": {
43
- "handlebars": "^4.7.8",
44
- "bank20baht-validator": "^0.0.5"
46
+ "bank20baht-validator": "^0.1.0"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@types/node": "^22",
@@ -12,7 +12,7 @@ export default function App() {
12
12
  }, []);
13
13
 
14
14
  return (
15
- <main style={ { fontFamily: 'system-ui', padding: '2rem' } }>
15
+ <main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
16
16
  <h1>{{name}}</h1>
17
17
  <p>{status}</p>
18
18
  </main>
@@ -1,16 +0,0 @@
1
- import { type CreateOptions } from '../domain/create-plan.js';
2
- import type { CommandRunner, FileWriter, Logger, Renderer, TemplateSource } from './ports.js';
3
- export interface CreateDeps {
4
- templates: TemplateSource;
5
- renderer: Renderer;
6
- writer: FileWriter;
7
- runner: CommandRunner;
8
- log: Logger;
9
- }
10
- export interface CreateInput extends CreateOptions {
11
- dryRun: boolean;
12
- force: boolean;
13
- }
14
- export declare function runCreate(input: CreateInput, deps: CreateDeps): void;
15
- export declare class CreateError extends Error {
16
- }
@@ -1,33 +0,0 @@
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;
@@ -1,23 +0,0 @@
1
- import type { Blueprint } from '../domain/blueprint.js';
2
- import type { TemplateFile } from '../domain/create-plan.js';
3
- /** Loads a template set (e.g. "create") shipped inside the CLI package. */
4
- export interface TemplateSource {
5
- load(set: string): TemplateFile[];
6
- }
7
- export interface Renderer {
8
- render(raw: string, data: Record<string, unknown>): string;
9
- }
10
- export interface FileWriter {
11
- /** Throws unless `dir` is missing/empty (or `force` is set). */
12
- ensureUsableDir(dir: string, force: boolean): void;
13
- writeFiles(blueprint: Blueprint): void;
14
- }
15
- export interface CommandRunner {
16
- /** Runs to completion with inherited stdio; throws on non-zero exit. */
17
- run(cmd: string, args: string[], cwd: string): void;
18
- }
19
- export interface Logger {
20
- info(message: string): void;
21
- step(message: string): void;
22
- error(message: string): void;
23
- }
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1,23 +0,0 @@
1
- /**
2
- * A Blueprint is WHAT will be generated, as plain data — no fs, no side
3
- * effects. Adapters (fs-writer, shell-runner) make it real; --dry-run just
4
- * prints it. This split is the testing seam for every generator.
5
- */
6
- export interface FileSpec {
7
- /** Path relative to the blueprint root. */
8
- path: string;
9
- contents: string;
10
- }
11
- export interface PostCommand {
12
- cmd: string;
13
- args: string[];
14
- /** Human-readable step label for progress output. */
15
- label: string;
16
- }
17
- export interface Blueprint {
18
- /** Absolute directory every FileSpec.path is relative to. */
19
- root: string;
20
- files: FileSpec[];
21
- /** Commands to run inside `root`, in order, after files are written. */
22
- postCommands: PostCommand[];
23
- }
@@ -1,24 +0,0 @@
1
- import type { Blueprint } from './blueprint.js';
2
- export interface CreateOptions {
3
- name: string;
4
- /** Absolute path of the directory to create the project in. */
5
- targetDir: string;
6
- install: boolean;
7
- git: boolean;
8
- }
9
- /** A template file as loaded from disk by the infrastructure layer. */
10
- export interface TemplateFile {
11
- /** Path relative to the template set root, e.g. "apps/api/src/index.ts.hbs". */
12
- relPath: string;
13
- raw: string;
14
- }
15
- export type RenderFn = (raw: string, data: Record<string, unknown>) => string;
16
- /**
17
- * Template naming conventions (the whole contract between templates/ and here):
18
- * - `*.hbs` → rendered with the data below, suffix stripped.
19
- * anything else → copied verbatim (so files with `${{ }}` like ci.yml
20
- * never meet handlebars).
21
- * - leading `_` on a basename → `.` (npm pack silently drops/renames real
22
- * .gitignore files inside packages, so templates ship `_gitignore`).
23
- */
24
- export declare function planCreate(options: CreateOptions, templates: TemplateFile[], render: RenderFn): Blueprint;
@@ -1,20 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
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;
@@ -1,2 +0,0 @@
1
- /** Project-name rules: must be usable as npm scope, directory, and package name. */
2
- export declare function validateProjectName(name: string): string[];
@@ -1,5 +0,0 @@
1
- import type { FileReader } from '../application/generate.js';
2
- export declare class DiskFileReader implements FileReader {
3
- exists(path: string): boolean;
4
- read(path: string): string | null;
5
- }
@@ -1,6 +0,0 @@
1
- import type { Blueprint } from '../domain/blueprint.js';
2
- import type { FileWriter } from '../application/ports.js';
3
- export declare class DiskFileWriter implements FileWriter {
4
- ensureUsableDir(dir: string, force: boolean): void;
5
- writeFiles(blueprint: Blueprint): void;
6
- }
@@ -1,4 +0,0 @@
1
- import type { Renderer } from '../application/ports.js';
2
- export declare class HbsRenderer implements Renderer {
3
- render(raw: string, data: Record<string, unknown>): string;
4
- }
@@ -1,7 +0,0 @@
1
- import Handlebars from 'handlebars';
2
- export class HbsRenderer {
3
- render(raw, data) {
4
- // noEscape: these are code/config files, not HTML — quotes must survive.
5
- return Handlebars.compile(raw, { noEscape: true, strict: true })(data);
6
- }
7
- }
@@ -1,4 +0,0 @@
1
- import type { CommandRunner } from '../application/ports.js';
2
- export declare class ShellRunner implements CommandRunner {
3
- run(cmd: string, args: string[], cwd: string): void;
4
- }
@@ -1,7 +0,0 @@
1
- import type { TemplateFile } from '../domain/create-plan.js';
2
- import type { TemplateSource } from '../application/ports.js';
3
- export declare class DiskTemplateSource implements TemplateSource {
4
- private readonly root;
5
- constructor(root?: string);
6
- load(set: string): TemplateFile[];
7
- }