@getjack/jack 0.1.28 → 0.1.30
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/cd.ts +163 -0
- package/src/commands/clone.ts +112 -68
- package/src/commands/domain.ts +506 -0
- package/src/commands/domains.ts +215 -0
- package/src/commands/down.ts +18 -12
- package/src/commands/hack.ts +185 -8
- package/src/commands/init.ts +52 -1
- package/src/commands/link.ts +25 -43
- package/src/commands/logs.ts +2 -2
- package/src/commands/mcp.ts +74 -3
- package/src/commands/new.ts +48 -54
- package/src/commands/projects.ts +53 -10
- package/src/commands/secrets.ts +5 -1
- package/src/commands/services.ts +16 -4
- package/src/commands/shell-init.ts +43 -0
- package/src/commands/ship.ts +2 -11
- package/src/commands/skills.ts +335 -0
- package/src/commands/update.ts +31 -0
- package/src/commands/upgrade.ts +14 -0
- package/src/index.ts +116 -24
- package/src/lib/agent-integration.ts +1 -2
- package/src/lib/agents.ts +2 -2
- package/src/lib/auth/login-flow.ts +1 -1
- package/src/lib/clone-core.ts +252 -0
- package/src/lib/config.ts +22 -0
- package/src/lib/control-plane.ts +31 -5
- package/src/lib/fuzzy.ts +93 -0
- package/src/lib/managed-deploy.ts +4 -1
- package/src/lib/managed-down.ts +20 -5
- package/src/lib/output.ts +90 -9
- package/src/lib/picker.ts +406 -0
- package/src/lib/project-detection.ts +5 -2
- package/src/lib/project-list.ts +66 -5
- package/src/lib/project-operations.ts +68 -6
- package/src/lib/prompts.ts +1 -1
- package/src/lib/services/db-execute.ts +8 -1
- package/src/lib/services/db-list.ts +4 -1
- package/src/lib/services/domain-operations.ts +379 -0
- package/src/lib/services/storage-config.ts +1 -5
- package/src/lib/services/storage-delete.ts +1 -1
- package/src/lib/services/storage-info.ts +2 -4
- package/src/lib/services/vectorize-config.ts +1 -5
- package/src/lib/services/vectorize-create.ts +3 -1
- package/src/lib/shell-integration.ts +202 -0
- package/src/lib/telemetry-config.ts +50 -4
- package/src/lib/telemetry.ts +71 -2
- package/src/lib/version-check.ts +1 -3
- package/src/lib/wrangler-config.test.ts +2 -2
- package/src/lib/wrangler-config.ts +1 -1
- package/src/lib/zip-packager.ts +1 -3
- package/src/mcp/tools/index.ts +261 -7
- package/src/templates/index.ts +10 -1
- package/templates/ai-chat/.jack.json +1 -5
- package/templates/ai-chat/public/chat.js +130 -130
- package/templates/ai-chat/src/index.ts +9 -13
- package/templates/ai-chat/src/jack-ai.ts +6 -2
- package/templates/saas/.jack.json +6 -1
- package/templates/saas/src/auth.ts +8 -4
- package/templates/saas/src/client/App.tsx +22 -7
- package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
- package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
- package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
- package/templates/saas/src/client/components/ui/alert.tsx +2 -2
- package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
- package/templates/saas/src/client/components/ui/badge.tsx +2 -2
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
- package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
- package/templates/saas/src/client/components/ui/button.tsx +2 -2
- package/templates/saas/src/client/components/ui/card.tsx +1 -1
- package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
- package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
- package/templates/saas/src/client/components/ui/command.tsx +2 -2
- package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
- package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/empty.tsx +1 -1
- package/templates/saas/src/client/components/ui/field.tsx +2 -2
- package/templates/saas/src/client/components/ui/form.tsx +5 -5
- package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
- package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
- package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
- package/templates/saas/src/client/components/ui/input.tsx +1 -1
- package/templates/saas/src/client/components/ui/item.tsx +3 -3
- package/templates/saas/src/client/components/ui/label.tsx +1 -1
- package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
- package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
- package/templates/saas/src/client/components/ui/popover.tsx +1 -1
- package/templates/saas/src/client/components/ui/progress.tsx +1 -1
- package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
- package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
- package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
- package/templates/saas/src/client/components/ui/select.tsx +1 -1
- package/templates/saas/src/client/components/ui/separator.tsx +1 -1
- package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
- package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
- package/templates/saas/src/client/components/ui/slider.tsx +1 -1
- package/templates/saas/src/client/components/ui/switch.tsx +1 -1
- package/templates/saas/src/client/components/ui/table.tsx +1 -1
- package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
- package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
- package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
- package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
- package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
- package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
- package/templates/saas/src/client/lib/auth-client.ts +1 -1
- package/templates/saas/src/client/lib/plans.ts +1 -6
- package/templates/saas/src/client/lib/utils.ts +1 -1
- package/templates/saas/src/client/main.tsx +1 -1
- package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
- package/templates/saas/src/client/pages/HomePage.tsx +11 -2
- package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
- package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
- package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
- package/templates/saas/src/index.ts +28 -19
- package/templates/saas/vite.config.ts +1 -1
- package/templates/semantic-search/.jack.json +1 -5
- package/templates/semantic-search/src/index.ts +8 -4
- package/templates/semantic-search/src/jack-ai.ts +6 -2
- package/templates/semantic-search/src/jack-vectorize.ts +5 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone Core - reusable clone logic for CLI and other callers
|
|
3
|
+
*
|
|
4
|
+
* This module extracts the core clone functionality from the clone command
|
|
5
|
+
* so it can be reused by other commands (e.g., `jack cd` for auto-cloning).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { downloadProjectSource, fetchProjectTags } from "./control-plane.ts";
|
|
11
|
+
import { formatSize } from "./format.ts";
|
|
12
|
+
import { registerPath } from "./paths-index.ts";
|
|
13
|
+
import { linkProject, updateProjectLink } from "./project-link.ts";
|
|
14
|
+
import { resolveProject } from "./project-resolver.ts";
|
|
15
|
+
import { cloneFromCloud, getRemoteManifest } from "./storage/index.ts";
|
|
16
|
+
import { extractZipToDirectory } from "./zip-utils.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for the clone operation
|
|
20
|
+
*/
|
|
21
|
+
export interface CloneOptions {
|
|
22
|
+
/**
|
|
23
|
+
* When true, don't show spinners or progress output.
|
|
24
|
+
* Useful for programmatic calls or when called from other commands.
|
|
25
|
+
*/
|
|
26
|
+
silent?: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* When true, skip interactive prompts for collision handling.
|
|
30
|
+
* If target directory exists, throws an error instead of prompting.
|
|
31
|
+
*/
|
|
32
|
+
skipPrompts?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of a clone operation
|
|
37
|
+
*/
|
|
38
|
+
export interface CloneResult {
|
|
39
|
+
/** The final absolute path where the project was cloned */
|
|
40
|
+
path: string;
|
|
41
|
+
/** Number of files restored */
|
|
42
|
+
fileCount: number;
|
|
43
|
+
/** Whether this was a managed (Jack Cloud) or BYO project */
|
|
44
|
+
mode: "managed" | "byo";
|
|
45
|
+
/** Project ID if managed */
|
|
46
|
+
projectId?: string;
|
|
47
|
+
/** Number of tags restored (if any) */
|
|
48
|
+
tagsRestored?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error thrown when clone cannot proceed due to existing directory
|
|
53
|
+
*/
|
|
54
|
+
export class CloneCollisionError extends Error {
|
|
55
|
+
constructor(
|
|
56
|
+
public readonly targetDir: string,
|
|
57
|
+
public readonly displayName: string,
|
|
58
|
+
) {
|
|
59
|
+
super(`Directory ${displayName} already exists`);
|
|
60
|
+
this.name = "CloneCollisionError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Error thrown when project is not found
|
|
66
|
+
*/
|
|
67
|
+
export class ProjectNotFoundError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
public readonly projectName: string,
|
|
70
|
+
public readonly isByo: boolean,
|
|
71
|
+
) {
|
|
72
|
+
super(
|
|
73
|
+
isByo
|
|
74
|
+
? `Project not found: ${projectName}. For BYO projects, run 'jack sync' first to backup your project.`
|
|
75
|
+
: `Project not found: ${projectName}`,
|
|
76
|
+
);
|
|
77
|
+
this.name = "ProjectNotFoundError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reporter interface for clone progress (optional)
|
|
83
|
+
*/
|
|
84
|
+
export interface CloneReporter {
|
|
85
|
+
onLookup?: (projectName: string) => void;
|
|
86
|
+
onLookupComplete?: (found: boolean, isManaged: boolean) => void;
|
|
87
|
+
onDownloadStart?: (source: "cloud" | "r2", details?: string) => void;
|
|
88
|
+
onDownloadComplete?: (fileCount: number, displayPath: string) => void;
|
|
89
|
+
onDownloadError?: (error: string) => void;
|
|
90
|
+
onTagsRestored?: (count: number) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clone a project from Jack Cloud or User R2 storage.
|
|
95
|
+
*
|
|
96
|
+
* This is the core clone logic that can be called from:
|
|
97
|
+
* - `jack clone` command (full UX with prompts)
|
|
98
|
+
* - `jack cd` command (silent, auto-clone mode)
|
|
99
|
+
* - MCP tools (programmatic access)
|
|
100
|
+
*
|
|
101
|
+
* @param projectName - The project name/slug to clone
|
|
102
|
+
* @param targetDir - The target directory (can be relative, will be resolved to absolute)
|
|
103
|
+
* @param options - Clone options (silent, skipPrompts)
|
|
104
|
+
* @param reporter - Optional reporter for progress callbacks
|
|
105
|
+
* @returns CloneResult with the final path and details
|
|
106
|
+
* @throws CloneCollisionError if target exists and skipPrompts is true
|
|
107
|
+
* @throws ProjectNotFoundError if project not found
|
|
108
|
+
* @throws Error for other failures (download, extract, etc.)
|
|
109
|
+
*/
|
|
110
|
+
export async function cloneProject(
|
|
111
|
+
projectName: string,
|
|
112
|
+
targetDir: string,
|
|
113
|
+
options: CloneOptions = {},
|
|
114
|
+
reporter?: CloneReporter,
|
|
115
|
+
): Promise<CloneResult> {
|
|
116
|
+
const { silent = false, skipPrompts = false } = options;
|
|
117
|
+
const absoluteTargetDir = resolve(targetDir);
|
|
118
|
+
const displayName = targetDir.startsWith("/") ? targetDir : targetDir.replace(/^\.\//, "");
|
|
119
|
+
|
|
120
|
+
// Check if target directory exists
|
|
121
|
+
if (existsSync(absoluteTargetDir)) {
|
|
122
|
+
if (skipPrompts) {
|
|
123
|
+
throw new CloneCollisionError(absoluteTargetDir, displayName);
|
|
124
|
+
}
|
|
125
|
+
// If not skipping prompts, caller is responsible for handling collision
|
|
126
|
+
// (the clone.ts command handles this with interactive prompts)
|
|
127
|
+
throw new CloneCollisionError(absoluteTargetDir, displayName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Look up project
|
|
131
|
+
reporter?.onLookup?.(projectName);
|
|
132
|
+
|
|
133
|
+
let project: Awaited<ReturnType<typeof resolveProject>> = null;
|
|
134
|
+
try {
|
|
135
|
+
project = await resolveProject(projectName);
|
|
136
|
+
} catch {
|
|
137
|
+
// Not found on control-plane, will fall back to User R2
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
reporter?.onLookupComplete?.(
|
|
141
|
+
!!project,
|
|
142
|
+
!!(project?.sources.controlPlane && project?.remote?.projectId),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Managed mode: download from control-plane
|
|
146
|
+
if (project?.sources.controlPlane && project.remote?.projectId) {
|
|
147
|
+
reporter?.onDownloadStart?.("cloud");
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const sourceZip = await downloadProjectSource(projectName);
|
|
151
|
+
const fileCount = await extractZipToDirectory(sourceZip, absoluteTargetDir);
|
|
152
|
+
|
|
153
|
+
reporter?.onDownloadComplete?.(fileCount, `./${displayName}/`);
|
|
154
|
+
|
|
155
|
+
// Link to control-plane
|
|
156
|
+
await linkProject(absoluteTargetDir, project.remote.projectId, "managed");
|
|
157
|
+
await registerPath(project.remote.projectId, absoluteTargetDir);
|
|
158
|
+
|
|
159
|
+
// Fetch and restore tags from control plane
|
|
160
|
+
let tagsRestored = 0;
|
|
161
|
+
try {
|
|
162
|
+
const remoteTags = await fetchProjectTags(project.remote.projectId);
|
|
163
|
+
if (remoteTags.length > 0) {
|
|
164
|
+
await updateProjectLink(absoluteTargetDir, { tags: remoteTags });
|
|
165
|
+
tagsRestored = remoteTags.length;
|
|
166
|
+
reporter?.onTagsRestored?.(tagsRestored);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Silent fail - tag restoration is non-critical
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
path: absoluteTargetDir,
|
|
174
|
+
fileCount,
|
|
175
|
+
mode: "managed",
|
|
176
|
+
projectId: project.remote.projectId,
|
|
177
|
+
tagsRestored: tagsRestored > 0 ? tagsRestored : undefined,
|
|
178
|
+
};
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const message = err instanceof Error ? err.message : "Could not download project source";
|
|
181
|
+
reporter?.onDownloadError?.(message);
|
|
182
|
+
throw new Error(message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// BYO mode: use existing User R2 flow
|
|
187
|
+
const manifest = await getRemoteManifest(projectName);
|
|
188
|
+
|
|
189
|
+
if (!manifest) {
|
|
190
|
+
throw new ProjectNotFoundError(projectName, true);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Show file count and size in reporter
|
|
194
|
+
const totalSize = manifest.files.reduce((sum, f) => sum + f.size, 0);
|
|
195
|
+
reporter?.onDownloadStart?.("r2", `${manifest.files.length} file(s) (${formatSize(totalSize)})`);
|
|
196
|
+
|
|
197
|
+
// Download files
|
|
198
|
+
const result = await cloneFromCloud(projectName, absoluteTargetDir);
|
|
199
|
+
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
const errorMsg = result.error || "Could not download project files";
|
|
202
|
+
reporter?.onDownloadError?.(errorMsg);
|
|
203
|
+
throw new Error(errorMsg);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
reporter?.onDownloadComplete?.(result.filesDownloaded, `./${displayName}/`);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
path: absoluteTargetDir,
|
|
210
|
+
fileCount: result.filesDownloaded,
|
|
211
|
+
mode: "byo",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a project exists (either on Jack Cloud or User R2).
|
|
217
|
+
* Useful for pre-flight checks before cloning.
|
|
218
|
+
*
|
|
219
|
+
* @param projectName - The project name/slug to check
|
|
220
|
+
* @returns Object with exists flag and source info
|
|
221
|
+
*/
|
|
222
|
+
export async function checkProjectExists(
|
|
223
|
+
projectName: string,
|
|
224
|
+
): Promise<{ exists: boolean; isManaged: boolean; projectId?: string }> {
|
|
225
|
+
// Check Jack Cloud first
|
|
226
|
+
try {
|
|
227
|
+
const project = await resolveProject(projectName);
|
|
228
|
+
if (project?.sources.controlPlane && project.remote?.projectId) {
|
|
229
|
+
return {
|
|
230
|
+
exists: true,
|
|
231
|
+
isManaged: true,
|
|
232
|
+
projectId: project.remote.projectId,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// Not found on control-plane
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check User R2
|
|
240
|
+
const manifest = await getRemoteManifest(projectName);
|
|
241
|
+
if (manifest) {
|
|
242
|
+
return {
|
|
243
|
+
exists: true,
|
|
244
|
+
isManaged: false,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
exists: false,
|
|
250
|
+
isManaged: false,
|
|
251
|
+
};
|
|
252
|
+
}
|
package/src/lib/config.ts
CHANGED
|
@@ -44,6 +44,28 @@ const DEFAULT_CONFIG_DIR = join(homedir(), ".config", "jack");
|
|
|
44
44
|
export const CONFIG_DIR = process.env.JACK_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
45
45
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Default Jack home directory for projects
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_JACK_HOME = join(homedir(), ".jack", "projects");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the Jack home directory for projects.
|
|
54
|
+
* Supports environment variable override via JACK_HOME.
|
|
55
|
+
* @returns The resolved Jack home path (~ expanded to home directory)
|
|
56
|
+
*/
|
|
57
|
+
export function getJackHome(): string {
|
|
58
|
+
const envHome = process.env.JACK_HOME;
|
|
59
|
+
if (envHome) {
|
|
60
|
+
// Expand ~ to home directory if present
|
|
61
|
+
if (envHome.startsWith("~")) {
|
|
62
|
+
return join(homedir(), envHome.slice(1));
|
|
63
|
+
}
|
|
64
|
+
return envHome;
|
|
65
|
+
}
|
|
66
|
+
return DEFAULT_JACK_HOME;
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
/**
|
|
48
70
|
* Ensure config directory exists
|
|
49
71
|
*/
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -112,7 +112,11 @@ export async function createManagedProject(
|
|
|
112
112
|
requestBody.use_prebuilt = options.usePrebuilt;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
debug("Creating managed project", {
|
|
115
|
+
debug("Creating managed project", {
|
|
116
|
+
name,
|
|
117
|
+
template: options?.template,
|
|
118
|
+
usePrebuilt: options?.usePrebuilt,
|
|
119
|
+
});
|
|
116
120
|
const start = Date.now();
|
|
117
121
|
|
|
118
122
|
const response = await authFetch(`${getControlApiUrl()}/v1/projects`, {
|
|
@@ -234,9 +238,7 @@ export interface ManagedProject {
|
|
|
234
238
|
export async function getManagedDatabaseInfo(projectId: string): Promise<DatabaseInfoResponse> {
|
|
235
239
|
const { authFetch } = await import("./auth/index.ts");
|
|
236
240
|
|
|
237
|
-
const response = await authFetch(
|
|
238
|
-
`${getControlApiUrl()}/v1/projects/${projectId}/database/info`,
|
|
239
|
-
);
|
|
241
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/database/info`);
|
|
240
242
|
|
|
241
243
|
if (response.status === 404) {
|
|
242
244
|
throw new Error("No database found for this project");
|
|
@@ -365,6 +367,28 @@ export async function listManagedProjects(): Promise<ManagedProject[]> {
|
|
|
365
367
|
return data.projects;
|
|
366
368
|
}
|
|
367
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Find a managed project by ID.
|
|
372
|
+
*/
|
|
373
|
+
export async function findProjectById(projectId: string): Promise<ManagedProject | null> {
|
|
374
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
375
|
+
|
|
376
|
+
const response = await authFetch(
|
|
377
|
+
`${getControlApiUrl()}/v1/projects/${encodeURIComponent(projectId)}`,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (response.status === 404) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
return null; // Silent fail - just can't look up the name
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const data = (await response.json()) as { project: ManagedProject };
|
|
389
|
+
return data.project;
|
|
390
|
+
}
|
|
391
|
+
|
|
368
392
|
/**
|
|
369
393
|
* Find a managed project by slug.
|
|
370
394
|
*/
|
|
@@ -729,8 +753,10 @@ export async function publishProject(projectId: string): Promise<PublishProjectR
|
|
|
729
753
|
export async function downloadProjectSource(slug: string): Promise<Buffer> {
|
|
730
754
|
const { authFetch } = await import("./auth/index.ts");
|
|
731
755
|
|
|
756
|
+
// Use /me/projects endpoint to download your own project's source
|
|
757
|
+
// (works for both published and unpublished projects you own)
|
|
732
758
|
const response = await authFetch(
|
|
733
|
-
`${getControlApiUrl()}/v1/projects
|
|
759
|
+
`${getControlApiUrl()}/v1/me/projects/${encodeURIComponent(slug)}/source`,
|
|
734
760
|
);
|
|
735
761
|
|
|
736
762
|
if (!response.ok) {
|
package/src/lib/fuzzy.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple fuzzy matching for project filtering
|
|
3
|
+
*
|
|
4
|
+
* Scoring priority:
|
|
5
|
+
* 1. Exact prefix match (highest)
|
|
6
|
+
* 2. Substring match (medium)
|
|
7
|
+
* 3. Character-by-character fuzzy (lower)
|
|
8
|
+
*
|
|
9
|
+
* Returns 0 for no match.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SCORE_EXACT_PREFIX = 1000;
|
|
13
|
+
const SCORE_SUBSTRING = 500;
|
|
14
|
+
const SCORE_FUZZY_BASE = 100;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculate fuzzy match score between query and target.
|
|
18
|
+
* Higher score = better match. 0 = no match.
|
|
19
|
+
*/
|
|
20
|
+
export function fuzzyMatch(query: string, target: string): number {
|
|
21
|
+
if (!query || !target) return 0;
|
|
22
|
+
|
|
23
|
+
const q = query.toLowerCase();
|
|
24
|
+
const t = target.toLowerCase();
|
|
25
|
+
|
|
26
|
+
// Exact prefix match (highest priority)
|
|
27
|
+
if (t.startsWith(q)) {
|
|
28
|
+
// Bonus for exact match
|
|
29
|
+
if (t === q) return SCORE_EXACT_PREFIX + 100;
|
|
30
|
+
return SCORE_EXACT_PREFIX;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Substring match (medium priority)
|
|
34
|
+
if (t.includes(q)) {
|
|
35
|
+
return SCORE_SUBSTRING;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Character-by-character fuzzy match
|
|
39
|
+
// All query characters must appear in order in target
|
|
40
|
+
let targetIdx = 0;
|
|
41
|
+
let matchedChars = 0;
|
|
42
|
+
let consecutiveBonus = 0;
|
|
43
|
+
let lastMatchIdx = -2;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < q.length; i++) {
|
|
46
|
+
const char = q[i];
|
|
47
|
+
let found = false;
|
|
48
|
+
|
|
49
|
+
while (targetIdx < t.length) {
|
|
50
|
+
if (t[targetIdx] === char) {
|
|
51
|
+
matchedChars++;
|
|
52
|
+
// Bonus for consecutive matches
|
|
53
|
+
if (targetIdx === lastMatchIdx + 1) {
|
|
54
|
+
consecutiveBonus += 10;
|
|
55
|
+
}
|
|
56
|
+
lastMatchIdx = targetIdx;
|
|
57
|
+
targetIdx++;
|
|
58
|
+
found = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
targetIdx++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!found) {
|
|
65
|
+
return 0; // Query char not found, no match
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Base score plus bonuses
|
|
70
|
+
// More matched chars and consecutive matches = higher score
|
|
71
|
+
const lengthRatio = matchedChars / t.length;
|
|
72
|
+
return Math.floor(SCORE_FUZZY_BASE * lengthRatio + consecutiveBonus);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Filter and sort items by fuzzy match score.
|
|
77
|
+
* Returns only items with score > 0, sorted by score descending.
|
|
78
|
+
*/
|
|
79
|
+
export function fuzzyFilter<T>(query: string, items: T[], getText: (item: T) => string): T[] {
|
|
80
|
+
if (!query) return items;
|
|
81
|
+
|
|
82
|
+
const scored = items
|
|
83
|
+
.map((item) => ({
|
|
84
|
+
item,
|
|
85
|
+
score: fuzzyMatch(query, getText(item)),
|
|
86
|
+
}))
|
|
87
|
+
.filter(({ score }) => score > 0);
|
|
88
|
+
|
|
89
|
+
// Sort by score descending
|
|
90
|
+
scored.sort((a, b) => b.score - a.score);
|
|
91
|
+
|
|
92
|
+
return scored.map(({ item }) => item);
|
|
93
|
+
}
|
|
@@ -15,7 +15,7 @@ import { formatSize } from "./format.ts";
|
|
|
15
15
|
import { createFileCountProgress, createUploadProgress } from "./progress.ts";
|
|
16
16
|
import type { OperationReporter } from "./project-operations.ts";
|
|
17
17
|
import { getProjectTags } from "./tags.ts";
|
|
18
|
-
import { Events, track } from "./telemetry.ts";
|
|
18
|
+
import { Events, track, trackActivationIfFirst } from "./telemetry.ts";
|
|
19
19
|
import { packageForDeploy } from "./zip-packager.ts";
|
|
20
20
|
|
|
21
21
|
export interface ManagedCreateResult {
|
|
@@ -169,8 +169,11 @@ export async function deployCodeToManagedProject(
|
|
|
169
169
|
// Track success
|
|
170
170
|
track(Events.MANAGED_DEPLOY_COMPLETED, {
|
|
171
171
|
duration_ms: Date.now() - startTime,
|
|
172
|
+
project_id: projectId,
|
|
172
173
|
});
|
|
173
174
|
|
|
175
|
+
await trackActivationIfFirst("managed");
|
|
176
|
+
|
|
174
177
|
// Fire-and-forget tag sync (non-blocking)
|
|
175
178
|
getProjectTags(projectPath)
|
|
176
179
|
.then((tags) => {
|
package/src/lib/managed-down.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Mirrors BYO down.ts flow but uses control plane APIs.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { mkdirSync } from "node:fs";
|
|
6
7
|
import { writeFile } from "node:fs/promises";
|
|
7
8
|
import { join } from "node:path";
|
|
9
|
+
import { getJackHome } from "./config.ts";
|
|
8
10
|
import {
|
|
9
11
|
deleteManagedProject,
|
|
10
12
|
exportManagedDatabase,
|
|
11
13
|
fetchProjectResources,
|
|
14
|
+
getManagedDatabaseInfo,
|
|
12
15
|
} from "./control-plane.ts";
|
|
13
16
|
import { promptSelect } from "./hooks.ts";
|
|
14
17
|
import { error, info, item, output, success, warn } from "./output.ts";
|
|
@@ -20,6 +23,7 @@ export interface ManagedDownFlags {
|
|
|
20
23
|
export interface ManagedProjectInfo {
|
|
21
24
|
projectId: string;
|
|
22
25
|
runjackUrl: string | null;
|
|
26
|
+
localPath: string | null;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export async function managedDown(
|
|
@@ -54,12 +58,21 @@ export async function managedDown(
|
|
|
54
58
|
// Interactive mode - fetch actual resources
|
|
55
59
|
let hasDatabase = false;
|
|
56
60
|
let databaseName: string | null = null;
|
|
61
|
+
let databaseNumTables = 0;
|
|
57
62
|
try {
|
|
58
63
|
const resources = await fetchProjectResources(projectId);
|
|
59
64
|
const d1Resource = resources.find((r) => r.resource_type === "d1");
|
|
60
65
|
if (d1Resource) {
|
|
61
66
|
hasDatabase = true;
|
|
62
67
|
databaseName = d1Resource.resource_name;
|
|
68
|
+
// Fetch table count to determine if export is needed
|
|
69
|
+
try {
|
|
70
|
+
const dbInfo = await getManagedDatabaseInfo(projectId);
|
|
71
|
+
databaseNumTables = dbInfo.numTables;
|
|
72
|
+
} catch {
|
|
73
|
+
// If we can't get info, assume it has data to be safe
|
|
74
|
+
databaseNumTables = 1;
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
} catch {
|
|
65
78
|
// If fetch fails, assume no database (safer than showing wrong info)
|
|
@@ -89,11 +102,13 @@ export async function managedDown(
|
|
|
89
102
|
return false;
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
// Auto-export database if it
|
|
105
|
+
// Auto-export database if it has tables (skip empty databases)
|
|
93
106
|
let exportPath: string | null = null;
|
|
94
|
-
if (hasDatabase) {
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
if (hasDatabase && databaseNumTables > 0) {
|
|
108
|
+
const backupDir = project.localPath ?? join(getJackHome(), projectName);
|
|
109
|
+
mkdirSync(backupDir, { recursive: true });
|
|
110
|
+
exportPath = join(backupDir, `${projectName}-backup.sql`);
|
|
111
|
+
output.start("Exporting database...");
|
|
97
112
|
|
|
98
113
|
try {
|
|
99
114
|
const exportResult = await exportManagedDatabase(projectId);
|
|
@@ -132,7 +147,7 @@ export async function managedDown(
|
|
|
132
147
|
console.error("");
|
|
133
148
|
success(`Undeployed '${projectName}'`);
|
|
134
149
|
if (exportPath) {
|
|
135
|
-
info(`Backup saved to
|
|
150
|
+
info(`Backup saved to ${exportPath}`);
|
|
136
151
|
}
|
|
137
152
|
console.error("");
|
|
138
153
|
return true;
|
package/src/lib/output.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import yoctoSpinner from "yocto-spinner";
|
|
2
|
+
import type { OperationReporter } from "./project-operations.ts";
|
|
2
3
|
|
|
3
4
|
const isColorEnabled = !process.env.NO_COLOR && process.stderr.isTTY !== false;
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Quiet mode detection for AI agents and CI environments.
|
|
8
|
+
* Reduces token usage by simplifying output (no spinners, no decorative boxes).
|
|
9
|
+
*/
|
|
10
|
+
export const isQuietMode =
|
|
11
|
+
!process.stderr.isTTY ||
|
|
12
|
+
process.env.CI === "true" ||
|
|
13
|
+
process.env.CI === "1" ||
|
|
14
|
+
process.env.JACK_QUIET === "1";
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* ANSI color codes for terminal output
|
|
7
18
|
*/
|
|
@@ -22,12 +33,19 @@ let currentSpinner: ReturnType<typeof yoctoSpinner> | null = null;
|
|
|
22
33
|
*/
|
|
23
34
|
export const output = {
|
|
24
35
|
start(text: string) {
|
|
36
|
+
if (isQuietMode) {
|
|
37
|
+
console.error(text);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
25
40
|
if (currentSpinner) {
|
|
26
41
|
currentSpinner.stop();
|
|
27
42
|
}
|
|
28
43
|
currentSpinner = yoctoSpinner({ text }).start();
|
|
29
44
|
},
|
|
30
45
|
stop() {
|
|
46
|
+
if (isQuietMode) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
31
49
|
if (currentSpinner) {
|
|
32
50
|
currentSpinner.stop();
|
|
33
51
|
currentSpinner = null;
|
|
@@ -46,6 +64,26 @@ export const output = {
|
|
|
46
64
|
* Create a spinner for long-running operations
|
|
47
65
|
*/
|
|
48
66
|
export function spinner(text: string) {
|
|
67
|
+
if (isQuietMode) {
|
|
68
|
+
console.error(text);
|
|
69
|
+
let currentText = text;
|
|
70
|
+
return {
|
|
71
|
+
success(message: string) {
|
|
72
|
+
success(message);
|
|
73
|
+
},
|
|
74
|
+
error(message: string) {
|
|
75
|
+
error(message);
|
|
76
|
+
},
|
|
77
|
+
stop() {},
|
|
78
|
+
get text() {
|
|
79
|
+
return currentText;
|
|
80
|
+
},
|
|
81
|
+
set text(value: string | undefined) {
|
|
82
|
+
currentText = value ?? "";
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
49
87
|
const spin = yoctoSpinner({ text }).start();
|
|
50
88
|
|
|
51
89
|
return {
|
|
@@ -121,7 +159,15 @@ export function getRandomPurple(): string {
|
|
|
121
159
|
* Print a boxed message for important info (cyberpunk style)
|
|
122
160
|
*/
|
|
123
161
|
export function box(title: string, lines: string[]): void {
|
|
124
|
-
|
|
162
|
+
if (isQuietMode) {
|
|
163
|
+
return; // Skip decorative boxes in quiet mode
|
|
164
|
+
}
|
|
165
|
+
// Respect terminal width (leave room for box borders + indent)
|
|
166
|
+
const termWidth = process.stderr.columns || process.stdout.columns || 80;
|
|
167
|
+
const maxBoxWidth = Math.max(30, termWidth - 6); // 2 indent + 2 borders + 2 padding
|
|
168
|
+
|
|
169
|
+
const contentMaxLen = Math.max(title.length, ...lines.map((l) => l.length));
|
|
170
|
+
const maxLen = Math.min(contentMaxLen, maxBoxWidth - 4);
|
|
125
171
|
const innerWidth = maxLen + 4;
|
|
126
172
|
|
|
127
173
|
const purple = isColorEnabled ? getRandomPurple() : "";
|
|
@@ -132,10 +178,15 @@ export function box(title: string, lines: string[]): void {
|
|
|
132
178
|
const fill = "▓".repeat(innerWidth);
|
|
133
179
|
const gradient = "░".repeat(innerWidth);
|
|
134
180
|
|
|
181
|
+
// Truncate text if too long for box
|
|
182
|
+
const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
|
|
183
|
+
|
|
135
184
|
// Pad plain text first, then apply colors (ANSI codes break padEnd calculation)
|
|
136
|
-
const pad = (text: string) => ` ${text.padEnd(maxLen)} `;
|
|
137
|
-
const padTitle = (text: string) =>
|
|
138
|
-
|
|
185
|
+
const pad = (text: string) => ` ${truncate(text).padEnd(maxLen)} `;
|
|
186
|
+
const padTitle = (text: string) => {
|
|
187
|
+
const t = truncate(text);
|
|
188
|
+
return ` ${bold}${t}${reset}${purple}${" ".repeat(maxLen - t.length)} `;
|
|
189
|
+
};
|
|
139
190
|
|
|
140
191
|
console.error("");
|
|
141
192
|
console.error(` ${purple}╔${bar}╗${reset}`);
|
|
@@ -154,7 +205,15 @@ export function box(title: string, lines: string[]): void {
|
|
|
154
205
|
* Print a celebration box (for final success after setup)
|
|
155
206
|
*/
|
|
156
207
|
export function celebrate(title: string, lines: string[]): void {
|
|
157
|
-
|
|
208
|
+
if (isQuietMode) {
|
|
209
|
+
return; // Skip decorative boxes in quiet mode
|
|
210
|
+
}
|
|
211
|
+
// Respect terminal width (leave room for box borders + indent)
|
|
212
|
+
const termWidth = process.stderr.columns || process.stdout.columns || 80;
|
|
213
|
+
const maxBoxWidth = Math.max(30, termWidth - 6); // 2 indent + 2 borders + 2 padding
|
|
214
|
+
|
|
215
|
+
const contentMaxLen = Math.max(title.length, ...lines.map((l) => l.length));
|
|
216
|
+
const maxLen = Math.min(contentMaxLen, maxBoxWidth - 4);
|
|
158
217
|
const innerWidth = maxLen + 4;
|
|
159
218
|
|
|
160
219
|
const purple = isColorEnabled ? getRandomPurple() : "";
|
|
@@ -166,12 +225,16 @@ export function celebrate(title: string, lines: string[]): void {
|
|
|
166
225
|
const gradient = "░".repeat(innerWidth);
|
|
167
226
|
const space = " ".repeat(innerWidth);
|
|
168
227
|
|
|
228
|
+
// Truncate text if too long for box
|
|
229
|
+
const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
|
|
230
|
+
|
|
169
231
|
// Center text based on visual length, then apply colors
|
|
170
232
|
const center = (text: string, applyBold = false) => {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
233
|
+
const t = truncate(text);
|
|
234
|
+
const left = Math.floor((innerWidth - t.length) / 2);
|
|
235
|
+
const right = innerWidth - t.length - left;
|
|
236
|
+
const centered = " ".repeat(left) + t + " ".repeat(right);
|
|
237
|
+
return applyBold ? centered.replace(t, bold + t + reset + purple) : centered;
|
|
175
238
|
};
|
|
176
239
|
|
|
177
240
|
console.error("");
|
|
@@ -188,3 +251,21 @@ export function celebrate(title: string, lines: string[]): void {
|
|
|
188
251
|
console.error(` ${purple}╚${bar}╝${reset}`);
|
|
189
252
|
console.error("");
|
|
190
253
|
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Create a standard reporter object for project operations.
|
|
257
|
+
* Respects quiet mode for reduced output in CI/agent environments.
|
|
258
|
+
*/
|
|
259
|
+
export function createReporter(): OperationReporter {
|
|
260
|
+
return {
|
|
261
|
+
start: output.start,
|
|
262
|
+
stop: output.stop,
|
|
263
|
+
spinner,
|
|
264
|
+
info,
|
|
265
|
+
warn,
|
|
266
|
+
error,
|
|
267
|
+
success,
|
|
268
|
+
box,
|
|
269
|
+
celebrate,
|
|
270
|
+
};
|
|
271
|
+
}
|