@stacksjs/buddy 0.70.113 → 0.70.115
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 +23 -2
- package/dist/commands/add.js +1 -1
- package/dist/commands/build.js +58 -11
- package/dist/commands/clean.js +9 -2
- package/dist/commands/cloud.js +27 -11
- package/dist/commands/commit.js +1 -1
- package/dist/commands/create.js +20 -10
- package/dist/commands/deploy.js +31 -6
- package/dist/commands/dev.js +17 -20
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +77 -19
- package/dist/commands/extension.js +38 -2
- package/dist/commands/fresh.js +9 -2
- package/dist/commands/make.js +48 -31
- package/dist/commands/migrate.js +2 -1
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +56 -10
- package/dist/commands/types.js +1 -1
- package/dist/lazy-commands.js +15 -5
- package/package.json +44 -43
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { cli, log } from "@stacksjs/cli";
|
|
4
4
|
import { path as p } from "@stacksjs/path";
|
|
5
|
+
try {
|
|
6
|
+
const { isSupportedBunVersion, minimumBunVersion } = await import("@stacksjs/utils"), currentBunVersion = typeof Bun < "u" ? Bun.version : process.versions.bun;
|
|
7
|
+
if (currentBunVersion && !isSupportedBunVersion(currentBunVersion)) {
|
|
8
|
+
console.error(`[buddy] Bun v${minimumBunVersion} or later is required (current: v${currentBunVersion}). Run: bun upgrade`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
} catch {}
|
|
5
12
|
const args = process.argv.slice(2), requestedCommand = args[0] || "help", isHelpFlag = args.includes("--help") || args.includes("-h"), isVersionOnly = ["--version", "-v", "version"].includes(requestedCommand), isHelpMode = requestedCommand === "help" || isHelpFlag && args.length <= 2, skipAppKeyCheck = [
|
|
6
13
|
"build",
|
|
7
14
|
"lint",
|
|
@@ -79,7 +86,7 @@ async function main() {
|
|
|
79
86
|
if (args.length === 0 && process.stdin.isTTY && !buddy.isNoInteraction)
|
|
80
87
|
await showInteractiveMenu(buddy);
|
|
81
88
|
else
|
|
82
|
-
buddy
|
|
89
|
+
await parseOrExit(buddy);
|
|
83
90
|
}
|
|
84
91
|
async function showInteractiveMenu(buddy) {
|
|
85
92
|
const { bold, green, intro } = await import("@stacksjs/cli"), { select } = await import("@stacksjs/cli");
|
|
@@ -107,7 +114,21 @@ async function showInteractiveMenu(buddy) {
|
|
|
107
114
|
buddy.outputHelp();
|
|
108
115
|
else {
|
|
109
116
|
process.argv = ["bun", "buddy", choice];
|
|
110
|
-
buddy
|
|
117
|
+
await parseOrExit(buddy);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function parseOrExit(buddy) {
|
|
121
|
+
try {
|
|
122
|
+
await buddy.parse();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!(error?.name === "ClappError" || error?.isUsageError === !0))
|
|
125
|
+
throw error;
|
|
126
|
+
process.stderr.write(`${error.message}
|
|
127
|
+
`);
|
|
128
|
+
if (error.usage)
|
|
129
|
+
process.stderr.write(`${error.usage}
|
|
130
|
+
`);
|
|
131
|
+
process.exit(typeof error.exitCode === "number" ? error.exitCode : 2);
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
await main();
|
package/dist/commands/add.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ExitCode } from "@stacksjs/types";
|
|
|
5
5
|
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
6
6
|
export function add(buddy) {
|
|
7
7
|
const descriptions = {
|
|
8
|
-
add: "Add a stack to your project (coming soon)",
|
|
8
|
+
add: "Add a stack to your project (table and calendar available, more coming soon)",
|
|
9
9
|
table: "Add the Table Stack to your project",
|
|
10
10
|
calendar: "Add the Calendar Stack to your project",
|
|
11
11
|
all: "Add all stacks",
|
package/dist/commands/build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { intro, log, onUnknownSubcommand, outro } from "@stacksjs/cli";
|
|
2
|
+
import { intro, log, multiselect, onUnknownSubcommand, outro } from "@stacksjs/cli";
|
|
3
3
|
import { Action } from "@stacksjs/enums";
|
|
4
|
+
import { hasTTY, isCI } from "@stacksjs/env";
|
|
4
5
|
import { ExitCode } from "@stacksjs/types";
|
|
5
6
|
let _runAction;
|
|
6
7
|
async function runAction(...args) {
|
|
@@ -60,24 +61,62 @@ export function build(buddy) {
|
|
|
60
61
|
default:
|
|
61
62
|
break;
|
|
62
63
|
}
|
|
63
|
-
if (hasNoOptions(options))
|
|
64
|
-
|
|
64
|
+
if (hasNoOptions(options)) {
|
|
65
|
+
if (!isCI && hasTTY && process.stdin.isTTY) {
|
|
66
|
+
const answers = await multiselect({
|
|
67
|
+
message: descriptions.select,
|
|
68
|
+
choices: [
|
|
69
|
+
{ label: "Frontend (views)", value: "views" },
|
|
70
|
+
{ label: "Components", value: "components" },
|
|
71
|
+
{ label: "Web Components", value: "webComponents" },
|
|
72
|
+
{ label: "Functions", value: "functions" },
|
|
73
|
+
{ label: "Documentation", value: "docs" },
|
|
74
|
+
{ label: "Stacks framework", value: "stacks" },
|
|
75
|
+
{ label: "Buddy CLI", value: "buddy" },
|
|
76
|
+
{ label: "Server (Docker image)", value: "server" }
|
|
77
|
+
]
|
|
78
|
+
}), selected = new Set(answers);
|
|
79
|
+
if (selected.has("views"))
|
|
80
|
+
options.views = !0;
|
|
81
|
+
if (selected.has("components"))
|
|
82
|
+
options.components = !0;
|
|
83
|
+
if (selected.has("webComponents"))
|
|
84
|
+
options.webComponents = !0;
|
|
85
|
+
if (selected.has("functions"))
|
|
86
|
+
options.functions = !0;
|
|
87
|
+
if (selected.has("docs"))
|
|
88
|
+
options.docs = !0;
|
|
89
|
+
if (selected.has("stacks"))
|
|
90
|
+
options.stacks = !0;
|
|
91
|
+
if (selected.has("buddy"))
|
|
92
|
+
options.buddy = !0;
|
|
93
|
+
if (selected.has("server"))
|
|
94
|
+
options.server = !0;
|
|
95
|
+
}
|
|
96
|
+
if (hasNoOptions(options)) {
|
|
97
|
+
options.views = !0;
|
|
98
|
+
log.info("No build target specified, defaulting to the frontend (views). See `buddy build --help` for all targets.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let succeeded = !0;
|
|
65
102
|
if (options.docs)
|
|
66
|
-
await
|
|
103
|
+
succeeded = await runBuildAction(Action.BuildDocs, "documentation") && succeeded;
|
|
67
104
|
if (options.components)
|
|
68
|
-
await
|
|
105
|
+
succeeded = await runBuildAction(Action.BuildComponentLibs, "component libraries") && succeeded;
|
|
69
106
|
if (options.webComponents)
|
|
70
|
-
await
|
|
107
|
+
succeeded = await runBuildAction(Action.BuildWebComponentLib, "web component library") && succeeded;
|
|
71
108
|
if (options.functions)
|
|
72
|
-
await
|
|
109
|
+
succeeded = await runBuildAction(Action.BuildFunctionLib, "function library") && succeeded;
|
|
73
110
|
if (options.views)
|
|
74
|
-
await
|
|
111
|
+
succeeded = await runBuildAction(Action.BuildViews, "frontend") && succeeded;
|
|
75
112
|
if (options.stacks)
|
|
76
|
-
await
|
|
113
|
+
succeeded = await runBuildAction(Action.BuildStacks, "Stacks framework") && succeeded;
|
|
77
114
|
if (options.buddy)
|
|
78
|
-
await
|
|
115
|
+
succeeded = await runBuildAction(Action.BuildCli, "Buddy CLI") && succeeded;
|
|
79
116
|
if (options.server)
|
|
80
|
-
await
|
|
117
|
+
succeeded = await runBuildAction(Action.BuildServer, "server") && succeeded;
|
|
118
|
+
if (!succeeded)
|
|
119
|
+
process.exit(ExitCode.FatalError);
|
|
81
120
|
process.exit(ExitCode.Success);
|
|
82
121
|
});
|
|
83
122
|
buddy.command("build:components", "Automagically build component libraries for production use & npm/CDN distribution").alias("prod:components").option("-c, --components", descriptions.components, { default: !0 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
@@ -151,3 +190,11 @@ export function build(buddy) {
|
|
|
151
190
|
function hasNoOptions(options) {
|
|
152
191
|
return !options.components && !options.webComponents && !options.elements && !options.functions && !options.views && !options.docs && !options.stacks && !options.buddy && !options.server;
|
|
153
192
|
}
|
|
193
|
+
async function runBuildAction(action, target) {
|
|
194
|
+
const result = await runAction(action);
|
|
195
|
+
if (result.isErr) {
|
|
196
|
+
log.error(`Failed to build ${target}.`, result.error);
|
|
197
|
+
return !1;
|
|
198
|
+
}
|
|
199
|
+
return !0;
|
|
200
|
+
}
|
package/dist/commands/clean.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { runAction } from "@stacksjs/actions";
|
|
3
3
|
import { intro, log, onUnknownSubcommand, outro } from "@stacksjs/cli";
|
|
4
|
+
import { hasTTY, isCI } from "@stacksjs/env";
|
|
4
5
|
import { Action } from "@stacksjs/enums";
|
|
5
6
|
import { ExitCode } from "@stacksjs/types";
|
|
6
7
|
export function clean(buddy) {
|
|
7
8
|
const descriptions = {
|
|
8
9
|
clean: "Removes all node_modules & lock files",
|
|
9
10
|
project: "Target a specific project",
|
|
11
|
+
force: "Skip the confirmation prompt (required in CI/non-interactive shells)",
|
|
10
12
|
verbose: "Enable verbose output"
|
|
11
13
|
};
|
|
12
|
-
buddy.command("clean", descriptions.clean).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
14
|
+
buddy.command("clean", descriptions.clean).option("-p, --project [project]", descriptions.project, { default: !1 }).option("-f, --force", descriptions.force, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
13
15
|
log.debug("Running `buddy clean` ...", options);
|
|
14
|
-
|
|
16
|
+
const skipConfirm = options.force === !0 || Boolean(buddy.isForce) || Boolean(buddy.isNoInteraction);
|
|
17
|
+
if (!skipConfirm && (isCI || !hasTTY || !process.stdin.isTTY)) {
|
|
18
|
+
log.syncError("Refusing to run `buddy clean` from a non-interactive shell without confirmation.");
|
|
19
|
+
log.fatal(" \u27A1\uFE0F Re-run with `--force` to proceed: `buddy clean --force`");
|
|
20
|
+
}
|
|
21
|
+
if (!skipConfirm) {
|
|
15
22
|
const { confirm } = await import("@stacksjs/cli");
|
|
16
23
|
if (!await confirm({
|
|
17
24
|
message: "This will remove all node_modules and lock files. Continue?",
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { intro, italic, log, onUnknownSubcommand, outro, prompts, runCommand, underline } from "@stacksjs/cli";
|
|
2
|
+
import { intro, italic, log, onUnknownSubcommand, outro, prompts, runCommand, text, underline } from "@stacksjs/cli";
|
|
3
3
|
import {
|
|
4
4
|
addJumpBox,
|
|
5
5
|
deleteCdkRemnants,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getCloudFrontDistributionId,
|
|
15
15
|
getJumpBoxInstanceId
|
|
16
16
|
} from "@stacksjs/cloud";
|
|
17
|
+
import { hasTTY, isCI } from "@stacksjs/env";
|
|
17
18
|
import { path as p } from "@stacksjs/path";
|
|
18
19
|
import { ExitCode } from "@stacksjs/types";
|
|
19
20
|
async function createTemporaryCdkRole(roleName) {
|
|
@@ -260,16 +261,25 @@ export function cloud(buddy) {
|
|
|
260
261
|
});
|
|
261
262
|
buddy.command("cloud:remove", descriptions.remove).alias("cloud:destroy").alias("cloud:rm").alias("undeploy").option("--jump-box", "Remove the jump-box", { default: !1 }).option("--force", "Force deletion of stack in bad state", { default: !1 }).option("--yes", "Skip confirmation prompts", { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
262
263
|
log.debug("Running `buddy cloud:remove` ...", options);
|
|
263
|
-
const startTime = await intro("buddy cloud:remove");
|
|
264
|
+
const startTime = await intro("buddy cloud:remove"), environment = process.env.APP_ENV || process.env.NODE_ENV || "production";
|
|
265
|
+
if (!options.yes && (isCI || !hasTTY || !process.stdin.isTTY)) {
|
|
266
|
+
log.syncError(`Refusing to remove the "${environment}" cloud infrastructure from a non-interactive shell without confirmation.`);
|
|
267
|
+
log.syncError(" \u27A1\uFE0F Re-run with `--yes` to confirm (e.g. in CI): `buddy cloud:remove --yes`");
|
|
268
|
+
await outro("cloud:remove cancelled.", { startTime, useSeconds: !0 });
|
|
269
|
+
await log.flush();
|
|
270
|
+
process.exit(ExitCode.FatalError);
|
|
271
|
+
}
|
|
264
272
|
if (options.jumpBox) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
if (!options.yes) {
|
|
274
|
+
const { confirm } = await prompts({
|
|
275
|
+
name: "confirm",
|
|
276
|
+
type: "confirm",
|
|
277
|
+
message: "Would you like to remove your jump-box for now?"
|
|
278
|
+
});
|
|
279
|
+
if (!confirm) {
|
|
280
|
+
await outro("Exited", { startTime, useSeconds: !0 });
|
|
281
|
+
process.exit(ExitCode.Success);
|
|
282
|
+
}
|
|
273
283
|
}
|
|
274
284
|
const result = await deleteJumpBox();
|
|
275
285
|
if (isResultError(result)) {
|
|
@@ -282,11 +292,17 @@ export function cloud(buddy) {
|
|
|
282
292
|
});
|
|
283
293
|
process.exit(ExitCode.Success);
|
|
284
294
|
}
|
|
295
|
+
if (!options.yes) {
|
|
296
|
+
log.warning(`This will permanently delete the "${environment}" cloud infrastructure (compute, storage, CDN, and DNS managed by Stacks). This cannot be undone.`);
|
|
297
|
+
if ((await text({ message: `Type the environment name "${environment}" to confirm (blank to cancel):` })).trim() !== environment) {
|
|
298
|
+
await outro("cloud:remove cancelled - confirmation did not match.", { startTime, useSeconds: !0 });
|
|
299
|
+
process.exit(ExitCode.Success);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
285
302
|
console.log("");
|
|
286
303
|
console.log("Removing cloud infrastructure...");
|
|
287
304
|
console.log(` ${italic("This typically takes 2-5 minutes.")}`);
|
|
288
305
|
console.log("");
|
|
289
|
-
const environment = process.env.APP_ENV || process.env.NODE_ENV || "production";
|
|
290
306
|
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
|
291
307
|
const { existsSync, readFileSync } = await import("node:fs"), { projectPath } = await import("@stacksjs/path"), envFiles = [
|
|
292
308
|
projectPath(`.env.${environment}`),
|
package/dist/commands/commit.js
CHANGED
|
@@ -4,7 +4,7 @@ import { log } from "@stacksjs/logging";
|
|
|
4
4
|
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
5
5
|
export function commit(buddy) {
|
|
6
6
|
const descriptions = {
|
|
7
|
-
commit: "Commit your
|
|
7
|
+
commit: "Commit your staged changes",
|
|
8
8
|
project: "Target a specific project",
|
|
9
9
|
verbose: "Enable verbose output"
|
|
10
10
|
};
|
package/dist/commands/create.js
CHANGED
|
@@ -6,7 +6,6 @@ import { Action } from "@stacksjs/enums";
|
|
|
6
6
|
import { resolve } from "@stacksjs/path";
|
|
7
7
|
import { isFolder } from "@stacksjs/storage";
|
|
8
8
|
import { ExitCode } from "@stacksjs/types";
|
|
9
|
-
import { useOnline } from "@stacksjs/utils";
|
|
10
9
|
import { uninstallAllFeatures } from "./features";
|
|
11
10
|
import { ensurePantryDependencies, ensurePantryInstalled } from "./setup";
|
|
12
11
|
export function create(buddy) {
|
|
@@ -28,13 +27,13 @@ export function create(buddy) {
|
|
|
28
27
|
minimal: "Skip optional feature bundles (cms, commerce, dashboard, marketing, monitoring, realtime, queue) \u2014 bare-bones API/SPA starter that can re-add them later via `./buddy <feature>:install`.",
|
|
29
28
|
verbose: "Enable verbose output"
|
|
30
29
|
};
|
|
31
|
-
buddy.command("new [name]", descriptions.command).alias("create [name]").option("-n, --name [name]", descriptions.name, { default: !1 }).option("-u, --ui", descriptions.ui, { default: !0 }).option("-c, --components", descriptions.components, { default: !0 }).option("-w, --web-components", descriptions.webComponents, { default: !0 }).option("-v, --vue", descriptions.vue, { default: !0 }).option("-p, --views", descriptions.views, { default: !0 }).option("-f, --functions", descriptions.functions, { default: !0 }).option("-a, --api", descriptions.api, { default: !0 }).option("-d, --database", descriptions.database, { default: !0 }).option("-ca, --cache", descriptions.cache, { default: !1 }).option("-e, --email", descriptions.email, { default: !1 }).option("-
|
|
30
|
+
buddy.command("new [name]", descriptions.command).alias("create [name]").option("-n, --name [name]", descriptions.name, { default: !1 }).option("-u, --ui", descriptions.ui, { default: !0 }).option("-c, --components", descriptions.components, { default: !0 }).option("-w, --web-components", descriptions.webComponents, { default: !0 }).option("-v, --vue", descriptions.vue, { default: !0 }).option("-p, --views", descriptions.views, { default: !0 }).option("-f, --functions", descriptions.functions, { default: !0 }).option("-a, --api", descriptions.api, { default: !0 }).option("-d, --database", descriptions.database, { default: !0 }).option("-ca, --cache", descriptions.cache, { default: !1 }).option("-e, --email", descriptions.email, { default: !1 }).option("-P, --project [project]", descriptions.project, { default: !1 }).option("-m, --minimal", descriptions.minimal, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (name, options) => {
|
|
32
31
|
log.debug("Running `buddy new <name>` ...", options);
|
|
33
32
|
const startTime = await intro("buddy new");
|
|
34
33
|
name = name ?? options.name;
|
|
35
34
|
const path = resolve(process.cwd(), name);
|
|
36
35
|
isFolderCheck(path);
|
|
37
|
-
onlineCheck();
|
|
36
|
+
await onlineCheck();
|
|
38
37
|
const result = await download(name, path, options);
|
|
39
38
|
if (result.isErr) {
|
|
40
39
|
log.error(result.error);
|
|
@@ -50,6 +49,8 @@ export function create(buddy) {
|
|
|
50
49
|
log.success(dim(`[${time.toFixed(2)}ms] Completed`));
|
|
51
50
|
}
|
|
52
51
|
log.info(bold("Welcome to the Stacks Framework! \u269B\uFE0F"));
|
|
52
|
+
log.info(`Get started: ${cyan(`cd ${name}`)} and then ${cyan("./buddy dev")}`);
|
|
53
|
+
log.info(`Run ${cyan("./buddy doctor")} anytime to check your setup`);
|
|
53
54
|
log.info("To learn more, visit https://stacksjs.com");
|
|
54
55
|
process.exit(ExitCode.Success);
|
|
55
56
|
});
|
|
@@ -61,19 +62,28 @@ function isFolderCheck(path) {
|
|
|
61
62
|
process.exit(ExitCode.FatalError);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
function onlineCheck() {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
async function onlineCheck() {
|
|
66
|
+
if (await isOnline())
|
|
67
|
+
return;
|
|
68
|
+
log.info("It appears you are disconnected from the internet.");
|
|
69
|
+
log.info("Creating a new project requires a brief internet connection to download the template and install dependencies.");
|
|
70
|
+
process.exit(ExitCode.FatalError);
|
|
71
|
+
}
|
|
72
|
+
async function isOnline() {
|
|
73
|
+
try {
|
|
74
|
+
return (await fetch("https://github.com", {
|
|
75
|
+
method: "HEAD",
|
|
76
|
+
signal: AbortSignal.timeout(3000)
|
|
77
|
+
})).ok;
|
|
78
|
+
} catch {
|
|
79
|
+
return !1;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
async function download(name, path, _options) {
|
|
73
83
|
log.info("Setting up your stack.");
|
|
74
84
|
try {
|
|
75
85
|
const { downloadTemplate } = await import("@stacksjs/gitit");
|
|
76
|
-
await downloadTemplate("stacks", { dir: name });
|
|
86
|
+
await downloadTemplate("gh:stacksjs/stacks", { dir: name });
|
|
77
87
|
log.success(`Successfully scaffolded your project at ${cyan(path)}`);
|
|
78
88
|
return { isErr: !1 };
|
|
79
89
|
} catch (error) {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -738,8 +738,11 @@ async function runHetznerDeploy(args) {
|
|
|
738
738
|
"temp",
|
|
739
739
|
".DS_Store",
|
|
740
740
|
"*.log",
|
|
741
|
+
".env",
|
|
741
742
|
".env.local",
|
|
742
|
-
".env.
|
|
743
|
+
".env.keys",
|
|
744
|
+
".env.production.bak",
|
|
745
|
+
".env.production.plain"
|
|
743
746
|
].flatMap((p) => [`--exclude='${p}'`, `--exclude='*/${p}'`]);
|
|
744
747
|
if (onlySite && !sites[onlySite]) {
|
|
745
748
|
log.error(`--site '${onlySite}' is not a configured site. Available: ${Object.keys(sites).join(", ")}`);
|
|
@@ -861,7 +864,10 @@ async function resolveAttachTargetBox(owner, environment) {
|
|
|
861
864
|
return { serverId: chosen.id, serverName: chosen.name, publicIp: chosen.public_net?.ipv4?.ip };
|
|
862
865
|
}
|
|
863
866
|
export async function provisionMailTenant(ip, logger) {
|
|
864
|
-
const cfg = emailConfig || {}
|
|
867
|
+
const cfg = emailConfig || {};
|
|
868
|
+
if (cfg.server?.enabled === !1)
|
|
869
|
+
return null;
|
|
870
|
+
const domain = cfg.domain || (typeof cfg.from?.address === "string" && cfg.from.address.includes("@") ? cfg.from.address.split("@")[1] : void 0), forwards = cfg.forwards && typeof cfg.forwards === "object" ? cfg.forwards : {}, hasForwards = Object.keys(forwards).length > 0, boxes = domain ? resolveMailboxes(cfg.mailboxes, domain) : [];
|
|
865
871
|
if (!domain && !hasForwards)
|
|
866
872
|
return null;
|
|
867
873
|
const { execSync } = await import("node:child_process"), sshArgs = ["-o", "StrictHostKeyChecking=accept-new", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", `root@${ip}`], forwardsB64 = hasForwards ? Buffer.from(JSON.stringify(forwards)).toString("base64") : "", readme = "Auto-forwarding rules, re-read on every message (edits take effect immediately, no restart). KEY = the delivered mailbox: the FULL address for per-domain isolated mailboxes (e.g. no-reply@app.com), or a bare local-part for legacy role mailboxes. VALUE = list of destination addresses; targets on a local domain are written straight to that mailbox Maildir, external targets are relayed. Managed by buddy deploy " + "from config/email.ts (merge-based \u2014 hand edits to other keys are preserved).", readmeB64 = Buffer.from(readme).toString("base64"), boxesB64 = boxes.length ? Buffer.from(boxes.map((b) => `${b.address} ${b.password}`).join(`
|
|
@@ -1083,6 +1089,21 @@ async function reconcileConfigDns(sites, logger) {
|
|
|
1083
1089
|
logger.warn(`DNS (config/dns.ts) reconcile for ${domain} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1084
1090
|
}
|
|
1085
1091
|
}
|
|
1092
|
+
async function verifyDnsRecord(provider, domain, fqdn, ip) {
|
|
1093
|
+
try {
|
|
1094
|
+
if (typeof provider?.listRecords !== "function")
|
|
1095
|
+
return !0;
|
|
1096
|
+
const res = await provider.listRecords(domain);
|
|
1097
|
+
if (!res?.success || !Array.isArray(res.records))
|
|
1098
|
+
return !0;
|
|
1099
|
+
return res.records.some((r) => {
|
|
1100
|
+
const name = typeof r?.name === "string" ? r.name.replace(/\.$/, "") : "";
|
|
1101
|
+
return r?.type === "A" && name === fqdn && r?.content === ip;
|
|
1102
|
+
});
|
|
1103
|
+
} catch {
|
|
1104
|
+
return !0;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1086
1107
|
async function reconcileHetznerDns(sites, ip, logger) {
|
|
1087
1108
|
const domains = new Set;
|
|
1088
1109
|
for (const site of Object.values(sites))
|
|
@@ -1129,11 +1150,15 @@ async function reconcileHetznerDns(sites, ip, logger) {
|
|
|
1129
1150
|
continue;
|
|
1130
1151
|
}
|
|
1131
1152
|
for (const sub of ["", "www"]) {
|
|
1132
|
-
const fqdn = sub ? `${sub}.${domain}` : domain, res = await provider.upsertRecord(domain, { name:
|
|
1133
|
-
if (res?.success === !1)
|
|
1134
|
-
logger.warn(` DNS: ${fqdn} \u2192 ${ip} failed: ${res.error || "unknown error"}`);
|
|
1135
|
-
|
|
1153
|
+
const fqdn = sub ? `${sub}.${domain}` : domain, res = await provider.upsertRecord(domain, { name: fqdn, type: "A", content: ip, ttl: 600 });
|
|
1154
|
+
if (res?.success === !1) {
|
|
1155
|
+
logger.warn(` DNS: ${fqdn} \u2192 ${ip} failed: ${res.error || res.message || "unknown error"}`);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (await verifyDnsRecord(provider, domain, fqdn, ip))
|
|
1136
1159
|
logger.success(` DNS: ${fqdn} \u2192 ${ip} (${provider.name})`);
|
|
1160
|
+
else
|
|
1161
|
+
logger.warn(` DNS: ${fqdn} \u2192 ${ip} reported success at ${provider.name} but no matching A record exists \u2014 create it manually: A ${fqdn} \u2192 ${ip}`);
|
|
1137
1162
|
}
|
|
1138
1163
|
} catch (err) {
|
|
1139
1164
|
logger.warn(` DNS: ${domain} reconciliation failed: ${err?.message || err}`);
|
package/dist/commands/dev.js
CHANGED
|
@@ -6,7 +6,6 @@ import { bold, cyan, dim, green, intro, log, onUnknownSubcommand, outro, prompts
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { promises as dns } from "node:dns";
|
|
10
9
|
import { Action } from "@stacksjs/enums";
|
|
11
10
|
import { libsPath, projectPath } from "@stacksjs/path";
|
|
12
11
|
import { ExitCode } from "@stacksjs/types";
|
|
@@ -187,7 +186,7 @@ export function dev(buddy) {
|
|
|
187
186
|
onUnknownSubcommand(buddy, "dev");
|
|
188
187
|
}
|
|
189
188
|
export async function startDevelopmentServer(_options, _startTime) {
|
|
190
|
-
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL, nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", frontendPort = Number(process.env.PORT) || 3000, apiPort = Number(process.env.PORT_API) || 3008, docsPort = Number(process.env.PORT_DOCS) || 3006, dashboardPort = Number(process.env.PORT_ADMIN) || 3002, includeDashboard = process.env.STACKS_DEV_DASHBOARD === "1", appLooksCustom = !nativeMode && appUrl && appUrl !== "localhost" && !appUrl.includes("localhost:"), domain = appLooksCustom ? appUrl.replace(/^https?:\/\//, "") : null,
|
|
189
|
+
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL, nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", frontendPort = Number(process.env.PORT) || 3000, apiPort = Number(process.env.PORT_API) || 3008, docsPort = Number(process.env.PORT_DOCS) || 3006, dashboardPort = Number(process.env.PORT_ADMIN) || 3002, includeDashboard = process.env.STACKS_DEV_DASHBOARD === "1", appLooksCustom = !nativeMode && appUrl && appUrl !== "localhost" && !appUrl.includes("localhost:"), domain = appLooksCustom ? appUrl.replace(/^https?:\/\//, "") : null, localhostOnly = process.env.STACKS_DEV_LOCALHOST === "1", hasCustomDomain = appLooksCustom && !proxyManagedExternally && !localhostOnly, dashboardDomain = domain ? `dashboard.${domain}` : null, frontendUrl = domain ? `https://${domain}` : `http://localhost:${frontendPort}`, apiUrl = domain ? `https://${domain}/api` : `http://localhost:${apiPort}`, docsUrl = domain ? `https://${domain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`, managedPorts = [
|
|
191
190
|
frontendPort,
|
|
192
191
|
apiPort,
|
|
193
192
|
docsPort,
|
|
@@ -201,6 +200,8 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
201
200
|
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
202
201
|
await cleanupStaleDevProcesses(managedPorts);
|
|
203
202
|
const rpxTlsPreflight = hasCustomDomain && domain ? prepareRpxTlsForDev({ domain, includeDashboard, options }) : Promise.resolve();
|
|
203
|
+
if (localhostOnly && domain && !proxyManagedExternally)
|
|
204
|
+
removeStalePublicDomainOverrides(domain, includeDashboard, options.verbose ?? !1).catch(() => {});
|
|
204
205
|
rpxTlsPreflight.catch(() => {});
|
|
205
206
|
let isExiting = !1, closeNativeApp;
|
|
206
207
|
const SHUTDOWN_GRACE_MS = 1500, cleanup = () => {
|
|
@@ -281,7 +282,7 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
281
282
|
printDevReadyBanner({
|
|
282
283
|
options,
|
|
283
284
|
nativeMode,
|
|
284
|
-
hasCustomDomain: !!appLooksCustom,
|
|
285
|
+
hasCustomDomain: !!appLooksCustom && !localhostOnly,
|
|
285
286
|
proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
|
|
286
287
|
frontendUrl,
|
|
287
288
|
apiUrl,
|
|
@@ -363,21 +364,22 @@ function printDevReadyBanner(input) {
|
|
|
363
364
|
domain,
|
|
364
365
|
dashboardPort,
|
|
365
366
|
dashboardDomain
|
|
366
|
-
} = input, verbose = options.verbose ?? !1, useProxy = hasCustomDomain && proxyReachable, feUrl = `http://localhost:${frontendPort}`, apUrl = `http://localhost:${apiPort}`, dcUrl = `http://localhost:${docsPort}`, dbUrl = `http://localhost:${dashboardPort}`, blogUrl = `${feUrl}/blog`;
|
|
367
|
+
} = input, verbose = options.verbose ?? !1, useProxy = hasCustomDomain && proxyReachable, feUrl = useProxy ? frontendUrl : `http://localhost:${frontendPort}`, apUrl = useProxy ? apiUrl : `http://localhost:${apiPort}`, dcUrl = useProxy ? docsUrl : `http://localhost:${docsPort}`, dbUrl = useProxy ? dashboardUrl : `http://localhost:${dashboardPort}`, blogUrl = `${feUrl}/blog`;
|
|
367
368
|
console.log();
|
|
368
369
|
console.log(` ${green("\u279C")} ${bold("Frontend")}: ${cyan(feUrl)}`);
|
|
369
370
|
if (nativeMode)
|
|
370
|
-
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192
|
|
371
|
+
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);
|
|
371
372
|
console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
|
|
372
373
|
console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);
|
|
373
374
|
if (blogIsConfigured())
|
|
374
375
|
console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);
|
|
375
376
|
if (includeDashboard)
|
|
376
377
|
console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);
|
|
377
|
-
if (useProxy
|
|
378
|
-
console.log(` ${dim("\u279C")} ${dim("
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
if (useProxy) {
|
|
379
|
+
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${frontendPort} (bypasses the proxy)`)}`);
|
|
380
|
+
if (includeDashboard && dashboardDomain)
|
|
381
|
+
console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`);
|
|
382
|
+
}
|
|
381
383
|
if (isComingSoonMode()) {
|
|
382
384
|
console.log();
|
|
383
385
|
console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`);
|
|
@@ -849,15 +851,7 @@ async function removeStalePublicDomainOverrides(domain, includeDashboard, verbos
|
|
|
849
851
|
}
|
|
850
852
|
}
|
|
851
853
|
async function prepareRpxTlsForDev(input) {
|
|
852
|
-
const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1
|
|
853
|
-
if (process.env.STACKS_DEV_ALLOW_PUBLIC_DOMAIN !== "1") {
|
|
854
|
-
if ((await dns.resolve4(domain).catch(() => [])).some((address) => !address.startsWith("127."))) {
|
|
855
|
-
log.warn(`Skipping local DNS override for public domain ${domain}; use a .localhost/.test domain for development`);
|
|
856
|
-
await removeStalePublicDomainOverrides(domain, includeDashboard, verbose);
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
const hosts = [
|
|
854
|
+
const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1, hosts = [
|
|
861
855
|
domain,
|
|
862
856
|
...includeDashboard ? [`dashboard.${domain}`] : []
|
|
863
857
|
], hostsNeedingFile = hosts.filter((host) => {
|
|
@@ -877,8 +871,11 @@ async function prepareRpxTlsForDev(input) {
|
|
|
877
871
|
}
|
|
878
872
|
async function startRpxDaemonIfNeeded(input) {
|
|
879
873
|
const { ensureDaemonRunning, isDaemonRunning: isRpxUp } = await importDevelopmentRpx();
|
|
880
|
-
if (await isRpxUp())
|
|
881
|
-
|
|
874
|
+
if (await isRpxUp()) {
|
|
875
|
+
if (await waitForHttpsProxy(443, 1500))
|
|
876
|
+
return;
|
|
877
|
+
await input.stopRpx({ timeoutMs: 5000 }).catch(() => {});
|
|
878
|
+
}
|
|
882
879
|
const spawnOpts = {
|
|
883
880
|
spawnCommand: input.spawnCommand,
|
|
884
881
|
spawnCwd: resolveRpxDaemonSpawnCwd(),
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { CLI } from '@stacksjs/types';
|
|
2
2
|
export declare function doctor(buddy: CLI): void;
|
|
3
|
+
/**
|
|
4
|
+
* Throw inside a probe fn to record a warning instead of a failure.
|
|
5
|
+
* Used when a check cannot run to completion (e.g. the driver exposes
|
|
6
|
+
* no raw query method, so there is nothing to probe with) or when the
|
|
7
|
+
* finding is advisory rather than fatal (e.g. a busy dev port).
|
|
8
|
+
*/
|
|
9
|
+
declare class ProbeWarning extends Error {
|
|
10
|
+
|
|
11
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,22 +2,28 @@ import process from "node:process";
|
|
|
2
2
|
import { bold, dim, green, intro, log, onUnknownSubcommand, red, yellow } from "@stacksjs/cli";
|
|
3
3
|
import { feature } from "@stacksjs/config";
|
|
4
4
|
import { storage } from "@stacksjs/storage";
|
|
5
|
+
import { isSupportedBunVersion, isVersionGreaterThanOrEqual, minimumBunVersion } from "@stacksjs/utils";
|
|
5
6
|
import { FEATURE_NAMES, featurePathsPresent } from "./features";
|
|
7
|
+
const minimumSqliteVersion = "3.47.2";
|
|
8
|
+
|
|
9
|
+
class ProbeWarning extends Error {
|
|
10
|
+
}
|
|
6
11
|
async function probe(checks, name, fn, timeoutMs = 2000) {
|
|
7
|
-
const start = Date.now();
|
|
12
|
+
const start = Date.now(), ac = new AbortController, timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
8
13
|
try {
|
|
9
|
-
const
|
|
14
|
+
const message = await Promise.race([
|
|
10
15
|
fn(),
|
|
11
16
|
new Promise((_, rej) => ac.signal.addEventListener("abort", () => rej(Error(`timed out (>${Math.round(timeoutMs / 1000)}s)`))))
|
|
12
17
|
]);
|
|
13
|
-
clearTimeout(timer);
|
|
14
18
|
checks.push({ name, status: "pass", message: `${message} (${Date.now() - start}ms)` });
|
|
15
19
|
} catch (err) {
|
|
16
20
|
checks.push({
|
|
17
21
|
name,
|
|
18
|
-
status: "fail",
|
|
22
|
+
status: err instanceof ProbeWarning ? "warn" : "fail",
|
|
19
23
|
message: err instanceof Error ? err.message : String(err)
|
|
20
24
|
});
|
|
25
|
+
} finally {
|
|
26
|
+
clearTimeout(timer);
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
export function doctor(buddy) {
|
|
@@ -25,19 +31,18 @@ export function doctor(buddy) {
|
|
|
25
31
|
log.debug("Running `buddy doctor` ...");
|
|
26
32
|
await intro("buddy doctor");
|
|
27
33
|
const checks = [], bunVersion = process.versions.bun;
|
|
28
|
-
if (bunVersion)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
34
|
+
if (bunVersion && isSupportedBunVersion(bunVersion))
|
|
35
|
+
checks.push({
|
|
36
|
+
name: "Bun Runtime",
|
|
37
|
+
status: "pass",
|
|
38
|
+
message: `v${bunVersion}`
|
|
39
|
+
});
|
|
40
|
+
else if (bunVersion)
|
|
41
|
+
checks.push({
|
|
42
|
+
name: "Bun Runtime",
|
|
43
|
+
status: "fail",
|
|
44
|
+
message: `v${bunVersion} (requires v${minimumBunVersion} or later, run: bun upgrade)`
|
|
45
|
+
});
|
|
41
46
|
else
|
|
42
47
|
checks.push({
|
|
43
48
|
name: "Bun Runtime",
|
|
@@ -100,9 +105,34 @@ export function doctor(buddy) {
|
|
|
100
105
|
status: "fail",
|
|
101
106
|
message: "Not set \u2014 features that depend on it (encrypted columns, signed URLs, env decryption) will refuse to run. Run `buddy key:generate`."
|
|
102
107
|
});
|
|
108
|
+
await probe(checks, "SQLite Engine", async () => {
|
|
109
|
+
const { Database } = await import("bun:sqlite"), memory = new Database(":memory:");
|
|
110
|
+
let version;
|
|
111
|
+
try {
|
|
112
|
+
const row = memory.query("SELECT sqlite_version() AS version").get();
|
|
113
|
+
if (typeof row?.version === "string")
|
|
114
|
+
version = row.version;
|
|
115
|
+
} finally {
|
|
116
|
+
memory.close();
|
|
117
|
+
}
|
|
118
|
+
if (!version)
|
|
119
|
+
throw new ProbeWarning("could not read sqlite_version() (skipped)");
|
|
120
|
+
if (!isVersionGreaterThanOrEqual(version, minimumSqliteVersion))
|
|
121
|
+
throw Error(`v${version} is below the required v${minimumSqliteVersion} (system.sqlite in package.json); upgrade Bun for a newer bundled SQLite`);
|
|
122
|
+
return `v${version}`;
|
|
123
|
+
});
|
|
103
124
|
await probe(checks, "Database", async () => {
|
|
104
|
-
const { db } = await import("@stacksjs/database");
|
|
105
|
-
|
|
125
|
+
const { db } = await import("@stacksjs/database"), unsafe = db.unsafe;
|
|
126
|
+
if (typeof unsafe !== "function")
|
|
127
|
+
throw new ProbeWarning("driver exposes no raw query method (skipped)");
|
|
128
|
+
const statement = unsafe("SELECT 1");
|
|
129
|
+
if (!statement || typeof statement.then !== "function" && typeof statement.execute !== "function")
|
|
130
|
+
throw new ProbeWarning("driver returned a non-executable statement (skipped)");
|
|
131
|
+
const result = typeof statement.execute === "function" ? await statement.execute() : await statement;
|
|
132
|
+
if (result === void 0 || result === null)
|
|
133
|
+
throw Error("probe query returned no result");
|
|
134
|
+
if (Array.isArray(result) && result.length === 0)
|
|
135
|
+
throw Error("probe query returned no rows");
|
|
106
136
|
return "Reachable";
|
|
107
137
|
});
|
|
108
138
|
await probe(checks, "Database FKs", async () => {
|
|
@@ -303,6 +333,34 @@ export function doctor(buddy) {
|
|
|
303
333
|
message: `Could not audit dev-domain overrides: ${err instanceof Error ? err.message : String(err)}`
|
|
304
334
|
});
|
|
305
335
|
}
|
|
336
|
+
await probe(checks, "Dev ports", async () => {
|
|
337
|
+
const net = await import("node:net"), { config } = await import("@stacksjs/config"), configured = config.ports ?? {}, targets = [
|
|
338
|
+
{ name: "frontend", key: "frontend", envVar: "PORT", fallback: 3000 },
|
|
339
|
+
{ name: "api", key: "api", envVar: "PORT_API", fallback: 3008 },
|
|
340
|
+
{ name: "docs", key: "docs", envVar: "PORT_DOCS", fallback: 3006 },
|
|
341
|
+
{ name: "dashboard", key: "admin", envVar: "PORT_ADMIN", fallback: 3002 }
|
|
342
|
+
].map((t) => ({ ...t, port: Number(configured[t.key]) || t.fallback })), canConnect = (port, host) => new Promise((resolve) => {
|
|
343
|
+
const socket = net.createConnection({ port, host });
|
|
344
|
+
socket.setTimeout(400);
|
|
345
|
+
const done = (occupied) => {
|
|
346
|
+
socket.destroy();
|
|
347
|
+
resolve(occupied);
|
|
348
|
+
};
|
|
349
|
+
socket.once("connect", () => done(!0));
|
|
350
|
+
socket.once("timeout", () => done(!1));
|
|
351
|
+
socket.once("error", () => done(!1));
|
|
352
|
+
}), occupied = new Set;
|
|
353
|
+
await Promise.all([...new Set(targets.map((t) => t.port))].map(async (port) => {
|
|
354
|
+
if (await canConnect(port, "127.0.0.1") || await canConnect(port, "::1"))
|
|
355
|
+
occupied.add(port);
|
|
356
|
+
}));
|
|
357
|
+
const busy = targets.filter((t) => occupied.has(t.port));
|
|
358
|
+
if (busy.length > 0) {
|
|
359
|
+
const list = busy.map((t) => `${t.name} :${t.port} (${t.envVar})`).join(", ");
|
|
360
|
+
throw new ProbeWarning(`in use: ${list}. buddy dev will fail to bind; stop the process holding the port or set the override env var`);
|
|
361
|
+
}
|
|
362
|
+
return `All free: ${targets.map((t) => `${t.name} :${t.port}`).join(", ")}`;
|
|
363
|
+
});
|
|
306
364
|
try {
|
|
307
365
|
const orphans = [];
|
|
308
366
|
for (const name of FEATURE_NAMES) {
|