easy-vps 0.1.8 → 0.2.0

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.
@@ -58,6 +58,12 @@ const domains = __importStar(require("./domain"));
58
58
  const ssh_1 = require("./ssh");
59
59
  /** Where clones live on the VPS, under the login user's home. */
60
60
  const APPS_DIR = '"$HOME/easy-vps-apps"';
61
+ // A non-interactive login shell misses tools installed outside the system
62
+ // prefix — nvm's node/npm in particular — so the user's install/build/run
63
+ // commands can't find them. Seed PATH the same way the system service does.
64
+ const PATH_PRIMER = 'export PATH="/usr/local/bin:/usr/bin:/snap/bin:$PATH"; ' +
65
+ 'for nvm_bin in "$HOME"/.nvm/versions/node/*/bin; do ' +
66
+ '[ -d "$nvm_bin" ] && PATH="$PATH:$nvm_bin"; done; export PATH;';
61
67
  /** Names are validated on the way in; this is the guard for any other caller. */
62
68
  const SAFE_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
63
69
  // remote.writeFile()/readFile() single-quote their path argument, so a literal
@@ -232,17 +238,21 @@ function buildScript(config) {
232
238
  // Double quotes, not quote(): the shell has to expand $HOME itself.
233
239
  const dir = `"$HOME/easy-vps-apps/${config.name}"`;
234
240
  return [
241
+ PATH_PRIMER,
235
242
  'set -e',
236
243
  `echo "==> Deploying ${config.name} (${config.domain ?? `port ${config.port}`})"`,
237
244
  `mkdir -p ${APPS_DIR}`,
238
245
  // Embed the stored Github token straight into the clone URL so the fetch
239
246
  // authenticates without depending on the credential helper being picked up
240
- // inside this streaming login shell. Falls back to the plain URL (credential
241
- // helper still applies) when no token is configured.
247
+ // inside this streaming login shell. Prefers the panel's state file, then
248
+ // git's credential store, then falls back to the plain URL.
242
249
  `REPO=${(0, ssh_1.quote)(config.repository)}`,
243
250
  step('Resolving Github credentials'),
244
251
  'TOKEN=""\n' +
245
- 'if [ -f ~/.git-credentials ]; then\n' +
252
+ 'if [ -f ~/.easy-vps/github.json ]; then\n' +
253
+ ' TOKEN=$(grep -o \'"token"[[:space:]]*:[[:space:]]*"[^"]*"\' ~/.easy-vps/github.json | head -n1 | sed -E \'s/.*:"([^"]*)"/\\1/\')\n' +
254
+ 'fi\n' +
255
+ 'if [ -z "$TOKEN" ] && [ -f ~/.git-credentials ]; then\n' +
246
256
  ' TOKEN=$(grep "github.com" ~/.git-credentials 2>/dev/null | head -n1 | sed -E "s#^https://([^@]+)@github.com#\\1#" | sed -E "s#^[^:]+:##")\n' +
247
257
  'fi\n' +
248
258
  'if [ -n "$TOKEN" ]; then\n' +
@@ -281,6 +291,7 @@ function rebuildScript(config) {
281
291
  throw new Error('Invalid project name');
282
292
  const dir = `"$HOME/easy-vps-apps/${config.name}"`;
283
293
  return [
294
+ PATH_PRIMER,
284
295
  'set -e',
285
296
  `echo "==> Rebuilding ${config.name} (${config.domain ?? `port ${config.port}`})"`,
286
297
  `if [ ! -d "$HOME/easy-vps-apps/${config.name}/.git" ]; then
@@ -4,7 +4,7 @@ export interface GithubConfig {
4
4
  email: string;
5
5
  /** The stored personal access token; returned so the UI can prefill it. */
6
6
  personalAccessToken: string;
7
- /** True when a token is already stored in the credential store on the server. */
7
+ /** True when a token is already stored on the server. */
8
8
  tokenSet: boolean;
9
9
  }
10
10
  /** Only the fields the user may change; each is optional on update. */
@@ -17,7 +17,8 @@ export interface GithubConfigPatch {
17
17
  export declare function getGithubConfig(remote: Remote): Promise<GithubConfig>;
18
18
  /**
19
19
  * Applies the supplied github identity over SSH and stores the personal access
20
- * token in git's credential store so HTTPS clones can authenticate. Returns the
21
- * resulting config (without the token, which is never sent back).
20
+ * token in git's credential store (so HTTPS clones authenticate) and in the
21
+ * panel's state file (the reliable source of truth for display and deploys).
22
+ * Returns the resulting config.
22
23
  */
23
24
  export declare function updateGithubConfig(remote: Remote, patch: GithubConfigPatch): Promise<GithubConfig>;
@@ -4,18 +4,70 @@ exports.getGithubConfig = getGithubConfig;
4
4
  exports.updateGithubConfig = updateGithubConfig;
5
5
  // Github identity and credential configuration on the managed VPS, over SSH.
6
6
  const ssh_1 = require("./ssh");
7
+ const STATE_DIR = '.easy-vps';
8
+ const STATE_FILE = 'github.json';
7
9
  function safeTrim(value) {
8
10
  return typeof value === 'string' ? value.trim() : '';
9
11
  }
12
+ /** Resolves the login user's home directory on the remote. */
13
+ async function homeDir(remote) {
14
+ const { stdout } = await remote.exec('echo $HOME', 10_000);
15
+ return stdout.trim() || '';
16
+ }
17
+ /** Path to the panel-owned state file holding the github config. */
18
+ async function statePath(remote) {
19
+ const home = await homeDir(remote);
20
+ return `${home}/${STATE_DIR}/${STATE_FILE}`;
21
+ }
22
+ /** Reads the panel's own state file; null when it has never been written. */
23
+ async function readState(remote) {
24
+ const path = await statePath(remote);
25
+ if (!(await remote.exists(path)))
26
+ return null;
27
+ try {
28
+ const parsed = JSON.parse(await remote.readFile(path));
29
+ return {
30
+ username: parsed.username ?? '',
31
+ email: parsed.email ?? '',
32
+ token: parsed.token ?? '',
33
+ };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** Persists the github config to the panel's state file (written via tee). */
40
+ async function writeState(remote, username, email, token) {
41
+ const home = await homeDir(remote);
42
+ const dir = `${home}/${STATE_DIR}`;
43
+ const path = `${dir}/${STATE_FILE}`;
44
+ await remote.exec(`mkdir -p ${(0, ssh_1.quote)(dir)}`);
45
+ await remote.writeFile(path, JSON.stringify({ username, email, token }));
46
+ }
47
+ /** Extracts a stored token from git's credential store, if present. */
48
+ async function tokenFromCredentials(remote) {
49
+ const result = await remote.exec((0, ssh_1.loginShell)("cred=$(grep 'github.com' ~/.git-credentials 2>/dev/null | head -n1); " +
50
+ "if [ -n \"$cred\" ]; then " +
51
+ "printf '%s' \"$cred\" | sed -E 's#^https://([^@]+)@github.com#\\1#' | sed -E 's#^[^:]+:##'; " +
52
+ "fi"), 10_000);
53
+ return result.stdout.trim();
54
+ }
10
55
  /** Reads the git identity currently configured for the remote user. */
11
56
  async function getGithubConfig(remote) {
57
+ // Prefer the panel's state file so display always reflects what was saved.
58
+ const state = await readState(remote);
59
+ if (state) {
60
+ return {
61
+ username: state.username,
62
+ email: state.email,
63
+ personalAccessToken: state.token,
64
+ tokenSet: state.token !== '',
65
+ };
66
+ }
67
+ // Fall back to git config + the credential store for partial setups.
12
68
  const nameResult = await remote.exec((0, ssh_1.loginShell)('git config --global --get user.name 2>/dev/null'), 10_000);
13
69
  const emailResult = await remote.exec((0, ssh_1.loginShell)('git config --global --get user.email 2>/dev/null'), 10_000);
14
- const tokenResult = await remote.exec((0, ssh_1.loginShell)("cred=$(grep 'github.com' ~/.git-credentials 2>/dev/null | head -n1); " +
15
- "if [ -n \"$cred\" ]; then " +
16
- "printf '%s' \"$cred\" | sed -E 's#^https://([^@]+)@github.com/?$#\\1#' | sed -E 's#^[^:]+:##'; " +
17
- "fi"), 10_000);
18
- const token = tokenResult.stdout.trim();
70
+ const token = await tokenFromCredentials(remote);
19
71
  return {
20
72
  username: nameResult.stdout.trim(),
21
73
  email: emailResult.stdout.trim(),
@@ -25,13 +77,15 @@ async function getGithubConfig(remote) {
25
77
  }
26
78
  /**
27
79
  * Applies the supplied github identity over SSH and stores the personal access
28
- * token in git's credential store so HTTPS clones can authenticate. Returns the
29
- * resulting config (without the token, which is never sent back).
80
+ * token in git's credential store (so HTTPS clones authenticate) and in the
81
+ * panel's state file (the reliable source of truth for display and deploys).
82
+ * Returns the resulting config.
30
83
  */
31
84
  async function updateGithubConfig(remote, patch) {
32
- const username = safeTrim(patch.username);
33
- const email = safeTrim(patch.email);
34
- const token = safeTrim(patch.personalAccessToken);
85
+ const existing = await getGithubConfig(remote);
86
+ const username = patch.username !== undefined ? safeTrim(patch.username) : existing.username;
87
+ const email = patch.email !== undefined ? safeTrim(patch.email) : existing.email;
88
+ const token = patch.personalAccessToken !== undefined ? safeTrim(patch.personalAccessToken) : existing.personalAccessToken;
35
89
  if (patch.username !== undefined && username === '') {
36
90
  throw new Error('Github username is required.');
37
91
  }
@@ -59,10 +113,14 @@ async function updateGithubConfig(remote, patch) {
59
113
  `printf '%s\\n' ${(0, ssh_1.quote)(url)} >> ~/.git-credentials; ` +
60
114
  `chmod 600 ~/.git-credentials`);
61
115
  }
62
- const script = (0, ssh_1.loginShell)(commands.join('\n'));
63
- const { code, stderr } = await remote.exec(script, 30_000);
64
- if (code !== 0) {
65
- throw new Error(`Failed to apply github configuration: ${stderr.trim()}`);
116
+ if (commands.length > 0) {
117
+ const script = (0, ssh_1.loginShell)(commands.join('\n'));
118
+ const { code, stderr } = await remote.exec(script, 30_000);
119
+ if (code !== 0) {
120
+ throw new Error(`Failed to apply github configuration: ${stderr.trim()}`);
121
+ }
66
122
  }
123
+ // Always persist the merged config to the panel's state file.
124
+ await writeState(remote, username, email, token);
67
125
  return getGithubConfig(remote);
68
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-vps",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Install dependencies, point domains, deploy apps and run Postgres on a VPS from one UI",
5
5
  "keywords": [
6
6
  "easy-vps",