Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/53149751fd834115905413080c30d8e7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"everything-dev": minor
---

Improve `bos sync` and `bos upgrade` with conflict detection, script cleanup, `--json` flag, and upgrade ordering fix

- **Conflict detection**: Snapshot-based 3-way hash comparison (local vs source vs snapshot) in sync. Files modified by both user and template are backed up to `.bos/sync-backup/` and overwritten with the template version. User-only changes are respected (left alone). Output includes a "For AI agents" block with version delta, conflict count, backup path, upstream PR link, and intent skills reference.
- **`--json` flag**: `bos sync --json` and `bos upgrade --json` output machine-parseable JSON for CI integration.
- **Upgrade ordering fix**: Install new packages BEFORE running migrations by re-executing `bos upgrade --migrations-only --json` from the newly installed package, ensuring migration code runs with the latest version.
- **Script exclusion**: Parent-only scripts (those not in `buildChildRootScripts`) are automatically cleaned from child project `package.json` during `personalizeConfig` and `migrateChildRootPackageJson`. Self-maintaining via `getParentOnlyScriptKeys()`.
- **Child root scripts cleaned up**: `node_modules/.bin/bos` replaced with `bos` shorthand. `test` script uses `--if-present` per workspace for flexible test execution when workspaces may or may not have tests.
- **Dead code removed**: Removed `--force` messaging, stale "Review changes" blocks from sync/upgrade output.
- **Root app support**: `bos upgrade` now skips sync and parent plugin discovery when there is no `extends` field, instead of silently swallowing a sync error. Framework updates and migrations still run normally in root apps.
- **No double banner**: Child processes (`bos types gen`, re-exec upgrade) now set `BOS_NO_BANNER=1` to suppress the banner, preventing duplicate output and fixing a latent JSON corruption bug in the re-exec path. The banner is gated on `process.env.BOS_NO_BANNER` being unset.
- **Safer cleanups**: Removed `.github/renovate.json` from obsolete files list (should be preserved). Changed "Removed" label to "Migrated" in CLI output since the list includes both rewritten and actually-deleted files.
- **Bug fixes**: Fixed `changelogUrl` always being `undefined` in non-dry-run upgrade results. Fixed `runTypesGen` being called twice. Fixed `skipped` array never being populated in sync results. Removed unused `sharedSync` variable and `existsSync` import.
10 changes: 10 additions & 0 deletions .changeset/improve-types-gen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"everything-dev": minor
---

Improve `bos types gen` with `--remote-plugins` flag, cleaner output, and local paths

- **`--remote-plugins` flag**: `bos types gen --remote-plugins auth,apps` forces specified plugins to fetch contract types remotely, matching `bos dev --remote-plugins` behavior. The flag is passed through from `runTypesGen` in init.ts, so `bos sync` and `bos upgrade` also benefit.
- **Cleaner output**: Replaced misleading "Mode: local|remote" (which only reflected the API source) with a "Contract sources:" section that shows each plugin's actual source. Output is now organized as "Written:" (generated files) and "Contract sources:" (per-plugin remote/local status).
- **Local paths shown**: Local contract sources now display their relative project path (e.g., `api local (api)`, `apps local (plugins/apps)`) instead of just "local".
- **Removed unused `source` field**: The `source` field was removed from `TypesGenResultSchema` and the handler since it was redundant — each contract source now reports its own status individually.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ node_modules/.federation
*.md
!**/skills/**/*.md
!AGENTS.md
!.changeset/*.md
!CONTRIBUTING.md
!**/README.md
!**/CHANGELOG.md
!.changeset/**/*.md
!.changeset/**/*.md.bos/sync-backup/
12 changes: 11 additions & 1 deletion host/src/services/tenant-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,22 @@ function buildEffectiveRuntimeConfig(
return effectiveConfig;
}

function matchesTenantPattern(accountId: string, pattern: string): boolean {
if (pattern === accountId) return true;
if (pattern.startsWith("*.") && accountId.endsWith(pattern.slice(1))) return true;
return false;
}

function isSsrAllowed(accountId: string): boolean {
if (parseBoolean(process.env.ALLOW_UNTRUSTED_SSR)) {
return true;
}

return getTenantWhitelist().has(accountId);
const whitelist = getTenantWhitelist();
for (const entry of whitelist) {
if (matchesTenantPattern(accountId, entry)) return true;
}
return false;
}

export async function resolveRequestRuntime(
Expand Down
8 changes: 8 additions & 0 deletions packages/everything-dev/src/api-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ export interface ContractBridgeStatus {
key: string;
source: "local" | "remote" | "skipped" | "failed";
url?: string;
localPath?: string;
error?: string;
}

Expand Down Expand Up @@ -514,6 +515,8 @@ export async function syncApiContractBridge(opts: {
key: "api",
source: opts.runtimeConfig.api.source,
url: opts.runtimeConfig.api.source !== "local" ? opts.apiBaseUrl : undefined,
localPath:
opts.runtimeConfig.api.source === "local" ? opts.runtimeConfig.api.localPath : undefined,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down Expand Up @@ -542,6 +545,10 @@ export async function syncApiContractBridge(opts: {
key: "auth",
source: opts.runtimeConfig.auth.source,
url: opts.runtimeConfig.auth.source !== "local" ? opts.runtimeConfig.auth.url : undefined,
localPath:
opts.runtimeConfig.auth.source === "local"
? opts.runtimeConfig.auth.localPath
: undefined,
});
if (authSource.generatedPath) {
generatedPath = authSource.generatedPath;
Expand Down Expand Up @@ -626,6 +633,7 @@ export async function syncApiContractBridge(opts: {
key,
source: plugin.source,
url: plugin.source !== "local" ? plugin.url : undefined,
localPath: plugin.source === "local" ? plugin.localPath : undefined,
});
if (result.value.source.generatedPath) {
generatedPath = result.value.source.generatedPath;
Expand Down
213 changes: 121 additions & 92 deletions packages/everything-dev/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ async function main() {
const displayVersion = edResolved?.installedVersion
? `${edResolved.installedVersion}${edResolved.isLinked ? " (linked)" : ""}`
: undefined;
printBanner("everything-dev", displayVersion);
if (!process.env.BOS_NO_BANNER) {
printBanner("everything-dev", displayVersion);
}

const runtime = createPluginRuntime({
registry: {
Expand Down Expand Up @@ -569,6 +571,10 @@ async function main() {
}

if (descriptor.key === "sync") {
if ((input as any).json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log();
if (result.status === "error") {
console.error(`[CLI] ${result.error || "Unknown error"}`);
Expand All @@ -577,48 +583,42 @@ async function main() {
if (result.status === "dry-run") {
console.log(colors.cyan(`${icons.ok} Dry run — no files written`));
} else {
console.log(colors.green(`${icons.ok} Template synced`));
}
if (result.updated.length > 0) {
console.log(` ${colors.dim("Updated:")} ${result.updated.length} file(s)`);
for (const f of result.updated) console.log(` ${colors.dim(f)}`);
console.log(colors.green(`${icons.ok} Synced`));
}
if (result.added.length > 0) {
console.log(` ${colors.dim("Added:")} ${result.added.length} file(s)`);
for (const f of result.added) console.log(` ${colors.dim(f)}`);
}
if (result.skipped.length > 0) {
if (result.updated.length > 0 || result.added.length > 0 || result.conflicted.length > 0) {
console.log(
` ${colors.yellow("Skipped:")} ${result.skipped.length} file(s) (locally modified, use --force to overwrite)`,
` ${colors.dim("Sync results:")} ${result.updated.length} updated, ${result.added.length} added, ${result.conflicted.length} conflicted`,
);
for (const f of result.skipped) console.log(` ${colors.dim(f)}`);
if (result.updated.length > 0) {
for (const f of result.updated) console.log(` ${colors.dim(f)}`);
}
if (result.added.length > 0) {
for (const f of result.added) console.log(` ${colors.dim(f)}`);
}
if (result.conflicted.length > 0) {
console.log(
` ${colors.yellow("Conflicted")} (template applied, your changes backed up):`,
);
if (result.backupDir) console.log(` ${colors.dim(result.backupDir)}`);
for (const f of result.conflicted) console.log(` ${colors.dim(f)}`);
}
}
if (result.updated.length === 0 && result.added.length === 0 && result.skipped.length === 0) {
if (
result.updated.length === 0 &&
result.added.length === 0 &&
result.conflicted.length === 0
) {
console.log(` ${colors.dim("Already up to date")}`);
}
if (result.status !== "dry-run" && result.updated.length > 0) {
console.log();
console.log(colors.dim(" Review changes — your customizations take priority:"));
console.log(
colors.dim(
" • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts — never overwritten",
),
);
console.log(
colors.dim(" • ui/src/components/**, ui/src/styles.css — never overwritten"),
);
console.log(
colors.dim(
" • Other updated files — accept framework improvements, then restore your changes",
),
);
console.log(colors.dim(" • Skipped files — yours already, only update with --force"));
}
console.log();
return;
}

if (descriptor.key === "upgrade") {
if ((input as any).json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log();
if (result.status === "error") {
console.error(`[CLI] ${result.error || "Unknown error"}`);
Expand All @@ -627,75 +627,85 @@ async function main() {
if (result.status === "dry-run") {
console.log(colors.cyan(`${icons.ok} Dry run — no changes applied`));
} else {
console.log(colors.green(`${icons.ok} Upgrade successful`));
console.log(colors.green(`${icons.ok} Upgrade complete`));
}
const mainPkg = result.packages.find(
(p: { name: string; from?: string; to: string }) => p.name === "everything-dev",
);
const versionDelta =
mainPkg?.from && mainPkg.from !== mainPkg.to ? `${mainPkg.from} → ${mainPkg.to}` : null;
if (versionDelta) {
console.log(` ${colors.dim(`Upgraded everything-dev ${versionDelta}`)}`);
}
for (const pkg of result.packages) {
if (pkg.name === "everything-dev") continue;
if (pkg.from && pkg.from !== pkg.to) {
console.log(` ${colors.dim(`${pkg.name}:`)} ${pkg.from} → ${pkg.to}`);
console.log(` ${colors.dim(`${pkg.name} ${pkg.from} → ${pkg.to}`)}`);
} else if (!pkg.from) {
console.log(` ${colors.dim(`${pkg.name}:`)} ${pkg.to} (new)`);
console.log(` ${colors.dim(`${pkg.name} ${pkg.to} (new)`)}`);
} else {
console.log(` ${colors.dim(`${pkg.name}:`)} ${pkg.to} (up to date)`);
console.log(` ${colors.dim(`${pkg.name} ${pkg.to} (up to date)`)}`);
}
}
if (result.changelogUrl) {
console.log(` ${colors.dim("Changelog:")} ${result.changelogUrl}`);
}
if (result.availablePlugins && result.availablePlugins.length > 0) {
console.log(` ${colors.dim("New parent plugins:")} ${result.availablePlugins.join(", ")}`);
console.log(` Changelog: ${result.changelogUrl}`);
}
if (result.selectedPlugins && result.selectedPlugins.length > 0) {
console.log(` ${colors.dim("Added plugins:")} ${result.selectedPlugins.join(", ")}`);
console.log(` Added plugins: ${result.selectedPlugins.join(", ")}`);
}
printTimingSummary(result.timings);
if (result.sync) {
const sync = result.sync;
if (sync.updated.length > 0) {
console.log(` ${colors.dim("Updated:")} ${sync.updated.length} file(s)`);
for (const f of sync.updated) console.log(` ${colors.dim(f)}`);
}
if (sync.added.length > 0) {
console.log(` ${colors.dim("Added:")} ${sync.added.length} file(s)`);
for (const f of sync.added) console.log(` ${colors.dim(f)}`);
}
if (sync.skipped.length > 0) {
console.log(` ${colors.dim("Sync results:")}`);
if (sync.updated.length > 0 || sync.added.length > 0 || sync.conflicted.length > 0) {
console.log(
` ${colors.yellow("Skipped:")} ${sync.skipped.length} file(s) (locally modified, use --force to overwrite)`,
` ${sync.updated.length} updated, ${sync.added.length} added, ${sync.conflicted.length} conflicted`,
);
for (const f of sync.skipped) console.log(` ${colors.dim(f)}`);
if (sync.conflicted.length > 0) {
console.log(
` ${colors.yellow("Conflicted")} (template applied, your changes backed up):`,
);
if (sync.backupDir) console.log(` ${colors.dim(sync.backupDir)}`);
for (const f of sync.conflicted) console.log(` ${colors.dim(f)}`);
}
} else {
console.log(` ${colors.dim("Already up to date")}`);
}
if (
result.status !== "dry-run" &&
(sync.updated.length > 0 || sync.added.length > 0 || sync.skipped.length > 0)
) {
console.log();
console.log(colors.dim(" Resolve differences — your code takes priority:"));
console.log();
console.log(colors.dim(" Never overwritten (safe):"));
}
if (result.migrated && result.migrated.length > 0) {
console.log(` Migrated: ${result.migrated.length} file(s)`);
for (const f of result.migrated) console.log(` ${colors.dim(f)}`);
}
if (versionDelta || (result.sync && result.sync.conflicted.length > 0)) {
console.log();
console.log(colors.dim(" For AI agents:"));
const parentRepo = "nearbuilders/everything-dev";
const somedelta =
mainPkg?.from && mainPkg.to
? `v${mainPkg.from.replace(/^[\^~>=v]+/, "")}...v${mainPkg.to.replace(/^[\^~>=v]+/, "")}`
: "";
console.log(
colors.dim(
` This upgrade applied template changes${somedelta ? ` (${somedelta})` : ""}.`,
),
);
if (result.sync && result.sync.conflicted.length > 0) {
console.log(
colors.dim(" • api/src/contract.ts, api/src/index.ts, api/src/db/schema.ts"),
colors.dim(
` ${result.sync.conflicted.length} file(s) had local conflicts — template version applied, backups at ${result.sync.backupDir || ".bos/sync-backup/"}.`,
),
);
console.log(colors.dim(" • ui/src/components/**, ui/src/styles.css"));
console.log();
console.log(colors.dim(" Replaced — review and keep your changes:"));
console.log(
colors.dim(
" • api/drizzle.config.ts, api/tsconfig.json, api/tsconfig.contract.json",
` If local changes should be preserved upstream, open a PR: https://github.com/${parentRepo}${somedelta ? `/compare/${somedelta}` : ""}`,
),
);
console.log(colors.dim(" • api/plugin.dev.ts, api/rspack.config.js"));
console.log(colors.dim(" • ui/src/routes/* (core routes only)"));
console.log();
console.log(colors.dim(" Merged — your deps preserved:"));
console.log(colors.dim(" • package.json, api/package.json, ui/package.json"));
console.log();
console.log(colors.dim(" Skipped — already yours:"));
console.log(colors.dim(" • Use --force only if you want framework updates"));
}
}
if (result.migrated && result.migrated.length > 0) {
console.log(` ${colors.yellow("Removed:")} ${result.migrated.length} obsolete file(s)`);
for (const f of result.migrated) console.log(` ${colors.dim(f)}`);
console.log(
colors.dim(
" Load matching intent skills: bunx @tanstack/intent@latest load everything-dev#...",
),
);
console.log(colors.dim(" Review AGENTS.md — local updates may need merging."));
}
console.log();
return;
Expand Down Expand Up @@ -859,26 +869,45 @@ async function main() {
process.exit(1);
}
console.log(colors.green(`${icons.ok} Types generated`));
if (result.source) {
console.log(
` ${colors.dim("Mode:")} ${result.source === "remote" ? colors.cyan("remote") : colors.dim("local")}`,
);
}
if (result.generated.length > 0) {
console.log(` ${colors.dim("Generated:")}`);
console.log(` ${colors.dim("Written:")}`);
for (const f of result.generated) console.log(` ${colors.dim(f)}`);
}
if (result.fetched.length > 0) {
console.log(` ${colors.dim("Fetched (remote):")}`);
for (const url of result.fetched) console.log(` ${colors.dim(url)}`);
}
if (result.skipped.length > 0) {
console.log(` ${colors.dim("Skipped:")}`);
for (const s of result.skipped) console.log(` ${colors.dim(s)}`);
if (result.fetched.length > 0 || result.skipped.length > 0 || result.failed.length > 0) {
console.log(` ${colors.dim("Contract sources:")}`);
for (const entry of result.fetched) {
const space = entry.indexOf(" ");
const key = space !== -1 ? entry.slice(0, space) : entry;
const rest = space !== -1 ? entry.slice(space + 1) : "";
const restSpace = rest.indexOf(" ");
const detail = restSpace !== -1 ? rest.slice(restSpace + 1) : "";
console.log(
` ${key} ${colors.cyan("remote")}${detail ? ` ${colors.dim(detail)}` : ""}`,
);
}
for (const entry of result.skipped) {
const space = entry.indexOf(" ");
const key = space !== -1 ? entry.slice(0, space) : entry;
const rest = space !== -1 ? entry.slice(space + 1) : "";
if (rest === "no URL resolved") {
console.log(` ${key} ${colors.dim("no URL resolved")}`);
continue;
}
const restSpace = rest.indexOf(" ");
const detail = restSpace !== -1 ? rest.slice(restSpace + 1) : "";
console.log(` ${key} ${colors.dim("local")}${detail ? ` ${colors.dim(detail)}` : ""}`);
}
}
if (result.failed.length > 0) {
console.log(` ${colors.yellow("Failed:")}`);
for (const f of result.failed) console.log(` ${colors.error(f)}`);
for (const f of result.failed) {
const colon = f.indexOf(": ");
if (colon !== -1) {
console.log(` ${colors.error(f.slice(0, colon))}${colors.dim(f.slice(colon))}`);
} else {
console.log(` ${colors.error(f)}`);
}
}
}
console.log();
return;
Expand Down
Loading
Loading