@universalcms/connect 0.1.0 → 0.13.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 (127) hide show
  1. package/README.md +694 -37
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +418 -15
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +200 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +32 -51
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +335 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +122 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/setup-environment.mjs +70 -0
  71. package/dist/source-bindings.mjs +77 -0
  72. package/dist/source-check.mjs +149 -0
  73. package/dist/source-collect.mjs +80 -0
  74. package/dist/source-config.mjs +20 -0
  75. package/dist/source-extract.mjs +514 -0
  76. package/dist/source-finalize.mjs +52 -0
  77. package/dist/source-routes.mjs +65 -0
  78. package/dist/source-setup.mjs +490 -0
  79. package/dist/staged-setup.mjs +287 -0
  80. package/dist/static-runtime.mjs +67 -0
  81. package/dist/static-setup.mjs +136 -0
  82. package/dist/svelte-runtime.d.mts +27 -0
  83. package/dist/svelte-runtime.mjs +40 -0
  84. package/dist/svelte-setup.mjs +135 -0
  85. package/dist/svelte-template.mjs +48 -0
  86. package/dist/templates.mjs +167 -3
  87. package/dist/vendor/astro-compiler/LICENSE +53 -0
  88. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  89. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  90. package/dist/vendor/astro-compiler/node/index.js +1 -0
  91. package/dist/vendor/project-analysis.mjs +56 -4
  92. package/dist/vendor/react-LICENSE.txt +21 -0
  93. package/dist/vendor/react-document.mjs +124 -0
  94. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  95. package/dist/vendor/react-form.mjs +63 -0
  96. package/dist/vendor/react-safety.mjs +47 -0
  97. package/dist/vendor/source-manifest-provider.mjs +104 -0
  98. package/dist/vendor/typescript-LICENSE.txt +55 -0
  99. package/dist/vendor/typescript.cjs +200276 -0
  100. package/dist/vite-publication-server.d.mts +3 -0
  101. package/dist/vite-publication-server.mjs +82 -0
  102. package/dist/vite-publication.d.mts +15 -0
  103. package/dist/vite-publication.mjs +241 -0
  104. package/dist/vite-source-discovery.mjs +315 -0
  105. package/dist/vite-source.d.mts +30 -0
  106. package/dist/vite-source.mjs +141 -0
  107. package/dist/vue-runtime.d.mts +10 -0
  108. package/dist/vue-runtime.mjs +50 -0
  109. package/dist/vue-server.d.mts +2 -0
  110. package/dist/vue-server.mjs +58 -0
  111. package/dist/vue-setup.mjs +127 -0
  112. package/dist/vue-source.mjs +168 -0
  113. package/dist/website-check.mjs +41 -0
  114. package/dist/website-http.mjs +149 -0
  115. package/dist/website-model.d.mts +20 -0
  116. package/dist/website-model.mjs +155 -0
  117. package/dist/website-next.d.mts +2 -0
  118. package/dist/website-next.mjs +29 -0
  119. package/dist/website-node.d.mts +3 -0
  120. package/dist/website-node.mjs +50 -0
  121. package/dist/website-runtime.d.mts +13 -0
  122. package/dist/website-runtime.mjs +396 -0
  123. package/dist/website-schema.mjs +22175 -0
  124. package/dist/website-setup.mjs +256 -0
  125. package/dist/website.css +49 -0
  126. package/dist/wizard.mjs +237 -38
  127. package/package.json +43 -7
package/dist/cli.mjs CHANGED
@@ -1,13 +1,59 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline/promises';
3
- import { analyze, planSetup, applyPlan, checkConnection, SetupError } from './core.mjs';
3
+ import { lstat } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { analyze, planSetup, applyPlan, checkConnection, readProjectFile, projectRoot, SetupError } from './core.mjs';
4
6
  import { runSetup } from './wizard.mjs';
7
+ import { preflightSetup } from './preflight.mjs';
8
+ import { inspectPageCandidates } from './page-discovery.mjs';
9
+ import { runPageMappingReview } from './page-mapping-review.mjs';
10
+ import { inspectNativeRouteCandidates, runNativeRouteReview } from './native-route-review.mjs';
11
+ import { setupReviewDiagnostic, setupRecovery } from './preflight-recovery.mjs';
12
+ import { runConnect, runServerKey, runPrepareServerKey, prepareSourceKeyCheck, readConnection } from './staged-setup.mjs';
13
+ import { readSetupEnvironment } from './setup-environment.mjs';
14
+ import { checkSource } from './source-check.mjs';
15
+ import { checkPublication } from './publication-check.mjs';
16
+ import { finalizeSource } from './source-finalize.mjs';
17
+ import { setupWebsiteSettings } from './website-setup.mjs';
18
+ import { checkWebsiteSettings } from './website-check.mjs';
19
+ import { WEBSITE_SECTIONS } from './website-http.mjs';
20
+ import { setupSource, applySourcePlan, syncSource, readSourceSettings } from './source-setup.mjs';
21
+ import { processIo, runGuided } from './guided.mjs';
5
22
 
6
- const help = `Universal CMS connector 0.1.0
7
- Usage: universalcms-connect <setup|analyze|init|check> [options]
23
+ const help = `Universal CMS connector 0.13.0
8
24
 
9
- setup Guided setup; saves entered values in private .env.local
25
+ Quick start (recommended) — run in your website folder:
26
+ npx @universalcms/connect@latest --cms https://YOUR_CMS_ADDRESS
10
27
 
28
+ Opens your browser to approve the website, installs this package, connects
29
+ your pages, header/footer and settings, saves the website key privately and
30
+ guides the restart, content import and final check. Run it again any time to
31
+ continue where it stopped.
32
+
33
+ --cms <origin> Your Universal CMS address (or UNIVERSALCMS_URL)
34
+ --code <code> One-time code from Developer setup → Connect without a browser
35
+ --details Show every file and technical message
36
+
37
+ Individual steps (advanced):
38
+ Usage: universalcms-connect <start|inspect-pages|review-pages|preflight|review-routes|review-layout|connect|setup|setup-settings|check-settings|server-key|setup-source|sync-source|finalize-source|analyze|init|check|check-source|check-publication> [options]
39
+
40
+ inspect-pages Discover page URLs and screen candidates without writes
41
+ review-pages Record local page/screen ownership intent; does not install
42
+ preflight Plan original-route setup using public values; no writes or keys
43
+ review-routes Review native Next application routes before full setup
44
+ review-layout Review supported native header/footer slots before full setup
45
+ connect Save public website connection values, without a key
46
+ setup Prepare pages and required header/footer settings, then key input
47
+ setup-settings Install or repair reviewed website settings adapters (--apply)
48
+ check-settings Check each settings adapter and key access without credentials
49
+ server-key Save the delivery key privately through hidden input
50
+ setup-source Prepare source export for a supported native framework
51
+ sync-source Refresh the source snapshot before a production build
52
+ check-source Read the website source export without credentials
53
+ check-publication Verify the managed adapter and an original website URL
54
+ finalize-source Close bootstrap source export after CMS import (--apply)
55
+
56
+ --check-routes Audit local Next App Router paths against the source export
11
57
  --project <directory> Website folder (default current directory)
12
58
  --framework <next|vite> Confirm a supported template
13
59
  --api-origin <origin> Hosted CMS API HTTPS origin
@@ -15,28 +61,209 @@ Usage: universalcms-connect <setup|analyze|init|check> [options]
15
61
  --environment <uuid> Environment ID from Developer setup
16
62
  --locale <locale> Published content locale (default en)
17
63
  --mount </cms> Free Next.js route prefix (default /cms)
18
- --website-origin <url> Browser-facing website origin (guided setup)
19
- --admin-origin <url> CMS admin origin for an existing integration (guided setup)
64
+ --original-routes-only Connect original pages without adding a new SDK demo
65
+ --prepare-only Save private server-key settings before adapter setup
66
+ --skip-key-validation Skip live key verification with CMS API
67
+ --require-server-key Require matching private key settings for setup-source
68
+ --website-origin <url> Exact website origin (setup or check-source)
69
+ --admin-origin <url> Exact CMS admin origin (setup or check-source)
20
70
  --apply Explicitly create the reviewed additive files
21
71
  --dry-run Print a plan only (the default)
22
72
  --non-interactive Never ask questions
23
73
  --slug <slug> Published page used by check
74
+ --path </path> Original URL path used by check-publication
24
75
 
25
76
  Analyze and init read bounded project evidence without executing project code.
77
+ Inspect-pages discovers candidates across framework conventions without claiming
78
+ that they have usable content bindings. Screens sharing a URL remain screens;
79
+ no URL is invented for them. Review-pages records CMS-page, connected-screen or
80
+ native ownership intent in .universalcms/pages.review.json after confirmation.
81
+ This review does not change website rendering or satisfy preflight. Unsupported
82
+ bindings still require a supported framework adapter before installation.
83
+ Both commands stay local: no source upload, key access, AI or network requests.
84
+ Preflight checks the actual framework, local runtime and original-route plan
85
+ before key preparation. Use the six public connection flags from Developer setup,
86
+ or an existing saved public connection. It never reads private environment files,
87
+ executes website code, makes network requests or changes files. Review-required
88
+ returns a nonzero exit status. Planned does not mean installed or live verified;
89
+ fresh header/footer settings still require their separate installation review.
26
90
  Init never edits existing files, installs packages, imports content or publishes.
27
91
  Check makes one private published-page request. CMS_API_KEY comes only from the
28
92
  process environment or a hidden terminal prompt; it is never saved. Use an
29
93
  environment-scoped server delivery key from Developer setup, not a provider key.
30
94
  No AI or provider credentials are required. All analysis stays on this computer.
31
- Setup asks for the delivery key with hidden input. It updates only connection
32
- values in .env.local and unchanged generated files, and preserves unrelated settings.
95
+ Run connect, then setup. Connect saves only public values in
96
+ .universalcms/connection.json; setup prepares integration files, then offers
97
+ to save a server key through hidden input. You can defer and run server-key later.
98
+ Setup includes a required header/footer and website settings review. Unsupported
99
+ or deferred settings leave SETUP INCOMPLETE; run setup-settings --apply after a
100
+ reviewed adapter is ready. Original website links remain alongside saved CMS links.
101
+ The key is saved in Git-ignored, owner-only .env.local. No key is accepted
102
+ as a command argument. These steps do not verify delivery or publish content.
103
+ For key-first or source-only setup, use server-key --prepare-only with the public
104
+ connection flags from Developer setup. It saves private delivery settings before
105
+ an adapter exists. It does not install rendering or verify key authorization.
106
+ Replacing a key preserves existing website integration. Restart or deploy, then
107
+ run check-settings. Navigation access permits reading saved header/footer links;
108
+ it does not add a missing website renderer or create menu links automatically.
109
+ Without a saved connect record, setup retains the all-in-one guided flow with
110
+ hidden key input. Existing custom files and unrelated settings are preserved.
111
+ Developer setup provides connector installation, private-key preparation and
112
+ full setup --original-routes-only for reviewed original page rendering. It validates
113
+ the complete source/publication/binding plan before saving. Source-only preparation
114
+ remains a separate import utility and cannot control publication visibility.
115
+ After importing source into CMS drafts, run finalize-source --apply, restart or
116
+ deploy, then check-publication. This closes the bootstrap content export. A checked
117
+ adapter and route still require actual rendered-content verification in a browser.
118
+ Fresh full setups use the bundled renderer; existing SDK demos retain dependency checks.
119
+ Analyze only prints a report. It does not create /api/cms-source or connect a site.
120
+ Next.js setup also prepares /api/cms-source for public static page copy. New static
121
+ pages are discovered on each development request and the production prebuild hook.
122
+ Dynamic/database content, private routes and unresolved components need explicit
123
+ review; diagnostics never claim those are complete. React/Vite can discover a
124
+ mounted static App or flat literal React Router pages. Other layouts need reviewed
125
+ .universalcms/source.entries.json and .universalcms/content.routes.json mappings.
126
+ Setup validates the local framework, source and rendering before saving integration
127
+ files or requesting a key. Unsupported frameworks get a manual integration guide.
128
+ No empty export or guessed content is generated. Full Vite setup pins localhost's
129
+ registered port; stop any old server on that port before restarting the website.
130
+ Setup-source is a plan by default; review then use --apply. It uses only the four
131
+ public scope/origin flags, or saved source configuration; no key or AI is needed.
132
+ For key-first setup, add --require-server-key and the exact --api-origin. This
133
+ checks the saved private key and connection scope before source files are saved;
134
+ the key is never sent to the source endpoint and authorization remains unverified.
135
+ After saving, setup-source checks the live endpoint for up to 10 seconds. Saved
136
+ files alone do not mean a running server loaded them; stop and restart an old
137
+ server using the displayed project command, then run the scoped check-source.
138
+ Full setup checks the running publication adapter for up to 10 seconds after
139
+ saving. CONNECTION NOT READY means the files were saved but the running website
140
+ still needs attention. Restart or deploy, then run the displayed check-publication
141
+ before checking the connection in the CMS. Setup never stops a server or changes
142
+ the registered port automatically.
143
+ Check-source uses --website-origin, --admin-origin, --site and --environment, or
144
+ saved local public values. All four explicit flags bypass saved settings; no init
145
+ or supported scaffold framework is required. It sends no key, cookies or content
146
+ to the CMS and changes no files. It checks the source envelope, scope and CORS,
147
+ not full block-schema validation, complete inventory or actual browser access.
33
148
  `;
34
149
 
150
+ const publicSourceValues = config => Object.fromEntries(['websiteOrigin', 'adminOrigin', 'siteId', 'environmentId'].map(name => [name, config[name]]));
151
+ const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
152
+ function sourceCheckCommand(config) {
153
+ return `npx --no-install universalcms-connect check-source --website-origin ${shellQuote(config.websiteOrigin)} --admin-origin ${shellQuote(config.adminOrigin)} --site ${shellQuote(config.siteId)} --environment ${shellQuote(config.environmentId)}`;
154
+ }
155
+ function publicationCheckCommand(config) {
156
+ return `npx --no-install universalcms-connect check-publication --website-origin ${shellQuote(config.websiteOrigin)} --admin-origin ${shellQuote(config.adminOrigin)} --site ${shellQuote(config.siteId)} --environment ${shellQuote(config.environmentId)}`;
157
+ }
158
+ async function reportReviewedSourceAlternative(project, options) {
159
+ try {
160
+ const saved = await readSetupEnvironment(project), connection = await readConnection(project), source = await readSourceSettings(project);
161
+ const binding = { ...saved.defaults, ...source, ...(connection ?? {}), ...Object.fromEntries(Object.entries(options).filter(([name, value]) => value !== undefined && ['apiOrigin', 'siteId', 'environmentId', 'locale', 'websiteOrigin', 'adminOrigin'].includes(name))) };
162
+ const plan = await setupSource(project, Object.fromEntries(['siteId', 'environmentId', 'locale', 'websiteOrigin', 'adminOrigin'].map(name => [name, binding[name]])));
163
+ if (plan.diagnostics.sourceOnly !== true || plan.diagnostics.publicationRendering !== false) return;
164
+ if (saved.keyConfigured) await prepareSourceKeyCheck(project, Object.fromEntries(['apiOrigin', 'siteId', 'environmentId', 'locale', 'websiteOrigin', 'adminOrigin'].map(name => [name, binding[name]])));
165
+ const keyFlags = saved.keyConfigured ? ` --require-server-key --api-origin ${shellQuote(binding.apiOrigin)}` : '';
166
+ process.stdout.write('Full setup remains unsupported for this website\'s native renderer. Its reviewed public content can be imported with the separate command below; this does not connect published CMS edits to the original design.\n');
167
+ process.stdout.write(`npx --no-install universalcms-connect setup-source --apply${keyFlags} --website-origin ${shellQuote(plan.config.websiteOrigin)} --admin-origin ${shellQuote(plan.config.adminOrigin)} --site ${shellQuote(plan.config.siteId)} --environment ${shellQuote(plan.config.environmentId)} --locale ${shellQuote(plan.config.locale)}\n`);
168
+ } catch { /* The original setup error remains authoritative. Never suggest an unvalidated alternative. */ }
169
+ }
170
+ async function sourceRestartCommand(plan) {
171
+ try {
172
+ const root = await projectRoot(plan.root);
173
+ const manifest = JSON.parse(await readProjectFile(root, 'package.json'));
174
+ const script = manifest?.scripts?.dev;
175
+ // Display a fixed package-manager command only for a recognized dev script.
176
+ // Never evaluate project scripts or copy their arbitrary shell text to output.
177
+ if (typeof script !== 'string') return null;
178
+ const vite = /^vite(?:\s+--(?:host(?:\s+(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1))?|port\s+\d{1,5}|strictPort|open|https))* --config \.universalcms\/source\.vite\.config\.mjs$/.test(script);
179
+ const next = /^next dev(?:\s+(?:--(?:turbo|turbopack|webpack)|(?:--port|-p)\s+\d{1,5}|(?:--hostname|-H)\s+(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)))*$/.test(script);
180
+ const astro = /^astro dev(?: --(?:host(?: (?:localhost|127\.0\.0\.1|0\.0\.0\.0))?|port [0-9]{1,5}|open))* --config \.universalcms\/astro\.config\.mjs$/.test(script);
181
+ if (!(plan.framework === 'vite' ? vite : plan.framework === 'next' ? next : plan.framework === 'astro' ? astro : vite || next || astro)) return null;
182
+ const declared = typeof manifest.packageManager === 'string' && /^(npm|pnpm|yarn|bun)@\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.exec(manifest.packageManager);
183
+ if (manifest.packageManager !== undefined) return declared ? `${declared[1]} run dev` : null;
184
+ const managers = new Set();
185
+ for (const [filename, manager] of [['package-lock.json', 'npm'], ['npm-shrinkwrap.json', 'npm'], ['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun']]) {
186
+ try {
187
+ const stat = await lstat(join(root, filename));
188
+ if (!stat.isFile() || stat.isSymbolicLink()) return null;
189
+ managers.add(manager);
190
+ } catch (error) { if (error.code !== 'ENOENT') return null; }
191
+ }
192
+ return managers.size <= 1 ? `${[...managers][0] ?? 'npm'} run dev` : null;
193
+ } catch { return null; }
194
+ }
195
+ async function sourceStopInstruction(plan) {
196
+ let astro = plan.framework === 'astro';
197
+ if (!plan.framework) {
198
+ try { const manifest = JSON.parse(await readProjectFile(await projectRoot(plan.root), 'package.json')); astro = Boolean(manifest.dependencies?.astro ?? manifest.devDependencies?.astro); } catch { /* Keep generic restart guidance. */ }
199
+ }
200
+ return astro
201
+ ? 'Astro 7 may run in the background. From this exact website folder run: npx --no-install astro dev stop. Ctrl+C stops a foreground server only. Confirm the registered port is free before restarting.'
202
+ : 'Stop the existing website server (Ctrl+C in its terminal).';
203
+ }
204
+
205
+ async function reportSourceReadiness(plan) {
206
+ process.stdout.write('Checking the live source endpoint (up to 10 seconds; no credentials).\n');
207
+ const outcome = await checkSource(plan.root, publicSourceValues(plan.config));
208
+ process.stdout.write(`${outcome.checked ? 'SOURCE READY' : 'SOURCE NOT READY'} (${outcome.status}): ${outcome.message}\n`);
209
+ if (!outcome.checked) {
210
+ if (['missing-route', 'not-json'].includes(outcome.status)) process.stdout.write('Files are saved, but the running server has not loaded the source endpoint at the configured origin.\n');
211
+ const restart = await sourceRestartCommand(plan);
212
+ process.stdout.write(restart
213
+ ? `${await sourceStopInstruction(plan)} Then from the website folder run: ${restart}\n`
214
+ : 'Stop the existing website server, then restart it with the project\'s normal command or deploy the updated website.\n');
215
+ process.stdout.write('Rerun the check below and confirm it passes before Connect & fetch content.\n');
216
+ }
217
+ process.stdout.write(`From the website folder, verify this exact scope:\n${sourceCheckCommand(plan.config)}\nPublished rendering remains unverified.\n`);
218
+ }
219
+
220
+ async function reportSetupReadiness(project) {
221
+ let config;
222
+ try {
223
+ config = publicSourceValues(await readSourceSettings(project));
224
+ if (Object.values(config).some(value => typeof value !== 'string' || !value)) throw Error();
225
+ } catch {
226
+ process.stdout.write('CONNECTION NOT READY: Integration files were saved, but their public connection settings could not be read. Review the saved setup in this website folder before checking the running website.\n');
227
+ return;
228
+ }
229
+ process.stdout.write('Integration files saved. Checking the running publication adapter (up to 10 seconds; no credentials).\n');
230
+ process.stdout.write('Checking the running header/footer settings adapter (up to 10 seconds; no credentials).\n');
231
+ const [outcome, settings] = await Promise.all([checkPublication(project, config), checkWebsiteSettings(project, config)]);
232
+ process.stdout.write(`${outcome.checked ? 'PUBLICATION CHECK PASSED' : 'CONNECTION NOT READY'} (${outcome.status}): ${outcome.message}\n`);
233
+ if (!outcome.checked) {
234
+ if (['missing-adapter', 'invalid-response'].includes(outcome.status)) process.stdout.write('Saved files do not prove that the server at this URL loaded them. An older server may still be serving the website. Removing integration files or reinstalling packages does not stop that process.\n');
235
+ const url = new URL(config.websiteOrigin);
236
+ if (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
237
+ const restart = await sourceRestartCommand({ root: project });
238
+ process.stdout.write(`Use the registered website origin ${url.origin} (port ${url.port || '80'}). If the port is in use, stop the old website server in its terminal before starting this project. Do not start a second server on another port.\n`);
239
+ process.stdout.write(`${await sourceStopInstruction({ root: project })}\n`);
240
+ process.stdout.write(restart ? `From this website folder, restart with: ${restart}\n` : 'Restart with this project\'s normal website command after reviewing its server configuration.\n');
241
+ } else process.stdout.write('Deploy the updated integration and private server configuration to this exact website origin, then verify the running deployment.\n');
242
+ process.stdout.write('Run the check below after restart or deployment and confirm it passes before checking the connection in Universal CMS.\n');
243
+ }
244
+ process.stdout.write(`Verify this exact website and environment:\n${publicationCheckCommand(config)}\nA source-export check alone cannot verify publication control. Actual rendered content and access from the CMS browser still need verification. No content was imported or published.\n`);
245
+ const navigation = settings.sections?.navigation;
246
+ if (!settings.checked || navigation?.installed === false || !navigation || navigation.state === 'unavailable') {
247
+ process.stdout.write('HEADER/FOOTER NOT READY: The running website has not verified its navigation adapter. Run setup-settings --apply if it is missing, then restart or deploy and run check-settings. Replacing a key alone cannot install the adapter.\n');
248
+ } else if (navigation.state === 'denied') {
249
+ process.stdout.write('HEADER/FOOTER ACCESS DENIED: The current server key cannot read Navigation. Review the intended key access yourself; setup has not changed it. Restart or deploy after an authorized key update, then run check-settings.\n');
250
+ } else if (navigation.state === 'missing') {
251
+ process.stdout.write('HEADER/FOOTER ADAPTER CHECK PASSED: No navigation configuration is saved for this connection. Save the intended links in CMS Header & footer, then verify their placement in the actual website.\n');
252
+ } else {
253
+ process.stdout.write('HEADER/FOOTER ADAPTER CHECK PASSED: Navigation configuration is available. Verify saved header and footer links in the actual desktop and mobile website; this diagnostic does not verify appearance.\n');
254
+ }
255
+ if (settings.checked) {
256
+ const missing = WEBSITE_SECTIONS.filter(name => settings.sections?.[name]?.installed !== true);
257
+ if (missing.length) process.stdout.write(`SETTINGS INCOMPLETE: Website adapters are not installed for ${missing.join(', ')}. Key access alone does not install them.\n`);
258
+ }
259
+ process.stdout.write('Configuration check: npx --no-install universalcms-connect check-settings\n');
260
+ }
261
+
35
262
  function argumentsFor(args) {
36
263
  const command = args.shift();
37
- if (!['setup', 'analyze', 'init', 'check'].includes(command)) throw new SetupError('Choose setup, analyze, init or check. Use --help for instructions.');
38
- const flags = new Set(['--apply', '--dry-run', '--non-interactive']);
39
- const names = new Set(['--project', '--framework', '--api-origin', '--site', '--environment', '--locale', '--mount', '--slug', '--website-origin', '--admin-origin']);
264
+ if (!['inspect-pages', 'review-pages', 'preflight', 'review-routes', 'review-layout', 'connect', 'setup', 'server-key', 'setup-source', 'sync-source', 'finalize-source', 'analyze', 'init', 'check', 'check-source', 'check-publication', 'setup-settings', 'check-settings'].includes(command)) throw new SetupError('Choose inspect-pages, review-pages, preflight, review-routes, review-layout, connect, setup, setup-settings, check-settings, server-key, setup-source, sync-source, finalize-source, analyze, init, check, check-source or check-publication. Use --help for instructions.');
265
+ const flags = new Set(['--apply', '--dry-run', '--non-interactive', '--check-routes', '--original-routes-only', '--prepare-only', '--require-server-key', '--skip-key-validation']);
266
+ const names = new Set(['--project', '--framework', '--api-origin', '--site', '--environment', '--locale', '--mount', '--slug', '--path', '--website-origin', '--admin-origin']);
40
267
  const options = {};
41
268
  for (let index = 0; index < args.length; index++) {
42
269
  const name = args[index];
@@ -49,7 +276,7 @@ function argumentsFor(args) {
49
276
  }
50
277
  }
51
278
  if (options['--apply'] && options['--dry-run']) throw new SetupError('Choose either --apply or --dry-run.');
52
- const allowed = command === 'setup' ? ['--project', '--api-origin', '--site', '--environment', '--locale', '--mount', '--website-origin', '--admin-origin'] : command === 'analyze' ? ['--project'] : command === 'check' ? ['--project', '--slug', '--non-interactive'] : [...flags, ...names].filter(name => !['--slug', '--website-origin', '--admin-origin'].includes(name));
279
+ const allowed = ['inspect-pages','review-pages'].includes(command) ? ['--project','--locale','--website-origin','--dry-run','--non-interactive'] : ['review-routes','review-layout'].includes(command) ? ['--project','--dry-run','--non-interactive'] : command === 'preflight' ? ['--project', '--api-origin', '--site', '--environment', '--locale', '--website-origin', '--admin-origin'] : command === 'setup-settings' ? ['--project','--apply','--dry-run','--non-interactive'] : command === 'check-settings' ? ['--project','--site','--environment','--website-origin','--admin-origin','--non-interactive'] : command === 'connect' ? ['--project', '--api-origin', '--site', '--environment', '--locale', '--website-origin', '--admin-origin'] : command === 'server-key' ? ['--project', '--prepare-only', '--skip-key-validation', ...(options['--prepare-only'] ? ['--api-origin', '--site', '--environment', '--locale', '--website-origin', '--admin-origin'] : [])] : command === 'setup' ? ['--project', '--api-origin', '--site', '--environment', '--locale', '--mount', '--website-origin', '--admin-origin', '--original-routes-only'] : command === 'setup-source' ? ['--project', '--site', '--environment', '--website-origin', '--admin-origin', '--locale', '--apply', '--dry-run', '--non-interactive', '--require-server-key', ...(options['--require-server-key'] ? ['--api-origin'] : [])] : command === 'sync-source' ? ['--project'] : command === 'finalize-source' ? ['--project', '--apply', '--dry-run'] : command === 'check-publication' ? ['--project', '--site', '--environment', '--website-origin', '--admin-origin', '--path', '--non-interactive'] : command === 'check-source' ? ['--project', '--site', '--environment', '--website-origin', '--admin-origin', '--non-interactive', '--check-routes'] : command === 'analyze' ? ['--project'] : command === 'check' ? ['--project', '--slug', '--non-interactive'] : [...flags, ...names].filter(name => !['--slug', '--path', '--website-origin', '--admin-origin', '--check-routes', '--original-routes-only', '--prepare-only', '--require-server-key', '--skip-key-validation'].includes(name));
53
280
  if (Object.keys(options).some(name => !allowed.includes(name))) throw new SetupError('An option is not supported for this command.');
54
281
  return { command, options };
55
282
  }
@@ -60,7 +287,6 @@ async function hiddenKey(prompt = 'Server delivery key (hidden; not saved): ') {
60
287
  if (!input.isTTY || typeof input.setRawMode !== 'function') return undefined;
61
288
  const previousRaw = input.isRaw;
62
289
  input.setRawMode(true); input.resume();
63
- output.write(prompt);
64
290
  try {
65
291
  return await new Promise((resolve, reject) => {
66
292
  let value = '';
@@ -83,6 +309,7 @@ async function hiddenKey(prompt = 'Server delivery key (hidden; not saved): ') {
83
309
  };
84
310
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(signal, onSignal);
85
311
  input.on('data', onData); input.on('end', onEnd);
312
+ output.write(prompt);
86
313
  });
87
314
  } finally { input.setRawMode(previousRaw); input.pause(); output.write('\n'); }
88
315
  }
@@ -102,19 +329,195 @@ async function ask(prompt) {
102
329
  } finally { input.off('SIGINT', cancel); input.off('close', cancel); input.close(); }
103
330
  }
104
331
 
332
+ function guidedArguments(args) {
333
+ if (args[0] === 'start') args = args.slice(1);
334
+ const names = new Set(['--cms', '--code', '--project']), options = {};
335
+ for (let index = 0; index < args.length; index++) {
336
+ const name = args[index];
337
+ if (Object.hasOwn(options, name)) throw new SetupError('Each option can be given once.');
338
+ if (name === '--details') { options[name] = true; continue; }
339
+ if (!names.has(name)) throw new SetupError(`Unknown option ${JSON.stringify(name.slice(0, 40))}. Use --help for instructions.`);
340
+ const value = args[++index];
341
+ if (!value || value.startsWith('--')) throw new SetupError('An option is missing its value.');
342
+ options[name] = value;
343
+ }
344
+ return options;
345
+ }
346
+
105
347
  async function main() {
106
348
  if (process.argv.slice(2).some(value => value === '--help' || value === '-h')) { process.stdout.write(help); return; }
349
+ const first = process.argv[2];
350
+ if (first === undefined || first === 'start' || first.startsWith('--')) {
351
+ // The one-time pairing code is the only accepted credential-like argument:
352
+ // it expires in ten minutes, works once and never reaches project files.
353
+ const options = guidedArguments(process.argv.slice(2));
354
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('This setup needs an interactive terminal. Run it directly in your website folder.');
355
+ await runGuided(options['--project'] ?? process.cwd(), { cms: options['--cms'], code: options['--code'], details: options['--details'] === true }, processIo({ ask, write: value => process.stdout.write(value) }));
356
+ return;
357
+ }
107
358
  const { command, options } = argumentsFor(process.argv.slice(2));
108
359
  const project = options['--project'] ?? process.cwd();
360
+ if (command === 'inspect-pages' || command === 'review-pages') {
361
+ const mappingOptions = { locale: options['--locale'] ?? 'en', ...(options['--website-origin'] ? { websiteOrigin: options['--website-origin'] } : {}) };
362
+ if (command === 'inspect-pages' || options['--dry-run'] || options['--non-interactive']) {
363
+ const inventory = await inspectPageCandidates(project, mappingOptions);
364
+ process.stdout.write(JSON.stringify({ ...inventory, installed: false, bindingsVerified: false, liveVerified: false,
365
+ message: 'Discovery only. Candidates are not verified content bindings. Review-pages can record ownership intent; it does not install website routes, resolve preflight blockers, or publish content.' }, null, 2) + '\n');
366
+ return;
367
+ }
368
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('Page mapping review requires an interactive terminal. Use inspect-pages or review-pages --dry-run to inspect without writes.');
369
+ await runPageMappingReview(project, { ask, write: value => process.stdout.write(value) }, mappingOptions);
370
+ return;
371
+ }
372
+ if (command === 'review-layout') {
373
+ const {nativeFramework} = await import('./native-adapters.mjs');
374
+ const reviewRoot = await projectRoot(project);
375
+ let reviewManifest; try { reviewManifest = JSON.parse(await readProjectFile(reviewRoot,'package.json')); } catch { throw new SetupError('A valid website package.json is required for layout review.'); }
376
+ if(nativeFramework(reviewManifest)) {
377
+ const {planNativeChromeReview,runNativeChromeReview} = await import('./native-chrome-review.mjs');
378
+ if(options['--dry-run'] || options['--non-interactive']) {
379
+ const plan=await planNativeChromeReview(project);
380
+ process.stdout.write('Native layout plan: preserve existing native links, controls and classes; add empty settings slots and a hashed review declaration.\n');
381
+ for(const page of plan.pages)process.stdout.write(` ${page.change}: ${page.path} (${page.slots.length} settings slots)\n`);
382
+ process.stdout.write('Plan only. No files changed. Run review-layout in an interactive terminal to review and save.\n');return;
383
+ }
384
+ if(!process.stdin.isTTY || !process.stderr.isTTY)throw new SetupError('Layout review requires an interactive terminal. Use --dry-run to inspect without writes.');
385
+ await runNativeChromeReview(project,{write:value=>process.stdout.write(value),ask});return;
386
+ }
387
+ const { planSettingsLayoutReview } = await import('./next-settings-layout.mjs');
388
+ const plan = await planSettingsLayoutReview(project);
389
+ process.stdout.write('Review existing website header/footer slots. Native controls and links are preserved; CMS settings consumers will be installed during full setup. This command saves a public review declaration only.\n');
390
+ for (const slot of plan.slots) process.stdout.write(` ${slot.kind}: ${slot.path} — append ${slot.kind === 'header' ? 'CMS branding, navigation and language links' : 'CMS navigation, social links and consent controls'}; preserve existing native content.\n`);
391
+ process.stdout.write(` Review covers ${plan.files.filter(file => file.state === 'unchanged').length} existing source/configuration files.\n`);
392
+ for (const file of plan.files.filter(file => ['create','update'].includes(file.state))) process.stdout.write(` ${file.state}: ${file.path}\n`);
393
+ if (options['--dry-run'] || options['--non-interactive']) {
394
+ process.stdout.write('Plan only. No files changed. Run review-layout in an interactive terminal to review and save.\n');
395
+ return;
396
+ }
397
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('Layout review requires an interactive terminal. Use --dry-run to inspect without writes.');
398
+ const answer = (await ask('Save this header/footer layout review? [y/N] ')).trim().toLowerCase();
399
+ if (!['y','yes'].includes(answer)) { process.stdout.write('Layout review cancelled. No files changed.\n'); return; }
400
+ if (JSON.stringify(await planSettingsLayoutReview(project)) !== JSON.stringify(plan)) throw new SetupError('The layout changed during review. No declaration was saved.');
401
+ await applyPlan(plan);
402
+ process.stdout.write('Layout review saved. Rerun scoped preflight, then complete full setup and the running-website checks. This review alone does not install an endpoint or verify settings.\n');
403
+ return;
404
+ }
405
+ if (command === 'review-routes') {
406
+ if (options['--dry-run'] || options['--non-interactive']) {
407
+ const {root, ...result} = await inspectNativeRouteCandidates(project);
408
+ process.stdout.write(JSON.stringify({ ...result, installed: false, message: 'Plan only. Run review-routes in an interactive terminal to select and confirm native routes.' }, null, 2) + '\n');
409
+ } else {
410
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('Route review requires an interactive terminal. Use --dry-run to inspect without writes.');
411
+ await runNativeRouteReview(project, { ask, write: value => process.stdout.write(value) });
412
+ }
413
+ return;
414
+ }
415
+ if (command === 'preflight') {
416
+ const result = await preflightSetup(project, {
417
+ apiOrigin: options['--api-origin'], siteId: options['--site'], environmentId: options['--environment'],
418
+ locale: options['--locale'], websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'],
419
+ });
420
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
421
+ if (result.status !== 'planned') process.exitCode = 1;
422
+ return;
423
+ }
424
+ if (command === 'connect' || command === 'server-key') {
425
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('Connect and server-key need an interactive terminal. No credentials are accepted as command-line options.');
426
+ const io = { ask, hidden: hiddenKey, write: value => process.stdout.write(value) };
427
+ const skipValidation = options['--skip-key-validation'] === true;
428
+ if (command === 'server-key' && options['--prepare-only']) await runPrepareServerKey(project, {
429
+ apiOrigin: options['--api-origin'], siteId: options['--site'], environmentId: options['--environment'],
430
+ locale: options['--locale'], websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'],
431
+ }, io);
432
+ else if (command === 'server-key') await runServerKey(project, io, {
433
+ validateKey: !skipValidation,
434
+ });
435
+ else await runConnect(project, {
436
+ apiOrigin: options['--api-origin'], siteId: options['--site'], environmentId: options['--environment'],
437
+ locale: options['--locale'], websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'],
438
+ }, io);
439
+ return;
440
+ }
441
+ if (command === 'setup-settings') {
442
+ process.stdout.write(`${JSON.stringify(await setupWebsiteSettings(project, {apply: options['--apply'] === true}),null,2)}\n`);
443
+ return;
444
+ }
445
+ if (command === 'check-settings') {
446
+ const result=await checkWebsiteSettings(project,{siteId:options['--site'],environmentId:options['--environment'],websiteOrigin:options['--website-origin'],adminOrigin:options['--admin-origin']});
447
+ process.stdout.write(`${JSON.stringify(result)}\n`);if(!result.checked||WEBSITE_SECTIONS.some(name=>result.sections?.[name]?.installed!==true))process.exitCode=1;return;
448
+ }
449
+ if (command === 'sync-source') {
450
+ process.stdout.write(`${JSON.stringify(await syncSource(project))}\n`);
451
+ return;
452
+ }
453
+ if (command === 'finalize-source') {
454
+ process.stdout.write(`${JSON.stringify(await finalizeSource(project, { apply: options['--apply'] === true }))}\n`);
455
+ return;
456
+ }
457
+ if (command === 'check-publication') {
458
+ const result = await checkPublication(project, { websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'], siteId: options['--site'], environmentId: options['--environment'], path: options['--path'] });
459
+ process.stdout.write(`${JSON.stringify(result)}\n`);
460
+ if (!result.checked) process.exitCode = 1;
461
+ return;
462
+ }
463
+ if (command === 'setup-source') {
464
+ if (options['--require-server-key'] && !options['--api-origin']) throw new SetupError('Key-first source setup requires --api-origin from Developer setup so the prepared key cannot be rebound to a different CMS API.');
465
+ const saved = await readSourceSettings(project);
466
+ const required = [['--website-origin', 'Website origin', saved.websiteOrigin], ['--admin-origin', 'CMS admin origin', saved.adminOrigin], ['--site', 'Website UUID', saved.siteId], ['--environment', 'Environment UUID', saved.environmentId]];
467
+ for (const [flag, label, fallback] of required) {
468
+ if (!options[flag] && !fallback && !options['--non-interactive'] && process.stdin.isTTY) options[flag] = (await ask(`${label}: `)).trim();
469
+ }
470
+ const keyCheck = options['--require-server-key'] ? await prepareSourceKeyCheck(project, {
471
+ apiOrigin: options['--api-origin'], siteId: options['--site'] ?? saved.siteId, environmentId: options['--environment'] ?? saved.environmentId,
472
+ locale: options['--locale'] ?? saved.locale ?? 'en', websiteOrigin: options['--website-origin'] ?? saved.websiteOrigin, adminOrigin: options['--admin-origin'] ?? saved.adminOrigin,
473
+ }) : null;
474
+ const plan = await setupSource(project, { websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'], siteId: options['--site'], environmentId: options['--environment'], locale: options['--locale'] });
475
+ if (keyCheck) process.stdout.write('SERVER KEY PREPARED: Saved private settings match this source setup. Key authorization and published rendering remain unverified; no key is sent to the source endpoint.\n');
476
+ process.stdout.write('CONTENT IMPORT ONLY: This command prepares the public source endpoint. It does not connect published CMS edits to the original website pages, install publication checks, or request a server key.\n');
477
+ process.stdout.write(plan.framework === 'astro'
478
+ ? 'Astro exports the reviewed literal public pages through its Node server. Private, dynamic or custom routes require a reviewed adapter; source exclusions are not guessed. No credentials or CMS writes are used.\n'
479
+ : plan.framework === 'vite' ? 'Vite export serves reviewed public pages/posts from .universalcms/source.entries.json. No credentials or CMS writes are used.\n' : 'Automatic export includes public static page copy. Exclude private paths in source.config.json; dynamic content and unresolved components need review. No credentials or CMS writes are used.\n');
480
+ for (const file of plan.files) process.stdout.write(` ${file.state}: ${file.path}\n`);
481
+ process.stdout.write(`${JSON.stringify(plan.diagnostics)}\n`);
482
+ if (plan.retainedAdapter) process.stdout.write('Existing source adapter retained. Its reviewed base mappings are preserved; verify that the adapter serves the refreshed automatic snapshot with check-source.\n');
483
+ if (options['--apply']) {
484
+ if (keyCheck) await keyCheck();
485
+ const result = await applySourcePlan(plan, keyCheck ? { beforeWrite: keyCheck } : undefined);
486
+ process.stdout.write(`Source setup saved: ${result.created.length} created; ${result.updated.length} updated.\n`);
487
+ await reportSourceReadiness(plan);
488
+ } else process.stdout.write('Plan only: no files changed. Review the public-content scope and run again with --apply.\n');
489
+ return;
490
+ }
109
491
  if (command === 'setup') {
110
492
  if (!process.stdin.isTTY || !process.stderr.isTTY) throw new SetupError('Guided setup needs an interactive terminal. Use init for non-interactive scaffold generation.');
111
- await runSetup(project, {
493
+ const setupOptions = {
494
+ originalRoutesOnly: options['--original-routes-only'],
112
495
  apiOrigin: options['--api-origin'], siteId: options['--site'], environmentId: options['--environment'],
113
496
  locale: options['--locale'], mount: options['--mount'], websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'],
114
- }, { ask, hidden: hiddenKey, write: value => process.stdout.write(value) });
497
+ };
498
+ try {
499
+ const result = await runSetup(project, setupOptions, { ask, hidden: hiddenKey, write: value => process.stdout.write(value) });
500
+ if (result.settingsInstalled === false) process.exitCode = 1;
501
+ else if (result.saved) await reportSetupReadiness(project);
502
+ }
503
+ catch (error) {
504
+ const blocker = setupReviewDiagnostic(error);
505
+ if (blocker) process.stderr.write(`${JSON.stringify({ status: 'review-required', blockers: [blocker], recovery: setupRecovery({ phase: blocker.stage, framework: 'Next.js', blocker }) }, null, 2)}\n`);
506
+ if (error instanceof SetupError && /custom history\/location router|Vite source export is missing|content route mapping/.test(error.message)) await reportReviewedSourceAlternative(project, setupOptions);
507
+ throw error;
508
+ }
115
509
  return;
116
510
  }
117
511
  if (command === 'analyze') { process.stdout.write(`${JSON.stringify(await analyze(project), null, 2)}\n`); return; }
512
+ if (command === 'check-source') {
513
+ const result = await checkSource(project, {
514
+ websiteOrigin: options['--website-origin'], adminOrigin: options['--admin-origin'],
515
+ siteId: options['--site'], environmentId: options['--environment'], checkRoutes: options['--check-routes'],
516
+ });
517
+ process.stdout.write(`${JSON.stringify(result)}\n`);
518
+ if (!result.checked) process.exitCode = 1;
519
+ return;
520
+ }
118
521
  if (command === 'check') {
119
522
  if (!options['--slug']) throw new SetupError('Check requires --slug with a published page slug.');
120
523
  const env = { CMS_API_KEY: process.env.CMS_API_KEY, CMS_API_BASE_URL: process.env.CMS_API_BASE_URL };