@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
|
@@ -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
|
|
|
@@ -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/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
|
|
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) => {
|
package/src/lib/picker.ts
CHANGED
|
@@ -86,7 +86,9 @@ export function requireTTY(): void {
|
|
|
86
86
|
* Interactive project picker using @clack/core primitives
|
|
87
87
|
* @param options.cloudOnly - If true, only shows cloud-only projects (for linking)
|
|
88
88
|
*/
|
|
89
|
-
export async function pickProject(
|
|
89
|
+
export async function pickProject(
|
|
90
|
+
options?: PickProjectOptions,
|
|
91
|
+
): Promise<PickerResult | PickerCancelResult> {
|
|
90
92
|
// Fetch all projects
|
|
91
93
|
let allProjects: ProjectListItem[];
|
|
92
94
|
try {
|
|
@@ -47,7 +47,7 @@ import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debu
|
|
|
47
47
|
import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
|
|
48
48
|
import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
|
|
49
49
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
50
|
-
import { type HookOutput, runHook } from "./hooks.ts";
|
|
50
|
+
import { type HookOutput, promptSelect, runHook } from "./hooks.ts";
|
|
51
51
|
import { loadTemplateKeywords, matchTemplateByIntent } from "./intent.ts";
|
|
52
52
|
import {
|
|
53
53
|
type ManagedCreateResult,
|
|
@@ -1605,13 +1605,36 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1605
1605
|
// User is logged into Jack Cloud - create managed project
|
|
1606
1606
|
const orphanedProjectName = await getProjectNameFromDir(projectPath);
|
|
1607
1607
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
// Get username for URL construction
|
|
1608
|
+
// Get username for confirmation prompt and URL construction
|
|
1611
1609
|
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1612
1610
|
const profile = await getCurrentUserProfile();
|
|
1613
1611
|
const ownerUsername = profile?.username ?? undefined;
|
|
1614
1612
|
|
|
1613
|
+
// Confirm before creating new project
|
|
1614
|
+
if (interactive) {
|
|
1615
|
+
reporter.info("This project isn't linked to jack cloud.");
|
|
1616
|
+
const choice = await promptSelect(
|
|
1617
|
+
["Yes", "No"],
|
|
1618
|
+
`Create new project "${orphanedProjectName}" under @${ownerUsername ?? "unknown"}?`,
|
|
1619
|
+
);
|
|
1620
|
+
if (choice !== 0) {
|
|
1621
|
+
reporter.info("Cancelled. To link to an existing project, use: jack link <project-id>");
|
|
1622
|
+
return {
|
|
1623
|
+
projectName: orphanedProjectName,
|
|
1624
|
+
workerUrl: null,
|
|
1625
|
+
deployMode: "managed" as DeployMode,
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
} else {
|
|
1629
|
+
throw new JackError(
|
|
1630
|
+
JackErrorCode.PROJECT_NOT_FOUND,
|
|
1631
|
+
"Project not linked to jack cloud (non-interactive mode)",
|
|
1632
|
+
"Run interactively or use: jack link <project-id>",
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
|
|
1637
|
+
|
|
1615
1638
|
// Create managed project on jack cloud
|
|
1616
1639
|
const remoteResult = await createManagedProjectRemote(orphanedProjectName, reporter, {
|
|
1617
1640
|
usePrebuilt: false,
|
|
@@ -1716,6 +1739,17 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1716
1739
|
);
|
|
1717
1740
|
}
|
|
1718
1741
|
|
|
1742
|
+
// Show current identity for visibility (managed mode only, not dry run)
|
|
1743
|
+
if (!dryRun) {
|
|
1744
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1745
|
+
const profile = await getCurrentUserProfile();
|
|
1746
|
+
if (profile?.username) {
|
|
1747
|
+
reporter.info(`Deploying as @${profile.username}...`);
|
|
1748
|
+
} else if (profile?.email) {
|
|
1749
|
+
reporter.info(`Deploying as ${profile.email}...`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1719
1753
|
// Dry run: build for validation then stop before actual deployment
|
|
1720
1754
|
// (deployToManagedProject handles its own build, so only build here for dry-run)
|
|
1721
1755
|
if (dryRun) {
|