Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ jobs:
run: npm run lint:knip
- name: Oxlint
run: npm run lint:ox
- name: Documented npm commands exist
run: npm run lint:docs

core-postgres:
name: Core Postgres tests
Expand Down
21 changes: 0 additions & 21 deletions fly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,27 +177,6 @@ For resident X tooling use the same prefix for native tool env, for example

## Smoke test

```bash
FLY_API_TOKEN="$(fly tokens create deploy -a "$FLY_SANDBOX_APP_NAME")" npm run smoke:fly
```

For image-resident X helper readiness:

```bash
FLY_API_TOKEN=... npm run smoke:x
```

This verifies `x-api` is on PATH and reports `missing_auth=auth_missing` when no
resident X token is installed. Add `X_SMOKE_REQUIRE_AUTH=1` after configuring
`X_BEARER_TOKEN` / `X_ACCESS_TOKEN`, or `X_SMOKE_REQUIRE_FIREHOSE=1` when a vendored
`x-firehose` binary should be present.

The smoke test uses a timestamped personal smoke-test scope, writes
workspace and resident-home state, backs it up, deletes the Fly machine, recreates it,
restores the backup, verifies `.aws/*` stayed excluded, then deletes the smoke
machine. Add `SNAPSHOT_STORE=s3 S3_BUCKET=...` to exercise the real S3 object store;
without those vars it uses the same backup-store code over an in-memory blob store.

For GitHub/GitLab resident CLI readiness:

```bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"lint": "eslint .",
"lint:ox": "oxlint --deny-warnings src plugins scripts cli test",
"dev-instance:doctor": "bash scripts/dev-instance.sh doctor",
"lint:knip": "knip"
"lint:knip": "knip",
"lint:docs": "node scripts/check-doc-npm-scripts.mjs"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.3.211",
Expand Down
76 changes: 76 additions & 0 deletions scripts/check-doc-npm-scripts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname } from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";

const REPO_ROOT = new URL("../", import.meta.url);
const DOC_COMMAND = /npm run ([A-Za-z][\w:.-]*)/g;

function trackedFiles(pattern) {
const out = execFileSync("git", ["ls-files", pattern], { cwd: REPO_ROOT, encoding: "utf8" });
return out.split("\n").filter(Boolean);
}

function definedScripts() {
const byPackage = new Map();
for (const file of trackedFiles("*package.json")) {
let manifest;
try {
manifest = JSON.parse(readFileSync(new URL(file, REPO_ROOT), "utf8"));
} catch (error) {
throw new Error(`${file} is not valid JSON`, { cause: error });
}
byPackage.set(dirname(file), Object.keys(manifest.scripts ?? {}));
}
if (byPackage.size === 0) throw new Error("No package.json files are tracked — cannot resolve documented commands");
return byPackage;
}

function documentedCommands() {
const references = [];
for (const file of trackedFiles("*.md")) {
const lines = readFileSync(new URL(file, REPO_ROOT), "utf8").split("\n");
for (const [index, line] of lines.entries()) {
for (const match of line.matchAll(DOC_COMMAND)) {
references.push({ script: match[1], where: `${file}:${index + 1}` });
}
}
}
return references;
}

function main() {
const { values: flags } = parseArgs({ options: { list: { type: "boolean" } } });
const byPackage = definedScripts();
const known = new Set([...byPackage.values()].flat());
const references = documentedCommands();

if (flags.list) {
for (const { script, where } of references) {
console.log(`${known.has(script) ? "ok " : "unknown"} npm run ${script} ${where}`);
}
return;
}

const unknown = references.filter(({ script }) => !known.has(script));
if (unknown.length > 0) {
console.error("Documented npm commands that no package.json defines:\n");
for (const { script, where } of unknown) console.error(` ${where}: npm run ${script}`);
console.error("\nEither add the script, correct the docs, or drop the command from the docs.");
throw new Error(`${unknown.length} documented npm command(s) do not exist`);
}

const scripts = new Set(references.map((r) => r.script));
console.log(
`Checked ${references.length} documented npm command(s) (${scripts.size} distinct) across ${byPackage.size} packages — all defined.`,
);
}

try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}