@getjack/jack 0.1.3 → 0.1.5

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 (55) hide show
  1. package/README.md +103 -0
  2. package/package.json +2 -6
  3. package/src/commands/agents.ts +9 -24
  4. package/src/commands/clone.ts +27 -0
  5. package/src/commands/down.ts +31 -57
  6. package/src/commands/feedback.ts +4 -5
  7. package/src/commands/link.ts +147 -0
  8. package/src/commands/logs.ts +8 -18
  9. package/src/commands/new.ts +7 -1
  10. package/src/commands/projects.ts +162 -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 +48 -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 +445 -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 +81 -168
  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/isr-test/page.tsx +22 -0
  42. package/templates/nextjs/app/layout.tsx +19 -0
  43. package/templates/nextjs/app/page.tsx +8 -0
  44. package/templates/nextjs/bun.lock +2232 -0
  45. package/templates/nextjs/cloudflare-env.d.ts +3 -0
  46. package/templates/nextjs/next-env.d.ts +6 -0
  47. package/templates/nextjs/next.config.ts +8 -0
  48. package/templates/nextjs/open-next.config.ts +6 -0
  49. package/templates/nextjs/package.json +24 -0
  50. package/templates/nextjs/public/_headers +2 -0
  51. package/templates/nextjs/tsconfig.json +44 -0
  52. package/templates/nextjs/wrangler.jsonc +17 -0
  53. package/src/lib/local-paths.test.ts +0 -902
  54. package/src/lib/local-paths.ts +0 -258
  55. package/src/lib/registry.ts +0 -181
@@ -1,258 +0,0 @@
1
- /**
2
- * Local Paths Index
3
- *
4
- * Tracks where projects live on the local filesystem.
5
- * This is a cache - can be rebuilt by scanning directories.
6
- *
7
- * Design:
8
- * - One project can have multiple local paths (forks, copies)
9
- * - Paths are verified on read (deleted dirs are pruned)
10
- * - Auto-registered when jack commands run from project dirs
11
- */
12
-
13
- import { existsSync } from "node:fs";
14
- import { readdir } from "node:fs/promises";
15
- import { join, resolve } from "node:path";
16
- import { CONFIG_DIR } from "./config.ts";
17
-
18
- /**
19
- * Local paths index structure
20
- */
21
- export interface LocalPathsIndex {
22
- version: 1;
23
- /** Map of project name -> array of local paths */
24
- paths: Record<string, string[]>;
25
- /** Last time the index was updated */
26
- updatedAt: string;
27
- }
28
-
29
- const INDEX_PATH = join(CONFIG_DIR, "local-paths.json");
30
-
31
- /** Directories to skip when scanning */
32
- const SKIP_DIRS = new Set([
33
- "node_modules",
34
- ".git",
35
- "dist",
36
- "build",
37
- ".next",
38
- ".nuxt",
39
- ".output",
40
- "coverage",
41
- ".turbo",
42
- ".cache",
43
- ]);
44
-
45
- /**
46
- * Check if a directory has a wrangler config file
47
- */
48
- function hasWranglerConfig(dir: string): boolean {
49
- return (
50
- existsSync(join(dir, "wrangler.jsonc")) ||
51
- existsSync(join(dir, "wrangler.toml")) ||
52
- existsSync(join(dir, "wrangler.json"))
53
- );
54
- }
55
-
56
- /**
57
- * Read the local paths index from disk
58
- */
59
- export async function readLocalPaths(): Promise<LocalPathsIndex> {
60
- if (!existsSync(INDEX_PATH)) {
61
- return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
62
- }
63
-
64
- try {
65
- return await Bun.file(INDEX_PATH).json();
66
- } catch {
67
- // Handle corrupted index file gracefully
68
- return { version: 1, paths: {}, updatedAt: new Date().toISOString() };
69
- }
70
- }
71
-
72
- /**
73
- * Write the local paths index to disk
74
- */
75
- export async function writeLocalPaths(index: LocalPathsIndex): Promise<void> {
76
- index.updatedAt = new Date().toISOString();
77
- await Bun.write(INDEX_PATH, JSON.stringify(index, null, 2));
78
- }
79
-
80
- /**
81
- * Register a local path for a project
82
- * Idempotent - won't add duplicates
83
- */
84
- export async function registerLocalPath(projectName: string, localPath: string): Promise<void> {
85
- const absolutePath = resolve(localPath);
86
- const index = await readLocalPaths();
87
-
88
- if (!index.paths[projectName]) {
89
- index.paths[projectName] = [];
90
- }
91
-
92
- // Avoid duplicates
93
- if (!index.paths[projectName].includes(absolutePath)) {
94
- index.paths[projectName].push(absolutePath);
95
- }
96
-
97
- await writeLocalPaths(index);
98
- }
99
-
100
- /**
101
- * Remove a local path for a project
102
- */
103
- export async function removeLocalPath(projectName: string, localPath: string): Promise<void> {
104
- const absolutePath = resolve(localPath);
105
- const index = await readLocalPaths();
106
-
107
- if (index.paths[projectName]) {
108
- index.paths[projectName] = index.paths[projectName].filter((p) => p !== absolutePath);
109
-
110
- // Clean up empty arrays
111
- if (index.paths[projectName].length === 0) {
112
- delete index.paths[projectName];
113
- }
114
- }
115
-
116
- await writeLocalPaths(index);
117
- }
118
-
119
- /**
120
- * Get all local paths for a project, verified to exist
121
- * Automatically prunes paths that no longer exist or lack wrangler config
122
- */
123
- export async function getLocalPaths(projectName: string): Promise<string[]> {
124
- const index = await readLocalPaths();
125
- const paths = index.paths[projectName] || [];
126
-
127
- // Verify paths exist and have wrangler config
128
- const validPaths: string[] = [];
129
- const invalidPaths: string[] = [];
130
-
131
- for (const path of paths) {
132
- if (hasWranglerConfig(path)) {
133
- validPaths.push(path);
134
- } else {
135
- invalidPaths.push(path);
136
- }
137
- }
138
-
139
- // Prune invalid paths
140
- if (invalidPaths.length > 0) {
141
- index.paths[projectName] = validPaths;
142
- if (validPaths.length === 0) {
143
- delete index.paths[projectName];
144
- }
145
- await writeLocalPaths(index);
146
- }
147
-
148
- return validPaths;
149
- }
150
-
151
- /**
152
- * Get all local paths for all projects, verified to exist
153
- * Returns a map of projectName -> paths[]
154
- */
155
- export async function getAllLocalPaths(): Promise<Record<string, string[]>> {
156
- const index = await readLocalPaths();
157
- const result: Record<string, string[]> = {};
158
- let needsWrite = false;
159
-
160
- for (const [projectName, paths] of Object.entries(index.paths)) {
161
- const validPaths: string[] = [];
162
-
163
- for (const path of paths) {
164
- if (hasWranglerConfig(path)) {
165
- validPaths.push(path);
166
- } else {
167
- needsWrite = true;
168
- }
169
- }
170
-
171
- if (validPaths.length > 0) {
172
- result[projectName] = validPaths;
173
- } else if (paths.length > 0) {
174
- needsWrite = true;
175
- }
176
- }
177
-
178
- // Write back pruned index if needed
179
- if (needsWrite) {
180
- index.paths = result;
181
- await writeLocalPaths(index);
182
- }
183
-
184
- return result;
185
- }
186
-
187
- /**
188
- * Scan a directory recursively for jack projects
189
- * Returns discovered projects with their paths
190
- */
191
- export async function scanDirectoryForProjects(
192
- rootDir: string,
193
- maxDepth = 3,
194
- ): Promise<Array<{ name: string; path: string }>> {
195
- const { getProjectNameFromDir } = await import("./storage/index.ts");
196
- const discovered: Array<{ name: string; path: string }> = [];
197
- const absoluteRoot = resolve(rootDir);
198
-
199
- async function scan(dir: string, depth: number): Promise<void> {
200
- if (depth > maxDepth) return;
201
-
202
- // Check if this directory is a jack project
203
- try {
204
- const name = await getProjectNameFromDir(dir);
205
- discovered.push({ name, path: dir });
206
- return; // Don't scan subdirectories of projects
207
- } catch {
208
- // Not a project, continue scanning subdirectories
209
- }
210
-
211
- // Scan subdirectories
212
- try {
213
- const entries = await readdir(dir, { withFileTypes: true });
214
-
215
- for (const entry of entries) {
216
- // Skip non-directories
217
- if (!entry.isDirectory()) continue;
218
-
219
- // Skip hidden directories and common non-project directories
220
- if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
221
- continue;
222
- }
223
-
224
- const fullPath = join(dir, entry.name);
225
- await scan(fullPath, depth + 1);
226
- }
227
- } catch {
228
- // Permission denied or other error, skip silently
229
- }
230
- }
231
-
232
- await scan(absoluteRoot, 0);
233
- return discovered;
234
- }
235
-
236
- /**
237
- * Register multiple discovered projects
238
- * More efficient than calling registerLocalPath for each project
239
- */
240
- export async function registerDiscoveredProjects(
241
- projects: Array<{ name: string; path: string }>,
242
- ): Promise<void> {
243
- const index = await readLocalPaths();
244
-
245
- for (const { name, path } of projects) {
246
- const absolutePath = resolve(path);
247
-
248
- if (!index.paths[name]) {
249
- index.paths[name] = [];
250
- }
251
-
252
- if (!index.paths[name].includes(absolutePath)) {
253
- index.paths[name].push(absolutePath);
254
- }
255
- }
256
-
257
- await writeLocalPaths(index);
258
- }
@@ -1,181 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { CONFIG_DIR } from "./config.ts";
4
-
5
- /**
6
- * Deploy mode for a project
7
- */
8
- export type DeployMode = "managed" | "byo";
9
-
10
- /**
11
- * Template origin tracking for agent file regeneration
12
- */
13
- export interface TemplateOrigin {
14
- type: "builtin" | "github";
15
- name: string; // "miniapp", "api", or "user/repo"
16
- }
17
-
18
- /**
19
- * Remote metadata for managed projects
20
- */
21
- export interface ManagedRemote {
22
- project_id: string;
23
- project_slug: string;
24
- org_id: string;
25
- runjack_url: string;
26
- }
27
-
28
- /**
29
- * Project data stored in registry
30
- */
31
- export interface Project {
32
- workerUrl: string | null;
33
- createdAt: string;
34
- lastDeployed: string | null;
35
- status?: "created" | "build_failed" | "live";
36
- cloudflare?: {
37
- accountId: string;
38
- workerId: string;
39
- };
40
- template?: TemplateOrigin;
41
- deploy_mode?: DeployMode;
42
- remote?: ManagedRemote;
43
- }
44
-
45
- /**
46
- * Project registry structure
47
- */
48
- export interface Registry {
49
- version: 2;
50
- projects: Record<string, Project>;
51
- }
52
-
53
- export const REGISTRY_PATH = join(CONFIG_DIR, "projects.json");
54
-
55
- /**
56
- * Migrate registry from v1 to v2
57
- * - Removes localPath field (no longer tracked)
58
- * - Removes resources.services.db field (fetch from control plane instead)
59
- */
60
- async function migrateV1ToV2(v1Registry: {
61
- version: 1;
62
- projects: Record<string, unknown>;
63
- }): Promise<Registry> {
64
- const migrated: Registry = {
65
- version: 2,
66
- projects: {},
67
- };
68
-
69
- for (const [name, project] of Object.entries(v1Registry.projects)) {
70
- const p = project as Record<string, unknown>;
71
- // Remove localPath and resources, keep everything else
72
- const { localPath, resources, ...rest } = p;
73
- migrated.projects[name] = rest as unknown as Project;
74
- }
75
-
76
- return migrated;
77
- }
78
-
79
- /**
80
- * Read project registry from disk
81
- */
82
- export async function readRegistry(): Promise<Registry> {
83
- if (!existsSync(REGISTRY_PATH)) {
84
- return { version: 2, projects: {} };
85
- }
86
-
87
- try {
88
- const raw = await Bun.file(REGISTRY_PATH).json();
89
-
90
- // Auto-migrate v1 to v2
91
- if (raw.version === 1) {
92
- const migrated = await migrateV1ToV2(raw);
93
- await writeRegistry(migrated);
94
- return migrated;
95
- }
96
-
97
- // Handle unversioned (legacy) registries
98
- if (!raw.version) {
99
- const migrated = await migrateV1ToV2({
100
- version: 1,
101
- projects: raw.projects || {},
102
- });
103
- await writeRegistry(migrated);
104
- return migrated;
105
- }
106
-
107
- return raw as Registry;
108
- } catch {
109
- return { version: 2, projects: {} };
110
- }
111
- }
112
-
113
- /**
114
- * Write project registry to disk
115
- */
116
- export async function writeRegistry(registry: Registry): Promise<void> {
117
- await Bun.write(REGISTRY_PATH, JSON.stringify(registry, null, 2));
118
- }
119
-
120
- /**
121
- * Register or update a project in the registry
122
- */
123
- export async function registerProject(name: string, data: Partial<Project>): Promise<void> {
124
- const registry = await readRegistry();
125
- const existing = registry.projects[name];
126
-
127
- if (existing) {
128
- registry.projects[name] = {
129
- ...existing,
130
- ...data,
131
- };
132
- } else {
133
- registry.projects[name] = data as Project;
134
- }
135
-
136
- await writeRegistry(registry);
137
- }
138
-
139
- /**
140
- * Update project with partial data
141
- */
142
- export async function updateProject(name: string, data: Partial<Project>): Promise<void> {
143
- const registry = await readRegistry();
144
- const existing = registry.projects[name];
145
-
146
- if (!existing) {
147
- throw new Error(`Project "${name}" not found in registry`);
148
- }
149
-
150
- registry.projects[name] = {
151
- ...existing,
152
- ...data,
153
- };
154
-
155
- await writeRegistry(registry);
156
- }
157
-
158
- /**
159
- * Remove project from registry
160
- */
161
- export async function removeProject(name: string): Promise<void> {
162
- const registry = await readRegistry();
163
- delete registry.projects[name];
164
- await writeRegistry(registry);
165
- }
166
-
167
- /**
168
- * Get single project from registry
169
- */
170
- export async function getProject(name: string): Promise<Project | null> {
171
- const registry = await readRegistry();
172
- return registry.projects[name] ?? null;
173
- }
174
-
175
- /**
176
- * Get all projects from registry
177
- */
178
- export async function getAllProjects(): Promise<Record<string, Project>> {
179
- const registry = await readRegistry();
180
- return registry.projects;
181
- }