@getjack/jack 0.1.4 → 0.1.6
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/login.ts +124 -1
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +166 -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 +137 -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 +449 -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 +86 -157
- 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/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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jack tag - Manage project tags
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* jack tag add <tags...> Add tags to current project
|
|
6
|
+
* jack tag add <project> <tags...> Add tags to named project
|
|
7
|
+
* jack tag remove <tags...> Remove tags from current project
|
|
8
|
+
* jack tag remove <project> <tags...> Remove tags from named project
|
|
9
|
+
* jack tag list List all tags across projects
|
|
10
|
+
* jack tag list [project] List tags for a specific project
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { error, info, item, success } from "../lib/output.ts";
|
|
14
|
+
import { readProjectLink } from "../lib/project-link.ts";
|
|
15
|
+
import {
|
|
16
|
+
addTags,
|
|
17
|
+
findProjectPathByName,
|
|
18
|
+
getAllTagsWithCounts,
|
|
19
|
+
getProjectTags,
|
|
20
|
+
removeTags,
|
|
21
|
+
validateTags,
|
|
22
|
+
} from "../lib/tags.ts";
|
|
23
|
+
|
|
24
|
+
export default async function tag(subcommand?: string, args: string[] = []): Promise<void> {
|
|
25
|
+
if (!subcommand) {
|
|
26
|
+
showHelp();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case "add":
|
|
32
|
+
return await addTagsCommand(args);
|
|
33
|
+
case "remove":
|
|
34
|
+
return await removeTagsCommand(args);
|
|
35
|
+
case "list":
|
|
36
|
+
return await listTagsCommand(args);
|
|
37
|
+
case "--help":
|
|
38
|
+
case "-h":
|
|
39
|
+
case "help":
|
|
40
|
+
showHelp();
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
error(`Unknown subcommand: ${subcommand}`);
|
|
44
|
+
info("Available: add, remove, list");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Show help for the tag command
|
|
51
|
+
*/
|
|
52
|
+
function showHelp(): void {
|
|
53
|
+
console.error(`
|
|
54
|
+
jack tag - Manage project tags
|
|
55
|
+
|
|
56
|
+
Usage
|
|
57
|
+
$ jack tag add <tags...> Add tags to current project
|
|
58
|
+
$ jack tag add <project> <tags...> Add tags to named project
|
|
59
|
+
$ jack tag remove <tags...> Remove tags from current project
|
|
60
|
+
$ jack tag remove <project> <tags...> Remove tags from named project
|
|
61
|
+
$ jack tag list List all tags across projects
|
|
62
|
+
$ jack tag list [project] List tags for a specific project
|
|
63
|
+
|
|
64
|
+
Tag Format
|
|
65
|
+
Tags must be lowercase alphanumeric with optional colons and hyphens.
|
|
66
|
+
Examples: backend, api:v2, my-service, prod
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
$ jack tag add backend api Add tags in project directory
|
|
70
|
+
$ jack tag add my-app backend Add tag to my-app project
|
|
71
|
+
$ jack tag remove deprecated Remove tag from current project
|
|
72
|
+
$ jack tag list Show all tags with counts
|
|
73
|
+
$ jack tag list my-app Show tags for my-app
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve project path from arguments
|
|
79
|
+
* Returns [projectPath, remainingArgs]
|
|
80
|
+
*
|
|
81
|
+
* Logic:
|
|
82
|
+
* 1. If in a linked project directory, use cwd and all args are tags
|
|
83
|
+
* 2. If not in project directory, first arg might be project name
|
|
84
|
+
*/
|
|
85
|
+
async function resolveProjectAndTags(args: string[]): Promise<[string | null, string[]]> {
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
|
|
88
|
+
// Check if we're in a linked project directory
|
|
89
|
+
const link = await readProjectLink(cwd);
|
|
90
|
+
|
|
91
|
+
if (link) {
|
|
92
|
+
// In a project directory - all args are tags
|
|
93
|
+
return [cwd, args];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Not in a project directory - first arg might be project name
|
|
97
|
+
if (args.length === 0) {
|
|
98
|
+
return [null, []];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const firstArg = args[0] as string; // Safe: we checked args.length > 0 above
|
|
102
|
+
const rest = args.slice(1);
|
|
103
|
+
|
|
104
|
+
// Try to find project by name
|
|
105
|
+
const projectPath = await findProjectPathByName(firstArg);
|
|
106
|
+
|
|
107
|
+
if (projectPath) {
|
|
108
|
+
// First arg was a project name
|
|
109
|
+
return [projectPath, rest];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// First arg wasn't a project name - we're not in a project directory
|
|
113
|
+
// and couldn't find a matching project
|
|
114
|
+
return [null, args];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add tags to a project
|
|
119
|
+
*/
|
|
120
|
+
async function addTagsCommand(args: string[]): Promise<void> {
|
|
121
|
+
const [projectPath, tagArgs] = await resolveProjectAndTags(args);
|
|
122
|
+
|
|
123
|
+
if (!projectPath) {
|
|
124
|
+
error("Not in a project directory and no valid project name provided");
|
|
125
|
+
info("Run from a project directory or specify project name: jack tag add <project> <tags...>");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (tagArgs.length === 0) {
|
|
130
|
+
error("No tags specified");
|
|
131
|
+
info("Usage: jack tag add <tags...>");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate tags first
|
|
136
|
+
const validation = validateTags(tagArgs);
|
|
137
|
+
if (!validation.valid) {
|
|
138
|
+
error("Invalid tags:");
|
|
139
|
+
for (const { tag, reason } of validation.invalidTags) {
|
|
140
|
+
item(`"${tag}": ${reason}`);
|
|
141
|
+
}
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await addTags(projectPath, tagArgs);
|
|
146
|
+
|
|
147
|
+
if (!result.success) {
|
|
148
|
+
error(result.error || "Failed to add tags");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (result.added && result.added.length > 0) {
|
|
153
|
+
success(`Added tags: ${result.added.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.skipped && result.skipped.length > 0) {
|
|
157
|
+
info(`Already present: ${result.skipped.join(", ")}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (result.tags.length > 0) {
|
|
161
|
+
info(`Current tags: ${result.tags.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remove tags from a project
|
|
167
|
+
*/
|
|
168
|
+
async function removeTagsCommand(args: string[]): Promise<void> {
|
|
169
|
+
const [projectPath, tagArgs] = await resolveProjectAndTags(args);
|
|
170
|
+
|
|
171
|
+
if (!projectPath) {
|
|
172
|
+
error("Not in a project directory and no valid project name provided");
|
|
173
|
+
info(
|
|
174
|
+
"Run from a project directory or specify project name: jack tag remove <project> <tags...>",
|
|
175
|
+
);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (tagArgs.length === 0) {
|
|
180
|
+
error("No tags specified");
|
|
181
|
+
info("Usage: jack tag remove <tags...>");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = await removeTags(projectPath, tagArgs);
|
|
186
|
+
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
error(result.error || "Failed to remove tags");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (result.removed && result.removed.length > 0) {
|
|
193
|
+
success(`Removed tags: ${result.removed.join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.skipped && result.skipped.length > 0) {
|
|
197
|
+
info(`Not found: ${result.skipped.join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (result.tags.length > 0) {
|
|
201
|
+
info(`Remaining tags: ${result.tags.join(", ")}`);
|
|
202
|
+
} else {
|
|
203
|
+
info("No tags remaining");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* List tags for a project or all tags across projects
|
|
209
|
+
*/
|
|
210
|
+
async function listTagsCommand(args: string[]): Promise<void> {
|
|
211
|
+
const [projectArg] = args;
|
|
212
|
+
|
|
213
|
+
if (projectArg) {
|
|
214
|
+
// List tags for a specific project
|
|
215
|
+
await listProjectTags(projectArg);
|
|
216
|
+
} else {
|
|
217
|
+
// Check if we're in a project directory
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
const link = await readProjectLink(cwd);
|
|
220
|
+
|
|
221
|
+
if (link) {
|
|
222
|
+
// In a project directory - show tags for this project
|
|
223
|
+
await listProjectTagsForPath(cwd);
|
|
224
|
+
} else {
|
|
225
|
+
// Not in project directory - show all tags
|
|
226
|
+
await listAllTags();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* List tags for a specific project by name
|
|
233
|
+
*/
|
|
234
|
+
async function listProjectTags(projectName: string): Promise<void> {
|
|
235
|
+
const projectPath = await findProjectPathByName(projectName);
|
|
236
|
+
|
|
237
|
+
if (!projectPath) {
|
|
238
|
+
error(`Project not found: ${projectName}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await listProjectTagsForPath(projectPath);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List tags for a project at a specific path
|
|
247
|
+
*/
|
|
248
|
+
async function listProjectTagsForPath(projectPath: string): Promise<void> {
|
|
249
|
+
const tags = await getProjectTags(projectPath);
|
|
250
|
+
|
|
251
|
+
console.error("");
|
|
252
|
+
if (tags.length === 0) {
|
|
253
|
+
info("No tags for this project");
|
|
254
|
+
info("Add tags with: jack tag add <tags...>");
|
|
255
|
+
} else {
|
|
256
|
+
info(`Tags (${tags.length}):`);
|
|
257
|
+
for (const tag of tags) {
|
|
258
|
+
item(tag);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
console.error("");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* List all tags across all projects with counts
|
|
266
|
+
*/
|
|
267
|
+
async function listAllTags(): Promise<void> {
|
|
268
|
+
const tagCounts = await getAllTagsWithCounts();
|
|
269
|
+
|
|
270
|
+
console.error("");
|
|
271
|
+
if (tagCounts.length === 0) {
|
|
272
|
+
info("No tags found across any projects");
|
|
273
|
+
info("Add tags with: jack tag add <tags...>");
|
|
274
|
+
} else {
|
|
275
|
+
info(`All tags (${tagCounts.length}):`);
|
|
276
|
+
for (const { tag, count } of tagCounts) {
|
|
277
|
+
const projectLabel = count === 1 ? "project" : "projects";
|
|
278
|
+
item(`${tag} (${count} ${projectLabel})`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
console.error("");
|
|
282
|
+
}
|
|
@@ -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
|
+
}
|