anhedral 0.1.1 → 0.1.4
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 +145 -35
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +67 -0
- package/dist/commands.d.ts +3 -8
- package/dist/commands.js +14 -64
- package/dist/dependencies.d.ts +35 -0
- package/dist/dependencies.js +175 -0
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +746 -419
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +117 -195
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +266 -68
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +86 -173
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +5 -16
- package/dist/util.d.ts +1 -0
- package/dist/util.js +65 -8
- package/package.json +12 -13
- package/dist/templates/default-web.d.ts +0 -2
- package/dist/templates/default-web.js +0 -1936
|
@@ -1,26 +1,32 @@
|
|
|
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';
|
|
6
|
+
import { FRONTEND_ADDON_DEPENDENCIES, withVersions } from '../dependencies.js';
|
|
5
7
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
6
|
-
export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
7
|
-
const dir = path.join(root, 'frontend');
|
|
8
|
+
export async function scaffoldFrontend(root, { projectName, displayName, skipInstall }) {
|
|
9
|
+
const dir = path.join(root, 'apps/frontend');
|
|
8
10
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
anhedralPrint.section('Frontend (Expo)');
|
|
12
|
+
anhedralPrint.step('Scaffolding Expo app with react-native-reusables');
|
|
11
13
|
fs.mkdirSync(dir, { recursive: true });
|
|
12
14
|
await execExpect(`pnpm dlx ${toolPackageRef('@react-native-reusables/cli', toolchain.reactNativeReusables)} init -t clerk-auth`, dir, [
|
|
13
15
|
['What is the name of your project?', projectName],
|
|
14
16
|
['Would you like to install dependencies?', 'n'],
|
|
15
17
|
['Would you like to initialize a Git repository?', 'n'],
|
|
16
18
|
]);
|
|
17
|
-
// The CLI scaffolds into a subdir named after the project — move contents up
|
|
18
19
|
liftNestedProject(dir, projectName);
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
anhedralPrint.done('Expo app scaffolded');
|
|
21
|
+
anhedralPrint.step('Installing frontend dependencies');
|
|
21
22
|
patchFrontendPackageJson(dir);
|
|
23
|
+
patchExpoAppConfig(dir);
|
|
24
|
+
if (!skipInstall) {
|
|
25
|
+
exec('pnpm install --no-frozen-lockfile', dir);
|
|
26
|
+
exec('pnpm exec expo install --fix --pnpm', dir);
|
|
27
|
+
}
|
|
22
28
|
appendGitignore(dir, ['.env', '.env.*', '!.env.example']);
|
|
23
|
-
|
|
29
|
+
anhedralPrint.done(skipInstall ? 'Frontend dependency manifests written' : 'Frontend dependencies installed');
|
|
24
30
|
writeApiClient(dir);
|
|
25
31
|
writeConfigFile(dir);
|
|
26
32
|
writeApiHook(dir);
|
|
@@ -29,190 +35,81 @@ export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
|
29
35
|
writeAppShellFiles(dir, displayName);
|
|
30
36
|
writeEnvExample(dir);
|
|
31
37
|
writeEnvFile(dir);
|
|
38
|
+
writeVercelConfig(dir);
|
|
32
39
|
writeAnhedralLogo(dir);
|
|
33
|
-
// ── 2b. Write subscription files ─────────────────────────────────────
|
|
34
40
|
writeSubscriptionProvider(dir);
|
|
35
41
|
writeUseSubscription(dir);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
anhedralPrint.step('Installing Expo native packages');
|
|
43
|
+
if (skipInstall) {
|
|
44
|
+
anhedralPrint.info('Skipping Expo native package install (--skip-install)');
|
|
45
|
+
anhedralPrint.info('Run after init: pnpm --filter ./apps/frontend exec expo install --pnpm expo-image-picker');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
exec('pnpm exec expo install --pnpm expo-image-picker', dir);
|
|
49
|
+
exec('pnpm exec expo install --fix --pnpm', dir);
|
|
50
|
+
}
|
|
51
|
+
anhedralPrint.done(skipInstall ? 'Expo native package install skipped' : 'Expo native packages installed');
|
|
52
|
+
anhedralPrint.step('Installing RevenueCat SDKs');
|
|
53
|
+
const revenueCatDependencies = withVersions({
|
|
54
|
+
'react-native-purchases': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases'],
|
|
55
|
+
'react-native-purchases-ui': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases-ui'],
|
|
56
|
+
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
57
|
+
});
|
|
58
|
+
if (skipInstall) {
|
|
59
|
+
anhedralPrint.info(`Skipping RevenueCat SDK install (--skip-install). Run after init: pnpm --filter ./apps/frontend add ${revenueCatDependencies.join(' ')}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
exec(`pnpm add ${revenueCatDependencies.join(' ')}`, dir);
|
|
63
|
+
}
|
|
64
|
+
anhedralPrint.done(skipInstall ? 'RevenueCat SDK install skipped' : 'Additional Expo SDKs installed');
|
|
40
65
|
}
|
|
41
66
|
function patchFrontendPackageJson(dir) {
|
|
42
67
|
const filePath = path.join(dir, 'package.json');
|
|
43
68
|
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
69
|
+
packageJson.dependencies = {
|
|
70
|
+
...(packageJson.dependencies ?? {}),
|
|
71
|
+
'@anhedral/api-client': FRONTEND_ADDON_DEPENDENCIES['@anhedral/api-client'],
|
|
72
|
+
'@clerk/clerk-expo': FRONTEND_ADDON_DEPENDENCIES['@clerk/clerk-expo'],
|
|
73
|
+
'react-native-purchases': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases'],
|
|
74
|
+
'react-native-purchases-ui': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases-ui'],
|
|
75
|
+
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
76
|
+
};
|
|
44
77
|
packageJson.scripts = {
|
|
45
78
|
...(packageJson.scripts ?? {}),
|
|
79
|
+
build: 'pnpm typecheck && pnpm build:web',
|
|
80
|
+
'build:web': 'expo export --platform web',
|
|
46
81
|
typecheck: 'tsc --noEmit',
|
|
47
82
|
};
|
|
48
83
|
writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
49
84
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const SWR_MAX_SIZE = 200;
|
|
56
|
-
const SWR_DEFAULT_STALE_MS = 15_000;
|
|
57
|
-
const SWR_DEFAULT_MAX_AGE_MS = 300_000;
|
|
58
|
-
|
|
59
|
-
type SWREntry<T> = { data: T; fetchedAt: number; staleMs: number; maxAgeMs: number };
|
|
60
|
-
const SWR_CACHE = new Map<string, SWREntry<unknown>>();
|
|
61
|
-
const SWR_INFLIGHT = new Map<string, Promise<unknown>>();
|
|
62
|
-
|
|
63
|
-
function swrEvict() {
|
|
64
|
-
if (SWR_CACHE.size <= SWR_MAX_SIZE) return;
|
|
65
|
-
const entries = [...SWR_CACHE.entries()].sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
|
66
|
-
const toRemove = entries.slice(0, entries.length - SWR_MAX_SIZE);
|
|
67
|
-
for (const [key] of toRemove) SWR_CACHE.delete(key);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class APIRequestError extends Error {
|
|
71
|
-
constructor(
|
|
72
|
-
public statusCode: number,
|
|
73
|
-
message: string,
|
|
74
|
-
public errorCode?: string,
|
|
75
|
-
) {
|
|
76
|
-
super(message);
|
|
77
|
-
this.name = 'APIRequestError';
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export class APIClient {
|
|
82
|
-
constructor(
|
|
83
|
-
private baseUrl: string,
|
|
84
|
-
private getToken: () => Promise<string | null>,
|
|
85
|
-
) {}
|
|
86
|
-
|
|
87
|
-
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
88
|
-
const token = await this.getToken();
|
|
89
|
-
const headers: Record<string, string> = {
|
|
90
|
-
'Content-Type': 'application/json',
|
|
91
|
-
'X-Platform': 'web',
|
|
92
|
-
};
|
|
93
|
-
if (token) headers['Authorization'] = \`Bearer \${token}\`;
|
|
94
|
-
|
|
95
|
-
const res = await fetch(\`\${this.baseUrl}\${path}\`, {
|
|
96
|
-
method,
|
|
97
|
-
headers,
|
|
98
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
if (!res.ok) {
|
|
102
|
-
let err: { error?: string; message?: string } = {};
|
|
103
|
-
try { err = await res.json(); } catch {}
|
|
104
|
-
throw new APIRequestError(res.status, err.message || \`Request failed (\${res.status})\`, err.error);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (res.status === 204) return {} as T;
|
|
108
|
-
return res.json();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async get<T>(path: string): Promise<T> { return this.request<T>('GET', path); }
|
|
112
|
-
async post<T>(path: string, body?: unknown): Promise<T> { return this.request<T>('POST', path, body); }
|
|
113
|
-
async delete<T>(path: string): Promise<T> { return this.request<T>('DELETE', path); }
|
|
114
|
-
|
|
115
|
-
async getCached<T>(
|
|
116
|
-
path: string,
|
|
117
|
-
opts?: { staleMs?: number; maxAgeMs?: number },
|
|
118
|
-
): Promise<T> {
|
|
119
|
-
const staleMs = opts?.staleMs ?? SWR_DEFAULT_STALE_MS;
|
|
120
|
-
const maxAgeMs = opts?.maxAgeMs ?? SWR_DEFAULT_MAX_AGE_MS;
|
|
121
|
-
const now = Date.now();
|
|
122
|
-
const cached = SWR_CACHE.get(path) as SWREntry<T> | undefined;
|
|
123
|
-
|
|
124
|
-
if (cached) {
|
|
125
|
-
const age = now - cached.fetchedAt;
|
|
126
|
-
if (age < staleMs) return cached.data;
|
|
127
|
-
if (age < maxAgeMs) {
|
|
128
|
-
if (!SWR_INFLIGHT.has(path)) {
|
|
129
|
-
const p = this.get<T>(path).then((data) => {
|
|
130
|
-
SWR_CACHE.set(path, { data, fetchedAt: Date.now(), staleMs, maxAgeMs });
|
|
131
|
-
return data;
|
|
132
|
-
}).finally(() => SWR_INFLIGHT.delete(path));
|
|
133
|
-
SWR_INFLIGHT.set(path, p);
|
|
134
|
-
}
|
|
135
|
-
return cached.data;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const inflight = SWR_INFLIGHT.get(path) as Promise<T> | undefined;
|
|
140
|
-
if (inflight) return inflight;
|
|
141
|
-
|
|
142
|
-
const p = this.get<T>(path).then((data) => {
|
|
143
|
-
SWR_CACHE.set(path, { data, fetchedAt: Date.now(), staleMs, maxAgeMs });
|
|
144
|
-
swrEvict();
|
|
145
|
-
return data;
|
|
146
|
-
}).finally(() => SWR_INFLIGHT.delete(path));
|
|
147
|
-
SWR_INFLIGHT.set(path, p);
|
|
148
|
-
return p;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
invalidateCache(path: string): void { SWR_CACHE.delete(path); }
|
|
152
|
-
invalidateCachePrefix(prefix: string): void {
|
|
153
|
-
for (const key of SWR_CACHE.keys()) {
|
|
154
|
-
if (key.startsWith(prefix)) SWR_CACHE.delete(key);
|
|
85
|
+
function patchExpoAppConfig(dir) {
|
|
86
|
+
const filePath = path.join(dir, 'app.json');
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
return;
|
|
155
89
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
id: string;
|
|
168
|
-
email: string;
|
|
169
|
-
firstName?: string | null;
|
|
170
|
-
lastName?: string | null;
|
|
171
|
-
displayName: string;
|
|
172
|
-
imageUrl?: string | null;
|
|
173
|
-
avatarUrl?: string | null;
|
|
174
|
-
creditsBalance: number;
|
|
175
|
-
subscriptionTier: string;
|
|
176
|
-
subscriptionStatus: string;
|
|
177
|
-
};
|
|
178
|
-
}>('/auth/me');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async signOut() {
|
|
182
|
-
return this.post<{ success: boolean }>('/auth/signout');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async uploadAvatar(input: { base64: string; mimeType: string; fileName?: string }) {
|
|
186
|
-
return this.post<{ ok: boolean; avatarUrl: string }>('/auth/avatar', input);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async deleteAccount() {
|
|
190
|
-
return this.delete<void>('/auth/account');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ── Subscription endpoints ───────────────────────────────────────────
|
|
194
|
-
async getSubscriptionPricing() {
|
|
195
|
-
return this.getCached<{
|
|
196
|
-
tiers: { tier: string; displayName: string; description: string; priceMonthly: number | null; priceYearly: number | null; currency: string; limits: { dailyLimit: number | null }; paymentInfo?: { revenueCatEntitlementId: string; revenueCatOfferingId: string } }[];
|
|
197
|
-
}>('/subscriptions/pricing', { staleMs: 300_000, maxAgeMs: 3_600_000 });
|
|
198
|
-
}
|
|
90
|
+
const appJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
91
|
+
appJson.expo ??= {};
|
|
92
|
+
appJson.expo.web = {
|
|
93
|
+
...(appJson.expo.web ?? {}),
|
|
94
|
+
bundler: 'metro',
|
|
95
|
+
output: 'static',
|
|
96
|
+
};
|
|
97
|
+
writeFile(filePath, JSON.stringify(appJson, null, 2) + '\n');
|
|
98
|
+
}
|
|
99
|
+
function writeApiClient(dir) {
|
|
100
|
+
writeFile(path.join(dir, 'api/client.ts'), `import { ApiClient, APIRequestError } from '@anhedral/api-client';
|
|
199
101
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pro: boolean; inTrial: boolean; trialEndsAt?: string; expiresAt?: string;
|
|
204
|
-
periodStart?: string; periodEnd?: string;
|
|
205
|
-
method?: 'trialing' | 'redeemed' | 'paid' | null;
|
|
206
|
-
managementUrl?: string; cancelAtPeriodEnd?: boolean;
|
|
207
|
-
}>(path);
|
|
102
|
+
export class APIClient extends ApiClient {
|
|
103
|
+
constructor(baseUrl: string, getToken: () => Promise<string | null>) {
|
|
104
|
+
super({ baseUrl, getToken, platform: 'frontend' });
|
|
208
105
|
}
|
|
209
106
|
|
|
210
|
-
|
|
211
|
-
return this.
|
|
107
|
+
redeemPromoCode(code: string) {
|
|
108
|
+
return this.redeemCode(code);
|
|
212
109
|
}
|
|
213
110
|
}
|
|
214
111
|
|
|
215
|
-
export {
|
|
112
|
+
export { APIRequestError };
|
|
216
113
|
`);
|
|
217
114
|
writeFile(path.join(dir, 'api/index.ts'), `export { APIClient, APIRequestError } from './client';
|
|
218
115
|
`);
|
|
@@ -283,6 +180,22 @@ export function useAPI() {
|
|
|
283
180
|
}
|
|
284
181
|
`);
|
|
285
182
|
}
|
|
183
|
+
function writeVercelConfig(dir) {
|
|
184
|
+
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
185
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
186
|
+
buildCommand: 'pnpm build:web',
|
|
187
|
+
outputDirectory: 'dist',
|
|
188
|
+
devCommand: 'pnpm dev',
|
|
189
|
+
cleanUrls: true,
|
|
190
|
+
framework: null,
|
|
191
|
+
rewrites: [
|
|
192
|
+
{
|
|
193
|
+
source: '/:path*',
|
|
194
|
+
destination: '/index.html',
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
}, null, 2) + '\n');
|
|
198
|
+
}
|
|
286
199
|
function writeAccountHook(dir) {
|
|
287
200
|
writeFile(path.join(dir, 'hooks/useAccount.ts'), `import { useAuth } from '@clerk/clerk-expo';
|
|
288
201
|
import * as React from 'react';
|
|
@@ -858,7 +771,7 @@ import { ScrollView, View } from 'react-native';
|
|
|
858
771
|
const FEATURES = [
|
|
859
772
|
{
|
|
860
773
|
title: 'Landing page here',
|
|
861
|
-
description: 'Replace this placeholder copy with the actual acquisition story for your
|
|
774
|
+
description: 'Replace this placeholder copy with the actual acquisition story for your Expo app.',
|
|
862
775
|
icon: CloudIcon,
|
|
863
776
|
},
|
|
864
777
|
{
|
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
|
@@ -1,28 +1,17 @@
|
|
|
1
|
+
import { TOOLCHAIN_DEPENDENCIES, VERIFIED_AT } from './dependencies.js';
|
|
1
2
|
export const TOOLCHAIN_CHANNELS = ['latest', 'stable'];
|
|
2
3
|
const TOOLCHAIN_SPECS = {
|
|
3
4
|
latest: {
|
|
4
5
|
verifiedAt: null,
|
|
5
|
-
viteCreate: 'latest',
|
|
6
6
|
shadcn: 'latest',
|
|
7
7
|
reactNativeReusables: 'latest',
|
|
8
8
|
wxt: 'latest',
|
|
9
|
-
tauriCli: 'latest',
|
|
10
|
-
tauriApi: 'latest',
|
|
11
9
|
},
|
|
12
10
|
stable: {
|
|
13
|
-
verifiedAt:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
shadcn: '4.1.0',
|
|
18
|
-
// renovate: datasource=npm depName=@react-native-reusables/cli
|
|
19
|
-
reactNativeReusables: '0.5.0',
|
|
20
|
-
// renovate: datasource=npm depName=wxt
|
|
21
|
-
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',
|
|
11
|
+
verifiedAt: VERIFIED_AT,
|
|
12
|
+
shadcn: TOOLCHAIN_DEPENDENCIES.shadcn,
|
|
13
|
+
reactNativeReusables: TOOLCHAIN_DEPENDENCIES.reactNativeReusables,
|
|
14
|
+
wxt: TOOLCHAIN_DEPENDENCIES.wxt,
|
|
26
15
|
},
|
|
27
16
|
};
|
|
28
17
|
export function resolveToolchainChannel(value) {
|
package/dist/util.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function writeFile(filePath: string, content: string): void;
|
|
2
2
|
export declare function appendGitignore(root: string, lines: string[]): void;
|
|
3
3
|
export declare function exec(cmd: string, cwd: string): void;
|
|
4
|
+
export declare function execCommand(command: string, args: string[], cwd: string): void;
|
|
4
5
|
export declare function execWithInput(cmd: string, cwd: string, stdinInput: string): void;
|
|
5
6
|
export declare function liftNestedProject(root: string, nestedName: string): void;
|
|
6
7
|
export declare function execExpect(cmd: string, cwd: string, prompts: [waitFor: string, answer: string][], timeoutMs?: number): Promise<void>;
|
package/dist/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { execSync, spawn } from 'node:child_process';
|
|
2
|
+
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export function writeFile(filePath, content) {
|
|
5
5
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -13,13 +13,66 @@ 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
|
+
}
|
|
27
|
+
function commandLabel(command, args) {
|
|
28
|
+
return [command, ...args].join(' ');
|
|
29
|
+
}
|
|
16
30
|
export function exec(cmd, cwd) {
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
if (VERBOSE) {
|
|
32
|
+
console.log(` $ ${cmd}`);
|
|
33
|
+
execSync(cmd, { cwd, stdio: 'inherit' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
dumpFailure(error, cmd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function execCommand(command, args, cwd) {
|
|
44
|
+
const label = commandLabel(command, args);
|
|
45
|
+
if (VERBOSE) {
|
|
46
|
+
console.log(` $ ${label}`);
|
|
47
|
+
}
|
|
48
|
+
const result = spawnSync(command, args, {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: VERBOSE ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
if (result.status === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!VERBOSE) {
|
|
57
|
+
if (result.stdout)
|
|
58
|
+
process.stderr.write(result.stdout);
|
|
59
|
+
if (result.stderr)
|
|
60
|
+
process.stderr.write(result.stderr);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Command failed (exit ${result.status ?? '?'}): ${label}`);
|
|
19
63
|
}
|
|
20
64
|
export function execWithInput(cmd, cwd, stdinInput) {
|
|
21
|
-
|
|
22
|
-
|
|
65
|
+
if (VERBOSE) {
|
|
66
|
+
console.log(` $ ${cmd}`);
|
|
67
|
+
execSync(cmd, { cwd, stdio: ['pipe', 'inherit', 'inherit'], input: stdinInput });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'], input: stdinInput });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
dumpFailure(error, cmd);
|
|
75
|
+
}
|
|
23
76
|
}
|
|
24
77
|
function moveDirectoryContents(sourceDir, targetDir) {
|
|
25
78
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
@@ -63,11 +116,14 @@ function trimOutputTail(output, maxLength = 4000) {
|
|
|
63
116
|
return output.slice(output.length - maxLength);
|
|
64
117
|
}
|
|
65
118
|
export async function execExpect(cmd, cwd, prompts, timeoutMs = 120_000) {
|
|
66
|
-
|
|
119
|
+
if (VERBOSE) {
|
|
120
|
+
console.log(` $ ${cmd}`);
|
|
121
|
+
}
|
|
67
122
|
await new Promise((resolve, reject) => {
|
|
68
|
-
const child = spawn(
|
|
123
|
+
const child = spawn(cmd, {
|
|
69
124
|
cwd,
|
|
70
125
|
env: process.env,
|
|
126
|
+
shell: true,
|
|
71
127
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
128
|
});
|
|
73
129
|
let combinedOutput = '';
|
|
@@ -90,7 +146,8 @@ export async function execExpect(cmd, cwd, prompts, timeoutMs = 120_000) {
|
|
|
90
146
|
const handleChunk = (chunk, target) => {
|
|
91
147
|
const text = chunk.toString();
|
|
92
148
|
combinedOutput += text;
|
|
93
|
-
|
|
149
|
+
if (VERBOSE)
|
|
150
|
+
target.write(text);
|
|
94
151
|
maybeAnswerPrompt();
|
|
95
152
|
};
|
|
96
153
|
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.4",
|
|
4
|
+
"description": "Opinionated init CLI for Expo, Fastify, WXT, shared packages, Neon, Drizzle, Clerk, RevenueCat, Stripe, R2, and Vercel.",
|
|
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,14 @@
|
|
|
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
|
|
45
|
-
"test:all": "pnpm test && pnpm test:e2e",
|
|
39
|
+
"test:cli": "node tests/test-cli.js",
|
|
40
|
+
"test:cli:validation": "node tests/test-cli-validation.js",
|
|
41
|
+
"test:deps": "pnpm build && node tests/test-dependency-manifest.js",
|
|
42
|
+
"test:e2e": "node tests/test-e2e-public-stacks.js",
|
|
43
|
+
"test:e2e:latest": "node tests/test-e2e-public-stacks.js --toolchain latest",
|
|
44
|
+
"test:all": "pnpm test && pnpm test:deps && pnpm test:e2e",
|
|
46
45
|
"release:check": "pnpm test:all && npm pack --dry-run",
|
|
47
|
-
"demo:refresh": "pnpm build && node
|
|
46
|
+
"demo:refresh": "pnpm build && node tests/refresh-demo.js"
|
|
48
47
|
},
|
|
49
48
|
"keywords": [
|
|
50
49
|
"cli",
|
|
@@ -55,8 +54,8 @@
|
|
|
55
54
|
"drizzle",
|
|
56
55
|
"neondb",
|
|
57
56
|
"revenuecat",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
57
|
+
"wxt",
|
|
58
|
+
"vercel"
|
|
60
59
|
],
|
|
61
60
|
"license": "MIT",
|
|
62
61
|
"packageManager": "pnpm@10.15.1",
|