@stacksjs/buddy 0.70.162 → 0.70.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/ai-context.d.ts +13 -0
- package/dist/commands/ai-context.js +29 -0
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.d.ts +11 -1
- package/dist/commands/deploy.js +29 -28
- package/dist/commands/dev.js +44 -14
- package/dist/commands/docs/buddy-commands.d.ts +8 -0
- package/dist/commands/docs/buddy-commands.js +116 -0
- package/dist/commands/docs/buddy-commands.test.d.ts +1 -0
- package/dist/commands/docs/buddy-commands.test.js +38 -0
- package/dist/commands/docs/generated-artifacts.d.ts +3 -0
- package/dist/commands/docs/generated-artifacts.js +69 -0
- package/dist/commands/docs/generated-artifacts.test.d.ts +1 -0
- package/dist/commands/docs/generated-artifacts.test.js +19 -0
- package/dist/commands/docs/links.d.ts +18 -0
- package/dist/commands/docs/links.js +83 -0
- package/dist/commands/docs/links.test.d.ts +1 -0
- package/dist/commands/docs/links.test.js +73 -0
- package/dist/commands/docs.d.ts +7 -0
- package/dist/commands/docs.js +24 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/env.d.ts +14 -0
- package/dist/commands/env.js +73 -7
- package/dist/commands/generate.js +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/mail.js +1 -1
- package/dist/commands/migrate.js +9 -7
- package/dist/commands/protocol/craft-evidence.d.ts +76 -0
- package/dist/commands/protocol/craft-evidence.js +98 -0
- package/dist/commands/protocol/craft-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/craft-evidence.test.js +20 -0
- package/dist/commands/protocol/desktop-lifecycle-report.d.ts +16 -0
- package/dist/commands/protocol/desktop-lifecycle-report.js +57 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +32 -0
- package/dist/commands/protocol/desktop-support.d.ts +1 -0
- package/dist/commands/protocol/desktop-support.js +32 -0
- package/dist/commands/protocol/desktop-workflow.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-workflow.test.js +33 -0
- package/dist/commands/protocol/driver-registry.d.ts +1 -0
- package/dist/commands/protocol/driver-registry.js +71 -0
- package/dist/commands/protocol/pantry-evidence.d.ts +67 -0
- package/dist/commands/protocol/pantry-evidence.js +144 -0
- package/dist/commands/protocol/pantry-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/pantry-evidence.test.js +29 -0
- package/dist/commands/protocol/release-manifest.d.ts +30 -0
- package/dist/commands/protocol/release-manifest.js +102 -0
- package/dist/commands/protocol/release-manifest.test.d.ts +1 -0
- package/dist/commands/protocol/release-manifest.test.js +34 -0
- package/dist/commands/protocol/run-conformance.d.ts +21 -0
- package/dist/commands/protocol/run-conformance.js +358 -0
- package/dist/commands/protocol/run-conformance.test.d.ts +1 -0
- package/dist/commands/protocol/run-conformance.test.js +52 -0
- package/dist/commands/protocol/run-driver-contracts.d.ts +7 -0
- package/dist/commands/protocol/run-driver-contracts.js +94 -0
- package/dist/commands/protocol/run-driver-contracts.test.d.ts +1 -0
- package/dist/commands/protocol/run-driver-contracts.test.js +18 -0
- package/dist/commands/protocol/source-manifest.d.ts +25 -0
- package/dist/commands/protocol/source-manifest.js +113 -0
- package/dist/commands/protocol/sync-suite.d.ts +8 -0
- package/dist/commands/protocol/sync-suite.js +137 -0
- package/dist/commands/protocol/sync-suite.test.d.ts +1 -0
- package/dist/commands/protocol/sync-suite.test.js +44 -0
- package/dist/commands/protocol.d.ts +8 -0
- package/dist/commands/protocol.js +60 -0
- package/dist/commands/publish.js +149 -0
- package/dist/commands/run-tool.d.ts +7 -0
- package/dist/commands/run-tool.js +9 -0
- package/dist/commands/seed.js +15 -40
- package/dist/commands/serve.d.ts +18 -5
- package/dist/commands/serve.js +21 -2
- package/dist/commands/setup-ai.d.ts +39 -0
- package/dist/commands/setup-ai.js +121 -0
- package/dist/commands/setup.js +22 -0
- package/dist/lazy-commands.js +28 -1
- package/dist/project-setup.js +1 -0
- package/package.json +43 -43
package/README.md
CHANGED
|
@@ -344,7 +344,7 @@ For help, discussion about best practices, or any other conversation that would
|
|
|
344
344
|
|
|
345
345
|
For casual chit-chat with others using this package:
|
|
346
346
|
|
|
347
|
-
[Join the Stacks Discord Server](https://
|
|
347
|
+
[Join the Stacks Discord Server](https://stacksjs.com/discord)
|
|
348
348
|
|
|
349
349
|
## Postcardware
|
|
350
350
|
|
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ async function main() {
|
|
|
38
38
|
await applyBuddyConfig(buddy);
|
|
39
39
|
} catch {}
|
|
40
40
|
if (needsFullSetup) {
|
|
41
|
+
p.ensureRuntimeDirectories();
|
|
41
42
|
const { loadCommands, getCommandsToLoad, markLoaded } = await import("./lazy-commands.js"), { setup } = await import("./commands/setup.js");
|
|
42
43
|
setup(buddy);
|
|
43
44
|
markLoaded(buddy, "setup");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CLI, CliOptions } from '@stacksjs/types';
|
|
2
|
+
export declare function generateAiContextOutput(options?: AiContextCommandOptions, projectRoot?: string): AiContextCommandResult;
|
|
3
|
+
export declare function aiContext(buddy: CLI): void;
|
|
4
|
+
export declare interface AiContextCommandOptions extends CliOptions {
|
|
5
|
+
json?: boolean
|
|
6
|
+
output?: string
|
|
7
|
+
maxChars?: string | number
|
|
8
|
+
model?: string
|
|
9
|
+
}
|
|
10
|
+
export declare interface AiContextCommandResult {
|
|
11
|
+
output: string
|
|
12
|
+
outputPath: string | null
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { buildProjectContext } from "@stacksjs/ai";
|
|
5
|
+
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
6
|
+
export function generateAiContextOutput(options = {}, projectRoot = process.cwd()) {
|
|
7
|
+
const maxChars = options.maxChars === void 0 ? void 0 : Number(options.maxChars), result = buildProjectContext(projectRoot, { maxChars, model: options.model }), output = options.json ? `${JSON.stringify(result, null, 2)}
|
|
8
|
+
` : `${result.text}
|
|
9
|
+
|
|
10
|
+
Context metrics:
|
|
11
|
+
${JSON.stringify(result.metrics, null, 2)}
|
|
12
|
+
`;
|
|
13
|
+
if (!options.output)
|
|
14
|
+
return { output, outputPath: null };
|
|
15
|
+
const outputPath = resolve(projectRoot, String(options.output));
|
|
16
|
+
mkdirSync(dirname(outputPath), { recursive: !0 });
|
|
17
|
+
writeFileSync(outputPath, output);
|
|
18
|
+
return { output, outputPath };
|
|
19
|
+
}
|
|
20
|
+
export function aiContext(buddy) {
|
|
21
|
+
buddy.command("ai:context", "Generate compact deterministic project context for coding models").option("-J, --json", "Emit the versioned machine-readable context contract", { default: !1 }).option("-o, --output [path]", "Write output to a file instead of stdout").option("--max-chars [characters]", "Maximum characters in the prompt context payload", { default: 4000 }).option("--model [model]", "Model family used for the heuristic token estimate", { default: "gpt-4o" }).example("buddy ai:context").example("buddy ai:context --json").example("buddy ai:context --json --output storage/framework/runtime/ai-context.json").action((options) => {
|
|
22
|
+
const generated = generateAiContextOutput(options);
|
|
23
|
+
if (generated.outputPath)
|
|
24
|
+
console.log(`Wrote AI project context to ${generated.outputPath}`);
|
|
25
|
+
else
|
|
26
|
+
process.stdout.write(generated.output);
|
|
27
|
+
});
|
|
28
|
+
onUnknownSubcommand(buddy, "ai:context");
|
|
29
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -38,8 +38,8 @@ export function create(buddy) {
|
|
|
38
38
|
log.error(result.error);
|
|
39
39
|
process.exit(ExitCode.FatalError);
|
|
40
40
|
}
|
|
41
|
-
await ensureEnv(path, options);
|
|
42
41
|
ensureExecutableScripts(path);
|
|
42
|
+
await ensureEnv(path, options);
|
|
43
43
|
await install(path, options);
|
|
44
44
|
if (options.minimal)
|
|
45
45
|
await stripFeatures(path);
|
|
@@ -18,11 +18,21 @@ import type { CLI } from '@stacksjs/types';
|
|
|
18
18
|
* ciphertext it never had a chance to decrypt (no DOTENV_PRIVATE_KEY_* in
|
|
19
19
|
* that 2-key set).
|
|
20
20
|
*
|
|
21
|
+
* Keys namespaced to another tenant on this box (`cloud.tenants` in
|
|
22
|
+
* `config/cloud.ts`) are dropped here, before anything is shipped. Those
|
|
23
|
+
* projects deploy from their own repositories with their own env files and
|
|
24
|
+
* never need the owner's copy — and because ts-cloud treats `site.env` as the
|
|
25
|
+
* complete `.env`, leaving them in writes one tenant's secrets into an
|
|
26
|
+
* unrelated site's `.env` on disk.
|
|
27
|
+
*
|
|
21
28
|
* Returns `{}` (not an error) when the file doesn't exist or fails to
|
|
22
29
|
* parse — an app with no `.env.production` yet shouldn't block deploying
|
|
23
30
|
* with whatever `site.env` overrides it does have.
|
|
31
|
+
*
|
|
32
|
+
* @param environment - Which `.env.<environment>` to read.
|
|
33
|
+
* @param tsCloudConfig - The deploy target's ts-cloud config, read for `project.slug`.
|
|
24
34
|
*/
|
|
25
|
-
export declare function resolveDeployEnvValues(environment: 'production' | 'staging' | 'development'): Promise<Record<string, string>>;
|
|
35
|
+
export declare function resolveDeployEnvValues(environment: 'production' | 'staging' | 'development', tsCloudConfig?: { project?: { slug?: string } }): Promise<Record<string, string>>;
|
|
26
36
|
/**
|
|
27
37
|
* Merge the deploy-target's resolved env values underneath each site's own
|
|
28
38
|
* explicit `env` overrides, stripping a general `PORT` when the site
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { runAction } from "@stacksjs/actions";
|
|
6
6
|
import { italic, onUnknownSubcommand, outro, prompts, runCommand } from "@stacksjs/cli";
|
|
@@ -395,7 +395,7 @@ async function pollUntil(opts) {
|
|
|
395
395
|
let lastHeartbeat = 0;
|
|
396
396
|
for (;; )
|
|
397
397
|
try {
|
|
398
|
-
opts.check();
|
|
398
|
+
await opts.check();
|
|
399
399
|
return;
|
|
400
400
|
} catch {
|
|
401
401
|
const elapsedSecs = Math.floor((Date.now() - started) / 1000);
|
|
@@ -408,33 +408,18 @@ async function pollUntil(opts) {
|
|
|
408
408
|
await new Promise((r) => setTimeout(r, opts.intervalMs ?? 5000));
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
async function waitForRemoteReady(ip
|
|
412
|
-
const {
|
|
413
|
-
"-o",
|
|
414
|
-
"StrictHostKeyChecking=no",
|
|
415
|
-
"-o",
|
|
416
|
-
"UserKnownHostsFile=/dev/null",
|
|
417
|
-
"-o",
|
|
418
|
-
"BatchMode=yes",
|
|
419
|
-
"-o",
|
|
420
|
-
"ConnectTimeout=10",
|
|
421
|
-
`root@${ip}`
|
|
422
|
-
], run = (remote) => execSync(`ssh ${sshArgs.map((a) => `'${a}'`).join(" ")} '${remote.replace(/'/g, "'\\''")}'`, {
|
|
423
|
-
encoding: "utf8",
|
|
424
|
-
stdio: ["ignore", "pipe", verbose ? "inherit" : "pipe"]
|
|
425
|
-
}), sshWaitSecs = readWaitSecs("TS_CLOUD_SSH_WAIT_SECS", 480);
|
|
411
|
+
async function waitForRemoteReady(ip) {
|
|
412
|
+
const { sshExecOrThrow } = await import("@stacksjs/ts-cloud"), run = (remote) => sshExecOrThrow(ip, remote, { user: "root", connectTimeoutSec: 10 }), sshWaitSecs = readWaitSecs("TS_CLOUD_SSH_WAIT_SECS", 480);
|
|
426
413
|
await pollUntil({
|
|
427
414
|
label: "Waiting for SSH to come up",
|
|
428
415
|
timeoutSecs: sshWaitSecs,
|
|
429
|
-
check: () =>
|
|
430
|
-
run("true");
|
|
431
|
-
},
|
|
416
|
+
check: () => run("true"),
|
|
432
417
|
timeoutMessage: (elapsed) => `SSH did not become reachable on ${ip} within ${fmtDuration(sshWaitSecs)} (waited ${elapsed}s). ` + "The box may still be booting \u2014 raise TS_CLOUD_SSH_WAIT_SECS and retry."
|
|
433
418
|
});
|
|
434
419
|
log.success("SSH is up");
|
|
435
420
|
log.info("Waiting for cloud-init (installing bun + caddy)...");
|
|
436
421
|
try {
|
|
437
|
-
run("cloud-init status --wait || true");
|
|
422
|
+
await run("cloud-init status --wait || true");
|
|
438
423
|
} catch (err) {
|
|
439
424
|
log.debug("cloud-init status --wait returned non-zero (continuing):", err);
|
|
440
425
|
}
|
|
@@ -442,14 +427,20 @@ async function waitForRemoteReady(ip, verbose) {
|
|
|
442
427
|
await pollUntil({
|
|
443
428
|
label: "Waiting for the bun runtime",
|
|
444
429
|
timeoutSecs: bootWaitSecs,
|
|
445
|
-
check: () =>
|
|
446
|
-
run("test -x /usr/local/bin/bun");
|
|
447
|
-
},
|
|
430
|
+
check: () => run("test -x /usr/local/bin/bun"),
|
|
448
431
|
timeoutMessage: (elapsed) => `bun runtime did not appear at /usr/local/bin/bun within ${fmtDuration(bootWaitSecs)} (waited ${elapsed}s). ` + "cloud-init may have failed \u2014 SSH in and check /var/log/cloud-init-output.log; " + "raise TS_CLOUD_BOOT_WAIT_SECS for slow regions."
|
|
449
432
|
});
|
|
450
433
|
log.success("Server is ready (bun installed)");
|
|
451
434
|
}
|
|
452
|
-
|
|
435
|
+
async function resolveDeclaredTenants() {
|
|
436
|
+
try {
|
|
437
|
+
const { config } = await import("@stacksjs/config"), tenants = config.cloud?.tenants;
|
|
438
|
+
return Array.isArray(tenants) ? tenants.filter((slug) => typeof slug === "string") : [];
|
|
439
|
+
} catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
export async function resolveDeployEnvValues(environment, tsCloudConfig) {
|
|
453
444
|
const fileName = environment === "production" ? ".env.production" : environment === "staging" ? ".env.staging" : existsSync(p.projectPath(".env.development")) ? ".env.development" : ".env", filePath = p.projectPath(fileName);
|
|
454
445
|
if (!existsSync(filePath))
|
|
455
446
|
return {};
|
|
@@ -465,7 +456,13 @@ export async function resolveDeployEnvValues(environment) {
|
|
|
465
456
|
continue;
|
|
466
457
|
values[key] = String(value);
|
|
467
458
|
}
|
|
468
|
-
|
|
459
|
+
const { foreignTenantKeys, partitionTenantEnv } = await import("@stacksjs/env"), partition = partitionTenantEnv(values, {
|
|
460
|
+
self: tsCloudConfig?.project?.slug,
|
|
461
|
+
tenants: await resolveDeclaredTenants()
|
|
462
|
+
});
|
|
463
|
+
for (const { tenant, keys } of foreignTenantKeys(partition))
|
|
464
|
+
log.warn(`[deploy] Skipping ${keys.length} '${tenant}' key(s) in ${fileName} \u2014 they belong to that tenant's own ` + `repository, and shipping them writes its secrets into this project's site .env files. Remove them with: buddy env:check --file ${fileName}. Keys: ${keys.join(", ")}`);
|
|
465
|
+
return partition.own;
|
|
469
466
|
} catch (error) {
|
|
470
467
|
log.debug(`[deploy] Failed to resolve ${fileName} for site env merging:`, error);
|
|
471
468
|
return {};
|
|
@@ -716,7 +713,7 @@ async function runHetznerDeploy(args) {
|
|
|
716
713
|
log.error("Provisioned server has no public IP \u2014 cannot deploy over SSH.");
|
|
717
714
|
process.exit(ExitCode.FatalError);
|
|
718
715
|
}
|
|
719
|
-
await waitForRemoteReady(ip
|
|
716
|
+
await waitForRemoteReady(ip);
|
|
720
717
|
const { execSync } = await import("node:child_process"), { tmpdir } = await import("node:os"), sites = applyEnvironmentToSites(tsCloudConfig.sites || {}, environment, tsCloudConfig), slug = tsCloudConfig.project?.slug || "app";
|
|
721
718
|
let sha;
|
|
722
719
|
try {
|
|
@@ -732,7 +729,11 @@ async function runHetznerDeploy(args) {
|
|
|
732
729
|
".cache",
|
|
733
730
|
"bin",
|
|
734
731
|
"dist",
|
|
732
|
+
relative(p.projectPath(), p.stxPath()).replace(/\/$/, ""),
|
|
735
733
|
".stx",
|
|
734
|
+
relative(p.projectPath(), p.cloudStatePath()).replace(/\/$/, ""),
|
|
735
|
+
".ts-cloud",
|
|
736
|
+
relative(p.projectPath(), p.frameworkRuntimePath()).replace(/\/$/, ""),
|
|
736
737
|
"tmp",
|
|
737
738
|
"temp",
|
|
738
739
|
".DS_Store",
|
|
@@ -771,7 +772,7 @@ async function runHetznerDeploy(args) {
|
|
|
771
772
|
}
|
|
772
773
|
if (docker)
|
|
773
774
|
await buildContainerImageWithPantry({ slug, sites, verbose });
|
|
774
|
-
const resolvedDeployEnv = await resolveDeployEnvValues(environment), sitesWithResolvedEnv = mergeSiteDeployEnv(sites, resolvedDeployEnv);
|
|
775
|
+
const resolvedDeployEnv = await resolveDeployEnvValues(environment, tsCloudConfig), sitesWithResolvedEnv = mergeSiteDeployEnv(sites, resolvedDeployEnv);
|
|
775
776
|
for (const [envKey, envValue] of Object.entries(resolvedDeployEnv))
|
|
776
777
|
if (process.env[envKey] === void 0)
|
|
777
778
|
process.env[envKey] = envValue;
|
package/dist/commands/dev.js
CHANGED
|
@@ -4,10 +4,10 @@ import readline from "node:readline";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { bold, cyan, dim, green, intro, log, onUnknownSubcommand, outro, prompts, runCommand, yellow } from "@stacksjs/cli";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { dirname, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { Action } from "@stacksjs/enums";
|
|
10
|
-
import { libsPath, projectPath } from "@stacksjs/path";
|
|
10
|
+
import { libsPath, projectPath, stxPath } from "@stacksjs/path";
|
|
11
11
|
import { ExitCode } from "@stacksjs/types";
|
|
12
12
|
import { version } from "../../package.json";
|
|
13
13
|
const DEV_BOOT_STARTING_LINE_COUNT = 3;
|
|
@@ -215,17 +215,32 @@ export function dev(buddy) {
|
|
|
215
215
|
onUnknownSubcommand(buddy, "dev");
|
|
216
216
|
}
|
|
217
217
|
export async function startDevelopmentServer(_options, _startTime) {
|
|
218
|
-
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1",
|
|
218
|
+
const options = _options, startedAt = _startTime, appUrl = process.env.APP_URL ?? "stacks.localhost", nativeMode = options.native === !0, proxyManagedExternally = process.env.STACKS_PROXY_MANAGED === "1", preferredFrontendPort = Number(process.env.PORT) || 3000, preferredApiPort = Number(process.env.PORT_API) || 3008, preferredDocsPort = Number(process.env.PORT_DOCS) || 3006, preferredDashboardPort = Number(process.env.PORT_ADMIN) || 3002;
|
|
219
|
+
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
220
|
+
await cleanupStaleDevProcesses([
|
|
221
|
+
preferredFrontendPort,
|
|
222
|
+
preferredApiPort,
|
|
223
|
+
preferredDocsPort,
|
|
224
|
+
preferredDashboardPort
|
|
225
|
+
]);
|
|
226
|
+
const portShifts = [], claimPort = async (label, preferred) => {
|
|
227
|
+
if (proxyManagedExternally)
|
|
228
|
+
return preferred;
|
|
229
|
+
const port = await findAvailablePort(preferred);
|
|
230
|
+
if (port !== preferred)
|
|
231
|
+
portShifts.push(`${label} :${preferred} \u2192 :${port}`);
|
|
232
|
+
return port;
|
|
233
|
+
}, frontendPort = await claimPort("Frontend", preferredFrontendPort), apiPort = await claimPort("API", preferredApiPort), docsPort = await claimPort("Docs", preferredDocsPort), dashboardPort = await claimPort("Dashboard", preferredDashboardPort);
|
|
234
|
+
process.env.PORT = String(frontendPort);
|
|
235
|
+
process.env.PORT_API = String(apiPort);
|
|
236
|
+
process.env.PORT_DOCS = String(docsPort);
|
|
237
|
+
process.env.PORT_ADMIN = String(dashboardPort);
|
|
238
|
+
const includeDashboard = process.env.STACKS_DEV_DASHBOARD === "1", domain = resolvePrettyDevDomain(appUrl, nativeMode), appLooksCustom = domain !== null, localhostOnly = process.env.STACKS_DEV_LOCALHOST === "1", prettyUrlsRequested = appLooksCustom && !localhostOnly, systemAuthorized = !prettyUrlsRequested || proxyManagedExternally ? !0 : await canStartPrettyDevProxy(), usePrettyUrls = shouldUsePrettyDevUrls({
|
|
219
239
|
domain,
|
|
220
240
|
localhostOnly,
|
|
221
241
|
proxyManagedExternally,
|
|
222
242
|
systemAuthorized
|
|
223
|
-
}), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null, frontendUrl = displayedDomain ? `https://${displayedDomain}` : `http://localhost:${frontendPort}`, apiUrl = displayedDomain ? `https://${displayedDomain}/api` : `http://localhost:${apiPort}`, docsUrl = displayedDomain ? `https://${displayedDomain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}
|
|
224
|
-
frontendPort,
|
|
225
|
-
apiPort,
|
|
226
|
-
docsPort,
|
|
227
|
-
...includeDashboard ? [dashboardPort] : []
|
|
228
|
-
];
|
|
243
|
+
}), prettySetupRequired = prettyUrlsRequested && !usePrettyUrls, hasCustomDomain = usePrettyUrls && !proxyManagedExternally, displayedDomain = usePrettyUrls ? domain : null, dashboardDomain = displayedDomain ? `dashboard.${displayedDomain}` : null, frontendUrl = displayedDomain ? `https://${displayedDomain}` : `http://localhost:${frontendPort}`, apiUrl = displayedDomain ? `https://${displayedDomain}/api` : `http://localhost:${apiPort}`, docsUrl = displayedDomain ? `https://${displayedDomain}/docs` : `http://localhost:${docsPort}`, dashboardUrl = dashboardDomain ? `https://${dashboardDomain}` : `http://localhost:${dashboardPort}`;
|
|
229
244
|
process.env.STACKS_PROXY_MANAGED = "1";
|
|
230
245
|
process.env.STACKS_DEV_QUIET = "1";
|
|
231
246
|
console.log();
|
|
@@ -234,10 +249,12 @@ export async function startDevelopmentServer(_options, _startTime) {
|
|
|
234
249
|
console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);
|
|
235
250
|
console.log();
|
|
236
251
|
}
|
|
252
|
+
if (portShifts.length > 0) {
|
|
253
|
+
console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);
|
|
254
|
+
console.log();
|
|
255
|
+
}
|
|
237
256
|
console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);
|
|
238
257
|
console.log();
|
|
239
|
-
if (process.env.STACKS_DEV_NO_KILL !== "1")
|
|
240
|
-
await cleanupStaleDevProcesses(managedPorts);
|
|
241
258
|
const rpxTlsPreflight = hasCustomDomain && domain ? prepareRpxTlsForDev({ domain, includeDashboard, options }) : Promise.resolve();
|
|
242
259
|
if (localhostOnly && domain && !proxyManagedExternally)
|
|
243
260
|
removeStalePublicDomainOverrides(domain, includeDashboard, options.verbose ?? !1).catch(() => {});
|
|
@@ -432,16 +449,16 @@ function printDevReadyBanner(input) {
|
|
|
432
449
|
console.log();
|
|
433
450
|
}
|
|
434
451
|
function printDevEngineNotes() {
|
|
435
|
-
const routesFile =
|
|
452
|
+
const routesFile = stxPath("routes.ts"), routesLabel = relative(projectPath(), routesFile), crosswindConfig = join(projectPath(), "config/crosswind.ts");
|
|
436
453
|
if (existsSync(crosswindConfig) || existsSync(join(projectPath(), "crosswind.config.ts")))
|
|
437
454
|
console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);
|
|
438
455
|
if (existsSync(routesFile))
|
|
439
456
|
try {
|
|
440
457
|
const routeCount = (readFileSync(routesFile, "utf8").match(/pattern:/g) ?? []).length;
|
|
441
458
|
if (routeCount > 0)
|
|
442
|
-
console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192
|
|
459
|
+
console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`);
|
|
443
460
|
} catch {
|
|
444
|
-
console.log(` ${green("[stx]")} ${dim(
|
|
461
|
+
console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`);
|
|
445
462
|
}
|
|
446
463
|
}
|
|
447
464
|
async function launchNativeAppWindow(url, options) {
|
|
@@ -617,6 +634,19 @@ async function probeDevServerHttp(port) {
|
|
|
617
634
|
return !0;
|
|
618
635
|
return !1;
|
|
619
636
|
}
|
|
637
|
+
async function findAvailablePort(preferred, attempts = 20) {
|
|
638
|
+
for (let port = preferred;port < preferred + attempts; port++) {
|
|
639
|
+
let server;
|
|
640
|
+
try {
|
|
641
|
+
server = Bun.serve({ port, hostname: "0.0.0.0", reusePort: !1, fetch: () => new Response });
|
|
642
|
+
} catch {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
server.stop(!0);
|
|
646
|
+
return port;
|
|
647
|
+
}
|
|
648
|
+
return preferred;
|
|
649
|
+
}
|
|
620
650
|
async function waitForPort(port, timeoutMs) {
|
|
621
651
|
const deadline = Date.now() + timeoutMs;
|
|
622
652
|
while (Date.now() < deadline) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuddyCommandInventoryEntry } from '../list';
|
|
2
|
+
export declare function renderBuddyCommandReference(inventory: BuddyInventory): string;
|
|
3
|
+
export declare function loadBuddyInventory(): BuddyInventory;
|
|
4
|
+
export declare function run(): Promise<void>;
|
|
5
|
+
declare interface BuddyInventory {
|
|
6
|
+
commands: BuddyCommandInventoryEntry[]
|
|
7
|
+
total: number
|
|
8
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), outputPath = resolve(root, "docs/guide/buddy/commands.md"), cliPath = resolve(root, "storage/framework/core/buddy/src/cli.ts");
|
|
4
|
+
function markdown(value) {
|
|
5
|
+
return String(value).replaceAll("\\", "\\\\").replaceAll("|", "\\|").replaceAll(`
|
|
6
|
+
`, "<br>");
|
|
7
|
+
}
|
|
8
|
+
function code(value) {
|
|
9
|
+
return `\`${String(value).replaceAll("`", "\\`")}\``;
|
|
10
|
+
}
|
|
11
|
+
function optionFlags(option) {
|
|
12
|
+
return option.flags.map((flag) => code(`${flag.length === 1 ? "-" : "--"}${flag}`)).join(", ");
|
|
13
|
+
}
|
|
14
|
+
function optionKind(option) {
|
|
15
|
+
return [
|
|
16
|
+
option.boolean ? "boolean" : "value",
|
|
17
|
+
option.required ? "required" : "optional",
|
|
18
|
+
...option.negated ? ["negated"] : []
|
|
19
|
+
].join(", ");
|
|
20
|
+
}
|
|
21
|
+
function argumentSyntax(argument) {
|
|
22
|
+
const value = `${argument.name}${argument.variadic ? "..." : ""}`;
|
|
23
|
+
return code(argument.required ? `<${value}>` : `[${value}]`);
|
|
24
|
+
}
|
|
25
|
+
function commandSection(command) {
|
|
26
|
+
const lines = [
|
|
27
|
+
`### ${code(command.name)}`,
|
|
28
|
+
"",
|
|
29
|
+
command.description || "No description provided.",
|
|
30
|
+
"",
|
|
31
|
+
`- Usage: ${code(command.usage)}`,
|
|
32
|
+
`- Namespace: ${command.namespace ? code(command.namespace) : "none"}`,
|
|
33
|
+
`- Aliases: ${command.aliases.length ? command.aliases.map(code).join(", ") : "none"}`,
|
|
34
|
+
`- Arguments: ${command.arguments.length ? command.arguments.map(argumentSyntax).join(", ") : "none"}`
|
|
35
|
+
];
|
|
36
|
+
if (command.options.length)
|
|
37
|
+
lines.push("", "| Option | Description | Contract | Default |", "| --- | --- | --- | --- |", ...command.options.map((option) => `| ${optionFlags(option)} | ${markdown(option.description || "\u2014")} | ${optionKind(option)} | ${option.default === void 0 ? "\u2014" : code(JSON.stringify(option.default))} |`));
|
|
38
|
+
if (command.examples.length)
|
|
39
|
+
lines.push("", "Examples:", "", "```bash", ...command.examples, "```");
|
|
40
|
+
return lines.join(`
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
export function renderBuddyCommandReference(inventory) {
|
|
44
|
+
const commands = [...inventory.commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
if (inventory.total !== commands.length)
|
|
46
|
+
throw Error(`Buddy inventory total ${inventory.total} does not match ${commands.length} commands`);
|
|
47
|
+
if (new Set(commands.map((command) => command.name)).size !== commands.length)
|
|
48
|
+
throw Error("Buddy inventory contains duplicate canonical command names");
|
|
49
|
+
const groups = new Map;
|
|
50
|
+
for (const command of commands) {
|
|
51
|
+
const group = command.namespace || (command.name.includes(":") ? command.name.split(":")[0] : "general"), entries = groups.get(group) || [];
|
|
52
|
+
entries.push(command);
|
|
53
|
+
groups.set(group, entries);
|
|
54
|
+
}
|
|
55
|
+
const groupSummary = [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([group, entries]) => `| ${code(group)} | ${entries.length} |`);
|
|
56
|
+
return `${[
|
|
57
|
+
"---",
|
|
58
|
+
"title: Buddy Command Reference",
|
|
59
|
+
"description: Generated reference for every Buddy command, argument, option, alias, default, and example.",
|
|
60
|
+
"---",
|
|
61
|
+
"<!-- Generated by storage/framework/core/buddy/src/commands/docs/buddy-commands.ts. Do not edit by hand. -->",
|
|
62
|
+
"",
|
|
63
|
+
"# Buddy Command Reference",
|
|
64
|
+
"",
|
|
65
|
+
`This reference is generated from Buddy's runtime command registry and currently contains **${commands.length} commands**. Run \`bun run docs:buddy\` after changing the registry; CI rejects stale output.`,
|
|
66
|
+
"",
|
|
67
|
+
"## Command groups",
|
|
68
|
+
"",
|
|
69
|
+
"| Group | Commands |",
|
|
70
|
+
"| --- | ---: |",
|
|
71
|
+
...groupSummary,
|
|
72
|
+
"",
|
|
73
|
+
"## Commands",
|
|
74
|
+
"",
|
|
75
|
+
...commands.flatMap((command) => [commandSection(command), ""])
|
|
76
|
+
].join(`
|
|
77
|
+
`).split(`
|
|
78
|
+
`).map((line) => line.trimEnd()).join(`
|
|
79
|
+
`).trimEnd()}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
export function loadBuddyInventory() {
|
|
83
|
+
const result = Bun.spawnSync([process.execPath, cliPath, "list", "--json"], {
|
|
84
|
+
cwd: root,
|
|
85
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
86
|
+
stdout: "pipe",
|
|
87
|
+
stderr: "pipe"
|
|
88
|
+
});
|
|
89
|
+
if (result.exitCode !== 0)
|
|
90
|
+
throw Error(`Buddy command inventory failed (${result.exitCode}): ${result.stderr.toString().trim()}`);
|
|
91
|
+
const inventory = JSON.parse(result.stdout.toString());
|
|
92
|
+
if (!Array.isArray(inventory.commands) || !Number.isInteger(inventory.total))
|
|
93
|
+
throw Error("Buddy command inventory returned an invalid payload");
|
|
94
|
+
return inventory;
|
|
95
|
+
}
|
|
96
|
+
export async function run() {
|
|
97
|
+
const mode = process.argv.includes("--write") ? "write" : process.argv.includes("--check") ? "check" : null;
|
|
98
|
+
if (!mode) {
|
|
99
|
+
console.error("usage: bun storage/framework/core/buddy/src/commands/docs/buddy-commands.ts --write | --check");
|
|
100
|
+
process.exit(2);
|
|
101
|
+
}
|
|
102
|
+
const expected = renderBuddyCommandReference(loadBuddyInventory());
|
|
103
|
+
if (mode === "write") {
|
|
104
|
+
mkdirSync(dirname(outputPath), { recursive: !0 });
|
|
105
|
+
writeFileSync(outputPath, expected);
|
|
106
|
+
console.log(`Wrote ${outputPath}`);
|
|
107
|
+
} else {
|
|
108
|
+
if (!existsSync(outputPath))
|
|
109
|
+
throw Error("Buddy command reference is missing; run bun run docs:buddy");
|
|
110
|
+
if (readFileSync(outputPath, "utf8") !== expected)
|
|
111
|
+
throw Error("Buddy command reference is stale; run bun run docs:buddy");
|
|
112
|
+
console.log("Buddy command reference matches the runtime registry");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (import.meta.main)
|
|
116
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { renderBuddyCommandReference } from "./buddy-commands";
|
|
3
|
+
const command = {
|
|
4
|
+
name: "deploy",
|
|
5
|
+
description: "Deploy the application",
|
|
6
|
+
aliases: ["ship"],
|
|
7
|
+
usage: "$ buddy deploy [environment]",
|
|
8
|
+
arguments: [{ name: "environment", required: !1, variadic: !1 }],
|
|
9
|
+
options: [{
|
|
10
|
+
name: "region",
|
|
11
|
+
flags: ["r", "region"],
|
|
12
|
+
description: "Cloud region",
|
|
13
|
+
required: !0,
|
|
14
|
+
boolean: !1,
|
|
15
|
+
negated: !1,
|
|
16
|
+
default: "us-east-1"
|
|
17
|
+
}],
|
|
18
|
+
examples: ["buddy deploy production"]
|
|
19
|
+
};
|
|
20
|
+
describe("Buddy command reference", () => {
|
|
21
|
+
it("renders every command contract field", () => {
|
|
22
|
+
const result = renderBuddyCommandReference({ commands: [command], total: 1 });
|
|
23
|
+
expect(result).toContain("**1 commands**");
|
|
24
|
+
expect(result).toContain("### `deploy`");
|
|
25
|
+
expect(result).toContain("Aliases: `ship`");
|
|
26
|
+
expect(result).toContain("Arguments: `[environment]`");
|
|
27
|
+
expect(result).toContain('| `-r`, `--region` | Cloud region | value, required | `"us-east-1"` |');
|
|
28
|
+
expect(result).toContain("buddy deploy production");
|
|
29
|
+
});
|
|
30
|
+
it("sorts commands and rejects incomplete inventories", () => {
|
|
31
|
+
const about = { ...command, name: "about" }, result = renderBuddyCommandReference({ commands: [command, about], total: 2 });
|
|
32
|
+
expect(result.indexOf("### `about`")).toBeLessThan(result.indexOf("### `deploy`"));
|
|
33
|
+
expect(() => renderBuddyCommandReference({ commands: [command], total: 2 })).toThrow("does not match");
|
|
34
|
+
});
|
|
35
|
+
it("rejects duplicate canonical names", () => {
|
|
36
|
+
expect(() => renderBuddyCommandReference({ commands: [command, command], total: 2 })).toThrow("duplicate");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { generateOpenApi } from "../../../../api/src/generate-openapi";
|
|
4
|
+
import { renderOpenApiTypes } from "../../../../api/src/generate-types";
|
|
5
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), openApiPath = resolve(root, "storage/framework/api/openapi.json"), apiTypesPath = resolve(root, "storage/framework/api/api-types.ts");
|
|
6
|
+
export function validateOpenApi(document) {
|
|
7
|
+
const errors = [], paths = Object.entries(document.paths || {});
|
|
8
|
+
if (paths.length < 10)
|
|
9
|
+
errors.push("OpenAPI document has fewer than 10 registered paths");
|
|
10
|
+
const operationIds = new Set;
|
|
11
|
+
for (const [route, item] of paths)
|
|
12
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
13
|
+
if (!operation.operationId)
|
|
14
|
+
errors.push(`${method.toUpperCase()} ${route}: operationId is missing`);
|
|
15
|
+
else if (operationIds.has(operation.operationId))
|
|
16
|
+
errors.push(`${method.toUpperCase()} ${route}: duplicate operationId ${operation.operationId}`);
|
|
17
|
+
else
|
|
18
|
+
operationIds.add(operation.operationId);
|
|
19
|
+
for (const parameter of operation.parameters || [])
|
|
20
|
+
if (parameter.in === "path" && !parameter.required)
|
|
21
|
+
errors.push(`${method.toUpperCase()} ${route}: path parameter ${parameter.name} must be required`);
|
|
22
|
+
}
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
async function expectedArtifacts() {
|
|
26
|
+
const document = await generateOpenApi({ write: !1 }), errors = validateOpenApi(document);
|
|
27
|
+
if (errors.length)
|
|
28
|
+
throw Error(errors.join(`
|
|
29
|
+
`));
|
|
30
|
+
return {
|
|
31
|
+
openApi: JSON.stringify(document, null, 2),
|
|
32
|
+
apiTypes: renderOpenApiTypes(document)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function write() {
|
|
36
|
+
const expected = await expectedArtifacts();
|
|
37
|
+
writeFileSync(openApiPath, expected.openApi);
|
|
38
|
+
writeFileSync(apiTypesPath, expected.apiTypes);
|
|
39
|
+
console.log("Generated OpenAPI and API type artifacts");
|
|
40
|
+
}
|
|
41
|
+
async function check() {
|
|
42
|
+
const expected = await expectedArtifacts(), errors = [];
|
|
43
|
+
if (readFileSync(openApiPath, "utf8") !== expected.openApi)
|
|
44
|
+
errors.push("storage/framework/api/openapi.json is stale");
|
|
45
|
+
if (readFileSync(apiTypesPath, "utf8") !== expected.apiTypes)
|
|
46
|
+
errors.push("storage/framework/api/api-types.ts is stale");
|
|
47
|
+
if (errors.length)
|
|
48
|
+
throw Error(`${errors.join(`
|
|
49
|
+
`)}
|
|
50
|
+
Run bun run docs:artifacts and review the generated diff.`);
|
|
51
|
+
console.log(`Generated API artifacts are current (${Object.keys(JSON.parse(expected.openApi).paths).length} paths)`);
|
|
52
|
+
}
|
|
53
|
+
export async function run() {
|
|
54
|
+
try {
|
|
55
|
+
if (process.argv.includes("--write"))
|
|
56
|
+
await write();
|
|
57
|
+
else if (process.argv.includes("--check"))
|
|
58
|
+
await check();
|
|
59
|
+
else {
|
|
60
|
+
console.error("usage: bun storage/framework/core/buddy/src/commands/docs/generated-artifacts.ts --write | --check");
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (import.meta.main)
|
|
69
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { validateOpenApi } from "./generated-artifacts";
|
|
3
|
+
describe("generated API artifact validation", () => {
|
|
4
|
+
it("rejects empty, duplicate, and optional path evidence", () => {
|
|
5
|
+
expect(validateOpenApi({ paths: {} })).toContain("OpenAPI document has fewer than 10 registered paths");
|
|
6
|
+
const paths = {};
|
|
7
|
+
for (let index = 0;index < 10; index++)
|
|
8
|
+
paths[`/items/${index}/{id}`] = {
|
|
9
|
+
get: {
|
|
10
|
+
operationId: index < 2 ? "duplicate" : `get_${index}`,
|
|
11
|
+
parameters: [{ name: "id", in: "path", required: index !== 2, schema: { type: "string" } }],
|
|
12
|
+
responses: {}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const errors = validateOpenApi({ paths });
|
|
16
|
+
expect(errors).toContain("GET /items/1/{id}: duplicate operationId duplicate");
|
|
17
|
+
expect(errors).toContain("GET /items/2/{id}: path parameter id must be required");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** True for links this checker deliberately does not resolve on disk. */
|
|
2
|
+
export declare function isSkippableLink(target: string): boolean;
|
|
3
|
+
/** Extract inline-link targets with 1-based line numbers, skipping code. */
|
|
4
|
+
export declare function extractLinks(content: string): Array<{ target: string, line: number }>;
|
|
5
|
+
/**
|
|
6
|
+
* On-disk paths a link could legitimately resolve to. Absolute (`/x`) links are
|
|
7
|
+
* rooted at `docsRoot`; relative links at the file's directory. Extensionless
|
|
8
|
+
* links also try `.md` and `index.md` (VitePress clean URLs), and `.html` links
|
|
9
|
+
* try their `.md` source.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCandidates(target: string, fileDir: string, docsRoot: string): string[];
|
|
12
|
+
export declare function checkDocsLinks(docsRoot?: unknown): BrokenDocLink[];
|
|
13
|
+
export declare function run(): Promise<void>;
|
|
14
|
+
export declare interface BrokenDocLink {
|
|
15
|
+
file: string
|
|
16
|
+
line: number
|
|
17
|
+
target: string
|
|
18
|
+
}
|