shopify-starter-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/skills/shopify-apps/SKILL.md +47 -0
- package/.agent/skills/shopify-automation/SKILL.md +172 -0
- package/.agent/skills/shopify-development/README.md +60 -0
- package/.agent/skills/shopify-development/SKILL.md +368 -0
- package/.agent/skills/shopify-development/references/app-development.md +578 -0
- package/.agent/skills/shopify-development/references/extensions.md +555 -0
- package/.agent/skills/shopify-development/references/themes.md +498 -0
- package/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
- package/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
- package/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
- package/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/bin/cli.js +3 -0
- package/package.json +32 -0
- package/src/index.js +116 -0
- package/templates/.agent/skills/shopify-apps/SKILL.md +47 -0
- package/templates/.agent/skills/shopify-automation/SKILL.md +172 -0
- package/templates/.agent/skills/shopify-development/README.md +60 -0
- package/templates/.agent/skills/shopify-development/SKILL.md +368 -0
- package/templates/.agent/skills/shopify-development/references/app-development.md +578 -0
- package/templates/.agent/skills/shopify-development/references/extensions.md +555 -0
- package/templates/.agent/skills/shopify-development/references/themes.md +498 -0
- package/templates/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
- package/templates/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
- package/templates/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
- package/templates/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/templates/.devcontainer/devcontainer.json +27 -0
- package/templates/tests/playwright.config.ts +26 -0
- package/templates/tests/vitest.config.ts +9 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for shopify_init.py
|
|
3
|
+
|
|
4
|
+
Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import pytest
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import Mock, patch, mock_open, MagicMock
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestEnvLoader:
|
|
21
|
+
"""Test EnvLoader class."""
|
|
22
|
+
|
|
23
|
+
def test_load_env_file_success(self, tmp_path):
|
|
24
|
+
"""Test loading valid .env file."""
|
|
25
|
+
env_file = tmp_path / ".env"
|
|
26
|
+
env_file.write_text("""
|
|
27
|
+
SHOPIFY_API_KEY=test_key
|
|
28
|
+
SHOPIFY_API_SECRET=test_secret
|
|
29
|
+
SHOP_DOMAIN=test.myshopify.com
|
|
30
|
+
# Comment line
|
|
31
|
+
SCOPES=read_products,write_products
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
result = EnvLoader.load_env_file(env_file)
|
|
35
|
+
|
|
36
|
+
assert result['SHOPIFY_API_KEY'] == 'test_key'
|
|
37
|
+
assert result['SHOPIFY_API_SECRET'] == 'test_secret'
|
|
38
|
+
assert result['SHOP_DOMAIN'] == 'test.myshopify.com'
|
|
39
|
+
assert result['SCOPES'] == 'read_products,write_products'
|
|
40
|
+
|
|
41
|
+
def test_load_env_file_with_quotes(self, tmp_path):
|
|
42
|
+
"""Test loading .env file with quoted values."""
|
|
43
|
+
env_file = tmp_path / ".env"
|
|
44
|
+
env_file.write_text("""
|
|
45
|
+
SHOPIFY_API_KEY="test_key"
|
|
46
|
+
SHOPIFY_API_SECRET='test_secret'
|
|
47
|
+
""")
|
|
48
|
+
|
|
49
|
+
result = EnvLoader.load_env_file(env_file)
|
|
50
|
+
|
|
51
|
+
assert result['SHOPIFY_API_KEY'] == 'test_key'
|
|
52
|
+
assert result['SHOPIFY_API_SECRET'] == 'test_secret'
|
|
53
|
+
|
|
54
|
+
def test_load_env_file_nonexistent(self, tmp_path):
|
|
55
|
+
"""Test loading non-existent .env file."""
|
|
56
|
+
result = EnvLoader.load_env_file(tmp_path / "nonexistent.env")
|
|
57
|
+
assert result == {}
|
|
58
|
+
|
|
59
|
+
def test_load_env_file_invalid_format(self, tmp_path):
|
|
60
|
+
"""Test loading .env file with invalid lines."""
|
|
61
|
+
env_file = tmp_path / ".env"
|
|
62
|
+
env_file.write_text("""
|
|
63
|
+
VALID_KEY=value
|
|
64
|
+
INVALID_LINE_NO_EQUALS
|
|
65
|
+
ANOTHER_VALID=test
|
|
66
|
+
""")
|
|
67
|
+
|
|
68
|
+
result = EnvLoader.load_env_file(env_file)
|
|
69
|
+
|
|
70
|
+
assert result['VALID_KEY'] == 'value'
|
|
71
|
+
assert result['ANOTHER_VALID'] == 'test'
|
|
72
|
+
assert 'INVALID_LINE_NO_EQUALS' not in result
|
|
73
|
+
|
|
74
|
+
def test_get_env_paths(self, tmp_path):
|
|
75
|
+
"""Test getting .env file paths from universal directory structure."""
|
|
76
|
+
# Create directory structure (works with .agent, .claude, .gemini, .cursor)
|
|
77
|
+
agent_dir = tmp_path / ".agent"
|
|
78
|
+
skills_dir = agent_dir / "skills"
|
|
79
|
+
skill_dir = skills_dir / "shopify"
|
|
80
|
+
|
|
81
|
+
skill_dir.mkdir(parents=True)
|
|
82
|
+
|
|
83
|
+
# Create .env files at each level
|
|
84
|
+
(skill_dir / ".env").write_text("SKILL=1")
|
|
85
|
+
(skills_dir / ".env").write_text("SKILLS=1")
|
|
86
|
+
(agent_dir / ".env").write_text("AGENT=1")
|
|
87
|
+
|
|
88
|
+
paths = EnvLoader.get_env_paths(skill_dir)
|
|
89
|
+
|
|
90
|
+
assert len(paths) == 3
|
|
91
|
+
assert skill_dir / ".env" in paths
|
|
92
|
+
assert skills_dir / ".env" in paths
|
|
93
|
+
assert agent_dir / ".env" in paths
|
|
94
|
+
|
|
95
|
+
def test_load_config_priority(self, tmp_path, monkeypatch):
|
|
96
|
+
"""Test configuration loading priority across different AI tool directories."""
|
|
97
|
+
skill_dir = tmp_path / "skill"
|
|
98
|
+
skills_dir = tmp_path
|
|
99
|
+
agent_dir = tmp_path.parent # Could be .agent, .claude, .gemini, .cursor
|
|
100
|
+
|
|
101
|
+
skill_dir.mkdir(parents=True)
|
|
102
|
+
|
|
103
|
+
(skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key")
|
|
104
|
+
(skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com")
|
|
105
|
+
|
|
106
|
+
monkeypatch.setenv("SHOPIFY_API_KEY", "process_key")
|
|
107
|
+
|
|
108
|
+
config = EnvLoader.load_config(skill_dir)
|
|
109
|
+
|
|
110
|
+
assert config.shopify_api_key == "process_key"
|
|
111
|
+
# Shop domain from skills/.env
|
|
112
|
+
assert config.shop_domain == "skills.myshopify.com"
|
|
113
|
+
|
|
114
|
+
def test_load_config_no_files(self, tmp_path):
|
|
115
|
+
"""Test configuration loading with no .env files."""
|
|
116
|
+
config = EnvLoader.load_config(tmp_path)
|
|
117
|
+
|
|
118
|
+
assert config.shopify_api_key is None
|
|
119
|
+
assert config.shopify_api_secret is None
|
|
120
|
+
assert config.shop_domain is None
|
|
121
|
+
assert config.scopes is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestShopifyInitializer:
|
|
125
|
+
"""Test ShopifyInitializer class."""
|
|
126
|
+
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
def config(self):
|
|
129
|
+
"""Create test config."""
|
|
130
|
+
return EnvConfig(
|
|
131
|
+
shopify_api_key="test_key",
|
|
132
|
+
shopify_api_secret="test_secret",
|
|
133
|
+
shop_domain="test.myshopify.com",
|
|
134
|
+
scopes="read_products,write_products"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@pytest.fixture
|
|
138
|
+
def initializer(self, config):
|
|
139
|
+
"""Create initializer instance."""
|
|
140
|
+
return ShopifyInitializer(config)
|
|
141
|
+
|
|
142
|
+
def test_prompt_with_default(self, initializer):
|
|
143
|
+
"""Test prompt with default value."""
|
|
144
|
+
with patch('builtins.input', return_value=''):
|
|
145
|
+
result = initializer.prompt("Test", "default_value")
|
|
146
|
+
assert result == "default_value"
|
|
147
|
+
|
|
148
|
+
def test_prompt_with_input(self, initializer):
|
|
149
|
+
"""Test prompt with user input."""
|
|
150
|
+
with patch('builtins.input', return_value='user_input'):
|
|
151
|
+
result = initializer.prompt("Test", "default_value")
|
|
152
|
+
assert result == "user_input"
|
|
153
|
+
|
|
154
|
+
def test_select_option_valid(self, initializer):
|
|
155
|
+
"""Test select option with valid choice."""
|
|
156
|
+
options = ['app', 'extension', 'theme']
|
|
157
|
+
with patch('builtins.input', return_value='2'):
|
|
158
|
+
result = initializer.select_option("Choose", options)
|
|
159
|
+
assert result == 'extension'
|
|
160
|
+
|
|
161
|
+
def test_select_option_invalid_then_valid(self, initializer):
|
|
162
|
+
"""Test select option with invalid then valid choice."""
|
|
163
|
+
options = ['app', 'extension']
|
|
164
|
+
with patch('builtins.input', side_effect=['5', 'invalid', '1']):
|
|
165
|
+
result = initializer.select_option("Choose", options)
|
|
166
|
+
assert result == 'app'
|
|
167
|
+
|
|
168
|
+
def test_check_cli_installed_success(self, initializer):
|
|
169
|
+
"""Test CLI installed check - success."""
|
|
170
|
+
mock_result = Mock()
|
|
171
|
+
mock_result.returncode = 0
|
|
172
|
+
|
|
173
|
+
with patch('subprocess.run', return_value=mock_result):
|
|
174
|
+
assert initializer.check_cli_installed() is True
|
|
175
|
+
|
|
176
|
+
def test_check_cli_installed_failure(self, initializer):
|
|
177
|
+
"""Test CLI installed check - failure."""
|
|
178
|
+
with patch('subprocess.run', side_effect=FileNotFoundError):
|
|
179
|
+
assert initializer.check_cli_installed() is False
|
|
180
|
+
|
|
181
|
+
def test_create_app_config(self, initializer, tmp_path):
|
|
182
|
+
"""Test creating app configuration file."""
|
|
183
|
+
initializer.create_app_config(tmp_path, "test-app", "read_products")
|
|
184
|
+
|
|
185
|
+
config_file = tmp_path / "shopify.app.toml"
|
|
186
|
+
assert config_file.exists()
|
|
187
|
+
|
|
188
|
+
content = config_file.read_text()
|
|
189
|
+
assert 'name = "test-app"' in content
|
|
190
|
+
assert 'scopes = "read_products"' in content
|
|
191
|
+
assert 'client_id = "test_key"' in content
|
|
192
|
+
|
|
193
|
+
def test_create_extension_config(self, initializer, tmp_path):
|
|
194
|
+
"""Test creating extension configuration file."""
|
|
195
|
+
initializer.create_extension_config(tmp_path, "test-ext", "checkout")
|
|
196
|
+
|
|
197
|
+
config_file = tmp_path / "shopify.extension.toml"
|
|
198
|
+
assert config_file.exists()
|
|
199
|
+
|
|
200
|
+
content = config_file.read_text()
|
|
201
|
+
assert 'name = "test-ext"' in content
|
|
202
|
+
assert 'purchase.checkout.block.render' in content
|
|
203
|
+
|
|
204
|
+
def test_create_extension_config_admin_action(self, initializer, tmp_path):
|
|
205
|
+
"""Test creating admin action extension config."""
|
|
206
|
+
initializer.create_extension_config(tmp_path, "admin-ext", "admin_action")
|
|
207
|
+
|
|
208
|
+
config_file = tmp_path / "shopify.extension.toml"
|
|
209
|
+
content = config_file.read_text()
|
|
210
|
+
assert 'admin.product-details.action.render' in content
|
|
211
|
+
|
|
212
|
+
def test_create_readme(self, initializer, tmp_path):
|
|
213
|
+
"""Test creating README file."""
|
|
214
|
+
initializer.create_readme(tmp_path, "app", "Test App")
|
|
215
|
+
|
|
216
|
+
readme_file = tmp_path / "README.md"
|
|
217
|
+
assert readme_file.exists()
|
|
218
|
+
|
|
219
|
+
content = readme_file.read_text()
|
|
220
|
+
assert '# Test App' in content
|
|
221
|
+
assert 'shopify app dev' in content
|
|
222
|
+
|
|
223
|
+
@patch('builtins.input')
|
|
224
|
+
@patch('builtins.print')
|
|
225
|
+
def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
|
|
226
|
+
"""Test app initialization."""
|
|
227
|
+
monkeypatch.chdir(tmp_path)
|
|
228
|
+
|
|
229
|
+
# Mock user inputs
|
|
230
|
+
mock_input.side_effect = ['my-app', 'read_products,write_products']
|
|
231
|
+
|
|
232
|
+
initializer.init_app()
|
|
233
|
+
|
|
234
|
+
# Check directory created
|
|
235
|
+
app_dir = tmp_path / "my-app"
|
|
236
|
+
assert app_dir.exists()
|
|
237
|
+
|
|
238
|
+
# Check files created
|
|
239
|
+
assert (app_dir / "shopify.app.toml").exists()
|
|
240
|
+
assert (app_dir / "README.md").exists()
|
|
241
|
+
assert (app_dir / "package.json").exists()
|
|
242
|
+
|
|
243
|
+
# Check package.json content
|
|
244
|
+
package_json = json.loads((app_dir / "package.json").read_text())
|
|
245
|
+
assert package_json['name'] == 'my-app'
|
|
246
|
+
assert 'dev' in package_json['scripts']
|
|
247
|
+
|
|
248
|
+
@patch('builtins.input')
|
|
249
|
+
@patch('builtins.print')
|
|
250
|
+
def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
|
|
251
|
+
"""Test extension initialization."""
|
|
252
|
+
monkeypatch.chdir(tmp_path)
|
|
253
|
+
|
|
254
|
+
# Mock user inputs: type selection (1 = checkout), name
|
|
255
|
+
mock_input.side_effect = ['1', 'my-extension']
|
|
256
|
+
|
|
257
|
+
initializer.init_extension()
|
|
258
|
+
|
|
259
|
+
# Check directory and files created
|
|
260
|
+
ext_dir = tmp_path / "my-extension"
|
|
261
|
+
assert ext_dir.exists()
|
|
262
|
+
assert (ext_dir / "shopify.extension.toml").exists()
|
|
263
|
+
assert (ext_dir / "README.md").exists()
|
|
264
|
+
|
|
265
|
+
@patch('builtins.input')
|
|
266
|
+
@patch('builtins.print')
|
|
267
|
+
def test_init_theme(self, mock_print, mock_input, initializer):
|
|
268
|
+
"""Test theme initialization."""
|
|
269
|
+
mock_input.return_value = 'my-theme'
|
|
270
|
+
|
|
271
|
+
initializer.init_theme()
|
|
272
|
+
|
|
273
|
+
assert mock_print.called
|
|
274
|
+
|
|
275
|
+
@patch('builtins.print')
|
|
276
|
+
def test_run_no_cli(self, mock_print, initializer):
|
|
277
|
+
"""Test run when CLI not installed."""
|
|
278
|
+
with patch.object(initializer, 'check_cli_installed', return_value=False):
|
|
279
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
280
|
+
initializer.run()
|
|
281
|
+
assert exc_info.value.code == 1
|
|
282
|
+
|
|
283
|
+
@patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
|
|
284
|
+
@patch.object(ShopifyInitializer, 'init_app')
|
|
285
|
+
@patch('builtins.input')
|
|
286
|
+
@patch('builtins.print')
|
|
287
|
+
def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer):
|
|
288
|
+
"""Test run with app selection."""
|
|
289
|
+
mock_input.return_value = '1' # Select app
|
|
290
|
+
|
|
291
|
+
initializer.run()
|
|
292
|
+
|
|
293
|
+
mock_init_app.assert_called_once()
|
|
294
|
+
|
|
295
|
+
@patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
|
|
296
|
+
@patch.object(ShopifyInitializer, 'init_extension')
|
|
297
|
+
@patch('builtins.input')
|
|
298
|
+
@patch('builtins.print')
|
|
299
|
+
def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer):
|
|
300
|
+
"""Test run with extension selection."""
|
|
301
|
+
mock_input.return_value = '2' # Select extension
|
|
302
|
+
|
|
303
|
+
initializer.run()
|
|
304
|
+
|
|
305
|
+
mock_init_ext.assert_called_once()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestMain:
|
|
309
|
+
"""Test main function."""
|
|
310
|
+
|
|
311
|
+
@patch('shopify_init.ShopifyInitializer')
|
|
312
|
+
@patch('shopify_init.EnvLoader')
|
|
313
|
+
def test_main_success(self, mock_loader, mock_initializer):
|
|
314
|
+
"""Test main function success path."""
|
|
315
|
+
from shopify_init import main
|
|
316
|
+
|
|
317
|
+
mock_config = Mock()
|
|
318
|
+
mock_loader.load_config.return_value = mock_config
|
|
319
|
+
|
|
320
|
+
mock_init_instance = Mock()
|
|
321
|
+
mock_initializer.return_value = mock_init_instance
|
|
322
|
+
|
|
323
|
+
with patch('builtins.print'):
|
|
324
|
+
main()
|
|
325
|
+
|
|
326
|
+
mock_init_instance.run.assert_called_once()
|
|
327
|
+
|
|
328
|
+
@patch('shopify_init.ShopifyInitializer')
|
|
329
|
+
@patch('sys.exit')
|
|
330
|
+
def test_main_keyboard_interrupt(self, mock_exit, mock_initializer):
|
|
331
|
+
"""Test main function with keyboard interrupt."""
|
|
332
|
+
from shopify_init import main
|
|
333
|
+
|
|
334
|
+
mock_initializer.return_value.run.side_effect = KeyboardInterrupt
|
|
335
|
+
|
|
336
|
+
with patch('builtins.print'):
|
|
337
|
+
main()
|
|
338
|
+
|
|
339
|
+
mock_exit.assert_called_with(0)
|
|
340
|
+
|
|
341
|
+
@patch('shopify_init.ShopifyInitializer')
|
|
342
|
+
@patch('sys.exit')
|
|
343
|
+
def test_main_exception(self, mock_exit, mock_initializer):
|
|
344
|
+
"""Test main function with exception."""
|
|
345
|
+
from shopify_init import main
|
|
346
|
+
|
|
347
|
+
mock_initializer.return_value.run.side_effect = Exception("Test error")
|
|
348
|
+
|
|
349
|
+
with patch('builtins.print'):
|
|
350
|
+
main()
|
|
351
|
+
|
|
352
|
+
mock_exit.assert_called_with(1)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class TestEnvConfig:
|
|
356
|
+
"""Test EnvConfig dataclass."""
|
|
357
|
+
|
|
358
|
+
def test_env_config_defaults(self):
|
|
359
|
+
"""Test EnvConfig default values."""
|
|
360
|
+
config = EnvConfig()
|
|
361
|
+
|
|
362
|
+
assert config.shopify_api_key is None
|
|
363
|
+
assert config.shopify_api_secret is None
|
|
364
|
+
assert config.shop_domain is None
|
|
365
|
+
assert config.scopes is None
|
|
366
|
+
|
|
367
|
+
def test_env_config_with_values(self):
|
|
368
|
+
"""Test EnvConfig with values."""
|
|
369
|
+
config = EnvConfig(
|
|
370
|
+
shopify_api_key="key",
|
|
371
|
+
shopify_api_secret="secret",
|
|
372
|
+
shop_domain="test.myshopify.com",
|
|
373
|
+
scopes="read_products"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
assert config.shopify_api_key == "key"
|
|
377
|
+
assert config.shopify_api_secret == "secret"
|
|
378
|
+
assert config.shop_domain == "test.myshopify.com"
|
|
379
|
+
assert config.scopes == "read_products"
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shopify-starter-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Next-Gen Shopify App CLI with pre-configured agent skills, devcontainers, and testing pipelines.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shopify-starter-kit": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node ./bin/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^4.1.2",
|
|
14
|
+
"commander": "^13.1.0",
|
|
15
|
+
"execa": "^5.1.1",
|
|
16
|
+
"fs-extra": "^11.3.0",
|
|
17
|
+
"prompts": "^2.4.2"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"shopify",
|
|
21
|
+
"cli",
|
|
22
|
+
"agent",
|
|
23
|
+
"skills",
|
|
24
|
+
"remix",
|
|
25
|
+
"devcontainer"
|
|
26
|
+
],
|
|
27
|
+
"author": "exaedge",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { program } = require('commander');
|
|
2
|
+
const prompts = require('prompts');
|
|
3
|
+
const execa = require('execa');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.version('1.0.0')
|
|
10
|
+
.description('Create an Agentic Shopify App with pre-configured developer tools')
|
|
11
|
+
.argument('[project-directory]', 'The directory to create the app in')
|
|
12
|
+
.action(async (projectDirectory) => {
|
|
13
|
+
let targetDir = projectDirectory;
|
|
14
|
+
|
|
15
|
+
if (!targetDir) {
|
|
16
|
+
const response = await prompts({
|
|
17
|
+
type: 'text',
|
|
18
|
+
name: 'dirName',
|
|
19
|
+
message: 'What is the name of your new Shopify app?',
|
|
20
|
+
initial: 'my-shopify-app'
|
|
21
|
+
});
|
|
22
|
+
targetDir = response.dirName;
|
|
23
|
+
|
|
24
|
+
if (!targetDir) {
|
|
25
|
+
console.log(chalk.red('Installation cancelled.'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const appPath = path.resolve(process.cwd(), targetDir);
|
|
31
|
+
if (!fs.existsSync(appPath)) {
|
|
32
|
+
console.error(chalk.red(`\nDirectory ${targetDir} does not exist. Please initialize the Shopify project first.\n`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const choices = await prompts({
|
|
37
|
+
type: 'multiselect',
|
|
38
|
+
name: 'features',
|
|
39
|
+
message: 'Which Next-Gen features do you want to add?',
|
|
40
|
+
choices: [
|
|
41
|
+
{ title: 'AI Coding Assistant Skills (.agent)', value: 'agent', selected: true },
|
|
42
|
+
{ title: 'Pre-configured Dev Environment (.devcontainer)', value: 'devcontainer', selected: true },
|
|
43
|
+
{ title: 'Unified Testing Pipeline (Vitest/Playwright)', value: 'testing', selected: true }
|
|
44
|
+
],
|
|
45
|
+
min: 1,
|
|
46
|
+
hint: '- Space to select. Return to submit'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!choices.features || choices.features.length === 0) {
|
|
50
|
+
console.log(chalk.yellow('\nNo features selected. Exiting.\n'));
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.cyan(`\nš Injecting Agentic Developer Tools into /${targetDir}...`));
|
|
55
|
+
try {
|
|
56
|
+
// Assume Shopify App is already initialized in targetDir
|
|
57
|
+
|
|
58
|
+
console.log(chalk.cyan(`\n⨠Project found. Proceeding with injections...\n`));
|
|
59
|
+
|
|
60
|
+
// Step 2: Inject .agent templates
|
|
61
|
+
if (choices.features.includes('agent')) {
|
|
62
|
+
const templateAgentDir = path.join(__dirname, '../.agent');
|
|
63
|
+
if (fs.existsSync(templateAgentDir)) {
|
|
64
|
+
await fs.copy(templateAgentDir, path.join(appPath, '.agent'));
|
|
65
|
+
console.log(chalk.green('ā Injected AI Coding Assistant Skills (.agent)'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step 3: Inject Devcontainer
|
|
70
|
+
if (choices.features.includes('devcontainer')) {
|
|
71
|
+
const templateDevcontainer = path.join(__dirname, '../templates/.devcontainer');
|
|
72
|
+
if (fs.existsSync(templateDevcontainer)) {
|
|
73
|
+
await fs.copy(templateDevcontainer, path.join(appPath, '.devcontainer'));
|
|
74
|
+
console.log(chalk.green('ā Injected Devcontainer Configuration'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 4: Inject unified testing
|
|
79
|
+
if (choices.features.includes('testing')) {
|
|
80
|
+
const templateTests = path.join(__dirname, '../templates/tests');
|
|
81
|
+
if (fs.existsSync(templateTests)) {
|
|
82
|
+
await fs.copy(templateTests, appPath);
|
|
83
|
+
console.log(chalk.green('ā Injected Testing Pipeline (Vitest/Playwright config)'));
|
|
84
|
+
|
|
85
|
+
// Update generated package.json with test scripts
|
|
86
|
+
const targetPkgPath = path.join(appPath, 'package.json');
|
|
87
|
+
if (fs.existsSync(targetPkgPath)) {
|
|
88
|
+
const targetPkg = await fs.readJson(targetPkgPath);
|
|
89
|
+
targetPkg.scripts = targetPkg.scripts || {};
|
|
90
|
+
targetPkg.scripts['test'] = 'vitest';
|
|
91
|
+
targetPkg.scripts['test:e2e'] = 'playwright test';
|
|
92
|
+
targetPkg.devDependencies = targetPkg.devDependencies || {};
|
|
93
|
+
targetPkg.devDependencies['vitest'] = '^1.0.0';
|
|
94
|
+
targetPkg.devDependencies['@playwright/test'] = '^1.40.0';
|
|
95
|
+
await fs.writeJson(targetPkgPath, targetPkg, { spaces: 2 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(chalk.cyan(`\nš Success! Your Next-Gen Agentic Shopify App is ready.\n`));
|
|
101
|
+
console.log(`Next steps:\n`);
|
|
102
|
+
console.log(` cd ${targetDir}`);
|
|
103
|
+
console.log(` npm install`);
|
|
104
|
+
console.log(` npm run dev\n`);
|
|
105
|
+
|
|
106
|
+
if (choices.features.includes('devcontainer')) {
|
|
107
|
+
console.log(chalk.yellow(`If you are using Cursor or GitHub Codespaces, you can open the project in the Devcontainer now.`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(chalk.red(`\nAn error occurred during creation:\n${error.message}`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shopify-apps
|
|
3
|
+
description: "Expert patterns for Shopify app development including Remix/React Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions. U..."
|
|
4
|
+
risk: unknown
|
|
5
|
+
source: "vibeship-spawner-skills (Apache 2.0)"
|
|
6
|
+
date_added: "2026-02-27"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Shopify Apps
|
|
10
|
+
|
|
11
|
+
## Patterns
|
|
12
|
+
|
|
13
|
+
### React Router App Setup
|
|
14
|
+
|
|
15
|
+
Modern Shopify app template with React Router
|
|
16
|
+
|
|
17
|
+
### Embedded App with App Bridge
|
|
18
|
+
|
|
19
|
+
Render app embedded in Shopify Admin
|
|
20
|
+
|
|
21
|
+
### Webhook Handling
|
|
22
|
+
|
|
23
|
+
Secure webhook processing with HMAC verification
|
|
24
|
+
|
|
25
|
+
## Anti-Patterns
|
|
26
|
+
|
|
27
|
+
### ā REST API for New Apps
|
|
28
|
+
|
|
29
|
+
### ā Webhook Processing Before Response
|
|
30
|
+
|
|
31
|
+
### ā Polling Instead of Webhooks
|
|
32
|
+
|
|
33
|
+
## ā ļø Sharp Edges
|
|
34
|
+
|
|
35
|
+
| Issue | Severity | Solution |
|
|
36
|
+
|-------|----------|----------|
|
|
37
|
+
| Issue | high | ## Respond immediately, process asynchronously |
|
|
38
|
+
| Issue | high | ## Check rate limit headers |
|
|
39
|
+
| Issue | high | ## Request protected customer data access |
|
|
40
|
+
| Issue | medium | ## Use TOML only (recommended) |
|
|
41
|
+
| Issue | medium | ## Handle both URL formats |
|
|
42
|
+
| Issue | high | ## Use GraphQL for all new code |
|
|
43
|
+
| Issue | high | ## Use latest App Bridge via script tag |
|
|
44
|
+
| Issue | high | ## Implement all GDPR handlers |
|
|
45
|
+
|
|
46
|
+
## When to Use
|
|
47
|
+
This skill is applicable to execute the workflow or actions described in the overview.
|