amxx-builder 1.5.1

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 (68) hide show
  1. package/AGENTS.md +111 -0
  2. package/README.md +485 -0
  3. package/action-entry.js +42 -0
  4. package/action.yml +55 -0
  5. package/defaults/amxbuild.defaults.yml +40 -0
  6. package/index.js +4 -0
  7. package/mcp/dep-resolver.js +75 -0
  8. package/mcp/handlers.js +862 -0
  9. package/mcp/mcp-server.js +112 -0
  10. package/mcp/registry.js +863 -0
  11. package/mcp/symbol-index.js +171 -0
  12. package/package.json +68 -0
  13. package/src/archiver.js +140 -0
  14. package/src/asset-fetcher.js +277 -0
  15. package/src/build-plan.js +101 -0
  16. package/src/build-service.js +188 -0
  17. package/src/cache-dir.js +21 -0
  18. package/src/cache-info.js +104 -0
  19. package/src/cli.js +302 -0
  20. package/src/collector.js +89 -0
  21. package/src/commands/build.js +49 -0
  22. package/src/commands/cache.js +77 -0
  23. package/src/commands/clean.js +33 -0
  24. package/src/commands/compile-renderer.js +38 -0
  25. package/src/commands/deploy.js +40 -0
  26. package/src/commands/deps-tree.js +92 -0
  27. package/src/commands/doctor.js +77 -0
  28. package/src/commands/dry-run.js +64 -0
  29. package/src/commands/init.js +228 -0
  30. package/src/commands/mcp.js +13 -0
  31. package/src/commands/releases.js +45 -0
  32. package/src/commands/resolve-manifest.js +27 -0
  33. package/src/commands/serve.js +489 -0
  34. package/src/commands/shared.js +24 -0
  35. package/src/commands/validate.js +34 -0
  36. package/src/commands/watch.js +209 -0
  37. package/src/compile-utils.js +65 -0
  38. package/src/compiler-fetcher.js +327 -0
  39. package/src/compiler.js +228 -0
  40. package/src/dep-graph.js +92 -0
  41. package/src/deployer.js +197 -0
  42. package/src/deps-resolver.js +127 -0
  43. package/src/deps-tree.js +202 -0
  44. package/src/env.js +18 -0
  45. package/src/events.js +29 -0
  46. package/src/format.js +23 -0
  47. package/src/fs-utils.js +87 -0
  48. package/src/include-tree.js +845 -0
  49. package/src/ini-builder.js +44 -0
  50. package/src/jsonrpc-transport.js +195 -0
  51. package/src/logger.js +50 -0
  52. package/src/manifest-path.js +34 -0
  53. package/src/manifest.js +373 -0
  54. package/src/progress.js +66 -0
  55. package/src/rcon.js +103 -0
  56. package/src/release-fetcher.js +206 -0
  57. package/src/release-lister.js +79 -0
  58. package/src/repo-fetcher.js +273 -0
  59. package/src/retry.js +50 -0
  60. package/src/schema.js +54 -0
  61. package/src/update-check.js +114 -0
  62. package/src/validate.js +69 -0
  63. package/src/watcher.js +135 -0
  64. package/templates/init-build.bat +11 -0
  65. package/templates/init-build.sh +7 -0
  66. package/templates/init-deploy.env +14 -0
  67. package/templates/init-manifest.yml +6 -0
  68. package/templates/init-workflow.yml +59 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * `amxb serve` — thin JSON-RPC interface adapter for editor integration.
6
+ *
7
+ * Generic JSON-RPC 2.0 over stdio (src/jsonrpc-transport.js). Every method is a
8
+ * thin mapping: normalize args → call the core single-source function → shape
9
+ * the result. NO domain logic lives here (per AGENTS.md); if a behavior is
10
+ * needed in more than one interface it belongs in src/.
11
+ *
12
+ * Environment: stdout must stay pure JSON-RPC, so logs go to stderr
13
+ * (logger.setStderr) and progress bars are disabled. .env is loaded from the
14
+ * workspace root (cwd), like the CLI.
15
+ *
16
+ * Method table:
17
+ * manifest.validate → validate.manifestFile
18
+ * manifest.resolve → env.loadEnv + manifest.resolveManifest
19
+ * include.resolve → include-tree parseIncludeDirective + searchIncludeFile
20
+ * include.list → include-tree fetchDepIncludeDir + collectIncFiles
21
+ * amxmodx.includes.list → compiler-fetcher fetchCompiler + glob
22
+ * amxmodx.include.get → compiler-fetcher fetchCompiler + glob + read
23
+ * deps.tree → deps-tree buildDepTree + assembleRootDeps
24
+ * releases.list → release-lister listReleases / listTags
25
+ * cache.info → cache-info getCacheInfo
26
+ * build.plan → build-plan buildPlanData
27
+ * build.start → build-service runBuild (+ event notifications)
28
+ * build.cancel → abort the running build (AbortController)
29
+ * compile.single → compiler.compileSingle
30
+ * watch.start / watch.stop → watcher.startWatch (+ watch.changed notifications)
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const os = require('os');
35
+ const path = require('path');
36
+ const glob = require('fast-glob');
37
+ const dotenv = require('dotenv');
38
+
39
+ const logger = require('../logger');
40
+ const progress = require('../progress');
41
+ const { JsonRpcServer } = require('../jsonrpc-transport');
42
+ const { on, off, EVENTS } = require('../events');
43
+
44
+ const { loadEnv } = require('../env');
45
+ const { resolveManifestPath } = require('../manifest-path');
46
+ const { resolveManifest, parseManifest, resolveGithubToken, parseDepString } = require('../manifest');
47
+ const { validateManifestFile } = require('../validate');
48
+ const { fetchDepIncludeDir, collectIncFiles, parseIncludeDirective, searchIncludeFile } = require('../include-tree');
49
+ const { fetchCompiler, resolveAmxmodxVersion } = require('../compiler-fetcher');
50
+ const { buildDepTree, assembleRootDeps } = require('../deps-tree');
51
+ const { listReleases, listTags } = require('../release-lister');
52
+ const { getCacheInfo } = require('../cache-info');
53
+ const { buildPlanData } = require('../build-plan');
54
+ const { runBuild } = require('../build-service');
55
+ const { compileSingle } = require('../compiler');
56
+ const { startWatch } = require('../watcher');
57
+
58
+ // ─── Small interface helpers (no domain logic) ────────────────────────────────
59
+
60
+ function readFileSafe(absPath) {
61
+ try {
62
+ const text = fs.readFileSync(absPath, 'utf8');
63
+ return text;
64
+ } catch (err) {
65
+ return `[error reading file: ${err.message}]`;
66
+ }
67
+ }
68
+
69
+ // Resolve the AMX Mod X version for a request: explicit `version` arg wins,
70
+ // then the project manifest's amxmodx.version, then latest. Priority logic
71
+ // lives in core (compiler-fetcher.resolveAmxmodxVersion).
72
+ async function resolveVersionFromParams(params) {
73
+ if (params?.version) return resolveAmxmodxVersion(null, { version: params.version });
74
+
75
+ const manifestPath = params?.manifest
76
+ ? path.resolve(params.manifest)
77
+ : resolveManifestPath().path;
78
+ let manifest = null;
79
+ if (fs.existsSync(manifestPath)) {
80
+ try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
81
+ }
82
+ return resolveAmxmodxVersion(manifest, { noFetch: params?.noFetch === true });
83
+ }
84
+
85
+ function manifestPathFor(params) {
86
+ return params?.manifest ? path.resolve(params.manifest) : resolveManifestPath().path;
87
+ }
88
+
89
+ /**
90
+ * Create and configure the JSON-RPC server with all methods wired to core.
91
+ * Does NOT connect or set up the environment — call runServe() for that.
92
+ */
93
+ function createServeServer() {
94
+ const server = new JsonRpcServer();
95
+
96
+ // One build / one watcher at a time (per-process).
97
+ let activeBuild = null; // AbortController for the running build
98
+ let activeWatcher = null; // chokidar watcher instance
99
+
100
+ // ─── Read-only: manifest ──────────────────────────────────────────────────
101
+
102
+ server.onRequest('manifest.validate', (params) => {
103
+ return validateManifestFile(manifestPathFor(params));
104
+ });
105
+
106
+ server.onRequest('manifest.resolve', (params) => {
107
+ const manifestPath = manifestPathFor(params);
108
+ loadEnv(manifestPath);
109
+ return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
110
+ });
111
+
112
+ // ─── Include resolution ──────────────────────────────────────────────────
113
+
114
+ server.onRequest('include.resolve', async (params) => {
115
+ let parsed;
116
+ try {
117
+ parsed = parseIncludeDirective(params?.directive || params?.include);
118
+ } catch (err) {
119
+ err.code = -32602;
120
+ throw err;
121
+ }
122
+ const { filename, localFirst } = parsed;
123
+ const searchPaths = [];
124
+
125
+ if (localFirst) {
126
+ const smaDir = params?.sma_file
127
+ ? path.dirname(path.resolve(params.sma_file))
128
+ : process.cwd();
129
+ searchPaths.push({
130
+ path: smaDir,
131
+ label: params?.sma_file ? `local (${path.basename(params.sma_file)})` : 'local (current directory)',
132
+ });
133
+ }
134
+
135
+ // Dep includes come BEFORE the stdlib — matching the real build's search
136
+ // order (deps first, then the compiler bundle).
137
+ const errors = [];
138
+ let manifest = null;
139
+ const manifestPath = manifestPathFor(params);
140
+ if (fs.existsSync(manifestPath)) {
141
+ try {
142
+ manifest = parseManifest(manifestPath);
143
+ for (const dep of manifest.globalDeps) {
144
+ try {
145
+ const depDir = await fetchDepIncludeDir(
146
+ dep, resolveGithubToken(manifest, dep.repo),
147
+ params?.noFetch === true, manifest.github.ssh
148
+ );
149
+ searchPaths.push({ path: depDir, label: `${dep.repo}@${dep.ref}` });
150
+ } catch (err) {
151
+ errors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
152
+ }
153
+ }
154
+ } catch (err) {
155
+ errors.push(`manifest ${manifestPath}: ${err.message}`);
156
+ }
157
+ }
158
+
159
+ const version = await resolveVersionFromParams(params);
160
+ const { includeDir } = await fetchCompiler(version);
161
+ if (includeDir) searchPaths.push({ path: includeDir, label: `AMXX stdlib ${version}` });
162
+
163
+ const result = searchIncludeFile(searchPaths, filename);
164
+ if (!result) {
165
+ return {
166
+ found: false,
167
+ filename,
168
+ searched: searchPaths.map((s) => s.label),
169
+ errors: errors.length ? errors : undefined,
170
+ };
171
+ }
172
+ return {
173
+ found: true,
174
+ filename,
175
+ absPath: result.foundPath,
176
+ source: result.label,
177
+ searched: searchPaths.map((s) => s.label),
178
+ errors: errors.length ? errors : undefined,
179
+ };
180
+ });
181
+
182
+ server.onRequest('include.list', async (params) => {
183
+ const manifestPath = manifestPathFor(params);
184
+ if (!fs.existsSync(manifestPath)) {
185
+ const err = new Error(`Manifest not found: ${manifestPath}`);
186
+ err.code = -32602;
187
+ throw err;
188
+ }
189
+ const manifest = parseManifest(manifestPath);
190
+
191
+ const deps = [];
192
+ for (const dep of manifest.globalDeps) {
193
+ try {
194
+ const includeDir = await fetchDepIncludeDir(
195
+ dep, resolveGithubToken(manifest, dep.repo),
196
+ params?.noFetch === true, manifest.github.ssh
197
+ );
198
+ const files = await collectIncFiles(includeDir);
199
+ deps.push({
200
+ repo: dep.repo,
201
+ ref: dep.ref,
202
+ include_path: dep.include_path || null,
203
+ include_dir: includeDir,
204
+ count: files.length,
205
+ files: files.map((f) => ({ rel: f.rel, abs: f.abs })),
206
+ });
207
+ } catch (err) {
208
+ deps.push({ repo: dep.repo, ref: dep.ref, error: err.message, files: [], count: 0 });
209
+ }
210
+ }
211
+ return { manifest: manifestPath, deps };
212
+ });
213
+
214
+ // ─── AMXX standard includes ──────────────────────────────────────────────
215
+
216
+ server.onRequest('amxmodx.includes.list', async (params) => {
217
+ const version = await resolveVersionFromParams(params);
218
+ const pattern = params?.pattern || '*.inc';
219
+
220
+ const { includeDir } = await fetchCompiler(version);
221
+ if (!includeDir) return { version, includeDir: null, pattern, count: 0, files: [] };
222
+
223
+ const files = await glob(pattern, { cwd: includeDir, dot: false });
224
+ files.sort();
225
+ return { version, includeDir, pattern, count: files.length, files };
226
+ });
227
+
228
+ server.onRequest('amxmodx.include.get', async (params) => {
229
+ const version = await resolveVersionFromParams(params);
230
+ const pattern = params?.file || params?.pattern || '*.inc';
231
+
232
+ const { includeDir } = await fetchCompiler(version);
233
+ if (!includeDir) return { version, includeDir: null, count: 0, files: [] };
234
+
235
+ const files = await glob(pattern, { cwd: includeDir, dot: false });
236
+ files.sort();
237
+ return {
238
+ version,
239
+ includeDir,
240
+ count: files.length,
241
+ files: files.map((rel) => ({ rel, content: readFileSafe(path.join(includeDir, rel)) })),
242
+ };
243
+ });
244
+
245
+ // ─── Deps tree ───────────────────────────────────────────────────────────
246
+
247
+ server.onRequest('deps.tree', async (params) => {
248
+ const depth = params?.depth || 0;
249
+ const noFetch = params?.noFetch === true;
250
+
251
+ if (params?.deps) {
252
+ const rootDeps = params.deps.map((entry) => {
253
+ if (typeof entry === 'string') {
254
+ const parsed = parseDepString(entry);
255
+ return { repo: parsed.repo, ref: parsed.ref, source: parsed.source, include_path: parsed.include_path, asset: parsed.asset };
256
+ }
257
+ return {
258
+ repo: entry.repo,
259
+ ref: entry.ref,
260
+ source: entry.source || 'git',
261
+ include_path: entry.include_path || null,
262
+ asset: entry.asset != null ? entry.asset : null,
263
+ };
264
+ });
265
+ return buildDepTree(rootDeps, { token: params?.token, noFetch, depth, from: 'user' });
266
+ }
267
+
268
+ const manifestPath = manifestPathFor(params);
269
+ loadEnv(manifestPath);
270
+ const manifest = parseManifest(manifestPath);
271
+ const assembled = assembleRootDeps(manifest);
272
+ return buildDepTree(assembled.rootDeps, {
273
+ token: params?.token,
274
+ tokenFor: (repo) => resolveGithubToken(manifest, repo),
275
+ noFetch,
276
+ depth,
277
+ from: 'manifest',
278
+ getDepsOverride: assembled.getDepsOverride,
279
+ });
280
+ });
281
+
282
+ // ─── Releases / cache / plan ─────────────────────────────────────────────
283
+
284
+ server.onRequest('releases.list', async (params) => {
285
+ if (!params?.repo) {
286
+ const err = new Error('Missing required "repo" field');
287
+ err.code = -32602;
288
+ throw err;
289
+ }
290
+ const token = params?.token || process.env.GITHUB_TOKEN || null;
291
+ const limit = params?.limit || 10;
292
+ if (params?.tags) return listTags(params.repo, { token, limit });
293
+ return listReleases(params.repo, { token, limit, includeAssets: params?.includeAssets });
294
+ });
295
+
296
+ server.onRequest('cache.info', (params) => {
297
+ const manifestPath = params?.manifest ? path.resolve(params.manifest) : undefined;
298
+ return getCacheInfo(manifestPath);
299
+ });
300
+
301
+ server.onRequest('build.plan', (params) => {
302
+ const manifestPath = manifestPathFor(params);
303
+ loadEnv(manifestPath, { quiet: true, override: false });
304
+ const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
305
+ return buildPlanData(manifest, {
306
+ detailedAssets: params?.detailedAssets === true,
307
+ listLocal: params?.listLocal !== false,
308
+ });
309
+ });
310
+
311
+ // ─── Build ───────────────────────────────────────────────────────────────
312
+
313
+ server.onRequest('build.start', async (params) => {
314
+ if (activeBuild) {
315
+ const err = new Error('Build already running');
316
+ err.code = -32000;
317
+ throw err;
318
+ }
319
+
320
+ const controller = new AbortController();
321
+ activeBuild = controller;
322
+
323
+ const manifestPath = manifestPathFor(params);
324
+ loadEnv(manifestPath);
325
+ const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
326
+
327
+ // Forward core lifecycle events as server→client notifications while the
328
+ // build runs. COMPILED/PROGRESS are emitted by compiler.js/progress.js on
329
+ // the bus; STAGE/DONE/ERROR by build-service.
330
+ const listeners = [
331
+ [EVENTS.STAGE, (p) => server.notify('build.stage', p)],
332
+ [EVENTS.COMPILED, (p) => server.notify('build.compiled', p)],
333
+ [EVENTS.PROGRESS, (p) => server.notify('build.progress', p)],
334
+ [EVENTS.DONE, (p) => server.notify('build.done', p)],
335
+ [EVENTS.ERROR, (p) => server.notify('build.error', p)],
336
+ ];
337
+ for (const [ev, fn] of listeners) on(ev, fn);
338
+
339
+ try {
340
+ const result = await runBuild(manifest, {
341
+ buildDir: params?.buildDir,
342
+ fetch: params?.fetch,
343
+ archive: params?.archive,
344
+ signal: controller.signal,
345
+ });
346
+ return result;
347
+ } catch (err) {
348
+ if (err.code === 'CANCELLED') {
349
+ return { ok: false, cancelled: true, message: err.message };
350
+ }
351
+ return { ok: false, message: err.message };
352
+ } finally {
353
+ for (const [ev, fn] of listeners) off(ev, fn);
354
+ activeBuild = null;
355
+ }
356
+ });
357
+
358
+ server.onRequest('build.cancel', () => {
359
+ if (!activeBuild) return { ok: false, error: 'No build running' };
360
+ activeBuild.abort();
361
+ return { ok: true };
362
+ });
363
+
364
+ // ─── Single-file compile ─────────────────────────────────────────────────
365
+
366
+ server.onRequest('compile.single', async (params) => {
367
+ if (!params?.sma_file) {
368
+ const err = new Error('Missing required "sma_file" parameter');
369
+ err.code = -32602;
370
+ throw err;
371
+ }
372
+ const smaPath = path.resolve(params.sma_file);
373
+ if (!fs.existsSync(smaPath)) {
374
+ const err = new Error(`File not found: ${smaPath}`);
375
+ err.code = -32602;
376
+ throw err;
377
+ }
378
+
379
+ const noFetch = params?.noFetch === true;
380
+
381
+ const manifestPath = manifestPathFor(params);
382
+ let manifest = null;
383
+ if (fs.existsSync(manifestPath)) {
384
+ try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
385
+ }
386
+
387
+ const version = await resolveVersionFromParams(params);
388
+ const { compilerPath, includeDir } = await fetchCompiler(version);
389
+
390
+ // Dep includes come BEFORE the stdlib — matching the real build order.
391
+ const depDirs = [];
392
+ const depErrors = [];
393
+ if (manifest) {
394
+ for (const dep of manifest.globalDeps) {
395
+ try {
396
+ depDirs.push(await fetchDepIncludeDir(
397
+ dep, resolveGithubToken(manifest, dep.repo), noFetch, manifest.github.ssh
398
+ ));
399
+ } catch (err) {
400
+ depErrors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
401
+ }
402
+ }
403
+ }
404
+
405
+ const includeDirs = [...depDirs];
406
+ if (includeDir) includeDirs.push(includeDir);
407
+ for (const d of (params?.include_dirs || [])) includeDirs.push(path.resolve(d));
408
+
409
+ const buildDir = path.join(os.tmpdir(), 'amxb-serve-compile');
410
+ const compileManifest = manifest || { amxmodx: { defines: [] } };
411
+ const amxxName = await compileSingle(
412
+ compileManifest,
413
+ smaPath,
414
+ compilerPath,
415
+ includeDirs,
416
+ buildDir,
417
+ params?.scripting_root ? path.resolve(params.scripting_root) : undefined
418
+ );
419
+
420
+ return {
421
+ ok: amxxName != null,
422
+ amxxName,
423
+ output_path: amxxName ? path.join(buildDir, 'amxmodx', 'plugins', amxxName) : null,
424
+ dep_errors: depErrors.length ? depErrors : undefined,
425
+ };
426
+ });
427
+
428
+ // ─── Watch ───────────────────────────────────────────────────────────────
429
+
430
+ server.onRequest('watch.start', (params) => {
431
+ if (activeWatcher) {
432
+ const err = new Error('Watch already running');
433
+ err.code = -32000;
434
+ throw err;
435
+ }
436
+
437
+ const manifestPath = manifestPathFor(params);
438
+ const manifest = parseManifest(manifestPath);
439
+
440
+ const notify = (kind, extra = {}) => server.notify('watch.changed', { kind, ...extra });
441
+ const watcher = startWatch(manifest, manifestPath, {
442
+ onSmaChange: (p) => notify('sma', { path: p }),
443
+ onIncChange: (p) => notify('inc', { path: p }),
444
+ onFileChange: (rel, section) => notify('file', { rel, section }),
445
+ onFileDelete: (rel, section) => notify('delete', { rel, section }),
446
+ onManifestChange: () => notify('manifest'),
447
+ });
448
+
449
+ activeWatcher = watcher;
450
+ return { ok: true, watching: manifestPath };
451
+ });
452
+
453
+ server.onRequest('watch.stop', async () => {
454
+ if (!activeWatcher) return { ok: false, error: 'No watcher running' };
455
+ await activeWatcher.close();
456
+ activeWatcher = null;
457
+ return { ok: true };
458
+ });
459
+
460
+ return server;
461
+ }
462
+
463
+ // Load project .env from the workspace root like the CLI does; keep stdout free
464
+ // for JSON-RPC (logs → stderr, progress bars disabled).
465
+ function prepareEnvironment() {
466
+ dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });
467
+ logger.setStderr(true);
468
+ progress.setEnabled(false);
469
+ }
470
+
471
+ /**
472
+ * Start the serve server — listens on stdin/stdout forever.
473
+ */
474
+ async function runServe() {
475
+ prepareEnvironment();
476
+ const server = createServeServer();
477
+ await server.connect();
478
+ }
479
+
480
+ module.exports = { runServe, createServeServer };
481
+
482
+ // ─── Direct execution guard ───────────────────────────────────────────────────
483
+
484
+ if (require.main === module) {
485
+ runServe().catch((err) => {
486
+ process.stderr.write(`Fatal serve error: ${err && err.message ? err.message : String(err)}\n`);
487
+ process.exit(1);
488
+ });
489
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { loadEnv } = require('../env');
6
+ const { resolveManifestPath: resolveManifestPathCore } = require('../manifest-path');
7
+
8
+ /**
9
+ * Resolve manifest file path from explicit arg or auto-detection.
10
+ *
11
+ * CLI wrapper around the core src/manifest-path.js: returns a plain string
12
+ * (absolute path) and renders the CLI-only 'manifest.yml is deprecated'
13
+ * warning. All discovery logic lives in the core.
14
+ */
15
+ function resolveManifestPath(explicit) {
16
+ const { path: manifestPath, usedDefault } = resolveManifestPathCore(explicit);
17
+ if (usedDefault && path.basename(manifestPath) === 'manifest.yml') {
18
+ const logger = require('../logger');
19
+ logger.warn('manifest.yml is deprecated — rename it to amxbuild.yml');
20
+ }
21
+ return manifestPath;
22
+ }
23
+
24
+ module.exports = { resolveManifestPath, loadEnv };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const logger = require('../logger');
6
+ const { validateManifestFile } = require('../validate');
7
+ const { resolveManifestPath } = require('./shared');
8
+
9
+ async function runValidate(options) {
10
+ const manifestPath = options.manifest ? path.resolve(options.manifest) : resolveManifestPath(undefined);
11
+ const result = validateManifestFile(manifestPath);
12
+
13
+ if (options.json) {
14
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
15
+ if (!result.valid) process.exitCode = 1;
16
+ return;
17
+ }
18
+
19
+ if (result.valid) {
20
+ logger.success('Manifest is valid');
21
+ return;
22
+ }
23
+
24
+ logger.error(`Manifest has ${result.errors.length} error(s) and ${result.warnings.length} warning(s):`);
25
+ for (const err of result.errors) {
26
+ logger.dim(` ${err.path}: ${err.message}`);
27
+ }
28
+ for (const warn of result.warnings) {
29
+ logger.warn(` ${warn.path}: ${warn.message}`);
30
+ }
31
+ process.exitCode = 1;
32
+ }
33
+
34
+ module.exports = { runValidate };