@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
package/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "jack": "./src/index.ts"
9
9
  },
10
- "files": [
11
- "src",
12
- "templates"
13
- ],
10
+ "files": ["src", "templates"],
14
11
  "engines": {
15
12
  "bun": ">=1.0.0"
16
13
  },
@@ -49,7 +46,6 @@
49
46
  "archiver": "^7.0.1",
50
47
  "human-id": "^4.1.3",
51
48
  "meow": "^14.0.0",
52
- "posthog-node": "^5.17.4",
53
49
  "yocto-spinner": "^1.0.0",
54
50
  "zod": "^4.2.1"
55
51
  }
@@ -17,7 +17,7 @@ import {
17
17
  } from "../lib/agents.ts";
18
18
  import { readConfig } from "../lib/config.ts";
19
19
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
20
- import { getProject } from "../lib/registry.ts";
20
+ import { readTemplateMetadata } from "../lib/project-link.ts";
21
21
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
22
22
  import { resolveTemplate } from "../templates/index.ts";
23
23
  import type { Template } from "../templates/types.ts";
@@ -116,7 +116,7 @@ async function scanAndPrompt(): Promise<void> {
116
116
  const newAgents = detectionResult.detected.filter(({ id }) => !existingAgents[id]);
117
117
 
118
118
  if (newAgents.length === 0) {
119
- success("No new agents found");
119
+ success("All agents up to date");
120
120
  await listAgents();
121
121
  return;
122
122
  }
@@ -331,19 +331,10 @@ async function preferAgentCommand(args: string[]): Promise<void> {
331
331
  async function refreshAgentFilesCommand(options: AgentsOptions = {}): Promise<void> {
332
332
  const projectDir = process.cwd();
333
333
  let projectName: string;
334
- let project = null;
335
334
 
336
335
  if (options.project) {
337
- // When --project is specified, we still need to run from that project's directory
338
- // since localPath is no longer stored in the registry
336
+ // When --project is specified, verify we're in that project's directory
339
337
  projectName = options.project;
340
- project = await getProject(projectName);
341
-
342
- if (!project) {
343
- error(`Project "${projectName}" not found in registry`);
344
- info("List projects with: jack projects list");
345
- process.exit(1);
346
- }
347
338
 
348
339
  // Verify the current directory matches the project
349
340
  try {
@@ -354,7 +345,7 @@ async function refreshAgentFilesCommand(options: AgentsOptions = {}): Promise<vo
354
345
  process.exit(1);
355
346
  }
356
347
  } catch {
357
- error(`Current directory is not a valid project`);
348
+ error("Current directory is not a valid project");
358
349
  info(`Run this command from the ${projectName} project directory`);
359
350
  process.exit(1);
360
351
  }
@@ -370,17 +361,11 @@ async function refreshAgentFilesCommand(options: AgentsOptions = {}): Promise<vo
370
361
  process.exit(1);
371
362
  }
372
363
  outputSpinner.stop();
373
-
374
- // 2. Get project from registry to find template origin
375
- project = await getProject(projectName);
376
- if (!project) {
377
- error(`Project "${projectName}" not found in registry`);
378
- info("List projects with: jack projects list");
379
- process.exit(1);
380
- }
381
364
  }
382
365
 
383
- if (!project?.template) {
366
+ // 2. Read template metadata from .jack/template.json
367
+ const templateMetadata = await readTemplateMetadata(projectDir);
368
+ if (!templateMetadata) {
384
369
  error("No template lineage found for this project");
385
370
  info("This project was created before lineage tracking was added.");
386
371
  info("Re-create the project with `jack new` to enable refresh.");
@@ -391,10 +376,10 @@ async function refreshAgentFilesCommand(options: AgentsOptions = {}): Promise<vo
391
376
  outputSpinner.start("Loading template...");
392
377
  let template: Template;
393
378
  try {
394
- template = await resolveTemplate(project.template.name);
379
+ template = await resolveTemplate(templateMetadata.name);
395
380
  } catch (err) {
396
381
  outputSpinner.stop();
397
- error(`Failed to load template: ${project.template.name}`);
382
+ error(`Failed to load template: ${templateMetadata.name}`);
398
383
  if (err instanceof Error) {
399
384
  info(err.message);
400
385
  }
@@ -1,8 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { select } from "@inquirer/prompts";
4
+ import { fetchProjectTags } from "../lib/control-plane.ts";
4
5
  import { formatSize } from "../lib/format.ts";
5
6
  import { box, error, info, spinner, success } from "../lib/output.ts";
7
+ import { registerPath } from "../lib/paths-index.ts";
8
+ import { linkProject, updateProjectLink } from "../lib/project-link.ts";
9
+ import { resolveProject } from "../lib/project-resolver.ts";
6
10
  import { cloneFromCloud, getRemoteManifest } from "../lib/storage/index.ts";
7
11
 
8
12
  export interface CloneFlags {
@@ -74,6 +78,29 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
74
78
 
75
79
  downloadSpin.success(`Restored to ./${flags.as ?? projectName}/`);
76
80
 
81
+ // Link project to control plane if it's a managed project
82
+ try {
83
+ const project = await resolveProject(projectName);
84
+ if (project?.sources.controlPlane && project.remote?.projectId) {
85
+ // Managed project - link with control plane project ID
86
+ await linkProject(targetDir, project.remote.projectId, "managed");
87
+ await registerPath(project.remote.projectId, targetDir);
88
+
89
+ // Fetch and restore tags from control plane
90
+ try {
91
+ const remoteTags = await fetchProjectTags(project.remote.projectId);
92
+ if (remoteTags.length > 0) {
93
+ await updateProjectLink(targetDir, { tags: remoteTags });
94
+ info(`Restored ${remoteTags.length} tag(s)`);
95
+ }
96
+ } catch {
97
+ // Silent fail - tag restoration is non-critical
98
+ }
99
+ }
100
+ } catch {
101
+ // Not a control plane project or offline - continue without linking
102
+ }
103
+
77
104
  // Show next steps
78
105
  box("Next steps:", [`cd ${flags.as ?? projectName}`, "bun install", "jack ship"]);
79
106
  }
@@ -9,8 +9,8 @@ import { fetchProjectResources } from "../lib/control-plane.ts";
9
9
  import { promptSelect } from "../lib/hooks.ts";
10
10
  import { managedDown } from "../lib/managed-down.ts";
11
11
  import { error, info, item, output, success, warn } from "../lib/output.ts";
12
+ import { type LocalProjectLink, readProjectLink } from "../lib/project-link.ts";
12
13
  import { resolveProject } from "../lib/project-resolver.ts";
13
- import { type Project, getProject, updateProject } from "../lib/registry.ts";
14
14
  import { parseWranglerResources } from "../lib/resources.ts";
15
15
  import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
16
16
 
@@ -19,11 +19,14 @@ import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.
19
19
  * For managed projects: fetch from control plane.
20
20
  * For BYO projects: parse from wrangler.jsonc in cwd.
21
21
  */
22
- async function resolveDatabaseName(project: Project, projectName: string): Promise<string | null> {
22
+ async function resolveDatabaseName(
23
+ link: LocalProjectLink | null,
24
+ projectName: string,
25
+ ): Promise<string | null> {
23
26
  // For managed projects, fetch from control plane
24
- if (project.deploy_mode === "managed" && project.remote?.project_id) {
27
+ if (link?.deploy_mode === "managed") {
25
28
  try {
26
- const resources = await fetchProjectResources(project.remote.project_id);
29
+ const resources = await fetchProjectResources(link.project_id);
27
30
  const d1 = resources.find((r) => r.resource_type === "d1");
28
31
  return d1?.resource_name || null;
29
32
  } catch {
@@ -70,50 +73,39 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
70
73
  }
71
74
  }
72
75
 
73
- // Resolve project from all sources (registry + control plane)
76
+ // Resolve project from all sources (local link + control plane)
74
77
  const resolved = await resolveProject(name);
75
78
 
79
+ // Read local project link
80
+ const link = await readProjectLink(process.cwd());
81
+
76
82
  // Check if found only on control plane (orphaned managed project)
77
- if (resolved?.sources.controlPlane && !resolved.sources.registry) {
83
+ if (resolved?.sources.controlPlane && !resolved.sources.filesystem) {
78
84
  console.error("");
79
85
  info(`Found "${name}" on jack cloud, linking locally...`);
80
86
  }
81
87
 
82
- // Get the registry project (may have been created by resolver cache)
83
- const project = await getProject(name);
84
-
85
- if (!resolved && !project) {
88
+ if (!resolved && !link) {
86
89
  // Not found anywhere
87
90
  warn(`Project '${name}' not found`);
88
91
  info("Will attempt to undeploy if deployed");
89
92
  }
90
93
 
91
- // Check if this is a managed project (either from resolved or registry)
92
- const isManaged =
93
- resolved?.remote?.projectId ||
94
- (project?.deploy_mode === "managed" && project.remote?.project_id);
94
+ // Check if this is a managed project (from link or resolved data)
95
+ const isManaged = link?.deploy_mode === "managed" || resolved?.remote?.projectId;
95
96
 
96
97
  if (isManaged) {
97
- // Build project object for managedDown if we only have resolved data
98
- const managedProject: Project = project || {
99
- workerUrl: resolved?.url || null,
100
- createdAt: resolved?.createdAt || new Date().toISOString(),
101
- lastDeployed: resolved?.updatedAt || null,
102
- status: resolved?.status === "live" ? "live" : "build_failed",
103
- deploy_mode: "managed",
104
- remote:
105
- resolved?.remote && resolved.url
106
- ? {
107
- project_id: resolved.remote.projectId,
108
- project_slug: resolved.slug,
109
- org_id: resolved.remote.orgId,
110
- runjack_url: resolved.url,
111
- }
112
- : undefined,
113
- };
98
+ // Get the project ID from link or resolved data
99
+ const projectId = link?.project_id || resolved?.remote?.projectId;
100
+ const runjackUrl = resolved?.url || null;
101
+
102
+ if (!projectId) {
103
+ error("Cannot determine project ID for managed deletion");
104
+ process.exit(1);
105
+ }
114
106
 
115
107
  // Route to managed deletion flow
116
- const deleteSuccess = await managedDown(managedProject, name, flags);
108
+ const deleteSuccess = await managedDown({ projectId, runjackUrl }, name, flags);
117
109
  if (!deleteSuccess) {
118
110
  process.exit(0); // User cancelled
119
111
  }
@@ -144,14 +136,6 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
144
136
  await deleteWorker(name);
145
137
  output.stop();
146
138
 
147
- // Update registry - keep entry but clear worker URL
148
- if (project) {
149
- await updateProject(name, {
150
- workerUrl: null,
151
- lastDeployed: null,
152
- });
153
- }
154
-
155
139
  console.error("");
156
140
  success(`'${name}' undeployed`);
157
141
  info("Databases and backups were not affected");
@@ -162,10 +146,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
162
146
  // Interactive mode - show what will be affected
163
147
  console.error("");
164
148
  info(`Project: ${name}`);
165
- if (project?.workerUrl) {
166
- item(`URL: ${project.workerUrl}`);
149
+ if (resolved?.url) {
150
+ item(`URL: ${resolved.url}`);
167
151
  }
168
- const dbName = project ? await resolveDatabaseName(project, name) : null;
152
+ const dbName = await resolveDatabaseName(link, name);
169
153
  if (dbName) {
170
154
  item(`Database: ${dbName}`);
171
155
  }
@@ -223,12 +207,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
223
207
 
224
208
  // Handle backup deletion
225
209
  let shouldDeleteR2 = false;
226
- if (project) {
227
- console.error("");
228
- info("Delete backup for this project?");
229
- const deleteR2Action = await promptSelect(["Yes", "No"]);
230
- shouldDeleteR2 = deleteR2Action === 0;
231
- }
210
+ console.error("");
211
+ info("Delete backup for this project?");
212
+ const deleteR2Action = await promptSelect(["Yes", "No"]);
213
+ shouldDeleteR2 = deleteR2Action === 0;
232
214
 
233
215
  // Execute deletions
234
216
  console.error("");
@@ -279,14 +261,6 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
279
261
  }
280
262
  }
281
263
 
282
- // Update registry - keep entry but clear worker URL
283
- if (project) {
284
- await updateProject(name, {
285
- workerUrl: null,
286
- lastDeployed: null,
287
- });
288
- }
289
-
290
264
  console.error("");
291
265
  success(`Project '${name}' undeployed`);
292
266
  console.error("");
@@ -9,7 +9,7 @@ import pkg from "../../package.json";
9
9
  import { getCredentials } from "../lib/auth/store.ts";
10
10
  import { getControlApiUrl } from "../lib/control-plane.ts";
11
11
  import { error, info, output, success } from "../lib/output.ts";
12
- import { getProject } from "../lib/registry.ts";
12
+ import { getDeployMode, readProjectLink } from "../lib/project-link.ts";
13
13
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
14
14
  import { getTelemetryConfig } from "../lib/telemetry.ts";
15
15
 
@@ -171,10 +171,9 @@ async function collectMetadata(attachPersonalInfo: boolean): Promise<FeedbackMet
171
171
  if (attachPersonalInfo) {
172
172
  try {
173
173
  projectName = await getProjectNameFromDir(process.cwd());
174
- if (projectName) {
175
- const project = await getProject(projectName);
176
- deployMode = project?.deploy_mode ?? null;
177
- }
174
+ // Read deploy mode from .jack/project.json
175
+ const link = await readProjectLink(process.cwd());
176
+ deployMode = link?.deploy_mode ?? null;
178
177
  } catch {
179
178
  // Not in a project directory, that's fine
180
179
  }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * jack link - Link current directory to a jack cloud project or create BYO link
3
+ *
4
+ * Usage:
5
+ * jack link my-api Link to existing managed project
6
+ * jack link --byo Create BYO link (generates local ID)
7
+ * jack link Interactive: prompts for project selection if logged in
8
+ */
9
+
10
+ import { existsSync } from "node:fs";
11
+ import { select } from "@inquirer/prompts";
12
+ import { isLoggedIn } from "../lib/auth/index.ts";
13
+ import {
14
+ type ManagedProject,
15
+ findProjectBySlug,
16
+ listManagedProjects,
17
+ } from "../lib/control-plane.ts";
18
+ import { error, info, output, success } from "../lib/output.ts";
19
+ import { registerPath } from "../lib/paths-index.ts";
20
+ import { generateByoProjectId, linkProject, readProjectLink } from "../lib/project-link.ts";
21
+
22
+ export interface LinkFlags {
23
+ byo?: boolean;
24
+ }
25
+
26
+ export default async function link(projectName?: string, flags: LinkFlags = {}): Promise<void> {
27
+ // Check if already linked
28
+ const existingLink = await readProjectLink(process.cwd());
29
+ if (existingLink) {
30
+ error("This directory is already linked");
31
+ info(`Project ID: ${existingLink.project_id}`);
32
+ info("To re-link, first run: jack unlink");
33
+ process.exit(1);
34
+ }
35
+
36
+ // Check for wrangler config
37
+ const hasWranglerConfig =
38
+ existsSync("wrangler.jsonc") || existsSync("wrangler.json") || existsSync("wrangler.toml");
39
+
40
+ if (!hasWranglerConfig) {
41
+ error("No wrangler config found");
42
+ info("Run this from a jack project directory");
43
+ process.exit(1);
44
+ }
45
+
46
+ // BYO mode - generate local ID
47
+ if (flags.byo) {
48
+ const projectId = generateByoProjectId();
49
+ output.start("Creating BYO link...");
50
+ await linkProject(process.cwd(), projectId, "byo");
51
+ await registerPath(projectId, process.cwd());
52
+ output.stop();
53
+ success("Linked as BYO project");
54
+ info(`Project ID: ${projectId}`);
55
+ return;
56
+ }
57
+
58
+ // Check if logged in for managed mode
59
+ const loggedIn = await isLoggedIn();
60
+
61
+ if (!loggedIn && !projectName) {
62
+ // Not logged in and no project name - suggest options
63
+ error("Not logged in to jack cloud");
64
+ info("Login with: jack login");
65
+ info("Or create a BYO link: jack link --byo");
66
+ process.exit(1);
67
+ }
68
+
69
+ // If project name provided, find it on control plane
70
+ if (projectName) {
71
+ if (!loggedIn) {
72
+ error("Login required to link managed projects");
73
+ info("Run: jack login");
74
+ process.exit(1);
75
+ }
76
+
77
+ output.start(`Finding project: ${projectName}...`);
78
+ let project: ManagedProject | null = null;
79
+ try {
80
+ project = await findProjectBySlug(projectName);
81
+ } catch (err) {
82
+ output.stop();
83
+ error("Failed to find project");
84
+ if (err instanceof Error) {
85
+ info(err.message);
86
+ }
87
+ process.exit(1);
88
+ }
89
+ output.stop();
90
+
91
+ if (!project) {
92
+ error(`Project not found: ${projectName}`);
93
+ info("List your projects with: jack projects list");
94
+ process.exit(1);
95
+ }
96
+
97
+ output.start("Linking project...");
98
+ await linkProject(process.cwd(), project.id, "managed");
99
+ await registerPath(project.id, process.cwd());
100
+ output.stop();
101
+ success(`Linked to: ${projectName}`);
102
+ return;
103
+ }
104
+
105
+ // Interactive mode - list and select project
106
+ output.start("Loading projects...");
107
+ let projects: ManagedProject[] = [];
108
+ try {
109
+ projects = await listManagedProjects();
110
+ } catch (err) {
111
+ output.stop();
112
+ error("Failed to load projects");
113
+ if (err instanceof Error) {
114
+ info(err.message);
115
+ }
116
+ process.exit(1);
117
+ }
118
+ output.stop();
119
+
120
+ if (projects.length === 0) {
121
+ error("No managed projects found");
122
+ info("Create one with: jack new");
123
+ info("Or link as BYO: jack link --byo");
124
+ process.exit(1);
125
+ }
126
+
127
+ console.error("");
128
+ const choice = await select({
129
+ message: "Select a project to link:",
130
+ choices: projects.map((p) => ({
131
+ value: p.id,
132
+ name: `${p.slug} (${p.status})`,
133
+ })),
134
+ });
135
+
136
+ const selected = projects.find((p) => p.id === choice);
137
+ if (!selected) {
138
+ error("No project selected");
139
+ process.exit(1);
140
+ }
141
+
142
+ output.start("Linking project...");
143
+ await linkProject(process.cwd(), selected.id, "managed");
144
+ await registerPath(selected.id, process.cwd());
145
+ output.stop();
146
+ success(`Linked to: ${selected.slug}`);
147
+ }
@@ -1,6 +1,12 @@
1
+ import { input } from "@inquirer/prompts";
1
2
  import { type DeviceAuthResponse, pollDeviceToken, startDeviceAuth } from "../lib/auth/client.ts";
2
3
  import { type AuthCredentials, saveCredentials } from "../lib/auth/store.ts";
3
- import { error, info, spinner, success } from "../lib/output.ts";
4
+ import {
5
+ checkUsernameAvailable,
6
+ getCurrentUserProfile,
7
+ setUsername,
8
+ } from "../lib/control-plane.ts";
9
+ import { error, info, spinner, success, warn } from "../lib/output.ts";
4
10
 
5
11
  interface LoginOptions {
6
12
  /** Skip the initial "Logging in..." message (used when called from auto-login) */
@@ -69,6 +75,9 @@ export default async function login(options: LoginOptions = {}): Promise<void> {
69
75
 
70
76
  console.error("");
71
77
  success(`Logged in as ${tokens.user.email}`);
78
+
79
+ // Prompt for username if not set
80
+ await promptForUsername(tokens.user.email);
72
81
  return;
73
82
  }
74
83
  } catch (err) {
@@ -86,3 +95,117 @@ export default async function login(options: LoginOptions = {}): Promise<void> {
86
95
  function sleep(ms: number): Promise<void> {
87
96
  return new Promise((resolve) => setTimeout(resolve, ms));
88
97
  }
98
+
99
+ async function promptForUsername(email: string): Promise<void> {
100
+ // Skip in non-TTY environments
101
+ if (!process.stdout.isTTY) {
102
+ return;
103
+ }
104
+
105
+ const spin = spinner("Checking account...");
106
+
107
+ try {
108
+ const profile = await getCurrentUserProfile();
109
+ spin.stop();
110
+
111
+ // If user already has a username, skip
112
+ if (profile?.username) {
113
+ return;
114
+ }
115
+
116
+ console.error("");
117
+ info("Choose a username for your jack cloud account.");
118
+ info("URLs will look like: alice-vibes.runjack.xyz");
119
+ console.error("");
120
+
121
+ // Generate suggestions from $USER env var and email
122
+ const suggestions = generateUsernameSuggestions(email);
123
+
124
+ let username: string | null = null;
125
+
126
+ while (!username) {
127
+ // Show suggestions if available
128
+ if (suggestions.length > 0) {
129
+ info(`Suggestions: ${suggestions.join(", ")}`);
130
+ }
131
+
132
+ const inputUsername = await input({
133
+ message: "Username:",
134
+ default: suggestions[0],
135
+ validate: (value) => {
136
+ if (!value || value.length < 3) {
137
+ return "Username must be at least 3 characters";
138
+ }
139
+ if (value.length > 39) {
140
+ return "Username must be 39 characters or less";
141
+ }
142
+ if (value !== value.toLowerCase()) {
143
+ return "Username must be lowercase";
144
+ }
145
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]{1,2}$/.test(value)) {
146
+ return "Use only lowercase letters, numbers, and hyphens";
147
+ }
148
+ return true;
149
+ },
150
+ });
151
+
152
+ // Check availability
153
+ const checkSpin = spinner("Checking availability...");
154
+ const availability = await checkUsernameAvailable(inputUsername);
155
+ checkSpin.stop();
156
+
157
+ if (!availability.available) {
158
+ warn(availability.error || `Username "${inputUsername}" is already taken. Try another.`);
159
+ continue;
160
+ }
161
+
162
+ // Try to set the username
163
+ const setSpin = spinner("Setting username...");
164
+ try {
165
+ await setUsername(inputUsername);
166
+ setSpin.stop();
167
+ username = inputUsername;
168
+ success(`Username set to "${username}"`);
169
+ } catch (err) {
170
+ setSpin.stop();
171
+ warn(err instanceof Error ? err.message : "Failed to set username");
172
+ }
173
+ }
174
+ } catch (err) {
175
+ spin.stop();
176
+ // Non-fatal - user can set username later
177
+ warn("Could not set username. You can set it later.");
178
+ }
179
+ }
180
+
181
+ function generateUsernameSuggestions(email: string): string[] {
182
+ const suggestions: string[] = [];
183
+
184
+ // Try $USER environment variable first
185
+ const envUser = process.env.USER || process.env.USERNAME;
186
+ if (envUser) {
187
+ const normalized = normalizeToUsername(envUser);
188
+ if (normalized && normalized.length >= 3) {
189
+ suggestions.push(normalized);
190
+ }
191
+ }
192
+
193
+ // Try email local part
194
+ const emailLocal = email.split("@")[0];
195
+ if (emailLocal) {
196
+ const normalized = normalizeToUsername(emailLocal);
197
+ if (normalized && normalized.length >= 3 && !suggestions.includes(normalized)) {
198
+ suggestions.push(normalized);
199
+ }
200
+ }
201
+
202
+ return suggestions.slice(0, 3); // Max 3 suggestions
203
+ }
204
+
205
+ function normalizeToUsername(input: string): string {
206
+ return input
207
+ .toLowerCase()
208
+ .replace(/[^a-z0-9]+/g, "-")
209
+ .replace(/^-+|-+$/g, "")
210
+ .slice(0, 39);
211
+ }
@@ -1,7 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { output } from "../lib/output.ts";
3
- import { getProject } from "../lib/registry.ts";
4
- import { getProjectNameFromDir } from "../lib/storage/index.ts";
3
+ import { getDeployMode } from "../lib/project-link.ts";
5
4
 
6
5
  // Lines containing these strings will be filtered out
7
6
  const FILTERED_PATTERNS = ["⛅️ wrangler"];
@@ -19,22 +18,13 @@ export default async function logs(): Promise<void> {
19
18
  process.exit(1);
20
19
  }
21
20
 
22
- // Check if this is a managed project
23
- let projectName: string | null = null;
24
- try {
25
- projectName = await getProjectNameFromDir(process.cwd());
26
- } catch {
27
- // Continue without project name - will fall through to wrangler tail
28
- }
29
-
30
- if (projectName) {
31
- const project = await getProject(projectName);
32
- if (project?.deploy_mode === "managed") {
33
- output.warn("Real-time logs not yet available for managed projects");
34
- output.info("Logs are being collected - web UI coming soon");
35
- output.info("Track progress: https://github.com/getjack-org/jack/issues/2");
36
- return;
37
- }
21
+ // Check if this is a managed project (read from .jack/project.json)
22
+ const deployMode = await getDeployMode(process.cwd());
23
+ if (deployMode === "managed") {
24
+ output.warn("Real-time logs not yet available for managed projects");
25
+ output.info("Logs are being collected - web UI coming soon");
26
+ output.info("Track progress: https://github.com/getjack-org/jack/issues/2");
27
+ return;
38
28
  }
39
29
 
40
30
  // BYOC project - use wrangler tail
@@ -8,7 +8,13 @@ import { createProject } from "../lib/project-operations.ts";
8
8
 
9
9
  export default async function newProject(
10
10
  nameOrPhrase?: string,
11
- options: { template?: string; intent?: string; managed?: boolean; byo?: boolean; ci?: boolean } = {},
11
+ options: {
12
+ template?: string;
13
+ intent?: string;
14
+ managed?: boolean;
15
+ byo?: boolean;
16
+ ci?: boolean;
17
+ } = {},
12
18
  ): Promise<void> {
13
19
  // Immediate feedback
14
20
  output.start("Starting...");