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
9 changes: 3 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ Altertable CLI — a TypeScript/Bun command-line tool for querying and managing
| ---------------------- | --------------------------------------------------------- |
| `cli/src/` | CLI commands, HTTP clients, formatting, config |
| `cli/src/**/*.test.ts` | Colocated Bun unit tests for CLI logic |
| `data-app/runtime/` | Shared data app package, contracts, components, unit tests |
| `data-app/starter/` | Runnable getting-started app copied by `app create` |
| `data-app/tests/` | Browser tests and fixtures for the starter and runtime |
| `data-app/tests/` | Browser tests and fixtures consuming the published package |
| `tests/` | Black-box end-user CLI tests run through `bin/altertable` |
| `specs/` | Client API specs (submodule — read-only from this repo) |
| `bin/altertable` | Dev launcher — do not edit |

For data app runtime, starter, distribution, and browser work, follow [data-app/AGENTS.md](data-app/AGENTS.md).
For data app starter, distribution, and browser work, follow [data-app/AGENTS.md](data-app/AGENTS.md).

## Start here

Expand All @@ -33,13 +32,11 @@ git submodule update --init --recursive # first checkout only
For data app changes, run the focused checks first from the repository root:

```fish
bun run --cwd data-app/runtime typecheck
bun run --cwd data-app/runtime test
bun run --cwd data-app/starter build
bun run --cwd cli data-app:test:browser
```

The starter build requires its local runtime; see [data-app/README.md](data-app/README.md)
The starter build requires its published package dependencies; see [data-app/README.md](data-app/README.md)
for setup and browser prerequisites. Run `bun run --cwd cli data-app:check` to check all
three data app projects together. Use `./scripts/verify.sh --quick` after focused checks,
then default `./scripts/verify.sh` before opening a PR.
Expand Down
6 changes: 2 additions & 4 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ altertable app create|dev|build|check|upgrade
- `dev` — Preview a data app locally with lakehouse access.
- `build` — Typecheck and build a data app without Altertable credentials.
- `check` — Validate a data app's format, lint, types, contracts, build, and client credential boundary.
- `upgrade` — Update an unmodified data app runtime to the CLI's current version.
- `upgrade` — Update the data app package to the CLI's tested version.

**Examples**

Expand Down Expand Up @@ -484,14 +484,12 @@ altertable app dev [options]
| --- | --- |
| `--dir <DIR>` | App directory (default: current directory). |
| `--port <PORT>` | Local dev server port (1–65535; default: app setting). |
| `--watch-runtime` | Upgrade generated runtime on source changes and restart preview. |

**Examples**

```bash
altertable app dev
altertable app dev --port 3022
altertable app dev --watch-runtime
altertable --profile staging app dev --dir ./my-app
```

Expand Down Expand Up @@ -544,7 +542,7 @@ altertable app check --lakehouse

#### `altertable app upgrade`

Update an unmodified data app runtime to the CLI's current version.
Update the data app package to the CLI's tested version.

**Usage**

Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,4 @@ When bumping the `specs/` submodule, extend the mapped tests before merge.

## Data apps

The runnable starter and shared runtime live in [`data-app/`](data-app/README.md). That guide covers source ownership, the copy allowlist, CLI embedding, runtime upgrades, and browser tests.
The runnable starter and package-consumer browser tests live in [`data-app/`](data-app/README.md). That guide covers starter embedding, npm package pins, package upgrades, and verification. Runtime development and releases belong to [the Data App repository](https://github.com/altertable-ai/data-app).
13 changes: 1 addition & 12 deletions cli-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,22 +897,11 @@
"repeatable": false,
"scope": "command",
"values": []
},
{
"name": "watch-runtime",
"aliases": [],
"type": "boolean",
"description": "Upgrade generated runtime on source changes and restart preview.",
"required": false,
"repeatable": false,
"scope": "command",
"values": []
}
],
"examples": [
"altertable app dev",
"altertable app dev --port 3022",
"altertable app dev --watch-runtime",
"altertable --profile staging app dev --dir ./my-app"
],
"subcommands": []
Expand Down Expand Up @@ -984,7 +973,7 @@
{
"id": "altertable-app-upgrade",
"command": "altertable app upgrade",
"description": "Update an unmodified data app runtime to the CLI's current version.",
"description": "Update the data app package to the CLI's tested version.",
"usage": [
"altertable app upgrade [options]"
],
Expand Down
5 changes: 2 additions & 3 deletions cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ bun test "$PWD"/tests/integration.e2e.ts

## Conventions

- For generated app authoring, edit [the starter app guide](../data-app/starter/AGENTS.md). Keep it a task router into focused authoring guides and the runtime API map. Types describe props; reserve JSDoc for constraints, ownership, security boundaries, and surprising behavior.
- When changing a runtime primitive, preserve semantic ownership: layout spacing belongs to `AppLayout`, `Stack`, and `TabPanels`; filters and date bounds belong to their variable and control contracts; request states belong to `DataSection`. Add a prop or slot when app authors repeatedly need the same custom wrapper.
- Add recurring icons to `../data-app/runtime/src/react/ui/icons.ts` with a semantic name and fixed optical size. Keep toolbar and presentation button variants explicit in their owning components. Verify the generated app in both themes and at desktop and phone widths.
- For generated app authoring, edit [the starter app guide](../data-app/starter/AGENTS.md). Keep it a task router into focused authoring guides and the installed package docs. Types describe props; reserve JSDoc for constraints, ownership, security boundaries, and surprising behavior.
- Runtime contracts and UI components are maintained in [the Data App repository](https://github.com/altertable-ai/data-app). This repository owns the starter and its integration with CLI commands.

- Declare and export each command immediately after its imports; keep supporting helpers and types below it.
- Import command types and `defineArgs` from `src/lib/command.ts`; its metadata drives parsing, help, completion, and generated documentation.
Expand Down
1 change: 0 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"pack:check": "bun run build && bun pm pack --dry-run",
"knip": "knip --no-config-hints",
"knip:production": "knip --production --no-config-hints",
"data-app:setup": "bun run scripts/package-data-app.ts --setup",
"data-app:check": "bun run scripts/check-data-app.ts",
"data-app:test:browser": "cd ../data-app/tests && bun run test"
},
Expand Down
10 changes: 2 additions & 8 deletions cli/scripts/check-data-app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { join } from "node:path";
import { dataAppDirectory } from "@/commands/app/lib/distribution.ts";
import { setupDataApp } from "@/../scripts/package-data-app.ts";

await setupDataApp();
for (const project of ["runtime", "starter", "tests"]) {
for (const project of ["starter", "tests"]) {
const commands = [
["install", "--frozen-lockfile"],
...["typecheck", "lint", "format:check"].map((name) => ["run", name]),
...(project === "runtime"
? [["test", "tests"]]
: project === "starter"
? [["run", "build"]]
: []),
...(project === "starter" ? [["run", "build"]] : []),
];
for (const args of commands) {
const child = Bun.spawn([process.execPath, ...args], {
Expand Down
37 changes: 3 additions & 34 deletions cli/scripts/package-data-app.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { BunPlugin } from "bun";
import { upgradeApp } from "@/commands/app/upgrade.ts";
import {
dataAppDirectory,
readDataAppPayload,
runtimeIntegrity,
runtimePath,
} from "@/commands/app/lib/distribution.ts";
import { readDataAppPayload } from "@/commands/app/lib/distribution.ts";

/** Both npm and native releases embed the same deterministic source payload. */
/** Both npm and native releases embed the same deterministic starter template. */
export function dataAppPlugin(): BunPlugin {
return {
name: "data-app-payload",
name: "data-app-starter",
setup(build) {
build.onLoad({ filter: /[/\\]commands[/\\]app[/\\]lib[/\\]payload\.ts$/ }, async () => ({
contents: `export const dataAppPayload = ${JSON.stringify(await readDataAppPayload())};`,
Expand All @@ -21,26 +13,3 @@ export function dataAppPlugin(): BunPlugin {
},
};
}

export async function setupDataApp(): Promise<void> {
const payload = await readDataAppPayload();
const directory = join(dataAppDirectory, "starter", runtimePath);
if (await Bun.file(join(directory, "integrity.json")).exists()) {
await upgradeApp(join(dataAppDirectory, "starter"), { runtimeFiles: payload.runtime });
return;
}
for (const [name, content] of Object.entries(payload.runtime)) {
await mkdir(dirname(join(directory, name)), { recursive: true });
await writeFile(join(directory, name), content);
}
await writeFile(
join(directory, "integrity.json"),
`${JSON.stringify(runtimeIntegrity(payload.runtime), null, 2)}\n`,
);
}

if (import.meta.main) {
if (!Bun.argv.includes("--setup"))
throw new Error("Usage: bun run scripts/package-data-app.ts --setup");
await setupDataApp();
}
17 changes: 14 additions & 3 deletions cli/scripts/smoke-data-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

/** Exercise the shipped CLI outside the checkout, including its embedded app sources. */
/** Exercise the shipped CLI outside the checkout, including its embedded starter and published runtime. */
export async function smokeDataApp(command: string[], scaffoldOnly = false): Promise<void> {
const directory = await mkdtemp(join(tmpdir(), "altertable-packaged-app-"));
const app = join(directory, "app");
Expand All @@ -24,10 +24,21 @@ export async function smokeDataApp(command: string[], scaffoldOnly = false): Pro
[...command, "app", "create", "package-smoke", "--dir", app, "--without-profile"],
directory,
);
if (!(await Bun.file(join(app, ".altertable/runtime/src/server/index.ts")).exists()))
throw new Error("Packaged runtime is missing");
const manifest = await Bun.file(join(app, "package.json")).json();
if (!/^\d+\.\d+\.\d+$/.test(manifest.dependencies?.["@altertable/data-app"] ?? ""))
throw new Error("Packaged starter must pin a published runtime");
if (await Bun.file(join(app, ".altertable/runtime/package.json")).exists())
throw new Error("Packaged starter must not vendor the runtime");
if (scaffoldOnly) return;
await run([process.execPath, "install", "--frozen-lockfile", "--ignore-scripts"], app);
await run(
[
process.execPath,
"-e",
'import { createDataHandler } from "@altertable/data-app/server"; import { localLakehouse } from "@altertable/data-app/server/bun"; if (typeof createDataHandler !== "function" || typeof localLakehouse !== "function") process.exit(1);',
],
app,
);
await run([...command, "app", "check", "--dir", app], directory);
} finally {
await rm(directory, { recursive: true, force: true });
Expand Down
81 changes: 69 additions & 12 deletions cli/src/commands/app/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ let directory: string;

beforeEach(() => {
directory = mkdtempSync(join(tmpdir(), "altertable-app-check-"));
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
writeFileSync(
join(directory, "node_modules/@altertable/data-app/package.json"),
JSON.stringify({
name: "@altertable/data-app",
type: "module",
exports: {
"./appearance": "./dist/core/appearance.js",
"./server": "./dist/server/index.js",
"./server/bun": "./dist/server/local.js",
},
}),
);
});
afterEach(() => {
rmSync(directory, { recursive: true, force: true });
Expand All @@ -16,8 +30,10 @@ afterEach(() => {
describe("data app contract", () => {
test("rejects unbounded or unchecked operations in the app process", async () => {
mkdirSync(join(directory, "src"));
mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), {
recursive: true,
});
writeFileSync(
join(directory, "app.json"),
JSON.stringify({
Expand All @@ -26,7 +42,7 @@ describe("data app contract", () => {
}),
);
writeFileSync(
join(directory, ".altertable/runtime/src/core/appearance.ts"),
join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
Expand Down Expand Up @@ -85,11 +101,11 @@ describe("data app contract", () => {

test("browser bundles reject value imports of operations but allow type imports", async () => {
mkdirSync(join(directory, "src"));
mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
writeFileSync(join(directory, "app.json"), JSON.stringify({ schemaVersion: 1, title: "Test" }));
writeFileSync(
join(directory, ".altertable/runtime/src/core/appearance.ts"),
join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
Expand All @@ -113,24 +129,24 @@ test("browser bundles reject value imports of operations but allow type imports"
await checkAppProject(directory);
});

test("lakehouse checks execute every declared input through the current runtime layout", async () => {
test("lakehouse checks execute every declared input through public package exports", async () => {
mkdirSync(join(directory, "src"));
mkdirSync(join(directory, ".altertable/runtime/src/core"), { recursive: true });
mkdirSync(join(directory, ".altertable/runtime/src/server"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/core"), { recursive: true });
mkdirSync(join(directory, "node_modules/@altertable/data-app/dist/server"), { recursive: true });
writeFileSync(
join(directory, "app.json"),
JSON.stringify({ schemaVersion: 1, title: "Live check" }),
);
writeFileSync(
join(directory, ".altertable/runtime/src/core/appearance.ts"),
join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
join(directory, ".altertable/runtime/src/server/local.ts"),
join(directory, "node_modules/@altertable/data-app/dist/server/local.js"),
"export function localLakehouse() { return {}; }",
);
writeFileSync(
join(directory, ".altertable/runtime/src/server/index.ts"),
join(directory, "node_modules/@altertable/data-app/dist/server/index.js"),
`
export function createDataHandler(operations) {
return async (request) => {
Expand All @@ -157,3 +173,44 @@ test("lakehouse checks execute every declared input through the current runtime
await checkAppProject(directory, {});
expect(readFileSync(join(directory, "executed.txt"), "utf8")).toBe("12");
});

test.each(["@altertable/data-app/server", "@altertable/data-app/server/bun"])(
"browser bundles reject the published %s entry even without credential strings",
async (entry) => {
mkdirSync(join(directory, "src"));
writeFileSync(
join(directory, "app.json"),
JSON.stringify({ schemaVersion: 1, title: "Boundary" }),
);
writeFileSync(
join(directory, "node_modules/@altertable/data-app/dist/core/appearance.js"),
"export function parseAppearance() {}",
);
writeFileSync(
join(directory, "node_modules/@altertable/data-app/dist/server/index.js"),
"export const secretFreeHandler = 1;",
);
writeFileSync(
join(directory, "node_modules/@altertable/data-app/dist/server/local.js"),
"export const secretFreeHandler = 1;",
);
writeFileSync(
join(directory, "src/operations.ts"),
"export const operations = { test: { checks: [{}], input: v => v, output: v => v, run: async () => ({}), policy: { maxQueryRows: 1, maxDurationMs: 1000 } } };",
);
writeFileSync(
join(directory, "src/index.html"),
'<script type="module" src="./main.ts"></script>',
);
writeFileSync(
join(directory, "src/main.ts"),
`import { secretFreeHandler } from "${entry}"; console.log(secretFreeHandler);`,
);
expect(checkAppProject(directory)).rejects.toThrow("Data app validation failed.");
writeFileSync(
join(directory, "src/main.ts"),
`import type { secretFreeHandler } from "${entry}"; console.log("browser safe");`,
);
await checkAppProject(directory);
},
);
15 changes: 3 additions & 12 deletions cli/src/commands/app/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
requireAppScripts,
runAppCommand,
} from "@/commands/app/lib/run.ts";
import { currentRuntimeIntegrity, installedRuntimeIntegrity } from "@/commands/app/lib/runtime.ts";
import { requirePublishedDataApp } from "@/commands/app/lib/package.ts";
import projectCheckScript from "@/commands/app/lib/project-check.js.txt";

type AppManifest = {
Expand Down Expand Up @@ -42,17 +42,8 @@ export const appCheckCommand = defineCommand({
requireAppScripts(directory, ["format:check", "lint", "typecheck", "build"]);
const manifest = await readManifest(directory);
if (args.lakehouse) await checkAppScope(directory, execution.profile);
const installed = await installedRuntimeIntegrity(directory);
const current = currentRuntimeIntegrity();
if (
installed.version !== current.version ||
Object.entries(current.sha256).some(([name, checksum]) => installed.sha256[name] !== checksum)
) {
throw new ConfigurationError(
`Data app runtime ${installed.version} is outdated. Run \`altertable app upgrade\`.`,
);
}
if ((await runAppCommand("install", directory)) !== 0) return { exitCode: EXIT_GENERIC };
await requirePublishedDataApp(directory);
if ((await runAppCommand("install-frozen", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("format:check", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("lint", directory)) !== 0) return { exitCode: EXIT_GENERIC };
if ((await runAppCommand("typecheck", directory)) !== 0) return { exitCode: EXIT_GENERIC };
Expand Down
Loading
Loading