querysub 0.656.0 → 0.658.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.656.0",
3
+ "version": "0.658.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",
@@ -79,8 +79,8 @@
79
79
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
80
80
  "pako": "^2.1.0",
81
81
  "peggy": "^5.0.6",
82
- "sliftutils": "^1.7.128",
83
- "socket-function": "^1.2.34",
82
+ "sliftutils": "^1.7.130",
83
+ "socket-function": "^1.2.35",
84
84
  "terser": "^5.31.0",
85
85
  "typenode": "^6.6.1",
86
86
  "typesafecss": "^0.32.0",
@@ -80,12 +80,12 @@ function extractChangedLines(diff: string): string {
80
80
  const rowButtonStyle = css.pad2(12, 8).button.bord2(0, 0, 20).fontWeight("bold");
81
81
 
82
82
  export class CommitModal extends qreact.Component<{
83
- /** True for the querysub repo, false for the main service repo. */
84
- isQuerysub?: boolean;
83
+ /** True when the commit also publishes querysub, so the diff covers both repos and the labels say so. */
84
+ includeQuerysub?: boolean;
85
85
  /** The `git status --porcelain` lines, shown as a file list. */
86
86
  changes: string[];
87
- /** Performs the actual commit + push (and publish, for querysub). */
88
- onCommit: (message: string) => Promise<void>;
87
+ /** Performs the actual commit + push (and publish, for querysub). Calls setStep as it moves through its steps, which is shown on the commit button. */
88
+ onCommit: (message: string, setStep: (step: string) => void) => Promise<void>;
89
89
  }> {
90
90
  state = t.state({
91
91
  message: t.string,
@@ -96,6 +96,7 @@ export class CommitModal extends qreact.Component<{
96
96
  diffLoading: t.boolean(true),
97
97
  summarizing: t.boolean(false),
98
98
  committing: t.boolean(false),
99
+ step: t.string,
99
100
  error: t.string,
100
101
  lastCost: t.number,
101
102
  });
@@ -108,9 +109,13 @@ export class CommitModal extends qreact.Component<{
108
109
 
109
110
  private async loadDiff() {
110
111
  try {
111
- let isQuerysub = Querysub.localRead(() => this.props.isQuerysub);
112
+ let includeQuerysub = Querysub.localRead(() => this.props.includeQuerysub);
112
113
  let controller = MachineServiceController(SocketFunction.browserNodeId());
113
- let diff = await controller.getGitDiff.promise({ useQuerysub: isQuerysub });
114
+ let diffs = await Promise.all([
115
+ includeQuerysub && controller.getGitDiff.promise({ useQuerysub: true }) || "",
116
+ controller.getGitDiff.promise({ useQuerysub: false }),
117
+ ]);
118
+ let diff = diffs.filter(x => x).join("\n");
114
119
  console.log("Received commit diff, changed lines:\n" + extractChangedLines(diff));
115
120
  Querysub.localCommit(() => {
116
121
  this.state.diff = diff;
@@ -172,22 +177,28 @@ export class CommitModal extends qreact.Component<{
172
177
  }
173
178
  Querysub.localCommit(() => {
174
179
  this.state.committing = true;
180
+ this.state.step = "";
175
181
  this.state.error = "";
176
182
  });
177
183
  try {
178
- await this.props.onCommit(message);
184
+ await this.props.onCommit(message, step => {
185
+ Querysub.localCommit(() => {
186
+ this.state.step = step;
187
+ });
188
+ });
179
189
  closeAllModals();
180
190
  } catch (e: any) {
181
191
  Querysub.localCommit(() => {
182
192
  this.state.error = `Commit failed: ${e.stack ?? e}`;
183
193
  this.state.committing = false;
194
+ this.state.step = "";
184
195
  });
185
196
  }
186
197
  }
187
198
 
188
199
  render() {
189
200
  let { diff, diffLoading, summarizing, committing } = this.state;
190
- let commitLabel = this.props.isQuerysub ? "Publish & Commit & Push" : "Commit & Push";
201
+ let commitLabel = this.props.includeQuerysub && "Publish Querysub & Commit & Push" || "Commit & Push";
191
202
 
192
203
  let summarizeLabel: string;
193
204
  if (diffLoading) {
@@ -274,7 +285,7 @@ export class CommitModal extends qreact.Component<{
274
285
  disabled={committing}
275
286
  onClick={() => void this.doCommit()}
276
287
  >
277
- {committing ? `⏳ Running ${commitLabel}…` : `⬆️ ${commitLabel}`}
288
+ {committing && `⏳ ${this.state.step || `Running ${commitLabel}…`}` || `⬆️ ${commitLabel}`}
278
289
  </button>
279
290
  </div>
280
291
 
@@ -145,46 +145,45 @@ export class UpdateButtons extends qreact.Component<{
145
145
  });
146
146
  let outdatedRefs = unique(outdatedServices.map(service => service.parameters.gitRef));
147
147
 
148
+ let querysubChanges = gitInfo?.querysubUncommitted ?? [];
149
+ let serviceChanges = gitInfo?.uncommitted ?? [];
150
+ let allChanges = [
151
+ ...querysubChanges.map(change => `querysub: ${change}`),
152
+ ...serviceChanges,
153
+ ];
154
+ let includeQuerysub = !!querysubChanges.length;
155
+
148
156
  return <>
149
- {!!gitInfo?.querysubUncommitted.length && <button
157
+ {!!allChanges.length && gitInfo && <button
150
158
  className={buttonStyle.hsl(260, 60, 90)}
151
- title={gitInfo?.querysubUncommitted.join("\n")}
159
+ title={allChanges.join("\n")}
152
160
  onClick={() => {
153
161
  showFullscreenModal({
154
162
  content: <CommitModal
155
- isQuerysub
156
- changes={gitInfo?.querysubUncommitted ?? []}
157
- onCommit={async message => {
158
- await controller.commitPushAndPublishQuerysub.promise(message);
163
+ includeQuerysub={includeQuerysub}
164
+ changes={allChanges}
165
+ onCommit={async (message, setStep) => {
166
+ if (includeQuerysub) {
167
+ setStep("Step 1/2: Publishing querysub (version bump, npm publish, commit & push, package.json update)…");
168
+ await controller.commitPushAndPublishQuerysub.promise(message);
169
+ setStep("Step 2/2: Committing & pushing the service repo…");
170
+ } else {
171
+ setStep("Committing & pushing the service repo…");
172
+ }
173
+ await controller.commitPushService.promise(message);
159
174
  }}
160
175
  />
161
176
  });
162
177
  }}
163
178
  >
164
179
  <div>
165
- {bigEmoji("📚", -5)}
166
- Publish & Commit & Push Querysub & Update Package.json ({gitInfo?.querysubUncommitted.length} Files Changed)
180
+ {bigEmoji(includeQuerysub && "📚" || "⬆️", -5)}
181
+ {includeQuerysub && "Publish Querysub & " || ""}Commit & Push All ({includeQuerysub && `${querysubChanges.length} querysub + ` || ""}{serviceChanges.length} Files Changed)
167
182
  </div>
168
- <div className={css.hbox(5)}>
183
+ {includeQuerysub && <div className={css.hbox(5)}>
169
184
  <b>Current</b>
170
185
  <RenderGitRefInfo gitRef={gitInfo.querysubRef} isQuerysub />
171
- </div>
172
- </button>}
173
- {!!gitInfo?.uncommitted.length && <button
174
- className={buttonStyle.hsl(210, 40, 90)}
175
- title={gitInfo?.uncommitted.join("\n")}
176
- onClick={() => {
177
- showFullscreenModal({
178
- content: <CommitModal
179
- changes={gitInfo?.uncommitted ?? []}
180
- onCommit={async message => {
181
- await controller.commitPushService.promise(message);
182
- }}
183
- />
184
- });
185
- }}
186
- >
187
- {bigEmoji("⬆️")} <span>Commit & Push ({gitInfo?.uncommitted.length} Files Changed)</span>
186
+ </div>}
188
187
  </button>}
189
188
 
190
189
  {outdatedServices.length > 0 && gitInfo && <>
@@ -114,6 +114,12 @@ async function writeRemoteFile(sshRemote: string, remotePath: string, content: s
114
114
  await runPromise(`ssh ${sshRemote} "echo ${encoded} | base64 -d | ${write}"`);
115
115
  }
116
116
 
117
+ /** Reads one value off the remote: the last line of the command's output, since banners and connection chatter come before it. */
118
+ async function sshValue(sshRemote: string, command: string, config?: { nothrow?: boolean }): Promise<string> {
119
+ let output = await runPromise(`ssh ${sshRemote} "${command}"`, { nothrow: config?.nothrow });
120
+ return (output.trim().split("\n").at(-1) || "").trim();
121
+ }
122
+
117
123
  function getGitHubKeyCachePath(repoUrl: string): string {
118
124
  let repoOwner = "";
119
125
  let repoName = "";
@@ -433,6 +439,17 @@ async function main() {
433
439
  console.log("✅ Build tools installed");
434
440
  }
435
441
 
442
+ // The daemon runs everything in tmux screens, so setup cannot get by without it
443
+ console.log("Ensuring tmux is installed...");
444
+ try {
445
+ await runPromise(`ssh ${sshRemote} "which tmux"`);
446
+ console.log("✅ Tmux already installed");
447
+ } catch {
448
+ console.log("Installing tmux...");
449
+ await runPromise(`ssh ${sshRemote} "sudo apt install -y tmux"`);
450
+ console.log("✅ Tmux installed");
451
+ }
452
+
436
453
  // 3. Ensure nodejs is installed on remote server
437
454
  console.log("Ensuring Node.js is installed...");
438
455
  try {
@@ -461,8 +478,9 @@ async function main() {
461
478
  await runPromise(`ssh ${sshRemote} "sudo timedatectl set-timezone America/New_York"`);
462
479
  console.log("✅ Timezone set to America/New_York");
463
480
 
481
+ // Via .tmux.conf rather than set-option, as set-option needs a running tmux server and there is none on a fresh machine
464
482
  console.log(`Setting tmux scrollback history to 100000...`);
465
- await runPromise(`ssh ${sshRemote} "tmux set-option -g history-limit 100000"`);
483
+ await runPromise(`ssh ${sshRemote} "grep -qxF 'set-option -g history-limit 100000' ~/.tmux.conf 2>/dev/null || echo 'set-option -g history-limit 100000' >> ~/.tmux.conf"`);
466
484
  console.log("✅ Tmux scrollback history set to 100000");
467
485
 
468
486
  // 5. Clone current repo into ~/machine-alwaysup with SSH key handling for private repos
@@ -491,7 +509,7 @@ async function main() {
491
509
  }
492
510
 
493
511
  // Get the public key from remote machine
494
- const sshPublicKey = await runPromise(`ssh ${sshRemote} "cat ~/.ssh/id_rsa.pub"`);
512
+ const sshPublicKey = await sshValue(sshRemote, "cat ~/.ssh/id_rsa.pub");
495
513
  const keyTitle = `Machine Setup - ${sshRemote} - ${new Date().toISOString()}`;
496
514
 
497
515
  // Add the deploy key to GitHub repository
@@ -513,8 +531,8 @@ async function main() {
513
531
  console.log("✅ Dependencies installed");
514
532
 
515
533
  // 6. Install the daemon that keeps the machine process running
516
- let remoteUser = (await runPromise(`ssh ${sshRemote} "whoami"`)).trim();
517
- let remoteHome = (await runPromise(`ssh ${sshRemote} "echo $HOME"`)).trim();
534
+ let remoteUser = await sshValue(sshRemote, "whoami");
535
+ let remoteHome = await sshValue(sshRemote, "echo $HOME");
518
536
  console.log(`Installing the ${SERVICE_UNIT_NAME} daemon (user ${remoteUser}, home ${remoteHome})...`);
519
537
  await writeRemoteFile(sshRemote, `~/${DAEMON_SCRIPT_NAME}`, getDaemonScript(remoteHome), false);
520
538
  await runPromise(`ssh ${sshRemote} "chmod +x ~/${DAEMON_SCRIPT_NAME}"`);
@@ -560,7 +578,7 @@ sudo systemctl start ${SERVICE_UNIT_NAME}
560
578
  let screenDeadline = Date.now() + SCREEN_VERIFY_TIMEOUT;
561
579
  let screenUp = false;
562
580
  while (Date.now() < screenDeadline) {
563
- let panePid = (await runPromise(`ssh ${sshRemote} "tmux list-panes -t ${SERVICE_NAME} -F '#{pane_pid}' 2>/dev/null || true"`, { nothrow: true })).trim();
581
+ let panePid = await sshValue(sshRemote, `tmux list-panes -t ${SERVICE_NAME} -F '#{pane_pid}' 2>/dev/null || true`, { nothrow: true });
564
582
  if (panePid) {
565
583
  screenUp = true;
566
584
  break;
@@ -16,6 +16,8 @@ export function shimConsoleLogs() {
16
16
  console.error(`Uncaught exception: ${String(error.stack || error)}`);
17
17
  });
18
18
  }
19
+ // Shared across log/warn/error/info, as writing one type can log another
20
+ let insideLogDisk = false;
19
21
  function hookConsoleFnc(fncName: "log" | "warn" | "error" | "info") {
20
22
  let console = globalThis.console;
21
23
  let originalFnc = console[fncName];
@@ -28,10 +30,17 @@ export function shimConsoleLogs() {
28
30
  if (
29
31
  args.length > 0
30
32
  && String(args[0]).trim().length > 0
33
+ // Writing a log can itself log (ex, building the log path generates the identity CA on a fresh machine, which logs), which would recurse forever. Those inner logs only go to the real console below, skipping the disk.
34
+ && !insideLogDisk
31
35
  ) {
32
- // Assign to a variable, so it isn't shimmed by injectFileLocationToConsole
33
- const logDiskThatIsntShimmed = diskLogger.logDisk;
34
- logDiskThatIsntShimmed(fncName, ...args);
36
+ insideLogDisk = true;
37
+ try {
38
+ // Assign to a variable, so it isn't shimmed by injectFileLocationToConsole
39
+ const logDiskThatIsntShimmed = diskLogger.logDisk;
40
+ logDiskThatIsntShimmed(fncName, ...args);
41
+ } finally {
42
+ insideLogDisk = false;
43
+ }
35
44
  }
36
45
 
37
46
  // Filter out objects added by injectFileLocationToConsole