@getjack/jack 0.1.4 → 0.1.5
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 +2 -6
- package/src/commands/agents.ts +9 -24
- package/src/commands/clone.ts +27 -0
- package/src/commands/down.ts +31 -57
- package/src/commands/feedback.ts +4 -5
- package/src/commands/link.ts +147 -0
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +162 -105
- package/src/commands/secrets.ts +7 -6
- package/src/commands/services.ts +5 -4
- package/src/commands/tag.ts +282 -0
- package/src/commands/unlink.ts +30 -0
- package/src/index.ts +46 -1
- package/src/lib/auth/index.ts +2 -0
- package/src/lib/auth/store.ts +26 -2
- package/src/lib/binding-validator.ts +4 -13
- package/src/lib/build-helper.ts +93 -5
- package/src/lib/control-plane.ts +48 -0
- package/src/lib/deploy-mode.ts +1 -1
- package/src/lib/managed-deploy.ts +11 -1
- package/src/lib/managed-down.ts +7 -20
- package/src/lib/paths-index.test.ts +546 -0
- package/src/lib/paths-index.ts +310 -0
- package/src/lib/project-link.test.ts +459 -0
- package/src/lib/project-link.ts +279 -0
- package/src/lib/project-list.test.ts +581 -0
- package/src/lib/project-list.ts +445 -0
- package/src/lib/project-operations.ts +304 -183
- package/src/lib/project-resolver.ts +191 -211
- package/src/lib/tags.ts +389 -0
- package/src/lib/telemetry.ts +81 -168
- package/src/lib/zip-packager.ts +9 -0
- package/src/templates/index.ts +5 -3
- package/templates/api/.jack/template.json +4 -0
- package/templates/hello/.jack/template.json +4 -0
- package/templates/miniapp/.jack/template.json +4 -0
- package/templates/nextjs/.jack.json +28 -0
- package/templates/nextjs/app/globals.css +9 -0
- package/templates/nextjs/app/isr-test/page.tsx +22 -0
- package/templates/nextjs/app/layout.tsx +19 -0
- package/templates/nextjs/app/page.tsx +8 -0
- package/templates/nextjs/bun.lock +2232 -0
- package/templates/nextjs/cloudflare-env.d.ts +3 -0
- package/templates/nextjs/next-env.d.ts +6 -0
- package/templates/nextjs/next.config.ts +8 -0
- package/templates/nextjs/open-next.config.ts +6 -0
- package/templates/nextjs/package.json +24 -0
- package/templates/nextjs/public/_headers +2 -0
- package/templates/nextjs/tsconfig.json +44 -0
- package/templates/nextjs/wrangler.jsonc +17 -0
- package/src/lib/local-paths.test.ts +0 -902
- package/src/lib/local-paths.ts +0 -258
- package/src/lib/registry.ts +0 -181
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jack unlink - Remove .jack/ directory from current project
|
|
3
|
+
*
|
|
4
|
+
* Removes the local project link but does NOT delete the project from cloud.
|
|
5
|
+
* You can re-link anytime with: jack link
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { error, info, success } from "../lib/output.ts";
|
|
9
|
+
import { unregisterPath } from "../lib/paths-index.ts";
|
|
10
|
+
import { readProjectLink, unlinkProject } from "../lib/project-link.ts";
|
|
11
|
+
|
|
12
|
+
export default async function unlink(): Promise<void> {
|
|
13
|
+
// Check if linked
|
|
14
|
+
const link = await readProjectLink(process.cwd());
|
|
15
|
+
|
|
16
|
+
if (!link) {
|
|
17
|
+
error("This directory is not linked");
|
|
18
|
+
info("Nothing to unlink");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Remove from paths index
|
|
23
|
+
await unregisterPath(link.project_id, process.cwd());
|
|
24
|
+
|
|
25
|
+
// Remove .jack/ directory
|
|
26
|
+
await unlinkProject(process.cwd());
|
|
27
|
+
|
|
28
|
+
success("Project unlinked");
|
|
29
|
+
info("You can re-link with: jack link");
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import pkg from "../package.json";
|
|
|
4
4
|
import { enableDebug } from "./lib/debug.ts";
|
|
5
5
|
import { isJackError } from "./lib/errors.ts";
|
|
6
6
|
import { info, error as printError } from "./lib/output.ts";
|
|
7
|
-
import { identify, shutdown, withTelemetry } from "./lib/telemetry.ts";
|
|
7
|
+
import { getEnvironmentProps, identify, shutdown, withTelemetry } from "./lib/telemetry.ts";
|
|
8
8
|
|
|
9
9
|
const cli = meow(
|
|
10
10
|
`
|
|
@@ -35,6 +35,11 @@ const cli = meow(
|
|
|
35
35
|
logout Sign out
|
|
36
36
|
whoami Show current user
|
|
37
37
|
|
|
38
|
+
Project Management
|
|
39
|
+
link [name] Link directory to a project
|
|
40
|
+
unlink Remove project link
|
|
41
|
+
tag Manage project tags
|
|
42
|
+
|
|
38
43
|
Advanced
|
|
39
44
|
agents Manage AI agent configs
|
|
40
45
|
secrets Manage project secrets
|
|
@@ -105,6 +110,18 @@ const cli = meow(
|
|
|
105
110
|
type: "boolean",
|
|
106
111
|
default: false,
|
|
107
112
|
},
|
|
113
|
+
all: {
|
|
114
|
+
type: "boolean",
|
|
115
|
+
shortFlag: "a",
|
|
116
|
+
default: false,
|
|
117
|
+
},
|
|
118
|
+
status: {
|
|
119
|
+
type: "string",
|
|
120
|
+
},
|
|
121
|
+
json: {
|
|
122
|
+
type: "boolean",
|
|
123
|
+
default: false,
|
|
124
|
+
},
|
|
108
125
|
project: {
|
|
109
126
|
type: "string",
|
|
110
127
|
shortFlag: "p",
|
|
@@ -125,6 +142,10 @@ const cli = meow(
|
|
|
125
142
|
type: "boolean",
|
|
126
143
|
default: false,
|
|
127
144
|
},
|
|
145
|
+
tag: {
|
|
146
|
+
type: "string",
|
|
147
|
+
isMultiple: true,
|
|
148
|
+
},
|
|
128
149
|
},
|
|
129
150
|
},
|
|
130
151
|
);
|
|
@@ -143,6 +164,7 @@ identify({
|
|
|
143
164
|
arch: process.arch,
|
|
144
165
|
node_version: process.version,
|
|
145
166
|
is_ci: !!process.env.CI,
|
|
167
|
+
...getEnvironmentProps(),
|
|
146
168
|
});
|
|
147
169
|
|
|
148
170
|
try {
|
|
@@ -200,6 +222,11 @@ try {
|
|
|
200
222
|
});
|
|
201
223
|
break;
|
|
202
224
|
}
|
|
225
|
+
case "tag": {
|
|
226
|
+
const { default: tag } = await import("./commands/tag.ts");
|
|
227
|
+
await withTelemetry("tag", tag)(args[0], args.slice(1));
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
203
230
|
case "sync": {
|
|
204
231
|
const { default: sync } = await import("./commands/sync.ts");
|
|
205
232
|
await withTelemetry(
|
|
@@ -279,6 +306,14 @@ try {
|
|
|
279
306
|
if (cli.flags.local) lsArgs.push("--local");
|
|
280
307
|
if (cli.flags.deployed) lsArgs.push("--deployed");
|
|
281
308
|
if (cli.flags.cloud) lsArgs.push("--cloud");
|
|
309
|
+
if (cli.flags.all) lsArgs.push("--all");
|
|
310
|
+
if (cli.flags.json) lsArgs.push("--json");
|
|
311
|
+
if (cli.flags.status) lsArgs.push("--status", cli.flags.status);
|
|
312
|
+
if (cli.flags.tag) {
|
|
313
|
+
for (const t of cli.flags.tag) {
|
|
314
|
+
lsArgs.push("--tag", t);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
282
317
|
await withTelemetry("projects", projects)("list", lsArgs);
|
|
283
318
|
break;
|
|
284
319
|
}
|
|
@@ -307,6 +342,16 @@ try {
|
|
|
307
342
|
await withTelemetry("feedback", feedback)();
|
|
308
343
|
break;
|
|
309
344
|
}
|
|
345
|
+
case "link": {
|
|
346
|
+
const { default: link } = await import("./commands/link.ts");
|
|
347
|
+
await withTelemetry("link", link)(args[0], { byo: cli.flags.byo });
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "unlink": {
|
|
351
|
+
const { default: unlink } = await import("./commands/unlink.ts");
|
|
352
|
+
await withTelemetry("unlink", unlink)();
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
310
355
|
default:
|
|
311
356
|
cli.showHelp(command ? 1 : 0);
|
|
312
357
|
}
|
package/src/lib/auth/index.ts
CHANGED
|
@@ -9,10 +9,12 @@ export {
|
|
|
9
9
|
export { requireAuth, requireAuthOrLogin, getCurrentUser } from "./guard.ts";
|
|
10
10
|
export {
|
|
11
11
|
deleteCredentials,
|
|
12
|
+
getAuthState,
|
|
12
13
|
getCredentials,
|
|
13
14
|
isLoggedIn,
|
|
14
15
|
isTokenExpired,
|
|
15
16
|
saveCredentials,
|
|
16
17
|
type AuthCredentials,
|
|
18
|
+
type AuthState,
|
|
17
19
|
type AuthUser,
|
|
18
20
|
} from "./store.ts";
|
package/src/lib/auth/store.ts
CHANGED
|
@@ -42,9 +42,33 @@ export async function deleteCredentials(): Promise<void> {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export
|
|
45
|
+
export type AuthState = "logged-in" | "not-logged-in" | "session-expired";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get detailed auth state
|
|
49
|
+
* - "logged-in": valid token (or successfully refreshed)
|
|
50
|
+
* - "not-logged-in": no credentials stored
|
|
51
|
+
* - "session-expired": had credentials but refresh failed
|
|
52
|
+
*/
|
|
53
|
+
export async function getAuthState(): Promise<AuthState> {
|
|
46
54
|
const creds = await getCredentials();
|
|
47
|
-
|
|
55
|
+
if (!creds) return "not-logged-in";
|
|
56
|
+
|
|
57
|
+
// If token is not expired, we're logged in
|
|
58
|
+
if (!isTokenExpired(creds)) return "logged-in";
|
|
59
|
+
|
|
60
|
+
// If expired, try to refresh (dynamic import to avoid circular dep)
|
|
61
|
+
try {
|
|
62
|
+
const { getValidAccessToken } = await import("./client.ts");
|
|
63
|
+
const token = await getValidAccessToken();
|
|
64
|
+
return token !== null ? "logged-in" : "session-expired";
|
|
65
|
+
} catch {
|
|
66
|
+
return "session-expired";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isLoggedIn(): Promise<boolean> {
|
|
71
|
+
return (await getAuthState()) === "logged-in";
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
export function isTokenExpired(creds: AuthCredentials): boolean {
|
|
@@ -12,7 +12,7 @@ import type { WranglerConfig } from "./build-helper.ts";
|
|
|
12
12
|
/**
|
|
13
13
|
* Bindings supported by jack cloud managed deployments.
|
|
14
14
|
*/
|
|
15
|
-
export const SUPPORTED_BINDINGS = ["d1_databases", "ai", "assets", "vars"] as const;
|
|
15
|
+
export const SUPPORTED_BINDINGS = ["d1_databases", "ai", "assets", "vars", "r2_buckets"] as const;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Bindings not yet supported by jack cloud.
|
|
@@ -23,7 +23,6 @@ export const UNSUPPORTED_BINDINGS = [
|
|
|
23
23
|
"durable_objects",
|
|
24
24
|
"queues",
|
|
25
25
|
"services",
|
|
26
|
-
"r2_buckets",
|
|
27
26
|
"hyperdrive",
|
|
28
27
|
"vectorize",
|
|
29
28
|
"browser",
|
|
@@ -38,7 +37,6 @@ const BINDING_DISPLAY_NAMES: Record<string, string> = {
|
|
|
38
37
|
durable_objects: "Durable Objects",
|
|
39
38
|
queues: "Queues",
|
|
40
39
|
services: "Service Bindings",
|
|
41
|
-
r2_buckets: "R2 Buckets",
|
|
42
40
|
hyperdrive: "Hyperdrive",
|
|
43
41
|
vectorize: "Vectorize",
|
|
44
42
|
browser: "Browser Rendering",
|
|
@@ -68,16 +66,9 @@ export function validateBindings(
|
|
|
68
66
|
const value = config[binding as keyof WranglerConfig];
|
|
69
67
|
if (value !== undefined && value !== null) {
|
|
70
68
|
const displayName = BINDING_DISPLAY_NAMES[binding] || binding;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`✗ R2 buckets not supported in managed deploy.\n For static files, use Workers Assets instead (assets.directory in wrangler.jsonc).\n Fix: Replace r2_buckets with assets config, or use 'wrangler deploy' for full control.`,
|
|
75
|
-
);
|
|
76
|
-
} else {
|
|
77
|
-
errors.push(
|
|
78
|
-
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
69
|
+
errors.push(
|
|
70
|
+
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
71
|
+
);
|
|
81
72
|
}
|
|
82
73
|
}
|
|
83
74
|
|
package/src/lib/build-helper.ts
CHANGED
|
@@ -36,12 +36,15 @@ export interface WranglerConfig {
|
|
|
36
36
|
run_worker_first?: boolean;
|
|
37
37
|
};
|
|
38
38
|
vars?: Record<string, string>;
|
|
39
|
+
r2_buckets?: Array<{
|
|
40
|
+
binding: string;
|
|
41
|
+
bucket_name: string;
|
|
42
|
+
}>;
|
|
39
43
|
// Unsupported bindings (for validation)
|
|
40
44
|
kv_namespaces?: unknown;
|
|
41
45
|
durable_objects?: unknown;
|
|
42
46
|
queues?: unknown;
|
|
43
47
|
services?: unknown;
|
|
44
|
-
r2_buckets?: unknown;
|
|
45
48
|
hyperdrive?: unknown;
|
|
46
49
|
vectorize?: unknown;
|
|
47
50
|
browser?: unknown;
|
|
@@ -82,6 +85,18 @@ export async function needsViteBuild(projectPath: string): Promise<boolean> {
|
|
|
82
85
|
);
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Checks if project requires OpenNext build by detecting open-next config files
|
|
90
|
+
* @param projectPath - Absolute path to project directory
|
|
91
|
+
* @returns true if open-next.config.ts or open-next.config.js exists
|
|
92
|
+
*/
|
|
93
|
+
export async function needsOpenNextBuild(projectPath: string): Promise<boolean> {
|
|
94
|
+
return (
|
|
95
|
+
existsSync(join(projectPath, "open-next.config.ts")) ||
|
|
96
|
+
existsSync(join(projectPath, "open-next.config.js"))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* Runs Vite build for the project
|
|
87
102
|
* @param projectPath - Absolute path to project directory
|
|
@@ -105,6 +120,29 @@ export async function runViteBuild(projectPath: string): Promise<void> {
|
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Runs OpenNext build for Next.js projects targeting Cloudflare
|
|
125
|
+
* @param projectPath - Absolute path to project directory
|
|
126
|
+
* @throws JackError if build fails
|
|
127
|
+
*/
|
|
128
|
+
export async function runOpenNextBuild(projectPath: string): Promise<void> {
|
|
129
|
+
// OpenNext builds Next.js for Cloudflare Workers
|
|
130
|
+
// Outputs to .open-next/worker.js and .open-next/assets/
|
|
131
|
+
const buildResult = await $`bunx opennextjs-cloudflare build`.cwd(projectPath).nothrow().quiet();
|
|
132
|
+
|
|
133
|
+
if (buildResult.exitCode !== 0) {
|
|
134
|
+
throw new JackError(
|
|
135
|
+
JackErrorCode.BUILD_FAILED,
|
|
136
|
+
"OpenNext build failed",
|
|
137
|
+
"Check your next.config and source files for errors",
|
|
138
|
+
{
|
|
139
|
+
exitCode: buildResult.exitCode,
|
|
140
|
+
stderr: buildResult.stderr.toString(),
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
/**
|
|
109
147
|
* Builds a Cloudflare Worker project using wrangler dry-run
|
|
110
148
|
* @param options - Build options with project path and optional reporter
|
|
@@ -117,13 +155,22 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
|
|
|
117
155
|
// Parse wrangler config first
|
|
118
156
|
const config = await parseWranglerConfig(projectPath);
|
|
119
157
|
|
|
120
|
-
// Check if
|
|
158
|
+
// Check if OpenNext build is needed (Next.js + Cloudflare)
|
|
159
|
+
const hasOpenNext = await needsOpenNextBuild(projectPath);
|
|
160
|
+
if (hasOpenNext) {
|
|
161
|
+
reporter?.start("Building...");
|
|
162
|
+
await runOpenNextBuild(projectPath);
|
|
163
|
+
reporter?.stop();
|
|
164
|
+
reporter?.success("Built");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if Vite build is needed and run it (skip if OpenNext already built)
|
|
121
168
|
const hasVite = await needsViteBuild(projectPath);
|
|
122
|
-
if (hasVite) {
|
|
123
|
-
reporter?.start("Building
|
|
169
|
+
if (hasVite && !hasOpenNext) {
|
|
170
|
+
reporter?.start("Building...");
|
|
124
171
|
await runViteBuild(projectPath);
|
|
125
172
|
reporter?.stop();
|
|
126
|
-
reporter?.success("Built
|
|
173
|
+
reporter?.success("Built");
|
|
127
174
|
}
|
|
128
175
|
|
|
129
176
|
// Create unique temp directory for build output
|
|
@@ -209,3 +256,44 @@ async function resolveEntrypoint(outDir: string, main?: string): Promise<string>
|
|
|
209
256
|
"Ensure wrangler outputs a single entry file (index.js or worker.js)",
|
|
210
257
|
);
|
|
211
258
|
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Ensures R2 buckets exist for BYO deploy.
|
|
262
|
+
* Creates buckets via wrangler if they don't exist.
|
|
263
|
+
* @param projectPath - Absolute path to project directory
|
|
264
|
+
* @returns Array of bucket names that were created or already existed
|
|
265
|
+
*/
|
|
266
|
+
export async function ensureR2Buckets(projectPath: string): Promise<string[]> {
|
|
267
|
+
const config = await parseWranglerConfig(projectPath);
|
|
268
|
+
|
|
269
|
+
if (!config.r2_buckets || config.r2_buckets.length === 0) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const results: string[] = [];
|
|
274
|
+
|
|
275
|
+
for (const bucket of config.r2_buckets) {
|
|
276
|
+
const bucketName = bucket.bucket_name;
|
|
277
|
+
|
|
278
|
+
// Try to create the bucket (wrangler handles "already exists" gracefully)
|
|
279
|
+
const result = await $`wrangler r2 bucket create ${bucketName}`
|
|
280
|
+
.cwd(projectPath)
|
|
281
|
+
.nothrow()
|
|
282
|
+
.quiet();
|
|
283
|
+
|
|
284
|
+
// Exit code 0 = created, non-zero with "already exists" = fine
|
|
285
|
+
const stderr = result.stderr.toString();
|
|
286
|
+
if (result.exitCode === 0 || stderr.includes("already exists")) {
|
|
287
|
+
results.push(bucketName);
|
|
288
|
+
} else {
|
|
289
|
+
throw new JackError(
|
|
290
|
+
JackErrorCode.RESOURCE_ERROR,
|
|
291
|
+
`Failed to create R2 bucket: ${bucketName}`,
|
|
292
|
+
"Check your Cloudflare account has R2 enabled",
|
|
293
|
+
{ stderr },
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return results;
|
|
299
|
+
}
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface CreateProjectResponse {
|
|
|
40
40
|
status?: "live" | "created";
|
|
41
41
|
url?: string;
|
|
42
42
|
prebuilt_failed?: boolean;
|
|
43
|
+
prebuilt_error?: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface SlugAvailabilityResponse {
|
|
@@ -164,6 +165,7 @@ export interface ManagedProject {
|
|
|
164
165
|
status: "active" | "error" | "deleted";
|
|
165
166
|
created_at: string;
|
|
166
167
|
updated_at: string;
|
|
168
|
+
tags?: string; // JSON string array from DB, e.g., '["backend", "api"]'
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
/**
|
|
@@ -293,3 +295,49 @@ export async function fetchProjectResources(projectId: string): Promise<ProjectR
|
|
|
293
295
|
const data = (await response.json()) as { resources: ProjectResource[] };
|
|
294
296
|
return data.resources;
|
|
295
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Sync project tags to the control plane.
|
|
301
|
+
* Fire-and-forget: errors are logged but not thrown.
|
|
302
|
+
*/
|
|
303
|
+
export async function syncProjectTags(projectId: string, tags: string[]): Promise<void> {
|
|
304
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
305
|
+
const { debug } = await import("./debug.ts");
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/tags`, {
|
|
309
|
+
method: "PUT",
|
|
310
|
+
headers: { "Content-Type": "application/json" },
|
|
311
|
+
body: JSON.stringify({ tags }),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
// Log but don't throw - tag sync is non-critical
|
|
316
|
+
debug(`Tag sync failed: ${response.status}`);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// Log but don't throw - tag sync is non-critical
|
|
320
|
+
debug(`Tag sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Fetch project tags from the control plane.
|
|
326
|
+
* Returns empty array on error.
|
|
327
|
+
*/
|
|
328
|
+
export async function fetchProjectTags(projectId: string): Promise<string[]> {
|
|
329
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/tags`);
|
|
333
|
+
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const data = (await response.json()) as { tags: string[] };
|
|
339
|
+
return data.tags ?? [];
|
|
340
|
+
} catch {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
package/src/lib/deploy-mode.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { validateBindings } from "./binding-validator.ts";
|
|
8
8
|
import { buildProject, parseWranglerConfig } from "./build-helper.ts";
|
|
9
|
-
import { createManagedProject } from "./control-plane.ts";
|
|
9
|
+
import { createManagedProject, syncProjectTags } from "./control-plane.ts";
|
|
10
10
|
import { uploadDeployment } from "./deploy-upload.ts";
|
|
11
11
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
12
12
|
import type { OperationReporter } from "./project-operations.ts";
|
|
13
|
+
import { getProjectTags } from "./tags.ts";
|
|
13
14
|
import { Events, track } from "./telemetry.ts";
|
|
14
15
|
import { packageForDeploy } from "./zip-packager.ts";
|
|
15
16
|
|
|
@@ -138,6 +139,15 @@ export async function deployCodeToManagedProject(
|
|
|
138
139
|
duration_ms: Date.now() - startTime,
|
|
139
140
|
});
|
|
140
141
|
|
|
142
|
+
// Fire-and-forget tag sync (non-blocking)
|
|
143
|
+
getProjectTags(projectPath)
|
|
144
|
+
.then((tags) => {
|
|
145
|
+
if (tags.length > 0) {
|
|
146
|
+
void syncProjectTags(projectId, tags);
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.catch(() => {});
|
|
150
|
+
|
|
141
151
|
return {
|
|
142
152
|
deploymentId: result.id,
|
|
143
153
|
status: result.status,
|
package/src/lib/managed-down.ts
CHANGED
|
@@ -8,25 +8,22 @@ import { join } from "node:path";
|
|
|
8
8
|
import { deleteManagedProject, exportManagedDatabase } from "./control-plane.ts";
|
|
9
9
|
import { promptSelect } from "./hooks.ts";
|
|
10
10
|
import { error, info, item, output, success, warn } from "./output.ts";
|
|
11
|
-
import type { Project } from "./registry.ts";
|
|
12
|
-
import { updateProject } from "./registry.ts";
|
|
13
11
|
|
|
14
12
|
export interface ManagedDownFlags {
|
|
15
13
|
force?: boolean;
|
|
16
14
|
}
|
|
17
15
|
|
|
16
|
+
export interface ManagedProjectInfo {
|
|
17
|
+
projectId: string;
|
|
18
|
+
runjackUrl: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
18
21
|
export async function managedDown(
|
|
19
|
-
project:
|
|
22
|
+
project: ManagedProjectInfo,
|
|
20
23
|
projectName: string,
|
|
21
24
|
flags: ManagedDownFlags = {},
|
|
22
25
|
): Promise<boolean> {
|
|
23
|
-
const
|
|
24
|
-
if (!remote?.project_id) {
|
|
25
|
-
throw new Error("Project is not linked to jack cloud");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const runjackUrl = remote.runjack_url;
|
|
29
|
-
const projectId = remote.project_id;
|
|
26
|
+
const { projectId, runjackUrl } = project;
|
|
30
27
|
|
|
31
28
|
// Force mode - quick deletion without prompts
|
|
32
29
|
if (flags.force) {
|
|
@@ -39,11 +36,6 @@ export async function managedDown(
|
|
|
39
36
|
await deleteManagedProject(projectId);
|
|
40
37
|
output.stop();
|
|
41
38
|
|
|
42
|
-
await updateProject(projectName, {
|
|
43
|
-
workerUrl: null,
|
|
44
|
-
lastDeployed: null,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
39
|
console.error("");
|
|
48
40
|
success(`'${projectName}' undeployed`);
|
|
49
41
|
info("Database and backups were deleted");
|
|
@@ -142,11 +134,6 @@ export async function managedDown(
|
|
|
142
134
|
}
|
|
143
135
|
}
|
|
144
136
|
|
|
145
|
-
await updateProject(projectName, {
|
|
146
|
-
workerUrl: null,
|
|
147
|
-
lastDeployed: null,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
137
|
console.error("");
|
|
151
138
|
success(`Project '${projectName}' undeployed`);
|
|
152
139
|
console.error("");
|