@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
package/src/commands/projects.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
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(
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
130
|
+
info("Create a project with: jack new <name>");
|
|
78
131
|
}
|
|
132
|
+
return;
|
|
79
133
|
}
|
|
80
134
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
// JSON output to stdout (pipeable)
|
|
136
|
+
if (jsonOutput) {
|
|
137
|
+
console.log(JSON.stringify(items, null, 2));
|
|
138
|
+
return;
|
|
85
139
|
}
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
console.error("");
|
|
147
|
+
// Default: grouped view
|
|
148
|
+
renderGroupedView(items);
|
|
149
|
+
}
|
|
105
150
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
//
|
|
124
|
-
if (
|
|
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
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
*
|
|
201
|
+
* Render flat table (for --all mode)
|
|
167
202
|
*/
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
372
|
-
locations.push("local
|
|
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 {
|
|
451
|
-
"../lib/local-paths.ts"
|
|
452
|
-
);
|
|
513
|
+
const { scanAndRegisterProjects } = await import("../lib/paths-index.ts");
|
|
453
514
|
|
|
454
|
-
|
|
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
|
|
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} ${
|
|
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
|
}
|
package/src/commands/secrets.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
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,
|
|
94
|
+
return { projectName, link, isManaged, projectId };
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
package/src/commands/services.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 (
|
|
50
|
+
if (link?.deploy_mode === "managed") {
|
|
50
51
|
try {
|
|
51
|
-
const resources = await fetchProjectResources(
|
|
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 {
|