@awilum/scope 1.0.0
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/LICENSE +21 -0
- package/README.md +381 -0
- package/bin/scope.js +612 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sergey Romanenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# Scope
|
|
2
|
+
|
|
3
|
+
Scope is a minimal, dependency-free command-line utility for exploring software projects and building a clean, structured project context as plain text.
|
|
4
|
+
|
|
5
|
+
Scope runs locally, reads your project according to explicit rules, and produces deterministic text that can be copied into a chat, issue, review, documentation tool, or another text-based development environment.
|
|
6
|
+
|
|
7
|
+
## Why Scope?
|
|
8
|
+
|
|
9
|
+
When working on an unfamiliar codebase, developers often need a quick answer to: _What is in this project, and what does the relevant source actually contain?_ Scope turns that task into one command.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
scope
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The default command builds `scope-context.txt`, containing project metadata, statistics, a complete readable tree, and the contents of recognized text files.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 18 or newer
|
|
20
|
+
- npm
|
|
21
|
+
- No runtime dependencies
|
|
22
|
+
- No network connection
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Global installation from npm:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @awilum/scope
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or from a local clone:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone <repository>
|
|
36
|
+
cd scope
|
|
37
|
+
npm install -g .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For local development, `npm link` is also supported:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm link
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then verify:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
scope --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
From any project directory:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd my-project
|
|
58
|
+
scope init
|
|
59
|
+
scope tree
|
|
60
|
+
scope stats
|
|
61
|
+
scope inspect src/index.js
|
|
62
|
+
scope build
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The shortest workflow is simply:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
scope
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
which is equivalent to `scope build`.
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
### `scope`
|
|
76
|
+
|
|
77
|
+
Builds project context using the current directory and `.scope.json` if present.
|
|
78
|
+
|
|
79
|
+
### `scope init`
|
|
80
|
+
|
|
81
|
+
Creates `.scope.json` with sensible defaults. Existing configuration is never overwritten.
|
|
82
|
+
|
|
83
|
+
### `scope build`
|
|
84
|
+
|
|
85
|
+
Generates `scope-context.txt` by default.
|
|
86
|
+
|
|
87
|
+
The output contains:
|
|
88
|
+
|
|
89
|
+
1. project metadata;
|
|
90
|
+
2. statistics;
|
|
91
|
+
3. project tree;
|
|
92
|
+
4. recognized text-file contents.
|
|
93
|
+
|
|
94
|
+
File boundaries are explicit:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
FILE: src/app.js
|
|
98
|
+
------------------
|
|
99
|
+
import ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Generated context contains no ANSI terminal escape sequences and uses relative paths for file content.
|
|
103
|
+
|
|
104
|
+
### `scope tree`
|
|
105
|
+
|
|
106
|
+
Displays a readable project tree.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
scope tree
|
|
110
|
+
scope tree 3
|
|
111
|
+
scope tree --depth 2
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `scope stats`
|
|
115
|
+
|
|
116
|
+
Displays file, directory, text/binary, size, line, skipped-file, and extension statistics.
|
|
117
|
+
|
|
118
|
+
### `scope inspect <file>`
|
|
119
|
+
|
|
120
|
+
Displays metadata and the content of one recognized text file.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
scope inspect package.json
|
|
124
|
+
scope inspect src/index.js
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The path is resolved inside the current project root. Traversal outside the root is rejected.
|
|
128
|
+
|
|
129
|
+
### `scope doctor`
|
|
130
|
+
|
|
131
|
+
Checks Node.js, the project root, configuration, include paths, and output path.
|
|
132
|
+
|
|
133
|
+
A missing `.scope.json` is normal: Scope simply uses defaults.
|
|
134
|
+
|
|
135
|
+
### `scope help`
|
|
136
|
+
|
|
137
|
+
Prints command help.
|
|
138
|
+
|
|
139
|
+
### `scope --version`
|
|
140
|
+
|
|
141
|
+
Prints the installed version.
|
|
142
|
+
|
|
143
|
+
## JSON mode
|
|
144
|
+
|
|
145
|
+
The tree, statistics, inspect, and doctor commands support machine-readable output:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
scope tree --json
|
|
149
|
+
scope stats --json
|
|
150
|
+
scope inspect package.json --json
|
|
151
|
+
scope doctor --json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
JSON output contains no ANSI styling or progress messages and is suitable for scripts.
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
Scope looks for `.scope.json` in the current project root.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"output": "scope-context.txt",
|
|
165
|
+
"include": ["."],
|
|
166
|
+
"exclude": ["node_modules", ".git", "dist", "build", "coverage"],
|
|
167
|
+
"excludeFiles": ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
168
|
+
"excludePaths": [],
|
|
169
|
+
"extensions": [".js", ".ts", ".tsx", ".json", ".md"],
|
|
170
|
+
"maxFileSize": 1048576
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `output`
|
|
175
|
+
|
|
176
|
+
Output filename for `scope build`. The output must remain inside the project root.
|
|
177
|
+
|
|
178
|
+
### `include`
|
|
179
|
+
|
|
180
|
+
Relative files or directories to process. The default is `["."]`.
|
|
181
|
+
|
|
182
|
+
### `exclude`
|
|
183
|
+
|
|
184
|
+
Directory names that are not traversed. Common generated and dependency directories are excluded by default.
|
|
185
|
+
|
|
186
|
+
### `excludeFiles`
|
|
187
|
+
|
|
188
|
+
Exact filenames that are skipped.
|
|
189
|
+
|
|
190
|
+
### `excludePaths`
|
|
191
|
+
|
|
192
|
+
Relative path prefixes that are skipped.
|
|
193
|
+
|
|
194
|
+
### `extensions`
|
|
195
|
+
|
|
196
|
+
Recognized textual file extensions. Files without extensions such as `Dockerfile`, `Makefile`, `Procfile`, `LICENSE`, and `README` are recognized as well.
|
|
197
|
+
|
|
198
|
+
### `maxFileSize`
|
|
199
|
+
|
|
200
|
+
Maximum file size read into context. The default is 1 MiB.
|
|
201
|
+
|
|
202
|
+
## Security and privacy
|
|
203
|
+
|
|
204
|
+
Scope is local-only. It does not upload, transmit, or call an external service.
|
|
205
|
+
|
|
206
|
+
Sensitive files are skipped by default, including environment files and common private credentials such as `.pem`, `.key`, `.p12`, `.pfx`, `id_rsa`, `id_ed25519`, `credentials.json`, and `service-account.json`.
|
|
207
|
+
|
|
208
|
+
Binary files are never copied into generated context. Files larger than `maxFileSize` are skipped from content generation.
|
|
209
|
+
|
|
210
|
+
Configuration is an explicit trust boundary: if you intentionally add a sensitive file to an allowed configuration, you are responsible for that choice.
|
|
211
|
+
|
|
212
|
+
## Generated context format
|
|
213
|
+
|
|
214
|
+
A typical context looks like:
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
PROJECT CONTEXT
|
|
218
|
+
===============
|
|
219
|
+
Project: my-project
|
|
220
|
+
Root: /path/to/my-project
|
|
221
|
+
Generated: 2026-09-27
|
|
222
|
+
|
|
223
|
+
PROJECT STATISTICS
|
|
224
|
+
==================
|
|
225
|
+
Files: 42
|
|
226
|
+
Directories: 9
|
|
227
|
+
Text files: 37
|
|
228
|
+
Binary files: 5
|
|
229
|
+
Total size: 482.0 KB
|
|
230
|
+
Context size: 310.2 KB
|
|
231
|
+
Lines: 8421
|
|
232
|
+
|
|
233
|
+
PROJECT TREE
|
|
234
|
+
============
|
|
235
|
+
src/
|
|
236
|
+
config.js
|
|
237
|
+
index.js
|
|
238
|
+
package.json
|
|
239
|
+
README.md
|
|
240
|
+
|
|
241
|
+
FILES
|
|
242
|
+
=====
|
|
243
|
+
FILE: package.json
|
|
244
|
+
------------------
|
|
245
|
+
{ ... }
|
|
246
|
+
|
|
247
|
+
FILE: src/index.js
|
|
248
|
+
------------------
|
|
249
|
+
import ...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The representation is deliberately plain text, stable, and easy to copy.
|
|
253
|
+
|
|
254
|
+
## Supported text formats
|
|
255
|
+
|
|
256
|
+
The default configuration recognizes common JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, Swift, C/C++, CSS, HTML, Vue, Svelte, Astro, JSON, YAML, XML, TOML, Markdown, shell, PHP, Ruby, SQL, GraphQL, SVG, and related formats.
|
|
257
|
+
|
|
258
|
+
The extension list is configurable.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
Run the CLI directly without installing it globally:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
node bin/scope.js --version
|
|
266
|
+
node bin/scope.js help
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Check syntax:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm run check
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Test the CLI in the repository:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
node bin/scope.js doctor
|
|
279
|
+
node bin/scope.js tree
|
|
280
|
+
node bin/scope.js stats
|
|
281
|
+
node bin/scope.js inspect package.json
|
|
282
|
+
node bin/scope.js build
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Then test the global command:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npm install -g .
|
|
289
|
+
scope --version
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Or:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
npm link
|
|
296
|
+
scope --version
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Publishing to npm
|
|
300
|
+
|
|
301
|
+
After choosing and confirming an available package name and authenticating with npm:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
npm publish --access public
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For the scoped package name used by this repository:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
npm install -g @awilum/scope
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## GitHub usage
|
|
314
|
+
|
|
315
|
+
The repository is intentionally small and can be inspected without learning a framework. A typical first commit is:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git init
|
|
319
|
+
git add .
|
|
320
|
+
git commit -m "Initial release"
|
|
321
|
+
git remote add origin <repository>
|
|
322
|
+
git push -u origin main
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The repository intentionally excludes `node_modules`, generated context files, logs, and macOS metadata.
|
|
326
|
+
|
|
327
|
+
## Troubleshooting
|
|
328
|
+
|
|
329
|
+
### `scope: command not found`
|
|
330
|
+
|
|
331
|
+
Confirm that the global npm bin directory is on your `PATH`, then reinstall:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
npm install -g .
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Configuration errors
|
|
338
|
+
|
|
339
|
+
Run:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
scope doctor
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Fix the reported `.scope.json` values and run the command again.
|
|
346
|
+
|
|
347
|
+
### A file is missing from context
|
|
348
|
+
|
|
349
|
+
Check whether it is:
|
|
350
|
+
|
|
351
|
+
- excluded by directory name;
|
|
352
|
+
- listed in `excludeFiles` or `excludePaths`;
|
|
353
|
+
- a recognized text extension;
|
|
354
|
+
- larger than `maxFileSize`;
|
|
355
|
+
- recognized as a sensitive file.
|
|
356
|
+
|
|
357
|
+
Use `scope stats` to see skipped counts.
|
|
358
|
+
|
|
359
|
+
### A file is outside the configured root
|
|
360
|
+
|
|
361
|
+
Scope intentionally refuses `inspect` paths that resolve outside the current project root.
|
|
362
|
+
|
|
363
|
+
## Project structure
|
|
364
|
+
|
|
365
|
+
```text
|
|
366
|
+
scope/
|
|
367
|
+
├── bin/
|
|
368
|
+
│ └── scope.js
|
|
369
|
+
├── examples/
|
|
370
|
+
├── .gitignore
|
|
371
|
+
├── .scope.json.example
|
|
372
|
+
├── LICENSE
|
|
373
|
+
├── package.json
|
|
374
|
+
└── README.md
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The implementation intentionally stays concentrated in one CLI file. Scope is a utility, not a framework.
|
|
378
|
+
|
|
379
|
+
## License
|
|
380
|
+
|
|
381
|
+
MIT. See `LICENSE`.
|
package/bin/scope.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
const VERSION = "1.0.0";
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
output: "scope-context.txt",
|
|
10
|
+
include: ["."],
|
|
11
|
+
exclude: [
|
|
12
|
+
"node_modules",
|
|
13
|
+
".git",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"coverage",
|
|
17
|
+
".cache",
|
|
18
|
+
".next",
|
|
19
|
+
".vite",
|
|
20
|
+
".venv",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".idea",
|
|
23
|
+
".vscode",
|
|
24
|
+
".DS_Store",
|
|
25
|
+
],
|
|
26
|
+
excludeFiles: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
27
|
+
excludePaths: [],
|
|
28
|
+
extensions: [
|
|
29
|
+
".js",
|
|
30
|
+
".jsx",
|
|
31
|
+
".mjs",
|
|
32
|
+
".cjs",
|
|
33
|
+
".ts",
|
|
34
|
+
".tsx",
|
|
35
|
+
".py",
|
|
36
|
+
".go",
|
|
37
|
+
".rs",
|
|
38
|
+
".java",
|
|
39
|
+
".kt",
|
|
40
|
+
".swift",
|
|
41
|
+
".c",
|
|
42
|
+
".cpp",
|
|
43
|
+
".h",
|
|
44
|
+
".hpp",
|
|
45
|
+
".css",
|
|
46
|
+
".scss",
|
|
47
|
+
".sass",
|
|
48
|
+
".less",
|
|
49
|
+
".html",
|
|
50
|
+
".htm",
|
|
51
|
+
".vue",
|
|
52
|
+
".svelte",
|
|
53
|
+
".astro",
|
|
54
|
+
".json",
|
|
55
|
+
".jsonc",
|
|
56
|
+
".yaml",
|
|
57
|
+
".yml",
|
|
58
|
+
".xml",
|
|
59
|
+
".toml",
|
|
60
|
+
".ini",
|
|
61
|
+
".env.example",
|
|
62
|
+
".md",
|
|
63
|
+
".mdx",
|
|
64
|
+
".txt",
|
|
65
|
+
".sh",
|
|
66
|
+
".bash",
|
|
67
|
+
".zsh",
|
|
68
|
+
".php",
|
|
69
|
+
".rb",
|
|
70
|
+
".sql",
|
|
71
|
+
".graphql",
|
|
72
|
+
".gql",
|
|
73
|
+
".svg",
|
|
74
|
+
],
|
|
75
|
+
maxFileSize: 1048576,
|
|
76
|
+
};
|
|
77
|
+
const SPECIAL = new Set([
|
|
78
|
+
"Dockerfile",
|
|
79
|
+
"Makefile",
|
|
80
|
+
"Procfile",
|
|
81
|
+
"LICENSE",
|
|
82
|
+
"README",
|
|
83
|
+
"README.md",
|
|
84
|
+
"README.txt",
|
|
85
|
+
".scope.json",
|
|
86
|
+
".gitignore",
|
|
87
|
+
]);
|
|
88
|
+
const SECRET_NAMES = new Set([
|
|
89
|
+
".env",
|
|
90
|
+
".env.local",
|
|
91
|
+
".env.production",
|
|
92
|
+
".env.development",
|
|
93
|
+
"credentials.json",
|
|
94
|
+
"service-account.json",
|
|
95
|
+
"id_rsa",
|
|
96
|
+
"id_ed25519",
|
|
97
|
+
]);
|
|
98
|
+
const SECRET_EXTENSIONS = new Set([".pem", ".key", ".p12", ".pfx"]);
|
|
99
|
+
|
|
100
|
+
const ansi = (code, s) =>
|
|
101
|
+
process.stdout.isTTY ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
102
|
+
const bold = (s) => ansi(1, s);
|
|
103
|
+
const dim = (s) => ansi(2, s);
|
|
104
|
+
const green = (s) => ansi(32, s);
|
|
105
|
+
const red = (s) => ansi(31, s);
|
|
106
|
+
const yellow = (s) => ansi(33, s);
|
|
107
|
+
const cyan = (s) => ansi(36, s);
|
|
108
|
+
const fail = (message, code = 1) => {
|
|
109
|
+
console.error(`${red("✗")} ${message}`);
|
|
110
|
+
process.exitCode = code;
|
|
111
|
+
};
|
|
112
|
+
const humanSize = (bytes) => {
|
|
113
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
114
|
+
let n = bytes,
|
|
115
|
+
i = 0;
|
|
116
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
117
|
+
n /= 1024;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
121
|
+
};
|
|
122
|
+
const rel = (root, p) => path.relative(root, p) || ".";
|
|
123
|
+
|
|
124
|
+
function parseArgs(argv) {
|
|
125
|
+
const args = { _: [], json: false, depth: null };
|
|
126
|
+
for (let i = 0; i < argv.length; i++) {
|
|
127
|
+
const a = argv[i];
|
|
128
|
+
if (a === "--json") args.json = true;
|
|
129
|
+
else if (a === "--help" || a === "-h") args.help = true;
|
|
130
|
+
else if (a === "--version" || a === "-v") args.version = true;
|
|
131
|
+
else if (a === "--depth") {
|
|
132
|
+
const v = argv[++i];
|
|
133
|
+
args.depth = Number(v);
|
|
134
|
+
} else if (a.startsWith("--depth=")) args.depth = Number(a.slice(8));
|
|
135
|
+
else args._.push(a);
|
|
136
|
+
}
|
|
137
|
+
return args;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadConfig(root) {
|
|
141
|
+
const file = path.join(root, ".scope.json");
|
|
142
|
+
if (!fs.existsSync(file))
|
|
143
|
+
return { config: { ...DEFAULT_CONFIG }, exists: false, error: null };
|
|
144
|
+
try {
|
|
145
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
146
|
+
const c = { ...DEFAULT_CONFIG, ...raw };
|
|
147
|
+
if (
|
|
148
|
+
!Array.isArray(c.include) ||
|
|
149
|
+
!Array.isArray(c.exclude) ||
|
|
150
|
+
!Array.isArray(c.excludeFiles) ||
|
|
151
|
+
!Array.isArray(c.excludePaths) ||
|
|
152
|
+
!Array.isArray(c.extensions)
|
|
153
|
+
)
|
|
154
|
+
throw new Error(
|
|
155
|
+
"include, exclude, excludeFiles, excludePaths and extensions must be arrays",
|
|
156
|
+
);
|
|
157
|
+
if (typeof c.output !== "string" || !c.output.trim())
|
|
158
|
+
throw new Error("output must be a non-empty string");
|
|
159
|
+
if (!Number.isInteger(c.maxFileSize) || c.maxFileSize <= 0)
|
|
160
|
+
throw new Error("maxFileSize must be a positive integer");
|
|
161
|
+
return { config: c, exists: true, error: null };
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return { config: { ...DEFAULT_CONFIG }, exists: true, error: e.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isSecret(relPath) {
|
|
168
|
+
const normalized = relPath.split(path.sep).join("/");
|
|
169
|
+
const base = path.basename(normalized);
|
|
170
|
+
if (SECRET_NAMES.has(base)) return true;
|
|
171
|
+
if ([...SECRET_EXTENSIONS].some((ext) => base.endsWith(ext))) return true;
|
|
172
|
+
if (base.startsWith(".env.") && base !== ".env.example") return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function isAllowedFile(name, config) {
|
|
176
|
+
if (SPECIAL.has(name)) return true;
|
|
177
|
+
const lower = name.toLowerCase();
|
|
178
|
+
return config.extensions.some((ext) => lower.endsWith(ext.toLowerCase()));
|
|
179
|
+
}
|
|
180
|
+
function shouldExclude(relPath, config) {
|
|
181
|
+
const parts = relPath.split(path.sep).filter(Boolean);
|
|
182
|
+
if (parts.some((p) => config.exclude.includes(p))) return true;
|
|
183
|
+
if (config.excludeFiles.includes(path.basename(relPath))) return true;
|
|
184
|
+
const normalized = relPath.split(path.sep).join("/");
|
|
185
|
+
return config.excludePaths.some(
|
|
186
|
+
(p) =>
|
|
187
|
+
normalized === p || normalized.startsWith(p.replace(/\/$/, "") + "/"),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
function inside(root, target) {
|
|
191
|
+
const r = path.resolve(root),
|
|
192
|
+
t = path.resolve(target);
|
|
193
|
+
return t === r || t.startsWith(r + path.sep);
|
|
194
|
+
}
|
|
195
|
+
function includedByConfig(relPath, config) {
|
|
196
|
+
const clean = relPath.split(path.sep).join("/");
|
|
197
|
+
return config.include.some(
|
|
198
|
+
(item) =>
|
|
199
|
+
item === "." ||
|
|
200
|
+
clean === item ||
|
|
201
|
+
clean.startsWith(item.replace(/\/$/, "") + "/"),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function walk(root, config) {
|
|
205
|
+
const entries = [];
|
|
206
|
+
const skipped = [];
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
const visit = (dir) => {
|
|
209
|
+
let names;
|
|
210
|
+
try {
|
|
211
|
+
names = fs
|
|
212
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
213
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
skipped.push({ path: rel(root, dir), reason: e.code || e.message });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const ent of names) {
|
|
219
|
+
const full = path.join(dir, ent.name),
|
|
220
|
+
rp = rel(root, full);
|
|
221
|
+
if (shouldExclude(rp, config) || !includedByConfig(rp, config)) continue;
|
|
222
|
+
let st;
|
|
223
|
+
try {
|
|
224
|
+
st = fs.lstatSync(full);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
skipped.push({ path: rp, reason: e.code || e.message });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (st.isSymbolicLink()) {
|
|
230
|
+
let target;
|
|
231
|
+
try {
|
|
232
|
+
target = fs.realpathSync(full);
|
|
233
|
+
if (!inside(root, target))
|
|
234
|
+
skipped.push({ path: rp, reason: "symlink outside project root" });
|
|
235
|
+
else {
|
|
236
|
+
const key = fs.realpathSync(full);
|
|
237
|
+
if (!seen.has(key)) {
|
|
238
|
+
seen.add(key);
|
|
239
|
+
const ts = fs.statSync(full);
|
|
240
|
+
if (ts.isDirectory()) visit(full);
|
|
241
|
+
else
|
|
242
|
+
entries.push({
|
|
243
|
+
path: rp,
|
|
244
|
+
type: "file",
|
|
245
|
+
size: ts.size,
|
|
246
|
+
symlink: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
skipped.push({ path: rp, reason: "broken symbolic link" });
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (st.isDirectory()) {
|
|
256
|
+
entries.push({ path: rp, type: "directory" });
|
|
257
|
+
visit(full);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (st.isFile()) {
|
|
261
|
+
if (isSecret(rp)) {
|
|
262
|
+
skipped.push({ path: rp, reason: "sensitive file" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const textual = isAllowedFile(ent.name, config);
|
|
266
|
+
entries.push({
|
|
267
|
+
path: rp,
|
|
268
|
+
type: "file",
|
|
269
|
+
size: st.size,
|
|
270
|
+
textual,
|
|
271
|
+
tooLarge: st.size > config.maxFileSize,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
visit(root);
|
|
277
|
+
return { entries, skipped };
|
|
278
|
+
}
|
|
279
|
+
function inspectFile(root, config, file) {
|
|
280
|
+
const target = path.resolve(root, file);
|
|
281
|
+
if (!inside(root, target))
|
|
282
|
+
throw new Error("Path is outside the project root");
|
|
283
|
+
if (!fs.existsSync(target)) throw new Error(`File does not exist: ${file}`);
|
|
284
|
+
const st = fs.lstatSync(target);
|
|
285
|
+
if (!st.isFile()) throw new Error("Path is not a regular file");
|
|
286
|
+
const rp = rel(root, target);
|
|
287
|
+
if (isSecret(rp)) throw new Error("Refusing to inspect a sensitive file");
|
|
288
|
+
if (!isAllowedFile(path.basename(target), config))
|
|
289
|
+
throw new Error("File type is not recognized as text");
|
|
290
|
+
if (st.size > config.maxFileSize)
|
|
291
|
+
throw new Error(
|
|
292
|
+
`File exceeds maxFileSize (${humanSize(config.maxFileSize)})`,
|
|
293
|
+
);
|
|
294
|
+
const content = fs.readFileSync(target, "utf8");
|
|
295
|
+
return {
|
|
296
|
+
path: rp,
|
|
297
|
+
size: st.size,
|
|
298
|
+
lines: content === "" ? 0 : content.split(/\r?\n/).length,
|
|
299
|
+
encoding: "UTF-8",
|
|
300
|
+
content,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function projectData(root, config) {
|
|
304
|
+
const { entries, skipped } = walk(root, config);
|
|
305
|
+
const files = entries.filter((e) => e.type === "file");
|
|
306
|
+
const dirs = entries.filter((e) => e.type === "directory");
|
|
307
|
+
const text = files.filter((e) => e.textual && !e.tooLarge);
|
|
308
|
+
const binary = files.filter((e) => !e.textual);
|
|
309
|
+
const tooLarge = files.filter((e) => e.tooLarge);
|
|
310
|
+
const ext = {};
|
|
311
|
+
let contextSize = 0,
|
|
312
|
+
totalLines = 0;
|
|
313
|
+
for (const f of text) {
|
|
314
|
+
const extn = path.extname(f.path).toLowerCase() || "[no extension]";
|
|
315
|
+
ext[extn] = (ext[extn] || 0) + 1;
|
|
316
|
+
try {
|
|
317
|
+
const c = fs.readFileSync(path.join(root, f.path), "utf8");
|
|
318
|
+
contextSize += Buffer.byteLength(c);
|
|
319
|
+
totalLines += c === "" ? 0 : c.split(/\r?\n/).length;
|
|
320
|
+
} catch {}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
entries,
|
|
324
|
+
skipped,
|
|
325
|
+
files,
|
|
326
|
+
dirs,
|
|
327
|
+
text,
|
|
328
|
+
binary,
|
|
329
|
+
tooLarge,
|
|
330
|
+
stats: {
|
|
331
|
+
files: files.length,
|
|
332
|
+
directories: dirs.length,
|
|
333
|
+
textFiles: text.length,
|
|
334
|
+
binaryFiles: binary.length,
|
|
335
|
+
totalSize: files.reduce((n, f) => n + f.size, 0),
|
|
336
|
+
contextSize,
|
|
337
|
+
lines: totalLines,
|
|
338
|
+
extensions: ext,
|
|
339
|
+
excluded: skipped.length,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function treeText(data, depth = Infinity) {
|
|
344
|
+
const nodes = new Map([[".", []]]);
|
|
345
|
+
for (const e of data.entries) {
|
|
346
|
+
const parts = e.path.split(path.sep);
|
|
347
|
+
if (parts.length > depth + 1) continue;
|
|
348
|
+
let cur = ".";
|
|
349
|
+
for (let i = 0; i < parts.length; i++) {
|
|
350
|
+
const p = parts[i],
|
|
351
|
+
key = cur + "/" + p;
|
|
352
|
+
if (!nodes.has(cur)) nodes.set(cur, []);
|
|
353
|
+
if (!nodes.get(cur).some((x) => x.name === p))
|
|
354
|
+
nodes
|
|
355
|
+
.get(cur)
|
|
356
|
+
.push({
|
|
357
|
+
name: p,
|
|
358
|
+
type: i === parts.length - 1 ? e.type : "directory",
|
|
359
|
+
});
|
|
360
|
+
cur = key;
|
|
361
|
+
if (!nodes.has(cur)) nodes.set(cur, []);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const lines = [];
|
|
365
|
+
const render = (parent, prefix) => {
|
|
366
|
+
const kids = (nodes.get(parent) || []).sort((a, b) =>
|
|
367
|
+
a.type === b.type
|
|
368
|
+
? a.name.localeCompare(b.name)
|
|
369
|
+
: a.type === "directory"
|
|
370
|
+
? -1
|
|
371
|
+
: 1,
|
|
372
|
+
);
|
|
373
|
+
for (const k of kids) {
|
|
374
|
+
lines.push(prefix + k.name + (k.type === "directory" ? "/" : ""));
|
|
375
|
+
if (k.type === "directory") render(parent + "/" + k.name, prefix + " ");
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
render(".", "");
|
|
379
|
+
return lines.join("\n");
|
|
380
|
+
}
|
|
381
|
+
function contextText(root, data, config) {
|
|
382
|
+
const name = path.basename(root);
|
|
383
|
+
const s = data.stats;
|
|
384
|
+
const lines = [];
|
|
385
|
+
lines.push(
|
|
386
|
+
"PROJECT CONTEXT",
|
|
387
|
+
"===============",
|
|
388
|
+
`Project: ${name}`,
|
|
389
|
+
`Root: ${root}`,
|
|
390
|
+
`Generated: ${new Date().toISOString().slice(0, 10)}`,
|
|
391
|
+
"",
|
|
392
|
+
"PROJECT STATISTICS",
|
|
393
|
+
"==================",
|
|
394
|
+
`Files: ${s.files}`,
|
|
395
|
+
`Directories: ${s.directories}`,
|
|
396
|
+
`Text files: ${s.textFiles}`,
|
|
397
|
+
`Binary files: ${s.binaryFiles}`,
|
|
398
|
+
`Total size: ${humanSize(s.totalSize)}`,
|
|
399
|
+
`Context size: ${humanSize(s.contextSize)}`,
|
|
400
|
+
`Lines: ${s.lines}`,
|
|
401
|
+
"",
|
|
402
|
+
"PROJECT TREE",
|
|
403
|
+
"============",
|
|
404
|
+
treeText(data),
|
|
405
|
+
"",
|
|
406
|
+
"FILES",
|
|
407
|
+
"=====",
|
|
408
|
+
);
|
|
409
|
+
for (const f of data.text.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
410
|
+
let content;
|
|
411
|
+
try {
|
|
412
|
+
content = fs.readFileSync(path.join(root, f.path), "utf8");
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
lines.push(
|
|
417
|
+
`FILE: ${f.path}`,
|
|
418
|
+
"------------------",
|
|
419
|
+
content.replace(/\r\n/g, "\n").replace(/\u0000/g, ""),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return lines.join("\n") + "\n";
|
|
423
|
+
}
|
|
424
|
+
function printHelp() {
|
|
425
|
+
console.log(
|
|
426
|
+
`${bold("Scope")} — minimal project exploration and context builder\n\nUsage:\n scope [command] [options]\n\nCommands:\n scope Build project context (same as scope build)\n scope init Create .scope.json\n scope build Generate scope-context.txt\n scope tree Show project tree\n scope stats Show project statistics\n scope inspect <file> Show file metadata and content\n scope doctor Check project/configuration health\n scope help Show this help\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Machine-readable JSON output\n --depth <n> Limit tree depth\n\nExamples:\n scope\n scope init\n scope tree 3\n scope tree --depth 2\n scope stats --json\n scope inspect src/index.js\n scope build`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function init(root) {
|
|
430
|
+
const file = path.join(root, ".scope.json");
|
|
431
|
+
if (fs.existsSync(file)) {
|
|
432
|
+
console.log(
|
|
433
|
+
`${yellow("!")} .scope.json already exists. Nothing was overwritten.`,
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
fs.writeFileSync(file, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
|
|
438
|
+
console.log(`${green("✓")} Created .scope.json`);
|
|
439
|
+
}
|
|
440
|
+
function doctor(root, configInfo) {
|
|
441
|
+
const checks = [];
|
|
442
|
+
checks.push({
|
|
443
|
+
name: "Node.js",
|
|
444
|
+
ok: Number(process.versions.node.split(".")[0]) >= 18,
|
|
445
|
+
value: process.version,
|
|
446
|
+
});
|
|
447
|
+
checks.push({ name: "Project root", ok: fs.existsSync(root), value: root });
|
|
448
|
+
checks.push({
|
|
449
|
+
name: "Configuration",
|
|
450
|
+
ok: !configInfo.error,
|
|
451
|
+
value: configInfo.exists ? ".scope.json" : "defaults",
|
|
452
|
+
});
|
|
453
|
+
if (!configInfo.error) {
|
|
454
|
+
for (const p of configInfo.config.include) {
|
|
455
|
+
const t = path.resolve(root, p);
|
|
456
|
+
checks.push({
|
|
457
|
+
name: `Include: ${p}`,
|
|
458
|
+
ok: inside(root, t) && fs.existsSync(t),
|
|
459
|
+
value: inside(root, t) ? t : "outside root",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const out = path.resolve(root, configInfo.config.output);
|
|
463
|
+
checks.push({ name: "Output path", ok: inside(root, out), value: out });
|
|
464
|
+
}
|
|
465
|
+
return checks;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function main() {
|
|
469
|
+
const root = process.cwd(),
|
|
470
|
+
args = parseArgs(process.argv.slice(2));
|
|
471
|
+
if (args.version) {
|
|
472
|
+
console.log(VERSION);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (args.help || args._[0] === "help") {
|
|
476
|
+
printHelp();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const command = args._[0] || "build";
|
|
480
|
+
const configInfo = loadConfig(root);
|
|
481
|
+
if (configInfo.error && command !== "doctor") {
|
|
482
|
+
fail(`Invalid .scope.json: ${configInfo.error}`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const config = configInfo.config;
|
|
486
|
+
try {
|
|
487
|
+
if (command === "init") {
|
|
488
|
+
init(root);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (command === "doctor") {
|
|
492
|
+
const checks = doctor(root, configInfo);
|
|
493
|
+
if (args.json) {
|
|
494
|
+
console.log(
|
|
495
|
+
JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2),
|
|
496
|
+
);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.log(
|
|
500
|
+
bold("SCOPE DOCTOR"),
|
|
501
|
+
"\n────────────\n" +
|
|
502
|
+
checks
|
|
503
|
+
.map(
|
|
504
|
+
(c) =>
|
|
505
|
+
` ${c.ok ? green("✓") : red("✗")} ${c.name.padEnd(18)} ${c.value}`,
|
|
506
|
+
)
|
|
507
|
+
.join("\n") +
|
|
508
|
+
"\n" +
|
|
509
|
+
(checks.every((c) => c.ok)
|
|
510
|
+
? "No problems found."
|
|
511
|
+
: "Problems found."),
|
|
512
|
+
);
|
|
513
|
+
if (!checks.every((c) => c.ok)) process.exitCode = 1;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!["build", "tree", "stats", "inspect"].includes(command)) {
|
|
517
|
+
fail(`Unknown command: ${command}\nRun: scope help`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const data = projectData(root, config);
|
|
521
|
+
if (command === "tree") {
|
|
522
|
+
let depth = args.depth;
|
|
523
|
+
if (depth === null && args._[1]) depth = Number(args._[1]);
|
|
524
|
+
if (depth !== null && (!Number.isInteger(depth) || depth < 0)) {
|
|
525
|
+
fail("Depth must be a non-negative integer");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const out = {
|
|
529
|
+
root,
|
|
530
|
+
tree: treeText(data, depth === null ? Infinity : depth),
|
|
531
|
+
entries: data.entries,
|
|
532
|
+
};
|
|
533
|
+
if (args.json) {
|
|
534
|
+
console.log(JSON.stringify(out, null, 2));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
console.log(
|
|
538
|
+
bold("PROJECT TREE"),
|
|
539
|
+
"\n─────────────\n" + treeText(data, depth === null ? Infinity : depth),
|
|
540
|
+
);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (command === "stats") {
|
|
544
|
+
const out = { root, stats: data.stats };
|
|
545
|
+
if (args.json) {
|
|
546
|
+
console.log(JSON.stringify(out, null, 2));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const ex = Object.entries(data.stats.extensions).sort(
|
|
550
|
+
(a, b) => b[1] - a[1],
|
|
551
|
+
);
|
|
552
|
+
console.log(
|
|
553
|
+
bold("PROJECT STATISTICS"),
|
|
554
|
+
"\n──────────────────\n" +
|
|
555
|
+
` Files ${data.stats.files}\n Directories ${data.stats.directories}\n Text files ${data.stats.textFiles}\n Binary files ${data.stats.binaryFiles}\n Total size ${humanSize(data.stats.totalSize)}\n Context size ${humanSize(data.stats.contextSize)}\n Lines ${data.stats.lines}\n Skipped ${data.stats.excluded}\n Extensions\n` +
|
|
556
|
+
ex.map(([k, v]) => ` ${k.padEnd(14)} ${v}`).join("\n"),
|
|
557
|
+
);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (command === "inspect") {
|
|
561
|
+
if (!args._[1]) {
|
|
562
|
+
fail("Missing file path. Example: scope inspect src/index.js");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const info = inspectFile(root, config, args._[1]);
|
|
566
|
+
if (args.json) {
|
|
567
|
+
console.log(JSON.stringify(info, null, 2));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
console.log(
|
|
571
|
+
bold("FILE"),
|
|
572
|
+
"\n────\n" +
|
|
573
|
+
` Path ${info.path}\n Size ${humanSize(info.size)}\n Lines ${info.lines}\n Encoding ${info.encoding}\n\n` +
|
|
574
|
+
bold("CONTENT"),
|
|
575
|
+
"\n───────\n" + info.content,
|
|
576
|
+
);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const output = path.resolve(root, config.output);
|
|
580
|
+
if (!inside(root, output))
|
|
581
|
+
throw new Error(
|
|
582
|
+
"Configured output path must stay inside the project root",
|
|
583
|
+
);
|
|
584
|
+
const context = contextText(root, data, config);
|
|
585
|
+
fs.writeFileSync(output, context, "utf8");
|
|
586
|
+
if (args.json) {
|
|
587
|
+
console.log(
|
|
588
|
+
JSON.stringify(
|
|
589
|
+
{
|
|
590
|
+
ok: true,
|
|
591
|
+
root,
|
|
592
|
+
output,
|
|
593
|
+
files: data.text.length,
|
|
594
|
+
contextSize: Buffer.byteLength(context),
|
|
595
|
+
durationMs: 0,
|
|
596
|
+
},
|
|
597
|
+
null,
|
|
598
|
+
2,
|
|
599
|
+
),
|
|
600
|
+
);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
console.log(
|
|
604
|
+
bold("PROJECT CONTEXT BUILDER"),
|
|
605
|
+
"\n────────────────────────\n" +
|
|
606
|
+
`Project\n───────\n Root ${root}\n Output ${rel(root, output)}\n\nScanning\n────────\n ${green("✓")} Found ${data.text.length} text files\n\nBuilding Context\n────────────────\n ${green("✓")} ${data.text.length} files included\n\nComplete\n────────\n ${green("✓")} Context generated successfully\n Files ${data.text.length}\n Context size ${humanSize(Buffer.byteLength(context))}\n Output ${output}\nContext build finished successfully.`,
|
|
607
|
+
);
|
|
608
|
+
} catch (e) {
|
|
609
|
+
fail(e.message || String(e));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awilum/scope",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A minimal dependency-free CLI for exploring projects and building structured project context.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Sergey Romanenko",
|
|
8
|
+
"email": "awilum@msn.com",
|
|
9
|
+
"url": "https://github.com/Awilum"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/Awilum/scope.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Awilum/scope/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/Awilum/scope",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"scope": "./bin/scope.js"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
".scope.json.example"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"cli",
|
|
34
|
+
"developer-tools",
|
|
35
|
+
"project-context",
|
|
36
|
+
"codebase",
|
|
37
|
+
"source-code",
|
|
38
|
+
"project-explorer",
|
|
39
|
+
"code-explorer",
|
|
40
|
+
"context",
|
|
41
|
+
"unix",
|
|
42
|
+
"nodejs"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"check": "node --check bin/scope.js",
|
|
46
|
+
"test": "node --test",
|
|
47
|
+
"test:cli": "node bin/scope.js doctor --json",
|
|
48
|
+
"format": "prettier --write .",
|
|
49
|
+
"format:check": "prettier --check .",
|
|
50
|
+
"pack:check": "npm pack --dry-run"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"esbuild": "^0.28.0",
|
|
57
|
+
"prettier": "^3.9.9"
|
|
58
|
+
}
|
|
59
|
+
}
|