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