@omnifyjp/omnify 5.8.18 → 5.8.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "5.8.18",
3
+ "version": "5.8.20",
4
4
  "description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "zod": "^3.24.0"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@omnifyjp/omnify-darwin-arm64": "5.8.18",
40
- "@omnifyjp/omnify-darwin-x64": "5.8.18",
41
- "@omnifyjp/omnify-linux-x64": "5.8.18",
42
- "@omnifyjp/omnify-linux-arm64": "5.8.18",
43
- "@omnifyjp/omnify-win32-x64": "5.8.18"
39
+ "@omnifyjp/omnify-darwin-arm64": "5.8.20",
40
+ "@omnifyjp/omnify-darwin-x64": "5.8.20",
41
+ "@omnifyjp/omnify-linux-x64": "5.8.20",
42
+ "@omnifyjp/omnify-linux-arm64": "5.8.20",
43
+ "@omnifyjp/omnify-win32-x64": "5.8.20"
44
44
  }
45
45
  }
package/ts-dist/cli.js CHANGED
@@ -30,7 +30,8 @@ import { parse as parseYaml } from 'yaml';
30
30
  import { generateTypeScript } from './generator.js';
31
31
  import { generatePhp, derivePhpConfig } from './php/index.js';
32
32
  import { pruneOrphanServiceFiles } from './php/orphan-cleanup.js';
33
- import { findSiblingWithSameBasename } from './php/sibling-skip.js';
33
+ import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
34
+ import { runPintFormat } from './php/pint-format.js';
34
35
  import { resolveInput, sniffInputKind } from './input-resolver.js';
35
36
  function resolveFromConfig(configPath) {
36
37
  const raw = readFileSync(configPath, 'utf-8');
@@ -222,6 +223,7 @@ program
222
223
  let phpSkipped = 0;
223
224
  let phpSkippedSibling = 0;
224
225
  const skippedSiblingExamples = [];
226
+ const writtenPaths = [];
225
227
  for (const file of phpFiles) {
226
228
  const filePath = resolve(configDir, file.path);
227
229
  mkdirSync(dirname(filePath), { recursive: true });
@@ -229,21 +231,29 @@ program
229
231
  phpSkipped++;
230
232
  continue;
231
233
  }
232
- // Issue #98 v5.8.18: skip user-editable stub when a project-owned
233
- // sibling with the same basename exists elsewhere in the layer's
234
- // editable subtree (e.g. project organizes Requests by domain
235
- // folder `Admin/BrandStoreRequest.php` while omnify wanted to
236
- // emit the FLAT default stub `BrandStoreRequest.php` at the
237
- // layer root). The flat stub is unused noise in that layout,
238
- // and every `omnify generate` would otherwise re-create it,
239
- // forcing the dev to delete it manually after each run.
240
- // Only applies to user-editable files (overwrite=false); base
241
- // files always regenerate. Scope: scan ONLY the file's parent
242
- // directory recursively — not sibling directories — so a
243
- // grouped `Cms/<Name>.php` stub doesn't get skipped by an
244
- // unrelated `Admin/<Name>.php`.
234
+ // Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
235
+ // when a project-owned sibling with the same basename exists
236
+ // elsewhere under the project's `app/` root. Two patterns
237
+ // covered:
238
+ // 1. Domain subfolder organization (#98 v5.8.18): project
239
+ // hand-writes `app/Http/Requests/Admin/BrandStoreRequest.php`;
240
+ // omnify wants to emit the flat default
241
+ // `app/Http/Requests/BrandStoreRequest.php`. The flat
242
+ // stub is unused noise.
243
+ // 2. v4 v5 upgrade (#99 v5.8.19): project has v4 wrapper at
244
+ // `app/Services/Omnify/<Name>Service.php`; v5 wants to
245
+ // emit at the new modular path
246
+ // `app/Omnify/Services/<Name>Service.php`. Two wrappers
247
+ // compete; the v4 one carries the project's actual logic.
248
+ //
249
+ // Scope: scan from the project's `app/` directory (walked up
250
+ // from the target). Falls back to the file's parent dir when
251
+ // no `app/` ancestor is found (e.g. unusual rootPath layout).
252
+ // Only applies to user-editable files (`overwrite=false`); base
253
+ // files always regenerate.
245
254
  if (!file.overwrite) {
246
- const collision = findSiblingWithSameBasename(dirname(filePath), basename(filePath), filePath);
255
+ const scanRoot = findAppRootAncestor(filePath) ?? dirname(filePath);
256
+ const collision = findSiblingWithSameBasename(scanRoot, basename(filePath), filePath);
247
257
  if (collision) {
248
258
  phpSkippedSibling++;
249
259
  if (skippedSiblingExamples.length < 3) {
@@ -253,6 +263,7 @@ program
253
263
  }
254
264
  }
255
265
  writeFileSync(filePath, file.content, 'utf-8');
266
+ writtenPaths.push(filePath);
256
267
  if (file.overwrite || !existsSync(filePath)) {
257
268
  phpOverwritten++;
258
269
  }
@@ -274,6 +285,26 @@ program
274
285
  console.log(` ... and ${phpSkippedSibling - skippedSiblingExamples.length} more`);
275
286
  }
276
287
  }
288
+ // Issue #100 v5.8.20: auto-format every written PHP file via
289
+ // Laravel Pint when it's installed in the project. Same preset
290
+ // the project's CI already uses, so codegen output stops failing
291
+ // `pint --test` and devs no longer need a manual sweep after
292
+ // every `omnify generate`. Silent no-op when Pint isn't found.
293
+ if (writtenPaths.length > 0) {
294
+ const pintResult = runPintFormat(writtenPaths);
295
+ if (pintResult.ran) {
296
+ if (pintResult.exitStatus === 0) {
297
+ console.log(` Formatted ${pintResult.formattedCount} PHP file(s) via Laravel Pint`);
298
+ }
299
+ else {
300
+ console.warn(` [omnify] Laravel Pint exited with status ${pintResult.exitStatus}; generated files may not match project formatting preset.`);
301
+ if (pintResult.errorMessage) {
302
+ const trimmed = pintResult.errorMessage.trim().split('\n').slice(0, 5).join('\n');
303
+ console.warn(` ${trimmed}`);
304
+ }
305
+ }
306
+ }
307
+ }
277
308
  // Orphan cleanup: when a schema is removed, renamed, flipped from
278
309
  // kind:object → kind:pivot, or opted out via `options.service: false`,
279
310
  // its previously-emitted `*ServiceBase.php` becomes orphan and
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Auto-format generated PHP files via Laravel Pint. Issue #100 v5.8.20.
3
+ *
4
+ * Reporter scenario (godx-jp/godx-kintai 2026-04-26 audit): every
5
+ * `omnify generate --force` produces 150+ PHP files that fail Laravel
6
+ * Pint's standard preset (`vendor/bin/pint --test`):
7
+ *
8
+ * - ordered_imports — `use` statements not alphabetised
9
+ * - fully_qualified_strict_types — exception types referenced via FQN
10
+ * - class_attributes_separation — missing blank lines between members
11
+ * - function_declaration — function-keyword spacing
12
+ * - unary_operator_spaces — `! \$x` vs `!\$x`
13
+ * - no_unused_imports — codegen leaves the occasional dangling import
14
+ * - single_line_empty_body — empty class bodies on multiple lines
15
+ * - self_static_accessor — `self::` vs `static::` on enums
16
+ * - blank_line_before_statement — enum `case` separators
17
+ *
18
+ * Auto-detect: when `<rootPath>/vendor/bin/pint` exists, run it on the
19
+ * files we just wrote. Same Pint preset the project's CI already uses,
20
+ * so no risk of formatter drift between codegen and CI checks.
21
+ *
22
+ * Opt-out: future config field; for now `chmod -x vendor/bin/pint` or
23
+ * deleting it disables the auto-run.
24
+ */
25
+ /**
26
+ * Resolve the `vendor/bin/pint` path for the project. Walks up from
27
+ * the first written file until we find a sibling `vendor/bin/pint`,
28
+ * stopping at filesystem root or after 12 levels (defensive against
29
+ * pathological inputs). Returns the absolute path or null.
30
+ */
31
+ export declare function findPintBinary(anyWrittenPhpPath: string): string | null;
32
+ export interface PintFormatResult {
33
+ ran: boolean;
34
+ pintPath: string | null;
35
+ /** Files passed to pint. */
36
+ formattedCount: number;
37
+ /** Pint's exit status if it ran. 0 = success. */
38
+ exitStatus: number | null;
39
+ /** Captured stderr when pint failed (status != 0). */
40
+ errorMessage?: string;
41
+ }
42
+ /**
43
+ * Run Laravel Pint on the given PHP files (absolute paths). No-op if
44
+ * Pint isn't installed in the project. Pint is invoked with `--quiet`
45
+ * so it stays out of the way unless something breaks.
46
+ *
47
+ * Pint takes a list of files (or directories) and formats them in
48
+ * place. It uses the project's `pint.json` config if present, else the
49
+ * Laravel preset.
50
+ */
51
+ export declare function runPintFormat(writtenPhpPaths: string[]): PintFormatResult;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Auto-format generated PHP files via Laravel Pint. Issue #100 v5.8.20.
3
+ *
4
+ * Reporter scenario (godx-jp/godx-kintai 2026-04-26 audit): every
5
+ * `omnify generate --force` produces 150+ PHP files that fail Laravel
6
+ * Pint's standard preset (`vendor/bin/pint --test`):
7
+ *
8
+ * - ordered_imports — `use` statements not alphabetised
9
+ * - fully_qualified_strict_types — exception types referenced via FQN
10
+ * - class_attributes_separation — missing blank lines between members
11
+ * - function_declaration — function-keyword spacing
12
+ * - unary_operator_spaces — `! \$x` vs `!\$x`
13
+ * - no_unused_imports — codegen leaves the occasional dangling import
14
+ * - single_line_empty_body — empty class bodies on multiple lines
15
+ * - self_static_accessor — `self::` vs `static::` on enums
16
+ * - blank_line_before_statement — enum `case` separators
17
+ *
18
+ * Auto-detect: when `<rootPath>/vendor/bin/pint` exists, run it on the
19
+ * files we just wrote. Same Pint preset the project's CI already uses,
20
+ * so no risk of formatter drift between codegen and CI checks.
21
+ *
22
+ * Opt-out: future config field; for now `chmod -x vendor/bin/pint` or
23
+ * deleting it disables the auto-run.
24
+ */
25
+ import { existsSync, statSync } from 'node:fs';
26
+ import { spawnSync } from 'node:child_process';
27
+ import { dirname, join } from 'node:path';
28
+ /**
29
+ * Resolve the `vendor/bin/pint` path for the project. Walks up from
30
+ * the first written file until we find a sibling `vendor/bin/pint`,
31
+ * stopping at filesystem root or after 12 levels (defensive against
32
+ * pathological inputs). Returns the absolute path or null.
33
+ */
34
+ export function findPintBinary(anyWrittenPhpPath) {
35
+ let cur = dirname(anyWrittenPhpPath);
36
+ for (let i = 0; i < 12; i++) {
37
+ const candidate = join(cur, 'vendor', 'bin', 'pint');
38
+ if (existsSync(candidate)) {
39
+ try {
40
+ const st = statSync(candidate);
41
+ if (st.isFile())
42
+ return candidate;
43
+ }
44
+ catch {
45
+ // ignore stat errors and keep climbing
46
+ }
47
+ }
48
+ const parent = dirname(cur);
49
+ if (parent === cur)
50
+ break;
51
+ cur = parent;
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * Run Laravel Pint on the given PHP files (absolute paths). No-op if
57
+ * Pint isn't installed in the project. Pint is invoked with `--quiet`
58
+ * so it stays out of the way unless something breaks.
59
+ *
60
+ * Pint takes a list of files (or directories) and formats them in
61
+ * place. It uses the project's `pint.json` config if present, else the
62
+ * Laravel preset.
63
+ */
64
+ export function runPintFormat(writtenPhpPaths) {
65
+ if (writtenPhpPaths.length === 0) {
66
+ return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null };
67
+ }
68
+ const pintPath = findPintBinary(writtenPhpPaths[0]);
69
+ if (!pintPath) {
70
+ return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null };
71
+ }
72
+ // Pint's working dir should be the Laravel project root (where
73
+ // composer.json + pint.json live). The pint binary is at
74
+ // `<projectRoot>/vendor/bin/pint`, so projectRoot is two parents up.
75
+ const projectRoot = dirname(dirname(dirname(pintPath)));
76
+ // Pass each file as its own arg. Pint accepts multiple file/dir
77
+ // arguments; capping the batch is unnecessary up to a few hundred
78
+ // files (well under the OS arg limit).
79
+ const result = spawnSync(pintPath, ['--quiet', ...writtenPhpPaths], {
80
+ cwd: projectRoot,
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ encoding: 'utf-8',
83
+ });
84
+ const exitStatus = result.status ?? null;
85
+ return {
86
+ ran: true,
87
+ pintPath,
88
+ formattedCount: writtenPhpPaths.length,
89
+ exitStatus,
90
+ errorMessage: exitStatus !== 0 ? (result.stderr || result.stdout || undefined) : undefined,
91
+ };
92
+ }
@@ -1,26 +1,39 @@
1
1
  /**
2
- * User-editable stub collision detection. Issue #98 v5.8.18.
2
+ * User-editable stub collision detection. Issue #98 v5.8.18 / #99 v5.8.19.
3
3
  *
4
4
  * Many Laravel projects organize Requests / Resources / Services by
5
- * domain subfolder (`Admin/`, `Cms/`, `Audit/`, etc.) BEFORE adopting
6
- * omnify. When omnify then emits the FLAT default stub at the layer
7
- * root (e.g. `app/Http/Requests/BrandStoreRequest.php`), the stub
8
- * collides ergonomically with the project's hand-written
9
- * `app/Http/Requests/Admin/BrandStoreRequest.php` same basename,
10
- * unused by any caller, gets re-created every `omnify generate`,
11
- * forcing the dev to delete it manually.
5
+ * domain subfolder (`Admin/`, `Cms/`, `Audit/`) BEFORE adopting omnify,
6
+ * OR they upgrade from v4 with wrappers at v4 paths
7
+ * (`app/Services/Omnify/<Name>Service.php`) and v5 wants to emit at
8
+ * the new modular path (`app/Omnify/Services/<Name>Service.php`). In
9
+ * both cases the auto-emitted user-editable stub is unused noise that
10
+ * regenerates every `omnify generate`, forcing the dev to delete it.
12
11
  *
13
- * Detection: scan the file's PARENT DIRECTORY recursively for any
12
+ * Detection: scan a configurable root directory recursively for any
14
13
  * file with the same basename. If found at a path != target, skip
15
14
  * emission.
16
15
  *
17
- * Scope rule: scan ONLY the file's parent directory (and its subdirs),
18
- * NOT sibling directories. So a grouped editable at `Cms/Banner.php`
19
- * isn't skipped by an unrelated `Admin/Banner.php`.
16
+ * Scope decision:
17
+ * - Wide scope (e.g. project's `app/`): finds cross-layer wrappers
18
+ * like the v4 v5 upgrade case (#99).
19
+ * - Narrow scope (file's parent dir): finds same-layer siblings only,
20
+ * like a domain-organized `Admin/Brand.php` next to the flat
21
+ * default (#98 v5.8.18).
20
22
  *
21
- * Bounded by `MAX_FILES_SCANNED` so a project root with tens of
22
- * thousands of files doesn't grind generate to a halt.
23
+ * The CLI passes whichever scope matches the layout. Bounded by
24
+ * `MAX_FILES_SCANNED` so a project with tens of thousands of files
25
+ * doesn't grind generate to a halt.
23
26
  */
27
+ /**
28
+ * Walk up from `filePath` looking for the closest ancestor directory
29
+ * named `app`. Returns its absolute path or null. Used by the CLI to
30
+ * pick a project-wide search root for sibling-skip — covers v4 → v5
31
+ * cross-layer wrappers (e.g. v4 wrapper at `app/Services/Omnify/<X>Service.php`
32
+ * vs v5 modular emit at `app/Omnify/Services/<X>Service.php`). Capped
33
+ * at 12 levels of climbing so we don't traverse the entire FS on a
34
+ * malformed input.
35
+ */
36
+ export declare function findAppRootAncestor(filePath: string): string | null;
24
37
  /**
25
38
  * Scan `rootDir` recursively for a file whose basename matches
26
39
  * `baseName`, excluding `excludePath` itself. Returns the first
@@ -1,29 +1,53 @@
1
1
  /**
2
- * User-editable stub collision detection. Issue #98 v5.8.18.
2
+ * User-editable stub collision detection. Issue #98 v5.8.18 / #99 v5.8.19.
3
3
  *
4
4
  * Many Laravel projects organize Requests / Resources / Services by
5
- * domain subfolder (`Admin/`, `Cms/`, `Audit/`, etc.) BEFORE adopting
6
- * omnify. When omnify then emits the FLAT default stub at the layer
7
- * root (e.g. `app/Http/Requests/BrandStoreRequest.php`), the stub
8
- * collides ergonomically with the project's hand-written
9
- * `app/Http/Requests/Admin/BrandStoreRequest.php` same basename,
10
- * unused by any caller, gets re-created every `omnify generate`,
11
- * forcing the dev to delete it manually.
5
+ * domain subfolder (`Admin/`, `Cms/`, `Audit/`) BEFORE adopting omnify,
6
+ * OR they upgrade from v4 with wrappers at v4 paths
7
+ * (`app/Services/Omnify/<Name>Service.php`) and v5 wants to emit at
8
+ * the new modular path (`app/Omnify/Services/<Name>Service.php`). In
9
+ * both cases the auto-emitted user-editable stub is unused noise that
10
+ * regenerates every `omnify generate`, forcing the dev to delete it.
12
11
  *
13
- * Detection: scan the file's PARENT DIRECTORY recursively for any
12
+ * Detection: scan a configurable root directory recursively for any
14
13
  * file with the same basename. If found at a path != target, skip
15
14
  * emission.
16
15
  *
17
- * Scope rule: scan ONLY the file's parent directory (and its subdirs),
18
- * NOT sibling directories. So a grouped editable at `Cms/Banner.php`
19
- * isn't skipped by an unrelated `Admin/Banner.php`.
16
+ * Scope decision:
17
+ * - Wide scope (e.g. project's `app/`): finds cross-layer wrappers
18
+ * like the v4 v5 upgrade case (#99).
19
+ * - Narrow scope (file's parent dir): finds same-layer siblings only,
20
+ * like a domain-organized `Admin/Brand.php` next to the flat
21
+ * default (#98 v5.8.18).
20
22
  *
21
- * Bounded by `MAX_FILES_SCANNED` so a project root with tens of
22
- * thousands of files doesn't grind generate to a halt.
23
+ * The CLI passes whichever scope matches the layout. Bounded by
24
+ * `MAX_FILES_SCANNED` so a project with tens of thousands of files
25
+ * doesn't grind generate to a halt.
23
26
  */
24
27
  import { existsSync, readdirSync, statSync } from 'node:fs';
25
- import { join } from 'node:path';
28
+ import { basename, dirname, join } from 'node:path';
26
29
  const MAX_FILES_SCANNED = 5000;
30
+ /**
31
+ * Walk up from `filePath` looking for the closest ancestor directory
32
+ * named `app`. Returns its absolute path or null. Used by the CLI to
33
+ * pick a project-wide search root for sibling-skip — covers v4 → v5
34
+ * cross-layer wrappers (e.g. v4 wrapper at `app/Services/Omnify/<X>Service.php`
35
+ * vs v5 modular emit at `app/Omnify/Services/<X>Service.php`). Capped
36
+ * at 12 levels of climbing so we don't traverse the entire FS on a
37
+ * malformed input.
38
+ */
39
+ export function findAppRootAncestor(filePath) {
40
+ let cur = dirname(filePath);
41
+ for (let i = 0; i < 12; i++) {
42
+ if (basename(cur) === 'app')
43
+ return cur;
44
+ const parent = dirname(cur);
45
+ if (parent === cur)
46
+ break;
47
+ cur = parent;
48
+ }
49
+ return null;
50
+ }
27
51
  /**
28
52
  * Scan `rootDir` recursively for a file whose basename matches
29
53
  * `baseName`, excluding `excludePath` itself. Returns the first