Skip to content

Commit 87f38b3

Browse files
committed
Expand CLI public API commands and release tooling
1 parent 71f2c2e commit 87f38b3

8 files changed

Lines changed: 906 additions & 113 deletions

File tree

README.md

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
# nylio-cli
22

3-
Open-source CLI for the Nylio public API.
4-
5-
Related docs:
6-
7-
- npm package: [nylio-cli](https://www.npmjs.com/package/nylio-cli)
8-
- monorepo integration note: [`function/docs/nylio-cli.md`](https://github.com/prodev1000/function/blob/main/docs/nylio-cli.md)
9-
10-
## License
11-
12-
MIT
3+
CLI for the Nylio public API.
134

145
## Install
156

167
```bash
178
npm install -g nylio-cli
189
```
1910

11+
macOS users can also install a self-contained Bun-compiled binary via Homebrew once a tap formula is published.
12+
2013
## Commands
2114

2215
```bash
@@ -27,67 +20,102 @@ nylio login --print-url
2720
nylio auth status
2821
nylio whoami
2922
nylio workspaces list
23+
nylio documents create --title "Draft"
3024
nylio documents list --limit 10 --offset 0
3125
nylio documents get <document-id-or-url>
3226
nylio documents edit --document <document-id-or-url> --old-string "<oldString>" --new-string "<newString>"
3327
cat replacement.txt | nylio documents edit --document <document-id-or-url> --old-string "<oldString>" --new-string-stdin
3428
nylio documents replace --document <document-id-or-url> --markdown "<full-enhanced-markdown-body>"
3529
cat body.md | nylio documents replace --document <document-id-or-url> --stdin
30+
nylio documents export <document-id-or-url> --format markdown
3631
nylio search "query"
3732
```
3833

39-
Every command and subcommand supports `--help` with targeted examples.
34+
Each command and subcommand now supports `--help` with targeted examples, for example:
35+
36+
```bash
37+
nylio documents get --help
38+
nylio documents replace --help
39+
```
4040

4141
## Build
4242

4343
```bash
44-
npm install
45-
npm run ci
46-
npm run pack:dry-run
44+
bun run --cwd packages/nylio-cli check:publish-safety
45+
bun run --cwd packages/nylio-cli typecheck
46+
bun run --cwd packages/nylio-cli build
47+
bun run --cwd packages/nylio-cli pack:dry-run
4748
```
4849

49-
The publish safety check rejects:
50+
The publish safety check fails if the CLI imports server-only code, workspace-internal aliases, or files outside `packages/nylio-cli/src`.
5051

51-
- imports from server-only or monorepo-internal modules
52-
- `process.env` usage inside `src`
52+
## Publish
5353

54-
## CI
54+
GitHub Actions publishes the package from the `Publish CLI` workflow when you push a tag in the format `nylio-cli-v<version>`, for example:
5555

56-
GitHub Actions runs:
56+
```bash
57+
git tag nylio-cli-v0.1.0
58+
git push origin nylio-cli-v0.1.0
59+
```
5760

58-
- `lint`
59-
- `typecheck`
60-
- `build`
61-
- `pack:dry-run`
61+
That same workflow also creates a GitHub Release for the tag and uploads:
6262

63-
The workflows run through Turborepo and persist the local `.turbo` cache between CI runs.
63+
- `nylio-cli-<version>-darwin-arm64.tar.gz`
64+
- `nylio-cli-<version>-darwin-x64.tar.gz`
65+
- `SHA256SUMS`
66+
- `nylio.rb` for Homebrew
6467

65-
## Publish
68+
GitHub Releases are the distribution point for the self-contained macOS binaries. Homebrew should point at those release assets.
69+
70+
Manual local publish is also wired:
71+
72+
```bash
73+
bun run --cwd packages/nylio-cli publish:npm
74+
```
6675

67-
Push a tag that matches the package version:
76+
To build the macOS release artifacts locally:
6877

6978
```bash
70-
git tag v0.1.0
71-
git push origin v0.1.0
79+
bun run --cwd packages/nylio-cli release:artifacts -- --repo prodev1000/function --tag nylio-cli-v0.1.0
7280
```
7381

74-
The publish workflow verifies the tag, runs the Turbo pipeline, and publishes to npm.
82+
That writes the archives, checksums, and a generated Homebrew formula to `packages/nylio-cli/dist/release`.
7583

76-
For local publishing, use:
84+
## Homebrew
85+
86+
The generated `nylio.rb` formula is intended for a tap repo and references the GitHub Release assets for the tagged version.
87+
88+
Recommended setup:
89+
90+
- keep `npm` as the cross-platform install path
91+
- publish macOS Bun binaries to GitHub Releases
92+
- maintain a small Homebrew tap repo that contains the generated `nylio.rb`
93+
94+
Once a tap exists, installs look like:
7795

7896
```bash
79-
npm run publish:npm
97+
brew tap <owner>/<tap-repo>
98+
brew install nylio
8099
```
81100

82101
## Configuration
83102

84103
- `--api-base-url <url>` overrides the default API origin
104+
- default output is compact plain text
85105
- `--json` renders machine-readable JSON output
106+
- `BETTER_AUTH_URL` sets the default API origin when `--api-base-url` is not passed
107+
- `NYLIO_OAUTH_CLI_CLIENT_ID` overrides the OAuth client id for custom environments
108+
109+
## Agent-friendly usage
86110

87-
The default API origin is `https://api.nylio.app`.
111+
- Unknown flags fail fast instead of being ignored.
112+
- Write commands accept explicit flags instead of requiring positional-only input.
113+
- `nylio documents replace --stdin` reads the replacement body from stdin.
114+
- `nylio documents edit --old-string-stdin` and `--new-string-stdin` let you pipe one side of the edit.
115+
- `--dry-run` previews `documents edit` and `documents replace` requests without sending them.
88116

89117
## Auth
90118

91-
The CLI uses OAuth 2.1 Authorization Code + PKCE and stores user tokens locally at `~/.config/nylio/auth.json`.
119+
The CLI uses OAuth 2.1 Authorization Code + PKCE against the Nylio Better Auth issuer and stores user tokens locally at `~/.config/nylio/auth.json`.
92120

93121
Use `nylio login --print-url` if you do not want the CLI to open a browser automatically.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
"ci": "turbo run lint typecheck build",
2929
"check:publish-safety": "node ./scripts/check-publish-safety.mjs",
3030
"build": "tsc -p tsconfig.build.json && chmod +x dist/index.js",
31+
"build:binary:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 --outfile dist/nylio-darwin-arm64 ./src/index.ts",
32+
"build:binary:darwin-x64": "bun build --compile --target=bun-darwin-x64 --outfile dist/nylio-darwin-x64 ./src/index.ts",
3133
"lint": "biome check package.json README.md src scripts .github/workflows",
3234
"pack:dry-run": "npm pack --dry-run",
35+
"release:artifacts": "node ./scripts/build-release-artifacts.mjs",
3336
"prepack": "turbo run check:publish-safety build",
3437
"prepublishOnly": "turbo run check:publish-safety lint typecheck build && npm pack --dry-run",
35-
"publish:npm": "npm publish",
38+
"publish:npm": "npm publish --provenance",
3639
"typecheck": "tsc --noEmit"
3740
},
3841
"devDependencies": {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env node
2+
3+
import { spawnSync } from "node:child_process";
4+
import { createHash } from "node:crypto";
5+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6+
import path from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
const packageDir = path.resolve(__dirname, "..");
12+
const packageJsonPath = path.join(packageDir, "package.json");
13+
const releaseDir = path.join(packageDir, "dist", "release");
14+
const stageDir = path.join(releaseDir, "stage");
15+
const binaryName = "nylio";
16+
17+
const parseArgs = (argv) => {
18+
const values = {
19+
repo: process.env.GITHUB_REPOSITORY ?? "",
20+
tag: process.env.GITHUB_REF_NAME ?? "",
21+
};
22+
23+
for (let index = 0; index < argv.length; index += 1) {
24+
const arg = argv[index];
25+
if (arg === "--repo") {
26+
values.repo = argv[index + 1] ?? "";
27+
index += 1;
28+
continue;
29+
}
30+
31+
if (arg === "--tag") {
32+
values.tag = argv[index + 1] ?? "";
33+
index += 1;
34+
}
35+
}
36+
37+
return values;
38+
};
39+
40+
const run = (command, args, cwd) => {
41+
const result = spawnSync(command, args, {
42+
cwd,
43+
stdio: "inherit",
44+
env: process.env,
45+
});
46+
47+
if (result.status !== 0) {
48+
process.exit(result.status ?? 1);
49+
}
50+
};
51+
52+
const sha256File = async (filePath) => {
53+
const content = await readFile(filePath);
54+
return createHash("sha256").update(content).digest("hex");
55+
};
56+
57+
const buildFormula = ({ repo, version, tag, arm64, x64 }) => `class Nylio < Formula
58+
desc "CLI for the Nylio public API"
59+
homepage "https://github.com/${repo}"
60+
version "${version}"
61+
62+
on_macos do
63+
if Hardware::CPU.arm?
64+
url "https://github.com/${repo}/releases/download/${tag}/${arm64.assetName}"
65+
sha256 "${arm64.sha256}"
66+
else
67+
url "https://github.com/${repo}/releases/download/${tag}/${x64.assetName}"
68+
sha256 "${x64.sha256}"
69+
end
70+
end
71+
72+
def install
73+
bin.install "${binaryName}"
74+
end
75+
76+
test do
77+
output = shell_output("#{bin}/${binaryName} --help")
78+
assert_match "CLI for the Nylio public API.", output
79+
end
80+
end
81+
`;
82+
83+
const main = async () => {
84+
const { repo, tag } = parseArgs(process.argv.slice(2));
85+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
86+
const version = packageJson.version;
87+
const expectedTag = `nylio-cli-v${version}`;
88+
89+
if (!repo) {
90+
throw new Error("Missing --repo <owner/repo> or GITHUB_REPOSITORY.");
91+
}
92+
93+
if (!tag) {
94+
throw new Error("Missing --tag <release-tag> or GITHUB_REF_NAME.");
95+
}
96+
97+
if (tag !== expectedTag) {
98+
throw new Error(
99+
`Release tag ${tag} does not match package version ${version}. Expected ${expectedTag}.`,
100+
);
101+
}
102+
103+
await rm(releaseDir, { force: true, recursive: true });
104+
await mkdir(stageDir, { recursive: true });
105+
106+
const targets = [
107+
{ bunTarget: "bun-darwin-arm64", suffix: "darwin-arm64" },
108+
{ bunTarget: "bun-darwin-x64", suffix: "darwin-x64" },
109+
];
110+
111+
const artifacts = [];
112+
113+
for (const target of targets) {
114+
const stageName = `nylio-cli-${version}-${target.suffix}`;
115+
const stagePath = path.join(stageDir, stageName);
116+
const outputPath = path.join(stagePath, binaryName);
117+
const archiveName = `${stageName}.tar.gz`;
118+
const archivePath = path.join(releaseDir, archiveName);
119+
120+
await mkdir(stagePath, { recursive: true });
121+
122+
run(
123+
"bun",
124+
[
125+
"build",
126+
"--compile",
127+
`--target=${target.bunTarget}`,
128+
`--outfile=${outputPath}`,
129+
"./src/index.ts",
130+
],
131+
packageDir,
132+
);
133+
134+
await chmod(outputPath, 0o755);
135+
136+
run("tar", ["-czf", archivePath, "-C", stageDir, stageName], packageDir);
137+
138+
artifacts.push({
139+
target: target.suffix,
140+
assetName: archiveName,
141+
assetPath: archivePath,
142+
sha256: await sha256File(archivePath),
143+
});
144+
}
145+
146+
const checksums = artifacts
147+
.map((artifact) => `${artifact.sha256} ${artifact.assetName}`)
148+
.join("\n");
149+
150+
await writeFile(path.join(releaseDir, "SHA256SUMS"), `${checksums}\n`);
151+
152+
const arm64 = artifacts.find((artifact) => artifact.target === "darwin-arm64");
153+
const x64 = artifacts.find((artifact) => artifact.target === "darwin-x64");
154+
155+
if (!arm64 || !x64) {
156+
throw new Error("Missing macOS release artifacts.");
157+
}
158+
159+
const formulaDir = path.join(releaseDir, "homebrew");
160+
await mkdir(formulaDir, { recursive: true });
161+
await writeFile(
162+
path.join(formulaDir, "nylio.rb"),
163+
buildFormula({ repo, version, tag, arm64, x64 }),
164+
);
165+
166+
console.log(`Release artifacts written to ${releaseDir}`);
167+
};
168+
169+
await main().catch((error) => {
170+
console.error(error instanceof Error ? error.message : String(error));
171+
process.exit(1);
172+
});

scripts/check-publish-safety.mjs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const packageDir = path.resolve(__dirname, "..");
77
const srcDir = path.join(packageDir, "src");
88

99
const DISALLOWED_PREFIXES = ["@/", "~/", "server", "server/", "/"];
10-
const ENV_ACCESS_PATTERN = /\bprocess\.env\b/g;
1110

1211
const IMPORT_PATTERN =
1312
/(?:import\s+(?:[^"']+?\s+from\s+)?|export\s+(?:[^"']+?\s+from\s+)?|import\s*\()\s*["']([^"']+)["']/g;
@@ -62,13 +61,6 @@ const main = async () => {
6261
specifier,
6362
});
6463
}
65-
66-
if (ENV_ACCESS_PATTERN.test(source)) {
67-
violations.push({
68-
filePath,
69-
specifier: "process.env",
70-
});
71-
}
7264
}
7365

7466
if (violations.length === 0) {
@@ -77,7 +69,7 @@ const main = async () => {
7769
}
7870

7971
console.error(
80-
"Refusing to publish nylio-cli because it uses environment variables or imports server-only/workspace-internal modules:",
72+
"Refusing to publish nylio-cli because it imports server-only or workspace-internal modules:",
8173
);
8274

8375
for (const violation of violations) {

0 commit comments

Comments
 (0)