@taskless/cli 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -195
- package/assets/reference.json +213 -0
- package/dist/binary-DSACXL6n.js +107 -0
- package/dist/index.js +14470 -17446
- package/dist/layout/index.d.ts +31 -0
- package/dist/layout.js +43 -0
- package/dist/node/runtimes/index.d.ts +102 -0
- package/dist/node/runtimes.js +10 -0
- package/dist/prompts/index.d.ts +103 -0
- package/dist/prompts/recipes.d.ts +114 -0
- package/dist/prompts.js +53 -0
- package/dist/recipes-w68swLg4.js +2662 -0
- package/dist/rules/ast-grep-binary.d.ts +26 -0
- package/dist/rules/capabilities.d.ts +351 -0
- package/dist/rules/constraints.d.ts +132 -0
- package/dist/rules/layout.d.ts +152 -0
- package/dist/rules/platform-binary.d.ts +113 -0
- package/dist/rules/vale/binary.d.ts +35 -0
- package/dist/schemas/index.d.ts +93 -0
- package/dist/schemas/rules-create.d.ts +19 -0
- package/dist/schemas/rules-improve.d.ts +22 -0
- package/dist/schemas/rules-verify.d.ts +67 -0
- package/dist/schemas/verify-test.d.ts +22 -0
- package/dist/schemas-OhdGAhu_.js +4512 -0
- package/dist/schemas.js +65 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/util/invocation.d.ts +42 -0
- package/dist/verify-test-BaZPwJqC.js +34 -0
- package/package.json +46 -16
package/README.md
CHANGED
|
@@ -1,235 +1,105 @@
|
|
|
1
1
|
# @taskless/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@taskless/cli)
|
|
4
|
+
[](https://github.com/taskless/cli/actions/workflows/validate.yml?query=branch%3Amain)
|
|
5
|
+
[](https://www.npmjs.com/package/@taskless/cli-nightly)
|
|
6
|
+
[](https://github.com/errata-ai/vale/releases)
|
|
7
|
+
[](https://github.com/ast-grep/ast-grep/releases)
|
|
4
8
|
|
|
5
|
-
CLI companion for [Taskless](https://taskless.io). Designed to
|
|
9
|
+
CLI companion for [Taskless](https://taskless.io). Designed to work with agent skills to add constraints that improve coding agent output.
|
|
6
10
|
|
|
7
|
-
##
|
|
11
|
+
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
|
-
|
|
11
|
-
npx @taskless/cli@latest info
|
|
12
|
-
|
|
13
|
-
# pnpm
|
|
14
|
-
pnpm dlx @taskless/cli@latest info
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Commands
|
|
18
|
-
|
|
19
|
-
### `taskless info`
|
|
20
|
-
|
|
21
|
-
Outputs CLI version, tool status, and login info as JSON to stdout:
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"version": "0.0.3",
|
|
26
|
-
"tools": [],
|
|
27
|
-
"loggedIn": true,
|
|
28
|
-
"auth": { "user": "jake", "email": "jake@example.com", "orgs": ["my-org"] }
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### `taskless init`
|
|
33
|
-
|
|
34
|
-
Launches an interactive wizard that detects supported tool directories in the
|
|
35
|
-
current project (`.claude/`, `.opencode/`, `.cursor/`, `.agents/`), asks which
|
|
36
|
-
tools to enable Taskless for, and walks through the auth tradeoff before
|
|
37
|
-
writing anything. Running `taskless` with no subcommand in a TTY also launches
|
|
38
|
-
this wizard. Without a TTY, bare `taskless` prints a short context preamble
|
|
39
|
-
followed by the topic index from `taskless help`.
|
|
40
|
-
|
|
41
|
-
In v0.7+, there is exactly one skill (`taskless`) and one command (`tskl`) —
|
|
42
|
-
no opt-in selection needed.
|
|
43
|
-
|
|
44
|
-
The skill and command content is written **once** to a canonical store in
|
|
45
|
-
`.taskless/skills/` and `.taskless/commands/`. Each enabled tool directory
|
|
46
|
-
receives only a thin reference stub — an ordinary file with a delegating body,
|
|
47
|
-
never a symlink — so there is a single source of truth and no drift between
|
|
48
|
-
copies. Stale layouts from earlier versions (full per-tool copies, symlinks)
|
|
49
|
-
are converged into stubs automatically on the next `init`/`update`.
|
|
50
|
-
|
|
51
|
-
For CI and scripted installs, pass `--no-interactive` to skip all prompts:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
taskless init # interactive wizard (default in a TTY)
|
|
55
|
-
taskless init --no-interactive # scripted install, no prompts
|
|
14
|
+
npx @taskless/cli
|
|
56
15
|
```
|
|
57
16
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
skills and commands during this diff. Cancelling the wizard at any step
|
|
62
|
-
(Ctrl-C) aborts cleanly with no filesystem changes.
|
|
63
|
-
|
|
64
|
-
### `taskless onboard`
|
|
65
|
-
|
|
66
|
-
Post-install discovery flow that helps a fresh user go from zero rules to a
|
|
67
|
-
useful starter set. Run it after `taskless init`. The CLI prints an
|
|
68
|
-
agent-facing recipe that walks the host AI tool through scanning the
|
|
69
|
-
codebase, agent-memory files (CLAUDE.md / AGENTS.md / .cursorrules),
|
|
70
|
-
recent PR review comments (when `gh` is available), and issue tracker
|
|
71
|
-
tickets (when a relevant MCP is wired in) for high-signal rule
|
|
72
|
-
candidates, then surfaces them as a bullet list the user can choose to
|
|
73
|
-
materialize via `taskless rule create`.
|
|
17
|
+
Run with no arguments in a terminal to launch the installer, which detects the
|
|
18
|
+
agent tools in your project (Claude Code, Cursor, OpenCode) and installs into
|
|
19
|
+
each of them. For scripted installs, skip the prompts:
|
|
74
20
|
|
|
75
21
|
```bash
|
|
76
|
-
taskless
|
|
77
|
-
taskless onboard --force # re-run even when previously marked complete
|
|
78
|
-
taskless onboard --mark-complete # record completion in .taskless/taskless.json
|
|
79
|
-
# (invoked by the agent after explicit user
|
|
80
|
-
# confirmation; never automatically)
|
|
22
|
+
npx @taskless/cli init --no-interactive
|
|
81
23
|
```
|
|
82
24
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Only the agent writes it, and only with the user's explicit confirmation.
|
|
86
|
-
`taskless init` does not set it. Pass `--force` to re-run regardless of the
|
|
87
|
-
current value.
|
|
88
|
-
|
|
89
|
-
After a successful `taskless init`, the CLI prints a one-line trailer
|
|
90
|
-
pointing the user at this command. The trailer wording adapts to the
|
|
91
|
-
install plan: when the install included slash commands (Claude Code or
|
|
92
|
-
Cursor), it mentions `/tskl onboard` along with the Taskless skill and the
|
|
93
|
-
bare CLI; when the install only wrote skills (OpenCode, Codex, the
|
|
94
|
-
`.agents/` fallback), it mentions the skill and the bare CLI only.
|
|
25
|
+
New to Taskless? Run `npx @taskless/cli onboard` after installing. It walks your
|
|
26
|
+
agent through your codebase and suggests a starter set of rules.
|
|
95
27
|
|
|
96
|
-
|
|
28
|
+
## How to Use via Agents
|
|
97
29
|
|
|
98
|
-
|
|
30
|
+
Installing adds one skill (`taskless`) and one slash command (`/tskl`). The skill
|
|
31
|
+
body is a small router: your agent fetches the canonical recipe for whatever you
|
|
32
|
+
asked for, then follows it.
|
|
99
33
|
|
|
100
|
-
```bash
|
|
101
|
-
taskless check # human-readable output, scans whole project
|
|
102
|
-
taskless check --json # JSON output
|
|
103
34
|
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
directories — useful for CI workflows that only want to check changed files.
|
|
107
|
-
Paths that don't exist on disk (e.g. files deleted in a diff) are silently
|
|
108
|
-
filtered, so raw git-diff output can be piped in directly:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
taskless check src/foo.ts src/bar.ts
|
|
112
|
-
taskless check $(git diff --name-only main...HEAD) # PR-only scan
|
|
113
|
-
taskless check $(git diff --cached --name-only) # pre-commit scan
|
|
35
|
+
/tskl create a rule that bans console.log
|
|
36
|
+
/tskl add taskless to CI
|
|
114
37
|
```
|
|
115
38
|
|
|
116
|
-
|
|
39
|
+
Plain language works too: "write a taskless rule for X", "run taskless check",
|
|
40
|
+
"taskless login" all engage the skill. You rarely need to run the CLI yourself.
|
|
117
41
|
|
|
118
|
-
|
|
42
|
+
To see what the agent sees, run `npx @taskless/cli agent` for the topic index, or
|
|
43
|
+
`npx @taskless/cli agent <topic>` for a full recipe.
|
|
119
44
|
|
|
120
|
-
|
|
45
|
+
## Taskless Check (CI and Constraints)
|
|
121
46
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
47
|
+
`taskless check` runs your rules against the codebase. It exits `0` when nothing
|
|
48
|
+
error-severity matched and `1` when something did, so it drops straight into a
|
|
49
|
+
pipeline:
|
|
125
50
|
|
|
126
51
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
52
|
+
npx @taskless/cli check # scan everything
|
|
53
|
+
npx @taskless/cli check $(git diff --name-only main...HEAD) # PR-only scan
|
|
54
|
+
npx @taskless/cli check --json # machine-readable
|
|
129
55
|
```
|
|
130
56
|
|
|
131
|
-
|
|
57
|
+
Paths that no longer exist are dropped silently, so raw `git diff` output can be
|
|
58
|
+
piped in without pre-filtering. Static rules need no login and make no network
|
|
59
|
+
calls, so CI needs no secrets. Runtime rules (which execute code) only run once
|
|
60
|
+
the server has verified their signature; otherwise they are reported as skipped
|
|
61
|
+
and never change the exit code.
|
|
132
62
|
|
|
133
|
-
|
|
63
|
+
Ask your agent to `/tskl add taskless to CI` and it will wire this into the CI
|
|
64
|
+
system you already use rather than replacing it.
|
|
134
65
|
|
|
135
|
-
|
|
66
|
+
## Why Teams Choose Taskless
|
|
136
67
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
68
|
+
- **Constraints, not suggestions.** Rules are real files in your repo, enforced
|
|
69
|
+
by ast-grep, Vale, and runtime checks: the same result every run, for every
|
|
70
|
+
agent and every human.
|
|
71
|
+
- **The same rules in the editor and in CI.** One command, one exit code.
|
|
72
|
+
- **Works with the agent you already have.** One skill installs into Claude Code,
|
|
73
|
+
Cursor, and OpenCode, plus the `/tskl` command wherever the tool supports slash
|
|
74
|
+
commands, with a plain `.agents/` fallback when none is detected.
|
|
75
|
+
- **Nothing to run locally.** No daemon, no install step in CI, no auth for the
|
|
76
|
+
checks that matter most.
|
|
140
77
|
|
|
141
|
-
|
|
78
|
+
## Docs
|
|
142
79
|
|
|
143
|
-
|
|
80
|
+
- [docs.taskless.io](https://docs.taskless.io): guides and reference
|
|
81
|
+
- [taskless.io](https://taskless.io): the product
|
|
82
|
+
- [github.com/taskless/cli](https://github.com/taskless/cli): source and issues
|
|
144
83
|
|
|
145
|
-
|
|
84
|
+
<details>
|
|
85
|
+
<summary><strong>Other</strong></summary>
|
|
146
86
|
|
|
147
|
-
|
|
148
|
-
topic (e.g. `taskless help rule create`), prints the full step-by-step recipe
|
|
149
|
-
for that operation, including an embedded JSON Schema for any `--from` input
|
|
150
|
-
and a table of stable error codes. Append `--anonymous` to fetch the
|
|
151
|
-
local-only variant where one exists (currently `rule create`/`rule improve`).
|
|
87
|
+
### Telemetry
|
|
152
88
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
### `--anonymous` flag
|
|
158
|
-
|
|
159
|
-
Recognized on every command. Behavior matrix:
|
|
160
|
-
|
|
161
|
-
- `rule create` / `rule improve` — exits with a pointer to
|
|
162
|
-
`taskless help <topic> --anonymous`. The local-only flow runs in the agent
|
|
163
|
-
per the recipe variant.
|
|
164
|
-
- `info` — skips the API/auth probe; reports local state only.
|
|
165
|
-
- `auth login` — rejected (auth commands cannot be anonymous).
|
|
166
|
-
- All others — accepted as no-op.
|
|
167
|
-
|
|
168
|
-
## For skill authors
|
|
169
|
-
|
|
170
|
-
Skills should detect the package manager by checking for lock files and invoke the CLI accordingly:
|
|
171
|
-
|
|
172
|
-
1. If `pnpm-lock.yaml` exists, use `pnpm dlx @taskless/cli@latest <command>`
|
|
173
|
-
2. Otherwise, use `npx @taskless/cli@latest <command>`
|
|
174
|
-
|
|
175
|
-
All commands output structured JSON to stdout by default. Parse with `JSON.parse()` and handle non-zero exit codes as errors.
|
|
176
|
-
|
|
177
|
-
## Developing
|
|
178
|
-
|
|
179
|
-
### Testing
|
|
89
|
+
The CLI reports anonymous usage (which command ran, whether it succeeded, how
|
|
90
|
+
long it took, and counts of findings. It never sends rule content, prompts, or
|
|
91
|
+
matched source. Disable it by setting either environment variable:
|
|
180
92
|
|
|
181
93
|
```bash
|
|
182
|
-
|
|
183
|
-
|
|
94
|
+
export TASKLESS_TELEMETRY_DISABLED=1
|
|
95
|
+
# or the cross-tool convention
|
|
96
|
+
export DO_NOT_TRACK=1
|
|
184
97
|
```
|
|
185
98
|
|
|
186
|
-
|
|
187
|
-
Integration tests that exercise the built binary (for example `detect`) run
|
|
188
|
-
against `dist/`, so run `pnpm --filter @taskless/cli build` first (or after any
|
|
189
|
-
source change) before invoking them directly.
|
|
190
|
-
|
|
191
|
-
**Two kinds of test, one of which is not fully automatable.** Most tests are
|
|
192
|
-
deterministic unit/integration checks. The route-honesty dataset
|
|
193
|
-
(`test/fixtures/route-eval.json`) is different: the actual routing decision is
|
|
194
|
-
made by an _agent_ following `help/route.txt`, so it cannot be asserted by a
|
|
195
|
-
code classifier. The automated test (`test/route-eval.test.ts`) therefore only
|
|
196
|
-
**guards the dataset** — that it stays structurally valid and balanced across
|
|
197
|
-
every route and both failure directions (over-claim / over-escalate). Running
|
|
198
|
-
the dataset _as an evaluation_ — feeding each case to an agent and scoring its
|
|
199
|
-
chosen destination — is a separate, manual calibration step with more setup; it
|
|
200
|
-
is not part of `pnpm test`. Keep the two distinct: the suite proves the fixtures
|
|
201
|
-
are well-formed; an agent run proves the recipe routes honestly.
|
|
202
|
-
|
|
203
|
-
### API base URL
|
|
204
|
-
|
|
205
|
-
The CLI resolves the API base URL in this order:
|
|
99
|
+
With either set, no client is created and no network request is made.
|
|
206
100
|
|
|
207
|
-
|
|
208
|
-
2. `~/.config/taskless/config.json` → `apiUrl` field
|
|
209
|
-
3. Default: `https://app.taskless.io/cli`
|
|
101
|
+
</details>
|
|
210
102
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
TASKLESS_API_URL=http://localhost:5173/cli taskless info
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### API schema introspection
|
|
218
|
-
|
|
219
|
-
All `/cli/api/*` endpoints support the `x-explain: 1` header. When present, the endpoint returns its JSON schema instead of executing — no authentication required.
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
# List available endpoints
|
|
223
|
-
curl -s -H "x-explain: 1" http://localhost:5173/cli/api
|
|
224
|
-
|
|
225
|
-
# Get the schema for rule generation
|
|
226
|
-
curl -s -H "x-explain: 1" -X POST http://localhost:5173/cli/api/rule
|
|
227
|
-
|
|
228
|
-
# Get the schema for rule status polling
|
|
229
|
-
curl -s -H "x-explain: 1" http://localhost:5173/cli/api/rule/any-id
|
|
230
|
-
|
|
231
|
-
# Get the schema for whoami
|
|
232
|
-
curl -s -H "x-explain: 1" http://localhost:5173/cli/api/whoami
|
|
233
|
-
```
|
|
103
|
+
---
|
|
234
104
|
|
|
235
|
-
|
|
105
|
+
MIT licensed.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"protocol": [
|
|
4
|
+
"Generate a rule from `prompt`, using your own pipeline.",
|
|
5
|
+
"Run `taskless verify` over what you generated. It enforces constraints beyond the engine's own schema, listed in `constraints` below, so a rule the engine executes correctly can still be refused. A rule that fails here is not deliverable however well it behaves, and every later step would be measuring the wrong thing. Check `enforcedBy` before concluding anything: some constraints are only decided once the fixtures run.",
|
|
6
|
+
"Run your generated rule against your own cases. It should pass; if it does not, the disagreement is inside your pipeline and nothing below will be informative.",
|
|
7
|
+
"Run your generated rule against `tests` here. A failure means your rule and ours disagree about the subject, and `tests` is the arbiter.",
|
|
8
|
+
"Run the rule in `rule` here against your cases. A failure means your cases and ours disagree, which is worth as much as the previous step and is the one nobody runs."
|
|
9
|
+
],
|
|
10
|
+
"layout": {
|
|
11
|
+
"rulesRoot": ".taskless/rules",
|
|
12
|
+
"ruleDirectory": ".taskless/rules/{engine}/{id}",
|
|
13
|
+
"testsDirectory": ".tests",
|
|
14
|
+
"engines": {
|
|
15
|
+
"sg": {
|
|
16
|
+
"ruleFile": "{id}.yml",
|
|
17
|
+
"ruleConfigFile": null,
|
|
18
|
+
"capturesDirectory": null,
|
|
19
|
+
"fixtureLayout": "ast-grep-test"
|
|
20
|
+
},
|
|
21
|
+
"vale": {
|
|
22
|
+
"ruleFile": "{id}.yml",
|
|
23
|
+
"ruleConfigFile": ".vale.ini",
|
|
24
|
+
"capturesDirectory": null,
|
|
25
|
+
"fixtureLayout": "case-documents"
|
|
26
|
+
},
|
|
27
|
+
"runtime": {
|
|
28
|
+
"ruleFile": "check.ts",
|
|
29
|
+
"ruleConfigFile": null,
|
|
30
|
+
"capturesDirectory": "captures",
|
|
31
|
+
"fixtureLayout": "case-directories"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"constraints": [
|
|
36
|
+
{
|
|
37
|
+
"id": "sg-id-matches-directory",
|
|
38
|
+
"engine": "sg",
|
|
39
|
+
"enforcedBy": "verify",
|
|
40
|
+
"summary": "A rule's `id:` must equal the directory it lives in.",
|
|
41
|
+
"rationale": "The directory name is the rule id: it is what `check` and `test` address, and what a person types to delete a rule. ast-grep registers the rule under the id in its body. With the two apart, `test` cannot find the rule at all, and `check` does run it but reports findings under a name no directory has, so nobody can locate what produced them."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "sg-regex-needs-kind",
|
|
45
|
+
"engine": "sg",
|
|
46
|
+
"enforcedBy": "verify",
|
|
47
|
+
"summary": "A `regex` needs a sibling `kind`, in `rule`, `constraints` and `utils`.",
|
|
48
|
+
"rationale": "A regex match with no kind to anchor it is ambiguous and slow: it is applied to every node rather than to the one shape the author meant. ast-grep accepts it, so the engine is not the thing that will tell you."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "sg-language-accepted",
|
|
52
|
+
"engine": "sg",
|
|
53
|
+
"enforcedBy": "verify",
|
|
54
|
+
"summary": "`language:` must be a spelling ast-grep itself uses; a resolvable but non-canonical one is a notice.",
|
|
55
|
+
"rationale": "An unrecognized name aborts config parsing, which takes every other sg rule in the project down with it and reports nothing. That is the loudest possible failure with the quietest possible symptom: a clean report."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "sg-files-globs-parse",
|
|
59
|
+
"engine": "sg",
|
|
60
|
+
"enforcedBy": "verify",
|
|
61
|
+
"summary": "`files:` globs must not name `.tsx` under TypeScript, or `.ts` under Tsx.",
|
|
62
|
+
"rationale": "A glob naming an extension the language cannot parse matches nothing, so the rule reports a clean codebase rather than an error. Only the TypeScript/Tsx pair is checked, and deliberately so: they are separate parsers rather than aliases, which is the one language/extension mismatch decidable from the rule file alone. No other extension is compared against `language`, so this is narrower than it first reads."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "sg-required-fields",
|
|
66
|
+
"engine": "sg",
|
|
67
|
+
"enforcedBy": "verify",
|
|
68
|
+
"summary": "`id`, `language`, `severity`, `message` and `rule` are required.",
|
|
69
|
+
"rationale": "ast-grep needs fewer of these than we do. The extras are what make a finding actionable and a rule addressable once it is on disk."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "sg-test-file-required",
|
|
73
|
+
"engine": "sg",
|
|
74
|
+
"enforcedBy": "verify",
|
|
75
|
+
"summary": "A rule must ship at least one test file under `.tests/`.",
|
|
76
|
+
"rationale": "A rule with no fixtures has shown neither that it fires nor that it stays quiet. `verify` requires the file; `test` requires the cases inside it to cover both."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "sg-fixture-id-matches-rule",
|
|
80
|
+
"engine": "sg",
|
|
81
|
+
"enforcedBy": "test",
|
|
82
|
+
"summary": "A test file's own `id:` must equal the rule id.",
|
|
83
|
+
"rationale": "Fixtures are attributed by the id inside the file, not by its name. A fixture carrying another rule's id is silently not counted, so a rule that ships one reads as a rule that shipped none. `verify` passes, because the FILE is there; `test` is where it bites."
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"rules": [
|
|
87
|
+
{
|
|
88
|
+
"engine": "sg",
|
|
89
|
+
"id": "no-eval-call",
|
|
90
|
+
"directory": ".taskless/rules/sg/no-eval-call",
|
|
91
|
+
"ruleFile": "no-eval-call.yml",
|
|
92
|
+
"prompt": "Create a rule that flags any call to `eval()` in TypeScript.\n\nExecuting a string at runtime is an injection risk, and it defeats every static\nanalysis the project runs: nothing downstream can see what the code will do.\nPrefer a parser, a lookup table, or an explicit dispatch.\n\nDeciding this needs only the expression itself. No other file has to be read,\nand no state has to be resolved.\n\nCode that should NOT be flagged:\n\n```ts\nconst parsed = JSON.parse(payload);\n```\n\nCode that SHOULD be flagged:\n\n```ts\nconst result = eval(payload);\n```\n",
|
|
93
|
+
"rule": [
|
|
94
|
+
{
|
|
95
|
+
"path": "no-eval-call.yml",
|
|
96
|
+
"content": "id: no-eval-call\nlanguage: TypeScript\nseverity: error\nmessage: \"`eval` executes arbitrary code at runtime; use a parser or a lookup table instead.\"\nnote: |-\n This is an example rule, shipped with the Taskless CLI to demonstrate the\n `sg` tier. It was not written for this repository.\n\n The evidence is one expression in one file, which is what makes this an `sg`\n rule rather than a runtime one: no other file has to be read to decide it.\nrule:\n pattern: eval($ARG)\n"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"tests": {
|
|
100
|
+
"grouping": "ast-grep-test",
|
|
101
|
+
"files": [
|
|
102
|
+
{
|
|
103
|
+
"path": ".tests/no-eval-call-test.yml",
|
|
104
|
+
"content": "id: no-eval-call\nvalid:\n - const parsed = JSON.parse(payload);\ninvalid:\n - const result = eval(payload);\n"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"engine": "vale",
|
|
111
|
+
"id": "prefer-use-over-utilize",
|
|
112
|
+
"directory": ".taskless/rules/vale/prefer-use-over-utilize",
|
|
113
|
+
"ruleFile": "prefer-use-over-utilize.yml",
|
|
114
|
+
"prompt": "Create a rule that flags the word \"utilize\" in markdown documentation and\nsuggests \"use\" instead.\n\n\"Utilize\" is longer than \"use\" and means the same thing in nearly every\nsentence a reader will meet. It should match case-insensitively and cover the\ninflected forms.\n\nThis is a prose rule. It applies to markdown, and it must not look at source\ncode.\n\nProse that should NOT be flagged:\n\n```md\nUse the installer to write the config file.\n```\n\nProse that SHOULD be flagged:\n\n```md\nUtilize the installer to write the config file.\n```\n",
|
|
115
|
+
"rule": [
|
|
116
|
+
{
|
|
117
|
+
"path": "prefer-use-over-utilize.yml",
|
|
118
|
+
"content": "extends: substitution\nmessage: \"Prefer '%s' over '%s'.\"\nlevel: warning\nignorecase: true\nswap:\n utilize: use\n utilizes: uses\n utilizing: using\n"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"path": ".vale.ini",
|
|
122
|
+
"content": "# Scoped to markdown, which is where prose rules belong. A demonstration rule\n# should never widen its own scope into a project's source.\n[**/*.md]\nBasedOnStyles =\nprefer-use-over-utilize.prefer-use-over-utilize = YES\n"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"tests": {
|
|
126
|
+
"grouping": "case-documents",
|
|
127
|
+
"files": [
|
|
128
|
+
{
|
|
129
|
+
"path": ".tests/pass/README.md",
|
|
130
|
+
"content": "# Setup\n\nUse the installer to write the config file.\n"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"path": ".tests/fail/README.md",
|
|
134
|
+
"content": "# Setup\n\nUtilize the installer to write the config file.\n"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"cases": [
|
|
138
|
+
{
|
|
139
|
+
"bucket": "pass",
|
|
140
|
+
"name": "README.md",
|
|
141
|
+
"path": ".tests/pass/README.md",
|
|
142
|
+
"files": [".tests/pass/README.md"]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"bucket": "fail",
|
|
146
|
+
"name": "README.md",
|
|
147
|
+
"path": ".tests/fail/README.md",
|
|
148
|
+
"files": [".tests/fail/README.md"]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"engine": "runtime",
|
|
155
|
+
"id": "env-keys-declared",
|
|
156
|
+
"directory": ".taskless/rules/runtime/env-keys-declared",
|
|
157
|
+
"ruleFile": "check.ts",
|
|
158
|
+
"prompt": "Create a rule that requires every environment variable read through\n`process.env` in a JavaScript or TypeScript file to have a matching key\ndeclared in the `.env` file at the repository root.\n\nReading a variable that is never declared is the failure worth catching: it is\n`undefined` at runtime rather than an error, so the program continues with a\nmissing value and fails somewhere else entirely.\n\nDeciding this needs both files at once — the read and the declaration — so it\ncannot be answered by matching a pattern within a single file.\n\nA repository that should NOT be flagged:\n\n```ts\n// src/config.ts\nexport const apiUrl = process.env.API_URL;\n```\n\n```\n# .env\nAPI_URL=https://api.example.com\n```\n\nA repository that SHOULD be flagged, on the second line only:\n\n```ts\n// src/config.ts\nexport const apiUrl = process.env.API_URL;\nexport const apiKey = process.env.API_KEY;\n```\n\n```\n# .env\nAPI_URL=https://api.example.com\n```\n",
|
|
159
|
+
"rule": [
|
|
160
|
+
{
|
|
161
|
+
"path": "check.ts",
|
|
162
|
+
"content": "/**\n * Every `process.env` read must have a matching key in the repository-root\n * `.env`.\n *\n * This is the demonstration rule, and its subject was chosen for one property:\n * the evidence spans two files. The read is in a source file, the declaration\n * is in `.env`, and no single-file pattern can compare them. A rule whose\n * evidence fits in one file is an `sg` rule, so a single-file subject would\n * demonstrate this tier by exercising none of what makes it a tier.\n *\n * It imports nothing from `@taskless/*`. The harness contract is structural —\n * a default-exported async function taking `(root, matches)` and returning\n * findings — so a delivered check never depends on a package being installed.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** One normalized ast-grep match, as the harness hands it over. */\ninterface Match {\n /** The capture rule's stable name, which is what a check branches on. */\n rule: string;\n /** Path relative to `root`. */\n file: string;\n line: number;\n column: number;\n /** Captured metavariables by name. */\n captures: Record<string, string>;\n}\n\n/** One result the check returns. */\ninterface Finding {\n file: string;\n line?: number;\n column?: number;\n message: string;\n severity?: \"error\" | \"warning\" | \"info\";\n}\n\n/**\n * Keys declared in the repository-root `.env`, or none when it is absent.\n *\n * A missing `.env` yields an empty set rather than an error: a project with no\n * `.env` at all has declared nothing, which is exactly what the empty set\n * means, and every read in it is then correctly reported.\n */\nfunction declaredKeys(root: string): Set<string> {\n let raw: string;\n try {\n raw = readFileSync(join(root, \".env\"), \"utf8\");\n } catch {\n return new Set();\n }\n\n const keys = new Set<string>();\n for (const line of raw.split(\"\\n\")) {\n const text = line.trim();\n if (text === \"\" || text.startsWith(\"#\") || text.startsWith(\"//\")) continue;\n const equals = text.indexOf(\"=\");\n // `<= 0` rather than `=== -1`: a line beginning with `=` has no name.\n if (equals <= 0) continue;\n keys.add(text.slice(0, equals).trim());\n }\n return keys;\n}\n\nexport default async function check(\n root: string,\n matches: Match[]\n): Promise<Finding[]> {\n const declared = declaredKeys(root);\n const findings: Finding[] = [];\n\n for (const match of matches) {\n // Branch on the capture rule's NAME, never on its id: the id is a\n // baked-in hash and is opaque to a check.\n if (match.rule !== \"env-read\") continue;\n\n const name = match.captures.VAR;\n if (name === undefined || declared.has(name)) continue;\n\n findings.push({\n file: match.file,\n line: match.line,\n column: match.column,\n message: `process.env.${name} is read here, but ${name} is not declared in .env`,\n severity: \"error\",\n });\n }\n\n return findings;\n}\n"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"path": "captures/env-read.yml",
|
|
166
|
+
"content": "# The syntactic narrow. It finds every `process.env.X` read; it cannot know\n# whether X is declared, because that answer lives in a different file. Deciding\n# is `check.ts`'s job, which is what makes this a runtime rule rather than an\n# `sg` one.\nid: env-read\nlanguage: TypeScript\nrule:\n pattern: process.env.$VAR\nmetadata:\n taskless:\n version: 1\n kind: runtime\n name: env-read\n check: check.ts\n match: anchor\n"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"tests": {
|
|
170
|
+
"grouping": "case-directories",
|
|
171
|
+
"files": [
|
|
172
|
+
{
|
|
173
|
+
"path": ".tests/pass/declared/src/config.ts",
|
|
174
|
+
"content": "// API_URL is read here and declared in .env, so this case is clean.\nexport const apiUrl = process.env.API_URL;\n"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"path": ".tests/pass/declared/.env",
|
|
178
|
+
"content": "API_URL=https://api.example.com\n"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"path": ".tests/fail/undeclared/src/config.ts",
|
|
182
|
+
"content": "// API_URL is declared; API_KEY is not, so the rule fires on the second.\nexport const apiUrl = process.env.API_URL;\nexport const apiKey = process.env.API_KEY;\n"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"path": ".tests/fail/undeclared/.env",
|
|
186
|
+
"content": "API_URL=https://api.example.com\n"
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"cases": [
|
|
190
|
+
{
|
|
191
|
+
"bucket": "pass",
|
|
192
|
+
"name": "declared",
|
|
193
|
+
"path": ".tests/pass/declared",
|
|
194
|
+
"files": [
|
|
195
|
+
".tests/pass/declared/src/config.ts",
|
|
196
|
+
".tests/pass/declared/.env"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"bucket": "fail",
|
|
201
|
+
"name": "undeclared",
|
|
202
|
+
"path": ".tests/fail/undeclared",
|
|
203
|
+
"files": [
|
|
204
|
+
".tests/fail/undeclared/src/config.ts",
|
|
205
|
+
".tests/fail/undeclared/.env"
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"signature": "1;h=sha-256;d=0000000000000000000000000000000000000000000000000000000000000000"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawnSync as v } from "node:child_process";
|
|
2
|
+
import { existsSync as u } from "node:fs";
|
|
3
|
+
import { createRequire as g } from "node:module";
|
|
4
|
+
import { resolve as i, dirname as c } from "node:path";
|
|
5
|
+
import { fileURLToPath as y } from "node:url";
|
|
6
|
+
const R = {
|
|
7
|
+
label: "ast-grep",
|
|
8
|
+
packagePrefix: "@ast-grep/cli",
|
|
9
|
+
toolchainSuffix: !0,
|
|
10
|
+
binaryNames: ["ast-grep", "sg"],
|
|
11
|
+
identity: /ast-grep/i
|
|
12
|
+
};
|
|
13
|
+
function m(e) {
|
|
14
|
+
const r = [process.platform, process.arch];
|
|
15
|
+
return e.toolchainSuffix && (process.platform === "linux" ? r.push("gnu") : process.platform === "win32" && r.push("msvc")), `${e.packagePrefix}-${r.join("-")}`;
|
|
16
|
+
}
|
|
17
|
+
function o(e) {
|
|
18
|
+
return process.platform === "win32" ? `${e}.exe` : e;
|
|
19
|
+
}
|
|
20
|
+
function p(e) {
|
|
21
|
+
return e.binaryNames.toReversed();
|
|
22
|
+
}
|
|
23
|
+
function b(e) {
|
|
24
|
+
return o(p(e)[0] ?? e.label);
|
|
25
|
+
}
|
|
26
|
+
function P(e, r) {
|
|
27
|
+
try {
|
|
28
|
+
const a = g(import.meta.url).resolve(
|
|
29
|
+
`${m(e)}/package.json`
|
|
30
|
+
);
|
|
31
|
+
return i(c(a), r);
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function h(e) {
|
|
37
|
+
const r = process.platform === "win32" ? ";" : ":";
|
|
38
|
+
for (const t of (process.env.PATH ?? "").split(r)) {
|
|
39
|
+
if (t === "") continue;
|
|
40
|
+
const a = i(t, e);
|
|
41
|
+
if (u(a)) return a;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function k(e, r) {
|
|
45
|
+
if (!u(r)) return !1;
|
|
46
|
+
const t = v(r, ["--version"], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
timeout: 5e3
|
|
49
|
+
});
|
|
50
|
+
return t.error !== void 0 || t.status !== 0 ? !1 : e.identity.test(`${t.stdout ?? ""}${t.stderr ?? ""}`);
|
|
51
|
+
}
|
|
52
|
+
function x(e) {
|
|
53
|
+
const r = i(
|
|
54
|
+
c(y(import.meta.url)),
|
|
55
|
+
"..",
|
|
56
|
+
"node_modules",
|
|
57
|
+
".bin"
|
|
58
|
+
), t = p(e), a = [
|
|
59
|
+
[
|
|
60
|
+
"platform-package",
|
|
61
|
+
m(e),
|
|
62
|
+
P(
|
|
63
|
+
e,
|
|
64
|
+
o(e.binaryNames[0] ?? e.label)
|
|
65
|
+
)
|
|
66
|
+
],
|
|
67
|
+
...t.map(
|
|
68
|
+
(n) => [
|
|
69
|
+
"node_modules/.bin",
|
|
70
|
+
"node_modules/.bin",
|
|
71
|
+
i(r, o(n))
|
|
72
|
+
]
|
|
73
|
+
),
|
|
74
|
+
...t.map(
|
|
75
|
+
(n) => ["PATH", "PATH", h(o(n))]
|
|
76
|
+
)
|
|
77
|
+
], l = [...new Set(a.map(([, n]) => n))];
|
|
78
|
+
for (const [n, , s] of a)
|
|
79
|
+
if (s !== void 0 && k(e, s))
|
|
80
|
+
return { path: s, source: n, tried: l };
|
|
81
|
+
return { path: void 0, source: void 0, tried: l };
|
|
82
|
+
}
|
|
83
|
+
const d = {
|
|
84
|
+
label: "vale",
|
|
85
|
+
packagePrefix: "@taskless/vale",
|
|
86
|
+
toolchainSuffix: !1,
|
|
87
|
+
binaryNames: ["vale"],
|
|
88
|
+
identity: /vale/i
|
|
89
|
+
};
|
|
90
|
+
let f;
|
|
91
|
+
function T() {
|
|
92
|
+
return f ??= x(d), f;
|
|
93
|
+
}
|
|
94
|
+
function _(e) {
|
|
95
|
+
return `Vale binary not found. Looked in: ${e.join(", ")}. Install a supported platform build, or put \`${b(d)}\` on your PATH. Other engines still ran.`;
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
R as A,
|
|
99
|
+
d as V,
|
|
100
|
+
m as a,
|
|
101
|
+
T as b,
|
|
102
|
+
h as f,
|
|
103
|
+
k as i,
|
|
104
|
+
b as p,
|
|
105
|
+
x as r,
|
|
106
|
+
_ as v
|
|
107
|
+
};
|