@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.
Files changed (184) hide show
  1. package/.github/workflows/release.yml +90 -0
  2. package/AGENTS.md +113 -0
  3. package/README.md +41 -0
  4. package/dist/commands/add.js +318 -45
  5. package/dist/commands/add.js.map +1 -1
  6. package/dist/commands/config.d.ts +2 -0
  7. package/dist/commands/config.js +68 -0
  8. package/dist/commands/config.js.map +1 -0
  9. package/dist/commands/docs.d.ts +2 -0
  10. package/dist/commands/docs.js +13 -0
  11. package/dist/commands/docs.js.map +1 -0
  12. package/dist/commands/domain.d.ts +9 -0
  13. package/dist/commands/domain.js +195 -0
  14. package/dist/commands/domain.js.map +1 -0
  15. package/dist/commands/git.js +175 -36
  16. package/dist/commands/git.js.map +1 -1
  17. package/dist/commands/init.d.ts +24 -0
  18. package/dist/commands/init.js +128 -86
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/link.d.ts +7 -0
  21. package/dist/commands/link.js +104 -33
  22. package/dist/commands/link.js.map +1 -1
  23. package/dist/commands/login.js +4 -3
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/commands/logs.js +223 -30
  26. package/dist/commands/logs.js.map +1 -1
  27. package/dist/commands/open.js +3 -2
  28. package/dist/commands/open.js.map +1 -1
  29. package/dist/commands/port.d.ts +7 -0
  30. package/dist/commands/port.js +49 -0
  31. package/dist/commands/port.js.map +1 -0
  32. package/dist/commands/projects.js +36 -6
  33. package/dist/commands/projects.js.map +1 -1
  34. package/dist/commands/ps.js +32 -39
  35. package/dist/commands/ps.js.map +1 -1
  36. package/dist/commands/redeploy.js +48 -8
  37. package/dist/commands/redeploy.js.map +1 -1
  38. package/dist/commands/regions.js +2 -5
  39. package/dist/commands/regions.js.map +1 -1
  40. package/dist/commands/restart.js +84 -10
  41. package/dist/commands/restart.js.map +1 -1
  42. package/dist/commands/run.d.ts +9 -0
  43. package/dist/commands/run.js +61 -22
  44. package/dist/commands/run.js.map +1 -1
  45. package/dist/commands/scale.d.ts +10 -0
  46. package/dist/commands/scale.js +166 -0
  47. package/dist/commands/scale.js.map +1 -0
  48. package/dist/commands/secrets.js +200 -89
  49. package/dist/commands/secrets.js.map +1 -1
  50. package/dist/commands/service-set.d.ts +49 -0
  51. package/dist/commands/service-set.js +552 -0
  52. package/dist/commands/service-set.js.map +1 -0
  53. package/dist/commands/service-show.d.ts +11 -0
  54. package/dist/commands/service-show.js +44 -0
  55. package/dist/commands/service-show.js.map +1 -0
  56. package/dist/commands/service.d.ts +8 -0
  57. package/dist/commands/service.js +262 -0
  58. package/dist/commands/service.js.map +1 -0
  59. package/dist/commands/skill.d.ts +2 -0
  60. package/dist/commands/skill.js +146 -0
  61. package/dist/commands/skill.js.map +1 -0
  62. package/dist/commands/ssh.d.ts +2 -0
  63. package/dist/commands/ssh.js +161 -0
  64. package/dist/commands/ssh.js.map +1 -0
  65. package/dist/commands/status.d.ts +7 -0
  66. package/dist/commands/status.js +49 -38
  67. package/dist/commands/status.js.map +1 -1
  68. package/dist/commands/unlink.d.ts +5 -0
  69. package/dist/commands/unlink.js +18 -0
  70. package/dist/commands/unlink.js.map +1 -0
  71. package/dist/commands/up.d.ts +9 -0
  72. package/dist/commands/up.js +417 -0
  73. package/dist/commands/up.js.map +1 -0
  74. package/dist/commands/upgrade.d.ts +2 -0
  75. package/dist/commands/upgrade.js +79 -0
  76. package/dist/commands/upgrade.js.map +1 -0
  77. package/dist/commands/whoami.js +26 -6
  78. package/dist/commands/whoami.js.map +1 -1
  79. package/dist/commands/workspace.d.ts +8 -0
  80. package/dist/commands/workspace.js +36 -0
  81. package/dist/commands/workspace.js.map +1 -0
  82. package/dist/index.js +209 -82
  83. package/dist/index.js.map +1 -1
  84. package/dist/lib/api.d.ts +17 -2
  85. package/dist/lib/api.js +85 -51
  86. package/dist/lib/api.js.map +1 -1
  87. package/dist/lib/auth.d.ts +3 -11
  88. package/dist/lib/auth.js +16 -36
  89. package/dist/lib/auth.js.map +1 -1
  90. package/dist/lib/config.d.ts +36 -15
  91. package/dist/lib/config.js +71 -58
  92. package/dist/lib/config.js.map +1 -1
  93. package/dist/lib/format.d.ts +1 -0
  94. package/dist/lib/format.js +17 -4
  95. package/dist/lib/format.js.map +1 -1
  96. package/dist/lib/name.d.ts +11 -0
  97. package/dist/lib/name.js +26 -0
  98. package/dist/lib/name.js.map +1 -0
  99. package/dist/lib/picker.d.ts +32 -0
  100. package/dist/lib/picker.js +91 -0
  101. package/dist/lib/picker.js.map +1 -0
  102. package/dist/lib/resolve.d.ts +85 -0
  103. package/dist/lib/resolve.js +203 -0
  104. package/dist/lib/resolve.js.map +1 -0
  105. package/dist/lib/updater.d.ts +16 -0
  106. package/dist/lib/updater.js +102 -0
  107. package/dist/lib/updater.js.map +1 -0
  108. package/lizard-wrapper.sh +2 -0
  109. package/package.json +11 -3
  110. package/skill-data/core/SKILL.md +239 -0
  111. package/src/commands/add.ts +388 -56
  112. package/src/commands/config.ts +80 -0
  113. package/src/commands/docs.ts +15 -0
  114. package/src/commands/domain.ts +248 -0
  115. package/src/commands/git.ts +201 -40
  116. package/src/commands/init.ts +149 -100
  117. package/src/commands/link.ts +127 -35
  118. package/src/commands/login.ts +4 -3
  119. package/src/commands/logs.ts +283 -27
  120. package/src/commands/open.ts +3 -2
  121. package/src/commands/port.ts +57 -0
  122. package/src/commands/projects.ts +43 -6
  123. package/src/commands/ps.ts +39 -60
  124. package/src/commands/redeploy.ts +51 -10
  125. package/src/commands/regions.ts +2 -6
  126. package/src/commands/restart.ts +84 -10
  127. package/src/commands/run.ts +68 -24
  128. package/src/commands/scale.ts +216 -0
  129. package/src/commands/secrets.ts +277 -100
  130. package/src/commands/service-set.ts +669 -0
  131. package/src/commands/service-show.ts +52 -0
  132. package/src/commands/service.ts +298 -0
  133. package/src/commands/skill.ts +157 -0
  134. package/src/commands/ssh.ts +176 -0
  135. package/src/commands/status.ts +51 -46
  136. package/src/commands/unlink.ts +17 -0
  137. package/src/commands/up.ts +461 -0
  138. package/src/commands/upgrade.ts +87 -0
  139. package/src/commands/whoami.ts +34 -6
  140. package/src/commands/workspace.ts +44 -0
  141. package/src/index.ts +219 -85
  142. package/src/lib/api.ts +114 -51
  143. package/src/lib/auth.ts +22 -46
  144. package/src/lib/config.ts +100 -65
  145. package/src/lib/format.ts +18 -4
  146. package/src/lib/name.ts +27 -0
  147. package/src/lib/picker.ts +133 -0
  148. package/src/lib/resolve.ts +285 -0
  149. package/src/lib/updater.ts +106 -0
  150. package/test/cli.test.ts +491 -0
  151. package/test/fixtures/hello-app/Dockerfile +5 -0
  152. package/test/fixtures/hello-app/index.js +5 -0
  153. package/test/unit/api.test.ts +66 -0
  154. package/test/unit/config.test.ts +94 -0
  155. package/test/unit/init.test.ts +211 -0
  156. package/test/unit/json.test.ts +208 -0
  157. package/test/unit/picker.test.ts +161 -0
  158. package/test/unit/resolve.test.ts +124 -0
  159. package/test/unit/service-set.test.ts +355 -0
  160. package/vitest.config.ts +10 -0
  161. package/dist/commands/connect.d.ts +0 -2
  162. package/dist/commands/connect.js +0 -117
  163. package/dist/commands/connect.js.map +0 -1
  164. package/dist/commands/context.d.ts +0 -2
  165. package/dist/commands/context.js +0 -71
  166. package/dist/commands/context.js.map +0 -1
  167. package/dist/commands/deploy.d.ts +0 -2
  168. package/dist/commands/deploy.js +0 -120
  169. package/dist/commands/deploy.js.map +0 -1
  170. package/dist/commands/destroy.d.ts +0 -2
  171. package/dist/commands/destroy.js +0 -51
  172. package/dist/commands/destroy.js.map +0 -1
  173. package/dist/commands/update.d.ts +0 -2
  174. package/dist/commands/update.js +0 -41
  175. package/dist/commands/update.js.map +0 -1
  176. package/dist/commands/version.d.ts +0 -2
  177. package/dist/commands/version.js +0 -37
  178. package/dist/commands/version.js.map +0 -1
  179. package/src/commands/connect.ts +0 -145
  180. package/src/commands/context.ts +0 -93
  181. package/src/commands/deploy.ts +0 -153
  182. package/src/commands/destroy.ts +0 -51
  183. package/src/commands/update.ts +0 -44
  184. package/src/commands/version.ts +0 -37
@@ -0,0 +1,90 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ id-token: write # required for npm OIDC Trusted Publisher
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ token: ${{ secrets.GITHUB_TOKEN }}
20
+
21
+ - name: Configure git
22
+ run: |
23
+ git config user.name "github-actions[bot]"
24
+ git config user.email "github-actions[bot]@users.noreply.github.com"
25
+
26
+ - name: Bump patch version
27
+ id: bump
28
+ run: |
29
+ CURRENT=$(node -p "require('./package.json').version")
30
+ IFS='.' read -r MAJ MIN PAT <<< "$CURRENT"
31
+ NEW="$MAJ.$MIN.$((PAT + 1))"
32
+ echo "version=$NEW" >> $GITHUB_OUTPUT
33
+
34
+ node -e "
35
+ const fs = require('fs');
36
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
37
+ pkg.version = '$NEW';
38
+ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
39
+ "
40
+ sed -i "s/export const CURRENT_VERSION = \".*\"/export const CURRENT_VERSION = \"$NEW\"/" src/lib/updater.ts
41
+
42
+ git add package.json src/lib/updater.ts
43
+ git commit -m "chore: bump version to v$NEW [skip ci]"
44
+ git tag "v$NEW"
45
+ git push origin main --tags
46
+
47
+ - uses: actions/setup-node@v4
48
+ with:
49
+ node-version: '22'
50
+
51
+ - name: Upgrade npm
52
+ run: npm install -g npm@latest
53
+
54
+ - name: Install dependencies
55
+ run: npm ci
56
+
57
+ - name: Build
58
+ run: npm run build
59
+
60
+ - name: Publish to npm
61
+ run: npm publish --provenance --access public
62
+
63
+ - uses: oven-sh/setup-bun@v2
64
+ with:
65
+ bun-version: latest
66
+
67
+ - name: Install dependencies (bun)
68
+ run: bun install
69
+
70
+ - name: Build binaries
71
+ run: |
72
+ mkdir -p bin
73
+ bun build --compile --target=bun-darwin-arm64 src/index.ts --outfile bin/lizard-darwin-arm64
74
+ bun build --compile --target=bun-darwin-x64 src/index.ts --outfile bin/lizard-darwin-x64
75
+ bun build --compile --target=bun-linux-x64 src/index.ts --outfile bin/lizard-linux-x64
76
+ bun build --compile --target=bun-linux-arm64 src/index.ts --outfile bin/lizard-linux-arm64
77
+ bun build --compile --target=bun-windows-x64 src/index.ts --outfile bin/lizard-windows-x64.exe
78
+
79
+ - name: Create GitHub release
80
+ uses: softprops/action-gh-release@v2
81
+ with:
82
+ tag_name: v${{ steps.bump.outputs.version }}
83
+ name: v${{ steps.bump.outputs.version }}
84
+ files: |
85
+ bin/lizard-darwin-arm64
86
+ bin/lizard-darwin-x64
87
+ bin/lizard-linux-x64
88
+ bin/lizard-linux-arm64
89
+ bin/lizard-windows-x64.exe
90
+ generate_release_notes: true
package/AGENTS.md ADDED
@@ -0,0 +1,113 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for AI coding agents working in this repo (`@lizard-build/cli`).
4
+
5
+ ## Package manager
6
+
7
+ This project uses **npm**. Don't introduce pnpm/yarn lockfiles. Scripts:
8
+
9
+ - `npm run build` — TypeScript compile to `dist/`
10
+ - `npm run dev` — run from source via `tsx`
11
+ - `npm test` — integration tests (`test/cli.test.ts`)
12
+ - `npm run test:unit` — unit tests (`test/unit/*`)
13
+
14
+ The published `bin` is `dist/index.js`. Always run `npm run build` before testing against `dist/`.
15
+
16
+ ## Project layout
17
+
18
+ ```
19
+ src/
20
+ index.ts # Commander root, banner, preAction (auth + JSON mode), --help --json dump
21
+ commands/<name>.ts # one file per top-level command; exports register<Name>(program)
22
+ lib/
23
+ api.ts # fetch wrapper, APIError
24
+ auth.ts # token store, requireAuth()
25
+ config.ts # ~/.lizard/config.json read/write
26
+ format.ts # printJSON, isJSONMode, success/error/info/warn, table, statusColor
27
+ picker.ts # interactive @clack pickers
28
+ resolve.ts # resolve workspace/project/service from flags or linked dir
29
+ updater.ts # self-update (precompiled binary only — see note below)
30
+ skill-data/<name>/SKILL.md # agent skills served by `lizard skill get <name>`
31
+ dist/ # tsc output, committed; this is what npm ships
32
+ test/cli.test.ts # integration tests (spawn the CLI)
33
+ test/unit/*.test.ts # unit tests
34
+ ```
35
+
36
+ ## Code style
37
+
38
+ - Kebab-case for CLI flags (`--service-name`, never `--serviceName`).
39
+ - No emojis in code, output, or docs. Unicode glyphs (`✓`, `✗`, `→`) via `chalk` are fine.
40
+ - Colors: use `chalk` and `lib/format.ts` helpers. Never hardcode ANSI escapes.
41
+ - `info`/`success`/`warn`/`error` write to **stderr**; payload (`printJSON`, table rows) goes to **stdout**. Pipes (`| jq`, `> file`) must work.
42
+ - Strict TypeScript. No `any` in new code unless interacting with `commander`'s loose types.
43
+
44
+ ## JSON mode is required
45
+
46
+ Every user-facing command must support `--json`. The harness in `src/index.ts` auto-enables it when stdout isn't a TTY, and `lib/format.ts` gates pretty prints. Pattern:
47
+
48
+ ```ts
49
+ if (isJSONMode()) {
50
+ printJSON({ /* machine-readable */ });
51
+ return;
52
+ }
53
+ // human-readable output
54
+ table(...); info(...);
55
+ ```
56
+
57
+ For streaming commands (`up`, `logs`), JSON mode emits **one event per line**:
58
+ `{ event: "log", line }`, terminating with `{ event: "done" }` or `{ event: "error", message }`. See `commands/logs.ts` and `commands/up.ts` for shape.
59
+
60
+ ## Adding a command
61
+
62
+ 1. Create `src/commands/<name>.ts` exporting `register<Name>(program: Command)`.
63
+ 2. Import + call in `src/index.ts` (alphabetical, both the import and the `register…(program)` line).
64
+ 3. If it shouldn't require login, add its top-level name to the `noAuth` set in `src/index.ts`. The walk-up matches subcommands automatically.
65
+ 4. Update **`skill-data/core/SKILL.md`** so the agent skill teaches the new flag/command. The CLI serves it via `lizard skill get core`, so users see the matching version regardless of when the skill was installed.
66
+ 5. Add tests in `test/unit/<name>.test.ts` (or extend `test/cli.test.ts` for integration).
67
+ 6. `--help --json` is autogenerated by the dumper in `src/index.ts` — no extra wiring needed, but verify the new flags/args show up correctly.
68
+
69
+ ## Skill content (`skill-data/`)
70
+
71
+ This directory holds the canonical agent skill markdown. The public discovery stub lives in [`lizard-build/lizard-skills`](https://github.com/lizard-build/lizard-skills) and is intentionally thin — it just tells agents to run `lizard skill get <name>` to fetch the version-matched content from the installed CLI.
72
+
73
+ When you change platform behavior (build pipeline, env precedence, addon env vars, deploy flow), update `skill-data/core/SKILL.md`. Do **not** put that content in the lizard-skills stub — it would go stale.
74
+
75
+ Frontmatter format (YAML):
76
+ ```yaml
77
+ ---
78
+ name: <slug>
79
+ description: "<trigger-rich one-liner>"
80
+ argument-hint: "[optional natural-language request]"
81
+ allowed-tools: Bash(lizard:*)
82
+ ---
83
+ ```
84
+
85
+ ## Auth handling
86
+
87
+ `requireAuth()` runs automatically in `preAction` for any command not in the `noAuth` set. It auto-triggers the login flow when no token is on disk. Don't call `requireAuth()` manually inside command actions.
88
+
89
+ ## Exit codes
90
+
91
+ Defined in `src/index.ts`:
92
+ - `0` success
93
+ - `1` generic error
94
+ - `2` auth (401/403)
95
+ - `3` not found (404)
96
+ - `4` timeout (408/504)
97
+ - `5` cancelled by user
98
+
99
+ Throw `APIError` (from `lib/api.ts`) with a `status` so the root catch maps it correctly. Don't `process.exit()` from inside command actions — let errors propagate.
100
+
101
+ ## Releases / versioning
102
+
103
+ Version lives in `package.json` AND `src/lib/updater.ts` (`CURRENT_VERSION`). Bumping one without the other breaks self-update reporting. Commit messages for bumps: `chore: bump version to vX.Y.Z [skip ci]`.
104
+
105
+ ## Known landmine: `updater.ts`
106
+
107
+ `selfUpdate()` calls `renameSync(downloadedBinary, process.execPath)`. This works for the precompiled standalone install (`~/.lizard/bin/lizard`) where `process.execPath` IS the lizard binary. For npm installs, `process.execPath` is `node` and self-update will overwrite the user's node interpreter. Guard before adding new self-update code paths; for npm users, prefer instructing `npm i -g @lizard-build/cli@latest`.
108
+
109
+ ## Tests
110
+
111
+ Unit tests are vitest, fast, and can be run while iterating. The integration suite (`npm test`) spawns the built CLI from `dist/` against fixtures in `test/fixtures/`, so re-run `npm run build` first.
112
+
113
+ Two pre-existing failures in `test/unit/json.test.ts` reference a `lizard list` command that was dropped in commit `1c856f9` — not blocking; the fixture list there needs to be updated separately.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Lizard CLI
2
+
3
+ Deploy and manage apps on [Lizard](https://lizard.build).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ curl -fsSL https://lizard.build/install.sh | bash
9
+ ```
10
+
11
+ Or via npm:
12
+
13
+ ```bash
14
+ npm i -g @lizard-build/cli
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ lizard login
21
+ lizard init
22
+ lizard up
23
+ ```
24
+
25
+ ## Common commands
26
+
27
+ ```
28
+ lizard up # upload and deploy code
29
+ lizard logs # stream runtime logs
30
+ lizard ps # list services
31
+ lizard add # add a database or service
32
+ lizard secrets # manage secrets
33
+ lizard scale # scale replicas / CPU / memory
34
+ lizard domain # manage domains
35
+ ```
36
+
37
+ Run `lizard --help` for the full list.
38
+
39
+ ## License
40
+
41
+ MIT
@@ -1,24 +1,154 @@
1
1
  import chalk from "chalk";
2
2
  import * as p from "@clack/prompts";
3
- import { api } from "../lib/api.js";
4
- import { resolveProjectId } from "../lib/config.js";
3
+ import { api, withQuery, withScope } from "../lib/api.js";
4
+ import { getProjectLink, updateProjectLink, DEFAULT_REGION } from "../lib/config.js";
5
+ import { scopeForProject } from "../lib/resolve.js";
6
+ import { resolveWorkspace } from "../lib/picker.js";
5
7
  import { success, info, isJSONMode, printJSON, isTTY, table, } from "../lib/format.js";
8
+ import { validateName, addonRefName } from "../lib/name.js";
6
9
  const CATALOG = [
7
10
  { name: "postgres", label: "PostgreSQL", description: "Relational database" },
8
11
  { name: "redis", label: "Redis", description: "In-memory key-value store" },
9
- { name: "mysql", label: "MySQL", description: "Relational database" },
10
- { name: "mongodb", label: "MongoDB", description: "Document database" },
12
+ { name: "s3", label: "S3 Bucket", description: "S3-compatible object storage" },
11
13
  ];
14
+ async function detectPortFromDockerfile(repo) {
15
+ const repoPath = repo.startsWith("http") ? repo.replace(/^https?:\/\/github\.com\//, "") : repo;
16
+ for (const branch of ["main", "master"]) {
17
+ try {
18
+ const res = await fetch(`https://raw.githubusercontent.com/${repoPath}/${branch}/Dockerfile`, { signal: AbortSignal.timeout(5000) });
19
+ if (!res.ok)
20
+ continue;
21
+ const text = await res.text();
22
+ const match = text.match(/^EXPOSE\s+(\d+)/m);
23
+ if (match)
24
+ return parseInt(match[1], 10);
25
+ }
26
+ catch { }
27
+ }
28
+ return undefined;
29
+ }
30
+ /** Most-useful env var to surface as a reference example for each addon type.
31
+ * Mirrors urlKey in lizard-client/src/components/AddonPanel.tsx. */
32
+ function addonExampleVar(type) {
33
+ switch (type) {
34
+ case "postgres":
35
+ case "mysql":
36
+ return "DATABASE_URL";
37
+ case "mongo":
38
+ return "MONGODB_URL";
39
+ case "redis":
40
+ return "REDIS_URL";
41
+ case "s3":
42
+ return "S3_ENDPOINT";
43
+ default:
44
+ return "KEY";
45
+ }
46
+ }
47
+ function normalizeDbName(name) {
48
+ for (const c of CATALOG) {
49
+ if (c.name === name)
50
+ return c.name;
51
+ if (c.aliases?.includes(name))
52
+ return c.name;
53
+ }
54
+ return name;
55
+ }
56
+ /**
57
+ * Resolve a project by name/slug/id. Name-based lookup hits /api/projects and
58
+ * matches against the list. Falls back to the cwd-linked project when no
59
+ * -p/--project is supplied.
60
+ *
61
+ * When `workspaceFlag` is provided, the lookup is constrained to that
62
+ * workspace — useful for disambiguating identically-named projects.
63
+ */
64
+ async function resolveProject(flagValue, workspaceFlag) {
65
+ if (flagValue) {
66
+ let workspaceId;
67
+ if (workspaceFlag) {
68
+ workspaceId = (await resolveWorkspace(workspaceFlag)).id;
69
+ }
70
+ const projects = await api.get(withQuery("/api/projects", { workspaceId }));
71
+ const matches = projects.filter((pr) => pr.id === flagValue ||
72
+ pr.slug === flagValue ||
73
+ pr.name === flagValue);
74
+ if (matches.length === 0) {
75
+ throw new Error(`Project "${flagValue}" not found. Available: ${projects.map((pr) => pr.name).join(", ") || "(none)"}`);
76
+ }
77
+ if (matches.length > 1) {
78
+ const detail = matches
79
+ .map((m) => ` • ${m.name} in ${m.workspaceName ?? "(personal)"}`)
80
+ .join("\n");
81
+ throw new Error(`Multiple projects named "${flagValue}" found:\n${detail}\nPass --workspace to disambiguate.`);
82
+ }
83
+ return matches[0].id;
84
+ }
85
+ const link = getProjectLink();
86
+ if (link?.projectId)
87
+ return link.projectId;
88
+ throw new Error("No project linked to this directory. Pass -p <project-name> or run `lizard init`.");
89
+ }
90
+ function parseVariables(pairs) {
91
+ if (!pairs?.length)
92
+ return {};
93
+ const out = {};
94
+ for (const pair of pairs) {
95
+ const eq = pair.indexOf("=");
96
+ if (eq < 1)
97
+ throw new Error(`Invalid variable: "${pair}". Use KEY=value`);
98
+ out[pair.slice(0, eq)] = pair.slice(eq + 1);
99
+ }
100
+ return out;
101
+ }
12
102
  export function registerAdd(program) {
13
103
  program
14
104
  .command("add")
15
- .argument("[name]", "Service name from catalog (postgres, redis, mysql, mongodb)")
16
- .description("Add a service to the project")
17
- .option("--list", "Show available services")
18
- .option("--region <region>", "Region for the service")
19
- .action(async (name, opts) => {
20
- // Show catalog
21
- if (opts.list || (!name && !isTTY())) {
105
+ .argument("[types...]", "Addon type(s) to add (postgres / redis / s3). Multiple allowed: `add postgres redis s3`")
106
+ .description("Add a database, service, or repo to the project")
107
+ .option("-a, --addon <type...>", "Add one or more managed addons (multi-add: -a postgres -a redis -a s3)")
108
+ .option("-s, --service <name>", "Create an empty service with this name")
109
+ .option("-r, --repo <repo>", "Create a service from a GitHub repo (owner/repo)")
110
+ .option("-v, --variables <kv>", "KEY=value pair to seed the service. Repeat for multiple: -v K1=v1 -v K2=v2. Ignored for managed addons.", (val, prev) => [...prev, val], [])
111
+ .option("-n, --name <name>", "Name used in ${{<name>.KEY}} templates and shown in the dashboard. Renamable; refs stay stable.")
112
+ .option("--instance-name <name>", "(deprecated) alias for --name")
113
+ .option("-w, --workspace <ws>", "Disambiguate project lookup by workspace")
114
+ .option("--region <code>", "Region to provision the addon/service in")
115
+ .option("--no-deploy", "With -r: attach repo but skip the initial build. First deploy fires on next `service set` or `redeploy`.")
116
+ .option("--list", "Show available database types")
117
+ .action(async (types, opts, command) => {
118
+ const merged = command.optsWithGlobals();
119
+ await runAdd({
120
+ types,
121
+ addon: opts.addon,
122
+ service: opts.service,
123
+ repo: opts.repo,
124
+ variables: opts.variables,
125
+ name: opts.name,
126
+ instanceName: opts.instanceName,
127
+ workspace: opts.workspace,
128
+ region: opts.region,
129
+ noDeploy: opts.deploy === false,
130
+ list: opts.list,
131
+ projectFlag: merged.project,
132
+ });
133
+ });
134
+ }
135
+ async function runAdd(input) {
136
+ const types = input.types ?? [];
137
+ const opts = {
138
+ addon: input.addon,
139
+ service: input.service,
140
+ repo: input.repo,
141
+ variables: input.variables,
142
+ name: input.name,
143
+ instanceName: input.instanceName,
144
+ list: input.list,
145
+ };
146
+ const projectFlag = input.projectFlag;
147
+ const workspaceFlag = input.workspace;
148
+ const region = input.region ?? DEFAULT_REGION;
149
+ {
150
+ // ── --list: show DB catalog and exit ──────────────────────────────
151
+ if (opts.list || (!types.length && !opts.addon && !opts.service && !opts.repo && !isTTY())) {
22
152
  if (isJSONMode()) {
23
153
  printJSON(CATALOG);
24
154
  }
@@ -27,46 +157,189 @@ export function registerAdd(program) {
27
157
  }
28
158
  return;
29
159
  }
30
- // Interactive selection
31
- if (!name) {
32
- const selected = await p.select({
33
- message: "Select service to add",
34
- options: CATALOG.map((c) => ({
35
- value: c.name,
36
- label: c.label,
37
- hint: c.description,
38
- })),
39
- });
40
- if (p.isCancel(selected))
41
- process.exit(5);
42
- name = selected;
160
+ const variables = parseVariables(opts.variables);
161
+ if (opts.instanceName && !opts.name) {
162
+ info(chalk.yellow("Warning: --instance-name is deprecated, use --name instead."));
163
+ opts.name = opts.instanceName;
43
164
  }
44
- // Validate name is in catalog
45
- const catalogEntry = CATALOG.find((c) => c.name === name);
46
- if (!catalogEntry) {
47
- throw new Error(`Unknown service "${name}". Available: ${CATALOG.map((c) => c.name).join(", ")}`);
165
+ if (opts.name) {
166
+ const err = validateName(opts.name);
167
+ if (err)
168
+ throw new Error(`Invalid --name: ${err}`);
48
169
  }
49
- const projectId = resolveProjectId(program.opts().project);
50
- const region = opts.region || program.opts().region;
51
- info(`Adding ${chalk.cyan(catalogEntry.label)}...`);
52
- const addon = await api.post(`/api/projects/${projectId}/addons`, {
53
- addonType: name,
54
- region,
55
- });
56
- if (isJSONMode()) {
57
- printJSON(addon);
170
+ // Resolve project up front so we fail before any wizard prompts or
171
+ // API calls instead of after the user has filled out the wizard.
172
+ const projectId = await resolveProject(projectFlag, workspaceFlag);
173
+ const scope = await scopeForProject(projectId);
174
+ // ── positional <types...> and/or -a <type...> ────────────────────
175
+ const databases = [];
176
+ const candidates = [...(opts.addon ?? []), ...types];
177
+ for (const t of candidates) {
178
+ const norm = normalizeDbName(t);
179
+ if (!CATALOG.some((c) => c.name === norm)) {
180
+ throw new Error(`Unknown addon "${t}". Available: ${CATALOG.map((c) => c.name).join(", ")}`);
181
+ }
182
+ databases.push(norm);
183
+ }
184
+ // Nudge users off the verbose single-arg `-a` form toward `lizard add <type>`.
185
+ if (opts.addon?.length === 1 && !types.length && !isJSONMode()) {
186
+ info(chalk.dim(`Tip: shorter form — \`lizard add ${opts.addon[0]}\``));
187
+ }
188
+ if (databases.length > 0) {
189
+ if (opts.variables?.length) {
190
+ info(chalk.yellow("Warning: --variables is ignored for managed addons"));
191
+ }
192
+ const isSingle = databases.length === 1;
193
+ for (const db of databases) {
194
+ const cat = CATALOG.find((c) => c.name === db);
195
+ info(`Adding ${chalk.cyan(cat.label)}...`);
196
+ const addon = await api.post(withScope(`/api/projects/${projectId}/addons`, scope), {
197
+ type: db,
198
+ region,
199
+ ...(opts.name ? { name: opts.name } : {}),
200
+ });
201
+ if (isJSONMode())
202
+ printJSON(addon);
203
+ else {
204
+ success(`${cat.label} added`);
205
+ const ref = addonRefName({ name: addon.name, type: addon.type, addonType: addon.addonType });
206
+ const exampleVar = addonExampleVar(db);
207
+ if (ref)
208
+ info(` Name: ${chalk.bold(ref)}`);
209
+ if (ref) {
210
+ info("");
211
+ info(chalk.dim(` Reference the ${exampleVar} from other services:`));
212
+ info(` ${chalk.cyan(`\${{${ref}.${exampleVar}}}`)}`);
213
+ }
214
+ }
215
+ if (isSingle) {
216
+ try {
217
+ updateProjectLink({ serviceId: addon.id, serviceName: addon.name });
218
+ }
219
+ catch { }
220
+ }
221
+ }
58
222
  return;
59
223
  }
60
- success(`${catalogEntry.label} added`);
61
- if (addon.hostname) {
62
- info(` Host: ${chalk.cyan(addon.hostname)}`);
224
+ // ── -r <repo> ─────────────────────────────────────────────────────
225
+ if (opts.repo) {
226
+ const serviceName = opts.name || opts.service || opts.repo.split("/").pop() || "service";
227
+ info(`Creating service ${chalk.bold(serviceName)} from ${chalk.cyan(opts.repo)}...`);
228
+ const detectedPort = await detectPortFromDockerfile(opts.repo);
229
+ if (detectedPort)
230
+ info(`Detected port ${chalk.bold(detectedPort)} from Dockerfile`);
231
+ const app = await api.post(withScope(`/api/projects/${projectId}/apps`, scope), {
232
+ name: serviceName,
233
+ repoUrl: opts.repo.startsWith("http")
234
+ ? opts.repo
235
+ : `https://github.com/${opts.repo}`,
236
+ region,
237
+ variables,
238
+ ...(detectedPort ? { containerPort: detectedPort } : {}),
239
+ ...(input.noDeploy ? { skipInitialDeploy: true } : {}),
240
+ });
241
+ if (isJSONMode())
242
+ printJSON(app);
243
+ else {
244
+ success(`Service ${chalk.bold(app.name)} created${input.noDeploy ? " (initial deploy deferred)" : ""}`);
245
+ info("");
246
+ if (input.noDeploy) {
247
+ info(chalk.dim(` Configure and trigger the first deploy:`));
248
+ info(` ${chalk.cyan(`lizard service set ${app.name} --set buildCommand='...' --set startCommand='...'`)}`);
249
+ info("");
250
+ }
251
+ info(chalk.dim(` Reference this service's private URL from other services:`));
252
+ info(` ${chalk.cyan(`\${{${app.name}.LIZARD_PRIVATE_DOMAIN}}`)}`);
253
+ }
254
+ try {
255
+ updateProjectLink({ serviceId: app.id, serviceName: app.name });
256
+ }
257
+ catch { }
258
+ return;
259
+ }
260
+ // ── --service <name> (empty service) ──────────────────────────────
261
+ if (opts.service) {
262
+ info(`Creating empty service ${chalk.bold(opts.service)}...`);
263
+ const app = await api.post(withScope(`/api/projects/${projectId}/apps`, scope), {
264
+ name: opts.service,
265
+ region,
266
+ variables,
267
+ });
268
+ if (isJSONMode())
269
+ printJSON(app);
270
+ else {
271
+ success(`Service ${chalk.bold(app.name)} created`);
272
+ info("");
273
+ info(chalk.dim(` Reference this service's private URL from other services:`));
274
+ info(` ${chalk.cyan(`\${{${app.name}.LIZARD_PRIVATE_DOMAIN}}`)}`);
275
+ }
276
+ try {
277
+ updateProjectLink({ serviceId: app.id, serviceName: app.name });
278
+ }
279
+ catch { }
280
+ return;
63
281
  }
64
- if (addon.envVars) {
65
- info(chalk.dim("\n Environment variables added to project:"));
66
- for (const [key, val] of Object.entries(addon.envVars)) {
67
- info(` ${chalk.bold(key)}=${chalk.dim(val)}`);
282
+ // ── No flags + no positional → interactive wizard ────────────────
283
+ // After the wizard collects a concrete choice we re-enter runAdd
284
+ // directly (no program.parseAsync round-trip): runAdd routes to a
285
+ // concrete branch above based on the inputs we hand it.
286
+ if (!types.length && !input.skipWizard && isTTY()) {
287
+ const kind = await p.select({
288
+ message: "What do you need?",
289
+ options: [
290
+ { value: "database", label: "Database", hint: "postgres / redis" },
291
+ { value: "s3", label: "S3 Bucket", hint: "S3-compatible object storage" },
292
+ { value: "repo", label: "GitHub Repo", hint: "create a service from a repo" },
293
+ { value: "service", label: "Empty Service", hint: "create a service to upload code into" },
294
+ ],
295
+ });
296
+ if (p.isCancel(kind))
297
+ process.exit(5);
298
+ if (kind === "database") {
299
+ const sel = await p.select({
300
+ message: "Select database",
301
+ options: CATALOG.filter((c) => c.name !== "s3").map((c) => ({
302
+ value: c.name,
303
+ label: c.label,
304
+ hint: c.description,
305
+ })),
306
+ });
307
+ if (p.isCancel(sel))
308
+ process.exit(5);
309
+ await runAdd({ ...input, types: [sel], skipWizard: true });
310
+ return;
311
+ }
312
+ if (kind === "s3") {
313
+ await runAdd({ ...input, types: ["s3"], skipWizard: true });
314
+ return;
315
+ }
316
+ if (kind === "repo") {
317
+ const repo = await p.text({ message: "Repo (owner/name)" });
318
+ if (p.isCancel(repo))
319
+ process.exit(5);
320
+ const svc = await p.text({ message: "Service name", placeholder: String(repo).split("/").pop() });
321
+ if (p.isCancel(svc))
322
+ process.exit(5);
323
+ await runAdd({
324
+ ...input,
325
+ repo: String(repo),
326
+ service: String(svc) || undefined,
327
+ skipWizard: true,
328
+ });
329
+ return;
330
+ }
331
+ if (kind === "service") {
332
+ const svc = await p.text({ message: "Service name" });
333
+ if (p.isCancel(svc))
334
+ process.exit(5);
335
+ await runAdd({ ...input, service: String(svc), skipWizard: true });
336
+ return;
68
337
  }
69
338
  }
70
- });
339
+ throw new Error("No service type specified. Examples:\n" +
340
+ " lizard add postgres Add a managed database\n" +
341
+ " lizard add -r owner/repo Create a service from a GitHub repo\n" +
342
+ " lizard add -s my-service Empty service");
343
+ }
71
344
  }
72
345
  //# sourceMappingURL=add.js.map