@nothumanwork/wit 0.1.15

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,259 @@
1
+ # wit
2
+
3
+ GitHub for AI Agents -- explore GitHub repositories without cloning. Repos are cached as shallow bare clones under your system temp directory by default (override with `WIT_CACHE_DIR`).
4
+
5
+ ## Status
6
+
7
+ **v0.1.0** - Early development
8
+
9
+ ## Installation
10
+
11
+ ### Install from npm
12
+
13
+ ```bash
14
+ npm install -g @nothumanwork/wit
15
+ ```
16
+
17
+ Run without global install:
18
+
19
+ ```bash
20
+ npx @nothumanwork/wit --help
21
+ ```
22
+
23
+ `@nothumanwork/wit` is a single npm package. During `npm install`, its `postinstall` script detects the host platform, selects the matching bundled release archive, and extracts it into npm's bin-managed install location, so one package covers:
24
+ - `darwin-x64`
25
+ - `darwin-arm64`
26
+ - `linux-x64`
27
+ - `linux-arm64`
28
+ - `win32-x64`
29
+ - `win32-arm64`
30
+
31
+ ### Install from binary release (`.sh` installer)
32
+
33
+ ```bash
34
+ curl -fsSL https://raw.githubusercontent.com/thehumanworks/wit/main/install.sh | sh
35
+ ```
36
+
37
+ Install a specific version:
38
+
39
+ ```bash
40
+ curl -fsSL https://raw.githubusercontent.com/thehumanworks/wit/main/install.sh | sh -s -- --version v0.1.0
41
+ ```
42
+
43
+ Install to a specific bin directory:
44
+
45
+ ```bash
46
+ curl -fsSL https://raw.githubusercontent.com/thehumanworks/wit/main/install.sh | sh -s -- --bin-dir ~/.local/bin
47
+ ```
48
+
49
+ The installer auto-detects platform and fetches these release artifacts:
50
+ - `wit-linux-x86_64.tar.gz`
51
+ - `wit-linux-aarch64.tar.gz`
52
+ - `wit-macos-x86_64.tar.gz`
53
+ - `wit-macos-aarch64.tar.gz`
54
+ - `wit-windows-x86_64.zip`
55
+ - `wit-windows-aarch64.zip` (best effort; falls back to x64 in shell environments if unavailable)
56
+
57
+ ### Install from source
58
+
59
+ ```bash
60
+ cargo install --path .
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ```bash
66
+ wit search -p 'ratatui' -l 'Rust' # Find repos
67
+ wit tree ratatui/ratatui # See structure
68
+ wit ls -l ratatui/ratatui src # Browse with sizes
69
+ wit rg -l 'impl Widget' ratatui/ratatui # Find files
70
+ wit cat -n ratatui/ratatui src/lib.rs # Read a file
71
+ wit head -n 30 ratatui/ratatui Cargo.toml # Preview a file
72
+ wit sed -n '100,150p' ratatui/ratatui src/lib.rs # Extract range
73
+ wit rg 'TODO' ratatui/ratatui --ignore '.git' --ignore '*.png' # Exclude paths
74
+ ```
75
+
76
+ ## Global Options
77
+
78
+ | Flag | Description |
79
+ |------|-------------|
80
+ | `--ignore <PATH\|GLOB>` | Exclude files/directories/globs from file operations. Repeat the flag to provide multiple patterns. |
81
+
82
+ Ignore examples:
83
+
84
+ ```bash
85
+ wit tree ratatui/ratatui --ignore '.github' --ignore 'assets/**'
86
+ wit ls ratatui/ratatui src --ignore 'generated'
87
+ wit rg 'fn main' ratatui/ratatui --ignore '.git' --ignore '*.lock'
88
+ wit cat ratatui/ratatui src/main.rs --ignore 'src/main.rs' # blocked (explicitly ignored)
89
+ ```
90
+
91
+ `search` accepts `--ignore`, but applies it only when `--with-snippets` is enabled.
92
+
93
+ ## Commands
94
+
95
+ ### search (alias: s)
96
+
97
+ Find GitHub repositories by name and search their code via grep.app.
98
+
99
+ ```bash
100
+ wit search -p 'ratatui' -l 'Rust' # Find Rust repos named 'ratatui'
101
+ wit search -p 'auth' -q 'JWT' -l 'Go' -w # Find Go auth repos using JWT, show code
102
+ wit search -p 'ratatui' -q 'impl Widget' -w -c # Matching lines only, no context
103
+ ```
104
+
105
+ | Flag | Long | Description |
106
+ |------|------|-------------|
107
+ | `-p` | `--pattern` | Regex pattern to match repository names (required) |
108
+ | `-l` | `--lang` | Filter results by language |
109
+ | `-q` | `--query` | Code pattern to search within repos (default: `.*`) |
110
+ | `-r` | `--regex` | Enable regex search (default: true) |
111
+ | `-w` | `--with-snippets` | Show code snippets with context |
112
+ | `-c` | `--compact` | Show only matching lines (requires `-w`) |
113
+
114
+ ### cache (alias: c)
115
+
116
+ Clone a repository into the local cache (or refresh an existing one). Repos are auto-cached on first use by other commands.
117
+
118
+ ```bash
119
+ wit cache ratatui/ratatui # Force re-clone of ratatui
120
+ ```
121
+
122
+ ### tree (alias: t)
123
+
124
+ Show the file tree of a repository (or subtree). Use `-l` for line counts and token estimates.
125
+
126
+ ```bash
127
+ wit tree ratatui/ratatui # Full repo tree
128
+ wit tree ratatui/ratatui src/widgets # Only the widgets subtree
129
+ wit tree -l ratatui/ratatui src # With line counts and token estimates
130
+ ```
131
+
132
+ ### ls
133
+
134
+ List directory contents (non-recursive). Unlike `tree`, shows only immediate children. Use `-l` for file sizes.
135
+
136
+ ```bash
137
+ wit ls ratatui/ratatui # List repo root
138
+ wit ls ratatui/ratatui src/widgets # List a subdirectory
139
+ wit ls -l ratatui/ratatui src # With line counts and token estimates
140
+ ```
141
+
142
+ ### cat
143
+
144
+ Print a file's contents. For large files, prefer `head`/`tail`/`sed` to read specific ranges, or `rg` to search.
145
+
146
+ ```bash
147
+ wit cat ratatui/ratatui Cargo.toml # Print file
148
+ wit cat -n ratatui/ratatui src/lib.rs # With line numbers
149
+ wit cat -b ratatui/ratatui README.md # Number non-blank lines only
150
+ ```
151
+
152
+ | Flag | Long | Description |
153
+ |------|------|-------------|
154
+ | `-n` | `--number` | Number all output lines |
155
+ | `-b` | `--number-nonblank` | Number non-blank lines only (overrides -n) |
156
+ | `-s` | `--squeeze-blank` | Suppress repeated empty lines |
157
+ | `-E` | `--show-ends` | Show `$` at end of each line |
158
+ | `-T` | `--show-tabs` | Show TAB as `^I` |
159
+ | `-A` | `--show-all` | Equivalent to `-ET` |
160
+
161
+ ### rg
162
+
163
+ Search file contents (ripgrep-style). Use `-l` to find files, `-g` to filter by type.
164
+
165
+ ```bash
166
+ wit rg 'impl Widget' ratatui/ratatui # Find implementations
167
+ wit rg -l 'struct.*Frame' ratatui/ratatui # List files containing pattern
168
+ wit rg -g '*.rs' -i 'todo' ratatui/ratatui # Case-insensitive in .rs files
169
+ wit rg -C 3 'fn render' ratatui/ratatui # 3 lines of context
170
+ wit rg -l --long 'Widget' ratatui/ratatui # File list with line counts
171
+ ```
172
+
173
+ | Flag | Long | Description |
174
+ |------|------|-------------|
175
+ | `-i` | `--ignore-case` | Case insensitive search |
176
+ | `-S` | `--smart-case` | Case-insensitive if pattern is all lowercase |
177
+ | `-w` | `--word-regexp` | Match whole words only |
178
+ | `-v` | `--invert-match` | Show non-matching lines |
179
+ | `-m` | `--max-count` | Maximum matches to show (0 = unlimited) |
180
+ | `-C` | `--context` | Lines of context before and after matches |
181
+ | `-B` | `--before-context` | Lines of context before matches |
182
+ | `-A` | `--after-context` | Lines of context after matches |
183
+ | `-g` | `--glob` | Glob pattern to filter files (e.g., `*.rs`) |
184
+ | `-l` | `--files-with-matches` | Only show file names with matches |
185
+ | `-c` | `--count` | Only show count of matches per file |
186
+ | | `--long` | Show file sizes alongside names (useful with `-l`) |
187
+
188
+ ### sed
189
+
190
+ Extract or transform file content using sed scripts (POSIX-style, Rust regex).
191
+
192
+ ```bash
193
+ wit sed -n '320,460p' modal-labs/modal-client modal/image.py # Print line range
194
+ wit sed -n '/TODO/p' ratatui/ratatui src/lib.rs # Lines matching pattern
195
+ wit sed 's/Widget/Component/g' ratatui/ratatui src/lib.rs # Substitute text
196
+ wit sed -n '/^pub fn/p' ratatui/ratatui src/lib.rs # Extract function signatures
197
+ ```
198
+
199
+ **Notes:**
200
+ - Regex uses Rust syntax (not POSIX BRE).
201
+ - `sed` operates on a single repo file (no stdin or in-place edits).
202
+ - Supports addresses, substitution, hold space, branching, and most POSIX commands.
203
+
204
+ ### head
205
+
206
+ Print the first N lines of a file (default: 10). Use to preview a file before deciding whether to read it fully.
207
+
208
+ ```bash
209
+ wit head ratatui/ratatui src/lib.rs # First 10 lines
210
+ wit head -n 50 ratatui/ratatui Cargo.toml # First 50 lines
211
+ wit head -N ratatui/ratatui README.md # With line numbers
212
+ ```
213
+
214
+ ### tail
215
+
216
+ Print the last N lines of a file, or from line N onward. Use `-p` to read from a specific line to end-of-file.
217
+
218
+ ```bash
219
+ wit tail ratatui/ratatui src/lib.rs # Last 10 lines
220
+ wit tail -n 20 ratatui/ratatui Cargo.toml # Last 20 lines
221
+ wit tail -p 100 ratatui/ratatui src/lib.rs # From line 100 to end
222
+ ```
223
+
224
+ ## Packaging & Release
225
+
226
+ - Every push to `main` triggers `.github/workflows/auto-tag.yml`, which increments the patch version in `Cargo.toml`, creates a new `vX.Y.Z` tag, and pushes it.
227
+ - Push a semver tag (for example `v0.2.0`) to trigger `.github/workflows/release.yml`.
228
+ - `.github/workflows/release.yml` can also be triggered manually with `workflow_dispatch` (select a `vX.Y.Z` tag ref).
229
+ - The workflow builds and uploads `wit-<platform>-<arch>` archives plus `wit-checksums.txt` to the GitHub release.
230
+ - `.github/workflows/publish-npm.yml` publishes `@nothumanwork/wit` from the GitHub release assets and can be re-run manually for an existing release tag without rebuilding Rust binaries.
231
+ - `install.sh` downloads the matching archive and verifies it against the checksum manifest when available.
232
+
233
+ ## Architecture
234
+
235
+ ```
236
+ src/
237
+ ├── cli.rs # CLI entry point and display logic
238
+ ├── lib.rs # Library exports
239
+ ├── sed.rs # POSIX-style sed engine
240
+ ├── gitops/
241
+ │ ├── mod.rs
242
+ │ └── ops.rs # Bare-repo caching, file access, tree, ls, head/tail, ripgrep
243
+ └── grep/
244
+ ├── mod.rs
245
+ ├── client.rs # grep.app API client
246
+ └── types.rs # Response types and data structures
247
+ ```
248
+
249
+ ## Dependencies
250
+
251
+ - `clap` - CLI argument parsing
252
+ - `gix` (gitoxide) - Bare clone + tree traversal + blob reading
253
+ - `reqwest` - HTTP client for grep.app
254
+ - `scraper` - HTML parsing for code snippets
255
+ - `grep-regex` / `grep-searcher` / `grep-matcher` - Ripgrep-style search on blobs
256
+ - `ptree` - Tree display
257
+ - `colored` - Terminal output formatting
258
+ - `tokio` - Async runtime
259
+ - `serde` - JSON serialization
package/bin/wit.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ const TARGETS = {
8
+ "darwin-x64": {
9
+ "binaryFile": "wit"
10
+ },
11
+ "darwin-arm64": {
12
+ "binaryFile": "wit"
13
+ },
14
+ "linux-x64": {
15
+ "binaryFile": "wit"
16
+ },
17
+ "linux-arm64": {
18
+ "binaryFile": "wit"
19
+ },
20
+ "win32-x64": {
21
+ "binaryFile": "wit.exe"
22
+ },
23
+ "win32-arm64": {
24
+ "binaryFile": "wit.exe"
25
+ }
26
+ };
27
+ const PACKAGE_NAME = "@nothumanwork/wit";
28
+
29
+ function fail(message) {
30
+ console.error(message);
31
+ process.exit(1);
32
+ }
33
+
34
+ function resolveBinaryPath() {
35
+ const key = `${process.platform}-${process.arch}`;
36
+ const target = TARGETS[key];
37
+ if (!target) {
38
+ fail(`Unsupported platform/arch: ${process.platform}/${process.arch}`);
39
+ }
40
+
41
+ const binaryPath = path.join(__dirname, target.binaryFile);
42
+ try {
43
+ require("node:fs").accessSync(binaryPath);
44
+ return binaryPath;
45
+ } catch {
46
+ fail(
47
+ `Binary not installed for ${process.platform}/${process.arch}. Reinstall ${PACKAGE_NAME} without --ignore-scripts.`,
48
+ );
49
+ }
50
+ }
51
+
52
+ const binaryPath = resolveBinaryPath();
53
+ const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
54
+
55
+ if (result.error) {
56
+ console.error(result.error.message);
57
+ process.exit(1);
58
+ }
59
+
60
+ if (result.signal) {
61
+ process.kill(process.pid, result.signal);
62
+ }
63
+
64
+ process.exit(result.status ?? 1);
@@ -0,0 +1,6 @@
1
+ cb0f5ca02cad7aa6ca9ab72f06c28274bc1d03cd0634878ec0c7860858a324e7 wit-linux-aarch64.tar.gz
2
+ 06fcc3ba0d0827aa0cb77b2c1c26ea98526f9e2d74aaa446629b889f23271a2c wit-linux-x86_64.tar.gz
3
+ 43322f2cd008f69e6d648c12ab2d120dc23b631fd0429424ce93eeebfd78fe2b wit-macos-aarch64.tar.gz
4
+ d92b487faa3e30290e69f853b2d1df067f8174d3104eacc5395ecd8b1cf2c2cc wit-macos-x86_64.tar.gz
5
+ 339ed6145aae0a6a25d22ba3b6572b550eac278462bd463e327ddfa2c99e38b4 wit-windows-aarch64.zip
6
+ 69e55278c53bee00c7a850c58c0ca1f2fa05cc7f5709173a488aab450006401e wit-windows-x86_64.zip
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@nothumanwork/wit",
3
+ "version": "0.1.15",
4
+ "description": "GitHub exploration CLI for AI agents",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/thehumanworks/wit.git"
9
+ },
10
+ "homepage": "https://github.com/thehumanworks/wit#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/thehumanworks/wit/issues"
13
+ },
14
+ "bin": {
15
+ "wit": "bin/wit.js"
16
+ },
17
+ "scripts": {
18
+ "postinstall": "node scripts/postinstall.js"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "files": [
24
+ "bin/wit.js",
25
+ "scripts/postinstall.js",
26
+ "README.md",
27
+ "dists/*"
28
+ ]
29
+ }
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const fsp = require("node:fs/promises");
6
+ const os = require("node:os");
7
+ const path = require("node:path");
8
+ const { createHash } = require("node:crypto");
9
+ const { spawnSync } = require("node:child_process");
10
+ const packageJson = require("../package.json");
11
+
12
+ const TARGETS = {
13
+ "darwin-x64": {
14
+ "artifact": "wit-macos-x86_64.tar.gz",
15
+ "binaryFile": "wit"
16
+ },
17
+ "darwin-arm64": {
18
+ "artifact": "wit-macos-aarch64.tar.gz",
19
+ "binaryFile": "wit"
20
+ },
21
+ "linux-x64": {
22
+ "artifact": "wit-linux-x86_64.tar.gz",
23
+ "binaryFile": "wit"
24
+ },
25
+ "linux-arm64": {
26
+ "artifact": "wit-linux-aarch64.tar.gz",
27
+ "binaryFile": "wit"
28
+ },
29
+ "win32-x64": {
30
+ "artifact": "wit-windows-x86_64.zip",
31
+ "binaryFile": "wit.exe"
32
+ },
33
+ "win32-arm64": {
34
+ "artifact": "wit-windows-aarch64.zip",
35
+ "binaryFile": "wit.exe"
36
+ }
37
+ };
38
+ const BINARY_NAME = "wit";
39
+ const PACKAGE_NAME = "@nothumanwork/wit";
40
+ const REPOSITORY_URL = "https://github.com/thehumanworks/wit";
41
+ const CHECKSUM_FILE = "wit-checksums.txt";
42
+
43
+ function log(message) {
44
+ console.log(`${PACKAGE_NAME}: ${message}`);
45
+ }
46
+
47
+ function warn(message) {
48
+ console.warn(`${PACKAGE_NAME}: ${message}`);
49
+ }
50
+
51
+ function fail(message) {
52
+ console.error(`${PACKAGE_NAME}: ${message}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ function resolveTarget() {
57
+ const key = `${process.platform}-${process.arch}`;
58
+ const target = TARGETS[key];
59
+ if (!target) {
60
+ fail(
61
+ `unsupported platform/arch: ${process.platform}/${process.arch}. Install from ${REPOSITORY_URL}/releases instead.`,
62
+ );
63
+ }
64
+ return target;
65
+ }
66
+
67
+ async function sha256(filePath) {
68
+ return new Promise((resolve, reject) => {
69
+ const hash = createHash("sha256");
70
+ const stream = fs.createReadStream(filePath);
71
+
72
+ stream.on("data", (chunk) => hash.update(chunk));
73
+ stream.on("end", () => resolve(hash.digest("hex")));
74
+ stream.on("error", reject);
75
+ });
76
+ }
77
+
78
+ function parseChecksums(rawManifest) {
79
+ const manifest = new Map();
80
+ for (const line of rawManifest.split(/\r?\n/)) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed) {
83
+ continue;
84
+ }
85
+
86
+ const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
87
+ if (!match) {
88
+ throw new Error(`invalid checksum entry: ${trimmed}`);
89
+ }
90
+ manifest.set(match[2], match[1]);
91
+ }
92
+ return manifest;
93
+ }
94
+
95
+ function runCommand(command, args) {
96
+ const result = spawnSync(command, args, { stdio: "inherit" });
97
+ if (result.error) {
98
+ throw result.error;
99
+ }
100
+ if (result.status !== 0) {
101
+ throw new Error(`command failed (${result.status}): ${command} ${args.join(" ")}`);
102
+ }
103
+ }
104
+
105
+ function extractArchive(archivePath, destination) {
106
+ if (archivePath.endsWith(".tar.gz")) {
107
+ runCommand("tar", ["-xzf", archivePath, "-C", destination]);
108
+ return;
109
+ }
110
+
111
+ if (archivePath.endsWith(".zip")) {
112
+ runCommand(
113
+ "powershell.exe",
114
+ [
115
+ "-NoProfile",
116
+ "-NonInteractive",
117
+ "-Command",
118
+ "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force",
119
+ archivePath,
120
+ destination,
121
+ ],
122
+ );
123
+ return;
124
+ }
125
+
126
+ throw new Error(`unsupported archive format: ${archivePath}`);
127
+ }
128
+
129
+ async function findFile(rootDir, fileName) {
130
+ const entries = await fsp.readdir(rootDir, { withFileTypes: true });
131
+ for (const entry of entries) {
132
+ const fullPath = path.join(rootDir, entry.name);
133
+ if (entry.isFile() && entry.name === fileName) {
134
+ return fullPath;
135
+ }
136
+ if (entry.isDirectory()) {
137
+ const nested = await findFile(fullPath, fileName);
138
+ if (nested) {
139
+ return nested;
140
+ }
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ async function installBinary({ target }) {
147
+ const packageRoot = path.resolve(__dirname, "..");
148
+ const binDir = path.join(packageRoot, "bin");
149
+ const distsDir = path.join(packageRoot, "dists");
150
+ const binaryPath = path.join(binDir, target.binaryFile);
151
+ const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), "wit-npm-"));
152
+ const archivePath = path.join(distsDir, target.artifact);
153
+ const checksumsPath = path.join(distsDir, CHECKSUM_FILE);
154
+ const extractDir = path.join(tempDir, "extract");
155
+
156
+ try {
157
+ log(`installing ${target.artifact} for ${process.platform}/${process.arch}`);
158
+ await fsp.access(archivePath);
159
+
160
+ const checksums = parseChecksums(await fsp.readFile(checksumsPath, "utf8"));
161
+ const expectedChecksum = checksums.get(target.artifact);
162
+ if (!expectedChecksum) {
163
+ throw new Error(`checksum entry missing for ${target.artifact}`);
164
+ }
165
+
166
+ const actualChecksum = await sha256(archivePath);
167
+ if (actualChecksum !== expectedChecksum) {
168
+ throw new Error(`checksum mismatch for ${target.artifact}`);
169
+ }
170
+
171
+ await fsp.mkdir(extractDir, { recursive: true });
172
+ extractArchive(archivePath, extractDir);
173
+
174
+ const sourceBinary =
175
+ (await findFile(extractDir, target.binaryFile)) || path.join(extractDir, target.binaryFile);
176
+ await fsp.access(sourceBinary);
177
+
178
+ await fsp.mkdir(binDir, { recursive: true });
179
+ await fsp.rm(binaryPath, { force: true });
180
+ await fsp.copyFile(sourceBinary, binaryPath);
181
+ if (!binaryPath.endsWith(".exe")) {
182
+ await fsp.chmod(binaryPath, 0o755);
183
+ }
184
+
185
+ log(`installed ${BINARY_NAME} ${packageJson.version}`);
186
+ } finally {
187
+ await fsp.rm(tempDir, { recursive: true, force: true });
188
+ }
189
+ }
190
+
191
+ async function main() {
192
+ const target = resolveTarget();
193
+ await installBinary({ target });
194
+ }
195
+
196
+ main().catch((error) => {
197
+ fail(error instanceof Error ? error.message : String(error));
198
+ });