@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.
- package/README.md +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/ai-context.js +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.d.ts +11 -1
- package/dist/commands/deploy.js +22 -4
- package/dist/commands/dev.js +62 -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 +4 -4
- 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 +27 -1
- package/package.json +43 -42
|
@@ -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 {
|
|
@@ -235,7 +235,7 @@ export function migrate(buddy) {
|
|
|
235
235
|
}
|
|
236
236
|
const lock = acquireMigrationLock();
|
|
237
237
|
if (!lock.acquired) {
|
|
238
|
-
log.syncError("Another migration is already running (
|
|
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.");
|
|
239
239
|
process.exit(ExitCode.FatalError);
|
|
240
240
|
}
|
|
241
241
|
if (!await confirmDestructiveMigrations({ force: options.force, fromDb: options.fromDb, applyRenames })) {
|
|
@@ -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 {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { craftEvidence, validateCraftEvidence } from "./craft-evidence";
|
|
3
|
+
describe("Craft protocol evidence", () => {
|
|
4
|
+
it("pins the full native lifecycle and release contract", () => {
|
|
5
|
+
expect(validateCraftEvidence(craftEvidence)).toEqual([]);
|
|
6
|
+
expect(craftEvidence.lifecycleMatrix).toHaveLength(4);
|
|
7
|
+
expect(craftEvidence.lifecycleOperations).toEqual(["install", "launch", "update", "rollback", "uninstall"]);
|
|
8
|
+
});
|
|
9
|
+
it("rejects untraceable source and premature signing claims", () => {
|
|
10
|
+
const invalid = structuredClone(craftEvidence);
|
|
11
|
+
Object.assign(invalid.source, { tag: "latest", revision: "short" });
|
|
12
|
+
Object.assign(invalid.release, { signing: "enforced", notarization: "enforced", blockedBy: "" });
|
|
13
|
+
expect(validateCraftEvidence(invalid)).toEqual(expect.arrayContaining([
|
|
14
|
+
"Craft tag does not match its version",
|
|
15
|
+
"Craft revision is not a full commit SHA",
|
|
16
|
+
"Craft signing/notarization cannot be promoted without retained release evidence",
|
|
17
|
+
"Craft release blocker must be linked"
|
|
18
|
+
]));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function validateLifecycleReport(report: LifecycleReport, expected: {
|
|
2
|
+
revision: string
|
|
3
|
+
platform: string
|
|
4
|
+
architecture: string
|
|
5
|
+
formats: string[]
|
|
6
|
+
}): string[];
|
|
7
|
+
export declare function run(): Promise<void>;
|
|
8
|
+
declare type LifecycleReport = {
|
|
9
|
+
revision?: string
|
|
10
|
+
orchestratorRevision?: string | null
|
|
11
|
+
status?: string
|
|
12
|
+
installLifecycleExercised?: boolean
|
|
13
|
+
runner?: { os?: string, arch?: string }
|
|
14
|
+
artifacts?: Array<{ format?: string, sha256?: string }>
|
|
15
|
+
steps?: Array<{ name?: string, status?: string }>
|
|
16
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export function validateLifecycleReport(report, expected) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
if (report.revision !== expected.revision)
|
|
6
|
+
errors.push(`Craft source revision mismatch: ${report.revision || "missing"}`);
|
|
7
|
+
if (report.status !== "passed")
|
|
8
|
+
errors.push(`Craft lifecycle status is ${report.status || "missing"}`);
|
|
9
|
+
if (report.installLifecycleExercised !== !0)
|
|
10
|
+
errors.push("privileged install lifecycle was not exercised");
|
|
11
|
+
if (report.runner?.os !== expected.platform || report.runner?.arch !== expected.architecture)
|
|
12
|
+
errors.push(`runner mismatch: ${report.runner?.os || "missing"}/${report.runner?.arch || "missing"}`);
|
|
13
|
+
const steps = new Map((report.steps || []).map((step) => [step.name, step.status]));
|
|
14
|
+
for (const name of ["install v1", "launch v1", "update to v2", "launch v2", "rollback to v1", "launch rollback", "uninstall", "verify uninstall"])
|
|
15
|
+
if (steps.get(name) !== "passed")
|
|
16
|
+
errors.push(`lifecycle step did not pass: ${name}`);
|
|
17
|
+
const artifacts = report.artifacts || [];
|
|
18
|
+
for (const format of expected.formats) {
|
|
19
|
+
const matching = artifacts.filter((artifact) => artifact.format === format);
|
|
20
|
+
if (matching.length !== 2)
|
|
21
|
+
errors.push(`expected two ${format} artifacts, found ${matching.length}`);
|
|
22
|
+
if (matching.some((artifact) => !/^[a-f0-9]{64}$/.test(artifact.sha256 || "")))
|
|
23
|
+
errors.push(`${format} artifact has an invalid digest`);
|
|
24
|
+
}
|
|
25
|
+
return errors;
|
|
26
|
+
}
|
|
27
|
+
export async function run() {
|
|
28
|
+
const value = (name) => {
|
|
29
|
+
const index = process.argv.indexOf(name), result = index === -1 ? void 0 : process.argv[index + 1];
|
|
30
|
+
if (!result)
|
|
31
|
+
throw Error(`${name} requires a value`);
|
|
32
|
+
return result;
|
|
33
|
+
}, reportPath = resolve(value("--report")), outputPath = resolve(value("--output")), expected = {
|
|
34
|
+
revision: value("--revision"),
|
|
35
|
+
platform: value("--platform"),
|
|
36
|
+
architecture: value("--architecture"),
|
|
37
|
+
formats: value("--formats").split(",")
|
|
38
|
+
}, report = JSON.parse(readFileSync(reportPath, "utf8")), errors = validateLifecycleReport(report, expected);
|
|
39
|
+
if (errors.length)
|
|
40
|
+
throw Error(errors.join(`
|
|
41
|
+
`));
|
|
42
|
+
const attestation = {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
status: "passed",
|
|
45
|
+
stacksRevision: process.env.GITHUB_SHA || "local",
|
|
46
|
+
craftRevision: expected.revision,
|
|
47
|
+
sourceReport: reportPath,
|
|
48
|
+
target: { platform: expected.platform, architecture: expected.architecture, formats: expected.formats },
|
|
49
|
+
checks: { lifecycle: "passed", artifactDigests: "passed", sourcePin: "passed" }
|
|
50
|
+
};
|
|
51
|
+
mkdirSync(dirname(outputPath), { recursive: !0 });
|
|
52
|
+
writeFileSync(outputPath, `${JSON.stringify(attestation, null, 2)}
|
|
53
|
+
`);
|
|
54
|
+
console.log(`Validated Craft lifecycle for ${expected.platform}/${expected.architecture}`);
|
|
55
|
+
}
|
|
56
|
+
if (import.meta.main)
|
|
57
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|