march-hare 0.13.0 → 0.13.2

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 (131) hide show
  1. package/README.md +45 -1
  2. package/dist/action/index.d.ts +2 -2
  3. package/dist/action/utils.d.ts +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/types.d.ts +3 -3
  6. package/dist/actions/utils.d.ts +3 -3
  7. package/dist/app/index.d.ts +7 -7
  8. package/dist/app/types.d.ts +5 -5
  9. package/dist/boundary/components/broadcast/index.d.ts +2 -2
  10. package/dist/boundary/components/broadcast/types.d.ts +1 -1
  11. package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
  12. package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
  13. package/dist/boundary/components/consumer/index.d.ts +5 -5
  14. package/dist/boundary/components/consumer/types.d.ts +1 -1
  15. package/dist/boundary/components/consumer/utils.d.ts +1 -1
  16. package/dist/boundary/components/env/index.d.ts +3 -27
  17. package/dist/boundary/components/env/types.d.ts +24 -2
  18. package/dist/boundary/components/env/utils.d.ts +1 -1
  19. package/dist/boundary/components/scope/index.d.ts +2 -2
  20. package/dist/boundary/components/scope/types.d.ts +1 -1
  21. package/dist/boundary/components/scope/utils.d.ts +1 -1
  22. package/dist/boundary/components/sharing/index.d.ts +1 -1
  23. package/dist/boundary/components/tap/index.d.ts +3 -3
  24. package/dist/boundary/components/tap/types.d.ts +2 -2
  25. package/dist/boundary/components/tap/utils.d.ts +1 -1
  26. package/dist/boundary/components/tasks/index.d.ts +2 -2
  27. package/dist/boundary/components/tasks/utils.d.ts +1 -1
  28. package/dist/boundary/index.d.ts +1 -1
  29. package/dist/boundary/types.d.ts +2 -2
  30. package/dist/cache/index.d.ts +91 -9
  31. package/dist/cache/types.d.ts +1 -1
  32. package/dist/cli/bin/mh.js +10 -0
  33. package/dist/cli/lib/banner/index.js +14 -0
  34. package/dist/cli/lib/commands/app/index.js +37 -0
  35. package/dist/cli/lib/commands/feature/index.js +55 -0
  36. package/dist/cli/lib/commands/index.js +89 -0
  37. package/dist/cli/lib/commands/init/index.js +29 -0
  38. package/dist/cli/lib/commands/shared/index.js +56 -0
  39. package/dist/cli/lib/index.js +56 -0
  40. package/dist/cli/lib/parser/index.js +24 -0
  41. package/dist/cli/lib/prompt/index.js +61 -0
  42. package/dist/cli/lib/runner/index.js +46 -0
  43. package/dist/cli/lib/runner/types.js +1 -0
  44. package/dist/cli/lib/runner/utils.js +60 -0
  45. package/dist/cli/lib/types.js +1 -0
  46. package/dist/cli/lib/utils.js +20 -0
  47. package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
  48. package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
  49. package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
  50. package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
  51. package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
  52. package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
  53. package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
  54. package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
  55. package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
  56. package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
  57. package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
  58. package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
  59. package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
  60. package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
  61. package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
  62. package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
  63. package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
  64. package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
  65. package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
  66. package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
  67. package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
  68. package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
  69. package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
  70. package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
  71. package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
  72. package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
  73. package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
  74. package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
  75. package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
  76. package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
  77. package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
  78. package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
  79. package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
  80. package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
  81. package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
  82. package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
  83. package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
  84. package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
  85. package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
  86. package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
  87. package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
  88. package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
  89. package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
  90. package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
  91. package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
  92. package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
  93. package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
  94. package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
  95. package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
  96. package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
  97. package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
  98. package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
  99. package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
  100. package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
  101. package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
  102. package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
  103. package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
  104. package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
  105. package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
  106. package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
  107. package/dist/coalesce/index.d.ts +1 -1
  108. package/dist/context/index.d.ts +1 -1
  109. package/dist/error/types.d.ts +1 -1
  110. package/dist/error/utils.d.ts +1 -1
  111. package/dist/index.d.ts +17 -17
  112. package/dist/march-hare.js +9 -8
  113. package/dist/march-hare.js.map +1 -1
  114. package/dist/march-hare.umd.cjs +1 -1
  115. package/dist/march-hare.umd.cjs.map +1 -1
  116. package/dist/resource/index.d.ts +6 -5
  117. package/dist/resource/types.d.ts +3 -3
  118. package/dist/resource/utils.d.ts +12 -5
  119. package/dist/scope/index.d.ts +3 -3
  120. package/dist/scope/types.d.ts +2 -2
  121. package/dist/scope/utils.d.ts +6 -4
  122. package/dist/shared/index.d.ts +4 -4
  123. package/dist/types/index.d.ts +5 -5
  124. package/dist/utils/index.d.ts +3 -3
  125. package/dist/utils/types.d.ts +1 -1
  126. package/dist/utils/utils.d.ts +1 -1
  127. package/dist/with/index.d.ts +3 -3
  128. package/dist/with/types.d.ts +2 -2
  129. package/dist/with/utils.d.ts +3 -3
  130. package/package.json +18 -4
  131. package/src/cli/README.md +314 -0
@@ -1,7 +1,8 @@
1
- import { ResourceHandle } from './types';
2
- import { Cache } from './utils';
3
- import { AppFetcher } from '../app/types';
4
- export type { Coalesce, Fetcher, Invocation, ResourceHandle } from './types';
1
+ import { ResourceHandle } from './types.js';
2
+ import { Cache } from './utils.js';
3
+ import { AppFetcher } from '../app/types.js';
4
+ import { Env } from '../boundary/components/env/types.js';
5
+ export type { Coalesce, Fetcher, Invocation, ResourceHandle } from './types.js';
5
6
  /**
6
7
  * Evicts cache entries across every Resource constructed in the
7
8
  * current process. Resources register themselves on declaration, so
@@ -74,4 +75,4 @@ export declare function nuke(where?: object): void;
74
75
  * — consumers should use `App({ cache })` instead of passing it
75
76
  * directly.
76
77
  */
77
- export declare function Resource<E extends object, T, P extends object = Record<never, never>>(ƒ: AppFetcher<E, T, P>, cache?: Cache): ResourceHandle<T, P>;
78
+ export declare function Resource<E extends object, T, P extends object = Record<never, never>>(ƒ: AppFetcher<E, T, P>, cache?: Cache, getEnv?: () => Env | undefined): ResourceHandle<T, P>;
@@ -1,6 +1,6 @@
1
- import { Env } from '../boundary/components/env/index';
2
- import { Cache } from '../cache/index';
3
- import { BroadcastPayload, MulticastPayload, Filter } from '../types/index';
1
+ import { Env } from '../boundary/components/env/types.js';
2
+ import { Cache } from '../cache/index.js';
3
+ import { BroadcastPayload, MulticastPayload, Filter } from '../types/index.js';
4
4
  /**
5
5
  * Dispatch surface exposed on a Resource fetcher's `context`. Restricted
6
6
  * to broadcast and multicast actions &mdash; unicast targets the calling
@@ -1,7 +1,8 @@
1
- import { Cache } from '../cache/index';
2
- import { unset } from '../utils/utils';
3
- import { Fetcher, ResourceEvictor, ResourceHandle } from './types';
4
- export { Cache } from '../cache/index';
1
+ import { Cache } from '../cache/index.js';
2
+ import { unset } from '../utils/utils.js';
3
+ import { Env } from '../boundary/components/env/types.js';
4
+ import { Fetcher, ResourceEvictor, ResourceHandle } from './types.js';
5
+ export { Cache } from '../cache/index.js';
5
6
  /**
6
7
  * Default in-memory `Cache` used when {@link Resource} is constructed
7
8
  * without an explicit one. Each fetcher gets its own slot via the
@@ -60,6 +61,12 @@ export declare function nextResourceId(fetcher: object): string;
60
61
  * `context.actions.resource(...)` for fetch/evict. `.get(params)` reads
61
62
  * the per-params cache slot synchronously.
62
63
  *
64
+ * `getEnv` is the App-supplied accessor used to resolve the live env at
65
+ * sync read time (`.get(params)`) and at App-side eviction (when the
66
+ * handler context isn't available). It returns `undefined` when no
67
+ * Boundary has mounted yet &mdash; in which case `cache.scope(undefined)`
68
+ * yields the empty prefix and the read/evict targets the unscoped slot.
69
+ *
63
70
  * @internal
64
71
  */
65
- export declare function build<T, P extends object>(ƒ: Fetcher<T, P>, backing: Cache, namespace: string | null): ResourceHandle<T, P>;
72
+ export declare function build<T, P extends object>(ƒ: Fetcher<T, P>, backing: Cache, namespace: string | null, getEnv: () => Env | undefined): ResourceHandle<T, P>;
@@ -1,6 +1,6 @@
1
- import { ScopeHandle } from './types';
2
- export type { ScopeHandle } from './types';
3
- export { createScope } from './utils';
1
+ import { ScopeHandle } from './types.js';
2
+ export type { ScopeHandle } from './types.js';
3
+ export { createScope } from './utils.js';
4
4
  /**
5
5
  * Standalone counterpart to `app.Scope<MulticastActions>()`, exported
6
6
  * as `shared.Scope` &mdash; opens a typed multicast scope without
@@ -1,5 +1,5 @@
1
- import { AppContextHandle, AppResource } from '../app/types';
2
- import { Actions, Model, Props } from '../types/index';
1
+ import { AppContextHandle, AppResource } from '../app/types.js';
2
+ import { Actions, Model, Props } from '../types/index.js';
3
3
  import type * as React from "react";
4
4
  /**
5
5
  * Handle returned by `app.Scope<MulticastActions>()`. Mirrors the
@@ -1,12 +1,14 @@
1
- import { Cache } from '../cache/index';
2
- import { ScopeHandle } from './types';
1
+ import { Cache } from '../cache/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ import { ScopeHandle } from './types.js';
3
4
  /**
4
5
  * Internal constructor for a {@link ScopeHandle}. Called from inside
5
6
  * `App<E>()` so the enclosing Env shape `E` is captured at the type
6
7
  * level. The optional `cache` is the same value `App({ cache })` was
7
8
  * constructed with &mdash; resources declared via `scope.Resource`
8
- * share that cache.
9
+ * share that cache. `getEnv` resolves the live Env from the enclosing
10
+ * `app.Boundary` so cache-key scoping works for sync `.get()` reads.
9
11
  *
10
12
  * @internal
11
13
  */
12
- export declare function createScope<E extends object, MulticastActions>(cache?: Cache): ScopeHandle<E, MulticastActions>;
14
+ export declare function createScope<E extends object, MulticastActions>(cache?: Cache, getEnv?: () => Env | undefined): ScopeHandle<E, MulticastActions>;
@@ -1,7 +1,7 @@
1
- import { AppFetcher } from '../app/types';
2
- import { ResourceHandle } from '../resource/types';
3
- export { useContext, useEnv } from '../app/index';
4
- export { Scope } from '../scope/index';
1
+ import { AppFetcher } from '../app/types.js';
2
+ import { ResourceHandle } from '../resource/types.js';
3
+ export { useContext, useEnv } from '../app/index.js';
4
+ export { Scope } from '../scope/index.js';
5
5
  /**
6
6
  * Standalone counterpart to `app.Resource`, exported as
7
7
  * `shared.Resource`. Takes the **Env shape `E` as a mandatory first
@@ -1,9 +1,9 @@
1
1
  import { Operation, Process, Inspect, Box } from 'immertation';
2
- import { ActionId, Task, Tasks } from '../boundary/components/tasks/types';
3
- import { Fault } from '../error/types';
4
- import { Env } from '../boundary/components/env/index';
5
- import { Coalesce, Invocation } from '../resource/types';
6
- import { WithHandle } from '../with/types';
2
+ import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.js';
3
+ import { Fault } from '../error/types.js';
4
+ import { Env } from '../boundary/components/env/types.js';
5
+ import { Coalesce, Invocation } from '../resource/types.js';
6
+ import { WithHandle } from '../with/types.js';
7
7
  import * as React from "react";
8
8
  /**
9
9
  * Chainable handle returned from `context.actions.resource(invocation)`.
@@ -1,6 +1,6 @@
1
- import { Pk } from '../types/index';
2
- export { unset } from './utils';
3
- export type { Stored, Unset } from './types';
1
+ import { Pk } from '../types/index.js';
2
+ export { unset } from './utils.js';
3
+ export type { Stored, Unset } from './types.js';
4
4
  /**
5
5
  * Returns a promise that resolves after the specified number of
6
6
  * milliseconds, or rejects with an {@link Aborted} when the signal is aborted. Use to inject a cancellable
@@ -1,4 +1,4 @@
1
- import { unset } from './utils';
1
+ import { unset } from './utils.js';
2
2
  /** Nominal type of the {@link unset} sentinel. */
3
3
  export type Unset = typeof unset;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { Stored } from './types';
1
+ import { Stored } from './types.js';
2
2
  /**
3
3
  * Sentinel symbol marking "no value present yet". Shared by the Resource
4
4
  * cache and by storage handles so callers can distinguish "nothing has been
@@ -1,6 +1,6 @@
1
- import { Actions, HandlerContext, Model, Props } from '../types/index';
2
- import { Env } from '../boundary/components/env/index';
3
- import { BooleanPaths, Get, Paths } from './types';
1
+ import { Actions, HandlerContext, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ import { BooleanPaths, Get, Paths } from './types.js';
4
4
  /**
5
5
  * Handler factories that wire an action directly to a model field. Prefer
6
6
  * `context.with` from `useContext<Model>()` for first-class autocompletion
@@ -1,5 +1,5 @@
1
- import { Actions, HandlerContext, Maybe, Model, Props } from '../types/index';
2
- import { Env } from '../boundary/components/env/index';
1
+ import { Actions, HandlerContext, Maybe, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
3
  /**
4
4
  * Non-nullable primitive leaves that {@link Paths} can terminate on. Used
5
5
  * internally to stop the recursive walk at scalar boundaries; consumers
@@ -1,6 +1,6 @@
1
- import { Actions, HandlerContext, Model, Props } from '../types/index';
2
- import { Env } from '../boundary/components/env/index';
3
- import { WithHandle } from './types';
1
+ import { Actions, HandlerContext, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ import { WithHandle } from './types.js';
4
4
  /**
5
5
  * Walks the lodash-style dotted `path` on `target`, stopping one segment
6
6
  * short so callers can mutate or read the leaf via the returned `cursor`
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "yarn@1.22.22",
7
7
  "main": "./dist/march-hare.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "mh": "./dist/cli/bin/mh.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/index.d.ts",
@@ -14,8 +17,16 @@
14
17
  }
15
18
  },
16
19
  "dependencies": {
20
+ "@inquirer/prompts": "^7.2.0",
21
+ "change-case": "^5.4.4",
22
+ "ejs": "^3.1.10",
17
23
  "eventemitter3": "^5.0.4",
18
- "immertation": "^0.1.26"
24
+ "figlet": "^1.7.0",
25
+ "find-up": "^8.0.0",
26
+ "gray-matter": "^4.0.3",
27
+ "immertation": "^0.1.27",
28
+ "kleur": "^4.1.5",
29
+ "tinyglobby": "^0.2.17"
19
30
  },
20
31
  "peerDependencies": {
21
32
  "@mobily/ts-belt": "^3.0.0",
@@ -26,10 +37,12 @@
26
37
  "vitest/vite": "^7.3.1"
27
38
  },
28
39
  "files": [
29
- "dist"
40
+ "dist",
41
+ "src/cli/README.md"
30
42
  ],
31
43
  "scripts": {
32
- "build": "vite build",
44
+ "build": "vite build && npm run build:cli",
45
+ "build:cli": "tsc -p tsconfig.cli.json && rm -rf dist/cli/templates && cp -R src/cli/templates dist/cli/templates && chmod +x dist/cli/bin/mh.js",
33
46
  "build:example": "vite build --mode example --outDir dist-example --base /MarchHare/ && mv dist-example/src/example/index.html dist-example/index.html && rm -rf dist-example/src",
34
47
  "dev": "vite",
35
48
  "preview": "vite preview",
@@ -53,6 +66,7 @@
53
66
  "@testing-library/jest-dom": "^6.9.1",
54
67
  "@testing-library/react": "^16.3.2",
55
68
  "@types/dom-navigation": "^1.0.7",
69
+ "@types/ejs": "^3.1.5",
56
70
  "@types/lodash": "^4.17.23",
57
71
  "@types/ms": "^2.1.0",
58
72
  "@types/ramda": "^0.31.1",
@@ -0,0 +1,314 @@
1
+ # `@march-hare/cli` — the `mh` scaffolder
2
+
3
+ A [Hygen](https://github.com/jondot/hygen)-style code generator that
4
+ scaffolds [March Hare](../../README.md) projects and the building
5
+ blocks inside them. Templates mirror the layout of
6
+ [`src/example/`](../example/) and the FSD layering rules enforced by
7
+ [`eslint-plugin-boundaries`](https://github.com/javierbrea/eslint-plugin-boundaries):
8
+ imports flow strictly downward (`app → features → shared`).
9
+
10
+ ```
11
+ __ ___ __ __ __
12
+ / |/ /___ ___________/ /_ / / / /___ _________
13
+ / /|_/ / __ `/ ___/ ___/ __ \ / /_/ / __ `/ ___/ _ \
14
+ / / / / /_/ / / / /__/ / / / / __ / /_/ / / / __/
15
+ /_/ /_/\__,_/_/ \___/_/ /_/ /_/ /_/\__,_/_/ \___/
16
+ ```
17
+
18
+ ## Contents
19
+
20
+ - [Quick start](#quick-start)
21
+ - [Interactive menu vs. direct commands](#interactive-menu-vs-direct-commands)
22
+ - [`mh init`](#mh-init)
23
+ - [`mh app …`](#mh-app-)
24
+ - [`mh feature …`](#mh-feature-)
25
+ - [`mh shared …`](#mh-shared-)
26
+ - [Generated project layout](#generated-project-layout)
27
+ - [How the templates work](#how-the-templates-work)
28
+ - [Adding your own generators](#adding-your-own-generators)
29
+ - [Development](#development)
30
+
31
+ ## Quick start
32
+
33
+ The CLI ships with `march-hare` itself — installing the library
34
+ exposes the `mh` binary:
35
+
36
+ ```bash
37
+ npm install -g march-hare # global: `mh` is on your PATH
38
+ mh init my-project
39
+
40
+ # — or —
41
+
42
+ npm install march-hare # local: invoke through npx / package scripts
43
+ npx mh init my-project
44
+ ```
45
+
46
+ Working inside this repo? Run the CLI straight from source — root
47
+ `yarn install` covers all its deps:
48
+
49
+ ```bash
50
+ node dist/cli/bin/mh.js # interactive menu
51
+ node dist/cli/bin/mh.js init demo # scaffold a project into ./demo
52
+ ```
53
+
54
+ When run without arguments the CLI prints the Figlet banner and an
55
+ interactive menu of every top-level command.
56
+
57
+ ## Interactive menu vs. direct commands
58
+
59
+ Every command in the CLI is a node in a tree. Pass none, one, or all
60
+ of the path segments — the CLI fills in the gaps with prompts.
61
+
62
+ | You type | What happens |
63
+ | -------------------- | -------------------------------------------------- |
64
+ | `mh` | Menu of top-level commands |
65
+ | `mh feature` | Menu of sub-commands under `feature` |
66
+ | `mh feature new` | Prompts you for the feature name then scaffolds it |
67
+ | `mh feature new foo` | Scaffolds `features/foo/` with no prompts |
68
+ | `mh --help` | Prints the whole command tree |
69
+
70
+ Any leaf command accepts `--name=value` and `--flag` style overrides
71
+ so you can drive it from scripts without prompts.
72
+
73
+ ## `mh init`
74
+
75
+ Bootstraps a brand-new March Hare project mirroring `src/example/`.
76
+
77
+ ```bash
78
+ mh init my-project
79
+ ```
80
+
81
+ Prompts you for:
82
+
83
+ - **Project name** — kebab-case slug used for the package name and the
84
+ folder it lives in.
85
+ - **Description** — populates `package.json` and the root README.
86
+ - **API base URL** — seeded into the App's Env at
87
+ `src/app/utils.ts`.
88
+
89
+ What you get:
90
+
91
+ ```
92
+ my-project/
93
+ ├── eslint.config.js ← FSD boundaries enforced via plugin-boundaries
94
+ ├── package.json ← React 19 + March Hare 0.13 + Vite + Vitest + Playwright
95
+ ├── playwright.config.ts
96
+ ├── tsconfig.json ← @app/* @features/* @shared/* path aliases
97
+ ├── vite.config.ts
98
+ ├── vitest.config.ts
99
+ ├── index.html
100
+ ├── README.md
101
+ ├── tests/
102
+ │ └── home.e2e.ts
103
+ └── src/
104
+ ├── index.tsx
105
+ ├── test-setup.ts
106
+ ├── vite-env.d.ts
107
+ ├── app/
108
+ │ ├── index.tsx ← <app.Boundary> root
109
+ │ ├── utils.ts ← App<Env.X>({ env: { apiBase } })
110
+ │ └── pages/home/ ← home page (button + greeting)
111
+ │ ├── index.tsx
112
+ │ ├── types.ts
113
+ │ ├── actions.ts
114
+ │ ├── styles.ts
115
+ │ └── index.integration.tsx
116
+ ├── features/greet/ ← stateful "say hello" feature
117
+ │ ├── index.tsx
118
+ │ ├── types.ts
119
+ │ ├── actions.ts
120
+ │ ├── utils.ts ← shared.Scope<Envs, typeof Multicast>()
121
+ │ └── index.test.tsx
122
+ └── shared/
123
+ ├── components/button/ ← antd wrapper + tests
124
+ ├── theme/ ← colour / spacing / font / radius / shadow tokens
125
+ ├── types/ ← Env namespace + Envs alias + Payload + Broadcast
126
+ └── resources/ ← empty barrel; add resources with `mh shared resource`
127
+ ```
128
+
129
+ A working "Say hello" button is wired end-to-end so you can verify the
130
+ project boots before deleting it.
131
+
132
+ ```bash
133
+ cd my-project
134
+ yarn install # or npm install
135
+ yarn dev
136
+ ```
137
+
138
+ ## `mh app …`
139
+
140
+ Manage the host layer.
141
+
142
+ | Command | What it does |
143
+ | ---------------------- | ------------------------------------------------------------------- |
144
+ | `mh app new <name>` | New page under `src/app/pages/<name>/` (index/types/actions/styles) |
145
+ | `mh app integration` | Picks a page, drops in an `index.integration.tsx` |
146
+ | `mh app action <page>` | Picks (or accepts) a page, injects a new `Actions.X` + handler |
147
+
148
+ Examples:
149
+
150
+ ```bash
151
+ mh app new dashboard --tagline="Live metrics"
152
+ mh app integration # prompts you to pick a page
153
+ mh app action dashboard Refresh # injects Actions.Refresh + a handler
154
+ ```
155
+
156
+ The `action` command injects into both `types.ts` (adds
157
+ `static Refresh = Action("Refresh")`) and `actions.ts` (adds an empty
158
+ `actions.useAction(Actions.Refresh, …)` block before `return actions`).
159
+ Re-running the same command is a no-op thanks to `skip_if`.
160
+
161
+ ## `mh feature …`
162
+
163
+ Manage feature slices.
164
+
165
+ | Command | What it does |
166
+ | -------------------------------- | ------------------------------------------------------------------------ |
167
+ | `mh feature new <name>` | New feature; asks whether it owns state (`--stateful` / `--no-stateful`) |
168
+ | `mh feature unit <name>` | Adds a `index.test.tsx` next to an existing feature |
169
+ | `mh feature action <feat> <act>` | Injects an `Actions.<Act>` member + a handler stub into the feature |
170
+ | `mh feature multicast <feat>` | Injects a multicast action into the feature's `Multicast` class |
171
+
172
+ Stateful features get four files: `index.tsx`, `types.ts`, `actions.ts`,
173
+ `utils.ts`. Presentational features get three (no `actions.ts`). Both
174
+ shapes include a `Multicast` class and a `scope` handle
175
+ (`shared.Scope<Envs, typeof Multicast>()`) — the structural contract
176
+ for the feature's private bus.
177
+
178
+ ## `mh shared …`
179
+
180
+ Manage reusable building blocks.
181
+
182
+ | Command | What it does |
183
+ | --------------------------------- | ------------------------------------------------------------ |
184
+ | `mh shared component <name>` | New presentational atom under `src/shared/components/` |
185
+ | `mh shared resource <name>` | New `shared.Resource<Envs, T>` under `src/shared/resources/` |
186
+ | `mh shared util <name>` | New util module under `src/shared/utils/` |
187
+ | `mh shared type payload <name>` | Injects a `Payload.<Name>` type into `shared/types` |
188
+ | `mh shared type broadcast <name>` | Injects a global broadcast action into `shared/types` |
189
+ | `mh shared unit <kind> <name>` | Adds a unit test for a shared module |
190
+
191
+ ```bash
192
+ mh shared component card
193
+ mh shared resource user
194
+ mh shared util parse-date
195
+ mh shared type payload Notification
196
+ mh shared type broadcast Toast
197
+ mh shared unit components card
198
+ ```
199
+
200
+ After creating a resource, re-export it from
201
+ `src/shared/resources/index.ts` so it stays reachable as
202
+ `resource.<name>.fetch()`:
203
+
204
+ ```ts
205
+ export * as user from "./user/index.ts";
206
+ ```
207
+
208
+ ## Generated project layout
209
+
210
+ The CLI's `init` template seeds the App's `Env` shape based on the
211
+ project name. If you run `mh init billing`, the generated
212
+ `shared/types/index.ts` declares `Env.Billing` and `Envs = Env.Billing`:
213
+
214
+ ```ts
215
+ export namespace Env {
216
+ export type Billing = {
217
+ apiBase: string;
218
+ };
219
+ }
220
+
221
+ export type Envs = Env.Billing;
222
+ ```
223
+
224
+ When you later add a second deployable (e.g. a marketing site) you
225
+ widen `Envs` to a union and add a [user-defined type
226
+ guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
227
+ to narrow at the call site — see the example's
228
+ [README](../example/README.md) for the full pattern.
229
+
230
+ ## How the templates work
231
+
232
+ Templates live under [`templates/<generator>/<action>/`](./templates).
233
+ Each `.ejs.t` file has a tiny [Hygen-style](https://www.hygen.io/docs/templates)
234
+ frontmatter block plus an EJS body.
235
+
236
+ ```ejs
237
+ ---
238
+ to: src/features/<%= name %>/index.tsx
239
+ ---
240
+ import * as React from "react";
241
+ // …
242
+ ```
243
+
244
+ Supported frontmatter keys:
245
+
246
+ | Key | What it does |
247
+ | --------- | ------------------------------------------------------------------------------------------ |
248
+ | `to` | Output path. Rendered through EJS so it can reference template vars. |
249
+ | `inject` | Set to `true` to inject the body into an existing file instead of overwriting it. |
250
+ | `before` | Regex (multiline). With `inject`, inserts the body **above** the first match. |
251
+ | `after` | Regex (multiline). With `inject`, inserts the body **below** the first match. |
252
+ | `skip_if` | Regex (multiline). With `inject`, skip the file if it already matches (idempotent reruns). |
253
+ | `if` | EJS expression. Skip the template entirely unless it evaluates truthy. |
254
+ | `force` | EJS expression. When truthy, overwrite an existing file (default: skip). |
255
+
256
+ Helpers available inside templates:
257
+
258
+ | Helper | Example | Result |
259
+ | ----------- | ------------------- | ----------- |
260
+ | `kebab(s)` | `kebab("AddCat")` | `"add-cat"` |
261
+ | `pascal(s)` | `pascal("add-cat")` | `"AddCat"` |
262
+ | `camel(s)` | `camel("add-cat")` | `"addCat"` |
263
+ | `title(s)` | `title("add-cat")` | `"Add Cat"` |
264
+
265
+ Standard variables passed to every render:
266
+
267
+ - `name` — the kebab-case name accepted from positional / prompt.
268
+ - `pascalName` — same value run through `pascal()`.
269
+
270
+ Commands pass additional vars (e.g. `feature`, `page`, `rawName`,
271
+ `env`, `apiBase`) — see the individual command files under
272
+ [`lib/commands/`](./lib/commands).
273
+
274
+ ## Adding your own generators
275
+
276
+ 1. Create `templates/<generator>/<action>/foo.ejs.t` with a `to:`
277
+ frontmatter and an EJS body.
278
+ 2. Wire a leaf into [`lib/commands/index.ts`](./lib/commands/index.ts) that calls
279
+ `scaffold("<generator>", "<action>", vars, { cwd })`.
280
+ 3. Optionally add prompts via the helpers in
281
+ [`lib/prompt/index.ts`](./lib/prompt/index.ts).
282
+
283
+ Templates can render any number of files — every `.ejs.t` under the
284
+ action directory is processed, with each `to:` resolved
285
+ independently. Use nested directories under the action to keep big
286
+ generators (like `init`) tidy.
287
+
288
+ ## Development
289
+
290
+ The CLI's runtime deps (`@inquirer/prompts`, `ejs`, `figlet`, `kleur`)
291
+ live in the root [`package.json`](../../package.json). A single
292
+ `yarn install` at the repo root is all you need:
293
+
294
+ ```bash
295
+ yarn install # from the repo root
296
+ node dist/cli/bin/mh.js # run the CLI from source
297
+ ```
298
+
299
+ There are no build steps — the CLI is plain ESM JavaScript. Templates
300
+ are read directly from disk at runtime.
301
+
302
+ To verify a change end-to-end:
303
+
304
+ ```bash
305
+ cd /tmp && rm -rf check && mkdir check && cd check
306
+ node "$OLDPWD/dist/cli/bin/mh.js" init demo \
307
+ --description="dev check" --apiBase="https://api.example.test"
308
+ cd demo
309
+ node /Users/adamtimberlake/Webroot/MarchHare/dist/cli/bin/mh.js feature new counter --stateful
310
+ node /Users/adamtimberlake/Webroot/MarchHare/dist/cli/bin/mh.js feature action counter Reset
311
+ ```
312
+
313
+ Then `yarn install && yarn checks` inside the generated project should
314
+ pass cleanly.