@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,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
|
+
}
|