create-factory 0.0.3-alpha.0 → 0.0.3-alpha.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # create-factory
2
2
 
3
+ ## 0.0.3-alpha.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Marked projects created by create-factory as factory-enabled on the Mastra platform. ([#19973](https://github.com/mastra-ai/mastra/pull/19973))
8
+
9
+ - Updated dependencies:
10
+ - mastra@1.20.0-alpha.13
11
+
12
+ ## 0.0.3-alpha.1
13
+
14
+ ### Patch Changes
15
+
16
+ - Apply prettier formatting to env.test.ts (post-merge follow-up) ([#19965](https://github.com/mastra-ai/mastra/pull/19965))
17
+
18
+ - Harden .env handling in create-factory: tighten file permissions and skip git init when .env cannot be gitignored ([#19945](https://github.com/mastra-ai/mastra/pull/19945))
19
+
20
+ `.env` files created during scaffolding are now written with `0600` permissions so platform secrets (`MASTRA_PLATFORM_SECRET_KEY`, `DATABASE_URL`) are only readable by the owner. If the scaffolder can't add `.env` to `.gitignore` (e.g. permission denied on `.gitignore`), it now skips `git init` entirely and warns the user, so freshly-minted secrets can't accidentally be committed to the initial commit.
21
+
22
+ - Fixed generated Software Factory projects missing their required `@mastra/memory` dependency. ([#19878](https://github.com/mastra-ai/mastra/pull/19878))
23
+
24
+ - Added platform sign-in, project creation, and Neon Postgres provisioning to the `create factory` CLI. After scaffolding, the CLI: ([#19945](https://github.com/mastra-ai/mastra/pull/19945))
25
+
26
+ - Signs the user in via the existing Mastra browser-auth flow.
27
+ - Creates a Mastra platform server project in the chosen organization.
28
+ - Mints an `sk_` organization API key scoped to the new factory.
29
+ - Attaches and provisions a Neon Postgres database.
30
+ - Writes `MASTRA_SHARED_API_URL`, `MASTRA_ORGANIZATION_ID`, `MASTRA_PROJECT_ID`, `MASTRA_PLATFORM_SECRET_KEY`, and `DATABASE_URL` to the project's `.env`.
31
+
32
+ The result is a locally-runnable factory that can talk to the Mastra platform on first `npm run dev` without any manual configuration.
33
+
34
+ **New flags:**
35
+
36
+ - `--no-platform` — skip the platform round-trip; useful when iterating on the template offline.
37
+ - `--region <region>` — pass a specific Neon region id through to the platform.
38
+
39
+ - Updated dependencies [[`8c88764`](https://github.com/mastra-ai/mastra/commit/8c88764162b32c8a24b3bf9d1ad2ec535aba5c9a), [`40ae860`](https://github.com/mastra-ai/mastra/commit/40ae860a674e7ee383a02c1f990d1e050619d5e4)]:
40
+ - mastra@1.20.0-alpha.12
41
+
3
42
  ## 0.0.3-alpha.0
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -1,47 +1,30 @@
1
1
  # create-factory
2
2
 
3
- Scaffolding CLI for the **Mastra Software Factory** — an open-source, agent-powered software delivery environment built on [Mastra](https://mastra.ai).
3
+ Scaffolding CLI for the **Mastra Factory** — an open-source, agent-powered software delivery environment built on [Mastra](https://mastra.ai).
4
4
 
5
5
  ```bash
6
6
  npm create factory
7
7
  ```
8
8
 
9
- The CLI is intentionally minimal: it asks for a project name, clones the [softwarefactory-template](https://github.com/mastra-ai/softwarefactory-template), installs dependencies, and initializes git. That's it — run `npm run dev` and finish setup (model providers, integrations, database) from the web UI on first load. An "auth with the Mastra platform" step is planned as a follow-up.
9
+ The CLI is intentionally minimal: it asks for a project name, clones the [softwarefactory-template](https://github.com/mastra-ai/softwarefactory-template), installs dependencies, and initializes git. That's it — run `npm run dev` and finish setup (model providers, integrations, database) from the web UI on first load.
10
10
 
11
11
  ## Flags
12
12
 
13
13
  ```text
14
- npm create factory [project-name] -- [options]
14
+ Usage: create-factory [options] [project-name]
15
15
 
16
- --default Non-interactive: default name
17
- --template-ref <ref> Pin a template repo tag/branch
18
- --template-dir <dir> Use a local template directory (development)
19
- -t, --timeout [ms] Timeout for dependency installation
20
- ```
21
-
22
- ## Development (monorepo)
23
-
24
- This package lives in the `mastra-ai/mastra` monorepo at `mastracode/mastra-factory`. Releases go through the monorepo's changesets release train (it is un-ignored in `.changeset/config.json`) — add a changeset with your change and the train versions and publishes it.
25
-
26
- ```bash
27
- pnpm --filter ./mastracode/mastra-factory build # bundle src -> dist
28
- pnpm --filter ./mastracode/mastra-factory test # vitest
29
- node bin/cli.mjs my-app --default --template-dir ./template-out
30
- ```
31
-
32
- ### Template sync
33
-
34
- `scripts/sync-template.mjs` generates the template tree from `mastracode/web`:
16
+ Create a new Mastra Factory project
35
17
 
36
- - `link:` deps → caret ranges on published versions (anchored on local monorepo versions by default, verified on npm; `--tag latest` for stable dist-tags)
37
- - monorepo tsconfig standalone, test files/deps stripped, user-facing scripts
38
- - checked-in `template/README.md` → user-facing README (version tokens filled at sync)
39
- - `.env.schema` → `.env.example` (varlock decorators removed)
18
+ Arguments:
19
+ project-name Directory name of the project
40
20
 
41
- Syncing to [softwarefactory-template](https://github.com/mastra-ai/softwarefactory-template) is automated, mirroring the `templates/*` process: the `sync-softwarefactory-template` workflow regenerates the template using published local monorepo versions and force-pushes it on every push to `main` that touches `mastracode/web`, the sync script, or `template/README.md`. The monorepo is the source of truth — direct commits to the template repo get overwritten.
42
-
43
- ```bash
44
- node scripts/sync-template.mjs # writes ./template-out (local development)
21
+ Options:
22
+ --template <template-name> Create a project from a template (public GitHub URL) (default: "https://github.com/mastra-ai/softwarefactory-template")
23
+ --no-platform Skip Mastra platform sign-in, project, and Neon provisioning
24
+ --org <org> Mastra organization id or name — skips the interactive org picker
25
+ --region <region> Neon region id (passed to the platform verbatim)
26
+ -v, --version output the version number
27
+ -h, --help display help for command
45
28
  ```
46
29
 
47
30
  ## License
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ #! /usr/bin/env node
2
+
1
3
  // src/index.ts
2
4
  import { readFileSync as readFileSync2 } from "fs";
3
5
  import path4 from "path";
@@ -67,156 +69,364 @@ var Analytics = class {
67
69
  };
68
70
 
69
71
  // src/create.ts
70
- import fs2 from "fs";
72
+ import fs3 from "fs";
71
73
  import path3 from "path";
72
74
  import * as p from "@clack/prompts";
75
+ import { fetchOrgs, getToken, MASTRA_PLATFORM_API_URL as MASTRA_PLATFORM_API_URL2, resolveCurrentOrg } from "mastra/internal/auth";
73
76
  import color from "picocolors";
77
+ import { x as x2 } from "tinyexec";
74
78
 
75
- // src/utils/clone.ts
79
+ // src/env.ts
76
80
  import fs from "fs";
77
- import path2 from "path";
78
-
79
- // src/utils/exec.ts
80
- import { execFile, spawn } from "child_process";
81
- import { promisify } from "util";
82
- var execFileAsync = promisify(execFile);
83
- function runInherit(command, args, options) {
84
- return new Promise((resolve, reject) => {
85
- const child = spawn(command, args, {
86
- cwd: options.cwd,
87
- stdio: "inherit",
88
- shell: process.platform === "win32"
89
- });
90
- let timeout;
91
- let killTimer;
92
- let timedOut = false;
93
- if (options.timeoutMs) {
94
- timeout = setTimeout(() => {
95
- timedOut = true;
96
- if (process.platform === "win32" && child.pid) {
97
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
98
- killer.on("error", () => child.kill());
99
- killer.on("exit", (code) => {
100
- if (code !== 0) child.kill();
101
- });
102
- } else {
103
- child.kill("SIGTERM");
104
- killTimer = setTimeout(() => child.kill("SIGKILL"), 5e3);
105
- }
106
- }, options.timeoutMs);
107
- }
108
- const clearTimers = () => {
109
- if (timeout) clearTimeout(timeout);
110
- if (killTimer) clearTimeout(killTimer);
111
- };
112
- child.on("error", (err) => {
113
- clearTimers();
114
- reject(err);
115
- });
116
- child.on("close", (code) => {
117
- clearTimers();
118
- if (timedOut) reject(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms`));
119
- else if (code === 0) resolve();
120
- else reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
121
- });
81
+ function upsertEnvFile(envPath, updates) {
82
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
83
+ const lines = existing === "" ? [] : existing.split("\n");
84
+ const remaining = new Map(Object.entries(updates));
85
+ const patched = lines.map((line) => {
86
+ const match = /^(\s*#\s*)?([A-Z][A-Z0-9_]*)\s*=/.exec(line);
87
+ if (!match) return line;
88
+ const key = match[2];
89
+ if (!remaining.has(key)) return line;
90
+ const value = remaining.get(key);
91
+ remaining.delete(key);
92
+ return `${key}=${value}`;
122
93
  });
94
+ if (remaining.size > 0) {
95
+ if (patched.length > 0 && patched[patched.length - 1] !== "") {
96
+ patched.push("");
97
+ }
98
+ for (const [key, value] of remaining) {
99
+ patched.push(`${key}=${value}`);
100
+ }
101
+ }
102
+ const output = patched.join("\n");
103
+ fs.writeFileSync(envPath, output, { mode: 384 });
104
+ try {
105
+ fs.chmodSync(envPath, 384);
106
+ } catch {
107
+ }
123
108
  }
124
109
 
125
- // src/utils/clone.ts
126
- var DEFAULT_TEMPLATE_REPO = "https://github.com/mastra-ai/softwarefactory-template";
127
- async function cloneTemplate(options) {
128
- const { repoUrl, projectPath, ref, localDir } = options;
129
- if (fs.existsSync(projectPath)) {
130
- throw new Error(`Directory ${path2.basename(projectPath)} already exists`);
131
- }
132
- if (localDir) {
133
- fs.cpSync(localDir, projectPath, {
134
- recursive: true,
135
- filter: (src) => !src.split(path2.sep).includes("node_modules") && !src.split(path2.sep).includes(".git")
136
- });
137
- return;
138
- }
139
- const args = ["clone", "--depth", "1"];
140
- if (ref) args.push("--branch", ref);
141
- args.push(repoUrl, projectPath);
110
+ // src/platform.ts
111
+ import { MASTRA_PLATFORM_API_URL, authHeaders, extractApiErrorDetail, platformFetch } from "mastra/internal/auth";
112
+ async function extractError(res) {
113
+ const text2 = await res.text().catch(() => "");
114
+ if (!text2) return `${res.status} ${res.statusText}`;
142
115
  try {
143
- await execFileAsync("git", args);
144
- } catch (err) {
145
- throw new Error(
146
- `Failed to clone template from ${repoUrl}${ref ? `@${ref}` : ""}: ${err instanceof Error ? err.message : String(err)}`
147
- );
116
+ const parsed = JSON.parse(text2);
117
+ const detail = extractApiErrorDetail(parsed);
118
+ if (detail) return detail;
119
+ } catch {
120
+ }
121
+ return text2.slice(0, 500);
122
+ }
123
+ var PlatformApiError = class extends Error {
124
+ status;
125
+ constructor(status, message) {
126
+ super(message);
127
+ this.name = "PlatformApiError";
128
+ this.status = status;
129
+ }
130
+ };
131
+ async function createServerProject({
132
+ token,
133
+ orgId,
134
+ name
135
+ }) {
136
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/server/projects`, {
137
+ method: "POST",
138
+ headers: { ...authHeaders(token, orgId), "Content-Type": "application/json" },
139
+ body: JSON.stringify({ name, factoryEnabled: true })
140
+ });
141
+ if (!res.ok) {
142
+ throw new PlatformApiError(res.status, `Failed to create project \u2014 ${await extractError(res)}`);
148
143
  }
149
- fs.rmSync(path2.join(projectPath, ".git"), { recursive: true, force: true });
144
+ const body = await res.json();
145
+ return body.project;
150
146
  }
151
- function renameProject(projectPath, projectName) {
147
+ async function mintOrgApiKey({
148
+ token,
149
+ orgId,
150
+ keyName
151
+ }) {
152
+ const res = await platformFetch(`${MASTRA_PLATFORM_API_URL}/v1/auth/tokens`, {
153
+ method: "POST",
154
+ headers: { ...authHeaders(token, orgId), "Content-Type": "application/json" },
155
+ body: JSON.stringify({ name: keyName })
156
+ });
157
+ if (!res.ok) {
158
+ throw new PlatformApiError(res.status, `Failed to create API key \u2014 ${await extractError(res)}`);
159
+ }
160
+ const body = await res.json();
161
+ return body.secret;
162
+ }
163
+ async function attachNeonDatabase({
164
+ token,
165
+ orgId,
166
+ projectId,
167
+ name,
168
+ regionId
169
+ }) {
170
+ const res = await platformFetch(
171
+ `${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases`,
172
+ {
173
+ method: "POST",
174
+ headers: { ...authHeaders(token, orgId), "Content-Type": "application/json" },
175
+ body: JSON.stringify({ kind: "neon", name, ...regionId ? { regionId } : {} })
176
+ }
177
+ );
178
+ if (!res.ok) {
179
+ if (res.status === 403) {
180
+ throw new PlatformApiError(
181
+ 403,
182
+ `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.`
183
+ );
184
+ }
185
+ throw new PlatformApiError(res.status, `Failed to attach Neon database \u2014 ${await extractError(res)}`);
186
+ }
187
+ const body = await res.json();
188
+ return body.database;
189
+ }
190
+ async function getDatabaseStatus({
191
+ token,
192
+ orgId,
193
+ projectId,
194
+ databaseId,
195
+ signal
196
+ }) {
197
+ const res = await platformFetch(
198
+ `${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases/${encodeURIComponent(databaseId)}`,
199
+ { headers: authHeaders(token, orgId), signal }
200
+ );
201
+ if (!res.ok) {
202
+ throw new PlatformApiError(res.status, `Failed to read database status \u2014 ${await extractError(res)}`);
203
+ }
204
+ const body = await res.json();
205
+ return body.database;
206
+ }
207
+ async function getDatabaseConnection({
208
+ token,
209
+ orgId,
210
+ projectId,
211
+ databaseId
212
+ }) {
213
+ const res = await platformFetch(
214
+ `${MASTRA_PLATFORM_API_URL}/v1/server/projects/${encodeURIComponent(projectId)}/databases/${encodeURIComponent(databaseId)}/connection`,
215
+ { headers: authHeaders(token, orgId) }
216
+ );
217
+ if (!res.ok) {
218
+ throw new PlatformApiError(res.status, `Failed to fetch connection string \u2014 ${await extractError(res)}`);
219
+ }
220
+ return await res.json();
221
+ }
222
+ async function waitForDatabaseReady({
223
+ token,
224
+ orgId,
225
+ projectId,
226
+ databaseId,
227
+ intervalMs = 2e3,
228
+ timeoutMs = 6e4,
229
+ signal
230
+ }) {
231
+ const deadline = Date.now() + timeoutMs;
232
+ let lastStatus = "provisioning";
233
+ const timeoutError = () => new PlatformApiError(
234
+ 504,
235
+ `Neon database is still ${lastStatus} after ${Math.round(timeoutMs / 1e3)}s. Check the dashboard or retry \`mastra login\` + re-run.`
236
+ );
237
+ while (true) {
238
+ signal?.throwIfAborted();
239
+ const remaining = deadline - Date.now();
240
+ if (remaining <= 0) throw timeoutError();
241
+ const perRequestSignal = composeSignals(signal, AbortSignal.timeout(remaining));
242
+ let row;
243
+ try {
244
+ row = await getDatabaseStatus({ token, orgId, projectId, databaseId, signal: perRequestSignal });
245
+ } catch (err) {
246
+ if (err instanceof DOMException && err.name === "TimeoutError") throw timeoutError();
247
+ throw err;
248
+ }
249
+ lastStatus = row.status;
250
+ if (row.status === "ready") return row;
251
+ if (row.status === "failed") {
252
+ throw new PlatformApiError(500, `Neon provisioning failed${row.error ? ` \u2014 ${row.error}` : ""}`);
253
+ }
254
+ if (Date.now() >= deadline) throw timeoutError();
255
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
256
+ }
257
+ }
258
+ function composeSignals(outer, inner) {
259
+ if (!outer) return inner;
260
+ if (typeof AbortSignal.any === "function") {
261
+ return AbortSignal.any([outer, inner]);
262
+ }
263
+ const controller = new AbortController();
264
+ const abort = (reason) => controller.abort(reason);
265
+ if (outer.aborted) abort(outer.reason);
266
+ else outer.addEventListener("abort", () => abort(outer.reason), { once: true });
267
+ if (inner.aborted) abort(inner.reason);
268
+ else inner.addEventListener("abort", () => abort(inner.reason), { once: true });
269
+ return controller.signal;
270
+ }
271
+
272
+ // src/utils/clone.ts
273
+ import fs2 from "fs/promises";
274
+ import path2 from "path";
275
+ import { x } from "tinyexec";
276
+ async function renameProject(projectPath, projectName) {
152
277
  const pkgPath = path2.join(projectPath, "package.json");
153
- const pkg2 = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
278
+ const pkg2 = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
154
279
  pkg2.name = toPackageName(projectName);
155
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg2, null, 2)}
280
+ await fs2.writeFile(pkgPath, `${JSON.stringify(pkg2, null, 2)}
156
281
  `);
157
282
  }
158
283
  function toPackageName(name) {
159
284
  const cleaned = name.toLowerCase().replace(/[^a-z0-9-_.~]+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
160
- return cleaned || "softwarefactory";
285
+ return cleaned || "mastra-factory";
286
+ }
287
+ async function pathExists(filePath) {
288
+ try {
289
+ await fs2.access(filePath);
290
+ return true;
291
+ } catch (error) {
292
+ if (error.code === "ENOENT") return false;
293
+ throw error;
294
+ }
295
+ }
296
+ async function directoryExists(dirPath) {
297
+ try {
298
+ return (await fs2.stat(dirPath)).isDirectory();
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+ async function cloneTemplate(repoUrl, targetPath) {
304
+ if (await pathExists(targetPath)) {
305
+ throw new Error(`Directory ${path2.basename(targetPath)} already exists`);
306
+ }
307
+ const tempRoot = await fs2.mkdtemp(path2.join(path2.dirname(targetPath), `.${path2.basename(targetPath)}-`));
308
+ const tempTarget = path2.join(tempRoot, "template");
309
+ try {
310
+ try {
311
+ const degitRepo = repoUrl.replace("https://github.com/", "");
312
+ await x("npx", ["degit", degitRepo, tempTarget], {
313
+ throwOnError: true,
314
+ nodeOptions: { cwd: process.cwd() }
315
+ });
316
+ if ((await fs2.readdir(tempTarget)).length === 0) {
317
+ throw new Error("degit completed without cloning template files");
318
+ }
319
+ } catch {
320
+ await fs2.rm(tempTarget, { recursive: true, force: true });
321
+ try {
322
+ await x("git", ["clone", repoUrl, tempTarget], {
323
+ throwOnError: true,
324
+ nodeOptions: { cwd: process.cwd() }
325
+ });
326
+ const gitDir = path2.join(tempTarget, ".git");
327
+ if (await directoryExists(gitDir)) {
328
+ await fs2.rm(gitDir, { recursive: true, force: true });
329
+ }
330
+ } catch (gitError) {
331
+ throw new Error(
332
+ `Failed to clone repository: ${gitError instanceof Error ? gitError.message : "Unknown error"}`
333
+ );
334
+ }
335
+ }
336
+ if (await pathExists(targetPath)) {
337
+ throw new Error(`Directory ${path2.basename(targetPath)} already exists`);
338
+ }
339
+ await fs2.rename(tempTarget, targetPath);
340
+ } finally {
341
+ await fs2.rm(tempRoot, { recursive: true, force: true });
342
+ }
161
343
  }
162
344
 
163
345
  // src/utils/pm.ts
164
346
  function detectPackageManager() {
165
- const agent = process.env.npm_config_user_agent ?? "";
166
- if (agent.startsWith("pnpm")) return "pnpm";
167
- if (agent.startsWith("yarn")) return "yarn";
168
- if (agent.startsWith("bun")) return "bun";
347
+ const userAgent = process.env.npm_config_user_agent || "";
348
+ const execPath = process.env.npm_execpath || "";
349
+ if (userAgent.includes("bun")) {
350
+ return "bun";
351
+ }
352
+ if (userAgent.includes("yarn")) {
353
+ return "yarn";
354
+ }
355
+ if (userAgent.includes("pnpm")) {
356
+ return "pnpm";
357
+ }
358
+ if (userAgent.includes("npm")) {
359
+ return "npm";
360
+ }
361
+ if (execPath.includes("bun")) {
362
+ return "bun";
363
+ }
364
+ if (execPath.includes("yarn")) {
365
+ return "yarn";
366
+ }
367
+ if (execPath.includes("pnpm")) {
368
+ return "pnpm";
369
+ }
370
+ if (execPath.includes("npm")) {
371
+ return "npm";
372
+ }
169
373
  return "npm";
170
374
  }
375
+ function getInstallArgs(packageManager) {
376
+ switch (packageManager) {
377
+ case "npm":
378
+ return ["install", "--prefer-offline", "--no-audit", "--no-fund"];
379
+ case "pnpm":
380
+ return ["install", "--prefer-offline"];
381
+ default:
382
+ return ["install"];
383
+ }
384
+ }
171
385
 
172
386
  // src/create.ts
173
387
  async function create(args) {
174
- p.intro(color.inverse(" Mastra Software Factory "));
175
- let projectName = args.projectName;
176
- if (!projectName && args.useDefaults) {
177
- projectName = "my-software-factory";
178
- }
179
- if (!projectName) {
180
- const entered = await p.text({
181
- message: "What is your project named?",
182
- initialValue: "my-software-factory",
183
- validate: (value) => {
184
- if (!value?.trim()) return "Required";
185
- if (fs2.existsSync(path3.resolve(value.trim()))) return `Directory ${value.trim()} already exists`;
186
- return void 0;
187
- }
188
- });
189
- if (p.isCancel(entered)) return cancel2();
190
- projectName = entered.trim();
388
+ p.intro(color.inverse(" Mastra Factory "));
389
+ const projectName = args.projectName ?? await p.text({
390
+ message: "What do you want to name your project?",
391
+ placeholder: "my-mastra-factory",
392
+ validate: (value) => {
393
+ if (!value?.trim()) return `Project name can't be empty`;
394
+ if (fs3.existsSync(path3.resolve(value.trim()))) return `Directory ${value.trim()} already exists`;
395
+ return void 0;
396
+ }
397
+ });
398
+ if (p.isCancel(projectName)) {
399
+ p.cancel("Operation cancelled");
400
+ process.exit(0);
191
401
  }
192
402
  const projectPath = path3.resolve(projectName);
193
403
  const packageManager = detectPackageManager();
194
404
  args.analytics.trackEvent("sf_create_started", {
195
405
  package_manager: packageManager,
196
- non_interactive: Boolean(args.useDefaults)
406
+ no_platform: Boolean(args.noPlatform)
197
407
  });
198
408
  const spinner2 = p.spinner();
199
- spinner2.start("Downloading the Software Factory template...");
409
+ spinner2.start("Downloading the Mastra Factory template...");
200
410
  try {
201
- await cloneTemplate({
202
- repoUrl: DEFAULT_TEMPLATE_REPO,
203
- projectPath,
204
- ref: args.templateRef,
205
- localDir: args.templateDir
206
- });
207
- renameProject(projectPath, projectName);
208
- fs2.copyFileSync(path3.join(projectPath, ".env.example"), path3.join(projectPath, ".env"));
411
+ await cloneTemplate(args.template, projectPath);
412
+ await renameProject(projectPath, projectName);
413
+ const envPath = path3.join(projectPath, ".env");
414
+ fs3.copyFileSync(path3.join(projectPath, ".env.example"), envPath);
415
+ try {
416
+ fs3.chmodSync(envPath, 384);
417
+ } catch {
418
+ }
209
419
  spinner2.stop("Template downloaded.");
210
420
  } catch (err) {
211
421
  spinner2.stop("Template download failed.");
212
422
  throw err;
213
423
  }
214
424
  const installSpinner = p.spinner();
215
- installSpinner.start(`Installing dependencies with ${packageManager} (this can take a few minutes)...`);
425
+ installSpinner.start(`Installing dependencies...`);
216
426
  try {
217
- await runInherit(packageManager, ["install"], {
218
- cwd: projectPath,
219
- timeoutMs: args.timeout
427
+ await x2(packageManager, getInstallArgs(packageManager), {
428
+ throwOnError: true,
429
+ nodeOptions: { cwd: projectPath }
220
430
  });
221
431
  installSpinner.stop("Dependencies installed.");
222
432
  } catch (err) {
@@ -226,36 +436,195 @@ async function create(args) {
226
436
  You can retry manually: cd ${projectName} && ${packageManager} install`
227
437
  );
228
438
  }
439
+ let platformResult = null;
440
+ let platformError = null;
441
+ if (!args.noPlatform) {
442
+ try {
443
+ platformResult = await runPlatformProvisioning({
444
+ projectName,
445
+ projectPath,
446
+ region: args.region,
447
+ org: args.org
448
+ });
449
+ } catch (err) {
450
+ platformError = err instanceof Error ? err.message : String(err);
451
+ p.log.warn(`Platform provisioning failed: ${platformError}`);
452
+ }
453
+ }
454
+ let gitignoreOk = true;
229
455
  try {
230
- await runInherit("git", ["init", "-q"], { cwd: projectPath });
231
- await runInherit("git", ["add", "-A"], { cwd: projectPath });
232
- await runInherit("git", ["commit", "-q", "-m", "Initial commit from create-factory"], {
233
- cwd: projectPath
234
- });
235
- } catch {
236
- p.log.warn("git init failed \u2014 you can initialize the repository yourself later.");
456
+ ensureEnvGitignored(projectPath);
457
+ } catch (err) {
458
+ gitignoreOk = false;
459
+ const detail = err instanceof Error ? err.message : String(err);
460
+ p.log.warn(
461
+ `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'.`
462
+ );
463
+ }
464
+ if (gitignoreOk) {
465
+ try {
466
+ await x2("git", ["init", "-q"], { throwOnError: true, nodeOptions: { cwd: projectPath } });
467
+ await x2("git", ["add", "-A"], { throwOnError: true, nodeOptions: { cwd: projectPath } });
468
+ await x2("git", ["commit", "-q", "-m", "Initial commit from create-factory"], {
469
+ throwOnError: true,
470
+ nodeOptions: { cwd: projectPath }
471
+ });
472
+ } catch {
473
+ p.log.warn("git init failed \u2014 you can initialize the repository yourself later.");
474
+ }
237
475
  }
238
476
  args.analytics.trackEvent("sf_create_completed", {
239
477
  package_manager: packageManager,
240
- non_interactive: Boolean(args.useDefaults)
478
+ no_platform: Boolean(args.noPlatform),
479
+ platform_provisioned: platformResult !== null
241
480
  });
242
481
  const lines = [
243
- color.green("Your Software Factory is ready!"),
482
+ color.green("Your Mastra Factory is ready!"),
244
483
  "",
245
484
  `${color.cyan("cd")} ${projectName}`,
246
485
  color.cyan(`${packageManager} run dev`),
247
486
  "",
248
487
  `Factory UI ${color.underline("http://localhost:5173")}`,
249
488
  `Mastra Studio ${color.underline("http://localhost:4111")}`,
250
- "",
251
- "Open the Factory UI to finish setup (models, integrations, database)."
489
+ ""
252
490
  ];
491
+ if (platformResult) {
492
+ lines.push(
493
+ `${color.green("Platform connected.")} Project ${color.cyan(platformResult.project.name)} in ${color.cyan(platformResult.orgName)}.`,
494
+ `Wrote ${color.cyan("MASTRA_PROJECT_ID")}, ${color.cyan("MASTRA_PLATFORM_SECRET_KEY")}, and ${color.cyan("DATABASE_URL")} to ${color.cyan(".env")}.`
495
+ );
496
+ } else if (args.noPlatform) {
497
+ lines.push(
498
+ `${color.yellow("Skipped platform provisioning (--no-platform).")} Configure ${color.cyan(".env")} manually before running.`
499
+ );
500
+ } else if (platformError) {
501
+ lines.push(
502
+ `${color.yellow("Platform provisioning failed:")} ${platformError}`,
503
+ `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")}.`
504
+ );
505
+ } else {
506
+ lines.push("Open the Factory UI to finish setup (models, integrations, database).");
507
+ }
253
508
  p.note(lines.join("\n"), "Next steps");
254
- p.outro(`Problems or feedback? ${color.underline("https://github.com/mastra-ai/mastra/issues")}`);
255
509
  }
256
- function cancel2() {
257
- p.cancel("Cancelled.");
258
- process.exitCode = 1;
510
+ async function runPlatformProvisioning({
511
+ projectName,
512
+ projectPath,
513
+ region,
514
+ org
515
+ }) {
516
+ const envAccumulator = {};
517
+ const envPath = path3.join(projectPath, ".env");
518
+ let flushed = false;
519
+ const flush = () => {
520
+ if (flushed || Object.keys(envAccumulator).length === 0) return;
521
+ upsertEnvFile(envPath, envAccumulator);
522
+ flushed = true;
523
+ };
524
+ try {
525
+ p.log.info("Signing in to Mastra\u2026");
526
+ const token = await getToken();
527
+ const { orgId, orgName } = org ? await resolveOrgFromFlag(token, org) : await resolveCurrentOrg(token, { forcePrompt: true });
528
+ p.log.info(`Using organization ${color.cyan(orgName)}.`);
529
+ envAccumulator.MASTRA_SHARED_API_URL = MASTRA_PLATFORM_API_URL2;
530
+ envAccumulator.MASTRA_ORGANIZATION_ID = orgId;
531
+ const projectSpinner = p.spinner();
532
+ projectSpinner.start(`Creating platform project "${projectName}"\u2026`);
533
+ let project;
534
+ try {
535
+ project = await createServerProject({ token, orgId, name: projectName });
536
+ projectSpinner.stop(`Created platform project ${color.cyan(project.slug)}.`);
537
+ } catch (err) {
538
+ projectSpinner.stop("Project creation failed.");
539
+ throw err;
540
+ }
541
+ envAccumulator.MASTRA_PROJECT_ID = project.id;
542
+ const keySpinner = p.spinner();
543
+ keySpinner.start("Creating platform API key\u2026");
544
+ let secretKey;
545
+ try {
546
+ secretKey = await mintOrgApiKey({
547
+ token,
548
+ orgId,
549
+ keyName: `create-factory: ${projectName}`
550
+ });
551
+ keySpinner.stop("Platform API key created.");
552
+ } catch (err) {
553
+ keySpinner.stop("API key creation failed.");
554
+ throw err;
555
+ }
556
+ envAccumulator.MASTRA_PLATFORM_SECRET_KEY = secretKey;
557
+ const neonSpinner = p.spinner();
558
+ neonSpinner.start("Provisioning Neon Postgres database\u2026");
559
+ let databaseUrl;
560
+ try {
561
+ const attached = await attachNeonDatabase({
562
+ token,
563
+ orgId,
564
+ projectId: project.id,
565
+ name: sanitizeDatabaseName(projectName),
566
+ regionId: region
567
+ });
568
+ const ready = await waitForDatabaseReady({
569
+ token,
570
+ orgId,
571
+ projectId: project.id,
572
+ databaseId: attached.id
573
+ });
574
+ const connection = await getDatabaseConnection({
575
+ token,
576
+ orgId,
577
+ projectId: project.id,
578
+ databaseId: ready.id
579
+ });
580
+ const dbEnv = connection.envVars.find((v) => v.name === "DATABASE_URL");
581
+ if (!dbEnv?.value) {
582
+ throw new PlatformApiError(500, "Platform connection response missing DATABASE_URL.");
583
+ }
584
+ databaseUrl = dbEnv.value;
585
+ neonSpinner.stop("Neon database ready.");
586
+ } catch (err) {
587
+ neonSpinner.stop("Database provisioning failed.");
588
+ throw err;
589
+ }
590
+ envAccumulator.DATABASE_URL = databaseUrl;
591
+ flush();
592
+ return { orgId, orgName, project, secretKey, databaseUrl };
593
+ } finally {
594
+ flush();
595
+ }
596
+ }
597
+ async function resolveOrgFromFlag(token, value) {
598
+ const orgs = await fetchOrgs(token);
599
+ const match = orgs.find((o) => o.id === value || o.name === value);
600
+ if (!match) {
601
+ const available = orgs.map((o) => `${o.name} (${o.id})`).join(", ") || "(none)";
602
+ throw new Error(`No organization matched --org "${value}". Available: ${available}.`);
603
+ }
604
+ return { orgId: match.id, orgName: match.name };
605
+ }
606
+ function sanitizeDatabaseName(projectName) {
607
+ const cleaned = projectName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
608
+ const truncated = (cleaned || "factory").slice(0, 64);
609
+ return truncated;
610
+ }
611
+ function ensureEnvGitignored(projectPath) {
612
+ const gitignorePath = path3.join(projectPath, ".gitignore");
613
+ const existing = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
614
+ const lines = existing.split(/\r?\n/).map((line) => line.trim());
615
+ const covered = lines.some((line) => {
616
+ if (!line || line.startsWith("#")) return false;
617
+ return line === ".env" || line === ".env*" || line === "/.env" || line === "/.env*";
618
+ });
619
+ if (covered) return;
620
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
621
+ fs3.writeFileSync(
622
+ gitignorePath,
623
+ `${existing}${prefix}
624
+ # Added by create-factory to protect platform credentials
625
+ .env
626
+ `
627
+ );
259
628
  }
260
629
 
261
630
  // src/index.ts
@@ -264,20 +633,15 @@ var pkg = JSON.parse(
264
633
  );
265
634
  var analytics = new Analytics(pkg.version);
266
635
  var program = new Command();
267
- program.name("create-factory").description("Create a Mastra Software Factory project").version(pkg.version, "-v, --version").argument("[project-name]", "Directory name of the project").option("--default", "Non-interactive: default name").option("--template-ref <ref>", "Pin a template repo tag/branch").option("--template-dir <dir>", "Use a local template directory instead of cloning (development)").option("-t, --timeout [ms]", "Timeout for dependency installation in ms").action(async (projectNameArg, args) => {
268
- let timeout;
269
- if (args.timeout !== void 0) {
270
- timeout = args.timeout === true ? 6e4 : Number(args.timeout);
271
- if (!Number.isInteger(timeout) || timeout <= 0) {
272
- throw new Error(`--timeout must be a positive integer in milliseconds (got ${String(args.timeout)})`);
273
- }
274
- }
636
+ var DEFAULT_TEMPLATE_REPO = "https://github.com/mastra-ai/softwarefactory-template";
637
+ 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 \u2014 skips the interactive org picker").option("--region <region>", "Neon region id (passed to the platform verbatim)").version(pkg.version, "-v, --version").action(async (projectNameArg, args) => {
275
638
  await create({
276
639
  projectName: projectNameArg,
277
- useDefaults: Boolean(args.default),
278
- templateRef: args.templateRef ? String(args.templateRef) : void 0,
279
- templateDir: args.templateDir ? String(args.templateDir) : void 0,
280
- timeout,
640
+ template: args.template,
641
+ // commander's `--no-platform` flips `platform` to false when present, true by default.
642
+ noPlatform: args.platform === false,
643
+ org: args.org ? String(args.org) : void 0,
644
+ region: args.region ? String(args.region) : void 0,
281
645
  analytics
282
646
  });
283
647
  });
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "create-factory",
3
- "version": "0.0.3-alpha.0",
4
- "description": "Create a Mastra Software Factory project: an agent-powered software delivery environment built on Mastra. Run `npm create factory` to scaffold and get started.",
3
+ "version": "0.0.3-alpha.2",
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
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "bin": {
8
- "create-factory": "./bin/cli.mjs"
8
+ "create-factory": "./dist/index.js"
9
9
  },
10
10
  "files": [
11
- "bin",
12
11
  "dist",
13
12
  "CHANGELOG.md"
14
13
  ],
15
14
  "engines": {
16
- "node": ">=20"
15
+ "node": ">=22.13.0"
17
16
  },
18
17
  "repository": {
19
18
  "type": "git",
@@ -37,7 +36,9 @@
37
36
  "@clack/prompts": "^1.7.0",
38
37
  "commander": "^14.0.3",
39
38
  "picocolors": "^1.1.1",
40
- "posthog-node": "^5.37.0"
39
+ "posthog-node": "^5.37.0",
40
+ "tinyexec": "^1.2.4",
41
+ "mastra": "1.20.0-alpha.13"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/node": "22.20.1",
package/bin/cli.mjs DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/index.js';