gemstack-cli 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -25
  3. data/README.md +6 -17
  4. data/exe/gemstack +8 -1
  5. metadata +6 -159
  6. data/lib/gemstack/cli/add_generator.rb +0 -239
  7. data/lib/gemstack/cli/app_generator.rb +0 -142
  8. data/lib/gemstack/cli/commands/db.rb +0 -135
  9. data/lib/gemstack/cli/commands/jobs.rb +0 -96
  10. data/lib/gemstack/cli/controller_generator.rb +0 -73
  11. data/lib/gemstack/cli/deploy_generator.rb +0 -102
  12. data/lib/gemstack/cli/doctor.rb +0 -308
  13. data/lib/gemstack/cli/generator.rb +0 -144
  14. data/lib/gemstack/cli/job_generator.rb +0 -48
  15. data/lib/gemstack/cli/migration_generator.rb +0 -52
  16. data/lib/gemstack/cli/policy_generator.rb +0 -35
  17. data/lib/gemstack/cli/project.rb +0 -53
  18. data/lib/gemstack/cli/resource_generator.rb +0 -170
  19. data/lib/gemstack/cli/resource_spec.rb +0 -95
  20. data/lib/gemstack/cli.rb +0 -304
  21. data/templates/app/Gemfile.tt +0 -44
  22. data/templates/app/README.md.tt +0 -29
  23. data/templates/app/app/controllers/application_controller.rb +0 -6
  24. data/templates/app/app/jobs/application_job.rb +0 -9
  25. data/templates/app/app/mailers/application_mailer.rb +0 -8
  26. data/templates/app/app/mailers/templates/dot_keep +0 -0
  27. data/templates/app/app/models/application_model.rb +0 -19
  28. data/templates/app/app/serializers/application_serializer.rb +0 -6
  29. data/templates/app/bin/gemstack +0 -7
  30. data/templates/app/config/app.rb.tt +0 -54
  31. data/templates/app/config/database.yml.tt +0 -61
  32. data/templates/app/config/environments/development.rb.tt +0 -35
  33. data/templates/app/config/environments/production.rb.tt +0 -40
  34. data/templates/app/config/environments/test.rb.tt +0 -21
  35. data/templates/app/config/puma.rb +0 -21
  36. data/templates/app/config/routes.rb +0 -10
  37. data/templates/app/config.ru +0 -6
  38. data/templates/app/db/migrations/dot_keep +0 -0
  39. data/templates/app/db/seeds.rb +0 -5
  40. data/templates/app/dot_env.example.tt +0 -19
  41. data/templates/app/dot_gitignore +0 -16
  42. data/templates/app/dot_node-version.tt +0 -1
  43. data/templates/app/dot_nvmrc.tt +0 -1
  44. data/templates/app/dot_ruby-version.tt +0 -1
  45. data/templates/app/dot_tool-versions.tt +0 -4
  46. data/templates/app/test/health_test.rb +0 -18
  47. data/templates/app/test/test_helper.rb.tt +0 -25
  48. data/templates/auth/app/controllers/api_tokens_controller.rb +0 -35
  49. data/templates/auth/app/controllers/email_verifications_controller.rb +0 -31
  50. data/templates/auth/app/controllers/password_resets_controller.rb +0 -40
  51. data/templates/auth/app/controllers/registrations_controller.rb +0 -19
  52. data/templates/auth/app/controllers/sessions_controller.rb +0 -29
  53. data/templates/auth/app/mailers/auth_mailer.rb +0 -20
  54. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.html.erb +0 -2
  55. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.text.erb +0 -2
  56. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.html.erb +0 -3
  57. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.text.erb +0 -6
  58. data/templates/auth/app/models/auth_token.rb +0 -15
  59. data/templates/auth/app/models/user.rb +0 -12
  60. data/templates/auth/app/policies/application_policy.rb +0 -6
  61. data/templates/auth/app/serializers/api_token_serializer.rb +0 -6
  62. data/templates/auth/app/serializers/new_api_token_serializer.rb +0 -9
  63. data/templates/auth/app/serializers/user_serializer.rb +0 -6
  64. data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +0 -41
  65. data/templates/auth/frontend/app/account/page.tsx +0 -116
  66. data/templates/auth/frontend/app/forgot-password/page.tsx +0 -48
  67. data/templates/auth/frontend/app/login/page.tsx +0 -26
  68. data/templates/auth/frontend/app/reset-password/page.tsx +0 -11
  69. data/templates/auth/frontend/app/signup/page.tsx +0 -27
  70. data/templates/auth/frontend/app/verify-email/page.tsx +0 -11
  71. data/templates/auth/frontend/components/auth/CredentialsForm.tsx +0 -58
  72. data/templates/auth/frontend/components/auth/ResetPasswordForm.tsx +0 -43
  73. data/templates/auth/frontend/components/auth/VerifyEmail.tsx +0 -28
  74. data/templates/auth/frontend/lib/auth.ts +0 -113
  75. data/templates/auth/test/controllers/auth_test.rb +0 -108
  76. data/templates/controller/app/controllers/%file_name%_controller.rb.tt +0 -10
  77. data/templates/controller/test/controllers/%file_name%_controller_test.rb.tt +0 -14
  78. data/templates/deploy/Caddyfile.tt +0 -19
  79. data/templates/deploy/Dockerfile.tt +0 -63
  80. data/templates/deploy/Procfile.tt +0 -6
  81. data/templates/deploy/compose.yaml.tt +0 -129
  82. data/templates/deploy/dot_dockerignore +0 -14
  83. data/templates/frontend/app/globals.css +0 -171
  84. data/templates/frontend/app/layout.tsx.tt +0 -19
  85. data/templates/frontend/app/page.module.css +0 -315
  86. data/templates/frontend/app/page.tsx.tt +0 -183
  87. data/templates/frontend/app/providers.tsx +0 -24
  88. data/templates/frontend/lib/gemstack/client.ts +0 -125
  89. data/templates/frontend/next-env.d.ts +0 -5
  90. data/templates/frontend/next.config.ts +0 -17
  91. data/templates/frontend/package.json.tt +0 -23
  92. data/templates/frontend/tsconfig.json +0 -21
  93. data/templates/job/app/jobs/%file_name%.rb.tt +0 -15
  94. data/templates/job/test/jobs/%file_name%_test.rb.tt +0 -15
  95. data/templates/migration/db/migrations/%timestamp%_%file_name%.rb.tt +0 -17
  96. data/templates/policy/app/policies/%file_name%_policy.rb.tt +0 -21
  97. data/templates/policy/test/policies/%file_name%_policy_test.rb.tt +0 -9
  98. data/templates/realtime/config/channels.rb +0 -14
  99. data/templates/realtime/frontend/lib/gemstack/realtime.ts +0 -120
  100. data/templates/resource/controller/app/controllers/%plural%_controller.rb.tt +0 -51
  101. data/templates/resource/controller/test/controllers/%plural%_controller_test.rb.tt +0 -74
  102. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/edit/page.tsx.tt +0 -45
  103. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/page.tsx.tt +0 -49
  104. data/templates/resource/frontend/frontend/app/%url_segment%/new/page.tsx.tt +0 -27
  105. data/templates/resource/frontend/frontend/app/%url_segment%/page.tsx.tt +0 -51
  106. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Card.tsx.tt +0 -15
  107. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Form.tsx.tt +0 -98
  108. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Table.tsx.tt +0 -36
  109. data/templates/resource/frontend/frontend/lib/format.ts +0 -12
  110. data/templates/resource/frontend/frontend/lib/queries/%url_segment%.ts.tt +0 -64
  111. data/templates/resource/migration/db/migrations/%timestamp%_create_%table%.rb.tt +0 -18
  112. data/templates/resource/model/app/models/%file_name%.rb.tt +0 -15
  113. data/templates/resource/model/test/models/%file_name%_test.rb.tt +0 -29
  114. data/templates/resource/serializer/app/serializers/%file_name%_serializer.rb.tt +0 -7
  115. data/templates/storage/app/controllers/uploads_controller.rb.tt +0 -33
  116. data/templates/storage/app/serializers/upload_serializer.rb +0 -11
  117. data/templates/storage/frontend/lib/upload.ts +0 -52
  118. data/templates/storage/test/controllers/uploads_test.rb.tt +0 -30
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "<%= "../" * (file_name.count("/") + 1) %>test_helper"
4
-
5
- class <%= class_name %>Test < GemStack::TestCase
6
- <% actions.each_with_index do |action, index| -%>
7
- <%= "\n" if index.positive? %> def test_<%= action %>
8
- <%= route_for(action)[0] %>_json "<%= request_path(action) %>"
9
-
10
- assert_status 200
11
- assert_equal({ "action" => "<%= file_name %>#<%= action %>" }, json_body)
12
- end
13
- <% end -%>
14
- end
@@ -1,19 +0,0 @@
1
- # One public origin: /api/* → Ruby, everything else → Next.js (docs/deployment.md).
2
- # SITE_ADDRESS=example.com gets a Let's Encrypt certificate automatically;
3
- # "localhost" uses Caddy's local CA (trust it with `caddy trust`, or curl -k).
4
- {$SITE_ADDRESS:localhost} {
5
- encode zstd gzip
6
-
7
- handle /api/* {
8
- reverse_proxy api:4000 {
9
- # Server-Sent Events: stream responses immediately.
10
- flush_interval -1
11
- }
12
- }
13
- <% if frontend? -%>
14
-
15
- handle {
16
- reverse_proxy web:3000
17
- }
18
- <% end -%>
19
- }
@@ -1,63 +0,0 @@
1
- # syntax=docker/dockerfile:1
2
- # Production images for <%= app_name %> (gemstack generate deploy — docs/deployment.md).
3
- #
4
- # docker build --target api -t <%= app_name %>-api . # Ruby API (Puma), also runs jobs and migrations
5
- <% if frontend? -%>
6
- # docker build --target web -t <%= app_name %>-web . # Next.js
7
- <% end -%>
8
- #
9
- # Configuration comes from the environment at runtime (SECRET_KEY_BASE,
10
- # DATABASE_URL, …); nothing secret is baked into the images.
11
-
12
- ARG RUBY_VERSION=<%= ruby_version %>
13
- ARG NODE_VERSION=<%= node_version %>
14
-
15
- # ── Ruby API ────────────────────────────────────────────────────────────────
16
- FROM ruby:${RUBY_VERSION}-slim AS api-build
17
- RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= build_packages %> \
18
- && rm -rf /var/lib/apt/lists/*
19
- WORKDIR /app
20
- ENV BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle
21
- COPY Gemfile Gemfile.lock ./
22
- COPY vendor/ ./vendor/
23
- RUN bundle install --jobs 4 && rm -rf /usr/local/bundle/cache "${BUNDLE_PATH}"/ruby/*/cache
24
- COPY . .
25
- RUN rm -rf frontend tmp/* log/* .env*
26
-
27
- FROM ruby:${RUBY_VERSION}-slim AS api
28
- RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= runtime_packages %> \
29
- && rm -rf /var/lib/apt/lists/* \
30
- && groupadd --system --gid 1000 app && useradd --system --uid 1000 --gid app --create-home app<% if sqlite? %> \
31
- && mkdir -p /data && chown app:app /data<% end %>
32
- WORKDIR /app
33
- ENV GEMSTACK_ENV=production BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle \
34
- GEMSTACK_API_HOST=0.0.0.0 GEMSTACK_API_PORT=4000
35
- COPY --from=api-build /usr/local/bundle /usr/local/bundle
36
- COPY --from=api-build --chown=app:app /app /app
37
- USER app
38
- EXPOSE 4000
39
- HEALTHCHECK --interval=10s --timeout=3s --start-period=20s CMD curl -fsS http://127.0.0.1:4000/api/health || exit 1
40
- # Other processes use the same image: `bundle exec gemstack jobs`, `bundle exec gemstack db:migrate`.
41
- CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
42
- <% if frontend? -%>
43
-
44
- # ── Next.js ─────────────────────────────────────────────────────────────────
45
- FROM node:${NODE_VERSION}-slim AS web-build
46
- WORKDIR /app
47
- COPY frontend/package.json frontend/package-lock.json ./
48
- RUN npm ci
49
- COPY frontend/ ./
50
- ENV NEXT_TELEMETRY_DISABLED=1
51
- # GEMSTACK_API_URL is only needed here when Next.js proxies /api itself (no reverse proxy in front).
52
- ARG GEMSTACK_API_URL
53
- ENV GEMSTACK_API_URL=${GEMSTACK_API_URL}
54
- RUN npm run build && npm prune --omit=dev
55
-
56
- FROM node:${NODE_VERSION}-slim AS web
57
- WORKDIR /app
58
- ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000
59
- COPY --from=web-build --chown=node:node /app ./
60
- USER node
61
- EXPOSE 3000
62
- CMD ["npm", "start"]
63
- <% end -%>
@@ -1,6 +0,0 @@
1
- # Process types for Procfile platforms (Heroku, Render, Railway, Dokku…) — docs/deployment.md.
2
- release: bundle exec gemstack db:migrate
3
- web: GEMSTACK_API_PORT=${PORT:-4000} bundle exec puma -C config/puma.rb
4
- <% if jobs? -%>
5
- worker: bundle exec gemstack jobs
6
- <% end -%>
@@ -1,129 +0,0 @@
1
- # Runs <%= app_name %> in production mode on this machine, behind Caddy on https://localhost:
2
- #
3
- # export SECRET_KEY_BASE=$(openssl rand -hex 64)<% if postgresql? %> POSTGRES_PASSWORD=$(openssl rand -hex 16)<% elsif mysql? %> MYSQL_PASSWORD=$(openssl rand -hex 16)<% end %>
4
- # docker compose up --build
5
- #
6
- # It's also a map of what a real deploy needs: one image per process type,
7
- # migrations before the API starts, and a proxy sending /api to Ruby.
8
- <% if sqlite? -%>
9
- # SQLite: the database file lives on the `data` volume, shared by the API, the
10
- # jobs worker and migrations — keep them on one host.
11
- <% end -%>
12
- name: <%= app_name.tr("_", "-") %>
13
-
14
- x-api-env: &api-env
15
- GEMSTACK_ENV: production
16
- SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE (openssl rand -hex 64)}
17
- <% if database? -%>
18
- DATABASE_URL: <%= database_url %>
19
- <% end -%>
20
- <% if redis? -%>
21
- REDIS_URL: redis://redis:6379/0 # realtime fan-out between the API and the jobs worker
22
- <% end -%>
23
- APP_URL: ${APP_URL:-https://localhost}
24
- <% if mail? -%>
25
- SMTP_URL: ${SMTP_URL:-} # e.g. smtp://user:pass@smtp.example.com:587 — emails retry until it is set
26
- MAIL_FROM: ${MAIL_FROM:-}
27
- <% end -%>
28
-
29
- services:
30
- <% if postgresql? -%>
31
- db:
32
- image: postgres:17
33
- environment:
34
- POSTGRES_USER: app
35
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
36
- POSTGRES_DB: app
37
- volumes: [db:/var/lib/postgresql/data]
38
- healthcheck:
39
- test: ["CMD-SHELL", "pg_isready -U app -d app"]
40
- interval: 2s
41
- retries: 30
42
-
43
- <% elsif mysql? -%>
44
- db:
45
- image: mysql:8.4
46
- environment:
47
- MYSQL_DATABASE: app
48
- MYSQL_USER: app
49
- MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
50
- MYSQL_RANDOM_ROOT_PASSWORD: "yes"
51
- volumes: [db:/var/lib/mysql]
52
- healthcheck:
53
- test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uapp -p$$MYSQL_PASSWORD --silent"]
54
- interval: 3s
55
- retries: 40
56
-
57
- <% end -%>
58
- <% if redis? -%>
59
- redis:
60
- image: redis:7
61
- command: ["redis-server", "--save", "", "--appendonly", "no"]
62
-
63
- <% end -%>
64
- <% if database? -%>
65
- migrate:
66
- image: <%= app_name %>-api
67
- build: { context: ., target: api }
68
- healthcheck: { disable: true }
69
- command: ["bundle", "exec", "gemstack", "db:migrate"]
70
- environment: *api-env
71
- <% if sqlite? -%>
72
- volumes: [data:/data]
73
- <% else -%>
74
- depends_on: { db: { condition: service_healthy } }
75
- <% end -%>
76
-
77
- <% end -%>
78
- api:
79
- image: <%= app_name %>-api
80
- build: { context: ., target: api }
81
- environment:
82
- <<: *api-env
83
- WEB_CONCURRENCY: ${WEB_CONCURRENCY:-2}
84
- <% if sqlite? -%>
85
- volumes: [data:/data]
86
- <% end -%>
87
- <% if database? -%>
88
- depends_on: { migrate: { condition: service_completed_successfully } }
89
- <% end -%>
90
- <% if jobs? -%>
91
-
92
- jobs:
93
- image: <%= app_name %>-api
94
- build: { context: ., target: api }
95
- healthcheck: { disable: true }
96
- command: ["bundle", "exec", "gemstack", "jobs"]
97
- environment: *api-env
98
- <% if sqlite? -%>
99
- volumes: [data:/data]
100
- <% end -%>
101
- depends_on: { migrate: { condition: service_completed_successfully } }
102
- <% end -%>
103
- <% if frontend? -%>
104
-
105
- web:
106
- image: <%= app_name %>-web
107
- build: { context: ., target: web }
108
- environment:
109
- GEMSTACK_API_URL: http://api:4000 # Server Components call the API directly
110
- depends_on: [api]
111
- <% end -%>
112
-
113
- proxy:
114
- image: caddy:2
115
- ports: ["80:80", "443:443"]
116
- environment:
117
- SITE_ADDRESS: ${SITE_ADDRESS:-localhost}
118
- volumes:
119
- - ./Caddyfile:/etc/caddy/Caddyfile:ro
120
- - caddy:/data
121
- depends_on: [api<%= ", web" if frontend? %>]
122
-
123
- volumes:
124
- <% if sqlite? -%>
125
- data:
126
- <% elsif database? -%>
127
- db:
128
- <% end -%>
129
- caddy:
@@ -1,14 +0,0 @@
1
- # Kept out of the build context (and so out of images).
2
- .git
3
- .env
4
- .env.*
5
- !.env.example
6
- tmp/
7
- log/
8
- storage/
9
- coverage/
10
- node_modules/
11
- frontend/node_modules/
12
- frontend/.next/
13
- **/*.pem
14
- **/*.key
@@ -1,171 +0,0 @@
1
- :root {
2
- --bg: #ffffff;
3
- --fg: #16181d;
4
- --muted: #5b6170;
5
- --code-bg: #f1f2f5;
6
- --ok: #1f9d55;
7
- --error: #d64545;
8
- --pending: #d99a1e;
9
- }
10
-
11
- @media (prefers-color-scheme: dark) {
12
- :root {
13
- --bg: #0f1115;
14
- --fg: #e6e8ee;
15
- --muted: #9aa0ad;
16
- --code-bg: #1c2030;
17
- }
18
- }
19
-
20
- * {
21
- box-sizing: border-box;
22
- }
23
-
24
- body {
25
- margin: 0;
26
- background: var(--bg);
27
- color: var(--fg);
28
- font: 16px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
29
- }
30
-
31
- code {
32
- background: var(--code-bg);
33
- padding: 0.1rem 0.35rem;
34
- border-radius: 4px;
35
- font-size: 0.9em;
36
- }
37
-
38
- .page {
39
- max-width: 40rem;
40
- margin: 0 auto;
41
- padding: 4rem 1rem;
42
- }
43
-
44
- .muted {
45
- color: var(--muted);
46
- }
47
-
48
- .status {
49
- display: flex;
50
- align-items: center;
51
- gap: 0.6rem;
52
- padding: 0.75rem 1rem;
53
- border-radius: 8px;
54
- background: var(--code-bg);
55
- }
56
-
57
- .dot {
58
- width: 0.6rem;
59
- height: 0.6rem;
60
- border-radius: 50%;
61
- flex: none;
62
- }
63
-
64
- .status-ok .dot {
65
- background: var(--ok);
66
- }
67
-
68
- .status-error .dot {
69
- background: var(--error);
70
- }
71
-
72
- .status-pending .dot {
73
- background: var(--pending);
74
- }
75
-
76
- a {
77
- color: inherit;
78
- }
79
-
80
- h1 {
81
- margin-top: 0;
82
- }
83
-
84
- .page-header,
85
- .actions {
86
- display: flex;
87
- align-items: center;
88
- justify-content: space-between;
89
- gap: 1rem;
90
- }
91
-
92
- .actions {
93
- justify-content: flex-start;
94
- margin-top: 1.5rem;
95
- }
96
-
97
- .button {
98
- display: inline-block;
99
- padding: 0.45rem 0.9rem;
100
- border: 1px solid var(--fg);
101
- border-radius: 6px;
102
- background: var(--fg);
103
- color: var(--bg);
104
- font: inherit;
105
- text-decoration: none;
106
- cursor: pointer;
107
- }
108
-
109
- .button:disabled {
110
- opacity: 0.6;
111
- cursor: default;
112
- }
113
-
114
- .button-danger {
115
- background: var(--error);
116
- border-color: var(--error);
117
- color: #fff;
118
- }
119
-
120
- .table {
121
- width: 100%;
122
- border-collapse: collapse;
123
- }
124
-
125
- .table th,
126
- .table td {
127
- padding: 0.5rem;
128
- border-bottom: 1px solid var(--code-bg);
129
- text-align: left;
130
- }
131
-
132
- .form {
133
- display: grid;
134
- gap: 1rem;
135
- max-width: 32rem;
136
- }
137
-
138
- .field {
139
- display: grid;
140
- gap: 0.3rem;
141
- }
142
-
143
- .field input:not([type="checkbox"]),
144
- .field textarea {
145
- padding: 0.45rem;
146
- border: 1px solid var(--muted);
147
- border-radius: 6px;
148
- background: var(--bg);
149
- color: var(--fg);
150
- font: inherit;
151
- }
152
-
153
- .field-error,
154
- .form-error {
155
- margin: 0;
156
- color: var(--error);
157
- }
158
-
159
- .card {
160
- display: grid;
161
- grid-template-columns: max-content 1fr;
162
- gap: 0.4rem 1.5rem;
163
- }
164
-
165
- .card dt {
166
- color: var(--muted);
167
- }
168
-
169
- .card dd {
170
- margin: 0;
171
- }
@@ -1,19 +0,0 @@
1
- import type { Metadata } from "next";
2
- import type { ReactNode } from "react";
3
- import { Providers } from "./providers";
4
- import "./globals.css";
5
-
6
- export const metadata: Metadata = {
7
- title: "<%= module_name %>",
8
- description: "A GemStack application",
9
- };
10
-
11
- export default function RootLayout({ children }: { children: ReactNode }) {
12
- return (
13
- <html lang="en">
14
- <body>
15
- <Providers>{children}</Providers>
16
- </body>
17
- </html>
18
- );
19
- }