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 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
- * Resolves the answers, asking only when there is a terminal to ask in.
6
+ * The `create-ortha-app` binary.
156
7
  *
157
- * Non-interactive is not an error case to warn about it is CI, a piped
158
- * install, and `--yes`. A scaffolder that blocks on a prompt nobody can answer
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
- async function resolveAnswers(argv, defaultName) {
163
- const asked = !flag(argv, 'yes') && ui.interactive();
164
- const defaultDatabaseUrl = `postgresql://ortha:ortha@localhost:5432/${defaultName.replace(/[^a-z0-9_]/gi, '_')}`;
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
  });
@@ -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
- /** The feature ids in force for a selection — what `ortha:if` tests against. */
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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"}
@@ -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
- /** The feature ids in force for a selection — what `ortha:if` tests against. */
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
- return new Set(selection.enabled);
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"}