anhedral 0.1.1 → 0.1.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.
- package/README.md +30 -26
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +86 -0
- package/dist/commands.d.ts +2 -3
- package/dist/commands.js +8 -32
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +770 -381
- package/dist/templates/backend.js +76 -190
- package/dist/templates/default-web.js +41 -27
- package/dist/templates/extension.js +200 -18
- package/dist/templates/frontend.js +11 -10
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -9
- package/dist/util.js +40 -7
- package/package.json +9 -11
|
@@ -1,37 +1,53 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { writeFile, exec } from '../util.js';
|
|
4
|
+
import { anhedralPrint } from '../print.js';
|
|
3
5
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
4
6
|
export async function scaffoldExtension(root, { projectName, displayName }) {
|
|
5
|
-
const
|
|
7
|
+
const appsRoot = path.join(root, 'apps');
|
|
8
|
+
const dir = path.join(appsRoot, 'extension');
|
|
6
9
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
anhedralPrint.section('Chrome extension (WXT)');
|
|
11
|
+
anhedralPrint.step('Scaffolding WXT extension');
|
|
12
|
+
writeFile(path.join(appsRoot, '.gitkeep'), '');
|
|
13
|
+
exec(`pnpm dlx ${toolPackageRef('wxt', toolchain.wxt)} init extension -t react --pm pnpm`, appsRoot);
|
|
11
14
|
writePackageJson(dir, projectName);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
exec(
|
|
15
|
-
exec(
|
|
16
|
-
|
|
15
|
+
anhedralPrint.done('WXT extension scaffolded');
|
|
16
|
+
anhedralPrint.step('Installing Clerk + React + Tailwind dependencies');
|
|
17
|
+
exec(`pnpm add -D @types/chrome @types/react @types/react-dom @wxt-dev/module-react autoprefixer postcss tailwindcss@3.4.19 typescript@5.9.3 ${toolPackageRef('wxt', toolchain.wxt)}`, dir);
|
|
18
|
+
exec('pnpm add @clerk/chrome-extension react react-dom clsx tailwind-merge class-variance-authority lucide-react@0.468.0', dir);
|
|
19
|
+
anhedralPrint.done('Extension dependencies installed');
|
|
20
|
+
cleanWxtStarterFiles(dir);
|
|
17
21
|
writeWxtConfig(dir, displayName);
|
|
18
22
|
writeTsConfig(dir);
|
|
19
23
|
writeEnvExample(dir);
|
|
24
|
+
writeEnvFile(dir);
|
|
20
25
|
writePostcssConfig(dir);
|
|
21
26
|
writeTailwindConfig(dir);
|
|
22
27
|
writeShadcnConfig(dir);
|
|
28
|
+
writeReadme(dir, displayName);
|
|
23
29
|
writeCnUtil(dir);
|
|
24
|
-
// ── 5. Write source files ─────────────────────────────────────────────
|
|
25
30
|
writeAuthContext(dir);
|
|
26
31
|
writeApiClient(dir);
|
|
27
32
|
writeBackground(dir);
|
|
33
|
+
writeContentScript(dir);
|
|
28
34
|
writeSidepanelEntry(dir);
|
|
29
35
|
writeSidepanelHtml(dir, displayName);
|
|
30
36
|
writeSidepanelApp(dir);
|
|
31
37
|
writeStyles(dir);
|
|
32
|
-
|
|
33
|
-
console.log(' Adding shadcn button component...');
|
|
38
|
+
anhedralPrint.step('Adding shadcn button component');
|
|
34
39
|
exec(`pnpm dlx ${toolPackageRef('shadcn', toolchain.shadcn)} add button`, dir);
|
|
40
|
+
writeStyles(dir);
|
|
41
|
+
anhedralPrint.done('shadcn button installed');
|
|
42
|
+
}
|
|
43
|
+
function cleanWxtStarterFiles(dir) {
|
|
44
|
+
for (const relativePath of [
|
|
45
|
+
'entrypoints',
|
|
46
|
+
'assets/react.svg',
|
|
47
|
+
'public/wxt.svg',
|
|
48
|
+
]) {
|
|
49
|
+
rmSync(path.join(dir, relativePath), { recursive: true, force: true });
|
|
50
|
+
}
|
|
35
51
|
}
|
|
36
52
|
function writePackageJson(dir, projectName) {
|
|
37
53
|
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -64,7 +80,7 @@ export default defineConfig({
|
|
|
64
80
|
description: '${displayName} Chrome Extension',
|
|
65
81
|
version: '0.1.0',
|
|
66
82
|
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
67
|
-
permissions: ['cookies', 'storage'],
|
|
83
|
+
permissions: ['activeTab', 'cookies', 'storage'],
|
|
68
84
|
host_permissions: [],
|
|
69
85
|
action: {
|
|
70
86
|
default_title: 'Open ${displayName}',
|
|
@@ -87,6 +103,7 @@ function writeTsConfig(dir) {
|
|
|
87
103
|
forceConsistentCasingInFileNames: true,
|
|
88
104
|
resolveJsonModule: true,
|
|
89
105
|
isolatedModules: true,
|
|
106
|
+
allowImportingTsExtensions: true,
|
|
90
107
|
noEmit: true,
|
|
91
108
|
jsx: 'react-jsx',
|
|
92
109
|
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
|
|
@@ -111,12 +128,29 @@ VITE_WEBSITE_URL=http://localhost:8081
|
|
|
111
128
|
# Chrome Extension CRX public key (optional, for stable extension ID)
|
|
112
129
|
VITE_CRX_PUBLIC_KEY=
|
|
113
130
|
|
|
131
|
+
# RevenueCat Web Billing URL (optional, for subscription management)
|
|
132
|
+
VITE_RC_BILLING_URL=
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
function writeEnvFile(dir) {
|
|
136
|
+
writeFile(path.join(dir, '.env'), `# Clerk Authentication
|
|
137
|
+
VITE_CLERK_PUBLISHABLE_KEY=
|
|
138
|
+
|
|
139
|
+
# Backend API URL
|
|
140
|
+
VITE_API_URL=http://localhost:8787
|
|
141
|
+
|
|
142
|
+
# Website URL (for sign-up and subscription links)
|
|
143
|
+
VITE_WEBSITE_URL=http://localhost:8081
|
|
144
|
+
|
|
145
|
+
# Chrome Extension CRX public key (optional, for stable extension ID)
|
|
146
|
+
VITE_CRX_PUBLIC_KEY=
|
|
147
|
+
|
|
114
148
|
# RevenueCat Web Billing URL (optional, for subscription management)
|
|
115
149
|
VITE_RC_BILLING_URL=
|
|
116
150
|
`);
|
|
117
151
|
}
|
|
118
152
|
function writePostcssConfig(dir) {
|
|
119
|
-
writeFile(path.join(dir, 'postcss.config.
|
|
153
|
+
writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
|
|
120
154
|
plugins: {
|
|
121
155
|
tailwindcss: {},
|
|
122
156
|
autoprefixer: {},
|
|
@@ -157,14 +191,79 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
157
191
|
`);
|
|
158
192
|
}
|
|
159
193
|
function writeTailwindConfig(dir) {
|
|
160
|
-
writeFile(path.join(dir, 'tailwind.config.
|
|
194
|
+
writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
|
|
161
195
|
module.exports = {
|
|
162
196
|
content: ['./src/**/*.{ts,tsx,html}'],
|
|
163
|
-
theme: {
|
|
197
|
+
theme: {
|
|
198
|
+
extend: {
|
|
199
|
+
colors: {
|
|
200
|
+
border: 'hsl(var(--border))',
|
|
201
|
+
input: 'hsl(var(--input))',
|
|
202
|
+
ring: 'hsl(var(--ring))',
|
|
203
|
+
background: 'hsl(var(--background))',
|
|
204
|
+
foreground: 'hsl(var(--foreground))',
|
|
205
|
+
primary: {
|
|
206
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
207
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
208
|
+
},
|
|
209
|
+
secondary: {
|
|
210
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
211
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
212
|
+
},
|
|
213
|
+
destructive: {
|
|
214
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
215
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
216
|
+
},
|
|
217
|
+
muted: {
|
|
218
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
219
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
220
|
+
},
|
|
221
|
+
accent: {
|
|
222
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
223
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
224
|
+
},
|
|
225
|
+
popover: {
|
|
226
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
227
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
228
|
+
},
|
|
229
|
+
card: {
|
|
230
|
+
DEFAULT: 'hsl(var(--card))',
|
|
231
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
borderRadius: {
|
|
235
|
+
lg: 'var(--radius)',
|
|
236
|
+
md: 'calc(var(--radius) - 2px)',
|
|
237
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
164
241
|
plugins: [],
|
|
165
242
|
};
|
|
166
243
|
`);
|
|
167
244
|
}
|
|
245
|
+
function writeReadme(dir, displayName) {
|
|
246
|
+
writeFile(path.join(dir, 'README.md'), `# ${displayName} Chrome Extension
|
|
247
|
+
|
|
248
|
+
WXT side-panel extension generated by anhedral.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
\`\`\`bash
|
|
253
|
+
pnpm dev
|
|
254
|
+
pnpm build
|
|
255
|
+
pnpm zip
|
|
256
|
+
\`\`\`
|
|
257
|
+
|
|
258
|
+
Set \`VITE_CLERK_PUBLISHABLE_KEY\` and \`VITE_API_URL\` in \`.env\` before using auth-backed routes. Set \`VITE_CRX_PUBLIC_KEY\` only when you need a stable Chrome extension ID.
|
|
259
|
+
|
|
260
|
+
## Chrome
|
|
261
|
+
|
|
262
|
+
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
263
|
+
|
|
264
|
+
The browser action opens the side panel. The background script initializes Clerk, the side panel handles sign-in/subscription state, and the content script is ready for page-to-extension messages.
|
|
265
|
+
`);
|
|
266
|
+
}
|
|
168
267
|
function writeAuthContext(dir) {
|
|
169
268
|
writeFile(path.join(dir, 'src/contexts/auth-context.tsx'), `import * as React from 'react';
|
|
170
269
|
import { ClerkProvider, useAuth as useClerkAuth, useUser } from '@clerk/chrome-extension';
|
|
@@ -379,6 +478,23 @@ export default defineBackground(() => {
|
|
|
379
478
|
});
|
|
380
479
|
`);
|
|
381
480
|
}
|
|
481
|
+
function writeContentScript(dir) {
|
|
482
|
+
writeFile(path.join(dir, 'src/entrypoints/content.ts'), `export default defineContentScript({
|
|
483
|
+
matches: ['<all_urls>'],
|
|
484
|
+
main() {
|
|
485
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
486
|
+
if (message?.type !== 'ANHEDRAL_PAGE_SNAPSHOT') return false;
|
|
487
|
+
|
|
488
|
+
sendResponse({
|
|
489
|
+
title: document.title,
|
|
490
|
+
location: window.location.href,
|
|
491
|
+
});
|
|
492
|
+
return true;
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
382
498
|
function writeSidepanelEntry(dir) {
|
|
383
499
|
writeFile(path.join(dir, 'src/entrypoints/sidepanel/main.tsx'), `import * as React from 'react';
|
|
384
500
|
import { createRoot } from 'react-dom/client';
|
|
@@ -418,9 +534,33 @@ function writeSidepanelApp(dir) {
|
|
|
418
534
|
writeFile(path.join(dir, 'src/entrypoints/sidepanel/app.tsx'), `import * as React from 'react';
|
|
419
535
|
import { useAuth } from '../../contexts/auth-context';
|
|
420
536
|
import { SignIn } from '@clerk/chrome-extension';
|
|
537
|
+
import { Button } from '../../components/ui/button';
|
|
538
|
+
|
|
539
|
+
type PageSnapshot = {
|
|
540
|
+
title: string;
|
|
541
|
+
location: string;
|
|
542
|
+
};
|
|
421
543
|
|
|
422
544
|
export function SidePanelApp() {
|
|
423
545
|
const { isSignedIn, isLoading, signOut, subscription } = useAuth();
|
|
546
|
+
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
547
|
+
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
548
|
+
|
|
549
|
+
const readActivePage = React.useCallback(async () => {
|
|
550
|
+
setPageError(null);
|
|
551
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
552
|
+
if (!tab.id) {
|
|
553
|
+
setPageError('No active tab is available.');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const response = await chrome.tabs.sendMessage(tab.id, { type: 'ANHEDRAL_PAGE_SNAPSHOT' });
|
|
559
|
+
setPage(response as PageSnapshot);
|
|
560
|
+
} catch {
|
|
561
|
+
setPageError('Refresh the active page, then try again.');
|
|
562
|
+
}
|
|
563
|
+
}, []);
|
|
424
564
|
|
|
425
565
|
if (isLoading) {
|
|
426
566
|
return <div style={{ padding: 24, textAlign: 'center' }}>Loading...</div>;
|
|
@@ -438,7 +578,15 @@ export function SidePanelApp() {
|
|
|
438
578
|
<div style={{ padding: 24 }}>
|
|
439
579
|
<h2>Welcome!</h2>
|
|
440
580
|
<p>Subscription: {subscription.status}</p>
|
|
441
|
-
<button onClick={
|
|
581
|
+
<Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
|
|
582
|
+
{page ? (
|
|
583
|
+
<div style={{ marginTop: 16 }}>
|
|
584
|
+
<strong>{page.title || 'Untitled page'}</strong>
|
|
585
|
+
<p style={{ overflowWrap: 'anywhere' }}>{page.location}</p>
|
|
586
|
+
</div>
|
|
587
|
+
) : null}
|
|
588
|
+
{pageError ? <p style={{ color: 'hsl(var(--destructive))' }}>{pageError}</p> : null}
|
|
589
|
+
<Button type="button" variant="outline" onClick={signOut}>Sign Out</Button>
|
|
442
590
|
</div>
|
|
443
591
|
);
|
|
444
592
|
}
|
|
@@ -449,6 +597,40 @@ function writeStyles(dir) {
|
|
|
449
597
|
@tailwind components;
|
|
450
598
|
@tailwind utilities;
|
|
451
599
|
|
|
600
|
+
@layer base {
|
|
601
|
+
:root {
|
|
602
|
+
--background: 0 0% 100%;
|
|
603
|
+
--foreground: 0 0% 9%;
|
|
604
|
+
--card: 0 0% 100%;
|
|
605
|
+
--card-foreground: 0 0% 9%;
|
|
606
|
+
--popover: 0 0% 100%;
|
|
607
|
+
--popover-foreground: 0 0% 9%;
|
|
608
|
+
--primary: 0 0% 9%;
|
|
609
|
+
--primary-foreground: 0 0% 98%;
|
|
610
|
+
--secondary: 0 0% 96.1%;
|
|
611
|
+
--secondary-foreground: 0 0% 9%;
|
|
612
|
+
--muted: 0 0% 96.1%;
|
|
613
|
+
--muted-foreground: 0 0% 45.1%;
|
|
614
|
+
--accent: 0 0% 96.1%;
|
|
615
|
+
--accent-foreground: 0 0% 9%;
|
|
616
|
+
--destructive: 0 84.2% 60.2%;
|
|
617
|
+
--destructive-foreground: 0 0% 98%;
|
|
618
|
+
--border: 0 0% 89.8%;
|
|
619
|
+
--input: 0 0% 89.8%;
|
|
620
|
+
--ring: 0 0% 63.9%;
|
|
621
|
+
--radius: 0.5rem;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
* {
|
|
625
|
+
border-color: hsl(var(--border));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
body {
|
|
629
|
+
background: hsl(var(--background));
|
|
630
|
+
color: hsl(var(--foreground));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
452
634
|
body {
|
|
453
635
|
margin: 0;
|
|
454
636
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { writeAnhedralLogo } from '../branding.js';
|
|
4
|
+
import { anhedralPrint } from '../print.js';
|
|
4
5
|
import { appendGitignore, writeFile, exec, execExpect, liftNestedProject } from '../util.js';
|
|
5
6
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
6
7
|
export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
7
|
-
const dir = path.join(root, '
|
|
8
|
+
const dir = path.join(root, 'apps/mobile');
|
|
8
9
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
anhedralPrint.section('Mobile (Expo)');
|
|
11
|
+
anhedralPrint.step('Scaffolding Expo app with react-native-reusables');
|
|
11
12
|
fs.mkdirSync(dir, { recursive: true });
|
|
12
13
|
await execExpect(`pnpm dlx ${toolPackageRef('@react-native-reusables/cli', toolchain.reactNativeReusables)} init -t clerk-auth`, dir, [
|
|
13
14
|
['What is the name of your project?', projectName],
|
|
14
15
|
['Would you like to install dependencies?', 'n'],
|
|
15
16
|
['Would you like to initialize a Git repository?', 'n'],
|
|
16
17
|
]);
|
|
17
|
-
// The CLI scaffolds into a subdir named after the project — move contents up
|
|
18
18
|
liftNestedProject(dir, projectName);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
anhedralPrint.done('Expo app scaffolded');
|
|
20
|
+
anhedralPrint.step('Installing mobile dependencies');
|
|
21
|
+
exec('pnpm install --no-frozen-lockfile', dir);
|
|
21
22
|
patchFrontendPackageJson(dir);
|
|
22
23
|
appendGitignore(dir, ['.env', '.env.*', '!.env.example']);
|
|
23
|
-
|
|
24
|
+
anhedralPrint.done('Mobile dependencies installed');
|
|
24
25
|
writeApiClient(dir);
|
|
25
26
|
writeConfigFile(dir);
|
|
26
27
|
writeApiHook(dir);
|
|
@@ -30,19 +31,19 @@ export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
|
30
31
|
writeEnvExample(dir);
|
|
31
32
|
writeEnvFile(dir);
|
|
32
33
|
writeAnhedralLogo(dir);
|
|
33
|
-
// ── 2b. Write subscription files ─────────────────────────────────────
|
|
34
34
|
writeSubscriptionProvider(dir);
|
|
35
35
|
writeUseSubscription(dir);
|
|
36
|
-
|
|
37
|
-
console.log(' Installing additional dependencies...');
|
|
36
|
+
anhedralPrint.step('Installing image picker + RevenueCat SDKs');
|
|
38
37
|
exec('pnpm exec expo install expo-image-picker', dir);
|
|
39
38
|
exec('pnpm add react-native-purchases react-native-purchases-ui @revenuecat/purchases-js', dir);
|
|
39
|
+
anhedralPrint.done('Additional mobile SDKs installed');
|
|
40
40
|
}
|
|
41
41
|
function patchFrontendPackageJson(dir) {
|
|
42
42
|
const filePath = path.join(dir, 'package.json');
|
|
43
43
|
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
44
44
|
packageJson.scripts = {
|
|
45
45
|
...(packageJson.scripts ?? {}),
|
|
46
|
+
build: 'pnpm typecheck',
|
|
46
47
|
typecheck: 'tsc --noEmit',
|
|
47
48
|
};
|
|
48
49
|
writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n');
|
package/dist/toolchain.d.ts
CHANGED
|
@@ -2,12 +2,9 @@ export declare const TOOLCHAIN_CHANNELS: readonly ["latest", "stable"];
|
|
|
2
2
|
export type ToolchainChannel = (typeof TOOLCHAIN_CHANNELS)[number];
|
|
3
3
|
export type ToolchainSpec = {
|
|
4
4
|
verifiedAt: string | null;
|
|
5
|
-
viteCreate: string;
|
|
6
5
|
shadcn: string;
|
|
7
6
|
reactNativeReusables: string;
|
|
8
7
|
wxt: string;
|
|
9
|
-
tauriCli: string;
|
|
10
|
-
tauriApi: string;
|
|
11
8
|
};
|
|
12
9
|
export declare function resolveToolchainChannel(value: string | undefined): ToolchainChannel;
|
|
13
10
|
export declare function resolveToolchain(channel: ToolchainChannel): ToolchainSpec;
|
package/dist/toolchain.js
CHANGED
|
@@ -2,27 +2,18 @@ export const TOOLCHAIN_CHANNELS = ['latest', 'stable'];
|
|
|
2
2
|
const TOOLCHAIN_SPECS = {
|
|
3
3
|
latest: {
|
|
4
4
|
verifiedAt: null,
|
|
5
|
-
viteCreate: 'latest',
|
|
6
5
|
shadcn: 'latest',
|
|
7
6
|
reactNativeReusables: 'latest',
|
|
8
7
|
wxt: 'latest',
|
|
9
|
-
tauriCli: 'latest',
|
|
10
|
-
tauriApi: 'latest',
|
|
11
8
|
},
|
|
12
9
|
stable: {
|
|
13
10
|
verifiedAt: '2026-03-22',
|
|
14
|
-
// renovate: datasource=npm depName=create-vite
|
|
15
|
-
viteCreate: '7.1.4',
|
|
16
11
|
// renovate: datasource=npm depName=shadcn
|
|
17
12
|
shadcn: '4.1.0',
|
|
18
13
|
// renovate: datasource=npm depName=@react-native-reusables/cli
|
|
19
14
|
reactNativeReusables: '0.5.0',
|
|
20
15
|
// renovate: datasource=npm depName=wxt
|
|
21
16
|
wxt: '0.20.20',
|
|
22
|
-
// renovate: datasource=npm depName=@tauri-apps/cli
|
|
23
|
-
tauriCli: '2.8.4',
|
|
24
|
-
// renovate: datasource=npm depName=@tauri-apps/api
|
|
25
|
-
tauriApi: '2.8.0',
|
|
26
17
|
},
|
|
27
18
|
};
|
|
28
19
|
export function resolveToolchainChannel(value) {
|
package/dist/util.js
CHANGED
|
@@ -13,13 +13,42 @@ export function appendGitignore(root, lines) {
|
|
|
13
13
|
const merged = [...new Set([...existing, ...lines])];
|
|
14
14
|
writeFile(filePath, merged.join('\n') + '\n');
|
|
15
15
|
}
|
|
16
|
+
const VERBOSE = process.env.ANHEDRAL_VERBOSE === '1';
|
|
17
|
+
function dumpFailure(error, cmd) {
|
|
18
|
+
const err = error;
|
|
19
|
+
const stdoutText = err.stdout ? err.stdout.toString() : '';
|
|
20
|
+
const stderrText = err.stderr ? err.stderr.toString() : '';
|
|
21
|
+
if (stdoutText)
|
|
22
|
+
process.stderr.write(stdoutText);
|
|
23
|
+
if (stderrText)
|
|
24
|
+
process.stderr.write(stderrText);
|
|
25
|
+
throw new Error(`Command failed (exit ${err.status ?? '?'}): ${cmd}`);
|
|
26
|
+
}
|
|
16
27
|
export function exec(cmd, cwd) {
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
if (VERBOSE) {
|
|
29
|
+
console.log(` $ ${cmd}`);
|
|
30
|
+
execSync(cmd, { cwd, stdio: 'inherit' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
dumpFailure(error, cmd);
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
40
|
export function execWithInput(cmd, cwd, stdinInput) {
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
if (VERBOSE) {
|
|
42
|
+
console.log(` $ ${cmd}`);
|
|
43
|
+
execSync(cmd, { cwd, stdio: ['pipe', 'inherit', 'inherit'], input: stdinInput });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'], input: stdinInput });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
dumpFailure(error, cmd);
|
|
51
|
+
}
|
|
23
52
|
}
|
|
24
53
|
function moveDirectoryContents(sourceDir, targetDir) {
|
|
25
54
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
@@ -63,11 +92,14 @@ function trimOutputTail(output, maxLength = 4000) {
|
|
|
63
92
|
return output.slice(output.length - maxLength);
|
|
64
93
|
}
|
|
65
94
|
export async function execExpect(cmd, cwd, prompts, timeoutMs = 120_000) {
|
|
66
|
-
|
|
95
|
+
if (VERBOSE) {
|
|
96
|
+
console.log(` $ ${cmd}`);
|
|
97
|
+
}
|
|
67
98
|
await new Promise((resolve, reject) => {
|
|
68
|
-
const child = spawn(
|
|
99
|
+
const child = spawn(cmd, {
|
|
69
100
|
cwd,
|
|
70
101
|
env: process.env,
|
|
102
|
+
shell: true,
|
|
71
103
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
104
|
});
|
|
73
105
|
let combinedOutput = '';
|
|
@@ -90,7 +122,8 @@ export async function execExpect(cmd, cwd, prompts, timeoutMs = 120_000) {
|
|
|
90
122
|
const handleChunk = (chunk, target) => {
|
|
91
123
|
const text = chunk.toString();
|
|
92
124
|
combinedOutput += text;
|
|
93
|
-
|
|
125
|
+
if (VERBOSE)
|
|
126
|
+
target.write(text);
|
|
94
127
|
maybeAnswerPrompt();
|
|
95
128
|
};
|
|
96
129
|
const timeoutId = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anhedral",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Opinionated init CLI for
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Opinionated init CLI for Expo or Next.js fullstack workspaces with Fastify, WXT, shared packages, Neon, Drizzle, Clerk, Stripe, RevenueCat, and R2.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"anhedral": "bin/anhedral.js"
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
"url": "https://github.com/anhedral/anhedral-init/issues"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
|
-
"access": "public"
|
|
27
|
-
"provenance": true
|
|
26
|
+
"access": "public"
|
|
28
27
|
},
|
|
29
28
|
"engines": {
|
|
30
29
|
"node": ">=20.0.0"
|
|
@@ -37,14 +36,13 @@
|
|
|
37
36
|
"test": "pnpm test:cli",
|
|
38
37
|
"pretest": "pnpm build",
|
|
39
38
|
"prepublishOnly": "pnpm test:all",
|
|
40
|
-
"test:cli": "node
|
|
41
|
-
"test:cli:validation": "node
|
|
42
|
-
"test:
|
|
43
|
-
"test:e2e": "node
|
|
44
|
-
"test:e2e:latest": "node scripts/test-e2e-public-stacks.js --toolchain latest",
|
|
39
|
+
"test:cli": "node tests/test-cli.js",
|
|
40
|
+
"test:cli:validation": "node tests/test-cli-validation.js",
|
|
41
|
+
"test:e2e": "node tests/test-e2e-public-stacks.js",
|
|
42
|
+
"test:e2e:latest": "node tests/test-e2e-public-stacks.js --toolchain latest",
|
|
45
43
|
"test:all": "pnpm test && pnpm test:e2e",
|
|
46
44
|
"release:check": "pnpm test:all && npm pack --dry-run",
|
|
47
|
-
"demo:refresh": "pnpm build && node
|
|
45
|
+
"demo:refresh": "pnpm build && node tests/refresh-demo.js"
|
|
48
46
|
},
|
|
49
47
|
"keywords": [
|
|
50
48
|
"cli",
|
|
@@ -55,7 +53,7 @@
|
|
|
55
53
|
"drizzle",
|
|
56
54
|
"neondb",
|
|
57
55
|
"revenuecat",
|
|
58
|
-
"
|
|
56
|
+
"wxt",
|
|
59
57
|
"shadcn"
|
|
60
58
|
],
|
|
61
59
|
"license": "MIT",
|