@shiva-fw/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.
Files changed (71) hide show
  1. package/.editorconfig +38 -0
  2. package/.gitattributes +18 -0
  3. package/.nvmrc +1 -0
  4. package/README.md +179 -0
  5. package/bin/shiva.js +4 -0
  6. package/package.json +44 -0
  7. package/recipes/full-rp.json +77 -0
  8. package/recipes/minimal.json +30 -0
  9. package/recipes/standard.json +46 -0
  10. package/src/commands/ai/context.js +89 -0
  11. package/src/commands/ai/link.js +38 -0
  12. package/src/commands/ai/mcp.js +39 -0
  13. package/src/commands/config/validate.js +65 -0
  14. package/src/commands/docs/api.js +81 -0
  15. package/src/commands/docs/build.js +14 -0
  16. package/src/commands/docs/deploy.js +14 -0
  17. package/src/commands/docs/serve.js +14 -0
  18. package/src/commands/init.js +167 -0
  19. package/src/commands/install.js +108 -0
  20. package/src/commands/locale/missing.js +83 -0
  21. package/src/commands/make/contract.js +45 -0
  22. package/src/commands/make/migration.js +69 -0
  23. package/src/commands/make/model.js +63 -0
  24. package/src/commands/make/module.js +115 -0
  25. package/src/commands/make/seed.js +51 -0
  26. package/src/commands/make/service.js +60 -0
  27. package/src/commands/make/test.js +53 -0
  28. package/src/commands/mcp.js +26 -0
  29. package/src/commands/migrate/rollback.js +155 -0
  30. package/src/commands/migrate/run.js +159 -0
  31. package/src/commands/migrate/status.js +137 -0
  32. package/src/commands/module/list.js +46 -0
  33. package/src/commands/module/status.js +64 -0
  34. package/src/commands/outdated.js +59 -0
  35. package/src/commands/remove.js +61 -0
  36. package/src/commands/seed.js +108 -0
  37. package/src/commands/test.js +88 -0
  38. package/src/commands/update.js +90 -0
  39. package/src/generators/index.js +78 -0
  40. package/src/generators/templates/contract.lua.tpl +12 -0
  41. package/src/generators/templates/migration.lua.tpl +15 -0
  42. package/src/generators/templates/model.lua.tpl +14 -0
  43. package/src/generators/templates/module/client/init.lua.tpl +5 -0
  44. package/src/generators/templates/module/config/config.lua.tpl +4 -0
  45. package/src/generators/templates/module/fxmanifest.lua.tpl +41 -0
  46. package/src/generators/templates/module/locales/en.lua.tpl +4 -0
  47. package/src/generators/templates/module/module.lua.tpl +10 -0
  48. package/src/generators/templates/module/server/init.lua.tpl +2 -0
  49. package/src/generators/templates/module/shared/init.lua.tpl +5 -0
  50. package/src/generators/templates/seed.lua.tpl +10 -0
  51. package/src/generators/templates/service.lua.tpl +7 -0
  52. package/src/generators/templates/test.lua.tpl +39 -0
  53. package/src/index.js +113 -0
  54. package/src/mcp/resources/contracts.js +68 -0
  55. package/src/mcp/resources/docs.js +56 -0
  56. package/src/mcp/resources/examples.js +235 -0
  57. package/src/mcp/server.js +121 -0
  58. package/src/mcp/tools/config.js +53 -0
  59. package/src/mcp/tools/contracts.js +37 -0
  60. package/src/mcp/tools/database.js +93 -0
  61. package/src/mcp/tools/docs.js +38 -0
  62. package/src/mcp/tools/events.js +26 -0
  63. package/src/mcp/tools/items.js +280 -0
  64. package/src/mcp/tools/modules.js +25 -0
  65. package/src/packages/lockfile.js +53 -0
  66. package/src/packages/registry.js +99 -0
  67. package/src/packages/resolver.js +83 -0
  68. package/src/utils/config-reader.js +74 -0
  69. package/src/utils/lua-annotations.js +319 -0
  70. package/src/utils/lua-parser.js +119 -0
  71. package/src/utils/server-root.js +66 -0
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ const { findServerRoot, getResourcesDir } = require('../../utils/server-root');
8
+ const { scanModules } = require('../../utils/lua-parser');
9
+ const { scanModuleAnnotations, toMarkdown } = require('../../utils/lua-annotations');
10
+
11
+ function docsApiCommand(program) {
12
+ program
13
+ .command('docs:api')
14
+ .description('Generate API reference from LuaLS annotations')
15
+ .option('-m, --module <name>', 'Generate for a specific module only')
16
+ .option('-o, --out <dir>', 'Output directory (default: ./docs/api)')
17
+ .action(async (options) => { await run(options); });
18
+ }
19
+
20
+ async function run(options) {
21
+ const serverRoot = findServerRoot(process.cwd());
22
+ if (!serverRoot) {
23
+ console.error(chalk.red('✖ Not in a Shiva server project. Run shiva init first.'));
24
+ process.exit(1);
25
+ }
26
+
27
+ let modules = scanModules(getResourcesDir(serverRoot));
28
+ if (options.module) {
29
+ const name = options.module.startsWith('shiva-') ? options.module : `shiva-${options.module}`;
30
+ modules = modules.filter(m => m.name === name || m.name === options.module);
31
+ if (modules.length === 0) {
32
+ console.error(chalk.red(`✖ Module not found: ${options.module}`));
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ const outDir = options.out
38
+ ? path.resolve(options.out)
39
+ : path.join(serverRoot, 'docs', 'api');
40
+
41
+ fs.mkdirSync(outDir, { recursive: true });
42
+
43
+ console.log('');
44
+ console.log(chalk.bold(`Scanning ${modules.length} module(s) for LuaLS annotations...\n`));
45
+
46
+ let totalClasses = 0;
47
+ let totalFns = 0;
48
+ let generated = 0;
49
+
50
+ for (const mod of modules) {
51
+ const api = scanModuleAnnotations(mod.path);
52
+ if (api.classes.length === 0 && api.functions.length === 0) {
53
+ console.log(chalk.gray(` ${mod.name} — no annotations found, skipping`));
54
+ continue;
55
+ }
56
+
57
+ totalClasses += api.classes.length;
58
+ totalFns += api.functions.length;
59
+
60
+ const markdown = toMarkdown(mod.name, api);
61
+ const outFile = path.join(outDir, `${mod.name}.md`);
62
+ fs.writeFileSync(outFile, markdown, 'utf-8');
63
+
64
+ console.log(
65
+ chalk.green(` ✔ ${mod.name}`) +
66
+ chalk.gray(` — ${api.classes.length} classes, ${api.functions.length} functions → ${path.relative(serverRoot, outFile)}`)
67
+ );
68
+ generated++;
69
+ }
70
+
71
+ if (generated === 0) {
72
+ console.log(chalk.yellow('\n No annotated modules found. Add ---@param / ---@return / ---@class to your Lua files.\n'));
73
+ return;
74
+ }
75
+
76
+ console.log('');
77
+ console.log(chalk.bold(`Generated ${generated} API doc file(s) in ${path.relative(process.cwd(), outDir)}/`));
78
+ console.log(chalk.gray(` ${totalClasses} classes, ${totalFns} functions documented\n`));
79
+ }
80
+
81
+ module.exports = docsApiCommand;
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ function docsBuildCommand(program) {
6
+ program.command('docs:build').description('Build the VitePress documentation site').action(() => {
7
+ console.log('');
8
+ console.log(chalk.cyan('docs:build is managed in the shiva-docs repository.'));
9
+ console.log(chalk.gray(' cd shiva-docs && npm run build'));
10
+ console.log('');
11
+ });
12
+ }
13
+
14
+ module.exports = docsBuildCommand;
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ function docsDeployCommand(program) {
6
+ program.command('docs:deploy').description('Deploy documentation to hosting').action(() => {
7
+ console.log('');
8
+ console.log(chalk.cyan('docs:deploy is managed in the shiva-docs repository.'));
9
+ console.log(chalk.gray(' cd shiva-docs && npm run deploy'));
10
+ console.log('');
11
+ });
12
+ }
13
+
14
+ module.exports = docsDeployCommand;
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ function docsServeCommand(program) {
6
+ program.command('docs:serve').description('Serve the documentation site locally').action(() => {
7
+ console.log('');
8
+ console.log(chalk.cyan('docs:serve is managed in the shiva-docs repository.'));
9
+ console.log(chalk.gray(' cd shiva-docs && npm run dev'));
10
+ console.log('');
11
+ });
12
+ }
13
+
14
+ module.exports = docsServeCommand;
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const inquirer = require('inquirer');
7
+
8
+ const { writeShivaConfig } = require('../utils/config-reader');
9
+
10
+ const RECIPES = {
11
+ minimal: require('../../recipes/minimal.json'),
12
+ standard: require('../../recipes/standard.json'),
13
+ 'full-rp': require('../../recipes/full-rp.json'),
14
+ };
15
+
16
+ /**
17
+ * Register the `shiva init` command.
18
+ * @param {import('commander').Command} program
19
+ */
20
+ function initCommand(program) {
21
+ program
22
+ .command('init')
23
+ .description('Initialise a new Shiva server project in the current directory')
24
+ .option('-r, --recipe <name>', 'Use a preset recipe (minimal | standard | full-rp)')
25
+ .option('-y, --yes', 'Skip prompts and use defaults')
26
+ .action(async (options) => {
27
+ await run(options);
28
+ });
29
+ }
30
+
31
+ async function run(options) {
32
+ const cwd = process.cwd();
33
+ const existingConfig = path.join(cwd, 'shiva.json');
34
+
35
+ if (fs.existsSync(existingConfig) && !options.yes) {
36
+ const { overwrite } = await inquirer.prompt([
37
+ {
38
+ type: 'confirm',
39
+ name: 'overwrite',
40
+ message: chalk.yellow('shiva.json already exists. Overwrite?'),
41
+ default: false,
42
+ },
43
+ ]);
44
+ if (!overwrite) {
45
+ console.log(chalk.gray('Aborted.'));
46
+ return;
47
+ }
48
+ }
49
+
50
+ let answers;
51
+
52
+ if (options.yes) {
53
+ answers = {
54
+ serverName: path.basename(cwd),
55
+ recipe: options.recipe || 'minimal',
56
+ dbHost: '127.0.0.1',
57
+ dbPort: 3306,
58
+ dbUser: 'root',
59
+ dbPassword: '',
60
+ dbName: 'shiva',
61
+ };
62
+ } else {
63
+ answers = await inquirer.prompt([
64
+ {
65
+ type: 'input',
66
+ name: 'serverName',
67
+ message: 'Server name:',
68
+ default: path.basename(cwd),
69
+ validate: (v) => v.trim().length > 0 || 'Server name cannot be empty',
70
+ },
71
+ {
72
+ type: 'list',
73
+ name: 'recipe',
74
+ message: 'Choose a starter recipe:',
75
+ default: options.recipe || 'minimal',
76
+ choices: [
77
+ { name: 'minimal — 24 essential modules', value: 'minimal' },
78
+ { name: 'standard — 40 modules (recommended)', value: 'standard' },
79
+ { name: 'full-rp — 71 modules, full RP experience', value: 'full-rp' },
80
+ { name: 'none — start with no modules', value: 'none' },
81
+ ],
82
+ },
83
+ {
84
+ type: 'input',
85
+ name: 'dbHost',
86
+ message: 'Database host:',
87
+ default: '127.0.0.1',
88
+ },
89
+ {
90
+ type: 'number',
91
+ name: 'dbPort',
92
+ message: 'Database port:',
93
+ default: 3306,
94
+ },
95
+ {
96
+ type: 'input',
97
+ name: 'dbUser',
98
+ message: 'Database user:',
99
+ default: 'root',
100
+ },
101
+ {
102
+ type: 'password',
103
+ name: 'dbPassword',
104
+ message: 'Database password:',
105
+ mask: '*',
106
+ },
107
+ {
108
+ type: 'input',
109
+ name: 'dbName',
110
+ message: 'Database name:',
111
+ default: 'shiva',
112
+ },
113
+ ]);
114
+ }
115
+
116
+ const recipeModules =
117
+ answers.recipe !== 'none' && RECIPES[answers.recipe]
118
+ ? RECIPES[answers.recipe].modules
119
+ : {};
120
+
121
+ const config = {
122
+ name: answers.serverName,
123
+ framework: 'shiva-core@^1.0.0',
124
+ modules: recipeModules,
125
+ database: {
126
+ host: answers.dbHost,
127
+ port: Number(answers.dbPort),
128
+ user: answers.dbUser,
129
+ password: answers.dbPassword,
130
+ database: answers.dbName,
131
+ },
132
+ };
133
+
134
+ writeShivaConfig(cwd, config);
135
+
136
+ // Create resources/[shiva] directory structure
137
+ const shivaDir = path.join(cwd, 'resources', '[shiva]');
138
+ fs.mkdirSync(shivaDir, { recursive: true });
139
+
140
+ const lockData = {
141
+ version: 1,
142
+ generatedAt: new Date().toISOString(),
143
+ modules: {},
144
+ };
145
+ fs.writeFileSync(path.join(cwd, 'shiva.lock'), JSON.stringify(lockData, null, 2) + '\n', 'utf-8');
146
+
147
+ console.log('');
148
+ console.log(chalk.green('✔ Created shiva.json'));
149
+ console.log(chalk.green('✔ Created shiva.lock'));
150
+ console.log(chalk.green('✔ Created resources/[shiva]/'));
151
+ console.log('');
152
+
153
+ if (answers.recipe !== 'none') {
154
+ const moduleCount = Object.keys(recipeModules).length;
155
+ console.log(chalk.cyan(` Recipe: ${answers.recipe} (${moduleCount} modules)`));
156
+ console.log(chalk.gray(' Run `shiva install` to download and install the modules.'));
157
+ }
158
+
159
+ console.log('');
160
+ console.log(chalk.bold('Next steps:'));
161
+ console.log(chalk.gray(' shiva install # Install modules from shiva.json'));
162
+ console.log(chalk.gray(' shiva migrate # Run database migrations'));
163
+ console.log(chalk.gray(' shiva module:list # List installed modules'));
164
+ console.log('');
165
+ }
166
+
167
+ module.exports = initCommand;
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const chalk = require('chalk');
6
+
7
+ const { requireServerRoot, getShivaModulesDir } = require('../utils/server-root');
8
+ const { readShivaConfig, writeShivaConfig } = require('../utils/config-reader');
9
+ const { lockModule } = require('../packages/lockfile');
10
+ const { buildInstallPlan } = require('../packages/resolver');
11
+ const registry = require('../packages/registry');
12
+
13
+ function installCommand(program) {
14
+ program
15
+ .command('install [module]')
16
+ .description('Install a module (or all modules from shiva.json)')
17
+ .option('--no-lock', 'Skip updating shiva.lock')
18
+ .action(async (moduleName, options) => { await run(moduleName, options); });
19
+ }
20
+
21
+ async function run(moduleName, options) {
22
+ const serverRoot = requireServerRoot();
23
+ const config = readShivaConfig(serverRoot);
24
+ const regUrl = registry.getRegistryUrl(config);
25
+
26
+ let toInstall;
27
+
28
+ if (moduleName) {
29
+ const [name, version] = moduleName.split('@');
30
+ toInstall = { [name]: version ? version : 'latest' };
31
+ } else {
32
+ toInstall = config.modules || {};
33
+ if (Object.keys(toInstall).length === 0) {
34
+ console.log(chalk.gray(' No modules declared in shiva.json.'));
35
+ return;
36
+ }
37
+ }
38
+
39
+ console.log('');
40
+ console.log(chalk.bold('Resolving dependencies...'));
41
+
42
+ let plan;
43
+ try {
44
+ plan = await buildInstallPlan(
45
+ toInstall,
46
+ (name) => registry.fetchVersions(regUrl, name),
47
+ async (name, version) => {
48
+ const meta = await registry.fetchModuleMeta(regUrl, name, version);
49
+ return meta.dependencies || {};
50
+ }
51
+ );
52
+ } catch (err) {
53
+ console.error(chalk.red(`✖ Resolution failed: ${err.message}`));
54
+ console.error(chalk.gray(' Is the registry reachable? Check your network or set "registry" in shiva.json.'));
55
+ process.exit(1);
56
+ }
57
+
58
+ const modulesDir = getShivaModulesDir(serverRoot);
59
+ fs.mkdirSync(modulesDir, { recursive: true });
60
+
61
+ console.log('');
62
+ for (const [name, version] of plan.entries()) {
63
+ const destDir = path.join(modulesDir, name);
64
+
65
+ if (version.startsWith('file:')) {
66
+ const src = path.resolve(serverRoot, version.slice(5));
67
+ console.log(chalk.gray(` Linking ${name} → ${src}`));
68
+ if (!fs.existsSync(destDir)) {
69
+ fs.symlinkSync(src, destDir, 'dir');
70
+ }
71
+ continue;
72
+ }
73
+
74
+ if (fs.existsSync(destDir)) {
75
+ console.log(chalk.gray(` ${name}@${version} already installed, skipping`));
76
+ continue;
77
+ }
78
+
79
+ process.stdout.write(chalk.gray(` Installing ${name}@${version} ...`));
80
+ try {
81
+ const meta = await registry.fetchModuleMeta(regUrl, name, version);
82
+ await registry.downloadModule(meta.downloadUrl, destDir);
83
+ if (options.lock !== false) {
84
+ lockModule(serverRoot, name, { version, resolved: meta.downloadUrl });
85
+ }
86
+ console.log(chalk.green(' done'));
87
+ } catch (err) {
88
+ console.log(chalk.red(' failed'));
89
+ console.error(chalk.red(` ${err.message}`));
90
+ }
91
+ }
92
+
93
+ if (moduleName) {
94
+ const [name] = moduleName.split('@');
95
+ const resolved = plan.get(name);
96
+ if (resolved && !config.modules[name]) {
97
+ config.modules[name] = `^${resolved}`;
98
+ writeShivaConfig(serverRoot, config);
99
+ console.log(chalk.green(`\n✔ Added ${name}@^${resolved} to shiva.json`));
100
+ }
101
+ }
102
+
103
+ console.log('');
104
+ console.log(chalk.green(`✔ Done. ${plan.size} module(s) processed.`));
105
+ console.log('');
106
+ }
107
+
108
+ module.exports = installCommand;
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const chalk = require('chalk');
6
+
7
+ const { requireServerRoot, getResourcesDir } = require('../../utils/server-root');
8
+ const { scanModules } = require('../../utils/lua-parser');
9
+
10
+ function localeMissingCommand(program) {
11
+ program
12
+ .command('locale:missing')
13
+ .description('Find missing translation keys across module locales')
14
+ .option('-l, --lang <lang>', 'Language to check against en', 'all')
15
+ .action((options) => { run(options); });
16
+ }
17
+
18
+ function run(options) {
19
+ const serverRoot = requireServerRoot();
20
+ const resourcesDir = getResourcesDir(serverRoot);
21
+ const modules = scanModules(resourcesDir);
22
+
23
+ let totalMissing = 0;
24
+ console.log('');
25
+
26
+ for (const mod of modules) {
27
+ const localesDir = path.join(mod.path, 'locales');
28
+ if (!fs.existsSync(localesDir)) continue;
29
+
30
+ const localeFiles = fs.readdirSync(localesDir).filter(f => f.endsWith('.lua'));
31
+ if (localeFiles.length === 0) continue;
32
+
33
+ const enFile = path.join(localesDir, 'en.lua');
34
+ if (!fs.existsSync(enFile)) continue;
35
+
36
+ const enKeys = extractLocaleKeys(fs.readFileSync(enFile, 'utf-8'));
37
+ if (enKeys.size === 0) continue;
38
+
39
+ const otherFiles = localeFiles.filter(f => f !== 'en.lua');
40
+ if (otherFiles.length === 0) continue;
41
+
42
+ let modMissing = 0;
43
+ for (const file of otherFiles) {
44
+ const lang = file.replace('.lua', '');
45
+ if (options.lang !== 'all' && lang !== options.lang) continue;
46
+
47
+ const content = fs.readFileSync(path.join(localesDir, file), 'utf-8');
48
+ const keys = extractLocaleKeys(content);
49
+ const missing = [...enKeys].filter(k => !keys.has(k));
50
+
51
+ if (missing.length > 0) {
52
+ if (modMissing === 0) {
53
+ console.log(chalk.bold.cyan(` ${mod.name}`));
54
+ }
55
+ console.log(chalk.yellow(` [${lang}] ${missing.length} missing key(s):`));
56
+ missing.forEach(k => console.log(chalk.gray(` - ${k}`)));
57
+ modMissing += missing.length;
58
+ totalMissing += missing.length;
59
+ }
60
+ }
61
+ }
62
+
63
+ if (totalMissing === 0) {
64
+ console.log(chalk.green('✔ All locale keys are present across all modules.'));
65
+ } else {
66
+ console.log('');
67
+ console.log(chalk.yellow(` ${totalMissing} missing key(s) found.`));
68
+ process.exit(1);
69
+ }
70
+ console.log('');
71
+ }
72
+
73
+ function extractLocaleKeys(content) {
74
+ const keys = new Set();
75
+ const re = /\['([^']+)'\]\s*=/g;
76
+ let match;
77
+ while ((match = re.exec(content)) !== null) {
78
+ keys.add(match[1]);
79
+ }
80
+ return keys;
81
+ }
82
+
83
+ module.exports = localeMissingCommand;
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ const { requireServerRoot } = require('../../utils/server-root');
8
+ const { writeTemplate, toPascalCase, toSnakeCase } = require('../../generators/index');
9
+
10
+ function makeContractCommand(program) {
11
+ program
12
+ .command('make:contract <name>')
13
+ .description('Scaffold a new service contract')
14
+ .action((name) => { run(name); });
15
+ }
16
+
17
+ function run(rawName) {
18
+ const ContractName = toPascalCase(rawName);
19
+ const contractSlug = toSnakeCase(rawName).replace(/-/g, '_');
20
+ const moduleShort = contractSlug;
21
+ const fileName = `sh_${contractSlug}.lua`;
22
+
23
+ const serverRoot = requireServerRoot();
24
+ const contractsDir = path.join(serverRoot, 'shared', 'contracts');
25
+ const destFile = path.join(contractsDir, fileName);
26
+
27
+ if (fs.existsSync(destFile)) {
28
+ console.error(chalk.red(`✖ Contract already exists: ${destFile}`));
29
+ process.exit(1);
30
+ }
31
+
32
+ writeTemplate('contract.lua.tpl', destFile, {
33
+ ContractName,
34
+ module_short: moduleShort,
35
+ });
36
+
37
+ console.log('');
38
+ console.log(chalk.green(`✔ Created contract: ${chalk.bold(ContractName)}`));
39
+ console.log(chalk.gray(` Path: ${path.relative(process.cwd(), destFile)}`));
40
+ console.log('');
41
+ console.log(chalk.gray(' Register it in your fxmanifest shared_scripts and module.lua provides.'));
42
+ console.log('');
43
+ }
44
+
45
+ module.exports = makeContractCommand;
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ const { requireServerRoot, getShivaModulesDir } = require('../../utils/server-root');
8
+ const { writeTemplate, toSnakeCase, normalizeModuleName } = require('../../generators/index');
9
+
10
+ /**
11
+ * Register the `shiva make:migration` command.
12
+ * @param {import('commander').Command} program
13
+ */
14
+ function makeMigrationCommand(program) {
15
+ program
16
+ .command('make:migration <name>')
17
+ .description('Create a new database migration file inside a module')
18
+ .requiredOption('-m, --module <module>', 'Target module name')
19
+ .action((name, options) => {
20
+ run(name, options);
21
+ });
22
+ }
23
+
24
+ function run(rawName, options) {
25
+ const snakeName = toSnakeCase(rawName).replace(/-/g, '_');
26
+ const moduleName = normalizeModuleName(options.module);
27
+ const now = new Date();
28
+ const pad = n => String(n).padStart(2, '0');
29
+ const timestamp = `${now.getFullYear()}_${pad(now.getMonth() + 1)}_${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
30
+ const fileName = `${timestamp}_${snakeName}.lua`;
31
+ const migrationName = `${timestamp}_${snakeName}`;
32
+ const createdAt = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
33
+
34
+ const serverRoot = requireServerRoot();
35
+ const modulesDir = getShivaModulesDir(serverRoot);
36
+ const moduleDir = path.join(modulesDir, moduleName);
37
+
38
+ if (!fs.existsSync(moduleDir)) {
39
+ console.error(chalk.red(`✖ Module not found: ${moduleName}`));
40
+ console.error(chalk.gray(` Run \`shiva make:module ${moduleName}\` first.`));
41
+ process.exit(1);
42
+ }
43
+
44
+ const migrationsDir = path.join(moduleDir, 'migrations');
45
+ fs.mkdirSync(migrationsDir, { recursive: true });
46
+
47
+ const destFile = path.join(migrationsDir, fileName);
48
+
49
+ if (fs.existsSync(destFile)) {
50
+ console.error(chalk.red(`✖ Migration already exists: ${destFile}`));
51
+ process.exit(1);
52
+ }
53
+
54
+ const vars = {
55
+ migration_name: migrationName,
56
+ module_name: moduleName,
57
+ snake_name: snakeName,
58
+ created_at: createdAt,
59
+ };
60
+
61
+ writeTemplate('migration.lua.tpl', destFile, vars);
62
+
63
+ console.log('');
64
+ console.log(chalk.green(`✔ Created migration: ${chalk.bold(fileName)}`));
65
+ console.log(chalk.gray(` Path: ${path.relative(process.cwd(), destFile)}`));
66
+ console.log('');
67
+ }
68
+
69
+ module.exports = makeMigrationCommand;
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ const { requireServerRoot, getShivaModulesDir } = require('../../utils/server-root');
8
+ const { writeTemplate, toPascalCase, toSnakeCase, normalizeModuleName } = require('../../generators/index');
9
+
10
+ /**
11
+ * Register the `shiva make:model` command.
12
+ * @param {import('commander').Command} program
13
+ */
14
+ function makeModelCommand(program) {
15
+ program
16
+ .command('make:model <name>')
17
+ .description('Scaffold a new model inside a module')
18
+ .requiredOption('-m, --module <module>', 'Target module name')
19
+ .option('-t, --table <table>', 'Override database table name')
20
+ .action((name, options) => {
21
+ run(name, options);
22
+ });
23
+ }
24
+
25
+ function run(rawName, options) {
26
+ const ModelName = toPascalCase(rawName);
27
+ const moduleName = normalizeModuleName(options.module);
28
+ const tableName = options.table || toSnakeCase(rawName) + 's';
29
+
30
+ const serverRoot = requireServerRoot();
31
+ const modulesDir = getShivaModulesDir(serverRoot);
32
+ const moduleDir = path.join(modulesDir, moduleName);
33
+
34
+ if (!fs.existsSync(moduleDir)) {
35
+ console.error(chalk.red(`✖ Module not found: ${moduleName}`));
36
+ console.error(chalk.gray(` Run \`shiva make:module ${moduleName}\` first.`));
37
+ process.exit(1);
38
+ }
39
+
40
+ const destDir = path.join(moduleDir, 'server', 'models');
41
+ const destFile = path.join(destDir, `${ModelName}.lua`);
42
+
43
+ if (fs.existsSync(destFile)) {
44
+ console.error(chalk.red(`✖ Model already exists: ${destFile}`));
45
+ process.exit(1);
46
+ }
47
+
48
+ const vars = {
49
+ ModelName,
50
+ module_name: moduleName,
51
+ table_name: tableName,
52
+ };
53
+
54
+ writeTemplate('model.lua.tpl', destFile, vars);
55
+
56
+ console.log('');
57
+ console.log(chalk.green(`✔ Created model: ${chalk.bold(ModelName)}`));
58
+ console.log(chalk.gray(` Table: ${tableName}`));
59
+ console.log(chalk.gray(` Path: ${path.relative(process.cwd(), destFile)}`));
60
+ console.log('');
61
+ }
62
+
63
+ module.exports = makeModelCommand;