@getjack/jack 0.1.30 → 0.1.32

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.
@@ -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,
@@ -95,6 +99,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
95
99
 
96
100
  interface ServiceOptions {
97
101
  project?: string;
102
+ json?: boolean;
98
103
  }
99
104
 
100
105
  export default async function services(
@@ -111,9 +116,11 @@ export default async function services(
111
116
  return await dbCommand(args, options);
112
117
  case "storage":
113
118
  return await storageCommand(args, options);
119
+ case "cron":
120
+ return await cronCommand(args, options);
114
121
  default:
115
122
  error(`Unknown service: ${subcommand}`);
116
- info("Available: db, storage");
123
+ info("Available: db, storage, cron");
117
124
  process.exit(1);
118
125
  }
119
126
  }
@@ -125,6 +132,7 @@ function showHelp(): void {
125
132
  console.error("Commands:");
126
133
  console.error(" db Manage database");
127
134
  console.error(" storage Manage storage (R2 buckets)");
135
+ console.error(" cron Manage scheduled tasks");
128
136
  console.error("");
129
137
  console.error("Run 'jack services <command>' for more information.");
130
138
  console.error("");
@@ -202,17 +210,23 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
202
210
  * Show database information
203
211
  */
204
212
  async function dbInfo(options: ServiceOptions): Promise<void> {
213
+ const jsonOutput = options.json ?? false;
205
214
  const projectName = await resolveProjectName(options);
206
215
  const projectDir = process.cwd();
207
216
  const link = await readProjectLink(projectDir);
208
217
 
209
218
  // For managed projects, use control plane API (no wrangler dependency)
210
219
  if (link?.deploy_mode === "managed") {
211
- outputSpinner.start("Fetching database info...");
220
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
212
221
  try {
213
222
  const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
214
223
  const dbInfo = await getManagedDatabaseInfo(link.project_id);
215
- outputSpinner.stop();
224
+ if (!jsonOutput) outputSpinner.stop();
225
+
226
+ if (jsonOutput) {
227
+ console.log(JSON.stringify({ name: dbInfo.name, id: dbInfo.id, sizeBytes: dbInfo.sizeBytes, numTables: dbInfo.numTables, source: "managed" }));
228
+ return;
229
+ }
216
230
 
217
231
  console.error("");
218
232
  success(`Database: ${dbInfo.name}`);
@@ -224,7 +238,14 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
224
238
  console.error("");
225
239
  return;
226
240
  } catch (err) {
227
- outputSpinner.stop();
241
+ if (!jsonOutput) outputSpinner.stop();
242
+ if (jsonOutput) {
243
+ const msg = err instanceof Error && err.message.includes("No database found")
244
+ ? "No database configured. Run 'jack services db create' to create one."
245
+ : `Failed to fetch database info: ${err instanceof Error ? err.message : String(err)}`;
246
+ console.log(JSON.stringify({ success: false, error: msg }));
247
+ process.exit(1);
248
+ }
228
249
  console.error("");
229
250
  if (err instanceof Error && err.message.includes("No database found")) {
230
251
  error("No database found for this project");
@@ -241,6 +262,10 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
241
262
  const dbInfo = await resolveDatabaseInfo(projectName);
242
263
 
243
264
  if (!dbInfo) {
265
+ if (jsonOutput) {
266
+ console.log(JSON.stringify({ success: false, error: "No database found for this project" }));
267
+ return;
268
+ }
244
269
  console.error("");
245
270
  error("No database found for this project");
246
271
  info("Create one with: jack services db create");
@@ -249,11 +274,15 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
249
274
  }
250
275
 
251
276
  // Fetch detailed database info via wrangler
252
- outputSpinner.start("Fetching database info...");
277
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
253
278
  const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
254
- outputSpinner.stop();
279
+ if (!jsonOutput) outputSpinner.stop();
255
280
 
256
281
  if (!wranglerDbInfo) {
282
+ if (jsonOutput) {
283
+ console.log(JSON.stringify({ success: false, error: "Database not found" }));
284
+ process.exit(1);
285
+ }
257
286
  console.error("");
258
287
  error("Database not found");
259
288
  info("It may have been deleted");
@@ -261,6 +290,11 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
261
290
  process.exit(1);
262
291
  }
263
292
 
293
+ if (jsonOutput) {
294
+ console.log(JSON.stringify({ name: wranglerDbInfo.name, id: dbInfo.id || wranglerDbInfo.id, sizeBytes: wranglerDbInfo.sizeBytes, numTables: wranglerDbInfo.numTables, source: "byo" }));
295
+ return;
296
+ }
297
+
264
298
  // Display info
265
299
  console.error("");
266
300
  success(`Database: ${wranglerDbInfo.name}`);
@@ -617,10 +651,16 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
617
651
  * List all databases in the project
618
652
  */
619
653
  async function dbList(options: ServiceOptions): Promise<void> {
620
- outputSpinner.start("Fetching databases...");
654
+ const jsonOutput = options.json ?? false;
655
+ if (!jsonOutput) outputSpinner.start("Fetching databases...");
621
656
  try {
622
657
  const databases = await listDatabases(process.cwd());
623
- outputSpinner.stop();
658
+ if (!jsonOutput) outputSpinner.stop();
659
+
660
+ if (jsonOutput) {
661
+ console.log(JSON.stringify(databases));
662
+ return;
663
+ }
624
664
 
625
665
  if (databases.length === 0) {
626
666
  console.error("");
@@ -647,7 +687,11 @@ async function dbList(options: ServiceOptions): Promise<void> {
647
687
  console.error("");
648
688
  }
649
689
  } catch (err) {
650
- outputSpinner.stop();
690
+ if (!jsonOutput) outputSpinner.stop();
691
+ if (jsonOutput) {
692
+ console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
693
+ process.exit(1);
694
+ }
651
695
  console.error("");
652
696
  error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
653
697
  process.exit(1);
@@ -722,7 +766,8 @@ function parseExecuteArgs(args: string[]): ExecuteArgs {
722
766
  /**
723
767
  * Execute SQL against the database
724
768
  */
725
- async function dbExecute(args: string[], _options: ServiceOptions): Promise<void> {
769
+ async function dbExecute(args: string[], options: ServiceOptions): Promise<void> {
770
+ const jsonOutput = options.json ?? false;
726
771
  const execArgs = parseExecuteArgs(args);
727
772
 
728
773
  // Validate input
@@ -759,7 +804,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
759
804
  const projectDir = process.cwd();
760
805
 
761
806
  try {
762
- outputSpinner.start("Executing SQL...");
807
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
763
808
 
764
809
  let result;
765
810
  if (execArgs.filePath) {
@@ -816,7 +861,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
816
861
  }
817
862
 
818
863
  // NOW execute with confirmation
819
- outputSpinner.start("Executing SQL...");
864
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
820
865
  if (execArgs.filePath) {
821
866
  result = await executeSqlFile({
822
867
  projectDir,
@@ -850,12 +895,37 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
850
895
  error_type: "execution_failed",
851
896
  });
852
897
 
898
+ if (jsonOutput) {
899
+ console.log(JSON.stringify({ success: false, error: result.error || "SQL execution failed" }));
900
+ process.exit(1);
901
+ }
902
+
853
903
  console.error("");
854
904
  error(result.error || "SQL execution failed");
855
905
  console.error("");
856
906
  process.exit(1);
857
907
  }
858
908
 
909
+ // Track telemetry
910
+ track(Events.SQL_EXECUTED, {
911
+ success: true,
912
+ risk_level: result.risk,
913
+ statement_count: result.statements.length,
914
+ from_file: !!execArgs.filePath,
915
+ });
916
+
917
+ // JSON output mode — structured result for agents
918
+ if (jsonOutput) {
919
+ console.log(JSON.stringify({
920
+ success: true,
921
+ results: result.results ?? [],
922
+ meta: result.meta,
923
+ risk: result.risk,
924
+ warning: result.warning,
925
+ }));
926
+ return;
927
+ }
928
+
859
929
  // Show results
860
930
  console.error("");
861
931
  success(`SQL executed (${getRiskDescription(result.risk)})`);
@@ -879,14 +949,6 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
879
949
  console.log(JSON.stringify(result.results, null, 2));
880
950
  }
881
951
  console.error("");
882
-
883
- // Track telemetry
884
- track(Events.SQL_EXECUTED, {
885
- success: true,
886
- risk_level: result.risk,
887
- statement_count: result.statements.length,
888
- from_file: !!execArgs.filePath,
889
- });
890
952
  } catch (err) {
891
953
  outputSpinner.stop();
892
954
 
@@ -897,6 +959,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
897
959
  risk_level: err.risk,
898
960
  });
899
961
 
962
+ if (jsonOutput) {
963
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Add --write flag" }));
964
+ process.exit(1);
965
+ }
966
+
900
967
  console.error("");
901
968
  error(err.message);
902
969
  info("Add the --write flag to allow data modification:");
@@ -912,6 +979,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
912
979
  risk_level: "destructive",
913
980
  });
914
981
 
982
+ if (jsonOutput) {
983
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Destructive operations require confirmation via CLI" }));
984
+ process.exit(1);
985
+ }
986
+
915
987
  console.error("");
916
988
  error(err.message);
917
989
  info("Destructive operations require confirmation via CLI.");
@@ -924,6 +996,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
924
996
  error_type: "unknown",
925
997
  });
926
998
 
999
+ if (jsonOutput) {
1000
+ console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
1001
+ process.exit(1);
1002
+ }
1003
+
927
1004
  console.error("");
928
1005
  error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
929
1006
  console.error("");
@@ -1181,3 +1258,295 @@ async function storageDelete(args: string[], options: ServiceOptions): Promise<v
1181
1258
  process.exit(1);
1182
1259
  }
1183
1260
  }
1261
+
1262
+ // ============================================================================
1263
+ // Cron Commands
1264
+ // ============================================================================
1265
+
1266
+ function showCronHelp(): void {
1267
+ console.error("");
1268
+ info("jack services cron - Manage scheduled tasks");
1269
+ console.error("");
1270
+ console.error("Actions:");
1271
+ console.error(' create <expression> Add a cron schedule (e.g., "0 * * * *")');
1272
+ console.error(" list List all cron schedules");
1273
+ console.error(" delete <expression> Remove a cron schedule");
1274
+ console.error(" test <expression> Validate and preview a cron expression");
1275
+ console.error("");
1276
+ console.error("Examples:");
1277
+ console.error(' jack services cron create "0 * * * *" Schedule hourly runs');
1278
+ console.error(" jack services cron list Show all schedules");
1279
+ console.error(' jack services cron delete "0 * * * *" Remove a schedule');
1280
+ console.error(' jack services cron test "*/15 * * * *" Preview next run times');
1281
+ console.error("");
1282
+ console.error("Common patterns:");
1283
+ for (const pattern of COMMON_CRON_PATTERNS) {
1284
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1285
+ }
1286
+ console.error("");
1287
+ console.error("Note: Cron schedules require Jack Cloud (managed) projects.");
1288
+ console.error("All times are in UTC.");
1289
+ console.error("");
1290
+ }
1291
+
1292
+ async function cronCommand(args: string[], options: ServiceOptions): Promise<void> {
1293
+ // Check if any argument is --help or -h
1294
+ if (args.includes("--help") || args.includes("-h")) {
1295
+ return showCronHelp();
1296
+ }
1297
+
1298
+ const action = args[0] || "list"; // Default to list
1299
+
1300
+ switch (action) {
1301
+ case "help":
1302
+ return showCronHelp();
1303
+ case "create":
1304
+ return await cronCreate(args.slice(1), options);
1305
+ case "list":
1306
+ return await cronList(options);
1307
+ case "delete":
1308
+ return await cronDelete(args.slice(1), options);
1309
+ case "test":
1310
+ return await cronTest(args.slice(1), options);
1311
+ default:
1312
+ error(`Unknown action: ${action}`);
1313
+ info("Available: create, list, delete, test");
1314
+ process.exit(1);
1315
+ }
1316
+ }
1317
+
1318
+ /**
1319
+ * Create a new cron schedule
1320
+ */
1321
+ async function cronCreate(args: string[], options: ServiceOptions): Promise<void> {
1322
+ const expression = args[0];
1323
+
1324
+ if (!expression) {
1325
+ console.error("");
1326
+ error("Cron expression required");
1327
+ info('Usage: jack services cron create "<expression>"');
1328
+ console.error("");
1329
+ console.error("Common patterns:");
1330
+ for (const pattern of COMMON_CRON_PATTERNS) {
1331
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1332
+ }
1333
+ console.error("");
1334
+ process.exit(1);
1335
+ }
1336
+
1337
+ outputSpinner.start("Creating cron schedule...");
1338
+ try {
1339
+ const result = await createCronSchedule(process.cwd(), expression, {
1340
+ interactive: true,
1341
+ });
1342
+ outputSpinner.stop();
1343
+
1344
+ // Track telemetry
1345
+ track(Events.SERVICE_CREATED, {
1346
+ service_type: "cron",
1347
+ created: result.created,
1348
+ });
1349
+
1350
+ console.error("");
1351
+ success("Cron schedule created");
1352
+ console.error("");
1353
+ item(`Expression: ${result.expression}`);
1354
+ item(`Schedule: ${result.description}`);
1355
+ item(`Next run: ${result.nextRunAt}`);
1356
+ console.error("");
1357
+ info("Make sure your Worker handles POST /__scheduled requests.");
1358
+ console.error("");
1359
+ } catch (err) {
1360
+ outputSpinner.stop();
1361
+ console.error("");
1362
+ error(`Failed to create cron schedule: ${err instanceof Error ? err.message : String(err)}`);
1363
+ process.exit(1);
1364
+ }
1365
+ }
1366
+
1367
+ /**
1368
+ * List all cron schedules
1369
+ */
1370
+ async function cronList(options: ServiceOptions): Promise<void> {
1371
+ outputSpinner.start("Fetching cron schedules...");
1372
+ try {
1373
+ const schedules = await listCronSchedules(process.cwd());
1374
+ outputSpinner.stop();
1375
+
1376
+ if (schedules.length === 0) {
1377
+ console.error("");
1378
+ info("No cron schedules found for this project.");
1379
+ console.error("");
1380
+ info('Create one with: jack services cron create "0 * * * *"');
1381
+ console.error("");
1382
+ return;
1383
+ }
1384
+
1385
+ console.error("");
1386
+ success(`Found ${schedules.length} cron schedule${schedules.length === 1 ? "" : "s"}:`);
1387
+ console.error("");
1388
+
1389
+ for (const schedule of schedules) {
1390
+ const statusIcon = schedule.enabled ? "+" : "-";
1391
+ item(`${statusIcon} ${schedule.expression}`);
1392
+ item(` ${schedule.description}`);
1393
+ item(` Next: ${schedule.nextRunAt}`);
1394
+ if (schedule.lastRunAt) {
1395
+ const statusLabel =
1396
+ schedule.lastRunStatus === "success"
1397
+ ? "success"
1398
+ : `${schedule.lastRunStatus} (${schedule.consecutiveFailures} failures)`;
1399
+ item(` Last: ${schedule.lastRunAt} - ${statusLabel}`);
1400
+ if (schedule.lastRunDurationMs !== null) {
1401
+ item(` Duration: ${schedule.lastRunDurationMs}ms`);
1402
+ }
1403
+ }
1404
+ console.error("");
1405
+ }
1406
+ } catch (err) {
1407
+ outputSpinner.stop();
1408
+ console.error("");
1409
+ error(`Failed to list cron schedules: ${err instanceof Error ? err.message : String(err)}`);
1410
+ process.exit(1);
1411
+ }
1412
+ }
1413
+
1414
+ /**
1415
+ * Delete a cron schedule
1416
+ */
1417
+ async function cronDelete(args: string[], options: ServiceOptions): Promise<void> {
1418
+ const expression = args[0];
1419
+
1420
+ if (!expression) {
1421
+ console.error("");
1422
+ error("Cron expression required");
1423
+ info('Usage: jack services cron delete "<expression>"');
1424
+ console.error("");
1425
+ process.exit(1);
1426
+ }
1427
+
1428
+ // Show what will be deleted
1429
+ console.error("");
1430
+ info(`Cron schedule: ${expression}`);
1431
+ console.error("");
1432
+ warn("This will stop all future scheduled runs");
1433
+ console.error("");
1434
+
1435
+ // Confirm deletion
1436
+ const { promptSelect } = await import("../lib/hooks.ts");
1437
+ const choice = await promptSelect(
1438
+ ["Yes, delete", "No, cancel"],
1439
+ `Delete cron schedule '${expression}'?`,
1440
+ );
1441
+
1442
+ if (choice !== 0) {
1443
+ info("Cancelled");
1444
+ return;
1445
+ }
1446
+
1447
+ outputSpinner.start("Deleting cron schedule...");
1448
+ try {
1449
+ const result = await deleteCronSchedule(process.cwd(), expression, {
1450
+ interactive: true,
1451
+ });
1452
+ outputSpinner.stop();
1453
+
1454
+ // Track telemetry
1455
+ track(Events.SERVICE_DELETED, {
1456
+ service_type: "cron",
1457
+ deleted: result.deleted,
1458
+ });
1459
+
1460
+ console.error("");
1461
+ success("Cron schedule deleted");
1462
+ console.error("");
1463
+ } catch (err) {
1464
+ outputSpinner.stop();
1465
+ console.error("");
1466
+ error(`Failed to delete cron schedule: ${err instanceof Error ? err.message : String(err)}`);
1467
+ process.exit(1);
1468
+ }
1469
+ }
1470
+
1471
+ /**
1472
+ * Test/validate a cron expression
1473
+ */
1474
+ async function cronTest(args: string[], options: ServiceOptions): Promise<void> {
1475
+ const expression = args[0];
1476
+
1477
+ if (!expression) {
1478
+ console.error("");
1479
+ error("Cron expression required");
1480
+ info('Usage: jack services cron test "<expression>"');
1481
+ console.error("");
1482
+ console.error("Common patterns:");
1483
+ for (const pattern of COMMON_CRON_PATTERNS) {
1484
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1485
+ }
1486
+ console.error("");
1487
+ process.exit(1);
1488
+ }
1489
+
1490
+ // Test the expression (no production trigger by default)
1491
+ const result = await testCronExpression(process.cwd(), expression, {
1492
+ interactive: true,
1493
+ });
1494
+
1495
+ if (!result.valid) {
1496
+ console.error("");
1497
+ error(`Invalid cron expression: ${result.error}`);
1498
+ console.error("");
1499
+ console.error("Common patterns:");
1500
+ for (const pattern of COMMON_CRON_PATTERNS) {
1501
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1502
+ }
1503
+ console.error("");
1504
+ process.exit(1);
1505
+ }
1506
+
1507
+ console.error("");
1508
+ success("Valid cron expression");
1509
+ console.error("");
1510
+ item(`Expression: ${result.expression}`);
1511
+ item(`Schedule: ${result.description}`);
1512
+ console.error("");
1513
+ info("Next 5 runs (UTC):");
1514
+ for (const time of result.nextTimes!) {
1515
+ item(` ${time.toISOString()}`);
1516
+ }
1517
+ console.error("");
1518
+
1519
+ // Check if this is a managed project for optional trigger
1520
+ const link = await readProjectLink(process.cwd());
1521
+ if (link?.deploy_mode === "managed") {
1522
+ const { promptSelect } = await import("../lib/hooks.ts");
1523
+ const choice = await promptSelect(
1524
+ ["Yes, trigger now", "No, skip"],
1525
+ "Trigger scheduled handler on production?",
1526
+ );
1527
+
1528
+ if (choice === 0) {
1529
+ outputSpinner.start("Triggering scheduled handler...");
1530
+ try {
1531
+ const triggerResult = await testCronExpression(process.cwd(), expression, {
1532
+ triggerProduction: true,
1533
+ interactive: true,
1534
+ });
1535
+ outputSpinner.stop();
1536
+
1537
+ if (triggerResult.triggerResult) {
1538
+ console.error("");
1539
+ success(
1540
+ `Triggered! Status: ${triggerResult.triggerResult.status}, Duration: ${triggerResult.triggerResult.durationMs}ms`,
1541
+ );
1542
+ console.error("");
1543
+ }
1544
+ } catch (err) {
1545
+ outputSpinner.stop();
1546
+ console.error("");
1547
+ error(`Failed to trigger: ${err instanceof Error ? err.message : String(err)}`);
1548
+ console.error("");
1549
+ }
1550
+ }
1551
+ }
1552
+ }
@@ -3,14 +3,15 @@ import { createReporter, output } from "../lib/output.ts";
3
3
  import { deployProject } from "../lib/project-operations.ts";
4
4
 
5
5
  export default async function ship(
6
- options: { managed?: boolean; byo?: boolean; dryRun?: boolean } = {},
6
+ options: { managed?: boolean; byo?: boolean; dryRun?: boolean; json?: boolean } = {},
7
7
  ): Promise<void> {
8
8
  const isCi = process.env.CI === "true" || process.env.CI === "1";
9
+ const jsonOutput = options.json ?? false;
9
10
  try {
10
11
  const result = await deployProject({
11
12
  projectPath: process.cwd(),
12
- reporter: createReporter(),
13
- interactive: !isCi,
13
+ reporter: jsonOutput ? undefined : createReporter(),
14
+ interactive: !isCi && !jsonOutput,
14
15
  includeSecrets: !options.dryRun,
15
16
  includeSync: !options.dryRun,
16
17
  managed: options.managed,
@@ -18,11 +19,35 @@ export default async function ship(
18
19
  dryRun: options.dryRun,
19
20
  });
20
21
 
22
+ if (jsonOutput) {
23
+ console.log(
24
+ JSON.stringify({
25
+ success: true,
26
+ projectName: result.projectName,
27
+ url: result.workerUrl,
28
+ deployMode: result.deployMode,
29
+ }),
30
+ );
31
+ return;
32
+ }
33
+
21
34
  if (!result.workerUrl && result.deployOutput) {
22
35
  console.error(result.deployOutput);
23
36
  }
24
37
  } catch (error) {
25
38
  const details = getErrorDetails(error);
39
+
40
+ if (jsonOutput) {
41
+ console.log(
42
+ JSON.stringify({
43
+ success: false,
44
+ error: details.message,
45
+ suggestion: details.suggestion,
46
+ }),
47
+ );
48
+ process.exit(details.meta?.exitCode ?? 1);
49
+ }
50
+
26
51
  if (!details.meta?.reported) {
27
52
  output.error(details.message);
28
53
  }
@@ -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
+ }