@localpulse/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @localpulse/cli
2
+
3
+ CLI for [Local Pulse](https://localpulse.nl) — ingest event posters, search events, and manage credentials.
4
+
5
+ ## Install
6
+
7
+ **Via bunx (zero-install):**
8
+
9
+ ```sh
10
+ bunx @localpulse/cli --help
11
+ ```
12
+
13
+ **Standalone binary:**
14
+
15
+ Download the latest binary for your platform from the [releases page](https://github.com/localpulse/local_pulse/releases), then:
16
+
17
+ ```sh
18
+ chmod +x localpulse
19
+ ./localpulse --help
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```sh
25
+ # Authenticate
26
+ localpulse auth login --token lp_cli_...
27
+
28
+ # Search events
29
+ localpulse search "techno amsterdam" --date weekend --tz Europe/Amsterdam
30
+
31
+ # Ingest a poster (creates a draft for review)
32
+ localpulse ingest poster.jpg --research metadata.json
33
+
34
+ # Ingest directly (skip draft)
35
+ localpulse ingest poster.jpg --research metadata.json --force
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ### `auth login`
41
+
42
+ Authenticate with a CLI token. Get one at [localpulse.nl/dev](https://localpulse.nl/dev).
43
+
44
+ ### `auth logout`
45
+
46
+ Remove stored credentials.
47
+
48
+ ### `ingest <file> --research <payload.json>`
49
+
50
+ Upload an event poster with research metadata. By default creates a draft for review at `localpulse.nl/publish/edit/<id>`. Use `--force` to submit directly. Use `--generate-skeleton` to see the research JSON format.
51
+
52
+ ### `search <query>`
53
+
54
+ Search events by text. Supports `--city`, `--date today|weekend`, `--tz`, `--limit`, `--cursor`.
55
+
56
+ ## Environment variables
57
+
58
+ | Variable | Purpose |
59
+ |----------|---------|
60
+ | `LP_TOKEN` | Override stored token |
61
+ | `LP_API_URL` | Override API URL (default: `https://localpulse.nl`) |
62
+
63
+ ## License
64
+
65
+ MIT
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@localpulse/cli",
3
+ "version": "0.0.1",
4
+ "description": "Local Pulse CLI — ingest event posters, search events, manage credentials",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "packageManager": "bun@1.3.5",
8
+ "bin": {
9
+ "localpulse": "./src/index.ts"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "dev": "bun run src/index.ts",
20
+ "test": "bun test",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "check:help": "bun run src/index.ts --help",
23
+ "build": "bun build --compile src/index.ts --outfile dist/localpulse",
24
+ "build:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/index.ts --outfile dist/localpulse-darwin-arm64",
25
+ "build:darwin-x64": "bun build --compile --target=bun-darwin-x64 src/index.ts --outfile dist/localpulse-darwin-x64",
26
+ "build:linux-x64": "bun build --compile --target=bun-linux-x64 src/index.ts --outfile dist/localpulse-linux-x64",
27
+ "build:all": "bun run build:darwin-arm64 && bun run build:darwin-x64 && bun run build:linux-x64"
28
+ },
29
+ "dependencies": {
30
+ "@sinclair/typebox": "^0.34.48",
31
+ "ajv": "^8.18.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.5.2",
35
+ "typescript": "^5.9.2"
36
+ }
37
+ }
@@ -0,0 +1,137 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+
7
+ import { validateResearchPayload } from "./lib/research-schema";
8
+
9
+ const cwd = new URL("..", import.meta.url).pathname;
10
+
11
+ function runCli(...args: string[]): { exitCode: number; stdout: string; stderr: string } {
12
+ const result = Bun.spawnSync({
13
+ cmd: ["bun", "run", "src/index.ts", ...args],
14
+ cwd,
15
+ stdout: "pipe",
16
+ stderr: "pipe",
17
+ });
18
+
19
+ return {
20
+ exitCode: result.exitCode,
21
+ stdout: new TextDecoder().decode(result.stdout),
22
+ stderr: new TextDecoder().decode(result.stderr),
23
+ };
24
+ }
25
+
26
+ describe("localpulse", () => {
27
+ it("shows root help with expected commands", () => {
28
+ const { exitCode, stdout } = runCli("--help");
29
+ expect(exitCode).toBe(0);
30
+ expect(stdout).toContain("ingest");
31
+ expect(stdout).toContain("search");
32
+ expect(stdout).toContain("auth");
33
+ expect(stdout).not.toMatch(/^\s+directives\s/m);
34
+ expect(stdout).not.toMatch(/^\s+event\s/m);
35
+ expect(stdout).not.toMatch(/^\s+status\s/m);
36
+ expect(stdout).not.toContain("--format");
37
+ expect(stdout).not.toContain("--filter-output");
38
+ });
39
+
40
+ it("shows version", () => {
41
+ const { exitCode, stdout } = runCli("--version");
42
+ expect(exitCode).toBe(0);
43
+ expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
44
+ });
45
+
46
+ it("shows ingest help with research as primary input", () => {
47
+ const { exitCode, stdout } = runCli("ingest", "--help");
48
+ expect(exitCode).toBe(0);
49
+ expect(stdout).toContain("--research");
50
+ expect(stdout).toContain("--generate-skeleton");
51
+ expect(stdout).toContain("--dry-run");
52
+ expect(stdout).not.toContain("--performers");
53
+ expect(stdout).not.toContain("--genre");
54
+ expect(stdout).not.toContain("--socials");
55
+ expect(stdout).not.toContain("--organizer");
56
+ expect(stdout).not.toContain("--price");
57
+ expect(stdout).not.toContain("--batch");
58
+ });
59
+
60
+ it("shows search help with --date instead of --time", () => {
61
+ const { exitCode, stdout } = runCli("search", "--help");
62
+ expect(exitCode).toBe(0);
63
+ expect(stdout).toContain("--date");
64
+ expect(stdout).toContain("--city");
65
+ expect(stdout).not.toContain("--time");
66
+ });
67
+
68
+ it("shows auth help", () => {
69
+ const { exitCode, stdout } = runCli("auth", "--help");
70
+ expect(exitCode).toBe(0);
71
+ expect(stdout).toContain("login");
72
+ expect(stdout).toContain("logout");
73
+ });
74
+
75
+ it("shows auth login help", () => {
76
+ const { exitCode, stdout } = runCli("auth", "login", "--help");
77
+ expect(exitCode).toBe(0);
78
+ expect(stdout).toContain("--token");
79
+ expect(stdout).toContain("--api-url");
80
+ });
81
+
82
+ it("rejects unknown commands", () => {
83
+ const { exitCode, stderr } = runCli("bogus");
84
+ expect(exitCode).toBe(1);
85
+ expect(stderr).toContain("Unknown command: bogus");
86
+ });
87
+
88
+ it("requires --research for ingest", () => {
89
+ const { exitCode, stderr } = runCli("ingest", "poster.jpg");
90
+ expect(exitCode).toBe(1);
91
+ expect(stderr).toContain("--research");
92
+ });
93
+
94
+ it("requires a file argument for ingest", () => {
95
+ const { exitCode, stderr } = runCli("ingest");
96
+ expect(exitCode).toBe(1);
97
+ expect(stderr).toContain("Usage:");
98
+ });
99
+
100
+ it("requires a query for search", () => {
101
+ const { exitCode, stderr } = runCli("search");
102
+ expect(exitCode).toBe(1);
103
+ expect(stderr).toContain("Search query is required");
104
+ });
105
+
106
+ it("outputs valid JSON from --generate-skeleton", () => {
107
+ const { exitCode, stdout } = runCli("ingest", "--generate-skeleton");
108
+ expect(exitCode).toBe(0);
109
+ const skeleton = JSON.parse(stdout);
110
+ expect(() => validateResearchPayload(skeleton)).not.toThrow();
111
+ expect(skeleton.performers).toBeDefined();
112
+ expect(skeleton.event).toBeDefined();
113
+ expect(skeleton.venue).toBeDefined();
114
+ expect(skeleton.organizer).toBeDefined();
115
+ });
116
+
117
+ it("validates research payload from file with --dry-run", async () => {
118
+ const researchFile = join(tmpdir(), `lp-test-${Date.now()}.json`);
119
+ await writeFile(researchFile, JSON.stringify({
120
+ performers: [{ name: "DJ Nobu", genre: "techno" }],
121
+ event: { title: "Test Night" },
122
+ }));
123
+
124
+ const { exitCode, stdout } = runCli("ingest", "../../README.md", "--research", researchFile, "--dry-run");
125
+ expect(exitCode).toBe(0);
126
+ expect(stdout).toContain("Dry run passed");
127
+ });
128
+
129
+ it("rejects invalid research payload", async () => {
130
+ const researchFile = join(tmpdir(), `lp-test-${Date.now()}.json`);
131
+ await writeFile(researchFile, JSON.stringify({ performers: "not-an-array" }));
132
+
133
+ const { exitCode, stderr } = runCli("ingest", "../../README.md", "--research", researchFile, "--dry-run");
134
+ expect(exitCode).toBe(1);
135
+ expect(stderr).toContain("Invalid research payload");
136
+ });
137
+ });