@cosmicdrift/kumiko-framework 0.377.0 → 0.379.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.
Files changed (118) hide show
  1. package/AGENTS.md +4 -1
  2. package/agents/examples/compose-stacks.md +1 -2
  3. package/agents/examples/currencies-global.md +2 -3
  4. package/agents/examples/delivery-notifications.md +25 -2
  5. package/agents/examples/embedded.md +177 -0
  6. package/agents/examples/event-sourcing.md +6 -5
  7. package/agents/examples/legal-pages.md +5 -5
  8. package/agents/examples/reference-data.md +26 -21
  9. package/agents/examples/relations.md +2 -1
  10. package/agents/features.md +5 -5
  11. package/agents/widgets.md +1 -0
  12. package/dist/api/entity-read-access.d.ts +2 -0
  13. package/dist/api/entity-read-access.js +27 -0
  14. package/dist/api/index.d.ts +1 -0
  15. package/dist/api/index.js +1 -0
  16. package/dist/api/server.js +10 -3
  17. package/dist/api/sse-route.d.ts +5 -0
  18. package/dist/api/sse-route.js +59 -9
  19. package/dist/db/entity-table-from-registry.d.ts +2 -1
  20. package/dist/db/entity-table-meta.d.ts +0 -2
  21. package/dist/db/entity-table-meta.js +1 -3
  22. package/dist/db/event-store-executor-write.js +21 -16
  23. package/dist/db/executor-managed-tables.d.ts +6 -0
  24. package/dist/db/executor-managed-tables.js +40 -0
  25. package/dist/db/index.d.ts +4 -2
  26. package/dist/db/index.js +4 -2
  27. package/dist/db/money.d.ts +3 -2
  28. package/dist/db/money.js +2 -7
  29. package/dist/db/queries/backfill-pii.js +22 -12
  30. package/dist/db/queries/database-now.d.ts +5 -0
  31. package/dist/db/queries/database-now.js +12 -0
  32. package/dist/db/queries/event-store-orphan-rows.d.ts +8 -0
  33. package/dist/db/queries/event-store-orphan-rows.js +32 -0
  34. package/dist/db/query.js +28 -0
  35. package/dist/db/reference-data.d.ts +5 -4
  36. package/dist/db/reference-data.js +23 -21
  37. package/dist/db/table-builder.js +2 -0
  38. package/dist/db/tenant-db-runner.d.ts +2 -0
  39. package/dist/db/tenant-db-runner.js +7 -0
  40. package/dist/db/tenant-db.d.ts +3 -0
  41. package/dist/db/tenant-db.js +54 -18
  42. package/dist/engine/boot-validator/entity-handler.js +12 -4
  43. package/dist/engine/boot-validator/form-derivations.d.ts +4 -0
  44. package/dist/engine/boot-validator/form-derivations.js +82 -0
  45. package/dist/engine/boot-validator/query-refs.js +8 -0
  46. package/dist/engine/boot-validator/screens.js +50 -12
  47. package/dist/engine/build-config-feature-schema.js +33 -10
  48. package/dist/engine/embedded-derived.d.ts +1 -1
  49. package/dist/engine/embedded-derived.js +7 -1
  50. package/dist/engine/feature-ui-extensions.js +4 -0
  51. package/dist/engine/form-derivations.d.ts +21 -0
  52. package/dist/engine/form-derivations.js +175 -0
  53. package/dist/engine/index.d.ts +4 -1
  54. package/dist/engine/index.js +2 -0
  55. package/dist/engine/minor-units.d.ts +2 -0
  56. package/dist/engine/minor-units.js +8 -0
  57. package/dist/engine/registry-state.js +2 -0
  58. package/dist/engine/registry-validate.js +4 -1
  59. package/dist/engine/schema-builder.js +18 -4
  60. package/dist/engine/screen-helpers.d.ts +1 -0
  61. package/dist/engine/screen-helpers.js +3 -0
  62. package/dist/engine/soft-delete-cleanup.js +53 -24
  63. package/dist/engine/steps/unsafe-projection-delete.js +2 -1
  64. package/dist/engine/template-variables.d.ts +2 -1
  65. package/dist/engine/template-variables.js +4 -0
  66. package/dist/engine/types/index.d.ts +2 -2
  67. package/dist/engine/with-form-derivations.d.ts +9 -0
  68. package/dist/engine/with-form-derivations.js +16 -0
  69. package/dist/entrypoint/index.d.ts +1 -3
  70. package/dist/errors/reasons.d.ts +1 -0
  71. package/dist/errors/reasons.js +4 -1
  72. package/dist/errors/serialize.d.ts +1 -0
  73. package/dist/errors/serialize.js +9 -5
  74. package/dist/errors/write-error-info.js +3 -2
  75. package/dist/errors/zod-bridge.js +7 -0
  76. package/dist/event-store/index.d.ts +1 -0
  77. package/dist/event-store/index.js +1 -0
  78. package/dist/files/action-form-upload.d.ts +16 -0
  79. package/dist/files/action-form-upload.js +45 -0
  80. package/dist/files/bind-pending-file-refs.d.ts +14 -0
  81. package/dist/files/bind-pending-file-refs.js +44 -4
  82. package/dist/files/create-file-ref.d.ts +3 -0
  83. package/dist/files/create-file-ref.js +41 -13
  84. package/dist/files/file-routes.js +29 -2
  85. package/dist/files/index.d.ts +2 -1
  86. package/dist/files/index.js +1 -0
  87. package/dist/files/purge-expired-file-ref-bytes.d.ts +3 -2
  88. package/dist/files/purge-expired-file-ref-bytes.js +11 -5
  89. package/dist/i18n/settings-hub-keys.js +1 -0
  90. package/dist/internal/testing.d.ts +2 -0
  91. package/dist/internal/testing.js +15 -1
  92. package/dist/migrations/pii-event-backfill.js +14 -7
  93. package/dist/observability/escape-hatch-report.d.ts +1 -1
  94. package/dist/observability/escape-hatch-report.js +14 -3
  95. package/dist/pipeline/cascade-handler.js +79 -10
  96. package/dist/pipeline/dispatch-shared.js +1 -9
  97. package/dist/pipeline/msp-rebuild.js +5 -3
  98. package/dist/pipeline/projection-rebuild.js +4 -2
  99. package/dist/pipeline/projections-runner.js +2 -1
  100. package/dist/rate-limit/index.d.ts +1 -1
  101. package/dist/rate-limit/index.js +1 -1
  102. package/dist/rate-limit/middleware.d.ts +4 -0
  103. package/dist/rate-limit/middleware.js +5 -2
  104. package/dist/search/configure-tenant-index.d.ts +4 -0
  105. package/dist/search/configure-tenant-index.js +8 -0
  106. package/dist/search/index.d.ts +1 -0
  107. package/dist/search/index.js +1 -0
  108. package/dist/ui-types/index.d.ts +4 -2
  109. package/dist/ui-types/index.js +3 -2
  110. package/dist/ui-types/list-row-meta.d.ts +3 -0
  111. package/dist/ui-types/list-row-meta.js +12 -1
  112. package/kumiko-es-tables.json +6 -0
  113. package/package.json +4 -3
  114. package/src/changes.json +142 -0
  115. package/src/scripts/codemod/README.md +20 -0
  116. package/src/scripts/codemod/codemod-support.ts +1 -1
  117. package/src/scripts/codemod/feature-layout.ts +416 -0
  118. package/src/scripts/codemod/package-merge.ts +0 -11
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env bun
2
+ // feature-layout — the runtime of a feature file follows from its path: <feature>/web/** is client
3
+ // code, everything else is server or isomorphic. This moves every file marked as runtime client of a
4
+ // feature that lies outside web/ into <feature>/web/ (sub-structure kept), rewrites each relative
5
+ // import / re-export / import() / mock.module that points at it or out of it, rewrites package.json
6
+ // export targets, and drops the marker. A marker the path already implies (client in web/, test in
7
+ // __tests__/, dev in dev/, tooling in scripts/) is only removed. A target that already exists, or a
8
+ // marker no path rule covers, is reported as manual and left alone.
9
+ //
10
+ // Features are found as src/features/<feature>/ and bundled-features/src/<feature>/ below the target
11
+ // directories; --feature-dir <dir> names a directory that is itself one feature, --features-root <dir>
12
+ // a directory whose children are features.
13
+ //
14
+ // Usage:
15
+ // bun scripts/codemod/feature-layout.ts <targetDir...> [--feature-dir <dir>] [--features-root <dir>]
16
+ // [--check | --dry-run | --write] [--no-format]
17
+ // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1 while
18
+ // any move, marker or manual site is left; --dry-run only reports.
19
+
20
+ import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
21
+ import { dirname, join, relative, resolve } from "node:path";
22
+ import { Node, Project } from "ts-morph";
23
+ import {
24
+ applyEdits,
25
+ type CodemodSite,
26
+ formatWithBiome,
27
+ moduleSpecifierLiterals,
28
+ SCRIPT_EXTENSION,
29
+ type TextEdit,
30
+ } from "./codemod-support.ts";
31
+ import { collectSourceFiles } from "./collect-source-files.ts";
32
+
33
+ const MARKER_HEAD_LINES = 8;
34
+ const MARKER_LINE = /^\s*\/\/\s*@runtime(?:\s+(\w+))?\s*$/;
35
+ const WEB_DIR = "web";
36
+ const IMPLICIT_FEATURE_PATTERNS: readonly RegExp[] = [
37
+ /^(.*\/src\/features\/[^/]+)\/(.+)$/,
38
+ /^(.*\/bundled-features\/src\/[^/]+)\/(.+)$/,
39
+ ];
40
+ const TEST_PATH = /(?:^|\/)__tests__\/|\.(?:test|integration|e2e|spec)\.[tj]sx?$/;
41
+
42
+ export interface FeatureLayoutOptions {
43
+ readonly featureDirs?: readonly string[];
44
+ readonly featuresRoots?: readonly string[];
45
+ }
46
+
47
+ export interface FeatureLayoutMove {
48
+ readonly from: string;
49
+ readonly to: string;
50
+ }
51
+
52
+ export interface FeatureLayoutResult {
53
+ // New content per final path (moved files under their new path).
54
+ readonly writes: ReadonlyMap<string, string>;
55
+ // Old paths that no longer exist after the moves.
56
+ readonly removals: readonly string[];
57
+ readonly moves: readonly FeatureLayoutMove[];
58
+ readonly markersRemoved: readonly string[];
59
+ readonly importRewrites: readonly CodemodSite[];
60
+ readonly manual: readonly CodemodSite[];
61
+ }
62
+
63
+ interface Marker {
64
+ readonly line: number;
65
+ readonly kind: string | undefined;
66
+ }
67
+
68
+ function findMarker(source: string): Marker | undefined {
69
+ const lines = source.split("\n", MARKER_HEAD_LINES);
70
+ for (const [index, text] of lines.entries()) {
71
+ const match = MARKER_LINE.exec(text);
72
+ if (match) return { line: index, kind: match[1] };
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function removeMarkerLine(source: string, line: number): string {
78
+ const lines = source.split("\n");
79
+ lines.splice(line, 1);
80
+ if (line === 0 && lines[0] === "") lines.shift();
81
+ return lines.join("\n");
82
+ }
83
+
84
+ function splitFeaturePath(
85
+ file: string,
86
+ options: FeatureLayoutOptions,
87
+ ): { readonly featureDir: string; readonly rel: string } | undefined {
88
+ for (const dir of options.featureDirs ?? []) {
89
+ if (file.startsWith(`${dir}/`)) return { featureDir: dir, rel: file.slice(dir.length + 1) };
90
+ }
91
+ for (const root of options.featuresRoots ?? []) {
92
+ if (!file.startsWith(`${root}/`)) continue;
93
+ const [feature, ...rest] = file.slice(root.length + 1).split("/");
94
+ if (feature !== undefined && rest.length > 0) {
95
+ return { featureDir: `${root}/${feature}`, rel: rest.join("/") };
96
+ }
97
+ }
98
+ for (const pattern of IMPLICIT_FEATURE_PATTERNS) {
99
+ const match = pattern.exec(file);
100
+ if (match?.[1] !== undefined && match[2] !== undefined) {
101
+ return { featureDir: match[1], rel: match[2] };
102
+ }
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function pathImpliedKind(rel: string): string | undefined {
108
+ if (TEST_PATH.test(rel)) return "test";
109
+ if (rel === WEB_DIR || rel.startsWith(`${WEB_DIR}/`) || /^web\.tsx?$/.test(rel)) return "client";
110
+ if (rel.startsWith("dev/")) return "dev";
111
+ if (rel.startsWith("scripts/")) return "tooling";
112
+ return undefined;
113
+ }
114
+
115
+ function stripScriptExtension(file: string): { readonly base: string; readonly extension: string } {
116
+ const extension = SCRIPT_EXTENSION.exec(file)?.[0] ?? "";
117
+ return { base: file.slice(0, file.length - extension.length), extension };
118
+ }
119
+
120
+ function movedTargetOf(
121
+ absoluteSpecifier: string,
122
+ moved: ReadonlyMap<string, string>,
123
+ ): string | undefined {
124
+ const { base, extension } = stripScriptExtension(absoluteSpecifier);
125
+ const candidates =
126
+ extension === ".js" || extension === ".ts" || extension === ".tsx" || extension === ""
127
+ ? [absoluteSpecifier, `${base}.ts`, `${base}.tsx`]
128
+ : [absoluteSpecifier];
129
+ for (const candidate of candidates) {
130
+ if (moved.has(candidate)) return candidate;
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ function relativeSpecifierFrom(fromDir: string, target: string): string {
136
+ const path = relative(fromDir, target).split("\\").join("/");
137
+ return path.startsWith(".") ? path : `./${path}`;
138
+ }
139
+
140
+ function rewrittenSpecifier(
141
+ specifier: string,
142
+ oldFile: string,
143
+ newFile: string,
144
+ moved: ReadonlyMap<string, string>,
145
+ ): string | undefined {
146
+ if (!specifier.startsWith(".")) return undefined;
147
+ const absolute = resolve(dirname(oldFile), specifier);
148
+ const oldTarget = movedTargetOf(absolute, moved);
149
+ if (oldTarget === undefined && oldFile === newFile) return undefined;
150
+ const newTarget = oldTarget === undefined ? absolute : moved.get(oldTarget);
151
+ if (newTarget === undefined) return undefined;
152
+ let next: string;
153
+ if (oldTarget === undefined) {
154
+ next = relativeSpecifierFrom(dirname(newFile), newTarget);
155
+ if (specifier.endsWith("/")) next = `${next}/`;
156
+ } else {
157
+ const { extension } = stripScriptExtension(absolute);
158
+ const { base } = stripScriptExtension(newTarget);
159
+ const isDirectoryImport = extension === "" && base.endsWith("/index");
160
+ const target = isDirectoryImport ? base.slice(0, -"/index".length) : `${base}${extension}`;
161
+ next = relativeSpecifierFrom(dirname(newFile), target);
162
+ }
163
+ return next === specifier ? undefined : next;
164
+ }
165
+
166
+ function rewriteSpecifiers(
167
+ source: string,
168
+ oldFile: string,
169
+ newFile: string,
170
+ moved: ReadonlyMap<string, string>,
171
+ sites: CodemodSite[],
172
+ ): string {
173
+ const project = new Project({
174
+ useInMemoryFileSystem: true,
175
+ skipAddingFilesFromTsConfig: true,
176
+ skipFileDependencyResolution: true,
177
+ });
178
+ const sourceFile = project.createSourceFile(oldFile, source);
179
+ const edits: TextEdit[] = [];
180
+ for (const literal of moduleSpecifierLiterals(sourceFile)) {
181
+ if (!Node.isStringLiteral(literal)) continue;
182
+ const current = literal.getLiteralValue();
183
+ const next = rewrittenSpecifier(current, oldFile, newFile, moved);
184
+ if (next === undefined) continue;
185
+ sites.push({
186
+ file: oldFile,
187
+ line: literal.getStartLineNumber(),
188
+ description: `${current} -> ${next}`,
189
+ });
190
+ edits.push({ start: literal.getStart() + 1, end: literal.getEnd() - 1, text: next });
191
+ }
192
+ return applyEdits(source, edits);
193
+ }
194
+
195
+ // package.json export targets: "./src/<rel>.ts" and the publishConfig mirror "./dist/<rel>.js|.d.ts".
196
+ function rewritePackageJson(
197
+ source: string,
198
+ packageJsonPath: string,
199
+ moved: ReadonlyMap<string, string>,
200
+ sites: CodemodSite[],
201
+ ): string {
202
+ const packageDir = dirname(packageJsonPath);
203
+ return source.replace(/"(\.\/(?:src|dist)\/[^"]+)"/g, (whole, target: string) => {
204
+ const isDist = target.startsWith("./dist/");
205
+ const sourceTarget = isDist
206
+ ? target.replace("./dist/", "./src/").replace(/(?:\.d\.ts|\.js)$/, "")
207
+ : target;
208
+ const { base } = stripScriptExtension(resolve(packageDir, sourceTarget));
209
+ const candidates = isDist ? [`${base}.ts`, `${base}.tsx`] : [resolve(packageDir, sourceTarget)];
210
+ const oldFile = candidates.find((candidate) => moved.has(candidate));
211
+ const newFile = oldFile === undefined ? undefined : moved.get(oldFile);
212
+ if (newFile === undefined) return whole;
213
+ const newRel = relative(packageDir, newFile).split("\\").join("/");
214
+ const next = isDist
215
+ ? target.replace(/^\.\/dist\/.*?(\.d\.ts|\.js)$/, (_all, ext: string) => {
216
+ return `./dist/${newRel.slice("src/".length).replace(/\.tsx?$/, "")}${ext}`;
217
+ })
218
+ : `./${newRel}`;
219
+ sites.push({
220
+ file: packageJsonPath,
221
+ line: source.slice(0, source.indexOf(whole)).split("\n").length,
222
+ description: `${target} -> ${next}`,
223
+ });
224
+ return `"${next}"`;
225
+ });
226
+ }
227
+
228
+ type MarkerAction =
229
+ | { readonly action: "strip" }
230
+ | { readonly action: "move"; readonly target: string }
231
+ | { readonly action: "manual"; readonly description: string };
232
+
233
+ function actionForMarker(
234
+ marker: Marker,
235
+ location: { readonly featureDir: string; readonly rel: string },
236
+ isTaken: (target: string) => boolean,
237
+ ): MarkerAction {
238
+ if (marker.kind === undefined) {
239
+ return { action: "manual", description: "bare @runtime marker without a kind" };
240
+ }
241
+ const implied = pathImpliedKind(location.rel);
242
+ if (marker.kind === implied || (marker.kind === "client" && implied !== undefined)) {
243
+ return { action: "strip" };
244
+ }
245
+ if (marker.kind !== "client") {
246
+ return { action: "manual", description: `@runtime ${marker.kind} has no path rule here` };
247
+ }
248
+ const target = join(location.featureDir, WEB_DIR, location.rel);
249
+ return isTaken(target)
250
+ ? { action: "manual", description: `cannot move to ${WEB_DIR}/${location.rel}: target exists` }
251
+ : { action: "move", target };
252
+ }
253
+
254
+ function buildWrites(
255
+ files: ReadonlyMap<string, string>,
256
+ stripped: ReadonlyMap<string, string>,
257
+ moved: ReadonlyMap<string, string>,
258
+ importRewrites: CodemodSite[],
259
+ ): Map<string, string> {
260
+ const writes = new Map<string, string>();
261
+ const basenames = [...moved.keys()].map((file) => stripScriptExtension(baseName(file)).base);
262
+ for (const [file, original] of files) {
263
+ const content = stripped.get(file) ?? original;
264
+ const newFile = moved.get(file) ?? file;
265
+ if (file.endsWith("package.json")) {
266
+ const next =
267
+ moved.size > 0 ? rewritePackageJson(content, file, moved, importRewrites) : content;
268
+ if (next !== original) writes.set(file, next);
269
+ continue;
270
+ }
271
+ const mentionsMoved = moved.has(file) || basenames.some((name) => content.includes(name));
272
+ const next = mentionsMoved
273
+ ? rewriteSpecifiers(content, file, newFile, moved, importRewrites)
274
+ : content;
275
+ if (next !== original || newFile !== file) writes.set(newFile, next);
276
+ }
277
+ return writes;
278
+ }
279
+
280
+ export function applyFeatureLayout(
281
+ files: ReadonlyMap<string, string>,
282
+ options: FeatureLayoutOptions = {},
283
+ ): FeatureLayoutResult {
284
+ const manual: CodemodSite[] = [];
285
+ const markersRemoved: string[] = [];
286
+ const stripped = new Map<string, string>();
287
+ const moved = new Map<string, string>();
288
+ const claimedTargets = new Set<string>();
289
+ const isTaken = (target: string): boolean => files.has(target) || claimedTargets.has(target);
290
+
291
+ for (const [file, source] of files) {
292
+ const location = splitFeaturePath(file, options);
293
+ const marker = location === undefined ? undefined : findMarker(source);
294
+ if (location === undefined || marker === undefined) continue;
295
+ const decision = actionForMarker(marker, location, isTaken);
296
+ if (decision.action === "manual") {
297
+ manual.push({ file, line: marker.line + 1, description: decision.description });
298
+ continue;
299
+ }
300
+ if (decision.action === "move") {
301
+ claimedTargets.add(decision.target);
302
+ moved.set(file, decision.target);
303
+ }
304
+ stripped.set(file, removeMarkerLine(source, marker.line));
305
+ markersRemoved.push(file);
306
+ }
307
+
308
+ const importRewrites: CodemodSite[] = [];
309
+ const writes = buildWrites(files, stripped, moved, importRewrites);
310
+
311
+ return {
312
+ writes,
313
+ removals: [...moved.keys()],
314
+ moves: [...moved].map(([from, to]) => ({ from, to })),
315
+ markersRemoved,
316
+ importRewrites,
317
+ manual,
318
+ };
319
+ }
320
+
321
+ function baseName(file: string): string {
322
+ return file.slice(file.lastIndexOf("/") + 1);
323
+ }
324
+
325
+ type Mode = "write" | "dry-run" | "check";
326
+
327
+ interface CliOptions extends FeatureLayoutOptions {
328
+ readonly mode: Mode;
329
+ readonly format: boolean;
330
+ readonly paths: readonly string[];
331
+ }
332
+
333
+ function parseArgs(argv: readonly string[]): CliOptions {
334
+ let mode: Mode = "write";
335
+ let format = true;
336
+ const paths: string[] = [];
337
+ const featureDirs: string[] = [];
338
+ const featuresRoots: string[] = [];
339
+ for (let i = 0; i < argv.length; i++) {
340
+ const arg = argv[i] ?? "";
341
+ if (arg === "--check") mode = "check";
342
+ else if (arg === "--dry-run") mode = "dry-run";
343
+ else if (arg === "--write") mode = "write";
344
+ else if (arg === "--no-format") format = false;
345
+ else if (arg === "--feature-dir" || arg === "--features-root") {
346
+ const value = argv[++i];
347
+ if (value === undefined) throw new Error(`${arg} needs a directory`);
348
+ (arg === "--feature-dir" ? featureDirs : featuresRoots).push(resolve(value));
349
+ } else if (arg.startsWith("--")) throw new Error(`unknown flag: ${arg}`);
350
+ else paths.push(arg);
351
+ }
352
+ if (paths.length === 0) throw new Error("at least one target directory is required");
353
+ return { mode, format, paths, featureDirs, featuresRoots };
354
+ }
355
+
356
+ function readTargetFiles(paths: readonly string[]): Map<string, string> {
357
+ const files = new Map<string, string>();
358
+ const names = collectSourceFiles(paths, {
359
+ isMigratableFile: (name) => /\.(ts|tsx)$/.test(name) && !name.endsWith(".d.ts"),
360
+ });
361
+ const packageJsons = collectSourceFiles(paths, {
362
+ isMigratableFile: (name) => name === "package.json",
363
+ });
364
+ for (const file of [...names, ...packageJsons]) {
365
+ if (file.includes("/__tests__/fixtures/")) continue;
366
+ if (statSync(file).isFile()) files.set(file, readFileSync(file, "utf8"));
367
+ }
368
+ return files;
369
+ }
370
+
371
+ function display(file: string): string {
372
+ const fromCwd = relative(process.cwd(), file);
373
+ return fromCwd.startsWith("..") ? file : fromCwd;
374
+ }
375
+
376
+ function writeResult(result: FeatureLayoutResult): void {
377
+ for (const old of result.removals) rmSync(old);
378
+ for (const [file, content] of result.writes) {
379
+ mkdirSync(dirname(file), { recursive: true });
380
+ writeFileSync(file, content);
381
+ }
382
+ }
383
+
384
+ async function main(argv: readonly string[]): Promise<void> {
385
+ let options: CliOptions;
386
+ try {
387
+ options = parseArgs(argv);
388
+ } catch (err) {
389
+ console.error(`[feature-layout] ${err instanceof Error ? err.message : String(err)}`);
390
+ process.exit(2);
391
+ }
392
+ const files = readTargetFiles(options.paths);
393
+ const result = applyFeatureLayout(files, options);
394
+ for (const move of result.moves) {
395
+ console.log(`${display(move.from)} moved -> ${display(move.to)}`);
396
+ }
397
+ for (const file of result.markersRemoved) console.log(`${display(file)} marker removed`);
398
+ for (const site of result.manual) {
399
+ console.log(`${display(site.file)}:${site.line} manual ${site.description}`);
400
+ }
401
+ console.log(
402
+ `\n[feature-layout] ${options.mode === "write" ? "moved" : "movable"}: ${result.moves.length} file(s); markers: ${result.markersRemoved.length}; import sites: ${result.importRewrites.length}; manual: ${result.manual.length}`,
403
+ );
404
+ if (options.mode === "write") {
405
+ writeResult(result);
406
+ if (options.format) {
407
+ const changed = [...result.writes.keys()].filter((file) => /\.(ts|tsx)$/.test(file));
408
+ if (changed.length > 0)
409
+ await formatWithBiome("feature-layout", changed, resolve(options.paths[0] ?? "."));
410
+ }
411
+ }
412
+ const pending = result.markersRemoved.length + result.manual.length;
413
+ process.exit(options.mode === "check" && pending > 0 ? 1 : 0);
414
+ }
415
+
416
+ if (import.meta.main) await main(process.argv.slice(2));
@@ -42,17 +42,6 @@ export const PACKAGE_MERGE_MAPPINGS: readonly PackageMergeMapping[] = [
42
42
  const DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "peerDependencies"] as const;
43
43
 
44
44
  const SKIPPED_PATH_SUFFIXES: readonly string[] = [
45
- "packages/http/src/index.ts",
46
- "packages/repo-manifest/src/index.ts",
47
- "packages/repo-manifest/src/app-ports/index.ts",
48
- "packages/headless/src/index.ts",
49
- "packages/headless/src/apex/index.ts",
50
- "packages/headless/src/dispatcher/index.ts",
51
- "packages/headless/src/locale-routing/index.ts",
52
- "packages/dispatcher-live/src/index.ts",
53
- "packages/renderer-web/src/index.ts",
54
- "packages/renderer-web/src/theme-plugin.ts",
55
- "packages/renderer-web/src/styles.css",
56
45
  "packages/framework/src/scripts/codemod/package-merge.ts",
57
46
  "packages/framework/src/__tests__/package-merge-codemod.test.ts",
58
47
  ];