@stacksjs/buddy 0.70.112 → 0.70.114
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 +38 -31
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +162 -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 +45 -44
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 = () => {
|
|
@@ -208,25 +209,34 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
208
209
|
return;
|
|
209
210
|
isExiting = !0;
|
|
210
211
|
closeNativeApp?.();
|
|
211
|
-
|
|
212
|
-
activeRpxRegistryIds.length = 0;
|
|
213
|
-
try {
|
|
214
|
-
process.kill(-process.pid, "SIGTERM");
|
|
215
|
-
} catch {
|
|
212
|
+
const rpxTeardown = (async () => {
|
|
216
213
|
try {
|
|
217
|
-
|
|
214
|
+
await unregisterRpxProxies(activeRpxRegistryIds);
|
|
215
|
+
activeRpxRegistryIds.length = 0;
|
|
216
|
+
if (hasCustomDomain && domain)
|
|
217
|
+
await removeStalePublicDomainOverrides(domain, includeDashboard, options.verbose ?? !1);
|
|
218
218
|
} catch {}
|
|
219
|
-
}
|
|
220
|
-
|
|
219
|
+
})(), teardownDeadline = new Promise((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_MS));
|
|
220
|
+
Promise.race([rpxTeardown, teardownDeadline]).finally(() => {
|
|
221
221
|
try {
|
|
222
|
-
process.kill(
|
|
222
|
+
process.kill(-process.pid, "SIGTERM");
|
|
223
223
|
} catch {
|
|
224
|
-
|
|
224
|
+
try {
|
|
225
|
+
process.kill(0, "SIGTERM");
|
|
226
|
+
} catch {}
|
|
225
227
|
}
|
|
226
|
-
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
try {
|
|
230
|
+
process.kill(0, "SIGKILL");
|
|
231
|
+
} catch {
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}, SHUTDOWN_GRACE_MS).unref();
|
|
235
|
+
});
|
|
227
236
|
};
|
|
228
237
|
process.on("SIGINT", cleanup);
|
|
229
238
|
process.on("SIGTERM", cleanup);
|
|
239
|
+
process.on("SIGHUP", cleanup);
|
|
230
240
|
const quietOpts = { ...options, quiet: !0 }, a = await actions(), ports = [
|
|
231
241
|
{ name: "Frontend", port: frontendPort },
|
|
232
242
|
{ name: "API", port: apiPort }
|
|
@@ -272,7 +282,7 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
272
282
|
printDevReadyBanner({
|
|
273
283
|
options,
|
|
274
284
|
nativeMode,
|
|
275
|
-
hasCustomDomain: !!appLooksCustom,
|
|
285
|
+
hasCustomDomain: !!appLooksCustom && !localhostOnly,
|
|
276
286
|
proxyReachable: proxyManagedExternally ? !0 : proxyReachable,
|
|
277
287
|
frontendUrl,
|
|
278
288
|
apiUrl,
|
|
@@ -354,21 +364,22 @@ function printDevReadyBanner(input) {
|
|
|
354
364
|
domain,
|
|
355
365
|
dashboardPort,
|
|
356
366
|
dashboardDomain
|
|
357
|
-
} = 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`;
|
|
358
368
|
console.log();
|
|
359
369
|
console.log(` ${green("\u279C")} ${bold("Frontend")}: ${cyan(feUrl)}`);
|
|
360
370
|
if (nativeMode)
|
|
361
|
-
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192
|
|
371
|
+
console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);
|
|
362
372
|
console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);
|
|
363
373
|
console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);
|
|
364
374
|
if (blogIsConfigured())
|
|
365
375
|
console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);
|
|
366
376
|
if (includeDashboard)
|
|
367
377
|
console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);
|
|
368
|
-
if (useProxy
|
|
369
|
-
console.log(` ${dim("\u279C")} ${dim("
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
|
372
383
|
if (isComingSoonMode()) {
|
|
373
384
|
console.log();
|
|
374
385
|
console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`);
|
|
@@ -840,15 +851,7 @@ async function removeStalePublicDomainOverrides(domain, includeDashboard, verbos
|
|
|
840
851
|
}
|
|
841
852
|
}
|
|
842
853
|
async function prepareRpxTlsForDev(input) {
|
|
843
|
-
const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1
|
|
844
|
-
if (process.env.STACKS_DEV_ALLOW_PUBLIC_DOMAIN !== "1") {
|
|
845
|
-
if ((await dns.resolve4(domain).catch(() => [])).some((address) => !address.startsWith("127."))) {
|
|
846
|
-
log.warn(`Skipping local DNS override for public domain ${domain}; use a .localhost/.test domain for development`);
|
|
847
|
-
await removeStalePublicDomainOverrides(domain, includeDashboard, verbose);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
const hosts = [
|
|
854
|
+
const { domain, includeDashboard, options } = input, verbose = options.verbose ?? !1, hosts = [
|
|
852
855
|
domain,
|
|
853
856
|
...includeDashboard ? [`dashboard.${domain}`] : []
|
|
854
857
|
], hostsNeedingFile = hosts.filter((host) => {
|
|
@@ -868,8 +871,11 @@ async function prepareRpxTlsForDev(input) {
|
|
|
868
871
|
}
|
|
869
872
|
async function startRpxDaemonIfNeeded(input) {
|
|
870
873
|
const { ensureDaemonRunning, isDaemonRunning: isRpxUp } = await importDevelopmentRpx();
|
|
871
|
-
if (await isRpxUp())
|
|
872
|
-
|
|
874
|
+
if (await isRpxUp()) {
|
|
875
|
+
if (await waitForHttpsProxy(443, 1500))
|
|
876
|
+
return;
|
|
877
|
+
await input.stopRpx({ timeoutMs: 5000 }).catch(() => {});
|
|
878
|
+
}
|
|
873
879
|
const spawnOpts = {
|
|
874
880
|
spawnCommand: input.spawnCommand,
|
|
875
881
|
spawnCwd: resolveRpxDaemonSpawnCwd(),
|
|
@@ -922,6 +928,7 @@ async function registerRpxProxiesForDomain(input) {
|
|
|
922
928
|
to: proxy.to,
|
|
923
929
|
cwd: process.cwd(),
|
|
924
930
|
createdAt,
|
|
931
|
+
pid: process.pid,
|
|
925
932
|
pathRewrites: proxy.pathRewrites
|
|
926
933
|
}, void 0, verbose);
|
|
927
934
|
if (!activeRpxRegistryIds.includes(proxy.id))
|
|
@@ -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
|
+
}
|