@tinycloud/cli 1.2.0-beta.2 → 1.2.0-beta.3

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/dist/index.js CHANGED
@@ -8,6 +8,114 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/lib/replication-registry.ts
12
+ function stopForSignal(signal) {
13
+ void closeReplication().finally(() => {
14
+ process.exit(signal === "SIGINT" ? 130 : 143);
15
+ });
16
+ }
17
+ function installSignalHandlers() {
18
+ if (state.installedSignalHandlers) return;
19
+ state.installedSignalHandlers = true;
20
+ state.sigintHandler = () => stopForSignal("SIGINT");
21
+ state.sigtermHandler = () => stopForSignal("SIGTERM");
22
+ process.once("SIGINT", state.sigintHandler);
23
+ process.once("SIGTERM", state.sigtermHandler);
24
+ }
25
+ function removeSignalHandlers() {
26
+ if (!state.installedSignalHandlers) return;
27
+ state.installedSignalHandlers = false;
28
+ if (state.sigintHandler) process.removeListener("SIGINT", state.sigintHandler);
29
+ if (state.sigtermHandler) process.removeListener("SIGTERM", state.sigtermHandler);
30
+ state.sigintHandler = void 0;
31
+ state.sigtermHandler = void 0;
32
+ }
33
+ function registerReplication(profile, control) {
34
+ state.controls.set(profile, control);
35
+ state.allControls.add(control);
36
+ state.generation += 1;
37
+ installSignalHandlers();
38
+ }
39
+ function replicationForProfile(profile) {
40
+ return state.controls.get(profile);
41
+ }
42
+ function registeredReplications() {
43
+ return state.controls;
44
+ }
45
+ function closeReplication(schedule = scheduleTimeout) {
46
+ if (state.closing) return state.closing;
47
+ removeSignalHandlers();
48
+ let resolveClosing;
49
+ let rejectClosing;
50
+ const closing = new Promise((resolve2, reject) => {
51
+ resolveClosing = resolve2;
52
+ rejectClosing = reject;
53
+ });
54
+ state.closing = closing;
55
+ void (async () => {
56
+ let timeout;
57
+ const deadline = new Promise((resolve2) => {
58
+ timeout = schedule(() => resolve2(true), CLOSE_TIMEOUT_MS);
59
+ });
60
+ let timedOut = false;
61
+ const abandonQueuedControls = () => {
62
+ for (const control of state.allControls) {
63
+ void Promise.resolve().then(() => control.close()).catch(() => void 0);
64
+ }
65
+ state.controls.clear();
66
+ state.allControls.clear();
67
+ };
68
+ try {
69
+ while (!timedOut) {
70
+ while (state.allControls.size > 0) {
71
+ const generation = state.generation;
72
+ const pending = [...state.allControls];
73
+ state.controls.clear();
74
+ state.allControls.clear();
75
+ const drained = Promise.allSettled(pending.map((control) => Promise.resolve().then(() => control.close()))).then(() => false);
76
+ timedOut ||= await Promise.race([drained, deadline]);
77
+ if (timedOut) {
78
+ abandonQueuedControls();
79
+ break;
80
+ }
81
+ if (state.generation === generation && state.allControls.size === 0) break;
82
+ }
83
+ if (timedOut) break;
84
+ const turn = new Promise((resolve2) => setImmediate(() => resolve2(false)));
85
+ timedOut ||= await Promise.race([turn, deadline]);
86
+ if (timedOut) abandonQueuedControls();
87
+ else if (state.allControls.size === 0) break;
88
+ }
89
+ state.closing = void 0;
90
+ if (state.allControls.size === 0) removeSignalHandlers();
91
+ resolveClosing(timedOut);
92
+ } catch (error) {
93
+ state.closing = void 0;
94
+ if (state.allControls.size === 0) removeSignalHandlers();
95
+ rejectClosing(error);
96
+ } finally {
97
+ clearTimeout(timeout);
98
+ }
99
+ })();
100
+ return closing;
101
+ }
102
+ var registryKey, globalState, state, CLOSE_TIMEOUT_MS, scheduleTimeout;
103
+ var init_replication_registry = __esm({
104
+ "src/lib/replication-registry.ts"() {
105
+ "use strict";
106
+ registryKey = /* @__PURE__ */ Symbol.for("@tinycloud/cli/replication-registry");
107
+ globalState = globalThis;
108
+ state = globalState[registryKey] ??= {
109
+ controls: /* @__PURE__ */ new Map(),
110
+ allControls: /* @__PURE__ */ new Set(),
111
+ installedSignalHandlers: false,
112
+ generation: 0
113
+ };
114
+ CLOSE_TIMEOUT_MS = 3e3;
115
+ scheduleTimeout = (callback, delayMs) => setTimeout(callback, delayMs);
116
+ }
117
+ });
118
+
11
119
  // src/config/constants.ts
12
120
  import {
13
121
  profilesPath,
@@ -369,7 +477,8 @@ function handleError(error) {
369
477
  ...Object.keys(meta).length ? { meta } : {},
370
478
  warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
371
479
  });
372
- process.exit(cliError.exitCode);
480
+ if (registeredReplications().size === 0) return process.exit(cliError.exitCode);
481
+ return closeReplication().then(() => process.exit(cliError.exitCode));
373
482
  }
374
483
  function safeCliRecovery(value, context) {
375
484
  if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
@@ -479,6 +588,7 @@ var activeProfileName, CLIError, SETUP_KEYS;
479
588
  var init_errors = __esm({
480
589
  "src/output/errors.ts"() {
481
590
  "use strict";
591
+ init_replication_registry();
482
592
  init_constants();
483
593
  init_formatter();
484
594
  init_storage();
@@ -967,11 +1077,11 @@ var init_md = __esm({
967
1077
  if (len % 4)
968
1078
  throw new Error("_sha2: outputLen should be aligned to 32bit");
969
1079
  const outLen = len / 4;
970
- const state = this.get();
971
- if (outLen > state.length)
1080
+ const state2 = this.get();
1081
+ if (outLen > state2.length)
972
1082
  throw new Error("_sha2: outputLen bigger than state");
973
1083
  for (let i = 0; i < outLen; i++)
974
- oview.setUint32(4 * i, state[i], isLE2);
1084
+ oview.setUint32(4 * i, state2[i], isLE2);
975
1085
  }
976
1086
  digest() {
977
1087
  const { buffer, outputLen } = this;
@@ -5615,13 +5725,127 @@ var init_delegation_expiry = __esm({
5615
5725
  }
5616
5726
  });
5617
5727
 
5728
+ // src/lib/replication-log.ts
5729
+ import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, writeFileSync } from "fs";
5730
+ import { dirname as dirname2, join as join4 } from "path";
5731
+ function appendReplicationEvent(root, event) {
5732
+ mkdirSync(root, { recursive: true, mode: DIR_MODE });
5733
+ chmodSync(root, DIR_MODE);
5734
+ const path = join4(root, "events.jsonl");
5735
+ let size = 0;
5736
+ try {
5737
+ size = statSync(path).size;
5738
+ } catch (error) {
5739
+ if (error.code !== "ENOENT") throw error;
5740
+ }
5741
+ const line = `${JSON.stringify(event)}
5742
+ `;
5743
+ if (size + Buffer.byteLength(line) > MAX_LOG_BYTES) {
5744
+ const rotated = join4(root, "events.jsonl.1");
5745
+ try {
5746
+ renameSync(path, rotated);
5747
+ } catch (error) {
5748
+ if (error.code !== "ENOENT") throw error;
5749
+ }
5750
+ writeFileSync(path, line, { mode: LOG_MODE });
5751
+ } else {
5752
+ appendFileSync(path, line, { mode: LOG_MODE });
5753
+ }
5754
+ chmodSync(path, LOG_MODE);
5755
+ }
5756
+ function replicationLogPath(profileRoot) {
5757
+ return join4(profileRoot, "replication", "events.jsonl");
5758
+ }
5759
+ function createReplicationEventSink(profileRoot, options) {
5760
+ const pinnedNotices = /* @__PURE__ */ new Set();
5761
+ const missingGrantWarnings = /* @__PURE__ */ new Set();
5762
+ return (event) => {
5763
+ appendProfileReplicationEvent(profileRoot, event);
5764
+ if (event.type === "replication.state" && event.state === "grant_missing") {
5765
+ const prefix = event.replica ?? event.space ?? "configured prefix";
5766
+ const key = `${event.space ?? ""}\0${event.replica ?? ""}`;
5767
+ if (!missingGrantWarnings.has(key)) {
5768
+ missingGrantWarnings.add(key);
5769
+ process.stderr.write(`[replication] Warning: no usable kv/get+sync grant for ${prefix}; reads use the network (${event.code ?? "grant_missing"}).
5770
+ `);
5771
+ }
5772
+ }
5773
+ if (options.debug) {
5774
+ if (event.type === "replication.read") {
5775
+ const detail = event.source === "replica" ? "replica hit" : `${event.source} ${event.reason}`;
5776
+ const syncStatus = event.syncedBeforeRead === void 0 ? "" : ` syncedBeforeRead:${event.syncedBeforeRead}`;
5777
+ const syncError = event.syncError === void 0 ? "" : ` syncError:${event.syncError}`;
5778
+ process.stderr.write(`[replication] ${event.op} ${event.key} \u2190 ${detail} ${event.latencyMs}ms${syncStatus}${syncError}${event.stalenessMs === null ? "" : ` (synced ${Math.round(event.stalenessMs / 1e3)}s ago)`}
5779
+ `);
5780
+ } else if (event.type === "replication.write") {
5781
+ process.stderr.write(`[replication] ${event.op} ${event.keys.join(",")} ${event.outcome}${event.code ? ` ${event.code}` : ""}
5782
+ `);
5783
+ } else if (event.type === "replication.sync") {
5784
+ const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
5785
+ process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}
5786
+ `);
5787
+ }
5788
+ }
5789
+ if (event.type === "replication.read" && (event.pendingState === "in_flight" || event.pendingState === "ambiguous") && !options.quiet && !pinnedNotices.has(event.key)) {
5790
+ pinnedNotices.add(event.key);
5791
+ process.stderr.write(`[replication] ${event.key} reads from the network: an earlier write's outcome is unknown (${event.pendingState}${event.code ? `, ${event.code}` : ""}). Review: tc replica report
5792
+ `);
5793
+ }
5794
+ };
5795
+ }
5796
+ function appendProfileReplicationEvent(profileRoot, event) {
5797
+ appendReplicationEvent(dirname2(replicationLogPath(profileRoot)), event);
5798
+ }
5799
+ var MAX_LOG_BYTES, LOG_MODE, DIR_MODE;
5800
+ var init_replication_log = __esm({
5801
+ "src/lib/replication-log.ts"() {
5802
+ "use strict";
5803
+ MAX_LOG_BYTES = 5 * 1024 * 1024;
5804
+ LOG_MODE = 384;
5805
+ DIR_MODE = 448;
5806
+ }
5807
+ });
5808
+
5809
+ // src/lib/replication-config.ts
5810
+ import { join as join5 } from "path";
5811
+ import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
5812
+ import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
5813
+ function createCliReplicationConfig(profile, settings, output, enabled) {
5814
+ if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
5815
+ const root = profilePath(profile);
5816
+ return {
5817
+ enabled: true,
5818
+ prefixes: settings.prefixes,
5819
+ allowSecrets: settings.allowSecrets === true,
5820
+ mode: "foreground",
5821
+ verify: process.env.TC_REPLICATION_VERIFY === "1",
5822
+ maxStalenessMs: Number(process.env.TC_REPLICATION_MAX_STALENESS_MS ?? 6e4),
5823
+ staleSyncTimeoutMs: Number(process.env.TC_REPLICATION_SYNC_TIMEOUT_MS ?? 1e4),
5824
+ storage: sqliteReplicaStorage({
5825
+ dir: join5(root, "replication"),
5826
+ guard: (section) => withProfileLock2(profile, section, { requireProfile: true, timeoutMs: 35e3 })
5827
+ }),
5828
+ onEvent: createReplicationEventSink(root, output)
5829
+ };
5830
+ }
5831
+ var init_replication_config = __esm({
5832
+ "src/lib/replication-config.ts"() {
5833
+ "use strict";
5834
+ init_replication_log();
5835
+ }
5836
+ });
5837
+
5618
5838
  // src/lib/sdk.ts
5619
5839
  var sdk_exports = {};
5620
5840
  __export(sdk_exports, {
5621
5841
  bootstrapDelegatedSession: () => bootstrapDelegatedSession,
5842
+ closeReplication: () => closeReplication,
5622
5843
  createSDKInstance: () => createSDKInstance,
5623
5844
  ensureAuthenticated: () => ensureAuthenticated,
5624
5845
  jwkHasPrivateParameter: () => jwkHasPrivateParameter,
5846
+ registerReplication: () => registerReplication,
5847
+ registeredReplications: () => registeredReplications,
5848
+ replicationForProfile: () => replicationForProfile,
5625
5849
  restoreProfileSessionNode: () => restoreProfileSessionNode,
5626
5850
  selectSignerJwk: () => selectSignerJwk,
5627
5851
  storedSessionVerifiesLocally: () => storedSessionVerifiesLocally
@@ -5736,10 +5960,21 @@ async function createSDKInstance(ctx, options) {
5736
5960
  );
5737
5961
  }
5738
5962
  if (profile?.authMethod === "local" && effectivePrivateKey) {
5963
+ const replication2 = createCliReplicationConfig(
5964
+ ctx.profile,
5965
+ profile?.replication,
5966
+ { debug: ctx.replicationDebug, quiet: ctx.quiet },
5967
+ ctx.replication
5968
+ );
5969
+ if (ctx.replication && !replication2) {
5970
+ process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
5971
+ }
5739
5972
  const node2 = new TinyCloudNode({
5740
5973
  host: ctx.host,
5741
- privateKey: effectivePrivateKey
5974
+ privateKey: effectivePrivateKey,
5975
+ ...replication2 ? { replication: replication2 } : {}
5742
5976
  });
5977
+ if (node2.replication) registerReplication(ctx.profile, node2.replication);
5743
5978
  const restoredOwnSession = Boolean(session && session.delegationHeader && session.delegationCid && session.spaceId);
5744
5979
  if (restoredOwnSession) {
5745
5980
  await restoreProfileSession(node2, ctx.profile, profile, persistedSessionData(ctx.profile, profile, session, key));
@@ -5755,10 +5990,21 @@ async function createSDKInstance(ctx, options) {
5755
5990
  }
5756
5991
  return node2;
5757
5992
  }
5993
+ const replication = createCliReplicationConfig(
5994
+ ctx.profile,
5995
+ profile?.replication,
5996
+ { debug: ctx.replicationDebug, quiet: ctx.quiet },
5997
+ ctx.replication
5998
+ );
5999
+ if (ctx.replication && !replication) {
6000
+ process.stderr.write("[replication] Enabled, but this profile has no configured prefixes; reads use the network.\n");
6001
+ }
5758
6002
  const node = new TinyCloudNode({
5759
6003
  host: ctx.host,
5760
- privateKey: options?.privateKey
6004
+ privateKey: options?.privateKey,
6005
+ ...replication ? { replication } : {}
5761
6006
  });
6007
+ if (node.replication) registerReplication(ctx.profile, node.replication);
5762
6008
  if (options?.privateKey) {
5763
6009
  await node.signIn();
5764
6010
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
@@ -5900,6 +6146,9 @@ var init_sdk = __esm({
5900
6146
  init_session_expired();
5901
6147
  init_warnings();
5902
6148
  init_delegation_expiry();
6149
+ init_replication_config();
6150
+ init_replication_registry();
6151
+ init_replication_registry();
5903
6152
  BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
5904
6153
  }
5905
6154
  });
@@ -6040,14 +6289,14 @@ var init_host = __esm({
6040
6289
 
6041
6290
  // src/config/profiles.ts
6042
6291
  import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
6043
- import { join as join4 } from "path";
6292
+ import { join as join6 } from "path";
6044
6293
  import {
6045
- profilePath,
6294
+ profilePath as profilePath2,
6046
6295
  readSession,
6047
6296
  recordProfileDeletion,
6048
6297
  refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
6049
6298
  removeSession,
6050
- withProfileLock as withProfileLock2,
6299
+ withProfileLock as withProfileLock3,
6051
6300
  writeSession
6052
6301
  } from "@tinycloud/operations/state";
6053
6302
  var ProfileManager;
@@ -6065,7 +6314,7 @@ var init_profiles = __esm({
6065
6314
  * it around a whole read-modify-write, not just each write.
6066
6315
  */
6067
6316
  static async withLock(name, action, options) {
6068
- return withProfileLock2(name, action, options);
6317
+ return withProfileLock3(name, action, options);
6069
6318
  }
6070
6319
  /**
6071
6320
  * Read-modify-write of profile.json under the profile lock, so concurrent
@@ -6091,7 +6340,7 @@ var init_profiles = __esm({
6091
6340
  /** Owner-only profile directory (0700), created or tightened before any write into it. */
6092
6341
  static async ensureProfileDir(name) {
6093
6342
  await _ProfileManager.ensureConfigDir();
6094
- const profileDir = join4(PROFILES_DIR, name);
6343
+ const profileDir = join6(PROFILES_DIR, name);
6095
6344
  await ensureDir(profileDir);
6096
6345
  await chmod2(profileDir, PRIVATE_DIR_MODE);
6097
6346
  return profileDir;
@@ -6120,8 +6369,8 @@ var init_profiles = __esm({
6120
6369
  * Throws CLIError if the profile doesn't exist.
6121
6370
  */
6122
6371
  static async getProfile(name) {
6123
- const profilePath2 = join4(PROFILES_DIR, name, "profile.json");
6124
- const profile = await readJson(profilePath2);
6372
+ const profilePath3 = join6(PROFILES_DIR, name, "profile.json");
6373
+ const profile = await readJson(profilePath3);
6125
6374
  if (!profile) {
6126
6375
  throw new CLIError(
6127
6376
  "PROFILE_NOT_FOUND",
@@ -6136,18 +6385,18 @@ var init_profiles = __esm({
6136
6385
  */
6137
6386
  static async setProfile(name, data) {
6138
6387
  await _ProfileManager.withLock(name, async () => {
6139
- await writeJson(join4(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
6388
+ await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
6140
6389
  });
6141
6390
  }
6142
6391
  /** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
6143
6392
  static async removeProfileConfig(name) {
6144
- await _ProfileManager.withLock(name, () => rm2(join4(PROFILES_DIR, name, "profile.json"), { force: true }));
6393
+ await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "profile.json"), { force: true }));
6145
6394
  }
6146
6395
  /**
6147
6396
  * Returns true if a profile directory exists.
6148
6397
  */
6149
6398
  static async profileExists(name) {
6150
- return fileExists(join4(PROFILES_DIR, name, "profile.json"));
6399
+ return fileExists(join6(PROFILES_DIR, name, "profile.json"));
6151
6400
  }
6152
6401
  /**
6153
6402
  * Returns an array of profile directory names.
@@ -6171,7 +6420,7 @@ var init_profiles = __esm({
6171
6420
  static async deleteProfile(name) {
6172
6421
  let profileDir;
6173
6422
  try {
6174
- profileDir = profilePath(name);
6423
+ profileDir = profilePath2(name);
6175
6424
  } catch {
6176
6425
  throw new CLIError(
6177
6426
  "INVALID_PROFILE_NAME",
@@ -6196,11 +6445,11 @@ var init_profiles = __esm({
6196
6445
  return;
6197
6446
  }
6198
6447
  await _ProfileManager.withLock(name, async () => {
6199
- for (const file of ["session.json", "key.json"]) await rm2(join4(profileDir, file), { force: true });
6448
+ for (const file of ["session.json", "key.json"]) await rm2(join6(profileDir, file), { force: true });
6200
6449
  for (const entry of await readdir2(profileDir)) {
6201
- if (entry !== ".lock" && entry !== "profile.json") await rm2(join4(profileDir, entry), { recursive: true, force: true });
6450
+ if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
6202
6451
  }
6203
- await rm2(join4(profileDir, "profile.json"), { force: true });
6452
+ await rm2(join6(profileDir, "profile.json"), { force: true });
6204
6453
  await recordProfileDeletion(name);
6205
6454
  });
6206
6455
  await rmdir(profileDir).catch((error) => {
@@ -6212,7 +6461,7 @@ var init_profiles = __esm({
6212
6461
  * Returns the parsed JWK for a profile, or null if no key exists.
6213
6462
  */
6214
6463
  static async getKey(name) {
6215
- return readJson(join4(PROFILES_DIR, name, "key.json"));
6464
+ return readJson(join6(PROFILES_DIR, name, "key.json"));
6216
6465
  }
6217
6466
  /**
6218
6467
  * Saves a JWK key under the profile lock (0600, in an owner-only profile
@@ -6222,12 +6471,12 @@ var init_profiles = __esm({
6222
6471
  static async setKey(name, jwk) {
6223
6472
  await _ProfileManager.withLock(name, async () => {
6224
6473
  await refuseWriteToDeletedProfile2(name);
6225
- await writeJson(join4(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
6474
+ await writeJson(join6(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
6226
6475
  });
6227
6476
  }
6228
6477
  /** Removes key.json under the profile lock (rollback of a key a failed login created). */
6229
6478
  static async removeKey(name) {
6230
- await _ProfileManager.withLock(name, () => rm2(join4(PROFILES_DIR, name, "key.json"), { force: true }));
6479
+ await _ProfileManager.withLock(name, () => rm2(join6(PROFILES_DIR, name, "key.json"), { force: true }));
6231
6480
  }
6232
6481
  // ── Session management ──────────────────────────────────────────────
6233
6482
  /**
@@ -6254,7 +6503,7 @@ var init_profiles = __esm({
6254
6503
  * created or tightened to 0700.
6255
6504
  */
6256
6505
  static async getCacheDir(name) {
6257
- const cacheDir = join4(await _ProfileManager.ensureProfileDir(name), "cache");
6506
+ const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
6258
6507
  await ensureDir(cacheDir);
6259
6508
  await chmod2(cacheDir, PRIVATE_DIR_MODE);
6260
6509
  return cacheDir;
@@ -6277,11 +6526,20 @@ var init_profiles = __esm({
6277
6526
  const config = await _ProfileManager.getConfig();
6278
6527
  const profile = options.profile ?? process.env.TC_PROFILE ?? config.defaultProfile ?? DEFAULT_PROFILE;
6279
6528
  let profileHost;
6529
+ let profileConfig;
6280
6530
  try {
6281
- const profileConfig = await _ProfileManager.getProfile(profile);
6531
+ profileConfig = await _ProfileManager.getProfile(profile);
6282
6532
  profileHost = profileConfig.host;
6283
6533
  } catch {
6284
6534
  }
6535
+ const envReplication = process.env.TC_REPLICATION?.toLowerCase();
6536
+ const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
6537
+ const explicitEnvOff = envReplication === "0" || envReplication === "false" || envReplication === "off";
6538
+ if (envReplication !== void 0 && !envEnabled && !explicitEnvOff) {
6539
+ throw new CLIError("INVALID_ARGUMENT", "TC_REPLICATION must be one of 1, true, on, 0, false or off.", ExitCode.USAGE_ERROR);
6540
+ }
6541
+ const explicitOff = options.noReplication === true || options.replication === false;
6542
+ const replication = options.replication === true ? true : explicitOff ? false : envEnabled;
6285
6543
  const explicitHost = options.host ?? process.env.TC_HOST;
6286
6544
  let host = explicitHost ?? profileHost ?? DEFAULT_HOST;
6287
6545
  let localDiscovery;
@@ -6301,6 +6559,8 @@ var init_profiles = __esm({
6301
6559
  verbose: options.verbose ?? false,
6302
6560
  noCache: options.noCache ?? false,
6303
6561
  quiet: options.quiet ?? false,
6562
+ replication,
6563
+ replicationDebug: options.replicationDebug === true || process.env.TC_REPLICATION_DEBUG === "1",
6304
6564
  ...localDiscovery === void 0 ? {} : { localDiscovery }
6305
6565
  };
6306
6566
  }
@@ -6409,6 +6669,7 @@ function emitBanner(version2) {
6409
6669
  }
6410
6670
 
6411
6671
  // src/index.ts
6672
+ init_replication_registry();
6412
6673
  init_theme();
6413
6674
  init_formatter();
6414
6675
  init_profiles();
@@ -8131,8 +8392,8 @@ var BLAKE3 = class _BLAKE3 extends BLAKE2 {
8131
8392
  }
8132
8393
  _cloneInto(to) {
8133
8394
  to = super._cloneInto(to);
8134
- const { IV, flags, state, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
8135
- to.state.set(state.slice());
8395
+ const { IV, flags, state: state2, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
8396
+ to.state.set(state2.slice());
8136
8397
  to.stack = stack.map((i) => Uint32Array.from(i));
8137
8398
  to.IV.set(IV);
8138
8399
  to.flags = flags;
@@ -15352,12 +15613,12 @@ async function notifyShare(input) {
15352
15613
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
15353
15614
  attempts += 1;
15354
15615
  try {
15355
- const state = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
15616
+ const state2 = await input.adapter.deliver({ shareId: input.shareId, recipient, idempotencyKey, ...input.record === void 0 ? {} : { record: input.record }, ...input.signal === void 0 ? {} : { signal: input.signal } });
15356
15617
  return {
15357
15618
  protocol: "tinycloud-share",
15358
15619
  version: 1,
15359
15620
  shareId: input.shareId,
15360
- state: state === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state,
15621
+ state: state2 === "delivered" && input.record?.deliveredRecipients?.includes(recipient) ? "already-delivered" : state2,
15361
15622
  idempotencyKey,
15362
15623
  attempts
15363
15624
  };
@@ -17054,7 +17315,7 @@ function receiveJson(result, path) {
17054
17315
  import { constants } from "fs";
17055
17316
  import { lstat as lstat2, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile3, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
17056
17317
  import { randomBytes as randomBytes2 } from "crypto";
17057
- import { basename as basename2, join as join5, resolve, sep } from "path";
17318
+ import { basename as basename2, join as join7, resolve, sep } from "path";
17058
17319
  init_errors();
17059
17320
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
17060
17321
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -17108,7 +17369,7 @@ async function assertDirectory(path) {
17108
17369
  const segments = absolute.split(sep).filter(Boolean);
17109
17370
  let current = absolute.startsWith(sep) ? sep : "";
17110
17371
  for (const segment of segments) {
17111
- current = current === sep ? join5(current, segment) : join5(current, segment);
17372
+ current = current === sep ? join7(current, segment) : join7(current, segment);
17112
17373
  try {
17113
17374
  const info = await lstat2(current);
17114
17375
  if (info.isSymbolicLink()) {
@@ -17130,17 +17391,17 @@ async function writeShareOutput(directory, filename, bytes, force) {
17130
17391
  const safeName = safeFilename(filename);
17131
17392
  const directoryHandle = await open2(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
17132
17393
  const stableDirectory = await realpath(outputDirectory);
17133
- const outputPath = join5(stableDirectory, safeName);
17394
+ const outputPath = join7(stableDirectory, safeName);
17134
17395
  const directoryIdentity = await directoryHandle.stat();
17135
17396
  const assertStableDirectory = async () => {
17136
17397
  const current = await stat2(stableDirectory);
17137
17398
  if (current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino) throw new Error("OUTPUT_EXISTS");
17138
17399
  };
17139
17400
  await assertStableDirectory();
17140
- const stagingDirectory = await mkdtemp(join5(stableDirectory, ".tinycloud-share-stage-"));
17401
+ const stagingDirectory = await mkdtemp(join7(stableDirectory, ".tinycloud-share-stage-"));
17141
17402
  const stagingInfo = await lstat2(stagingDirectory);
17142
17403
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
17143
- const stagingPath = join5(stagingDirectory, `.tinycloud-share-${randomBytes2(16).toString("hex")}.tmp`);
17404
+ const stagingPath = join7(stagingDirectory, `.tinycloud-share-${randomBytes2(16).toString("hex")}.tmp`);
17144
17405
  let temporaryPath;
17145
17406
  let handle;
17146
17407
  try {
@@ -17177,7 +17438,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
17177
17438
  await rm3(stagingDirectory, { recursive: true, force: true });
17178
17439
  await directoryHandle.close();
17179
17440
  }
17180
- return join5(outputDirectory, safeName);
17441
+ return join7(outputDirectory, safeName);
17181
17442
  }
17182
17443
 
17183
17444
  // src/share/errors.ts
@@ -17463,7 +17724,7 @@ function registerShareCommand(program2) {
17463
17724
  if (json) writeJson2({ ...redactPublishedShare(result), expiryClamped: result.metadata.expiryClamped === true, ...notification === void 0 ? {} : { notification } });
17464
17725
  else publishHuman(result);
17465
17726
  } catch (error) {
17466
- handleError(shareCliError(error));
17727
+ return handleError(shareCliError(error));
17467
17728
  }
17468
17729
  });
17469
17730
  share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
@@ -17473,7 +17734,7 @@ function registerShareCommand(program2) {
17473
17734
  if (json) writeJson2(result);
17474
17735
  else inspectHuman(result);
17475
17736
  } catch (error) {
17476
- handleError(shareCliError(error));
17737
+ return handleError(shareCliError(error));
17477
17738
  }
17478
17739
  });
17479
17740
  share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
@@ -17521,7 +17782,7 @@ function registerShareCommand(program2) {
17521
17782
  if (json) receiveJson(result, output);
17522
17783
  else receiveHuman(output);
17523
17784
  } catch (error) {
17524
- handleError(shareCliError(error));
17785
+ return handleError(shareCliError(error));
17525
17786
  }
17526
17787
  });
17527
17788
  share.command("list").description("List encrypted sender history without complete bearer URLs").option("--json", "Print versioned redacted JSON").action(async (options, command) => {
@@ -17532,7 +17793,7 @@ function registerShareCommand(program2) {
17532
17793
  if (json) writeJson2({ protocol: "tinycloud-share", version: 1, shares: result });
17533
17794
  else process.stdout.write(result.map((item) => `${item.shareId} ${item.target} ${item.expiresAt}`).join("\n") + (result.length ? "\n" : ""));
17534
17795
  } catch (error) {
17535
- handleError(shareCliError(error));
17796
+ return handleError(shareCliError(error));
17536
17797
  }
17537
17798
  });
17538
17799
  share.command("show <id>").description("Show one redacted sender-history record").option("--reveal-link", "Explicitly include the complete link").option("--json", "Print versioned redacted JSON").action(async (id, options, command) => {
@@ -17544,7 +17805,7 @@ function registerShareCommand(program2) {
17544
17805
  if (json) writeJson2({ protocol: "tinycloud-share", version: 1, share: result });
17545
17806
  else writeJson2(result);
17546
17807
  } catch (error) {
17547
- handleError(shareCliError(error));
17808
+ return handleError(shareCliError(error));
17548
17809
  }
17549
17810
  });
17550
17811
  share.command("notify <id>").description("Retry idempotent delivery within 5 minutes of publication; after that publish a new share").requiredOption("--to <address>", "Recipient email").option("--json", "Print versioned JSON").action(async (id, options, command) => {
@@ -17571,7 +17832,7 @@ function registerShareCommand(program2) {
17571
17832
  if (result.reason === "delivery-window-expired") process.stderr.write(NOTIFY_WINDOW_MESSAGE);
17572
17833
  if (result.state === "partial-failure") process.exitCode = 9;
17573
17834
  } catch (error) {
17574
- handleError(shareCliError(error, "notify"));
17835
+ return handleError(shareCliError(error, "notify"));
17575
17836
  }
17576
17837
  });
17577
17838
  share.command("revoke <id>").description("Revoke addressed shares; report bearer retention honestly").option("--ancestor", "Revoke the owner delegation ancestry").option("--json", "Print versioned JSON").action(async (id, options, command) => {
@@ -17588,7 +17849,7 @@ function registerShareCommand(program2) {
17588
17849
  else process.stdout.write(`${result.state}
17589
17850
  `);
17590
17851
  } catch (error) {
17591
- handleError(shareCliError(error));
17852
+ return handleError(shareCliError(error));
17592
17853
  }
17593
17854
  });
17594
17855
  }
@@ -17599,7 +17860,7 @@ init_profiles();
17599
17860
  init_constants();
17600
17861
  init_session_expired();
17601
17862
  import { readFile as readFile4 } from "fs/promises";
17602
- import { join as join6 } from "path";
17863
+ import { join as join8 } from "path";
17603
17864
  import { createHash } from "crypto";
17604
17865
  import { writeJsonAtomic as writeJsonAtomic2 } from "@tinycloud/operations/state";
17605
17866
  import { LocationRecordValidationError, LocationRegistryHttpError, revokePolicyRootV3 } from "@tinycloud/sdk-core";
@@ -17704,8 +17965,8 @@ function createEncryptedProfileHistory(profileName, sessionSigner) {
17704
17965
  const observedProfiles = /* @__PURE__ */ new Set();
17705
17966
  let preparedKeys;
17706
17967
  let preparedKey;
17707
- const path = async (profile) => join6(await ProfileManager.getCacheDir(profile), "share-history-v2.json");
17708
- const legacyPath = async (profile) => join6(await ProfileManager.getCacheDir(profile), "share-history-v1.bin");
17968
+ const path = async (profile) => join8(await ProfileManager.getCacheDir(profile), "share-history-v2.json");
17969
+ const legacyPath = async (profile) => join8(await ProfileManager.getCacheDir(profile), "share-history-v1.bin");
17709
17970
  const b64 = (value) => Buffer.from(value).toString("base64url");
17710
17971
  const unb64 = (value) => {
17711
17972
  if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share history is unavailable");
@@ -18269,7 +18530,7 @@ function selectedShareProfile() {
18269
18530
  }
18270
18531
  return process.env.TC_PROFILE;
18271
18532
  }
18272
- program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output");
18533
+ program.name("tc").description("TinyCloud CLI \u2014 self-sovereign storage from the terminal").version(version).option("-p, --profile <name>", "Profile to use").option("-H, --host <url>", "TinyCloud node URL").option("-v, --verbose", "Enable verbose output").option("--no-cache", "Disable caching").option("-q, --quiet", "Suppress non-essential output").option("--json", "Force JSON output").option("--replication", "Enable read-through replication for this invocation").option("--no-replication", "Disable read-through replication for this invocation").option("--replication-debug", "Write replication diagnostics to stderr");
18273
18534
  program.hook("preAction", async (thisCommand) => {
18274
18535
  const opts = thisCommand.optsWithGlobals();
18275
18536
  setJsonOutputRequested(opts.json === true);
@@ -18320,7 +18581,7 @@ function firstCommandToken(values) {
18320
18581
  continue;
18321
18582
  }
18322
18583
  if (value.startsWith("--profile=") || value.startsWith("--host=")) continue;
18323
- if (value === "--verbose" || value === "--no-cache" || value === "-q" || value === "--quiet" || value === "--json") continue;
18584
+ if (value === "--verbose" || value === "--no-cache" || value === "-q" || value === "--quiet" || value === "--json" || value === "--replication" || value === "--no-replication" || value === "--replication-debug") continue;
18324
18585
  if (value.startsWith("-")) continue;
18325
18586
  return value;
18326
18587
  }
@@ -18355,8 +18616,18 @@ ${theme.muted("Repo:")} ${theme.accent("https://github.com/tinycloudlabs/web-sdk
18355
18616
  });
18356
18617
  try {
18357
18618
  await program.parseAsync(process.argv);
18619
+ if (await closeReplication()) {
18620
+ process.exitCode = 0;
18621
+ const stdoutFlushed = Promise.withResolvers();
18622
+ process.stdout.write("", () => stdoutFlushed.resolve());
18623
+ await stdoutFlushed.promise;
18624
+ const stderrFlushed = Promise.withResolvers();
18625
+ process.stderr.write("", () => stderrFlushed.resolve());
18626
+ await stderrFlushed.promise;
18627
+ process.exit();
18628
+ }
18358
18629
  } catch (error) {
18359
- handleError(error);
18630
+ await handleError(error);
18360
18631
  }
18361
18632
  /*! Bundled license information:
18362
18633