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.
Files changed (32) hide show
  1. package/dist/cli.js +6 -267
  2. package/dist/lib/features.d.ts +42 -20
  3. package/dist/lib/features.d.ts.map +1 -1
  4. package/dist/lib/features.js +56 -22
  5. package/dist/lib/run.d.ts +22 -0
  6. package/dist/lib/run.d.ts.map +1 -0
  7. package/dist/lib/run.js +318 -0
  8. package/dist/lib/template.d.ts.map +1 -1
  9. package/dist/lib/template.js +11 -1
  10. package/package.json +1 -1
  11. package/templates/default/README.md.tmpl +38 -12
  12. package/templates/default/apps/admin/src/plugins.spec.ts +5 -1
  13. package/templates/default/apps/admin/src/plugins.ts +21 -2
  14. package/templates/default/apps/admin/tsconfig.json +2 -1
  15. package/templates/default/apps/server/config/copilot-anthropic.ts +22 -0
  16. package/templates/default/apps/server/config/copilot-openai.ts +21 -0
  17. package/templates/default/apps/server/config/copilot.ts +61 -0
  18. package/templates/default/apps/server/config/docs.ts +14 -0
  19. package/templates/default/apps/server/config/i18n.ts +16 -0
  20. package/templates/default/apps/server/config/identity.ts +129 -0
  21. package/templates/default/apps/server/config/mcp.ts +25 -0
  22. package/templates/default/apps/server/config/media-storage.ts +84 -0
  23. package/templates/default/apps/server/config/media.ts +67 -0
  24. package/templates/default/apps/server/config/sso-github.ts +42 -0
  25. package/templates/default/apps/server/config/sso-oidc.ts +34 -0
  26. package/templates/default/apps/server/config/sso-saml.ts +46 -0
  27. package/templates/default/apps/server/ortha.config.ts +60 -449
  28. package/templates/default/apps/server/src/plugins.spec.ts +7 -0
  29. package/templates/default/apps/server/src/plugins.ts +84 -19
  30. package/templates/default/apps/server/tsconfig.spec.json +25 -0
  31. package/templates/default/env.tmpl +84 -2
  32. 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 ids a feature group falls back to when nothing is picked.
6
+ * The `create-ortha-app` binary.
88
7
  *
89
- * A locked feature is in every answer, including `--protocols none`: REST is
90
- * not something the flag can switch off.
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
- function defaultsOf(features) {
93
- return features
94
- .filter((feature) => (feature.enabledByDefault || feature.locked) &&
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
  });
@@ -39,15 +39,18 @@ export interface Feature {
39
39
  /**
40
40
  * The packages every app gets, whatever it opts into.
41
41
  *
42
- * The **copilot** is here — plugin, admin panel and the offline `fake` adapter
43
- * even though it is a large feature nobody may want. Two reasons. Its server
44
- * half arrives anyway: five core plugins (`content`, `activity`, `i18n`,
45
- * `media`, `users`) depend on `copilot-server` to contribute their tools, so
46
- * the code is on disk whatever the manifest says, and leaving it undeclared
47
- * bought nothing but a missing chat panel. And `fake` needs no key and no
48
- * network, so a default app gets a copilot that genuinely works offline —
49
- * while `COPILOT_ENABLED` stays `false`, so nothing reaches a model until an
50
- * operator says so.
42
+ * The **copilot** is here — plugin and admin panel even though it is a large
43
+ * feature nobody may want, because its server half arrives anyway: five core
44
+ * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
45
+ * `copilot-server` to contribute their tools, so the code is on disk whatever
46
+ * the manifest says, and leaving it undeclared bought nothing but a missing chat
47
+ * panel. No **model backend** comes with it: those are the opt-in
48
+ * `COPILOT_PROVIDERS` below, and an app that picks none has the plugin
49
+ * installed and nothing registered. `COPILOT_ENABLED` stays `false`, which
50
+ * unregisters the copilot's routes, so a generated app ships with the chat
51
+ * surfaces absent rather than visible and refusing — and turning it on without
52
+ * a configured backend fails at boot rather than shipping a chat that cannot
53
+ * answer.
51
54
  *
52
55
  * The **extension points** are here for the same reason — `content-domain`,
53
56
  * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
@@ -64,9 +67,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, exactly as `copilot-provider-fake` is for the chat. Note
68
- * that shipping it installs nothing: an adapter only does something once the
69
- * composition root registers it, and the template registers none.
70
+ * exercised offline. Note that shipping it installs nothing: an adapter only
71
+ * does something once the composition root registers it, and the template
72
+ * registers none.
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
- * `copilot-provider-fake` is not offered here it is installed automatically
116
- * whenever the copilot is on. It is a shipped adapter rather than test
117
- * scaffolding (ADR-0004 §3): it needs no key and no network, so it is what
118
- * makes the chat work offline, and it is registered last so it is the default
119
- * only when it is the only one.
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
- * like `copilot-provider-fake`, because it needs no tenant and no network and
145
- * is how a generated app's sign-in page is exercised offline. Installing it
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
- /** 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
+ */
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAqC1C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAGhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,OAAO,EAyC7C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,OAAO,EAiB/C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAyB3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EA0BvC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAK1C,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,EAAE,CASrE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAErE"}
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/lib/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACpB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -21,15 +21,18 @@ exports.resolveFlags = resolveFlags;
21
21
  /**
22
22
  * The packages every app gets, whatever it opts into.
23
23
  *
24
- * The **copilot** is here — plugin, admin panel and the offline `fake` adapter
25
- * even though it is a large feature nobody may want. Two reasons. Its server
26
- * half arrives anyway: five core plugins (`content`, `activity`, `i18n`,
27
- * `media`, `users`) depend on `copilot-server` to contribute their tools, so
28
- * the code is on disk whatever the manifest says, and leaving it undeclared
29
- * bought nothing but a missing chat panel. And `fake` needs no key and no
30
- * network, so a default app gets a copilot that genuinely works offline —
31
- * while `COPILOT_ENABLED` stays `false`, so nothing reaches a model until an
32
- * operator says so.
24
+ * The **copilot** is here — plugin and admin panel even though it is a large
25
+ * feature nobody may want, because its server half arrives anyway: five core
26
+ * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
27
+ * `copilot-server` to contribute their tools, so the code is on disk whatever
28
+ * the manifest says, and leaving it undeclared bought nothing but a missing chat
29
+ * panel. No **model backend** comes with it: those are the opt-in
30
+ * `COPILOT_PROVIDERS` below, and an app that picks none has the plugin
31
+ * installed and nothing registered. `COPILOT_ENABLED` stays `false`, which
32
+ * unregisters the copilot's routes, so a generated app ships with the chat
33
+ * surfaces absent rather than visible and refusing — and turning it on without
34
+ * a configured backend fails at boot rather than shipping a chat that cannot
35
+ * answer.
33
36
  *
34
37
  * The **extension points** are here for the same reason — `content-domain`,
35
38
  * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
@@ -46,9 +49,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, exactly as `copilot-provider-fake` is for the chat. Note
50
- * that shipping it installs nothing: an adapter only does something once the
51
- * composition root registers it, and the template registers none.
52
+ * exercised offline. Note that shipping it installs nothing: an adapter only
53
+ * does something once the composition root registers it, and the template
54
+ * registers none.
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
- * `copilot-provider-fake` is not offered here it is installed automatically
179
- * whenever the copilot is on. It is a shipped adapter rather than test
180
- * scaffolding (ADR-0004 §3): it needs no key and no network, so it is what
181
- * makes the chat work offline, and it is registered last so it is the default
182
- * only when it is the only one.
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
- * like `copilot-provider-fake`, because it needs no tenant and no network and
225
- * is how a generated app's sign-in page is exercised offline. Installing it
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
- /** 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
+ */
326
356
  function resolveFlags(selection) {
327
- 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;
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"}