create-ortha-app 0.4.3 → 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 -299
- package/dist/lib/features.d.ts +21 -1
- package/dist/lib/features.d.ts.map +1 -1
- package/dist/lib/features.js +30 -2
- 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/package.json +1 -1
- package/templates/default/README.md.tmpl +26 -7
- package/templates/default/apps/admin/src/plugins.spec.ts +4 -1
- package/templates/default/apps/admin/src/plugins.ts +17 -2
- package/templates/default/apps/server/config/copilot-openai.ts +1 -1
- package/templates/default/apps/server/config/identity.ts +38 -5
- package/templates/default/apps/server/config/media-storage.ts +3 -2
- package/templates/default/apps/server/config/sso-github.ts +42 -0
- package/templates/default/apps/server/config/sso-oidc.ts +1 -1
- package/templates/default/apps/server/config/sso-saml.ts +46 -0
- package/templates/default/apps/server/ortha.config.ts +6 -1
- package/templates/default/apps/server/src/plugins.spec.ts +6 -0
- package/templates/default/apps/server/src/plugins.ts +68 -13
- package/templates/default/env.tmpl +77 -0
package/dist/cli.js
CHANGED
|
@@ -2,308 +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
|
-
* 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
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* This package's own version, which every `@orthacms/*` dependency in the
|
|
84
|
-
* generated app is pinned to.
|
|
85
|
-
*
|
|
86
|
-
* The scaffolder is released in lockstep with the packages it scaffolds, so its
|
|
87
|
-
* version *is* the matching set — which is the whole mechanism keeping a
|
|
88
|
-
* generated app internally consistent. Reading it from the manifest rather than
|
|
89
|
-
* resolving `latest` from the registry also means `npx create-ortha-app@0.3.0`
|
|
90
|
-
* generates a 0.3.0 app, not whatever shipped since.
|
|
91
|
-
*/
|
|
92
|
-
function ownVersion() {
|
|
93
|
-
const manifest = (0, node_path_1.join)(__dirname, '../package.json');
|
|
94
|
-
return JSON.parse((0, node_fs_1.readFileSync)(manifest, 'utf8'))
|
|
95
|
-
.version;
|
|
96
|
-
}
|
|
97
|
-
/** Runs a command in `cwd`, returning whether it succeeded. */
|
|
98
|
-
function runCommand(command, args, cwd) {
|
|
99
|
-
return ((0, node_child_process_1.spawnSync)(command, args, {
|
|
100
|
-
cwd,
|
|
101
|
-
stdio: 'inherit',
|
|
102
|
-
shell: process.platform === 'win32'
|
|
103
|
-
}).status === 0);
|
|
104
|
-
}
|
|
105
|
-
/** Turns a feature into a picker row. */
|
|
106
|
-
function toChoice(feature) {
|
|
107
|
-
return {
|
|
108
|
-
value: feature.id,
|
|
109
|
-
label: feature.label,
|
|
110
|
-
hint: feature.hint,
|
|
111
|
-
selected: feature.enabledByDefault,
|
|
112
|
-
disabled: !feature.available,
|
|
113
|
-
locked: feature.locked
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* The ids a feature group falls back to when nothing is picked.
|
|
118
|
-
*
|
|
119
|
-
* A locked feature is in every answer, including `--protocols none`: REST is
|
|
120
|
-
* not something the flag can switch off.
|
|
121
|
-
*/
|
|
122
|
-
function defaultsOf(features) {
|
|
123
|
-
return features
|
|
124
|
-
.filter((feature) => (feature.enabledByDefault || feature.locked) &&
|
|
125
|
-
feature.available)
|
|
126
|
-
.map((feature) => feature.id);
|
|
127
|
-
}
|
|
128
|
-
/** The ids that are on regardless of what was asked or passed. */
|
|
129
|
-
function lockedOf(features) {
|
|
130
|
-
return features
|
|
131
|
-
.filter((feature) => feature.locked && feature.available)
|
|
132
|
-
.map((feature) => feature.id);
|
|
133
|
-
}
|
|
134
|
-
/** Asks a free-text question, re-asking until it validates. */
|
|
135
|
-
async function askText(label, fallback, validate) {
|
|
136
|
-
const rl = (0, promises_1.createInterface)({
|
|
137
|
-
input: process.stdin,
|
|
138
|
-
output: process.stdout
|
|
139
|
-
});
|
|
140
|
-
try {
|
|
141
|
-
for (;;) {
|
|
142
|
-
const raw = await rl.question(`${ui.cyan('◆')} ${ui.bold(label)} ${ui.dim(`(${fallback})`)} `);
|
|
143
|
-
const answer = raw.trim() || fallback;
|
|
144
|
-
const problem = validate?.(answer);
|
|
145
|
-
if (!problem)
|
|
146
|
-
return answer;
|
|
147
|
-
ui.error(problem);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
rl.close();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
5
|
/**
|
|
155
|
-
*
|
|
6
|
+
* The `create-ortha-app` binary.
|
|
156
7
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* hangs a pipeline until it times out, which is a far worse failure than
|
|
160
|
-
* defaulting, so the flags and the defaults cover every question.
|
|
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.
|
|
161
10
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
170
|
-
const protocols = [
|
|
171
|
-
...lockedOf(features_1.PROTOCOLS),
|
|
172
|
-
...selected(argv, 'protocols', features_1.PROTOCOLS)
|
|
173
|
-
];
|
|
174
|
-
if (!asked) {
|
|
175
|
-
return {
|
|
176
|
-
appName: defaultName,
|
|
177
|
-
databaseUrl: defaultDatabaseUrl,
|
|
178
|
-
adminEmail: 'admin@example.com',
|
|
179
|
-
selection: {
|
|
180
|
-
enabled: new Set([...media, ...copilot, ...sso, ...protocols])
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
ui.section('Project');
|
|
185
|
-
const appName = await askText('App name', defaultName, validate_1.validateAppName);
|
|
186
|
-
const databaseUrl = await askText('Database URL', defaultDatabaseUrl, validate_1.validateDatabaseUrl);
|
|
187
|
-
const adminEmail = await askText('Admin email', 'admin@example.com');
|
|
188
|
-
ui.section('Features');
|
|
189
|
-
ui.note('Everything else is installed for you. These are the choices.');
|
|
190
|
-
console.log('');
|
|
191
|
-
// Only ask when there is more than one answer available. S3 is not
|
|
192
|
-
// published, so today this is a question with a single possible reply, and
|
|
193
|
-
// asking it would be noise pretending to be a choice.
|
|
194
|
-
const selectable = features_1.MEDIA_PROVIDERS.filter((provider) => provider.available);
|
|
195
|
-
const chosenMedia = selectable.length > 1
|
|
196
|
-
? [
|
|
197
|
-
(await ui.select('Where should uploads be stored?', features_1.MEDIA_PROVIDERS.map(toChoice))) ?? media[0]
|
|
198
|
-
]
|
|
199
|
-
: media;
|
|
200
|
-
const chosenCopilot = (await ui.multiselect('AI copilot — which model backends?', features_1.COPILOT_PROVIDERS.map(toChoice))) ?? copilot;
|
|
201
|
-
const chosenSso = (await ui.multiselect('Single sign-on — which identity providers?', features_1.SSO_PROVIDERS.map(toChoice))) ?? sso;
|
|
202
|
-
const chosenProtocols = (await ui.multiselect('Which protocols should the content API speak?', features_1.PROTOCOLS.map(toChoice))) ?? protocols;
|
|
203
|
-
return {
|
|
204
|
-
appName,
|
|
205
|
-
databaseUrl,
|
|
206
|
-
adminEmail,
|
|
207
|
-
selection: {
|
|
208
|
-
enabled: new Set([
|
|
209
|
-
...chosenMedia.filter(Boolean),
|
|
210
|
-
...chosenCopilot,
|
|
211
|
-
...chosenSso,
|
|
212
|
-
...chosenProtocols
|
|
213
|
-
])
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
/** Human-readable summary of what was chosen. */
|
|
218
|
-
function describeSelection(selection) {
|
|
219
|
-
const labelsFor = (features) => {
|
|
220
|
-
const chosen = features
|
|
221
|
-
.filter((feature) => selection.enabled.has(feature.id))
|
|
222
|
-
.map((feature) => feature.label);
|
|
223
|
-
return chosen.length > 0 ? chosen.join(', ') : ui.dim('none');
|
|
224
|
-
};
|
|
225
|
-
const providers = features_1.COPILOT_PROVIDERS.filter((provider) => selection.enabled.has(provider.id));
|
|
226
|
-
return [
|
|
227
|
-
['Storage', labelsFor(features_1.MEDIA_PROVIDERS)],
|
|
228
|
-
['Protocols', labelsFor(features_1.PROTOCOLS)],
|
|
229
|
-
[
|
|
230
|
-
'Copilot',
|
|
231
|
-
providers.length > 0
|
|
232
|
-
? providers.map((p) => p.label).join(', ')
|
|
233
|
-
: ui.dim('no backend')
|
|
234
|
-
],
|
|
235
|
-
['Sign-in', labelsFor(features_1.SSO_PROVIDERS)],
|
|
236
|
-
['Ortha packages', String((0, features_1.resolvePackages)(selection).length + 1)]
|
|
237
|
-
];
|
|
238
|
-
}
|
|
239
|
-
async function main() {
|
|
240
|
-
const argv = process.argv.slice(2);
|
|
241
|
-
if (flag(argv, 'help') || argv.includes('-h')) {
|
|
242
|
-
console.log(USAGE);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const positional = argv.filter((arg) => !arg.startsWith('-'));
|
|
246
|
-
const target = (0, node_path_1.resolve)(positional[0] ?? '.');
|
|
247
|
-
ui.banner('Ortha CMS', `Creating an app in ${target}`);
|
|
248
|
-
if ((0, template_1.isNonEmptyDirectory)(target)) {
|
|
249
|
-
throw new Error(`${target} already exists and is not empty. Pick a new directory, ` +
|
|
250
|
-
`or empty that one first.`);
|
|
251
|
-
}
|
|
252
|
-
const answers = await resolveAnswers(argv, (0, node_path_1.basename)(target));
|
|
253
|
-
// Generated rather than prompted: a password typed at a prompt is echoed to
|
|
254
|
-
// the terminal and lands in shell history. This one is written only to the
|
|
255
|
-
// git-ignored .env, and printed once below.
|
|
256
|
-
const adminPassword = (0, node_crypto_1.randomBytes)(12).toString('base64url');
|
|
257
|
-
ui.summary('Your app', [
|
|
258
|
-
['Name', answers.appName],
|
|
259
|
-
['Database', (0, validate_1.databaseNameFrom)(answers.databaseUrl)],
|
|
260
|
-
...describeSelection(answers.selection)
|
|
261
|
-
]);
|
|
262
|
-
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
263
|
-
(0, template_1.renderTemplate)((0, node_path_1.join)(__dirname, '../templates/default'), target, {
|
|
264
|
-
appName: answers.appName,
|
|
265
|
-
appTitle: answers.appName,
|
|
266
|
-
databaseUrl: answers.databaseUrl,
|
|
267
|
-
databaseName: (0, validate_1.databaseNameFrom)(answers.databaseUrl),
|
|
268
|
-
adminEmail: answers.adminEmail,
|
|
269
|
-
adminPassword,
|
|
270
|
-
orthaVersion: ownVersion(),
|
|
271
|
-
selection: answers.selection
|
|
272
|
-
});
|
|
273
|
-
console.log('');
|
|
274
|
-
ui.success('Files written');
|
|
275
|
-
if (!flag(argv, 'no-git') && !(0, node_fs_1.existsSync)((0, node_path_1.join)(target, '.git'))) {
|
|
276
|
-
if (runCommand('git', ['init', '--quiet'], target)) {
|
|
277
|
-
ui.success('Git repository initialised');
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
let installed = true;
|
|
281
|
-
if (!flag(argv, 'no-install')) {
|
|
282
|
-
console.log('');
|
|
283
|
-
ui.note('Installing dependencies…');
|
|
284
|
-
installed = runCommand('npm', ['install', '--no-audit', '--no-fund'], target);
|
|
285
|
-
if (installed)
|
|
286
|
-
ui.success('Dependencies installed');
|
|
287
|
-
}
|
|
288
|
-
if (!installed) {
|
|
289
|
-
ui.error('Dependency installation failed — fix the error above, then run ' +
|
|
290
|
-
'`npm install` in the new directory.');
|
|
291
|
-
process.exitCode = 1;
|
|
292
|
-
}
|
|
293
|
-
ui.summary('Your admin account', [
|
|
294
|
-
['Email', answers.adminEmail],
|
|
295
|
-
['Password', ui.bold(adminPassword)],
|
|
296
|
-
['', ui.dim('Also written to .env')]
|
|
297
|
-
]);
|
|
298
|
-
ui.nextSteps([
|
|
299
|
-
`cd ${(0, node_path_1.basename)(target)}`,
|
|
300
|
-
...(flag(argv, 'no-install') ? ['npm install'] : []),
|
|
301
|
-
'docker compose up -d',
|
|
302
|
-
'npm run migrate',
|
|
303
|
-
`npm run dev ${ui.dim('→ http://localhost:4200')}`
|
|
304
|
-
]);
|
|
305
|
-
}
|
|
306
|
-
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) => {
|
|
307
14
|
ui.error(error instanceof Error ? error.message : String(error));
|
|
308
15
|
process.exit(1);
|
|
309
16
|
});
|
package/dist/lib/features.d.ts
CHANGED
|
@@ -71,6 +71,13 @@ export interface Feature {
|
|
|
71
71
|
* does something once the composition root registers it, and the template
|
|
72
72
|
* registers none.
|
|
73
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.
|
|
80
|
+
*
|
|
74
81
|
* `design-system`, `utils-admin` and `utils-server` are here even though the
|
|
75
82
|
* template's own files barely touch them: they are the first things anyone
|
|
76
83
|
* reaches for when writing a page or a plugin of their own, and relying on
|
|
@@ -182,6 +189,19 @@ export interface FeatureSelection {
|
|
|
182
189
|
export declare function resolvePackages(selection: FeatureSelection): string[];
|
|
183
190
|
/** The dev-time `@orthacms/*` dependencies, sorted. */
|
|
184
191
|
export declare function resolveDevPackages(): string[];
|
|
185
|
-
/**
|
|
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
|
+
*/
|
|
186
206
|
export declare function resolveFlags(selection: FeatureSelection): Set<string>;
|
|
187
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
|
@@ -53,6 +53,13 @@ exports.resolveFlags = resolveFlags;
|
|
|
53
53
|
* does something once the composition root registers it, and the template
|
|
54
54
|
* registers none.
|
|
55
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.
|
|
62
|
+
*
|
|
56
63
|
* `design-system`, `utils-admin` and `utils-server` are here even though the
|
|
57
64
|
* template's own files barely touch them: they are the first things anyone
|
|
58
65
|
* reaches for when writing a page or a plugin of their own, and relying on
|
|
@@ -84,6 +91,7 @@ exports.CORE_PACKAGES = [
|
|
|
84
91
|
'@orthacms/identity-server',
|
|
85
92
|
'@orthacms/insights-admin',
|
|
86
93
|
'@orthacms/media-admin',
|
|
94
|
+
'@orthacms/media-domain',
|
|
87
95
|
'@orthacms/media-server',
|
|
88
96
|
'@orthacms/query-builder-admin',
|
|
89
97
|
'@orthacms/segments-admin',
|
|
@@ -98,6 +106,9 @@ exports.CORE_PACKAGES = [
|
|
|
98
106
|
'@orthacms/users-server',
|
|
99
107
|
'@orthacms/utils-admin',
|
|
100
108
|
'@orthacms/utils-server',
|
|
109
|
+
'@orthacms/webhooks-admin',
|
|
110
|
+
'@orthacms/webhooks-domain',
|
|
111
|
+
'@orthacms/webhooks-server',
|
|
101
112
|
'@orthacms/workspaces-admin',
|
|
102
113
|
'@orthacms/workspaces-server',
|
|
103
114
|
'@orthacms/wysiwyg-admin'
|
|
@@ -328,7 +339,24 @@ function resolvePackages(selection) {
|
|
|
328
339
|
function resolveDevPackages() {
|
|
329
340
|
return [...exports.CORE_DEV_PACKAGES].sort();
|
|
330
341
|
}
|
|
331
|
-
/**
|
|
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
|
+
*/
|
|
332
356
|
function resolveFlags(selection) {
|
|
333
|
-
|
|
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;
|
|
334
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"}
|