servyx-cli 0.1.2 → 0.1.4
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 +6 -5
- package/package.json +1 -1
- package/src/commands/deploy.js +13 -1
- package/src/lib/rsync.js +35 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ servyx status
|
|
|
38
38
|
|
|
39
39
|
- Global config: `~/.servyx/config.json` (mode 600)
|
|
40
40
|
- Project config: `servyx.json` in your project root
|
|
41
|
-
- Optional excludes: `.servyxignore` (rsync patterns)
|
|
41
|
+
- Optional excludes: `.servyxignore` (rsync patterns). Hidden files (`.env`, `.htaccess`, etc.) are not uploaded from your machine; existing server copies are preserved during deploy.
|
|
42
42
|
|
|
43
43
|
Environment variables:
|
|
44
44
|
|
|
@@ -47,7 +47,8 @@ Environment variables:
|
|
|
47
47
|
## How it works
|
|
48
48
|
|
|
49
49
|
1. `servyx deploy` fetches the deploy target (SSH user, host, staging path) from cli-service
|
|
50
|
-
2. Files are rsynced to `<app_root>/.cli-staging` on the server
|
|
51
|
-
3.
|
|
52
|
-
4.
|
|
53
|
-
5. The
|
|
50
|
+
2. Files are rsynced to `<app_root>/.cli-staging` on the server (local hidden files are skipped)
|
|
51
|
+
3. Live dotfiles from `public` (`.env`, `.htaccess`, etc.) are copied into staging so they are not removed on deploy
|
|
52
|
+
4. cli-service calls server-service to queue a CLI deployment
|
|
53
|
+
5. The agent syncs staging → public, runs the pipeline, and reports progress
|
|
54
|
+
6. The CLI polls until the deployment succeeds or fails
|
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -2,7 +2,7 @@ import pc from 'picocolors';
|
|
|
2
2
|
import { getDeployTarget, getDeployment, startDeployment } from '../lib/api.js';
|
|
3
3
|
import { getToken } from '../lib/config.js';
|
|
4
4
|
import { loadProjectConfig, loadServyxIgnore } from '../lib/project.js';
|
|
5
|
-
import { rsyncDeploy, sshPreflight } from '../lib/rsync.js';
|
|
5
|
+
import { rsyncDeploy, sshPreflight, DEFAULT_RSYNC_EXCLUDES, preserveServerDotfiles } from '../lib/rsync.js';
|
|
6
6
|
|
|
7
7
|
const TERMINAL_STATUSES = new Set(['success', 'failure', 'cancelled']);
|
|
8
8
|
|
|
@@ -59,6 +59,7 @@ export async function runDeploy(options = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const excludes = [
|
|
62
|
+
...DEFAULT_RSYNC_EXCLUDES,
|
|
62
63
|
...(target.rsync_excludes || []),
|
|
63
64
|
...loadServyxIgnore(root),
|
|
64
65
|
];
|
|
@@ -74,6 +75,17 @@ export async function runDeploy(options = {}) {
|
|
|
74
75
|
});
|
|
75
76
|
console.log(pc.green('Rsync complete.'));
|
|
76
77
|
|
|
78
|
+
const publicPath = target.public_path || `${target.root_path}/public`;
|
|
79
|
+
console.log(pc.bold('Preserving server dotfiles (.env, .htaccess, etc.)...'));
|
|
80
|
+
preserveServerDotfiles({
|
|
81
|
+
host: target.ssh_host,
|
|
82
|
+
port: target.ssh_port || 22,
|
|
83
|
+
user: target.ssh_user,
|
|
84
|
+
publicPath,
|
|
85
|
+
stagingPath: target.staging_path,
|
|
86
|
+
});
|
|
87
|
+
console.log(pc.green('Server dotfiles preserved.'));
|
|
88
|
+
|
|
77
89
|
console.log(pc.bold('Starting deployment...'));
|
|
78
90
|
let startResult;
|
|
79
91
|
try {
|
package/src/lib/rsync.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
+
/** Local hidden files are not uploaded; server copies are re-seeded into staging before deploy. */
|
|
5
|
+
export const DEFAULT_RSYNC_EXCLUDES = ['.*'];
|
|
6
|
+
|
|
7
|
+
function shellQuote(value) {
|
|
8
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export function requireBinary(name) {
|
|
5
12
|
const result = spawnSync('which', [name], { encoding: 'utf8' });
|
|
6
13
|
if (result.status !== 0) {
|
|
@@ -56,3 +63,31 @@ export function rsyncDeploy({ localDir, host, port = 22, user, remotePath, exclu
|
|
|
56
63
|
|
|
57
64
|
return { target };
|
|
58
65
|
}
|
|
66
|
+
|
|
67
|
+
/** Copy live dotfiles from public into staging so the agent's staging→public sync does not delete them. */
|
|
68
|
+
export function preserveServerDotfiles({ host, port = 22, user, publicPath, stagingPath }) {
|
|
69
|
+
requireBinary('ssh');
|
|
70
|
+
const target = `${user}@${host}`;
|
|
71
|
+
const cmd = [
|
|
72
|
+
`mkdir -p ${shellQuote(stagingPath)}`,
|
|
73
|
+
`&& find ${shellQuote(publicPath)} -maxdepth 1 -name '.*' -type f`,
|
|
74
|
+
`-exec cp -a {} ${shellQuote(`${stagingPath.replace(/\/$/, '')}/`)} \\;`,
|
|
75
|
+
'2>/dev/null || true',
|
|
76
|
+
].join(' ');
|
|
77
|
+
|
|
78
|
+
const args = [
|
|
79
|
+
'-o', 'BatchMode=yes',
|
|
80
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
81
|
+
'-o', 'ConnectTimeout=10',
|
|
82
|
+
'-p', String(port),
|
|
83
|
+
target,
|
|
84
|
+
cmd,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const result = spawnSync('ssh', args, { encoding: 'utf8' });
|
|
88
|
+
if (result.status !== 0) {
|
|
89
|
+
const err = new Error(`Failed to preserve server dotfiles: ${(result.stderr || '').trim() || 'unknown error'}`);
|
|
90
|
+
err.exitCode = result.status;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|