@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 +79 -0
- package/package.json +44 -0
- package/src/commands/link.js +79 -0
- package/src/commands/reload.js +41 -0
- package/src/commands/setup.js +111 -0
- package/src/commands/start.js +133 -0
- package/src/commands/stop.js +41 -0
- package/src/commands/unlink.js +71 -0
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
|
+
}
|