@getjack/jack 0.1.30 → 0.1.31
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/package.json +4 -5
- package/src/commands/domain.ts +148 -17
- package/src/commands/domains.ts +28 -2
- package/src/commands/services.ts +300 -1
- package/src/commands/skills.ts +58 -1
- package/src/lib/auth/login-flow.ts +34 -0
- package/src/lib/control-plane.ts +156 -0
- package/src/lib/mcp-config.ts +26 -4
- package/src/lib/output.ts +4 -2
- package/src/lib/picker.ts +3 -1
- package/src/lib/project-operations.ts +38 -4
- package/src/lib/services/cron-create.ts +73 -0
- package/src/lib/services/cron-delete.ts +66 -0
- package/src/lib/services/cron-list.ts +59 -0
- package/src/lib/services/cron-test.ts +93 -0
- package/src/lib/services/cron-utils.ts +78 -0
- package/src/lib/services/domain-operations.ts +89 -18
- package/src/mcp/server.ts +20 -0
- package/src/mcp/tools/index.ts +279 -0
package/src/commands/services.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { formatSize } from "../lib/format.ts";
|
|
|
5
5
|
import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
|
|
6
6
|
import { readProjectLink } from "../lib/project-link.ts";
|
|
7
7
|
import { parseWranglerResources } from "../lib/resources.ts";
|
|
8
|
+
import { createCronSchedule } from "../lib/services/cron-create.ts";
|
|
9
|
+
import { deleteCronSchedule } from "../lib/services/cron-delete.ts";
|
|
10
|
+
import { listCronSchedules } from "../lib/services/cron-list.ts";
|
|
11
|
+
import { COMMON_CRON_PATTERNS, testCronExpression } from "../lib/services/cron-test.ts";
|
|
8
12
|
import { createDatabase } from "../lib/services/db-create.ts";
|
|
9
13
|
import {
|
|
10
14
|
DestructiveOperationError,
|
|
@@ -111,9 +115,11 @@ export default async function services(
|
|
|
111
115
|
return await dbCommand(args, options);
|
|
112
116
|
case "storage":
|
|
113
117
|
return await storageCommand(args, options);
|
|
118
|
+
case "cron":
|
|
119
|
+
return await cronCommand(args, options);
|
|
114
120
|
default:
|
|
115
121
|
error(`Unknown service: ${subcommand}`);
|
|
116
|
-
info("Available: db, storage");
|
|
122
|
+
info("Available: db, storage, cron");
|
|
117
123
|
process.exit(1);
|
|
118
124
|
}
|
|
119
125
|
}
|
|
@@ -125,6 +131,7 @@ function showHelp(): void {
|
|
|
125
131
|
console.error("Commands:");
|
|
126
132
|
console.error(" db Manage database");
|
|
127
133
|
console.error(" storage Manage storage (R2 buckets)");
|
|
134
|
+
console.error(" cron Manage scheduled tasks");
|
|
128
135
|
console.error("");
|
|
129
136
|
console.error("Run 'jack services <command>' for more information.");
|
|
130
137
|
console.error("");
|
|
@@ -1181,3 +1188,295 @@ async function storageDelete(args: string[], options: ServiceOptions): Promise<v
|
|
|
1181
1188
|
process.exit(1);
|
|
1182
1189
|
}
|
|
1183
1190
|
}
|
|
1191
|
+
|
|
1192
|
+
// ============================================================================
|
|
1193
|
+
// Cron Commands
|
|
1194
|
+
// ============================================================================
|
|
1195
|
+
|
|
1196
|
+
function showCronHelp(): void {
|
|
1197
|
+
console.error("");
|
|
1198
|
+
info("jack services cron - Manage scheduled tasks");
|
|
1199
|
+
console.error("");
|
|
1200
|
+
console.error("Actions:");
|
|
1201
|
+
console.error(' create <expression> Add a cron schedule (e.g., "0 * * * *")');
|
|
1202
|
+
console.error(" list List all cron schedules");
|
|
1203
|
+
console.error(" delete <expression> Remove a cron schedule");
|
|
1204
|
+
console.error(" test <expression> Validate and preview a cron expression");
|
|
1205
|
+
console.error("");
|
|
1206
|
+
console.error("Examples:");
|
|
1207
|
+
console.error(' jack services cron create "0 * * * *" Schedule hourly runs');
|
|
1208
|
+
console.error(" jack services cron list Show all schedules");
|
|
1209
|
+
console.error(' jack services cron delete "0 * * * *" Remove a schedule');
|
|
1210
|
+
console.error(' jack services cron test "*/15 * * * *" Preview next run times');
|
|
1211
|
+
console.error("");
|
|
1212
|
+
console.error("Common patterns:");
|
|
1213
|
+
for (const pattern of COMMON_CRON_PATTERNS) {
|
|
1214
|
+
console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
|
|
1215
|
+
}
|
|
1216
|
+
console.error("");
|
|
1217
|
+
console.error("Note: Cron schedules require Jack Cloud (managed) projects.");
|
|
1218
|
+
console.error("All times are in UTC.");
|
|
1219
|
+
console.error("");
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function cronCommand(args: string[], options: ServiceOptions): Promise<void> {
|
|
1223
|
+
// Check if any argument is --help or -h
|
|
1224
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1225
|
+
return showCronHelp();
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const action = args[0] || "list"; // Default to list
|
|
1229
|
+
|
|
1230
|
+
switch (action) {
|
|
1231
|
+
case "help":
|
|
1232
|
+
return showCronHelp();
|
|
1233
|
+
case "create":
|
|
1234
|
+
return await cronCreate(args.slice(1), options);
|
|
1235
|
+
case "list":
|
|
1236
|
+
return await cronList(options);
|
|
1237
|
+
case "delete":
|
|
1238
|
+
return await cronDelete(args.slice(1), options);
|
|
1239
|
+
case "test":
|
|
1240
|
+
return await cronTest(args.slice(1), options);
|
|
1241
|
+
default:
|
|
1242
|
+
error(`Unknown action: ${action}`);
|
|
1243
|
+
info("Available: create, list, delete, test");
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Create a new cron schedule
|
|
1250
|
+
*/
|
|
1251
|
+
async function cronCreate(args: string[], options: ServiceOptions): Promise<void> {
|
|
1252
|
+
const expression = args[0];
|
|
1253
|
+
|
|
1254
|
+
if (!expression) {
|
|
1255
|
+
console.error("");
|
|
1256
|
+
error("Cron expression required");
|
|
1257
|
+
info('Usage: jack services cron create "<expression>"');
|
|
1258
|
+
console.error("");
|
|
1259
|
+
console.error("Common patterns:");
|
|
1260
|
+
for (const pattern of COMMON_CRON_PATTERNS) {
|
|
1261
|
+
console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
|
|
1262
|
+
}
|
|
1263
|
+
console.error("");
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
outputSpinner.start("Creating cron schedule...");
|
|
1268
|
+
try {
|
|
1269
|
+
const result = await createCronSchedule(process.cwd(), expression, {
|
|
1270
|
+
interactive: true,
|
|
1271
|
+
});
|
|
1272
|
+
outputSpinner.stop();
|
|
1273
|
+
|
|
1274
|
+
// Track telemetry
|
|
1275
|
+
track(Events.SERVICE_CREATED, {
|
|
1276
|
+
service_type: "cron",
|
|
1277
|
+
created: result.created,
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
console.error("");
|
|
1281
|
+
success("Cron schedule created");
|
|
1282
|
+
console.error("");
|
|
1283
|
+
item(`Expression: ${result.expression}`);
|
|
1284
|
+
item(`Schedule: ${result.description}`);
|
|
1285
|
+
item(`Next run: ${result.nextRunAt}`);
|
|
1286
|
+
console.error("");
|
|
1287
|
+
info("Make sure your Worker handles POST /__scheduled requests.");
|
|
1288
|
+
console.error("");
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
outputSpinner.stop();
|
|
1291
|
+
console.error("");
|
|
1292
|
+
error(`Failed to create cron schedule: ${err instanceof Error ? err.message : String(err)}`);
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* List all cron schedules
|
|
1299
|
+
*/
|
|
1300
|
+
async function cronList(options: ServiceOptions): Promise<void> {
|
|
1301
|
+
outputSpinner.start("Fetching cron schedules...");
|
|
1302
|
+
try {
|
|
1303
|
+
const schedules = await listCronSchedules(process.cwd());
|
|
1304
|
+
outputSpinner.stop();
|
|
1305
|
+
|
|
1306
|
+
if (schedules.length === 0) {
|
|
1307
|
+
console.error("");
|
|
1308
|
+
info("No cron schedules found for this project.");
|
|
1309
|
+
console.error("");
|
|
1310
|
+
info('Create one with: jack services cron create "0 * * * *"');
|
|
1311
|
+
console.error("");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
console.error("");
|
|
1316
|
+
success(`Found ${schedules.length} cron schedule${schedules.length === 1 ? "" : "s"}:`);
|
|
1317
|
+
console.error("");
|
|
1318
|
+
|
|
1319
|
+
for (const schedule of schedules) {
|
|
1320
|
+
const statusIcon = schedule.enabled ? "+" : "-";
|
|
1321
|
+
item(`${statusIcon} ${schedule.expression}`);
|
|
1322
|
+
item(` ${schedule.description}`);
|
|
1323
|
+
item(` Next: ${schedule.nextRunAt}`);
|
|
1324
|
+
if (schedule.lastRunAt) {
|
|
1325
|
+
const statusLabel =
|
|
1326
|
+
schedule.lastRunStatus === "success"
|
|
1327
|
+
? "success"
|
|
1328
|
+
: `${schedule.lastRunStatus} (${schedule.consecutiveFailures} failures)`;
|
|
1329
|
+
item(` Last: ${schedule.lastRunAt} - ${statusLabel}`);
|
|
1330
|
+
if (schedule.lastRunDurationMs !== null) {
|
|
1331
|
+
item(` Duration: ${schedule.lastRunDurationMs}ms`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
console.error("");
|
|
1335
|
+
}
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
outputSpinner.stop();
|
|
1338
|
+
console.error("");
|
|
1339
|
+
error(`Failed to list cron schedules: ${err instanceof Error ? err.message : String(err)}`);
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Delete a cron schedule
|
|
1346
|
+
*/
|
|
1347
|
+
async function cronDelete(args: string[], options: ServiceOptions): Promise<void> {
|
|
1348
|
+
const expression = args[0];
|
|
1349
|
+
|
|
1350
|
+
if (!expression) {
|
|
1351
|
+
console.error("");
|
|
1352
|
+
error("Cron expression required");
|
|
1353
|
+
info('Usage: jack services cron delete "<expression>"');
|
|
1354
|
+
console.error("");
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Show what will be deleted
|
|
1359
|
+
console.error("");
|
|
1360
|
+
info(`Cron schedule: ${expression}`);
|
|
1361
|
+
console.error("");
|
|
1362
|
+
warn("This will stop all future scheduled runs");
|
|
1363
|
+
console.error("");
|
|
1364
|
+
|
|
1365
|
+
// Confirm deletion
|
|
1366
|
+
const { promptSelect } = await import("../lib/hooks.ts");
|
|
1367
|
+
const choice = await promptSelect(
|
|
1368
|
+
["Yes, delete", "No, cancel"],
|
|
1369
|
+
`Delete cron schedule '${expression}'?`,
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
if (choice !== 0) {
|
|
1373
|
+
info("Cancelled");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
outputSpinner.start("Deleting cron schedule...");
|
|
1378
|
+
try {
|
|
1379
|
+
const result = await deleteCronSchedule(process.cwd(), expression, {
|
|
1380
|
+
interactive: true,
|
|
1381
|
+
});
|
|
1382
|
+
outputSpinner.stop();
|
|
1383
|
+
|
|
1384
|
+
// Track telemetry
|
|
1385
|
+
track(Events.SERVICE_DELETED, {
|
|
1386
|
+
service_type: "cron",
|
|
1387
|
+
deleted: result.deleted,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
console.error("");
|
|
1391
|
+
success("Cron schedule deleted");
|
|
1392
|
+
console.error("");
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
outputSpinner.stop();
|
|
1395
|
+
console.error("");
|
|
1396
|
+
error(`Failed to delete cron schedule: ${err instanceof Error ? err.message : String(err)}`);
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Test/validate a cron expression
|
|
1403
|
+
*/
|
|
1404
|
+
async function cronTest(args: string[], options: ServiceOptions): Promise<void> {
|
|
1405
|
+
const expression = args[0];
|
|
1406
|
+
|
|
1407
|
+
if (!expression) {
|
|
1408
|
+
console.error("");
|
|
1409
|
+
error("Cron expression required");
|
|
1410
|
+
info('Usage: jack services cron test "<expression>"');
|
|
1411
|
+
console.error("");
|
|
1412
|
+
console.error("Common patterns:");
|
|
1413
|
+
for (const pattern of COMMON_CRON_PATTERNS) {
|
|
1414
|
+
console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
|
|
1415
|
+
}
|
|
1416
|
+
console.error("");
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Test the expression (no production trigger by default)
|
|
1421
|
+
const result = await testCronExpression(process.cwd(), expression, {
|
|
1422
|
+
interactive: true,
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
if (!result.valid) {
|
|
1426
|
+
console.error("");
|
|
1427
|
+
error(`Invalid cron expression: ${result.error}`);
|
|
1428
|
+
console.error("");
|
|
1429
|
+
console.error("Common patterns:");
|
|
1430
|
+
for (const pattern of COMMON_CRON_PATTERNS) {
|
|
1431
|
+
console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
|
|
1432
|
+
}
|
|
1433
|
+
console.error("");
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
console.error("");
|
|
1438
|
+
success("Valid cron expression");
|
|
1439
|
+
console.error("");
|
|
1440
|
+
item(`Expression: ${result.expression}`);
|
|
1441
|
+
item(`Schedule: ${result.description}`);
|
|
1442
|
+
console.error("");
|
|
1443
|
+
info("Next 5 runs (UTC):");
|
|
1444
|
+
for (const time of result.nextTimes!) {
|
|
1445
|
+
item(` ${time.toISOString()}`);
|
|
1446
|
+
}
|
|
1447
|
+
console.error("");
|
|
1448
|
+
|
|
1449
|
+
// Check if this is a managed project for optional trigger
|
|
1450
|
+
const link = await readProjectLink(process.cwd());
|
|
1451
|
+
if (link?.deploy_mode === "managed") {
|
|
1452
|
+
const { promptSelect } = await import("../lib/hooks.ts");
|
|
1453
|
+
const choice = await promptSelect(
|
|
1454
|
+
["Yes, trigger now", "No, skip"],
|
|
1455
|
+
"Trigger scheduled handler on production?",
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
if (choice === 0) {
|
|
1459
|
+
outputSpinner.start("Triggering scheduled handler...");
|
|
1460
|
+
try {
|
|
1461
|
+
const triggerResult = await testCronExpression(process.cwd(), expression, {
|
|
1462
|
+
triggerProduction: true,
|
|
1463
|
+
interactive: true,
|
|
1464
|
+
});
|
|
1465
|
+
outputSpinner.stop();
|
|
1466
|
+
|
|
1467
|
+
if (triggerResult.triggerResult) {
|
|
1468
|
+
console.error("");
|
|
1469
|
+
success(
|
|
1470
|
+
`Triggered! Status: ${triggerResult.triggerResult.status}, Duration: ${triggerResult.triggerResult.durationMs}ms`,
|
|
1471
|
+
);
|
|
1472
|
+
console.error("");
|
|
1473
|
+
}
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
outputSpinner.stop();
|
|
1476
|
+
console.error("");
|
|
1477
|
+
error(`Failed to trigger: ${err instanceof Error ? err.message : String(err)}`);
|
|
1478
|
+
console.error("");
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
package/src/commands/skills.ts
CHANGED
|
@@ -124,9 +124,12 @@ export default async function skills(subcommand?: string, args: string[] = []):
|
|
|
124
124
|
case "remove":
|
|
125
125
|
case "rm":
|
|
126
126
|
return await removeSkill(args[0]);
|
|
127
|
+
case "upgrade":
|
|
128
|
+
case "update":
|
|
129
|
+
return await upgradeSkill(args[0]);
|
|
127
130
|
default:
|
|
128
131
|
error(`Unknown subcommand: ${subcommand}`);
|
|
129
|
-
info("Available: run, list, remove");
|
|
132
|
+
info("Available: run, list, remove, upgrade");
|
|
130
133
|
process.exit(1);
|
|
131
134
|
}
|
|
132
135
|
}
|
|
@@ -139,6 +142,7 @@ async function showHelp(): Promise<void> {
|
|
|
139
142
|
console.log(" run <name> Install (if needed) and launch agent with skill");
|
|
140
143
|
console.log(" list List installed skills in current project");
|
|
141
144
|
console.log(" remove <name> Remove a skill from project");
|
|
145
|
+
console.log(" upgrade <name> Re-install skill to get latest version");
|
|
142
146
|
console.log("");
|
|
143
147
|
const skills = await fetchAvailableSkills();
|
|
144
148
|
if (skills.length > 0) {
|
|
@@ -333,3 +337,56 @@ async function removeSkill(skillName?: string): Promise<void> {
|
|
|
333
337
|
info(`Skill ${skillName} not found in project`);
|
|
334
338
|
}
|
|
335
339
|
}
|
|
340
|
+
|
|
341
|
+
async function upgradeSkill(skillName?: string): Promise<void> {
|
|
342
|
+
if (!skillName) {
|
|
343
|
+
error("Missing skill name");
|
|
344
|
+
info("Usage: jack skills upgrade <name>");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const projectDir = process.cwd();
|
|
349
|
+
const skillPath = join(projectDir, ".claude/skills", skillName);
|
|
350
|
+
|
|
351
|
+
// Check if installed
|
|
352
|
+
if (!existsSync(skillPath)) {
|
|
353
|
+
error(`Skill ${skillName} not installed`);
|
|
354
|
+
info(`Install it first: jack skills run ${skillName}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Remove existing installation
|
|
359
|
+
info(`Removing old version of ${skillName}...`);
|
|
360
|
+
const dirs = [".agents/skills", ".claude/skills", ".codex/skills", ".cursor/skills"];
|
|
361
|
+
for (const dir of dirs) {
|
|
362
|
+
const path = join(projectDir, dir, skillName);
|
|
363
|
+
if (existsSync(path)) {
|
|
364
|
+
await rm(path, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Re-install from GitHub
|
|
369
|
+
info(`Installing latest ${skillName}...`);
|
|
370
|
+
const agentFlags = SUPPORTED_AGENTS.flatMap((a) => ["--agent", a]);
|
|
371
|
+
const proc = Bun.spawn(
|
|
372
|
+
["npx", "skills", "add", SKILLS_REPO, "--skill", skillName, ...agentFlags, "--yes"],
|
|
373
|
+
{
|
|
374
|
+
cwd: projectDir,
|
|
375
|
+
stdout: "pipe",
|
|
376
|
+
stderr: "pipe",
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const stderr = await new Response(proc.stderr).text();
|
|
381
|
+
await proc.exited;
|
|
382
|
+
|
|
383
|
+
if (proc.exitCode !== 0) {
|
|
384
|
+
error(`Failed to upgrade skill: ${skillName}`);
|
|
385
|
+
if (stderr.trim()) {
|
|
386
|
+
console.error(stderr);
|
|
387
|
+
}
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
success(`Upgraded ${skillName} to latest version`);
|
|
392
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { text } from "@clack/prompts";
|
|
5
5
|
import {
|
|
6
|
+
applyReferralCode,
|
|
6
7
|
checkUsernameAvailable,
|
|
7
8
|
getCurrentUserProfile,
|
|
8
9
|
registerUser,
|
|
@@ -114,6 +115,11 @@ export async function runLoginFlow(options?: LoginFlowOptions): Promise<LoginFlo
|
|
|
114
115
|
isNewUser = await promptForUsername(tokens.user.email, tokens.user.first_name);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Prompt for referral code for new users only (one-time, no retry)
|
|
119
|
+
if (isNewUser && process.stdout.isTTY) {
|
|
120
|
+
await promptForReferral();
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
console.error("");
|
|
118
124
|
const displayName = tokens.user.first_name || "you";
|
|
119
125
|
if (isNewUser) {
|
|
@@ -294,3 +300,31 @@ function normalizeToUsername(input: string): string {
|
|
|
294
300
|
.replace(/^-+|-+$/g, "")
|
|
295
301
|
.slice(0, 39);
|
|
296
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Prompt new users for a referral code (one-time, no retry on failure).
|
|
306
|
+
*/
|
|
307
|
+
async function promptForReferral(): Promise<void> {
|
|
308
|
+
console.error("");
|
|
309
|
+
const referralInput = await text({
|
|
310
|
+
message: "Were you referred by someone? Enter their username (or press Enter to skip):",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (isCancel(referralInput)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const code = referralInput.trim();
|
|
318
|
+
if (!code) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const result = await applyReferralCode(code);
|
|
324
|
+
if (result.applied) {
|
|
325
|
+
success("Referral applied! You'll both get a bonus when you upgrade.");
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Silently continue - referral is not critical
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -666,6 +666,35 @@ export async function setUsername(username: string): Promise<SetUsernameResponse
|
|
|
666
666
|
return response.json() as Promise<SetUsernameResponse>;
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
export interface ApplyReferralResult {
|
|
670
|
+
applied: boolean;
|
|
671
|
+
reason?: "invalid" | "self_referral" | "already_referred";
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Apply a referral code (username) for the current user.
|
|
676
|
+
* Returns whether the code was applied successfully.
|
|
677
|
+
*/
|
|
678
|
+
export async function applyReferralCode(code: string): Promise<ApplyReferralResult> {
|
|
679
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
680
|
+
|
|
681
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/referral/apply`, {
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers: { "Content-Type": "application/json" },
|
|
684
|
+
body: JSON.stringify({ code }),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
if (response.status === 429) {
|
|
688
|
+
return { applied: false, reason: "invalid" };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
return { applied: false, reason: "invalid" };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return response.json() as Promise<ApplyReferralResult>;
|
|
696
|
+
}
|
|
697
|
+
|
|
669
698
|
/**
|
|
670
699
|
* Get the current user's profile including username.
|
|
671
700
|
*/
|
|
@@ -777,6 +806,36 @@ export interface LogSessionInfo {
|
|
|
777
806
|
expires_at: string;
|
|
778
807
|
}
|
|
779
808
|
|
|
809
|
+
// ============================================================================
|
|
810
|
+
// Cron Schedule Types
|
|
811
|
+
// ============================================================================
|
|
812
|
+
|
|
813
|
+
export interface CronScheduleInfo {
|
|
814
|
+
id: string;
|
|
815
|
+
expression: string;
|
|
816
|
+
description: string;
|
|
817
|
+
enabled: boolean;
|
|
818
|
+
next_run_at: string;
|
|
819
|
+
last_run_at: string | null;
|
|
820
|
+
last_run_status: string | null;
|
|
821
|
+
last_run_duration_ms: number | null;
|
|
822
|
+
consecutive_failures: number;
|
|
823
|
+
created_at: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export interface CreateCronScheduleResponse {
|
|
827
|
+
id: string;
|
|
828
|
+
expression: string;
|
|
829
|
+
description: string;
|
|
830
|
+
next_run_at: string;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export interface TriggerCronScheduleResponse {
|
|
834
|
+
triggered: boolean;
|
|
835
|
+
status: string;
|
|
836
|
+
duration_ms: number;
|
|
837
|
+
}
|
|
838
|
+
|
|
780
839
|
export interface StartLogSessionResponse {
|
|
781
840
|
success: boolean;
|
|
782
841
|
session: LogSessionInfo;
|
|
@@ -807,3 +866,100 @@ export async function startLogSession(
|
|
|
807
866
|
|
|
808
867
|
return response.json() as Promise<StartLogSessionResponse>;
|
|
809
868
|
}
|
|
869
|
+
|
|
870
|
+
// ============================================================================
|
|
871
|
+
// Cron Schedule Operations
|
|
872
|
+
// ============================================================================
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Create a cron schedule for a managed project.
|
|
876
|
+
*/
|
|
877
|
+
export async function createCronSchedule(
|
|
878
|
+
projectId: string,
|
|
879
|
+
expression: string,
|
|
880
|
+
): Promise<CreateCronScheduleResponse> {
|
|
881
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
882
|
+
|
|
883
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: { "Content-Type": "application/json" },
|
|
886
|
+
body: JSON.stringify({ expression }),
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
891
|
+
message?: string;
|
|
892
|
+
};
|
|
893
|
+
throw new Error(err.message || `Failed to create cron schedule: ${response.status}`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return response.json() as Promise<CreateCronScheduleResponse>;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* List all cron schedules for a managed project.
|
|
901
|
+
*/
|
|
902
|
+
export async function listCronSchedules(projectId: string): Promise<CronScheduleInfo[]> {
|
|
903
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
904
|
+
|
|
905
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`);
|
|
906
|
+
|
|
907
|
+
if (!response.ok) {
|
|
908
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
909
|
+
message?: string;
|
|
910
|
+
};
|
|
911
|
+
throw new Error(err.message || `Failed to list cron schedules: ${response.status}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const data = (await response.json()) as { schedules: CronScheduleInfo[] };
|
|
915
|
+
return data.schedules;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Delete a cron schedule from a managed project.
|
|
920
|
+
*/
|
|
921
|
+
export async function deleteCronSchedule(projectId: string, cronId: string): Promise<void> {
|
|
922
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
923
|
+
|
|
924
|
+
const response = await authFetch(
|
|
925
|
+
`${getControlApiUrl()}/v1/projects/${projectId}/crons/${cronId}`,
|
|
926
|
+
{ method: "DELETE" },
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
if (response.status === 404) {
|
|
930
|
+
// Already deleted, treat as success
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (!response.ok) {
|
|
935
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
936
|
+
message?: string;
|
|
937
|
+
};
|
|
938
|
+
throw new Error(err.message || `Failed to delete cron schedule: ${response.status}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Manually trigger a cron schedule on a managed project.
|
|
944
|
+
*/
|
|
945
|
+
export async function triggerCronSchedule(
|
|
946
|
+
projectId: string,
|
|
947
|
+
expression: string,
|
|
948
|
+
): Promise<TriggerCronScheduleResponse> {
|
|
949
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
950
|
+
|
|
951
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons/trigger`, {
|
|
952
|
+
method: "POST",
|
|
953
|
+
headers: { "Content-Type": "application/json" },
|
|
954
|
+
body: JSON.stringify({ expression }),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
if (!response.ok) {
|
|
958
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
959
|
+
message?: string;
|
|
960
|
+
};
|
|
961
|
+
throw new Error(err.message || `Failed to trigger cron schedule: ${response.status}`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return response.json() as Promise<TriggerCronScheduleResponse>;
|
|
965
|
+
}
|
package/src/lib/mcp-config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CONFIG_DIR } from "./config.ts";
|
|
|
9
9
|
* MCP server configuration structure
|
|
10
10
|
*/
|
|
11
11
|
export interface McpServerConfig {
|
|
12
|
+
type: "stdio";
|
|
12
13
|
command: string;
|
|
13
14
|
args: string[];
|
|
14
15
|
env?: Record<string, string>;
|
|
@@ -48,19 +49,40 @@ export const APP_MCP_CONFIGS: Record<string, AppMcpConfig> = {
|
|
|
48
49
|
const JACK_MCP_CONFIG_DIR = join(CONFIG_DIR, "mcp");
|
|
49
50
|
const JACK_MCP_CONFIG_PATH = join(JACK_MCP_CONFIG_DIR, "config.json");
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Find the jack binary path
|
|
54
|
+
* Checks common install locations
|
|
55
|
+
*/
|
|
56
|
+
function findJackBinary(): string {
|
|
57
|
+
const bunBin = join(homedir(), ".bun", "bin", "jack");
|
|
58
|
+
const npmBin = join(homedir(), ".npm-global", "bin", "jack");
|
|
59
|
+
const homebrewBin = "/opt/homebrew/bin/jack";
|
|
60
|
+
const usrLocalBin = "/usr/local/bin/jack";
|
|
61
|
+
|
|
62
|
+
// Check in order of priority
|
|
63
|
+
for (const path of [bunBin, npmBin, homebrewBin, usrLocalBin]) {
|
|
64
|
+
if (existsSync(path)) {
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fallback to just "jack" and hope PATH works
|
|
70
|
+
return "jack";
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
/**
|
|
52
74
|
* Returns the jack MCP server configuration
|
|
53
|
-
*
|
|
75
|
+
* Uses full path to jack binary for reliability
|
|
54
76
|
*/
|
|
55
77
|
export function getJackMcpConfig(): McpServerConfig {
|
|
56
|
-
// Build PATH with common locations
|
|
57
|
-
// ~/.bun/bin is where `bun link` installs global commands
|
|
78
|
+
// Build PATH with common locations (still needed for child processes)
|
|
58
79
|
const bunBin = join(homedir(), ".bun", "bin");
|
|
59
80
|
const npmBin = join(homedir(), ".npm-global", "bin");
|
|
60
81
|
const defaultPaths = [bunBin, npmBin, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
61
82
|
|
|
62
83
|
return {
|
|
63
|
-
|
|
84
|
+
type: "stdio",
|
|
85
|
+
command: findJackBinary(),
|
|
64
86
|
args: ["mcp", "serve"],
|
|
65
87
|
env: {
|
|
66
88
|
PATH: defaultPaths.join(":"),
|
package/src/lib/output.ts
CHANGED
|
@@ -179,7 +179,8 @@ export function box(title: string, lines: string[]): void {
|
|
|
179
179
|
const gradient = "░".repeat(innerWidth);
|
|
180
180
|
|
|
181
181
|
// Truncate text if too long for box
|
|
182
|
-
const truncate = (text: string) =>
|
|
182
|
+
const truncate = (text: string) =>
|
|
183
|
+
text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
|
|
183
184
|
|
|
184
185
|
// Pad plain text first, then apply colors (ANSI codes break padEnd calculation)
|
|
185
186
|
const pad = (text: string) => ` ${truncate(text).padEnd(maxLen)} `;
|
|
@@ -226,7 +227,8 @@ export function celebrate(title: string, lines: string[]): void {
|
|
|
226
227
|
const space = " ".repeat(innerWidth);
|
|
227
228
|
|
|
228
229
|
// Truncate text if too long for box
|
|
229
|
-
const truncate = (text: string) =>
|
|
230
|
+
const truncate = (text: string) =>
|
|
231
|
+
text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
|
|
230
232
|
|
|
231
233
|
// Center text based on visual length, then apply colors
|
|
232
234
|
const center = (text: string, applyBold = false) => {
|