@getjack/jack 0.1.22 → 0.1.23
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 +1 -1
- package/src/commands/clone.ts +5 -5
- package/src/commands/down.ts +44 -69
- package/src/commands/link.ts +9 -6
- package/src/commands/new.ts +54 -76
- package/src/commands/publish.ts +7 -2
- package/src/commands/secrets.ts +2 -1
- package/src/commands/services.ts +41 -15
- package/src/commands/update.ts +2 -2
- package/src/index.ts +43 -2
- package/src/lib/agent-integration.ts +217 -0
- package/src/lib/auth/login-flow.ts +2 -1
- package/src/lib/binding-validator.ts +2 -3
- package/src/lib/build-helper.ts +7 -1
- package/src/lib/hooks.ts +101 -21
- package/src/lib/managed-down.ts +32 -55
- package/src/lib/project-detection.ts +48 -21
- package/src/lib/project-operations.ts +31 -13
- package/src/lib/prompts.ts +16 -23
- package/src/lib/services/db-execute.ts +39 -0
- package/src/lib/services/sql-classifier.test.ts +2 -2
- package/src/lib/services/sql-classifier.ts +5 -4
- package/src/lib/version-check.ts +15 -10
- package/src/lib/zip-packager.ts +16 -0
- package/src/mcp/resources/index.ts +42 -2
- package/src/templates/index.ts +63 -3
- package/templates/ai-chat/.jack.json +29 -0
- package/templates/ai-chat/bun.lock +18 -0
- package/templates/ai-chat/package.json +14 -0
- package/templates/ai-chat/public/chat.js +149 -0
- package/templates/ai-chat/public/index.html +209 -0
- package/templates/ai-chat/src/index.ts +105 -0
- package/templates/ai-chat/wrangler.jsonc +12 -0
- package/templates/semantic-search/.jack.json +26 -0
- package/templates/semantic-search/bun.lock +18 -0
- package/templates/semantic-search/package.json +12 -0
- package/templates/semantic-search/public/app.js +120 -0
- package/templates/semantic-search/public/index.html +210 -0
- package/templates/semantic-search/schema.sql +5 -0
- package/templates/semantic-search/src/index.ts +144 -0
- package/templates/semantic-search/tsconfig.json +13 -0
- package/templates/semantic-search/wrangler.jsonc +27 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent integration module
|
|
3
|
+
*
|
|
4
|
+
* Ensures AI agents have proper context for jack projects.
|
|
5
|
+
* Called during both project creation and first BYO deploy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { Template } from "../templates/types.ts";
|
|
11
|
+
import { installMcpConfigsToAllApps, isAppInstalled } from "./mcp-config.ts";
|
|
12
|
+
|
|
13
|
+
export interface EnsureAgentResult {
|
|
14
|
+
mcpInstalled: string[];
|
|
15
|
+
jackMdCreated: boolean;
|
|
16
|
+
referencesAdded: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EnsureAgentOptions {
|
|
20
|
+
template?: Template;
|
|
21
|
+
silent?: boolean;
|
|
22
|
+
projectName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate JACK.md content
|
|
27
|
+
* Uses template agentContext if available, otherwise generic jack instructions
|
|
28
|
+
*/
|
|
29
|
+
export function generateJackMd(projectName?: string, template?: Template): string {
|
|
30
|
+
const header = projectName ? `# ${projectName}\n\n` : "# Jack\n\n";
|
|
31
|
+
|
|
32
|
+
const templateSummary = template?.agentContext?.summary;
|
|
33
|
+
const templateFullText = template?.agentContext?.full_text;
|
|
34
|
+
|
|
35
|
+
const summarySection = templateSummary ? `> ${templateSummary}\n\n` : "";
|
|
36
|
+
|
|
37
|
+
const templateSection = templateFullText ? `${templateFullText}\n\n` : "";
|
|
38
|
+
|
|
39
|
+
return `${header}${summarySection}This project is deployed and managed via jack.
|
|
40
|
+
|
|
41
|
+
## Quick Commands
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
|---------|--------------|
|
|
45
|
+
| \`jack ship\` | Deploy to production |
|
|
46
|
+
| \`jack logs\` | Stream live logs |
|
|
47
|
+
| \`jack services\` | Manage databases, KV, and other bindings |
|
|
48
|
+
| \`jack secrets\` | Manage environment secrets |
|
|
49
|
+
|
|
50
|
+
## Important
|
|
51
|
+
|
|
52
|
+
- **Never run \`wrangler\` commands directly** - jack handles all infrastructure
|
|
53
|
+
- Use \`jack services db\` to create and query databases
|
|
54
|
+
- Secrets sync automatically across deploys
|
|
55
|
+
|
|
56
|
+
## Services & Bindings
|
|
57
|
+
|
|
58
|
+
Jack manages your project's services. To add a database:
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
jack services db create
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
To query it:
|
|
65
|
+
|
|
66
|
+
\`\`\`bash
|
|
67
|
+
jack services db query "SELECT * FROM users"
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
More bindings (KV, R2, queues) coming soon.
|
|
71
|
+
|
|
72
|
+
${templateSection}## For AI Agents
|
|
73
|
+
|
|
74
|
+
### MCP Tools
|
|
75
|
+
|
|
76
|
+
If jack MCP is connected, prefer these tools over CLI commands:
|
|
77
|
+
|
|
78
|
+
| Tool | Use for |
|
|
79
|
+
|------|---------|
|
|
80
|
+
| \`mcp__jack__deploy_project\` | Deploy changes |
|
|
81
|
+
| \`mcp__jack__create_database\` | Create a new database |
|
|
82
|
+
| \`mcp__jack__execute_sql\` | Query the database |
|
|
83
|
+
| \`mcp__jack__list_projects\` | List all projects |
|
|
84
|
+
| \`mcp__jack__get_project_status\` | Check deployment status |
|
|
85
|
+
|
|
86
|
+
### Documentation
|
|
87
|
+
|
|
88
|
+
Full jack documentation: https://docs.getjack.org/llms-full.txt
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create JACK.md if it doesn't exist
|
|
94
|
+
*/
|
|
95
|
+
async function ensureJackMd(
|
|
96
|
+
projectPath: string,
|
|
97
|
+
projectName?: string,
|
|
98
|
+
template?: Template,
|
|
99
|
+
): Promise<boolean> {
|
|
100
|
+
const jackMdPath = join(projectPath, "JACK.md");
|
|
101
|
+
|
|
102
|
+
if (existsSync(jackMdPath)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = generateJackMd(projectName, template);
|
|
107
|
+
await Bun.write(jackMdPath, content);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Append JACK.md reference to existing agent files (CLAUDE.md, AGENTS.md)
|
|
113
|
+
*/
|
|
114
|
+
async function appendJackMdReferences(projectPath: string): Promise<string[]> {
|
|
115
|
+
const filesToCheck = ["CLAUDE.md", "AGENTS.md"];
|
|
116
|
+
const referencesAdded: string[] = [];
|
|
117
|
+
const jackMdPath = join(projectPath, "JACK.md");
|
|
118
|
+
|
|
119
|
+
// Only add references if JACK.md exists
|
|
120
|
+
if (!existsSync(jackMdPath)) {
|
|
121
|
+
return referencesAdded;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const referenceBlock = `<!-- Added by jack -->
|
|
125
|
+
> **Jack project** - See [JACK.md](./JACK.md) for deployment, services, and bindings.
|
|
126
|
+
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
for (const filename of filesToCheck) {
|
|
130
|
+
const filePath = join(projectPath, filename);
|
|
131
|
+
|
|
132
|
+
if (!existsSync(filePath)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const content = await Bun.file(filePath).text();
|
|
138
|
+
|
|
139
|
+
// Skip if reference already exists
|
|
140
|
+
if (content.includes("JACK.md") || content.includes("<!-- Added by jack -->")) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Find position after first heading, or prepend if no heading
|
|
145
|
+
const headingMatch = content.match(/^#[^\n]*\n/m);
|
|
146
|
+
let newContent: string;
|
|
147
|
+
|
|
148
|
+
if (headingMatch && headingMatch.index !== undefined) {
|
|
149
|
+
const insertPos = headingMatch.index + headingMatch[0].length;
|
|
150
|
+
newContent =
|
|
151
|
+
content.slice(0, insertPos) + "\n" + referenceBlock + content.slice(insertPos);
|
|
152
|
+
} else {
|
|
153
|
+
newContent = referenceBlock + content;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await Bun.write(filePath, newContent);
|
|
157
|
+
referencesAdded.push(filename);
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore errors reading/writing individual files
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return referencesAdded;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Ensure MCP is configured for detected AI apps
|
|
168
|
+
* Returns list of apps that were configured
|
|
169
|
+
*/
|
|
170
|
+
async function ensureMcpConfigured(): Promise<string[]> {
|
|
171
|
+
// Only attempt if at least one supported app is installed
|
|
172
|
+
const hasClaudeCode = isAppInstalled("claude-code");
|
|
173
|
+
const hasClaudeDesktop = isAppInstalled("claude-desktop");
|
|
174
|
+
|
|
175
|
+
if (!hasClaudeCode && !hasClaudeDesktop) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
return await installMcpConfigsToAllApps();
|
|
181
|
+
} catch {
|
|
182
|
+
// Don't fail if MCP install fails
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Ensure agent integration is set up for a project
|
|
189
|
+
*
|
|
190
|
+
* This function:
|
|
191
|
+
* 1. Creates JACK.md if not exists (with template context if available)
|
|
192
|
+
* 2. Appends JACK.md reference to existing CLAUDE.md/AGENTS.md
|
|
193
|
+
* 3. Installs MCP config to detected AI apps
|
|
194
|
+
*
|
|
195
|
+
* Safe to call multiple times - all operations are idempotent.
|
|
196
|
+
*/
|
|
197
|
+
export async function ensureAgentIntegration(
|
|
198
|
+
projectPath: string,
|
|
199
|
+
options: EnsureAgentOptions = {},
|
|
200
|
+
): Promise<EnsureAgentResult> {
|
|
201
|
+
const { template, projectName } = options;
|
|
202
|
+
|
|
203
|
+
// 1. Create JACK.md if not exists
|
|
204
|
+
const jackMdCreated = await ensureJackMd(projectPath, projectName, template);
|
|
205
|
+
|
|
206
|
+
// 2. Append references to existing agent files
|
|
207
|
+
const referencesAdded = await appendJackMdReferences(projectPath);
|
|
208
|
+
|
|
209
|
+
// 3. Ensure MCP is configured
|
|
210
|
+
const mcpInstalled = await ensureMcpConfigured();
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
mcpInstalled,
|
|
214
|
+
jackMdCreated,
|
|
215
|
+
referencesAdded,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -19,6 +19,7 @@ export const SUPPORTED_BINDINGS = [
|
|
|
19
19
|
"vars",
|
|
20
20
|
"r2_buckets",
|
|
21
21
|
"kv_namespaces",
|
|
22
|
+
"vectorize",
|
|
22
23
|
] as const;
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -30,7 +31,6 @@ export const UNSUPPORTED_BINDINGS = [
|
|
|
30
31
|
"queues",
|
|
31
32
|
"services",
|
|
32
33
|
"hyperdrive",
|
|
33
|
-
"vectorize",
|
|
34
34
|
"browser",
|
|
35
35
|
"mtls_certificates",
|
|
36
36
|
] as const;
|
|
@@ -43,7 +43,6 @@ const BINDING_DISPLAY_NAMES: Record<string, string> = {
|
|
|
43
43
|
queues: "Queues",
|
|
44
44
|
services: "Service Bindings",
|
|
45
45
|
hyperdrive: "Hyperdrive",
|
|
46
|
-
vectorize: "Vectorize",
|
|
47
46
|
browser: "Browser Rendering",
|
|
48
47
|
mtls_certificates: "mTLS Certificates",
|
|
49
48
|
};
|
|
@@ -72,7 +71,7 @@ export function validateBindings(
|
|
|
72
71
|
if (value !== undefined && value !== null) {
|
|
73
72
|
const displayName = BINDING_DISPLAY_NAMES[binding] || binding;
|
|
74
73
|
errors.push(
|
|
75
|
-
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, KV, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
74
|
+
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, KV, Vectorize, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
76
75
|
);
|
|
77
76
|
}
|
|
78
77
|
}
|
package/src/lib/build-helper.ts
CHANGED
|
@@ -57,12 +57,18 @@ export interface WranglerConfig {
|
|
|
57
57
|
binding: string;
|
|
58
58
|
id?: string; // Optional - wrangler auto-provisions if missing
|
|
59
59
|
}>;
|
|
60
|
+
vectorize?: Array<{
|
|
61
|
+
binding: string;
|
|
62
|
+
index_name?: string;
|
|
63
|
+
preset?: "cloudflare" | "cloudflare-small" | "cloudflare-large";
|
|
64
|
+
dimensions?: number;
|
|
65
|
+
metric?: "cosine" | "euclidean" | "dot-product";
|
|
66
|
+
}>;
|
|
60
67
|
// Unsupported bindings (for validation)
|
|
61
68
|
durable_objects?: unknown;
|
|
62
69
|
queues?: unknown;
|
|
63
70
|
services?: unknown;
|
|
64
71
|
hyperdrive?: unknown;
|
|
65
|
-
vectorize?: unknown;
|
|
66
72
|
browser?: unknown;
|
|
67
73
|
mtls_certificates?: unknown;
|
|
68
74
|
}
|
package/src/lib/hooks.ts
CHANGED
|
@@ -23,14 +23,13 @@ async function readMultilineJson(prompt: string): Promise<string> {
|
|
|
23
23
|
|
|
24
24
|
return new Promise((resolve) => {
|
|
25
25
|
rl.on("line", (line) => {
|
|
26
|
-
|
|
26
|
+
// Empty line = submit (or skip if nothing entered)
|
|
27
|
+
if (line.trim() === "") {
|
|
27
28
|
rl.close();
|
|
28
29
|
resolve(lines.join("\n"));
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
lines.push(line);
|
|
33
|
-
}
|
|
32
|
+
lines.push(line);
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
rl.on("close", () => {
|
|
@@ -82,31 +81,112 @@ const noopOutput: HookOutput = {
|
|
|
82
81
|
* - writeJson: { path, set, successMessage? } -> JSON update (runs in non-interactive)
|
|
83
82
|
*/
|
|
84
83
|
|
|
84
|
+
// Re-export isCancel for consumers
|
|
85
|
+
export { isCancel } from "@clack/core";
|
|
86
|
+
|
|
87
|
+
// Unicode symbols (with ASCII fallbacks for non-unicode terminals)
|
|
88
|
+
const isUnicodeSupported =
|
|
89
|
+
process.platform !== "win32" ||
|
|
90
|
+
Boolean(process.env.CI) ||
|
|
91
|
+
Boolean(process.env.WT_SESSION) ||
|
|
92
|
+
process.env.TERM_PROGRAM === "vscode" ||
|
|
93
|
+
process.env.TERM === "xterm-256color";
|
|
94
|
+
|
|
95
|
+
const S_RADIO_ACTIVE = isUnicodeSupported ? "●" : ">";
|
|
96
|
+
const S_RADIO_INACTIVE = isUnicodeSupported ? "○" : " ";
|
|
97
|
+
|
|
98
|
+
export interface SelectOption<T = string> {
|
|
99
|
+
value: T;
|
|
100
|
+
label: string;
|
|
101
|
+
hint?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
105
|
+
* Clean select prompt with bullet-point style (no vertical bars)
|
|
106
|
+
* Supports both:
|
|
107
|
+
* - Number keys (1, 2, 3...) for immediate selection
|
|
108
|
+
* - Arrow keys (up/down) + Enter for navigation-based selection
|
|
109
|
+
*
|
|
110
|
+
* @param message - The prompt message to display
|
|
111
|
+
* @param options - Array of options (strings or {value, label, hint?} objects)
|
|
112
|
+
* @returns The selected value, or symbol if cancelled
|
|
89
113
|
*/
|
|
90
|
-
export async function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
export async function promptSelectValue<T>(
|
|
115
|
+
message: string,
|
|
116
|
+
options: Array<SelectOption<T> | string>,
|
|
117
|
+
): Promise<T | symbol> {
|
|
118
|
+
const { SelectPrompt, isCancel } = await import("@clack/core");
|
|
119
|
+
const pc = await import("picocolors");
|
|
120
|
+
|
|
121
|
+
// Normalize options to {value, label} format
|
|
122
|
+
const normalizedOptions = options.map((opt, index) => {
|
|
123
|
+
if (typeof opt === "string") {
|
|
124
|
+
return { value: opt as T, label: opt, key: String(index + 1) };
|
|
125
|
+
}
|
|
126
|
+
return { ...opt, key: String(index + 1) };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const prompt = new SelectPrompt({
|
|
130
|
+
options: normalizedOptions,
|
|
131
|
+
initialValue: normalizedOptions[0]?.value,
|
|
132
|
+
render() {
|
|
133
|
+
const title = `${message}\n`;
|
|
134
|
+
const lines: string[] = [];
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < normalizedOptions.length; i++) {
|
|
137
|
+
const opt = normalizedOptions[i];
|
|
138
|
+
const isActive = this.cursor === i;
|
|
139
|
+
const num = `${i + 1}.`;
|
|
140
|
+
|
|
141
|
+
if (isActive) {
|
|
142
|
+
const hint = opt.hint ? pc.dim(` (${opt.hint})`) : "";
|
|
143
|
+
lines.push(`${pc.green(S_RADIO_ACTIVE)} ${num} ${opt.label}${hint}`);
|
|
144
|
+
} else {
|
|
145
|
+
lines.push(`${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(num)} ${pc.dim(opt.label)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return title + lines.join("\n");
|
|
150
|
+
},
|
|
102
151
|
});
|
|
103
152
|
|
|
153
|
+
// Add number key support for immediate selection
|
|
154
|
+
prompt.on("key", (char) => {
|
|
155
|
+
if (!char) return;
|
|
156
|
+
const num = Number.parseInt(char, 10);
|
|
157
|
+
if (num >= 1 && num <= normalizedOptions.length) {
|
|
158
|
+
prompt.value = normalizedOptions[num - 1]?.value;
|
|
159
|
+
prompt.emit("submit");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await prompt.prompt();
|
|
164
|
+
|
|
165
|
+
if (isCancel(result)) {
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result as T;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Simple select prompt for string options (returns index)
|
|
174
|
+
* Supports both number keys (1, 2...) and arrow keys + Enter
|
|
175
|
+
* Returns the selected option index (0-based) or -1 if cancelled
|
|
176
|
+
*/
|
|
177
|
+
export async function promptSelect(options: string[], message?: string): Promise<number> {
|
|
178
|
+
const { isCancel } = await import("@clack/core");
|
|
179
|
+
|
|
180
|
+
const result = await promptSelectValue(
|
|
181
|
+
message ?? "",
|
|
182
|
+
options.map((label, index) => ({ value: index, label })),
|
|
183
|
+
);
|
|
184
|
+
|
|
104
185
|
if (isCancel(result)) {
|
|
105
186
|
return -1;
|
|
106
187
|
}
|
|
107
188
|
|
|
108
|
-
|
|
109
|
-
return Number.parseInt(result as string, 10) - 1;
|
|
189
|
+
return result as number;
|
|
110
190
|
}
|
|
111
191
|
|
|
112
192
|
/**
|
package/src/lib/managed-down.ts
CHANGED
|
@@ -76,9 +76,12 @@ export async function managedDown(
|
|
|
76
76
|
}
|
|
77
77
|
console.error("");
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Single confirmation with clear description
|
|
80
80
|
console.error("");
|
|
81
|
-
|
|
81
|
+
const confirmMsg = hasDatabase
|
|
82
|
+
? "Undeploy this project? All resources will be deleted."
|
|
83
|
+
: "Undeploy this project?";
|
|
84
|
+
info(confirmMsg);
|
|
82
85
|
const action = await promptSelect(["Yes", "No"]);
|
|
83
86
|
|
|
84
87
|
if (action !== 0) {
|
|
@@ -86,77 +89,51 @@ export async function managedDown(
|
|
|
86
89
|
return false;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
//
|
|
92
|
+
// Auto-export database if it exists (no prompt)
|
|
93
|
+
let exportPath: string | null = null;
|
|
90
94
|
if (hasDatabase) {
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
exportPath = join(process.cwd(), `${projectName}-backup.sql`);
|
|
96
|
+
output.start(`Exporting database to ${exportPath}...`);
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const exportResult = await exportManagedDatabase(projectId);
|
|
104
|
-
|
|
105
|
-
// Download the SQL file
|
|
106
|
-
const response = await fetch(exportResult.download_url);
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const sqlContent = await response.text();
|
|
112
|
-
await writeFile(exportPath, sqlContent, "utf-8");
|
|
113
|
-
|
|
114
|
-
output.stop();
|
|
115
|
-
success(`Database exported to ${exportPath}`);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
output.stop();
|
|
118
|
-
error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
-
|
|
120
|
-
// If export times out, abort
|
|
121
|
-
if (err instanceof Error && err.message.includes("timed out")) {
|
|
122
|
-
error("Export timeout - deletion aborted");
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.error("");
|
|
127
|
-
info("Continue without exporting?");
|
|
128
|
-
const continueAction = await promptSelect(["Yes", "No"]);
|
|
129
|
-
|
|
130
|
-
if (continueAction !== 0) {
|
|
131
|
-
info("Cancelled");
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
98
|
+
try {
|
|
99
|
+
const exportResult = await exportManagedDatabase(projectId);
|
|
100
|
+
|
|
101
|
+
// Download the SQL file
|
|
102
|
+
const response = await fetch(exportResult.download_url);
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
134
105
|
}
|
|
106
|
+
|
|
107
|
+
const sqlContent = await response.text();
|
|
108
|
+
await writeFile(exportPath, sqlContent, "utf-8");
|
|
109
|
+
|
|
110
|
+
output.stop();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
output.stop();
|
|
113
|
+
warn(`Could not export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
114
|
+
exportPath = null;
|
|
135
115
|
}
|
|
136
116
|
}
|
|
137
117
|
|
|
138
118
|
// Execute deletion
|
|
139
|
-
console.error("");
|
|
140
|
-
info("Executing cleanup...");
|
|
141
|
-
console.error("");
|
|
142
|
-
|
|
143
119
|
output.start("Undeploying from jack cloud...");
|
|
144
120
|
try {
|
|
145
121
|
const result = await deleteManagedProject(projectId);
|
|
146
122
|
output.stop();
|
|
147
|
-
success(`'${projectName}' undeployed`);
|
|
148
123
|
|
|
149
|
-
// Report resource
|
|
124
|
+
// Report any resource deletion failures
|
|
150
125
|
for (const resource of result.resources) {
|
|
151
|
-
if (resource.success) {
|
|
152
|
-
success(`Deleted ${resource.resource}`);
|
|
153
|
-
} else {
|
|
126
|
+
if (!resource.success) {
|
|
154
127
|
warn(`Failed to delete ${resource.resource}: ${resource.error}`);
|
|
155
128
|
}
|
|
156
129
|
}
|
|
157
130
|
|
|
131
|
+
// Final success message
|
|
158
132
|
console.error("");
|
|
159
|
-
success(`
|
|
133
|
+
success(`Undeployed '${projectName}'`);
|
|
134
|
+
if (exportPath) {
|
|
135
|
+
info(`Backup saved to ./${projectName}-backup.sql`);
|
|
136
|
+
}
|
|
160
137
|
console.error("");
|
|
161
138
|
return true;
|
|
162
139
|
} catch (err) {
|
|
@@ -34,6 +34,7 @@ interface PackageJson {
|
|
|
34
34
|
name?: string;
|
|
35
35
|
dependencies?: Record<string, string>;
|
|
36
36
|
devDependencies?: Record<string, string>;
|
|
37
|
+
workspaces?: string[] | { packages: string[] };
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const CONFIG_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
@@ -212,6 +213,16 @@ export function detectProjectType(projectPath: string): DetectionResult {
|
|
|
212
213
|
const detectedDeps: string[] = [];
|
|
213
214
|
const configFiles: string[] = [];
|
|
214
215
|
|
|
216
|
+
// Check for monorepo - user is in wrong directory
|
|
217
|
+
if (pkg?.workspaces) {
|
|
218
|
+
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
|
|
219
|
+
const hint = workspaces.length > 0 ? workspaces[0]?.replace("/*", "/your-app") : "apps/your-app";
|
|
220
|
+
return {
|
|
221
|
+
type: "unknown",
|
|
222
|
+
error: `This is a monorepo root, not a deployable project.\n\ncd into a package first:\n cd ${hint}\n jack ship`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
215
226
|
// Check for unsupported/coming-soon frameworks first (before reading package.json)
|
|
216
227
|
// This provides better error messages for frameworks we recognize but don't auto-deploy yet
|
|
217
228
|
const unsupported = detectUnsupportedFramework(projectPath, pkg);
|
|
@@ -363,6 +374,11 @@ function shouldExclude(relativePath: string): boolean {
|
|
|
363
374
|
return false;
|
|
364
375
|
}
|
|
365
376
|
|
|
377
|
+
// Derive skip directories from DEFAULT_EXCLUDES patterns (e.g., "node_modules/**" -> "node_modules")
|
|
378
|
+
const SKIP_DIRECTORIES = new Set(
|
|
379
|
+
DEFAULT_EXCLUDES.filter((p) => p.endsWith("/**")).map((p) => p.slice(0, -3)),
|
|
380
|
+
);
|
|
381
|
+
|
|
366
382
|
export async function validateProject(projectPath: string): Promise<ValidationResult> {
|
|
367
383
|
if (!existsSync(join(projectPath, "package.json"))) {
|
|
368
384
|
return {
|
|
@@ -374,37 +390,41 @@ export async function validateProject(projectPath: string): Promise<ValidationRe
|
|
|
374
390
|
let fileCount = 0;
|
|
375
391
|
let totalSizeBytes = 0;
|
|
376
392
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
withFileTypes: true,
|
|
381
|
-
});
|
|
393
|
+
// Walk directory tree manually to skip excluded dirs early (don't descend into node_modules)
|
|
394
|
+
async function walkDir(dirPath: string): Promise<void> {
|
|
395
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
382
396
|
|
|
383
397
|
for (const entry of entries) {
|
|
384
|
-
|
|
398
|
+
// Skip excluded directories entirely (don't descend)
|
|
399
|
+
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) {
|
|
385
400
|
continue;
|
|
386
401
|
}
|
|
387
402
|
|
|
388
|
-
const
|
|
389
|
-
const absolutePath = join(parentDir, entry.name);
|
|
403
|
+
const absolutePath = join(dirPath, entry.name);
|
|
390
404
|
const relativePath = relative(projectPath, absolutePath);
|
|
391
405
|
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
406
|
+
if (entry.isDirectory()) {
|
|
407
|
+
// Recursively walk subdirectories
|
|
408
|
+
await walkDir(absolutePath);
|
|
409
|
+
} else if (entry.isFile()) {
|
|
410
|
+
// Check glob-based exclusions for files
|
|
411
|
+
if (shouldExclude(relativePath)) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
395
414
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
error: `Project has more than ${MAX_FILES} files (excluding node_modules, .git, etc.)`,
|
|
401
|
-
fileCount,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
415
|
+
fileCount++;
|
|
416
|
+
if (fileCount > MAX_FILES) {
|
|
417
|
+
throw new Error(`Project has more than ${MAX_FILES} files (excluding node_modules, .git, etc.)`);
|
|
418
|
+
}
|
|
404
419
|
|
|
405
|
-
|
|
406
|
-
|
|
420
|
+
const stats = await stat(absolutePath);
|
|
421
|
+
totalSizeBytes += stats.size;
|
|
422
|
+
}
|
|
407
423
|
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
await walkDir(projectPath);
|
|
408
428
|
|
|
409
429
|
const totalSizeKb = Math.round(totalSizeBytes / 1024);
|
|
410
430
|
|
|
@@ -423,6 +443,13 @@ export async function validateProject(projectPath: string): Promise<ValidationRe
|
|
|
423
443
|
totalSizeKb,
|
|
424
444
|
};
|
|
425
445
|
} catch (err) {
|
|
446
|
+
if (err instanceof Error && err.message.includes("more than")) {
|
|
447
|
+
return {
|
|
448
|
+
valid: false,
|
|
449
|
+
error: err.message,
|
|
450
|
+
fileCount,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
426
453
|
return {
|
|
427
454
|
valid: false,
|
|
428
455
|
error: `Failed to scan project: ${err instanceof Error ? err.message : String(err)}`,
|