@stagepass/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.
package/bin/index.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { setup } from '../src/commands/setup.js';
6
+ import { link } from '../src/commands/link.js';
7
+ import { unlink } from '../src/commands/unlink.js';
8
+ import { start } from '../src/commands/start.js';
9
+ import { stop } from '../src/commands/stop.js';
10
+ import { reload } from '../src/commands/reload.js';
11
+
12
+ program
13
+ .name('stagepass')
14
+ .description('The missing link between Webflow and local dev.')
15
+ .version('1.0.0');
16
+
17
+ // Command: SETUP
18
+ program
19
+ .command('setup')
20
+ .description('Install dependencies (Caddy, PHP, Dnsmasq) and configure .sp domain')
21
+ .option('-v, --verbose', 'Show detailed installation logs')
22
+ .action(async (options) => {
23
+ try {
24
+ await setup(options);
25
+ } catch (error) {
26
+ console.error(chalk.red('\nSetup failed.'));
27
+ if (options.verbose) console.error(error);
28
+ else console.error(chalk.dim('Run with -v for details.'));
29
+ }
30
+ });
31
+
32
+ // Command: LINK
33
+ program
34
+ .command('link')
35
+ .description('Link current directory to a .sp domain')
36
+ .argument('[domain]', 'Domain name (optional)')
37
+ .option('-v, --verbose', 'Show Caddy reload output')
38
+ .action(async (domain, options) => {
39
+ await link(domain, options);
40
+ });
41
+
42
+ // Command: UNLINK
43
+ program
44
+ .command('unlink')
45
+ .description('Unlink current directory from .sp domain')
46
+ .argument('[domain]', 'Domain name (optional, defaults to current folder name)')
47
+ .option('-v, --verbose', 'Show Caddy reload output')
48
+ .action(async (domain, options) => {
49
+ await unlink(domain, options);
50
+ });
51
+
52
+ // Command: RELOAD
53
+ program
54
+ .command('reload')
55
+ .description('Reload Caddy configuration')
56
+ .option('-v, --verbose', 'Show Caddy reload output')
57
+ .action(async (options) => {
58
+ await reload(options);
59
+ });
60
+
61
+ // Command: START
62
+ program
63
+ .command('start')
64
+ .description('Start background services (Caddy & PHP)')
65
+ .option('-v, --verbose', 'Show detailed Caddy server logs')
66
+ .action(async (options) => {
67
+ await start(options);
68
+ });
69
+
70
+ // Command: STOP
71
+ program
72
+ .command('stop')
73
+ .description('Stop all background services')
74
+ .option('-v, --verbose', 'Show detailed stop logs')
75
+ .action(async (options) => {
76
+ await stop(options);
77
+ });
78
+
79
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@stagepass/cli",
3
+ "version": "1.0.0",
4
+ "description": "Local Development Orchestrator for Webflow",
5
+ "type": "module",
6
+ "main": "bin/index.js",
7
+ "bin": {
8
+ "stagepass": "./bin/index.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/index.js"
16
+ },
17
+ "keywords": [
18
+ "webflow",
19
+ "development",
20
+ "local",
21
+ "proxy",
22
+ "ssl",
23
+ "caddy",
24
+ "hot-reload"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/arobertherz/stagepass.git",
29
+ "directory": "packages/cli"
30
+ },
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "chalk": "^5.3.0",
34
+ "commander": "^11.1.0",
35
+ "execa": "^8.0.0",
36
+ "fs-extra": "^11.2.0",
37
+ "inquirer": "^9.2.0",
38
+ "ora": "^7.0.1",
39
+ "sudo-prompt": "^9.2.1"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import chalk from 'chalk';
5
+ import { execa } from 'execa';
6
+ import ora from 'ora';
7
+
8
+ export async function link(domain, options = {}) {
9
+ const currentDir = process.cwd();
10
+
11
+ // Domain logic
12
+ let targetDomain = domain;
13
+ if (!targetDomain) {
14
+ targetDomain = path.basename(currentDir);
15
+ }
16
+ targetDomain = targetDomain.replace(/\.sp$/, '') + '.sp';
17
+
18
+ // Config paths
19
+ const caddyDir = path.join(os.homedir(), '.stagepass');
20
+ const caddyFilePath = path.join(caddyDir, 'Caddyfile');
21
+
22
+ const spinner = ora(`Linking ${chalk.bold(targetDomain)}...`).start();
23
+
24
+ // 1. Write config
25
+ try {
26
+ await fs.ensureDir(caddyDir);
27
+
28
+ let content = '';
29
+ if (await fs.pathExists(caddyFilePath)) {
30
+ content = await fs.readFile(caddyFilePath, 'utf-8');
31
+ }
32
+
33
+ const blockStart = `# START: ${targetDomain}`;
34
+ const blockEnd = `# END: ${targetDomain}`;
35
+
36
+ const newBlock = `
37
+ ${blockStart}
38
+ ${targetDomain} {
39
+ root * "${currentDir}"
40
+ php_fastcgi 127.0.0.1:9000
41
+ file_server
42
+ tls internal
43
+ header Access-Control-Allow-Origin *
44
+ }
45
+ ${blockEnd}`;
46
+
47
+ const regex = new RegExp(`${escapeRegExp(blockStart)}[\\s\\S]*?${escapeRegExp(blockEnd)}`, 'g');
48
+
49
+ if (regex.test(content)) {
50
+ content = content.replace(regex, newBlock.trim());
51
+ } else {
52
+ content += `\n${newBlock.trim()}\n`;
53
+ }
54
+
55
+ await fs.writeFile(caddyFilePath, content.trim());
56
+ } catch (e) {
57
+ spinner.fail('Failed to write config.');
58
+ if (options.verbose) console.error(e);
59
+ return;
60
+ }
61
+
62
+ // 2. Reload Caddy
63
+ try {
64
+ // If verbose is ON, show output. If OFF, ignore it (pipe).
65
+ const stdioMode = options.verbose ? 'inherit' : 'ignore';
66
+
67
+ await execa('caddy', ['reload', '--config', caddyFilePath], { stdio: stdioMode });
68
+
69
+ spinner.succeed(`Linked: https://${targetDomain} -> ${currentDir}`);
70
+ } catch (error) {
71
+ spinner.warn('Config written, but Caddy reload failed.');
72
+ console.log(chalk.dim(' Is Stagepass running? Try: stagepass start'));
73
+ if (options.verbose) console.error(error);
74
+ }
75
+ }
76
+
77
+ function escapeRegExp(string) {
78
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ }
@@ -0,0 +1,41 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import chalk from 'chalk';
4
+ import { execa } from 'execa';
5
+ import ora from 'ora';
6
+ import fs from 'fs-extra';
7
+
8
+ export async function reload(options = {}) {
9
+ const caddyDir = path.join(os.homedir(), '.stagepass');
10
+ const caddyFilePath = path.join(caddyDir, 'Caddyfile');
11
+
12
+ const spinner = ora('Reloading Caddy...').start();
13
+
14
+ // Check if Caddyfile exists
15
+ if (!await fs.pathExists(caddyFilePath)) {
16
+ spinner.fail('No Caddyfile found. Run "stagepass link" first.');
17
+ return;
18
+ }
19
+
20
+ // Check if Caddy is running
21
+ try {
22
+ await execa('caddy', ['version'], { stdio: 'ignore' });
23
+ } catch (e) {
24
+ spinner.fail('Caddy is not installed or not accessible.');
25
+ console.log(chalk.dim(' Run "stagepass setup" to install dependencies.'));
26
+ return;
27
+ }
28
+
29
+ // Reload Caddy
30
+ try {
31
+ const stdioMode = options.verbose ? 'inherit' : 'ignore';
32
+
33
+ await execa('caddy', ['reload', '--config', caddyFilePath], { stdio: stdioMode });
34
+
35
+ spinner.succeed('Caddy reloaded successfully.');
36
+ } catch (error) {
37
+ spinner.fail('Failed to reload Caddy.');
38
+ console.log(chalk.dim(' Is Stagepass running? Try: stagepass start'));
39
+ if (options.verbose) console.error(error);
40
+ }
41
+ }
@@ -0,0 +1,111 @@
1
+ import { execa } from 'execa';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import sudo from 'sudo-prompt';
8
+
9
+ const sudoExec = (command) => {
10
+ return new Promise((resolve, reject) => {
11
+ sudo.exec(command, { name: 'Stagepass Setup' }, (error, stdout, stderr) => {
12
+ if (error) reject(error);
13
+ else resolve(stdout);
14
+ });
15
+ });
16
+ };
17
+
18
+ export async function setup(options = {}) {
19
+ console.log(chalk.bold('\nšŸ›  Initializing Stagepass Environment...'));
20
+
21
+ const execOpts = options.verbose ? { stdio: 'inherit' } : {};
22
+
23
+ // 1. Check Homebrew
24
+ const brewSpinner = ora('Checking system requirements...').start();
25
+ try {
26
+ await execa('brew', ['--version']);
27
+ brewSpinner.succeed('System requirements met.');
28
+ } catch (e) {
29
+ brewSpinner.fail('Homebrew is missing.');
30
+ return;
31
+ }
32
+
33
+ // 2. Install Dependencies
34
+ const installSpinner = ora('Installing dependencies...').start();
35
+ try {
36
+ await execa('brew', ['install', 'caddy', 'php', 'dnsmasq'], execOpts);
37
+ installSpinner.succeed('Core dependencies installed.');
38
+ } catch (e) {
39
+ installSpinner.fail('Installation failed.');
40
+ throw e;
41
+ }
42
+
43
+ // 3. Configure Dnsmasq (DIRECT WRITE STRATEGY)
44
+ const dnsSpinner = ora('Configuring local DNS (.sp)...').start();
45
+ try {
46
+ const { stdout: brewPrefixRaw } = await execa('brew', ['--prefix']);
47
+ const brewPrefix = brewPrefixRaw.trim();
48
+ const mainConfFile = path.join(brewPrefix, 'etc', 'dnsmasq.conf');
49
+
50
+ // Ensure config exists
51
+ if (!await fs.pathExists(mainConfFile)) {
52
+ await fs.writeFile(mainConfFile, '# Stagepass Config\n');
53
+ }
54
+
55
+ let content = await fs.readFile(mainConfFile, 'utf-8');
56
+ const rule = 'address=/.sp/127.0.0.1';
57
+
58
+ // Check if rule already exists
59
+ if (!content.includes(rule)) {
60
+ await fs.appendFile(mainConfFile, `\n\n# Stagepass Rule\n${rule}\n`);
61
+ }
62
+
63
+ dnsSpinner.succeed('Local DNS configured.');
64
+ } catch (e) {
65
+ dnsSpinner.fail('DNS config failed.');
66
+ if (options.verbose) console.error(e);
67
+ throw e;
68
+ }
69
+
70
+ // 4. Configure System Resolver (Root)
71
+ const resolverFile = '/etc/resolver/sp';
72
+ if (!fs.existsSync(resolverFile)) {
73
+ console.log(chalk.yellow(' sudo access required for system resolver...'));
74
+ try {
75
+ const cmd = `mkdir -p /etc/resolver && echo "nameserver 127.0.0.1" > ${resolverFile}`;
76
+ await sudoExec(cmd);
77
+ console.log(chalk.green(' āœ” Root permissions granted.'));
78
+ } catch (e) {
79
+ console.log(chalk.red(' āŒ Permission denied.'));
80
+ return;
81
+ }
82
+ }
83
+
84
+ // 5. Restart DNS Services
85
+ const restartSpinner = ora('Restarting DNS services...').start();
86
+ try {
87
+ // Dnsmasq Restart
88
+ try {
89
+ await execa('sudo', ['brew', 'services', 'restart', 'dnsmasq'], execOpts);
90
+ } catch (e) {
91
+ await execa('brew', ['services', 'restart', 'dnsmasq'], execOpts);
92
+ }
93
+
94
+ // DNS Cache Flush
95
+ try { await execa('sudo', ['killall', '-HUP', 'mDNSResponder']); } catch(e) {}
96
+
97
+ restartSpinner.succeed('DNS services active.');
98
+ } catch(e) {
99
+ restartSpinner.warn('Could not restart Dnsmasq automatically.');
100
+ }
101
+
102
+ // 6. Init Caddyfile
103
+ const caddyDir = path.join(os.homedir(), '.stagepass');
104
+ await fs.ensureDir(caddyDir);
105
+ const caddyFile = path.join(caddyDir, 'Caddyfile');
106
+ if (!await fs.pathExists(caddyFile)) {
107
+ await fs.writeFile(caddyFile, '# Stagepass Global Config\n');
108
+ }
109
+
110
+ console.log(chalk.green('\nāœ… Setup complete!'));
111
+ }
@@ -0,0 +1,133 @@
1
+ import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import fs from 'fs-extra';
6
+ import inquirer from 'inquirer';
7
+ import ora from 'ora';
8
+
9
+ // --- HELPERS ---
10
+
11
+ async function checkPort443() {
12
+ try {
13
+ const cmd = "sudo lsof -iTCP:443 -n -P | grep LISTEN | awk '{print $2}'";
14
+ const { stdout: rawOutput } = await execa(cmd, { shell: true });
15
+
16
+ if (!rawOutput || !rawOutput.trim()) return null;
17
+
18
+ const pids = rawOutput.trim().split('\n');
19
+ const pid = pids[0].trim();
20
+ if (!pid) return null;
21
+
22
+ const { stdout: commandPath } = await execa('ps', ['-p', pid, '-o', 'comm=']);
23
+ const name = path.basename(commandPath.trim());
24
+
25
+ const ignoreList = ['Google Chrome', 'Chrome Helper', 'OneDrive', 'Music', 'Safari', 'caddy'];
26
+ if (ignoreList.some(ignore => name.includes(ignore))) {
27
+ return null;
28
+ }
29
+
30
+ return { pid, name };
31
+ } catch (e) {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function killProcess(proc) {
37
+ try {
38
+ if (proc.name.includes('nginx')) {
39
+ try { await execa('valet', ['stop']); return true; } catch (e) {}
40
+ try { await execa('brew', ['services', 'stop', 'nginx']); return true; } catch (e) {}
41
+ }
42
+ if (proc.name.includes('httpd')) {
43
+ await execa('sudo', ['apachectl', 'stop']);
44
+ return true;
45
+ }
46
+ await execa('sudo', ['kill', '-9', proc.pid]);
47
+ return true;
48
+ } catch (error) {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ // --- MAIN COMMAND ---
54
+
55
+ export async function start(options = {}) {
56
+ const caddyDir = path.join(os.homedir(), '.stagepass');
57
+ const caddyFile = path.join(caddyDir, 'Caddyfile');
58
+ const logFile = path.join(caddyDir, 'server.log');
59
+
60
+ console.log(chalk.bold('\nšŸš€ Starting Stagepass...'));
61
+
62
+ // 1. Sudo warm-up (interactive, before spinner starts)
63
+ try {
64
+ await execa('sudo', ['-v'], { stdio: 'inherit' });
65
+ } catch (e) {
66
+ console.log(chalk.red('āŒ Root permissions required.'));
67
+ return;
68
+ }
69
+
70
+ // 2. Port check (interactive)
71
+ const conflict = await checkPort443();
72
+ if (conflict) {
73
+ console.log(chalk.yellow(`\nāš ļø Port 443 is blocked by: ${chalk.bold(conflict.name)}`));
74
+ const { shouldKill } = await inquirer.prompt([{
75
+ type: 'confirm',
76
+ name: 'shouldKill',
77
+ message: 'Auto-fix conflict?',
78
+ default: true
79
+ }]);
80
+
81
+ if (shouldKill) {
82
+ const killSpinner = ora('Freeing port...').start();
83
+ const success = await killProcess(conflict);
84
+ if (success) killSpinner.succeed('Port freed.');
85
+ else {
86
+ killSpinner.fail('Could not free port.');
87
+ return;
88
+ }
89
+ } else {
90
+ return;
91
+ }
92
+ }
93
+
94
+ // 3. Boot services
95
+ const spinner = ora('Booting background services...').start();
96
+
97
+ // Prepare logging (standard mode)
98
+ let stdioMode = 'ignore';
99
+ if (options.verbose) {
100
+ spinner.stop();
101
+ console.log(chalk.dim('--- Verbose Logs ---'));
102
+ stdioMode = 'inherit';
103
+ } else {
104
+ // Redirect stdout/stderr to logfile
105
+ const logStream = fs.openSync(logFile, 'w');
106
+ stdioMode = ['ignore', logStream, logStream];
107
+ }
108
+
109
+ try {
110
+ // Start PHP
111
+ try {
112
+ await execa('brew', ['services', 'restart', 'php'], { stdio: stdioMode === 'inherit' ? 'inherit' : 'ignore' });
113
+ } catch (e) { /* ignore php errors */ }
114
+
115
+ // Start Caddy
116
+ // Stop old first
117
+ try { await execa('caddy', ['stop']); } catch (e) {}
118
+
119
+ await execa('caddy', ['start', '--config', caddyFile], { stdio: stdioMode });
120
+
121
+ if (!options.verbose) {
122
+ spinner.succeed(chalk.green('Stagepass is active.'));
123
+ console.log(chalk.dim(` Logs: ${logFile}`));
124
+ }
125
+
126
+ } catch (error) {
127
+ spinner.fail('Failed to start.');
128
+ if (!options.verbose) {
129
+ console.log(chalk.red('See error log for details:'));
130
+ console.log(chalk.dim(logFile));
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,41 @@
1
+ import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+
5
+ export async function stop(options = {}) {
6
+ console.log(chalk.bold('\nšŸ›‘ Stopping Stagepass...'));
7
+
8
+ const spinner = ora('Shutting down services...').start();
9
+ const stdioMode = options.verbose ? 'inherit' : 'ignore';
10
+
11
+ if (options.verbose) {
12
+ spinner.stop();
13
+ console.log(chalk.dim('--- Verbose Logs ---'));
14
+ }
15
+
16
+ try {
17
+ // 1. Stop Caddy
18
+ try {
19
+ await execa('caddy', ['stop'], { stdio: stdioMode });
20
+ } catch (e) {
21
+ // Ignore if already stopped
22
+ }
23
+
24
+ // 2. Stop PHP (optional, but cleaner)
25
+ try {
26
+ await execa('brew', ['services', 'stop', 'php'], { stdio: stdioMode });
27
+ } catch (e) {
28
+ // Ignore
29
+ }
30
+
31
+ if (!options.verbose) {
32
+ spinner.succeed(chalk.green('Services stopped.'));
33
+ } else {
34
+ console.log(chalk.green('āœ” Services stopped.'));
35
+ }
36
+
37
+ } catch (error) {
38
+ if (!options.verbose) spinner.fail('Error stopping services.');
39
+ console.error(chalk.red(error.message));
40
+ }
41
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import chalk from 'chalk';
5
+ import { execa } from 'execa';
6
+ import ora from 'ora';
7
+
8
+ function escapeRegExp(string) {
9
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
11
+
12
+ export async function unlink(domain, options = {}) {
13
+ const currentDir = process.cwd();
14
+
15
+ // Domain Logic
16
+ let targetDomain = domain;
17
+ if (!targetDomain) {
18
+ targetDomain = path.basename(currentDir);
19
+ }
20
+ targetDomain = targetDomain.replace(/\.sp$/, '') + '.sp';
21
+
22
+ // Config Paths
23
+ const caddyDir = path.join(os.homedir(), '.stagepass');
24
+ const caddyFilePath = path.join(caddyDir, 'Caddyfile');
25
+
26
+ const spinner = ora(`Unlinking ${chalk.bold(targetDomain)}...`).start();
27
+
28
+ // 1. Read Config
29
+ try {
30
+ if (!await fs.pathExists(caddyFilePath)) {
31
+ spinner.warn('No Caddyfile found. Nothing to unlink.');
32
+ return;
33
+ }
34
+
35
+ let content = await fs.readFile(caddyFilePath, 'utf-8');
36
+
37
+ const blockStart = `# START: ${targetDomain}`;
38
+ const blockEnd = `# END: ${targetDomain}`;
39
+ const regex = new RegExp(`${escapeRegExp(blockStart)}[\\s\\S]*?${escapeRegExp(blockEnd)}`, 'g');
40
+
41
+ if (!regex.test(content)) {
42
+ spinner.warn(`Domain ${chalk.bold(targetDomain)} is not linked.`);
43
+ return;
44
+ }
45
+
46
+ // Remove the block
47
+ content = content.replace(regex, '').trim();
48
+
49
+ // Clean up multiple empty lines
50
+ content = content.replace(/\n{3,}/g, '\n\n');
51
+
52
+ await fs.writeFile(caddyFilePath, content);
53
+ } catch (e) {
54
+ spinner.fail('Failed to update config.');
55
+ if (options.verbose) console.error(e);
56
+ return;
57
+ }
58
+
59
+ // 2. Reload Caddy
60
+ try {
61
+ const stdioMode = options.verbose ? 'inherit' : 'ignore';
62
+
63
+ await execa('caddy', ['reload', '--config', caddyFilePath], { stdio: stdioMode });
64
+
65
+ spinner.succeed(`Unlinked: ${targetDomain}`);
66
+ } catch (error) {
67
+ spinner.warn('Config updated, but Caddy reload failed.');
68
+ console.log(chalk.dim(' Is Stagepass running? Try: stagepass start'));
69
+ if (options.verbose) console.error(error);
70
+ }
71
+ }