@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,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
|
package/dist/commands/add.js
CHANGED
|
@@ -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 {
|
|
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: "
|
|
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("[
|
|
16
|
-
.description("Add a service to the project")
|
|
17
|
-
.option("--
|
|
18
|
-
.option("--
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
31
|
-
if (!name) {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
165
|
+
if (opts.name) {
|
|
166
|
+
const err = validateName(opts.name);
|
|
167
|
+
if (err)
|
|
168
|
+
throw new Error(`Invalid --name: ${err}`);
|
|
48
169
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|