@lizard-build/cli 0.1.0 → 0.3.29
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/.github/workflows/release.yml +90 -0
- package/README.md +41 -0
- package/dist/commands/add.js +318 -45
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +68 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +13 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/domain.d.ts +9 -0
- package/dist/commands/domain.js +195 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/git.js +175 -36
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +128 -86
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +104 -33
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.js +4 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.js +223 -30
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/open.js +3 -2
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/port.d.ts +7 -0
- package/dist/commands/port.js +49 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/projects.js +36 -6
- package/dist/commands/projects.js.map +1 -1
- package/dist/commands/ps.js +32 -39
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/redeploy.js +48 -8
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/regions.js +2 -5
- package/dist/commands/regions.js.map +1 -1
- package/dist/commands/restart.js +84 -10
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/run.d.ts +9 -0
- package/dist/commands/run.js +61 -22
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scale.d.ts +10 -0
- package/dist/commands/scale.js +166 -0
- package/dist/commands/scale.js.map +1 -0
- package/dist/commands/secrets.js +200 -89
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/service-set.d.ts +49 -0
- package/dist/commands/service-set.js +552 -0
- package/dist/commands/service-set.js.map +1 -0
- package/dist/commands/service-show.d.ts +11 -0
- package/dist/commands/service-show.js +44 -0
- package/dist/commands/service-show.js.map +1 -0
- package/dist/commands/service.d.ts +8 -0
- package/dist/commands/service.js +262 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/ssh.d.ts +2 -0
- package/dist/commands/ssh.js +161 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +49 -38
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +5 -0
- package/dist/commands/unlink.js +18 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/up.d.ts +9 -0
- package/dist/commands/up.js +417 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +79 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/whoami.js +26 -6
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +36 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.js +204 -82
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +17 -2
- package/dist/lib/api.js +85 -51
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -11
- package/dist/lib/auth.js +16 -36
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.d.ts +36 -15
- package/dist/lib/config.js +71 -58
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +17 -4
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/name.d.ts +11 -0
- package/dist/lib/name.js +26 -0
- package/dist/lib/name.js.map +1 -0
- package/dist/lib/picker.d.ts +32 -0
- package/dist/lib/picker.js +91 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/lib/resolve.d.ts +85 -0
- package/dist/lib/resolve.js +203 -0
- package/dist/lib/resolve.js.map +1 -0
- package/dist/lib/updater.d.ts +16 -0
- package/dist/lib/updater.js +102 -0
- package/dist/lib/updater.js.map +1 -0
- package/lizard-wrapper.sh +2 -0
- package/package.json +11 -3
- package/src/commands/add.ts +388 -56
- package/src/commands/config.ts +80 -0
- package/src/commands/docs.ts +15 -0
- package/src/commands/domain.ts +248 -0
- package/src/commands/git.ts +201 -40
- package/src/commands/init.ts +149 -100
- package/src/commands/link.ts +127 -35
- package/src/commands/login.ts +4 -3
- package/src/commands/logs.ts +283 -27
- package/src/commands/open.ts +3 -2
- package/src/commands/port.ts +57 -0
- package/src/commands/projects.ts +43 -6
- package/src/commands/ps.ts +39 -60
- package/src/commands/redeploy.ts +51 -10
- package/src/commands/regions.ts +2 -6
- package/src/commands/restart.ts +84 -10
- package/src/commands/run.ts +68 -24
- package/src/commands/scale.ts +216 -0
- package/src/commands/secrets.ts +277 -100
- package/src/commands/service-set.ts +669 -0
- package/src/commands/service-show.ts +52 -0
- package/src/commands/service.ts +298 -0
- package/src/commands/ssh.ts +176 -0
- package/src/commands/status.ts +51 -46
- package/src/commands/unlink.ts +17 -0
- package/src/commands/up.ts +461 -0
- package/src/commands/upgrade.ts +87 -0
- package/src/commands/whoami.ts +34 -6
- package/src/commands/workspace.ts +44 -0
- package/src/index.ts +214 -85
- package/src/lib/api.ts +114 -51
- package/src/lib/auth.ts +22 -46
- package/src/lib/config.ts +100 -65
- package/src/lib/format.ts +18 -4
- package/src/lib/name.ts +27 -0
- package/src/lib/picker.ts +133 -0
- package/src/lib/resolve.ts +285 -0
- package/src/lib/updater.ts +106 -0
- package/test/cli.test.ts +491 -0
- package/test/fixtures/hello-app/Dockerfile +5 -0
- package/test/fixtures/hello-app/index.js +5 -0
- package/test/unit/api.test.ts +66 -0
- package/test/unit/config.test.ts +94 -0
- package/test/unit/init.test.ts +211 -0
- package/test/unit/json.test.ts +208 -0
- package/test/unit/picker.test.ts +161 -0
- package/test/unit/resolve.test.ts +124 -0
- package/test/unit/service-set.test.ts +355 -0
- package/vitest.config.ts +10 -0
- package/dist/commands/connect.d.ts +0 -2
- package/dist/commands/connect.js +0 -117
- package/dist/commands/connect.js.map +0 -1
- package/dist/commands/context.d.ts +0 -2
- package/dist/commands/context.js +0 -71
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/deploy.d.ts +0 -2
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/destroy.d.ts +0 -2
- package/dist/commands/destroy.js +0 -51
- package/dist/commands/destroy.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.js +0 -41
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/version.d.ts +0 -2
- package/dist/commands/version.js +0 -37
- package/dist/commands/version.js.map +0 -1
- package/src/commands/connect.ts +0 -145
- package/src/commands/context.ts +0 -93
- package/src/commands/deploy.ts +0 -153
- package/src/commands/destroy.ts +0 -51
- package/src/commands/update.ts +0 -44
- package/src/commands/version.ts +0 -37
package/src/commands/init.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import chalk from "chalk";
|
|
2
|
+
import path from "node:path";
|
|
4
3
|
import * as p from "@clack/prompts";
|
|
5
4
|
import { Command } from "commander";
|
|
6
|
-
import { api } from "../lib/api.js";
|
|
5
|
+
import { api, withQuery, type Workspace } from "../lib/api.js";
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
getProjectLink,
|
|
8
|
+
setProjectLink,
|
|
9
|
+
type ProjectLink,
|
|
10
10
|
} from "../lib/config.js";
|
|
11
11
|
import {
|
|
12
12
|
success,
|
|
@@ -15,125 +15,174 @@ import {
|
|
|
15
15
|
printJSON,
|
|
16
16
|
isTTY,
|
|
17
17
|
} from "../lib/format.js";
|
|
18
|
+
import { fetchWorkspaces, pickWorkspace } from "../lib/picker.js";
|
|
18
19
|
|
|
19
20
|
interface Project {
|
|
20
21
|
id: string;
|
|
21
22
|
name: string;
|
|
22
23
|
slug: string;
|
|
24
|
+
workspaceId?: string | null;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the current directory is linked to a project. If already linked and
|
|
29
|
+
* `force` is false, returns the existing link. Otherwise runs the
|
|
30
|
+
* create-or-select flow.
|
|
31
|
+
*
|
|
32
|
+
* Flow (mirrors Railway's `link` wizard):
|
|
33
|
+
* 1. Workspace — selected by --workspace, single workspace auto-pick,
|
|
34
|
+
* or interactive select. Skipped when only one workspace exists.
|
|
35
|
+
* 2. Project — matched by --name within the workspace, picked from the
|
|
36
|
+
* workspace's project list, or created.
|
|
37
|
+
*
|
|
38
|
+
* `projectName` (from --name / --project) is matched against existing
|
|
39
|
+
* projects inside the resolved workspace; if not found, a new project
|
|
40
|
+
* with that name is created in the resolved workspace.
|
|
41
|
+
*/
|
|
42
|
+
export async function ensureLinked(opts: {
|
|
43
|
+
projectName?: string;
|
|
44
|
+
workspaceFlag?: string;
|
|
45
|
+
force?: boolean;
|
|
46
|
+
relinkPrompt?: boolean;
|
|
47
|
+
} = {}): Promise<ProjectLink> {
|
|
48
|
+
const existing = getProjectLink();
|
|
34
49
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (has("requirements.txt") || has("pyproject.toml"))
|
|
50
|
-
return { name: "Python", port: 8000, buildCmd: "pip install -r requirements.txt", startCmd: "python app.py" };
|
|
51
|
-
if (has("package.json"))
|
|
52
|
-
return { name: "Node.js", port: 3000, buildCmd: "npm run build", startCmd: "npm start" };
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
50
|
+
if (existing && !opts.force) {
|
|
51
|
+
if (!opts.relinkPrompt) return existing;
|
|
52
|
+
|
|
53
|
+
if (!isTTY()) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Already linked to ${existing.projectName || existing.projectId}. Use --force to relink.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const proceed = await p.confirm({
|
|
59
|
+
message: `Directory is linked to "${existing.projectName || existing.projectId}". Relink?`,
|
|
60
|
+
initialValue: false,
|
|
61
|
+
});
|
|
62
|
+
if (p.isCancel(proceed) || !proceed) return existing;
|
|
63
|
+
}
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
// 1. Workspace
|
|
66
|
+
const workspaces = await fetchWorkspaces();
|
|
67
|
+
const workspace = await pickWorkspace({
|
|
68
|
+
flag: opts.workspaceFlag,
|
|
69
|
+
projectNameHint: opts.projectName,
|
|
70
|
+
workspaces,
|
|
71
|
+
});
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
toml += `\n[resources]\ncpu = 1\nmemory = 512\n`;
|
|
73
|
+
// 2. Project — pulled from the chosen workspace only
|
|
74
|
+
const projects = await api.get<Project[]>(
|
|
75
|
+
withQuery("/api/projects", { workspaceId: workspace.id }),
|
|
76
|
+
);
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
let project: Project;
|
|
79
|
+
|
|
80
|
+
if (opts.projectName) {
|
|
81
|
+
const lower = opts.projectName.toLowerCase();
|
|
82
|
+
const match = projects.find(
|
|
83
|
+
(pr) =>
|
|
84
|
+
pr.name.toLowerCase() === lower ||
|
|
85
|
+
pr.slug.toLowerCase() === lower ||
|
|
86
|
+
pr.id.toLowerCase() === lower,
|
|
87
|
+
);
|
|
88
|
+
project =
|
|
89
|
+
match ??
|
|
90
|
+
(await api.post<Project>("/api/projects", {
|
|
91
|
+
name: opts.projectName,
|
|
92
|
+
workspaceId: workspace.id,
|
|
93
|
+
}));
|
|
94
|
+
} else if (!isTTY()) {
|
|
95
|
+
project = await api.post<Project>("/api/projects", {
|
|
96
|
+
name: path.basename(process.cwd()),
|
|
97
|
+
workspaceId: workspace.id,
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
let action: "create" | "select" = "create";
|
|
101
|
+
if (projects.length > 0) {
|
|
102
|
+
const choice = await p.select({
|
|
103
|
+
message: `Link a project in ${workspace.name}`,
|
|
104
|
+
options: [
|
|
105
|
+
{ value: "create", label: "Create new project" },
|
|
106
|
+
{ value: "select", label: "Select existing project" },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
if (p.isCancel(choice)) process.exit(5);
|
|
110
|
+
action = choice as "create" | "select";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (action === "create") {
|
|
114
|
+
const nameRes = await p.text({
|
|
115
|
+
message: "Project name",
|
|
116
|
+
defaultValue: path.basename(process.cwd()),
|
|
117
|
+
placeholder: path.basename(process.cwd()),
|
|
118
|
+
});
|
|
119
|
+
if (p.isCancel(nameRes)) process.exit(5);
|
|
120
|
+
project = await api.post<Project>("/api/projects", {
|
|
121
|
+
name: nameRes as string,
|
|
122
|
+
workspaceId: workspace.id,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
const selected = await p.select({
|
|
126
|
+
message: "Select project",
|
|
127
|
+
options: projects.map((pr) => ({
|
|
128
|
+
value: pr.id,
|
|
129
|
+
label: pr.name,
|
|
130
|
+
hint: pr.id,
|
|
131
|
+
})),
|
|
132
|
+
});
|
|
133
|
+
if (p.isCancel(selected)) process.exit(5);
|
|
134
|
+
project = projects.find((pr) => pr.id === selected)!;
|
|
135
|
+
}
|
|
72
136
|
}
|
|
137
|
+
|
|
138
|
+
const link: ProjectLink = {
|
|
139
|
+
projectId: project.id,
|
|
140
|
+
projectName: project.name,
|
|
141
|
+
workspaceId: workspace.id,
|
|
142
|
+
workspaceName: workspace.name,
|
|
143
|
+
};
|
|
144
|
+
setProjectLink(link);
|
|
145
|
+
return link;
|
|
73
146
|
}
|
|
74
147
|
|
|
75
148
|
export function registerInit(program: Command) {
|
|
76
149
|
program
|
|
77
150
|
.command("init")
|
|
78
|
-
.description(
|
|
79
|
-
|
|
151
|
+
.description(
|
|
152
|
+
"Create or select a project and link it to the current directory",
|
|
153
|
+
)
|
|
154
|
+
.option("-n, --name <name>", "Project name (use existing or create if missing)")
|
|
155
|
+
.option("--project <name>", "Alias for --name (kept for backwards compat)")
|
|
156
|
+
.option("-w, --workspace <ws>", "Workspace id, slug, or name")
|
|
157
|
+
.option("--force", "Relink even if this directory is already linked")
|
|
80
158
|
.action(async (opts) => {
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Detect framework
|
|
90
|
-
const framework = detectFramework();
|
|
91
|
-
if (framework && !isJSONMode()) {
|
|
92
|
-
info(`Detected framework: ${chalk.cyan(framework.name)}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Get project name
|
|
96
|
-
let projectName = opts.name;
|
|
97
|
-
if (!projectName) {
|
|
98
|
-
if (!isTTY()) {
|
|
99
|
-
throw new Error("--name is required in non-interactive mode");
|
|
100
|
-
}
|
|
101
|
-
const result = await p.text({
|
|
102
|
-
message: "Project name",
|
|
103
|
-
defaultValue: path.basename(process.cwd()),
|
|
104
|
-
placeholder: path.basename(process.cwd()),
|
|
105
|
-
});
|
|
106
|
-
if (p.isCancel(result)) process.exit(5);
|
|
107
|
-
projectName = result as string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Create project via API
|
|
111
|
-
const project = await api.post<Project>("/api/projects", {
|
|
112
|
-
name: projectName,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Save local config
|
|
116
|
-
saveProjectConfig({
|
|
117
|
-
projectId: project.id,
|
|
118
|
-
projectName: project.name,
|
|
159
|
+
const projectName = opts.name || opts.project;
|
|
160
|
+
const link = await ensureLinked({
|
|
161
|
+
projectName,
|
|
162
|
+
workspaceFlag: opts.workspace,
|
|
163
|
+
force: opts.force,
|
|
164
|
+
relinkPrompt: true,
|
|
119
165
|
});
|
|
120
166
|
|
|
121
|
-
// Write lizard.toml
|
|
122
|
-
writeLizardToml(framework);
|
|
123
|
-
|
|
124
167
|
if (isJSONMode()) {
|
|
125
168
|
printJSON({
|
|
126
|
-
projectId:
|
|
127
|
-
name:
|
|
128
|
-
|
|
169
|
+
projectId: link.projectId,
|
|
170
|
+
name: link.projectName,
|
|
171
|
+
workspaceId: link.workspaceId,
|
|
172
|
+
workspaceName: link.workspaceName,
|
|
129
173
|
});
|
|
130
174
|
} else {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
175
|
+
const wsLabel = link.workspaceName
|
|
176
|
+
? chalk.dim(` (${link.workspaceName})`)
|
|
177
|
+
: "";
|
|
178
|
+
success(
|
|
179
|
+
`Linked to ${chalk.bold(link.projectName || link.projectId)}${wsLabel}`,
|
|
180
|
+
);
|
|
181
|
+
info(chalk.dim(" Saved to ~/.lizard/config.json"));
|
|
137
182
|
}
|
|
138
183
|
});
|
|
139
184
|
}
|
|
185
|
+
|
|
186
|
+
// Re-export Workspace so consumers can import the type from this module if
|
|
187
|
+
// they ever want to (kept for older imports, harmless if unused).
|
|
188
|
+
export type { Workspace };
|
package/src/commands/link.ts
CHANGED
|
@@ -1,63 +1,155 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import { api } from "../lib/api.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { api, withQuery } from "../lib/api.js";
|
|
5
|
+
import { setProjectLink, type ProjectLink } from "../lib/config.js";
|
|
6
|
+
import { isJSONMode, printJSON, isTTY, success } from "../lib/format.js";
|
|
7
|
+
import { fetchWorkspaces, pickWorkspace } from "../lib/picker.js";
|
|
7
8
|
|
|
8
9
|
interface Project {
|
|
9
10
|
id: string;
|
|
10
11
|
name: string;
|
|
11
12
|
slug: string;
|
|
13
|
+
workspaceId?: string | null;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
interface ServiceInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ServicesResponse {
|
|
23
|
+
apps?: ServiceInfo[];
|
|
24
|
+
addons?: ServiceInfo[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `lizard link` — associates the current directory with an existing
|
|
29
|
+
* project and (optionally) service. Each piece can be passed via flags
|
|
30
|
+
* or selected interactively.
|
|
31
|
+
*
|
|
32
|
+
* Order matches Railway's wizard: workspace → project → service.
|
|
33
|
+
*/
|
|
14
34
|
export function registerLink(program: Command) {
|
|
15
35
|
program
|
|
16
36
|
.command("link")
|
|
17
|
-
.description("
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
.description("Associate the current directory with an existing project")
|
|
38
|
+
.argument("[service]", "Service name or ID (optional)")
|
|
39
|
+
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
40
|
+
.option("-w, --workspace <ws>", "Workspace id, slug, or name")
|
|
41
|
+
.option("-s, --service <name>", "Service name or ID (optional)")
|
|
42
|
+
.action(async (serviceArg: string | undefined, opts) => {
|
|
43
|
+
const projectFlag = opts.project;
|
|
44
|
+
const serviceFlag = serviceArg || opts.service;
|
|
45
|
+
|
|
46
|
+
// 1. Workspace
|
|
47
|
+
const workspaces = await fetchWorkspaces();
|
|
48
|
+
const workspace = await pickWorkspace({
|
|
49
|
+
flag: opts.workspace,
|
|
50
|
+
projectNameHint: projectFlag,
|
|
51
|
+
workspaces,
|
|
52
|
+
});
|
|
29
53
|
|
|
30
|
-
|
|
54
|
+
// 2. Project (within workspace)
|
|
55
|
+
const projects = await api.get<Project[]>(
|
|
56
|
+
withQuery("/api/projects", { workspaceId: workspace.id }),
|
|
57
|
+
);
|
|
58
|
+
if (projects.length === 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`No projects in ${workspace.name}. Run \`lizard init\` to create one.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
let project: Project;
|
|
64
|
+
if (projectFlag) {
|
|
65
|
+
const lower = projectFlag.toLowerCase();
|
|
66
|
+
const match = projects.find(
|
|
67
|
+
(pr) =>
|
|
68
|
+
pr.id.toLowerCase() === lower ||
|
|
69
|
+
pr.slug?.toLowerCase() === lower ||
|
|
70
|
+
pr.name.toLowerCase() === lower,
|
|
71
|
+
);
|
|
72
|
+
if (!match) {
|
|
31
73
|
throw new Error(
|
|
32
|
-
"
|
|
33
|
-
projects.map((p) => `${p.name} (${p.id})`).join(", "),
|
|
74
|
+
`Project "${projectFlag}" not found in ${workspace.name}. Available: ${projects.map((pr) => pr.name).join(", ")}`,
|
|
34
75
|
);
|
|
35
76
|
}
|
|
77
|
+
project = match;
|
|
78
|
+
} else if (projects.length === 1) {
|
|
79
|
+
project = projects[0];
|
|
80
|
+
} else {
|
|
81
|
+
if (!isTTY()) throw new Error("--project required in non-interactive mode");
|
|
82
|
+
const sel = await p.select({
|
|
83
|
+
message: `Select a project in ${workspace.name}`,
|
|
84
|
+
options: projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id })),
|
|
85
|
+
});
|
|
86
|
+
if (p.isCancel(sel)) process.exit(5);
|
|
87
|
+
project = projects.find((pr) => pr.id === sel)!;
|
|
88
|
+
}
|
|
36
89
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
// 3. Service (optional)
|
|
91
|
+
const services = await api
|
|
92
|
+
.get<ServicesResponse>(
|
|
93
|
+
withQuery(`/api/projects/${project.id}/services`, {
|
|
94
|
+
workspaceId: workspace.id,
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
.catch(() => ({ apps: [], addons: [] }) as ServicesResponse);
|
|
98
|
+
const allServices = [
|
|
99
|
+
...(services.apps || []),
|
|
100
|
+
...(services.addons || []),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
let service: ServiceInfo | null = null;
|
|
104
|
+
if (serviceFlag) {
|
|
105
|
+
const lower = serviceFlag.toLowerCase();
|
|
106
|
+
service =
|
|
107
|
+
allServices.find(
|
|
108
|
+
(s) => s.id.toLowerCase() === lower || s.name?.toLowerCase() === lower,
|
|
109
|
+
) || null;
|
|
110
|
+
if (!service) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Service "${serviceFlag}" not found. Available: ${allServices.map((s) => s.name).join(", ")}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} else if (allServices.length > 0 && isTTY()) {
|
|
116
|
+
const choices = [
|
|
117
|
+
{ value: "", label: "(none — link only project)" },
|
|
118
|
+
...allServices.map((s) => ({ value: s.id, label: s.name, hint: s.status })),
|
|
119
|
+
];
|
|
120
|
+
const sel = await p.select({
|
|
121
|
+
message: "Select a service (optional)",
|
|
122
|
+
options: choices,
|
|
44
123
|
});
|
|
45
|
-
if (p.isCancel(
|
|
46
|
-
|
|
47
|
-
projectName = projects.find((p) => p.id === projectId)?.name;
|
|
124
|
+
if (p.isCancel(sel)) process.exit(5);
|
|
125
|
+
if (sel) service = allServices.find((s) => s.id === sel)!;
|
|
48
126
|
}
|
|
49
127
|
|
|
50
|
-
|
|
51
|
-
|
|
128
|
+
// Save
|
|
129
|
+
const link: ProjectLink = {
|
|
130
|
+
projectId: project.id,
|
|
131
|
+
projectName: project.name,
|
|
132
|
+
workspaceId: workspace.id,
|
|
133
|
+
workspaceName: workspace.name,
|
|
134
|
+
serviceId: service?.id,
|
|
135
|
+
serviceName: service?.name,
|
|
136
|
+
};
|
|
137
|
+
setProjectLink(link);
|
|
52
138
|
|
|
53
139
|
if (isJSONMode()) {
|
|
54
|
-
printJSON({
|
|
55
|
-
|
|
140
|
+
printJSON({
|
|
141
|
+
projectId: link.projectId,
|
|
142
|
+
projectName: link.projectName,
|
|
143
|
+
workspaceId: link.workspaceId,
|
|
144
|
+
workspaceName: link.workspaceName,
|
|
145
|
+
serviceId: link.serviceId,
|
|
146
|
+
serviceName: link.serviceName,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
const wsLabel = link.workspaceName ? chalk.dim(` (${link.workspaceName})`) : "";
|
|
56
150
|
success(
|
|
57
|
-
`
|
|
151
|
+
`Linked to ${chalk.bold(project.name)}${service ? ` / ${chalk.bold(service.name)}` : ""}${wsLabel}`,
|
|
58
152
|
);
|
|
59
|
-
} else {
|
|
60
|
-
success(`Linked to ${chalk.bold(projectName || projectId)}`);
|
|
61
153
|
}
|
|
62
154
|
});
|
|
63
155
|
}
|
package/src/commands/login.ts
CHANGED
|
@@ -131,15 +131,16 @@ export function registerLogin(program: Command) {
|
|
|
131
131
|
.description("Log in to Lizard")
|
|
132
132
|
.option("--token <token>", "Authenticate with an API token")
|
|
133
133
|
.action(async (opts) => {
|
|
134
|
-
|
|
134
|
+
const token = opts.token;
|
|
135
|
+
if (token) {
|
|
135
136
|
// Direct token auth — validate it
|
|
136
137
|
const res = await fetch(`${getBaseURL()}/api/auth/me`, {
|
|
137
|
-
headers: { Authorization: `Bearer ${
|
|
138
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
138
139
|
});
|
|
139
140
|
if (!res.ok) throw new Error("Invalid token");
|
|
140
141
|
const user = (await res.json()) as any;
|
|
141
142
|
saveCredentials({
|
|
142
|
-
accessToken:
|
|
143
|
+
accessToken: token,
|
|
143
144
|
userId: user.id,
|
|
144
145
|
username: user.username,
|
|
145
146
|
email: user.email,
|