@objectstack/cli 3.0.7 → 3.0.9

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.
@@ -0,0 +1,176 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { Args, Command, Flags } from '@oclif/core';
4
+ import chalk from 'chalk';
5
+ import path from 'path';
6
+ import fs from 'fs';
7
+ import crypto from 'crypto';
8
+ import { createTimer, printHeader, printKV, printStep, printSuccess, printError, printWarning, printInfo } from '../../utils/format.js';
9
+
10
+ /**
11
+ * Publish a plugin artifact to the ObjectStack marketplace.
12
+ *
13
+ * Validates the artifact locally (unless --skipValidation), computes the
14
+ * SHA-256 checksum, and uploads to the marketplace REST API.
15
+ *
16
+ * Architecture alignment: `npm publish`, `helm push`, `vsce publish`.
17
+ */
18
+ export default class PluginPublish extends Command {
19
+ static override description = 'Publish a plugin artifact to the marketplace';
20
+
21
+ static override args = {
22
+ artifact: Args.string({ description: 'Path to the artifact file', required: true }),
23
+ };
24
+
25
+ static override flags = {
26
+ registryUrl: Flags.string({ char: 'r', description: 'Marketplace API base URL', default: 'https://marketplace.objectstack.com/api/v1' }),
27
+ token: Flags.string({ char: 't', description: 'Authentication token', env: 'OBJECTSTACK_MARKETPLACE_TOKEN' }),
28
+ releaseNotes: Flags.string({ description: 'Release notes for this version' }),
29
+ preRelease: Flags.boolean({ description: 'Mark as a pre-release', default: false }),
30
+ skipValidation: Flags.boolean({ description: 'Skip local validation before publish', default: false }),
31
+ access: Flags.string({ description: 'Access level (public | restricted)', default: 'public', options: ['public', 'restricted'] }),
32
+ tags: Flags.string({ description: 'Comma-separated tags', multiple: true }),
33
+ json: Flags.boolean({ description: 'Output result as JSON' }),
34
+ };
35
+
36
+ async run(): Promise<void> {
37
+ const { args, flags } = await this.parse(PluginPublish);
38
+ const timer = createTimer();
39
+
40
+ if (!flags.json) {
41
+ printHeader('Plugin Publish');
42
+ }
43
+
44
+ try {
45
+ const artifactPath = path.resolve(process.cwd(), args.artifact);
46
+
47
+ if (!fs.existsSync(artifactPath)) {
48
+ throw new Error(`Artifact not found: ${artifactPath}`);
49
+ }
50
+
51
+ if (!flags.token) {
52
+ throw new Error('Authentication token required. Set --token or OBJECTSTACK_MARKETPLACE_TOKEN environment variable.');
53
+ }
54
+
55
+ if (!flags.json) {
56
+ printKV('Artifact', path.relative(process.cwd(), artifactPath));
57
+ printKV('Registry', flags.registryUrl);
58
+ }
59
+
60
+ // 1. Read artifact
61
+ const artifactBuffer = fs.readFileSync(artifactPath);
62
+ const sha256 = crypto.createHash('sha256').update(artifactBuffer).digest('hex');
63
+
64
+ // 2. Extract manifest info
65
+ let manifest: Record<string, unknown> | undefined;
66
+ try {
67
+ manifest = JSON.parse(artifactBuffer.toString('utf-8'));
68
+ } catch {
69
+ throw new Error('Artifact does not contain a valid JSON manifest');
70
+ }
71
+
72
+ const name = (manifest as any).manifest?.name || (manifest as any).name || 'unknown';
73
+ const version = (manifest as any).manifest?.version || (manifest as any).version || '0.0.0';
74
+
75
+ if (!flags.json) {
76
+ printKV('Package', `${name}@${version}`);
77
+ }
78
+
79
+ // 3. Local validation (unless skipped)
80
+ if (!flags.skipValidation) {
81
+ if (!flags.json) printStep('Running local validation...');
82
+
83
+ if (artifactBuffer.length === 0) {
84
+ throw new Error('Artifact is empty — cannot publish');
85
+ }
86
+
87
+ const checksumFile = artifactPath + '.sha256';
88
+ if (fs.existsSync(checksumFile)) {
89
+ const expectedHash = fs.readFileSync(checksumFile, 'utf-8').trim().split(/\s+/)[0];
90
+ if (expectedHash !== sha256) {
91
+ throw new Error(`SHA-256 mismatch: artifact has changed since build`);
92
+ }
93
+ }
94
+
95
+ if (!flags.json) printSuccess('Local validation passed');
96
+ }
97
+
98
+ // 4. Upload to marketplace
99
+ if (!flags.json) printStep('Uploading to marketplace...');
100
+
101
+ const uploadUrl = `${flags.registryUrl}/packages/upload`;
102
+ const tags = flags.tags?.flatMap(t => t.split(',')) || [];
103
+
104
+ const uploadPayload = {
105
+ packageName: name,
106
+ version,
107
+ sha256,
108
+ size: artifactBuffer.length,
109
+ preRelease: flags.preRelease,
110
+ access: flags.access,
111
+ releaseNotes: flags.releaseNotes,
112
+ tags,
113
+ };
114
+
115
+ // Perform HTTP upload
116
+ const response = await fetch(uploadUrl, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ 'Authorization': `Bearer ${flags.token}`,
121
+ },
122
+ body: JSON.stringify(uploadPayload),
123
+ });
124
+
125
+ if (!response.ok) {
126
+ const errorBody = await response.text().catch(() => 'Unknown error');
127
+ throw new Error(`Marketplace upload failed (${response.status}): ${errorBody}`);
128
+ }
129
+
130
+ const responseData = await response.json().catch(() => ({})) as Record<string, unknown>;
131
+
132
+ const result = {
133
+ success: true,
134
+ packageId: (responseData.packageId as string) || name,
135
+ version,
136
+ artifactUrl: responseData.artifactUrl as string | undefined,
137
+ sha256,
138
+ submissionId: responseData.submissionId as string | undefined,
139
+ message: `Published ${name}@${version} to marketplace`,
140
+ };
141
+
142
+ if (flags.json) {
143
+ console.log(JSON.stringify(result, null, 2));
144
+ return;
145
+ }
146
+
147
+ console.log('');
148
+ printSuccess(`Published ${chalk.cyan(`${name}@${version}`)}`);
149
+ console.log('');
150
+ printKV('SHA-256', sha256.slice(0, 16) + '...');
151
+ printKV('Size', `${(artifactBuffer.length / 1024).toFixed(1)} KB`);
152
+
153
+ if (result.artifactUrl) {
154
+ printKV('URL', result.artifactUrl);
155
+ }
156
+ if (result.submissionId) {
157
+ printKV('Submission', result.submissionId);
158
+ }
159
+
160
+ if (flags.preRelease) {
161
+ printWarning('Published as pre-release');
162
+ }
163
+
164
+ console.log('');
165
+ printInfo(`Duration: ${timer.display()}`);
166
+ console.log('');
167
+ } catch (error: any) {
168
+ if (flags.json) {
169
+ console.log(JSON.stringify({ success: false, errorMessage: error.message }));
170
+ this.exit(1);
171
+ }
172
+ printError(error.message || String(error));
173
+ this.exit(1);
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,268 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { Args, Command, Flags } from '@oclif/core';
4
+ import chalk from 'chalk';
5
+ import path from 'path';
6
+ import fs from 'fs';
7
+ import crypto from 'crypto';
8
+ import { createTimer, printHeader, printKV, printStep, printSuccess, printError, printWarning, printInfo } from '../../utils/format.js';
9
+
10
+ /**
11
+ * Validate a plugin artifact (.tgz) for structural integrity, checksum
12
+ * correctness, digital signature, and platform compatibility.
13
+ *
14
+ * Architecture alignment: `npm pack --dry-run`, `helm lint`, `vsce ls`.
15
+ */
16
+ export default class PluginValidate extends Command {
17
+ static override description = 'Validate a plugin artifact for integrity and compliance';
18
+
19
+ static override args = {
20
+ artifact: Args.string({ description: 'Path to the artifact file', required: true }),
21
+ };
22
+
23
+ static override flags = {
24
+ verifySignature: Flags.boolean({ description: 'Verify digital signature', default: true, allowNo: true }),
25
+ publicKeyPath: Flags.string({ description: 'Path to public key for signature verification' }),
26
+ verifyChecksums: Flags.boolean({ description: 'Verify SHA-256 checksums', default: true, allowNo: true }),
27
+ validateMetadata: Flags.boolean({ description: 'Validate metadata schema compliance', default: true, allowNo: true }),
28
+ platformVersion: Flags.string({ description: 'Target platform version for compatibility check' }),
29
+ json: Flags.boolean({ description: 'Output result as JSON' }),
30
+ };
31
+
32
+ async run(): Promise<void> {
33
+ const { args, flags } = await this.parse(PluginValidate);
34
+ const timer = createTimer();
35
+
36
+ if (!flags.json) {
37
+ printHeader('Plugin Validate');
38
+ }
39
+
40
+ try {
41
+ const artifactPath = path.resolve(process.cwd(), args.artifact);
42
+
43
+ if (!fs.existsSync(artifactPath)) {
44
+ throw new Error(`Artifact not found: ${artifactPath}`);
45
+ }
46
+
47
+ if (!flags.json) {
48
+ printKV('Artifact', path.relative(process.cwd(), artifactPath));
49
+ printStep('Reading artifact...');
50
+ }
51
+
52
+ const artifactBuffer = fs.readFileSync(artifactPath);
53
+ const findings: Array<{ severity: string; rule: string; message: string; path?: string }> = [];
54
+
55
+ // 1. Verify file existence and basic structure
56
+ if (artifactBuffer.length === 0) {
57
+ findings.push({ severity: 'error', rule: 'artifact.empty', message: 'Artifact file is empty' });
58
+ }
59
+
60
+ // 2. Try to parse as JSON manifest
61
+ let manifest: Record<string, unknown> | undefined;
62
+ try {
63
+ manifest = JSON.parse(artifactBuffer.toString('utf-8'));
64
+ findings.push({ severity: 'info', rule: 'manifest.parsed', message: 'Manifest parsed successfully' });
65
+ } catch {
66
+ findings.push({ severity: 'error', rule: 'manifest.invalid', message: 'Artifact does not contain valid JSON manifest' });
67
+ }
68
+
69
+ // 3. Validate required manifest fields
70
+ if (manifest) {
71
+ if (!flags.json) printStep('Validating manifest fields...');
72
+
73
+ if (!(manifest as any).manifest && !(manifest as any).name) {
74
+ findings.push({ severity: 'warning', rule: 'manifest.name', message: 'No package name found in manifest' });
75
+ }
76
+ if (!(manifest as any).manifest?.version && !(manifest as any).version) {
77
+ findings.push({ severity: 'warning', rule: 'manifest.version', message: 'No version found in manifest' });
78
+ }
79
+ }
80
+
81
+ // 4. Checksum verification
82
+ let checksumResult: { passed: boolean; mismatches?: string[] } | undefined;
83
+ if (flags.verifyChecksums) {
84
+ if (!flags.json) printStep('Verifying checksums...');
85
+ const checksumFile = artifactPath + '.sha256';
86
+ if (fs.existsSync(checksumFile)) {
87
+ const checksumContent = fs.readFileSync(checksumFile, 'utf-8').trim();
88
+ const expectedHash = checksumContent.split(/\s+/)[0];
89
+ const actualHash = crypto.createHash('sha256').update(artifactBuffer).digest('hex');
90
+ if (expectedHash === actualHash) {
91
+ checksumResult = { passed: true };
92
+ findings.push({ severity: 'info', rule: 'checksum.sha256', message: 'SHA-256 checksum verified' });
93
+ } else {
94
+ checksumResult = { passed: false, mismatches: ['artifact'] };
95
+ findings.push({ severity: 'error', rule: 'checksum.sha256', message: `SHA-256 mismatch: expected ${expectedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...` });
96
+ }
97
+ } else {
98
+ findings.push({ severity: 'warning', rule: 'checksum.missing', message: 'No .sha256 checksum file found alongside artifact' });
99
+ }
100
+ }
101
+
102
+ // 5. Signature verification
103
+ let signatureResult: { passed: boolean; failureReason?: string } | undefined;
104
+ if (flags.verifySignature) {
105
+ if (!flags.json) printStep('Verifying signature...');
106
+ const sigFile = artifactPath + '.sig';
107
+ if (fs.existsSync(sigFile) && flags.publicKeyPath && fs.existsSync(flags.publicKeyPath)) {
108
+ try {
109
+ const publicKey = fs.readFileSync(flags.publicKeyPath, 'utf-8');
110
+ const signature = fs.readFileSync(sigFile, 'utf-8');
111
+ const verifier = crypto.createVerify('RSA-SHA256');
112
+ verifier.update(artifactBuffer);
113
+ const valid = verifier.verify(publicKey, signature, 'base64');
114
+ signatureResult = { passed: valid, failureReason: valid ? undefined : 'Signature does not match' };
115
+ findings.push({
116
+ severity: valid ? 'info' : 'error',
117
+ rule: 'signature.verify',
118
+ message: valid ? 'Digital signature verified' : 'Digital signature verification failed',
119
+ });
120
+ } catch (e: any) {
121
+ signatureResult = { passed: false, failureReason: e.message };
122
+ findings.push({ severity: 'error', rule: 'signature.verify', message: `Signature verification error: ${e.message}` });
123
+ }
124
+ } else if (fs.existsSync(sigFile) && !flags.publicKeyPath) {
125
+ findings.push({ severity: 'warning', rule: 'signature.nokey', message: 'Signature file found but no --publicKeyPath provided' });
126
+ } else if (!fs.existsSync(sigFile)) {
127
+ findings.push({ severity: 'info', rule: 'signature.absent', message: 'No signature file found (unsigned artifact)' });
128
+ }
129
+ }
130
+
131
+ // 6. Platform compatibility check
132
+ let platformResult: { compatible: boolean; requiredRange?: string; targetVersion?: string } | undefined;
133
+ if (flags.platformVersion && manifest) {
134
+ if (!flags.json) printStep('Checking platform compatibility...');
135
+ const engine = (manifest as any).manifest?.engine || (manifest as any).engine;
136
+ const required = engine?.objectstack as string | undefined;
137
+ if (required) {
138
+ // Semver range check supporting >=, >, ^, ~, and exact versions
139
+ const parseSemver = (v: string) => {
140
+ const parts = v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
141
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
142
+ };
143
+
144
+ const target = parseSemver(flags.platformVersion);
145
+ let compatible = false;
146
+ let matched = false;
147
+
148
+ // >=X.Y.Z — greater than or equal
149
+ const gteMatch = required.match(/^>=\s*([\d.]+)/);
150
+ if (gteMatch) {
151
+ const req = parseSemver(gteMatch[1]);
152
+ compatible = (target.major > req.major) ||
153
+ (target.major === req.major && target.minor > req.minor) ||
154
+ (target.major === req.major && target.minor === req.minor && target.patch >= req.patch);
155
+ matched = true;
156
+ }
157
+
158
+ // >X.Y.Z — strictly greater than
159
+ if (!matched) {
160
+ const gtMatch = required.match(/^>\s*([\d.]+)/);
161
+ if (gtMatch) {
162
+ const req = parseSemver(gtMatch[1]);
163
+ compatible = (target.major > req.major) ||
164
+ (target.major === req.major && target.minor > req.minor) ||
165
+ (target.major === req.major && target.minor === req.minor && target.patch > req.patch);
166
+ matched = true;
167
+ }
168
+ }
169
+
170
+ // ^X.Y.Z — caret range (same major, >= minor.patch)
171
+ if (!matched) {
172
+ const caretMatch = required.match(/^\^\s*([\d.]+)/);
173
+ if (caretMatch) {
174
+ const req = parseSemver(caretMatch[1]);
175
+ compatible = target.major === req.major &&
176
+ ((target.minor > req.minor) ||
177
+ (target.minor === req.minor && target.patch >= req.patch));
178
+ matched = true;
179
+ }
180
+ }
181
+
182
+ // ~X.Y.Z — tilde range (same major.minor, >= patch)
183
+ if (!matched) {
184
+ const tildeMatch = required.match(/^~\s*([\d.]+)/);
185
+ if (tildeMatch) {
186
+ const req = parseSemver(tildeMatch[1]);
187
+ compatible = target.major === req.major && target.minor === req.minor && target.patch >= req.patch;
188
+ matched = true;
189
+ }
190
+ }
191
+
192
+ // Exact version match
193
+ if (!matched && /^\d+\.\d+\.\d+$/.test(required)) {
194
+ const req = parseSemver(required);
195
+ compatible = target.major === req.major && target.minor === req.minor && target.patch === req.patch;
196
+ matched = true;
197
+ }
198
+
199
+ if (matched) {
200
+ platformResult = { compatible, requiredRange: required, targetVersion: flags.platformVersion };
201
+ findings.push({
202
+ severity: compatible ? 'info' : 'error',
203
+ rule: 'platform.compatibility',
204
+ message: compatible
205
+ ? `Compatible with platform v${flags.platformVersion}`
206
+ : `Requires platform ${required}, but target is v${flags.platformVersion}`,
207
+ });
208
+ }
209
+ } else {
210
+ platformResult = { compatible: true, targetVersion: flags.platformVersion };
211
+ findings.push({ severity: 'info', rule: 'platform.noreq', message: 'No platform version requirement specified — assumed compatible' });
212
+ }
213
+ }
214
+
215
+ // 7. Summary
216
+ const errors = findings.filter(f => f.severity === 'error').length;
217
+ const warns = findings.filter(f => f.severity === 'warning').length;
218
+ const infos = findings.filter(f => f.severity === 'info').length;
219
+ const valid = errors === 0;
220
+
221
+ const result = {
222
+ valid,
223
+ checksumVerification: checksumResult,
224
+ signatureVerification: signatureResult,
225
+ platformCompatibility: platformResult,
226
+ findings,
227
+ summary: { errors, warnings: warns, infos },
228
+ };
229
+
230
+ if (flags.json) {
231
+ console.log(JSON.stringify(result, null, 2));
232
+ return;
233
+ }
234
+
235
+ console.log('');
236
+ if (valid) {
237
+ printSuccess(`Validation passed ${chalk.dim(`(${timer.display()})`)}`);
238
+ } else {
239
+ printError(`Validation failed ${chalk.dim(`(${timer.display()})`)}`);
240
+ }
241
+
242
+ console.log('');
243
+ for (const finding of findings) {
244
+ const icon = finding.severity === 'error' ? chalk.red('✗')
245
+ : finding.severity === 'warning' ? chalk.yellow('⚠')
246
+ : chalk.blue('ℹ');
247
+ console.log(` ${icon} ${chalk.dim(`[${finding.rule}]`)} ${finding.message}`);
248
+ }
249
+
250
+ console.log('');
251
+ printKV('Errors', errors);
252
+ printKV('Warnings', warns);
253
+ printKV('Info', infos);
254
+ console.log('');
255
+
256
+ if (!valid) {
257
+ this.exit(1);
258
+ }
259
+ } catch (error: any) {
260
+ if (flags.json) {
261
+ console.log(JSON.stringify({ valid: false, findings: [{ severity: 'error', rule: 'system', message: error.message }], summary: { errors: 1, warnings: 0, infos: 0 } }));
262
+ this.exit(1);
263
+ }
264
+ printError(error.message || String(error));
265
+ this.exit(1);
266
+ }
267
+ }
268
+ }
@@ -17,6 +17,9 @@ import PluginList from '../src/commands/plugin/list';
17
17
  import PluginInfo from '../src/commands/plugin/info';
18
18
  import PluginAdd from '../src/commands/plugin/add';
19
19
  import PluginRemove from '../src/commands/plugin/remove';
20
+ import PluginBuild from '../src/commands/plugin/build';
21
+ import PluginValidate from '../src/commands/plugin/validate';
22
+ import PluginPublish from '../src/commands/plugin/publish';
20
23
  import V2ToV3 from '../src/commands/codemod/v2-to-v3';
21
24
 
22
25
  describe('CLI Commands (oclif)', () => {
@@ -105,5 +108,21 @@ describe('CLI Commands (oclif)', () => {
105
108
  it('should have plugin list alias', () => {
106
109
  expect(PluginList.aliases).toContain('plugin ls');
107
110
  });
111
+
112
+ it('should have plugin build command', () => {
113
+ expect(PluginBuild.description).toContain('Build');
114
+ });
115
+
116
+ it('should have plugin build alias', () => {
117
+ expect(PluginBuild.aliases).toContain('plugin pack');
118
+ });
119
+
120
+ it('should have plugin validate command', () => {
121
+ expect(PluginValidate.description).toContain('Validate');
122
+ });
123
+
124
+ it('should have plugin publish command', () => {
125
+ expect(PluginPublish.description).toContain('Publish');
126
+ });
108
127
  });
109
128
  });