@stacksjs/buddy 0.70.163 → 0.70.165

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.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +1 -0
  3. package/dist/commands/ai-context.js +1 -1
  4. package/dist/commands/create.js +1 -1
  5. package/dist/commands/deploy.d.ts +11 -1
  6. package/dist/commands/deploy.js +22 -4
  7. package/dist/commands/dev.js +62 -14
  8. package/dist/commands/docs/buddy-commands.d.ts +8 -0
  9. package/dist/commands/docs/buddy-commands.js +116 -0
  10. package/dist/commands/docs/buddy-commands.test.d.ts +1 -0
  11. package/dist/commands/docs/buddy-commands.test.js +38 -0
  12. package/dist/commands/docs/generated-artifacts.d.ts +3 -0
  13. package/dist/commands/docs/generated-artifacts.js +69 -0
  14. package/dist/commands/docs/generated-artifacts.test.d.ts +1 -0
  15. package/dist/commands/docs/generated-artifacts.test.js +19 -0
  16. package/dist/commands/docs/links.d.ts +18 -0
  17. package/dist/commands/docs/links.js +83 -0
  18. package/dist/commands/docs/links.test.d.ts +1 -0
  19. package/dist/commands/docs/links.test.js +73 -0
  20. package/dist/commands/docs.d.ts +7 -0
  21. package/dist/commands/docs.js +24 -0
  22. package/dist/commands/doctor.js +8 -0
  23. package/dist/commands/env.d.ts +14 -0
  24. package/dist/commands/env.js +73 -7
  25. package/dist/commands/generate.js +1 -1
  26. package/dist/commands/index.d.ts +2 -0
  27. package/dist/commands/index.js +2 -0
  28. package/dist/commands/mail.js +1 -1
  29. package/dist/commands/migrate.js +4 -4
  30. package/dist/commands/protocol/craft-evidence.d.ts +76 -0
  31. package/dist/commands/protocol/craft-evidence.js +98 -0
  32. package/dist/commands/protocol/craft-evidence.test.d.ts +1 -0
  33. package/dist/commands/protocol/craft-evidence.test.js +20 -0
  34. package/dist/commands/protocol/desktop-lifecycle-report.d.ts +16 -0
  35. package/dist/commands/protocol/desktop-lifecycle-report.js +57 -0
  36. package/dist/commands/protocol/desktop-lifecycle-report.test.d.ts +1 -0
  37. package/dist/commands/protocol/desktop-lifecycle-report.test.js +32 -0
  38. package/dist/commands/protocol/desktop-support.d.ts +1 -0
  39. package/dist/commands/protocol/desktop-support.js +32 -0
  40. package/dist/commands/protocol/desktop-workflow.test.d.ts +1 -0
  41. package/dist/commands/protocol/desktop-workflow.test.js +33 -0
  42. package/dist/commands/protocol/driver-registry.d.ts +1 -0
  43. package/dist/commands/protocol/driver-registry.js +71 -0
  44. package/dist/commands/protocol/pantry-evidence.d.ts +67 -0
  45. package/dist/commands/protocol/pantry-evidence.js +144 -0
  46. package/dist/commands/protocol/pantry-evidence.test.d.ts +1 -0
  47. package/dist/commands/protocol/pantry-evidence.test.js +29 -0
  48. package/dist/commands/protocol/release-manifest.d.ts +30 -0
  49. package/dist/commands/protocol/release-manifest.js +102 -0
  50. package/dist/commands/protocol/release-manifest.test.d.ts +1 -0
  51. package/dist/commands/protocol/release-manifest.test.js +34 -0
  52. package/dist/commands/protocol/run-conformance.d.ts +21 -0
  53. package/dist/commands/protocol/run-conformance.js +358 -0
  54. package/dist/commands/protocol/run-conformance.test.d.ts +1 -0
  55. package/dist/commands/protocol/run-conformance.test.js +52 -0
  56. package/dist/commands/protocol/run-driver-contracts.d.ts +7 -0
  57. package/dist/commands/protocol/run-driver-contracts.js +94 -0
  58. package/dist/commands/protocol/run-driver-contracts.test.d.ts +1 -0
  59. package/dist/commands/protocol/run-driver-contracts.test.js +18 -0
  60. package/dist/commands/protocol/source-manifest.d.ts +25 -0
  61. package/dist/commands/protocol/source-manifest.js +113 -0
  62. package/dist/commands/protocol/sync-suite.d.ts +8 -0
  63. package/dist/commands/protocol/sync-suite.js +137 -0
  64. package/dist/commands/protocol/sync-suite.test.d.ts +1 -0
  65. package/dist/commands/protocol/sync-suite.test.js +44 -0
  66. package/dist/commands/protocol.d.ts +8 -0
  67. package/dist/commands/protocol.js +60 -0
  68. package/dist/commands/publish.js +149 -0
  69. package/dist/commands/run-tool.d.ts +7 -0
  70. package/dist/commands/run-tool.js +9 -0
  71. package/dist/commands/seed.js +15 -40
  72. package/dist/commands/serve.d.ts +18 -5
  73. package/dist/commands/serve.js +21 -2
  74. package/dist/commands/setup-ai.d.ts +39 -0
  75. package/dist/commands/setup-ai.js +121 -0
  76. package/dist/commands/setup.js +22 -0
  77. package/dist/lazy-commands.js +27 -1
  78. package/package.json +43 -42
@@ -0,0 +1,113 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, extname, resolve } from "node:path";
5
+ const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, ".github/protocol/evidence/source-manifest.json");
6
+ function runGit(...arguments_) {
7
+ try {
8
+ return execFileSync("git", arguments_, { cwd: root, maxBuffer: 67108864 });
9
+ } catch (error) {
10
+ throw Error(`git ${arguments_.join(" ")} failed: ${error instanceof Error ? error.message : String(error)}`);
11
+ }
12
+ }
13
+ function revision(value) {
14
+ return runGit("rev-parse", `${value}^{commit}`).toString().trim();
15
+ }
16
+ function showJson(sourceRevision, path) {
17
+ return JSON.parse(runGit("show", `${sourceRevision}:${path}`).toString());
18
+ }
19
+ function category(path) {
20
+ if (/(^|\/)(test|tests|__tests__)(\/|$)|\.(test|spec)\.[^.]+$/.test(path))
21
+ return "tests";
22
+ if (path.startsWith("protocol/suite/"))
23
+ return "vendored-protocol";
24
+ if (/(^|\/)(dist|generated|coverage)(\/|$)|\.d\.ts$/.test(path))
25
+ return "generated";
26
+ if (/\.(md|mdx|rst|txt)$/.test(path) || /(^|\/)docs\//.test(path))
27
+ return "documentation";
28
+ if (/(^|\/)(config|configs|\.github)(\/|$)|(^|\/)(tsconfig|package|bun\.lock|pantry)\b/.test(path))
29
+ return "configuration";
30
+ if (/\.(ts|tsx|js|jsx|vue|svelte|zig|rs|go|py|sh)$/.test(path))
31
+ return "source";
32
+ return "assets-and-data";
33
+ }
34
+ function addCount(record, key, bytes) {
35
+ const current = record[key] || { files: 0, bytes: 0 };
36
+ record[key] = { files: current.files + 1, bytes: current.bytes + bytes };
37
+ }
38
+ export function buildSourceManifest(requestedRevision) {
39
+ const sourceRevision = revision(requestedRevision), sourceTree = runGit("rev-parse", `${sourceRevision}^{tree}`).toString().trim(), treeListing = runGit("ls-tree", "-r", "-l", sourceRevision).toString(), lines = treeListing.trim().split(`
40
+ `).filter(Boolean), categories = {}, extensions = {};
41
+ let totalBytes = 0;
42
+ const paths = [];
43
+ for (const line of lines) {
44
+ const match = line.match(/^\d+\s+blob\s+[0-9a-f]+\s+(\d+)\t(.+)$/);
45
+ if (!match)
46
+ continue;
47
+ const bytes = Number(match[1]), path = match[2];
48
+ if (path == null)
49
+ continue;
50
+ paths.push(path);
51
+ totalBytes += bytes;
52
+ addCount(categories, category(path), bytes);
53
+ const extension = extname(path).toLowerCase() || "[none]";
54
+ extensions[extension] = (extensions[extension] || 0) + 1;
55
+ }
56
+ const packages = paths.filter((path) => path === "package.json" || path.endsWith("/package.json")).flatMap((path) => {
57
+ const pkg = showJson(sourceRevision, path);
58
+ if (!pkg.name || !pkg.version)
59
+ return [];
60
+ return [{ name: pkg.name, path, version: pkg.version, publishable: pkg.private !== !0 }];
61
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)), rootPackage = showJson(sourceRevision, "package.json"), suiteLock = showJson(sourceRevision, "protocol/suite.lock.json");
62
+ return {
63
+ schemaVersion: "1.0.0",
64
+ repository: "https://github.com/stacksjs/stacks",
65
+ sourceRevision,
66
+ sourceTree,
67
+ sourceDigest: `sha256:${createHash("sha256").update(treeListing).digest("hex")}`,
68
+ generatedAt: "derived-from-immutable-git-objects",
69
+ classification: {
70
+ status: "reference-implementation",
71
+ conformance: "unverified",
72
+ notes: "Reference implementation status does not imply a protocol profile claim; consult the report for per-requirement evidence."
73
+ },
74
+ prerequisites: rootPackage.system || {},
75
+ packages,
76
+ inventory: {
77
+ totalFiles: lines.length,
78
+ totalBytes,
79
+ categories: Object.fromEntries(Object.entries(categories).sort(([a], [b]) => a.localeCompare(b))),
80
+ extensions: Object.fromEntries(Object.entries(extensions).sort(([a], [b]) => a.localeCompare(b)))
81
+ },
82
+ protocolSuite: { version: suiteLock.protocolVersion, rfcsRevision: suiteLock.rfcsRevision }
83
+ };
84
+ }
85
+ function render(manifest) {
86
+ return `${JSON.stringify(manifest, null, 2)}
87
+ `;
88
+ }
89
+ export async function run() {
90
+ const mode = process.argv.includes("--write") ? "write" : process.argv.includes("--check") ? "check" : null;
91
+ if (!mode) {
92
+ console.error("usage: bun storage/framework/core/buddy/src/commands/protocol/source-manifest.ts --write [--revision <ref>] | --check");
93
+ process.exit(2);
94
+ }
95
+ if (mode === "write") {
96
+ const index = process.argv.indexOf("--revision"), requestedRevision = index === -1 ? "HEAD" : process.argv[index + 1];
97
+ if (!requestedRevision)
98
+ throw Error("--revision requires a Git ref");
99
+ const manifest = buildSourceManifest(requestedRevision);
100
+ mkdirSync(dirname(outputPath), { recursive: !0 });
101
+ writeFileSync(outputPath, render(manifest));
102
+ console.log(`Wrote source manifest for ${manifest.sourceRevision} (${manifest.inventory.totalFiles} files, ${manifest.packages.length} packages)`);
103
+ } else {
104
+ if (!existsSync(outputPath))
105
+ throw Error("source manifest is missing; run bun run protocol:manifest");
106
+ const current = JSON.parse(readFileSync(outputPath, "utf8")), expected = render(buildSourceManifest(current.sourceRevision));
107
+ if (readFileSync(outputPath, "utf8") !== expected)
108
+ throw Error(`source manifest is stale or modified; run bun storage/framework/core/buddy/src/commands/protocol/source-manifest.ts --write --revision ${current.sourceRevision}`);
109
+ console.log(`Source manifest matches ${current.sourceRevision}`);
110
+ }
111
+ }
112
+ if (import.meta.main)
113
+ await run();
@@ -0,0 +1,8 @@
1
+ /** Requirement ids that appear more than once in `ids` (sorted, deduped). */
2
+ export declare function duplicateRequirementIds(ids: string[]): string[];
3
+ /**
4
+ * Requirement ids referenced by a fixture that are absent from `catalogIds`,
5
+ * formatted `fixtureId -> requirementId`.
6
+ */
7
+ export declare function danglingFixtureRequirements(fixtures: Array<{ id: string, requirements?: string[] }>, catalogIds: Set<string>): string[];
8
+ export declare function run(): Promise<void>;
@@ -0,0 +1,137 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, relative, resolve } from "node:path";
4
+ const root = resolve(import.meta.dir, "../../../../../../.."), suiteRoot = resolve(root, ".github/protocol/suite/1.0-draft"), lockPath = resolve(root, ".github/protocol/suite.lock.json");
5
+ function digest(data) {
6
+ return `sha256:${createHash("sha256").update(data).digest("hex")}`;
7
+ }
8
+ function filesBelow(directory) {
9
+ return readdirSync(directory, { withFileTypes: !0 }).flatMap((entry) => {
10
+ const path = resolve(directory, entry.name);
11
+ return entry.isDirectory() ? filesBelow(path) : [path];
12
+ }).sort();
13
+ }
14
+ function argument(name) {
15
+ const index = process.argv.indexOf(name);
16
+ return index === -1 ? void 0 : process.argv[index + 1];
17
+ }
18
+ function gitRevision(repository) {
19
+ const result = Bun.spawnSync(["git", "-C", repository, "rev-parse", "HEAD"]);
20
+ if (result.exitCode !== 0)
21
+ throw Error(`Could not resolve the RFC repository revision: ${result.stderr.toString().trim()}`);
22
+ return result.stdout.toString().trim();
23
+ }
24
+ function writeSnapshot(sourceRepository) {
25
+ const sourceRoot = resolve(sourceRepository, "protocol/1.0-draft");
26
+ if (!existsSync(resolve(sourceRoot, "catalog.json")))
27
+ throw Error(`No protocol suite found at ${sourceRoot}`);
28
+ rmSync(suiteRoot, { force: !0, recursive: !0 });
29
+ const files = {}, sourceFiles = [
30
+ ...filesBelow(sourceRoot).map((sourcePath) => ({ sourcePath, path: relative(sourceRoot, sourcePath) })),
31
+ ...["LICENSE-SPECIFICATION.md", "LICENSE-FIXTURES.md"].map((path) => ({ sourcePath: resolve(sourceRepository, path), path }))
32
+ ];
33
+ for (const { sourcePath, path } of sourceFiles) {
34
+ const outputPath = resolve(suiteRoot, path), contents = readFileSync(sourcePath);
35
+ mkdirSync(dirname(outputPath), { recursive: !0 });
36
+ writeFileSync(outputPath, contents);
37
+ files[path] = digest(contents);
38
+ }
39
+ const lock = {
40
+ protocolVersion: "1.0-draft",
41
+ rfcsRevision: gitRevision(sourceRepository),
42
+ sourceRepository: "https://github.com/stacksjs/rfcs",
43
+ files
44
+ };
45
+ mkdirSync(dirname(lockPath), { recursive: !0 });
46
+ writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}
47
+ `);
48
+ console.log(`Pinned ${Object.keys(files).length} protocol files at ${lock.rfcsRevision}`);
49
+ }
50
+ function checkSnapshot() {
51
+ if (!existsSync(lockPath))
52
+ throw Error(".github/protocol/suite.lock.json is missing; run bun run protocol:sync");
53
+ const lock = JSON.parse(readFileSync(lockPath, "utf8"));
54
+ if (!/^[0-9a-f]{40}$/.test(lock.rfcsRevision))
55
+ throw Error("suite lock has an invalid RFC revision");
56
+ const actualFiles = filesBelow(suiteRoot).map((path) => relative(suiteRoot, path)), expectedFiles = Object.keys(lock.files).sort(), unexpected = actualFiles.filter((path) => !lock.files[path]), missing = expectedFiles.filter((path) => !actualFiles.includes(path)), changed = expectedFiles.filter((path) => {
57
+ const absolutePath = resolve(suiteRoot, path);
58
+ return existsSync(absolutePath) && digest(readFileSync(absolutePath)) !== lock.files[path];
59
+ }), errors = [
60
+ ...missing.map((path) => `missing: ${path}`),
61
+ ...unexpected.map((path) => `unexpected: ${path}`),
62
+ ...changed.map((path) => `modified: ${path}`)
63
+ ];
64
+ if (errors.length > 0)
65
+ throw Error(`Vendored protocol suite does not match its lock:
66
+ ${errors.join(`
67
+ `)}`);
68
+ console.log(`Protocol suite matches ${basename(lock.sourceRepository)}@${lock.rfcsRevision} (${actualFiles.length} files)`);
69
+ }
70
+ export function duplicateRequirementIds(ids) {
71
+ const seen = new Set, duplicates = new Set;
72
+ for (const id of ids)
73
+ if (seen.has(id))
74
+ duplicates.add(id);
75
+ else
76
+ seen.add(id);
77
+ return [...duplicates].sort();
78
+ }
79
+ function checkCatalog() {
80
+ const catalogPath = resolve(suiteRoot, "catalog.json");
81
+ if (!existsSync(catalogPath))
82
+ throw Error(`protocol catalog missing at ${relative(root, catalogPath)}; run bun run protocol:sync`);
83
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf8")), requirements = Array.isArray(catalog.requirements) ? catalog.requirements : [];
84
+ if (requirements.length === 0)
85
+ throw Error("protocol catalog has no requirements");
86
+ const ids = requirements.map((requirement) => requirement?.id), missing = ids.filter((id) => typeof id !== "string" || id.length === 0).length;
87
+ if (missing > 0)
88
+ throw Error(`protocol catalog has ${missing} requirement(s) without a string id`);
89
+ const duplicates = duplicateRequirementIds(ids);
90
+ if (duplicates.length > 0)
91
+ throw Error(`protocol catalog has duplicate requirement id(s): ${duplicates.join(", ")}`);
92
+ console.log(`Protocol catalog: ${ids.length} requirement ids, all unique`);
93
+ }
94
+ export function danglingFixtureRequirements(fixtures, catalogIds) {
95
+ const dangling = [];
96
+ for (const fixture of fixtures)
97
+ for (const requirement of fixture.requirements ?? [])
98
+ if (!catalogIds.has(requirement))
99
+ dangling.push(`${fixture.id} -> ${requirement}`);
100
+ return dangling;
101
+ }
102
+ function checkFixtures() {
103
+ const fixturesPath = resolve(suiteRoot, "fixtures/conformance.json");
104
+ if (!existsSync(fixturesPath))
105
+ throw Error(`protocol fixture corpus missing at ${relative(root, fixturesPath)}; run bun run protocol:sync`);
106
+ const corpus = JSON.parse(readFileSync(fixturesPath, "utf8")), fixtures = Array.isArray(corpus.fixtures) ? corpus.fixtures : [];
107
+ if (fixtures.length === 0)
108
+ throw Error("protocol fixture corpus has no fixtures");
109
+ const ids = fixtures.map((fixture) => fixture?.id), missing = ids.filter((id) => typeof id !== "string" || id.length === 0).length;
110
+ if (missing > 0)
111
+ throw Error(`protocol fixture corpus has ${missing} fixture(s) without a string id`);
112
+ const duplicates = duplicateRequirementIds(ids);
113
+ if (duplicates.length > 0)
114
+ throw Error(`protocol fixture corpus has duplicate fixture id(s): ${duplicates.join(", ")}`);
115
+ const catalog = JSON.parse(readFileSync(resolve(suiteRoot, "catalog.json"), "utf8")), catalogIds = new Set((catalog.requirements ?? []).map((requirement) => requirement?.id).filter((id) => typeof id === "string")), normalized = fixtures.map((fixture) => ({
116
+ id: String(fixture?.id),
117
+ requirements: Array.isArray(fixture?.requirements) ? fixture.requirements.filter((requirement) => typeof requirement === "string") : []
118
+ })), dangling = danglingFixtureRequirements(normalized, catalogIds);
119
+ if (dangling.length > 0)
120
+ throw Error(`protocol fixtures reference unknown requirement id(s): ${dangling.slice(0, 10).join(", ")}`);
121
+ console.log(`Protocol fixtures: ${ids.length} fixtures, ids unique, all requirement refs resolve`);
122
+ }
123
+ export async function run() {
124
+ if (process.argv.includes("--write")) {
125
+ const source = resolve(argument("--source") || process.env.STACKS_RFC_SOURCE || resolve(root, "../rfcs"));
126
+ writeSnapshot(source);
127
+ } else if (process.argv.includes("--check")) {
128
+ checkSnapshot();
129
+ checkCatalog();
130
+ checkFixtures();
131
+ } else {
132
+ console.error("usage: bun storage/framework/core/buddy/src/commands/protocol/sync-suite.ts --write [--source ../rfcs] | --check");
133
+ process.exit(2);
134
+ }
135
+ }
136
+ if (import.meta.main)
137
+ await run();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "bun:test";
4
+ import { danglingFixtureRequirements, duplicateRequirementIds } from "./sync-suite";
5
+ describe("protocol requirement-id uniqueness (stacksjs/stacks#2050)", () => {
6
+ it("reports no duplicates for a unique list", () => {
7
+ expect(duplicateRequirementIds(["CORE-CONV-01", "CORE-CONV-02", "CORE-MVA-01"])).toEqual([]);
8
+ });
9
+ it("reports each duplicated id once, sorted", () => {
10
+ expect(duplicateRequirementIds(["B-1", "A-1", "B-1", "A-1", "A-1", "C-1"])).toEqual(["A-1", "B-1"]);
11
+ });
12
+ it("handles an empty list", () => {
13
+ expect(duplicateRequirementIds([])).toEqual([]);
14
+ });
15
+ it("the vendored catalog.json has unique requirement ids", () => {
16
+ const ids = JSON.parse(readFileSync(resolve(import.meta.dir, "../../../../../../../.github/protocol/suite/1.0-draft/catalog.json"), "utf8")).requirements.map((requirement) => requirement.id);
17
+ expect(ids.length).toBeGreaterThan(0);
18
+ expect(duplicateRequirementIds(ids)).toEqual([]);
19
+ });
20
+ });
21
+ describe("protocol fixture-corpus integrity (stacksjs/stacks#2051)", () => {
22
+ const catalogIds = new Set(["CORE-CONV-01", "CORE-CONV-02", "CORE-MVA-01"]);
23
+ it("reports no dangling refs when every requirement resolves", () => {
24
+ expect(danglingFixtureRequirements([
25
+ { id: "fixture.a", requirements: ["CORE-CONV-01", "CORE-CONV-02"] },
26
+ { id: "fixture.b", requirements: ["CORE-MVA-01"] }
27
+ ], catalogIds)).toEqual([]);
28
+ });
29
+ it("reports each unknown requirement ref as `fixture -> requirement`", () => {
30
+ expect(danglingFixtureRequirements([
31
+ { id: "fixture.a", requirements: ["CORE-CONV-01", "CORE-GONE-99"] },
32
+ { id: "fixture.b", requirements: ["NOPE-01"] }
33
+ ], catalogIds)).toEqual(["fixture.a -> CORE-GONE-99", "fixture.b -> NOPE-01"]);
34
+ });
35
+ it("tolerates fixtures with no requirements", () => {
36
+ expect(danglingFixtureRequirements([{ id: "fixture.a" }], catalogIds)).toEqual([]);
37
+ });
38
+ it("the vendored fixture corpus has unique ids and no dangling requirement refs", () => {
39
+ const base = resolve(import.meta.dir, "../../../../../../../.github/protocol/suite/1.0-draft"), corpus = JSON.parse(readFileSync(resolve(base, "fixtures/conformance.json"), "utf8")), catalog = JSON.parse(readFileSync(resolve(base, "catalog.json"), "utf8")), realIds = new Set(catalog.requirements.map((requirement) => requirement.id));
40
+ expect(corpus.fixtures.length).toBeGreaterThan(0);
41
+ expect(duplicateRequirementIds(corpus.fixtures.map((fixture) => fixture.id))).toEqual([]);
42
+ expect(danglingFixtureRequirements(corpus.fixtures, realIds)).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,8 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ /**
3
+ * Framework-repo `buddy protocol:*` commands. The governance tooling itself
4
+ * lives beside this file under `commands/protocol/`; the protocol suite +
5
+ * evidence DATA stays under `.github/protocol/`. These wrap the tooling so it
6
+ * is discoverable via the CLI (`buddy protocol:conformance`).
7
+ */
8
+ export declare function protocol(buddy: CLI): void;
@@ -0,0 +1,60 @@
1
+ import { run as runConformance } from "./protocol/run-conformance";
2
+ import { run as runCraft } from "./protocol/craft-evidence";
3
+ import { run as runDesktop } from "./protocol/desktop-support";
4
+ import { run as runDrivers } from "./protocol/driver-registry";
5
+ import { run as runDriverContracts } from "./protocol/run-driver-contracts";
6
+ import { run as runPantry } from "./protocol/pantry-evidence";
7
+ import { run as runRelease } from "./protocol/release-manifest";
8
+ import { run as runManifest } from "./protocol/source-manifest";
9
+ import { run as runSync } from "./protocol/sync-suite";
10
+ import { runTool } from "./run-tool";
11
+ export function protocol(buddy) {
12
+ buddy.command("protocol:conformance", "Generate the Stacks protocol conformance report").action(async () => {
13
+ await runConformance();
14
+ });
15
+ buddy.command("protocol:sync", "Sync the vendored protocol suite from stacksjs/rfcs").option("--source <path>", "Path to a local rfcs checkout", { default: void 0 }).action(async (options) => {
16
+ await runTool(runSync, "--write", ...options.source ? ["--source", options.source] : []);
17
+ });
18
+ buddy.command("protocol:check", "Verify the vendored protocol suite is pinned + internally consistent").action(async () => {
19
+ await runTool(runSync, "--check");
20
+ });
21
+ buddy.command("protocol:manifest", "Write the protocol source manifest").option("--revision <ref>", "Source revision to pin (default HEAD)", { default: void 0 }).action(async (options) => {
22
+ await runTool(runManifest, "--write", ...options.revision ? ["--revision", options.revision] : []);
23
+ });
24
+ buddy.command("protocol:manifest:check", "Verify the protocol source manifest is current").action(async () => {
25
+ await runTool(runManifest, "--check");
26
+ });
27
+ buddy.command("protocol:release", "Write the protocol release manifest").option("--tag <tag>", "Release tag", { default: void 0 }).action(async (options) => {
28
+ await runTool(runRelease, "--write", ...options.tag ? ["--tag", options.tag] : []);
29
+ });
30
+ buddy.command("protocol:release:check", "Verify the protocol release manifest is current").action(async () => {
31
+ await runTool(runRelease, "--check");
32
+ });
33
+ buddy.command("protocol:drivers", "Write the driver capability registry evidence").action(async () => {
34
+ await runTool(runDrivers, "--write");
35
+ });
36
+ buddy.command("protocol:drivers:check", "Verify the driver capability registry evidence").action(async () => {
37
+ await runTool(runDrivers, "--check");
38
+ });
39
+ buddy.command("protocol:drivers:test", "Run the driver contract suite").action(async () => {
40
+ await runTool(runDriverContracts);
41
+ });
42
+ buddy.command("protocol:desktop", "Write the desktop support matrix evidence").action(async () => {
43
+ await runTool(runDesktop, "--write");
44
+ });
45
+ buddy.command("protocol:desktop:check", "Verify the desktop support matrix evidence").action(async () => {
46
+ await runTool(runDesktop, "--check");
47
+ });
48
+ buddy.command("protocol:pantry", "Write the pantry evidence").action(async () => {
49
+ await runTool(runPantry, "--write");
50
+ });
51
+ buddy.command("protocol:pantry:check", "Verify the pantry evidence").action(async () => {
52
+ await runTool(runPantry, "--check");
53
+ });
54
+ buddy.command("protocol:craft", "Write the Craft evidence").action(async () => {
55
+ await runTool(runCraft, "--write");
56
+ });
57
+ buddy.command("protocol:craft:check", "Verify the Craft evidence").action(async () => {
58
+ await runTool(runCraft, "--check");
59
+ });
60
+ }
@@ -14,9 +14,12 @@ export function publish(buddy) {
14
14
  middleware: "Publish a default middleware from storage/framework/defaults/app/Middleware/ to app/Middleware/",
15
15
  action: "Publish a default action from storage/framework/defaults/app/Actions/ to app/Actions/",
16
16
  core: "Publish a framework package source from node_modules/@stacksjs/<pkg>/ into storage/framework/core/<pkg>/ for editing",
17
+ unpublishCore: "Drop a vendored storage/framework/core/<pkg>/ and go back to the installed @stacksjs/<pkg>",
18
+ all: "Unvendor the whole framework: remove storage/framework/core and resolve every @stacksjs package from the `stacks` dependency in package.json",
17
19
  name: "The name of the resource to publish (e.g. Cart, User)",
18
20
  pkg: "The name of the framework package (e.g. router, orm, faker \u2014 without @stacksjs/ prefix)",
19
21
  force: "Overwrite an existing userland file",
22
+ forceUnpublish: "Delete the vendored source even when it has uncommitted changes",
20
23
  verbose: "Enable verbose output"
21
24
  };
22
25
  buddy.command("publish:model <name>", descriptions.model).option("--force", descriptions.force, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (name, options) => {
@@ -58,6 +61,17 @@ export function publish(buddy) {
58
61
  buddy.command("publish:core <pkg>", descriptions.core).option("--force", descriptions.force, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (pkg, options) => {
59
62
  await publishCorePackage(pkg, !!options.force);
60
63
  });
64
+ buddy.command("unpublish:core [pkg]", descriptions.unpublishCore).option("--all", descriptions.all, { default: !1 }).option("--force", descriptions.forceUnpublish, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (pkg, options) => {
65
+ if (options.all) {
66
+ await unvendorFramework(!!options.force);
67
+ return;
68
+ }
69
+ if (!pkg) {
70
+ log.error("Usage: buddy unpublish:core <pkg> (or --all to move the whole framework to node_modules)");
71
+ process.exit(ExitCode.FatalError);
72
+ }
73
+ await unpublishCorePackage(pkg, !!options.force);
74
+ });
61
75
  buddy.command("publish [resource] [name]", descriptions.command).option("--force", descriptions.force, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (resource, name, options) => {
62
76
  if (!resource || !name) {
63
77
  log.error("Usage: buddy publish:<resource> <Name> (e.g. buddy publish:model Cart)");
@@ -170,3 +184,138 @@ async function publishResource(ctx) {
170
184
  await fs.promises.copyFile(sourcePath, targetPath);
171
185
  log.success(`Published ${kind} ${italic(name)} \u2192 ${italic(targetPath.replace(`${process.cwd()}/`, ""))}`);
172
186
  }
187
+ async function unpublishCorePackage(pkg, force) {
188
+ const shortName = normalizeCoreName(pkg), targetDir = path.frameworkPath(`core/${shortName}`), rel = (p) => p.replace(`${process.cwd()}/`, "");
189
+ if (!existsSync(targetDir)) {
190
+ log.info(`Not vendored: ${italic(rel(targetDir))} \u2014 nothing to do.`);
191
+ return;
192
+ }
193
+ const installed = resolve(process.cwd(), "node_modules", "@stacksjs", shortName);
194
+ if (!existsSync(installed) && !force) {
195
+ log.error(`@stacksjs/${shortName} is not installed, so removing the vendored copy would leave nothing to resolve.`);
196
+ log.info(`Run \`bun add @stacksjs/${shortName}\` first, or pass --force to remove it anyway.`);
197
+ process.exit(ExitCode.FatalError);
198
+ }
199
+ await assertNoUncommittedChanges(targetDir, force);
200
+ await fs.promises.rm(targetDir, { recursive: !0, force: !0 });
201
+ log.success(`Unpublished ${italic(rel(targetDir))} \u2014 @stacksjs/${shortName} now resolves from node_modules.`);
202
+ }
203
+ async function unvendorFramework(force) {
204
+ const coreDir = path.frameworkPath("core"), rel = (p) => p.replace(`${process.cwd()}/`, "");
205
+ if (!existsSync(coreDir)) {
206
+ log.info("No storage/framework/core in this project \u2014 already on the installed packages.");
207
+ return;
208
+ }
209
+ const corePkgPath = resolve(coreDir, "package.json");
210
+ if (!existsSync(corePkgPath)) {
211
+ log.error(`${rel(coreDir)} has no package.json, so its version cannot be determined.`);
212
+ log.info("Unvendor the packages individually with `buddy unpublish:core <pkg>` instead.");
213
+ process.exit(ExitCode.FatalError);
214
+ }
215
+ const corePkg = JSON.parse(await fs.promises.readFile(corePkgPath, "utf-8")), version = corePkg.version;
216
+ if (!version) {
217
+ log.error(`${rel(corePkgPath)} has no version field.`);
218
+ process.exit(ExitCode.FatalError);
219
+ }
220
+ await assertNoUncommittedChanges(coreDir, force);
221
+ const rootPkgPath = resolve(process.cwd(), "package.json"), rootPkgRaw = await fs.promises.readFile(rootPkgPath, "utf-8"), rootPkg = JSON.parse(rootPkgRaw), depName = corePkg.name ?? "stacks", range = `^${version}`, provided = new Set([depName, ...Object.keys(corePkg.dependencies ?? {}).filter((name) => name.startsWith("@stacksjs/"))]);
222
+ for (const entry of await readdir(coreDir, { withFileTypes: !0 }))
223
+ if (entry.isDirectory())
224
+ provided.add(`@stacksjs/${entry.name}`);
225
+ let repointed = 0;
226
+ const repointWorkspaceRanges = (pkg) => {
227
+ let touched = !1;
228
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
229
+ const deps = pkg[field];
230
+ if (!deps)
231
+ continue;
232
+ for (const [name, spec] of Object.entries(deps)) {
233
+ if (!spec.startsWith("workspace:") || !provided.has(name))
234
+ continue;
235
+ deps[name] = range;
236
+ touched = !0;
237
+ repointed++;
238
+ }
239
+ }
240
+ return touched;
241
+ };
242
+ repointWorkspaceRanges(rootPkg);
243
+ if (!rootPkg.dependencies?.[depName] && !rootPkg.devDependencies?.[depName])
244
+ rootPkg.dependencies = { ...rootPkg.dependencies, [depName]: range };
245
+ if (Array.isArray(rootPkg.workspaces)) {
246
+ rootPkg.workspaces = rootPkg.workspaces.filter((glob) => !isCoreWorkspaceGlob(glob));
247
+ if (rootPkg.workspaces.length === 0)
248
+ delete rootPkg.workspaces;
249
+ }
250
+ await fs.promises.writeFile(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}
251
+ `);
252
+ for (const glob of rootPkg.workspaces ?? [])
253
+ for (const memberPkgPath of globSync(`${glob.replace(/\/$/, "")}/package.json`, { cwd: process.cwd(), absolute: !0 })) {
254
+ const raw = await fs.promises.readFile(memberPkgPath, "utf-8"), memberPkg = JSON.parse(raw);
255
+ if (repointWorkspaceRanges(memberPkg))
256
+ await fs.promises.writeFile(memberPkgPath, `${JSON.stringify(memberPkg, null, 2)}
257
+ `);
258
+ }
259
+ const bunfigPath = resolve(process.cwd(), "bunfig.toml");
260
+ let rewrittenPreloads = 0;
261
+ if (existsSync(bunfigPath)) {
262
+ const bunfig = await fs.promises.readFile(bunfigPath, "utf-8"), next = bunfig.replace(/(["'])\.?\/?storage\/framework\/core\/([\w-]+)\/([^"']+?)\.ts\1/g, (_match, quote, pkgName, subpath) => {
263
+ rewrittenPreloads++;
264
+ return `${quote}@stacksjs/${pkgName}/${subpath}.js${quote}`;
265
+ });
266
+ if (next !== bunfig)
267
+ await fs.promises.writeFile(bunfigPath, next);
268
+ }
269
+ await fs.promises.rm(coreDir, { recursive: !0, force: !0 });
270
+ log.success(`Removed ${italic(rel(coreDir))}`);
271
+ log.info(`package.json now depends on ${depName}@${range}`);
272
+ if (repointed > 0)
273
+ log.info(`Repointed ${repointed} workspace: range${repointed === 1 ? "" : "s"} to ${range}`);
274
+ if (rewrittenPreloads > 0)
275
+ log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads === 1 ? "" : "s"} to package specifiers`);
276
+ log.info("Installing the published packages...");
277
+ if (await Bun.spawn(["bun", "install"], { cwd: process.cwd(), stdout: "inherit", stderr: "inherit" }).exited !== 0) {
278
+ log.error("`bun install` failed. package.json and bunfig.toml were updated; re-run the install once the failure is resolved.");
279
+ process.exit(ExitCode.FatalError);
280
+ }
281
+ log.success("This project now runs on the published Stacks packages.");
282
+ log.info("Vendor an individual package again any time with `buddy publish:core <pkg>`.");
283
+ }
284
+ function normalizeCoreName(pkg) {
285
+ const shortName = pkg.replace(/^@stacksjs\//, "").replace(/^core\//, "");
286
+ if (!shortName || shortName.includes("/") || shortName.includes("..")) {
287
+ process.stderr.write(`Invalid package name: ${pkg}
288
+ `);
289
+ process.stderr.write(" Use a short name like `router` or the fully qualified `@stacksjs/router`.\n");
290
+ process.exit(ExitCode.FatalError);
291
+ }
292
+ return shortName;
293
+ }
294
+ function isCoreWorkspaceGlob(glob) {
295
+ const normalized = glob.replace(/^\.\//, "").replace(/\/$/, "");
296
+ return normalized === "storage/framework/core" || normalized.startsWith("storage/framework/core/");
297
+ }
298
+ async function assertNoUncommittedChanges(dir, force) {
299
+ if (force)
300
+ return;
301
+ try {
302
+ const proc = Bun.spawn(["git", "status", "--porcelain", "--", dir], {
303
+ cwd: process.cwd(),
304
+ stdout: "pipe",
305
+ stderr: "ignore"
306
+ }), output = await new Response(proc.stdout).text();
307
+ if (await proc.exited !== 0)
308
+ return;
309
+ const changed = output.split(`
310
+ `).filter(Boolean);
311
+ if (changed.length === 0)
312
+ return;
313
+ log.error(`${changed.length} uncommitted change${changed.length === 1 ? "" : "s"} under ${italic(dir.replace(`${process.cwd()}/`, ""))}:`);
314
+ for (const line of changed.slice(0, 10))
315
+ log.info(` ${line}`);
316
+ if (changed.length > 10)
317
+ log.info(` ... and ${changed.length - 10} more`);
318
+ log.info("Commit or stash them first, or pass --force to delete them anyway.");
319
+ process.exit(ExitCode.FatalError);
320
+ } catch {}
321
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Invoke a migrated tooling module's `run()` with a synthetic argv so its
3
+ * existing `process.argv` flag parsing (`--check` / `--write` / `--revision` …)
4
+ * sees the flags the buddy command wants, then restore the real argv. Buddy runs
5
+ * one command per process and the action is terminal, so this swap is safe.
6
+ */
7
+ export declare function runTool(run: () => void | Promise<void>, ...flags: string[]): Promise<void>;
@@ -0,0 +1,9 @@
1
+ export async function runTool(run, ...flags) {
2
+ const saved = process.argv;
3
+ process.argv = [saved[0] ?? "bun", saved[1] ?? "buddy", ...flags];
4
+ try {
5
+ await run();
6
+ } finally {
7
+ process.argv = saved;
8
+ }
9
+ }
@@ -7,49 +7,24 @@ export function seed(buddy) {
7
7
  project: "Target a specific project",
8
8
  verbose: "Enable verbose output"
9
9
  };
10
- buddy.command("seed", descriptions.seed).alias("db:seed").option("-p, --project [project]", descriptions.project, { default: !1 }).option("-c, --class [class]", "Run a specific seeder class from database/seeders/", { default: "" }).option("--allow-protected", "Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)", { default: !1 }).option("--fresh", "Truncate tables before seeding", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
10
+ buddy.command("seed", descriptions.seed).alias("db:seed").option("-p, --project [project]", descriptions.project, { default: !1 }).option("--only [models]", "Comma-separated list of models to seed", { default: "" }).option("--except [models]", "Comma-separated list of models to skip", { default: "" }).option("--include-defaults", "Also seed the framework's built-in models", { default: !1 }).option("--allow-protected", "Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)", { default: !1 }).option("--fresh", "Truncate tables before seeding", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
11
11
  log.debug("Running `buddy seed` ...", options);
12
- const perf = await intro("buddy seed");
13
- if (options.class) {
14
- const { injectGlobalAutoImports } = await import("@stacksjs/server");
15
- await injectGlobalAutoImports();
16
- const { runClassSeeders } = await import("@stacksjs/database"), result = await runClassSeeders({ class: options.class });
17
- await outro(`Class seeders: ran=${result.ran.length}, skipped=${result.skipped.length}`, { startTime: perf, useSeconds: !0 });
18
- process.exit(result.ran.length > 0 ? ExitCode.Success : ExitCode.FatalError);
19
- }
20
- const { injectGlobalAutoImports } = await import("@stacksjs/server");
12
+ const perf = await intro("buddy seed"), { injectGlobalAutoImports } = await import("@stacksjs/server");
21
13
  await injectGlobalAutoImports();
22
- const { runClassSeeders, listSeedableModels } = await import("@stacksjs/database"), { path } = await import("@stacksjs/path"), { fs } = await import("@stacksjs/storage"), seedableModels = await listSeedableModels(), seedersDir = path.projectPath("database/seeders"), existingSeederFiles = fs.existsSync(seedersDir) ? new Set(fs.readdirSync(seedersDir).filter((f) => f.endsWith(".ts"))) : new Set, unmigratedModels = seedableModels.filter((m) => !existingSeederFiles.has(`${m.name}Seeder.ts`));
23
- if (unmigratedModels.length > 0)
24
- log.warn(`[seed] ${unmigratedModels.length} model(s) declare the deprecated \`useSeeder\` trait but have no class seeder file: ${unmigratedModels.map((m) => m.name).join(", ")}. ` + "The auto-walker has been removed (stacksjs/stacks#1919) \u2014 these models will NOT seed. " + "Run `./buddy seed:scaffold` to codemod a class seeder per model and strip the trait (stacksjs/stacks#1929), then re-run `./buddy seed`.");
25
- const classResult = await runClassSeeders(), APP_ENV = process.env.APP_ENV || "local", ran = classResult.ran.length;
26
- await outro(`Seeded your ${APP_ENV} database. Class seeders: ran=${ran}, skipped=${classResult.skipped.length}.`, { startTime: perf, useSeconds: !0 });
27
- process.exit(ExitCode.Success);
28
- });
29
- buddy.command("seed:scaffold", "Generate class seeders for every model with a useSeeder trait (codemod for stacksjs/stacks#1919)").option("--force", "Overwrite existing seeder files", { default: !1 }).option("--dry-run", "Print what would be generated without writing files", { default: !1 }).action(async (options) => {
30
- const perf = await intro("buddy seed:scaffold");
31
- try {
32
- const { scaffoldClassSeedersFromModels } = await import("@stacksjs/database"), result = await scaffoldClassSeedersFromModels({
33
- force: options.force,
34
- dryRun: options.dryRun
35
- }), generated = result.generated.length, alreadyThere = result.skipped.filter((s) => s.reason === "already-exists").length, stripped = result.strippedTrait.length, errors = result.errors.length;
36
- for (const g of result.generated)
37
- console.log(` + ${g.model} \u2192 ${g.file}`);
38
- for (const s of result.skipped.filter((s) => s.reason === "already-exists"))
39
- console.log(` \xB7 ${s.model}: seeder exists (pass --force to overwrite)`);
40
- for (const t of result.strippedTrait)
41
- console.log(` - ${t.model}: removed useSeeder trait from model`);
42
- for (const t of result.traitStripSkipped)
43
- log.warn(` ! ${t.model}: useSeeder value couldn't be auto-removed \u2014 strip it manually (${t.file})`);
44
- for (const e of result.errors)
45
- log.warn(` ! ${e.model}: ${e.error}`);
46
- const verb = options.dryRun ? "would generate" : "generated", strippedVerb = options.dryRun ? "would strip" : "stripped";
47
- await outro(`Seeder scaffold: ${verb} ${generated}, skipped ${alreadyThere} existing, ${strippedVerb} ${stripped} trait(s), ${errors} error(s).`, { startTime: perf, useSeconds: !0 });
48
- process.exit(errors > 0 && generated === 0 ? ExitCode.FatalError : ExitCode.Success);
49
- } catch (err) {
50
- await outro("seed:scaffold failed", { startTime: perf, useSeconds: !0 }, err);
51
- process.exit(ExitCode.FatalError);
14
+ const { seed: seedDatabase } = await import("@stacksjs/database"), list = (value) => value ? value.split(",").map((entry) => entry.trim()).filter(Boolean) : void 0, summary = await seedDatabase({
15
+ verbose: options.verbose,
16
+ fresh: options.fresh,
17
+ only: list(options.only),
18
+ except: list(options.except),
19
+ includeDefaults: options.includeDefaults,
20
+ allowProtected: options.allowProtected
21
+ }), APP_ENV = process.env.APP_ENV || "local";
22
+ if (summary.total === 0) {
23
+ await outro("No models declare a `useSeeder` trait \u2014 nothing to seed.", { startTime: perf, useSeconds: !0 });
24
+ process.exit(ExitCode.Success);
52
25
  }
26
+ await outro(`Seeded your ${APP_ENV} database. ${summary.successful}/${summary.total} model(s) seeded${summary.failed > 0 ? `, ${summary.failed} failed` : ""}.`, { startTime: perf, useSeconds: !0 });
27
+ process.exit(summary.failed > 0 ? ExitCode.FatalError : ExitCode.Success);
53
28
  });
54
29
  buddy.command("seed:roles", "Seed default RBAC role packs (admin, dev, client)").alias("roles:seed").action(async () => {
55
30
  const perf = await intro("buddy seed:roles");