@tinycloud/cli 0.7.0-beta.3 → 0.7.0-beta.5

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
  }
@@ -12178,6 +12173,79 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
12178
12173
  // src/commands/kv.ts
12179
12174
  import { readFile as readFile4 } from "fs/promises";
12180
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
+ }
12181
12249
  async function readStdin2() {
12182
12250
  const chunks = [];
12183
12251
  for await (const chunk of process.stdin) {
@@ -12187,7 +12255,8 @@ async function readStdin2() {
12187
12255
  }
12188
12256
  async function kvHandle(node, spaceInput, profileName) {
12189
12257
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
12190
- return spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12258
+ const kv = spaceUri ? node.kvForSpace(spaceUri) : node.kv;
12259
+ return { kv, spaceUri };
12191
12260
  }
12192
12261
  function registerKvCommand(program2) {
12193
12262
  const kv = program2.command("kv").description("Key-value store operations");
@@ -12196,17 +12265,19 @@ function registerKvCommand(program2) {
12196
12265
  const globalOpts = cmd.optsWithGlobals();
12197
12266
  const ctx = await ProfileManager.resolveContext(globalOpts);
12198
12267
  const node = await ensureAuthenticated(ctx);
12199
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12268
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12200
12269
  const wantBytes = !!options.output || !!options.raw;
12201
12270
  const result = await withSpinner(
12202
12271
  `Getting ${key}...`,
12203
12272
  () => kv2.get(key, wantBytes ? { binary: true } : void 0)
12204
12273
  );
12205
12274
  if (!result.ok) {
12275
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12276
+ if (hosted) throw hosted;
12206
12277
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12207
12278
  throw new CLIError("NOT_FOUND", `Key "${key}" not found`, ExitCode.NOT_FOUND);
12208
12279
  }
12209
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12280
+ await throwKvError(result.error, spaceUri, ctx.profile);
12210
12281
  }
12211
12282
  const data = result.data.data;
12212
12283
  const metadata = result.data.headers ?? {};
@@ -12257,10 +12328,10 @@ function registerKvCommand(program2) {
12257
12328
  putValue = value;
12258
12329
  }
12259
12330
  }
12260
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12331
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12261
12332
  const result = await withSpinner(`Writing ${key}...`, () => kv2.put(key, putValue));
12262
12333
  if (!result.ok) {
12263
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12334
+ await throwKvError(result.error, spaceUri, ctx.profile);
12264
12335
  }
12265
12336
  outputJson({ key, written: true });
12266
12337
  } catch (error) {
@@ -12272,10 +12343,10 @@ function registerKvCommand(program2) {
12272
12343
  const globalOpts = cmd.optsWithGlobals();
12273
12344
  const ctx = await ProfileManager.resolveContext(globalOpts);
12274
12345
  const node = await ensureAuthenticated(ctx);
12275
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12346
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12276
12347
  const result = await withSpinner(`Deleting ${key}...`, () => kv2.delete(key));
12277
12348
  if (!result.ok) {
12278
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12349
+ await throwKvError(result.error, spaceUri, ctx.profile);
12279
12350
  }
12280
12351
  outputJson({ key, deleted: true });
12281
12352
  } catch (error) {
@@ -12287,11 +12358,11 @@ function registerKvCommand(program2) {
12287
12358
  const globalOpts = cmd.optsWithGlobals();
12288
12359
  const ctx = await ProfileManager.resolveContext(globalOpts);
12289
12360
  const node = await ensureAuthenticated(ctx);
12290
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12361
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12291
12362
  const listOptions = options.prefix ? { prefix: options.prefix } : void 0;
12292
12363
  const result = await withSpinner("Listing keys...", () => kv2.list(listOptions));
12293
12364
  if (!result.ok) {
12294
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12365
+ await throwKvError(result.error, spaceUri, ctx.profile);
12295
12366
  }
12296
12367
  const rawData = result.data.data ?? result.data;
12297
12368
  const keyList = Array.isArray(rawData) ? rawData : rawData?.keys ?? [];
@@ -12322,14 +12393,16 @@ function registerKvCommand(program2) {
12322
12393
  const globalOpts = cmd.optsWithGlobals();
12323
12394
  const ctx = await ProfileManager.resolveContext(globalOpts);
12324
12395
  const node = await ensureAuthenticated(ctx);
12325
- const kv2 = await kvHandle(node, options.space, ctx.profile);
12396
+ const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
12326
12397
  const result = await withSpinner(`Checking ${key}...`, () => kv2.head(key));
12327
12398
  if (!result.ok) {
12399
+ const hosted = await unhostedSpaceError(result.error, spaceUri, ctx.profile);
12400
+ if (hosted) throw hosted;
12328
12401
  if (result.error.code === "KV_NOT_FOUND" || result.error.code === "NOT_FOUND") {
12329
12402
  outputJson({ key, exists: false, metadata: {} });
12330
12403
  return;
12331
12404
  }
12332
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
12405
+ await throwKvError(result.error, spaceUri, ctx.profile);
12333
12406
  }
12334
12407
  outputJson({
12335
12408
  key,
@@ -12343,6 +12416,13 @@ function registerKvCommand(program2) {
12343
12416
  }
12344
12417
 
12345
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
+ }
12346
12426
  function registerSpaceCommand(program2) {
12347
12427
  const space = program2.command("space").description("Space management");
12348
12428
  space.command("list").description("List spaces").action(async (_options, cmd) => {
@@ -12383,6 +12463,53 @@ function registerSpaceCommand(program2) {
12383
12463
  handleError(error);
12384
12464
  }
12385
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
+ });
12386
12513
  space.command("info [space-id]").description("Get space info").action(async (spaceId, _options, cmd) => {
12387
12514
  try {
12388
12515
  const globalOpts = cmd.optsWithGlobals();
@@ -12415,6 +12542,22 @@ function registerSpaceCommand(program2) {
12415
12542
  }
12416
12543
  });
12417
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
+ }
12418
12561
 
12419
12562
  // src/lib/duration.ts
12420
12563
  function parseDuration(input) {
@@ -13062,7 +13205,7 @@ complete -c tc -l quiet -s q -d "Suppress non-essential output"
13062
13205
 
13063
13206
  // src/commands/vault.ts
13064
13207
  import { readFile as readFile5 } from "fs/promises";
13065
- import { writeFile as writeFile4 } from "fs/promises";
13208
+ import { writeFile as writeFile5 } from "fs/promises";
13066
13209
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
13067
13210
  async function readStdin3() {
13068
13211
  const chunks = [];
@@ -13151,7 +13294,7 @@ function registerVaultCommand(program2) {
13151
13294
  const data = result.data.data ?? result.data;
13152
13295
  if (options.output) {
13153
13296
  const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
13154
- await writeFile4(options.output, content);
13297
+ await writeFile5(options.output, content);
13155
13298
  outputJson({ key, written: options.output });
13156
13299
  return;
13157
13300
  }
@@ -13235,7 +13378,7 @@ function registerVaultCommand(program2) {
13235
13378
 
13236
13379
  // src/commands/secrets.ts
13237
13380
  import { readFile as readFile6 } from "fs/promises";
13238
- import { writeFile as writeFile5 } from "fs/promises";
13381
+ import { writeFile as writeFile6 } from "fs/promises";
13239
13382
  import { join as join5 } from "path";
13240
13383
  import { homedir as homedir2 } from "os";
13241
13384
  var SECRETS_SPACE2 = "secrets";
@@ -13826,7 +13969,7 @@ function registerSecretsCommand(program2) {
13826
13969
  })
13827
13970
  );
13828
13971
  if (options.output) {
13829
- await writeFile5(options.output, value2);
13972
+ await writeFile6(options.output, value2);
13830
13973
  outputJson({ name, written: options.output });
13831
13974
  return;
13832
13975
  }
@@ -13855,7 +13998,7 @@ function registerSecretsCommand(program2) {
13855
13998
  }
13856
13999
  const value = String(result.data);
13857
14000
  if (options.output) {
13858
- await writeFile5(options.output, value);
14001
+ await writeFile6(options.output, value);
13859
14002
  outputJson({ name, written: options.output });
13860
14003
  return;
13861
14004
  }
@@ -13973,7 +14116,7 @@ function registerSecretsCommand(program2) {
13973
14116
 
13974
14117
  // src/commands/vars.ts
13975
14118
  import { readFile as readFile7 } from "fs/promises";
13976
- import { writeFile as writeFile6 } from "fs/promises";
14119
+ import { writeFile as writeFile7 } from "fs/promises";
13977
14120
  var VARIABLES_PREFIX = "variables/";
13978
14121
  async function readStdin5() {
13979
14122
  const chunks = [];
@@ -14045,7 +14188,7 @@ function registerVarsCommand(program2) {
14045
14188
  value = typeof data === "string" ? data : JSON.stringify(data);
14046
14189
  }
14047
14190
  if (options.output) {
14048
- await writeFile6(options.output, value);
14191
+ await writeFile7(options.output, value);
14049
14192
  outputJson({ name, written: options.output });
14050
14193
  return;
14051
14194
  }
@@ -14219,12 +14362,18 @@ function registerDoctorCommand(program2) {
14219
14362
  }
14220
14363
 
14221
14364
  // src/commands/sql.ts
14222
- import { writeFile as writeFile7 } from "fs/promises";
14365
+ import { writeFile as writeFile8 } from "fs/promises";
14223
14366
  import { resolve } from "path";
14224
14367
  async function dbHandle(node, dbName, spaceInput, profileName) {
14225
14368
  const spaceUri = await resolveSpaceUri(spaceInput, profileName);
14226
14369
  const sql = spaceUri ? node.sqlForSpace(spaceUri) : node.sql;
14227
- 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);
14228
14377
  }
14229
14378
  function registerSqlCommand(program2) {
14230
14379
  const sql = program2.command("sql").description("SQLite database operations for your TinyCloud space").addHelpText("after", `
@@ -14270,13 +14419,13 @@ Output:
14270
14419
  const ctx = await ProfileManager.resolveContext(globalOpts);
14271
14420
  const node = await ensureAuthenticated(ctx);
14272
14421
  const params = options.params ? JSON.parse(options.params) : void 0;
14273
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14422
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14274
14423
  const result = await withSpinner(
14275
14424
  "Running query...",
14276
14425
  () => handle.query(sqlStr, params)
14277
14426
  );
14278
14427
  if (!result.ok) {
14279
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14428
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14280
14429
  }
14281
14430
  const { columns, rows, rowCount } = result.data;
14282
14431
  if (shouldOutputJson()) {
@@ -14314,13 +14463,13 @@ Output:
14314
14463
  const ctx = await ProfileManager.resolveContext(globalOpts);
14315
14464
  const node = await ensureAuthenticated(ctx);
14316
14465
  const params = options.params ? JSON.parse(options.params) : void 0;
14317
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14466
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14318
14467
  const result = await withSpinner(
14319
14468
  "Executing statement...",
14320
14469
  () => handle.execute(sqlStr, params)
14321
14470
  );
14322
14471
  if (!result.ok) {
14323
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14472
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14324
14473
  }
14325
14474
  outputJson({
14326
14475
  changes: result.data.changes,
@@ -14344,18 +14493,18 @@ Output:
14344
14493
  const globalOpts = cmd.optsWithGlobals();
14345
14494
  const ctx = await ProfileManager.resolveContext(globalOpts);
14346
14495
  const node = await ensureAuthenticated(ctx);
14347
- const handle = await dbHandle(node, options.db, options.space, ctx.profile);
14496
+ const { handle, spaceUri } = await dbHandle(node, options.db, options.space, ctx.profile);
14348
14497
  const result = await withSpinner(
14349
14498
  "Exporting database...",
14350
14499
  () => handle.export()
14351
14500
  );
14352
14501
  if (!result.ok) {
14353
- throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR, result.error.meta);
14502
+ await throwSqlError(result.error, spaceUri, ctx.profile);
14354
14503
  }
14355
14504
  const blob = result.data;
14356
14505
  const buffer = Buffer.from(await blob.arrayBuffer());
14357
14506
  const outputPath = resolve(options.output);
14358
- await writeFile7(outputPath, buffer);
14507
+ await writeFile8(outputPath, buffer);
14359
14508
  outputJson({
14360
14509
  file: outputPath,
14361
14510
  size: blob.size,
@@ -14399,8 +14548,8 @@ Notes:
14399
14548
  ExitCode.USAGE_ERROR
14400
14549
  );
14401
14550
  }
14402
- const fromHandle = await dbHandle(node, options.fromDb, fromSpaceInput, ctx.profile);
14403
- 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);
14404
14553
  let tables;
14405
14554
  if (options.table && options.table.length > 0) {
14406
14555
  tables = options.table.flatMap((t) => t.split(",").map((s) => s.trim()).filter(Boolean));
@@ -14409,7 +14558,7 @@ Notes:
14409
14558
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
14410
14559
  );
14411
14560
  if (!listing.ok) {
14412
- 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: ");
14413
14562
  }
14414
14563
  tables = listing.data.rows.map((r) => String(r[0]));
14415
14564
  }
@@ -14425,12 +14574,7 @@ Notes:
14425
14574
  const safe = quoteIdent(table);
14426
14575
  const countResult = await fromHandle.query(`SELECT count(*) AS n FROM ${safe}`);
14427
14576
  if (!countResult.ok) {
14428
- throw new CLIError(
14429
- countResult.error.code,
14430
- `Cannot count rows in source table "${table}": ${countResult.error.message}`,
14431
- ExitCode.ERROR,
14432
- countResult.error.meta
14433
- );
14577
+ await throwSqlError(countResult.error, fromSpaceUriResolved, ctx.profile, `Cannot count rows in source table "${table}": `);
14434
14578
  }
14435
14579
  const rows = Number(countResult.data.rows[0]?.[0] ?? 0);
14436
14580
  plan.push({ table, rows, copied: 0, skipped: 0 });
@@ -14448,7 +14592,7 @@ Notes:
14448
14592
  const safe = quoteIdent(entry.table);
14449
14593
  const fetched = await fromHandle.query(`SELECT * FROM ${safe}`);
14450
14594
  if (!fetched.ok) {
14451
- 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}": `);
14452
14596
  }
14453
14597
  const columns = fetched.data.columns;
14454
14598
  const rows = fetched.data.rows;
@@ -14459,12 +14603,7 @@ Notes:
14459
14603
  for (const row of rows) {
14460
14604
  const writeResult = await toHandle.execute(insertSql, row);
14461
14605
  if (!writeResult.ok) {
14462
- throw new CLIError(
14463
- writeResult.error.code,
14464
- `Insert into "${entry.table}" failed after ${entry.copied} row(s): ${writeResult.error.message}`,
14465
- ExitCode.ERROR,
14466
- writeResult.error.meta
14467
- );
14606
+ await throwSqlError(writeResult.error, toSpaceUriResolved, ctx.profile, `Insert into "${entry.table}" failed after ${entry.copied} row(s): `);
14468
14607
  }
14469
14608
  entry.copied += writeResult.data.changes ?? 1;
14470
14609
  }
@@ -14484,7 +14623,7 @@ function quoteIdent(name) {
14484
14623
  }
14485
14624
 
14486
14625
  // src/commands/duckdb.ts
14487
- import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
14626
+ import { readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
14488
14627
  import { resolve as resolve2 } from "path";
14489
14628
  function registerDuckdbCommand(program2) {
14490
14629
  const duckdb = program2.command("duckdb").description("DuckDB database operations");
@@ -14598,7 +14737,7 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
14598
14737
  const blob = result.data;
14599
14738
  const buffer = Buffer.from(await blob.arrayBuffer());
14600
14739
  const outputPath = resolve2(options.output);
14601
- await writeFile8(outputPath, buffer);
14740
+ await writeFile9(outputPath, buffer);
14602
14741
  outputJson({
14603
14742
  file: outputPath,
14604
14743
  size: blob.size,