@bitsocial/bitsocial-cli 0.19.66 → 0.19.68

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/README.md CHANGED
@@ -421,7 +421,7 @@ EXAMPLES
421
421
  $ bitsocial challenge install ./my-local-challenge
422
422
  ```
423
423
 
424
- _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/install.ts)_
424
+ _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/challenge/install.ts)_
425
425
 
426
426
  ## `bitsocial challenge list`
427
427
 
@@ -447,7 +447,7 @@ EXAMPLES
447
447
  $ bitsocial challenge list -q
448
448
  ```
449
449
 
450
- _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/list.ts)_
450
+ _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/challenge/list.ts)_
451
451
 
452
452
  ## `bitsocial challenge ls`
453
453
 
@@ -501,7 +501,7 @@ EXAMPLES
501
501
  $ bitsocial challenge remove @scope/my-challenge
502
502
  ```
503
503
 
504
- _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/remove.ts)_
504
+ _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/challenge/remove.ts)_
505
505
 
506
506
  ## `bitsocial challenge rm NAME`
507
507
 
@@ -615,7 +615,7 @@ EXAMPLES
615
615
  $ bitsocial community create --jsonFile ./create-options.json
616
616
  ```
617
617
 
618
- _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/create.ts)_
618
+ _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/create.ts)_
619
619
 
620
620
  ## `bitsocial community delete ADDRESSES`
621
621
 
@@ -640,7 +640,7 @@ EXAMPLES
640
640
  $ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
641
641
  ```
642
642
 
643
- _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/delete.ts)_
643
+ _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/delete.ts)_
644
644
 
645
645
  ## `bitsocial community edit ADDRESS`
646
646
 
@@ -710,7 +710,7 @@ EXAMPLES
710
710
  $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
711
711
  ```
712
712
 
713
- _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/edit.ts)_
713
+ _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/edit.ts)_
714
714
 
715
715
  ## `bitsocial community export [ADDRESS]`
716
716
 
@@ -751,7 +751,7 @@ EXAMPLES
751
751
  $ bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
752
752
  ```
753
753
 
754
- _See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/export.ts)_
754
+ _See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/export.ts)_
755
755
 
756
756
  ## `bitsocial community get [ADDRESS]`
757
757
 
@@ -782,7 +782,7 @@ EXAMPLES
782
782
  $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
783
783
  ```
784
784
 
785
- _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/get.ts)_
785
+ _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/get.ts)_
786
786
 
787
787
  ## `bitsocial community list`
788
788
 
@@ -805,7 +805,7 @@ EXAMPLES
805
805
  $ bitsocial community list
806
806
  ```
807
807
 
808
- _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/list.ts)_
808
+ _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/list.ts)_
809
809
 
810
810
  ## `bitsocial community start ADDRESSES`
811
811
 
@@ -839,7 +839,7 @@ EXAMPLES
839
839
  $ bitsocial community start $(bitsocial community list -q) --concurrency 1
840
840
  ```
841
841
 
842
- _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/start.ts)_
842
+ _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/start.ts)_
843
843
 
844
844
  ## `bitsocial community stop ADDRESSES`
845
845
 
@@ -864,7 +864,7 @@ EXAMPLES
864
864
  $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
865
865
  ```
866
866
 
867
- _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/stop.ts)_
867
+ _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/community/stop.ts)_
868
868
 
869
869
  ## `bitsocial daemon`
870
870
 
@@ -911,7 +911,7 @@ EXAMPLES
911
911
  $ bitsocial daemon --no-allowPrivateKeyExport
912
912
  ```
913
913
 
914
- _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/daemon.ts)_
914
+ _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/daemon.ts)_
915
915
 
916
916
  ## `bitsocial help [COMMAND]`
917
917
 
@@ -977,7 +977,7 @@ EXAMPLES
977
977
  $ bitsocial logs --stdout -f
978
978
  ```
979
979
 
980
- _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/logs.ts)_
980
+ _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/logs.ts)_
981
981
 
982
982
  ## `bitsocial update check`
983
983
 
@@ -994,7 +994,7 @@ EXAMPLES
994
994
  $ bitsocial update check
995
995
  ```
996
996
 
997
- _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/check.ts)_
997
+ _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/update/check.ts)_
998
998
 
999
999
  ## `bitsocial update install [VERSION]`
1000
1000
 
@@ -1026,7 +1026,7 @@ EXAMPLES
1026
1026
  $ bitsocial update install --no-restart-daemons
1027
1027
  ```
1028
1028
 
1029
- _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/install.ts)_
1029
+ _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/update/install.ts)_
1030
1030
 
1031
1031
  ## `bitsocial update versions`
1032
1032
 
@@ -1048,7 +1048,7 @@ EXAMPLES
1048
1048
  $ bitsocial update versions --limit 5
1049
1049
  ```
1050
1050
 
1051
- _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/versions.ts)_
1051
+ _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.68/src/cli/commands/update/versions.ts)_
1052
1052
  <!-- commandsstop -->
1053
1053
 
1054
1054
  ## Contribution
@@ -9,7 +9,7 @@ import { printBanner } from "../ascii-banner.js";
9
9
  import { loadChallengesIntoPKC, formatChallengeNameVersion } from "../../challenge-packages/challenge-utils.js";
10
10
  import { migrateDataDirectory } from "../../common-utils/data-migration.js";
11
11
  import { createBsoResolvers, DEFAULT_PROVIDERS } from "../../common-utils/resolvers.js";
12
- import { pruneStaleStates, writeDaemonState, deleteDaemonState } from "../../common-utils/daemon-state.js";
12
+ import { pruneStaleStates, writeDaemonState, deleteDaemonState, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../common-utils/daemon-state.js";
13
13
  import { createDaemonFileLogger } from "../../common-utils/daemon-file-logger.js";
14
14
  import fs from "fs";
15
15
  import fsPromise from "fs/promises";
@@ -468,6 +468,16 @@ export default class Daemon extends Command {
468
468
  }
469
469
  };
470
470
  const killKuboProcess = async () => {
471
+ // Test hook (issue #70): hold off the kubo teardown for a fixed delay so a test can
472
+ // deterministically reproduce the window where the daemon's RPC port is already free
473
+ // (daemonServer.destroy() runs in parallel) but kubo is still alive and bound. This is
474
+ // exactly the window `update install` must not restart into — see
475
+ // test/cli/update-install-restart-race.test.ts. The daemon process stays alive for the
476
+ // duration because the exit hook awaits killKuboProcess() before exiting.
477
+ const kuboShutdownDelayRaw = process.env["PKC_CLI_TEST_KUBO_SHUTDOWN_DELAY_MS"];
478
+ const kuboShutdownDelay = kuboShutdownDelayRaw ? Number(kuboShutdownDelayRaw) : 0;
479
+ if (Number.isFinite(kuboShutdownDelay) && kuboShutdownDelay > 0)
480
+ await new Promise((resolve) => setTimeout(resolve, kuboShutdownDelay));
471
481
  // Wait (bounded) for any in-flight start attempt so we kill the kubo it may still
472
482
  // spawn. Both promises settle on all failure paths (issue #70), but a spawned kubo
473
483
  // that wedges before "Daemon is ready" without exiting keeps them pending — the
@@ -538,7 +548,7 @@ export default class Daemon extends Command {
538
548
  log.error("Error shutting down daemon server", e);
539
549
  }
540
550
  await kuboKillPromise;
541
- }, { wait: 120000 } // could take two minutes to shut down
551
+ }, { wait: DAEMON_SHUTDOWN_TIMEOUT_MS } // could take two minutes to shut down
542
552
  );
543
553
  // Emergency cleanup: if the process force-exits (e.g. double Ctrl+C),
544
554
  // synchronously SIGKILL every live kubo's process group. This is a no-op if
@@ -163,6 +163,14 @@ export default class Logs extends Command {
163
163
  // Follow mode: dump existing content (filtered + tailed) then watch for new data
164
164
  let currentLogFile = latestLogFile;
165
165
  const existingContent = await fsPromise.readFile(currentLogFile, "utf-8");
166
+ // Anchor the follow offset to exactly the bytes we just read, NOT a separate
167
+ // fsPromise.stat() taken afterwards. A later stat is racy: any append landing between
168
+ // this read and the stat is skipped (position jumps past it) yet was never in the dump
169
+ // above, so follow mode silently drops those lines. Under load that window widens — this
170
+ // was the cause of the intermittent CI failure where an appended line was never surfaced
171
+ // (issue #77). Byte length (not string length) because position indexes bytes in the file.
172
+ let position = Buffer.byteLength(existingContent, "utf-8");
173
+ let pendingBuffer = "";
166
174
  const entries = this._parseLogEntries(existingContent);
167
175
  const filtered = this._filterEntries(entries, since, until);
168
176
  const streamFiltered = streamFilter ? this._filterByStream(filtered, streamFilter) : filtered;
@@ -170,9 +178,6 @@ export default class Logs extends Command {
170
178
  const initialOutput = tailed.map((e) => e.lines.join("\n")).join("\n");
171
179
  if (initialOutput)
172
180
  process.stdout.write(initialOutput + "\n");
173
- const stat = await fsPromise.stat(currentLogFile);
174
- let position = stat.size;
175
- let pendingBuffer = "";
176
181
  // Watch for new data by reading directly from `position`. We intentionally do
177
182
  // NOT gate on fsPromise.stat().size — on Windows + NTFS, stat() returns a stale
178
183
  // size for a short window after another process appends, which causes the gate
@@ -10,5 +10,11 @@ export default class Install extends Command {
10
10
  };
11
11
  static examples: string[];
12
12
  run(): Promise<void>;
13
+ /**
14
+ * Poll until the given PID no longer exists (signal 0 throws ESRCH), or the timeout elapses.
15
+ * Returns true if the process exited, false on timeout. EPERM means the process is still alive
16
+ * but owned by another user, so we keep waiting.
17
+ */
18
+ private _waitForProcessExit;
13
19
  private _restartDaemons;
14
20
  }
@@ -4,7 +4,7 @@ import tcpPortUsed from "tcp-port-used";
4
4
  import { fetchLatestVersion, installGlobal } from "../../../update/npm-registry.js";
5
5
  import { fastInstallGlobal } from "../../../update/fast-update.js";
6
6
  import { compareVersions } from "../../../update/semver.js";
7
- import { getAliveDaemonStates } from "../../../common-utils/daemon-state.js";
7
+ import { getAliveDaemonStates, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../../common-utils/daemon-state.js";
8
8
  export default class Install extends Command {
9
9
  static description = "Install a specific version of bitsocial from npm";
10
10
  static args = {
@@ -54,15 +54,17 @@ export default class Install extends Command {
54
54
  throw e;
55
55
  }
56
56
  }
57
- // Wait for all daemon ports to be free
57
+ // Wait for each daemon process to fully exit — NOT just for its RPC port to free.
58
+ // The daemon releases its RPC port (daemonServer.destroy()) before it finishes killing
59
+ // its kubo child, so a port-only wait lets us restart while the old kubo still holds the
60
+ // IPFS API port; the new daemon then dies on startup with "IPFS API port already in use"
61
+ // (issue #70). The daemon's exit hook kills kubo before the process exits, so waiting for
62
+ // the PID to disappear guarantees the kubo port is free before we restart.
58
63
  for (const d of aliveDaemons) {
59
- const url = new URL(d.pkcRpcUrl);
60
- const port = Number(url.port);
61
- const host = url.hostname;
62
- this.log(`Waiting for port ${port} to be free...`);
63
- const freed = await tcpPortUsed.waitUntilFree(port, 500, 30000).then(() => true).catch(() => false);
64
- if (!freed) {
65
- this.error(`Daemon (PID ${d.pid}) did not shut down within 30 seconds on port ${port}.`, { exit: 1 });
64
+ this.log(`Waiting for daemon (PID ${d.pid}) to exit...`);
65
+ const exited = await this._waitForProcessExit(d.pid, DAEMON_SHUTDOWN_TIMEOUT_MS);
66
+ if (!exited) {
67
+ this.error(`Daemon (PID ${d.pid}) did not shut down within ${DAEMON_SHUTDOWN_TIMEOUT_MS / 1000} seconds.`, { exit: 1 });
66
68
  }
67
69
  }
68
70
  this.log("All daemons stopped.");
@@ -118,6 +120,33 @@ export default class Install extends Command {
118
120
  this.log("To see the daemon logs run `bitsocial logs --stdout`");
119
121
  }
120
122
  }
123
+ /**
124
+ * Poll until the given PID no longer exists (signal 0 throws ESRCH), or the timeout elapses.
125
+ * Returns true if the process exited, false on timeout. EPERM means the process is still alive
126
+ * but owned by another user, so we keep waiting.
127
+ */
128
+ async _waitForProcessExit(pid, timeoutMs) {
129
+ const deadline = Date.now() + timeoutMs;
130
+ while (Date.now() < deadline) {
131
+ try {
132
+ process.kill(pid, 0);
133
+ }
134
+ catch (e) {
135
+ if (e.code === "ESRCH")
136
+ return true; // no such process — it exited
137
+ }
138
+ await new Promise((resolve) => setTimeout(resolve, 250));
139
+ }
140
+ // Final check so a process that exits in the last interval isn't reported as a timeout
141
+ try {
142
+ process.kill(pid, 0);
143
+ }
144
+ catch (e) {
145
+ if (e.code === "ESRCH")
146
+ return true;
147
+ }
148
+ return false;
149
+ }
121
150
  async _restartDaemons(daemons) {
122
151
  this.log(`Restarting ${daemons.length} daemon(s)...`);
123
152
  for (const d of daemons) {
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Maximum time a daemon is allowed to shut down its kubo + RPC server during its
3
+ * async exit hook. The `update install --restart-daemons` orchestrator must wait at
4
+ * least this long for a stopped daemon's PID to disappear before giving up — otherwise
5
+ * a slow-but-valid shutdown (within the daemon's own contract) aborts the update midway.
6
+ */
7
+ export declare const DAEMON_SHUTDOWN_TIMEOUT_MS = 120000;
1
8
  export interface DaemonState {
2
9
  pid: number;
3
10
  startedAt: string;
@@ -5,6 +5,13 @@ import { execFile } from "child_process";
5
5
  import { promisify } from "util";
6
6
  const execFileAsync = promisify(execFile);
7
7
  const DAEMON_STATES_DIR = path.join(defaults.PKC_DATA_PATH, ".daemon_states");
8
+ /**
9
+ * Maximum time a daemon is allowed to shut down its kubo + RPC server during its
10
+ * async exit hook. The `update install --restart-daemons` orchestrator must wait at
11
+ * least this long for a stopped daemon's PID to disappear before giving up — otherwise
12
+ * a slow-but-valid shutdown (within the daemon's own contract) aborts the update midway.
13
+ */
14
+ export const DAEMON_SHUTDOWN_TIMEOUT_MS = 120000;
8
15
  function stateFilePath(pid) {
9
16
  return path.join(DAEMON_STATES_DIR, `${pid}-daemon.state`);
10
17
  }
@@ -871,5 +871,5 @@
871
871
  ]
872
872
  }
873
873
  },
874
- "version": "0.19.66"
874
+ "version": "0.19.68"
875
875
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.66",
3
+ "version": "0.19.68",
4
4
  "description": "Command line interface to Bitsocial API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "homepage": "https://github.com/bitsocialnet/bitsocial-cli",
@@ -125,7 +125,7 @@
125
125
  "env-paths": "2.2.1",
126
126
  "exit-hook": "4.0.0",
127
127
  "express": "4.19.2",
128
- "kubo": "0.41.0",
128
+ "kubo": "0.42.0",
129
129
  "p-limit": "7.3.0",
130
130
  "strip-json-comments": "5.0.3",
131
131
  "tcp-port-used": "1.0.2",