@diagonal-hq/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package is the authoring SDK for file-based workspaces. A workspace repository installs it as a dev dependency. It gives these things:
4
4
 
5
- - The configuration helpers: `defineConfig`, `defineResource`, `definePage`, `defineEmail`, `defineNavigation`, and the `field` builders. Each `define*` function returns its first argument. Each `field.<type>(options)` call returns `{ $field, ...options }`, where `$field` is the stored field type. The package root exports them as ESM JavaScript with declarations, so a Vite build or a Node script can import a configuration file.
5
+ - The configuration helpers: `defineConfig`, `defineResource`, `definePage`, `defineNavigation`, `defineNotificationType`, `defineNotificationTemplate`, `defineEmailLayout`, and the `field` builders. Each `define*` function returns its first argument. Each `field.<type>(options)` call returns `{ $field, ...options }`, where `$field` is the stored field type. The package root exports them as ESM JavaScript with declarations, so a Vite build or a Node script can import a configuration file.
6
6
  - Types for the helpers, `WorkflowConfig`, and the `Sdk` and `React` globals.
7
7
  - Editor types for the workflow script SDK: `@diagonal/workflow`, `@diagonal/workflow/testing`, and `@diagonal/workflow/workspace`.
8
8
  - The `diagonal check` command. It runs the same build and checks that a platform deploy runs, including the workflow tests.
@@ -47,14 +47,208 @@ diagonal run <path> [--input '<json object>'] [--json]
47
47
 
48
48
  The build reads each file with a parser. It runs only the workflow tests.
49
49
 
50
- - `defineConfig`, `defineResource`, `defineNavigation`, and the first argument of `definePage` and `defineEmail` must be static literals. The `field.<type>({...})` calls are the only calls that the build accepts in this data.
51
- - The build removes imports from `@diagonal-hq/cli`. It reads imports from `@diagonal/runtime`, the old name of the package, the same way. Pages, layouts, components, and workflow scripts can import workspace files through `@app/`. Workflow scripts can also import `@diagonal/workflow` and `@diagonal/workflow/workspace`. Other external imports in these source files are rejected.
52
- - The second argument of `definePage` and `defineEmail`, and the default export of a layout or component file, must be one function.
53
- - Pages, layouts, components, and emails go through the page lint gate: syntax, scope, the component return shape, and a type check against the page Sdk contract and the workspace resources. A page can name a resource by id or by slug, for example `Sdk.resource('members')`.
54
- - Each `workflows/<name>.ts` is a script workflow. It has one `export const config = { title, trigger, runAs? } satisfies WorkflowConfig` and one `export default defineWorkflow(...)`. The config must be a static literal. The build validates it: the trigger fields, the resources (by slug, name, or id), the workflow paths of `workflowEvent` triggers, and the run-as rules. A `recordEvent`, `recordSchedule`, `scheduled`, or `workflowEvent` workflow needs `runAs`.
50
+ - `defineConfig`, `defineResource`, `defineNavigation`, and the first argument of `definePage` must be static literals. The `field.<type>({...})` calls are the only calls that the build accepts in this data.
51
+ - Each `resources/*.ts` file goes through the resource checks in [Resources and fields](#resources-and-fields).
52
+ - The build removes imports from `@diagonal-hq/cli`. Pages, layouts, components, and workflow scripts can import workspace files through `@app/`. A resource file can import only `@app/generated/resources`. Workflow scripts can also import `@diagonal/workflow` and `@diagonal/workflow/workspace`. Other external imports in these source files are rejected.
53
+ - The second argument of `definePage`, and the default export of a layout or component file, must be one function.
54
+ - Pages, layouts, and components go through the page lint gate: syntax, scope, the component return shape, and a type check against the page Sdk contract and the workspace resources. A page names a resource by slug, for example `Sdk.resource(Resources.Members)`.
55
+ - Each `workflows/<name>.ts` is a script workflow. It has one `export const config = { title, trigger, runAs? } satisfies WorkflowConfig` and one `export default defineWorkflow(...)`. The config must be a static literal. The build validates it: the trigger fields, the resources (see [Resource references](#resource-references)), the workflow paths of `workflowEvent` triggers, and the run-as rules. A `recordEvent`, `recordSchedule`, `scheduled`, or `workflowEvent` workflow needs `runAs`.
55
56
  - The build removes the config and the `@diagonal-hq/cli` imports from the workflow, with spaces in their place, and compiles the rest with the workflow script compiler against the workspace resources. A compiler error has the line and column of the workflow file.
56
- - `workflows/<name>.test.ts` tests `workflows/<name>.ts` with `node:test` and `node:assert`. It imports the workflow, `runWorkflow` and `MockTransport` from `@diagonal/workflow/testing`, and the resource ids from `@diagonal/workflow/workspace`. The build transpiles the test and the workflows and runs them in one Node process. The process can read only its own temporary directory, cannot start processes, and has an empty environment. Each failing test is an error at the test. A test file without a workflow is an error.
57
- - The build ignores the files at the root other than `diagonal.config.ts`, dotfiles, and `node_modules`.
57
+ - `workflows/<name>.test.ts` tests `workflows/<name>.ts` with `node:test` and `node:assert`. It imports the workflow, `runWorkflow` and `MockTransport` from `@diagonal/workflow/testing`, and `Resources` from `@app/generated/resources`. The build transpiles the test and the workflows and runs them in one Node process. The process can read only its own temporary directory, cannot start processes, and has an empty environment. Each failing test is an error at the test. A test file without a workflow is an error.
58
+ - The build ignores the files at the root other than `diagonal.config.ts`, dotfiles, and `node_modules`. It does not build the files in `generated/`, but it checks `generated/resources.ts`.
59
+
60
+ - The build does not read `notifications/` or `email-layouts/` yet. The editor type-checks the shape of their definitions. See [Notifications](#notifications).
61
+
62
+ ## Resource references
63
+
64
+ The file name is the resource slug: `resources/job-postings.ts` declares the resource `job-postings`. A file name must start with a lowercase letter and contain only a-z, 0-9, `_`, and `-`. A resource has no `id` in source. The platform derives the stored id from the workspace and the slug, so each branch and environment has its own ids.
65
+
66
+ `diagonal check` writes `generated/resources.ts` from the `resources/` folder. Commit the file:
67
+
68
+ ```ts
69
+ // AUTO-GENERATED by `diagonal check`. Do not edit.
70
+ export const Resources = {
71
+ JobPostings: 'job-postings',
72
+ User: 'user'
73
+ } as const;
74
+ ```
75
+
76
+ Each property is the studly case of a slug, and its value is the slug. A deploy fails when the file does not match the folder, and the error gives the expected content. Two slugs with the same name, such as `job-postings` and `job_postings`, are an error.
77
+
78
+ Refer to a resource with `Resources.<Name>` from `@app/generated/resources` everywhere: `field.relation({ resource: Resources.User })`, a workflow `trigger.resource` and `runAs.resource`, a page `authorizer.resource` and `params.<name>.resource`, `Sdk.resource(Resources.User)`, `data.search({ resource: Resources.User })`, and `v.record(Resources.User)`. The build reads `Resources.<Name>` like a literal. An unknown name is an error at its location, and the editor reports it too.
79
+
80
+ A slug string, such as `resource: 'user'`, builds with a warning. A resource id, a resource name, a singular name, or a slug without a file is an error.
81
+
82
+ The platform provisions some resources itself, such as the staff resource, the roles, and the permissions. A new repository has a reference file for each one, for example `resources/roles.ts`:
83
+
84
+ ```ts
85
+ import { defineResource } from '@diagonal-hq/cli';
86
+
87
+ export default defineResource({ platform: true });
88
+ ```
89
+
90
+ The file gives the resource its `Resources` entry, so source names it like any other resource. The platform owns its fields, and the deploy does not change it. The build does not know its fields, so a data call types its records loosely, and a relation to it cannot declare an inverse. A deploy fails when the workspace has no platform resource with the slug of the file.
91
+
92
+ ## Notifications
93
+
94
+ A notification has a type, one or more templates, and optionally an email layout. Each definition is a static literal. Its Liquid is inline text or a `.liquid` file next to it, written as `{ file: './standard.email.liquid' }`.
95
+
96
+ ```text
97
+ notifications/
98
+ member-welcome/
99
+ type.ts
100
+ standard.template.ts
101
+ standard.email.liquid
102
+ standard.sms.liquid
103
+ fixtures/
104
+ default.json
105
+ email-layouts/
106
+ standard/
107
+ layout.ts
108
+ body.liquid
109
+ fixtures/
110
+ default.json
111
+ ```
112
+
113
+ The type sets the recipient resource, the variables, and the default template. The recipient resource maps the email and phone fields. A type without `recipient` sends to email addresses. A variable uses a `field` builder other than `field.relation`, or `{ resource }` for one record of a resource. Templates read a record variable's fields, for example `{{ first_event.name }}`.
114
+
115
+ ```ts
116
+ import { Resources } from '@app/generated/resources';
117
+ import { defineNotificationType, field } from '@diagonal-hq/cli';
118
+
119
+ export default defineNotificationType({
120
+ key: 'member-welcome',
121
+ name: 'Member welcome',
122
+ recipient: { resource: Resources.Members },
123
+ variables: {
124
+ joined_on: field.date({ label: 'Joined on', required: true }),
125
+ first_event: { resource: Resources.Events, label: 'First event' }
126
+ },
127
+ defaultTemplate: 'standard'
128
+ });
129
+ ```
130
+
131
+ A template has an email, an SMS, or both. A channel is on when the template has it. The email gives a Liquid value for each field of its layout. `layout` defaults to `'default'`, the built-in layout, which has one field, `content`. `references` adds generators that the template reads as `{{ ref.<key> }}`.
132
+
133
+ ```ts
134
+ import { defineNotificationTemplate } from '@diagonal-hq/cli';
135
+
136
+ export default defineNotificationTemplate({
137
+ key: 'standard',
138
+ type: 'member-welcome',
139
+ name: 'Standard welcome',
140
+ email: {
141
+ subject: 'Welcome to {{ workspace.name }}, {{ record.name }}',
142
+ layout: 'standard',
143
+ fields: { heading: 'Welcome aboard', body: { file: './standard.email.liquid' } }
144
+ },
145
+ sms: { body: { file: './standard.sms.liquid' } },
146
+ references: {
147
+ portal_link: {
148
+ label: 'Member portal',
149
+ generator: 'page.url',
150
+ settings: { page: 'portal/home' }
151
+ }
152
+ }
153
+ });
154
+ ```
155
+
156
+ An email layout is the frame that templates fill in. Each field is a slot that a template fills with Liquid.
157
+
158
+ ```ts
159
+ import { defineEmailLayout } from '@diagonal-hq/cli';
160
+
161
+ export default defineEmailLayout({
162
+ key: 'standard',
163
+ name: 'Standard',
164
+ body: { file: './body.liquid' },
165
+ fields: {
166
+ heading: { label: 'Heading', required: true },
167
+ body: { label: 'Body', required: true }
168
+ }
169
+ });
170
+ ```
171
+
172
+ ### Rules
173
+
174
+ The editor checks the shape of each definition. These rules need other files or server data, so the editor does not check them yet:
175
+
176
+ - A type cannot have a variable named `record` or `ref`. Templates read the recipient as `{{ record.* }}` and the generators as `{{ ref.* }}`.
177
+ - A template's `type` names an existing type, and the type's `defaultTemplate` names one of its templates.
178
+ - An email's `fields` match the fields of its layout, and fill each required field.
179
+ - A layout cannot use the key `default`.
180
+ - Each reference key starts with a lowercase letter. A template has at most 50 references, and each one names a generator that the server has.
181
+ - The `recipient` resource can receive notifications.
182
+
183
+ ### Fixtures
184
+
185
+ Fixtures are sample data for previews. They are not tests, and they are never sent. A type folder and a layout folder can each have a `fixtures/` folder of JSON files. `default.json` is the case that a preview opens first.
186
+
187
+ - A layout fixture holds a value for each layout field, for example `{ "heading": "Welcome aboard", "body": "<p>Hi Avery.</p>" }`.
188
+ - A notification fixture holds a template key, a sample recipient, and sample variables, for example `{ "template": "standard", "recipient": { "name": "Avery Shaw", "email": "avery@example.com", "phone": null }, "variables": { "joined_on": "2026-11-02", "first_event": { "name": "Open house" } } }`. A record variable holds the record's fields.
189
+
190
+ ## Resources and fields
191
+
192
+ `defineResource` takes `name`, `singularName`, `displayField`, `searchableFields`, `fieldOrder`, `fields`, `indexes`, `embeddings`, `dataApi`, `static`, `policies`, and `capabilities`. The build refuses an unknown property, an unknown setting, and a duplicate key, and gives the line and column. It does not remove anything without a diagnostic.
193
+
194
+ - `searchableFields` lists the field keys and relation paths that record search matches, for example `['title', 'author.full_name']`. The build checks each path against the related resources. Without it, search matches the display field and every text and markdown field.
195
+ - `fieldOrder` lists every field key once. It sets the order of the fields in forms and tables. The default is the order of `fields`.
196
+ - `embeddings` is `{ enabled, fields }`. `fields` names fields of the resource, not relation paths.
197
+ - `dataApi.endpoints` turns off data API endpoints, for example `{ delete_many: { enabled: false } }`. An omitted endpoint stays enabled.
198
+ - `static: true` refuses normal record writes to the resource.
199
+
200
+ There is one `field.<type>` builder for each resource field type, in camel case: `field.eventSchedule()` stores an `event_schedule` field. `field.file()` stores a `file_reference` field, and `field.longText()` stores a `text` field. `fields.generated.d.ts` declares the builders, `fields.generated.js` implements them, and `field-catalog.json` lists their settings. `npm run generate-types` generates the three files from the platform field types. Do not edit them.
201
+
202
+ A builder takes these options:
203
+
204
+ - `label`, `description`, `required`, `default`, `unique`, and `index`.
205
+ - `guard`: more validation rules, for example `['max:255']`. Use `required` for the `required` and `nullable` rules.
206
+ - `settings`: the settings of the field type, with their stored names, and `settings.ui` for the record form. The seeder settings `seed_type`, `faker_method`, `faker_parameters`, and `seed_data` apply to every type.
207
+ - `computed`: a resolver tree that computes the value when a record is read. A computed field is not stored, so it cannot have `default`, `getter`, `setter`, `unique`, or `index`.
208
+ - `getter`: a resolver tree that transforms the stored value when it is read.
209
+ - `setter`: a resolver tree that computes the stored value before each create and update. `setterWhenEmpty: true` runs it only when the value is null or empty.
210
+
211
+ `field.select` takes `options` and `field.relation` takes `resource` (`Resources.<Name>`) and `type` as options. Other settings go in `settings`. A setting that an option writes, such as `settings.unique`, is refused. A relation can declare `settings.inverse: { key, label?, settings? }`. The build then adds the inverse relation field to the target resource. A target that declares a field with the same key is an error.
212
+
213
+ The platform deploy validates every field again with the rules of the resource builder, so a value that the build cannot check, such as an unknown guard rule, fails the deploy with the file path.
214
+
215
+ ## Capabilities
216
+
217
+ A resource gets a platform capability, such as Authenticatable, Notifiable, Payable, Subscription, or Eventable, from its `capabilities` key. The settings have the names that the resource builder uses. A field reference names a field of the same resource by its key:
218
+
219
+ ```ts
220
+ export default defineResource({
221
+ id: '0199f300-0000-7000-8000-000000000005',
222
+ slug: 'members',
223
+ name: 'Members',
224
+ singularName: 'Member',
225
+ fields: {
226
+ name: field.text({ required: true }),
227
+ email: field.email({ required: true, unique: true }),
228
+ password: field.password()
229
+ },
230
+ policies: [],
231
+ capabilities: {
232
+ authenticatable: { email_field: 'email', password_field: 'password' },
233
+ notifiable: { email_field: 'email' }
234
+ }
235
+ });
236
+ ```
237
+
238
+ `capabilities.generated.d.ts` declares the settings of each capability, and `capability-catalog.json` lists them. `npm run generate-types` generates both from the platform capabilities. Do not edit them.
239
+
240
+ - The types accept only a field key whose field type the setting allows. For example, `notifiable.email_field` accepts only an `email` field.
241
+ - A file cannot declare `role` or `permission`. Authorization provisioning owns the Role and Permission resources. It also maps `authenticatable.roles_field`, so a file cannot set it.
242
+ - A file cannot set a page reference, such as `authenticatable.login_page`. A page reference names a page row, and the pages of a file-based workspace are files.
243
+ - The build checks the setting names, the required settings, the select values, and each field reference. A relation path, such as an Itemizable offering path, names a relation of each resource on the way.
244
+ - Each field reference is stored as the field id, as the resource builder stores it. This includes a nested reference, such as an Eventable excluded field, and each key of a relation path.
245
+ - The deploy validates each capability with the resource builder's rules against the deployed schema. These rules include the required select options, the field types, and the capability of each resource that a relation setting targets. A capability that fails rejects the deploy, and the previous deployment keeps serving.
246
+ - After the deploy, the platform runs the same provisioning as the resource builder. It creates the system fields of a Subscription, the accounting entity fields, the billing relations, the record indexes, and the booking and schedule setup. For an Authenticatable resource, it creates the Role and Permission resources, so members can sign in.
247
+ - A setting that provisioning fills can be left out. A Subscription starts from the builder's default settings, so set only the defaults that change.
248
+ - Provisioning can create a field, for example `accounting_entity`. The files cannot declare a field with the same key later. The deploy refuses it until you delete the field in the resource builder or use another key.
249
+ - A capability change in the resource builder is drift. The next deploy reports it and restores the capability from the files.
250
+
251
+ A workflow script reads the capabilities of each resource from `@diagonal/workflow/workspace`. An input that needs a capability, such as the `resource` of `notification.setConsent`, accepts only a resource that declares it.
58
252
 
59
253
  ## Editor types
60
254
 
@@ -72,24 +266,28 @@ Add a `tsconfig.json` with `"types": ["@diagonal-hq/cli", "node"]` and `@types/n
72
266
  }
73
267
  ```
74
268
 
75
- In the editor, `resources` from `@diagonal/workflow/workspace` maps every name to a string, and record data is untyped. `diagonal check` compiles against the workspace resources and their fields.
269
+ In the editor, a data call takes any resource string, and record data is untyped. `Resources` from `@app/generated/resources` catches a wrong resource name. `diagonal check` compiles against the workspace resources and their fields. In a file-based workspace, `@diagonal/workflow/workspace` exports no `resources` map.
76
270
 
77
271
  `globals.d.ts` at the package root has loose `Sdk` and `React` types. The source `index.d.ts` uses them before the package is built.
78
272
 
79
273
  ## Build the package
80
274
 
81
- `npm run build:cli-package` (`scripts/build-cli-package.mjs`) writes `dist/` and packs `public/runtime-sdk/diagonal-hq-cli-<version>.tgz`. It also writes a copy as `diagonal-runtime-<version>.tgz`, the URL that workspaces made before the rename depend on. `dist/cli.mjs` bundles every dependency, the TypeScript lib files, the workflow script compiler with the script SDK sources, and the bundled script SDK that workflow tests import, so the command needs no Diagonal checkout. The script needs the page Sdk contract and `scripts/generated/typeCheckLibs.mjs`. `npm run generate-typescript` makes both and then runs the script.
275
+ `npm run build:cli-package` (`scripts/build-cli-package.mjs`) writes `dist/` and packs `scripts/dist/npm/diagonal-hq-cli-<version>.tgz`. `dist/cli.mjs` bundles every dependency, the TypeScript lib files, the workflow script compiler with the script SDK sources, and the bundled script SDK that workflow tests import, so the command needs no Diagonal checkout. The script needs the page Sdk contract and `scripts/generated/typeCheckLibs.mjs`. `npm run generate-typescript` makes both and then runs the script.
82
276
 
83
- `node scripts/smoke-cli-package.mjs [tarball]` installs the packed tarball in a copy of the chamber template outside the repository. It checks the packed file list, imports the helpers through the `exports` map, runs `tsc` against the packed declarations, and runs `diagonal check`. The `generated-assets` job of PR CI runs it.
277
+ `node scripts/smoke-cli-package.mjs [tarball]` installs the packed tarball in a copy of the starter template outside the repository, with one workflow and its test added. It checks the packed file list, imports the helpers through the `exports` map, runs `tsc` against the packed declarations, and runs `diagonal check`. The `generated-assets` job of PR CI runs it.
84
278
 
85
279
  ## Publish the package
86
280
 
87
- The `Publish npm package` workflow (`.github/workflows/publish-npm-package.yml`) publishes the package to npm. It runs only from `main`.
281
+ The `Publish npm package` workflow (`.github/workflows/publish-npm-package.yml`) publishes the package to npm after each push to `main`. You can also run it from the Actions tab.
282
+
283
+ 1. The build job builds and packs the package. It compares the packed files with the tarball that npm has for the version in `package.json`. The `version` field does not count.
284
+ 2. When the files differ, a model reads the diff and the commit subjects since that publish. It classifies the change as breaking, feature, or fix. Before 1.0.0, a breaking change raises the minor part, and other changes raise the patch part. From 1.0.0, the workflow follows semantic versioning.
285
+ 3. The build job packs the package again at the new version and runs the smoke test on the tarball.
286
+ 4. The commit job commits the new version to `main` with an `[automated] release:` commit.
287
+ 5. The publish job runs in the `npm-publish` environment and publishes the tested bytes with npm trusted publishing. It does not build again. It refuses a version that npm already has.
88
288
 
89
- 1. Set `version` in `package.json` to a version that npm does not have. A version with a prerelease part, such as `0.2.0-next.1`, gets the `next` tag. Other versions get `latest`.
90
- 2. Merge the change to `main`.
91
- 3. Run the workflow from the Actions tab and select `cli`.
289
+ To choose a version yourself, such as 1.0.0 or a prerelease, set `version` in `package.json` to a version that npm does not have, and merge the change. The workflow publishes that version as it is. A version with a prerelease part, such as `0.2.0-next.1`, gets the `next` tag. Other versions get `latest`. The workflow does not raise a prerelease version. Set the next version by hand.
92
290
 
93
- The build job builds the package, packs it, and runs the smoke test on the tarball. The publish job runs in the `npm-publish` environment and publishes those exact bytes with npm trusted publishing. It does not build again. It refuses a version that npm already has.
291
+ The model call uses the `OPENROUTER_API_KEY` secret. When the call fails, the release fails. The workflow does not guess a version.
94
292
 
95
293
  npm must list `Diagonal-HQ/diagonal`, the `publish-npm-package.yml` workflow, and the `npm-publish` environment as the trusted publisher of `@diagonal-hq/cli`.