@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,78 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEMPLATES_DIR = path.join(__dirname, 'templates');
7
+
8
+ /**
9
+ * Render a template file, replacing {{KEY}} placeholders.
10
+ * @param {string} templateRelPath Path relative to src/generators/templates/
11
+ * @param {object} vars Key/value pairs for substitution
12
+ * @returns {string}
13
+ */
14
+ function renderTemplate(templateRelPath, vars = {}) {
15
+ const fullPath = path.join(TEMPLATES_DIR, templateRelPath);
16
+ if (!fs.existsSync(fullPath)) {
17
+ throw new Error(`Template not found: ${fullPath}`);
18
+ }
19
+ let content = fs.readFileSync(fullPath, 'utf-8');
20
+ for (const [key, value] of Object.entries(vars)) {
21
+ content = content.split(`{{${key}}}`).join(String(value));
22
+ }
23
+ return content;
24
+ }
25
+
26
+ /**
27
+ * Write a rendered template to a destination file.
28
+ * Creates parent directories automatically.
29
+ * @param {string} templateRelPath
30
+ * @param {string} destPath
31
+ * @param {object} vars
32
+ */
33
+ function writeTemplate(templateRelPath, destPath, vars = {}) {
34
+ const content = renderTemplate(templateRelPath, vars);
35
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
36
+ fs.writeFileSync(destPath, content, 'utf-8');
37
+ }
38
+
39
+ /**
40
+ * Convert a name to PascalCase.
41
+ * e.g. "fishing-rod" → "FishingRod", "fishing_rod" → "FishingRod"
42
+ * @param {string} name
43
+ * @returns {string}
44
+ */
45
+ function toPascalCase(name) {
46
+ return name
47
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
48
+ .replace(/^(.)/, c => c.toUpperCase());
49
+ }
50
+
51
+ /**
52
+ * Convert a name to snake_case.
53
+ * e.g. "FishingRod" → "fishing_rod"
54
+ * @param {string} name
55
+ * @returns {string}
56
+ */
57
+ function toSnakeCase(name) {
58
+ return name
59
+ .replace(/([A-Z])/g, '_$1')
60
+ .toLowerCase()
61
+ .replace(/^[-_]/, '')
62
+ .replace(/[-]/g, '_');
63
+ }
64
+
65
+ /**
66
+ * Normalize a module name, ensuring "shiva-" prefix.
67
+ * @param {string} name
68
+ * @returns {string}
69
+ */
70
+ function normalizeModuleName(name) {
71
+ name = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
72
+ if (!name.startsWith('shiva-')) {
73
+ name = 'shiva-' + name;
74
+ }
75
+ return name;
76
+ }
77
+
78
+ module.exports = { renderTemplate, writeTemplate, toPascalCase, toSnakeCase, normalizeModuleName };
@@ -0,0 +1,12 @@
1
+ return {
2
+ name = '{{ContractName}}',
3
+ server = {
4
+ -- 'methodName',
5
+ },
6
+ client = {
7
+ -- 'methodName',
8
+ },
9
+ events = {
10
+ -- '{{module_short}}:eventName',
11
+ },
12
+ }
@@ -0,0 +1,15 @@
1
+ return {
2
+ up = function(DB)
3
+ DB.execute([[
4
+ -- TODO: Write your migration SQL
5
+ --
6
+ -- Example:
7
+ -- CREATE TABLE IF NOT EXISTS `{{snake_name}}` (
8
+ -- `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
9
+ -- `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ -- `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
11
+ -- PRIMARY KEY (`id`)
12
+ -- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
13
+ ]])
14
+ end,
15
+ }
@@ -0,0 +1,14 @@
1
+ ---@class {{ModelName}} : ModelClass
2
+ {{ModelName}} = Model.define('{{table_name}}')
3
+
4
+ ---@param id integer
5
+ ---@return table|nil
6
+ function {{ModelName}}.findById(id)
7
+ return DB.table('{{table_name}}'):where('id', id):first()
8
+ end
9
+
10
+ -- TODO: Add custom query methods
11
+ -- Example:
12
+ -- function {{ModelName}}.findByField(value)
13
+ -- return DB.table('{{table_name}}'):where('field', value):get()
14
+ -- end
@@ -0,0 +1,5 @@
1
+ -- Request initial state from server when character is ready.
2
+ EventBus.on('player:characterReady', function()
3
+ -- TriggerServerEvent('{{module_short}}:requestState')
4
+ end)
5
+ Log.info('[{{module_short}}] Client ready.')
@@ -0,0 +1,4 @@
1
+ Config.register('{{module_short}}', {
2
+ -- TODO: Add module configuration options
3
+ -- enabled = true,
4
+ })
@@ -0,0 +1,41 @@
1
+ fx_version 'cerulean'
2
+ game 'gta5'
3
+ lua54 'yes'
4
+
5
+ name '{{module_name}}'
6
+ description '{{description}}'
7
+ version '1.0.0'
8
+ author '{{author}}'
9
+
10
+ dependencies {
11
+ 'shiva-core',
12
+ }
13
+
14
+ shared_scripts {
15
+ 'module.lua',
16
+ 'config/config.lua',
17
+ 'locales/en.lua',
18
+ 'shared/sh_{{module_short}}.lua',
19
+ }
20
+
21
+ server_scripts {
22
+ -- Migrations — add entries here as you create them:
23
+ -- 'migrations/YYYY_MM_DD_HHMMSS_create_example.lua',
24
+
25
+ -- Models
26
+ -- 'server/models/Example.lua',
27
+
28
+ -- Services
29
+ -- 'server/services/ExampleService.lua',
30
+
31
+ -- Events
32
+ -- 'server/events/handlers.lua',
33
+
34
+ 'server/boot.lua',
35
+ }
36
+
37
+ client_scripts {
38
+ -- 'client/services/ExampleClient.lua',
39
+ -- 'client/events/handlers.lua',
40
+ 'client/boot.lua',
41
+ }
@@ -0,0 +1,4 @@
1
+ Locale.register('{{module_short}}', 'en', {
2
+ -- TODO: Add locale strings
3
+ -- ['error.notFound'] = 'Not found.',
4
+ })
@@ -0,0 +1,10 @@
1
+ return {
2
+ name = '{{module_name}}',
3
+ version = '1.0.0',
4
+ description = '{{description}}',
5
+ author = '{{author}}',
6
+ dependencies = {},
7
+ provides = {},
8
+ migrations = {},
9
+ events = {},
10
+ }
@@ -0,0 +1,2 @@
1
+ Container.bind('{{PascalName}}', {{PascalName}}Service)
2
+ Log.info('[{{module_short}}] {{PascalName}}Service ready.')
@@ -0,0 +1,5 @@
1
+ ---@enum {{PascalName}}Type
2
+ {{PascalName}}Type = {
3
+ -- TODO: define enum values
4
+ -- EXAMPLE = 'example',
5
+ }
@@ -0,0 +1,10 @@
1
+ return {
2
+ run = function(db)
3
+ -- TODO: Insert seed data
4
+ -- Example:
5
+ -- db.execute([[
6
+ -- INSERT IGNORE INTO `{{table_name}}` (`id`, `name`) VALUES
7
+ -- (1, 'example')
8
+ -- ]])
9
+ end,
10
+ }
@@ -0,0 +1,7 @@
1
+ {{ServiceName}} = {}
2
+
3
+ ---@return any
4
+ function {{ServiceName}}.example()
5
+ -- TODO: Implement
6
+ -- Available globals: Container, Locale, Config, EventBus, Log, DB
7
+ end
@@ -0,0 +1,39 @@
1
+ ---@diagnostic disable: undefined-field, duplicate-set-field, need-check-nil
2
+ -- tests/{{test_name}}_spec.lua
3
+ -- Unit tests for {{describe_name}}.
4
+ -- Run with: shiva test --module {{module_name}}
5
+
6
+ -- Resolve shiva-test relative to this file (both modules live in the same [category] folder)
7
+ package.path = '../../shiva-test/?.lua;../../shiva-test/?/init.lua;' .. package.path
8
+
9
+ local Test = require('src.init')
10
+
11
+ Test.setup({ mode = 'server' })
12
+
13
+ local describe = Test.describe
14
+ local it = Test.it
15
+ local expect = Test.expect
16
+ local beforeEach = Test.beforeEach
17
+
18
+ describe('{{describe_name}}', function()
19
+ beforeEach(function()
20
+ -- Stub Shiva globals used by {{describe_name}}
21
+ Container = { resolve = function() end, bind = function() end }
22
+ Config = { get = function() end, register = function() end }
23
+ Locale = { t = function(_, key) return key end, register = function() end }
24
+ EventBus = { emit = function() end, on = function() end }
25
+ DB = { table = function() return DB end, execute = function() end }
26
+ Log = { info = function() end, debug = function() end, warn = function() end }
27
+ end)
28
+
29
+ it('passes a basic sanity check', function()
30
+ expect(true):toBeTrue()
31
+ end)
32
+
33
+ -- it('should ...', function()
34
+ -- local actual = ...
35
+ -- expect(actual):toBe(expected)
36
+ -- end)
37
+ end)
38
+
39
+ Test.run()
package/src/index.js ADDED
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+
7
+ // Scaffolding
8
+ const initCommand = require('./commands/init');
9
+ const makeModuleCommand = require('./commands/make/module');
10
+ const makeServiceCommand = require('./commands/make/service');
11
+ const makeModelCommand = require('./commands/make/model');
12
+ const makeMigrationCommand = require('./commands/make/migration');
13
+ const makeTestCommand = require('./commands/make/test');
14
+ const makeContractCommand = require('./commands/make/contract');
15
+ const makeSeedCommand = require('./commands/make/seed');
16
+
17
+ // Migrations
18
+ const migrateRunCommand = require('./commands/migrate/run');
19
+ const migrateRollbackCommand = require('./commands/migrate/rollback');
20
+ const migrateStatusCommand = require('./commands/migrate/status');
21
+
22
+ // Seeding
23
+ const seedCommand = require('./commands/seed');
24
+
25
+ // Module inspection
26
+ const moduleListCommand = require('./commands/module/list');
27
+ const moduleStatusCommand = require('./commands/module/status');
28
+
29
+ // Config & locale
30
+ const configValidateCommand = require('./commands/config/validate');
31
+ const localeMissingCommand = require('./commands/locale/missing');
32
+
33
+ // Package management
34
+ const installCommand = require('./commands/install');
35
+ const updateCommand = require('./commands/update');
36
+ const outdatedCommand = require('./commands/outdated');
37
+ const removeCommand = require('./commands/remove');
38
+
39
+ // Testing
40
+ const testCommand = require('./commands/test');
41
+
42
+ // AI integration
43
+ const aiContextCommand = require('./commands/ai/context');
44
+ const aiLinkCommand = require('./commands/ai/link');
45
+ const aiMcpCommand = require('./commands/ai/mcp');
46
+
47
+ // MCP server
48
+ const mcpCommand = require('./commands/mcp');
49
+
50
+ // Docs
51
+ const docsBuildCommand = require('./commands/docs/build');
52
+ const docsServeCommand = require('./commands/docs/serve');
53
+ const docsApiCommand = require('./commands/docs/api');
54
+ const docsDeployCommand = require('./commands/docs/deploy');
55
+
56
+ program
57
+ .name('shiva')
58
+ .description(chalk.cyan('Developer CLI for the Shiva FiveM framework'))
59
+ .version(pkg.version, '-v, --version', 'Output the current version');
60
+
61
+ initCommand(program);
62
+
63
+ makeModuleCommand(program);
64
+ makeServiceCommand(program);
65
+ makeModelCommand(program);
66
+ makeMigrationCommand(program);
67
+ makeTestCommand(program);
68
+ makeContractCommand(program);
69
+ makeSeedCommand(program);
70
+
71
+ migrateRunCommand(program);
72
+ migrateRollbackCommand(program);
73
+ migrateStatusCommand(program);
74
+
75
+ seedCommand(program);
76
+
77
+ moduleListCommand(program);
78
+ moduleStatusCommand(program);
79
+
80
+ configValidateCommand(program);
81
+ localeMissingCommand(program);
82
+
83
+ installCommand(program);
84
+ updateCommand(program);
85
+ outdatedCommand(program);
86
+ removeCommand(program);
87
+
88
+ testCommand(program);
89
+
90
+ aiContextCommand(program);
91
+ aiLinkCommand(program);
92
+ aiMcpCommand(program);
93
+
94
+ mcpCommand(program);
95
+
96
+ docsBuildCommand(program);
97
+ docsServeCommand(program);
98
+ docsApiCommand(program);
99
+ docsDeployCommand(program);
100
+
101
+ program.addHelpText('after', `
102
+ ${chalk.bold('Examples:')}
103
+ $ shiva init
104
+ $ shiva make:module fishing
105
+ $ shiva make:service FishingService --module shiva-fishing
106
+ $ shiva make:migration create_fish_catches --module shiva-fishing
107
+ $ shiva migrate
108
+ $ shiva module:list
109
+ $ shiva install shiva-fishing
110
+ $ shiva mcp start
111
+ `);
112
+
113
+ program.parse(process.argv);
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { findServerRoot } = require('../../utils/server-root');
7
+
8
+ const TEMPLATE_URI = 'shiva:contracts/{name}';
9
+
10
+ /**
11
+ * Resolve the contracts directory — checks project-level then shiva-core submodule.
12
+ * @param {string} serverRoot
13
+ * @returns {string|null}
14
+ */
15
+ function findContractsDir(serverRoot) {
16
+ const candidates = [
17
+ path.join(serverRoot, 'shared', 'contracts'),
18
+ path.join(serverRoot, 'shared', 'sh_contracts'),
19
+ path.join(serverRoot, 'resources', '[shiva]', 'shiva-core', 'shared', 'sh_contracts'),
20
+ ];
21
+ return candidates.find(d => fs.existsSync(d)) || null;
22
+ }
23
+
24
+ /**
25
+ * List all available contracts.
26
+ * @param {string} serverRoot
27
+ * @returns {Array<{uri, name, description, mimeType}>}
28
+ */
29
+ function listContracts(serverRoot) {
30
+ const dir = findContractsDir(serverRoot);
31
+ if (!dir) return [];
32
+
33
+ return fs.readdirSync(dir)
34
+ .filter(f => f.endsWith('.lua'))
35
+ .map(f => {
36
+ const slug = f.replace(/^sh_/, '').replace(/\.lua$/, '');
37
+ const name = slug.charAt(0).toUpperCase() + slug.slice(1);
38
+ return {
39
+ uri: `shiva:contracts/${slug}`,
40
+ name: `${name} contract`,
41
+ description: `Contract interface: ${name}`,
42
+ mimeType: 'text/x-lua',
43
+ };
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Read a contract resource by URI.
49
+ * @param {string} uri e.g. "shiva:contracts/economy"
50
+ * @param {string} serverRoot
51
+ * @returns {{uri, mimeType, text}|null}
52
+ */
53
+ function readContract(uri, serverRoot) {
54
+ const slug = uri.replace(/^shiva:contracts\//, '');
55
+ const dir = findContractsDir(serverRoot);
56
+ if (!dir) return null;
57
+
58
+ const file = path.join(dir, `sh_${slug}.lua`);
59
+ if (!fs.existsSync(file)) return null;
60
+
61
+ return {
62
+ uri,
63
+ mimeType: 'text/x-lua',
64
+ text: fs.readFileSync(file, 'utf-8'),
65
+ };
66
+ }
67
+
68
+ module.exports = { TEMPLATE_URI, listContracts, readContract };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { scanModules } = require('../../utils/lua-parser');
7
+ const { getResourcesDir } = require('../../utils/server-root');
8
+
9
+ const TEMPLATE_URI = 'shiva:docs/{topic}';
10
+
11
+ /**
12
+ * List all available doc topics (module AGENTS.md files).
13
+ * @param {string} serverRoot
14
+ * @returns {Array<{uri, name, description, mimeType}>}
15
+ */
16
+ function listDocs(serverRoot) {
17
+ const modules = scanModules(getResourcesDir(serverRoot));
18
+ const resources = [];
19
+
20
+ for (const mod of modules) {
21
+ const agentsFile = path.join(mod.path, 'AGENTS.md');
22
+ if (!fs.existsSync(agentsFile)) continue;
23
+ resources.push({
24
+ uri: `shiva:docs/${mod.name}`,
25
+ name: `${mod.name} documentation`,
26
+ description: mod.manifest.description || `AGENTS.md for ${mod.name}`,
27
+ mimeType: 'text/markdown',
28
+ });
29
+ }
30
+
31
+ return resources;
32
+ }
33
+
34
+ /**
35
+ * Read a doc resource by URI.
36
+ * @param {string} uri e.g. "shiva:docs/shiva-economy"
37
+ * @param {string} serverRoot
38
+ * @returns {{uri, mimeType, text}|null}
39
+ */
40
+ function readDoc(uri, serverRoot) {
41
+ const topic = uri.replace(/^shiva:docs\//, '');
42
+ const modules = scanModules(getResourcesDir(serverRoot));
43
+ const mod = modules.find(m => m.name === topic);
44
+ if (!mod) return null;
45
+
46
+ const agentsFile = path.join(mod.path, 'AGENTS.md');
47
+ if (!fs.existsSync(agentsFile)) return null;
48
+
49
+ return {
50
+ uri,
51
+ mimeType: 'text/markdown',
52
+ text: fs.readFileSync(agentsFile, 'utf-8'),
53
+ };
54
+ }
55
+
56
+ module.exports = { TEMPLATE_URI, listDocs, readDoc };