@crombolo/cli 0.16.0 → 0.18.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.
@@ -1152,6 +1152,10 @@
1152
1152
  {
1153
1153
  "name": "template",
1154
1154
  "type": "boolean"
1155
+ },
1156
+ {
1157
+ "name": "email",
1158
+ "type": "boolean"
1155
1159
  }
1156
1160
  ],
1157
1161
  "errors": [
@@ -8073,5 +8077,5 @@
8073
8077
  ]
8074
8078
  }
8075
8079
  },
8076
- "version": "0.16.0"
8080
+ "version": "0.18.0"
8077
8081
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crombolo/cli",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "The crombolo command line: scaffolding a site, schema, views, domains, content transfer and upgrades.",
5
5
  "license": "MIT",
6
6
  "author": "Crombolo",
@@ -36,13 +36,13 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@crombolo/api-management": "0.16.0",
40
- "@crombolo/auth": "0.16.0",
41
- "@crombolo/core": "0.16.0",
42
- "@crombolo/data": "0.16.0",
43
- "@crombolo/schema": "0.16.0",
44
- "@crombolo/server": "0.16.0",
45
- "@crombolo/transfer": "0.16.0",
39
+ "@crombolo/api-management": "0.18.0",
40
+ "@crombolo/auth": "0.18.0",
41
+ "@crombolo/core": "0.18.0",
42
+ "@crombolo/data": "0.18.0",
43
+ "@crombolo/schema": "0.18.0",
44
+ "@crombolo/server": "0.18.0",
45
+ "@crombolo/transfer": "0.18.0",
46
46
  "@oclif/core": "^5.1.8"
47
47
  },
48
48
  "oclif": {
package/src/classify.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * code and the envelope follow from one table.
4
4
  */
5
5
  import { MigrationStepError, UpgradeStateError } from '@crombolo/data'
6
- import { CommandError, PassphraseError } from '@crombolo/server'
6
+ import { CommandError, PassphraseError, setting } from '@crombolo/server'
7
7
  import { CliError, ERROR_KINDS, type ErrorKind } from './errors.ts'
8
8
  import { RemoteError, TimedOut } from './remote/client.ts'
9
9
 
@@ -84,7 +84,7 @@ export function classify(error: unknown): CliError {
84
84
  if (typeof code === 'string' && SYSTEM_CODES[code])
85
85
  return new CliError(SYSTEM_CODES[code], (error as Error).message)
86
86
  return new CliError('internal', error instanceof Error ? error.message : String(error), {
87
- details: process.env.CROMBOLO_DEBUG && error instanceof Error ? error.stack : undefined,
87
+ details: setting('CROMBOLO_DEBUG') && error instanceof Error ? error.stack : undefined,
88
88
  })
89
89
  }
90
90
 
@@ -2,10 +2,16 @@ import { existsSync } from 'node:fs'
2
2
  import { mkdir, writeFile } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
4
  import { viewScaffold } from '@crombolo/schema'
5
- import { componentFileFor, componentScaffold, isSafeAlias } from '@crombolo/server'
5
+ import {
6
+ componentFileFor,
7
+ componentScaffold,
8
+ EMAIL_FOLDER,
9
+ emailScaffold,
10
+ isSafeAlias,
11
+ } from '@crombolo/server'
6
12
  import { Args } from '@oclif/core'
7
13
  import { BaseCommand } from '../../base.ts'
8
- import { declaredTypes } from '../../support/components.ts'
14
+ import { declaredTypes, formsNamingEmailTemplate } from '../../support/components.ts'
9
15
  import { here } from '../../support/describe.ts'
10
16
  import { siteConfig } from '../../support/site.ts'
11
17
 
@@ -21,6 +27,7 @@ export default class ComponentsNew extends BaseCommand {
21
27
  outputFields: [
22
28
  { name: 'created', type: 'string' },
23
29
  { name: 'template', type: 'boolean' },
30
+ { name: 'email', type: 'boolean' },
24
31
  ],
25
32
  errors: ['usage', 'conflict'],
26
33
  } as const
@@ -33,17 +40,25 @@ export default class ComponentsNew extends BaseCommand {
33
40
  const file = componentFileFor(config.componentsDir, args.alias)
34
41
  if (existsSync(file)) this.fail('conflict', `${here(file)} already exists.`)
35
42
  // A content type that names this alias gets the page scaffold, which takes
36
- // PageProps; anything else gets the plain one. Nothing turns on the folder.
43
+ // PageProps; one in emails/, or one a form's Send email names, gets the
44
+ // e-mail one, which takes EmailProps; anything else gets the plain one.
37
45
  const type = declaredTypes(config).find((candidate) =>
38
46
  candidate.components.includes(args.alias),
39
47
  )
40
- if (!type)
48
+ const email =
49
+ !type &&
50
+ (args.alias.startsWith(EMAIL_FOLDER) ||
51
+ formsNamingEmailTemplate(config, args.alias).length > 0)
52
+ if (!type && !email)
41
53
  this.say(
42
54
  ` note no content type declares the template "${args.alias}" — add it to one's templates, or \`crombolo types new\``,
43
55
  )
44
56
  await mkdir(join(file, '..'), { recursive: true })
45
- await writeFile(file, type ? viewScaffold(type) : componentScaffold(args.alias))
57
+ await writeFile(
58
+ file,
59
+ type ? viewScaffold(type) : email ? emailScaffold(args.alias) : componentScaffold(args.alias),
60
+ )
46
61
  this.say(` created ${here(file)}`)
47
- return { created: here(file), template: Boolean(type) }
62
+ return { created: here(file), template: Boolean(type), email }
48
63
  }
49
64
  }
@@ -67,6 +67,7 @@ import {
67
67
  replaceSummary,
68
68
  type SiteExportOptions,
69
69
  type SiteExportResult,
70
+ setting,
70
71
  syncOptionsFor,
71
72
  type TransferInput,
72
73
  useSchemaStore,
@@ -723,7 +724,7 @@ export interface CommandAudit {
723
724
  * Never fails the command: what it records has already happened.
724
725
  */
725
726
  export async function recordCommand(config: CromboloConfig, entry: CommandAudit): Promise<void> {
726
- let actor = Bun.env.CROMBOLO_ACTOR
727
+ let actor = setting('CROMBOLO_ACTOR')
727
728
  try {
728
729
  actor ??= userInfo().username
729
730
  } catch {
package/src/reference.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The command line reference as HTML, for the product site's developer pages:
3
- * built from the CLI manifest's data, through the same clispec description
4
- * `crombolo schema` prints, so the page cannot say anything the CLI does not.
2
+ * The command line reference as HTML, for the product site's docs: built from
3
+ * the CLI manifest's data, through the same clispec description
4
+ * `crombolo schema` prints, so the pages cannot say anything the CLI does not.
5
5
  */
6
6
  import type { Interfaces } from '@oclif/core'
7
7
  import { clispecDocument } from './clispec.ts'
@@ -25,6 +25,73 @@ interface Arg {
25
25
  description?: string
26
26
  }
27
27
 
28
+ /** One page of the reference: the topics, by the first word of a command, it covers. */
29
+ export interface CliPageSpec {
30
+ slug: string
31
+ title: string
32
+ summary: string
33
+ topics: readonly string[]
34
+ }
35
+
36
+ /** Where each topic is documented. A new topic has to be placed here, or the reference test fails. */
37
+ export const CLI_PAGES: readonly CliPageSpec[] = [
38
+ {
39
+ slug: 'sites',
40
+ title: 'Sites',
41
+ summary: 'Create a site, start it, check on it, and describe the command line to a script.',
42
+ topics: ['init', 'start', 'status', 'health', 'generate', 'export', 'schema'],
43
+ },
44
+ {
45
+ slug: 'types',
46
+ title: 'Content types',
47
+ summary: 'Check, apply and change the content model kept in schema/.',
48
+ topics: ['types'],
49
+ },
50
+ {
51
+ slug: 'components',
52
+ title: 'Components and assets',
53
+ summary:
54
+ 'Create and check the templates under components/, and the styles and scripts beside them.',
55
+ topics: ['components', 'assets'],
56
+ },
57
+ {
58
+ slug: 'content',
59
+ title: 'Content and media',
60
+ summary: 'Pages, media, the dictionary, domains and the recycle bin.',
61
+ topics: ['content', 'media', 'dictionary', 'domains', 'recycle-bin'],
62
+ },
63
+ {
64
+ slug: 'remotes',
65
+ title: 'Remotes, pull and push',
66
+ summary: 'Sign in to another environment, and move content and schema between them.',
67
+ topics: ['login', 'logout', 'whoami', 'remote', 'pull', 'push', 'bundle'],
68
+ },
69
+ {
70
+ slug: 'import',
71
+ title: 'Importing from Umbraco',
72
+ summary: 'Bring an Umbraco site’s content types, content and media across.',
73
+ topics: ['import'],
74
+ },
75
+ {
76
+ slug: 'users',
77
+ title: 'Users and access',
78
+ summary: 'Back office users, the administrator’s password and API clients.',
79
+ topics: ['users', 'admin', 'api-client'],
80
+ },
81
+ {
82
+ slug: 'operations',
83
+ title: 'Operations',
84
+ summary: 'Caches, the search index, maintenance pauses and upgrades.',
85
+ topics: ['cache', 'index', 'maintenance', 'upgrade'],
86
+ },
87
+ ]
88
+
89
+ export interface CliReferencePage extends CliPageSpec {
90
+ html: string
91
+ /** The commands on the page, as typed: `content publish`. */
92
+ commands: string[]
93
+ }
94
+
28
95
  function usage(name: string, args: Arg[]): string {
29
96
  const words = args.map((arg) => {
30
97
  if (!arg.name.startsWith('--')) return arg.required ? `<${arg.name}>` : `[<${arg.name}>]`
@@ -61,58 +128,128 @@ const EFFECTS: Record<string, string> = {
61
128
  non_idempotent: 'Each run acts again.',
62
129
  }
63
130
 
64
- /** The reference, grouped by the first word of each command. */
65
- export function cliReferenceHtml(manifest: Interfaces.Manifest): string {
66
- const document = clispecDocument({
131
+ const documentOf = (manifest: Interfaces.Manifest) =>
132
+ clispecDocument({
67
133
  bin: 'crombolo',
68
134
  version: manifest.version,
69
135
  commands: Object.values(manifest.commands),
70
136
  })
71
- const groups = new Map<string, typeof document.commands>()
72
- for (const command of document.commands) {
73
- const group = String(command.name).split(' ')[0] as string
74
- groups.set(group, [...(groups.get(group) ?? []), command])
75
- }
76
- const parts: string[] = [
77
- '<p>Every command, generated from the CLI itself. Each takes ' +
78
- `${code('--help')} for this page's entry, and ${code('-o json')} for a result a script can read. ` +
79
- `${code('crombolo schema')} prints the same description for a machine, as a ` +
80
- '<a href="https://clispec.dev">clispec</a> document.</p>',
137
+
138
+ type Command = ReturnType<typeof documentOf>['commands'][number]
139
+
140
+ const topicOf = (command: Command) => String(command.name).split(' ')[0] as string
141
+
142
+ /** The anchor a command has on its page: `content publish` → `content-publish`. */
143
+ export const commandAnchor = (name: string): string => name.replaceAll(' ', '-')
144
+
145
+ function commandHtml(command: Command): string {
146
+ const name = String(command.name)
147
+ const args = ((command.args as Arg[] | undefined) ?? []).slice()
148
+ const parts = [
149
+ `<h2 id="${escapeHtml(commandAnchor(name))}">${code(`crombolo ${name}`)}</h2>`,
150
+ `<p>${escapeHtml(sentence(String(command.description)))} ${EFFECTS[String(command.effects)] ?? ''}</p>`,
151
+ `<pre><code>${escapeHtml(usage(name, args))}</code></pre>`,
81
152
  ]
82
- for (const [group, commands] of groups) {
83
- parts.push(`<h2 id="${escapeHtml(group)}">${escapeHtml(group)}</h2>`)
84
- for (const command of commands) {
85
- const name = String(command.name)
86
- const args = ((command.args as Arg[] | undefined) ?? []).slice()
87
- parts.push(
88
- `<h3 id="${escapeHtml(name.replaceAll(' ', '-'))}">${code(`crombolo ${name}`)}</h3>`,
89
- )
90
- parts.push(
91
- `<p>${escapeHtml(sentence(String(command.description)))} ${EFFECTS[String(command.effects)] ?? ''}</p>`,
92
- )
93
- parts.push(`<pre><code>${escapeHtml(usage(name, args))}</code></pre>`)
94
- if (args.length > 0)
95
- parts.push(
96
- `<table><thead><tr><th>Argument</th><th>What it is</th></tr></thead><tbody>${args.map(argRow).join('')}</tbody></table>`,
153
+ if (args.length > 0)
154
+ parts.push(
155
+ `<table><thead><tr><th>Argument</th><th>What it is</th></tr></thead><tbody>${args.map(argRow).join('')}</tbody></table>`,
156
+ )
157
+ return parts.join('\n')
158
+ }
159
+
160
+ /** The topics no page in `CLI_PAGES` covers. */
161
+ export function unplacedTopics(manifest: Interfaces.Manifest): string[] {
162
+ const placed = new Set(CLI_PAGES.flatMap((page) => page.topics))
163
+ return [...new Set(documentOf(manifest).commands.map(topicOf))].filter((t) => !placed.has(t))
164
+ }
165
+
166
+ /**
167
+ * One page per entry in `CLI_PAGES`, in its order. A topic no page names — a
168
+ * plugin's, say — gets a page of its own after them, so nothing is dropped.
169
+ */
170
+ export function cliReferencePages(manifest: Interfaces.Manifest): CliReferencePage[] {
171
+ const commands = documentOf(manifest).commands
172
+ const specs = [
173
+ ...CLI_PAGES,
174
+ ...unplacedTopics(manifest).map((topic) => ({
175
+ slug: topic,
176
+ title: topic,
177
+ summary: `The ${topic} commands.`,
178
+ topics: [topic],
179
+ })),
180
+ ]
181
+ return specs
182
+ .map((spec) => {
183
+ const on = spec.topics.flatMap((topic) => commands.filter((c) => topicOf(c) === topic))
184
+ return {
185
+ ...spec,
186
+ commands: on.map((c) => String(c.name)),
187
+ html: on.map(commandHtml).join('\n'),
188
+ }
189
+ })
190
+ .filter((page) => page.commands.length > 0)
191
+ }
192
+
193
+ /** The front page of the reference: what every command shares, then each page and its commands. */
194
+ export function cliIndexHtml(pages: readonly CliReferencePage[], base: string): string {
195
+ const parts = [
196
+ `<p>Every command, generated from the CLI itself. Each takes ${code('--help')} for its entry ` +
197
+ `here, and ${code('-o json')} for a result a script can read. ${code('crombolo schema')} prints ` +
198
+ 'the same description for a machine, as a <a href="https://clispec.dev">clispec</a> document. ' +
199
+ 'A command that fails exits with a code saying why: see the <a href="/docs/reference/exit-codes/">exit codes</a>.</p>',
200
+ ]
201
+ for (const page of pages) {
202
+ const url = `${base}${page.slug}/`
203
+ parts.push(
204
+ `<h2 id="${escapeHtml(page.slug)}"><a href="${url}">${escapeHtml(page.title)}</a></h2>`,
205
+ )
206
+ parts.push(`<p>${escapeHtml(page.summary)}</p>`)
207
+ parts.push(
208
+ `<ul>${page.commands
209
+ .map(
210
+ (name) =>
211
+ `<li><a href="${url}#${commandAnchor(name)}">${code(`crombolo ${name}`)}</a></li>`,
97
212
  )
98
- }
213
+ .join('')}</ul>`,
214
+ )
99
215
  }
100
- parts.push('<h2 id="exit-codes">Exit codes</h2>')
101
- parts.push(
102
- `<p>A failure exits with its kind's code; with ${code('-o json')}, or whenever stderr is not a terminal, ` +
103
- "the last line of stderr is a JSON envelope naming the kind. A check that finds what it looks for exits with an outcome's code, and its findings are its output.</p>",
104
- )
216
+ return parts.join('\n')
217
+ }
218
+
219
+ /** The exit codes and the error envelope, from the CLI's own list. */
220
+ export function exitCodesHtml(manifest: Interfaces.Manifest): string {
221
+ const document = documentOf(manifest)
105
222
  const rows = [
106
223
  ...document.errors.map((e) => [String(e.exit_code), e.kind, e.description] as const),
107
224
  ...document.outcomes.map((o) => [String(o.code), o.name, o.description] as const),
108
225
  ].sort((a, b) => Number(a[0]) - Number(b[0]))
109
- parts.push(
226
+ return [
227
+ '<h2 id="codes">The codes</h2>',
228
+ `<p>A command that succeeds exits with ${code('0')}. A failure exits with its kind's code, and a ` +
229
+ "check that finds what it looks for exits with an outcome's code, its findings being its output.</p>",
110
230
  `<table><thead><tr><th>Code</th><th>Kind</th><th>When</th></tr></thead><tbody>${rows
111
231
  .map(
112
232
  ([codeNumber, kind, description]) =>
113
233
  `<tr><td>${codeNumber}</td><td>${code(kind)}</td><td>${escapeHtml(description).replaceAll(/`([^`]+)`/g, '<code>$1</code>')}</td></tr>`,
114
234
  )
115
235
  .join('')}</tbody></table>`,
116
- )
117
- return parts.join('\n')
236
+ '<h2 id="envelope">The error envelope</h2>',
237
+ `<p>With ${code('-o json')}, or whenever stderr is not a terminal, the last line of stderr is a JSON ` +
238
+ 'object naming the kind, so a script can tell a missing page from a refused one without reading the message.</p>',
239
+ `<pre><code>${escapeHtml(
240
+ JSON.stringify(
241
+ {
242
+ error: {
243
+ kind: 'not_found',
244
+ message: 'There is no page at /about/team.',
245
+ hint: "Check the page's path or key.",
246
+ exit_code: 5,
247
+ },
248
+ },
249
+ null,
250
+ 2,
251
+ ),
252
+ )}</code></pre>`,
253
+ `<p>${code('hint')}, ${code('details')} and ${code('retryable')} appear only when there is something to say.</p>`,
254
+ ].join('\n')
118
255
  }
@@ -1,7 +1,17 @@
1
- /** The content types a site declares, which say which components are templates. */
1
+ /** The content types and forms a site declares, which say which components are templates. */
2
2
  import { existsSync } from 'node:fs'
3
3
  import type { CromboloConfig } from '@crombolo/server'
4
4
  import { loadTypes } from './types.ts'
5
5
 
6
6
  export const declaredTypes = (config: CromboloConfig) =>
7
7
  existsSync(config.schemaDir) ? loadTypes(config).set.contentTypes : []
8
+
9
+ /** The forms whose Send email workflow names `alias` as its e-mail template. */
10
+ export const formsNamingEmailTemplate = (config: CromboloConfig, alias: string) =>
11
+ existsSync(config.schemaDir)
12
+ ? (loadTypes(config).set.forms ?? []).filter((form) =>
13
+ form.workflows.some(
14
+ (workflow) => workflow.type === 'sendEmail' && workflow.settings.template === alias,
15
+ ),
16
+ )
17
+ : []
@@ -4,7 +4,7 @@
4
4
  * confirmation refuses at once, naming the flag that answers it, and never
5
5
  * reads stdin.
6
6
  */
7
- import type { CromboloConfig } from '@crombolo/server'
7
+ import { type CromboloConfig, setting } from '@crombolo/server'
8
8
  import { CliError } from '../errors.ts'
9
9
  import * as ops from '../operations/commands.ts'
10
10
 
@@ -83,7 +83,7 @@ export async function passphraseToOpen(
83
83
  target: string,
84
84
  config: CromboloConfig,
85
85
  ): Promise<string | undefined> {
86
- const given = process.env[PASSPHRASE_ENV]
86
+ const given = setting(PASSPHRASE_ENV)
87
87
  if (given) return given
88
88
  let sealed = false
89
89
  try {
@@ -100,14 +100,14 @@ export async function passphraseToOpen(
100
100
  /** The passphrase for the people a file carries: the environment's, asked for, or none. */
101
101
  export async function passphraseForPeople(): Promise<string | undefined> {
102
102
  return (
103
- process.env[PASSPHRASE_ENV] ??
103
+ setting(PASSPHRASE_ENV) ??
104
104
  (interactive() ? await readSecret(' Passphrase for its people: ') : undefined)
105
105
  )
106
106
  }
107
107
 
108
108
  /** A new passphrase to seal an export with: the environment's, or asked for twice. */
109
109
  export async function passphraseToSeal(): Promise<string> {
110
- const given = process.env[PASSPHRASE_ENV]
110
+ const given = setting(PASSPHRASE_ENV)
111
111
  if (given) {
112
112
  if (given.length < MIN_PASSPHRASE)
113
113
  throw new CliError('usage', `${PASSPHRASE_ENV} is shorter than ${MIN_PASSPHRASE} characters.`)
@@ -14,7 +14,7 @@ import {
14
14
  describeRefFailure,
15
15
  resolveNodeRef,
16
16
  } from '@crombolo/data'
17
- import { bootstrapDatabase } from '@crombolo/server'
17
+ import { bootstrapDatabase, createContentNotifier, resolveEmailPort } from '@crombolo/server'
18
18
  import { Args, Flags } from '@oclif/core'
19
19
  import { CliError } from '../errors.ts'
20
20
  import { many } from './flags.ts'
@@ -124,12 +124,19 @@ export async function publishPages(
124
124
  )
125
125
  }
126
126
 
127
+ // The people who asked to hear of a publish here hear of this one too (story 045).
128
+ const notifier = createContentNotifier(db, { canSend: Boolean(resolveEmailPort(config)) })
127
129
  for (const node of targets) {
128
130
  const result = publishing
129
131
  ? await docs.publish(node.key, cultures.length > 0 ? cultures : null)
130
132
  : await docs.unpublish(node.key, cultures.length > 0 ? cultures : null)
131
133
  items.push({ key: node.key, name: node.text ?? node.key, changed: Boolean(result) })
132
134
  if (!result) continue
135
+ if (publishing)
136
+ await notifier.raise(
137
+ { actions: ['Umb.Document.Publish'], key: node.key, keyKind: 'document' },
138
+ { key: null, name: 'The command line' },
139
+ )
133
140
  io.say(` ${publishing ? 'published' : 'unpublished'} ${node.text ?? node.key}`)
134
141
  }
135
142
  io.say(
@@ -23,6 +23,7 @@ import {
23
23
  mediaStoreFor,
24
24
  placeBlobs,
25
25
  type ServerHandle,
26
+ setting,
26
27
  } from '@crombolo/server'
27
28
  import { importBundle } from '@crombolo/transfer'
28
29
  import { CliError } from '../errors.ts'
@@ -199,7 +200,7 @@ export async function serveSite(io: Io, options: StartOptions): Promise<Started>
199
200
  * platform's restart policy and alerts take over. docs/operations.md.
200
201
  */
201
202
  async function bootWithRetries(io: Io, config: CromboloConfig, options: StartOptions) {
202
- const attempts = Math.max(1, Number(Bun.env.CROMBOLO_BOOT_ATTEMPTS ?? 10) || 10)
203
+ const attempts = Math.max(1, Number(setting('CROMBOLO_BOOT_ATTEMPTS') ?? 10) || 10)
203
204
  const delays = bootRetryDelays(attempts)
204
205
  let failure: BootFailure | undefined
205
206
  let progress = { attempt: 1, attempts, retryInSeconds: undefined as number | undefined }
package/src/templates.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
18
18
  import { join, relative } from 'node:path'
19
19
  import { siteMediaTypeFiles } from '@crombolo/schema'
20
- import { VERSION } from '@crombolo/server'
20
+ import { setting, VERSION } from '@crombolo/server'
21
21
 
22
22
  /**
23
23
  * `templates/`, beside `src/` and `bin/` in the published package.
@@ -25,7 +25,7 @@ import { VERSION } from '@crombolo/server'
25
25
  * without writing into the package that other runs read.
26
26
  */
27
27
  export const TEMPLATES_DIR =
28
- process.env.CROMBOLO_TEMPLATES_DIR || join(import.meta.dir, '..', 'templates')
28
+ setting('CROMBOLO_TEMPLATES_DIR') || join(import.meta.dir, '..', 'templates')
29
29
 
30
30
  /**
31
31
  * A bundle whose server half a template wires into the site it scaffolds: the
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": ">=0.16.0"
9
+ "@crombolo/crombolo": ">=0.18.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "basic",
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": ">=0.16.0"
9
+ "@crombolo/crombolo": ">=0.18.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "demo-harbourstone",
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": ">=0.16.0"
9
+ "@crombolo/crombolo": ">=0.18.0"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "starter",