@thyme-sh/cli 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 +142 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1127 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
// src/utils/ui.ts
|
|
12
|
+
import * as clack from "@clack/prompts";
|
|
13
|
+
import pc from "picocolors";
|
|
14
|
+
function intro2(title) {
|
|
15
|
+
clack.intro(pc.bgCyan(pc.black(` ${title} `)));
|
|
16
|
+
}
|
|
17
|
+
function outro2(message) {
|
|
18
|
+
clack.outro(message);
|
|
19
|
+
}
|
|
20
|
+
function error(message) {
|
|
21
|
+
clack.log.error(pc.red(message));
|
|
22
|
+
}
|
|
23
|
+
function info(message) {
|
|
24
|
+
clack.log.info(pc.cyan(message));
|
|
25
|
+
}
|
|
26
|
+
function warn(message) {
|
|
27
|
+
clack.log.warn(pc.yellow(message));
|
|
28
|
+
}
|
|
29
|
+
function step(message) {
|
|
30
|
+
clack.log.step(message);
|
|
31
|
+
}
|
|
32
|
+
function log2(message) {
|
|
33
|
+
console.log(message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/commands/init.ts
|
|
37
|
+
async function initCommand(projectName) {
|
|
38
|
+
intro2("Thyme CLI - Initialize Project");
|
|
39
|
+
let finalProjectName = projectName;
|
|
40
|
+
if (!finalProjectName) {
|
|
41
|
+
const name = await clack.text({
|
|
42
|
+
message: "What is your project name?",
|
|
43
|
+
placeholder: "my-thyme-project",
|
|
44
|
+
validate: (value) => {
|
|
45
|
+
if (!value) return "Project name is required";
|
|
46
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
47
|
+
return "Project name must be lowercase alphanumeric with hyphens";
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (clack.isCancel(name)) {
|
|
51
|
+
clack.cancel("Operation cancelled");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
finalProjectName = name;
|
|
55
|
+
}
|
|
56
|
+
const projectPath = join(process.cwd(), finalProjectName);
|
|
57
|
+
if (existsSync(projectPath)) {
|
|
58
|
+
error(`Directory "${finalProjectName}" already exists`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const spinner = clack.spinner();
|
|
62
|
+
spinner.start("Creating project structure...");
|
|
63
|
+
try {
|
|
64
|
+
await mkdir(projectPath, { recursive: true });
|
|
65
|
+
await mkdir(join(projectPath, "functions"), { recursive: true });
|
|
66
|
+
const packageJson = {
|
|
67
|
+
name: finalProjectName,
|
|
68
|
+
version: "0.1.0",
|
|
69
|
+
type: "module",
|
|
70
|
+
private: true,
|
|
71
|
+
scripts: {
|
|
72
|
+
dev: "thyme run"
|
|
73
|
+
},
|
|
74
|
+
dependencies: {
|
|
75
|
+
"@thyme-sh/sdk": "^0.1.0",
|
|
76
|
+
viem: "^2.21.54",
|
|
77
|
+
zod: "^3.24.1"
|
|
78
|
+
},
|
|
79
|
+
devDependencies: {
|
|
80
|
+
"@thyme-sh/cli": "^0.1.0",
|
|
81
|
+
typescript: "^5.7.2"
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(projectPath, "package.json"),
|
|
86
|
+
JSON.stringify(packageJson, null, 2)
|
|
87
|
+
);
|
|
88
|
+
const tsconfig = {
|
|
89
|
+
compilerOptions: {
|
|
90
|
+
target: "ES2022",
|
|
91
|
+
module: "ESNext",
|
|
92
|
+
moduleResolution: "bundler",
|
|
93
|
+
lib: ["ES2022", "DOM"],
|
|
94
|
+
strict: true,
|
|
95
|
+
esModuleInterop: true,
|
|
96
|
+
skipLibCheck: true,
|
|
97
|
+
forceConsistentCasingInFileNames: true,
|
|
98
|
+
resolveJsonModule: true
|
|
99
|
+
},
|
|
100
|
+
include: ["functions/**/*"]
|
|
101
|
+
};
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(projectPath, "tsconfig.json"),
|
|
104
|
+
JSON.stringify(tsconfig, null, 2)
|
|
105
|
+
);
|
|
106
|
+
const envExample = `# Simulation settings (for --simulate flag)
|
|
107
|
+
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-key
|
|
108
|
+
SIMULATE_ACCOUNT=0x742d35Cc6634C0532925a3b844Bc454e4438f44e
|
|
109
|
+
|
|
110
|
+
# Cloud authentication (set by \`thyme login\`)
|
|
111
|
+
THYME_AUTH_TOKEN=
|
|
112
|
+
|
|
113
|
+
# Cloud API URL (required - your Convex deployment URL)
|
|
114
|
+
# Example: https://your-deployment.convex.cloud
|
|
115
|
+
THYME_API_URL=
|
|
116
|
+
`;
|
|
117
|
+
await writeFile(join(projectPath, ".env.example"), envExample);
|
|
118
|
+
const gitignore = `node_modules/
|
|
119
|
+
dist/
|
|
120
|
+
.env
|
|
121
|
+
.env.local
|
|
122
|
+
*.log
|
|
123
|
+
`;
|
|
124
|
+
await writeFile(join(projectPath, ".gitignore"), gitignore);
|
|
125
|
+
const readme = `# ${finalProjectName}
|
|
126
|
+
|
|
127
|
+
A Thyme project for Web3 automation tasks.
|
|
128
|
+
|
|
129
|
+
## Getting Started
|
|
130
|
+
|
|
131
|
+
\`\`\`bash
|
|
132
|
+
# Install dependencies
|
|
133
|
+
npm install
|
|
134
|
+
|
|
135
|
+
# Create a new task
|
|
136
|
+
thyme new my-task
|
|
137
|
+
|
|
138
|
+
# Run a task locally
|
|
139
|
+
thyme run my-task
|
|
140
|
+
|
|
141
|
+
# Simulate on-chain
|
|
142
|
+
thyme run my-task --simulate
|
|
143
|
+
|
|
144
|
+
# Deploy to cloud
|
|
145
|
+
thyme login
|
|
146
|
+
thyme upload my-task
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Project Structure
|
|
150
|
+
|
|
151
|
+
\`\`\`
|
|
152
|
+
functions/
|
|
153
|
+
my-task/
|
|
154
|
+
index.ts # Task definition
|
|
155
|
+
args.json # Test arguments
|
|
156
|
+
\`\`\`
|
|
157
|
+
`;
|
|
158
|
+
await writeFile(join(projectPath, "README.md"), readme);
|
|
159
|
+
spinner.stop("Project created successfully!");
|
|
160
|
+
outro2(
|
|
161
|
+
`${pc.green("\u2713")} Project initialized!
|
|
162
|
+
|
|
163
|
+
Next steps:
|
|
164
|
+
${pc.cyan("cd")} ${finalProjectName}
|
|
165
|
+
${pc.cyan("npm install")}
|
|
166
|
+
${pc.cyan("thyme new")} my-task
|
|
167
|
+
${pc.cyan("thyme run")} my-task`
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
spinner.stop("Failed to create project");
|
|
171
|
+
error(err instanceof Error ? err.message : String(err));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/utils/tasks.ts
|
|
177
|
+
import { existsSync as existsSync2 } from "fs";
|
|
178
|
+
import { readdir } from "fs/promises";
|
|
179
|
+
import { join as join2 } from "path";
|
|
180
|
+
async function discoverTasks(projectRoot) {
|
|
181
|
+
const functionsDir = join2(projectRoot, "functions");
|
|
182
|
+
if (!existsSync2(functionsDir)) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const entries = await readdir(functionsDir, { withFileTypes: true });
|
|
187
|
+
return entries.filter((e) => e.isDirectory()).filter((e) => existsSync2(join2(functionsDir, e.name, "index.ts"))).map((e) => e.name);
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function getTaskPath(projectRoot, taskName) {
|
|
193
|
+
return join2(projectRoot, "functions", taskName, "index.ts");
|
|
194
|
+
}
|
|
195
|
+
function getTaskArgsPath(projectRoot, taskName) {
|
|
196
|
+
return join2(projectRoot, "functions", taskName, "args.json");
|
|
197
|
+
}
|
|
198
|
+
function isThymeProject(projectRoot) {
|
|
199
|
+
const functionsDir = join2(projectRoot, "functions");
|
|
200
|
+
return existsSync2(functionsDir);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/commands/list.ts
|
|
204
|
+
async function listCommand() {
|
|
205
|
+
intro2("Thyme CLI - List Tasks");
|
|
206
|
+
const projectRoot = process.cwd();
|
|
207
|
+
if (!isThymeProject(projectRoot)) {
|
|
208
|
+
outro2(pc.red("Not in a Thyme project"));
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const tasks = await discoverTasks(projectRoot);
|
|
212
|
+
if (tasks.length === 0) {
|
|
213
|
+
outro2(pc.yellow("No tasks found. Create one with `thyme new`"));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
step(`Found ${tasks.length} task(s):`);
|
|
217
|
+
for (const task of tasks) {
|
|
218
|
+
console.log(` ${pc.cyan("\u25CF")} ${task}`);
|
|
219
|
+
}
|
|
220
|
+
outro2("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/commands/login.ts
|
|
224
|
+
import { existsSync as existsSync4 } from "fs";
|
|
225
|
+
import { appendFile, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
226
|
+
import { join as join4 } from "path";
|
|
227
|
+
|
|
228
|
+
// src/utils/env.ts
|
|
229
|
+
import { existsSync as existsSync3 } from "fs";
|
|
230
|
+
import { join as join3 } from "path";
|
|
231
|
+
import { config } from "dotenv";
|
|
232
|
+
function loadEnv(projectRoot) {
|
|
233
|
+
const envPath = join3(projectRoot, ".env");
|
|
234
|
+
if (existsSync3(envPath)) {
|
|
235
|
+
config({ path: envPath });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function getEnv(key, fallback) {
|
|
239
|
+
return process.env[key] ?? fallback;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/commands/login.ts
|
|
243
|
+
async function loginCommand() {
|
|
244
|
+
intro2("Thyme CLI - Login");
|
|
245
|
+
const projectRoot = process.cwd();
|
|
246
|
+
const envPath = join4(projectRoot, ".env");
|
|
247
|
+
loadEnv(projectRoot);
|
|
248
|
+
info("To authenticate with Thyme Cloud:");
|
|
249
|
+
clack.log.message(
|
|
250
|
+
` 1. Visit ${pc.cyan("https://thyme.sh/settings/api-keys")}`
|
|
251
|
+
);
|
|
252
|
+
clack.log.message(" 2. Generate a new API token");
|
|
253
|
+
clack.log.message(" 3. Copy the token and paste it below");
|
|
254
|
+
clack.log.message("");
|
|
255
|
+
const token = await clack.password({
|
|
256
|
+
message: "Paste your API token:",
|
|
257
|
+
validate: (value) => {
|
|
258
|
+
if (!value) return "Token is required";
|
|
259
|
+
if (value.length < 10) return "Token seems too short";
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (clack.isCancel(token)) {
|
|
263
|
+
clack.cancel("Operation cancelled");
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
const spinner = clack.spinner();
|
|
267
|
+
spinner.start("Verifying token...");
|
|
268
|
+
try {
|
|
269
|
+
const apiUrl = getEnv("THYME_API_URL");
|
|
270
|
+
if (!apiUrl) {
|
|
271
|
+
spinner.stop("Configuration error");
|
|
272
|
+
error(
|
|
273
|
+
"THYME_API_URL is not set. Please set it to your Convex deployment URL (e.g., https://your-deployment.convex.cloud)"
|
|
274
|
+
);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const verifyResponse = await fetch(`${apiUrl}/api/auth/verify`, {
|
|
278
|
+
method: "GET",
|
|
279
|
+
headers: {
|
|
280
|
+
Authorization: `Bearer ${token}`
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (!verifyResponse.ok) {
|
|
284
|
+
spinner.stop("Token verification failed");
|
|
285
|
+
const errorText = await verifyResponse.text();
|
|
286
|
+
error(`Invalid token: ${errorText}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
const verifyData = await verifyResponse.json();
|
|
290
|
+
spinner.stop("Token verified!");
|
|
291
|
+
const saveSpinner = clack.spinner();
|
|
292
|
+
saveSpinner.start("Saving token...");
|
|
293
|
+
let envContent = "";
|
|
294
|
+
if (existsSync4(envPath)) {
|
|
295
|
+
envContent = await readFile(envPath, "utf-8");
|
|
296
|
+
}
|
|
297
|
+
const tokenRegex = /^THYME_AUTH_TOKEN=.*$/m;
|
|
298
|
+
if (tokenRegex.test(envContent)) {
|
|
299
|
+
envContent = envContent.replace(tokenRegex, `THYME_AUTH_TOKEN=${token}`);
|
|
300
|
+
await writeFile2(envPath, envContent);
|
|
301
|
+
} else {
|
|
302
|
+
const newLine = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
303
|
+
await appendFile(envPath, `${newLine}THYME_AUTH_TOKEN=${token}
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
saveSpinner.stop("Token saved successfully!");
|
|
307
|
+
clack.log.message("");
|
|
308
|
+
clack.log.success("Authenticated as:");
|
|
309
|
+
clack.log.message(
|
|
310
|
+
` ${pc.cyan("User:")} ${verifyData.user.name || verifyData.user.email}`
|
|
311
|
+
);
|
|
312
|
+
clack.log.message(` ${pc.cyan("Email:")} ${verifyData.user.email}`);
|
|
313
|
+
if (verifyData.organizations && verifyData.organizations.length > 0) {
|
|
314
|
+
clack.log.message("");
|
|
315
|
+
clack.log.message(`${pc.cyan("Organizations:")}`);
|
|
316
|
+
for (const org of verifyData.organizations) {
|
|
317
|
+
clack.log.message(` \u2022 ${org.name} ${pc.dim(`(${org.role})`)}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
outro2(`
|
|
321
|
+
You can now upload tasks with ${pc.cyan("thyme upload")}`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
spinner.stop("Failed to verify token");
|
|
324
|
+
error(err instanceof Error ? err.message : String(err));
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/commands/new.ts
|
|
330
|
+
import { existsSync as existsSync5 } from "fs";
|
|
331
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
332
|
+
import { join as join5 } from "path";
|
|
333
|
+
async function newCommand(taskName) {
|
|
334
|
+
intro2("Thyme CLI - Create New Task");
|
|
335
|
+
const projectRoot = process.cwd();
|
|
336
|
+
if (!isThymeProject(projectRoot)) {
|
|
337
|
+
error("Not in a Thyme project. Run `thyme init` first.");
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
let finalTaskName = taskName;
|
|
341
|
+
if (!finalTaskName) {
|
|
342
|
+
const name = await clack.text({
|
|
343
|
+
message: "What is your task name?",
|
|
344
|
+
placeholder: "my-task",
|
|
345
|
+
validate: (value) => {
|
|
346
|
+
if (!value) return "Task name is required";
|
|
347
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
348
|
+
return "Task name must be lowercase alphanumeric with hyphens";
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
if (clack.isCancel(name)) {
|
|
352
|
+
clack.cancel("Operation cancelled");
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
finalTaskName = name;
|
|
356
|
+
}
|
|
357
|
+
const taskPath = join5(projectRoot, "functions", finalTaskName);
|
|
358
|
+
if (existsSync5(taskPath)) {
|
|
359
|
+
error(`Task "${finalTaskName}" already exists`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
const spinner = clack.spinner();
|
|
363
|
+
spinner.start("Creating task...");
|
|
364
|
+
try {
|
|
365
|
+
await mkdir2(taskPath, { recursive: true });
|
|
366
|
+
const indexTs = `import { defineTask, z } from '@thyme-sh/sdk'
|
|
367
|
+
import { encodeFunctionData } from 'viem'
|
|
368
|
+
|
|
369
|
+
export default defineTask({
|
|
370
|
+
schema: z.object({
|
|
371
|
+
targetAddress: z.address(),
|
|
372
|
+
}),
|
|
373
|
+
|
|
374
|
+
async run(ctx) {
|
|
375
|
+
const { targetAddress } = ctx.args
|
|
376
|
+
|
|
377
|
+
// Your task logic here
|
|
378
|
+
console.log('Running task with address:', targetAddress)
|
|
379
|
+
|
|
380
|
+
// Example: Read from blockchain using the public client
|
|
381
|
+
// const balance = await ctx.client.getBalance({ address: targetAddress })
|
|
382
|
+
// const blockNumber = await ctx.client.getBlockNumber()
|
|
383
|
+
// const value = await ctx.client.readContract({
|
|
384
|
+
// address: targetAddress,
|
|
385
|
+
// abi: [...],
|
|
386
|
+
// functionName: 'balanceOf',
|
|
387
|
+
// args: [someAddress],
|
|
388
|
+
// })
|
|
389
|
+
|
|
390
|
+
// Example: Return calls to execute
|
|
391
|
+
return {
|
|
392
|
+
canExec: true,
|
|
393
|
+
calls: [
|
|
394
|
+
{
|
|
395
|
+
to: targetAddress,
|
|
396
|
+
data: '0x' as const,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Example with encodeFunctionData:
|
|
402
|
+
// const abi = [...] as const
|
|
403
|
+
// return {
|
|
404
|
+
// canExec: true,
|
|
405
|
+
// calls: [
|
|
406
|
+
// {
|
|
407
|
+
// to: targetAddress,
|
|
408
|
+
// data: encodeFunctionData({
|
|
409
|
+
// abi,
|
|
410
|
+
// functionName: 'transfer',
|
|
411
|
+
// args: [recipientAddress, 1000n],
|
|
412
|
+
// }),
|
|
413
|
+
// },
|
|
414
|
+
// ],
|
|
415
|
+
// }
|
|
416
|
+
|
|
417
|
+
// Or return false if conditions not met
|
|
418
|
+
// return {
|
|
419
|
+
// canExec: false,
|
|
420
|
+
// message: 'Conditions not met'
|
|
421
|
+
// }
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
`;
|
|
425
|
+
await writeFile3(join5(taskPath, "index.ts"), indexTs);
|
|
426
|
+
const args = {
|
|
427
|
+
targetAddress: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
|
428
|
+
};
|
|
429
|
+
await writeFile3(join5(taskPath, "args.json"), JSON.stringify(args, null, 2));
|
|
430
|
+
spinner.stop("Task created successfully!");
|
|
431
|
+
outro2(
|
|
432
|
+
`${pc.green("\u2713")} Task "${finalTaskName}" created!
|
|
433
|
+
|
|
434
|
+
Next steps:
|
|
435
|
+
${pc.cyan("Edit")} functions/${finalTaskName}/index.ts
|
|
436
|
+
${pc.cyan("Update")} functions/${finalTaskName}/args.json
|
|
437
|
+
${pc.cyan("thyme run")} ${finalTaskName}`
|
|
438
|
+
);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
spinner.stop("Failed to create task");
|
|
441
|
+
error(err instanceof Error ? err.message : String(err));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/commands/run.ts
|
|
447
|
+
import { existsSync as existsSync6 } from "fs";
|
|
448
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
449
|
+
import { http, createPublicClient, formatEther } from "viem";
|
|
450
|
+
|
|
451
|
+
// src/deno/runner.ts
|
|
452
|
+
import { spawn } from "child_process";
|
|
453
|
+
import { dirname, resolve } from "path";
|
|
454
|
+
async function runInDeno(taskPath, args, config2) {
|
|
455
|
+
const taskDir = dirname(resolve(taskPath));
|
|
456
|
+
const absoluteTaskPath = resolve(taskPath);
|
|
457
|
+
const denoFlags = ["run", "--no-prompt"];
|
|
458
|
+
denoFlags.push(`--allow-read=${taskDir}`);
|
|
459
|
+
if (config2.memory) {
|
|
460
|
+
denoFlags.push(`--v8-flags=--max-old-space-size=${config2.memory}`);
|
|
461
|
+
}
|
|
462
|
+
if (config2.network) {
|
|
463
|
+
denoFlags.push("--allow-net");
|
|
464
|
+
}
|
|
465
|
+
denoFlags.push("-");
|
|
466
|
+
const execScript = `
|
|
467
|
+
import task from '${absoluteTaskPath}';
|
|
468
|
+
import { createPublicClient, http } from 'npm:viem@2.21.54';
|
|
469
|
+
|
|
470
|
+
// Create RPC request counter
|
|
471
|
+
let rpcRequestCount = 0;
|
|
472
|
+
|
|
473
|
+
// Wrap the http transport to count requests
|
|
474
|
+
const countingHttp = (url) => {
|
|
475
|
+
const baseTransport = http(url);
|
|
476
|
+
return (config) => {
|
|
477
|
+
const transport = baseTransport(config);
|
|
478
|
+
return {
|
|
479
|
+
...transport,
|
|
480
|
+
request: async (params) => {
|
|
481
|
+
rpcRequestCount++;
|
|
482
|
+
return transport.request(params);
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Create public client for blockchain reads
|
|
489
|
+
const client = createPublicClient({
|
|
490
|
+
transport: countingHttp(${config2.rpcUrl ? `'${config2.rpcUrl}'` : "undefined"}),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const context = {
|
|
494
|
+
args: ${JSON.stringify(args)},
|
|
495
|
+
client,
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
// Track execution time and memory
|
|
500
|
+
const startTime = performance.now();
|
|
501
|
+
const startMemory = Deno.memoryUsage().heapUsed;
|
|
502
|
+
|
|
503
|
+
const result = await task.run(context);
|
|
504
|
+
|
|
505
|
+
const endTime = performance.now();
|
|
506
|
+
const endMemory = Deno.memoryUsage().heapUsed;
|
|
507
|
+
|
|
508
|
+
const executionTime = endTime - startTime;
|
|
509
|
+
const memoryUsed = endMemory - startMemory;
|
|
510
|
+
|
|
511
|
+
console.log('__THYME_RESULT__' + JSON.stringify(result));
|
|
512
|
+
console.log('__THYME_STATS__' + JSON.stringify({ executionTime, memoryUsed, rpcRequestCount }));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.error('Task execution error:', error instanceof Error ? error.message : String(error));
|
|
515
|
+
Deno.exit(1);
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
return new Promise((resolve2) => {
|
|
519
|
+
const proc = spawn("deno", denoFlags, {
|
|
520
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
521
|
+
timeout: config2.timeout * 1e3,
|
|
522
|
+
cwd: taskDir
|
|
523
|
+
});
|
|
524
|
+
let stdout = "";
|
|
525
|
+
let stderr = "";
|
|
526
|
+
const logs = [];
|
|
527
|
+
proc.stdin?.write(execScript);
|
|
528
|
+
proc.stdin?.end();
|
|
529
|
+
proc.stdout?.on("data", (data) => {
|
|
530
|
+
stdout += data.toString();
|
|
531
|
+
});
|
|
532
|
+
proc.stderr?.on("data", (data) => {
|
|
533
|
+
stderr += data.toString();
|
|
534
|
+
});
|
|
535
|
+
proc.on("close", (code) => {
|
|
536
|
+
if (code !== 0) {
|
|
537
|
+
resolve2({
|
|
538
|
+
success: false,
|
|
539
|
+
logs,
|
|
540
|
+
error: stderr || `Process exited with code ${code}`
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const lines = stdout.trim().split("\n");
|
|
546
|
+
let resultLine;
|
|
547
|
+
let statsLine;
|
|
548
|
+
for (const line of lines) {
|
|
549
|
+
if (line.startsWith("__THYME_RESULT__")) {
|
|
550
|
+
resultLine = line.substring("__THYME_RESULT__".length);
|
|
551
|
+
} else if (line.startsWith("__THYME_STATS__")) {
|
|
552
|
+
statsLine = line.substring("__THYME_STATS__".length);
|
|
553
|
+
} else if (line.trim()) {
|
|
554
|
+
logs.push(line.trim());
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!resultLine) {
|
|
558
|
+
throw new Error("No result found in output");
|
|
559
|
+
}
|
|
560
|
+
const result = JSON.parse(resultLine);
|
|
561
|
+
const stats = statsLine ? JSON.parse(statsLine) : {
|
|
562
|
+
executionTime: void 0,
|
|
563
|
+
memoryUsed: void 0,
|
|
564
|
+
rpcRequestCount: void 0
|
|
565
|
+
};
|
|
566
|
+
resolve2({
|
|
567
|
+
success: true,
|
|
568
|
+
result,
|
|
569
|
+
logs,
|
|
570
|
+
executionTime: stats.executionTime,
|
|
571
|
+
memoryUsed: stats.memoryUsed,
|
|
572
|
+
rpcRequestCount: stats.rpcRequestCount
|
|
573
|
+
});
|
|
574
|
+
} catch (error2) {
|
|
575
|
+
resolve2({
|
|
576
|
+
success: false,
|
|
577
|
+
logs,
|
|
578
|
+
error: `Failed to parse result: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
proc.on("error", (error2) => {
|
|
583
|
+
resolve2({
|
|
584
|
+
success: false,
|
|
585
|
+
logs,
|
|
586
|
+
error: `Failed to spawn Deno: ${error2.message}`
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async function checkDeno() {
|
|
592
|
+
return new Promise((resolve2) => {
|
|
593
|
+
const proc = spawn("deno", ["--version"], { stdio: "ignore" });
|
|
594
|
+
proc.on("close", (code) => resolve2(code === 0));
|
|
595
|
+
proc.on("error", () => resolve2(false));
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/commands/run.ts
|
|
600
|
+
async function runCommand(taskName, options = {}) {
|
|
601
|
+
intro2("Thyme CLI - Run Task");
|
|
602
|
+
const projectRoot = process.cwd();
|
|
603
|
+
loadEnv(projectRoot);
|
|
604
|
+
if (!isThymeProject(projectRoot)) {
|
|
605
|
+
error("Not in a Thyme project");
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
const hasDeno = await checkDeno();
|
|
609
|
+
if (!hasDeno) {
|
|
610
|
+
error("Deno is not installed. Please install Deno: https://deno.land/");
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
let finalTaskName = taskName;
|
|
614
|
+
if (!finalTaskName) {
|
|
615
|
+
const tasks = await discoverTasks(projectRoot);
|
|
616
|
+
if (tasks.length === 0) {
|
|
617
|
+
error("No tasks found. Create one with `thyme new`");
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
const selected = await clack.select({
|
|
621
|
+
message: "Select a task to run:",
|
|
622
|
+
options: tasks.map((task) => ({ value: task, label: task }))
|
|
623
|
+
});
|
|
624
|
+
if (clack.isCancel(selected)) {
|
|
625
|
+
clack.cancel("Operation cancelled");
|
|
626
|
+
process.exit(0);
|
|
627
|
+
}
|
|
628
|
+
finalTaskName = selected;
|
|
629
|
+
}
|
|
630
|
+
const taskPath = getTaskPath(projectRoot, finalTaskName);
|
|
631
|
+
const argsPath = getTaskArgsPath(projectRoot, finalTaskName);
|
|
632
|
+
if (!existsSync6(taskPath)) {
|
|
633
|
+
error(`Task "${finalTaskName}" not found`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const config2 = {
|
|
637
|
+
memory: 128,
|
|
638
|
+
timeout: 30,
|
|
639
|
+
network: true,
|
|
640
|
+
rpcUrl: getEnv("RPC_URL")
|
|
641
|
+
};
|
|
642
|
+
let args = {};
|
|
643
|
+
if (existsSync6(argsPath)) {
|
|
644
|
+
try {
|
|
645
|
+
const argsData = await readFile2(argsPath, "utf-8");
|
|
646
|
+
args = JSON.parse(argsData);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
warn(
|
|
649
|
+
`Failed to load args.json: ${err instanceof Error ? err.message : String(err)}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const spinner = clack.spinner();
|
|
654
|
+
spinner.start("Executing task in Deno sandbox...");
|
|
655
|
+
const result = await runInDeno(taskPath, args, config2);
|
|
656
|
+
if (!result.success) {
|
|
657
|
+
spinner.stop("Task execution failed");
|
|
658
|
+
error(result.error ?? "Unknown error");
|
|
659
|
+
if (result.logs.length > 0) {
|
|
660
|
+
step("Task output:");
|
|
661
|
+
for (const taskLog of result.logs) {
|
|
662
|
+
log2(` ${taskLog}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
spinner.stop("Task executed successfully");
|
|
668
|
+
if (result.logs.length > 0) {
|
|
669
|
+
log2("");
|
|
670
|
+
step("Task output:");
|
|
671
|
+
for (const taskLog of result.logs) {
|
|
672
|
+
log2(` ${taskLog}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (!result.result) {
|
|
676
|
+
error("No result returned from task");
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
log2("");
|
|
680
|
+
if (result.result.canExec) {
|
|
681
|
+
info(
|
|
682
|
+
`${pc.green("\u2713")} Result: canExec = true (${result.result.calls.length} call(s))`
|
|
683
|
+
);
|
|
684
|
+
log2("");
|
|
685
|
+
step("Calls to execute:");
|
|
686
|
+
for (const call of result.result.calls) {
|
|
687
|
+
log2(` ${pc.cyan("\u2192")} to: ${call.to}`);
|
|
688
|
+
log2(` data: ${call.data.slice(0, 20)}...`);
|
|
689
|
+
}
|
|
690
|
+
if (options.simulate) {
|
|
691
|
+
log2("");
|
|
692
|
+
await simulateCalls(result.result.calls);
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
warn("Result: canExec = false");
|
|
696
|
+
info(`Message: ${result.result.message}`);
|
|
697
|
+
}
|
|
698
|
+
log2("");
|
|
699
|
+
if (result.executionTime !== void 0 || result.memoryUsed !== void 0 || result.rpcRequestCount !== void 0) {
|
|
700
|
+
step("Execution stats:");
|
|
701
|
+
if (result.executionTime !== void 0) {
|
|
702
|
+
log2(` Duration: ${result.executionTime.toFixed(2)}ms`);
|
|
703
|
+
}
|
|
704
|
+
if (result.memoryUsed !== void 0) {
|
|
705
|
+
const memoryMB = (result.memoryUsed / 1024 / 1024).toFixed(2);
|
|
706
|
+
log2(` Memory: ${memoryMB}MB`);
|
|
707
|
+
}
|
|
708
|
+
if (result.rpcRequestCount !== void 0) {
|
|
709
|
+
log2(` RPC Requests: ${result.rpcRequestCount}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (result.result?.canExec && !options.simulate) {
|
|
713
|
+
log2("");
|
|
714
|
+
info(
|
|
715
|
+
`${pc.dim("\u{1F4A1} Tip: Test calls on-chain with:")} ${pc.cyan(`thyme run ${finalTaskName} --simulate`)}`
|
|
716
|
+
);
|
|
717
|
+
outro2("");
|
|
718
|
+
} else {
|
|
719
|
+
outro2("");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function simulateCalls(calls) {
|
|
723
|
+
const rpcUrl = getEnv("RPC_URL");
|
|
724
|
+
const account = getEnv("SIMULATE_ACCOUNT");
|
|
725
|
+
if (!rpcUrl || !account) {
|
|
726
|
+
warn("Simulation requires RPC_URL and SIMULATE_ACCOUNT in .env file");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const spinner = clack.spinner();
|
|
730
|
+
spinner.start("Simulating on-chain...");
|
|
731
|
+
try {
|
|
732
|
+
const client = createPublicClient({
|
|
733
|
+
transport: http(rpcUrl)
|
|
734
|
+
});
|
|
735
|
+
const chainId = await client.getChainId();
|
|
736
|
+
const blockNumber = await client.getBlockNumber();
|
|
737
|
+
spinner.stop("Simulating on-chain...");
|
|
738
|
+
log2("");
|
|
739
|
+
info(`Chain ID: ${chainId}`);
|
|
740
|
+
info(`Block: ${blockNumber}`);
|
|
741
|
+
info(`Account: ${account}`);
|
|
742
|
+
const simulationSpinner = clack.spinner();
|
|
743
|
+
simulationSpinner.start("Running simulation...");
|
|
744
|
+
for (const call of calls) {
|
|
745
|
+
try {
|
|
746
|
+
await client.call({
|
|
747
|
+
account,
|
|
748
|
+
to: call.to,
|
|
749
|
+
data: call.data
|
|
750
|
+
});
|
|
751
|
+
} catch (err) {
|
|
752
|
+
simulationSpinner.stop("Simulation failed");
|
|
753
|
+
log2("");
|
|
754
|
+
error(
|
|
755
|
+
`Call to ${call.to} would revert: ${err instanceof Error ? err.message : String(err)}`
|
|
756
|
+
);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const gasPrice = await client.getGasPrice();
|
|
761
|
+
simulationSpinner.stop("Simulation successful!");
|
|
762
|
+
log2("");
|
|
763
|
+
step("Simulation results:");
|
|
764
|
+
log2(` ${pc.green("\u2713")} All calls would succeed`);
|
|
765
|
+
log2(` Gas price: ${formatEther(gasPrice)} ETH`);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
spinner.stop("Simulation failed");
|
|
768
|
+
log2("");
|
|
769
|
+
error(err instanceof Error ? err.message : String(err));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/commands/upload.ts
|
|
774
|
+
import { existsSync as existsSync7 } from "fs";
|
|
775
|
+
|
|
776
|
+
// src/utils/bundler.ts
|
|
777
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
778
|
+
import { build } from "esbuild";
|
|
779
|
+
async function bundleTask(taskPath) {
|
|
780
|
+
const source = await readFile3(taskPath, "utf-8");
|
|
781
|
+
const nodeBuiltins = [
|
|
782
|
+
"assert",
|
|
783
|
+
"buffer",
|
|
784
|
+
"child_process",
|
|
785
|
+
"cluster",
|
|
786
|
+
"crypto",
|
|
787
|
+
"dgram",
|
|
788
|
+
"dns",
|
|
789
|
+
"events",
|
|
790
|
+
"fs",
|
|
791
|
+
"http",
|
|
792
|
+
"http2",
|
|
793
|
+
"https",
|
|
794
|
+
"net",
|
|
795
|
+
"os",
|
|
796
|
+
"path",
|
|
797
|
+
"perf_hooks",
|
|
798
|
+
"process",
|
|
799
|
+
"querystring",
|
|
800
|
+
"readline",
|
|
801
|
+
"stream",
|
|
802
|
+
"string_decoder",
|
|
803
|
+
"timers",
|
|
804
|
+
"tls",
|
|
805
|
+
"tty",
|
|
806
|
+
"url",
|
|
807
|
+
"util",
|
|
808
|
+
"v8",
|
|
809
|
+
"vm",
|
|
810
|
+
"zlib",
|
|
811
|
+
// Node: prefix versions
|
|
812
|
+
"node:assert",
|
|
813
|
+
"node:buffer",
|
|
814
|
+
"node:child_process",
|
|
815
|
+
"node:cluster",
|
|
816
|
+
"node:crypto",
|
|
817
|
+
"node:dgram",
|
|
818
|
+
"node:dns",
|
|
819
|
+
"node:events",
|
|
820
|
+
"node:fs",
|
|
821
|
+
"node:http",
|
|
822
|
+
"node:http2",
|
|
823
|
+
"node:https",
|
|
824
|
+
"node:net",
|
|
825
|
+
"node:os",
|
|
826
|
+
"node:path",
|
|
827
|
+
"node:perf_hooks",
|
|
828
|
+
"node:process",
|
|
829
|
+
"node:querystring",
|
|
830
|
+
"node:readline",
|
|
831
|
+
"node:stream",
|
|
832
|
+
"node:string_decoder",
|
|
833
|
+
"node:timers",
|
|
834
|
+
"node:tls",
|
|
835
|
+
"node:tty",
|
|
836
|
+
"node:url",
|
|
837
|
+
"node:util",
|
|
838
|
+
"node:v8",
|
|
839
|
+
"node:vm",
|
|
840
|
+
"node:zlib"
|
|
841
|
+
];
|
|
842
|
+
const result = await build({
|
|
843
|
+
entryPoints: [taskPath],
|
|
844
|
+
bundle: true,
|
|
845
|
+
format: "esm",
|
|
846
|
+
platform: "neutral",
|
|
847
|
+
target: "esnext",
|
|
848
|
+
write: false,
|
|
849
|
+
treeShaking: true,
|
|
850
|
+
minify: false,
|
|
851
|
+
// Keep readable for debugging
|
|
852
|
+
sourcemap: false,
|
|
853
|
+
external: nodeBuiltins,
|
|
854
|
+
// Don't bundle Node.js built-ins
|
|
855
|
+
logLevel: "silent"
|
|
856
|
+
});
|
|
857
|
+
if (result.outputFiles.length === 0) {
|
|
858
|
+
throw new Error("No output from bundler");
|
|
859
|
+
}
|
|
860
|
+
const bundle = result.outputFiles[0].text;
|
|
861
|
+
return {
|
|
862
|
+
source,
|
|
863
|
+
bundle
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/utils/compress.ts
|
|
868
|
+
import { compressTask as sdkCompressTask } from "@thyme-sh/sdk";
|
|
869
|
+
function compressTask(source, bundle) {
|
|
870
|
+
const { zipBuffer, checksum } = sdkCompressTask(source, bundle);
|
|
871
|
+
return {
|
|
872
|
+
zipBuffer: Buffer.from(zipBuffer),
|
|
873
|
+
checksum
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/utils/schema-extractor.ts
|
|
878
|
+
function extractSchemaFromTask(taskCode) {
|
|
879
|
+
try {
|
|
880
|
+
const schemaExtractor = `
|
|
881
|
+
const { z } = require('zod');
|
|
882
|
+
const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
883
|
+
|
|
884
|
+
// Extended z with address validator
|
|
885
|
+
const zodExtended = {
|
|
886
|
+
...z,
|
|
887
|
+
address: () => z.string().refine((val) => /^0x[a-fA-F0-9]{40}$/.test(val), {
|
|
888
|
+
message: 'Invalid Ethereum address',
|
|
889
|
+
}),
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
// Mock defineTask to capture schema
|
|
893
|
+
let capturedSchema = null;
|
|
894
|
+
const defineTask = (definition) => {
|
|
895
|
+
if (definition.schema) {
|
|
896
|
+
capturedSchema = definition.schema;
|
|
897
|
+
}
|
|
898
|
+
return definition;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Mock viem exports
|
|
902
|
+
const encodeFunctionData = () => '0x';
|
|
903
|
+
|
|
904
|
+
// Evaluate task code
|
|
905
|
+
${taskCode}
|
|
906
|
+
|
|
907
|
+
// Convert schema to JSON Schema
|
|
908
|
+
if (capturedSchema) {
|
|
909
|
+
const jsonSchema = zodToJsonSchema(capturedSchema, { target: 'openApi3' });
|
|
910
|
+
console.log(JSON.stringify(jsonSchema));
|
|
911
|
+
} else {
|
|
912
|
+
console.log('null');
|
|
913
|
+
}
|
|
914
|
+
`;
|
|
915
|
+
const schemaMatch = taskCode.match(/schema:\s*z\.object\(\{([^}]+)\}\)/);
|
|
916
|
+
if (!schemaMatch) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const schemaContent = schemaMatch[1];
|
|
920
|
+
const fields = {};
|
|
921
|
+
const fieldMatches = schemaContent.matchAll(
|
|
922
|
+
/(\w+):\s*z\.(\w+)\(\)/g
|
|
923
|
+
);
|
|
924
|
+
for (const match of fieldMatches) {
|
|
925
|
+
const [, fieldName, fieldType] = match;
|
|
926
|
+
if (fieldName && fieldType) {
|
|
927
|
+
let jsonType = "string";
|
|
928
|
+
switch (fieldType) {
|
|
929
|
+
case "string":
|
|
930
|
+
jsonType = "string";
|
|
931
|
+
break;
|
|
932
|
+
case "number":
|
|
933
|
+
jsonType = "number";
|
|
934
|
+
break;
|
|
935
|
+
case "boolean":
|
|
936
|
+
jsonType = "boolean";
|
|
937
|
+
break;
|
|
938
|
+
case "address":
|
|
939
|
+
jsonType = "string";
|
|
940
|
+
break;
|
|
941
|
+
default:
|
|
942
|
+
jsonType = "string";
|
|
943
|
+
}
|
|
944
|
+
fields[fieldName] = {
|
|
945
|
+
type: jsonType,
|
|
946
|
+
...fieldType === "address" && {
|
|
947
|
+
pattern: "^0x[a-fA-F0-9]{40}$",
|
|
948
|
+
description: "Ethereum address"
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (Object.keys(fields).length === 0) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const jsonSchema = {
|
|
957
|
+
type: "object",
|
|
958
|
+
properties: fields,
|
|
959
|
+
required: Object.keys(fields)
|
|
960
|
+
};
|
|
961
|
+
return JSON.stringify(jsonSchema);
|
|
962
|
+
} catch (err) {
|
|
963
|
+
console.error("Error extracting schema:", err);
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/commands/upload.ts
|
|
969
|
+
async function uploadCommand(taskName, organizationId) {
|
|
970
|
+
intro2("Thyme CLI - Upload Task");
|
|
971
|
+
const projectRoot = process.cwd();
|
|
972
|
+
loadEnv(projectRoot);
|
|
973
|
+
if (!isThymeProject(projectRoot)) {
|
|
974
|
+
error("Not in a Thyme project");
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
const authToken = getEnv("THYME_AUTH_TOKEN");
|
|
978
|
+
if (!authToken) {
|
|
979
|
+
error("Not authenticated. Run `thyme login` first.");
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const apiUrl = getEnv("THYME_API_URL");
|
|
983
|
+
if (!apiUrl) {
|
|
984
|
+
error(
|
|
985
|
+
"THYME_API_URL is not set. Please set it to your Convex deployment URL in .env"
|
|
986
|
+
);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
let finalTaskName = taskName;
|
|
990
|
+
if (!finalTaskName) {
|
|
991
|
+
const tasks = await discoverTasks(projectRoot);
|
|
992
|
+
if (tasks.length === 0) {
|
|
993
|
+
error("No tasks found. Create one with `thyme new`");
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
const selected = await clack.select({
|
|
997
|
+
message: "Select a task to upload:",
|
|
998
|
+
options: tasks.map((task) => ({ value: task, label: task }))
|
|
999
|
+
});
|
|
1000
|
+
if (clack.isCancel(selected)) {
|
|
1001
|
+
clack.cancel("Operation cancelled");
|
|
1002
|
+
process.exit(0);
|
|
1003
|
+
}
|
|
1004
|
+
finalTaskName = selected;
|
|
1005
|
+
}
|
|
1006
|
+
const orgSpinner = clack.spinner();
|
|
1007
|
+
orgSpinner.start("Fetching organizations...");
|
|
1008
|
+
let organizations = [];
|
|
1009
|
+
try {
|
|
1010
|
+
const verifyResponse = await fetch(`${apiUrl}/api/auth/verify`, {
|
|
1011
|
+
method: "GET",
|
|
1012
|
+
headers: {
|
|
1013
|
+
Authorization: `Bearer ${authToken}`
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
if (!verifyResponse.ok) {
|
|
1017
|
+
orgSpinner.stop("Failed to fetch organizations");
|
|
1018
|
+
error("Failed to authenticate. Please run `thyme login` again.");
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
const verifyData = await verifyResponse.json();
|
|
1022
|
+
organizations = verifyData.organizations || [];
|
|
1023
|
+
orgSpinner.stop("Organizations loaded");
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
orgSpinner.stop("Failed to fetch organizations");
|
|
1026
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
if (organizations.length === 0) {
|
|
1030
|
+
error("You are not a member of any organizations. Please create or join an organization first.");
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
let selectedOrgId = organizationId;
|
|
1034
|
+
if (selectedOrgId) {
|
|
1035
|
+
const orgExists = organizations.find((org) => org.id === selectedOrgId);
|
|
1036
|
+
if (!orgExists) {
|
|
1037
|
+
error(`Organization with ID "${selectedOrgId}" not found or you don't have access to it.`);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
const selected = await clack.select({
|
|
1042
|
+
message: "Select an organization to upload to:",
|
|
1043
|
+
options: organizations.map((org) => ({
|
|
1044
|
+
value: org.id,
|
|
1045
|
+
label: `${org.name} ${pc.dim(`(${org.role})`)}`
|
|
1046
|
+
}))
|
|
1047
|
+
});
|
|
1048
|
+
if (clack.isCancel(selected)) {
|
|
1049
|
+
clack.cancel("Operation cancelled");
|
|
1050
|
+
process.exit(0);
|
|
1051
|
+
}
|
|
1052
|
+
selectedOrgId = selected;
|
|
1053
|
+
}
|
|
1054
|
+
const taskPath = getTaskPath(projectRoot, finalTaskName);
|
|
1055
|
+
if (!existsSync7(taskPath)) {
|
|
1056
|
+
error(`Task "${finalTaskName}" not found`);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
const spinner = clack.spinner();
|
|
1060
|
+
spinner.start("Bundling task...");
|
|
1061
|
+
try {
|
|
1062
|
+
const { source, bundle } = await bundleTask(taskPath);
|
|
1063
|
+
spinner.message("Extracting schema...");
|
|
1064
|
+
const schema = extractSchemaFromTask(source);
|
|
1065
|
+
spinner.message("Compressing files...");
|
|
1066
|
+
const { zipBuffer, checksum } = compressTask(source, bundle);
|
|
1067
|
+
spinner.message("Uploading to cloud...");
|
|
1068
|
+
const formData = new FormData();
|
|
1069
|
+
formData.append(
|
|
1070
|
+
"data",
|
|
1071
|
+
JSON.stringify({
|
|
1072
|
+
organizationId: selectedOrgId,
|
|
1073
|
+
checkSum: checksum,
|
|
1074
|
+
schema: schema || void 0
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1077
|
+
formData.append("blob", new Blob([zipBuffer]), "task.zip");
|
|
1078
|
+
const response = await fetch(`${apiUrl}/api/task/upload`, {
|
|
1079
|
+
method: "POST",
|
|
1080
|
+
headers: {
|
|
1081
|
+
Authorization: `Bearer ${authToken}`
|
|
1082
|
+
},
|
|
1083
|
+
body: formData
|
|
1084
|
+
});
|
|
1085
|
+
if (!response.ok) {
|
|
1086
|
+
const errorText = await response.text();
|
|
1087
|
+
throw new Error(`Upload failed: ${errorText}`);
|
|
1088
|
+
}
|
|
1089
|
+
const result = await response.json();
|
|
1090
|
+
spinner.stop("Task uploaded successfully!");
|
|
1091
|
+
const selectedOrg = organizations.find((org) => org.id === selectedOrgId);
|
|
1092
|
+
clack.log.message("");
|
|
1093
|
+
clack.log.success("Upload details:");
|
|
1094
|
+
clack.log.message(` ${pc.dim("Task:")} ${pc.cyan(finalTaskName)}`);
|
|
1095
|
+
clack.log.message(
|
|
1096
|
+
` ${pc.dim("Organization:")} ${pc.cyan(selectedOrg?.name || "Unknown")}`
|
|
1097
|
+
);
|
|
1098
|
+
clack.log.message(
|
|
1099
|
+
` ${pc.dim("Size:")} ${(zipBuffer.length / 1024).toFixed(2)} KB`
|
|
1100
|
+
);
|
|
1101
|
+
clack.log.message(` ${pc.dim("Checksum:")} ${checksum.slice(0, 16)}...`);
|
|
1102
|
+
if (result.taskId) {
|
|
1103
|
+
clack.log.message(` ${pc.dim("Task ID:")} ${pc.green(result.taskId)}`);
|
|
1104
|
+
}
|
|
1105
|
+
outro2(
|
|
1106
|
+
`${pc.green("\u2713")} Task uploaded!
|
|
1107
|
+
|
|
1108
|
+
Configure triggers in the dashboard: ${pc.cyan("https://thyme.sh/dashboard")}`
|
|
1109
|
+
);
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
spinner.stop("Upload failed");
|
|
1112
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/index.ts
|
|
1118
|
+
var program = new Command();
|
|
1119
|
+
program.name("thyme").description("CLI for developing and deploying Thyme tasks").version("0.1.0");
|
|
1120
|
+
program.command("init").description("Initialize a new Thyme project").argument("[name]", "Project name").action(initCommand);
|
|
1121
|
+
program.command("new").description("Create a new task").argument("[name]", "Task name").action(newCommand);
|
|
1122
|
+
program.command("run").description("Run a task locally").argument("[task]", "Task name").option("--simulate", "Simulate on-chain execution").action((task, options) => runCommand(task, options));
|
|
1123
|
+
program.command("list").description("List all tasks").action(listCommand);
|
|
1124
|
+
program.command("login").description("Authenticate with Thyme Cloud").action(loginCommand);
|
|
1125
|
+
program.command("upload").description("Upload a task to Thyme Cloud").argument("[task]", "Task name").option("-o, --organization <id>", "Organization ID to upload to").action((task, options) => uploadCommand(task, options.organization));
|
|
1126
|
+
program.parse();
|
|
1127
|
+
//# sourceMappingURL=index.js.map
|