@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,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { requireServerRoot } = require('../../utils/server-root');
|
|
8
|
+
const { getDatabaseConfig } = require('../../utils/config-reader');
|
|
9
|
+
const { scanModules } = require('../../utils/lua-parser');
|
|
10
|
+
|
|
11
|
+
const MIGRATIONS_TABLE = 'shiva_migrations';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the `shiva migrate:status` command.
|
|
15
|
+
* @param {import('commander').Command} program
|
|
16
|
+
*/
|
|
17
|
+
function migrateStatusCommand(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('migrate:status')
|
|
20
|
+
.description('Show the status of all database migrations')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
await run();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function run() {
|
|
27
|
+
const serverRoot = requireServerRoot();
|
|
28
|
+
const dbConfig = getDatabaseConfig(serverRoot);
|
|
29
|
+
|
|
30
|
+
if (!dbConfig) {
|
|
31
|
+
console.error(chalk.red('✖ No database configuration found in shiva.json'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let connection;
|
|
36
|
+
try {
|
|
37
|
+
const mysql = require('mysql2/promise');
|
|
38
|
+
connection = await mysql.createConnection({
|
|
39
|
+
host: dbConfig.host,
|
|
40
|
+
port: dbConfig.port || 3306,
|
|
41
|
+
user: dbConfig.user,
|
|
42
|
+
password: dbConfig.password,
|
|
43
|
+
database: dbConfig.database,
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(chalk.red('✖ Could not connect to database:'), err.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const resourcesDir = path.join(serverRoot, 'resources');
|
|
52
|
+
const modules = scanModules(resourcesDir);
|
|
53
|
+
const allMigrations = collectMigrations(modules);
|
|
54
|
+
|
|
55
|
+
let ranMigrations = new Map();
|
|
56
|
+
const tableExists = await checkTableExists(connection, dbConfig.database);
|
|
57
|
+
if (tableExists) {
|
|
58
|
+
const [rows] = await connection.execute(
|
|
59
|
+
`SELECT \`migration\`, \`batch\`, \`ran_at\` FROM \`${MIGRATIONS_TABLE}\` ORDER BY \`id\``
|
|
60
|
+
);
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
ranMigrations.set(row.migration, row);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.bold('Migration Status'));
|
|
68
|
+
console.log(chalk.gray('─'.repeat(72)));
|
|
69
|
+
|
|
70
|
+
if (allMigrations.length === 0) {
|
|
71
|
+
console.log(chalk.gray(' No migration files found.'));
|
|
72
|
+
} else {
|
|
73
|
+
const statusWidth = 9;
|
|
74
|
+
const batchWidth = 7;
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.bold(
|
|
77
|
+
` ${'Status'.padEnd(statusWidth)} ${'Batch'.padEnd(batchWidth)} Migration`
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
console.log(chalk.gray(' ' + '─'.repeat(68)));
|
|
81
|
+
|
|
82
|
+
for (const migration of allMigrations) {
|
|
83
|
+
const info = ranMigrations.get(migration.name);
|
|
84
|
+
if (info) {
|
|
85
|
+
console.log(
|
|
86
|
+
chalk.green(' ' + 'Ran'.padEnd(statusWidth)) +
|
|
87
|
+
chalk.gray(' ' + String(info.batch).padEnd(batchWidth)) +
|
|
88
|
+
' ' + migration.name
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.yellow(' ' + 'Pending'.padEnd(statusWidth)) +
|
|
93
|
+
chalk.gray(' ' + '—'.padEnd(batchWidth)) +
|
|
94
|
+
' ' + chalk.yellow(migration.name)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(chalk.gray('─'.repeat(72)));
|
|
101
|
+
const ranCount = allMigrations.filter(m => ranMigrations.has(m.name)).length;
|
|
102
|
+
const pendingCount = allMigrations.length - ranCount;
|
|
103
|
+
console.log(chalk.gray(` ${ranCount} ran · ${pendingCount} pending`));
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
} finally {
|
|
107
|
+
await connection.end();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function checkTableExists(conn, database) {
|
|
112
|
+
const [rows] = await conn.execute(
|
|
113
|
+
`SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
|
|
114
|
+
[database, MIGRATIONS_TABLE]
|
|
115
|
+
);
|
|
116
|
+
return rows.length > 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectMigrations(modules) {
|
|
120
|
+
const migrations = [];
|
|
121
|
+
for (const mod of modules) {
|
|
122
|
+
const migrationsDir = path.join(mod.path, 'migrations');
|
|
123
|
+
if (!fs.existsSync(migrationsDir)) continue;
|
|
124
|
+
const files = fs.readdirSync(migrationsDir)
|
|
125
|
+
.filter(f => f.endsWith('.lua'))
|
|
126
|
+
.sort();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
migrations.push({
|
|
129
|
+
name: `${mod.name}/${file.replace('.lua', '')}`,
|
|
130
|
+
module: mod.name,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return migrations.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = migrateStatusCommand;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
const { requireServerRoot, getResourcesDir } = require('../../utils/server-root');
|
|
7
|
+
const { scanModules } = require('../../utils/lua-parser');
|
|
8
|
+
|
|
9
|
+
function moduleListCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('module:list')
|
|
12
|
+
.description('List all installed Shiva modules')
|
|
13
|
+
.action(() => { run(); });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run() {
|
|
17
|
+
const serverRoot = requireServerRoot();
|
|
18
|
+
const resourcesDir = getResourcesDir(serverRoot);
|
|
19
|
+
const modules = scanModules(resourcesDir);
|
|
20
|
+
|
|
21
|
+
console.log('');
|
|
22
|
+
if (modules.length === 0) {
|
|
23
|
+
console.log(chalk.gray(' No modules found. Run `shiva install` to install modules.'));
|
|
24
|
+
console.log('');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nameW = Math.max(20, ...modules.map(m => m.name.length)) + 2;
|
|
29
|
+
const verW = 10;
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold(` ${'Module'.padEnd(nameW)} ${'Version'.padEnd(verW)} Dependencies`));
|
|
32
|
+
console.log(chalk.gray(' ' + '─'.repeat(nameW + verW + 30)));
|
|
33
|
+
|
|
34
|
+
for (const mod of modules) {
|
|
35
|
+
const name = mod.name.padEnd(nameW);
|
|
36
|
+
const version = (mod.manifest.version || '?').padEnd(verW);
|
|
37
|
+
const deps = (mod.manifest.dependencies || []).join(', ') || chalk.gray('—');
|
|
38
|
+
console.log(` ${chalk.cyan(name)} ${chalk.gray(version)} ${deps}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(chalk.gray(` ${modules.length} module(s) installed.`));
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = moduleListCommand;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { requireServerRoot, getResourcesDir } = require('../../utils/server-root');
|
|
8
|
+
const { scanModules } = require('../../utils/lua-parser');
|
|
9
|
+
|
|
10
|
+
function moduleStatusCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('module:status')
|
|
13
|
+
.description('Show detailed status for all installed modules')
|
|
14
|
+
.option('-m, --module <name>', 'Show status for a specific module only')
|
|
15
|
+
.action((options) => { run(options); });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function run(options) {
|
|
19
|
+
const serverRoot = requireServerRoot();
|
|
20
|
+
const resourcesDir = getResourcesDir(serverRoot);
|
|
21
|
+
let modules = scanModules(resourcesDir);
|
|
22
|
+
|
|
23
|
+
if (options.module) {
|
|
24
|
+
const filter = options.module.startsWith('shiva-') ? options.module : `shiva-${options.module}`;
|
|
25
|
+
modules = modules.filter(m => m.name === filter);
|
|
26
|
+
if (modules.length === 0) {
|
|
27
|
+
console.error(chalk.red(`✖ Module not found: ${filter}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (modules.length === 0) {
|
|
33
|
+
console.log(chalk.gray('\n No modules found.\n'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
for (const mod of modules) {
|
|
39
|
+
const m = mod.manifest;
|
|
40
|
+
const migrationsDir = path.join(mod.path, 'migrations');
|
|
41
|
+
const migrationCount = fs.existsSync(migrationsDir)
|
|
42
|
+
? fs.readdirSync(migrationsDir).filter(f => f.endsWith('.lua')).length
|
|
43
|
+
: 0;
|
|
44
|
+
|
|
45
|
+
const testsDir = path.join(mod.path, 'tests');
|
|
46
|
+
const testCount = fs.existsSync(testsDir)
|
|
47
|
+
? fs.readdirSync(testsDir).filter(f => f.endsWith('_spec.lua')).length
|
|
48
|
+
: 0;
|
|
49
|
+
|
|
50
|
+
console.log(chalk.bold.cyan(` ${mod.name}`));
|
|
51
|
+
console.log(chalk.gray(` ${'─'.repeat(50)}`));
|
|
52
|
+
console.log(` Version : ${chalk.white(m.version || '?')}`);
|
|
53
|
+
console.log(` Description : ${chalk.gray(m.description || '—')}`);
|
|
54
|
+
console.log(` Path : ${chalk.gray(mod.path)}`);
|
|
55
|
+
console.log(` Dependencies : ${(m.dependencies || []).length > 0 ? (m.dependencies || []).join(', ') : chalk.gray('none')}`);
|
|
56
|
+
console.log(` Provides : ${(m.provides || []).length > 0 ? (m.provides || []).join(', ') : chalk.gray('none')}`);
|
|
57
|
+
console.log(` Migrations : ${migrationCount}`);
|
|
58
|
+
console.log(` Tests : ${testCount}`);
|
|
59
|
+
console.log(` Events : ${(m.events || []).length}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = moduleStatusCommand;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireServerRoot } = require('../utils/server-root');
|
|
5
|
+
const { readShivaConfig } = require('../utils/config-reader');
|
|
6
|
+
const { readLockfile } = require('../packages/lockfile');
|
|
7
|
+
const { resolveVersion } = require('../packages/resolver');
|
|
8
|
+
const registry = require('../packages/registry');
|
|
9
|
+
|
|
10
|
+
function outdatedCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('outdated')
|
|
13
|
+
.description('Show modules with available updates')
|
|
14
|
+
.action(async () => { await run(); });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function run() {
|
|
18
|
+
const serverRoot = requireServerRoot();
|
|
19
|
+
const config = readShivaConfig(serverRoot);
|
|
20
|
+
const lock = readLockfile(serverRoot);
|
|
21
|
+
const regUrl = registry.getRegistryUrl(config);
|
|
22
|
+
const modules = config.modules || {};
|
|
23
|
+
|
|
24
|
+
console.log('');
|
|
25
|
+
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const [name, constraint] of Object.entries(modules)) {
|
|
28
|
+
if (constraint.startsWith('file:')) continue;
|
|
29
|
+
let versions;
|
|
30
|
+
try { versions = await registry.fetchVersions(regUrl, name); } catch { continue; }
|
|
31
|
+
const latest = resolveVersion(versions, 'latest');
|
|
32
|
+
const wanted = resolveVersion(versions, constraint);
|
|
33
|
+
const current = lock.modules[name]?.version || chalk.gray('not installed');
|
|
34
|
+
if (latest && latest !== current) {
|
|
35
|
+
rows.push({ name, current, wanted: wanted || '?', latest });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rows.length === 0) {
|
|
40
|
+
console.log(chalk.green('✔ All modules are up to date.'));
|
|
41
|
+
console.log('');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nw = Math.max(20, ...rows.map(r => r.name.length)) + 2;
|
|
46
|
+
console.log(chalk.bold(` ${'Module'.padEnd(nw)} ${'Current'.padEnd(12)} ${'Wanted'.padEnd(12)} Latest`));
|
|
47
|
+
console.log(chalk.gray(' ' + '─'.repeat(nw + 38)));
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
console.log(
|
|
50
|
+
` ${chalk.cyan(r.name.padEnd(nw))} ` +
|
|
51
|
+
`${chalk.gray(String(r.current).padEnd(12))} ` +
|
|
52
|
+
`${chalk.yellow(String(r.wanted).padEnd(12))} ` +
|
|
53
|
+
`${chalk.green(r.latest)}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = outdatedCommand;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
|
|
8
|
+
const { requireServerRoot, getShivaModulesDir, getResourcesDir } = require('../utils/server-root');
|
|
9
|
+
const { readShivaConfig, writeShivaConfig } = require('../utils/config-reader');
|
|
10
|
+
const { unlockModule } = require('../packages/lockfile');
|
|
11
|
+
const { scanModules } = require('../utils/lua-parser');
|
|
12
|
+
const { normalizeModuleName } = require('../generators/index');
|
|
13
|
+
|
|
14
|
+
function removeCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('remove <module>')
|
|
17
|
+
.description('Remove an installed module')
|
|
18
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
19
|
+
.action(async (moduleName, options) => { await run(moduleName, options); });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(rawName, options) {
|
|
23
|
+
const serverRoot = requireServerRoot();
|
|
24
|
+
const name = normalizeModuleName(rawName);
|
|
25
|
+
const moduleDir = path.join(getShivaModulesDir(serverRoot), name);
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(moduleDir)) {
|
|
28
|
+
console.error(chalk.red(`✖ Module not found: ${name}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const modules = scanModules(getResourcesDir(serverRoot));
|
|
33
|
+
const dependents = modules.filter(m => (m.manifest.dependencies || []).includes(name));
|
|
34
|
+
if (dependents.length > 0) {
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(chalk.yellow(` ⚠ The following modules depend on ${name}:`));
|
|
37
|
+
dependents.forEach(m => console.log(chalk.yellow(` - ${m.name}`)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!options.force) {
|
|
41
|
+
const { confirm } = await inquirer.prompt([{
|
|
42
|
+
type: 'confirm', name: 'confirm',
|
|
43
|
+
message: `Remove ${chalk.bold(name)}?`,
|
|
44
|
+
default: false,
|
|
45
|
+
}]);
|
|
46
|
+
if (!confirm) { console.log(chalk.gray('Aborted.')); return; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fs.rmSync(moduleDir, { recursive: true, force: true });
|
|
50
|
+
unlockModule(serverRoot, name);
|
|
51
|
+
|
|
52
|
+
const config = readShivaConfig(serverRoot);
|
|
53
|
+
delete config.modules[name];
|
|
54
|
+
writeShivaConfig(serverRoot, config);
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk.green(`✔ Removed ${name}`));
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = removeCommand;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { requireServerRoot, getResourcesDir } = require('../utils/server-root');
|
|
8
|
+
const { getDatabaseConfig } = require('../utils/config-reader');
|
|
9
|
+
const { scanModules } = require('../utils/lua-parser');
|
|
10
|
+
const { normalizeModuleName } = require('../generators/index');
|
|
11
|
+
|
|
12
|
+
function seedCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('seed')
|
|
15
|
+
.description('Run database seeders')
|
|
16
|
+
.option('-m, --module <module>', 'Seed only a specific module')
|
|
17
|
+
.option('--dry-run', 'Show what would run without executing')
|
|
18
|
+
.action(async (options) => { await run(options); });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(options) {
|
|
22
|
+
const serverRoot = requireServerRoot();
|
|
23
|
+
const dbConfig = getDatabaseConfig(serverRoot);
|
|
24
|
+
|
|
25
|
+
if (!dbConfig) {
|
|
26
|
+
console.error(chalk.red('✖ No database configuration found in shiva.json'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let connection;
|
|
31
|
+
try {
|
|
32
|
+
const mysql = require('mysql2/promise');
|
|
33
|
+
connection = await mysql.createConnection({
|
|
34
|
+
host: dbConfig.host,
|
|
35
|
+
port: dbConfig.port || 3306,
|
|
36
|
+
user: dbConfig.user,
|
|
37
|
+
password: dbConfig.password,
|
|
38
|
+
database: dbConfig.database,
|
|
39
|
+
multipleStatements: true,
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(chalk.red('✖ Could not connect to database:'), err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const resourcesDir = getResourcesDir(serverRoot);
|
|
48
|
+
let modules = scanModules(resourcesDir);
|
|
49
|
+
|
|
50
|
+
if (options.module) {
|
|
51
|
+
const name = normalizeModuleName(options.module);
|
|
52
|
+
modules = modules.filter(m => m.name === name);
|
|
53
|
+
if (modules.length === 0) {
|
|
54
|
+
console.error(chalk.red(`✖ Module not found: ${name}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const seeds = collectSeeds(modules);
|
|
60
|
+
|
|
61
|
+
if (seeds.length === 0) {
|
|
62
|
+
console.log(chalk.gray(' No seed files found (expected at seeds/*.lua inside each module).'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.bold(`Running ${seeds.length} seeder(s)...\n`));
|
|
68
|
+
|
|
69
|
+
for (const seed of seeds) {
|
|
70
|
+
if (options.dryRun) {
|
|
71
|
+
console.log(chalk.yellow(` [dry-run] ${seed.name}`));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.stdout.write(chalk.gray(` Seeding: ${seed.name} ...`));
|
|
76
|
+
try {
|
|
77
|
+
const mod = require(seed.path);
|
|
78
|
+
await mod.run({ execute: (sql, params) => connection.execute(sql, params || []) });
|
|
79
|
+
console.log(chalk.green(' done'));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.log(chalk.red(' failed'));
|
|
82
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!options.dryRun) {
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(chalk.green('✔ Seeding complete.'));
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
await connection.end();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectSeeds(modules) {
|
|
96
|
+
const seeds = [];
|
|
97
|
+
for (const mod of modules) {
|
|
98
|
+
const seedsDir = path.join(mod.path, 'seeds');
|
|
99
|
+
if (!fs.existsSync(seedsDir)) continue;
|
|
100
|
+
const files = fs.readdirSync(seedsDir).filter(f => f.endsWith('.lua')).sort();
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
seeds.push({ name: `${mod.name}/${file}`, path: path.join(seedsDir, file) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return seeds;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = seedCommand;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
const { requireServerRoot, getResourcesDir } = require('../utils/server-root');
|
|
9
|
+
const { scanModules } = require('../utils/lua-parser');
|
|
10
|
+
const { normalizeModuleName } = require('../generators/index');
|
|
11
|
+
|
|
12
|
+
function testCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('test')
|
|
15
|
+
.description('Run module test suites')
|
|
16
|
+
.option('-m, --module <module>', 'Run tests for a specific module')
|
|
17
|
+
.option('-f, --filter <pattern>', 'Filter test files by name pattern')
|
|
18
|
+
.action(async (options) => { await run(options); });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(options) {
|
|
22
|
+
const serverRoot = requireServerRoot();
|
|
23
|
+
const resourcesDir = getResourcesDir(serverRoot);
|
|
24
|
+
let modules = scanModules(resourcesDir);
|
|
25
|
+
|
|
26
|
+
if (options.module) {
|
|
27
|
+
const name = normalizeModuleName(options.module);
|
|
28
|
+
modules = modules.filter(m => m.name === name);
|
|
29
|
+
if (modules.length === 0) {
|
|
30
|
+
console.error(chalk.red(`✖ Module not found: ${name}`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const specs = collectSpecs(modules, options.filter);
|
|
36
|
+
|
|
37
|
+
if (specs.length === 0) {
|
|
38
|
+
console.log(chalk.gray('\n No test specs found (_spec.lua files in tests/ directories).\n'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.bold(`Running ${specs.length} spec file(s)...\n`));
|
|
44
|
+
|
|
45
|
+
let passed = 0;
|
|
46
|
+
let failed = 0;
|
|
47
|
+
|
|
48
|
+
for (const spec of specs) {
|
|
49
|
+
process.stdout.write(chalk.gray(` ${spec.label} ...`));
|
|
50
|
+
try {
|
|
51
|
+
execSync(`lua ${spec.path}`, { stdio: 'pipe', cwd: path.dirname(spec.path) });
|
|
52
|
+
console.log(chalk.green(' pass'));
|
|
53
|
+
passed++;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.log(chalk.red(' fail'));
|
|
56
|
+
const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
57
|
+
output.split('\n').filter(Boolean).forEach(l => console.log(chalk.red(` ${l}`)));
|
|
58
|
+
failed++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(
|
|
64
|
+
failed === 0
|
|
65
|
+
? chalk.green(`✔ All ${passed} spec(s) passed.`)
|
|
66
|
+
: chalk.red(`✖ ${failed} failed, ${passed} passed.`)
|
|
67
|
+
);
|
|
68
|
+
console.log('');
|
|
69
|
+
|
|
70
|
+
if (failed > 0) process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectSpecs(modules, filter) {
|
|
74
|
+
const specs = [];
|
|
75
|
+
for (const mod of modules) {
|
|
76
|
+
const testsDir = path.join(mod.path, 'tests');
|
|
77
|
+
if (!fs.existsSync(testsDir)) continue;
|
|
78
|
+
const files = fs.readdirSync(testsDir)
|
|
79
|
+
.filter(f => f.endsWith('_spec.lua'))
|
|
80
|
+
.filter(f => !filter || f.includes(filter));
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
specs.push({ label: `${mod.name}/${file}`, path: path.join(testsDir, file) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return specs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = testCommand;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const { requireServerRoot, getShivaModulesDir } = require('../utils/server-root');
|
|
8
|
+
const { readShivaConfig } = require('../utils/config-reader');
|
|
9
|
+
const { readLockfile, lockModule } = require('../packages/lockfile');
|
|
10
|
+
const { resolveVersion } = require('../packages/resolver');
|
|
11
|
+
const registry = require('../packages/registry');
|
|
12
|
+
const { normalizeModuleName } = require('../generators/index');
|
|
13
|
+
|
|
14
|
+
function updateCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('update [module]')
|
|
17
|
+
.description('Update installed modules to the latest compatible version')
|
|
18
|
+
.action(async (moduleName) => { await run(moduleName); });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(moduleName) {
|
|
22
|
+
const serverRoot = requireServerRoot();
|
|
23
|
+
const config = readShivaConfig(serverRoot);
|
|
24
|
+
const lock = readLockfile(serverRoot);
|
|
25
|
+
const regUrl = registry.getRegistryUrl(config);
|
|
26
|
+
|
|
27
|
+
let targets = Object.entries(config.modules || {});
|
|
28
|
+
if (moduleName) {
|
|
29
|
+
const name = normalizeModuleName(moduleName);
|
|
30
|
+
targets = targets.filter(([n]) => n === name);
|
|
31
|
+
if (targets.length === 0) {
|
|
32
|
+
console.error(chalk.red(`✖ ${name} not found in shiva.json`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
let updated = 0;
|
|
39
|
+
|
|
40
|
+
for (const [name, constraint] of targets) {
|
|
41
|
+
if (constraint.startsWith('file:')) {
|
|
42
|
+
console.log(chalk.gray(` ${name}: file reference, skipping`));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let versions;
|
|
47
|
+
try {
|
|
48
|
+
versions = await registry.fetchVersions(regUrl, name);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(chalk.red(` ${name}: failed to fetch versions — ${err.message}`));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const latest = resolveVersion(versions, constraint);
|
|
55
|
+
const current = lock.modules[name]?.version;
|
|
56
|
+
|
|
57
|
+
if (!latest) {
|
|
58
|
+
console.log(chalk.yellow(` ${name}: no version matching ${constraint}`));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (current === latest) {
|
|
63
|
+
console.log(chalk.gray(` ${name}@${latest} already up to date`));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const destDir = path.join(getShivaModulesDir(serverRoot), name);
|
|
68
|
+
process.stdout.write(chalk.gray(` Updating ${name} ${current || '?'} → ${latest} ...`));
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const meta = await registry.fetchModuleMeta(regUrl, name, latest);
|
|
72
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
|
|
73
|
+
await registry.downloadModule(meta.downloadUrl, destDir);
|
|
74
|
+
lockModule(serverRoot, name, { version: latest, resolved: meta.downloadUrl });
|
|
75
|
+
console.log(chalk.green(' done'));
|
|
76
|
+
updated++;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.log(chalk.red(' failed'));
|
|
79
|
+
console.error(chalk.red(` ${err.message}`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(updated > 0
|
|
85
|
+
? chalk.green(`✔ Updated ${updated} module(s).`)
|
|
86
|
+
: chalk.gray(' Everything is up to date.'));
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = updateCommand;
|