@taskless/cli 0.11.0 → 0.11.2

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 CHANGED
@@ -16,10 +16,11 @@ npx @taskless/cli
16
16
 
17
17
  Run with no arguments in a terminal to launch the installer, which detects the
18
18
  agent tools in your project (Claude Code, Cursor, OpenCode) and installs into
19
- each of them. For scripted installs, skip the prompts:
19
+ each of them. For scripted installs and upgrades, `init` runs the same install
20
+ without prompts, in a terminal or not:
20
21
 
21
22
  ```bash
22
- npx @taskless/cli init --no-interactive
23
+ npx @taskless/cli init
23
24
  ```
24
25
 
25
26
  New to Taskless? Run `npx @taskless/cli onboard` after installing. It walks your
@@ -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,108 @@
1
+ import { spawnSync as g } from "node:child_process";
2
+ import { existsSync as c } from "node:fs";
3
+ import { createRequire as y } from "node:module";
4
+ import { resolve as s, dirname as m } from "node:path";
5
+ import o from "node:process";
6
+ import { fileURLToPath as b } from "node:url";
7
+ const _ = {
8
+ label: "ast-grep",
9
+ packagePrefix: "@ast-grep/cli",
10
+ toolchainSuffix: !0,
11
+ binaryNames: ["ast-grep", "sg"],
12
+ identity: /ast-grep/i
13
+ };
14
+ function p(e) {
15
+ const t = [o.platform, o.arch];
16
+ return e.toolchainSuffix && (o.platform === "linux" ? t.push("gnu") : o.platform === "win32" && t.push("msvc")), `${e.packagePrefix}-${t.join("-")}`;
17
+ }
18
+ function i(e) {
19
+ return o.platform === "win32" ? `${e}.exe` : e;
20
+ }
21
+ function d(e) {
22
+ return e.binaryNames.toReversed();
23
+ }
24
+ function P(e) {
25
+ return i(d(e)[0] ?? e.label);
26
+ }
27
+ function h(e, t) {
28
+ try {
29
+ const a = y(import.meta.url).resolve(
30
+ `${p(e)}/package.json`
31
+ );
32
+ return s(m(a), t);
33
+ } catch {
34
+ return;
35
+ }
36
+ }
37
+ function k(e) {
38
+ const t = o.platform === "win32" ? ";" : ":";
39
+ for (const r of (o.env.PATH ?? "").split(t)) {
40
+ if (r === "") continue;
41
+ const a = s(r, e);
42
+ if (c(a)) return a;
43
+ }
44
+ }
45
+ function x(e, t) {
46
+ if (!c(t)) return !1;
47
+ const r = g(t, ["--version"], {
48
+ encoding: "utf8",
49
+ timeout: 5e3
50
+ });
51
+ return r.error !== void 0 || r.status !== 0 ? !1 : e.identity.test(`${r.stdout ?? ""}${r.stderr ?? ""}`);
52
+ }
53
+ function N(e) {
54
+ const t = s(
55
+ m(b(import.meta.url)),
56
+ "..",
57
+ "node_modules",
58
+ ".bin"
59
+ ), r = d(e), a = [
60
+ [
61
+ "platform-package",
62
+ p(e),
63
+ h(
64
+ e,
65
+ i(e.binaryNames[0] ?? e.label)
66
+ )
67
+ ],
68
+ ...r.map(
69
+ (n) => [
70
+ "node_modules/.bin",
71
+ "node_modules/.bin",
72
+ s(t, i(n))
73
+ ]
74
+ ),
75
+ ...r.map(
76
+ (n) => ["PATH", "PATH", k(i(n))]
77
+ )
78
+ ], f = [...new Set(a.map(([, n]) => n))];
79
+ for (const [n, , l] of a)
80
+ if (l !== void 0 && x(e, l))
81
+ return { path: l, source: n, tried: f };
82
+ return { path: void 0, source: void 0, tried: f };
83
+ }
84
+ const v = {
85
+ label: "vale",
86
+ packagePrefix: "@taskless/vale",
87
+ toolchainSuffix: !1,
88
+ binaryNames: ["vale"],
89
+ identity: /vale/i
90
+ };
91
+ let u;
92
+ function w() {
93
+ return u ??= N(v), u;
94
+ }
95
+ function H(e) {
96
+ return `Vale binary not found. Looked in: ${e.join(", ")}. Install a supported platform build, or put \`${P(v)}\` on your PATH. Other engines still ran.`;
97
+ }
98
+ export {
99
+ _ as A,
100
+ v as V,
101
+ p as a,
102
+ w as b,
103
+ k as f,
104
+ x as i,
105
+ P as p,
106
+ N as r,
107
+ H as v
108
+ };