@trillioncore/cli 1.0.0-next.3 → 1.0.0-next.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.
Files changed (3) hide show
  1. package/README.md +8 -2
  2. package/dist/index.js +192 -118
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `tc` CLI signs you into your own Trillioncore organization and reads the integration accounts you authorize. No repository checkout, provider secrets, `.env` file, or manually entered organization ID is required.
4
4
 
5
- **Requirements:** Node.js 22+, a browser that can reach the CLI computer's loopback listener, and a Trillioncore deployment with CLI login enabled. This is a fresh v1 preview, not v0 command parity; v0 configuration is not imported.
5
+ **Requirements:** Node.js 22+, a browser on any device, and a Trillioncore deployment with CLI device login enabled. This is a fresh v1 preview, not v0 command parity; v0 configuration is not imported.
6
6
 
7
7
  ## Get started
8
8
 
@@ -18,6 +18,8 @@ npm exec --yes --package=@trillioncore/cli@next -- tc sql '<PostgreSQL account I
18
18
  npm exec --yes --package=@trillioncore/cli@next -- tc logout
19
19
  ```
20
20
 
21
+ `tc login` registers a five-minute request and prints a browser URL plus a separate eight-character code. Open the URL on any device and enter the code shown in your terminal. Keep the code private and approve only a login you started. The CLI polls without a callback listener, tunnel, or copied token. After approval it verifies your identity, saves the private session, and acknowledges delivery.
22
+
21
23
  Browser sign-in lets you choose an organization and explicitly select its enabled accounts. An organization administrator must approve access. For a brand-new account, finish organization setup and connect your integrations in the app, then rerun `tc login` if prompted. No provider reconnection is required just to authorize the CLI.
22
24
 
23
25
  Prefer `npm exec` during evaluation. `npm install -g @trillioncore/cli@next` replaces your local `tc` executable, even though publishing `next` does not change the npm `latest` tag.
@@ -53,6 +55,8 @@ The catalog describes stored content kinds or approved **active synchronized** P
53
55
 
54
56
  For native content, `recordSchemas` describes the allowed scalar fields in `get`'s `attributes`, grouped by recognized observed record kind. These field names/types come from the same definitions used to validate stored records. Fields may be absent on individual records; a schema does not prove populated values, complete history, relationship IDs, or SQL support. Empty connections return no observed schemas; unknown kinds remain listed without invented definitions. Table resources retain their separate `relation.columns` metadata.
55
57
 
58
+ `lifecycle` reports the persisted `activeRun` (queued/running) and `lastFinishedRun` (completed/failed), independently of the last successful sync timestamp. Null means no matching run was recorded. The stored `startedAt` can be enqueue time; a running state does not prove worker health. Progress counts are processed pages/records, not percentages or newly created records. A completed run does not prove exhaustive source coverage. Native `page-upserts` may remain visible even if a later page fails; `active-revision` describes snapshot publication, not whether production execution is configured. `sourceRemovals: not-propagated` means native source removal events do not remove stored records; `revision-scoped` describes active-snapshot visibility, not a guarantee of continuous deletion tracking. Lifecycle/counter and freshness changes alone do not invalidate resource cursors; schema/revision changes still require restarting discovery. No cursors, raw failure messages or credentials appear in lifecycle metadata.
59
+
56
60
  Respect `operations[].available` and `warnings`. Production synced SQL is currently not wired, so its schemas can be discovered but SQL is explicitly unavailable with `synced_sql_not_configured`. Live PostgreSQL resources are outside this catalog's coverage. No semantic search or actions are advertised. Native connectors retain search/get capabilities when empty; `recordKinds` lists only observed kinds (capped at 100 with an explicit warning). Synced manifests use existing limits of 25 tables/1,000 columns. A successful sync timestamp does not establish complete history or current provider state. Business definitions, currency and attribution are not inferred.
57
61
 
58
62
  All discovery uses the same organization, live token scope, connection permission and enabled-state checks as existing agent reads. Multiple internal adapters may eventually fulfill a connection's capabilities; this catalog does not introduce a separate user-visible integration for each adapter.
@@ -64,7 +68,9 @@ Sessions refresh automatically. File-based credentials live in `~/.trillioncore/
64
68
  One profile is active at a time. A new successful login replaces it and attempts to revoke the prior connection. Change account scopes or revoke access from the app's **Tokens** page. Membership changes and server revocation are enforced on subsequent requests.
65
69
 
66
70
  - Default API: `https://api.trillioncore.com`. A saved session remains pinned to its issuing server. Custom origins must use HTTPS, except literal loopback development endpoints.
67
- - `--no-browser` prints the sign-in URL; it is **not** a remote device-code flow. SSH users need appropriate loopback forwarding or a browser on the same computer. Do not share the sign-in URL.
71
+ - `--no-browser` prints the device sign-in URL and separate code without launching a browser. Use it over SSH or on a headless machine. Default login uses the same remote-capable flow; there is no loopback fallback flag.
72
+ - Pending and transient polling failures retry within a fixed five-minute deadline. Rate-limit retries respect `Retry-After`. Ctrl-C or timeout makes one best-effort cancellation attempt; unreachable requests expire on the server. If registration itself is lost, rerun login and let the unknown request expire.
73
+ - A lost completed poll can be retrieved again before expiry. Acknowledgement occurs only after private persistence. If acknowledgement fails, the saved session remains active and the temporary delivery expires automatically.
68
74
  - A disabled/not-yet-deployed CLI login returns a clear deployment-unavailable error. Signing in repeatedly cannot enable the server.
69
75
  - Advanced/manual use: `TRILLIONCORE_TOKEN` and `TRILLIONCORE_ORG_ID` must be supplied together and override the saved session. Remove both to use browser login. API overrides prefer `TRILLIONCORE_CLI_API_URL`, then `TRILLIONCORE_API_URL`; a mismatch with a saved session is refused, not silently redirected. Application API configuration is never rewritten.
70
76
  - `TRILLIONCORE_CONFIG_DIR` selects a private CLI-only profile directory (also useful for isolated testing). Legacy v0 configuration is not migrated.
package/dist/index.js CHANGED
@@ -3051,8 +3051,8 @@ import { fileURLToPath } from "node:url";
3051
3051
  import { realpathSync } from "node:fs";
3052
3052
 
3053
3053
  // src/auth.ts
3054
- import { createServer } from "node:http";
3055
- import { createHash, randomBytes as randomBytes2 } from "node:crypto";
3054
+ import { setTimeout as delay } from "node:timers/promises";
3055
+ import { createHash, randomBytes as randomBytes2, randomInt } from "node:crypto";
3056
3056
  import { spawn } from "node:child_process";
3057
3057
 
3058
3058
  // src/session-store.ts
@@ -3243,6 +3243,14 @@ function sessionStore(environment = process.env) {
3243
3243
 
3244
3244
  // src/auth.ts
3245
3245
  var DEFAULT_API = "https://api.trillioncore.com";
3246
+ var deviceSecret = /^[A-Za-z0-9_-]{43}$/;
3247
+ var RetryableAuthError = class extends Error {
3248
+ constructor(message, retryAfterMs = 0) {
3249
+ super(message);
3250
+ this.retryAfterMs = retryAfterMs;
3251
+ }
3252
+ retryAfterMs;
3253
+ };
3246
3254
  async function jsonRequest(issuer, path, init = {}) {
3247
3255
  let response;
3248
3256
  try {
@@ -3253,14 +3261,27 @@ async function jsonRequest(issuer, path, init = {}) {
3253
3261
  });
3254
3262
  } catch {
3255
3263
  if (init.signal?.aborted) throw init.signal.reason;
3256
- throw new Error(
3264
+ throw new RetryableAuthError(
3257
3265
  "Cannot reach the CLI authentication service. Check connectivity and retry."
3258
3266
  );
3259
3267
  }
3260
- if (!response.ok)
3268
+ if (response.status === 429 || response.status >= 500) {
3269
+ const header = response.headers.get("retry-after")?.trim();
3270
+ const seconds = header && /^\d+$/.test(header) ? Number(header) : NaN;
3271
+ const until = header ? Date.parse(header) : NaN;
3272
+ const retryAfterMs = Number.isFinite(seconds) ? seconds * 1e3 : Number.isFinite(until) ? Math.max(0, until - Date.now()) : 0;
3273
+ await response.body?.cancel();
3274
+ throw new RetryableAuthError(
3275
+ "CLI authentication is temporarily unavailable.",
3276
+ retryAfterMs
3277
+ );
3278
+ }
3279
+ if (!response.ok) {
3280
+ await response.body?.cancel();
3261
3281
  throw new Error(
3262
3282
  response.status === 404 ? "CLI login is not enabled on this deployment yet." : "CLI authentication was refused or expired. Run tc login again."
3263
3283
  );
3284
+ }
3264
3285
  if (!response.body)
3265
3286
  throw new Error(
3266
3287
  "The CLI authentication service returned an invalid response."
@@ -3270,7 +3291,11 @@ async function jsonRequest(issuer, path, init = {}) {
3270
3291
  const parts = [];
3271
3292
  try {
3272
3293
  while (true) {
3273
- const part = await reader.read();
3294
+ const part = await reader.read().catch(() => {
3295
+ throw new RetryableAuthError(
3296
+ "CLI authentication response was interrupted."
3297
+ );
3298
+ });
3274
3299
  if (part.done) break;
3275
3300
  size += part.value.length;
3276
3301
  if (size > 32768) {
@@ -3283,7 +3308,9 @@ async function jsonRequest(issuer, path, init = {}) {
3283
3308
  if (!value || typeof value !== "object" || Array.isArray(value))
3284
3309
  throw new Error();
3285
3310
  return value;
3286
- } catch {
3311
+ } catch (error) {
3312
+ if (init.signal?.aborted) throw init.signal.reason;
3313
+ if (error instanceof RetryableAuthError) throw error;
3287
3314
  throw new Error(
3288
3315
  "The CLI authentication service returned an invalid response."
3289
3316
  );
@@ -3298,13 +3325,20 @@ function form(values) {
3298
3325
  body: new URLSearchParams(values)
3299
3326
  };
3300
3327
  }
3301
- function tokens(value) {
3328
+ function tokens(value, device = false) {
3302
3329
  if (typeof value.access_token !== "string" || !/^tc_cli_[A-Za-z0-9_-]{43}$/.test(value.access_token) || typeof value.refresh_token !== "string" || !/^tc_refresh_[A-Za-z0-9_-]{43}$/.test(value.refresh_token) || value.token_type !== "Bearer" || typeof value.expires_in !== "number" || !Number.isFinite(value.expires_in) || value.expires_in <= 0 || value.expires_in > 86400)
3303
3330
  throw new Error("The service did not return a valid CLI session.");
3331
+ const now = Date.now();
3332
+ let expiresAt = now + value.expires_in * 1e3;
3333
+ if (device) {
3334
+ if (typeof value.access_expires_at !== "number" || !Number.isFinite(value.access_expires_at) || value.access_expires_at <= now || value.access_expires_at > now + 86400 * 1e3)
3335
+ throw new Error("The service did not return a valid CLI session expiry.");
3336
+ expiresAt = value.access_expires_at;
3337
+ }
3304
3338
  return {
3305
3339
  accessToken: value.access_token,
3306
3340
  refreshToken: value.refresh_token,
3307
- expiresAt: Date.now() + value.expires_in * 1e3
3341
+ expiresAt
3308
3342
  };
3309
3343
  }
3310
3344
  async function identity(issuer, accessToken, signal) {
@@ -3346,125 +3380,147 @@ async function revoke(session) {
3346
3380
  "Could not revoke this connection. Retry logout or revoke it on the Tokens page."
3347
3381
  );
3348
3382
  }
3383
+ function loginOrigin(value) {
3384
+ const origin = apiOrigin(value);
3385
+ if (origin.startsWith("http:") && !/^http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\/?$/i.test(value))
3386
+ throw new Error(
3387
+ "API URL must be an HTTPS origin or literal loopback development origin."
3388
+ );
3389
+ return origin;
3390
+ }
3391
+ function verificationUrl(value, requestId) {
3392
+ try {
3393
+ if (typeof value !== "string") throw new Error();
3394
+ const url = new URL(value);
3395
+ loginOrigin(value.slice(0, value.indexOf("/connect/cli")));
3396
+ if (url.username || url.password || url.hash || url.pathname !== "/connect/cli" || url.search !== `?request=${requestId}`)
3397
+ throw new Error();
3398
+ return url.toString();
3399
+ } catch {
3400
+ throw new Error(
3401
+ "The service returned an invalid browser verification URL."
3402
+ );
3403
+ }
3404
+ }
3349
3405
  async function login(options = {}, environment = process.env) {
3350
- const issuer = apiOrigin(
3406
+ const issuer = loginOrigin(
3351
3407
  options.apiUrl ?? environment.TRILLIONCORE_CLI_API_URL ?? environment.TRILLIONCORE_API_URL ?? DEFAULT_API
3352
3408
  );
3353
3409
  const abort = new AbortController();
3354
- const state = randomBytes2(32).toString("base64url");
3355
3410
  const verifier = randomBytes2(32).toString("base64url");
3356
- let receive;
3357
- let rejectCallback;
3358
- const callback = new Promise((resolve, reject) => {
3359
- receive = resolve;
3360
- rejectCallback = reject;
3361
- });
3362
- void callback.catch(() => {
3363
- });
3364
- let expectedHost = "";
3365
- const server = createServer((request2, response) => {
3366
- response.setHeader("Cache-Control", "no-store");
3367
- response.setHeader("Referrer-Policy", "no-referrer");
3368
- let url;
3411
+ const alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
3412
+ const userCode = Array.from(
3413
+ { length: 8 },
3414
+ () => alphabet[randomInt(alphabet.length)]
3415
+ ).join("");
3416
+ const started = Date.now();
3417
+ const timeout = options.timeoutMs ?? 3e5;
3418
+ if (!Number.isFinite(timeout) || timeout <= 0)
3419
+ throw new Error("Invalid sign-in timeout.");
3420
+ let deadline = started + Math.min(timeout, 3e5);
3421
+ let requestId;
3422
+ let persisted = false;
3423
+ const stop = (message) => abort.abort(new Error(message));
3424
+ const cancel = () => {
3425
+ if (!persisted) stop("Sign-in cancelled.");
3426
+ };
3427
+ process.on("SIGINT", cancel);
3428
+ let timer = setTimeout(
3429
+ () => stop("Sign-in timed out. Run tc login again."),
3430
+ deadline - Date.now()
3431
+ );
3432
+ const check = () => {
3433
+ if (Date.now() >= deadline) stop("Sign-in timed out. Run tc login again.");
3434
+ abort.signal.throwIfAborted();
3435
+ };
3436
+ const wait = async (milliseconds) => {
3437
+ check();
3369
3438
  try {
3370
- url = new URL(request2.url ?? "/", `http://${expectedHost}`);
3439
+ await delay(Math.min(milliseconds, deadline - Date.now()), void 0, {
3440
+ signal: abort.signal
3441
+ });
3371
3442
  } catch {
3372
- response.writeHead(400).end("Invalid sign-in response.");
3373
- return;
3443
+ abort.signal.throwIfAborted();
3374
3444
  }
3375
- if (request2.method !== "GET" || request2.headers.host !== expectedHost || url.pathname !== "/callback") {
3376
- response.writeHead(404).end();
3377
- return;
3378
- }
3379
- if ([...url.searchParams.keys()].some(
3380
- (key) => url.searchParams.getAll(key).length !== 1
3381
- ) || url.searchParams.get("state") !== state || url.searchParams.get("iss") !== issuer) {
3382
- response.writeHead(400).end("Invalid sign-in response.");
3383
- return;
3384
- }
3385
- if (url.searchParams.has("error")) {
3386
- response.end("Sign-in cancelled. Return to your terminal.");
3387
- rejectCallback(new Error("Sign-in was not approved."));
3388
- return;
3389
- }
3390
- if (!/^[A-Za-z0-9_-]{43}$/.test(url.searchParams.get("code") ?? "")) {
3391
- response.writeHead(400).end("Invalid sign-in response.");
3392
- return;
3393
- }
3394
- response.end(
3395
- "Sign-in response received. Return to your terminal to confirm completion."
3396
- );
3397
- receive(url);
3398
- });
3399
- const stop = (message) => {
3400
- const error = new Error(message);
3401
- abort.abort(error);
3402
- rejectCallback(error);
3445
+ check();
3403
3446
  };
3404
- const cancel = () => stop("Sign-in cancelled.");
3405
- process.once("SIGINT", cancel);
3406
- const timer = setTimeout(
3407
- () => stop("Sign-in timed out. Run tc login again."),
3408
- options.timeoutMs ?? 3e5
3409
- );
3447
+ const post = (operation, body, signal) => jsonRequest(issuer, `/cli/oauth/device/${operation}`, {
3448
+ method: "POST",
3449
+ headers: { "content-type": "application/json" },
3450
+ body: JSON.stringify(body),
3451
+ ...signal ? { signal } : {}
3452
+ });
3410
3453
  try {
3411
- await new Promise((resolve, reject) => {
3412
- server.once("error", reject);
3413
- server.listen(0, "127.0.0.1", resolve);
3414
- });
3415
- const address = server.address();
3416
- if (!address || typeof address === "string")
3417
- throw new Error("Unable to start local sign-in listener.");
3418
- expectedHost = `127.0.0.1:${address.port}`;
3419
- const redirectUri = `http://${expectedHost}/callback`;
3420
- const client = await jsonRequest(issuer, "/cli/oauth/register", {
3421
- signal: abort.signal,
3422
- method: "POST",
3423
- headers: { "content-type": "application/json" },
3424
- body: JSON.stringify({
3425
- client_name: "Trillioncore CLI",
3426
- redirect_uris: [redirectUri],
3427
- token_endpoint_auth_method: "none"
3428
- })
3429
- });
3430
- if (typeof client.client_id !== "string" || !/^cli_[a-f0-9]{32}$/.test(client.client_id))
3431
- throw new Error("The service returned an invalid CLI client.");
3432
- const authorization = new URL(`${issuer}/cli/oauth/authorize`);
3433
- authorization.search = new URLSearchParams({
3434
- client_id: client.client_id,
3435
- redirect_uri: redirectUri,
3436
- response_type: "code",
3437
- code_challenge_method: "S256",
3438
- code_challenge: createHash("sha256").update(verifier).digest("base64url"),
3439
- resource: `${issuer}/cli`,
3440
- scope: "cli:access offline_access",
3441
- state
3442
- }).toString();
3454
+ const registration = await post(
3455
+ "register",
3456
+ {
3457
+ verifier_digest: createHash("sha256").update(verifier).digest("hex"),
3458
+ user_code_digest: createHash("sha256").update(userCode).digest("hex")
3459
+ },
3460
+ abort.signal
3461
+ );
3462
+ if (typeof registration.request_id !== "string" || !deviceSecret.test(registration.request_id))
3463
+ throw new Error("The service returned an invalid device request.");
3464
+ requestId = registration.request_id;
3465
+ if (registration.client_id !== "cli_device" || typeof registration.expires_in !== "number" || !Number.isInteger(registration.expires_in) || registration.expires_in <= 0 || registration.expires_in > 300 || typeof registration.interval !== "number" || !Number.isInteger(registration.interval) || registration.interval <= 0 || registration.interval > 30)
3466
+ throw new Error("The service returned an invalid device request.");
3467
+ deadline = Math.min(deadline, started + registration.expires_in * 1e3);
3468
+ clearTimeout(timer);
3469
+ check();
3470
+ timer = setTimeout(
3471
+ () => stop("Sign-in timed out. Run tc login again."),
3472
+ deadline - Date.now()
3473
+ );
3474
+ const authorization = verificationUrl(
3475
+ registration.verification_uri,
3476
+ requestId
3477
+ );
3443
3478
  if (options.onAuthorizationUrl)
3444
- await options.onAuthorizationUrl(authorization.toString());
3479
+ await options.onAuthorizationUrl(authorization, userCode);
3445
3480
  else {
3446
3481
  console.log(
3447
- `Sign in and approve the organization/accounts in your browser:
3482
+ `Open this URL in a browser on any device:
3448
3483
  ${authorization}
3449
- Do not share this sign-in link. The browser must reach this computer's loopback listener.`
3484
+ Enter this separate code: ${userCode}
3485
+ Only approve a login you started. Keep the code private.`
3450
3486
  );
3451
- if (!options.noBrowser) openBrowser(authorization.toString());
3487
+ if (!options.noBrowser) openBrowser(authorization);
3452
3488
  }
3453
- const approved = await callback;
3454
- const fresh = tokens(
3455
- await jsonRequest(issuer, "/cli/oauth/token", {
3456
- ...form({
3457
- grant_type: "authorization_code",
3458
- client_id: client.client_id,
3459
- redirect_uri: redirectUri,
3460
- code: approved.searchParams.get("code"),
3461
- code_verifier: verifier,
3462
- resource: `${issuer}/cli`
3463
- }),
3464
- signal: abort.signal
3465
- })
3466
- );
3467
- const credentials = { issuer, clientId: client.client_id, ...fresh };
3489
+ let approved;
3490
+ let attempts = 0;
3491
+ const interval = registration.interval * 1e3;
3492
+ while (true) {
3493
+ check();
3494
+ try {
3495
+ approved = await post(
3496
+ "poll",
3497
+ { request_id: requestId, verifier },
3498
+ abort.signal
3499
+ );
3500
+ check();
3501
+ if (approved.status !== "pending") break;
3502
+ attempts = 0;
3503
+ await wait(interval);
3504
+ } catch (error) {
3505
+ check();
3506
+ if (!(error instanceof RetryableAuthError)) throw error;
3507
+ const backoff = Math.min(
3508
+ 1e4,
3509
+ interval * 2 ** Math.min(attempts++, 4)
3510
+ );
3511
+ await wait(
3512
+ Math.max(
3513
+ interval,
3514
+ backoff * (1 + Math.random() * 0.2),
3515
+ error.retryAfterMs
3516
+ )
3517
+ );
3518
+ }
3519
+ }
3520
+ if (approved.client_id !== "cli_device" || typeof approved.delivery_receipt !== "string" || !deviceSecret.test(approved.delivery_receipt))
3521
+ throw new Error("The service returned an invalid device delivery.");
3522
+ const fresh = tokens(approved, true);
3523
+ const credentials = { issuer, clientId: "cli_device", ...fresh };
3468
3524
  try {
3469
3525
  const who = await identity(issuer, fresh.accessToken, abort.signal);
3470
3526
  const value = {
@@ -3476,8 +3532,10 @@ Do not share this sign-in link. The browser must reach this computer's loopback
3476
3532
  const store = sessionStore(environment);
3477
3533
  await store.withLock(async () => {
3478
3534
  const previous = await store.read().catch(() => null);
3479
- abort.signal.throwIfAborted();
3535
+ check();
3480
3536
  await store.write(value);
3537
+ persisted = true;
3538
+ clearTimeout(timer);
3481
3539
  if (previous)
3482
3540
  await revoke(previous).catch(
3483
3541
  () => console.error(
@@ -3485,20 +3543,31 @@ Do not share this sign-in link. The browser must reach this computer's loopback
3485
3543
  )
3486
3544
  );
3487
3545
  });
3488
- return { issuer, ...who };
3489
- } catch (error) {
3490
- await revoke(credentials).catch(
3546
+ await post("acknowledge", {
3547
+ request_id: requestId,
3548
+ verifier,
3549
+ delivery_receipt: approved.delivery_receipt
3550
+ }).catch(
3491
3551
  () => console.error(
3492
- "Incomplete sign-in could not be revoked. Revoke the unused connection on the Tokens page."
3552
+ "Signed in, but delivery acknowledgement failed. The temporary delivery expires automatically."
3493
3553
  )
3494
3554
  );
3555
+ return { issuer, ...who };
3556
+ } catch (error) {
3557
+ if (!persisted)
3558
+ await revoke(credentials).catch(
3559
+ () => console.error(
3560
+ "Incomplete sign-in could not be revoked. Revoke the unused connection on the Tokens page."
3561
+ )
3562
+ );
3495
3563
  throw error;
3496
3564
  }
3497
3565
  } finally {
3498
3566
  clearTimeout(timer);
3567
+ if (requestId && !persisted)
3568
+ await post("cancel", { request_id: requestId, verifier }).catch(() => {
3569
+ });
3499
3570
  process.removeListener("SIGINT", cancel);
3500
- server.closeAllConnections();
3501
- await new Promise((resolve) => server.close(() => resolve()));
3502
3571
  }
3503
3572
  }
3504
3573
  async function authenticatedEnvironment(environment = process.env) {
@@ -3853,6 +3922,11 @@ function renderIndex(result) {
3853
3922
  return [
3854
3923
  `Resources: ${catalog.connectionRef}`,
3855
3924
  `Last successful sync: ${catalog.freshness.lastSuccessfulSyncAt ?? "unknown"}; source completeness: ${catalog.freshness.sourceCompleteness}`,
3925
+ ...catalog.lifecycle ? [
3926
+ `Publication: ${catalog.lifecycle.publication}; source removals: ${catalog.lifecycle.sourceRemovals}`,
3927
+ ...[["Active run", catalog.lifecycle.activeRun], ["Last finished run", catalog.lifecycle.lastFinishedRun]].map(([label, run]) => run ? `${label}: ${run.status}; recorded at: ${run.startedAt}; finished: ${run.completedAt ?? "not recorded"}; attempts: ${run.attemptCount}; pages: ${run.progress.pagesProcessed}; seen: ${run.progress.recordsSeen}; upserted: ${run.progress.recordsUpserted}; skipped: ${run.progress.recordsSkipped}` : `${label}: none recorded`),
3928
+ "Run state is recorded metadata, not worker health. Completed does not mean full source coverage."
3929
+ ] : [],
3856
3930
  ...catalog.resources.flatMap((resource) => [
3857
3931
  `${resource.name} (${resource.kind})`,
3858
3932
  ...resource.recordKinds ? [` Observed record kinds: ${resource.recordKinds.join(", ") || "none"}`] : [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trillioncore/cli",
3
- "version": "1.0.0-next.3",
3
+ "version": "1.0.0-next.5",
4
4
  "description": "Trillioncore v1 CLI: sign in and read your authorized integration records",
5
5
  "type": "module",
6
6
  "license": "MIT",