@cogcoin/client 1.0.1 → 1.0.2

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.
Files changed (50) hide show
  1. package/README.md +2 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -0
  3. package/dist/bitcoind/indexer-daemon.js +58 -8
  4. package/dist/bitcoind/retryable-rpc.js +3 -0
  5. package/dist/bitcoind/service.d.ts +1 -0
  6. package/dist/bitcoind/service.js +31 -9
  7. package/dist/cli/commands/mining-admin.js +9 -0
  8. package/dist/cli/commands/update.d.ts +2 -0
  9. package/dist/cli/commands/update.js +101 -0
  10. package/dist/cli/context.js +31 -0
  11. package/dist/cli/mining-format.js +28 -0
  12. package/dist/cli/mining-json.js +6 -0
  13. package/dist/cli/output.js +50 -2
  14. package/dist/cli/parse.d.ts +1 -1
  15. package/dist/cli/parse.js +5 -0
  16. package/dist/cli/prompt.js +109 -0
  17. package/dist/cli/read-json.d.ts +13 -0
  18. package/dist/cli/read-json.js +17 -0
  19. package/dist/cli/runner.js +4 -0
  20. package/dist/cli/types.d.ts +6 -1
  21. package/dist/cli/update-notifier.js +7 -222
  22. package/dist/cli/update-service.d.ts +44 -0
  23. package/dist/cli/update-service.js +218 -0
  24. package/dist/client/initialization.js +5 -0
  25. package/dist/wallet/lifecycle.d.ts +10 -0
  26. package/dist/wallet/lifecycle.js +6 -0
  27. package/dist/wallet/mining/config.js +13 -3
  28. package/dist/wallet/mining/control.d.ts +2 -1
  29. package/dist/wallet/mining/control.js +143 -19
  30. package/dist/wallet/mining/index.d.ts +1 -1
  31. package/dist/wallet/mining/provider-model.d.ts +30 -0
  32. package/dist/wallet/mining/provider-model.js +134 -0
  33. package/dist/wallet/mining/runner.d.ts +98 -3
  34. package/dist/wallet/mining/runner.js +493 -95
  35. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  36. package/dist/wallet/mining/sentences.d.ts +2 -2
  37. package/dist/wallet/mining/sentences.js +25 -2
  38. package/dist/wallet/mining/types.d.ts +9 -1
  39. package/dist/wallet/mining/visualizer.js +28 -5
  40. package/dist/wallet/read/context.js +3 -0
  41. package/dist/wallet/reset.js +1 -0
  42. package/dist/wallet/tx/anchor.js +1 -0
  43. package/dist/wallet/tx/bitcoin-transfer.js +1 -0
  44. package/dist/wallet/tx/cog.js +3 -0
  45. package/dist/wallet/tx/domain-admin.js +1 -0
  46. package/dist/wallet/tx/domain-market.js +3 -0
  47. package/dist/wallet/tx/field.js +1 -0
  48. package/dist/wallet/tx/register.js +1 -0
  49. package/dist/wallet/tx/reputation.js +1 -0
  50. package/package.json +3 -2
@@ -1,5 +1,9 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  const CLEAR_SENSITIVE_DISPLAY_SEQUENCE = "\u001B[2J\u001B[3J\u001B[H";
3
+ const CLEAR_MENU_SEQUENCE = "\u001B[0J";
4
+ function countRenderedLines(text) {
5
+ return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
6
+ }
3
7
  export function createTerminalPrompter(input, output) {
4
8
  const ensureReadableInput = () => {
5
9
  if (!("on" in input) || !("off" in input)) {
@@ -8,6 +12,12 @@ export function createTerminalPrompter(input, output) {
8
12
  return input;
9
13
  };
10
14
  const ensureWritableOutput = () => output;
15
+ const supportsRawSelection = () => {
16
+ const readable = ensureReadableInput();
17
+ return Boolean(readable.isTTY
18
+ && output.isTTY
19
+ && typeof readable.setRawMode === "function");
20
+ };
11
21
  const ask = async (message, questionOutput) => {
12
22
  const readline = createInterface({
13
23
  input: ensureReadableInput(),
@@ -20,6 +30,99 @@ export function createTerminalPrompter(input, output) {
20
30
  readline.close();
21
31
  }
22
32
  };
33
+ const promptSelectionFallback = async (options) => {
34
+ const writableOutput = ensureWritableOutput();
35
+ writableOutput.write(`${options.message}\n`);
36
+ for (const [index, option] of options.options.entries()) {
37
+ const description = option.description == null || option.description.length === 0
38
+ ? ""
39
+ : ` - ${option.description}`;
40
+ writableOutput.write(`${index + 1}. ${option.label}${description}\n`);
41
+ }
42
+ if (options.footer != null && options.footer.length > 0) {
43
+ writableOutput.write(`${options.footer}\n`);
44
+ }
45
+ while (true) {
46
+ const answer = (await ask(`Choice [1-${options.options.length}]: `, writableOutput)).trim();
47
+ if (/^(q|quit|esc|escape)$/i.test(answer)) {
48
+ throw new Error("mining_setup_canceled");
49
+ }
50
+ const selection = Number.parseInt(answer, 10);
51
+ if (Number.isInteger(selection) && selection >= 1 && selection <= options.options.length) {
52
+ return options.options[selection - 1].value;
53
+ }
54
+ writableOutput.write(`Enter a number from 1 to ${options.options.length}, or q to cancel.\n`);
55
+ }
56
+ };
57
+ const promptSelectionRaw = async (options) => {
58
+ const readableInput = ensureReadableInput();
59
+ const writableOutput = ensureWritableOutput();
60
+ const initialIndex = options.initialValue == null
61
+ ? -1
62
+ : options.options.findIndex((option) => option.value === options.initialValue);
63
+ let selectedIndex = initialIndex === -1 ? 0 : initialIndex;
64
+ let renderedLineCount = 0;
65
+ const renderMenu = () => {
66
+ if (renderedLineCount > 0) {
67
+ writableOutput.write(`\u001B[${renderedLineCount}A${CLEAR_MENU_SEQUENCE}`);
68
+ }
69
+ const lines = [
70
+ options.message,
71
+ "Use Up/Down to choose, Enter to confirm, or q/Esc/Ctrl+C to cancel.",
72
+ ...options.options.map((option, index) => {
73
+ const prefix = index === selectedIndex ? ">" : " ";
74
+ const description = option.description == null || option.description.length === 0
75
+ ? ""
76
+ : ` - ${option.description}`;
77
+ return `${prefix} ${option.label}${description}`;
78
+ }),
79
+ ];
80
+ if (options.footer != null && options.footer.length > 0) {
81
+ lines.push(options.footer);
82
+ }
83
+ const rendered = `${lines.join("\n")}\n`;
84
+ renderedLineCount = countRenderedLines(rendered);
85
+ writableOutput.write(rendered);
86
+ };
87
+ return await new Promise((resolve, reject) => {
88
+ const finish = (handler) => {
89
+ readableInput.off("data", onData);
90
+ readableInput.setRawMode(false);
91
+ readableInput.pause?.();
92
+ writableOutput.write("\n");
93
+ handler();
94
+ };
95
+ const onData = (chunk) => {
96
+ const value = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
97
+ switch (value) {
98
+ case "\u001B[A":
99
+ selectedIndex = selectedIndex === 0 ? options.options.length - 1 : selectedIndex - 1;
100
+ renderMenu();
101
+ return;
102
+ case "\u001B[B":
103
+ selectedIndex = selectedIndex === options.options.length - 1 ? 0 : selectedIndex + 1;
104
+ renderMenu();
105
+ return;
106
+ case "\r":
107
+ case "\n":
108
+ finish(() => resolve(options.options[selectedIndex].value));
109
+ return;
110
+ case "\u001B":
111
+ case "\u0003":
112
+ case "q":
113
+ case "Q":
114
+ finish(() => reject(new Error("mining_setup_canceled")));
115
+ return;
116
+ default:
117
+ return;
118
+ }
119
+ };
120
+ readableInput.setRawMode(true);
121
+ readableInput.resume?.();
122
+ readableInput.on("data", onData);
123
+ renderMenu();
124
+ });
125
+ };
23
126
  return {
24
127
  isInteractive: Boolean(input.isTTY && output.isTTY),
25
128
  writeLine(message) {
@@ -44,6 +147,12 @@ export function createTerminalPrompter(input, output) {
44
147
  output.write("\n");
45
148
  }
46
149
  },
150
+ async selectOption(options) {
151
+ if (!supportsRawSelection()) {
152
+ return await promptSelectionFallback(options);
153
+ }
154
+ return await promptSelectionRaw(options);
155
+ },
47
156
  clearSensitiveDisplay(scope) {
48
157
  if (!input.isTTY || !output.isTTY) {
49
158
  return;
@@ -114,6 +114,19 @@ export declare function buildMineStatusJson(mining: MiningControlPlaneView): Rea
114
114
  phase: string;
115
115
  lastSuspendDetectedAtUnixMs: number | null;
116
116
  pauseReason: string | null;
117
+ providerState: string | null;
118
+ provider: {
119
+ configured: boolean;
120
+ kind: string | null;
121
+ modelId: string | null;
122
+ effectiveModel: string | null;
123
+ modelOverride: string | null;
124
+ modelSelectionSource: string | null;
125
+ usingDefaultModel: boolean | null;
126
+ extraPromptConfigured: boolean;
127
+ estimatedDailyCostUsd: number | null;
128
+ estimatedDailyCostDisplay: string | null;
129
+ };
117
130
  fees: Record<string, unknown>;
118
131
  worker: Record<string, unknown>;
119
132
  availability: Record<string, JsonAvailabilityEntry>;
@@ -288,6 +288,18 @@ function buildMiningStatusData(mining) {
288
288
  phase: mining.runtime.currentPhase,
289
289
  lastSuspendDetectedAtUnixMs: mining.runtime.lastSuspendDetectedAtUnixMs,
290
290
  pauseReason: mining.runtime.pauseReason,
291
+ provider: {
292
+ configured: mining.provider.configured,
293
+ kind: mining.provider.provider,
294
+ modelId: mining.provider.modelId,
295
+ effectiveModel: mining.provider.effectiveModel,
296
+ modelOverride: mining.provider.modelOverride,
297
+ modelSelectionSource: mining.provider.modelSelectionSource,
298
+ usingDefaultModel: mining.provider.usingDefaultModel,
299
+ extraPromptConfigured: mining.provider.extraPromptConfigured,
300
+ estimatedDailyCostUsd: mining.provider.estimatedDailyCostUsd,
301
+ estimatedDailyCostDisplay: mining.provider.estimatedDailyCostDisplay,
302
+ },
291
303
  providerState: mining.runtime.providerState,
292
304
  tipsAligned: mining.runtime.tipsAligned,
293
305
  sameDomainCompetitorSuppressed: mining.runtime.sameDomainCompetitorSuppressed,
@@ -393,6 +405,11 @@ export function buildMineStatusJson(mining) {
393
405
  if (mining.runtime.miningState === "repair-required") {
394
406
  nextSteps.push("Run `cogcoin repair` before mining again.");
395
407
  }
408
+ else if (mining.runtime.providerState === "not-found") {
409
+ nextSteps.push(mining.provider.usingDefaultModel === false
410
+ ? "Run `cogcoin mine setup` and clear or correct the provider model."
411
+ : "Run `cogcoin mine setup` and choose a valid provider model.");
412
+ }
396
413
  else if (mining.runtime.pauseReason === "zero-reward") {
397
414
  nextSteps.push("Wait for the next positive-reward target height; mining resumes automatically.");
398
415
  }
@@ -10,6 +10,7 @@ import { runMiningRuntimeCommand } from "./commands/mining-runtime.js";
10
10
  import { runServiceRuntimeCommand } from "./commands/service-runtime.js";
11
11
  import { runStatusCommand } from "./commands/status.js";
12
12
  import { runSyncCommand } from "./commands/sync.js";
13
+ import { runUpdateCommand } from "./commands/update.js";
13
14
  import { runWalletAdminCommand } from "./commands/wallet-admin.js";
14
15
  import { runWalletMutationCommand } from "./commands/wallet-mutation.js";
15
16
  import { runWalletReadCommand } from "./commands/wallet-read.js";
@@ -52,6 +53,9 @@ export async function runCli(argv, contextOverrides = {}) {
52
53
  }
53
54
  await maybeNotifyAboutCliUpdate(parsed, context);
54
55
  try {
56
+ if (parsed.command === "update") {
57
+ return runUpdateCommand(parsed, context);
58
+ }
55
59
  if (commandUsesExistingWalletSeed(parsed)) {
56
60
  const mainPaths = context.resolveWalletRuntimePaths("main");
57
61
  const seedIndex = await loadWalletSeedIndex({
@@ -14,7 +14,7 @@ import type { ensureBuiltInMiningSetupIfNeeded, followMiningLog, inspectMiningCo
14
14
  import type { anchorDomain, transferBitcoin, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain } from "../wallet/tx/index.js";
15
15
  export type ProgressOutput = "auto" | "tty" | "none";
16
16
  export type OutputMode = "text" | "json" | "preview-json";
17
- export type CommandName = "init" | "restore" | "reset" | "repair" | "sync" | "status" | "client-lock" | "client-change-password" | "client-unlock" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "bitcoin-transfer" | "indexer-start" | "indexer-stop" | "indexer-status" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-init" | "wallet-delete" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
17
+ export type CommandName = "init" | "restore" | "reset" | "repair" | "update" | "sync" | "status" | "client-lock" | "client-change-password" | "client-unlock" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "bitcoin-transfer" | "indexer-start" | "indexer-stop" | "indexer-status" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-init" | "wallet-delete" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
18
18
  export interface WritableLike {
19
19
  isTTY?: boolean;
20
20
  write(chunk: string): void;
@@ -92,6 +92,11 @@ export interface CliRunnerContext {
92
92
  signalSource?: SignalSource;
93
93
  forceExit?: (code: number) => never | void;
94
94
  fetchImpl?: typeof fetch;
95
+ runGlobalClientUpdateInstall?: (options: {
96
+ stdout: WritableLike;
97
+ stderr: WritableLike;
98
+ env: NodeJS.ProcessEnv;
99
+ }) => Promise<void>;
95
100
  openSqliteStore?: typeof openSqliteStore;
96
101
  openManagedBitcoindClient?: (options: {
97
102
  store: ClientStoreAdapter;
@@ -1,143 +1,14 @@
1
- import { readFile } from "node:fs/promises";
2
- import { writeJsonFileAtomic } from "../wallet/fs/atomic.js";
3
1
  import { writeLine } from "./io.js";
4
- const UPDATE_CHECK_CACHE_SCHEMA_VERSION = 1;
5
- const UPDATE_CHECK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
6
- const UPDATE_CHECK_TIMEOUT_MS = 500;
7
- const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
8
- function createEmptyUpdateCheckCache() {
9
- return {
10
- schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
11
- lastCheckedAtUnixMs: 0,
12
- latestVersion: null,
13
- lastNotifiedCurrentVersion: null,
14
- lastNotifiedLatestVersion: null,
15
- lastNotifiedAtUnixMs: null,
16
- };
17
- }
18
- function parseSemver(version) {
19
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version.trim());
20
- if (match === null) {
21
- return null;
22
- }
23
- const prerelease = match[4] === undefined
24
- ? []
25
- : match[4].split(".").map((raw) => ({
26
- raw,
27
- numeric: /^(0|[1-9]\d*)$/.test(raw),
28
- numericValue: /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : null,
29
- }));
30
- return {
31
- major: Number(match[1]),
32
- minor: Number(match[2]),
33
- patch: Number(match[3]),
34
- prerelease,
35
- };
36
- }
37
- function compareSemver(left, right) {
38
- const leftParsed = parseSemver(left);
39
- const rightParsed = parseSemver(right);
40
- if (leftParsed === null || rightParsed === null) {
41
- return null;
42
- }
43
- if (leftParsed.major !== rightParsed.major) {
44
- return leftParsed.major > rightParsed.major ? 1 : -1;
45
- }
46
- if (leftParsed.minor !== rightParsed.minor) {
47
- return leftParsed.minor > rightParsed.minor ? 1 : -1;
48
- }
49
- if (leftParsed.patch !== rightParsed.patch) {
50
- return leftParsed.patch > rightParsed.patch ? 1 : -1;
51
- }
52
- if (leftParsed.prerelease.length === 0 && rightParsed.prerelease.length === 0) {
53
- return 0;
54
- }
55
- if (leftParsed.prerelease.length === 0) {
56
- return 1;
57
- }
58
- if (rightParsed.prerelease.length === 0) {
59
- return -1;
60
- }
61
- const maxLength = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length);
62
- for (let index = 0; index < maxLength; index += 1) {
63
- const leftIdentifier = leftParsed.prerelease[index];
64
- const rightIdentifier = rightParsed.prerelease[index];
65
- if (leftIdentifier === undefined) {
66
- return -1;
67
- }
68
- if (rightIdentifier === undefined) {
69
- return 1;
70
- }
71
- if (leftIdentifier.numeric && rightIdentifier.numeric) {
72
- if (leftIdentifier.numericValue !== rightIdentifier.numericValue) {
73
- return leftIdentifier.numericValue > rightIdentifier.numericValue ? 1 : -1;
74
- }
75
- continue;
76
- }
77
- if (leftIdentifier.numeric !== rightIdentifier.numeric) {
78
- return leftIdentifier.numeric ? -1 : 1;
79
- }
80
- if (leftIdentifier.raw !== rightIdentifier.raw) {
81
- return leftIdentifier.raw > rightIdentifier.raw ? 1 : -1;
82
- }
83
- }
84
- return 0;
85
- }
86
- function isUpdateCheckDisabled(env) {
87
- const raw = env.COGCOIN_DISABLE_UPDATE_CHECK;
88
- if (raw === undefined) {
2
+ import { PASSIVE_UPDATE_CHECK_TIMEOUT_MS, UPDATE_CHECK_MAX_AGE_MS, applyUpdateCheckResult, compareSemver, createEmptyUpdateCheckCache, fetchLatestPublishedVersion, isUpdateCheckDisabled, loadUpdateCheckCache, persistUpdateCheckCache, recordUpdateNotification, shouldRefreshUpdateCheck, } from "./update-service.js";
3
+ function isEligibleForUpdateNotification(parsed, context) {
4
+ if (parsed.command === "update") {
89
5
  return false;
90
6
  }
91
- const normalized = raw.trim().toLowerCase();
92
- return normalized === "1" || normalized === "true" || normalized === "yes";
93
- }
94
- function isEligibleForUpdateNotification(parsed, context) {
95
7
  if (parsed.outputMode !== "text" || parsed.help || parsed.version) {
96
8
  return false;
97
9
  }
98
10
  return context.stdout.isTTY === true || context.stderr.isTTY === true;
99
11
  }
100
- function normalizeUpdateCheckCache(parsed) {
101
- if (typeof parsed !== "object" || parsed === null) {
102
- return null;
103
- }
104
- const candidate = parsed;
105
- if (candidate.schemaVersion !== UPDATE_CHECK_CACHE_SCHEMA_VERSION) {
106
- return null;
107
- }
108
- return {
109
- schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
110
- lastCheckedAtUnixMs: typeof candidate.lastCheckedAtUnixMs === "number" ? candidate.lastCheckedAtUnixMs : 0,
111
- latestVersion: typeof candidate.latestVersion === "string" ? candidate.latestVersion : null,
112
- lastNotifiedCurrentVersion: typeof candidate.lastNotifiedCurrentVersion === "string"
113
- ? candidate.lastNotifiedCurrentVersion
114
- : null,
115
- lastNotifiedLatestVersion: typeof candidate.lastNotifiedLatestVersion === "string"
116
- ? candidate.lastNotifiedLatestVersion
117
- : null,
118
- lastNotifiedAtUnixMs: typeof candidate.lastNotifiedAtUnixMs === "number"
119
- ? candidate.lastNotifiedAtUnixMs
120
- : null,
121
- lastCheckErrorKind: typeof candidate.lastCheckErrorKind === "string"
122
- ? candidate.lastCheckErrorKind
123
- : undefined,
124
- };
125
- }
126
- async function loadUpdateCheckCache(cachePath) {
127
- try {
128
- const raw = await readFile(cachePath, "utf8");
129
- return normalizeUpdateCheckCache(JSON.parse(raw));
130
- }
131
- catch (error) {
132
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
133
- return null;
134
- }
135
- return null;
136
- }
137
- }
138
- function shouldRefreshUpdateCheck(cache, now) {
139
- return now - cache.lastCheckedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
140
- }
141
12
  function shouldNotifyForVersionPair(cache, currentVersion, latestVersion, now) {
142
13
  const versionComparison = compareSemver(latestVersion, currentVersion);
143
14
  if (versionComparison === null || versionComparison <= 0) {
@@ -152,117 +23,31 @@ function shouldNotifyForVersionPair(cache, currentVersion, latestVersion, now) {
152
23
  }
153
24
  return now - cache.lastNotifiedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
154
25
  }
155
- function recordUpdateNotification(cache, currentVersion, latestVersion, now) {
156
- return {
157
- ...cache,
158
- lastNotifiedCurrentVersion: currentVersion,
159
- lastNotifiedLatestVersion: latestVersion,
160
- lastNotifiedAtUnixMs: now,
161
- };
162
- }
163
- async function fetchLatestPublishedVersion(fetchImpl) {
164
- const controller = new AbortController();
165
- const timer = setTimeout(() => {
166
- controller.abort();
167
- }, UPDATE_CHECK_TIMEOUT_MS);
168
- try {
169
- const response = await fetchImpl(UPDATE_CHECK_URL, {
170
- headers: {
171
- accept: "application/json",
172
- },
173
- signal: controller.signal,
174
- });
175
- if (!response.ok) {
176
- return {
177
- kind: "failure",
178
- errorKind: `http_${response.status}`,
179
- };
180
- }
181
- let payload;
182
- try {
183
- payload = await response.json();
184
- }
185
- catch {
186
- return {
187
- kind: "failure",
188
- errorKind: "invalid_json",
189
- };
190
- }
191
- const latestVersion = typeof payload.version === "string"
192
- ? payload.version
193
- : null;
194
- if (latestVersion === null) {
195
- return {
196
- kind: "failure",
197
- errorKind: "invalid_payload",
198
- };
199
- }
200
- if (parseSemver(latestVersion) === null) {
201
- return {
202
- kind: "failure",
203
- errorKind: "invalid_semver",
204
- };
205
- }
206
- return {
207
- kind: "success",
208
- latestVersion,
209
- };
210
- }
211
- catch (error) {
212
- if (error instanceof Error && error.name === "AbortError") {
213
- return {
214
- kind: "failure",
215
- errorKind: "timeout",
216
- };
217
- }
218
- return {
219
- kind: "failure",
220
- errorKind: "network",
221
- };
222
- }
223
- finally {
224
- clearTimeout(timer);
225
- }
226
- }
227
26
  function writeUpdateNotice(context, currentVersion, latestVersion) {
228
27
  writeLine(context.stderr, `Update available: Cogcoin ${currentVersion} -> ${latestVersion}`);
229
28
  writeLine(context.stderr, "Run: npm install -g @cogcoin/client");
230
29
  }
231
- async function persistUpdateCheckCache(cachePath, cache) {
232
- await writeJsonFileAtomic(cachePath, cache);
233
- }
234
30
  export async function maybeNotifyAboutCliUpdate(parsed, context) {
235
31
  try {
236
32
  if (!isEligibleForUpdateNotification(parsed, context) || isUpdateCheckDisabled(context.env)) {
237
33
  return;
238
34
  }
239
35
  const currentVersion = await context.readPackageVersion();
240
- if (parseSemver(currentVersion) === null) {
241
- return;
242
- }
243
36
  const cachePath = context.resolveUpdateCheckStatePath();
244
37
  const now = context.now();
245
38
  let cache = await loadUpdateCheckCache(cachePath) ?? createEmptyUpdateCheckCache();
246
39
  let cacheChanged = false;
247
40
  if (shouldRefreshUpdateCheck(cache, now)) {
248
- const updateResult = await fetchLatestPublishedVersion(context.fetchImpl);
249
- cache = {
250
- ...cache,
251
- lastCheckedAtUnixMs: now,
252
- latestVersion: updateResult.kind === "success"
253
- ? updateResult.latestVersion
254
- : cache.latestVersion,
255
- lastCheckErrorKind: updateResult.kind === "success"
256
- ? undefined
257
- : updateResult.errorKind,
258
- };
41
+ const updateResult = await fetchLatestPublishedVersion(context.fetchImpl, {
42
+ timeoutMs: PASSIVE_UPDATE_CHECK_TIMEOUT_MS,
43
+ });
44
+ cache = applyUpdateCheckResult(cache, updateResult, now);
259
45
  cacheChanged = true;
260
46
  }
261
47
  if (cache.latestVersion !== null
262
48
  && shouldNotifyForVersionPair(cache, currentVersion, cache.latestVersion, now)) {
263
49
  writeUpdateNotice(context, currentVersion, cache.latestVersion);
264
50
  cache = recordUpdateNotification(cache, currentVersion, cache.latestVersion, now);
265
- cacheChanged = true;
266
51
  await persistUpdateCheckCache(cachePath, cache);
267
52
  return;
268
53
  }
@@ -0,0 +1,44 @@
1
+ export declare const UPDATE_CHECK_CACHE_SCHEMA_VERSION = 1;
2
+ export declare const UPDATE_CHECK_MAX_AGE_MS: number;
3
+ export declare const PASSIVE_UPDATE_CHECK_TIMEOUT_MS = 500;
4
+ export declare const EXPLICIT_UPDATE_CHECK_TIMEOUT_MS = 5000;
5
+ export declare const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
6
+ export declare const CLI_INSTALL_COMMAND = "npm install -g @cogcoin/client";
7
+ export interface ParsedSemver {
8
+ major: number;
9
+ minor: number;
10
+ patch: number;
11
+ prerelease: Array<{
12
+ raw: string;
13
+ numeric: boolean;
14
+ numericValue: number | null;
15
+ }>;
16
+ }
17
+ export interface UpdateCheckCache {
18
+ schemaVersion: typeof UPDATE_CHECK_CACHE_SCHEMA_VERSION;
19
+ lastCheckedAtUnixMs: number;
20
+ latestVersion: string | null;
21
+ lastNotifiedCurrentVersion: string | null;
22
+ lastNotifiedLatestVersion: string | null;
23
+ lastNotifiedAtUnixMs: number | null;
24
+ lastCheckErrorKind?: string;
25
+ }
26
+ export type UpdateCheckResult = {
27
+ kind: "success";
28
+ latestVersion: string;
29
+ } | {
30
+ kind: "failure";
31
+ errorKind: string;
32
+ };
33
+ export declare function createEmptyUpdateCheckCache(): UpdateCheckCache;
34
+ export declare function parseSemver(version: string): ParsedSemver | null;
35
+ export declare function compareSemver(left: string, right: string): number | null;
36
+ export declare function isUpdateCheckDisabled(env: NodeJS.ProcessEnv): boolean;
37
+ export declare function loadUpdateCheckCache(cachePath: string): Promise<UpdateCheckCache | null>;
38
+ export declare function shouldRefreshUpdateCheck(cache: UpdateCheckCache, now: number): boolean;
39
+ export declare function applyUpdateCheckResult(cache: UpdateCheckCache, result: UpdateCheckResult, now: number): UpdateCheckCache;
40
+ export declare function recordUpdateNotification(cache: UpdateCheckCache, currentVersion: string, latestVersion: string, now: number): UpdateCheckCache;
41
+ export declare function fetchLatestPublishedVersion(fetchImpl: typeof fetch, options?: {
42
+ timeoutMs?: number;
43
+ }): Promise<UpdateCheckResult>;
44
+ export declare function persistUpdateCheckCache(cachePath: string, cache: UpdateCheckCache): Promise<void>;