refactor: rename cli-v2 → cli, archive legacy cli, plus broker-side grants + auto-migrate
- apps/cli/ is now the canonical CLI (was apps/cli-v2/). - apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag 'cli-v0-legacy-final' before deletion; git history preserves it too. - .github/workflows/release-cli.yml paths updated. - pnpm-lock.yaml regenerated. Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities): - 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member. - handleSend in broker fetches recipient grant maps once per send, drops messages silently when sender lacks the required capability. - POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric. - CLI grant/revoke/block now mirror to broker via syncToBroker. Auto-migrate on broker startup: - apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock before the HTTP server binds. Exits non-zero on failure so Coolify healthcheck fails closed. - Dockerfile copies packages/db/migrations into /app/migrations. - postgres 3.4.5 added as direct broker dep. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
apps/cli/src/utils/format.ts
Normal file
11
apps/cli/src/utils/format.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return ms + "ms";
|
||||
if (ms < 60_000) return (ms / 1000).toFixed(1) + "s";
|
||||
return Math.floor(ms / 60_000) + "m " + Math.floor((ms % 60_000) / 1000) + "s";
|
||||
}
|
||||
6
apps/cli/src/utils/index.ts
Normal file
6
apps/cli/src/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { levenshtein } from "./levenshtein.js";
|
||||
export { toSlug } from "./slug.js";
|
||||
export { isInviteUrl, extractInviteCode } from "./url.js";
|
||||
export { formatBytes, formatDuration } from "./format.js";
|
||||
export { isNewer } from "./semver.js";
|
||||
export { retry } from "./retry.js";
|
||||
18
apps/cli/src/utils/levenshtein.ts
Normal file
18
apps/cli/src/utils/levenshtein.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function levenshtein(a: string, b: string): number {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
||||
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
||||
);
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
dp[i]![j] = Math.min(
|
||||
dp[i - 1]![j]! + 1,
|
||||
dp[i]![j - 1]! + 1,
|
||||
dp[i - 1]![j - 1]! + cost,
|
||||
);
|
||||
}
|
||||
}
|
||||
return dp[m]![n]!;
|
||||
}
|
||||
16
apps/cli/src/utils/retry.ts
Normal file
16
apps/cli/src/utils/retry.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
opts: { attempts?: number; delayMs?: number } = {},
|
||||
): Promise<T> {
|
||||
const { attempts = 3, delayMs = 1000 } = opts;
|
||||
let lastErr: unknown;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
if (i < attempts - 1) await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
7
apps/cli/src/utils/semver.ts
Normal file
7
apps/cli/src/utils/semver.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function isNewer(current: string, latest: string): boolean {
|
||||
const cp = current.split(".").map(Number);
|
||||
const lp = latest.split(".").map(Number);
|
||||
const a = cp[0] ?? 0, b = cp[1] ?? 0, c = cp[2] ?? 0;
|
||||
const x = lp[0] ?? 0, y = lp[1] ?? 0, z = lp[2] ?? 0;
|
||||
return x > a || (x === a && y > b) || (x === a && y === b && z > c);
|
||||
}
|
||||
3
apps/cli/src/utils/slug.ts
Normal file
3
apps/cli/src/utils/slug.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function toSlug(input: string): string {
|
||||
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
}
|
||||
30
apps/cli/src/utils/url.ts
Normal file
30
apps/cli/src/utils/url.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export function isInviteUrl(input: string): boolean {
|
||||
return (
|
||||
/^https?:\/\/[^/]+\/(?:[a-z]{2}\/)?i\//.test(input) ||
|
||||
/^https?:\/\/[^/]+\/(?:[a-z]{2}\/)?join\//.test(input) ||
|
||||
/^ic:\/\//.test(input) ||
|
||||
/^claudemesh:\/\//.test(input)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise any accepted invite input to the canonical HTTPS short URL
|
||||
* (`https://claudemesh.com/i/<code>`) or long URL, so downstream parsers
|
||||
* only have to handle one scheme.
|
||||
*/
|
||||
export function normaliseInviteUrl(input: string, host = "claudemesh.com"): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed.startsWith("claudemesh://")) {
|
||||
const rest = trimmed.slice("claudemesh://".length).replace(/^\/+/, "");
|
||||
const m = rest.match(/^(?:i|join)\/(.+)$/);
|
||||
const tail = m ? m[1]! : rest;
|
||||
const kind = rest.startsWith("join/") ? "join" : "i";
|
||||
return `https://${host}/${kind}/${tail}`;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function extractInviteCode(url: string): string | null {
|
||||
const match = url.match(/\/i\/([A-Za-z0-9]+)/) || url.match(/^ic:\/\/([A-Za-z0-9]+)/);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user