hostfn 0.1.5 → 0.1.7
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 +1 -2
- package/dist/index.js +0 -0
- package/package.json +14 -3
- package/src/__tests__/core/sync.test.ts +42 -0
- package/src/__tests__/core/workspace.test.ts +180 -0
- package/src/commands/deploy.ts +122 -83
- package/src/commands/init.ts +1 -1
- package/src/config/loader.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/core/ssh.ts +2 -0
- package/src/core/sync.ts +64 -23
- package/src/core/workspace.ts +111 -6
- package/dist/__tests__/core/backup.test.d.ts +0 -2
- package/dist/__tests__/core/backup.test.d.ts.map +0 -1
- package/dist/__tests__/core/backup.test.js +0 -108
- package/dist/__tests__/core/backup.test.js.map +0 -1
- package/dist/__tests__/core/health.test.d.ts +0 -2
- package/dist/__tests__/core/health.test.d.ts.map +0 -1
- package/dist/__tests__/core/health.test.js +0 -97
- package/dist/__tests__/core/health.test.js.map +0 -1
- package/dist/__tests__/core/lock.test.d.ts +0 -2
- package/dist/__tests__/core/lock.test.d.ts.map +0 -1
- package/dist/__tests__/core/lock.test.js +0 -136
- package/dist/__tests__/core/lock.test.js.map +0 -1
- package/dist/__tests__/core/nginx-multi-domain.test.d.ts +0 -2
- package/dist/__tests__/core/nginx-multi-domain.test.d.ts.map +0 -1
- package/dist/__tests__/core/nginx-multi-domain.test.js +0 -158
- package/dist/__tests__/core/nginx-multi-domain.test.js.map +0 -1
- package/dist/__tests__/runtimes/pm2.test.d.ts +0 -2
- package/dist/__tests__/runtimes/pm2.test.d.ts.map +0 -1
- package/dist/__tests__/runtimes/pm2.test.js +0 -111
- package/dist/__tests__/runtimes/pm2.test.js.map +0 -1
- package/dist/__tests__/utils/validation.test.d.ts +0 -2
- package/dist/__tests__/utils/validation.test.d.ts.map +0 -1
- package/dist/__tests__/utils/validation.test.js +0 -136
- package/dist/__tests__/utils/validation.test.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -11
- package/dist/commands/deploy.d.ts.map +0 -1
- package/dist/commands/deploy.js +0 -636
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/env.d.ts +0 -21
- package/dist/commands/env.d.ts.map +0 -1
- package/dist/commands/env.js +0 -317
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/expose.d.ts +0 -6
- package/dist/commands/expose.d.ts.map +0 -1
- package/dist/commands/expose.js +0 -379
- package/dist/commands/expose.js.map +0 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -175
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -10
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -75
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/rollback.d.ts +0 -6
- package/dist/commands/rollback.d.ts.map +0 -1
- package/dist/commands/rollback.js +0 -113
- package/dist/commands/rollback.js.map +0 -1
- package/dist/commands/server/info.d.ts +0 -2
- package/dist/commands/server/info.d.ts.map +0 -1
- package/dist/commands/server/info.js +0 -104
- package/dist/commands/server/info.js.map +0 -1
- package/dist/commands/server/setup.d.ts +0 -11
- package/dist/commands/server/setup.d.ts.map +0 -1
- package/dist/commands/server/setup.js +0 -161
- package/dist/commands/server/setup.js.map +0 -1
- package/dist/commands/status.d.ts +0 -6
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -120
- package/dist/commands/status.js.map +0 -1
- package/dist/config/loader.d.ts +0 -21
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js +0 -54
- package/dist/config/loader.js.map +0 -1
- package/dist/config/schema.d.ts +0 -323
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/config/schema.js +0 -108
- package/dist/config/schema.js.map +0 -1
- package/dist/core/backup.d.ts +0 -34
- package/dist/core/backup.d.ts.map +0 -1
- package/dist/core/backup.js +0 -95
- package/dist/core/backup.js.map +0 -1
- package/dist/core/health.d.ts +0 -31
- package/dist/core/health.d.ts.map +0 -1
- package/dist/core/health.js +0 -78
- package/dist/core/health.js.map +0 -1
- package/dist/core/local.d.ts +0 -19
- package/dist/core/local.d.ts.map +0 -1
- package/dist/core/local.js +0 -50
- package/dist/core/local.js.map +0 -1
- package/dist/core/lock.d.ts +0 -28
- package/dist/core/lock.d.ts.map +0 -1
- package/dist/core/lock.js +0 -89
- package/dist/core/lock.js.map +0 -1
- package/dist/core/nginx.d.ts +0 -43
- package/dist/core/nginx.d.ts.map +0 -1
- package/dist/core/nginx.js +0 -131
- package/dist/core/nginx.js.map +0 -1
- package/dist/core/ssh.d.ts +0 -79
- package/dist/core/ssh.d.ts.map +0 -1
- package/dist/core/ssh.js +0 -264
- package/dist/core/ssh.js.map +0 -1
- package/dist/core/sync.d.ts +0 -25
- package/dist/core/sync.d.ts.map +0 -1
- package/dist/core/sync.js +0 -117
- package/dist/core/sync.js.map +0 -1
- package/dist/core/workspace.d.ts +0 -13
- package/dist/core/workspace.d.ts.map +0 -1
- package/dist/core/workspace.js +0 -141
- package/dist/core/workspace.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/runtimes/base.d.ts +0 -115
- package/dist/runtimes/base.d.ts.map +0 -1
- package/dist/runtimes/base.js +0 -16
- package/dist/runtimes/base.js.map +0 -1
- package/dist/runtimes/nodejs/detector.d.ts +0 -47
- package/dist/runtimes/nodejs/detector.d.ts.map +0 -1
- package/dist/runtimes/nodejs/detector.js +0 -143
- package/dist/runtimes/nodejs/detector.js.map +0 -1
- package/dist/runtimes/nodejs/index.d.ts +0 -14
- package/dist/runtimes/nodejs/index.d.ts.map +0 -1
- package/dist/runtimes/nodejs/index.js +0 -213
- package/dist/runtimes/nodejs/index.js.map +0 -1
- package/dist/runtimes/nodejs/pm2.d.ts +0 -17
- package/dist/runtimes/nodejs/pm2.d.ts.map +0 -1
- package/dist/runtimes/nodejs/pm2.js +0 -60
- package/dist/runtimes/nodejs/pm2.js.map +0 -1
- package/dist/runtimes/registry.d.ts +0 -34
- package/dist/runtimes/registry.d.ts.map +0 -1
- package/dist/runtimes/registry.js +0 -58
- package/dist/runtimes/registry.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -47
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -76
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/validation.d.ts +0 -32
- package/dist/utils/validation.d.ts.map +0 -1
- package/dist/utils/validation.js +0 -125
- package/dist/utils/validation.js.map +0 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ Create `hostfn.config.json` in your project root:
|
|
|
178
178
|
"build": {
|
|
179
179
|
"command": "npm run build",
|
|
180
180
|
"directory": "dist",
|
|
181
|
-
"nodeModules": "
|
|
181
|
+
"nodeModules": "all"
|
|
182
182
|
},
|
|
183
183
|
"start": {
|
|
184
184
|
"command": "npm start",
|
|
@@ -1133,4 +1133,3 @@ MIT
|
|
|
1133
1133
|
|
|
1134
1134
|
|
|
1135
1135
|
|
|
1136
|
-
|
package/dist/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hostfn",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"license": "MIT",
|
|
5
5
|
"description": "Universal application deployment CLI",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
"chalk": "^5.3.0",
|
|
22
22
|
"ora": "^7.0.1",
|
|
23
23
|
"inquirer": "^9.2.12",
|
|
24
|
+
"semver": "^7.7.2",
|
|
24
25
|
"zod": "^3.22.4",
|
|
25
26
|
"ssh2": "^1.15.0",
|
|
26
27
|
"dotenv": "^16.3.1",
|
|
27
28
|
"execa": "^8.0.1"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
31
|
+
"@types/semver": "^7.7.1",
|
|
30
32
|
"@types/node": "^20.10.0",
|
|
31
33
|
"@types/ssh2": "^1.15.0",
|
|
32
34
|
"@types/inquirer": "^9.0.7",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"author": "21n",
|
|
40
42
|
"repository": {
|
|
41
43
|
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/21nCo/super-functions.git"
|
|
44
|
+
"url": "git+https://github.com/21nCo/super-functions.git",
|
|
45
|
+
"directory": "hostfn/cli"
|
|
43
46
|
},
|
|
44
47
|
"bugs": {
|
|
45
48
|
"url": "https://github.com/21nCo/super-functions/issues"
|
|
@@ -50,5 +53,13 @@
|
|
|
50
53
|
],
|
|
51
54
|
"engines": {
|
|
52
55
|
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"superfunctions": {
|
|
61
|
+
"initFunction": "hostFn",
|
|
62
|
+
"schemaVersion": 1,
|
|
63
|
+
"namespace": "hostfn"
|
|
53
64
|
}
|
|
54
65
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { FileSync } from '../../core/sync.js';
|
|
3
|
+
|
|
4
|
+
const { execaMock } = vi.hoisted(() => ({
|
|
5
|
+
execaMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('execa', () => ({
|
|
9
|
+
execa: execaMock,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('FileSync.syncLocal', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
execaMock.mockReset();
|
|
15
|
+
execaMock.mockResolvedValue({
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
stdout: '',
|
|
18
|
+
stderr: '',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('applies include filters before exclude filters so rsync overrides work', async () => {
|
|
23
|
+
await FileSync.syncLocal('/tmp/source', '/tmp/destination', {
|
|
24
|
+
include: ['.env.production'],
|
|
25
|
+
exclude: ['.env.*'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(execaMock).toHaveBeenCalledWith(
|
|
29
|
+
'rsync',
|
|
30
|
+
expect.arrayContaining([
|
|
31
|
+
'--include',
|
|
32
|
+
'.env.production',
|
|
33
|
+
'--exclude',
|
|
34
|
+
'.env.*',
|
|
35
|
+
]),
|
|
36
|
+
expect.any(Object),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const args = execaMock.mock.calls[0][1] as string[];
|
|
40
|
+
expect(args.indexOf('--include')).toBeLessThan(args.indexOf('--exclude'));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { WorkspaceManager } from '../../core/workspace.js';
|
|
6
|
+
|
|
7
|
+
const execSyncMock = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock('child_process', () => ({
|
|
10
|
+
execSync: execSyncMock,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('WorkspaceManager.generateLockfile', () => {
|
|
14
|
+
let rootDir: string;
|
|
15
|
+
let serviceDir: string;
|
|
16
|
+
let targetDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
execSyncMock.mockReset();
|
|
20
|
+
rootDir = mkdtempSync(join(tmpdir(), 'hostfn-workspace-root-'));
|
|
21
|
+
serviceDir = join(rootDir, 'services', 'api');
|
|
22
|
+
targetDir = mkdtempSync(join(tmpdir(), 'hostfn-workspace-target-'));
|
|
23
|
+
|
|
24
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(rootDir, 'package.json'),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
private: true,
|
|
30
|
+
workspaces: ['services/*'],
|
|
31
|
+
}, null, 2),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(serviceDir, 'package.json'),
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
name: '@hostfn/api',
|
|
38
|
+
dependencies: {
|
|
39
|
+
commander: '^12.0.0',
|
|
40
|
+
chalk: '^5.0.0',
|
|
41
|
+
'zero-lib': '^0.2.3',
|
|
42
|
+
},
|
|
43
|
+
}, null, 2),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
writeFileSync(
|
|
47
|
+
join(rootDir, 'package-lock.json'),
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
name: 'workspace-root',
|
|
50
|
+
lockfileVersion: 3,
|
|
51
|
+
packages: {
|
|
52
|
+
'': { name: 'workspace-root' },
|
|
53
|
+
'node_modules/commander': { version: '4.1.1', name: 'commander' },
|
|
54
|
+
'services/api/node_modules/commander': { version: '12.1.0', name: 'commander' },
|
|
55
|
+
'node_modules/chalk': { version: '5.3.0', name: 'chalk' },
|
|
56
|
+
'node_modules/zero-lib': { version: '0.5.0', name: 'zero-lib' },
|
|
57
|
+
'services/api/node_modules/zero-lib': { version: '0.2.8', name: 'zero-lib' },
|
|
58
|
+
},
|
|
59
|
+
}, null, 2),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
writeFileSync(
|
|
63
|
+
join(targetDir, 'package.json'),
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
name: '@hostfn/api',
|
|
66
|
+
dependencies: {
|
|
67
|
+
commander: '^12.0.0',
|
|
68
|
+
chalk: '^5.0.0',
|
|
69
|
+
'zero-lib': '^0.2.3',
|
|
70
|
+
},
|
|
71
|
+
}, null, 2),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
77
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('pins package.json to the exact workspace-scoped versions used for lockfile generation', async () => {
|
|
81
|
+
execSyncMock.mockImplementation(() => {
|
|
82
|
+
const pinnedPackageJson = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
|
|
83
|
+
expect(pinnedPackageJson.dependencies.commander).toBe('12.1.0');
|
|
84
|
+
expect(pinnedPackageJson.dependencies.chalk).toBe('5.3.0');
|
|
85
|
+
expect(pinnedPackageJson.dependencies['zero-lib']).toBe('0.2.8');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const manager = new WorkspaceManager();
|
|
89
|
+
await manager.detectWorkspace(serviceDir);
|
|
90
|
+
await manager.generateLockfile(targetDir, serviceDir);
|
|
91
|
+
|
|
92
|
+
expect(execSyncMock).toHaveBeenCalledWith(
|
|
93
|
+
'npm install --package-lock-only --ignore-scripts',
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
cwd: targetDir,
|
|
96
|
+
stdio: 'ignore',
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const deployedPackageJson = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
|
|
101
|
+
expect(deployedPackageJson.dependencies.commander).toBe('12.1.0');
|
|
102
|
+
expect(deployedPackageJson.dependencies.chalk).toBe('5.3.0');
|
|
103
|
+
expect(deployedPackageJson.dependencies['zero-lib']).toBe('0.2.8');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('WorkspaceManager.rewritePackageJson', () => {
|
|
108
|
+
let rootDir: string;
|
|
109
|
+
let serviceDir: string;
|
|
110
|
+
let toolingDir: string;
|
|
111
|
+
let targetDir: string;
|
|
112
|
+
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
rootDir = mkdtempSync(join(tmpdir(), 'hostfn-workspace-root-'));
|
|
115
|
+
serviceDir = join(rootDir, 'services', 'api');
|
|
116
|
+
toolingDir = join(rootDir, 'services', 'tooling');
|
|
117
|
+
targetDir = mkdtempSync(join(tmpdir(), 'hostfn-workspace-target-'));
|
|
118
|
+
|
|
119
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
120
|
+
mkdirSync(toolingDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
writeFileSync(
|
|
123
|
+
join(rootDir, 'package.json'),
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
private: true,
|
|
126
|
+
workspaces: ['services/*'],
|
|
127
|
+
}, null, 2),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
writeFileSync(
|
|
131
|
+
join(toolingDir, 'package.json'),
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
name: '@hostfn/tooling',
|
|
134
|
+
version: '1.0.0',
|
|
135
|
+
}, null, 2),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
writeFileSync(
|
|
139
|
+
join(serviceDir, 'package.json'),
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
name: '@hostfn/api',
|
|
142
|
+
dependencies: {
|
|
143
|
+
'@hostfn/tooling': 'workspace:*',
|
|
144
|
+
},
|
|
145
|
+
devDependencies: {
|
|
146
|
+
'@hostfn/tooling': 'workspace:*',
|
|
147
|
+
},
|
|
148
|
+
}, null, 2),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
writeFileSync(
|
|
152
|
+
join(targetDir, 'package.json'),
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
name: '@hostfn/api',
|
|
155
|
+
dependencies: {
|
|
156
|
+
'@hostfn/tooling': 'workspace:*',
|
|
157
|
+
},
|
|
158
|
+
devDependencies: {
|
|
159
|
+
'@hostfn/tooling': 'workspace:*',
|
|
160
|
+
},
|
|
161
|
+
}, null, 2),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
167
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('preserves workspace devDependencies as file references in the deployment bundle', async () => {
|
|
171
|
+
const manager = new WorkspaceManager();
|
|
172
|
+
await manager.detectWorkspace(serviceDir);
|
|
173
|
+
|
|
174
|
+
manager.rewritePackageJson(serviceDir, targetDir);
|
|
175
|
+
|
|
176
|
+
const rewrittenPackageJson = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
|
|
177
|
+
expect(rewrittenPackageJson.dependencies['@hostfn/tooling']).toBe('file:./__workspace__/@hostfn/tooling');
|
|
178
|
+
expect(rewrittenPackageJson.devDependencies['@hostfn/tooling']).toBe('file:./__workspace__/@hostfn/tooling');
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import { tmpdir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { mkdtempSync, rmSync, cpSync } from 'fs';
|
|
4
|
+
import { mkdtempSync, rmSync, cpSync, existsSync } from 'fs';
|
|
5
5
|
import { Logger } from '../utils/logger.js';
|
|
6
6
|
import { ConfigLoader } from '../config/loader.js';
|
|
7
7
|
import { createSSHConnection, SSHConnection } from '../core/ssh.js';
|
|
@@ -25,6 +25,37 @@ interface DeployOptions {
|
|
|
25
25
|
all?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
interface InstallPlan {
|
|
29
|
+
nodeModulesMode: 'all' | 'production' | 'none';
|
|
30
|
+
useCi: boolean;
|
|
31
|
+
installCmd: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function computeInstallPlan(
|
|
35
|
+
config: HostfnConfig,
|
|
36
|
+
hasLockFile: boolean,
|
|
37
|
+
): InstallPlan {
|
|
38
|
+
const nodeModulesMode =
|
|
39
|
+
config.build?.nodeModules ?? (config.build?.command ? 'all' : 'production');
|
|
40
|
+
|
|
41
|
+
const installCmd =
|
|
42
|
+
nodeModulesMode === 'none'
|
|
43
|
+
? null
|
|
44
|
+
: hasLockFile
|
|
45
|
+
? (nodeModulesMode === 'all'
|
|
46
|
+
? 'npm ci --install-links'
|
|
47
|
+
: 'npm ci --production --install-links')
|
|
48
|
+
: (nodeModulesMode === 'all'
|
|
49
|
+
? 'npm install --install-links'
|
|
50
|
+
: 'npm install --production --install-links');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
nodeModulesMode,
|
|
54
|
+
useCi: hasLockFile,
|
|
55
|
+
installCmd,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
export async function deployCommand(
|
|
29
60
|
environment: string,
|
|
30
61
|
options: DeployOptions
|
|
@@ -310,8 +341,16 @@ async function deploySingleService(
|
|
|
310
341
|
Logger.section('Pre-flight Checks');
|
|
311
342
|
Logger.br();
|
|
312
343
|
|
|
313
|
-
//
|
|
344
|
+
// Local mode still relies on rsync so include the same pre-flight check.
|
|
314
345
|
if (options.local) {
|
|
346
|
+
const rsyncSpinner = ora('Checking rsync availability...').start();
|
|
347
|
+
const hasRsync = await FileSync.isRsyncAvailable();
|
|
348
|
+
if (!hasRsync) {
|
|
349
|
+
rsyncSpinner.fail('rsync not installed');
|
|
350
|
+
throw new Error('rsync is required for deployment. Please install it first.');
|
|
351
|
+
}
|
|
352
|
+
rsyncSpinner.succeed('rsync available');
|
|
353
|
+
|
|
315
354
|
const localSpinner = ora('Initializing local deployment...').start();
|
|
316
355
|
ssh = new LocalExecutor();
|
|
317
356
|
localSpinner.succeed('Local mode ready');
|
|
@@ -404,6 +443,19 @@ async function deploySingleService(
|
|
|
404
443
|
bundleSpinner.text = 'Bundling workspace dependencies...';
|
|
405
444
|
await workspaceManager.bundleWorkspaceDependencies(sourceDir, bundleDir);
|
|
406
445
|
|
|
446
|
+
// Rewrite package.json to use file: references for workspace deps
|
|
447
|
+
workspaceManager.rewritePackageJson(sourceDir, bundleDir);
|
|
448
|
+
|
|
449
|
+
const { nodeModulesMode } = computeInstallPlan(
|
|
450
|
+
config,
|
|
451
|
+
existsSync(join(bundleDir, 'package-lock.json')),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
if (nodeModulesMode !== 'none') {
|
|
455
|
+
bundleSpinner.text = 'Generating lockfile...';
|
|
456
|
+
await workspaceManager.generateLockfile(bundleDir, sourceDir);
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
bundleSpinner.succeed('Workspace dependencies bundled');
|
|
408
460
|
Logger.br();
|
|
409
461
|
|
|
@@ -416,84 +468,58 @@ async function deploySingleService(
|
|
|
416
468
|
Logger.br();
|
|
417
469
|
|
|
418
470
|
if (options.local) {
|
|
419
|
-
// Local mode:
|
|
471
|
+
// Local mode: use rsync locally so include/exclude behavior stays aligned
|
|
472
|
+
// with the remote deploy path.
|
|
420
473
|
const syncSpinner = ora('Copying files locally...').start();
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
474
|
+
const rawExcludes = config.sync?.exclude || [
|
|
475
|
+
'node_modules',
|
|
476
|
+
'.git',
|
|
477
|
+
'.github',
|
|
478
|
+
'dist',
|
|
479
|
+
'build',
|
|
480
|
+
'.env',
|
|
481
|
+
'.env.*',
|
|
482
|
+
'*.log',
|
|
483
|
+
'.turbo',
|
|
484
|
+
'.wrangler',
|
|
485
|
+
];
|
|
486
|
+
const localExcludes = bundleDir
|
|
487
|
+
? rawExcludes.map((pattern) => (pattern === 'dist' || pattern === 'build') ? `/${pattern}` : pattern)
|
|
488
|
+
: rawExcludes;
|
|
489
|
+
|
|
490
|
+
await FileSync.syncLocal(actualSourceDir, remoteDir, {
|
|
491
|
+
exclude: localExcludes,
|
|
492
|
+
include: config.sync?.include,
|
|
493
|
+
verbose: false,
|
|
435
494
|
});
|
|
436
495
|
|
|
437
496
|
syncSpinner.succeed('Files copied successfully');
|
|
438
|
-
|
|
439
|
-
// Copy workspace dependencies if they exist
|
|
440
|
-
if (bundleDir) {
|
|
441
|
-
const bundledNodeModules = join(bundleDir, 'node_modules');
|
|
442
|
-
const { existsSync } = await import('fs');
|
|
443
|
-
|
|
444
|
-
if (existsSync(bundledNodeModules)) {
|
|
445
|
-
const uploadSpinner = ora('Copying workspace dependencies...').start();
|
|
446
|
-
cpSync(bundledNodeModules, join(remoteDir, 'node_modules'), { recursive: true });
|
|
447
|
-
uploadSpinner.succeed('Workspace dependencies copied');
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
497
|
} else {
|
|
451
498
|
// Remote mode: use rsync
|
|
452
499
|
const syncSpinner = ora('Syncing files to server...').start();
|
|
500
|
+
|
|
501
|
+
// When syncing a workspace bundle, anchor 'dist' to root (/dist) so it only
|
|
502
|
+
// excludes the service's own dist folder and not workspace deps' compiled output
|
|
503
|
+
// (e.g. __workspace__/@21n/email/dist which is needed for type declarations).
|
|
504
|
+
const rawExcludes = config.sync?.exclude || [
|
|
505
|
+
'node_modules', '.git', '.github', 'dist', 'build', '.env', '.env.*', '*.log', '.turbo', '.wrangler',
|
|
506
|
+
];
|
|
507
|
+
const syncExcludes = bundleDir
|
|
508
|
+
? rawExcludes.map(p => (p === 'dist' || p === 'build') ? `/${p}` : p)
|
|
509
|
+
: rawExcludes;
|
|
453
510
|
|
|
454
511
|
await FileSync.sync(
|
|
455
512
|
actualSourceDir,
|
|
456
513
|
remoteDir,
|
|
457
514
|
host,
|
|
458
515
|
{
|
|
459
|
-
exclude:
|
|
460
|
-
|
|
461
|
-
'.git',
|
|
462
|
-
'dist',
|
|
463
|
-
'.env',
|
|
464
|
-
'*.log',
|
|
465
|
-
],
|
|
516
|
+
exclude: syncExcludes,
|
|
517
|
+
include: config.sync?.include,
|
|
466
518
|
verbose: false,
|
|
467
519
|
}
|
|
468
520
|
);
|
|
469
521
|
|
|
470
522
|
syncSpinner.succeed('Files synced successfully');
|
|
471
|
-
|
|
472
|
-
// Upload bundled workspace dependencies if they exist (before npm install)
|
|
473
|
-
if (bundleDir) {
|
|
474
|
-
const bundledNodeModules = join(bundleDir, 'node_modules');
|
|
475
|
-
const { existsSync } = await import('fs');
|
|
476
|
-
|
|
477
|
-
if (existsSync(bundledNodeModules)) {
|
|
478
|
-
Logger.section('Uploading Workspace Dependencies');
|
|
479
|
-
Logger.br();
|
|
480
|
-
|
|
481
|
-
const uploadSpinner = ora('Uploading bundled workspace dependencies...').start();
|
|
482
|
-
|
|
483
|
-
await FileSync.sync(
|
|
484
|
-
bundledNodeModules,
|
|
485
|
-
join(remoteDir, 'node_modules'),
|
|
486
|
-
host,
|
|
487
|
-
{
|
|
488
|
-
exclude: [],
|
|
489
|
-
verbose: false,
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
uploadSpinner.succeed('Workspace dependencies uploaded');
|
|
494
|
-
Logger.br();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
523
|
}
|
|
498
524
|
|
|
499
525
|
Logger.br();
|
|
@@ -504,30 +530,31 @@ async function deploySingleService(
|
|
|
504
530
|
|
|
505
531
|
// Install dependencies
|
|
506
532
|
const installSpinner = ora('Installing dependencies...').start();
|
|
507
|
-
|
|
533
|
+
|
|
508
534
|
// Check if package-lock.json exists, use npm ci if available, otherwise npm install
|
|
509
535
|
const lockFileCheck = await ssh.exec(
|
|
510
536
|
'test -f package-lock.json && echo "exists"',
|
|
511
537
|
{ cwd: remoteDir, streaming: false }
|
|
512
538
|
);
|
|
513
539
|
const hasLockFile = lockFileCheck.stdout.trim() === 'exists';
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
540
|
+
|
|
541
|
+
const { installCmd } = computeInstallPlan(config, hasLockFile);
|
|
542
|
+
|
|
543
|
+
if (installCmd) {
|
|
544
|
+
const installResult = await ssh.exec(
|
|
545
|
+
installCmd,
|
|
546
|
+
{ cwd: remoteDir, streaming: false }
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (installResult.exitCode !== 0) {
|
|
550
|
+
installSpinner.fail('Dependency installation failed');
|
|
551
|
+
const errorOutput = installResult.stderr || installResult.stdout;
|
|
552
|
+
throw new Error(`${installCmd} failed: ${errorOutput}`);
|
|
553
|
+
}
|
|
554
|
+
installSpinner.succeed('Dependencies installed');
|
|
555
|
+
} else {
|
|
556
|
+
installSpinner.info('Skipping dependency installation (build.nodeModules=none)');
|
|
529
557
|
}
|
|
530
|
-
installSpinner.succeed('Dependencies installed');
|
|
531
558
|
|
|
532
559
|
// Build application
|
|
533
560
|
if (config.build?.command) {
|
|
@@ -684,8 +711,9 @@ EOF`);
|
|
|
684
711
|
|
|
685
712
|
const healthSpinner = ora('Waiting for service to be ready...').start();
|
|
686
713
|
const healthPath = config.health?.path || '/health';
|
|
714
|
+
const timeout = config.health?.timeout || 60;
|
|
687
715
|
const retries = config.health?.retries || 10;
|
|
688
|
-
const interval = config.health?.interval ||
|
|
716
|
+
const interval = config.health?.interval || 3;
|
|
689
717
|
|
|
690
718
|
let healthy = false;
|
|
691
719
|
for (let i = 0; i < retries; i++) {
|
|
@@ -693,7 +721,7 @@ EOF`);
|
|
|
693
721
|
|
|
694
722
|
// Check health via SSH using curl on localhost
|
|
695
723
|
const healthCheckResult = await ssh.exec(
|
|
696
|
-
`curl -sf http://localhost:${envConfig.port}${healthPath}`,
|
|
724
|
+
`curl --max-time ${timeout} -sf http://localhost:${envConfig.port}${healthPath}`,
|
|
697
725
|
{ cwd: remoteDir, streaming: false }
|
|
698
726
|
);
|
|
699
727
|
|
|
@@ -704,7 +732,7 @@ EOF`);
|
|
|
704
732
|
}
|
|
705
733
|
|
|
706
734
|
if (i < retries - 1) {
|
|
707
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
735
|
+
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
708
736
|
}
|
|
709
737
|
}
|
|
710
738
|
|
|
@@ -804,8 +832,19 @@ async function dryRunDeploy(
|
|
|
804
832
|
Logger.log(` → Exclude: ${config.sync?.exclude?.join(', ')}`);
|
|
805
833
|
Logger.br();
|
|
806
834
|
|
|
835
|
+
const hasLocalLockFile = existsSync(join(process.cwd(), 'package-lock.json'));
|
|
836
|
+
const workspaceManager = new WorkspaceManager();
|
|
837
|
+
const isWorkspace = await workspaceManager.detectWorkspace(process.cwd());
|
|
838
|
+
const hasBundledWorkspaceDeps =
|
|
839
|
+
isWorkspace && workspaceManager.getWorkspaceDependencies(process.cwd()).length > 0;
|
|
840
|
+
const { installCmd } = computeInstallPlan(config, hasLocalLockFile || hasBundledWorkspaceDeps);
|
|
841
|
+
|
|
807
842
|
Logger.log('3. Remote Build');
|
|
808
|
-
|
|
843
|
+
if (installCmd) {
|
|
844
|
+
Logger.log(` → ${installCmd}`);
|
|
845
|
+
} else {
|
|
846
|
+
Logger.log(' → Skip dependency installation (build.nodeModules=none)');
|
|
847
|
+
}
|
|
809
848
|
if (config.build?.command) {
|
|
810
849
|
Logger.log(` → ${config.build.command}`);
|
|
811
850
|
}
|
package/src/commands/init.ts
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -37,7 +37,7 @@ export class ConfigLoader {
|
|
|
37
37
|
const result = HostfnConfigSchema.safeParse(config);
|
|
38
38
|
|
|
39
39
|
if (!result.success) {
|
|
40
|
-
const errors = result.error.
|
|
40
|
+
const errors = result.error.issues
|
|
41
41
|
.map(err => ` - ${err.path.join('.')}: ${err.message}`)
|
|
42
42
|
.join('\n');
|
|
43
43
|
|
package/src/config/schema.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
|
|
19
19
|
export const BuildConfigSchema = z.object({
|
|
20
20
|
command: z.string().describe('Build command to run'),
|
|
21
21
|
directory: z.string().default('dist').describe('Output directory'),
|
|
22
|
-
nodeModules: z.enum(['all', 'production', 'none']).
|
|
22
|
+
nodeModules: z.enum(['all', 'production', 'none']).optional(),
|
|
23
23
|
}).optional();
|
|
24
24
|
|
|
25
25
|
export type BuildConfig = z.infer<typeof BuildConfigSchema>;
|
package/src/core/ssh.ts
CHANGED
|
@@ -35,6 +35,8 @@ export class SSHConnection {
|
|
|
35
35
|
host: options.host,
|
|
36
36
|
port: options.port || 22,
|
|
37
37
|
username: options.username,
|
|
38
|
+
keepaliveInterval: 30000, // Send keepalive every 30s to survive long npm installs
|
|
39
|
+
keepaliveCountMax: 10,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
// Add authentication method
|