@getjack/jack 0.1.31 → 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 +1 -1
- package/src/commands/services.ts +89 -19
- package/src/commands/ship.ts +28 -3
- 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/store.ts +3 -0
- 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/package.json
CHANGED
package/src/commands/services.ts
CHANGED
|
@@ -99,6 +99,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
|
|
|
99
99
|
|
|
100
100
|
interface ServiceOptions {
|
|
101
101
|
project?: string;
|
|
102
|
+
json?: boolean;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export default async function services(
|
|
@@ -209,17 +210,23 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
|
|
|
209
210
|
* Show database information
|
|
210
211
|
*/
|
|
211
212
|
async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
213
|
+
const jsonOutput = options.json ?? false;
|
|
212
214
|
const projectName = await resolveProjectName(options);
|
|
213
215
|
const projectDir = process.cwd();
|
|
214
216
|
const link = await readProjectLink(projectDir);
|
|
215
217
|
|
|
216
218
|
// For managed projects, use control plane API (no wrangler dependency)
|
|
217
219
|
if (link?.deploy_mode === "managed") {
|
|
218
|
-
outputSpinner.start("Fetching database info...");
|
|
220
|
+
if (!jsonOutput) outputSpinner.start("Fetching database info...");
|
|
219
221
|
try {
|
|
220
222
|
const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
|
|
221
223
|
const dbInfo = await getManagedDatabaseInfo(link.project_id);
|
|
222
|
-
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
|
+
}
|
|
223
230
|
|
|
224
231
|
console.error("");
|
|
225
232
|
success(`Database: ${dbInfo.name}`);
|
|
@@ -231,7 +238,14 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
231
238
|
console.error("");
|
|
232
239
|
return;
|
|
233
240
|
} catch (err) {
|
|
234
|
-
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
|
+
}
|
|
235
249
|
console.error("");
|
|
236
250
|
if (err instanceof Error && err.message.includes("No database found")) {
|
|
237
251
|
error("No database found for this project");
|
|
@@ -248,6 +262,10 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
248
262
|
const dbInfo = await resolveDatabaseInfo(projectName);
|
|
249
263
|
|
|
250
264
|
if (!dbInfo) {
|
|
265
|
+
if (jsonOutput) {
|
|
266
|
+
console.log(JSON.stringify({ success: false, error: "No database found for this project" }));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
251
269
|
console.error("");
|
|
252
270
|
error("No database found for this project");
|
|
253
271
|
info("Create one with: jack services db create");
|
|
@@ -256,11 +274,15 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
// Fetch detailed database info via wrangler
|
|
259
|
-
outputSpinner.start("Fetching database info...");
|
|
277
|
+
if (!jsonOutput) outputSpinner.start("Fetching database info...");
|
|
260
278
|
const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
|
|
261
|
-
outputSpinner.stop();
|
|
279
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
262
280
|
|
|
263
281
|
if (!wranglerDbInfo) {
|
|
282
|
+
if (jsonOutput) {
|
|
283
|
+
console.log(JSON.stringify({ success: false, error: "Database not found" }));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
264
286
|
console.error("");
|
|
265
287
|
error("Database not found");
|
|
266
288
|
info("It may have been deleted");
|
|
@@ -268,6 +290,11 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
|
|
|
268
290
|
process.exit(1);
|
|
269
291
|
}
|
|
270
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
|
+
|
|
271
298
|
// Display info
|
|
272
299
|
console.error("");
|
|
273
300
|
success(`Database: ${wranglerDbInfo.name}`);
|
|
@@ -624,10 +651,16 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
|
|
|
624
651
|
* List all databases in the project
|
|
625
652
|
*/
|
|
626
653
|
async function dbList(options: ServiceOptions): Promise<void> {
|
|
627
|
-
|
|
654
|
+
const jsonOutput = options.json ?? false;
|
|
655
|
+
if (!jsonOutput) outputSpinner.start("Fetching databases...");
|
|
628
656
|
try {
|
|
629
657
|
const databases = await listDatabases(process.cwd());
|
|
630
|
-
outputSpinner.stop();
|
|
658
|
+
if (!jsonOutput) outputSpinner.stop();
|
|
659
|
+
|
|
660
|
+
if (jsonOutput) {
|
|
661
|
+
console.log(JSON.stringify(databases));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
631
664
|
|
|
632
665
|
if (databases.length === 0) {
|
|
633
666
|
console.error("");
|
|
@@ -654,7 +687,11 @@ async function dbList(options: ServiceOptions): Promise<void> {
|
|
|
654
687
|
console.error("");
|
|
655
688
|
}
|
|
656
689
|
} catch (err) {
|
|
657
|
-
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
|
+
}
|
|
658
695
|
console.error("");
|
|
659
696
|
error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
|
|
660
697
|
process.exit(1);
|
|
@@ -729,7 +766,8 @@ function parseExecuteArgs(args: string[]): ExecuteArgs {
|
|
|
729
766
|
/**
|
|
730
767
|
* Execute SQL against the database
|
|
731
768
|
*/
|
|
732
|
-
async function dbExecute(args: string[],
|
|
769
|
+
async function dbExecute(args: string[], options: ServiceOptions): Promise<void> {
|
|
770
|
+
const jsonOutput = options.json ?? false;
|
|
733
771
|
const execArgs = parseExecuteArgs(args);
|
|
734
772
|
|
|
735
773
|
// Validate input
|
|
@@ -766,7 +804,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
766
804
|
const projectDir = process.cwd();
|
|
767
805
|
|
|
768
806
|
try {
|
|
769
|
-
outputSpinner.start("Executing SQL...");
|
|
807
|
+
if (!jsonOutput) outputSpinner.start("Executing SQL...");
|
|
770
808
|
|
|
771
809
|
let result;
|
|
772
810
|
if (execArgs.filePath) {
|
|
@@ -823,7 +861,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
823
861
|
}
|
|
824
862
|
|
|
825
863
|
// NOW execute with confirmation
|
|
826
|
-
outputSpinner.start("Executing SQL...");
|
|
864
|
+
if (!jsonOutput) outputSpinner.start("Executing SQL...");
|
|
827
865
|
if (execArgs.filePath) {
|
|
828
866
|
result = await executeSqlFile({
|
|
829
867
|
projectDir,
|
|
@@ -857,12 +895,37 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
857
895
|
error_type: "execution_failed",
|
|
858
896
|
});
|
|
859
897
|
|
|
898
|
+
if (jsonOutput) {
|
|
899
|
+
console.log(JSON.stringify({ success: false, error: result.error || "SQL execution failed" }));
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
|
|
860
903
|
console.error("");
|
|
861
904
|
error(result.error || "SQL execution failed");
|
|
862
905
|
console.error("");
|
|
863
906
|
process.exit(1);
|
|
864
907
|
}
|
|
865
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
|
+
|
|
866
929
|
// Show results
|
|
867
930
|
console.error("");
|
|
868
931
|
success(`SQL executed (${getRiskDescription(result.risk)})`);
|
|
@@ -886,14 +949,6 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
886
949
|
console.log(JSON.stringify(result.results, null, 2));
|
|
887
950
|
}
|
|
888
951
|
console.error("");
|
|
889
|
-
|
|
890
|
-
// Track telemetry
|
|
891
|
-
track(Events.SQL_EXECUTED, {
|
|
892
|
-
success: true,
|
|
893
|
-
risk_level: result.risk,
|
|
894
|
-
statement_count: result.statements.length,
|
|
895
|
-
from_file: !!execArgs.filePath,
|
|
896
|
-
});
|
|
897
952
|
} catch (err) {
|
|
898
953
|
outputSpinner.stop();
|
|
899
954
|
|
|
@@ -904,6 +959,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
904
959
|
risk_level: err.risk,
|
|
905
960
|
});
|
|
906
961
|
|
|
962
|
+
if (jsonOutput) {
|
|
963
|
+
console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Add --write flag" }));
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
|
|
907
967
|
console.error("");
|
|
908
968
|
error(err.message);
|
|
909
969
|
info("Add the --write flag to allow data modification:");
|
|
@@ -919,6 +979,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
919
979
|
risk_level: "destructive",
|
|
920
980
|
});
|
|
921
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
|
+
|
|
922
987
|
console.error("");
|
|
923
988
|
error(err.message);
|
|
924
989
|
info("Destructive operations require confirmation via CLI.");
|
|
@@ -931,6 +996,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
|
|
|
931
996
|
error_type: "unknown",
|
|
932
997
|
});
|
|
933
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
|
+
|
|
934
1004
|
console.error("");
|
|
935
1005
|
error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
936
1006
|
console.error("");
|
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
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jack tokens - Manage API tokens for headless authentication
|
|
3
|
+
*
|
|
4
|
+
* Tokens are account-level (not project-scoped).
|
|
5
|
+
* Set JACK_API_TOKEN in your environment for CI/CD and automated pipelines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { error, info, success } from "../lib/output.ts";
|
|
9
|
+
import {
|
|
10
|
+
type TokenInfo,
|
|
11
|
+
createApiToken,
|
|
12
|
+
listApiTokens,
|
|
13
|
+
revokeApiToken,
|
|
14
|
+
} from "../lib/services/token-operations.ts";
|
|
15
|
+
import { Events, track } from "../lib/telemetry.ts";
|
|
16
|
+
|
|
17
|
+
export default async function tokens(
|
|
18
|
+
subcommand?: string,
|
|
19
|
+
args: string[] = [],
|
|
20
|
+
flags: Record<string, unknown> = {},
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (!subcommand) {
|
|
23
|
+
return showHelp();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
27
|
+
return showHelp();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case "create":
|
|
32
|
+
case "new":
|
|
33
|
+
return await createToken(args, flags);
|
|
34
|
+
case "list":
|
|
35
|
+
case "ls":
|
|
36
|
+
return await listTokens();
|
|
37
|
+
case "revoke":
|
|
38
|
+
case "rm":
|
|
39
|
+
case "delete":
|
|
40
|
+
return await revokeToken(args);
|
|
41
|
+
default:
|
|
42
|
+
error(`Unknown subcommand: ${subcommand}`);
|
|
43
|
+
info("Available: create, list, revoke");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showHelp(): void {
|
|
49
|
+
console.error("");
|
|
50
|
+
info("jack tokens - Manage API tokens for headless authentication");
|
|
51
|
+
console.error("");
|
|
52
|
+
console.error("Commands:");
|
|
53
|
+
console.error(" create [name] Create a new API token");
|
|
54
|
+
console.error(" list List active tokens");
|
|
55
|
+
console.error(" revoke <id> Revoke a token");
|
|
56
|
+
console.error("");
|
|
57
|
+
console.error("Usage:");
|
|
58
|
+
console.error(" Set JACK_API_TOKEN in your environment for headless auth.");
|
|
59
|
+
console.error(" Tokens work in CI/CD, Docker, and automated pipelines.");
|
|
60
|
+
console.error("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createToken(args: string[], flags: Record<string, unknown> = {}): Promise<void> {
|
|
64
|
+
// Accept name from --name flag or first positional arg
|
|
65
|
+
let name = "CLI Token";
|
|
66
|
+
if (flags.name && typeof flags.name === "string") {
|
|
67
|
+
name = flags.name;
|
|
68
|
+
} else if (args[0] && !args[0].startsWith("-")) {
|
|
69
|
+
name = args[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await createApiToken(name);
|
|
73
|
+
|
|
74
|
+
track(Events.TOKEN_CREATED);
|
|
75
|
+
|
|
76
|
+
success("Token created");
|
|
77
|
+
console.error("");
|
|
78
|
+
console.error(` ${data.token}`);
|
|
79
|
+
console.error("");
|
|
80
|
+
console.error(" Save this token -- it will not be shown again.");
|
|
81
|
+
console.error("");
|
|
82
|
+
console.error(" Usage:");
|
|
83
|
+
console.error(" export JACK_API_TOKEN=<token>");
|
|
84
|
+
console.error(" jack ship");
|
|
85
|
+
console.error("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function listTokens(): Promise<void> {
|
|
89
|
+
const tokenList = await listApiTokens();
|
|
90
|
+
|
|
91
|
+
if (tokenList.length === 0) {
|
|
92
|
+
info("No active tokens");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.error("");
|
|
97
|
+
for (const t of tokenList) {
|
|
98
|
+
const lastUsed = t.last_used_at ? `last used ${t.last_used_at}` : "never used";
|
|
99
|
+
console.error(` ${t.id} ${t.name} (${lastUsed})`);
|
|
100
|
+
}
|
|
101
|
+
console.error("");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function revokeToken(args: string[]): Promise<void> {
|
|
105
|
+
const tokenId = args[0];
|
|
106
|
+
|
|
107
|
+
if (!tokenId) {
|
|
108
|
+
error("Missing token ID");
|
|
109
|
+
info("Usage: jack tokens revoke <token-id>");
|
|
110
|
+
info("Run 'jack tokens list' to see token IDs");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await revokeApiToken(tokenId);
|
|
115
|
+
|
|
116
|
+
track(Events.TOKEN_REVOKED);
|
|
117
|
+
|
|
118
|
+
success(`Token revoked: ${tokenId}`);
|
|
119
|
+
}
|
package/src/commands/whoami.ts
CHANGED
|
@@ -19,13 +19,17 @@ export default async function whoami(): Promise<void> {
|
|
|
19
19
|
item(`Name: ${creds.user.first_name}${creds.user.last_name ? ` ${creds.user.last_name}` : ""}`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const hours = Math.floor(expiresIn / 3600);
|
|
25
|
-
const minutes = Math.floor((expiresIn % 3600) / 60);
|
|
26
|
-
item(`Token expires: ${hours}h ${minutes}m`);
|
|
22
|
+
if (process.env.JACK_API_TOKEN) {
|
|
23
|
+
item("Auth: API token");
|
|
27
24
|
} else {
|
|
28
|
-
|
|
25
|
+
const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
|
|
26
|
+
if (expiresIn > 0) {
|
|
27
|
+
const hours = Math.floor(expiresIn / 3600);
|
|
28
|
+
const minutes = Math.floor((expiresIn % 3600) / 60);
|
|
29
|
+
item(`Token expires: ${hours}h ${minutes}m`);
|
|
30
|
+
} else {
|
|
31
|
+
item("Token: expired (will refresh on next request)");
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
console.error("");
|
|
31
35
|
}
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ const cli = meow(
|
|
|
35
35
|
login Sign in
|
|
36
36
|
logout Sign out
|
|
37
37
|
whoami Show current user
|
|
38
|
+
tokens Manage API tokens
|
|
38
39
|
update Update jack to latest version
|
|
39
40
|
|
|
40
41
|
Project Management
|
|
@@ -181,6 +182,9 @@ const cli = meow(
|
|
|
181
182
|
sort: {
|
|
182
183
|
type: "string",
|
|
183
184
|
},
|
|
185
|
+
name: {
|
|
186
|
+
type: "string",
|
|
187
|
+
},
|
|
184
188
|
},
|
|
185
189
|
},
|
|
186
190
|
);
|
|
@@ -208,6 +212,7 @@ const [command, ...args] = cli.input;
|
|
|
208
212
|
os: process.platform,
|
|
209
213
|
arch: process.arch,
|
|
210
214
|
node_version: process.version,
|
|
215
|
+
auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
|
|
211
216
|
});
|
|
212
217
|
|
|
213
218
|
// Update lastIdentifyDate
|
|
@@ -220,6 +225,7 @@ const [command, ...args] = cli.input;
|
|
|
220
225
|
os: process.platform,
|
|
221
226
|
arch: process.arch,
|
|
222
227
|
node_version: process.version,
|
|
228
|
+
auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
|
|
223
229
|
});
|
|
224
230
|
}
|
|
225
231
|
})();
|
|
@@ -285,6 +291,7 @@ try {
|
|
|
285
291
|
managed: cli.flags.managed,
|
|
286
292
|
byo: cli.flags.byo,
|
|
287
293
|
dryRun: cli.flags.dryRun,
|
|
294
|
+
json: cli.flags.json,
|
|
288
295
|
});
|
|
289
296
|
break;
|
|
290
297
|
}
|
|
@@ -389,6 +396,7 @@ try {
|
|
|
389
396
|
|
|
390
397
|
await withTelemetry("services", services, { subcommand })(args[0], serviceArgs, {
|
|
391
398
|
project: cli.flags.project,
|
|
399
|
+
json: cli.flags.json,
|
|
392
400
|
});
|
|
393
401
|
break;
|
|
394
402
|
}
|
|
@@ -399,6 +407,15 @@ try {
|
|
|
399
407
|
});
|
|
400
408
|
break;
|
|
401
409
|
}
|
|
410
|
+
case "tokens": {
|
|
411
|
+
const { default: tokens } = await import("./commands/tokens.ts");
|
|
412
|
+
await withTelemetry("tokens", tokens, { subcommand: args[0] })(
|
|
413
|
+
args[0],
|
|
414
|
+
args.slice(1),
|
|
415
|
+
cli.flags,
|
|
416
|
+
);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
402
419
|
case "domain": {
|
|
403
420
|
const { default: domain } = await import("./commands/domain.ts");
|
|
404
421
|
await withTelemetry("domain", domain, { subcommand: args[0] })(args[0], args.slice(1));
|
package/src/lib/auth/client.ts
CHANGED
|
@@ -73,6 +73,16 @@ export async function refreshToken(refreshTokenValue: string): Promise<TokenResp
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export async function getValidAccessToken(): Promise<string | null> {
|
|
76
|
+
// Priority 1: API token from environment
|
|
77
|
+
const apiToken = process.env.JACK_API_TOKEN;
|
|
78
|
+
if (apiToken) {
|
|
79
|
+
if (!apiToken.startsWith("jkt_")) {
|
|
80
|
+
console.error("Warning: JACK_API_TOKEN should start with 'jkt_'");
|
|
81
|
+
}
|
|
82
|
+
return apiToken;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Priority 2: Stored OAuth credentials
|
|
76
86
|
const creds = await getCredentials();
|
|
77
87
|
if (!creds) {
|
|
78
88
|
return null;
|
|
@@ -102,7 +112,7 @@ export async function getValidAccessToken(): Promise<string | null> {
|
|
|
102
112
|
export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
|
103
113
|
const token = await getValidAccessToken();
|
|
104
114
|
if (!token) {
|
|
105
|
-
throw new Error("Not authenticated. Run 'jack login'
|
|
115
|
+
throw new Error("Not authenticated. Run 'jack login' or set JACK_API_TOKEN.");
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
return fetch(url, {
|
package/src/lib/auth/guard.ts
CHANGED
|
@@ -13,7 +13,7 @@ export async function requireAuth(): Promise<string> {
|
|
|
13
13
|
throw new JackError(
|
|
14
14
|
JackErrorCode.AUTH_FAILED,
|
|
15
15
|
"Not logged in",
|
|
16
|
-
"Run 'jack login' to sign in
|
|
16
|
+
"Run 'jack login' to sign in, or set JACK_API_TOKEN for headless use",
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
package/src/lib/auth/store.ts
CHANGED
|
@@ -51,6 +51,9 @@ export type AuthState = "logged-in" | "not-logged-in" | "session-expired";
|
|
|
51
51
|
* - "session-expired": had credentials but refresh failed
|
|
52
52
|
*/
|
|
53
53
|
export async function getAuthState(): Promise<AuthState> {
|
|
54
|
+
// API token always counts as logged in
|
|
55
|
+
if (process.env.JACK_API_TOKEN) return "logged-in";
|
|
56
|
+
|
|
54
57
|
const creds = await getCredentials();
|
|
55
58
|
if (!creds) return "not-logged-in";
|
|
56
59
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token operations service layer for jack cloud
|
|
3
|
+
*
|
|
4
|
+
* Provides shared API token management functions for both CLI and MCP.
|
|
5
|
+
* Returns pure data - no console.log or process.exit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { authFetch } from "../auth/index.ts";
|
|
9
|
+
import { getControlApiUrl } from "../control-plane.ts";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface CreateTokenResult {
|
|
16
|
+
token: string;
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
expires_at: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TokenInfo {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
id_prefix: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
last_used_at: string | null;
|
|
29
|
+
expires_at: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Service Functions
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new API token for headless authentication.
|
|
38
|
+
*/
|
|
39
|
+
export async function createApiToken(
|
|
40
|
+
name: string,
|
|
41
|
+
expiresInDays?: number,
|
|
42
|
+
): Promise<CreateTokenResult> {
|
|
43
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/tokens`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ name, expires_in_days: expiresInDays }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const err = (await response.json().catch(() => ({}))) as { message?: string };
|
|
51
|
+
throw new Error(err.message || `Failed to create token: ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return response.json() as Promise<CreateTokenResult>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* List all active API tokens for the current user.
|
|
59
|
+
*/
|
|
60
|
+
export async function listApiTokens(): Promise<TokenInfo[]> {
|
|
61
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/tokens`);
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const err = (await response.json().catch(() => ({}))) as { message?: string };
|
|
65
|
+
throw new Error(err.message || `Failed to list tokens: ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = (await response.json()) as { tokens: TokenInfo[] };
|
|
69
|
+
return data.tokens;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Revoke an API token by ID.
|
|
74
|
+
*/
|
|
75
|
+
export async function revokeApiToken(tokenId: string): Promise<void> {
|
|
76
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/tokens/${tokenId}`, {
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const err = (await response.json().catch(() => ({}))) as { message?: string };
|
|
82
|
+
throw new Error(err.message || `Failed to revoke token: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -44,6 +44,9 @@ export const Events = {
|
|
|
44
44
|
BYO_DEPLOY_STARTED: "byo_deploy_started",
|
|
45
45
|
BYO_DEPLOY_COMPLETED: "byo_deploy_completed",
|
|
46
46
|
BYO_DEPLOY_FAILED: "byo_deploy_failed",
|
|
47
|
+
// Token management events
|
|
48
|
+
TOKEN_CREATED: "token_created",
|
|
49
|
+
TOKEN_REVOKED: "token_revoked",
|
|
47
50
|
} as const;
|
|
48
51
|
|
|
49
52
|
type EventName = (typeof Events)[keyof typeof Events];
|
|
@@ -127,6 +130,7 @@ export interface UserProperties {
|
|
|
127
130
|
is_tty?: boolean;
|
|
128
131
|
locale?: string;
|
|
129
132
|
config_style?: "byoc" | "jack-cloud";
|
|
133
|
+
auth_method?: "oauth" | "token";
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
// Detect environment properties (for user profile - stable properties)
|
|
@@ -31,6 +31,13 @@ export function registerResources(
|
|
|
31
31
|
description: "Semantic information about jack's capabilities for AI agents",
|
|
32
32
|
mimeType: "application/json",
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
uri: "agents://workflows",
|
|
36
|
+
name: "Workflow Recipes",
|
|
37
|
+
description:
|
|
38
|
+
"Multi-step workflow templates for common tasks like creating APIs with databases, debugging production issues, and setting up cron jobs",
|
|
39
|
+
mimeType: "text/markdown",
|
|
40
|
+
},
|
|
34
41
|
],
|
|
35
42
|
};
|
|
36
43
|
});
|
|
@@ -160,6 +167,172 @@ Check for JACK.md in the project root for project-specific instructions.
|
|
|
160
167
|
};
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
if (uri === "agents://workflows") {
|
|
171
|
+
const workflows = `# Jack Workflow Recipes
|
|
172
|
+
|
|
173
|
+
Multi-step workflows for common tasks. Each workflow can be executed by an agent team or a single agent working sequentially.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 1. Create API with Database
|
|
178
|
+
|
|
179
|
+
Goal: Scaffold a new API, add a database, create tables, and verify.
|
|
180
|
+
|
|
181
|
+
\`\`\`
|
|
182
|
+
Step 1: Create project
|
|
183
|
+
jack new my-api --template api
|
|
184
|
+
|
|
185
|
+
Step 2: Add database
|
|
186
|
+
mcp__jack__create_database (or: jack services db create)
|
|
187
|
+
|
|
188
|
+
Step 3: Create schema
|
|
189
|
+
mcp__jack__execute_sql
|
|
190
|
+
sql: "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"
|
|
191
|
+
allow_write: true
|
|
192
|
+
|
|
193
|
+
Step 4: Edit src/index.ts — add routes that use c.env.DB
|
|
194
|
+
|
|
195
|
+
Step 5: Deploy
|
|
196
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
197
|
+
|
|
198
|
+
Step 6: Verify
|
|
199
|
+
curl https://<slug>.runjack.xyz/api/items
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 2. Debug Production Issue
|
|
205
|
+
|
|
206
|
+
Goal: Identify and fix a bug reported in production.
|
|
207
|
+
|
|
208
|
+
\`\`\`
|
|
209
|
+
Step 1: Check current status
|
|
210
|
+
mcp__jack__get_project_status (or: jack info)
|
|
211
|
+
|
|
212
|
+
Step 2: Collect recent logs
|
|
213
|
+
mcp__jack__tail_logs with max_events: 100, duration_ms: 5000
|
|
214
|
+
|
|
215
|
+
Step 3: Inspect database state if relevant
|
|
216
|
+
mcp__jack__execute_sql
|
|
217
|
+
sql: "SELECT * FROM <table> WHERE <condition>"
|
|
218
|
+
|
|
219
|
+
Step 4: Read and fix the source code
|
|
220
|
+
|
|
221
|
+
Step 5: Deploy fix
|
|
222
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
223
|
+
|
|
224
|
+
Step 6: Verify fix via logs
|
|
225
|
+
mcp__jack__tail_logs with max_events: 20, duration_ms: 3000
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 3. Add Scheduled Task (Cron)
|
|
231
|
+
|
|
232
|
+
Goal: Run code on a schedule (cleanup, sync, reports).
|
|
233
|
+
|
|
234
|
+
\`\`\`
|
|
235
|
+
Step 1: Add scheduled handler to src/index.ts
|
|
236
|
+
export default {
|
|
237
|
+
async fetch(request, env) { ... },
|
|
238
|
+
async scheduled(event, env, ctx) {
|
|
239
|
+
// your cron logic here
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
Or with Hono, add a POST /__scheduled route.
|
|
244
|
+
|
|
245
|
+
Step 2: Deploy the handler
|
|
246
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
247
|
+
|
|
248
|
+
Step 3: Create cron schedule
|
|
249
|
+
mcp__jack__create_cron with expression: "0 * * * *"
|
|
250
|
+
(or: jack services cron create "0 * * * *")
|
|
251
|
+
|
|
252
|
+
Step 4: Verify schedule
|
|
253
|
+
mcp__jack__test_cron with expression: "0 * * * *"
|
|
254
|
+
Shows next 5 scheduled times.
|
|
255
|
+
|
|
256
|
+
Step 5: Monitor via logs
|
|
257
|
+
mcp__jack__tail_logs
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 4. Add File Storage
|
|
263
|
+
|
|
264
|
+
Goal: Enable file uploads and downloads via R2.
|
|
265
|
+
|
|
266
|
+
\`\`\`
|
|
267
|
+
Step 1: Create storage bucket
|
|
268
|
+
mcp__jack__create_storage_bucket (or: jack services storage create)
|
|
269
|
+
|
|
270
|
+
Step 2: Deploy to activate binding
|
|
271
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
272
|
+
|
|
273
|
+
Step 3: Add upload/download routes to src/index.ts
|
|
274
|
+
Upload: c.env.BUCKET.put(key, body)
|
|
275
|
+
Download: c.env.BUCKET.get(key)
|
|
276
|
+
List: c.env.BUCKET.list()
|
|
277
|
+
|
|
278
|
+
Step 4: Deploy routes
|
|
279
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 5. Add Semantic Search (Vectorize + AI)
|
|
285
|
+
|
|
286
|
+
Goal: Enable vector similarity search using embeddings.
|
|
287
|
+
|
|
288
|
+
\`\`\`
|
|
289
|
+
Step 1: Create vector index
|
|
290
|
+
mcp__jack__create_vectorize_index (or: jack services vectorize create)
|
|
291
|
+
Default: 768 dimensions, cosine metric (matches bge-base-en-v1.5)
|
|
292
|
+
|
|
293
|
+
Step 2: Deploy to activate binding
|
|
294
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
295
|
+
|
|
296
|
+
Step 3: Add indexing route
|
|
297
|
+
Generate embedding: c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [input] })
|
|
298
|
+
Insert: c.env.VECTORIZE_INDEX.insert([{ id, values, metadata }])
|
|
299
|
+
|
|
300
|
+
Step 4: Add search route
|
|
301
|
+
Generate query embedding, then:
|
|
302
|
+
c.env.VECTORIZE_INDEX.query(embedding, { topK: 5, returnMetadata: "all" })
|
|
303
|
+
|
|
304
|
+
Step 5: Deploy and test
|
|
305
|
+
mcp__jack__deploy_project (or: jack ship)
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Parallel Task Patterns (Agent Teams)
|
|
311
|
+
|
|
312
|
+
For Opus 4.6 Agent Teams, these tasks can run in parallel:
|
|
313
|
+
|
|
314
|
+
**Independent (run simultaneously):**
|
|
315
|
+
- Creating database + creating storage bucket
|
|
316
|
+
- Reading logs + checking project status
|
|
317
|
+
- Multiple SQL queries on different tables
|
|
318
|
+
|
|
319
|
+
**Sequential (must wait for previous):**
|
|
320
|
+
- Create project → then add services
|
|
321
|
+
- Create database → then create tables → then deploy
|
|
322
|
+
- Edit code → then deploy → then verify
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
contents: [
|
|
327
|
+
{
|
|
328
|
+
uri,
|
|
329
|
+
mimeType: "text/markdown",
|
|
330
|
+
text: workflows,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
163
336
|
throw new Error(`Unknown resource URI: ${uri}`);
|
|
164
337
|
});
|
|
165
338
|
}
|