@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.
- package/README.md +77 -29
- 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/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
package/src/commands/down.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
-
*
|
|
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
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
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("
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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("
|
|
161
|
-
|
|
162
|
-
|
|
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 ===
|
|
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("
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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("
|
|
196
|
-
|
|
197
|
-
|
|
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 ===
|
|
221
|
+
shouldDeleteDb = deleteAction === 0;
|
|
205
222
|
}
|
|
206
223
|
|
|
207
|
-
// Handle
|
|
224
|
+
// Handle backup deletion
|
|
208
225
|
let shouldDeleteR2 = false;
|
|
209
226
|
if (project) {
|
|
210
227
|
console.error("");
|
|
211
|
-
|
|
212
|
-
const deleteR2Action = await
|
|
213
|
-
|
|
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
|
|
265
|
+
// Delete backup if requested
|
|
258
266
|
if (shouldDeleteR2) {
|
|
259
|
-
output.start("Deleting
|
|
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("
|
|
272
|
+
success("Backup deleted");
|
|
265
273
|
} else {
|
|
266
|
-
warn("No
|
|
274
|
+
warn("No backup found or already deleted");
|
|
267
275
|
}
|
|
268
276
|
} catch (err) {
|
|
269
277
|
output.stop();
|
|
270
|
-
warn(`Failed to delete
|
|
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
|
+
}
|
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
|
|