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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d9243d3986f9d45d5aa9de84f699ea7fcac37504ed266d7998e5a4eab6ff484
4
- data.tar.gz: 6cf9300583fb65e103c7b71b48859721863766837180a362365639ec0186c5fc
3
+ metadata.gz: 3b7ea1041a871d84d72d33529ee4b303145363665a6605d0cc5d533fd4604157
4
+ data.tar.gz: 02625e8747b1efe7fae2191d356376bc8bdde1500d892cef5f945badb1d5451d
5
5
  SHA512:
6
- metadata.gz: 54009542d20d3bcc5ae67e2b89e19965c285e32b6400ca43e7ecf22af1f2bc3a8303803c2c1708cc967f5c50fed4c159e7807e5f41bdb9da702c87f9cc4e8229
7
- data.tar.gz: 0acb8e759970a5154f11fae58d530c960575849ec4f93018f46b9a8ef70f1d43cfac289c0606ae16508efd16f843ade368b0fa49777e15caacdb42e42e9db22a
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 Shoaib Malik
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 — see [LICENSE.txt](LICENSE.txt).
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 (.ruby-version, .tool-versions).
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]
@@ -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 wanted && !wanted.empty? && !RUBY_VERSION.start_with?(wanted)
54
- caution("Ruby #{RUBY_VERSION}",
55
- "the app pins #{wanted} (.ruby-version) — install it: asdf install ruby #{wanted}")
56
- elsif Gem::Version.new(RUBY_VERSION) < Gem::Version.new("4.0")
57
- problem("Ruby #{RUBY_VERSION}", "GemStack needs Ruby 4.0 or newer")
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
- out, status = @run.call("node", "--version")
67
- version = status.success? ? out.strip.delete_prefix("v") : nil
68
- if version.nil?
69
- problem("Node.js not found", "install Node.js #{MIN_NODE.join(".")}+ (the frontend is Next.js)")
70
- elsif (version.split(".").map(&:to_i) <=> MIN_NODE).negative?
71
- problem("Node.js #{version}", "Next.js 16 needs Node.js #{MIN_NODE.join(".")} or newer")
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
@@ -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`; a bundled (not default) gem since Ruby 4.0
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 %>
@@ -1 +1,4 @@
1
1
  ruby <%= ruby_version %>
2
+ <% if frontend? -%>
3
+ nodejs <%= node_version %>
4
+ <% end -%>
@@ -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
- // Starter page: confirms the browser reaches the Ruby API through the same origin.
7
- // Replace it with your application.
8
- export default function Home() {
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: () => api.get<{ status: string }>("/health"),
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&apos;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
- const state = health.isPending ? "pending" : health.isError ? "error" : "ok";
15
- const label = {
16
- pending: "Connecting to the Ruby API…",
17
- error: `Ruby API unreachable: ${health.error?.message}`,
18
- ok: `Ruby API connected — GET ${API_PATH}/health → ${health.data?.status}`,
19
- }[state];
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
- <main className="page">
23
- <h1><%= module_name %></h1>
24
- <p className="muted">Next.js + TypeScript frontend, GemStack Ruby API, one origin.</p>
25
-
26
- <p className={`status status-${state}`} role="status">
27
- <span className="dot" aria-hidden="true" />
28
- {label}
29
- </p>
30
-
31
- <h2>Next steps</h2>
32
- <ul>
33
- <li>
34
- Add an endpoint: <code>gemstack generate controller Status show</code>
35
- </li>
36
- <li>
37
- Call it from here: <code>api.get(&quot;/status/1&quot;)</code> (see <code>lib/gemstack/client.ts</code>)
38
- </li>
39
- <li>
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.1
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.1
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.1
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.1
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.1
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: '4.0'
195
+ version: '3.3'
192
196
  required_rubygems_version: !ruby/object:Gem::Requirement
193
197
  requirements:
194
198
  - - ">="