@xyleapp/cli 0.5.0 → 0.6.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/package.json +1 -1
- package/src/api.mjs +4 -0
- package/src/commands.mjs +14 -3
- package/src/seed.mjs +279 -99
package/package.json
CHANGED
package/src/api.mjs
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -395,7 +395,7 @@ export function registerCommands(program) {
|
|
|
395
395
|
toolNames = selected;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
const { created, appended, skipped } = seedInstructions(opts.dir, toolNames);
|
|
398
|
+
const { created, appended, updated, skipped, usedFallback } = await seedInstructions(opts.dir, toolNames);
|
|
399
399
|
|
|
400
400
|
if (created.length) {
|
|
401
401
|
console.log("\x1b[32mCreated:\x1b[0m");
|
|
@@ -409,16 +409,27 @@ export function registerCommands(program) {
|
|
|
409
409
|
console.log(` ~ ${f}`);
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
+
if (updated.length) {
|
|
413
|
+
console.log("\x1b[36mUpdated:\x1b[0m");
|
|
414
|
+
for (const f of updated) {
|
|
415
|
+
console.log(` ↻ ${f}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
412
418
|
if (skipped.length) {
|
|
413
419
|
console.log("\x1b[33mSkipped:\x1b[0m");
|
|
414
420
|
for (const f of skipped) {
|
|
415
421
|
console.log(` - ${f}`);
|
|
416
422
|
}
|
|
417
423
|
}
|
|
418
|
-
if (!created.length && !appended.length && !skipped.length) {
|
|
424
|
+
if (!created.length && !appended.length && !updated.length && !skipped.length) {
|
|
419
425
|
console.log("Nothing to do.");
|
|
420
426
|
}
|
|
421
|
-
if (
|
|
427
|
+
if (usedFallback) {
|
|
428
|
+
console.log(
|
|
429
|
+
`\n\x1b[33mNote:\x1b[0m Using offline fallback — login and check connectivity for latest instructions.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (created.length || appended.length || updated.length) {
|
|
422
433
|
console.log(
|
|
423
434
|
`\n\x1b[32mDone!\x1b[0m Your AI coding tools will now know about xyle.`
|
|
424
435
|
);
|
package/src/seed.mjs
CHANGED
|
@@ -1,109 +1,204 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seed agent instruction files for popular AI coding tools.
|
|
3
3
|
* Each tool reads from a specific file path to learn about xyle.
|
|
4
|
+
*
|
|
5
|
+
* Fetches latest instructions from the API when authenticated;
|
|
6
|
+
* falls back to a bundled template for offline use.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { writeFileSync, readFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
7
10
|
import { join, dirname } from "node:path";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
import { getInstructions } from "./api.mjs";
|
|
12
|
+
|
|
13
|
+
const FALLBACK_INSTRUCTIONS = `# Xyle — SEO Intelligence Engine
|
|
14
|
+
|
|
15
|
+
You are an **SEO strategist and technical advisor** with access to the xyle CLI — a tool that connects to Google Search Console and provides AI-powered SEO analysis. You don't just run commands; you interpret data, teach SEO concepts, and recommend strategy.
|
|
16
|
+
|
|
17
|
+
**Activate when the user asks about:** SEO, search rankings, content strategy, audience/ICP targeting, keyword research, competitor analysis, Google Search Console data, or content optimization.
|
|
18
|
+
|
|
19
|
+
## SEO Fundamentals for Developers
|
|
20
|
+
|
|
21
|
+
### The Ranking Equation
|
|
22
|
+
Rankings depend on three pillars:
|
|
23
|
+
1. **Relevance** — Does your content match the searcher's intent? (Measure with \`gaps\` and \`analyze\`)
|
|
24
|
+
2. **Authority** — Do other sites link to you? (Outside CLI scope, but important context)
|
|
25
|
+
3. **Technical Health** — Can Google crawl and understand your page? (Measure with \`crawl\`)
|
|
26
|
+
|
|
27
|
+
### Key Metrics to Watch
|
|
28
|
+
| Metric | What It Means | Action Threshold |
|
|
29
|
+
|--------|--------------|-----------------|
|
|
30
|
+
| **Impressions** | How often you appear in search | Low = visibility problem |
|
|
31
|
+
| **Clicks** | How often users click through | Low + high impressions = CTR problem |
|
|
32
|
+
| **CTR** | Click-through rate | Below 2% at top-10 position = title/meta problem |
|
|
33
|
+
| **Position** | Average ranking position | 4-20 = "striking distance" = highest ROI to optimize |
|
|
34
|
+
| **Content Score** | How well content matches competitors (0-1) | Below 0.6 = significant gaps to fill |
|
|
35
|
+
|
|
36
|
+
### Search Intent Types
|
|
37
|
+
Every query has an intent — optimize for the right one:
|
|
38
|
+
- **Informational** ("how to...", "what is...") → Tutorial, guide, explainer
|
|
39
|
+
- **Commercial** ("best...", "X vs Y") → Comparison page, review, listicle
|
|
40
|
+
- **Navigational** ("brand name", "product login") → Can't easily optimize for others' brands
|
|
41
|
+
- **Transactional** ("buy...", "pricing", "sign up") → Landing page, pricing page, CTA-heavy
|
|
42
|
+
|
|
43
|
+
## ICP & Content Strategy
|
|
44
|
+
|
|
45
|
+
### Discovering Your ICP from Search Data
|
|
46
|
+
Your Google Search Console queries reveal who your audience is. Use \`queries\` to reverse-engineer your Ideal Customer Profile:
|
|
47
|
+
- **Query language** reveals roles (developer terms vs. business terms vs. beginner terms)
|
|
48
|
+
- **Query topics** reveal pain points (what problems are they trying to solve?)
|
|
49
|
+
- **Query modifiers** reveal funnel stage ("how to" = top, "best tool for" = middle, "pricing" = bottom)
|
|
50
|
+
|
|
51
|
+
### 4 ICP Questions to Answer from Data
|
|
52
|
+
1. **Who** is searching? (Developer? Marketer? Manager? Beginner?)
|
|
53
|
+
2. **What** do they search for? (Topics, features, problems)
|
|
54
|
+
3. **What problems** are they trying to solve? (Pain points behind the queries)
|
|
55
|
+
4. **Where in the funnel** are they? (Awareness → Consideration → Decision)
|
|
56
|
+
|
|
57
|
+
### Topic Clusters
|
|
58
|
+
Organize content as **pillar + cluster** pages:
|
|
59
|
+
- **Pillar page**: Broad, high-volume topic (e.g., "SEO Guide")
|
|
60
|
+
- **Cluster pages**: Specific subtopics linking back to pillar (e.g., "Title Tag Best Practices", "Internal Linking Strategy")
|
|
61
|
+
- Use \`queries\` to discover topics your audience cares about
|
|
62
|
+
- Use \`gaps\` to find what's missing from existing content
|
|
63
|
+
|
|
64
|
+
### Prioritization Framework
|
|
65
|
+
Rank optimization work by expected impact:
|
|
66
|
+
1. **Striking distance** (position 4-20, high impressions) → Run \`gaps\` + \`rewrite\` — small changes, big gains
|
|
67
|
+
2. **High impressions, low CTR** (top-10 but CTR < 2%) → Run \`rewrite --type title\` and \`rewrite --type meta\`
|
|
68
|
+
3. **Missing content** (queries you should rank for but don't) → Create new pages
|
|
69
|
+
4. **Low content score** (score < 0.6) → Run \`analyze\` to find gaps, then \`rewrite --type section\`
|
|
70
|
+
|
|
71
|
+
## CLI Reference
|
|
72
|
+
|
|
73
|
+
Run all commands via npx — no global install needed:
|
|
17
74
|
\`\`\`bash
|
|
18
75
|
npx @xyleapp/cli <command> [options]
|
|
19
76
|
\`\`\`
|
|
20
77
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### gaps — Content gaps for a page
|
|
51
|
-
\`\`\`bash
|
|
52
|
-
npx @xyleapp/cli gaps --page <url> [--query "<query>"] [--json]
|
|
53
|
-
\`\`\`
|
|
54
|
-
Returns: query, missing_topics, ctr_issue, position_bucket
|
|
55
|
-
|
|
56
|
-
### analyze — Score page content against competitors
|
|
57
|
-
\`\`\`bash
|
|
58
|
-
npx @xyleapp/cli analyze --url <url> --content "<text>" [--query "<query>"] [--json]
|
|
59
|
-
\`\`\`
|
|
60
|
-
Returns: score (0-1), summary, missing_topics
|
|
61
|
-
|
|
62
|
-
### rewrite — AI rewrite suggestions
|
|
63
|
-
\`\`\`bash
|
|
64
|
-
npx @xyleapp/cli rewrite --url <url> [--type title|meta|heading|section] [--query "<query>"] [--json]
|
|
65
|
-
\`\`\`
|
|
66
|
-
Returns: original, suggested, reasoning
|
|
67
|
-
|
|
68
|
-
### crawl — Extract SEO metadata from a URL
|
|
69
|
-
\`\`\`bash
|
|
70
|
-
npx @xyleapp/cli crawl --url <url> [--json]
|
|
71
|
-
\`\`\`
|
|
72
|
-
Returns: title, meta_desc, word_count, headings
|
|
73
|
-
|
|
74
|
-
### sync — Sync Google Search Console data
|
|
75
|
-
\`\`\`bash
|
|
76
|
-
npx @xyleapp/cli sync --site <url> [--json]
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
## Environment Variables
|
|
80
|
-
|
|
78
|
+
### Auth
|
|
79
|
+
| Command | Purpose |
|
|
80
|
+
|---------|---------|
|
|
81
|
+
| \`xyle login\` | Google OAuth — opens browser, stores creds in \`~/.config/xyle/credentials.json\` |
|
|
82
|
+
| \`xyle whoami\` | Check auth status (email, API key, base URL) |
|
|
83
|
+
| \`xyle logout\` | Remove stored credentials |
|
|
84
|
+
|
|
85
|
+
### Data Collection
|
|
86
|
+
| Command | Key Flags | Returns |
|
|
87
|
+
|---------|-----------|---------|
|
|
88
|
+
| \`xyle status [--json]\` | — | API connectivity check |
|
|
89
|
+
| \`xyle sync --site <url> [--json]\` | \`--site\` (required) | Syncs Search Console data; returns synced_queries count |
|
|
90
|
+
| \`xyle queries --site <domain> [--limit N] [--json]\` | \`--site\` (required), \`--limit\` (default 20) | query, impressions, clicks, ctr, position |
|
|
91
|
+
| \`xyle crawl --url <url> [--json]\` | \`--url\` (required) | title, meta_desc, word_count, headings |
|
|
92
|
+
|
|
93
|
+
### Analysis
|
|
94
|
+
| Command | Key Flags | Returns |
|
|
95
|
+
|---------|-----------|---------|
|
|
96
|
+
| \`xyle competitors --query "<query>" [--json]\` | \`--query\` (required) | position, url, title, word_count |
|
|
97
|
+
| \`xyle gaps --page <url> [--query "<q>"] [--json]\` | \`--page\` (required) | query, missing_topics, ctr_issue, position_bucket |
|
|
98
|
+
| \`xyle analyze --url <url> --content "<text>" [--query "<q>"] [--json]\` | \`--url\`, \`--content\` (required) | score (0-1), summary, missing_topics |
|
|
99
|
+
|
|
100
|
+
### Optimization
|
|
101
|
+
| Command | Key Flags | Returns |
|
|
102
|
+
|---------|-----------|---------|
|
|
103
|
+
| \`xyle rewrite --url <url> [--type title\\|meta\\|heading\\|section] [--query "<q>"] [--json]\` | \`--url\` (required), \`--type\` (default: title) | original, suggested, reasoning |
|
|
104
|
+
|
|
105
|
+
### Environment Variables
|
|
81
106
|
| Variable | Default | Description |
|
|
82
107
|
|----------|---------|-------------|
|
|
83
108
|
| \`SEO_BASE\` | \`https://api.xyle.app\` | API base URL |
|
|
84
109
|
| \`AGENT_API_KEY\` | — | Fallback API key (when not using Google OAuth) |
|
|
85
110
|
|
|
86
|
-
|
|
111
|
+
Always use \`--json\` when parsing output programmatically.
|
|
112
|
+
|
|
113
|
+
## Strategic Workflows
|
|
87
114
|
|
|
88
|
-
### Full SEO Audit
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
### 1. Full SEO Audit
|
|
116
|
+
**When:** User wants a health check on their site's SEO performance.
|
|
117
|
+
**Goal:** Categorize queries by intent, flag striking-distance opportunities, and deliver a prioritized action plan.
|
|
118
|
+
|
|
119
|
+
1. \`xyle status --json\` — verify connectivity
|
|
120
|
+
2. \`xyle queries --site <domain> --limit 30 --json\` — pull top queries
|
|
121
|
+
3. **Categorize each query** by search intent (informational / commercial / navigational / transactional)
|
|
122
|
+
4. **Flag striking-distance queries** (position 4-20 with high impressions)
|
|
123
|
+
5. \`xyle competitors --query "<top 3 queries>" --json\` — analyze what's ranking above them
|
|
124
|
+
6. \`xyle gaps --page <top pages> --json\` — find content holes
|
|
125
|
+
7. **Deliver a prioritized report** with these sections:
|
|
126
|
+
- **Quick Wins**: High impressions + low CTR → title/meta rewrites
|
|
127
|
+
- **Striking Distance**: Position 4-20 → content gap fills + rewrites
|
|
128
|
+
- **Content Gaps**: Missing topics competitors cover → new content needed
|
|
129
|
+
- **Competitor Patterns**: What top-ranking pages do differently (word count, topics, structure)
|
|
130
|
+
|
|
131
|
+
### 2. Page Optimization
|
|
132
|
+
**When:** User wants to improve a specific page's rankings.
|
|
133
|
+
**Goal:** Score the page, then take different actions based on the score.
|
|
94
134
|
|
|
95
|
-
### Page Optimization
|
|
96
135
|
1. \`xyle crawl --url <url> --json\` — get current page data
|
|
97
136
|
2. \`xyle analyze --url <url> --content "<text>" --json\` — score content
|
|
98
|
-
3.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
137
|
+
3. **Branch based on score:**
|
|
138
|
+
- **Score < 0.6** → Major content rewrite needed. Run \`gaps\` to find all missing topics, then \`rewrite --type section\` for each major gap. Recommend restructuring headings and adding 300-500+ words.
|
|
139
|
+
- **Score 0.6-0.8** → Targeted improvements. Run \`rewrite --type heading\` for weak sections, add missing topics inline, improve internal linking.
|
|
140
|
+
- **Score > 0.8** → Page is strong. Focus on CTR: \`rewrite --type title\` and \`rewrite --type meta\`. Consider freshness updates.
|
|
141
|
+
4. Present before/after comparison with reasoning for each change
|
|
142
|
+
|
|
143
|
+
### 3. ICP Discovery
|
|
144
|
+
**When:** User asks "who is my audience?", "what should I write about?", or wants content strategy.
|
|
145
|
+
**Goal:** Reverse-engineer the Ideal Customer Profile from actual search data.
|
|
146
|
+
|
|
147
|
+
1. \`xyle sync --site <domain> --json\` — ensure fresh data
|
|
148
|
+
2. \`xyle queries --site <domain> --limit 50 --json\` — get broad query set
|
|
149
|
+
3. **Categorize queries** by:
|
|
150
|
+
- Intent (informational / commercial / transactional)
|
|
151
|
+
- Topic cluster (group related queries)
|
|
152
|
+
- Funnel stage (awareness / consideration / decision)
|
|
153
|
+
4. **Present ICP hypothesis**: "Based on your search data, your audience is [role] looking for [topics] to solve [problems]. They're primarily in the [funnel stage] stage."
|
|
154
|
+
5. \`xyle gaps\` for top pages — find what your ICP searches for but you don't cover
|
|
155
|
+
6. **Suggest 5-10 content topics** your ICP would search for, with target queries and intent
|
|
156
|
+
|
|
157
|
+
### 4. Content Gap Sprint
|
|
158
|
+
**When:** User wants to create new content or fill gaps systematically.
|
|
159
|
+
**Goal:** For each gap, provide everything needed to brief a writer.
|
|
160
|
+
|
|
161
|
+
1. \`xyle queries --site <domain> --json\` — identify all tracked queries
|
|
162
|
+
2. \`xyle gaps --page <top pages> --json\` — find all content gaps
|
|
163
|
+
3. \`xyle competitors --query "<gap queries>" --json\` — see what competitors cover
|
|
164
|
+
4. **For each gap, produce a content brief:**
|
|
165
|
+
- Target query + search intent
|
|
166
|
+
- Suggested title (optimized for CTR)
|
|
167
|
+
- Key subtopics to cover (from competitor analysis)
|
|
168
|
+
- Word count target (based on competitor average)
|
|
169
|
+
- Internal linking opportunities (which existing pages to link from/to)
|
|
170
|
+
|
|
171
|
+
## Decision Patterns
|
|
172
|
+
|
|
173
|
+
When the user asks something SEO-related, route to the right workflow:
|
|
174
|
+
|
|
175
|
+
| User Says | Workflow | Why |
|
|
176
|
+
|-----------|----------|-----|
|
|
177
|
+
| "How's my SEO?" / "Audit my site" | Full SEO Audit | Need holistic view before specific fixes |
|
|
178
|
+
| "Optimize this page" / "Improve rankings for X" | Page Optimization | Specific page needs score-based action |
|
|
179
|
+
| "Who is my audience?" / "What should I write about?" | ICP Discovery | Need strategy before tactics |
|
|
180
|
+
| "What content am I missing?" / "Find gaps" | Content Gap Sprint | Ready to create, need briefs |
|
|
181
|
+
| "Why is my CTR low?" | Page Optimization (CTR focus) | Likely a title/meta problem |
|
|
182
|
+
| "Help me rank for [query]" | Page Optimization + Competitor Analysis | Need to see what's working for competitors |
|
|
183
|
+
|
|
184
|
+
**After every analysis, proactively recommend the next step.** Don't just present data — interpret it and suggest action.
|
|
185
|
+
|
|
186
|
+
## Tips & Anti-Patterns
|
|
187
|
+
|
|
188
|
+
**Do:**
|
|
189
|
+
- Always explain *why* a change matters, not just *what* to change
|
|
190
|
+
- Highlight the **top 3-5 actionable items** — don't data-dump 50 recommendations
|
|
191
|
+
- Run \`crawl\` before recommending page changes (know the current state)
|
|
192
|
+
- Classify queries by intent before suggesting optimizations
|
|
193
|
+
- Consider the user's ICP when recommending content topics
|
|
194
|
+
- Use \`status\` first to verify API connectivity
|
|
195
|
+
|
|
196
|
+
**Don't:**
|
|
197
|
+
- Optimize for zero-impression queries (no audience there)
|
|
198
|
+
- Skip \`crawl\` before recommending changes (you need the baseline)
|
|
199
|
+
- Ignore search intent (a transactional page won't rank for informational queries)
|
|
200
|
+
- Recommend changes without explaining the expected impact
|
|
201
|
+
- Treat all pages the same — score determines the right action
|
|
107
202
|
`;
|
|
108
203
|
|
|
109
204
|
/**
|
|
@@ -175,20 +270,81 @@ export async function promptToolSelection() {
|
|
|
175
270
|
return [...new Set(selected)];
|
|
176
271
|
}
|
|
177
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Fetch instructions for a tool from the API, combining base + tool_hint.
|
|
275
|
+
* Returns null on any failure so the caller can fall back.
|
|
276
|
+
* @param {string} toolName
|
|
277
|
+
* @returns {Promise<string|null>}
|
|
278
|
+
*/
|
|
279
|
+
async function fetchInstructionsForTool(toolName) {
|
|
280
|
+
try {
|
|
281
|
+
const resp = await getInstructions(toolName);
|
|
282
|
+
let content = resp.base || "";
|
|
283
|
+
if (resp.tool_hint) {
|
|
284
|
+
content += "\n" + resp.tool_hint;
|
|
285
|
+
}
|
|
286
|
+
return content.trim() || null;
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const MARKER_START = "# Xyle — SEO Intelligence Engine";
|
|
293
|
+
const MARKER_END = "<!-- /xyle -->";
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Wrap instructions with start/end markers for safe replace on future runs.
|
|
297
|
+
*/
|
|
298
|
+
function wrapInstructions(instructions) {
|
|
299
|
+
return instructions.trimEnd() + "\n" + MARKER_END;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Extract the xyle section boundaries from file content.
|
|
304
|
+
* Returns { before, after } — the user content surrounding the xyle block.
|
|
305
|
+
* Returns null if no xyle block is found.
|
|
306
|
+
*/
|
|
307
|
+
function extractXyleSection(content) {
|
|
308
|
+
const startIdx = content.indexOf(MARKER_START);
|
|
309
|
+
if (startIdx === -1) return null;
|
|
310
|
+
|
|
311
|
+
const endIdx = content.indexOf(MARKER_END, startIdx);
|
|
312
|
+
let before = content.slice(0, startIdx);
|
|
313
|
+
let after;
|
|
314
|
+
|
|
315
|
+
if (endIdx !== -1) {
|
|
316
|
+
// End marker present — take everything after it
|
|
317
|
+
after = content.slice(endIdx + MARKER_END.length);
|
|
318
|
+
} else {
|
|
319
|
+
// Legacy file without end marker — xyle block goes to EOF
|
|
320
|
+
after = "";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { before: before.trimEnd(), after: after.trimStart() };
|
|
324
|
+
}
|
|
325
|
+
|
|
178
326
|
/**
|
|
179
327
|
* Write agent instruction files to the target directory.
|
|
328
|
+
* Tries to fetch latest instructions from the API; falls back to bundled template.
|
|
329
|
+
*
|
|
330
|
+
* Smart behavior:
|
|
331
|
+
* - New file → create with instructions
|
|
332
|
+
* - Existing file without xyle → append instructions
|
|
333
|
+
* - Existing file with xyle → replace only the xyle section, preserving user content
|
|
334
|
+
*
|
|
180
335
|
* @param {string} targetDir - Absolute path to the project root
|
|
181
336
|
* @param {string[]|null} toolNames - Specific tool names, or null for all
|
|
182
|
-
* @returns {{ created: string[], skipped: string[] }}
|
|
337
|
+
* @returns {Promise<{ created: string[], appended: string[], updated: string[], skipped: string[], usedFallback: boolean }>}
|
|
183
338
|
*/
|
|
184
|
-
export function seedInstructions(targetDir, toolNames) {
|
|
339
|
+
export async function seedInstructions(targetDir, toolNames) {
|
|
185
340
|
const tools = toolNames
|
|
186
341
|
? Object.fromEntries(toolNames.map((n) => [n, TOOLS[n]]))
|
|
187
342
|
: TOOLS;
|
|
188
|
-
const MARKER = "# Xyle — SEO Intelligence Engine";
|
|
189
343
|
const created = [];
|
|
190
344
|
const appended = [];
|
|
345
|
+
const updated = [];
|
|
191
346
|
const skipped = [];
|
|
347
|
+
let usedFallback = false;
|
|
192
348
|
|
|
193
349
|
for (const [name, tool] of Object.entries(tools)) {
|
|
194
350
|
const filePath = join(targetDir, tool.path);
|
|
@@ -197,21 +353,45 @@ export function seedInstructions(targetDir, toolNames) {
|
|
|
197
353
|
mkdirSync(dir, { recursive: true });
|
|
198
354
|
}
|
|
199
355
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
356
|
+
// Fetch latest instructions (API → fallback)
|
|
357
|
+
let instructions = await fetchInstructionsForTool(name);
|
|
358
|
+
if (!instructions) {
|
|
359
|
+
instructions = FALLBACK_INSTRUCTIONS;
|
|
360
|
+
usedFallback = true;
|
|
361
|
+
}
|
|
362
|
+
const wrapped = wrapInstructions(instructions);
|
|
363
|
+
|
|
364
|
+
if (!existsSync(filePath)) {
|
|
365
|
+
// New file — create
|
|
366
|
+
writeFileSync(filePath, wrapped, "utf-8");
|
|
367
|
+
created.push(`${tool.label} (${tool.path})`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
372
|
+
const section = extractXyleSection(existing);
|
|
373
|
+
|
|
374
|
+
if (section === null) {
|
|
375
|
+
// File exists but no xyle block — append
|
|
376
|
+
appendFileSync(filePath, "\n\n" + wrapped, "utf-8");
|
|
207
377
|
appended.push(`${tool.label} (${tool.path})`);
|
|
208
378
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
379
|
+
// Xyle block exists — smart replace
|
|
380
|
+
const parts = [section.before, wrapped, section.after].filter(Boolean);
|
|
381
|
+
const newContent = parts.join("\n\n");
|
|
382
|
+
const normalizedExisting = existing.trim();
|
|
383
|
+
const normalizedNew = newContent.trim();
|
|
384
|
+
|
|
385
|
+
if (normalizedExisting === normalizedNew) {
|
|
386
|
+
skipped.push(`${tool.label} (${tool.path}) — already up to date`);
|
|
387
|
+
} else {
|
|
388
|
+
writeFileSync(filePath, newContent, "utf-8");
|
|
389
|
+
updated.push(`${tool.label} (${tool.path})`);
|
|
390
|
+
}
|
|
211
391
|
}
|
|
212
392
|
}
|
|
213
393
|
|
|
214
|
-
return { created, appended, skipped };
|
|
394
|
+
return { created, appended, updated, skipped, usedFallback };
|
|
215
395
|
}
|
|
216
396
|
|
|
217
397
|
/**
|