@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,310 @@
1
+ /**
2
+ * Paths Index
3
+ *
4
+ * Tracks where projects live locally, keyed by project_id (not name).
5
+ * This is a lightweight discovery index that can be rebuilt by scanning.
6
+ *
7
+ * Design:
8
+ * - Keyed by project_id for stability (names can collide/change)
9
+ * - Array of paths per project (one project can have multiple local copies)
10
+ * - Auto-pruned on read (invalid paths removed)
11
+ * - Rebuildable via scanAndRegisterProjects()
12
+ */
13
+
14
+ import { existsSync } from "node:fs";
15
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
16
+ import { join, resolve } from "node:path";
17
+ import { CONFIG_DIR } from "./config.ts";
18
+ import { type DeployMode, getJackDir, readProjectLink } from "./project-link.ts";
19
+
20
+ /**
21
+ * Paths index structure stored in ~/.config/jack/paths.json
22
+ */
23
+ export interface PathsIndex {
24
+ version: 1;
25
+ /** Map of project_id -> array of local paths */
26
+ paths: Record<string, string[]>;
27
+ /** Last time the index was updated */
28
+ updatedAt: string;
29
+ }
30
+
31
+ const INDEX_PATH = join(CONFIG_DIR, "paths.json");
32
+
33
+ /** Directories to skip when scanning */
34
+ const SKIP_DIRS = new Set([
35
+ "node_modules",
36
+ ".git",
37
+ "dist",
38
+ "build",
39
+ ".next",
40
+ ".nuxt",
41
+ ".output",
42
+ "coverage",
43
+ ".turbo",
44
+ ".cache",
45
+ ".venv",
46
+ "venv",
47
+ "__pycache__",
48
+ ".idea",
49
+ ".vscode",
50
+ ]);
51
+
52
+ /**
53
+ * Ensure config directory exists
54
+ */
55
+ async function ensureConfigDir(): Promise<void> {
56
+ if (!existsSync(CONFIG_DIR)) {
57
+ await mkdir(CONFIG_DIR, { recursive: true });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Read the paths index from disk
63
+ */
64
+ export async function readPathsIndex(): Promise<PathsIndex> {
65
+ if (!existsSync(INDEX_PATH)) {
66
+ return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
67
+ }
68
+
69
+ try {
70
+ const content = await readFile(INDEX_PATH, "utf-8");
71
+ return JSON.parse(content) as PathsIndex;
72
+ } catch {
73
+ // Handle corrupted index file gracefully
74
+ return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Write the paths index to disk
80
+ */
81
+ export async function writePathsIndex(index: PathsIndex): Promise<void> {
82
+ await ensureConfigDir();
83
+ index.updatedAt = new Date().toISOString();
84
+ await writeFile(INDEX_PATH, JSON.stringify(index, null, 2));
85
+ }
86
+
87
+ /**
88
+ * Check if a path has a valid .jack/project.json with matching project ID
89
+ */
90
+ async function isValidProjectPath(projectId: string, path: string): Promise<boolean> {
91
+ const link = await readProjectLink(path);
92
+ return link !== null && link.project_id === projectId;
93
+ }
94
+
95
+ /**
96
+ * Register a local path for a project (by ID)
97
+ * Idempotent - won't add duplicates
98
+ */
99
+ export async function registerPath(projectId: string, localPath: string): Promise<void> {
100
+ const absolutePath = resolve(localPath);
101
+ const index = await readPathsIndex();
102
+
103
+ if (!index.paths[projectId]) {
104
+ index.paths[projectId] = [];
105
+ }
106
+
107
+ // Avoid duplicates
108
+ if (!index.paths[projectId].includes(absolutePath)) {
109
+ index.paths[projectId].push(absolutePath);
110
+ }
111
+
112
+ await writePathsIndex(index);
113
+ }
114
+
115
+ /**
116
+ * Remove a local path for a project
117
+ */
118
+ export async function unregisterPath(projectId: string, localPath: string): Promise<void> {
119
+ const absolutePath = resolve(localPath);
120
+ const index = await readPathsIndex();
121
+
122
+ if (index.paths[projectId]) {
123
+ index.paths[projectId] = index.paths[projectId].filter((p) => p !== absolutePath);
124
+
125
+ // Clean up empty arrays
126
+ if (index.paths[projectId].length === 0) {
127
+ delete index.paths[projectId];
128
+ }
129
+ }
130
+
131
+ await writePathsIndex(index);
132
+ }
133
+
134
+ /**
135
+ * Get all local paths for a project, verified to exist.
136
+ * Auto-prunes paths where .jack/project.json is missing or has wrong project_id.
137
+ */
138
+ export async function getPathsForProject(projectId: string): Promise<string[]> {
139
+ const index = await readPathsIndex();
140
+ const paths = index.paths[projectId] || [];
141
+
142
+ const validPaths: string[] = [];
143
+ const invalidPaths: string[] = [];
144
+
145
+ for (const path of paths) {
146
+ if (await isValidProjectPath(projectId, path)) {
147
+ validPaths.push(path);
148
+ } else {
149
+ invalidPaths.push(path);
150
+ }
151
+ }
152
+
153
+ // Prune invalid paths
154
+ if (invalidPaths.length > 0) {
155
+ index.paths[projectId] = validPaths;
156
+ if (validPaths.length === 0) {
157
+ delete index.paths[projectId];
158
+ }
159
+ await writePathsIndex(index);
160
+ }
161
+
162
+ return validPaths;
163
+ }
164
+
165
+ /**
166
+ * Get all paths for all projects, verified to exist.
167
+ * Auto-prunes invalid paths across all projects.
168
+ */
169
+ export async function getAllPaths(): Promise<Record<string, string[]>> {
170
+ const index = await readPathsIndex();
171
+ const result: Record<string, string[]> = {};
172
+ let needsWrite = false;
173
+
174
+ for (const [projectId, paths] of Object.entries(index.paths)) {
175
+ const validPaths: string[] = [];
176
+
177
+ for (const path of paths) {
178
+ if (await isValidProjectPath(projectId, path)) {
179
+ validPaths.push(path);
180
+ } else {
181
+ needsWrite = true;
182
+ }
183
+ }
184
+
185
+ if (validPaths.length > 0) {
186
+ result[projectId] = validPaths;
187
+ } else if (paths.length > 0) {
188
+ needsWrite = true;
189
+ }
190
+ }
191
+
192
+ // Write back pruned index if needed
193
+ if (needsWrite) {
194
+ index.paths = result;
195
+ await writePathsIndex(index);
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ /**
202
+ * Information about a discovered project
203
+ */
204
+ export interface DiscoveredProject {
205
+ projectId: string;
206
+ path: string;
207
+ deployMode: DeployMode;
208
+ }
209
+
210
+ /**
211
+ * Scan a directory for Jack projects (.jack/project.json) and register them.
212
+ * Only finds linked projects, ignores directories without .jack/
213
+ */
214
+ export async function scanAndRegisterProjects(
215
+ rootDir: string,
216
+ maxDepth = 3,
217
+ ): Promise<DiscoveredProject[]> {
218
+ const discovered: DiscoveredProject[] = [];
219
+ const absoluteRoot = resolve(rootDir);
220
+
221
+ async function scan(dir: string, depth: number): Promise<void> {
222
+ if (depth > maxDepth) return;
223
+
224
+ // Check if this directory has a .jack/project.json
225
+ const link = await readProjectLink(dir);
226
+ if (link) {
227
+ discovered.push({
228
+ projectId: link.project_id,
229
+ path: dir,
230
+ deployMode: link.deploy_mode,
231
+ });
232
+ return; // Don't scan subdirectories of linked projects
233
+ }
234
+
235
+ // Scan subdirectories
236
+ try {
237
+ const entries = await readdir(dir, { withFileTypes: true });
238
+
239
+ for (const entry of entries) {
240
+ // Skip non-directories
241
+ if (!entry.isDirectory()) continue;
242
+
243
+ // Skip hidden directories and common non-project directories
244
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
245
+ continue;
246
+ }
247
+
248
+ const fullPath = join(dir, entry.name);
249
+ await scan(fullPath, depth + 1);
250
+ }
251
+ } catch {
252
+ // Permission denied or other error, skip silently
253
+ }
254
+ }
255
+
256
+ await scan(absoluteRoot, 0);
257
+
258
+ // Register all discovered projects
259
+ if (discovered.length > 0) {
260
+ await registerDiscoveredProjects(discovered);
261
+ }
262
+
263
+ return discovered;
264
+ }
265
+
266
+ /**
267
+ * Register multiple discovered projects efficiently.
268
+ * More efficient than calling registerPath for each project.
269
+ */
270
+ export async function registerDiscoveredProjects(projects: DiscoveredProject[]): Promise<void> {
271
+ const index = await readPathsIndex();
272
+
273
+ for (const { projectId, path } of projects) {
274
+ const absolutePath = resolve(path);
275
+
276
+ if (!index.paths[projectId]) {
277
+ index.paths[projectId] = [];
278
+ }
279
+
280
+ if (!index.paths[projectId].includes(absolutePath)) {
281
+ index.paths[projectId].push(absolutePath);
282
+ }
283
+ }
284
+
285
+ await writePathsIndex(index);
286
+ }
287
+
288
+ /**
289
+ * Find project ID by path (reverse lookup).
290
+ * Scans the index to find which project owns a given path.
291
+ */
292
+ export async function findProjectIdByPath(localPath: string): Promise<string | null> {
293
+ const absolutePath = resolve(localPath);
294
+ const index = await readPathsIndex();
295
+
296
+ for (const [projectId, paths] of Object.entries(index.paths)) {
297
+ if (paths.includes(absolutePath)) {
298
+ return projectId;
299
+ }
300
+ }
301
+
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * Get the index file path (for testing/debugging)
307
+ */
308
+ export function getIndexPath(): string {
309
+ return INDEX_PATH;
310
+ }