@stacksjs/buddy 0.70.162 → 0.70.164
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/README.md +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/ai-context.d.ts +13 -0
- package/dist/commands/ai-context.js +29 -0
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.d.ts +11 -1
- package/dist/commands/deploy.js +29 -28
- package/dist/commands/dev.js +44 -14
- package/dist/commands/docs/buddy-commands.d.ts +8 -0
- package/dist/commands/docs/buddy-commands.js +116 -0
- package/dist/commands/docs/buddy-commands.test.d.ts +1 -0
- package/dist/commands/docs/buddy-commands.test.js +38 -0
- package/dist/commands/docs/generated-artifacts.d.ts +3 -0
- package/dist/commands/docs/generated-artifacts.js +69 -0
- package/dist/commands/docs/generated-artifacts.test.d.ts +1 -0
- package/dist/commands/docs/generated-artifacts.test.js +19 -0
- package/dist/commands/docs/links.d.ts +18 -0
- package/dist/commands/docs/links.js +83 -0
- package/dist/commands/docs/links.test.d.ts +1 -0
- package/dist/commands/docs/links.test.js +73 -0
- package/dist/commands/docs.d.ts +7 -0
- package/dist/commands/docs.js +24 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/env.d.ts +14 -0
- package/dist/commands/env.js +73 -7
- package/dist/commands/generate.js +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/mail.js +1 -1
- package/dist/commands/migrate.js +9 -7
- package/dist/commands/protocol/craft-evidence.d.ts +76 -0
- package/dist/commands/protocol/craft-evidence.js +98 -0
- package/dist/commands/protocol/craft-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/craft-evidence.test.js +20 -0
- package/dist/commands/protocol/desktop-lifecycle-report.d.ts +16 -0
- package/dist/commands/protocol/desktop-lifecycle-report.js +57 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +32 -0
- package/dist/commands/protocol/desktop-support.d.ts +1 -0
- package/dist/commands/protocol/desktop-support.js +32 -0
- package/dist/commands/protocol/desktop-workflow.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-workflow.test.js +33 -0
- package/dist/commands/protocol/driver-registry.d.ts +1 -0
- package/dist/commands/protocol/driver-registry.js +71 -0
- package/dist/commands/protocol/pantry-evidence.d.ts +67 -0
- package/dist/commands/protocol/pantry-evidence.js +144 -0
- package/dist/commands/protocol/pantry-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/pantry-evidence.test.js +29 -0
- package/dist/commands/protocol/release-manifest.d.ts +30 -0
- package/dist/commands/protocol/release-manifest.js +102 -0
- package/dist/commands/protocol/release-manifest.test.d.ts +1 -0
- package/dist/commands/protocol/release-manifest.test.js +34 -0
- package/dist/commands/protocol/run-conformance.d.ts +21 -0
- package/dist/commands/protocol/run-conformance.js +358 -0
- package/dist/commands/protocol/run-conformance.test.d.ts +1 -0
- package/dist/commands/protocol/run-conformance.test.js +52 -0
- package/dist/commands/protocol/run-driver-contracts.d.ts +7 -0
- package/dist/commands/protocol/run-driver-contracts.js +94 -0
- package/dist/commands/protocol/run-driver-contracts.test.d.ts +1 -0
- package/dist/commands/protocol/run-driver-contracts.test.js +18 -0
- package/dist/commands/protocol/source-manifest.d.ts +25 -0
- package/dist/commands/protocol/source-manifest.js +113 -0
- package/dist/commands/protocol/sync-suite.d.ts +8 -0
- package/dist/commands/protocol/sync-suite.js +137 -0
- package/dist/commands/protocol/sync-suite.test.d.ts +1 -0
- package/dist/commands/protocol/sync-suite.test.js +44 -0
- package/dist/commands/protocol.d.ts +8 -0
- package/dist/commands/protocol.js +60 -0
- package/dist/commands/publish.js +149 -0
- package/dist/commands/run-tool.d.ts +7 -0
- package/dist/commands/run-tool.js +9 -0
- package/dist/commands/seed.js +15 -40
- package/dist/commands/serve.d.ts +18 -5
- package/dist/commands/serve.js +21 -2
- package/dist/commands/setup-ai.d.ts +39 -0
- package/dist/commands/setup-ai.js +121 -0
- package/dist/commands/setup.js +22 -0
- package/dist/lazy-commands.js +28 -1
- package/dist/project-setup.js +1 -0
- package/package.json +43 -43
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), docsDir = resolve(root, "docs"), INLINE_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
4
|
+
export function isSkippableLink(target) {
|
|
5
|
+
return target === "" || target.startsWith("#") || /^[a-z][\w+.-]*:/i.test(target) || target.startsWith("//") || target.startsWith("{{") || target.includes("<");
|
|
6
|
+
}
|
|
7
|
+
export function extractLinks(content) {
|
|
8
|
+
const out = [], lines = content.replace(/<!--[\s\S]*?-->/g, (match) => match.replace(/[^\n]/g, " ")).split(`
|
|
9
|
+
`);
|
|
10
|
+
let inFence = !1;
|
|
11
|
+
for (let index = 0;index < lines.length; index++) {
|
|
12
|
+
const raw = lines[index];
|
|
13
|
+
if (/^\s*(```|~~~)/.test(raw)) {
|
|
14
|
+
inFence = !inFence;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (inFence)
|
|
18
|
+
continue;
|
|
19
|
+
const line = raw.replace(/`[^`]*`/g, "");
|
|
20
|
+
for (const match of line.matchAll(INLINE_LINK)) {
|
|
21
|
+
let target = match[1].trim();
|
|
22
|
+
const space = target.search(/\s/);
|
|
23
|
+
if (space !== -1)
|
|
24
|
+
target = target.slice(0, space);
|
|
25
|
+
out.push({ target, line: index + 1 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function resolveCandidates(target, fileDir, docsRoot) {
|
|
31
|
+
const clean = target.split("#")[0].split("?")[0];
|
|
32
|
+
if (!clean)
|
|
33
|
+
return [];
|
|
34
|
+
const base = clean.startsWith("/") ? join(docsRoot, clean.slice(1)) : resolve(fileDir, clean), candidates = [base];
|
|
35
|
+
if (!/\.\w+$/.test(clean))
|
|
36
|
+
candidates.push(`${base}.md`, join(base, "index.md"));
|
|
37
|
+
else if (clean.endsWith(".html"))
|
|
38
|
+
candidates.push(base.replace(/\.html$/, ".md"), join(base.replace(/\.html$/, ""), "index.md"));
|
|
39
|
+
return candidates;
|
|
40
|
+
}
|
|
41
|
+
function walkMarkdown(dir) {
|
|
42
|
+
const files = [];
|
|
43
|
+
for (const entry of readdirSync(dir, { withFileTypes: !0 })) {
|
|
44
|
+
const full = join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
47
|
+
continue;
|
|
48
|
+
files.push(...walkMarkdown(full));
|
|
49
|
+
} else if (entry.name.endsWith(".md"))
|
|
50
|
+
files.push(full);
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
export function checkDocsLinks(docsRoot = docsDir) {
|
|
55
|
+
const broken = [];
|
|
56
|
+
for (const file of walkMarkdown(docsRoot)) {
|
|
57
|
+
const content = readFileSync(file, "utf8");
|
|
58
|
+
for (const { target, line } of extractLinks(content)) {
|
|
59
|
+
if (isSkippableLink(target))
|
|
60
|
+
continue;
|
|
61
|
+
const candidates = resolveCandidates(target, dirname(file), docsRoot);
|
|
62
|
+
if (candidates.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
if (!candidates.some((candidate) => existsSync(candidate) && statSync(candidate).isFile()))
|
|
65
|
+
broken.push({ file: relative(docsRoot, file), line, target });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return broken;
|
|
69
|
+
}
|
|
70
|
+
export async function run() {
|
|
71
|
+
const broken = checkDocsLinks();
|
|
72
|
+
if (broken.length === 0)
|
|
73
|
+
console.log("\u2713 All internal documentation links resolve.");
|
|
74
|
+
else {
|
|
75
|
+
console.error(`\u2717 ${broken.length} broken internal documentation link(s):`);
|
|
76
|
+
for (const link of broken)
|
|
77
|
+
console.error(` ${link.file}:${link.line} -> ${link.target}`);
|
|
78
|
+
if (process.argv.includes("--check"))
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (import.meta.main)
|
|
83
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { extractLinks, isSkippableLink, resolveCandidates } from "./links";
|
|
3
|
+
describe("docs link checker (stacksjs/stacks#2056)", () => {
|
|
4
|
+
describe("isSkippableLink", () => {
|
|
5
|
+
it("skips external, mail/tel, anchors, protocol-relative, and templated links", () => {
|
|
6
|
+
for (const target of ["https://x.com", "http://x.com", "mailto:a@b.c", "tel:123", "#section", "//cdn.example.com/x", "{{ url }}", "data:image/png;base64,AAAA", ""])
|
|
7
|
+
expect(isSkippableLink(target)).toBe(!0);
|
|
8
|
+
});
|
|
9
|
+
it("does not skip internal relative or absolute links", () => {
|
|
10
|
+
for (const target of ["./foo.md", "../bar/baz.md", "/guide/intro", "sibling.md"])
|
|
11
|
+
expect(isSkippableLink(target)).toBe(!1);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe("extractLinks", () => {
|
|
15
|
+
it("extracts inline links with 1-based line numbers", () => {
|
|
16
|
+
expect(extractLinks(`intro
|
|
17
|
+
|
|
18
|
+
see [a](./a.md) and [b](../b.md)
|
|
19
|
+
`)).toEqual([
|
|
20
|
+
{ target: "./a.md", line: 3 },
|
|
21
|
+
{ target: "../b.md", line: 3 }
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
it("strips an optional link title", () => {
|
|
25
|
+
expect(extractLinks('[x](/guide/x "The X page")')).toEqual([{ target: "/guide/x", line: 1 }]);
|
|
26
|
+
});
|
|
27
|
+
it("ignores links inside fenced code blocks", () => {
|
|
28
|
+
expect(extractLinks("```md\n[x](./nope.md)\n```\n[y](./yes.md)\n")).toEqual([{ target: "./yes.md", line: 4 }]);
|
|
29
|
+
});
|
|
30
|
+
it("ignores links inside inline code spans", () => {
|
|
31
|
+
expect(extractLinks("use `[x](./nope.md)` but link [y](./yes.md)")).toEqual([{ target: "./yes.md", line: 1 }]);
|
|
32
|
+
});
|
|
33
|
+
it("ignores links inside HTML comments while preserving line numbers", () => {
|
|
34
|
+
expect(extractLinks(`line1
|
|
35
|
+
<!--  -->
|
|
36
|
+
real [y](./y.md)
|
|
37
|
+
`)).toEqual([{ target: "./y.md", line: 3 }]);
|
|
38
|
+
});
|
|
39
|
+
it("handles a multi-line HTML comment", () => {
|
|
40
|
+
expect(extractLinks(`<!--
|
|
41
|
+
[x](./a.md)
|
|
42
|
+
-->
|
|
43
|
+
[y](./b.md)
|
|
44
|
+
`)).toEqual([{ target: "./b.md", line: 4 }]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("resolveCandidates", () => {
|
|
48
|
+
const docsRoot = "/docs", fileDir = "/docs/guide/components";
|
|
49
|
+
it("resolves a relative link against the file directory", () => {
|
|
50
|
+
expect(resolveCandidates("./tabs.md", fileDir, docsRoot)).toEqual(["/docs/guide/components/tabs.md"]);
|
|
51
|
+
});
|
|
52
|
+
it("resolves an absolute link against the docs root", () => {
|
|
53
|
+
expect(resolveCandidates("/guide/intro.md", fileDir, docsRoot)).toEqual(["/docs/guide/intro.md"]);
|
|
54
|
+
});
|
|
55
|
+
it("offers .md and index.md candidates for an extensionless (clean-URL) link", () => {
|
|
56
|
+
expect(resolveCandidates("../intro", fileDir, docsRoot)).toEqual([
|
|
57
|
+
"/docs/guide/intro",
|
|
58
|
+
"/docs/guide/intro.md",
|
|
59
|
+
"/docs/guide/intro/index.md"
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it("offers the .md source for an .html link", () => {
|
|
63
|
+
const candidates = resolveCandidates("./api.html", fileDir, docsRoot);
|
|
64
|
+
expect(candidates).toContain("/docs/guide/components/api.md");
|
|
65
|
+
});
|
|
66
|
+
it("strips the anchor before resolving", () => {
|
|
67
|
+
expect(resolveCandidates("./tabs.md#usage", fileDir, docsRoot)).toEqual(["/docs/guide/components/tabs.md"]);
|
|
68
|
+
});
|
|
69
|
+
it("returns no candidates for a pure anchor", () => {
|
|
70
|
+
expect(resolveCandidates("#usage", fileDir, docsRoot)).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CLI } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Framework-repo `buddy docs:*` commands wrapping the documentation-freshness
|
|
4
|
+
* tooling under `commands/docs/`: the generated buddy command reference,
|
|
5
|
+
* generated API artifacts (OpenAPI + types), and internal link checking.
|
|
6
|
+
*/
|
|
7
|
+
export declare function docs(buddy: CLI): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { run as runArtifacts } from "./docs/generated-artifacts";
|
|
2
|
+
import { run as runBuddyDocs } from "./docs/buddy-commands";
|
|
3
|
+
import { run as runLinks } from "./docs/links";
|
|
4
|
+
import { runTool } from "./run-tool";
|
|
5
|
+
export function docs(buddy) {
|
|
6
|
+
buddy.command("docs:buddy", "Regenerate the buddy command reference doc").action(async () => {
|
|
7
|
+
await runTool(runBuddyDocs, "--write");
|
|
8
|
+
});
|
|
9
|
+
buddy.command("docs:buddy:check", "Verify the buddy command reference doc is current").action(async () => {
|
|
10
|
+
await runTool(runBuddyDocs, "--check");
|
|
11
|
+
});
|
|
12
|
+
buddy.command("docs:artifacts", "Regenerate the generated API artifacts (OpenAPI + types)").action(async () => {
|
|
13
|
+
await runTool(runArtifacts, "--write");
|
|
14
|
+
});
|
|
15
|
+
buddy.command("docs:artifacts:check", "Verify the generated API artifacts are current").action(async () => {
|
|
16
|
+
await runTool(runArtifacts, "--check");
|
|
17
|
+
});
|
|
18
|
+
buddy.command("docs:links", "Report internal documentation links").action(async () => {
|
|
19
|
+
await runTool(runLinks);
|
|
20
|
+
});
|
|
21
|
+
buddy.command("docs:links:check", "Verify internal documentation links resolve").action(async () => {
|
|
22
|
+
await runTool(runLinks, "--check");
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -185,6 +185,14 @@ export function doctor(buddy) {
|
|
|
185
185
|
const { config } = await import("@stacksjs/config");
|
|
186
186
|
return `Driver: ${config.email?.default ?? "log"}`;
|
|
187
187
|
});
|
|
188
|
+
{
|
|
189
|
+
const { ensureRuntimeDirectories, projectPath } = await import("@stacksjs/path"), leftover = ensureRuntimeDirectories().filter((entry) => !entry.cleared);
|
|
190
|
+
checks.push(leftover.length === 0 ? { name: "Runtime directories", status: "pass", message: "All under storage/" } : {
|
|
191
|
+
name: "Runtime directories",
|
|
192
|
+
status: "warn",
|
|
193
|
+
message: `Still in the project root: ${leftover.map((entry) => entry.legacy.slice(projectPath().length + 1)).join(", ")} \u2014 remove them so state stays under storage/`
|
|
194
|
+
});
|
|
195
|
+
}
|
|
188
196
|
const hasAwsCreds = Boolean(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY), hasAwsRole = Boolean(process.env.AWS_PROFILE) || Boolean(process.env.AWS_ROLE_ARN);
|
|
189
197
|
if (hasAwsCreds || hasAwsRole)
|
|
190
198
|
checks.push({
|
package/dist/commands/env.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
import type { CLI } from '@stacksjs/types';
|
|
2
2
|
export declare function env(buddy: CLI): void;
|
|
3
|
+
/**
|
|
4
|
+
* Parses `KEY=value` assignments out of a dotenv file into a flat map.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately tolerant of what real `.env` files contain: comments, blank
|
|
7
|
+
* lines, `export ` prefixes, quoted values, and - the case that matters here -
|
|
8
|
+
* dotenvx ciphertext, which wraps across many lines inside its quotes. A naive
|
|
9
|
+
* `split('\n')` treats each wrapped fragment as its own line and loses the key
|
|
10
|
+
* it belongs to.
|
|
11
|
+
*
|
|
12
|
+
* Values are returned raw (still encrypted, if they were). `env:check` only
|
|
13
|
+
* needs the key names and whether a value is non-empty, so nothing here has to
|
|
14
|
+
* decrypt - which also means it works without the private key present.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseEnvAssignments(content: string): Record<string, string>;
|
package/dist/commands/env.js
CHANGED
|
@@ -135,16 +135,14 @@ export function env(buddy) {
|
|
|
135
135
|
message: "Found"
|
|
136
136
|
});
|
|
137
137
|
try {
|
|
138
|
-
const envContent = await storage.readTextFile(envPath),
|
|
139
|
-
`).filter((line) => line.trim() && !line.startsWith("#")), varCount = lines.length;
|
|
138
|
+
const envContent = await storage.readTextFile(envPath), contentStr = typeof envContent === "string" ? envContent : envContent.data, values = parseEnvAssignments(contentStr), keys = Object.keys(values), varCount = keys.length;
|
|
140
139
|
checks.push({
|
|
141
140
|
name: "Environment variables",
|
|
142
141
|
status: "pass",
|
|
143
142
|
message: `${varCount} variables defined`
|
|
144
143
|
});
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
if (appKeyValue && appKeyValue.length > 0)
|
|
144
|
+
if ("APP_KEY" in values)
|
|
145
|
+
if ((values.APP_KEY ?? "").length > 0)
|
|
148
146
|
checks.push({
|
|
149
147
|
name: "APP_KEY",
|
|
150
148
|
status: "pass",
|
|
@@ -156,13 +154,13 @@ export function env(buddy) {
|
|
|
156
154
|
status: "warn",
|
|
157
155
|
message: "Empty (run: buddy key:generate)"
|
|
158
156
|
});
|
|
159
|
-
|
|
157
|
+
else
|
|
160
158
|
checks.push({
|
|
161
159
|
name: "APP_KEY",
|
|
162
160
|
status: "warn",
|
|
163
161
|
message: "Not found (run: buddy key:generate)"
|
|
164
162
|
});
|
|
165
|
-
const hasPublicKey =
|
|
163
|
+
const hasPublicKey = keys.some((key) => key.startsWith("DOTENV_PUBLIC_KEY")), hasPrivateKey = keys.some((key) => key.startsWith("DOTENV_PRIVATE_KEY")) || existsSync(resolve(process.cwd(), ".env.keys"));
|
|
166
164
|
if (hasPublicKey && hasPrivateKey)
|
|
167
165
|
checks.push({
|
|
168
166
|
name: "Encryption keys",
|
|
@@ -181,6 +179,41 @@ export function env(buddy) {
|
|
|
181
179
|
status: "warn",
|
|
182
180
|
message: "Not configured (optional)"
|
|
183
181
|
});
|
|
182
|
+
const declaredTenants = await resolveDeclaredTenants();
|
|
183
|
+
if (declaredTenants.tenants.length === 0)
|
|
184
|
+
checks.push({
|
|
185
|
+
name: "Tenant isolation",
|
|
186
|
+
status: "pass",
|
|
187
|
+
message: "No tenants declared (cloud.tenants)"
|
|
188
|
+
});
|
|
189
|
+
else {
|
|
190
|
+
const { foreignTenantKeys, partitionTenantEnv } = await import("@stacksjs/env"), foreign = foreignTenantKeys(partitionTenantEnv(values, declaredTenants)), isShipped = /^\.env\.[a-z]+$/.test(envFile), total = foreign.reduce((sum, entry) => sum + entry.keys.length, 0);
|
|
191
|
+
if (foreign.length === 0)
|
|
192
|
+
checks.push({
|
|
193
|
+
name: "Tenant isolation",
|
|
194
|
+
status: "pass",
|
|
195
|
+
message: `No foreign keys (checked ${declaredTenants.tenants.join(", ")})`
|
|
196
|
+
});
|
|
197
|
+
else if (!isShipped)
|
|
198
|
+
checks.push({
|
|
199
|
+
name: "Tenant isolation",
|
|
200
|
+
status: "pass",
|
|
201
|
+
message: `${total} archived key(s) \u2014 ${envFile} is local-only and never deployed`
|
|
202
|
+
});
|
|
203
|
+
else {
|
|
204
|
+
checks.push({
|
|
205
|
+
name: "Tenant isolation",
|
|
206
|
+
status: "warn",
|
|
207
|
+
message: `${total} key(s) belong to another tenant \u2014 move them to .env`
|
|
208
|
+
});
|
|
209
|
+
for (const { tenant, keys } of foreign)
|
|
210
|
+
checks.push({
|
|
211
|
+
name: ` ${tenant}`,
|
|
212
|
+
status: "warn",
|
|
213
|
+
message: keys.join(", ")
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
184
217
|
} catch (error) {
|
|
185
218
|
checks.push({
|
|
186
219
|
name: `${envFile} content`,
|
|
@@ -244,3 +277,36 @@ export function env(buddy) {
|
|
|
244
277
|
});
|
|
245
278
|
onUnknownSubcommand(buddy, "env");
|
|
246
279
|
}
|
|
280
|
+
async function resolveDeclaredTenants() {
|
|
281
|
+
try {
|
|
282
|
+
const { config } = await import("@stacksjs/config"), cloud = config.cloud, app = config.app, tenants = Array.isArray(cloud?.tenants) ? cloud.tenants.filter((slug) => typeof slug === "string") : [];
|
|
283
|
+
return { self: app?.name, tenants };
|
|
284
|
+
} catch {
|
|
285
|
+
return { tenants: [] };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export function parseEnvAssignments(content) {
|
|
289
|
+
const values = {}, assignment = /^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i, lines = content.split(/\r?\n/);
|
|
290
|
+
for (let i = 0;i < lines.length; i++) {
|
|
291
|
+
const line = lines[i];
|
|
292
|
+
if (!line.trim() || line.trimStart().startsWith("#"))
|
|
293
|
+
continue;
|
|
294
|
+
const match = line.match(assignment);
|
|
295
|
+
if (!match)
|
|
296
|
+
continue;
|
|
297
|
+
const [, key, first] = match;
|
|
298
|
+
let raw = first.trim();
|
|
299
|
+
const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : void 0;
|
|
300
|
+
if (quote) {
|
|
301
|
+
raw = raw.slice(1);
|
|
302
|
+
while (!raw.endsWith(quote) && i + 1 < lines.length) {
|
|
303
|
+
i++;
|
|
304
|
+
raw += lines[i];
|
|
305
|
+
}
|
|
306
|
+
raw = raw.endsWith(quote) ? raw.slice(0, -1) : raw;
|
|
307
|
+
} else
|
|
308
|
+
raw = raw.replace(/\s+#.*$/, "").trim();
|
|
309
|
+
values[key] = raw;
|
|
310
|
+
}
|
|
311
|
+
return values;
|
|
312
|
+
}
|
|
@@ -96,7 +96,7 @@ export function generate(buddy) {
|
|
|
96
96
|
process.exit(ExitCode.FatalError);
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
|
-
buddy.command("generate:core-symlink", "
|
|
99
|
+
buddy.command("generate:core-symlink", "Symlink `.framework` -> storage/framework. A shortcut for core developers.").action(async (options) => {
|
|
100
100
|
log.debug("Running `buddy core-symlink` ...", options);
|
|
101
101
|
await generateCoreSymlink();
|
|
102
102
|
});
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './create';
|
|
|
13
13
|
export * from './deploy';
|
|
14
14
|
export * from './dev';
|
|
15
15
|
export * from './dns';
|
|
16
|
+
export * from './docs';
|
|
16
17
|
export * from './doctor';
|
|
17
18
|
export * from './domains';
|
|
18
19
|
export * from './email';
|
|
@@ -34,6 +35,7 @@ export * from './phone';
|
|
|
34
35
|
export * from './ports';
|
|
35
36
|
export * from './prepublish';
|
|
36
37
|
export * from './projects';
|
|
38
|
+
export * from './protocol';
|
|
37
39
|
export * from './publish';
|
|
38
40
|
export * from './queue';
|
|
39
41
|
export * from './release';
|
package/dist/commands/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./create";
|
|
|
13
13
|
export * from "./deploy";
|
|
14
14
|
export * from "./dev";
|
|
15
15
|
export * from "./dns";
|
|
16
|
+
export * from "./docs";
|
|
16
17
|
export * from "./doctor";
|
|
17
18
|
export * from "./domains";
|
|
18
19
|
export * from "./email";
|
|
@@ -34,6 +35,7 @@ export * from "./phone";
|
|
|
34
35
|
export * from "./ports";
|
|
35
36
|
export * from "./prepublish";
|
|
36
37
|
export * from "./projects";
|
|
38
|
+
export * from "./protocol";
|
|
37
39
|
export * from "./publish";
|
|
38
40
|
export * from "./queue";
|
|
39
41
|
export * from "./release";
|
package/dist/commands/mail.js
CHANGED
|
@@ -17,7 +17,7 @@ export function sanitizeLineCount(value) {
|
|
|
17
17
|
const count = Number.parseInt(value || "50", 10);
|
|
18
18
|
return String(Number.isFinite(count) ? Math.min(5000, Math.max(1, count)) : 50);
|
|
19
19
|
}
|
|
20
|
-
function shellQuote(
|
|
20
|
+
function shellQuote(value) {
|
|
21
21
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
22
22
|
}
|
|
23
23
|
async function resolveOperationalMailHost() {
|
package/dist/commands/migrate.js
CHANGED
|
@@ -3,7 +3,7 @@ import process from "node:process";
|
|
|
3
3
|
import { confirm, intro, log, onUnknownSubcommand, outro, text } from "@stacksjs/cli";
|
|
4
4
|
import { Action } from "@stacksjs/enums";
|
|
5
5
|
import { hasTTY, isCI } from "@stacksjs/env";
|
|
6
|
-
import { appPath, frameworkPath, projectPath } from "@stacksjs/path";
|
|
6
|
+
import { appPath, frameworkPath, frameworkRuntimePath, projectPath } from "@stacksjs/path";
|
|
7
7
|
import { ExitCode } from "@stacksjs/types";
|
|
8
8
|
let _runAction;
|
|
9
9
|
async function runAction(...args) {
|
|
@@ -12,7 +12,7 @@ async function runAction(...args) {
|
|
|
12
12
|
return _runAction(...args);
|
|
13
13
|
}
|
|
14
14
|
function acquireMigrationLock() {
|
|
15
|
-
const lockDir =
|
|
15
|
+
const lockDir = frameworkRuntimePath(), lockFile = `${lockDir}/migrations.lock`;
|
|
16
16
|
try {
|
|
17
17
|
if (!existsSync(lockDir))
|
|
18
18
|
mkdirSync(lockDir, { recursive: !0 });
|
|
@@ -36,7 +36,7 @@ function acquireMigrationLock() {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function readMigrateMarker() {
|
|
39
|
-
const file =
|
|
39
|
+
const file = frameworkRuntimePath("last-migrate-result.json");
|
|
40
40
|
if (!existsSync(file))
|
|
41
41
|
return null;
|
|
42
42
|
try {
|
|
@@ -214,7 +214,7 @@ export function migrate(buddy) {
|
|
|
214
214
|
log.info(` \u2022 ${describeOp(op)}${op.destructive ? " [destructive]" : ""}`);
|
|
215
215
|
}
|
|
216
216
|
} catch (error) {
|
|
217
|
-
log.error("Failed to preview migrations:", error);
|
|
217
|
+
await log.error("Failed to preview migrations:", error);
|
|
218
218
|
}
|
|
219
219
|
await outro("Diff complete \u2014 no changes applied.", { startTime: perf, useSeconds: !0 });
|
|
220
220
|
process.exit(ExitCode.Success);
|
|
@@ -224,6 +224,7 @@ export function migrate(buddy) {
|
|
|
224
224
|
log.debug("[migrate] confirmMigrate guard skipped \u2014 non-interactive environment.");
|
|
225
225
|
else {
|
|
226
226
|
const APP_ENV = process.env.APP_ENV || "local";
|
|
227
|
+
await log.flush();
|
|
227
228
|
if (!await confirm({
|
|
228
229
|
message: `Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,
|
|
229
230
|
initial: !0
|
|
@@ -234,7 +235,7 @@ export function migrate(buddy) {
|
|
|
234
235
|
}
|
|
235
236
|
const lock = acquireMigrationLock();
|
|
236
237
|
if (!lock.acquired) {
|
|
237
|
-
log.
|
|
238
|
+
log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");
|
|
238
239
|
process.exit(ExitCode.FatalError);
|
|
239
240
|
}
|
|
240
241
|
if (!await confirmDestructiveMigrations({ force: options.force, fromDb: options.fromDb, applyRenames })) {
|
|
@@ -303,7 +304,7 @@ export function migrate(buddy) {
|
|
|
303
304
|
}
|
|
304
305
|
const guards = await resolveMigrationGuards(), dbLabel = currentDatabaseLabel(), APP_ENV = process.env.APP_ENV || "local";
|
|
305
306
|
if (guards.migrateFresh === "disabled") {
|
|
306
|
-
log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
307
|
+
await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
307
308
|
Target: ${APP_ENV} database "${dbLabel}"
|
|
308
309
|
To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
|
|
309
310
|
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);
|
|
@@ -313,11 +314,12 @@ export function migrate(buddy) {
|
|
|
313
314
|
if (!(guards.migrateFresh === "allow" && options.force === !0)) {
|
|
314
315
|
if (isCI || !hasTTY) {
|
|
315
316
|
const hint = guards.migrateFresh === "confirm" ? 'Guard is "confirm": migrate:fresh must be run interactively.' : "Re-run with --force to drop the database non-interactively.";
|
|
316
|
-
log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);
|
|
317
|
+
await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);
|
|
317
318
|
await outro("migrate:fresh cancelled.", { startTime: perf, useSeconds: !0 });
|
|
318
319
|
process.exit(ExitCode.FatalError);
|
|
319
320
|
}
|
|
320
321
|
log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);
|
|
322
|
+
await log.flush();
|
|
321
323
|
if ((await text({ message: `Type the database name "${dbLabel}" to confirm (blank to cancel):` })).trim() !== dbLabel) {
|
|
322
324
|
await outro("migrate:fresh cancelled \u2014 confirmation did not match.", { startTime: perf, useSeconds: !0 });
|
|
323
325
|
process.exit(ExitCode.Success);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare function validateCraftEvidence(evidence: typeof craftEvidence): string[];
|
|
2
|
+
export declare function run(): Promise<void>;
|
|
3
|
+
export declare const craftEvidence: {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
status: 'source-verified-ci-retained';
|
|
6
|
+
source: {
|
|
7
|
+
repository: 'https://github.com/home-lang/craft';
|
|
8
|
+
version: '0.0.48';
|
|
9
|
+
tag: 'v0.0.48';
|
|
10
|
+
revision: 'bf75807669e3af6cfb4ae34724be5c613442aee2'
|
|
11
|
+
};
|
|
12
|
+
issues: {
|
|
13
|
+
support: 'https://github.com/stacksjs/stacks/issues/2059';
|
|
14
|
+
signing: 'https://github.com/stacksjs/stacks/issues/2062';
|
|
15
|
+
lifecycle: 'https://github.com/stacksjs/stacks/issues/2063';
|
|
16
|
+
upstreamActions: 'https://github.com/home-lang/craft/issues/11';
|
|
17
|
+
dmgContention: 'https://github.com/home-lang/craft/issues/16'
|
|
18
|
+
};
|
|
19
|
+
contracts: readonly [{
|
|
20
|
+
path: 'packages/typescript/src/package.ts';
|
|
21
|
+
sha256: 'a7522f8c79dbd50a0f3bbc113525fb46eb8d6d1bab711d434cae85dc892ccdc1'
|
|
22
|
+
}, {
|
|
23
|
+
path: 'packages/typescript/src/package.test.ts';
|
|
24
|
+
sha256: '673338f3793e397a83590daf84815869b0698b4afe97eaad8039426418554b7a'
|
|
25
|
+
}, {
|
|
26
|
+
path: 'scripts/native-lifecycle.ts';
|
|
27
|
+
sha256: 'a2c0af2c2165981ca7fbd31880adeec64db64fc305d4ca3efe62a11483b289b4'
|
|
28
|
+
}, {
|
|
29
|
+
path: 'scripts/native-lifecycle-plan.ts';
|
|
30
|
+
sha256: 'ac8e8f101246e94c0c26fdcbf9202536426bf192a303606079273d9689e4e27d'
|
|
31
|
+
}, {
|
|
32
|
+
path: 'scripts/native-lifecycle-plan.test.ts';
|
|
33
|
+
sha256: '5cc839b60f7c8e32716f29201b159af817f4ac40b2c7963131124b3ac5de0638'
|
|
34
|
+
}, {
|
|
35
|
+
path: '.github/workflows/native-lifecycle.yml';
|
|
36
|
+
sha256: '493b614554f03dc669724ff31bd56ae8361851dd2a9b32ac37560d9c0bb1b85e'
|
|
37
|
+
}, {
|
|
38
|
+
path: '.github/workflows/release.yml';
|
|
39
|
+
sha256: 'e3c7edd45841a7260fb4dfe810f0ff3f63f3117f7d0f4d14984e4dfb36b1f638'
|
|
40
|
+
}];
|
|
41
|
+
lifecycleMatrix: readonly [{
|
|
42
|
+
runner: 'macos-15';
|
|
43
|
+
platform: 'darwin';
|
|
44
|
+
architecture: 'arm64';
|
|
45
|
+
formats: readonly ['dmg', 'pkg']
|
|
46
|
+
}, {
|
|
47
|
+
runner: 'macos-15-intel';
|
|
48
|
+
platform: 'darwin';
|
|
49
|
+
architecture: 'x64';
|
|
50
|
+
formats: readonly ['dmg', 'pkg']
|
|
51
|
+
}, {
|
|
52
|
+
runner: 'ubuntu-24.04';
|
|
53
|
+
platform: 'linux';
|
|
54
|
+
architecture: 'x64';
|
|
55
|
+
formats: readonly ['deb']
|
|
56
|
+
}, {
|
|
57
|
+
runner: 'windows-2025';
|
|
58
|
+
platform: 'win32';
|
|
59
|
+
architecture: 'x64';
|
|
60
|
+
formats: readonly ['msi', 'zip']
|
|
61
|
+
}];
|
|
62
|
+
lifecycleOperations: readonly ['install', 'launch', 'update', 'rollback', 'uninstall'];
|
|
63
|
+
stacksWorkflow: 'https://github.com/stacksjs/stacks/actions/workflows/desktop-lifecycle.yml';
|
|
64
|
+
release: {
|
|
65
|
+
sourceTagged: true;
|
|
66
|
+
githubRelease: null;
|
|
67
|
+
packagePublished: false;
|
|
68
|
+
signing: 'pending';
|
|
69
|
+
notarization: 'pending';
|
|
70
|
+
blockedBy: 'https://github.com/home-lang/craft/issues/11'
|
|
71
|
+
};
|
|
72
|
+
verification: {
|
|
73
|
+
local: readonly ['bun test packages/typescript/src/package.test.ts scripts/native-lifecycle-plan.test.ts', 'bun run typecheck', 'bun .github/scripts/native-lifecycle.ts'];
|
|
74
|
+
retained: 'https://github.com/stacksjs/stacks/actions/runs/29890804159'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, ".github/protocol/evidence/craft.json");
|
|
4
|
+
export const craftEvidence = {
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
status: "source-verified-ci-retained",
|
|
7
|
+
source: {
|
|
8
|
+
repository: "https://github.com/home-lang/craft",
|
|
9
|
+
version: "0.0.48",
|
|
10
|
+
tag: "v0.0.48",
|
|
11
|
+
revision: "bf75807669e3af6cfb4ae34724be5c613442aee2"
|
|
12
|
+
},
|
|
13
|
+
issues: {
|
|
14
|
+
support: "https://github.com/stacksjs/stacks/issues/2059",
|
|
15
|
+
signing: "https://github.com/stacksjs/stacks/issues/2062",
|
|
16
|
+
lifecycle: "https://github.com/stacksjs/stacks/issues/2063",
|
|
17
|
+
upstreamActions: "https://github.com/home-lang/craft/issues/11",
|
|
18
|
+
dmgContention: "https://github.com/home-lang/craft/issues/16"
|
|
19
|
+
},
|
|
20
|
+
contracts: [
|
|
21
|
+
{ path: "packages/typescript/src/package.ts", sha256: "a7522f8c79dbd50a0f3bbc113525fb46eb8d6d1bab711d434cae85dc892ccdc1" },
|
|
22
|
+
{ path: "packages/typescript/src/package.test.ts", sha256: "673338f3793e397a83590daf84815869b0698b4afe97eaad8039426418554b7a" },
|
|
23
|
+
{ path: "scripts/native-lifecycle.ts", sha256: "a2c0af2c2165981ca7fbd31880adeec64db64fc305d4ca3efe62a11483b289b4" },
|
|
24
|
+
{ path: "scripts/native-lifecycle-plan.ts", sha256: "ac8e8f101246e94c0c26fdcbf9202536426bf192a303606079273d9689e4e27d" },
|
|
25
|
+
{ path: "scripts/native-lifecycle-plan.test.ts", sha256: "5cc839b60f7c8e32716f29201b159af817f4ac40b2c7963131124b3ac5de0638" },
|
|
26
|
+
{ path: ".github/workflows/native-lifecycle.yml", sha256: "493b614554f03dc669724ff31bd56ae8361851dd2a9b32ac37560d9c0bb1b85e" },
|
|
27
|
+
{ path: ".github/workflows/release.yml", sha256: "e3c7edd45841a7260fb4dfe810f0ff3f63f3117f7d0f4d14984e4dfb36b1f638" }
|
|
28
|
+
],
|
|
29
|
+
lifecycleMatrix: [
|
|
30
|
+
{ runner: "macos-15", platform: "darwin", architecture: "arm64", formats: ["dmg", "pkg"] },
|
|
31
|
+
{ runner: "macos-15-intel", platform: "darwin", architecture: "x64", formats: ["dmg", "pkg"] },
|
|
32
|
+
{ runner: "ubuntu-24.04", platform: "linux", architecture: "x64", formats: ["deb"] },
|
|
33
|
+
{ runner: "windows-2025", platform: "win32", architecture: "x64", formats: ["msi", "zip"] }
|
|
34
|
+
],
|
|
35
|
+
lifecycleOperations: ["install", "launch", "update", "rollback", "uninstall"],
|
|
36
|
+
stacksWorkflow: "https://github.com/stacksjs/stacks/actions/workflows/desktop-lifecycle.yml",
|
|
37
|
+
release: {
|
|
38
|
+
sourceTagged: !0,
|
|
39
|
+
githubRelease: null,
|
|
40
|
+
packagePublished: !1,
|
|
41
|
+
signing: "pending",
|
|
42
|
+
notarization: "pending",
|
|
43
|
+
blockedBy: "https://github.com/home-lang/craft/issues/11"
|
|
44
|
+
},
|
|
45
|
+
verification: {
|
|
46
|
+
local: ["bun test packages/typescript/src/package.test.ts scripts/native-lifecycle-plan.test.ts", "bun run typecheck", "bun .github/scripts/native-lifecycle.ts"],
|
|
47
|
+
retained: "https://github.com/stacksjs/stacks/actions/runs/29890804159"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export function validateCraftEvidence(evidence) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (evidence.source.tag !== `v${evidence.source.version}`)
|
|
53
|
+
errors.push("Craft tag does not match its version");
|
|
54
|
+
if (!/^[a-f0-9]{40}$/.test(evidence.source.revision))
|
|
55
|
+
errors.push("Craft revision is not a full commit SHA");
|
|
56
|
+
if (evidence.contracts.length !== 7)
|
|
57
|
+
errors.push("all Craft packaging/release contracts must be pinned");
|
|
58
|
+
for (const contract of evidence.contracts)
|
|
59
|
+
if (!/^[a-f0-9]{64}$/.test(contract.sha256))
|
|
60
|
+
errors.push(`${contract.path}: invalid SHA-256 digest`);
|
|
61
|
+
const targets = new Set(evidence.lifecycleMatrix.map((row) => `${row.platform}/${row.architecture}`));
|
|
62
|
+
for (const target of ["darwin/arm64", "darwin/x64", "linux/x64", "win32/x64"])
|
|
63
|
+
if (!targets.has(target))
|
|
64
|
+
errors.push(`Craft lifecycle matrix is missing ${target}`);
|
|
65
|
+
if (evidence.release.signing !== "pending" || evidence.release.notarization !== "pending")
|
|
66
|
+
errors.push("Craft signing/notarization cannot be promoted without retained release evidence");
|
|
67
|
+
if (!evidence.release.blockedBy)
|
|
68
|
+
errors.push("Craft release blocker must be linked");
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
function serializedEvidence() {
|
|
72
|
+
return `${JSON.stringify(craftEvidence, null, 2)}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
export async function run() {
|
|
76
|
+
const errors = validateCraftEvidence(craftEvidence);
|
|
77
|
+
if (process.argv.includes("--write")) {
|
|
78
|
+
if (errors.length)
|
|
79
|
+
throw Error(errors.join(`
|
|
80
|
+
`));
|
|
81
|
+
writeFileSync(outputPath, serializedEvidence());
|
|
82
|
+
console.log(`Wrote Craft evidence for ${craftEvidence.source.tag}`);
|
|
83
|
+
} else if (process.argv.includes("--check")) {
|
|
84
|
+
if (!existsSync(outputPath))
|
|
85
|
+
errors.push(".github/protocol/evidence/craft.json is missing");
|
|
86
|
+
else if (readFileSync(outputPath, "utf8") !== serializedEvidence())
|
|
87
|
+
errors.push("Craft evidence is stale; run bun run protocol:craft");
|
|
88
|
+
if (errors.length)
|
|
89
|
+
throw Error(errors.join(`
|
|
90
|
+
`));
|
|
91
|
+
console.log(`Craft evidence pins ${craftEvidence.source.tag} at ${craftEvidence.source.revision}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error("usage: bun storage/framework/core/buddy/src/commands/protocol/craft-evidence.ts --write | --check");
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (import.meta.main)
|
|
98
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|