@tinycloud/cli 1.1.0-beta.8 → 1.1.0

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
@@ -32,7 +32,9 @@ var init_constants = __esm({
32
32
  NOT_FOUND: 4,
33
33
  PERMISSION_DENIED: 5,
34
34
  NETWORK_ERROR: 6,
35
- NODE_ERROR: 7
35
+ NODE_ERROR: 7,
36
+ /** A write was refused because the owner's TinyCloud storage is full. */
37
+ STORAGE_FULL: 10
36
38
  };
37
39
  }
38
40
  });
@@ -72,8 +74,14 @@ var init_theme = __esm({
72
74
 
73
75
  // src/output/formatter.ts
74
76
  import ora from "ora";
75
- function outputError(code3, message, hint) {
76
- if (isInteractive()) {
77
+ function warningLine(warnings) {
78
+ const which = warnings.map((warning) => `${warning.grantCid ?? "unidentified"}: ${warning.reason}`).join(", ");
79
+ const subject = warnings.length === 1 ? "A stored grant was" : `${warnings.length} stored grants were`;
80
+ return `${subject} not used (${which}). Approve access again when a command needs it; the old grant expires on its own.`;
81
+ }
82
+ function outputError(code3, message, hint, details = {}) {
83
+ const { meta, warnings = [] } = details;
84
+ if (!shouldOutputJson()) {
77
85
  process.stderr.write(
78
86
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
79
87
  `
@@ -84,26 +92,127 @@ function outputError(code3, message, hint) {
84
92
  `);
85
93
  }
86
94
  }
95
+ if (warnings.length > 0) process.stderr.write(` ${theme.warn(warningLine(warnings))}
96
+ `);
87
97
  } else {
88
98
  const payload = {
89
99
  error: { code: code3, message }
90
100
  };
91
101
  if (hint) payload.error.hint = hint;
102
+ if (meta) payload.error.meta = meta;
103
+ if (warnings.length > 0) payload.error.warnings = warnings;
92
104
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
93
105
  }
94
106
  }
107
+ function operationWarnings(value) {
108
+ if (!Array.isArray(value)) return [];
109
+ return value.flatMap((warning) => {
110
+ if (warning === null || typeof warning !== "object") return [];
111
+ const { code: code3, reason, grantCid } = warning;
112
+ if (typeof code3 !== "string" || !WARNING_CODE.test(code3) || typeof reason !== "string" || !WARNING_REASON.test(reason)) {
113
+ return [];
114
+ }
115
+ return [{ code: code3, reason, ...typeof grantCid === "string" && DELEGATION_CID.test(grantCid) ? { grantCid } : {} }];
116
+ });
117
+ }
95
118
  function isInteractive() {
96
119
  return Boolean(process.stdout.isTTY);
97
120
  }
98
- function formatBytes(bytes) {
99
- if (bytes < 1024) return `${bytes} B`;
100
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
101
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
121
+ function setJsonOutputRequested(requested) {
122
+ globalThis[JSON_REQUESTED] = requested;
102
123
  }
124
+ function shouldOutputJson() {
125
+ return !isInteractive() || (globalThis[JSON_REQUESTED] ?? process.argv.includes("--json"));
126
+ }
127
+ var WARNING_CODE, WARNING_REASON, DELEGATION_CID, JSON_REQUESTED;
103
128
  var init_formatter = __esm({
104
129
  "src/output/formatter.ts"() {
105
130
  "use strict";
106
131
  init_theme();
132
+ WARNING_CODE = /^[A-Z][A-Z_]{0,63}$/;
133
+ WARNING_REASON = /^[a-z][a-z_]{0,63}$/;
134
+ DELEGATION_CID = /^bafkr4i[a-z2-7]{52}$/;
135
+ JSON_REQUESTED = /* @__PURE__ */ Symbol.for("tinycloud.cli.jsonOutputRequested");
136
+ }
137
+ });
138
+
139
+ // src/output/storage.ts
140
+ function storageRejection(error) {
141
+ let node = error;
142
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null; depth += 1) {
143
+ const record = node;
144
+ let code3;
145
+ if (record.code === "STORAGE_QUOTA_EXCEEDED" || record.code === "STORAGE_LIMIT_REACHED") {
146
+ code3 = record.code;
147
+ } else if (record.code !== void 0) {
148
+ return void 0;
149
+ } else if (isStorageStatus(record)) {
150
+ code3 = storageCodeFromText(record.message);
151
+ }
152
+ if (code3) {
153
+ const account = accountTotals(record) ?? accountTotals(record.meta) ?? accountTotals(record.metadata);
154
+ return account ? { code: code3, account } : { code: code3 };
155
+ }
156
+ node = record.cause;
157
+ }
158
+ return void 0;
159
+ }
160
+ function describeStorageRejection(rejection, progress) {
161
+ const reason = rejection.code === "STORAGE_LIMIT_REACHED" ? "This write is larger than the TinyCloud storage you have left" : "TinyCloud storage is full";
162
+ const message = progress ? `${progress}${reason}.` : `${reason}; nothing was written.`;
163
+ const { account } = rejection;
164
+ const totals = account ? `${formatStorageBytes(account.usedBytes)} used of ${formatStorageBytes(account.limitBytes)}${account.plan ? ` (${account.plan} plan)` : ""}. ` : "";
165
+ return {
166
+ message,
167
+ hint: `${totals}Reading still works.
168
+ Free up space or upgrade: ${MANAGE_STORAGE_URL}`
169
+ };
170
+ }
171
+ function formatStorageBytes(bytes) {
172
+ const units = ["KiB", "MiB", "GiB", "TiB"];
173
+ if (bytes < 1024) return `${bytes} B`;
174
+ let value = bytes / 1024;
175
+ let unit = 0;
176
+ while (value >= 1024 && unit < units.length - 1) {
177
+ value /= 1024;
178
+ unit += 1;
179
+ }
180
+ return `${value.toFixed(1).replace(/\.0$/, "")} ${units[unit]}`;
181
+ }
182
+ function storageCodeFromText(message) {
183
+ if (typeof message !== "string") return void 0;
184
+ if (message.includes("larger than the TinyCloud storage you have left")) return "STORAGE_LIMIT_REACHED";
185
+ if (message.includes("TinyCloud storage is full") || /storage quota exceeded/i.test(message)) {
186
+ return "STORAGE_QUOTA_EXCEEDED";
187
+ }
188
+ return void 0;
189
+ }
190
+ function isStorageStatus(record) {
191
+ const meta = "meta" in record && typeof record.meta === "object" && record.meta !== null ? record.meta : {};
192
+ const statuses = [
193
+ "status" in record ? record.status : void 0,
194
+ "statusCode" in record ? record.statusCode : void 0,
195
+ "status" in meta ? meta.status : void 0
196
+ ];
197
+ return statuses.some((value) => value === 402 || value === 413);
198
+ }
199
+ function accountTotals(meta) {
200
+ if (typeof meta !== "object" || meta === null) return void 0;
201
+ const account = meta.account;
202
+ if (typeof account !== "object" || account === null) return void 0;
203
+ const { usedBytes, limitBytes, plan } = account;
204
+ if (!isByteCount(usedBytes) || !isByteCount(limitBytes) || limitBytes === 0) return void 0;
205
+ return typeof plan === "string" && /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/.test(plan) ? { usedBytes, limitBytes, plan } : { usedBytes, limitBytes };
206
+ }
207
+ function isByteCount(value) {
208
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
209
+ }
210
+ var MANAGE_STORAGE_URL, MAX_CAUSE_DEPTH;
211
+ var init_storage = __esm({
212
+ "src/output/storage.ts"() {
213
+ "use strict";
214
+ MANAGE_STORAGE_URL = "https://account.tinycloud.xyz/billing";
215
+ MAX_CAUSE_DEPTH = 4;
107
216
  }
108
217
  });
109
218
 
@@ -111,23 +220,25 @@ var init_formatter = __esm({
111
220
  import { readFileSync, readdirSync } from "fs";
112
221
  import { join } from "path";
113
222
  import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
223
+ import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
114
224
  function setActiveProfileName(name) {
115
225
  activeProfileName = name;
116
226
  }
117
227
  function wrapError(error) {
228
+ const storageFull = storageFullError(error);
229
+ if (storageFull) return storageFull;
230
+ if (error instanceof CLIError) return error;
118
231
  const message = error instanceof Error ? error.message : String(error);
119
- if (message.includes("Missing private key parameter in JWK")) {
120
- const profileName = activeProfileName ?? process.env.TC_PROFILE ?? DEFAULT_PROFILE;
121
- return new CLIError(
122
- "AUTH_REQUIRED",
123
- `Profile "${profileName}" cannot restore its session because its private key material is missing.`,
124
- ExitCode.AUTH_REQUIRED,
125
- {
126
- hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey`
127
- }
128
- );
232
+ const verdict = authorizationVerdictOf(error);
233
+ if (verdict === "unauthenticated") {
234
+ return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
235
+ }
236
+ if (verdict === "forbidden") {
237
+ return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
238
+ }
239
+ if (verdict === void 0 && message.includes("Missing private key parameter in JWK")) {
240
+ return missingPrivateKeyError();
129
241
  }
130
- if (error instanceof CLIError) return error;
131
242
  if (error instanceof ProfileLockTimeoutError) {
132
243
  return new CLIError(
133
244
  "PROFILE_LOCK_TIMEOUT",
@@ -139,13 +250,13 @@ function wrapError(error) {
139
250
  if (error instanceof ProfileDeletedError) {
140
251
  return new CLIError("PROFILE_NOT_FOUND", message);
141
252
  }
142
- if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
253
+ if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
143
254
  return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
144
255
  }
145
256
  if (message.includes("NOT_FOUND") || message.includes("KV_NOT_FOUND")) {
146
257
  return new CLIError("NOT_FOUND", message, ExitCode.NOT_FOUND);
147
258
  }
148
- if (message.includes("PERMISSION_DENIED")) {
259
+ if (verdict === void 0 && message.includes("PERMISSION_DENIED")) {
149
260
  return new CLIError("PERMISSION_DENIED", message, ExitCode.PERMISSION_DENIED);
150
261
  }
151
262
  if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
@@ -153,39 +264,61 @@ function wrapError(error) {
153
264
  }
154
265
  return new CLIError("ERROR", message, ExitCode.ERROR);
155
266
  }
267
+ function missingPrivateKeyError() {
268
+ const profileName = activeProfileName ?? process.env.TC_PROFILE ?? DEFAULT_PROFILE;
269
+ return new CLIError(
270
+ "AUTH_REQUIRED",
271
+ `Profile "${profileName}" cannot restore its session because its private key material is missing.`,
272
+ ExitCode.AUTH_REQUIRED,
273
+ { hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey` }
274
+ );
275
+ }
276
+ function storageFullError(error, progress) {
277
+ if (error instanceof CLIError && error.exitCode === ExitCode.STORAGE_FULL) return error;
278
+ const rejection = storageRejection(error);
279
+ if (!rejection) return void 0;
280
+ const { message, hint } = describeStorageRejection(rejection, progress);
281
+ return new CLIError(rejection.code, message, ExitCode.STORAGE_FULL, { hint });
282
+ }
156
283
  function handleError(error) {
157
284
  const cliError = wrapError(error);
158
285
  const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
159
286
  const hint = prebuilt ?? buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
160
- outputError(cliError.code, cliError.message, hint);
287
+ const authMeta = cliError.code === "AUTH_REQUIRED" || cliError.code === "PERMISSION_DENIED" ? cliError.metadata : void 0;
288
+ const meta = {};
289
+ if (cliError.status !== void 0 && authMeta !== void 0) meta.status = cliError.status;
290
+ const capability = validatedCapabilityOf({ meta: authMeta });
291
+ if (capability) {
292
+ meta.resource = capability.resource;
293
+ meta.requiredAction = capability.requiredAction;
294
+ }
295
+ outputError(cliError.code, cliError.message, hint, {
296
+ ...Object.keys(meta).length ? { meta } : {},
297
+ warnings: operationWarnings(cliError.metadata?.warnings)
298
+ });
161
299
  process.exit(cliError.exitCode);
162
300
  }
163
301
  function buildAuthHint(error) {
164
- const resource = error.metadata?.resource;
165
- const requiredAction = error.metadata?.requiredAction;
166
- if (typeof resource !== "string" || typeof requiredAction !== "string") {
167
- return void 0;
168
- }
169
- const spec = capSpecFromAuthMeta(resource, requiredAction);
302
+ const capability = validatedCapabilityOf({ meta: error.metadata });
303
+ if (!capability) return void 0;
304
+ const spec = capSpecFromAuthMeta(capability.resource, capability.requiredAction);
170
305
  if (!spec) return void 0;
171
306
  return [
172
307
  "The active session is missing a TinyCloud capability.",
173
- `Request it with: tc auth request --cap "${spec}"`,
308
+ `Request it with: tc auth request --cap '${spec.replaceAll("'", "'\\''")}'`,
174
309
  "Then retry the original command."
175
310
  ].join("\n");
176
311
  }
177
312
  function capSpecFromAuthMeta(resource, action) {
178
- const slash = resource.indexOf("/");
179
- if (slash <= 0 || slash === resource.length - 1) return void 0;
180
- const spaceUri = resource.slice(0, slash);
181
- const rest = resource.slice(slash + 1);
182
- const nextSlash = rest.indexOf("/");
183
- if (nextSlash <= 0) return void 0;
184
- const serviceShort = rest.slice(0, nextSlash);
185
- const path = rest.slice(nextSlash + 1);
186
- const actionName = action.includes("/") ? action.slice(action.indexOf("/") + 1) : action;
187
- const spaceName = spaceUri.startsWith("tinycloud:") ? spaceUri.slice(spaceUri.lastIndexOf(":") + 1) : spaceUri;
188
- return `tinycloud.${serviceShort}:${spaceName}:${path}:${actionName}`;
313
+ const slash = action.indexOf("/");
314
+ if (slash < 0) return void 0;
315
+ const longService = action.slice(0, slash);
316
+ const serviceShort = SERVICE_LONG_TO_SHORT[longService];
317
+ if (!serviceShort) return void 0;
318
+ const parsed = parseCapabilityResource(resource, serviceShort);
319
+ if (!parsed) return void 0;
320
+ const spaceName = parsed.space.slice(parsed.space.lastIndexOf(":") + 1);
321
+ return `${longService}:${spaceName}:${parsed.path}:${action.slice(slash + 1)}`;
189
322
  }
190
323
  function buildNetworkHint() {
191
324
  const readHost = (name) => {
@@ -232,6 +365,7 @@ var init_errors = __esm({
232
365
  "use strict";
233
366
  init_constants();
234
367
  init_formatter();
368
+ init_storage();
235
369
  CLIError = class extends Error {
236
370
  constructor(code3, message, exitCode = ExitCode.ERROR, metadata) {
237
371
  super(message);
@@ -239,7 +373,9 @@ var init_errors = __esm({
239
373
  this.exitCode = exitCode;
240
374
  this.metadata = metadata;
241
375
  this.name = "CLIError";
376
+ if (typeof metadata?.status === "number" && Number.isInteger(metadata.status) && metadata.status >= 400 && metadata.status <= 599) this.status = metadata.status;
242
377
  }
378
+ status;
243
379
  };
244
380
  }
245
381
  });
@@ -297,7 +433,7 @@ async function listDirs(dirPath) {
297
433
  }
298
434
  }
299
435
  var PRIVATE_FILE_MODE, PRIVATE_DIR_MODE;
300
- var init_storage = __esm({
436
+ var init_storage2 = __esm({
301
437
  "src/config/storage.ts"() {
302
438
  "use strict";
303
439
  PRIVATE_FILE_MODE = 384;
@@ -555,7 +691,7 @@ var init_profiles = __esm({
555
691
  "src/config/profiles.ts"() {
556
692
  "use strict";
557
693
  init_constants();
558
- init_storage();
694
+ init_storage2();
559
695
  init_errors();
560
696
  ProfileManager = class _ProfileManager {
561
697
  // ── Initialization ──────────────────────────────────────────────────
@@ -625,7 +761,7 @@ var init_profiles = __esm({
625
761
  if (!profile) {
626
762
  throw new CLIError(
627
763
  "PROFILE_NOT_FOUND",
628
- `Profile "${name}" does not exist. Run \`tc init\` or \`tc profile create ${name}\` first.`
764
+ `Profile "${name}" does not exist. Create it with \`tc init --name ${name}\`.`
629
765
  );
630
766
  }
631
767
  return profile;
@@ -5636,6 +5772,37 @@ var init_zod = __esm({
5636
5772
  }
5637
5773
  });
5638
5774
 
5775
+ // src/auth/session-expired.ts
5776
+ function signInAgainHint(profileName, posture) {
5777
+ if (posture === "local-owner-key") {
5778
+ return `Sign in again with: tc --profile ${profileName} auth login --method local`;
5779
+ }
5780
+ if (posture === "delegate-session") {
5781
+ return `Have the owner approve a new scoped login: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
5782
+ }
5783
+ return `Sign in again with: tc --profile ${profileName} auth login --method openkey`;
5784
+ }
5785
+ function sessionExpiredError(profileName, posture) {
5786
+ return new CLIError(
5787
+ "AUTH_REQUIRED",
5788
+ `The session for profile "${profileName}" has expired or is no longer valid.`,
5789
+ ExitCode.AUTH_REQUIRED,
5790
+ { hint: signInAgainHint(profileName, posture), reason: SESSION_EXPIRED_REASON }
5791
+ );
5792
+ }
5793
+ function isSessionExpiredError(error) {
5794
+ return error instanceof CLIError && error.metadata?.reason === SESSION_EXPIRED_REASON;
5795
+ }
5796
+ var SESSION_EXPIRED_REASON;
5797
+ var init_session_expired = __esm({
5798
+ "src/auth/session-expired.ts"() {
5799
+ "use strict";
5800
+ init_constants();
5801
+ init_errors();
5802
+ SESSION_EXPIRED_REASON = "session_expired";
5803
+ }
5804
+ });
5805
+
5639
5806
  // src/lib/raw-encryption.ts
5640
5807
  var init_raw_encryption = __esm({
5641
5808
  "src/lib/raw-encryption.ts"() {
@@ -5698,13 +5865,14 @@ async function replayAdditionalDelegations(node, profile, options) {
5698
5865
  for (const stored of entries) {
5699
5866
  const kind = storedDelegationKind(stored);
5700
5867
  if (kind === "compact" || kind === "signed-login") {
5701
- const installed = await replayStoredDelegation(activator, stored, {
5868
+ const replay = await replayStoredDelegation(activator, stored, {
5702
5869
  host: options.host,
5703
5870
  migrated,
5704
5871
  resolveSpace
5705
5872
  });
5706
- if (installed === void 0 && process.env.TC_DEBUG_REPLAY === "1") {
5707
- process.stderr.write("[replay] skipping a stored delegation refused by validation or its request binding\n");
5873
+ if (replay.status === "skipped" && process.env.TC_DEBUG_REPLAY === "1") {
5874
+ process.stderr.write(`[replay] skipping a stored delegation: ${replay.reason}
5875
+ `);
5708
5876
  }
5709
5877
  continue;
5710
5878
  }
@@ -5735,7 +5903,7 @@ var init_permissions = __esm({
5735
5903
  "src/lib/permissions.ts"() {
5736
5904
  "use strict";
5737
5905
  init_constants();
5738
- init_storage();
5906
+ init_storage2();
5739
5907
  init_profiles();
5740
5908
  init_errors();
5741
5909
  init_constants();
@@ -5782,6 +5950,16 @@ function signerJwkForProfile(profileName, sessionJwk, key) {
5782
5950
  }
5783
5951
  );
5784
5952
  }
5953
+ async function restoreProfileSession(node, profileName, profile, sessionData) {
5954
+ try {
5955
+ await node.restoreSession(sessionData);
5956
+ } catch (error) {
5957
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "AUTH_EXPIRED") {
5958
+ throw sessionExpiredError(profileName, profile === null ? void 0 : resolveProfilePosture(profile));
5959
+ }
5960
+ throw error;
5961
+ }
5962
+ }
5785
5963
  async function createSDKInstance(ctx, options) {
5786
5964
  const profile = options?.privateKey ? await ProfileManager.getProfile(ctx.profile).catch(() => null) : await ProfileManager.getProfile(ctx.profile);
5787
5965
  const session = await ProfileManager.getSession(ctx.profile);
@@ -5790,7 +5968,7 @@ async function createSDKInstance(ctx, options) {
5790
5968
  if (!key && !effectivePrivateKey && !(profile?.authMethod === "openkey" && session !== null)) {
5791
5969
  throw new CLIError(
5792
5970
  "AUTH_REQUIRED",
5793
- `No key found for profile "${ctx.profile}". Run \`tc init\` first.`,
5971
+ `No key found for profile "${ctx.profile}". Run \`tc --profile ${ctx.profile} auth rotate\` to create a new key and sign in.`,
5794
5972
  ExitCode.AUTH_REQUIRED
5795
5973
  );
5796
5974
  }
@@ -5801,7 +5979,7 @@ async function createSDKInstance(ctx, options) {
5801
5979
  });
5802
5980
  let restoredOwnSession2 = false;
5803
5981
  if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5804
- await node2.restoreSession({
5982
+ await restoreProfileSession(node2, ctx.profile, profile, {
5805
5983
  delegationHeader: session.delegationHeader,
5806
5984
  delegationCid: session.delegationCid,
5807
5985
  spaceId: session.spaceId,
@@ -5831,7 +6009,7 @@ async function createSDKInstance(ctx, options) {
5831
6009
  if (options?.privateKey) {
5832
6010
  await node.signIn();
5833
6011
  } else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
5834
- await node.restoreSession({
6012
+ await restoreProfileSession(node, ctx.profile, profile, {
5835
6013
  delegationHeader: session.delegationHeader,
5836
6014
  delegationCid: session.delegationCid,
5837
6015
  spaceId: session.spaceId,
@@ -5975,6 +6153,7 @@ var init_sdk = __esm({
5975
6153
  init_errors();
5976
6154
  init_constants();
5977
6155
  init_permissions();
6156
+ init_session_expired();
5978
6157
  BOOTSTRAP_PROFILE_FIELDS = ["sessionDid", "spaceId"];
5979
6158
  }
5980
6159
  });
@@ -6073,7 +6252,7 @@ function formatBannerLine(version2) {
6073
6252
  }
6074
6253
  function emitBanner(version2) {
6075
6254
  if (bannerEmitted) return;
6076
- if (!isInteractive()) return;
6255
+ if (shouldOutputJson()) return;
6077
6256
  if (process.env.TC_HIDE_BANNER === "1") return;
6078
6257
  bannerEmitted = true;
6079
6258
  process.stderr.write(formatBannerLine(version2) + "\n\n");
@@ -15019,7 +15198,6 @@ async function notifyShare(input) {
15019
15198
  };
15020
15199
  }
15021
15200
  let attempts = 0;
15022
- let lastError;
15023
15201
  while (attempts < attemptsLimit) {
15024
15202
  if (input.signal?.aborted) throw new ShareNotifyError("share delivery was cancelled");
15025
15203
  attempts += 1;
@@ -15046,10 +15224,19 @@ async function notifyShare(input) {
15046
15224
  reason: "delivery-window-expired"
15047
15225
  };
15048
15226
  }
15049
- lastError = error;
15227
+ if (error !== null && typeof error === "object" && "status" in error && (error.status === 401 || error.status === 403)) {
15228
+ return {
15229
+ protocol: "tinycloud-share",
15230
+ version: 1,
15231
+ shareId: input.shareId,
15232
+ state: "partial-failure",
15233
+ idempotencyKey,
15234
+ attempts,
15235
+ retryable: false
15236
+ };
15237
+ }
15050
15238
  }
15051
15239
  }
15052
- void lastError;
15053
15240
  return { protocol: "tinycloud-share", version: 1, shareId: input.shareId, state: "partial-failure", idempotencyKey, attempts, retryable: true };
15054
15241
  }
15055
15242
  async function defaultIdempotencyKey(shareId, recipient) {
@@ -16676,7 +16863,6 @@ function parseDuration(input) {
16676
16863
  }
16677
16864
 
16678
16865
  // src/commands/share.ts
16679
- init_formatter();
16680
16866
  init_errors();
16681
16867
 
16682
16868
  // src/share/output.ts
@@ -16942,9 +17128,8 @@ function shareCliError(error, operation = "publish") {
16942
17128
  if (failure.kind === "registry-rejected") {
16943
17129
  return new CLIError("REGISTRY_REJECTED", "the TinyCloud location registry rejected this session's location record, so nothing was shared; retrying will not help. Log in again, and report the problem if it persists", 6);
16944
17130
  }
16945
- if (failure.kind === "storage-quota-exceeded") {
16946
- const sizes = failure.usedBytes === void 0 || failure.limitBytes === void 0 ? "" : ` (${formatBytes(failure.usedBytes)} used of ${formatBytes(failure.limitBytes)} limit)`;
16947
- return new CLIError("STORAGE_QUOTA_EXCEEDED", `storage quota exceeded${sizes}; nothing was shared`, 4);
17131
+ if (failure.kind === "storage-full") {
17132
+ return storageFullError(failure);
16948
17133
  }
16949
17134
  if (failure.kind === "upload-failed") {
16950
17135
  return new CLIError("UPLOAD_FAILED", "share source upload failed; nothing was shared", 4);
@@ -16964,6 +17149,8 @@ function shareCliError(error, operation = "publish") {
16964
17149
  const code3 = error.code === "fetch-failed" ? "NOT_FOUND" : error.code.replaceAll("-", "_").toUpperCase();
16965
17150
  return new CLIError(code3, error.message, exit);
16966
17151
  }
17152
+ const storageFull = storageFullError(error);
17153
+ if (storageFull) return storageFull;
16967
17154
  const message = error instanceof Error ? error.message : String(error);
16968
17155
  const nodeCode = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
16969
17156
  const known = {
@@ -17257,8 +17444,10 @@ function registerShareCommand(program2) {
17257
17444
  }
17258
17445
 
17259
17446
  // src/share/adapters.ts
17447
+ init_storage();
17260
17448
  init_profiles();
17261
17449
  init_constants();
17450
+ init_session_expired();
17262
17451
  import { readFile as readFile4 } from "fs/promises";
17263
17452
  import { join as join6 } from "path";
17264
17453
  import { createHash } from "crypto";
@@ -17270,15 +17459,9 @@ function requiredKvAction(meta) {
17270
17459
  const action = meta.requiredAction;
17271
17460
  return action === "tinycloud.kv/put" || action === "tinycloud.kv/get" ? action : void 0;
17272
17461
  }
17273
- function isByteCount(value) {
17274
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
17275
- }
17276
17462
  function throwKvUploadFailure(error) {
17277
- if (typeof error === "object" && error !== null && "code" in error && error.code === "STORAGE_QUOTA_EXCEEDED") {
17278
- const meta = "meta" in error && typeof error.meta === "object" && error.meta !== null ? error.meta : {};
17279
- const { usedBytes, limitBytes } = meta;
17280
- throw new SharePublishAuthorityError(isByteCount(usedBytes) && isByteCount(limitBytes) ? { kind: "storage-quota-exceeded", usedBytes, limitBytes } : { kind: "storage-quota-exceeded" });
17281
- }
17463
+ const rejection = storageRejection(error);
17464
+ if (rejection) throw new SharePublishAuthorityError({ kind: "storage-full", ...rejection });
17282
17465
  throw new SharePublishAuthorityError({ kind: "upload-failed" });
17283
17466
  }
17284
17467
  var DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
@@ -17577,7 +17760,7 @@ function createShareAuthorityAdapters(input = {}) {
17577
17760
  } catch (error) {
17578
17761
  const profileConfig = await ProfileManager.getProfile(profile).catch(() => void 0);
17579
17762
  const code3 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
17580
- if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED") {
17763
+ if (error instanceof InvalidRestoredSessionError || code3 === "AUTH_EXPIRED" || isSessionExpiredError(error)) {
17581
17764
  throw new SharePublishAuthorityError({
17582
17765
  kind: "owner-space-unresolved",
17583
17766
  localKey: profileConfig?.authMethod === "local",
@@ -17939,6 +18122,7 @@ function selectedShareProfile() {
17939
18122
  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");
17940
18123
  program.hook("preAction", async (thisCommand) => {
17941
18124
  const opts = thisCommand.optsWithGlobals();
18125
+ setJsonOutputRequested(opts.json === true);
17942
18126
  const parentName = thisCommand.parent?.name();
17943
18127
  const isShareCommand = parentName === "share" || thisCommand.name() === "share";
17944
18128
  if (!opts.quiet && !isShareCommand) {
@@ -17947,7 +18131,7 @@ program.hook("preAction", async (thisCommand) => {
17947
18131
  const commandName = thisCommand.name();
17948
18132
  const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
17949
18133
  const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
17950
- if (!skipGuard && !opts.quiet && isInteractive()) {
18134
+ if (!skipGuard && !opts.quiet && !shouldOutputJson()) {
17951
18135
  try {
17952
18136
  const config = await ProfileManager.getConfig();
17953
18137
  const profileName = opts.profile || config.defaultProfile;