@tryclean/create 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/dist/index.js +147 -66
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -42,6 +42,12 @@ async function isDockerRunning() {
42
42
  async function startDocker() {
43
43
  if (process.platform === "darwin") {
44
44
  await exec("open -a Docker");
45
+ } else if (process.platform === "win32") {
46
+ try {
47
+ await exec(`start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"`);
48
+ } catch {
49
+ await exec(`powershell -Command "Start-Process 'Docker Desktop'"`);
50
+ }
45
51
  } else if (process.platform === "linux") {
46
52
  await exec("systemctl start docker");
47
53
  } else {
@@ -112,6 +118,7 @@ async function composeCp(cwd, src, dest) {
112
118
  // src/lib/network.ts
113
119
  import { createServer } from "net";
114
120
  import { hostname } from "os";
121
+ var isWindows = process.platform === "win32";
115
122
  function checkPort(port) {
116
123
  return new Promise((resolve) => {
117
124
  const server = createServer();
@@ -124,11 +131,23 @@ function checkPort(port) {
124
131
  }
125
132
  async function getProcessOnPort(port) {
126
133
  try {
127
- const { stdout } = await exec(`lsof -ti :${port}`);
128
- const pid = stdout.trim().split("\n")[0];
129
- if (!pid) return null;
130
- const { stdout: psOut } = await exec(`ps -p ${pid} -o comm=`);
131
- return { pid, command: psOut.trim() };
134
+ if (isWindows) {
135
+ const { stdout } = await exec(`netstat -ano | findstr ":${port}" | findstr LISTENING`);
136
+ const line = stdout.trim().split("\n")[0];
137
+ if (!line) return null;
138
+ const parts = line.trim().split(/\s+/);
139
+ const pid = parts[parts.length - 1];
140
+ if (!pid || pid === "0") return null;
141
+ const { stdout: taskOut } = await exec(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`);
142
+ const match = taskOut.match(/"([^"]+)"/);
143
+ return { pid, command: match?.[1] ?? "unknown" };
144
+ } else {
145
+ const { stdout } = await exec(`lsof -ti :${port}`);
146
+ const pid = stdout.trim().split("\n")[0];
147
+ if (!pid) return null;
148
+ const { stdout: psOut } = await exec(`ps -p ${pid} -o comm=`);
149
+ return { pid, command: psOut.trim() };
150
+ }
132
151
  } catch {
133
152
  return null;
134
153
  }
@@ -142,7 +161,8 @@ async function getDockerContainerOnPort(port) {
142
161
  }
143
162
  }
144
163
  function isDockerProcess(command2) {
145
- return command2.includes("docker") || command2.includes("com.docker");
164
+ const lower = command2.toLowerCase();
165
+ return lower.includes("docker") || lower.includes("com.docker");
146
166
  }
147
167
  async function killProcessOnPort(port) {
148
168
  try {
@@ -157,14 +177,30 @@ async function killProcessOnPort(port) {
157
177
  return true;
158
178
  }
159
179
  }
160
- const { stdout } = await exec(`lsof -ti :${port}`);
161
- const pids = stdout.trim().split("\n").filter(Boolean);
162
- if (pids.length === 0) return false;
163
- for (const pid of pids) {
164
- await exec(`kill -9 ${pid}`);
180
+ if (isWindows) {
181
+ const { stdout } = await exec(`netstat -ano | findstr ":${port}" | findstr LISTENING`);
182
+ const pids = /* @__PURE__ */ new Set();
183
+ for (const line of stdout.trim().split("\n")) {
184
+ const parts = line.trim().split(/\s+/);
185
+ const pid = parts[parts.length - 1];
186
+ if (pid && pid !== "0") pids.add(pid);
187
+ }
188
+ if (pids.size === 0) return false;
189
+ for (const pid of pids) {
190
+ await exec(`taskkill /F /PID ${pid}`);
191
+ }
192
+ await new Promise((r) => setTimeout(r, 500));
193
+ return true;
194
+ } else {
195
+ const { stdout } = await exec(`lsof -ti :${port}`);
196
+ const pids = stdout.trim().split("\n").filter(Boolean);
197
+ if (pids.length === 0) return false;
198
+ for (const pid of pids) {
199
+ await exec(`kill -9 ${pid}`);
200
+ }
201
+ await new Promise((r) => setTimeout(r, 500));
202
+ return true;
165
203
  }
166
- await new Promise((r) => setTimeout(r, 500));
167
- return true;
168
204
  } catch {
169
205
  return false;
170
206
  }
@@ -187,7 +223,7 @@ async function preflight() {
187
223
  const dockerInstalled = await isDockerInstalled();
188
224
  if (!dockerInstalled) {
189
225
  s.stop("Docker not found", 2);
190
- const installCmd = process.platform === "darwin" ? "brew install --cask docker" : process.platform === "linux" ? "curl -fsSL https://get.docker.com | sh" : null;
226
+ const installCmd = process.platform === "darwin" ? "brew install --cask docker" : process.platform === "linux" ? "curl -fsSL https://get.docker.com | sh" : process.platform === "win32" ? "winget install Docker.DockerDesktop" : null;
191
227
  if (installCmd) {
192
228
  const install = await p2.confirm({
193
229
  message: `Docker is required. Install it now? (${pc.cyan(installCmd)})`,
@@ -198,8 +234,11 @@ async function preflight() {
198
234
  const { exec: exec2 } = await import("./exec-OIKPD6DO.js");
199
235
  s.start("Installing Docker");
200
236
  try {
201
- await exec2(installCmd);
237
+ await exec2(installCmd, { timeout: 3e5 });
202
238
  s.stop("Docker installed");
239
+ if (process.platform === "win32") {
240
+ p2.log.info("You may need to restart your terminal or log out/in for Docker to work.");
241
+ }
203
242
  } catch (err) {
204
243
  s.stop("Installation failed", 1);
205
244
  p2.log.error(
@@ -397,8 +436,8 @@ async function activateLicense(code) {
397
436
  body: JSON.stringify({ code })
398
437
  });
399
438
  if (!resp.ok) {
400
- const text7 = await resp.text().catch(() => "");
401
- throw new Error(`Activation failed (${resp.status}): ${text7 || resp.statusText}`);
439
+ const text8 = await resp.text().catch(() => "");
440
+ throw new Error(`Activation failed (${resp.status}): ${text8 || resp.statusText}`);
402
441
  }
403
442
  const data = await resp.json();
404
443
  return data.license_key;
@@ -1662,28 +1701,58 @@ function readEnvVar(envPath, key) {
1662
1701
  return null;
1663
1702
  }
1664
1703
  }
1704
+ function findLocalEnv() {
1705
+ const cwd = process.cwd();
1706
+ if (existsSync2(join3(cwd, ".env"))) return join3(cwd, ".env");
1707
+ if (existsSync2(join3(cwd, "clean", ".env"))) return join3(cwd, "clean", ".env");
1708
+ return null;
1709
+ }
1665
1710
  async function connect() {
1666
1711
  banner();
1667
1712
  p15.intro(pc15.bgCyan(pc15.black(" Connect AI Tools ")));
1668
- const cwd = process.cwd();
1669
- const envPath = existsSync2(join3(cwd, ".env")) ? join3(cwd, ".env") : join3(cwd, "clean", ".env");
1670
- if (!existsSync2(envPath)) {
1671
- p15.log.error(
1672
- `No Clean installation found.
1673
- Run ${pc15.cyan("npx create-clean")} first, or cd into your installation directory.`
1674
- );
1675
- process.exit(1);
1676
- }
1677
- const apiKey = readEnvVar(envPath, "CLEAN_API_KEY") ?? "<your-api-key>";
1678
- const authUrl = readEnvVar(envPath, "AUTH_URL") ?? "http://localhost:3000";
1679
- const cleanPort = readEnvVar(envPath, "CLEAN_PORT") ?? "8000";
1680
1713
  let engineUrl;
1681
- try {
1682
- const parsed = new URL(authUrl);
1683
- parsed.port = cleanPort;
1684
- engineUrl = parsed.toString().replace(/\/$/, "");
1685
- } catch {
1686
- engineUrl = `http://localhost:${cleanPort}`;
1714
+ let apiKey;
1715
+ let dashboardUrl = null;
1716
+ const envPath = findLocalEnv();
1717
+ if (envPath) {
1718
+ const localApiKey = readEnvVar(envPath, "CLEAN_API_KEY");
1719
+ const authUrl = readEnvVar(envPath, "AUTH_URL") ?? "http://localhost:3000";
1720
+ const cleanPort = readEnvVar(envPath, "CLEAN_PORT") ?? "8000";
1721
+ try {
1722
+ const parsed = new URL(authUrl);
1723
+ parsed.port = cleanPort;
1724
+ engineUrl = parsed.toString().replace(/\/$/, "");
1725
+ } catch {
1726
+ engineUrl = `http://localhost:${cleanPort}`;
1727
+ }
1728
+ apiKey = localApiKey ?? "<your-api-key>";
1729
+ dashboardUrl = authUrl;
1730
+ p15.log.info(`Found local installation \u2014 using ${pc15.bold(engineUrl)}`);
1731
+ } else {
1732
+ p15.log.info("No local installation found. Enter your Clean server details.");
1733
+ const url = await p15.text({
1734
+ message: "Clean engine URL:",
1735
+ placeholder: "https://clean.yourcompany.com:8000",
1736
+ validate(value) {
1737
+ if (!value.trim()) return "URL is required";
1738
+ try {
1739
+ new URL(value);
1740
+ } catch {
1741
+ return "Invalid URL";
1742
+ }
1743
+ }
1744
+ });
1745
+ if (isCancel(url)) cancelled();
1746
+ engineUrl = url.replace(/\/$/, "");
1747
+ const key = await p15.text({
1748
+ message: "API key:",
1749
+ placeholder: "your-api-key or clean_sk_...",
1750
+ validate(value) {
1751
+ if (!value.trim()) return "API key is required";
1752
+ }
1753
+ });
1754
+ if (isCancel(key)) cancelled();
1755
+ apiKey = key;
1687
1756
  }
1688
1757
  const sseUrl = `${engineUrl}/mcp/sse`;
1689
1758
  const claudeConfig = JSON.stringify(
@@ -1703,7 +1772,7 @@ async function connect() {
1703
1772
  p15.log.step(pc15.bold("MCP Server Connection"));
1704
1773
  p15.log.info(
1705
1774
  `${pc15.bold("Endpoint:")} ${pc15.cyan(sseUrl)}
1706
- ${pc15.bold("API Key:")} ${pc15.dim(apiKey.slice(0, 8) + "..." + apiKey.slice(-4))}`
1775
+ ${pc15.bold("API Key:")} ${pc15.dim(apiKey.length > 12 ? apiKey.slice(0, 8) + "..." + apiKey.slice(-4) : apiKey)}`
1707
1776
  );
1708
1777
  p15.note(
1709
1778
  `${pc15.bold("Claude Desktop / Claude Code")}
@@ -1718,11 +1787,13 @@ ${pc15.cyan(claudeConfig)}`,
1718
1787
  Cursor: ${pc15.dim("Settings \u2192 MCP Servers \u2192 Add")}
1719
1788
  VS Code: ${pc15.dim(".vscode/mcp.json or settings.json")}`
1720
1789
  );
1721
- p15.log.info(
1722
- `${pc15.bold("Dashboard API Keys")}
1790
+ if (dashboardUrl) {
1791
+ p15.log.info(
1792
+ `${pc15.bold("Dashboard API Keys")}
1723
1793
  Create scoped API keys with repo restrictions at:
1724
- ${pc15.cyan(authUrl)}`
1725
- );
1794
+ ${pc15.cyan(dashboardUrl)}`
1795
+ );
1796
+ }
1726
1797
  const copy = await p15.confirm({
1727
1798
  message: "Copy MCP config to clipboard?",
1728
1799
  initialValue: true
@@ -1732,6 +1803,8 @@ ${pc15.cyan(claudeConfig)}`,
1732
1803
  const { exec: exec2 } = await import("./exec-OIKPD6DO.js");
1733
1804
  if (process.platform === "darwin") {
1734
1805
  await exec2(`echo ${JSON.stringify(claudeConfig)} | pbcopy`);
1806
+ } else if (process.platform === "win32") {
1807
+ await exec2(`echo ${JSON.stringify(claudeConfig)} | clip`);
1735
1808
  } else {
1736
1809
  await exec2(`echo ${JSON.stringify(claudeConfig)} | xclip -selection clipboard`);
1737
1810
  }
@@ -1763,38 +1836,46 @@ function findInstallation() {
1763
1836
  return null;
1764
1837
  }
1765
1838
  async function smartStart() {
1766
- const installDir = findInstallation();
1767
- if (!installDir) {
1768
- await setup();
1769
- return;
1770
- }
1771
1839
  const p16 = await import("@clack/prompts");
1772
1840
  const pc16 = (await import("picocolors")).default;
1773
1841
  const { banner: banner2 } = await import("./ui-6FJJNJ7F.js");
1842
+ const installDir = findInstallation();
1774
1843
  banner2();
1775
1844
  p16.intro(pc16.bgCyan(pc16.black(" Clean ")));
1776
- p16.log.info(`Existing installation found in ${pc16.bold(installDir)}`);
1777
- const action = await p16.select({
1778
- message: "What would you like to do?",
1779
- options: [
1780
- { value: "status", label: "Check status", hint: "service health" },
1781
- { value: "update", label: "Update", hint: "pull latest images + restart" },
1782
- { value: "connect", label: "Connect AI tools", hint: "MCP setup for Claude, Cursor, etc." },
1783
- { value: "logs", label: "View logs", hint: "tail docker compose logs" },
1784
- { value: "backup", label: "Backup data", hint: "postgres + vector data" },
1785
- { value: "setup", label: "Re-run setup wizard", hint: "fresh install" }
1786
- ]
1787
- });
1788
- if (p16.isCancel(action)) {
1789
- p16.outro("Bye!");
1790
- return;
1791
- }
1792
- if (action === "setup") {
1793
- await setup();
1794
- } else if (action === "connect") {
1795
- await connect();
1845
+ if (installDir) {
1846
+ p16.log.info(`Existing installation found in ${pc16.bold(installDir)}`);
1847
+ const action = await p16.select({
1848
+ message: "What would you like to do?",
1849
+ options: [
1850
+ { value: "status", label: "Check status", hint: "service health" },
1851
+ { value: "update", label: "Update", hint: "pull latest images + restart" },
1852
+ { value: "connect", label: "Connect AI tools", hint: "MCP setup for Claude, Cursor, etc." },
1853
+ { value: "logs", label: "View logs", hint: "tail docker compose logs" },
1854
+ { value: "backup", label: "Backup data", hint: "postgres + vector data" },
1855
+ { value: "setup", label: "Re-run setup wizard", hint: "fresh install" }
1856
+ ]
1857
+ });
1858
+ if (p16.isCancel(action)) {
1859
+ p16.outro("Bye!");
1860
+ return;
1861
+ }
1862
+ if (action === "setup") await setup();
1863
+ else if (action === "connect") await connect();
1864
+ else await commands[action]();
1796
1865
  } else {
1797
- await commands[action]();
1866
+ const action = await p16.select({
1867
+ message: "What would you like to do?",
1868
+ options: [
1869
+ { value: "connect", label: "Connect to Clean server", hint: "set up MCP for your AI tools" },
1870
+ { value: "setup", label: "Host Clean server", hint: "self-hosted setup wizard (needs Docker)" }
1871
+ ]
1872
+ });
1873
+ if (p16.isCancel(action)) {
1874
+ p16.outro("Bye!");
1875
+ return;
1876
+ }
1877
+ if (action === "connect") await connect();
1878
+ else await setup();
1798
1879
  }
1799
1880
  }
1800
1881
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryclean/create",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Set up Clean — semantic code search for AI agents",
5
5
  "bin": {
6
6
  "create-clean": "./dist/index.js"