tslaws 0.0.0-stage → 0.3.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/LICENSE +202 -0
- package/README.md +215 -2
- package/bin/tslaws.js +5 -0
- package/dist/adapters/angular.d.ts +42 -0
- package/dist/adapters/angular.js +40 -0
- package/dist/adapters/dom.d.ts +5 -0
- package/dist/adapters/dom.js +17 -0
- package/dist/adapters/element.d.ts +10 -0
- package/dist/adapters/element.js +28 -0
- package/dist/adapters/react.d.ts +24 -0
- package/dist/adapters/react.js +15 -0
- package/dist/api.d.ts +59 -0
- package/dist/api.js +103 -0
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +252 -0
- package/dist/browserApi.d.ts +62 -0
- package/dist/browserApi.js +139 -0
- package/dist/bundled.d.ts +17 -0
- package/dist/bundled.js +67 -0
- package/dist/check.d.ts +12 -0
- package/dist/check.js +269 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +360 -0
- package/dist/compile.d.ts +15 -0
- package/dist/compile.js +142 -0
- package/dist/component.d.ts +37 -0
- package/dist/component.js +99 -0
- package/dist/coverage.d.ts +40 -0
- package/dist/coverage.js +237 -0
- package/dist/e2e.d.ts +46 -0
- package/dist/e2e.js +322 -0
- package/dist/e2eApi.d.ts +80 -0
- package/dist/e2eApi.js +140 -0
- package/dist/generators.d.ts +18 -0
- package/dist/generators.js +184 -0
- package/dist/hints.d.ts +3 -0
- package/dist/hints.js +37 -0
- package/dist/init.d.ts +23 -0
- package/dist/init.js +139 -0
- package/dist/lock.d.ts +16 -0
- package/dist/lock.js +40 -0
- package/dist/mcp.d.ts +30 -0
- package/dist/mcp.js +301 -0
- package/dist/mutate.d.ts +54 -0
- package/dist/mutate.js +293 -0
- package/dist/promote.d.ts +38 -0
- package/dist/promote.js +217 -0
- package/dist/property.d.ts +37 -0
- package/dist/property.js +172 -0
- package/dist/propertyWorker.d.ts +1 -0
- package/dist/propertyWorker.js +24 -0
- package/dist/queries.d.ts +37 -0
- package/dist/queries.js +297 -0
- package/dist/regressions.d.ts +31 -0
- package/dist/regressions.js +153 -0
- package/dist/reporters.d.ts +22 -0
- package/dist/reporters.js +160 -0
- package/dist/settings.d.ts +16 -0
- package/dist/settings.js +15 -0
- package/dist/smt.d.ts +33 -0
- package/dist/smt.js +396 -0
- package/dist/symbolic.d.ts +200 -0
- package/dist/symbolic.js +1727 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.js +326 -0
- package/dist/uiPage.d.ts +1 -0
- package/dist/uiPage.js +4 -0
- package/hooks/AGENTS.snippet.md +10 -0
- package/hooks/claude-hook.mjs +35 -0
- package/hooks/claude-settings.example.json +8 -0
- package/package.json +85 -4
- package/ui/index.html +1132 -0
package/dist/bundled.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The compiler that npm installs with tslaws: one optional dependency per platform, `tslaws-compiler-<platform>`, holding
|
|
2
|
+
// the native executable and the TypeScript lib files beside it.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
export const platforms = [
|
|
7
|
+
{ name: 'darwin-arm64', os: 'darwin', cpu: 'arm64' },
|
|
8
|
+
{ name: 'darwin-x64', os: 'darwin', cpu: 'x64' },
|
|
9
|
+
{ name: 'linux-arm64-gnu', os: 'linux', cpu: 'arm64', libc: 'glibc' },
|
|
10
|
+
{ name: 'linux-arm64-musl', os: 'linux', cpu: 'arm64', libc: 'musl' },
|
|
11
|
+
{ name: 'linux-x64-gnu', os: 'linux', cpu: 'x64', libc: 'glibc' },
|
|
12
|
+
{ name: 'linux-x64-musl', os: 'linux', cpu: 'x64', libc: 'musl' },
|
|
13
|
+
{ name: 'win32-x64-msvc', os: 'win32', cpu: 'x64' },
|
|
14
|
+
];
|
|
15
|
+
export const packageName = (platform) => `tslaws-compiler-${platform.name}`;
|
|
16
|
+
export const executableName = (os = process.platform) => (os === 'win32' ? 'tsc-ts.exe' : 'tsc-ts');
|
|
17
|
+
function hostLibc() {
|
|
18
|
+
if (process.platform !== 'linux')
|
|
19
|
+
return undefined;
|
|
20
|
+
const report = process.report.getReport();
|
|
21
|
+
if (report.header.glibcVersionRuntime)
|
|
22
|
+
return 'glibc';
|
|
23
|
+
if (report.sharedObjects.some((path) => /(?:^|\/)ld-musl-|libc\.musl-/.test(path)))
|
|
24
|
+
return 'musl';
|
|
25
|
+
const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';
|
|
26
|
+
return existsSync(`/lib/ld-musl-${arch}.so.1`) ? 'musl' : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** The platform of this process, or undefined when there is no compiler build for it. */
|
|
29
|
+
export function hostPlatform() {
|
|
30
|
+
const libc = hostLibc();
|
|
31
|
+
return platforms.find((p) => p.os === process.platform && p.cpu === process.arch && (p.libc === undefined || p.libc === libc));
|
|
32
|
+
}
|
|
33
|
+
/** The compiler installed as an optional dependency of tslaws, when there is one for this machine. */
|
|
34
|
+
export function bundledCompiler() {
|
|
35
|
+
const platform = hostPlatform();
|
|
36
|
+
if (platform === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
try {
|
|
39
|
+
const manifest = createRequire(import.meta.url).resolve(`${packageName(platform)}/package.json`);
|
|
40
|
+
const executable = join(dirname(manifest), 'lib', executableName());
|
|
41
|
+
return existsSync(executable) ? executable : undefined;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The compiler built in a checkout of the tsc-laws repository (packages/tsc-ts/dist), found by walking up from `from`. */
|
|
48
|
+
export function checkoutCompiler(from) {
|
|
49
|
+
const name = process.platform === 'win32' ? 'tsc.exe' : 'tsc';
|
|
50
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
51
|
+
const candidate = join(dir, 'packages', 'tsc-ts', 'dist', name);
|
|
52
|
+
if (existsSync(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
if (dirname(dir) === dir)
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The version of this package, for messages. */
|
|
59
|
+
export function ownVersion() {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
62
|
+
.version;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CheckOptions, LawResult } from './types.ts';
|
|
2
|
+
export interface CheckReport {
|
|
3
|
+
results: LawResult[];
|
|
4
|
+
compileDiagnostics: string;
|
|
5
|
+
compileOk: boolean;
|
|
6
|
+
lockMissing: boolean;
|
|
7
|
+
/** Whether every law holds and nothing is open, failed or tampered with. */
|
|
8
|
+
ok: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The `@tslaws-exempt` functions of the project, for `tslaws accept` to record. */
|
|
11
|
+
export declare function exemptionsOf(options: CheckOptions): string[];
|
|
12
|
+
export declare function check(options: CheckOptions, enforceLock: boolean): Promise<CheckReport>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { build, readManifest } from "./compile.js";
|
|
6
|
+
import { compilerOptions, declarationFiles, isDiscard, jsPath, runLawGuarded, typesOf, } from "./property.js";
|
|
7
|
+
import { analyzeCoverage, coverFiles, readConfig } from "./coverage.js";
|
|
8
|
+
import { compareLock, lawKey, readLock } from "./lock.js";
|
|
9
|
+
import { proveLaw } from "./smt.js";
|
|
10
|
+
import { decodeExample, loadExamples, regressionMessage, saveExample } from "./regressions.js";
|
|
11
|
+
import { closeBrowser, runBrowserLaw } from "./browser.js";
|
|
12
|
+
import { ensureServer, importsBoth, lawMode, runE2eLaw, stopServer } from "./e2e.js";
|
|
13
|
+
/** The `@tslaws-exempt` functions of the project, for `tslaws accept` to record. */
|
|
14
|
+
export function exemptionsOf(options) {
|
|
15
|
+
const covered = coverFiles(options.cwd, readConfig(options.cwd).cover ?? []);
|
|
16
|
+
const compiled = [...new Set([...options.files, ...covered])];
|
|
17
|
+
const built = build(options.compiler, compiled, options.cwd);
|
|
18
|
+
const program = ts.createProgram(declarationFiles(built.outDir), compilerOptions);
|
|
19
|
+
return analyzeCoverage({
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
outDir: built.outDir,
|
|
22
|
+
lawFiles: options.files,
|
|
23
|
+
coverFiles: covered,
|
|
24
|
+
program,
|
|
25
|
+
}).exempt;
|
|
26
|
+
}
|
|
27
|
+
export async function check(options, enforceLock) {
|
|
28
|
+
const laws = readManifest(options.compiler, options.files, options.cwd);
|
|
29
|
+
const results = [];
|
|
30
|
+
const blocked = new Set();
|
|
31
|
+
const add = (law, status, method, extra = {}) => {
|
|
32
|
+
const result = { file: law.file, name: law.name, line: law.line, status, method, ...extra };
|
|
33
|
+
results.push(result);
|
|
34
|
+
blocked.add(lawKey(law.file, law.name));
|
|
35
|
+
options.onEvent?.({ type: 'result', result });
|
|
36
|
+
};
|
|
37
|
+
options.onEvent?.({ type: 'plan', laws: laws.map((l) => ({ file: l.file, name: l.name, line: l.line })) });
|
|
38
|
+
let lockMissing = false;
|
|
39
|
+
let lock;
|
|
40
|
+
if (enforceLock) {
|
|
41
|
+
lock = readLock(options.lockFile);
|
|
42
|
+
lockMissing = lock === undefined;
|
|
43
|
+
const byKey = new Map(laws.map((l) => [lawKey(l.file, l.name), l]));
|
|
44
|
+
for (const violation of compareLock(lock?.laws, laws)) {
|
|
45
|
+
const law = byKey.get(violation.key);
|
|
46
|
+
if (law === undefined) {
|
|
47
|
+
const [file, name] = violation.key.split('::');
|
|
48
|
+
results.push({
|
|
49
|
+
file,
|
|
50
|
+
name,
|
|
51
|
+
line: 0,
|
|
52
|
+
status: 'tampered',
|
|
53
|
+
method: 'lock',
|
|
54
|
+
message: 'law was removed since the lock was accepted',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
add(law, 'tampered', 'lock', {
|
|
59
|
+
message: violation.reason === 'added'
|
|
60
|
+
? 'law is not in the lock; a person must run `tslaws accept`'
|
|
61
|
+
: 'law changed since the lock was accepted',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const law of laws) {
|
|
67
|
+
if (law.problems.length > 0 && !blocked.has(lawKey(law.file, law.name)))
|
|
68
|
+
add(law, 'error', 'manifest', { message: law.problems.join('; ') });
|
|
69
|
+
}
|
|
70
|
+
const covered = coverFiles(options.cwd, readConfig(options.cwd).cover ?? []);
|
|
71
|
+
const compiled = [...new Set([...options.files, ...covered])];
|
|
72
|
+
const built = build(options.compiler, compiled, options.cwd);
|
|
73
|
+
const program = ts.createProgram(declarationFiles(built.outDir), compilerOptions);
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
for (const law of laws) {
|
|
76
|
+
if (options.only !== undefined && !options.only.includes(law.name))
|
|
77
|
+
continue;
|
|
78
|
+
if (blocked.has(lawKey(law.file, law.name)))
|
|
79
|
+
continue;
|
|
80
|
+
options.onEvent?.({ type: 'start', file: law.file, name: law.name });
|
|
81
|
+
let types;
|
|
82
|
+
try {
|
|
83
|
+
types = typesOf(program, built.outDir, law);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
add(law, 'error', 'generators', { message: error.message });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let source = '';
|
|
90
|
+
try {
|
|
91
|
+
source = readFileSync(join(options.cwd, law.file), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// The compiler already reported an unreadable file.
|
|
95
|
+
}
|
|
96
|
+
if (importsBoth(source)) {
|
|
97
|
+
add(law, 'error', 'manifest', {
|
|
98
|
+
message: `${law.file}: a browser laws file is either component level (tslaws/browser) or end-to-end (tslaws/e2e), not both`,
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const mode = lawMode(law.file, source);
|
|
103
|
+
const browser = mode === 'component';
|
|
104
|
+
const e2e = mode === 'e2e';
|
|
105
|
+
const method = browser ? 'browser' : e2e ? 'e2e' : 'property';
|
|
106
|
+
let fn;
|
|
107
|
+
// A browser law touches the DOM when it loads, so only the browser loads it.
|
|
108
|
+
if (!browser) {
|
|
109
|
+
try {
|
|
110
|
+
fn = require(jsPath(built.outDir, law.file))[law.name];
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
add(law, 'error', 'load', { message: `cannot load compiled law: ${error.message}` });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (typeof fn !== 'function') {
|
|
117
|
+
add(law, 'error', 'load', { message: 'compiled module does not export the law' });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let details;
|
|
122
|
+
// Counterexamples found before are tried first.
|
|
123
|
+
let lawOptions = options.noRegressions
|
|
124
|
+
? options
|
|
125
|
+
: { ...options, regressionExamples: loadExamples(options.cwd, law.file, law.name) };
|
|
126
|
+
// End-to-end inputs are expensive: fewer runs and a longer limit, unless the command line says otherwise.
|
|
127
|
+
const e2eConfig = e2e ? readConfig(options.cwd).e2e : undefined;
|
|
128
|
+
let limitMs = options.propertyTimeoutMs ?? 30000;
|
|
129
|
+
if (e2e) {
|
|
130
|
+
if (!options.explicit?.includes('runs'))
|
|
131
|
+
lawOptions = { ...lawOptions, runs: e2eConfig?.runs ?? 10 };
|
|
132
|
+
if (!options.explicit?.includes('property-timeout'))
|
|
133
|
+
limitMs = e2eConfig?.timeoutMs ?? 120000;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
details = e2e
|
|
137
|
+
? await runE2eLaw({ file: law.file, name: law.name, fn: fn, generators: types.generators }, lawOptions, await ensureServer(e2eConfig ?? {}, options.cwd), limitMs)
|
|
138
|
+
: browser
|
|
139
|
+
? await runBrowserLaw({ file: law.file, name: law.name, jsFile: jsPath(built.outDir, law.file), generators: types.generators }, lawOptions)
|
|
140
|
+
: await runLawGuarded(built.outDir, law, lawOptions);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
add(law, 'error', method, { message: error.message });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (details === 'timeout') {
|
|
147
|
+
add(law, 'error', method, {
|
|
148
|
+
message: `property test ran longer than ${limitMs / 1000}s; the law or the code it calls may not terminate`,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (details.failed) {
|
|
153
|
+
if (details.counterexample === undefined) {
|
|
154
|
+
add(law, 'open', method, {
|
|
155
|
+
runs: details.numRuns,
|
|
156
|
+
seed: details.seed,
|
|
157
|
+
message: 'assume() discards nearly every generated input',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
if (!options.noRegressions && details.encoded !== undefined && !details.regression)
|
|
162
|
+
saveExample(options.cwd, law.file, law.name, decodeExample(details.encoded));
|
|
163
|
+
const reason = details.message === undefined || details.message.startsWith('Property failed by returning false')
|
|
164
|
+
? 'returned false'
|
|
165
|
+
: `threw: ${details.message}`;
|
|
166
|
+
add(law, 'failed', method, {
|
|
167
|
+
runs: details.numRuns,
|
|
168
|
+
seed: details.seed,
|
|
169
|
+
counterexample: details.counterexample,
|
|
170
|
+
screenshot: details.screenshot,
|
|
171
|
+
message: details.regression ? `${regressionMessage} (${details.counterexample}): ${reason}` : reason,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Browser and async laws are tested, never proved: the prover cannot model a DOM or an await.
|
|
177
|
+
const unprovable = browser || e2e ? 'runs in a browser' : fn.constructor.name === 'AsyncFunction' ? 'is async' : undefined;
|
|
178
|
+
if (options.noProve || unprovable !== undefined) {
|
|
179
|
+
add(law, law.proved ? 'open' : 'tested', method, {
|
|
180
|
+
runs: details.numRuns,
|
|
181
|
+
seed: details.seed,
|
|
182
|
+
message: law.proved
|
|
183
|
+
? unprovable !== undefined
|
|
184
|
+
? `marked proved but the law ${unprovable}`
|
|
185
|
+
: 'marked proved but proving is disabled'
|
|
186
|
+
: undefined,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const holds = (inputs) => {
|
|
191
|
+
try {
|
|
192
|
+
return fn(...inputs) !== false;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return isDiscard(error);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const proof = await proveLaw({
|
|
199
|
+
law,
|
|
200
|
+
shapes: types.shapes,
|
|
201
|
+
jsFile: jsPath(built.outDir, law.file),
|
|
202
|
+
bound: options.bound,
|
|
203
|
+
timeoutMs: options.proofTimeoutMs,
|
|
204
|
+
holds,
|
|
205
|
+
});
|
|
206
|
+
if (proof.kind === 'proved')
|
|
207
|
+
add(law, 'proved', 'smt', { runs: details.numRuns, seed: details.seed });
|
|
208
|
+
else if (proof.kind === 'bounded') {
|
|
209
|
+
// `law proved` promises every input; a bounded proof is the most Z3 could show.
|
|
210
|
+
const message = `holds within bounds: ${proof.limits.join('; ')}`;
|
|
211
|
+
if (law.proved)
|
|
212
|
+
add(law, 'open', 'smt', {
|
|
213
|
+
runs: details.numRuns,
|
|
214
|
+
seed: details.seed,
|
|
215
|
+
message: `marked proved but only ${message}`,
|
|
216
|
+
});
|
|
217
|
+
else
|
|
218
|
+
add(law, 'bounded', 'smt', { runs: details.numRuns, seed: details.seed, message });
|
|
219
|
+
}
|
|
220
|
+
else if (proof.kind === 'refuted')
|
|
221
|
+
add(law, 'failed', 'smt', { counterexample: proof.model, message: 'refuted by the prover' });
|
|
222
|
+
else if (law.proved)
|
|
223
|
+
add(law, 'open', 'smt', {
|
|
224
|
+
runs: details.numRuns,
|
|
225
|
+
seed: details.seed,
|
|
226
|
+
message: `marked proved but not proved: ${proof.reason}`,
|
|
227
|
+
});
|
|
228
|
+
else
|
|
229
|
+
add(law, 'tested', 'property', { runs: details.numRuns, seed: details.seed, message: proof.reason });
|
|
230
|
+
}
|
|
231
|
+
if (enforceLock) {
|
|
232
|
+
const coverage = analyzeCoverage({
|
|
233
|
+
cwd: options.cwd,
|
|
234
|
+
outDir: built.outDir,
|
|
235
|
+
lawFiles: options.files,
|
|
236
|
+
coverFiles: covered,
|
|
237
|
+
program,
|
|
238
|
+
});
|
|
239
|
+
for (const { file, name } of coverage.uncovered) {
|
|
240
|
+
results.push({
|
|
241
|
+
file,
|
|
242
|
+
name,
|
|
243
|
+
line: 0,
|
|
244
|
+
status: 'uncovered',
|
|
245
|
+
method: 'coverage',
|
|
246
|
+
message: 'no law reaches this exported function; add a law, or a person can mark it @tslaws-exempt and accept',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const key of coverage.exempt) {
|
|
250
|
+
if (lock?.exempt.includes(key) === true)
|
|
251
|
+
continue;
|
|
252
|
+
const [file, name] = key.split('::');
|
|
253
|
+
results.push({
|
|
254
|
+
file,
|
|
255
|
+
name,
|
|
256
|
+
line: 0,
|
|
257
|
+
status: 'tampered',
|
|
258
|
+
method: 'lock',
|
|
259
|
+
message: 'exempted from coverage but not in the lock; a person must run `tslaws accept`',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
await closeBrowser();
|
|
264
|
+
await stopServer();
|
|
265
|
+
const ok = built.ok &&
|
|
266
|
+
!lockMissing &&
|
|
267
|
+
results.every((r) => r.status === 'proved' || r.status === 'bounded' || r.status === 'tested');
|
|
268
|
+
return { results, compileDiagnostics: built.diagnostics, compileOk: built.ok, lockMissing, ok };
|
|
269
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(args: string[]): Promise<number>;
|