From 4ad62ac2297c1cb8f54bca69a5a608d303817af2 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 14:35:46 +0000 Subject: [PATCH 1/3] feat: add application list CLI command and use build artifacts in expert-cd - Add `perstack application list` command via @perstack/studio - Rewrite expert-cd workflow to use built artifacts (`node dist/bin/cli.js`) instead of `bun .ts` or inline `bun -e` scripts - Replace ad-hoc inline API client code with proper CLI commands Co-Authored-By: Claude Opus 4.6 --- .github/workflows/expert-cd.yml | 54 ++++++++++++--------- apps/perstack/bin/cli.ts | 16 ++++++ docs/references/cli.md | 54 ++++++++++++++++----- packages/studio/src/application-handlers.ts | 25 ++++++++++ packages/studio/src/index.ts | 4 ++ 5 files changed, 116 insertions(+), 37 deletions(-) create mode 100644 packages/studio/src/application-handlers.ts diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 8e603b52..2e67e259 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -7,6 +7,9 @@ on: paths: - 'definitions/**/perstack.toml' +env: + PERSTACK_CLI: node apps/perstack/dist/bin/cli.js + jobs: publish: name: Publish Expert Definitions @@ -19,10 +22,16 @@ jobs: steps: - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile + - run: bun run build + - name: Extract version from perstack.toml id: version run: | @@ -33,7 +42,7 @@ jobs: - name: Check if version already exists id: check run: | - OUTPUT=$(bun ./apps/perstack/bin/cli.ts expert versions ${{ matrix.definition.name }} \ + OUTPUT=$($PERSTACK_CLI expert versions ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} 2>&1) || true echo "$OUTPUT" if echo "$OUTPUT" | grep -q "^ ${{ steps.version.outputs.version }}"; then @@ -47,27 +56,24 @@ jobs: - name: Resolve or create draft scope if: steps.check.outputs.should-publish == 'true' id: draft - env: - PERSTACK_API_KEY: ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} run: | - DRAFT_SCOPE_ID=$(bun -e " - import { createApiClient } from '@perstack/api-client'; - const client = createApiClient({ apiKey: process.env.PERSTACK_API_KEY }); - const list = await client.expertDrafts.list({ filter: '${{ matrix.definition.name }}' }); - if (list.ok && list.data.data.length > 0) { - console.log(list.data.data[0].id); - } else { - const apps = await client.applications.list(); - if (!apps.ok) throw new Error('Failed to list applications: ' + apps.error.message); - const appId = apps.data.data.applications[0].id; - const create = await client.expertDrafts.create({ - scopeName: '${{ matrix.definition.name }}', - applicationId: appId, - }); - if (!create.ok) throw new Error('Failed to create draft: ' + create.error.message); - console.log(create.data.data.id); - } - ") + # Try to find existing draft scope + OUTPUT=$($PERSTACK_CLI expert list --filter ${{ matrix.definition.name }} \ + --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) + echo "$OUTPUT" + DRAFT_SCOPE_ID=$(echo "$OUTPUT" | grep "ID:" | head -1 | awk '{print $2}') + + if [ -z "$DRAFT_SCOPE_ID" ]; then + # No draft scope exists — create one + APP_ID=$($PERSTACK_CLI application list \ + --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} | grep "ID:" | head -1 | awk '{print $2}') + CREATE_OUTPUT=$($PERSTACK_CLI expert create ${{ matrix.definition.name }} \ + --app "$APP_ID" \ + --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) + echo "$CREATE_OUTPUT" + DRAFT_SCOPE_ID=$(echo "$CREATE_OUTPUT" | grep "ID:" | awk '{print $2}') + fi + echo "draft-scope-id=$DRAFT_SCOPE_ID" >> "$GITHUB_OUTPUT" echo "Resolved draft scope ID: $DRAFT_SCOPE_ID" @@ -75,7 +81,7 @@ jobs: if: steps.check.outputs.should-publish == 'true' id: push run: | - OUTPUT=$(bun ./apps/perstack/bin/cli.ts expert push ${{ steps.draft.outputs.draft-scope-id }} \ + OUTPUT=$($PERSTACK_CLI expert push ${{ steps.draft.outputs.draft-scope-id }} \ --config ${{ matrix.definition.path }}/perstack.toml \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) echo "$OUTPUT" @@ -85,13 +91,13 @@ jobs: - name: Publish scope if private if: steps.check.outputs.should-publish == 'true' run: | - bun ./apps/perstack/bin/cli.ts expert publish ${{ matrix.definition.name }} \ + $PERSTACK_CLI expert publish ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} || true - name: Assign version if: steps.check.outputs.should-publish == 'true' run: | - bun ./apps/perstack/bin/cli.ts expert version \ + $PERSTACK_CLI expert version \ ${{ steps.draft.outputs.draft-scope-id }} \ ${{ steps.push.outputs.ref-id }} \ ${{ steps.version.outputs.version }} \ diff --git a/apps/perstack/bin/cli.ts b/apps/perstack/bin/cli.ts index 689e6ca7..d5ff86a9 100755 --- a/apps/perstack/bin/cli.ts +++ b/apps/perstack/bin/cli.ts @@ -11,6 +11,7 @@ import { loadLockfile, } from "@perstack/perstack-toml" import { + applicationsListHandler, expertCreateHandler, expertDeleteHandler, expertListHandler, @@ -286,6 +287,21 @@ expertCmd await expertYankHandler(key, parent) }) +// Application management commands +const applicationCmd = program + .command("application") + .description("Manage applications on Perstack API") + .option("--api-key ", "Perstack API key (default: PERSTACK_API_KEY env)") + .option("--base-url ", "Custom API base URL") + +applicationCmd + .command("list") + .description("List applications") + .action(async function (this: InstanceType) { + const parent = getParentOptions(this) + await applicationsListHandler(parent) + }) + program.parseAsync().catch((error) => { if (error instanceof PerstackError) { console.error(error.message) diff --git a/docs/references/cli.md b/docs/references/cli.md index bd9d5bc4..8cd2ccbd 100644 --- a/docs/references/cli.md +++ b/docs/references/cli.md @@ -12,17 +12,19 @@ perstack ├── run Headless execution with JSON event output ├── log View execution history and events ├── install Pre-collect tool definitions for faster startup -└── expert Manage experts on Perstack API - ├── list List draft scopes - ├── create Create a new draft scope - ├── delete Delete a draft scope - ├── push Push local expert definitions to a draft ref - ├── refs List draft refs - ├── version Assign a version to a draft ref - ├── versions List published versions - ├── publish Make an expert scope public - ├── unpublish Make an expert scope private - └── yank Deprecate a specific version +├── expert Manage experts on Perstack API +│ ├── list List draft scopes +│ ├── create Create a new draft scope +│ ├── delete Delete a draft scope +│ ├── push Push local expert definitions to a draft ref +│ ├── refs List draft refs +│ ├── version Assign a version to a draft ref +│ ├── versions List published versions +│ ├── publish Make an expert scope public +│ ├── unpublish Make an expert scope private +│ └── yank Deprecate a specific version +└── application Manage applications on Perstack API + └── list List applications ``` ## Running Experts @@ -367,11 +369,34 @@ perstack expert yank | -------- | -------- | --------------------------------------------- | | `` | Yes | Expert key with version (e.g., `my-expert@1.0.0`) | +## Application Management + +The `application` command group manages applications on the Perstack API. + +```bash +perstack application [options] +``` + +**Parent options (inherited by all subcommands):** + +| Option | Default | Description | +| ------------------ | -------------------- | -------------- | +| `--api-key ` | `PERSTACK_API_KEY` env var | API key | +| `--base-url ` | `https://api.perstack.ai` | API base URL | + +### `application list` + +List applications. + +```bash +perstack application list +``` + ## Environment Variables | Variable | Description | | ---------------------- | -------------------------------------------------------- | -| `PERSTACK_API_KEY` | API key for `expert` commands | +| `PERSTACK_API_KEY` | API key for `expert` and `application` commands | | `PERSTACK_STORAGE_PATH`| Storage directory for job/run data (default: `./perstack`) | ## Examples @@ -405,8 +430,11 @@ perstack run @org/expert@1.0.0 "query" # Generate lockfile perstack install +# List applications +perstack application list + # Expert lifecycle -perstack expert create my-expert --app app_123 +perstack expert create my-expert --app perstack expert push --config ./perstack.toml perstack expert version 1.0.0 --tag latest perstack expert versions my-expert diff --git a/packages/studio/src/application-handlers.ts b/packages/studio/src/application-handlers.ts new file mode 100644 index 00000000..b4feab8f --- /dev/null +++ b/packages/studio/src/application-handlers.ts @@ -0,0 +1,25 @@ +import { PerstackError } from "@perstack/core" +import { createStudioClient, resolveApiKey } from "./client.js" + +export interface ApplicationsListOptions { + apiKey?: string + baseUrl?: string +} + +export async function applicationsListHandler(options: ApplicationsListOptions) { + const apiKey = resolveApiKey(options.apiKey) + const client = createStudioClient({ apiKey, baseUrl: options.baseUrl }) + const result = await client.applications.list() + if (!result.ok) { + throw new PerstackError(`Failed to list applications: ${result.error.message}`) + } + const { applications } = result.data.data + if (applications.length === 0) { + console.log("No applications found.") + return + } + for (const app of applications) { + console.log(` ${app.name}`) + console.log(` ID: ${app.id}`) + } +} diff --git a/packages/studio/src/index.ts b/packages/studio/src/index.ts index 03342cd4..6cc61632 100644 --- a/packages/studio/src/index.ts +++ b/packages/studio/src/index.ts @@ -1,3 +1,7 @@ +export { + type ApplicationsListOptions, + applicationsListHandler, +} from "./application-handlers.js" export { createStudioClient, resolveApiKey, type StudioOptions } from "./client.js" export { type ExpertCreateOptions, From a7398c41f0a9e321a23190dcec882c44b91273cb Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 14:42:41 +0000 Subject: [PATCH 2/3] ci: use npx perstack@latest in expert-cd workflow Replace source build (npm install + turbo build) with npx perstack@latest from npm registry. Only checkout definitions/ via sparse-checkout for reading perstack.toml files. Add changeset for application list command. Co-Authored-By: Claude Opus 4.6 --- .changeset/add-application-list-command.md | 6 ++++++ .github/workflows/expert-cd.yml | 10 +++------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .changeset/add-application-list-command.md diff --git a/.changeset/add-application-list-command.md b/.changeset/add-application-list-command.md new file mode 100644 index 00000000..d841f236 --- /dev/null +++ b/.changeset/add-application-list-command.md @@ -0,0 +1,6 @@ +--- +"@perstack/studio": patch +"perstack": patch +--- + +Add `perstack application list` CLI command for listing applications. diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 2e67e259..6ce9d8e5 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -8,7 +8,7 @@ on: - 'definitions/**/perstack.toml' env: - PERSTACK_CLI: node apps/perstack/dist/bin/cli.js + PERSTACK_CLI: npx perstack@latest jobs: publish: @@ -21,17 +21,13 @@ jobs: path: definitions/create-expert steps: - uses: actions/checkout@v6 + with: + sparse-checkout: definitions - uses: actions/setup-node@v6 with: node-version: 22 - - uses: oven-sh/setup-bun@v2 - - - run: bun install --frozen-lockfile - - - run: bun run build - - name: Extract version from perstack.toml id: version run: | From 3100540d30e5c1559bfdaf3dd294ac5b7b69e510 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Feb 2026 14:43:31 +0000 Subject: [PATCH 3/3] ci: inline npx perstack@latest, remove env variable Co-Authored-By: Claude Opus 4.6 --- .github/workflows/expert-cd.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/expert-cd.yml b/.github/workflows/expert-cd.yml index 6ce9d8e5..50239058 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -7,9 +7,6 @@ on: paths: - 'definitions/**/perstack.toml' -env: - PERSTACK_CLI: npx perstack@latest - jobs: publish: name: Publish Expert Definitions @@ -38,7 +35,7 @@ jobs: - name: Check if version already exists id: check run: | - OUTPUT=$($PERSTACK_CLI expert versions ${{ matrix.definition.name }} \ + OUTPUT=$(npx perstack@latest expert versions ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} 2>&1) || true echo "$OUTPUT" if echo "$OUTPUT" | grep -q "^ ${{ steps.version.outputs.version }}"; then @@ -54,16 +51,16 @@ jobs: id: draft run: | # Try to find existing draft scope - OUTPUT=$($PERSTACK_CLI expert list --filter ${{ matrix.definition.name }} \ + OUTPUT=$(npx perstack@latest expert list --filter ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) echo "$OUTPUT" DRAFT_SCOPE_ID=$(echo "$OUTPUT" | grep "ID:" | head -1 | awk '{print $2}') if [ -z "$DRAFT_SCOPE_ID" ]; then # No draft scope exists — create one - APP_ID=$($PERSTACK_CLI application list \ + APP_ID=$(npx perstack@latest application list \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} | grep "ID:" | head -1 | awk '{print $2}') - CREATE_OUTPUT=$($PERSTACK_CLI expert create ${{ matrix.definition.name }} \ + CREATE_OUTPUT=$(npx perstack@latest expert create ${{ matrix.definition.name }} \ --app "$APP_ID" \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) echo "$CREATE_OUTPUT" @@ -77,7 +74,7 @@ jobs: if: steps.check.outputs.should-publish == 'true' id: push run: | - OUTPUT=$($PERSTACK_CLI expert push ${{ steps.draft.outputs.draft-scope-id }} \ + OUTPUT=$(npx perstack@latest expert push ${{ steps.draft.outputs.draft-scope-id }} \ --config ${{ matrix.definition.path }}/perstack.toml \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }}) echo "$OUTPUT" @@ -87,13 +84,13 @@ jobs: - name: Publish scope if private if: steps.check.outputs.should-publish == 'true' run: | - $PERSTACK_CLI expert publish ${{ matrix.definition.name }} \ + npx perstack@latest expert publish ${{ matrix.definition.name }} \ --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} || true - name: Assign version if: steps.check.outputs.should-publish == 'true' run: | - $PERSTACK_CLI expert version \ + npx perstack@latest expert version \ ${{ steps.draft.outputs.draft-scope-id }} \ ${{ steps.push.outputs.ref-id }} \ ${{ steps.version.outputs.version }} \