localssl-cli 0.1.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,25 @@
1
+ # localssl
2
+
3
+ One-command local HTTPS for development teams.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -D localssl
9
+ npx localssl
10
+ ```
11
+
12
+ ## Commands
13
+
14
+ - `localssl` / `localssl use` - initialize + create project cert + configure framework
15
+ - `localssl init` - initialize machine CA and trust stores
16
+ - `localssl status` - check expiry and team sync status
17
+ - `localssl renew` - renew project cert
18
+ - `localssl trust` - import team public CAs from `localssl.json`
19
+ - `localssl qr` - serve CA certificate + print QR for phone install
20
+ - `localssl ci` - ephemeral cert setup for CI (`CI=true`)
21
+ - `localssl remove` - uninstall trust and local files
22
+
23
+ ## Team sharing
24
+
25
+ `localssl.json` is safe to commit. It stores only public CA certificates.
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ const { Command } = require('commander');
3
+ const { initCommand } = require('../src/init');
4
+ const { useProject } = require('../src/use');
5
+ const { runQrServer } = require('../src/mobile');
6
+ const { runCiSetup } = require('../src/ci');
7
+ const { status } = require('../src/status');
8
+ const { renew } = require('../src/renew');
9
+ const { removeAll } = require('../src/remove');
10
+ const { trustTeam } = require('../src/trust');
11
+ const { isCI, err } = require('../src/utils');
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('localssl')
17
+ .description('One-command local HTTPS setup')
18
+ .version('0.1.0');
19
+
20
+ program
21
+ .command('init')
22
+ .description('Initialize machine CA and trust stores')
23
+ .action(runGuard(initCommand));
24
+
25
+ program
26
+ .command('use')
27
+ .description('Generate per-project certificate and configure framework')
28
+ .argument('[hosts...]', 'additional hosts')
29
+ .action(runGuard(async (hosts) => useProject(hosts || [])));
30
+
31
+ program
32
+ .command('status')
33
+ .description('Show cert status and expiry')
34
+ .action(runGuard(status));
35
+
36
+ program
37
+ .command('renew')
38
+ .description('Regenerate project certificate')
39
+ .action(runGuard(renew));
40
+
41
+ program
42
+ .command('qr')
43
+ .description('Start mobile CA QR installer server')
44
+ .action(runGuard(runQrServer));
45
+
46
+ program
47
+ .command('ci')
48
+ .description('Setup ephemeral HTTPS for CI')
49
+ .action(runGuard(async () => {
50
+ if (!isCI()) {
51
+ throw new Error('CI mode expected CI=true environment variable.');
52
+ }
53
+ await runCiSetup();
54
+ }));
55
+
56
+ program
57
+ .command('trust')
58
+ .description('Trust teammate public CAs from localssl.json')
59
+ .action(runGuard(trustTeam));
60
+
61
+ program
62
+ .command('remove')
63
+ .description('Uninstall localssl from machine and project')
64
+ .action(runGuard(removeAll));
65
+
66
+ program.action(runGuard(async () => {
67
+ await useProject([]);
68
+ }));
69
+
70
+ program.parseAsync(process.argv);
71
+
72
+ function runGuard(fn) {
73
+ return async (...args) => {
74
+ try {
75
+ await fn(...args);
76
+ } catch (error) {
77
+ err(error.message || 'localssl failed');
78
+ process.exitCode = 1;
79
+ }
80
+ };
81
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "localssl-cli",
3
+ "version": "0.1.0",
4
+ "description": "One-command local HTTPS setup for teams",
5
+ "type": "commonjs",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "localssl": "bin/localssl.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "start": "node bin/localssl.js",
15
+ "test": "node -e \"console.log('No tests yet')\""
16
+ },
17
+ "keywords": [
18
+ "https",
19
+ "localhost",
20
+ "mkcert",
21
+ "developer-experience"
22
+ ],
23
+ "license": "MIT",
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "README.md",
28
+ "package.json"
29
+ ],
30
+ "dependencies": {
31
+ "chalk": "^4.1.2",
32
+ "commander": "^12.1.0",
33
+ "detect-port": "^2.1.0",
34
+ "fs-extra": "^11.2.0",
35
+ "qrcode-terminal": "^0.12.0"
36
+ }
37
+ }
@@ -0,0 +1,175 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const https = require('https');
5
+ const { execFile } = require('child_process');
6
+ const { LOCALSSL_HOME, LOCALSSL_CA_PUBLIC, step, warn } = require('./utils');
7
+ const { trustCertificate: trustMac } = require('./trust/macos');
8
+ const { trustCertificate: trustWindows } = require('./trust/windows');
9
+ const { trustCertificate: trustLinux } = require('./trust/linux');
10
+ const { trustInFirefox } = require('./trust/firefox');
11
+ const { trustInChromium } = require('./trust/chromium');
12
+
13
+ const MKCERT_VERSION = 'v1.4.4';
14
+
15
+ function getMkcertBinaryPath() {
16
+ const ext = process.platform === 'win32' ? '.exe' : '';
17
+ return path.join(LOCALSSL_HOME, `mkcert${ext}`);
18
+ }
19
+
20
+ function getMkcertAssetName() {
21
+ const platform = process.platform;
22
+ const arch = os.arch();
23
+
24
+ if (platform === 'win32') {
25
+ if (arch === 'x64') return 'mkcert-v1.4.4-windows-amd64.exe';
26
+ if (arch === 'arm64') return 'mkcert-v1.4.4-windows-arm64.exe';
27
+ }
28
+
29
+ if (platform === 'darwin') {
30
+ if (arch === 'x64') return 'mkcert-v1.4.4-darwin-amd64';
31
+ if (arch === 'arm64') return 'mkcert-v1.4.4-darwin-arm64';
32
+ }
33
+
34
+ if (platform === 'linux') {
35
+ if (arch === 'x64') return 'mkcert-v1.4.4-linux-amd64';
36
+ if (arch === 'arm64') return 'mkcert-v1.4.4-linux-arm64';
37
+ }
38
+
39
+ throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
40
+ }
41
+
42
+ function downloadFile(url, outputPath) {
43
+ return new Promise((resolve, reject) => {
44
+ const request = https.get(url, (response) => {
45
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
46
+ downloadFile(response.headers.location, outputPath).then(resolve).catch(reject);
47
+ return;
48
+ }
49
+
50
+ if (response.statusCode !== 200) {
51
+ reject(new Error(`mkcert download failed: ${response.statusCode}`));
52
+ return;
53
+ }
54
+
55
+ const file = fs.createWriteStream(outputPath, { mode: 0o755 });
56
+ response.pipe(file);
57
+ file.on('finish', () => file.close(resolve));
58
+ });
59
+
60
+ request.on('error', reject);
61
+ });
62
+ }
63
+
64
+ async function ensureMkcert() {
65
+ await fs.ensureDir(LOCALSSL_HOME);
66
+ const mkcertPath = getMkcertBinaryPath();
67
+
68
+ if (await fs.pathExists(mkcertPath)) {
69
+ return mkcertPath;
70
+ }
71
+
72
+ const asset = getMkcertAssetName();
73
+ const url = `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/${asset}`;
74
+ await downloadFile(url, mkcertPath);
75
+
76
+ if (process.platform !== 'win32') {
77
+ await fs.chmod(mkcertPath, 0o755);
78
+ }
79
+
80
+ return mkcertPath;
81
+ }
82
+
83
+ function execMkcert(mkcertPath, args) {
84
+ return new Promise((resolve, reject) => {
85
+ execFile(mkcertPath, args, { env: { ...process.env, CAROOT: LOCALSSL_HOME } }, (error, stdout, stderr) => {
86
+ if (error) {
87
+ reject(new Error(stderr || error.message));
88
+ return;
89
+ }
90
+ resolve(stdout);
91
+ });
92
+ });
93
+ }
94
+
95
+ async function trustSystem(certPath) {
96
+ if (process.platform === 'darwin') {
97
+ await trustMac(certPath);
98
+ return 'macOS system store';
99
+ }
100
+
101
+ if (process.platform === 'win32') {
102
+ await trustWindows(certPath);
103
+ return 'Windows Root store';
104
+ }
105
+
106
+ await trustLinux(certPath);
107
+ return 'Linux system store';
108
+ }
109
+
110
+ async function configureNodeExtraCACerts() {
111
+ const value = LOCALSSL_CA_PUBLIC;
112
+
113
+ if (process.platform === 'win32') {
114
+ return new Promise((resolve) => {
115
+ execFile('setx', ['NODE_EXTRA_CA_CERTS', value], { windowsHide: true }, () => resolve('setx NODE_EXTRA_CA_CERTS'));
116
+ });
117
+ }
118
+
119
+ const shellProfile = process.platform === 'darwin' ? path.join(os.homedir(), '.zprofile') : path.join(os.homedir(), '.profile');
120
+ const exportLine = `export NODE_EXTRA_CA_CERTS=\"${value}\"`;
121
+
122
+ let current = '';
123
+ if (await fs.pathExists(shellProfile)) {
124
+ current = await fs.readFile(shellProfile, 'utf8');
125
+ }
126
+
127
+ if (!current.includes('NODE_EXTRA_CA_CERTS')) {
128
+ await fs.appendFile(shellProfile, `\n${exportLine}\n`);
129
+ return `updated ${path.basename(shellProfile)}`;
130
+ }
131
+
132
+ return `${path.basename(shellProfile)} already configured`;
133
+ }
134
+
135
+ async function initMachine({ quiet = false } = {}) {
136
+ await fs.ensureDir(LOCALSSL_HOME);
137
+ const mkcertPath = await ensureMkcert();
138
+
139
+ const hasCA = await fs.pathExists(LOCALSSL_CA_PUBLIC);
140
+ if (hasCA) {
141
+ if (!quiet) {
142
+ step(1, 1, 'Machine CA setup', 'skip', '(already configured)');
143
+ }
144
+ return { mkcertPath, initialized: false };
145
+ }
146
+
147
+ await execMkcert(mkcertPath, ['-install']);
148
+ const systemResult = await trustSystem(LOCALSSL_CA_PUBLIC);
149
+ const firefoxResult = await trustInFirefox(LOCALSSL_CA_PUBLIC);
150
+ const chromiumResult = await trustInChromium(LOCALSSL_CA_PUBLIC);
151
+ const nodeResult = await configureNodeExtraCACerts();
152
+
153
+ if (!quiet) {
154
+ const firefoxText = firefoxResult.trusted ? `+ Firefox (${firefoxResult.reason})` : `+ Firefox skipped (${firefoxResult.reason})`;
155
+ const chromiumText = chromiumResult.trusted ? `+ Chrome/Edge (${chromiumResult.reason})` : `+ Chrome/Edge skipped (${chromiumResult.reason})`;
156
+ step(1, 1, 'Installing local CA', 'ok', `(${systemResult} ${firefoxText} ${chromiumText}; ${nodeResult})`);
157
+ }
158
+
159
+ if (!firefoxResult.trusted) {
160
+ warn(`Firefox trust skipped: ${firefoxResult.reason}`);
161
+ }
162
+
163
+ if (!chromiumResult.trusted) {
164
+ warn(`Chrome/Edge NSS trust skipped: ${chromiumResult.reason}`);
165
+ }
166
+
167
+ return { mkcertPath, initialized: true };
168
+ }
169
+
170
+ module.exports = {
171
+ initMachine,
172
+ ensureMkcert,
173
+ execMkcert,
174
+ getMkcertBinaryPath
175
+ };
package/src/certgen.js ADDED
@@ -0,0 +1,66 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR, PROJECT_LOCALSSL_DIR, PROJECT_CERT, PROJECT_KEY } = require('./utils');
4
+ const { execMkcert } = require('./bootstrap');
5
+
6
+ function parseHostsFromUrl(value) {
7
+ try {
8
+ const normalized = value.startsWith('http') ? value : `https://${value}`;
9
+ const host = new URL(normalized).hostname;
10
+ return host ? [host] : [];
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ async function detectHostsFromProject() {
17
+ const hosts = new Set(['localhost', '127.0.0.1', '::1']);
18
+
19
+ const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
20
+ if (await fs.pathExists(packageJsonPath)) {
21
+ const pkg = await fs.readJson(packageJsonPath).catch(() => ({}));
22
+ if (typeof pkg.proxy === 'string') {
23
+ parseHostsFromUrl(pkg.proxy).forEach((host) => hosts.add(host));
24
+ }
25
+ }
26
+
27
+ for (const file of ['.env', '.env.local']) {
28
+ const envPath = path.join(PROJECT_DIR, file);
29
+ if (!(await fs.pathExists(envPath))) {
30
+ continue;
31
+ }
32
+
33
+ const content = await fs.readFile(envPath, 'utf8');
34
+ const lines = content.split(/\r?\n/);
35
+ for (const line of lines) {
36
+ const [key, raw] = line.split('=');
37
+ if (!key || !raw) {
38
+ continue;
39
+ }
40
+
41
+ const value = raw.trim().replace(/^['"]|['"]$/g, '');
42
+ if (/VITE_DEV_SERVER_HOST|NEXT_PUBLIC_URL|APP_URL|HOST|DEV_URL/i.test(key)) {
43
+ parseHostsFromUrl(value).forEach((host) => hosts.add(host));
44
+ }
45
+ }
46
+ }
47
+
48
+ return [...hosts];
49
+ }
50
+
51
+ async function generateProjectCert({ mkcertPath, hosts, force = false }) {
52
+ await fs.ensureDir(PROJECT_LOCALSSL_DIR);
53
+
54
+ const certExists = (await fs.pathExists(PROJECT_CERT)) && (await fs.pathExists(PROJECT_KEY));
55
+ if (certExists && !force) {
56
+ return { generated: false, certPath: PROJECT_CERT, keyPath: PROJECT_KEY };
57
+ }
58
+
59
+ await execMkcert(mkcertPath, ['-cert-file', PROJECT_CERT, '-key-file', PROJECT_KEY, ...hosts]);
60
+ return { generated: true, certPath: PROJECT_CERT, keyPath: PROJECT_KEY };
61
+ }
62
+
63
+ module.exports = {
64
+ detectHostsFromProject,
65
+ generateProjectCert
66
+ };
package/src/ci.js ADDED
@@ -0,0 +1,51 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { execFile } = require('child_process');
5
+ const { ensureMkcert } = require('./bootstrap');
6
+
7
+ function execWithEnv(file, args, env) {
8
+ return new Promise((resolve, reject) => {
9
+ execFile(file, args, { env }, (error, stdout, stderr) => {
10
+ if (error) {
11
+ reject(new Error(stderr || error.message));
12
+ return;
13
+ }
14
+ resolve(stdout);
15
+ });
16
+ });
17
+ }
18
+
19
+ async function runCiSetup() {
20
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'localssl-'));
21
+ const certDir = path.join(tempRoot, 'certs');
22
+ const cert = path.join(certDir, 'cert.pem');
23
+ const key = path.join(certDir, 'key.pem');
24
+ const ca = path.join(tempRoot, 'rootCA.pem');
25
+ await fs.ensureDir(certDir);
26
+
27
+ const mkcertPath = await ensureMkcert();
28
+ const env = { ...process.env, CAROOT: tempRoot };
29
+ await execWithEnv(mkcertPath, ['-install'], env);
30
+ await execWithEnv(mkcertPath, ['-cert-file', cert, '-key-file', key, 'localhost', '127.0.0.1', '::1'], env);
31
+
32
+ const vars = {
33
+ NODE_EXTRA_CA_CERTS: ca,
34
+ SSL_CERT_FILE: ca,
35
+ REQUESTS_CA_BUNDLE: ca,
36
+ LOCALSSL_CERT_FILE: cert,
37
+ LOCALSSL_KEY_FILE: key
38
+ };
39
+
40
+ if (process.env.GITHUB_ENV) {
41
+ const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
42
+ await fs.appendFile(process.env.GITHUB_ENV, lines);
43
+ }
44
+
45
+ console.log(' CI HTTPS ready ✓');
46
+ for (const [k, v] of Object.entries(vars)) {
47
+ console.log(` export ${k}=${v}`);
48
+ }
49
+ }
50
+
51
+ module.exports = { runCiSetup };
@@ -0,0 +1,42 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ async function configureCra(certPath, keyPath) {
6
+ const envLocalPath = path.join(PROJECT_DIR, '.env.local');
7
+ const vars = {
8
+ HTTPS: 'true',
9
+ SSL_CRT_FILE: certPath,
10
+ SSL_KEY_FILE: keyPath
11
+ };
12
+
13
+ let current = '';
14
+ if (await fs.pathExists(envLocalPath)) {
15
+ current = await fs.readFile(envLocalPath, 'utf8');
16
+ }
17
+
18
+ let changed = false;
19
+ let updated = current;
20
+ for (const [key, value] of Object.entries(vars)) {
21
+ const line = `${key}=${value}`;
22
+ const regex = new RegExp(`^${key}=.*$`, 'm');
23
+ if (regex.test(updated)) {
24
+ if (!updated.includes(line)) {
25
+ updated = updated.replace(regex, line);
26
+ changed = true;
27
+ }
28
+ } else {
29
+ updated += `${updated.endsWith('\n') || !updated ? '' : '\n'}${line}\n`;
30
+ changed = true;
31
+ }
32
+ }
33
+
34
+ if (!changed) {
35
+ return { updated: false, details: 'already configured' };
36
+ }
37
+
38
+ await fs.writeFile(envLocalPath, updated);
39
+ return { updated: true, details: '.env.local updated' };
40
+ }
41
+
42
+ module.exports = { configureCra };
@@ -0,0 +1,23 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ async function detectFramework() {
6
+ const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
7
+ if (!(await fs.pathExists(packageJsonPath))) {
8
+ return 'generic';
9
+ }
10
+
11
+ const pkg = await fs.readJson(packageJsonPath).catch(() => ({}));
12
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13
+
14
+ if (deps.vite) return 'vite';
15
+ if (deps.next) return 'nextjs';
16
+ if (deps['react-scripts']) return 'cra';
17
+ if (deps.express) return 'express';
18
+ if (deps['webpack-dev-server']) return 'webpack';
19
+
20
+ return 'generic';
21
+ }
22
+
23
+ module.exports = { detectFramework };
@@ -0,0 +1,17 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ async function configureExpress(certPath, keyPath) {
6
+ const helperPath = path.join(PROJECT_DIR, 'localssl.js');
7
+ if (await fs.pathExists(helperPath)) {
8
+ return { updated: false, details: 'localssl.js already exists' };
9
+ }
10
+
11
+ const content = `const fs = require('fs');\n\nconst httpsOptions = {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n};\n\nmodule.exports = { httpsOptions };\n`;
12
+
13
+ await fs.writeFile(helperPath, content);
14
+ return { updated: true, details: 'localssl.js created' };
15
+ }
16
+
17
+ module.exports = { configureExpress };
@@ -0,0 +1,43 @@
1
+ const { detectFramework } = require('./detect');
2
+ const { configureVite } = require('./vite');
3
+ const { configureNext } = require('./nextjs');
4
+ const { configureCra } = require('./cra');
5
+ const { configureExpress } = require('./express');
6
+ const { configureWebpack } = require('./webpack');
7
+
8
+ async function configureFramework(certPath, keyPath) {
9
+ const framework = await detectFramework();
10
+
11
+ if (framework === 'vite') {
12
+ const result = await configureVite(certPath, keyPath);
13
+ return { framework: 'Vite', ...result };
14
+ }
15
+
16
+ if (framework === 'nextjs') {
17
+ const result = await configureNext(certPath, keyPath);
18
+ return { framework: 'Next.js', ...result };
19
+ }
20
+
21
+ if (framework === 'cra') {
22
+ const result = await configureCra(certPath, keyPath);
23
+ return { framework: 'Create React App', ...result };
24
+ }
25
+
26
+ if (framework === 'express') {
27
+ const result = await configureExpress(certPath, keyPath);
28
+ return { framework: 'Express', ...result };
29
+ }
30
+
31
+ if (framework === 'webpack') {
32
+ const result = await configureWebpack(certPath, keyPath);
33
+ return { framework: 'Webpack Dev Server', ...result };
34
+ }
35
+
36
+ return {
37
+ framework: 'Generic',
38
+ updated: false,
39
+ details: `Use cert: ${certPath} and key: ${keyPath}`
40
+ };
41
+ }
42
+
43
+ module.exports = { configureFramework };
@@ -0,0 +1,35 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ function ensureHttpsFlag(script, certPath, keyPath) {
6
+ if (!script || script.includes('--experimental-https')) {
7
+ return script;
8
+ }
9
+
10
+ const cert = certPath.replace(/\\/g, '/');
11
+ const key = keyPath.replace(/\\/g, '/');
12
+ return `${script} --experimental-https --experimental-https-key ${key} --experimental-https-cert ${cert}`;
13
+ }
14
+
15
+ async function configureNext(certPath, keyPath) {
16
+ const packageJsonPath = path.join(PROJECT_DIR, 'package.json');
17
+ if (!(await fs.pathExists(packageJsonPath))) {
18
+ return { updated: false, details: 'package.json not found' };
19
+ }
20
+
21
+ const pkg = await fs.readJson(packageJsonPath);
22
+ pkg.scripts = pkg.scripts || {};
23
+ const before = pkg.scripts.dev || 'next dev';
24
+ const nextScript = ensureHttpsFlag(before, certPath, keyPath);
25
+
26
+ if (nextScript === before) {
27
+ return { updated: false, details: 'already configured' };
28
+ }
29
+
30
+ pkg.scripts.dev = nextScript;
31
+ await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
32
+ return { updated: true, details: 'package.json scripts updated' };
33
+ }
34
+
35
+ module.exports = { configureNext };
@@ -0,0 +1,30 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ async function configureVite(certPath, keyPath) {
6
+ const tsPath = path.join(PROJECT_DIR, 'vite.config.ts');
7
+ const jsPath = path.join(PROJECT_DIR, 'vite.config.js');
8
+ const configPath = (await fs.pathExists(tsPath)) ? tsPath : jsPath;
9
+
10
+ if (!configPath || !(await fs.pathExists(configPath))) {
11
+ const content = `import { defineConfig } from 'vite';\nimport fs from 'fs';\n\nexport default defineConfig({\n server: {\n https: {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n }\n }\n});\n`;
12
+ await fs.writeFile(tsPath, content);
13
+ return { updated: true, details: 'vite.config.ts created' };
14
+ }
15
+
16
+ const existing = await fs.readFile(configPath, 'utf8');
17
+ if (/server\s*:\s*{[\s\S]*https\s*:/m.test(existing) || /https\s*:\s*true/m.test(existing)) {
18
+ return { updated: false, details: 'already configured' };
19
+ }
20
+
21
+ const injection = `\n\n// localssl\nimport fs from 'fs';\n\nconst localsslHttps = {\n cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'),\n key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}')\n};\n`;
22
+
23
+ const updated = existing.replace(/defineConfig\s*\(\s*{/, `defineConfig({\n server: { https: localsslHttps },`);
24
+ const finalText = updated === existing ? `${existing}${injection}` : `${injection}${updated}`;
25
+ await fs.writeFile(configPath, finalText);
26
+
27
+ return { updated: true, details: path.basename(configPath) + ' updated' };
28
+ }
29
+
30
+ module.exports = { configureVite };
@@ -0,0 +1,30 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { PROJECT_DIR } = require('../utils');
4
+
5
+ async function configureWebpack(certPath, keyPath) {
6
+ const configPath = path.join(PROJECT_DIR, 'webpack.config.js');
7
+ if (!(await fs.pathExists(configPath))) {
8
+ return { updated: false, details: 'webpack.config.js not found' };
9
+ }
10
+
11
+ const existing = await fs.readFile(configPath, 'utf8');
12
+ if (/devServer\s*:\s*{[\s\S]*https\s*:/m.test(existing)) {
13
+ return { updated: false, details: 'already configured' };
14
+ }
15
+
16
+ const injection = `\nconst fs = require('fs');\n`;
17
+ const httpsBlock = `https: { cert: fs.readFileSync('${certPath.replace(/\\/g, '/')}'), key: fs.readFileSync('${keyPath.replace(/\\/g, '/')}') },`;
18
+
19
+ let updated = existing;
20
+ if (!updated.includes("const fs = require('fs');")) {
21
+ updated = `${injection}${updated}`;
22
+ }
23
+
24
+ updated = updated.replace(/devServer\s*:\s*{/, `devServer: {\n ${httpsBlock}`);
25
+ await fs.writeFile(configPath, updated);
26
+
27
+ return { updated: true, details: 'webpack.config.js updated' };
28
+ }
29
+
30
+ module.exports = { configureWebpack };