Skip to content

Commit 993bbb4

Browse files
authored
Merge pull request #5 from InsForge/devel
Devel
2 parents dea0ff4 + 75a028b commit 993bbb4

10 files changed

Lines changed: 489 additions & 18 deletions

File tree

.github/workflows/release.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: release
2+
3+
# Build native `insta` binaries with Bun and publish them to a GitHub Release.
4+
# Trigger by pushing a tag (v*), or manually via workflow_dispatch with a version.
5+
on:
6+
push:
7+
tags: ['v*']
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: 'Version to release, without the leading v (e.g. 0.1.0)'
12+
required: true
13+
14+
permissions:
15+
contents: write # create releases + upload assets
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest # Bun cross-compiles every target from one runner
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
include:
24+
- { target: bun-darwin-arm64, name: insta-darwin-arm64 }
25+
- { target: bun-darwin-x64, name: insta-darwin-x64 }
26+
- { target: bun-linux-arm64, name: insta-linux-arm64 }
27+
- { target: bun-linux-x64, name: insta-linux-x64 }
28+
- { target: bun-windows-x64, name: insta-windows-x64.exe }
29+
steps:
30+
- uses: actions/checkout@v4
31+
- uses: oven-sh/setup-bun@v2
32+
with:
33+
bun-version: latest
34+
- name: Resolve version
35+
id: v
36+
run: |
37+
if [ -n "${{ github.event.inputs.version }}" ]; then
38+
echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
39+
else
40+
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
41+
fi
42+
- name: Build ${{ matrix.name }}
43+
run: |
44+
bun install --frozen-lockfile
45+
bun build ./src/index.ts --compile --minify \
46+
--target=${{ matrix.target }} \
47+
--define "process.env.INSTA_CLI_VERSION=\"${{ steps.v.outputs.version }}\"" \
48+
--outfile "${{ matrix.name }}"
49+
- uses: actions/upload-artifact@v4
50+
with:
51+
name: ${{ matrix.name }}
52+
path: ${{ matrix.name }}
53+
if-no-files-found: error
54+
55+
release:
56+
needs: build
57+
runs-on: ubuntu-latest
58+
steps:
59+
- uses: actions/checkout@v4
60+
- uses: actions/download-artifact@v4
61+
with:
62+
path: dist-bin
63+
merge-multiple: true # flatten all per-target artifacts into one dir
64+
- name: Checksums
65+
run: |
66+
cd dist-bin
67+
sha256sum insta-* > SHA256SUMS
68+
ls -lh
69+
cat SHA256SUMS
70+
- name: Resolve tag
71+
id: t
72+
run: |
73+
if [ -n "${{ github.event.inputs.version }}" ]; then
74+
echo "tag=v${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
75+
else
76+
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
77+
fi
78+
- name: Publish release
79+
env:
80+
GH_TOKEN: ${{ github.token }}
81+
run: |
82+
TAG="${{ steps.t.outputs.tag }}"
83+
# Create the release if it doesn't exist (dispatch also creates the tag), else just upload.
84+
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
85+
gh release create "$TAG" \
86+
--repo "$GITHUB_REPOSITORY" \
87+
--target "$GITHUB_SHA" \
88+
--title "insta $TAG" \
89+
--generate-notes
90+
fi
91+
gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \
92+
dist-bin/insta-* dist-bin/SHA256SUMS install.sh

README.md

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,29 @@ InstaCloud CLI (`insta`) — a thin client of the [platform](../platform) contro
55

66
技术栈:Node 20 + TypeScript(ESM)+ commander。所有命令都是平台 API 的封装。
77

8-
## 安装 / 构建
8+
## 安装
9+
10+
**一键装(原生二进制,无需 node)** — macOS / Linux / WSL:
11+
12+
```bash
13+
curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh
14+
# 装到 ~/.insta/bin/insta(可 INSTA_INSTALL_DIR 覆盖);校验 SHA256SUMS。
15+
# 固定版本:curl -fsSL .../install.sh | INSTA_VERSION=v0.1.0 sh
16+
# Windows:从 releases 页下载 insta-windows-x64.exe。
17+
```
18+
19+
**从源码构建(需 node):**
920

1021
```bash
1122
npm install
1223
npm run build # -> dist/index.js(bin: insta;纯 JS,运行需 node)
1324
node dist/index.js --help
1425
```
1526

16-
### 原生二进制(Bun,运行时无需 node
27+
### 自己出二进制(Bun 交叉编译
1728

18-
用 Bun 把 CLI 编译成各平台**独立可执行文件**(内嵌运行时,像 Railway CLI 那样)——npm 包仍发布 JS
19-
`dist/index.js`),这些二进制是另一条下载渠道(GitHub releases / curl 安装器)。需要 [Bun](https://bun.sh)
29+
`install.sh` 装的二进制由 CI(tag `v*``.github/workflows/release.yml`)用 Bun 交叉编译并发到 GitHub
30+
releases。本地也可出:需 [Bun](https://bun.sh)。npm 包仍发布 JS(`dist/index.js`)——二进制是另一条渠道。
2031

2132
```bash
2233
npm run compile # 只编译当前平台 -> dist/bin/insta
@@ -32,9 +43,11 @@ npm run build:binaries # 交叉编译全平台 -> dist/bin/insta-<os>-<arch>
3243
```bash
3344
# 指向控制面(默认 http://localhost:8080,可用 $INSTA_API_URL 或 --api-url 覆盖)
3445
insta login --email you@example.com --password ****** --api-url http://localhost:8080
35-
insta project create my-app # provision DB+storage+compute,并 link 当前目录
46+
insta project create my-app # 新建空 project 并 link 当前目录(默认不含 service)
47+
insta services add postgres db # 按需添加 service(postgres/storage/compute)
48+
insta services add compute api # compute 用于部署镜像
3649
insta secrets # 把当前 branch 的凭证写入 ./.env(secret seam)
37-
insta deploy --image <registry/img> # 部署容器镜像到当前 branch 的 compute
50+
insta deploy --image <registry/img> # 部署容器镜像到当前 branch 的 compute service
3851
insta status # 登录态 + 已 link 的 project/branch
3952
```
4053

@@ -45,10 +58,14 @@ insta status # 登录态 + 已 link 的 project/branch
4558
| `insta login [--email --password --api-url]` | 登录(email/password;token 自动 refresh) |
4659
| `insta login --oauth <github\|google>` | 浏览器 OAuth 登录(启本地回环端口,浏览器授权后自动带回 token) |
4760
| `insta logout` / `insta status [--json]` | 登出 / 查看状态 |
48-
| `insta org list [--json]` / `org create <name>` | 组织 |
49-
| `insta project create <name> [--org]` | provision 新 project 并 link |
61+
| `insta org list [--json]` / `org create <name>` | 组织(每个用户仅可拥有一个 free org) |
62+
| `insta project create <name> [--org]` | 新建空 project 并 link(默认不含任何 service) |
5063
| `insta project list [--org] [--json]` / `link <id>` / `delete` | 项目管理 |
51-
| `insta branch create <name> [--from]` | 新建分支环境(克隆 DB/storage/compute) |
64+
| `insta services add <postgres\|storage\|compute> <name>` | 按需 provision 一个 service(postgres/compute 分配默认访问域名) |
65+
| `insta services list [--json]` / `services remove <type> <name>` | 列出 / 删除 service |
66+
| `insta services scale compute <name> <number> [region]` | 设置 compute 机器数(付费档;free 拒绝) |
67+
| `insta services upgrade <compute\|postgres> <name> <spec>` | 升级 spec(付费档;只升不降) |
68+
| `insta branch create <name> [--from]` | 新建分支环境(物化 project 当前的 services;每 project 上限 10 个 branch) |
5269
| `insta branch list [--json]` / `switch <name>` / `delete <name>` | 分支管理 |
5370
| `insta secrets [--branch -o --print --json]` | secret seam:凭证写入 `.env` |
5471
| `insta secrets list [--branch]` | 仅列出 secret 名 |
@@ -63,9 +80,9 @@ insta status # 登录态 + 已 link 的 project/branch
6380
| `insta billing portal [--org --no-open --json]` | 打开 Stripe Customer Portal(改套餐 / 卡 / 取消) |
6481
| `insta approvals list [--status] [--json]` | 治理审批列表 |
6582
| `insta approvals approve <id> [--always]` / `deny <id>` | 批准 / 拒绝(admin) |
66-
| `insta policy get [--json]` / `policy set <action> <decision>` | 治理策略 |
83+
| `insta policy get [--json]` / `policy set <action> <decision>` | 治理策略(action 含 `service.add/remove/scale/upgrade` |
6784

68-
被 governance gate 的操作(`secrets.read`/`deploy`/`project.delete`/`branch.delete`)命中审批时,
85+
被 governance gate 的操作(`secrets.read`/`deploy`/`project.delete`/`branch.delete`/`service.add`/`service.remove`/`service.scale`/`service.upgrade`)命中审批时,
6986
CLI 会提示 `approval required — run: insta approvals approve <id>`
7087

7188
## 配置位置
@@ -97,4 +114,4 @@ insta login --oauth github # 或 google
97114
> 平台侧需配置该 provider 的 OAuth 应用(`GITHUB_OAUTH_CLIENT_ID/SECRET``GOOGLE_*`),
98115
> 且应用的回调 URL 必须是 **`{INSTA_API_BASE_URL}/api/auth/callback/<provider>`**(不是回环地址)。
99116
100-
> `metrics` / `logs` / `usage` 已支持(usage 为采集层聚合)。`compute add-group/scale/set-domain`镜像构建后续加入。
117+
> `metrics` / `logs` / `usage` 已支持(usage 为采集层聚合)。compute service(`services add compute`)、`services scale/upgrade` 已实现;镜像构建后续加入。多 postgres/storage service(每 project >1 个)暂受 credential-seam 限制,为后续工作

install.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env sh
2+
# InstaCloud CLI installer. Downloads the prebuilt native binary for your platform from GitHub
3+
# releases, verifies it against SHA256SUMS, and installs it. macOS / Linux / WSL.
4+
#
5+
# curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh
6+
#
7+
# Options (env):
8+
# INSTA_VERSION release tag to install (e.g. v0.1.0); default: latest
9+
# INSTA_INSTALL_DIR install directory; default: $HOME/.insta/bin
10+
set -eu
11+
12+
REPO="InsForge/insta-cli"
13+
BIN="insta"
14+
INSTALL_DIR="${INSTA_INSTALL_DIR:-$HOME/.insta/bin}"
15+
16+
command -v curl >/dev/null 2>&1 || { echo "error: curl is required" >&2; exit 1; }
17+
18+
# ---- detect platform ----
19+
os="$(uname -s)"; arch="$(uname -m)"
20+
case "$os" in
21+
Darwin) os="darwin" ;;
22+
Linux) os="linux" ;;
23+
*) echo "error: unsupported OS '$os' (Windows: download insta-windows-x64.exe from the releases page)" >&2; exit 1 ;;
24+
esac
25+
case "$arch" in
26+
arm64|aarch64) arch="arm64" ;;
27+
x86_64|amd64) arch="x64" ;;
28+
*) echo "error: unsupported architecture '$arch'" >&2; exit 1 ;;
29+
esac
30+
asset="insta-${os}-${arch}"
31+
32+
# ---- resolve release URL ----
33+
version="${INSTA_VERSION:-latest}"
34+
if [ "$version" = "latest" ]; then
35+
base="https://github.com/$REPO/releases/latest/download"
36+
else
37+
base="https://github.com/$REPO/releases/download/$version"
38+
fi
39+
40+
tmp="$(mktemp -d)"
41+
trap 'rm -rf "$tmp"' EXIT
42+
43+
echo "Installing $BIN ($asset, $version)…"
44+
curl -fsSL "$base/$asset" -o "$tmp/$BIN" || { echo "error: download failed ($base/$asset)" >&2; exit 1; }
45+
curl -fsSL "$base/SHA256SUMS" -o "$tmp/SHA256SUMS" || { echo "error: could not fetch SHA256SUMS" >&2; exit 1; }
46+
47+
# ---- verify checksum ----
48+
expected="$(grep " ${asset}\$" "$tmp/SHA256SUMS" | awk '{print $1}')"
49+
[ -n "$expected" ] || { echo "error: no checksum for $asset in SHA256SUMS" >&2; exit 1; }
50+
if command -v sha256sum >/dev/null 2>&1; then
51+
actual="$(sha256sum "$tmp/$BIN" | awk '{print $1}')"
52+
else
53+
actual="$(shasum -a 256 "$tmp/$BIN" | awk '{print $1}')"
54+
fi
55+
if [ "$expected" != "$actual" ]; then
56+
echo "error: checksum mismatch for $asset" >&2
57+
echo " expected $expected" >&2
58+
echo " actual $actual" >&2
59+
exit 1
60+
fi
61+
62+
# ---- install ----
63+
mkdir -p "$INSTALL_DIR"
64+
chmod +x "$tmp/$BIN"
65+
mv "$tmp/$BIN" "$INSTALL_DIR/$BIN"
66+
echo "✓ installed to $INSTALL_DIR/$BIN"
67+
"$INSTALL_DIR/$BIN" --version 2>/dev/null || true
68+
69+
# ---- PATH hint ----
70+
case ":${PATH}:" in
71+
*":$INSTALL_DIR:"*) ;;
72+
*)
73+
echo
74+
echo "Add $BIN to your PATH by adding this to your shell profile (~/.zshrc, ~/.bashrc):"
75+
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
76+
;;
77+
esac

src/commands/project.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ApiClient, requireProject } from '../api.js'
22
import { writeProject } from '../config.js'
33
import { info, die, printJson, handleApproval } from '../util.js'
44
import { installObserve } from '../observe/install.js'
5+
import { installSkills } from '../ensure-skills.js'
56

67
// Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
78
function tryInstallObserve(): void {
@@ -27,6 +28,7 @@ export async function projectCreate(name: string, opts: { org?: string }): Promi
2728
info(` resources: ${out.resources.map((r: any) => r.kind).join(', ')}`)
2829
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`)
2930
tryInstallObserve()
31+
await installSkills({ cwd: process.cwd() })
3032
}
3133

3234
export async function projectList(opts: { org?: string; json?: boolean }): Promise<void> {
@@ -44,6 +46,7 @@ export async function projectLink(id: string): Promise<void> {
4446
await writeProject({ projectId: project.id, orgId: project.org_id, branch: 'main' })
4547
info(`linked project ${project.id} (${project.name})`)
4648
tryInstallObserve()
49+
await installSkills({ cwd: process.cwd() })
4750
}
4851

4952
export async function projectDelete(opts: { project?: string }): Promise<void> {

src/commands/services.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// `insta services` — manage a project's opt-in services (postgres | storage | compute).
2+
import { ApiClient, requireProject } from '../api.js'
3+
import { info, printJson, handleApproval } from '../util.js'
4+
5+
export const SERVICE_TYPES = ['postgres', 'storage', 'compute'] as const
6+
export type ServiceType = (typeof SERVICE_TYPES)[number]
7+
8+
// ---- pure, unit-tested helpers (throw plain Errors; the CLI guard turns them into clean output) ----
9+
10+
// Validate a service-type argument against the allowed set for a command.
11+
export function assertType(type: string, allowed: readonly string[] = SERVICE_TYPES): asserts type is ServiceType {
12+
if (!allowed.includes(type)) throw new Error(`type must be ${allowed.join('|')}`)
13+
}
14+
15+
// Parse a positive-integer machine count.
16+
export function parseCount(raw: string): number {
17+
const n = Number(raw)
18+
if (!Number.isInteger(n) || n < 1) throw new Error(`count must be a positive integer, got: ${raw}`)
19+
return n
20+
}
21+
22+
// Resolve a service id from a `services list` result by (type, name).
23+
export function resolveServiceId(services: Array<{ id: string; type: string; name: string }>, type: string, name: string): string {
24+
const svc = services.find((s) => s.type === type && s.name === name)
25+
if (!svc) throw new Error(`service not found: ${type} ${name}`)
26+
return svc.id
27+
}
28+
29+
// ---- commands ----
30+
31+
export async function servicesAdd(type: string, name: string): Promise<void> {
32+
assertType(type)
33+
const api = await ApiClient.load()
34+
const p = await requireProject()
35+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name })
36+
if (handleApproval(res)) return
37+
const svc = res.body.service
38+
info(`added ${type} service ${name} (${svc.id})${svc.domain ? ` — ${svc.domain}` : ''}`)
39+
}
40+
41+
export async function servicesList(opts: { json?: boolean }): Promise<void> {
42+
const api = await ApiClient.load()
43+
const p = await requireProject()
44+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
45+
if (opts.json) return printJson(services)
46+
if (!services.length) return info('(no services — add one with `insta services add <postgres|storage|compute> <name>`)')
47+
for (const s of services) {
48+
const extra = s.type === 'compute' ? ` x${s.machine_count}` : ''
49+
info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`)
50+
}
51+
}
52+
53+
export async function servicesRemove(type: string, name: string): Promise<void> {
54+
assertType(type)
55+
const api = await ApiClient.load()
56+
const p = await requireProject()
57+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
58+
const id = resolveServiceId(services, type, name)
59+
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`)
60+
if (handleApproval(res)) return
61+
info(`removed ${type} service ${name}`)
62+
}
63+
64+
// insta services scale compute <name> <number> [region]
65+
export async function servicesScale(type: string, name: string, number: string, region: string | undefined, _opts: { json?: boolean }): Promise<void> {
66+
assertType(type, ['compute'])
67+
const machineCount = parseCount(number)
68+
const api = await ApiClient.load()
69+
const p = await requireProject()
70+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
71+
const id = resolveServiceId(services, type, name)
72+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region })
73+
if (handleApproval(res)) return
74+
if (_opts.json) return printJson(res.body.service)
75+
info(`scaled compute ${name} to ${machineCount} machine(s)${region ? ` in ${region}` : ''}`)
76+
}
77+
78+
// insta services upgrade <compute|postgres> <name> <new-spec>
79+
export async function servicesUpgrade(type: string, name: string, spec: string, _opts: { json?: boolean }): Promise<void> {
80+
assertType(type, ['compute', 'postgres'])
81+
const api = await ApiClient.load()
82+
const p = await requireProject()
83+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
84+
const id = resolveServiceId(services, type, name)
85+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec })
86+
if (handleApproval(res)) return
87+
if (_opts.json) return printJson(res.body.service)
88+
info(`upgraded ${type} ${name} to ${spec}`)
89+
}

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type GlobalConfig = {
1717

1818
export type ProjectConfig = { projectId: string; orgId: string; branch: string }
1919

20-
const DEFAULT_API = process.env.INSTA_API_URL ?? 'http://localhost:8080'
20+
const DEFAULT_API = process.env.INSTA_API_URL ?? 'https://beta-api.insta.insforge.dev'
2121

2222
export async function readGlobal(): Promise<GlobalConfig> {
2323
try {

0 commit comments

Comments
 (0)