@getjack/jack 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/README.md +0 -55
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
|
@@ -8,28 +8,46 @@
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
import { $ } from "bun";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
BUILTIN_TEMPLATES,
|
|
13
|
+
type ResolvedTemplate,
|
|
14
|
+
renderTemplate,
|
|
15
|
+
resolveTemplateWithOrigin,
|
|
16
|
+
} from "../templates/index.ts";
|
|
12
17
|
import type { Template } from "../templates/types.ts";
|
|
13
18
|
import { generateAgentFiles } from "./agent-files.ts";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
import {
|
|
20
|
+
getActiveAgents,
|
|
21
|
+
getAgentDefinition,
|
|
22
|
+
getOneShotAgent,
|
|
23
|
+
runAgentOneShot,
|
|
24
|
+
validateAgentPaths,
|
|
25
|
+
} from "./agents.ts";
|
|
26
|
+
import { needsViteBuild, runViteBuild } from "./build-helper.ts";
|
|
27
|
+
import { checkWorkerExists, getAccountId, listD1Databases } from "./cloudflare-api.ts";
|
|
17
28
|
import { getSyncConfig } from "./config.ts";
|
|
29
|
+
import { debug, isDebug } from "./debug.ts";
|
|
30
|
+
import { resolveDeployMode, validateModeAvailability } from "./deploy-mode.ts";
|
|
18
31
|
import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
|
|
19
32
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
20
33
|
import { type HookOutput, runHook } from "./hooks.ts";
|
|
34
|
+
import { loadTemplateKeywords, matchTemplateByIntent } from "./intent.ts";
|
|
35
|
+
import { registerLocalPath } from "./local-paths.ts";
|
|
36
|
+
import { createManagedProjectRemote, deployToManagedProject } from "./managed-deploy.ts";
|
|
21
37
|
import { generateProjectName } from "./names.ts";
|
|
22
38
|
import { filterNewSecrets, promptSaveSecrets } from "./prompts.ts";
|
|
39
|
+
import type { DeployMode, TemplateOrigin } from "./registry.ts";
|
|
23
40
|
import {
|
|
24
41
|
getAllProjects,
|
|
25
42
|
getProject,
|
|
26
|
-
getProjectDatabaseName,
|
|
27
43
|
registerProject,
|
|
28
44
|
removeProject,
|
|
45
|
+
updateProject,
|
|
29
46
|
} from "./registry.ts";
|
|
30
|
-
import { applySchema, getD1DatabaseName, hasD1Config } from "./schema.ts";
|
|
31
|
-
import { getSavedSecrets } from "./secrets.ts";
|
|
47
|
+
import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./schema.ts";
|
|
48
|
+
import { getSavedSecrets, saveSecrets } from "./secrets.ts";
|
|
32
49
|
import { getProjectNameFromDir, getRemoteManifest, syncToCloud } from "./storage/index.ts";
|
|
50
|
+
import { Events, track } from "./telemetry.ts";
|
|
33
51
|
|
|
34
52
|
// ============================================================================
|
|
35
53
|
// Type Definitions
|
|
@@ -37,14 +55,18 @@ import { getProjectNameFromDir, getRemoteManifest, syncToCloud } from "./storage
|
|
|
37
55
|
|
|
38
56
|
export interface CreateProjectOptions {
|
|
39
57
|
template?: string;
|
|
58
|
+
intent?: string;
|
|
40
59
|
reporter?: OperationReporter;
|
|
41
60
|
interactive?: boolean;
|
|
61
|
+
managed?: boolean; // Force managed deploy mode
|
|
62
|
+
byo?: boolean; // Force BYO deploy mode
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
export interface CreateProjectResult {
|
|
45
66
|
projectName: string;
|
|
46
67
|
targetDir: string;
|
|
47
68
|
workerUrl: string | null;
|
|
69
|
+
deployMode: DeployMode; // The deploy mode used
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
export interface DeployOptions {
|
|
@@ -53,12 +75,15 @@ export interface DeployOptions {
|
|
|
53
75
|
interactive?: boolean;
|
|
54
76
|
includeSecrets?: boolean;
|
|
55
77
|
includeSync?: boolean;
|
|
78
|
+
managed?: boolean; // Force managed deploy mode
|
|
79
|
+
byo?: boolean; // Force BYO deploy mode
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
export interface DeployResult {
|
|
59
83
|
workerUrl: string | null;
|
|
60
84
|
projectName: string;
|
|
61
85
|
deployOutput?: string;
|
|
86
|
+
deployMode: DeployMode; // The deploy mode used
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
export interface ProjectStatus {
|
|
@@ -80,7 +105,7 @@ export interface ProjectStatus {
|
|
|
80
105
|
|
|
81
106
|
export interface StaleProject {
|
|
82
107
|
name: string;
|
|
83
|
-
reason: "
|
|
108
|
+
reason: "worker not deployed";
|
|
84
109
|
workerUrl: string | null;
|
|
85
110
|
}
|
|
86
111
|
|
|
@@ -93,6 +118,7 @@ export interface OperationSpinner {
|
|
|
93
118
|
success(message: string): void;
|
|
94
119
|
error(message: string): void;
|
|
95
120
|
stop(): void;
|
|
121
|
+
text?: string;
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
export interface OperationReporter extends HookOutput {
|
|
@@ -120,6 +146,63 @@ const noopReporter: OperationReporter = {
|
|
|
120
146
|
box() {},
|
|
121
147
|
};
|
|
122
148
|
|
|
149
|
+
const DEFAULT_D1_LIMIT = 10;
|
|
150
|
+
|
|
151
|
+
async function preflightD1Capacity(
|
|
152
|
+
projectDir: string,
|
|
153
|
+
reporter: OperationReporter,
|
|
154
|
+
interactive: boolean,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const bindings = await getD1Bindings(projectDir);
|
|
157
|
+
const needsCreate = bindings.some((binding) => !binding.database_id && binding.database_name);
|
|
158
|
+
if (!needsCreate) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let databases: Array<{ name?: string; uuid?: string }>;
|
|
163
|
+
try {
|
|
164
|
+
databases = await listD1Databases();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
reporter.warn("Could not check D1 limits before deploy");
|
|
167
|
+
if (err instanceof Error) {
|
|
168
|
+
reporter.info(err.message);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const count = databases.length;
|
|
174
|
+
if (count < DEFAULT_D1_LIMIT) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
reporter.warn(`D1 limit likely reached: ${count} databases`);
|
|
179
|
+
reporter.info("Delete old D1 databases with: wrangler d1 list / wrangler d1 delete <name>");
|
|
180
|
+
reporter.info("Or reuse an existing database by setting database_id in wrangler.jsonc");
|
|
181
|
+
|
|
182
|
+
if (!interactive) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { promptSelect } = await import("./hooks.ts");
|
|
187
|
+
console.error("");
|
|
188
|
+
console.error(
|
|
189
|
+
` You have ${count} D1 databases. If your limit is ${DEFAULT_D1_LIMIT}, deploy may fail.`,
|
|
190
|
+
);
|
|
191
|
+
console.error("");
|
|
192
|
+
console.error(" Continue anyway?");
|
|
193
|
+
|
|
194
|
+
const choice = await promptSelect(["Yes", "No"]);
|
|
195
|
+
|
|
196
|
+
if (choice !== 0) {
|
|
197
|
+
throw new JackError(
|
|
198
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
199
|
+
"D1 limit likely reached",
|
|
200
|
+
"Delete old D1 databases or reuse an existing database_id",
|
|
201
|
+
{ exitCode: 0, reported: true },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
123
206
|
// ============================================================================
|
|
124
207
|
// Create Project Operation
|
|
125
208
|
// ============================================================================
|
|
@@ -138,11 +221,20 @@ export async function createProject(
|
|
|
138
221
|
name?: string,
|
|
139
222
|
options: CreateProjectOptions = {},
|
|
140
223
|
): Promise<CreateProjectResult> {
|
|
141
|
-
const {
|
|
142
|
-
|
|
224
|
+
const {
|
|
225
|
+
template: templateOption,
|
|
226
|
+
intent: intentPhrase,
|
|
227
|
+
reporter: providedReporter,
|
|
228
|
+
interactive: interactiveOption,
|
|
229
|
+
} = options;
|
|
143
230
|
const reporter = providedReporter ?? noopReporter;
|
|
144
231
|
const hasReporter = Boolean(providedReporter);
|
|
145
|
-
|
|
232
|
+
// CI mode: JACK_CI env or standard CI env
|
|
233
|
+
const isCi =
|
|
234
|
+
process.env.JACK_CI === "1" ||
|
|
235
|
+
process.env.JACK_CI === "true" ||
|
|
236
|
+
process.env.CI === "true" ||
|
|
237
|
+
process.env.CI === "1";
|
|
146
238
|
const interactive = interactiveOption ?? !isCi;
|
|
147
239
|
|
|
148
240
|
// Check if jack init was run (throws if not)
|
|
@@ -152,24 +244,195 @@ export async function createProject(
|
|
|
152
244
|
throw new JackError(JackErrorCode.VALIDATION_ERROR, "jack is not set up yet", "Run: jack init");
|
|
153
245
|
}
|
|
154
246
|
|
|
247
|
+
// Resolve deploy mode (omakase: logged in => managed, logged out => BYO)
|
|
248
|
+
const deployMode = await resolveDeployMode({
|
|
249
|
+
managed: options.managed,
|
|
250
|
+
byo: options.byo,
|
|
251
|
+
});
|
|
252
|
+
const modeError = await validateModeAvailability(deployMode);
|
|
253
|
+
if (modeError) {
|
|
254
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Close the "Starting..." spinner from new.ts
|
|
258
|
+
reporter.stop();
|
|
259
|
+
reporter.success("Initialized");
|
|
260
|
+
|
|
155
261
|
// Generate or use provided name
|
|
262
|
+
const nameWasProvided = name !== undefined;
|
|
156
263
|
const projectName = name ?? generateProjectName();
|
|
157
264
|
const targetDir = resolve(projectName);
|
|
158
265
|
|
|
159
266
|
// Check directory doesn't exist
|
|
160
267
|
if (existsSync(targetDir)) {
|
|
161
|
-
throw new JackError(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
268
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, `Directory ${projectName} already exists`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Early slug availability check for managed mode (only if user provided explicit name)
|
|
272
|
+
// Skip for auto-generated names - collision is rare, control plane will catch it anyway
|
|
273
|
+
if (deployMode === "managed" && nameWasProvided) {
|
|
274
|
+
reporter.start("Checking name availability...");
|
|
275
|
+
const { checkAvailability } = await import("./project-resolver.ts");
|
|
276
|
+
const { available, existingProject } = await checkAvailability(projectName);
|
|
277
|
+
reporter.stop();
|
|
278
|
+
if (available) {
|
|
279
|
+
reporter.success("Name available");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!available && existingProject) {
|
|
283
|
+
// Project exists remotely but not locally - offer to link
|
|
284
|
+
if (existingProject.sources.controlPlane && !existingProject.sources.filesystem) {
|
|
285
|
+
if (interactive) {
|
|
286
|
+
const { promptSelect } = await import("./hooks.ts");
|
|
287
|
+
console.error("");
|
|
288
|
+
console.error(` Project "${projectName}" exists on jack cloud but not locally.`);
|
|
289
|
+
console.error("");
|
|
290
|
+
|
|
291
|
+
const choice = await promptSelect(["Link existing project", "Choose different name"]);
|
|
292
|
+
|
|
293
|
+
if (choice === 0) {
|
|
294
|
+
// User chose to link - cache in registry and proceed
|
|
295
|
+
await registerProject(projectName, {
|
|
296
|
+
workerUrl: existingProject.url || null,
|
|
297
|
+
createdAt: existingProject.createdAt,
|
|
298
|
+
lastDeployed: existingProject.updatedAt || null,
|
|
299
|
+
status: existingProject.status === "live" ? "live" : "build_failed",
|
|
300
|
+
deploy_mode: "managed",
|
|
301
|
+
remote: existingProject.remote
|
|
302
|
+
? {
|
|
303
|
+
project_id: existingProject.remote.projectId,
|
|
304
|
+
project_slug: existingProject.slug,
|
|
305
|
+
org_id: existingProject.remote.orgId,
|
|
306
|
+
runjack_url:
|
|
307
|
+
existingProject.url || `https://${existingProject.slug}.runjack.xyz`,
|
|
308
|
+
}
|
|
309
|
+
: undefined,
|
|
310
|
+
});
|
|
311
|
+
reporter.success(`Linked to existing project: ${existingProject.url || projectName}`);
|
|
312
|
+
// Continue with project creation - user wants to link
|
|
313
|
+
} else {
|
|
314
|
+
// User chose different name
|
|
315
|
+
throw new JackError(
|
|
316
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
317
|
+
`Project "${projectName}" already exists on jack cloud`,
|
|
318
|
+
`Try a different name: jack new ${projectName}-2`,
|
|
319
|
+
{ exitCode: 0, reported: true },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Non-interactive mode - fail with clear message
|
|
324
|
+
throw new JackError(
|
|
325
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
326
|
+
`Project "${projectName}" already exists on jack cloud`,
|
|
327
|
+
`Try a different name: jack new ${projectName}-2`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
// Project exists in registry with local path - it's truly taken
|
|
332
|
+
throw new JackError(
|
|
333
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
334
|
+
`Project "${projectName}" already exists`,
|
|
335
|
+
`Try a different name: jack new ${projectName}-2`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
165
339
|
}
|
|
166
340
|
|
|
167
341
|
reporter.start("Creating project...");
|
|
168
342
|
|
|
169
|
-
//
|
|
343
|
+
// Intent-based template matching
|
|
344
|
+
let resolvedTemplate = templateOption;
|
|
345
|
+
|
|
346
|
+
if (intentPhrase && !templateOption) {
|
|
347
|
+
reporter.start("Matching intent to template...");
|
|
348
|
+
|
|
349
|
+
const templates = await loadTemplateKeywords();
|
|
350
|
+
const matches = matchTemplateByIntent(intentPhrase, templates);
|
|
351
|
+
|
|
352
|
+
reporter.stop();
|
|
353
|
+
|
|
354
|
+
if (matches.length === 0) {
|
|
355
|
+
// Track no match
|
|
356
|
+
track(Events.INTENT_NO_MATCH, {});
|
|
357
|
+
|
|
358
|
+
// No match - prompt user to choose
|
|
359
|
+
if (interactive) {
|
|
360
|
+
const { select } = await import("@clack/prompts");
|
|
361
|
+
console.error("");
|
|
362
|
+
console.error(` No template matched for: "${intentPhrase}"`);
|
|
363
|
+
console.error("");
|
|
364
|
+
|
|
365
|
+
const choice = await select({
|
|
366
|
+
message: "Select a template:",
|
|
367
|
+
options: BUILTIN_TEMPLATES.map((t, i) => ({ value: t, label: `${i + 1}. ${t}` })),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (typeof choice !== "string") {
|
|
371
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
|
|
372
|
+
exitCode: 0,
|
|
373
|
+
reported: true,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
resolvedTemplate = choice;
|
|
377
|
+
} else {
|
|
378
|
+
throw new JackError(
|
|
379
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
380
|
+
`No template matched intent: "${intentPhrase}"`,
|
|
381
|
+
`Available templates: ${BUILTIN_TEMPLATES.join(", ")}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
} else if (matches.length === 1) {
|
|
385
|
+
resolvedTemplate = matches[0]?.template;
|
|
386
|
+
reporter.success(`Matched template: ${resolvedTemplate}`);
|
|
387
|
+
|
|
388
|
+
// Track single match
|
|
389
|
+
track(Events.INTENT_MATCHED, {
|
|
390
|
+
template: resolvedTemplate,
|
|
391
|
+
match_count: 1,
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
// Track multiple matches
|
|
395
|
+
track(Events.INTENT_MATCHED, {
|
|
396
|
+
template: matches[0]?.template,
|
|
397
|
+
match_count: matches.length,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Multiple matches
|
|
401
|
+
if (interactive) {
|
|
402
|
+
const { select } = await import("@clack/prompts");
|
|
403
|
+
console.error("");
|
|
404
|
+
console.error(` Multiple templates matched: "${intentPhrase}"`);
|
|
405
|
+
console.error("");
|
|
406
|
+
|
|
407
|
+
const matchedNames = matches.map((m) => m.template);
|
|
408
|
+
const choice = await select({
|
|
409
|
+
message: "Select a template:",
|
|
410
|
+
options: matchedNames.map((t, i) => ({ value: t, label: `${i + 1}. ${t}` })),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (typeof choice !== "string") {
|
|
414
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
|
|
415
|
+
exitCode: 0,
|
|
416
|
+
reported: true,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
resolvedTemplate = choice;
|
|
420
|
+
} else {
|
|
421
|
+
resolvedTemplate = matches[0]?.template;
|
|
422
|
+
reporter.info(`Multiple matches, using: ${resolvedTemplate}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
reporter.start("Creating project...");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Load template with origin tracking for lineage
|
|
170
430
|
let template: Template;
|
|
431
|
+
let templateOrigin: TemplateOrigin;
|
|
171
432
|
try {
|
|
172
|
-
|
|
433
|
+
const resolved = await resolveTemplateWithOrigin(resolvedTemplate);
|
|
434
|
+
template = resolved.template;
|
|
435
|
+
templateOrigin = resolved.origin;
|
|
173
436
|
} catch (err) {
|
|
174
437
|
reporter.stop();
|
|
175
438
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -208,11 +471,75 @@ export async function createProject(
|
|
|
208
471
|
reporter.start("Creating project...");
|
|
209
472
|
}
|
|
210
473
|
|
|
474
|
+
// Handle optional secrets (only in interactive mode)
|
|
475
|
+
if (template.optionalSecrets?.length && interactive) {
|
|
476
|
+
const saved = await getSavedSecrets();
|
|
477
|
+
|
|
478
|
+
for (const optionalSecret of template.optionalSecrets) {
|
|
479
|
+
// Skip if already saved
|
|
480
|
+
const savedValue = saved[optionalSecret.name];
|
|
481
|
+
if (savedValue) {
|
|
482
|
+
secretsToUse[optionalSecret.name] = savedValue;
|
|
483
|
+
reporter.stop();
|
|
484
|
+
reporter.success(`Using saved secret: ${optionalSecret.name}`);
|
|
485
|
+
reporter.start("Creating project...");
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Prompt user
|
|
490
|
+
reporter.stop();
|
|
491
|
+
const { input, select } = await import("@inquirer/prompts");
|
|
492
|
+
console.error("");
|
|
493
|
+
console.error(` ${optionalSecret.description}`);
|
|
494
|
+
if (optionalSecret.setupUrl) {
|
|
495
|
+
console.error(` Setup: ${optionalSecret.setupUrl}`);
|
|
496
|
+
}
|
|
497
|
+
console.error("");
|
|
498
|
+
console.error(" Esc to skip\n");
|
|
499
|
+
|
|
500
|
+
const choice = await select({
|
|
501
|
+
message: `Add ${optionalSecret.name}?`,
|
|
502
|
+
choices: [
|
|
503
|
+
{ name: "1. Yes", value: "yes" },
|
|
504
|
+
{ name: "2. Skip", value: "skip" },
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (choice === "yes") {
|
|
509
|
+
const value = await input({
|
|
510
|
+
message: `Enter ${optionalSecret.name}:`,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (value.trim()) {
|
|
514
|
+
secretsToUse[optionalSecret.name] = value.trim();
|
|
515
|
+
// Save to global secrets for reuse
|
|
516
|
+
await saveSecrets([
|
|
517
|
+
{
|
|
518
|
+
key: optionalSecret.name,
|
|
519
|
+
value: value.trim(),
|
|
520
|
+
source: "optional-template",
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
reporter.success(`Saved ${optionalSecret.name}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
reporter.start("Creating project...");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
211
531
|
// Write all template files
|
|
212
532
|
for (const [filePath, content] of Object.entries(rendered.files)) {
|
|
213
533
|
await Bun.write(join(targetDir, filePath), content);
|
|
214
534
|
}
|
|
215
535
|
|
|
536
|
+
// Preflight: check D1 capacity before spending time on installs (BYO only)
|
|
537
|
+
reporter.stop();
|
|
538
|
+
if (deployMode === "byo") {
|
|
539
|
+
await preflightD1Capacity(targetDir, reporter, interactive);
|
|
540
|
+
}
|
|
541
|
+
reporter.start("Creating project...");
|
|
542
|
+
|
|
216
543
|
// Write secrets files (.env for Vite, .dev.vars for wrangler local, .secrets.json for wrangler bulk)
|
|
217
544
|
if (Object.keys(secretsToUse).length > 0) {
|
|
218
545
|
const envContent = generateEnvFile(secretsToUse);
|
|
@@ -309,113 +636,219 @@ export async function createProject(
|
|
|
309
636
|
}
|
|
310
637
|
}
|
|
311
638
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
639
|
+
// One-shot agent customization if intent was provided
|
|
640
|
+
if (intentPhrase) {
|
|
641
|
+
const oneShotAgent = await getOneShotAgent();
|
|
642
|
+
|
|
643
|
+
if (oneShotAgent) {
|
|
644
|
+
const agentDefinition = getAgentDefinition(oneShotAgent);
|
|
645
|
+
const agentLabel = agentDefinition?.name ?? oneShotAgent;
|
|
646
|
+
reporter.info(`Customizing with ${agentLabel}`);
|
|
647
|
+
reporter.info(`Intent: ${intentPhrase}`);
|
|
648
|
+
const debugEnabled = isDebug();
|
|
649
|
+
const customizationSpinner = debugEnabled ? null : reporter.spinner("Customizing...");
|
|
650
|
+
|
|
651
|
+
// Track customization start
|
|
652
|
+
track(Events.INTENT_CUSTOMIZATION_STARTED, { agent: oneShotAgent });
|
|
653
|
+
|
|
654
|
+
const result = await runAgentOneShot(oneShotAgent, targetDir, intentPhrase, {
|
|
655
|
+
info: reporter.info,
|
|
656
|
+
warn: reporter.warn,
|
|
657
|
+
status: customizationSpinner
|
|
658
|
+
? (message) => {
|
|
659
|
+
customizationSpinner.text = message;
|
|
660
|
+
}
|
|
661
|
+
: undefined,
|
|
662
|
+
});
|
|
316
663
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
664
|
+
if (customizationSpinner) {
|
|
665
|
+
customizationSpinner.stop();
|
|
666
|
+
}
|
|
667
|
+
if (result.success) {
|
|
668
|
+
reporter.success("Project customized");
|
|
669
|
+
// Track successful customization
|
|
670
|
+
track(Events.INTENT_CUSTOMIZATION_COMPLETED, { agent: oneShotAgent });
|
|
671
|
+
} else {
|
|
672
|
+
reporter.warn(`Customization skipped: ${result.error ?? "unknown error"}`);
|
|
673
|
+
// Track failed customization
|
|
674
|
+
track(Events.INTENT_CUSTOMIZATION_FAILED, {
|
|
675
|
+
agent: oneShotAgent,
|
|
676
|
+
error_type: "agent_error",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
reporter.info?.("No compatible agent for customization (Claude Code or Codex required)");
|
|
327
681
|
}
|
|
328
|
-
|
|
329
|
-
reporter.stop();
|
|
330
|
-
reporter.success("Built");
|
|
331
682
|
}
|
|
332
683
|
|
|
333
|
-
|
|
334
|
-
reporter.start("Deploying...");
|
|
684
|
+
let workerUrl: string | null = null;
|
|
335
685
|
|
|
336
|
-
|
|
686
|
+
// Deploy based on mode
|
|
687
|
+
if (deployMode === "managed") {
|
|
688
|
+
// Managed mode: create project and deploy via jack cloud
|
|
689
|
+
const remoteResult = await createManagedProjectRemote(projectName, reporter, {
|
|
690
|
+
template: resolvedTemplate || "hello",
|
|
691
|
+
usePrebuilt: true,
|
|
692
|
+
});
|
|
337
693
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
694
|
+
// Register project as soon as remote is created
|
|
695
|
+
try {
|
|
696
|
+
await registerProject(projectName, {
|
|
697
|
+
workerUrl: remoteResult.runjackUrl,
|
|
698
|
+
createdAt: new Date().toISOString(),
|
|
699
|
+
lastDeployed: remoteResult.status === "live" ? new Date().toISOString() : null,
|
|
700
|
+
status: remoteResult.status === "live" ? "live" : "created",
|
|
701
|
+
template: templateOrigin,
|
|
702
|
+
deploy_mode: "managed",
|
|
703
|
+
remote: {
|
|
704
|
+
project_id: remoteResult.projectId,
|
|
705
|
+
project_slug: remoteResult.projectSlug,
|
|
706
|
+
org_id: remoteResult.orgId,
|
|
707
|
+
runjack_url: remoteResult.runjackUrl,
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
} catch (err) {
|
|
711
|
+
debug("Failed to register managed project:", err);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Check if prebuilt deployment succeeded
|
|
715
|
+
if (remoteResult.status === "live") {
|
|
716
|
+
// Prebuilt succeeded - skip the fresh build
|
|
717
|
+
workerUrl = remoteResult.runjackUrl;
|
|
718
|
+
reporter.success(`Deployed: ${workerUrl}`);
|
|
719
|
+
} else {
|
|
720
|
+
// Prebuilt not available - fall back to fresh build
|
|
721
|
+
if (remoteResult.prebuiltFailed) {
|
|
722
|
+
// Show debug info about why prebuilt failed
|
|
723
|
+
const errorDetail = remoteResult.prebuiltError ? ` (${remoteResult.prebuiltError})` : "";
|
|
724
|
+
debug(`Prebuilt failed${errorDetail}`);
|
|
725
|
+
reporter.info("Pre-built not available, building fresh...");
|
|
726
|
+
}
|
|
348
727
|
|
|
349
|
-
// Apply schema.sql after deploy
|
|
350
|
-
if (await hasD1Config(targetDir)) {
|
|
351
|
-
const dbName = await getD1DatabaseName(targetDir);
|
|
352
|
-
if (dbName) {
|
|
353
728
|
try {
|
|
354
|
-
await
|
|
729
|
+
await deployToManagedProject(remoteResult.projectId, targetDir, reporter);
|
|
355
730
|
} catch (err) {
|
|
356
|
-
|
|
357
|
-
|
|
731
|
+
try {
|
|
732
|
+
await updateProject(projectName, {
|
|
733
|
+
status: "build_failed",
|
|
734
|
+
workerUrl: remoteResult.runjackUrl,
|
|
735
|
+
});
|
|
736
|
+
} catch (updateErr) {
|
|
737
|
+
debug("Failed to update managed project status:", updateErr);
|
|
738
|
+
}
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
workerUrl = remoteResult.runjackUrl;
|
|
742
|
+
reporter.success(`Created: ${workerUrl}`);
|
|
743
|
+
|
|
744
|
+
// Update project status to live after successful fresh build
|
|
745
|
+
try {
|
|
746
|
+
await updateProject(projectName, {
|
|
747
|
+
lastDeployed: new Date().toISOString(),
|
|
748
|
+
status: "live",
|
|
749
|
+
});
|
|
750
|
+
} catch (err) {
|
|
751
|
+
// Log but don't fail - registry is convenience, not critical path
|
|
752
|
+
debug("Failed to update managed project status:", err);
|
|
358
753
|
}
|
|
359
754
|
}
|
|
360
|
-
}
|
|
755
|
+
} else {
|
|
756
|
+
// BYO mode: deploy via wrangler
|
|
361
757
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
758
|
+
// Build first if needed (wrangler needs dist/ for assets)
|
|
759
|
+
if (await needsViteBuild(targetDir)) {
|
|
760
|
+
reporter.start("Building...");
|
|
761
|
+
try {
|
|
762
|
+
await runViteBuild(targetDir);
|
|
763
|
+
reporter.stop();
|
|
764
|
+
reporter.success("Built");
|
|
765
|
+
} catch (err) {
|
|
766
|
+
reporter.stop();
|
|
767
|
+
reporter.error("Build failed");
|
|
768
|
+
throw err;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
366
771
|
|
|
367
|
-
|
|
368
|
-
.cwd(targetDir)
|
|
369
|
-
.nothrow()
|
|
370
|
-
.quiet();
|
|
772
|
+
reporter.start("Deploying...");
|
|
371
773
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
reporter.info("Run manually: wrangler secret bulk .secrets.json");
|
|
376
|
-
} else {
|
|
774
|
+
const deployResult = await $`wrangler deploy`.cwd(targetDir).nothrow().quiet();
|
|
775
|
+
|
|
776
|
+
if (deployResult.exitCode !== 0) {
|
|
377
777
|
reporter.stop();
|
|
378
|
-
reporter.
|
|
778
|
+
reporter.error("Deploy failed");
|
|
779
|
+
throw new JackError(JackErrorCode.DEPLOY_FAILED, "Deploy failed", undefined, {
|
|
780
|
+
exitCode: 0,
|
|
781
|
+
stderr: deployResult.stderr.toString(),
|
|
782
|
+
reported: hasReporter,
|
|
783
|
+
});
|
|
379
784
|
}
|
|
380
|
-
}
|
|
381
785
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
786
|
+
// Apply schema.sql after deploy
|
|
787
|
+
if (await hasD1Config(targetDir)) {
|
|
788
|
+
const dbName = await getD1DatabaseName(targetDir);
|
|
789
|
+
if (dbName) {
|
|
790
|
+
try {
|
|
791
|
+
await applySchema(dbName, targetDir);
|
|
792
|
+
} catch (err) {
|
|
793
|
+
reporter.warn(`Schema application failed: ${err}`);
|
|
794
|
+
reporter.info("Run manually: bun run db:migrate");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
386
798
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
reporter.success("Deployed");
|
|
392
|
-
}
|
|
799
|
+
// Push secrets to Cloudflare
|
|
800
|
+
const secretsJsonPath = join(targetDir, ".secrets.json");
|
|
801
|
+
if (existsSync(secretsJsonPath)) {
|
|
802
|
+
reporter.start("Configuring secrets...");
|
|
393
803
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
804
|
+
const secretsResult = await $`wrangler secret bulk .secrets.json`
|
|
805
|
+
.cwd(targetDir)
|
|
806
|
+
.nothrow()
|
|
807
|
+
.quiet();
|
|
398
808
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
809
|
+
if (secretsResult.exitCode !== 0) {
|
|
810
|
+
reporter.stop();
|
|
811
|
+
reporter.warn("Failed to push secrets to Cloudflare");
|
|
812
|
+
reporter.info("Run manually: wrangler secret bulk .secrets.json");
|
|
813
|
+
} else {
|
|
814
|
+
reporter.stop();
|
|
815
|
+
reporter.success("Secrets configured");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Parse URL from output
|
|
820
|
+
const deployOutput = deployResult.stdout.toString();
|
|
821
|
+
const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
|
|
822
|
+
workerUrl = urlMatch ? urlMatch[0] : null;
|
|
823
|
+
|
|
824
|
+
reporter.stop();
|
|
825
|
+
if (workerUrl) {
|
|
826
|
+
reporter.success(`Live: ${workerUrl}`);
|
|
827
|
+
} else {
|
|
828
|
+
reporter.success("Deployed");
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Register project with BYO mode
|
|
832
|
+
try {
|
|
833
|
+
const accountId = await getAccountId();
|
|
834
|
+
|
|
835
|
+
await registerProject(projectName, {
|
|
836
|
+
workerUrl,
|
|
837
|
+
createdAt: new Date().toISOString(),
|
|
838
|
+
lastDeployed: workerUrl ? new Date().toISOString() : null,
|
|
839
|
+
cloudflare: {
|
|
840
|
+
accountId,
|
|
841
|
+
workerId: projectName,
|
|
411
842
|
},
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
843
|
+
template: templateOrigin,
|
|
844
|
+
deploy_mode: "byo",
|
|
845
|
+
});
|
|
846
|
+
} catch {
|
|
847
|
+
// Don't fail the creation if registry update fails
|
|
848
|
+
}
|
|
416
849
|
}
|
|
417
850
|
|
|
418
|
-
// Run post-deploy hooks
|
|
851
|
+
// Run post-deploy hooks (for both modes)
|
|
419
852
|
if (template.hooks?.postDeploy?.length && workerUrl) {
|
|
420
853
|
const domain = workerUrl.replace(/^https?:\/\//, "");
|
|
421
854
|
await runHook(
|
|
@@ -430,10 +863,18 @@ export async function createProject(
|
|
|
430
863
|
);
|
|
431
864
|
}
|
|
432
865
|
|
|
866
|
+
// Auto-register local path for project discovery
|
|
867
|
+
try {
|
|
868
|
+
await registerLocalPath(projectName, targetDir);
|
|
869
|
+
} catch {
|
|
870
|
+
// Silent fail - registration is best-effort
|
|
871
|
+
}
|
|
872
|
+
|
|
433
873
|
return {
|
|
434
874
|
projectName,
|
|
435
875
|
targetDir,
|
|
436
876
|
workerUrl,
|
|
877
|
+
deployMode,
|
|
437
878
|
};
|
|
438
879
|
}
|
|
439
880
|
|
|
@@ -460,7 +901,12 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
460
901
|
} = options;
|
|
461
902
|
const reporter = providedReporter ?? noopReporter;
|
|
462
903
|
const hasReporter = Boolean(providedReporter);
|
|
463
|
-
|
|
904
|
+
// CI mode: JACK_CI env or standard CI env
|
|
905
|
+
const isCi =
|
|
906
|
+
process.env.JACK_CI === "1" ||
|
|
907
|
+
process.env.JACK_CI === "true" ||
|
|
908
|
+
process.env.CI === "true" ||
|
|
909
|
+
process.env.CI === "1";
|
|
464
910
|
const interactive = interactiveOption ?? !isCi;
|
|
465
911
|
|
|
466
912
|
// Check for wrangler config
|
|
@@ -477,55 +923,93 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
477
923
|
);
|
|
478
924
|
}
|
|
479
925
|
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
existsSync(join(projectPath, "vite.config.ts")) ||
|
|
483
|
-
existsSync(join(projectPath, "vite.config.js")) ||
|
|
484
|
-
existsSync(join(projectPath, "vite.config.mjs"));
|
|
926
|
+
// Get project name from directory
|
|
927
|
+
const projectName = await getProjectNameFromDir(projectPath);
|
|
485
928
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const buildResult = await $`npx vite build`.cwd(projectPath).nothrow().quiet();
|
|
929
|
+
// Get project from registry to check stored mode
|
|
930
|
+
const project = await getProject(projectName);
|
|
489
931
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
buildSpin.success("Built");
|
|
932
|
+
// Determine effective mode: explicit flag > stored mode > default BYO
|
|
933
|
+
let deployMode: DeployMode;
|
|
934
|
+
if (options.managed) {
|
|
935
|
+
deployMode = "managed";
|
|
936
|
+
} else if (options.byo) {
|
|
937
|
+
deployMode = "byo";
|
|
938
|
+
} else {
|
|
939
|
+
deployMode = project?.deploy_mode ?? "byo";
|
|
499
940
|
}
|
|
500
941
|
|
|
501
|
-
//
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (result.exitCode !== 0) {
|
|
506
|
-
spin.error("Deploy failed");
|
|
507
|
-
throw new JackError(JackErrorCode.DEPLOY_FAILED, "Deploy failed", undefined, {
|
|
508
|
-
exitCode: result.exitCode ?? 1,
|
|
509
|
-
stderr: result.stderr.toString(),
|
|
510
|
-
reported: hasReporter,
|
|
511
|
-
});
|
|
942
|
+
// Validate mode availability
|
|
943
|
+
const modeError = await validateModeAvailability(deployMode);
|
|
944
|
+
if (modeError) {
|
|
945
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
|
|
512
946
|
}
|
|
513
947
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
|
|
517
|
-
const workerUrl = urlMatch ? urlMatch[0] : null;
|
|
518
|
-
const projectName = await getProjectNameFromDir(projectPath);
|
|
948
|
+
let workerUrl: string | null = null;
|
|
949
|
+
let deployOutput: string | undefined;
|
|
519
950
|
|
|
520
|
-
|
|
521
|
-
|
|
951
|
+
// Deploy based on mode
|
|
952
|
+
if (deployMode === "managed") {
|
|
953
|
+
// Managed mode: deploy via jack cloud
|
|
954
|
+
if (!project?.remote?.project_id) {
|
|
955
|
+
throw new JackError(
|
|
956
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
957
|
+
"Project not linked to jack cloud",
|
|
958
|
+
"Create a new managed project or use --byo",
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// deployToManagedProject now handles both template and code deploy
|
|
963
|
+
const result = await deployToManagedProject(project.remote.project_id, projectPath, reporter);
|
|
964
|
+
|
|
965
|
+
workerUrl = project.remote.runjack_url;
|
|
966
|
+
|
|
967
|
+
// Update lastDeployed in registry (will be persisted below)
|
|
968
|
+
if (project) {
|
|
969
|
+
project.lastDeployed = new Date().toISOString();
|
|
970
|
+
}
|
|
522
971
|
} else {
|
|
523
|
-
|
|
972
|
+
// BYO mode: deploy via wrangler
|
|
973
|
+
|
|
974
|
+
// Build first if needed (wrangler needs dist/ for assets)
|
|
975
|
+
if (await needsViteBuild(projectPath)) {
|
|
976
|
+
const buildSpin = reporter.spinner("Building...");
|
|
977
|
+
try {
|
|
978
|
+
await runViteBuild(projectPath);
|
|
979
|
+
buildSpin.success("Built");
|
|
980
|
+
} catch (err) {
|
|
981
|
+
buildSpin.error("Build failed");
|
|
982
|
+
throw err;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const spin = reporter.spinner("Deploying...");
|
|
987
|
+
const result = await $`wrangler deploy`.cwd(projectPath).nothrow().quiet();
|
|
988
|
+
|
|
989
|
+
if (result.exitCode !== 0) {
|
|
990
|
+
spin.error("Deploy failed");
|
|
991
|
+
throw new JackError(JackErrorCode.DEPLOY_FAILED, "Deploy failed", undefined, {
|
|
992
|
+
exitCode: result.exitCode ?? 1,
|
|
993
|
+
stderr: result.stderr.toString(),
|
|
994
|
+
reported: hasReporter,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Parse URL from output
|
|
999
|
+
deployOutput = result.stdout.toString();
|
|
1000
|
+
const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
|
|
1001
|
+
workerUrl = urlMatch ? urlMatch[0] : null;
|
|
1002
|
+
|
|
1003
|
+
if (workerUrl) {
|
|
1004
|
+
spin.success(`Live: ${workerUrl}`);
|
|
1005
|
+
} else {
|
|
1006
|
+
spin.success("Deployed");
|
|
1007
|
+
}
|
|
524
1008
|
}
|
|
525
1009
|
|
|
526
|
-
// Apply schema if needed
|
|
1010
|
+
// Apply schema if needed (BYO only - managed projects have their own DB)
|
|
527
1011
|
let dbName: string | null = null;
|
|
528
|
-
if (await hasD1Config(projectPath)) {
|
|
1012
|
+
if (deployMode === "byo" && (await hasD1Config(projectPath))) {
|
|
529
1013
|
dbName = await getD1DatabaseName(projectPath);
|
|
530
1014
|
if (dbName) {
|
|
531
1015
|
try {
|
|
@@ -540,14 +1024,8 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
540
1024
|
// Update registry
|
|
541
1025
|
try {
|
|
542
1026
|
await registerProject(projectName, {
|
|
543
|
-
localPath: projectPath,
|
|
544
1027
|
workerUrl,
|
|
545
1028
|
lastDeployed: new Date().toISOString(),
|
|
546
|
-
resources: {
|
|
547
|
-
services: {
|
|
548
|
-
db: dbName,
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
1029
|
});
|
|
552
1030
|
} catch {
|
|
553
1031
|
// Don't fail the deploy if registry update fails
|
|
@@ -585,10 +1063,18 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
585
1063
|
}
|
|
586
1064
|
}
|
|
587
1065
|
|
|
1066
|
+
// Auto-register local path for project discovery
|
|
1067
|
+
try {
|
|
1068
|
+
await registerLocalPath(projectName, projectPath);
|
|
1069
|
+
} catch {
|
|
1070
|
+
// Silent fail - registration is best-effort
|
|
1071
|
+
}
|
|
1072
|
+
|
|
588
1073
|
return {
|
|
589
1074
|
workerUrl,
|
|
590
1075
|
projectName,
|
|
591
1076
|
deployOutput: workerUrl ? undefined : deployOutput,
|
|
1077
|
+
deployMode,
|
|
592
1078
|
};
|
|
593
1079
|
}
|
|
594
1080
|
|
|
@@ -602,7 +1088,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
602
1088
|
* Extracted from commands/projects.ts infoProject to enable programmatic status checks.
|
|
603
1089
|
*
|
|
604
1090
|
* @param name - Project name (auto-detected from cwd if not provided)
|
|
605
|
-
* @param projectPath - Project path (defaults to cwd)
|
|
1091
|
+
* @param projectPath - Project path (defaults to cwd, used for local status checks)
|
|
606
1092
|
* @returns Project status or null if not found
|
|
607
1093
|
*/
|
|
608
1094
|
export async function getProjectStatus(
|
|
@@ -610,11 +1096,12 @@ export async function getProjectStatus(
|
|
|
610
1096
|
projectPath?: string,
|
|
611
1097
|
): Promise<ProjectStatus | null> {
|
|
612
1098
|
let projectName = name;
|
|
1099
|
+
const resolvedPath = projectPath ?? process.cwd();
|
|
613
1100
|
|
|
614
1101
|
// If no name provided, try to get from project path or cwd
|
|
615
1102
|
if (!projectName) {
|
|
616
1103
|
try {
|
|
617
|
-
projectName = await getProjectNameFromDir(
|
|
1104
|
+
projectName = await getProjectNameFromDir(resolvedPath);
|
|
618
1105
|
} catch {
|
|
619
1106
|
// Could not determine project name
|
|
620
1107
|
return null;
|
|
@@ -627,8 +1114,15 @@ export async function getProjectStatus(
|
|
|
627
1114
|
return null;
|
|
628
1115
|
}
|
|
629
1116
|
|
|
630
|
-
// Check
|
|
631
|
-
const
|
|
1117
|
+
// Check if local project exists at the resolved path
|
|
1118
|
+
const hasWranglerConfig =
|
|
1119
|
+
existsSync(join(resolvedPath, "wrangler.jsonc")) ||
|
|
1120
|
+
existsSync(join(resolvedPath, "wrangler.toml")) ||
|
|
1121
|
+
existsSync(join(resolvedPath, "wrangler.json"));
|
|
1122
|
+
const localExists = hasWranglerConfig;
|
|
1123
|
+
const localPath = localExists ? resolvedPath : null;
|
|
1124
|
+
|
|
1125
|
+
// Check actual deployment status
|
|
632
1126
|
const [workerExists, manifest] = await Promise.all([
|
|
633
1127
|
checkWorkerExists(projectName),
|
|
634
1128
|
getRemoteManifest(projectName),
|
|
@@ -637,113 +1131,54 @@ export async function getProjectStatus(
|
|
|
637
1131
|
const backupFiles = manifest ? manifest.files.length : null;
|
|
638
1132
|
const backupLastSync = manifest ? manifest.lastSync : null;
|
|
639
1133
|
|
|
1134
|
+
// Get database name on-demand
|
|
1135
|
+
let dbName: string | null = null;
|
|
1136
|
+
if (project.deploy_mode === "managed" && project.remote?.project_id) {
|
|
1137
|
+
// For managed projects, fetch from control plane
|
|
1138
|
+
try {
|
|
1139
|
+
const { fetchProjectResources } = await import("./control-plane.ts");
|
|
1140
|
+
const resources = await fetchProjectResources(project.remote.project_id);
|
|
1141
|
+
const d1 = resources.find((r) => r.resource_type === "d1");
|
|
1142
|
+
dbName = d1?.resource_name || null;
|
|
1143
|
+
} catch {
|
|
1144
|
+
// Ignore errors, dbName stays null
|
|
1145
|
+
}
|
|
1146
|
+
} else if (localExists) {
|
|
1147
|
+
// For BYO, parse from wrangler config
|
|
1148
|
+
try {
|
|
1149
|
+
const { parseWranglerResources } = await import("./resources.ts");
|
|
1150
|
+
const resources = await parseWranglerResources(resolvedPath);
|
|
1151
|
+
dbName = resources.d1?.name || null;
|
|
1152
|
+
} catch {
|
|
1153
|
+
// Ignore errors, dbName stays null
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
640
1157
|
return {
|
|
641
1158
|
name: projectName,
|
|
642
|
-
localPath
|
|
1159
|
+
localPath,
|
|
643
1160
|
workerUrl: project.workerUrl,
|
|
644
1161
|
lastDeployed: project.lastDeployed,
|
|
645
1162
|
createdAt: project.createdAt,
|
|
646
|
-
accountId: project.cloudflare
|
|
647
|
-
workerId: project.cloudflare
|
|
648
|
-
dbName
|
|
1163
|
+
accountId: project.cloudflare?.accountId ?? null,
|
|
1164
|
+
workerId: project.cloudflare?.workerId ?? null,
|
|
1165
|
+
dbName,
|
|
649
1166
|
deployed: workerExists || !!project.workerUrl,
|
|
650
1167
|
local: localExists,
|
|
651
1168
|
backedUp,
|
|
652
|
-
missing:
|
|
1169
|
+
missing: false, // No longer tracking local paths in registry
|
|
653
1170
|
backupFiles,
|
|
654
1171
|
backupLastSync,
|
|
655
1172
|
};
|
|
656
1173
|
}
|
|
657
1174
|
|
|
658
|
-
// ============================================================================
|
|
659
|
-
// List All Projects Operation
|
|
660
|
-
// ============================================================================
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* List all projects with status information
|
|
664
|
-
*
|
|
665
|
-
* Extracted from commands/projects.ts listProjects to enable programmatic project listing.
|
|
666
|
-
*
|
|
667
|
-
* @param filter - Filter projects by status
|
|
668
|
-
* @returns Array of project statuses
|
|
669
|
-
*/
|
|
670
|
-
export async function listAllProjects(
|
|
671
|
-
filter?: "all" | "local" | "deployed" | "cloud",
|
|
672
|
-
): Promise<ProjectStatus[]> {
|
|
673
|
-
const projects = await getAllProjects();
|
|
674
|
-
const projectNames = Object.keys(projects);
|
|
675
|
-
|
|
676
|
-
if (projectNames.length === 0) {
|
|
677
|
-
return [];
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Determine status for each project
|
|
681
|
-
const statuses: ProjectStatus[] = await Promise.all(
|
|
682
|
-
projectNames.map(async (name) => {
|
|
683
|
-
const project = projects[name];
|
|
684
|
-
if (!project) {
|
|
685
|
-
return null;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const local = project.localPath ? existsSync(project.localPath) : false;
|
|
689
|
-
const missing = project.localPath ? !local : false;
|
|
690
|
-
|
|
691
|
-
// Check if deployed
|
|
692
|
-
let deployed = false;
|
|
693
|
-
if (project.workerUrl) {
|
|
694
|
-
deployed = true;
|
|
695
|
-
} else {
|
|
696
|
-
deployed = await checkWorkerExists(name);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Check if backed up
|
|
700
|
-
const manifest = await getRemoteManifest(name);
|
|
701
|
-
const backedUp = manifest !== null;
|
|
702
|
-
const backupFiles = manifest ? manifest.files.length : null;
|
|
703
|
-
const backupLastSync = manifest ? manifest.lastSync : null;
|
|
704
|
-
|
|
705
|
-
return {
|
|
706
|
-
name,
|
|
707
|
-
localPath: project.localPath,
|
|
708
|
-
workerUrl: project.workerUrl,
|
|
709
|
-
lastDeployed: project.lastDeployed,
|
|
710
|
-
createdAt: project.createdAt,
|
|
711
|
-
accountId: project.cloudflare.accountId,
|
|
712
|
-
workerId: project.cloudflare.workerId,
|
|
713
|
-
dbName: getProjectDatabaseName(project),
|
|
714
|
-
local,
|
|
715
|
-
deployed,
|
|
716
|
-
backedUp,
|
|
717
|
-
missing,
|
|
718
|
-
backupFiles,
|
|
719
|
-
backupLastSync,
|
|
720
|
-
};
|
|
721
|
-
}),
|
|
722
|
-
).then((results) => results.filter((s): s is ProjectStatus => s !== null));
|
|
723
|
-
|
|
724
|
-
// Apply filter
|
|
725
|
-
if (!filter || filter === "all") {
|
|
726
|
-
return statuses;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
switch (filter) {
|
|
730
|
-
case "local":
|
|
731
|
-
return statuses.filter((s) => s.local);
|
|
732
|
-
case "deployed":
|
|
733
|
-
return statuses.filter((s) => s.deployed);
|
|
734
|
-
case "cloud":
|
|
735
|
-
return statuses.filter((s) => s.backedUp);
|
|
736
|
-
default:
|
|
737
|
-
return statuses;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
1175
|
// ============================================================================
|
|
742
1176
|
// Cleanup Operations
|
|
743
1177
|
// ============================================================================
|
|
744
1178
|
|
|
745
1179
|
/**
|
|
746
|
-
* Scan registry for stale projects
|
|
1180
|
+
* Scan registry for stale projects.
|
|
1181
|
+
* Checks for projects with worker URLs that no longer have deployed workers.
|
|
747
1182
|
* Returns total project count and stale entries with reasons.
|
|
748
1183
|
*/
|
|
749
1184
|
export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
@@ -755,21 +1190,13 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
|
755
1190
|
const project = projects[name];
|
|
756
1191
|
if (!project) continue;
|
|
757
1192
|
|
|
758
|
-
if
|
|
759
|
-
stale.push({
|
|
760
|
-
name,
|
|
761
|
-
reason: "local folder deleted",
|
|
762
|
-
workerUrl: project.workerUrl,
|
|
763
|
-
});
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
|
|
1193
|
+
// Check if worker URL is set but worker doesn't exist
|
|
767
1194
|
if (project.workerUrl) {
|
|
768
1195
|
const workerExists = await checkWorkerExists(name);
|
|
769
1196
|
if (!workerExists) {
|
|
770
1197
|
stale.push({
|
|
771
1198
|
name,
|
|
772
|
-
reason: "
|
|
1199
|
+
reason: "worker not deployed",
|
|
773
1200
|
workerUrl: project.workerUrl,
|
|
774
1201
|
});
|
|
775
1202
|
}
|