@norskvideo/ctl-dev-kit 0.1.18 → 0.1.19
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.
|
@@ -25,8 +25,14 @@ export interface DriftReport {
|
|
|
25
25
|
problems: string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const BEGIN = "<!-- BEGIN ctl-shared-conventions v1 -->";
|
|
29
|
-
const END = "<!-- END ctl-shared-conventions v1 -->";
|
|
28
|
+
export const BEGIN = "<!-- BEGIN ctl-shared-conventions v1 -->";
|
|
29
|
+
export const END = "<!-- END ctl-shared-conventions v1 -->";
|
|
30
|
+
|
|
31
|
+
// The .gitignore shared block is delimited by these markers (see gitignore.core),
|
|
32
|
+
// so the sync writer can locate-and-replace it deterministically. The gate itself
|
|
33
|
+
// checks the whole marker-delimited block as a verbatim substring.
|
|
34
|
+
export const GITIGNORE_BEGIN = "# BEGIN ctl-shared-gitignore v1";
|
|
35
|
+
export const GITIGNORE_END = "# END ctl-shared-gitignore v1";
|
|
30
36
|
|
|
31
37
|
const RESYNC = "Edit the dev-kit source and re-sync — never hand-edit the copy.";
|
|
32
38
|
|
|
@@ -45,8 +51,8 @@ function firstDiffLine(actual: string, expected: string): string {
|
|
|
45
51
|
|
|
46
52
|
// The one legitimately per-repo line in the shared workflows: the dashboard key
|
|
47
53
|
// the CI result is dispatched under. Everything else is verbatim-shared.
|
|
48
|
-
const PRODUCT_LINE = /^(\s*product:\s*).*$/gm;
|
|
49
|
-
const PRODUCT_SENTINEL = "__PRODUCT__";
|
|
54
|
+
export const PRODUCT_LINE = /^(\s*product:\s*).*$/gm;
|
|
55
|
+
export const PRODUCT_SENTINEL = "__PRODUCT__";
|
|
50
56
|
|
|
51
57
|
function workflowProblem(relPath: string, canonicalRef: string, actual: string, canonical: string): string | undefined {
|
|
52
58
|
// Compare structure with the product value masked so any real key passes, then
|
|
@@ -66,8 +72,8 @@ function workflowProblem(relPath: string, canonicalRef: string, actual: string,
|
|
|
66
72
|
// this pin only feeds the dev flake; it floats per repo and over time and is NOT
|
|
67
73
|
// a shared convention. Mask it so the gate compares flake STRUCTURE, not the pin
|
|
68
74
|
// — same idea as masking upgrade-latest's product: line.
|
|
69
|
-
const CTL_VERSION_LINE = /ctlVersion = "[^"]*";/;
|
|
70
|
-
const CTL_HASH_LINE = /hash = "sha256-[^"]*";/g;
|
|
75
|
+
export const CTL_VERSION_LINE = /ctlVersion = "[^"]*";/;
|
|
76
|
+
export const CTL_HASH_LINE = /hash = "sha256-[^"]*";/g;
|
|
71
77
|
|
|
72
78
|
function flakeProblem(actual: string, canonical: string): string | undefined {
|
|
73
79
|
const mask = (s: string) =>
|
|
@@ -82,10 +88,10 @@ function flakeProblem(actual: string, canonical: string): string | undefined {
|
|
|
82
88
|
// inside a sanctioned marker block at the head of `excludes`. Everything outside
|
|
83
89
|
// the block is byte-compared; inside, only well-formed `"entry",` lines are
|
|
84
90
|
// allowed so the block can't smuggle arbitrary config past the byte check.
|
|
85
|
-
const DPRINT_BEGIN = "// BEGIN repo-specific excludes";
|
|
86
|
-
const DPRINT_END = "// END repo-specific excludes";
|
|
91
|
+
export const DPRINT_BEGIN = "// BEGIN repo-specific excludes";
|
|
92
|
+
export const DPRINT_END = "// END repo-specific excludes";
|
|
87
93
|
|
|
88
|
-
function splitAtMarkers(s: string): { prefix: string; body: string[]; suffix: string } | undefined {
|
|
94
|
+
export function splitAtMarkers(s: string): { prefix: string; body: string[]; suffix: string } | undefined {
|
|
89
95
|
const lines = s.split("\n");
|
|
90
96
|
const begin = lines.findIndex((l) => l.trim() === DPRINT_BEGIN);
|
|
91
97
|
const end = lines.findIndex((l) => l.trim() === DPRINT_END);
|
|
@@ -7,22 +7,21 @@
|
|
|
7
7
|
# need re-syncing. This workflow closes the first gap and surfaces the second.
|
|
8
8
|
#
|
|
9
9
|
# It bumps the @norskvideo/ctl-dev-kit pin to the newest published version,
|
|
10
|
-
# reinstalls,
|
|
11
|
-
#
|
|
10
|
+
# reinstalls, re-syncs this product's forced copies from the new canonical
|
|
11
|
+
# (sync-drift), then runs the gate (check:drift + lint + typecheck + unit tests):
|
|
12
12
|
#
|
|
13
|
-
# - gate green ->
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# Re-sync on the branch until green, then merge.
|
|
13
|
+
# - gate green -> the bump plus any mechanical re-sync is correct; commit it all
|
|
14
|
+
# straight to main, hands-free. This is the common case.
|
|
15
|
+
# - gate red -> the new dev-kit genuinely does not fit this product: a
|
|
16
|
+
# convention that fails lint/typecheck/tests, or a copy sync-drift could not
|
|
17
|
+
# mechanically converge. Push a branch and open a PR for a human -- real
|
|
18
|
+
# engineering to adopt the convention, not a copy-paste.
|
|
20
19
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
20
|
+
# sync-drift re-derives each copy from the canonical (never a second opinion that
|
|
21
|
+
# can disagree), preserving the per-repo bits the gate masks -- the product: keys,
|
|
22
|
+
# the flake ctl pin, the dprint/gitignore/CLAUDE repo content. The gate runs AFTER
|
|
23
|
+
# it, so a bad rewrite fails check:drift and a breaking convention fails the build,
|
|
24
|
+
# rather than landing silently. Structural per-repo files it never touches.
|
|
26
25
|
#
|
|
27
26
|
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-dev-kit.yml) and
|
|
28
27
|
# copied verbatim into each product repo; the check:drift gate fails CI if a copy
|
|
@@ -81,7 +80,7 @@ jobs:
|
|
|
81
80
|
echo "old=$old_bare" >> "$GITHUB_OUTPUT"
|
|
82
81
|
echo "new=$new" >> "$GITHUB_OUTPUT"
|
|
83
82
|
|
|
84
|
-
- name: Reinstall
|
|
83
|
+
- name: Reinstall, re-sync the copies from canonical, run the product gate
|
|
85
84
|
id: gate
|
|
86
85
|
if: steps.sync.outputs.changed == '1'
|
|
87
86
|
continue-on-error: true
|
|
@@ -89,6 +88,7 @@ jobs:
|
|
|
89
88
|
nix develop .#build --command bash -c '
|
|
90
89
|
set -euo pipefail
|
|
91
90
|
bun install
|
|
91
|
+
bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts
|
|
92
92
|
bun run check:drift
|
|
93
93
|
bun run lint
|
|
94
94
|
bun run typecheck
|
|
@@ -104,9 +104,10 @@ jobs:
|
|
|
104
104
|
git add -A
|
|
105
105
|
git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
|
|
106
106
|
|
|
107
|
-
@norskvideo/ctl-dev-kit pin bumped to the newest published version
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
@norskvideo/ctl-dev-kit pin bumped to the newest published version and the
|
|
108
|
+
forced copies re-synced from the new canonical (sync-drift); check:drift +
|
|
109
|
+
lint + typecheck + unit tests green against the new bytes. Lockfile
|
|
110
|
+
regenerated."
|
|
110
111
|
# A GITHUB_TOKEN push does not retrigger checks (Actions' recursion
|
|
111
112
|
# guard) -- fine, the gate above already ran them. Rebase-and-retry so a
|
|
112
113
|
# commit landing mid-run does not red the sync; a real conflict aborts.
|
|
@@ -134,18 +135,17 @@ jobs:
|
|
|
134
135
|
title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
|
|
135
136
|
body="$(printf '%s\n' \
|
|
136
137
|
"Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest" \
|
|
137
|
-
"published version
|
|
138
|
-
"
|
|
138
|
+
"published version and sync-drift re-synced the forced copies from the new" \
|
|
139
|
+
"canonical -- but the gate is **red**, so this bump needs a human." \
|
|
139
140
|
"" \
|
|
140
|
-
"
|
|
141
|
+
"The mechanical re-sync is already on this branch. A red gate here means the" \
|
|
142
|
+
"new dev-kit genuinely does not fit the product yet -- run the gate to see which:" \
|
|
141
143
|
"" \
|
|
142
144
|
" bun install" \
|
|
143
|
-
" bun run check:drift #
|
|
144
|
-
" bun run lint && bun run typecheck && bun run test:unit" \
|
|
145
|
+
" bun run check:drift # a copy sync-drift could not mechanically converge" \
|
|
146
|
+
" bun run lint && bun run typecheck && bun run test:unit # a convention that breaks real code" \
|
|
145
147
|
"" \
|
|
146
|
-
"
|
|
147
|
-
"hand-edit against memory), or resolve the convention change this pin" \
|
|
148
|
-
"introduced, pushing to this branch until green -- then merge.")"
|
|
148
|
+
"Resolve whichever is failing, pushing to this branch until green -- then merge.")"
|
|
149
149
|
if existing="$(gh pr list --head "$branch" --state open --json number -q '.[0].number')" && [ -n "$existing" ]; then
|
|
150
150
|
gh pr edit "$existing" --title "$title" --body "$body"
|
|
151
151
|
else
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// The writer half of the drift gate. check-drift.ts proves a product's forced
|
|
2
|
+
// copies match this dev-kit's canonical; sync-drift.ts *makes* them match, by
|
|
3
|
+
// re-deriving each copy from the same canonical bytes. It is the mechanical
|
|
4
|
+
// inverse of the checker and shares its constants (imported below) so the two
|
|
5
|
+
// cannot disagree about what "in sync" means.
|
|
6
|
+
//
|
|
7
|
+
// It only touches copies whose convergent form is mechanical — there is no
|
|
8
|
+
// judgement anywhere:
|
|
9
|
+
// - verbatim (biome.json, tsconfig.base.json) -> overwrite with canonical
|
|
10
|
+
// - masked workflows (checks / upgrade / sync) -> canonical, repo's product: key kept
|
|
11
|
+
// - flake.nix -> canonical, repo's floating ctl pin kept
|
|
12
|
+
// - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
|
|
13
|
+
// Structural per-repo files (root tsconfig.json, deployment/build-image.sh,
|
|
14
|
+
// manifest.seed.json) have NO canonical bytes to apply — the gate checks their
|
|
15
|
+
// shape, not their content — so they are left untouched.
|
|
16
|
+
//
|
|
17
|
+
// The safety net is that the gate still runs AFTER sync: a copy this writer
|
|
18
|
+
// cannot mechanically converge (e.g. a workflow with no product key to preserve)
|
|
19
|
+
// is left for check-drift to flag, and a convention that genuinely breaks the
|
|
20
|
+
// product surfaces as a red lint/typecheck/test — not as a silent bad copy.
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import {
|
|
24
|
+
BEGIN,
|
|
25
|
+
type CanonicalBytes,
|
|
26
|
+
CTL_HASH_LINE,
|
|
27
|
+
CTL_VERSION_LINE,
|
|
28
|
+
END,
|
|
29
|
+
GITIGNORE_BEGIN,
|
|
30
|
+
GITIGNORE_END,
|
|
31
|
+
PRODUCT_LINE,
|
|
32
|
+
PRODUCT_SENTINEL,
|
|
33
|
+
splitAtMarkers,
|
|
34
|
+
} from "./check-drift.ts";
|
|
35
|
+
|
|
36
|
+
export interface SyncReport {
|
|
37
|
+
written: string[];
|
|
38
|
+
skipped: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeIfChanged(path: string, content: string, rel: string, written: string[]): void {
|
|
42
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
43
|
+
if (current !== content) {
|
|
44
|
+
writeFileSync(path, content);
|
|
45
|
+
written.push(rel);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// checks.yml / upgrade-latest.yml / sync-dev-kit.yml: canonical byte-for-byte
|
|
50
|
+
// except the one `product:` dispatch key, which the gate masks. Re-emit the
|
|
51
|
+
// canonical with the repo's own key filled back in. A workflow with no key to
|
|
52
|
+
// preserve (absent, or still the sentinel) is left for the gate to flag.
|
|
53
|
+
function syncWorkflow(repoRoot: string, rel: string, canonical: string, r: SyncReport): void {
|
|
54
|
+
const path = join(repoRoot, rel);
|
|
55
|
+
if (!existsSync(path)) {
|
|
56
|
+
r.skipped.push(`${rel} (absent)`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const key = readFileSync(path, "utf8").match(/^\s*product:\s*(\S+)/m)?.[1];
|
|
60
|
+
if (!key || key === PRODUCT_SENTINEL) {
|
|
61
|
+
r.skipped.push(`${rel} (no product: key to preserve)`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
writeIfChanged(path, canonical.replace(PRODUCT_LINE, `$1${key}`), rel, r.written);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// flake.nix is verbatim except the dev-only ctl pin (ctlVersion + the four
|
|
68
|
+
// per-platform hashes), which floats per repo. Re-emit canonical STRUCTURE with
|
|
69
|
+
// the repo's pin restored in order — so a structural edit (e.g. a stray comment)
|
|
70
|
+
// is replaced while the pin is kept. Structure is identical across repos, so the
|
|
71
|
+
// Nth hash line in canonical maps to the Nth in the repo.
|
|
72
|
+
function syncFlake(repoRoot: string, canonical: string, r: SyncReport): void {
|
|
73
|
+
const path = join(repoRoot, "flake.nix");
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
r.skipped.push("flake.nix (absent)");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const existing = readFileSync(path, "utf8");
|
|
79
|
+
const version = existing.match(CTL_VERSION_LINE)?.[0];
|
|
80
|
+
const hashes = existing.match(CTL_HASH_LINE) ?? [];
|
|
81
|
+
let out = version ? canonical.replace(CTL_VERSION_LINE, version) : canonical;
|
|
82
|
+
let i = 0;
|
|
83
|
+
out = out.replace(CTL_HASH_LINE, (m) => hashes[i++] ?? m);
|
|
84
|
+
writeIfChanged(path, out, "flake.nix", r.written);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Replace the region between two markers with the canonical block, preserving
|
|
88
|
+
// everything outside it. `canonicalBlock` starts at the BEGIN marker and ends at
|
|
89
|
+
// the END marker.
|
|
90
|
+
function replaceMarkerBlock(existing: string, begin: string, end: string, canonicalBlock: string): string | undefined {
|
|
91
|
+
const b = existing.indexOf(begin);
|
|
92
|
+
const e = existing.indexOf(end);
|
|
93
|
+
if (b === -1 || e === -1 || e < b) return undefined;
|
|
94
|
+
const after = existing.slice(e + end.length);
|
|
95
|
+
return existing.slice(0, b) + canonicalBlock.trimEnd() + (after.startsWith("\n") ? after : `\n${after}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CLAUDE.md: the fenced conventions core lives between the markers; the repo's
|
|
99
|
+
// own intro/outro prose is outside and kept.
|
|
100
|
+
function syncClaude(repoRoot: string, core: string, r: SyncReport): void {
|
|
101
|
+
const path = join(repoRoot, "CLAUDE.md");
|
|
102
|
+
if (!existsSync(path)) {
|
|
103
|
+
r.skipped.push("CLAUDE.md (absent)");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const out = replaceMarkerBlock(readFileSync(path, "utf8"), BEGIN, END, core);
|
|
107
|
+
if (out === undefined) {
|
|
108
|
+
r.skipped.push("CLAUDE.md (no marker block)");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
writeIfChanged(path, out, "CLAUDE.md", r.written);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// .gitignore: the shared block is marker-delimited; repo-specific ignores live
|
|
115
|
+
// outside it. Replace the block, or prepend it if the repo has none yet (the
|
|
116
|
+
// one-time migration case).
|
|
117
|
+
function syncGitignore(repoRoot: string, canonicalCore: string, r: SyncReport): void {
|
|
118
|
+
const path = join(repoRoot, ".gitignore");
|
|
119
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
120
|
+
const replaced = replaceMarkerBlock(existing, GITIGNORE_BEGIN, GITIGNORE_END, canonicalCore);
|
|
121
|
+
const out = replaced ?? `${canonicalCore.trimEnd()}\n${existing}`;
|
|
122
|
+
writeIfChanged(path, out, ".gitignore", r.written);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// dprint.json: canonical outside the repo-specific-excludes markers, the repo's
|
|
126
|
+
// own exclude entries kept between them.
|
|
127
|
+
function syncDprint(repoRoot: string, canonical: string, r: SyncReport): void {
|
|
128
|
+
const path = join(repoRoot, "dprint.json");
|
|
129
|
+
if (!existsSync(path)) {
|
|
130
|
+
r.skipped.push("dprint.json (absent)");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const c = splitAtMarkers(canonical);
|
|
134
|
+
if (!c) throw new Error("canonical dprint.base.jsonc is missing its own marker block");
|
|
135
|
+
const a = splitAtMarkers(readFileSync(path, "utf8"));
|
|
136
|
+
if (!a) {
|
|
137
|
+
r.skipped.push("dprint.json (no marker block)");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// splitAtMarkers keeps the file's trailing newline as an empty final line in
|
|
141
|
+
// suffix, so the join alone reproduces it — appending another would drift.
|
|
142
|
+
const out = [c.prefix, ...a.body, c.suffix].join("\n");
|
|
143
|
+
writeIfChanged(path, out, "dprint.json", r.written);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncReport {
|
|
147
|
+
const r: SyncReport = { written: [], skipped: [] };
|
|
148
|
+
|
|
149
|
+
writeIfChanged(join(repoRoot, "biome.json"), canonical.biome, "biome.json", r.written);
|
|
150
|
+
writeIfChanged(join(repoRoot, "tsconfig.base.json"), canonical.tsconfigBase, "tsconfig.base.json", r.written);
|
|
151
|
+
|
|
152
|
+
syncWorkflow(repoRoot, ".github/workflows/checks.yml", canonical.checks, r);
|
|
153
|
+
syncWorkflow(repoRoot, ".github/workflows/upgrade-latest.yml", canonical.upgradeLatest, r);
|
|
154
|
+
syncWorkflow(repoRoot, ".github/workflows/sync-dev-kit.yml", canonical.syncDevKit, r);
|
|
155
|
+
|
|
156
|
+
syncFlake(repoRoot, canonical.flake, r);
|
|
157
|
+
syncClaude(repoRoot, canonical.core, r);
|
|
158
|
+
syncGitignore(repoRoot, canonical.gitignoreCore, r);
|
|
159
|
+
syncDprint(repoRoot, canonical.dprint, r);
|
|
160
|
+
|
|
161
|
+
return r;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (import.meta.main) {
|
|
165
|
+
const repoRoot = process.argv[2] ?? process.cwd();
|
|
166
|
+
const dir = import.meta.dir;
|
|
167
|
+
const canonical: CanonicalBytes = {
|
|
168
|
+
core: readFileSync(join(dir, "CLAUDE.core.md"), "utf8"),
|
|
169
|
+
flake: readFileSync(join(dir, "..", "build", "flake.nix"), "utf8"),
|
|
170
|
+
upgradeLatest: readFileSync(join(dir, "upgrade-latest.yml"), "utf8"),
|
|
171
|
+
syncDevKit: readFileSync(join(dir, "sync-dev-kit.yml"), "utf8"),
|
|
172
|
+
checks: readFileSync(join(dir, "checks.yml"), "utf8"),
|
|
173
|
+
biome: readFileSync(join(dir, "biome.base.json"), "utf8"),
|
|
174
|
+
tsconfigBase: readFileSync(join(dir, "tsconfig.base.json"), "utf8"),
|
|
175
|
+
dprint: readFileSync(join(dir, "dprint.base.jsonc"), "utf8"),
|
|
176
|
+
buildImageBootstrap: readFileSync(join(dir, "..", "build", "build-image.bootstrap.sh"), "utf8"),
|
|
177
|
+
gitignoreCore: readFileSync(join(dir, "gitignore.core"), "utf8"),
|
|
178
|
+
};
|
|
179
|
+
const report = syncDrift(repoRoot, canonical);
|
|
180
|
+
if (report.written.length === 0) {
|
|
181
|
+
console.log("sync-drift: all shared-convention copies already match @norskvideo/ctl-dev-kit.");
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`sync-drift: rewrote ${report.written.length} copy(ies) from @norskvideo/ctl-dev-kit:`);
|
|
184
|
+
for (const w of report.written) console.log(` - ${w}`);
|
|
185
|
+
}
|
|
186
|
+
if (report.skipped.length > 0) {
|
|
187
|
+
console.log("left for check-drift / a human:");
|
|
188
|
+
for (const s of report.skipped) console.log(` - ${s}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -111,6 +111,7 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
111
111
|
"test:unit": "bun test shared/src/ backend/src/ tests/unit/",
|
|
112
112
|
"test:image": "bun test tests/image/",
|
|
113
113
|
"check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
|
|
114
|
+
"sync:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts",
|
|
114
115
|
typecheck: "bunx tsc --noEmit -p shared && bunx tsc --noEmit -p backend && bunx tsc --noEmit -p .",
|
|
115
116
|
build: "bun run lint && bun run typecheck && bun run build:no-lint",
|
|
116
117
|
"build:no-lint": "bun run build:shared && bun run build:backend",
|