@btc-vision/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 (110) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/dependabot.yml +9 -0
  3. package/.github/workflows/ci.yml +48 -0
  4. package/.prettierrc.json +12 -0
  5. package/CONTRIBUTING.md +56 -0
  6. package/LICENSE +190 -0
  7. package/NOTICE +17 -0
  8. package/README.md +509 -0
  9. package/SECURITY.md +35 -0
  10. package/build/commands/AcceptCommand.d.ts +7 -0
  11. package/build/commands/AcceptCommand.js +110 -0
  12. package/build/commands/BaseCommand.d.ts +12 -0
  13. package/build/commands/BaseCommand.js +27 -0
  14. package/build/commands/CompileCommand.d.ts +7 -0
  15. package/build/commands/CompileCommand.js +138 -0
  16. package/build/commands/ConfigCommand.d.ts +17 -0
  17. package/build/commands/ConfigCommand.js +124 -0
  18. package/build/commands/DeprecateCommand.d.ts +7 -0
  19. package/build/commands/DeprecateCommand.js +112 -0
  20. package/build/commands/InfoCommand.d.ts +10 -0
  21. package/build/commands/InfoCommand.js +223 -0
  22. package/build/commands/InitCommand.d.ts +16 -0
  23. package/build/commands/InitCommand.js +336 -0
  24. package/build/commands/InstallCommand.d.ts +7 -0
  25. package/build/commands/InstallCommand.js +130 -0
  26. package/build/commands/KeygenCommand.d.ts +13 -0
  27. package/build/commands/KeygenCommand.js +133 -0
  28. package/build/commands/ListCommand.d.ts +7 -0
  29. package/build/commands/ListCommand.js +117 -0
  30. package/build/commands/LoginCommand.d.ts +9 -0
  31. package/build/commands/LoginCommand.js +139 -0
  32. package/build/commands/LogoutCommand.d.ts +7 -0
  33. package/build/commands/LogoutCommand.js +57 -0
  34. package/build/commands/PublishCommand.d.ts +7 -0
  35. package/build/commands/PublishCommand.js +163 -0
  36. package/build/commands/SearchCommand.d.ts +7 -0
  37. package/build/commands/SearchCommand.js +97 -0
  38. package/build/commands/SignCommand.d.ts +7 -0
  39. package/build/commands/SignCommand.js +80 -0
  40. package/build/commands/TransferCommand.d.ts +8 -0
  41. package/build/commands/TransferCommand.js +179 -0
  42. package/build/commands/UndeprecateCommand.d.ts +7 -0
  43. package/build/commands/UndeprecateCommand.js +95 -0
  44. package/build/commands/UpdateCommand.d.ts +7 -0
  45. package/build/commands/UpdateCommand.js +130 -0
  46. package/build/commands/VerifyCommand.d.ts +7 -0
  47. package/build/commands/VerifyCommand.js +167 -0
  48. package/build/commands/WhoamiCommand.d.ts +7 -0
  49. package/build/commands/WhoamiCommand.js +84 -0
  50. package/build/index.d.ts +2 -0
  51. package/build/index.js +64 -0
  52. package/build/lib/PackageRegistry.abi.d.ts +2 -0
  53. package/build/lib/PackageRegistry.abi.js +356 -0
  54. package/build/lib/binary.d.ts +16 -0
  55. package/build/lib/binary.js +165 -0
  56. package/build/lib/config.d.ts +11 -0
  57. package/build/lib/config.js +160 -0
  58. package/build/lib/credentials.d.ts +10 -0
  59. package/build/lib/credentials.js +89 -0
  60. package/build/lib/ipfs.d.ts +16 -0
  61. package/build/lib/ipfs.js +209 -0
  62. package/build/lib/manifest.d.ts +14 -0
  63. package/build/lib/manifest.js +88 -0
  64. package/build/lib/provider.d.ts +9 -0
  65. package/build/lib/provider.js +48 -0
  66. package/build/lib/registry.d.ts +58 -0
  67. package/build/lib/registry.js +197 -0
  68. package/build/lib/wallet.d.ts +32 -0
  69. package/build/lib/wallet.js +114 -0
  70. package/build/types/PackageRegistry.d.ts +177 -0
  71. package/build/types/PackageRegistry.js +1 -0
  72. package/build/types/index.d.ts +30 -0
  73. package/build/types/index.js +52 -0
  74. package/eslint.config.js +41 -0
  75. package/gulpfile.js +41 -0
  76. package/package.json +83 -0
  77. package/src/commands/AcceptCommand.ts +151 -0
  78. package/src/commands/BaseCommand.ts +59 -0
  79. package/src/commands/CompileCommand.ts +196 -0
  80. package/src/commands/ConfigCommand.ts +144 -0
  81. package/src/commands/DeprecateCommand.ts +156 -0
  82. package/src/commands/InfoCommand.ts +293 -0
  83. package/src/commands/InitCommand.ts +465 -0
  84. package/src/commands/InstallCommand.ts +179 -0
  85. package/src/commands/KeygenCommand.ts +157 -0
  86. package/src/commands/ListCommand.ts +169 -0
  87. package/src/commands/LoginCommand.ts +197 -0
  88. package/src/commands/LogoutCommand.ts +76 -0
  89. package/src/commands/PublishCommand.ts +230 -0
  90. package/src/commands/SearchCommand.ts +141 -0
  91. package/src/commands/SignCommand.ts +122 -0
  92. package/src/commands/TransferCommand.ts +235 -0
  93. package/src/commands/UndeprecateCommand.ts +134 -0
  94. package/src/commands/UpdateCommand.ts +200 -0
  95. package/src/commands/VerifyCommand.ts +228 -0
  96. package/src/commands/WhoamiCommand.ts +113 -0
  97. package/src/index.ts +86 -0
  98. package/src/lib/PackageRegistry.abi.json +765 -0
  99. package/src/lib/PackageRegistry.abi.ts +365 -0
  100. package/src/lib/binary.ts +336 -0
  101. package/src/lib/config.ts +265 -0
  102. package/src/lib/credentials.ts +176 -0
  103. package/src/lib/ipfs.ts +369 -0
  104. package/src/lib/manifest.ts +172 -0
  105. package/src/lib/provider.ts +121 -0
  106. package/src/lib/registry.ts +464 -0
  107. package/src/lib/wallet.ts +271 -0
  108. package/src/types/PackageRegistry.ts +344 -0
  109. package/src/types/index.ts +145 -0
  110. package/tsconfig.json +25 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Init command - Initialize a new OPNet plugin project
3
+ *
4
+ * @module commands/InitCommand
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { confirm, input, select } from '@inquirer/prompts';
10
+ import { BaseCommand } from './BaseCommand.js';
11
+ import { validatePluginName } from '../lib/manifest.js';
12
+
13
+ interface InitOptions {
14
+ template: string;
15
+ yes?: boolean;
16
+ force?: boolean;
17
+ }
18
+
19
+ export class InitCommand extends BaseCommand {
20
+ constructor() {
21
+ super('init', 'Initialize a new OPNet plugin project');
22
+ }
23
+
24
+ protected configure(): void {
25
+ this.command
26
+ .argument('[name]', 'Plugin name')
27
+ .option('-t, --template <type>', 'Template type (standalone, library)', 'standalone')
28
+ .option('-y, --yes', 'Skip prompts and use defaults')
29
+ .option('--force', 'Overwrite existing files')
30
+ .action((name?: string, options?: InitOptions) => this.execute(name, options));
31
+ }
32
+
33
+ private async execute(name?: string, options?: InitOptions): Promise<void> {
34
+ try {
35
+ const config = await this.gatherConfig(name, options);
36
+ await this.createProject(config, options?.force);
37
+
38
+ this.logger.success('Plugin initialized successfully!');
39
+ this.logger.log('');
40
+ this.logger.log('Next steps:');
41
+ this.logger.log(' 1. npm install');
42
+ this.logger.log(' 2. Edit src/index.ts');
43
+ this.logger.log(' 3. npm run build');
44
+ this.logger.log(' 4. opnet compile');
45
+ this.logger.log('');
46
+ } catch (error) {
47
+ if (this.isUserCancelled(error)) {
48
+ this.logger.warn('Initialization cancelled.');
49
+ process.exit(0);
50
+ }
51
+ this.exitWithError(this.formatError(error));
52
+ }
53
+ }
54
+
55
+ private async gatherConfig(
56
+ name?: string,
57
+ options?: InitOptions,
58
+ ): Promise<{
59
+ pluginName: string;
60
+ authorName: string;
61
+ authorEmail?: string;
62
+ description?: string;
63
+ pluginType: 'standalone' | 'library';
64
+ }> {
65
+ if (options?.yes && name) {
66
+ return {
67
+ pluginName: name,
68
+ authorName: 'Author',
69
+ pluginType: (options.template as 'standalone' | 'library') || 'standalone',
70
+ };
71
+ }
72
+
73
+ this.logger.info('\nOPNet Plugin Initialization\n');
74
+
75
+ const pluginName =
76
+ name ||
77
+ (await input({
78
+ message: 'Plugin name:',
79
+ default: path.basename(process.cwd()),
80
+ validate: (value) => {
81
+ const errors = validatePluginName(value);
82
+ return errors.length > 0 ? errors[0] : true;
83
+ },
84
+ }));
85
+
86
+ const description = (await input({ message: 'Description:', default: '' })) || undefined;
87
+ const authorName = await input({
88
+ message: 'Author name:',
89
+ default: process.env.USER || 'Author',
90
+ });
91
+ const authorEmail =
92
+ (await input({ message: 'Author email (optional):', default: '' })) || undefined;
93
+
94
+ const pluginType = await select({
95
+ message: 'Plugin type:',
96
+ choices: [
97
+ {
98
+ name: 'Standalone',
99
+ value: 'standalone' as const,
100
+ description: 'Independent plugin',
101
+ },
102
+ { name: 'Library', value: 'library' as const, description: 'Shared library' },
103
+ ],
104
+ default: options?.template || 'standalone',
105
+ });
106
+
107
+ return { pluginName, authorName, authorEmail, description, pluginType };
108
+ }
109
+
110
+ private async createProject(
111
+ config: {
112
+ pluginName: string;
113
+ authorName: string;
114
+ authorEmail?: string;
115
+ description?: string;
116
+ pluginType: 'standalone' | 'library';
117
+ },
118
+ force?: boolean,
119
+ ): Promise<void> {
120
+ const nameErrors = validatePluginName(config.pluginName);
121
+ if (nameErrors.length > 0) {
122
+ this.exitWithError(`Invalid plugin name: ${nameErrors.join(', ')}`);
123
+ }
124
+
125
+ const projectDir = process.cwd();
126
+ const pluginJsonPath = path.join(projectDir, 'plugin.json');
127
+
128
+ if (fs.existsSync(pluginJsonPath) && !force) {
129
+ const overwrite = await confirm({
130
+ message: 'plugin.json exists. Overwrite?',
131
+ default: false,
132
+ });
133
+ if (!overwrite) {
134
+ this.logger.warn('Initialization cancelled.');
135
+ return;
136
+ }
137
+ }
138
+
139
+ this.logger.info('Creating project structure...');
140
+
141
+ // Create directories
142
+ for (const dir of ['src', 'dist', 'build', 'test']) {
143
+ const dirPath = path.join(projectDir, dir);
144
+ if (!fs.existsSync(dirPath)) {
145
+ fs.mkdirSync(dirPath, { recursive: true });
146
+ }
147
+ }
148
+
149
+ // Create plugin.json
150
+ this.createPluginJson(projectDir, config);
151
+ this.logger.success(' Created plugin.json');
152
+
153
+ // Create package.json
154
+ this.createPackageJson(projectDir, config, force);
155
+
156
+ // Create tsconfig.json
157
+ this.createTsConfig(projectDir, force);
158
+
159
+ // Create src/index.ts
160
+ this.createEntryPoint(projectDir, config, force);
161
+
162
+ // Create .gitignore
163
+ this.createGitignore(projectDir, force);
164
+
165
+ // Create README.md
166
+ this.createReadme(projectDir, config, force);
167
+ }
168
+
169
+ private createPluginJson(
170
+ projectDir: string,
171
+ config: {
172
+ pluginName: string;
173
+ authorName: string;
174
+ authorEmail?: string;
175
+ description?: string;
176
+ pluginType: 'standalone' | 'library';
177
+ },
178
+ ): void {
179
+ const manifest: Record<string, unknown> = {
180
+ name: config.pluginName,
181
+ version: '1.0.0',
182
+ opnetVersion: '^1.0.0',
183
+ main: 'dist/index.jsc',
184
+ target: 'bytenode',
185
+ type: 'plugin',
186
+ checksum: '',
187
+ author: config.authorEmail
188
+ ? { name: config.authorName, email: config.authorEmail }
189
+ : { name: config.authorName },
190
+ pluginType: config.pluginType,
191
+ permissions: {
192
+ database: {
193
+ enabled: false,
194
+ collections: [],
195
+ },
196
+ blocks: {
197
+ preProcess: false,
198
+ postProcess: false,
199
+ onChange: false,
200
+ },
201
+ epochs: {
202
+ onChange: false,
203
+ onFinalized: false,
204
+ },
205
+ mempool: {
206
+ txFeed: false,
207
+ txSubmit: false,
208
+ },
209
+ api: {
210
+ addEndpoints: false,
211
+ addWebsocket: false,
212
+ },
213
+ threading: {
214
+ maxWorkers: 1,
215
+ maxMemoryMB: 256,
216
+ },
217
+ filesystem: {
218
+ configDir: false,
219
+ tempDir: false,
220
+ },
221
+ blockchain: {
222
+ blocks: false,
223
+ transactions: false,
224
+ contracts: false,
225
+ utxos: false,
226
+ },
227
+ },
228
+ resources: {
229
+ memory: {
230
+ maxHeapMB: 256,
231
+ maxOldGenMB: 128,
232
+ maxYoungGenMB: 64,
233
+ },
234
+ cpu: {
235
+ maxThreads: 2,
236
+ priority: 'normal',
237
+ },
238
+ timeout: {
239
+ initMs: 30000,
240
+ hookMs: 5000,
241
+ shutdownMs: 10000,
242
+ },
243
+ },
244
+ lifecycle: {
245
+ loadPriority: 100,
246
+ enabledByDefault: true,
247
+ requiresRestart: false,
248
+ },
249
+ dependencies: {},
250
+ };
251
+
252
+ if (config.description) {
253
+ manifest.description = config.description;
254
+ }
255
+
256
+ fs.writeFileSync(path.join(projectDir, 'plugin.json'), JSON.stringify(manifest, null, 4));
257
+ }
258
+
259
+ private createPackageJson(
260
+ projectDir: string,
261
+ config: {
262
+ pluginName: string;
263
+ authorName: string;
264
+ authorEmail?: string;
265
+ description?: string;
266
+ },
267
+ force?: boolean,
268
+ ): void {
269
+ const packageJsonPath = path.join(projectDir, 'package.json');
270
+ if (fs.existsSync(packageJsonPath) && !force) return;
271
+
272
+ const packageJson = {
273
+ name: config.pluginName,
274
+ version: '1.0.0',
275
+ description: config.description || 'OPNet plugin',
276
+ type: 'module',
277
+ main: 'dist/index.js',
278
+ scripts: {
279
+ build: 'tsc',
280
+ compile: 'opnet compile',
281
+ verify: 'opnet verify',
282
+ lint: 'eslint src/',
283
+ },
284
+ author: config.authorEmail
285
+ ? `${config.authorName} <${config.authorEmail}>`
286
+ : config.authorName,
287
+ license: 'Apache-2.0',
288
+ dependencies: { '@btc-vision/plugin-sdk': '^1.0.0' },
289
+ devDependencies: {
290
+ '@types/node': '^22.0.0',
291
+ typescript: '^5.8.0',
292
+ '@btc-vision/cli': '^1.0.0',
293
+ },
294
+ };
295
+
296
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4));
297
+ this.logger.success(' Created package.json');
298
+ }
299
+
300
+ private createTsConfig(projectDir: string, force?: boolean): void {
301
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
302
+ if (fs.existsSync(tsconfigPath) && !force) return;
303
+
304
+ const tsconfig = {
305
+ compilerOptions: {
306
+ target: 'ES2022',
307
+ module: 'NodeNext',
308
+ moduleResolution: 'NodeNext',
309
+ lib: ['ES2022'],
310
+ outDir: './dist',
311
+ rootDir: './src',
312
+ strict: true,
313
+ esModuleInterop: true,
314
+ skipLibCheck: true,
315
+ forceConsistentCasingInFileNames: true,
316
+ declaration: true,
317
+ sourceMap: true,
318
+ },
319
+ include: ['src/**/*'],
320
+ exclude: ['node_modules', 'dist', 'build'],
321
+ };
322
+
323
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 4));
324
+ this.logger.success(' Created tsconfig.json');
325
+ }
326
+
327
+ private createEntryPoint(
328
+ projectDir: string,
329
+ config: { pluginName: string; pluginType: 'standalone' | 'library' },
330
+ force?: boolean,
331
+ ): void {
332
+ const indexPath = path.join(projectDir, 'src', 'index.ts');
333
+ if (fs.existsSync(indexPath) && !force) return;
334
+
335
+ const className = this.toPascalCase(config.pluginName);
336
+ const content =
337
+ config.pluginType === 'standalone'
338
+ ? `import { PluginBase, IPluginContext } from '@btc-vision/plugin-sdk';
339
+
340
+ /**
341
+ * ${className} Plugin
342
+ *
343
+ * Extend PluginBase and override only the hooks you need.
344
+ * See the plugin-sdk documentation for available hooks.
345
+ */
346
+ export default class ${className}Plugin extends PluginBase {
347
+ /**
348
+ * Called when the plugin is loaded.
349
+ * Always call super.onLoad(context) first to initialize this.context.
350
+ */
351
+ public async onLoad(context: IPluginContext): Promise<void> {
352
+ await super.onLoad(context);
353
+ this.context.logger.info('${className} plugin loaded');
354
+ }
355
+
356
+ /**
357
+ * Called when the plugin is being unloaded.
358
+ * Clean up any resources here.
359
+ */
360
+ public async onUnload(): Promise<void> {
361
+ this.context.logger.info('${className} plugin unloading');
362
+ }
363
+
364
+ /**
365
+ * Called when the plugin is enabled.
366
+ */
367
+ public async onEnable(): Promise<void> {
368
+ this.context.logger.info('${className} plugin enabled');
369
+ }
370
+
371
+ /**
372
+ * Called when the plugin is disabled.
373
+ */
374
+ public async onDisable(): Promise<void> {
375
+ this.context.logger.info('${className} plugin disabled');
376
+ }
377
+ }
378
+ `
379
+ : `export * from './lib/index.js';
380
+ `;
381
+
382
+ fs.writeFileSync(indexPath, content);
383
+ this.logger.success(' Created src/index.ts');
384
+
385
+ if (config.pluginType === 'library') {
386
+ const libDir = path.join(projectDir, 'src', 'lib');
387
+ fs.mkdirSync(libDir, { recursive: true });
388
+ fs.writeFileSync(
389
+ path.join(libDir, 'index.ts'),
390
+ `export function hello(): string {
391
+ return 'Hello from ${config.pluginName}!';
392
+ }
393
+ `,
394
+ );
395
+ this.logger.success(' Created src/lib/index.ts');
396
+ }
397
+ }
398
+
399
+ private createGitignore(projectDir: string, force?: boolean): void {
400
+ const gitignorePath = path.join(projectDir, '.gitignore');
401
+ if (fs.existsSync(gitignorePath) && !force) return;
402
+
403
+ fs.writeFileSync(
404
+ gitignorePath,
405
+ `node_modules/
406
+ dist/
407
+ build/
408
+ *.jsc
409
+ *.opnet
410
+ .idea/
411
+ .vscode/
412
+ .DS_Store
413
+ .env
414
+ *.log
415
+ coverage/
416
+ `,
417
+ );
418
+ this.logger.success(' Created .gitignore');
419
+ }
420
+
421
+ private createReadme(
422
+ projectDir: string,
423
+ config: { pluginName: string; description?: string; pluginType: string },
424
+ force?: boolean,
425
+ ): void {
426
+ const readmePath = path.join(projectDir, 'README.md');
427
+ if (fs.existsSync(readmePath) && !force) return;
428
+
429
+ fs.writeFileSync(
430
+ readmePath,
431
+ `# ${config.pluginName}
432
+
433
+ ${config.description || `An OPNet ${config.pluginType} plugin.`}
434
+
435
+ ## Installation
436
+
437
+ \`\`\`bash
438
+ npm install
439
+ \`\`\`
440
+
441
+ ## Development
442
+
443
+ \`\`\`bash
444
+ npm run build # Build TypeScript
445
+ npm run compile # Compile to .opnet
446
+ npm run verify # Verify binary
447
+ \`\`\`
448
+
449
+ ## License
450
+
451
+ Apache-2.0
452
+ `,
453
+ );
454
+ this.logger.success(' Created README.md');
455
+ }
456
+
457
+ private toPascalCase(str: string): string {
458
+ return str
459
+ .split(/[-_]/)
460
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
461
+ .join('');
462
+ }
463
+ }
464
+
465
+ export const initCommand = new InitCommand().getCommand();
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Install command - Download and verify plugins from registry
3
+ *
4
+ * @module commands/InstallCommand
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { BaseCommand } from './BaseCommand.js';
10
+ import { getPackage, getVersion, registryToMldsaLevel } from '../lib/registry.js';
11
+ import { fetchFromIPFS, isValidCid } from '../lib/ipfs.js';
12
+ import { formatFileSize, parseOpnetBinary, verifyChecksum } from '../lib/binary.js';
13
+ import { CLIWallet } from '../lib/wallet.js';
14
+ import { NetworkName } from '../types/index.js';
15
+
16
+ interface InstallOptions {
17
+ output?: string;
18
+ network: string;
19
+ skipVerify?: boolean;
20
+ }
21
+
22
+ export class InstallCommand extends BaseCommand {
23
+ constructor() {
24
+ super('install', 'Download and verify a plugin from the registry');
25
+ }
26
+
27
+ protected configure(): void {
28
+ this.command
29
+ .argument('<package>', 'Package name[@version] or IPFS CID')
30
+ .option('-o, --output <path>', 'Output directory (default: ./plugins/)')
31
+ .option('-n, --network <network>', 'Network', 'mainnet')
32
+ .option('--skip-verify', 'Skip signature verification')
33
+ .action((packageInput: string, options?: InstallOptions) =>
34
+ this.execute(packageInput, options || { network: 'mainnet' }),
35
+ );
36
+ }
37
+
38
+ private async execute(packageInput: string, options?: InstallOptions): Promise<void> {
39
+ try {
40
+ let cid: string;
41
+ let packageName: string;
42
+ let version: string;
43
+
44
+ // Check if input is a CID
45
+ if (isValidCid(packageInput)) {
46
+ cid = packageInput;
47
+ packageName = 'unknown';
48
+ version = 'unknown';
49
+ this.logger.info(`Installing from CID: ${cid}`);
50
+ } else {
51
+ // Parse package name and version
52
+ const atIndex = packageInput.lastIndexOf('@');
53
+ if (atIndex > 0 && !packageInput.startsWith('@')) {
54
+ packageName = packageInput.substring(0, atIndex);
55
+ version = packageInput.substring(atIndex + 1);
56
+ } else if (packageInput.startsWith('@') && packageInput.indexOf('@', 1) > 0) {
57
+ const secondAt = packageInput.indexOf('@', 1);
58
+ packageName = packageInput.substring(0, secondAt);
59
+ version = packageInput.substring(secondAt + 1);
60
+ } else {
61
+ packageName = packageInput;
62
+ version = 'latest';
63
+ }
64
+
65
+ // Fetch from registry
66
+ this.logger.info(`Fetching ${packageName}...`);
67
+ const network = (options?.network || 'mainnet') as NetworkName;
68
+
69
+ const packageInfo = await getPackage(packageName, network);
70
+ if (!packageInfo) {
71
+ this.logger.fail('Package not found');
72
+ this.logger.error(`Package "${packageName}" does not exist.`);
73
+ process.exit(1);
74
+ }
75
+
76
+ // Resolve version
77
+ if (version === 'latest') {
78
+ version = packageInfo.latestVersion;
79
+ }
80
+
81
+ const versionInfo = await getVersion(packageName, version, network);
82
+ if (!versionInfo) {
83
+ this.logger.fail('Version not found');
84
+ this.logger.error(`Version "${version}" does not exist.`);
85
+ process.exit(1);
86
+ }
87
+
88
+ if (versionInfo.deprecated) {
89
+ this.logger.warn(`Version ${version} is deprecated`);
90
+ }
91
+
92
+ cid = versionInfo.ipfsCid;
93
+ this.logger.success(`Found: ${packageName}@${version}`);
94
+
95
+ // Display info
96
+ this.logger.log('');
97
+ this.logger.log(`IPFS CID: ${cid}`);
98
+ this.logger.log(`MLDSA Level: ${registryToMldsaLevel(versionInfo.mldsaLevel)}`);
99
+ this.logger.log(`Publisher: ${versionInfo.publisher}`);
100
+ this.logger.log('');
101
+ }
102
+
103
+ // Download from IPFS
104
+ this.logger.info('Downloading from IPFS...');
105
+ const result = await fetchFromIPFS(cid);
106
+ this.logger.success(`Downloaded (${formatFileSize(result.size)})`);
107
+
108
+ // Parse and verify
109
+ this.logger.info('Verifying binary...');
110
+ const parsed = parseOpnetBinary(result.data);
111
+
112
+ // Verify checksum
113
+ if (!verifyChecksum(parsed)) {
114
+ this.logger.fail('Checksum verification failed');
115
+ this.logger.error('The binary appears to be corrupted.');
116
+ process.exit(1);
117
+ }
118
+
119
+ // Verify signature
120
+ if (!options?.skipVerify) {
121
+ const isUnsigned = parsed.publicKey.every((b) => b === 0);
122
+ if (isUnsigned) {
123
+ this.logger.warn('Binary is unsigned');
124
+ } else {
125
+ const actualMldsaLevel = ([44, 65, 87] as const)[parsed.mldsaLevel];
126
+ const signatureValid = CLIWallet.verifyMLDSA(
127
+ parsed.checksum,
128
+ parsed.signature,
129
+ parsed.publicKey,
130
+ actualMldsaLevel,
131
+ );
132
+
133
+ if (!signatureValid) {
134
+ this.logger.fail('Signature verification failed');
135
+ this.logger.error('The binary signature is invalid.');
136
+ process.exit(1);
137
+ }
138
+ this.logger.success('Signature verified');
139
+ }
140
+ } else {
141
+ this.logger.warn('Skipping signature verification');
142
+ }
143
+
144
+ // Update package name from metadata
145
+ if (packageName === 'unknown') {
146
+ packageName = parsed.metadata.name;
147
+ version = parsed.metadata.version;
148
+ }
149
+
150
+ // Determine output path
151
+ const outputDir = options?.output || path.join(process.cwd(), 'plugins');
152
+ fs.mkdirSync(outputDir, { recursive: true });
153
+
154
+ const fileName = `${packageName.replace(/^@/, '').replace(/\//g, '-')}-${version}.opnet`;
155
+ const outputPath = path.join(outputDir, fileName);
156
+
157
+ // Save file
158
+ this.logger.info('Saving plugin...');
159
+ fs.writeFileSync(outputPath, result.data);
160
+ this.logger.success('Plugin installed');
161
+
162
+ // Summary
163
+ this.logger.log('');
164
+ this.logger.success('Plugin installed successfully!');
165
+ this.logger.log('');
166
+ this.logger.log(`Package: ${parsed.metadata.name}`);
167
+ this.logger.log(`Version: ${parsed.metadata.version}`);
168
+ this.logger.log(`Type: ${parsed.metadata.pluginType}`);
169
+ this.logger.log(`Size: ${formatFileSize(result.size)}`);
170
+ this.logger.log(`Output: ${outputPath}`);
171
+ this.logger.log('');
172
+ } catch (error) {
173
+ this.logger.fail('Installation failed');
174
+ this.exitWithError(this.formatError(error));
175
+ }
176
+ }
177
+ }
178
+
179
+ export const installCommand = new InstallCommand().getCommand();