create-egregore 0.1.0

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/bin/cli.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-egregore — Set up Egregore in one command.
5
+ *
6
+ * Two modes:
7
+ * npx create-egregore --token st_xxxx (from website — primary path)
8
+ * npx create-egregore (terminal-only fallback)
9
+ */
10
+
11
+ const ui = require("../lib/ui");
12
+ const { EgregoreAPI } = require("../lib/api");
13
+ const { deviceFlow } = require("../lib/auth");
14
+ const { install } = require("../lib/setup");
15
+
16
+ const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
17
+
18
+ function parseArgs(argv) {
19
+ const args = {};
20
+ for (let i = 2; i < argv.length; i++) {
21
+ if (argv[i] === "--token" && argv[i + 1]) {
22
+ args.token = argv[++i];
23
+ } else if (argv[i].startsWith("--token=")) {
24
+ args.token = argv[i].split("=")[1];
25
+ } else if (argv[i] === "--api" && argv[i + 1]) {
26
+ args.api = argv[++i];
27
+ } else if (argv[i] === "--help" || argv[i] === "-h") {
28
+ args.help = true;
29
+ }
30
+ }
31
+ return args;
32
+ }
33
+
34
+ function showHelp() {
35
+ ui.banner();
36
+ ui.info("Usage:");
37
+ ui.info(" npx create-egregore --token <setup-token> Install from website");
38
+ ui.info(" npx create-egregore Interactive setup");
39
+ ui.info("");
40
+ ui.info("Options:");
41
+ ui.info(" --token <token> Setup token from egregore.dev");
42
+ ui.info(" --api <url> API URL override");
43
+ ui.info(" -h, --help Show this help");
44
+ ui.info("");
45
+ }
46
+
47
+ async function main() {
48
+ const args = parseArgs(process.argv);
49
+
50
+ if (args.help) {
51
+ showHelp();
52
+ process.exit(0);
53
+ }
54
+
55
+ const api = new EgregoreAPI(args.api || API_URL);
56
+
57
+ ui.banner();
58
+
59
+ if (args.token) {
60
+ // ===== Website flow: token provided =====
61
+ await tokenFlow(api, args.token);
62
+ } else {
63
+ // ===== Terminal fallback: interactive =====
64
+ await interactiveFlow(api);
65
+ }
66
+ }
67
+
68
+ async function tokenFlow(api, token) {
69
+ const s = ui.spinner("Claiming setup token...");
70
+ try {
71
+ const data = await api.claimToken(token);
72
+ s.stop(`Claimed — setting up ${ui.bold(data.org_name)}`);
73
+ await install(data, ui);
74
+ } catch (err) {
75
+ s.fail("Token claim failed");
76
+ ui.error(err.message);
77
+ ui.info("");
78
+ ui.info("The token may have expired or already been used.");
79
+ ui.info("Visit egregore.dev to get a new one, or run without --token:");
80
+ ui.info(" npx create-egregore");
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ async function interactiveFlow(api) {
86
+ // 1. GitHub auth
87
+ ui.info("Let's set up Egregore. First, sign in with GitHub.\n");
88
+ let githubToken;
89
+ try {
90
+ githubToken = await deviceFlow(ui);
91
+ ui.success("Authenticated with GitHub");
92
+ } catch (err) {
93
+ ui.error(`GitHub auth failed: ${err.message}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ // 2. Detect orgs
98
+ console.log("");
99
+ const s = ui.spinner("Checking your organizations...");
100
+ let orgsData;
101
+ try {
102
+ orgsData = await api.getOrgs(githubToken);
103
+ s.stop("Found your organizations");
104
+ } catch (err) {
105
+ s.fail("Failed to check organizations");
106
+ ui.error(err.message);
107
+ process.exit(1);
108
+ }
109
+
110
+ // 3. Build choices
111
+ const choices = [];
112
+
113
+ // Orgs with egregore → join
114
+ for (const org of orgsData.orgs) {
115
+ if (org.has_egregore) {
116
+ choices.push({
117
+ label: org.name || org.login,
118
+ description: "Join existing",
119
+ action: "join",
120
+ login: org.login,
121
+ });
122
+ }
123
+ }
124
+
125
+ // Orgs without egregore → set up
126
+ for (const org of orgsData.orgs) {
127
+ if (!org.has_egregore) {
128
+ choices.push({
129
+ label: org.name || org.login,
130
+ description: "Set up new",
131
+ action: "setup",
132
+ login: org.login,
133
+ });
134
+ }
135
+ }
136
+
137
+ // Personal account
138
+ if (orgsData.personal.has_egregore) {
139
+ choices.push({
140
+ label: `${orgsData.user.login} (personal)`,
141
+ description: "Join existing",
142
+ action: "join",
143
+ login: orgsData.user.login,
144
+ });
145
+ } else {
146
+ choices.push({
147
+ label: `${orgsData.user.login} (personal)`,
148
+ description: "Set up personal",
149
+ action: "setup",
150
+ login: orgsData.user.login,
151
+ is_personal: true,
152
+ });
153
+ }
154
+
155
+ if (choices.length === 0) {
156
+ ui.error("No organizations found. Check your GitHub permissions.");
157
+ process.exit(1);
158
+ }
159
+
160
+ // 4. User picks
161
+ const choice = await ui.choose("Where do you want Egregore?", choices);
162
+
163
+ // 5. Execute
164
+ console.log("");
165
+ if (choice.action === "join") {
166
+ await joinFlow(api, githubToken, choice);
167
+ } else {
168
+ await setupFlow(api, githubToken, choice);
169
+ }
170
+ }
171
+
172
+ async function setupFlow(api, githubToken, choice) {
173
+ const orgName = await ui.prompt(`Organization display name [${choice.login}]:`);
174
+ const name = orgName || choice.login;
175
+
176
+ const s = ui.spinner(`Setting up Egregore for ${ui.bold(name)}...`);
177
+ try {
178
+ const result = await api.setupOrg(githubToken, {
179
+ github_org: choice.login,
180
+ org_name: name,
181
+ is_personal: choice.is_personal || false,
182
+ });
183
+ s.stop("Setup complete on GitHub");
184
+
185
+ // Claim the token immediately
186
+ const data = await api.claimToken(result.setup_token);
187
+ await install(data, ui);
188
+
189
+ if (result.telegram_invite_link) {
190
+ console.log("");
191
+ ui.info(`Connect Telegram (optional): ${result.telegram_invite_link}`);
192
+ }
193
+ } catch (err) {
194
+ s.fail("Setup failed");
195
+ ui.error(err.message);
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ async function joinFlow(api, githubToken, choice) {
201
+ const s = ui.spinner(`Joining ${ui.bold(choice.login)}...`);
202
+ try {
203
+ const result = await api.joinOrg(githubToken, {
204
+ github_org: choice.login,
205
+ });
206
+ s.stop("Joined");
207
+
208
+ const data = await api.claimToken(result.setup_token);
209
+ await install(data, ui);
210
+ } catch (err) {
211
+ s.fail("Join failed");
212
+ ui.error(err.message);
213
+ process.exit(1);
214
+ }
215
+ }
216
+
217
+ main().catch((err) => {
218
+ ui.error(err.message);
219
+ process.exit(1);
220
+ });
package/lib/api.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Egregore API client.
3
+ * Zero dependencies — uses node:https built-in.
4
+ */
5
+
6
+ const https = require("node:https");
7
+ const { URL } = require("node:url");
8
+
9
+ const DEFAULT_API = "https://egregore-production-55f2.up.railway.app";
10
+
11
+ function request(method, url, { body, headers = {}, timeout = 30000 } = {}) {
12
+ return new Promise((resolve, reject) => {
13
+ const parsed = new URL(url);
14
+ const opts = {
15
+ hostname: parsed.hostname,
16
+ port: parsed.port || 443,
17
+ path: parsed.pathname + parsed.search,
18
+ method,
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Accept: "application/json",
22
+ ...headers,
23
+ },
24
+ timeout,
25
+ };
26
+
27
+ const req = https.request(opts, (res) => {
28
+ let data = "";
29
+ res.on("data", (chunk) => (data += chunk));
30
+ res.on("end", () => {
31
+ try {
32
+ const json = JSON.parse(data);
33
+ if (res.statusCode >= 400) {
34
+ reject(new Error(json.detail || `HTTP ${res.statusCode}`));
35
+ } else {
36
+ resolve(json);
37
+ }
38
+ } catch {
39
+ if (res.statusCode >= 400) {
40
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
41
+ } else {
42
+ resolve(data);
43
+ }
44
+ }
45
+ });
46
+ });
47
+
48
+ req.on("error", reject);
49
+ req.on("timeout", () => {
50
+ req.destroy();
51
+ reject(new Error("Request timed out"));
52
+ });
53
+
54
+ if (body) {
55
+ req.write(JSON.stringify(body));
56
+ }
57
+ req.end();
58
+ });
59
+ }
60
+
61
+ class EgregoreAPI {
62
+ constructor(baseUrl) {
63
+ this.base = (baseUrl || DEFAULT_API).replace(/\/$/, "");
64
+ }
65
+
66
+ async claimToken(token) {
67
+ return request("GET", `${this.base}/api/org/claim/${token}`);
68
+ }
69
+
70
+ async getOrgs(githubToken) {
71
+ return request("GET", `${this.base}/api/org/setup/orgs`, {
72
+ headers: { Authorization: `Bearer ${githubToken}` },
73
+ });
74
+ }
75
+
76
+ async setupOrg(githubToken, { github_org, org_name, is_personal = false }) {
77
+ return request("POST", `${this.base}/api/org/setup`, {
78
+ headers: { Authorization: `Bearer ${githubToken}` },
79
+ body: { github_org, org_name, is_personal },
80
+ });
81
+ }
82
+
83
+ async joinOrg(githubToken, { github_org }) {
84
+ return request("POST", `${this.base}/api/org/join`, {
85
+ headers: { Authorization: `Bearer ${githubToken}` },
86
+ body: { github_org },
87
+ });
88
+ }
89
+
90
+ async exchangeCode(code) {
91
+ return request("POST", `${this.base}/api/auth/github/callback`, {
92
+ body: { code },
93
+ });
94
+ }
95
+
96
+ async getClientId() {
97
+ return request("GET", `${this.base}/api/auth/github/client-id`);
98
+ }
99
+ }
100
+
101
+ module.exports = { EgregoreAPI, request };
package/lib/auth.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * GitHub Device Flow authentication.
3
+ * Zero dependencies — uses node:https built-in.
4
+ */
5
+
6
+ const https = require("node:https");
7
+
8
+ const CLIENT_ID = "Ov23lizB4nYEeIRsHTdb";
9
+ const SCOPE = "repo,read:org";
10
+
11
+ function post(url, body) {
12
+ return new Promise((resolve, reject) => {
13
+ const parsed = new URL(url);
14
+ const data = new URLSearchParams(body).toString();
15
+ const req = https.request(
16
+ {
17
+ hostname: parsed.hostname,
18
+ path: parsed.pathname,
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/x-www-form-urlencoded",
22
+ Accept: "application/json",
23
+ "Content-Length": Buffer.byteLength(data),
24
+ },
25
+ },
26
+ (res) => {
27
+ let buf = "";
28
+ res.on("data", (c) => (buf += c));
29
+ res.on("end", () => {
30
+ try {
31
+ resolve(JSON.parse(buf));
32
+ } catch {
33
+ reject(new Error(buf));
34
+ }
35
+ });
36
+ }
37
+ );
38
+ req.on("error", reject);
39
+ req.write(data);
40
+ req.end();
41
+ });
42
+ }
43
+
44
+ function sleep(ms) {
45
+ return new Promise((r) => setTimeout(r, ms));
46
+ }
47
+
48
+ function openBrowser(url) {
49
+ const { exec } = require("node:child_process");
50
+ const cmd =
51
+ process.platform === "darwin"
52
+ ? `open "${url}"`
53
+ : process.platform === "win32"
54
+ ? `start "${url}"`
55
+ : `xdg-open "${url}"`;
56
+ exec(cmd, () => {});
57
+ }
58
+
59
+ function copyToClipboard(text) {
60
+ const { exec } = require("node:child_process");
61
+ if (process.platform === "darwin") {
62
+ exec(`printf '%s' '${text}' | pbcopy`, () => {});
63
+ return true;
64
+ }
65
+ try {
66
+ exec(`printf '%s' '${text}' | xclip -selection clipboard`, () => {});
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Run GitHub Device Flow.
75
+ * Returns the access token string, or throws on failure.
76
+ */
77
+ async function deviceFlow(ui) {
78
+ // 1. Request device code
79
+ const codeResp = await post("https://github.com/login/device/code", {
80
+ client_id: CLIENT_ID,
81
+ scope: SCOPE,
82
+ });
83
+
84
+ if (!codeResp.device_code) {
85
+ throw new Error("Failed to start GitHub device flow");
86
+ }
87
+
88
+ const { device_code, user_code, verification_uri, interval: pollInterval } = codeResp;
89
+ const verifyUrl = codeResp.verification_uri_complete || verification_uri;
90
+
91
+ // 2. Show code + open browser
92
+ const copied = copyToClipboard(user_code);
93
+ ui.info("");
94
+ if (copied) {
95
+ ui.info(`Code copied to clipboard: ${ui.bold(user_code)}`);
96
+ } else {
97
+ ui.info(`Your code: ${ui.bold(user_code)}`);
98
+ }
99
+ ui.info("Opening browser — paste the code and authorize.");
100
+ ui.info("");
101
+ openBrowser(verifyUrl);
102
+
103
+ // 3. Poll for token
104
+ let interval = (pollInterval || 5) * 1000;
105
+ const timeout = 300000; // 5 minutes
106
+ const start = Date.now();
107
+
108
+ while (Date.now() - start < timeout) {
109
+ await sleep(interval);
110
+
111
+ const tokenResp = await post("https://github.com/login/oauth/access_token", {
112
+ client_id: CLIENT_ID,
113
+ device_code,
114
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
115
+ });
116
+
117
+ if (tokenResp.access_token) {
118
+ return tokenResp.access_token;
119
+ }
120
+
121
+ if (tokenResp.error === "authorization_pending") {
122
+ continue;
123
+ }
124
+ if (tokenResp.error === "slow_down") {
125
+ interval += 5000;
126
+ continue;
127
+ }
128
+
129
+ throw new Error(tokenResp.error_description || tokenResp.error || "Auth failed");
130
+ }
131
+
132
+ throw new Error("Timed out waiting for authorization (5 minutes)");
133
+ }
134
+
135
+ module.exports = { deviceFlow };
package/lib/setup.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Local setup — clone, symlink, alias, .env.
3
+ * Zero dependencies — uses node:child_process, node:fs, node:path, node:os.
4
+ */
5
+
6
+ const { execSync } = require("node:child_process");
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const os = require("node:os");
10
+
11
+ function run(cmd, opts = {}) {
12
+ return execSync(cmd, { stdio: "pipe", encoding: "utf-8", timeout: 60000, ...opts }).trim();
13
+ }
14
+
15
+ /**
16
+ * Full local setup from claimed token data.
17
+ *
18
+ * @param {object} data - from /api/org/claim/{token}
19
+ * @param {object} ui - UI module for output
20
+ * @param {string} targetDir - where to install (default: cwd)
21
+ */
22
+ async function install(data, ui, targetDir) {
23
+ const { fork_url, memory_url, github_token, org_name, github_org, slug } = data;
24
+ const base = targetDir || process.cwd();
25
+
26
+ const egregoreDir = path.join(base, "egregore");
27
+ const memoryDirName = memory_url
28
+ .split("/")
29
+ .pop()
30
+ .replace(/\.git$/, "");
31
+ const memoryDir = path.join(base, memoryDirName);
32
+
33
+ const totalSteps = 5;
34
+
35
+ // Configure git credential helper for HTTPS cloning
36
+ configureGitCredentials(github_token);
37
+
38
+ // 1. Clone fork
39
+ ui.step(1, totalSteps, "Cloning egregore...");
40
+ if (fs.existsSync(egregoreDir)) {
41
+ ui.warn("egregore/ already exists — pulling latest");
42
+ run("git pull", { cwd: egregoreDir });
43
+ } else {
44
+ run(`git clone "${fork_url}" "${egregoreDir}"`);
45
+ }
46
+ ui.success("Cloned egregore");
47
+
48
+ // 2. Clone memory
49
+ ui.step(2, totalSteps, "Cloning shared memory...");
50
+ if (fs.existsSync(memoryDir)) {
51
+ ui.warn(`${memoryDirName}/ already exists — pulling latest`);
52
+ run("git pull", { cwd: memoryDir });
53
+ } else {
54
+ run(`git clone "${memory_url}" "${memoryDir}"`);
55
+ }
56
+ ui.success("Cloned memory");
57
+
58
+ // 3. Symlink
59
+ ui.step(3, totalSteps, "Linking memory...");
60
+ const symlinkTarget = path.join(egregoreDir, "memory");
61
+ if (fs.existsSync(symlinkTarget)) {
62
+ ui.warn("memory/ symlink already exists");
63
+ } else {
64
+ const relPath = path.relative(egregoreDir, memoryDir);
65
+ fs.symlinkSync(relPath, symlinkTarget);
66
+ }
67
+ ui.success("Linked");
68
+
69
+ // 4. Write .env
70
+ ui.step(4, totalSteps, "Writing credentials...");
71
+ const envPath = path.join(egregoreDir, ".env");
72
+ const envContent = `GITHUB_TOKEN=${github_token}\n`;
73
+ fs.writeFileSync(envPath, envContent, { mode: 0o600 });
74
+ ui.success("Credentials saved");
75
+
76
+ // 5. Shell alias
77
+ ui.step(5, totalSteps, "Setting up launch command...");
78
+ setupAlias(egregoreDir, ui);
79
+
80
+ // Done
81
+ console.log("");
82
+ ui.success(`Egregore is ready for ${ui.bold(org_name)}`);
83
+ console.log("");
84
+ ui.info(`Your workspace:`);
85
+ ui.info(` ${ui.cyan("./egregore/")} — Your Egregore instance`);
86
+ ui.info(` ${ui.cyan(`./${memoryDirName}/`)} — Shared knowledge`);
87
+ console.log("");
88
+ ui.info(`Next: type ${ui.bold("egregore")} in any terminal to start.`);
89
+ console.log("");
90
+ }
91
+
92
+ function configureGitCredentials(token) {
93
+ try {
94
+ run("git config --global credential.helper store");
95
+ const credentialInput = `protocol=https\nhost=github.com\nusername=x-access-token\npassword=${token}\n`;
96
+ execSync("git credential-store store", {
97
+ input: credentialInput,
98
+ stdio: ["pipe", "pipe", "pipe"],
99
+ encoding: "utf-8",
100
+ });
101
+ } catch {
102
+ // Non-fatal — user may have their own credential setup
103
+ }
104
+ }
105
+
106
+ function setupAlias(egregoreDir, ui) {
107
+ const profiles = [
108
+ path.join(os.homedir(), ".zshrc"),
109
+ path.join(os.homedir(), ".bashrc"),
110
+ path.join(os.homedir(), ".bash_profile"),
111
+ ];
112
+
113
+ const profile = profiles.find((p) => fs.existsSync(p));
114
+ if (!profile) {
115
+ ui.warn("No shell profile found — add this to your shell config:");
116
+ ui.info(` alias egregore='cd "${egregoreDir}" && claude start'`);
117
+ return;
118
+ }
119
+
120
+ const existing = fs.readFileSync(profile, "utf-8");
121
+ // Remove old alias
122
+ const cleaned = existing
123
+ .split("\n")
124
+ .filter((line) => !line.includes("alias egregore=") && !line.match(/^# Egregore$/))
125
+ .join("\n");
126
+
127
+ const aliasBlock = `\n# Egregore\nalias egregore='cd "${egregoreDir}" && claude start'\n`;
128
+ fs.writeFileSync(profile, cleaned + aliasBlock);
129
+
130
+ ui.success(`Added ${ui.dim("egregore")} alias to ${path.basename(profile)}`);
131
+ }
132
+
133
+ module.exports = { install };
package/lib/ui.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Terminal UI — colors, spinner, prompts, banner.
3
+ * Zero dependencies. Node 18+ built-ins only.
4
+ */
5
+
6
+ const readline = require("node:readline");
7
+
8
+ const RESET = "\x1b[0m";
9
+ const BOLD = "\x1b[1m";
10
+ const DIM = "\x1b[2m";
11
+ const GREEN = "\x1b[32m";
12
+ const CYAN = "\x1b[36m";
13
+ const YELLOW = "\x1b[33m";
14
+ const RED = "\x1b[31m";
15
+ const WHITE = "\x1b[37m";
16
+
17
+ const BANNER = `
18
+ ${BOLD}${CYAN} ███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
19
+ ██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝
20
+ █████╗ ██║ ███╗██████╔╝█████╗ ██║ ███╗██║ ██║██████╔╝█████╗
21
+ ██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██║ ██║██╔══██╗██╔══╝
22
+ ███████╗╚██████╔╝██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ██║███████╗
23
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝${RESET}
24
+ `;
25
+
26
+ function banner() {
27
+ console.log(BANNER);
28
+ }
29
+
30
+ function info(msg) {
31
+ console.log(` ${msg}`);
32
+ }
33
+
34
+ function success(msg) {
35
+ console.log(` ${GREEN}✓${RESET} ${msg}`);
36
+ }
37
+
38
+ function warn(msg) {
39
+ console.log(` ${YELLOW}!${RESET} ${msg}`);
40
+ }
41
+
42
+ function error(msg) {
43
+ console.error(` ${RED}✗${RESET} ${msg}`);
44
+ }
45
+
46
+ function step(n, total, msg) {
47
+ console.log(`\n ${DIM}[${n}/${total}]${RESET} ${msg}`);
48
+ }
49
+
50
+ function dim(msg) {
51
+ return `${DIM}${msg}${RESET}`;
52
+ }
53
+
54
+ function bold(msg) {
55
+ return `${BOLD}${msg}${RESET}`;
56
+ }
57
+
58
+ function cyan(msg) {
59
+ return `${CYAN}${msg}${RESET}`;
60
+ }
61
+
62
+ // Simple spinner
63
+ function spinner(msg) {
64
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
65
+ let i = 0;
66
+ const interval = setInterval(() => {
67
+ process.stdout.write(`\r ${CYAN}${frames[i++ % frames.length]}${RESET} ${msg}`);
68
+ }, 80);
69
+ return {
70
+ stop(result) {
71
+ clearInterval(interval);
72
+ process.stdout.write(`\r ${GREEN}✓${RESET} ${result || msg}\n`);
73
+ },
74
+ fail(result) {
75
+ clearInterval(interval);
76
+ process.stdout.write(`\r ${RED}✗${RESET} ${result || msg}\n`);
77
+ },
78
+ };
79
+ }
80
+
81
+ // Prompt for input
82
+ function prompt(question) {
83
+ return new Promise((resolve) => {
84
+ const rl = readline.createInterface({
85
+ input: process.stdin,
86
+ output: process.stdout,
87
+ });
88
+ rl.question(` ${question} `, (answer) => {
89
+ rl.close();
90
+ resolve(answer.trim());
91
+ });
92
+ });
93
+ }
94
+
95
+ // Prompt with numbered choices
96
+ async function choose(question, options) {
97
+ console.log(`\n ${question}\n`);
98
+ for (let i = 0; i < options.length; i++) {
99
+ const { label, description } = options[i];
100
+ console.log(` ${BOLD}${i + 1}.${RESET} ${label}${description ? ` ${DIM}— ${description}${RESET}` : ""}`);
101
+ }
102
+ console.log();
103
+
104
+ while (true) {
105
+ const answer = await prompt(`Pick [1-${options.length}]:`);
106
+ const n = parseInt(answer, 10);
107
+ if (n >= 1 && n <= options.length) {
108
+ return options[n - 1];
109
+ }
110
+ warn(`Enter a number between 1 and ${options.length}`);
111
+ }
112
+ }
113
+
114
+ module.exports = {
115
+ banner, info, success, warn, error, step, dim, bold, cyan,
116
+ spinner, prompt, choose,
117
+ };
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-egregore",
3
+ "version": "0.1.0",
4
+ "description": "Set up Egregore for your team in one command",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "create-egregore": "./bin/cli.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/"
15
+ ],
16
+ "keywords": [
17
+ "egregore",
18
+ "claude",
19
+ "ai",
20
+ "collaboration"
21
+ ]
22
+ }