create-ortha-app 0.4.2 → 0.5.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/dist/cli.js +6 -267
- package/dist/lib/features.d.ts +42 -20
- package/dist/lib/features.d.ts.map +1 -1
- package/dist/lib/features.js +56 -22
- package/dist/lib/run.d.ts +22 -0
- package/dist/lib/run.d.ts.map +1 -0
- package/dist/lib/run.js +318 -0
- 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 +38 -12
- package/templates/default/apps/admin/src/plugins.spec.ts +5 -1
- package/templates/default/apps/admin/src/plugins.ts +21 -2
- 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 +129 -0
- package/templates/default/apps/server/config/mcp.ts +25 -0
- package/templates/default/apps/server/config/media-storage.ts +84 -0
- package/templates/default/apps/server/config/media.ts +67 -0
- package/templates/default/apps/server/config/sso-github.ts +42 -0
- package/templates/default/apps/server/config/sso-oidc.ts +34 -0
- package/templates/default/apps/server/config/sso-saml.ts +46 -0
- package/templates/default/apps/server/ortha.config.ts +60 -449
- package/templates/default/apps/server/src/plugins.spec.ts +7 -0
- package/templates/default/apps/server/src/plugins.ts +84 -19
- package/templates/default/apps/server/tsconfig.spec.json +25 -0
- package/templates/default/env.tmpl +84 -2
- package/templates/default/tsconfig.json +3 -0
package/dist/cli.js
CHANGED
|
@@ -2,276 +2,15 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const node_crypto_1 = require("node:crypto");
|
|
6
|
-
const node_child_process_1 = require("node:child_process");
|
|
7
|
-
const node_fs_1 = require("node:fs");
|
|
8
|
-
const node_path_1 = require("node:path");
|
|
9
|
-
const promises_1 = require("node:readline/promises");
|
|
10
|
-
const features_1 = require("./lib/features");
|
|
11
|
-
const template_1 = require("./lib/template");
|
|
12
|
-
const ui = tslib_1.__importStar(require("./lib/ui"));
|
|
13
|
-
const validate_1 = require("./lib/validate");
|
|
14
|
-
const USAGE = `create-ortha-app — scaffold an Ortha CMS app
|
|
15
|
-
|
|
16
|
-
Usage: npx create-ortha-app <directory> [options]
|
|
17
|
-
|
|
18
|
-
Options:
|
|
19
|
-
--yes Accept every default, asking nothing
|
|
20
|
-
--media <id> Storage adapter (default: media-local)
|
|
21
|
-
--copilot <ids> Comma-separated copilot providers, or "none"
|
|
22
|
-
--sso <ids> Comma-separated identity providers (sso-oidc), or "none"
|
|
23
|
-
--protocols <ids> Comma-separated protocols beyond REST (graphql, mcp), or "none"
|
|
24
|
-
--no-install Skip installing dependencies
|
|
25
|
-
--no-git Skip initialising a git repository
|
|
26
|
-
-h, --help Show this message
|
|
27
|
-
`;
|
|
28
|
-
/** Whether a bare `--flag` is present. */
|
|
29
|
-
function flag(argv, name) {
|
|
30
|
-
return argv.includes(`--${name}`);
|
|
31
|
-
}
|
|
32
|
-
/** Reads `--name=value` or `--name value` from argv. */
|
|
33
|
-
function option(argv, name) {
|
|
34
|
-
const inline = argv.find((arg) => arg.startsWith(`--${name}=`));
|
|
35
|
-
if (inline)
|
|
36
|
-
return inline.slice(`--${name}=`.length);
|
|
37
|
-
const index = argv.indexOf(`--${name}`);
|
|
38
|
-
const next = index === -1 ? undefined : argv[index + 1];
|
|
39
|
-
return next && !next.startsWith('-') ? next : undefined;
|
|
40
|
-
}
|
|
41
|
-
/** Splits a comma-separated flag value; `none` means an empty selection. */
|
|
42
|
-
function idList(raw) {
|
|
43
|
-
if (raw === undefined)
|
|
44
|
-
return undefined;
|
|
45
|
-
if (raw.trim() === 'none')
|
|
46
|
-
return [];
|
|
47
|
-
return raw
|
|
48
|
-
.split(',')
|
|
49
|
-
.map((id) => id.trim())
|
|
50
|
-
.filter(Boolean);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* This package's own version, which every `@orthacms/*` dependency in the
|
|
54
|
-
* generated app is pinned to.
|
|
55
|
-
*
|
|
56
|
-
* The scaffolder is released in lockstep with the packages it scaffolds, so its
|
|
57
|
-
* version *is* the matching set — which is the whole mechanism keeping a
|
|
58
|
-
* generated app internally consistent. Reading it from the manifest rather than
|
|
59
|
-
* resolving `latest` from the registry also means `npx create-ortha-app@0.3.0`
|
|
60
|
-
* generates a 0.3.0 app, not whatever shipped since.
|
|
61
|
-
*/
|
|
62
|
-
function ownVersion() {
|
|
63
|
-
const manifest = (0, node_path_1.join)(__dirname, '../package.json');
|
|
64
|
-
return JSON.parse((0, node_fs_1.readFileSync)(manifest, 'utf8'))
|
|
65
|
-
.version;
|
|
66
|
-
}
|
|
67
|
-
/** Runs a command in `cwd`, returning whether it succeeded. */
|
|
68
|
-
function runCommand(command, args, cwd) {
|
|
69
|
-
return ((0, node_child_process_1.spawnSync)(command, args, {
|
|
70
|
-
cwd,
|
|
71
|
-
stdio: 'inherit',
|
|
72
|
-
shell: process.platform === 'win32'
|
|
73
|
-
}).status === 0);
|
|
74
|
-
}
|
|
75
|
-
/** Turns a feature into a picker row. */
|
|
76
|
-
function toChoice(feature) {
|
|
77
|
-
return {
|
|
78
|
-
value: feature.id,
|
|
79
|
-
label: feature.label,
|
|
80
|
-
hint: feature.hint,
|
|
81
|
-
selected: feature.enabledByDefault,
|
|
82
|
-
disabled: !feature.available,
|
|
83
|
-
locked: feature.locked
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
5
|
/**
|
|
87
|
-
* The
|
|
6
|
+
* The `create-ortha-app` binary.
|
|
88
7
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
8
|
+
* Nothing but the entry point: the scaffolder lives in `lib/run.ts`, so a test
|
|
9
|
+
* can import it without a module that writes an app the moment it is required.
|
|
91
10
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
feature.available)
|
|
96
|
-
.map((feature) => feature.id);
|
|
97
|
-
}
|
|
98
|
-
/** The ids that are on regardless of what was asked or passed. */
|
|
99
|
-
function lockedOf(features) {
|
|
100
|
-
return features
|
|
101
|
-
.filter((feature) => feature.locked && feature.available)
|
|
102
|
-
.map((feature) => feature.id);
|
|
103
|
-
}
|
|
104
|
-
/** Asks a free-text question, re-asking until it validates. */
|
|
105
|
-
async function askText(label, fallback, validate) {
|
|
106
|
-
const rl = (0, promises_1.createInterface)({
|
|
107
|
-
input: process.stdin,
|
|
108
|
-
output: process.stdout
|
|
109
|
-
});
|
|
110
|
-
try {
|
|
111
|
-
for (;;) {
|
|
112
|
-
const raw = await rl.question(`${ui.cyan('◆')} ${ui.bold(label)} ${ui.dim(`(${fallback})`)} `);
|
|
113
|
-
const answer = raw.trim() || fallback;
|
|
114
|
-
const problem = validate?.(answer);
|
|
115
|
-
if (!problem)
|
|
116
|
-
return answer;
|
|
117
|
-
ui.error(problem);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
finally {
|
|
121
|
-
rl.close();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Resolves the answers, asking only when there is a terminal to ask in.
|
|
126
|
-
*
|
|
127
|
-
* Non-interactive is not an error case to warn about — it is CI, a piped
|
|
128
|
-
* install, and `--yes`. A scaffolder that blocks on a prompt nobody can answer
|
|
129
|
-
* hangs a pipeline until it times out, which is a far worse failure than
|
|
130
|
-
* defaulting, so the flags and the defaults cover every question.
|
|
131
|
-
*/
|
|
132
|
-
async function resolveAnswers(argv, defaultName) {
|
|
133
|
-
const asked = !flag(argv, 'yes') && ui.interactive();
|
|
134
|
-
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);
|
|
138
|
-
const protocols = [
|
|
139
|
-
...lockedOf(features_1.PROTOCOLS),
|
|
140
|
-
...(idList(option(argv, 'protocols')) ?? defaultsOf(features_1.PROTOCOLS))
|
|
141
|
-
];
|
|
142
|
-
if (!asked) {
|
|
143
|
-
return {
|
|
144
|
-
appName: defaultName,
|
|
145
|
-
databaseUrl: defaultDatabaseUrl,
|
|
146
|
-
adminEmail: 'admin@example.com',
|
|
147
|
-
selection: {
|
|
148
|
-
enabled: new Set([...media, ...copilot, ...sso, ...protocols])
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
ui.section('Project');
|
|
153
|
-
const appName = await askText('App name', defaultName, validate_1.validateAppName);
|
|
154
|
-
const databaseUrl = await askText('Database URL', defaultDatabaseUrl, validate_1.validateDatabaseUrl);
|
|
155
|
-
const adminEmail = await askText('Admin email', 'admin@example.com');
|
|
156
|
-
ui.section('Features');
|
|
157
|
-
ui.note('Everything else is installed for you. These are the choices.');
|
|
158
|
-
console.log('');
|
|
159
|
-
// Only ask when there is more than one answer available. S3 is not
|
|
160
|
-
// published, so today this is a question with a single possible reply, and
|
|
161
|
-
// asking it would be noise pretending to be a choice.
|
|
162
|
-
const selectable = features_1.MEDIA_PROVIDERS.filter((provider) => provider.available);
|
|
163
|
-
const chosenMedia = selectable.length > 1
|
|
164
|
-
? [
|
|
165
|
-
(await ui.select('Where should uploads be stored?', features_1.MEDIA_PROVIDERS.map(toChoice))) ?? media[0]
|
|
166
|
-
]
|
|
167
|
-
: media;
|
|
168
|
-
const chosenCopilot = (await ui.multiselect('AI copilot — which model backends?', features_1.COPILOT_PROVIDERS.map(toChoice))) ?? copilot;
|
|
169
|
-
const chosenSso = (await ui.multiselect('Single sign-on — which identity providers?', features_1.SSO_PROVIDERS.map(toChoice))) ?? sso;
|
|
170
|
-
const chosenProtocols = (await ui.multiselect('Which protocols should the content API speak?', features_1.PROTOCOLS.map(toChoice))) ?? protocols;
|
|
171
|
-
return {
|
|
172
|
-
appName,
|
|
173
|
-
databaseUrl,
|
|
174
|
-
adminEmail,
|
|
175
|
-
selection: {
|
|
176
|
-
enabled: new Set([
|
|
177
|
-
...chosenMedia.filter(Boolean),
|
|
178
|
-
...chosenCopilot,
|
|
179
|
-
...chosenSso,
|
|
180
|
-
...chosenProtocols
|
|
181
|
-
])
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
/** Human-readable summary of what was chosen. */
|
|
186
|
-
function describeSelection(selection) {
|
|
187
|
-
const labelsFor = (features) => {
|
|
188
|
-
const chosen = features
|
|
189
|
-
.filter((feature) => selection.enabled.has(feature.id))
|
|
190
|
-
.map((feature) => feature.label);
|
|
191
|
-
return chosen.length > 0 ? chosen.join(', ') : ui.dim('none');
|
|
192
|
-
};
|
|
193
|
-
const providers = features_1.COPILOT_PROVIDERS.filter((provider) => selection.enabled.has(provider.id));
|
|
194
|
-
return [
|
|
195
|
-
['Storage', labelsFor(features_1.MEDIA_PROVIDERS)],
|
|
196
|
-
['Protocols', labelsFor(features_1.PROTOCOLS)],
|
|
197
|
-
[
|
|
198
|
-
'Copilot',
|
|
199
|
-
providers.length > 0
|
|
200
|
-
? `${providers.map((p) => p.label).join(', ')} + offline fake`
|
|
201
|
-
: 'offline fake only'
|
|
202
|
-
],
|
|
203
|
-
['Sign-in', labelsFor(features_1.SSO_PROVIDERS)],
|
|
204
|
-
['Ortha packages', String((0, features_1.resolvePackages)(selection).length + 1)]
|
|
205
|
-
];
|
|
206
|
-
}
|
|
207
|
-
async function main() {
|
|
208
|
-
const argv = process.argv.slice(2);
|
|
209
|
-
if (flag(argv, 'help') || argv.includes('-h')) {
|
|
210
|
-
console.log(USAGE);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const positional = argv.filter((arg) => !arg.startsWith('-'));
|
|
214
|
-
const target = (0, node_path_1.resolve)(positional[0] ?? '.');
|
|
215
|
-
ui.banner('Ortha CMS', `Creating an app in ${target}`);
|
|
216
|
-
if ((0, template_1.isNonEmptyDirectory)(target)) {
|
|
217
|
-
throw new Error(`${target} already exists and is not empty. Pick a new directory, ` +
|
|
218
|
-
`or empty that one first.`);
|
|
219
|
-
}
|
|
220
|
-
const answers = await resolveAnswers(argv, (0, node_path_1.basename)(target));
|
|
221
|
-
// Generated rather than prompted: a password typed at a prompt is echoed to
|
|
222
|
-
// the terminal and lands in shell history. This one is written only to the
|
|
223
|
-
// git-ignored .env, and printed once below.
|
|
224
|
-
const adminPassword = (0, node_crypto_1.randomBytes)(12).toString('base64url');
|
|
225
|
-
ui.summary('Your app', [
|
|
226
|
-
['Name', answers.appName],
|
|
227
|
-
['Database', (0, validate_1.databaseNameFrom)(answers.databaseUrl)],
|
|
228
|
-
...describeSelection(answers.selection)
|
|
229
|
-
]);
|
|
230
|
-
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
231
|
-
(0, template_1.renderTemplate)((0, node_path_1.join)(__dirname, '../templates/default'), target, {
|
|
232
|
-
appName: answers.appName,
|
|
233
|
-
appTitle: answers.appName,
|
|
234
|
-
databaseUrl: answers.databaseUrl,
|
|
235
|
-
databaseName: (0, validate_1.databaseNameFrom)(answers.databaseUrl),
|
|
236
|
-
adminEmail: answers.adminEmail,
|
|
237
|
-
adminPassword,
|
|
238
|
-
orthaVersion: ownVersion(),
|
|
239
|
-
selection: answers.selection
|
|
240
|
-
});
|
|
241
|
-
console.log('');
|
|
242
|
-
ui.success('Files written');
|
|
243
|
-
if (!flag(argv, 'no-git') && !(0, node_fs_1.existsSync)((0, node_path_1.join)(target, '.git'))) {
|
|
244
|
-
if (runCommand('git', ['init', '--quiet'], target)) {
|
|
245
|
-
ui.success('Git repository initialised');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
let installed = true;
|
|
249
|
-
if (!flag(argv, 'no-install')) {
|
|
250
|
-
console.log('');
|
|
251
|
-
ui.note('Installing dependencies…');
|
|
252
|
-
installed = runCommand('npm', ['install', '--no-audit', '--no-fund'], target);
|
|
253
|
-
if (installed)
|
|
254
|
-
ui.success('Dependencies installed');
|
|
255
|
-
}
|
|
256
|
-
if (!installed) {
|
|
257
|
-
ui.error('Dependency installation failed — fix the error above, then run ' +
|
|
258
|
-
'`npm install` in the new directory.');
|
|
259
|
-
process.exitCode = 1;
|
|
260
|
-
}
|
|
261
|
-
ui.summary('Your admin account', [
|
|
262
|
-
['Email', answers.adminEmail],
|
|
263
|
-
['Password', ui.bold(adminPassword)],
|
|
264
|
-
['', ui.dim('Also written to .env')]
|
|
265
|
-
]);
|
|
266
|
-
ui.nextSteps([
|
|
267
|
-
`cd ${(0, node_path_1.basename)(target)}`,
|
|
268
|
-
...(flag(argv, 'no-install') ? ['npm install'] : []),
|
|
269
|
-
'docker compose up -d',
|
|
270
|
-
'npm run migrate',
|
|
271
|
-
`npm run dev ${ui.dim('→ http://localhost:4200')}`
|
|
272
|
-
]);
|
|
273
|
-
}
|
|
274
|
-
main().catch((error) => {
|
|
11
|
+
const run_1 = require("./lib/run");
|
|
12
|
+
const ui = tslib_1.__importStar(require("./lib/ui"));
|
|
13
|
+
(0, run_1.main)().catch((error) => {
|
|
275
14
|
ui.error(error instanceof Error ? error.message : String(error));
|
|
276
15
|
process.exit(1);
|
|
277
16
|
});
|
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,16 @@ 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.
|
|
73
|
+
*
|
|
74
|
+
* `media-domain` is the storage port, on the same reasoning again: it is what
|
|
75
|
+
* an operator implements to write a `StorageProvider` for a backend we ship no
|
|
76
|
+
* adapter for, and it arrives transitively through both `media-server` and
|
|
77
|
+
* whichever `media-provider-*` the app chose. It is also the package that makes
|
|
78
|
+
* that choice cheap — the port lives away from the server precisely so
|
|
79
|
+
* installing an adapter installs an adapter.
|
|
70
80
|
*
|
|
71
81
|
* `design-system`, `utils-admin` and `utils-server` are here even though the
|
|
72
82
|
* template's own files barely touch them: they are the first things anyone
|
|
@@ -112,11 +122,10 @@ export declare const MEDIA_PROVIDERS: readonly Feature[];
|
|
|
112
122
|
* §10 says is an operator's decision to make explicitly. Pick nothing and the
|
|
113
123
|
* copilot is not registered at all.
|
|
114
124
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* only when it is the only one.
|
|
125
|
+
* There is no offline stand-in to fall back on. The scripted `fake` adapter is
|
|
126
|
+
* a private test fixture of the CMS repo, not a published package, so an app
|
|
127
|
+
* that picks nothing here has the copilot plugin installed with no backend
|
|
128
|
+
* registered — and `COPILOT_ENABLED` must stay `false` until one is.
|
|
120
129
|
*/
|
|
121
130
|
export declare const COPILOT_PROVIDERS: readonly Feature[];
|
|
122
131
|
/**
|
|
@@ -141,8 +150,8 @@ export declare const COPILOT_PROVIDERS: readonly Feature[];
|
|
|
141
150
|
* second reason not to install them for an app that will never speak them.
|
|
142
151
|
*
|
|
143
152
|
* `identity-provider-fake` is not offered: it is installed unconditionally,
|
|
144
|
-
*
|
|
145
|
-
*
|
|
153
|
+
* because it needs no tenant and no network and is how a generated app's
|
|
154
|
+
* sign-in page is exercised offline. Installing it
|
|
146
155
|
* registers nothing — an adapter only does something once the composition root
|
|
147
156
|
* names it, and the template names none.
|
|
148
157
|
*/
|
|
@@ -180,6 +189,19 @@ export interface FeatureSelection {
|
|
|
180
189
|
export declare function resolvePackages(selection: FeatureSelection): string[];
|
|
181
190
|
/** The dev-time `@orthacms/*` dependencies, sorted. */
|
|
182
191
|
export declare function resolveDevPackages(): string[];
|
|
183
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* The feature ids in force for a selection — what `ortha:if` tests against.
|
|
194
|
+
*
|
|
195
|
+
* The picked ids, plus **one** derived group flag: `sso`, set when any
|
|
196
|
+
* `sso-*` provider was chosen.
|
|
197
|
+
*
|
|
198
|
+
* It exists because `ortha:if` is deliberately line-based with no expression
|
|
199
|
+
* language, so a block cannot say "any of these three". Three providers share
|
|
200
|
+
* one `ssoProviders` key in the config, one builder function in `plugins.ts`
|
|
201
|
+
* and one extra argument to `IdentityPlugin` — each of which has to appear if
|
|
202
|
+
* *any* of them was picked, and none of which may be left behind as an empty
|
|
203
|
+
* husk when none was. `sso` is that condition, and it is derived here rather
|
|
204
|
+
* than added to the picker so it can never be selected on its own.
|
|
205
|
+
*/
|
|
184
206
|
export declare function resolveFlags(selection: FeatureSelection): Set<string>;
|
|
185
207
|
//# sourceMappingURL=features.d.ts.map
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EA6C1C,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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAQrE"}
|
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,16 @@ 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.
|
|
55
|
+
*
|
|
56
|
+
* `media-domain` is the storage port, on the same reasoning again: it is what
|
|
57
|
+
* an operator implements to write a `StorageProvider` for a backend we ship no
|
|
58
|
+
* adapter for, and it arrives transitively through both `media-server` and
|
|
59
|
+
* whichever `media-provider-*` the app chose. It is also the package that makes
|
|
60
|
+
* that choice cheap — the port lives away from the server precisely so
|
|
61
|
+
* installing an adapter installs an adapter.
|
|
52
62
|
*
|
|
53
63
|
* `design-system`, `utils-admin` and `utils-server` are here even though the
|
|
54
64
|
* template's own files barely touch them: they are the first things anyone
|
|
@@ -60,6 +70,8 @@ exports.resolveFlags = resolveFlags;
|
|
|
60
70
|
exports.CORE_PACKAGES = [
|
|
61
71
|
'@orthacms/activity-admin',
|
|
62
72
|
'@orthacms/activity-server',
|
|
73
|
+
'@orthacms/alarms-admin',
|
|
74
|
+
'@orthacms/alarms-server',
|
|
63
75
|
'@orthacms/api-tokens-admin',
|
|
64
76
|
'@orthacms/bootstrap-admin',
|
|
65
77
|
'@orthacms/bootstrap-server',
|
|
@@ -68,7 +80,6 @@ exports.CORE_PACKAGES = [
|
|
|
68
80
|
'@orthacms/content-server',
|
|
69
81
|
'@orthacms/copilot-admin',
|
|
70
82
|
'@orthacms/copilot-domain',
|
|
71
|
-
'@orthacms/copilot-provider-fake',
|
|
72
83
|
'@orthacms/copilot-server',
|
|
73
84
|
'@orthacms/database',
|
|
74
85
|
'@orthacms/design-system',
|
|
@@ -80,8 +91,12 @@ exports.CORE_PACKAGES = [
|
|
|
80
91
|
'@orthacms/identity-server',
|
|
81
92
|
'@orthacms/insights-admin',
|
|
82
93
|
'@orthacms/media-admin',
|
|
94
|
+
'@orthacms/media-domain',
|
|
83
95
|
'@orthacms/media-server',
|
|
84
96
|
'@orthacms/query-builder-admin',
|
|
97
|
+
'@orthacms/segments-admin',
|
|
98
|
+
'@orthacms/segments-domain',
|
|
99
|
+
'@orthacms/segments-server',
|
|
85
100
|
'@orthacms/shell-admin',
|
|
86
101
|
'@orthacms/tools-server',
|
|
87
102
|
'@orthacms/transfer-admin',
|
|
@@ -91,6 +106,9 @@ exports.CORE_PACKAGES = [
|
|
|
91
106
|
'@orthacms/users-server',
|
|
92
107
|
'@orthacms/utils-admin',
|
|
93
108
|
'@orthacms/utils-server',
|
|
109
|
+
'@orthacms/webhooks-admin',
|
|
110
|
+
'@orthacms/webhooks-domain',
|
|
111
|
+
'@orthacms/webhooks-server',
|
|
94
112
|
'@orthacms/workspaces-admin',
|
|
95
113
|
'@orthacms/workspaces-server',
|
|
96
114
|
'@orthacms/wysiwyg-admin'
|
|
@@ -175,11 +193,10 @@ exports.MEDIA_PROVIDERS = [
|
|
|
175
193
|
* §10 says is an operator's decision to make explicitly. Pick nothing and the
|
|
176
194
|
* copilot is not registered at all.
|
|
177
195
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* only when it is the only one.
|
|
196
|
+
* There is no offline stand-in to fall back on. The scripted `fake` adapter is
|
|
197
|
+
* a private test fixture of the CMS repo, not a published package, so an app
|
|
198
|
+
* that picks nothing here has the copilot plugin installed with no backend
|
|
199
|
+
* registered — and `COPILOT_ENABLED` must stay `false` until one is.
|
|
183
200
|
*/
|
|
184
201
|
exports.COPILOT_PROVIDERS = [
|
|
185
202
|
{
|
|
@@ -221,8 +238,8 @@ exports.COPILOT_PROVIDERS = [
|
|
|
221
238
|
* second reason not to install them for an app that will never speak them.
|
|
222
239
|
*
|
|
223
240
|
* `identity-provider-fake` is not offered: it is installed unconditionally,
|
|
224
|
-
*
|
|
225
|
-
*
|
|
241
|
+
* because it needs no tenant and no network and is how a generated app's
|
|
242
|
+
* sign-in page is exercised offline. Installing it
|
|
226
243
|
* registers nothing — an adapter only does something once the composition root
|
|
227
244
|
* names it, and the template names none.
|
|
228
245
|
*/
|
|
@@ -322,7 +339,24 @@ function resolvePackages(selection) {
|
|
|
322
339
|
function resolveDevPackages() {
|
|
323
340
|
return [...exports.CORE_DEV_PACKAGES].sort();
|
|
324
341
|
}
|
|
325
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* The feature ids in force for a selection — what `ortha:if` tests against.
|
|
344
|
+
*
|
|
345
|
+
* The picked ids, plus **one** derived group flag: `sso`, set when any
|
|
346
|
+
* `sso-*` provider was chosen.
|
|
347
|
+
*
|
|
348
|
+
* It exists because `ortha:if` is deliberately line-based with no expression
|
|
349
|
+
* language, so a block cannot say "any of these three". Three providers share
|
|
350
|
+
* one `ssoProviders` key in the config, one builder function in `plugins.ts`
|
|
351
|
+
* and one extra argument to `IdentityPlugin` — each of which has to appear if
|
|
352
|
+
* *any* of them was picked, and none of which may be left behind as an empty
|
|
353
|
+
* husk when none was. `sso` is that condition, and it is derived here rather
|
|
354
|
+
* than added to the picker so it can never be selected on its own.
|
|
355
|
+
*/
|
|
326
356
|
function resolveFlags(selection) {
|
|
327
|
-
|
|
357
|
+
const flags = new Set(selection.enabled);
|
|
358
|
+
if (exports.SSO_PROVIDERS.some((provider) => flags.has(provider.id))) {
|
|
359
|
+
flags.add('sso');
|
|
360
|
+
}
|
|
361
|
+
return flags;
|
|
328
362
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Feature, type FeatureSelection } from './features';
|
|
2
|
+
export declare const USAGE = "create-ortha-app \u2014 scaffold an Ortha CMS app\n\nUsage: npx create-ortha-app <directory> [options]\n\nOptions:\n --yes Accept every default, asking nothing\n --media <id> Storage adapter (default: media-local)\n --copilot <ids> Comma-separated copilot providers, or \"none\"\n --sso <ids> Comma-separated identity providers (sso-oidc, sso-github,\n sso-saml), or \"none\"\n --protocols <ids> Comma-separated protocols beyond REST (graphql, mcp), or \"none\"\n --no-install Skip installing dependencies\n --no-git Skip initialising a git repository\n -h, --help Show this message\n";
|
|
3
|
+
/** The ids that are on regardless of what was asked or passed. */
|
|
4
|
+
export declare function lockedOf(features: readonly Feature[]): string[];
|
|
5
|
+
/** Everything the wizard resolves. */
|
|
6
|
+
export interface Answers {
|
|
7
|
+
appName: string;
|
|
8
|
+
databaseUrl: string;
|
|
9
|
+
adminEmail: string;
|
|
10
|
+
selection: FeatureSelection;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the answers, asking only when there is a terminal to ask in.
|
|
14
|
+
*
|
|
15
|
+
* Non-interactive is not an error case to warn about — it is CI, a piped
|
|
16
|
+
* install, and `--yes`. A scaffolder that blocks on a prompt nobody can answer
|
|
17
|
+
* hangs a pipeline until it times out, which is a far worse failure than
|
|
18
|
+
* defaulting, so the flags and the defaults cover every question.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveAnswers(argv: readonly string[], defaultName: string): Promise<Answers>;
|
|
21
|
+
export declare function main(argv?: readonly string[]): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAeA,OAAO,EAMH,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACxB,MAAM,YAAY,CAAC;AASpB,eAAO,MAAM,KAAK,8oBAcjB,CAAC;AA6HF,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CAI/D;AA6BD,sCAAsC;AACtC,MAAM,WAAW,OAAO;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAuFlB;AA+BD,wBAAsB,IAAI,CACtB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAChD,OAAO,CAAC,IAAI,CAAC,CAsFf"}
|