@getjack/jack 0.1.2 → 0.1.4

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/README.md +77 -29
  2. package/package.json +54 -47
  3. package/src/commands/agents.ts +145 -10
  4. package/src/commands/down.ts +110 -102
  5. package/src/commands/feedback.ts +189 -0
  6. package/src/commands/init.ts +8 -12
  7. package/src/commands/login.ts +88 -0
  8. package/src/commands/logout.ts +14 -0
  9. package/src/commands/logs.ts +21 -0
  10. package/src/commands/mcp.ts +134 -7
  11. package/src/commands/new.ts +43 -17
  12. package/src/commands/open.ts +13 -6
  13. package/src/commands/projects.ts +269 -143
  14. package/src/commands/secrets.ts +413 -0
  15. package/src/commands/services.ts +96 -123
  16. package/src/commands/ship.ts +5 -1
  17. package/src/commands/whoami.ts +31 -0
  18. package/src/index.ts +218 -144
  19. package/src/lib/agent-files.ts +34 -0
  20. package/src/lib/agents.ts +390 -22
  21. package/src/lib/asset-hash.ts +50 -0
  22. package/src/lib/auth/client.ts +115 -0
  23. package/src/lib/auth/constants.ts +5 -0
  24. package/src/lib/auth/guard.ts +57 -0
  25. package/src/lib/auth/index.ts +18 -0
  26. package/src/lib/auth/store.ts +54 -0
  27. package/src/lib/binding-validator.ts +136 -0
  28. package/src/lib/build-helper.ts +211 -0
  29. package/src/lib/cloudflare-api.ts +24 -0
  30. package/src/lib/config.ts +5 -6
  31. package/src/lib/control-plane.ts +295 -0
  32. package/src/lib/debug.ts +3 -1
  33. package/src/lib/deploy-mode.ts +93 -0
  34. package/src/lib/deploy-upload.ts +92 -0
  35. package/src/lib/errors.ts +2 -0
  36. package/src/lib/github.ts +31 -1
  37. package/src/lib/hooks.ts +4 -12
  38. package/src/lib/intent.ts +88 -0
  39. package/src/lib/jsonc.ts +125 -0
  40. package/src/lib/local-paths.test.ts +902 -0
  41. package/src/lib/local-paths.ts +258 -0
  42. package/src/lib/managed-deploy.ts +175 -0
  43. package/src/lib/managed-down.ts +159 -0
  44. package/src/lib/mcp-config.ts +55 -34
  45. package/src/lib/names.ts +9 -29
  46. package/src/lib/project-operations.ts +676 -249
  47. package/src/lib/project-resolver.ts +476 -0
  48. package/src/lib/registry.ts +76 -37
  49. package/src/lib/resources.ts +196 -0
  50. package/src/lib/schema.ts +30 -1
  51. package/src/lib/storage/file-filter.ts +1 -0
  52. package/src/lib/storage/index.ts +5 -1
  53. package/src/lib/telemetry.ts +14 -0
  54. package/src/lib/tty.ts +15 -0
  55. package/src/lib/zip-packager.ts +255 -0
  56. package/src/mcp/resources/index.ts +8 -2
  57. package/src/mcp/server.ts +32 -4
  58. package/src/mcp/tools/index.ts +35 -13
  59. package/src/mcp/types.ts +6 -0
  60. package/src/mcp/utils.ts +1 -1
  61. package/src/templates/index.ts +42 -4
  62. package/src/templates/types.ts +13 -0
  63. package/templates/CLAUDE.md +166 -0
  64. package/templates/api/.jack.json +4 -0
  65. package/templates/api/bun.lock +1 -0
  66. package/templates/api/wrangler.jsonc +5 -0
  67. package/templates/hello/.jack.json +28 -0
  68. package/templates/hello/package.json +10 -0
  69. package/templates/hello/src/index.ts +11 -0
  70. package/templates/hello/tsconfig.json +11 -0
  71. package/templates/hello/wrangler.jsonc +5 -0
  72. package/templates/miniapp/.jack.json +15 -4
  73. package/templates/miniapp/bun.lock +135 -40
  74. package/templates/miniapp/index.html +1 -0
  75. package/templates/miniapp/package.json +3 -1
  76. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  77. package/templates/miniapp/public/icon.png +0 -0
  78. package/templates/miniapp/public/og.png +0 -0
  79. package/templates/miniapp/schema.sql +8 -0
  80. package/templates/miniapp/src/App.tsx +254 -3
  81. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  82. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  83. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  84. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  85. package/templates/miniapp/src/index.css +15 -0
  86. package/templates/miniapp/src/lib/api.ts +2 -1
  87. package/templates/miniapp/src/worker.ts +515 -1
  88. package/templates/miniapp/wrangler.jsonc +15 -3
  89. package/LICENSE +0 -190
  90. package/src/commands/cloud.ts +0 -230
  91. package/templates/api/wrangler.toml +0 -3
@@ -1,63 +1,55 @@
1
- import { existsSync } from "node:fs";
2
1
  import { join } from "node:path";
3
- import { select } from "@inquirer/prompts";
4
2
  import {
5
3
  checkWorkerExists,
6
4
  deleteDatabase,
7
5
  deleteWorker,
8
6
  exportDatabase,
9
7
  } from "../lib/cloudflare-api.ts";
8
+ import { fetchProjectResources } from "../lib/control-plane.ts";
9
+ import { promptSelect } from "../lib/hooks.ts";
10
+ import { managedDown } from "../lib/managed-down.ts";
10
11
  import { error, info, item, output, success, warn } from "../lib/output.ts";
11
- import {
12
- type Project,
13
- getProject,
14
- getProjectDatabaseName,
15
- updateProject,
16
- updateProjectDatabase,
17
- } from "../lib/registry.ts";
12
+ import { resolveProject } from "../lib/project-resolver.ts";
13
+ import { type Project, getProject, updateProject } from "../lib/registry.ts";
14
+ import { parseWranglerResources } from "../lib/resources.ts";
18
15
  import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
19
16
 
20
17
  /**
21
- * Get database name for a project, with fallback to wrangler config
18
+ * Resolve database name for a project.
19
+ * For managed projects: fetch from control plane.
20
+ * For BYO projects: parse from wrangler.jsonc in cwd.
22
21
  */
23
- async function resolveDbName(project: Project): Promise<string | null> {
24
- // First check registry
25
- const dbFromRegistry = getProjectDatabaseName(project);
26
- if (dbFromRegistry) {
27
- return dbFromRegistry;
22
+ async function resolveDatabaseName(project: Project, projectName: string): Promise<string | null> {
23
+ // For managed projects, fetch from control plane
24
+ if (project.deploy_mode === "managed" && project.remote?.project_id) {
25
+ try {
26
+ const resources = await fetchProjectResources(project.remote.project_id);
27
+ const d1 = resources.find((r) => r.resource_type === "d1");
28
+ return d1?.resource_name || null;
29
+ } catch {
30
+ return null;
31
+ }
28
32
  }
29
33
 
30
- // Fallback: read from wrangler config file
31
- if (project.localPath && existsSync(project.localPath)) {
32
- const jsoncPath = join(project.localPath, "wrangler.jsonc");
33
- if (existsSync(jsoncPath)) {
34
- try {
35
- const content = await Bun.file(jsoncPath).text();
36
- const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
37
- const config = JSON.parse(jsonContent);
38
- if (config.d1_databases?.[0]?.database_name) {
39
- return config.d1_databases[0].database_name;
40
- }
41
- } catch {
42
- // Ignore parse errors
43
- }
34
+ // For BYO, parse from wrangler config in cwd
35
+ try {
36
+ let cwdProjectName: string | null = null;
37
+ try {
38
+ cwdProjectName = await getProjectNameFromDir(process.cwd());
39
+ } catch {
40
+ cwdProjectName = null;
44
41
  }
45
42
 
46
- const tomlPath = join(project.localPath, "wrangler.toml");
47
- if (existsSync(tomlPath)) {
48
- try {
49
- const content = await Bun.file(tomlPath).text();
50
- const match = content.match(/database_name\s*=\s*"([^"]+)"/);
51
- if (match?.[1]) {
52
- return match[1];
53
- }
54
- } catch {
55
- // Ignore read errors
56
- }
43
+ if (!cwdProjectName || cwdProjectName !== projectName) {
44
+ warn(`Run this command from the ${projectName} project directory to manage its database.`);
45
+ return null;
57
46
  }
58
- }
59
47
 
60
- return null;
48
+ const resources = await parseWranglerResources(process.cwd());
49
+ return resources.d1?.name || null;
50
+ } catch {
51
+ return null;
52
+ }
61
53
  }
62
54
 
63
55
  export interface DownFlags {
@@ -78,13 +70,58 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
78
70
  }
79
71
  }
80
72
 
81
- // Get project from registry
73
+ // Resolve project from all sources (registry + control plane)
74
+ const resolved = await resolveProject(name);
75
+
76
+ // Check if found only on control plane (orphaned managed project)
77
+ if (resolved?.sources.controlPlane && !resolved.sources.registry) {
78
+ console.error("");
79
+ info(`Found "${name}" on jack cloud, linking locally...`);
80
+ }
81
+
82
+ // Get the registry project (may have been created by resolver cache)
82
83
  const project = await getProject(name);
83
- if (!project) {
84
- warn(`Project '${name}' not found in registry`);
84
+
85
+ if (!resolved && !project) {
86
+ // Not found anywhere
87
+ warn(`Project '${name}' not found`);
85
88
  info("Will attempt to undeploy if deployed");
86
89
  }
87
90
 
91
+ // Check if this is a managed project (either from resolved or registry)
92
+ const isManaged =
93
+ resolved?.remote?.projectId ||
94
+ (project?.deploy_mode === "managed" && project.remote?.project_id);
95
+
96
+ if (isManaged) {
97
+ // Build project object for managedDown if we only have resolved data
98
+ const managedProject: Project = project || {
99
+ workerUrl: resolved?.url || null,
100
+ createdAt: resolved?.createdAt || new Date().toISOString(),
101
+ lastDeployed: resolved?.updatedAt || null,
102
+ status: resolved?.status === "live" ? "live" : "build_failed",
103
+ deploy_mode: "managed",
104
+ remote:
105
+ resolved?.remote && resolved.url
106
+ ? {
107
+ project_id: resolved.remote.projectId,
108
+ project_slug: resolved.slug,
109
+ org_id: resolved.remote.orgId,
110
+ runjack_url: resolved.url,
111
+ }
112
+ : undefined,
113
+ };
114
+
115
+ // Route to managed deletion flow
116
+ const deleteSuccess = await managedDown(managedProject, name, flags);
117
+ if (!deleteSuccess) {
118
+ process.exit(0); // User cancelled
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Continue with existing BYO flow...
124
+
88
125
  // Check if worker exists
89
126
  output.start("Checking deployment...");
90
127
  const workerExists = await checkWorkerExists(name);
@@ -117,7 +154,7 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
117
154
 
118
155
  console.error("");
119
156
  success(`'${name}' undeployed`);
120
- info("Databases and cloud storage were not affected");
157
+ info("Databases and backups were not affected");
121
158
  console.error("");
122
159
  return;
123
160
  }
@@ -128,23 +165,18 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
128
165
  if (project?.workerUrl) {
129
166
  item(`URL: ${project.workerUrl}`);
130
167
  }
131
- const dbName = project ? await resolveDbName(project) : null;
168
+ const dbName = project ? await resolveDatabaseName(project, name) : null;
132
169
  if (dbName) {
133
170
  item(`Database: ${dbName}`);
134
171
  }
135
172
  console.error("");
136
173
 
137
174
  // Confirm undeploy
138
- console.error(" Esc to skip\n");
139
- const action = await select({
140
- message: "Undeploy this project?",
141
- choices: [
142
- { name: "1. Yes", value: "yes" },
143
- { name: "2. No", value: "no" },
144
- ],
145
- });
146
-
147
- if (action === "no") {
175
+ console.error("");
176
+ info("Undeploy this project?");
177
+ const action = await promptSelect(["Yes", "No"]);
178
+
179
+ if (action !== 0) {
148
180
  info("Cancelled");
149
181
  return;
150
182
  }
@@ -157,16 +189,11 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
157
189
  info(`Found database: ${dbName}`);
158
190
 
159
191
  // Ask if they want to export first
160
- console.error(" Esc to skip\n");
161
- const exportAction = await select({
162
- message: `Export database '${dbName}' before deleting?`,
163
- choices: [
164
- { name: "1. Yes", value: "yes" },
165
- { name: "2. No", value: "no" },
166
- ],
167
- });
192
+ console.error("");
193
+ info(`Export database '${dbName}' before deleting?`);
194
+ const exportAction = await promptSelect(["Yes", "No"]);
168
195
 
169
- if (exportAction === "yes") {
196
+ if (exportAction === 0) {
170
197
  const exportPath = join(process.cwd(), `${dbName}-backup.sql`);
171
198
  output.start(`Exporting database to ${exportPath}...`);
172
199
  try {
@@ -176,15 +203,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
176
203
  } catch (err) {
177
204
  output.stop();
178
205
  error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
179
- console.error(" Esc to skip\n");
180
- const continueAction = await select({
181
- message: "Continue without exporting?",
182
- choices: [
183
- { name: "1. Yes", value: "yes" },
184
- { name: "2. No", value: "no" },
185
- ],
186
- });
187
- if (continueAction === "no") {
206
+ console.error("");
207
+ info("Continue without exporting?");
208
+ const continueAction = await promptSelect(["Yes", "No"]);
209
+ if (continueAction !== 0) {
188
210
  info("Cancelled");
189
211
  return;
190
212
  }
@@ -192,31 +214,20 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
192
214
  }
193
215
 
194
216
  // Ask if they want to delete the database
195
- console.error(" Esc to skip\n");
196
- const deleteAction = await select({
197
- message: `Delete database '${dbName}'?`,
198
- choices: [
199
- { name: "1. Yes", value: "yes" },
200
- { name: "2. No", value: "no" },
201
- ],
202
- });
217
+ console.error("");
218
+ info(`Delete database '${dbName}'?`);
219
+ const deleteAction = await promptSelect(["Yes", "No"]);
203
220
 
204
- shouldDeleteDb = deleteAction === "yes";
221
+ shouldDeleteDb = deleteAction === 0;
205
222
  }
206
223
 
207
- // Handle R2 backup deletion
224
+ // Handle backup deletion
208
225
  let shouldDeleteR2 = false;
209
226
  if (project) {
210
227
  console.error("");
211
- console.error(" Esc to skip\n");
212
- const deleteR2Action = await select({
213
- message: "Delete cloud backup for this project?",
214
- choices: [
215
- { name: "1. Yes", value: "yes" },
216
- { name: "2. No", value: "no" },
217
- ],
218
- });
219
- shouldDeleteR2 = deleteR2Action === "yes";
228
+ info("Delete backup for this project?");
229
+ const deleteR2Action = await promptSelect(["Yes", "No"]);
230
+ shouldDeleteR2 = deleteR2Action === 0;
220
231
  }
221
232
 
222
233
  // Execute deletions
@@ -243,9 +254,6 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
243
254
  await deleteDatabase(dbName);
244
255
  output.stop();
245
256
  success(`Database '${dbName}' deleted`);
246
-
247
- // Update registry
248
- await updateProjectDatabase(name, null);
249
257
  } catch (err) {
250
258
  output.stop();
251
259
  warn(
@@ -254,20 +262,20 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
254
262
  }
255
263
  }
256
264
 
257
- // Delete cloud backup if requested
265
+ // Delete backup if requested
258
266
  if (shouldDeleteR2) {
259
- output.start("Deleting cloud backup...");
267
+ output.start("Deleting backup...");
260
268
  try {
261
269
  const deleted = await deleteCloudProject(name);
262
270
  output.stop();
263
271
  if (deleted) {
264
- success("Cloud backup deleted");
272
+ success("Backup deleted");
265
273
  } else {
266
- warn("No cloud backup found or already deleted");
274
+ warn("No backup found or already deleted");
267
275
  }
268
276
  } catch (err) {
269
277
  output.stop();
270
- warn(`Failed to delete cloud backup: ${err instanceof Error ? err.message : String(err)}`);
278
+ warn(`Failed to delete backup: ${err instanceof Error ? err.message : String(err)}`);
271
279
  }
272
280
  }
273
281
 
@@ -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