aw-spec-cli 0.1.2 → 0.1.4

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 (77) hide show
  1. package/README.md +291 -0
  2. package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
  3. package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +5726 -0
  4. package/assets/docs/ModularMonolith/pack-foundation.ps1 +145 -0
  5. package/assets/docs/ModularMonolith/pack-foundation.sh +196 -0
  6. package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +157 -0
  7. package/assets/docs/ModularMonolith/pack-reference-foundation.sh +209 -0
  8. package/assets/docs/ModularMonolith/unpack-foundation.ps1 +126 -0
  9. package/assets/docs/ModularMonolith/unpack-foundation.sh +217 -0
  10. package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
  11. package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +218 -0
  12. package/assets/docs/UI.Visualization/PACKAGING.md +191 -0
  13. package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +11768 -0
  14. package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +11214 -0
  15. package/assets/docs/UI.Visualization/pack-foundation.ps1 +165 -0
  16. package/assets/docs/UI.Visualization/pack-foundation.sh +206 -0
  17. package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +159 -0
  18. package/assets/docs/UI.Visualization/pack-reference-foundation.sh +205 -0
  19. package/assets/docs/UI.Visualization/unpack-foundation.ps1 +126 -0
  20. package/assets/docs/UI.Visualization/unpack-foundation.sh +217 -0
  21. package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +129 -0
  22. package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +218 -0
  23. package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
  24. package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +157 -0
  25. package/assets/docs/VerticalSlice/pack-reference-foundation.sh +209 -0
  26. package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
  27. package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +218 -0
  28. package/assets/docs/components/Email/README.md +7 -0
  29. package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
  30. package/assets/docs/components/Email/integration.md +57 -17
  31. package/assets/docs/components/Jobs/integration.md +29 -15
  32. package/assets/docs/components/QrGenerator/README.md +7 -0
  33. package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
  34. package/assets/docs/components/QrGenerator/integration.md +51 -6
  35. package/assets/docs/components/README.md +19 -2
  36. package/assets/docs/domainspecs/UIVisualization/chart-bubble.md +70 -0
  37. package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +58 -0
  38. package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +62 -0
  39. package/assets/docs/domainspecs/UIVisualization/chart-network.md +69 -0
  40. package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +75 -0
  41. package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +66 -0
  42. package/assets/docs/domainspecs/UIVisualization/chart-survival.md +69 -0
  43. package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +68 -0
  44. package/assets/docs/domainspecs/UIVisualization/chart-violin.md +61 -0
  45. package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +63 -0
  46. package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +80 -0
  47. package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +87 -0
  48. package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +47 -0
  49. package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +130 -0
  50. package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +67 -0
  51. package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +68 -0
  52. package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +110 -0
  53. package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +55 -0
  54. package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +67 -0
  55. package/assets/docs/domainspecs/UIVisualization/testing.domain.md +52 -0
  56. package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +39 -0
  57. package/assets/docs/domainspecs/healthcare/comandrulemap.md +47 -0
  58. package/assets/docs/domainspecs/pharma/comandrulemap.md +50 -0
  59. package/dist/commands/init.d.ts.map +1 -1
  60. package/dist/commands/init.js +54 -7
  61. package/dist/commands/init.js.map +1 -1
  62. package/dist/commands/update.d.ts +3 -0
  63. package/dist/commands/update.d.ts.map +1 -0
  64. package/dist/commands/update.js +142 -0
  65. package/dist/commands/update.js.map +1 -0
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/utils/manifest.d.ts +31 -0
  69. package/dist/utils/manifest.d.ts.map +1 -0
  70. package/dist/utils/manifest.js +91 -0
  71. package/dist/utils/manifest.js.map +1 -0
  72. package/dist/utils/update-engine.d.ts +52 -0
  73. package/dist/utils/update-engine.d.ts.map +1 -0
  74. package/dist/utils/update-engine.js +203 -0
  75. package/dist/utils/update-engine.js.map +1 -0
  76. package/package.json +5 -2
  77. package/assets/docs/requirement-spec.txt +0 -15
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # unpack-reference-foundation.sh — bash/macOS port of unpack-reference-foundation.ps1
4
+ #
5
+ # SYNOPSIS
6
+ # Unpack an AW-Spec reference pack file into a target project, renaming the
7
+ # sentinel app name to the caller's app name.
8
+ #
9
+ # DESCRIPTION
10
+ # Reads --pack-file (produced by pack-reference-foundation.sh/.ps1), takes the sentinel
11
+ # from its header (override with --sentinel), derives four case forms of the target app
12
+ # name, and writes every file block to --out-dir with the sentinel rewritten in both file
13
+ # CONTENTS and file/dir PATHS. Text blocks are rewritten and written as UTF-8; base64
14
+ # blocks are decoded as-is. Refuses to overwrite existing files unless --force.
15
+ #
16
+ # EXAMPLE
17
+ # ./unpack-reference-foundation.sh \
18
+ # --pack-file ./VerticalSlice.Reference.pack.txt \
19
+ # --target-app-name AppWeaver.Pharma \
20
+ # --out-dir ../../
21
+ #
22
+ set -euo pipefail
23
+
24
+ # ------------------------------------------------------------------ defaults ---
25
+ PACK_FILE=''
26
+ TARGET_APP_NAME=''
27
+ OUT_DIR='.'
28
+ SENTINEL=''
29
+ FORCE=0
30
+ DRYRUN=0
31
+
32
+ usage() {
33
+ cat >&2 <<'EOF'
34
+ Usage: unpack-reference-foundation.sh --pack-file <file> --target-app-name <name>
35
+ [--out-dir <dir>] [--sentinel <name>]
36
+ [--force] [--dry-run]
37
+
38
+ -p, --pack-file <file> Reference pack file to unpack (required)
39
+ -t, --target-app-name <name> New app name to rename the sentinel into (required)
40
+ -o, --out-dir <dir> Output root (default: .)
41
+ --sentinel <name> Override the sentinel from the pack header
42
+ --force Overwrite files that already exist
43
+ --dry-run List what would be written, write nothing
44
+ -h, --help Show this help
45
+
46
+ Positional fallback: unpack-reference-foundation.sh <pack-file> <target-app-name> [out-dir]
47
+ EOF
48
+ }
49
+
50
+ # -------------------------------------------------------------- arg parsing ---
51
+ while [ $# -gt 0 ]; do
52
+ case "$1" in
53
+ -p|--pack-file) PACK_FILE="${2:?--pack-file needs a value}"; shift 2 ;;
54
+ -t|--target-app-name) TARGET_APP_NAME="${2:?--target-app-name needs a value}"; shift 2 ;;
55
+ -o|--out-dir) OUT_DIR="${2:?--out-dir needs a value}"; shift 2 ;;
56
+ --sentinel) SENTINEL="${2:?--sentinel needs a value}"; shift 2 ;;
57
+ --force) FORCE=1; shift ;;
58
+ --dry-run) DRYRUN=1; shift ;;
59
+ -h|--help) usage; exit 0 ;;
60
+ --) shift; break ;;
61
+ -*) echo "Error: unknown option: $1" >&2; usage; exit 2 ;;
62
+ *)
63
+ if [ -z "$PACK_FILE" ]; then PACK_FILE="$1"
64
+ elif [ -z "$TARGET_APP_NAME" ]; then TARGET_APP_NAME="$1"
65
+ else OUT_DIR="$1"
66
+ fi
67
+ shift ;;
68
+ esac
69
+ done
70
+
71
+ [ -n "$PACK_FILE" ] || { echo "Error: --pack-file is required." >&2; usage; exit 2; }
72
+ [ -n "$TARGET_APP_NAME" ] || { echo "Error: --target-app-name is required." >&2; usage; exit 2; }
73
+ [ -f "$PACK_FILE" ] || { echo "Error: pack file not found: $PACK_FILE" >&2; exit 1; }
74
+
75
+ case "$TARGET_APP_NAME" in
76
+ *[!A-Za-z0-9._-]*)
77
+ echo "Error: --target-app-name may contain only letters, digits, '.', '_' or '-': $TARGET_APP_NAME" >&2
78
+ exit 2 ;;
79
+ esac
80
+
81
+ # ------------------------------------------------ sentinel from header --------
82
+ if [ -z "$SENTINEL" ]; then
83
+ SENTINEL="$(LC_ALL=C sed -n 's/^#[[:space:]]*sentinel:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' "$PACK_FILE" | head -n1)"
84
+ fi
85
+ [ -n "$SENTINEL" ] || { echo "Error: Sentinel not found in pack header; supply it with --sentinel." >&2; exit 1; }
86
+
87
+ # ------------------------------------------------ derive case forms -----------
88
+ to_lower() { printf '%s' "$1" | LC_ALL=C tr '[:upper:]' '[:lower:]'; }
89
+
90
+ S_DOT="$SENTINEL"; T_DOT="$TARGET_APP_NAME"
91
+ S_UND="${SENTINEL//./_}"; T_UND="${TARGET_APP_NAME//./_}"
92
+ s_low="$(to_lower "$SENTINEL")"; t_low="$(to_lower "$TARGET_APP_NAME")"
93
+ S_HYP="${s_low//./-}"; T_HYP="${t_low//./-}"
94
+ S_LDOT="$s_low"; T_LDOT="$t_low"
95
+
96
+ FROM=(); TO=()
97
+ add_form() {
98
+ local from="$1" to="$2" e
99
+ [ -n "$from" ] || return 0
100
+ [ "$from" = "$to" ] && return 0
101
+ if [ "${#FROM[@]}" -gt 0 ]; then
102
+ for e in "${FROM[@]}"; do [ "$e" = "$from" ] && return 0; done
103
+ fi
104
+ FROM+=("$from"); TO+=("$to")
105
+ }
106
+ add_form "$S_DOT" "$T_DOT"
107
+ add_form "$S_UND" "$T_UND"
108
+ add_form "$S_HYP" "$T_HYP"
109
+ add_form "$S_LDOT" "$T_LDOT"
110
+
111
+ # Sort indices by FROM length descending
112
+ ORDER=()
113
+ {
114
+ local_n="${#FROM[@]}"
115
+ used=(); for ((k = 0; k < local_n; k++)); do used[$k]=0; done
116
+ for ((pass = 0; pass < local_n; pass++)); do
117
+ best=-1; bestlen=-1
118
+ for ((j = 0; j < local_n; j++)); do
119
+ if [ "${used[$j]}" -eq 0 ] && [ "${#FROM[$j]}" -gt "$bestlen" ]; then
120
+ bestlen="${#FROM[$j]}"; best="$j"
121
+ fi
122
+ done
123
+ used[$best]=1; ORDER+=("$best")
124
+ done
125
+ }
126
+
127
+ REPL_OUT=''
128
+ apply_repl() {
129
+ local text="$1" idx from to
130
+ if [ "${#ORDER[@]}" -gt 0 ]; then
131
+ for idx in "${ORDER[@]}"; do
132
+ from="${FROM[$idx]}"; to="${TO[$idx]}"
133
+ text="${text//"$from"/$to}"
134
+ done
135
+ fi
136
+ REPL_OUT="$text"
137
+ }
138
+
139
+ # ------------------------------------------------ output root ----------------
140
+ [ -d "$OUT_DIR" ] || mkdir -p "$OUT_DIR"
141
+ OUT_ROOT="$(cd "$OUT_DIR" && pwd -P)"
142
+
143
+ # ------------------------------------------------ base64 decoder (portable) --
144
+ B64D=()
145
+ if printf 'aGk=' | base64 -d >/dev/null 2>&1; then B64D=(base64 -d)
146
+ elif printf 'aGk=' | base64 -D >/dev/null 2>&1; then B64D=(base64 -D)
147
+ elif printf 'aGk=' | base64 --decode >/dev/null 2>&1; then B64D=(base64 --decode)
148
+ else B64D=(openssl base64 -d -A)
149
+ fi
150
+
151
+ # ------------------------------------------------ parse file blocks ----------
152
+ rels=(); encs=(); contents=()
153
+ in_block=0; cur_rel=''; cur_enc=''; cur_content=''; first_line=1
154
+ file_re='^<<<FILE path="([^"]+)"( encoding="([^"]+)")?>>>[[:space:]]*$'
155
+
156
+ while IFS= read -r line || [ -n "$line" ]; do
157
+ line="${line%$'\r'}"
158
+ if [ "$in_block" -eq 0 ]; then
159
+ if [[ "$line" =~ $file_re ]]; then
160
+ cur_rel="${BASH_REMATCH[1]}"
161
+ cur_enc="${BASH_REMATCH[3]:-}"
162
+ in_block=1; first_line=1; cur_content=''
163
+ fi
164
+ else
165
+ if [ "$line" = '<<<ENDFILE>>>' ]; then
166
+ rels+=("$cur_rel"); encs+=("$cur_enc"); contents+=("$cur_content")
167
+ in_block=0
168
+ elif [ "$first_line" -eq 1 ]; then
169
+ cur_content="$line"; first_line=0
170
+ else
171
+ cur_content="$cur_content"$'\n'"$line"
172
+ fi
173
+ fi
174
+ done < "$PACK_FILE"
175
+
176
+ [ "${#rels[@]}" -gt 0 ] || { echo "Error: No <<<FILE>>> blocks found in pack: $PACK_FILE" >&2; exit 1; }
177
+
178
+ # ------------------------------------------------ resolve destinations -------
179
+ dests=()
180
+ for ((i = 0; i < ${#rels[@]}; i++)); do
181
+ apply_repl "${rels[$i]}"
182
+ dests+=("$OUT_ROOT/$REPL_OUT")
183
+ done
184
+
185
+ # ------------------------------------------------ conflict guard -------------
186
+ if [ "$FORCE" -eq 0 ] && [ "$DRYRUN" -eq 0 ]; then
187
+ conflicts=()
188
+ for d in "${dests[@]}"; do [ -e "$d" ] && conflicts+=("$d"); done
189
+ if [ "${#conflicts[@]}" -gt 0 ]; then
190
+ echo "Error: Refusing to overwrite existing files (re-run with --force):" >&2
191
+ printf ' %s\n' "${conflicts[@]}" >&2
192
+ exit 1
193
+ fi
194
+ fi
195
+
196
+ # ------------------------------------------------ write ----------------------
197
+ written=0
198
+ for ((i = 0; i < ${#rels[@]}; i++)); do
199
+ rel="${rels[$i]}"; enc="${encs[$i]}"; dest="${dests[$i]}"
200
+ if [ "$DRYRUN" -eq 1 ]; then
201
+ echo "WOULD WRITE $rel -> $dest"
202
+ continue
203
+ fi
204
+ mkdir -p "$(dirname "$dest")"
205
+ if [ "$enc" = 'base64' ]; then
206
+ printf '%s' "${contents[$i]}" | tr -d '[:space:]' | "${B64D[@]}" > "$dest"
207
+ else
208
+ apply_repl "${contents[$i]}"
209
+ printf '%s' "$REPL_OUT" > "$dest"
210
+ fi
211
+ written=$((written + 1))
212
+ done
213
+
214
+ if [ "$DRYRUN" -eq 1 ]; then
215
+ echo "DryRun: ${#rels[@]} file(s) would be written to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
216
+ else
217
+ echo "Unpacked $written file(s) to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
218
+ fi
@@ -106,6 +106,13 @@ framework/Messaging.Email/
106
106
  TestApp/Program.cs ← OAuth2 + File multi-provider sample
107
107
  ```
108
108
 
109
+ ## Sample reference endpoint
110
+
111
+ A ready-to-run FastEndpoint that sends mail inline (no command/handler) is bundled at
112
+ [`ReferenceEndpoints/SendEmailEndpoint.cs`](./ReferenceEndpoints/SendEmailEndpoint.cs). It is offered
113
+ (opt-in) at the end of the [integration.md](./integration.md) runbook and, if accepted, copied into
114
+ the host project's `ReferenceEndpoints/` folder. Endpoint: `POST /reference/email/send`.
115
+
109
116
  ## Related
110
117
 
111
118
  - [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
@@ -0,0 +1,84 @@
1
+ using FastEndpoints;
2
+ using Microsoft.Extensions.Configuration;
3
+ using AppWeaver.Messaging.Email;
4
+
5
+ // NOTE: This is a bundled SAMPLE endpoint for the AppWeaver Email component.
6
+ // It lives under docs/ and is NOT compiled here. /aw-spec-addcomponent copies it into the
7
+ // host project's ReferenceEndpoints/ folder and replaces the __HOSTPROJECT__ namespace token
8
+ // with the host project's root namespace (e.g. AppWeaver.Health.Service).
9
+ namespace __HOSTPROJECT__.ReferenceEndpoints;
10
+
11
+ /// <summary>
12
+ /// Sample reference endpoint for the AppWeaver Email component.
13
+ ///
14
+ /// Demonstrates the full send sequence inline — it injects the <see cref="IEmailService"/>
15
+ /// abstraction directly and calls <c>SendAsync</c> with the provider wired during DI
16
+ /// registration (see the component's integration runbook). There is intentionally no
17
+ /// command / command-handler / mediator: this is a copy-paste sample only.
18
+ ///
19
+ /// The active transport is whatever <c>options.Provider</c> was set to when
20
+ /// <c>AddEmailMessaging(...)</c> was registered. With the <c>File</c> provider the message is
21
+ /// written to <c>Emails_&lt;yyyy-MM-dd&gt;.txt</c> in the application base directory.
22
+ /// </summary>
23
+ public class SendEmailEndpoint(IEmailService _email, IConfiguration _config)
24
+ : Endpoint<SendEmailRequest>
25
+ {
26
+ public override void Configure()
27
+ {
28
+ Post("/reference/email/send");
29
+ AllowAnonymous();
30
+ Summary(s =>
31
+ {
32
+ s.Summary = "Send a sample email via the configured Email provider";
33
+ s.Description = "Reference endpoint for the AppWeaver Email component. Injects "
34
+ + "IEmailService directly (no command/handler). 'from' falls back to "
35
+ + "Messaging:Email:DefaultFrom when omitted.";
36
+ });
37
+ Options(o => o.WithTags("Reference - Email"));
38
+ }
39
+
40
+ public override async Task HandleAsync(SendEmailRequest req, CancellationToken ct)
41
+ {
42
+ if (string.IsNullOrWhiteSpace(req.To) || string.IsNullOrWhiteSpace(req.Subject))
43
+ {
44
+ HttpContext.Response.StatusCode = 400;
45
+ await HttpContext.Response.WriteAsJsonAsync(
46
+ new { error = "'to' and 'subject' are required." }, ct);
47
+ return;
48
+ }
49
+
50
+ // 'from' is optional in the request — fall back to the configured DefaultFrom.
51
+ var from = string.IsNullOrWhiteSpace(req.From)
52
+ ? _config["Messaging:Email:DefaultFrom"] ?? "no-reply@your-domain.com"
53
+ : req.From;
54
+
55
+ await _email.SendAsync(from, req.To, req.Subject, req.Body ?? string.Empty, req.IsHtml, ct);
56
+
57
+ await HttpContext.Response.WriteAsJsonAsync(new
58
+ {
59
+ status = "sent",
60
+ from,
61
+ to = req.To,
62
+ note = "With the File provider, output is written to Emails_<yyyy-MM-dd>.txt "
63
+ + "in the app base directory."
64
+ }, ct);
65
+ }
66
+ }
67
+
68
+ public class SendEmailRequest
69
+ {
70
+ /// <summary>Recipient address (required).</summary>
71
+ public string To { get; set; } = string.Empty;
72
+
73
+ /// <summary>Subject line (required).</summary>
74
+ public string Subject { get; set; } = string.Empty;
75
+
76
+ /// <summary>HTML or plain-text body.</summary>
77
+ public string Body { get; set; } = string.Empty;
78
+
79
+ /// <summary>Treat <see cref="Body"/> as HTML. Defaults to true.</summary>
80
+ public bool IsHtml { get; set; } = true;
81
+
82
+ /// <summary>Optional sender; falls back to Messaging:Email:DefaultFrom when omitted.</summary>
83
+ public string? From { get; set; }
84
+ }
@@ -11,9 +11,13 @@
11
11
  ## Agent workflow — execute in order
12
12
 
13
13
  1. **Load context.** Read [README.md](./README.md) (contract + provider matrix) and this file.
14
- 2. **Locate target projects** in the solution (see [[folder-structure]]):
15
- `…Application` (injects `IEmailService`), `…Infrastructure` (does the registration),
16
- `…Service` (holds `appsettings.json`). If the layout differs, map these roles before continuing.
14
+ 2. **Resolve the host project** `<HostProject>` (see [[folder-structure]], [[components-readme]] R0.2):
15
+ the single startup project that owns **`Program.cs`** + the ASP.NET Core web SDK —
16
+ `<SampleProjectName>.Service` (Vertical Slice foundation), `<SampleProjectName>.API` (Modular
17
+ Monolith foundation), or another name in a future template. **Find it by locating `Program.cs`, not
18
+ by assuming the name.** In these foundations application/domain/infrastructure code all live in this
19
+ one project (modules are folders); there is **no** separate `…Application` / `…Infrastructure`
20
+ project. `<HostProject>` is the target for C2–C4 below.
17
21
  3. **Present the implementation catalog (below) as a selection list and ASK the user which
18
22
  implementation(s) to integrate.** Use an interactive multi-select prompt (`AskUserQuestion`,
19
23
  `multiSelect: true`). The list options are the rows of the *Implementation catalog*.
@@ -27,7 +31,10 @@
27
31
  6. **Set the default provider:** `configureOptions: options => options.Provider = "<default key>"`.
28
32
  All selected implementations stay registered; the active one is switchable later via config.
29
33
  7. **(Optional) Wire a usage example** in the relevant feature handler (§Usage).
30
- 8. **Verify** (§Verification) and report what was added.
34
+ 8. **Offer the bundled sample endpoint (ASK).** After the wiring above, ask the user (single-select
35
+ yes/no via `AskUserQuestion`) whether to add the ready-made sample endpoint. If **yes**, copy it
36
+ into the host project — see §Sample reference endpoint. If **no**, skip silently.
37
+ 9. **Verify** (§Verification) and report what was added.
31
38
 
32
39
  > **Guardrail — always set `options.Provider`.** When `Provider` is null the facade falls back to a
33
40
  > **hardcoded `"SendGrid"`** key; if SendGrid is not registered, resolution throws at runtime. Never
@@ -68,26 +75,24 @@ Present these as the multi-select options. `Key` is the provider key and the `Us
68
75
 
69
76
  ### C2 — Project references
70
77
 
71
- **File:** `src/<SampleProjectName>.Application/<SampleProjectName>.Application.csproj`
78
+ **File:** `src/<HostProject>/<HostProject>.csproj` (the host project resolved in step 2 — single
79
+ project, no separate Application/Infrastructure). Add the abstraction **and** every selected
80
+ implementation's package here (no `Version=` — versions live in `Directory.Packages.props`, C1).
72
81
 
73
82
  ```xml
74
83
  <ItemGroup>
75
84
  <PackageReference Include="AppWeaver.Messaging.Email" /> <!-- no Version= -->
76
- </ItemGroup>
77
- ```
78
-
79
- **File:** `src/<SampleProjectName>.Infrastructure/<SampleProjectName>.Infrastructure.csproj`
80
- (add the abstraction here too, plus each selected implementation’s package — see its block).
81
-
82
- ```xml
83
- <ItemGroup>
84
- <PackageReference Include="AppWeaver.Messaging.Email" />
85
+ <!-- + each selected implementation’s package — see its block below -->
85
86
  </ItemGroup>
86
87
  ```
87
88
 
88
89
  ### C3 — Registration scaffold
89
90
 
90
- **File:** `src/<SampleProjectName>.Infrastructure/Extensions/InfrastructureServiceRegistration.cs`
91
+ **File:** the host project `<HostProject>` (step 2) — add the registration in its
92
+ `Extensions/InfrastructureServiceRegistration.cs` (`AddInfrastructure`) when that file exists (it does
93
+ in the Vertical Slice / Modular Monolith foundations), otherwise directly in `Program.cs` **before**
94
+ `builder.Build()`. (`configuration` below = the `IConfiguration` in scope: `config` inside
95
+ `AddInfrastructure`, `builder.Configuration` in `Program.cs`.)
91
96
 
92
97
  Add the base call; each selected implementation contributes one `builder.Use…(…)` line (C4).
93
98
 
@@ -103,9 +108,16 @@ services.AddEmailMessaging(
103
108
  configureOptions: options => options.Provider = "<default key>"); // step 6
104
109
  ```
105
110
 
111
+ > **Dual local/Aspire startup ([[components-readme]] R0.2a).** If the host project also has an
112
+ > `AddAspireInfrastructure` method (`Extensions/AspireInfrastructureExtensions.cs`) selected by
113
+ > `if (isAspire) … else …` in `Program.cs`, add this **same** `AddEmailMessaging(...)` block to
114
+ > **both** `AddInfrastructure` and `AddAspireInfrastructure`. The Email registration is DB-agnostic, so
115
+ > it is identical in both methods — copy it verbatim. Registering in only one leaves `IEmailService`
116
+ > unresolvable (throws at runtime) in the other run mode.
117
+
106
118
  ### C4 — Base appsettings block
107
119
 
108
- **File:** `src/<SampleProjectName>.Service/appsettings.json`
120
+ **File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
109
121
 
110
122
  ```jsonc
111
123
  {
@@ -128,7 +140,7 @@ line**, **config**. Secrets MUST be supplied via user-secrets / env / key vault
128
140
  ### MsGraph
129
141
  ```xml
130
142
  <!-- Directory.Packages.props --> <PackageVersion Include="AppWeaver.Messaging.Email.MsGraph" Version="0.1.2" />
131
- <!-- Infrastructure.csproj --> <PackageReference Include="AppWeaver.Messaging.Email.MsGraph" />
143
+ <!-- host .csproj --> <PackageReference Include="AppWeaver.Messaging.Email.MsGraph" />
132
144
  ```
133
145
  ```csharp
134
146
  using AppWeaver.Messaging.Email.MsGraph.Extensions;
@@ -244,6 +256,31 @@ see [README.md](./README.md)).
244
256
 
245
257
  ---
246
258
 
259
+ ## Sample reference endpoint
260
+
261
+ A ready-to-run FastEndpoint is bundled in this component folder at
262
+ [`ReferenceEndpoints/SendEmailEndpoint.cs`](./ReferenceEndpoints/SendEmailEndpoint.cs). It injects
263
+ `IEmailService` directly and performs the **full send inline** — no command/handler/mediator — purely
264
+ as a copy-paste sample. It is **not** wired automatically; it is offered in workflow step 8.
265
+
266
+ **When the user opts in (step 8) — copy procedure:**
267
+
268
+ 1. Confirm `docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs` exists. If it is missing,
269
+ tell the user and skip (do not hand-write a replacement).
270
+ 2. Create `src/<HostProject>/ReferenceEndpoints/` if it does not exist (`<HostProject>` = the project
271
+ resolved in step 2).
272
+ 3. Copy the file to `src/<HostProject>/ReferenceEndpoints/SendEmailEndpoint.cs`, replacing the
273
+ namespace token `__HOSTPROJECT__` with `<HostProject>`'s root namespace (e.g.
274
+ `AppWeaver.Health.Service`). Change nothing else.
275
+ 4. Do **not** touch the C1–C4 wiring. FastEndpoints auto-discovers the endpoint by assembly scan, so
276
+ no extra registration is required.
277
+
278
+ **Endpoint:** `POST /reference/email/send` — body `{ "to", "subject", "body", "isHtml"?, "from"? }`.
279
+ `from` falls back to `Messaging:Email:DefaultFrom`. With the `File` provider the message is written
280
+ to `Emails_<yyyy-MM-dd>.txt` in the app base directory (no real mail is sent).
281
+
282
+ ---
283
+
247
284
  ## Verification
248
285
 
249
286
  1. **Builds** — `dotnet build` succeeds; no `Version=` on package references.
@@ -253,6 +290,9 @@ see [README.md](./README.md)).
253
290
  4. **Sends** — a send succeeds; for `File`, confirm `Emails_<yyyy-MM-dd>.txt` appears in the app
254
291
  base directory.
255
292
  5. **No secrets in diff** — `git diff` shows no secret / API key / password literals.
293
+ 6. **Sample endpoint (only if copied in step 8)** — the build still succeeds with
294
+ `ReferenceEndpoints/SendEmailEndpoint.cs` added, the `__HOSTPROJECT__` token was replaced, and
295
+ `POST /reference/email/send` is discoverable.
256
296
 
257
297
  ## Related
258
298
 
@@ -12,9 +12,13 @@
12
12
 
13
13
  1. **Load context.** Read [README.md](./README.md) and this file. The functional engine is
14
14
  **Quartz** — `AppWeaver.Jobs` + `AppWeaver.Jobs.Quartz` are always installed.
15
- 2. **Locate target projects** (see [[folder-structure]]): `…Application` (job classes + handlers
16
- that depend on the abstractions), `…Infrastructure` (registration + DbContext), `…Service`
17
- (`appsettings.json`).
15
+ 2. **Resolve the host project** `<HostProject>` (see [[folder-structure]], [[components-readme]] R0.2):
16
+ the single startup project that owns **`Program.cs`** + the ASP.NET Core web SDK —
17
+ `<SampleProjectName>.Service` (Vertical Slice foundation), `<SampleProjectName>.API` (Modular
18
+ Monolith foundation), or another name in a future template. **Find it by locating `Program.cs`, not
19
+ by assuming the name.** In these foundations job classes, handlers, registration, `DbContext`, and
20
+ `appsettings.json` all live in this one project (modules are folders) — there is **no** separate
21
+ `…Application` / `…Infrastructure` project. `<HostProject>` is the target for the steps below.
18
22
  3. **Present the implementation catalog (below) as a selection list and ASK the user which
19
23
  implementation(s) to integrate.** Use a multi-select prompt (`AskUserQuestion`,
20
24
  `multiSelect: true`). Enforce the grouping rules when reading the answer:
@@ -69,17 +73,12 @@
69
73
 
70
74
  ### C2 — Project references (no `Version=`)
71
75
 
72
- **File:** `src/<SampleProjectName>.Application/<SampleProjectName>.Application.csproj`
76
+ **File:** `src/<HostProject>/<HostProject>.csproj` (the host project resolved in step 2 — single
77
+ project, no separate Application/Infrastructure). Add the abstraction, the Quartz engine, and (if a
78
+ Group B item is selected) the EF Core persistence package here:
73
79
  ```xml
74
80
  <ItemGroup>
75
- <PackageReference Include="AppWeaver.Jobs" /> <!-- abstractions: IJob, IJobScheduler, IJobProgress -->
76
- </ItemGroup>
77
- ```
78
-
79
- **File:** `src/<SampleProjectName>.Infrastructure/<SampleProjectName>.Infrastructure.csproj`
80
- ```xml
81
- <ItemGroup>
82
- <PackageReference Include="AppWeaver.Jobs" />
81
+ <PackageReference Include="AppWeaver.Jobs" /> <!-- abstractions: IJob, IJobScheduler, IJobProgress -->
83
82
  <PackageReference Include="AppWeaver.Jobs.Quartz" />
84
83
  <!-- + AppWeaver.Jobs.Persistence.EfCore if a Group B item is selected -->
85
84
  </ItemGroup>
@@ -87,14 +86,18 @@
87
86
 
88
87
  ### C3 — appsettings
89
88
 
90
- **File:** `src/<SampleProjectName>.Service/appsettings.json`
89
+ **File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
91
90
  ```jsonc
92
91
  { "AppWeaver": { "Jobs": { "Provider": "Quartz", "Quartz": { "MaxConcurrency": 10 } } } }
93
92
  ```
94
93
 
95
94
  ### C4 — Registration scaffold
96
95
 
97
- **File:** `src/<SampleProjectName>.Infrastructure/Extensions/InfrastructureServiceRegistration.cs`
96
+ **File:** the host project `<HostProject>` (step 2) — register in its
97
+ `Extensions/InfrastructureServiceRegistration.cs` (`AddInfrastructure`) when that file exists (it does
98
+ in the Vertical Slice / Modular Monolith foundations), otherwise directly in `Program.cs` **before**
99
+ `builder.Build()`. (`configuration` below = the `IConfiguration` in scope: `config` inside
100
+ `AddInfrastructure`, `builder.Configuration` in `Program.cs`.)
98
101
 
99
102
  The selected blocks fill in the `UseQuartzJobScheduler` body (Group A) and the persistence chain
100
103
  (Group B).
@@ -117,6 +120,16 @@ services.AddAppWeaverJobs(
117
120
  configureOptions: options => options.Provider = "Quartz");
118
121
  ```
119
122
 
123
+ > **Dual local/Aspire startup ([[components-readme]] R0.2a).** If the host project also has an
124
+ > `AddAspireInfrastructure` method (`Extensions/AspireInfrastructureExtensions.cs`) selected by
125
+ > `if (isAspire) … else …` in `Program.cs`, add this `AddAppWeaverJobs(...)` registration to **both**
126
+ > `AddInfrastructure` and `AddAspireInfrastructure`, or `IJobScheduler` is unregistered whenever the
127
+ > app runs under Aspire. Keep the structure identical, but make any **provider-specific** call match
128
+ > each method's database: in `AddAspireInfrastructure` use the PostgreSQL forms (`s.UsePostgres(...)`,
129
+ > `opts.UseNpgsql(...)`); in `AddInfrastructure` use the SQLite forms (`s.UseMicrosoftSQLite(...)`,
130
+ > `opts.UseSqlite(...)`). `EfCore.DataStore` (`.UseAppWeaverDataStore()`) is provider-agnostic — it
131
+ > rides whichever DataStore that method already registered.
132
+
120
133
  ---
121
134
 
122
135
  ## Implementation blocks
@@ -199,7 +212,8 @@ public class ExportDataJob(IExportService _exports, IJobProgress _progress) : IJ
199
212
  ```
200
213
 
201
214
  ### Register every job class as `Scoped`
202
- **File:** `…/Extensions/InfrastructureServiceRegistration.cs`
215
+ **File:** the host project's `Extensions/InfrastructureServiceRegistration.cs` (or `Program.cs`) — the
216
+ same registration site as C4
203
217
  ```csharp
204
218
  services.AddScoped<ExportDataJob>();
205
219
  ```
@@ -72,6 +72,13 @@ framework/QrGenerator/
72
72
  QrGenerator.QrCoder.Tests/ ← QRCoderServiceTests (xUnit)
73
73
  ```
74
74
 
75
+ ## Sample reference endpoint
76
+
77
+ A ready-to-run FastEndpoint that generates a QR PNG inline (no command/handler) is bundled at
78
+ [`ReferenceEndpoints/GenerateQrEndpoint.cs`](./ReferenceEndpoints/GenerateQrEndpoint.cs). It is
79
+ offered (opt-in) at the end of the [integration.md](./integration.md) runbook and, if accepted,
80
+ copied into the host project's `ReferenceEndpoints/` folder. Endpoint: `POST /reference/qr-codes`.
81
+
75
82
  ## Related
76
83
 
77
84
  - [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
@@ -0,0 +1,57 @@
1
+ using FastEndpoints;
2
+ using AppWeaver.QRGenerator; // namespace uses QRGenerator (capital R); package id is QrGenerator
3
+
4
+ // NOTE: This is a bundled SAMPLE endpoint for the AppWeaver QrGenerator component.
5
+ // It lives under docs/ and is NOT compiled here. /aw-spec-addcomponent copies it into the
6
+ // host project's ReferenceEndpoints/ folder and replaces the __HOSTPROJECT__ namespace token
7
+ // with the host project's root namespace (e.g. AppWeaver.Health.Service).
8
+ namespace __HOSTPROJECT__.ReferenceEndpoints;
9
+
10
+ /// <summary>
11
+ /// Sample reference endpoint for the AppWeaver QrGenerator component.
12
+ ///
13
+ /// Demonstrates the full sequence inline — it injects the <see cref="IQRGenerator"/> abstraction
14
+ /// directly, calls <c>GenerateQR</c>, and streams the PNG back as <c>image/png</c>. There is
15
+ /// intentionally no command / command-handler / mediator: this is a copy-paste sample only.
16
+ ///
17
+ /// Behaviour note: <c>GenerateQR(null)</c> throws <see cref="System.ArgumentNullException"/>, but an
18
+ /// empty string is valid and returns a (small) PNG, so only <c>null</c> content is rejected here.
19
+ /// </summary>
20
+ public class GenerateQrEndpoint(IQRGenerator _qr) : Endpoint<GenerateQrRequest>
21
+ {
22
+ public override void Configure()
23
+ {
24
+ Post("/reference/qr-codes");
25
+ AllowAnonymous();
26
+ Summary(s =>
27
+ {
28
+ s.Summary = "Generate a QR-code PNG from text content";
29
+ s.Description = "Reference endpoint for the AppWeaver QrGenerator component. Injects "
30
+ + "IQRGenerator directly (no command/handler) and returns an image/png body.";
31
+ });
32
+ Options(o => o.WithTags("Reference - QrGenerator"));
33
+ }
34
+
35
+ public override async Task HandleAsync(GenerateQrRequest req, CancellationToken ct)
36
+ {
37
+ if (req.Content is null) // null throws inside the provider; empty string is allowed
38
+ {
39
+ HttpContext.Response.StatusCode = 400;
40
+ await HttpContext.Response.WriteAsJsonAsync(
41
+ new { error = "'content' is required (an empty string is allowed)." }, ct);
42
+ return;
43
+ }
44
+
45
+ byte[] png = _qr.GenerateQR(req.Content);
46
+
47
+ HttpContext.Response.ContentType = "image/png";
48
+ HttpContext.Response.Headers["Content-Disposition"] = "inline; filename=qr-code.png";
49
+ await HttpContext.Response.Body.WriteAsync(png, ct);
50
+ }
51
+ }
52
+
53
+ public class GenerateQrRequest
54
+ {
55
+ /// <summary>Text to encode into the QR code. Empty string is valid; null is rejected.</summary>
56
+ public string? Content { get; set; }
57
+ }