Skip to content

Commit 65be6df

Browse files
theahuranori-agent
andauthored
Fix broken npm install (missing dist) + add CI (#2)
## Summary 🤖 Generated with [Nori](https://www.npmjs.com/package/nori-ai) - `nori-slack-cli@0.1.0` on npm shipped without `dist/`, so `npm install -g nori-slack-cli` created a `nori-slack` bin pointing at a missing file. Fix: add `"files": ["dist"]` and `"prepare": "npm run build"` to `package.json`; bump to `0.1.1`. - New `test/packaging.test.ts` black-box test that `npm pack`s (in an isolated tmpdir to avoid racing with `build.test.ts`), installs the tarball into another tmpdir, and runs `nori-slack list-methods --namespace chat` — verifies the install works end-to-end. Runs on every `npm test`. - New `.github/workflows/pr-ci.yaml` + `main-ci.yaml` mirroring `nori-registrar` conventions (kebab-case names, single-entry matrix, `.nvmrc`-driven Node 22). Steps: install → build → test. - Docs updated: `README.md` now documents `npm install -g nori-slack-cli` as the primary install path; `docs.md`/`src/docs.md`/`test/docs.md` corrected for pre-existing stale claims (e.g., that `postbuild` runs `npm link`, which it never did) and new packaging invariants. ## Test Plan - [ ] CI passes on this PR. - [ ] After merge + `npm publish` (manual, from a clean checkout), `npm install -g nori-slack-cli@0.1.1` exposes a working `nori-slack` on PATH. - [ ] `nori-slack list-methods --namespace chat` returns JSON with `chat.postMessage` in the methods array. - [ ] `nori-slack --version` prints `0.1.1`. ## Notes / open questions for review - Action versions pinned at `@v3` to match `nori-registrar` — happy to bump to `@v4` in a follow-up if preferred. - `src/index.ts` has a hardcoded `.version('0.1.1')` duplicate with `package.json`. Left as-is — `test/build.test.ts` already enforces drift detection by asserting `--version` matches `pkg.version`. - Did not unpublish `0.1.0` from npm. Within the 72h window it is possible via `npm unpublish nori-slack-cli@0.1.0` — your call. Share Nori with your team: https://www.npmjs.com/package/nori-skillsets Co-authored-by: Nori <contact@tilework.tech>
1 parent dae545e commit 65be6df

10 files changed

Lines changed: 207 additions & 20 deletions

File tree

.github/workflows/main-ci.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Runs on push to main branch (post-merge)
2+
3+
name: main-ci
4+
5+
on:
6+
push:
7+
branches:
8+
- main
9+
10+
jobs:
11+
ci:
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest]
15+
16+
runs-on: ${{ matrix.os }}
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
21+
- name: Set up root node
22+
uses: actions/setup-node@v3
23+
with:
24+
node-version-file: '.nvmrc'
25+
cache: 'npm'
26+
cache-dependency-path: 'package-lock.json'
27+
28+
- name: Install dependencies
29+
run: npm install
30+
31+
- name: Build
32+
run: npm run build
33+
34+
- name: Run tests
35+
run: npm test
36+
env:
37+
NODE_OPTIONS: --max-old-space-size=4096

.github/workflows/pr-ci.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Runs on pull requests to main branch
2+
3+
name: pr-ci
4+
5+
on:
6+
pull_request:
7+
branches:
8+
- main
9+
10+
jobs:
11+
ci:
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest]
15+
16+
runs-on: ${{ matrix.os }}
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
21+
- name: Set up root node
22+
uses: actions/setup-node@v3
23+
with:
24+
node-version-file: '.nvmrc'
25+
cache: 'npm'
26+
cache-dependency-path: 'package-lock.json'
27+
28+
- name: Install dependencies
29+
run: npm install
30+
31+
- name: Build
32+
run: npm run build
33+
34+
- name: Run tests
35+
run: npm test
36+
env:
37+
NODE_OPTIONS: --max-old-space-size=4096

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ Bolt is built for human developers writing TypeScript. This CLI is built for cod
1212
- **Exhaustive surface.** The agent has access to the full Slack Web API — not a hand-picked subset. Capability boundaries are enforced through **bot token scopes**, not through code.
1313
- **Bot tokens only.** Uses `SLACK_BOT_TOKEN` exclusively. There is no user-OAuth flow because there is no human in the loop.
1414
- **Self-locating errors.** Every error response includes a `source` field with the on-disk path to the CLI, so an agent can read the source code to debug.
15-
- **Distributed as source.** Build it locally; the postbuild step makes `nori-slack` available on your `PATH`.
15+
- **Install from npm.** `npm install -g nori-slack-cli` puts `nori-slack` on your `PATH`. Cloning and building from source is also supported for contributors.
1616

1717
## Install
1818

19+
From npm:
20+
21+
```bash
22+
npm install -g nori-slack-cli
23+
```
24+
25+
From source (for contributors):
26+
1927
```bash
2028
git clone https://github.com/tilework-tech/nori-slack-cli.git
2129
cd nori-slack-cli

docs.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Noridoc: nori-slack-cli
22

3-
Path: @/nori-slack-cli
3+
Path: @/
44

55
### Overview
66
- A TypeScript CLI that exposes the entire Slack Web API as a single command: `nori-slack <method> [--param value ...]`
@@ -11,31 +11,49 @@ Path: @/nori-slack-cli
1111
- Supports `describe <method>` to look up parameter documentation for any Slack API method without requiring a token -- the metadata map covers all methods in `KNOWN_METHODS`, so agents always get full parameter documentation rather than a fallback
1212

1313
### How it fits into the larger codebase
14-
- Lives as a standalone tool under the `nori-integrations` monorepo, in the `slack` worktree
15-
- Intended to be `npm link`ed or installed globally so agents can invoke `nori-slack` from any working directory
14+
- Standalone repository (was originally imported from the `nori-integrations` monorepo and now lives on its own). Distributed via the public npm registry as `nori-slack-cli`
15+
- The canonical install path is `npm install -g nori-slack-cli`, which places the `nori-slack` binary on `PATH`; `npm link` from a local clone is retained for contributors
1616
- Authentication is bot-token-only via `SLACK_BOT_TOKEN` environment variable (no user OAuth flows)
1717
- The CLI is a thin wrapper -- it does not contain business logic, scheduling, or state management; it translates CLI flags into Slack API calls and returns the raw JSON response
18-
- The project spec lives in [spec/APPLICATION-SPEC.md](spec/APPLICATION-SPEC.md)
1918
- The pagination merge logic in [src/paginate.ts](src/paginate.ts) is a pure function decoupled from the Slack SDK -- it operates on any `AsyncIterable` of page objects
19+
- User-facing installation and usage documentation lives in [README.md](README.md)
2020

2121
### Core Implementation
2222
- Entry point is [src/index.ts](src/index.ts), which uses Commander.js with `allowUnknownOption()` so arbitrary `--flag value` pairs pass through without Commander rejecting them
2323
- The dynamic handler has three code paths: `--dry-run` short-circuits after param resolution (no token required, no API call), `--paginate` triggers `WebClient.paginate()` + `mergePages()`, and the default path uses `WebClient.apiCall()`
2424
- Two input modes: CLI flags (`--channel C123 --text "hi"`) and piped JSON via `--json-input`; when both are provided, CLI flags override stdin values
25-
- Two discovery subcommands that do not require `SLACK_BOT_TOKEN`: `list-methods` outputs known method names as JSON (supports `--namespace` filtering and `--descriptions` to include method descriptions), and `describe <method>` returns structured parameter documentation (required params, optional params, pagination support, deprecation notices, and docs URL)
25+
- Two discovery subcommands that do not require `SLACK_BOT_TOKEN`: `list-methods` outputs known method names as JSON (supports `--namespace` filtering and `--descriptions` to include method descriptions), and `describe <method>` returns structured parameter documentation
2626
- `describe` uses [src/method-metadata.ts](src/method-metadata.ts), a hand-curated static map covering every method in `KNOWN_METHODS` -- this is static because `@slack/web-api` erases parameter type information at compile time, so runtime introspection is not possible
2727
- For unknown methods (not in `KNOWN_METHODS`), `getMethodMetadata()` returns a fallback entry with empty params and a generated docs URL, so `describe` never errors; the `known` field in the output distinguishes curated entries from fallbacks
28-
- When an unknown method is used, [src/suggest.ts](src/suggest.ts) provides fuzzy matching via Levenshtein distance against `KNOWN_METHODS`, surfacing up to 3 "Did you mean?" suggestions in both `--dry-run` JSON output and stderr warnings before API calls; suggestions are non-blocking -- unknown methods still proceed to the API
28+
- When an unknown method is used, [src/suggest.ts](src/suggest.ts) provides fuzzy matching via Levenshtein distance against `KNOWN_METHODS`, surfacing "Did you mean?" suggestions; suggestions are non-blocking -- unknown methods still proceed to the API
2929
- Successful API responses and error responses both go to stdout as JSON; errors additionally write a human-readable line to stderr
3030
- Exit codes: `0` for success, `1` for API/token errors, `2` for missing args or invalid stdin JSON
3131

32+
### Packaging and distribution
33+
34+
The published npm artifact is assembled at pack time, not committed to git. The relevant `package.json` fields form a single chain that must stay in sync:
35+
36+
```
37+
dist/ (gitignored)
38+
└─ produced by `prepare: "npm run build"` (runs on npm pack / npm publish / npm install from tarball)
39+
└─ made executable by `postbuild: "chmod +x dist/index.js"`
40+
└─ included in the tarball by `files: ["dist"]`
41+
└─ exposed as `nori-slack` via `bin: { "nori-slack": "./dist/index.js" }`
42+
└─ verified end-to-end by test/packaging.test.ts on every `npm test`
43+
```
44+
45+
- `test/packaging.test.ts` runs `npm pack`, installs the resulting tarball into a tmpdir, and executes the installed `nori-slack` binary to confirm `dist/` actually ships. See [test/docs.md](test/docs.md) for test-level detail
46+
- CI is defined in [.github/workflows/pr-ci.yaml](.github/workflows/pr-ci.yaml) (on pull requests to `main`) and [.github/workflows/main-ci.yaml](.github/workflows/main-ci.yaml) (on push to `main`). Both mirror `nori-registrar` conventions: checkout, `actions/setup-node` reading Node version from [.nvmrc](.nvmrc), `npm install`, `npm run build`, `npm test`
47+
- [.nvmrc](.nvmrc) pins Node 22 to match the `nori-registrar` baseline
48+
3249
### Things to Know
3350
- Flag parsing in [src/parse-args.ts](src/parse-args.ts) converts `--kebab-case` to `snake_case` because the Slack API uses snake_case parameter names
3451
- Type coercion in `coerceValue` handles booleans (`"true"`/`"false"`), numbers (but preserves leading-zero strings like `"007"`), and inline JSON arrays/objects
3552
- A standalone `--flag` with no following value (or followed by another `--flag`) is treated as boolean `true`
3653
- Error formatting in [src/errors.ts](src/errors.ts) maps Slack error codes to actionable suggestions (e.g., `channel_not_found` suggests running `conversations.list`); unknown errors get a generic suggestion pointing to the source directory
3754
- Every error response includes a `source` field with the filesystem path to the CLI, so agents can locate the source code for debugging
3855
- The method metadata in [src/method-metadata.ts](src/method-metadata.ts) marks `files.upload` as deprecated with a pointer to the two-step `files.getUploadURLExternal` + `files.completeUploadExternal` flow
39-
- The `postbuild` script runs `chmod +x` on the output and `npm link` to make the binary available immediately after build
56+
- The CLI version string is currently duplicated: once in [package.json](package.json) `version` and once as a hardcoded argument to Commander's `.version()` call in [src/index.ts](src/index.ts). Both must be bumped together on release
57+
- **Packaging invariant**: anything that changes how the distributed artifact is produced must keep the `prepare` script, the `files` allowlist, `bin`, and [test/packaging.test.ts](test/packaging.test.ts) consistent. Concretely, any future change that removes `prepare`, removes `files`, emits generated code outside `dist/`, or adds a second bin entry needs matching updates in the allowlist and the packaging test -- otherwise `npm install -g nori-slack-cli` silently ships a broken binary (this was the exact `0.1.0` regression)
4058

4159
Created and maintained by Nori.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
{
22
"name": "nori-slack-cli",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "CLI for interacting with the Slack Web API, designed for coding agents",
55
"type": "module",
66
"bin": {
77
"nori-slack": "./dist/index.js"
88
},
9+
"files": [
10+
"dist"
11+
],
912
"scripts": {
1013
"build": "tsc",
1114
"postbuild": "chmod +x dist/index.js",
15+
"prepare": "npm run build",
1216
"test": "vitest run",
1317
"test:watch": "vitest"
1418
},

src/docs.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
# Noridoc: nori-slack-cli/src
1+
# Noridoc: src
22

3-
Path: @/nori-slack-cli/src
3+
Path: @/src
44

55
### Overview
66
- Contains all source modules for the CLI: entry point, argument parsing, error formatting, pagination merging, fuzzy method suggestion, the known-methods catalog, and the method metadata registry
77
- Compiles from `src/` to `dist/` via TypeScript (ES2022 target, Node16 module resolution)
88

99
### How it fits into the larger codebase
10-
- [index.ts](index.ts) is the CLI entry point (shebang `#!/usr/bin/env node`), compiled to `dist/index.js` and exposed as the `nori-slack` binary via `package.json` `bin` field
11-
- [parse-args.ts](parse-args.ts), [errors.ts](errors.ts), and [paginate.ts](paginate.ts) are pure utility modules with no side effects -- they are independently testable and tested in [@/nori-slack-cli/test](../test/)
10+
- [index.ts](index.ts) is the CLI entry point (shebang `#!/usr/bin/env node`), compiled to `dist/index.js` and exposed as the `nori-slack` binary via the `bin` field in [../package.json](../package.json). The compiled `dist/` directory is produced at pack time by the `prepare` script and shipped to the npm registry via the `files` allowlist -- see [../docs.md](../docs.md) for the full packaging chain
11+
- [parse-args.ts](parse-args.ts), [errors.ts](errors.ts), and [paginate.ts](paginate.ts) are pure utility modules with no side effects -- they are independently testable and tested in [@/test](../test/)
1212
- [methods.ts](methods.ts) is a static data file; it is only used by the `list-methods` subcommand and has no effect on which methods the CLI can actually call
1313

1414
### Core Implementation

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const program = new Command();
1919
program
2020
.name('nori-slack')
2121
.description('CLI for the Slack Web API. Designed for coding agents.\n\nUsage: nori-slack <method> [--param value ...]\n\nExamples:\n nori-slack chat.postMessage --channel C123 --text "Hello"\n nori-slack conversations.list --limit 10\n nori-slack api.test --foo bar\n echo \'{"channel":"C123","text":"hi"}\' | nori-slack chat.postMessage --json-input')
22-
.version('0.1.0');
22+
.version('0.1.1');
2323

2424
program
2525
.command('list-methods')

test/docs.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
# Noridoc: nori-slack-cli/test
1+
# Noridoc: test
22

3-
Path: @/nori-slack-cli/test
3+
Path: @/test
44

55
### Overview
6-
- Unit tests for `parseArgs`, `formatError`, `mergePages`, and method metadata coverage, plus integration tests that invoke the CLI as a subprocess
7-
- Uses Vitest as the test runner; integration tests in `cli.test.ts` use `tsx` to run TypeScript source directly, while `build.test.ts` compiles via `tsc` and runs the built `dist/index.js` artifact
6+
- Unit tests for `parseArgs`, `formatError`, `mergePages`, and method metadata coverage, plus integration tests that invoke the CLI as a subprocess, plus an end-to-end packaging test that installs the npm tarball
7+
- Uses Vitest as the test runner; integration tests in `cli.test.ts` use `tsx` to run TypeScript source directly, `build.test.ts` compiles via `tsc` and runs the built `dist/index.js` artifact, and `packaging.test.ts` runs `npm pack` and installs the tarball into a tmpdir
88

99
### How it fits into the larger codebase
10-
- Tests cover the pure utility modules in [@/nori-slack-cli/src](../src/): argument parsing, error formatting, pagination merging, and method metadata
10+
- Tests cover the pure utility modules in [@/src](../src/): argument parsing, error formatting, pagination merging, and method metadata
1111
- Integration tests in [cli.test.ts](cli.test.ts) exercise the full CLI binary by spawning `npx tsx src/index.ts` as a child process, verifying end-to-end behavior including exit codes, stdout JSON structure, and stderr output
12+
- [packaging.test.ts](packaging.test.ts) closes the loop on the npm distribution path documented in [@/docs.md](../docs.md) -- it is the guard against the `0.1.0` regression where `dist/` was missing from the published tarball
13+
- All tests run on every PR and on every push to `main` via the workflows in [@/.github/workflows](../.github/workflows/)
1214
- The test directory is excluded from TypeScript compilation via `tsconfig.json`
1315

1416
### Core Implementation
@@ -45,12 +47,19 @@ Path: @/nori-slack-cli/test
4547

4648
**`build.test.ts`** -- Build verification tests that exercise the compiled output:
4749
- `beforeAll` runs `tsc` once; all tests share the resulting `dist/index.js`
48-
- Uses `node dist/index.js` directly (unlike `cli.test.ts` which uses `npx tsx src/index.ts`), verifying the actual build artifact that `npm link` would expose
50+
- Uses `node dist/index.js` directly (unlike `cli.test.ts` which uses `npx tsx src/index.ts`), verifying the actual build artifact that a global install would expose
4951
- Validates `--version` output, `list-methods` JSON structure, and no-args usage error exit code
5052

53+
**`packaging.test.ts`** -- End-to-end packaging test that validates the npm distribution path:
54+
- `beforeAll` creates two tmpdirs, runs `npm pack` on the project root to produce a `.tgz`, then `npm init -y` + `npm install --no-save <tarball>` in the second tmpdir to simulate a downstream install
55+
- Asserts the installed `node_modules/.bin/nori-slack` binary exists and that running it with `list-methods --namespace chat` returns exit 0 with JSON containing `chat.postMessage`
56+
- Runs on every `npm test` invocation (not gated) so the tarball contents are continuously verified; the `beforeAll` has a 180s timeout because `npm pack` + `npm install` of the tarball is slow
57+
- This test is the enforcement mechanism for the packaging invariant documented in [@/docs.md](../docs.md): if `prepare`, `files`, or `bin` regress, this test fails before a broken version can be published
58+
5159
### Things to Know
5260
- Integration tests make real HTTP calls to Slack's API (with invalid tokens), so they require network access
5361
- The `runCli` helper sets a 10-second timeout to prevent hangs
5462
- Tests intentionally verify structure (JSON shape, field presence, field types) rather than exact string values, making them resilient to Slack API message changes
63+
- `packaging.test.ts` shells out to `npm` and writes into `os.tmpdir()`, so CI runners must have npm available and writable tmp space
5564

5665
Created and maintained by Nori.

test/packaging.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { execFileSync } from 'node:child_process';
3+
import {
4+
mkdtempSync,
5+
rmSync,
6+
readdirSync,
7+
existsSync,
8+
copyFileSync,
9+
cpSync,
10+
symlinkSync,
11+
} from 'node:fs';
12+
import { tmpdir } from 'node:os';
13+
import path from 'node:path';
14+
import { fileURLToPath } from 'node:url';
15+
16+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
17+
const PROJECT_ROOT = path.resolve(__dirname, '..');
18+
19+
describe('Package install from tarball', () => {
20+
let sourceDir: string;
21+
let packDir: string;
22+
let installDir: string;
23+
let tarballPath: string;
24+
25+
beforeAll(() => {
26+
sourceDir = mkdtempSync(path.join(tmpdir(), 'nori-slack-source-'));
27+
packDir = mkdtempSync(path.join(tmpdir(), 'nori-slack-pack-'));
28+
installDir = mkdtempSync(path.join(tmpdir(), 'nori-slack-install-'));
29+
30+
copyFileSync(path.join(PROJECT_ROOT, 'package.json'), path.join(sourceDir, 'package.json'));
31+
copyFileSync(path.join(PROJECT_ROOT, 'tsconfig.json'), path.join(sourceDir, 'tsconfig.json'));
32+
copyFileSync(path.join(PROJECT_ROOT, 'README.md'), path.join(sourceDir, 'README.md'));
33+
copyFileSync(path.join(PROJECT_ROOT, 'LICENSE'), path.join(sourceDir, 'LICENSE'));
34+
cpSync(path.join(PROJECT_ROOT, 'src'), path.join(sourceDir, 'src'), { recursive: true });
35+
symlinkSync(path.join(PROJECT_ROOT, 'node_modules'), path.join(sourceDir, 'node_modules'));
36+
37+
execFileSync('npm', ['pack', '--pack-destination', packDir], {
38+
cwd: sourceDir,
39+
stdio: 'pipe',
40+
});
41+
42+
const tarball = readdirSync(packDir).find((f) => f.endsWith('.tgz'));
43+
if (!tarball) throw new Error('npm pack did not produce a .tgz');
44+
tarballPath = path.join(packDir, tarball);
45+
46+
execFileSync('npm', ['init', '-y'], { cwd: installDir, stdio: 'pipe' });
47+
execFileSync('npm', ['install', '--no-save', tarballPath], {
48+
cwd: installDir,
49+
stdio: 'pipe',
50+
});
51+
}, 180000);
52+
53+
afterAll(() => {
54+
if (sourceDir) rmSync(sourceDir, { recursive: true, force: true });
55+
if (packDir) rmSync(packDir, { recursive: true, force: true });
56+
if (installDir) rmSync(installDir, { recursive: true, force: true });
57+
});
58+
59+
it('installs a working nori-slack binary that lists methods', { timeout: 30000 }, () => {
60+
const binPath = path.join(installDir, 'node_modules', '.bin', 'nori-slack');
61+
expect(existsSync(binPath)).toBe(true);
62+
63+
const output = execFileSync(binPath, ['list-methods', '--namespace', 'chat'], {
64+
cwd: installDir,
65+
env: { ...process.env, SLACK_BOT_TOKEN: '' },
66+
encoding: 'utf-8',
67+
});
68+
69+
const parsed = JSON.parse(output);
70+
expect(parsed.namespace).toBe('chat');
71+
expect(parsed.methods).toContain('chat.postMessage');
72+
});
73+
});

0 commit comments

Comments
 (0)