@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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Linking
|
|
3
|
+
*
|
|
4
|
+
* Connects local directories to control plane projects or creates local-only BYO links.
|
|
5
|
+
* This module manages the .jack/project.json file that serves as a local pointer.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - .jack/project.json is a minimal pointer (project_id + deploy_mode), not a cache
|
|
9
|
+
* - Control plane is the source of truth for managed projects
|
|
10
|
+
* - BYO projects get a locally-generated UUIDv7 for tracking
|
|
11
|
+
* - .jack/ is automatically added to .gitignore
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Deploy mode for a project
|
|
20
|
+
*/
|
|
21
|
+
export type DeployMode = "managed" | "byo";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Local project link stored in .jack/project.json
|
|
25
|
+
*/
|
|
26
|
+
export interface LocalProjectLink {
|
|
27
|
+
version: 1;
|
|
28
|
+
project_id: string;
|
|
29
|
+
deploy_mode: DeployMode;
|
|
30
|
+
linked_at: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Template metadata stored in .jack/template.json
|
|
36
|
+
*/
|
|
37
|
+
export interface TemplateMetadata {
|
|
38
|
+
type: "builtin" | "github";
|
|
39
|
+
name: string; // "miniapp", "api", or "user/repo" for github
|
|
40
|
+
ref?: string; // git ref for github templates
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const JACK_DIR = ".jack";
|
|
44
|
+
const PROJECT_LINK_FILE = "project.json";
|
|
45
|
+
const TEMPLATE_FILE = "template.json";
|
|
46
|
+
const GITIGNORE_ENTRY = ".jack/";
|
|
47
|
+
const GITIGNORE_COMMENT = "# Jack project link (local-only)";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the .jack directory path for a project
|
|
51
|
+
*/
|
|
52
|
+
export function getJackDir(projectDir: string): string {
|
|
53
|
+
return join(resolve(projectDir), JACK_DIR);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the .jack/project.json path for a project
|
|
58
|
+
*/
|
|
59
|
+
export function getProjectLinkPath(projectDir: string): string {
|
|
60
|
+
return join(getJackDir(projectDir), PROJECT_LINK_FILE);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the .jack/template.json path for a project
|
|
65
|
+
*/
|
|
66
|
+
export function getTemplatePath(projectDir: string): string {
|
|
67
|
+
return join(getJackDir(projectDir), TEMPLATE_FILE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a new BYO project ID using UUIDv7
|
|
72
|
+
* Format: byo_<uuidv7> for easy identification
|
|
73
|
+
*/
|
|
74
|
+
export function generateByoProjectId(): string {
|
|
75
|
+
// UUIDv7: timestamp-based with random suffix for uniqueness
|
|
76
|
+
const timestamp = Date.now();
|
|
77
|
+
const random = crypto.getRandomValues(new Uint8Array(10));
|
|
78
|
+
const hex = Array.from(random)
|
|
79
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
80
|
+
.join("");
|
|
81
|
+
|
|
82
|
+
// UUIDv7-like format: timestamp + random
|
|
83
|
+
const timestampHex = timestamp.toString(16).padStart(12, "0");
|
|
84
|
+
const uuid = `${timestampHex.slice(0, 8)}-${timestampHex.slice(8, 12)}-7${hex.slice(0, 3)}-${hex.slice(3, 7)}-${hex.slice(7, 19)}`;
|
|
85
|
+
|
|
86
|
+
return `byo_${uuid}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Link a local directory to a control plane project (managed)
|
|
91
|
+
* or create a local-only link (BYO with provided/generated ID)
|
|
92
|
+
*/
|
|
93
|
+
export async function linkProject(
|
|
94
|
+
projectDir: string,
|
|
95
|
+
projectId: string,
|
|
96
|
+
deployMode: DeployMode,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const jackDir = getJackDir(projectDir);
|
|
99
|
+
const linkPath = getProjectLinkPath(projectDir);
|
|
100
|
+
|
|
101
|
+
// Create .jack directory if it doesn't exist
|
|
102
|
+
if (!existsSync(jackDir)) {
|
|
103
|
+
await mkdir(jackDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const link: LocalProjectLink = {
|
|
107
|
+
version: 1,
|
|
108
|
+
project_id: projectId,
|
|
109
|
+
deploy_mode: deployMode,
|
|
110
|
+
linked_at: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await writeFile(linkPath, JSON.stringify(link, null, 2));
|
|
114
|
+
|
|
115
|
+
// Auto-add .jack/ to .gitignore
|
|
116
|
+
await ensureGitignored(projectDir);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Unlink a local directory. Removes .jack/ directory entirely.
|
|
121
|
+
*/
|
|
122
|
+
export async function unlinkProject(projectDir: string): Promise<void> {
|
|
123
|
+
const jackDir = getJackDir(projectDir);
|
|
124
|
+
|
|
125
|
+
if (existsSync(jackDir)) {
|
|
126
|
+
await rm(jackDir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read the project link from a directory.
|
|
132
|
+
* Returns null if no .jack/project.json exists or if it's invalid.
|
|
133
|
+
*/
|
|
134
|
+
export async function readProjectLink(projectDir: string): Promise<LocalProjectLink | null> {
|
|
135
|
+
const linkPath = getProjectLinkPath(projectDir);
|
|
136
|
+
|
|
137
|
+
if (!existsSync(linkPath)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const content = await readFile(linkPath, "utf-8");
|
|
143
|
+
const link = JSON.parse(content) as LocalProjectLink;
|
|
144
|
+
|
|
145
|
+
// Validate required fields
|
|
146
|
+
if (!link.version || !link.project_id || !link.deploy_mode) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return link;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a directory is linked (has valid .jack/project.json)
|
|
158
|
+
*/
|
|
159
|
+
export async function isLinked(projectDir: string): Promise<boolean> {
|
|
160
|
+
const link = await readProjectLink(projectDir);
|
|
161
|
+
return link !== null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the project ID for a directory, or null if not linked.
|
|
166
|
+
*/
|
|
167
|
+
export async function getProjectId(projectDir: string): Promise<string | null> {
|
|
168
|
+
const link = await readProjectLink(projectDir);
|
|
169
|
+
return link?.project_id ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the deploy mode for a directory.
|
|
174
|
+
* Returns "byo" if not linked (default assumption per PRD).
|
|
175
|
+
*/
|
|
176
|
+
export async function getDeployMode(projectDir: string): Promise<DeployMode> {
|
|
177
|
+
const link = await readProjectLink(projectDir);
|
|
178
|
+
return link?.deploy_mode ?? "byo";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Ensure .jack/ is in .gitignore
|
|
183
|
+
*/
|
|
184
|
+
export async function ensureGitignored(projectDir: string): Promise<void> {
|
|
185
|
+
const gitignorePath = join(resolve(projectDir), ".gitignore");
|
|
186
|
+
|
|
187
|
+
// Check if .gitignore exists
|
|
188
|
+
if (!existsSync(gitignorePath)) {
|
|
189
|
+
// Create new .gitignore with .jack/ entry
|
|
190
|
+
const content = `${GITIGNORE_COMMENT}\n${GITIGNORE_ENTRY}\n`;
|
|
191
|
+
await writeFile(gitignorePath, content);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Read existing .gitignore
|
|
196
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
197
|
+
|
|
198
|
+
// Check if .jack/ is already present (with or without trailing slash)
|
|
199
|
+
const lines = content.split("\n");
|
|
200
|
+
const hasJackEntry = lines.some((line) => {
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
return trimmed === ".jack" || trimmed === ".jack/";
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (hasJackEntry) {
|
|
206
|
+
return; // Already gitignored
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Append .jack/ entry
|
|
210
|
+
const needsNewline = content.length > 0 && !content.endsWith("\n");
|
|
211
|
+
const toAppend = `${needsNewline ? "\n" : ""}\n${GITIGNORE_COMMENT}\n${GITIGNORE_ENTRY}\n`;
|
|
212
|
+
await appendFile(gitignorePath, toAppend);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Write template metadata to .jack/template.json
|
|
217
|
+
*/
|
|
218
|
+
export async function writeTemplateMetadata(
|
|
219
|
+
projectDir: string,
|
|
220
|
+
template: TemplateMetadata,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const jackDir = getJackDir(projectDir);
|
|
223
|
+
const templatePath = getTemplatePath(projectDir);
|
|
224
|
+
|
|
225
|
+
// Create .jack directory if it doesn't exist
|
|
226
|
+
if (!existsSync(jackDir)) {
|
|
227
|
+
await mkdir(jackDir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await writeFile(templatePath, JSON.stringify(template, null, 2));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read template metadata from .jack/template.json
|
|
235
|
+
* Returns null if no template.json exists or if it's invalid.
|
|
236
|
+
*/
|
|
237
|
+
export async function readTemplateMetadata(projectDir: string): Promise<TemplateMetadata | null> {
|
|
238
|
+
const templatePath = getTemplatePath(projectDir);
|
|
239
|
+
|
|
240
|
+
if (!existsSync(templatePath)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const content = await readFile(templatePath, "utf-8");
|
|
246
|
+
const template = JSON.parse(content) as TemplateMetadata;
|
|
247
|
+
|
|
248
|
+
// Validate required fields
|
|
249
|
+
if (!template.type || !template.name) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return template;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Update the project link with partial data (e.g., after deploy)
|
|
261
|
+
*/
|
|
262
|
+
export async function updateProjectLink(
|
|
263
|
+
projectDir: string,
|
|
264
|
+
updates: Partial<Omit<LocalProjectLink, "version">>,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const existing = await readProjectLink(projectDir);
|
|
267
|
+
|
|
268
|
+
if (!existing) {
|
|
269
|
+
throw new Error("Project is not linked. Use linkProject() first.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const updated: LocalProjectLink = {
|
|
273
|
+
...existing,
|
|
274
|
+
...updates,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const linkPath = getProjectLinkPath(projectDir);
|
|
278
|
+
await writeFile(linkPath, JSON.stringify(updated, null, 2));
|
|
279
|
+
}
|