create-ortha-app 0.4.2 → 0.4.3

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/dist/cli.js CHANGED
@@ -49,6 +49,36 @@ function idList(raw) {
49
49
  .map((id) => id.trim())
50
50
  .filter(Boolean);
51
51
  }
52
+ /**
53
+ * The ids a `--media` / `--copilot` / `--sso` / `--protocols` flag named, checked
54
+ * against the group it belongs to.
55
+ *
56
+ * An id nobody recognises has to stop the run. The flags take *feature ids*
57
+ * (`media-s3`), not the label or the bare adapter name, and `--media s3` is the
58
+ * natural guess — it used to select nothing, and "nothing" is a real answer for
59
+ * three of the four groups, so it scaffolded an app quietly missing whatever was
60
+ * asked for. For storage that app cannot even compile: no adapter means no
61
+ * `storage` setting and no `mediaStorage()` for the config to call. Naming the
62
+ * valid ids here costs a typo nothing and turns a broken app into one line of
63
+ * output.
64
+ *
65
+ * `required` marks a single-choice group that must end up with something —
66
+ * storage, where "no adapter" is not a configuration an app can run.
67
+ */
68
+ function selected(argv, name, group, required = false) {
69
+ const chosen = idList(option(argv, name)) ?? defaultsOf(group);
70
+ const known = new Set(group.map((feature) => feature.id));
71
+ const unknown = chosen.filter((id) => !known.has(id));
72
+ if (unknown.length > 0) {
73
+ throw new Error(`--${name}: no such option ${unknown.map((id) => `"${id}"`).join(', ')}. ` +
74
+ `Expected ${[...known].join(', ')}${required ? '' : ', or none'}.`);
75
+ }
76
+ if (required && chosen.length === 0) {
77
+ throw new Error(`--${name}: an app needs one of ${[...known].join(', ')} — ` +
78
+ 'there is no configuration for "no adapter at all".');
79
+ }
80
+ return chosen;
81
+ }
52
82
  /**
53
83
  * This package's own version, which every `@orthacms/*` dependency in the
54
84
  * generated app is pinned to.
@@ -132,12 +162,14 @@ async function askText(label, fallback, validate) {
132
162
  async function resolveAnswers(argv, defaultName) {
133
163
  const asked = !flag(argv, 'yes') && ui.interactive();
134
164
  const defaultDatabaseUrl = `postgresql://ortha:ortha@localhost:5432/${defaultName.replace(/[^a-z0-9_]/gi, '_')}`;
135
- const media = idList(option(argv, 'media')) ?? defaultsOf(features_1.MEDIA_PROVIDERS);
136
- const copilot = idList(option(argv, 'copilot')) ?? defaultsOf(features_1.COPILOT_PROVIDERS);
137
- const sso = idList(option(argv, 'sso')) ?? defaultsOf(features_1.SSO_PROVIDERS);
165
+ // Storage is `required`: it is a single choice, and an app with no adapter
166
+ // has no `storage` setting for `plugins.ts` to hand its factory.
167
+ const media = selected(argv, 'media', features_1.MEDIA_PROVIDERS, true);
168
+ const copilot = selected(argv, 'copilot', features_1.COPILOT_PROVIDERS);
169
+ const sso = selected(argv, 'sso', features_1.SSO_PROVIDERS);
138
170
  const protocols = [
139
171
  ...lockedOf(features_1.PROTOCOLS),
140
- ...(idList(option(argv, 'protocols')) ?? defaultsOf(features_1.PROTOCOLS))
172
+ ...selected(argv, 'protocols', features_1.PROTOCOLS)
141
173
  ];
142
174
  if (!asked) {
143
175
  return {
@@ -197,8 +229,8 @@ function describeSelection(selection) {
197
229
  [
198
230
  'Copilot',
199
231
  providers.length > 0
200
- ? `${providers.map((p) => p.label).join(', ')} + offline fake`
201
- : 'offline fake only'
232
+ ? providers.map((p) => p.label).join(', ')
233
+ : ui.dim('no backend')
202
234
  ],
203
235
  ['Sign-in', labelsFor(features_1.SSO_PROVIDERS)],
204
236
  ['Ortha packages', String((0, features_1.resolvePackages)(selection).length + 1)]
@@ -39,15 +39,18 @@ export interface Feature {
39
39
  /**
40
40
  * The packages every app gets, whatever it opts into.
41
41
  *
42
- * The **copilot** is here — plugin, admin panel and the offline `fake` adapter
43
- * even though it is a large feature nobody may want. Two reasons. Its server
44
- * half arrives anyway: five core plugins (`content`, `activity`, `i18n`,
45
- * `media`, `users`) depend on `copilot-server` to contribute their tools, so
46
- * the code is on disk whatever the manifest says, and leaving it undeclared
47
- * bought nothing but a missing chat panel. And `fake` needs no key and no
48
- * network, so a default app gets a copilot that genuinely works offline —
49
- * while `COPILOT_ENABLED` stays `false`, so nothing reaches a model until an
50
- * operator says so.
42
+ * The **copilot** is here — plugin and admin panel even though it is a large
43
+ * feature nobody may want, because its server half arrives anyway: five core
44
+ * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
45
+ * `copilot-server` to contribute their tools, so the code is on disk whatever
46
+ * the manifest says, and leaving it undeclared bought nothing but a missing chat
47
+ * panel. No **model backend** comes with it: those are the opt-in
48
+ * `COPILOT_PROVIDERS` below, and an app that picks none has the plugin
49
+ * installed and nothing registered. `COPILOT_ENABLED` stays `false`, which
50
+ * unregisters the copilot's routes, so a generated app ships with the chat
51
+ * surfaces absent rather than visible and refusing — and turning it on without
52
+ * a configured backend fails at boot rather than shipping a chat that cannot
53
+ * answer.
51
54
  *
52
55
  * The **extension points** are here for the same reason — `content-domain`,
53
56
  * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
@@ -64,9 +67,9 @@ export interface Feature {
64
67
  * what makes that resolution something the app owns rather than borrows.
65
68
  * `identity-provider-fake` is the scripted identity provider: it needs no
66
69
  * tenant and no network, so it is how a generated app's sign-in page can be
67
- * exercised offline, exactly as `copilot-provider-fake` is for the chat. Note
68
- * that shipping it installs nothing: an adapter only does something once the
69
- * composition root registers it, and the template registers none.
70
+ * exercised offline. Note that shipping it installs nothing: an adapter only
71
+ * does something once the composition root registers it, and the template
72
+ * registers none.
70
73
  *
71
74
  * `design-system`, `utils-admin` and `utils-server` are here even though the
72
75
  * template's own files barely touch them: they are the first things anyone
@@ -112,11 +115,10 @@ export declare const MEDIA_PROVIDERS: readonly Feature[];
112
115
  * §10 says is an operator's decision to make explicitly. Pick nothing and the
113
116
  * copilot is not registered at all.
114
117
  *
115
- * `copilot-provider-fake` is not offered here it is installed automatically
116
- * whenever the copilot is on. It is a shipped adapter rather than test
117
- * scaffolding (ADR-0004 §3): it needs no key and no network, so it is what
118
- * makes the chat work offline, and it is registered last so it is the default
119
- * only when it is the only one.
118
+ * There is no offline stand-in to fall back on. The scripted `fake` adapter is
119
+ * a private test fixture of the CMS repo, not a published package, so an app
120
+ * that picks nothing here has the copilot plugin installed with no backend
121
+ * registered and `COPILOT_ENABLED` must stay `false` until one is.
120
122
  */
121
123
  export declare const COPILOT_PROVIDERS: readonly Feature[];
122
124
  /**
@@ -141,8 +143,8 @@ export declare const COPILOT_PROVIDERS: readonly Feature[];
141
143
  * second reason not to install them for an app that will never speak them.
142
144
  *
143
145
  * `identity-provider-fake` is not offered: it is installed unconditionally,
144
- * like `copilot-provider-fake`, because it needs no tenant and no network and
145
- * is how a generated app's sign-in page is exercised offline. Installing it
146
+ * because it needs no tenant and no network and is how a generated app's
147
+ * sign-in page is exercised offline. Installing it
146
148
  * registers nothing — an adapter only does something once the composition root
147
149
  * names it, and the template names none.
148
150
  */
@@ -1 +1 @@
1
- {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/lib/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACpB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAqC1C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAGhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,OAAO,EAyC7C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,OAAO,EAiB/C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAyB3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EA0BvC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAK1C,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,EAAE,CASrE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAErE"}
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/lib/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACpB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAyC1C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAGhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,OAAO,EAyC7C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,OAAO,EAiB/C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAyB3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EA0BvC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAK1C,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,EAAE,CASrE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAErE"}
@@ -21,15 +21,18 @@ exports.resolveFlags = resolveFlags;
21
21
  /**
22
22
  * The packages every app gets, whatever it opts into.
23
23
  *
24
- * The **copilot** is here — plugin, admin panel and the offline `fake` adapter
25
- * even though it is a large feature nobody may want. Two reasons. Its server
26
- * half arrives anyway: five core plugins (`content`, `activity`, `i18n`,
27
- * `media`, `users`) depend on `copilot-server` to contribute their tools, so
28
- * the code is on disk whatever the manifest says, and leaving it undeclared
29
- * bought nothing but a missing chat panel. And `fake` needs no key and no
30
- * network, so a default app gets a copilot that genuinely works offline —
31
- * while `COPILOT_ENABLED` stays `false`, so nothing reaches a model until an
32
- * operator says so.
24
+ * The **copilot** is here — plugin and admin panel even though it is a large
25
+ * feature nobody may want, because its server half arrives anyway: five core
26
+ * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
27
+ * `copilot-server` to contribute their tools, so the code is on disk whatever
28
+ * the manifest says, and leaving it undeclared bought nothing but a missing chat
29
+ * panel. No **model backend** comes with it: those are the opt-in
30
+ * `COPILOT_PROVIDERS` below, and an app that picks none has the plugin
31
+ * installed and nothing registered. `COPILOT_ENABLED` stays `false`, which
32
+ * unregisters the copilot's routes, so a generated app ships with the chat
33
+ * surfaces absent rather than visible and refusing — and turning it on without
34
+ * a configured backend fails at boot rather than shipping a chat that cannot
35
+ * answer.
33
36
  *
34
37
  * The **extension points** are here for the same reason — `content-domain`,
35
38
  * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
@@ -46,9 +49,9 @@ exports.resolveFlags = resolveFlags;
46
49
  * what makes that resolution something the app owns rather than borrows.
47
50
  * `identity-provider-fake` is the scripted identity provider: it needs no
48
51
  * tenant and no network, so it is how a generated app's sign-in page can be
49
- * exercised offline, exactly as `copilot-provider-fake` is for the chat. Note
50
- * that shipping it installs nothing: an adapter only does something once the
51
- * composition root registers it, and the template registers none.
52
+ * exercised offline. Note that shipping it installs nothing: an adapter only
53
+ * does something once the composition root registers it, and the template
54
+ * registers none.
52
55
  *
53
56
  * `design-system`, `utils-admin` and `utils-server` are here even though the
54
57
  * template's own files barely touch them: they are the first things anyone
@@ -60,6 +63,8 @@ exports.resolveFlags = resolveFlags;
60
63
  exports.CORE_PACKAGES = [
61
64
  '@orthacms/activity-admin',
62
65
  '@orthacms/activity-server',
66
+ '@orthacms/alarms-admin',
67
+ '@orthacms/alarms-server',
63
68
  '@orthacms/api-tokens-admin',
64
69
  '@orthacms/bootstrap-admin',
65
70
  '@orthacms/bootstrap-server',
@@ -68,7 +73,6 @@ exports.CORE_PACKAGES = [
68
73
  '@orthacms/content-server',
69
74
  '@orthacms/copilot-admin',
70
75
  '@orthacms/copilot-domain',
71
- '@orthacms/copilot-provider-fake',
72
76
  '@orthacms/copilot-server',
73
77
  '@orthacms/database',
74
78
  '@orthacms/design-system',
@@ -82,6 +86,9 @@ exports.CORE_PACKAGES = [
82
86
  '@orthacms/media-admin',
83
87
  '@orthacms/media-server',
84
88
  '@orthacms/query-builder-admin',
89
+ '@orthacms/segments-admin',
90
+ '@orthacms/segments-domain',
91
+ '@orthacms/segments-server',
85
92
  '@orthacms/shell-admin',
86
93
  '@orthacms/tools-server',
87
94
  '@orthacms/transfer-admin',
@@ -175,11 +182,10 @@ exports.MEDIA_PROVIDERS = [
175
182
  * §10 says is an operator's decision to make explicitly. Pick nothing and the
176
183
  * copilot is not registered at all.
177
184
  *
178
- * `copilot-provider-fake` is not offered here it is installed automatically
179
- * whenever the copilot is on. It is a shipped adapter rather than test
180
- * scaffolding (ADR-0004 §3): it needs no key and no network, so it is what
181
- * makes the chat work offline, and it is registered last so it is the default
182
- * only when it is the only one.
185
+ * There is no offline stand-in to fall back on. The scripted `fake` adapter is
186
+ * a private test fixture of the CMS repo, not a published package, so an app
187
+ * that picks nothing here has the copilot plugin installed with no backend
188
+ * registered and `COPILOT_ENABLED` must stay `false` until one is.
183
189
  */
184
190
  exports.COPILOT_PROVIDERS = [
185
191
  {
@@ -221,8 +227,8 @@ exports.COPILOT_PROVIDERS = [
221
227
  * second reason not to install them for an app that will never speak them.
222
228
  *
223
229
  * `identity-provider-fake` is not offered: it is installed unconditionally,
224
- * like `copilot-provider-fake`, because it needs no tenant and no network and
225
- * is how a generated app's sign-in page is exercised offline. Installing it
230
+ * because it needs no tenant and no network and is how a generated app's
231
+ * sign-in page is exercised offline. Installing it
226
232
  * registers nothing — an adapter only does something once the composition root
227
233
  * names it, and the template names none.
228
234
  */
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/lib/template.ts"],"names":[],"mappings":"AAWA,OAAO,EAIH,KAAK,gBAAgB,EACxB,MAAM,YAAY,CAAC;AAEpB,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AAoBD,yDAAyD;AACzD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAevE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACvB,MAAM,CAmBR;AAuBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,IAAI,CAiCN;AAED,+CAA+C;AAC/C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMxD"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/lib/template.ts"],"names":[],"mappings":"AAWA,OAAO,EAIH,KAAK,gBAAgB,EACxB,MAAM,YAAY,CAAC;AAEpB,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AAoBD,yDAAyD;AACzD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAevE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,GACvB,MAAM,CAmBR;AAuBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,IAAI,CA0CN;AAED,+CAA+C;AAC/C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMxD"}
@@ -104,7 +104,17 @@ function renderTemplate(templateDir, target, values) {
104
104
  (0, node_fs_1.writeFileSync)(destination, renderManifest(raw, values));
105
105
  continue;
106
106
  }
107
- (0, node_fs_1.writeFileSync)(destination, render((0, conditionals_1.applyConditionals)(raw, flags, file), values));
107
+ const contents = (0, conditionals_1.applyConditionals)(raw, flags, file);
108
+ // A file that conditioned *itself* away is not written at all. This is
109
+ // what lets one module per optional plugin live under `config/`: wrap
110
+ // the whole of `config/mcp.ts` in `ortha:if mcp` and an app generated
111
+ // without MCP has no such file, rather than an empty one whose only
112
+ // job is to explain why it is empty. Guarded on the source having had
113
+ // content, so a template file that is deliberately blank still ships.
114
+ if (raw.trim() !== '' && contents.trim() === '') {
115
+ continue;
116
+ }
117
+ (0, node_fs_1.writeFileSync)(destination, render(contents, values));
108
118
  }
109
119
  }
110
120
  /** Whether `dir` exists and holds anything. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ortha-app",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Create an Ortha CMS app: npx create-ortha-app my-cms",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/create-ortha-app",
@@ -145,8 +145,14 @@ talking to itself.
145
145
  ### AI copilot
146
146
 
147
147
  Installed, and **off** until you turn it on: set `COPILOT_ENABLED=true` in
148
- `.env`. Enabling a hosted backend sends workspace content to a third party, so
149
- it is an explicit decision rather than a default.
148
+ `.env`. Off means off with it `false` the app registers no `/api/copilot`
149
+ route, so those endpoints 404 and the admin's chat panel, Agents view and
150
+ Skills page are not there. Whether this app offers an AI assistant at all is a
151
+ decision rather than a default.
152
+
153
+ It is not the switch that keeps your content in-house, though. A backend is
154
+ registered only when its settings are present (below), so an app with no key
155
+ reaches no third party either way.
150
156
 
151
157
  Backends are registered in `src/server/plugins.ts`, and **the order is the
152
158
  setting** — there is no `defaultProvider`. The first registered backend serves a
@@ -154,9 +160,10 @@ run that names none, and it is what the admin's model picker opens on. A backend
154
160
  is only registered when its connection settings are present, so an unconfigured
155
161
  one never appears as an option that fails on the first message.
156
162
 
157
- The bundled `fake` provider needs no key and no network, and is registered last
158
- so with nothing configured the chat still works, and it is the default only
159
- because it is the only one.
163
+ There is no offline stand-in. Configure no backend and the copilot has nothing
164
+ to call, so leave `COPILOT_ENABLED=false` until you have one turning it on
165
+ with an empty provider list refuses to boot rather than shipping a chat that
166
+ cannot answer.
160
167
  <!-- ortha:if graphql -->
161
168
  ### GraphQL
162
169
 
@@ -31,6 +31,7 @@ const EXPECTED_PLUGINS = [
31
31
  'i18n',
32
32
  'wysiwyg',
33
33
  'media',
34
+ 'alarms',
34
35
  'copilot',
35
36
  'users',
36
37
  'activity',
@@ -11,6 +11,7 @@ import { UsersPlugin } from '@orthacms/users-admin';
11
11
  import { WorkspacesPlugin } from '@orthacms/workspaces-admin';
12
12
  import { WysiwygPlugin } from '@orthacms/wysiwyg-admin';
13
13
  import { CopilotPlugin } from '@orthacms/copilot-admin';
14
+ import { AlarmsPlugin } from '@orthacms/alarms-admin';
14
15
 
15
16
  /**
16
17
  * The admin's composition, mirroring `src/server/plugins.ts` on the UI side.
@@ -46,6 +47,9 @@ export function buildPlugins(): AdminPlugin[] {
46
47
  I18nPlugin(),
47
48
  WysiwygPlugin(),
48
49
  MediaPlugin(),
50
+ // Another Content Library slot filler — the entry rail's checks block,
51
+ // an optional records column, and "Save as rule" in the toolbar.
52
+ AlarmsPlugin(),
49
53
  // The docked chat panel plus the full-page Agents view. Belongs with
50
54
  // the workspace-interior features: the panel mounts into the workspace
51
55
  // shell's sidebar footer.
@@ -11,7 +11,8 @@
11
11
  "jsx": "react-jsx",
12
12
  "types": [
13
13
  "node",
14
- "vite/client"
14
+ "vite/client",
15
+ "vitest/globals"
15
16
  ],
16
17
  "strict": true,
17
18
  "skipLibCheck": true,
@@ -0,0 +1,22 @@
1
+ // ortha:if copilot-anthropic
2
+ import type { AnthropicProviderConfig } from '@orthacms/copilot-provider-anthropic';
3
+ import { readEnv, readList } from '@orthacms/utils-server';
4
+
5
+ /**
6
+ * Native Claude, or nothing.
7
+ *
8
+ * Setting the key is what REGISTERS this backend — leave it empty and there is
9
+ * no `claude` in the picker at all, rather than one that fails on the first
10
+ * message.
11
+ */
12
+ export function anthropicProvider(): AnthropicProviderConfig | undefined {
13
+ const apiKey = readEnv('ANTHROPIC_API_KEY');
14
+ if (!apiKey) {
15
+ return undefined;
16
+ }
17
+ return {
18
+ apiKey,
19
+ models: readList('COPILOT_ANTHROPIC_MODELS', 'claude-sonnet-5')
20
+ };
21
+ }
22
+ // ortha:end
@@ -0,0 +1,21 @@
1
+ // ortha:if copilot-openai
2
+ import type { OpenAiProviderConfig } from '@orthacms/copilot-provider-openai';
3
+ import { readEnv, readList } from '@orthacms/utils-server';
4
+
5
+ /**
6
+ * An OpenAI-wire backend, or nothing. No default endpoint: an unset variable
7
+ * means "this deployment has no such backend", not "assume one is running on
8
+ * this laptop".
9
+ */
10
+ export function openAiProvider(): OpenAiProviderConfig | undefined {
11
+ const baseUrl = readEnv('COPILOT_OPENAI_BASE_URL');
12
+ if (!baseUrl) {
13
+ return undefined;
14
+ }
15
+ return {
16
+ baseUrl,
17
+ apiKey: process.env['COPILOT_OPENAI_API_KEY'] ?? '',
18
+ models: readList('COPILOT_OPENAI_MODELS', 'llama3.1')
19
+ };
20
+ }
21
+ // ortha:end
@@ -0,0 +1,61 @@
1
+ /** The copilot kill switch and the model backends it can reach. */
2
+ import type { CopilotPluginConfig } from '@orthacms/copilot-server';
3
+ // ortha:if copilot-anthropic
4
+ import type { AnthropicProviderConfig } from '@orthacms/copilot-provider-anthropic';
5
+ // ortha:end
6
+ // ortha:if copilot-openai
7
+ import type { OpenAiProviderConfig } from '@orthacms/copilot-provider-openai';
8
+ // ortha:end
9
+ import { defined, readFlag, readPositiveInt } from '@orthacms/utils-server';
10
+
11
+ // ortha:if copilot-anthropic
12
+ import { anthropicProvider } from './copilot-anthropic';
13
+ // ortha:end
14
+ // ortha:if copilot-openai
15
+ import { openAiProvider } from './copilot-openai';
16
+ // ortha:end
17
+
18
+ /**
19
+ * Copilot settings plus the backends this deployment can reach.
20
+ *
21
+ * The provider settings live **here**, not inside `CopilotPluginConfig`: the
22
+ * plugin is adapter-agnostic by decision, so it names no provider kind. A key
23
+ * is present only when the deployment configured that backend, and `plugins.ts`
24
+ * registers exactly the ones that are — "configured" is a fact this file can
25
+ * read, where a `defaultProvider` naming one of them could be misspelled or
26
+ * point at a backend nobody registered.
27
+ */
28
+ export interface AppCopilotConfig extends CopilotPluginConfig {
29
+ providers: {
30
+ // ortha:if copilot-anthropic
31
+ /** Native Claude. Present when ANTHROPIC_API_KEY is set. */
32
+ claude?: AnthropicProviderConfig;
33
+ // ortha:end
34
+ // ortha:if copilot-openai
35
+ /**
36
+ * An OpenAI-wire endpoint — Ollama, vLLM, LiteLLM, Azure or OpenAI.
37
+ * Present when COPILOT_OPENAI_BASE_URL is set: an endpoint nobody
38
+ * named is a backend that can only time out.
39
+ */
40
+ openai?: OpenAiProviderConfig;
41
+ // ortha:end
42
+ };
43
+ }
44
+
45
+ /** The copilot kill switch and the model backends it can reach. */
46
+ export function copilotConfig(): AppCopilotConfig {
47
+ return {
48
+ // Off by default: enabling a hosted provider sends workspace content to
49
+ // a third party, which is an operator's decision to make explicitly.
50
+ enabled: readFlag('COPILOT_ENABLED', false),
51
+ maxOutputTokens: readPositiveInt('COPILOT_MAX_OUTPUT_TOKENS', 8192),
52
+ providers: defined({
53
+ // ortha:if copilot-anthropic
54
+ claude: anthropicProvider(),
55
+ // ortha:end
56
+ // ortha:if copilot-openai
57
+ openai: openAiProvider()
58
+ // ortha:end
59
+ })
60
+ };
61
+ }
@@ -0,0 +1,14 @@
1
+ /** The OpenAPI document and the API reference it is served as. */
2
+ import type { ApiDocsOptions } from '@orthacms/bootstrap-server';
3
+ import { isProduction, readFlag } from '@orthacms/utils-server';
4
+
5
+ /** The OpenAPI document and the API reference it is served as. */
6
+ export function docsConfig(): ApiDocsOptions {
7
+ return {
8
+ // On outside production, where the reference is a development tool.
9
+ // `API_DOCS=true` publishes it from a deployed instance.
10
+ enabled: readFlag('API_DOCS', !isProduction()),
11
+ title: '__APP_TITLE__ API',
12
+ version: '1.0.0'
13
+ };
14
+ }
@@ -0,0 +1,16 @@
1
+ /** The content locales, and what to do about rows left in a removed one. */
2
+ import type { I18nPluginConfig } from '@orthacms/i18n-server';
3
+
4
+ /** The content locales, and what to do about rows left in a removed one. */
5
+ export function i18nConfig(): I18nPluginConfig {
6
+ return {
7
+ // Content locales. Stable product configuration, hence literals. The
8
+ // slugs are stored on entry rows, so removing one hides its rows rather
9
+ // than deleting them — which is what `orphanedLocales` is about below.
10
+ locales: [{ slug: 'en', name: 'English', isDefault: true }],
11
+ // Rows in a locale no longer listed above are intact and unreachable,
12
+ // the worst shape for a silent failure. Fail the boot and put the
13
+ // choice in front of whoever edited the array.
14
+ orphanedLocales: 'fail'
15
+ };
16
+ }
@@ -0,0 +1,96 @@
1
+ /** Identity — sessions, tokens, the SSO handshake, and the first admin. */
2
+ import type { IdentityPluginConfig } from '@orthacms/identity-server';
3
+ // ortha:if sso-oidc
4
+ import type { OidcProviderConfig } from '@orthacms/identity-provider-oidc';
5
+ // ortha:end
6
+ import {
7
+ defined,
8
+ isProduction,
9
+ readEnv,
10
+ readList,
11
+ readPositiveInt
12
+ } from '@orthacms/utils-server';
13
+
14
+ // ortha:if sso-oidc
15
+ import { oidcProvider } from './sso-oidc';
16
+ // ortha:end
17
+
18
+ /**
19
+ * Identity settings, plus the identity providers this app can reach.
20
+ *
21
+ * The provider settings live here rather than inside `IdentityPluginConfig`,
22
+ * for the same reason the copilot's backends do: the plugin names no protocol,
23
+ * and this file is the one place that reads the environment. The constructed
24
+ * adapters are registered in `src/plugins.ts`.
25
+ */
26
+ export interface AppIdentityConfig extends IdentityPluginConfig {
27
+ /**
28
+ * Identity providers, keyed by the name they are registered under. That
29
+ * name appears in the sign-in URL and in every `sso_identities` row, so
30
+ * renaming one orphans the links that name it.
31
+ *
32
+ * Optional, and absent unless this app was generated with single sign-on.
33
+ */
34
+ ssoProviders?: {
35
+ // ortha:if sso-oidc
36
+ /** A generic OpenID Connect provider. Present when both env vars are set. */
37
+ oidc?: OidcProviderConfig & { name: string };
38
+ // ortha:end
39
+ };
40
+ }
41
+
42
+ /** Identity — sessions, tokens, the SSO handshake, and the first admin. */
43
+ export function identityConfig(): AppIdentityConfig {
44
+ return {
45
+ // Origins allowed to make state-changing calls (login-CSRF defence).
46
+ // In development that is the Vite dev server; in production the app is
47
+ // same-origin, so this list is what a separately-hosted admin would
48
+ // need adding to.
49
+ allowedOrigins: readList(
50
+ 'ALLOWED_ORIGINS',
51
+ `http://localhost:${readPositiveInt('ADMIN_PORT', 4200)}`
52
+ ),
53
+ session: {
54
+ ttlSeconds: readPositiveInt('SESSION_TTL_SECONDS', 60 * 60 * 24 * 7),
55
+ cookieSecure: isProduction(),
56
+ cookieSameSite: 'lax' as const
57
+ },
58
+ token: {
59
+ inviteTtlSeconds: readPositiveInt(
60
+ 'INVITE_TTL_SECONDS',
61
+ 60 * 60 * 24 * 7
62
+ ),
63
+ resetTtlSeconds: readPositiveInt('RESET_TTL_SECONDS', 60 * 60)
64
+ },
65
+ rateLimit: {
66
+ ttlSeconds: readPositiveInt('LOGIN_RATE_LIMIT_TTL_SECONDS', 60),
67
+ limit: readPositiveInt('LOGIN_RATE_LIMIT', 10)
68
+ },
69
+ sso: ssoConfig(),
70
+ // ortha:if sso-oidc
71
+ ssoProviders: defined({ oidc: oidcProvider() }),
72
+ // ortha:end
73
+ // With an email set, an admin is provisioned on boot — idempotent and
74
+ // non-destructive. This is how you get your first login.
75
+ rootAdmin: {
76
+ email: process.env['ORTHA_ROOT_ADMIN_EMAIL'] ?? '',
77
+ password: process.env['ORTHA_ROOT_ADMIN_PASSWORD'] ?? '',
78
+ name: process.env['ORTHA_ROOT_ADMIN_NAME'] ?? ''
79
+ }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * The shape of the single sign-on handshake. The providers themselves are
85
+ * constructed in `src/plugins.ts`; these settings say how the round trip runs.
86
+ */
87
+ function ssoConfig(): NonNullable<IdentityPluginConfig['sso']> {
88
+ return defined({
89
+ // The origin browsers reach this API on. It builds the redirect_uri
90
+ // you register with each provider, and it is configured rather than
91
+ // read from the request's Host header, which a client controls. Leave
92
+ // it unset when the admin and the API share an origin — the usual case.
93
+ publicBaseUrl: readEnv('SSO_PUBLIC_BASE_URL'),
94
+ requestTtlSeconds: readPositiveInt('SSO_REQUEST_TTL_SECONDS', 600)
95
+ });
96
+ }
@@ -0,0 +1,25 @@
1
+ // ortha:if mcp
2
+ import type { McpPluginConfig } from '@orthacms/mcp-server';
3
+ import { readFlag, readPositiveInt } from '@orthacms/utils-server';
4
+
5
+ /** The MCP front door — off unless an operator turns it on. */
6
+ export function mcpConfig(): McpPluginConfig {
7
+ return {
8
+ // Off by default: once on, any holder of a full-scope API token can
9
+ // drive content CRUD from an external agent.
10
+ enabled: readFlag('MCP_ENABLED', false),
11
+ // The identity MCP clients display in their connector lists.
12
+ name: '__APP_NAME__',
13
+ version: '1.0.0',
14
+ // A request/response transport owes its caller an answer, and the tool
15
+ // registry has no deadline of its own — so without this the only bound
16
+ // on a `tools/call` is the query underneath it, and a blocked pool
17
+ // turns one call into a socket held until the client gives up.
18
+ callTimeoutMs: readPositiveInt('MCP_CALL_TIMEOUT_MS', 30_000),
19
+ // Deliberately generous: the ceiling exists to stop a pathological
20
+ // result being serialised several times over, not to shape normal use.
21
+ // A result this large does not fit a model's context anyway.
22
+ maxResultBytes: readPositiveInt('MCP_MAX_RESULT_BYTES', 4_194_304)
23
+ };
24
+ }
25
+ // ortha:end