sandstream-kit 1.5.0 → 1.6.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/README.md +6 -0
- package/dist/cli.js +27 -7
- package/dist/cli.js.map +1 -1
- package/dist/service-registry.d.ts +54 -0
- package/dist/service-registry.js +248 -0
- package/dist/service-registry.js.map +1 -0
- package/dist/stack-detector.js +176 -55
- package/dist/stack-detector.js.map +1 -1
- package/dist/toml-generator.d.ts +4 -0
- package/dist/toml-generator.js +55 -99
- package/dist/toml-generator.js.map +1 -1
- package/dist/vault-meta.d.ts +7 -0
- package/dist/vault-meta.js +13 -0
- package/dist/vault-meta.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the services kit detects and wires up.
|
|
3
|
+
*
|
|
4
|
+
* This unifies two tables that used to live apart and drift: the detection
|
|
5
|
+
* table (which deps/files mean "this repo uses X") in stack-detector.ts, and the
|
|
6
|
+
* generation table (X's login/check/secret-keys/tool) in toml-generator.ts.
|
|
7
|
+
* Keeping them as one `ServiceDef` per service means:
|
|
8
|
+
* - adding a service (or a whole new DB/BaaS) is ONE data entry, not edits to
|
|
9
|
+
* two files that must be kept in sync;
|
|
10
|
+
* - detection is language-agnostic: a Python or Go repo that uses Stripe now
|
|
11
|
+
* gets `services: ["stripe"]` (the per-language detectors used to hardcode
|
|
12
|
+
* `services: []`, so the whole secret/login layer was Node-only).
|
|
13
|
+
*
|
|
14
|
+
* stack-detector.ts calls {@link detectServices}; toml-generator.ts reads
|
|
15
|
+
* {@link SERVICE_BY_ID}. Registry ORDER is detection + emit order — keep it
|
|
16
|
+
* stable (tests pin "supabase before stripe", and migrate precedence is
|
|
17
|
+
* "first detected service that declares a `migrate`", so supabase must precede
|
|
18
|
+
* prisma/drizzle here).
|
|
19
|
+
*/
|
|
20
|
+
export interface ServiceDef {
|
|
21
|
+
id: string;
|
|
22
|
+
/** node: exact package name in dependencies/devDependencies. */
|
|
23
|
+
deps?: string[];
|
|
24
|
+
/** python: substring matched (case-insensitive) in requirements.txt/pyproject.toml. */
|
|
25
|
+
pyDeps?: string[];
|
|
26
|
+
/** go: substring matched in go.mod (module path). */
|
|
27
|
+
goMods?: string[];
|
|
28
|
+
/** marker files/dirs, any language (checked relative to repo root). */
|
|
29
|
+
files?: string[];
|
|
30
|
+
/** login command, or a `#`-prefixed informational note when there's no CLI. */
|
|
31
|
+
login?: string;
|
|
32
|
+
/** verify command, or a `#`-prefixed informational note. */
|
|
33
|
+
check?: string;
|
|
34
|
+
/** env keys this service needs. */
|
|
35
|
+
secrets?: string[];
|
|
36
|
+
/** mise tool to add to [tools] when this service is present. */
|
|
37
|
+
tool?: string;
|
|
38
|
+
/** migrate command — first detected service that declares one wins (see setupSection). */
|
|
39
|
+
migrate?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare const SERVICE_REGISTRY: ServiceDef[];
|
|
42
|
+
/** Lookup by service id, for the generator. */
|
|
43
|
+
export declare const SERVICE_BY_ID: Record<string, ServiceDef>;
|
|
44
|
+
/**
|
|
45
|
+
* Detect which services a repo uses, language-agnostically. Returns ids in
|
|
46
|
+
* registry order. `fileExists` is repo-root-relative so each language detector
|
|
47
|
+
* can pass its own cwd-bound checker.
|
|
48
|
+
*/
|
|
49
|
+
export declare function detectServices(signals: {
|
|
50
|
+
deps?: string[];
|
|
51
|
+
pyText?: string;
|
|
52
|
+
goMod?: string;
|
|
53
|
+
fileExists: (relPath: string) => Promise<boolean>;
|
|
54
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
export const SERVICE_REGISTRY = [
|
|
2
|
+
// ── existing 16 (ported verbatim from SERVICE_DETECTORS + SERVICE_TEMPLATES;
|
|
3
|
+
// strings preserved exactly for byte-identical output) ──
|
|
4
|
+
{
|
|
5
|
+
id: "stripe",
|
|
6
|
+
deps: ["stripe"],
|
|
7
|
+
pyDeps: ["stripe"],
|
|
8
|
+
goMods: ["github.com/stripe/stripe-go"],
|
|
9
|
+
login: "stripe login",
|
|
10
|
+
check: "stripe config --list",
|
|
11
|
+
secrets: ["STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY", "STRIPE_WEBHOOK_SECRET"],
|
|
12
|
+
tool: "stripe",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "supabase",
|
|
16
|
+
deps: ["@supabase/supabase-js"],
|
|
17
|
+
pyDeps: ["supabase"],
|
|
18
|
+
files: ["supabase"],
|
|
19
|
+
login: "supabase login",
|
|
20
|
+
check: "supabase projects list",
|
|
21
|
+
secrets: ["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY"],
|
|
22
|
+
tool: "supabase",
|
|
23
|
+
migrate: "supabase db push",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "prisma",
|
|
27
|
+
deps: ["prisma", "@prisma/client"],
|
|
28
|
+
migrate: "npx prisma migrate deploy",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "resend",
|
|
32
|
+
deps: ["resend"],
|
|
33
|
+
pyDeps: ["resend"],
|
|
34
|
+
login: "# resend — no CLI login; set RESEND_API_KEY in env",
|
|
35
|
+
check: "# resend — check RESEND_API_KEY is set",
|
|
36
|
+
secrets: ["RESEND_API_KEY", "RESEND_FROM_EMAIL"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "clerk",
|
|
40
|
+
deps: ["@clerk/nextjs", "@clerk/clerk-react"],
|
|
41
|
+
login: "# clerk — no CLI login; get keys from https://dashboard.clerk.com",
|
|
42
|
+
check: "# clerk — check CLERK_SECRET_KEY is set",
|
|
43
|
+
secrets: ["CLERK_SECRET_KEY", "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "drizzle",
|
|
47
|
+
deps: ["drizzle-orm"],
|
|
48
|
+
migrate: "npx drizzle-kit migrate",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "liveblocks",
|
|
52
|
+
deps: ["liveblocks", "@liveblocks/client"],
|
|
53
|
+
login: "# liveblocks — no CLI login; get keys from https://liveblocks.io/dashboard",
|
|
54
|
+
check: "# liveblocks — check LIVEBLOCKS_SECRET_KEY is set",
|
|
55
|
+
secrets: ["LIVEBLOCKS_SECRET_KEY", "NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY"],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "trigger",
|
|
59
|
+
deps: ["@trigger.dev/sdk", "trigger.dev"],
|
|
60
|
+
login: "# trigger — no CLI login; get key from https://cloud.trigger.dev",
|
|
61
|
+
check: "# trigger — check TRIGGER_SECRET_KEY is set",
|
|
62
|
+
secrets: ["TRIGGER_SECRET_KEY"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "inngest",
|
|
66
|
+
deps: ["inngest"],
|
|
67
|
+
login: "# inngest — no CLI login; get keys from https://app.inngest.com",
|
|
68
|
+
check: "# inngest — check INNGEST_EVENT_KEY is set",
|
|
69
|
+
secrets: ["INNGEST_EVENT_KEY", "INNGEST_SIGNING_KEY"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "vercel",
|
|
73
|
+
deps: ["@vercel/analytics", "vercel"],
|
|
74
|
+
files: ["vercel.json"],
|
|
75
|
+
login: "vercel login",
|
|
76
|
+
check: "vercel whoami",
|
|
77
|
+
secrets: [],
|
|
78
|
+
tool: "vercel",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "expo",
|
|
82
|
+
files: [".expo"],
|
|
83
|
+
login: "eas login",
|
|
84
|
+
check: "eas whoami",
|
|
85
|
+
secrets: ["EXPO_TOKEN"],
|
|
86
|
+
tool: "eas-cli",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "sentry",
|
|
90
|
+
deps: ["@sentry/nextjs", "@sentry/node", "@sentry/react", "@sentry/svelte", "@sentry/astro", "@sentry/remix"],
|
|
91
|
+
pyDeps: ["sentry-sdk"],
|
|
92
|
+
goMods: ["github.com/getsentry/sentry-go"],
|
|
93
|
+
login: "# sentry — no CLI login; get DSN from https://sentry.io",
|
|
94
|
+
check: "# sentry — check SENTRY_DSN is set",
|
|
95
|
+
secrets: ["SENTRY_DSN", "SENTRY_ORG", "SENTRY_PROJECT", "SENTRY_AUTH_TOKEN"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "typeorm",
|
|
99
|
+
deps: ["typeorm", "@nestjs/typeorm"],
|
|
100
|
+
login: "# typeorm — no CLI login; configure DATABASE_URL",
|
|
101
|
+
check: "# typeorm — check DATABASE_URL is set",
|
|
102
|
+
secrets: ["DATABASE_URL"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "mongoose",
|
|
106
|
+
deps: ["mongoose", "@nestjs/mongoose"],
|
|
107
|
+
login: "# mongoose — no CLI login; configure MONGODB_URI",
|
|
108
|
+
check: "# mongoose — check MONGODB_URI is set",
|
|
109
|
+
secrets: ["MONGODB_URI"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "netlify",
|
|
113
|
+
files: ["netlify.toml"],
|
|
114
|
+
login: "netlify login",
|
|
115
|
+
check: "netlify status",
|
|
116
|
+
secrets: ["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"],
|
|
117
|
+
tool: "netlify",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "cloudflare-pages",
|
|
121
|
+
files: ["wrangler.toml"],
|
|
122
|
+
login: "wrangler login",
|
|
123
|
+
check: "wrangler whoami",
|
|
124
|
+
secrets: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"],
|
|
125
|
+
tool: "wrangler",
|
|
126
|
+
},
|
|
127
|
+
// ── new entries (init-v2): databases / BaaS / warehouses. The whole point of
|
|
128
|
+
// the registry is that these are just data. New strings use plain hyphens
|
|
129
|
+
// (no em-dashes), per the project's written-content rule. ──
|
|
130
|
+
{
|
|
131
|
+
id: "convex",
|
|
132
|
+
deps: ["convex"],
|
|
133
|
+
login: "# convex - run `npx convex dev` to log in + create a deployment",
|
|
134
|
+
check: "# convex - check CONVEX_DEPLOYMENT is set",
|
|
135
|
+
secrets: ["CONVEX_DEPLOYMENT", "NEXT_PUBLIC_CONVEX_URL"],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "firebase",
|
|
139
|
+
deps: ["firebase", "firebase-admin"],
|
|
140
|
+
files: ["firebase.json", ".firebaserc"],
|
|
141
|
+
login: "firebase login",
|
|
142
|
+
check: "firebase projects:list",
|
|
143
|
+
secrets: ["FIREBASE_API_KEY", "FIREBASE_PROJECT_ID", "GOOGLE_APPLICATION_CREDENTIALS"],
|
|
144
|
+
tool: "firebase",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "mysql",
|
|
148
|
+
deps: ["mysql", "mysql2"],
|
|
149
|
+
pyDeps: ["mysqlclient", "pymysql"],
|
|
150
|
+
login: "# mysql - no CLI login; configure DATABASE_URL",
|
|
151
|
+
check: "# mysql - check DATABASE_URL is set",
|
|
152
|
+
secrets: ["DATABASE_URL"],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "planetscale",
|
|
156
|
+
deps: ["@planetscale/database"],
|
|
157
|
+
login: "# planetscale - configure DATABASE_URL (or run `pscale auth login`)",
|
|
158
|
+
check: "# planetscale - check DATABASE_URL is set",
|
|
159
|
+
secrets: ["DATABASE_URL"],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "neon",
|
|
163
|
+
deps: ["@neondatabase/serverless"],
|
|
164
|
+
login: "# neon - configure DATABASE_URL from the Neon console",
|
|
165
|
+
check: "# neon - check DATABASE_URL is set",
|
|
166
|
+
secrets: ["DATABASE_URL"],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "turso",
|
|
170
|
+
deps: ["@libsql/client"],
|
|
171
|
+
login: "turso auth login",
|
|
172
|
+
check: "turso auth whoami",
|
|
173
|
+
secrets: ["TURSO_DATABASE_URL", "TURSO_AUTH_TOKEN"],
|
|
174
|
+
tool: "turso",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "bigquery",
|
|
178
|
+
deps: ["@google-cloud/bigquery"],
|
|
179
|
+
pyDeps: ["google-cloud-bigquery"],
|
|
180
|
+
login: "gcloud auth application-default login",
|
|
181
|
+
check: "# bigquery - check GOOGLE_APPLICATION_CREDENTIALS + GCP_PROJECT_ID are set",
|
|
182
|
+
secrets: ["GOOGLE_APPLICATION_CREDENTIALS", "GCP_PROJECT_ID"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "snowflake",
|
|
186
|
+
deps: ["snowflake-sdk"],
|
|
187
|
+
pyDeps: ["snowflake-connector-python"],
|
|
188
|
+
login: "# snowflake - no CLI login; set SNOWFLAKE_* connection vars",
|
|
189
|
+
check: "# snowflake - check SNOWFLAKE_ACCOUNT is set",
|
|
190
|
+
secrets: ["SNOWFLAKE_ACCOUNT", "SNOWFLAKE_USER", "SNOWFLAKE_PASSWORD"],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "redshift",
|
|
194
|
+
deps: ["@aws-sdk/client-redshift", "@aws-sdk/client-redshift-data"],
|
|
195
|
+
login: "# redshift - no CLI login; set DATABASE_URL (or assume an IAM role)",
|
|
196
|
+
check: "# redshift - check DATABASE_URL is set",
|
|
197
|
+
secrets: ["DATABASE_URL"],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "redis",
|
|
201
|
+
deps: ["@upstash/redis", "ioredis", "redis"],
|
|
202
|
+
pyDeps: ["redis"],
|
|
203
|
+
login: "# redis - no CLI login; set REDIS_URL (or UPSTASH_REDIS_REST_*)",
|
|
204
|
+
check: "# redis - check REDIS_URL is set",
|
|
205
|
+
secrets: ["REDIS_URL", "UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN"],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "auth0",
|
|
209
|
+
deps: ["@auth0/nextjs-auth0", "@auth0/auth0-react", "auth0"],
|
|
210
|
+
login: "# auth0 - no CLI login; get keys from https://manage.auth0.com",
|
|
211
|
+
check: "# auth0 - check AUTH0_CLIENT_ID is set",
|
|
212
|
+
secrets: ["AUTH0_SECRET", "AUTH0_BASE_URL", "AUTH0_ISSUER_BASE_URL", "AUTH0_CLIENT_ID", "AUTH0_CLIENT_SECRET"],
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
/** Lookup by service id, for the generator. */
|
|
216
|
+
export const SERVICE_BY_ID = Object.fromEntries(SERVICE_REGISTRY.map((s) => [s.id, s]));
|
|
217
|
+
/**
|
|
218
|
+
* Detect which services a repo uses, language-agnostically. Returns ids in
|
|
219
|
+
* registry order. `fileExists` is repo-root-relative so each language detector
|
|
220
|
+
* can pass its own cwd-bound checker.
|
|
221
|
+
*/
|
|
222
|
+
export async function detectServices(signals) {
|
|
223
|
+
const deps = signals.deps ?? [];
|
|
224
|
+
const py = signals.pyText?.toLowerCase() ?? "";
|
|
225
|
+
const go = signals.goMod ?? "";
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const def of SERVICE_REGISTRY) {
|
|
228
|
+
let hit = false;
|
|
229
|
+
if (def.deps?.some((d) => deps.includes(d)))
|
|
230
|
+
hit = true;
|
|
231
|
+
if (!hit && py && def.pyDeps?.some((p) => py.includes(p.toLowerCase())))
|
|
232
|
+
hit = true;
|
|
233
|
+
if (!hit && go && def.goMods?.some((m) => go.includes(m)))
|
|
234
|
+
hit = true;
|
|
235
|
+
if (!hit && def.files) {
|
|
236
|
+
for (const f of def.files) {
|
|
237
|
+
if (await signals.fileExists(f)) {
|
|
238
|
+
hit = true;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (hit)
|
|
244
|
+
out.push(def.id);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=service-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.js","sourceRoot":"","sources":["../src/service-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/stack-detector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile, access } from "node:fs/promises";
|
|
1
|
+
import { readFile, access, readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { detectServices } from "./service-registry.js";
|
|
3
4
|
async function fileExists(path) {
|
|
4
5
|
try {
|
|
5
6
|
await access(path);
|
|
@@ -18,18 +19,96 @@ async function readJson(path) {
|
|
|
18
19
|
return null;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/** Expand a workspace glob. Supports exact paths and a single trailing `/*`
|
|
23
|
+
* (one directory level) — covers the common `apps/*` / `packages/*` layouts;
|
|
24
|
+
* deeper globs are rare for stack detection and fall back to no match. */
|
|
25
|
+
async function expandWorkspaceGlob(cwd, pattern) {
|
|
26
|
+
if (!pattern.includes("*"))
|
|
27
|
+
return [pattern];
|
|
28
|
+
const base = pattern.replace(/\/?\*+$/, "");
|
|
29
|
+
try {
|
|
30
|
+
const entries = await readdir(join(cwd, base), { withFileTypes: true });
|
|
31
|
+
return entries.filter((e) => e.isDirectory()).map((e) => (base ? `${base}/${e.name}` : e.name));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
23
36
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
/** Monorepo support: union the dependencies of every workspace member so a
|
|
38
|
+
* turborepo whose `next`/`stripe`/etc. live in `apps/*` is detected from the
|
|
39
|
+
* root, instead of coming up empty. Reads `package.json#workspaces` and
|
|
40
|
+
* `pnpm-workspace.yaml`. Returns [] for a non-workspace repo. */
|
|
41
|
+
async function collectWorkspaceDeps(cwd, pkg) {
|
|
42
|
+
const globs = [];
|
|
43
|
+
if (Array.isArray(pkg.workspaces))
|
|
44
|
+
globs.push(...pkg.workspaces);
|
|
45
|
+
else if (pkg.workspaces?.packages)
|
|
46
|
+
globs.push(...pkg.workspaces.packages);
|
|
47
|
+
const pnpmWs = await readFile(join(cwd, "pnpm-workspace.yaml"), "utf-8").catch(() => null);
|
|
48
|
+
if (pnpmWs) {
|
|
49
|
+
for (const m of pnpmWs.matchAll(/^\s*-\s*['"]?([^'"\n]+?)['"]?\s*$/gm))
|
|
50
|
+
globs.push(m[1].trim());
|
|
51
|
+
}
|
|
52
|
+
if (globs.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
const deps = new Set();
|
|
55
|
+
for (const g of globs) {
|
|
56
|
+
for (const dir of await expandWorkspaceGlob(cwd, g)) {
|
|
57
|
+
const member = await readJson(join(cwd, dir, "package.json"));
|
|
58
|
+
if (member) {
|
|
59
|
+
for (const k of Object.keys({ ...member.dependencies, ...member.devDependencies }))
|
|
60
|
+
deps.add(k);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...deps];
|
|
65
|
+
}
|
|
66
|
+
/** Resolve the Node major to pin, respecting the repo's existing truth.
|
|
67
|
+
* Precedence: .tool-versions > Volta > .node-version / .nvmrc > engines.node > 22.
|
|
68
|
+
* (Respecting these avoids installing the wrong runtime, the #1 brownfield trap.) */
|
|
69
|
+
async function resolveNodeVersion(cwd, pkg) {
|
|
70
|
+
const toolVersions = await readFile(join(cwd, ".tool-versions"), "utf-8").catch(() => null);
|
|
71
|
+
if (toolVersions) {
|
|
72
|
+
const m = toolVersions.match(/^\s*nodejs?\s+v?(\d+)/m);
|
|
73
|
+
if (m)
|
|
74
|
+
return m[1];
|
|
75
|
+
}
|
|
76
|
+
if (pkg.volta?.node) {
|
|
77
|
+
const m = pkg.volta.node.match(/(\d+)/);
|
|
78
|
+
if (m)
|
|
79
|
+
return m[1];
|
|
80
|
+
}
|
|
81
|
+
for (const f of [".node-version", ".nvmrc"]) {
|
|
82
|
+
const c = await readFile(join(cwd, f), "utf-8").catch(() => null);
|
|
83
|
+
if (c) {
|
|
84
|
+
const m = c.match(/v?(\d+)/);
|
|
85
|
+
if (m)
|
|
86
|
+
return m[1];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (pkg.engines?.node) {
|
|
90
|
+
const m = pkg.engines.node.match(/(\d+)/);
|
|
91
|
+
if (m)
|
|
92
|
+
return m[1];
|
|
30
93
|
}
|
|
31
94
|
return "22";
|
|
32
95
|
}
|
|
96
|
+
/** Resolve the Python minor to pin: .python-version > .tool-versions > 3.12. */
|
|
97
|
+
async function resolvePythonVersion(cwd) {
|
|
98
|
+
const pv = await readFile(join(cwd, ".python-version"), "utf-8").catch(() => null);
|
|
99
|
+
if (pv) {
|
|
100
|
+
const m = pv.match(/(\d+\.\d+)/);
|
|
101
|
+
if (m)
|
|
102
|
+
return m[1];
|
|
103
|
+
}
|
|
104
|
+
const tv = await readFile(join(cwd, ".tool-versions"), "utf-8").catch(() => null);
|
|
105
|
+
if (tv) {
|
|
106
|
+
const m = tv.match(/^\s*python\s+v?(\d+\.\d+)/m);
|
|
107
|
+
if (m)
|
|
108
|
+
return m[1];
|
|
109
|
+
}
|
|
110
|
+
return "3.12";
|
|
111
|
+
}
|
|
33
112
|
function detectPackageManager(pkg, cwd) {
|
|
34
113
|
if (pkg.packageManager?.startsWith("pnpm"))
|
|
35
114
|
return Promise.resolve("pnpm");
|
|
@@ -48,29 +127,8 @@ function detectPackageManager(pkg, cwd) {
|
|
|
48
127
|
return "npm";
|
|
49
128
|
})();
|
|
50
129
|
}
|
|
51
|
-
// Service
|
|
52
|
-
//
|
|
53
|
-
const SERVICE_DETECTORS = [
|
|
54
|
-
{ service: "stripe", deps: ["stripe"] },
|
|
55
|
-
{ service: "supabase", deps: ["@supabase/supabase-js"], files: ["supabase"] },
|
|
56
|
-
{ service: "prisma", deps: ["prisma", "@prisma/client"] },
|
|
57
|
-
{ service: "resend", deps: ["resend"] },
|
|
58
|
-
{ service: "clerk", deps: ["@clerk/nextjs", "@clerk/clerk-react"] },
|
|
59
|
-
{ service: "drizzle", deps: ["drizzle-orm"] },
|
|
60
|
-
{ service: "liveblocks", deps: ["liveblocks", "@liveblocks/client"] },
|
|
61
|
-
{ service: "trigger", deps: ["@trigger.dev/sdk", "trigger.dev"] },
|
|
62
|
-
{ service: "inngest", deps: ["inngest"] },
|
|
63
|
-
{ service: "vercel", deps: ["@vercel/analytics", "vercel"], files: ["vercel.json"] },
|
|
64
|
-
{ service: "expo", files: [".expo"] },
|
|
65
|
-
{
|
|
66
|
-
service: "sentry",
|
|
67
|
-
deps: ["@sentry/nextjs", "@sentry/node", "@sentry/react", "@sentry/svelte", "@sentry/astro", "@sentry/remix"],
|
|
68
|
-
},
|
|
69
|
-
{ service: "typeorm", deps: ["typeorm", "@nestjs/typeorm"] },
|
|
70
|
-
{ service: "mongoose", deps: ["mongoose", "@nestjs/mongoose"] },
|
|
71
|
-
{ service: "netlify", files: ["netlify.toml"] },
|
|
72
|
-
{ service: "cloudflare-pages", files: ["wrangler.toml"] },
|
|
73
|
-
];
|
|
130
|
+
// Service detection now lives in the data-driven SERVICE_REGISTRY (service-registry.ts),
|
|
131
|
+
// shared with the generator and matched across languages via detectServices().
|
|
74
132
|
// Framework = first match wins (priority order: meta-frameworks before their base).
|
|
75
133
|
const FRAMEWORK_DETECTORS = [
|
|
76
134
|
{ framework: "nextjs", deps: ["next"] },
|
|
@@ -79,6 +137,8 @@ const FRAMEWORK_DETECTORS = [
|
|
|
79
137
|
{ framework: "sveltekit", deps: ["@sveltejs/kit"] },
|
|
80
138
|
{ framework: "nestjs", deps: ["@nestjs/core"] },
|
|
81
139
|
{ framework: "express", deps: ["express"] },
|
|
140
|
+
// react-native before react: an RN app depends on both, RN is the real story.
|
|
141
|
+
{ framework: "react-native", deps: ["react-native"] },
|
|
82
142
|
{ framework: "react", deps: ["react"] },
|
|
83
143
|
{ framework: "vue", deps: ["vue"] },
|
|
84
144
|
];
|
|
@@ -86,29 +146,24 @@ async function detectFromPackageJson(cwd) {
|
|
|
86
146
|
const pkg = await readJson(join(cwd, "package.json"));
|
|
87
147
|
if (!pkg)
|
|
88
148
|
return null;
|
|
89
|
-
const node =
|
|
149
|
+
const node = await resolveNodeVersion(cwd, pkg);
|
|
90
150
|
const pm = await detectPackageManager(pkg, cwd);
|
|
91
151
|
const tools = { node };
|
|
92
152
|
if (pm !== "npm")
|
|
93
153
|
tools[pm] = "latest";
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (byDep || byFile)
|
|
106
|
-
services.push(d.service);
|
|
107
|
-
}
|
|
154
|
+
// Union root deps with workspace-member deps so monorepos (turborepo, pnpm
|
|
155
|
+
// workspaces) whose framework/services live in apps/* or packages/* are not
|
|
156
|
+
// detected as an empty root.
|
|
157
|
+
const rootDeps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
158
|
+
const deps = [...new Set([...rootDeps, ...(await collectWorkspaceDeps(cwd, pkg))])];
|
|
159
|
+
const services = await detectServices({
|
|
160
|
+
deps,
|
|
161
|
+
fileExists: (p) => fileExists(join(cwd, p)),
|
|
162
|
+
});
|
|
108
163
|
// Framework — first match wins (priority order in FRAMEWORK_DETECTORS).
|
|
109
164
|
let framework;
|
|
110
165
|
for (const fw of FRAMEWORK_DETECTORS) {
|
|
111
|
-
if (fw.deps.some((dep) =>
|
|
166
|
+
if (fw.deps.some((dep) => deps.includes(dep))) {
|
|
112
167
|
framework = fw.framework;
|
|
113
168
|
break;
|
|
114
169
|
}
|
|
@@ -141,11 +196,15 @@ async function detectFromPython(cwd) {
|
|
|
141
196
|
framework = "django";
|
|
142
197
|
else if (/flask/i.test(contents))
|
|
143
198
|
framework = "flask";
|
|
199
|
+
const services = await detectServices({
|
|
200
|
+
pyText: contents,
|
|
201
|
+
fileExists: (p) => fileExists(join(cwd, p)),
|
|
202
|
+
});
|
|
144
203
|
return {
|
|
145
204
|
language: "python",
|
|
146
205
|
framework,
|
|
147
|
-
services
|
|
148
|
-
tools: { python:
|
|
206
|
+
services,
|
|
207
|
+
tools: { python: await resolvePythonVersion(cwd), uv: "latest" },
|
|
149
208
|
confidence: framework ? 0.85 : 0.5,
|
|
150
209
|
};
|
|
151
210
|
}
|
|
@@ -160,10 +219,14 @@ async function detectFromGo(cwd) {
|
|
|
160
219
|
framework = "echo";
|
|
161
220
|
else if (/github\.com\/gofiber\/fiber/.test(goMod))
|
|
162
221
|
framework = "fiber";
|
|
222
|
+
const services = await detectServices({
|
|
223
|
+
goMod,
|
|
224
|
+
fileExists: (p) => fileExists(join(cwd, p)),
|
|
225
|
+
});
|
|
163
226
|
return {
|
|
164
227
|
language: "go",
|
|
165
228
|
framework,
|
|
166
|
-
services
|
|
229
|
+
services,
|
|
167
230
|
tools: { go: "1.22" },
|
|
168
231
|
confidence: framework ? 0.85 : 0.7,
|
|
169
232
|
};
|
|
@@ -179,10 +242,11 @@ async function detectFromRust(cwd) {
|
|
|
179
242
|
framework = "actix";
|
|
180
243
|
else if (/rocket/.test(cargoToml))
|
|
181
244
|
framework = "rocket";
|
|
245
|
+
const services = await detectServices({ fileExists: (p) => fileExists(join(cwd, p)) });
|
|
182
246
|
return {
|
|
183
247
|
language: "rust",
|
|
184
248
|
framework,
|
|
185
|
-
services
|
|
249
|
+
services,
|
|
186
250
|
tools: { rust: "latest" },
|
|
187
251
|
confidence: framework ? 0.85 : 0.7,
|
|
188
252
|
};
|
|
@@ -196,25 +260,82 @@ async function detectFromPhp(cwd) {
|
|
|
196
260
|
framework = "laravel";
|
|
197
261
|
else if (composer.require?.["symfony/framework-bundle"])
|
|
198
262
|
framework = "symfony";
|
|
263
|
+
const services = await detectServices({ fileExists: (p) => fileExists(join(cwd, p)) });
|
|
199
264
|
return {
|
|
200
265
|
language: "php",
|
|
201
266
|
framework,
|
|
202
|
-
services
|
|
267
|
+
services,
|
|
203
268
|
tools: { php: "8.3", composer: "latest" },
|
|
204
269
|
confidence: framework ? 0.85 : 0.6,
|
|
205
270
|
};
|
|
206
271
|
}
|
|
272
|
+
async function detectFromFlutter(cwd) {
|
|
273
|
+
const pubspec = await readFile(join(cwd, "pubspec.yaml"), "utf-8").catch(() => null);
|
|
274
|
+
if (pubspec === null)
|
|
275
|
+
return null;
|
|
276
|
+
// pubspec.yaml is also used by pure-Dart packages; "flutter:" / sdk: flutter
|
|
277
|
+
// marks an actual Flutter app.
|
|
278
|
+
const framework = /flutter/i.test(pubspec) ? "flutter" : undefined;
|
|
279
|
+
const services = await detectServices({ fileExists: (p) => fileExists(join(cwd, p)) });
|
|
280
|
+
return {
|
|
281
|
+
language: "dart",
|
|
282
|
+
framework,
|
|
283
|
+
services,
|
|
284
|
+
tools: {},
|
|
285
|
+
confidence: framework ? 0.9 : 0.7,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function detectFromSwift(cwd) {
|
|
289
|
+
const hasPackage = await fileExists(join(cwd, "Package.swift"));
|
|
290
|
+
const hasPodfile = await fileExists(join(cwd, "Podfile"));
|
|
291
|
+
if (!hasPackage && !hasPodfile)
|
|
292
|
+
return null;
|
|
293
|
+
// Podfile (CocoaPods) is an iOS-app signal; bare Package.swift can also be
|
|
294
|
+
// server-side Swift (Vapor), so it stays framework-less.
|
|
295
|
+
const framework = hasPodfile ? "ios" : undefined;
|
|
296
|
+
const services = await detectServices({ fileExists: (p) => fileExists(join(cwd, p)) });
|
|
297
|
+
return {
|
|
298
|
+
language: "swift",
|
|
299
|
+
framework,
|
|
300
|
+
services,
|
|
301
|
+
tools: {},
|
|
302
|
+
confidence: hasPodfile ? 0.85 : 0.7,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async function detectFromAndroid(cwd) {
|
|
306
|
+
const gradle = (await readFile(join(cwd, "build.gradle.kts"), "utf-8").catch(() => null)) ??
|
|
307
|
+
(await readFile(join(cwd, "build.gradle"), "utf-8").catch(() => null));
|
|
308
|
+
const hasSettings = (await fileExists(join(cwd, "settings.gradle.kts"))) || (await fileExists(join(cwd, "settings.gradle")));
|
|
309
|
+
if (gradle === null && !hasSettings)
|
|
310
|
+
return null;
|
|
311
|
+
// Only call it Android when the Android Gradle plugin is applied; otherwise
|
|
312
|
+
// it's a generic JVM/Gradle project (label the language, not a mobile framework).
|
|
313
|
+
const framework = gradle && /com\.android\.(application|library)/.test(gradle) ? "android" : undefined;
|
|
314
|
+
const services = await detectServices({ fileExists: (p) => fileExists(join(cwd, p)) });
|
|
315
|
+
return {
|
|
316
|
+
language: "kotlin",
|
|
317
|
+
framework,
|
|
318
|
+
services,
|
|
319
|
+
tools: {},
|
|
320
|
+
confidence: framework ? 0.85 : 0.6,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
207
323
|
/**
|
|
208
324
|
* Detect the project stack from files in the given directory.
|
|
209
325
|
* Returns null only if no recognizable project files are found.
|
|
210
326
|
*/
|
|
211
327
|
export async function detectStack(cwd) {
|
|
212
|
-
// Try detection in priority order — first match wins
|
|
328
|
+
// Try detection in priority order — first match wins. Node first (RN/Expo apps
|
|
329
|
+
// carry package.json); native-mobile detectors are reached only when none of
|
|
330
|
+
// the language manifests above them matched.
|
|
213
331
|
const result = (await detectFromPackageJson(cwd)) ??
|
|
214
332
|
(await detectFromPython(cwd)) ??
|
|
215
333
|
(await detectFromGo(cwd)) ??
|
|
216
334
|
(await detectFromRust(cwd)) ??
|
|
217
|
-
(await detectFromPhp(cwd))
|
|
335
|
+
(await detectFromPhp(cwd)) ??
|
|
336
|
+
(await detectFromFlutter(cwd)) ??
|
|
337
|
+
(await detectFromSwift(cwd)) ??
|
|
338
|
+
(await detectFromAndroid(cwd));
|
|
218
339
|
if (result)
|
|
219
340
|
return result;
|
|
220
341
|
// Unknown project — return minimal fallback
|