feat(ce-polish-beta): human-in-the-loop polish phase between /ce:review and merge (#568)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
89
plugins/compound-engineering/skills/ce-polish-beta/SKILL.md
Normal file
89
plugins/compound-engineering/skills/ce-polish-beta/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: ce:polish-beta
|
||||
description: "[BETA] Start the dev server, open the feature in a browser, and iterate on improvements together."
|
||||
disable-model-invocation: true
|
||||
argument-hint: "[PR number, branch name, or blank for current branch]"
|
||||
---
|
||||
|
||||
# Polish
|
||||
|
||||
Start the dev server, open the feature in a browser, and iterate. You use the feature, say what feels off, and fixes happen.
|
||||
|
||||
## Phase 0: Get on the right branch
|
||||
|
||||
1. If a PR number or branch name was provided, check it out (probe for existing worktrees first).
|
||||
2. If blank, use the current branch.
|
||||
3. Verify the current branch is not main/master.
|
||||
|
||||
## Phase 1: Start the dev server
|
||||
|
||||
### 1.1 Check for `.claude/launch.json`
|
||||
|
||||
Run `bash scripts/read-launch-json.sh`. If it finds a configuration, use it — the user already told us how to start the project.
|
||||
|
||||
### 1.2 Auto-detect (when no launch.json)
|
||||
|
||||
Run `bash scripts/detect-project-type.sh` to identify the framework.
|
||||
|
||||
Route by type to the matching recipe reference for start command and port defaults:
|
||||
|
||||
| Type | Recipe |
|
||||
|------|--------|
|
||||
| `rails` | `references/dev-server-rails.md` |
|
||||
| `next` | `references/dev-server-next.md` |
|
||||
| `vite` | `references/dev-server-vite.md` |
|
||||
| `nuxt` | `references/dev-server-nuxt.md` |
|
||||
| `astro` | `references/dev-server-astro.md` |
|
||||
| `remix` | `references/dev-server-remix.md` |
|
||||
| `sveltekit` | `references/dev-server-sveltekit.md` |
|
||||
| `procfile` | `references/dev-server-procfile.md` |
|
||||
| `unknown` | Ask the user how to start the project |
|
||||
|
||||
For framework types that need a package manager, run `bash scripts/resolve-package-manager.sh` and substitute the result into the start command.
|
||||
|
||||
Resolve the port with `bash scripts/resolve-port.sh --type <type>`.
|
||||
|
||||
### 1.3 Start the server
|
||||
|
||||
Start the dev server in the background, log output to a temp file. Probe `http://localhost:<port>` for up to 30 seconds. If it doesn't come up, show the last 20 lines of the log and ask the user what to do.
|
||||
|
||||
### 1.4 Open in browser
|
||||
|
||||
Load `references/ide-detection.md` for the env-var probe table. Open the browser using the IDE's mechanism (Claude Code → `open`, Cursor → Cursor browser, VS Code → Simple Browser).
|
||||
|
||||
Tell the user:
|
||||
```
|
||||
Dev server running on http://localhost:<port>
|
||||
Browse the feature and tell me what could be better.
|
||||
```
|
||||
|
||||
## Phase 2: Iterate
|
||||
|
||||
This is the core loop. The user browses the feature and tells you what to improve. You fix it. Repeat until they're happy.
|
||||
|
||||
- When the user describes something to fix → make the change, the dev server hot-reloads
|
||||
- When the user asks to check something → use `agent-browser` to screenshot or inspect the page
|
||||
- When the user says they're done → commit the fixes and stop
|
||||
|
||||
No checklist. No envelope. Just conversation.
|
||||
|
||||
## References
|
||||
|
||||
Reference files (loaded on demand):
|
||||
- `references/launch-json-schema.md` — launch.json schema + per-framework stubs
|
||||
- `references/ide-detection.md` — host IDE detection and browser-handoff
|
||||
- `references/dev-server-detection.md` — port resolution documentation
|
||||
- `references/dev-server-rails.md` — Rails dev-server defaults
|
||||
- `references/dev-server-next.md` — Next.js dev-server defaults
|
||||
- `references/dev-server-vite.md` — Vite dev-server defaults
|
||||
- `references/dev-server-nuxt.md` — Nuxt dev-server defaults
|
||||
- `references/dev-server-astro.md` — Astro dev-server defaults
|
||||
- `references/dev-server-remix.md` — Remix dev-server defaults
|
||||
- `references/dev-server-sveltekit.md` — SvelteKit dev-server defaults
|
||||
- `references/dev-server-procfile.md` — Procfile-based dev-server defaults
|
||||
|
||||
Scripts (invoked via `bash scripts/<name>`):
|
||||
- `scripts/read-launch-json.sh` — launch.json reader
|
||||
- `scripts/detect-project-type.sh` — project-type classifier
|
||||
- `scripts/resolve-package-manager.sh` — lockfile-based package-manager resolver
|
||||
- `scripts/resolve-port.sh` — port resolution cascade
|
||||
@@ -0,0 +1,58 @@
|
||||
# Astro dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `astro` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `astro.config.js`, `astro.config.mjs`, or `astro.config.ts` exists
|
||||
- `package.json` contains an `astro` dependency
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The `dev` script in `package.json` typically wraps `astro dev`. Also valid (read `package.json` scripts to confirm which the project uses):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Prefer the package manager indicated by the lockfile:
|
||||
- `pnpm-lock.yaml` -> `pnpm dev`
|
||||
- `yarn.lock` -> `yarn dev`
|
||||
- `bun.lock` / `bun.lockb` -> `bun run dev`
|
||||
- `package-lock.json` or none -> `npm run dev`
|
||||
|
||||
## Port
|
||||
|
||||
Default: `4321`. Astro respects `--port <port>` and the `server.port` field in `astro.config.*`. Overrides follow the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Astro dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 4321
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute the resolved package manager (`npm` / `pnpm` / `yarn` / `bun`) and port.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **SSR vs SSG:** `astro dev` runs identically for both output modes; the difference only matters at build time. Polish does not need to distinguish between them.
|
||||
- **Astro config takes precedence over Vite config:** Astro uses Vite under the hood but ships its own config file. The `astro` type takes precedence over `vite` when both `astro.config.*` and `vite.config.*` exist. This is rare -- Astro projects do not usually have a separate Vite config file.
|
||||
- **Dev toolbar (Astro 4+):** Astro 4+ includes a dev toolbar that adds overlay UI in the browser. It does not affect port binding or URL routing -- polish can ignore it.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Dev-server port detection
|
||||
|
||||
Port resolution runs via `scripts/resolve-port.sh`. This document explains the probe order, framework defaults, and intentional divergences from the `test-browser` skill's inline cascade.
|
||||
|
||||
This cascade runs **only when** `.claude/launch.json` is absent or has no `port` field for the resolved configuration. When `launch.json` specifies a port, use it verbatim and skip this cascade entirely.
|
||||
|
||||
## Priority order
|
||||
|
||||
1. **Explicit `--port` flag** -- if the caller passed `--port <n>`, use it directly.
|
||||
2. **Framework config files** -- `next.config.*`, `vite.config.*`, `nuxt.config.*`, `astro.config.*` scanned with a conservative regex matching only numeric literal port values. Variable references (`process.env.PORT`, `getPort()`) are deliberately not matched.
|
||||
3. **Rails `config/puma.rb`** -- grep for `port <n>`.
|
||||
4. **`Procfile.dev`** -- web line scanned for `-p <n>` / `--port <n>` / `-p=<n>` / `--port=<n>`.
|
||||
5. **`docker-compose.yml`** -- line-anchored grep for `"<n>:<n>"` port mapping patterns. Not full YAML parsing.
|
||||
6. **`package.json`** -- `dev`/`start` scripts scanned for `--port <n>` / `-p <n>` / `--port=<n>` / `-p=<n>`.
|
||||
7. **`.env` files** -- checked in override order: `.env.local` -> `.env.development` -> `.env` (first hit wins). Parses `PORT=<n>` with quote stripping and comment truncation.
|
||||
8. **Framework default lookup table** -- see table below.
|
||||
|
||||
## Framework defaults
|
||||
|
||||
| Framework | Default port |
|
||||
|-----------|-------------|
|
||||
| Rails | 3000 |
|
||||
| Next.js | 3000 |
|
||||
| Nuxt | 3000 |
|
||||
| Remix (classic) | 3000 |
|
||||
| Vite | 5173 |
|
||||
| SvelteKit | 5173 |
|
||||
| Astro | 4321 |
|
||||
| Procfile | 3000 |
|
||||
| Unknown | 3000 |
|
||||
|
||||
## Sync-note block
|
||||
|
||||
`resolve-port.sh` and the `test-browser` skill's inline cascade overlap in purpose but diverge in three specific ways. These divergences are intentional -- do not "fix" one to match the other without understanding the rationale.
|
||||
|
||||
**(a) Quote stripping on `.env` values.** `resolve-port.sh` strips surrounding `"` and `'` from `PORT=` values (so `PORT="3001"` resolves to `3001`). The `test-browser` inline cascade does not strip quotes. The script version is more robust for real-world `.env` files where quoting is common.
|
||||
|
||||
**(b) Comment stripping on `.env` values.** `resolve-port.sh` truncates at `#` after trimming whitespace (so `PORT=3001 # dev only` resolves to `3001`). The `test-browser` inline cascade does not strip comments. Same rationale: real `.env` files frequently contain inline comments.
|
||||
|
||||
**(c) Removal of the `AGENTS.md`/`CLAUDE.md` grep.** `resolve-port.sh` does not scan instruction files for port references. The `test-browser` inline cascade does. Instruction files carry natural language that may mention ports in contexts unrelated to the dev server (documentation, examples, troubleshooting), producing false positives that are hard to debug. Framework config files and `.env` are more reliable sources of truth.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Next.js dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `next` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `next.config.js`, `next.config.mjs`, `next.config.ts`, or `next.config.cjs` exists
|
||||
- `package.json` contains a `next` dependency
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Also valid (read `package.json` scripts to confirm which the project uses):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Prefer the package manager indicated by the lockfile:
|
||||
- `pnpm-lock.yaml` -> `pnpm dev`
|
||||
- `yarn.lock` -> `yarn dev`
|
||||
- `bun.lock` / `bun.lockb` -> `bun run dev`
|
||||
- `package-lock.json` or none -> `npm run dev`
|
||||
|
||||
## Port
|
||||
|
||||
Default: `3000`. Next.js respects `-p <port>` / `--port <port>` and the `PORT` env var. Overrides follow the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Turbopack
|
||||
|
||||
Next.js 14+ supports `--turbo` (and 15+ makes it default). If the `dev` script in `package.json` includes `--turbo`, preserve it. Turbopack changes reload behavior but not port or URL conventions.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute the resolved package manager (`npm` / `pnpm` / `yarn` / `bun`) and port.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **App Router vs Pages Router:** dev-server behavior is the same; polish doesn't care. Checklist generation (Unit 5) does — pages in `app/` and `pages/` are different surfaces.
|
||||
- **Monorepo roots:** in a pnpm/Turborepo monorepo, `npm run dev` at the root typically fans out to multiple packages. Users should set `cwd` in `.claude/launch.json` to the specific Next app (`cwd: "apps/web"`).
|
||||
- **Env loading:** `.env.local` is loaded automatically by Next; polish does not need to export it.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Nuxt dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `nuxt` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `nuxt.config.js`, `nuxt.config.mjs`, or `nuxt.config.ts` exists
|
||||
- `package.json` contains a `nuxt` dependency
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Also valid (read `package.json` scripts to confirm which the project uses):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Prefer the package manager indicated by the lockfile:
|
||||
- `pnpm-lock.yaml` -> `pnpm dev`
|
||||
- `yarn.lock` -> `yarn dev`
|
||||
- `bun.lock` / `bun.lockb` -> `bun run dev`
|
||||
- `package-lock.json` or none -> `npm run dev`
|
||||
|
||||
## Port
|
||||
|
||||
Default: `3000`. Nuxt respects `--port <port>` and the `PORT` env var. Overrides follow the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Nuxt dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute the resolved package manager (`npm` / `pnpm` / `yarn` / `bun`) and port.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Nitro server engine:** Nitro (Nuxt's server engine) adds its own dev server behind Nuxt's; polish only cares about the Nuxt port. Do not probe the Nitro internal port separately.
|
||||
- **Port auto-increment:** Nuxt auto-increments the port if 3000 is already taken (unlike Next.js which errors). Polish's kill-by-port step handles this by reclaiming the port before starting, so the auto-increment behavior does not cause issues in practice.
|
||||
- **Nuxt 3 vs Nuxt 2:** Nuxt 3 uses `nuxt.config.ts`, Nuxt 2 uses `nuxt.config.js` -- both are detected by the signature check. The dev-server command and port defaults are the same across both versions.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Procfile / Overmind dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `procfile` and there is no `.claude/launch.json` to consult. Rails apps with `bin/dev` take precedence over the bare Procfile path (see `dev-server-rails.md`).
|
||||
|
||||
## Signature
|
||||
|
||||
- `Procfile` or `Procfile.dev` exists at the repo root
|
||||
- `bin/dev` is **not** present (if it is, use the Rails recipe)
|
||||
|
||||
## Start command
|
||||
|
||||
Prefer `overmind` when available — it handles socket files, supports hot-restart per process, and is the community default for multi-process dev:
|
||||
|
||||
```bash
|
||||
overmind start -f Procfile.dev
|
||||
```
|
||||
|
||||
Fallback to `foreman` when `overmind` is not installed:
|
||||
|
||||
```bash
|
||||
foreman start -f Procfile.dev
|
||||
```
|
||||
|
||||
If both are missing, prompt the user for the start command rather than guessing.
|
||||
|
||||
## Port
|
||||
|
||||
Default: `3000`. Procfile-based projects list their processes in `Procfile.dev`, so the authoritative port comes from the `web:` line:
|
||||
|
||||
```
|
||||
web: bundle exec puma -p 3000 -C config/puma.rb
|
||||
worker: bundle exec sidekiq
|
||||
```
|
||||
|
||||
Parse the `web:` line for `-p <n>` or `--port <n>`. If neither is present, fall through to the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Overmind dev",
|
||||
"runtimeExecutable": "overmind",
|
||||
"runtimeArgs": ["start", "-f", "Procfile.dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute `foreman` if `overmind` is unavailable on the user's machine — the stub represents what the user will run, not a canonical recipe.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Socket files:** `overmind` writes a socket to `.overmind.sock` by default. Polish's kill-by-port logic reclaims the port but does not clean up the socket. If overmind is already running and polish restarts it, the new process may fail with "connection refused" until the stale socket is removed. The `OVERMIND_SOCKET` env var can redirect the socket to a per-run path if needed.
|
||||
- **Procfile vs Procfile.dev:** production and development Procfiles often differ. Always prefer `Procfile.dev` for polish.
|
||||
- **Multiple web processes:** some Procfiles split web traffic across multiple processes (API + frontend). Polish can only open one URL — users with multi-web setups should author `.claude/launch.json` explicitly to select which process is "the dev server" for polish.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Rails dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `rails` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `bin/dev` exists and is executable
|
||||
- `Gemfile` exists
|
||||
|
||||
## Start command
|
||||
|
||||
```bash
|
||||
bin/dev
|
||||
```
|
||||
|
||||
`bin/dev` is the Rails 7+ convention for "start everything" (web + assets watcher + optional workers). It is a one-liner script that invokes `foreman start -f Procfile.dev` under the hood, so `Procfile.dev` is the canonical place to read the *actual* command if `bin/dev` is missing or non-executable.
|
||||
|
||||
## Port
|
||||
|
||||
Default: `3000`. Overrides follow the cascade in `references/dev-server-detection.md`:
|
||||
1. `Procfile.dev` `web:` line may contain `-p <n>`
|
||||
2. `config/puma.rb` may bind to a non-default port
|
||||
3. `.env` / `.env.development` `PORT=<n>`
|
||||
4. `AGENTS.md` / `CLAUDE.md` project instructions
|
||||
|
||||
## Stub generation for `.claude/launch.json`
|
||||
|
||||
When the user accepts "Save this as `.claude/launch.json`?", emit the Rails stub from `launch-json-schema.md`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Rails dev",
|
||||
"runtimeExecutable": "bin/dev",
|
||||
"runtimeArgs": [],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If the cascade resolved a non-3000 port, substitute it in the stub's `port` field before writing.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Bundler path:** some machines require `bundle exec bin/dev`. If `bin/dev` fails with a load-path error, fall back to `bundle exec bin/dev`.
|
||||
- **Foreman vs overmind:** `Procfile` vs `Procfile.dev` often both exist. Rails' `bin/dev` resolves to `Procfile.dev`; if the project uses `overmind` explicitly, prefer `overmind start -f Procfile.dev` (see `dev-server-procfile.md`).
|
||||
- **SSL dev server:** `rails s` with `--ssl` changes the URL scheme. Polish's reachability probe uses `http://`; users with SSL dev servers should set `port` explicitly in `.claude/launch.json` and note the scheme in the checklist.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Remix dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `remix` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `remix.config.js` or `remix.config.ts` exists (classic Remix)
|
||||
- Remix 2.x+ on Vite has no `remix.config.*` -- it uses `vite.config.ts` with the Remix plugin, so it resolves as `vite` type, not `remix`
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The `dev` script in `package.json` typically wraps `remix dev`. Also valid (read `package.json` scripts to confirm which the project uses):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Prefer the package manager indicated by the lockfile:
|
||||
- `pnpm-lock.yaml` -> `pnpm dev`
|
||||
- `yarn.lock` -> `yarn dev`
|
||||
- `bun.lock` / `bun.lockb` -> `bun run dev`
|
||||
- `package-lock.json` or none -> `npm run dev`
|
||||
|
||||
## Port
|
||||
|
||||
Default: `3000`. Remix respects `--port <port>` flag. Classic Remix dev server also reads the `PORT` env var. Overrides follow the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Remix dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute the resolved package manager (`npm` / `pnpm` / `yarn` / `bun`) and port.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Classic vs Vite:** Classic Remix uses `remix.config.js`; new Remix (v2+) uses Vite -- detected as `vite` type, not `remix`. The `remix` type is specifically for classic Remix projects that still have a `remix.config.*` file.
|
||||
- **Remix v1 vs v2 dev server:** `remix dev` in v2 starts an Express-based dev server that binds a port; `remix dev` in v1 was a watcher only (no server). Polish needs v2+ for the dev server to bind a port and respond to reachability probes.
|
||||
- **Remix on Vite inherits Vite's port:** When Remix runs on Vite (no `remix.config.*`), the default port is 5173 (Vite's default), not 3000. That case is handled by the `vite` recipe, not this one.
|
||||
@@ -0,0 +1,58 @@
|
||||
# SvelteKit dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `sveltekit` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `svelte.config.js`, `svelte.config.mjs`, or `svelte.config.ts` exists
|
||||
- `package.json` contains a `@sveltejs/kit` dependency
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The `dev` script in `package.json` typically wraps `vite dev` via SvelteKit. Also valid (read `package.json` scripts to confirm which the project uses):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Prefer the package manager indicated by the lockfile:
|
||||
- `pnpm-lock.yaml` -> `pnpm dev`
|
||||
- `yarn.lock` -> `yarn dev`
|
||||
- `bun.lock` / `bun.lockb` -> `bun run dev`
|
||||
- `package-lock.json` or none -> `npm run dev`
|
||||
|
||||
## Port
|
||||
|
||||
Default: `5173` (inherited from Vite). SvelteKit respects `--port <port>` flag and Vite's `server.port` config in `vite.config.ts`. Overrides follow the cascade in `references/dev-server-detection.md`.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "SvelteKit dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5173
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Substitute the resolved package manager (`npm` / `pnpm` / `yarn` / `bun`) and port.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Vite under the hood:** SvelteKit uses Vite internally -- same port default (5173), same HMR behavior. The `sveltekit` type exists because `svelte.config.js` is a more precise signal than a generic `vite.config.ts`, allowing polish to generate a SvelteKit-specific stub name and label.
|
||||
- **Adapter does not matter for dev:** `adapter-auto`, `adapter-node`, `adapter-static`, and other adapters all produce the same dev server. The adapter only affects the production build output.
|
||||
- **`svelte.config.js` is the primary signature:** `svelte.config.js` always exists in SvelteKit projects, even when `vite.config.ts` also exists. This is the file that distinguishes a SvelteKit project from a plain Vite project.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Vite dev-server recipe (auto-detect fallback)
|
||||
|
||||
Loaded when `detect-project-type.sh` returns `vite` and there is no `.claude/launch.json` to consult.
|
||||
|
||||
## Signature
|
||||
|
||||
- `vite.config.js`, `vite.config.ts`, `vite.config.mjs`, or `vite.config.cjs` exists
|
||||
|
||||
## Start command
|
||||
|
||||
Standard:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The `dev` script in `package.json` typically wraps `vite` directly. Prefer the package manager indicated by the lockfile (see the Next.js recipe for the lockfile → command mapping).
|
||||
|
||||
## Port
|
||||
|
||||
Default: `5173`. Vite respects `--port <n>` and the `VITE_PORT` env var. The cascade in `references/dev-server-detection.md` picks up `--port` from `package.json` scripts and `PORT` from `.env*`.
|
||||
|
||||
Vite's `--strictPort` flag causes the dev server to fail rather than increment to the next available port when the requested port is in use. Polish's kill-by-port step will reclaim the port before starting, so `strictPort` is not a problem in practice — but users who disable port reclamation and run multiple Vite instances will see the port auto-increment unless `strictPort: true` is set in `vite.config.ts`.
|
||||
|
||||
## Host binding
|
||||
|
||||
Vite binds to `127.0.0.1` by default. For polish running inside a devcontainer or WSL, users may need `--host 0.0.0.0` in `runtimeArgs`. The checklist can note this if relevant to the diff.
|
||||
|
||||
## Stub generation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Vite dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5173
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **HMR websocket port:** Vite's HMR uses a separate websocket that inherits the dev-server port by default. If the project pins `server.hmr.port` in `vite.config.ts`, the polish reachability probe against the dev-server port still works, but the embedded browser may need additional configuration to reach HMR.
|
||||
- **Framework on top of Vite:** SvelteKit, SolidStart, Qwik City, and Astro all use Vite but add their own dev scripts. The `vite` signature catches them, and `npm run dev` is the right command for all of them. Different default ports apply (SvelteKit: 5173, Astro: 4321, Qwik: 5173) — rely on the cascade to pick up the actual port from `package.json` or `.env`.
|
||||
@@ -0,0 +1,47 @@
|
||||
# IDE detection for browser handoff
|
||||
|
||||
Polish attempts to hand the running dev-server URL off to an IDE's embedded browser so the user can test without a context switch. Detection is best-effort — failure falls through to printing the URL in the interactive summary.
|
||||
|
||||
## Detection order
|
||||
|
||||
Probe environment variables in this order and stop at the first positive match. Earlier entries are more specific; later entries are general fallbacks.
|
||||
|
||||
| Order | Signal | IDE | Handoff method |
|
||||
|-------|--------|-----|----------------|
|
||||
| 1 | `CLAUDE_CODE` env var set (any value) | Claude Code desktop | Print `claude-code://browser?url=http://localhost:<port>` as a clickable hint; Claude Code's desktop app intercepts `claude-code://` URLs. |
|
||||
| 2 | `CURSOR_TRACE_ID` env var set | Cursor | Emit `cursor://anysphere.cursor-retrieval/open?url=...` if Cursor's URL scheme is stable in the user's version; otherwise print the URL with a note to open it in Cursor's simple-browser view. |
|
||||
| 3 | `TERM_PROGRAM=vscode` AND no Cursor/Claude Code signal | Plain VS Code | Print the URL with a hint: `Open in VS Code: Ctrl+Shift+P → "Simple Browser: Show" → paste URL`. |
|
||||
| 4 | None of the above | Terminal / unknown IDE | Print the URL. No handoff attempt. |
|
||||
|
||||
## Why env-var probe, not a fancier approach
|
||||
|
||||
- Env vars are cross-platform (macOS, Linux, Windows/WSL)
|
||||
- They fail open — if a probe returns nothing, polish still works
|
||||
- They don't require any IDE API or socket connection
|
||||
- They encode "is this shell running inside a known IDE" without guessing
|
||||
|
||||
## Codex and other platforms
|
||||
|
||||
Codex (Claude Agent SDK, Gemini CLI, etc.) do not yet expose an embedded-browser handoff. For these platforms, polish falls through to the terminal branch (print the URL). When a convention emerges, add a new row to the detection table above.
|
||||
|
||||
## Detection failure is never fatal
|
||||
|
||||
If environment probing fails or returns ambiguous results, polish prints the URL verbatim and continues. The dev server is already running by this point — the user can always copy-paste the URL into any browser. The IDE handoff is a convenience, not a gate.
|
||||
|
||||
## Probe pattern (reference)
|
||||
|
||||
The skill consumes these probes inline rather than via a shell script (no state, no parsing, one-shot reads). Typical usage:
|
||||
|
||||
```
|
||||
if [ -n "${CLAUDE_CODE:-}" ]; then
|
||||
IDE="claude-code"
|
||||
elif [ -n "${CURSOR_TRACE_ID:-}" ]; then
|
||||
IDE="cursor"
|
||||
elif [ "${TERM_PROGRAM:-}" = "vscode" ]; then
|
||||
IDE="vscode"
|
||||
else
|
||||
IDE="none"
|
||||
fi
|
||||
```
|
||||
|
||||
Never chain probes with `||` between different variables — a missing env var must resolve to "no signal", not "error". The `${VAR:-}` default-to-empty pattern is mandatory under `set -u`.
|
||||
@@ -0,0 +1,177 @@
|
||||
# `.claude/launch.json` schema
|
||||
|
||||
Polish reads `.claude/launch.json` at the repo root to resolve the dev-server start command. The schema is a subset of VS Code's `launch.json` format — chosen because Claude Code, Cursor, and VS Code all understand it and because users often already have one for editor integration.
|
||||
|
||||
## Top-level shape
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "<human label>",
|
||||
"runtimeExecutable": "<binary>",
|
||||
"runtimeArgs": ["<arg>", "<arg>"],
|
||||
"port": <number>,
|
||||
"cwd": "<optional, repo-relative>",
|
||||
"env": { "<key>": "<value>" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Fields polish consumes
|
||||
|
||||
| Field | Required | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `name` | yes (when multiple configurations) | Used to disambiguate when the array has more than one entry. Polish asks the user to pick by `name`. |
|
||||
| `runtimeExecutable` | yes | The binary polish spawns (e.g., `bin/dev`, `npm`, `overmind`, `bun`). |
|
||||
| `runtimeArgs` | no | Array of arguments passed to `runtimeExecutable`. Default: empty array. |
|
||||
| `port` | yes | The port the dev server will listen on. Polish probes `http://localhost:<port>` for reachability and uses it for the IDE browser handoff. |
|
||||
| `cwd` | no | Repo-relative working directory for the dev server. Default: repo root. Useful for monorepos (`apps/web`, `packages/frontend`). |
|
||||
| `env` | no | Additional environment variables for the dev-server process. Default: inherit polish's environment. |
|
||||
|
||||
## Stub template (written on first run when user accepts)
|
||||
|
||||
When polish auto-detects a project type and the user confirms "Save this as `.claude/launch.json`?", polish writes a minimal stub derived from the detected type. These templates intentionally hard-code common defaults — users can edit them later.
|
||||
|
||||
### Rails stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Rails dev",
|
||||
"runtimeExecutable": "bin/dev",
|
||||
"runtimeArgs": [],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Vite stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Vite dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5173
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Procfile / Overmind stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Overmind dev",
|
||||
"runtimeExecutable": "overmind",
|
||||
"runtimeArgs": ["start", "-f", "Procfile.dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Nuxt stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Nuxt dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Astro stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Astro dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 4321
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Remix stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Remix dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### SvelteKit stub
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "SvelteKit dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5173
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why a subset of VS Code's schema
|
||||
|
||||
Polish does not use `type`, `request`, `console`, `stopOnEntry`, or any of the other VS Code fields. Including them is harmless — polish ignores them — but the stub writer never adds them. The fields polish cares about are the ones that describe *how to start a long-running dev server on a known port*, which is a smaller surface than what VS Code uses for debug-stepping.
|
||||
|
||||
## Cross-IDE notes
|
||||
|
||||
`.claude/launch.json` is not yet a fully unified standard across Claude Code, Cursor, VS Code, and Codex. Polish leads with `.claude/launch.json` because:
|
||||
- Claude Code, Cursor, and VS Code can all read it as a launch config
|
||||
- It sits at a clean repo-root trust boundary (user-authored, not auto-detected)
|
||||
- Users who prefer `.vscode/launch.json` can symlink or mirror the two files manually
|
||||
|
||||
If a cross-IDE standard emerges (e.g., `.workspace/launch.json`), the stub writer and reader can swap paths without touching the rest of the skill.
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# detect-project-type.sh — inspect signature files at the repo root (and, if
|
||||
# no root match is found, probe shallow subdirectories) to emit a project-type
|
||||
# identifier on stdout.
|
||||
#
|
||||
# Usage:
|
||||
# detect-project-type.sh
|
||||
#
|
||||
# Output grammar (one line on stdout):
|
||||
#
|
||||
# <type> — single signature match at root
|
||||
# e.g. "next", "rails", "vite"
|
||||
#
|
||||
# <type>@<relative-dir> — single monorepo hit (no root match)
|
||||
# e.g. "next@apps/web"
|
||||
#
|
||||
# multiple — two or more disjoint root signatures
|
||||
# (caller must prompt for disambiguation)
|
||||
#
|
||||
# multiple:<type>@<dir>,<type>@<dir> — multiple monorepo hits (no root match)
|
||||
# e.g. "multiple:next@apps/web,rails@apps/api"
|
||||
#
|
||||
# unknown — no signatures found at root or in probe
|
||||
#
|
||||
# Supported root types: rails, next, vite, nuxt, astro, remix, sveltekit, procfile
|
||||
#
|
||||
# Monorepo probe:
|
||||
# Runs only when root detection finds ZERO matches. Searches subdirectories
|
||||
# up to depth 3 (e.g. services/api/server/vite.config.ts) for framework
|
||||
# signature files. Deeper nesting is ignored to avoid false positives.
|
||||
#
|
||||
# Excluded directories (not real project roots):
|
||||
# node_modules .git vendor dist build coverage .next .nuxt
|
||||
# .svelte-kit .turbo tmp fixtures
|
||||
#
|
||||
# `multiple` vs `rails`: Rails apps commonly ship a Procfile.dev alongside
|
||||
# bin/dev. To avoid treating every Rails app as a monorepo, the `rails`
|
||||
# signature takes precedence over a bare `procfile` match. `multiple` is
|
||||
# reserved for genuine disambiguation cases (e.g., Rails + Next, Next + Vite).
|
||||
|
||||
set -u
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "ERROR: not in a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT" || { echo "ERROR: cannot cd to repo root" >&2; exit 1; }
|
||||
|
||||
MATCHES=()
|
||||
|
||||
# Rails: bin/dev AND Gemfile together. A Gemfile alone (or bin/dev alone) is
|
||||
# insufficient -- plenty of gems have Gemfiles without bin/dev, and bin/dev
|
||||
# may exist in non-Rails projects.
|
||||
if [ -f "bin/dev" ] && [ -f "Gemfile" ]; then
|
||||
MATCHES+=("rails")
|
||||
fi
|
||||
|
||||
# Next.js
|
||||
if [ -f "next.config.js" ] || [ -f "next.config.mjs" ] || [ -f "next.config.ts" ] || [ -f "next.config.cjs" ]; then
|
||||
MATCHES+=("next")
|
||||
fi
|
||||
|
||||
# Vite
|
||||
if [ -f "vite.config.js" ] || [ -f "vite.config.ts" ] || [ -f "vite.config.mjs" ] || [ -f "vite.config.cjs" ]; then
|
||||
MATCHES+=("vite")
|
||||
fi
|
||||
|
||||
# Nuxt
|
||||
if [ -f "nuxt.config.js" ] || [ -f "nuxt.config.mjs" ] || [ -f "nuxt.config.ts" ]; then
|
||||
MATCHES+=("nuxt")
|
||||
fi
|
||||
|
||||
# Astro
|
||||
if [ -f "astro.config.js" ] || [ -f "astro.config.mjs" ] || [ -f "astro.config.ts" ]; then
|
||||
MATCHES+=("astro")
|
||||
fi
|
||||
|
||||
# Remix (classic — Remix on Vite uses vite.config.ts, detected as vite)
|
||||
if [ -f "remix.config.js" ] || [ -f "remix.config.ts" ]; then
|
||||
MATCHES+=("remix")
|
||||
fi
|
||||
|
||||
# SvelteKit
|
||||
if [ -f "svelte.config.js" ] || [ -f "svelte.config.mjs" ] || [ -f "svelte.config.ts" ]; then
|
||||
MATCHES+=("sveltekit")
|
||||
fi
|
||||
|
||||
# Procfile / Overmind / Foreman — only if we didn't already detect rails
|
||||
if [ ${#MATCHES[@]} -eq 0 ] || [ "${MATCHES[0]}" != "rails" ]; then
|
||||
if [ -f "Procfile" ] || [ -f "Procfile.dev" ]; then
|
||||
MATCHES+=("procfile")
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Root result ──────────────────────────────────────────────────────────────
|
||||
case ${#MATCHES[@]} in
|
||||
0)
|
||||
# No root match — run monorepo probe (shallow find, depth <= 3).
|
||||
;;
|
||||
1)
|
||||
echo "${MATCHES[0]}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "multiple"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Monorepo probe ─────────────────────────────────────────────────────────
|
||||
# When root detection returns zero matches, descend up to depth 3 looking for
|
||||
# framework signatures in workspace directories. Common layouts:
|
||||
# apps/web/next.config.js (depth 2)
|
||||
# packages/frontend/vite.config.ts (depth 2)
|
||||
# services/api/server/vite.config.ts (depth 3)
|
||||
#
|
||||
# Exclusion list: directories that ship framework configs as fixtures or build
|
||||
# output, not as real project roots.
|
||||
|
||||
EXCLUDE_DIRS="node_modules .git vendor dist build coverage .next .nuxt .svelte-kit .turbo tmp fixtures"
|
||||
EXCLUDE_ARGS=""
|
||||
for d in $EXCLUDE_DIRS; do
|
||||
EXCLUDE_ARGS="$EXCLUDE_ARGS -path './$d' -prune -o -path '*/$d' -prune -o"
|
||||
done
|
||||
|
||||
# Signature file patterns to look for
|
||||
SIGNATURE_PATTERNS=(
|
||||
"next.config.js" "next.config.mjs" "next.config.ts" "next.config.cjs"
|
||||
"vite.config.js" "vite.config.ts" "vite.config.mjs" "vite.config.cjs"
|
||||
"nuxt.config.js" "nuxt.config.mjs" "nuxt.config.ts"
|
||||
"astro.config.js" "astro.config.mjs" "astro.config.ts"
|
||||
"remix.config.js" "remix.config.ts"
|
||||
"svelte.config.js" "svelte.config.mjs" "svelte.config.ts"
|
||||
)
|
||||
|
||||
# Build the find -name arguments
|
||||
NAME_ARGS=""
|
||||
for i in "${!SIGNATURE_PATTERNS[@]}"; do
|
||||
if [ "$i" -gt 0 ]; then
|
||||
NAME_ARGS="$NAME_ARGS -o"
|
||||
fi
|
||||
NAME_ARGS="$NAME_ARGS -name '${SIGNATURE_PATTERNS[$i]}'"
|
||||
done
|
||||
|
||||
# Run find. Use eval because the dynamically built arguments contain quoted
|
||||
# strings that must be expanded by the shell.
|
||||
FOUND_FILES=$(eval "find . -maxdepth 4 $EXCLUDE_ARGS \\( $NAME_ARGS \\) -print" 2>/dev/null | sort)
|
||||
|
||||
# Also check for Rails signature (bin/dev + Gemfile in the same subdir)
|
||||
RAILS_HITS=""
|
||||
# Find all Gemfiles at depth <= 3, check each dir for bin/dev
|
||||
while IFS= read -r gemfile; do
|
||||
[ -z "$gemfile" ] && continue
|
||||
gdir=$(dirname "$gemfile")
|
||||
if [ -f "$gdir/bin/dev" ]; then
|
||||
RAILS_HITS="$RAILS_HITS
|
||||
$gdir"
|
||||
fi
|
||||
done < <(eval "find . -maxdepth 4 $EXCLUDE_ARGS -name 'Gemfile' -print" 2>/dev/null)
|
||||
|
||||
# Parse found files into (type, relative-dir) pairs
|
||||
declare -A MONO_HITS=() # key = "type@dir", value = 1 (dedup)
|
||||
|
||||
if [ -n "$FOUND_FILES" ]; then
|
||||
for f in $FOUND_FILES; do
|
||||
[ -z "$f" ] && continue
|
||||
fname=$(basename "$f")
|
||||
fdir=$(dirname "$f")
|
||||
# Normalize dir: strip leading ./
|
||||
fdir="${fdir#./}"
|
||||
|
||||
# Enforce depth cap of 3: count slashes in the relative path of the file.
|
||||
# A file at apps/web/next.config.js has dir apps/web (1 slash = depth 2).
|
||||
# A file at a/b/c/d/next.config.js has dir a/b/c/d (3 slashes = depth 4 = too deep).
|
||||
# We want maxdepth 3 for the directory, meaning at most 2 slashes in fdir.
|
||||
slash_count=$(echo "$fdir" | tr -cd '/' | wc -c | tr -d ' ')
|
||||
if [ "$slash_count" -gt 2 ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$fname" in
|
||||
next.config.*) ftype="next" ;;
|
||||
vite.config.*) ftype="vite" ;;
|
||||
nuxt.config.*) ftype="nuxt" ;;
|
||||
astro.config.*) ftype="astro" ;;
|
||||
remix.config.*) ftype="remix" ;;
|
||||
svelte.config.*) ftype="sveltekit" ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
# Skip root hits (those would have been caught by root detection)
|
||||
if [ "$fdir" = "." ]; then continue; fi
|
||||
|
||||
MONO_HITS["${ftype}@${fdir}"]=1
|
||||
done
|
||||
fi
|
||||
|
||||
# Add Rails monorepo hits
|
||||
if [ -n "$RAILS_HITS" ]; then
|
||||
for rdir in $RAILS_HITS; do
|
||||
[ -z "$rdir" ] && continue
|
||||
rdir="${rdir#./}"
|
||||
if [ "$rdir" != "." ] && [ -n "$rdir" ]; then
|
||||
# Enforce depth cap for Rails hits too
|
||||
slash_count=$(echo "$rdir" | tr -cd '/' | wc -c | tr -d ' ')
|
||||
if [ "$slash_count" -le 2 ]; then
|
||||
MONO_HITS["rails@${rdir}"]=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ${#MONO_HITS[@]} triggers "unbound variable" under set -u on macOS bash 3.2
|
||||
# when the array is empty. Use the ${var+expr} expansion to guard it.
|
||||
MONO_COUNT=${MONO_HITS[@]+${#MONO_HITS[@]}}
|
||||
MONO_COUNT=${MONO_COUNT:-0}
|
||||
|
||||
case $MONO_COUNT in
|
||||
0)
|
||||
echo "unknown"
|
||||
;;
|
||||
1)
|
||||
# Single monorepo hit: emit type@cwd
|
||||
for key in "${!MONO_HITS[@]}"; do
|
||||
echo "$key"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
# Multiple hits: emit multiple:type1@cwd1,type2@cwd2,...
|
||||
result=""
|
||||
for key in "${!MONO_HITS[@]}"; do
|
||||
if [ -n "$result" ]; then
|
||||
result="${result},${key}"
|
||||
else
|
||||
result="$key"
|
||||
fi
|
||||
done
|
||||
echo "multiple:$result"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# read-launch-json.sh — read .claude/launch.json from the repo root and emit
|
||||
# the selected configuration as JSON on stdout, or a sentinel on failure.
|
||||
#
|
||||
# Usage:
|
||||
# read-launch-json.sh [config-name]
|
||||
#
|
||||
# Arguments:
|
||||
# config-name (optional) — if multiple configurations exist and this arg
|
||||
# matches a configuration's `name`, emit that one.
|
||||
# If omitted and there are multiple configurations,
|
||||
# emit a __MULTIPLE_CONFIGS__ sentinel followed by a
|
||||
# JSON array of configuration names on the next line.
|
||||
#
|
||||
# Output contract:
|
||||
# Success: single-line JSON object on stdout representing the chosen
|
||||
# configuration. Shape mirrors VS Code's launch.json entry:
|
||||
# {name, runtimeExecutable, runtimeArgs, port, cwd, env}.
|
||||
# Sentinels (printed to stdout, one per line):
|
||||
# __NO_LAUNCH_JSON__ - file not found
|
||||
# __INVALID_LAUNCH_JSON__ - file exists but fails JSON parsing
|
||||
# __MISSING_CONFIGURATIONS__ - valid JSON but no `configurations` array
|
||||
# __MULTIPLE_CONFIGS__ - ambiguity, needs caller disambiguation.
|
||||
# Followed by a JSON array of names on line 2.
|
||||
# __CONFIG_NOT_FOUND__ - caller-provided name doesn't match any entry
|
||||
#
|
||||
# The script never exits non-zero for a missing or malformed file -- callers
|
||||
# parse the sentinel and decide how to proceed. Exit code 1 is reserved for
|
||||
# genuine operational failures (missing `jq`, git root not found).
|
||||
|
||||
set -u
|
||||
|
||||
REQUESTED_NAME="${1:-}"
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "ERROR: not in a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "ERROR: jq is required but not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAUNCH_PATH="$REPO_ROOT/.claude/launch.json"
|
||||
|
||||
if [ ! -f "$LAUNCH_PATH" ]; then
|
||||
echo "__NO_LAUNCH_JSON__"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate JSON. We parse with `jq empty` so malformed JSON is caught
|
||||
# before any downstream query runs.
|
||||
if ! jq empty "$LAUNCH_PATH" >/dev/null 2>&1; then
|
||||
echo "__INVALID_LAUNCH_JSON__"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CONFIG_COUNT=$(jq '(.configurations // []) | length' "$LAUNCH_PATH")
|
||||
|
||||
if [ "$CONFIG_COUNT" = "0" ]; then
|
||||
echo "__MISSING_CONFIGURATIONS__"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$CONFIG_COUNT" = "1" ]; then
|
||||
jq -c '.configurations[0]' "$LAUNCH_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Multiple configurations. If the caller named one, emit it. Otherwise, emit
|
||||
# the sentinel + name list so the caller can prompt the user.
|
||||
if [ -n "$REQUESTED_NAME" ]; then
|
||||
MATCH=$(jq -c --arg name "$REQUESTED_NAME" '.configurations[] | select(.name == $name)' "$LAUNCH_PATH")
|
||||
if [ -z "$MATCH" ]; then
|
||||
echo "__CONFIG_NOT_FOUND__"
|
||||
exit 0
|
||||
fi
|
||||
echo "$MATCH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "__MULTIPLE_CONFIGS__"
|
||||
jq -c '[.configurations[].name]' "$LAUNCH_PATH"
|
||||
exit 0
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# resolve-package-manager.sh — detect which JS package manager a project uses
|
||||
# by inspecting lockfiles, and emit the binary name plus canonical command tail.
|
||||
#
|
||||
# Usage:
|
||||
# resolve-package-manager.sh [path]
|
||||
#
|
||||
# Arguments:
|
||||
# path (optional) — directory to inspect. When omitted, defaults to the
|
||||
# repo root via `git rev-parse --show-toplevel`.
|
||||
#
|
||||
# Output contract (two lines on stdout):
|
||||
# Line 1: package-manager binary token (`npm` | `pnpm` | `yarn` | `bun`)
|
||||
# Line 2: canonical argv tail for running a dev script
|
||||
# - npm: "run dev" (npm requires the `run` verb)
|
||||
# - pnpm: "dev" (pnpm allows bare script names)
|
||||
# - yarn: "dev" (yarn allows bare script names)
|
||||
# - bun: "run dev" (bun requires the `run` verb)
|
||||
#
|
||||
# Lockfile priority order (first match wins):
|
||||
# 1. pnpm-lock.yaml -> pnpm
|
||||
# 2. yarn.lock -> yarn
|
||||
# 3. bun.lock -> bun (text format, preferred — newer canonical)
|
||||
# 4. bun.lockb -> bun (binary format, legacy)
|
||||
# 5. package-lock.json -> npm
|
||||
# When both bun.lock and bun.lockb are present, bun.lock (text) is checked
|
||||
# first and wins because it is the newer canonical format.
|
||||
#
|
||||
# Sentinel (stdout, exit 0):
|
||||
# __NO_PACKAGE_JSON__ — the target directory has no package.json
|
||||
#
|
||||
# Errors (stderr, exit 1):
|
||||
# ERROR: <message> — path does not exist, is not a directory, or
|
||||
# no positional arg and not inside a git repo
|
||||
|
||||
set -u
|
||||
|
||||
TARGET_PATH="${1:-}"
|
||||
|
||||
# Resolve target directory: positional arg or git repo root.
|
||||
if [ -n "$TARGET_PATH" ]; then
|
||||
if [ ! -d "$TARGET_PATH" ]; then
|
||||
echo "ERROR: path does not exist or is not a directory: $TARGET_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
TARGET_PATH=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$TARGET_PATH" ]; then
|
||||
echo "ERROR: not in a git repository and no path argument provided" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sentinel: no package.json means this is not a JS/TS project.
|
||||
if [ ! -f "$TARGET_PATH/package.json" ]; then
|
||||
echo "__NO_PACKAGE_JSON__"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check lockfiles in priority order.
|
||||
if [ -f "$TARGET_PATH/pnpm-lock.yaml" ]; then
|
||||
echo "pnpm"
|
||||
echo "dev"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$TARGET_PATH/yarn.lock" ]; then
|
||||
echo "yarn"
|
||||
echo "dev"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$TARGET_PATH/bun.lock" ]; then
|
||||
echo "bun"
|
||||
echo "run dev"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$TARGET_PATH/bun.lockb" ]; then
|
||||
echo "bun"
|
||||
echo "run dev"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$TARGET_PATH/package-lock.json" ]; then
|
||||
echo "npm"
|
||||
echo "run dev"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback: package.json present but no recognized lockfile.
|
||||
echo "npm"
|
||||
echo "run dev"
|
||||
exit 0
|
||||
308
plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-port.sh
Executable file
308
plugins/compound-engineering/skills/ce-polish-beta/scripts/resolve-port.sh
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# resolve-port.sh -- resolve the dev-server port for a project.
|
||||
#
|
||||
# Usage:
|
||||
# resolve-port.sh [path] [--type <type>] [--port <n>]
|
||||
#
|
||||
# Arguments:
|
||||
# path (optional) -- project root directory. Defaults to the git repo root.
|
||||
# --type (optional) -- framework type to scope probes (rails|next|vite|nuxt|
|
||||
# astro|remix|sveltekit|procfile). Unset runs all probes.
|
||||
# --port (optional) -- explicit port override. Emitted immediately when present.
|
||||
#
|
||||
# Output:
|
||||
# Single line on stdout: the resolved port number.
|
||||
# stderr is reserved for ERROR: messages only.
|
||||
#
|
||||
# Probe order (FIRST HIT WINS):
|
||||
#
|
||||
# 1. Explicit --port flag
|
||||
# 2. Framework config files (next.config.*, vite.config.*, nuxt.config.*,
|
||||
# astro.config.*) -- conservative regex matching only numeric literal
|
||||
# port values. Variable references like process.env.PORT or getPort()
|
||||
# are deliberately not matched; the probe falls through.
|
||||
# 3. Rails: config/puma.rb for `port <n>`
|
||||
# 4. Procfile.dev: web line scanned for -p/-p=<n>/--port/--port=<n>
|
||||
# 5. docker-compose.yml: line-anchored grep for "- "<n>:<n>"" port mapping
|
||||
# 6. package.json: dev/start script for --port/-p flags
|
||||
# 7. .env files in override order: .env.local -> .env.development -> .env
|
||||
# (first hit wins). Values are parsed with quote stripping (" and ')
|
||||
# and comment truncation (at #, after trimming whitespace).
|
||||
# 8. Framework default lookup table
|
||||
#
|
||||
# Why config-before-prose: framework config files are the most reliable source
|
||||
# of truth for the intended port; instruction files and env files are often
|
||||
# stale or overridden. Prose files (AGENTS.md, CLAUDE.md) are deliberately NOT
|
||||
# scanned -- they carry natural language that may mention ports in contexts
|
||||
# unrelated to the dev server (documentation, examples, troubleshooting).
|
||||
# Scanning them produces false positives that are hard to debug.
|
||||
#
|
||||
# .env parsing contract: surrounding double or single quotes are stripped.
|
||||
# Inline comments (# ...) are truncated after trimming whitespace. This is
|
||||
# intentionally more aggressive than the test-browser skill's inline cascade,
|
||||
# which does neither. See dev-server-detection.md for the divergence notes.
|
||||
|
||||
set -u
|
||||
|
||||
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT_ROOT=""
|
||||
PROJ_TYPE=""
|
||||
EXPLICIT_PORT=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--type)
|
||||
PROJ_TYPE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
EXPLICIT_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
PROJECT_ROOT="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Default to git repo root when no positional path is given.
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -z "$PROJECT_ROOT" ]; then
|
||||
echo "ERROR: not in a git repository and no path provided" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROJECT_ROOT" ]; then
|
||||
echo "ERROR: path does not exist: $PROJECT_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# should_probe TYPE PROBE_NAME
|
||||
# Returns 0 (true) if the probe should run for the given --type.
|
||||
should_probe() {
|
||||
local ptype="$1"
|
||||
local probe="$2"
|
||||
|
||||
if [ -z "$ptype" ]; then
|
||||
return 0 # no type filter -- run all probes
|
||||
fi
|
||||
|
||||
case "$ptype" in
|
||||
rails)
|
||||
case "$probe" in
|
||||
puma|procfile|docker-compose|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
next|nuxt|astro|remix|vite|sveltekit)
|
||||
case "$probe" in
|
||||
framework-config|package-json|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
procfile)
|
||||
case "$probe" in
|
||||
procfile|docker-compose|env|default) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
return 0 # unknown type -- run all probes
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# parse_env_port FILE
|
||||
# Parses PORT=<n> from the given file. Strips surrounding quotes and inline
|
||||
# comments. Prints the port on stdout or nothing.
|
||||
parse_env_port() {
|
||||
local envfile="$1"
|
||||
if [ ! -f "$envfile" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local line
|
||||
line=$(grep -E '^PORT=' "$envfile" 2>/dev/null | tail -1)
|
||||
if [ -z "$line" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract value after PORT=
|
||||
local value
|
||||
value="${line#PORT=}"
|
||||
|
||||
# Trim whitespace, then truncate at # (inline comment) -- comment stripping
|
||||
# must happen BEFORE quote stripping so PORT="3001" # comment -> "3001" -> 3001
|
||||
value=$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*#.*$//;s/[[:space:]]*$//')
|
||||
|
||||
# Strip surrounding double quotes
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
|
||||
# Strip surrounding single quotes
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
|
||||
# Trim any remaining whitespace
|
||||
value=$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Probe 1: Explicit --port flag ────────────────────────────────────────────
|
||||
|
||||
if [ -n "$EXPLICIT_PORT" ]; then
|
||||
echo "$EXPLICIT_PORT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Probe 2: Framework config files ─────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "framework-config"; then
|
||||
for cfg in \
|
||||
"$PROJECT_ROOT"/next.config.js \
|
||||
"$PROJECT_ROOT"/next.config.ts \
|
||||
"$PROJECT_ROOT"/next.config.mjs \
|
||||
"$PROJECT_ROOT"/next.config.cjs \
|
||||
"$PROJECT_ROOT"/vite.config.js \
|
||||
"$PROJECT_ROOT"/vite.config.ts \
|
||||
"$PROJECT_ROOT"/vite.config.mjs \
|
||||
"$PROJECT_ROOT"/vite.config.cjs \
|
||||
"$PROJECT_ROOT"/nuxt.config.js \
|
||||
"$PROJECT_ROOT"/nuxt.config.ts \
|
||||
"$PROJECT_ROOT"/nuxt.config.mjs \
|
||||
"$PROJECT_ROOT"/nuxt.config.cjs \
|
||||
"$PROJECT_ROOT"/astro.config.js \
|
||||
"$PROJECT_ROOT"/astro.config.ts \
|
||||
"$PROJECT_ROOT"/astro.config.mjs \
|
||||
"$PROJECT_ROOT"/astro.config.cjs \
|
||||
; do
|
||||
if [ ! -f "$cfg" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Conservative regex: match "port:" + digits, then verify nothing non-numeric
|
||||
# follows (rejects variable references like "port: process.env.PORT || 3000").
|
||||
local_line=$(grep -E 'port:[[:space:]]*["'"'"']?[0-9]+' "$cfg" 2>/dev/null | head -1)
|
||||
if [ -z "$local_line" ]; then continue; fi
|
||||
|
||||
local_port=$(printf '%s' "$local_line" | grep -Eo 'port:[[:space:]]*["'"'"']?[0-9]+["'"'"']?' | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$local_port" ]; then
|
||||
local_after=$(printf '%s' "$local_line" | sed "s/.*port:[[:space:]]*[\"']*${local_port}[\"']*//" )
|
||||
if [ -z "$local_after" ] || printf '%s' "$local_after" | grep -qE '^[[:space:],})]*$'; then
|
||||
echo "$local_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Probe 3: Rails config/puma.rb ───────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "puma"; then
|
||||
puma_file="$PROJECT_ROOT/config/puma.rb"
|
||||
if [ -f "$puma_file" ]; then
|
||||
puma_port=$(grep -Eo 'port[[:space:]]+[0-9]+' "$puma_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$puma_port" ]; then
|
||||
echo "$puma_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 4: Procfile.dev ───────────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "procfile"; then
|
||||
procfile="$PROJECT_ROOT/Procfile.dev"
|
||||
if [ -f "$procfile" ]; then
|
||||
# Extract the web line
|
||||
web_line=$(grep -E '^web:' "$procfile" 2>/dev/null | head -1)
|
||||
if [ -n "$web_line" ]; then
|
||||
# Match -p <n>, -p<n>, --port <n>, -p=<n>, --port=<n>
|
||||
proc_port=$(printf '%s' "$web_line" | grep -Eo '(-p[= ]*|--port[= ]+)[0-9]+' | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$proc_port" ]; then
|
||||
echo "$proc_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 5: docker-compose.yml ─────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "docker-compose"; then
|
||||
compose_file="$PROJECT_ROOT/docker-compose.yml"
|
||||
if [ -f "$compose_file" ]; then
|
||||
# Simple line-anchored grep for port mappings: - "NNNN:NNNN" or - NNNN:NNNN
|
||||
compose_port=$(grep -Eo '"[0-9]+:[0-9]+"' "$compose_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+' | head -1)
|
||||
if [ -n "$compose_port" ]; then
|
||||
echo "$compose_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 6: package.json scripts ───────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "package-json"; then
|
||||
pkg_file="$PROJECT_ROOT/package.json"
|
||||
if [ -f "$pkg_file" ]; then
|
||||
# Look for --port or -p in dev/start scripts
|
||||
pkg_port=$(grep -Eo '(-p[= ]+|--port[= ]+)[0-9]+' "$pkg_file" 2>/dev/null | head -1 | grep -Eo '[0-9]+')
|
||||
if [ -n "$pkg_port" ]; then
|
||||
echo "$pkg_port"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Probe 7: .env files ─────────────────────────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "env"; then
|
||||
for envfile in \
|
||||
"$PROJECT_ROOT/.env.local" \
|
||||
"$PROJECT_ROOT/.env.development" \
|
||||
"$PROJECT_ROOT/.env" \
|
||||
; do
|
||||
env_port=$(parse_env_port "$envfile")
|
||||
if [ -n "$env_port" ]; then
|
||||
echo "$env_port"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Probe 8: Framework default lookup table ──────────────────────────────────
|
||||
|
||||
if should_probe "$PROJ_TYPE" "default"; then
|
||||
case "$PROJ_TYPE" in
|
||||
rails|next|nuxt|remix|procfile|"")
|
||||
echo "3000"
|
||||
;;
|
||||
vite|sveltekit)
|
||||
echo "5173"
|
||||
;;
|
||||
astro)
|
||||
echo "4321"
|
||||
;;
|
||||
*)
|
||||
echo "3000"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Final fallback (should not normally be reached)
|
||||
echo "3000"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user