@getjack/jack 0.1.2 → 0.1.3
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 +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/README.md +0 -55
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jack feedback - Submit feedback to the jack team
|
|
3
|
+
*
|
|
4
|
+
* Works without login. Auto-collects metadata.
|
|
5
|
+
* Free-form text input, no categories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pkg from "../../package.json";
|
|
9
|
+
import { getCredentials } from "../lib/auth/store.ts";
|
|
10
|
+
import { getControlApiUrl } from "../lib/control-plane.ts";
|
|
11
|
+
import { error, info, output, success } from "../lib/output.ts";
|
|
12
|
+
import { getProject } from "../lib/registry.ts";
|
|
13
|
+
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
14
|
+
import { getTelemetryConfig } from "../lib/telemetry.ts";
|
|
15
|
+
|
|
16
|
+
interface FeedbackMetadata {
|
|
17
|
+
jack_version: string;
|
|
18
|
+
os: string;
|
|
19
|
+
project_name: string | null;
|
|
20
|
+
deploy_mode: "managed" | "byo" | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if user has allowed telemetry (respects privacy preferences)
|
|
25
|
+
*/
|
|
26
|
+
async function shouldAttachPersonalInfo(): Promise<boolean> {
|
|
27
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
28
|
+
if (process.env.CI === "true") return false;
|
|
29
|
+
if (process.env.JACK_TELEMETRY_DISABLED === "1") return false;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const config = await getTelemetryConfig();
|
|
33
|
+
return config.enabled;
|
|
34
|
+
} catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function feedback(): Promise<void> {
|
|
40
|
+
// Check for interactive terminal
|
|
41
|
+
if (!process.stdin.isTTY) {
|
|
42
|
+
error("Feedback requires interactive input.");
|
|
43
|
+
info("Run in a terminal, or open an issue at github.com/getjack-org/jack");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Show prompt
|
|
48
|
+
console.error("");
|
|
49
|
+
info("Share feedback, report a bug, or suggest a feature.");
|
|
50
|
+
info("Press Enter on an empty line to submit. Escape to cancel.");
|
|
51
|
+
console.error("");
|
|
52
|
+
|
|
53
|
+
// Read multi-line input
|
|
54
|
+
const message = await readMultilineInput();
|
|
55
|
+
|
|
56
|
+
if (!message.trim()) {
|
|
57
|
+
info("No feedback provided.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check privacy preferences
|
|
62
|
+
const attachPersonalInfo = await shouldAttachPersonalInfo();
|
|
63
|
+
|
|
64
|
+
// Collect metadata (respects privacy settings)
|
|
65
|
+
const metadata = await collectMetadata(attachPersonalInfo);
|
|
66
|
+
|
|
67
|
+
// Get email if logged in AND telemetry is enabled
|
|
68
|
+
let email: string | null = null;
|
|
69
|
+
if (attachPersonalInfo) {
|
|
70
|
+
const creds = await getCredentials();
|
|
71
|
+
email = creds?.user?.email ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Submit
|
|
75
|
+
output.start("Sending feedback...");
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${getControlApiUrl()}/v1/feedback`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
message: message.trim(),
|
|
83
|
+
email,
|
|
84
|
+
metadata,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
output.stop();
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`HTTP ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
success("Done! Thanks for your feedback.");
|
|
95
|
+
} catch (err) {
|
|
96
|
+
output.stop();
|
|
97
|
+
|
|
98
|
+
// Network errors
|
|
99
|
+
if (err instanceof TypeError && err.message.includes("fetch")) {
|
|
100
|
+
error("Could not reach jack servers.");
|
|
101
|
+
info("Check your internet connection and try again.");
|
|
102
|
+
} else {
|
|
103
|
+
error("Failed to submit feedback.");
|
|
104
|
+
info("Try again later, or open an issue at github.com/getjack-org/jack");
|
|
105
|
+
}
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readMultilineInput(): Promise<string> {
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
const rl = await import("node:readline");
|
|
113
|
+
|
|
114
|
+
// Enable keypress events on stdin
|
|
115
|
+
rl.emitKeypressEvents(process.stdin);
|
|
116
|
+
|
|
117
|
+
const readline = rl.createInterface({
|
|
118
|
+
input: process.stdin,
|
|
119
|
+
output: process.stderr,
|
|
120
|
+
prompt: "> ",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
let emptyLineCount = 0;
|
|
125
|
+
let cancelled = false;
|
|
126
|
+
|
|
127
|
+
// Listen for Escape key
|
|
128
|
+
const onKeypress = (_ch: string, key: { name: string; ctrl?: boolean }) => {
|
|
129
|
+
if (key?.name === "escape") {
|
|
130
|
+
cancelled = true;
|
|
131
|
+
readline.close();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
process.stdin.on("keypress", onKeypress);
|
|
135
|
+
|
|
136
|
+
readline.prompt();
|
|
137
|
+
|
|
138
|
+
readline.on("line", (line) => {
|
|
139
|
+
if (line === "") {
|
|
140
|
+
emptyLineCount++;
|
|
141
|
+
if (emptyLineCount >= 1 && lines.length > 0) {
|
|
142
|
+
// Empty line after content = submit
|
|
143
|
+
readline.close();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
emptyLineCount = 0;
|
|
148
|
+
lines.push(line);
|
|
149
|
+
}
|
|
150
|
+
readline.prompt();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
readline.on("close", () => {
|
|
154
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
155
|
+
resolve(cancelled ? "" : lines.join("\n"));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle Ctrl+C gracefully
|
|
159
|
+
readline.on("SIGINT", () => {
|
|
160
|
+
cancelled = true;
|
|
161
|
+
readline.close();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function collectMetadata(attachPersonalInfo: boolean): Promise<FeedbackMetadata> {
|
|
167
|
+
let projectName: string | null = null;
|
|
168
|
+
let deployMode: "managed" | "byo" | null = null;
|
|
169
|
+
|
|
170
|
+
// Only collect project info if telemetry is enabled
|
|
171
|
+
if (attachPersonalInfo) {
|
|
172
|
+
try {
|
|
173
|
+
projectName = await getProjectNameFromDir(process.cwd());
|
|
174
|
+
if (projectName) {
|
|
175
|
+
const project = await getProject(projectName);
|
|
176
|
+
deployMode = project?.deploy_mode ?? null;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Not in a project directory, that's fine
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
jack_version: pkg.version,
|
|
185
|
+
os: process.platform,
|
|
186
|
+
project_name: projectName,
|
|
187
|
+
deploy_mode: deployMode,
|
|
188
|
+
};
|
|
189
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -5,11 +5,7 @@ import {
|
|
|
5
5
|
updateAgent,
|
|
6
6
|
} from "../lib/agents.ts";
|
|
7
7
|
import { readConfig, writeConfig } from "../lib/config.ts";
|
|
8
|
-
import {
|
|
9
|
-
getIdeDisplayName,
|
|
10
|
-
installMcpConfigsToAllIdes,
|
|
11
|
-
saveMcpConfig,
|
|
12
|
-
} from "../lib/mcp-config.ts";
|
|
8
|
+
import { getAppDisplayName, installMcpConfigsToAllApps, saveMcpConfig } from "../lib/mcp-config.ts";
|
|
13
9
|
import { info, item, spinner, success } from "../lib/output.ts";
|
|
14
10
|
import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts";
|
|
15
11
|
|
|
@@ -79,20 +75,20 @@ export default async function init(options: InitOptions = {}): Promise<void> {
|
|
|
79
75
|
info("No agents detected (you can add them later with: jack agents add)");
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
// Step 4: Install MCP configs to detected
|
|
78
|
+
// Step 4: Install MCP configs to detected apps (unless --skip-mcp)
|
|
83
79
|
if (!options.skipMcp) {
|
|
84
80
|
const mcpSpinner = spinner("Installing MCP server configs...");
|
|
85
81
|
try {
|
|
86
|
-
const
|
|
82
|
+
const installedApps = await installMcpConfigsToAllApps();
|
|
87
83
|
mcpSpinner.stop();
|
|
88
84
|
|
|
89
|
-
if (
|
|
90
|
-
success(`MCP server installed to ${
|
|
91
|
-
for (const
|
|
92
|
-
item(` ${
|
|
85
|
+
if (installedApps.length > 0) {
|
|
86
|
+
success(`MCP server installed to ${installedApps.length} app(s)`);
|
|
87
|
+
for (const appId of installedApps) {
|
|
88
|
+
item(` ${getAppDisplayName(appId)}`);
|
|
93
89
|
}
|
|
94
90
|
} else {
|
|
95
|
-
info("No supported
|
|
91
|
+
info("No supported apps detected for MCP installation");
|
|
96
92
|
}
|
|
97
93
|
} catch (err) {
|
|
98
94
|
mcpSpinner.stop();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type DeviceAuthResponse, pollDeviceToken, startDeviceAuth } from "../lib/auth/client.ts";
|
|
2
|
+
import { type AuthCredentials, saveCredentials } from "../lib/auth/store.ts";
|
|
3
|
+
import { error, info, spinner, success } from "../lib/output.ts";
|
|
4
|
+
|
|
5
|
+
interface LoginOptions {
|
|
6
|
+
/** Skip the initial "Logging in..." message (used when called from auto-login) */
|
|
7
|
+
silent?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function login(options: LoginOptions = {}): Promise<void> {
|
|
11
|
+
if (!options.silent) {
|
|
12
|
+
info("Logging in to jack cloud...");
|
|
13
|
+
console.error("");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const spin = spinner("Starting login...");
|
|
17
|
+
let deviceAuth: DeviceAuthResponse;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
deviceAuth = await startDeviceAuth();
|
|
21
|
+
spin.stop();
|
|
22
|
+
} catch (err) {
|
|
23
|
+
spin.stop();
|
|
24
|
+
error(err instanceof Error ? err.message : "Failed to start login");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.error("");
|
|
29
|
+
console.error(" ┌────────────────────────────────────┐");
|
|
30
|
+
console.error(" │ │");
|
|
31
|
+
console.error(` │ Your code: ${deviceAuth.user_code.padEnd(12)} │`);
|
|
32
|
+
console.error(" │ │");
|
|
33
|
+
console.error(" └────────────────────────────────────┘");
|
|
34
|
+
console.error("");
|
|
35
|
+
info(`Opening ${deviceAuth.verification_uri} in your browser...`);
|
|
36
|
+
console.error("");
|
|
37
|
+
|
|
38
|
+
// Open browser - use Bun.spawn for cross-platform
|
|
39
|
+
try {
|
|
40
|
+
const platform = process.platform;
|
|
41
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
42
|
+
Bun.spawn([cmd, deviceAuth.verification_uri_complete]);
|
|
43
|
+
} catch {
|
|
44
|
+
info(`If the browser didn't open, go to: ${deviceAuth.verification_uri_complete}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pollSpin = spinner("Waiting for you to complete login in browser...");
|
|
48
|
+
const interval = (deviceAuth.interval || 5) * 1000;
|
|
49
|
+
const expiresAt = Date.now() + deviceAuth.expires_in * 1000;
|
|
50
|
+
|
|
51
|
+
while (Date.now() < expiresAt) {
|
|
52
|
+
await sleep(interval);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const tokens = await pollDeviceToken(deviceAuth.device_code);
|
|
56
|
+
|
|
57
|
+
if (tokens) {
|
|
58
|
+
pollSpin.stop();
|
|
59
|
+
|
|
60
|
+
// Default to 5 minutes if expires_in not provided
|
|
61
|
+
const expiresIn = tokens.expires_in ?? 300;
|
|
62
|
+
const creds: AuthCredentials = {
|
|
63
|
+
access_token: tokens.access_token,
|
|
64
|
+
refresh_token: tokens.refresh_token,
|
|
65
|
+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
66
|
+
user: tokens.user,
|
|
67
|
+
};
|
|
68
|
+
await saveCredentials(creds);
|
|
69
|
+
|
|
70
|
+
console.error("");
|
|
71
|
+
success(`Logged in as ${tokens.user.email}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
pollSpin.stop();
|
|
76
|
+
error(err instanceof Error ? err.message : "Login failed");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pollSpin.stop();
|
|
82
|
+
error("Login timed out. Please try again.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sleep(ms: number): Promise<void> {
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { deleteCredentials, getCredentials } from "../lib/auth/store.ts";
|
|
2
|
+
import { info, success } from "../lib/output.ts";
|
|
3
|
+
|
|
4
|
+
export default async function logout(): Promise<void> {
|
|
5
|
+
const creds = await getCredentials();
|
|
6
|
+
|
|
7
|
+
if (!creds) {
|
|
8
|
+
info("Not logged in");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
await deleteCredentials();
|
|
13
|
+
success("Logged out");
|
|
14
|
+
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { output } from "../lib/output.ts";
|
|
3
|
+
import { getProject } from "../lib/registry.ts";
|
|
4
|
+
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
3
5
|
|
|
4
6
|
// Lines containing these strings will be filtered out
|
|
5
7
|
const FILTERED_PATTERNS = ["⛅️ wrangler"];
|
|
@@ -17,6 +19,25 @@ export default async function logs(): Promise<void> {
|
|
|
17
19
|
process.exit(1);
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
// Check if this is a managed project
|
|
23
|
+
let projectName: string | null = null;
|
|
24
|
+
try {
|
|
25
|
+
projectName = await getProjectNameFromDir(process.cwd());
|
|
26
|
+
} catch {
|
|
27
|
+
// Continue without project name - will fall through to wrangler tail
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (projectName) {
|
|
31
|
+
const project = await getProject(projectName);
|
|
32
|
+
if (project?.deploy_mode === "managed") {
|
|
33
|
+
output.warn("Real-time logs not yet available for managed projects");
|
|
34
|
+
output.info("Logs are being collected - web UI coming soon");
|
|
35
|
+
output.info("Track progress: https://github.com/getjack-org/jack/issues/2");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// BYOC project - use wrangler tail
|
|
20
41
|
output.info("Streaming logs from Cloudflare Worker...");
|
|
21
42
|
output.info("Press Ctrl+C to stop\n");
|
|
22
43
|
|
package/src/commands/mcp.ts
CHANGED
|
@@ -1,18 +1,145 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { error, info, success } from "../lib/output.ts";
|
|
2
3
|
import { startMcpServer } from "../mcp/server.ts";
|
|
3
4
|
|
|
4
5
|
interface McpOptions {
|
|
5
6
|
project?: string;
|
|
7
|
+
debug?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export default async function mcp(subcommand?: string, options: McpOptions = {}): Promise<void> {
|
|
9
|
-
if (subcommand
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if (subcommand === "serve") {
|
|
12
|
+
await startMcpServer({
|
|
13
|
+
projectPath: options.project,
|
|
14
|
+
debug: options.debug,
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (subcommand === "test") {
|
|
20
|
+
await testMcpServer();
|
|
21
|
+
return;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
error("Unknown subcommand. Use: jack mcp serve or jack mcp test");
|
|
25
|
+
info("Usage:");
|
|
26
|
+
info(" jack mcp serve [--project /path] [--debug] Start MCP server");
|
|
27
|
+
info(" jack mcp test Test MCP server connectivity");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Test MCP server by spawning it and sending test requests
|
|
33
|
+
*/
|
|
34
|
+
async function testMcpServer(): Promise<void> {
|
|
35
|
+
info("Testing MCP server...\n");
|
|
36
|
+
|
|
37
|
+
const proc = spawn("./src/index.ts", ["mcp", "serve"], {
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
39
|
});
|
|
40
|
+
|
|
41
|
+
const results: { test: string; passed: boolean; error?: string }[] = [];
|
|
42
|
+
|
|
43
|
+
const sendRequest = (id: number, method: string, params: object = {}): Promise<unknown> => {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const timeout = setTimeout(() => reject(new Error("Timeout")), 10000);
|
|
46
|
+
|
|
47
|
+
const handler = (data: Buffer) => {
|
|
48
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
try {
|
|
51
|
+
const response = JSON.parse(line);
|
|
52
|
+
if (response.id === id) {
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
proc.stdout.off("data", handler);
|
|
55
|
+
if (response.error) {
|
|
56
|
+
reject(new Error(response.error.message));
|
|
57
|
+
} else {
|
|
58
|
+
resolve(response.result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Not JSON, ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
proc.stdout.on("data", handler);
|
|
68
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params, id })}\n`);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Test 1: Initialize
|
|
74
|
+
info("1. Testing initialize...");
|
|
75
|
+
const initResult = (await sendRequest(1, "initialize", {
|
|
76
|
+
protocolVersion: "2024-11-05",
|
|
77
|
+
capabilities: {},
|
|
78
|
+
clientInfo: { name: "jack-mcp-test", version: "1.0" },
|
|
79
|
+
})) as { serverInfo?: { name: string; version: string } };
|
|
80
|
+
if (initResult?.serverInfo?.name === "jack") {
|
|
81
|
+
results.push({ test: "initialize", passed: true });
|
|
82
|
+
success(` ✓ Server info: ${initResult.serverInfo.name} v${initResult.serverInfo.version}`);
|
|
83
|
+
} else {
|
|
84
|
+
results.push({ test: "initialize", passed: false, error: "Invalid response" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Test 2: List tools
|
|
88
|
+
info("2. Testing tools/list...");
|
|
89
|
+
const toolsResult = (await sendRequest(2, "tools/list")) as { tools?: { name: string }[] };
|
|
90
|
+
const toolNames = toolsResult?.tools?.map((t) => t.name) ?? [];
|
|
91
|
+
if (toolNames.length > 0) {
|
|
92
|
+
results.push({ test: "tools/list", passed: true });
|
|
93
|
+
success(` ✓ Found ${toolNames.length} tools: ${toolNames.join(", ")}`);
|
|
94
|
+
} else {
|
|
95
|
+
results.push({ test: "tools/list", passed: false, error: "No tools found" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Test 3: List resources
|
|
99
|
+
info("3. Testing resources/list...");
|
|
100
|
+
const resourcesResult = (await sendRequest(3, "resources/list")) as {
|
|
101
|
+
resources?: { name: string }[];
|
|
102
|
+
};
|
|
103
|
+
const resourceCount = resourcesResult?.resources?.length ?? 0;
|
|
104
|
+
results.push({ test: "resources/list", passed: true });
|
|
105
|
+
success(` ✓ Found ${resourceCount} resource(s)`);
|
|
106
|
+
|
|
107
|
+
// Test 4: Call list_projects tool
|
|
108
|
+
info("4. Testing tools/call (list_projects)...");
|
|
109
|
+
const callResult = (await sendRequest(4, "tools/call", {
|
|
110
|
+
name: "list_projects",
|
|
111
|
+
arguments: {},
|
|
112
|
+
})) as { content?: { text: string }[] };
|
|
113
|
+
const responseText = callResult?.content?.[0]?.text;
|
|
114
|
+
if (responseText) {
|
|
115
|
+
const parsed = JSON.parse(responseText);
|
|
116
|
+
if (parsed.success) {
|
|
117
|
+
results.push({ test: "tools/call", passed: true });
|
|
118
|
+
success(` ✓ list_projects returned ${parsed.data?.length ?? 0} projects`);
|
|
119
|
+
} else {
|
|
120
|
+
results.push({ test: "tools/call", passed: false, error: parsed.error?.message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
results.push({ test: "unknown", passed: false, error: errorMsg });
|
|
126
|
+
error(` ✗ Error: ${errorMsg}`);
|
|
127
|
+
} finally {
|
|
128
|
+
proc.kill();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Summary
|
|
132
|
+
console.log("");
|
|
133
|
+
const passed = results.filter((r) => r.passed).length;
|
|
134
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
135
|
+
|
|
136
|
+
if (failed === 0) {
|
|
137
|
+
success(`All ${passed} tests passed! MCP server is working correctly.`);
|
|
138
|
+
} else {
|
|
139
|
+
error(`${failed}/${results.length} tests failed.`);
|
|
140
|
+
for (const r of results.filter((r) => !r.passed)) {
|
|
141
|
+
error(` - ${r.test}: ${r.error}`);
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
18
145
|
}
|
package/src/commands/new.ts
CHANGED
|
@@ -2,22 +2,48 @@ import { getPreferredLaunchAgent, launchAgent } from "../lib/agents.ts";
|
|
|
2
2
|
import { debug } from "../lib/debug.ts";
|
|
3
3
|
import { getErrorDetails } from "../lib/errors.ts";
|
|
4
4
|
import { promptSelect } from "../lib/hooks.ts";
|
|
5
|
+
import { isIntentPhrase } from "../lib/intent.ts";
|
|
5
6
|
import { output, spinner } from "../lib/output.ts";
|
|
6
7
|
import { createProject } from "../lib/project-operations.ts";
|
|
7
8
|
|
|
8
9
|
export default async function newProject(
|
|
9
|
-
|
|
10
|
-
options: { template?: string } = {},
|
|
10
|
+
nameOrPhrase?: string,
|
|
11
|
+
options: { template?: string; intent?: string; managed?: boolean; byo?: boolean; ci?: boolean } = {},
|
|
11
12
|
): Promise<void> {
|
|
12
13
|
// Immediate feedback
|
|
13
14
|
output.start("Starting...");
|
|
14
|
-
debug("newProject called", {
|
|
15
|
-
|
|
15
|
+
debug("newProject called", { nameOrPhrase, options });
|
|
16
|
+
// CI mode: explicit --ci flag, JACK_CI env, or standard CI env
|
|
17
|
+
const isCi =
|
|
18
|
+
options.ci ||
|
|
19
|
+
process.env.JACK_CI === "1" ||
|
|
20
|
+
process.env.JACK_CI === "true" ||
|
|
21
|
+
process.env.CI === "true" ||
|
|
22
|
+
process.env.CI === "1";
|
|
23
|
+
|
|
24
|
+
// Determine if first arg is intent phrase or project name
|
|
25
|
+
let projectName: string | undefined;
|
|
26
|
+
let intentPhrase: string | undefined = options.intent;
|
|
27
|
+
|
|
28
|
+
if (nameOrPhrase) {
|
|
29
|
+
if (options.intent) {
|
|
30
|
+
// Explicit -m flag means first arg is definitely a name
|
|
31
|
+
projectName = nameOrPhrase;
|
|
32
|
+
} else if (isIntentPhrase(nameOrPhrase)) {
|
|
33
|
+
// Detected as intent phrase - name will be auto-generated
|
|
34
|
+
intentPhrase = nameOrPhrase;
|
|
35
|
+
projectName = undefined;
|
|
36
|
+
} else {
|
|
37
|
+
// Treat as project name
|
|
38
|
+
projectName = nameOrPhrase;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
16
41
|
|
|
17
42
|
let result: Awaited<ReturnType<typeof createProject>>;
|
|
18
43
|
try {
|
|
19
|
-
result = await createProject(
|
|
44
|
+
result = await createProject(projectName, {
|
|
20
45
|
template: options.template,
|
|
46
|
+
intent: intentPhrase,
|
|
21
47
|
reporter: {
|
|
22
48
|
start: output.start,
|
|
23
49
|
stop: output.stop,
|
|
@@ -29,6 +55,8 @@ export default async function newProject(
|
|
|
29
55
|
box: output.box,
|
|
30
56
|
},
|
|
31
57
|
interactive: !isCi,
|
|
58
|
+
managed: options.managed,
|
|
59
|
+
byo: options.byo,
|
|
32
60
|
});
|
|
33
61
|
} catch (error) {
|
|
34
62
|
const details = getErrorDetails(error);
|
|
@@ -37,9 +65,9 @@ export default async function newProject(
|
|
|
37
65
|
output.error(details.message);
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
for (const key of
|
|
68
|
+
const missingSecrets = details.meta?.missingSecrets;
|
|
69
|
+
if (missingSecrets?.length) {
|
|
70
|
+
for (const key of missingSecrets) {
|
|
43
71
|
output.info(` Run: jack secrets add ${key}`);
|
|
44
72
|
}
|
|
45
73
|
}
|
|
@@ -48,7 +76,7 @@ export default async function newProject(
|
|
|
48
76
|
console.error(details.meta.stderr);
|
|
49
77
|
}
|
|
50
78
|
|
|
51
|
-
if (details.suggestion && !details.meta?.reported && !
|
|
79
|
+
if (details.suggestion && !details.meta?.reported && !missingSecrets?.length) {
|
|
52
80
|
output.info(details.suggestion);
|
|
53
81
|
}
|
|
54
82
|
|
|
@@ -63,8 +91,8 @@ export default async function newProject(
|
|
|
63
91
|
console.error("");
|
|
64
92
|
output.info(`Project: ${result.targetDir}`);
|
|
65
93
|
|
|
66
|
-
// Prompt to open preferred agent (only in interactive TTY)
|
|
67
|
-
if (process.stdout.isTTY) {
|
|
94
|
+
// Prompt to open preferred agent (only in interactive TTY, skip in CI mode)
|
|
95
|
+
if (process.stdout.isTTY && !isCi) {
|
|
68
96
|
const preferred = await getPreferredLaunchAgent();
|
|
69
97
|
if (preferred) {
|
|
70
98
|
console.error("");
|
|
@@ -73,12 +101,10 @@ export default async function newProject(
|
|
|
73
101
|
const choice = await promptSelect(["Yes", "No"]);
|
|
74
102
|
|
|
75
103
|
if (choice === 0) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const launchResult = await launchAgent(preferred.launch, result.targetDir);
|
|
104
|
+
const launchResult = await launchAgent(preferred.launch, result.targetDir, {
|
|
105
|
+
projectName: result.projectName,
|
|
106
|
+
url: result.workerUrl,
|
|
107
|
+
});
|
|
82
108
|
if (!launchResult.success) {
|
|
83
109
|
output.warn(`Failed to launch ${preferred.definition.name}`);
|
|
84
110
|
if (launchResult.command?.length) {
|
package/src/commands/open.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $ } from "bun";
|
|
2
2
|
import { error, info } from "../lib/output.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProject } from "../lib/project-resolver.ts";
|
|
4
4
|
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
5
5
|
|
|
6
6
|
export interface OpenFlags {
|
|
@@ -22,19 +22,26 @@ export default async function open(projectName?: string, flags: OpenFlags = {}):
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const project = await
|
|
25
|
+
// Resolve project from registry and control plane
|
|
26
|
+
const project = await resolveProject(name);
|
|
27
27
|
|
|
28
|
-
// Determine URL based on flags
|
|
28
|
+
// Determine URL based on flags and deploy mode
|
|
29
29
|
let url: string;
|
|
30
30
|
|
|
31
31
|
if (flags.dash) {
|
|
32
|
+
// Dashboard URLs - use Cloudflare dash for now
|
|
32
33
|
url = `https://dash.cloudflare.com/workers/services/view/${name}`;
|
|
33
34
|
} else if (flags.logs) {
|
|
34
35
|
url = `https://dash.cloudflare.com/workers/services/view/${name}/logs`;
|
|
35
36
|
} else {
|
|
36
|
-
// Default: use
|
|
37
|
-
|
|
37
|
+
// Default: use mode-appropriate URL
|
|
38
|
+
if (project?.sources.controlPlane && project.url) {
|
|
39
|
+
// Managed project: use runjack URL
|
|
40
|
+
url = project.url;
|
|
41
|
+
} else {
|
|
42
|
+
// BYO project: use workers.dev URL or stored URL
|
|
43
|
+
url = project?.url || `https://${name}.workers.dev`;
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
// Open browser using platform-specific command
|