@xyleapp/cli 0.2.0 → 0.4.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/README.md CHANGED
@@ -60,7 +60,7 @@ All data commands accept `--json` for machine-readable output.
60
60
 
61
61
  | Environment Variable | Default | Description |
62
62
  | -------------------- | ------------------------ | -------------------- |
63
- | `SEO_BASE` | `http://localhost:8765` | API base URL |
63
+ | `SEO_BASE` | `https://api.xyle.app` | API base URL |
64
64
  | `AGENT_API_KEY` | `local-agent-secret-key` | Fallback API key |
65
65
 
66
66
  Credentials are stored in `~/.config/xyle/credentials.json` (shared with the Python CLI).
package/bin/xyle.mjs CHANGED
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name("xyle")
10
10
  .description("SEO Intelligence Engine CLI")
11
- .version("0.2.0");
11
+ .version("0.4.0");
12
12
 
13
13
  registerCommands(program);
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyleapp/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for the Xyle SEO Intelligence Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -3,9 +3,9 @@
3
3
  * Uses native fetch (Node 18+). Zero external dependencies.
4
4
  */
5
5
 
6
- import { getApiKey } from "./auth.mjs";
6
+ import { getApiKey, getCredentials } from "./auth.mjs";
7
7
 
8
- const SEO_BASE = process.env.SEO_BASE || "http://localhost:8765";
8
+ const SEO_BASE = process.env.SEO_BASE || "https://api.xyle.app";
9
9
 
10
10
  /**
11
11
  * Build auth headers, preferring personal API key over shared env key.
@@ -96,6 +96,12 @@ export function syncGsc(site) {
96
96
  return request("POST", "/admin/sync", { params: { site } });
97
97
  }
98
98
 
99
+ export function listSites() {
100
+ const creds = getCredentials();
101
+ const email = creds?.email || null;
102
+ return request("GET", "/admin/sites", { params: { email } });
103
+ }
104
+
99
105
  /**
100
106
  * Fetch the OAuth auth URL from the API.
101
107
  */
package/src/commands.mjs CHANGED
@@ -14,10 +14,11 @@ import {
14
14
  getRewriteSuggestions,
15
15
  crawlPage,
16
16
  syncGsc,
17
+ listSites,
17
18
  SEO_BASE,
18
19
  } from "./api.mjs";
19
20
  import { getCredentials, clearCredentials, runLoginFlow } from "./auth.mjs";
20
- import { seedInstructions, getToolNames, TOOLS } from "./seed.mjs";
21
+ import { seedInstructions, getToolNames, promptToolSelection, TOOLS } from "./seed.mjs";
21
22
 
22
23
  /**
23
24
  * Print error message in red to stderr and exit.
@@ -220,12 +221,32 @@ export function registerCommands(program) {
220
221
  }
221
222
  });
222
223
 
224
+ // --- sites ---
225
+ program
226
+ .command("sites")
227
+ .description("List all connected sites for your account")
228
+ .option("--json", "Output as JSON")
229
+ .action(async (opts) => {
230
+ try {
231
+ const data = await listSites();
232
+ if (opts.json) {
233
+ console.log(printJson(data));
234
+ } else if (!data || data.length === 0) {
235
+ console.log("\x1b[33mNo sites connected. Use the dashboard or `xyle sync --site <domain>` to add one.\x1b[0m");
236
+ } else {
237
+ console.log(printTable(data, ["domain", "synced_queries", "gsc_url", "created_at"]));
238
+ }
239
+ } catch (e) {
240
+ handleError(e);
241
+ }
242
+ });
243
+
223
244
  // --- login ---
224
245
  program
225
246
  .command("login")
226
247
  .description("Authenticate with Google for Search Console access")
227
248
  .action(async () => {
228
- const seoBase = process.env.SEO_BASE || "http://localhost:8765";
249
+ const seoBase = process.env.SEO_BASE || "https://api.xyle.app";
229
250
 
230
251
  const existing = getCredentials();
231
252
  if (existing && existing.authenticated) {
@@ -292,11 +313,12 @@ export function registerCommands(program) {
292
313
  .description("Add xyle agent instructions to your project for AI coding tools")
293
314
  .option(
294
315
  "--tool <name>",
295
- `Specific tool: ${getToolNames().join(", ")} (default: all)`
316
+ `Specific tool: ${getToolNames().join(", ")}`
296
317
  )
318
+ .option("--all", "Seed all tools without prompting")
297
319
  .option("--dir <path>", "Target directory", process.cwd())
298
320
  .option("--list", "List supported tools")
299
- .action((opts) => {
321
+ .action(async (opts) => {
300
322
  if (opts.list) {
301
323
  console.log("Supported tools:\n");
302
324
  for (const [name, tool] of Object.entries(TOOLS)) {
@@ -312,7 +334,22 @@ export function registerCommands(program) {
312
334
  process.exit(1);
313
335
  }
314
336
 
315
- const { created, skipped } = seedInstructions(opts.dir, opts.tool || null);
337
+ let toolNames;
338
+ if (opts.tool) {
339
+ toolNames = [opts.tool];
340
+ } else if (opts.all) {
341
+ toolNames = null; // all
342
+ } else {
343
+ // Interactive selection
344
+ const selected = await promptToolSelection();
345
+ if (!selected.length) {
346
+ console.log("No tools selected.");
347
+ return;
348
+ }
349
+ toolNames = selected;
350
+ }
351
+
352
+ const { created, skipped } = seedInstructions(opts.dir, toolNames);
316
353
 
317
354
  if (created.length) {
318
355
  console.log("\x1b[32mCreated:\x1b[0m");
package/src/seed.mjs CHANGED
@@ -144,14 +144,47 @@ export const TOOLS = {
144
144
  },
145
145
  };
146
146
 
147
+ /**
148
+ * Prompt user to select tools interactively.
149
+ * @returns {Promise<string[]>} Selected tool names
150
+ */
151
+ export async function promptToolSelection() {
152
+ const readline = await import("node:readline");
153
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
154
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
155
+
156
+ const entries = Object.entries(TOOLS);
157
+ console.log("\nSelect tools to seed (comma-separated numbers, or 'a' for all):\n");
158
+ entries.forEach(([name, tool], i) => {
159
+ console.log(` ${i + 1}) ${tool.label.padEnd(26)} ${tool.path}`);
160
+ });
161
+ console.log();
162
+
163
+ const answer = await ask("Your selection: ");
164
+ rl.close();
165
+
166
+ if (answer.trim().toLowerCase() === "a") {
167
+ return Object.keys(TOOLS);
168
+ }
169
+
170
+ const nums = answer.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
171
+ const selected = nums
172
+ .filter((n) => n >= 1 && n <= entries.length)
173
+ .map((n) => entries[n - 1][0]);
174
+
175
+ return [...new Set(selected)];
176
+ }
177
+
147
178
  /**
148
179
  * Write agent instruction files to the target directory.
149
180
  * @param {string} targetDir - Absolute path to the project root
150
- * @param {string|null} toolName - Specific tool name, or null for all
181
+ * @param {string[]|null} toolNames - Specific tool names, or null for all
151
182
  * @returns {{ created: string[], skipped: string[] }}
152
183
  */
153
- export function seedInstructions(targetDir, toolName) {
154
- const tools = toolName ? { [toolName]: TOOLS[toolName] } : TOOLS;
184
+ export function seedInstructions(targetDir, toolNames) {
185
+ const tools = toolNames
186
+ ? Object.fromEntries(toolNames.map((n) => [n, TOOLS[n]]))
187
+ : TOOLS;
155
188
  const created = [];
156
189
  const skipped = [];
157
190