servyx-cli 0.1.3 → 0.1.5

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). Hidden files (`.env`, `.htaccess`, etc.) are excluded by default.
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.3",
3
+ "version": "0.1.5",
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, DEFAULT_RSYNC_EXCLUDES } from '../lib/rsync.js';
5
+ import { rsyncDeploy, sshPreflight, DEFAULT_RSYNC_EXCLUDES, preserveServerDotfiles, ensureRemoteDir, verifyRemoteStagingDir } from '../lib/rsync.js';
6
6
 
7
7
  const TERMINAL_STATUSES = new Set(['success', 'failure', 'cancelled']);
8
8
 
@@ -65,6 +65,18 @@ export async function runDeploy(options = {}) {
65
65
  ];
66
66
 
67
67
  console.log(pc.bold('Syncing files via rsync...'));
68
+ ensureRemoteDir({
69
+ host: target.ssh_host,
70
+ port: target.ssh_port || 22,
71
+ user: target.ssh_user,
72
+ remotePath: target.root_path,
73
+ });
74
+ ensureRemoteDir({
75
+ host: target.ssh_host,
76
+ port: target.ssh_port || 22,
77
+ user: target.ssh_user,
78
+ remotePath: target.staging_path,
79
+ });
68
80
  rsyncDeploy({
69
81
  localDir: root,
70
82
  host: target.ssh_host,
@@ -73,8 +85,34 @@ export async function runDeploy(options = {}) {
73
85
  remotePath: target.staging_path,
74
86
  excludes,
75
87
  });
88
+
89
+ const stagingCheck = verifyRemoteStagingDir({
90
+ host: target.ssh_host,
91
+ port: target.ssh_port || 22,
92
+ user: target.ssh_user,
93
+ remotePath: target.staging_path,
94
+ });
95
+ if (!stagingCheck.ok) {
96
+ console.error(pc.red(`Staging directory is missing or empty after rsync: ${target.staging_path}`));
97
+ if (stagingCheck.stderr) {
98
+ console.error(pc.dim(stagingCheck.stderr));
99
+ }
100
+ console.error(pc.yellow('Ensure the app system user exists on the server and can write under /srv/users/<user>/apps/.'));
101
+ process.exit(1);
102
+ }
76
103
  console.log(pc.green('Rsync complete.'));
77
104
 
105
+ const publicPath = target.public_path || `${target.root_path}/public`;
106
+ console.log(pc.bold('Preserving server dotfiles (.env, .htaccess, etc.)...'));
107
+ preserveServerDotfiles({
108
+ host: target.ssh_host,
109
+ port: target.ssh_port || 22,
110
+ user: target.ssh_user,
111
+ publicPath,
112
+ stagingPath: target.staging_path,
113
+ });
114
+ console.log(pc.green('Server dotfiles preserved.'));
115
+
78
116
  console.log(pc.bold('Starting deployment...'));
79
117
  let startResult;
80
118
  try {
@@ -24,7 +24,7 @@ export async function runLink(options = {}) {
24
24
  name: 'appId',
25
25
  message: 'Select an app to link',
26
26
  choices: apps.map((app) => ({
27
- title: `${app.name} (${app.slug}) — ${app.server_name || app.hostname}`,
27
+ title: `${app.name} (${app.system_username}) — ${app.server_name || app.hostname}`,
28
28
  value: app.id,
29
29
  })),
30
30
  });
@@ -45,10 +45,10 @@ export async function runLink(options = {}) {
45
45
  saveProjectConfig(cwd, {
46
46
  appId: app.id,
47
47
  appName: app.name,
48
- appSlug: app.slug,
48
+ appSlug: app.system_username,
49
49
  linkedAt: new Date().toISOString(),
50
50
  });
51
51
 
52
- console.log(pc.green(`Linked ${app.name} (${app.slug})`));
52
+ console.log(pc.green(`Linked ${app.name} (${app.system_username})`));
53
53
  console.log(pc.dim(`Wrote servyx.json in ${cwd}`));
54
54
  }
package/src/lib/rsync.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { spawnSync } from 'child_process';
2
2
  import path from 'path';
3
3
 
4
- /** Hidden files/dirs are not synced so server-side config (.env, .htaccess, etc.) is preserved. */
4
+ /** Local hidden files are not uploaded; server copies are re-seeded into staging before deploy. */
5
5
  export const DEFAULT_RSYNC_EXCLUDES = ['.*'];
6
6
 
7
+ function shellQuote(value) {
8
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
9
+ }
10
+
7
11
  export function requireBinary(name) {
8
12
  const result = spawnSync('which', [name], { encoding: 'utf8' });
9
13
  if (result.status !== 0) {
@@ -59,3 +63,73 @@ export function rsyncDeploy({ localDir, host, port = 22, user, remotePath, exclu
59
63
 
60
64
  return { target };
61
65
  }
66
+
67
+ export function ensureRemoteDir({ host, port = 22, user, remotePath }) {
68
+ requireBinary('ssh');
69
+ const target = `${user}@${host}`;
70
+ const cmd = `mkdir -p ${shellQuote(remotePath)}`;
71
+ const args = [
72
+ '-o', 'BatchMode=yes',
73
+ '-o', 'StrictHostKeyChecking=accept-new',
74
+ '-o', 'ConnectTimeout=10',
75
+ '-p', String(port),
76
+ target,
77
+ cmd,
78
+ ];
79
+ const result = spawnSync('ssh', args, { encoding: 'utf8' });
80
+ if (result.status !== 0) {
81
+ const err = new Error(`Failed to create remote directory ${remotePath}: ${(result.stderr || '').trim() || 'unknown error'}`);
82
+ err.exitCode = result.status;
83
+ throw err;
84
+ }
85
+ }
86
+
87
+ export function verifyRemoteStagingDir({ host, port = 22, user, remotePath }) {
88
+ requireBinary('ssh');
89
+ const target = `${user}@${host}`;
90
+ const cmd = [
91
+ `test -d ${shellQuote(remotePath)}`,
92
+ `&& [ -n "$(ls -A ${shellQuote(remotePath)} 2>/dev/null | head -1)" ]`,
93
+ ].join(' ');
94
+ const args = [
95
+ '-o', 'BatchMode=yes',
96
+ '-o', 'StrictHostKeyChecking=accept-new',
97
+ '-o', 'ConnectTimeout=10',
98
+ '-p', String(port),
99
+ target,
100
+ cmd,
101
+ ];
102
+ const result = spawnSync('ssh', args, { encoding: 'utf8' });
103
+ return {
104
+ ok: result.status === 0,
105
+ stderr: (result.stderr || '').trim(),
106
+ };
107
+ }
108
+
109
+ /** Copy live dotfiles from public into staging so the agent's staging→public sync does not delete them. */
110
+ export function preserveServerDotfiles({ host, port = 22, user, publicPath, stagingPath }) {
111
+ requireBinary('ssh');
112
+ const target = `${user}@${host}`;
113
+ const cmd = [
114
+ `mkdir -p ${shellQuote(stagingPath)}`,
115
+ `&& find ${shellQuote(publicPath)} -maxdepth 1 -name '.*' -type f`,
116
+ `-exec cp -a {} ${shellQuote(`${stagingPath.replace(/\/$/, '')}/`)} \\;`,
117
+ '2>/dev/null || true',
118
+ ].join(' ');
119
+
120
+ const args = [
121
+ '-o', 'BatchMode=yes',
122
+ '-o', 'StrictHostKeyChecking=accept-new',
123
+ '-o', 'ConnectTimeout=10',
124
+ '-p', String(port),
125
+ target,
126
+ cmd,
127
+ ];
128
+
129
+ const result = spawnSync('ssh', args, { encoding: 'utf8' });
130
+ if (result.status !== 0) {
131
+ const err = new Error(`Failed to preserve server dotfiles: ${(result.stderr || '').trim() || 'unknown error'}`);
132
+ err.exitCode = result.status;
133
+ throw err;
134
+ }
135
+ }