@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.
Files changed (54) hide show
  1. package/package.json +2 -6
  2. package/src/commands/agents.ts +9 -24
  3. package/src/commands/clone.ts +27 -0
  4. package/src/commands/down.ts +31 -57
  5. package/src/commands/feedback.ts +4 -5
  6. package/src/commands/link.ts +147 -0
  7. package/src/commands/login.ts +124 -1
  8. package/src/commands/logs.ts +8 -18
  9. package/src/commands/new.ts +7 -1
  10. package/src/commands/projects.ts +166 -105
  11. package/src/commands/secrets.ts +7 -6
  12. package/src/commands/services.ts +5 -4
  13. package/src/commands/tag.ts +282 -0
  14. package/src/commands/unlink.ts +30 -0
  15. package/src/index.ts +46 -1
  16. package/src/lib/auth/index.ts +2 -0
  17. package/src/lib/auth/store.ts +26 -2
  18. package/src/lib/binding-validator.ts +4 -13
  19. package/src/lib/build-helper.ts +93 -5
  20. package/src/lib/control-plane.ts +137 -0
  21. package/src/lib/deploy-mode.ts +1 -1
  22. package/src/lib/managed-deploy.ts +11 -1
  23. package/src/lib/managed-down.ts +7 -20
  24. package/src/lib/paths-index.test.ts +546 -0
  25. package/src/lib/paths-index.ts +310 -0
  26. package/src/lib/project-link.test.ts +459 -0
  27. package/src/lib/project-link.ts +279 -0
  28. package/src/lib/project-list.test.ts +581 -0
  29. package/src/lib/project-list.ts +449 -0
  30. package/src/lib/project-operations.ts +304 -183
  31. package/src/lib/project-resolver.ts +191 -211
  32. package/src/lib/tags.ts +389 -0
  33. package/src/lib/telemetry.ts +86 -157
  34. package/src/lib/zip-packager.ts +9 -0
  35. package/src/templates/index.ts +5 -3
  36. package/templates/api/.jack/template.json +4 -0
  37. package/templates/hello/.jack/template.json +4 -0
  38. package/templates/miniapp/.jack/template.json +4 -0
  39. package/templates/nextjs/.jack.json +28 -0
  40. package/templates/nextjs/app/globals.css +9 -0
  41. package/templates/nextjs/app/layout.tsx +19 -0
  42. package/templates/nextjs/app/page.tsx +8 -0
  43. package/templates/nextjs/bun.lock +2232 -0
  44. package/templates/nextjs/cloudflare-env.d.ts +3 -0
  45. package/templates/nextjs/next-env.d.ts +6 -0
  46. package/templates/nextjs/next.config.ts +8 -0
  47. package/templates/nextjs/open-next.config.ts +6 -0
  48. package/templates/nextjs/package.json +24 -0
  49. package/templates/nextjs/public/_headers +2 -0
  50. package/templates/nextjs/tsconfig.json +44 -0
  51. package/templates/nextjs/wrangler.jsonc +17 -0
  52. package/src/lib/local-paths.test.ts +0 -902
  53. package/src/lib/local-paths.ts +0 -258
  54. 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
+ }