create-factory 0.0.0-aggregate-trace-20261015182851

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/LICENSE.md ADDED
@@ -0,0 +1,32 @@
1
+ Portions of this software are licensed as follows:
2
+
3
+ - All content that resides under any directory named `ee/` within this
4
+ repository, including but not limited to:
5
+ - `@mastra/core/auth/ee`
6
+ - `@mastra/core/agent-builder/ee`
7
+ - `@mastra/editor/ee`
8
+
9
+ is licensed under the license defined in [`ee/LICENSE`](https://github.com/mastra-ai/mastra/blob/main/ee/LICENSE).
10
+
11
+ - All third-party components incorporated into the Mastra Software are
12
+ licensed under the original license provided by the owner of the
13
+ applicable component.
14
+
15
+ - Content outside of the above-mentioned directories or restrictions is
16
+ available under the "Apache License 2.0" as defined below.
17
+
18
+ # Apache License 2.0
19
+
20
+ Copyright (c) 2025 Kepler Software, Inc.
21
+
22
+ Licensed under the Apache License, Version 2.0 (the "License");
23
+ you may not use this file except in compliance with the License.
24
+ You may obtain a copy of the License at
25
+
26
+ http://www.apache.org/licenses/LICENSE-2.0
27
+
28
+ Unless required by applicable law or agreed to in writing, software
29
+ distributed under the License is distributed on an "AS IS" BASIS,
30
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
+ See the License for the specific language governing permissions and
32
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # create-factory
2
+
3
+ Mastra Factory is an open source environment for building software with coding agents. Connect your repository to turn issues into plans, implementations, and reviewed pull requests.
4
+
5
+ `create-factory` is the recommended way to scaffold your Mastra Factory. By default,
6
+
7
+ ## Installation
8
+
9
+ > [!IMPORTANT]
10
+ > Make sure that you have Node.js 22.13.0 or later installed on your system.
11
+
12
+ Run the latest version directly with your package manager.
13
+
14
+ Using npm:
15
+
16
+ ```bash
17
+ npx create-factory@latest
18
+ ```
19
+
20
+ Using Yarn:
21
+
22
+ ```bash
23
+ yarn dlx create-factory@latest
24
+ ```
25
+
26
+ Using pnpm:
27
+
28
+ ```bash
29
+ pnpm create factory@latest
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ The interactive setup asks where to create the project and helps configure your Mastra Factory project.
35
+
36
+ By default, the setup wizard provisions Mastra platform resources during setup, enabling you to fully run Mastra Factory on platform or locally with cloud-backed capabilities. If you prefer to [self-host](https://factory.mastra.ai/deployment#self-host) Mastra Factory, run the setup with the `--no-platform` flag.
37
+
38
+ ```bash
39
+ npx create-factory@latest -- --no-platform
40
+ ```
41
+
42
+ Pass `--help` to see all options:
43
+
44
+ ```bash
45
+ npx create-factory@latest --help
46
+ ```
47
+
48
+ ## Documentation
49
+
50
+ - [create-factory CLI reference](https://factory.mastra.ai/reference/create-factory)
51
+
52
+ ## Changelog
53
+
54
+ See the [package changelog](https://github.com/mastra-ai/mastra/blob/main/mastracode/mastra-factory/CHANGELOG.md) for version history and release notes.
55
+
56
+ ## Support
57
+
58
+ We have an [open community Discord](https://discord.gg/mastra-ai). Come and say hello and let us know if you have any questions or need any help getting things running.
package/dist/index.js ADDED
@@ -0,0 +1,726 @@
1
+ #! /usr/bin/env node
2
+ import fs, { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import { PosthogAnalytics, setAnalytics } from "mastra/dist/analytics/index.js";
7
+ import * as p from "@clack/prompts";
8
+ import { LoginCancelledError, MASTRA_PLATFORM_API_URL, authHeaders, extractApiErrorDetail, fetchOrgs, getToken, loadCredentials, platformFetch, resolveCurrentOrg } from "mastra/internal/auth";
9
+ import color from "picocolors";
10
+ import { x } from "tinyexec";
11
+ import fs$1 from "node:fs/promises";
12
+ //#region src/env.ts
13
+ /**
14
+ * Idempotently update a `.env` file.
15
+ *
16
+ * Replaces any existing `KEY=` line (commented or not) with `KEY=value`.
17
+ * Appends missing keys at the end of the file. Preserves other lines
18
+ * verbatim. Values are written raw — do not pass secrets that contain
19
+ * newlines or leading spaces (WorkOS `sk_` keys and Neon connection strings
20
+ * are safe).
21
+ */
22
+ function upsertEnvFile(envPath, updates) {
23
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
24
+ const lines = existing === "" ? [] : existing.split("\n");
25
+ const remaining = new Map(Object.entries(updates));
26
+ const patched = lines.map((line) => {
27
+ const match = /^(\s*#\s*)?([A-Z][A-Z0-9_]*)\s*=/.exec(line);
28
+ if (!match) return line;
29
+ const key = match[2];
30
+ if (!remaining.has(key)) return line;
31
+ const value = remaining.get(key);
32
+ remaining.delete(key);
33
+ return `${key}=${value}`;
34
+ });
35
+ if (remaining.size > 0) {
36
+ if (patched.length > 0 && patched[patched.length - 1] !== "") patched.push("");
37
+ for (const [key, value] of remaining) patched.push(`${key}=${value}`);
38
+ }
39
+ const output = patched.join("\n");
40
+ fs.writeFileSync(envPath, output, { mode: 384 });
41
+ try {
42
+ fs.chmodSync(envPath, 384);
43
+ } catch {}
44
+ }
45
+ //#endregion
46
+ //#region src/platform.ts
47
+ /**
48
+ * Common failure path: read `detail`/`message` out of the JSON body if the
49
+ * response wasn't JSON-parseable, fall back to the raw text.
50
+ */
51
+ async function extractError(res) {
52
+ const text = await res.text().catch(() => "");
53
+ if (!text) return `${res.status} ${res.statusText}`;
54
+ try {
55
+ const detail = extractApiErrorDetail(JSON.parse(text));
56
+ if (detail) return detail;
57
+ } catch {}
58
+ return text.slice(0, 500);
59
+ }
60
+ var PlatformApiError = class extends Error {
61
+ status;
62
+ constructor(status, message) {
63
+ super(message);
64
+ this.name = "PlatformApiError";
65
+ this.status = status;
66
+ }
67
+ };
68
+ /** POST /v1/server/projects — create a server project (Railway-provisioned). */
69
+ async function createServerProject({ token, orgId, name, region }) {
70
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/server/projects`, {
71
+ method: "POST",
72
+ headers: {
73
+ ...authHeaders(token, orgId),
74
+ "Content-Type": "application/json"
75
+ },
76
+ body: JSON.stringify({
77
+ name,
78
+ region,
79
+ factoryEnabled: true
80
+ })
81
+ });
82
+ if (!res.ok) throw new PlatformApiError(res.status, `Failed to create project — ${await extractError(res)}`);
83
+ return (await res.json()).project;
84
+ }
85
+ async function ensureProductionEnvironment({ token, orgId, projectId, region }) {
86
+ const url = `${MASTRA_PLATFORM_API_URL}/v1/projects/${encodeURIComponent(projectId)}/environments`;
87
+ const headers = authHeaders(token, orgId);
88
+ const res = await platformFetch(url, { headers });
89
+ if (!res.ok) throw new PlatformApiError(res.status, `Failed to list environments — ${await extractError(res)}`);
90
+ const production = (await res.json()).environments.find((environment) => environment.type === "production");
91
+ if (production) return production.id;
92
+ const created = await platformFetch(url, {
93
+ method: "POST",
94
+ headers: {
95
+ ...headers,
96
+ "Content-Type": "application/json"
97
+ },
98
+ body: JSON.stringify({
99
+ name: "production",
100
+ type: "production",
101
+ region
102
+ })
103
+ });
104
+ if (!created.ok) throw new PlatformApiError(created.status, `Failed to create production environment — ${await extractError(created)}`);
105
+ return (await created.json()).environment.id;
106
+ }
107
+ /**
108
+ * POST /v1/auth/tokens — mint an `sk_` WorkOS org API key.
109
+ * Returns the plaintext secret; the platform never returns it again.
110
+ */
111
+ async function mintOrgApiKey({ token, orgId, keyName }) {
112
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/auth/tokens`, {
113
+ method: "POST",
114
+ headers: {
115
+ ...authHeaders(token, orgId),
116
+ "Content-Type": "application/json"
117
+ },
118
+ body: JSON.stringify({ name: keyName })
119
+ });
120
+ if (!res.ok) throw new PlatformApiError(res.status, `Failed to create API key — ${await extractError(res)}`);
121
+ return (await res.json()).secret;
122
+ }
123
+ /**
124
+ * POST /v1/server/projects/:id/databases — attach a Neon Postgres database.
125
+ * Returns immediately with `status: 'provisioning'`; poll for `ready`.
126
+ *
127
+ * Note: this route requires `requireRole('admin')`. Non-admin org members
128
+ * will get a 403.
129
+ */
130
+ async function attachNeonDatabase({ token, orgId, projectId, name, regionId }) {
131
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases`, {
132
+ method: "POST",
133
+ headers: {
134
+ ...authHeaders(token, orgId),
135
+ "Content-Type": "application/json"
136
+ },
137
+ body: JSON.stringify({
138
+ kind: "neon",
139
+ name,
140
+ regionId
141
+ })
142
+ });
143
+ if (!res.ok) {
144
+ if (res.status === 403) throw new PlatformApiError(403, `Attaching a database requires the admin role in your organization. Ask an org admin to run \`create-factory\`, or attach a database from the dashboard.`);
145
+ throw new PlatformApiError(res.status, `Failed to attach Neon database — ${await extractError(res)}`);
146
+ }
147
+ return (await res.json()).database;
148
+ }
149
+ /** GET /v1/server/projects/:id/databases/:dbId — status poll target. */
150
+ async function getDatabaseStatus({ token, orgId, projectId, databaseId, signal }) {
151
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases/${encodeURIComponent(databaseId)}`, {
152
+ headers: authHeaders(token, orgId),
153
+ signal
154
+ });
155
+ if (!res.ok) throw new PlatformApiError(res.status, `Failed to read database status — ${await extractError(res)}`);
156
+ return (await res.json()).database;
157
+ }
158
+ /** GET /v1/server/projects/:id/databases/:dbId/connection — only 200s when `ready`. */
159
+ async function getDatabaseConnection({ token, orgId, projectId, databaseId }) {
160
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases/${encodeURIComponent(databaseId)}/connection`, { headers: authHeaders(token, orgId) });
161
+ if (!res.ok) throw new PlatformApiError(res.status, `Failed to fetch connection string — ${await extractError(res)}`);
162
+ return await res.json();
163
+ }
164
+ /**
165
+ * Poll `getDatabaseStatus` until the database is `ready`.
166
+ *
167
+ * Each poll is bounded by the *remaining* overall budget via a per-request
168
+ * `AbortSignal`, so a hung `platformFetch` can't blow past `timeoutMs`. When
169
+ * an outer `signal` is supplied it composes with the per-request timeout —
170
+ * whichever fires first aborts the in-flight request.
171
+ *
172
+ * @param intervalMs how often to poll (default 2s)
173
+ * @param timeoutMs total budget (default 60s)
174
+ */
175
+ async function waitForDatabaseReady({ token, orgId, projectId, databaseId, intervalMs = 2e3, timeoutMs = 6e4, signal }) {
176
+ const deadline = Date.now() + timeoutMs;
177
+ let lastStatus = "provisioning";
178
+ const timeoutError = () => new PlatformApiError(504, `Neon database is still ${lastStatus} after ${Math.round(timeoutMs / 1e3)}s. Check the dashboard or retry \`mastra login\` + re-run.`);
179
+ while (true) {
180
+ signal?.throwIfAborted();
181
+ const remaining = deadline - Date.now();
182
+ if (remaining <= 0) throw timeoutError();
183
+ const perRequestSignal = composeSignals(signal, AbortSignal.timeout(remaining));
184
+ let row;
185
+ try {
186
+ row = await getDatabaseStatus({
187
+ token,
188
+ orgId,
189
+ projectId,
190
+ databaseId,
191
+ signal: perRequestSignal
192
+ });
193
+ } catch (err) {
194
+ if (err instanceof DOMException && err.name === "TimeoutError") throw timeoutError();
195
+ throw err;
196
+ }
197
+ lastStatus = row.status;
198
+ if (row.status === "ready") return row;
199
+ if (row.status === "failed") throw new PlatformApiError(500, `Neon provisioning failed${row.error ? ` — ${row.error}` : ""}`);
200
+ if (Date.now() >= deadline) throw timeoutError();
201
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
202
+ }
203
+ }
204
+ /**
205
+ * Compose an outer AbortSignal with a per-request timeout signal. Uses
206
+ * `AbortSignal.any` where available (Node ≥20.3); falls back to a manual
207
+ * proxy for older runtimes.
208
+ */
209
+ function composeSignals(outer, inner) {
210
+ if (!outer) return inner;
211
+ if (typeof AbortSignal.any === "function") return AbortSignal.any([outer, inner]);
212
+ const controller = new AbortController();
213
+ const abort = (reason) => controller.abort(reason);
214
+ if (outer.aborted) abort(outer.reason);
215
+ else outer.addEventListener("abort", () => abort(outer.reason), { once: true });
216
+ if (inner.aborted) abort(inner.reason);
217
+ else inner.addEventListener("abort", () => abort(inner.reason), { once: true });
218
+ return controller.signal;
219
+ }
220
+ //#endregion
221
+ //#region src/utils/clone.ts
222
+ /** Rewrite the scaffolded package.json name to the chosen project name. */
223
+ async function renameProject(projectPath, projectName) {
224
+ const pkgPath = path.join(projectPath, "package.json");
225
+ const pkg = JSON.parse(await fs$1.readFile(pkgPath, "utf8"));
226
+ pkg.name = toPackageName(projectName);
227
+ await fs$1.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
228
+ }
229
+ /** Best-effort conversion of a directory name into a valid npm package name. */
230
+ function toPackageName(name) {
231
+ return name.toLowerCase().replace(/[^a-z0-9-_.~]+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "") || "mastra-factory";
232
+ }
233
+ async function pathExists(filePath) {
234
+ try {
235
+ await fs$1.access(filePath);
236
+ return true;
237
+ } catch (error) {
238
+ if (error.code === "ENOENT") return false;
239
+ throw error;
240
+ }
241
+ }
242
+ async function directoryExists(dirPath) {
243
+ try {
244
+ return (await fs$1.stat(dirPath)).isDirectory();
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+ async function cloneTemplate(repoUrl, targetPath) {
250
+ if (await pathExists(targetPath)) throw new Error(`Directory ${path.basename(targetPath)} already exists`);
251
+ const tempRoot = await fs$1.mkdtemp(path.join(path.dirname(targetPath), `.${path.basename(targetPath)}-`));
252
+ const tempTarget = path.join(tempRoot, "template");
253
+ try {
254
+ try {
255
+ await x("npx", [
256
+ "degit",
257
+ repoUrl.replace("https://github.com/", ""),
258
+ tempTarget
259
+ ], {
260
+ throwOnError: true,
261
+ nodeOptions: { cwd: process.cwd() }
262
+ });
263
+ if ((await fs$1.readdir(tempTarget)).length === 0) throw new Error("degit completed without cloning template files");
264
+ } catch {
265
+ await fs$1.rm(tempTarget, {
266
+ recursive: true,
267
+ force: true
268
+ });
269
+ try {
270
+ await x("git", [
271
+ "clone",
272
+ repoUrl,
273
+ tempTarget
274
+ ], {
275
+ throwOnError: true,
276
+ nodeOptions: { cwd: process.cwd() }
277
+ });
278
+ const gitDir = path.join(tempTarget, ".git");
279
+ if (await directoryExists(gitDir)) await fs$1.rm(gitDir, {
280
+ recursive: true,
281
+ force: true
282
+ });
283
+ } catch (gitError) {
284
+ throw new Error(`Failed to clone repository: ${gitError instanceof Error ? gitError.message : "Unknown error"}`);
285
+ }
286
+ }
287
+ if (await pathExists(targetPath)) throw new Error(`Directory ${path.basename(targetPath)} already exists`);
288
+ await fs$1.rename(tempTarget, targetPath);
289
+ } finally {
290
+ await fs$1.rm(tempRoot, {
291
+ recursive: true,
292
+ force: true
293
+ });
294
+ }
295
+ }
296
+ //#endregion
297
+ //#region src/utils/pm.ts
298
+ function detectPackageManager() {
299
+ const userAgent = process.env.npm_config_user_agent || "";
300
+ const execPath = process.env.npm_execpath || "";
301
+ if (userAgent.includes("bun")) return "bun";
302
+ if (userAgent.includes("yarn")) return "yarn";
303
+ if (userAgent.includes("pnpm")) return "pnpm";
304
+ if (userAgent.includes("npm")) return "npm";
305
+ if (execPath.includes("bun")) return "bun";
306
+ if (execPath.includes("yarn")) return "yarn";
307
+ if (execPath.includes("pnpm")) return "pnpm";
308
+ if (execPath.includes("npm")) return "npm";
309
+ return "npm";
310
+ }
311
+ function getInstallArgs(packageManager) {
312
+ switch (packageManager) {
313
+ case "npm": return [
314
+ "install",
315
+ "--no-audit",
316
+ "--no-fund"
317
+ ];
318
+ default: return ["install"];
319
+ }
320
+ }
321
+ //#endregion
322
+ //#region src/create.ts
323
+ const PROJECT_REGION_OPTIONS = [{
324
+ value: "eu",
325
+ label: "🇪🇺 eu"
326
+ }, {
327
+ value: "us",
328
+ label: "🇺🇸 us"
329
+ }];
330
+ const NEON_REGION_BY_PROJECT_REGION = {
331
+ eu: "aws-eu-central-1",
332
+ us: "aws-us-west-2"
333
+ };
334
+ function parseProjectRegion(region) {
335
+ if (region === "eu" || region === "us") return region;
336
+ throw new Error(`Invalid --region "${region}". Expected one of: eu, us.`);
337
+ }
338
+ async function create(args) {
339
+ p.intro(color.inverse(" Mastra Factory "));
340
+ const requestedRegion = args.region ? parseProjectRegion(args.region) : void 0;
341
+ const projectName = args.projectName ?? await p.text({
342
+ message: "What do you want to name your project?",
343
+ placeholder: "my-mastra-factory",
344
+ validate: (value) => {
345
+ if (!value?.trim()) return `Project name can't be empty`;
346
+ if (fs.existsSync(path.resolve(value.trim()))) return `Directory ${value.trim()} already exists`;
347
+ }
348
+ });
349
+ if (p.isCancel(projectName)) {
350
+ p.cancel("Operation cancelled");
351
+ process.exit(0);
352
+ }
353
+ const projectPath = path.resolve(projectName);
354
+ const packageManager = detectPackageManager();
355
+ args.analytics.trackEvent("sf_create_started", {
356
+ package_manager: packageManager,
357
+ no_platform: Boolean(args.noPlatform)
358
+ });
359
+ const spinner = p.spinner();
360
+ spinner.start("Downloading the Mastra Factory template...");
361
+ try {
362
+ await cloneTemplate(args.template, projectPath);
363
+ await renameProject(projectPath, projectName);
364
+ const envPath = path.join(projectPath, ".env");
365
+ fs.copyFileSync(path.join(projectPath, ".env.example"), envPath);
366
+ try {
367
+ fs.chmodSync(envPath, 384);
368
+ } catch {}
369
+ spinner.stop("Template downloaded.");
370
+ } catch (err) {
371
+ spinner.stop("Template download failed.");
372
+ throw err;
373
+ }
374
+ const installSpinner = p.spinner();
375
+ installSpinner.start(`Installing dependencies...`);
376
+ try {
377
+ await x(packageManager, getInstallArgs(packageManager), {
378
+ throwOnError: true,
379
+ nodeOptions: { cwd: projectPath }
380
+ });
381
+ installSpinner.stop("Dependencies installed.");
382
+ } catch (err) {
383
+ installSpinner.stop("Dependency install failed.");
384
+ throw new Error(`${err instanceof Error ? err.message : String(err)}\nYou can retry manually: cd ${projectName} && ${packageManager} install`);
385
+ }
386
+ let platformResult = null;
387
+ let platformError = null;
388
+ let platformSkipped = false;
389
+ if (!args.noPlatform) try {
390
+ platformResult = await runPlatformProvisioning({
391
+ projectName,
392
+ projectPath,
393
+ region: requestedRegion,
394
+ org: args.org
395
+ });
396
+ } catch (err) {
397
+ if (err instanceof LoginCancelledError) {
398
+ platformSkipped = true;
399
+ p.log.info("Skipping Mastra platform setup.");
400
+ } else {
401
+ platformError = err instanceof Error ? err.message : String(err);
402
+ p.log.warn(`Platform provisioning failed: ${platformError}`);
403
+ }
404
+ }
405
+ let gitignoreOk = true;
406
+ try {
407
+ ensureEnvGitignored(projectPath);
408
+ } catch (err) {
409
+ gitignoreOk = false;
410
+ const detail = err instanceof Error ? err.message : String(err);
411
+ p.log.warn(`Could not update .gitignore to protect .env (${detail}). Skipping git init to avoid committing secrets. Add ".env" to .gitignore manually before running 'git init && git add -A'.`);
412
+ }
413
+ if (gitignoreOk) try {
414
+ await x("git", ["init", "-q"], {
415
+ throwOnError: true,
416
+ nodeOptions: { cwd: projectPath }
417
+ });
418
+ await x("git", ["add", "-A"], {
419
+ throwOnError: true,
420
+ nodeOptions: { cwd: projectPath }
421
+ });
422
+ await x("git", [
423
+ "commit",
424
+ "-q",
425
+ "-m",
426
+ "Initial commit from create-factory"
427
+ ], {
428
+ throwOnError: true,
429
+ nodeOptions: { cwd: projectPath }
430
+ });
431
+ } catch {
432
+ p.log.warn("git init failed — you can initialize the repository yourself later.");
433
+ }
434
+ args.analytics.trackEvent("sf_create_completed", {
435
+ package_manager: packageManager,
436
+ no_platform: Boolean(args.noPlatform),
437
+ platform_provisioned: platformResult !== null
438
+ });
439
+ const lines = [
440
+ color.green("Your Mastra Factory is ready!"),
441
+ "",
442
+ `${color.cyan("cd")} ${projectName}`,
443
+ color.cyan(`${packageManager} run dev`),
444
+ "",
445
+ `Factory UI ${color.underline("http://localhost:4111")}`,
446
+ ""
447
+ ];
448
+ if (platformResult) lines.push(`${color.green("Provisioned on Mastra platform")} in ${color.cyan(platformResult.orgName)}:`, ` - Project ${color.cyan(platformResult.project.name)}`, ` - Postgres database (credentials written to ${color.cyan(".env")})`, ` - Sandboxes (code agent sessions run here)`, "", `Manage your project at ${color.underline("https://projects.mastra.ai")}`);
449
+ else if (platformSkipped) lines.push(`${color.yellow("Skipped Mastra platform setup.")} Configure ${color.cyan(".env")} manually before running.`);
450
+ else if (args.noPlatform) lines.push(`${color.yellow("Skipped platform provisioning (--no-platform).")} Configure ${color.cyan(".env")} manually before running.`);
451
+ else if (platformError) lines.push(`${color.yellow("Platform provisioning failed:")} ${platformError}`, `Any credentials that were minted before the failure have been written to ${color.cyan(".env")}. Re-run \`npx create factory\` after fixing, or fill in the rest manually from ${color.underline("https://platform.mastra.ai")}.`);
452
+ else lines.push("Open the Factory UI to finish setup (models, integrations, database).");
453
+ p.note(lines.join("\n"), "Next steps");
454
+ }
455
+ async function runPlatformProvisioning({ projectName, projectPath, region, org }) {
456
+ const envAccumulator = {};
457
+ const envPath = path.join(projectPath, ".env");
458
+ let flushed = false;
459
+ const flush = () => {
460
+ if (flushed || Object.keys(envAccumulator).length === 0) return;
461
+ upsertEnvFile(envPath, envAccumulator);
462
+ flushed = true;
463
+ };
464
+ try {
465
+ if (!process.env.MASTRA_API_TOKEN && !await loadCredentials()) {
466
+ const proceed = await p.text({
467
+ message: "Mastra account is required, press enter to continue...",
468
+ defaultValue: ""
469
+ });
470
+ if (p.isCancel(proceed)) {
471
+ p.cancel("Operation cancelled");
472
+ process.exit(0);
473
+ }
474
+ }
475
+ p.log.info("Signing in to Mastra…");
476
+ const token = await getToken(void 0, { skipOnInput: true });
477
+ const { orgId, orgName } = org ? await resolveOrgFromFlag(token, org) : await resolveCurrentOrg(token, { forcePrompt: true });
478
+ p.log.info(`Using organization ${color.cyan(orgName)}.`);
479
+ envAccumulator.MASTRA_ORGANIZATION_ID = orgId;
480
+ const projectRegion = region ?? await p.select({
481
+ message: "Where should your Factory project run?",
482
+ options: [...PROJECT_REGION_OPTIONS]
483
+ });
484
+ if (p.isCancel(projectRegion)) {
485
+ p.cancel("Operation cancelled");
486
+ process.exit(0);
487
+ }
488
+ const projectSpinner = p.spinner();
489
+ projectSpinner.start(`Creating platform project "${projectName}"…`);
490
+ let project;
491
+ try {
492
+ project = await createServerProject({
493
+ token,
494
+ orgId,
495
+ name: projectName,
496
+ region: projectRegion
497
+ });
498
+ projectSpinner.stop(`Created platform project ${color.cyan(project.slug)}.`);
499
+ } catch (err) {
500
+ projectSpinner.stop("Project creation failed.");
501
+ throw err;
502
+ }
503
+ envAccumulator.MASTRA_PROJECT_ID = project.id;
504
+ const keySpinner = p.spinner();
505
+ keySpinner.start("Creating platform API key…");
506
+ let secretKey;
507
+ try {
508
+ secretKey = await mintOrgApiKey({
509
+ token,
510
+ orgId,
511
+ keyName: `create-factory: ${projectName}`
512
+ });
513
+ keySpinner.stop("Platform API key created.");
514
+ } catch (err) {
515
+ keySpinner.stop("API key creation failed.");
516
+ throw err;
517
+ }
518
+ envAccumulator.MASTRA_PLATFORM_SECRET_KEY = secretKey;
519
+ const environmentSpinner = p.spinner();
520
+ environmentSpinner.start("Configuring production environment…");
521
+ try {
522
+ envAccumulator.MASTRA_ENVIRONMENT_ID = await ensureProductionEnvironment({
523
+ token,
524
+ orgId,
525
+ projectId: project.id,
526
+ region: projectRegion
527
+ });
528
+ environmentSpinner.stop("Production environment ready.");
529
+ } catch (err) {
530
+ environmentSpinner.stop("Environment setup failed.");
531
+ throw err;
532
+ }
533
+ const neonSpinner = p.spinner();
534
+ neonSpinner.start("Provisioning Neon Postgres database…");
535
+ let databaseUrl;
536
+ try {
537
+ const attached = await attachNeonDatabase({
538
+ token,
539
+ orgId,
540
+ projectId: project.id,
541
+ name: sanitizeDatabaseName(projectName),
542
+ regionId: NEON_REGION_BY_PROJECT_REGION[projectRegion]
543
+ });
544
+ const ready = await waitForDatabaseReady({
545
+ token,
546
+ orgId,
547
+ projectId: project.id,
548
+ databaseId: attached.id
549
+ });
550
+ const dbEnv = (await getDatabaseConnection({
551
+ token,
552
+ orgId,
553
+ projectId: project.id,
554
+ databaseId: ready.id
555
+ })).envVars.find((v) => v.name === "DATABASE_URL");
556
+ if (!dbEnv?.value) throw new PlatformApiError(500, "Platform connection response missing DATABASE_URL.");
557
+ databaseUrl = dbEnv.value;
558
+ neonSpinner.stop("Neon database ready.");
559
+ } catch (err) {
560
+ neonSpinner.stop("Database provisioning failed.");
561
+ throw err;
562
+ }
563
+ envAccumulator.DATABASE_URL = databaseUrl;
564
+ flush();
565
+ return {
566
+ orgId,
567
+ orgName,
568
+ project,
569
+ secretKey,
570
+ databaseUrl
571
+ };
572
+ } finally {
573
+ flush();
574
+ }
575
+ }
576
+ /**
577
+ * `--org <value>` matches by org id or exact name. Not a substring match —
578
+ * an ambiguous or non-existent value bails with a clear message instead of
579
+ * silently picking the wrong org.
580
+ */
581
+ async function resolveOrgFromFlag(token, value) {
582
+ const orgs = await fetchOrgs(token);
583
+ const match = orgs.find((o) => o.id === value || o.name === value);
584
+ if (!match) {
585
+ const available = orgs.map((o) => `${o.name} (${o.id})`).join(", ") || "(none)";
586
+ throw new Error(`No organization matched --org "${value}". Available: ${available}.`);
587
+ }
588
+ return {
589
+ orgId: match.id,
590
+ orgName: match.name
591
+ };
592
+ }
593
+ /**
594
+ * Neon display-name charset: `[a-zA-Z0-9_-]+`, up to 64 chars. Drop anything
595
+ * outside that, and clip length.
596
+ */
597
+ function sanitizeDatabaseName(projectName) {
598
+ return (projectName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "factory").slice(0, 64);
599
+ }
600
+ /**
601
+ * Append `.env` to the scaffolded project's `.gitignore` if it isn't already
602
+ * ignored. Runs before the initial `git add -A` so freshly-provisioned platform
603
+ * credentials (MASTRA_PLATFORM_SECRET_KEY, DATABASE_URL) never reach the
604
+ * initial commit.
605
+ *
606
+ * Throws if `.gitignore` cannot be written. Callers MUST treat this as fatal
607
+ * for the git-init step when `.env` may already contain platform secrets —
608
+ * silently continuing would stage secrets into the initial commit.
609
+ */
610
+ function ensureEnvGitignored(projectPath) {
611
+ const gitignorePath = path.join(projectPath, ".gitignore");
612
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
613
+ if (existing.split(/\r?\n/).map((line) => line.trim()).some((line) => {
614
+ if (!line || line.startsWith("#")) return false;
615
+ return line === ".env" || line === ".env*" || line === "/.env" || line === "/.env*";
616
+ })) return;
617
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
618
+ fs.writeFileSync(gitignorePath, `${existing}${prefix}\n# Added by create-factory to protect platform credentials\n.env\n`);
619
+ }
620
+ //#endregion
621
+ //#region src/utils/redact.ts
622
+ /**
623
+ * Sanitizes error messages before they are sent to analytics.
624
+ *
625
+ * Failure telemetry reports `error.message`, and several failure paths embed
626
+ * user-supplied input in their messages: a clone failure includes the full
627
+ * `git clone <url>` command (a custom template URL can identify a private repo
628
+ * and may carry embedded credentials), and `--region`/`--org` validation
629
+ * errors echo the raw flag value back. The user still sees the original
630
+ * message on stderr — only the analytics copy is redacted.
631
+ */
632
+ const REDACTED = "[redacted]";
633
+ /** Strips `user:password@` credentials from any URL in the message. */
634
+ function stripUrlCredentials(message) {
635
+ return message.replace(/\/\/[^\s/@]+@/g, `//${REDACTED}@`);
636
+ }
637
+ /**
638
+ * A value can surface in messages in derived forms: without embedded
639
+ * credentials (git may omit them when echoing the URL) or with the
640
+ * `https://github.com/` prefix stripped (degit's repo shorthand).
641
+ */
642
+ function variantsOf(value) {
643
+ const variants = /* @__PURE__ */ new Set([value, value.replace(/\/\/[^\s/@]+@/g, "//")]);
644
+ for (const variant of [...variants]) if (variant.startsWith("https://github.com/")) variants.add(variant.slice(19));
645
+ return [...variants];
646
+ }
647
+ /**
648
+ * Redacts every literal occurrence (and derived variant) of the provided
649
+ * user-supplied values, any URL credentials, and the org list echoed by
650
+ * `--org` mismatch errors.
651
+ */
652
+ function redactErrorMessage(message, sensitiveValues = []) {
653
+ let redacted = message;
654
+ for (const value of sensitiveValues) {
655
+ if (!value) continue;
656
+ for (const variant of variantsOf(value)) redacted = redacted.split(variant).join(REDACTED);
657
+ }
658
+ redacted = stripUrlCredentials(redacted);
659
+ redacted = redacted.replace(/Available: .*$/s, `Available: ${REDACTED}.`);
660
+ return redacted;
661
+ }
662
+ /** Returns a copy of `error` safe to report to analytics. */
663
+ function redactError(error, sensitiveValues = []) {
664
+ const message = error instanceof Error ? error.message : String(error);
665
+ return new Error(redactErrorMessage(message, sensitiveValues));
666
+ }
667
+ //#endregion
668
+ //#region src/index.ts
669
+ const pkg = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
670
+ const analytics = new PosthogAnalytics({
671
+ apiKey: "phc_SBLpZVAB6jmHOct9CABq3PF0Yn5FU3G2FgT4xUr2XrT",
672
+ host: "https://us.posthog.com",
673
+ version: pkg.version
674
+ });
675
+ setAnalytics(analytics);
676
+ const program = new Command();
677
+ const DEFAULT_TEMPLATE_REPO = "https://github.com/mastra-ai/softwarefactory-template";
678
+ program.name("create-factory").description("Create a new Mastra Factory project").argument("[project-name]", "Directory name of the project").option("--template <template-name>", "Create a project from a template (public GitHub URL)", DEFAULT_TEMPLATE_REPO).option("--no-platform", "Skip Mastra platform sign-in, project, and Neon provisioning").option("--org <org>", "Mastra organization id or name — skips the interactive org picker").option("--region <region>", "Platform project region (eu or us); prompts when omitted").version(pkg.version, "-v, --version").action(async (projectNameArg, args) => {
679
+ const region = args.region ? String(args.region) : void 0;
680
+ const validRegion = region === "eu" || region === "us" ? region : void 0;
681
+ const sensitiveValues = [
682
+ args.template === DEFAULT_TEMPLATE_REPO ? void 0 : args.template,
683
+ args.org ? String(args.org) : void 0,
684
+ validRegion ? void 0 : region,
685
+ projectNameArg
686
+ ];
687
+ let rawError;
688
+ try {
689
+ await analytics.trackCommandExecution({
690
+ command: "create-factory",
691
+ args: {
692
+ default_template: args.template === DEFAULT_TEMPLATE_REPO,
693
+ no_platform: args.platform === false,
694
+ has_org: Boolean(args.org),
695
+ region: validRegion ?? (region ? "invalid" : void 0)
696
+ },
697
+ execution: async () => {
698
+ try {
699
+ await create({
700
+ projectName: projectNameArg,
701
+ template: args.template,
702
+ noPlatform: args.platform === false,
703
+ org: args.org ? String(args.org) : void 0,
704
+ region,
705
+ analytics
706
+ });
707
+ } catch (err) {
708
+ rawError = err;
709
+ throw redactError(err, sensitiveValues);
710
+ }
711
+ }
712
+ });
713
+ } catch (err) {
714
+ throw rawError ?? err;
715
+ }
716
+ });
717
+ try {
718
+ await program.parseAsync(process.argv);
719
+ } catch (err) {
720
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
721
+ process.exitCode = 1;
722
+ } finally {
723
+ await analytics.shutdown(1e3);
724
+ }
725
+ //#endregion
726
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "create-factory",
3
+ "version": "0.0.0-aggregate-trace-20261015182851",
4
+ "description": "Create a Mastra Factory project: an agent-powered software delivery environment built on Mastra. Run `npm create factory` to scaffold and get started.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "create-factory": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=22.13.0"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/mastra-ai/mastra.git",
19
+ "directory": "mastracode/mastra-factory"
20
+ },
21
+ "homepage": "https://mastra.ai",
22
+ "bugs": {
23
+ "url": "https://github.com/mastra-ai/mastra/issues"
24
+ },
25
+ "keywords": [
26
+ "mastra",
27
+ "software-factory",
28
+ "create",
29
+ "scaffold",
30
+ "cli",
31
+ "agents",
32
+ "ai"
33
+ ],
34
+ "dependencies": {
35
+ "@clack/prompts": "^1.7.0",
36
+ "commander": "^14.0.3",
37
+ "picocolors": "^1.1.1",
38
+ "tinyexec": "^1.2.4",
39
+ "mastra": "0.0.0-aggregate-trace-20261015182851"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "22.20.1",
43
+ "eslint": "^10.4.1",
44
+ "tsdown": "0.22.9",
45
+ "typescript": "^7.0.2",
46
+ "vitest": "4.1.11",
47
+ "@internal/lint": "0.0.0-aggregate-trace-20261015182851"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown --silent --config tsdown.config.ts",
51
+ "check": "tsc --noEmit",
52
+ "test": "vitest run",
53
+ "lint": "oxlint . && eslint .",
54
+ "lint:fix": "oxlint --fix . && eslint --fix ."
55
+ }
56
+ }