@rpcbase/cli 0.120.0 → 0.121.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/cli",
3
- "version": "0.120.0",
3
+ "version": "0.121.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -0,0 +1,148 @@
1
+ import crypto from "crypto"
2
+ import fs from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import { execSync } from "child_process"
6
+
7
+
8
+ export const baseExcludeList = [
9
+ ".husky/",
10
+ ".github/",
11
+ ".git/",
12
+ ".wireit/",
13
+ "coverage/",
14
+ "node_modules/",
15
+ "infrastructure/data/",
16
+ ".gitignore",
17
+ "*.css.map",
18
+ // "*.env*",
19
+ // "*.js.map",
20
+ "*.md",
21
+ ]
22
+
23
+ const hashTree = (rootDir) => {
24
+ const fileHashes = {}
25
+
26
+ const walk = (dir) => {
27
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ const fullPath = path.join(dir, entry.name)
29
+ if (entry.isDirectory()) {
30
+ walk(fullPath)
31
+ } else {
32
+ const rel = path.relative(rootDir, fullPath)
33
+ const content = fs.readFileSync(fullPath)
34
+ const digest = crypto.createHash("sha256").update(content).digest("hex")
35
+ fileHashes[rel] = digest
36
+ }
37
+ }
38
+ }
39
+
40
+ walk(rootDir)
41
+
42
+ const aggregate = crypto.createHash("sha256")
43
+ Object.keys(fileHashes).sort().forEach((rel) => {
44
+ aggregate.update(rel)
45
+ aggregate.update("\0")
46
+ aggregate.update(fileHashes[rel])
47
+ aggregate.update("\0")
48
+ })
49
+
50
+ return {
51
+ treeHash: aggregate.digest("hex"),
52
+ fileHashes,
53
+ }
54
+ }
55
+
56
+ const buildRsyncCmd = ({ includeArgs, excludeArgs, keyPath, source, dest, remote }) => {
57
+ const parts = [
58
+ "rsync -az --delete --filter=':- .gitignore'",
59
+ includeArgs.join(" "),
60
+ excludeArgs,
61
+ remote ? `-e "ssh -i '${keyPath}' -o StrictHostKeyChecking=no"` : null,
62
+ source,
63
+ dest,
64
+ ].filter(Boolean)
65
+
66
+ return parts.join(" ")
67
+ }
68
+
69
+ export const detectComposeChanges = ({ keyPath, user, host, deployDir, log, extraExcludes = [] }) => {
70
+ const includeArgs = ["--include='*/'", "--include='*compose*.yml'", "--exclude='*'"]
71
+ const excludeArgs = baseExcludeList
72
+ .concat(extraExcludes)
73
+ .map((p) => `--exclude='${p}'`)
74
+ .join(" ")
75
+
76
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "rb-compose-"))
77
+ const remoteDir = path.join(tempRoot, "remote")
78
+ const localDir = path.join(tempRoot, "local")
79
+ fs.mkdirSync(remoteDir)
80
+ fs.mkdirSync(localDir)
81
+
82
+ const cleanup = () => {
83
+ try {
84
+ fs.rmSync(tempRoot, { recursive: true, force: true })
85
+ } catch (err) {
86
+ console.warn(`Cleanup failed for ${tempRoot}: ${err}`)
87
+ }
88
+ }
89
+
90
+ try {
91
+ // fetch remote compose files into temp dir
92
+ const remoteSyncCmd = buildRsyncCmd({
93
+ includeArgs,
94
+ excludeArgs,
95
+ keyPath,
96
+ source: `${user}@${host}:~/apps/${deployDir}/`,
97
+ dest: `${remoteDir}/`,
98
+ remote: true,
99
+ })
100
+ log(`Syncing remote compose files: ${remoteSyncCmd}`)
101
+ execSync(remoteSyncCmd, { stdio: "ignore" })
102
+
103
+ // copy local compose files into temp dir (same filters to keep parity)
104
+ const localSyncCmd = buildRsyncCmd({
105
+ includeArgs,
106
+ excludeArgs,
107
+ keyPath,
108
+ source: "./",
109
+ dest: `${localDir}/`,
110
+ remote: false,
111
+ })
112
+ log(`Syncing local compose files: ${localSyncCmd}`)
113
+ execSync(localSyncCmd, { stdio: "ignore" })
114
+
115
+ const remoteHashes = hashTree(remoteDir)
116
+ const localHashes = hashTree(localDir)
117
+
118
+ const remotePaths = new Set(Object.keys(remoteHashes.fileHashes))
119
+ const localPaths = new Set(Object.keys(localHashes.fileHashes))
120
+
121
+ const missingLocal = [...remotePaths].filter((p) => !localPaths.has(p))
122
+ const missingRemote = [...localPaths].filter((p) => !remotePaths.has(p))
123
+ const changed = [...remotePaths].filter(
124
+ (p) => localPaths.has(p) && remoteHashes.fileHashes[p] !== localHashes.fileHashes[p],
125
+ )
126
+
127
+ const hasChanges = (
128
+ remoteHashes.treeHash !== localHashes.treeHash ||
129
+ missingLocal.length > 0 ||
130
+ missingRemote.length > 0 ||
131
+ changed.length > 0
132
+ )
133
+
134
+ const summary = [
135
+ `local tree: ${localHashes.treeHash}`,
136
+ `remote tree: ${remoteHashes.treeHash}`,
137
+ ]
138
+
139
+ if (missingLocal.length) summary.push(`Only on remote: ${missingLocal.join(", ")}`)
140
+ if (missingRemote.length) summary.push(`Only on local: ${missingRemote.join(", ")}`)
141
+ if (changed.length) summary.push(`Content differs: ${changed.join(", ")}`)
142
+ if (!hasChanges) summary.push("Compose files are identical.")
143
+
144
+ return { hasChanges, output: summary.join("\n") }
145
+ } finally {
146
+ cleanup()
147
+ }
148
+ }
@@ -1,77 +1,12 @@
1
1
  import { execSync } from "child_process"
2
2
  import fs from "fs"
3
- import path from "path"
4
3
  import os from "os"
4
+ import path from "path"
5
5
 
6
6
  import validator from "validator"
7
7
 
8
- import { purgeCloudflareCaches } from "./cloudflare-purge.js"
9
-
10
-
11
- const baseExcludeList = [
12
- ".husky/",
13
- ".github/",
14
- ".git/",
15
- ".wireit/",
16
- "coverage/",
17
- "node_modules/",
18
- "infrastructure/data/",
19
- ".gitignore",
20
- "*.css.map",
21
- // "*.env*",
22
- // "*.js.map",
23
- "*.md",
24
- ]
25
-
26
-
27
- const detectComposeChanges = ({ keyPath, user, host, deployDir, log, extraExcludes = [] }) => {
28
- const includeArgs = ["--include='*/'", "--include='*compose*.yml'", "--exclude='*'"]
29
- const excludeArgs = baseExcludeList
30
- .concat(extraExcludes)
31
- .map((p) => `--exclude='${p}'`)
32
- .join(" ")
33
-
34
- // keep --delete so remote-only compose files show up as *deleting in dry-run; dropping it hides stale files
35
- const rsyncCmd = [
36
- "rsync -avzn --delete --itemize-changes --filter=':- .gitignore'",
37
- includeArgs.join(" "),
38
- excludeArgs,
39
- `-e "ssh -i '${keyPath}' -o StrictHostKeyChecking=no"`,
40
- ".",
41
- `${user}@${host}:~/apps/${deployDir}/`,
42
- ].filter(Boolean).join(" ")
43
-
44
- log(`Checking compose files (dry-run): ${rsyncCmd}`)
45
-
46
- let out
47
- try {
48
- out = execSync(rsyncCmd, { stdio: "pipe" }).toString()
49
- } catch (error) {
50
- const allowedExitCodes = [23, 24]
51
- if (allowedExitCodes.includes(error?.status) && error?.stdout) {
52
- out = error.stdout.toString()
53
- } else {
54
- throw error
55
- }
56
- }
57
-
58
- // rsync --itemize-changes prefixes change lines; any line that isn't a summary indicates a diff
59
- const changeLines = out
60
- .split("\n")
61
- .map((line) => line.trim())
62
- .filter((line) => {
63
- if (!line) return false
64
- if (line.startsWith("sending ")) return false
65
- if (line.startsWith("sent ")) return false
66
- if (line.startsWith("total size")) return false
67
- if (line.startsWith("receiving incremental file list")) return false
68
- return true
69
- })
70
-
71
- const hasChanges = changeLines.length > 0
72
-
73
- return { hasChanges, output: out }
74
- }
8
+ import { purgeCloudflareCaches } from "./purge-cloudflare.js"
9
+ import { baseExcludeList, detectComposeChanges } from "./detect-compose-changes.js"
75
10
 
76
11
 
77
12
  export const deploy = async (argv) => {
@@ -169,7 +104,6 @@ export const deploy = async (argv) => {
169
104
 
170
105
  const excludeList = baseExcludeList.concat(argv.ignore)
171
106
 
172
-
173
107
  const excludeArgs = excludeList.map((p) => `--exclude='${p}'`).join(" ")
174
108
 
175
109
  const rsyncCmd = [
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ import { hideBin } from "yargs/helpers"
7
7
  import dotenv from "dotenv"
8
8
  import { expand } from "dotenv-expand"
9
9
 
10
- import { deploy } from "./cmd-deploy.js"
10
+ import { deploy } from "./cmd-deploy/index.js"
11
11
  import { ssh } from "./cmd-ssh.js"
12
12
  import { waitFor } from "./cmd-wait-for/index.js"
13
13