servyx-cli 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 CHANGED
@@ -27,9 +27,12 @@ servyx login
27
27
  cd my-app
28
28
  servyx link
29
29
 
30
- # 3. Deploy
30
+ # 3. Deploy (server .env and .htaccess are preserved by default)
31
31
  servyx deploy
32
32
 
33
+ # 3b. React/Vite frontends: upload local .env files too
34
+ servyx deploy --with-env
35
+
33
36
  # 4. Check recent deployments
34
37
  servyx status
35
38
  ```
@@ -38,7 +41,7 @@ servyx status
38
41
 
39
42
  - Global config: `~/.servyx/config.json` (mode 600)
40
43
  - Project config: `servyx.json` in your project root
41
- - Optional excludes: `.servyxignore` (rsync patterns). Hidden files (`.env`, `.htaccess`, etc.) are not uploaded from your machine; existing server copies are preserved during deploy.
44
+ - Optional excludes: `.servyxignore` (rsync patterns). By default, hidden files (`.env`, `.htaccess`, etc.) are not uploaded and existing server copies are preserved. Use `servyx deploy --with-env` to upload local `.env` / `.env.*` files (useful for React/Vite builds).
42
45
 
43
46
  Environment variables:
44
47
 
@@ -47,8 +50,8 @@ Environment variables:
47
50
  ## How it works
48
51
 
49
52
  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 (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
53
+ 2. Files are rsynced to `<app_root>/.cli-staging` on the server (local hidden files are skipped unless `--with-env`)
54
+ 3. Live dotfiles from `public` are copied into staging so they are not removed on deploy (`.env` is skipped when using `--with-env`)
55
+ 4. cli-service calls server-service to queue a CLI deployment (`with_env` tells the agent to sync uploaded env files)
56
+ 5. The agent syncs staging → public, preserving `.htaccess` and (by default) `.env` on the server
54
57
  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.5",
3
+ "version": "0.1.7",
4
4
  "description": "Servyx CLI — Upload local projects and deploy via the Servyx platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,12 +58,17 @@ export async function runDeploy(options = {}) {
58
58
  process.exit(1);
59
59
  }
60
60
 
61
+ const withEnv = Boolean(options.withEnv);
61
62
  const excludes = [
62
63
  ...DEFAULT_RSYNC_EXCLUDES,
63
64
  ...(target.rsync_excludes || []),
64
65
  ...loadServyxIgnore(root),
65
66
  ];
66
67
 
68
+ if (withEnv) {
69
+ console.log(pc.yellow('Including local .env files in this deploy.'));
70
+ }
71
+
67
72
  console.log(pc.bold('Syncing files via rsync...'));
68
73
  ensureRemoteDir({
69
74
  host: target.ssh_host,
@@ -84,6 +89,7 @@ export async function runDeploy(options = {}) {
84
89
  user: target.ssh_user,
85
90
  remotePath: target.staging_path,
86
91
  excludes,
92
+ withEnv,
87
93
  });
88
94
 
89
95
  const stagingCheck = verifyRemoteStagingDir({
@@ -103,20 +109,25 @@ export async function runDeploy(options = {}) {
103
109
  console.log(pc.green('Rsync complete.'));
104
110
 
105
111
  const publicPath = target.public_path || `${target.root_path}/public`;
106
- console.log(pc.bold('Preserving server dotfiles (.env, .htaccess, etc.)...'));
112
+ if (!withEnv) {
113
+ console.log(pc.bold('Preserving server dotfiles (.env, .htaccess, etc.)...'));
114
+ } else {
115
+ console.log(pc.bold('Preserving server dotfiles (.htaccess, etc.)...'));
116
+ }
107
117
  preserveServerDotfiles({
108
118
  host: target.ssh_host,
109
119
  port: target.ssh_port || 22,
110
120
  user: target.ssh_user,
111
121
  publicPath,
112
122
  stagingPath: target.staging_path,
123
+ skipEnv: withEnv,
113
124
  });
114
125
  console.log(pc.green('Server dotfiles preserved.'));
115
126
 
116
127
  console.log(pc.bold('Starting deployment...'));
117
128
  let startResult;
118
129
  try {
119
- startResult = await startDeployment(appId, message);
130
+ startResult = await startDeployment(appId, message, { withEnv });
120
131
  } catch (err) {
121
132
  if (err.code === 'APP_NOT_FOUND') {
122
133
  console.error(pc.red('Application not found by server-service.'));
@@ -50,5 +50,5 @@ export async function runLink(options = {}) {
50
50
  });
51
51
 
52
52
  console.log(pc.green(`Linked ${app.name} (${app.system_username})`));
53
- console.log(pc.dim(`Wrote servyx.json in ${cwd}`));
53
+ // console.log(pc.dim(`Wrote servyx.json in ${cwd}`));
54
54
  }
@@ -63,5 +63,5 @@ export async function runLogin(options = {}) {
63
63
 
64
64
  setAuth({ token, apiUrl, user });
65
65
  console.log(pc.green(`Logged in as ${user?.username || user?.email || username}`));
66
- console.log(pc.dim(`API: ${apiUrl}`));
66
+ // console.log(pc.dim(`API: ${apiUrl}`));
67
67
  }
package/src/index.js CHANGED
@@ -45,9 +45,10 @@ program
45
45
  .command('deploy')
46
46
  .description('Rsync local files and trigger a deployment')
47
47
  .option('-m, --message <message>', 'Deployment message')
48
+ .option('--with-env', 'Upload local .env files (for React/Vite frontends)')
48
49
  .action(async (options) => {
49
50
  try {
50
- await runDeploy(options);
51
+ await runDeploy({ ...options, withEnv: Boolean(options.withEnv) });
51
52
  } catch (err) {
52
53
  console.error(pc.red(err.message || 'Deploy failed'));
53
54
  process.exit(1);
package/src/lib/api.js CHANGED
@@ -54,10 +54,13 @@ export async function getDeployTarget(appId) {
54
54
  return unwrap(await client.get(`/apps/${appId}/deploy-target`));
55
55
  }
56
56
 
57
- export async function startDeployment(appId, message) {
57
+ export async function startDeployment(appId, message, { withEnv = false } = {}) {
58
58
  try {
59
59
  const client = createClient();
60
- return unwrap(await client.post(`/apps/${appId}/deployments`, { message }));
60
+ return unwrap(await client.post(`/apps/${appId}/deployments`, {
61
+ message,
62
+ with_env: withEnv || undefined,
63
+ }));
61
64
  } catch (err) {
62
65
  mapNetworkError(err, 'start deployment');
63
66
  }
package/src/lib/rsync.js CHANGED
@@ -1,9 +1,34 @@
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. */
4
+ /** Local hidden files are not uploaded by default; server copies are re-seeded into staging before deploy. */
5
5
  export const DEFAULT_RSYNC_EXCLUDES = ['.*'];
6
6
 
7
+ /** React/Vite-style env files that can be opted in with --with-env. */
8
+ export const ENV_FILE_PATTERNS = ['.env', '.env.*'];
9
+
10
+ export function buildRsyncFilters({ withEnv = false, excludes = [] } = {}) {
11
+ const filters = [];
12
+ if (withEnv) {
13
+ for (const pattern of ENV_FILE_PATTERNS) {
14
+ filters.push('--include', pattern);
15
+ }
16
+ }
17
+ filters.push('--exclude', '.*');
18
+
19
+ const filteredExcludes = excludes.filter((pattern) => {
20
+ if (pattern === '.*') return false;
21
+ if (withEnv && ENV_FILE_PATTERNS.includes(pattern)) return false;
22
+ return true;
23
+ });
24
+
25
+ for (const pattern of filteredExcludes) {
26
+ filters.push('--exclude', pattern);
27
+ }
28
+
29
+ return filters;
30
+ }
31
+
7
32
  function shellQuote(value) {
8
33
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
9
34
  }
@@ -36,7 +61,7 @@ export function sshPreflight({ host, port = 22, user }) {
36
61
  };
37
62
  }
38
63
 
39
- export function rsyncDeploy({ localDir, host, port = 22, user, remotePath, excludes = [] }) {
64
+ export function rsyncDeploy({ localDir, host, port = 22, user, remotePath, excludes = [], withEnv = false }) {
40
65
  requireBinary('rsync');
41
66
  const target = `${user}@${host}:${remotePath.replace(/\/$/, '')}/`;
42
67
  const args = [
@@ -45,7 +70,7 @@ export function rsyncDeploy({ localDir, host, port = 22, user, remotePath, exclu
45
70
  '--human-readable',
46
71
  '--progress',
47
72
  '-e', `ssh -p ${port} -o StrictHostKeyChecking=accept-new`,
48
- ...excludes.flatMap((pattern) => ['--exclude', pattern]),
73
+ ...buildRsyncFilters({ withEnv, excludes }),
49
74
  `${path.resolve(localDir)}/`,
50
75
  target,
51
76
  ];
@@ -107,14 +132,19 @@ export function verifyRemoteStagingDir({ host, port = 22, user, remotePath }) {
107
132
  }
108
133
 
109
134
  /** 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 }) {
135
+ export function preserveServerDotfiles({ host, port = 22, user, publicPath, stagingPath, skipEnv = false }) {
111
136
  requireBinary('ssh');
112
137
  const target = `${user}@${host}`;
138
+ const skipEnvFlag = skipEnv ? '1' : '0';
113
139
  const cmd = [
114
140
  `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',
141
+ `&& SKIP_ENV=${skipEnvFlag}`,
142
+ `find ${shellQuote(publicPath)} -maxdepth 1 -name '.*' -type f`,
143
+ `| while IFS= read -r f; do`,
144
+ `base=$(basename "$f");`,
145
+ `if [ "$SKIP_ENV" = "1" ]; then case "$base" in .env|.env.*) continue;; esac; fi;`,
146
+ `cp -a "$f" ${shellQuote(`${stagingPath.replace(/\/$/, '')}/`)};`,
147
+ `done 2>/dev/null || true`,
118
148
  ].join(' ');
119
149
 
120
150
  const args = [