@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
@@ -1,8 +1,23 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, resolve } from "node:path";
3
+ import { resolve } from "node:path";
4
4
  import { promptSelect } from "../lib/hooks.ts";
5
5
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
+ import {
7
+ type ProjectListItem,
8
+ STATUS_ICONS,
9
+ buildTagColorMap,
10
+ colors,
11
+ filterByStatus,
12
+ filterByTag,
13
+ formatCloudSection,
14
+ formatErrorSection,
15
+ formatLocalSection,
16
+ formatTagsInline,
17
+ groupProjects,
18
+ sortByUpdated,
19
+ toListItems,
20
+ } from "../lib/project-list.ts";
6
21
  import {
7
22
  cleanupStaleProjects,
8
23
  getProjectStatus,
@@ -51,133 +66,181 @@ export default async function projects(subcommand?: string, args: string[] = [])
51
66
  }
52
67
  }
53
68
 
69
+ /**
70
+ * Extract a flag value from args (e.g., --status live -> "live")
71
+ */
72
+ function extractFlagValue(args: string[], flag: string): string | null {
73
+ const idx = args.indexOf(flag);
74
+ if (idx !== -1 && idx + 1 < args.length) {
75
+ return args[idx + 1] ?? null;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Extract multiple flag values from args (e.g., --tag api --tag prod -> ["api", "prod"])
82
+ */
83
+ function extractFlagValues(args: string[], flag: string): string[] {
84
+ const values: string[] = [];
85
+ for (let i = 0; i < args.length; i++) {
86
+ if (args[i] === flag && i + 1 < args.length) {
87
+ const value = args[i + 1];
88
+ if (value) values.push(value);
89
+ }
90
+ }
91
+ return values;
92
+ }
93
+
54
94
  /**
55
95
  * List all projects with status indicators
56
96
  */
57
- async function listProjects(_args: string[]): Promise<void> {
97
+ async function listProjects(args: string[]): Promise<void> {
98
+ // Parse flags
99
+ const showAll = args.includes("--all") || args.includes("-a");
100
+ const statusFilter = extractFlagValue(args, "--status");
101
+ const tagFilters = extractFlagValues(args, "--tag");
102
+ const jsonOutput = args.includes("--json");
103
+ const localOnly = args.includes("--local");
104
+ const cloudOnly = args.includes("--cloud");
105
+
58
106
  // Fetch all projects from registry and control plane
59
107
  outputSpinner.start("Checking project status...");
60
108
  const projects: ResolvedProject[] = await listAllProjects();
61
109
  outputSpinner.stop();
62
110
 
63
- if (projects.length === 0) {
64
- info("No projects found");
65
- info("Create a project with: jack new <name>");
66
- return;
67
- }
111
+ // Convert to list items
112
+ let items = toListItems(projects);
68
113
 
69
- // Separate local projects from cloud-only projects
70
- const localProjects: ResolvedProject[] = [];
71
- const cloudOnlyProjects: ResolvedProject[] = [];
114
+ // Apply filters
115
+ if (statusFilter) items = filterByStatus(items, statusFilter);
116
+ if (localOnly) items = items.filter((i) => i.isLocal);
117
+ if (cloudOnly) items = items.filter((i) => i.isCloudOnly);
118
+ if (tagFilters.length > 0) items = filterByTag(items, tagFilters);
72
119
 
73
- for (const proj of projects) {
74
- if (proj.localPath && proj.sources.filesystem) {
75
- localProjects.push(proj);
120
+ // Handle empty state
121
+ if (items.length === 0) {
122
+ if (jsonOutput) {
123
+ console.log("[]");
124
+ return;
125
+ }
126
+ info("No projects found");
127
+ if (statusFilter || localOnly || cloudOnly || tagFilters.length > 0) {
128
+ info("Try removing filters to see all projects");
76
129
  } else {
77
- cloudOnlyProjects.push(proj);
130
+ info("Create a project with: jack new <name>");
78
131
  }
132
+ return;
79
133
  }
80
134
 
81
- // Group local projects by parent directory
82
- interface DirectoryGroup {
83
- displayPath: string;
84
- projects: ResolvedProject[];
135
+ // JSON output to stdout (pipeable)
136
+ if (jsonOutput) {
137
+ console.log(JSON.stringify(items, null, 2));
138
+ return;
85
139
  }
86
140
 
87
- const groups = new Map<string, DirectoryGroup>();
88
- const home = homedir();
89
-
90
- for (const proj of localProjects) {
91
- if (!proj.localPath) continue;
92
- const parent = dirname(proj.localPath);
93
- if (!groups.has(parent)) {
94
- // Replace home directory with ~ for display
95
- const displayPath = parent.startsWith(home) ? `~${parent.slice(home.length)}` : parent;
96
- groups.set(parent, { displayPath, projects: [] });
97
- }
98
- groups.get(parent)?.projects.push(proj);
141
+ // Flat table for --all mode
142
+ if (showAll) {
143
+ renderFlatTable(items);
144
+ return;
99
145
  }
100
146
 
101
- // Display header
102
- console.error("");
103
- info("Your projects");
104
- console.error("");
147
+ // Default: grouped view
148
+ renderGroupedView(items);
149
+ }
105
150
 
106
- // Display local project groups
107
- for (const [_parentPath, group] of groups) {
108
- console.error(` ${colors.dim}${group.displayPath}/${colors.reset}`);
109
- const sortedProjects = group.projects.sort((a, b) => a.name.localeCompare(b.name));
151
+ /**
152
+ * Render the grouped view (default)
153
+ */
154
+ function renderGroupedView(items: ProjectListItem[]): void {
155
+ const groups = groupProjects(items);
156
+ const total = items.length;
110
157
 
111
- for (let i = 0; i < sortedProjects.length; i++) {
112
- const proj = sortedProjects[i];
113
- if (!proj) continue;
114
- const isLast = i === sortedProjects.length - 1;
115
- const prefix = isLast ? "└──" : "├──";
158
+ // Build consistent tag color map across all projects
159
+ const tagColorMap = buildTagColorMap(items);
116
160
 
117
- const statusBadge = buildStatusBadge(proj);
118
- console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${statusBadge}`);
119
- }
161
+ console.error("");
162
+ info(`${total} projects`);
163
+
164
+ // Section 1: Errors (always show all)
165
+ if (groups.errors.length > 0) {
120
166
  console.error("");
167
+ console.error(formatErrorSection(groups.errors, { tagColorMap }));
121
168
  }
122
169
 
123
- // Display cloud-only projects
124
- if (cloudOnlyProjects.length > 0) {
125
- console.error(` ${colors.dim}On jack cloud (no local files)${colors.reset}`);
126
- const sortedCloudProjects = cloudOnlyProjects.sort((a, b) => a.name.localeCompare(b.name));
127
-
128
- for (let i = 0; i < sortedCloudProjects.length; i++) {
129
- const proj = sortedCloudProjects[i];
130
- if (!proj) continue;
131
- const isLast = i === sortedCloudProjects.length - 1;
132
- const prefix = isLast ? "└──" : "├──";
133
-
134
- const statusBadge = buildStatusBadge(proj);
135
- console.error(` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${statusBadge}`);
136
- }
170
+ // Section 2: Local projects (grouped by parent dir)
171
+ if (groups.local.length > 0) {
137
172
  console.error("");
173
+ console.error(
174
+ ` ${colors.dim}${STATUS_ICONS["local-only"]} Local (${groups.local.length})${colors.reset}`,
175
+ );
176
+ console.error(formatLocalSection(groups.local, { tagColorMap }));
138
177
  }
139
178
 
140
- // Summary
141
- const liveCount = projects.filter((p) => p.status === "live").length;
142
- const localOnlyCount = projects.filter((p) => p.status === "local-only").length;
143
- const errorCount = projects.filter((p) => p.status === "error").length;
179
+ // Section 3: Cloud-only (show last N by updatedAt)
180
+ if (groups.cloudOnly.length > 0) {
181
+ const CLOUD_LIMIT = 5;
182
+ const sorted = sortByUpdated(groups.cloudOnly);
144
183
 
145
- const parts: string[] = [];
146
- if (liveCount > 0) parts.push(`${liveCount} live`);
147
- if (localOnlyCount > 0) parts.push(`${localOnlyCount} local-only`);
148
- if (errorCount > 0) parts.push(`${errorCount} error`);
184
+ console.error("");
185
+ console.error(
186
+ formatCloudSection(sorted, {
187
+ limit: CLOUD_LIMIT,
188
+ total: groups.cloudOnly.length,
189
+ tagColorMap,
190
+ }),
191
+ );
192
+ }
149
193
 
150
- const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
151
- info(`${projects.length} projects${summary}`);
194
+ // Footer hint
195
+ console.error("");
196
+ info("jack ls --all for full list, --status error to filter");
152
197
  console.error("");
153
198
  }
154
199
 
155
- // Color codes
156
- const colors = {
157
- reset: "\x1b[0m",
158
- dim: "\x1b[90m",
159
- green: "\x1b[32m",
160
- yellow: "\x1b[33m",
161
- red: "\x1b[31m",
162
- cyan: "\x1b[36m",
163
- };
164
-
165
200
  /**
166
- * Build a user-friendly status badge for a project
201
+ * Render flat table (for --all mode)
167
202
  */
168
- function buildStatusBadge(project: ResolvedProject): string {
169
- switch (project.status) {
170
- case "live":
171
- return `${colors.green}[live]${colors.reset} ${colors.cyan}${project.url || ""}${colors.reset}`;
172
- case "local-only":
173
- return `${colors.dim}[local only]${colors.reset}`;
174
- case "error":
175
- return `${colors.red}[error]${colors.reset} ${project.errorMessage || "deployment failed"}`;
176
- case "syncing":
177
- return `${colors.yellow}[syncing]${colors.reset}`;
178
- default:
179
- return "";
203
+ function renderFlatTable(items: ProjectListItem[]): void {
204
+ // Sort: errors first, then by name
205
+ const sorted = [...items].sort((a, b) => {
206
+ if (a.status === "error" && b.status !== "error") return -1;
207
+ if (a.status !== "error" && b.status === "error") return 1;
208
+ return a.name.localeCompare(b.name);
209
+ });
210
+
211
+ // Build consistent tag color map
212
+ const tagColorMap = buildTagColorMap(items);
213
+
214
+ console.error("");
215
+ info(`${items.length} projects`);
216
+ console.error("");
217
+
218
+ // Header
219
+ console.error(` ${colors.dim}${"NAME".padEnd(22)} ${"STATUS".padEnd(12)} URL${colors.reset}`);
220
+
221
+ // Rows
222
+ for (const item of sorted) {
223
+ const icon = STATUS_ICONS[item.status] || "?";
224
+ const statusColor =
225
+ item.status === "error" || item.status === "auth-expired"
226
+ ? colors.red
227
+ : item.status === "live"
228
+ ? colors.green
229
+ : item.status === "syncing"
230
+ ? colors.yellow
231
+ : colors.dim;
232
+
233
+ const name = item.name.slice(0, 20).padEnd(22);
234
+ const tags = formatTagsInline(item.tags, tagColorMap);
235
+ const status = item.status.padEnd(12);
236
+ const url = item.url ? item.url.replace("https://", "") : "\u2014"; // em-dash
237
+
238
+ console.error(
239
+ ` ${statusColor}${icon}${colors.reset} ${name}${tags ? ` ${tags}` : ""} ${statusColor}${status}${colors.reset} ${url}`,
240
+ );
180
241
  }
242
+
243
+ console.error("");
181
244
  }
182
245
 
183
246
  /**
@@ -368,8 +431,8 @@ async function removeProjectEntry(args: string[]): Promise<void> {
368
431
 
369
432
  // Show where we'll remove from
370
433
  const locations: string[] = [];
371
- if (project.sources.registry) {
372
- locations.push("local registry");
434
+ if (project.sources.filesystem) {
435
+ locations.push("local project");
373
436
  }
374
437
  if (project.sources.controlPlane) {
375
438
  locations.push("jack cloud");
@@ -447,16 +510,15 @@ async function scanProjects(args: string[]): Promise<void> {
447
510
 
448
511
  outputSpinner.start(`Scanning ${targetDir} for jack projects...`);
449
512
 
450
- const { scanDirectoryForProjects, registerDiscoveredProjects } = await import(
451
- "../lib/local-paths.ts"
452
- );
513
+ const { scanAndRegisterProjects } = await import("../lib/paths-index.ts");
453
514
 
454
- const discovered = await scanDirectoryForProjects(absoluteDir);
515
+ // scanAndRegisterProjects both discovers and registers projects
516
+ const discovered = await scanAndRegisterProjects(absoluteDir);
455
517
  outputSpinner.stop();
456
518
 
457
519
  if (discovered.length === 0) {
458
- info("No jack projects found");
459
- info("Projects must have a wrangler.toml or wrangler.jsonc file");
520
+ info("No linked jack projects found");
521
+ info("Projects must have a .jack/project.json file");
460
522
  return;
461
523
  }
462
524
 
@@ -467,17 +529,16 @@ async function scanProjects(args: string[]): Promise<void> {
467
529
  for (let i = 0; i < discovered.length; i++) {
468
530
  const proj = discovered[i];
469
531
  if (!proj) continue;
532
+ // Extract project name from path
533
+ const projectName = proj.path.split("/").pop() || proj.projectId;
470
534
  const displayPath = proj.path.startsWith(home) ? `~${proj.path.slice(home.length)}` : proj.path;
471
535
  const isLast = i === discovered.length - 1;
472
536
  const prefix = isLast ? "└──" : "├──";
473
537
  console.error(
474
- ` ${colors.dim}${prefix}${colors.reset} ${proj.name} ${colors.dim}${displayPath}${colors.reset}`,
538
+ ` ${colors.dim}${prefix}${colors.reset} ${projectName} ${colors.dim}${displayPath}${colors.reset}`,
475
539
  );
476
540
  }
477
541
 
478
- // Register all discovered projects
479
- await registerDiscoveredProjects(discovered);
480
-
481
542
  console.error("");
482
543
  success(`Registered ${discovered.length} local path(s)`);
483
544
  }
@@ -11,7 +11,7 @@ import { $ } from "bun";
11
11
  import { getControlApiUrl } from "../lib/control-plane.ts";
12
12
  import { JackError, JackErrorCode } from "../lib/errors.ts";
13
13
  import { error, info, output, success, warn } from "../lib/output.ts";
14
- import { type Project, getProject } from "../lib/registry.ts";
14
+ import { type LocalProjectLink, readProjectLink } from "../lib/project-link.ts";
15
15
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
16
16
 
17
17
  interface SecretsOptions {
@@ -68,7 +68,7 @@ function showHelp(): void {
68
68
  */
69
69
  async function resolveProjectContext(options: SecretsOptions): Promise<{
70
70
  projectName: string;
71
- project: Project | null;
71
+ link: LocalProjectLink | null;
72
72
  isManaged: boolean;
73
73
  projectId: string | null;
74
74
  }> {
@@ -86,11 +86,12 @@ async function resolveProjectContext(options: SecretsOptions): Promise<{
86
86
  }
87
87
  }
88
88
 
89
- const project = await getProject(projectName);
90
- const isManaged = project?.deploy_mode === "managed";
91
- const projectId = project?.remote?.project_id ?? null;
89
+ // Read deploy mode from .jack/project.json
90
+ const link = await readProjectLink(process.cwd());
91
+ const isManaged = link?.deploy_mode === "managed";
92
+ const projectId = link?.project_id ?? null;
92
93
 
93
- return { projectName, project, isManaged, projectId };
94
+ return { projectName, link, isManaged, projectId };
94
95
  }
95
96
 
96
97
  /**
@@ -3,7 +3,7 @@ import { fetchProjectResources } from "../lib/control-plane.ts";
3
3
  import { formatSize } from "../lib/format.ts";
4
4
  import { promptSelect } from "../lib/hooks.ts";
5
5
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
- import { getProject } from "../lib/registry.ts";
6
+ import { readProjectLink } from "../lib/project-link.ts";
7
7
  import { parseWranglerResources } from "../lib/resources.ts";
8
8
  import {
9
9
  deleteDatabase,
@@ -43,12 +43,13 @@ async function ensureLocalProjectContext(projectName: string): Promise<void> {
43
43
  * For BYO: parse from wrangler.jsonc
44
44
  */
45
45
  async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabaseInfo | null> {
46
- const project = await getProject(projectName);
46
+ // Read deploy mode from .jack/project.json
47
+ const link = await readProjectLink(process.cwd());
47
48
 
48
49
  // For managed projects, fetch from control plane
49
- if (project?.deploy_mode === "managed" && project.remote?.project_id) {
50
+ if (link?.deploy_mode === "managed") {
50
51
  try {
51
- const resources = await fetchProjectResources(project.remote.project_id);
52
+ const resources = await fetchProjectResources(link.project_id);
52
53
  const d1 = resources.find((r) => r.resource_type === "d1");
53
54
  if (d1) {
54
55
  return {