@lizard-build/cli 0.1.0 → 0.3.30
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/AGENTS.md +113 -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/skill.d.ts +2 -0
- package/dist/commands/skill.js +146 -0
- package/dist/commands/skill.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 +209 -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/skill-data/core/SKILL.md +239 -0
- 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/skill.ts +157 -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 +219 -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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
let tmpDir: string;
|
|
7
|
+
let originalCwd: string;
|
|
8
|
+
let originalLizardHome: string | undefined;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalCwd = process.cwd();
|
|
12
|
+
originalLizardHome = process.env.LIZARD_HOME;
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lizard-resolve-test-"));
|
|
14
|
+
process.env.LIZARD_HOME = tmpDir;
|
|
15
|
+
process.chdir(tmpDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
process.chdir(originalCwd);
|
|
20
|
+
if (originalLizardHome === undefined) delete process.env.LIZARD_HOME;
|
|
21
|
+
else process.env.LIZARD_HOME = originalLizardHome;
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
vi.resetModules();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function withMockedApi(getHandler: (path: string) => unknown) {
|
|
28
|
+
vi.resetModules();
|
|
29
|
+
const calls: string[] = [];
|
|
30
|
+
vi.doMock("../../src/lib/api.js", async () => {
|
|
31
|
+
const actual = await vi.importActual<typeof import("../../src/lib/api.js")>(
|
|
32
|
+
"../../src/lib/api.js",
|
|
33
|
+
);
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
api: {
|
|
37
|
+
get: (p: string) => {
|
|
38
|
+
calls.push(p);
|
|
39
|
+
return Promise.resolve(getHandler(p));
|
|
40
|
+
},
|
|
41
|
+
post: () => Promise.reject(new Error("unexpected POST")),
|
|
42
|
+
put: () => Promise.reject(new Error("unexpected PUT")),
|
|
43
|
+
patch: () => Promise.reject(new Error("unexpected PATCH")),
|
|
44
|
+
delete: () => Promise.reject(new Error("unexpected DELETE")),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const resolve = await import("../../src/lib/resolve.js");
|
|
49
|
+
const config = await import("../../src/lib/config.js");
|
|
50
|
+
return { resolve, config, calls };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeLink(homeDir: string, link: Record<string, unknown>) {
|
|
54
|
+
// Key under process.cwd() — macOS symlinks /var → /private/var so the
|
|
55
|
+
// disk-stored path must match what getProjectLink() will lookup.
|
|
56
|
+
const cwdKey = process.cwd();
|
|
57
|
+
fs.mkdirSync(path.join(homeDir, ".lizard"), { recursive: true });
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
path.join(homeDir, ".lizard", "config.json"),
|
|
60
|
+
JSON.stringify({ projects: { [cwdKey]: link } }),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("resolveProjectScope", () => {
|
|
65
|
+
test("uses workspaceId from the link when present", async () => {
|
|
66
|
+
writeLink(tmpDir, {
|
|
67
|
+
projectId: "proj_1",
|
|
68
|
+
workspaceId: "ws_already",
|
|
69
|
+
workspaceName: "team",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { resolve, calls } = await withMockedApi(() => null);
|
|
73
|
+
const { projectId, scope } = await resolve.resolveProjectScope();
|
|
74
|
+
|
|
75
|
+
expect(projectId).toBe("proj_1");
|
|
76
|
+
expect(scope.workspaceId).toBe("ws_already");
|
|
77
|
+
// No need to fetch the project — workspaceId was already on disk.
|
|
78
|
+
expect(calls.filter((c) => c.startsWith("/api/projects/proj_1"))).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("lazy-fills workspaceId when missing and updates the link on disk", async () => {
|
|
82
|
+
writeLink(tmpDir, { projectId: "proj_legacy", projectName: "old" });
|
|
83
|
+
|
|
84
|
+
const { resolve, config, calls } = await withMockedApi((p) =>
|
|
85
|
+
p === "/api/projects/proj_legacy"
|
|
86
|
+
? { workspaceId: "ws_fetched", workspaceName: "fetched-ws" }
|
|
87
|
+
: null,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const { scope } = await resolve.resolveProjectScope();
|
|
91
|
+
expect(scope.workspaceId).toBe("ws_fetched");
|
|
92
|
+
expect(calls).toContain("/api/projects/proj_legacy");
|
|
93
|
+
|
|
94
|
+
const linkAfter = config.getProjectLink(process.cwd());
|
|
95
|
+
expect(linkAfter?.workspaceId).toBe("ws_fetched");
|
|
96
|
+
expect(linkAfter?.workspaceName).toBe("fetched-ws");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns null scope when the lookup fails", async () => {
|
|
100
|
+
writeLink(tmpDir, { projectId: "proj_unknown" });
|
|
101
|
+
const { resolve } = await withMockedApi(() => {
|
|
102
|
+
throw new Error("404");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const { scope } = await resolve.resolveProjectScope();
|
|
106
|
+
expect(scope.workspaceId).toBeFalsy();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("getScope", () => {
|
|
111
|
+
test("returns normalized scope from a resolved context", async () => {
|
|
112
|
+
vi.resetModules();
|
|
113
|
+
const { getScope } = await import("../../src/lib/resolve.js");
|
|
114
|
+
expect(
|
|
115
|
+
getScope({
|
|
116
|
+
projectId: "x",
|
|
117
|
+
workspaceId: "ws_1",
|
|
118
|
+
}),
|
|
119
|
+
).toEqual({ workspaceId: "ws_1" });
|
|
120
|
+
expect(getScope({ projectId: "x" })).toEqual({
|
|
121
|
+
workspaceId: null,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
flattenPatch,
|
|
4
|
+
validateSetPath,
|
|
5
|
+
validateName,
|
|
6
|
+
parseValue,
|
|
7
|
+
setDeep,
|
|
8
|
+
SERVICE_FIELDS,
|
|
9
|
+
} from "../../src/commands/service-set.js";
|
|
10
|
+
|
|
11
|
+
const ID = "app_123";
|
|
12
|
+
const NAME = "api";
|
|
13
|
+
const NAMES = new Map([[ID, NAME]]);
|
|
14
|
+
|
|
15
|
+
// ── flattenPatch: canonical flat shape ─────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("flattenPatch — flat canonical shape", () => {
|
|
18
|
+
test("flat cfg fields land 1:1 on the wire", () => {
|
|
19
|
+
const body = flattenPatch(
|
|
20
|
+
{
|
|
21
|
+
services: {
|
|
22
|
+
[ID]: {
|
|
23
|
+
buildCommand: "npm run build",
|
|
24
|
+
startCommand: "node dist/index.js",
|
|
25
|
+
preDeployCommand: "npm run migrate",
|
|
26
|
+
healthcheckPath: "/health",
|
|
27
|
+
healthcheckTimeoutMs: 5000,
|
|
28
|
+
sourceType: "github",
|
|
29
|
+
repoUrl: "https://github.com/acme/api",
|
|
30
|
+
branch: "main",
|
|
31
|
+
rootDirectory: "apps/api",
|
|
32
|
+
watchPatterns: ["apps/api/**"],
|
|
33
|
+
dockerfilePath: "apps/api/Dockerfile",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
NAMES,
|
|
38
|
+
);
|
|
39
|
+
expect(body).toEqual({
|
|
40
|
+
services: [
|
|
41
|
+
{
|
|
42
|
+
id: ID,
|
|
43
|
+
buildCommand: "npm run build",
|
|
44
|
+
startCommand: "node dist/index.js",
|
|
45
|
+
preDeployCommand: "npm run migrate",
|
|
46
|
+
healthcheckPath: "/health",
|
|
47
|
+
healthcheckTimeoutMs: 5000,
|
|
48
|
+
sourceType: "github",
|
|
49
|
+
repoUrl: "https://github.com/acme/api",
|
|
50
|
+
branch: "main",
|
|
51
|
+
rootDirectory: "apps/api",
|
|
52
|
+
watchPatterns: ["apps/api/**"],
|
|
53
|
+
dockerfilePath: "apps/api/Dockerfile",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("empty cfg yields service entry with id only (no implicit name)", () => {
|
|
60
|
+
const body = flattenPatch({ services: { [ID]: {} } }, NAMES);
|
|
61
|
+
expect(body.services).toEqual([{ id: ID }]);
|
|
62
|
+
expect(body.services[0]).not.toHaveProperty("name");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("each canonical field is independently round-trippable", () => {
|
|
66
|
+
for (const f of SERVICE_FIELDS) {
|
|
67
|
+
if (f === "name") continue;
|
|
68
|
+
const cfg: Record<string, unknown> = {};
|
|
69
|
+
cfg[f] = f === "healthcheckTimeoutMs" ? 1234 : f === "watchPatterns" ? ["x"] : "v";
|
|
70
|
+
const body = flattenPatch({ services: { [ID]: cfg } }, NAMES);
|
|
71
|
+
expect(body.services[0]).toMatchObject({ id: ID, [f]: cfg[f] });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── flattenPatch: legacy dotted prefixes are gone ──────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("flattenPatch — legacy prefixed paths throw", () => {
|
|
79
|
+
test.each([
|
|
80
|
+
["build", { buildCommand: "npm run build" }],
|
|
81
|
+
["deploy", { startCommand: "node dist/index.js" }],
|
|
82
|
+
["source", { type: "github" }],
|
|
83
|
+
])("nested '%s' block is rejected as unknown field", (prefix, inner) => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
flattenPatch({ services: { [ID]: { [prefix]: inner } } }, NAMES),
|
|
86
|
+
).toThrow(/Unknown field '\w+' in service "api"/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("rejects arbitrary opaque keys with hint to --help", () => {
|
|
90
|
+
expect(() =>
|
|
91
|
+
flattenPatch({ services: { [ID]: { foo: "bar" } } }, NAMES),
|
|
92
|
+
).toThrow(/Unknown field 'foo'.*service set --help/s);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── rename ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe("flattenPatch — rename", () => {
|
|
99
|
+
test("emits 'name' only when it differs from current", () => {
|
|
100
|
+
const body = flattenPatch(
|
|
101
|
+
{ services: { [ID]: { name: "api-v2" } } },
|
|
102
|
+
NAMES,
|
|
103
|
+
);
|
|
104
|
+
expect(body.services).toEqual([{ id: ID, name: "api-v2" }]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("equal name is dropped (no fake audit-log churn)", () => {
|
|
108
|
+
const body = flattenPatch(
|
|
109
|
+
{ services: { [ID]: { name: NAME, buildCommand: "x" } } },
|
|
110
|
+
NAMES,
|
|
111
|
+
);
|
|
112
|
+
expect(body.services[0]).not.toHaveProperty("name");
|
|
113
|
+
expect(body.services[0]).toMatchObject({ id: ID, buildCommand: "x" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("invalid new name throws before send", () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
flattenPatch({ services: { [ID]: { name: "Foo Bar" } } }, NAMES),
|
|
119
|
+
).toThrow(/Invalid 'name'.*lowercase/);
|
|
120
|
+
expect(() =>
|
|
121
|
+
flattenPatch({ services: { [ID]: { name: "-leadinghyphen" } } }, NAMES),
|
|
122
|
+
).toThrow(/Invalid 'name'/);
|
|
123
|
+
expect(() =>
|
|
124
|
+
flattenPatch({ services: { [ID]: { name: "x".repeat(41) } } }, NAMES),
|
|
125
|
+
).toThrow(/40 characters or fewer/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("unknown service id throws with clear message", () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
flattenPatch(
|
|
131
|
+
{ services: { ["app_does_not_exist"]: { buildCommand: "x" } } },
|
|
132
|
+
NAMES,
|
|
133
|
+
),
|
|
134
|
+
).toThrow(/no longer exists/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── variables / secrets namespaces ─────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
describe("flattenPatch — variables and secrets", () => {
|
|
141
|
+
test("variables.<KEY>=v lands in secrets.services[<name>]", () => {
|
|
142
|
+
const body = flattenPatch(
|
|
143
|
+
{ services: { [ID]: { variables: { PORT: "3000" } } } },
|
|
144
|
+
NAMES,
|
|
145
|
+
);
|
|
146
|
+
expect(body.services[0]).toEqual({ id: ID });
|
|
147
|
+
expect(body.secrets).toEqual({ services: { [NAME]: { PORT: "3000" } } });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("variables.<KEY>.value template form unwraps to its string", () => {
|
|
151
|
+
const body = flattenPatch(
|
|
152
|
+
{
|
|
153
|
+
services: {
|
|
154
|
+
[ID]: {
|
|
155
|
+
variables: { DB_URL: { value: "${{ postgres.DATABASE_URL }}" } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
NAMES,
|
|
160
|
+
);
|
|
161
|
+
expect(body.secrets!.services[NAME].DB_URL).toBe(
|
|
162
|
+
"${{ postgres.DATABASE_URL }}",
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("variables.<KEY>=null deletes (passed through as null)", () => {
|
|
167
|
+
const body = flattenPatch(
|
|
168
|
+
{ services: { [ID]: { variables: { OLD: null } } } },
|
|
169
|
+
NAMES,
|
|
170
|
+
);
|
|
171
|
+
expect(body.secrets!.services[NAME]).toEqual({ OLD: null });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("top-level sharedVariables → secrets.shared", () => {
|
|
175
|
+
const body = flattenPatch(
|
|
176
|
+
{
|
|
177
|
+
services: { [ID]: {} },
|
|
178
|
+
sharedVariables: { LOG_LEVEL: "debug" },
|
|
179
|
+
},
|
|
180
|
+
NAMES,
|
|
181
|
+
);
|
|
182
|
+
expect(body.secrets!.shared).toEqual({ LOG_LEVEL: "debug" });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("explicit top-level secrets.services and secrets.shared pass through and merge", () => {
|
|
186
|
+
const body = flattenPatch(
|
|
187
|
+
{
|
|
188
|
+
services: { [ID]: { variables: { A: "1" } } },
|
|
189
|
+
secrets: {
|
|
190
|
+
services: { [NAME]: { B: "2" } },
|
|
191
|
+
shared: { GLOBAL: "g" },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
NAMES,
|
|
195
|
+
);
|
|
196
|
+
expect(body.secrets!.services[NAME]).toEqual({ A: "1", B: "2" });
|
|
197
|
+
expect(body.secrets!.shared).toEqual({ GLOBAL: "g" });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── rename + secrets: CLI keys per-service secrets by pre-rename name ─────
|
|
202
|
+
|
|
203
|
+
describe("flattenPatch — rename combined with secrets", () => {
|
|
204
|
+
// The backend's secrets.services lookup uses a pre-rename snapshot of the
|
|
205
|
+
// apps table, so per-service secret keys must match the pre-rename name.
|
|
206
|
+
// The CLI keys variables/envVars by `currentName` (= pre-rename) on its
|
|
207
|
+
// own, so this combination produces a body the backend accepts.
|
|
208
|
+
test("rename + variables.X — variables stay keyed by pre-rename name", () => {
|
|
209
|
+
const body = flattenPatch(
|
|
210
|
+
{ services: { [ID]: { name: "api-v2", variables: { K: "v" } } } },
|
|
211
|
+
NAMES,
|
|
212
|
+
);
|
|
213
|
+
expect(body.services).toEqual([{ id: ID, name: "api-v2" }]);
|
|
214
|
+
expect(body.secrets).toEqual({ services: { [NAME]: { K: "v" } } });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("rename + explicit secrets.services keyed by old name passes through", () => {
|
|
218
|
+
const body = flattenPatch(
|
|
219
|
+
{
|
|
220
|
+
services: { [ID]: { name: "api-v2" } },
|
|
221
|
+
secrets: { services: { [NAME]: { K: "v" } } },
|
|
222
|
+
},
|
|
223
|
+
NAMES,
|
|
224
|
+
);
|
|
225
|
+
expect(body.services).toEqual([{ id: ID, name: "api-v2" }]);
|
|
226
|
+
expect(body.secrets!.services[NAME]).toEqual({ K: "v" });
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ── --set path validation ──────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
describe("validateSetPath", () => {
|
|
233
|
+
test.each(SERVICE_FIELDS)("accepts canonical field '%s'", (f) => {
|
|
234
|
+
expect(() => validateSetPath(f)).not.toThrow();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test.each([
|
|
238
|
+
"variables.PORT",
|
|
239
|
+
"variables.DB_URL.value",
|
|
240
|
+
"variables.A_LONG_KEY_123",
|
|
241
|
+
])("accepts variables form '%s'", (path) => {
|
|
242
|
+
expect(() => validateSetPath(path)).not.toThrow();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test.each([
|
|
246
|
+
"build.buildCommand",
|
|
247
|
+
"deploy.startCommand",
|
|
248
|
+
"deploy.buildCommand",
|
|
249
|
+
"deploy.preDeployCommand",
|
|
250
|
+
"source.type",
|
|
251
|
+
"source.repoUrl",
|
|
252
|
+
"source.branch",
|
|
253
|
+
])("rejects legacy prefixed path '%s'", (path) => {
|
|
254
|
+
expect(() => validateSetPath(path)).toThrow(/Unknown --set field/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("rejects arbitrary typos with --help hint", () => {
|
|
258
|
+
expect(() => validateSetPath("buld.buildCommand")).toThrow(
|
|
259
|
+
/Unknown --set field.*service set --help/s,
|
|
260
|
+
);
|
|
261
|
+
expect(() => validateSetPath("foo")).toThrow(/Unknown --set field/);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("rejects malformed variables paths", () => {
|
|
265
|
+
expect(() => validateSetPath("variables")).toThrow(/Unknown --set field/);
|
|
266
|
+
expect(() => validateSetPath("variables.")).toThrow(/Invalid --set path/);
|
|
267
|
+
expect(() => validateSetPath("variables.K.notvalue")).toThrow(
|
|
268
|
+
/Invalid --set path/,
|
|
269
|
+
);
|
|
270
|
+
expect(() => validateSetPath("variables.K.value.extra")).toThrow(
|
|
271
|
+
/Invalid --set path/,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── validateName mirror of backend ─────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe("validateName", () => {
|
|
279
|
+
test.each(["api", "api-v2", "x", "a1", "my-cool-service-99"])(
|
|
280
|
+
"accepts valid name '%s'",
|
|
281
|
+
(n) => {
|
|
282
|
+
expect(validateName(n)).toBeNull();
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
test.each([
|
|
287
|
+
["", /required/],
|
|
288
|
+
["x".repeat(41), /40 characters or fewer/],
|
|
289
|
+
["Api", /lowercase/],
|
|
290
|
+
["-api", /lowercase/],
|
|
291
|
+
["api-", /lowercase/],
|
|
292
|
+
["api_v2", /lowercase/],
|
|
293
|
+
["api/v2", /lowercase/],
|
|
294
|
+
["api v2", /lowercase/],
|
|
295
|
+
])("rejects '%s' with reason matching %s", (n, re) => {
|
|
296
|
+
const msg = validateName(n);
|
|
297
|
+
expect(msg).not.toBeNull();
|
|
298
|
+
expect(msg!).toMatch(re);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ── parseValue ──────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
describe("parseValue", () => {
|
|
305
|
+
test("healthcheckTimeoutMs coerces to number", () => {
|
|
306
|
+
expect(parseValue("healthcheckTimeoutMs", "5000")).toBe(5000);
|
|
307
|
+
expect(() => parseValue("healthcheckTimeoutMs", "fast")).toThrow(
|
|
308
|
+
/expects a number/,
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("watchPatterns: JSON array form", () => {
|
|
313
|
+
expect(parseValue("watchPatterns", '["src/**","apps/api/**"]')).toEqual([
|
|
314
|
+
"src/**",
|
|
315
|
+
"apps/api/**",
|
|
316
|
+
]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("watchPatterns: comma-separated form", () => {
|
|
320
|
+
expect(parseValue("watchPatterns", "src/**, apps/api/**, ")).toEqual([
|
|
321
|
+
"src/**",
|
|
322
|
+
"apps/api/**",
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("plain string field passes through as-is (preserves whitespace)", () => {
|
|
327
|
+
expect(parseValue("startCommand", "node dist/index.js")).toBe(
|
|
328
|
+
"node dist/index.js",
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("JSON-shaped string is parsed when valid, kept raw on parse fail", () => {
|
|
333
|
+
expect(parseValue("repoUrl", '{"a":1}')).toEqual({ a: 1 });
|
|
334
|
+
expect(parseValue("repoUrl", "{not json}")).toBe("{not json}");
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ── setDeep ─────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
describe("setDeep", () => {
|
|
341
|
+
test("writes a flat key", () => {
|
|
342
|
+
const o: any = {};
|
|
343
|
+
setDeep(o, "buildCommand", "foo");
|
|
344
|
+
expect(o).toEqual({ buildCommand: "foo" });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("creates nested objects as needed", () => {
|
|
348
|
+
const o: any = {};
|
|
349
|
+
setDeep(o, "variables.PORT", "3000");
|
|
350
|
+
setDeep(o, "variables.DB_URL.value", "x");
|
|
351
|
+
expect(o).toEqual({
|
|
352
|
+
variables: { PORT: "3000", DB_URL: { value: "x" } },
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
package/vitest.config.ts
ADDED
package/dist/commands/connect.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import * as p from "@clack/prompts";
|
|
4
|
-
import { api } from "../lib/api.js";
|
|
5
|
-
import { resolveProjectId } from "../lib/config.js";
|
|
6
|
-
import { info, isJSONMode, printJSON, isTTY } from "../lib/format.js";
|
|
7
|
-
const CLIENT_COMMANDS = {
|
|
8
|
-
postgres: "psql",
|
|
9
|
-
mysql: "mysql",
|
|
10
|
-
mongodb: "mongosh",
|
|
11
|
-
redis: "redis-cli",
|
|
12
|
-
};
|
|
13
|
-
export function registerConnect(program) {
|
|
14
|
-
program
|
|
15
|
-
.command("connect")
|
|
16
|
-
.argument("[service]", "Service type or ID (postgres, redis, etc.)")
|
|
17
|
-
.description("Connect to a managed service")
|
|
18
|
-
.option("--url", "Print connection string without connecting")
|
|
19
|
-
.action(async (service, opts) => {
|
|
20
|
-
const projectId = resolveProjectId(program.opts().project);
|
|
21
|
-
// Get addons
|
|
22
|
-
const addons = await api.get(`/api/projects/${projectId}/addons`);
|
|
23
|
-
if (addons.length === 0) {
|
|
24
|
-
throw new Error("No managed services in this project. Use `lizard add`.");
|
|
25
|
-
}
|
|
26
|
-
let addon;
|
|
27
|
-
if (service) {
|
|
28
|
-
// Match by type or ID
|
|
29
|
-
addon =
|
|
30
|
-
addons.find((a) => a.addonType === service) ||
|
|
31
|
-
addons.find((a) => a.id === service) ||
|
|
32
|
-
addons.find((a) => a.name === service);
|
|
33
|
-
}
|
|
34
|
-
else if (addons.length === 1) {
|
|
35
|
-
addon = addons[0];
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
if (!isTTY()) {
|
|
39
|
-
throw new Error("Multiple services found. Specify one: " +
|
|
40
|
-
addons.map((a) => a.addonType || a.name).join(", "));
|
|
41
|
-
}
|
|
42
|
-
const selected = await p.select({
|
|
43
|
-
message: "Select service to connect to",
|
|
44
|
-
options: addons.map((a) => ({
|
|
45
|
-
value: a.id,
|
|
46
|
-
label: a.name || a.addonType,
|
|
47
|
-
hint: a.addonType,
|
|
48
|
-
})),
|
|
49
|
-
});
|
|
50
|
-
if (p.isCancel(selected))
|
|
51
|
-
process.exit(5);
|
|
52
|
-
addon = addons.find((a) => a.id === selected);
|
|
53
|
-
}
|
|
54
|
-
if (!addon) {
|
|
55
|
-
throw new Error(`Service "${service}" not found`);
|
|
56
|
-
}
|
|
57
|
-
if (addon.status !== "running") {
|
|
58
|
-
throw new Error(`Service is ${addon.status}, not running`);
|
|
59
|
-
}
|
|
60
|
-
// Build connection string from secrets
|
|
61
|
-
const secrets = await api.get(`/api/projects/${projectId}/secrets`);
|
|
62
|
-
const connString = findConnectionString(addon.addonType, secrets);
|
|
63
|
-
if (opts.url || isJSONMode()) {
|
|
64
|
-
if (isJSONMode()) {
|
|
65
|
-
printJSON({ type: addon.addonType, connectionString: connString });
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
console.log(connString || "Connection string not found in secrets");
|
|
69
|
-
}
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (!connString) {
|
|
73
|
-
throw new Error("Connection string not found in project secrets. Check `lizard secret list --show`.");
|
|
74
|
-
}
|
|
75
|
-
// Connect using native client
|
|
76
|
-
const clientCmd = CLIENT_COMMANDS[addon.addonType];
|
|
77
|
-
if (!clientCmd) {
|
|
78
|
-
info(`Connection string: ${connString}`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
info(chalk.dim(`Connecting via ${clientCmd}...\n`));
|
|
82
|
-
try {
|
|
83
|
-
if (addon.addonType === "postgres") {
|
|
84
|
-
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
85
|
-
}
|
|
86
|
-
else if (addon.addonType === "redis") {
|
|
87
|
-
// redis-cli -u redis://...
|
|
88
|
-
execSync(`${clientCmd} -u "${connString}"`, { stdio: "inherit" });
|
|
89
|
-
}
|
|
90
|
-
else if (addon.addonType === "mysql") {
|
|
91
|
-
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
92
|
-
}
|
|
93
|
-
else if (addon.addonType === "mongodb") {
|
|
94
|
-
execSync(`${clientCmd} "${connString}"`, { stdio: "inherit" });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
process.exit(err.status || 1);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
function findConnectionString(type, secrets) {
|
|
103
|
-
const envKeys = {
|
|
104
|
-
postgres: ["DATABASE_URL", "POSTGRES_URL", "PG_URL"],
|
|
105
|
-
mysql: ["MYSQL_URL", "DATABASE_URL"],
|
|
106
|
-
mongodb: ["MONGODB_URL", "MONGO_URL"],
|
|
107
|
-
redis: ["REDIS_URL"],
|
|
108
|
-
};
|
|
109
|
-
const keys = envKeys[type] || [];
|
|
110
|
-
for (const key of keys) {
|
|
111
|
-
const s = secrets.find((s) => s.key === key);
|
|
112
|
-
if (s)
|
|
113
|
-
return s.value;
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
//# sourceMappingURL=connect.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"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"}
|