basalt-ui 1.40.3 → 1.41.0
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/AGENTS.md +4 -2
- package/CHANGELOG.md +13 -0
- package/MIGRATING.md +36 -12
- package/README.md +1 -1
- package/agent/rules/basalt-batteries.md +1 -1
- package/agent/skills/basalt-app/SKILL.md +1 -1
- package/agent/skills/basalt-design/SKILL.md +1 -1
- package/agent/templates/DESIGN.md.tpl +1 -1
- package/configs/check.yml +1 -1
- package/configs/oxlint-plugin.js +5 -5
- package/configs/oxlint-plugin.test.ts +1 -1
- package/dist/agent/foreign.d.ts +3 -2
- package/dist/agent/foreign.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +5 -2
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +29 -0
- package/dist/cli/index.d.ts +61 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +112 -6
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +10 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +18 -1
- package/dist/content/copy-action.d.ts +1 -1
- package/dist/content/glyphs.d.ts +1 -1
- package/dist/data/virtual-list.d.ts +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/foreign.ts +3 -2
- package/src/charts/index.ts +2 -2
- package/src/cli/doctor.ts +47 -4
- package/src/cli/index.ts +180 -11
- package/src/cli/init.ts +14 -0
- package/src/cli/sync.ts +24 -1
- package/src/content/copy-action.tsx +1 -1
- package/src/content/glyphs.tsx +1 -1
- package/src/data/virtual-list.tsx +1 -1
- package/src/guard/index.ts +1 -1
- /package/agent/templates/{CLAUDE-block.md.tpl → AGENTS-block.md.tpl} +0 -0
package/src/cli/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readFileSync,
|
|
15
15
|
rmdirSync,
|
|
16
16
|
statSync,
|
|
17
|
+
realpathSync,
|
|
17
18
|
unlinkSync,
|
|
18
19
|
writeFileSync,
|
|
19
20
|
} from 'node:fs'
|
|
@@ -338,7 +339,7 @@ export function resolveProjectDir(cwd: string): ProjectResolution {
|
|
|
338
339
|
/**
|
|
339
340
|
* Which shape of consumer a project-scoped command is looking at — DECLARED, never inferred
|
|
340
341
|
* (`--tokens-only`, or `"basalt": { "profile": "tokens-only" }`), because the answer switches the
|
|
341
|
-
* Mantine-remedy guard kinds off. Why that direction: package
|
|
342
|
+
* Mantine-remedy guard kinds off. Why that direction: package MAINTAINING.md § CLI.
|
|
342
343
|
*/
|
|
343
344
|
export function declaredProfile(cfg: BasaltConfig, flags: readonly string[]): DoctorProfile {
|
|
344
345
|
if (flags.includes('--tokens-only')) return 'tokens-only'
|
|
@@ -452,7 +453,7 @@ export function waiverHintFor(relPath: string, profile: DoctorProfile): string {
|
|
|
452
453
|
*
|
|
453
454
|
* - `managed` : basalt owns it. sync refreshes it to the shipped version; a local edit is skipped
|
|
454
455
|
* and reported (`--force` overwrites); `--check` exits 1 on drift. Applies to exactly
|
|
455
|
-
* what Claude reads (.claude/rules/*, .claude/skills/*, the
|
|
456
|
+
* what Claude reads (.claude/rules/*, .claude/skills/*, the AGENTS.md block) —
|
|
456
457
|
* Claude Code cannot load rules or skills from node_modules, and that platform limit
|
|
457
458
|
* is the only reason anything is copied at all. The sync diff is the review gate.
|
|
458
459
|
* - `seed` : written once if absent, then owned entirely by the consumer. sync never overwrites
|
|
@@ -462,7 +463,7 @@ export function waiverHintFor(relPath: string, profile: DoctorProfile): string {
|
|
|
462
463
|
* the package while the consumer still owns the file.
|
|
463
464
|
*
|
|
464
465
|
* A managed file with `markers: true` is spliced as a `<!-- basalt:begin -->…<!-- basalt:end -->`
|
|
465
|
-
* region inside a host file the consumer otherwise owns (
|
|
466
|
+
* region inside a host file the consumer otherwise owns (AGENTS.md) — not a third mode, just
|
|
466
467
|
* managed ownership scoped to a region.
|
|
467
468
|
*/
|
|
468
469
|
type Mode = 'managed' | 'seed'
|
|
@@ -518,11 +519,15 @@ type TemplateVars = {
|
|
|
518
519
|
BASALT_BIN: string
|
|
519
520
|
}
|
|
520
521
|
|
|
521
|
-
// The block markers the
|
|
522
|
+
// The block markers the AGENTS.md template emits. The begin marker carries the framework version
|
|
522
523
|
// (`<!-- basalt:begin 1.0.0 -->`), so the region is matched by the begin PREFIX, not an exact string.
|
|
523
524
|
const BLOCK_BEGIN_PREFIX = '<!-- basalt:begin'
|
|
524
525
|
const BLOCK_END = '<!-- basalt:end -->'
|
|
525
526
|
export const MANIFEST_PATH = '.basalt/manifest.json'
|
|
527
|
+
/** The host file the managed block is spliced into (and its manifest key). */
|
|
528
|
+
export const AGENTS_MD = 'AGENTS.md'
|
|
529
|
+
/** Where the block lived before it moved to {@link AGENTS_MD} — read only to migrate out of it. */
|
|
530
|
+
export const LEGACY_BLOCK_HOST = 'CLAUDE.md'
|
|
526
531
|
|
|
527
532
|
export function sha256(text: string): string {
|
|
528
533
|
return createHash('sha256').update(text, 'utf8').digest('hex')
|
|
@@ -633,6 +638,156 @@ function applyBlock(host: string, block: string): string {
|
|
|
633
638
|
return base.length === 0 ? `${block.trim()}\n` : `${base}\n\n${block.trim()}\n`
|
|
634
639
|
}
|
|
635
640
|
|
|
641
|
+
/** True when both paths resolve to one file (a symlinked pair). A missing path is simply "no". */
|
|
642
|
+
export function sameFile(a: string, b: string): boolean {
|
|
643
|
+
try {
|
|
644
|
+
return realpathSync(a) === realpathSync(b)
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
647
|
+
throw error
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** True when the host text carries a managed block (begin marker present). */
|
|
652
|
+
export function hasManagedBlock(host: string | null): boolean {
|
|
653
|
+
return host !== null && host.includes(BLOCK_BEGIN_PREFIX)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The block region of a host, or why it cannot be read: duplicate begin markers, or a begin marker
|
|
658
|
+
* with no end. The ONE detector `migrateBlockToAgentsMd` and `doctor` share, so they cannot disagree
|
|
659
|
+
* about whether a `CLAUDE.md` is well-formed.
|
|
660
|
+
*/
|
|
661
|
+
export function scanBlock(host: string): {
|
|
662
|
+
region: { start: number; end: number } | null
|
|
663
|
+
problem: string | null
|
|
664
|
+
} {
|
|
665
|
+
const begin = hasManagedBlock(host)
|
|
666
|
+
if (!begin && !host.includes(BLOCK_END)) return { region: null, problem: null }
|
|
667
|
+
const bad = (problem: string): { region: null; problem: string } => ({ region: null, problem })
|
|
668
|
+
if (!begin) return bad(`\`${BLOCK_END}\` with no \`${BLOCK_BEGIN_PREFIX}\``)
|
|
669
|
+
let region: { start: number; end: number } | null
|
|
670
|
+
try {
|
|
671
|
+
region = findBlockRegion(host)
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return bad((error as Error).message)
|
|
674
|
+
}
|
|
675
|
+
if (region === null) return bad(`\`${BLOCK_BEGIN_PREFIX}\` has no matching \`${BLOCK_END}\``)
|
|
676
|
+
// Exactly one end marker, after the begin: an orphan or extra one means the region is a guess.
|
|
677
|
+
if (host.indexOf(BLOCK_END) < region.start || host.indexOf(BLOCK_END, region.end) !== -1) {
|
|
678
|
+
return bad(`an extra or misplaced \`${BLOCK_END}\` outside the basalt block`)
|
|
679
|
+
}
|
|
680
|
+
return { region, problem: null }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** One line that is exactly the `@AGENTS.md` import — not a prose or code-span mention, not `.bak`. */
|
|
684
|
+
export function isAgentsImportLine(line: string): boolean {
|
|
685
|
+
return line.trim() === `@${AGENTS_MD}`
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Whether a `CLAUDE.md` imports `AGENTS.md`, so Claude Code still loads it. */
|
|
689
|
+
export function importsAgentsMd(text: string): boolean {
|
|
690
|
+
return text.split(/\r?\n/).some(isAgentsImportLine)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export type BlockMigration = {
|
|
694
|
+
/** The legacy host (`CLAUDE.md`) carried a managed block that has to move. */
|
|
695
|
+
pending: boolean
|
|
696
|
+
/** The legacy host was left empty (or `@AGENTS.md` only) and removed. */
|
|
697
|
+
deletedHost: boolean
|
|
698
|
+
/** Why nothing was moved and the run must fail: malformed markers, or two differing blocks. */
|
|
699
|
+
problem: string | null
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Move the managed block out of `CLAUDE.md` into `AGENTS.md` (an install from before the move).
|
|
704
|
+
* Idempotent: a second run finds nothing and returns `pending: false`.
|
|
705
|
+
* 1. The ledger entry is re-keyed `CLAUDE.md` → `AGENTS.md` in memory (the caller persists it), so
|
|
706
|
+
* an unedited legacy block classifies `unchanged`, not untracked.
|
|
707
|
+
* 2. The region is cut out of `CLAUDE.md` and — unless `AGENTS.md` already has one — spliced into
|
|
708
|
+
* `AGENTS.md` AS IT IS, not as the new rendering: the normal three-way classify then sees a
|
|
709
|
+
* hand-edit and skips it like any drifted unit. The rest of `CLAUDE.md` is consumer-owned and
|
|
710
|
+
* stays, except the file is deleted when nothing but whitespace or `@AGENTS.md` is left.
|
|
711
|
+
* 3. Nothing is discarded silently: if `AGENTS.md` already has a block that differs (normalized,
|
|
712
|
+
* as the ledger compares) from the legacy one, or either file's markers are malformed, BOTH
|
|
713
|
+
* files are left byte-identical and `problem` says what to resolve by hand.
|
|
714
|
+
* `dryRun` (`sync --check`) writes nothing but still reports `pending` and `problem`.
|
|
715
|
+
*/
|
|
716
|
+
export function migrateBlockToAgentsMd(
|
|
717
|
+
cwd: string,
|
|
718
|
+
manifest: Manifest,
|
|
719
|
+
opts: { dryRun?: boolean } = {},
|
|
720
|
+
): BlockMigration {
|
|
721
|
+
const legacyHash = manifest.files[LEGACY_BLOCK_HOST]
|
|
722
|
+
if (legacyHash !== undefined) {
|
|
723
|
+
manifest.files[AGENTS_MD] ??= legacyHash
|
|
724
|
+
delete manifest.files[LEGACY_BLOCK_HOST]
|
|
725
|
+
}
|
|
726
|
+
const none: BlockMigration = { pending: false, deletedHost: false, problem: null }
|
|
727
|
+
const legacyAbs = resolve(cwd, LEGACY_BLOCK_HOST)
|
|
728
|
+
const agentsAbs = resolve(cwd, AGENTS_MD)
|
|
729
|
+
const legacy = readIfExists(legacyAbs)
|
|
730
|
+
if (legacy === null) return none
|
|
731
|
+
// A symlinked pair is one file: cutting the block out of "CLAUDE.md" would cut AGENTS.md too.
|
|
732
|
+
// Fail closed when that cannot be proven (EACCES, ELOOP, …) — and say so, not a stack trace.
|
|
733
|
+
try {
|
|
734
|
+
if (sameFile(legacyAbs, agentsAbs)) return none
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const why = (error as NodeJS.ErrnoException).code ?? (error as Error).message
|
|
737
|
+
return { ...none, problem: `cannot tell whether ${LEGACY_BLOCK_HOST} is ${AGENTS_MD} (${why})` }
|
|
738
|
+
}
|
|
739
|
+
const legacyScan = scanBlock(legacy)
|
|
740
|
+
const { region } = legacyScan
|
|
741
|
+
if (legacyScan.problem !== null) {
|
|
742
|
+
return { ...none, problem: `${LEGACY_BLOCK_HOST}: ${legacyScan.problem}` }
|
|
743
|
+
}
|
|
744
|
+
if (region === null) return none
|
|
745
|
+
|
|
746
|
+
const agents = readIfExists(agentsAbs) ?? ''
|
|
747
|
+
const agentsScan = scanBlock(agents)
|
|
748
|
+
if (agentsScan.problem !== null)
|
|
749
|
+
return { ...none, problem: `${AGENTS_MD}: ${agentsScan.problem}` }
|
|
750
|
+
const legacyBlock = legacy.slice(region.start, region.end)
|
|
751
|
+
if (
|
|
752
|
+
agentsScan.region !== null &&
|
|
753
|
+
normalizeForLedger(agents.slice(agentsScan.region.start, agentsScan.region.end)) !==
|
|
754
|
+
normalizeForLedger(legacyBlock)
|
|
755
|
+
) {
|
|
756
|
+
return {
|
|
757
|
+
...none,
|
|
758
|
+
problem:
|
|
759
|
+
`${LEGACY_BLOCK_HOST} and ${AGENTS_MD} each carry a basalt block and they differ — ` +
|
|
760
|
+
`delete the stale block from ${LEGACY_BLOCK_HOST} (or the one in ${AGENTS_MD}, if ` +
|
|
761
|
+
`${LEGACY_BLOCK_HOST}'s is the one you want kept), then re-run.`,
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const before = legacy.slice(0, region.start).trimEnd()
|
|
766
|
+
const after = legacy.slice(region.end).trimStart()
|
|
767
|
+
const remainder = before === '' || after === '' ? `${before}${after}` : `${before}\n\n${after}`
|
|
768
|
+
const deletedHost = remainder
|
|
769
|
+
.split(/\r?\n/)
|
|
770
|
+
.every((l) => l.trim() === '' || isAgentsImportLine(l))
|
|
771
|
+
if (opts.dryRun === true) return { pending: true, deletedHost, problem: null }
|
|
772
|
+
|
|
773
|
+
if (agentsScan.region === null) writeFileEnsuringDir(agentsAbs, applyBlock(agents, legacyBlock))
|
|
774
|
+
if (deletedHost) unlinkSync(legacyAbs)
|
|
775
|
+
else writeFileSync(legacyAbs, `${remainder.trimEnd()}\n`)
|
|
776
|
+
return { pending: true, deletedHost, problem: null }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* The refusal `init` and `sync` run BEFORE any side effect (roots patch, package.json script,
|
|
781
|
+
* manifest): a dry-run migration against a scratch manifest, so a block that cannot be moved
|
|
782
|
+
* safely stops the command with nothing written. Null = safe to proceed.
|
|
783
|
+
*/
|
|
784
|
+
export function blockMigrationProblem(cwd: string): string | null {
|
|
785
|
+
const { problem } = migrateBlockToAgentsMd(cwd, { version: 1, files: {} }, { dryRun: true })
|
|
786
|
+
return problem === null
|
|
787
|
+
? null
|
|
788
|
+
: `${problem.replace(/\.$/, '')}. Nothing else was reconciled — resolve this by hand, then re-run.`
|
|
789
|
+
}
|
|
790
|
+
|
|
636
791
|
/**
|
|
637
792
|
* Explicit `--with-router` / `--with-query` CLI flags force-include a router/query scaffold even
|
|
638
793
|
* when the peer isn't detected in the consumer's package.json yet (e.g. it's about to be added).
|
|
@@ -809,15 +964,17 @@ export function managedFiles(
|
|
|
809
964
|
render: (ctx: RenderContext) => readSource(ctx.pkgRoot, `agent/skills/${name}/SKILL.md`),
|
|
810
965
|
}))
|
|
811
966
|
|
|
812
|
-
|
|
813
|
-
|
|
967
|
+
// AGENTS.md, not CLAUDE.md: Claude Code loads a repo-root AGENTS.md directly, but ANY CLAUDE.md
|
|
968
|
+
// in or above the cwd suppresses it — so the block must not be the reason a CLAUDE.md exists.
|
|
969
|
+
const agentsBlock: ManagedFile = {
|
|
970
|
+
dest: AGENTS_MD,
|
|
814
971
|
mode: 'managed',
|
|
815
972
|
markers: true,
|
|
816
|
-
source: 'agent/templates/
|
|
973
|
+
source: 'agent/templates/AGENTS-block.md.tpl',
|
|
817
974
|
// With markers, render() returns the fully-rendered region INCLUDING its begin/end markers.
|
|
818
|
-
// The writer splices it into the consumer's host
|
|
975
|
+
// The writer splices it into the consumer's host AGENTS.md at apply time.
|
|
819
976
|
render: (ctx) => {
|
|
820
|
-
const tpl = readSource(ctx.pkgRoot, 'agent/templates/
|
|
977
|
+
const tpl = readSource(ctx.pkgRoot, 'agent/templates/AGENTS-block.md.tpl')
|
|
821
978
|
if (tpl === null) return null
|
|
822
979
|
return fillTemplate(tpl, ctx.vars).trim()
|
|
823
980
|
},
|
|
@@ -946,7 +1103,7 @@ export function managedFiles(
|
|
|
946
1103
|
// lefthook/CI config, and writing over or beside it is worse than not writing at all.
|
|
947
1104
|
const rootTooling: ManagedFile[] = placement.isPackageRepoRoot ? [lefthook, ci] : []
|
|
948
1105
|
|
|
949
|
-
return [...rules, ...skills,
|
|
1106
|
+
return [...rules, ...skills, agentsBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds]
|
|
950
1107
|
}
|
|
951
1108
|
|
|
952
1109
|
/**
|
|
@@ -1086,6 +1243,18 @@ export function reportPrune(pruned: PruneResult): void {
|
|
|
1086
1243
|
)
|
|
1087
1244
|
}
|
|
1088
1245
|
|
|
1246
|
+
/** Name what {@link migrateBlockToAgentsMd} did — a file leaving a consumer's tree is never silent. */
|
|
1247
|
+
export function reportBlockMigration(migration: BlockMigration, command: 'init' | 'sync'): void {
|
|
1248
|
+
if (!migration.pending) return
|
|
1249
|
+
const tail = migration.deletedHost
|
|
1250
|
+
? `${LEGACY_BLOCK_HOST} held nothing else and was deleted.`
|
|
1251
|
+
: `the rest of ${LEGACY_BLOCK_HOST} is yours; while it exists Claude Code loads it INSTEAD of ` +
|
|
1252
|
+
`${AGENTS_MD} unless it imports @${AGENTS_MD} — move your content over and delete it.`
|
|
1253
|
+
console.log(
|
|
1254
|
+
`\nbasalt-ui ${command}: moved the basalt block from ${LEGACY_BLOCK_HOST} to ${AGENTS_MD} — ${tail}`,
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1089
1258
|
/**
|
|
1090
1259
|
* Delete the managed rule/skill files this basalt version retired, and drop their manifest entries.
|
|
1091
1260
|
*
|
|
@@ -1194,7 +1363,7 @@ type Classification = 'unchanged' | 'drifted' | 'missing' | 'current'
|
|
|
1194
1363
|
* - Blank out the per-release `BASALT_VERSION` token in a marker's begin line
|
|
1195
1364
|
* (`<!-- basalt:begin 1.0.1 -->` → `<!-- basalt:begin -->`). It changes on every release and is
|
|
1196
1365
|
* the only per-release variable that lands INSIDE the hashed region (the rest of
|
|
1197
|
-
* `
|
|
1366
|
+
* `AGENTS-block.md.tpl` has no other `{{…}}` placeholders today), so stripping it is what lets
|
|
1198
1367
|
* one normalized rendering stand in for every version of a given template body instead of
|
|
1199
1368
|
* needing one ledger entry per release. If a future marker template interpolates another
|
|
1200
1369
|
* per-release var inside its region, extend this normalization the same way.
|
package/src/cli/init.ts
CHANGED
|
@@ -10,9 +10,11 @@ import { deriveSpacing } from '../tokens'
|
|
|
10
10
|
import type { MergeLintResult, ScaffoldFlags } from './index'
|
|
11
11
|
import {
|
|
12
12
|
MANIFEST_PATH,
|
|
13
|
+
blockMigrationProblem,
|
|
13
14
|
findBasaltInstall,
|
|
14
15
|
managedFiles,
|
|
15
16
|
mergeOxlintExtends,
|
|
17
|
+
migrateBlockToAgentsMd,
|
|
16
18
|
normalizeForLedger,
|
|
17
19
|
patchPackageJson,
|
|
18
20
|
placementNotices,
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
readManifest,
|
|
21
23
|
reconcileRoots,
|
|
22
24
|
renderContext,
|
|
25
|
+
reportBlockMigration,
|
|
23
26
|
reportPrune,
|
|
24
27
|
resolvePeerFlags,
|
|
25
28
|
resolvePlacement,
|
|
@@ -52,6 +55,13 @@ const KEPT_FILE_COST: Record<string, string> = {
|
|
|
52
55
|
|
|
53
56
|
/** Scaffold all managed files into the consumer repo, then write the manifest. Idempotent. Returns 0. */
|
|
54
57
|
export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags = {}): number {
|
|
58
|
+
// First, before anything is written: a block that cannot be moved safely refuses the whole run.
|
|
59
|
+
const blockProblem = blockMigrationProblem(cwd)
|
|
60
|
+
if (blockProblem !== null) {
|
|
61
|
+
console.error(`✖ basalt-ui init: ${blockProblem}`)
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// ── Describe the repo BEFORE rendering anything ───────────────────────────
|
|
56
66
|
// Every roots-derived seed (the CI oxfmt globs, DESIGN.md's series path, the default scan
|
|
57
67
|
// exemption) renders from `basalt.roots`, so the key has to exist first. Writing it is what
|
|
@@ -86,6 +96,9 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
|
|
|
86
96
|
const placement = resolvePlacement(cwd)
|
|
87
97
|
const files = managedFiles(peers, placement)
|
|
88
98
|
|
|
99
|
+
// Before the loop: an install from before the move carries the block in CLAUDE.md.
|
|
100
|
+
const migration = migrateBlockToAgentsMd(cwd, manifest)
|
|
101
|
+
|
|
89
102
|
const writtenFiles: string[] = []
|
|
90
103
|
const keptFiles: string[] = []
|
|
91
104
|
const missingSources: string[] = []
|
|
@@ -131,6 +144,7 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
|
|
|
131
144
|
`basalt-ui init: ${writtenFiles.length} written, ${keptFiles.length} kept, manifest at ${MANIFEST_PATH}`,
|
|
132
145
|
)
|
|
133
146
|
reportPrune(pruned)
|
|
147
|
+
reportBlockMigration(migration, 'init')
|
|
134
148
|
if (missingSources.length > 0) {
|
|
135
149
|
console.log(
|
|
136
150
|
`basalt-ui init: ${missingSources.length} shipped asset(s) not present, skipped: ${missingSources.join(', ')}`,
|
package/src/cli/sync.ts
CHANGED
|
@@ -17,8 +17,12 @@ import {
|
|
|
17
17
|
diffSummary,
|
|
18
18
|
findBasaltInstall,
|
|
19
19
|
inspectLefthookGate,
|
|
20
|
+
AGENTS_MD,
|
|
21
|
+
blockMigrationProblem,
|
|
20
22
|
isStringArray,
|
|
23
|
+
LEGACY_BLOCK_HOST,
|
|
21
24
|
managedFiles,
|
|
25
|
+
migrateBlockToAgentsMd,
|
|
22
26
|
normalizeForLedger,
|
|
23
27
|
noteRelocation,
|
|
24
28
|
OXLINT_FILES,
|
|
@@ -32,6 +36,7 @@ import {
|
|
|
32
36
|
reconcileRoots,
|
|
33
37
|
relativePosix,
|
|
34
38
|
renderContext,
|
|
39
|
+
reportBlockMigration,
|
|
35
40
|
reportPrune,
|
|
36
41
|
resolvePeerFlags,
|
|
37
42
|
resolvePlacement,
|
|
@@ -149,6 +154,13 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
149
154
|
return 1
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
// A block that cannot be moved safely refuses the whole run BEFORE reconcileRoots writes anything.
|
|
158
|
+
const blockProblem = blockMigrationProblem(cwd)
|
|
159
|
+
if (blockProblem !== null) {
|
|
160
|
+
console.error(`✖ basalt-ui sync: ${blockProblem}`)
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
152
164
|
// Before renderContext, which reads `basalt.roots` for the roots-derived template variables.
|
|
153
165
|
const rootsState = reconcileRoots(cwd, { write: opts.check !== true })
|
|
154
166
|
const ctx = renderContext(cwd)
|
|
@@ -157,6 +169,10 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
157
169
|
const placement = resolvePlacement(cwd)
|
|
158
170
|
const files = managedFiles(peers, placement)
|
|
159
171
|
|
|
172
|
+
// Before the loop: the block moved from CLAUDE.md to AGENTS.md, and the loop reads AGENTS.md.
|
|
173
|
+
// `--check` only reports it (and stays red until a real sync moves it).
|
|
174
|
+
const migration = migrateBlockToAgentsMd(cwd, manifest, { dryRun: opts.check === true })
|
|
175
|
+
|
|
160
176
|
let updated = 0
|
|
161
177
|
// `created` and `recreated` were one counter, and `0 updated, 20 recreated` is what a full
|
|
162
178
|
// scaffold printed — a word that means "it was here, it went missing, I put it back" describing
|
|
@@ -272,10 +288,16 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
272
288
|
`${retired.join(', ')} — run \`basalt-ui sync\`.`,
|
|
273
289
|
)
|
|
274
290
|
}
|
|
291
|
+
if (migration.pending) {
|
|
292
|
+
console.error(
|
|
293
|
+
`basalt-ui sync --check: the basalt block is still in ${LEGACY_BLOCK_HOST} — it now ` +
|
|
294
|
+
`lives in ${AGENTS_MD}. Run \`basalt-ui sync\` to move it.`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
275
297
|
if (staleForCheck > 0) {
|
|
276
298
|
console.error(`basalt-ui sync --check: ${staleForCheck} managed file(s) out of date.`)
|
|
277
|
-
return 1
|
|
278
299
|
}
|
|
300
|
+
if (staleForCheck > 0 || migration.pending) return 1
|
|
279
301
|
const n = placementSkips.length
|
|
280
302
|
const skips = n === 0 ? '' : ` (${n} permanent placement skip(s) — --verbose for detail)`
|
|
281
303
|
console.log(`✓ basalt-ui sync --check: all managed files current.${skips}`)
|
|
@@ -291,6 +313,7 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
291
313
|
`${skippedDrift} skipped (drift), ${pruned.removed.length} retired.`,
|
|
292
314
|
)
|
|
293
315
|
reportPrune(pruned)
|
|
316
|
+
reportBlockMigration(migration, 'sync')
|
|
294
317
|
if (driftLines.length > 0) {
|
|
295
318
|
console.log('Locally-edited files were skipped (run with --force to overwrite):')
|
|
296
319
|
for (const l of driftLines) console.log(l)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The check + tooltip are the entire success signal — no notification (docs/CONTENT-SPEC.md §7).
|
|
8
8
|
* `color="teal"` resolves through the bridged Mantine family, so it stays on the token layer in
|
|
9
|
-
* both schemes (see the package
|
|
9
|
+
* both schemes (see the package MAINTAINING.md on `cssVariablesResolver`).
|
|
10
10
|
*
|
|
11
11
|
* Not part of the public surface — reach it through `CodeBlock` / `HeadingAnchor`.
|
|
12
12
|
*/
|
package/src/content/glyphs.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* hand-drawn ones that drift apart.
|
|
5
5
|
*
|
|
6
6
|
* Inline SVG rather than an icon dep: the framework takes icons as `ReactNode` from the consumer
|
|
7
|
-
* (see the package
|
|
7
|
+
* (see the package MAINTAINING.md) and ships none, but these are internal chrome of a shipped component,
|
|
8
8
|
* not a consumer-facing slot. `currentColor` + `1em`-agnostic sizing keeps them steerable from CSS
|
|
9
9
|
* — the heading anchor sizes its glyph as an optical ratio of whatever step the heading lands on
|
|
10
10
|
* (docs/DESIGN-SPEC.md §3), while the code-block chrome pins a fixed 14px.
|
|
@@ -146,7 +146,7 @@ export type BasaltVirtualListHandle = {
|
|
|
146
146
|
/**
|
|
147
147
|
* The underlying `@tanstack/react-virtual` instance — the full escape hatch. Returns the RAW
|
|
148
148
|
* `@tanstack/react-virtual` `Virtualizer`, so a future major of that library reaching this shape
|
|
149
|
-
* ships here as a plain `feat:` under the no-majors doctrine (
|
|
149
|
+
* ships here as a plain `feat:` under the no-majors doctrine (root `AGENTS.md` "Commit type
|
|
150
150
|
* discipline"), not a silent break.
|
|
151
151
|
*/
|
|
152
152
|
getVirtualizer: () => Virtualizer<HTMLDivElement, Element>
|
package/src/guard/index.ts
CHANGED
|
@@ -788,7 +788,7 @@ function insideHostTag(codeText: string, offset: number): boolean {
|
|
|
788
788
|
*
|
|
789
789
|
* KEBAB and PascalCase both count, because a repo picks one file-naming law and the convention has
|
|
790
790
|
* to survive either: `edit-session-modal.tsx`, and `CbbiPanel.tsx` in a repo mandating
|
|
791
|
-
* `PascalCase.tsx` for component files (basalt's own root `
|
|
791
|
+
* `PascalCase.tsx` for component files (basalt's own root `AGENTS.md` included). Each needs the
|
|
792
792
|
* leading SUBJECT — a bare `modal.tsx` / `Panel.tsx` is a page module, not the convention.
|
|
793
793
|
*
|
|
794
794
|
* The trade is that a whole file goes unscanned on a naming convention — the same bargain
|
|
File without changes
|