runlify 0.0.794 → 0.0.797
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/build/cli.js +8 -2
- package/build/projectsGeneration/builders/generationPaths.js +44 -1
- package/build/projectsGeneration/generateProject/back/src/generateBackSrc.js +11 -2
- package/build/projectsGeneration/generateProject/fileHandlers.js +6 -2
- package/build/projectsGeneration/generateProject/front/generateFrontSrc.js +10 -1
- package/build/projectsGeneration/generateProject/front/translations/generateFrontSrcTranslations.js +33 -1
- package/build/projectsGeneration/generateProject/generateBackClickHouseBootstrap.js +1 -4
- package/build/projectsGeneration/generateProject/utils.js +2 -2
- package/build/projectsGeneration/generators/fileTemplates/back/clickhouse/jobs.js +41 -4
- package/build/projectsGeneration/generators/fileTemplates/back/roles/customPermissions.js +12 -0
- package/build/projectsGeneration/generators/fileTemplates/back/roles/customUiPermissions.js +13 -0
- package/build/projectsGeneration/generators/fileTemplates/back/roles/initPermissions.js +31 -0
- package/build/projectsGeneration/generators/fileTemplates/back/roles/uiPermissions.js +24 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/NotFoundPage.js +6 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/PermissionPage.js +6 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/environment/src/layout/Menu.js +1 -1
- package/build/projectsGeneration/generators/fileTemplates/ui/environment/src/routes.js +5 -5
- package/build/projectsGeneration/generators/fileTemplates/ui/getDefaultMenu.js +5 -1
- package/build/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nIndexTmpl.js +35 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nTypesTmpl.js +33 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nValidationTmpl.js +38 -0
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityCreate/DefaultEntityCreate.js +16 -5
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityEdit/DefaultEntityEdit.js +27 -11
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityList/DefaultEntityFilter.js +13 -4
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityList/DefaultEntityList.js +14 -7
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityShow/DefaultMainTab.js +13 -4
- package/build/projectsGeneration/generators/fileTemplates/ui/pages/EntityShow/DependencyTab.js +12 -3
- package/build/projectsGeneration/generators/fileTemplates/ui/resources.js +21 -8
- package/build/projectsGeneration/generators/fileTemplates/ui/utils/permissions.js +89 -0
- package/build/projectsGeneration/generators/ui/getShowComponent.js +2 -2
- package/build/types/projectsGeneration/builders/generationPaths.d.ts +42 -0
- package/build/types/projectsGeneration/generateProject/fileHandlers.d.ts +1 -0
- package/build/types/projectsGeneration/generateProject/types.d.ts +1 -1
- package/build/types/projectsGeneration/generateProject/utils.d.ts +1 -1
- package/build/types/projectsGeneration/generators/fileTemplates/back/roles/customPermissions.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/back/roles/customUiPermissions.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/back/roles/initPermissions.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/back/roles/uiPermissions.d.ts +3 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/NotFoundPage.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/PermissionPage.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nIndexTmpl.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nTypesTmpl.d.ts +3 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/i18n/skeleton/uiI18nValidationTmpl.d.ts +2 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/pages/EntityEdit/DefaultEntityEdit.d.ts +1 -0
- package/build/types/projectsGeneration/generators/fileTemplates/ui/utils/permissions.d.ts +2 -0
- package/docs/01-overview.md +172 -0
- package/docs/02-system-meta-builder.md +405 -0
- package/docs/03-entity-types.md +471 -0
- package/docs/04-fields.md +357 -0
- package/docs/05-custom-methods.md +249 -0
- package/docs/06-storage.md +129 -0
- package/docs/07-backend-file-graph.md +231 -0
- package/docs/08-frontend-file-graph.md +260 -0
- package/docs/09-options.md +213 -0
- package/docs/10-modules.md +164 -0
- package/docs/11-permissions.md +231 -0
- package/docs/INDEX.md +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Bootstrap Options (`BootstrapEntityOptions`)
|
|
2
|
+
|
|
3
|
+
> **Load this file when:** you need to toggle a generation feature on/off, change
|
|
4
|
+
> project identity settings, configure infrastructure (k8s, CI, Docker), or understand
|
|
5
|
+
> what a specific option flag does.
|
|
6
|
+
>
|
|
7
|
+
> Source: `src/projectsGeneration/types.ts`
|
|
8
|
+
> Consumed from: `src/meta/options.json` (read by `runlify regen`)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## How options are applied
|
|
13
|
+
|
|
14
|
+
`options.json` is read once per `regen` and applied globally to all entities.
|
|
15
|
+
Individual entities can receive their own options override by passing `options` as the
|
|
16
|
+
third argument to `addCatalog`, `addDocument`, etc.:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
system.addCatalog('specialCatalog', undefined, { ...defaultBootstrapEntityOptions, genUiResources: false })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Most projects use the same options object for all entities.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## All flags
|
|
27
|
+
|
|
28
|
+
### Code generation toggles — Backend
|
|
29
|
+
|
|
30
|
+
| Flag | Default | Description |
|
|
31
|
+
|------|---------|-------------|
|
|
32
|
+
| `genPrismaServices` | `true` | Generate Prisma-based service files |
|
|
33
|
+
| `genPrismaSchema` | `true` | Generate `.prisma` schema files |
|
|
34
|
+
| `genGraphSchema` | `true` | Generate GraphQL type definitions |
|
|
35
|
+
| `genGraphResolvers` | `true` | Generate GraphQL resolvers |
|
|
36
|
+
| `genContext` | `true` | Generate request context wiring |
|
|
37
|
+
| `typesOnly` | `false` | Internal — first generation pass (types only). Do not set manually. |
|
|
38
|
+
| `readOnly` | `false` | Skip write operations in generation. Rarely used. |
|
|
39
|
+
| `corePrismaGetter` | `true` | Generate the Prisma client getter (`getPrisma.ts`) |
|
|
40
|
+
| `coreIndex` | `true` | Generate the main `index.ts` entry point |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### Code generation toggles — Frontend
|
|
45
|
+
|
|
46
|
+
| Flag | Default | Description |
|
|
47
|
+
|------|---------|-------------|
|
|
48
|
+
| `genFrontend` | `true` | Generate the entire frontend. Set `false` to generate backend only (`regen --backOnly` also sets this). |
|
|
49
|
+
| `genUiResources` | `true` | Generate react-admin resource registrations |
|
|
50
|
+
| `genUiResourcesPage` | `true` | Generate the `/resources` debug page |
|
|
51
|
+
| `genUiEntityMapping` | `true` | Generate `entityMapping.ts` |
|
|
52
|
+
| `genUiMenu` | `true` | Generate `getDefaultMenu.ts` |
|
|
53
|
+
| `genUiElements` | `true` | Generate entity page components |
|
|
54
|
+
| `genUiRoutes` | `true` | Generate `routes.tsx` |
|
|
55
|
+
| `genUIApp` | `true` | Generate the root `App.tsx` |
|
|
56
|
+
| `genUiFunctions` | `true` | Generate the functions page |
|
|
57
|
+
| `genUiDashboard` | `true` | Generate `Dashboard.tsx` (created once) |
|
|
58
|
+
| `genUiCountWidget` | `true` | Generate count widgets per entity |
|
|
59
|
+
| `genUiListWidget` | `true` | Generate list widgets per entity |
|
|
60
|
+
| `genUiAppBar` | `true` | Generate `AppBar.tsx` |
|
|
61
|
+
| `showMetaPage` | `true` | Show the `/meta` debug page in the UI |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### Form generation toggles
|
|
66
|
+
|
|
67
|
+
Nested under `forms`:
|
|
68
|
+
|
|
69
|
+
| Flag | Default | Description |
|
|
70
|
+
|------|---------|-------------|
|
|
71
|
+
| `forms.list.gen` | `true` | Generate list page components |
|
|
72
|
+
| `forms.show.gen` | `true` | Generate show page components |
|
|
73
|
+
| `forms.edit.gen` | `true` | Generate edit form components |
|
|
74
|
+
| `forms.edit.idEditable` | `false` | Show `id` field in edit forms |
|
|
75
|
+
| `forms.create.gen` | `true` | Generate create form components |
|
|
76
|
+
| `forms.create.idEditable` | `false` | Show `id` field in create forms |
|
|
77
|
+
| `forms.menu.show` | `true` | Show entity in sidebar menu |
|
|
78
|
+
| `forms.resourcesPage.show` | `true` | Show entity on resources page |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### CI/CD — GitLab CI
|
|
83
|
+
|
|
84
|
+
| Flag | Default | Description |
|
|
85
|
+
|------|---------|-------------|
|
|
86
|
+
| `genBackGitlabCi` | `true` | Generate `.gitlab-ci.yml` for backend |
|
|
87
|
+
| `genUiGitlabCi` | `true` | Generate `.gitlab-ci.yml` for frontend |
|
|
88
|
+
| `genBackCiNotify` | `true` | Include Slack/chat notifications in backend CI |
|
|
89
|
+
| `genUiCiNotify` | `true` | Include Slack/chat notifications in frontend CI |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### CI/CD — Docker
|
|
94
|
+
|
|
95
|
+
| Flag | Default | Description |
|
|
96
|
+
|------|---------|-------------|
|
|
97
|
+
| `genDockerfileBack` | `true` | Generate `Dockerfile` for backend |
|
|
98
|
+
| `genDockerfileUI` | `true` | Generate `Dockerfile` for frontend |
|
|
99
|
+
| `adminBaseDockerimage` | `'nginx:1.23-alpine'` | Base image for frontend Docker build |
|
|
100
|
+
| `backendBaseDockerimage` | `'registry.gitlab.com/making.ventures/images/node-base'` | Base image for backend Docker build |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Kubernetes / Helm
|
|
105
|
+
|
|
106
|
+
| Flag | Default | Description |
|
|
107
|
+
|------|---------|-------------|
|
|
108
|
+
| `genBackChartValues` | `true` | Generate `chart/values.yaml` for backend |
|
|
109
|
+
| `genBackChartIngress` | `true` | Generate `chart/templates/ingress.yaml` for backend |
|
|
110
|
+
| `genBackChartBack` | `true` | Generate `chart/templates/back.yaml` for backend |
|
|
111
|
+
| `genUiChartIngress` | `true` | Generate ingress chart for frontend |
|
|
112
|
+
| `genUiChartFront` | `true` | Generate deployment chart for frontend |
|
|
113
|
+
| `k8sChartName` | `''` | Helm chart name |
|
|
114
|
+
| `k8sNamespacePrefix` | `''` | Kubernetes namespace prefix |
|
|
115
|
+
| `k8sAppsDomain` | `'apps.making.ventures'` | Base domain for k8s ingress |
|
|
116
|
+
| `k8sSubdomainPrefix` | `''` | Subdomain prefix applied to all ingress hosts |
|
|
117
|
+
| `k8sImagePullSecrets` | `'docker-registry'` | k8s image pull secret name |
|
|
118
|
+
| `ingressAnnotationBodySize` | `'50m'` | `nginx.ingress.kubernetes.io/proxy-body-size` annotation |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Project identity
|
|
123
|
+
|
|
124
|
+
| Flag | Default | Description |
|
|
125
|
+
|------|---------|-------------|
|
|
126
|
+
| `projectsGroup` | `''` | GitLab group or organisation name |
|
|
127
|
+
| `projectPrefix` | `''` | Technical prefix (e.g. `'myapp'`) — used in k8s names, image tags |
|
|
128
|
+
| `dbName` | `''` | Default database name in Postgres |
|
|
129
|
+
| `projectName` | `''` | Human-readable project name |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Feature flags
|
|
134
|
+
|
|
135
|
+
| Flag | Default | Description |
|
|
136
|
+
|------|---------|-------------|
|
|
137
|
+
| `usersEnabled` | `true` | Generate user management entities and auth |
|
|
138
|
+
| `tenantsAvailable` | `false` | Enable multi-tenancy support |
|
|
139
|
+
| `themesEnabled` | `true` | Enable theme switching in UI |
|
|
140
|
+
| `mainColorOfAppTitile` | `true` | Apply primary color to app title |
|
|
141
|
+
| `sharding` | `false` | Enable sharding support globally |
|
|
142
|
+
| `breadcrumb` | `false` | Enable breadcrumb navigation in UI |
|
|
143
|
+
| `telemetry` | `false` | Enable OpenTelemetry tracing |
|
|
144
|
+
| `mountebankEnabled` | `false` | Enable Mountebank mock server integration |
|
|
145
|
+
| `exportHtmlEnabled` | `false` | Enable HTML export feature |
|
|
146
|
+
| `auditableOnlyByUser` | `false` | Only audit changes made by users (not system) |
|
|
147
|
+
| `useSortedFilter` | `false` | Use sorted filter UI in list pages |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### System menu visibility
|
|
152
|
+
|
|
153
|
+
| Flag | Default | Description |
|
|
154
|
+
|------|---------|-------------|
|
|
155
|
+
| `showFunctionsInMenu` | `true` | Show system Functions page in menu |
|
|
156
|
+
| `showResourcesInMenu` | `true` | Show Resources debug page in menu |
|
|
157
|
+
| `showMetaInMenu` | `true` | Show Meta debug page in menu |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Output paths
|
|
162
|
+
|
|
163
|
+
| Flag | Default | Description |
|
|
164
|
+
|------|---------|-------------|
|
|
165
|
+
| `detachedBackProject` | `''` | Absolute path to the backend project. Auto-resolved to `../<prefix>-back` if empty. |
|
|
166
|
+
| `detachedUiProject` | `''` | Absolute path to the frontend project. Auto-resolved to `../<prefix>-ui` if empty. |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Other
|
|
171
|
+
|
|
172
|
+
| Flag | Default | Description |
|
|
173
|
+
|------|---------|-------------|
|
|
174
|
+
| `skipWarningThisIsGenerated` | `false` | Suppress the `DO NOT EDIT! THIS IS GENERATED FILE` header |
|
|
175
|
+
| `graphGeneratorCommand` | `''` | Command to run the local GraphQL schema generator (alternative to built-in) |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Anti-patterns
|
|
180
|
+
|
|
181
|
+
### Setting `typesOnly: true` in `options.json`
|
|
182
|
+
|
|
183
|
+
**Wrong:** manually setting `typesOnly: true` in the options file.
|
|
184
|
+
|
|
185
|
+
**Why:** this is an internal flag used by `generateProject` for the first generation
|
|
186
|
+
pass. It produces an incomplete output — only TypeScript type files, no resolvers, no
|
|
187
|
+
Prisma schema. If set permanently, your backend will be broken.
|
|
188
|
+
|
|
189
|
+
**Correct:** never set `typesOnly` manually.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Setting `genFrontend: false` in `options.json` permanently to skip frontend
|
|
194
|
+
|
|
195
|
+
**Wrong:** permanently disabling frontend in `options.json` while the team still needs
|
|
196
|
+
frontend generation.
|
|
197
|
+
|
|
198
|
+
**Why:** other developers running `regen` won't regenerate the frontend, leading to
|
|
199
|
+
divergent states.
|
|
200
|
+
|
|
201
|
+
**Correct:** use the CLI flag `runlify regen --backOnly` for a one-off backend-only
|
|
202
|
+
regen. It temporarily sets `genFrontend=false` without changing `options.json`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Leaving `projectPrefix`, `dbName`, `projectName` empty
|
|
207
|
+
|
|
208
|
+
**Wrong:** keeping default empty strings in `options.json`.
|
|
209
|
+
|
|
210
|
+
**Why:** these values are embedded into generated Helm chart names, Docker image tags,
|
|
211
|
+
database names, and CI variables. Empty strings produce broken infrastructure configs.
|
|
212
|
+
|
|
213
|
+
**Correct:** always set all three to meaningful values for your project.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
|
|
3
|
+
> **Load this file when:** you need to use a built-in module, understand what
|
|
4
|
+
> `addCommonEntities` adds to the system, or write a reusable module function.
|
|
5
|
+
>
|
|
6
|
+
> Related: [02-system-meta-builder.md](./02-system-meta-builder.md) ·
|
|
7
|
+
> [03-entity-types.md](./03-entity-types.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What a module is
|
|
12
|
+
|
|
13
|
+
A module is a plain function with the signature:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
(system: SystemMetaBuilder) => void
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It calls `system.addCatalog`, `system.addDocument`, `system.addConfigVar`, etc. to
|
|
20
|
+
register a bundle of related entities and config in one call. There is no special
|
|
21
|
+
registration mechanism — a module is just a function you call.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## `addCommonEntities(system)` {#addCommonEntities}
|
|
26
|
+
|
|
27
|
+
Source: `src/projectsGeneration/commonEntities/addCommonEntities.ts`
|
|
28
|
+
Import: `import { addCommonEntities } from 'runlify'`
|
|
29
|
+
|
|
30
|
+
The standard foundation layer. Adds all infrastructure entities required by the
|
|
31
|
+
framework itself. Most meta projects call this once near the top of `metadata.ts`.
|
|
32
|
+
|
|
33
|
+
### What it adds
|
|
34
|
+
|
|
35
|
+
| Entity | Type | Purpose |
|
|
36
|
+
|--------|------|---------|
|
|
37
|
+
| `languages` | Catalog | Supported UI languages |
|
|
38
|
+
| `tenants` | Catalog | Tenants (only used when `tenantsAvailable=true`) |
|
|
39
|
+
| `managers` | Catalog | Admin users |
|
|
40
|
+
| `users` | Catalog | End users |
|
|
41
|
+
| `auditLogs` | Catalog | Change history records |
|
|
42
|
+
| `roles` | Catalog | Access roles |
|
|
43
|
+
| `refreshTokens` | Catalog | JWT refresh token storage |
|
|
44
|
+
| `autogeneration` | Catalog | Code generation tracking |
|
|
45
|
+
| `aggregateTrackings` | Catalog | External search sync tracking |
|
|
46
|
+
|
|
47
|
+
Plus:
|
|
48
|
+
- Email module entities (see [`addEmailModuleEntities`](#addEmailModuleEntities))
|
|
49
|
+
- Configuration variable entities (`configurationVariables` — stores runtime config in DB)
|
|
50
|
+
- Standard system CLI commands
|
|
51
|
+
|
|
52
|
+
**Anti-pattern — calling `addCommonEntities` after adding your own entities:**
|
|
53
|
+
|
|
54
|
+
The common entities use fixed names. If you add a catalog named `users` or `roles`
|
|
55
|
+
before calling `addCommonEntities`, the call throws (duplicate entity name).
|
|
56
|
+
|
|
57
|
+
**Correct:** always call `addCommonEntities(system)` **before** adding your domain
|
|
58
|
+
entities.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## `addEmailModuleEntities(system)` {#addEmailModuleEntities}
|
|
63
|
+
|
|
64
|
+
Source: `src/projectsGeneration/modules/addEmailModuleEntities.ts`
|
|
65
|
+
Import: `import { addEmailModuleEntities } from 'runlify'` (also called by `addCommonEntities`)
|
|
66
|
+
|
|
67
|
+
Adds all entities required for the email / notification system.
|
|
68
|
+
|
|
69
|
+
### What it adds
|
|
70
|
+
|
|
71
|
+
| Entity | Type | Purpose |
|
|
72
|
+
|--------|------|---------|
|
|
73
|
+
| `messageTypes` | Catalog | Message type registry (e.g. `plain`) |
|
|
74
|
+
| `templateStyles` | Catalog | HTML/CSS styles for email templates |
|
|
75
|
+
| `messageTemplates` | Catalog | Email template definitions |
|
|
76
|
+
| `mailingCampaignStatuses` | Catalog | Campaign lifecycle statuses |
|
|
77
|
+
| `mailingCampaigns` | Catalog | Mailing campaign records |
|
|
78
|
+
| `mailingMessages` | Catalog | Individual messages in a campaign |
|
|
79
|
+
|
|
80
|
+
Called automatically by `addCommonEntities`. Only call it directly if you are NOT using
|
|
81
|
+
`addCommonEntities` but still need the email module.
|
|
82
|
+
|
|
83
|
+
**Anti-pattern — calling `addEmailModuleEntities` twice:**
|
|
84
|
+
|
|
85
|
+
Called once by `addCommonEntities`. Calling it again throws duplicate entity name errors.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Writing your own module {#custom-module}
|
|
90
|
+
|
|
91
|
+
A module is just a function. Convention: one file per module, function named `add<ModuleName>`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// src/meta/modules/addOrderingModule.ts
|
|
95
|
+
import SystemMetaBuilder from 'runlify'
|
|
96
|
+
|
|
97
|
+
export const addOrderingModule = (system: SystemMetaBuilder) => {
|
|
98
|
+
const orderStatuses = system.addCatalog('orderStatuses', { singular: 'Order status', plural: 'Order statuses' })
|
|
99
|
+
orderStatuses.getKey().setType('string')
|
|
100
|
+
orderStatuses.addField('title', undefined, { isTitleField: true }).setType('string').setRequired()
|
|
101
|
+
orderStatuses.addPredefinedElements([
|
|
102
|
+
{ id: 'new', title: 'New' },
|
|
103
|
+
{ id: 'processing', title: 'Processing' },
|
|
104
|
+
{ id: 'completed', title: 'Completed' },
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
const orders = system.addDocument('orders', { singular: 'Order', plural: 'Orders' })
|
|
108
|
+
orders.addField('amount').setType('float').setRequired()
|
|
109
|
+
orders.addLinkField(orderStatuses, 'statusId').setRequired()
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Then in `metadata.ts`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { addOrderingModule } from './modules/addOrderingModule'
|
|
117
|
+
|
|
118
|
+
const system = new SystemMetaBuilder('myapp')
|
|
119
|
+
addCommonEntities(system)
|
|
120
|
+
addOrderingModule(system)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Module composition rules
|
|
126
|
+
|
|
127
|
+
1. **Order matters.** Modules that reference entities from other modules must be called
|
|
128
|
+
after those modules.
|
|
129
|
+
2. **No circular references.** Module A cannot reference entities that Module B adds, if
|
|
130
|
+
Module B also references entities from Module A.
|
|
131
|
+
3. **Call `addCommonEntities` first** — it seeds `users`, `tenants`, `roles`, and other
|
|
132
|
+
system entities that domain modules typically reference.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Anti-patterns
|
|
137
|
+
|
|
138
|
+
### Defining a module that conditionally adds entities
|
|
139
|
+
|
|
140
|
+
**Wrong:**
|
|
141
|
+
```ts
|
|
142
|
+
export const addOrderingModule = (system: SystemMetaBuilder, withReturns: boolean) => {
|
|
143
|
+
system.addDocument('orders')
|
|
144
|
+
if (withReturns) {
|
|
145
|
+
system.addDocument('returns') // conditional entity
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Why:** generates inconsistent meta across environments or team members. The system
|
|
151
|
+
description must be deterministic and environment-independent.
|
|
152
|
+
|
|
153
|
+
**Correct:** always include or always exclude. Use separate modules for separate
|
|
154
|
+
feature sets: `addOrderingModule` and `addOrderingWithReturnsModule`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Putting module logic inside `metadata.ts` directly
|
|
159
|
+
|
|
160
|
+
**Wrong:** writing 500 lines of entity definitions directly in `metadata.ts`.
|
|
161
|
+
|
|
162
|
+
**Why:** hard to maintain, hard to reuse across projects, impossible to test in isolation.
|
|
163
|
+
|
|
164
|
+
**Correct:** extract domain areas into module functions in `src/meta/modules/`.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Permissions
|
|
2
|
+
|
|
3
|
+
> **Load this file when:** you need to understand how access control works — which
|
|
4
|
+
> permission gates a page/field/action, how permission strings are named, or how new
|
|
5
|
+
> permissions get seeded into the database.
|
|
6
|
+
>
|
|
7
|
+
> Related: [07-backend-file-graph.md](./07-backend-file-graph.md) ·
|
|
8
|
+
> [08-frontend-file-graph.md](./08-frontend-file-graph.md)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Two permission spaces
|
|
13
|
+
|
|
14
|
+
Runlify generates checks against **two distinct kinds** of permission string. They look
|
|
15
|
+
similar but are enforced completely differently.
|
|
16
|
+
|
|
17
|
+
| Space | Format | Enforced by | Meaning |
|
|
18
|
+
|-------|--------|-------------|---------|
|
|
19
|
+
| **Data** | `<entity>.<action>` | **Backend** — every GraphQL resolver checks it | Can the caller read/write this entity's data at all, from anywhere (its own page, a reference field on another page, an autocomplete, …) |
|
|
20
|
+
| **UI** | `ui.<entity>.list` | **Frontend only** — never sent to or checked by the backend | Can the caller open this entity's own list page / see it in the menu |
|
|
21
|
+
|
|
22
|
+
The split exists because a single `<entity>.all` permission used to do both jobs at
|
|
23
|
+
once: granting it opened the entity's own list page **and** silently made the entity
|
|
24
|
+
visible everywhere it's referenced from another entity (filters, autocompletes,
|
|
25
|
+
reference fields, related-record tabs). Those are different concerns — someone might
|
|
26
|
+
need to see a member's `memberType` label in the members list without ever needing to
|
|
27
|
+
open the `memberTypes` list page themselves.
|
|
28
|
+
|
|
29
|
+
`ui.<entity>.list` is **only ever checked in the generated frontend code**. It is never
|
|
30
|
+
part of a GraphQL permission check, never appears in
|
|
31
|
+
[`permissionsToGraphql.ts`](./07-backend-file-graph.md), and granting it does **not**
|
|
32
|
+
give access to any data. It exists purely to gate navigation.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Data-space actions
|
|
37
|
+
|
|
38
|
+
| Action | Meaning |
|
|
39
|
+
|--------|---------|
|
|
40
|
+
| `.get` | Read a single record — gates the Show page and `ReferenceField` links to it |
|
|
41
|
+
| `.all` | Read a list of records — gates list queries, autocompletes, filters, reference display |
|
|
42
|
+
| `.meta` | Read list pagination metadata (total count) |
|
|
43
|
+
| `.create` | Create a record |
|
|
44
|
+
| `.update` | Update a record |
|
|
45
|
+
| `.delete` | Delete a record |
|
|
46
|
+
|
|
47
|
+
These come from `getRuntimePermissions()` (backend, not generated by Runlify — see
|
|
48
|
+
[Where data-space permissions come from](#where-data-space-permissions-come-from)),
|
|
49
|
+
which reflects over every service's methods. Any method a service exposes becomes a
|
|
50
|
+
checkable permission; `get`/`all`/`meta`/`create`/`update`/`delete` exist simply because
|
|
51
|
+
`BaseService` defines those methods and every entity service extends it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Where each permission is checked
|
|
56
|
+
|
|
57
|
+
### Own page — `resources.ts`
|
|
58
|
+
|
|
59
|
+
Each `<Resource>`'s `show`/`edit`/`create`/`list` prop is wrapped in
|
|
60
|
+
[`withPermission`](#uti-lspermissionsts) so an unauthorized visit always renders a
|
|
61
|
+
fallback component instead of `undefined` (see
|
|
62
|
+
[Fallback components](#fallback-components)):
|
|
63
|
+
|
|
64
|
+
| Resource prop | Required permissions |
|
|
65
|
+
|---------------|----------------------|
|
|
66
|
+
| `show` | `<entity>.get` |
|
|
67
|
+
| `list` | `<entity>.all` **and** `<entity>.meta` **and** `ui.<entity>.list` |
|
|
68
|
+
| `edit` | `<entity>.update` **and** `<ref>.all` for every **required** link field shown in the edit form |
|
|
69
|
+
| `create` | `<entity>.create` **and** `<ref>.all` for every **required** link field shown in the create form |
|
|
70
|
+
|
|
71
|
+
`list` needs all three because opening the list page requires reading records
|
|
72
|
+
(`.all`), reading pagination metadata (`.meta`), and having UI access to the page
|
|
73
|
+
itself (`ui.<entity>.list`) — the data permissions alone are not enough to open the
|
|
74
|
+
page, and the UI permission alone is not enough to load any data.
|
|
75
|
+
|
|
76
|
+
`edit`/`create` add the referenced entity's `.all` permission for every **required**
|
|
77
|
+
link field, because a required autocomplete that the user cannot see would make the
|
|
78
|
+
form impossible to submit. Optional link fields don't gate the whole page — see
|
|
79
|
+
[Optional link fields](#optional-link-fields) below.
|
|
80
|
+
|
|
81
|
+
### Referenced entity — everywhere else
|
|
82
|
+
|
|
83
|
+
When an entity's data is shown or picked *from inside another entity's page* (not its
|
|
84
|
+
own page), only the data-space `<ref>.all` / `<ref>.get` permissions are checked —
|
|
85
|
+
never `ui.<ref>.list`. Seeing a reference's title, filtering by it, or autocompleting
|
|
86
|
+
it does not require access to that entity's own list page.
|
|
87
|
+
|
|
88
|
+
| Location | Permission checked |
|
|
89
|
+
|----------|---------------------|
|
|
90
|
+
| `ReferenceField` (List, Show `MainTab`, dependency tabs) | `<ref>.all` gates whether the field renders at all; `<ref>.get` gates whether it renders as a clickable link to the Show page |
|
|
91
|
+
| `ReferenceInput` in a Filter | `<ref>.all` gates whether the whole input renders |
|
|
92
|
+
| `rowClick` on a list Datagrid | `<entity>.get` — can the user open the Show page for the clicked row |
|
|
93
|
+
| `BulkDeleteButton` / `DeleteButton` | `<entity>.delete` |
|
|
94
|
+
| Menu item for an entity | `<entity>.all`, `<entity>.meta`, `ui.<entity>.list` — same trio as the list page, so a menu entry never links to a page the user can't open |
|
|
95
|
+
|
|
96
|
+
### System pages — `ui.dashboard` / `ui.functions` / `ui.resources` / `ui.meta` {#system-pages}
|
|
97
|
+
|
|
98
|
+
A handful of non-entity pages (Dashboard, Functions, Resources debug page, Meta debug
|
|
99
|
+
page) use the same `ui.*` namespace, via `<Guard shouldHave='ui.<page>'>` in `routes.ts`
|
|
100
|
+
and `permissions: ['ui.<page>']` on the corresponding menu item in `getDefaultMenu.ts`.
|
|
101
|
+
They aren't tied to an entity, so they aren't generated by `uiPermissions.ts`'s
|
|
102
|
+
per-entity loop — instead they're added conditionally based on the `BootstrapEntityOptions`
|
|
103
|
+
flag that controls whether the page is generated at all (`genUiDashboard`,
|
|
104
|
+
`showFunctionsInMenu`, `showResourcesInMenu`, `showMetaInMenu`). If a page is disabled
|
|
105
|
+
in options, its `ui.*` permission isn't generated either.
|
|
106
|
+
|
|
107
|
+
### Optional link fields — Create/Edit forms {#optional-link-fields}
|
|
108
|
+
|
|
109
|
+
A link field that is **not required on input** doesn't gate the whole page. Instead,
|
|
110
|
+
the individual autocomplete is hidden if the user lacks `<ref>.all` — the rest of the
|
|
111
|
+
form still renders normally:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
{debug && hasPermission(permissions, '<ref>.all') && <Grid item>...</Grid>}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
(`debug &&` is a pre-existing, unrelated dev-only field toggle — it wraps every
|
|
118
|
+
optional field, not just link fields.)
|
|
119
|
+
|
|
120
|
+
A **required** link field is never hidden this way, because the form couldn't be
|
|
121
|
+
submitted without it. Instead its permission is added to the page-level check in
|
|
122
|
+
`resources.ts` (see the table above) — if the user can't see it, they can't open the
|
|
123
|
+
page at all.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## `utils/permissions.ts` {#utils-permissionsts}
|
|
128
|
+
|
|
129
|
+
Generated (`[gen]`) — see [08-frontend-file-graph.md](./08-frontend-file-graph.md).
|
|
130
|
+
Exports four functions, all operating on the raw `permissions: string[]` array from
|
|
131
|
+
`usePermissions()`:
|
|
132
|
+
|
|
133
|
+
| Function | Behaviour |
|
|
134
|
+
|----------|-----------|
|
|
135
|
+
| `hasPermission(permissions, key)` | `true` if `key` matches (see below) |
|
|
136
|
+
| `hasAnyPermission(permissions, keys)` | `true` if at least one of `keys` matches (empty `keys` → `true`) |
|
|
137
|
+
| `hasAllPermissions(permissions, keys)` | `true` only if every key in `keys` matches (empty `keys` → `true`) |
|
|
138
|
+
| `withPermission(permissions, keys, Component)` | Returns `Component` if `hasAllPermissions` passes, otherwise `PermissionPage` |
|
|
139
|
+
|
|
140
|
+
**Matching:** a required key ending in `.*` (e.g. `'ui.memberTypes.*'`) matches by
|
|
141
|
+
prefix — satisfied if at least one current permission starts with `'ui.memberTypes.'`.
|
|
142
|
+
A plain key without `.*` is matched by exact equality.
|
|
143
|
+
|
|
144
|
+
`withPermission` is what makes every `<Resource>` prop in `resources.ts` always resolve
|
|
145
|
+
to a real component — never `undefined`. Passing `undefined` to a react-admin
|
|
146
|
+
`<Resource>` prop causes react-router to silently fall through to a nearby catch-all
|
|
147
|
+
route (usually `list`) instead of showing a permission error, which is confusing and
|
|
148
|
+
was the original motivation for this file.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Fallback components {#fallback-components}
|
|
153
|
+
|
|
154
|
+
Two once (`[once]`) components, generated only if missing:
|
|
155
|
+
|
|
156
|
+
| File | Content | Used when |
|
|
157
|
+
|------|---------|-----------|
|
|
158
|
+
| `src/adm/PermissionPage.tsx` | Re-exports react-admin's `AccessDenied` | `withPermission` denies access (missing permission) |
|
|
159
|
+
| `src/adm/NotFoundPage.tsx` | Re-exports react-admin's `NotFound` | The action doesn't exist for this entity at all — e.g. `edit` when `entity.setUpdatableByUser(false)` |
|
|
160
|
+
|
|
161
|
+
Both are safe to replace with a custom component — Runlify never touches them again
|
|
162
|
+
once created. Anything importing `withPermission`'s fallback keeps working as long as
|
|
163
|
+
the file still default-exports a component.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Where data-space permissions come from {#where-data-space-permissions-come-from}
|
|
168
|
+
|
|
169
|
+
`getRuntimePermissions.ts` is **not generated by Runlify** — it's a hand-written
|
|
170
|
+
backend file (`src/adm/services/getRuntimePermissions.ts`) that reflects over every
|
|
171
|
+
registered service's prototype chain at runtime and turns every method name into
|
|
172
|
+
`<service>.<methodName>`. Because `BaseService` defines `get`/`all`/`meta`/`create`/
|
|
173
|
+
`update`/`delete`, and every entity service extends it, those six actions exist for
|
|
174
|
+
every entity automatically — no per-entity registration needed, in Runlify or
|
|
175
|
+
anywhere else. Adding a new entity and running `regen` is enough; the next time
|
|
176
|
+
`getRuntimePermissions()` runs, the new service's methods are picked up.
|
|
177
|
+
|
|
178
|
+
This is separate from `initPermissions.ts` actually **inserting** rows into the
|
|
179
|
+
`permissions` table — see the next section.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Seeding permissions into the database {#seeding}
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
src/init/roles/
|
|
187
|
+
generated/
|
|
188
|
+
initPermissions.ts [gen] Collects all four sources, writes them to the DB
|
|
189
|
+
uiPermissions.ts [gen] One 'ui.<entity>.list' per entity + enabled system pages (see #system-pages)
|
|
190
|
+
customPermissions.ts [once] Hand-written data-space permissions (empty array to start)
|
|
191
|
+
customUiPermissions.ts [once] Hand-written UI-space permissions (empty array to start)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The two `[gen]` files are grouped in `generated/` so it's visually unambiguous which
|
|
195
|
+
files in this directory are regenerated and which are yours — the two `custom*.ts`
|
|
196
|
+
files sit one level up, outside `generated/`.
|
|
197
|
+
|
|
198
|
+
`initPermissions.ts` merges four sources and calls
|
|
199
|
+
`ctx.service('permissions').createMany(...)`:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
getRuntimePermissions(ctx) ← reflected data-space permissions (see above)
|
|
203
|
+
uiPermissions ← generated 'ui.<entity>.list' list, one per entity
|
|
204
|
+
customPermissions ← your hand-written extras (data-space)
|
|
205
|
+
customUiPermissions ← your hand-written extras (UI-space)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Running `initPermissions` (via `yarn init:permissions` or equivalent) is a **manual
|
|
209
|
+
step** — it is not triggered automatically by `regen`. A new entity's permissions exist
|
|
210
|
+
in code as soon as `regen` finishes (the service exists, `uiPermissions.ts` lists it),
|
|
211
|
+
but they only land in the `permissions` table the next time someone runs the seed
|
|
212
|
+
script.
|
|
213
|
+
|
|
214
|
+
**Anti-pattern — adding a custom permission string directly to `initPermissions.ts` or
|
|
215
|
+
`uiPermissions.ts`:**
|
|
216
|
+
|
|
217
|
+
Both are `[gen]` — overwritten on every `regen`. Add hand-written permissions to
|
|
218
|
+
`customPermissions.ts` (data-space) or `customUiPermissions.ts` (UI-space) instead.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Naming a new permission
|
|
223
|
+
|
|
224
|
+
- Data-space, tied to an existing service method → nothing to name; it already exists
|
|
225
|
+
via reflection.
|
|
226
|
+
- Data-space, not backed by a service method (a custom action) → add it to
|
|
227
|
+
`customPermissions.ts`, as `<entity-or-domain>.<action>`, matching the existing
|
|
228
|
+
`<entity>.<action>` convention.
|
|
229
|
+
- UI-only, not covered by the generated `ui.<entity>.list` set → add it to
|
|
230
|
+
`customUiPermissions.ts`, prefixed `ui.` so it's unambiguous that it is never
|
|
231
|
+
enforced by the backend.
|
package/docs/INDEX.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Runlify Documentation
|
|
2
|
+
|
|
3
|
+
> Written for **consumers** of Runlify — projects that already use it.
|
|
4
|
+
> Primary audience: AI agents. Secondary: human developers.
|
|
5
|
+
>
|
|
6
|
+
> Each file in this documentation is **self-contained** for its topic.
|
|
7
|
+
> Load only the file relevant to your current task.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
| # | File | Topic |
|
|
14
|
+
|---|------|-------|
|
|
15
|
+
| 1 | [01-overview.md](./01-overview.md) | Mental model, data flow, two classes of files (generated vs yours) |
|
|
16
|
+
| 2 | [02-system-meta-builder.md](./02-system-meta-builder.md) | All top-level methods on `SystemMetaBuilder` |
|
|
17
|
+
| 3 | [03-entity-types.md](./03-entity-types.md) | `Catalog`, `Document`, `InfoRegistry`, `SumRegistry`, `Report` — when to use each |
|
|
18
|
+
| 4 | [04-fields.md](./04-fields.md) | All field builders, scalar types, filters, indexes, constraints |
|
|
19
|
+
| 5 | [05-custom-methods.md](./05-custom-methods.md) | `MethodBuilder`, Input/Output models, workers, cron schedules |
|
|
20
|
+
| 6 | [06-storage.md](./06-storage.md) | `StorageType` options: Postgres, ClickHouse, Elasticsearch |
|
|
21
|
+
| 7 | [07-backend-file-graph.md](./07-backend-file-graph.md) | Annotated file tree for `<prefix>-back` |
|
|
22
|
+
| 8 | [08-frontend-file-graph.md](./08-frontend-file-graph.md) | Annotated file tree for `<prefix>-ui` |
|
|
23
|
+
| 9 | [09-options.md](./09-options.md) | All `BootstrapEntityOptions` flags with defaults |
|
|
24
|
+
| 10 | [10-modules.md](./10-modules.md) | Built-in modules, how to write your own |
|
|
25
|
+
| 11 | [11-permissions.md](./11-permissions.md) | Data-space vs UI-space permissions, where each is checked, how they're seeded |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick orientation
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
meta-project/
|
|
33
|
+
src/meta/metadata.ts ← you write this (SystemMetaBuilder DSL)
|
|
34
|
+
src/meta/options.json ← generation options (BootstrapEntityOptions)
|
|
35
|
+
|
|
36
|
+
runlify regen ← regenerates everything from the meta
|
|
37
|
+
|
|
38
|
+
<prefix>-back/ ← generated NestJS/Prisma/GraphQL backend
|
|
39
|
+
<prefix>-ui/ ← generated react-admin frontend
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The meta project **describes** the system. Runlify **generates** the implementation.
|
|
43
|
+
Business logic lives only in files Runlify creates once and never overwrites.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Two classes of files
|
|
48
|
+
|
|
49
|
+
| Class | Owner | Overwritten on `regen`? | Marker |
|
|
50
|
+
|-------|-------|------------------------|--------|
|
|
51
|
+
| **Generated** (`[gen]`) | Runlify | **Yes — every run** | `DO NOT EDIT! THIS IS GENERATED FILE` at top |
|
|
52
|
+
| **Yours** (`[yours]` / `[once]`) | Developer | No — created only if missing | Most have no marker; some (e.g. `PermissionPage.tsx`, `i18n/<lang>/index.ts`) have a short `Generated once, not overwritten on next generation. Edit freely.` note instead |
|
|
53
|
+
|
|
54
|
+
Yours files: `Additional<Entity>Service.ts`, `hooks/*`, `additionalRoutes.tsx`,
|
|
55
|
+
`Dashboard.tsx`, `getAdditionalMenu.ts`, `restRouter.ts`, `PermissionPage.tsx`,
|
|
56
|
+
`NotFoundPage.tsx`, `customPermissions.ts`, `customUiPermissions.ts`, and others.
|
|
57
|
+
See [07-backend-file-graph.md](./07-backend-file-graph.md) and [08-frontend-file-graph.md](./08-frontend-file-graph.md) for the full list.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Features not covered in this documentation
|
|
62
|
+
|
|
63
|
+
The following exist in Runlify but are intentionally omitted here — they are used
|
|
64
|
+
selectively and vary significantly per project. Refer to the Runlify source at
|
|
65
|
+
`src/projectsGeneration/builders/` and `src/commands/` for details.
|
|
66
|
+
|
|
67
|
+
- **Workers** — `addWorker()` on `SystemMetaBuilder`
|
|
68
|
+
- **Telegram Bots** — `addTelegramBot()` on `SystemMetaBuilder`
|
|
69
|
+
- **Integration Clients** — `addIntegrationClient()` on `SystemMetaBuilder`
|
|
70
|
+
- **Cloud CLI** — `login`, `pullEnvs`, `runlify start` commands
|