@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,235 @@
|
|
|
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:examples/{pattern}';
|
|
10
|
+
|
|
11
|
+
// Built-in code examples derived from shiva-core patterns
|
|
12
|
+
const BUILTIN_EXAMPLES = [
|
|
13
|
+
{
|
|
14
|
+
pattern: 'service',
|
|
15
|
+
name: 'Service registration',
|
|
16
|
+
mimeType: 'text/x-lua',
|
|
17
|
+
text: `-- Register a service into the container
|
|
18
|
+
Container.register('Economy', {
|
|
19
|
+
getBalance = function(source)
|
|
20
|
+
local char = State.getPlayer(source, 'character')
|
|
21
|
+
return DB.table('accounts'):where('character_id', char.id):first()
|
|
22
|
+
end,
|
|
23
|
+
|
|
24
|
+
addMoney = function(source, amount, reason)
|
|
25
|
+
local char = State.getPlayer(source, 'character')
|
|
26
|
+
local account = DB.table('accounts'):where('character_id', char.id):first()
|
|
27
|
+
DB.table('accounts'):where('id', account.id):update({ balance = account.balance + amount })
|
|
28
|
+
EventBus.emit('economy:balanceChanged', source, { delta = amount, reason = reason })
|
|
29
|
+
end,
|
|
30
|
+
})`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: 'migration',
|
|
34
|
+
name: 'Database migration',
|
|
35
|
+
mimeType: 'text/x-lua',
|
|
36
|
+
text: `-- migrations/001_create_accounts.lua
|
|
37
|
+
return {
|
|
38
|
+
up = function(DB)
|
|
39
|
+
DB.execute([[
|
|
40
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
41
|
+
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
42
|
+
character_id INT UNSIGNED NOT NULL,
|
|
43
|
+
balance INT UNSIGNED NOT NULL DEFAULT 0,
|
|
44
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
INDEX idx_character_id (character_id)
|
|
46
|
+
)
|
|
47
|
+
]])
|
|
48
|
+
end,
|
|
49
|
+
down = function(DB)
|
|
50
|
+
DB.execute('DROP TABLE IF EXISTS accounts')
|
|
51
|
+
end,
|
|
52
|
+
}`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: 'model',
|
|
56
|
+
name: 'Model definition',
|
|
57
|
+
mimeType: 'text/x-lua',
|
|
58
|
+
text: `-- server/models/Account.lua
|
|
59
|
+
local Account = Model.define('accounts', {
|
|
60
|
+
primaryKey = 'id',
|
|
61
|
+
fillable = { 'character_id', 'balance' },
|
|
62
|
+
casts = { metadata = 'json' },
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
-- Usage
|
|
66
|
+
local acc = Account:where('character_id', charId):first()
|
|
67
|
+
acc:update({ balance = acc.balance + 100 })`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
pattern: 'event',
|
|
71
|
+
name: 'EventBus usage',
|
|
72
|
+
mimeType: 'text/x-lua',
|
|
73
|
+
text: `-- Listen for an event
|
|
74
|
+
EventBus.on('economy:balanceChanged', function(source, payload)
|
|
75
|
+
Log.info('economy', 'Balance changed', { source = source, delta = payload.delta })
|
|
76
|
+
end, 10)
|
|
77
|
+
|
|
78
|
+
-- Before plugin (can halt)
|
|
79
|
+
EventBus.before('Economy', 'transfer', function(from, to, amount)
|
|
80
|
+
if amount < 0 then
|
|
81
|
+
return EventBus.halt({ success = false, error = 'Amount must be positive' })
|
|
82
|
+
end
|
|
83
|
+
end)
|
|
84
|
+
|
|
85
|
+
-- After plugin (chain result)
|
|
86
|
+
EventBus.after('Economy', 'transfer', function(result, from, to, amount)
|
|
87
|
+
Log.info('audit', 'Transfer completed', { from = from, to = to, amount = amount })
|
|
88
|
+
return result
|
|
89
|
+
end)`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: 'command',
|
|
93
|
+
name: 'Command registration',
|
|
94
|
+
mimeType: 'text/x-lua',
|
|
95
|
+
text: `-- Register a command via the Commands contract
|
|
96
|
+
Commands.register('givemoney', {
|
|
97
|
+
description = 'Give money to a player',
|
|
98
|
+
permission = 'admin',
|
|
99
|
+
params = {
|
|
100
|
+
{ name = 'target', type = 'number', help = 'Target player ID' },
|
|
101
|
+
{ name = 'amount', type = 'number', help = 'Amount to give' },
|
|
102
|
+
{ name = 'reason', type = 'string', help = 'Reason', optional = true },
|
|
103
|
+
},
|
|
104
|
+
}, function(source, args)
|
|
105
|
+
local Economy = Container.resolve('Economy')
|
|
106
|
+
Economy.addMoney(args.target, args.amount, args.reason or 'admin_grant')
|
|
107
|
+
end)`,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pattern: 'contract',
|
|
111
|
+
name: 'Contract definition',
|
|
112
|
+
mimeType: 'text/x-lua',
|
|
113
|
+
text: `-- shared/sh_contracts/sh_economy.lua
|
|
114
|
+
return {
|
|
115
|
+
name = 'Economy',
|
|
116
|
+
server = {
|
|
117
|
+
'getBalance',
|
|
118
|
+
'addMoney',
|
|
119
|
+
'removeMoney',
|
|
120
|
+
'transfer',
|
|
121
|
+
},
|
|
122
|
+
client = {
|
|
123
|
+
'getBalance',
|
|
124
|
+
},
|
|
125
|
+
events = {
|
|
126
|
+
'economy:balanceChanged',
|
|
127
|
+
'economy:transfer',
|
|
128
|
+
},
|
|
129
|
+
}`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
pattern: 'module',
|
|
133
|
+
name: 'Module manifest',
|
|
134
|
+
mimeType: 'text/x-lua',
|
|
135
|
+
text: `-- module.lua
|
|
136
|
+
return {
|
|
137
|
+
name = 'shiva-economy',
|
|
138
|
+
version = '1.0.0',
|
|
139
|
+
description = 'Player economy with cash, bank, and transfers',
|
|
140
|
+
|
|
141
|
+
dependencies = {
|
|
142
|
+
'shiva-player',
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
optionalDependencies = {
|
|
146
|
+
'shiva-discord',
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
provides = {
|
|
150
|
+
'Economy',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
metadata = {
|
|
154
|
+
accounts = { type = 'table', default = {} },
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
events = {
|
|
158
|
+
'economy:balanceChanged',
|
|
159
|
+
'economy:transfer',
|
|
160
|
+
},
|
|
161
|
+
}`,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List all available example patterns, plus any from installed modules.
|
|
167
|
+
* @param {string} serverRoot
|
|
168
|
+
* @returns {Array<{uri, name, description, mimeType}>}
|
|
169
|
+
*/
|
|
170
|
+
function listExamples(serverRoot) {
|
|
171
|
+
const resources = BUILTIN_EXAMPLES.map(ex => ({
|
|
172
|
+
uri: `shiva:examples/${ex.pattern}`,
|
|
173
|
+
name: ex.name,
|
|
174
|
+
description: `Example: ${ex.name}`,
|
|
175
|
+
mimeType: ex.mimeType,
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
// Also surface example files from installed modules (examples/ directories)
|
|
179
|
+
if (serverRoot) {
|
|
180
|
+
try {
|
|
181
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
182
|
+
for (const mod of modules) {
|
|
183
|
+
const examplesDir = path.join(mod.path, 'examples');
|
|
184
|
+
if (!fs.existsSync(examplesDir)) continue;
|
|
185
|
+
for (const file of fs.readdirSync(examplesDir)) {
|
|
186
|
+
if (!file.endsWith('.lua')) continue;
|
|
187
|
+
const slug = `${mod.name}/${file.replace(/\.lua$/, '')}`;
|
|
188
|
+
resources.push({
|
|
189
|
+
uri: `shiva:examples/${slug}`,
|
|
190
|
+
name: `${mod.name}: ${file}`,
|
|
191
|
+
description: `Example from ${mod.name}`,
|
|
192
|
+
mimeType: 'text/x-lua',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// modules not found — skip
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return resources;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read an example resource by URI.
|
|
206
|
+
* @param {string} uri e.g. "shiva:examples/service"
|
|
207
|
+
* @param {string} serverRoot
|
|
208
|
+
* @returns {{uri, mimeType, text}|null}
|
|
209
|
+
*/
|
|
210
|
+
function readExample(uri, serverRoot) {
|
|
211
|
+
const pattern = uri.replace(/^shiva:examples\//, '');
|
|
212
|
+
|
|
213
|
+
// Built-in examples
|
|
214
|
+
const builtin = BUILTIN_EXAMPLES.find(ex => ex.pattern === pattern);
|
|
215
|
+
if (builtin) return { uri, mimeType: builtin.mimeType, text: builtin.text };
|
|
216
|
+
|
|
217
|
+
// Module-sourced examples (e.g. "shiva-economy/transfer")
|
|
218
|
+
if (serverRoot) {
|
|
219
|
+
const parts = pattern.split('/');
|
|
220
|
+
const modName = parts.slice(0, -1).join('/');
|
|
221
|
+
const file = parts[parts.length - 1] + '.lua';
|
|
222
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
223
|
+
const mod = modules.find(m => m.name === modName);
|
|
224
|
+
if (mod) {
|
|
225
|
+
const filePath = path.join(mod.path, 'examples', file);
|
|
226
|
+
if (fs.existsSync(filePath)) {
|
|
227
|
+
return { uri, mimeType: 'text/x-lua', text: fs.readFileSync(filePath, 'utf-8') };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { TEMPLATE_URI, listExamples, readExample };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
4
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
+
const {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ListResourcesRequestSchema,
|
|
9
|
+
ListResourceTemplatesRequestSchema,
|
|
10
|
+
ReadResourceRequestSchema,
|
|
11
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
12
|
+
|
|
13
|
+
const { findServerRoot } = require('../utils/server-root');
|
|
14
|
+
|
|
15
|
+
// ─── Load tools ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const contractsTool = require('./tools/contracts');
|
|
18
|
+
const modulesTool = require('./tools/modules');
|
|
19
|
+
const databaseTools = require('./tools/database'); // array
|
|
20
|
+
const configTool = require('./tools/config');
|
|
21
|
+
const eventsTool = require('./tools/events');
|
|
22
|
+
const docsTool = require('./tools/docs');
|
|
23
|
+
const itemsTools = require('./tools/items'); // array
|
|
24
|
+
|
|
25
|
+
const TOOLS = [
|
|
26
|
+
contractsTool,
|
|
27
|
+
modulesTool,
|
|
28
|
+
...databaseTools,
|
|
29
|
+
configTool,
|
|
30
|
+
eventsTool,
|
|
31
|
+
docsTool,
|
|
32
|
+
...itemsTools,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// ─── Load resources ───────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const docsResource = require('./resources/docs');
|
|
38
|
+
const contractsResource = require('./resources/contracts');
|
|
39
|
+
const examplesResource = require('./resources/examples');
|
|
40
|
+
|
|
41
|
+
const RESOURCE_TEMPLATES = [
|
|
42
|
+
{ uriTemplate: docsResource.TEMPLATE_URI, name: 'Module docs', description: 'AGENTS.md content for an installed module', mimeType: 'text/markdown' },
|
|
43
|
+
{ uriTemplate: contractsResource.TEMPLATE_URI, name: 'Contract', description: 'Shiva contract interface definition', mimeType: 'text/x-lua' },
|
|
44
|
+
{ uriTemplate: examplesResource.TEMPLATE_URI, name: 'Code example', description: 'Example Lua code for common Shiva patterns', mimeType: 'text/x-lua' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// ─── Server factory ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
async function startMcpServer() {
|
|
50
|
+
const serverRoot = findServerRoot(process.cwd());
|
|
51
|
+
|
|
52
|
+
const server = new Server(
|
|
53
|
+
{ name: 'shiva', version: '1.0.0' },
|
|
54
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ── Tools ──────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
60
|
+
tools: TOOLS.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
64
|
+
const tool = TOOLS.find(t => t.name === request.params.name);
|
|
65
|
+
if (!tool) {
|
|
66
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const result = await tool.handler(request.params.arguments || {}, serverRoot);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Resources ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
81
|
+
resourceTemplates: RESOURCE_TEMPLATES,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
85
|
+
if (!serverRoot) return { resources: [] };
|
|
86
|
+
const resources = [
|
|
87
|
+
...docsResource.listDocs(serverRoot),
|
|
88
|
+
...contractsResource.listContracts(serverRoot),
|
|
89
|
+
...examplesResource.listExamples(serverRoot),
|
|
90
|
+
];
|
|
91
|
+
return { resources };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
95
|
+
const { uri } = request.params;
|
|
96
|
+
if (!serverRoot) {
|
|
97
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: 'Not in a Shiva project directory.' }] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let resource = null;
|
|
101
|
+
|
|
102
|
+
if (uri.startsWith('shiva:docs/')) {
|
|
103
|
+
resource = docsResource.readDoc(uri, serverRoot);
|
|
104
|
+
} else if (uri.startsWith('shiva:contracts/')) {
|
|
105
|
+
resource = contractsResource.readContract(uri, serverRoot);
|
|
106
|
+
} else if (uri.startsWith('shiva:examples/')) {
|
|
107
|
+
resource = examplesResource.readExample(uri, serverRoot);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!resource) {
|
|
111
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: `Resource not found: ${uri}` }] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.text }] };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const transport = new StdioServerTransport();
|
|
118
|
+
await server.connect(transport);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { startMcpServer };
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
module.exports = {
|
|
10
|
+
name: 'shiva:getModuleConfig',
|
|
11
|
+
description: 'Get the configuration file and schema for a module',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
module: { type: 'string', description: 'Module name, e.g. shiva-economy or economy' },
|
|
16
|
+
},
|
|
17
|
+
required: ['module'],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async handler({ module: moduleName }, serverRoot) {
|
|
21
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
22
|
+
|
|
23
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
24
|
+
const mod = modules.find(m =>
|
|
25
|
+
m.name === moduleName ||
|
|
26
|
+
m.name === `shiva-${moduleName}` ||
|
|
27
|
+
m.name.endsWith(`-${moduleName}`)
|
|
28
|
+
);
|
|
29
|
+
if (!mod) return `Module not found: ${moduleName}`;
|
|
30
|
+
|
|
31
|
+
const configFile = path.join(mod.path, 'config', 'config.lua');
|
|
32
|
+
const schemaFile = path.join(mod.path, 'config', 'config.schema.lua');
|
|
33
|
+
const result = { module: mod.name, path: mod.path };
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(configFile)) {
|
|
36
|
+
result.config = fs.readFileSync(configFile, 'utf-8');
|
|
37
|
+
} else {
|
|
38
|
+
result.config = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(schemaFile)) {
|
|
42
|
+
result.schema = fs.readFileSync(schemaFile, 'utf-8');
|
|
43
|
+
} else {
|
|
44
|
+
result.schema = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!result.config && !result.schema) {
|
|
48
|
+
return `No config files found for ${moduleName}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'shiva:getContractMethods',
|
|
8
|
+
description: 'Get methods defined in a Shiva contract file',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
contract: { type: 'string', description: 'Contract name, e.g. Economy' },
|
|
13
|
+
},
|
|
14
|
+
required: ['contract'],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async handler({ contract }, serverRoot) {
|
|
18
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
19
|
+
|
|
20
|
+
// Check project-level contracts first, then shiva-core contracts
|
|
21
|
+
const slug = contract.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
22
|
+
const tries = [
|
|
23
|
+
path.join(serverRoot, 'shared', 'contracts', `sh_${slug}.lua`),
|
|
24
|
+
path.join(serverRoot, 'shared', 'sh_contracts', `sh_${slug}.lua`),
|
|
25
|
+
path.join(serverRoot, 'resources', '[shiva]', 'shiva-core', 'shared', 'sh_contracts', `sh_${slug}.lua`),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const file of tries) {
|
|
29
|
+
if (fs.existsSync(file)) {
|
|
30
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
31
|
+
return { file: path.relative(serverRoot, file), content: raw };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `Contract not found: sh_${slug}.lua — tried ${tries.map(t => path.relative(serverRoot, t)).join(', ')}`;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getDatabaseConfig } = require('../../utils/config-reader');
|
|
4
|
+
const { scanModules } = require('../../utils/lua-parser');
|
|
5
|
+
const { getResourcesDir } = require('../../utils/server-root');
|
|
6
|
+
|
|
7
|
+
async function openConnection(dbConfig) {
|
|
8
|
+
const mysql = require('mysql2/promise');
|
|
9
|
+
return mysql.createConnection({
|
|
10
|
+
host: dbConfig.host || '127.0.0.1',
|
|
11
|
+
port: dbConfig.port || 3306,
|
|
12
|
+
user: dbConfig.user,
|
|
13
|
+
password: dbConfig.password,
|
|
14
|
+
database: dbConfig.database,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = [
|
|
19
|
+
{
|
|
20
|
+
name: 'shiva:getDatabaseSchema',
|
|
21
|
+
description: 'Get all tables and columns from the live database',
|
|
22
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
23
|
+
|
|
24
|
+
async handler(_, serverRoot) {
|
|
25
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
26
|
+
const dbConfig = getDatabaseConfig(serverRoot);
|
|
27
|
+
if (!dbConfig) return 'No database configuration found in shiva.json';
|
|
28
|
+
|
|
29
|
+
const conn = await openConnection(dbConfig);
|
|
30
|
+
try {
|
|
31
|
+
const [rows] = await conn.execute(
|
|
32
|
+
`SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY
|
|
33
|
+
FROM information_schema.COLUMNS
|
|
34
|
+
WHERE TABLE_SCHEMA = ?
|
|
35
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION`,
|
|
36
|
+
[dbConfig.database]
|
|
37
|
+
);
|
|
38
|
+
const schema = {};
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
if (!schema[row.TABLE_NAME]) schema[row.TABLE_NAME] = [];
|
|
41
|
+
schema[row.TABLE_NAME].push({
|
|
42
|
+
column: row.COLUMN_NAME,
|
|
43
|
+
type: row.COLUMN_TYPE,
|
|
44
|
+
nullable: row.IS_NULLABLE === 'YES',
|
|
45
|
+
default: row.COLUMN_DEFAULT,
|
|
46
|
+
key: row.COLUMN_KEY || null,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return schema;
|
|
50
|
+
} finally {
|
|
51
|
+
await conn.end();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
name: 'shiva:getMigrationStatus',
|
|
58
|
+
description: 'Show which migrations have run and which are pending',
|
|
59
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
60
|
+
|
|
61
|
+
async handler(_, serverRoot) {
|
|
62
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
63
|
+
const dbConfig = getDatabaseConfig(serverRoot);
|
|
64
|
+
if (!dbConfig) return 'No database configuration found in shiva.json';
|
|
65
|
+
|
|
66
|
+
const conn = await openConnection(dbConfig);
|
|
67
|
+
try {
|
|
68
|
+
const [rows] = await conn.execute(
|
|
69
|
+
'SELECT migration, ran_at FROM shiva_migrations ORDER BY id'
|
|
70
|
+
).catch(() => [[]]);
|
|
71
|
+
|
|
72
|
+
const ran = new Set(rows.map(r => r.migration));
|
|
73
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
74
|
+
const all = [];
|
|
75
|
+
|
|
76
|
+
for (const mod of modules) {
|
|
77
|
+
for (const mig of (mod.manifest.migrations || [])) {
|
|
78
|
+
const id = `${mod.name}/${mig}`;
|
|
79
|
+
const ranAt = rows.find(r => r.migration === id);
|
|
80
|
+
all.push({
|
|
81
|
+
migration: id,
|
|
82
|
+
status: ran.has(id) ? 'ran' : 'pending',
|
|
83
|
+
ran_at: ranAt ? ranAt.ran_at : null,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return all;
|
|
88
|
+
} finally {
|
|
89
|
+
await conn.end();
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
module.exports = {
|
|
10
|
+
name: 'shiva:searchDocs',
|
|
11
|
+
description: 'Full-text search across AGENTS.md files in installed modules',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
query: { type: 'string', description: 'Search query' },
|
|
16
|
+
},
|
|
17
|
+
required: ['query'],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async handler({ query }, serverRoot) {
|
|
21
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
22
|
+
|
|
23
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
24
|
+
const results = [];
|
|
25
|
+
const q = query.toLowerCase();
|
|
26
|
+
|
|
27
|
+
for (const mod of modules) {
|
|
28
|
+
const agentsFile = path.join(mod.path, 'AGENTS.md');
|
|
29
|
+
if (!fs.existsSync(agentsFile)) continue;
|
|
30
|
+
const content = fs.readFileSync(agentsFile, 'utf-8');
|
|
31
|
+
if (!content.toLowerCase().includes(q)) continue;
|
|
32
|
+
const lines = content.split('\n').filter(l => l.toLowerCase().includes(q));
|
|
33
|
+
results.push({ module: mod.name, matches: lines.slice(0, 5) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return results.length > 0 ? results : `No documentation matches for "${query}"`;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { scanModules } = require('../../utils/lua-parser');
|
|
4
|
+
const { getResourcesDir } = require('../../utils/server-root');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'shiva:getRegisteredEvents',
|
|
8
|
+
description: 'Get all events declared across installed modules',
|
|
9
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
10
|
+
|
|
11
|
+
async handler(_, serverRoot) {
|
|
12
|
+
if (!serverRoot) return 'Not in a Shiva project directory.';
|
|
13
|
+
|
|
14
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
15
|
+
const events = [];
|
|
16
|
+
|
|
17
|
+
for (const mod of modules) {
|
|
18
|
+
for (const event of (mod.manifest.events || [])) {
|
|
19
|
+
events.push({ module: mod.name, event });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (events.length === 0) return 'No events declared in installed modules.';
|
|
24
|
+
return events;
|
|
25
|
+
},
|
|
26
|
+
};
|