@needmoretruth/nmts-cli 0.34.4 → 0.36.1

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 (156) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +43 -0
  3. package/README.ko.md +33 -35
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +169 -0
  7. package/dist/arg-options.js +10 -0
  8. package/dist/args.d.ts +2 -157
  9. package/dist/args.js +2 -0
  10. package/dist/artifact-about.d.ts +1 -1
  11. package/dist/autonomy.d.ts +3 -10
  12. package/dist/autonomy.js +15 -17
  13. package/dist/bytes.d.ts +15 -0
  14. package/dist/bytes.js +63 -0
  15. package/dist/collision.d.ts +5 -5
  16. package/dist/collision.js +19 -22
  17. package/dist/commands/delete-account.js +1 -1
  18. package/dist/commands/erase.js +1 -1
  19. package/dist/commands/extend.d.ts +2 -0
  20. package/dist/commands/extend.js +8 -1
  21. package/dist/commands/get.js +1 -1
  22. package/dist/commands/listfile.js +1 -1
  23. package/dist/commands/mcp.js +5 -1
  24. package/dist/commands/mode.js +2 -2
  25. package/dist/commands/on-collision.d.ts +1 -1
  26. package/dist/commands/on-collision.js +3 -3
  27. package/dist/commands/platform.d.ts +9 -0
  28. package/dist/commands/platform.js +124 -0
  29. package/dist/commands/pull.js +1 -1
  30. package/dist/commands/push-wallet.js +8 -1
  31. package/dist/commands/push.d.ts +2 -0
  32. package/dist/commands/push.js +6 -4
  33. package/dist/commands/put-payer.d.ts +8 -0
  34. package/dist/commands/put-payer.js +41 -0
  35. package/dist/commands/put-wallet.d.ts +9 -3
  36. package/dist/commands/put-wallet.js +108 -186
  37. package/dist/commands/put.d.ts +5 -7
  38. package/dist/commands/put.js +11 -31
  39. package/dist/commands/receive.js +1 -1
  40. package/dist/commands/settings.js +1 -1
  41. package/dist/commands/support.js +3 -3
  42. package/dist/commands/unlock.js +2 -2
  43. package/dist/commands/wallet-donate.d.ts +3 -0
  44. package/dist/commands/wallet-donate.js +6 -2
  45. package/dist/commands/wallet-hall.d.ts +4 -1
  46. package/dist/commands/wallet-hall.js +16 -8
  47. package/dist/commands/wallet-list.d.ts +18 -0
  48. package/dist/commands/wallet-list.js +116 -0
  49. package/dist/commands/wallet-send.d.ts +4 -0
  50. package/dist/commands/wallet-send.js +6 -2
  51. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  52. package/dist/commands/wallet-storage-ops.js +7 -2
  53. package/dist/commands/wallet-swap.d.ts +3 -0
  54. package/dist/commands/wallet-swap.js +8 -4
  55. package/dist/commands/wallet-use.d.ts +7 -0
  56. package/dist/commands/wallet-use.js +55 -0
  57. package/dist/commands/wallet.d.ts +4 -0
  58. package/dist/commands/wallet.js +20 -8
  59. package/dist/crypto.d.ts +8 -5
  60. package/dist/crypto.js +15 -61
  61. package/dist/download-sink-node.d.ts +49 -0
  62. package/dist/download-sink-node.js +204 -0
  63. package/dist/download-sink.d.ts +0 -48
  64. package/dist/download-sink.js +6 -199
  65. package/dist/download.js +5 -4
  66. package/dist/engine-node.d.ts +19 -0
  67. package/dist/engine-node.js +77 -0
  68. package/dist/env-vars.d.ts +20 -0
  69. package/dist/env-vars.js +27 -0
  70. package/dist/extend-plan.d.ts +2 -0
  71. package/dist/gate.js +1 -1
  72. package/dist/help.js +12 -5
  73. package/dist/host-contract.d.ts +7 -0
  74. package/dist/host-contract.js +95 -0
  75. package/dist/host-node.d.ts +27 -0
  76. package/dist/host-node.js +192 -0
  77. package/dist/host.d.ts +99 -0
  78. package/dist/host.js +59 -0
  79. package/dist/index.d.ts +6 -40
  80. package/dist/index.js +20 -32
  81. package/dist/main.js +11 -10
  82. package/dist/manifest-chunk-cache.d.ts +3 -3
  83. package/dist/manifest-chunk-cache.js +39 -44
  84. package/dist/manifest-chunk-flow.js +15 -14
  85. package/dist/manifest-write.js +1 -1
  86. package/dist/manifest.d.ts +2 -2
  87. package/dist/manifest.js +37 -34
  88. package/dist/mcp-tools/tiers.js +1 -1
  89. package/dist/network.d.ts +1 -1
  90. package/dist/network.js +6 -2
  91. package/dist/platform-sign.d.ts +143 -0
  92. package/dist/platform-sign.js +220 -0
  93. package/dist/portable.d.ts +59 -0
  94. package/dist/portable.js +82 -0
  95. package/dist/product.d.ts +1 -1
  96. package/dist/product.js +1 -1
  97. package/dist/progress-node.d.ts +2 -0
  98. package/dist/progress-node.js +14 -0
  99. package/dist/progress.d.ts +0 -1
  100. package/dist/progress.js +4 -8
  101. package/dist/redact.js +3 -2
  102. package/dist/registration.js +5 -2
  103. package/dist/risk.d.ts +11 -0
  104. package/dist/risk.js +16 -0
  105. package/dist/run-log.d.ts +4 -4
  106. package/dist/run-log.js +29 -36
  107. package/dist/seal.js +5 -4
  108. package/dist/server.d.ts +1 -1
  109. package/dist/server.js +6 -2
  110. package/dist/setup-questions.js +2 -2
  111. package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
  112. package/dist/shared/lib/drive/manifest-ops.js +7 -39
  113. package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
  114. package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
  115. package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
  116. package/dist/shared/lib/drive/manifest-settings.js +66 -28
  117. package/dist/shared/lib/wallet/discover.d.ts +54 -0
  118. package/dist/shared/lib/wallet/discover.js +66 -0
  119. package/dist/standing-tip.d.ts +3 -0
  120. package/dist/standing-tip.js +1 -0
  121. package/dist/upload-file-node.d.ts +3 -0
  122. package/dist/upload-file-node.js +38 -0
  123. package/dist/upload-file.d.ts +2 -2
  124. package/dist/upload-file.js +15 -45
  125. package/dist/upload-price-node.d.ts +8 -0
  126. package/dist/upload-price-node.js +37 -0
  127. package/dist/upload-price.d.ts +0 -8
  128. package/dist/upload-price.js +4 -33
  129. package/dist/upload-steps.js +3 -3
  130. package/dist/upload-store.d.ts +14 -14
  131. package/dist/upload-store.js +68 -75
  132. package/dist/upload-wallet-chain.js +3 -2
  133. package/dist/upload-wallet-put.d.ts +118 -0
  134. package/dist/upload-wallet-put.js +221 -0
  135. package/dist/upload-wallet.d.ts +3 -0
  136. package/dist/upload-wallet.js +18 -15
  137. package/dist/upload.js +16 -15
  138. package/dist/wallet-list-chain.d.ts +4 -0
  139. package/dist/wallet-list-chain.js +21 -0
  140. package/dist/wallet-pay-index.d.ts +21 -0
  141. package/dist/wallet-pay-index.js +68 -0
  142. package/dist/wallet-sign-seams.d.ts +69 -0
  143. package/dist/wallet-sign-seams.js +16 -0
  144. package/dist/wallet-sign.d.ts +3 -53
  145. package/dist/wallet-sign.js +22 -18
  146. package/dist/wallet.d.ts +13 -13
  147. package/dist/wallet.js +15 -15
  148. package/dist/walrus-write.js +3 -2
  149. package/dist/walrus.d.ts +1 -16
  150. package/dist/walrus.js +11 -22
  151. package/docs/commands/credits.md +1 -1
  152. package/docs/commands/platform.md +14 -0
  153. package/docs/commands/support.md +1 -1
  154. package/docs/commands/trash.md +1 -1
  155. package/docs/commands/wallet.md +26 -8
  156. package/package.json +13 -2
package/dist/risk.d.ts CHANGED
@@ -152,6 +152,9 @@ export declare const ACTS: {
152
152
  readonly mcp: {
153
153
  readonly tier: "none";
154
154
  };
155
+ readonly platform: {
156
+ readonly tier: "none";
157
+ };
155
158
  readonly "accept-terms": {
156
159
  readonly tier: "none";
157
160
  };
@@ -226,6 +229,10 @@ export declare const ACTS: {
226
229
  readonly tier: "medium";
227
230
  readonly what: "Revoke an API key. Whatever used it stops working.";
228
231
  };
232
+ readonly "platform.keygen": {
233
+ readonly tier: "medium";
234
+ readonly what: "Make a business key pair and write its private half to a file on this disk.";
235
+ };
229
236
  readonly "public-code.publish": {
230
237
  readonly tier: "medium";
231
238
  readonly what: "Publish this account's public code. Publishing cannot be undone.";
@@ -344,6 +351,10 @@ export declare const ACTS: {
344
351
  readonly what: "Send a gift to the developer from the wallet.";
345
352
  readonly asksItself: true;
346
353
  };
354
+ readonly "wallet.use": {
355
+ readonly tier: "low";
356
+ readonly what: "Change which of this key's wallets pays for storage.";
357
+ };
347
358
  readonly "wallet.hall": {
348
359
  readonly tier: "none";
349
360
  };
package/dist/risk.js CHANGED
@@ -69,6 +69,9 @@ export const ACTS = {
69
69
  verify: { tier: "none" },
70
70
  support: { tier: "none" },
71
71
  mcp: { tier: "none" },
72
+ // The bare word and `platform register` only say where a business registers; what MAKES
73
+ // something is `platform.keygen` below.
74
+ platform: { tier: "none" },
72
75
  "accept-terms": { tier: "none" },
73
76
  // ── low ──
74
77
  rm: { tier: "low", what: "Move these to the trash, restorable for 30 days." },
@@ -90,6 +93,11 @@ export const ACTS = {
90
93
  "key.list": { tier: "none" },
91
94
  "key.new": { tier: "medium", what: "Make a new API key for this machine." },
92
95
  "key.revoke": { tier: "medium", what: "Revoke an API key. Whatever used it stops working." },
96
+ // ⛔ MEDIUM, BESIDE `key.new`, AND NOT `high`. It writes a long-lived secret to this disk, which
97
+ // is more than a read; but the pair opens nothing until a person registers the public half in
98
+ // a browser, nothing is spent, and the file is never replaced — so it is not the tier that
99
+ // waits for an unlock.
100
+ "platform.keygen": { tier: "medium", what: "Make a business key pair and write its private half to a file on this disk." },
93
101
  "public-code.publish": { tier: "medium", what: "Publish this account's public code. Publishing cannot be undone." },
94
102
  "losses.dismiss": { tier: "medium", what: "Put this loss notice down. It will not be shown again." },
95
103
  // ⛔ MEDIUM AND NOT HIGH. It moves value, so it is not `low`; but it moves it INSIDE one person's
@@ -125,6 +133,10 @@ export const ACTS = {
125
133
  // ⛔ READING THE HALL IS `none` AND LISTING A NAME IS `medium`. The read signs nothing and asks
126
134
  // nobody's wallet a question; setting a name PUBLISHES a name beside an address on a public
127
135
  // page, which is undone by one more run of the same command but cannot be unseen.
136
+ // ⛔ `list` READS; `use` WRITES — it changes which address every later payment leaves from, on
137
+ // every device. Low rather than high because it signs nothing and moves nothing, and running
138
+ // it again puts the old number back.
139
+ "wallet.use": { tier: "low", what: "Change which of this key's wallets pays for storage." },
128
140
  "wallet.hall": { tier: "none" },
129
141
  "wallet.hall.set": { tier: "medium", what: "Publish this name beside your wallet's address in the gift hall of fame." },
130
142
  "wallet.storage.reshape": { tier: "high", lock: "wallet", what: "Cut or join a storage resource the wallet holds — a signed transaction.", asksItself: true },
@@ -156,6 +168,8 @@ export function actOf(args) {
156
168
  // both land on the act that publishes; the bare command is a read like `wallet` itself.
157
169
  if (sub === "hall")
158
170
  return args.name !== undefined || args.remove ? "wallet.hall.set" : "wallet.hall";
171
+ if (sub === "use")
172
+ return "wallet.use";
159
173
  return sub === "send" ? "wallet.send" : sub === "swap" ? "wallet.swap" : sub === "donate" ? "wallet.donate" : "wallet";
160
174
  case "put":
161
175
  return args.pay === "wallet" ? "put.wallet" : "put";
@@ -173,6 +187,8 @@ export function actOf(args) {
173
187
  return args.publish ? "public-code.publish" : "public-code";
174
188
  case "support":
175
189
  return sub === "send" || sub === "reply" ? "support.send" : "support";
190
+ case "platform":
191
+ return sub === "keygen" ? "platform.keygen" : "platform";
176
192
  case "on-collision":
177
193
  return sub === "" ? "on-collision" : "on-collision.set";
178
194
  case "padding":
package/dist/run-log.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** The one key the record lives under. On this machine that is `runs.jsonl` in the config directory. */
2
+ export declare const RUN_LOG_KEY = "runlog";
1
3
  /** One request this run made, and what came back. */
2
4
  export interface HttpEvent {
3
5
  readonly kind: "http";
@@ -37,8 +39,6 @@ export declare const NO_RUN_LOG_ENV_VAR = "NMTS_NO_RUN_LOG";
37
39
  export declare const MAX_LOG_BYTES: number;
38
40
  /** The most runs `--attach-log` will take. */
39
41
  export declare const MAX_ATTACHED_RUNS = 20;
40
- /** Where the log lives. Inside the tool's own directory, which is already 0700. */
41
- export declare function runLogPath(): string;
42
42
  /** Has the person turned it off? */
43
43
  export declare function runLogIsOff(): boolean;
44
44
  /**
@@ -73,6 +73,6 @@ export declare function safeArgs(argv: readonly string[]): string[];
73
73
  * Returns nothing and throws nothing: see the header. A run that could not be written is a run
74
74
  * that is missing from a report, which is a smaller problem than a command that failed for it.
75
75
  */
76
- export declare function recordRun(argv: readonly string[], exit: number, ms: number, now?: Date): void;
76
+ export declare function recordRun(argv: readonly string[], exit: number, ms: number, now?: Date): Promise<void>;
77
77
  /** The newest `count` runs, oldest first. An unreadable or missing file is no runs. */
78
- export declare function readRuns(count: number): RunRecord[];
78
+ export declare function readRuns(count: number): Promise<RunRecord[]>;
package/dist/run-log.js CHANGED
@@ -21,12 +21,13 @@
21
21
  // `nmts get` to report a failure it did not have. Every write is inside a `try` that swallows.
22
22
  // ⚠ There is no debug switch in this tool to say so through, so it is silent — which is why
23
23
  // this paragraph is here rather than a line of output.
24
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
25
- import { join } from "node:path";
26
24
  import { OPTIONS_TAKING_A_VALUE } from "./args.js";
27
- import { configDir, modesAreEnforced } from "./credentials.js";
25
+ import { fromUtf8, utf8 } from "./bytes.js";
26
+ import { host } from "./host.js";
28
27
  import { redact } from "./redact.js";
29
28
  import { VERSION } from "./product.js";
29
+ /** The one key the record lives under. On this machine that is `runs.jsonl` in the config directory. */
30
+ export const RUN_LOG_KEY = "runlog";
30
31
  /** Set this to `1` and nothing is written. */
31
32
  export const NO_RUN_LOG_ENV_VAR = "NMTS_NO_RUN_LOG";
32
33
  /**
@@ -69,13 +70,9 @@ const HIDDEN_VALUES = {
69
70
  * common and should not be given one just for this.
70
71
  */
71
72
  let collected = [];
72
- /** Where the log lives. Inside the tool's own directory, which is already 0700. */
73
- export function runLogPath() {
74
- return join(configDir(), "runs.jsonl");
75
- }
76
73
  /** Has the person turned it off? */
77
74
  export function runLogIsOff() {
78
- return process.env[NO_RUN_LOG_ENV_VAR] === "1";
75
+ return host().env(NO_RUN_LOG_ENV_VAR) === "1";
79
76
  }
80
77
  function remember(event) {
81
78
  if (runLogIsOff())
@@ -158,41 +155,34 @@ export function safeArgs(argv) {
158
155
  }
159
156
  return out;
160
157
  }
161
- /** Write one line, creating the file 0600 if it is not there yet. */
162
- function append(line) {
163
- const dir = configDir();
164
- mkdirSync(dir, { recursive: true, mode: 0o700 });
165
- const path = runLogPath();
166
- if (!existsSync(path)) {
167
- writeFileSync(path, "", { mode: 0o600 });
168
- if (modesAreEnforced())
169
- chmodSync(path, 0o600);
170
- }
171
- appendFileSync(path, line);
172
- if (statSync(path).size > MAX_LOG_BYTES)
173
- trim(path);
174
- }
175
158
  /**
176
- * Drop the oldest whole lines until what is left fits.
159
+ * Add one line, trimming the oldest away when the record has grown past its cap.
160
+ *
161
+ * ⛔ WHOLE LINES. Cutting at a byte offset leaves a half-written JSON object at the top, and a
162
+ * reader that meets one has to decide whether the record is corrupt or merely trimmed.
177
163
  *
178
- * WHOLE LINES. Cutting the file at a byte offset leaves a half-written JSON object at the top,
179
- * and a reader that meets one has to decide whether the file is corrupt or merely trimmed.
164
+ * READ, ADD, WRITE, rather than an append. A host's state is bytes under a key the only shape
165
+ * a browser's store and a file both have and the record is a quarter of a megabyte at most,
166
+ * which is what makes rewriting it affordable at the end of a run.
180
167
  */
181
- function trim(path) {
182
- const lines = readFileSync(path, "utf8").split("\n");
168
+ async function append(line) {
169
+ const state = host().state;
170
+ const held = await state.read(RUN_LOG_KEY);
171
+ const lines = (held === undefined ? "" : fromUtf8(held)).split("\n");
172
+ lines.push(line.replace(/\n$/, ""));
183
173
  const kept = [];
184
174
  let bytes = 0;
185
175
  for (let i = lines.length - 1; i >= 0; i -= 1) {
186
- const line = lines[i];
187
- if (line === undefined || line === "")
176
+ const one = lines[i];
177
+ if (one === undefined || one === "")
188
178
  continue;
189
- const size = Buffer.byteLength(line, "utf8") + 1;
179
+ const size = utf8(one).length + 1;
190
180
  if (bytes + size > MAX_LOG_BYTES)
191
181
  break;
192
- kept.unshift(line);
182
+ kept.unshift(one);
193
183
  bytes += size;
194
184
  }
195
- writeFileSync(path, kept.length === 0 ? "" : `${kept.join("\n")}\n`, { mode: 0o600 });
185
+ await state.write(RUN_LOG_KEY, utf8(kept.length === 0 ? "" : `${kept.join("\n")}\n`));
196
186
  }
197
187
  /**
198
188
  * Write this run down, and forget what was collected.
@@ -200,7 +190,7 @@ function trim(path) {
200
190
  * Returns nothing and throws nothing: see the header. A run that could not be written is a run
201
191
  * that is missing from a report, which is a smaller problem than a command that failed for it.
202
192
  */
203
- export function recordRun(argv, exit, ms, now = new Date()) {
193
+ export async function recordRun(argv, exit, ms, now = new Date()) {
204
194
  if (runLogIsOff()) {
205
195
  forgetRun();
206
196
  return;
@@ -220,7 +210,7 @@ export function recordRun(argv, exit, ms, now = new Date()) {
220
210
  };
221
211
  forgetRun();
222
212
  try {
223
- append(`${JSON.stringify(record)}\n`);
213
+ await append(`${JSON.stringify(record)}\n`);
224
214
  }
225
215
  catch {
226
216
  // Swallowed on purpose. See the header: the log is a convenience and the command is the work.
@@ -274,10 +264,13 @@ function asRecord(value) {
274
264
  };
275
265
  }
276
266
  /** The newest `count` runs, oldest first. An unreadable or missing file is no runs. */
277
- export function readRuns(count) {
267
+ export async function readRuns(count) {
278
268
  let text;
279
269
  try {
280
- text = readFileSync(runLogPath(), "utf8");
270
+ const held = await host().state.read(RUN_LOG_KEY);
271
+ if (held === undefined)
272
+ return [];
273
+ text = fromUtf8(held);
281
274
  }
282
275
  catch {
283
276
  return [];
package/dist/seal.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // ⛔ THE PLAINTEXT NEVER LEAVES THIS PROCESS. What goes out is the NCF-3 stream; what the server
9
9
  // is told is its LENGTH. The name, the folder and the real size are written into the account's
10
10
  // sealed file list, which the server cannot open.
11
- import { createHash } from "node:crypto";
11
+ import { sha256 } from "@noble/hashes/sha2.js";
12
+ import { toBase64Url } from "./bytes.js";
12
13
  import { AAD } from "./crypto.js";
13
14
  import { NmtsError } from "./errors.js";
14
15
  import { chunkCount, sealedLenFor as sealedLength, } from "./shared/lib/crypto/size-padding.js";
@@ -108,8 +109,8 @@ export function fileSecrets(crypt, dataKey, contentDigest) {
108
109
  const contentHashCt = crypt.envelope_seal(dataKey, encoder.encode(AAD.contentHash), contentDigest);
109
110
  return {
110
111
  dek,
111
- dekWrapped: Buffer.from(dekWrapped).toString("base64url"),
112
- contentHashCt: Buffer.from(contentHashCt).toString("base64url"),
112
+ dekWrapped: toBase64Url(dekWrapped),
113
+ contentHashCt: toBase64Url(contentHashCt),
113
114
  };
114
115
  }
115
116
  /**
@@ -180,7 +181,7 @@ export async function sealFile(crypt, dataKey, plaintext) {
180
181
  nextStep: "The storage network has nothing to store and would refuse the reservation.",
181
182
  });
182
183
  }
183
- const digest = new Uint8Array(createHash("sha256").update(plaintext).digest());
184
+ const digest = sha256(plaintext);
184
185
  const secrets = fileSecrets(crypt, dataKey, digest);
185
186
  digest.fill(0);
186
187
  try {
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const DEFAULT_SERVER = "https://nmts.me";
2
- export declare const SERVER_ENV_VAR = "NMTS_SERVER";
2
+ export { SERVER_ENV_VAR } from "./env-vars.ts";
3
3
  /**
4
4
  * Resolve the server for this run: an explicit argument, then the environment, then the default.
5
5
  *
package/dist/server.js CHANGED
@@ -4,9 +4,13 @@
4
4
  // development against a local stack, not as a knob to tune. It is read from an argument or the
5
5
  // environment rather than stored, so a credentials file cannot silently point a later run
6
6
  // somewhere else than the run that wrote it.
7
+ import { SERVER_ENV_VAR } from "./env-vars.js";
7
8
  import { NmtsError } from "./errors.js";
9
+ import { host } from "./host.js";
8
10
  export const DEFAULT_SERVER = "https://nmts.me";
9
- export const SERVER_ENV_VAR = "NMTS_SERVER";
11
+ // Re-exported so every caller still finds it here; the name itself lives in a module with no
12
+ // imports, because `nmts --help` prints it (`env-vars.ts`).
13
+ export { SERVER_ENV_VAR } from "./env-vars.js";
10
14
  /**
11
15
  * Resolve the server for this run: an explicit argument, then the environment, then the default.
12
16
  *
@@ -14,7 +18,7 @@ export const SERVER_ENV_VAR = "NMTS_SERVER";
14
18
  * typo that lands on another scheme must stop here rather than somewhere further in.
15
19
  */
16
20
  export function resolveServer(explicit) {
17
- const raw = explicit ?? process.env[SERVER_ENV_VAR] ?? DEFAULT_SERVER;
21
+ const raw = explicit ?? host().env(SERVER_ENV_VAR) ?? DEFAULT_SERVER;
18
22
  let url;
19
23
  try {
20
24
  url = new URL(raw);
@@ -19,13 +19,13 @@ import { promptLine, stdinIsATerminal } from "./prompt.js";
19
19
  * never sees a line.
20
20
  */
21
21
  export async function askAboutCollisions(say) {
22
- if (hasChosen() || !stdinIsATerminal())
22
+ if ((await hasChosen()) || !stdinIsATerminal())
23
23
  return;
24
24
  say(``);
25
25
  say(`When a file with that name is already in the drive:`);
26
26
  say(` ${ANSWER_NUMBER.rename} ${COLLISION_MEANS.rename}`);
27
27
  say(` ${ANSWER_NUMBER.overwrite} ${COLLISION_MEANS.overwrite}`);
28
28
  const choice = readAnswer(await promptLine(`[${ANSWER_NUMBER.rename}] `));
29
- setChoice(choice, VERSION, new Date());
29
+ await setChoice(choice, VERSION, new Date());
30
30
  say(`${choice} — ${COLLISION_MEANS[choice]} Change it: ${BINARY_NAME} on-collision <rename|overwrite>`);
31
31
  }
@@ -1,4 +1,4 @@
1
- import type { AccountSettings, ManifestEntry } from "./manifest-codec.ts";
1
+ import type { ManifestEntry } from "./manifest-codec.ts";
2
2
  /** One drive edit, in a form that can be replayed onto a newer list. */
3
3
  export type ManifestIntent =
4
4
  /** Insert or replace by id. Upload-commit and folder-create both land here. */
@@ -115,37 +115,15 @@ export type ManifestIntent =
115
115
  */
116
116
  export declare function applyIntent(entries: readonly ManifestEntry[], intent: ManifestIntent): readonly ManifestEntry[];
117
117
  /**
118
- * Which size-padding rule an account seals its next upload under.
118
+ * The account settings' own vocabulary — the padding rule, one settings edit, and how an edit
119
+ * lands — comes back out of `manifest-settings-patch.ts`, beside the fields it names.
119
120
  *
120
- * ⛔ DECLARED BESIDE THE PATCH THAT CARRIES IT, not in `lib/crypto/padding.ts` where the padding
121
- * itself lives. `padding.ts` imports it from here. The direction matters: this file is copied
122
- * byte-for-byte into the `nmts` command-line package, and a type reaching out of it into the
123
- * crypto tree would drag that whole tree along with it for the sake of two string literals.
121
+ * ⛔ RE-EXPORTED RATHER THAN MOVED AWAY. Every caller in both packages spells it `manifest-ops`,
122
+ * and a settings patch IS one of this file's intents as far as they are concerned. It moved
123
+ * because the settings write path had grown into the file's ceiling (`check:size`), and
124
+ * splitting it puts each rule beside the field it is about.
124
125
  */
125
- export type PaddingMode = "padme" | "pow2" | "none";
126
- /**
127
- * One account-settings edit, as DESIRED STATE per field — never "toggle", so replaying it onto a
128
- * list another device wrote lands the same answer. A default value means absence in the codec.
129
- */
130
- export interface SettingsPatch {
131
- developerMode?: boolean;
132
- textScalePct?: number;
133
- /** Which rule seals future uploads: `"padme"` = the default, `"none"` = the file's exact length. */
134
- paddingMode?: PaddingMode;
135
- /** Credits held back with each credit-paid upload, 0 to `DEPOSIT_MAX_CREDITS`. */
136
- depositDefault?: number;
137
- /** The standing tip in tenths of a percent (0 = none) and the instant its terms were agreed to (0 clears). */
138
- tipTenths?: number;
139
- tipConsentAt?: number;
140
- }
141
- /**
142
- * Apply one settings patch, returning new settings. Returns the SAME reference when nothing
143
- * changed, so callers can skip a save (a version bump every other device must download).
144
- *
145
- * A text scale is CLAMPED into the codec's bounds here — this is the one write path, so a value
146
- * the slider or the typed field lets through never reaches the wire out of range.
147
- */
148
- export declare function applySettingsPatch(settings: AccountSettings, patch: SettingsPatch): AccountSettings;
126
+ export { applySettingsPatch, type PaddingMode, type SettingsPatch, } from "./manifest-settings-patch.ts";
149
127
  /** Replay a whole queue in order. Used to rebuild after a version conflict. */
150
128
  export declare function applyIntents(entries: readonly ManifestEntry[], intents: readonly ManifestIntent[]): readonly ManifestEntry[];
151
129
  /**
@@ -1,6 +1,3 @@
1
- // Runtime import (relative + .ts — the header's node --test rule) for the bounds the patch obeys.
2
- import { TEXT_SCALE_DEFAULT_PCT, TEXT_SCALE_MAX_PCT, TEXT_SCALE_MIN_PCT, } from "./manifest-codec.js";
3
- import { applyDepositPatch, applyTipPatch } from "./manifest-settings.js";
4
1
  /**
5
2
  * Apply one intent, returning a new list. The input is never mutated: the store keeps the
6
3
  * pre-save snapshot around to rebuild from after a version conflict.
@@ -160,44 +157,15 @@ function withShares(e, shares, at) {
160
157
  return { ...e, shares, updatedAt: at };
161
158
  }
162
159
  /**
163
- * Apply one settings patch, returning new settings. Returns the SAME reference when nothing
164
- * changed, so callers can skip a save (a version bump every other device must download).
160
+ * The account settings' own vocabulary — the padding rule, one settings edit, and how an edit
161
+ * lands comes back out of `manifest-settings-patch.ts`, beside the fields it names.
165
162
  *
166
- * A text scale is CLAMPED into the codec's bounds here this is the one write path, so a value
167
- * the slider or the typed field lets through never reaches the wire out of range.
163
+ * RE-EXPORTED RATHER THAN MOVED AWAY. Every caller in both packages spells it `manifest-ops`,
164
+ * and a settings patch IS one of this file's intents as far as they are concerned. It moved
165
+ * because the settings write path had grown into the file's ceiling (`check:size`), and
166
+ * splitting it puts each rule beside the field it is about.
168
167
  */
169
- export function applySettingsPatch(settings, patch) {
170
- const next = { ...settings };
171
- if (patch.developerMode !== undefined) {
172
- if (patch.developerMode)
173
- next.developerMode = true;
174
- else
175
- delete next.developerMode;
176
- }
177
- if (patch.paddingMode !== undefined) {
178
- // The default is spelled as absence, like every other field here — so two devices that both
179
- // "choose the default" write the same bytes and neither bumps the list's version.
180
- if (patch.paddingMode === "pow2" || patch.paddingMode === "none")
181
- next.paddingMode = patch.paddingMode;
182
- else
183
- delete next.paddingMode;
184
- }
185
- if (patch.textScalePct !== undefined && Number.isFinite(patch.textScalePct)) {
186
- const pct = Math.round(Math.min(TEXT_SCALE_MAX_PCT, Math.max(TEXT_SCALE_MIN_PCT, patch.textScalePct)));
187
- if (pct === TEXT_SCALE_DEFAULT_PCT)
188
- delete next.textScalePct;
189
- else
190
- next.textScalePct = pct;
191
- }
192
- applyDepositPatch(next, patch.depositDefault);
193
- applyTipPatch(next, patch.tipTenths, patch.tipConsentAt);
194
- const same = (next.developerMode === true) === (settings.developerMode === true) &&
195
- next.paddingMode === settings.paddingMode && next.depositDefault === settings.depositDefault &&
196
- next.textScalePct === settings.textScalePct &&
197
- next.tipTenths === settings.tipTenths &&
198
- next.tipConsentAt === settings.tipConsentAt;
199
- return same ? settings : next;
200
- }
168
+ export { applySettingsPatch, } from "./manifest-settings-patch.js";
201
169
  /** Replay a whole queue in order. Used to rebuild after a version conflict. */
202
170
  export function applyIntents(entries, intents) {
203
171
  return intents.reduce(applyIntent, entries);
@@ -0,0 +1,52 @@
1
+ import { type AccountSettings } from "./manifest-settings.ts";
2
+ /** Folds a deposit patch into a settings copy: out-of-range is clamped, the full deposit clears. */
3
+ export declare function applyDepositPatch(next: AccountSettings, depositDefault?: number): void;
4
+ /** Folds a tip patch into a settings copy: 0 clears, above the cap is capped, fractions are rounded. */
5
+ export declare function applyTipPatch(next: AccountSettings, tipTenths?: number, tipConsentAt?: number): void;
6
+ /**
7
+ * Folds a wallet patch into a settings copy: out-of-range is clamped, wallet 0 and a list of one
8
+ * clear, and the count is left holding the paying wallet.
9
+ *
10
+ * ⛔ THE COUNT FOLLOWS THE NUMBER, in this one write path. `wallet use 7` on a list of three
11
+ * wallets means the person is paying from a wallet the screen would not have drawn; raising the
12
+ * count here is what makes the two facts agree on every device at once, rather than in whichever
13
+ * screen happens to notice.
14
+ */
15
+ export declare function applyWalletPatch(next: AccountSettings, activeWallet?: number, walletCount?: number): void;
16
+ /**
17
+ * Which size-padding rule an account seals its next upload under.
18
+ *
19
+ * ⛔ DECLARED BESIDE THE FIELD THAT CARRIES IT, not in `lib/crypto/padding.ts` where the padding
20
+ * itself lives — `padding.ts` imports it through `manifest-ops.ts`, which re-exports it. The
21
+ * direction matters: this file is copied byte-for-byte into the `nmts` command-line package,
22
+ * and a type reaching out of it into the crypto tree would drag that whole tree along with it
23
+ * for the sake of two string literals.
24
+ */
25
+ export type PaddingMode = "padme" | "pow2" | "none";
26
+ /**
27
+ * One account-settings edit, as DESIRED STATE per field — never "toggle", so replaying it onto a
28
+ * list another device wrote lands the same answer. A default value means absence in the codec.
29
+ */
30
+ export interface SettingsPatch {
31
+ developerMode?: boolean;
32
+ textScalePct?: number;
33
+ /** Which rule seals future uploads: `"padme"` = the default, `"none"` = the file's exact length. */
34
+ paddingMode?: PaddingMode;
35
+ /** Credits held back with each credit-paid upload, 0 to `DEPOSIT_MAX_CREDITS`. */
36
+ depositDefault?: number;
37
+ /** The standing tip in tenths of a percent (0 = none) and the instant its terms were agreed to (0 clears). */
38
+ tipTenths?: number;
39
+ tipConsentAt?: number;
40
+ /** Which wallet pays, by index (0 = the wallet every account starts with). */
41
+ activeWallet?: number;
42
+ /** How many wallets this account's list holds. Raised to hold `activeWallet`, never lowered. */
43
+ walletCount?: number;
44
+ }
45
+ /**
46
+ * Apply one settings patch, returning new settings. Returns the SAME reference when nothing
47
+ * changed, so callers can skip a save (a version bump every other device must download).
48
+ *
49
+ * A text scale is CLAMPED into the codec's bounds here — this is the one write path, so a value
50
+ * the slider or the typed field lets through never reaches the wire out of range.
51
+ */
52
+ export declare function applySettingsPatch(settings: AccountSettings, patch: SettingsPatch): AccountSettings;
@@ -0,0 +1,110 @@
1
+ // What a DEVICE writes into the account settings — one edit, as desired state per field.
2
+ //
3
+ // ⛔ APART FROM `manifest-settings.ts` BECAUSE THE TWO ANSWER DIFFERENT QUESTIONS. That file says
4
+ // what the bytes carry and what a value some other build wrote is worth (out of range is
5
+ // DROPPED — falling back to a default is always safe to read). This one says what happens when
6
+ // a person moves a control here (out of range is CLAMPED — the slider must not be able to write
7
+ // something the codec would then throw away). Mixing the two is how a rule ends up applied on
8
+ // one side only.
9
+ //
10
+ // ⚠ PUBLISHED — copied byte-for-byte into the `nmts` command-line package, and reached from there
11
+ // through `manifest-ops.ts`, which re-exports everything below.
12
+ import { ACTIVE_WALLET_DEFAULT, DEPOSIT_DEFAULT_CREDITS, DEPOSIT_MAX_CREDITS, TEXT_SCALE_DEFAULT_PCT, TEXT_SCALE_MAX_PCT, TEXT_SCALE_MIN_PCT, TIP_TENTHS_MAX, WALLET_COUNT_DEFAULT, WALLET_COUNT_MAX, WALLET_INDEX_LIMIT, walletCountOf, } from "./manifest-settings.js";
13
+ /** Folds a deposit patch into a settings copy: out-of-range is clamped, the full deposit clears. */
14
+ export function applyDepositPatch(next, depositDefault) {
15
+ if (depositDefault === undefined || !Number.isFinite(depositDefault))
16
+ return;
17
+ const credits = Math.round(Math.min(DEPOSIT_MAX_CREDITS, Math.max(0, depositDefault)));
18
+ if (credits === DEPOSIT_DEFAULT_CREDITS)
19
+ delete next.depositDefault;
20
+ else
21
+ next.depositDefault = credits;
22
+ }
23
+ /** Folds a tip patch into a settings copy: 0 clears, above the cap is capped, fractions are rounded. */
24
+ export function applyTipPatch(next, tipTenths, tipConsentAt) {
25
+ if (tipTenths !== undefined && Number.isFinite(tipTenths)) {
26
+ const t = Math.round(Math.min(TIP_TENTHS_MAX, Math.max(0, tipTenths)));
27
+ if (t === 0)
28
+ delete next.tipTenths;
29
+ else
30
+ next.tipTenths = t;
31
+ }
32
+ if (tipConsentAt !== undefined && Number.isFinite(tipConsentAt)) {
33
+ if (tipConsentAt <= 0)
34
+ delete next.tipConsentAt;
35
+ else
36
+ next.tipConsentAt = Math.round(tipConsentAt);
37
+ }
38
+ }
39
+ /**
40
+ * Folds a wallet patch into a settings copy: out-of-range is clamped, wallet 0 and a list of one
41
+ * clear, and the count is left holding the paying wallet.
42
+ *
43
+ * ⛔ THE COUNT FOLLOWS THE NUMBER, in this one write path. `wallet use 7` on a list of three
44
+ * wallets means the person is paying from a wallet the screen would not have drawn; raising the
45
+ * count here is what makes the two facts agree on every device at once, rather than in whichever
46
+ * screen happens to notice.
47
+ */
48
+ export function applyWalletPatch(next, activeWallet, walletCount) {
49
+ if (activeWallet !== undefined && Number.isFinite(activeWallet)) {
50
+ const index = Math.round(Math.min(WALLET_INDEX_LIMIT - 1, Math.max(0, activeWallet)));
51
+ if (index === ACTIVE_WALLET_DEFAULT)
52
+ delete next.activeWallet;
53
+ else
54
+ next.activeWallet = index;
55
+ }
56
+ if (walletCount !== undefined && Number.isFinite(walletCount)) {
57
+ const count = Math.round(Math.min(WALLET_COUNT_MAX, Math.max(WALLET_COUNT_DEFAULT, walletCount)));
58
+ if (count === WALLET_COUNT_DEFAULT)
59
+ delete next.walletCount;
60
+ else
61
+ next.walletCount = count;
62
+ }
63
+ const counted = walletCountOf(next);
64
+ if (counted === WALLET_COUNT_DEFAULT)
65
+ delete next.walletCount;
66
+ else
67
+ next.walletCount = counted;
68
+ }
69
+ /**
70
+ * Apply one settings patch, returning new settings. Returns the SAME reference when nothing
71
+ * changed, so callers can skip a save (a version bump every other device must download).
72
+ *
73
+ * A text scale is CLAMPED into the codec's bounds here — this is the one write path, so a value
74
+ * the slider or the typed field lets through never reaches the wire out of range.
75
+ */
76
+ export function applySettingsPatch(settings, patch) {
77
+ const next = { ...settings };
78
+ if (patch.developerMode !== undefined) {
79
+ if (patch.developerMode)
80
+ next.developerMode = true;
81
+ else
82
+ delete next.developerMode;
83
+ }
84
+ if (patch.paddingMode !== undefined) {
85
+ // The default is spelled as absence, like every other field here — so two devices that both
86
+ // "choose the default" write the same bytes and neither bumps the list's version.
87
+ if (patch.paddingMode === "pow2" || patch.paddingMode === "none")
88
+ next.paddingMode = patch.paddingMode;
89
+ else
90
+ delete next.paddingMode;
91
+ }
92
+ if (patch.textScalePct !== undefined && Number.isFinite(patch.textScalePct)) {
93
+ const pct = Math.round(Math.min(TEXT_SCALE_MAX_PCT, Math.max(TEXT_SCALE_MIN_PCT, patch.textScalePct)));
94
+ if (pct === TEXT_SCALE_DEFAULT_PCT)
95
+ delete next.textScalePct;
96
+ else
97
+ next.textScalePct = pct;
98
+ }
99
+ applyDepositPatch(next, patch.depositDefault);
100
+ applyTipPatch(next, patch.tipTenths, patch.tipConsentAt);
101
+ applyWalletPatch(next, patch.activeWallet, patch.walletCount);
102
+ const same = (next.developerMode === true) === (settings.developerMode === true) &&
103
+ next.paddingMode === settings.paddingMode && next.depositDefault === settings.depositDefault &&
104
+ next.textScalePct === settings.textScalePct &&
105
+ next.tipTenths === settings.tipTenths &&
106
+ next.tipConsentAt === settings.tipConsentAt &&
107
+ next.activeWallet === settings.activeWallet &&
108
+ next.walletCount === settings.walletCount;
109
+ return same ? settings : next;
110
+ }