@kradle/cli 0.0.17 → 0.2.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.
- package/README.md +93 -65
- package/dist/commands/agent/list.d.ts +4 -0
- package/dist/commands/agent/list.js +6 -4
- package/dist/commands/challenge/build.d.ts +9 -1
- package/dist/commands/challenge/build.js +40 -12
- package/dist/commands/challenge/create.d.ts +5 -1
- package/dist/commands/challenge/create.js +17 -18
- package/dist/commands/challenge/delete.d.ts +4 -1
- package/dist/commands/challenge/delete.js +5 -5
- package/dist/commands/challenge/list.d.ts +5 -0
- package/dist/commands/challenge/list.js +11 -10
- package/dist/commands/challenge/run.d.ts +8 -1
- package/dist/commands/challenge/run.js +13 -8
- package/dist/commands/challenge/watch.d.ts +4 -1
- package/dist/commands/challenge/watch.js +8 -8
- package/dist/commands/{evaluation → experiment}/create.d.ts +4 -0
- package/dist/commands/{evaluation → experiment}/create.js +22 -21
- package/dist/commands/{evaluation → experiment}/list.js +17 -19
- package/dist/commands/experiment/recordings.d.ts +19 -0
- package/dist/commands/experiment/recordings.js +416 -0
- package/dist/commands/experiment/run.d.ts +17 -0
- package/dist/commands/experiment/run.js +67 -0
- package/dist/commands/init.js +2 -2
- package/dist/lib/api-client.d.ts +51 -10
- package/dist/lib/api-client.js +108 -39
- package/dist/lib/arguments.d.ts +3 -2
- package/dist/lib/arguments.js +5 -3
- package/dist/lib/challenge.d.ts +13 -18
- package/dist/lib/challenge.js +58 -62
- package/dist/lib/experiment/experimenter.d.ts +92 -0
- package/dist/lib/experiment/experimenter.js +368 -0
- package/dist/lib/{evaluation → experiment}/index.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/index.js +1 -1
- package/dist/lib/{evaluation → experiment}/runner.d.ts +2 -0
- package/dist/lib/{evaluation → experiment}/runner.js +21 -2
- package/dist/lib/{evaluation → experiment}/tui.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/tui.js +3 -3
- package/dist/lib/{evaluation → experiment}/types.d.ts +10 -4
- package/dist/lib/{evaluation → experiment}/types.js +5 -3
- package/dist/lib/flags.d.ts +47 -0
- package/dist/lib/flags.js +63 -0
- package/dist/lib/schemas.d.ts +63 -2
- package/dist/lib/schemas.js +27 -1
- package/dist/lib/utils.d.ts +9 -10
- package/dist/lib/utils.js +12 -12
- package/oclif.manifest.json +423 -64
- package/package.json +11 -8
- package/static/challenge.ts +12 -13
- package/static/experiment_template.ts +114 -0
- package/static/project_template/dev.env +5 -5
- package/static/project_template/prod.env +4 -4
- package/static/project_template/tsconfig.json +1 -1
- package/dist/commands/challenge/multi-upload.d.ts +0 -6
- package/dist/commands/challenge/multi-upload.js +0 -80
- package/dist/commands/evaluation/run.d.ts +0 -13
- package/dist/commands/evaluation/run.js +0 -61
- package/dist/lib/config.d.ts +0 -12
- package/dist/lib/config.js +0 -49
- package/dist/lib/evaluation/evaluator.d.ts +0 -88
- package/dist/lib/evaluation/evaluator.js +0 -268
- package/static/evaluation_template.ts +0 -69
- /package/dist/commands/{evaluation → experiment}/list.d.ts +0 -0
package/dist/lib/api-client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, HumanSchema, JobResponseSchema, RecordingDownloadUrlResponseSchema, RecordingsListResponseSchema, RunStatusSchema, UploadUrlResponseSchema, } from "./schemas.js";
|
|
2
4
|
const DEFAULT_PAGE_SIZE = 30;
|
|
3
5
|
const DEFAULT_CHALLENGE_SCHEMA = {
|
|
4
6
|
slug: "",
|
|
@@ -21,37 +23,40 @@ const DEFAULT_CHALLENGE_SCHEMA = {
|
|
|
21
23
|
},*/,
|
|
22
24
|
};
|
|
23
25
|
export class ApiClient {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
apiUrl;
|
|
27
|
+
kradleApiKey;
|
|
28
|
+
isStudio;
|
|
29
|
+
constructor(apiUrl, kradleApiKey, isStudio = false) {
|
|
30
|
+
this.apiUrl = apiUrl;
|
|
31
|
+
this.kradleApiKey = kradleApiKey;
|
|
32
|
+
this.isStudio = isStudio;
|
|
27
33
|
}
|
|
28
|
-
async request(
|
|
29
|
-
const
|
|
30
|
-
const fullUrl = `${baseUrl}/${url}`;
|
|
34
|
+
async request(endpoint, options) {
|
|
35
|
+
const fullUrl = `${this.apiUrl}/${endpoint}`;
|
|
31
36
|
const response = await fetch(fullUrl, {
|
|
32
37
|
...options,
|
|
33
38
|
headers: {
|
|
34
|
-
Authorization: `Bearer ${this.
|
|
39
|
+
Authorization: `Bearer ${this.kradleApiKey}`,
|
|
35
40
|
"Content-Type": "application/json",
|
|
36
41
|
...options.headers,
|
|
37
42
|
},
|
|
38
43
|
});
|
|
39
44
|
if (!response.ok) {
|
|
40
45
|
const text = await response.text();
|
|
41
|
-
throw new Error(`API call failed: ${
|
|
46
|
+
throw new Error(`API call failed: ${fullUrl} - ${response.status} ${response.statusText}\n${text}`);
|
|
42
47
|
}
|
|
43
48
|
return response;
|
|
44
49
|
}
|
|
45
|
-
async get(
|
|
46
|
-
const response = await this.request(
|
|
50
|
+
async get(url, options = {}, schema) {
|
|
51
|
+
const response = await this.request(url, {
|
|
47
52
|
method: "GET",
|
|
48
53
|
...options,
|
|
49
54
|
});
|
|
50
55
|
const data = await response.json();
|
|
51
56
|
return schema ? schema.parse(data) : data;
|
|
52
57
|
}
|
|
53
|
-
async post(
|
|
54
|
-
const response = await this.request(
|
|
58
|
+
async post(url, options = {}, schema) {
|
|
59
|
+
const response = await this.request(url, {
|
|
55
60
|
method: "POST",
|
|
56
61
|
...options,
|
|
57
62
|
});
|
|
@@ -62,14 +67,14 @@ export class ApiClient {
|
|
|
62
67
|
const data = JSON.parse(text);
|
|
63
68
|
return schema ? schema.parse(data) : data;
|
|
64
69
|
}
|
|
65
|
-
async put(
|
|
66
|
-
return await this.request(
|
|
70
|
+
async put(url, options = {}) {
|
|
71
|
+
return await this.request(url, {
|
|
67
72
|
method: "PUT",
|
|
68
73
|
...options,
|
|
69
74
|
});
|
|
70
75
|
}
|
|
71
|
-
async delete(
|
|
72
|
-
return await this.request(
|
|
76
|
+
async delete(url, options = {}) {
|
|
77
|
+
return await this.request(url, {
|
|
73
78
|
method: "DELETE",
|
|
74
79
|
...options,
|
|
75
80
|
});
|
|
@@ -97,7 +102,7 @@ export class ApiClient {
|
|
|
97
102
|
if (pageToken)
|
|
98
103
|
params.set("page_token", pageToken);
|
|
99
104
|
params.set("page_size", String(pageSize));
|
|
100
|
-
const response = await this.get(
|
|
105
|
+
const response = await this.get(`${url}?${params}`, {
|
|
101
106
|
headers: {
|
|
102
107
|
"Content-Type": "application/json",
|
|
103
108
|
},
|
|
@@ -109,7 +114,7 @@ export class ApiClient {
|
|
|
109
114
|
}
|
|
110
115
|
async getHuman() {
|
|
111
116
|
const url = "human";
|
|
112
|
-
return this.get(
|
|
117
|
+
return this.get(url, {}, HumanSchema);
|
|
113
118
|
}
|
|
114
119
|
async listChallenges() {
|
|
115
120
|
return this.listResource("challenges", "challenges", ChallengesResponseSchema);
|
|
@@ -122,7 +127,7 @@ export class ApiClient {
|
|
|
122
127
|
}
|
|
123
128
|
async getChallenge(challengeId) {
|
|
124
129
|
const url = `challenges/${challengeId}`;
|
|
125
|
-
return this.get(
|
|
130
|
+
return this.get(url, {}, ChallengeSchema);
|
|
126
131
|
}
|
|
127
132
|
/**
|
|
128
133
|
* Check if a challenge exists in the cloud.
|
|
@@ -132,7 +137,7 @@ export class ApiClient {
|
|
|
132
137
|
async challengeExists(slug) {
|
|
133
138
|
const url = `challenges/${slug}`;
|
|
134
139
|
try {
|
|
135
|
-
const res = await this.get(
|
|
140
|
+
const res = await this.get(url, {});
|
|
136
141
|
return res !== null && res !== undefined;
|
|
137
142
|
}
|
|
138
143
|
catch (error) {
|
|
@@ -142,37 +147,79 @@ export class ApiClient {
|
|
|
142
147
|
async createChallenge(slug) {
|
|
143
148
|
const challenge = { ...DEFAULT_CHALLENGE_SCHEMA, slug: slug, name: slug };
|
|
144
149
|
const url = "challenges";
|
|
145
|
-
return this.post(
|
|
150
|
+
return this.post(url, {
|
|
146
151
|
body: JSON.stringify(challenge),
|
|
147
152
|
});
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* Update a challenge definition in the cloud.
|
|
151
|
-
* @param
|
|
152
|
-
* @param challengeConfig - The challenge config to upload.
|
|
156
|
+
* @param challengeSlug - The slug of the challenge.
|
|
157
|
+
* @param challengeConfig - The challenge config to upload.
|
|
158
|
+
* @param visibility - The visibility to set.
|
|
153
159
|
* @returns The updated challenge.
|
|
154
160
|
*/
|
|
155
|
-
async updateChallenge(
|
|
156
|
-
const url = `challenges/${
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
body: JSON.stringify(config),
|
|
161
|
+
async updateChallenge(challengeSlug, challengeConfig, visibility) {
|
|
162
|
+
const url = `challenges/${challengeSlug}`;
|
|
163
|
+
const fullConfig = { ...challengeConfig, slug: challengeSlug, visibility };
|
|
164
|
+
await this.put(url, {
|
|
165
|
+
body: JSON.stringify(fullConfig),
|
|
161
166
|
});
|
|
162
167
|
}
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Update the visibility of a challenge.
|
|
170
|
+
* @param challengeSlug - The slug of the challenge.
|
|
171
|
+
* @param visibility - The visibility to set.
|
|
172
|
+
* @returns The updated challenge.
|
|
173
|
+
*/
|
|
174
|
+
async updateChallengeVisibility(challengeSlug, visibility) {
|
|
175
|
+
const url = `challenges/${challengeSlug}/visibility`;
|
|
176
|
+
await this.post(url, {
|
|
177
|
+
body: JSON.stringify({ visibility }),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Upload a challenge datapack to Google Cloud Storage.
|
|
182
|
+
* @param slug - The slug of the challenge.
|
|
183
|
+
* @param tarballPath - The path to the tarball file.
|
|
184
|
+
* @returns The upload URL.
|
|
185
|
+
*/
|
|
186
|
+
async uploadChallengeDatapack(slug, tarballPath) {
|
|
187
|
+
const uploadUrl = await this.getChallengeUploadUrl(slug);
|
|
188
|
+
if (!existsSync(tarballPath)) {
|
|
189
|
+
throw new Error(`Tarball not found at ${tarballPath}`);
|
|
190
|
+
}
|
|
191
|
+
const fileBuffer = await fs.readFile(tarballPath);
|
|
192
|
+
const response = await fetch(uploadUrl, {
|
|
193
|
+
method: "PUT",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/gzip",
|
|
196
|
+
"Content-Length": fileBuffer.length.toString(),
|
|
197
|
+
},
|
|
198
|
+
body: fileBuffer,
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`Failed to upload datapack: ${response.statusText}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the upload URL for a challenge datapack.
|
|
206
|
+
* @param slug - The slug of the challenge.
|
|
207
|
+
* @returns The upload URL.
|
|
208
|
+
*/
|
|
209
|
+
async getChallengeUploadUrl(slug) {
|
|
210
|
+
const response = await this.get(`challenges/${slug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
|
|
165
211
|
return response.uploadUrl;
|
|
166
212
|
}
|
|
167
|
-
async runChallenge(runData
|
|
213
|
+
async runChallenge(runData) {
|
|
168
214
|
const url = "jobs";
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
215
|
+
const payload = this.isStudio ? runData : { ...runData, jobType: "background" };
|
|
216
|
+
return this.post(url, {
|
|
217
|
+
body: JSON.stringify(payload),
|
|
218
|
+
}, JobResponseSchema);
|
|
172
219
|
}
|
|
173
220
|
async deleteChallenge(challengeId) {
|
|
174
221
|
const url = `challenges/${challengeId}`;
|
|
175
|
-
await this.delete(
|
|
222
|
+
await this.delete(url);
|
|
176
223
|
}
|
|
177
224
|
/**
|
|
178
225
|
* Get the status of a run.
|
|
@@ -181,7 +228,7 @@ export class ApiClient {
|
|
|
181
228
|
*/
|
|
182
229
|
async getRunStatus(runId) {
|
|
183
230
|
const url = `runs/${runId}`;
|
|
184
|
-
return this.get(
|
|
231
|
+
return this.get(url, {}, RunStatusSchema);
|
|
185
232
|
}
|
|
186
233
|
/**
|
|
187
234
|
* Add a tag to a run.
|
|
@@ -191,8 +238,30 @@ export class ApiClient {
|
|
|
191
238
|
*/
|
|
192
239
|
async tagRun(runId, tag) {
|
|
193
240
|
const url = `runs/${runId}/tag`;
|
|
194
|
-
await this.post(
|
|
241
|
+
await this.post(url, {
|
|
195
242
|
body: JSON.stringify({ tag }),
|
|
196
243
|
});
|
|
197
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Get recordings for a specific participant in a run.
|
|
247
|
+
* @param runId - The ID of the run.
|
|
248
|
+
* @param participantId - The ID of the participant.
|
|
249
|
+
* @returns Array of recording metadata.
|
|
250
|
+
*/
|
|
251
|
+
async getRunRecordings(runId, participantId) {
|
|
252
|
+
const url = `runs/${runId}/recordings/${participantId}`;
|
|
253
|
+
const response = await this.get(url, {}, RecordingsListResponseSchema);
|
|
254
|
+
return response.recordings;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get a signed download URL for a specific recording.
|
|
258
|
+
* @param runId - The ID of the run.
|
|
259
|
+
* @param participantId - The ID of the participant.
|
|
260
|
+
* @param timestamp - The timestamp of the recording.
|
|
261
|
+
* @returns Download URL and expiration time.
|
|
262
|
+
*/
|
|
263
|
+
async getRecordingDownloadUrl(runId, participantId, timestamp) {
|
|
264
|
+
const url = `runs/${runId}/recordings/${participantId}/downloadUrl?timestamp=${encodeURIComponent(timestamp)}`;
|
|
265
|
+
return this.get(url, {}, RecordingDownloadUrlResponseSchema);
|
|
266
|
+
}
|
|
198
267
|
}
|
package/dist/lib/arguments.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Arg } from "@oclif/core/interfaces";
|
|
|
2
2
|
/**
|
|
3
3
|
* Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
|
|
4
4
|
*/
|
|
5
|
-
export declare function getChallengeSlugArgument({ description }: {
|
|
5
|
+
export declare function getChallengeSlugArgument<R extends boolean = true>({ description, required, }: {
|
|
6
6
|
description: string;
|
|
7
|
-
|
|
7
|
+
required?: R;
|
|
8
|
+
}): Arg<R extends true ? string : string | undefined>;
|
package/dist/lib/arguments.js
CHANGED
|
@@ -3,10 +3,10 @@ const CHALLENGE_SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
|
|
5
5
|
*/
|
|
6
|
-
export function getChallengeSlugArgument({ description }) {
|
|
7
|
-
|
|
6
|
+
export function getChallengeSlugArgument({ description, required, }) {
|
|
7
|
+
const arg = Args.string({
|
|
8
8
|
description,
|
|
9
|
-
required: true,
|
|
9
|
+
required: required ?? true,
|
|
10
10
|
parse: async (input) => {
|
|
11
11
|
if (!CHALLENGE_SLUG_REGEX.test(input)) {
|
|
12
12
|
throw new Error(`Invalid challenge slug: ${input}. Challenge slugs must be lowercase alphanumeric characters and hyphens, and must not start or end with a hyphen.`);
|
|
@@ -14,4 +14,6 @@ export function getChallengeSlugArgument({ description }) {
|
|
|
14
14
|
return input;
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: Typescript can't handle the conditional "required"
|
|
18
|
+
return arg;
|
|
17
19
|
}
|
package/dist/lib/challenge.d.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import type { ApiClient } from "./api-client.js";
|
|
2
|
-
import type
|
|
3
|
-
import { type ChallengeSchemaType } from "./schemas.js";
|
|
2
|
+
import { type ChallengeConfigSchemaType } from "./schemas.js";
|
|
4
3
|
export declare const SOURCE_FOLDER = ".src";
|
|
5
4
|
export declare class Challenge {
|
|
6
|
-
readonly
|
|
7
|
-
private readonly
|
|
8
|
-
constructor(
|
|
9
|
-
private get kradleChallengesPath();
|
|
10
|
-
/**
|
|
11
|
-
* Get the short slug without username prefix
|
|
12
|
-
* e.g., "florianernst:challenge" -> "challenge"
|
|
13
|
-
*/
|
|
14
|
-
get shortSlug(): string;
|
|
5
|
+
readonly shortSlug: string;
|
|
6
|
+
private readonly kradleChallengesPath;
|
|
7
|
+
constructor(shortSlug: string, kradleChallengesPath: string);
|
|
15
8
|
/**
|
|
16
9
|
* Get the path to the challenge directory
|
|
17
10
|
*/
|
|
@@ -47,22 +40,24 @@ export declare class Challenge {
|
|
|
47
40
|
/**
|
|
48
41
|
* Load the challenge configuration from config.ts
|
|
49
42
|
*/
|
|
50
|
-
loadConfig(): Promise<
|
|
43
|
+
loadConfig(): Promise<ChallengeConfigSchemaType>;
|
|
51
44
|
/**
|
|
52
|
-
*
|
|
45
|
+
* Build the challenge datapack and upload it to the cloud.
|
|
46
|
+
* @param api - The API client to use.
|
|
47
|
+
* @param asPublic - Whether the challenge should be uploaded as public.
|
|
48
|
+
* @returns The config and datapack hash.
|
|
53
49
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
config: ChallengeSchemaType;
|
|
50
|
+
buildAndUpload(api: ApiClient, asPublic: boolean): Promise<{
|
|
51
|
+
config: ChallengeConfigSchemaType;
|
|
57
52
|
datapackHash: string;
|
|
58
53
|
}>;
|
|
59
54
|
/**
|
|
60
55
|
* Get all local challenges from the challenges directory
|
|
61
56
|
*/
|
|
62
|
-
static getLocalChallenges(): Promise<
|
|
57
|
+
static getLocalChallenges(): Promise<string[]>;
|
|
63
58
|
/**
|
|
64
59
|
* Create a new local challenge folder with challenge.ts template
|
|
65
60
|
* Note: config.ts is NOT created here - it should be generated later via `challenge config download`
|
|
66
61
|
*/
|
|
67
|
-
static createLocal(slug: string,
|
|
62
|
+
static createLocal(slug: string, kradleChallengesPath: string): Promise<void>;
|
|
68
63
|
}
|
package/dist/lib/challenge.js
CHANGED
|
@@ -4,25 +4,15 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import * as tar from "tar";
|
|
7
|
-
import {
|
|
7
|
+
import { ChallengeConfigSchema } from "./schemas.js";
|
|
8
8
|
import { executeNodeCommand, executeTypescriptFile, getStaticResourcePath, readDirSorted } from "./utils.js";
|
|
9
9
|
export const SOURCE_FOLDER = ".src";
|
|
10
10
|
export class Challenge {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
constructor(
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
}
|
|
17
|
-
get kradleChallengesPath() {
|
|
18
|
-
return path.resolve(this.config.KRADLE_CHALLENGES_PATH);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Get the short slug without username prefix
|
|
22
|
-
* e.g., "florianernst:challenge" -> "challenge"
|
|
23
|
-
*/
|
|
24
|
-
get shortSlug() {
|
|
25
|
-
return this.name.includes(":") ? this.name.split(":")[1] : this.name;
|
|
11
|
+
shortSlug;
|
|
12
|
+
kradleChallengesPath;
|
|
13
|
+
constructor(shortSlug, kradleChallengesPath) {
|
|
14
|
+
this.shortSlug = shortSlug;
|
|
15
|
+
this.kradleChallengesPath = path.resolve(kradleChallengesPath);
|
|
26
16
|
}
|
|
27
17
|
/**
|
|
28
18
|
* Get the path to the challenge directory
|
|
@@ -94,15 +84,27 @@ export class Challenge {
|
|
|
94
84
|
throw new Error(`Challenge file not found at ${this.challengePath}`);
|
|
95
85
|
}
|
|
96
86
|
try {
|
|
97
|
-
await executeTypescriptFile(this.challengePath,
|
|
87
|
+
await executeTypescriptFile(this.challengePath, {
|
|
88
|
+
KRADLE_CHALLENGES_PATH: this.kradleChallengesPath,
|
|
89
|
+
NAMESPACE: "kradle",
|
|
90
|
+
}, { silent });
|
|
98
91
|
}
|
|
99
92
|
catch (error) {
|
|
100
93
|
throw new Error(`Failed to build datapack: ${error instanceof Error ? error.message : error}`);
|
|
101
94
|
}
|
|
102
95
|
// @TODO - re-enable once we have a proper build pipeline
|
|
103
96
|
// Copy challenge.ts and config.ts to the src directory
|
|
104
|
-
await fs.cp(path.join(this.challengeDir, "challenge.ts"), path.join(this.
|
|
105
|
-
await fs.cp(path.join(this.challengeDir, "config.ts"), path.join(this.
|
|
97
|
+
await fs.cp(path.join(this.challengeDir, "challenge.ts"), path.join(this.kradleChallengesPath, this.shortSlug, SOURCE_FOLDER, "challenge.ts"));
|
|
98
|
+
await fs.cp(path.join(this.challengeDir, "config.ts"), path.join(this.kradleChallengesPath, this.shortSlug, SOURCE_FOLDER, "config.ts"));
|
|
99
|
+
// Create tarball
|
|
100
|
+
// We need to use the cwd option to make sure the structure of the tarball uses paths relative to the challenge directory
|
|
101
|
+
await tar.create({
|
|
102
|
+
"no-mtime": true,
|
|
103
|
+
file: this.tarballPath, // Output file name
|
|
104
|
+
gzip: true,
|
|
105
|
+
cwd: this.datapackPath, // Input directory
|
|
106
|
+
strict: true,
|
|
107
|
+
}, ["datapack", SOURCE_FOLDER]);
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
110
|
* Load the challenge configuration from config.ts
|
|
@@ -118,60 +120,54 @@ export class Challenge {
|
|
|
118
120
|
"--no-warnings",
|
|
119
121
|
"-e",
|
|
120
122
|
`console.log(JSON.stringify(require("${this.configPath}").config));`,
|
|
121
|
-
],
|
|
122
|
-
|
|
123
|
+
], {
|
|
124
|
+
KRADLE_CHALLENGES_PATH: this.kradleChallengesPath,
|
|
125
|
+
NAMESPACE: "kradle",
|
|
126
|
+
});
|
|
127
|
+
return ChallengeConfigSchema.parse(JSON.parse(stdout));
|
|
123
128
|
}
|
|
124
129
|
/**
|
|
125
|
-
*
|
|
130
|
+
* Build the challenge datapack and upload it to the cloud.
|
|
131
|
+
* @param api - The API client to use.
|
|
132
|
+
* @param asPublic - Whether the challenge should be uploaded as public.
|
|
133
|
+
* @returns The config and datapack hash.
|
|
126
134
|
*/
|
|
127
|
-
async
|
|
128
|
-
// Create tarball
|
|
129
|
-
// We need to use the cwd option to make sure the structure of the tarball uses paths relative to the challenge directory
|
|
130
|
-
await tar.create({
|
|
131
|
-
"no-mtime": true,
|
|
132
|
-
file: this.tarballPath, // Output file name
|
|
133
|
-
gzip: true,
|
|
134
|
-
cwd: this.datapackPath, // Input directory
|
|
135
|
-
strict: true,
|
|
136
|
-
}, ["datapack", SOURCE_FOLDER]);
|
|
137
|
-
const uploadUrl = await apiClient.getChallengeUploadUrl(this);
|
|
138
|
-
const fileBuffer = await fs.readFile(this.tarballPath);
|
|
139
|
-
const response = await fetch(uploadUrl, {
|
|
140
|
-
method: "PUT",
|
|
141
|
-
headers: {
|
|
142
|
-
"Content-Type": "application/gzip",
|
|
143
|
-
"Content-Length": fileBuffer.length.toString(),
|
|
144
|
-
},
|
|
145
|
-
body: fileBuffer,
|
|
146
|
-
});
|
|
147
|
-
if (!response.ok) {
|
|
148
|
-
throw new Error(`Failed to upload datapack: ${response.statusText}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
async buildAndUpload(api) {
|
|
135
|
+
async buildAndUpload(api, asPublic) {
|
|
152
136
|
// Ensure challenge exists locally
|
|
153
137
|
if (!this.exists()) {
|
|
154
|
-
throw new Error(`Challenge "${this.
|
|
138
|
+
throw new Error(`Challenge "${this.shortSlug}" does not exist locally. Make sure both the challenge.ts file and the config.ts file exist.`);
|
|
155
139
|
}
|
|
156
140
|
// Ensure challenge exists in the cloud
|
|
157
|
-
if (!(await api.challengeExists(this.
|
|
158
|
-
console.log(pc.yellow(`Challenge not found in cloud: ${this.
|
|
159
|
-
console.log(pc.yellow(`Creating challenge: ${this.
|
|
160
|
-
await api.createChallenge(this.
|
|
141
|
+
if (!(await api.challengeExists(this.shortSlug))) {
|
|
142
|
+
console.log(pc.yellow(`Challenge not found in cloud: ${this.shortSlug}`));
|
|
143
|
+
console.log(pc.yellow(`Creating challenge: ${this.shortSlug}`));
|
|
144
|
+
await api.createChallenge(this.shortSlug);
|
|
161
145
|
console.log(pc.green(`✓ Challenge created in cloud`));
|
|
162
146
|
}
|
|
163
|
-
// Load config
|
|
164
147
|
const config = await this.loadConfig();
|
|
148
|
+
// Ensure challenge's visibility is set to private - else, temporarily set it to private
|
|
149
|
+
const cloudChallengeVisibility = (await api.getChallenge(this.shortSlug)).visibility;
|
|
150
|
+
if (cloudChallengeVisibility === "public") {
|
|
151
|
+
console.log(pc.yellow(`⚠️ Cloud challenge is currently public. Setting its visibility to private...`));
|
|
152
|
+
await api.updateChallengeVisibility(this.shortSlug, "private");
|
|
153
|
+
console.log(pc.yellow(`✓ Challenge visibility set to private\n`));
|
|
154
|
+
}
|
|
165
155
|
// Upload config
|
|
166
156
|
console.log(pc.blue(`>> Uploading config: ${this.shortSlug}`));
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
// We have to set it to private because we cannot update public challenges
|
|
158
|
+
await api.updateChallenge(this.shortSlug, config, "private");
|
|
159
|
+
console.log(pc.green(`✓ Config uploaded\n`));
|
|
169
160
|
// Build and upload datapack
|
|
170
161
|
console.log(pc.blue(`>> Building datapack: ${this.shortSlug}`));
|
|
171
162
|
await this.build();
|
|
172
163
|
console.log(pc.blue(`>> Uploading datapack: ${this.shortSlug}`));
|
|
173
|
-
await this.
|
|
174
|
-
console.log(pc.green(`✓
|
|
164
|
+
await api.uploadChallengeDatapack(this.shortSlug, this.tarballPath);
|
|
165
|
+
console.log(pc.green(`✓ Build & upload complete for ${this.shortSlug}\n`));
|
|
166
|
+
if (asPublic) {
|
|
167
|
+
console.log(pc.green(`👀 Setting challenge visibility to public...`));
|
|
168
|
+
await api.updateChallengeVisibility(this.shortSlug, "public");
|
|
169
|
+
console.log(pc.green(`✓ Challenge visibility set to public\n`));
|
|
170
|
+
}
|
|
175
171
|
const datapackHash = await this.getDatapackHash();
|
|
176
172
|
return { config, datapackHash };
|
|
177
173
|
}
|
|
@@ -181,16 +177,16 @@ export class Challenge {
|
|
|
181
177
|
static async getLocalChallenges() {
|
|
182
178
|
const challengesDir = path.resolve(process.cwd(), "challenges");
|
|
183
179
|
if (!existsSync(challengesDir)) {
|
|
184
|
-
return
|
|
180
|
+
return [];
|
|
185
181
|
}
|
|
186
182
|
const entries = await readDirSorted(challengesDir);
|
|
187
|
-
const challenges =
|
|
183
|
+
const challenges = [];
|
|
188
184
|
for (const entry of entries) {
|
|
189
185
|
if (entry.isDirectory()) {
|
|
190
186
|
// Check if it has a challenge.ts file
|
|
191
187
|
const challengePath = path.join(challengesDir, entry.name, "challenge.ts");
|
|
192
188
|
if (existsSync(challengePath)) {
|
|
193
|
-
challenges
|
|
189
|
+
challenges.push(entry.name);
|
|
194
190
|
}
|
|
195
191
|
}
|
|
196
192
|
}
|
|
@@ -200,8 +196,8 @@ export class Challenge {
|
|
|
200
196
|
* Create a new local challenge folder with challenge.ts template
|
|
201
197
|
* Note: config.ts is NOT created here - it should be generated later via `challenge config download`
|
|
202
198
|
*/
|
|
203
|
-
static async createLocal(slug,
|
|
204
|
-
const challenge = new Challenge(slug,
|
|
199
|
+
static async createLocal(slug, kradleChallengesPath) {
|
|
200
|
+
const challenge = new Challenge(slug, kradleChallengesPath);
|
|
205
201
|
const challengeTemplatePath = getStaticResourcePath("challenge.ts");
|
|
206
202
|
// Create challenge directory
|
|
207
203
|
await fs.mkdir(challenge.challengeDir, { recursive: true });
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client.js";
|
|
2
|
+
import type { ExperimentMetadata, ExperimentOptions, Manifest, Progress } from "./types.js";
|
|
3
|
+
export declare class Experimenter {
|
|
4
|
+
private name;
|
|
5
|
+
private webUrl;
|
|
6
|
+
private api;
|
|
7
|
+
experimentDir: string;
|
|
8
|
+
metadataPath: string;
|
|
9
|
+
private runner?;
|
|
10
|
+
private tui?;
|
|
11
|
+
private currentVersion?;
|
|
12
|
+
constructor(name: string, webUrl: string, api: ApiClient);
|
|
13
|
+
/**
|
|
14
|
+
* Get paths for a specific version
|
|
15
|
+
*/
|
|
16
|
+
private getVersionPaths;
|
|
17
|
+
get configPath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Get the current version directory path
|
|
20
|
+
*/
|
|
21
|
+
getCurrentVersionDir(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Check if experiment exists
|
|
24
|
+
*/
|
|
25
|
+
exists(): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if config.ts exists (master config)
|
|
28
|
+
*/
|
|
29
|
+
configExists(): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Load experiment metadata
|
|
32
|
+
*/
|
|
33
|
+
loadMetadata(): Promise<ExperimentMetadata | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Save experiment metadata
|
|
36
|
+
*/
|
|
37
|
+
saveMetadata(metadata: ExperimentMetadata): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the current version number, or -1 if none exists
|
|
40
|
+
*/
|
|
41
|
+
getCurrentVersionNumber(): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Create a new version
|
|
44
|
+
*/
|
|
45
|
+
createNewVersion(): Promise<number>;
|
|
46
|
+
/**
|
|
47
|
+
* Get or create a version
|
|
48
|
+
* @param createNew - If true, always create a new version. Otherwise, use current version or create first one if none exists.
|
|
49
|
+
*/
|
|
50
|
+
getOrCreateVersion(createNew: boolean): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Load manifest from version
|
|
53
|
+
*/
|
|
54
|
+
loadManifest(version: number): Promise<Manifest>;
|
|
55
|
+
/**
|
|
56
|
+
* Load progress from version
|
|
57
|
+
*/
|
|
58
|
+
loadProgress(version: number): Promise<Progress | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Save progress to current version
|
|
61
|
+
*/
|
|
62
|
+
saveProgress(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Execute config.ts to generate manifest
|
|
65
|
+
*/
|
|
66
|
+
generateManifest(configPath: string): Promise<Manifest>;
|
|
67
|
+
/**
|
|
68
|
+
* Execute config.ts file and return the manifest
|
|
69
|
+
*/
|
|
70
|
+
private executeConfigFile;
|
|
71
|
+
/**
|
|
72
|
+
* Run the experiment
|
|
73
|
+
*/
|
|
74
|
+
run(options: ExperimentOptions): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Handle state change from runner
|
|
77
|
+
*/
|
|
78
|
+
private onRunStateChange;
|
|
79
|
+
/**
|
|
80
|
+
* Handle quit request
|
|
81
|
+
*/
|
|
82
|
+
private handleQuit;
|
|
83
|
+
/**
|
|
84
|
+
* Open run in browser
|
|
85
|
+
*/
|
|
86
|
+
private openRun;
|
|
87
|
+
/**
|
|
88
|
+
* Download recordings for a completed run with smart polling
|
|
89
|
+
* Polls for 90 seconds after run completion (matching pod grace period)
|
|
90
|
+
*/
|
|
91
|
+
private downloadRecordingsForRun;
|
|
92
|
+
}
|