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 +9 -6
- package/package.json +1 -1
- package/src/commands/deploy.js +13 -2
- package/src/commands/link.js +1 -1
- package/src/commands/login.js +1 -1
- package/src/index.js +2 -1
- package/src/lib/api.js +5 -2
- package/src/lib/rsync.js +37 -7
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).
|
|
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`
|
|
52
|
-
4. cli-service calls server-service to queue a CLI deployment
|
|
53
|
-
5. The agent syncs staging → public,
|
|
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
package/src/commands/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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.'));
|
package/src/commands/link.js
CHANGED
package/src/commands/login.js
CHANGED
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`, {
|
|
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
|
-
...
|
|
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
|
-
`&&
|
|
116
|
-
|
|
117
|
-
|
|
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 = [
|