Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,690 @@
# Category -> plugin -> package assignment manifest.
#
# Layout: `packages/<category>/<plugin>/<pkg>/` — three levels under packages/.
#
# Each top-level key (other than the `__categories__` block and `__retired*__`
# bookkeeping keys) is a plugin name. Each plugin's value is the list of
# packages that live in `packages/<category>/<plugin>/<pkg>/`, where
# <category> is the super-category assigned to that plugin in
# `__categories__:` below.
#
# Rules enforced by tooling:
# 1. Every package referenced here must be unique across all plugins.
# 2. Every plugin must appear in exactly one super-category in __categories__.
# 3. The 18 super-category vocabulary below is closed — changing it requires
# an OpenSpec change against the monorepo-layout capability.
#
# Source of truth: FOLDERS.md.
# 18 super-categories. Each maps to the list of plugin folders that live
# directly under packages/<category>/. Adding or removing a super-category
# requires an OpenSpec change.
__categories__:
foundation: [core, data, random, timing, bitpacker, barcode, flow, etl, feature-flags]
persistence: [database, storage, cache, search]
networking: [network, drive, events]
communication: [messaging, emails, webhook-client]
api-stack: [api]
identity: [auth]
security-stack: [security, crypto]
observability: [monitor]
media: [audio, audio-fx, audio-ir, image, fonts, video]
ui: [react, forms]
threejs: [r3f]
localization: [i18n]
geospatial: [geo, logistics]
money: [finance, payment]
commerce: [booking, contacts]
testkit: [testing]
devtools: [tooling, docs, docker]
gaming: [games]
__retired__: []
__retired_categories__:
- codes
- config
- control-flow
- dev
- email
- media
- observability
- primitives
- search
- streams
- svg
- third-party
- validation
- vault
- workflow
# ----------------------------------------------------------------
api:
- api-schemas
- api-types
- api-manifest
- api
- create-suaveplan-plugin
- api-gateway
- api-worker-rpc
- api-logger-worker-proxy
- api-plugin-admin
- api-plugin-analytics
- api-plugin-audit
- api-plugin-auth
- api-plugin-campaigns
- api-plugin-cms
- api-plugin-database
- api-plugin-events
- api-plugin-export
- api-plugin-file-upload
- api-plugin-graphql
- api-plugin-import
- api-plugin-rate-limit
- api-plugin-search
- api-plugin-site
- api-plugin-social
- api-plugin-sse
- api-plugin-tenant
- api-plugin-user
- api-plugin-webhook
- api-shared
- api-2pc
# ----------------------------------------------------------------
audio:
- audio-analysis-loudness
- audio-codecs
- audio-core
- audio-metadata
- voice
- audio-controllers
- audio-engine
- audio-engine-browser
- audio-fx-plugin-base
- audio-fx-plugin-audio-analyzer
- audio-fx-plugin-audio-player
- audio-fx-plugin-auto-filter
- audio-fx-plugin-chorus
- audio-fx-plugin-compressor
- audio-fx-plugin-delay
- audio-fx-plugin-distortion
- audio-fx-plugin-eq
- audio-fx-plugin-filter
- audio-fx-plugin-flanger
- audio-fx-plugin-game-audio
- audio-fx-plugin-gate
- audio-fx-plugin-glados
- audio-fx-plugin-limiter
- audio-fx-plugin-microphone
- audio-fx-plugin-mixer
- audio-fx-plugin-phaser
- audio-fx-plugin-pitch-shifter
- audio-fx-plugin-reverb
- audio-fx-plugin-sampler
- audio-fx-plugin-spatial
- audio-fx-plugin-synthesizer
- audio-fx-plugin-tremolo
- audio-fx-plugin-widener
- audio-sprite-packer
- dmx
- python-audio-fingerprint
- r3f-audio
- r3f-dmx
- react-audio
- react-audio-fingerprint
- react-audio-player
- react-audio-stream
- react-midi
# ----------------------------------------------------------------
auth:
- auth
- jwt-tenant
- macos-keychain
- oauth-client
- permissions
- webauthn
- react-auth
- react-auth-whitelabel
- react-permissions
# ----------------------------------------------------------------
barcode:
- barcode
- qrcode
# ----------------------------------------------------------------
bitpacker:
- bitpacker
- bitpacker-compressed
- bitpacker-crypto
- bitpacker-delta
- bitpacker-game
- bitpacker-network
- bitpacker-schema
- bitpacker-strategies
- bitpacker-streaming
- bitpacker-validation
# ----------------------------------------------------------------
commerce:
- api-plugin-asset-register
- api-plugin-bank
- api-plugin-banking
- api-plugin-payment
- bank
- banking
- booking
- currency
- currency-provider-binance
- currency-provider-coingecko
- currency-provider-fixer
- currency-providers
- money
- react-bank
# ----------------------------------------------------------------
payment:
- payment
- payment-providers
- payment-provider-adyen
- payment-provider-mollie
- payment-provider-paypal
- payment-provider-square
- payment-provider-stripe
# ----------------------------------------------------------------
contacts:
- api-plugin-contacts
- contacts
- macos-contacts
- react-contacts
# ----------------------------------------------------------------
core:
- config
- env
- error
- schema-registry
- types
- typescript-config
- utils
# ----------------------------------------------------------------
crypto:
- crypto
- crypto-bin
- crypto-browser
- crypto-cipher
- crypto-cli
- crypto-core
- crypto-curve
- crypto-hash
- crypto-rsa
- crypto-sodium
- crypto-utils
- crypto-vault
- api-plugin-vault
- cryptocurrency
- keys
- react-crypto
# ----------------------------------------------------------------
data:
- bytes
- clipboard
- data-masking
- dedup
- diff
- json
- media
- mime
- packer
- packer-browser
- pagination
- pwa
- react-packer
- serialize
- templates
- validation
- web-workers
# ----------------------------------------------------------------
database:
- db
- db-sqlite
- migration
- surrealdb
- seed
# ----------------------------------------------------------------
docs:
- compress-svg
- sitemap
- api-docs
- docs
- pdf
- react-docs
- react-pdf
- react-rich-text
# ----------------------------------------------------------------
drive:
- drive
- drive-mesh
- drive-mesh-protocol
- drive-plugin-chunks
- drive-react-sharing
- drive-sharing
- drive-template
- drive-transport-https
- drive-transport-webrtc
- drive-webrtc
# ----------------------------------------------------------------
emails:
- dmarc
- domain-normalizer
- suppression
- email-processing
- email-templates
- emailer
- campaigns
# ----------------------------------------------------------------
events:
- event-sourcing
- events
- events-metrics
- events-otel
- events-schema
- events-transport
- broadcast-channel
- message-bridge
# ----------------------------------------------------------------
cache:
- cache
- cache-redis
- cache-storage
# ----------------------------------------------------------------
flow:
- cbor
- crdt
- idempotency
- job-scheduler
- queue
- queue-distribution
- queue-distribution-http
- queue-distribution-worker-thread
- rate-limiter
- retry
- rules
- rules-hono
- streams
- undo
# ----------------------------------------------------------------
fonts:
- font-compress
- font-convert
- font-subset
- font-decompress
- react-font-decompress
# ----------------------------------------------------------------
forms:
- react-forms
- form-builder
- react-admin-forms
- react-calendar
- react-color-picker
- react-file-upload
- react-wizard
# ----------------------------------------------------------------
games:
- animation-events
- api-plugin-anticheat
- api-plugin-game-persistence
- api-plugin-game-server
- api-plugin-matchmaking
- behavior-tree
- bone-mapping
- building
- character-physics
- character-vehicle-picker
- chat
- combat
- controller
- crafting
- day-night
- destructibles
- dialogue
- economy
- ecs
- emotes
- fauna
- fruit-machine
- game-loop
- game-persistence
- game-server
- game-state-schemas
- heightmap-tiles
- hitbox
- ik-solver
- leaderboard
- lobby
- mixamo
- npc
- osm-buildings
- party
- progression
- quest
- react-controller
- react-crafting
- react-dialogue
- react-game-hud
- react-game-map
- react-game-settings
- react-social-game
- vehicle-physics
- weapons
- weather
# ----------------------------------------------------------------
geo:
- geo
- geo-earth
- geo-planet
- solar-system
- stars
- tides
- universe
# ----------------------------------------------------------------
gtap:
- api-plugin-gtap
# ----------------------------------------------------------------
i18n:
- i18n
- i18n-backend-chained
- i18n-backend-fs-multifile
- i18n-backend-http
- i18n-backend-memory
- i18n-backend-storage
- i18n-detector
- i18n-formatter-collator
- i18n-formatter-currency
- i18n-formatter-date
- i18n-formatter-display-name
- i18n-formatter-list
- i18n-formatter-number
- i18n-formatter-relative-time
- i18n-formatter-segmenter
- i18n-icu-parser
- i18n-shared
- locale-data
- react-i18n
# ----------------------------------------------------------------
image:
- compress-image
- exif
- caddy-imgproxy
- emblem-solver
- image
- image-quantisation
- react-image
# ----------------------------------------------------------------
logistics:
- api-plugin-customs
- api-plugin-logistics
- api-plugin-port-tracking
- api-plugin-procurement
- api-plugin-warehouse-ops
- customs
- delivery-tracking
- logistics
- port-tracking
- ports
- procurement
- react-port-tracking
- warehouse-ops
# ----------------------------------------------------------------
messaging:
- elevenlabs
- api-plugin-messaging
- api-plugin-notifications
- discord-client
- imessage-adapter
- react-notifications
- slack-client
- sms
- sms-providers
- telegram-client
# ----------------------------------------------------------------
monitor:
- analytics-browser
- audit-logging
- health-check
- logger
- opentelemetry
# ----------------------------------------------------------------
network:
- network
- network-bootstrap
- network-discovery-crdt
- network-discovery-gundb
- network-discovery-hybrid
- network-discovery-peerjs
- network-discovery-trystero
- network-discovery-webtorrent
- network-gundb-server
- network-libp2p-server
- network-plugin-assignment
- network-plugin-audit-mesh
- network-plugin-auth
- network-plugin-circuit-breaker
- network-plugin-content-delivery
- network-plugin-crypto-messages
- network-plugin-devtools
- network-plugin-failover
- network-plugin-governance
- network-plugin-intra-relay
- network-plugin-middleware
- network-plugin-migration
- network-plugin-monitor
- network-plugin-partition
- network-plugin-protocol
- network-plugin-qos
- network-plugin-registry
- network-plugin-relay
- network-plugin-server-bridge
- network-plugin-session
- network-plugin-simulation
- network-plugin-state
- network-plugin-topology
- network-plugin-transport-manager
- network-testing
- network-transport-hyperswarm
- network-transport-libp2p
- network-transport-standby
- network-transport-webrtc
- network-transport-websockets
- network-trystero-server
- react-network-libp2p-demo
- service-discovery
- websocket-gateway
- network-observability
- network-quality
# ----------------------------------------------------------------
platform:
- etl
- feature-flags
- search
- webhook-client
- audience-rules
- notification-channel
# ----------------------------------------------------------------
r3f:
- r3f-networking
- r3f-animation
- r3f-booking
- r3f-building
- r3f-buildings
- r3f-camera
- r3f-character
- r3f-character-vehicle
- r3f-character-vehicle-picker
- r3f-combat
- r3f-controller
- r3f-earth
- r3f-emulator
- r3f-environment
- r3f-lod
- r3f-minimap
- r3f-npc
- r3f-number-plate
- r3f-parallax
- r3f-particles
- r3f-pathfinding
- r3f-performance
- r3f-physics-cascade
- r3f-physics-core
- r3f-physics-rapier
- r3f-shaders
- r3f-terrain
- r3f-ui
- r3f-vegetation
- r3f-vehicle
- r3f-water
- r3f-weather
- r3f-world
- r3f-xr
# ----------------------------------------------------------------
random:
- fsm
- noise
- pseudo-random
- random
# ----------------------------------------------------------------
react:
- react-ui
- react-campaigns
- react-charts
- react-command-palette
- react-consent-overlay
- react-data-table
- react-dnd
- react-error-boundary
- react-hotkeys
- react-infinite-scroll
- react-iphone
- react-kanban
- react-loading
- react-modal
- react-skeleton
- react-social
- react-timeline
- react-tour
- react-tree-view
- react-ui-social
- react-virtual
# ----------------------------------------------------------------
security:
- hibp
- jws
- otp
- password-formats
- password-generator
- password-strength
- security-codes
- slsa
- srp
- tpm-struct
- vault
- vault-sync
- x509
# ----------------------------------------------------------------
storage:
- storage
- storage-browser-indexeddb
- storage-browser-localstorage
- storage-browser-opfs
- storage-file
- storage-s3
- storage-uri-plugin-fallback
# ----------------------------------------------------------------
store:
- api-plugin-inventory
- api-plugin-store
- api-plugin-subscription
- api-plugin-virtual-currency
- r3f-inventory
- react-store
- store
- subscription
- virtual-currency
# ----------------------------------------------------------------
testing:
- ab-testing
- testing
- testing-dom
- testing-e2e
- testing-integration
- testing-integration-garage-s3
- testing-integration-mailpit
- testing-integration-postgres
- testing-integration-redis
- testing-integration-step-ca
- testing-integration-surrealdb
- testing-integration-typesense
# ----------------------------------------------------------------
timing:
- cron
- datetime
- timing
# ----------------------------------------------------------------
tooling:
- cli
- install-agent-skills
- installer
- mcp
- readline
- electrobun
- license
- linter
- overview
- profiler
- sandbox
- server-keys
- test-vite
- wip
# ----------------------------------------------------------------
vatdash:
- api-plugin-vatdash-bridge
# ----------------------------------------------------------------
video:
- react-video
- video
- video-thumbnails

View File

@@ -0,0 +1,361 @@
#!/usr/bin/env bun
/**
* Generates MANIFEST.md at repo root from on-disk packages/<super>/<plugin>/<pkg>/package.json.
*
* Replaces @suaveplan/overview. Sections emitted:
* 1. Plugins table — plugin | package count | total workspace deps
* 2. Packages table — npm name | plugin | tier | version | workspace deps
* 3. Tier overlay — packages grouped by inferred tier
* 4. Dependency graph — DOT block for Graphviz
*
* Usage:
* bun scripts/folder-reorg/generate-manifest.ts # write MANIFEST.md
* bun scripts/folder-reorg/generate-manifest.ts --check # exit 1 if drift
* bun scripts/folder-reorg/generate-manifest.ts --stdout # print only
*
* Tier inference priority (matches openspec/specs/monorepo-structure/spec.md):
* python → r3f → react → browser → server → universal
*/
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..");
const manifestPath = join(repoRoot, "MANIFEST.md");
const packagesRoot = join(repoRoot, "packages");
type Tier = "python" | "r3f" | "react" | "browser" | "server" | "universal";
interface PkgJson {
name: string;
version: string;
description?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
browser?: unknown;
bin?: unknown;
engines?: Record<string, string>;
python?: unknown;
}
interface PkgRecord {
name: string; // @suaveplan/<x>
short: string; // <x>
category: string; // top-level super-category (e.g. foundation, threejs)
plugin: string; // mid-level plugin folder (e.g. core, r3f)
dirName: string;
version: string;
description: string;
tier: Tier;
workspaceDeps: string[]; // sorted, dedup
}
const SERVER_DEP_HINTS = new Set([
"bun:sqlite",
"node:fs",
"node:net",
"node:cluster",
"node:dgram",
"node:tls",
"node:dns",
"node:child_process",
"pg",
"ioredis",
"better-sqlite3",
"@sqlite.org/sqlite-wasm-node",
]);
function readPkgJson(absDir: string): PkgJson | null {
const f = join(absDir, "package.json");
if (!existsSync(f)) return null;
try {
return JSON.parse(readFileSync(f, "utf8")) as PkgJson;
} catch {
return null;
}
}
function inferTier(short: string, p: PkgJson): Tier {
if (p.python !== undefined || short.startsWith("python-")) return "python";
const peerKeys = Object.keys(p.peerDependencies ?? {});
if (
peerKeys.includes("@react-three/fiber") ||
peerKeys.includes("three") ||
short.startsWith("r3f-")
)
return "r3f";
if (peerKeys.includes("react") || short.startsWith("react-"))
return "react";
if (p.browser !== undefined) return "browser";
const enginesKeys = Object.keys(p.engines ?? {});
if (
enginesKeys.length > 0 &&
!enginesKeys.includes("node") &&
!enginesKeys.includes("bun")
)
return "browser";
if (p.bin !== undefined) return "server";
const allDeps = { ...p.dependencies, ...p.devDependencies };
for (const key of Object.keys(allDeps)) {
if (SERVER_DEP_HINTS.has(key)) return "server";
}
return "universal";
}
function collectWorkspaceDeps(p: PkgJson): string[] {
const out = new Set<string>();
for (const map of [
p.dependencies,
p.peerDependencies,
p.optionalDependencies,
]) {
if (!map) continue;
for (const [name, range] of Object.entries(map)) {
if (
typeof range === "string" &&
range.startsWith("workspace:") &&
name.startsWith("@suaveplan/")
) {
out.add(name);
}
}
}
return [...out].sort();
}
function discoverPackages(): PkgRecord[] {
if (!existsSync(packagesRoot)) return [];
const records: PkgRecord[] = [];
for (const category of readdirSync(packagesRoot)) {
const categoryPath = join(packagesRoot, category);
if (!statSync(categoryPath).isDirectory()) continue;
for (const plugin of readdirSync(categoryPath)) {
const pluginPath = join(categoryPath, plugin);
if (!statSync(pluginPath).isDirectory()) continue;
for (const pkgDir of readdirSync(pluginPath)) {
const pkgPath = join(pluginPath, pkgDir);
if (!statSync(pkgPath).isDirectory()) continue;
const pj = readPkgJson(pkgPath);
if (pj === null) continue;
const short = pj.name.replace(/^@suaveplan\//, "");
records.push({
name: pj.name,
short,
category,
plugin,
dirName: pkgDir,
version: pj.version ?? "0.1.0",
description: pj.description ?? "",
tier: inferTier(short, pj),
workspaceDeps: collectWorkspaceDeps(pj),
});
}
}
}
records.sort((a, b) => a.name.localeCompare(b.name));
return records;
}
function truncate(s: string, max: number): string {
if (s.length <= max) return s;
return `${s.slice(0, max - 1).trimEnd()}…`;
}
function emitPluginsSection(records: PkgRecord[]): string {
const counts = new Map<
string,
{ category: string; pkgs: number; deps: number }
>();
for (const r of records) {
const key = `${r.category}/${r.plugin}`;
const cur = counts.get(key) ?? {
category: r.category,
pkgs: 0,
deps: 0,
};
cur.pkgs += 1;
cur.deps += r.workspaceDeps.length;
counts.set(key, cur);
}
const lines: string[] = [];
lines.push("## Plugins");
lines.push("");
lines.push("| Category | Plugin | Packages | Workspace deps |");
lines.push("|---|---|---:|---:|");
for (const [key, c] of [...counts.entries()].sort()) {
const plugin = key.split("/")[1];
lines.push(
`| \`${c.category}\` | \`${plugin}\` | ${c.pkgs} | ${c.deps} |`
);
}
return lines.join("\n");
}
function emitPackagesSection(records: PkgRecord[]): string {
const lines: string[] = [];
lines.push("## Packages");
lines.push("");
lines.push(
"| Package | Category | Plugin | Tier | Version | Deps | Description |"
);
lines.push("|---|---|---|---|---|---:|---|");
for (const r of records) {
lines.push(
`| \`${r.name}\` | \`${r.category}\` | \`${r.plugin}\` | ${r.tier} | ${r.version} | ${r.workspaceDeps.length} | ${truncate(r.description, 80)} |`
);
}
return lines.join("\n");
}
function emitTierOverlaySection(records: PkgRecord[]): string {
const byTier = new Map<Tier, PkgRecord[]>();
const tierOrder: Tier[] = [
"universal",
"browser",
"react",
"r3f",
"server",
"python",
];
for (const t of tierOrder) byTier.set(t, []);
for (const r of records) byTier.get(r.tier)?.push(r);
const lines: string[] = [];
lines.push("## Tier overlay");
lines.push("");
lines.push(
"Tier is inferred per `openspec/specs/monorepo-structure/spec.md` priority order: python → r3f → react → browser → server → universal."
);
lines.push("");
for (const tier of tierOrder) {
const list = byTier.get(tier) ?? [];
if (list.length === 0) continue;
lines.push(`### ${tier} (${list.length})`);
lines.push("");
for (const r of list)
lines.push(`- \`${r.name}\` (\`${r.category}/${r.plugin}\`)`);
lines.push("");
}
return lines.join("\n").trimEnd();
}
function emitDependencyGraphSection(records: PkgRecord[]): string {
const known = new Set(records.map((r) => r.name));
const lines: string[] = [];
lines.push("## Dependency graph");
lines.push("");
lines.push(
"Workspace edges only. Pipe to Graphviz: `awk '/^```dot/,/^```$/' MANIFEST.md | sed '1d;$d' | dot -Tsvg > graph.svg`."
);
lines.push("");
lines.push("```dot");
lines.push("digraph workspace {");
lines.push(" rankdir=LR;");
lines.push(' node [shape=box, fontname="monospace", fontsize=10];');
for (const r of records) {
for (const dep of r.workspaceDeps) {
if (!known.has(dep)) continue;
lines.push(` "${r.name}" -> "${dep}";`);
}
}
lines.push("}");
lines.push("```");
return lines.join("\n");
}
function emitHeader(records: PkgRecord[]): string {
const tierCounts = new Map<Tier, number>();
for (const r of records)
tierCounts.set(r.tier, (tierCounts.get(r.tier) ?? 0) + 1);
const tiers = [...tierCounts.entries()]
.map(([t, c]) => `${t}: ${c}`)
.sort()
.join(", ");
const plugins = new Set(records.map((r) => `${r.category}/${r.plugin}`))
.size;
const categories = new Set(records.map((r) => r.category)).size;
const lines: string[] = [];
lines.push("# Suaveplan Monorepo Manifest");
lines.push("");
lines.push(
"> **Auto-generated by `scripts/folder-reorg/generate-manifest.ts`. Do not edit by hand — run the script after package changes.**"
);
lines.push("");
lines.push(
`Packages: **${records.length}** across **${plugins}** plugins in **${categories}** categories. Tiers: ${tiers || "—"}.`
);
return lines.join("\n");
}
function buildManifest(records: PkgRecord[]): string {
if (records.length === 0) {
return [
"# Suaveplan Monorepo Manifest",
"",
"> **Auto-generated by `scripts/folder-reorg/generate-manifest.ts`.**",
"",
"_No packages discovered under `packages/`. Run the folder reorg migration first._",
"",
].join("\n");
}
const sections = [
emitHeader(records),
emitPluginsSection(records),
emitPackagesSection(records),
emitTierOverlaySection(records),
emitDependencyGraphSection(records),
];
return `${sections.join("\n\n")}\n`;
}
function main(): void {
const argv = process.argv.slice(2);
const checkMode = argv.includes("--check");
const stdoutMode = argv.includes("--stdout");
const records = discoverPackages();
const generated = buildManifest(records);
if (stdoutMode) {
process.stdout.write(generated);
return;
}
if (checkMode) {
const onDisk = existsSync(manifestPath)
? readFileSync(manifestPath, "utf8")
: "";
if (onDisk === generated) {
console.log(
`✓ MANIFEST.md is up to date (${records.length} packages).`
);
process.exit(0);
}
console.error(
"✗ MANIFEST.md is stale. Re-run: bun scripts/folder-reorg/generate-manifest.ts"
);
process.exit(1);
}
writeFileSync(manifestPath, generated);
console.log(
`✓ Wrote ${manifestPath} (${records.length} packages, ${new Set(records.map((r) => `${r.category}/${r.plugin}`)).size} plugins, ${new Set(records.map((r) => r.category)).size} categories).`
);
}
main();

View File

@@ -0,0 +1,410 @@
#!/usr/bin/env bun
/**
* Folder reorganisation migration script.
*
* Reads scripts/folder-reorg/assignment.yml and moves every package currently
* under universal/<domain>/<pkg>/ to packages/<plugin>/<pkg>/ via `git mv`.
*
* Run modes:
* bun scripts/folder-reorg/migrate-folders.ts --dry-run # plan only
* bun scripts/folder-reorg/migrate-folders.ts # execute
* bun scripts/folder-reorg/migrate-folders.ts --check # validate manifest only
*
* Exits non-zero on:
* - duplicate package across plugins
* - on-disk package not present in any plugin's list
* - destination path already exists
* - git working tree dirty (execute mode only)
*/
import { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmdirSync,
rmSync,
statSync,
} from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..");
const assignmentPath = join(here, "assignment.yml");
const LEGACY_ROOTS = [
"universal",
"browser",
"react",
"r3f",
"server",
"finance",
"mcp",
"python",
"gaming",
"api",
] as const;
type Mode = "dry-run" | "execute" | "check";
interface Assignment {
[plugin: string]: string[];
}
interface PlannedMove {
pkg: string;
plugin: string;
from: string; // absolute
to: string; // absolute
}
const log = (...args: unknown[]) => console.log(...args);
const warn = (...args: unknown[]) => console.warn("⚠ ", ...args);
const fail = (msg: string): never => {
console.error(`✗ ${msg}`);
process.exit(1);
};
function git(args: string[], cwd: string = repoRoot): string {
return execFileSync("git", args, { cwd, encoding: "utf8" });
}
function parseMode(argv: string[]): Mode {
if (argv.includes("--dry-run")) return "dry-run";
if (argv.includes("--check")) return "check";
return "execute";
}
function loadAssignment(): Assignment {
if (!existsSync(assignmentPath)) {
fail(`Assignment manifest not found: ${assignmentPath}`);
}
const raw = readFileSync(assignmentPath, "utf8");
const parsed = parseYaml(raw) as unknown;
if (typeof parsed !== "object" || parsed === null) {
fail("assignment.yml did not parse to an object");
}
const a = parsed as Record<string, unknown>;
for (const [plugin, pkgs] of Object.entries(a)) {
if (!Array.isArray(pkgs)) fail(`Plugin "${plugin}" is not a list`);
for (const p of pkgs) {
if (typeof p !== "string")
fail(`Plugin "${plugin}" has non-string entry`);
}
}
return a as Assignment;
}
interface ParsedAssignment {
pkgToPlugin: Map<string, string>;
retired: Set<string>;
}
function validateUniqueness(assignment: Assignment): ParsedAssignment {
const pkgToPlugin = new Map<string, string>();
const retired = new Set<string>(assignment.__retired__ ?? []);
const duplicates: string[] = [];
for (const [plugin, pkgs] of Object.entries(assignment)) {
if (plugin === "__retired__") continue;
for (const pkg of pkgs) {
if (retired.has(pkg)) {
duplicates.push(
` ${pkg}: listed in plugin "${plugin}" AND in __retired__`
);
continue;
}
const prior = pkgToPlugin.get(pkg);
if (prior !== undefined) {
duplicates.push(` ${pkg}: in "${prior}" AND "${plugin}"`);
} else {
pkgToPlugin.set(pkg, plugin);
}
}
}
if (duplicates.length > 0) {
fail(`Duplicate package assignments found:\n${duplicates.join("\n")}`);
}
return { pkgToPlugin, retired };
}
function discoverOnDiskPackages(): Map<string, string> {
const result = new Map<string, string>();
for (const root of LEGACY_ROOTS) {
const rootPath = join(repoRoot, root);
if (!existsSync(rootPath) || !statSync(rootPath).isDirectory())
continue;
for (const domain of readdirSync(rootPath)) {
const domainPath = join(rootPath, domain);
if (!statSync(domainPath).isDirectory()) continue;
for (const pkg of readdirSync(domainPath)) {
const pkgPath = join(domainPath, pkg);
if (!statSync(pkgPath).isDirectory()) continue;
if (!existsSync(join(pkgPath, "package.json"))) continue;
if (result.has(pkg)) {
fail(
`On-disk duplicate package directory: "${pkg}" exists in both ${result.get(pkg)} and ${pkgPath}`
);
}
result.set(pkg, pkgPath);
}
}
}
return result;
}
function planMoves(
parsed: ParsedAssignment,
onDisk: Map<string, string>
): { moves: PlannedMove[]; orphans: string[]; retiredOnDisk: string[] } {
const moves: PlannedMove[] = [];
const orphans: string[] = [];
const retiredOnDisk: string[] = [];
for (const [pkg, fromAbs] of onDisk) {
if (parsed.retired.has(pkg)) {
retiredOnDisk.push(pkg);
continue;
}
const plugin = parsed.pkgToPlugin.get(pkg);
if (plugin === undefined) {
orphans.push(pkg);
continue;
}
const toAbs = join(repoRoot, "packages", plugin, pkg);
moves.push({ pkg, plugin, from: fromAbs, to: toAbs });
}
moves.sort((a, b) =>
a.plugin === b.plugin
? a.pkg.localeCompare(b.pkg)
: a.plugin.localeCompare(b.plugin)
);
return { moves, orphans, retiredOnDisk };
}
function checkGitClean(): void {
const out = git(["status", "--porcelain"]);
if (out.trim().length > 0) {
fail(
`Working tree is dirty — commit or stash before migrating.\n\n${out}\n` +
"If you really want to proceed with uncommitted changes, run with --dry-run first to verify the plan."
);
}
}
function ensureDestinationsClear(moves: PlannedMove[]): void {
const collisions: string[] = [];
for (const m of moves) {
if (existsSync(m.to)) collisions.push(` ${relative(repoRoot, m.to)}`);
}
if (collisions.length > 0) {
fail(`Destination paths already exist:\n${collisions.join("\n")}`);
}
}
// Skip these from `cp -R` — build/test artifacts + dependency trees, regenerated
// by `bun install` post-migration. Skipping keeps moves under 1s per package
// instead of minutes (a single testing-integration package node_modules is ~500MB).
const COPY_SKIP = new Set([
"node_modules",
"dist",
".turbo",
"coverage",
".bd_modules",
".tsbuildinfo",
]);
function copyTrackedOnly(fromAbs: string, toAbs: string): void {
cpSync(fromAbs, toAbs, {
recursive: true,
filter: (src) => {
const base = src.split("/").pop() ?? "";
if (COPY_SKIP.has(base)) return false;
if (base.endsWith(".tsbuildinfo")) return false;
return true;
},
});
}
function moveOne(fromRel: string, toRel: string): "git-mv" | "cp-rm" {
// Try `git mv` first — preserves rename in the index cleanly.
try {
git(["mv", fromRel, toRel]);
return "git-mv";
} catch (err) {
const msg = (err as Error).message;
if (!msg.includes("Permission denied")) throw err;
}
// WSL2/DrvFs fallback: rename syscall blocked by Windows-side handle.
// Copy tracked files only (skip node_modules etc), remove source, restage.
// Git's rename detection still works for `log --follow` on similar files.
const fromAbs = join(repoRoot, fromRel);
const toAbs = join(repoRoot, toRel);
copyTrackedOnly(fromAbs, toAbs);
rmSync(fromAbs, { recursive: true, force: true });
git(["add", toRel]);
git(["rm", "-r", "--quiet", fromRel]);
return "cp-rm";
}
function executeMoves(moves: PlannedMove[]): void {
const seenPluginDirs = new Set<string>();
let cpRmCount = 0;
for (const m of moves) {
const pluginDir = join(repoRoot, "packages", m.plugin);
if (!seenPluginDirs.has(pluginDir)) {
mkdirSync(pluginDir, { recursive: true });
seenPluginDirs.add(pluginDir);
}
const fromRel = relative(repoRoot, m.from);
const toRel = relative(repoRoot, m.to);
const method = moveOne(fromRel, toRel);
const tag = method === "cp-rm" ? " cp+rm " : " git mv ";
log(`${tag}${fromRel} → ${toRel}`);
if (method === "cp-rm") cpRmCount += 1;
}
if (cpRmCount > 0) {
log("");
log(
`(${cpRmCount}/${moves.length} moves used cp+rm fallback due to WSL2 rename block.)`
);
}
}
function pruneEmptyDir(absPath: string): boolean {
// Returns true if the directory was removed.
if (!existsSync(absPath) || !statSync(absPath).isDirectory()) return false;
const entries = readdirSync(absPath);
let allRemoved = true;
for (const entry of entries) {
const child = join(absPath, entry);
if (statSync(child).isDirectory()) {
if (!pruneEmptyDir(child)) allRemoved = false;
} else {
allRemoved = false;
}
}
if (allRemoved && readdirSync(absPath).length === 0) {
rmdirSync(absPath);
return true;
}
return false;
}
function pruneEmptyLegacyDirs(): void {
for (const root of LEGACY_ROOTS) {
const rootPath = join(repoRoot, root);
if (pruneEmptyDir(rootPath)) {
log(` rmdir ${root}/`);
}
}
}
function summarise(
moves: PlannedMove[],
orphans: string[],
retiredOnDisk: string[],
assignment: Assignment,
onDisk: Map<string, string>,
mode: Mode
): void {
const pluginCounts = new Map<string, number>();
for (const m of moves)
pluginCounts.set(m.plugin, (pluginCounts.get(m.plugin) ?? 0) + 1);
const pluginKeys = Object.keys(assignment).filter(
(k) => k !== "__retired__"
);
const totalAssigned = pluginKeys.reduce(
(n, k) => n + (assignment[k]?.length ?? 0),
0
);
log("");
log(`Mode: ${mode}`);
log(`Plugins defined: ${pluginKeys.length}`);
log(`Packages assigned: ${totalAssigned}`);
log(`Packages retired: ${assignment.__retired__?.length ?? 0}`);
log(`Packages on disk: ${onDisk.size}`);
log(`Packages to migrate: ${moves.length}`);
log(`Packages on disk (retired, skipped): ${retiredOnDisk.length}`);
log(`Packages future-only: ${totalAssigned - moves.length}`);
log("");
log("Per-plugin migration counts:");
for (const [plugin, count] of [...pluginCounts.entries()].sort()) {
log(` ${plugin.padEnd(24)} ${count}`);
}
if (retiredOnDisk.length > 0) {
log("");
log("Retired packages on disk (NOT migrated; remove via tasks.md §8):");
for (const p of retiredOnDisk)
log(` ${p} (${relative(repoRoot, onDisk.get(p) ?? "")})`);
}
if (orphans.length > 0) {
log("");
warn("On-disk packages not assigned to any plugin:");
for (const p of orphans) warn(` ${p}`);
fail(
"Refusing to proceed — every on-disk package must appear in assignment.yml (or in __retired__)."
);
}
}
function main(): void {
const mode = parseMode(process.argv.slice(2));
log(`folder-reorg/migrate-folders.ts — mode: ${mode}`);
const assignment = loadAssignment();
const parsed = validateUniqueness(assignment);
if (mode === "check") {
const pluginCount = Object.keys(assignment).filter(
(k) => k !== "__retired__"
).length;
log(
`✓ Manifest valid: ${parsed.pkgToPlugin.size} unique packages across ${pluginCount} plugins, ${parsed.retired.size} retired.`
);
return;
}
const onDisk = discoverOnDiskPackages();
const { moves, orphans, retiredOnDisk } = planMoves(parsed, onDisk);
summarise(moves, orphans, retiredOnDisk, assignment, onDisk, mode);
if (mode === "dry-run") {
log("");
log("Planned moves (first 10):");
for (const m of moves.slice(0, 10)) {
log(
` ${relative(repoRoot, m.from)} → ${relative(repoRoot, m.to)}`
);
}
if (moves.length > 10) log(` ...and ${moves.length - 10} more`);
log("");
log("✓ Dry run complete. Re-run without --dry-run to execute.");
return;
}
checkGitClean();
ensureDestinationsClear(moves);
log("");
log("Executing moves:");
executeMoves(moves);
pruneEmptyLegacyDirs();
log("");
log(`✓ Migrated ${moves.length} packages.`);
log("");
log("Next steps:");
log(' 1. Update root package.json workspaces to: "packages/*/*"');
log(" 2. Run: rm -rf node_modules bun.lockb && bun install --force");
log(" 3. Run: bunx turbo lint typecheck test");
log(
" 4. Commit: chore(structure): move all packages to domain-first layout"
);
}
main();

View File

@@ -0,0 +1,409 @@
#!/usr/bin/env bun
/**
* Category realignment migration script.
*
* Reads scripts/folder-reorg/assignment.yml and moves every package currently
* under packages/<old-category>/<pkg>/ to packages/<new-category>/<pkg>/
* via `git mv`.
*
* Run modes:
* bun scripts/folder-reorg/realign-to-categories.ts --dry-run # plan only
* bun scripts/folder-reorg/realign-to-categories.ts # execute
* bun scripts/folder-reorg/realign-to-categories.ts --check # validate manifest only
*
* Exits non-zero on:
* - duplicate package across categories
* - on-disk package not present in any category's list
* - destination path already exists
* - git working tree dirty (execute mode only)
*/
import { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmdirSync,
rmSync,
statSync,
} from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..");
const assignmentPath = join(here, "assignment.yml");
const packagesRoot = join(repoRoot, "packages");
type Mode = "dry-run" | "execute" | "check";
interface Assignment {
[category: string]: string[];
}
interface PlannedMove {
pkg: string;
category: string;
from: string; // absolute
to: string; // absolute
}
const log = (...args: unknown[]) => console.log(...args);
const warn = (...args: unknown[]) => console.warn("⚠ ", ...args);
const fail = (msg: string): never => {
console.error(`✗ ${msg}`);
process.exit(1);
};
function git(args: string[], cwd: string = repoRoot): string {
return execFileSync("git", args, { cwd, encoding: "utf8" });
}
function parseMode(argv: string[]): Mode {
if (argv.includes("--dry-run")) return "dry-run";
if (argv.includes("--check")) return "check";
return "execute";
}
function loadAssignment(): Assignment {
if (!existsSync(assignmentPath)) {
fail(`Assignment manifest not found: ${assignmentPath}`);
}
const raw = readFileSync(assignmentPath, "utf8");
const parsed = parseYaml(raw) as unknown;
if (typeof parsed !== "object" || parsed === null) {
fail("assignment.yml did not parse to an object");
}
const a = parsed as Record<string, unknown>;
for (const [category, pkgs] of Object.entries(a)) {
if (!Array.isArray(pkgs)) fail(`Category "${category}" is not a list`);
for (const p of pkgs) {
if (typeof p !== "string")
fail(`Category "${category}" has non-string entry`);
}
}
return a as Assignment;
}
interface ParsedAssignment {
pkgToCategory: Map<string, string>;
retiredPackages: Set<string>;
retiredCategories: Set<string>;
}
function validateUniqueness(assignment: Assignment): ParsedAssignment {
const pkgToCategory = new Map<string, string>();
const retiredPackages = new Set<string>(assignment.__retired__ ?? []);
const retiredCategories = new Set<string>(
Array.isArray(assignment.__retired_categories__)
? assignment.__retired_categories__
: []
);
const duplicates: string[] = [];
for (const [category, pkgs] of Object.entries(assignment)) {
if (category === "__retired__" || category === "__retired_categories__")
continue;
for (const pkg of pkgs) {
if (retiredPackages.has(pkg)) {
duplicates.push(
` ${pkg}: listed in category "${category}" AND in __retired__`
);
continue;
}
const prior = pkgToCategory.get(pkg);
if (prior !== undefined) {
duplicates.push(` ${pkg}: in "${prior}" AND "${category}"`);
} else {
pkgToCategory.set(pkg, category);
}
}
}
if (duplicates.length > 0) {
fail(`Duplicate package assignments found:\n${duplicates.join("\n")}`);
}
return { pkgToCategory, retiredPackages, retiredCategories };
}
function discoverOnDiskPackages(): Map<string, string> {
const result = new Map<string, string>();
if (!existsSync(packagesRoot) || !statSync(packagesRoot).isDirectory()) {
fail(`Packages root does not exist: ${packagesRoot}`);
}
for (const category of readdirSync(packagesRoot)) {
const categoryPath = join(packagesRoot, category);
if (!statSync(categoryPath).isDirectory()) continue;
for (const pkg of readdirSync(categoryPath)) {
const pkgPath = join(categoryPath, pkg);
if (!statSync(pkgPath).isDirectory()) continue;
if (!existsSync(join(pkgPath, "package.json"))) continue;
if (result.has(pkg)) {
fail(
`On-disk duplicate package directory: "${pkg}" exists in both ${result.get(pkg)} and ${pkgPath}`
);
}
result.set(pkg, pkgPath);
}
}
return result;
}
function planMoves(
parsed: ParsedAssignment,
onDisk: Map<string, string>
): { moves: PlannedMove[]; orphans: string[]; retiredOnDisk: string[] } {
const moves: PlannedMove[] = [];
const orphans: string[] = [];
const retiredOnDisk: string[] = [];
for (const [pkg, fromAbs] of onDisk) {
if (parsed.retiredPackages.has(pkg)) {
retiredOnDisk.push(pkg);
continue;
}
const category = parsed.pkgToCategory.get(pkg);
if (category === undefined) {
orphans.push(pkg);
continue;
}
const toAbs = join(packagesRoot, category, pkg);
if (toAbs !== fromAbs) {
moves.push({ pkg, category, from: fromAbs, to: toAbs });
}
}
moves.sort((a, b) =>
a.category === b.category
? a.pkg.localeCompare(b.pkg)
: a.category.localeCompare(b.category)
);
return { moves, orphans, retiredOnDisk };
}
function checkGitClean(): void {
const out = git(["status", "--porcelain"]);
if (out.trim().length > 0) {
fail(
`Working tree is dirty — commit or stash before migrating.\n\n${out}\n` +
"If you really want to proceed with uncommitted changes, run with --dry-run first to verify the plan."
);
}
}
function ensureDestinationsClear(moves: PlannedMove[]): void {
const collisions: string[] = [];
for (const m of moves) {
if (existsSync(m.to)) collisions.push(` ${relative(repoRoot, m.to)}`);
}
if (collisions.length > 0) {
fail(`Destination paths already exist:\n${collisions.join("\n")}`);
}
}
const COPY_SKIP = new Set([
"node_modules",
"dist",
".turbo",
"coverage",
".bd_modules",
".tsbuildinfo",
]);
function copyTrackedOnly(fromAbs: string, toAbs: string): void {
cpSync(fromAbs, toAbs, {
recursive: true,
filter: (src) => {
const base = src.split("/").pop() ?? "";
if (COPY_SKIP.has(base)) return false;
if (base.endsWith(".tsbuildinfo")) return false;
return true;
},
});
}
function moveOne(fromRel: string, toRel: string): "git-mv" | "cp-rm" {
try {
git(["mv", fromRel, toRel]);
return "git-mv";
} catch (err) {
const msg = (err as Error).message;
if (!msg.includes("Permission denied")) throw err;
}
const fromAbs = join(repoRoot, fromRel);
const toAbs = join(repoRoot, toRel);
copyTrackedOnly(fromAbs, toAbs);
rmSync(fromAbs, { recursive: true, force: true });
git(["add", toRel]);
git(["rm", "-r", "--quiet", fromRel]);
return "cp-rm";
}
function executeMoves(moves: PlannedMove[]): void {
const seenCategoryDirs = new Set<string>();
let cpRmCount = 0;
for (const m of moves) {
const categoryDir = join(packagesRoot, m.category);
if (!seenCategoryDirs.has(categoryDir)) {
mkdirSync(categoryDir, { recursive: true });
seenCategoryDirs.add(categoryDir);
}
const fromRel = relative(repoRoot, m.from);
const toRel = relative(repoRoot, m.to);
const method = moveOne(fromRel, toRel);
const tag = method === "cp-rm" ? " cp+rm " : " git mv ";
log(`${tag}${fromRel} → ${toRel}`);
if (method === "cp-rm") cpRmCount += 1;
}
if (cpRmCount > 0) {
log("");
log(
`(${cpRmCount}/${moves.length} moves used cp+rm fallback due to WSL2 rename block.)`
);
}
}
function pruneEmptyDir(absPath: string): boolean {
if (!existsSync(absPath) || !statSync(absPath).isDirectory()) return false;
const entries = readdirSync(absPath);
let allRemoved = true;
for (const entry of entries) {
const child = join(absPath, entry);
if (statSync(child).isDirectory()) {
if (!pruneEmptyDir(child)) allRemoved = false;
} else {
allRemoved = false;
}
}
if (allRemoved && readdirSync(absPath).length === 0) {
rmdirSync(absPath);
return true;
}
return false;
}
function pruneRetiredCategories(retiredCategories: Set<string>): void {
for (const category of retiredCategories) {
const categoryPath = join(packagesRoot, category);
if (pruneEmptyDir(categoryPath)) {
log(` rmdir packages/${category}/`);
}
}
}
function summarise(
moves: PlannedMove[],
orphans: string[],
retiredOnDisk: string[],
assignment: Assignment,
onDisk: Map<string, string>,
mode: Mode
): void {
const categoryCounts = new Map<string, number>();
for (const m of moves)
categoryCounts.set(
m.category,
(categoryCounts.get(m.category) ?? 0) + 1
);
const categoryKeys = Object.keys(assignment).filter(
(k) => k !== "__retired__"
);
const totalAssigned = categoryKeys.reduce(
(n, k) => n + (assignment[k]?.length ?? 0),
0
);
log("");
log(`Mode: ${mode}`);
log(`Categories defined: ${categoryKeys.length}`);
log(`Packages assigned: ${totalAssigned}`);
log(`Packages retired: ${assignment.__retired__?.length ?? 0}`);
log(
`Categories retired: ${assignment.__retired_categories__?.length ?? 0}`
);
log(`Packages on disk: ${onDisk.size}`);
log(`Packages to migrate: ${moves.length}`);
log(
`Packages already correct: ${onDisk.size - moves.length - orphans.length - retiredOnDisk.length}`
);
log(`Packages on disk (retired, skipped): ${retiredOnDisk.length}`);
log(`Packages future-only: ${totalAssigned - onDisk.size}`);
log("");
log("Per-category migration counts:");
for (const [category, count] of [...categoryCounts.entries()].sort()) {
log(` ${category.padEnd(24)} ${count}`);
}
if (retiredOnDisk.length > 0) {
log("");
log("Retired packages on disk (NOT migrated; remove via tasks.md §8):");
for (const p of retiredOnDisk)
log(` ${p} (${relative(repoRoot, onDisk.get(p) ?? "")})`);
}
if (orphans.length > 0) {
log("");
warn("On-disk packages not assigned to any category:");
for (const p of orphans) warn(` ${p}`);
fail(
"Refusing to proceed — every on-disk package must appear in assignment.yml (or in __retired__)."
);
}
}
function main(): void {
const mode = parseMode(process.argv.slice(2));
log(`folder-reorg/realign-to-categories.ts — mode: ${mode}`);
const assignment = loadAssignment();
const parsed = validateUniqueness(assignment);
if (mode === "check") {
const categoryCount = Object.keys(assignment).filter(
(k) => k !== "__retired__" && k !== "__retired_categories__"
).length;
log(
`✓ Manifest valid: ${parsed.pkgToCategory.size} unique packages across ${categoryCount} categories, ${parsed.retiredPackages.size} retired.`
);
return;
}
const onDisk = discoverOnDiskPackages();
const { moves, orphans, retiredOnDisk } = planMoves(parsed, onDisk);
summarise(moves, orphans, retiredOnDisk, assignment, onDisk, mode);
if (mode === "dry-run") {
log("");
log("Planned moves (first 10):");
for (const m of moves.slice(0, 10)) {
log(
` ${relative(repoRoot, m.from)} → ${relative(repoRoot, m.to)}`
);
}
if (moves.length > 10) log(` ...and ${moves.length - 10} more`);
log("");
log("✓ Dry run complete. Re-run without --dry-run to execute.");
return;
}
checkGitClean();
ensureDestinationsClear(moves);
log("");
log("Executing moves:");
executeMoves(moves);
pruneRetiredCategories(parsed.retiredCategories);
log("");
log(`✓ Migrated ${moves.length} packages.`);
log("");
log("Next steps:");
log(
' 1. Run: rm -rf node_modules bun.lockb && find . -type d -name "node_modules" -exec rm -rf {} + && bun install --force'
);
log(" 2. Run: bunx turbo lint typecheck test");
log(" 3. Run: bun scripts/folder-reorg/generate-manifest.ts");
log(
" 4. Commit: chore(structure): realign packages to FOLDERS.md categories"
);
}
main();