chore(cli-v2): un-ignore CLI source tree for binary release workflow
The CLI source (242 files, ~14k lines) was gitignored during the earlier cli→cli-v2 reorg so only the published npm package carried it. That blocks the GitHub Actions release workflow (release-cli.yml), which clones the repo fresh on each runner and needs the source to compile binaries via `bun build --compile`. Moves the gitignore from root-level to `apps/cli-v2/.gitignore` with only the usual build artefacts excluded (node_modules, dist, .turbo, .cache). Source is now in git at apps/cli-v2/src/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
apps/cli-v2/src/commands/invite.ts
Normal file
96
apps/cli-v2/src/commands/invite.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import { getStoredToken } from "~/services/auth/facade.js";
|
||||
import { generateInvite } from "~/services/invite/generate.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { writeClipboard } from "~/services/clipboard/facade.js";
|
||||
import { green, bold, dim, icons } from "~/ui/styles.js";
|
||||
import { renderQrAsync } from "~/ui/qr.js";
|
||||
import { EXIT } from "~/constants/exit-codes.js";
|
||||
|
||||
function prompt(question: string): Promise<string> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
|
||||
});
|
||||
}
|
||||
|
||||
export async function invite(
|
||||
email?: string,
|
||||
opts: { mesh?: string; expires?: string; uses?: number; role?: string; json?: boolean } = {},
|
||||
): Promise<number> {
|
||||
const auth = getStoredToken();
|
||||
if (!auth) {
|
||||
console.error(" Not signed in. Run `claudemesh login` first.");
|
||||
return EXIT.AUTH_FAILED;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
if (config.meshes.length === 0) {
|
||||
console.error(" No meshes. Create one with `claudemesh mesh create <name>`.");
|
||||
return EXIT.NOT_FOUND;
|
||||
}
|
||||
|
||||
// Resolve which mesh to share
|
||||
let meshSlug = opts.mesh;
|
||||
if (!meshSlug) {
|
||||
if (config.meshes.length === 1) {
|
||||
meshSlug = config.meshes[0]!.slug;
|
||||
} else {
|
||||
// Show picker
|
||||
console.log("\n Select mesh to share:\n");
|
||||
config.meshes.forEach((m, i) => {
|
||||
console.log(` ${bold(String(i + 1) + ")")} ${m.slug} ${dim("(" + m.name + ")")}`);
|
||||
});
|
||||
console.log("");
|
||||
const choice = await prompt(" Choice [1]: ") || "1";
|
||||
const idx = parseInt(choice, 10) - 1;
|
||||
meshSlug = config.meshes[idx >= 0 && idx < config.meshes.length ? idx : 0]!.slug;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await generateInvite(meshSlug, {
|
||||
email,
|
||||
expires_in: opts.expires ?? "7d",
|
||||
max_uses: opts.uses,
|
||||
role: opts.role,
|
||||
});
|
||||
|
||||
const copied = writeClipboard(result.url);
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify({ schema_version: "1.0", ...result, copied }, null, 2));
|
||||
} else {
|
||||
if (email) {
|
||||
if (result.emailed) {
|
||||
console.log(`\n ${green(icons.check)} Invite sent to ${bold(email)}`);
|
||||
if (copied) console.log(` ${green(icons.check)} Link also copied to clipboard`);
|
||||
} else {
|
||||
console.log(`\n ${icons.cross} Email to ${bold(email)} was NOT sent (server did not send).`);
|
||||
console.log(` ${dim("Share the link manually:")}`);
|
||||
console.log(` ${result.url}`);
|
||||
if (copied) console.log(` ${green(icons.check)} Link copied to clipboard`);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n ${green(icons.check)} Invite link${copied ? " copied to clipboard" : ""}:`);
|
||||
console.log(` ${result.url}`);
|
||||
// Print QR for phone→laptop pairing. Small variant is ~17 lines tall.
|
||||
const qr = await renderQrAsync(result.url, { small: true });
|
||||
console.log("");
|
||||
for (const line of qr.split("\n")) console.log(` ${line}`);
|
||||
}
|
||||
console.log(`\n ${dim("Expires " + result.expires_at + ". Anyone with this link can join \"" + meshSlug + "\".")}\n`);
|
||||
}
|
||||
|
||||
return EXIT.SUCCESS;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("403") || msg.includes("permission")) {
|
||||
console.error(` ${icons.cross} You don't have permission to invite to "${meshSlug}".`);
|
||||
console.error(` ${dim("Ask the mesh owner to grant you invite permissions.")}`);
|
||||
} else {
|
||||
console.error(` ${icons.cross} Failed: ${msg}`);
|
||||
}
|
||||
return EXIT.INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user