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
22 changes: 22 additions & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,28 @@ altertable api spec
altertable api spec --json
```

### `altertable doctor`

Diagnose local configuration and Altertable connectivity.

**Usage**

`altertable doctor [options]`

**Options**

| Option | Description |
| --- | --- |
| `--offline` | Inspect local configuration without contacting Altertable APIs. |

**Examples**

```bash
altertable doctor
altertable doctor --offline
altertable --json doctor
```

### `altertable update`

Update Altertable CLI to the latest release.
Expand Down
6 changes: 4 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ bun run lint:fix # oxlint --fix
bun run format # oxfmt
bun run format:check # CI formatting check
bun run knip # required dead-code/unused-export check
bun run generate # regenerate OpenAPI types
bun run generate # regenerate OpenAPI types, operation index, and COMMANDS.md
bun run generate:commands # regenerate only COMMANDS.md after command changes
bun run generate:check # non-mutating generated-artifact drift check
bun run spec:refresh # fetch hosted OpenAPI spec (see specs/rest/SPEC.md) + generate
bun run build # bundle to cli/dist/cli.js
bun run pack:check # build + dry-run pack (verify publish contents)
Expand Down Expand Up @@ -167,7 +169,7 @@ NODE_TLS_REJECT_UNAUTHORIZED=0 altertable login
From repo root, one script mirrors CI (minus native binary compile):

```bash
./scripts/verify.sh --quick # typecheck, lint, format, knip, coverage, openapi drift
./scripts/verify.sh --quick # typecheck, lint, format, knip, coverage, generated-artifact drift
./scripts/verify.sh # + build, pack:check, top-level black-box tests
./scripts/verify.sh --integration # + tests/integration.e2e.ts (requires mock at :15000)
```
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Query and manage your Altertable data platform from the terminal.
- [Commands](#commands)
- [Lakehouse](#lakehouse)
- [Management](#management)
- [Diagnostics](#diagnostics)
- [Shell completion](#shell-completion)
- [Global flags](#global-flags)
- [Scripting](#scripting)
Expand Down Expand Up @@ -415,6 +416,23 @@ altertable api /environments/production/connections --input postgres-connection.
printf '%s' '{"name":"Analytics"}' | altertable api /environments/production/databases --input -
```

### Diagnostics

Run read-only checks against the selected profile, credential store, and both API
planes:

```bash
altertable doctor
altertable doctor --offline
altertable --json doctor
```

`--offline` validates only local configuration and credential presence. Network
checks use the global `--connect-timeout` and `--read-timeout` values. Doctor
findings do not refresh OAuth tokens, provision lakehouse credentials, or modify
profile files. A completed diagnostic exits successfully even when its report is
unhealthy; scripts should inspect the JSON `healthy` field.

### Shell completion

Install completion for bash, zsh, or fish:
Expand Down
2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"test": "bun test",
"test:coverage": "bun test --coverage",
"generate": "bun run scripts/generate-openapi-types.ts && bun run scripts/generate-openapi-index.ts && bun run scripts/generate-command-reference.ts",
"generate:check": "bun run scripts/generate-openapi-types.ts --check && bun run scripts/generate-openapi-index.ts --check && bun run scripts/generate-command-reference.ts --check",
"generate:commands": "bun run scripts/generate-command-reference.ts",
"spec:refresh": "curl -fsSL https://app.altertable.ai/rest/v1/openapi.yaml -o openapi/openapi.yaml && bun run generate && oxfmt openapi/openapi.yaml",
"build": "bun run scripts/npm-bundle.ts",
"release:build": "bun run scripts/release.ts build",
Expand Down
15 changes: 12 additions & 3 deletions cli/scripts/generate-command-reference.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { buildMainCommand } from "@/cli.ts";
import { resolveCommandDescriptor, validateCommandDescriptor } from "@/lib/command-descriptor.ts";
import { renderCommandReference } from "@/lib/command-reference.ts";
import {
parseGeneratedArtifactMode,
updateOrCheckGeneratedArtifact,
} from "@/../scripts/generated-artifact.ts";

const outputPath = join(import.meta.dir, "../../COMMANDS.md");
const descriptor = await resolveCommandDescriptor(buildMainCommand());
validateCommandDescriptor(descriptor);
writeFileSync(outputPath, renderCommandReference(descriptor));
console.log(`Wrote ${outputPath}`);
const mode = parseGeneratedArtifactMode(process.argv.slice(2));
updateOrCheckGeneratedArtifact({
outputPath,
content: renderCommandReference(descriptor),
mode,
generateCommand: "bun run generate:commands",
});
console.log(`${mode === "check" ? "Checked" : "Wrote"} ${outputPath}`);
13 changes: 10 additions & 3 deletions cli/scripts/generate-openapi-index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { readFileSync, writeFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
parseGeneratedArtifactMode,
updateOrCheckGeneratedArtifact,
} from "@/../scripts/generated-artifact.ts";

const HTTP_METHODS = ["get", "post", "patch", "delete", "put"] as const;

Expand Down Expand Up @@ -70,5 +74,8 @@ ${lines.join("\n")}
];
`;

writeFileSync(outputPath, output);
console.log(`Wrote ${outputPath} (${operations.length} operations)`);
const mode = parseGeneratedArtifactMode(process.argv.slice(2));
updateOrCheckGeneratedArtifact({ outputPath, content: output, mode });
console.log(
`${mode === "check" ? "Checked" : "Wrote"} ${outputPath} (${operations.length} operations)`,
);
16 changes: 11 additions & 5 deletions cli/scripts/generate-openapi-types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import openapiTS, { astToString } from "openapi-typescript";
import { readFileSync, writeFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
parseGeneratedArtifactMode,
updateOrCheckGeneratedArtifact,
} from "@/../scripts/generated-artifact.ts";

const specPath = join(import.meta.dir, "../openapi/openapi.yaml");
const outputPath = join(import.meta.dir, "../src/generated/openapi-types.ts");

const spec = readFileSync(specPath, "utf8");
const ast = await openapiTS(spec);
const types = astToString(ast);
writeFileSync(
const mode = parseGeneratedArtifactMode(process.argv.slice(2));
updateOrCheckGeneratedArtifact({
outputPath,
`/* AUTO-GENERATED by scripts/generate-openapi-types.ts — do not edit */\n\n${types}\n`,
);
console.log(`Wrote ${outputPath}`);
content: `/* AUTO-GENERATED by scripts/generate-openapi-types.ts — do not edit */\n\n${types}\n`,
mode,
});
console.log(`${mode === "check" ? "Checked" : "Wrote"} ${outputPath}`);
68 changes: 68 additions & 0 deletions cli/scripts/generated-artifact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
parseGeneratedArtifactMode,
updateOrCheckGeneratedArtifact,
} from "@/../scripts/generated-artifact.ts";

const testDirectories: string[] = [];

function createTestArtifactPath(): string {
const directory = mkdtempSync(join(tmpdir(), "altertable-generated-artifact-"));
testDirectories.push(directory);
return join(directory, "artifact.txt");
}

afterEach(() => {
for (const directory of testDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});

describe("generated artifact synchronization", () => {
test("selects check mode explicitly", () => {
expect(parseGeneratedArtifactMode([])).toBe("write");
expect(parseGeneratedArtifactMode(["--check"])).toBe("check");
});

test("writes generated content", () => {
const outputPath = createTestArtifactPath();

updateOrCheckGeneratedArtifact({ outputPath, content: "generated\n", mode: "write" });

expect(readFileSync(outputPath, "utf8")).toBe("generated\n");
});

test("checks current content without writing", () => {
const outputPath = createTestArtifactPath();
writeFileSync(outputPath, "generated\n");

updateOrCheckGeneratedArtifact({ outputPath, content: "generated\n", mode: "check" });

expect(readFileSync(outputPath, "utf8")).toBe("generated\n");
});

test("reports stale and missing output with regeneration guidance", () => {
const stalePath = createTestArtifactPath();
writeFileSync(stalePath, "old\n");

expect(() =>
updateOrCheckGeneratedArtifact({
outputPath: stalePath,
content: "new\n",
mode: "check",
generateCommand: "bun run generate:commands",
}),
).toThrow("Run: bun run generate:commands");

expect(() =>
updateOrCheckGeneratedArtifact({
outputPath: createTestArtifactPath(),
content: "new\n",
mode: "check",
}),
).toThrow("Generated artifact is stale");
});
});
36 changes: 36 additions & 0 deletions cli/scripts/generated-artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFileSync, writeFileSync } from "node:fs";

export type GeneratedArtifactMode = "write" | "check";

type GeneratedArtifactOptions = {
outputPath: string;
content: string;
mode: GeneratedArtifactMode;
generateCommand?: string;
};

export function parseGeneratedArtifactMode(argv: readonly string[]): GeneratedArtifactMode {
return argv.includes("--check") ? "check" : "write";
}

export function updateOrCheckGeneratedArtifact({
outputPath,
content,
mode,
generateCommand = "bun run generate",
}: GeneratedArtifactOptions): void {
if (mode === "write") {
writeFileSync(outputPath, content);
return;
}

let current: string | undefined;
try {
current = readFileSync(outputPath, "utf8");
} catch {
// Missing output is reported as stale with the same remediation.
}
if (current !== content) {
throw new Error(`Generated artifact is stale: ${outputPath}\nRun: ${generateCommand}`);
}
}
11 changes: 0 additions & 11 deletions cli/src/commands/completion/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { defineCommand, type Command } from "@/lib/command.ts";
import { executeCommand } from "@/lib/command-parser.ts";
import { buildMainCommand } from "@/cli.ts";
import { createCompletionCommand } from "@/commands/completion/index.ts";
import { buildCompletionSpec } from "@/commands/completion/lib/spec.ts";
import { createCliRuntime } from "@/lib/runtime.ts";
import { runWithCliRuntime } from "@/test-utils/runtime.ts";

Expand Down Expand Up @@ -256,14 +255,4 @@ describe("completion command", () => {
expect(existsSync(join(home, ".bashrc"))).toBe(false);
expect(visibleTerminalText(output)).toContain("Startup: left unchanged (--no-rc)");
});

test("integration root command top-level count matches registry", async () => {
const spec = await buildCompletionSpec(buildMainCommand());
const output = await runCompletion(buildMainCommand, "bash");
const topLevelCount = spec.subcommands.length;
expect(topLevelCount).toBe(14);
expect(output).toContain("completion");
expect(output).toContain("upgrade");
expect(output).not.toContain(" GET ");
});
});
Loading