@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,319 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * LuaLS annotation parser for shiva-cli.
5
+ *
6
+ * Parses the following LuaLS annotation tags from Lua source files:
7
+ * ---@class ClassName [: ParentClass]
8
+ * ---@field name type [description]
9
+ * ---@param name type [description]
10
+ * ---@return type [name] [description]
11
+ * ---@alias name type
12
+ * ---@type type
13
+ *
14
+ * Usage:
15
+ * const { parseAnnotations, scanModuleAnnotations } = require('./lua-annotations');
16
+ * const api = scanModuleAnnotations('/path/to/module');
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ─── Regex patterns ────────────────────────────────────────────────────────────
23
+
24
+ const RE_CLASS = /^---@class\s+(\w+)(?:\s*:\s*(\w+))?\s*(?:--\s*(.*))?$/;
25
+ const RE_FIELD = /^---@field\s+(\w+)\s+(\S+)(?:\s+(.*))?$/;
26
+ const RE_PARAM = /^---@param\s+(\w+)\s+(\S+)(?:\s+(.*))?$/;
27
+ const RE_RETURN = /^---@return\s+(\S+)(?:\s+(\w+))?(?:\s+(.*))?$/;
28
+ const RE_ALIAS = /^---@alias\s+(\w+)\s+(\S+)(?:\s+(.*))?$/;
29
+ const RE_FN_DEF = /^(?:local\s+)?function\s+([\w.]+)\s*\(([^)]*)\)/;
30
+ const RE_FN_ASSIGN = /^(?:local\s+)?([\w.]+)\s*=\s*function\s*\(([^)]*)\)/;
31
+ const RE_COMMENT = /^---\s*(.*)/;
32
+
33
+ // ─── Types ────────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * @typedef {Object} LuaParam
37
+ * @property {string} name
38
+ * @property {string} type
39
+ * @property {string|null} description
40
+ */
41
+
42
+ /**
43
+ * @typedef {Object} LuaReturn
44
+ * @property {string} type
45
+ * @property {string|null} name
46
+ * @property {string|null} description
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} LuaFunction
51
+ * @property {string} name
52
+ * @property {string|null} description
53
+ * @property {LuaParam[]} params
54
+ * @property {LuaReturn[]} returns
55
+ * @property {string} file
56
+ * @property {number} line
57
+ */
58
+
59
+ /**
60
+ * @typedef {Object} LuaClass
61
+ * @property {string} name
62
+ * @property {string|null} parent
63
+ * @property {string|null} description
64
+ * @property {Array<{name:string,type:string,description:string|null}>} fields
65
+ * @property {string} file
66
+ * @property {number} line
67
+ */
68
+
69
+ /**
70
+ * @typedef {Object} LuaAlias
71
+ * @property {string} name
72
+ * @property {string} type
73
+ * @property {string|null} description
74
+ */
75
+
76
+ /**
77
+ * @typedef {Object} ParseResult
78
+ * @property {LuaClass[]} classes
79
+ * @property {LuaFunction[]} functions
80
+ * @property {LuaAlias[]} aliases
81
+ */
82
+
83
+ // ─── Core parser ──────────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Parse LuaLS annotations from a single Lua source string.
87
+ * @param {string} source Lua source code
88
+ * @param {string} [filePath] Path for source attribution
89
+ * @returns {ParseResult}
90
+ */
91
+ function parseAnnotations(source, filePath = '<unknown>') {
92
+ const lines = source.split('\n');
93
+ const classes = [];
94
+ const functions = [];
95
+ const aliases = [];
96
+
97
+ let pendingParams = /** @type {LuaParam[]} */ ([]);
98
+ let pendingReturns = /** @type {LuaReturn[]} */ ([]);
99
+ let pendingDesc = /** @type {string[]} */ ([]);
100
+ let currentClass = /** @type {LuaClass|null} */ (null);
101
+
102
+ const flushPending = () => {
103
+ pendingParams = [];
104
+ pendingReturns = [];
105
+ pendingDesc = [];
106
+ };
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i].trim();
110
+
111
+ // ── @class ────────────────────────────────────────────────
112
+ const classMatch = line.match(RE_CLASS);
113
+ if (classMatch) {
114
+ const [, name, parent, desc] = classMatch;
115
+ currentClass = { name, parent: parent || null, description: desc || null, fields: [], file: filePath, line: i + 1 };
116
+ classes.push(currentClass);
117
+ flushPending();
118
+ continue;
119
+ }
120
+
121
+ // ── @field (attached to the last @class) ─────────────────
122
+ const fieldMatch = line.match(RE_FIELD);
123
+ if (fieldMatch && currentClass) {
124
+ const [, name, type, desc] = fieldMatch;
125
+ currentClass.fields.push({ name, type, description: desc || null });
126
+ continue;
127
+ }
128
+
129
+ // ── @alias ────────────────────────────────────────────────
130
+ const aliasMatch = line.match(RE_ALIAS);
131
+ if (aliasMatch) {
132
+ const [, name, type, desc] = aliasMatch;
133
+ aliases.push({ name, type, description: desc || null });
134
+ flushPending();
135
+ continue;
136
+ }
137
+
138
+ // ── @param ────────────────────────────────────────────────
139
+ const paramMatch = line.match(RE_PARAM);
140
+ if (paramMatch) {
141
+ const [, name, type, desc] = paramMatch;
142
+ pendingParams.push({ name, type, description: desc || null });
143
+ currentClass = null;
144
+ continue;
145
+ }
146
+
147
+ // ── @return ───────────────────────────────────────────────
148
+ const returnMatch = line.match(RE_RETURN);
149
+ if (returnMatch) {
150
+ const [, type, name, desc] = returnMatch;
151
+ pendingReturns.push({ type, name: name || null, description: desc || null });
152
+ currentClass = null;
153
+ continue;
154
+ }
155
+
156
+ // ── Free-standing doc comment ─────────────────────────────
157
+ const commentMatch = line.match(RE_COMMENT);
158
+ if (commentMatch && !line.startsWith('---@')) {
159
+ pendingDesc.push(commentMatch[1].trim());
160
+ continue;
161
+ }
162
+
163
+ // ── Function definitions ──────────────────────────────────
164
+ const fnDef = line.match(RE_FN_DEF);
165
+ const fnAssign = line.match(RE_FN_ASSIGN);
166
+ const fnMatch = fnDef || fnAssign;
167
+
168
+ if (fnMatch) {
169
+ const name = fnMatch[1];
170
+ const rawArgs = fnMatch[2] || '';
171
+ const args = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
172
+ const desc = pendingDesc.length > 0 ? pendingDesc.join(' ') : null;
173
+
174
+ // Merge @param annotations; fill in un-annotated args as unknown
175
+ const params = args.map(argName => {
176
+ const annotated = pendingParams.find(p => p.name === argName);
177
+ return annotated || { name: argName, type: 'any', description: null };
178
+ });
179
+
180
+ // Add any extra @params not matched to positional args (e.g. varargs)
181
+ for (const p of pendingParams) {
182
+ if (!params.find(ep => ep.name === p.name)) {
183
+ params.push(p);
184
+ }
185
+ }
186
+
187
+ functions.push({
188
+ name,
189
+ description: desc,
190
+ params,
191
+ returns: [...pendingReturns],
192
+ file: filePath,
193
+ line: i + 1,
194
+ });
195
+
196
+ flushPending();
197
+ currentClass = null;
198
+ continue;
199
+ }
200
+
201
+ // ── Non-annotation, non-function line: reset if not blank ─
202
+ if (line !== '' && !line.startsWith('---')) {
203
+ flushPending();
204
+ if (!line.startsWith('--')) currentClass = null;
205
+ }
206
+ }
207
+
208
+ return { classes, functions, aliases };
209
+ }
210
+
211
+ // ─── Scanner ──────────────────────────────────────────────────────────────────
212
+
213
+ /**
214
+ * Recursively collect all .lua files in a directory.
215
+ * @param {string} dir
216
+ * @param {string[]} [results]
217
+ * @returns {string[]}
218
+ */
219
+ function collectLuaFiles(dir, results = []) {
220
+ if (!fs.existsSync(dir)) return results;
221
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
222
+ const full = path.join(dir, entry.name);
223
+ if (entry.isDirectory()) {
224
+ collectLuaFiles(full, results);
225
+ } else if (entry.isFile() && entry.name.endsWith('.lua')) {
226
+ results.push(full);
227
+ }
228
+ }
229
+ return results;
230
+ }
231
+
232
+ /**
233
+ * Scan all Lua files in a module directory and return combined annotations.
234
+ * @param {string} modulePath Root path of a Shiva module
235
+ * @returns {ParseResult}
236
+ */
237
+ function scanModuleAnnotations(modulePath) {
238
+ const luaFiles = collectLuaFiles(modulePath);
239
+ const combined = /** @type {ParseResult} */ ({ classes: [], functions: [], aliases: [] });
240
+
241
+ for (const file of luaFiles) {
242
+ const source = fs.readFileSync(file, 'utf-8');
243
+ const result = parseAnnotations(source, file);
244
+ combined.classes.push(...result.classes);
245
+ combined.functions.push(...result.functions);
246
+ combined.aliases.push(...result.aliases);
247
+ }
248
+
249
+ return combined;
250
+ }
251
+
252
+ /**
253
+ * Generate a Markdown API reference from parsed annotations.
254
+ * @param {string} moduleName
255
+ * @param {ParseResult} api
256
+ * @returns {string}
257
+ */
258
+ function toMarkdown(moduleName, api) {
259
+ const lines = [`# ${moduleName} API Reference\n`];
260
+
261
+ if (api.classes.length > 0) {
262
+ lines.push('## Classes\n');
263
+ for (const cls of api.classes) {
264
+ const heading = cls.parent ? `### \`${cls.name}\` *(extends ${cls.parent})*` : `### \`${cls.name}\``;
265
+ lines.push(heading);
266
+ if (cls.description) lines.push(`\n${cls.description}\n`);
267
+ if (cls.fields.length > 0) {
268
+ lines.push('\n**Fields:**\n');
269
+ lines.push('| Name | Type | Description |');
270
+ lines.push('|------|------|-------------|');
271
+ for (const f of cls.fields) {
272
+ lines.push(`| \`${f.name}\` | \`${f.type}\` | ${f.description || ''} |`);
273
+ }
274
+ }
275
+ lines.push('');
276
+ }
277
+ }
278
+
279
+ if (api.aliases.length > 0) {
280
+ lines.push('## Aliases\n');
281
+ for (const a of api.aliases) {
282
+ lines.push(`- **\`${a.name}\`** = \`${a.type}\`${a.description ? ' — ' + a.description : ''}`);
283
+ }
284
+ lines.push('');
285
+ }
286
+
287
+ if (api.functions.length > 0) {
288
+ lines.push('## Functions\n');
289
+ for (const fn of api.functions) {
290
+ const paramStr = fn.params.map(p => p.name).join(', ');
291
+ lines.push(`### \`${fn.name}(${paramStr})\``);
292
+ if (fn.description) lines.push(`\n${fn.description}\n`);
293
+
294
+ if (fn.params.length > 0) {
295
+ lines.push('\n**Parameters:**\n');
296
+ lines.push('| Name | Type | Description |');
297
+ lines.push('|------|------|-------------|');
298
+ for (const p of fn.params) {
299
+ lines.push(`| \`${p.name}\` | \`${p.type}\` | ${p.description || ''} |`);
300
+ }
301
+ }
302
+
303
+ if (fn.returns.length > 0) {
304
+ lines.push('\n**Returns:**\n');
305
+ for (const r of fn.returns) {
306
+ const label = r.name ? `\`${r.name}\`` : '';
307
+ const desc = r.description || '';
308
+ lines.push(`- \`${r.type}\`${label ? ' ' + label : ''}${desc ? ' — ' + desc : ''}`);
309
+ }
310
+ }
311
+
312
+ lines.push(`\n*Defined in: \`${path.basename(fn.file)}\` line ${fn.line}*\n`);
313
+ }
314
+ }
315
+
316
+ return lines.join('\n');
317
+ }
318
+
319
+ module.exports = { parseAnnotations, scanModuleAnnotations, collectLuaFiles, toMarkdown };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Parse a module.lua manifest file.
8
+ * Extracts name, version, dependencies etc. using simple regex patterns.
9
+ * @param {string} filePath
10
+ * @returns {object}
11
+ */
12
+ function parseModuleManifest(filePath) {
13
+ if (!fs.existsSync(filePath)) {
14
+ return null;
15
+ }
16
+
17
+ const content = fs.readFileSync(filePath, 'utf-8');
18
+
19
+ const result = {
20
+ name: extractLuaString(content, 'name'),
21
+ version: extractLuaString(content, 'version'),
22
+ description: extractLuaString(content, 'description'),
23
+ author: extractLuaString(content, 'author'),
24
+ dependencies: extractLuaArray(content, 'dependencies'),
25
+ events: extractLuaArray(content, 'events'),
26
+ };
27
+
28
+ return result;
29
+ }
30
+
31
+ /**
32
+ * Parse an fxmanifest.lua file.
33
+ * @param {string} filePath
34
+ * @returns {object}
35
+ */
36
+ function parseFxManifest(filePath) {
37
+ if (!fs.existsSync(filePath)) {
38
+ return null;
39
+ }
40
+
41
+ const content = fs.readFileSync(filePath, 'utf-8');
42
+
43
+ return {
44
+ fxVersion: extractManifestValue(content, 'fx_version'),
45
+ game: extractManifestValue(content, 'game'),
46
+ name: extractManifestValue(content, 'name') || extractManifestValue(content, 'resource'),
47
+ version: extractManifestValue(content, 'version'),
48
+ description: extractManifestValue(content, 'description'),
49
+ };
50
+ }
51
+
52
+ function extractLuaString(content, key) {
53
+ const re = new RegExp(`${key}\\s*=\\s*['"]([^'"]+)['"]`);
54
+ const match = content.match(re);
55
+ return match ? match[1] : null;
56
+ }
57
+
58
+ function extractManifestValue(content, key) {
59
+ const re = new RegExp(`^${key}\\s+['"]([^'"]+)['"]`, 'm');
60
+ const match = content.match(re);
61
+ return match ? match[1] : null;
62
+ }
63
+
64
+ function extractLuaArray(content, key) {
65
+ const re = new RegExp(`${key}\\s*=\\s*\\{([^}]*)\\}`, 's');
66
+ const match = content.match(re);
67
+ if (!match) return [];
68
+
69
+ const items = match[1].match(/['"]([^'"]+)['"]/g);
70
+ if (!items) return [];
71
+ return items.map(s => s.replace(/['"]/g, ''));
72
+ }
73
+
74
+ /**
75
+ * Scan a resources directory for Shiva modules.
76
+ * Looks for module.lua files under resources/[shiva]/ directories.
77
+ * @param {string} resourcesDir
78
+ * @returns {Array<{name: string, path: string, manifest: object}>}
79
+ */
80
+ function scanModules(resourcesDir) {
81
+ if (!fs.existsSync(resourcesDir)) return [];
82
+
83
+ const modules = [];
84
+ const shivaDir = path.join(resourcesDir, '[shiva]');
85
+
86
+ if (fs.existsSync(shivaDir)) {
87
+ scanCategoryDir(shivaDir, modules);
88
+ }
89
+
90
+ // Also scan other [category] dirs for compatibility
91
+ const entries = fs.readdirSync(resourcesDir, { withFileTypes: true });
92
+ for (const entry of entries) {
93
+ if (entry.isDirectory() && entry.name.startsWith('[') && entry.name !== '[shiva]') {
94
+ scanCategoryDir(path.join(resourcesDir, entry.name), modules);
95
+ }
96
+ }
97
+
98
+ return modules;
99
+ }
100
+
101
+ function scanCategoryDir(categoryDir, modules) {
102
+ if (!fs.existsSync(categoryDir)) return;
103
+ const entries = fs.readdirSync(categoryDir, { withFileTypes: true });
104
+ for (const entry of entries) {
105
+ if (!entry.isDirectory()) continue;
106
+ const modulePath = path.join(categoryDir, entry.name);
107
+ const manifestPath = path.join(modulePath, 'module.lua');
108
+ const manifest = parseModuleManifest(manifestPath);
109
+ if (manifest) {
110
+ modules.push({
111
+ name: manifest.name || entry.name,
112
+ path: modulePath,
113
+ manifest,
114
+ });
115
+ }
116
+ }
117
+ }
118
+
119
+ module.exports = { parseModuleManifest, parseFxManifest, scanModules };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * Walk up directory tree looking for a Shiva server root.
8
+ * Identifies root by presence of shiva.json or server.cfg.
9
+ * @param {string} startDir
10
+ * @returns {string|null}
11
+ */
12
+ function findServerRoot(startDir = process.cwd()) {
13
+ let dir = path.resolve(startDir);
14
+ const { root } = path.parse(dir);
15
+
16
+ while (dir !== root) {
17
+ if (fs.existsSync(path.join(dir, 'shiva.json'))) {
18
+ return dir;
19
+ }
20
+ if (fs.existsSync(path.join(dir, 'server.cfg'))) {
21
+ return dir;
22
+ }
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir) break;
25
+ dir = parent;
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Get the resources directory for the server root.
33
+ * @param {string} serverRoot
34
+ * @returns {string}
35
+ */
36
+ function getResourcesDir(serverRoot) {
37
+ return path.join(serverRoot, 'resources');
38
+ }
39
+
40
+ /**
41
+ * Get the [shiva] category directory within resources.
42
+ * Follows FiveM's [category] folder convention.
43
+ * @param {string} serverRoot
44
+ * @returns {string}
45
+ */
46
+ function getShivaModulesDir(serverRoot) {
47
+ return path.join(serverRoot, 'resources', '[shiva]');
48
+ }
49
+
50
+ /**
51
+ * Require a server root or throw a helpful error.
52
+ * @param {string} [startDir]
53
+ * @returns {string}
54
+ */
55
+ function requireServerRoot(startDir = process.cwd()) {
56
+ const root = findServerRoot(startDir);
57
+ if (!root) {
58
+ const chalk = require('chalk');
59
+ console.error(chalk.red('✖ Could not find a Shiva server root.'));
60
+ console.error(chalk.yellow(' Run this command from within a Shiva project, or run `shiva init` first.'));
61
+ process.exit(1);
62
+ }
63
+ return root;
64
+ }
65
+
66
+ module.exports = { findServerRoot, getResourcesDir, getShivaModulesDir, requireServerRoot };