querysub 0.627.0 → 0.628.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": "querysub",
3
- "version": "0.627.0",
3
+ "version": "0.628.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -1,19 +1,92 @@
1
1
  import * as child_process from "child_process";
2
2
  import { cache } from "socket-function/src/caching";
3
- import { runPromise } from "../functional/runCommand";
4
3
  import { measureWrap } from "socket-function/src/profiling/measure";
4
+ import { timeInMinute } from "socket-function/src/misc";
5
+ import { blue, red } from "socket-function/src/formatting/logColors";
6
+ import { formatTime } from "socket-function/src/formatting/format";
5
7
  import fs from "fs";
6
8
  import os from "os";
7
9
  import path from "path";
10
+
11
+ // Resolving on 'close' (as socket-function's runPromise does) waits for the stdio pipes to close, and git background daemons (ex, fsmonitor, which a plain `git status` auto-starts on Windows) inherit those pipe handles and keep them open forever after git itself has exited. So we resolve on 'exit', giving remaining output this long to drain first.
12
+ const OUTPUT_DRAIN_TIME = 100;
13
+ // A hard ceiling so a wedged command (a fetch that never completes, a credential prompt with no console) fails loudly instead of hanging the caller forever. Generous, as clones of large repos legitimately take minutes.
14
+ const COMMAND_TIMEOUT = 10 * timeInMinute;
15
+
16
+ async function runGitCommand(command: string, config?: {
17
+ cwd?: string;
18
+ quiet?: boolean;
19
+ nothrow?: boolean;
20
+ }): Promise<string> {
21
+ if (!config?.quiet) {
22
+ console.log(">" + blue(command));
23
+ }
24
+ return new Promise<string>((resolve, reject) => {
25
+ let childProc = child_process.spawn(command, {
26
+ shell: true,
27
+ cwd: config?.cwd,
28
+ stdio: ["inherit", "pipe", "pipe"],
29
+ });
30
+ let fullOutput = "";
31
+ let stderr = "";
32
+ let finished = false;
33
+ function finish(result: { code: number | null } | { error: Error }) {
34
+ if (finished) return;
35
+ finished = true;
36
+ clearTimeout(timeoutHandle);
37
+ if ("error" in result) {
38
+ if (config?.nothrow) {
39
+ resolve(fullOutput);
40
+ } else {
41
+ reject(result.error);
42
+ }
43
+ return;
44
+ }
45
+ if (result.code === 0 || config?.nothrow) {
46
+ resolve(fullOutput);
47
+ } else {
48
+ let errorMessage = `Process exited with code ${result.code} for command: ${command} (cwd ${path.resolve(config?.cwd || ".")})`;
49
+ if (stderr) {
50
+ errorMessage += `\n${stderr}`;
51
+ }
52
+ reject(new Error(errorMessage));
53
+ }
54
+ }
55
+ let timeoutHandle = setTimeout(() => {
56
+ childProc.kill();
57
+ finish({ error: new Error(`Command timed out after ${formatTime(COMMAND_TIMEOUT)}: ${command} (cwd ${path.resolve(config?.cwd || ".")})\n${fullOutput}`) });
58
+ }, COMMAND_TIMEOUT);
59
+ childProc.stdout?.on("data", data => {
60
+ let chunk = data.toString();
61
+ fullOutput += chunk;
62
+ if (!config?.quiet) {
63
+ process.stdout.write(chunk);
64
+ }
65
+ });
66
+ childProc.stderr?.on("data", data => {
67
+ let chunk = data.toString();
68
+ stderr += chunk;
69
+ fullOutput += chunk;
70
+ if (!config?.quiet) {
71
+ process.stderr.write(red(chunk));
72
+ }
73
+ });
74
+ childProc.on("error", error => finish({ error }));
75
+ childProc.on("exit", code => {
76
+ setTimeout(() => finish({ code }), OUTPUT_DRAIN_TIME);
77
+ });
78
+ childProc.on("close", code => finish({ code }));
79
+ });
80
+ }
8
81
  export async function getGitURLLive(gitDir = ".") {
9
- return (await runPromise(`git remote get-url origin`, { cwd: gitDir })).trim();
82
+ return (await runGitCommand(`git remote get-url origin`, { cwd: gitDir })).trim();
10
83
  }
11
84
  export async function getGitRefLive(gitDir = ".") {
12
- return (await runPromise(`git rev-parse HEAD`, { cwd: gitDir })).trim();
85
+ return (await runGitCommand(`git rev-parse HEAD`, { cwd: gitDir })).trim();
13
86
  }
14
87
 
15
88
  export async function getGitUncommitted(gitDir = "."): Promise<string[]> {
16
- return (await runPromise(`git status --porcelain`, { cwd: gitDir })).split("\n").map(x => x.trim()).filter(x => x);
89
+ return (await runGitCommand(`git status --porcelain`, { cwd: gitDir })).split("\n").map(x => x.trim()).filter(x => x);
17
90
  }
18
91
 
19
92
  // Cap the diff we hand to the client / AI, so a huge accidental change doesn't
@@ -29,14 +102,14 @@ export async function getGitDiff(gitDir = "."): Promise<string> {
29
102
  let sections: string[] = [];
30
103
 
31
104
  // Tracked modifications (staged + unstaged) relative to the last commit.
32
- let trackedDiff = await runPromise(`git diff HEAD`, { cwd: gitDir, quiet: true });
105
+ let trackedDiff = await runGitCommand(`git diff HEAD`, { cwd: gitDir, quiet: true });
33
106
  if (trackedDiff.trim()) {
34
107
  sections.push(trackedDiff);
35
108
  }
36
109
 
37
110
  // Untracked files don't show up in `git diff HEAD`, so include their
38
111
  // contents as all-addition hunks.
39
- let untrackedFiles = (await runPromise(`git ls-files --others --exclude-standard`, { cwd: gitDir, quiet: true }))
112
+ let untrackedFiles = (await runGitCommand(`git ls-files --others --exclude-standard`, { cwd: gitDir, quiet: true }))
40
113
  .split("\n").map(x => x.trim()).filter(x => x);
41
114
  for (let file of untrackedFiles) {
42
115
  let content: string;
@@ -72,8 +145,8 @@ export async function getGitDiff(gitDir = "."): Promise<string> {
72
145
  }
73
146
 
74
147
  export async function getLatestRefOnUpstreamBranch(gitDir = ".") {
75
- await runPromise(`git fetch`, { cwd: gitDir });
76
- return (await runPromise(`git rev-parse @{upstream}`, { cwd: gitDir })).trim();
148
+ await runGitCommand(`git fetch`, { cwd: gitDir });
149
+ return (await runGitCommand(`git rev-parse @{upstream}`, { cwd: gitDir })).trim();
77
150
  }
78
151
 
79
152
  export async function commitAndPush(config: {
@@ -85,9 +158,9 @@ export async function commitAndPush(config: {
85
158
 
86
159
  try {
87
160
  await fs.promises.writeFile(tempFile, config.message, "utf8");
88
- await runPromise(`git add --all`, { cwd: config.gitDir });
89
- await runPromise(`git commit -F "${tempFile}"`, { cwd: config.gitDir });
90
- await runPromise(`git push`, { cwd: config.gitDir });
161
+ await runGitCommand(`git add --all`, { cwd: config.gitDir });
162
+ await runGitCommand(`git commit -F "${tempFile}"`, { cwd: config.gitDir });
163
+ await runGitCommand(`git push`, { cwd: config.gitDir });
91
164
  } finally {
92
165
  // Clean up temporary file
93
166
  try {
@@ -105,8 +178,8 @@ export async function getGitRefInfo(config: {
105
178
  time: number;
106
179
  description: string;
107
180
  }> {
108
- const timeOutput = await runPromise(`git show --format="%ct" -s ${config.ref}`, { cwd: config.gitDir });
109
- const descriptionOutput = await runPromise(`git show --format="%B" -s ${config.ref}`, { cwd: config.gitDir });
181
+ const timeOutput = await runGitCommand(`git show --format="%ct" -s ${config.ref}`, { cwd: config.gitDir });
182
+ const descriptionOutput = await runGitCommand(`git show --format="%B" -s ${config.ref}`, { cwd: config.gitDir });
110
183
 
111
184
  return {
112
185
  time: parseInt(timeOutput.trim(), 10) * 1000,
@@ -128,7 +201,7 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
128
201
  await fs.promises.mkdir(config.gitFolder, { recursive: true });
129
202
  // ssh-keyscan is a network call; a transient failure here must not abort the sync. The host key is normally already in known_hosts from machine setup, and callers only escalate to clobbering the checkout on *repo* failures — not on a momentary inability to refresh a host key.
130
203
  try {
131
- let hostKey = await runPromise(`ssh-keyscan -t rsa bitbucket.org`);
204
+ let hostKey = await runGitCommand(`ssh-keyscan -t rsa bitbucket.org`);
132
205
  hostKey = hostKey.split("\n").filter(x => !x.startsWith("#")).join("\n");
133
206
  let knownHostsPath = os.homedir() + "/.ssh/known_hosts";
134
207
  if (hostKey && (!fs.existsSync(knownHostsPath) || !fs.readFileSync(knownHostsPath).toString().includes(hostKey))) {
@@ -138,14 +211,14 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
138
211
  console.warn(`ssh-keyscan for bitbucket.org failed, continuing: ${e.stack ?? e}`);
139
212
  }
140
213
 
141
- await runPromise(`git remote update`, { cwd: config.gitFolder });
142
- await runPromise(`git add --all`, { cwd: config.gitFolder });
214
+ await runGitCommand(`git remote update`, { cwd: config.gitFolder });
215
+ await runGitCommand(`git add --all`, { cwd: config.gitFolder });
143
216
  // nothrow: on a freshly re-initialized repo (rebuildGitFolder) there is no initial commit yet, so `git stash` errors. That's harmless — the `git reset --hard` below is what actually forces the working tree to the target ref.
144
- await runPromise(`git stash`, { cwd: config.gitFolder, nothrow: true });
145
- await runPromise(`git fetch --all`, { cwd: config.gitFolder });
146
- await runPromise(`git reset --hard ${config.gitRef}`, { cwd: config.gitFolder });
217
+ await runGitCommand(`git stash`, { cwd: config.gitFolder, nothrow: true });
218
+ await runGitCommand(`git fetch --all`, { cwd: config.gitFolder });
219
+ await runGitCommand(`git reset --hard ${config.gitRef}`, { cwd: config.gitFolder });
147
220
  // Allows us to remove deleted objects from storage, ex, if we accidentally commit a 1GB, this deletes it, so we don't have to fix each server individually. Also I think the repo breaks if we go too long without pruning it?
148
- await runPromise(`git prune`, { cwd: config.gitFolder });
221
+ await runGitCommand(`git prune`, { cwd: config.gitFolder });
149
222
  });
150
223
 
151
224
  /** Recovers a broken repo WITHOUT disturbing the working-tree files. We delete only .git, re-init, re-add the remote, then let setGitRef's `git reset --hard` reconcile the tracked files to the ref. Untracked / gitignored files (notably node_modules, which is expensive to reinstall and may be open in a running service) are left exactly as they are. When the directory is empty this produces the same result as a fresh clone. */
@@ -156,8 +229,8 @@ export const rebuildGitFolder = measureWrap(async function rebuildGitFolder(conf
156
229
  }) {
157
230
  await fs.promises.mkdir(config.gitFolder, { recursive: true });
158
231
  await fs.promises.rm(config.gitFolder + ".git", { recursive: true, force: true });
159
- await runPromise(`git init`, { cwd: config.gitFolder });
160
- await runPromise(`git remote add origin ${config.repoUrl}`, { cwd: config.gitFolder });
232
+ await runGitCommand(`git init`, { cwd: config.gitFolder });
233
+ await runGitCommand(`git remote add origin ${config.repoUrl}`, { cwd: config.gitFolder });
161
234
  await setGitRef({ gitFolder: config.gitFolder, gitRef: config.gitRef });
162
235
  });
163
236
 
@@ -170,7 +243,7 @@ export const nuclearReclone = measureWrap(async function nuclearReclone(config:
170
243
  let target = config.gitFolder.replace(/[\/\\]+$/, "");
171
244
  let tempDir = target + ".reclone-" + Date.now();
172
245
  await fs.promises.rm(tempDir, { recursive: true, force: true });
173
- await runPromise(`git clone ${config.repoUrl} ${tempDir}`);
246
+ await runGitCommand(`git clone ${config.repoUrl} ${tempDir}`);
174
247
  await setGitRef({ gitFolder: tempDir + "/", gitRef: config.gitRef });
175
248
  await fs.promises.rm(target, { recursive: true, force: true });
176
249
  await fs.promises.rename(tempDir, target);
@@ -1,6 +1,7 @@
1
1
  process.env.PRODUCTION = "true";
2
2
  import { magenta } from "socket-function/src/formatting/logColors";
3
3
  import "./inject";
4
+ process.argv.push("--nobreak");
4
5
 
5
6
  // The storage server entry point (see bin/storageserve.js): sets up the CLI args the sliftutils storage server expects, synchronizes our network trust list into its trust store, and runs its CLI.
6
7