hostfn 0.1.4 → 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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -2
  3. package/dist/index.js +7 -1
  4. package/package.json +18 -6
  5. package/src/__tests__/core/sync.test.ts +42 -0
  6. package/src/__tests__/core/workspace.test.ts +180 -0
  7. package/src/commands/deploy.ts +122 -83
  8. package/src/commands/init.ts +1 -1
  9. package/src/config/loader.ts +1 -1
  10. package/src/config/schema.ts +1 -1
  11. package/src/core/ssh.ts +2 -0
  12. package/src/core/sync.ts +64 -23
  13. package/src/core/workspace.ts +111 -6
  14. package/src/index.ts +8 -1
  15. package/dist/__tests__/core/backup.test.d.ts +0 -2
  16. package/dist/__tests__/core/backup.test.d.ts.map +0 -1
  17. package/dist/__tests__/core/backup.test.js +0 -108
  18. package/dist/__tests__/core/backup.test.js.map +0 -1
  19. package/dist/__tests__/core/health.test.d.ts +0 -2
  20. package/dist/__tests__/core/health.test.d.ts.map +0 -1
  21. package/dist/__tests__/core/health.test.js +0 -97
  22. package/dist/__tests__/core/health.test.js.map +0 -1
  23. package/dist/__tests__/core/lock.test.d.ts +0 -2
  24. package/dist/__tests__/core/lock.test.d.ts.map +0 -1
  25. package/dist/__tests__/core/lock.test.js +0 -136
  26. package/dist/__tests__/core/lock.test.js.map +0 -1
  27. package/dist/__tests__/core/nginx-multi-domain.test.d.ts +0 -2
  28. package/dist/__tests__/core/nginx-multi-domain.test.d.ts.map +0 -1
  29. package/dist/__tests__/core/nginx-multi-domain.test.js +0 -158
  30. package/dist/__tests__/core/nginx-multi-domain.test.js.map +0 -1
  31. package/dist/__tests__/runtimes/pm2.test.d.ts +0 -2
  32. package/dist/__tests__/runtimes/pm2.test.d.ts.map +0 -1
  33. package/dist/__tests__/runtimes/pm2.test.js +0 -111
  34. package/dist/__tests__/runtimes/pm2.test.js.map +0 -1
  35. package/dist/__tests__/utils/validation.test.d.ts +0 -2
  36. package/dist/__tests__/utils/validation.test.d.ts.map +0 -1
  37. package/dist/__tests__/utils/validation.test.js +0 -136
  38. package/dist/__tests__/utils/validation.test.js.map +0 -1
  39. package/dist/commands/deploy.d.ts +0 -11
  40. package/dist/commands/deploy.d.ts.map +0 -1
  41. package/dist/commands/deploy.js +0 -636
  42. package/dist/commands/deploy.js.map +0 -1
  43. package/dist/commands/env.d.ts +0 -21
  44. package/dist/commands/env.d.ts.map +0 -1
  45. package/dist/commands/env.js +0 -317
  46. package/dist/commands/env.js.map +0 -1
  47. package/dist/commands/expose.d.ts +0 -6
  48. package/dist/commands/expose.d.ts.map +0 -1
  49. package/dist/commands/expose.js +0 -379
  50. package/dist/commands/expose.js.map +0 -1
  51. package/dist/commands/init.d.ts +0 -2
  52. package/dist/commands/init.d.ts.map +0 -1
  53. package/dist/commands/init.js +0 -175
  54. package/dist/commands/init.js.map +0 -1
  55. package/dist/commands/logs.d.ts +0 -10
  56. package/dist/commands/logs.d.ts.map +0 -1
  57. package/dist/commands/logs.js +0 -75
  58. package/dist/commands/logs.js.map +0 -1
  59. package/dist/commands/rollback.d.ts +0 -6
  60. package/dist/commands/rollback.d.ts.map +0 -1
  61. package/dist/commands/rollback.js +0 -113
  62. package/dist/commands/rollback.js.map +0 -1
  63. package/dist/commands/server/info.d.ts +0 -2
  64. package/dist/commands/server/info.d.ts.map +0 -1
  65. package/dist/commands/server/info.js +0 -104
  66. package/dist/commands/server/info.js.map +0 -1
  67. package/dist/commands/server/setup.d.ts +0 -11
  68. package/dist/commands/server/setup.d.ts.map +0 -1
  69. package/dist/commands/server/setup.js +0 -161
  70. package/dist/commands/server/setup.js.map +0 -1
  71. package/dist/commands/status.d.ts +0 -6
  72. package/dist/commands/status.d.ts.map +0 -1
  73. package/dist/commands/status.js +0 -120
  74. package/dist/commands/status.js.map +0 -1
  75. package/dist/config/loader.d.ts +0 -21
  76. package/dist/config/loader.d.ts.map +0 -1
  77. package/dist/config/loader.js +0 -54
  78. package/dist/config/loader.js.map +0 -1
  79. package/dist/config/schema.d.ts +0 -323
  80. package/dist/config/schema.d.ts.map +0 -1
  81. package/dist/config/schema.js +0 -108
  82. package/dist/config/schema.js.map +0 -1
  83. package/dist/core/backup.d.ts +0 -34
  84. package/dist/core/backup.d.ts.map +0 -1
  85. package/dist/core/backup.js +0 -95
  86. package/dist/core/backup.js.map +0 -1
  87. package/dist/core/health.d.ts +0 -31
  88. package/dist/core/health.d.ts.map +0 -1
  89. package/dist/core/health.js +0 -78
  90. package/dist/core/health.js.map +0 -1
  91. package/dist/core/local.d.ts +0 -19
  92. package/dist/core/local.d.ts.map +0 -1
  93. package/dist/core/local.js +0 -50
  94. package/dist/core/local.js.map +0 -1
  95. package/dist/core/lock.d.ts +0 -28
  96. package/dist/core/lock.d.ts.map +0 -1
  97. package/dist/core/lock.js +0 -89
  98. package/dist/core/lock.js.map +0 -1
  99. package/dist/core/nginx.d.ts +0 -43
  100. package/dist/core/nginx.d.ts.map +0 -1
  101. package/dist/core/nginx.js +0 -131
  102. package/dist/core/nginx.js.map +0 -1
  103. package/dist/core/ssh.d.ts +0 -79
  104. package/dist/core/ssh.d.ts.map +0 -1
  105. package/dist/core/ssh.js +0 -264
  106. package/dist/core/ssh.js.map +0 -1
  107. package/dist/core/sync.d.ts +0 -25
  108. package/dist/core/sync.d.ts.map +0 -1
  109. package/dist/core/sync.js +0 -117
  110. package/dist/core/sync.js.map +0 -1
  111. package/dist/core/workspace.d.ts +0 -13
  112. package/dist/core/workspace.d.ts.map +0 -1
  113. package/dist/core/workspace.js +0 -141
  114. package/dist/core/workspace.js.map +0 -1
  115. package/dist/index.d.ts +0 -3
  116. package/dist/index.d.ts.map +0 -1
  117. package/dist/index.js.map +0 -1
  118. package/dist/runtimes/base.d.ts +0 -115
  119. package/dist/runtimes/base.d.ts.map +0 -1
  120. package/dist/runtimes/base.js +0 -16
  121. package/dist/runtimes/base.js.map +0 -1
  122. package/dist/runtimes/nodejs/detector.d.ts +0 -47
  123. package/dist/runtimes/nodejs/detector.d.ts.map +0 -1
  124. package/dist/runtimes/nodejs/detector.js +0 -143
  125. package/dist/runtimes/nodejs/detector.js.map +0 -1
  126. package/dist/runtimes/nodejs/index.d.ts +0 -14
  127. package/dist/runtimes/nodejs/index.d.ts.map +0 -1
  128. package/dist/runtimes/nodejs/index.js +0 -213
  129. package/dist/runtimes/nodejs/index.js.map +0 -1
  130. package/dist/runtimes/nodejs/pm2.d.ts +0 -17
  131. package/dist/runtimes/nodejs/pm2.d.ts.map +0 -1
  132. package/dist/runtimes/nodejs/pm2.js +0 -60
  133. package/dist/runtimes/nodejs/pm2.js.map +0 -1
  134. package/dist/runtimes/registry.d.ts +0 -34
  135. package/dist/runtimes/registry.d.ts.map +0 -1
  136. package/dist/runtimes/registry.js +0 -58
  137. package/dist/runtimes/registry.js.map +0 -1
  138. package/dist/utils/logger.d.ts +0 -47
  139. package/dist/utils/logger.d.ts.map +0 -1
  140. package/dist/utils/logger.js +0 -76
  141. package/dist/utils/logger.js.map +0 -1
  142. package/dist/utils/validation.d.ts +0 -32
  143. package/dist/utils/validation.d.ts.map +0 -1
  144. package/dist/utils/validation.js +0 -125
  145. package/dist/utils/validation.js.map +0 -1
package/src/core/sync.ts CHANGED
@@ -12,6 +12,31 @@ export interface SyncOptions {
12
12
  * Sync files to remote server using rsync
13
13
  */
14
14
  export class FileSync {
15
+ private static buildBaseArgs(options: SyncOptions = {}): string[] {
16
+ const args = [
17
+ '-avz',
18
+ '--delete',
19
+ ];
20
+
21
+ if (options.dryRun) {
22
+ args.push('--dry-run');
23
+ }
24
+
25
+ if (options.include && options.include.length > 0) {
26
+ for (const pattern of options.include) {
27
+ args.push('--include', pattern);
28
+ }
29
+ }
30
+
31
+ if (options.exclude && options.exclude.length > 0) {
32
+ for (const pattern of options.exclude) {
33
+ args.push('--exclude', pattern);
34
+ }
35
+ }
36
+
37
+ return args;
38
+ }
39
+
15
40
  /**
16
41
  * Sync local directory to remote server
17
42
  */
@@ -21,10 +46,7 @@ export class FileSync {
21
46
  sshConnection: string, // user@host
22
47
  options: SyncOptions = {}
23
48
  ): Promise<void> {
24
- const args = [
25
- '-avz', // archive, verbose, compress
26
- '--delete', // delete files on remote that don't exist locally
27
- ];
49
+ const args = this.buildBaseArgs(options);
28
50
 
29
51
  // Handle SSH authentication for CI/CD mode
30
52
  let tempKeyPath: string | undefined;
@@ -49,25 +71,6 @@ export class FileSync {
49
71
  args.push('-e', 'ssh -o StrictHostKeyChecking=no -o BatchMode=yes');
50
72
  }
51
73
 
52
- // Add dry-run flag
53
- if (options.dryRun) {
54
- args.push('--dry-run');
55
- }
56
-
57
- // Add exclude patterns
58
- if (options.exclude && options.exclude.length > 0) {
59
- for (const pattern of options.exclude) {
60
- args.push('--exclude', pattern);
61
- }
62
- }
63
-
64
- // Add include patterns
65
- if (options.include && options.include.length > 0) {
66
- for (const pattern of options.include) {
67
- args.push('--include', pattern);
68
- }
69
- }
70
-
71
74
  // Ensure trailing slash on source (rsync behavior)
72
75
  const source = localPath.endsWith('/') ? localPath : `${localPath}/`;
73
76
  const destination = `${sshConnection}:${remotePath}`;
@@ -111,6 +114,44 @@ export class FileSync {
111
114
  }
112
115
  }
113
116
 
117
+ /**
118
+ * Sync local directory to another local directory using the same rsync rules
119
+ * as remote deploys.
120
+ */
121
+ static async syncLocal(
122
+ sourcePath: string,
123
+ destinationPath: string,
124
+ options: SyncOptions = {}
125
+ ): Promise<void> {
126
+ const args = this.buildBaseArgs(options);
127
+ const source = sourcePath.endsWith('/') ? sourcePath : `${sourcePath}/`;
128
+
129
+ args.push(source, destinationPath);
130
+
131
+ try {
132
+ const result = await execa('rsync', args, {
133
+ stdio: options.verbose ? 'inherit' : 'pipe',
134
+ });
135
+
136
+ if (result.exitCode !== 0) {
137
+ throw new Error(`rsync failed with exit code ${result.exitCode}`);
138
+ }
139
+ } catch (error) {
140
+ if (error instanceof Error) {
141
+ if (error.message.includes('ENOENT') || error.message.includes('not found')) {
142
+ throw new Error(
143
+ 'rsync is not installed on your system.\n' +
144
+ 'Please install it:\n' +
145
+ ' - macOS: brew install rsync\n' +
146
+ ' - Ubuntu/Debian: apt-get install rsync\n' +
147
+ ' - Windows: Install via WSL or use Git Bash'
148
+ );
149
+ }
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+
114
155
  /**
115
156
  * Check if rsync is available
116
157
  */
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync } from 'fs';
2
- import { join, dirname, resolve } from 'path';
2
+ import { join, dirname, resolve, relative, sep } from 'path';
3
+ import { valid, validRange, satisfies } from 'semver';
3
4
  import { Logger } from '../utils/logger.js';
4
5
 
5
6
  interface PackageJson {
@@ -95,8 +96,11 @@ export class WorkspaceManager {
95
96
 
96
97
  Logger.info(`Bundling ${workspaceDeps.length} workspace dependencies...`);
97
98
 
98
- const nodeModulesDir = join(targetDir, 'node_modules');
99
- mkdirSync(nodeModulesDir, { recursive: true });
99
+ // Store workspace deps in __workspace__/ (not node_modules/) so they are:
100
+ // 1. Included in the main rsync (not excluded like node_modules)
101
+ // 2. Not wiped out when npm ci/install cleans node_modules
102
+ const workspaceDir = join(targetDir, '__workspace__');
103
+ mkdirSync(workspaceDir, { recursive: true });
100
104
 
101
105
  for (const depName of workspaceDeps) {
102
106
  const depPath = this.workspacePackages.get(depName);
@@ -113,7 +117,7 @@ export class WorkspaceManager {
113
117
  }
114
118
  }
115
119
 
116
- const targetDepDir = join(nodeModulesDir, depName);
120
+ const targetDepDir = join(workspaceDir, depName);
117
121
 
118
122
  mkdirSync(dirname(targetDepDir), { recursive: true });
119
123
 
@@ -130,6 +134,55 @@ export class WorkspaceManager {
130
134
  }
131
135
  }
132
136
 
137
+ async generateLockfile(targetDir: string, servicePath?: string): Promise<void> {
138
+ const { execSync } = await import('child_process');
139
+ const pkgJsonPath = join(targetDir, 'package.json');
140
+
141
+ // Temporarily pin direct dependencies to their exact resolved versions from the
142
+ // root monorepo lockfile. Without this, `npm install --package-lock-only` would
143
+ // re-resolve all dependencies from the npm registry and may pick up newer
144
+ // (potentially breaking) versions (e.g. better-auth 1.3.x → 1.5.x).
145
+ //
146
+ // We pin direct deps only (not transitive). Overriding transitive deps causes
147
+ // EUSAGE errors from `npm ci` because the lockfile's transitive versions may no
148
+ // longer match what the pinned direct dep's own package.json requires.
149
+ if (this.workspaceRoot) {
150
+ const rootLockfilePath = join(this.workspaceRoot, 'package-lock.json');
151
+ if (existsSync(rootLockfilePath)) {
152
+ try {
153
+ const rootLockfile = JSON.parse(readFileSync(rootLockfilePath, 'utf-8'));
154
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
155
+
156
+ // Replace semver ranges in direct deps with the exact resolved version.
157
+ // This prevents npm from upgrading direct deps when generating the lockfile.
158
+ for (const depGroup of ['dependencies', 'devDependencies'] as const) {
159
+ for (const depName of Object.keys(pkg[depGroup] || {})) {
160
+ const declaredVersion = pkg[depGroup]![depName];
161
+ const exactVersion = this.getResolvedVersionForDependency(
162
+ rootLockfile.packages || {},
163
+ servicePath,
164
+ depName,
165
+ declaredVersion,
166
+ );
167
+ if (exactVersion) {
168
+ pkg[depGroup]![depName] = exactVersion;
169
+ }
170
+ }
171
+ }
172
+
173
+ writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
174
+ } catch {
175
+ // Fall through without pinning
176
+ }
177
+ }
178
+ }
179
+
180
+ execSync('npm install --package-lock-only --ignore-scripts', {
181
+ cwd: targetDir,
182
+ stdio: 'ignore',
183
+ });
184
+ }
185
+
133
186
  rewritePackageJson(servicePath: string, targetDir: string): void {
134
187
  const workspaceDeps = this.getWorkspaceDependencies(servicePath);
135
188
 
@@ -143,7 +196,7 @@ export class WorkspaceManager {
143
196
  if (pkg.dependencies) {
144
197
  for (const depName of workspaceDeps) {
145
198
  if (pkg.dependencies[depName]) {
146
- pkg.dependencies[depName] = `file:./node_modules/${depName}`;
199
+ pkg.dependencies[depName] = `file:./__workspace__/${depName}`;
147
200
  }
148
201
  }
149
202
  }
@@ -151,7 +204,7 @@ export class WorkspaceManager {
151
204
  if (pkg.devDependencies) {
152
205
  for (const depName of workspaceDeps) {
153
206
  if (pkg.devDependencies[depName]) {
154
- delete pkg.devDependencies[depName];
207
+ pkg.devDependencies[depName] = `file:./__workspace__/${depName}`;
155
208
  }
156
209
  }
157
210
  }
@@ -177,4 +230,56 @@ export class WorkspaceManager {
177
230
  isInWorkspace(): boolean {
178
231
  return this.workspaceRoot !== null;
179
232
  }
233
+
234
+ private getResolvedVersionForDependency(
235
+ lockfilePackages: Record<string, { version?: string; name?: string }>,
236
+ servicePath: string | undefined,
237
+ depName: string,
238
+ declaredRange: string,
239
+ ): string | null {
240
+ const candidatePaths = this.getDependencyCandidatePaths(servicePath, depName);
241
+
242
+ for (const candidatePath of candidatePaths) {
243
+ const pkgEntry = lockfilePackages[candidatePath];
244
+ if (!pkgEntry?.version) continue;
245
+ if (pkgEntry.name !== undefined && pkgEntry.name !== depName) continue;
246
+ if (!this.isResolvedVersionCompatible(declaredRange, pkgEntry.version)) continue;
247
+ return pkgEntry.version;
248
+ }
249
+
250
+ return null;
251
+ }
252
+
253
+ private getDependencyCandidatePaths(servicePath: string | undefined, depName: string): string[] {
254
+ const candidates: string[] = [];
255
+
256
+ if (this.workspaceRoot && servicePath) {
257
+ const relativeServicePath = relative(this.workspaceRoot, servicePath).split(sep).join('/');
258
+ if (relativeServicePath && relativeServicePath !== '.') {
259
+ candidates.push(`${relativeServicePath}/node_modules/${depName}`);
260
+ }
261
+ }
262
+
263
+ candidates.push(`node_modules/${depName}`);
264
+ return candidates;
265
+ }
266
+
267
+ private isResolvedVersionCompatible(declaredRange: string, resolvedVersion: string): boolean {
268
+ const normalizedRange = declaredRange.startsWith('workspace:')
269
+ ? declaredRange.slice('workspace:'.length)
270
+ : declaredRange;
271
+
272
+ if (normalizedRange === '' || normalizedRange === '*') {
273
+ return true;
274
+ }
275
+
276
+ const validResolvedVersion = valid(resolvedVersion);
277
+ const validResolvedRange = validRange(normalizedRange);
278
+
279
+ if (!validResolvedVersion || !validResolvedRange) {
280
+ return false;
281
+ }
282
+
283
+ return satisfies(validResolvedVersion, validResolvedRange);
284
+ }
180
285
  }
package/src/index.ts CHANGED
@@ -4,6 +4,13 @@ import { Command } from 'commander';
4
4
  import { Logger } from './utils/logger.js';
5
5
  import { RuntimeRegistry } from './runtimes/registry.js';
6
6
  import { NodeJSAdapter } from './runtimes/nodejs/index.js';
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
7
14
 
8
15
  // Register runtime adapters
9
16
  RuntimeRegistry.register(new NodeJSAdapter());
@@ -13,7 +20,7 @@ const program = new Command();
13
20
  program
14
21
  .name('hostfn')
15
22
  .description('Universal application deployment CLI')
16
- .version('0.1.0');
23
+ .version(packageJson.version);
17
24
 
18
25
  // Init command
19
26
  program
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=backup.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"backup.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/core/backup.test.ts"],"names":[],"mappings":""}
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { BackupManager } from '../../core/backup.js';
3
- // Mock SSH connection
4
- const mockSSH = {
5
- exec: vi.fn(),
6
- exists: vi.fn(),
7
- mkdir: vi.fn(),
8
- upload: vi.fn(),
9
- download: vi.fn(),
10
- disconnect: vi.fn(),
11
- };
12
- describe('BackupManager', () => {
13
- let backupManager;
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- backupManager = new BackupManager(mockSSH, '/var/www/myapp');
17
- });
18
- describe('create', () => {
19
- it('should create backup with timestamp', async () => {
20
- mockSSH.mkdir.mockResolvedValue(undefined);
21
- mockSSH.exists.mockResolvedValue(true); // dist exists
22
- mockSSH.exec.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
23
- const backupPath = await backupManager.create();
24
- expect(backupPath).toMatch(/\/var\/www\/myapp\/backups\/\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/);
25
- expect(mockSSH.mkdir).toHaveBeenCalled();
26
- expect(mockSSH.exec).toHaveBeenCalled();
27
- });
28
- it('should return path even if source directory does not exist', async () => {
29
- mockSSH.mkdir.mockResolvedValue(undefined);
30
- mockSSH.exists.mockResolvedValue(false); // No dist directory
31
- const backupPath = await backupManager.create();
32
- // Returns backup path but doesn't fail
33
- expect(backupPath).toMatch(/\/var\/www\/myapp\/backups\/\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/);
34
- expect(mockSSH.mkdir).toHaveBeenCalled();
35
- });
36
- });
37
- describe('list', () => {
38
- it('should list all backups with timestamps', async () => {
39
- mockSSH.exists.mockResolvedValue(true);
40
- mockSSH.exec.mockResolvedValue({
41
- stdout: 'myapp.backup.20251114_130000\nmyapp.backup.20251114_120000\n', // sorted reverse
42
- stderr: '',
43
- exitCode: 0
44
- });
45
- const backups = await backupManager.list();
46
- expect(backups).toHaveLength(2);
47
- expect(backups[0]).toContain('20251114_130000'); // Most recent first
48
- expect(backups[1]).toContain('20251114_120000');
49
- });
50
- it('should return empty array when no backups exist', async () => {
51
- mockSSH.exec.mockResolvedValue({
52
- stdout: '',
53
- stderr: '',
54
- exitCode: 0
55
- });
56
- const backups = await backupManager.list();
57
- expect(backups).toHaveLength(0);
58
- });
59
- });
60
- describe('restore', () => {
61
- it('should restore from backup', async () => {
62
- mockSSH.exists.mockResolvedValue(true);
63
- mockSSH.exec.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
64
- await backupManager.restore('myapp.backup.20251114_120000');
65
- expect(mockSSH.exec).toHaveBeenCalledWith(expect.stringContaining('cp -r'));
66
- });
67
- });
68
- describe('cleanup', () => {
69
- it('should keep only specified number of backups', async () => {
70
- const backupNames = [
71
- 'myapp.backup.20251114_150000', // Most recent (sorted)
72
- 'myapp.backup.20251114_140000',
73
- 'myapp.backup.20251114_130000',
74
- 'myapp.backup.20251114_120000',
75
- 'myapp.backup.20251114_110000',
76
- 'myapp.backup.20251114_100000', // Oldest
77
- ];
78
- mockSSH.exists.mockResolvedValue(true);
79
- mockSSH.exec
80
- .mockResolvedValueOnce({
81
- stdout: backupNames.join('\n'),
82
- stderr: '',
83
- exitCode: 0
84
- })
85
- .mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
86
- await backupManager.cleanup(3);
87
- // Should remove oldest 3 backups
88
- expect(mockSSH.exec).toHaveBeenCalledWith(expect.stringContaining('rm -rf'));
89
- });
90
- it('should not remove backups if under limit', async () => {
91
- const backupNames = [
92
- 'myapp.backup.20251114_100000',
93
- 'myapp.backup.20251114_110000',
94
- ];
95
- mockSSH.exists.mockResolvedValue(true);
96
- mockSSH.exec.mockResolvedValueOnce({
97
- stdout: backupNames.join('\n'),
98
- stderr: '',
99
- exitCode: 0
100
- });
101
- await backupManager.cleanup(5);
102
- // Should only call list, not remove (called twice: exists + ls)
103
- expect(mockSSH.exec).toHaveBeenCalledTimes(1);
104
- expect(mockSSH.exec).toHaveBeenCalledWith(expect.stringContaining('ls -1'));
105
- });
106
- });
107
- });
108
- //# sourceMappingURL=backup.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"backup.test.js","sourceRoot":"","sources":["../../../src/__tests__/core/backup.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=health.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"health.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/core/health.test.ts"],"names":[],"mappings":""}
@@ -1,97 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { HealthCheck } from '../../core/health.js';
3
- // Mock fetch globally
4
- global.fetch = vi.fn();
5
- describe('HealthCheck', () => {
6
- beforeEach(() => {
7
- vi.clearAllMocks();
8
- });
9
- describe('check', () => {
10
- it('should return healthy for successful response', async () => {
11
- global.fetch.mockResolvedValueOnce({
12
- ok: true,
13
- status: 200,
14
- });
15
- const result = await HealthCheck.check({
16
- url: 'http://localhost:3000/health',
17
- });
18
- expect(result.healthy).toBe(true);
19
- expect(result.statusCode).toBe(200);
20
- expect(result.responseTime).toBeGreaterThanOrEqual(0);
21
- });
22
- it('should return unhealthy for failed response', async () => {
23
- global.fetch.mockResolvedValueOnce({
24
- ok: false,
25
- status: 500,
26
- });
27
- const result = await HealthCheck.check({
28
- url: 'http://localhost:3000/health',
29
- });
30
- expect(result.healthy).toBe(false);
31
- expect(result.statusCode).toBe(500);
32
- });
33
- it('should return unhealthy for network error', async () => {
34
- global.fetch.mockRejectedValueOnce(new Error('Network error'));
35
- const result = await HealthCheck.check({
36
- url: 'http://localhost:3000/health',
37
- });
38
- expect(result.healthy).toBe(false);
39
- expect(result.error).toBeDefined();
40
- });
41
- it('should respect timeout', async () => {
42
- global.fetch.mockImplementationOnce(() => new Promise((resolve) => setTimeout(resolve, 2000)));
43
- const result = await HealthCheck.check({
44
- url: 'http://localhost:3000/health',
45
- timeout: 100,
46
- });
47
- expect(result.healthy).toBe(false);
48
- });
49
- });
50
- describe('waitForReady', () => {
51
- it('should return true when service becomes healthy', async () => {
52
- let callCount = 0;
53
- global.fetch.mockImplementation(() => {
54
- callCount++;
55
- return Promise.resolve({
56
- ok: callCount >= 2, // Becomes healthy on second call
57
- status: callCount >= 2 ? 200 : 503,
58
- });
59
- });
60
- const result = await HealthCheck.waitForReady({
61
- url: 'http://localhost:3000/health',
62
- retries: 5,
63
- interval: 10,
64
- });
65
- expect(result).toBe(true);
66
- expect(callCount).toBeGreaterThanOrEqual(2);
67
- });
68
- it('should return false when max retries exceeded', async () => {
69
- global.fetch.mockResolvedValue({
70
- ok: false,
71
- status: 503,
72
- });
73
- const result = await HealthCheck.waitForReady({
74
- url: 'http://localhost:3000/health',
75
- retries: 2,
76
- interval: 10,
77
- });
78
- expect(result).toBe(false);
79
- });
80
- it('should call progress callback on each attempt', async () => {
81
- global.fetch.mockResolvedValue({
82
- ok: false,
83
- status: 503,
84
- });
85
- const progressCalls = [];
86
- await HealthCheck.waitForReady({
87
- url: 'http://localhost:3000/health',
88
- retries: 3,
89
- interval: 10,
90
- }, (attempt) => {
91
- progressCalls.push(attempt);
92
- });
93
- expect(progressCalls).toEqual([1, 2, 3]);
94
- });
95
- });
96
- });
97
- //# sourceMappingURL=health.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"health.test.js","sourceRoot":"","sources":["../../../src/__tests__/core/health.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=lock.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"lock.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/core/lock.test.ts"],"names":[],"mappings":""}