@toloka-platform/sdk 0.1.0-preview.4 → 0.1.0-preview.6

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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/toloka.js +132 -126
  3. package/dist/cli/toloka.js.jsc +0 -0
  4. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  5. package/dist/cli/toloka.js.sha256 +1 -1
  6. package/docs/cli-v2.md +4 -0
  7. package/docs/cli.md +82 -6
  8. package/docs/datasets.md +8 -9
  9. package/docs/getting-started.md +2 -2
  10. package/docs/limitations.md +5 -8
  11. package/docs/troubleshooting.md +0 -11
  12. package/openapi/gen/api.ts +782 -876
  13. package/openapi/gen/models/baseModel.ts +9 -0
  14. package/openapi/gen/models/baseModelId.ts +2 -0
  15. package/openapi/gen/models/baseModelIdProperty.ts +2 -0
  16. package/openapi/gen/models/createDatasetBody.ts +2 -0
  17. package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
  18. package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
  19. package/openapi/gen/models/createProjectModelBody.ts +8 -0
  20. package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
  21. package/openapi/gen/models/dataset.ts +2 -0
  22. package/openapi/gen/models/datasetProfile.ts +3 -0
  23. package/openapi/gen/models/datasetProfileStamp.ts +6 -0
  24. package/openapi/gen/models/datasetProfileStampName.ts +2 -0
  25. package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  26. package/openapi/gen/models/index.ts +21 -0
  27. package/openapi/gen/models/listBaseModels200.ts +5 -0
  28. package/openapi/gen/models/listModelVersions200.ts +7 -0
  29. package/openapi/gen/models/listModelVersionsParams.ts +5 -0
  30. package/openapi/gen/models/listProjectModels200.ts +7 -0
  31. package/openapi/gen/models/listProjectModelsParams.ts +5 -0
  32. package/openapi/gen/models/model.ts +16 -0
  33. package/openapi/gen/models/modelArchivedAt.ts +2 -0
  34. package/openapi/gen/models/modelDescription.ts +2 -0
  35. package/openapi/gen/models/modelId.ts +2 -0
  36. package/openapi/gen/models/modelVersion.ts +9 -0
  37. package/openapi/gen/models/modelVersionId.ts +2 -0
  38. package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  39. package/openapi/gen/models/runCost.ts +7 -0
  40. package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  41. package/package.json +2 -1
  42. package/skills/catalog.json +22 -0
  43. package/skills/{SKILL.md → toloka-build/SKILL.md} +2 -2
  44. package/skills/{references → toloka-build/references}/workflow-and-cli.md +26 -5
  45. package/skills/toloka-report-issue/SKILL.md +25 -0
  46. package/skills/toloka-report-issue/references/report-template.md +34 -0
  47. package/src/cli/analytics.ts +53 -1
  48. package/src/cli/data.ts +148 -25
  49. package/src/cli/file-system.ts +34 -0
  50. package/src/cli/format-error.ts +19 -0
  51. package/src/cli/init.ts +149 -194
  52. package/src/cli/models.ts +188 -0
  53. package/src/cli/program.ts +202 -22
  54. package/src/cli/report.ts +144 -0
  55. package/src/cli/skill.ts +363 -0
  56. package/src/client.ts +109 -35
  57. package/src/errors.ts +25 -10
  58. package/src/pipeline-graph-v1.contract.json +5 -1
  59. package/src/transport.ts +16 -13
  60. /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
@@ -4,14 +4,13 @@ import { resolve } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { createClient } from "../client";
6
6
  import type { PotatoClient } from "../client";
7
- import { PotatoApiError } from "../errors";
8
7
  import { readTolokaState } from "../state";
9
8
  import type { TolokaState } from "../state";
10
9
  import { authStatus, login, logout } from "./auth";
11
10
  import type { AuthPrompt } from "./auth";
12
11
  import { commandNameFromArgv, createCliAnalytics } from "./analytics";
13
12
  import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
14
- import { PRODUCTION_BASE_URL, resolveTolokaConfig } from "./config";
13
+ import { PRODUCTION_BASE_URL, resolveInternalBaseUrl, resolveTolokaConfig } from "./config";
15
14
  import type { TolokaEnvironment } from "./config";
16
15
  import {
17
16
  findCommandCredentials,
@@ -26,7 +25,7 @@ import {
26
25
  setProjectSecret,
27
26
  uploadDataset,
28
27
  } from "./data";
29
- import { initProject, readAgentSkillStalenessWarning } from "./init";
28
+ import { SDK_VERSION, initProject, readAgentSkillStalenessWarning } from "./init";
30
29
  import {
31
30
  applyProject,
32
31
  importProject,
@@ -34,6 +33,16 @@ import {
34
33
  projectStatus,
35
34
  validateProject,
36
35
  } from "./project";
36
+ import {
37
+ archiveModel,
38
+ createModel,
39
+ getBaseModel,
40
+ getModel,
41
+ listBaseModels,
42
+ listModels,
43
+ listModelVersions,
44
+ parseBaseModelId,
45
+ } from "./models";
37
46
  import {
38
47
  MISSING_PROJECT_ID_MESSAGE,
39
48
  addProject,
@@ -45,6 +54,9 @@ import {
45
54
  switchProject,
46
55
  } from "./project-context";
47
56
  import { estimatePipelineCost } from "./cost";
57
+ import { submitIssueReport } from "./report";
58
+ import type { IssueReportAuthor } from "./report";
59
+ import { formatCliError } from "./format-error";
48
60
  import {
49
61
  cancelPipelineRun,
50
62
  listLivePipelines,
@@ -56,6 +68,7 @@ import {
56
68
  } from "./runs";
57
69
 
58
70
  import type { RunOutcome } from "./runs";
71
+ import { skillInstallCommand, skillListCommand, skillSearchCommand } from "./skill";
59
72
  import { findSourceRoot } from "./source";
60
73
 
61
74
  export type CliIo = {
@@ -88,6 +101,12 @@ const CREDENTIAL_PRECEDENCE = [
88
101
  `The platform defaults to ${PRODUCTION_BASE_URL}.`,
89
102
  ].join("\n");
90
103
 
104
+ const authoredByOption = (raw: string): IssueReportAuthor => {
105
+ if (raw !== "human" && raw !== "agent")
106
+ throw new InvalidArgumentError('Expected "human" or "agent".');
107
+ return raw;
108
+ };
109
+
91
110
  const positiveInteger = (raw: string): number => {
92
111
  const value = Number(raw);
93
112
  if (!Number.isInteger(value) || value <= 0)
@@ -104,6 +123,7 @@ const exitCodeForOutcome = (outcome: RunOutcome): number => {
104
123
  type Resolved = {
105
124
  readonly sourceRoot: string;
106
125
  readonly client: PotatoClient;
126
+ readonly baseUrl: string;
107
127
  readonly state: TolokaState | undefined;
108
128
  readonly stored: StoredCredentials | undefined;
109
129
  readonly projectId?: string;
@@ -154,6 +174,7 @@ const resolveContext = async (
154
174
  return {
155
175
  sourceRoot,
156
176
  client,
177
+ baseUrl: config.baseUrl,
157
178
  state,
158
179
  stored,
159
180
  ...(config.projectId === undefined ? {} : { projectId: config.projectId }),
@@ -187,6 +208,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
187
208
  .description(
188
209
  "Preview. Build and operate Toloka pipeline projects from TypeScript source. The public API may change.",
189
210
  )
211
+ .version(SDK_VERSION, "-v, --version", "output the current version")
190
212
  .option(
191
213
  "--source-root <directory>",
192
214
  "project root; discovered from the working directory when omitted",
@@ -203,19 +225,30 @@ export function createProgram(context: ProgramContext): CreatedProgram {
203
225
 
204
226
  const globals = (): GlobalOptions => program.opts<GlobalOptions>();
205
227
 
206
- program.hook("postAction", async (thisCommand) => {
228
+ program.hook("postAction", async (_thisCommand, actionCommand) => {
207
229
  if (warnedAboutStaleSkills) return;
208
230
  const commandNames = [];
209
- for (let current: Command | null = thisCommand; current !== null; current = current.parent) {
231
+ for (let current: Command | null = actionCommand; current !== null; current = current.parent) {
210
232
  commandNames.push(current.name());
211
233
  }
212
- if (commandNames.includes("init") || commandNames.includes("help")) return;
234
+ if (
235
+ commandNames.includes("init") ||
236
+ commandNames.includes("help") ||
237
+ commandNames.includes("report") ||
238
+ commandNames.includes("url") ||
239
+ commandNames.includes("version")
240
+ )
241
+ return;
213
242
  const sourceRoot = await resolveSourceRoot(globals(), context.io);
214
243
  const warning = await readAgentSkillStalenessWarning(sourceRoot).catch(() => undefined);
215
244
  if (warning === undefined) return;
216
245
  warnedAboutStaleSkills = true;
217
246
  context.io.stderr(warning);
218
247
  });
248
+ program
249
+ .command("version")
250
+ .description("print the current CLI version")
251
+ .action(() => context.io.stdout(SDK_VERSION));
219
252
 
220
253
  program
221
254
  .command("login")
@@ -351,6 +384,19 @@ export function createProgram(context: ProgramContext): CreatedProgram {
351
384
  });
352
385
  if (result.project === undefined) exitCode.value = 1;
353
386
  });
387
+ project
388
+ .command("url")
389
+ .description("print the current project URL")
390
+ .action(async () => {
391
+ const { baseUrl, client, projectId } = await resolveContext(globals(), context);
392
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
393
+ const live = await client.projects.get({ projectId });
394
+ const url = new URL(
395
+ `/teams/${encodeURIComponent(live.teamId)}/projects/${encodeURIComponent(live.id)}`,
396
+ baseUrl,
397
+ );
398
+ context.io.stdout(url.href);
399
+ });
354
400
 
355
401
  project
356
402
  .command("remove")
@@ -390,7 +436,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
390
436
 
391
437
  program
392
438
  .command("init")
393
- .description("initialize this project directory and install agent skills")
439
+ .description("initialize this project directory and install the agent bootstrap")
394
440
  .action(async () => {
395
441
  const flags = globals();
396
442
  const sourceRoot = resolveInitRoot(flags);
@@ -497,6 +543,94 @@ export function createProgram(context: ProgramContext): CreatedProgram {
497
543
  }
498
544
  });
499
545
 
546
+ const baseModel = program
547
+ .command("base-model")
548
+ .description("inspect the global base model catalog");
549
+ baseModel
550
+ .command("list")
551
+ .option("--json", "print JSON", false)
552
+ .description("list base models")
553
+ .action(async (options: { json: boolean }) => {
554
+ const { client } = await resolveContext(globals(), context);
555
+ await listBaseModels(client, { json: options.json }, context.io);
556
+ });
557
+ baseModel
558
+ .command("get")
559
+ .argument("<id>", "base model ID")
560
+ .option("--json", "print JSON", false)
561
+ .description("show one base model")
562
+ .action(async (id: string, options: { json: boolean }) => {
563
+ const { client } = await resolveContext(globals(), context);
564
+ await getBaseModel(client, { id: parseBaseModelId(id), json: options.json }, context.io);
565
+ });
566
+
567
+ const model = program
568
+ .command("model")
569
+ .description("manage project Models and immutable versions");
570
+ model
571
+ .command("list")
572
+ .option("--json", "print JSON", false)
573
+ .description("list Models owned by the selected project")
574
+ .action(async (options: { json: boolean }) => {
575
+ const { client, projectId } = await resolveContext(globals(), context);
576
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
577
+ await listModels(client, { projectId, json: options.json }, context.io);
578
+ });
579
+ model
580
+ .command("get")
581
+ .argument("<name-or-id>", "project Model name or ID")
582
+ .option("--json", "print JSON", false)
583
+ .description("show one project Model")
584
+ .action(async (nameOrId: string, options: { json: boolean }) => {
585
+ const { client, projectId } = await resolveContext(globals(), context);
586
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
587
+ await getModel(client, { projectId, nameOrId, json: options.json }, context.io);
588
+ });
589
+ model
590
+ .command("versions")
591
+ .argument("<name-or-id>", "project Model name or ID")
592
+ .option("--json", "print JSON", false)
593
+ .description("list immutable versions of a model")
594
+ .action(async (nameOrId: string, options: { json: boolean }) => {
595
+ const { client, projectId } = await resolveContext(globals(), context);
596
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
597
+ await listModelVersions(client, { projectId, nameOrId, json: options.json }, context.io);
598
+ });
599
+ model
600
+ .command("create")
601
+ .argument("<name>", "new Model name")
602
+ .requiredOption("--base <id>", "base model ID")
603
+ .option("--description <text>", "Model description")
604
+ .option("--json", "print JSON", false)
605
+ .description("create project Model metadata without a Model Version")
606
+ .action(
607
+ async (name: string, options: { base: string; description?: string; json: boolean }) => {
608
+ const { client, projectId } = await resolveContext(globals(), context);
609
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
610
+ await createModel(
611
+ client,
612
+ {
613
+ projectId,
614
+ name,
615
+ description: options.description,
616
+ baseModelId: parseBaseModelId(options.base),
617
+ json: options.json,
618
+ },
619
+ context.io,
620
+ );
621
+ },
622
+ );
623
+ model
624
+ .command("archive")
625
+ .option("--json", "print JSON", false)
626
+ .argument("<name-or-id>", "project Model name or ID")
627
+ .description("archive a project Model")
628
+ .action(async (nameOrId: string, options: { json: boolean }) => {
629
+ const { client, projectId } = await resolveContext(globals(), context);
630
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
631
+ await archiveModel(client, { projectId, nameOrId, json: options.json }, context.io);
632
+ });
633
+
500
634
  const dataset = program
501
635
  .command("dataset")
502
636
  .description("inspect and move dataset catalog and rows");
@@ -544,7 +678,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
544
678
  dataset
545
679
  .command("upload")
546
680
  .argument("<name>", "dataset name or id")
547
- .argument("<file>", "JSONL file, one JSON object per line")
681
+ .argument("<file>", "JSON array, JSONL, or CSV file")
548
682
  .description("append rows to a dataset")
549
683
  .action(async (name: string, file: string) => {
550
684
  const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
@@ -677,6 +811,66 @@ export function createProgram(context: ProgramContext): CreatedProgram {
677
811
  const result = await projectStatus(client, { sourceRoot, projectId });
678
812
  context.io.stdout(result.output);
679
813
  });
814
+ program
815
+ .command("report")
816
+ .argument("<markdownPath>", "path to a UTF-8 Markdown report")
817
+ .option("--authored-by <who>", "human or agent", authoredByOption, "human")
818
+ .option("--confirm", "confirm submission in a non-interactive environment", false)
819
+ .description("preview and send an issue report to Toloka through PostHog")
820
+ .action(
821
+ async (
822
+ markdownPath: string,
823
+ options: { authoredBy: IssueReportAuthor; confirm: boolean },
824
+ ) => {
825
+ assert(context.analytics, "Issue reporting requires CLI analytics.");
826
+ const sourceRoot = resolveInitRoot(globals());
827
+ const baseUrl =
828
+ globals().baseUrl ??
829
+ context.environment.POTATO_BASE_URL ??
830
+ resolveInternalBaseUrl(context.environment) ??
831
+ PRODUCTION_BASE_URL;
832
+ exitCode.value = await submitIssueReport({
833
+ path: markdownPath,
834
+ authoredBy: options.authoredBy,
835
+ platform: new URL(baseUrl).origin,
836
+ confirm: options.confirm,
837
+ sourceRoot,
838
+ cwd: process.cwd(),
839
+ analytics: context.analytics,
840
+ io: context.io,
841
+ });
842
+ },
843
+ );
844
+
845
+ const skill = program
846
+ .command("skill")
847
+ .description("discover bundled agent skills and install them into .agents/skills");
848
+ skill
849
+ .command("list")
850
+ .option("--json", "print Skill Descriptors with their status as JSON", false)
851
+ .description("list bundled skills as not-installed, current, or stale")
852
+ .action(async (options: { json: boolean }) => {
853
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
854
+ await skillListCommand({ sourceRoot, json: options.json, io: context.io });
855
+ });
856
+ skill
857
+ .command("search")
858
+ .argument("<query>", "text matched case-insensitively against id, name, description, and tags")
859
+ .option("--json", "print matching Skill Descriptors with their status as JSON", false)
860
+ .description("find bundled skills for a task")
861
+ .action(async (query: string, options: { json: boolean }) => {
862
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
863
+ await skillSearchCommand({ sourceRoot, query, json: options.json, io: context.io });
864
+ });
865
+ skill
866
+ .command("install")
867
+ .argument("<skillId>", "skill id from toloka skill list")
868
+ .option("-f, --force", "replace an existing .agents/skills/<skillId> directory", false)
869
+ .description("copy one bundled skill into .agents/skills/<skillId>")
870
+ .action(async (skillId: string, options: { force: boolean }) => {
871
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
872
+ await skillInstallCommand({ sourceRoot, skillId, force: options.force, io: context.io });
873
+ });
680
874
 
681
875
  return { program, exitCode };
682
876
  }
@@ -785,20 +979,6 @@ type CommandResult = {
785
979
  readonly outcome: CliAnalyticsCommandOutcome;
786
980
  readonly error?: unknown;
787
981
  };
788
- const formatCliError = (error: unknown): string => {
789
- if (error instanceof PotatoApiError) {
790
- const details =
791
- typeof error.body === "string"
792
- ? error.body
793
- : error.body === undefined
794
- ? undefined
795
- : JSON.stringify(error.body);
796
- return details === undefined || details.length === 0
797
- ? error.message
798
- : `${error.message}\n${details}`;
799
- }
800
- return error instanceof Error ? error.message : String(error);
801
- };
802
982
 
803
983
  const runProgram = async (
804
984
  program: Command,
@@ -0,0 +1,144 @@
1
+ import { assert } from "es-toolkit";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { lstat, open, type FileHandle } from "node:fs/promises";
5
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import type { CliAnalytics } from "./analytics";
7
+
8
+ export type IssueReportAuthor = "human" | "agent";
9
+
10
+ type IssueReportIo = {
11
+ readonly stdout: (text: string) => void;
12
+ readonly stderr: (text: string) => void;
13
+ readonly prompt?: (question: string) => Promise<string>;
14
+ };
15
+
16
+ const ISSUE_REPORT_MAX_BYTES = 256 * 1024;
17
+ const REPORT_DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
18
+
19
+ const assertNoSymlinkParents = async (absolutePath: string, sourceRoot: string): Promise<void> => {
20
+ const relativePath = relative(sourceRoot, absolutePath);
21
+ if (
22
+ relativePath === "" ||
23
+ relativePath === ".." ||
24
+ relativePath.startsWith(`..${sep}`) ||
25
+ isAbsolute(relativePath)
26
+ ) {
27
+ return;
28
+ }
29
+
30
+ const segments = relativePath.split(sep);
31
+ for (let index = 1; index < segments.length; index += 1) {
32
+ const directory = join(sourceRoot, ...segments.slice(0, index));
33
+ const stats = await lstat(directory);
34
+ assert(
35
+ !stats.isSymbolicLink(),
36
+ `Report file ${absolutePath} is inside the symbolic link ${directory}; select a path that does not traverse symbolic links.`,
37
+ );
38
+ }
39
+ };
40
+
41
+ const openReportFile = async (
42
+ path: string,
43
+ sourceRoot: string,
44
+ cwd: string,
45
+ ): Promise<{ handle: FileHandle; absolutePath: string; size: number }> => {
46
+ const absolutePath = resolve(cwd, path);
47
+ await assertNoSymlinkParents(absolutePath, sourceRoot);
48
+ let handle: FileHandle;
49
+ try {
50
+ handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
51
+ } catch (error) {
52
+ assert(
53
+ !(error instanceof Error && "code" in error && error.code === "ELOOP"),
54
+ `Report file ${absolutePath} is a symbolic link; select the file it points to directly.`,
55
+ );
56
+ throw error;
57
+ }
58
+
59
+ try {
60
+ const stats = await handle.stat();
61
+ assert(stats.isFile(), `Report file ${absolutePath} is not a regular file.`);
62
+ assert(stats.size > 0, `Report file ${absolutePath} is empty.`);
63
+ assert(
64
+ stats.size <= ISSUE_REPORT_MAX_BYTES,
65
+ `Report file ${absolutePath} is ${stats.size} bytes; the limit is 256 KiB.`,
66
+ );
67
+ return { handle, absolutePath, size: stats.size };
68
+ } catch (error) {
69
+ await handle.close();
70
+ throw error;
71
+ }
72
+ };
73
+
74
+ const readReportText = async (path: string, sourceRoot: string, cwd: string): Promise<string> => {
75
+ const opened = await openReportFile(path, sourceRoot, cwd);
76
+ try {
77
+ const bytes = await opened.handle.readFile();
78
+ assert(
79
+ bytes.byteLength === opened.size,
80
+ `${opened.absolutePath} changed while it was being read.`,
81
+ );
82
+ try {
83
+ return REPORT_DECODER.decode(bytes);
84
+ } catch {
85
+ throw new Error(`Report file ${opened.absolutePath} is not valid UTF-8.`);
86
+ }
87
+ } finally {
88
+ await opened.handle.close();
89
+ }
90
+ };
91
+
92
+ const confirmed = async (io: IssueReportIo, confirm: boolean): Promise<boolean> => {
93
+ if (confirm) return true;
94
+ if (io.prompt === undefined) return false;
95
+ const answer = (await io.prompt("Send this report to Toloka? [y/N] ")).trim().toLowerCase();
96
+ return answer === "y" || answer === "yes";
97
+ };
98
+
99
+ export async function submitIssueReport(options: {
100
+ readonly path: string;
101
+ readonly authoredBy: IssueReportAuthor;
102
+ readonly platform: string;
103
+ readonly confirm: boolean;
104
+ readonly sourceRoot: string;
105
+ readonly cwd: string;
106
+ readonly analytics: CliAnalytics;
107
+ readonly io: IssueReportIo;
108
+ }): Promise<number> {
109
+ const { analytics, io } = options;
110
+ assert(
111
+ analytics.issueReportingEnabled,
112
+ "Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.",
113
+ );
114
+ const reportText = await readReportText(options.path, options.sourceRoot, options.cwd);
115
+ io.stdout("Issue report preview:");
116
+ io.stdout("");
117
+ io.stdout(
118
+ reportText.replace(
119
+ /(?![\t\n])(?!\r\n)(?:\p{Cc}|\p{Bidi_Control})/gu,
120
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
121
+ ),
122
+ );
123
+ io.stdout("");
124
+ io.stdout(`Authored by: ${options.authoredBy}`);
125
+ io.stdout(`Platform: ${options.platform}`);
126
+ io.stdout("");
127
+
128
+ if (!(await confirmed(io, options.confirm))) {
129
+ io.stderr(
130
+ "Refusing to submit without confirmation in a non-interactive environment. Pass --confirm after reviewing the preview above.",
131
+ );
132
+ return 1;
133
+ }
134
+
135
+ const reportId = randomUUID();
136
+ await analytics.submitIssueReport({
137
+ reportId,
138
+ reportText,
139
+ authoredBy: options.authoredBy,
140
+ platform: options.platform,
141
+ });
142
+ io.stdout(`Submitted issue report: ${reportId}`);
143
+ return 0;
144
+ }