@tinycloud/cli 0.7.0-beta.2 → 0.7.0-beta.4

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
@@ -322,7 +322,8 @@ function wrapError(error) {
322
322
  }
323
323
  function handleError(error) {
324
324
  const cliError = wrapError(error);
325
- const hint = buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
325
+ const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
326
+ const hint = prebuilt ?? buildAuthHint(cliError) ?? (cliError.code === "NETWORK_ERROR" ? buildNetworkHint() : void 0);
326
327
  outputError(cliError.code, cliError.message, hint);
327
328
  process.exit(cliError.exitCode);
328
329
  }
@@ -1053,7 +1054,7 @@ import { spawn } from "child_process";
1053
1054
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1054
1055
  import { dirname as dirname2 } from "path";
1055
1056
  import { createInterface as createInterface2 } from "readline";
1056
- import { principalDidEquals } from "@tinycloud/node-sdk";
1057
+ import { grantAuthRequest, principalDidEquals } from "@tinycloud/node-sdk";
1057
1058
 
1058
1059
  // src/config/types.ts
1059
1060
  var CLI_PROFILE_POSTURES = [
@@ -5740,6 +5741,31 @@ var KVService = class extends BaseService {
5740
5741
  }
5741
5742
  return void 0;
5742
5743
  }
5744
+ /**
5745
+ * Classify a KV 404 by reading the response body once.
5746
+ *
5747
+ * The server returns 404 both for a genuinely missing key AND for an
5748
+ * un-hosted space (body "Space not found"). Previously get/head/delete
5749
+ * collapsed every 404 to KV_NOT_FOUND before reading the body, so an
5750
+ * un-hosted-space read was indistinguishable from a missing key. We now
5751
+ * preserve status + the "Space not found" body for the un-hosted case (so the
5752
+ * CLI/SDK can normalize it to SPACE_NOT_HOSTED, matching put/list/sql), and
5753
+ * fall through to KV_NOT_FOUND for a real missing key.
5754
+ */
5755
+ async classifyNotFound(response, key) {
5756
+ const errorText = await response.text();
5757
+ if (/space not found/i.test(errorText)) {
5758
+ return err(
5759
+ serviceError(
5760
+ ErrorCodes.KV_NOT_FOUND,
5761
+ `KV ${response.status} - ${errorText}`,
5762
+ "kv",
5763
+ { meta: { status: response.status, statusText: response.statusText } }
5764
+ )
5765
+ );
5766
+ }
5767
+ return err(serviceError(ErrorCodes.KV_NOT_FOUND, `Key not found: ${key}`, "kv"));
5768
+ }
5743
5769
  /**
5744
5770
  * Get the full path with optional prefix.
5745
5771
  *
@@ -5986,13 +6012,7 @@ var KVService = class extends BaseService {
5986
6012
  }));
5987
6013
  }
5988
6014
  if (response.status === 404) {
5989
- return err(
5990
- serviceError(
5991
- ErrorCodes.KV_NOT_FOUND,
5992
- `Key not found: ${key}`,
5993
- "kv"
5994
- )
5995
- );
6015
+ return this.classifyNotFound(response, key);
5996
6016
  }
5997
6017
  const errorText = await response.text();
5998
6018
  return err(
@@ -6253,13 +6273,7 @@ var KVService = class extends BaseService {
6253
6273
  }));
6254
6274
  }
6255
6275
  if (response.status === 404) {
6256
- return err(
6257
- serviceError(
6258
- ErrorCodes.KV_NOT_FOUND,
6259
- `Key not found: ${key}`,
6260
- "kv"
6261
- )
6262
- );
6276
+ return this.classifyNotFound(response, key);
6263
6277
  }
6264
6278
  const errorText = await response.text();
6265
6279
  return err(
@@ -6304,13 +6318,7 @@ var KVService = class extends BaseService {
6304
6318
  }));
6305
6319
  }
6306
6320
  if (response.status === 404) {
6307
- return err(
6308
- serviceError(
6309
- ErrorCodes.KV_NOT_FOUND,
6310
- `Key not found: ${key}`,
6311
- "kv"
6312
- )
6313
- );
6321
+ return this.classifyNotFound(response, key);
6314
6322
  }
6315
6323
  const errorText = await response.text();
6316
6324
  return err(
@@ -11488,21 +11496,8 @@ function registerAuthCommand(program2) {
11488
11496
  expiryOption: parsed.requestedExpiry,
11489
11497
  yes: options.yes === true
11490
11498
  });
11491
- const result = await node.delegateTo(
11492
- parsed.sessionDid,
11493
- parsed.requested,
11494
- parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
11495
- );
11496
- outputJson({
11497
- kind: "tinycloud.auth.delegation",
11498
- version: 1,
11499
- requestId: parsed.requestId,
11500
- delegationCid: result.delegation.cid,
11501
- delegation: result.delegation,
11502
- permissions: parsed.requested,
11503
- expiry: result.delegation.expiry.toISOString(),
11504
- prompted: result.prompted
11505
- });
11499
+ const grant = await grantAuthRequest(node, parsed);
11500
+ outputJson(grant);
11506
11501
  } catch (error) {
11507
11502
  handleError(error);
11508
11503
  }
@@ -11918,9 +11913,10 @@ function groupPermissionsBySpace(permissions) {
11918
11913
  rawEntries.push(permission);
11919
11914
  continue;
11920
11915
  }
11921
- const group = groups.get(permission.space) ?? [];
11916
+ const key = normalizeSpaceForCompare(permission.space);
11917
+ const group = groups.get(key) ?? [];
11922
11918
  group.push(permission);
11923
- groups.set(permission.space, group);
11919
+ groups.set(key, group);
11924
11920
  }
11925
11921
  const grouped = Array.from(groups.values());
11926
11922
  if (grouped.length === 0) {
@@ -11932,8 +11928,16 @@ function groupPermissionsBySpace(permissions) {
11932
11928
  function isRawPermission(permission) {
11933
11929
  return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
11934
11930
  }
11931
+ function normalizeSpaceForCompare(space) {
11932
+ return space.replace(
11933
+ /(eip155:\d+:)(0x[0-9a-fA-F]{40})/,
11934
+ (_match, prefix, addr) => prefix + addr.toLowerCase()
11935
+ );
11936
+ }
11935
11937
  function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
11936
- if (returnedSpace === expectedSpace) return true;
11938
+ if (normalizeSpaceForCompare(returnedSpace) === normalizeSpaceForCompare(expectedSpace)) {
11939
+ return true;
11940
+ }
11937
11941
  if (!returnedSpace.startsWith("tinycloud:")) return false;
11938
11942
  const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
11939
11943
  return returnedName === expectedSpace;
@@ -11942,7 +11946,7 @@ function portableFromOpenKeyDelegation(data, permissions, host) {
11942
11946
  const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
11943
11947
  const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
11944
11948
  const expectedSpaces = new Set(
11945
- permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
11949
+ permissions.filter((permission) => !isRawPermission(permission)).map((permission) => normalizeSpaceForCompare(permission.space))
11946
11950
  );
11947
11951
  const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
11948
11952
  if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
@@ -12169,6 +12173,79 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
12169
12173
  // src/commands/kv.ts
12170
12174
  import { readFile as readFile4 } from "fs/promises";
12171
12175
  import { writeFile as writeFile3 } from "fs/promises";
12176
+
12177
+ // src/lib/host.ts
12178
+ function canonicalizeAddress2(address) {
12179
+ const trimmed = address.trim();
12180
+ return trimmed.startsWith("0x") ? `0x${trimmed.slice(2).toLowerCase()}` : trimmed.toLowerCase();
12181
+ }
12182
+ async function resolveLocalAddress(profile, profileName) {
12183
+ const session = await ProfileManager.getSession(profileName);
12184
+ const sessAddr = session?.address;
12185
+ if (typeof sessAddr === "string" && sessAddr.length > 0) {
12186
+ return canonicalizeAddress2(sessAddr);
12187
+ }
12188
+ if (profile.address) return canonicalizeAddress2(profile.address);
12189
+ if (profile.ownerDid) {
12190
+ const match = profile.ownerDid.match(/^did:pkh:eip155:\d+:(0x[a-fA-F0-9]{40})$/);
12191
+ if (match) return canonicalizeAddress2(match[1]);
12192
+ }
12193
+ return null;
12194
+ }
12195
+ function ownerAddressFromSpaceUri(spaceUri) {
12196
+ const match = spaceUri.match(/^tinycloud:pkh:eip155:\d+:(0x[a-fA-F0-9]{40}):/);
12197
+ return match ? canonicalizeAddress2(match[1]) : null;
12198
+ }
12199
+ function ownerDidFromSpaceUri(spaceUri) {
12200
+ const match = spaceUri.match(/^tinycloud:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40}):/);
12201
+ if (!match) return null;
12202
+ return `did:pkh:eip155:${match[1]}:${canonicalizeAddress2(match[2])}`;
12203
+ }
12204
+ async function isRootAuthority(spaceUri, profileName) {
12205
+ const profile = await ProfileManager.getProfile(profileName);
12206
+ if (resolveProfilePosture(profile) === "delegate-session") return false;
12207
+ const ownerAddr = ownerAddressFromSpaceUri(spaceUri);
12208
+ if (!ownerAddr) return false;
12209
+ const selfAddr = await resolveLocalAddress(profile, profileName);
12210
+ return selfAddr !== null && selfAddr === ownerAddr;
12211
+ }
12212
+ function spaceNameFromUri(spaceUri) {
12213
+ return spaceUri.slice(spaceUri.lastIndexOf(":") + 1);
12214
+ }
12215
+ async function unhostedSpaceError(error, spaceUri, profileName) {
12216
+ if (!spaceUri) return null;
12217
+ const status = error.meta?.status;
12218
+ const isUnhosted = status === 404 && /space not found/i.test(error.message);
12219
+ if (!isUnhosted) return null;
12220
+ const spaceName = spaceNameFromUri(spaceUri);
12221
+ const owner = await isRootAuthority(spaceUri, profileName);
12222
+ const hint = owner ? [
12223
+ "You are the owner. Host it once:",
12224
+ ` tc space host ${spaceName}`,
12225
+ "Then retry."
12226
+ ].join("\n") : [
12227
+ "You are a delegate and CANNOT host this space \u2014 only its owner can.",
12228
+ "Emit a host request:",
12229
+ ` tc space host-request ${spaceName} --emit ./host-request.json`,
12230
+ "Send it to the owner; they run `tc space host` and confirm. Then retry."
12231
+ ].join("\n");
12232
+ const message = owner ? `Space '${spaceName}' (${spaceUri}) is not hosted.` : `Space '${spaceName}' (owner ${ownerDidFromSpaceUri(spaceUri) ?? spaceUri}) is not hosted.`;
12233
+ return new CLIError("SPACE_NOT_HOSTED", message, ExitCode.ERROR, { hint });
12234
+ }
12235
+ async function resolveHostSpace(name, profileName) {
12236
+ const resolved = await resolveSpaceUri(name, profileName);
12237
+ if (!resolved) {
12238
+ throw new Error(`Could not resolve a space for "${name}".`);
12239
+ }
12240
+ return resolved;
12241
+ }
12242
+
12243
+ // src/commands/kv.ts
12244
+ async function throwKvError(error, spaceUri, profileName) {
12245
+ const hosted = await unhostedSpaceError(error, spaceUri, profileName);
12246
+ if (hosted) throw hosted;
12247
+ throw new CLIError(error.code, error.message, ExitCode.ERROR);
12248
+ }
12172
12249
  async function readStdin2() {
12173
12250
  const chunks = [];
12174
12251
  for await (const chunk of process.stdin) {
@@ -12178,7 +12255,8 @@ async function readStdin2() {
12178
12255
  }
12179
12256
  async function kvHandle(node, spaceInput, profileName) {
12180
12257
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
12181
- return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12258
+ const kv = spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12259
+ return { kv, spaceUri };
12182
12260
  }
12183
12261
  function registerKvCommand(program2) {
12184
12262
  const kv = program2.command("kv").description("Key-value store operations");
@@ -12187,17 +12265,19 @@ function registerKvCommand(program2) {
12187
12265
  const globalOpts = cmd.optsWithGlobals();
12188
12266
  const ctx = await ProfileManager.resolveContext(globalOpts);
12189
12267
  const node = await ensureAuthenticated(ctx);
12190
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12268
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12191
12269
  const wantBytes = !!options.output || !!options.raw;
12192
12270
  const result = await withSpinner(
12193
12271
  `Getting ${key}...`,
12194
12272
  () => kv2.get(key, wantBytes ? { binary: true } : void 0)
12195
12273
  );
12196
12274
  if (!result.ok) {
12275
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12276
+ if (hosted) throw hosted;
12197
12277
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12198
12278
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
12199
12279
  }
12200
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12280
+ await throwKvError(result.error, spaceUri, ctx.profile);
12201
12281
  }
12202
12282
  const data = result.data.data;
12203
12283
  const metadata = result.data.headers ?? {};
@@ -12248,10 +12328,10 @@ function registerKvCommand(program2) {
12248
12328
  putValue = value;
12249
12329
  }
12250
12330
  }
12251
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12331
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12252
12332
  const result = await withSpinner(`Writing ${key}...`, () => kv2.put(key, putValue));
12253
12333
  if (!result.ok) {
12254
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12334
+ await throwKvError(result.error, spaceUri, ctx.profile);
12255
12335
  }
12256
12336
  outputJson({ key, written: true });
12257
12337
  } catch (error) {
@@ -12263,10 +12343,10 @@ function registerKvCommand(program2) {
12263
12343
  const globalOpts = cmd.optsWithGlobals();
12264
12344
  const ctx = await ProfileManager.resolveContext(globalOpts);
12265
12345
  const node = await ensureAuthenticated(ctx);
12266
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12346
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12267
12347
  const result = await withSpinner(`Deleting ${key}...`, () => kv2.delete(key));
12268
12348
  if (!result.ok) {
12269
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12349
+ await throwKvError(result.error, spaceUri, ctx.profile);
12270
12350
  }
12271
12351
  outputJson({ key, deleted: true });
12272
12352
  } catch (error) {
@@ -12278,11 +12358,11 @@ function registerKvCommand(program2) {
12278
12358
  const globalOpts = cmd.optsWithGlobals();
12279
12359
  const ctx = await ProfileManager.resolveContext(globalOpts);
12280
12360
  const node = await ensureAuthenticated(ctx);
12281
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12361
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12282
12362
  const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
12283
12363
  const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
12284
12364
  if (!result.ok) {
12285
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12365
+ await throwKvError(result.error, spaceUri, ctx.profile);
12286
12366
  }
12287
12367
  const rawData = result.data.data ?? result.data;
12288
12368
  const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
@@ -12313,14 +12393,16 @@ function registerKvCommand(program2) {
12313
12393
  const globalOpts = cmd.optsWithGlobals();
12314
12394
  const ctx = await ProfileManager.resolveContext(globalOpts);
12315
12395
  const node = await ensureAuthenticated(ctx);
12316
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12396
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12317
12397
  const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
12318
12398
  if (!result.ok) {
12399
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12400
+ if (hosted) throw hosted;
12319
12401
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12320
12402
  outputJson({ key, exists: false, metadata: {} });
12321
12403
  return;
12322
12404
  }
12323
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12405
+ await throwKvError(result.error, spaceUri, ctx.profile);
12324
12406
  }
12325
12407
  outputJson({
12326
12408
  key,
@@ -12334,6 +12416,13 @@ function registerKvCommand(program2) {
12334
12416
  }
12335
12417
 
12336
12418
  // src/commands/space.ts
12419
+ import { randomBytes as randomBytes3 } from "crypto";
12420
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
12421
+ import { dirname as dirname3 } from "path";
12422
+ function didWithoutFragment2(did) {
12423
+ const fragment = did.indexOf("#");
12424
+ return fragment === -1 ? did : did.slice(0, fragment);
12425
+ }
12337
12426
  function registerSpaceCommand(program2) {
12338
12427
  const space = program2.command("space").description("Space management");
12339
12428
  space.command("list").description("List spaces").action(async (_options, cmd) => {
@@ -12374,6 +12463,53 @@ function registerSpaceCommand(program2) {
12374
12463
  handleError(error);
12375
12464
  }
12376
12465
  });
12466
+ space.command("host-request <name>").description("Emit a request asking the space owner to host it (delegate-only)").option("--emit [file]", "Write the request artifact to file (or stdout when no path)").addHelpText("after", `
12467
+
12468
+ A delegate cannot host a space \u2014 only its owner (root authority) can. This
12469
+ emits a tinycloud.host.request artifact naming the space and its owner so you
12470
+ can hand it to the owner; they run \`tc space host <name>\` and confirm.
12471
+
12472
+ If you ARE the owner of the resolved space, this refuses and tells you to host
12473
+ it directly with \`tc space host <name>\` (no request needed).
12474
+ `).action(async (name, options, cmd) => {
12475
+ try {
12476
+ const globalOpts = cmd.optsWithGlobals();
12477
+ const ctx = await ProfileManager.resolveContext(globalOpts);
12478
+ const profile = await ProfileManager.getProfile(ctx.profile);
12479
+ const spaceId = await resolveHostSpace(name, ctx.profile);
12480
+ const spaceName = spaceNameFromUri(spaceId);
12481
+ if (await isRootAuthority(spaceId, ctx.profile)) {
12482
+ throw new CLIError(
12483
+ "ALREADY_ROOT_AUTHORITY",
12484
+ `You are the owner of ${spaceId}. Host it directly: tc space host ${spaceName}`,
12485
+ ExitCode.USAGE_ERROR
12486
+ );
12487
+ }
12488
+ const requesterDid = didWithoutFragment2(profile.sessionDid ?? profile.did);
12489
+ const ownerDid = ownerDidFromSpaceUri(spaceId);
12490
+ if (!ownerDid) {
12491
+ throw new CLIError(
12492
+ "UNRESOLVABLE_OWNER",
12493
+ `Cannot determine the owner of ${spaceId}; host-request needs a pkh space URI.`,
12494
+ ExitCode.USAGE_ERROR
12495
+ );
12496
+ }
12497
+ const artifact = {
12498
+ kind: "tinycloud.host.request",
12499
+ version: 1,
12500
+ requestId: `hostreq_${Date.now().toString(36)}_${randomBytes3(4).toString("hex")}`,
12501
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
12502
+ spaceName,
12503
+ spaceId,
12504
+ ownerDid,
12505
+ requesterDid,
12506
+ host: ctx.host
12507
+ };
12508
+ await emitHostRequestArtifact(artifact, options.emit);
12509
+ } catch (error) {
12510
+ handleError(error);
12511
+ }
12512
+ });
12377
12513
  space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
12378
12514
  try {
12379
12515
  const globalOpts = cmd.optsWithGlobals();
@@ -12406,6 +12542,22 @@ function registerSpaceCommand(program2) {
12406
12542
  }
12407
12543
  });
12408
12544
  }
12545
+ async function emitHostRequestArtifact(artifact, emitOption) {
12546
+ if (typeof emitOption === "string" && emitOption.length > 0) {
12547
+ await mkdir3(dirname3(emitOption), { recursive: true });
12548
+ await writeFile4(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
12549
+ outputJson({
12550
+ emitted: true,
12551
+ path: emitOption,
12552
+ requestId: artifact.requestId,
12553
+ spaceName: artifact.spaceName,
12554
+ spaceId: artifact.spaceId,
12555
+ ownerDid: artifact.ownerDid
12556
+ });
12557
+ return;
12558
+ }
12559
+ outputJson(artifact);
12560
+ }
12409
12561
 
12410
12562
  // src/lib/duration.ts
12411
12563
  function parseDuration(input) {
@@ -13053,7 +13205,7 @@ complete -c tc -l quiet -s q -d "Suppress non-essential output"
13053
13205
 
13054
13206
  // src/commands/vault.ts
13055
13207
  import { readFile as readFile5 } from "fs/promises";
13056
- import { writeFile as writeFile4 } from "fs/promises";
13208
+ import { writeFile as writeFile5 } from "fs/promises";
13057
13209
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
13058
13210
  async function readStdin3() {
13059
13211
  const chunks = [];
@@ -13142,7 +13294,7 @@ function registerVaultCommand(program2) {
13142
13294
  const data = result.data.data ?? result.data;
13143
13295
  if (options.output) {
13144
13296
  const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
13145
- await writeFile4(options.output, content);
13297
+ await writeFile5(options.output, content);
13146
13298
  outputJson({ key, written: options.output });
13147
13299
  return;
13148
13300
  }
@@ -13226,7 +13378,7 @@ function registerVaultCommand(program2) {
13226
13378
 
13227
13379
  // src/commands/secrets.ts
13228
13380
  import { readFile as readFile6 } from "fs/promises";
13229
- import { writeFile as writeFile5 } from "fs/promises";
13381
+ import { writeFile as writeFile6 } from "fs/promises";
13230
13382
  import { join as join5 } from "path";
13231
13383
  import { homedir as homedir2 } from "os";
13232
13384
  var SECRETS_SPACE2 = "secrets";
@@ -13817,7 +13969,7 @@ function registerSecretsCommand(program2) {
13817
13969
  })
13818
13970
  );
13819
13971
  if (options.output) {
13820
- await writeFile5(options.output, value2);
13972
+ await writeFile6(options.output, value2);
13821
13973
  outputJson({ name, written: options.output });
13822
13974
  return;
13823
13975
  }
@@ -13846,7 +13998,7 @@ function registerSecretsCommand(program2) {
13846
13998
  }
13847
13999
  const value = String(result.data);
13848
14000
  if (options.output) {
13849
- await writeFile5(options.output, value);
14001
+ await writeFile6(options.output, value);
13850
14002
  outputJson({ name, written: options.output });
13851
14003
  return;
13852
14004
  }
@@ -13964,7 +14116,7 @@ function registerSecretsCommand(program2) {
13964
14116
 
13965
14117
  // src/commands/vars.ts
13966
14118
  import { readFile as readFile7 } from "fs/promises";
13967
- import { writeFile as writeFile6 } from "fs/promises";
14119
+ import { writeFile as writeFile7 } from "fs/promises";
13968
14120
  var VARIABLES_PREFIX = "variables/";
13969
14121
  async function readStdin5() {
13970
14122
  const chunks = [];
@@ -14036,7 +14188,7 @@ function registerVarsCommand(program2) {
14036
14188
  value = typeof data === "string" ? data : JSON.stringify(data);
14037
14189
  }
14038
14190
  if (options.output) {
14039
- await writeFile6(options.output, value);
14191
+ await writeFile7(options.output, value);
14040
14192
  outputJson({ name, written: options.output });
14041
14193
  return;
14042
14194
  }
@@ -14210,12 +14362,18 @@ function registerDoctorCommand(program2) {
14210
14362
  }
14211
14363
 
14212
14364
  // src/commands/sql.ts
14213
- import { writeFile as writeFile7 } from "fs/promises";
14365
+ import { writeFile as writeFile8 } from "fs/promises";
14214
14366
  import { resolve } from "path";
14215
14367
  async function dbHandle(node, dbName, spaceInput, profileName) {
14216
14368
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
14217
14369
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
14218
- return sql.db(dbName);
14370
+ return { handle: sql.db(dbName), spaceUri };
14371
+ }
14372
+ async function throwSqlError(error, spaceUri, profileName, prefix) {
14373
+ const hosted = await unhostedSpaceError(error, spaceUri, profileName);
14374
+ if (hosted) throw hosted;
14375
+ const message = prefix ? `${prefix}${error.message}` : error.message;
14376
+ throw new CLIError(error.code, message, ExitCode.ERROR, error.meta);
14219
14377
  }
14220
14378
  function registerSqlCommand(program2) {
14221
14379
  const sql = program2.command("sql").description("SQLite database operations for your TinyCloud space").addHelpText("after", `
@@ -14261,13 +14419,13 @@ Output:
14261
14419
  const ctx = await ProfileManager.resolveContext(globalOpts);
14262
14420
  const node = await ensureAuthenticated(ctx);
14263
14421
  const params = options.params ? JSON.parse(options.params) : void 0;
14264
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14422
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14265
14423
  const result = await withSpinner(
14266
14424
  "Running query...",
14267
14425
  () => handle.query(sqlStr, params)
14268
14426
  );
14269
14427
  if (!result.ok) {
14270
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14428
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14271
14429
  }
14272
14430
  const { columns, rows, rowCount } = result.data;
14273
14431
  if (shouldOutputJson()) {
@@ -14305,13 +14463,13 @@ Output:
14305
14463
  const ctx = await ProfileManager.resolveContext(globalOpts);
14306
14464
  const node = await ensureAuthenticated(ctx);
14307
14465
  const params = options.params ? JSON.parse(options.params) : void 0;
14308
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14466
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14309
14467
  const result = await withSpinner(
14310
14468
  "Executing statement...",
14311
14469
  () => handle.execute(sqlStr, params)
14312
14470
  );
14313
14471
  if (!result.ok) {
14314
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14472
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14315
14473
  }
14316
14474
  outputJson({
14317
14475
  changes: result.data.changes,
@@ -14335,18 +14493,18 @@ Output:
14335
14493
  const globalOpts = cmd.optsWithGlobals();
14336
14494
  const ctx = await ProfileManager.resolveContext(globalOpts);
14337
14495
  const node = await ensureAuthenticated(ctx);
14338
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14496
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14339
14497
  const result = await withSpinner(
14340
14498
  "Exporting database...",
14341
14499
  () => handle.export()
14342
14500
  );
14343
14501
  if (!result.ok) {
14344
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14502
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14345
14503
  }
14346
14504
  const blob = result.data;
14347
14505
  const buffer = Buffer.from(await blob.arrayBuffer());
14348
14506
  const outputPath = resolve(options.output);
14349
- await writeFile7(outputPath, buffer);
14507
+ await writeFile8(outputPath, buffer);
14350
14508
  outputJson({
14351
14509
  file: outputPath,
14352
14510
  size: blob.size,
@@ -14390,8 +14548,8 @@ Notes:
14390
14548
  ExitCode.USAGE_ERROR
14391
14549
  );
14392
14550
  }
14393
- const fromHandle = await dbHandle(node, options.fromDb, fromSpaceInput, ctx.profile);
14394
- const toHandle = await dbHandle(node, options.toDb, toSpaceInput, ctx.profile);
14551
+ const { handle: fromHandle, spaceUri: fromSpaceUriResolved } = await dbHandle(node, options.fromDb, fromSpaceInput, ctx.profile);
14552
+ const { handle: toHandle, spaceUri: toSpaceUriResolved } = await dbHandle(node, options.toDb, toSpaceInput, ctx.profile);
14395
14553
  let tables;
14396
14554
  if (options.table && options.table.length > 0) {
14397
14555
  tables = options.table.flatMap((t) => t.split(",").map((s) => s.trim()).filter(Boolean));
@@ -14400,7 +14558,7 @@ Notes:
14400
14558
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
14401
14559
  );
14402
14560
  if (!listing.ok) {
14403
- throw new CLIError(listing.error.code, `Cannot list source tables: ${listing.error.message}`, ExitCode.ERROR, listing.error.meta);
14561
+ await throwSqlError(listing.error, fromSpaceUriResolved, ctx.profile, "Cannot list source tables: ");
14404
14562
  }
14405
14563
  tables = listing.data.rows.map((r) => String(r[0]));
14406
14564
  }
@@ -14416,12 +14574,7 @@ Notes:
14416
14574
  const safe = quoteIdent(table);
14417
14575
  const countResult = await fromHandle.query(`SELECT count(*) AS n FROM ${safe}`);
14418
14576
  if (!countResult.ok) {
14419
- throw new CLIError(
14420
- countResult.error.code,
14421
- `Cannot count rows in source table "${table}": ${countResult.error.message}`,
14422
- ExitCode.ERROR,
14423
- countResult.error.meta
14424
- );
14577
+ await throwSqlError(countResult.error, fromSpaceUriResolved, ctx.profile, `Cannot count rows in source table "${table}": `);
14425
14578
  }
14426
14579
  const rows = Number(countResult.data.rows[0]?.[0] ?? 0);
14427
14580
  plan.push({ table, rows, copied: 0, skipped: 0 });
@@ -14439,7 +14592,7 @@ Notes:
14439
14592
  const safe = quoteIdent(entry.table);
14440
14593
  const fetched = await fromHandle.query(`SELECT * FROM ${safe}`);
14441
14594
  if (!fetched.ok) {
14442
- throw new CLIError(fetched.error.code, `Failed to read "${entry.table}": ${fetched.error.message}`, ExitCode.ERROR, fetched.error.meta);
14595
+ await throwSqlError(fetched.error, fromSpaceUriResolved, ctx.profile, `Failed to read "${entry.table}": `);
14443
14596
  }
14444
14597
  const columns = fetched.data.columns;
14445
14598
  const rows = fetched.data.rows;
@@ -14450,12 +14603,7 @@ Notes:
14450
14603
  for (const row of rows) {
14451
14604
  const writeResult = await toHandle.execute(insertSql, row);
14452
14605
  if (!writeResult.ok) {
14453
- throw new CLIError(
14454
- writeResult.error.code,
14455
- `Insert into "${entry.table}" failed after ${entry.copied} row(s): ${writeResult.error.message}`,
14456
- ExitCode.ERROR,
14457
- writeResult.error.meta
14458
- );
14606
+ await throwSqlError(writeResult.error, toSpaceUriResolved, ctx.profile, `Insert into "${entry.table}" failed after ${entry.copied} row(s): `);
14459
14607
  }
14460
14608
  entry.copied += writeResult.data.changes ?? 1;
14461
14609
  }
@@ -14475,7 +14623,7 @@ function quoteIdent(name) {
14475
14623
  }
14476
14624
 
14477
14625
  // src/commands/duckdb.ts
14478
- import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
14626
+ import { readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
14479
14627
  import { resolve as resolve2 } from "path";
14480
14628
  function registerDuckdbCommand(program2) {
14481
14629
  const duckdb = program2.command("duckdb").description("DuckDB database operations");
@@ -14589,7 +14737,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
14589
14737
  const blob = result.data;
14590
14738
  const buffer = Buffer.from(await blob.arrayBuffer());
14591
14739
  const outputPath = resolve2(options.output);
14592
- await writeFile8(outputPath, buffer);
14740
+ await writeFile9(outputPath, buffer);
14593
14741
  outputJson({
14594
14742
  file: outputPath,
14595
14743
  size: blob.size,