@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/README.md +23 -1
- package/dist/index.js +319 -48
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1700 -638
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/REFERENCE.md +28 -1
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
|
|
971
|
-
if (outLen >
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
6124
|
-
const profile = await readJson(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
6450
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(profileDir, entry), { recursive: true, force: true });
|
|
6202
6451
|
}
|
|
6203
|
-
await rm2(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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 ?
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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) =>
|
|
17708
|
-
const legacyPath = async (profile) =>
|
|
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
|
|