gemstack-cli 0.2.1 → 0.2.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +2 -2
- data/lib/gemstack/cli/app_generator.rb +9 -2
- data/lib/gemstack/cli/doctor.rb +15 -14
- data/templates/app/Gemfile.tt +1 -1
- data/templates/app/dot_node-version.tt +1 -0
- data/templates/app/dot_nvmrc.tt +1 -0
- data/templates/app/dot_tool-versions.tt +3 -0
- data/templates/frontend/app/page.module.css +315 -0
- data/templates/frontend/app/page.tsx.tt +171 -33
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b7ea1041a871d84d72d33529ee4b303145363665a6605d0cc5d533fd4604157
|
|
4
|
+
data.tar.gz: 02625e8747b1efe7fae2191d356376bc8bdde1500d892cef5f945badb1d5451d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96aabfd8af652f60639dd4aa4b155351b654be18ff569a262520c1fec6a1d7f73a1f192c62b7dba551eb4bbdce86fc3e4ad1a00f100d4b4ad6ba51e5a3e56f21
|
|
7
|
+
data.tar.gz: 2efe2c5964c8a597efae119096f5293b635574b1b2db68b5be483a203d2d3228ec3ff87018f37865a5caf081b1efd7b97708b55d44465cba61bab223aed6d210
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
## 0.2.2
|
|
8
|
+
|
|
9
|
+
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
10
|
+
|
|
3
11
|
## 0.2.1
|
|
4
12
|
|
|
5
13
|
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
data/LICENSE.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 Adware Technologies (https://www.adwaretech.com)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
GemStack command-line interface and generators.
|
|
4
4
|
|
|
5
5
|
Part of [GemStack](https://github.com/gemstack-rb/gemstack), a modular Ruby API framework for Next.js
|
|
6
|
-
applications. All GemStack gems are developed together in that repository and released with the same
|
|
6
|
+
applications by [Adware Technologies](https://www.adwaretech.com). All GemStack gems are developed together in that repository and released with the same
|
|
7
7
|
version.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
@@ -21,4 +21,4 @@ Source, issues and pull requests: [gemstack-rb/gemstack](https://github.com/gems
|
|
|
21
21
|
|
|
22
22
|
## License
|
|
23
23
|
|
|
24
|
-
MIT —
|
|
24
|
+
Open source under the MIT License — © [Adware Technologies](https://www.adwaretech.com). See [LICENSE.txt](LICENSE.txt).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "gemstack/dev"
|
|
4
|
+
|
|
3
5
|
module GemStack
|
|
4
6
|
class CLI < Thor
|
|
5
7
|
# `gemstack new NAME` — a Ruby API + Next.js application containing the
|
|
@@ -30,8 +32,12 @@ module GemStack
|
|
|
30
32
|
|
|
31
33
|
def version = GemStack::VERSION
|
|
32
34
|
|
|
33
|
-
# Pins the app to the Ruby that generated it
|
|
35
|
+
# Pins the app to the Ruby that generated it, in files every version
|
|
36
|
+
# manager reads (.ruby-version: rbenv, rvm, chruby, asdf, mise;
|
|
37
|
+
# .tool-versions: asdf, mise), and Node.js likewise (.node-version,
|
|
38
|
+
# .nvmrc, .tool-versions) — DECISIONS D-066.
|
|
34
39
|
def ruby_version = RUBY_VERSION
|
|
40
|
+
def node_version = @node_version ||= Dev::Toolchain.pinned_node_version
|
|
35
41
|
def frontend? = !@options[:skip_frontend]
|
|
36
42
|
def database? = !@options[:skip_database]
|
|
37
43
|
|
|
@@ -54,7 +60,8 @@ module GemStack
|
|
|
54
60
|
database_adapter if database? # fail early on an unknown --database
|
|
55
61
|
@output.puts("Creating GemStack application #{name} in #{destination}")
|
|
56
62
|
render_directory("app", destination, skip: lambda { |rel|
|
|
57
|
-
!database? && (rel.start_with?("db/", "app/models/") || rel == "config/database.yml.tt")
|
|
63
|
+
(!database? && (rel.start_with?("db/", "app/models/") || rel == "config/database.yml.tt")) ||
|
|
64
|
+
(!frontend? && %w[dot_node-version.tt dot_nvmrc.tt].include?(rel))
|
|
58
65
|
})
|
|
59
66
|
render_directory("frontend", File.join(destination, "frontend")) if frontend?
|
|
60
67
|
install unless @options[:skip_install]
|
data/lib/gemstack/cli/doctor.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "socket"
|
|
5
|
+
require "gemstack/dev"
|
|
5
6
|
|
|
6
7
|
module GemStack
|
|
7
8
|
class CLI < Thor
|
|
@@ -50,11 +51,11 @@ module GemStack
|
|
|
50
51
|
|
|
51
52
|
def check_ruby
|
|
52
53
|
wanted = read(".ruby-version")&.strip&.delete_prefix("ruby-")
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
elsif
|
|
57
|
-
|
|
54
|
+
if !Dev::Toolchain.ruby_ok?
|
|
55
|
+
problem("Ruby #{RUBY_VERSION}", "GemStack needs Ruby #{Dev::Toolchain::MIN_RUBY} or newer — " \
|
|
56
|
+
"#{Dev::Toolchain.ruby_hint(wanted.to_s.empty? ? "3.4" : wanted)}")
|
|
57
|
+
elsif wanted && !wanted.empty? && !RUBY_VERSION.start_with?(wanted)
|
|
58
|
+
caution("Ruby #{RUBY_VERSION}", "the app pins #{wanted} (.ruby-version) — #{Dev::Toolchain.ruby_hint(wanted)}")
|
|
58
59
|
else
|
|
59
60
|
pass("Ruby #{RUBY_VERSION}#{" (YJIT available)" if defined?(RubyVM::YJIT)}")
|
|
60
61
|
end
|
|
@@ -63,17 +64,17 @@ module GemStack
|
|
|
63
64
|
def check_node
|
|
64
65
|
return unless File.directory?(path("frontend"))
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if
|
|
69
|
-
problem("Node.js not found",
|
|
70
|
-
elsif (version
|
|
71
|
-
problem("Node.js #{version}
|
|
67
|
+
node = Dev::Toolchain.node(@run)
|
|
68
|
+
wanted = read(".node-version")&.strip || read(".nvmrc")&.strip || Dev::Toolchain::LTS_NODE
|
|
69
|
+
if node.nil?
|
|
70
|
+
problem("Node.js not found", Dev::Toolchain.node_hint(wanted, nil))
|
|
71
|
+
elsif !Dev::Toolchain.node_ok?(node[:version])
|
|
72
|
+
problem("Node.js #{node[:version]} (#{node[:path]})",
|
|
73
|
+
"Next.js 16 needs #{MIN_NODE.join(".")}+ — " \
|
|
74
|
+
"#{Dev::Toolchain.node_hint(wanted, Dev::Toolchain.node_manager(node[:path]))}")
|
|
72
75
|
else
|
|
73
|
-
pass("Node.js #{version}")
|
|
76
|
+
pass("Node.js #{node[:version]}")
|
|
74
77
|
end
|
|
75
|
-
rescue SystemCallError
|
|
76
|
-
problem("Node.js not found", "install Node.js #{MIN_NODE.join(".")}+ (the frontend is Next.js)")
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def check_frontend_dependencies
|
data/templates/app/Gemfile.tt
CHANGED
|
@@ -38,7 +38,7 @@ gem "<%= driver_gem %>", "<%= driver_version %>" # the <%= database_adapter %> d
|
|
|
38
38
|
# gem "stripe"
|
|
39
39
|
|
|
40
40
|
group :development, :test do
|
|
41
|
-
gem "irb" # `gemstack console
|
|
41
|
+
gem "irb" # `gemstack console` (a bundled gem, not a default one, in newer Rubies)
|
|
42
42
|
gem "minitest", "~> 5.25"
|
|
43
43
|
gem "rack-test", "~> 2.2"
|
|
44
44
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= node_version %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= node_version %>
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/* The GemStack welcome page (app/page.tsx). Delete both files when you replace it. */
|
|
2
|
+
|
|
3
|
+
.shell {
|
|
4
|
+
--ruby: #e11d48;
|
|
5
|
+
--violet: #7c3aed;
|
|
6
|
+
--ink: #16181d;
|
|
7
|
+
--muted: #5b6170;
|
|
8
|
+
--surface: rgba(255, 255, 255, 0.72);
|
|
9
|
+
--border: rgba(22, 24, 29, 0.09);
|
|
10
|
+
--code: #f3f4f7;
|
|
11
|
+
--ok: #16a34a;
|
|
12
|
+
--pending: #d99a1e;
|
|
13
|
+
--error: #dc2626;
|
|
14
|
+
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
color: var(--ink);
|
|
19
|
+
background:
|
|
20
|
+
radial-gradient(60rem 30rem at 10% -10%, rgba(225, 29, 72, 0.12), transparent 60%),
|
|
21
|
+
radial-gradient(50rem 30rem at 100% 0%, rgba(124, 58, 237, 0.12), transparent 60%),
|
|
22
|
+
#fbfbfd;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (prefers-color-scheme: dark) {
|
|
26
|
+
.shell {
|
|
27
|
+
--ink: #eceef4;
|
|
28
|
+
--muted: #9aa0ad;
|
|
29
|
+
--surface: rgba(24, 27, 36, 0.72);
|
|
30
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
31
|
+
--code: rgba(255, 255, 255, 0.06);
|
|
32
|
+
--ok: #4ade80;
|
|
33
|
+
--pending: #fbbf24;
|
|
34
|
+
--error: #f87171;
|
|
35
|
+
|
|
36
|
+
background:
|
|
37
|
+
radial-gradient(60rem 30rem at 10% -10%, rgba(225, 29, 72, 0.22), transparent 60%),
|
|
38
|
+
radial-gradient(50rem 30rem at 100% 0%, rgba(124, 58, 237, 0.22), transparent 60%),
|
|
39
|
+
#0c0d12;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.main {
|
|
44
|
+
flex: 1;
|
|
45
|
+
width: 100%;
|
|
46
|
+
max-width: 68rem;
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
padding: clamp(3rem, 8vw, 6rem) 1.25rem 3rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Hero */
|
|
52
|
+
|
|
53
|
+
.hero {
|
|
54
|
+
text-align: center;
|
|
55
|
+
margin-bottom: clamp(2.5rem, 6vw, 4rem);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.logo {
|
|
59
|
+
width: 5rem;
|
|
60
|
+
height: 5rem;
|
|
61
|
+
filter: drop-shadow(0 12px 24px rgba(225, 29, 72, 0.35));
|
|
62
|
+
animation: float 6s ease-in-out infinite;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes float {
|
|
66
|
+
50% {
|
|
67
|
+
transform: translateY(-6px) rotate(-2deg);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
.logo {
|
|
73
|
+
animation: none;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.eyebrow {
|
|
78
|
+
margin: 1rem 0 0.5rem;
|
|
79
|
+
font-size: 0.8rem;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
letter-spacing: 0.18em;
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
color: var(--muted);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.title {
|
|
87
|
+
margin: 0;
|
|
88
|
+
font-size: clamp(2.2rem, 6vw, 3.6rem);
|
|
89
|
+
line-height: 1.1;
|
|
90
|
+
letter-spacing: -0.03em;
|
|
91
|
+
font-weight: 800;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.accent {
|
|
95
|
+
background: linear-gradient(120deg, #ff5f8f, var(--ruby) 45%, var(--violet));
|
|
96
|
+
-webkit-background-clip: text;
|
|
97
|
+
background-clip: text;
|
|
98
|
+
color: transparent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.lead {
|
|
102
|
+
max-width: 38rem;
|
|
103
|
+
margin: 1.1rem auto 0;
|
|
104
|
+
font-size: clamp(1.02rem, 2.2vw, 1.2rem);
|
|
105
|
+
color: var(--muted);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.lead strong {
|
|
109
|
+
color: var(--ink);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Live checks */
|
|
113
|
+
|
|
114
|
+
.checks {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-wrap: wrap;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
gap: 0.6rem;
|
|
119
|
+
margin-top: 2rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.check {
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 0.55rem;
|
|
126
|
+
padding: 0.5rem 0.95rem;
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
background: var(--surface);
|
|
130
|
+
backdrop-filter: blur(8px);
|
|
131
|
+
font-size: 0.92rem;
|
|
132
|
+
overflow-wrap: anywhere;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.check code {
|
|
136
|
+
font-size: 0.85em;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dot {
|
|
140
|
+
width: 0.55rem;
|
|
141
|
+
height: 0.55rem;
|
|
142
|
+
flex: none;
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
background: currentColor;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ok .dot {
|
|
148
|
+
background: var(--ok);
|
|
149
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 22%, transparent);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.pending .dot {
|
|
153
|
+
background: var(--pending);
|
|
154
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.error .dot {
|
|
158
|
+
background: var(--error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@keyframes pulse {
|
|
162
|
+
50% {
|
|
163
|
+
opacity: 0.35;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Cards */
|
|
168
|
+
|
|
169
|
+
.grid {
|
|
170
|
+
display: grid;
|
|
171
|
+
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
|
172
|
+
gap: 1rem;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.card {
|
|
176
|
+
position: relative;
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: 0.6rem;
|
|
180
|
+
padding: 1.4rem;
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 16px;
|
|
183
|
+
background: var(--surface);
|
|
184
|
+
backdrop-filter: blur(8px);
|
|
185
|
+
transition:
|
|
186
|
+
transform 0.2s ease,
|
|
187
|
+
border-color 0.2s ease,
|
|
188
|
+
box-shadow 0.2s ease;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.card:hover {
|
|
192
|
+
transform: translateY(-2px);
|
|
193
|
+
border-color: color-mix(in srgb, var(--ruby) 35%, var(--border));
|
|
194
|
+
box-shadow: 0 16px 40px -24px rgba(225, 29, 72, 0.45);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.step {
|
|
198
|
+
display: grid;
|
|
199
|
+
place-items: center;
|
|
200
|
+
width: 1.9rem;
|
|
201
|
+
height: 1.9rem;
|
|
202
|
+
border-radius: 10px;
|
|
203
|
+
background: linear-gradient(135deg, var(--ruby), var(--violet));
|
|
204
|
+
color: #fff;
|
|
205
|
+
font-size: 0.85rem;
|
|
206
|
+
font-weight: 700;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.cardTitle {
|
|
210
|
+
margin: 0.3rem 0 0;
|
|
211
|
+
font-size: 1.08rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.cardText {
|
|
215
|
+
flex: 1;
|
|
216
|
+
margin: 0;
|
|
217
|
+
color: var(--muted);
|
|
218
|
+
font-size: 0.93rem;
|
|
219
|
+
line-height: 1.5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.command {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: flex-start;
|
|
225
|
+
gap: 0.5rem;
|
|
226
|
+
padding: 0.55rem 0.6rem 0.55rem 0.8rem;
|
|
227
|
+
border-radius: 10px;
|
|
228
|
+
background: var(--code);
|
|
229
|
+
font-size: 0.82rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.command code {
|
|
233
|
+
flex: 1;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
padding: 0.15rem 0 0;
|
|
236
|
+
background: none;
|
|
237
|
+
line-height: 1.5;
|
|
238
|
+
overflow-wrap: anywhere; /* long commands wrap instead of being cut off */
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.prompt {
|
|
242
|
+
color: var(--ruby);
|
|
243
|
+
user-select: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.copy {
|
|
247
|
+
flex: none;
|
|
248
|
+
padding: 0.25rem 0.6rem;
|
|
249
|
+
border: 1px solid var(--border);
|
|
250
|
+
border-radius: 7px;
|
|
251
|
+
background: transparent;
|
|
252
|
+
color: var(--ink);
|
|
253
|
+
font: inherit;
|
|
254
|
+
font-size: 0.75rem;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.copy:hover,
|
|
259
|
+
.copy:focus-visible {
|
|
260
|
+
border-color: var(--ruby);
|
|
261
|
+
color: var(--ruby);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.link {
|
|
265
|
+
align-self: flex-start;
|
|
266
|
+
color: var(--ruby);
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
text-decoration: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.link:hover,
|
|
272
|
+
.link:focus-visible {
|
|
273
|
+
text-decoration: underline;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.edit {
|
|
277
|
+
margin: 2.5rem 0 0;
|
|
278
|
+
text-align: center;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
font-size: 0.92rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Footer */
|
|
284
|
+
|
|
285
|
+
.footer {
|
|
286
|
+
display: flex;
|
|
287
|
+
flex-wrap: wrap;
|
|
288
|
+
justify-content: space-between;
|
|
289
|
+
gap: 0.5rem 1.5rem;
|
|
290
|
+
width: 100%;
|
|
291
|
+
max-width: 68rem;
|
|
292
|
+
margin: 0 auto;
|
|
293
|
+
padding: 1.5rem 1.25rem 2rem;
|
|
294
|
+
border-top: 1px solid var(--border);
|
|
295
|
+
color: var(--muted);
|
|
296
|
+
font-size: 0.85rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.footer a {
|
|
300
|
+
color: var(--ink);
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
text-decoration: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.footer a:hover,
|
|
306
|
+
.footer a:focus-visible {
|
|
307
|
+
color: var(--ruby);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@media (max-width: 40rem) {
|
|
311
|
+
.footer {
|
|
312
|
+
justify-content: center;
|
|
313
|
+
text-align: center;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -1,45 +1,183 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useState } from "react";
|
|
4
5
|
import { api, API_PATH } from "@/lib/gemstack/client";
|
|
6
|
+
import styles from "./page.module.css";
|
|
5
7
|
|
|
6
|
-
//
|
|
7
|
-
// Replace it with your
|
|
8
|
-
|
|
8
|
+
// The GemStack welcome page: shows that Next.js renders and that the browser
|
|
9
|
+
// reaches the Ruby API through the same origin. Replace it with your app —
|
|
10
|
+
// this file and app/page.module.css are yours to delete.
|
|
11
|
+
|
|
12
|
+
const DOCS = "https://github.com/gemstack-rb/gemstack/tree/main/docs";
|
|
13
|
+
|
|
14
|
+
export default function Welcome() {
|
|
9
15
|
const health = useQuery({
|
|
10
|
-
queryKey: ["health"],
|
|
11
|
-
queryFn: () =>
|
|
16
|
+
queryKey: ["gemstack", "health"],
|
|
17
|
+
queryFn: async () => {
|
|
18
|
+
const started = performance.now();
|
|
19
|
+
const body = await api.get<{ status: string }>("/health");
|
|
20
|
+
return { ...body, ms: Math.round(performance.now() - started) };
|
|
21
|
+
},
|
|
22
|
+
retry: 1,
|
|
12
23
|
});
|
|
24
|
+
const apiState = health.isPending ? "pending" : health.isError ? "error" : "ok";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={styles.shell}>
|
|
28
|
+
<main className={styles.main}>
|
|
29
|
+
<header className={styles.hero}>
|
|
30
|
+
<GemLogo className={styles.logo} />
|
|
31
|
+
<p className={styles.eyebrow}>GemStack</p>
|
|
32
|
+
<h1 className={styles.title}>
|
|
33
|
+
Yay! You're on <span className={styles.accent}>GemStack</span>.
|
|
34
|
+
</h1>
|
|
35
|
+
<p className={styles.lead}>
|
|
36
|
+
<strong><%= module_name %></strong> is running: a Next.js frontend and a Ruby API, served from one origin.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<div className={styles.checks} role="status" aria-live="polite">
|
|
40
|
+
<span className={`${styles.check} ${styles.ok}`}>
|
|
41
|
+
<span className={styles.dot} aria-hidden="true" /> Next.js rendering
|
|
42
|
+
</span>
|
|
43
|
+
<span className={`${styles.check} ${styles[apiState]}`}>
|
|
44
|
+
<span className={styles.dot} aria-hidden="true" />
|
|
45
|
+
{apiState === "pending" && "Connecting to the Ruby API…"}
|
|
46
|
+
{apiState === "ok" && (
|
|
47
|
+
<>
|
|
48
|
+
Ruby API connected · <code>GET {API_PATH}/health</code> in {health.data?.ms} ms
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
{apiState === "error" && <>Ruby API unreachable: {health.error?.message}</>}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
13
55
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
56
|
+
<section className={styles.grid} aria-label="Next steps">
|
|
57
|
+
<Card
|
|
58
|
+
step="1"
|
|
59
|
+
title="Generate a resource"
|
|
60
|
+
text="Migration, model, API, typed TypeScript client and Next.js pages — in one command."
|
|
61
|
+
command="gemstack g resource Product name:string"
|
|
62
|
+
/>
|
|
63
|
+
<Card
|
|
64
|
+
step="2"
|
|
65
|
+
title="Explore your API"
|
|
66
|
+
text="Every endpoint with its types, and a form to try requests from the browser."
|
|
67
|
+
href={`${API_PATH}/docs`}
|
|
68
|
+
linkLabel={`Open ${API_PATH}/docs`}
|
|
69
|
+
/>
|
|
70
|
+
<Card
|
|
71
|
+
step="3"
|
|
72
|
+
title="Check your setup"
|
|
73
|
+
text="Ruby, Node.js, the database, migrations and the TypeScript contract — with the fix for anything off."
|
|
74
|
+
command="gemstack doctor"
|
|
75
|
+
/>
|
|
76
|
+
<Card
|
|
77
|
+
step="4"
|
|
78
|
+
title="Read the guides"
|
|
79
|
+
text="Models, controllers, jobs, realtime, authentication, storage and deployment."
|
|
80
|
+
href={DOCS}
|
|
81
|
+
linkLabel="Open the guides"
|
|
82
|
+
external
|
|
83
|
+
/>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<p className={styles.edit}>
|
|
87
|
+
Edit <code>frontend/app/page.tsx</code> to replace this page · API code lives in <code>app/</code>
|
|
88
|
+
</p>
|
|
89
|
+
</main>
|
|
90
|
+
|
|
91
|
+
<footer className={styles.footer}>
|
|
92
|
+
<span>
|
|
93
|
+
GemStack <%= version %> · Ruby <%= ruby_version %> · Next.js
|
|
94
|
+
</span>
|
|
95
|
+
<span>
|
|
96
|
+
Open source by{" "}
|
|
97
|
+
<a href="https://www.adwaretech.com" target="_blank" rel="noreferrer">
|
|
98
|
+
Adware Technologies
|
|
99
|
+
</a>
|
|
100
|
+
</span>
|
|
101
|
+
</footer>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Card(props: {
|
|
107
|
+
step: string;
|
|
108
|
+
title: string;
|
|
109
|
+
text: string;
|
|
110
|
+
command?: string;
|
|
111
|
+
href?: string;
|
|
112
|
+
linkLabel?: string;
|
|
113
|
+
external?: boolean;
|
|
114
|
+
}) {
|
|
115
|
+
return (
|
|
116
|
+
<article className={styles.card}>
|
|
117
|
+
<span className={styles.step} aria-hidden="true">
|
|
118
|
+
{props.step}
|
|
119
|
+
</span>
|
|
120
|
+
<h2 className={styles.cardTitle}>{props.title}</h2>
|
|
121
|
+
<p className={styles.cardText}>{props.text}</p>
|
|
122
|
+
{props.command && <Command text={props.command} />}
|
|
123
|
+
{props.href && (
|
|
124
|
+
<a
|
|
125
|
+
className={styles.link}
|
|
126
|
+
href={props.href}
|
|
127
|
+
{...(props.external ? { target: "_blank", rel: "noreferrer" } : {})}
|
|
128
|
+
>
|
|
129
|
+
{props.linkLabel} <span aria-hidden="true">→</span>
|
|
130
|
+
</a>
|
|
131
|
+
)}
|
|
132
|
+
</article>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function Command({ text }: { text: string }) {
|
|
137
|
+
const [copied, setCopied] = useState(false);
|
|
138
|
+
async function copy() {
|
|
139
|
+
try {
|
|
140
|
+
await navigator.clipboard.writeText(text);
|
|
141
|
+
setCopied(true);
|
|
142
|
+
setTimeout(() => setCopied(false), 1500);
|
|
143
|
+
} catch {
|
|
144
|
+
// Clipboard unavailable (e.g. not a secure context): the command stays selectable.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return (
|
|
148
|
+
<div className={styles.command}>
|
|
149
|
+
<code>
|
|
150
|
+
<span className={styles.prompt} aria-hidden="true">
|
|
151
|
+
$
|
|
152
|
+
</span>{" "}
|
|
153
|
+
{text}
|
|
154
|
+
</code>
|
|
155
|
+
<button type="button" className={styles.copy} onClick={copy} aria-label={`Copy: ${text}`}>
|
|
156
|
+
{copied ? "Copied" : "Copy"}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
20
161
|
|
|
162
|
+
function GemLogo({ className }: { className?: string }) {
|
|
21
163
|
return (
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Edit this page: <code>frontend/app/page.tsx</code>
|
|
41
|
-
</li>
|
|
42
|
-
</ul>
|
|
43
|
-
</main>
|
|
164
|
+
<svg className={className} viewBox="0 0 64 64" role="img" aria-label="GemStack">
|
|
165
|
+
<defs>
|
|
166
|
+
<linearGradient id="gemstack-gem" x1="0" y1="0" x2="1" y2="1">
|
|
167
|
+
<stop offset="0" stopColor="#ff5f8f" />
|
|
168
|
+
<stop offset="0.55" stopColor="#e11d48" />
|
|
169
|
+
<stop offset="1" stopColor="#7c3aed" />
|
|
170
|
+
</linearGradient>
|
|
171
|
+
</defs>
|
|
172
|
+
<path d="M18 8h28l12 15-26 34L6 23z" fill="url(#gemstack-gem)" />
|
|
173
|
+
<path
|
|
174
|
+
d="M6 23h52M18 8l7 15 7-15 7 15 7-15M25 23l7 34 7-34"
|
|
175
|
+
fill="none"
|
|
176
|
+
stroke="rgba(255,255,255,0.55)"
|
|
177
|
+
strokeWidth="1.4"
|
|
178
|
+
strokeLinejoin="round"
|
|
179
|
+
/>
|
|
180
|
+
<path d="M18 8h28l-7 15H25z" fill="rgba(255,255,255,0.18)" />
|
|
181
|
+
</svg>
|
|
44
182
|
);
|
|
45
183
|
}
|
metadata
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstack-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
+
- Adware Technologies
|
|
7
8
|
- Shoaib Malik
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
@@ -15,28 +16,28 @@ dependencies:
|
|
|
15
16
|
requirements:
|
|
16
17
|
- - '='
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.2.
|
|
19
|
+
version: 0.2.3
|
|
19
20
|
type: :runtime
|
|
20
21
|
prerelease: false
|
|
21
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
23
|
requirements:
|
|
23
24
|
- - '='
|
|
24
25
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.2.
|
|
26
|
+
version: 0.2.3
|
|
26
27
|
- !ruby/object:Gem::Dependency
|
|
27
28
|
name: gemstack-dev
|
|
28
29
|
requirement: !ruby/object:Gem::Requirement
|
|
29
30
|
requirements:
|
|
30
31
|
- - '='
|
|
31
32
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.2.
|
|
33
|
+
version: 0.2.3
|
|
33
34
|
type: :runtime
|
|
34
35
|
prerelease: false
|
|
35
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
37
|
requirements:
|
|
37
38
|
- - '='
|
|
38
39
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.2.
|
|
40
|
+
version: 0.2.3
|
|
40
41
|
- !ruby/object:Gem::Dependency
|
|
41
42
|
name: thor
|
|
42
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -98,6 +99,8 @@ files:
|
|
|
98
99
|
- templates/app/db/seeds.rb
|
|
99
100
|
- templates/app/dot_env.example.tt
|
|
100
101
|
- templates/app/dot_gitignore
|
|
102
|
+
- templates/app/dot_node-version.tt
|
|
103
|
+
- templates/app/dot_nvmrc.tt
|
|
101
104
|
- templates/app/dot_ruby-version.tt
|
|
102
105
|
- templates/app/dot_tool-versions.tt
|
|
103
106
|
- templates/app/test/health_test.rb
|
|
@@ -139,6 +142,7 @@ files:
|
|
|
139
142
|
- templates/deploy/dot_dockerignore
|
|
140
143
|
- templates/frontend/app/globals.css
|
|
141
144
|
- templates/frontend/app/layout.tsx.tt
|
|
145
|
+
- templates/frontend/app/page.module.css
|
|
142
146
|
- templates/frontend/app/page.tsx.tt
|
|
143
147
|
- templates/frontend/app/providers.tsx
|
|
144
148
|
- templates/frontend/lib/gemstack/client.ts
|
|
@@ -188,7 +192,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
188
192
|
requirements:
|
|
189
193
|
- - ">="
|
|
190
194
|
- !ruby/object:Gem::Version
|
|
191
|
-
version: '
|
|
195
|
+
version: '3.3'
|
|
192
196
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
197
|
requirements:
|
|
194
198
|
- - ">="
|