@endora-commerce/cli 0.100.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `endora install <dir>` — one command from nothing to an installed Endora
|
|
3
|
+
* Commerce (`specs/125-first-mile-install/spec.md` §4.3, FR-140…FR-161).
|
|
4
|
+
*
|
|
5
|
+
* ## What it is, in one sentence
|
|
6
|
+
*
|
|
7
|
+
* The two `new` commands write trees and run nothing; between the second of
|
|
8
|
+
* them and a shop there were **nine** further typed steps (§2.3). This verb
|
|
9
|
+
* runs them, and it is deliberately the only place in the product where a
|
|
10
|
+
* scaffold and a pipeline meet.
|
|
11
|
+
*
|
|
12
|
+
* ## It composes, and reimplements nothing (FR-143)
|
|
13
|
+
*
|
|
14
|
+
* `runNewInstance` and `runNewStorefront` are the writers. Nothing here renders
|
|
15
|
+
* a template, resolves a module or decides a member: a second implementation of
|
|
16
|
+
* any of those is D-100's failure mode with a new name, and the case in
|
|
17
|
+
* `test/install.test.ts` that reads this file's own source is what keeps it
|
|
18
|
+
* true. The four flags that belong to `new instance` — `--module`,
|
|
19
|
+
* `--deployment`, `--registry`, `--topology` — are passed through unread.
|
|
20
|
+
*
|
|
21
|
+
* ## When it asks, and when it never does
|
|
22
|
+
*
|
|
23
|
+
* `cli-product.md` R2.5c is a **ceiling**: a prompt may be issued only when
|
|
24
|
+
* both descriptors are TTYs and no `--non-interactive`, `--dry-run` or CI
|
|
25
|
+
* marker is present. Below it, this command asks nothing at all and every
|
|
26
|
+
* missing answer is part of the one refusal — the posture Phase 3 shipped, and
|
|
27
|
+
* unchanged. At a terminal it runs the wizard (`wizard.ts`, spec §6), under
|
|
28
|
+
* R2.5f — 125's PR-1(b), accepted by the owner on 2026-09-25 and placed as
|
|
29
|
+
* D-269. The wizard is a thing that fills flags in: its question set is the
|
|
30
|
+
* flag set, so what it hands the rest of this function is the same options a
|
|
31
|
+
* fully-flagged `--non-interactive` run supplies (SC-107).
|
|
32
|
+
*
|
|
33
|
+
* ## Validate completely, then write — and then, only then, run
|
|
34
|
+
*
|
|
35
|
+
* `runNewInstance`'s discipline (R5.2), one layer up and with a third phase.
|
|
36
|
+
* Every precondition is decided before the first byte is written: the target,
|
|
37
|
+
* the Node version, a package-manager runner, a reachable Docker daemon unless
|
|
38
|
+
* `--no-services`, a reference storefront unless `--no-storefront`, and every
|
|
39
|
+
* answer the pipeline will need. They are reported in **one** refusal naming
|
|
40
|
+
* all of them (FR-157), because a client who has to run a command five times to
|
|
41
|
+
* learn five things has been handed a puzzle.
|
|
42
|
+
*
|
|
43
|
+
* ## Where nothing of ours is installed, it provisions a host first (D-271)
|
|
44
|
+
*
|
|
45
|
+
* `new instance` reads the platform's and every module's manifest off the
|
|
46
|
+
* packages installed beside the target or the working directory, and under
|
|
47
|
+
* `npx` there are none there. So when the platform does not resolve, the first
|
|
48
|
+
* step installs this CLI's release index into a temporary directory
|
|
49
|
+
* (`host.ts`) and `new instance` is called with that directory as its working
|
|
50
|
+
* directory — before the target is written, removed once it has been, kept and
|
|
51
|
+
* named when anything fails. It is not a pipeline step: the instance is not
|
|
52
|
+
* left with it, and a checkout or a pre-installed host never plans it.
|
|
53
|
+
*
|
|
54
|
+
* ## Every step prints the command it is about to run (FR-156)
|
|
55
|
+
*
|
|
56
|
+
* So an operator watching can reproduce any step by hand, and a failure names a
|
|
57
|
+
* command rather than a phase. A failure exits with **that step's own** exit
|
|
58
|
+
* code and prints what is left as a resumable list — the steps are the
|
|
59
|
+
* instance's own root scripts, so the list is a sequence a client can finish by
|
|
60
|
+
* typing.
|
|
61
|
+
*/
|
|
62
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
63
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
64
|
+
import { tmpdir } from 'node:os';
|
|
65
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
66
|
+
import { parseEnvFile, writeEnvFile } from '../inputs/env-file.js';
|
|
67
|
+
import { generateSecret, interactivityOf, mayPrompt, } from '../inputs/resolve.js';
|
|
68
|
+
import { ownReleaseIndexPath } from '../lib/release-index.js';
|
|
69
|
+
import { developmentAddresses, developmentMailUrl, DEV_COMPOSE_PATH, } from '../new-instance/deploy.js';
|
|
70
|
+
import { MEMBER_VOCABULARY, memberRefusal, runNewInstance, } from '../new-instance/index.js';
|
|
71
|
+
import { probePlatform } from '../new-instance/host.js';
|
|
72
|
+
import { runNewStorefront, } from '../new-storefront/index.js';
|
|
73
|
+
import { HOST_DIRECTORY_PREFIX, hostNpmrc, readReleaseIndex, writeHost } from './host.js';
|
|
74
|
+
import { answeredByFlags, answersLine, askWizard, WizardClosedError, } from './wizard.js';
|
|
75
|
+
/** A refusal the operator can act on — exit 1. */
|
|
76
|
+
export class InstallInputError extends Error {
|
|
77
|
+
name = 'InstallInputError';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* An input this run could not read — exit 2 — or a failed host install, which
|
|
81
|
+
* exits with **that step's own** code like every other step (FR-156).
|
|
82
|
+
*/
|
|
83
|
+
export class InstallHostError extends Error {
|
|
84
|
+
name = 'InstallHostError';
|
|
85
|
+
exitCode;
|
|
86
|
+
constructor(message, exitCode = 2) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.exitCode = exitCode;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The lowest Node this command will run an instance on.
|
|
93
|
+
*
|
|
94
|
+
* The **CLI's own** `engines.node`, read from its own manifest, for R2.3's
|
|
95
|
+
* reason: an instance re-declares that value rather than choosing one, so a
|
|
96
|
+
* one-shot that checked a different number would be enforcing a requirement
|
|
97
|
+
* nobody wrote down.
|
|
98
|
+
*/
|
|
99
|
+
function ownEnginesNode() {
|
|
100
|
+
try {
|
|
101
|
+
const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
102
|
+
return typeof manifest.engines?.node === 'string' ? manifest.engines.node : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** `>=22.17.0` against `process.versions.node`, and nothing cleverer. */
|
|
109
|
+
function satisfiesNode(range, version) {
|
|
110
|
+
const wanted = /(\d+)\.(\d+)\.(\d+)/.exec(range);
|
|
111
|
+
if (wanted === null)
|
|
112
|
+
return true;
|
|
113
|
+
const have = /(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
114
|
+
if (have === null)
|
|
115
|
+
return true;
|
|
116
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
117
|
+
const left = Number(have[index]);
|
|
118
|
+
const right = Number(wanted[index]);
|
|
119
|
+
if (left !== right)
|
|
120
|
+
return left > right;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/** Does this command answer at all? A probe, never a version comparison. */
|
|
125
|
+
function probe(command, args) {
|
|
126
|
+
const result = spawnSync(command, [...args], { stdio: 'ignore', timeout: 20_000 });
|
|
127
|
+
return result.error === undefined && result.status === 0;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* How this machine runs `pnpm`, in the declared order (FR-158).
|
|
131
|
+
*
|
|
132
|
+
* `pnpm` on `PATH` first, then `corepack pnpm@<range>` — and **never**
|
|
133
|
+
* `corepack enable`, which is baseline step A1 and a command that writes shims
|
|
134
|
+
* into a directory this program does not own. `corepack pnpm@…` runs the
|
|
135
|
+
* package manager without changing anything about the machine, which is the
|
|
136
|
+
* property that lets this step be a fallback rather than an installation.
|
|
137
|
+
*/
|
|
138
|
+
function resolvePackageManagers() {
|
|
139
|
+
const found = [];
|
|
140
|
+
if (probe('pnpm', ['--version'])) {
|
|
141
|
+
found.push({ command: 'pnpm', prefix: [], label: 'pnpm' });
|
|
142
|
+
}
|
|
143
|
+
if (probe('corepack', ['--version'])) {
|
|
144
|
+
found.push({ command: 'corepack', prefix: ['pnpm@latest'], label: 'corepack pnpm@latest' });
|
|
145
|
+
}
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Is a Docker daemon there? `docker info` answers, `docker --version` does not.
|
|
150
|
+
*
|
|
151
|
+
* **The server version it prints is the answer, not its exit status.** Docker
|
|
152
|
+
* CLI 28 exits 0 with an empty line when no daemon is listening (29 exits 1),
|
|
153
|
+
* so a status-only probe told a client on 28 with Docker stopped that the
|
|
154
|
+
* services step could run. Only a daemon can name its own version.
|
|
155
|
+
*/
|
|
156
|
+
function dockerIsReachable() {
|
|
157
|
+
const result = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], {
|
|
158
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
timeout: 20_000,
|
|
161
|
+
});
|
|
162
|
+
return result.error === undefined && result.status === 0 && result.stdout.trim() !== '';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The refusals, collected.
|
|
166
|
+
*
|
|
167
|
+
* One object, filled completely and then thrown once: FR-157's *"in one
|
|
168
|
+
* refusal naming all of them"*. A `throw` at the first finding is the shape
|
|
169
|
+
* this deliberately is not — it makes the number of runs a client needs equal
|
|
170
|
+
* to the number of things they got wrong.
|
|
171
|
+
*/
|
|
172
|
+
class Refusals {
|
|
173
|
+
found = [];
|
|
174
|
+
add(sentence) {
|
|
175
|
+
this.found.push(sentence);
|
|
176
|
+
}
|
|
177
|
+
get length() {
|
|
178
|
+
return this.found.length;
|
|
179
|
+
}
|
|
180
|
+
throwIfAny() {
|
|
181
|
+
if (this.found.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
throw new InstallInputError(`\`endora install\` cannot run yet — ${String(this.found.length)} thing` +
|
|
184
|
+
`${this.found.length === 1 ? '' : 's'} to settle first:\n` +
|
|
185
|
+
this.found.map((sentence) => ` - ${sentence}`).join('\n') +
|
|
186
|
+
`\n\nNothing was written and nothing was started.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The target must be empty, or hold nothing but a `.env` — `new instance`'s rule. */
|
|
190
|
+
function occupied(dir) {
|
|
191
|
+
if (!existsSync(dir))
|
|
192
|
+
return null;
|
|
193
|
+
const entries = readdirSync(dir);
|
|
194
|
+
if (entries.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
if (entries.length === 1 && entries[0] === '.env')
|
|
197
|
+
return null;
|
|
198
|
+
return entries.slice(0, 5).join(', ');
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Is there a reference storefront to copy, from where this command is standing?
|
|
202
|
+
*
|
|
203
|
+
* **The finding this function exists for**: `endora new storefront` copies the
|
|
204
|
+
* reference storefront out of a **checkout of the platform repository**
|
|
205
|
+
* (`new-storefront/reference.ts`: *"no pnpm-workspace.yaml above …"*), so an
|
|
206
|
+
* installed CLI standing in an empty directory cannot write one however it is
|
|
207
|
+
* asked. The spec's §7.6 assumes otherwise. The honest answer here is to refuse
|
|
208
|
+
* **before** writing an instance, naming the flag that skips the storefront —
|
|
209
|
+
* fail-closed, with a remedy the client can act on — rather than to write half
|
|
210
|
+
* the trio and fail in the middle of the pipeline.
|
|
211
|
+
*/
|
|
212
|
+
function referenceStorefrontRoot(cwd) {
|
|
213
|
+
let dir = resolve(cwd);
|
|
214
|
+
for (;;) {
|
|
215
|
+
if (existsSync(join(dir, 'pnpm-workspace.yaml')))
|
|
216
|
+
return dir;
|
|
217
|
+
const parent = resolve(dir, '..');
|
|
218
|
+
if (parent === dir)
|
|
219
|
+
return null;
|
|
220
|
+
dir = parent;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* FR-105's derivation, performed on the tree this run just wrote.
|
|
225
|
+
*
|
|
226
|
+
* The values are read off **the rendered `compose.dev.yml`** and are written
|
|
227
|
+
* only over names the instance's own `.env` already lists as unanswered
|
|
228
|
+
* placeholders. That is the intersection stated structurally rather than as a
|
|
229
|
+
* list: the placeholders are this instance's declaration, rendered, so an
|
|
230
|
+
* instance that installs neither `search` nor `email` has no line to fill in
|
|
231
|
+
* for either and no name of theirs appears in this file.
|
|
232
|
+
*
|
|
233
|
+
* A value the operator already supplied is never written over — `writeEnvFile`
|
|
234
|
+
* fills a **commented** placeholder in place, and a filled line is not one.
|
|
235
|
+
*/
|
|
236
|
+
function deriveEnvironment(targetDir) {
|
|
237
|
+
const envPath = join(targetDir, '.env');
|
|
238
|
+
const composePath = join(targetDir, DEV_COMPOSE_PATH);
|
|
239
|
+
if (!existsSync(envPath) || !existsSync(composePath))
|
|
240
|
+
return [];
|
|
241
|
+
const text = readFileSync(envPath, 'utf8');
|
|
242
|
+
const answered = parseEnvFile(text);
|
|
243
|
+
const placeholders = new Set([...text.matchAll(/^\s*#\s*([A-Z][A-Z0-9_]*)\s*=/gm)].map((match) => match[1]));
|
|
244
|
+
const addresses = [...developmentAddresses(readFileSync(composePath, 'utf8'))].filter(([name]) => placeholders.has(name) && !answered.has(name));
|
|
245
|
+
if (addresses.length === 0)
|
|
246
|
+
return [];
|
|
247
|
+
// The sentence is the whole of the mitigation, so it names **which** values
|
|
248
|
+
// it is about: they are filled in above, each over its own placeholder and in
|
|
249
|
+
// the declaration's own order, and a comment at the bottom saying "these" and
|
|
250
|
+
// nothing more would send a reader looking for a block that is not there.
|
|
251
|
+
const header = [
|
|
252
|
+
'',
|
|
253
|
+
`# ${addresses.map(([name]) => name).join(', ')} above were DERIVED by \`endora install\``,
|
|
254
|
+
`# from ${DEV_COMPOSE_PATH} beside this file: they are the addresses of the containers`,
|
|
255
|
+
'# `pnpm run dev:services` starts, on THIS machine. Nothing invented them and nothing',
|
|
256
|
+
'# asked you — change a published port in that file and they are what it publishes.',
|
|
257
|
+
'#',
|
|
258
|
+
'# On a machine that is not a development one they are not the values you want: replace',
|
|
259
|
+
'# each with the address of the service you actually run. They fail closed — a database',
|
|
260
|
+
'# that is not there refuses a connection — rather than quietly writing somewhere else.',
|
|
261
|
+
].join('\n');
|
|
262
|
+
const written = writeEnvFile(`${text.replace(/\n+$/, '')}\n${header}\n`, new Map(addresses));
|
|
263
|
+
writeFileSync(envPath, written, 'utf8');
|
|
264
|
+
return addresses.map(([name]) => name);
|
|
265
|
+
}
|
|
266
|
+
/** The storefront's own `.env`, with the values this run owes it (FR-153/154). */
|
|
267
|
+
function storefrontInputs(instance, secret) {
|
|
268
|
+
const port = '3001';
|
|
269
|
+
return {
|
|
270
|
+
NEXT_PUBLIC_API_BASE_URL: `http://localhost:${port}`,
|
|
271
|
+
BACKEND_BASE_URL: `http://localhost:${port}`,
|
|
272
|
+
NEXT_PUBLIC_SITE_URL: 'http://localhost:3000',
|
|
273
|
+
// The platform's own fallback, which is what the instance's default Sales
|
|
274
|
+
// Channel is created with when `DEFAULT_SALES_CHANNEL_CODE` is unset
|
|
275
|
+
// (`kernel/sales-channels/default-channel-reconciler.ts`). A different
|
|
276
|
+
// string here would point the storefront at a channel nothing creates.
|
|
277
|
+
NEXT_PUBLIC_SALES_CHANNEL_CODE: 'default',
|
|
278
|
+
REVALIDATE_SECRET: secret,
|
|
279
|
+
...Object.fromEntries(instance.resolved
|
|
280
|
+
.filter((entry) => entry.name === 'DEFAULT_SALES_CHANNEL_CODE')
|
|
281
|
+
.map((entry) => ['NEXT_PUBLIC_SALES_CHANNEL_CODE', entry.value])),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The whole command.
|
|
286
|
+
*
|
|
287
|
+
* Three phases, in this order and never interleaved: **decide** (every
|
|
288
|
+
* precondition, one refusal), **write** (the two scaffolds and the derived
|
|
289
|
+
* `.env`), **run** (the pipeline, each step echoed). A `--dry-run` stops after
|
|
290
|
+
* the first and reports the other two.
|
|
291
|
+
*/
|
|
292
|
+
export async function runInstall(given) {
|
|
293
|
+
const cwd = given.cwd ?? process.cwd();
|
|
294
|
+
const output = [];
|
|
295
|
+
const say = (line) => {
|
|
296
|
+
output.push(line);
|
|
297
|
+
given.echo?.(line);
|
|
298
|
+
};
|
|
299
|
+
// ── ask, only where R2.5c's conjunction holds (R2.5f i) ───────────────────
|
|
300
|
+
const interactive = mayPrompt(given.interactivity ??
|
|
301
|
+
interactivityOf({
|
|
302
|
+
nonInteractive: given.nonInteractive === true,
|
|
303
|
+
dryRun: given.dryRun === true,
|
|
304
|
+
}));
|
|
305
|
+
let options = given;
|
|
306
|
+
let provenance;
|
|
307
|
+
if (interactive) {
|
|
308
|
+
try {
|
|
309
|
+
const outcome = await askWizard(given, given.io ?? { input: process.stdin, output: process.stdout, terminal: true }, {
|
|
310
|
+
vocabulary: MEMBER_VOCABULARY,
|
|
311
|
+
storefront: {
|
|
312
|
+
available: referenceStorefrontRoot(cwd) !== null,
|
|
313
|
+
reason: 'not from here: it is copied out of a checkout of the platform repository, ' +
|
|
314
|
+
'and there is none above this directory — `endora new storefront` adds it later',
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
options = { ...given, ...outcome.answers };
|
|
318
|
+
provenance = outcome;
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error instanceof WizardClosedError)
|
|
322
|
+
throw new InstallInputError(error.message);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// Nothing asked: every answer is a flag, and the two offered questions a
|
|
328
|
+
// flag did not answer take §6.2's recommendation — *everything*, *yes* —
|
|
329
|
+
// which is what Phase 3 already did and is now said out loud.
|
|
330
|
+
const fromFlags = answeredByFlags(given);
|
|
331
|
+
provenance = {
|
|
332
|
+
fromFlags,
|
|
333
|
+
prompted: [],
|
|
334
|
+
recommended: ['parts', 'services'].filter((id) => !fromFlags.has(id)),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// ── decide ────────────────────────────────────────────────────────────────
|
|
338
|
+
const refusals = new Refusals();
|
|
339
|
+
// A missing directory is one refusal among the others rather than the first
|
|
340
|
+
// and only one: a run with no answers at all is told everything it owes in
|
|
341
|
+
// one message (FR-157, R3.2), and the checks that need a directory are the
|
|
342
|
+
// only ones it skips.
|
|
343
|
+
const namedDir = options.dir === undefined || options.dir.trim().length === 0
|
|
344
|
+
? null
|
|
345
|
+
: isAbsolute(options.dir)
|
|
346
|
+
? options.dir
|
|
347
|
+
: resolve(cwd, options.dir);
|
|
348
|
+
if (namedDir === null) {
|
|
349
|
+
refusals.add('`endora install` takes the directory to write — `<dir>` — and outside the wizard has no ' +
|
|
350
|
+
"default: the directory's basename becomes the workspace name, so a default would " +
|
|
351
|
+
'invent a name nobody chose.');
|
|
352
|
+
}
|
|
353
|
+
const wantsStorefront = options.storefront !== false;
|
|
354
|
+
const wantsServices = options.services !== false;
|
|
355
|
+
const namedStorefrontDir = !wantsStorefront || namedDir === null
|
|
356
|
+
? null
|
|
357
|
+
: options.storefrontDir === undefined
|
|
358
|
+
? `${namedDir}-storefront`
|
|
359
|
+
: isAbsolute(options.storefrontDir)
|
|
360
|
+
? options.storefrontDir
|
|
361
|
+
: resolve(cwd, options.storefrontDir);
|
|
362
|
+
const declinedRefusal = memberRefusal(options.without ?? []);
|
|
363
|
+
if (declinedRefusal !== null)
|
|
364
|
+
refusals.add(declinedRefusal);
|
|
365
|
+
const inTheWay = namedDir === null ? null : occupied(namedDir);
|
|
366
|
+
if (inTheWay !== null) {
|
|
367
|
+
refusals.add(`${namedDir} exists and is not empty (${inTheWay}). An instance's files are yours; ` +
|
|
368
|
+
'this command never merges into a directory. Scaffold into an empty one — a ' +
|
|
369
|
+
'directory holding nothing but a `.env` is the exception, and that file is read as ' +
|
|
370
|
+
'your own answers.');
|
|
371
|
+
}
|
|
372
|
+
// FR-160 — a sibling, never a child. A storefront inside the instance would
|
|
373
|
+
// be swept into its `pnpm-workspace.yaml` globs and become a member of a
|
|
374
|
+
// workspace it is not part of, which is D-230's topology broken by a
|
|
375
|
+
// directory choice.
|
|
376
|
+
if (namedStorefrontDir !== null && namedStorefrontDir.startsWith(namedDir + sep)) {
|
|
377
|
+
refusals.add(`--storefront-dir ${namedStorefrontDir} is inside ${namedDir}. The storefront is its own ` +
|
|
378
|
+
"repository (D-195): written there it would be swept into the instance's workspace " +
|
|
379
|
+
'globs and become a member of a workspace it is not part of. Put it beside the ' +
|
|
380
|
+
'instance — the default is `<dir>-storefront`.');
|
|
381
|
+
}
|
|
382
|
+
if (namedStorefrontDir !== null) {
|
|
383
|
+
const storefrontInTheWay = occupied(namedStorefrontDir);
|
|
384
|
+
if (storefrontInTheWay !== null) {
|
|
385
|
+
refusals.add(`${namedStorefrontDir} exists and is not empty (${storefrontInTheWay}). Pass ` +
|
|
386
|
+
'`--storefront-dir <path>` for another directory, or `--no-storefront` to write no ' +
|
|
387
|
+
'storefront at all.');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const engines = ownEnginesNode();
|
|
391
|
+
if (engines !== undefined && !satisfiesNode(engines, process.versions.node)) {
|
|
392
|
+
refusals.add(`this build needs Node ${engines} and is running on ${process.versions.node}. The ` +
|
|
393
|
+
'instance it writes declares the same range, so a run on this one would install a ' +
|
|
394
|
+
'tree its own manifest refuses.');
|
|
395
|
+
}
|
|
396
|
+
const runners = options.packageManagers ?? resolvePackageManagers();
|
|
397
|
+
if (runners.length === 0) {
|
|
398
|
+
refusals.add('no package manager to run: `pnpm` is not on PATH and `corepack` is not either. ' +
|
|
399
|
+
'Install pnpm (`npm i -g pnpm`), or use a Node that ships corepack — this command ' +
|
|
400
|
+
'runs `corepack pnpm@latest` when it has to, and never `corepack enable`, which ' +
|
|
401
|
+
'writes shims into a directory it does not own.');
|
|
402
|
+
}
|
|
403
|
+
if (wantsServices && (options.dockerReachable ?? dockerIsReachable()) === false) {
|
|
404
|
+
refusals.add('no Docker daemon answered. The development stack — PostgreSQL, Redis, Meilisearch ' +
|
|
405
|
+
'and a mail catcher — is started with `docker compose`, so this run has nothing to ' +
|
|
406
|
+
'migrate against. Start Docker, or pass `--no-services` and point the instance at ' +
|
|
407
|
+
'services you run yourself.');
|
|
408
|
+
}
|
|
409
|
+
if (wantsStorefront && referenceStorefrontRoot(cwd) === null) {
|
|
410
|
+
refusals.add('`endora new storefront` copies the reference storefront out of a checkout of the ' +
|
|
411
|
+
`platform repository, and there is none above ${cwd}. Run this from inside a ` +
|
|
412
|
+
'checkout to get both trees, or pass `--no-storefront` to write the instance alone ' +
|
|
413
|
+
'— the storefront can be added later with `endora new storefront <dir>`.');
|
|
414
|
+
}
|
|
415
|
+
const admin = {
|
|
416
|
+
email: options.adminEmail,
|
|
417
|
+
password: options.adminPassword,
|
|
418
|
+
firstName: options.adminFirstName,
|
|
419
|
+
lastName: options.adminLastName,
|
|
420
|
+
};
|
|
421
|
+
const missingAdmin = [
|
|
422
|
+
['--admin-email', admin.email],
|
|
423
|
+
['--admin-password', admin.password],
|
|
424
|
+
['--admin-first-name', admin.firstName],
|
|
425
|
+
['--admin-last-name', admin.lastName],
|
|
426
|
+
].filter(([, value]) => value === undefined || value.trim().length === 0);
|
|
427
|
+
if (missingAdmin.length > 0) {
|
|
428
|
+
// FR-159 — a generated password is not permitted: `generable` requires that
|
|
429
|
+
// two correct values are interchangeable, and a password the operator has
|
|
430
|
+
// to remember is not one of those.
|
|
431
|
+
refusals.add(`the administrator is missing ${missingAdmin.map(([flag]) => flag).join(', ')}. ` +
|
|
432
|
+
'Nothing else creates one — a freshly migrated instance has no account at all — and ' +
|
|
433
|
+
'the password is never generated: it is the one value you have to remember.');
|
|
434
|
+
}
|
|
435
|
+
if (options.demo === undefined) {
|
|
436
|
+
// FR-124 — required, by 125 PR-2 option (c) as ruled (D-269).
|
|
437
|
+
refusals.add('say whether to install demo data: `--demo` seeds every installed module\'s example ' +
|
|
438
|
+
'rows, `--no-demo` seeds none. There is deliberately no default — an instance you ' +
|
|
439
|
+
'are going to sell from wants none of it and one you are evaluating wants it before ' +
|
|
440
|
+
'the first screen, and picking for you would be picking wrong for one of them.');
|
|
441
|
+
}
|
|
442
|
+
refusals.throwIfAny();
|
|
443
|
+
// Both are decided: a missing directory was a refusal above.
|
|
444
|
+
const targetDir = namedDir;
|
|
445
|
+
const storefrontDir = namedStorefrontDir;
|
|
446
|
+
const runner = runners[0];
|
|
447
|
+
const dryRun = options.dryRun === true;
|
|
448
|
+
const run = options.run ?? spawnStep;
|
|
449
|
+
// ── provision, only where nothing of ours resolves (D-271) ───────────────
|
|
450
|
+
// Decided before anything is written, like every other input: an index this
|
|
451
|
+
// build cannot read, or a `--registry` that is not one, refuses here.
|
|
452
|
+
const hostStep = planHost({
|
|
453
|
+
cwd,
|
|
454
|
+
targetDir,
|
|
455
|
+
runner,
|
|
456
|
+
registry: options.registry,
|
|
457
|
+
releaseIndexFile: options.releaseIndexFile,
|
|
458
|
+
});
|
|
459
|
+
// ── write ─────────────────────────────────────────────────────────────────
|
|
460
|
+
say(`endora install ${targetDir}${dryRun ? ' — dry run, nothing written' : ''}`);
|
|
461
|
+
const answers = answersLine(provenance);
|
|
462
|
+
say(` ${answers}`);
|
|
463
|
+
if (hostStep !== null) {
|
|
464
|
+
const keptLine = ` the temporary host is kept at ${hostStep.cwd} so you can read what its install ` +
|
|
465
|
+
'left; delete it when you are done.';
|
|
466
|
+
say(`\n[host] ${hostStep.command} # in ${hostStep.cwd}`);
|
|
467
|
+
say(` ${hostStep.purpose}`);
|
|
468
|
+
const code = await run(hostStep);
|
|
469
|
+
if (code !== 0) {
|
|
470
|
+
say(keptLine);
|
|
471
|
+
throw new InstallHostError(`${hostStep.command} failed (exit ${String(code)}) in the temporary host ` +
|
|
472
|
+
`${hostStep.cwd}, so no package of this release could be read and nothing was ` +
|
|
473
|
+
`written to ${targetDir}. The host is kept there so you can read what its install ` +
|
|
474
|
+
'left. If the registry needs configuration, pass `--registry <url>`; then run this ' +
|
|
475
|
+
'command again.', code);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const without = (options.without ?? []).filter((name) => name.trim().length > 0);
|
|
479
|
+
let instance;
|
|
480
|
+
try {
|
|
481
|
+
instance = await runNewInstance({
|
|
482
|
+
dir: targetDir,
|
|
483
|
+
...(options.modules === undefined ? {} : { modules: options.modules }),
|
|
484
|
+
// R6.3b — nothing at all when every member is wanted, so the common case
|
|
485
|
+
// is the argv a bare `endora new instance <dir>` has.
|
|
486
|
+
...(without.length === 0 ? {} : { without }),
|
|
487
|
+
...(options.deployment === undefined ? {} : { deployment: options.deployment }),
|
|
488
|
+
...(options.registry === undefined ? {} : { registry: options.registry }),
|
|
489
|
+
...(options.topology === undefined ? {} : { topology: options.topology }),
|
|
490
|
+
// D-270 — with no `--module`, every module of the open-source set this
|
|
491
|
+
// run resolved rather than the smallest set. A policy, not a list: which
|
|
492
|
+
// modules that is stays `new instance`'s to derive (FR-143).
|
|
493
|
+
moduleSeed: 'available',
|
|
494
|
+
dryRun,
|
|
495
|
+
// The host, when there is one, is only a place to look: every resolution
|
|
496
|
+
// is `new instance`'s, unchanged (D-271 clause 1.1).
|
|
497
|
+
cwd: hostStep?.cwd ?? cwd,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (hostStep !== null) {
|
|
502
|
+
say(` the temporary host is kept at ${hostStep.cwd} so you can read what it holds; ` +
|
|
503
|
+
'delete it when you are done.');
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
if (hostStep !== null) {
|
|
508
|
+
rmSync(hostStep.cwd, { recursive: true, force: true });
|
|
509
|
+
say(` removed the temporary host at ${hostStep.cwd}` +
|
|
510
|
+
(dryRun ? ` — it was provisioned to read the module set; nothing was written to ${targetDir}` : ''));
|
|
511
|
+
}
|
|
512
|
+
say(` ${dryRun ? 'would write' : 'wrote'} ${String(instance.plan.files.length)} files across ` +
|
|
513
|
+
`${instance.plan.members.join(', ')} — ${instance.modules.ids.length} module(s)`);
|
|
514
|
+
say(` ${instance.provenance}`);
|
|
515
|
+
// FR-153 — generated **once**, for two trees. See `revalidateSecret` above.
|
|
516
|
+
const secret = options.revalidateSecret ?? (dryRun ? '<generated on a real run>' : generateSecret());
|
|
517
|
+
let storefront = null;
|
|
518
|
+
if (storefrontDir !== null) {
|
|
519
|
+
storefront = await runNewStorefront({
|
|
520
|
+
dir: storefrontDir,
|
|
521
|
+
cwd,
|
|
522
|
+
dryRun,
|
|
523
|
+
nonInteractive: true,
|
|
524
|
+
inputs: storefrontInputs(instance, secret),
|
|
525
|
+
});
|
|
526
|
+
say(` ${dryRun ? 'would write' : 'wrote'} the storefront at ${storefrontDir} — ` +
|
|
527
|
+
`${String(storefront.plan.files.length)} files`);
|
|
528
|
+
say(` ${storefront.provenance}`);
|
|
529
|
+
}
|
|
530
|
+
const derived = dryRun || !wantsServices ? [] : deriveEnvironment(targetDir);
|
|
531
|
+
if (derived.length > 0) {
|
|
532
|
+
say(` derived from ${DEV_COMPOSE_PATH} into .env: ${derived.join(', ')} — the addresses of ` +
|
|
533
|
+
'the containers the next step starts, on this machine');
|
|
534
|
+
}
|
|
535
|
+
if (!dryRun && storefrontDir !== null && options.revalidateSecret === undefined) {
|
|
536
|
+
writeSharedSecret(targetDir, secret);
|
|
537
|
+
say(' generated REVALIDATE_SECRET once and wrote it into both trees — the one value no ' +
|
|
538
|
+
'sequence of `endora new instance` and `endora new storefront` can agree on');
|
|
539
|
+
}
|
|
540
|
+
// ── run ───────────────────────────────────────────────────────────────────
|
|
541
|
+
const steps = plan({
|
|
542
|
+
targetDir,
|
|
543
|
+
storefrontDir,
|
|
544
|
+
runner,
|
|
545
|
+
services: wantsServices,
|
|
546
|
+
demo: options.demo === true,
|
|
547
|
+
admin,
|
|
548
|
+
});
|
|
549
|
+
let exitCode = 0;
|
|
550
|
+
const done = [];
|
|
551
|
+
if (!dryRun) {
|
|
552
|
+
for (const step of steps) {
|
|
553
|
+
say(`\n[${String(done.length + 1)}/${String(steps.length)}] ${step.command}`);
|
|
554
|
+
say(` ${step.purpose}`);
|
|
555
|
+
const code = await run(step);
|
|
556
|
+
done.push(step);
|
|
557
|
+
if (code === 0)
|
|
558
|
+
continue;
|
|
559
|
+
if (step.optional === true) {
|
|
560
|
+
say(` that failed (exit ${String(code)}) and the install is complete without it. ` +
|
|
561
|
+
`Retry with \`${step.command}\`, and \`pnpm run cli demo reset\` withdraws it again.`);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
exitCode = code;
|
|
565
|
+
say(`\n${step.command} failed (exit ${String(code)}). Remaining steps, in order:`);
|
|
566
|
+
for (const left of steps.slice(done.length))
|
|
567
|
+
say(` ${left.command} # ${left.purpose}`);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
say('\nThe pipeline it would run, in order:');
|
|
573
|
+
for (const step of steps)
|
|
574
|
+
say(` ${step.command} # ${step.purpose}`);
|
|
575
|
+
}
|
|
576
|
+
if (exitCode === 0) {
|
|
577
|
+
for (const line of closing({
|
|
578
|
+
targetDir,
|
|
579
|
+
storefrontDir,
|
|
580
|
+
admin,
|
|
581
|
+
instance,
|
|
582
|
+
dryRun,
|
|
583
|
+
demo: options.demo === true,
|
|
584
|
+
recommended: provenance.recommended,
|
|
585
|
+
services: wantsServices,
|
|
586
|
+
passwordFromFlag: provenance.fromFlags.has('admin-password'),
|
|
587
|
+
})) {
|
|
588
|
+
say(line);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
targetDir,
|
|
593
|
+
storefrontDir,
|
|
594
|
+
instance,
|
|
595
|
+
storefront,
|
|
596
|
+
steps,
|
|
597
|
+
hostStep,
|
|
598
|
+
output,
|
|
599
|
+
exitCode,
|
|
600
|
+
derived,
|
|
601
|
+
answers,
|
|
602
|
+
dryRun,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* The temporary host step, or `null` when the platform already resolves.
|
|
607
|
+
*
|
|
608
|
+
* D-271: provisioned **exactly** when `@endora-commerce/platform` does not
|
|
609
|
+
* resolve beside the target or the working directory — a checkout, the
|
|
610
|
+
* acceptance harness and a client who installed the packages beside all plan
|
|
611
|
+
* none, and run exactly as before. Its files are written here, before the
|
|
612
|
+
* header is printed, because a release index this build cannot read is an input
|
|
613
|
+
* the run could not read and is refused before anything is started.
|
|
614
|
+
*/
|
|
615
|
+
function planHost(input) {
|
|
616
|
+
const { scope, resolves } = probePlatform({ cwd: input.cwd, targetDir: input.targetDir });
|
|
617
|
+
if (resolves)
|
|
618
|
+
return null;
|
|
619
|
+
const file = input.releaseIndexFile ?? ownReleaseIndexPath();
|
|
620
|
+
const read = readReleaseIndex(file);
|
|
621
|
+
if ('problem' in read) {
|
|
622
|
+
throw new InstallHostError(`${scope}platform does not resolve from ${input.targetDir} or ${input.cwd}, and this ` +
|
|
623
|
+
`build's release index — the packages it would install to read the module set from — ` +
|
|
624
|
+
`could not be read: ${read.problem}. Nothing was written. Reinstall ` +
|
|
625
|
+
`\`${scope}cli\`, or install the platform beside the target directory and run this ` +
|
|
626
|
+
'command again.');
|
|
627
|
+
}
|
|
628
|
+
const npmrc = hostNpmrc(input.registry, scope);
|
|
629
|
+
const dir = mkdtempSync(join(tmpdir(), HOST_DIRECTORY_PREFIX));
|
|
630
|
+
writeHost(dir, read.index, npmrc);
|
|
631
|
+
const argv = ['install'];
|
|
632
|
+
return {
|
|
633
|
+
id: 'host',
|
|
634
|
+
command: `${input.runner.label} ${argv.join(' ')}`,
|
|
635
|
+
purpose: `nothing of ours is installed beside ${input.targetDir}, so every package of this ` +
|
|
636
|
+
`release (${String(read.index.packages.length)}) is installed into a temporary host to ` +
|
|
637
|
+
'read the module manifests from. It is removed when the instance is written.',
|
|
638
|
+
bin: input.runner.command,
|
|
639
|
+
argv: [...input.runner.prefix, ...argv],
|
|
640
|
+
cwd: dir,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* The shared secret, into the instance's own `.env`.
|
|
645
|
+
*
|
|
646
|
+
* The storefront gets it as a resolved input — it declares the variable, so its
|
|
647
|
+
* own resolution writes it. The instance's `.env` lists it as a placeholder
|
|
648
|
+
* (the platform declares it `generable: false`, so `new instance` writes no
|
|
649
|
+
* value), and this is where the second half of the pair is filled in.
|
|
650
|
+
*/
|
|
651
|
+
function writeSharedSecret(targetDir, secret) {
|
|
652
|
+
const envPath = join(targetDir, '.env');
|
|
653
|
+
if (!existsSync(envPath))
|
|
654
|
+
return;
|
|
655
|
+
const text = readFileSync(envPath, 'utf8');
|
|
656
|
+
if (!/^\s*#?\s*REVALIDATE_SECRET\s*=/m.test(text))
|
|
657
|
+
return;
|
|
658
|
+
if (parseEnvFile(text).has('REVALIDATE_SECRET'))
|
|
659
|
+
return;
|
|
660
|
+
writeFileSync(envPath, writeEnvFile(text, new Map([['REVALIDATE_SECRET', secret]])), 'utf8');
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The pipeline (FR-155).
|
|
664
|
+
*
|
|
665
|
+
* It is the printed next-steps sequence and nothing else, and it runs the
|
|
666
|
+
* instance's **own root scripts** rather than their contents: `setup` is the
|
|
667
|
+
* composite the block names, so a client reproducing this by hand types the
|
|
668
|
+
* same four words. A step this list has that the block does not would make the
|
|
669
|
+
* two paths two products.
|
|
670
|
+
*/
|
|
671
|
+
function plan(input) {
|
|
672
|
+
const { runner } = input;
|
|
673
|
+
const step = (id, cwd, argv, purpose, optional = false) => ({
|
|
674
|
+
id,
|
|
675
|
+
command: `${runner.label} ${argv.join(' ')}`.trim(),
|
|
676
|
+
purpose,
|
|
677
|
+
bin: runner.command,
|
|
678
|
+
argv: [...runner.prefix, ...argv],
|
|
679
|
+
cwd,
|
|
680
|
+
...(optional ? { optional: true } : {}),
|
|
681
|
+
});
|
|
682
|
+
const steps = [
|
|
683
|
+
step('install', input.targetDir, ['install'], 'the platform, the admin shell and every module you declared, from the registry'),
|
|
684
|
+
];
|
|
685
|
+
if (input.services) {
|
|
686
|
+
steps.push(step('services', input.targetDir, ['run', 'dev:services'], 'PostgreSQL, Redis, Meilisearch and a mail catcher, waited for until each is healthy'));
|
|
687
|
+
}
|
|
688
|
+
steps.push(step('setup', input.targetDir, ['run', 'setup'], 'generate, build, migrate and install every module — the four steps, in order'), step('admin', input.targetDir, [
|
|
689
|
+
'run',
|
|
690
|
+
'admin:create',
|
|
691
|
+
'--',
|
|
692
|
+
`--email=${input.admin.email ?? ''}`,
|
|
693
|
+
`--password=${input.admin.password ?? ''}`,
|
|
694
|
+
`--first-name=${input.admin.firstName ?? ''}`,
|
|
695
|
+
`--last-name=${input.admin.lastName ?? ''}`,
|
|
696
|
+
], 'the administrator you sign in as. Nothing else creates one'));
|
|
697
|
+
if (input.demo) {
|
|
698
|
+
// FR-126 — after `module:install --all` and after the administrator, and
|
|
699
|
+
// non-fatal. The reverse ordering would make an optional extra able to red
|
|
700
|
+
// a mandatory outcome.
|
|
701
|
+
steps.push(step('demo', input.targetDir, ['run', 'cli', 'demo', 'seed'], "every installed module's example rows, withdrawable with `cli demo reset`", true));
|
|
702
|
+
}
|
|
703
|
+
if (input.storefrontDir !== null) {
|
|
704
|
+
steps.push(step('storefront-install', input.storefrontDir, ['install'], 'the storefront\'s own dependencies, in its own repository'));
|
|
705
|
+
}
|
|
706
|
+
return steps;
|
|
707
|
+
}
|
|
708
|
+
/** What a client reads when it worked: where things are, and what is next. */
|
|
709
|
+
function closing(input) {
|
|
710
|
+
const lines = ['', input.dryRun ? 'It would then be yours to start:' : 'Done. To start it:'];
|
|
711
|
+
// One command first (`specs/136-open-source-publication/` GAP-7, FR-060):
|
|
712
|
+
// the supervisor over the per-layer commands below, which stay printed for
|
|
713
|
+
// the operator who wants a layer on its own. A storefront somewhere other
|
|
714
|
+
// than the default sibling is named, since `dev:all` looks for the default.
|
|
715
|
+
const defaultStorefront = `${input.targetDir}-storefront`;
|
|
716
|
+
const devAllArgs = input.storefrontDir !== null && input.storefrontDir !== defaultStorefront
|
|
717
|
+
? ` -- --storefront-dir ${input.storefrontDir}`
|
|
718
|
+
: '';
|
|
719
|
+
const rootManifest = input.instance.plan.files.find((file) => file.path === 'package.json');
|
|
720
|
+
const rootScripts = rootManifest === undefined
|
|
721
|
+
? {}
|
|
722
|
+
: (JSON.parse(rootManifest.content).scripts ?? {});
|
|
723
|
+
if (typeof rootScripts['dev:all'] === 'string') {
|
|
724
|
+
lines.push(` cd ${input.targetDir} && pnpm run dev:all${devAllArgs} # every layer, one terminal; Ctrl-C stops them`, '', 'Or one layer at a time:');
|
|
725
|
+
}
|
|
726
|
+
lines.push(` cd ${input.targetDir} && pnpm run start # the API, on http://localhost:3001`);
|
|
727
|
+
if (input.instance.plan.members.includes('admin')) {
|
|
728
|
+
lines.push(' pnpm run preview:admin # the admin bundle, in a second terminal');
|
|
729
|
+
}
|
|
730
|
+
if (input.storefrontDir !== null) {
|
|
731
|
+
lines.push(` cd ${input.storefrontDir} && pnpm run build && pnpm run start # the shop`);
|
|
732
|
+
}
|
|
733
|
+
const mail = developmentMailUrl(existsSync(join(input.targetDir, DEV_COMPOSE_PATH))
|
|
734
|
+
? readFileSync(join(input.targetDir, DEV_COMPOSE_PATH), 'utf8')
|
|
735
|
+
: '');
|
|
736
|
+
if (mail !== undefined) {
|
|
737
|
+
lines.push(` mail this instance sends is caught at ${mail} and leaves your machine never`);
|
|
738
|
+
}
|
|
739
|
+
lines.push('', `Sign in as ${input.admin.email ?? ''} with the password you ` +
|
|
740
|
+
`${input.passwordFromFlag ? 'passed on the command line' : 'entered above'}.`, input.demo
|
|
741
|
+
? `Demo data ${input.dryRun ? 'would be' : 'was'} seeded. \`pnpm run cli demo reset\` withdraws it ` +
|
|
742
|
+
'and leaves your own rows alone.'
|
|
743
|
+
: `No demo data ${input.dryRun ? 'would be' : 'was'} seeded. \`pnpm run cli demo seed\` adds a ` +
|
|
744
|
+
"shop's worth of it, and `demo reset` withdraws it again.");
|
|
745
|
+
// D-270 — the module set is not a question and not an input, so it is not in
|
|
746
|
+
// the `[answers]` or `[inputs]` line; it is a derived value, and the run says
|
|
747
|
+
// what it derived and how to take any of it back. Off is non-destructive
|
|
748
|
+
// (Constitution XVII), which is what makes "everything" the safe default.
|
|
749
|
+
if (input.instance.modules.defaulted) {
|
|
750
|
+
const count = input.instance.modules.ids.length;
|
|
751
|
+
lines.push('', `${String(count)} module${count === 1 ? '' : 's'} ${input.dryRun ? 'would be' : 'were'} ` +
|
|
752
|
+
'installed, because no `--module` was given: every module of the open-source set ' +
|
|
753
|
+
'this run found, each switched on.', 'Switch any of them off in the admin under Modules (/platform/modules). Off keeps its ' +
|
|
754
|
+
'data, and switching it back on brings it back.');
|
|
755
|
+
}
|
|
756
|
+
// R2.5f (iv) — every answer taken as a recommendation, said to be one, with
|
|
757
|
+
// what reverses it. A recommendation nobody is told they accepted is a
|
|
758
|
+
// default with better manners.
|
|
759
|
+
const reversals = {
|
|
760
|
+
directory: `the directory ${input.targetDir} — pass another as \`endora install <dir>\``,
|
|
761
|
+
parts: 'every part this build can write — `--without <member>` and `--no-storefront` leave one ' +
|
|
762
|
+
'out of the next install',
|
|
763
|
+
services: input.services
|
|
764
|
+
? 'the development services, started — `pnpm run dev:services:down` stops them'
|
|
765
|
+
: null,
|
|
766
|
+
demo: null,
|
|
767
|
+
'admin-email': null,
|
|
768
|
+
'admin-password': null,
|
|
769
|
+
'admin-name': null,
|
|
770
|
+
};
|
|
771
|
+
const taken = input.recommended
|
|
772
|
+
.map((id) => reversals[id])
|
|
773
|
+
.filter((line) => line !== null);
|
|
774
|
+
if (taken.length > 0) {
|
|
775
|
+
lines.push('', 'Recommended, and taken because nothing said otherwise:');
|
|
776
|
+
for (const line of taken)
|
|
777
|
+
lines.push(` ${line}`);
|
|
778
|
+
}
|
|
779
|
+
return lines;
|
|
780
|
+
}
|
|
781
|
+
/** The default runner: spawn it, inherit the descriptors, answer its code. */
|
|
782
|
+
function spawnStep(step) {
|
|
783
|
+
return new Promise((resolveCode) => {
|
|
784
|
+
const child = spawn(step.bin, [...step.argv], {
|
|
785
|
+
cwd: step.cwd,
|
|
786
|
+
stdio: 'inherit',
|
|
787
|
+
env: process.env,
|
|
788
|
+
});
|
|
789
|
+
child.on('error', () => resolveCode(127));
|
|
790
|
+
child.on('close', (code) => resolveCode(code ?? 1));
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=index.js.map
|