@stacksjs/buddy 0.70.208 → 0.70.210

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.
@@ -0,0 +1,43 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function resolveAppleDesktopConfig(options?: AppleDesktopOptions): AppleDesktopConfig;
3
+ export declare function validateAppleDesktopConfig(config: AppleDesktopConfig, includeApi?: boolean): string[];
4
+ export declare function renderInfoPlist(config: AppleDesktopConfig, executable?: string): string;
5
+ export declare function renderAppEntitlements(config: AppleDesktopConfig): string;
6
+ export declare function renderHelperEntitlements(): string;
7
+ export declare function renderAppleWorkflowCaller(): string;
8
+ export declare function desktopApple(buddy: CLI): void;
9
+ declare interface AppleDesktopOptions {
10
+ appName?: string
11
+ bundleId?: string
12
+ teamId?: string
13
+ appVersion?: string
14
+ buildNumber?: string
15
+ minimumMacos?: string
16
+ category?: string
17
+ appSigningIdentity?: string
18
+ installerSigningIdentity?: string
19
+ provisioningProfile?: string
20
+ apiKeyId?: string
21
+ apiIssuerId?: string
22
+ apiKeyPath?: string
23
+ icon?: string
24
+ skipBuild?: boolean
25
+ validateOnly?: boolean
26
+ packageOnly?: boolean
27
+ }
28
+ export declare interface AppleDesktopConfig {
29
+ appName: string
30
+ bundleId: string
31
+ teamId: string
32
+ version: string
33
+ buildNumber: string
34
+ minimumMacos: string
35
+ category: string
36
+ appSigningIdentity: string
37
+ installerSigningIdentity: string
38
+ provisioningProfile: string
39
+ apiKeyId: string
40
+ apiIssuerId: string
41
+ apiKeyPath: string
42
+ icon?: string
43
+ }
@@ -0,0 +1,375 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync
10
+ } from "node:fs";
11
+ import { basename, join, resolve } from "node:path";
12
+ import process from "node:process";
13
+ import { log } from "@stacksjs/cli";
14
+ import { projectPath, storagePath } from "@stacksjs/path";
15
+ const REQUIRED_TOOLS = ["codesign", "security", "productbuild", "pkgutil", "xcrun"];
16
+ function env(name) {
17
+ return process.env[name]?.trim() || "";
18
+ }
19
+ function xml(value) {
20
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
21
+ }
22
+ function packageMetadata() {
23
+ const packageJson = JSON.parse(readFileSync(projectPath("package.json"), "utf8"));
24
+ return {
25
+ name: packageJson.name || "Stacks",
26
+ version: packageJson.version || "0.1.0"
27
+ };
28
+ }
29
+ export function resolveAppleDesktopConfig(options = {}) {
30
+ const metadata = packageMetadata(), provisioningProfile = options.provisioningProfile || env("APPLE_PROVISIONING_PROFILE"), apiKeyPath = options.apiKeyPath || env("APP_STORE_CONNECT_API_KEY_PATH");
31
+ return {
32
+ appName: options.appName || env("APPLE_APP_NAME") || env("APP_NAME") || metadata.name,
33
+ bundleId: options.bundleId || env("APPLE_BUNDLE_ID"),
34
+ teamId: options.teamId || env("APPLE_TEAM_ID"),
35
+ version: options.appVersion || env("APPLE_APP_VERSION") || metadata.version,
36
+ buildNumber: options.buildNumber || env("APPLE_BUILD_NUMBER") || env("GITHUB_RUN_NUMBER") || String(Math.floor(Date.now() / 1000)),
37
+ minimumMacos: options.minimumMacos || env("APPLE_MINIMUM_MACOS") || "13.0",
38
+ category: options.category || env("APPLE_APP_CATEGORY") || "public.app-category.productivity",
39
+ appSigningIdentity: options.appSigningIdentity || env("APPLE_APP_SIGNING_IDENTITY"),
40
+ installerSigningIdentity: options.installerSigningIdentity || env("APPLE_INSTALLER_SIGNING_IDENTITY"),
41
+ provisioningProfile: provisioningProfile ? resolve(provisioningProfile) : "",
42
+ apiKeyId: options.apiKeyId || env("APP_STORE_CONNECT_API_KEY_ID"),
43
+ apiIssuerId: options.apiIssuerId || env("APP_STORE_CONNECT_API_ISSUER_ID"),
44
+ apiKeyPath: apiKeyPath ? resolve(apiKeyPath) : "",
45
+ icon: options.icon || env("APPLE_APP_ICON") || void 0
46
+ };
47
+ }
48
+ export function validateAppleDesktopConfig(config, includeApi = !0) {
49
+ const errors = [];
50
+ if (process.platform !== "darwin")
51
+ errors.push("Mac App Store packaging must run on macOS");
52
+ if (!config.appName)
53
+ errors.push("APPLE_APP_NAME or --app-name is required");
54
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(config.bundleId))
55
+ errors.push("APPLE_BUNDLE_ID must be a reverse-DNS bundle identifier");
56
+ if (!/^[A-Z0-9]{10}$/.test(config.teamId))
57
+ errors.push("APPLE_TEAM_ID must be the 10-character Apple Developer team ID");
58
+ if (!/^\d+(?:\.\d+){0,2}$/.test(config.version))
59
+ errors.push("The marketing version must contain one to three numeric components");
60
+ if (!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))
61
+ errors.push("The build number must contain only letters, numbers, periods, and hyphens");
62
+ if (!config.appSigningIdentity)
63
+ errors.push("APPLE_APP_SIGNING_IDENTITY is required");
64
+ if (!config.installerSigningIdentity)
65
+ errors.push("APPLE_INSTALLER_SIGNING_IDENTITY is required");
66
+ if (!config.provisioningProfile || !existsSync(config.provisioningProfile))
67
+ errors.push("APPLE_PROVISIONING_PROFILE must point to an existing .provisionprofile file");
68
+ if (config.icon && !existsSync(resolve(config.icon)))
69
+ errors.push(`Apple app icon does not exist: ${config.icon}`);
70
+ if (includeApi) {
71
+ if (!config.apiKeyId)
72
+ errors.push("APP_STORE_CONNECT_API_KEY_ID is required");
73
+ if (!config.apiIssuerId)
74
+ errors.push("APP_STORE_CONNECT_API_ISSUER_ID is required");
75
+ if (!config.apiKeyPath || !existsSync(config.apiKeyPath))
76
+ errors.push("APP_STORE_CONNECT_API_KEY_PATH must point to an existing .p8 file");
77
+ }
78
+ for (const tool of REQUIRED_TOOLS)
79
+ if (!Bun.which(tool))
80
+ errors.push(`Required Apple command is unavailable: ${tool}`);
81
+ return errors;
82
+ }
83
+ export function renderInfoPlist(config, executable = "stacks-desktop") {
84
+ const iconEntry = config.icon ? `
85
+ <key>CFBundleIconFile</key>
86
+ <string>AppIcon</string>` : "";
87
+ return `<?xml version="1.0" encoding="UTF-8"?>
88
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
89
+ <plist version="1.0">
90
+ <dict>
91
+ <key>CFBundleDisplayName</key>
92
+ <string>${xml(config.appName)}</string>
93
+ <key>CFBundleExecutable</key>
94
+ <string>${xml(executable)}</string>
95
+ <key>CFBundleIdentifier</key>
96
+ <string>${xml(config.bundleId)}</string>
97
+ <key>CFBundleInfoDictionaryVersion</key>
98
+ <string>6.0</string>
99
+ <key>CFBundleName</key>
100
+ <string>${xml(config.appName)}</string>
101
+ <key>CFBundlePackageType</key>
102
+ <string>APPL</string>
103
+ <key>CFBundleShortVersionString</key>
104
+ <string>${xml(config.version)}</string>
105
+ <key>CFBundleVersion</key>
106
+ <string>${xml(config.buildNumber)}</string>
107
+ <key>LSApplicationCategoryType</key>
108
+ <string>${xml(config.category)}</string>
109
+ <key>LSMinimumSystemVersion</key>
110
+ <string>${xml(config.minimumMacos)}</string>${iconEntry}
111
+ <key>NSHighResolutionCapable</key>
112
+ <true/>
113
+ </dict>
114
+ </plist>
115
+ `;
116
+ }
117
+ export function renderAppEntitlements(config) {
118
+ return `<?xml version="1.0" encoding="UTF-8"?>
119
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
120
+ <plist version="1.0">
121
+ <dict>
122
+ <key>com.apple.application-identifier</key>
123
+ <string>${xml(config.teamId)}.${xml(config.bundleId)}</string>
124
+ <key>com.apple.developer.team-identifier</key>
125
+ <string>${xml(config.teamId)}</string>
126
+ <key>com.apple.security.app-sandbox</key>
127
+ <true/>
128
+ <key>com.apple.security.network.client</key>
129
+ <true/>
130
+ </dict>
131
+ </plist>
132
+ `;
133
+ }
134
+ export function renderHelperEntitlements() {
135
+ return `<?xml version="1.0" encoding="UTF-8"?>
136
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
137
+ <plist version="1.0">
138
+ <dict>
139
+ <key>com.apple.security.app-sandbox</key>
140
+ <true/>
141
+ <key>com.apple.security.inherit</key>
142
+ <true/>
143
+ </dict>
144
+ </plist>
145
+ `;
146
+ }
147
+ export function renderAppleWorkflowCaller() {
148
+ return `name: Publish Mac App Store
149
+
150
+ on:
151
+ workflow_dispatch:
152
+ inputs:
153
+ validate-only:
154
+ description: Validate the signed package without uploading it
155
+ required: false
156
+ default: true
157
+ type: boolean
158
+
159
+ jobs:
160
+ publish:
161
+ uses: stacksjs/stacks/.github/workflows/desktop-app-store.yml@main
162
+ with:
163
+ app-name: \${{ vars.APPLE_APP_NAME }}
164
+ bundle-id: \${{ vars.APPLE_BUNDLE_ID }}
165
+ team-id: \${{ vars.APPLE_TEAM_ID }}
166
+ desktop-url: \${{ vars.DESKTOP_URL }}
167
+ app-signing-identity: \${{ vars.APPLE_APP_SIGNING_IDENTITY }}
168
+ installer-signing-identity: \${{ vars.APPLE_INSTALLER_SIGNING_IDENTITY }}
169
+ validate-only: \${{ inputs.validate-only }}
170
+ secrets: inherit
171
+ `;
172
+ }
173
+ function command(args, cwd = projectPath()) {
174
+ const result = Bun.spawnSync(args, {
175
+ cwd,
176
+ stdin: "inherit",
177
+ stdout: "inherit",
178
+ stderr: "inherit"
179
+ });
180
+ if (result.exitCode !== 0)
181
+ throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with code ${result.exitCode}`);
182
+ }
183
+ function signingIdentityExists(identity) {
184
+ if (!identity)
185
+ return !1;
186
+ const result = Bun.spawnSync(["security", "find-identity", "-v"], {
187
+ stdout: "pipe",
188
+ stderr: "pipe"
189
+ });
190
+ return result.exitCode === 0 && result.stdout.toString().includes(identity);
191
+ }
192
+ function provisioningProfileMatches(config) {
193
+ if (!config.provisioningProfile || !existsSync(config.provisioningProfile))
194
+ return !1;
195
+ const result = Bun.spawnSync(["security", "cms", "-D", "-i", config.provisioningProfile], {
196
+ stdout: "pipe",
197
+ stderr: "pipe"
198
+ });
199
+ if (result.exitCode !== 0)
200
+ return !1;
201
+ const profile = result.stdout.toString();
202
+ return profile.includes(config.bundleId) && profile.includes(config.teamId);
203
+ }
204
+ async function buildDesktop() {
205
+ const { runAction } = await import("@stacksjs/actions"), { Action } = await import("@stacksjs/enums"), result = await runAction(Action.BuildDesktop);
206
+ if (result.isErr)
207
+ throw result.error;
208
+ }
209
+ function sha256(path) {
210
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
211
+ }
212
+ async function packageAppleDesktop(config, skipBuild = !1) {
213
+ const errors = validateAppleDesktopConfig(config, !1);
214
+ if (!signingIdentityExists(config.appSigningIdentity))
215
+ errors.push(`App signing identity is not installed: ${config.appSigningIdentity}`);
216
+ if (!signingIdentityExists(config.installerSigningIdentity))
217
+ errors.push(`Installer signing identity is not installed: ${config.installerSigningIdentity}`);
218
+ if (!provisioningProfileMatches(config))
219
+ errors.push(`Provisioning profile does not match ${config.teamId}.${config.bundleId}`);
220
+ if (errors.length)
221
+ throw Error(errors.join(`
222
+ `));
223
+ if (!skipBuild)
224
+ await buildDesktop();
225
+ const desktopDist = storagePath("framework/desktop-dist"), launcher = join(desktopDist, "stacks-desktop"), runtime = join(desktopDist, "craft-runtime"), manifest = join(desktopDist, "desktop.json");
226
+ for (const path of [launcher, runtime, manifest])
227
+ if (!existsSync(path))
228
+ throw Error(`Desktop build artifact is missing: ${path}`);
229
+ const appleDir = join(desktopDist, "apple"), appPath = join(appleDir, `${config.appName}.app`), contents = join(appPath, "Contents"), macosDir = join(contents, "MacOS"), resourcesDir = join(contents, "Resources");
230
+ if (existsSync(appleDir))
231
+ rmSync(appleDir, { recursive: !0 });
232
+ mkdirSync(macosDir, { recursive: !0 });
233
+ mkdirSync(resourcesDir, { recursive: !0 });
234
+ copyFileSync(launcher, join(macosDir, "stacks-desktop"));
235
+ copyFileSync(runtime, join(macosDir, "craft-runtime"));
236
+ copyFileSync(manifest, join(macosDir, "desktop.json"));
237
+ copyFileSync(config.provisioningProfile, join(contents, "embedded.provisionprofile"));
238
+ chmodSync(join(macosDir, "stacks-desktop"), 493);
239
+ chmodSync(join(macosDir, "craft-runtime"), 493);
240
+ if (config.icon)
241
+ copyFileSync(resolve(config.icon), join(resourcesDir, "AppIcon.icns"));
242
+ const infoPlist = join(contents, "Info.plist"), appEntitlements = join(appleDir, "app.entitlements"), helperEntitlements = join(appleDir, "helper.entitlements");
243
+ writeFileSync(infoPlist, renderInfoPlist(config));
244
+ writeFileSync(appEntitlements, renderAppEntitlements(config));
245
+ writeFileSync(helperEntitlements, renderHelperEntitlements());
246
+ command(["plutil", "-lint", infoPlist, appEntitlements, helperEntitlements]);
247
+ command([
248
+ "codesign",
249
+ "--force",
250
+ "--timestamp",
251
+ "--options",
252
+ "runtime",
253
+ "--entitlements",
254
+ helperEntitlements,
255
+ "--sign",
256
+ config.appSigningIdentity,
257
+ join(macosDir, "craft-runtime")
258
+ ]);
259
+ command([
260
+ "codesign",
261
+ "--force",
262
+ "--timestamp",
263
+ "--options",
264
+ "runtime",
265
+ "--entitlements",
266
+ appEntitlements,
267
+ "--sign",
268
+ config.appSigningIdentity,
269
+ appPath
270
+ ]);
271
+ command(["codesign", "--verify", "--deep", "--strict", "--verbose=2", appPath]);
272
+ command(["codesign", "-d", "--entitlements", ":-", appPath]);
273
+ const packagePath = join(appleDir, `${config.appName}-${config.version}-${config.buildNumber}.pkg`);
274
+ command([
275
+ "productbuild",
276
+ "--component",
277
+ appPath,
278
+ "/Applications",
279
+ "--sign",
280
+ config.installerSigningIdentity,
281
+ packagePath
282
+ ]);
283
+ command(["pkgutil", "--check-signature", packagePath]);
284
+ const packageHash = sha256(packagePath);
285
+ writeFileSync(join(appleDir, "checksums.sha256"), `${packageHash} ${basename(packagePath)}
286
+ `);
287
+ writeFileSync(join(appleDir, "apple-provenance.json"), `${JSON.stringify({
288
+ schemaVersion: "1.0.0",
289
+ appName: config.appName,
290
+ bundleId: config.bundleId,
291
+ teamId: config.teamId,
292
+ version: config.version,
293
+ buildNumber: config.buildNumber,
294
+ minimumMacos: config.minimumMacos,
295
+ package: { name: basename(packagePath), sha256: packageHash },
296
+ sourceRevision: Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: projectPath() }).stdout.toString().trim(),
297
+ craft: { sha256: sha256(join(macosDir, "craft-runtime")) }
298
+ }, null, 2)}
299
+ `);
300
+ return packagePath;
301
+ }
302
+ function validateOrUpload(packagePath, config, validateOnly) {
303
+ const errors = validateAppleDesktopConfig(config, !0);
304
+ if (errors.length)
305
+ throw Error(errors.join(`
306
+ `));
307
+ const common = [
308
+ "--type",
309
+ "macos",
310
+ "--file",
311
+ packagePath,
312
+ "--apiKey",
313
+ config.apiKeyId,
314
+ "--apiIssuer",
315
+ config.apiIssuerId
316
+ ];
317
+ process.env.API_PRIVATE_KEYS_DIR = resolve(config.apiKeyPath, "..");
318
+ command(["xcrun", "altool", "--validate-app", ...common]);
319
+ if (!validateOnly)
320
+ command(["xcrun", "altool", "--upload-app", ...common]);
321
+ }
322
+ function fail(error) {
323
+ log.error(error instanceof Error ? error.message : String(error));
324
+ process.exit(1);
325
+ }
326
+ export function desktopApple(buddy) {
327
+ buddy.command("desktop:apple:doctor", "Validate Mac App Store tooling, credentials, certificates, and project metadata").option("--app-name <name>", "Mac App Store display name").option("--bundle-id <id>", "Reverse-DNS bundle identifier").option("--team-id <id>", "Apple Developer team ID").option("--app-signing-identity <identity>", "Mac App Distribution signing identity").option("--installer-signing-identity <identity>", "Mac Installer Distribution signing identity").option("--provisioning-profile <path>", "Mac App Store provisioning profile").option("--api-key-id <id>", "App Store Connect API key ID").option("--api-issuer-id <id>", "App Store Connect API issuer ID").option("--api-key-path <path>", "App Store Connect AuthKey .p8 file").action((options) => {
328
+ try {
329
+ const config = resolveAppleDesktopConfig(options), errors = validateAppleDesktopConfig(config, !0);
330
+ if (!signingIdentityExists(config.appSigningIdentity))
331
+ errors.push(`App signing identity is not installed: ${config.appSigningIdentity}`);
332
+ if (!signingIdentityExists(config.installerSigningIdentity))
333
+ errors.push(`Installer signing identity is not installed: ${config.installerSigningIdentity}`);
334
+ if (!provisioningProfileMatches(config))
335
+ errors.push(`Provisioning profile does not match ${config.teamId}.${config.bundleId}`);
336
+ if (errors.length)
337
+ throw Error(errors.join(`
338
+ `));
339
+ log.success(`Mac App Store prerequisites are ready for ${config.bundleId}`);
340
+ } catch (error) {
341
+ fail(error);
342
+ }
343
+ });
344
+ buddy.command("desktop:apple:init", "Create a GitHub Actions caller for the reusable Stacks Mac App Store workflow").option("--force", "Replace an existing workflow").action((options) => {
345
+ try {
346
+ const workflowPath = projectPath(".github/workflows/apple-app-store.yml");
347
+ if (existsSync(workflowPath) && !options.force)
348
+ throw Error(`${workflowPath} already exists. Use --force to replace it.`);
349
+ mkdirSync(resolve(workflowPath, ".."), { recursive: !0 });
350
+ writeFileSync(workflowPath, renderAppleWorkflowCaller());
351
+ log.success(`Created ${workflowPath}`);
352
+ } catch (error) {
353
+ fail(error);
354
+ }
355
+ });
356
+ const addSharedOptions = (commandBuilder) => commandBuilder.option("--app-name <name>", "Mac App Store display name").option("--bundle-id <id>", "Reverse-DNS bundle identifier").option("--team-id <id>", "Apple Developer team ID").option("--app-version <version>", "Marketing version").option("--build-number <number>", "Unique App Store build number").option("--minimum-macos <version>", "Minimum supported macOS version").option("--category <category>", "LSApplicationCategoryType value").option("--app-signing-identity <identity>", "Mac App Distribution signing identity").option("--installer-signing-identity <identity>", "Mac Installer Distribution signing identity").option("--provisioning-profile <path>", "Mac App Store provisioning profile").option("--icon <path>", "Optional .icns app icon").option("--skip-build", "Package existing storage/framework/desktop-dist artifacts");
357
+ addSharedOptions(buddy.command("desktop:apple:package", "Build, sandbox, sign, and package a Mac App Store desktop app")).action(async (options) => {
358
+ try {
359
+ const packagePath = await packageAppleDesktop(resolveAppleDesktopConfig(options), Boolean(options.skipBuild));
360
+ log.success(`Created signed Mac App Store package ${packagePath}`);
361
+ } catch (error) {
362
+ fail(error);
363
+ }
364
+ });
365
+ addSharedOptions(buddy.command("desktop:apple:publish", "Build and validate or upload a signed Mac App Store package")).option("--api-key-id <id>", "App Store Connect API key ID").option("--api-issuer-id <id>", "App Store Connect API issuer ID").option("--api-key-path <path>", "App Store Connect AuthKey .p8 file").option("--validate-only", "Validate with App Store Connect without uploading").option("--package-only", "Create the signed package without contacting App Store Connect").action(async (options) => {
366
+ try {
367
+ const config = resolveAppleDesktopConfig(options), packagePath = await packageAppleDesktop(config, Boolean(options.skipBuild));
368
+ if (!options.packageOnly)
369
+ validateOrUpload(packagePath, config, Boolean(options.validateOnly));
370
+ log.success(options.packageOnly ? `Created signed Mac App Store package ${packagePath}` : options.validateOnly ? `Validated ${packagePath} with App Store Connect` : `Uploaded ${packagePath} to App Store Connect`);
371
+ } catch (error) {
372
+ fail(error);
373
+ }
374
+ });
375
+ }
@@ -16,6 +16,10 @@ const commandRegistry = {
16
16
  new: { path: "./commands/create.js", exportName: "create" },
17
17
  deploy: { path: "./commands/deploy.js", exportName: "deploy" },
18
18
  dev: { path: "./commands/dev.js", exportName: "dev" },
19
+ "desktop:apple:doctor": { path: "./commands/desktop-apple.js", exportName: "desktopApple" },
20
+ "desktop:apple:init": { path: "./commands/desktop-apple.js", exportName: "desktopApple" },
21
+ "desktop:apple:package": { path: "./commands/desktop-apple.js", exportName: "desktopApple" },
22
+ "desktop:apple:publish": { path: "./commands/desktop-apple.js", exportName: "desktopApple" },
19
23
  dns: { path: "./commands/dns.js", exportName: "dns" },
20
24
  "dns:pull": { path: "./commands/dns.js", exportName: "dns" },
21
25
  "dns:diff": { path: "./commands/dns.js", exportName: "dns" },
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.208",
5
+ "version": "0.70.210",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [