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 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. cli-service calls server-service to queue a CLI deployment
52
- 4. The agent syncs staging public, runs the pipeline, and reports progress
53
- 5. The CLI polls until the deployment succeeds or fails
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servyx-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Servyx CLI — Upload local projects and deploy via the Servyx platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }