@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.
- package/package.json +4 -5
- package/src/commands/domain.ts +148 -17
- package/src/commands/domains.ts +28 -2
- package/src/commands/services.ts +389 -20
- package/src/commands/ship.ts +28 -3
- package/src/commands/skills.ts +58 -1
- package/src/commands/tokens.ts +119 -0
- package/src/commands/whoami.ts +10 -6
- package/src/index.ts +17 -0
- package/src/lib/auth/client.ts +11 -1
- package/src/lib/auth/guard.ts +1 -1
- package/src/lib/auth/login-flow.ts +34 -0
- package/src/lib/auth/store.ts +3 -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/lib/services/token-operations.ts +84 -0
- package/src/lib/telemetry.ts +4 -0
- package/src/mcp/resources/index.ts +173 -0
- 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,
|
|
@@ -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
|
-
|
|
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[],
|
|
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
|
+
}
|
package/src/commands/ship.ts
CHANGED
|
@@ -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
|
}
|
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
|
+
}
|