@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.
- package/.editorconfig +38 -0
- package/.gitattributes +18 -0
- package/.nvmrc +1 -0
- package/README.md +179 -0
- package/bin/shiva.js +4 -0
- package/package.json +44 -0
- package/recipes/full-rp.json +77 -0
- package/recipes/minimal.json +30 -0
- package/recipes/standard.json +46 -0
- package/src/commands/ai/context.js +89 -0
- package/src/commands/ai/link.js +38 -0
- package/src/commands/ai/mcp.js +39 -0
- package/src/commands/config/validate.js +65 -0
- package/src/commands/docs/api.js +81 -0
- package/src/commands/docs/build.js +14 -0
- package/src/commands/docs/deploy.js +14 -0
- package/src/commands/docs/serve.js +14 -0
- package/src/commands/init.js +167 -0
- package/src/commands/install.js +108 -0
- package/src/commands/locale/missing.js +83 -0
- package/src/commands/make/contract.js +45 -0
- package/src/commands/make/migration.js +69 -0
- package/src/commands/make/model.js +63 -0
- package/src/commands/make/module.js +115 -0
- package/src/commands/make/seed.js +51 -0
- package/src/commands/make/service.js +60 -0
- package/src/commands/make/test.js +53 -0
- package/src/commands/mcp.js +26 -0
- package/src/commands/migrate/rollback.js +155 -0
- package/src/commands/migrate/run.js +159 -0
- package/src/commands/migrate/status.js +137 -0
- package/src/commands/module/list.js +46 -0
- package/src/commands/module/status.js +64 -0
- package/src/commands/outdated.js +59 -0
- package/src/commands/remove.js +61 -0
- package/src/commands/seed.js +108 -0
- package/src/commands/test.js +88 -0
- package/src/commands/update.js +90 -0
- package/src/generators/index.js +78 -0
- package/src/generators/templates/contract.lua.tpl +12 -0
- package/src/generators/templates/migration.lua.tpl +15 -0
- package/src/generators/templates/model.lua.tpl +14 -0
- package/src/generators/templates/module/client/init.lua.tpl +5 -0
- package/src/generators/templates/module/config/config.lua.tpl +4 -0
- package/src/generators/templates/module/fxmanifest.lua.tpl +41 -0
- package/src/generators/templates/module/locales/en.lua.tpl +4 -0
- package/src/generators/templates/module/module.lua.tpl +10 -0
- package/src/generators/templates/module/server/init.lua.tpl +2 -0
- package/src/generators/templates/module/shared/init.lua.tpl +5 -0
- package/src/generators/templates/seed.lua.tpl +10 -0
- package/src/generators/templates/service.lua.tpl +7 -0
- package/src/generators/templates/test.lua.tpl +39 -0
- package/src/index.js +113 -0
- package/src/mcp/resources/contracts.js +68 -0
- package/src/mcp/resources/docs.js +56 -0
- package/src/mcp/resources/examples.js +235 -0
- package/src/mcp/server.js +121 -0
- package/src/mcp/tools/config.js +53 -0
- package/src/mcp/tools/contracts.js +37 -0
- package/src/mcp/tools/database.js +93 -0
- package/src/mcp/tools/docs.js +38 -0
- package/src/mcp/tools/events.js +26 -0
- package/src/mcp/tools/items.js +280 -0
- package/src/mcp/tools/modules.js +25 -0
- package/src/packages/lockfile.js +53 -0
- package/src/packages/registry.js +99 -0
- package/src/packages/resolver.js +83 -0
- package/src/utils/config-reader.js +74 -0
- package/src/utils/lua-annotations.js +319 -0
- package/src/utils/lua-parser.js +119 -0
- 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 };
|