@stagepass/cli 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stagepass/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local Development Orchestrator for Webflow",
5
5
  "type": "module",
6
6
  "main": "bin/index.js",
@@ -12,7 +12,10 @@
12
12
  "src"
13
13
  ],
14
14
  "scripts": {
15
- "start": "node bin/index.js"
15
+ "start": "node bin/index.js",
16
+ "build": "echo 'No build step required for CLI package'",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
16
19
  },
17
20
  "keywords": [
18
21
  "webflow",
@@ -38,6 +41,9 @@
38
41
  "ora": "^7.0.1",
39
42
  "sudo-prompt": "^9.2.1"
40
43
  },
44
+ "devDependencies": {
45
+ "vitest": "^2.1.0"
46
+ },
41
47
  "engines": {
42
48
  "node": ">=18.0.0"
43
49
  }
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { link } from '../link.js';
3
+ import fs from 'fs-extra';
4
+ import { execa } from 'execa';
5
+ import os from 'os';
6
+ import path from 'path';
7
+
8
+ // Mock dependencies
9
+ vi.mock('fs-extra');
10
+ vi.mock('execa');
11
+ vi.mock('ora', () => ({
12
+ default: vi.fn(() => ({
13
+ start: vi.fn(() => ({
14
+ succeed: vi.fn(),
15
+ fail: vi.fn(),
16
+ warn: vi.fn(),
17
+ })),
18
+ })),
19
+ }));
20
+
21
+ describe('link command', () => {
22
+ const mockCaddyDir = path.join(os.homedir(), '.stagepass');
23
+ const mockCaddyFile = path.join(mockCaddyDir, 'Caddyfile');
24
+ const originalCwd = process.cwd;
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ process.cwd = vi.fn(() => '/test/project');
29
+ });
30
+
31
+ afterEach(() => {
32
+ process.cwd = originalCwd;
33
+ });
34
+
35
+ it('should use current directory name as domain if not provided', async () => {
36
+ fs.pathExists.mockResolvedValue(false);
37
+ fs.ensureDir.mockResolvedValue();
38
+ fs.writeFile.mockResolvedValue();
39
+ execa.mockResolvedValue({});
40
+
41
+ await link(undefined, {});
42
+
43
+ expect(fs.writeFile).toHaveBeenCalled();
44
+ const writtenContent = fs.writeFile.mock.calls[0][1];
45
+ expect(writtenContent).toContain('project.sp');
46
+ });
47
+
48
+ it('should add .sp extension if missing', async () => {
49
+ fs.pathExists.mockResolvedValue(false);
50
+ fs.ensureDir.mockResolvedValue();
51
+ fs.writeFile.mockResolvedValue();
52
+ execa.mockResolvedValue({});
53
+
54
+ await link('my-domain', {});
55
+
56
+ const writtenContent = fs.writeFile.mock.calls[0][1];
57
+ expect(writtenContent).toContain('my-domain.sp');
58
+ });
59
+
60
+ it('should replace existing domain block if already linked', async () => {
61
+ const existingContent = `
62
+ # START: test.sp
63
+ test.sp {
64
+ root * "/old/path"
65
+ }
66
+ # END: test.sp
67
+ `;
68
+
69
+ fs.pathExists.mockResolvedValue(true);
70
+ fs.readFile.mockResolvedValue(existingContent);
71
+ fs.ensureDir.mockResolvedValue();
72
+ fs.writeFile.mockResolvedValue();
73
+ execa.mockResolvedValue({});
74
+
75
+ await link('test', {});
76
+
77
+ expect(fs.writeFile).toHaveBeenCalled();
78
+ const writtenContent = fs.writeFile.mock.calls[0][1];
79
+ expect(writtenContent).toContain('test.sp');
80
+ expect(writtenContent).toContain('/test/project');
81
+ // Should not contain old path
82
+ expect(writtenContent).not.toContain('/old/path');
83
+ });
84
+
85
+ it('should append new domain block if not exists', async () => {
86
+ const existingContent = '# Some existing config';
87
+
88
+ fs.pathExists.mockResolvedValue(true);
89
+ fs.readFile.mockResolvedValue(existingContent);
90
+ fs.ensureDir.mockResolvedValue();
91
+ fs.writeFile.mockResolvedValue();
92
+ execa.mockResolvedValue({});
93
+
94
+ await link('new-domain', {});
95
+
96
+ expect(fs.writeFile).toHaveBeenCalled();
97
+ const writtenContent = fs.writeFile.mock.calls[0][1];
98
+ expect(writtenContent).toContain('Some existing config');
99
+ expect(writtenContent).toContain('new-domain.sp');
100
+ });
101
+
102
+ it('should handle Caddy reload failure gracefully', async () => {
103
+ fs.pathExists.mockResolvedValue(false);
104
+ fs.ensureDir.mockResolvedValue();
105
+ fs.writeFile.mockResolvedValue();
106
+ execa.mockRejectedValue(new Error('Caddy not running'));
107
+
108
+ await link('test', {});
109
+
110
+ // Should still write config even if reload fails
111
+ expect(fs.writeFile).toHaveBeenCalled();
112
+ });
113
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { reload } from '../reload.js';
3
+ import fs from 'fs-extra';
4
+ import { execa } from 'execa';
5
+ import os from 'os';
6
+ import path from 'path';
7
+
8
+ // Mock dependencies
9
+ vi.mock('fs-extra');
10
+ vi.mock('execa');
11
+ vi.mock('ora', () => ({
12
+ default: vi.fn(() => ({
13
+ start: vi.fn(() => ({
14
+ succeed: vi.fn(),
15
+ fail: vi.fn(),
16
+ })),
17
+ })),
18
+ }));
19
+
20
+ describe('reload command', () => {
21
+ const mockCaddyDir = path.join(os.homedir(), '.stagepass');
22
+ const mockCaddyFile = path.join(mockCaddyDir, 'Caddyfile');
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ it('should reload Caddy when Caddyfile exists', async () => {
29
+ fs.pathExists.mockResolvedValue(true);
30
+ execa.mockResolvedValue({});
31
+
32
+ await reload({});
33
+
34
+ expect(execa).toHaveBeenCalledWith(
35
+ 'caddy',
36
+ ['reload', '--config', mockCaddyFile],
37
+ expect.any(Object)
38
+ );
39
+ });
40
+
41
+ it('should fail if Caddyfile does not exist', async () => {
42
+ fs.pathExists.mockResolvedValue(false);
43
+
44
+ await reload({});
45
+
46
+ expect(execa).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it('should fail if Caddy is not installed', async () => {
50
+ fs.pathExists.mockResolvedValue(true);
51
+ execa.mockRejectedValueOnce(new Error('caddy: command not found'));
52
+
53
+ await reload({});
54
+
55
+ // Should not attempt reload if version check fails
56
+ expect(execa).toHaveBeenCalledTimes(1);
57
+ expect(execa).toHaveBeenCalledWith('caddy', ['version'], expect.any(Object));
58
+ });
59
+
60
+ it('should use verbose stdio when verbose option is set', async () => {
61
+ fs.pathExists.mockResolvedValue(true);
62
+ execa.mockResolvedValue({});
63
+
64
+ await reload({ verbose: true });
65
+
66
+ expect(execa).toHaveBeenCalledWith(
67
+ 'caddy',
68
+ ['reload', '--config', mockCaddyFile],
69
+ expect.objectContaining({ stdio: 'inherit' })
70
+ );
71
+ });
72
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { unlink } from '../unlink.js';
3
+ import fs from 'fs-extra';
4
+ import { execa } from 'execa';
5
+ import os from 'os';
6
+ import path from 'path';
7
+
8
+ // Mock dependencies
9
+ vi.mock('fs-extra');
10
+ vi.mock('execa');
11
+ vi.mock('ora', () => ({
12
+ default: vi.fn(() => ({
13
+ start: vi.fn(() => ({
14
+ succeed: vi.fn(),
15
+ fail: vi.fn(),
16
+ warn: vi.fn(),
17
+ })),
18
+ })),
19
+ }));
20
+
21
+ describe('unlink command', () => {
22
+ const mockCaddyDir = path.join(os.homedir(), '.stagepass');
23
+ const mockCaddyFile = path.join(mockCaddyDir, 'Caddyfile');
24
+
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ it('should remove domain block from Caddyfile', async () => {
30
+ const existingContent = `
31
+ # START: test.sp
32
+ test.sp {
33
+ root * "/some/path"
34
+ }
35
+ # END: test.sp
36
+
37
+ # START: other.sp
38
+ other.sp {
39
+ root * "/other/path"
40
+ }
41
+ # END: other.sp
42
+ `;
43
+
44
+ fs.pathExists.mockResolvedValue(true);
45
+ fs.readFile.mockResolvedValue(existingContent);
46
+ fs.writeFile.mockResolvedValue();
47
+ execa.mockResolvedValue({});
48
+
49
+ await unlink('test', {});
50
+
51
+ expect(fs.writeFile).toHaveBeenCalled();
52
+ const writtenContent = fs.writeFile.mock.calls[0][1];
53
+ expect(writtenContent).not.toContain('test.sp');
54
+ expect(writtenContent).toContain('other.sp');
55
+ });
56
+
57
+ it('should use current directory name if domain not provided', async () => {
58
+ const originalCwd = process.cwd;
59
+ process.cwd = vi.fn(() => '/test/my-project');
60
+
61
+ const existingContent = `
62
+ # START: my-project.sp
63
+ my-project.sp {
64
+ root * "/test/my-project"
65
+ }
66
+ # END: my-project.sp
67
+ `;
68
+
69
+ fs.pathExists.mockResolvedValue(true);
70
+ fs.readFile.mockResolvedValue(existingContent);
71
+ fs.writeFile.mockResolvedValue();
72
+ execa.mockResolvedValue({});
73
+
74
+ await unlink(undefined, {});
75
+
76
+ const writtenContent = fs.writeFile.mock.calls[0][1];
77
+ expect(writtenContent).not.toContain('my-project.sp');
78
+
79
+ process.cwd = originalCwd;
80
+ });
81
+
82
+ it('should fail if Caddyfile does not exist', async () => {
83
+ fs.pathExists.mockResolvedValue(false);
84
+
85
+ await unlink('test', {});
86
+
87
+ expect(fs.writeFile).not.toHaveBeenCalled();
88
+ expect(execa).not.toHaveBeenCalled();
89
+ });
90
+ });
@@ -36,11 +36,22 @@ export async function link(domain, options = {}) {
36
36
  const newBlock = `
37
37
  ${blockStart}
38
38
  ${targetDomain} {
39
+ @options method OPTIONS
40
+ handle @options {
41
+ header Access-Control-Allow-Origin *
42
+ header Access-Control-Allow-Methods *
43
+ header Access-Control-Allow-Headers *
44
+ header Access-Control-Max-Age 86400
45
+ respond 204
46
+ }
47
+
39
48
  root * "${currentDir}"
40
49
  php_fastcgi 127.0.0.1:9000
41
50
  file_server
42
51
  tls internal
43
52
  header Access-Control-Allow-Origin *
53
+ header Access-Control-Allow-Methods *
54
+ header Access-Control-Allow-Headers *
44
55
  }
45
56
  ${blockEnd}`;
46
57