@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/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
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
|
}
|
package/src/commands/agents.ts
CHANGED
|
@@ -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 {
|
|
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("
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
379
|
+
template = await resolveTemplate(templateMetadata.name);
|
|
395
380
|
} catch (err) {
|
|
396
381
|
outputSpinner.stop();
|
|
397
|
-
error(`Failed to load template: ${
|
|
382
|
+
error(`Failed to load template: ${templateMetadata.name}`);
|
|
398
383
|
if (err instanceof Error) {
|
|
399
384
|
info(err.message);
|
|
400
385
|
}
|
package/src/commands/clone.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/down.ts
CHANGED
|
@@ -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(
|
|
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 (
|
|
27
|
+
if (link?.deploy_mode === "managed") {
|
|
25
28
|
try {
|
|
26
|
-
const resources = await fetchProjectResources(
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
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 (
|
|
166
|
-
item(`URL: ${
|
|
149
|
+
if (resolved?.url) {
|
|
150
|
+
item(`URL: ${resolved.url}`);
|
|
167
151
|
}
|
|
168
|
-
const dbName =
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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("");
|
package/src/commands/feedback.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { output } from "../lib/output.ts";
|
|
3
|
-
import {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
package/src/commands/new.ts
CHANGED
|
@@ -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: {
|
|
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...");
|