Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ dist
dist-bin

generated
# Keep committed snapshots of the generated GraphQL/REST artifacts so the
# binary can build without live codegen (the tenant has GraphQL introspection
# disabled, so `bun codegen:gql` cannot regenerate them).
!src/gql/generated/
!src/rest/generated/

.env
!.env.example
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ src/
├── commands/ # CLI commands organized by resource
│ ├── alert/ # Alert commands (list, view)
│ ├── auth/ # Auth commands (configure, login, logout, status)
│ ├── board/ # Board (dashboard) commands (create, update, get, list, delete, scaffold, set-default, clear-default)
│ ├── folder/ # Folder commands (create, get, update, delete)
│ ├── cli/ # CLI management (install, uninstall, upgrade)
│ ├── content/ # Content pack management (experimental: gated + hidden)
│ │ ├── host/ # Host Explorer (install, view)
Expand All @@ -52,6 +54,8 @@ src/
├── gql/ # GraphQL layer
│ ├── generated/ # Auto-generated types (DO NOT EDIT)
│ ├── authtoken/ # Auth token mutations
│ ├── board/ # Board (dashboard) queries/mutations (hand-authored; codegen unavailable)
│ ├── folder/ # Folder queries/mutations (hand-authored; codegen unavailable)
│ ├── content/ # Content pack queries/mutations
│ ├── dataset/ # Dataset queries
│ ├── datastream/ # Datastream queries/mutations
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ To update installed skills after edits in this repo, run `npx skills update`.

## Commands

> Note: some commands listed below are temporarily disabled in this build. They
> depend on generated GraphQL/REST types that cannot be regenerated while the
> target tenant has GraphQL introspection disabled. They are commented out in
> `src/app.ts` (search for "TEMP: trimmed pending full Observe schema access")
> and will be restored once `bun codegen:gql` can run. The currently active
> commands are: `help`, `dataset`, `alert`, `board`, `folder`, `cli`.

| Command | Description |
| --------------------------------------- | ------------------------------------------------------- |
| `observe help` | Show help information |
Expand All @@ -55,6 +62,18 @@ To update installed skills after edits in this repo, run `npx skills update`.
| `observe datastream view` | View a datastream by ID |
| `observe datastream update` | Update a datastream |
| `observe datastream-token check-status` | Poll a datastream token until ingest data arrives |
| `observe board create` | Create a board (dashboard) from a JSON file |
| `observe board update` | Update an existing board from a JSON file |
| `observe board get` | Get a board by ID as JSON |
| `observe board list` | List boards in a workspace |
| `observe board delete` | Delete a board by ID |
| `observe board scaffold` | Print a minimal board JSON template |
| `observe board set-default` | Set the default dashboard for a dataset |
| `observe board clear-default` | Clear the default dashboard for a dataset |
| `observe folder create` | Create a folder (use --ensure to make it idempotent) |
| `observe folder get` | Look up a folder by name and print its ID |
| `observe folder update` | Update a folder's name, description, or icon URL |
| `observe folder delete` | Delete a folder by ID |
| `observe cli install` | Configure shell integration (PATH, completions) |
| `observe cli uninstall` | Remove shell integration |
| `observe cli upgrade` | Upgrade to the latest version |
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
"node": ">=22"
},
"scripts": {
"build": "bun run codegen && bun run scripts/build.ts",
"build:single": "bun run codegen && bun run scripts/build.ts --single",
"build": "bun run ifneeded:codegen && bun run scripts/build.ts",
"build:single": "bun run ifneeded:codegen && bun run scripts/build.ts --single",
"dev": "bun run ifneeded:codegen && bun run src/bin.ts",
"test": "bun run codegen && bun run typecheck && bun run lint && bun run format && bun test src",
"test": "bun run ifneeded:codegen && bun run typecheck && bun run lint && bun run format && bun test src",
"test:integration": "bun test integration/ --concurrent --max-concurrency 5",
"typecheck": "tsc",
"lint": "eslint",
Expand Down
64 changes: 40 additions & 24 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,58 @@
import { buildApplication } from "@stricli/core";
import { name } from "../package.json";
import { alertRoutes } from "./commands/alert/index.js";
import { dataConnectionRoutes } from "./commands/data-connection/index.js";
import { datasourceRoutes } from "./commands/datasource/index.js";
import { datastreamTokenRoutes } from "./commands/datastream-token/index.js";
import { authRoutes } from "./commands/auth/index.js";
// TEMP: trimmed pending full Observe schema access (introspection disabled). Restore when schema available.
// The committed src/gql/generated/graphql.ts snapshot only contains the three
// OPAL operations (CheckQueries, ValidateIngestFilter, VerbsAndFunctions); the
// generated Document constants and result types for every other upstream
// operation are absent and cannot be regenerated (the tenant has GraphQL
// introspection disabled). Every route below whose command modules statically
// import a missing generated GraphQL export therefore fails to load, so they
// are trimmed until schema access is restored and `bun codegen:gql` can run.
// GraphQL-broken: auth (logout->delete-authtoken), metric, query, content,
// ingest-token, datastream, datastream-token.
// Schema-type-broken (skill/tag/datasource/data-connection): as before.
// import { dataConnectionRoutes } from "./commands/data-connection/index.js";
// import { datasourceRoutes } from "./commands/datasource/index.js";
// import { datastreamTokenRoutes } from "./commands/datastream-token/index.js";
// import { authRoutes } from "./commands/auth/index.js";
import { cliRoutes } from "./commands/cli/index.js";
import { contentRoutes } from "./commands/content/index.js";
// import { contentRoutes } from "./commands/content/index.js";
import { datasetRoutes } from "./commands/dataset/index.js";
import { datastreamRoutes } from "./commands/datastream/index.js";
// import { datastreamRoutes } from "./commands/datastream/index.js";
import { helpCommand } from "./commands/help.js";
import { ingestTokenRoutes } from "./commands/ingest-token/index.js";
import { metricRoutes } from "./commands/metric/index.js";
import { queryCommand } from "./commands/query.js";
import { skillRoutes } from "./commands/skill/index.js";
import { tagKeyRoutes } from "./commands/tag-key/index.js";
import { tagValueRoutes } from "./commands/tag-value/index.js";
// import { ingestTokenRoutes } from "./commands/ingest-token/index.js";
// import { metricRoutes } from "./commands/metric/index.js";
// import { queryCommand } from "./commands/query.js";
// import { skillRoutes } from "./commands/skill/index.js";
// import { tagKeyRoutes } from "./commands/tag-key/index.js";
// import { tagValueRoutes } from "./commands/tag-value/index.js";
import { boardRoutes } from "./commands/board/index.js";
import { folderRoutes } from "./commands/folder/index.js";
import { CURRENT_CLI_VERSION } from "./lib/constants.js";
import { defineRoutes } from "./lib/stricli-wrappers.js";

/** Top-level route map containing all CLI commands */
export const routes = defineRoutes({
routes: {
help: helpCommand,
auth: authRoutes,
"tag-value": tagValueRoutes,
"tag-key": tagKeyRoutes,
// TEMP: trimmed pending full Observe schema access (introspection disabled). Restore when schema available.
// auth: authRoutes,
// "tag-value": tagValueRoutes,
// "tag-key": tagKeyRoutes,
dataset: datasetRoutes,
metric: metricRoutes,
query: queryCommand,
skill: skillRoutes,
// metric: metricRoutes,
// query: queryCommand,
// skill: skillRoutes,
alert: alertRoutes,
content: contentRoutes,
"ingest-token": ingestTokenRoutes,
datasource: datasourceRoutes,
"data-connection": dataConnectionRoutes,
datastream: datastreamRoutes,
"datastream-token": datastreamTokenRoutes,
// content: contentRoutes,
// "ingest-token": ingestTokenRoutes,
// datasource: datasourceRoutes,
// "data-connection": dataConnectionRoutes,
// datastream: datastreamRoutes,
// "datastream-token": datastreamTokenRoutes,
board: boardRoutes,
folder: folderRoutes,
cli: cliRoutes,
},
defaultCommand: "help",
Expand Down
53 changes: 53 additions & 0 deletions src/commands/board/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { buildCommand } from "@stricli/core";
import type { LocalContext } from "../../context";
import { saveBoard } from "../../gql/board/save-board";
import { GqlApiError } from "../../gql/gql-request";
import { loadConfig } from "../../lib/config";
import { readBoardInput } from "./read-input";
import { boardViewURL } from "./view-url";

async function create(
this: LocalContext,
_flags: Record<string, never>,
file: string,
): Promise<void> {
const { process, writer } = this;

try {
const config = loadConfig();
const input = readBoardInput(file);

const board = await saveBoard(config, input);

writer.write(`Created: ${board.name} (id: ${board.id})`);
writer.write(`Visibility: ${board.visibility ?? ""}`);
writer.write(`View: ${boardViewURL(config, board.workspaceId, board.id)}`);
} catch (error) {
if (error instanceof GqlApiError) {
writer.error(`API Error (${error.statusCode}): ${error.message}`);
} else {
const message = error instanceof Error ? error.message : String(error);
writer.error(`Error: ${message}`);
}
process.exit(1);
}
}

export const createCommand = buildCommand({
loader: async () => create,
parameters: {
positional: {
kind: "tuple",
parameters: [
{
brief: "Path to a board (dashboard) JSON file",
parse: String,
},
],
},
flags: {},
},
docs: {
brief: "Create a board (dashboard) from a JSON file",
},
});
56 changes: 56 additions & 0 deletions src/commands/board/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { buildCommand } from "@stricli/core";
import type { LocalContext } from "../../context";
import { deleteBoard } from "../../gql/board/delete-board";
import { GqlApiError } from "../../gql/gql-request";
import { loadConfig } from "../../lib/config";

async function del(
this: LocalContext,
_flags: Record<string, never>,
id: string,
): Promise<void> {
const { process, writer } = this;

try {
const config = loadConfig();
const result = await deleteBoard(config, id);

// The API reports success via `success: true` and may populate
// `errorMessage` with a success notice, so only treat it as a failure
// when `success` is explicitly false.
if (result.success === false) {
writer.error(`Error: board delete: ${result.errorMessage ?? "failed"}`);
process.exit(1);
return;
}

writer.write(`Deleted board ${id}`);
} catch (error) {
if (error instanceof GqlApiError) {
writer.error(`API Error (${error.statusCode}): ${error.message}`);
} else {
const message = error instanceof Error ? error.message : String(error);
writer.error(`Error: ${message}`);
}
process.exit(1);
}
}

export const deleteCommand = buildCommand({
loader: async () => del,
parameters: {
positional: {
kind: "tuple",
parameters: [
{
brief: "Board (dashboard) ID to delete",
parse: String,
},
],
},
flags: {},
},
docs: {
brief: "Delete a board (dashboard) by ID",
},
});
53 changes: 53 additions & 0 deletions src/commands/board/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { buildCommand } from "@stricli/core";
import type { LocalContext } from "../../context";
import { getBoard } from "../../gql/board/get-board";
import { GqlApiError } from "../../gql/gql-request";
import { loadConfig } from "../../lib/config";

async function get(
this: LocalContext,
_flags: Record<string, never>,
id: string,
): Promise<void> {
const { process, writer } = this;

try {
const config = loadConfig();
const board = await getBoard(config, id);

if (board === null) {
writer.error(`Error: board not found: ${id}`);
process.exit(1);
return;
}

writer.write(JSON.stringify(board, null, 2));
} catch (error) {
if (error instanceof GqlApiError) {
writer.error(`API Error (${error.statusCode}): ${error.message}`);
} else {
const message = error instanceof Error ? error.message : String(error);
writer.error(`Error: ${message}`);
}
process.exit(1);
}
}

export const getCommand = buildCommand({
loader: async () => get,
parameters: {
positional: {
kind: "tuple",
parameters: [
{
brief: "Board (dashboard) ID",
parse: String,
},
],
},
flags: {},
},
docs: {
brief: "Get a board (dashboard) by ID as JSON",
},
});
38 changes: 38 additions & 0 deletions src/commands/board/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { buildRouteMap } from "@stricli/core";
import { createCommand } from "./create";
import { updateCommand } from "./update";
import { getCommand } from "./get";
import { listCommand } from "./list";
import { deleteCommand } from "./delete";
import { scaffoldCommand } from "./scaffold";
import { clearDefaultCommand, setDefaultCommand } from "./set-default";

export const boardRoutes = buildRouteMap({
routes: {
create: createCommand,
update: updateCommand,
get: getCommand,
list: listCommand,
delete: deleteCommand,
scaffold: scaffoldCommand,
"set-default": setDefaultCommand,
"clear-default": clearDefaultCommand,
},
docs: {
brief: "Manage boards (dashboards)",
fullDescription: [
"Create, update, read, list, delete, and scaffold boards (dashboards),",
"and set or clear the default dashboard for a dataset.",
"",
"Commands:",
" create Create a board from a JSON file",
" update Update an existing board from a JSON file",
" get Get a board by ID as JSON",
" list List boards in a workspace",
" delete Delete a board by ID",
" scaffold Print a minimal board JSON template",
" set-default Set the default dashboard for a dataset",
" clear-default Clear the default dashboard for a dataset",
].join("\n"),
},
});
Loading
Loading