@stacksjs/actions 0.70.87 → 0.70.90
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/action.js +78 -2
- package/dist/actions.js +2 -2
- package/dist/add.js +20 -2
- package/dist/ai/request-model-access.js +10 -2
- package/dist/auth/client.js +44 -3
- package/dist/auth/prune.js +35 -2
- package/dist/auth/setup.js +152 -36
- package/dist/auth/token.js +10 -2
- package/dist/blog-admin.d.ts +55 -0
- package/dist/blog-admin.js +156 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.js +306 -56
- package/dist/build/cli.js +6 -2
- package/dist/build/component-libs.js +7 -2
- package/dist/build/core.js +55 -2
- package/dist/build/desktop.js +5 -2
- package/dist/build/docs.js +5 -2
- package/dist/build/frontend-static.js +123 -8
- package/dist/build/server.js +6 -2
- package/dist/build/stacks.js +8 -2
- package/dist/build/views.js +11 -2
- package/dist/build.js +64 -2
- package/dist/bump.js +162 -7
- package/dist/cdn/invalidate-cache.js +0 -1
- package/dist/changelog.js +27 -3
- package/dist/check/ports.js +16 -2
- package/dist/clean.js +4 -2
- package/dist/commit.js +12 -2
- package/dist/copy-types.js +10 -3
- package/dist/database/seed.js +26 -2
- package/dist/deploy/create-receipt-rule.js +36 -2
- package/dist/deploy/index.js +1318 -44
- package/dist/dev/api.js +94 -6
- package/dist/dev/components.js +7 -2
- package/dist/dev/dashboard-globals.js +89 -3
- package/dist/dev/dashboard-utils.js +380 -2
- package/dist/dev/dashboard.js +435 -8
- package/dist/dev/desktop.js +5 -2
- package/dist/dev/docs.js +24 -2
- package/dist/dev/index.js +29 -2
- package/dist/dev/migrate-watcher.js +74 -2
- package/dist/dev/system-tray.js +5 -2
- package/dist/dev/views.js +147 -2
- package/dist/discover-packages.js +32 -2
- package/dist/domains/add.js +61 -2
- package/dist/domains/purchase.js +66 -2
- package/dist/domains/remove.js +24 -2
- package/dist/examples.js +34 -2
- package/dist/find-projects.js +16 -2
- package/dist/fresh.js +17 -2
- package/dist/generate/action-types.js +9 -4
- package/dist/generate/component-meta.js +9 -3
- package/dist/generate/custom-cli-file.js +20 -3
- package/dist/generate/env-files.js +46 -6
- package/dist/generate/ide-helpers.js +8 -3
- package/dist/generate/index.js +158 -2
- package/dist/generate/lib-entries.js +16 -3
- package/dist/generate/pantry-file.js +46 -2
- package/dist/generate/symlink-core.js +2 -2
- package/dist/generate/types.js +3 -2
- package/dist/generate/vscode-custom-data.js +9 -3
- package/dist/generate/web-types.js +0 -1
- package/dist/helpers/component-meta.js +295 -2
- package/dist/helpers/index.js +1 -2
- package/dist/helpers/lib-entries.js +73 -2
- package/dist/helpers/package-json.js +45 -14
- package/dist/helpers/utils.js +105 -2
- package/dist/helpers/vscode-custom-data.js +30 -2
- package/dist/index.d.ts +13 -0
- package/dist/index.js +50 -2
- package/dist/key-generate.js +24 -2
- package/dist/lint/fix.js +4 -3
- package/dist/lint/index.js +4 -3
- package/dist/lint/lint.js +34 -2
- package/dist/make-command.js +64 -14
- package/dist/make-job.js +41 -16
- package/dist/make-policy.js +74 -26
- package/dist/make-resource.js +50 -21
- package/dist/make.js +328 -2
- package/dist/migrate/database.js +16 -2
- package/dist/migrate/fresh.js +31 -2
- package/dist/orm/base.js +0 -2
- package/dist/orm/test.js +0 -1
- package/dist/prepublish.js +16 -2
- package/dist/queue/clear.js +42 -2
- package/dist/queue/dlq-purge.js +28 -2
- package/dist/queue/dlq-retry.js +35 -2
- package/dist/queue/dlq.js +55 -5
- package/dist/queue/failed.js +69 -5
- package/dist/queue/flush.js +49 -3
- package/dist/queue/inspect.js +154 -8
- package/dist/queue/list.js +69 -5
- package/dist/queue/monitor.js +94 -6
- package/dist/queue/pause.js +28 -2
- package/dist/queue/quarantine.js +42 -3
- package/dist/queue/resume.js +27 -2
- package/dist/queue/retry.js +41 -2
- package/dist/queue/schedule-list.js +33 -5
- package/dist/queue/schedule.js +12 -2
- package/dist/queue/status.js +68 -5
- package/dist/queue/table.js +10 -2
- package/dist/queue/unquarantine.js +27 -2
- package/dist/queue/work.js +57 -2
- package/dist/release.js +21 -3
- package/dist/route/list.js +78 -3
- package/dist/run-action.js +1 -2
- package/dist/saas/setup.js +9 -2
- package/dist/scaffold-crud.js +164 -40
- package/dist/schedule/run.js +7 -2
- package/dist/search/flush.js +10 -2
- package/dist/search/import.js +10 -2
- package/dist/search/settings-list.js +10 -2
- package/dist/search/settings.js +9 -2
- package/dist/serve/api.js +35 -2
- package/dist/setup/index.js +1 -2
- package/dist/setup/ssl.js +271 -2
- package/dist/stacks.js +265 -2
- package/dist/templates.js +301 -2
- package/dist/test/feature.js +11 -2
- package/dist/test/index.js +11 -2
- package/dist/test/ui.js +4 -2
- package/dist/test/unit.js +11 -2
- package/dist/typecheck.js +4 -2
- package/dist/types.js +16 -2
- package/dist/upgrade/binary.js +29 -2
- package/dist/upgrade/bun.js +0 -1
- package/dist/upgrade/dependencies.js +0 -2
- package/dist/upgrade/framework-utils.js +193 -2
- package/dist/upgrade/framework.js +310 -7
- package/dist/upgrade/index.js +17 -2
- package/dist/upgrade/packages.js +99 -12
- package/dist/upgrade/shell.js +40 -4
- package/dist/upgrade.js +38 -2
- package/package.json +16 -16
- package/dist/chunk-0c8sd1t6.js +0 -2
- package/dist/chunk-0g2nzhx9.js +0 -240
- package/dist/chunk-0gadhvn7.js +0 -3
- package/dist/chunk-0h27aa4r.js +0 -93
- package/dist/chunk-3peh8kqs.js +0 -851
- package/dist/chunk-62gdfav8.js +0 -3
- package/dist/chunk-6bk53y02.js +0 -3
- package/dist/chunk-78c5n3s2.js +0 -3
- package/dist/chunk-c74qv38e.js +0 -17
- package/dist/chunk-cdk2r2jd.js +0 -1
- package/dist/chunk-dbz9dp2h.js +0 -3
- package/dist/chunk-e142dheq.js +0 -3
- package/dist/chunk-ec4gky40.js +0 -3
- package/dist/chunk-h3d81wrg.js +0 -3
- package/dist/chunk-m3q0x4h0.js +0 -688
- package/dist/chunk-mqgbyhph.js +0 -5
- package/dist/chunk-n24m8prb.js +0 -3
- package/dist/chunk-n5eqwhck.js +0 -7
- package/dist/chunk-nsvnqv8y.js +0 -5
- package/dist/chunk-nty6mmx4.js +0 -283
- package/dist/chunk-s735m9mg.js +0 -1
- package/dist/chunk-tpb8jgsk.js +0 -7
- package/dist/chunk-tsbk5vne.js +0 -3
- package/dist/chunk-xg9xscqz.js +0 -1
- package/dist/chunk-y6e448he.js +0 -7
package/dist/setup/ssl.js
CHANGED
|
@@ -1,2 +1,271 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { log } from "@stacksjs/cli";
|
|
7
|
+
const HOSTS_FILE = process.platform === "win32" ? path.join(process.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts", SSL_DIR = path.join(os.homedir(), ".stacks", "ssl");
|
|
8
|
+
function isInteractiveTerminal() {
|
|
9
|
+
return process.stdin.isTTY === !0 && process.stdout.isTTY === !0;
|
|
10
|
+
}
|
|
11
|
+
function getSudoPassword() {
|
|
12
|
+
return process.env.SUDO_PASSWORD;
|
|
13
|
+
}
|
|
14
|
+
function canRunSudo() {
|
|
15
|
+
return isInteractiveTerminal() || !!getSudoPassword();
|
|
16
|
+
}
|
|
17
|
+
export function isDomainInHosts(domain) {
|
|
18
|
+
try {
|
|
19
|
+
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf-8");
|
|
20
|
+
return hostsContent.includes(`127.0.0.1 ${domain}`) || hostsContent.includes(`::1 ${domain}`);
|
|
21
|
+
} catch {
|
|
22
|
+
if (process.platform !== "win32") {
|
|
23
|
+
const sudoPassword = getSudoPassword();
|
|
24
|
+
if (sudoPassword)
|
|
25
|
+
try {
|
|
26
|
+
const result = execSync(`echo '${sudoPassword}' | sudo -S cat "${HOSTS_FILE}" 2>/dev/null`, { encoding: "utf-8" });
|
|
27
|
+
return result.includes(`127.0.0.1 ${domain}`) || result.includes(`::1 ${domain}`);
|
|
28
|
+
} catch {
|
|
29
|
+
return !1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return !1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function addDomainToHosts(domain, verbose) {
|
|
36
|
+
if (isDomainInHosts(domain)) {
|
|
37
|
+
if (verbose)
|
|
38
|
+
log.info(`${domain} already in hosts file`);
|
|
39
|
+
return !0;
|
|
40
|
+
}
|
|
41
|
+
const hostsEntry = `
|
|
42
|
+
# Added by Stacks
|
|
43
|
+
127.0.0.1 ${domain}
|
|
44
|
+
::1 ${domain}
|
|
45
|
+
`;
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
log.warn("Please add this to your hosts file manually:");
|
|
48
|
+
log.warn(`127.0.0.1 ${domain}`);
|
|
49
|
+
log.warn(`::1 ${domain}`);
|
|
50
|
+
log.warn(`File location: ${HOSTS_FILE}`);
|
|
51
|
+
return !1;
|
|
52
|
+
}
|
|
53
|
+
if (!canRunSudo()) {
|
|
54
|
+
if (verbose) {
|
|
55
|
+
log.warn(`Cannot add ${domain} to hosts file (non-interactive mode and no SUDO_PASSWORD set)`);
|
|
56
|
+
log.info("Run `buddy setup:ssl` in a terminal or set SUDO_PASSWORD environment variable");
|
|
57
|
+
}
|
|
58
|
+
return !1;
|
|
59
|
+
}
|
|
60
|
+
const sudoPassword = getSudoPassword();
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let child;
|
|
63
|
+
if (sudoPassword)
|
|
64
|
+
child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "echo '${hostsEntry}' >> '${HOSTS_FILE}'"`], {
|
|
65
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
66
|
+
});
|
|
67
|
+
else
|
|
68
|
+
child = spawn("sudo", ["sh", "-c", `echo '${hostsEntry}' >> "${HOSTS_FILE}"`], {
|
|
69
|
+
stdio: "inherit"
|
|
70
|
+
});
|
|
71
|
+
child.on("close", (code) => {
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
if (verbose)
|
|
74
|
+
log.success(`Added ${domain} to hosts file`);
|
|
75
|
+
resolve(!0);
|
|
76
|
+
} else {
|
|
77
|
+
if (verbose) {
|
|
78
|
+
log.error(`Failed to add ${domain} to hosts file`);
|
|
79
|
+
log.warn(`Manually add: 127.0.0.1 ${domain}`);
|
|
80
|
+
}
|
|
81
|
+
resolve(!1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
child.on("error", () => {
|
|
85
|
+
log.error("Failed to run sudo command");
|
|
86
|
+
resolve(!1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function sslCertificatesExist(domain) {
|
|
91
|
+
const certPath = path.join(SSL_DIR, `${domain}.crt`), keyPath = path.join(SSL_DIR, `${domain}.key`), caPath = path.join(SSL_DIR, `${domain}.ca.crt`);
|
|
92
|
+
return fs.existsSync(certPath) && fs.existsSync(keyPath) && fs.existsSync(caPath);
|
|
93
|
+
}
|
|
94
|
+
export function isCertificateTrusted(domain) {
|
|
95
|
+
if (process.platform !== "darwin")
|
|
96
|
+
return !0;
|
|
97
|
+
const certPath = path.join(SSL_DIR, `${domain}.crt`);
|
|
98
|
+
if (!fs.existsSync(certPath))
|
|
99
|
+
return !1;
|
|
100
|
+
try {
|
|
101
|
+
const fingerprint = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`, { encoding: "utf-8" }), eqIdx = fingerprint.indexOf("="), fingerprintValue = eqIdx >= 0 ? fingerprint.slice(eqIdx + 1).trim() : "";
|
|
102
|
+
if (!fingerprintValue)
|
|
103
|
+
return !1;
|
|
104
|
+
return execSync("security find-certificate -a -Z -p | openssl x509 -noout -fingerprint -sha256 2>/dev/null || true", { encoding: "utf-8" }).includes(fingerprintValue);
|
|
105
|
+
} catch {
|
|
106
|
+
return !1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function trustCertificate(domain, verbose) {
|
|
110
|
+
const caCertPath = path.join(SSL_DIR, `${domain}.ca.crt`), certPath = path.join(SSL_DIR, `${domain}.crt`);
|
|
111
|
+
if (!fs.existsSync(caCertPath)) {
|
|
112
|
+
log.error(`CA certificate not found: ${caCertPath}`);
|
|
113
|
+
return !1;
|
|
114
|
+
}
|
|
115
|
+
if (isCertificateTrusted(domain)) {
|
|
116
|
+
if (verbose)
|
|
117
|
+
log.info("Certificate is already trusted, skipping trust step");
|
|
118
|
+
return !0;
|
|
119
|
+
}
|
|
120
|
+
if (!canRunSudo()) {
|
|
121
|
+
if (verbose) {
|
|
122
|
+
log.warn("Cannot trust certificate (non-interactive mode and no SUDO_PASSWORD set)");
|
|
123
|
+
log.info("Run `buddy setup:ssl` in a terminal or set SUDO_PASSWORD environment variable");
|
|
124
|
+
}
|
|
125
|
+
return !1;
|
|
126
|
+
}
|
|
127
|
+
const sudoPassword = getSudoPassword();
|
|
128
|
+
if (process.platform === "darwin")
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const combinedCommand = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain '${caCertPath}' && security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain '${certPath}'`;
|
|
131
|
+
let child;
|
|
132
|
+
if (sudoPassword)
|
|
133
|
+
child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "${combinedCommand}"`], {
|
|
134
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
135
|
+
});
|
|
136
|
+
else
|
|
137
|
+
child = spawn("sudo", ["sh", "-c", combinedCommand], {
|
|
138
|
+
stdio: "inherit"
|
|
139
|
+
});
|
|
140
|
+
child.on("close", (code) => {
|
|
141
|
+
if (code === 0) {
|
|
142
|
+
try {
|
|
143
|
+
execSync(`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db "${certPath}" 2>/dev/null || true`);
|
|
144
|
+
} catch {}
|
|
145
|
+
resolve(!0);
|
|
146
|
+
} else {
|
|
147
|
+
if (verbose) {
|
|
148
|
+
log.warn("Could not trust certificate automatically");
|
|
149
|
+
log.info(` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${caCertPath}"`);
|
|
150
|
+
}
|
|
151
|
+
resolve(!1);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
child.on("error", () => {
|
|
155
|
+
log.error("Failed to run sudo command");
|
|
156
|
+
resolve(!1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
else if (process.platform === "linux")
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const linuxCommands = `mkdir -p /usr/local/share/ca-certificates/stacks && cp '${caCertPath}' /usr/local/share/ca-certificates/stacks/ && update-ca-certificates`;
|
|
162
|
+
let child;
|
|
163
|
+
if (sudoPassword)
|
|
164
|
+
child = spawn("sh", ["-c", `echo '${sudoPassword}' | sudo -S sh -c "${linuxCommands}"`], {
|
|
165
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
166
|
+
});
|
|
167
|
+
else
|
|
168
|
+
child = spawn("sudo", ["sh", "-c", linuxCommands], {
|
|
169
|
+
stdio: "inherit"
|
|
170
|
+
});
|
|
171
|
+
child.on("close", (code) => {
|
|
172
|
+
if (code === 0) {
|
|
173
|
+
log.success("Certificate trusted");
|
|
174
|
+
resolve(!0);
|
|
175
|
+
} else {
|
|
176
|
+
log.warn("Could not trust certificate automatically");
|
|
177
|
+
resolve(!1);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
child.on("error", () => resolve(!1));
|
|
181
|
+
});
|
|
182
|
+
else if (process.platform === "win32") {
|
|
183
|
+
log.warn("Please manually trust the certificate in Windows:");
|
|
184
|
+
log.warn(` 1. Double-click: ${caCertPath}`);
|
|
185
|
+
log.warn(' 2. Click "Install Certificate"');
|
|
186
|
+
log.warn(' 3. Select "Local Machine" and place in "Trusted Root Certification Authorities"');
|
|
187
|
+
return !1;
|
|
188
|
+
}
|
|
189
|
+
return !1;
|
|
190
|
+
}
|
|
191
|
+
export async function generateCertificates(domain, verbose) {
|
|
192
|
+
try {
|
|
193
|
+
const tlsxModule = await import("@stacksjs/tlsx"), { generateCertificate, createRootCA } = tlsxModule, _httpsConfig = tlsxModule.httpsConfig;
|
|
194
|
+
if (!fs.existsSync(SSL_DIR))
|
|
195
|
+
fs.mkdirSync(SSL_DIR, { recursive: !0 });
|
|
196
|
+
const config = {
|
|
197
|
+
domain,
|
|
198
|
+
hostCertCN: domain,
|
|
199
|
+
basePath: SSL_DIR,
|
|
200
|
+
caCertPath: path.join(SSL_DIR, `${domain}.ca.crt`),
|
|
201
|
+
certPath: path.join(SSL_DIR, `${domain}.crt`),
|
|
202
|
+
keyPath: path.join(SSL_DIR, `${domain}.key`),
|
|
203
|
+
altNameIPs: ["127.0.0.1", "::1"],
|
|
204
|
+
altNameURIs: ["localhost", domain],
|
|
205
|
+
organizationName: "Stacks Development",
|
|
206
|
+
countryName: "US",
|
|
207
|
+
stateName: "California",
|
|
208
|
+
localityName: "Playa Vista",
|
|
209
|
+
commonName: domain,
|
|
210
|
+
validityDays: 825,
|
|
211
|
+
verbose: verbose || !1,
|
|
212
|
+
subjectAltNames: [
|
|
213
|
+
{ type: 2, value: domain },
|
|
214
|
+
{ type: 2, value: `*.${domain.split(".").slice(1).join(".")}` },
|
|
215
|
+
{ type: 2, value: "localhost" },
|
|
216
|
+
{ type: 2, value: "*.localhost" }
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
if (verbose)
|
|
220
|
+
log.info("Generating Root CA certificate...");
|
|
221
|
+
const caCert = await createRootCA(config);
|
|
222
|
+
if (verbose)
|
|
223
|
+
log.info(`Generating host certificate for ${domain}...`);
|
|
224
|
+
const hostCert = await generateCertificate({
|
|
225
|
+
...config,
|
|
226
|
+
rootCA: {
|
|
227
|
+
certificate: caCert.certificate,
|
|
228
|
+
privateKey: caCert.privateKey
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
fs.writeFileSync(config.caCertPath, caCert.certificate);
|
|
232
|
+
fs.writeFileSync(config.certPath, hostCert.certificate);
|
|
233
|
+
fs.writeFileSync(config.keyPath, hostCert.privateKey);
|
|
234
|
+
if (verbose)
|
|
235
|
+
log.success(`SSL certificates generated for ${domain}`);
|
|
236
|
+
return !0;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
log.error(`Failed to generate certificates: ${error}`);
|
|
239
|
+
return !1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export async function setupSSL(options = {}) {
|
|
243
|
+
const domain = options.domain || process.env.APP_URL?.replace(/^https?:\/\//, "") || "stacks.localhost";
|
|
244
|
+
if (domain === "localhost" || domain.startsWith("localhost:")) {
|
|
245
|
+
log.info("Skipping SSL setup for localhost");
|
|
246
|
+
return !0;
|
|
247
|
+
}
|
|
248
|
+
let success = !0;
|
|
249
|
+
if (!options.skipHosts) {
|
|
250
|
+
if (!await addDomainToHosts(domain, options.verbose) && options.verbose)
|
|
251
|
+
log.warn("Hosts file not updated - you may need to add the entry manually");
|
|
252
|
+
}
|
|
253
|
+
if (!sslCertificatesExist(domain)) {
|
|
254
|
+
if (options.verbose)
|
|
255
|
+
log.info("Generating SSL certificates...");
|
|
256
|
+
if (!await generateCertificates(domain, options.verbose)) {
|
|
257
|
+
log.error("Failed to generate SSL certificates");
|
|
258
|
+
return !1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!options.skipTrust && !isCertificateTrusted(domain)) {
|
|
262
|
+
if (!await trustCertificate(domain, options.verbose)) {
|
|
263
|
+
log.warn("Certificate not trusted - you may see browser warnings");
|
|
264
|
+
success = !1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return success;
|
|
268
|
+
}
|
|
269
|
+
export function isSSLSetupComplete(domain) {
|
|
270
|
+
return isDomainInHosts(domain) && sslCertificatesExist(domain);
|
|
271
|
+
}
|
package/dist/stacks.js
CHANGED
|
@@ -1,2 +1,265 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import { log } from "@stacksjs/logging";
|
|
2
|
+
import { dirname, join, projectPath, relative, stacksBackupPath, stacksLockPath } from "@stacksjs/path";
|
|
3
|
+
import { copyFile, createFolder, deleteFile, deleteEmptyFolders, doesFolderExist, fs } from "@stacksjs/storage";
|
|
4
|
+
const STACK_DIRS = ["app", "config", "database", "resources", "routes", "public", "locales"];
|
|
5
|
+
async function readStackLock() {
|
|
6
|
+
const lockPath = stacksLockPath();
|
|
7
|
+
try {
|
|
8
|
+
if (fs.existsSync(lockPath)) {
|
|
9
|
+
const content = await Bun.file(lockPath).text();
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
} catch {}
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
generatedAt: new Date().toISOString(),
|
|
16
|
+
stacks: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function writeStackLock(lock) {
|
|
20
|
+
const lockPath = stacksLockPath(), lockDir = dirname(lockPath);
|
|
21
|
+
if (!fs.existsSync(lockDir))
|
|
22
|
+
await createFolder(lockDir);
|
|
23
|
+
lock.generatedAt = new Date().toISOString();
|
|
24
|
+
await Bun.write(lockPath, JSON.stringify(lock, null, 2));
|
|
25
|
+
}
|
|
26
|
+
function resolveStackPackagePath(name) {
|
|
27
|
+
const pantryDir = projectPath("pantry"), directPath = join(pantryDir, name);
|
|
28
|
+
if (fs.existsSync(join(directPath, "package.json")))
|
|
29
|
+
return directPath;
|
|
30
|
+
const scopedPath = join(pantryDir, "@stacksjs", name);
|
|
31
|
+
if (fs.existsSync(join(scopedPath, "package.json")))
|
|
32
|
+
return scopedPath;
|
|
33
|
+
if (name.startsWith("@")) {
|
|
34
|
+
const fullPath = join(pantryDir, name);
|
|
35
|
+
if (fs.existsSync(join(fullPath, "package.json")))
|
|
36
|
+
return fullPath;
|
|
37
|
+
}
|
|
38
|
+
const patterns = ["*/package.json", "@*/*/package.json"];
|
|
39
|
+
for (const pattern of patterns) {
|
|
40
|
+
const glob = new Bun.Glob(pattern);
|
|
41
|
+
for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
|
|
42
|
+
try {
|
|
43
|
+
if (JSON.parse(fs.readFileSync(file, "utf-8"))?.stacks?.name === name)
|
|
44
|
+
return dirname(file);
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function collectFiles(dir, baseDir) {
|
|
50
|
+
const files = [];
|
|
51
|
+
if (!fs.existsSync(dir))
|
|
52
|
+
return files;
|
|
53
|
+
const entries = fs.readdirSync(dir, { withFileTypes: !0 });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const fullPath = join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory())
|
|
57
|
+
files.push(...collectFiles(fullPath, baseDir));
|
|
58
|
+
else
|
|
59
|
+
files.push(relative(baseDir, fullPath));
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
export async function installStack(options) {
|
|
64
|
+
const { name, force = !1, dryRun = !1, verbose = !1 } = options, conflict = force ? "overwrite" : options.conflict ?? "skip";
|
|
65
|
+
log.info(`Installing stack "${name}"...`);
|
|
66
|
+
const stackPath = resolveStackPackagePath(name);
|
|
67
|
+
if (!stackPath) {
|
|
68
|
+
log.error(`Stack "${name}" not found in pantry. Install it first with: bun add <package-name>`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let pkg, meta;
|
|
72
|
+
try {
|
|
73
|
+
const pkgContent = await Bun.file(join(stackPath, "package.json")).text();
|
|
74
|
+
pkg = JSON.parse(pkgContent);
|
|
75
|
+
meta = pkg.stacks;
|
|
76
|
+
if (!meta?.name) {
|
|
77
|
+
log.error(`Package at "${stackPath}" does not have a valid "stacks" field in package.json`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
log.error(`Failed to read stack package.json: ${error}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const lock = await readStackLock(), packageName = pkg.name || name;
|
|
85
|
+
if (lock.stacks[packageName] && !force) {
|
|
86
|
+
log.warn(`Stack "${meta.name}" is already installed. Use --force to reinstall.`);
|
|
87
|
+
return lock.stacks[packageName];
|
|
88
|
+
}
|
|
89
|
+
const dirsToScan = meta.directories ?? STACK_DIRS, allFiles = [];
|
|
90
|
+
for (const dir of dirsToScan) {
|
|
91
|
+
const dirPath = join(stackPath, dir);
|
|
92
|
+
if (fs.existsSync(dirPath)) {
|
|
93
|
+
const files = collectFiles(dirPath, stackPath);
|
|
94
|
+
allFiles.push(...files);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (allFiles.length === 0) {
|
|
98
|
+
log.warn(`Stack "${meta.name}" has no files to install`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (verbose)
|
|
102
|
+
log.info(`Found ${allFiles.length} file(s) to install`);
|
|
103
|
+
const copiedFiles = [], skippedFiles = [];
|
|
104
|
+
for (const relPath of allFiles) {
|
|
105
|
+
const src = join(stackPath, relPath), dest = projectPath(relPath);
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
if (fs.existsSync(dest)) {
|
|
108
|
+
log.info(`[dry-run] Would ${conflict}: ${relPath}`);
|
|
109
|
+
if (conflict === "skip")
|
|
110
|
+
skippedFiles.push(relPath);
|
|
111
|
+
else
|
|
112
|
+
copiedFiles.push(relPath);
|
|
113
|
+
} else {
|
|
114
|
+
log.info(`[dry-run] Would copy: ${relPath}`);
|
|
115
|
+
copiedFiles.push(relPath);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (fs.existsSync(dest))
|
|
120
|
+
switch (conflict) {
|
|
121
|
+
case "skip":
|
|
122
|
+
if (verbose)
|
|
123
|
+
log.warn(`Skipping (file exists): ${relPath}`);
|
|
124
|
+
skippedFiles.push(relPath);
|
|
125
|
+
continue;
|
|
126
|
+
case "backup": {
|
|
127
|
+
const backupDir = stacksBackupPath(meta.name), backupDest = join(backupDir, relPath), backupDestDir = dirname(backupDest);
|
|
128
|
+
if (!fs.existsSync(backupDestDir))
|
|
129
|
+
await createFolder(backupDestDir);
|
|
130
|
+
copyFile(dest, backupDest);
|
|
131
|
+
if (verbose)
|
|
132
|
+
log.info(`Backed up: ${relPath}`);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "overwrite":
|
|
136
|
+
if (verbose)
|
|
137
|
+
log.warn(`Overwriting: ${relPath}`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const destDir = dirname(dest);
|
|
141
|
+
if (!fs.existsSync(destDir))
|
|
142
|
+
await createFolder(destDir);
|
|
143
|
+
copyFile(src, dest);
|
|
144
|
+
copiedFiles.push(relPath);
|
|
145
|
+
if (verbose)
|
|
146
|
+
log.info(`Copied: ${relPath}`);
|
|
147
|
+
}
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
log.info(`[dry-run] Would install ${copiedFiles.length} file(s), skip ${skippedFiles.length} file(s)`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const entry = {
|
|
153
|
+
packageName,
|
|
154
|
+
name: meta.name,
|
|
155
|
+
version: pkg.version || "0.0.0",
|
|
156
|
+
installedAt: new Date().toISOString(),
|
|
157
|
+
files: copiedFiles,
|
|
158
|
+
skipped: skippedFiles,
|
|
159
|
+
conflictStrategy: conflict
|
|
160
|
+
};
|
|
161
|
+
lock.stacks[packageName] = entry;
|
|
162
|
+
await writeStackLock(lock);
|
|
163
|
+
log.success(`Installed stack "${meta.name}" (${copiedFiles.length} file(s) copied, ${skippedFiles.length} skipped)`);
|
|
164
|
+
return entry;
|
|
165
|
+
}
|
|
166
|
+
export async function uninstallStack(options) {
|
|
167
|
+
const { name, force = !1, verbose = !1 } = options;
|
|
168
|
+
log.info(`Uninstalling stack "${name}"...`);
|
|
169
|
+
const lock = await readStackLock();
|
|
170
|
+
let entryKey = null, entry = null;
|
|
171
|
+
for (const [key, value] of Object.entries(lock.stacks))
|
|
172
|
+
if (value.name === name || key === name) {
|
|
173
|
+
entryKey = key;
|
|
174
|
+
entry = value;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (!entry || !entryKey) {
|
|
178
|
+
log.error(`Stack "${name}" is not installed`);
|
|
179
|
+
return !1;
|
|
180
|
+
}
|
|
181
|
+
let removedCount = 0;
|
|
182
|
+
for (const relPath of entry.files) {
|
|
183
|
+
const filePath = projectPath(relPath);
|
|
184
|
+
if (!fs.existsSync(filePath)) {
|
|
185
|
+
if (verbose)
|
|
186
|
+
log.info(`Already removed: ${relPath}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!force) {
|
|
190
|
+
const stackPath = resolveStackPackagePath(name);
|
|
191
|
+
if (stackPath) {
|
|
192
|
+
const srcPath = join(stackPath, relPath);
|
|
193
|
+
if (fs.existsSync(srcPath)) {
|
|
194
|
+
const srcContent = await Bun.file(srcPath).text(), destContent = await Bun.file(filePath).text();
|
|
195
|
+
if (srcContent !== destContent) {
|
|
196
|
+
log.warn(`Skipping modified file: ${relPath} (use --force to remove)`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await deleteFile(filePath);
|
|
203
|
+
removedCount++;
|
|
204
|
+
if (verbose)
|
|
205
|
+
log.info(`Removed: ${relPath}`);
|
|
206
|
+
}
|
|
207
|
+
const backupDir = stacksBackupPath(entry.name);
|
|
208
|
+
if (fs.existsSync(backupDir) && doesFolderExist(backupDir)) {
|
|
209
|
+
const backupFiles = collectFiles(backupDir, backupDir);
|
|
210
|
+
for (const relPath of backupFiles) {
|
|
211
|
+
const backupSrc = join(backupDir, relPath), dest = projectPath(relPath), destDir = dirname(dest);
|
|
212
|
+
if (!fs.existsSync(destDir))
|
|
213
|
+
await createFolder(destDir);
|
|
214
|
+
copyFile(backupSrc, dest);
|
|
215
|
+
if (verbose)
|
|
216
|
+
log.info(`Restored from backup: ${relPath}`);
|
|
217
|
+
}
|
|
218
|
+
fs.rmSync(backupDir, { recursive: !0, force: !0 });
|
|
219
|
+
}
|
|
220
|
+
for (const dir of STACK_DIRS) {
|
|
221
|
+
const dirPath = projectPath(dir);
|
|
222
|
+
if (fs.existsSync(dirPath))
|
|
223
|
+
await deleteEmptyFolders(dirPath);
|
|
224
|
+
}
|
|
225
|
+
delete lock.stacks[entryKey];
|
|
226
|
+
await writeStackLock(lock);
|
|
227
|
+
log.success(`Uninstalled stack "${entry.name}" (${removedCount} file(s) removed)`);
|
|
228
|
+
return !0;
|
|
229
|
+
}
|
|
230
|
+
export async function listStacks() {
|
|
231
|
+
const entries = [], lock = await readStackLock(), pantryDir = projectPath("pantry");
|
|
232
|
+
if (fs.existsSync(pantryDir)) {
|
|
233
|
+
const patterns = ["*/package.json", "@*/*/package.json"];
|
|
234
|
+
for (const pattern of patterns) {
|
|
235
|
+
const glob = new Bun.Glob(pattern);
|
|
236
|
+
for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
|
|
237
|
+
try {
|
|
238
|
+
const pkgContent = fs.readFileSync(file, "utf-8"), pkg = JSON.parse(pkgContent);
|
|
239
|
+
if (!pkg?.stacks?.name)
|
|
240
|
+
continue;
|
|
241
|
+
const packageName = pkg.name, installed = !!lock.stacks[packageName], lockEntry = lock.stacks[packageName];
|
|
242
|
+
entries.push({
|
|
243
|
+
name: pkg.stacks.name,
|
|
244
|
+
packageName,
|
|
245
|
+
version: pkg.version || "0.0.0",
|
|
246
|
+
description: pkg.stacks.description,
|
|
247
|
+
installed,
|
|
248
|
+
installedAt: lockEntry?.installedAt,
|
|
249
|
+
fileCount: lockEntry?.files.length
|
|
250
|
+
});
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [packageName, entry] of Object.entries(lock.stacks))
|
|
255
|
+
if (!entries.some((e) => e.packageName === packageName))
|
|
256
|
+
entries.push({
|
|
257
|
+
name: entry.name,
|
|
258
|
+
packageName,
|
|
259
|
+
version: entry.version,
|
|
260
|
+
installed: !0,
|
|
261
|
+
installedAt: entry.installedAt,
|
|
262
|
+
fileCount: entry.files.length
|
|
263
|
+
});
|
|
264
|
+
return entries;
|
|
265
|
+
}
|