@milesroxas/design-hub 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +80 -2
- package/client.d.ts +6 -0
- package/configs/biome/biome.json +64 -0
- package/configs/tsconfig.project.json +17 -0
- package/core/bytes.test.ts +14 -0
- package/core/bytes.ts +14 -0
- package/core/catalog.test.ts +44 -0
- package/core/catalog.ts +43 -0
- package/core/checks.test.ts +23 -0
- package/core/checks.ts +72 -0
- package/core/contacts.test.ts +31 -0
- package/core/contacts.ts +38 -0
- package/core/doc.ts +4 -0
- package/core/feature.ts +6 -0
- package/core/fixtures.test.ts +33 -0
- package/core/fixtures.ts +69 -0
- package/core/frontmatter.ts +15 -0
- package/core/html.ts +3 -0
- package/core/image.test.ts +22 -0
- package/core/image.ts +37 -0
- package/core/jev.test.ts +66 -0
- package/core/jev.ts +77 -0
- package/core/review/model.ts +189 -0
- package/core/review/policy.ts +26 -0
- package/core/review/service.test.ts +163 -0
- package/core/review/service.ts +309 -0
- package/core/review/store.ts +51 -0
- package/core/review/triage.test.ts +41 -0
- package/core/review/triage.ts +58 -0
- package/core/states.test.ts +36 -0
- package/core/states.ts +34 -0
- package/dist/core/bytes.d.ts +5 -0
- package/dist/core/bytes.js +9 -0
- package/dist/core/catalog.d.ts +17 -0
- package/dist/core/catalog.js +24 -0
- package/dist/core/checks.d.ts +41 -0
- package/dist/core/checks.js +53 -0
- package/dist/core/contacts.d.ts +9 -0
- package/dist/core/contacts.js +34 -0
- package/dist/core/doc.d.ts +11 -0
- package/dist/core/doc.js +1 -0
- package/dist/core/feature.d.ts +3 -0
- package/dist/core/feature.js +4 -0
- package/dist/core/fixtures.d.ts +19 -0
- package/dist/core/fixtures.js +52 -0
- package/dist/core/frontmatter.d.ts +4 -0
- package/dist/core/frontmatter.js +15 -0
- package/dist/core/html.d.ts +2 -0
- package/dist/core/html.js +2 -0
- package/dist/core/image.d.ts +6 -0
- package/dist/core/image.js +34 -0
- package/dist/core/jev.d.ts +54 -0
- package/dist/core/jev.js +52 -0
- package/dist/core/review/model.d.ts +85 -0
- package/dist/core/review/model.js +112 -0
- package/dist/core/review/policy.d.ts +3 -0
- package/dist/core/review/policy.js +19 -0
- package/dist/core/review/service.d.ts +79 -0
- package/dist/core/review/service.js +245 -0
- package/dist/core/review/store.d.ts +20 -0
- package/dist/core/review/store.js +37 -0
- package/dist/core/review/triage.d.ts +6 -0
- package/dist/core/review/triage.js +45 -0
- package/dist/core/states.d.ts +6 -0
- package/dist/core/states.js +29 -0
- package/dist/scripts/bootstrap.d.ts +51 -0
- package/dist/scripts/bootstrap.js +278 -0
- package/dist/scripts/calibrate.d.ts +127 -0
- package/dist/scripts/calibrate.js +164 -0
- package/dist/scripts/check.d.ts +25 -0
- package/dist/scripts/check.js +30 -0
- package/dist/scripts/cli.d.ts +2 -0
- package/dist/scripts/cli.js +73 -0
- package/dist/scripts/commands.d.ts +25 -0
- package/dist/scripts/commands.js +49 -0
- package/dist/scripts/comments.d.ts +63 -0
- package/dist/scripts/comments.js +152 -0
- package/dist/scripts/dev.d.ts +46 -0
- package/dist/scripts/dev.js +84 -0
- package/dist/scripts/elyx.d.ts +61 -0
- package/dist/scripts/elyx.js +195 -0
- package/dist/scripts/fixtures-pull.d.ts +60 -0
- package/dist/scripts/fixtures-pull.js +121 -0
- package/dist/scripts/guard.d.ts +58 -0
- package/dist/scripts/guard.js +201 -0
- package/dist/scripts/hook.d.ts +18 -0
- package/dist/scripts/hook.js +46 -0
- package/dist/scripts/lib/bin.d.ts +4 -0
- package/dist/scripts/lib/bin.js +20 -0
- package/dist/scripts/lib/catalog.d.ts +7 -0
- package/dist/scripts/lib/catalog.js +18 -0
- package/dist/scripts/lib/cli.d.ts +75 -0
- package/dist/scripts/lib/cli.js +88 -0
- package/dist/scripts/lib/css-tokens.d.ts +13 -0
- package/dist/scripts/lib/css-tokens.js +67 -0
- package/dist/scripts/lib/judge.d.ts +24 -0
- package/dist/scripts/lib/judge.js +60 -0
- package/dist/scripts/lib/ports.d.ts +35 -0
- package/dist/scripts/lib/ports.js +113 -0
- package/dist/scripts/lib/review.d.ts +7 -0
- package/dist/scripts/lib/review.js +22 -0
- package/dist/scripts/lib/rules.d.ts +12 -0
- package/dist/scripts/lib/rules.js +20 -0
- package/dist/scripts/lib/vercel.d.ts +17 -0
- package/dist/scripts/lib/vercel.js +53 -0
- package/dist/scripts/new.d.ts +47 -0
- package/dist/scripts/new.js +113 -0
- package/dist/scripts/people.d.ts +59 -0
- package/dist/scripts/people.js +73 -0
- package/dist/scripts/preflight.d.ts +35 -0
- package/dist/scripts/preflight.js +251 -0
- package/dist/scripts/release-check.d.ts +19 -0
- package/dist/scripts/release-check.js +39 -0
- package/dist/scripts/skill-hint.d.ts +32 -0
- package/dist/scripts/skill-hint.js +103 -0
- package/dist/scripts/skills.d.ts +13 -0
- package/dist/scripts/skills.js +34 -0
- package/dist/scripts/visual-match.d.ts +46 -0
- package/dist/scripts/visual-match.js +93 -0
- package/dist/server/api.d.ts +22 -0
- package/dist/server/api.js +143 -0
- package/dist/server/checks.d.ts +4 -0
- package/dist/server/checks.js +22 -0
- package/dist/server/dev.d.ts +2 -0
- package/dist/server/dev.js +54 -0
- package/dist/server/env.d.ts +8 -0
- package/dist/server/env.js +57 -0
- package/dist/server/fake-upstash.d.ts +1 -0
- package/dist/server/fake-upstash.js +37 -0
- package/dist/server/file-kv.d.ts +2 -0
- package/dist/server/file-kv.js +24 -0
- package/dist/server/local.d.ts +4 -0
- package/dist/server/local.js +25 -0
- package/dist/server/paths.d.ts +22 -0
- package/dist/server/paths.js +81 -0
- package/dist/server/session.d.ts +22 -0
- package/dist/server/session.js +110 -0
- package/dist/server/upstash.d.ts +7 -0
- package/dist/server/upstash.js +41 -0
- package/dist/vercel/index.d.ts +5 -0
- package/dist/vercel/index.js +12 -0
- package/dist/vercel/proxy.d.ts +11 -0
- package/dist/vercel/proxy.js +238 -0
- package/dist/vite/kit.d.ts +4 -0
- package/dist/vite/kit.js +35 -0
- package/dist/vite/markdown.d.ts +2 -0
- package/dist/vite/markdown.js +212 -0
- package/dist/vite/plugin.d.ts +15 -0
- package/dist/vite/plugin.js +84 -0
- package/dist/vite/registry.d.ts +7 -0
- package/dist/vite/registry.js +70 -0
- package/kit/Icon.tsx +39 -0
- package/kit/Space.tsx +25 -0
- package/kit/Variants.tsx +134 -0
- package/kit/ViewTools.tsx +34 -0
- package/kit/base.css +27 -0
- package/kit/dials.css +98 -0
- package/kit/dials.tsx +322 -0
- package/kit/fixtures.ts +3 -0
- package/kit/format.ts +13 -0
- package/kit/harness.css +99 -0
- package/kit/hooks.ts +35 -0
- package/kit/icons.ts +329 -0
- package/kit/motion.ts +18 -0
- package/kit/prefs.ts +111 -0
- package/kit/space.css +12 -0
- package/kit/ui/Button.tsx +135 -0
- package/kit/ui/Menu.tsx +386 -0
- package/kit/ui/Overlay.tsx +300 -0
- package/kit/ui/Toast.tsx +187 -0
- package/kit/ui/index.ts +13 -0
- package/kit/ui/layer.ts +51 -0
- package/kit/ui/ui.css +238 -0
- package/package.json +87 -3
- package/shell/Guide.tsx +160 -0
- package/shell/Index.tsx +301 -0
- package/shell/Notice.tsx +27 -0
- package/shell/Prototype.tsx +31 -0
- package/shell/a11y.ts +50 -0
- package/shell/doc.d.ts +7 -0
- package/shell/doc.ts +3 -0
- package/shell/guide.css +133 -0
- package/shell/index.html +17 -0
- package/shell/main.tsx +53 -0
- package/shell/public/favicon.svg +1 -0
- package/shell/registry.ts +21 -0
- package/shell/registry.virtual.d.ts +10 -0
- package/shell/review/Review.tsx +989 -0
- package/shell/review/anchor.ts +132 -0
- package/shell/review/client.ts +165 -0
- package/shell/review/mount.tsx +12 -0
- package/shell/review/review.css +296 -0
- package/shell/shell.css +160 -0
- package/shell/system.css +238 -0
- package/skills/miles-conductor/SKILL.md +37 -0
- package/skills/miles-deploy/SKILL.md +67 -0
- package/skills/miles-design-hub/SKILL.md +62 -0
- package/skills/miles-get-hostile/SKILL.md +61 -0
- package/skills/miles-new-feature/SKILL.md +40 -0
- package/skills/miles-paper-to-prototype/SKILL.md +46 -0
- package/skills/miles-port/SKILL.md +59 -0
- package/skills/miles-port/references/astro.md +35 -0
- package/skills/miles-port/references/next.md +36 -0
- package/skills/miles-prototype/SKILL.md +83 -0
- package/skills/miles-prototype/references/accessibility.md +48 -0
- package/skills/miles-prototype/references/motion.md +59 -0
- package/skills/miles-prototype/references/performance.md +45 -0
- package/skills/miles-review/SKILL.md +39 -0
- package/templates/app/README.md +41 -0
- package/templates/app/index.tsx +32 -0
- package/templates/app/styles.css +1 -0
- package/templates/conductor/settings.toml +29 -0
- package/templates/guide.md +38 -0
- package/templates/space/README.md +37 -0
- package/templates/space/index.tsx +43 -0
- package/templates/space/styles.css +13 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
// The mechanical setup of a project: what a prose step would skip or
|
|
4
|
+
// improvise on some runs, where the failure is silent (a port another project
|
|
5
|
+
// owns, a missing env var). Judgment stays in the skills.
|
|
6
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { readEnv, writeEnv } from '../server/env.js';
|
|
12
|
+
import { ROOT, TEMPLATES } from '../server/paths.js';
|
|
13
|
+
import { CliError } from './lib/cli.js';
|
|
14
|
+
import { holderOf, pickPort, readClaims, writeClaim } from './lib/ports.js';
|
|
15
|
+
import { ENVIRONMENTS, envNames, linkedProject, STORE_ENV, setEnv, vercel, } from './lib/vercel.js';
|
|
16
|
+
export const spec = {
|
|
17
|
+
name: 'bootstrap',
|
|
18
|
+
summary: 'Set the project up on this machine, and optionally on Vercel. Safe to run again.',
|
|
19
|
+
flags: {
|
|
20
|
+
port: {
|
|
21
|
+
type: 'number',
|
|
22
|
+
description: 'Use this port. Refused when another process holds it or another project claims it.',
|
|
23
|
+
},
|
|
24
|
+
'no-install': { type: 'boolean', description: 'Skip pnpm install.' },
|
|
25
|
+
'typesafe-key': { type: 'string', description: "Turn TypeSafe's Jev checks on with this API key." },
|
|
26
|
+
conductor: { type: 'boolean', description: 'Write .conductor/settings.toml for Conductor workspaces.' },
|
|
27
|
+
branch: { type: 'string', default: 'main', description: 'The branch Conductor workspaces land on.' },
|
|
28
|
+
vercel: { type: 'string', description: 'Link this Vercel project and set the password gate env on it.' },
|
|
29
|
+
'gate-password': {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The share password (8+ characters). Given again, it rotates.',
|
|
32
|
+
},
|
|
33
|
+
review: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
description: 'Provision the review store (Upstash Redis) on the Vercel project.',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
output: '{ ok, node, port, install, typesafe, conductor?, vercel?, share?, review?, warnings }',
|
|
39
|
+
};
|
|
40
|
+
const MIN_NODE = [22, 18];
|
|
41
|
+
const UPSTASH = 'upstash/upstash-kv';
|
|
42
|
+
function run(cmd, argv, cwd = ROOT) {
|
|
43
|
+
const out = spawnSync(cmd, argv, { cwd, encoding: 'utf8', env: process.env });
|
|
44
|
+
if (out.error)
|
|
45
|
+
throw new CliError(`${cmd} is not installed.`, { command: [cmd, ...argv].join(' ') });
|
|
46
|
+
if (out.status !== 0)
|
|
47
|
+
throw new CliError(`${cmd} ${argv[0]} failed.`, {
|
|
48
|
+
command: [cmd, ...argv].join(' '),
|
|
49
|
+
stderr: out.stderr.trim(),
|
|
50
|
+
});
|
|
51
|
+
return out.stdout;
|
|
52
|
+
}
|
|
53
|
+
function checkNode() {
|
|
54
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
55
|
+
if (major < MIN_NODE[0] || (major === MIN_NODE[0] && minor < MIN_NODE[1]))
|
|
56
|
+
throw new CliError(`Node ${MIN_NODE.join('.')} or newer is needed.`, {
|
|
57
|
+
found: process.versions.node,
|
|
58
|
+
fix: 'Install the current LTS from https://nodejs.org or `brew install node`.',
|
|
59
|
+
});
|
|
60
|
+
return process.versions.node;
|
|
61
|
+
}
|
|
62
|
+
function choosePort(wanted, current, warnings) {
|
|
63
|
+
const conductor = process.env.CONDUCTOR_PORT;
|
|
64
|
+
if (conductor) {
|
|
65
|
+
// Conductor owns its workspace ports; they never go in the machine registry.
|
|
66
|
+
const port = Number(conductor);
|
|
67
|
+
const held = holderOf(port);
|
|
68
|
+
if (held && held.cwd !== ROOT)
|
|
69
|
+
throw new CliError(`Conductor's port ${port} is taken.`, {
|
|
70
|
+
holder: held,
|
|
71
|
+
fix: 'Stop that server; never use another port.',
|
|
72
|
+
});
|
|
73
|
+
return port;
|
|
74
|
+
}
|
|
75
|
+
const choice = pickPort({ dir: ROOT, wanted, current, claims: readClaims(), holder: holderOf });
|
|
76
|
+
if (!choice.ok)
|
|
77
|
+
throw new CliError(choice.error, choice.detail);
|
|
78
|
+
warnings.push(...choice.warnings);
|
|
79
|
+
writeClaim(ROOT, choice.port);
|
|
80
|
+
return choice.port;
|
|
81
|
+
}
|
|
82
|
+
/** Keeps .claude/launch.json on the same port, when the project has one. */
|
|
83
|
+
function writeLaunch(port) {
|
|
84
|
+
const file = join(ROOT, '.claude', 'launch.json');
|
|
85
|
+
if (!existsSync(file))
|
|
86
|
+
return;
|
|
87
|
+
const launch = JSON.parse(readFileSync(file, 'utf8'));
|
|
88
|
+
for (const c of launch.configurations) {
|
|
89
|
+
if ('port' in c)
|
|
90
|
+
c.port = port;
|
|
91
|
+
if (c.url)
|
|
92
|
+
c.url = c.url.replace(/localhost:\d+/, `localhost:${port}`);
|
|
93
|
+
}
|
|
94
|
+
writeFileSync(file, `${JSON.stringify(launch, null, 2)}\n`);
|
|
95
|
+
}
|
|
96
|
+
function writeConductor(branch, warnings) {
|
|
97
|
+
if (!/^[\w./-]+$/.test(branch))
|
|
98
|
+
throw new CliError('--branch takes a branch name.', { got: branch });
|
|
99
|
+
const file = join(ROOT, '.conductor', 'settings.toml');
|
|
100
|
+
const text = readFileSync(join(TEMPLATES, 'conductor', 'settings.toml'), 'utf8').replaceAll('{{branch}}', branch);
|
|
101
|
+
if (existsSync(file)) {
|
|
102
|
+
if (readFileSync(file, 'utf8') !== text)
|
|
103
|
+
warnings.push('.conductor/settings.toml differs from the template; kept as is.');
|
|
104
|
+
return 'kept';
|
|
105
|
+
}
|
|
106
|
+
mkdirSync(join(ROOT, '.conductor'), { recursive: true });
|
|
107
|
+
writeFileSync(file, text);
|
|
108
|
+
return 'written';
|
|
109
|
+
}
|
|
110
|
+
function cli(args, input) {
|
|
111
|
+
const out = vercel(ROOT, args, input);
|
|
112
|
+
if (!out)
|
|
113
|
+
throw new CliError('The Vercel CLI is not installed.', { fix: 'npm i -g vercel, then vercel login.' });
|
|
114
|
+
if (out.status !== 0)
|
|
115
|
+
throw new CliError(`vercel ${args.slice(0, 2).join(' ')} failed.`, {
|
|
116
|
+
stderr: out.stderr.trim().slice(-600),
|
|
117
|
+
fix: /log ?in|credentials|token/i.test(out.stderr)
|
|
118
|
+
? 'Ask the user to run `vercel login` in their terminal.'
|
|
119
|
+
: undefined,
|
|
120
|
+
});
|
|
121
|
+
return out.stdout;
|
|
122
|
+
}
|
|
123
|
+
function link(project, warnings) {
|
|
124
|
+
const current = linkedProject(ROOT);
|
|
125
|
+
if (current === project)
|
|
126
|
+
return 'kept';
|
|
127
|
+
if (current)
|
|
128
|
+
warnings.push(`.vercel/project.json pointed at ${current}; relinked to ${project}.`);
|
|
129
|
+
cli(['link', '--yes', '--project', project]);
|
|
130
|
+
return 'ran';
|
|
131
|
+
}
|
|
132
|
+
/** Sets each value where it is missing; `rotate` names the ones given again, which replace what is there. */
|
|
133
|
+
function ensureEnv(values, rotate) {
|
|
134
|
+
const state = {};
|
|
135
|
+
for (const target of ENVIRONMENTS) {
|
|
136
|
+
const names = envNames(ROOT, target);
|
|
137
|
+
if (!names)
|
|
138
|
+
throw new CliError(`vercel env ls ${target} failed.`, { fix: 'vercel login, then run this again.' });
|
|
139
|
+
for (const [name, value] of Object.entries(values)) {
|
|
140
|
+
const key = `${name}:${target}`;
|
|
141
|
+
const exists = names.has(name);
|
|
142
|
+
if (exists && !rotate.has(name)) {
|
|
143
|
+
state[key] = 'kept';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const out = setEnv(ROOT, name, target, value, exists);
|
|
147
|
+
if (out?.status !== 0)
|
|
148
|
+
throw new CliError(`vercel env add ${name} ${target} failed.`, { stderr: out?.stderr.trim() });
|
|
149
|
+
state[key] = exists ? 'rotated' : 'set';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
154
|
+
// A password a reviewer can type from a message: lowercase letters and digits, no look-alikes.
|
|
155
|
+
function readablePassword() {
|
|
156
|
+
const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
157
|
+
return Array.from(randomBytes(12), (b) => alphabet[b % alphabet.length]).join('');
|
|
158
|
+
}
|
|
159
|
+
/** Reads one environment's values into memory and deletes the file, keeping only `names`. */
|
|
160
|
+
function pullValues(target, names) {
|
|
161
|
+
const dir = mkdtempSync(join(tmpdir(), 'hub-env-'));
|
|
162
|
+
try {
|
|
163
|
+
const file = join(dir, '.env');
|
|
164
|
+
cli(['env', 'pull', file, `--environment=${target}`, '--yes']);
|
|
165
|
+
const pulled = readEnv(dir);
|
|
166
|
+
return Object.fromEntries(names.filter((n) => pulled[n]).map((n) => [n, pulled[n]]));
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
rmSync(dir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function provisionReview(project, warnings) {
|
|
173
|
+
const names = envNames(ROOT, 'production');
|
|
174
|
+
let store = 'kept';
|
|
175
|
+
if (!names || !STORE_ENV.every((n) => names.has(n))) {
|
|
176
|
+
const out = vercel(ROOT, [
|
|
177
|
+
'integration',
|
|
178
|
+
'add',
|
|
179
|
+
UPSTASH,
|
|
180
|
+
'--name',
|
|
181
|
+
`${project}-review`,
|
|
182
|
+
'-e',
|
|
183
|
+
'production',
|
|
184
|
+
'-e',
|
|
185
|
+
'preview',
|
|
186
|
+
'-e',
|
|
187
|
+
'development',
|
|
188
|
+
'--no-env-pull',
|
|
189
|
+
]);
|
|
190
|
+
if (out?.status !== 0)
|
|
191
|
+
throw new CliError('Could not add the Upstash for Redis store.', {
|
|
192
|
+
stderr: out?.stderr.trim().slice(-600),
|
|
193
|
+
fix: `Ask the user to run \`vercel integration add ${UPSTASH}\` once in their terminal (it accepts the Marketplace terms), then run this again.`,
|
|
194
|
+
});
|
|
195
|
+
store = 'created';
|
|
196
|
+
}
|
|
197
|
+
// The local commands (design-hub people, design-hub comments) read the same store.
|
|
198
|
+
const values = pullValues('production', STORE_ENV);
|
|
199
|
+
const missing = STORE_ENV.filter((n) => !values[n]);
|
|
200
|
+
if (missing.length)
|
|
201
|
+
warnings.push(`Could not read ${missing.join(', ')} back; copy them from the store's page on Vercel into .env.`);
|
|
202
|
+
writeEnv(ROOT, values);
|
|
203
|
+
return { store, local: missing.length ? 'partial' : 'written' };
|
|
204
|
+
}
|
|
205
|
+
function install(skip) {
|
|
206
|
+
if (skip)
|
|
207
|
+
return 'skipped';
|
|
208
|
+
run('pnpm', existsSync(join(ROOT, 'pnpm-lock.yaml')) ? ['install', '--frozen-lockfile'] : ['install']);
|
|
209
|
+
return 'ran';
|
|
210
|
+
}
|
|
211
|
+
/** The key in use after this run: the one given, else the one already in .env. */
|
|
212
|
+
function typesafe(given, current) {
|
|
213
|
+
if (given === undefined)
|
|
214
|
+
return current ?? '';
|
|
215
|
+
if (!/^[\w.-]{16,}$/.test(given))
|
|
216
|
+
throw new CliError('--typesafe-key does not look like an API key.');
|
|
217
|
+
writeEnv(ROOT, { TYPESAFE_API_KEY: given });
|
|
218
|
+
return given;
|
|
219
|
+
}
|
|
220
|
+
/** The gate's values: the password given, kept, or made, and the secret kept or made. */
|
|
221
|
+
export function gateValues(given, env) {
|
|
222
|
+
const password = given ?? env.GATE_PASSWORD ?? readablePassword();
|
|
223
|
+
if (password.length < 8)
|
|
224
|
+
throw new CliError('--gate-password needs 8 characters or more.');
|
|
225
|
+
const secret = env.GATE_SECRET || randomBytes(32).toString('base64');
|
|
226
|
+
return { password, secret, rotated: given !== undefined && given !== env.GATE_PASSWORD };
|
|
227
|
+
}
|
|
228
|
+
function setUpVercel(project, flags, env, typesafeKey, warnings) {
|
|
229
|
+
const linked = link(project, warnings);
|
|
230
|
+
const { password, secret, rotated } = gateValues(flags['gate-password'], env);
|
|
231
|
+
writeEnv(ROOT, { GATE_PASSWORD: password, GATE_SECRET: secret });
|
|
232
|
+
const values = { GATE_PASSWORD: password, GATE_SECRET: secret };
|
|
233
|
+
// Comment triage runs on the server, so with the review store on, the key goes to Vercel too.
|
|
234
|
+
if (flags.review && typesafeKey)
|
|
235
|
+
values.TYPESAFE_API_KEY = typesafeKey;
|
|
236
|
+
if (rotated)
|
|
237
|
+
warnings.push('The password changed: every visitor signs in again.');
|
|
238
|
+
const result = {
|
|
239
|
+
vercel: { project, linked, env: ensureEnv(values, new Set(rotated ? ['GATE_PASSWORD'] : [])) },
|
|
240
|
+
share: { password },
|
|
241
|
+
};
|
|
242
|
+
if (!flags.review)
|
|
243
|
+
return result;
|
|
244
|
+
warnings.push('Redeploy so the new env reaches the site; then design-hub people invite --name "<you>" --role editor.');
|
|
245
|
+
return { ...result, review: provisionReview(project, warnings) };
|
|
246
|
+
}
|
|
247
|
+
function envIgnored(warnings) {
|
|
248
|
+
try {
|
|
249
|
+
execFileSync('git', ['-C', ROOT, 'check-ignore', '-q', '.env']);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
warnings.push('.env is not gitignored; it holds secrets. Add `.env` to .gitignore before committing.');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function main({ flags }) {
|
|
256
|
+
if (flags.review && !flags.vercel)
|
|
257
|
+
throw new CliError('--review needs --vercel <project>.');
|
|
258
|
+
const warnings = [];
|
|
259
|
+
const node = checkNode();
|
|
260
|
+
const env = readEnv(ROOT);
|
|
261
|
+
const port = choosePort(flags.port, env.PORT ? Number(env.PORT) : undefined, warnings);
|
|
262
|
+
writeEnv(ROOT, { PORT: String(port) });
|
|
263
|
+
writeLaunch(port);
|
|
264
|
+
const installed = install(flags['no-install']);
|
|
265
|
+
const typesafeKey = typesafe(flags['typesafe-key'], env.TYPESAFE_API_KEY);
|
|
266
|
+
const result = {
|
|
267
|
+
node,
|
|
268
|
+
port,
|
|
269
|
+
install: installed,
|
|
270
|
+
typesafe: typesafeKey ? 'on' : 'off',
|
|
271
|
+
};
|
|
272
|
+
if (flags.conductor)
|
|
273
|
+
result.conductor = writeConductor(flags.branch, warnings);
|
|
274
|
+
if (flags.vercel)
|
|
275
|
+
Object.assign(result, setUpVercel(flags.vercel, flags, env, typesafeKey, warnings));
|
|
276
|
+
envIgnored(warnings);
|
|
277
|
+
return { ...result, warnings };
|
|
278
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type Args } from './lib/cli.ts';
|
|
3
|
+
export declare const spec: {
|
|
4
|
+
readonly name: 'calibrate';
|
|
5
|
+
readonly summary: 'Label logged Jev judgments and derive the checks.json thresholds from the labels.';
|
|
6
|
+
readonly flags: {
|
|
7
|
+
readonly yes: {
|
|
8
|
+
readonly type: 'list';
|
|
9
|
+
readonly description: 'A judgment id that was right to flag: the rule really was broken.';
|
|
10
|
+
};
|
|
11
|
+
readonly no: {
|
|
12
|
+
readonly type: 'list';
|
|
13
|
+
readonly description: 'A judgment id that should not have flagged.';
|
|
14
|
+
};
|
|
15
|
+
readonly unlabeled: {
|
|
16
|
+
readonly type: 'boolean';
|
|
17
|
+
readonly description: 'List judgments without a label, newest first.';
|
|
18
|
+
};
|
|
19
|
+
readonly limit: {
|
|
20
|
+
readonly type: 'number';
|
|
21
|
+
readonly default: 20;
|
|
22
|
+
readonly description: 'How many --unlabeled to list.';
|
|
23
|
+
};
|
|
24
|
+
readonly write: {
|
|
25
|
+
readonly type: 'boolean';
|
|
26
|
+
readonly description: 'Write the suggested thresholds into checks.json.';
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
readonly output: '{ ok, thresholds: [{ threshold, current, suggested, labelled, positives, reason }] } or { judgments }';
|
|
30
|
+
};
|
|
31
|
+
export declare const MIN_LABELS = 20;
|
|
32
|
+
export declare const MIN_POSITIVES = 5;
|
|
33
|
+
export type Entry = {
|
|
34
|
+
id: string;
|
|
35
|
+
check: string;
|
|
36
|
+
question: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
p?: number;
|
|
39
|
+
excerpt?: string;
|
|
40
|
+
at: string;
|
|
41
|
+
};
|
|
42
|
+
export type Point = {
|
|
43
|
+
p: number;
|
|
44
|
+
truth: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* What each threshold is for decides how it is chosen. A block must be right
|
|
48
|
+
* almost every time it fires (precision). A warning may be wrong more often.
|
|
49
|
+
* The private-contact threshold must miss almost nothing (recall), because a
|
|
50
|
+
* miss puts a person's details behind a forwarded link.
|
|
51
|
+
*/
|
|
52
|
+
export declare const TARGETS: {
|
|
53
|
+
readonly 'fidelity.warnAt': {
|
|
54
|
+
readonly checks: readonly ['fidelity'];
|
|
55
|
+
readonly questions: null;
|
|
56
|
+
readonly goal: 'precision';
|
|
57
|
+
readonly at: 0.6;
|
|
58
|
+
};
|
|
59
|
+
readonly 'fidelity.blockAt': {
|
|
60
|
+
readonly checks: readonly ['fidelity'];
|
|
61
|
+
readonly questions: null;
|
|
62
|
+
readonly goal: 'precision';
|
|
63
|
+
readonly at: 0.95;
|
|
64
|
+
};
|
|
65
|
+
readonly 'states.blockAt': {
|
|
66
|
+
readonly checks: readonly ['states'];
|
|
67
|
+
readonly questions: null;
|
|
68
|
+
readonly goal: 'precision';
|
|
69
|
+
readonly at: 0.95;
|
|
70
|
+
};
|
|
71
|
+
readonly 'contacts.privateAt': {
|
|
72
|
+
readonly checks: readonly ['contacts'];
|
|
73
|
+
readonly questions: null;
|
|
74
|
+
readonly goal: 'recall';
|
|
75
|
+
readonly at: 0.95;
|
|
76
|
+
};
|
|
77
|
+
readonly 'skillHint.fitsAt': {
|
|
78
|
+
readonly checks: readonly ['skillHint'];
|
|
79
|
+
readonly questions: RegExp;
|
|
80
|
+
readonly goal: 'precision';
|
|
81
|
+
readonly at: 0.8;
|
|
82
|
+
};
|
|
83
|
+
readonly 'visual.warnAt': {
|
|
84
|
+
readonly checks: readonly ['visual'];
|
|
85
|
+
readonly questions: null;
|
|
86
|
+
readonly goal: 'precision';
|
|
87
|
+
readonly at: 0.6;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export declare function rates(points: Point[], t: number): {
|
|
91
|
+
precision: number;
|
|
92
|
+
recall: number;
|
|
93
|
+
};
|
|
94
|
+
/** The threshold that meets the goal, or null when the labels cannot support one. */
|
|
95
|
+
export declare function suggestThreshold(points: Point[], goal: 'precision' | 'recall', at: number): number | null;
|
|
96
|
+
type Current = Record<string, Record<string, unknown>>;
|
|
97
|
+
export type Row = {
|
|
98
|
+
threshold: string;
|
|
99
|
+
current: unknown;
|
|
100
|
+
suggested: number | null;
|
|
101
|
+
labelled: number;
|
|
102
|
+
positives: number;
|
|
103
|
+
reason: string;
|
|
104
|
+
};
|
|
105
|
+
/** One row per threshold: what it is, what the labels suggest, and why. */
|
|
106
|
+
export declare function report(judged: Entry[], truth: Map<string, boolean>, current: Current): Row[];
|
|
107
|
+
export declare function main({ flags }: Args<typeof spec>): {
|
|
108
|
+
labelled: number;
|
|
109
|
+
} | {
|
|
110
|
+
judgments: {
|
|
111
|
+
id: string;
|
|
112
|
+
check: string;
|
|
113
|
+
question: string;
|
|
114
|
+
subject: string;
|
|
115
|
+
p: number | undefined;
|
|
116
|
+
excerpt: string | undefined;
|
|
117
|
+
}[];
|
|
118
|
+
labelled?: undefined;
|
|
119
|
+
thresholds?: undefined;
|
|
120
|
+
written?: undefined;
|
|
121
|
+
} | {
|
|
122
|
+
judgments: number;
|
|
123
|
+
labelled: number;
|
|
124
|
+
thresholds: Row[];
|
|
125
|
+
written: boolean;
|
|
126
|
+
};
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
// Every Jev judgment lands in .design-hub/jev.jsonl with an id. A person
|
|
4
|
+
// labels some; this turns the labels into thresholds, so a check blocks only
|
|
5
|
+
// where the data says it is right. A threshold moves only with enough
|
|
6
|
+
// labels behind it; otherwise it stays as it is and the report says why.
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { DEFAULT_CHECKS, validateChecks } from '../core/checks.js';
|
|
10
|
+
import { CHECKS_FILE } from '../server/paths.js';
|
|
11
|
+
import { CliError } from './lib/cli.js';
|
|
12
|
+
import { appendJsonl, JEV_LOG, LABELS } from './lib/judge.js';
|
|
13
|
+
export const spec = {
|
|
14
|
+
name: 'calibrate',
|
|
15
|
+
summary: 'Label logged Jev judgments and derive the checks.json thresholds from the labels.',
|
|
16
|
+
flags: {
|
|
17
|
+
yes: { type: 'list', description: 'A judgment id that was right to flag: the rule really was broken.' },
|
|
18
|
+
no: { type: 'list', description: 'A judgment id that should not have flagged.' },
|
|
19
|
+
unlabeled: { type: 'boolean', description: 'List judgments without a label, newest first.' },
|
|
20
|
+
limit: { type: 'number', default: 20, description: 'How many --unlabeled to list.' },
|
|
21
|
+
write: { type: 'boolean', description: 'Write the suggested thresholds into checks.json.' },
|
|
22
|
+
},
|
|
23
|
+
output: '{ ok, thresholds: [{ threshold, current, suggested, labelled, positives, reason }] } or { judgments }',
|
|
24
|
+
};
|
|
25
|
+
export const MIN_LABELS = 20;
|
|
26
|
+
export const MIN_POSITIVES = 5;
|
|
27
|
+
const CANDIDATES = Array.from({ length: 19 }, (_, i) => Math.round((0.05 + i * 0.05) * 100) / 100);
|
|
28
|
+
/**
|
|
29
|
+
* What each threshold is for decides how it is chosen. A block must be right
|
|
30
|
+
* almost every time it fires (precision). A warning may be wrong more often.
|
|
31
|
+
* The private-contact threshold must miss almost nothing (recall), because a
|
|
32
|
+
* miss puts a person's details behind a forwarded link.
|
|
33
|
+
*/
|
|
34
|
+
export const TARGETS = {
|
|
35
|
+
'fidelity.warnAt': { checks: ['fidelity'], questions: null, goal: 'precision', at: 0.6 },
|
|
36
|
+
'fidelity.blockAt': { checks: ['fidelity'], questions: null, goal: 'precision', at: 0.95 },
|
|
37
|
+
'states.blockAt': { checks: ['states'], questions: null, goal: 'precision', at: 0.95 },
|
|
38
|
+
'contacts.privateAt': { checks: ['contacts'], questions: null, goal: 'recall', at: 0.95 },
|
|
39
|
+
'skillHint.fitsAt': { checks: ['skillHint'], questions: /^fits:/, goal: 'precision', at: 0.8 },
|
|
40
|
+
'visual.warnAt': { checks: ['visual'], questions: null, goal: 'precision', at: 0.6 },
|
|
41
|
+
};
|
|
42
|
+
export function rates(points, t) {
|
|
43
|
+
const flagged = points.filter((x) => x.p >= t);
|
|
44
|
+
const positives = points.filter((x) => x.truth);
|
|
45
|
+
const hits = flagged.filter((x) => x.truth).length;
|
|
46
|
+
return {
|
|
47
|
+
precision: flagged.length ? hits / flagged.length : 1,
|
|
48
|
+
recall: positives.length ? hits / positives.length : 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** The threshold that meets the goal, or null when the labels cannot support one. */
|
|
52
|
+
export function suggestThreshold(points, goal, at) {
|
|
53
|
+
if (points.length < MIN_LABELS || points.filter((x) => x.truth).length < MIN_POSITIVES)
|
|
54
|
+
return null;
|
|
55
|
+
// Precision: the lowest threshold that is precise enough, so the check fires as often as it can.
|
|
56
|
+
// Recall: the highest threshold that still catches enough, so it stops as little as it can.
|
|
57
|
+
const ok = CANDIDATES.filter((t) => rates(points, t)[goal] >= at);
|
|
58
|
+
if (!ok.length)
|
|
59
|
+
return null;
|
|
60
|
+
return goal === 'precision' ? Math.min(...ok) : Math.max(...ok);
|
|
61
|
+
}
|
|
62
|
+
const readJsonl = (file) => existsSync(file)
|
|
63
|
+
? readFileSync(file, 'utf8')
|
|
64
|
+
.split('\n')
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.flatMap((l) => {
|
|
67
|
+
try {
|
|
68
|
+
return [JSON.parse(l)];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A line cut short when a process died mid-append; the rest of the log still counts.
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
: [];
|
|
76
|
+
function labels() {
|
|
77
|
+
// The last label for an id wins, so a mistake is fixed by labelling again.
|
|
78
|
+
const out = new Map();
|
|
79
|
+
for (const l of readJsonl(LABELS))
|
|
80
|
+
out.set(l.id, l.truth);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function reason(target, points, suggested) {
|
|
84
|
+
const positives = points.filter((x) => x.truth).length;
|
|
85
|
+
if (suggested !== null)
|
|
86
|
+
return `${target.goal === 'recall' ? 'highest' : 'lowest'} threshold with ${target.goal} ≥ ${target.at}`;
|
|
87
|
+
if (points.length < MIN_LABELS || positives < MIN_POSITIVES)
|
|
88
|
+
return `needs ${MIN_LABELS} labels with ${MIN_POSITIVES} yes; has ${points.length} with ${positives}`;
|
|
89
|
+
return `no threshold reaches ${target.goal} ${target.at}`;
|
|
90
|
+
}
|
|
91
|
+
/** One row per threshold: what it is, what the labels suggest, and why. */
|
|
92
|
+
export function report(judged, truth, current) {
|
|
93
|
+
return Object.entries(TARGETS).map(([path, target]) => {
|
|
94
|
+
const points = judged
|
|
95
|
+
.filter((e) => target.checks.includes(e.check))
|
|
96
|
+
.filter((e) => !target.questions || target.questions.test(e.question))
|
|
97
|
+
.flatMap((e) => (truth.has(e.id) ? [{ p: e.p, truth: truth.get(e.id) === true }] : []));
|
|
98
|
+
const [group, key] = path.split('.');
|
|
99
|
+
const suggested = suggestThreshold(points, target.goal, target.at);
|
|
100
|
+
return {
|
|
101
|
+
threshold: path,
|
|
102
|
+
current: current[group]?.[key] ?? null,
|
|
103
|
+
suggested,
|
|
104
|
+
labelled: points.length,
|
|
105
|
+
positives: points.filter((x) => x.truth).length,
|
|
106
|
+
reason: reason(target, points, suggested),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function label(marks, known) {
|
|
111
|
+
const unknown = marks.filter(([id]) => !known.has(id)).map(([id]) => id);
|
|
112
|
+
if (unknown.length)
|
|
113
|
+
throw new CliError('No judgment has these ids.', { unknown, fix: 'design-hub calibrate --unlabeled' });
|
|
114
|
+
for (const [id, truth] of marks)
|
|
115
|
+
appendJsonl(LABELS, { id, truth, at: new Date().toISOString() });
|
|
116
|
+
return { labelled: marks.length };
|
|
117
|
+
}
|
|
118
|
+
function write(current, rows) {
|
|
119
|
+
for (const { threshold, suggested } of rows) {
|
|
120
|
+
if (suggested === null)
|
|
121
|
+
continue;
|
|
122
|
+
const [group, key] = threshold.split('.');
|
|
123
|
+
current[group] = { ...current[group], [key]: suggested };
|
|
124
|
+
}
|
|
125
|
+
const problems = validateChecks(current);
|
|
126
|
+
if (problems.length)
|
|
127
|
+
throw new CliError('The result would not validate; checks.json is unchanged.', { problems });
|
|
128
|
+
mkdirSync(dirname(CHECKS_FILE), { recursive: true });
|
|
129
|
+
writeFileSync(CHECKS_FILE, `${JSON.stringify(current, null, 2)}\n`);
|
|
130
|
+
}
|
|
131
|
+
/** The project's checks.json as written, or the defaults when the project has none yet. */
|
|
132
|
+
const currentChecks = () => existsSync(CHECKS_FILE)
|
|
133
|
+
? JSON.parse(readFileSync(CHECKS_FILE, 'utf8'))
|
|
134
|
+
: JSON.parse(JSON.stringify(DEFAULT_CHECKS));
|
|
135
|
+
export function main({ flags }) {
|
|
136
|
+
const judged = readJsonl(JEV_LOG).filter((e) => e.id && typeof e.p === 'number');
|
|
137
|
+
const marks = [
|
|
138
|
+
...flags.yes.map((id) => [id, true]),
|
|
139
|
+
...flags.no.map((id) => [id, false]),
|
|
140
|
+
];
|
|
141
|
+
if (marks.length)
|
|
142
|
+
return label(marks, new Set(judged.map((e) => e.id)));
|
|
143
|
+
const truth = labels();
|
|
144
|
+
if (flags.unlabeled)
|
|
145
|
+
return {
|
|
146
|
+
judgments: judged
|
|
147
|
+
.filter((e) => !truth.has(e.id))
|
|
148
|
+
.reverse()
|
|
149
|
+
.slice(0, flags.limit)
|
|
150
|
+
.map(({ id, check, question, subject, p, excerpt }) => ({
|
|
151
|
+
id,
|
|
152
|
+
check,
|
|
153
|
+
question,
|
|
154
|
+
subject,
|
|
155
|
+
p,
|
|
156
|
+
excerpt,
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
const current = currentChecks();
|
|
160
|
+
const thresholds = report(judged, truth, current);
|
|
161
|
+
if (flags.write)
|
|
162
|
+
write(current, thresholds);
|
|
163
|
+
return { judgments: judged.length, labelled: truth.size, thresholds, written: flags.write };
|
|
164
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { Args } from './lib/cli.ts';
|
|
3
|
+
export declare const spec: {
|
|
4
|
+
readonly name: 'check';
|
|
5
|
+
readonly summary: 'TypeScript and Biome over the project. Must pass before a change is done.';
|
|
6
|
+
readonly flags: {
|
|
7
|
+
readonly fix: {
|
|
8
|
+
readonly type: 'boolean';
|
|
9
|
+
readonly description: 'Let Biome format and apply its safe fixes first.';
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
readonly output: '{ ok, types: { ok, output? }, lint: { ok, output? } }';
|
|
13
|
+
};
|
|
14
|
+
type Run = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
output?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const typecheck: () => Run;
|
|
19
|
+
export declare function lint(fix: boolean): Run;
|
|
20
|
+
export declare function main({ flags }: Args<typeof spec>): {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
types: Run;
|
|
23
|
+
lint: Run;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
// `design-hub check`: TypeScript over the project, then Biome. Both binaries
|
|
4
|
+
// come with this package, so a project installs neither; Biome runs on the
|
|
5
|
+
// package's config, so every project is checked the same way.
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { PACKAGE, ROOT } from '../server/paths.js';
|
|
9
|
+
import { biomeBin, tscBin } from './lib/bin.js';
|
|
10
|
+
export const spec = {
|
|
11
|
+
name: 'check',
|
|
12
|
+
summary: 'TypeScript and Biome over the project. Must pass before a change is done.',
|
|
13
|
+
flags: { fix: { type: 'boolean', description: 'Let Biome format and apply its safe fixes first.' } },
|
|
14
|
+
output: '{ ok, types: { ok, output? }, lint: { ok, output? } }',
|
|
15
|
+
};
|
|
16
|
+
const BIOME_CONFIG = join(PACKAGE, 'configs', 'biome');
|
|
17
|
+
function node(script, args) {
|
|
18
|
+
const out = spawnSync(process.execPath, [script, ...args], { cwd: ROOT, encoding: 'utf8' });
|
|
19
|
+
const output = `${out.stdout}${out.stderr}`.trim();
|
|
20
|
+
return out.status === 0 ? { ok: true } : { ok: false, output: output.split('\n').slice(-40).join('\n') };
|
|
21
|
+
}
|
|
22
|
+
export const typecheck = () => node(tscBin(), ['--noEmit', '-p', ROOT]);
|
|
23
|
+
export function lint(fix) {
|
|
24
|
+
return node(biomeBin(), ['check', ...(fix ? ['--write'] : []), '--config-path', BIOME_CONFIG, ROOT]);
|
|
25
|
+
}
|
|
26
|
+
export function main({ flags }) {
|
|
27
|
+
const lintRun = lint(flags.fix);
|
|
28
|
+
const types = typecheck();
|
|
29
|
+
return { ok: types.ok && lintRun.ok, types, lint: lintRun };
|
|
30
|
+
}
|