create-factory 0.0.3-alpha.0 → 0.0.3-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +13 -30
- package/dist/index.js +500 -136
- package/package.json +7 -6
- package/bin/cli.mjs +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# create-factory
|
|
2
2
|
|
|
3
|
+
## 0.0.3-alpha.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The Software Factory template now pins every Mastra dep to `"latest"` instead of a caret range anchored on the current monorepo version, matching how every other create-mastra template ships. `sync-template.mjs` no longer shells out to `npm view` and no longer needs the `--tag` flag or the `legacy-peer-deps=true` `.npmrc` (which only existed to work around prerelease pins). The sync workflow no longer breaks whenever a linked package sits mid-alpha between publishes. ([#19989](https://github.com/mastra-ai/mastra/pull/19989))
|
|
8
|
+
|
|
9
|
+
## 0.0.3-alpha.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Marked projects created by create-factory as factory-enabled on the Mastra platform. ([#19973](https://github.com/mastra-ai/mastra/pull/19973))
|
|
14
|
+
|
|
15
|
+
- Updated dependencies:
|
|
16
|
+
- mastra@1.20.0-alpha.13
|
|
17
|
+
|
|
18
|
+
## 0.0.3-alpha.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Apply prettier formatting to env.test.ts (post-merge follow-up) ([#19965](https://github.com/mastra-ai/mastra/pull/19965))
|
|
23
|
+
|
|
24
|
+
- 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))
|
|
25
|
+
|
|
26
|
+
`.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.
|
|
27
|
+
|
|
28
|
+
- Fixed generated Software Factory projects missing their required `@mastra/memory` dependency. ([#19878](https://github.com/mastra-ai/mastra/pull/19878))
|
|
29
|
+
|
|
30
|
+
- 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))
|
|
31
|
+
|
|
32
|
+
- Signs the user in via the existing Mastra browser-auth flow.
|
|
33
|
+
- Creates a Mastra platform server project in the chosen organization.
|
|
34
|
+
- Mints an `sk_` organization API key scoped to the new factory.
|
|
35
|
+
- Attaches and provisions a Neon Postgres database.
|
|
36
|
+
- Writes `MASTRA_SHARED_API_URL`, `MASTRA_ORGANIZATION_ID`, `MASTRA_PROJECT_ID`, `MASTRA_PLATFORM_SECRET_KEY`, and `DATABASE_URL` to the project's `.env`.
|
|
37
|
+
|
|
38
|
+
The result is a locally-runnable factory that can talk to the Mastra platform on first `npm run dev` without any manual configuration.
|
|
39
|
+
|
|
40
|
+
**New flags:**
|
|
41
|
+
|
|
42
|
+
- `--no-platform` — skip the platform round-trip; useful when iterating on the template offline.
|
|
43
|
+
- `--region <region>` — pass a specific Neon region id through to the platform.
|
|
44
|
+
|
|
45
|
+
- Updated dependencies [[`8c88764`](https://github.com/mastra-ai/mastra/commit/8c88764162b32c8a24b3bf9d1ad2ec535aba5c9a), [`40ae860`](https://github.com/mastra-ai/mastra/commit/40ae860a674e7ee383a02c1f990d1e050619d5e4)]:
|
|
46
|
+
- mastra@1.20.0-alpha.12
|
|
47
|
+
|
|
3
48
|
## 0.0.3-alpha.0
|
|
4
49
|
|
|
5
50
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,47 +1,30 @@
|
|
|
1
1
|
# create-factory
|
|
2
2
|
|
|
3
|
-
Scaffolding CLI for the **Mastra
|
|
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.
|
|
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
|
-
|
|
14
|
+
Usage: create-factory [options] [project-name]
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
37
|
-
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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/
|
|
79
|
+
// src/env.ts
|
|
76
80
|
import fs from "fs";
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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/
|
|
126
|
-
|
|
127
|
-
async function
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
return body.project;
|
|
150
146
|
}
|
|
151
|
-
function
|
|
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(
|
|
278
|
+
const pkg2 = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
|
|
154
279
|
pkg2.name = toPackageName(projectName);
|
|
155
|
-
|
|
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 || "
|
|
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
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
406
|
+
no_platform: Boolean(args.noPlatform)
|
|
197
407
|
});
|
|
198
408
|
const spinner2 = p.spinner();
|
|
199
|
-
spinner2.start("Downloading the
|
|
409
|
+
spinner2.start("Downloading the Mastra Factory template...");
|
|
200
410
|
try {
|
|
201
|
-
await cloneTemplate(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
425
|
+
installSpinner.start(`Installing dependencies...`);
|
|
216
426
|
try {
|
|
217
|
-
await
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
478
|
+
no_platform: Boolean(args.noPlatform),
|
|
479
|
+
platform_provisioned: platformResult !== null
|
|
241
480
|
});
|
|
242
481
|
const lines = [
|
|
243
|
-
color.green("Your
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
4
|
-
"description": "Create a Mastra
|
|
3
|
+
"version": "0.0.3-alpha.3",
|
|
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": "./
|
|
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": ">=
|
|
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