@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.
Files changed (91) hide show
  1. package/package.json +54 -47
  2. package/src/commands/agents.ts +145 -10
  3. package/src/commands/down.ts +110 -102
  4. package/src/commands/feedback.ts +189 -0
  5. package/src/commands/init.ts +8 -12
  6. package/src/commands/login.ts +88 -0
  7. package/src/commands/logout.ts +14 -0
  8. package/src/commands/logs.ts +21 -0
  9. package/src/commands/mcp.ts +134 -7
  10. package/src/commands/new.ts +43 -17
  11. package/src/commands/open.ts +13 -6
  12. package/src/commands/projects.ts +269 -143
  13. package/src/commands/secrets.ts +413 -0
  14. package/src/commands/services.ts +96 -123
  15. package/src/commands/ship.ts +5 -1
  16. package/src/commands/whoami.ts +31 -0
  17. package/src/index.ts +218 -144
  18. package/src/lib/agent-files.ts +34 -0
  19. package/src/lib/agents.ts +390 -22
  20. package/src/lib/asset-hash.ts +50 -0
  21. package/src/lib/auth/client.ts +115 -0
  22. package/src/lib/auth/constants.ts +5 -0
  23. package/src/lib/auth/guard.ts +57 -0
  24. package/src/lib/auth/index.ts +18 -0
  25. package/src/lib/auth/store.ts +54 -0
  26. package/src/lib/binding-validator.ts +136 -0
  27. package/src/lib/build-helper.ts +211 -0
  28. package/src/lib/cloudflare-api.ts +24 -0
  29. package/src/lib/config.ts +5 -6
  30. package/src/lib/control-plane.ts +295 -0
  31. package/src/lib/debug.ts +3 -1
  32. package/src/lib/deploy-mode.ts +93 -0
  33. package/src/lib/deploy-upload.ts +92 -0
  34. package/src/lib/errors.ts +2 -0
  35. package/src/lib/github.ts +31 -1
  36. package/src/lib/hooks.ts +4 -12
  37. package/src/lib/intent.ts +88 -0
  38. package/src/lib/jsonc.ts +125 -0
  39. package/src/lib/local-paths.test.ts +902 -0
  40. package/src/lib/local-paths.ts +258 -0
  41. package/src/lib/managed-deploy.ts +175 -0
  42. package/src/lib/managed-down.ts +159 -0
  43. package/src/lib/mcp-config.ts +55 -34
  44. package/src/lib/names.ts +9 -29
  45. package/src/lib/project-operations.ts +676 -249
  46. package/src/lib/project-resolver.ts +476 -0
  47. package/src/lib/registry.ts +76 -37
  48. package/src/lib/resources.ts +196 -0
  49. package/src/lib/schema.ts +30 -1
  50. package/src/lib/storage/file-filter.ts +1 -0
  51. package/src/lib/storage/index.ts +5 -1
  52. package/src/lib/telemetry.ts +14 -0
  53. package/src/lib/tty.ts +15 -0
  54. package/src/lib/zip-packager.ts +255 -0
  55. package/src/mcp/resources/index.ts +8 -2
  56. package/src/mcp/server.ts +32 -4
  57. package/src/mcp/tools/index.ts +35 -13
  58. package/src/mcp/types.ts +6 -0
  59. package/src/mcp/utils.ts +1 -1
  60. package/src/templates/index.ts +42 -4
  61. package/src/templates/types.ts +13 -0
  62. package/templates/CLAUDE.md +166 -0
  63. package/templates/api/.jack.json +4 -0
  64. package/templates/api/bun.lock +1 -0
  65. package/templates/api/wrangler.jsonc +5 -0
  66. package/templates/hello/.jack.json +28 -0
  67. package/templates/hello/package.json +10 -0
  68. package/templates/hello/src/index.ts +11 -0
  69. package/templates/hello/tsconfig.json +11 -0
  70. package/templates/hello/wrangler.jsonc +5 -0
  71. package/templates/miniapp/.jack.json +15 -4
  72. package/templates/miniapp/bun.lock +135 -40
  73. package/templates/miniapp/index.html +1 -0
  74. package/templates/miniapp/package.json +3 -1
  75. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  76. package/templates/miniapp/public/icon.png +0 -0
  77. package/templates/miniapp/public/og.png +0 -0
  78. package/templates/miniapp/schema.sql +8 -0
  79. package/templates/miniapp/src/App.tsx +254 -3
  80. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  81. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  82. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  83. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  84. package/templates/miniapp/src/index.css +15 -0
  85. package/templates/miniapp/src/lib/api.ts +2 -1
  86. package/templates/miniapp/src/worker.ts +515 -1
  87. package/templates/miniapp/wrangler.jsonc +15 -3
  88. package/LICENSE +0 -190
  89. package/README.md +0 -55
  90. package/src/commands/cloud.ts +0 -230
  91. 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
+ }
@@ -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 IDEs (unless --skip-mcp)
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 installedIdes = await installMcpConfigsToAllIdes();
82
+ const installedApps = await installMcpConfigsToAllApps();
87
83
  mcpSpinner.stop();
88
84
 
89
- if (installedIdes.length > 0) {
90
- success(`MCP server installed to ${installedIdes.length} IDE(s)`);
91
- for (const ideId of installedIdes) {
92
- item(` ${getIdeDisplayName(ideId)}`);
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 IDEs detected for MCP installation");
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
+ }
@@ -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
 
@@ -1,18 +1,145 @@
1
- import { error, info } from "../lib/output.ts";
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 !== "serve") {
10
- error("Unknown subcommand. Use: jack mcp serve");
11
- info("Usage: jack mcp serve [--project /path/to/project]");
12
- process.exit(1);
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
- await startMcpServer({
16
- projectPath: options.project,
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
  }
@@ -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
- name?: string,
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", { name, options });
15
- const isCi = process.env.CI === "true" || process.env.CI === "1";
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(name, {
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 hasMissingSecrets = !!details.meta?.missingSecrets?.length;
41
- if (hasMissingSecrets) {
42
- for (const key of details.meta.missingSecrets) {
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 && !hasMissingSecrets) {
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
- // Ensure terminal is in normal state before handoff
77
- if (process.stdin.isTTY) {
78
- process.stdin.setRawMode(false);
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) {
@@ -1,6 +1,6 @@
1
1
  import { $ } from "bun";
2
2
  import { error, info } from "../lib/output.ts";
3
- import { getProject } from "../lib/registry.ts";
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
- // Get project from registry
26
- const project = await getProject(name);
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 worker URL from registry or construct it
37
- url = project?.workerUrl || `https://${name}.workers.dev`;
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