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 +38 -6
- package/dist/lib/features.d.ts +21 -19
- package/dist/lib/features.d.ts.map +1 -1
- package/dist/lib/features.js +26 -20
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +11 -1
- package/package.json +1 -1
- package/templates/default/README.md.tmpl +12 -5
- package/templates/default/apps/admin/src/plugins.spec.ts +1 -0
- package/templates/default/apps/admin/src/plugins.ts +4 -0
- package/templates/default/apps/admin/tsconfig.json +2 -1
- package/templates/default/apps/server/config/copilot-anthropic.ts +22 -0
- package/templates/default/apps/server/config/copilot-openai.ts +21 -0
- package/templates/default/apps/server/config/copilot.ts +61 -0
- package/templates/default/apps/server/config/docs.ts +14 -0
- package/templates/default/apps/server/config/i18n.ts +16 -0
- package/templates/default/apps/server/config/identity.ts +96 -0
- package/templates/default/apps/server/config/mcp.ts +25 -0
- package/templates/default/apps/server/config/media-storage.ts +83 -0
- package/templates/default/apps/server/config/media.ts +67 -0
- package/templates/default/apps/server/config/sso-oidc.ts +34 -0
- package/templates/default/apps/server/ortha.config.ts +55 -449
- package/templates/default/apps/server/src/plugins.spec.ts +1 -0
- package/templates/default/apps/server/src/plugins.ts +16 -6
- package/templates/default/apps/server/tsconfig.spec.json +25 -0
- package/templates/default/env.tmpl +7 -2
- package/templates/default/tsconfig.json +3 -0
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
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
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
|
-
...(
|
|
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
|
-
?
|
|
201
|
-
: '
|
|
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)]
|
package/dist/lib/features.d.ts
CHANGED
|
@@ -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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
46
|
-
* the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
*
|
|
145
|
-
*
|
|
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
|
|
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"}
|
package/dist/lib/features.js
CHANGED
|
@@ -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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
-
*
|
|
225
|
-
*
|
|
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,
|
|
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"}
|
package/dist/lib/template.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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`.
|
|
149
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
|
@@ -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.
|
|
@@ -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
|