track-cli 4.0.3 → 4.1.0-rc1

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 (90) hide show
  1. package/esm/_dnt.shims.d.ts +1 -1
  2. package/esm/_dnt.test_shims.d.ts +20 -0
  3. package/esm/_dnt.test_shims.js +77 -0
  4. package/esm/deps/deno.land/std@0.195.0/_util/diff.d.ts +26 -0
  5. package/esm/deps/deno.land/std@0.195.0/_util/diff.js +311 -0
  6. package/esm/deps/deno.land/std@0.195.0/assert/_constants.d.ts +1 -0
  7. package/esm/deps/deno.land/std@0.195.0/assert/_constants.js +2 -0
  8. package/esm/deps/deno.land/std@0.195.0/assert/_format.d.ts +1 -0
  9. package/esm/deps/deno.land/std@0.195.0/assert/_format.js +23 -0
  10. package/esm/deps/deno.land/std@0.195.0/assert/assert_almost_equals.d.ts +18 -0
  11. package/esm/deps/deno.land/std@0.195.0/assert/assert_almost_equals.js +32 -0
  12. package/esm/deps/deno.land/std@0.195.0/assert/assert_array_includes.d.ts +14 -0
  13. package/esm/deps/deno.land/std@0.195.0/assert/assert_array_includes.js +38 -0
  14. package/esm/deps/deno.land/std@0.195.0/assert/assert_equals.d.ts +17 -0
  15. package/esm/deps/deno.land/std@0.195.0/assert/assert_equals.js +45 -0
  16. package/esm/deps/deno.land/std@0.195.0/assert/assert_exists.d.ts +5 -0
  17. package/esm/deps/deno.land/std@0.195.0/assert/assert_exists.js +14 -0
  18. package/esm/deps/deno.land/std@0.195.0/assert/assert_false.d.ts +4 -0
  19. package/esm/deps/deno.land/std@0.195.0/assert/assert_false.js +7 -0
  20. package/esm/deps/deno.land/std@0.195.0/assert/assert_instance_of.d.ts +8 -0
  21. package/esm/deps/deno.land/std@0.195.0/assert/assert_instance_of.js +38 -0
  22. package/esm/deps/deno.land/std@0.195.0/assert/assert_is_error.d.ts +7 -0
  23. package/esm/deps/deno.land/std@0.195.0/assert/assert_is_error.js +26 -0
  24. package/esm/deps/deno.land/std@0.195.0/assert/assert_match.d.ts +5 -0
  25. package/esm/deps/deno.land/std@0.195.0/assert/assert_match.js +13 -0
  26. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_equals.d.ts +14 -0
  27. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_equals.js +37 -0
  28. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_instance_of.d.ts +5 -0
  29. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_instance_of.js +14 -0
  30. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_match.d.ts +5 -0
  31. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_match.js +14 -0
  32. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_strict_equals.d.ts +11 -0
  33. package/esm/deps/deno.land/std@0.195.0/assert/assert_not_strict_equals.js +20 -0
  34. package/esm/deps/deno.land/std@0.195.0/assert/assert_object_match.d.ts +5 -0
  35. package/esm/deps/deno.land/std@0.195.0/assert/assert_object_match.js +78 -0
  36. package/esm/deps/deno.land/std@0.195.0/assert/assert_rejects.d.ts +64 -0
  37. package/esm/deps/deno.land/std@0.195.0/assert/assert_rejects.js +50 -0
  38. package/esm/deps/deno.land/std@0.195.0/assert/assert_strict_equals.d.ts +23 -0
  39. package/esm/deps/deno.land/std@0.195.0/assert/assert_strict_equals.js +60 -0
  40. package/esm/deps/deno.land/std@0.195.0/assert/assert_string_includes.d.ts +5 -0
  41. package/esm/deps/deno.land/std@0.195.0/assert/assert_string_includes.js +13 -0
  42. package/esm/deps/deno.land/std@0.195.0/assert/assert_throws.d.ts +54 -0
  43. package/esm/deps/deno.land/std@0.195.0/assert/assert_throws.js +44 -0
  44. package/esm/deps/deno.land/std@0.195.0/assert/equal.d.ts +6 -0
  45. package/esm/deps/deno.land/std@0.195.0/assert/equal.js +102 -0
  46. package/esm/deps/deno.land/std@0.195.0/assert/fail.d.ts +4 -0
  47. package/esm/deps/deno.land/std@0.195.0/assert/fail.js +9 -0
  48. package/esm/deps/deno.land/std@0.195.0/assert/mod.d.ts +32 -0
  49. package/esm/deps/deno.land/std@0.195.0/assert/mod.js +33 -0
  50. package/esm/deps/deno.land/std@0.195.0/assert/unimplemented.d.ts +2 -0
  51. package/esm/deps/deno.land/std@0.195.0/assert/unimplemented.js +7 -0
  52. package/esm/deps/deno.land/std@0.195.0/assert/unreachable.d.ts +2 -0
  53. package/esm/deps/deno.land/std@0.195.0/assert/unreachable.js +6 -0
  54. package/esm/deps/deno.land/std@0.195.0/testing/_test_suite.d.ts +70 -0
  55. package/esm/deps/deno.land/std@0.195.0/testing/_test_suite.js +321 -0
  56. package/esm/deps/deno.land/std@0.195.0/testing/asserts.d.ts +329 -0
  57. package/esm/deps/deno.land/std@0.195.0/testing/asserts.js +330 -0
  58. package/esm/deps/deno.land/std@0.195.0/testing/bdd.d.ts +440 -0
  59. package/esm/deps/deno.land/std@0.195.0/testing/bdd.js +215 -0
  60. package/esm/deps/deno.land/std@0.195.0/testing/mock.d.ts +110 -0
  61. package/esm/deps/deno.land/std@0.195.0/testing/mock.js +746 -0
  62. package/esm/src/action/clone.js +4 -3
  63. package/esm/src/action/frontend-template-switch.d.ts +1 -0
  64. package/esm/src/action/frontend-template-switch.js +56 -0
  65. package/esm/src/action/frontend-template.d.ts +2 -0
  66. package/esm/src/action/frontend-template.js +12 -0
  67. package/esm/src/main.js +9 -2
  68. package/esm/src/meta.d.ts +1 -1
  69. package/esm/src/meta.js +108 -25
  70. package/esm/src/orca/client.js +1 -1
  71. package/esm/src/shared/config.d.ts +1 -0
  72. package/esm/src/shared/file.d.ts +1 -0
  73. package/esm/src/shared/file.js +32 -0
  74. package/esm/src/shared/mod.d.ts +1 -1
  75. package/esm/src/shared/mod.js +10 -2
  76. package/esm/src/shared/types.d.ts +15 -0
  77. package/esm/src/shared/types.js +1 -0
  78. package/esm/src/track/client.d.ts +4 -1
  79. package/esm/src/track/test.d.ts +5 -2
  80. package/esm/src/track/test.js +18 -2
  81. package/esm/src/track/training.d.ts +4 -1
  82. package/esm/src/track/training.js +9 -0
  83. package/esm/test/shared/config_test.d.ts +1 -0
  84. package/esm/test/shared/config_test.js +57 -0
  85. package/esm/test/shared/file_test.d.ts +1 -0
  86. package/esm/test/shared/file_test.js +265 -0
  87. package/esm/test/shared/mod_test.d.ts +1 -0
  88. package/esm/test/shared/mod_test.js +353 -0
  89. package/package.json +2 -1
  90. package/test_runner.js +186 -0
@@ -8,9 +8,9 @@ import { TrainingClient } from "../track/training.js";
8
8
  import { exists } from "../../deps/deno.land/std@0.195.0/fs/mod.js";
9
9
  export async function clone(url, options) {
10
10
  // console.log(`url: ${url}`);
11
- // console.log(`target_dir: ${targetDir}`);
12
- // console.log(`basicAuthUser: ${basicAuthUser}`);
13
- // console.log(`basicAuthPassword: ${basicAuthPassword}`);
11
+ // console.log(`target_dir: ${options.targetDir}`);
12
+ // console.log(`basicAuthUser: ${options.basicAuthUser}`);
13
+ // console.log(`basicAuthPassword: ${options.basicAuthPassword}`);
14
14
  const cloneUrl = new CloneUrl(url);
15
15
  const basicAuth = options?.basicAuthUser
16
16
  ? {
@@ -55,6 +55,7 @@ export async function clone(url, options) {
55
55
  token: cloneUrl.token,
56
56
  cookies: api.getCookies(),
57
57
  programmingLanguage: result.programmingLanguage,
58
+ frontendTemplate: codingContext.selectedFrontendTemplate?.seriesCode,
58
59
  });
59
60
  await config.save(trackConfig, {
60
61
  path: `${challengeDir}/.track/config.json`,
@@ -0,0 +1 @@
1
+ export declare function switchFrontendTemplate(): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { load as loadConfig, save as saveConfig, } from "../shared/config.js";
3
+ import { OtherError } from "../shared/errors.js";
4
+ import { ChallengeStyle } from "../shared/types.js";
5
+ import { archiveExistingChallengeFiles, chooseFrontendTemplate, getCommonChallengeContext, trackClientFromConfig, } from "../shared/mod.js";
6
+ import { unzip } from "../shared/file.js";
7
+ export async function switchFrontendTemplate() {
8
+ const config = await loadConfig();
9
+ const api = trackClientFromConfig(config);
10
+ const [codingContext, challenge] = await getCommonChallengeContext(config, api);
11
+ if (challenge.style !== ChallengeStyle.Frontend) {
12
+ throw new OtherError("This challenge is not a frontend challenge");
13
+ }
14
+ // Get available frontend templates from Track
15
+ const allFrontendTemplates = await api.frontendTemplates();
16
+ // Filter by allowed templates for this challenge
17
+ const allowedCodes = challenge.allowedTemplateSeriesCodes;
18
+ const availableTemplates = allowedCodes && allowedCodes.length > 0
19
+ ? allFrontendTemplates.filter((t) => allowedCodes.includes(t.seriesCode))
20
+ : allFrontendTemplates;
21
+ if (availableTemplates.length === 0) {
22
+ throw new OtherError("This challenge does not support changing frontend templates");
23
+ }
24
+ console.log("Your existing code will be archived in this directory and the original challenge code for the new template will be downloaded");
25
+ const shouldContinue = dntShim.confirm("Are you sure you want to continue?");
26
+ if (!shouldContinue) {
27
+ return;
28
+ }
29
+ const desiredTemplate = chooseFrontendTemplate(availableTemplates);
30
+ if (desiredTemplate.seriesCode ===
31
+ codingContext.selectedFrontendTemplate?.seriesCode) {
32
+ // already using this template
33
+ return;
34
+ }
35
+ // Get template details with presigned URLs
36
+ const templateDetails = await api.frontendTemplate(desiredTemplate.seriesCode);
37
+ if (!templateDetails.downloadUrl) {
38
+ throw new OtherError("Template does not have a download URL available");
39
+ }
40
+ const currentDir = dntShim.Deno.cwd();
41
+ await archiveExistingChallengeFiles(config.orgName);
42
+ // Download and extract the template ZIP file
43
+ const templateResp = await dntShim.fetch(templateDetails.downloadUrl);
44
+ if (!templateResp.ok) {
45
+ throw new OtherError(`Failed to download template: ${templateResp.status} ${templateResp.statusText}`);
46
+ }
47
+ // Extract template files
48
+ const templateZip = new Uint8Array(await templateResp.arrayBuffer());
49
+ await unzip(templateZip, currentDir);
50
+ const updatedConfig = {
51
+ ...config,
52
+ frontendTemplate: desiredTemplate.seriesCode,
53
+ };
54
+ await saveConfig(updatedConfig);
55
+ console.debug("Frontend template switch completed.");
56
+ }
@@ -0,0 +1,2 @@
1
+ import { FrontendTemplateInfo } from "../shared/types.js";
2
+ export declare function chooseFrontendTemplate(availableTemplates: FrontendTemplateInfo[]): FrontendTemplateInfo;
@@ -0,0 +1,12 @@
1
+ import { Prompt } from "../shared/mod.js";
2
+ export function chooseFrontendTemplate(availableTemplates) {
3
+ while (true) {
4
+ const desiredTemplateIndex = Prompt.select("Please select a frontend template:", availableTemplates.map((t) => t.seriesName));
5
+ if (desiredTemplateIndex !== undefined) {
6
+ return availableTemplates[desiredTemplateIndex];
7
+ }
8
+ else {
9
+ console.log("An invalid frontend template was specified");
10
+ }
11
+ }
12
+ }
package/esm/src/main.js CHANGED
@@ -4,6 +4,7 @@ import * as dntShim from "../_dnt.shims.js";
4
4
  import { clone as cloneAction } from "./action/clone.js";
5
5
  import { run as runAction } from "./action/run.js";
6
6
  import { lang as langAction } from "./action/lang.js";
7
+ import { switchFrontendTemplate as frontendTemplateAction } from "./action/frontend-template-switch.js";
7
8
  import { reset as resetAction } from "./action/reset.js";
8
9
  import { pull as pullAction } from "./action/pull.js";
9
10
  import { add as addAction } from "./action/add.js";
@@ -37,6 +38,11 @@ import { Command } from "../deps/deno.land/x/cliffy@v0.25.7/command/mod.js";
37
38
  .action(async () => {
38
39
  await langAction();
39
40
  });
41
+ const frontendTemplateCommand = new Command()
42
+ .description("Change the frontend template used for this challenge.")
43
+ .action(async () => {
44
+ await frontendTemplateAction();
45
+ });
40
46
  const resetCommand = new Command()
41
47
  .description("Reset the challenge files back to their starting point and archive your current challenge files.")
42
48
  .action(async () => {
@@ -65,7 +71,7 @@ import { Command } from "../deps/deno.land/x/cliffy@v0.25.7/command/mod.js";
65
71
  .action(async () => {
66
72
  await statusAction();
67
73
  });
68
- if (!await checkUpdates()) {
74
+ if (!(await checkUpdates())) {
69
75
  dntShim.Deno.exit(1);
70
76
  }
71
77
  await new Command()
@@ -78,6 +84,7 @@ import { Command } from "../deps/deno.land/x/cliffy@v0.25.7/command/mod.js";
78
84
  .command("clone", cloneCommand)
79
85
  .command("run", runCommand)
80
86
  .command("lang", langCommand)
87
+ .command("frontend-template", frontendTemplateCommand)
81
88
  .command("reset", resetCommand)
82
89
  .command("pull", pullCommand)
83
90
  .command("add", addCommand)
@@ -87,7 +94,7 @@ import { Command } from "../deps/deno.land/x/cliffy@v0.25.7/command/mod.js";
87
94
  dntShim.Deno.exit(0);
88
95
  })().catch((err) => {
89
96
  if (err instanceof TrackError) {
90
- console.error(`${red("[ERROR]")} ${err.message}`);
97
+ console.error(`${red("[ERROR]")} ${"message" in err ? String(err.message) : String(err)}`);
91
98
  dntShim.Deno.exit(1);
92
99
  }
93
100
  else {
package/esm/src/meta.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const VERSION = "4.0.3";
1
+ export declare const VERSION = "4.1.0-rc1";
2
2
  export declare const DESCRIPTION = "A CLI for interacting with tracks.run and running code tests on track's servers";
3
3
  export declare function checkUpdates(): Promise<boolean>;
package/esm/src/meta.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as colors from "../deps/deno.land/std@0.195.0/fmt/colors.js";
2
2
  // @ts-ignore: has no exported member
3
3
  import { CookieJar, fetch } from "node-fetch-cookies";
4
- export const VERSION = "4.0.3";
4
+ export const VERSION = "4.1.0-rc1";
5
5
  export const DESCRIPTION = "A CLI for interacting with tracks.run and running code tests on track's servers";
6
6
  const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-?(.*))$/;
7
7
  function parseSemver(s) {
@@ -9,43 +9,126 @@ function parseSemver(s) {
9
9
  if (!match) {
10
10
  return;
11
11
  }
12
- return [
13
- parseInt(match[1]),
14
- parseInt(match[2]),
15
- parseInt(match[3]),
16
- match[4],
17
- ];
12
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), match[4]];
18
13
  }
19
14
  export async function checkUpdates() {
20
15
  const res = await fetch(new CookieJar(), "https://track-cli.s3-ap-northeast-1.amazonaws.com/version.json");
21
16
  const publishedVersion = await res.json();
22
- const supportedFrom = parseSemver(publishedVersion.supported_from) ??
23
- [1, 0, 0, ""];
17
+ const supportedFrom = parseSemver(publishedVersion.supported_from) ?? [
18
+ 1,
19
+ 0,
20
+ 0,
21
+ "",
22
+ ];
24
23
  const latest = parseSemver(publishedVersion.latest) ?? [1, 0, 0, ""];
25
24
  const current = parseSemver(VERSION);
26
- // deno-fmt-ignore
27
25
  if (parseSemver(VERSION) < supportedFrom) {
28
- console.log("╭─────────────────────────────────────────────────────────────────────────────────────────────╮");
29
- console.log("| |");
30
- console.log(`| ${colors.bold(colors.red("!!! OUTDATED !!!"))} |`);
31
- console.log("| This version of Track CLI is no more supported. |");
32
- console.log(`| Current version: ${VERSION.padEnd(20)} |`);
33
- console.log(`| Latest version: ${publishedVersion.latest.padEnd(20)} |`);
34
- console.log("| |");
35
- console.log("╰─────────────────────────────────────────────────────────────────────────────────────────────╯");
26
+ new Box({ width: 95 }).printBox([
27
+ { center: true },
28
+ [
29
+ applyFormatting("!!! OUTDATED !!!", (s) => colors.bold(colors.red(s))),
30
+ ],
31
+ ], [{ left: 22 }, ["This version of Track CLI is no longer supported."]], [{ left: 22 + 11 }, [`Installed version: ${VERSION}`]], [{ left: 22 + 11 }, [` Latest version: ${publishedVersion.latest}`]]);
36
32
  return false;
37
33
  }
38
34
  else if (current < latest) {
39
- console.log("╭─────────────────────────────────────────────────────────────────────────────────────────────╮");
40
- console.log("| |");
41
- console.log("| New version of Track CLI is available! |");
42
- console.log(`| Current version: ${VERSION.padEnd(20)} |`);
43
- console.log(`| Latest version: ${publishedVersion.latest.padEnd(20)} |`);
44
- console.log("| |");
45
- console.log("╰─────────────────────────────────────────────────────────────────────────────────────────────╯");
35
+ new Box({ width: 95 }).printBox([{ left: 22 }, ["A new version of Track CLI is available!"]], [{ left: 22 + 11 }, [`Installed version: ${VERSION}`]], [{ left: 22 + 11 }, [` Latest version: ${publishedVersion.latest}`]]);
46
36
  return true;
47
37
  }
48
38
  else {
49
39
  return true;
50
40
  }
51
41
  }
42
+ function applyFormatting(s, formatting) {
43
+ return { textWidth: s.length, formatted: formatting(s) };
44
+ }
45
+ class Box {
46
+ /**
47
+ * Inclusive of the pipes and the 1 space padding on each side:
48
+ * `"| content |"` has width 11.
49
+ */
50
+ constructor(p) {
51
+ Object.defineProperty(this, "p", {
52
+ enumerable: true,
53
+ configurable: true,
54
+ writable: true,
55
+ value: p
56
+ });
57
+ }
58
+ print(message) {
59
+ console.log(message);
60
+ }
61
+ printBox(...lines) {
62
+ this.printTop();
63
+ this.printLine({ left: 0 }, [""]);
64
+ for (const [align, content] of lines) {
65
+ this.printLine(align, content);
66
+ }
67
+ this.printLine({ left: 0 }, [""]);
68
+ this.printBottom();
69
+ }
70
+ printTop() {
71
+ const message = Box.CORNERS[0][0] +
72
+ Box.HORIZONTAL.repeat(this.p.width - 2) +
73
+ Box.CORNERS[0][1];
74
+ this.print(message);
75
+ }
76
+ printBottom() {
77
+ const message = Box.CORNERS[1][0] +
78
+ Box.HORIZONTAL.repeat(this.p.width - 2) +
79
+ Box.CORNERS[1][1];
80
+ this.print(message);
81
+ }
82
+ printLine(align, content) {
83
+ let contentWidth = 0;
84
+ const parts = [];
85
+ for (const part of content) {
86
+ if (typeof part === "string") {
87
+ contentWidth += part.length;
88
+ parts.push(part);
89
+ }
90
+ else {
91
+ contentWidth += part.textWidth;
92
+ parts.push(part.formatted);
93
+ }
94
+ }
95
+ const innerWidth = this.p.width - 4;
96
+ let leftPadding = 0;
97
+ if (align.center) {
98
+ leftPadding = Math.max(0, Math.floor((innerWidth - contentWidth) / 2));
99
+ }
100
+ else if (align.left !== undefined) {
101
+ leftPadding = align.left;
102
+ }
103
+ const rightPadding = innerWidth - leftPadding - contentWidth;
104
+ const message = [
105
+ Box.PIPE,
106
+ " ".repeat(1 + leftPadding),
107
+ ...parts,
108
+ rightPadding > 0 ? " ".repeat(rightPadding + 1) : "",
109
+ rightPadding >= 0 ? Box.PIPE : "",
110
+ ].join("");
111
+ this.print(message);
112
+ }
113
+ }
114
+ Object.defineProperty(Box, "PIPE", {
115
+ enumerable: true,
116
+ configurable: true,
117
+ writable: true,
118
+ value: "|"
119
+ });
120
+ Object.defineProperty(Box, "HORIZONTAL", {
121
+ enumerable: true,
122
+ configurable: true,
123
+ writable: true,
124
+ value: "─"
125
+ });
126
+ Object.defineProperty(Box, "CORNERS", {
127
+ enumerable: true,
128
+ configurable: true,
129
+ writable: true,
130
+ value: [
131
+ ["╭", "╮"],
132
+ ["╰", "╯"],
133
+ ]
134
+ });
@@ -128,7 +128,7 @@ export class OrcaClient {
128
128
  }));
129
129
  }
130
130
  catch (e) {
131
- rej(new OtherError(e.message));
131
+ rej(new OtherError(e && typeof e === "object" && "message" in e ? String(e.message) : String(e)));
132
132
  }
133
133
  });
134
134
  }
@@ -8,6 +8,7 @@ export interface TrackConfigCore {
8
8
  basicAuth?: BasicAuth;
9
9
  token: string;
10
10
  programmingLanguage?: ProgrammingLanguage;
11
+ frontendTemplate?: string;
11
12
  cookies: Record<string, string>;
12
13
  }
13
14
  export interface TrackTestConfigPart {
@@ -24,6 +24,7 @@ export declare function getAllDirFiles(p: string): Promise<string[]>;
24
24
  export declare function toNixStyle(p: string): string;
25
25
  export declare function toNativeStyle(p: string): string;
26
26
  export declare function untgz(compressed: Uint8Array, dest: string): Promise<void>;
27
+ export declare function unzip(zipData: Uint8Array, dest: string): Promise<void>;
27
28
  export type GetUniqueDirNameOptions = {
28
29
  root?: string;
29
30
  };
@@ -97,6 +97,38 @@ export async function untgz(compressed, dest) {
97
97
  cwd: dest,
98
98
  });
99
99
  }
100
+ export async function unzip(zipData, dest) {
101
+ const JSZip = (await import("jszip")).default;
102
+ try {
103
+ // destination directory exists
104
+ await fs.ensureDir(dest);
105
+ const zip = new JSZip();
106
+ await zip.loadAsync(zipData);
107
+ const promises = [];
108
+ zip.forEach((relativePath, file) => {
109
+ if (!file.dir) {
110
+ // file
111
+ promises.push((async () => {
112
+ const content = await file.async("uint8array");
113
+ const filePath = path.join(dest, relativePath);
114
+ await fs.ensureFile(filePath);
115
+ await dntShim.Deno.writeFile(filePath, content);
116
+ })());
117
+ }
118
+ else {
119
+ // directory
120
+ promises.push((async () => {
121
+ const dirPath = path.join(dest, relativePath);
122
+ await fs.ensureDir(dirPath);
123
+ })());
124
+ }
125
+ });
126
+ await Promise.all(promises);
127
+ }
128
+ catch (error) {
129
+ throw new Error(`Failed to extract ZIP file: ${error}`);
130
+ }
131
+ }
100
132
  export async function getUniqueDirName(base, options) {
101
133
  let counter = 1;
102
134
  const root = options?.root ?? dntShim.Deno.cwd();
@@ -39,5 +39,5 @@ export type ArchiveExistingChallengeFilesOptions = {
39
39
  selectedLanguage?: ProgrammingLanguageInfo;
40
40
  };
41
41
  export declare function archiveExistingChallengeFiles(orgName: string): Promise<void>;
42
+ export { chooseFrontendTemplate } from "../action/frontend-template.js";
42
43
  export declare function trackClientFromConfig(config: TrackConfig): TrackClient;
43
- export {};
@@ -2,7 +2,7 @@ import * as dntShim from "../../_dnt.shims.js";
2
2
  import { save as saveConfig, } from "./config.js";
3
3
  import { BadTarballURL, ChallengeAlreadyFinished, OtherError, } from "./errors.js";
4
4
  import { toNativeStyle, untgz } from "./file.js";
5
- import { ChallengeResultStatus, FileListType, TrackApp, } from "./types.js";
5
+ import { ChallengeResultStatus, ChallengeStyle, FileListType, TrackApp, } from "./types.js";
6
6
  import { TestClient } from "../track/test.js";
7
7
  import { TrainingClient } from "../track/training.js";
8
8
  import * as datetime from "../../deps/deno.land/std@0.195.0/datetime/mod.js";
@@ -100,6 +100,13 @@ export async function tryStartingChallenge(api, challenge) {
100
100
  await api.updateLanguage(desiredLanguage.value);
101
101
  }
102
102
  }
103
+ // Handle template selection for frontend challenges
104
+ if (challenge.style === ChallengeStyle.Frontend) {
105
+ const frontendTemplates = await api.frontendTemplates();
106
+ if (frontendTemplates.length > 0) {
107
+ console.log("Frontend challenge detected. Use 'track frontend-template' to switch templates after cloning.");
108
+ }
109
+ }
103
110
  result = await api.start();
104
111
  break;
105
112
  }
@@ -354,11 +361,12 @@ export /**
354
361
  await fs.move(`${challengeDir}/${dirEntry.name}`, `${archiveDir}/${dirEntry.name}`);
355
362
  }
356
363
  }
364
+ export { chooseFrontendTemplate } from "../action/frontend-template.js";
357
365
  export function trackClientFromConfig(config) {
358
366
  let api;
359
367
  switch (config.app) {
360
368
  case TrackApp.Test:
361
- api = new TestClient(config.baseUrl, config.orgName, config.token, config.challengeId, config.basicAuth, config.cookies);
369
+ api = new TestClient(config.baseUrl, config.orgName, config.token, config.challengeId, config.basicAuth, config.cookies, config.applicantExamId, config.challengeResultId);
362
370
  break;
363
371
  case TrackApp.Training:
364
372
  api = new TrainingClient(config.baseUrl, config.orgName, config.token, config.courseMaterialId, config.klassId, config.klassResultId, config.basicAuth, config.cookies);
@@ -25,6 +25,7 @@ export interface ChallengeSession {
25
25
  timeLimitMinutes?: number;
26
26
  displayOrder: number;
27
27
  programmingLanguages: ProgrammingLanguage[];
28
+ allowedTemplateSeriesCodes?: string[];
28
29
  localCodingAllowed: boolean;
29
30
  openTestcases?: number;
30
31
  result?: ChallengeResult;
@@ -59,6 +60,8 @@ export interface CodingContext {
59
60
  allowEdit?: boolean;
60
61
  programmingLanguages: ProgrammingLanguage[];
61
62
  selectedLanguage?: ProgrammingLanguage;
63
+ selectedFrontendTemplate?: FrontendTemplateInfo;
64
+ availableFrontendTemplates?: FrontendTemplateInfo[];
62
65
  challengeLanguage: SpokenLanguage;
63
66
  settings: ChallengeSettings;
64
67
  answers: CodingAnswers;
@@ -122,6 +125,7 @@ export declare const ChallengeStyle: {
122
125
  readonly Development: 2;
123
126
  readonly Algorithm: 3;
124
127
  readonly Ai: 4;
128
+ readonly Frontend: 8;
125
129
  };
126
130
  export type ChallengeStyle = (typeof ChallengeStyle)[keyof typeof ChallengeStyle];
127
131
  export declare const ChallengeResultStatus: {
@@ -140,3 +144,14 @@ export declare const SpokenLanguage: {
140
144
  };
141
145
  export type SpokenLanguage = (typeof SpokenLanguage)[keyof typeof SpokenLanguage];
142
146
  export type ProgrammingLanguage = number;
147
+ export interface FrontendTemplateInfo {
148
+ seriesCode: string;
149
+ seriesName: string;
150
+ versionId: number;
151
+ versionName: string;
152
+ downloadUrl?: string;
153
+ readmeUrl?: {
154
+ en?: string;
155
+ ja?: string;
156
+ };
157
+ }
@@ -84,6 +84,7 @@ export const ChallengeStyle = {
84
84
  Development: 2,
85
85
  Algorithm: 3,
86
86
  Ai: 4,
87
+ Frontend: 8,
87
88
  };
88
89
  export const ChallengeResultStatus = {
89
90
  Prepared: 0,
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../../_dnt.shims.js";
2
2
  import { TrackTestConfigPart, TrackTrainingConfigPart } from "../shared/config.js";
3
- import { ChallengeResult, ChallengeSession, CodingContext, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
3
+ import { ChallengeResult, ChallengeSession, CodingContext, FrontendTemplateInfo, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
4
4
  import { CookieJar } from "node-fetch-cookies";
5
5
  import { ProxyAgent } from "proxy-agent";
6
6
  export interface BasicAuth {
@@ -41,9 +41,12 @@ export interface TrackClient {
41
41
  };
42
42
  startChallengeSession(): Promise<ChallengeSession>;
43
43
  languages(): Promise<ProgrammingLanguageInfo[]>;
44
+ frontendTemplates(): Promise<FrontendTemplateInfo[]>;
45
+ frontendTemplate(code: string): Promise<FrontendTemplateInfo>;
44
46
  start(): Promise<ChallengeResult>;
45
47
  prepare(): Promise<ChallengeResult>;
46
48
  updateLanguage(language: ProgrammingLanguage): Promise<void>;
49
+ updateFrontendTemplate(templateCode: string): Promise<void>;
47
50
  timeLeft(): Promise<number>;
48
51
  context(): Promise<CodingContext>;
49
52
  presigned(files: string[]): Promise<{
@@ -1,5 +1,5 @@
1
1
  import { TrackTestConfigPart } from "../shared/config.js";
2
- import { ChallengeResult, ChallengeSession, CodingContext, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
2
+ import { ChallengeResult, ChallengeSession, CodingContext, FrontendTemplateInfo, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
3
3
  import { BasicAuth, TrackClient, TrackClientBase } from "./client.js";
4
4
  export declare class TestClient extends TrackClientBase implements TrackClient {
5
5
  private orgName;
@@ -7,7 +7,7 @@ export declare class TestClient extends TrackClientBase implements TrackClient {
7
7
  private challengeId;
8
8
  private applicantExamId;
9
9
  private resultId;
10
- constructor(baseUrl: string, orgName: string, token: string, challengeId: number, basic?: BasicAuth, cookies?: Record<string, string>);
10
+ constructor(baseUrl: string, orgName: string, token: string, challengeId: number, basic?: BasicAuth, cookies?: Record<string, string>, applicantExamId?: number, challengeResultId?: number);
11
11
  config(): TrackTestConfigPart;
12
12
  authenticate(): Promise<void>;
13
13
  startChallengeSession(): Promise<ChallengeSession>;
@@ -16,9 +16,12 @@ export declare class TestClient extends TrackClientBase implements TrackClient {
16
16
  private getExamSession;
17
17
  private getChallengeFromExamSession;
18
18
  languages(): Promise<ProgrammingLanguageInfo[]>;
19
+ frontendTemplates(): Promise<FrontendTemplateInfo[]>;
20
+ frontendTemplate(code: string): Promise<FrontendTemplateInfo>;
19
21
  start(): Promise<ChallengeResult>;
20
22
  prepare(): Promise<ChallengeResult>;
21
23
  updateLanguage(language: ProgrammingLanguage): Promise<void>;
24
+ updateFrontendTemplate(templateCode: string): Promise<void>;
22
25
  timeLeft(): Promise<number>;
23
26
  context(): Promise<CodingContext>;
24
27
  presigned(files: string[]): Promise<{
@@ -5,7 +5,7 @@ import { FormType, TrackClientBase, } from "./client.js";
5
5
  // @ts-ignore: has no exported member
6
6
  import { CookieJar, fetch } from "node-fetch-cookies";
7
7
  export class TestClient extends TrackClientBase {
8
- constructor(baseUrl, orgName, token, challengeId, basic, cookies) {
8
+ constructor(baseUrl, orgName, token, challengeId, basic, cookies, applicantExamId, challengeResultId) {
9
9
  super(baseUrl, basic, cookies);
10
10
  Object.defineProperty(this, "orgName", {
11
11
  enumerable: true,
@@ -40,6 +40,8 @@ export class TestClient extends TrackClientBase {
40
40
  this.orgName = orgName;
41
41
  this.token = token;
42
42
  this.challengeId = challengeId;
43
+ this.applicantExamId = applicantExamId ?? 0;
44
+ this.resultId = challengeResultId ?? 0;
43
45
  }
44
46
  config() {
45
47
  return {
@@ -100,12 +102,19 @@ export class TestClient extends TrackClientBase {
100
102
  .map((esc) => toChallengeSession(esc, result))
101
103
  .find((c) => c.challengeId === this.challengeId &&
102
104
  (c.style === ChallengeStyle.Development ||
103
- c.style === ChallengeStyle.Algorithm));
105
+ c.style === ChallengeStyle.Algorithm ||
106
+ c.style === ChallengeStyle.Frontend));
104
107
  }
105
108
  async languages() {
106
109
  return (await this._get(`/api/enum/challenges/programminglanguages`))
107
110
  .map(toProgrammingLanguageInfo);
108
111
  }
112
+ async frontendTemplates() {
113
+ return await this._get(`/api/dock-public/v2/consumer/frontend-templates`);
114
+ }
115
+ async frontendTemplate(code) {
116
+ return await this._get(`/api/dock-public/v2/consumer/frontend-templates/${code}`);
117
+ }
109
118
  async start() {
110
119
  return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/start`, FormType.URLEncoded, {
111
120
  takenBy: 3, // 3 = LocalMachine
@@ -121,6 +130,11 @@ export class TestClient extends TrackClientBase {
121
130
  programmingLanguage: language,
122
131
  }); // ChallengeResult
123
132
  }
133
+ async updateFrontendTemplate(templateCode) {
134
+ return await this._put(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/frontend-template`, FormType.URLEncoded, {
135
+ frontendTemplate: templateCode,
136
+ });
137
+ }
124
138
  async timeLeft() {
125
139
  return await this._get(`/api/applicants/exams/${this.applicantExamId}/results/${this.resultId}/timeleft`);
126
140
  }
@@ -171,6 +185,8 @@ function toChallengeSession(esc, result) {
171
185
  timeLimitMinutes: esc.timeLimitMinutes,
172
186
  displayOrder: esc.displayOrder,
173
187
  programmingLanguages: esc.programmingLanguages,
188
+ allowedTemplateSeriesCodes: esc.frontendStyleConfig
189
+ ?.allowedTemplateSeriesCodes,
174
190
  localCodingAllowed: esc.localExamEnabled,
175
191
  openTestcases: esc.openTestcases,
176
192
  result,
@@ -1,5 +1,5 @@
1
1
  import { TrackTrainingConfigPart } from "../shared/config.js";
2
- import { ChallengeResult, ChallengeSession, CodingContext, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
2
+ import { ChallengeResult, ChallengeSession, CodingContext, FrontendTemplateInfo, ProgrammingLanguage, ProgrammingLanguageInfo, SaveFilesRequest } from "../shared/types.js";
3
3
  import { BasicAuth, TrackClient, TrackClientBase } from "./client.js";
4
4
  export declare class TrainingClient extends TrackClientBase implements TrackClient {
5
5
  private orgName;
@@ -13,9 +13,12 @@ export declare class TrainingClient extends TrackClientBase implements TrackClie
13
13
  startChallengeSession(): Promise<ChallengeSession>;
14
14
  continueChallengeSession(): Promise<ChallengeSession>;
15
15
  languages(): Promise<ProgrammingLanguageInfo[]>;
16
+ frontendTemplates(): Promise<FrontendTemplateInfo[]>;
17
+ frontendTemplate(_code: string): Promise<FrontendTemplateInfo>;
16
18
  start(): Promise<ChallengeResult>;
17
19
  prepare(): Promise<ChallengeResult>;
18
20
  updateLanguage(language: ProgrammingLanguage): Promise<void>;
21
+ updateFrontendTemplate(_templateCode: string): Promise<void>;
19
22
  timeLeft(): Promise<number>;
20
23
  context(): Promise<CodingContext>;
21
24
  presigned(files: string[]): Promise<{
@@ -74,6 +74,12 @@ export class TrainingClient extends TrackClientBase {
74
74
  return (await this._get(`/cli/languages`))
75
75
  .map(toProgrammingLanguageInfo);
76
76
  }
77
+ frontendTemplates() {
78
+ throw new OtherError("Frontend templates are not supported in training mode");
79
+ }
80
+ frontendTemplate(_code) {
81
+ throw new OtherError("Frontend templates are not supported in training mode");
82
+ }
77
83
  async start() {
78
84
  return await this._put(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/start`, FormType.JSON, {
79
85
  takenBy: 3, // 3 = LocalMachine
@@ -87,6 +93,9 @@ export class TrainingClient extends TrackClientBase {
87
93
  lang: language,
88
94
  });
89
95
  }
96
+ updateFrontendTemplate(_templateCode) {
97
+ throw new OtherError("Frontend templates are not supported in training mode");
98
+ }
90
99
  async timeLeft() {
91
100
  return await this._get(`/api/train/${this.klassId}/challenges/results/${this.klassResultId}/timeleft`);
92
101
  }
@@ -0,0 +1 @@
1
+ export {};