@tinycloud/cli 0.6.0-beta.1 → 0.6.0-beta.10

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
@@ -549,7 +549,7 @@ var ProfileManager = class _ProfileManager {
549
549
 
550
550
  // src/auth/local-key.ts
551
551
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
552
- import { PrivateKeySigner } from "@tinycloud/node-sdk";
552
+ import { PrivateKeySigner, pkhDid } from "@tinycloud/node-sdk";
553
553
  import { randomBytes } from "crypto";
554
554
  var wasmInitialized = false;
555
555
  function ensureWasm() {
@@ -583,7 +583,7 @@ async function deriveAddress(privateKey) {
583
583
  return signer.getAddress();
584
584
  }
585
585
  function addressToDID(address, chainId = 1) {
586
- return `did:pkh:eip155:${chainId}:${address}`;
586
+ return pkhDid(address, chainId);
587
587
  }
588
588
  async function generateLocalIdentity(chainId = 1) {
589
589
  const privateKey = generateEthereumPrivateKey();
@@ -620,6 +620,25 @@ async function localKeySignIn(options) {
620
620
  // src/auth/browser-auth.ts
621
621
  import { createServer } from "http";
622
622
  import { createInterface } from "readline";
623
+ var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set([
624
+ "d",
625
+ "p",
626
+ "q",
627
+ "dp",
628
+ "dq",
629
+ "qi",
630
+ "oth",
631
+ "k"
632
+ ]);
633
+ function publicJwkForDelegation(jwk) {
634
+ const publicJwk = {};
635
+ for (const [key, value] of Object.entries(jwk)) {
636
+ if (!PRIVATE_JWK_FIELDS.has(key)) {
637
+ publicJwk[key] = value;
638
+ }
639
+ }
640
+ return publicJwk;
641
+ }
623
642
  async function startAuthFlow(did, options = {}) {
624
643
  if (options.paste) {
625
644
  return pasteFlow(did, options);
@@ -641,7 +660,9 @@ function buildAuthUrl(did, options = {}) {
641
660
  params.set("callback", options.callback);
642
661
  }
643
662
  if (options.jwk) {
644
- const jwkB64 = Buffer.from(JSON.stringify(options.jwk)).toString("base64url");
663
+ const jwkB64 = Buffer.from(
664
+ JSON.stringify(publicJwkForDelegation(options.jwk))
665
+ ).toString("base64url");
645
666
  params.set("jwk", jwkB64);
646
667
  }
647
668
  if (options.host) {
@@ -843,7 +864,7 @@ function registerInitCommand(program2) {
843
864
  await ProfileManager.setProfile(profileName, {
844
865
  ...profileConfig,
845
866
  spaceId: delegationData.spaceId,
846
- primaryDid: delegationData.primaryDid
867
+ ownerDid: delegationData.ownerDid
847
868
  });
848
869
  outputJson({
849
870
  profile: profileName,
@@ -903,13 +924,22 @@ import {
903
924
  } from "@tinycloud/node-sdk";
904
925
 
905
926
  // src/lib/space.ts
927
+ import {
928
+ buildSpaceUri,
929
+ canonicalizeAddress,
930
+ makePkhSpaceId,
931
+ parsePkhDid,
932
+ parseSpaceUri
933
+ } from "@tinycloud/node-sdk";
906
934
  function resolveAddress(profile, session) {
907
935
  const sessAddr = session?.address;
908
- if (typeof sessAddr === "string" && sessAddr.length > 0) return sessAddr;
909
- if (profile.address) return profile.address;
910
- if (profile.primaryDid) {
911
- const match = profile.primaryDid.match(/^did:pkh:eip155:\d+:(0x[a-fA-F0-9]{40})$/);
912
- if (match) return match[1];
936
+ if (typeof sessAddr === "string" && sessAddr.length > 0) {
937
+ return canonicalizeAddress(sessAddr);
938
+ }
939
+ if (profile.address) return canonicalizeAddress(profile.address);
940
+ if (profile.ownerDid) {
941
+ const pkh = parsePkhDid(profile.ownerDid);
942
+ if (pkh) return pkh.address;
913
943
  }
914
944
  throw new CLIError(
915
945
  "ADDRESS_UNKNOWN",
@@ -924,7 +954,17 @@ function resolveChainId(profile, session) {
924
954
  }
925
955
  async function resolveSpaceUri(input, profileName) {
926
956
  if (!input) return void 0;
927
- if (input.startsWith("tinycloud:")) return input;
957
+ if (input.startsWith("tinycloud:")) {
958
+ const parsed = parseSpaceUri(input);
959
+ if (!parsed) {
960
+ throw new CLIError(
961
+ "INVALID_SPACE",
962
+ `Invalid --space "${input}". Use a short name ([A-Za-z0-9_-]) or a full tinycloud:... URI.`,
963
+ ExitCode.USAGE_ERROR
964
+ );
965
+ }
966
+ return buildSpaceUri(parsed.owner, parsed.name);
967
+ }
928
968
  if (!/^[A-Za-z0-9_-]+$/.test(input)) {
929
969
  throw new CLIError(
930
970
  "INVALID_SPACE",
@@ -936,7 +976,7 @@ async function resolveSpaceUri(input, profileName) {
936
976
  const session = await ProfileManager.getSession(profileName);
937
977
  const address = resolveAddress(profile, session);
938
978
  const chainId = resolveChainId(profile, session);
939
- return `tinycloud:pkh:eip155:${chainId}:${address}:${input}`;
979
+ return makePkhSpaceId(address, chainId, input);
940
980
  }
941
981
 
942
982
  // src/lib/permissions.ts
@@ -959,8 +999,8 @@ function createPermissionRequestArtifact(params) {
959
999
  posture: resolveProfilePosture(params.profile),
960
1000
  operatorType: resolveProfileOperatorType(params.profile),
961
1001
  host: params.host,
962
- did: didWithoutFragment(params.profile.sessionDid ?? params.profile.did),
963
- primaryDid: params.profile.primaryDid,
1002
+ sessionDid: didWithoutFragment(params.profile.sessionDid ?? params.profile.did),
1003
+ ownerDid: params.profile.ownerDid,
964
1004
  spaceId: params.profile.spaceId,
965
1005
  requestedExpiry: params.requestedExpiry,
966
1006
  requested: params.requested,
@@ -1362,6 +1402,15 @@ function registerAuthCommand(program2) {
1362
1402
  handleError(error);
1363
1403
  }
1364
1404
  });
1405
+ auth.command("rotate").description("Rotate the active profile session key").option("--paste", "Use manual paste mode instead of browser callback").action(async (options, cmd) => {
1406
+ try {
1407
+ const globalOpts = cmd.optsWithGlobals();
1408
+ const ctx = await ProfileManager.resolveContext(globalOpts);
1409
+ await rotateAuthKey(ctx.profile, ctx.host, { paste: options.paste });
1410
+ } catch (error) {
1411
+ handleError(error);
1412
+ }
1413
+ });
1365
1414
  auth.command("status").description("Show current authentication state").action(async (_options, cmd) => {
1366
1415
  try {
1367
1416
  const globalOpts = cmd.optsWithGlobals();
@@ -1382,7 +1431,7 @@ function registerAuthCommand(program2) {
1382
1431
  authenticated,
1383
1432
  did: profile?.did ?? null,
1384
1433
  sessionDid: profile?.sessionDid ?? null,
1385
- primaryDid: profile?.primaryDid ?? null,
1434
+ ownerDid: profile?.ownerDid ?? null,
1386
1435
  spaceId: profile?.spaceId ?? null,
1387
1436
  host: ctx.host,
1388
1437
  profile: ctx.profile,
@@ -1402,7 +1451,7 @@ function registerAuthCommand(program2) {
1402
1451
  process.stdout.write(formatField("Host", ctx.host) + "\n");
1403
1452
  process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
1404
1453
  process.stdout.write(formatField("Session DID", profile?.sessionDid ?? null) + "\n");
1405
- process.stdout.write(formatField("Primary DID", profile?.primaryDid ?? null) + "\n");
1454
+ process.stdout.write(formatField("Owner DID", profile?.ownerDid ?? null) + "\n");
1406
1455
  process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
1407
1456
  process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
1408
1457
  process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
@@ -1603,7 +1652,7 @@ function registerAuthCommand(program2) {
1603
1652
  yes: options.yes === true
1604
1653
  });
1605
1654
  const result = await node.delegateTo(
1606
- parsed.did,
1655
+ parsed.sessionDid,
1607
1656
  parsed.requested,
1608
1657
  parsed.requestedExpiry !== void 0 ? { expiry: parsed.requestedExpiry } : void 0
1609
1658
  );
@@ -1735,7 +1784,7 @@ function registerAuthCommand(program2) {
1735
1784
  profile: ctx.profile,
1736
1785
  did: profile.did,
1737
1786
  sessionDid: profile.sessionDid ?? null,
1738
- primaryDid: profile.primaryDid ?? null,
1787
+ ownerDid: profile.ownerDid ?? null,
1739
1788
  spaceId: profile.spaceId ?? null,
1740
1789
  host: profile.host,
1741
1790
  authenticated,
@@ -1749,7 +1798,7 @@ function registerAuthCommand(program2) {
1749
1798
  process.stdout.write(formatField("Profile", ctx.profile) + "\n");
1750
1799
  process.stdout.write(formatField("DID", profile.did) + "\n");
1751
1800
  process.stdout.write(formatField("Session DID", profile.sessionDid ?? null) + "\n");
1752
- process.stdout.write(formatField("Primary DID", profile.primaryDid ?? null) + "\n");
1801
+ process.stdout.write(formatField("Owner DID", profile.ownerDid ?? null) + "\n");
1753
1802
  process.stdout.write(formatField("Auth Method", profile.authMethod ?? null) + "\n");
1754
1803
  process.stdout.write(formatField("Posture", posture) + "\n");
1755
1804
  process.stdout.write(formatField("Operator", operatorType) + "\n");
@@ -1880,7 +1929,7 @@ function normalizePortableDelegation(delegation) {
1880
1929
  return { ...delegation, expiry };
1881
1930
  }
1882
1931
  async function ensureDelegationAuthority(params) {
1883
- if (params.node.hasRuntimePermissions(params.requested)) return;
1932
+ if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
1884
1933
  if (params.profile.authMethod === "openkey") {
1885
1934
  const key = await ProfileManager.getKey(params.ctx.profile);
1886
1935
  if (!key) {
@@ -2026,18 +2075,40 @@ function parseExpiryOption(raw) {
2026
2075
  }
2027
2076
  function groupPermissionsBySpace(permissions) {
2028
2077
  const groups = /* @__PURE__ */ new Map();
2078
+ const rawEntries = [];
2029
2079
  for (const permission of permissions) {
2080
+ if (isRawPermission(permission)) {
2081
+ rawEntries.push(permission);
2082
+ continue;
2083
+ }
2030
2084
  const group = groups.get(permission.space) ?? [];
2031
2085
  group.push(permission);
2032
2086
  groups.set(permission.space, group);
2033
2087
  }
2034
- return Array.from(groups.values());
2088
+ const grouped = Array.from(groups.values());
2089
+ if (grouped.length === 0) {
2090
+ return rawEntries.length > 0 ? [rawEntries] : [];
2091
+ }
2092
+ grouped[0].push(...rawEntries);
2093
+ return grouped;
2094
+ }
2095
+ function isRawPermission(permission) {
2096
+ return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
2097
+ }
2098
+ function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
2099
+ if (returnedSpace === expectedSpace) return true;
2100
+ if (!returnedSpace.startsWith("tinycloud:")) return false;
2101
+ const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
2102
+ return returnedName === expectedSpace;
2035
2103
  }
2036
2104
  function portableFromOpenKeyDelegation(data, permissions, host) {
2037
- const primary = permissions[0];
2038
- const returnedSpace = String(data.spaceId ?? primary.space);
2039
- const expectedSpaces = new Set(permissions.map((permission) => permission.space));
2040
- if (expectedSpaces.size !== 1 || !expectedSpaces.has(returnedSpace)) {
2105
+ const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
2106
+ const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
2107
+ const expectedSpaces = new Set(
2108
+ permissions.filter((permission) => !isRawPermission(permission)).map((permission) => permission.space)
2109
+ );
2110
+ const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
2111
+ if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
2041
2112
  throw new CLIError(
2042
2113
  "OPENKEY_SCOPE_MISMATCH",
2043
2114
  `OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
@@ -2072,16 +2143,74 @@ function inferDelegationExpiry(data) {
2072
2143
  }
2073
2144
  return new Date(Date.now() + 60 * 60 * 1e3);
2074
2145
  }
2075
- async function handleLocalAuth(profileName, host) {
2146
+ async function rotateAuthKey(profileName, host, options = {}) {
2147
+ const profile = await ProfileManager.getProfile(profileName);
2148
+ const posture = resolveProfilePosture(profile);
2149
+ const oldDid = profile.sessionDid ?? profile.did;
2150
+ if (posture === "delegate-session") {
2151
+ throw new CLIError(
2152
+ "ROTATE_DELEGATE_SESSION_UNSUPPORTED",
2153
+ `Profile "${profileName}" is a delegated session. Request or import a new owner delegation instead of rotating it locally.`,
2154
+ ExitCode.PERMISSION_DENIED
2155
+ );
2156
+ }
2157
+ if (profile.authMethod === "local" || posture === "local-owner-key") {
2158
+ if (!profile.privateKey) {
2159
+ throw new CLIError(
2160
+ "LOCAL_OWNER_KEY_REQUIRED",
2161
+ `Profile "${profileName}" does not have a local owner private key. Run \`tc auth login --method local\` first.`,
2162
+ ExitCode.AUTH_REQUIRED
2163
+ );
2164
+ }
2165
+ await ProfileManager.clearSession(profileName);
2166
+ const result2 = await handleLocalAuth(profileName, host, {
2167
+ emitOutput: false,
2168
+ forceSessionKey: true
2169
+ });
2170
+ outputRotationResult(result2.profile, profileName, oldDid, "local");
2171
+ return;
2172
+ }
2173
+ const { jwk, did } = await withSpinner("Generating session key...", async () => {
2174
+ return generateKey();
2175
+ });
2176
+ await ProfileManager.setKey(profileName, jwk);
2177
+ await ProfileManager.clearSession(profileName);
2178
+ await ProfileManager.setProfile(profileName, {
2179
+ ...profile,
2180
+ host,
2181
+ did,
2182
+ sessionDid: did,
2183
+ posture: profile.posture ?? "owner-openkey",
2184
+ operatorType: profile.operatorType ?? "human",
2185
+ authMethod: "openkey"
2186
+ });
2187
+ const result = await refreshOpenKeySession(profileName, host, {
2188
+ paste: options.paste
2189
+ });
2190
+ outputRotationResult(result.profile, profileName, oldDid, "openkey");
2191
+ }
2192
+ function outputRotationResult(profile, profileName, oldDid, authMethod) {
2193
+ outputJson({
2194
+ rotated: true,
2195
+ profile: profileName,
2196
+ oldDid,
2197
+ did: profile.did,
2198
+ sessionDid: profile.sessionDid ?? null,
2199
+ authMethod,
2200
+ spaceId: profile.spaceId ?? null
2201
+ });
2202
+ }
2203
+ async function handleLocalAuth(profileName, host, options = {}) {
2076
2204
  const profile = await ProfileManager.getProfile(profileName).catch(() => null);
2205
+ const posture = profile ? resolveProfilePosture(profile) : null;
2077
2206
  let privateKey;
2078
2207
  let address;
2079
2208
  let did;
2080
2209
  let sessionDid = profile?.sessionDid;
2081
- if (profile?.authMethod === "local" && profile.privateKey && profile.address) {
2210
+ if ((profile?.authMethod === "local" || posture === "local-owner-key") && profile.privateKey) {
2082
2211
  privateKey = profile.privateKey;
2083
- address = profile.address;
2084
- did = profile.did;
2212
+ address = profile.address ?? await deriveAddress(privateKey);
2213
+ did = profile.did.startsWith("did:pkh:") ? profile.did : addressToDID(address, profile.chainId ?? DEFAULT_CHAIN_ID);
2085
2214
  if (isInteractive()) {
2086
2215
  process.stderr.write(theme.muted("Using existing local key") + "\n");
2087
2216
  process.stderr.write(formatField("Address", address) + "\n");
@@ -2100,7 +2229,7 @@ async function handleLocalAuth(profileName, host) {
2100
2229
  }
2101
2230
  }
2102
2231
  const hasKey = await ProfileManager.getKey(profileName);
2103
- if (!hasKey) {
2232
+ if (options.forceSessionKey || !hasKey) {
2104
2233
  const { jwk, did: generatedSessionDid } = await withSpinner("Generating session key...", async () => {
2105
2234
  return generateKey();
2106
2235
  });
@@ -2125,7 +2254,7 @@ async function handleLocalAuth(profileName, host) {
2125
2254
  signature: sessionResult.signature
2126
2255
  });
2127
2256
  sessionDid = sessionResult.verificationMethod;
2128
- await ProfileManager.setProfile(profileName, {
2257
+ const updatedProfile = {
2129
2258
  ...profile,
2130
2259
  name: profileName,
2131
2260
  host,
@@ -2133,7 +2262,7 @@ async function handleLocalAuth(profileName, host) {
2133
2262
  spaceName: "default",
2134
2263
  did,
2135
2264
  sessionDid,
2136
- primaryDid: did,
2265
+ ownerDid: did,
2137
2266
  spaceId: sessionResult.spaceId,
2138
2267
  createdAt: profile?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2139
2268
  posture: profile?.posture ?? "local-owner-key",
@@ -2141,18 +2270,32 @@ async function handleLocalAuth(profileName, host) {
2141
2270
  authMethod: "local",
2142
2271
  privateKey,
2143
2272
  address
2144
- });
2273
+ };
2274
+ await ProfileManager.setProfile(profileName, updatedProfile);
2275
+ if (options.emitOutput ?? true) {
2276
+ outputJson({
2277
+ authenticated: true,
2278
+ profile: profileName,
2279
+ did,
2280
+ sessionDid,
2281
+ address,
2282
+ spaceId: sessionResult.spaceId,
2283
+ authMethod: "local"
2284
+ });
2285
+ }
2286
+ return { profile: updatedProfile, sessionResult };
2287
+ }
2288
+ async function handleOpenKeyAuth(profileName, host, paste) {
2289
+ const { profile, delegationData } = await refreshOpenKeySession(profileName, host, { paste });
2145
2290
  outputJson({
2146
2291
  authenticated: true,
2147
2292
  profile: profileName,
2148
- did,
2149
- sessionDid,
2150
- address,
2151
- spaceId: sessionResult.spaceId,
2152
- authMethod: "local"
2293
+ did: profile.did,
2294
+ spaceId: delegationData.spaceId,
2295
+ authMethod: "openkey"
2153
2296
  });
2154
2297
  }
2155
- async function handleOpenKeyAuth(profileName, host, paste) {
2298
+ async function refreshOpenKeySession(profileName, host, options = {}) {
2156
2299
  const key = await ProfileManager.getKey(profileName);
2157
2300
  if (!key) {
2158
2301
  throw new CLIError(
@@ -2163,7 +2306,7 @@ async function handleOpenKeyAuth(profileName, host, paste) {
2163
2306
  }
2164
2307
  const profile = await ProfileManager.getProfile(profileName);
2165
2308
  const delegationData = await startAuthFlow(profile.did, {
2166
- paste,
2309
+ paste: options.paste,
2167
2310
  jwk: key,
2168
2311
  host,
2169
2312
  openkeyHost: resolveOpenKeyHost(profile)
@@ -2178,16 +2321,10 @@ async function handleOpenKeyAuth(profileName, host, paste) {
2178
2321
  };
2179
2322
  if (delegationData.spaceId) {
2180
2323
  updatedProfile.spaceId = delegationData.spaceId;
2181
- updatedProfile.primaryDid = delegationData.primaryDid;
2324
+ updatedProfile.ownerDid = delegationData.ownerDid;
2182
2325
  }
2183
2326
  await ProfileManager.setProfile(profileName, updatedProfile);
2184
- outputJson({
2185
- authenticated: true,
2186
- profile: profileName,
2187
- did: profile.did,
2188
- spaceId: delegationData.spaceId,
2189
- authMethod: "openkey"
2190
- });
2327
+ return { profile: updatedProfile, delegationData };
2191
2328
  }
2192
2329
 
2193
2330
  // src/commands/kv.ts
@@ -2452,6 +2589,15 @@ function parseExpiry(input) {
2452
2589
  }
2453
2590
 
2454
2591
  // src/commands/delegation.ts
2592
+ import { principalDidEquals } from "@tinycloud/node-sdk";
2593
+ function didMatches(actual, expected) {
2594
+ if (!actual) return false;
2595
+ try {
2596
+ return principalDidEquals(actual, expected);
2597
+ } catch {
2598
+ return actual === expected;
2599
+ }
2600
+ }
2455
2601
  function registerDelegationCommand(program2) {
2456
2602
  const delegation = program2.command("delegation").description("Manage delegations");
2457
2603
  delegation.command("create").description("Create a delegation").requiredOption("--to <did>", "Recipient DID").requiredOption("--path <path>", "KV path scope").requiredOption("--actions <actions>", "Comma-separated actions (e.g., kv/get,kv/list)").option("--expiry <duration>", "Expiry duration (e.g., 1h, 7d, ISO date)", "1h").action(async (options, cmd) => {
@@ -2496,10 +2642,10 @@ function registerDelegationCommand(program2) {
2496
2642
  let delegations = result.data;
2497
2643
  if (options.granted) {
2498
2644
  const myDid = node.did;
2499
- delegations = delegations.filter((d) => d.delegatorDID === myDid);
2645
+ delegations = delegations.filter((d) => didMatches(d.delegatorDID, myDid));
2500
2646
  } else if (options.received) {
2501
2647
  const myDid = node.did;
2502
- delegations = delegations.filter((d) => d.delegateDID === myDid || d.delegateDID?.includes(myDid));
2648
+ delegations = delegations.filter((d) => didMatches(d.delegateDID, myDid));
2503
2649
  }
2504
2650
  outputJson({
2505
2651
  delegations: delegations.map((d) => ({
@@ -2910,7 +3056,7 @@ _tc_completions() {
2910
3056
  commands="init auth kv space delegation share node profile completion"
2911
3057
 
2912
3058
  case "\${COMP_WORDS[1]}" in
2913
- auth) subcommands="login logout status whoami" ;;
3059
+ auth) subcommands="login logout rotate status whoami" ;;
2914
3060
  kv) subcommands="get put delete list head" ;;
2915
3061
  space) subcommands="list create info switch" ;;
2916
3062
  delegation) subcommands="create list info revoke" ;;
@@ -2960,7 +3106,7 @@ _tc() {
2960
3106
  ;;
2961
3107
  args)
2962
3108
  case $words[1] in
2963
- auth) _values 'subcommand' login logout status whoami ;;
3109
+ auth) _values 'subcommand' login logout rotate status whoami ;;
2964
3110
  kv) _values 'subcommand' get put delete list head ;;
2965
3111
  space) _values 'subcommand' list create info switch ;;
2966
3112
  delegation) _values 'subcommand' create list info revoke ;;
@@ -2995,7 +3141,7 @@ complete -c tc -n "not __fish_seen_subcommand_from $commands" -a profile -d "Pro
2995
3141
  complete -c tc -n "not __fish_seen_subcommand_from $commands" -a completion -d "Generate shell completions"
2996
3142
 
2997
3143
  # Subcommands
2998
- complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout status whoami"
3144
+ complete -c tc -n "__fish_seen_subcommand_from auth" -a "login logout rotate status whoami"
2999
3145
  complete -c tc -n "__fish_seen_subcommand_from kv" -a "get put delete list head"
3000
3146
  complete -c tc -n "__fish_seen_subcommand_from space" -a "list create info switch"
3001
3147
  complete -c tc -n "__fish_seen_subcommand_from delegation" -a "create list info revoke"
@@ -3189,6 +3335,17 @@ function registerVaultCommand(program2) {
3189
3335
  // src/commands/secrets.ts
3190
3336
  import { readFile as readFile6 } from "fs/promises";
3191
3337
  import { writeFile as writeFile5 } from "fs/promises";
3338
+ import {
3339
+ resolveSecretListPrefix,
3340
+ resolveSecretPath
3341
+ } from "@tinycloud/node-sdk";
3342
+ var SECRETS_SPACE = "secrets";
3343
+ var SECRET_KV_ABILITIES = {
3344
+ get: "tinycloud.kv/get",
3345
+ put: "tinycloud.kv/put",
3346
+ del: "tinycloud.kv/del",
3347
+ list: "tinycloud.kv/list"
3348
+ };
3192
3349
  async function readStdin4() {
3193
3350
  const chunks = [];
3194
3351
  for await (const chunk of process.stdin) {
@@ -3204,6 +3361,123 @@ function resolveSecretScope(options) {
3204
3361
  const scope = options.scope ?? options.space;
3205
3362
  return scope ? { scope } : void 0;
3206
3363
  }
3364
+ async function ensureSecretsNode(ctx, options) {
3365
+ const auth = authOptions(options);
3366
+ if (auth?.privateKey) {
3367
+ return ensureAuthenticated(ctx, auth);
3368
+ }
3369
+ const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
3370
+ if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
3371
+ const session = await ProfileManager.getSession(ctx.profile);
3372
+ if (!session || isStoredSessionExpired(session)) {
3373
+ await withSpinner(
3374
+ session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
3375
+ () => refreshOpenKeySession(ctx.profile, ctx.host)
3376
+ );
3377
+ }
3378
+ }
3379
+ return ensureAuthenticated(ctx, auth);
3380
+ }
3381
+ async function runSecretOperation(params) {
3382
+ const first = await runSecretOperationAttempt(params.label, params.operation);
3383
+ if (first.ok || !shouldRequestSecretPermissions(first.error)) {
3384
+ return first;
3385
+ }
3386
+ const profile = await ProfileManager.getProfile(params.ctx.profile);
3387
+ if (!canRequestOwnerPermissions(profile)) {
3388
+ return first;
3389
+ }
3390
+ const requested = secretPermissionEntries({
3391
+ action: params.action,
3392
+ name: params.name,
3393
+ options: params.scopeOptions,
3394
+ node: params.node
3395
+ });
3396
+ await withSpinner(
3397
+ "Requesting secret permissions...",
3398
+ () => ensureDelegationAuthority({
3399
+ ctx: params.ctx,
3400
+ profile,
3401
+ node: params.node,
3402
+ requested,
3403
+ expiryOption: void 0,
3404
+ yes: true,
3405
+ force: true
3406
+ })
3407
+ );
3408
+ return runSecretOperationAttempt(params.label, params.operation);
3409
+ }
3410
+ async function runSecretOperationAttempt(label, operation) {
3411
+ try {
3412
+ return await withSpinner(label, operation);
3413
+ } catch (error) {
3414
+ const permissionError = thrownPermissionError(error);
3415
+ if (permissionError) return permissionError;
3416
+ throw error;
3417
+ }
3418
+ }
3419
+ function canRequestOwnerPermissions(profile) {
3420
+ const posture = resolveProfilePosture(profile);
3421
+ return posture === "owner-openkey" || posture === "local-owner-key";
3422
+ }
3423
+ function shouldRequestSecretPermissions(error) {
3424
+ if (error.code !== "PERMISSION_DENIED") return false;
3425
+ return /permission|session expired|autosign|capabilit/i.test(error.message);
3426
+ }
3427
+ function thrownPermissionError(error) {
3428
+ const record = error;
3429
+ const message = typeof record?.message === "string" ? record.message : String(error);
3430
+ const code = typeof record?.code === "string" ? record.code : "PERMISSION_DENIED";
3431
+ if (code !== "PERMISSION_DENIED" && !/permission|session expired|autosign|capabilit/i.test(message)) {
3432
+ return null;
3433
+ }
3434
+ return {
3435
+ ok: false,
3436
+ error: {
3437
+ code: "PERMISSION_DENIED",
3438
+ message
3439
+ }
3440
+ };
3441
+ }
3442
+ function isStoredSessionExpired(session) {
3443
+ const record = session;
3444
+ const direct = parseDate(record.expiresAt ?? record.expiry ?? record.expirationTime);
3445
+ if (direct) return direct.getTime() <= Date.now();
3446
+ if (typeof record.siwe !== "string") return false;
3447
+ const match = record.siwe.match(/^Expiration Time:\s*(.+)$/im);
3448
+ const expiry = match ? parseDate(match[1].trim()) : null;
3449
+ return expiry !== null && expiry.getTime() <= Date.now();
3450
+ }
3451
+ function parseDate(value) {
3452
+ if (value instanceof Date) {
3453
+ return Number.isNaN(value.getTime()) ? null : value;
3454
+ }
3455
+ if (typeof value !== "string" || value.trim() === "") return null;
3456
+ const date = new Date(value);
3457
+ return Number.isNaN(date.getTime()) ? null : date;
3458
+ }
3459
+ function secretKvAbility(action) {
3460
+ return SECRET_KV_ABILITIES[action];
3461
+ }
3462
+ function secretPermissionEntries(params) {
3463
+ const path = params.action === "list" ? resolveSecretListPrefix(params.options) : resolveSecretPath(params.name ?? "", params.options).permissionPaths.vault;
3464
+ const permissions = [{
3465
+ service: "tinycloud.kv",
3466
+ space: SECRETS_SPACE,
3467
+ path,
3468
+ actions: [secretKvAbility(params.action)],
3469
+ skipPrefix: true
3470
+ }];
3471
+ if (params.action === "get") {
3472
+ permissions.push({
3473
+ service: "tinycloud.encryption",
3474
+ path: params.node.getDefaultEncryptionNetworkId(),
3475
+ actions: ["tinycloud.encryption/decrypt"],
3476
+ skipPrefix: true
3477
+ });
3478
+ }
3479
+ return permissions;
3480
+ }
3207
3481
  function registerSecretsCommand(program2) {
3208
3482
  const secrets = program2.command("secrets").description("Encrypted secrets management");
3209
3483
  const network = secrets.command("network").description("Manage the default secrets encryption network");
@@ -3249,12 +3523,16 @@ function registerSecretsCommand(program2) {
3249
3523
  try {
3250
3524
  const globalOpts = cmd.optsWithGlobals();
3251
3525
  const ctx = await ProfileManager.resolveContext(globalOpts);
3252
- const node = await ensureAuthenticated(ctx, authOptions(options));
3526
+ const node = await ensureSecretsNode(ctx, options);
3253
3527
  const scopeOptions = resolveSecretScope(options);
3254
- const result = await withSpinner(
3255
- "Listing secrets...",
3256
- () => node.secrets.list(scopeOptions)
3257
- );
3528
+ const result = await runSecretOperation({
3529
+ ctx,
3530
+ node,
3531
+ action: "list",
3532
+ scopeOptions,
3533
+ label: "Listing secrets...",
3534
+ operation: () => node.secrets.list(scopeOptions)
3535
+ });
3258
3536
  if (!result.ok) {
3259
3537
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
3260
3538
  }
@@ -3273,12 +3551,17 @@ function registerSecretsCommand(program2) {
3273
3551
  try {
3274
3552
  const globalOpts = cmd.optsWithGlobals();
3275
3553
  const ctx = await ProfileManager.resolveContext(globalOpts);
3276
- const node = await ensureAuthenticated(ctx, authOptions(options));
3554
+ const node = await ensureSecretsNode(ctx, options);
3277
3555
  const scopeOptions = resolveSecretScope(options);
3278
- const result = await withSpinner(
3279
- `Getting secret ${name}...`,
3280
- () => node.secrets.get(name, scopeOptions)
3281
- );
3556
+ const result = await runSecretOperation({
3557
+ ctx,
3558
+ node,
3559
+ action: "get",
3560
+ name,
3561
+ scopeOptions,
3562
+ label: `Getting secret ${name}...`,
3563
+ operation: () => node.secrets.get(name, scopeOptions)
3564
+ });
3282
3565
  if (!result.ok) {
3283
3566
  if (result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND") {
3284
3567
  throw new CLIError("NOT_FOUND", `Secret "${name}" not found`, ExitCode.NOT_FOUND);
@@ -3304,7 +3587,7 @@ function registerSecretsCommand(program2) {
3304
3587
  try {
3305
3588
  const globalOpts = cmd.optsWithGlobals();
3306
3589
  const ctx = await ProfileManager.resolveContext(globalOpts);
3307
- const node = await ensureAuthenticated(ctx, authOptions(options));
3590
+ const node = await ensureSecretsNode(ctx, options);
3308
3591
  let secretValue;
3309
3592
  const sources = [value !== void 0, !!options.file, !!options.stdin].filter(Boolean);
3310
3593
  if (sources.length === 0) {
@@ -3321,10 +3604,15 @@ function registerSecretsCommand(program2) {
3321
3604
  secretValue = value;
3322
3605
  }
3323
3606
  const scopeOptions = resolveSecretScope(options);
3324
- const result = await withSpinner(
3325
- `Storing secret ${name}...`,
3326
- () => node.secrets.put(name, secretValue, scopeOptions)
3327
- );
3607
+ const result = await runSecretOperation({
3608
+ ctx,
3609
+ node,
3610
+ action: "put",
3611
+ name,
3612
+ scopeOptions,
3613
+ label: `Storing secret ${name}...`,
3614
+ operation: () => node.secrets.put(name, secretValue, scopeOptions)
3615
+ });
3328
3616
  if (!result.ok) {
3329
3617
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
3330
3618
  }
@@ -3337,12 +3625,17 @@ function registerSecretsCommand(program2) {
3337
3625
  try {
3338
3626
  const globalOpts = cmd.optsWithGlobals();
3339
3627
  const ctx = await ProfileManager.resolveContext(globalOpts);
3340
- const node = await ensureAuthenticated(ctx, authOptions(options));
3628
+ const node = await ensureSecretsNode(ctx, options);
3341
3629
  const scopeOptions = resolveSecretScope(options);
3342
- const result = await withSpinner(
3343
- `Deleting secret ${name}...`,
3344
- () => node.secrets.delete(name, scopeOptions)
3345
- );
3630
+ const result = await runSecretOperation({
3631
+ ctx,
3632
+ node,
3633
+ action: "del",
3634
+ name,
3635
+ scopeOptions,
3636
+ label: `Deleting secret ${name}...`,
3637
+ operation: () => node.secrets.delete(name, scopeOptions)
3638
+ });
3346
3639
  if (!result.ok) {
3347
3640
  throw new CLIError(result.error.code, result.error.message, ExitCode.ERROR);
3348
3641
  }
@@ -4253,6 +4546,358 @@ function registerUpgradeCommand(program2) {
4253
4546
  });
4254
4547
  }
4255
4548
 
4549
+ // src/commands/status.ts
4550
+ import {
4551
+ NodeWasmBindings
4552
+ } from "@tinycloud/node-sdk";
4553
+ var wasmBindings = null;
4554
+ function registerStatusCommand(program2) {
4555
+ program2.command("status").description("Show local TinyCloud profile, session, delegation, and permission state").action(async (_options, cmd) => {
4556
+ try {
4557
+ const globalOpts = cmd.optsWithGlobals();
4558
+ const ctx = await ProfileManager.resolveContext(globalOpts);
4559
+ const config = await ProfileManager.getConfig();
4560
+ const names = (await ProfileManager.listProfiles()).sort(
4561
+ (a, b) => a.localeCompare(b)
4562
+ );
4563
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4564
+ const profiles = await Promise.all(
4565
+ names.map(
4566
+ (name) => inspectProfile({
4567
+ name,
4568
+ activeProfile: ctx.profile,
4569
+ defaultProfile: config.defaultProfile
4570
+ })
4571
+ )
4572
+ );
4573
+ const summary = {
4574
+ generatedAt,
4575
+ activeProfile: ctx.profile,
4576
+ defaultProfile: config.defaultProfile,
4577
+ profileCount: profiles.length,
4578
+ authenticatedProfileCount: profiles.filter((p) => p.authenticated).length,
4579
+ activeDelegationCount: profiles.reduce(
4580
+ (sum, profile) => sum + profile.activeDelegationCount,
4581
+ 0
4582
+ ),
4583
+ profiles
4584
+ };
4585
+ if (shouldOutputJson()) {
4586
+ outputJson(summary);
4587
+ return;
4588
+ }
4589
+ process.stdout.write(formatStatus(summary));
4590
+ } catch (error) {
4591
+ handleError(error);
4592
+ }
4593
+ });
4594
+ }
4595
+ async function inspectProfile(params) {
4596
+ const issues = [];
4597
+ const profile = await readProfile(params.name, issues);
4598
+ const session = await readSession(params.name, issues);
4599
+ const hasKey = await readHasKey(params.name, issues);
4600
+ const storedDelegations = await readDelegations(params.name, issues);
4601
+ const sessionPermissions = session ? sessionPermissionsFromRecap(session) : [];
4602
+ const sessionExpiry = session ? extractSessionExpiry(session) : null;
4603
+ const sessionExpired = sessionExpiry === null ? null : sessionExpiry.getTime() <= Date.now();
4604
+ const statusSession = {
4605
+ present: session !== null,
4606
+ expired: session === null ? null : sessionExpired,
4607
+ expiresAt: sessionExpiry?.toISOString() ?? null,
4608
+ permissions: sessionPermissions,
4609
+ permissionsCompact: compactPermissions(sessionPermissions)
4610
+ };
4611
+ const delegations = storedDelegations.map(inspectDelegation);
4612
+ const activeDelegationPermissions = delegations.filter((delegation) => delegation.active).flatMap((delegation) => delegation.permissions);
4613
+ const permissions = uniquePermissions([
4614
+ ...sessionPermissions,
4615
+ ...activeDelegationPermissions
4616
+ ]);
4617
+ const hasPrivateKey = typeof profile?.privateKey === "string" && profile.privateKey.length > 0;
4618
+ const localKeyAuthenticated = profile?.authMethod === "local" && hasPrivateKey;
4619
+ const sessionAuthenticated = session !== null && sessionExpired !== true;
4620
+ const authenticated = localKeyAuthenticated || sessionAuthenticated;
4621
+ const status = resolveStatus({
4622
+ exists: profile !== null,
4623
+ authenticated,
4624
+ localKeyAuthenticated,
4625
+ sessionExpired
4626
+ });
4627
+ return {
4628
+ name: params.name,
4629
+ active: params.name === params.activeProfile,
4630
+ default: params.name === params.defaultProfile,
4631
+ exists: profile !== null,
4632
+ status,
4633
+ host: profile?.host ?? null,
4634
+ did: profile?.did ?? null,
4635
+ sessionDid: profile?.sessionDid ?? null,
4636
+ ownerDid: profile?.ownerDid ?? null,
4637
+ address: profile?.address ?? null,
4638
+ spaceId: profile?.spaceId ?? null,
4639
+ authMethod: profile?.authMethod ?? null,
4640
+ posture: profile ? resolveProfilePosture(profile) : null,
4641
+ operatorType: profile ? resolveProfileOperatorType(profile) : null,
4642
+ hasKey,
4643
+ hasPrivateKey,
4644
+ authenticated,
4645
+ session: statusSession,
4646
+ delegations,
4647
+ permissions,
4648
+ permissionsCompact: compactPermissions(permissions),
4649
+ permissionCount: permissions.length,
4650
+ activeDelegationCount: delegations.filter((delegation) => delegation.active).length,
4651
+ delegationCount: delegations.length,
4652
+ issues
4653
+ };
4654
+ }
4655
+ async function readProfile(name, issues) {
4656
+ try {
4657
+ return await ProfileManager.getProfile(name);
4658
+ } catch (error) {
4659
+ issues.push(`profile: ${messageFromError(error)}`);
4660
+ return null;
4661
+ }
4662
+ }
4663
+ async function readSession(name, issues) {
4664
+ try {
4665
+ return asRecord(await ProfileManager.getSession(name));
4666
+ } catch (error) {
4667
+ issues.push(`session: ${messageFromError(error)}`);
4668
+ return null;
4669
+ }
4670
+ }
4671
+ async function readHasKey(name, issues) {
4672
+ try {
4673
+ return await ProfileManager.getKey(name) !== null;
4674
+ } catch (error) {
4675
+ issues.push(`key: ${messageFromError(error)}`);
4676
+ return false;
4677
+ }
4678
+ }
4679
+ async function readDelegations(name, issues) {
4680
+ try {
4681
+ return await loadAdditionalDelegations(name);
4682
+ } catch (error) {
4683
+ issues.push(`delegations: ${messageFromError(error)}`);
4684
+ return [];
4685
+ }
4686
+ }
4687
+ function inspectDelegation(entry) {
4688
+ const expiry = parseDate2(entry.delegation.expiry);
4689
+ const expired = expiry === null ? null : expiry.getTime() <= Date.now();
4690
+ const permissions = normalizePermissions(
4691
+ Array.isArray(entry.permissions) && entry.permissions.length > 0 ? entry.permissions : permissionsFromDelegation(entry.delegation)
4692
+ );
4693
+ return {
4694
+ cid: entry.delegation.cid,
4695
+ active: expired !== true,
4696
+ expired,
4697
+ expiresAt: expiry?.toISOString() ?? null,
4698
+ permissions,
4699
+ permissionsCompact: compactPermissions(permissions)
4700
+ };
4701
+ }
4702
+ function resolveStatus(params) {
4703
+ if (!params.exists) return "missing";
4704
+ if (params.localKeyAuthenticated) return "local-key";
4705
+ if (params.authenticated) return "logged-in";
4706
+ if (params.sessionExpired === true) return "expired";
4707
+ return "signed-out";
4708
+ }
4709
+ function sessionPermissionsFromRecap(session) {
4710
+ if (typeof session.siwe !== "string" || session.siwe.length === 0) return [];
4711
+ try {
4712
+ const rawEntries = getWasmBindings().parseRecapFromSiwe(session.siwe);
4713
+ if (!Array.isArray(rawEntries)) return [];
4714
+ return normalizePermissions(rawEntries.map(permissionFromRawRecap));
4715
+ } catch {
4716
+ return [];
4717
+ }
4718
+ }
4719
+ function permissionFromRawRecap(value) {
4720
+ const record = asRecord(value);
4721
+ if (!record) return null;
4722
+ const service = stringValue(record.service);
4723
+ const space = stringValue(record.space);
4724
+ const path = stringValue(record.path);
4725
+ const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
4726
+ if (!service || !space || path === null || actions.length === 0) return null;
4727
+ return {
4728
+ service: normalizeService2(service),
4729
+ space,
4730
+ path,
4731
+ actions
4732
+ };
4733
+ }
4734
+ function normalizePermissions(entries) {
4735
+ const permissions = [];
4736
+ for (const entry of entries) {
4737
+ const permission = permissionFromUnknown(entry);
4738
+ if (permission) permissions.push(permission);
4739
+ }
4740
+ return uniquePermissions(permissions);
4741
+ }
4742
+ function permissionFromUnknown(value) {
4743
+ const record = asRecord(value);
4744
+ if (!record) return null;
4745
+ const service = stringValue(record.service);
4746
+ const space = stringValue(record.space);
4747
+ const path = stringValue(record.path);
4748
+ const actions = Array.isArray(record.actions) ? record.actions.map(String).filter(Boolean) : [];
4749
+ if (!service || !space || path === null || actions.length === 0) return null;
4750
+ return {
4751
+ service: normalizeService2(service),
4752
+ space,
4753
+ path,
4754
+ actions
4755
+ };
4756
+ }
4757
+ function uniquePermissions(entries) {
4758
+ const seen = /* @__PURE__ */ new Set();
4759
+ const unique2 = [];
4760
+ for (const entry of entries) {
4761
+ const key = compactPermission(entry);
4762
+ if (seen.has(key)) continue;
4763
+ seen.add(key);
4764
+ unique2.push(entry);
4765
+ }
4766
+ return unique2;
4767
+ }
4768
+ function compactPermissions(entries) {
4769
+ return entries.map(compactPermission);
4770
+ }
4771
+ function extractSessionExpiry(session) {
4772
+ for (const key of ["expiresAt", "expiry", "expirationTime"]) {
4773
+ const parsed = parseDate2(session[key]);
4774
+ if (parsed) return parsed;
4775
+ }
4776
+ if (typeof session.siwe !== "string") return null;
4777
+ const match = session.siwe.match(/^Expiration Time:\s*(.+)$/im);
4778
+ return match ? parseDate2(match[1].trim()) : null;
4779
+ }
4780
+ function parseDate2(value) {
4781
+ if (value instanceof Date) {
4782
+ return Number.isNaN(value.getTime()) ? null : value;
4783
+ }
4784
+ if (typeof value === "number" && Number.isFinite(value)) {
4785
+ const millis = value > 0 && value < 1e12 ? value * 1e3 : value;
4786
+ const date2 = new Date(millis);
4787
+ return Number.isNaN(date2.getTime()) ? null : date2;
4788
+ }
4789
+ if (typeof value !== "string" || value.trim() === "") return null;
4790
+ const date = new Date(value);
4791
+ return Number.isNaN(date.getTime()) ? null : date;
4792
+ }
4793
+ function getWasmBindings() {
4794
+ wasmBindings ??= new NodeWasmBindings();
4795
+ return wasmBindings;
4796
+ }
4797
+ function normalizeService2(service) {
4798
+ return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
4799
+ }
4800
+ function asRecord(value) {
4801
+ return value !== null && typeof value === "object" ? value : null;
4802
+ }
4803
+ function stringValue(value) {
4804
+ return typeof value === "string" ? value : null;
4805
+ }
4806
+ function messageFromError(error) {
4807
+ return error instanceof Error ? error.message : String(error);
4808
+ }
4809
+ function formatStatus(summary) {
4810
+ const lines = [];
4811
+ lines.push(theme.heading("TinyCloud Status"));
4812
+ lines.push(`Active profile: ${theme.value(summary.activeProfile)}`);
4813
+ lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
4814
+ lines.push("");
4815
+ if (summary.profiles.length === 0) {
4816
+ lines.push(theme.muted("No profiles configured. Run: tc init"));
4817
+ return `${lines.join("\n")}
4818
+ `;
4819
+ }
4820
+ lines.push(theme.label("Profiles"));
4821
+ for (const profile of summary.profiles) {
4822
+ lines.push(formatProfile(profile));
4823
+ }
4824
+ return `${lines.join("\n")}
4825
+ `;
4826
+ }
4827
+ function formatProfile(profile) {
4828
+ const marker = profile.active ? theme.success("*") : " ";
4829
+ const name = profile.default ? `${profile.name} (default)` : profile.name;
4830
+ const host = profile.host ? theme.muted(profile.host) : theme.muted("no host");
4831
+ const summary = [
4832
+ `${marker} ${profile.active ? theme.brand(name) : name}`,
4833
+ formatProfileStatus(profile.status),
4834
+ profile.posture ?? "no posture",
4835
+ plural(profile.permissionCount, "permission"),
4836
+ `${profile.activeDelegationCount}/${profile.delegationCount} delegations`,
4837
+ host
4838
+ ].join(" ");
4839
+ const lines = [summary];
4840
+ lines.push(` session: ${formatSession(profile.session)}`);
4841
+ if (profile.permissionsCompact.length > 0) {
4842
+ lines.push(" permissions:");
4843
+ for (const permission of profile.permissionsCompact) {
4844
+ lines.push(` ${permission}`);
4845
+ }
4846
+ }
4847
+ if (profile.delegations.length > 0) {
4848
+ lines.push(" delegations:");
4849
+ for (const delegation of profile.delegations) {
4850
+ lines.push(` ${formatDelegation(delegation)}`);
4851
+ }
4852
+ }
4853
+ if (profile.issues.length > 0) {
4854
+ lines.push(" issues:");
4855
+ for (const issue of profile.issues) {
4856
+ lines.push(` ${theme.warn(issue)}`);
4857
+ }
4858
+ }
4859
+ return lines.join("\n");
4860
+ }
4861
+ function formatProfileStatus(status) {
4862
+ switch (status) {
4863
+ case "logged-in":
4864
+ return theme.success("logged in");
4865
+ case "local-key":
4866
+ return theme.success("local key");
4867
+ case "expired":
4868
+ return theme.warn("expired");
4869
+ case "missing":
4870
+ return theme.warn("missing");
4871
+ case "signed-out":
4872
+ return theme.muted("signed out");
4873
+ }
4874
+ }
4875
+ function formatSession(session) {
4876
+ if (!session.present) return theme.muted("none");
4877
+ if (session.expired === true) {
4878
+ return `${theme.warn("expired")}${formatExpiresAt(session.expiresAt)}`;
4879
+ }
4880
+ if (session.expired === false) {
4881
+ return `${theme.success("active")}${formatExpiresAt(session.expiresAt)}`;
4882
+ }
4883
+ return `${theme.success("present")}${formatExpiresAt(session.expiresAt)}`;
4884
+ }
4885
+ function formatDelegation(delegation) {
4886
+ const state = delegation.expired === true ? theme.warn("expired") : theme.success("active");
4887
+ return [
4888
+ delegation.cid,
4889
+ state,
4890
+ formatExpiresAt(delegation.expiresAt).trim(),
4891
+ plural(delegation.permissions.length, "permission")
4892
+ ].filter(Boolean).join(" ");
4893
+ }
4894
+ function formatExpiresAt(expiresAt) {
4895
+ return expiresAt ? ` until ${expiresAt}` : "";
4896
+ }
4897
+ function plural(count, label) {
4898
+ return `${count} ${label}${count === 1 ? "" : "s"}`;
4899
+ }
4900
+
4256
4901
  // src/index.ts
4257
4902
  var { version } = JSON.parse(
4258
4903
  readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
@@ -4267,7 +4912,7 @@ program.hook("preAction", async (thisCommand) => {
4267
4912
  const commandName = thisCommand.name();
4268
4913
  const parentName = thisCommand.parent?.name();
4269
4914
  const fullCommand = parentName && parentName !== "tc" ? `${parentName} ${commandName}` : commandName;
4270
- const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade"].includes(commandName) || fullCommand === "profile create";
4915
+ const skipGuard = ["tc", "init", "doctor", "completion", "help", "upgrade", "status"].includes(commandName) || fullCommand === "profile create";
4271
4916
  if (!skipGuard && !opts.quiet && isInteractive()) {
4272
4917
  try {
4273
4918
  const config = await ProfileManager.getConfig();
@@ -4302,6 +4947,9 @@ registerSqlCommand(program);
4302
4947
  registerDuckdbCommand(program);
4303
4948
  registerManifestCommand(program);
4304
4949
  registerUpgradeCommand(program);
4950
+ registerStatusCommand(program);
4951
+ program.addHelpText("before", () => `${theme.label("Version:")} ${theme.value(version)}
4952
+ `);
4305
4953
  program.addHelpText("afterAll", () => {
4306
4954
  if (!process.stdout.isTTY) return "";
4307
4955
  return `