@rpcbase/cli 0.120.0 → 0.122.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
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
const runRsync = (cmd) => {
|
|
70
|
+
try {
|
|
71
|
+
execSync(cmd, { stdio: "ignore" })
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const allowed = [23, 24]
|
|
74
|
+
if (!allowed.includes(err?.status)) throw err
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pull remote compose files into a temp dir, mirror local ones into another, then hash trees to detect diffs.
|
|
79
|
+
// This is informational only; callers should treat errors as non-blocking.
|
|
80
|
+
export const detectComposeChanges = ({ keyPath, user, host, deployDir, log, extraExcludes = [] }) => {
|
|
81
|
+
const includeArgs = ["--include='*/'", "--include='*compose*.yml'", "--exclude='*'"]
|
|
82
|
+
const excludeArgs = baseExcludeList
|
|
83
|
+
.concat(extraExcludes)
|
|
84
|
+
.map((p) => `--exclude='${p}'`)
|
|
85
|
+
.join(" ")
|
|
86
|
+
|
|
87
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "rb-compose-"))
|
|
88
|
+
const remoteDir = path.join(tempRoot, "remote")
|
|
89
|
+
const localDir = path.join(tempRoot, "local")
|
|
90
|
+
fs.mkdirSync(remoteDir)
|
|
91
|
+
fs.mkdirSync(localDir)
|
|
92
|
+
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
try {
|
|
95
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.warn(`Cleanup failed for ${tempRoot}: ${err}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const remoteSyncCmd = buildRsyncCmd({
|
|
103
|
+
includeArgs,
|
|
104
|
+
excludeArgs,
|
|
105
|
+
keyPath,
|
|
106
|
+
source: `${user}@${host}:~/apps/${deployDir}/`,
|
|
107
|
+
dest: `${remoteDir}/`,
|
|
108
|
+
remote: true,
|
|
109
|
+
})
|
|
110
|
+
log?.(`Syncing remote compose files: ${remoteSyncCmd}`)
|
|
111
|
+
runRsync(remoteSyncCmd)
|
|
112
|
+
|
|
113
|
+
const localSyncCmd = buildRsyncCmd({
|
|
114
|
+
includeArgs,
|
|
115
|
+
excludeArgs,
|
|
116
|
+
keyPath,
|
|
117
|
+
source: "./",
|
|
118
|
+
dest: `${localDir}/`,
|
|
119
|
+
remote: false,
|
|
120
|
+
})
|
|
121
|
+
log?.(`Syncing local compose files: ${localSyncCmd}`)
|
|
122
|
+
runRsync(localSyncCmd)
|
|
123
|
+
|
|
124
|
+
const remoteHashes = hashTree(remoteDir)
|
|
125
|
+
const localHashes = hashTree(localDir)
|
|
126
|
+
|
|
127
|
+
const remotePaths = new Set(Object.keys(remoteHashes.fileHashes))
|
|
128
|
+
const localPaths = new Set(Object.keys(localHashes.fileHashes))
|
|
129
|
+
|
|
130
|
+
const missingLocal = [...remotePaths].filter((p) => !localPaths.has(p))
|
|
131
|
+
const missingRemote = [...localPaths].filter((p) => !remotePaths.has(p))
|
|
132
|
+
const changed = [...remotePaths].filter(
|
|
133
|
+
(p) => localPaths.has(p) && remoteHashes.fileHashes[p] !== localHashes.fileHashes[p],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const hasChanges = (
|
|
137
|
+
remoteHashes.treeHash !== localHashes.treeHash ||
|
|
138
|
+
missingLocal.length > 0 ||
|
|
139
|
+
missingRemote.length > 0 ||
|
|
140
|
+
changed.length > 0
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const summary = [
|
|
144
|
+
`local tree: ${localHashes.treeHash}`,
|
|
145
|
+
`remote tree: ${remoteHashes.treeHash}`,
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if (missingLocal.length) summary.push(`Only on remote: ${missingLocal.join(", ")}`)
|
|
149
|
+
if (missingRemote.length) summary.push(`Only on local: ${missingRemote.join(", ")}`)
|
|
150
|
+
if (changed.length) summary.push(`Content differs: ${changed.join(", ")}`)
|
|
151
|
+
if (!hasChanges) summary.push("Compose files are identical.")
|
|
152
|
+
|
|
153
|
+
return { hasChanges, output: summary.join("\n") }
|
|
154
|
+
} finally {
|
|
155
|
+
cleanup()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -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
|
|
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) => {
|
|
@@ -153,14 +88,22 @@ export const deploy = async (argv) => {
|
|
|
153
88
|
console.log(`${user}@${host}`, res)
|
|
154
89
|
await ssh(`mkdir -p ~/apps/${deployDir}`)
|
|
155
90
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
91
|
+
let infrastructureChanged = false
|
|
92
|
+
let infrastructureDiff = ""
|
|
93
|
+
try {
|
|
94
|
+
({ hasChanges: infrastructureChanged, output: infrastructureDiff } = detectComposeChanges({
|
|
95
|
+
keyPath,
|
|
96
|
+
user,
|
|
97
|
+
host,
|
|
98
|
+
deployDir,
|
|
99
|
+
log,
|
|
100
|
+
extraExcludes: argv.ignore || [],
|
|
101
|
+
}))
|
|
102
|
+
} catch (diffError) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`Compose diff check skipped (non-blocking): ${diffError instanceof Error ? diffError.message : diffError}`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
164
107
|
|
|
165
108
|
if (infrastructureChanged) {
|
|
166
109
|
console.log("Infrastructure compose files differ between local and remote (info only; deploy proceeds).")
|
|
@@ -169,7 +112,6 @@ export const deploy = async (argv) => {
|
|
|
169
112
|
|
|
170
113
|
const excludeList = baseExcludeList.concat(argv.ignore)
|
|
171
114
|
|
|
172
|
-
|
|
173
115
|
const excludeArgs = excludeList.map((p) => `--exclude='${p}'`).join(" ")
|
|
174
116
|
|
|
175
117
|
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
|
|
|
File without changes
|