@apso/cli 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ class Deploy extends base_command_1.default {
23
23
  this.error("Project not linked. Run 'apso link' first.");
24
24
  }
25
25
  // Fetch service
26
- const service = await services_1.servicesApi.get(link.workspaceSlug, link.serviceSlug);
26
+ const service = await services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
27
27
  // Run migration sandbox (unless skipped)
28
28
  let migrationCount = 0;
29
29
  if (!flags["skip-migrate"]) {
@@ -60,7 +60,7 @@ class Init extends base_command_1.default {
60
60
  },
61
61
  ]);
62
62
  // Select service
63
- const servicesResponse = await services_1.servicesApi.list(workspace.slug);
63
+ const servicesResponse = await services_1.servicesApi.list(workspace.id);
64
64
  if (servicesResponse.data.length === 0) {
65
65
  this.error(`No services found in workspace "${workspace.name}". Create one first or choose "Create a new project".`);
66
66
  }
@@ -177,7 +177,7 @@ class Init extends base_command_1.default {
177
177
  // Create service on platform
178
178
  this.log(`Creating service "${projectName}" in workspace "${workspace.name}"...`);
179
179
  const serviceName = projectName;
180
- const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.slug, {
180
+ const service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, {
181
181
  name: serviceName,
182
182
  }));
183
183
  // Clone template
@@ -63,10 +63,10 @@ class Link extends base_command_1.default {
63
63
  // Determine service
64
64
  let service;
65
65
  if (flags.service) {
66
- service = await services_1.servicesApi.get(workspace.slug, flags.service);
66
+ service = await services_1.servicesApi.get(workspace.id, flags.service);
67
67
  }
68
68
  else {
69
- const servicesResponse = await services_1.servicesApi.list(workspace.slug);
69
+ const servicesResponse = await services_1.servicesApi.list(workspace.id);
70
70
  const services = servicesResponse.data;
71
71
  const choices = [
72
72
  ...services.map((svc) => ({
@@ -97,7 +97,7 @@ class Link extends base_command_1.default {
97
97
  },
98
98
  ]);
99
99
  this.log(`Creating service "${serviceName}"...`);
100
- service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.slug, {
100
+ service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, {
101
101
  name: serviceName,
102
102
  }));
103
103
  }
@@ -298,9 +298,9 @@ class Login extends base_command_1.default {
298
298
  this.log(`Successfully logged in as ${tokenResponse.user.email}`);
299
299
  this.log("");
300
300
  this.log("Next steps:");
301
- this.log(" apso whoami - View your account info");
302
- this.log(" apso services - List your services");
303
- this.log(" apso tui - Launch interactive UI");
301
+ this.log(" apso use - Select your workspace");
302
+ this.log(" apso whoami - View your account + workspace");
303
+ this.log(" apso projects - List services in a workspace");
304
304
  }
305
305
  catch (error) {
306
306
  if (error instanceof Error) {
@@ -13,17 +13,28 @@ class Projects extends base_command_1.default {
13
13
  if (!config_1.credentials.isValid()) {
14
14
  this.error("Not logged in. Run 'apso login' first.");
15
15
  }
16
- // Determine workspace
16
+ // Determine workspace. The BFF scopes services by the NUMERIC workspace id,
17
+ // so resolve slugs to an id before listing.
18
+ let workspaceId;
17
19
  let workspaceSlug;
18
20
  if (flags.workspace) {
19
- workspaceSlug = flags.workspace;
21
+ const ws = await services_1.workspacesApi.get(flags.workspace);
22
+ workspaceId = String(ws.id);
23
+ workspaceSlug = ws.slug;
20
24
  }
21
25
  else {
22
- // Try from existing link
26
+ // Prefer an existing link, then the active workspace set via `apso use`,
27
+ // and only prompt as a last resort.
23
28
  const link = config_1.projectLink.read();
29
+ const active = config_1.globalConfig.read();
24
30
  if (link) {
31
+ workspaceId = String(link.workspaceId);
25
32
  workspaceSlug = link.workspaceSlug;
26
33
  }
34
+ else if (active.activeWorkspaceId) {
35
+ workspaceId = active.activeWorkspaceId;
36
+ workspaceSlug = active.activeWorkspaceSlug || active.activeWorkspaceId;
37
+ }
27
38
  else {
28
39
  // Prompt
29
40
  const workspaces = await services_1.workspacesApi.list();
@@ -41,11 +52,12 @@ class Projects extends base_command_1.default {
41
52
  })),
42
53
  },
43
54
  ]);
55
+ workspaceId = String(response.workspace.id);
44
56
  workspaceSlug = response.workspace.slug;
45
57
  }
46
58
  }
47
- // Fetch services
48
- const servicesResponse = await services_1.servicesApi.list(workspaceSlug);
59
+ // Fetch services (scoped by numeric workspace id)
60
+ const servicesResponse = await services_1.servicesApi.list(workspaceId);
49
61
  const services = servicesResponse.data;
50
62
  if (services.length === 0) {
51
63
  this.log(`No services found in workspace "${workspaceSlug}".`);
@@ -16,7 +16,7 @@ class Status extends base_command_1.default {
16
16
  this.error("Project not linked. Run 'apso link' first.");
17
17
  }
18
18
  // Fetch service
19
- const service = await services_1.servicesApi.get(link.workspaceSlug, link.serviceSlug);
19
+ const service = await services_1.servicesApi.get(link.workspaceId, link.serviceSlug);
20
20
  this.log(`Service: ${service.name} (${service.slug})`);
21
21
  this.log(`Workspace: ${link.workspaceSlug}`);
22
22
  this.log(`Status: ${service.status}`);
@@ -0,0 +1,14 @@
1
+ import BaseCommand from "../lib/base-command";
2
+ /**
3
+ * Select the active workspace for subsequent CLI commands. With no argument it
4
+ * shows an interactive picker; pass a slug/name to set it directly. The choice
5
+ * persists to ~/.apso/config.json and scopes every workspace-aware command.
6
+ */
7
+ export default class Use extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static args: {
11
+ workspace: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
6
+ const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
7
+ const config_1 = require("../lib/config");
8
+ const services_1 = require("../lib/api/services");
9
+ /**
10
+ * Select the active workspace for subsequent CLI commands. With no argument it
11
+ * shows an interactive picker; pass a slug/name to set it directly. The choice
12
+ * persists to ~/.apso/config.json and scopes every workspace-aware command.
13
+ */
14
+ class Use extends base_command_1.default {
15
+ async run() {
16
+ const { args } = await this.parse(Use);
17
+ if (!config_1.credentials.isValid()) {
18
+ this.error("Not logged in. Run 'apso login' first.");
19
+ }
20
+ const workspaces = await services_1.workspacesApi.list();
21
+ if (workspaces.length === 0) {
22
+ this.error("No workspaces found. Create one at https://app.apso.cloud first.");
23
+ }
24
+ let ws;
25
+ if (args.workspace) {
26
+ ws = workspaces.find((w) => w.slug === args.workspace || w.name === args.workspace);
27
+ if (!ws) {
28
+ this.error(`Workspace not found: ${args.workspace}\nRun 'apso use' to pick from a list.`);
29
+ }
30
+ }
31
+ else if (workspaces.length === 1) {
32
+ ws = workspaces[0];
33
+ }
34
+ else {
35
+ const answer = await inquirer_1.default.prompt([
36
+ {
37
+ type: "list",
38
+ name: "workspace",
39
+ message: "Select your workspace:",
40
+ choices: workspaces.map((w) => ({
41
+ name: `${w.name} (${w.slug})`,
42
+ value: w,
43
+ })),
44
+ },
45
+ ]);
46
+ ws = answer.workspace;
47
+ }
48
+ config_1.globalConfig.write({
49
+ activeWorkspaceId: String(ws.id),
50
+ activeWorkspaceSlug: ws.slug,
51
+ activeWorkspaceName: ws.name,
52
+ });
53
+ this.log("");
54
+ this.log(` Now using workspace: ${ws.name} (${ws.slug})`);
55
+ // Show what's in the workspace so the next step is obvious.
56
+ try {
57
+ const services = (await services_1.servicesApi.list(ws.id)).data;
58
+ this.log("");
59
+ if (services.length > 0) {
60
+ this.log(" Services:");
61
+ for (const s of services) {
62
+ this.log(` - ${s.name} (${s.slug})`);
63
+ }
64
+ this.log("");
65
+ this.log(" Run 'apso link' inside a project to work with one.");
66
+ }
67
+ else {
68
+ this.log(" No services yet.");
69
+ this.log(" Run 'apso init' to scaffold one, then 'apso deploy' to ship it.");
70
+ }
71
+ }
72
+ catch {
73
+ // listing is a convenience; never fail `use` over it
74
+ }
75
+ this.log("");
76
+ }
77
+ }
78
+ Use.description = "Select the active workspace for CLI commands";
79
+ Use.examples = ["$ apso use", "$ apso use my-workspace"];
80
+ Use.args = {
81
+ workspace: core_1.Args.string({
82
+ description: "Workspace slug or name (skips the picker)",
83
+ required: false,
84
+ }),
85
+ };
86
+ exports.default = Use;
@@ -71,12 +71,14 @@ class Whoami extends base_command_1.default {
71
71
  this.log(` Workspace: ${link.workspaceSlug}`);
72
72
  this.log(` Service: ${link.serviceSlug}`);
73
73
  }
74
- else if (config.defaultWorkspace) {
75
- this.log(` Workspace: ${config.defaultWorkspace} (default)`);
76
- this.log(" Service: None (run 'apso use <service>' to set)");
74
+ else if (config.activeWorkspaceName || config.activeWorkspaceSlug) {
75
+ const name = config.activeWorkspaceName || config.activeWorkspaceSlug;
76
+ const slug = config.activeWorkspaceSlug;
77
+ this.log(` Workspace: ${name}${slug ? ` (${slug})` : ""}`);
78
+ this.log(" Service: None (run 'apso link' in a project to set)");
77
79
  }
78
80
  else {
79
- this.log(" Workspace: None (run 'apso use <service>' to set)");
81
+ this.log(" Workspace: None (run 'apso use' to select one)");
80
82
  this.log(" Service: None");
81
83
  }
82
84
  this.log("");
@@ -0,0 +1,7 @@
1
+ import { Hook } from "@oclif/core";
2
+ /**
3
+ * Runs before any command. Initializes PostHog + Sentry and records that a
4
+ * command was invoked. Best-effort: never blocks or fails the command.
5
+ */
6
+ declare const hook: Hook<"init">;
7
+ export default hook;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const telemetry_1 = require("../../lib/telemetry/telemetry");
4
+ /**
5
+ * Runs before any command. Initializes PostHog + Sentry and records that a
6
+ * command was invoked. Best-effort: never blocks or fails the command.
7
+ */
8
+ const hook = async function (opts) {
9
+ (0, telemetry_1.initTelemetry)();
10
+ if (opts.id) {
11
+ (0, telemetry_1.track)("cli_command_started", { command: opts.id });
12
+ }
13
+ };
14
+ exports.default = hook;
@@ -0,0 +1,8 @@
1
+ import { Hook } from "@oclif/core";
2
+ /**
3
+ * Runs after a command completes successfully. Records completion + duration,
4
+ * then flushes both clients before the short-lived CLI process exits (posthog
5
+ * batches, so an explicit flush is required or events are lost).
6
+ */
7
+ declare const hook: Hook<"postrun">;
8
+ export default hook;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const telemetry_1 = require("../../lib/telemetry/telemetry");
4
+ /**
5
+ * Runs after a command completes successfully. Records completion + duration,
6
+ * then flushes both clients before the short-lived CLI process exits (posthog
7
+ * batches, so an explicit flush is required or events are lost).
8
+ */
9
+ const hook = async function (opts) {
10
+ var _a;
11
+ (0, telemetry_1.track)("cli_command_completed", {
12
+ command: (_a = opts.Command) === null || _a === void 0 ? void 0 : _a.id,
13
+ duration_ms: (0, telemetry_1.elapsedMs)(),
14
+ success: true,
15
+ });
16
+ await (0, telemetry_1.shutdownTelemetry)();
17
+ };
18
+ exports.default = hook;
@@ -225,7 +225,9 @@ async function apiRequest(path, options = {}) {
225
225
  // Resolve the active workspace id from, in order: an explicit header the
226
226
  // caller passed, the APSO_WORKSPACE_ID env override, or the linked project.
227
227
  if (!requestHeaders["X-Workspace-Id"]) {
228
- const activeWorkspaceId = process.env.APSO_WORKSPACE_ID || ((_a = config_1.projectLink.read()) === null || _a === void 0 ? void 0 : _a.workspaceId);
228
+ const activeWorkspaceId = process.env.APSO_WORKSPACE_ID ||
229
+ ((_a = config_1.projectLink.read()) === null || _a === void 0 ? void 0 : _a.workspaceId) ||
230
+ config_1.globalConfig.read().activeWorkspaceId;
229
231
  if (activeWorkspaceId) {
230
232
  requestHeaders["X-Workspace-Id"] = String(activeWorkspaceId);
231
233
  }
@@ -37,15 +37,15 @@ export declare const workspacesApi: {
37
37
  * Services API
38
38
  */
39
39
  export declare const servicesApi: {
40
- list(_workspaceSlug?: string, options?: {
40
+ list(workspaceId?: string | number, options?: {
41
41
  status?: string;
42
42
  page?: number;
43
43
  pageSize?: number;
44
44
  }): Promise<PaginatedResponse<Service>>;
45
45
  getById(id: string): Promise<Service>;
46
- /** Resolve (workspace, serviceSlug) to a service via the workspace-scoped list. */
47
- get(_workspaceSlug: string, serviceSlug: string): Promise<Service>;
48
- create(_workspaceSlug: string, data: {
46
+ /** Resolve (workspaceId, serviceSlug) to a service via the workspace-scoped list. */
47
+ get(workspaceId: string, serviceSlug: string): Promise<Service>;
48
+ create(workspaceId: string, data: {
49
49
  name: string;
50
50
  slug?: string;
51
51
  description?: string;
@@ -128,30 +128,39 @@ exports.workspacesApi = {
128
128
  * Services API
129
129
  */
130
130
  exports.servicesApi = {
131
- async list(_workspaceSlug, options) {
131
+ async list(workspaceId, options) {
132
132
  const params = {};
133
133
  if (options === null || options === void 0 ? void 0 : options.pageSize)
134
134
  params.limit = options.pageSize;
135
135
  if ((options === null || options === void 0 ? void 0 : options.page) && (options === null || options === void 0 ? void 0 : options.pageSize)) {
136
136
  params.offset = (options.page - 1) * options.pageSize;
137
137
  }
138
- const raw = await client_1.api.get("/api/services", { params });
138
+ // The BFF scopes /api/services by the NUMERIC workspace id in the
139
+ // X-Workspace-Id header (not the slug). Pass it explicitly so the call
140
+ // works even before a project is linked.
141
+ const headers = workspaceId
142
+ ? { "X-Workspace-Id": String(workspaceId) }
143
+ : undefined;
144
+ const raw = await client_1.api.get("/api/services", { params, headers });
139
145
  return adaptPaginated(raw, adaptService);
140
146
  },
141
147
  async getById(id) {
142
148
  const raw = await client_1.api.get(`/api/services/${id}`);
143
149
  return adaptService(raw);
144
150
  },
145
- /** Resolve (workspace, serviceSlug) to a service via the workspace-scoped list. */
146
- async get(_workspaceSlug, serviceSlug) {
147
- const list = await this.list();
151
+ /** Resolve (workspaceId, serviceSlug) to a service via the workspace-scoped list. */
152
+ async get(workspaceId, serviceSlug) {
153
+ const list = await this.list(workspaceId);
148
154
  const match = list.data.find((s) => s.slug === serviceSlug);
149
155
  if (!match)
150
156
  throw new Error(`Service not found: ${serviceSlug}`);
151
157
  return match;
152
158
  },
153
- async create(_workspaceSlug, data) {
154
- const raw = await client_1.api.post("/api/services", { name: data.name });
159
+ async create(workspaceId, data) {
160
+ const headers = workspaceId
161
+ ? { "X-Workspace-Id": String(workspaceId) }
162
+ : undefined;
163
+ const raw = await client_1.api.post("/api/services", { name: data.name }, { headers });
155
164
  return adaptService(raw);
156
165
  },
157
166
  async update(_workspaceSlug, serviceSlugOrId, data) {
@@ -1,5 +1,13 @@
1
1
  import { Command } from "@oclif/core";
2
2
  export default abstract class BaseCommand extends Command {
3
+ /**
4
+ * Report failures to Sentry + PostHog before oclif exits. The postrun hook
5
+ * does not fire on error, so the flush has to happen here.
6
+ */
7
+ catch(err: Error & {
8
+ exitCode?: number;
9
+ code?: string;
10
+ }): Promise<any>;
3
11
  runCommand(command: string, args: string[], silent?: boolean): Promise<void>;
4
12
  runNpmCommand(args: string[], silent?: boolean): Promise<void>;
5
13
  }
@@ -4,7 +4,28 @@ const tslib_1 = require("tslib");
4
4
  const core_1 = require("@oclif/core");
5
5
  const child_process_1 = require("child_process");
6
6
  const os_1 = tslib_1.__importDefault(require("os"));
7
+ const telemetry_1 = require("./telemetry/telemetry");
7
8
  class BaseCommand extends core_1.Command {
9
+ /**
10
+ * Report failures to Sentry + PostHog before oclif exits. The postrun hook
11
+ * does not fire on error, so the flush has to happen here.
12
+ */
13
+ async catch(err) {
14
+ try {
15
+ (0, telemetry_1.captureException)(err, { command: this.id });
16
+ (0, telemetry_1.track)("cli_command_failed", {
17
+ command: this.id,
18
+ duration_ms: (0, telemetry_1.elapsedMs)(),
19
+ error: err === null || err === void 0 ? void 0 : err.message,
20
+ error_code: err === null || err === void 0 ? void 0 : err.code,
21
+ });
22
+ await (0, telemetry_1.shutdownTelemetry)();
23
+ }
24
+ catch {
25
+ // never mask the original error with a telemetry failure
26
+ }
27
+ return super.catch(err);
28
+ }
8
29
  async runCommand(command, args, silent = false) {
9
30
  return new Promise((resolve, reject) => {
10
31
  const isWindows = os_1.default.platform() === "win32";
@@ -72,6 +72,15 @@ exports.globalConfig = {
72
72
  if (fileConfig) {
73
73
  Object.assign(config, fileConfig);
74
74
  }
75
+ // Auto-migrate legacy apso.ai hosts written by an older CLI. Without this a
76
+ // saved config keeps pointing login/API at the retired app.apso.ai/
77
+ // api.apso.ai instead of the current apso.cloud defaults.
78
+ if (config.apiUrl && /(?:app|api)\.apso\.ai\b/.test(config.apiUrl)) {
79
+ config.apiUrl = types_1.DEFAULT_GLOBAL_CONFIG.apiUrl;
80
+ }
81
+ if (config.webUrl && /(?:app|api)\.apso\.ai\b/.test(config.webUrl)) {
82
+ config.webUrl = types_1.DEFAULT_GLOBAL_CONFIG.webUrl;
83
+ }
75
84
  // Override with environment variables
76
85
  if (process.env.APSO_API_URL) {
77
86
  config.apiUrl = process.env.APSO_API_URL;
@@ -96,6 +96,13 @@ export interface GlobalConfigFile {
96
96
  * Default workspace slug to use
97
97
  */
98
98
  defaultWorkspace?: string;
99
+ /**
100
+ * Active workspace, set via `apso use`. The numeric id scopes API calls
101
+ * (X-Workspace-Id); slug/name are for display.
102
+ */
103
+ activeWorkspaceId?: string;
104
+ activeWorkspaceSlug?: string;
105
+ activeWorkspaceName?: string;
99
106
  /**
100
107
  * Platform API base URL
101
108
  */
package/dist/lib/git.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare function setOrigin(cwd: string, repoUrl: string): void;
18
18
  export declare function hasUncommittedChanges(cwd: string): boolean;
19
19
  /** Stage everything and commit, unless there is nothing to commit. */
20
20
  export declare function commitAll(cwd: string, message: string): boolean;
21
+ /** Attribution trailer appended to every commit the CLI creates. */
22
+ export declare const APSO_COMMIT_TRAILER = "Generated with Apso (https://apso.ai)";
21
23
  /** Push the current HEAD to origin/<branch>, setting upstream. */
22
24
  export declare function pushHead(cwd: string, branch: string): void;
23
25
  export declare function httpsUrlFor(repoFullName: string): string;
package/dist/lib/git.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * deploy time.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.httpsUrlFor = exports.pushHead = exports.commitAll = exports.hasUncommittedChanges = exports.setOrigin = exports.localRepoFullName = exports.parseRepoFullName = exports.getOriginUrl = exports.initRepo = exports.isGitRepo = exports.gitAvailable = void 0;
11
+ exports.httpsUrlFor = exports.pushHead = exports.APSO_COMMIT_TRAILER = exports.commitAll = exports.hasUncommittedChanges = exports.setOrigin = exports.localRepoFullName = exports.parseRepoFullName = exports.getOriginUrl = exports.initRepo = exports.isGitRepo = exports.gitAvailable = void 0;
12
12
  const tslib_1 = require("tslib");
13
13
  const child_process_1 = require("child_process");
14
14
  const fs = tslib_1.__importStar(require("fs"));
@@ -93,10 +93,15 @@ function commitAll(cwd, message) {
93
93
  if (!staged)
94
94
  return false;
95
95
  }
96
- run(`git commit -m ${JSON.stringify(message)}`, cwd);
96
+ // Second -m adds a trailer paragraph attributing the code to Apso. Mirrors
97
+ // the "Co-Authored-By" convention: a machine-visible footprint on every
98
+ // commit the CLI makes.
99
+ run(`git commit -m ${JSON.stringify(message)} -m ${JSON.stringify(exports.APSO_COMMIT_TRAILER)}`, cwd);
97
100
  return true;
98
101
  }
99
102
  exports.commitAll = commitAll;
103
+ /** Attribution trailer appended to every commit the CLI creates. */
104
+ exports.APSO_COMMIT_TRAILER = "Generated with Apso (https://apso.ai)";
100
105
  /** Push the current HEAD to origin/<branch>, setting upstream. */
101
106
  function pushHead(cwd, branch) {
102
107
  run(`git push -u origin HEAD:${branch}`, cwd);
@@ -0,0 +1,21 @@
1
+ declare function isDisabled(telemetryDisabled: boolean): boolean;
2
+ /** Stable, non-PII fallback id for logged-out users (no username, no MAC). */
3
+ declare function anonMachineId(): string;
4
+ /**
5
+ * Initialize telemetry. Call once at process start (init hook). Safe to call
6
+ * when disabled — it just no-ops everything downstream.
7
+ */
8
+ export declare function initTelemetry(): void;
9
+ /** Emit a PostHog event. No-op when disabled. */
10
+ export declare function track(event: string, properties?: Record<string, unknown>): void;
11
+ /** Report an exception to Sentry. No-op when disabled. */
12
+ export declare function captureException(err: unknown, context?: Record<string, unknown>): void;
13
+ /** Milliseconds since the process (CLI invocation) started. */
14
+ export declare function elapsedMs(): number;
15
+ /** Flush + close both clients. Call before the process exits. */
16
+ export declare function shutdownTelemetry(): Promise<void>;
17
+ export declare const __testing: {
18
+ isDisabled: typeof isDisabled;
19
+ anonMachineId: typeof anonMachineId;
20
+ };
21
+ export {};