@stacksjs/buddy 0.70.59 → 0.70.60
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.js +162 -4
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.js +137 -0
- package/dist/commands/deploy.js +1404 -0
- package/dist/commands/dev.js +911 -0
- package/dist/commands/dns.js +62 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.js +526 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.js +30 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.js +23 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.js +50 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.js +693 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.js +373 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.js +169 -0
- package/dist/commands/queue.js +249 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.js +176 -0
- package/dist/commands/setup.js +203 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +209 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.js +97 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.js +1 -3424
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +182 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.js +231 -0
- package/dist/migrators/laravel/models.js +132 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { runAction } from "@stacksjs/actions";
|
|
3
|
+
import { intro, log, outro } from "@stacksjs/cli";
|
|
4
|
+
import { Action } from "@stacksjs/enums";
|
|
5
|
+
import { ExitCode } from "@stacksjs/types";
|
|
6
|
+
export function saas(buddy) {
|
|
7
|
+
const descriptions = {
|
|
8
|
+
stripe: "Sets up stripe products in the dashboard",
|
|
9
|
+
project: "Target a specific project",
|
|
10
|
+
verbose: "Enable verbose output"
|
|
11
|
+
};
|
|
12
|
+
buddy.command("stripe:setup", descriptions.stripe).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
13
|
+
log.debug("Running `buddy stripe:setup` ...", options);
|
|
14
|
+
const perf = await intro("buddy stripe:setup"), result = await runAction(Action.StripeSetup, options);
|
|
15
|
+
if (result.isErr) {
|
|
16
|
+
await outro("While running the stripe:setup command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
17
|
+
process.exit(ExitCode.FatalError);
|
|
18
|
+
}
|
|
19
|
+
await outro("Stripe products created successfully", {
|
|
20
|
+
startTime: perf,
|
|
21
|
+
useSeconds: !0
|
|
22
|
+
});
|
|
23
|
+
process.exit(ExitCode.Success);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { runAction } from "@stacksjs/actions";
|
|
3
|
+
import { intro, log, outro } from "@stacksjs/cli";
|
|
4
|
+
import { Action } from "@stacksjs/enums";
|
|
5
|
+
import { ExitCode } from "@stacksjs/types";
|
|
6
|
+
export function schedule(buddy) {
|
|
7
|
+
const descriptions = {
|
|
8
|
+
project: "Target a specific project",
|
|
9
|
+
schedule: "Run the scheduler",
|
|
10
|
+
verbose: "Enable verbose output",
|
|
11
|
+
list: "List all registered scheduled tasks with their next run time",
|
|
12
|
+
status: "Show currently-held overlap locks (this-process only)"
|
|
13
|
+
};
|
|
14
|
+
buddy.command("schedule:run", descriptions.schedule).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
15
|
+
log.debug("Running `buddy schedule:run` ...", options);
|
|
16
|
+
const perf = await intro("buddy schedule:run"), result = await runAction(Action.ScheduleRun, options);
|
|
17
|
+
if (result.isErr) {
|
|
18
|
+
await outro("While running the schedule:run command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
19
|
+
process.exit(ExitCode.FatalError);
|
|
20
|
+
}
|
|
21
|
+
process.exit(ExitCode.Success);
|
|
22
|
+
});
|
|
23
|
+
buddy.command("schedule:list", descriptions.list).action(async () => {
|
|
24
|
+
try {
|
|
25
|
+
await import(`${process.cwd()}/routes/scheduler.ts`).catch(() => {
|
|
26
|
+
log.warn("[schedule:list] no routes/scheduler.ts found \u2014 registry will be empty unless tasks were registered elsewhere");
|
|
27
|
+
});
|
|
28
|
+
const { Schedule } = await import("@stacksjs/scheduler"), jobs = Schedule.listJobs();
|
|
29
|
+
if (jobs.length === 0) {
|
|
30
|
+
log.info("No scheduled tasks registered.");
|
|
31
|
+
process.exit(ExitCode.Success);
|
|
32
|
+
}
|
|
33
|
+
const nameWidth = Math.max(4, ...jobs.map((j) => j.name.length)), patternWidth = Math.max(7, ...jobs.map((j) => (j.pattern ?? "").length));
|
|
34
|
+
log.info(`${"NAME".padEnd(nameWidth)} ${"PATTERN".padEnd(patternWidth)} TIMEZONE NEXT RUN (UTC)`);
|
|
35
|
+
for (const j of jobs) {
|
|
36
|
+
const next = j.nextRun ? j.nextRun.toISOString() : "\u2014";
|
|
37
|
+
log.info(`${j.name.padEnd(nameWidth)} ${(j.pattern ?? "").padEnd(patternWidth)} ${(j.timezone ?? "UTC").padEnd(25)} ${next}`);
|
|
38
|
+
}
|
|
39
|
+
process.exit(ExitCode.Success);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
log.error(`[schedule:list] failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
process.exit(ExitCode.FatalError);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
buddy.command("schedule:status", descriptions.status).action(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const { Schedule } = await import("@stacksjs/scheduler"), held = Schedule.listLocks();
|
|
48
|
+
if (held.length === 0)
|
|
49
|
+
log.info("No in-process scheduler locks held.");
|
|
50
|
+
else {
|
|
51
|
+
log.info(`Held locks (${held.length}):`);
|
|
52
|
+
for (const name of held)
|
|
53
|
+
log.info(` \u2022 ${name}`);
|
|
54
|
+
}
|
|
55
|
+
process.exit(ExitCode.Success);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
log.error(`[schedule:status] failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
process.exit(ExitCode.FatalError);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { runAction } from "@stacksjs/actions";
|
|
3
|
+
import { intro, log, outro } from "@stacksjs/cli";
|
|
4
|
+
import { Action } from "@stacksjs/enums";
|
|
5
|
+
import { ExitCode } from "@stacksjs/types";
|
|
6
|
+
export function search(buddy) {
|
|
7
|
+
const descriptions = {
|
|
8
|
+
import: "Indexes database data to search engine",
|
|
9
|
+
flush: "Flushes all data from search engine",
|
|
10
|
+
settings: "Update index settings",
|
|
11
|
+
list: "List index settings",
|
|
12
|
+
model: "Target a specific model",
|
|
13
|
+
project: "Target a specific project",
|
|
14
|
+
verbose: "Enable verbose output"
|
|
15
|
+
};
|
|
16
|
+
buddy.command("search-engine:update", descriptions.import).option("-m, --model [model]", descriptions.model, { default: "" }).option("-f, --flush [flush]", descriptions.flush, { default: !1 }).option("-s, --settings [settings]", descriptions.settings, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).action(async (options) => {
|
|
17
|
+
log.debug("Running `search-engine:update` ...", options);
|
|
18
|
+
let actionString = Action.SearchEngineImport, introString = "search-engine:update";
|
|
19
|
+
if (options.model) {
|
|
20
|
+
actionString = Action.SearchEngineImport;
|
|
21
|
+
introString = `search-engine:update --model ${options.model}`;
|
|
22
|
+
}
|
|
23
|
+
if (options.settings) {
|
|
24
|
+
actionString = Action.SearchEnginePushSettings;
|
|
25
|
+
introString = "search-engine:update --settings";
|
|
26
|
+
}
|
|
27
|
+
if (options.flush) {
|
|
28
|
+
actionString = Action.SearchEngineFlush;
|
|
29
|
+
introString = "search-engine:update --flush";
|
|
30
|
+
}
|
|
31
|
+
const perf = await intro(introString), result = await runAction(actionString, options);
|
|
32
|
+
if (result.isErr) {
|
|
33
|
+
await outro("While running the search-engine:update command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
34
|
+
process.exit(ExitCode.FatalError);
|
|
35
|
+
}
|
|
36
|
+
await outro("Successfully imported model data to search engine.", {
|
|
37
|
+
startTime: perf,
|
|
38
|
+
useSeconds: !0
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
buddy.command("search-engine:settings", descriptions.list).option("-m, --model [model]", descriptions.model, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
42
|
+
if (!options.model)
|
|
43
|
+
log.error("Missing required option --model");
|
|
44
|
+
log.debug("Running `search-engine:settings` ...", options);
|
|
45
|
+
const perf = await intro("search-engine:settings"), result = await runAction(Action.SearchEngineListSettings, options);
|
|
46
|
+
if (result.isErr) {
|
|
47
|
+
await outro("While running the search-engine:settings command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
48
|
+
process.exit(ExitCode.FatalError);
|
|
49
|
+
}
|
|
50
|
+
await outro("Successfully listed search engine index settings.", {
|
|
51
|
+
startTime: perf,
|
|
52
|
+
useSeconds: !0
|
|
53
|
+
});
|
|
54
|
+
process.exit(ExitCode.Success);
|
|
55
|
+
});
|
|
56
|
+
buddy.command("search:reindex [model]", "Re-index a model (Scout-style alias for search-engine:update)").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (model, options) => {
|
|
57
|
+
const opts = { ...options, model: model ?? "" };
|
|
58
|
+
log.debug("Running `search:reindex` ...", opts);
|
|
59
|
+
const perf = await intro(`search:reindex${model ? ` ${model}` : ""}`), result = await runAction(Action.SearchEngineImport, opts);
|
|
60
|
+
if (result.isErr) {
|
|
61
|
+
await outro("While running the search:reindex command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
62
|
+
process.exit(ExitCode.FatalError);
|
|
63
|
+
}
|
|
64
|
+
await outro(model ? `Reindexed ${model}.` : "Reindexed all searchable models.", {
|
|
65
|
+
startTime: perf,
|
|
66
|
+
useSeconds: !0
|
|
67
|
+
});
|
|
68
|
+
process.exit(ExitCode.Success);
|
|
69
|
+
});
|
|
70
|
+
buddy.command("search:flush [model]", "Delete all documents from a model index (Scout-style alias)").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (model, options) => {
|
|
71
|
+
const opts = { ...options, model: model ?? "", flush: !0 };
|
|
72
|
+
log.debug("Running `search:flush` ...", opts);
|
|
73
|
+
const perf = await intro(`search:flush${model ? ` ${model}` : ""}`), result = await runAction(Action.SearchEngineFlush, opts);
|
|
74
|
+
if (result.isErr) {
|
|
75
|
+
await outro("While running the search:flush command, there was an issue", { startTime: perf, useSeconds: !0 }, result.error);
|
|
76
|
+
process.exit(ExitCode.FatalError);
|
|
77
|
+
}
|
|
78
|
+
await outro(model ? `Flushed ${model} from search.` : "Flushed all model indices.", {
|
|
79
|
+
startTime: perf,
|
|
80
|
+
useSeconds: !0
|
|
81
|
+
});
|
|
82
|
+
process.exit(ExitCode.Success);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { intro, log, onUnknownSubcommand, outro } from "@stacksjs/cli";
|
|
3
|
+
import { ExitCode } from "@stacksjs/types";
|
|
4
|
+
export function seed(buddy) {
|
|
5
|
+
const descriptions = {
|
|
6
|
+
seed: "Seed your database",
|
|
7
|
+
project: "Target a specific project",
|
|
8
|
+
verbose: "Enable verbose output"
|
|
9
|
+
};
|
|
10
|
+
buddy.command("seed", descriptions.seed).alias("db:seed").option("-p, --project [project]", descriptions.project, { default: !1 }).option("-c, --class [class]", "Run a specific seeder class from database/seeders/", { default: "" }).option("--allow-protected", "Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)", { default: !1 }).option("--fresh", "Truncate tables before seeding", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
11
|
+
log.debug("Running `buddy seed` ...", options);
|
|
12
|
+
const perf = await intro("buddy seed");
|
|
13
|
+
if (options.class) {
|
|
14
|
+
const { injectGlobalAutoImports } = await import("@stacksjs/server");
|
|
15
|
+
await injectGlobalAutoImports();
|
|
16
|
+
const { runClassSeeders } = await import("@stacksjs/database"), result = await runClassSeeders({ class: options.class });
|
|
17
|
+
await outro(`Class seeders: ran=${result.ran.length}, skipped=${result.skipped.length}`, { startTime: perf, useSeconds: !0 });
|
|
18
|
+
process.exit(result.ran.length > 0 ? ExitCode.Success : ExitCode.FatalError);
|
|
19
|
+
}
|
|
20
|
+
const { injectGlobalAutoImports } = await import("@stacksjs/server");
|
|
21
|
+
await injectGlobalAutoImports();
|
|
22
|
+
const { runClassSeeders, listSeedableModels } = await import("@stacksjs/database"), { path } = await import("@stacksjs/path"), { fs } = await import("@stacksjs/storage"), seedableModels = await listSeedableModels(), seedersDir = path.projectPath("database/seeders"), existingSeederFiles = fs.existsSync(seedersDir) ? new Set(fs.readdirSync(seedersDir).filter((f) => f.endsWith(".ts"))) : new Set, unmigratedModels = seedableModels.filter((m) => !existingSeederFiles.has(`${m.name}Seeder.ts`));
|
|
23
|
+
if (unmigratedModels.length > 0)
|
|
24
|
+
log.warn(`[seed] ${unmigratedModels.length} model(s) declare the deprecated \`useSeeder\` trait but have no class seeder file: ${unmigratedModels.map((m) => m.name).join(", ")}. ` + "The auto-walker has been removed (stacksjs/stacks#1919) \u2014 these models will NOT seed. " + "Run `./buddy seed:scaffold` to codemod a class seeder per model and strip the trait (stacksjs/stacks#1929), then re-run `./buddy seed`.");
|
|
25
|
+
const classResult = await runClassSeeders(), APP_ENV = process.env.APP_ENV || "local", ran = classResult.ran.length;
|
|
26
|
+
await outro(`Seeded your ${APP_ENV} database. Class seeders: ran=${ran}, skipped=${classResult.skipped.length}.`, { startTime: perf, useSeconds: !0 });
|
|
27
|
+
process.exit(ExitCode.Success);
|
|
28
|
+
});
|
|
29
|
+
buddy.command("seed:scaffold", "Generate class seeders for every model with a useSeeder trait (codemod for stacksjs/stacks#1919)").option("--force", "Overwrite existing seeder files", { default: !1 }).option("--dry-run", "Print what would be generated without writing files", { default: !1 }).action(async (options) => {
|
|
30
|
+
const perf = await intro("buddy seed:scaffold");
|
|
31
|
+
try {
|
|
32
|
+
const { scaffoldClassSeedersFromModels } = await import("@stacksjs/database"), result = await scaffoldClassSeedersFromModels({
|
|
33
|
+
force: options.force,
|
|
34
|
+
dryRun: options.dryRun
|
|
35
|
+
}), generated = result.generated.length, alreadyThere = result.skipped.filter((s) => s.reason === "already-exists").length, stripped = result.strippedTrait.length, errors = result.errors.length;
|
|
36
|
+
for (const g of result.generated)
|
|
37
|
+
console.log(` + ${g.model} \u2192 ${g.file}`);
|
|
38
|
+
for (const s of result.skipped.filter((s) => s.reason === "already-exists"))
|
|
39
|
+
console.log(` \xB7 ${s.model}: seeder exists (pass --force to overwrite)`);
|
|
40
|
+
for (const t of result.strippedTrait)
|
|
41
|
+
console.log(` - ${t.model}: removed useSeeder trait from model`);
|
|
42
|
+
for (const t of result.traitStripSkipped)
|
|
43
|
+
log.warn(` ! ${t.model}: useSeeder value couldn't be auto-removed \u2014 strip it manually (${t.file})`);
|
|
44
|
+
for (const e of result.errors)
|
|
45
|
+
log.warn(` ! ${e.model}: ${e.error}`);
|
|
46
|
+
const verb = options.dryRun ? "would generate" : "generated", strippedVerb = options.dryRun ? "would strip" : "stripped";
|
|
47
|
+
await outro(`Seeder scaffold: ${verb} ${generated}, skipped ${alreadyThere} existing, ${strippedVerb} ${stripped} trait(s), ${errors} error(s).`, { startTime: perf, useSeconds: !0 });
|
|
48
|
+
process.exit(errors > 0 && generated === 0 ? ExitCode.FatalError : ExitCode.Success);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
await outro("seed:scaffold failed", { startTime: perf, useSeconds: !0 }, err);
|
|
51
|
+
process.exit(ExitCode.FatalError);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
buddy.command("seed:roles", "Seed default RBAC role packs (admin, dev, client)").alias("roles:seed").action(async () => {
|
|
55
|
+
const perf = await intro("buddy seed:roles");
|
|
56
|
+
try {
|
|
57
|
+
const { seedDefaultRoles } = await import("@stacksjs/auth"), result = await seedDefaultRoles();
|
|
58
|
+
if (result.created.length === 0 && result.skipped.length > 0)
|
|
59
|
+
await outro(`All ${result.skipped.length} default role packs already exist \u2014 nothing to do.`, { startTime: perf, useSeconds: !0 });
|
|
60
|
+
else {
|
|
61
|
+
const createdNames = result.created.map((r) => r.name).join(", ");
|
|
62
|
+
await outro(`Created ${result.created.length} role pack(s): ${createdNames}. Skipped ${result.skipped.length} existing.`, { startTime: perf, useSeconds: !0 });
|
|
63
|
+
}
|
|
64
|
+
process.exit(ExitCode.Success);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
await outro("Failed to seed default roles. Most often: migrations haven't run yet (try `./buddy migrate` first).", { startTime: perf, useSeconds: !0 }, err);
|
|
67
|
+
process.exit(ExitCode.FatalError);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
onUnknownSubcommand(buddy, "seed");
|
|
71
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { log } from "@stacksjs/cli";
|
|
6
|
+
globalThis.requestContext = {
|
|
7
|
+
cookie(name) {
|
|
8
|
+
return globalThis.__stxServeCookies?.[name] ?? null;
|
|
9
|
+
},
|
|
10
|
+
url() {
|
|
11
|
+
return globalThis.__stxServeSearch ?? "";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function parseCookies(req) {
|
|
15
|
+
const out = {}, header = req.headers.get("cookie") || "";
|
|
16
|
+
if (!header)
|
|
17
|
+
return out;
|
|
18
|
+
for (const part of header.split(";")) {
|
|
19
|
+
const trimmed = part.trim(), eq = trimmed.indexOf("=");
|
|
20
|
+
if (eq === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const k = trimmed.slice(0, eq).trim(), v = trimmed.slice(eq + 1).trim();
|
|
23
|
+
if (!k)
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
out[k] = decodeURIComponent(v);
|
|
27
|
+
} catch {
|
|
28
|
+
out[k] = v;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function serve(buddy) {
|
|
34
|
+
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(async (options) => {
|
|
35
|
+
if (options?.port)
|
|
36
|
+
process.env.PORT = String(options.port);
|
|
37
|
+
process.env.APP_ENV = process.env.APP_ENV || "production";
|
|
38
|
+
const port = Number(process.env.PORT) || 3000, { config, overridesReady } = await import("@stacksjs/config");
|
|
39
|
+
await overridesReady;
|
|
40
|
+
const { injectGlobalAutoImports } = await import("@stacksjs/server");
|
|
41
|
+
await injectGlobalAutoImports();
|
|
42
|
+
let stxServe;
|
|
43
|
+
const serveCandidates = [
|
|
44
|
+
join(homedir(), "Code/Tools/stx/packages/bun-plugin/dist/serve.js"),
|
|
45
|
+
join(process.cwd(), "pantry/bun-plugin-stx/dist/serve.js")
|
|
46
|
+
];
|
|
47
|
+
for (const entry of serveCandidates)
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(entry)) {
|
|
50
|
+
({ serve: stxServe } = await import(entry));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
({ serve: stxServe } = await import("bun-plugin-stx/serve"));
|
|
55
|
+
const stxModule = await resolveVendoredStxModule(), { site: siteConfig, i18n: i18nConfig } = await loadStxSiteConfig(), userViewsPath = "resources/views", defaultsResources = resolveDefaultsResources(), defaultViewsPath = join(defaultsResources, "views"), userLayoutsPath = existsSync("resources/views/layouts") ? "resources/views/layouts" : "resources/layouts", userComponentsPath = existsSync("resources/views/components") ? "resources/views/components" : "resources/components", apiBase = process.env.API_URL || `http://127.0.0.1:${Number(process.env.PORT_API) || config.ports?.api || 3008}`;
|
|
56
|
+
log.info(`Starting production server on port ${port}...`);
|
|
57
|
+
await stxServe({
|
|
58
|
+
patterns: [userViewsPath, defaultViewsPath],
|
|
59
|
+
port,
|
|
60
|
+
autoIncrementPort: !1,
|
|
61
|
+
reusePort: ["production", "staging", "development"].includes((process.env.APP_ENV || "").toLowerCase()),
|
|
62
|
+
componentsDir: join(defaultsResources, "components"),
|
|
63
|
+
layoutsDir: userLayoutsPath,
|
|
64
|
+
partialsDir: userComponentsPath,
|
|
65
|
+
fallbackLayoutsDir: join(defaultsResources, "layouts"),
|
|
66
|
+
fallbackPartialsDir: defaultViewsPath,
|
|
67
|
+
quiet: options?.verbose !== !0,
|
|
68
|
+
...stxModule && { stxModule },
|
|
69
|
+
...i18nConfig && { i18n: i18nConfig },
|
|
70
|
+
...siteConfig?.url && { site: siteConfig },
|
|
71
|
+
onRequest: async (req) => {
|
|
72
|
+
const { maintenanceGate, isApiBoundRequest, proxyToBackend } = await import("@stacksjs/server"), gated = await maintenanceGate(req);
|
|
73
|
+
if (gated)
|
|
74
|
+
return gated;
|
|
75
|
+
const url = new URL(req.url);
|
|
76
|
+
if (isApiBoundRequest(req, url.pathname))
|
|
77
|
+
try {
|
|
78
|
+
return await proxyToBackend(req, apiBase);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
log.error(`API proxy to ${apiBase} failed: ${error.message}`);
|
|
81
|
+
return new Response("Bad Gateway", { status: 502 });
|
|
82
|
+
}
|
|
83
|
+
globalThis.__stxServeSearch = url.search;
|
|
84
|
+
globalThis.__stxServeCookies = parseCookies(req);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
log.success(`Production server listening on http://0.0.0.0:${port}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function serveApi(buddy) {
|
|
92
|
+
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) => {
|
|
93
|
+
if (options?.port)
|
|
94
|
+
process.env.PORT = String(options.port);
|
|
95
|
+
process.env.APP_ENV = process.env.APP_ENV || "production";
|
|
96
|
+
await import("@stacksjs/actions/serve/api");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function resolveDefaultsResources() {
|
|
100
|
+
const vendored = "storage/framework/defaults/resources";
|
|
101
|
+
if (existsSync(vendored))
|
|
102
|
+
return vendored;
|
|
103
|
+
try {
|
|
104
|
+
const pkgJson = Bun.resolveSync("@stacksjs/defaults/package.json", process.cwd());
|
|
105
|
+
return join(dirname(pkgJson), "resources");
|
|
106
|
+
} catch {
|
|
107
|
+
return vendored;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function resolveVendoredStxModule() {
|
|
111
|
+
const candidates = [
|
|
112
|
+
join(homedir(), "Code/Tools/stx/packages/stx/dist/index.js"),
|
|
113
|
+
join(process.cwd(), "pantry/@stacksjs/stx/dist/index.js")
|
|
114
|
+
];
|
|
115
|
+
for (const entry of candidates)
|
|
116
|
+
try {
|
|
117
|
+
if (existsSync(entry))
|
|
118
|
+
return await import(entry);
|
|
119
|
+
} catch {}
|
|
120
|
+
try {
|
|
121
|
+
return await import("@stacksjs/stx");
|
|
122
|
+
} catch {}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
function fallbackI18nFromSite(site) {
|
|
126
|
+
const locales = site.i18n.locales, defaultLocale = site.i18n.defaultLocale ?? locales[0];
|
|
127
|
+
return {
|
|
128
|
+
locales,
|
|
129
|
+
defaultLocale,
|
|
130
|
+
labels: site.i18n.labels ?? Object.fromEntries(locales.map((c) => [c, c.toUpperCase()])),
|
|
131
|
+
translations: {},
|
|
132
|
+
pickerSelector: site.i18n.pickerSelector ?? "#lang-picker"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function resolveSiteI18n(site) {
|
|
136
|
+
const resolverPaths = [
|
|
137
|
+
join(homedir(), "Code/Tools/stx/packages/stx/src/site-builder/i18n.ts"),
|
|
138
|
+
join(homedir(), "Code/Tools/stx/packages/stx/dist/index.js"),
|
|
139
|
+
join(process.cwd(), "pantry/@stacksjs/stx/dist/index.js")
|
|
140
|
+
];
|
|
141
|
+
for (const resolverPath of resolverPaths)
|
|
142
|
+
try {
|
|
143
|
+
if (!existsSync(resolverPath))
|
|
144
|
+
continue;
|
|
145
|
+
const resolved = await import(resolverPath);
|
|
146
|
+
if (typeof resolved.resolveI18n !== "function")
|
|
147
|
+
continue;
|
|
148
|
+
const i18n = resolved.resolveI18n(site, process.cwd());
|
|
149
|
+
if (i18n)
|
|
150
|
+
return i18n;
|
|
151
|
+
} catch {}
|
|
152
|
+
try {
|
|
153
|
+
const resolved = await import("@stacksjs/stx");
|
|
154
|
+
if (typeof resolved.resolveI18n === "function") {
|
|
155
|
+
const i18n = resolved.resolveI18n(site, process.cwd());
|
|
156
|
+
if (i18n)
|
|
157
|
+
return i18n;
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
return fallbackI18nFromSite(site);
|
|
161
|
+
}
|
|
162
|
+
async function loadStxSiteConfig() {
|
|
163
|
+
const sitePath = join(process.cwd(), "site.config.ts");
|
|
164
|
+
if (!existsSync(sitePath))
|
|
165
|
+
return {};
|
|
166
|
+
try {
|
|
167
|
+
const site = (await import(sitePath)).default;
|
|
168
|
+
if (!site)
|
|
169
|
+
return {};
|
|
170
|
+
if (!site.i18n)
|
|
171
|
+
return { site };
|
|
172
|
+
const i18n = await resolveSiteI18n(site);
|
|
173
|
+
return { site, i18n };
|
|
174
|
+
} catch {}
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { runAction, setupSSL } from "@stacksjs/actions";
|
|
5
|
+
import { log, onUnknownSubcommand, runCommand } from "@stacksjs/cli";
|
|
6
|
+
import { Action } from "@stacksjs/enums";
|
|
7
|
+
import { handleError } from "@stacksjs/error-handling";
|
|
8
|
+
import { path as p } from "@stacksjs/path";
|
|
9
|
+
import { copyFile, storage } from "@stacksjs/storage";
|
|
10
|
+
import { ExitCode } from "@stacksjs/types";
|
|
11
|
+
function getTimeoutMs(envVar, fallbackMs) {
|
|
12
|
+
const value = Number(process.env[envVar]);
|
|
13
|
+
if (Number.isFinite(value) && value > 0)
|
|
14
|
+
return value;
|
|
15
|
+
return fallbackMs;
|
|
16
|
+
}
|
|
17
|
+
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);
|
|
18
|
+
export function setup(buddy) {
|
|
19
|
+
const descriptions = {
|
|
20
|
+
setup: "This command ensures your project is setup correctly",
|
|
21
|
+
ssl: "Setup SSL certificates and hosts file for HTTPS development",
|
|
22
|
+
ohMyZsh: "Enable Oh My Zsh",
|
|
23
|
+
aws: "Ensures AWS is connected to the project",
|
|
24
|
+
project: "Target a specific project",
|
|
25
|
+
verbose: "Enable verbose output",
|
|
26
|
+
domain: "Custom domain to setup (defaults to APP_URL)",
|
|
27
|
+
skipHosts: "Skip adding domain to hosts file",
|
|
28
|
+
skipTrust: "Skip trusting the certificate",
|
|
29
|
+
skipAws: "Skip AWS configuration during setup",
|
|
30
|
+
skipKeygen: "Skip generating an application key during setup"
|
|
31
|
+
};
|
|
32
|
+
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) => {
|
|
33
|
+
log.debug("Running `buddy setup` ...", options);
|
|
34
|
+
await ensurePantryInstalled();
|
|
35
|
+
await optimizePantryDeps();
|
|
36
|
+
await initializeProject(options);
|
|
37
|
+
});
|
|
38
|
+
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) => {
|
|
39
|
+
log.debug("Running `buddy setup:ssl` ...", options);
|
|
40
|
+
if (!await setupSSL({
|
|
41
|
+
domain: options.domain,
|
|
42
|
+
skipHosts: options.skipHosts,
|
|
43
|
+
skipTrust: options.skipTrust,
|
|
44
|
+
verbose: options.verbose
|
|
45
|
+
})) {
|
|
46
|
+
log.warn("SSL setup completed with warnings");
|
|
47
|
+
log.info("You may need to manually trust certificates or update hosts file");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
buddy.command("setup:oh-my-zsh", descriptions.ohMyZsh).alias("upgrade:oh-my-zsh").option("--verbose", descriptions.verbose, { default: !1 }).action(async (_options) => {
|
|
51
|
+
log.debug("Running `buddy setup:oh-my-zsh` ...", _options);
|
|
52
|
+
const result = await runAction(Action.UpgradeShell);
|
|
53
|
+
if (result.isErr) {
|
|
54
|
+
log.error(result.error);
|
|
55
|
+
process.exit(ExitCode.FatalError);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
onUnknownSubcommand(buddy, "setup");
|
|
59
|
+
}
|
|
60
|
+
async function isPantryInstalled() {
|
|
61
|
+
if ((await runCommand("pantry --version", {
|
|
62
|
+
silent: !0,
|
|
63
|
+
timeoutMs: PANTRY_CHECK_TIMEOUT_MS
|
|
64
|
+
})).isOk)
|
|
65
|
+
return !0;
|
|
66
|
+
return !1;
|
|
67
|
+
}
|
|
68
|
+
async function installPantry() {
|
|
69
|
+
const result = await runCommand(p.frameworkPath("scripts/pantry-install"), {
|
|
70
|
+
timeoutMs: PANTRY_INSTALL_TIMEOUT_MS
|
|
71
|
+
});
|
|
72
|
+
if (result.isOk)
|
|
73
|
+
return;
|
|
74
|
+
handleError(result.error);
|
|
75
|
+
process.exit(ExitCode.FatalError);
|
|
76
|
+
}
|
|
77
|
+
export async function ensurePantryInstalled() {
|
|
78
|
+
if (!await isPantryInstalled())
|
|
79
|
+
await installPantry();
|
|
80
|
+
}
|
|
81
|
+
export async function ensurePantryDependencies(cwd) {
|
|
82
|
+
log.info("Installing Pantry dependencies...");
|
|
83
|
+
const result = await runCommand("pantry install", {
|
|
84
|
+
cwd,
|
|
85
|
+
timeoutMs: PANTRY_DEPENDENCIES_TIMEOUT_MS
|
|
86
|
+
});
|
|
87
|
+
if (result.isOk) {
|
|
88
|
+
log.success("Installed Pantry dependencies");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
handleError(result.error);
|
|
92
|
+
process.exit(ExitCode.FatalError);
|
|
93
|
+
}
|
|
94
|
+
function hasAppKey(cwd) {
|
|
95
|
+
const envPath = join(cwd, ".env");
|
|
96
|
+
if (!existsSync(envPath))
|
|
97
|
+
return !1;
|
|
98
|
+
return /^APP_KEY=.+$/m.test(readFileSync(envPath, "utf-8"));
|
|
99
|
+
}
|
|
100
|
+
export async function ensureAppKey(cwd) {
|
|
101
|
+
if (hasAppKey(cwd)) {
|
|
102
|
+
log.success("APP_KEY existed");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const keyResult = await runCommand("./buddy key:generate", {
|
|
106
|
+
cwd,
|
|
107
|
+
timeoutMs: KEYGEN_TIMEOUT_MS
|
|
108
|
+
});
|
|
109
|
+
if (keyResult.isErr) {
|
|
110
|
+
handleError(keyResult.error);
|
|
111
|
+
process.exit(ExitCode.FatalError);
|
|
112
|
+
}
|
|
113
|
+
log.success("Generated application key");
|
|
114
|
+
}
|
|
115
|
+
async function initializeProject(options) {
|
|
116
|
+
const cwd = options.cwd || p.projectPath();
|
|
117
|
+
await ensurePantryDependencies(cwd);
|
|
118
|
+
await ensureEnvIsSet(options);
|
|
119
|
+
if (!options.skipKeygen)
|
|
120
|
+
await ensureAppKey(cwd);
|
|
121
|
+
if (!options.skipAws) {
|
|
122
|
+
log.info("Ensuring AWS is connected...");
|
|
123
|
+
const awsResult = await runCommand("./buddy configure:aws", {
|
|
124
|
+
cwd,
|
|
125
|
+
timeoutMs: AWS_CONFIG_TIMEOUT_MS
|
|
126
|
+
});
|
|
127
|
+
if (awsResult.isErr) {
|
|
128
|
+
handleError(awsResult.error);
|
|
129
|
+
process.exit(ExitCode.FatalError);
|
|
130
|
+
}
|
|
131
|
+
log.success("Configured AWS");
|
|
132
|
+
}
|
|
133
|
+
log.success("Project is setup");
|
|
134
|
+
log.info("Happy coding! \uD83D\uDC99");
|
|
135
|
+
}
|
|
136
|
+
const DB_CONNECTION_PACKAGES = {
|
|
137
|
+
postgres: "postgresql.org",
|
|
138
|
+
mysql: "mysql.com",
|
|
139
|
+
sqlite: "sqlite.org"
|
|
140
|
+
};
|
|
141
|
+
function detectDbPackage(cwd) {
|
|
142
|
+
const envPath = join(cwd, ".env"), envExamplePath = join(cwd, ".env.example"), filePath = existsSync(envPath) ? envPath : existsSync(envExamplePath) ? envExamplePath : void 0;
|
|
143
|
+
if (!filePath)
|
|
144
|
+
return;
|
|
145
|
+
const match = readFileSync(filePath, "utf-8").match(/^DB_CONNECTION=(.+)$/m);
|
|
146
|
+
if (!match)
|
|
147
|
+
return;
|
|
148
|
+
const value = match[1].trim().replace(/['"]/g, "");
|
|
149
|
+
return DB_CONNECTION_PACKAGES[value];
|
|
150
|
+
}
|
|
151
|
+
export async function optimizePantryDeps() {
|
|
152
|
+
const cwd = p.projectPath(), depsConfigPath = join(cwd, "config", "deps.ts");
|
|
153
|
+
if (!existsSync(depsConfigPath)) {
|
|
154
|
+
log.debug("No config/deps.ts found, skipping dependency optimization");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let configDeps = {};
|
|
158
|
+
try {
|
|
159
|
+
const mod = await import(depsConfigPath), config = mod.config || mod.default;
|
|
160
|
+
if (config?.dependencies)
|
|
161
|
+
configDeps = { ...config.dependencies };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.debug("Could not load config/deps.ts, skipping dependency optimization");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const dbPackage = detectDbPackage(cwd);
|
|
167
|
+
if (dbPackage) {
|
|
168
|
+
if (!Object.keys(configDeps).some((key) => key === dbPackage || key.startsWith(`${dbPackage}/`))) {
|
|
169
|
+
log.info(`Detected DB_CONNECTION requires ${dbPackage}, adding to dependencies`);
|
|
170
|
+
configDeps[dbPackage] = "*";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const lines = [
|
|
174
|
+
"# Auto-generated from config/deps.ts and .env sniffing.",
|
|
175
|
+
"# This file is regenerated on each `buddy setup` run.",
|
|
176
|
+
"#",
|
|
177
|
+
"# To learn more, please visit:",
|
|
178
|
+
"# https://stacksjs.com/docs/dependency-management",
|
|
179
|
+
"",
|
|
180
|
+
"dependencies:"
|
|
181
|
+
];
|
|
182
|
+
for (const [pkg, version] of Object.entries(configDeps))
|
|
183
|
+
lines.push(` ${pkg}: ${version}`);
|
|
184
|
+
const depsYamlPath = join(cwd, "deps.yaml");
|
|
185
|
+
writeFileSync(depsYamlPath, `${lines.join(`
|
|
186
|
+
`)}
|
|
187
|
+
`);
|
|
188
|
+
log.success("Generated deps.yaml from config/deps.ts");
|
|
189
|
+
}
|
|
190
|
+
export async function ensureEnvIsSet(options) {
|
|
191
|
+
log.info("Ensuring .env exists...");
|
|
192
|
+
const cwd = options.cwd || p.projectPath(), envPath = `${cwd}/.env`, envExamplePath = `${cwd}/.env.example`;
|
|
193
|
+
if (storage.doesNotExist(envPath)) {
|
|
194
|
+
try {
|
|
195
|
+
copyFile(envExamplePath, envPath);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
handleError(error);
|
|
198
|
+
process.exit(ExitCode.FatalError);
|
|
199
|
+
}
|
|
200
|
+
log.success(".env created");
|
|
201
|
+
} else
|
|
202
|
+
log.success(".env existed");
|
|
203
|
+
}
|