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 8e603b52..50239058 100644 --- a/.github/workflows/expert-cd.yml +++ b/.github/workflows/expert-cd.yml @@ -18,10 +18,12 @@ jobs: path: definitions/create-expert steps: - uses: actions/checkout@v6 + with: + sparse-checkout: definitions - - uses: oven-sh/setup-bun@v2 - - - run: bun install --frozen-lockfile + - uses: actions/setup-node@v6 + with: + node-version: 22 - name: Extract version from perstack.toml id: version @@ -33,7 +35,7 @@ jobs: - name: Check if version already exists id: check run: | - OUTPUT=$(bun ./apps/perstack/bin/cli.ts 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 @@ -47,27 +49,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=$(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=$(npx perstack@latest application list \ + --api-key ${{ secrets.PERSTACK_PRODUCTION_API_KEY }} | grep "ID:" | head -1 | awk '{print $2}') + CREATE_OUTPUT=$(npx perstack@latest 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 +74,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=$(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" @@ -85,13 +84,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 }} \ + 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: | - bun ./apps/perstack/bin/cli.ts expert version \ + npx perstack@latest 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,