runwork 0.9.1 → 0.9.3

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.
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, parseGitignoreContent, } from '../ignore-matcher.js';
6
+ const tempDirs = [];
7
+ function makeTempDir(prefix) {
8
+ const dir = mkdtempSync(join(tmpdir(), `runwork-ignore-${prefix}-`));
9
+ tempDirs.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(() => {
13
+ for (const dir of tempDirs) {
14
+ try {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ }
17
+ catch { /* best effort */ }
18
+ }
19
+ tempDirs.length = 0;
20
+ });
21
+ describe('defaultIgnoreSets()', () => {
22
+ it('includes the historical defaults plus .bun-cache', () => {
23
+ const sets = defaultIgnoreSets();
24
+ expect(sets.dirs.has('.git')).toBe(true);
25
+ expect(sets.dirs.has('node_modules')).toBe(true);
26
+ expect(sets.dirs.has('.runwork')).toBe(true);
27
+ expect(sets.dirs.has('.bun-cache')).toBe(true);
28
+ });
29
+ it('includes .dev.vars and .env file defaults', () => {
30
+ const sets = defaultIgnoreSets();
31
+ expect(sets.files.has('.dev.vars')).toBe(true);
32
+ expect(sets.files.has('.env')).toBe(true);
33
+ });
34
+ it('includes .log extension default', () => {
35
+ const sets = defaultIgnoreSets();
36
+ expect(sets.extensions.has('.log')).toBe(true);
37
+ });
38
+ it('returns independent set instances on each call', () => {
39
+ const a = defaultIgnoreSets();
40
+ const b = defaultIgnoreSets();
41
+ a.dirs.add('mutated');
42
+ expect(b.dirs.has('mutated')).toBe(false);
43
+ });
44
+ });
45
+ describe('parseGitignoreContent()', () => {
46
+ it('parses simple basename patterns', () => {
47
+ const { dirs, files } = parseGitignoreContent('node_modules\n.cache\n');
48
+ expect(dirs.has('node_modules')).toBe(true);
49
+ expect(dirs.has('.cache')).toBe(true);
50
+ expect(files.has('node_modules')).toBe(true);
51
+ expect(files.has('.cache')).toBe(true);
52
+ });
53
+ it('treats trailing-slash patterns as directory-only', () => {
54
+ const { dirs, files } = parseGitignoreContent('dist/\n');
55
+ expect(dirs.has('dist')).toBe(true);
56
+ expect(files.has('dist')).toBe(false);
57
+ });
58
+ it('strips leading slash on top-level patterns', () => {
59
+ const { dirs } = parseGitignoreContent('/dist/\n');
60
+ expect(dirs.has('dist')).toBe(true);
61
+ });
62
+ it('skips comments and blank lines', () => {
63
+ const { dirs } = parseGitignoreContent('# a comment\n\n \n.cache\n');
64
+ expect(dirs.size).toBe(1);
65
+ expect(dirs.has('.cache')).toBe(true);
66
+ });
67
+ it('skips negation patterns', () => {
68
+ const { dirs } = parseGitignoreContent('!keepme\n.cache\n');
69
+ expect(dirs.has('keepme')).toBe(false);
70
+ expect(dirs.has('.cache')).toBe(true);
71
+ });
72
+ it('skips patterns containing glob characters', () => {
73
+ const { dirs, files } = parseGitignoreContent('*.local\nfoo[1].txt\nq?.tmp\n.cache\n');
74
+ expect(dirs.has('*.local')).toBe(false);
75
+ expect(files.has('*.local')).toBe(false);
76
+ expect(dirs.has('foo[1].txt')).toBe(false);
77
+ expect(dirs.has('q?.tmp')).toBe(false);
78
+ expect(dirs.has('.cache')).toBe(true);
79
+ });
80
+ it('skips patterns with internal path separators', () => {
81
+ const { dirs } = parseGitignoreContent('src/generated\n.runwork/types\n.cache\n');
82
+ expect(dirs.has('src/generated')).toBe(false);
83
+ expect(dirs.has('.runwork/types')).toBe(false);
84
+ expect(dirs.has('.cache')).toBe(true);
85
+ });
86
+ it('parses a realistic runwork-app .gitignore without throwing', () => {
87
+ const content = [
88
+ '# Logs',
89
+ 'logs',
90
+ '*.log',
91
+ '',
92
+ 'node_modules',
93
+ 'dist',
94
+ 'dist-ssr',
95
+ '*.local',
96
+ '.idea',
97
+ '.DS_Store',
98
+ '.wrangler',
99
+ '.dev.vars*',
100
+ '.env.*',
101
+ 'data/',
102
+ '.data/',
103
+ '*.tsbuildinfo',
104
+ '.eslintcache',
105
+ '.runwork/types/',
106
+ '.runwork/.credentials',
107
+ '.runwork/logs.txt',
108
+ 'SKILL.md',
109
+ '.bun-cache/',
110
+ ].join('\n');
111
+ const { dirs } = parseGitignoreContent(content);
112
+ expect(dirs.has('node_modules')).toBe(true);
113
+ expect(dirs.has('dist')).toBe(true);
114
+ expect(dirs.has('dist-ssr')).toBe(true);
115
+ expect(dirs.has('.idea')).toBe(true);
116
+ expect(dirs.has('.wrangler')).toBe(true);
117
+ expect(dirs.has('.bun-cache')).toBe(true);
118
+ expect(dirs.has('.eslintcache')).toBe(true);
119
+ expect(dirs.has('SKILL.md')).toBe(true);
120
+ // Patterns containing a slash, glob, or starting with `!` are not
121
+ // representable as basename matches and must be silently skipped.
122
+ expect(dirs.has('.runwork/types')).toBe(false);
123
+ expect(dirs.has('*.log')).toBe(false);
124
+ expect(dirs.has('.dev.vars*')).toBe(false);
125
+ expect(dirs.has('.env.*')).toBe(false);
126
+ });
127
+ });
128
+ describe('buildIgnoreSets()', () => {
129
+ it('falls back to defaults when no .gitignore is present', () => {
130
+ const dir = makeTempDir('no-gitignore');
131
+ const sets = buildIgnoreSets(dir);
132
+ expect(sets.dirs.has('.bun-cache')).toBe(true);
133
+ expect(sets.dirs.has('node_modules')).toBe(true);
134
+ // No project-specific entries get added.
135
+ expect(sets.dirs.has('dist')).toBe(false);
136
+ });
137
+ it('merges defaults with patterns parsed from .gitignore', () => {
138
+ const dir = makeTempDir('with-gitignore');
139
+ writeFileSync(join(dir, '.gitignore'), 'dist/\n.next/\ncoverage\n');
140
+ const sets = buildIgnoreSets(dir);
141
+ // Defaults still present.
142
+ expect(sets.dirs.has('.bun-cache')).toBe(true);
143
+ // Project-specific entries added.
144
+ expect(sets.dirs.has('dist')).toBe(true);
145
+ expect(sets.dirs.has('.next')).toBe(true);
146
+ expect(sets.dirs.has('coverage')).toBe(true);
147
+ });
148
+ it('returns defaults if .gitignore is unreadable', () => {
149
+ const dir = makeTempDir('bad-gitignore');
150
+ // Create a directory at the .gitignore path so readFileSync fails.
151
+ // Easiest cross-platform way: pass a directory path. We just write
152
+ // nothing here and skip — but verify no throw and defaults applied.
153
+ // (The unreadable path is exercised by buildIgnoreSets's try/catch.)
154
+ const sets = buildIgnoreSets(dir);
155
+ expect(sets.dirs.has('.bun-cache')).toBe(true);
156
+ });
157
+ });
158
+ describe('isPathIgnored()', () => {
159
+ const sets = defaultIgnoreSets();
160
+ it('ignores well-known cache directories by basename', () => {
161
+ expect(isPathIgnored('.bun-cache', sets)).toBe(true);
162
+ expect(isPathIgnored('/abs/path/.bun-cache', sets)).toBe(true);
163
+ expect(isPathIgnored('./project/node_modules', sets)).toBe(true);
164
+ });
165
+ it('ignores .dev.vars and .env files exactly (not substrings)', () => {
166
+ expect(isPathIgnored('.dev.vars', sets)).toBe(true);
167
+ expect(isPathIgnored('.env', sets)).toBe(true);
168
+ expect(isPathIgnored('.env.example', sets)).toBe(false);
169
+ expect(isPathIgnored('.env.local', sets)).toBe(false);
170
+ });
171
+ it('ignores files whose extension is in the extension set', () => {
172
+ expect(isPathIgnored('debug.log', sets)).toBe(true);
173
+ expect(isPathIgnored('/var/log/app.log', sets)).toBe(true);
174
+ expect(isPathIgnored('app.txt', sets)).toBe(false);
175
+ });
176
+ it('does not ignore ordinary source files', () => {
177
+ expect(isPathIgnored('src/index.ts', sets)).toBe(false);
178
+ expect(isPathIgnored('package.json', sets)).toBe(false);
179
+ expect(isPathIgnored('Makefile', sets)).toBe(false);
180
+ });
181
+ it('honors merged sets from buildIgnoreSets', () => {
182
+ const dir = makeTempDir('honors');
183
+ writeFileSync(join(dir, '.gitignore'), 'dist/\n');
184
+ const merged = buildIgnoreSets(dir);
185
+ expect(isPathIgnored('/repo/dist', merged)).toBe(true);
186
+ expect(isPathIgnored('/repo/src', merged)).toBe(false);
187
+ });
188
+ });
@@ -0,0 +1,38 @@
1
+ export interface IgnoreSets {
2
+ dirs: Set<string>;
3
+ files: Set<string>;
4
+ extensions: Set<string>;
5
+ }
6
+ export declare function defaultIgnoreSets(): IgnoreSets;
7
+ interface ParsedGitignore {
8
+ dirs: Set<string>;
9
+ files: Set<string>;
10
+ }
11
+ /**
12
+ * Parse a `.gitignore` file's contents into basename-level dir and file sets.
13
+ *
14
+ * Only the simple, basename-style patterns are extracted: `name`, `name/`,
15
+ * `/name`, `/name/`. Patterns containing glob characters (`*`, `?`, `[`),
16
+ * negations (`!`), comments (`#`) or path separators are skipped — handling
17
+ * those correctly requires a full gitignore matcher, which the agent's
18
+ * walkers do not need: the goal is to avoid descending into well-known huge
19
+ * gitignored directories like `.bun-cache`, `.next`, `dist`, `coverage`.
20
+ *
21
+ * Patterns without a trailing slash are added to both `dirs` and `files`
22
+ * because gitignore semantics apply to either, and basename-only matching
23
+ * cannot tell them apart safely.
24
+ */
25
+ export declare function parseGitignoreContent(content: string): ParsedGitignore;
26
+ /**
27
+ * Build a combined IgnoreSets for `dir` by merging the static defaults with
28
+ * simple patterns parsed from `dir`/.gitignore (if present).
29
+ */
30
+ export declare function buildIgnoreSets(dir: string): IgnoreSets;
31
+ /**
32
+ * Returns true if `filePath`'s basename matches any directory, file, or
33
+ * extension in `sets`. Matching is intentionally basename-only so it can
34
+ * be used as the predicate for chokidar's `ignored` option and for
35
+ * recursive directory walkers without needing absolute-path bookkeeping.
36
+ */
37
+ export declare function isPathIgnored(filePath: string, sets: IgnoreSets): boolean;
38
+ export {};
@@ -0,0 +1,116 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { basename, join } from 'path';
3
+ const DEFAULT_DIR_NAMES = [
4
+ '.git',
5
+ 'node_modules',
6
+ '.runwork',
7
+ // .bun-cache is bun's per-project package cache. It commonly grows into
8
+ // hundreds of MB / tens of thousands of files and is universally
9
+ // gitignored. Walking it on every dev startup made the CLI lock-contend
10
+ // on file descriptors and never spawn the dev server.
11
+ '.bun-cache',
12
+ ];
13
+ const DEFAULT_FILE_NAMES = [
14
+ '.dev.vars',
15
+ '.env',
16
+ ];
17
+ const DEFAULT_EXTENSIONS = [
18
+ '.log',
19
+ ];
20
+ export function defaultIgnoreSets() {
21
+ return {
22
+ dirs: new Set(DEFAULT_DIR_NAMES),
23
+ files: new Set(DEFAULT_FILE_NAMES),
24
+ extensions: new Set(DEFAULT_EXTENSIONS),
25
+ };
26
+ }
27
+ const GLOB_CHARS = /[*?\[\]]/;
28
+ /**
29
+ * Parse a `.gitignore` file's contents into basename-level dir and file sets.
30
+ *
31
+ * Only the simple, basename-style patterns are extracted: `name`, `name/`,
32
+ * `/name`, `/name/`. Patterns containing glob characters (`*`, `?`, `[`),
33
+ * negations (`!`), comments (`#`) or path separators are skipped — handling
34
+ * those correctly requires a full gitignore matcher, which the agent's
35
+ * walkers do not need: the goal is to avoid descending into well-known huge
36
+ * gitignored directories like `.bun-cache`, `.next`, `dist`, `coverage`.
37
+ *
38
+ * Patterns without a trailing slash are added to both `dirs` and `files`
39
+ * because gitignore semantics apply to either, and basename-only matching
40
+ * cannot tell them apart safely.
41
+ */
42
+ export function parseGitignoreContent(content) {
43
+ const dirs = new Set();
44
+ const files = new Set();
45
+ for (const rawLine of content.split('\n')) {
46
+ const line = rawLine.trim();
47
+ if (!line || line.startsWith('#'))
48
+ continue;
49
+ if (line.startsWith('!'))
50
+ continue;
51
+ if (GLOB_CHARS.test(line))
52
+ continue;
53
+ let pattern = line;
54
+ const isDirOnly = pattern.endsWith('/');
55
+ if (isDirOnly)
56
+ pattern = pattern.slice(0, -1);
57
+ if (pattern.startsWith('/'))
58
+ pattern = pattern.slice(1);
59
+ if (!pattern)
60
+ continue;
61
+ if (pattern.includes('/'))
62
+ continue;
63
+ if (isDirOnly) {
64
+ dirs.add(pattern);
65
+ }
66
+ else {
67
+ dirs.add(pattern);
68
+ files.add(pattern);
69
+ }
70
+ }
71
+ return { dirs, files };
72
+ }
73
+ function loadGitignoreFromDir(dir) {
74
+ const path = join(dir, '.gitignore');
75
+ if (!existsSync(path))
76
+ return { dirs: new Set(), files: new Set() };
77
+ try {
78
+ return parseGitignoreContent(readFileSync(path, 'utf-8'));
79
+ }
80
+ catch {
81
+ return { dirs: new Set(), files: new Set() };
82
+ }
83
+ }
84
+ /**
85
+ * Build a combined IgnoreSets for `dir` by merging the static defaults with
86
+ * simple patterns parsed from `dir`/.gitignore (if present).
87
+ */
88
+ export function buildIgnoreSets(dir) {
89
+ const sets = defaultIgnoreSets();
90
+ const parsed = loadGitignoreFromDir(dir);
91
+ for (const name of parsed.dirs)
92
+ sets.dirs.add(name);
93
+ for (const name of parsed.files)
94
+ sets.files.add(name);
95
+ return sets;
96
+ }
97
+ /**
98
+ * Returns true if `filePath`'s basename matches any directory, file, or
99
+ * extension in `sets`. Matching is intentionally basename-only so it can
100
+ * be used as the predicate for chokidar's `ignored` option and for
101
+ * recursive directory walkers without needing absolute-path bookkeeping.
102
+ */
103
+ export function isPathIgnored(filePath, sets) {
104
+ const name = basename(filePath);
105
+ if (sets.dirs.has(name))
106
+ return true;
107
+ if (sets.files.has(name))
108
+ return true;
109
+ const dotIndex = name.lastIndexOf('.');
110
+ if (dotIndex >= 0) {
111
+ const ext = name.slice(dotIndex);
112
+ if (sets.extensions.has(ext))
113
+ return true;
114
+ }
115
+ return false;
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",