@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,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
+ };