chrome-ext-ci-cd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # chrome-ext-ci-cd
2
+
3
+ CLI toolkit for Chrome extension CI/CD: validate, version, package, and release.
4
+
5
+ Zero runtime dependencies. Works with any Chrome extension project using Manifest V3.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npm install -D chrome-ext-ci-cd
11
+ npx chrome-ext-ci-cd init
12
+ npx chrome-ext-ci-cd release patch
13
+ ```
14
+
15
+ `init` scaffolds a GitHub Actions workflow, `.extensionignore`, `.nvmrc`, and npm scripts into your project. `release` validates your manifest, bumps the version, packages a `.zip`, commits, tags, and pushes -- triggering CI.
16
+
17
+ ## Commands
18
+
19
+ ### validate
20
+
21
+ Validate `manifest.json` for Chrome Web Store compatibility.
22
+
23
+ ```bash
24
+ chrome-ext-ci-cd validate [options]
25
+ ```
26
+
27
+ **Options:**
28
+
29
+ | Flag | Description |
30
+ |------|-------------|
31
+ | `--manifest, -m <path>` | Path to manifest.json (default: `manifest.json`) |
32
+ | `--help, -h` | Show help |
33
+
34
+ **Checks:** MV3 compliance, version format, icon file existence, permissions audit (flags dangerous permissions), CSP safety (blocks `unsafe-eval`).
35
+
36
+ **Exit codes:** `0` on success, `1` on validation errors. Warnings do not affect the exit code.
37
+
38
+ ```bash
39
+ npx chrome-ext-ci-cd validate
40
+ npx chrome-ext-ci-cd validate --manifest src/manifest.json
41
+ ```
42
+
43
+ ### package
44
+
45
+ Package the extension directory into a `.zip` for distribution.
46
+
47
+ ```bash
48
+ chrome-ext-ci-cd package [options]
49
+ ```
50
+
51
+ **Options:**
52
+
53
+ | Flag | Description |
54
+ |------|-------------|
55
+ | `--source <dir>` | Extension source directory (default: `.`) |
56
+ | `--output <path>` | Output zip file path (default: `dist/extension.zip`) |
57
+ | `--help, -h` | Show help |
58
+
59
+ Respects `.extensionignore` patterns plus built-in default exclusions (node_modules, .git, tests, etc.). Reports file count and size. Warns above 50 MB (Chrome Web Store soft limit).
60
+
61
+ ```bash
62
+ npx chrome-ext-ci-cd package
63
+ npx chrome-ext-ci-cd package --source . --output dist/my-ext.zip
64
+ ```
65
+
66
+ ### bump
67
+
68
+ Bump the version in `manifest.json`, create a git commit and annotated tag, and push.
69
+
70
+ ```bash
71
+ chrome-ext-ci-cd bump <patch|minor|major|x.y.z> [options]
72
+ ```
73
+
74
+ **Options:**
75
+
76
+ | Flag | Description |
77
+ |------|-------------|
78
+ | `--dry-run` | Preview version change without making changes |
79
+ | `--manifest, -m <path>` | Path to manifest.json (default: `manifest.json`) |
80
+ | `--help, -h` | Show help |
81
+
82
+ Updates both `manifest.json` and `package.json` (if present). Refuses to run if the working tree has uncommitted changes. `--dry-run` shows what would happen without modifying any files.
83
+
84
+ ```bash
85
+ npx chrome-ext-ci-cd bump patch
86
+ npx chrome-ext-ci-cd bump minor --dry-run
87
+ npx chrome-ext-ci-cd bump 2.0.0
88
+ ```
89
+
90
+ ### release
91
+
92
+ Run the full release pipeline: validate, bump, and package in sequence.
93
+
94
+ ```bash
95
+ chrome-ext-ci-cd release <patch|minor|major> [options]
96
+ ```
97
+
98
+ **Options:**
99
+
100
+ | Flag | Description |
101
+ |------|-------------|
102
+ | `--dry-run` | Validate (real), bump (preview), skip package |
103
+ | `--manifest, -m <path>` | Path to manifest.json (default: `manifest.json`) |
104
+ | `--source <dir>` | Extension source directory (default: `.`) |
105
+ | `--output <path>` | Output zip file path (default: `dist/extension.zip`) |
106
+ | `--help, -h` | Show help |
107
+
108
+ Stops on first failure with a descriptive error naming which step failed. `--dry-run` runs validation normally, shows the version bump preview, and skips packaging.
109
+
110
+ ```bash
111
+ npx chrome-ext-ci-cd release patch
112
+ npx chrome-ext-ci-cd release minor --dry-run
113
+ npx chrome-ext-ci-cd release major --source src --output dist/ext.zip
114
+ ```
115
+
116
+ ### init
117
+
118
+ Scaffold CI/CD files into your Chrome extension project.
119
+
120
+ ```bash
121
+ chrome-ext-ci-cd init [options]
122
+ ```
123
+
124
+ **Options:**
125
+
126
+ | Flag | Description |
127
+ |------|-------------|
128
+ | `--force, -f` | Overwrite existing files |
129
+ | `--help, -h` | Show help |
130
+
131
+ **Creates:**
132
+ - `.github/workflows/release.yml` -- GitHub Actions workflow for CI/CD
133
+ - `.extensionignore` -- Patterns for files to exclude from the zip
134
+ - `.nvmrc` -- Node version file
135
+
136
+ **Injects npm scripts** into `package.json`:
137
+ - `ext:validate` -- `chrome-ext-ci-cd validate`
138
+ - `ext:package` -- `chrome-ext-ci-cd package`
139
+ - `ext:release:patch` -- `chrome-ext-ci-cd release patch`
140
+ - `ext:release:minor` -- `chrome-ext-ci-cd release minor`
141
+ - `ext:release:major` -- `chrome-ext-ci-cd release major`
142
+
143
+ Warns before overwriting existing files. Use `--force` to overwrite.
144
+
145
+ ```bash
146
+ npx chrome-ext-ci-cd init
147
+ npx chrome-ext-ci-cd init --force
148
+ ```
149
+
150
+ ## Manual Setup
151
+
152
+ If you prefer not to use `init`, create these files manually:
153
+
154
+ ### 1. GitHub Actions Workflow
155
+
156
+ Create `.github/workflows/release.yml`. The workflow should trigger on version tags (`v*`) and PRs to `main`, with jobs for validation, packaging, GitHub Release creation, and optional Chrome Web Store publishing. See the [template](templates/release.yml) for a complete example.
157
+
158
+ ### 2. Extension Ignore
159
+
160
+ Create `.extensionignore` in your project root with patterns for files to exclude from the extension zip:
161
+
162
+ ```
163
+ .vscode/
164
+ .idea/
165
+ docs/
166
+ *.md
167
+ webpack.config.*
168
+ *.psd
169
+ *.sketch
170
+ ```
171
+
172
+ Built-in defaults (node_modules, .git, tests, etc.) are always excluded regardless of this file.
173
+
174
+ ### 3. npm Scripts
175
+
176
+ Add these scripts to your `package.json`:
177
+
178
+ ```json
179
+ {
180
+ "scripts": {
181
+ "ext:validate": "chrome-ext-ci-cd validate",
182
+ "ext:package": "chrome-ext-ci-cd package",
183
+ "ext:release:patch": "chrome-ext-ci-cd release patch",
184
+ "ext:release:minor": "chrome-ext-ci-cd release minor",
185
+ "ext:release:major": "chrome-ext-ci-cd release major"
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Requirements
191
+
192
+ - **Node.js** 20 or later
193
+ - **git** (for bump and release commands)
194
+ - **zip** (for packaging -- pre-installed on most systems)
195
+
196
+ ## License
197
+
198
+ Proprietary. All rights reserved.
package/bin/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { version } = require('../package.json');
5
+ const license = require('../lib/license');
6
+
7
+ const COMMANDS = {
8
+ validate: { module: '../lib/validate', description: 'Validate manifest.json' },
9
+ package: { module: '../lib/package', description: 'Package extension as .zip' },
10
+ bump: { module: '../lib/bump', description: 'Bump version in manifest.json' },
11
+ release: { module: '../lib/release', description: 'Full release pipeline (validate + bump + package)' },
12
+ init: { module: '../lib/init', description: 'Scaffold CI/CD into your project' },
13
+ activate: { module: '../lib/activate', description: 'Activate a license key' },
14
+ keygen: { module: '../lib/keygen', description: 'Generate a license key (owner-only)' },
15
+ };
16
+
17
+ function printHelp() {
18
+ console.log(`chrome-ext-ci-cd v${version}`);
19
+ console.log('');
20
+ console.log('Usage: chrome-ext-ci-cd <command> [options]');
21
+ console.log('');
22
+ console.log('Commands:');
23
+ for (const [name, cmd] of Object.entries(COMMANDS)) {
24
+ console.log(` ${name.padEnd(12)}${cmd.description}`);
25
+ }
26
+ console.log('');
27
+ console.log('Options:');
28
+ console.log(' --help, -h Show this help message');
29
+ console.log(' --version, -v Show version number');
30
+ console.log('');
31
+ console.log("Run 'chrome-ext-ci-cd <command> --help' for command-specific help.");
32
+ }
33
+
34
+ const arg = process.argv[2];
35
+
36
+ if (!arg || arg === '--help' || arg === '-h') {
37
+ printHelp();
38
+ process.exit(0);
39
+ }
40
+
41
+ if (arg === '--version' || arg === '-v') {
42
+ console.log(version);
43
+ process.exit(0);
44
+ }
45
+
46
+ if (!COMMANDS[arg]) {
47
+ console.error(`Unknown command: ${arg}`);
48
+ console.error('');
49
+ printHelp();
50
+ process.exit(1);
51
+ }
52
+
53
+ // --- License Gate ---
54
+ if (arg !== 'activate' && arg !== 'keygen') {
55
+ var licenseResult = license.checkLicense();
56
+ if (!licenseResult.valid) {
57
+ console.error(licenseResult.message);
58
+ process.exit(1);
59
+ }
60
+ if (licenseResult.trial) {
61
+ console.error('Trial: ' + licenseResult.daysLeft + ' day' + (licenseResult.daysLeft !== 1 ? 's' : '') + ' remaining. Activate a license: chrome-ext-ci-cd activate <key>');
62
+ console.error('');
63
+ }
64
+ }
65
+
66
+ const cmd = require(COMMANDS[arg].module);
67
+ process.exitCode = cmd.run(process.argv.slice(3));
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs } = require('node:util');
6
+ const license = require('./license');
7
+
8
+ // --- Help ---
9
+
10
+ function printHelp() {
11
+ console.log('Usage: chrome-ext-ci-cd activate <key>');
12
+ console.log('');
13
+ console.log('Activate a license key for this machine.');
14
+ console.log('');
15
+ console.log('Arguments:');
16
+ console.log(' key The license key (starts with CECICD-)');
17
+ console.log('');
18
+ console.log('Options:');
19
+ console.log(' --help, -h Show this help message');
20
+ console.log('');
21
+ console.log('Examples:');
22
+ console.log(' chrome-ext-ci-cd activate CECICD-eyJ0aWV....-a1b2c3d4...');
23
+ }
24
+
25
+ // --- CLI Runner ---
26
+
27
+ function run(args) {
28
+ var values;
29
+ var positionals;
30
+ try {
31
+ var parsed = parseArgs({
32
+ args: args,
33
+ options: {
34
+ help: { type: 'boolean', short: 'h', default: false },
35
+ },
36
+ allowPositionals: true,
37
+ });
38
+ values = parsed.values;
39
+ positionals = parsed.positionals;
40
+ } catch (err) {
41
+ if (err.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
42
+ console.error('Unknown option: ' + err.message);
43
+ console.error('');
44
+ printHelp();
45
+ return 1;
46
+ }
47
+ throw err;
48
+ }
49
+
50
+ if (values.help) {
51
+ printHelp();
52
+ return 0;
53
+ }
54
+
55
+ // Get key from positionals
56
+ var key = positionals[0];
57
+ if (!key) {
58
+ console.error('Missing license key. Usage: chrome-ext-ci-cd activate <key>');
59
+ return 1;
60
+ }
61
+
62
+ // Validate key format and signature
63
+ var result = license.verifyKey(key, license.HMAC_SECRET);
64
+
65
+ if (!result.valid && result.expired) {
66
+ console.error('License key is expired (valid until ' + result.payload.exp + ').');
67
+ console.error('Contact the vendor for a renewal key.');
68
+ return 1;
69
+ }
70
+
71
+ if (!result.valid) {
72
+ console.error('Invalid license key: ' + result.error);
73
+ console.error('');
74
+ console.error('Check the key and try again:');
75
+ console.error(' chrome-ext-ci-cd activate <key>');
76
+ return 1;
77
+ }
78
+
79
+ // Store the key
80
+ var configDir = license.getConfigDir();
81
+ fs.mkdirSync(configDir, { recursive: true });
82
+ var licensePath = path.join(configDir, 'license');
83
+ fs.writeFileSync(licensePath, key + '\n');
84
+
85
+ // Print activation summary
86
+ console.log('License activated successfully.');
87
+ console.log('');
88
+ console.log(' Customer: ' + result.payload.customer);
89
+ console.log(' Tier: ' + result.payload.tier);
90
+ console.log(' Expires: ' + result.payload.exp);
91
+ console.log(' Stored at: ' + licensePath);
92
+
93
+ return 0;
94
+ }
95
+
96
+ // --- Exports ---
97
+
98
+ module.exports = { run: run };
package/lib/bump.js ADDED
@@ -0,0 +1,299 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+ const { parseArgs } = require('node:util');
7
+
8
+ // --- Helpers ---
9
+
10
+ function calculateNewVersion(currentVersion, bumpType) {
11
+ if (['patch', 'minor', 'major'].includes(bumpType)) {
12
+ var parts = currentVersion.split('.').map(Number);
13
+ // Pad to 3 parts if needed
14
+ while (parts.length < 3) parts.push(0);
15
+
16
+ switch (bumpType) {
17
+ case 'major':
18
+ parts[0]++;
19
+ parts[1] = 0;
20
+ parts[2] = 0;
21
+ break;
22
+ case 'minor':
23
+ parts[1]++;
24
+ parts[2] = 0;
25
+ break;
26
+ case 'patch':
27
+ parts[2]++;
28
+ break;
29
+ }
30
+ return parts.join('.');
31
+ }
32
+ // Explicit version provided
33
+ return bumpType;
34
+ }
35
+
36
+ function checkDirtyTree() {
37
+ try {
38
+ execFileSync('git', ['diff', '--quiet'], { stdio: 'pipe' });
39
+ execFileSync('git', ['diff', '--cached', '--quiet'], { stdio: 'pipe' });
40
+ return { clean: true };
41
+ } catch (e) {
42
+ return {
43
+ clean: false,
44
+ message: 'Working tree has uncommitted changes. Commit or stash changes first.'
45
+ };
46
+ }
47
+ }
48
+
49
+ // --- Pure Bump Function ---
50
+
51
+ function bump(bumpType, options) {
52
+ var opts = options || {};
53
+
54
+ // Validate bumpType
55
+ if (!bumpType) {
56
+ return {
57
+ success: false,
58
+ errors: ['Bump type required. Use: patch, minor, major, or an explicit version (e.g., 2.1.0)'],
59
+ warnings: [],
60
+ data: {}
61
+ };
62
+ }
63
+
64
+ // Resolve manifest path
65
+ var manifestPath = path.resolve(
66
+ opts.manifestPath || opts.manifest || process.env.MANIFEST_PATH || path.join(process.cwd(), 'manifest.json')
67
+ );
68
+
69
+ var data = { manifestPath: manifestPath };
70
+
71
+ // Check manifest exists
72
+ if (!fs.existsSync(manifestPath)) {
73
+ return {
74
+ success: false,
75
+ errors: ['Manifest not found: ' + manifestPath + '. Check the file path or use --manifest to specify location.'],
76
+ warnings: [],
77
+ data: data
78
+ };
79
+ }
80
+
81
+ // Read and parse manifest
82
+ var manifest;
83
+ try {
84
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
85
+ } catch (e) {
86
+ return {
87
+ success: false,
88
+ errors: ['Invalid JSON in ' + manifestPath + ': ' + e.message],
89
+ warnings: [],
90
+ data: data
91
+ };
92
+ }
93
+
94
+ // Get current version
95
+ var currentVersion = manifest.version;
96
+ if (!currentVersion) {
97
+ return {
98
+ success: false,
99
+ errors: ['manifest.json is missing "version" field.'],
100
+ warnings: [],
101
+ data: data
102
+ };
103
+ }
104
+
105
+ // Calculate new version
106
+ var newVersion = calculateNewVersion(currentVersion, bumpType);
107
+ var tag = 'v' + newVersion;
108
+
109
+ data.previousVersion = currentVersion;
110
+ data.newVersion = newVersion;
111
+ data.tag = tag;
112
+
113
+ // Dry run -- return early without side effects
114
+ if (opts.dryRun) {
115
+ data.dryRun = true;
116
+ return {
117
+ success: true,
118
+ errors: [],
119
+ warnings: [],
120
+ data: data
121
+ };
122
+ }
123
+
124
+ // Check dirty tree
125
+ var treeStatus = checkDirtyTree();
126
+ if (!treeStatus.clean) {
127
+ return {
128
+ success: false,
129
+ errors: [treeStatus.message],
130
+ warnings: [],
131
+ data: data
132
+ };
133
+ }
134
+
135
+ // Update manifest.json
136
+ manifest.version = newVersion;
137
+ if (manifest.version_name !== undefined) {
138
+ manifest.version_name = newVersion;
139
+ }
140
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
141
+
142
+ // Update package.json in the same directory (D-05: bump always syncs both)
143
+ var packageJsonPath = path.join(path.dirname(manifestPath), 'package.json');
144
+ var updatedPackageJson = false;
145
+ if (fs.existsSync(packageJsonPath)) {
146
+ try {
147
+ var pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
148
+ pkg.version = newVersion;
149
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
150
+ updatedPackageJson = true;
151
+ } catch (e) {
152
+ // Skip silently if package.json can't be parsed
153
+ }
154
+ }
155
+
156
+ // Git operations
157
+ try {
158
+ // Stage manifest.json (always)
159
+ execFileSync('git', ['add', manifestPath], { stdio: 'pipe' });
160
+
161
+ // Stage package.json only if it was updated
162
+ if (updatedPackageJson) {
163
+ execFileSync('git', ['add', packageJsonPath], { stdio: 'pipe' });
164
+ }
165
+
166
+ // Commit
167
+ execFileSync('git', ['commit', '-m', 'chore: bump version to ' + newVersion], { stdio: 'pipe' });
168
+
169
+ // Tag (annotated)
170
+ execFileSync('git', ['tag', '-a', tag, '-m', 'Release ' + tag], { stdio: 'pipe' });
171
+ } catch (e) {
172
+ var errMsg = e.stderr ? e.stderr.toString() : e.message;
173
+ return {
174
+ success: false,
175
+ errors: ['Git operations failed: ' + errMsg + '. Commit and tag were not created.'],
176
+ warnings: [],
177
+ data: data
178
+ };
179
+ }
180
+
181
+ // Push
182
+ try {
183
+ var branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
184
+ encoding: 'utf-8'
185
+ }).trim();
186
+
187
+ execFileSync('git', ['push', 'origin', branch, '--follow-tags'], { stdio: 'inherit' });
188
+ data.branch = branch;
189
+ } catch (e) {
190
+ return {
191
+ success: false,
192
+ errors: ['Push failed. You can push manually: git push origin --follow-tags'],
193
+ warnings: [],
194
+ data: data
195
+ };
196
+ }
197
+
198
+ return {
199
+ success: true,
200
+ errors: [],
201
+ warnings: [],
202
+ data: data
203
+ };
204
+ }
205
+
206
+ // --- Help ---
207
+
208
+ function printHelp() {
209
+ console.log('Usage: chrome-ext-ci-cd bump <patch|minor|major|x.y.z> [options]');
210
+ console.log('');
211
+ console.log('Bump version in manifest.json, commit, tag, and push.');
212
+ console.log('');
213
+ console.log('Arguments:');
214
+ console.log(' patch Increment patch version (1.0.0 -> 1.0.1)');
215
+ console.log(' minor Increment minor version (1.0.0 -> 1.1.0)');
216
+ console.log(' major Increment major version (1.0.0 -> 2.0.0)');
217
+ console.log(' x.y.z Set explicit version');
218
+ console.log('');
219
+ console.log('Options:');
220
+ console.log(' --dry-run Preview version change without making changes');
221
+ console.log(' --manifest, -m Path to manifest.json (default: manifest.json)');
222
+ console.log(' --help, -h Show this help message');
223
+ console.log('');
224
+ console.log('Examples:');
225
+ console.log(' chrome-ext-ci-cd bump patch');
226
+ console.log(' chrome-ext-ci-cd bump minor --dry-run');
227
+ console.log(' chrome-ext-ci-cd bump 2.0.0');
228
+ }
229
+
230
+ // --- CLI Runner ---
231
+
232
+ function run(args) {
233
+ var values, positionals;
234
+ try {
235
+ var parsed = parseArgs({
236
+ args: args,
237
+ options: {
238
+ 'dry-run': { type: 'boolean', default: false },
239
+ manifest: { type: 'string', short: 'm', default: process.env.MANIFEST_PATH || 'manifest.json' },
240
+ help: { type: 'boolean', short: 'h', default: false },
241
+ },
242
+ allowPositionals: true,
243
+ });
244
+ values = parsed.values;
245
+ positionals = parsed.positionals;
246
+ } catch (err) {
247
+ if (err.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
248
+ console.error('Unknown option: ' + err.message);
249
+ console.error('');
250
+ printHelp();
251
+ return 1;
252
+ }
253
+ throw err;
254
+ }
255
+
256
+ if (values.help) {
257
+ printHelp();
258
+ return 0;
259
+ }
260
+
261
+ var bumpType = positionals[0];
262
+ if (!bumpType) {
263
+ console.error('Bump type required. Usage: chrome-ext-ci-cd bump <patch|minor|major> [--dry-run]');
264
+ return 1;
265
+ }
266
+
267
+ var result = bump(bumpType, {
268
+ manifestPath: path.resolve(values.manifest),
269
+ dryRun: values['dry-run']
270
+ });
271
+
272
+ // Format output
273
+ if (result.data.previousVersion) {
274
+ console.log('Current version: ' + result.data.previousVersion);
275
+ }
276
+ if (result.data.newVersion) {
277
+ console.log('New version: ' + result.data.newVersion);
278
+ }
279
+
280
+ if (result.data.dryRun) {
281
+ console.log('');
282
+ console.log('[DRY RUN] No changes made.');
283
+ } else if (result.success) {
284
+ console.log('');
285
+ console.log('Created commit and tag: ' + result.data.tag);
286
+ if (result.data.branch) {
287
+ console.log('Pushed ' + result.data.branch + ' + ' + result.data.tag + ' to origin.');
288
+ }
289
+ }
290
+
291
+ if (result.errors.length > 0) {
292
+ console.log('');
293
+ result.errors.forEach(function(e) { console.error('ERROR: ' + e); });
294
+ }
295
+
296
+ return result.success ? 0 : 1;
297
+ }
298
+
299
+ module.exports = { bump, run };