@stacksjs/buddy 0.70.258 → 0.70.259
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/dist/cli.d.ts +11 -1
- package/dist/cli.js +5 -164
- package/dist/commands/about.js +1 -35
- package/dist/commands/add.js +1 -25
- package/dist/commands/ai-context.js +4 -27
- package/dist/commands/auth.js +1 -68
- package/dist/commands/build.js +1 -221
- package/dist/commands/cd.js +8 -39
- package/dist/commands/changelog.js +1 -32
- package/dist/commands/clean.js +1 -44
- package/dist/commands/cloud.js +2 -519
- package/dist/commands/commit.js +1 -16
- package/dist/commands/completion.js +6 -42
- package/dist/commands/config-migrate.js +6 -94
- package/dist/commands/configure.js +2 -45
- package/dist/commands/create.js +1 -162
- package/dist/commands/deploy.js +13 -1676
- package/dist/commands/desktop-apple.js +12 -399
- package/dist/commands/dev.js +6 -1083
- package/dist/commands/dns.js +3 -103
- package/dist/commands/docs/buddy-commands.js +10 -132
- package/dist/commands/docs/buddy-commands.test.js +1 -38
- package/dist/commands/docs/framework-repo.js +1 -14
- package/dist/commands/docs/framework-repo.test.js +1 -68
- package/dist/commands/docs/generated-artifacts.js +3 -70
- package/dist/commands/docs/generated-artifacts.test.js +1 -19
- package/dist/commands/docs/links.js +2 -85
- package/dist/commands/docs/links.test.js +4 -66
- package/dist/commands/docs.js +1 -24
- package/dist/commands/doctor.js +2 -478
- package/dist/commands/domains.js +2 -152
- package/dist/commands/email.js +31 -503
- package/dist/commands/env.js +1 -312
- package/dist/commands/extension.js +1 -239
- package/dist/commands/features.js +9 -345
- package/dist/commands/fresh.js +1 -46
- package/dist/commands/generate.js +2 -152
- package/dist/commands/http.js +1 -33
- package/dist/commands/index.js +1 -57
- package/dist/commands/install.js +1 -22
- package/dist/commands/key.js +1 -23
- package/dist/commands/link.js +2 -147
- package/dist/commands/lint.js +2 -84
- package/dist/commands/list.js +1 -140
- package/dist/commands/mail.js +15 -1477
- package/dist/commands/maintenance.js +1 -141
- package/dist/commands/make.js +1 -392
- package/dist/commands/migrate-project.js +1 -49
- package/dist/commands/migrate.js +27 -625
- package/dist/commands/outdated.js +1 -19
- package/dist/commands/package.js +1 -17
- package/dist/commands/phone.js +17 -181
- package/dist/commands/ports.js +2 -118
- package/dist/commands/prepublish.js +1 -17
- package/dist/commands/projects.js +1 -29
- package/dist/commands/protocol/craft-evidence.js +4 -98
- package/dist/commands/protocol/craft-evidence.test.js +1 -20
- package/dist/commands/protocol/desktop-lifecycle-report.js +3 -57
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +1 -32
- package/dist/commands/protocol/desktop-support.js +2 -32
- package/dist/commands/protocol/desktop-workflow.test.js +3 -35
- package/dist/commands/protocol/driver-registry.js +2 -71
- package/dist/commands/protocol/pantry-evidence.js +4 -144
- package/dist/commands/protocol/pantry-evidence.test.js +1 -29
- package/dist/commands/protocol/release-manifest.js +3 -102
- package/dist/commands/protocol/release-manifest.test.js +2 -34
- package/dist/commands/protocol/run-conformance.js +4 -346
- package/dist/commands/protocol/run-conformance.test.js +1 -52
- package/dist/commands/protocol/run-driver-contracts.js +4 -83
- package/dist/commands/protocol/run-driver-contracts.test.js +1 -18
- package/dist/commands/protocol/source-manifest.js +3 -113
- package/dist/commands/protocol/sync-suite.js +3 -136
- package/dist/commands/protocol/sync-suite.test.js +1 -44
- package/dist/commands/protocol.js +1 -60
- package/dist/commands/publish.js +8 -500
- package/dist/commands/queue.js +1 -248
- package/dist/commands/release.js +1 -42
- package/dist/commands/route.js +1 -21
- package/dist/commands/run-tool.js +1 -9
- package/dist/commands/saas.js +1 -25
- package/dist/commands/schedule.js +1 -61
- package/dist/commands/search.js +1 -84
- package/dist/commands/seed.js +1 -47
- package/dist/commands/serve.js +1 -55
- package/dist/commands/setup-ai.js +2 -121
- package/dist/commands/setup.js +2 -305
- package/dist/commands/share.js +1 -214
- package/dist/commands/sms.js +32 -319
- package/dist/commands/stacks.js +1 -72
- package/dist/commands/telemetry.js +1 -74
- package/dist/commands/test.js +1 -130
- package/dist/commands/tinker.js +1 -37
- package/dist/commands/types.js +1 -18
- package/dist/commands/upgrade.js +1 -98
- package/dist/commands/user.js +3 -84
- package/dist/commands/version.js +1 -14
- package/dist/config.js +1 -231
- package/dist/custom-cli.js +1 -23
- package/dist/database-preflight.js +1 -88
- package/dist/global-options.js +1 -4
- package/dist/index.js +1 -1
- package/dist/lazy-commands.js +1 -241
- package/dist/migrators/index.js +2 -62
- package/dist/migrators/laravel/index.js +1 -148
- package/dist/migrators/laravel/migrations.js +4 -231
- package/dist/migrators/laravel/models.js +8 -111
- package/dist/migrators/rails/index.js +1 -11
- package/dist/production-server.js +1 -257
- package/dist/project-setup.js +1 -36
- package/dist/serve-entry.js +1 -5
- package/dist/version-info.js +1 -5
- package/package.json +53 -46
package/dist/commands/serve.js
CHANGED
|
@@ -1,55 +1 @@
|
|
|
1
|
-
import { existsSync }
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { log } from "@stacksjs/logging";
|
|
5
|
-
import { ExitCode } from "@stacksjs/types";
|
|
6
|
-
import { startProductionServer } from "../production-server";
|
|
7
|
-
export {
|
|
8
|
-
loadStxPartialsDir,
|
|
9
|
-
resolveUserPartialsPath,
|
|
10
|
-
startProductionServer
|
|
11
|
-
} from "../production-server";
|
|
12
|
-
export function serve(buddy) {
|
|
13
|
-
buddy.command("serve", "Start the production HTTP server (STX views + /api proxy + coming-soon/maintenance gate)").option("-p, --port <port>", "Port to listen on (defaults to PORT env or 3000)").option("--verbose", "Enable verbose output", { default: !1 }).action(startProductionServer);
|
|
14
|
-
}
|
|
15
|
-
export function serveApi(buddy) {
|
|
16
|
-
buddy.command("serve:api", "Start the production API server (bun-router routes the frontend proxies /api to)").option("-p, --port <port>", "Port to listen on (defaults to PORT env or 3008)").action(async (options) => {
|
|
17
|
-
if (options?.port)
|
|
18
|
-
process.env.PORT = String(options.port);
|
|
19
|
-
process.env.APP_ENV = process.env.APP_ENV || "production";
|
|
20
|
-
await import(resolveApiEntry());
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
function resolveApiEntry() {
|
|
24
|
-
const vendored = join(process.cwd(), "storage/framework/core/actions/src/serve/api.ts");
|
|
25
|
-
if (existsSync(vendored))
|
|
26
|
-
return vendored;
|
|
27
|
-
return "@stacksjs/actions/serve/api";
|
|
28
|
-
}
|
|
29
|
-
export function preview(buddy) {
|
|
30
|
-
buddy.command("preview [dir]", "Serve a static build locally, the way a static host would").option("-p, --port <port>", "Port to listen on", { default: "3001" }).example("buddy preview").example("buddy preview dist --port 4000").action(async (dir, options) => {
|
|
31
|
-
const root = dir || "dist", port = Number(options?.port) || 3001;
|
|
32
|
-
if (!existsSync(root)) {
|
|
33
|
-
log.error(`No \`${root}\` directory to preview. Run \`buddy build\` first, or pass the directory to serve.`);
|
|
34
|
-
process.exit(ExitCode.FatalError);
|
|
35
|
-
}
|
|
36
|
-
Bun.serve({
|
|
37
|
-
port,
|
|
38
|
-
async fetch(req) {
|
|
39
|
-
let pathname = new URL(req.url).pathname;
|
|
40
|
-
if (pathname === "/" || pathname === "")
|
|
41
|
-
pathname = "/index.html";
|
|
42
|
-
else if (!pathname.includes("."))
|
|
43
|
-
pathname = `${pathname.replace(/\/$/, "")}.html`;
|
|
44
|
-
const file = Bun.file(join(root, pathname));
|
|
45
|
-
if (await file.exists())
|
|
46
|
-
return new Response(file);
|
|
47
|
-
const notFound = Bun.file(join(root, "404.html"));
|
|
48
|
-
if (await notFound.exists())
|
|
49
|
-
return new Response(notFound, { status: 404 });
|
|
50
|
-
return new Response("Not Found", { status: 404 });
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
log.success(`Previewing ./${root} at http://localhost:${port}`);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
1
|
+
import{existsSync}from"node:fs";import{join}from"node:path";import process from"node:process";import{log}from"@stacksjs/logging";import{ExitCode}from"@stacksjs/types";import{startProductionServer}from"../production-server";export{loadStxPartialsDir,resolveUserPartialsPath,startProductionServer}from"../production-server";export function serve(buddy){buddy.command("serve","Start the production HTTP server (STX views + /api proxy + coming-soon/maintenance gate)").option("-p, --port <port>","Port to listen on (defaults to PORT env or 3000)").option("--verbose","Enable verbose output",{default:!1}).action(startProductionServer)}export function serveApi(buddy){buddy.command("serve:api","Start the production API server (bun-router routes the frontend proxies /api to)").option("-p, --port <port>","Port to listen on (defaults to PORT env or 3008)").action(async(options)=>{if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";await import(resolveApiEntry())})}function resolveApiEntry(){const vendored=join(process.cwd(),"storage/framework/core/actions/src/serve/api.ts");if(existsSync(vendored))return vendored;return"@stacksjs/actions/serve/api"}export function preview(buddy){buddy.command("preview [dir]","Serve a static build locally, the way a static host would").option("-p, --port <port>","Port to listen on",{default:"3001"}).example("buddy preview").example("buddy preview dist --port 4000").action(async(dir,options)=>{const root=dir||"dist",port=Number(options?.port)||3001;if(!existsSync(root)){log.error(`No \`${root}\` directory to preview. Run \`buddy build\` first, or pass the directory to serve.`);process.exit(ExitCode.FatalError)}Bun.serve({port,async fetch(req){let pathname=new URL(req.url).pathname;if(pathname==="/"||pathname==="")pathname="/index.html";else if(!pathname.includes("."))pathname=`${pathname.replace(/\/$/,"")}.html`;const file=Bun.file(join(root,pathname));if(await file.exists())return new Response(file);const notFound=Bun.file(join(root,"404.html"));if(await notFound.exists())return new Response(notFound,{status:404});return new Response("Not Found",{status:404})}});log.success(`Previewing ./${root} at http://localhost:${port}`)})}
|
|
@@ -1,121 +1,2 @@
|
|
|
1
|
-
import { cpSync, existsSync,
|
|
2
|
-
|
|
3
|
-
import { dim, green, log, yellow } from "@stacksjs/cli";
|
|
4
|
-
import { frameworkPath, join, projectPath, relative } from "@stacksjs/path";
|
|
5
|
-
import { listSkills, resolveSkillPath } from "@stacksjs/skills";
|
|
6
|
-
export const AI_PROVIDERS = [
|
|
7
|
-
{ id: "claude", label: "Claude Code", reads: ["AGENTS.md", "CLAUDE.md", ".claude/skills", ".claude/launch.json"] },
|
|
8
|
-
{ id: "codex", label: "OpenAI Codex CLI", reads: ["AGENTS.md"] },
|
|
9
|
-
{ id: "cursor", label: "Cursor", reads: ["AGENTS.md", ".cursor/rules"] },
|
|
10
|
-
{ id: "copilot", label: "GitHub Copilot", reads: ["AGENTS.md", ".github/copilot-instructions.md"] },
|
|
11
|
-
{ id: "gemini", label: "Gemini CLI", reads: ["AGENTS.md", "GEMINI.md"] }
|
|
12
|
-
];
|
|
13
|
-
export function isAiProvider(value) {
|
|
14
|
-
return AI_PROVIDERS.some((provider) => provider.id === value);
|
|
15
|
-
}
|
|
16
|
-
function materialize(target, path, options) {
|
|
17
|
-
const existing = lstatSync(path, { throwIfNoEntry: !1 });
|
|
18
|
-
if (existing) {
|
|
19
|
-
if (!existing.isSymbolicLink() && !options.force)
|
|
20
|
-
return !1;
|
|
21
|
-
rmSync(path, { recursive: !0, force: !0 });
|
|
22
|
-
}
|
|
23
|
-
mkdirSync(join(path, ".."), { recursive: !0 });
|
|
24
|
-
if (options.copy)
|
|
25
|
-
cpSync(target, path, { recursive: !0 });
|
|
26
|
-
else
|
|
27
|
-
symlinkSync(relative(join(path, ".."), target), path, process.platform === "win32" ? "junction" : "dir");
|
|
28
|
-
return !0;
|
|
29
|
-
}
|
|
30
|
-
function materializeFile(source, path, options) {
|
|
31
|
-
if (existsSync(path) && !options.force)
|
|
32
|
-
return !1;
|
|
33
|
-
mkdirSync(join(path, ".."), { recursive: !0 });
|
|
34
|
-
cpSync(source, path, { recursive: !0, force: !0 });
|
|
35
|
-
return !0;
|
|
36
|
-
}
|
|
37
|
-
function installSkills(destination, options) {
|
|
38
|
-
const names = listSkills();
|
|
39
|
-
let installed = 0, skipped = 0;
|
|
40
|
-
mkdirSync(destination, { recursive: !0 });
|
|
41
|
-
for (const entry of readdirSync(destination)) {
|
|
42
|
-
const path = join(destination, entry);
|
|
43
|
-
if (lstatSync(path, { throwIfNoEntry: !1 })?.isSymbolicLink() && !existsSync(path))
|
|
44
|
-
rmSync(path, { force: !0 });
|
|
45
|
-
}
|
|
46
|
-
for (const name of names) {
|
|
47
|
-
const source = resolveSkillPath(name);
|
|
48
|
-
if (!source)
|
|
49
|
-
continue;
|
|
50
|
-
if (materialize(source, join(destination, name), options))
|
|
51
|
-
installed++;
|
|
52
|
-
else
|
|
53
|
-
skipped++;
|
|
54
|
-
}
|
|
55
|
-
return { installed, skipped };
|
|
56
|
-
}
|
|
57
|
-
export function setupAiProvider(provider, options = {}) {
|
|
58
|
-
const created = [], skipped = [], defaults = frameworkPath("defaults/ai"), rel = (path) => relative(projectPath(), path), record = (path, didCreate) => {
|
|
59
|
-
(didCreate ? created : skipped).push(rel(path));
|
|
60
|
-
}, agents = projectPath("AGENTS.md");
|
|
61
|
-
record(agents, materializeFile(join(defaults, "AGENTS.md"), agents, { ...options, force: !1 }));
|
|
62
|
-
switch (provider) {
|
|
63
|
-
case "claude": {
|
|
64
|
-
const claudeMd = projectPath("CLAUDE.md");
|
|
65
|
-
record(claudeMd, materialize(agents, claudeMd, { ...options, copy: !1 }));
|
|
66
|
-
const launch = projectPath(".claude/launch.json");
|
|
67
|
-
record(launch, materializeFile(join(defaults, "claude/launch.json"), launch, options));
|
|
68
|
-
const skillsDir = projectPath(".claude/skills"), { installed, skipped: untouched } = installSkills(skillsDir, options);
|
|
69
|
-
if (installed > 0)
|
|
70
|
-
created.push(`${rel(skillsDir)} (${installed} skills)`);
|
|
71
|
-
if (untouched > 0)
|
|
72
|
-
skipped.push(`${rel(skillsDir)} (${untouched} skills)`);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case "codex":
|
|
76
|
-
break;
|
|
77
|
-
case "cursor": {
|
|
78
|
-
const rules = projectPath(".cursor/rules");
|
|
79
|
-
record(rules, materialize(frameworkPath("defaults/ide/cursor/rules"), rules, options));
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
case "copilot": {
|
|
83
|
-
const instructions = projectPath(".github/copilot-instructions.md");
|
|
84
|
-
if (existsSync(instructions) && !options.force)
|
|
85
|
-
skipped.push(rel(instructions));
|
|
86
|
-
else {
|
|
87
|
-
mkdirSync(projectPath(".github"), { recursive: !0 });
|
|
88
|
-
writeFileSync(instructions, pointerFile("AGENTS.md"), "utf8");
|
|
89
|
-
created.push(rel(instructions));
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
case "gemini": {
|
|
94
|
-
const geminiMd = projectPath("GEMINI.md");
|
|
95
|
-
record(geminiMd, materialize(agents, geminiMd, { ...options, copy: !1 }));
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { created, skipped };
|
|
100
|
-
}
|
|
101
|
-
function pointerFile(target) {
|
|
102
|
-
return [
|
|
103
|
-
"# Project instructions",
|
|
104
|
-
"",
|
|
105
|
-
`This project keeps its agent guidance in [\`${target}\`](../${target}), so every`,
|
|
106
|
-
"agent works from the same rules. Read that file.",
|
|
107
|
-
"",
|
|
108
|
-
"Regenerate this pointer with `buddy setup:ai copilot --force`.",
|
|
109
|
-
""
|
|
110
|
-
].join(`
|
|
111
|
-
`);
|
|
112
|
-
}
|
|
113
|
-
export function reportAiSetup(provider, result) {
|
|
114
|
-
for (const path of result.created)
|
|
115
|
-
log.info(` ${green("+")} ${path}`);
|
|
116
|
-
for (const path of result.skipped)
|
|
117
|
-
log.info(` ${yellow("\xB7")} ${path} ${dim("(already present, left alone)")}`);
|
|
118
|
-
if (result.created.length === 0)
|
|
119
|
-
log.info(dim(" Nothing to do - re-run with --force to overwrite."));
|
|
120
|
-
log.success(`${provider.label} is set up. It reads: ${provider.reads.join(", ")}`);
|
|
121
|
-
}
|
|
1
|
+
import{cpSync,existsSync,lstatSync,mkdirSync,readdirSync,rmSync,symlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{dim,green,log,yellow}from"@stacksjs/cli";import{frameworkPath,join,projectPath,relative}from"@stacksjs/path";import{listSkills,resolveSkillPath}from"@stacksjs/skills";export const AI_PROVIDERS=[{id:"claude",label:"Claude Code",reads:["AGENTS.md","CLAUDE.md",".claude/skills",".claude/launch.json"]},{id:"codex",label:"OpenAI Codex CLI",reads:["AGENTS.md"]},{id:"cursor",label:"Cursor",reads:["AGENTS.md",".cursor/rules"]},{id:"copilot",label:"GitHub Copilot",reads:["AGENTS.md",".github/copilot-instructions.md"]},{id:"gemini",label:"Gemini CLI",reads:["AGENTS.md","GEMINI.md"]}];export function isAiProvider(value){return AI_PROVIDERS.some((provider)=>provider.id===value)}function materialize(target,path,options){const existing=lstatSync(path,{throwIfNoEntry:!1});if(existing){if(!existing.isSymbolicLink()&&!options.force)return!1;rmSync(path,{recursive:!0,force:!0})}mkdirSync(join(path,".."),{recursive:!0});if(options.copy)cpSync(target,path,{recursive:!0});else symlinkSync(relative(join(path,".."),target),path,process.platform==="win32"?"junction":"dir");return!0}function materializeFile(source,path,options){if(existsSync(path)&&!options.force)return!1;mkdirSync(join(path,".."),{recursive:!0});cpSync(source,path,{recursive:!0,force:!0});return!0}function installSkills(destination,options){const names=listSkills();let installed=0,skipped=0;mkdirSync(destination,{recursive:!0});for(const entry of readdirSync(destination)){const path=join(destination,entry);if(lstatSync(path,{throwIfNoEntry:!1})?.isSymbolicLink()&&!existsSync(path))rmSync(path,{force:!0})}for(const name of names){const source=resolveSkillPath(name);if(!source)continue;if(materialize(source,join(destination,name),options))installed++;else skipped++}return{installed,skipped}}export function setupAiProvider(provider,options={}){const created=[],skipped=[],defaults=frameworkPath("defaults/ai"),rel=(path)=>relative(projectPath(),path),record=(path,didCreate)=>{(didCreate?created:skipped).push(rel(path))},agents=projectPath("AGENTS.md");record(agents,materializeFile(join(defaults,"AGENTS.md"),agents,{...options,force:!1}));switch(provider){case"claude":{const claudeMd=projectPath("CLAUDE.md");record(claudeMd,materialize(agents,claudeMd,{...options,copy:!1}));const launch=projectPath(".claude/launch.json");record(launch,materializeFile(join(defaults,"claude/launch.json"),launch,options));const skillsDir=projectPath(".claude/skills"),{installed,skipped:untouched}=installSkills(skillsDir,options);if(installed>0)created.push(`${rel(skillsDir)} (${installed} skills)`);if(untouched>0)skipped.push(`${rel(skillsDir)} (${untouched} skills)`);break}case"codex":break;case"cursor":{const rules=projectPath(".cursor/rules");record(rules,materialize(frameworkPath("defaults/ide/cursor/rules"),rules,options));break}case"copilot":{const instructions=projectPath(".github/copilot-instructions.md");if(existsSync(instructions)&&!options.force)skipped.push(rel(instructions));else{mkdirSync(projectPath(".github"),{recursive:!0});writeFileSync(instructions,pointerFile("AGENTS.md"),"utf8");created.push(rel(instructions))}break}case"gemini":{const geminiMd=projectPath("GEMINI.md");record(geminiMd,materialize(agents,geminiMd,{...options,copy:!1}));break}}return{created,skipped}}function pointerFile(target){return["# Project instructions","",`This project keeps its agent guidance in [\`${target}\`](../${target}), so every`,"agent works from the same rules. Read that file.","","Regenerate this pointer with `buddy setup:ai copilot --force`.",""].join(`
|
|
2
|
+
`)}export function reportAiSetup(provider,result){for(const path of result.created)log.info(` ${green("+")} ${path}`);for(const path of result.skipped)log.info(` ${yellow("\xB7")} ${path} ${dim("(already present, left alone)")}`);if(result.created.length===0)log.info(dim(" Nothing to do - re-run with --force to overwrite."));log.success(`${provider.label} is set up. It reads: ${provider.reads.join(", ")}`)}
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,306 +1,3 @@
|
|
|
1
|
-
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { runAction } from "@stacksjs/actions";
|
|
6
|
-
import { log, onUnknownSubcommand, runCommand } from "@stacksjs/cli";
|
|
7
|
-
import { Action } from "@stacksjs/enums";
|
|
8
|
-
import { handleError } from "@stacksjs/error-handling";
|
|
9
|
-
import { path as p } from "@stacksjs/path";
|
|
10
|
-
import { copyFile, storage } from "@stacksjs/storage";
|
|
11
|
-
import { ExitCode } from "@stacksjs/types";
|
|
12
|
-
import { setupPrettyDevEnvironment } from "./dev";
|
|
13
|
-
function getTimeoutMs(envVar, fallbackMs) {
|
|
14
|
-
const value = Number(process.env[envVar]);
|
|
15
|
-
if (Number.isFinite(value) && value > 0)
|
|
16
|
-
return value;
|
|
17
|
-
return fallbackMs;
|
|
18
|
-
}
|
|
19
|
-
const PANTRY_CHECK_TIMEOUT_MS = getTimeoutMs("PANTRY_CHECK_TIMEOUT_MS", 15000), PANTRY_INSTALL_TIMEOUT_MS = getTimeoutMs("PANTRY_INSTALL_TIMEOUT_MS", 600000), PANTRY_DEPENDENCIES_TIMEOUT_MS = getTimeoutMs("PANTRY_DEPENDENCIES_TIMEOUT_MS", 1200000), KEYGEN_TIMEOUT_MS = getTimeoutMs("KEYGEN_TIMEOUT_MS", 120000), AWS_CONFIG_TIMEOUT_MS = getTimeoutMs("AWS_CONFIG_TIMEOUT_MS", 900000);
|
|
20
|
-
export function setup(buddy) {
|
|
21
|
-
const descriptions = {
|
|
22
|
-
setup: "This command ensures your project is setup correctly",
|
|
23
|
-
ssl: "Setup SSL certificates and hosts file for HTTPS development",
|
|
24
|
-
ai: "Set the project up for an AI coding agent (Claude Code, Codex, Cursor, Copilot, Gemini)",
|
|
25
|
-
copy: "Copy the agent files instead of symlinking them, so they can be edited per project",
|
|
26
|
-
force: "Overwrite files that already exist",
|
|
27
|
-
ohMyZsh: "Enable Oh My Zsh",
|
|
28
|
-
aws: "Ensures AWS is connected to the project",
|
|
29
|
-
project: "Target a specific project",
|
|
30
|
-
verbose: "Enable verbose output",
|
|
31
|
-
domain: "Custom domain to setup (defaults to APP_URL)",
|
|
32
|
-
skipHosts: "Skip adding domain to hosts file",
|
|
33
|
-
skipTrust: "Skip trusting the certificate",
|
|
34
|
-
skipAws: "Skip AWS configuration during setup",
|
|
35
|
-
skipKeygen: "Skip generating an application key during setup"
|
|
36
|
-
};
|
|
37
|
-
buddy.command("setup", descriptions.setup).alias("ensure").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--skip-aws", descriptions.skipAws, { default: !1 }).option("--skip-keygen", descriptions.skipKeygen, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
38
|
-
log.debug("Running `buddy setup` ...", options);
|
|
39
|
-
await ensurePantryInstalled();
|
|
40
|
-
await optimizePantryDeps();
|
|
41
|
-
await initializeProject(options);
|
|
42
|
-
});
|
|
43
|
-
buddy.command("setup:ssl", descriptions.ssl).alias("ssl:setup").option("-d, --domain [domain]", descriptions.domain).option("--skip-hosts", descriptions.skipHosts, { default: !1 }).option("--skip-trust", descriptions.skipTrust, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
44
|
-
log.debug("Running `buddy setup:ssl` ...", options);
|
|
45
|
-
if (!await setupPrettyDevEnvironment({
|
|
46
|
-
domain: options.domain,
|
|
47
|
-
skipHosts: options.skipHosts,
|
|
48
|
-
skipTrust: options.skipTrust,
|
|
49
|
-
verbose: options.verbose
|
|
50
|
-
})) {
|
|
51
|
-
log.warn("SSL setup completed with warnings");
|
|
52
|
-
log.info("You may need to manually trust certificates or update hosts file");
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
buddy.command("setup:ai [provider]", descriptions.ai).alias("ai:setup").option("--copy", descriptions.copy, { default: !1 }).option("--force", descriptions.force, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (provider, options) => {
|
|
56
|
-
log.debug("Running `buddy setup:ai` ...", options);
|
|
57
|
-
const { AI_PROVIDERS, isAiProvider, reportAiSetup, setupAiProvider } = await import("./setup-ai");
|
|
58
|
-
let id = provider;
|
|
59
|
-
if (!id) {
|
|
60
|
-
const { select } = await import("@stacksjs/cli");
|
|
61
|
-
id = await select({
|
|
62
|
-
message: "Which AI coding agent do you use?",
|
|
63
|
-
choices: AI_PROVIDERS.map((entry) => ({ value: entry.id, label: entry.label })),
|
|
64
|
-
initial: 0
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (!id || !isAiProvider(id)) {
|
|
68
|
-
log.error(`Unknown AI provider: ${id}. Expected one of: ${AI_PROVIDERS.map((entry) => entry.id).join(", ")}`);
|
|
69
|
-
process.exit(ExitCode.InvalidArgument);
|
|
70
|
-
}
|
|
71
|
-
const definition = AI_PROVIDERS.find((entry) => entry.id === id);
|
|
72
|
-
reportAiSetup(definition, setupAiProvider(id, { copy: options.copy, force: options.force }));
|
|
73
|
-
});
|
|
74
|
-
buddy.command("setup:oh-my-zsh", descriptions.ohMyZsh).alias("upgrade:oh-my-zsh").option("--verbose", descriptions.verbose, { default: !1 }).action(async (_options) => {
|
|
75
|
-
log.debug("Running `buddy setup:oh-my-zsh` ...", _options);
|
|
76
|
-
const result = await runAction(Action.UpgradeShell);
|
|
77
|
-
if (result.isErr) {
|
|
78
|
-
log.error(result.error);
|
|
79
|
-
process.exit(ExitCode.FatalError);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
onUnknownSubcommand(buddy, "setup");
|
|
83
|
-
}
|
|
84
|
-
async function isPantryInstalled() {
|
|
85
|
-
try {
|
|
86
|
-
return (await runCommand("pantry --version", {
|
|
87
|
-
silent: !0,
|
|
88
|
-
timeoutMs: PANTRY_CHECK_TIMEOUT_MS
|
|
89
|
-
})).isOk;
|
|
90
|
-
} catch {
|
|
91
|
-
return !1;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
async function installPantry() {
|
|
95
|
-
const bundledInstaller = p.frameworkPath("scripts/pantry-install"), command = existsSync(bundledInstaller) ? [bundledInstaller] : ["sh", "-c", "curl -fsSL https://pantry.dev | bash"], result = await runCommand(command, {
|
|
96
|
-
timeoutMs: PANTRY_INSTALL_TIMEOUT_MS
|
|
97
|
-
}), localBin = join(homedir(), ".local", "bin");
|
|
98
|
-
if (!process.env.PATH?.split(":").includes(localBin))
|
|
99
|
-
process.env.PATH = `${localBin}:${process.env.PATH || ""}`;
|
|
100
|
-
if (result.isOk && await isPantryInstalled())
|
|
101
|
-
return;
|
|
102
|
-
if (result.isErr)
|
|
103
|
-
handleError(result.error);
|
|
104
|
-
else
|
|
105
|
-
log.error("Pantry installed but is not available on PATH. Open a new shell and run `buddy setup` again.");
|
|
106
|
-
process.exit(ExitCode.FatalError);
|
|
107
|
-
}
|
|
108
|
-
export async function ensurePantryInstalled() {
|
|
109
|
-
if (await isPantryInstalled())
|
|
110
|
-
return;
|
|
111
|
-
log.info("Pantry is required. Installing it from https://pantry.dev...");
|
|
112
|
-
await installPantry();
|
|
113
|
-
}
|
|
114
|
-
export async function ensurePantryDependencies(cwd) {
|
|
115
|
-
await ensurePantryInstalled();
|
|
116
|
-
log.info("Installing project dependencies with Pantry...");
|
|
117
|
-
const result = await runCommand("pantry install", {
|
|
118
|
-
cwd,
|
|
119
|
-
timeoutMs: PANTRY_DEPENDENCIES_TIMEOUT_MS
|
|
120
|
-
});
|
|
121
|
-
if (result.isErr) {
|
|
122
|
-
handleError(result.error);
|
|
123
|
-
process.exit(ExitCode.FatalError);
|
|
124
|
-
}
|
|
125
|
-
if (existsSync(join(cwd, "package.json")) && !existsSync(join(cwd, "node_modules"))) {
|
|
126
|
-
log.error("Pantry completed without installing the project JavaScript dependencies.");
|
|
127
|
-
process.exit(ExitCode.FatalError);
|
|
128
|
-
}
|
|
129
|
-
log.success("Installed project dependencies with Pantry");
|
|
130
|
-
}
|
|
131
|
-
function hasAppKey(cwd) {
|
|
132
|
-
const envPath = join(cwd, ".env");
|
|
133
|
-
if (!existsSync(envPath))
|
|
134
|
-
return !1;
|
|
135
|
-
return /^APP_KEY=.+$/m.test(readFileSync(envPath, "utf-8"));
|
|
136
|
-
}
|
|
137
|
-
export async function ensureAppKey(cwd) {
|
|
138
|
-
if (hasAppKey(cwd) || process.env.APP_KEY && process.env.APP_KEY.length > 0) {
|
|
139
|
-
log.success("APP_KEY existed");
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const keyResult = await runCommand("./buddy key:generate", {
|
|
143
|
-
cwd,
|
|
144
|
-
timeoutMs: KEYGEN_TIMEOUT_MS
|
|
145
|
-
});
|
|
146
|
-
if (keyResult.isErr) {
|
|
147
|
-
handleError(keyResult.error);
|
|
148
|
-
process.exit(ExitCode.FatalError);
|
|
149
|
-
}
|
|
150
|
-
log.success("Generated application key");
|
|
151
|
-
}
|
|
152
|
-
async function runInitialMigration(cwd) {
|
|
153
|
-
const appEnv = (process.env.APP_ENV || process.env.NODE_ENV || "local").toLowerCase();
|
|
154
|
-
if (!["local", "development", "dev", "test"].includes(appEnv)) {
|
|
155
|
-
log.info(`Skipping initial migration in the ${appEnv} environment`);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
log.info("Running initial database migration...");
|
|
159
|
-
try {
|
|
160
|
-
const result = await runAction(Action.Migrate, { cwd });
|
|
161
|
-
if (result.isErr) {
|
|
162
|
-
log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");
|
|
163
|
-
log.debug(result.error);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
log.success("Database is migrated");
|
|
167
|
-
} catch (error) {
|
|
168
|
-
log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");
|
|
169
|
-
log.debug(error);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
async function initializeProject(options) {
|
|
173
|
-
const cwd = options.cwd || p.projectPath();
|
|
174
|
-
await ensurePantryDependencies(cwd);
|
|
175
|
-
await ensureEnvIsSet(options);
|
|
176
|
-
if (!options.skipKeygen)
|
|
177
|
-
await ensureAppKey(cwd);
|
|
178
|
-
await runInitialMigration(cwd);
|
|
179
|
-
ensureIdeSettings(cwd);
|
|
180
|
-
if (!options.skipAws) {
|
|
181
|
-
log.info("Ensuring AWS is connected...");
|
|
182
|
-
try {
|
|
183
|
-
const awsResult = await runCommand("./buddy configure:aws", {
|
|
184
|
-
cwd,
|
|
185
|
-
timeoutMs: AWS_CONFIG_TIMEOUT_MS
|
|
186
|
-
});
|
|
187
|
-
if (awsResult.isErr) {
|
|
188
|
-
log.warn("AWS not configured - you can do this later via ./buddy configure:aws");
|
|
189
|
-
log.debug(awsResult.error);
|
|
190
|
-
} else
|
|
191
|
-
log.success("Configured AWS");
|
|
192
|
-
} catch (error) {
|
|
193
|
-
log.warn("AWS not configured - you can do this later via ./buddy configure:aws");
|
|
194
|
-
log.debug(error);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
log.success("Project is setup");
|
|
198
|
-
log.info("Run `./buddy doctor` anytime to check your setup. Happy coding! \uD83D\uDC99");
|
|
199
|
-
}
|
|
200
|
-
export function ensureIdeSettings(cwd) {
|
|
201
|
-
const source = p.frameworkPath("defaults/ide/vscode/.vscode"), destination = join(cwd, ".vscode");
|
|
202
|
-
if (existsSync(destination)) {
|
|
203
|
-
log.debug(".vscode already exists; keeping the project settings");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (!existsSync(source)) {
|
|
207
|
-
log.debug("No bundled VS Code settings found; skipping IDE setup");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
cpSync(source, destination, { recursive: !0 });
|
|
211
|
-
log.success("Installed project VS Code settings");
|
|
212
|
-
}
|
|
213
|
-
const DB_CONNECTION_PACKAGES = {
|
|
214
|
-
postgres: { name: "postgresql.org", version: "^17.10", service: "postgres" },
|
|
215
|
-
mysql: { name: "mysql.com", version: "*", service: "mysql" },
|
|
216
|
-
sqlite: { name: "sqlite.org", version: "^3.47.2" }
|
|
217
|
-
};
|
|
218
|
-
function databaseAliases(connection, pkg) {
|
|
219
|
-
return [connection, pkg.name];
|
|
220
|
-
}
|
|
221
|
-
export function pantryDatabasePackage(connection) {
|
|
222
|
-
return DB_CONNECTION_PACKAGES[connection];
|
|
223
|
-
}
|
|
224
|
-
function detectDbPackage(cwd) {
|
|
225
|
-
const envPath = join(cwd, ".env"), envExamplePath = join(cwd, ".env.example"), filePath = existsSync(envPath) ? envPath : existsSync(envExamplePath) ? envExamplePath : void 0;
|
|
226
|
-
if (!filePath)
|
|
227
|
-
return;
|
|
228
|
-
const match = readFileSync(filePath, "utf-8").match(/^DB_CONNECTION=(.+)$/m);
|
|
229
|
-
if (!match)
|
|
230
|
-
return;
|
|
231
|
-
const value = match[1].trim().replace(/['"]/g, "");
|
|
232
|
-
return pantryDatabasePackage(value);
|
|
233
|
-
}
|
|
234
|
-
export async function optimizePantryDeps() {
|
|
235
|
-
const cwd = p.projectPath(), depsConfigPath = join(cwd, "config", "deps.ts");
|
|
236
|
-
if (!existsSync(depsConfigPath)) {
|
|
237
|
-
log.debug("No config/deps.ts found, skipping dependency optimization");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
let configDeps = {}, configServices = [];
|
|
241
|
-
try {
|
|
242
|
-
const mod = await import(depsConfigPath), config = mod.config || mod.default;
|
|
243
|
-
if (config?.dependencies)
|
|
244
|
-
configDeps = { ...config.dependencies };
|
|
245
|
-
if (Array.isArray(config?.services?.autoStart))
|
|
246
|
-
configServices = config.services.autoStart.filter((name) => typeof name === "string");
|
|
247
|
-
} catch (err) {
|
|
248
|
-
log.debug("Could not load config/deps.ts, skipping dependency optimization");
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const dbPackage = detectDbPackage(cwd), autoStart = [...configServices];
|
|
252
|
-
if (dbPackage) {
|
|
253
|
-
const selected = new Set(Object.entries(DB_CONNECTION_PACKAGES).filter(([, pkg]) => pkg.name === dbPackage.name).flatMap(([connection, pkg]) => databaseAliases(connection, pkg))), unused = new Set(Object.entries(DB_CONNECTION_PACKAGES).flatMap(([connection, pkg]) => databaseAliases(connection, pkg)).filter((alias) => !selected.has(alias)));
|
|
254
|
-
for (const pkg of Object.keys(configDeps)) {
|
|
255
|
-
const domain = pkg.split("/")[0];
|
|
256
|
-
if (unused.has(domain)) {
|
|
257
|
-
log.info(`DB_CONNECTION selects ${dbPackage.name}, dropping unused ${pkg}`);
|
|
258
|
-
delete configDeps[pkg];
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (!Object.keys(configDeps).some((key) => {
|
|
262
|
-
const domain = key.split("/")[0];
|
|
263
|
-
return selected.has(domain);
|
|
264
|
-
})) {
|
|
265
|
-
log.info(`Detected DB_CONNECTION requires ${dbPackage.name}, adding to dependencies`);
|
|
266
|
-
configDeps[dbPackage.name] = dbPackage.version;
|
|
267
|
-
}
|
|
268
|
-
if (dbPackage.service && !autoStart.includes(dbPackage.service))
|
|
269
|
-
autoStart.push(dbPackage.service);
|
|
270
|
-
}
|
|
271
|
-
const lines = [
|
|
272
|
-
"# Auto-generated from config/deps.ts and .env sniffing.",
|
|
273
|
-
"# This file is regenerated on each `buddy setup` run.",
|
|
274
|
-
"#",
|
|
275
|
-
"# To learn more, please visit:",
|
|
276
|
-
"# https://stacksjs.com/docs/dependency-management",
|
|
277
|
-
"",
|
|
278
|
-
"dependencies:"
|
|
279
|
-
];
|
|
280
|
-
for (const [pkg, version] of Object.entries(configDeps))
|
|
281
|
-
lines.push(` ${pkg}: ${version}`);
|
|
282
|
-
if (autoStart.length > 0) {
|
|
283
|
-
lines.push("", "services:", " enabled: true", " autoStart:");
|
|
284
|
-
for (const service of autoStart)
|
|
285
|
-
lines.push(` - ${service}`);
|
|
286
|
-
}
|
|
287
|
-
const depsYamlPath = join(cwd, "deps.yaml");
|
|
288
|
-
writeFileSync(depsYamlPath, `${lines.join(`
|
|
1
|
+
import{cpSync,existsSync,readFileSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{join}from"node:path";import process from"node:process";import{runAction}from"@stacksjs/actions";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{handleError}from"@stacksjs/error-handling";import{path as p}from"@stacksjs/path";import{copyFile,storage}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";import{setupPrettyDevEnvironment}from"./dev";function getTimeoutMs(envVar,fallbackMs){const value=Number(process.env[envVar]);if(Number.isFinite(value)&&value>0)return value;return fallbackMs}const PANTRY_CHECK_TIMEOUT_MS=getTimeoutMs("PANTRY_CHECK_TIMEOUT_MS",15000),PANTRY_INSTALL_TIMEOUT_MS=getTimeoutMs("PANTRY_INSTALL_TIMEOUT_MS",600000),PANTRY_DEPENDENCIES_TIMEOUT_MS=getTimeoutMs("PANTRY_DEPENDENCIES_TIMEOUT_MS",1200000),KEYGEN_TIMEOUT_MS=getTimeoutMs("KEYGEN_TIMEOUT_MS",120000),AWS_CONFIG_TIMEOUT_MS=getTimeoutMs("AWS_CONFIG_TIMEOUT_MS",900000);export function setup(buddy){const descriptions={setup:"This command ensures your project is setup correctly",ssl:"Setup SSL certificates and hosts file for HTTPS development",ai:"Set the project up for an AI coding agent (Claude Code, Codex, Cursor, Copilot, Gemini)",copy:"Copy the agent files instead of symlinking them, so they can be edited per project",force:"Overwrite files that already exist",ohMyZsh:"Enable Oh My Zsh",aws:"Ensures AWS is connected to the project",project:"Target a specific project",verbose:"Enable verbose output",domain:"Custom domain to setup (defaults to APP_URL)",skipHosts:"Skip adding domain to hosts file",skipTrust:"Skip trusting the certificate",skipAws:"Skip AWS configuration during setup",skipKeygen:"Skip generating an application key during setup"};buddy.command("setup",descriptions.setup).alias("ensure").option("-p, --project [project]",descriptions.project,{default:!1}).option("--skip-aws",descriptions.skipAws,{default:!1}).option("--skip-keygen",descriptions.skipKeygen,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy setup` ...",options);await ensurePantryInstalled();await optimizePantryDeps();await initializeProject(options)});buddy.command("setup:ssl",descriptions.ssl).alias("ssl:setup").option("-d, --domain [domain]",descriptions.domain).option("--skip-hosts",descriptions.skipHosts,{default:!1}).option("--skip-trust",descriptions.skipTrust,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy setup:ssl` ...",options);if(!await setupPrettyDevEnvironment({domain:options.domain,skipHosts:options.skipHosts,skipTrust:options.skipTrust,verbose:options.verbose})){log.warn("SSL setup completed with warnings");log.info("You may need to manually trust certificates or update hosts file")}});buddy.command("setup:ai [provider]",descriptions.ai).alias("ai:setup").option("--copy",descriptions.copy,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(provider,options)=>{log.debug("Running `buddy setup:ai` ...",options);const{AI_PROVIDERS,isAiProvider,reportAiSetup,setupAiProvider}=await import("./setup-ai");let id=provider;if(!id){const{select}=await import("@stacksjs/cli");id=await select({message:"Which AI coding agent do you use?",choices:AI_PROVIDERS.map((entry)=>({value:entry.id,label:entry.label})),initial:0})}if(!id||!isAiProvider(id)){log.error(`Unknown AI provider: ${id}. Expected one of: ${AI_PROVIDERS.map((entry)=>entry.id).join(", ")}`);process.exit(ExitCode.InvalidArgument)}const definition=AI_PROVIDERS.find((entry)=>entry.id===id);reportAiSetup(definition,setupAiProvider(id,{copy:options.copy,force:options.force}))});buddy.command("setup:oh-my-zsh",descriptions.ohMyZsh).alias("upgrade:oh-my-zsh").option("--verbose",descriptions.verbose,{default:!1}).action(async(_options)=>{log.debug("Running `buddy setup:oh-my-zsh` ...",_options);const result=await runAction(Action.UpgradeShell);if(result.isErr){log.error(result.error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"setup")}async function isPantryInstalled(){try{return(await runCommand("pantry --version",{silent:!0,timeoutMs:PANTRY_CHECK_TIMEOUT_MS})).isOk}catch{return!1}}async function installPantry(){const bundledInstaller=p.frameworkPath("scripts/pantry-install"),command=existsSync(bundledInstaller)?[bundledInstaller]:["sh","-c","curl -fsSL https://pantry.dev | bash"],result=await runCommand(command,{timeoutMs:PANTRY_INSTALL_TIMEOUT_MS}),localBin=join(homedir(),".local","bin");if(!process.env.PATH?.split(":").includes(localBin))process.env.PATH=`${localBin}:${process.env.PATH||""}`;if(result.isOk&&await isPantryInstalled())return;if(result.isErr)handleError(result.error);else log.error("Pantry installed but is not available on PATH. Open a new shell and run `buddy setup` again.");process.exit(ExitCode.FatalError)}export async function ensurePantryInstalled(){if(await isPantryInstalled())return;log.info("Pantry is required. Installing it from https://pantry.dev...");await installPantry()}export async function ensurePantryDependencies(cwd){await ensurePantryInstalled();log.info("Installing project dependencies with Pantry...");const result=await runCommand("pantry install",{cwd,timeoutMs:PANTRY_DEPENDENCIES_TIMEOUT_MS});if(result.isErr){handleError(result.error);process.exit(ExitCode.FatalError)}if(existsSync(join(cwd,"package.json"))&&!existsSync(join(cwd,"node_modules"))){log.error("Pantry completed without installing the project JavaScript dependencies.");process.exit(ExitCode.FatalError)}log.success("Installed project dependencies with Pantry")}function hasAppKey(cwd){const envPath=join(cwd,".env");if(!existsSync(envPath))return!1;return/^APP_KEY=.+$/m.test(readFileSync(envPath,"utf-8"))}export async function ensureAppKey(cwd){if(hasAppKey(cwd)||process.env.APP_KEY&&process.env.APP_KEY.length>0){log.success("APP_KEY existed");return}const keyResult=await runCommand("./buddy key:generate",{cwd,timeoutMs:KEYGEN_TIMEOUT_MS});if(keyResult.isErr){handleError(keyResult.error);process.exit(ExitCode.FatalError)}log.success("Generated application key")}async function runInitialMigration(cwd){const appEnv=(process.env.APP_ENV||process.env.NODE_ENV||"local").toLowerCase();if(!["local","development","dev","test"].includes(appEnv)){log.info(`Skipping initial migration in the ${appEnv} environment`);return}log.info("Running initial database migration...");try{const result=await runAction(Action.Migrate,{cwd});if(result.isErr){log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");log.debug(result.error);return}log.success("Database is migrated")}catch(error){log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");log.debug(error)}}async function initializeProject(options){const cwd=options.cwd||p.projectPath();await ensurePantryDependencies(cwd);await ensureEnvIsSet(options);if(!options.skipKeygen)await ensureAppKey(cwd);await runInitialMigration(cwd);ensureIdeSettings(cwd);if(!options.skipAws){log.info("Ensuring AWS is connected...");try{const awsResult=await runCommand("./buddy configure:aws",{cwd,timeoutMs:AWS_CONFIG_TIMEOUT_MS});if(awsResult.isErr){log.warn("AWS not configured - you can do this later via ./buddy configure:aws");log.debug(awsResult.error)}else log.success("Configured AWS")}catch(error){log.warn("AWS not configured - you can do this later via ./buddy configure:aws");log.debug(error)}}log.success("Project is setup");log.info("Run `./buddy doctor` anytime to check your setup. Happy coding! \uD83D\uDC99")}export function ensureIdeSettings(cwd){const source=p.frameworkPath("defaults/ide/vscode/.vscode"),destination=join(cwd,".vscode");if(existsSync(destination)){log.debug(".vscode already exists; keeping the project settings");return}if(!existsSync(source)){log.debug("No bundled VS Code settings found; skipping IDE setup");return}cpSync(source,destination,{recursive:!0});log.success("Installed project VS Code settings")}const DB_CONNECTION_PACKAGES={postgres:{name:"postgresql.org",version:"^17.10",service:"postgres"},mysql:{name:"mysql.com",version:"*",service:"mysql"},sqlite:{name:"sqlite.org",version:"^3.47.2"}};function databaseAliases(connection,pkg){return[connection,pkg.name]}export function pantryDatabasePackage(connection){return DB_CONNECTION_PACKAGES[connection]}function detectDbPackage(cwd){const envPath=join(cwd,".env"),envExamplePath=join(cwd,".env.example"),filePath=existsSync(envPath)?envPath:existsSync(envExamplePath)?envExamplePath:void 0;if(!filePath)return;const match=readFileSync(filePath,"utf-8").match(/^DB_CONNECTION=(.+)$/m);if(!match)return;const value=match[1].trim().replace(/['"]/g,"");return pantryDatabasePackage(value)}export async function optimizePantryDeps(){const cwd=p.projectPath(),depsConfigPath=join(cwd,"config","deps.ts");if(!existsSync(depsConfigPath)){log.debug("No config/deps.ts found, skipping dependency optimization");return}let configDeps={},configServices=[];try{const mod=await import(depsConfigPath),config=mod.config||mod.default;if(config?.dependencies)configDeps={...config.dependencies};if(Array.isArray(config?.services?.autoStart))configServices=config.services.autoStart.filter((name)=>typeof name==="string")}catch(err){log.debug("Could not load config/deps.ts, skipping dependency optimization");return}const dbPackage=detectDbPackage(cwd),autoStart=[...configServices];if(dbPackage){const selected=new Set(Object.entries(DB_CONNECTION_PACKAGES).filter(([,pkg])=>pkg.name===dbPackage.name).flatMap(([connection,pkg])=>databaseAliases(connection,pkg))),unused=new Set(Object.entries(DB_CONNECTION_PACKAGES).flatMap(([connection,pkg])=>databaseAliases(connection,pkg)).filter((alias)=>!selected.has(alias)));for(const pkg of Object.keys(configDeps)){const domain=pkg.split("/")[0];if(unused.has(domain)){log.info(`DB_CONNECTION selects ${dbPackage.name}, dropping unused ${pkg}`);delete configDeps[pkg]}}if(!Object.keys(configDeps).some((key)=>{const domain=key.split("/")[0];return selected.has(domain)})){log.info(`Detected DB_CONNECTION requires ${dbPackage.name}, adding to dependencies`);configDeps[dbPackage.name]=dbPackage.version}if(dbPackage.service&&!autoStart.includes(dbPackage.service))autoStart.push(dbPackage.service)}const lines=["# Auto-generated from config/deps.ts and .env sniffing.","# This file is regenerated on each `buddy setup` run.","#","# To learn more, please visit:","# https://stacksjs.com/docs/dependency-management","","dependencies:"];for(const[pkg,version]of Object.entries(configDeps))lines.push(` ${pkg}: ${version}`);if(autoStart.length>0){lines.push("","services:"," enabled: true"," autoStart:");for(const service of autoStart)lines.push(` - ${service}`)}const depsYamlPath=join(cwd,"deps.yaml");writeFileSync(depsYamlPath,`${lines.join(`
|
|
289
2
|
`)}
|
|
290
|
-
`);
|
|
291
|
-
log.success("Generated deps.yaml from config/deps.ts");
|
|
292
|
-
}
|
|
293
|
-
export async function ensureEnvIsSet(options) {
|
|
294
|
-
log.info("Ensuring .env exists...");
|
|
295
|
-
const cwd = options.cwd || p.projectPath(), envPath = `${cwd}/.env`, envExamplePath = `${cwd}/.env.example`;
|
|
296
|
-
if (storage.doesNotExist(envPath)) {
|
|
297
|
-
try {
|
|
298
|
-
copyFile(envExamplePath, envPath);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
handleError(error);
|
|
301
|
-
process.exit(ExitCode.FatalError);
|
|
302
|
-
}
|
|
303
|
-
log.success(".env created");
|
|
304
|
-
} else
|
|
305
|
-
log.success(".env existed");
|
|
306
|
-
}
|
|
3
|
+
`);log.success("Generated deps.yaml from config/deps.ts")}export async function ensureEnvIsSet(options){log.info("Ensuring .env exists...");const cwd=options.cwd||p.projectPath(),envPath=`${cwd}/.env`,envExamplePath=`${cwd}/.env.example`;if(storage.doesNotExist(envPath)){try{copyFile(envExamplePath,envPath)}catch(error){handleError(error);process.exit(ExitCode.FatalError)}log.success(".env created")}else log.success(".env existed")}
|